├── .gitignore ├── src ├── Commands │ ├── Optimizing │ │ ├── OptimizeKeyframes.json │ │ ├── Clear Extra Blank Keyframes.jsfl │ │ ├── Usage Report.jsfl │ │ ├── Filter Finder.jsfl │ │ ├── Optimize Keyframes.jsfl │ │ ├── Optimize Graphics.jsfl │ │ └── HTML5 Canvas Size Report.jsfl │ ├── Timeline │ │ ├── AnimationEventLabel.json │ │ └── Animation Event Label.jsfl │ ├── Exporting │ │ ├── Export Frame Label Sequences.jsfl │ │ └── Export BitmapMovieClip.jsfl │ └── Utilities │ │ └── Copy Layers To Bitmap.jsfl ├── JSFLLibraries │ ├── ArrayUtils.jsfl │ ├── ObjectUtils.jsfl │ ├── JSON.jsfl │ ├── Command.jsfl │ └── XULWindow.jsfl ├── io.springroll.toolkit.mxi └── Tools │ └── HTMLCanvasPostPublish.jsfl ├── AlphaSplitter.zip ├── .travis.yml ├── package.json ├── LICENSE ├── Gruntfile.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | settings.json 2 | node_modules 3 | FlashToolkit.zxp 4 | -------------------------------------------------------------------------------- /src/Commands/Optimizing/OptimizeKeyframes.json: -------------------------------------------------------------------------------- 1 | {"moveTolerance":1,"scaleTolerance":0.01} -------------------------------------------------------------------------------- /AlphaSplitter.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpringRoll/FlashToolkit/HEAD/AlphaSplitter.zip -------------------------------------------------------------------------------- /src/Commands/Timeline/AnimationEventLabel.json: -------------------------------------------------------------------------------- 1 | { 2 | "value" : false, 3 | "_isBasic" : true 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | before_install: 5 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.3.2", 3 | "private": true, 4 | "devDependencies": { 5 | "grunt": "^0.4.5", 6 | "grunt-contrib-clean": "^0.6.0", 7 | "grunt-contrib-jshint": "^0.11.2", 8 | "grunt-exec": "^0.4.6", 9 | "grunt-simple-version": "^0.2.4" 10 | }, 11 | "scripts": { 12 | "test": "grunt test --verbose" 13 | } 14 | } -------------------------------------------------------------------------------- /src/JSFLLibraries/ArrayUtils.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | /** 4 | * Add utility methods to the Array class 5 | * @class Array.prototype 6 | */ 7 | 8 | /** 9 | * See if an array contains a value 10 | * @method contains 11 | * @param {mixed} needle The value to check for 12 | */ 13 | Array.prototype.contains = function(needle) 14 | { 15 | for(var k in this) 16 | { 17 | if (this[k] == needle) { return true; } 18 | } 19 | return false; 20 | }; 21 | 22 | }()); -------------------------------------------------------------------------------- /src/JSFLLibraries/ObjectUtils.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | /** 4 | * Add utility methods to the Object class 5 | * @class Object.prototype 6 | */ 7 | 8 | /** 9 | * See if an array contains a value 10 | * @method contains 11 | * @param {mixed} needle The value to check for 12 | */ 13 | Object.prototype.contains = function(needle) 14 | { 15 | for(var k in this) 16 | { 17 | if (this[k] == needle) { return true; } 18 | } 19 | return false; 20 | }; 21 | 22 | }()); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 SpringRoll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) 2 | { 3 | grunt.loadNpmTasks('grunt-contrib-jshint'); 4 | grunt.loadNpmTasks('grunt-contrib-clean'); 5 | grunt.loadNpmTasks('grunt-simple-version'); 6 | grunt.loadNpmTasks('grunt-exec'); 7 | 8 | var settings; 9 | 10 | if (grunt.file.exists('settings.json')) 11 | { 12 | settings = grunt.file.readJSON('settings.json'); 13 | } 14 | else 15 | { 16 | grunt.log.error("To build the FlashToolkit there must be a settings.json " + 17 | "file which contains keys 'packager' (path to ZXPSignCmd), 'keystore' " + 18 | "(path to p12 certificate) and 'storepass' (certificate password)."); 19 | } 20 | 21 | grunt.initConfig({ 22 | settings: settings || {}, 23 | output: 'FlashToolkit.zxp', 24 | jshint: { 25 | all: { 26 | src: [ 27 | 'Gruntfile.js', 28 | 'src/‘**/*.jsfl' 29 | ] 30 | } 31 | }, 32 | exec: { 33 | package: "\"<%= settings.packager %>\" " + 34 | "-sign " + 35 | "src " + 36 | "\"<%= output %>\" " + 37 | "\"<%= settings.keystore %>\" " + 38 | "<%= settings.storepass %>" 39 | 40 | }, 41 | version: { 42 | options: { 43 | "src/io.springroll.toolkit.mxi": function(content, version) 44 | { 45 | return content.replace( 46 | /version\=\".+?\"\>/, 47 | 'version="' + version + '">' 48 | ); 49 | } 50 | } 51 | }, 52 | clean: { 53 | build: ["<%= output %>"] 54 | } 55 | }); 56 | 57 | grunt.registerTask( 58 | 'default', 59 | 'Default build and package', 60 | [ 61 | 'version:current', 62 | 'clean:build', 63 | 'test', 64 | 'exec:package' 65 | ] 66 | ); 67 | 68 | grunt.registerTask( 69 | 'test', 70 | 'Test to run for continuous integration', 71 | ['jshint:all'] 72 | ); 73 | }; -------------------------------------------------------------------------------- /src/JSFLLibraries/JSON.jsfl: -------------------------------------------------------------------------------- 1 | (function(global){ 2 | 3 | /** 4 | * The JSON serialization and unserialization methods 5 | * @class JSON 6 | */ 7 | var JSON = {}; 8 | 9 | JSON.prettyPrint = false; 10 | 11 | /** 12 | * implement JSON.stringify serialization 13 | * @method stringify 14 | * @param {Object} obj The object to convert 15 | */ 16 | JSON.stringify = function(obj) 17 | { 18 | return _internalStringify(obj, 0); 19 | }; 20 | 21 | function _internalStringify(obj, depth, fromArray) 22 | { 23 | var t = typeof (obj); 24 | if (t != "object" || obj === null) 25 | { 26 | // simple data type 27 | if (t == "string") return '"'+obj+'"'; 28 | return String(obj); 29 | } 30 | else 31 | { 32 | // recurse array or object 33 | var n, v, json = [], arr = (obj && obj.constructor == Array); 34 | 35 | var joinString, bracketString, firstPropString; 36 | if(JSON.prettyPrint) 37 | { 38 | joinString = ",\n"; 39 | bracketString = "\n"; 40 | for(var i = 0; i < depth; ++i) 41 | { 42 | joinString += "\t"; 43 | bracketString += "\t"; 44 | } 45 | joinString += "\t";//one extra for the properties of this object 46 | firstPropString = bracketString + "\t"; 47 | } 48 | else 49 | { 50 | joinString = ","; 51 | firstPropString = bracketString = ""; 52 | } 53 | for (n in obj) 54 | { 55 | v = obj[n]; t = typeof(v); 56 | 57 | // Ignore functions 58 | if (t == "function") continue; 59 | 60 | if (t == "string") v = '"'+v+'"'; 61 | else if (t == "object" && v !== null) v = _internalStringify(v, depth + 1, arr); 62 | 63 | json.push((arr ? "" : '"' + n + '":') + String(v)); 64 | } 65 | return (fromArray || depth === 0 ? "" : bracketString)+ (arr ? "[" : "{") + firstPropString + json.join(joinString) + bracketString + (arr ? "]" : "}"); 66 | } 67 | } 68 | 69 | /** 70 | * Implement JSON.parse de-serialization 71 | * @method parse 72 | * @param {String} str The string to de-serialize 73 | */ 74 | JSON.parse = function(str) 75 | { 76 | if (str === "") str = '""'; 77 | eval("var p=" + str + ";"); // jshint ignore:line 78 | return p; 79 | }; 80 | 81 | // Assign to global space 82 | global.JSON = JSON; 83 | 84 | }(window)); -------------------------------------------------------------------------------- /src/io.springroll.toolkit.mxi: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Commands/Exporting/Export Frame Label Sequences.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | // Include the command library 4 | var dir = fl.scriptURI.split("/"); 5 | dir = dir.slice(0,dir.length - 3).join("/"); 6 | fl.runScript(dir + "/JSFLLibraries/Command.jsfl"); 7 | 8 | /** 9 | * Export a Bitmap sequence for all frame labels on the 10 | * current timeline. 11 | * @class FrameSequence 12 | */ 13 | var FrameSequence = function() 14 | { 15 | // Check for document 16 | if (!this.getDOM()) return; 17 | 18 | /** 19 | * The current timeline 20 | * @property {Timeline} timeline 21 | */ 22 | this.timeline = null; 23 | 24 | /** 25 | * The animation labels 26 | * @property {array} animationLabels 27 | */ 28 | this.animationLabels = []; 29 | 30 | var folderURI = fl.browseForFolderURL("Select a folder to export to"); 31 | 32 | if(!folderURI) return; 33 | 34 | folderURI += "/"; 35 | 36 | fl.outputPanel.clear(); 37 | 38 | this.timeline = this.dom.getTimeline(); 39 | 40 | var layers = this.timeline.layers, 41 | j = 0, 42 | k = 0, 43 | layer, 44 | frame; 45 | 46 | // Remove any stop or loop frames 47 | for (j=0; j < layers.length; j++) 48 | { 49 | layer = layers[j]; 50 | if (layer.layerType != "guided" && layer.layerType != "folder") 51 | { 52 | for (k=0; k < layer.frames.length;) 53 | { 54 | frame = layer.frames[k]; 55 | if (frame.name.search(/ stop$| loop$/) > -1) 56 | { 57 | this.timeline.clearKeyframes(frame.startFrame); 58 | } 59 | k += frame.duration;//increment k to get to the next frame 60 | } 61 | } 62 | } 63 | 64 | // Get all of the frame labels 65 | for (j=0; j < layers.length; j++) 66 | { 67 | layer = layers[j]; 68 | if (layer.layerType != "guided" && layer.layerType != "folder") 69 | { 70 | for (k=0; k < layer.frames.length;) 71 | { 72 | frame = layer.frames[k]; 73 | if (frame.name && frame.name !== "") 74 | { 75 | this.animationLabels.push(frame); 76 | } 77 | //increment k to get to the next frame 78 | k += frame.duration; 79 | } 80 | } 81 | } 82 | 83 | if (this.animationLabels.length === 0) 84 | { 85 | alert("no animations found"); 86 | return; 87 | } 88 | 89 | for (j=0; j < this.animationLabels.length; ++j) 90 | { 91 | var f = this.animationLabels[j]; 92 | 93 | // because exportInstanceToPNGSequence() doesn't 94 | // work the way we want it to, we get to 95 | // do this the old fashioned way. 96 | this.exportPNGSequence(folderURI + f.name, f.startFrame + 1, f.startFrame + f.duration); 97 | } 98 | }; 99 | 100 | var p = FrameSequence.prototype = new Command(); 101 | 102 | /** 103 | * Export a PNG sequence 104 | * @method exportPNGSequence 105 | * @param {string} fileURI_noExt The full URI without an extension 106 | * @param {int} startFrame The starting frame number 107 | * @param {int} endFrame The ending frame number 108 | */ 109 | p.exportPNGSequence = function(fileURI_noExt, startFrame, endFrame) 110 | { 111 | var frameCount = 0; 112 | for (var i = startFrame - 1; i < endFrame; ++i) 113 | { 114 | frameCount++; 115 | this.timeline.currentFrame = i; 116 | this.dom.exportPNG(fileURI_noExt + padNum(frameCount.toString()) + ".png", true, true); 117 | } 118 | }; 119 | 120 | /** 121 | * Add padding to a number 122 | * @param {int} input The input number 123 | * @return {string} The padded number 124 | */ 125 | var padNum = function(input) 126 | { 127 | while (input.length < 4) 128 | input = "0" + input; 129 | return input; 130 | }; 131 | 132 | new FrameSequence(); 133 | 134 | }()); -------------------------------------------------------------------------------- /src/JSFLLibraries/Command.jsfl: -------------------------------------------------------------------------------- 1 | /** 2 | * @module window 3 | */ 4 | (function(global){ 5 | 6 | /** 7 | * An abstract command for extending cloudkid's commands 8 | * @class Command 9 | */ 10 | var Command = function() 11 | { 12 | /** 13 | * The current document 14 | * @property {Document} dom 15 | */ 16 | this.dom = null; 17 | 18 | /** 19 | * If the DOM is an html5 canvas FLA 20 | * @property {Boolean} isCanvas 21 | */ 22 | this.isCanvas = false; 23 | }; 24 | 25 | // reference to the prototype 26 | var p = Command.prototype = {}; 27 | 28 | /** 29 | * Get the parent directory of a path or file 30 | * @method dirname 31 | * @param {String} path The path to get the parent directory for 32 | * @return {String} The parent path 33 | */ 34 | Command.dirname = function(path) 35 | { 36 | // Remove the last slash if it's there 37 | if (path.substr(-1, 1) == "/") 38 | { 39 | path = path.substr(0, path.length - 1); 40 | } 41 | 42 | // Get the parent directory 43 | var dir = path.split("/"); 44 | dir.pop(); 45 | dir = dir.join("/") + "/"; 46 | 47 | return dir; 48 | }; 49 | 50 | /** 51 | * Get the config directory, relative to this directory 52 | * this is a better version than configURI, for debugging 53 | * purposese. 54 | * @method dir 55 | * @static 56 | * @readOnly 57 | */ 58 | Command.dir = function() 59 | { 60 | return Command.dirname( 61 | Command.dirname(fl.scriptURI) 62 | ); 63 | }(); 64 | 65 | /** 66 | * Require a library file or additional script 67 | * @method require 68 | * @static 69 | * @param {Object} path The relative path to file 70 | */ 71 | Command.require = function(path) 72 | { 73 | fl.runScript(Command.dir + path); 74 | }; 75 | 76 | // Include required utilities classes 77 | Command.require("JSFLLibraries/JSON.jsfl"); 78 | Command.require("JSFLLibraries/ArrayUtils.jsfl"); 79 | Command.require("JSFLLibraries/ObjectUtils.jsfl"); 80 | 81 | /** 82 | * Get the settings from the settings file 83 | * @method loadSettings 84 | * @param {String} path The relative path to file 85 | * @return {String} The settings to grab 86 | */ 87 | p.loadSettings = function(path) 88 | { 89 | var value = FLfile.read(Command.dir + path); 90 | if (value) 91 | { 92 | value = JSON.parse(value); 93 | if (value._isBasic) 94 | { 95 | value = value.value; 96 | } 97 | } 98 | return value; 99 | }; 100 | 101 | /** 102 | * Get the settings 103 | * @method loadSettings 104 | * @param {String} path The relative path to file 105 | * @param {mixed} [value=""] The value to save 106 | * @return {Boolean} If the save was successful 107 | */ 108 | p.saveSettings = function(path, value) 109 | { 110 | value = value || ""; 111 | 112 | if (typeof value != "object") 113 | { 114 | value = { 115 | "value" : value, 116 | "_isBasic" : true 117 | }; 118 | } 119 | if (!FLfile.write(Command.dir + path, JSON.stringify(value))) 120 | { 121 | alert("Unable to save to " + path); 122 | return false; 123 | } 124 | return true; 125 | }; 126 | 127 | /** 128 | * Create a new dialog 129 | * @method dialog 130 | * @param {String} title The title of the dialog 131 | * @param {*} [button] The collection of buttons 132 | */ 133 | p.dialog = function(title) 134 | { 135 | //if (!global.XULWindow) 136 | //{ 137 | Command.require("JSFLLibraries/XULWindow.jsfl"); 138 | //} 139 | return new global.XULWindow(title, "accept", "cancel"); 140 | }; 141 | 142 | /** 143 | * Get the current document and check if it's available, 144 | * set's document 145 | * @method getDOM 146 | * @return {Document} Returns null if no document, also does alert 147 | */ 148 | p.getDOM = function() 149 | { 150 | var dom = fl.getDocumentDOM(); 151 | 152 | if (!dom) 153 | { 154 | alert("No document opened"); 155 | return; 156 | } 157 | this.dom = dom; 158 | this.isCanvas = dom.type == "htmlcanvas"; 159 | 160 | return dom; 161 | }; 162 | 163 | // Assign to the namespace 164 | global.Command = Command; 165 | 166 | }(window)); -------------------------------------------------------------------------------- /src/Commands/Timeline/Animation Event Label.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | // Include the command library 4 | var dir = fl.scriptURI.split("/"); 5 | dir = dir.slice(0,dir.length - 3).join("/"); 6 | fl.runScript(dir + "/JSFLLibraries/Command.jsfl"); 7 | 8 | /** 9 | * This script allows adding frame labels form animation events 10 | * used by the Animation library. 11 | * @class AnimationEventLabel 12 | * @extends Command 13 | */ 14 | var AnimationEventLabel = function() 15 | { 16 | if (!this.getDOM()) return; 17 | 18 | var timeline = this.dom.getTimeline(); 19 | var sel = timeline.getSelectedFrames(); 20 | 21 | if (sel === "") 22 | { 23 | alert("No frames selected!"); 24 | return; 25 | } 26 | 27 | // The collection of labels 28 | var labels = this.getLabels(timeline); 29 | 30 | // The settings file 31 | this.settingsFile = 'Commands/Utilities/AnimationEventLabel.json'; 32 | 33 | // The sections selected 34 | var sections = sel.length / 3; 35 | 36 | // Create the dialog so we can reuse it 37 | var dialog = this.createDialog(); 38 | 39 | for (var i=0; i < sections; i++) 40 | { 41 | var l = sel[i*3]; 42 | var sf = sel[i*3 + 1]; 43 | var ef = sel[i*3 + 2] - 1; 44 | 45 | // Check that there is a selection 46 | if ( ef - sf <= 0 ) 47 | { 48 | alert("Selecting a single frame is not a valid selection."); 49 | break; 50 | } 51 | 52 | // Get the settings 53 | var settings = dialog.create(); 54 | 55 | // Silently dismiss 56 | if (settings.dismiss != "accept") return; 57 | 58 | var label = settings.label; 59 | var underscore = this.isCanvas ? true : settings.underscore == "true"; 60 | 61 | // If we're not canvas save the underscore setting 62 | // so we can reset next time 63 | if (!this.isCanvas) 64 | { 65 | this.saveSettings(this.settingsFile, underscore); 66 | } 67 | 68 | var action = (underscore ? "_" : " ") + settings.action; 69 | 70 | // No label was provided 71 | if (!label) 72 | { 73 | alert("Label cannot be empty."); 74 | break; 75 | } 76 | 77 | // Already contains the labels! 78 | if (labels.contains(label) || labels.contains(label + action)) 79 | { 80 | alert("Label '"+label+"' is already taken."); 81 | break; 82 | } 83 | 84 | timeline.currentLayer = l; 85 | 86 | var start = timeline.layers[l].frames[sf]; 87 | var end = timeline.layers[l].frames[ef]; 88 | 89 | // If the start frame is not a keyframe 90 | if (!start || start.startFrame != sf) 91 | { 92 | timeline.convertToBlankKeyframes(sf); 93 | start = timeline.layers[l].frames[sf]; 94 | } 95 | 96 | // If the end frame is not a keyframe 97 | if (!end || end.startFrame != ef ) 98 | { 99 | timeline.convertToBlankKeyframes(ef); 100 | end = timeline.layers[l].frames[ef]; 101 | } 102 | 103 | // Add the start label 104 | start.labelType = "name"; 105 | start.name = label; 106 | 107 | // Add the stop label 108 | end.labelType = "name"; 109 | end.name = label + action; 110 | } 111 | }; 112 | 113 | // Extends Command 114 | var p = AnimationEventLabel.prototype = new Command(); 115 | 116 | /** 117 | * Get a collection of labels on the current timeline 118 | * @method getLabels 119 | * @param {Timeline} timeline The current timeline 120 | */ 121 | p.getLabels = function(timeline) 122 | { 123 | var labels = []; 124 | 125 | // Get all of the frame labels 126 | for (var j=0; j < timeline.layers.length; j++) 127 | { 128 | var layer = timeline.layers[j]; 129 | if (layer.layerType != "guided" && layer.layerType != "folder") 130 | { 131 | for (var k=0; k < layer.frames.length;) 132 | { 133 | var frame = layer.frames[k]; 134 | 135 | if (frame.name !== "") 136 | { 137 | labels.push(frame.name); 138 | } 139 | k += frame.duration; 140 | } 141 | } 142 | } 143 | return labels; 144 | }; 145 | 146 | /** 147 | * Create a dialog to create the frame labels 148 | * @method createDialog 149 | * @return {XULWindow} The created dialog 150 | */ 151 | p.createDialog = function() 152 | { 153 | // Ask for the name of the label 154 | var dialog = this.dialog("Animation Event Label"); 155 | 156 | var actions = [ 157 | {label:"Stop",value:"stop"}, 158 | {label:"Loop",value:"loop"}, 159 | ]; 160 | 161 | dialog.columns(2, 2) 162 | .openRow() 163 | .addLabel("Event Name", "label", 100) 164 | .addTextBox("label", "", 150) 165 | .closeRow() 166 | .openRow() 167 | .addLabel("Action Type", "action", 100) 168 | .addRadioGroup("action", actions) 169 | .closeRow(); 170 | 171 | // Only give the javascript option 172 | // if we are not the html canvas type fla 173 | if (!this.isCanvas) 174 | { 175 | var underscore = this.loadSettings(this.settingsFile); 176 | dialog.addCheckbox("Javascript Compliant", "underscore", underscore); 177 | } 178 | return dialog; 179 | }; 180 | 181 | // Run 182 | new AnimationEventLabel(); 183 | 184 | }()); -------------------------------------------------------------------------------- /src/Commands/Optimizing/Clear Extra Blank Keyframes.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | /** 4 | * Command to remove extraneous blank keyframes 5 | * @class ClearExtraBlankKeyframes 6 | */ 7 | var ClearExtraBlankKeyframes = function() 8 | { 9 | /** 10 | * The cache of items that have already been searched 11 | * @property {Array} objectsSearched 12 | */ 13 | this.objectsSearched = []; 14 | 15 | /** 16 | * The total number of empty keyframes removed 17 | * @property {int} totalRemoved 18 | */ 19 | this.totalRemoved = 0; 20 | 21 | this.initialize(); 22 | }; 23 | 24 | // Reference to the prototype 25 | var p = ClearExtraBlankKeyframes.prototype = {}; 26 | 27 | /** 28 | * Initialize 29 | * @method initialize 30 | */ 31 | p.initialize = function() 32 | { 33 | var doc = fl.getDocumentDOM(); 34 | 35 | if (!doc) 36 | { 37 | alert("No document opened"); 38 | return; 39 | } 40 | 41 | var curTimelines = doc.timelines; 42 | var i; 43 | 44 | // Search through the timelines of this document 45 | for(i = 0; i < curTimelines.length; ++i) 46 | { 47 | this.clearByTimeline(curTimelines[i]); 48 | } 49 | 50 | var libraryItems = doc.library.items; 51 | 52 | // Search through library items not put on stage 53 | for(i = 0; i < libraryItems.length; ++i) 54 | { 55 | this.searchLibraryItem(libraryItems[i]); 56 | } 57 | 58 | if (this.totalRemoved > 0) 59 | { 60 | alert("Cleared " + this.totalRemoved + " extra blank keyframe" + (this.totalRemoved > 1 ? "s" : "")); 61 | } 62 | else 63 | { 64 | alert("No extra blank keyframes cleared"); 65 | } 66 | }; 67 | 68 | /** 69 | * Clear extra empty keyframes by a timeline 70 | * @method clearByTimeline 71 | * @param {Timeline} timeline The timeline to clear frames from 72 | */ 73 | p.clearByTimeline = function(timeline) 74 | { 75 | var j = 0, 76 | k = 0, 77 | l = 0, 78 | el, 79 | num, 80 | prevNum = -1, 81 | frm, 82 | layer, 83 | lyrVisibility; 84 | 85 | // cycle through each of the layers in this timeline 86 | for(j = 0; j < timeline.layers.length; ++j) 87 | { 88 | // cycle through each of the layers in this timeline 89 | layer = timeline.layers[j]; 90 | 91 | // Ignore non-normal layers 92 | if (layer.layerType != "normal") 93 | { 94 | continue; 95 | } 96 | timeline.setSelectedLayers(j); 97 | 98 | // store the layer visibility and then make the layer visible. 99 | // Elements cannot be found on invisible layers 100 | lyrVisibility = layer.visible; 101 | layer.visible = true; 102 | 103 | // Loop through the frames 104 | for(k = 0; k < layer.frames.length; ) 105 | { 106 | // step through the frames on this layer 107 | frm = layer.frames[k]; 108 | num = frm.elements.length; 109 | 110 | // Match empty keyframes 111 | if (num === 0) 112 | { 113 | // Previous keyframe has no elements and 114 | // there's no sound on this frame 115 | if (prevNum === 0 && !frm.soundLibraryItem && !frm.name) 116 | { 117 | timeline.clearKeyframes(k); 118 | this.totalRemoved++; 119 | } 120 | } 121 | else 122 | { 123 | for (l = 0; l < num; ++l) 124 | { 125 | // then cycle through the elements on this frame 126 | el = frm.elements[l]; 127 | 128 | if (el.elementType == "instance") 129 | { 130 | // lets get the library item for this element 131 | this.searchLibraryItem(el.libraryItem); 132 | } 133 | } 134 | } 135 | prevNum = num; 136 | k += frm.duration; 137 | } 138 | prevNum = -1; 139 | 140 | // return this layer to its original visibility (optional) 141 | layer.visible = lyrVisibility; 142 | } 143 | }; 144 | 145 | /** 146 | * Search the library by item 147 | * @method searchLibraryItem 148 | * @param {LibraryItem} libItem The library item to search for 149 | */ 150 | p.searchLibraryItem = function(libItem) 151 | { 152 | var validSymbolTypes = ["graphic", "movie clip", "button"]; 153 | 154 | // Ignore invalid symbol types 155 | if (!inObject(libItem.itemType, validSymbolTypes)) 156 | { 157 | this.objectsSearched.push(libItem.name); 158 | return; 159 | } 160 | 161 | // only process new objects and not runtime shared-assets 162 | if ( !inObject(libItem.name, this.objectsSearched)) 163 | { 164 | this.objectsSearched.push(libItem.name); 165 | 166 | if (libItem.timeline) 167 | { 168 | // if there is a timeline, repeat the scan as a recursion 169 | this.clearByTimeline(libItem.timeline); 170 | } 171 | } 172 | }; 173 | 174 | /** 175 | * utility function to check if a value is in an object or array 176 | * @method inObject 177 | * @private 178 | * @static 179 | * @param {*} needle The value to check 180 | * @param {Object|Array} haystack The object to check in 181 | * @param {Boolean} If the needle is in the haystack 182 | */ 183 | var inObject = function(needle, haystack) 184 | { 185 | for(var k in haystack ) 186 | { 187 | if ( haystack[k] == needle ) { return true; } 188 | } 189 | return false; 190 | }; 191 | 192 | // Run the command 193 | new ClearExtraBlankKeyframes(); 194 | 195 | }()); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpringRoll Flash Toolkit [![Build Status](https://travis-ci.org/SpringRoll/FlashToolkit.svg)](https://travis-ci.org/SpringRoll/FlashToolkit) [![devDependency Status](https://david-dm.org/SpringRoll/FlashToolkit/dev-status.svg)](https://david-dm.org/SpringRoll/FlashToolkit#info=devDependencies) [![GitHub version](https://badge.fury.io/gh/SpringRoll%2FFlashToolkit.svg)](http://badge.fury.io/gh/SpringRoll%2FFlashToolkit) 2 | 3 | Adobe Flash Professional Commands for Exporting, Optimizing and Publish HTML5 Canvas document. While these commands are exclusive to using SpringRoll, there are a couple helper function for use with certain SpringRoll features. 4 | 5 | ## Installation 6 | 7 | * Visit the latest release [here](https://github.com/SpringRoll/FlashToolkit/releases) 8 | * Download **FlashToolkit.zxp** 9 | * CC & CC 2014: Install ZXP with the [Adobe Extension Manager CC](https://www.adobe.com/exchange/em_download/). 10 | * CC 2015: Install ZXP with the [Manage Extensions app](https://blogs.adobe.com/flashpro/installing-zxp-extensions-using-the-manage-extensions-utility/) 11 | 12 | ## Usage 13 | 14 | All the scripts are runable from the Commands menu within Flash Professional. 15 | 16 | #### Exporting/Export BitmapMovieClip 17 | 18 | Create JSON and spritesheet useable for `springroll.easeljs.BitmapMovieClip`. To use, select a MovieClip or MovieClips from the library to export then run the command. 19 | 20 | #### Exporting/Export Frame Label Sequence 21 | 22 | Export a sequence of PNGs based on the frame labels. Useful if you're creating spritesheets using TexturePacker. To use, run the command on the current timeline which contains frame labels. 23 | 24 | #### Optimizing/Clear Extra Blank Keyframe 25 | 26 | Remove all redundant clear keyframes. In earlier versions of CC and CC 2014, blank keyframes added more filesize and produced playback issues in CreateJS. To use, run the command on the current timeline. 27 | 28 | #### Optimizing/Filter Finder 29 | 30 | Generate a report of all the MovieClips within the current project which contain filters (Color transforms, Drop shadows, etc). This is useful for tracking down and remove filters which harm performance. To use, simply run the command. 31 | 32 | #### Optimizing/HTML5 Canvas Size Report 33 | 34 | Generate a report of the output size of all the assets in an HTML5 Canvas published document. To use, publish the current document and then run the command. 35 | 36 | #### Optimizing/Optimize Graphics 37 | 38 | Remove all unused graphic frame from a selected graphic symbol in the library. To run, select a graphic from the library and then run the command. 39 | 40 | #### Optimizing/Optimize Keyframe 41 | 42 | Reduce the number of keyframes by eliminating keyframes that are similar. To run, navigate to a timeline of frames you would like to remove, then run the command. There are two values "Move Tolerance" and "Scale Tolerance". If two keyframes contain a symbol within those tolerance the second keyframe is removed. 43 | 44 | #### Timeline/Animation Event Label 45 | 46 | Create animation event labels on the timeline which work with `springroll.easeljs.Animator`. To use, select a range of frames on the current timeline and run the command. This will prompt for an event name to create a start and end label for use with Animator. For instance, "idle" event name will create "idle" and "idle_stop" labels. Animator has two types of event labels, `stop` (stop at the end) and `loop` (return to the beginning and keep playing). 47 | 48 | #### Utilities/Copy Layers To Bitmap 49 | 50 | This command combines all layers on the current timeline non-destructively into a single bitmap. All the layers are guided, locked and hidden. Running the script again will undo this convert and give you the normal vector layers. 51 | 52 | #### HTML5 Canvas Post Publish 53 | 54 | This is not a command. This script automatically is invoked whenever an HTML5 Canvas FLA is published. It does two things: first, a JSON file is created of the `lib.properties` (which contains the manifest, size, framerate, etc) of the current document. Also, the output file is optimize to better work with a JavaScript minification tool (e.g., [Uglify](https://github.com/mishoo/UglifyJS2)). 55 | 56 | ## Building Toolkit 57 | 58 | ### Dependencies 59 | 60 | To rebuild the extension for Flash, you need to have [Node](http://nodejs.org) and [Grunt](http://gruntjs.com/getting-started) installed. 61 | 62 | Download and install the [Adobe Exchange Packager](https://www.adobeexchange.com/resources) (you will need to login to the Adobe Exchange to be able to download the application). 63 | 64 | ### Create Environment Settings 65 | 66 | Create a **settings.json** file within the FlashToolkit project folder with the following format: 67 | 68 | ```json 69 | { 70 | "packager": "/Applications/Adobe Exchange Packager.app/Contents/Resources/macosx/ZXPSignCmd", 71 | "keystore": "/Users/username/Documents/MySignedCertificate.p12", 72 | "storepass": "myCertPassword" 73 | } 74 | ``` 75 | You may need to make the ZXPSignCmd file executable, as it might not have the needed permissions by default. 76 | 77 | ### Building the ZXP 78 | 79 | Rebuild the Toolkit by running the default Grunt task: 80 | 81 | ```bash 82 | grunt 83 | ``` 84 | 85 | ## License 86 | 87 | Copyright (c) 2015 [CloudKid](https://github.com/cloudkidstudio) 88 | 89 | Released under the MIT License. 90 | -------------------------------------------------------------------------------- /src/Tools/HTMLCanvasPostPublish.jsfl: -------------------------------------------------------------------------------- 1 | (function(window) 2 | { 3 | fl.addEventListener("postPublish", onPostPublish); 4 | 5 | function onPostPublish() 6 | { 7 | fl.showIdleMessage(false); 8 | postPublish(); 9 | fl.showIdleMessage(true); 10 | } 11 | 12 | function postPublish() 13 | { 14 | // Get the current document 15 | var dom = fl.getDocumentDOM(); 16 | 17 | // Ignore unsaved FLAs 18 | if (!dom.pathURI) return; 19 | 20 | // Check for canvas 21 | if (dom.type != "htmlcanvas") return; 22 | 23 | // The folder URI 24 | var folder = dirname(dom.pathURI); 25 | 26 | // If the FLA is uncompressed use the parent 27 | // folder for relative publish pathing 28 | if (/\.xfl$/i.test(dom.pathURI)) 29 | { 30 | folder = dirname(folder); 31 | } 32 | 33 | // Add the trailing slash 34 | folder += "/"; 35 | 36 | // Get the file name 37 | var filename = exportFile(dom); 38 | 39 | // No filename found 40 | if (!filename) return; 41 | 42 | var file = folder + filename; 43 | 44 | // Output file doesn't exist 45 | if (!FLfile.exists(file)) return; 46 | 47 | var content = FLfile.read(file); 48 | 49 | //find the 'var lib, images, createj, ss;' line at the end of the file 50 | var varFinder = /var (\w+), (\w+), (\w+), (\w+);$/m; 51 | //also, the Flash 2014 'var lib, images, createj;' 52 | var oldVarFinder = /var (\w+), (\w+), (\w+);$/m; 53 | 54 | if(!(varFinder.exec(content) || oldVarFinder.exec(content))) 55 | { 56 | alert("File was published improperly. Please quit Flash and try publishing again."); 57 | return; 58 | } 59 | 60 | // Extract the properties 61 | var regex = new RegExp("properties = [^;]+;", "gm"); 62 | var result = regex.exec(content); 63 | if (result) 64 | { 65 | var properties; 66 | eval(String(result)); // jshint ignore:line 67 | FLfile.write(file + "on", stringify(properties)); 68 | content = content.replace(/[\w]+\.properties = [^;]+;/gm, ''); 69 | } 70 | 71 | content = classRef(content, "cjs.Shape"); 72 | content = classRef(content, "cjs.Container"); 73 | content = classRef(content, "cjs.Rectangle"); 74 | content = classRef(content, "cjs.Tween"); 75 | content = classRef(content, "cjs.Bitmap"); 76 | content = classRef(content, "cjs.MovieClip"); 77 | content = classRef(content, "cjs.Sprite"); 78 | content = classRef(content, "cjs.Ease"); 79 | 80 | var libItem = /\(([\w\d]+)\.[\w\d]+? \= function\(.*?\) \{([\s\S]+?)\}\)\.prototype \= p/gm; 81 | var libItems = content.match(libItem); 82 | 83 | if (libItems) 84 | { 85 | libItems.forEach(function(lib) 86 | { 87 | //var name = lib.substring(1, lib.indexOf(" =")); 88 | //fl.trace("Symbol " + name); 89 | var start = lib.indexOf('{') + 1; 90 | var end = lib.lastIndexOf('}'); 91 | var libContent = lib.substring(start, end); 92 | var matches = libContent.match(/this\.([\w]+?) =/g); 93 | 94 | if (matches) 95 | { 96 | // add the var block at the beginning 97 | for(var id, i = 0; i < matches.length; i++) 98 | { 99 | id = matches[i].substr(5).replace(" =", ''); 100 | libContent = "\n\tvar " + id + ";" + libContent; 101 | libContent = libContent.replace( 102 | new RegExp("this\." + id + "([\.\,\)\}\;])", "g"), 103 | id + "$1" 104 | ); 105 | } 106 | 107 | // Replace the "this.instance =" with "instance = this.instance =" 108 | libContent = libContent.replace(/this\.([\w]+?) =/g, "$1 = this.$1 ="); 109 | } 110 | 111 | // Update the content 112 | content = content.replace(lib, lib.substr(0, start) + libContent + lib.substr(end)); 113 | }); 114 | } 115 | FLfile.write(file, content); 116 | } 117 | 118 | /** 119 | * Get the director path from another path 120 | * @method dirname 121 | * @private 122 | * @param {string} path The incoming path 123 | * @return {string} The base parent folder 124 | */ 125 | function dirname(path) 126 | { 127 | return path.replace(/\\/g, '/') 128 | .replace(/\/[^\/]*\/?$/, ''); 129 | } 130 | 131 | /** 132 | * Get an export filename 133 | * @method exportFile 134 | * @return {String|null} The export filename 135 | */ 136 | function exportFile(dom) 137 | { 138 | // We need to get some information from the publish profile 139 | // this includes the path to the images, the libs namespace 140 | // and the export file name 141 | var profile = dom.exportPublishProfileString(), 142 | xml = new XML(profile), 143 | properties = xml.PublishProperties, 144 | name; 145 | 146 | for (var i = 0, len = properties.length(); i < len; i++) 147 | { 148 | name = properties[i].attribute('name'); 149 | 150 | // Ignore other PublishProperties properties other than HTML 151 | if (name != "JavaScript/HTML") continue; 152 | 153 | // Get the list of properties 154 | var props = properties[i].Property; 155 | 156 | // Find the export filename 157 | for (var j = 0, l = props.length(); j < l; j++) 158 | { 159 | name = props[j].attribute('name'); 160 | if (name == "filename") return String(props[j]); 161 | } 162 | } 163 | return null; 164 | } 165 | 166 | /** 167 | * Substitute a class reference 168 | * @method classRef 169 | * @param {String} content The javascript content 170 | * @param {String} className The filename to replace 171 | * @return {String} The updated content 172 | */ 173 | function classRef(content, className) 174 | { 175 | var regex = new RegExp(className.replace(".", "\\."), "g"); 176 | if (content.match(regex)) 177 | { 178 | var token = "var p; // shortcut to reference prototypes"; 179 | var name = className.substr(className.indexOf('.') + 1); 180 | //fl.trace("Class " + className + " replace with " + name); 181 | content = content.replace(regex, name); 182 | content = content.replace(token, token += "\nvar " + name + " = " + className + ";"); 183 | } 184 | return content; 185 | } 186 | 187 | function stringify(obj) 188 | { 189 | var t = typeof (obj); 190 | if (t != "object" || obj === null) 191 | { 192 | // simple data type 193 | if (t == "string") obj = '"' + obj + '"'; 194 | return String(obj); 195 | } 196 | else 197 | { 198 | // recurse array or object 199 | var n, v, json = [], arr = (obj && obj.constructor == Array); 200 | 201 | for (n in obj) 202 | { 203 | v = obj[n]; 204 | t = typeof(v); 205 | if (obj.hasOwnProperty(n)) 206 | { 207 | if (t == "string") 208 | v = '"' + v + '"'; 209 | else if (t == "object" && v !== null) 210 | v = stringify(v); 211 | json.push((arr ? "" : '"' + n + '":') + String(v)); 212 | } 213 | } 214 | return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}"); 215 | } 216 | } 217 | 218 | }(window)); -------------------------------------------------------------------------------- /src/Commands/Optimizing/Usage Report.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | // Include the command library 4 | var dir = fl.scriptURI.split("/"); 5 | dir = dir.slice(0,dir.length - 3).join("/"); 6 | fl.runScript(dir + "/JSFLLibraries/Command.jsfl"); 7 | 8 | /* 9 | * This script parses all of the items inside of a Flash document (fla) 10 | * and give a report of all the references. 11 | * @class UsageReport 12 | * @extends Command 13 | */ 14 | var UsageReport = function() 15 | { 16 | if (!this.getDOM()) return; 17 | 18 | /** 19 | * The objects we've checked already 20 | * @property {Array} objectsUsed 21 | */ 22 | this.objectsUsed = []; 23 | 24 | /** 25 | * The output report 26 | * @property {Array} output 27 | */ 28 | this.output = []; 29 | 30 | /** 31 | * The valid type of symbols 32 | * @property {Array} validTypes 33 | */ 34 | this.validTypes = ["movie clip","graphic","button"]; 35 | 36 | /** 37 | * The item we're searching for 38 | * @property {Item} item 39 | */ 40 | this.item = null; 41 | 42 | var curTimelines = this.dom.timelines; 43 | var libraryItems = this.dom.library.items; 44 | 45 | // Clear the output window 46 | fl.outputPanel.clear(); 47 | 48 | var items = this.dom.library.getSelectedItems(); 49 | 50 | if (items.length != 1) 51 | { 52 | alert("Select a library item to start."); 53 | return; 54 | } 55 | 56 | // Get the library item 57 | this.item = items[0]; 58 | 59 | // Search through the timelines of this document 60 | for(var i = 0; i < curTimelines.length; ++i) 61 | { 62 | this.searchInTimeline(curTimelines[i], "(Main Timeline)"); 63 | } 64 | 65 | // Search through library items not put on stage 66 | for(i = 0; i < libraryItems.length; ++i) 67 | { 68 | this.searchInItem(libraryItems[i]); 69 | } 70 | 71 | fl.trace("\nSearching for: " + this.item.name); 72 | fl.trace("Usages found: " + this.output.length + "\n\n"); 73 | 74 | this.tableLayout(["Timeline", "Layer", "Frame"], this.output); 75 | }; 76 | 77 | // Extend the command 78 | var p = UsageReport.prototype = new Command(); 79 | 80 | /** 81 | * The number of space to pad the report with 82 | * @property {int} PADDING 83 | * @final 84 | * @readOnly 85 | */ 86 | UsageReport.PADDING = 4; 87 | 88 | /** 89 | * Create a table layout 90 | * @method tableLayout 91 | * @param {Array} header The names of the headers 92 | * @param {Array} entries The list of entries 93 | */ 94 | p.tableLayout = function(headers, entries) 95 | { 96 | var finalStr = ""; 97 | var layout = []; 98 | 99 | // Set the minimum values 100 | for (i = 0; i < headers.length; ++i) 101 | { 102 | layout[i] = headers[i].length + UsageReport.PADDING; 103 | } 104 | 105 | // Measure the layout for prettier display 106 | for (i = 0; i < entries.length; ++i) 107 | { 108 | for(j = 0; j < layout.length; ++j) 109 | { 110 | layout[j] = Math.max(layout[j], String(entries[i][j]).length + UsageReport.PADDING); 111 | } 112 | } 113 | 114 | // Show the table headers 115 | var res = ""; 116 | var total = 0; 117 | for (j = 0; j < layout.length; ++j) 118 | { 119 | res += this.fontLayout(headers[j], layout[j]); 120 | total += layout[j]; 121 | } 122 | finalStr += res + "\n"; 123 | 124 | // Place a divider 125 | var hr = ""; 126 | for (i = 0; i < total; ++i) 127 | { 128 | hr += "-"; 129 | } 130 | 131 | finalStr += hr + "\n"; 132 | 133 | // Prep the entries 134 | for (i = 0; i < entries.length; ++i) 135 | { 136 | res = ""; 137 | for (j = 0; j < layout.length; ++j) 138 | { 139 | res += this.fontLayout(String(entries[i][j]), layout[j]); 140 | } 141 | finalStr += res + "\n"; 142 | } 143 | 144 | finalStr += hr + "\n"; 145 | 146 | fl.trace(finalStr); 147 | }; 148 | 149 | /** 150 | * For the table layout, add padding to a string 151 | * @method fontLayout 152 | * @param {String} str The string to layout 153 | * @param {int} len The number of spaces to pad 154 | * @return {String} The padded string 155 | */ 156 | p.fontLayout = function(str, len) 157 | { 158 | while(str.length < len) 159 | { 160 | str = str + " "; 161 | } 162 | return str; 163 | }; 164 | 165 | /** 166 | * Search for a library item within an item 167 | * @method searchInItem 168 | * @param {Item} libItem The item to search within 169 | */ 170 | p.searchInItem = function(libItem) 171 | { 172 | // Ignore shared runtime libraries 173 | if (!this.isCanvas && libItem.linkageImportForRS) return; 174 | 175 | // only process new objects and not runtime shared-assets 176 | if (this.validTypes.contains(libItem.itemType) && !this.objectsUsed.contains(libItem.name)) 177 | { 178 | this.objectsUsed.push(libItem.name); 179 | 180 | if (libItem.timeline) 181 | { 182 | // if there is a timeline, repeat the scan as a recursion 183 | this.searchInTimeline(libItem.timeline, libItem.name); 184 | } 185 | } 186 | }; 187 | 188 | /** 189 | * Find the usage within a timeline 190 | * @method searchInTimeline 191 | * @param {Timeline} tLine THe time to look withint 192 | * @param {String} timelineName The name of the timeline 193 | */ 194 | p.searchInTimeline = function(tLine, timelineName) 195 | { 196 | var j = 0; 197 | var k = 0; 198 | var l = 0; 199 | var el; 200 | var frm; 201 | var layer; 202 | var lyrVisibility; 203 | 204 | // cycle through each of the layers in this timeline 205 | for(j = 0; j < tLine.layers.length; ++j ) 206 | { 207 | // cycle through each of the layers in this timeline 208 | layer = tLine.layers[j]; 209 | 210 | // store the layer visibility and then make the layer visible. 211 | // Elements cannot be found on invisible layers 212 | lyrVisibility = layer.visible; 213 | layer.visible = true; 214 | 215 | for(k = 0; k < layer.frames.length;) 216 | { 217 | // step through the frames on this layer 218 | frm = layer.frames[k]; 219 | 220 | for (l = 0; l < frm.elements.length; ++l ) 221 | { 222 | // then cycle through the elements on this frame 223 | el = frm.elements[l]; 224 | 225 | // If the element is a text field, check the font face 226 | if (el.elementType == "instance") 227 | { 228 | if (el.libraryItem.name == this.item.name) 229 | { 230 | var entry = []; 231 | 232 | entry.push(timelineName); 233 | entry.push(layer.name); 234 | entry.push(k+1); 235 | 236 | this.output.push(entry); 237 | } 238 | else 239 | { 240 | this.searchInItem(el.libraryItem); 241 | } 242 | } 243 | } 244 | k += frm.duration; 245 | } 246 | // return this layer to its original visibility (optional) 247 | layer.visible = lyrVisibility; 248 | } 249 | }; 250 | 251 | new UsageReport(); 252 | 253 | }()); -------------------------------------------------------------------------------- /src/Commands/Optimizing/Filter Finder.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | /** 4 | * This script parses all of the items inside of a Flash document (fla) 5 | * and alerts the user to replace the font face with something else. 6 | * @class FilterFinder 7 | */ 8 | var FilterFinder = function() 9 | { 10 | var doc = fl.getDocumentDOM(); 11 | 12 | // Check for document 13 | if (!doc) return; 14 | 15 | /** 16 | * Collection of the library item names used to keep track of what we've already searched 17 | * @property {Array} objectsUsed 18 | * @private 19 | */ 20 | this.objectsUsed = []; 21 | 22 | /** 23 | * The output result 24 | * @property {Array} output 25 | * @private 26 | */ 27 | this.output = []; 28 | 29 | var i; 30 | var curTimelines = doc.timelines; 31 | var libraryItems = doc.library.items; 32 | 33 | this.isCanvas = doc.type == "htmlcanvas"; 34 | 35 | // Create the headers 36 | var headers = [ 37 | 'symbol', 38 | 'timeline', 39 | 'layer', 40 | 'frame', 41 | 'filters', 42 | 'colorized' 43 | ]; 44 | 45 | // Clear the output window 46 | fl.outputPanel.clear(); 47 | 48 | // Search through the timelines of this document 49 | for (i = 0; i < curTimelines.length; ++i) 50 | { 51 | this.findFilters(curTimelines[i], "(Main Timeline)"); 52 | } 53 | 54 | // Search through library items not put on stage 55 | for (i = 0; i < libraryItems.length; ++i) 56 | { 57 | this.searchLibraryItem(libraryItems[i]); 58 | } 59 | 60 | // Check for no finds 61 | if (this.output.length < 1) 62 | { 63 | alert("Document contains no filters or colorizations."); 64 | return; 65 | } 66 | 67 | fl.trace("\nINSTANCE FILTERS"); 68 | fl.trace(this.tableLayout(headers, this.output)); 69 | }; 70 | 71 | // Save the prototype 72 | var p = FilterFinder.prototype; 73 | 74 | /** 75 | * The amount of padding in the output report 76 | * @property {int} PADDING 77 | * @static 78 | * @readOnly 79 | * @default 4 80 | */ 81 | var PADDING = 4; 82 | 83 | /** 84 | * Generate an ASCII table 85 | * @method tableLayout 86 | * @private 87 | * @param {Array} header Collection of header labels 88 | * @param {Array} entries Collection of array entries 89 | * @return {String} The full ASCII table 90 | */ 91 | p.tableLayout = function(headers, entries) 92 | { 93 | var finalStr = ""; 94 | 95 | // How wide each column is 96 | var layout = []; 97 | 98 | // Set the minimum values 99 | for (i = 0; i < headers.length; ++i) 100 | { 101 | layout[i] = headers[i].length + PADDING; 102 | } 103 | 104 | // Measure the layout for prettier display 105 | for (i = 0; i < entries.length; ++i) 106 | { 107 | for (j = 0; j < layout.length; ++j) 108 | { 109 | layout[j] = Math.max(layout[j], String(entries[i][headers[j]]).length + PADDING); 110 | } 111 | } 112 | 113 | // Show the table headers 114 | res = ""; 115 | var total = 0; 116 | for (j = 0; j < layout.length; ++j) 117 | { 118 | var th = headers[j]; 119 | res += fontLayout(th[0].toUpperCase() + th.substr(1), layout[j]); 120 | total += layout[j]; 121 | } 122 | finalStr += res + "\n"; 123 | 124 | // Place a divider 125 | var hr = ""; 126 | for (i = 0; i < total; ++i) 127 | { 128 | hr += "-"; 129 | } 130 | 131 | finalStr += hr + "\n"; 132 | 133 | // Prep the entries 134 | for (i = 0; i < entries.length; ++i) 135 | { 136 | res = ""; 137 | for (j = 0; j < layout.length; ++j) 138 | { 139 | res += fontLayout(String(entries[i][headers[j]]), layout[j]); 140 | } 141 | finalStr += res + "\n"; 142 | } 143 | 144 | finalStr += hr + "\n"; 145 | return "\n" + finalStr; 146 | }; 147 | 148 | /** 149 | * Prepend string with text based on a fixed length 150 | * @method fontLayout 151 | * @param {String} str THe string to pad 152 | * @param {int} len The length of the output string 153 | */ 154 | var fontLayout = function(str, len) 155 | { 156 | while(str.length < len) 157 | { 158 | str = str + " "; 159 | } 160 | return str; 161 | }; 162 | 163 | /** 164 | * Find a text fields on a timeline 165 | * @method findFilters 166 | * @param {Timeline} tLine The flash Timeline object 167 | * @param {String} timelineName The name of the timeline for reporting 168 | */ 169 | p.findFilters = function(tLine, timelineName) 170 | { 171 | var j = 0; 172 | var k = 0; 173 | var l = 0; 174 | var el; 175 | var frm; 176 | var layer; 177 | var lyrVisibility; 178 | 179 | // cycle through each of the layers in this timeline 180 | for (j = 0; j < tLine.layers.length; ++j ) 181 | { 182 | // cycle through each of the layers in this timeline 183 | layer = tLine.layers[j]; 184 | 185 | // store the layer visibility and then make the layer visible. 186 | // Elements cannot be found on invisible layers 187 | lyrVisibility = layer.visible; 188 | layer.visible = true; 189 | 190 | //fl.trace("layer is : '"+layer.name+"'"); 191 | for (k = 0; k < layer.frames.length;) 192 | { 193 | // step through the frames on this layer 194 | frm = layer.frames[k]; 195 | 196 | for ( l = 0; l < frm.elements.length; ++l ) 197 | { 198 | // then cycle through the elements on this frame 199 | el = frm.elements[l]; 200 | 201 | // If the element is a text field, check the font face 202 | if (el.elementType == "instance" && el.instanceType == "symbol") 203 | { 204 | var colorized = el.colorMode != "none" && el.colorMode != "alpha"; 205 | var filters = el.filters ? el.filters.length : 0; 206 | if (filters > 0 || colorized) 207 | { 208 | this.output.push({ 209 | symbol: el.libraryItem.name, 210 | timeline: timelineName, 211 | layer: layer.name, 212 | frame: k + 1, 213 | filters: filters, 214 | colorized: colorized 215 | }); 216 | } 217 | 218 | // lets get the library item for this element 219 | this.searchLibraryItem(el.libraryItem); 220 | } 221 | } 222 | // Go to the next keyframe 223 | k += frm.duration; 224 | } 225 | // return this layer to its original visibility (optional) 226 | layer.visible = lyrVisibility; 227 | } 228 | }; 229 | 230 | /** 231 | * Search for fonts within a library item 232 | * @searchLibraryItem 233 | * @param {Item} libItem The library item 234 | */ 235 | p.searchLibraryItem = function(libItem) 236 | { 237 | var validTypes = ["movie clip","graphic","button"]; 238 | var runtimeShared = !this.isCanvas && libItem.linkageImportForRS; 239 | 240 | // only process new objects and not runtime shared-assets 241 | if ( validTypes.contains(libItem.itemType) && 242 | !this.objectsUsed.contains(libItem.name) && 243 | !runtimeShared) 244 | { 245 | this.objectsUsed.push(libItem.name); 246 | 247 | // get the timeline of this library symbol 248 | libTLine = libItem.timeline; 249 | 250 | if (libTLine) 251 | { 252 | // if there is a timeline, repeat the scan as a recursion 253 | this.findFilters(libTLine, libItem.name); 254 | } 255 | } 256 | }; 257 | 258 | /** 259 | * See if an array contains a value 260 | * @method contains 261 | * @param {mixed} needle The value to check for 262 | */ 263 | Array.prototype.contains = function(needle) 264 | { 265 | for(var k in this) 266 | { 267 | if (this[k] == needle) { return true; } 268 | } 269 | return false; 270 | }; 271 | 272 | new FilterFinder(); 273 | 274 | }()); -------------------------------------------------------------------------------- /src/Commands/Utilities/Copy Layers To Bitmap.jsfl: -------------------------------------------------------------------------------- 1 | (function() 2 | { 3 | var dom = fl.getDocumentDOM(); 4 | 5 | if (!dom) return; 6 | 7 | var timeline = dom.getTimeline(); 8 | var folderLayer = timeline.layers[0]; 9 | 10 | var i, bitmapLayer, bitmap, frame; 11 | 12 | if (folderLayer.name == "vectors") 13 | { 14 | // Get the bitmap layer 15 | var bitmapIndex = timeline.layers.length - 1; 16 | bitmapLayer = timeline.layers[bitmapIndex]; 17 | 18 | // Delete all the bitmaps 19 | for(i = 0; i < bitmapLayer.frames.length; i++) 20 | { 21 | frame = bitmapLayer.frames[i] 22 | if (frame.elements.length) 23 | { 24 | bitmap = frame.elements[0].libraryItem; 25 | dom.library.deleteItem(bitmap.name); 26 | } 27 | } 28 | 29 | // Delete the bitmap layer 30 | timeline.deleteLayer(bitmapIndex); 31 | 32 | // Delete the vectors layer 33 | folderLayer.visible = true; 34 | folderLayer.locked = false; 35 | folderLayer.layerType = "normal"; 36 | timeline.deleteLayer(0); 37 | 38 | for (i = timeline.layers.length - 1; i >= 0; i--) 39 | { 40 | timeline.layers[i].layerType = "normal"; 41 | } 42 | return; 43 | } 44 | 45 | var bitmapName; 46 | var scale = localToGlobalScale(); 47 | 48 | // The prompt was cancelled 49 | if (scale === null) 50 | { 51 | return; 52 | } 53 | 54 | // If we're inside a symbol, use the name of the 55 | if (timeline.libraryItem) 56 | { 57 | var item = timeline.libraryItem; 58 | bitmapName = item.name; 59 | var index = bitmapName.indexOf('/'); 60 | if (index > -1) 61 | { 62 | bitmapName = bitmapName.substr(index + 1); 63 | } 64 | } 65 | else 66 | { 67 | if (!dom.name) 68 | { 69 | return alert("Please save document first."); 70 | } 71 | 72 | // Chop of the ".fla" or ".xfl" extension 73 | bitmapName = dom.name.substr(0, dom.name.lastIndexOf('.')); 74 | } 75 | 76 | // The number of layers 77 | var origLength = timeline.layers.length; 78 | 79 | // Copy the current layers 80 | timeline.copyLayers(0, origLength - 1); 81 | 82 | // Create a new folder for the hidden, guided, locked vector layers 83 | var folderLayerIndex = timeline.addNewLayer('vectors', 'folder'); 84 | 85 | // Make sure the folder is first 86 | timeline.reorderLayer(folderLayerIndex, 0); 87 | 88 | // Copy to the folder 89 | timeline.pasteLayers(0); 90 | 91 | // Guide out the child layers 92 | for(i = 1; i <= origLength; i++) 93 | { 94 | timeline.layers[i].layerType = "guide"; 95 | } 96 | 97 | // Lock and hide all the vectors 98 | var parentLayer = timeline.layers[0]; 99 | parentLayer.visible = false; 100 | parentLayer.locked = true; 101 | 102 | // Add a new bitmap layer above the copied layers 103 | var bitmapLayerIndex = origLength + 1; 104 | timeline.setSelectedLayers(bitmapLayerIndex); 105 | timeline.addNewLayer('bitmap', 'normal', true); 106 | bitmapLayer = timeline.layers[bitmapLayerIndex]; 107 | 108 | var EMPTY = -1; 109 | var KEYFRAME = 1; 110 | 111 | // Select the contents of the original layers 112 | var status; 113 | var numFrames = timeline.frameCount; 114 | for(i = numFrames - 1; i >=0 ; --i) 115 | { 116 | timeline.currentFrame = i; 117 | 118 | // Check the status of the current frame 119 | // 0 = no keyframes 120 | // 1 = keyframes no elements 121 | // 2 = keyframes + elements 122 | status = frameStatus(bitmapLayerIndex + 1, i); 123 | 124 | // Current frame has no keyframes, no content 125 | if (status < KEYFRAME) 126 | { 127 | if (status == EMPTY) 128 | { 129 | timeline.setSelectedLayers(bitmapLayerIndex); 130 | timeline.insertBlankKeyframe(); 131 | } 132 | continue; 133 | } 134 | 135 | //ensure that there is a blank keyframe there to paste into 136 | selectFrame(bitmapLayerIndex, i); 137 | if (i > 0) timeline.insertBlankKeyframe(); // don't insert on the first frame 138 | 139 | // Copy all the frames and paste on the bitmap layer 140 | dom.selectAll(); 141 | dom.clipCopy(); 142 | selectFrame(bitmapLayerIndex, i); 143 | dom.clipPaste(true); 144 | 145 | // Scale the selection 146 | dom.transformSelection(scale, 0, 0, scale); 147 | 148 | // Convert the selection to a bitmap 149 | dom.convertSelectionToBitmap(); 150 | 151 | // Undo scale to the selection 152 | var bitmap = bitmapLayer.frames[i].elements[0]; 153 | dom.selection = [bitmap]; 154 | dom.transformSelection(1/scale, 0, 0, 1/scale); 155 | 156 | // Get the library item from the instance and rename it 157 | if (bitmapName) 158 | { 159 | var bitmapItem = bitmap.libraryItem; 160 | bitmapItem.name = bitmapName + (i+1); 161 | } 162 | } 163 | 164 | // Delete the rest of the layers 165 | while(bitmapLayerIndex + 1 < timeline.layers.length) 166 | { 167 | timeline.deleteLayer(timeline.layers.length - 1); 168 | } 169 | 170 | function selectFrame(layer, frame) 171 | { 172 | // Select the current frame 173 | timeline.setSelectedLayers(layer); 174 | timeline.setSelectedFrames(frame, frame + 1); 175 | } 176 | 177 | // Function to check the current status of a frame 178 | // -1 = no content 179 | // 0 = content but no keyframe 180 | // 1 = keyframes + content 181 | function frameStatus(index, currentFrame) 182 | { 183 | var layer = timeline.layers[index]; 184 | var status = -1; // empty 185 | var frame; 186 | while(layer) 187 | { 188 | // if (currentFrame >= layer.frameCount) continue; 189 | 190 | frame = layer.frames[currentFrame]; 191 | 192 | // Has content on it 193 | if (frame && frame.elements.length) 194 | { 195 | status = 0; 196 | if (frame.startFrame == i) 197 | { 198 | status = 1; 199 | break; 200 | } 201 | } 202 | layer = timeline.layers[++index]; 203 | } 204 | return status; 205 | } 206 | 207 | function localToGlobalScale() 208 | { 209 | var doc = fl.getDocumentDOM(); 210 | 211 | if (!doc) return; 212 | 213 | var scaleX = 1; 214 | var scaleY = 1; 215 | var scale; 216 | 217 | var timeline = doc.getTimeline(); 218 | var originalItem = libraryItem = timeline.libraryItem; 219 | var steps = 0; 220 | 221 | // We're on the main stage, ignore the rest set to 100% 222 | if (!libraryItem) 223 | { 224 | return 1; 225 | } 226 | 227 | var scaleKey = 'copyLayersToBitmapScale'; 228 | 229 | while(libraryItem) 230 | { 231 | // Go "up" a nested level 232 | doc.exitEditMode(); 233 | steps++; 234 | 235 | // Get the new timeline 236 | timeline = doc.getTimeline(); 237 | 238 | var element = doc.selection.length ? doc.selection[0] : null; 239 | if (element && element.libraryItem == libraryItem) 240 | { 241 | scaleX *= element.scaleX; 242 | scaleY *= element.scaleY; 243 | } 244 | else 245 | { 246 | fl.outputPanel.clear(); 247 | fl.trace("WARNING: Unable to measure the relative scale either because the current item was opened directly from the library "); 248 | fl.trace(" or because a tween is preventing the exit and enter of the symbol. Prompting for scale..."); 249 | 250 | // Go back into the symbol after we exited 251 | doc.library.editItem(originalItem.name); 252 | 253 | // Get the saved scale amount 254 | var defaultScale = originalItem.hasData(scaleKey) ? 255 | originalItem.getData(scaleKey) : 1; 256 | 257 | // Aask for the scale 258 | var scale = prompt("Output scale", defaultScale); 259 | 260 | if (!scale) return null; 261 | 262 | scale = parseFloat(scale); 263 | 264 | // Save the scale to use at the default later on 265 | originalItem.addData(scaleKey, "double", scale); 266 | 267 | return scale; 268 | } 269 | libraryItem = timeline.libraryItem; 270 | } 271 | 272 | // Go back to where we started 273 | if (steps) 274 | { 275 | while(steps--) 276 | { 277 | if (doc.selection.length) 278 | { 279 | doc.enterEditMode("inPlace"); 280 | } 281 | } 282 | } 283 | 284 | // Do a little rounding 285 | scaleX = Math.round(scaleX * 100000) / 100000; 286 | scaleY = Math.round(scaleY * 100000) / 100000; 287 | 288 | // Get the larger scale size 289 | scale = Math.max(scaleX, scaleY); 290 | 291 | // Save the scale to the library item if we have it 292 | if (originalItem) 293 | { 294 | // Save the scale to use at the default later on 295 | originalItem.addData(scaleKey, "double", scale); 296 | } 297 | 298 | // Get the largest scale 299 | return scale; 300 | } 301 | 302 | }()); 303 | -------------------------------------------------------------------------------- /src/Commands/Exporting/Export BitmapMovieClip.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | // Include the command library 4 | var dir = fl.scriptURI.split("/"); 5 | dir = dir.slice(0,dir.length - 3).join("/"); 6 | fl.runScript(dir + "/JSFLLibraries/Command.jsfl"); 7 | 8 | /** 9 | * Export a MovieClip or Graphic as a spritesheet 10 | * with some extra JSON data that describes the timeline 11 | * @class BitmapMovieClip 12 | */ 13 | var BitmapMovieClip = function() 14 | { 15 | // Check for document 16 | if (!this.getDOM()) return; 17 | 18 | JSON.prettyPrint = true; 19 | 20 | var library = this.dom.library; 21 | var selectedItems = library.getSelectedItems().slice(); 22 | if(selectedItems.length === 0) 23 | { 24 | alert("You must have one or more movieclips selected in the library to export."); 25 | return; 26 | } 27 | for(var i = 0; i < selectedItems.length; ++i) 28 | { 29 | var item = selectedItems[i]; 30 | if(item.itemType == "movie clip" || item.itemType == "graphic") 31 | { 32 | this.exportClip(item); 33 | } 34 | } 35 | }; 36 | 37 | // Reference tot he prototype 38 | var p = BitmapMovieClip.prototype = new Command(); 39 | 40 | /** 41 | * Export a single clip 42 | * @param {SymbolInstance} selectedItem The symbol movieclip or graphic to export 43 | */ 44 | p.exportClip = function(selectedItem) 45 | { 46 | var symbolPath = selectedItem.name.substring(0, selectedItem.name.lastIndexOf("/") + 1), 47 | symbolName = selectedItem.name.substring(selectedItem.name.lastIndexOf("/") + 1), 48 | scaledItem, 49 | library; 50 | 51 | var scale = prompt("What scale is " + symbolName + 52 | " being exported at?\n(press cancel to skip this movieclip)", "1"); 53 | 54 | if (!scale) 55 | { 56 | return; 57 | } 58 | else if(parseFloat(scale) > 0) 59 | { 60 | scale = parseFloat(scale); 61 | } 62 | else 63 | { 64 | scale = 1; 65 | } 66 | 67 | var outputObj = {}; 68 | 69 | //keep track of this for proper reassembly 70 | outputObj.scale = scale; 71 | outputObj.fps = this.dom.frameRate; 72 | outputObj.labels = {}; 73 | 74 | //get all those pesky frame labels 75 | var timeline = selectedItem.timeline; 76 | var layers = timeline.layers, frames; 77 | 78 | var totalLength = 0; 79 | for (var l = 0, lLen = layers.length; l < lLen; ++l) 80 | { 81 | var layer = layers[l]; 82 | if(layer.layerType == "folder") continue; 83 | 84 | // hide guide/mask layers and show everything else, 85 | // for origin calculation purposes 86 | layer.visible = layer.layerType != "guide" && layer.layerType != "mask"; 87 | 88 | frames = layer.frames; 89 | fLen = frames.length; 90 | if(fLen > totalLength) 91 | totalLength = fLen; 92 | for(var f = 0; f < fLen;) 93 | { 94 | var frame = frames[f]; 95 | if(frame.name) 96 | { 97 | outputObj.labels[frame.name] = f; 98 | } 99 | f += frame.duration; 100 | } 101 | } 102 | // set up information on the pngs that would be exported 103 | var data = {}; 104 | outputObj.frames = [data]; 105 | 106 | // the name of the clip with a # to replace with the frame number 107 | data.name = symbolName + "#"; 108 | 109 | // flash frame numbers start at 0 when you use the spritesheet exporter 110 | data.min = 0; 111 | 112 | // go until the end 113 | data.max = timeline.frameCount - 1; 114 | 115 | // flash frame numbers always have 4 digits 116 | data.digits = 4; 117 | 118 | //get the origin 119 | var left = 100000000; 120 | var top = 100000000; 121 | 122 | for (var i = 1, len = timeline.frameCount + 1; i < len; ++i) 123 | { 124 | var bounds = timeline.getBounds(i, false);//don't get hidden layers, aka the masks and guides we just hid 125 | if(bounds.left < left) 126 | left = bounds.left; 127 | if(bounds.top < top) 128 | top = bounds.top; 129 | } 130 | outputObj.origin = {x: -left * scale, y: -top * scale}; 131 | 132 | //apply scaling if not 1 133 | if(scale != 1) 134 | { 135 | library = this.dom.library; 136 | selectedItem.name = "EBMC_TEMP"; 137 | library.addNewItem("movie clip", symbolName); 138 | scaledItem = library.items[library.findItemIndex(symbolName)]; 139 | library.editItem(symbolName); 140 | library.addItemToDocument({x:0, y:0}, symbolPath + "EBMC_TEMP"); 141 | var element = scaledItem.timeline.layers[0].frames[0].elements[0]; 142 | element.symbolType = "graphic"; 143 | element.scaleX = scale; 144 | element.scaleY = scale; 145 | scaledItem.timeline.insertFrames(1, true, totalLength - 1); 146 | } 147 | 148 | //export the movieclip data 149 | var uri = fl.browseForFileURL("save", "Select a file to save the movieclip data for " + symbolName, "JSON Files (*.json)", "json"); 150 | if (uri) 151 | { 152 | FLfile.write(uri, JSON.stringify(outputObj)); 153 | } 154 | 155 | //For Windows, we need to supply a single file type, otherwise it gets all weird, 156 | //especially when not supplying an extension. 157 | uri = fl.browseForFileURL("save", "Select a file to save the spritesheet (png + json atlas) for " + symbolName, "Images (*.png)", "png"); 158 | 159 | if (uri) 160 | { 161 | //strip off the extension, so that we can use it for both .json and .png 162 | if(uri.lastIndexOf(".") > 0) 163 | uri = uri.substring(0, uri.lastIndexOf(".")); 164 | 165 | var exporter = new SpriteSheetExporter(); 166 | exporter.layoutFormat = "JSON"; 167 | exporter.algorithm = "maxRects"; 168 | //While SpringRoll supports rotated textures, it seems like Flash's exporter doesn't 169 | //handle them properly and outputs sprites with a little bit of an offset 170 | exporter.allowRotate = false; 171 | exporter.allowTrimming = true; 172 | exporter.autoSize = true; 173 | exporter.maxSheetWidth = 2048; 174 | exporter.maxSheetHeight = 2048; 175 | exporter.shapePadding = 1; 176 | exporter.stackDuplicateFrames = true; 177 | exporter.addSymbol(scaledItem || selectedItem); 178 | if(exporter.overflowed) 179 | fl.trace("WARNING: Unable to fit all frames in 2048x2048 spritesheet"); 180 | exporter.exportSpriteSheet(uri ,{ 181 | format:"png", 182 | bitDepth:32, 183 | backgroundColor:"#00000000" 184 | }); 185 | 186 | //read the published texture atlas and get rid of *sequential* duplicate frames, they 187 | //are easily reassembled by the TextureAtlas class used by BitmapMovieClip 188 | var exportedJSON = JSON.parse(FLfile.read(uri + ".json")); 189 | frames = exportedJSON.frames; 190 | var prevObj; 191 | for(var id in frames) 192 | { 193 | var obj = frames[id]; 194 | if(typeof obj != "object") 195 | continue; 196 | if(prevObj) 197 | { 198 | if(areFramesEqual(prevObj, obj)) 199 | { 200 | delete frames[id]; 201 | continue; 202 | } 203 | } 204 | prevObj = obj; 205 | } 206 | JSON.prettyPrint = false; 207 | FLfile.write(uri + ".json", JSON.stringify(exportedJSON)); 208 | } 209 | 210 | //undo scaling if not 1 211 | if(scale != 1) 212 | { 213 | library.deleteItem(symbolName); 214 | selectedItem.name = symbolName; 215 | } 216 | }; 217 | 218 | function areFramesEqual(obj1, obj2) 219 | { 220 | /* 221 | "frame": {"x":0,"y":152,"w":118,"h":151}, 222 | "rotated": false, 223 | "trimmed": false, 224 | "spriteSourceSize": {"x":0,"y":0,"w":118,"h":151}, 225 | "sourceSize": {"w":118,"h":151}, 226 | "pivot": {"x":0,"y":0} 227 | */ 228 | if(obj1.trimmed != obj2.trimmed) 229 | return false; 230 | if(obj1.rotated != obj2.rotated) 231 | return false; 232 | var compare1 = obj1.frame, compare2 = obj2.frame; 233 | if(compare1.x != compare2.x || compare1.y != compare2.y || compare1.w != compare2.w || 234 | compare1.h != compare2.h) 235 | { 236 | return false; 237 | } 238 | compare1 = obj1.spriteSourceSize; 239 | compare2 = obj2.spriteSourceSize; 240 | if(compare1.x != compare2.x || compare1.y != compare2.y || compare1.w != compare2.w || 241 | compare1.h != compare2.h) 242 | { 243 | return false; 244 | } 245 | compare1 = obj1.sourceSize; 246 | compare2 = obj2.sourceSize; 247 | if(compare1.w != compare2.w || compare1.h != compare2.h) 248 | { 249 | return false; 250 | } 251 | compare1 = obj1.pivot; 252 | compare2 = obj2.pivot; 253 | if(compare1 && compare2 && (compare1.x != compare2.x || compare1.y != compare2.y)) 254 | { 255 | return false; 256 | } 257 | return true; 258 | } 259 | 260 | // Run script 261 | new BitmapMovieClip(); 262 | 263 | }()); -------------------------------------------------------------------------------- /src/Commands/Optimizing/Optimize Keyframes.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | // Include the command library 4 | var dir = fl.scriptURI.split("/"); 5 | dir = dir.slice(0,dir.length - 3).join("/"); 6 | fl.runScript(dir + "/JSFLLibraries/Command.jsfl"); 7 | 8 | /** 9 | * Script to remove redundant keyframes from the timeline. 10 | * @class OptimizeKeyframes 11 | */ 12 | var OptimizeKeyframes = function() 13 | { 14 | if (!this.getDOM()) return; 15 | 16 | fl.showIdleMessage(false); 17 | 18 | // Get the saved checks 19 | var settingsPath = 'Commands/Optimizing/OptimizeKeyframes.json'; 20 | var settings = this.loadSettings(settingsPath) || {}; 21 | 22 | /** 23 | * The tolerance when comparing a,b,c,d matrix values 24 | * @propety {Number} scaleTolerance 25 | * @default 0.1 26 | */ 27 | this.scaleTolerance = settings.scaleTolerance || 0.01; 28 | 29 | /** 30 | * The tolerance when comparing translation tx, ty values 31 | * @propety {Number} moveTolerance 32 | * @default 2.0 33 | */ 34 | this.moveTolerance = settings.moveTolerance || 1.0; 35 | 36 | var ui = this.dialog("Optimize Keyframes") 37 | .columns(2) 38 | .openRow() 39 | .openHBox() 40 | .addLabel('Move Tolerance', 'moveTolerance', 180) 41 | .addTextBox('moveTolerance', this.moveTolerance) 42 | .closeHBox() 43 | .closeRow() 44 | .openRow() 45 | .openHBox() 46 | .addLabel('Scale Tolerance', 'scaleTolerance', 180) 47 | .addTextBox('scaleTolerance', this.scaleTolerance) 48 | .closeHBox() 49 | .closeRow() 50 | .create(); 51 | 52 | // Handle the cancel 53 | if (ui.dismiss == "cancel") return; 54 | 55 | // Update the settings 56 | this.moveTolerance = settings.moveTolerance = parseFloat(ui.moveTolerance); 57 | this.scaleTolerance = settings.scaleTolerance = parseFloat(ui.scaleTolerance); 58 | 59 | // Save the settings with the update from the dialog 60 | this.saveSettings(settingsPath, settings); 61 | 62 | // get the current timeline 63 | this.preprocess(this.dom.getTimeline()); 64 | 65 | fl.showIdleMessage(true); 66 | }; 67 | 68 | // Reference to the prototype 69 | var p = OptimizeKeyframes.prototype = new Command(); 70 | 71 | /** 72 | * First, a preprocess task will remove any tween where the keyframes 73 | * are the same, this will help eliminate more cases where tweens 74 | * would typically block the optimizing of keyframes 75 | * @method preprocess 76 | */ 77 | p.preprocess = function(timeline) 78 | { 79 | var layers = timeline.layers; 80 | var i, j, frames, frame, lastKeyframe; 81 | 82 | for (i = 0; i < layers.length; i++) 83 | { 84 | // Select the current layer so we can remove 85 | // keyframes from it 86 | timeline.currentLayer = i; 87 | 88 | // The collection of frames 89 | frames = layers[i].frames; 90 | 91 | for (j = 0; j < frames.length;) 92 | { 93 | // The current keyframe 94 | frame = frames[j]; 95 | 96 | // No content, skip to the next keyframe 97 | if (frame.isEmpty) 98 | { 99 | lastKeyframe = null; 100 | j += frame.duration; 101 | continue; 102 | } 103 | 104 | // Remove tween for frames that are one frame long 105 | if (frame.duration === 1 && frame.tweenType != "none") 106 | { 107 | frame.tweenType = "none"; 108 | } 109 | 110 | if (lastKeyframe && // valid last keyframe 111 | lastKeyframe.tweenType != "none" && // previous keyframe is a tween 112 | this.frameCompare(lastKeyframe, frame)) // keyframes are the 113 | { 114 | // Remove the tween for keyframes that are the same 115 | lastKeyframe.tweenType = "none"; 116 | } 117 | 118 | // Save the last invalid keyframe 119 | lastKeyframe = frame; 120 | 121 | // go to the next keyframe 122 | j += frame.duration; 123 | } 124 | 125 | // Clear the keyframe for each layer 126 | lastKeyframe = null; 127 | } 128 | 129 | // Remove the keyframes 130 | for (i = 0; i < layers.length; i++) 131 | { 132 | // Select the current layer so we can remove 133 | // keyframes from it 134 | timeline.currentLayer = i; 135 | 136 | // The collection of frames 137 | frames = layers[i].frames; 138 | 139 | for (j = 0; j < frames.length;) 140 | { 141 | // The current keyframe 142 | frame = frames[j]; 143 | 144 | // For keyframes that are empty or are the start 145 | // of a tween, then we'll ignore and goto the next 146 | // keyframe 147 | if (frame.tweenType !== "none" || frame.isEmpty) 148 | { 149 | lastKeyframe = null; 150 | j += frame.duration; 151 | continue; 152 | } 153 | 154 | // If the frames are the same 155 | // then we'll remove the current keyframe 156 | if (lastKeyframe && this.frameCompare(lastKeyframe, frame)) 157 | { 158 | timeline.clearKeyframes(j); 159 | } 160 | else 161 | { 162 | // Save the last invalid keyframe 163 | lastKeyframe = frame; 164 | } 165 | 166 | // go to the next keyframe 167 | j += frame.duration; 168 | } 169 | 170 | // Clear the keyframe for each layer 171 | lastKeyframe = null; 172 | } 173 | }; 174 | 175 | /** 176 | * Compare two keyframes to see if they are the same 177 | * @method frameCompare 178 | * @param {Frame} lastKeyframe The last keyframe to compare to 179 | * @param {Frame} currKeyframe The current keyframe to compare to 180 | * @return {Boolean} True if the keyframes are the same, false if not 181 | */ 182 | p.frameCompare = function(lastKeyframe, currKeyframe) 183 | { 184 | // Different element length counts, not the same 185 | if (lastKeyframe.elements.length != currKeyframe.elements.length) 186 | { 187 | return false; 188 | } 189 | 190 | // Loop through all the elements, they should 191 | // be in the same layered order to be considered the same 192 | // so we'll compare an element in the last keyframe 193 | // and a keyframe in the current keyframe 194 | for (var i = 0, len = lastKeyframe.elements.length; i < len; i++) 195 | { 196 | if (!this.elementCompare(lastKeyframe.elements[i], currKeyframe.elements[i])) 197 | { 198 | return false; 199 | } 200 | } 201 | return true; 202 | }; 203 | 204 | /** 205 | * Are the elements the same 206 | * @method elementCompare 207 | * @param {Element} element1 First element to compare 208 | * @param {Element} element2 Second element to compare 209 | * @return {Boolean} True if the elements are the same, false if not 210 | */ 211 | p.elementCompare = function(element1, element2) 212 | { 213 | var props; 214 | 215 | // The elements should be the same type 216 | if (element1.elementType != element2.elementType) 217 | { 218 | return false; 219 | } 220 | // Compare the positions 221 | else if (!this.matrixCompare(element1.matrix, element2.matrix)) 222 | { 223 | return false; 224 | } 225 | 226 | // Special compare instances 227 | if (element1.elementType === "instance") 228 | { 229 | // Library items aren't the same 230 | if (element1.libraryItem !== element2.libraryItem) 231 | { 232 | return false; 233 | } 234 | // Unlikely that these are different 235 | else if (element1.instanceType !== element2.instanceType) 236 | { 237 | return false; 238 | } 239 | // Special checks for symbols types 240 | if (element1.instanceType === "symbol") 241 | { 242 | 243 | props = [ 244 | 'symbolType', 245 | 'colorMode', 246 | 'blendMode', 247 | 'loop', 248 | 'slient', 249 | 'is3D', 250 | 'visible', 251 | 'firstFrame', 252 | 'cacheAsBitmap' 253 | ]; 254 | 255 | if (!this.propsCompare(element1, element2, props)) 256 | { 257 | return false; 258 | } 259 | } 260 | } 261 | return true; 262 | }; 263 | 264 | /** 265 | * Compare a list of properties 266 | * @method propsCompare 267 | * @param {Element} element1 First element to compare 268 | * @param {Element} element2 Second element to compare 269 | * @param {Array} props The collectin of properties 270 | * @return {Boolean} True if the elements are the same, false if not 271 | */ 272 | p.propsCompare = function(element1, element2, props) 273 | { 274 | for (var i = 0; i < props.length; i++) 275 | { 276 | if (element1[props[i]] !== element2[props[i]]) 277 | { 278 | return false; 279 | } 280 | } 281 | return true; 282 | }; 283 | 284 | /** 285 | * Method to compare two matrices 286 | * @method matrixCompare 287 | * @param {Matrix} matrix1 First matrix to compare 288 | * @param {Matrix} matrix2 Second matrix to compare 289 | * @return {Boolean} If the matrices are the same 290 | */ 291 | p.matrixCompare = function(matrix1, matrix2) 292 | { 293 | return Math.abs(matrix1.a - matrix2.a) <= this.scaleTolerance && 294 | Math.abs(matrix1.b - matrix2.b) <= this.scaleTolerance && 295 | Math.abs(matrix1.c - matrix2.c) <= this.scaleTolerance && 296 | Math.abs(matrix1.d - matrix2.d) <= this.scaleTolerance && 297 | Math.abs(matrix1.tx - matrix2.tx) <= this.moveTolerance && 298 | Math.abs(matrix1.ty - matrix2.ty) <= this.moveTolerance; 299 | }; 300 | 301 | new OptimizeKeyframes(); 302 | 303 | }()); -------------------------------------------------------------------------------- /src/Commands/Optimizing/Optimize Graphics.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | /** 4 | * The class for removing unused graphic frames 5 | * @class OptimizeGraphics 6 | */ 7 | var OptimizeGraphics = function() 8 | { 9 | /** 10 | * The cache of items that have already been searched 11 | * @property {Array} objectsSearched 12 | */ 13 | this.objectsSearched = []; 14 | 15 | /** 16 | * The collection of frames found 17 | * @property {Array} framesFound 18 | */ 19 | this.framesFound = []; 20 | 21 | /** 22 | * The specific library item we're searching for 23 | * @property {LibraryItem} searchItem 24 | */ 25 | this.searchItem = null; 26 | 27 | /** 28 | * CC-support if the current FLA is an html5canvase 29 | * @property {Boolean} isCanvas 30 | */ 31 | this.isCanvas = false; 32 | 33 | /** 34 | * The number of frames in the search item 35 | * @property {int} frameCount 36 | */ 37 | this.frameCount = 0; 38 | 39 | // run the constructor 40 | this.initialize(); 41 | }; 42 | 43 | // Reference to the prototype 44 | var p = OptimizeGraphics.prototype = {}; 45 | 46 | /** 47 | * Constructor, start creating the optimize graphics object 48 | * @method initialize 49 | */ 50 | p.initialize = function() 51 | { 52 | var i = 0, j = 0, length = 0; 53 | 54 | if ( !fl.getDocumentDOM() ) 55 | { 56 | alert("No document opened."); 57 | return; 58 | } 59 | 60 | var doc = fl.getDocumentDOM(); 61 | var curTimelines = doc.timelines; 62 | var libraryItems = doc.library.items; 63 | var items = doc.library.getSelectedItems(); 64 | 65 | if (items.length === 0) 66 | { 67 | alert("Must select a library item to optimize"); 68 | return; 69 | } 70 | 71 | if (doc.type) 72 | { 73 | this.isCanvas = (doc.type == "htmlcanvas"); 74 | } 75 | this.searchItem = items[0]; 76 | var frameCount = this.frameCount = this.searchItem.timeline.frameCount; 77 | 78 | var con = confirm("You're about to optimize '" + this.searchItem.name + "' by removing unused frames. This may take several minutes, do you want to continue?"); 79 | 80 | if (!con) 81 | { 82 | return; 83 | } 84 | 85 | fl.showIdleMessage(false); 86 | 87 | // Clear the output window 88 | fl.outputPanel.clear(); 89 | fl.trace("+------------------------------------------+"); 90 | fl.trace("| Optimizing '" + this.searchItem.name + "'"); 91 | fl.trace("+------------------------------------------+\n"); 92 | var now = microtime(); 93 | 94 | // Search through the timelines of this document 95 | for(i = 0, length = curTimelines.length; i < length; ++i) 96 | { 97 | this.findSymbolsByTimeline(curTimelines[i]); 98 | } 99 | 100 | // Search through library items not put on stage 101 | for(j = 0, length = libraryItems.length; j < length; ++j) 102 | { 103 | this.searchLibraryItem(libraryItems[j]); 104 | } 105 | 106 | if ( this.framesFound.length === 0 ) 107 | { 108 | alert("No frames found in this document."); 109 | } 110 | else 111 | { 112 | this.framesFound.sort(sortNumber); 113 | doc.library.editItem(this.searchItem.name); 114 | var timeline = doc.getTimeline(); 115 | for(j=0; j < frameCount; j++) 116 | { 117 | if (!inObject(j, this.framesFound)) 118 | { 119 | fl.trace("Clearing frame : " + (j+1)); 120 | this.clearFrame(timeline, j); 121 | } 122 | } 123 | } 124 | var sec = Math.round((microtime() - now) * 1000) / 1000; 125 | fl.trace("\nFinished optimizing in " + sec + " sec"); 126 | 127 | fl.showIdleMessage(true); 128 | }; 129 | 130 | /** 131 | * Get the current time in microtime 132 | * @method microtime 133 | * @private 134 | * @return {Number} Either the Number of seconds or radable seconds 135 | */ 136 | var microtime = Date.now ? 137 | function () 138 | { 139 | return Date.now() / 1000; 140 | } : 141 | function () 142 | { 143 | return new Date().getTime() / 1000; 144 | }; 145 | 146 | /** 147 | * Clear the frame 148 | * @method clearFrame 149 | * @param {Timeline} timeline The timeline object 150 | * @param {int} frameIndex The frame number to clear 151 | */ 152 | p.clearFrame = function(timeline, frameIndex) 153 | { 154 | var n = frameIndex; 155 | var layers = timeline.layers; 156 | 157 | for(var i=0, length = layers.length; i < length; i++) 158 | { 159 | var layer = layers[i]; 160 | if (layer.layerType != "normal" || !layer.frames[n]) 161 | { 162 | continue; 163 | } 164 | var frame = layer.frames[n]; 165 | if (frame.elements.length === 0) 166 | { 167 | continue; 168 | } 169 | timeline.setSelectedLayers(i); 170 | if(frame.startFrame + frame.duration > n + 1) 171 | timeline.insertKeyframe(n + 1); 172 | timeline.clearFrames(n, n+1); 173 | } 174 | }; 175 | 176 | /** 177 | * Comparator function for sorting 178 | * @private 179 | * @method sortNumber 180 | * @param {Number} a 181 | * @param {Number} b 182 | * @return {Number} 183 | */ 184 | var sortNumber = function (a,b) 185 | { 186 | return a - b; 187 | }; 188 | 189 | /** 190 | * Find a symbol by the timeline 191 | * @method findSymbolsByTimeline 192 | * @param {Timeline} tLine The timeline to search on 193 | */ 194 | p.findSymbolsByTimeline = function(tLine) 195 | { 196 | var j = 0, 197 | jLen = 0, 198 | k = 0, 199 | kLen = 0, 200 | l = 0, 201 | lLen = 0, 202 | el, 203 | frm, 204 | layer, 205 | lyrVisibility; 206 | 207 | var layers = tLine.layers; 208 | // cycle through each of the layers in this timeline 209 | for(j = 0, jLen = layers.length; j < jLen; ++j) 210 | { 211 | // cycle through each of the layers in this timeline 212 | layer = layers[j]; 213 | 214 | if (layer.layerType != "normal" || this.framesFound.length == this.frameCount) 215 | { 216 | continue; 217 | } 218 | 219 | // store the layer visibility and then make the layer visible. 220 | // Elements cannot be found on invisible layers 221 | lyrVisibility = layer.visible; 222 | layer.visible = true; 223 | 224 | var frames = layer.frames; 225 | // Loop through the frames 226 | for(k = 0, kLen = frames.length; k < kLen;) 227 | { 228 | // step through the frames on this layer 229 | frm = frames[k]; 230 | 231 | // Ignore empty keyframes or non-keyframes 232 | if (frm.elements.length === 0 || this.framesFound.length == this.frameCount) 233 | { 234 | k += frm.duration; 235 | continue; 236 | } 237 | 238 | var elements = frm.elements; 239 | for (l = 0, lLen = elements.length; l < lLen; ++l) 240 | { 241 | // then cycle through the elements on this frame 242 | el = elements[l]; 243 | 244 | if (el.elementType == "instance") 245 | { 246 | if (el.libraryItem == this.searchItem) 247 | { 248 | // If we're a movieclip or a button, then select all frames 249 | if (el.symbolType != "graphic") 250 | { 251 | this.addFrameRange(0, this.frameCount); 252 | } 253 | else 254 | { 255 | // If we are loop or play once, we will select a range 256 | if (el.loop == "loop" || el.loop == "play once") 257 | { 258 | this.addFrameRange(el.firstFrame, el.firstFrame + frm.duration, el.loop == "loop"); 259 | } 260 | else 261 | { 262 | // Easier is to select first frame 263 | this.addFrame(el.firstFrame); 264 | } 265 | } 266 | } 267 | else 268 | { 269 | // lets get the library item for this element 270 | this.searchLibraryItem(el.libraryItem); 271 | } 272 | } 273 | } 274 | k += frm.duration; 275 | } 276 | 277 | // return this layer to its original visibility (optional) 278 | layer.visible = lyrVisibility; 279 | } 280 | }; 281 | 282 | var validSymbolTypes = ["graphic", "movie clip", "button"]; 283 | 284 | /** 285 | * Search the library by item 286 | * @method searchLibraryItem 287 | * @param {LibraryItem} libItem The library item to search for 288 | */ 289 | p.searchLibraryItem = function(libItem) 290 | { 291 | // Ignore invalid symbol types 292 | if (!inObject(libItem.itemType, validSymbolTypes) || (!this.isCanvas && libItem.linkageImportForRS)) 293 | { 294 | this.objectsSearched.push(libItem.name); 295 | return; 296 | } 297 | 298 | // only process new objects and not runtime shared-assets 299 | if ( !inObject(libItem.name, this.objectsSearched)) 300 | { 301 | this.objectsSearched.push(libItem.name); 302 | 303 | // get the timeline of this library symbol 304 | libTLine = libItem.timeline; 305 | 306 | if (libTLine) 307 | { 308 | // if there is a timeline, repeat the scan as a recursion 309 | this.findSymbolsByTimeline(libTLine); 310 | } 311 | } 312 | }; 313 | 314 | /** 315 | * Add a frame number to the already found frames 316 | * @method addFrame 317 | * @param {int} frameNum The frame number found 318 | */ 319 | p.addFrame = function(frameNum) 320 | { 321 | if (!inObject(frameNum, this.framesFound)) 322 | { 323 | this.framesFound.push(frameNum); 324 | } 325 | }; 326 | 327 | /** 328 | * Add a frame range to already found frames 329 | * @method addFrameRange 330 | * @param {int} startFrame The starting frame index 331 | * @param {int} endFrame The ending frame index 332 | * @param {Boolean} [bLoop=false] If the graphic is set to loop 333 | */ 334 | p.addFrameRange = function(startFrame, endFrame, bLoop) 335 | { 336 | var frameCount = this.frameCount; 337 | for(var i=startFrame; i < endFrame; i++) 338 | { 339 | if (i < frameCount) 340 | { 341 | this.addFrame(i); 342 | } 343 | else if (i >= frameCount && bLoop) 344 | { 345 | this.addFrame(i % frameCount); 346 | } 347 | } 348 | }; 349 | 350 | /** 351 | * utility function to check if a value is in an object or array 352 | * @method inObject 353 | * @private 354 | * @static 355 | * @param {*} needle The value to check 356 | * @param {Object|Array} haystack The object to check in 357 | * @param {Boolean} If the needle is in the haystack 358 | */ 359 | var inObject = function(needle, haystack) 360 | { 361 | for(var k in haystack ) 362 | { 363 | if ( haystack[k] == needle ) { return true; } 364 | } 365 | return false; 366 | }; 367 | 368 | // Start 369 | new OptimizeGraphics(); 370 | 371 | }()); -------------------------------------------------------------------------------- /src/Commands/Optimizing/HTML5 Canvas Size Report.jsfl: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | /** 4 | * Generate a filesize report for HTML Canvas FLAs 5 | * @class FilesizeReport 6 | */ 7 | var FilesizeReport = function() 8 | { 9 | var dom = fl.getDocumentDOM(); 10 | 11 | if (!dom) 12 | { 13 | alert("No document opened"); 14 | return; 15 | } 16 | 17 | // Check for canvas 18 | if (dom.type != "htmlcanvas") 19 | { 20 | alert("Only works with HTML Canvas FLA documents"); 21 | return; 22 | } 23 | 24 | // Make sure FLA is saved 25 | if (!dom.pathURI) 26 | { 27 | alert("Must save the document first"); 28 | return; 29 | } 30 | 31 | // The folder URI 32 | this.folder = dirname(dom.pathURI); 33 | 34 | // If the FLA is uncompressed use the parent 35 | // folder for relative publish pathing 36 | if (/\.xfl$/i.test(dom.pathURI)) 37 | { 38 | this.folder = dirname(this.folder); 39 | } 40 | 41 | // Add the trailing slash 42 | this.folder += "/"; 43 | 44 | // Clear output 45 | fl.outputPanel.clear(); 46 | 47 | // We need to get some information from the publish profile 48 | // this includes the path to the images, the libs namespace 49 | // and the export file name 50 | var profile = dom.exportPublishProfileString(), 51 | xml = new XML(profile), 52 | properties = xml.PublishProperties, 53 | name; 54 | 55 | for (var i = 0, len = properties.length(); i < len; i++) 56 | { 57 | name = properties[i].attribute('name'); 58 | if (name == "JavaScript/HTML") 59 | { 60 | var props = properties.Property; 61 | var filename; 62 | var libNS; 63 | 64 | for (var j = 0, l = props.length(); j < l; j++) 65 | { 66 | name = props[j].attribute('name'); 67 | 68 | if (name == "filename") filename = props[j]; 69 | if (name == "libNS") libNS = props[j]; 70 | } 71 | 72 | // Save the publis file contents 73 | this.publishFile = FLfile.read(this.folder + filename); 74 | 75 | if (!FLfile.exists(this.folder + filename)) 76 | { 77 | alert("Publish the FLA before running."); 78 | return; 79 | } 80 | var assets = this.assetReport(filename, libNS); 81 | var media = this.mediaReport(filename); 82 | 83 | logTitle("SUMMARY"); 84 | 85 | fl.trace(this.tableLayout([ 86 | { 87 | type: "JavaScript Library Assets", 88 | size: filesize(this.totalSize) 89 | }, 90 | { 91 | type: "JavaScript Overhead", 92 | size: filesize(this.assetSize - this.totalSize) 93 | }, 94 | { 95 | type: "Number of Library Assets", 96 | size: this.numAssets 97 | }, 98 | { 99 | type: "External Media Files", 100 | size: filesize(this.mediaSize) 101 | }, 102 | { 103 | type: "Total JavaScript & External", 104 | size: filesize(this.mediaSize + this.assetSize) 105 | }], ['type', 'size']) 106 | ); 107 | 108 | 109 | logTitle("JAVASCRIPT LIBRARY ASSETS"); 110 | fl.trace(assets); 111 | 112 | logTitle("EXTERNAL MEDIA FILES"); 113 | fl.trace(media); 114 | 115 | return; 116 | } 117 | } 118 | alert("Unable to find an HTML publishing target"); 119 | }; 120 | 121 | // Reference to the prototype 122 | var p = FilesizeReport.prototype; 123 | 124 | /** 125 | * Generate a filesize report of media (images/sounds) 126 | * @method mediaReport 127 | * @param {filename} filename The relative or absolute path to export JS file 128 | */ 129 | p.mediaReport = function(filename) 130 | { 131 | this.mediaSize = 0; 132 | 133 | var parent = dirname(this.folder + filename); 134 | 135 | var manifestRegExp = /manifest\: (\[[\s\S]*?\])/m; 136 | var match = manifestRegExp.exec(this.publishFile); 137 | 138 | if (match) 139 | { 140 | eval("var manifest = " + match[1]); // jshint ignore:line 141 | 142 | if (manifest && manifest.length) 143 | { 144 | var report = [], file, src; 145 | for (var i = 0; i < manifest.length; i++) 146 | { 147 | file = manifest[i]; 148 | src = parent + "/" + manifest[i].src; 149 | 150 | // Ignore files we can't find 151 | if (!FLfile.exists(src)) continue; 152 | 153 | var bytes = FLfile.getSize(src); 154 | 155 | report.push({ 156 | file: file.src.substr(file.src.lastIndexOf("/") + 1), 157 | bytes: bytes, 158 | size: filesize(bytes) 159 | 160 | }); 161 | this.mediaSize += bytes; 162 | } 163 | 164 | report.sort(sortByBytes); 165 | 166 | return this.tableLayout(report, ['file', 'size']); 167 | } 168 | } 169 | return ""; 170 | }; 171 | 172 | /** 173 | * Generate a filesize report 174 | * @method assetReport 175 | * @param {filename} filename The relative or absolute path to export JS file 176 | * @param {string} libNS Libraries namespace 177 | */ 178 | p.assetReport = function(filename, libNS) 179 | { 180 | // Total file size of export 181 | var assetSize = FLfile.getSize(this.folder + filename); 182 | 183 | // Looking for something that starts with "(lib.* = function(*) {" 184 | // and ends with "p.nominalBounds = new cjs.Rectangle(*);" which 185 | // is the entire library asset. 186 | var assetRegExp = new RegExp("\\(" + libNS + "\\.([\\w\\d\\-]+) \\= function\\([\\w\\,]*\\) \\{[\\s\\S]*?p.nominalBounds \\= (null|new (cjs\\.)?Rectangle\\([\\-\\d\\.\\,]+\\))\\;", "gm"); 187 | 188 | // Get just the name of the asset 189 | var assetNameRegExp = new RegExp("^\\(" + libNS + "\\.([\\w\\d\\-]+)"); 190 | 191 | // Get the collection of matched assets 192 | var assets = this.publishFile.match(assetRegExp), 193 | report = [], 194 | totalSize = 0, 195 | bytes; 196 | 197 | for (var i = 0; i < assets.length; i++) 198 | { 199 | bytes = assets[i].length; 200 | totalSize += bytes; 201 | 202 | report.push({ 203 | name: assetNameRegExp.exec(assets[i])[1], 204 | size: filesize(bytes), 205 | bytes: bytes, 206 | percent: percentage(bytes / assetSize) 207 | }); 208 | } 209 | 210 | report.sort(sortByBytes); 211 | 212 | this.assetSize = assetSize; 213 | this.totalSize = totalSize; 214 | this.numAssets = assets.length; 215 | 216 | return this.tableLayout(report, ['name', 'size', 'percent']); 217 | }; 218 | 219 | /** 220 | * Get the director path from another path 221 | * @method dirname 222 | * @private 223 | * @param {string} path The incoming path 224 | * @return {string} The base parent folder 225 | */ 226 | function dirname(path) 227 | { 228 | return path.replace(/\\/g, '/') 229 | .replace(/\/[^\/]*\/?$/, ''); 230 | } 231 | 232 | /** 233 | * Sort function by the bytes size 234 | * @method sortByBytes 235 | * @private 236 | * @param {object} a asset entry 237 | * @param {object} b asset entry 238 | * @return {int} Comparitor output 239 | */ 240 | function sortByBytes(a, b) 241 | { 242 | return a.bytes < b.bytes; 243 | } 244 | 245 | /** 246 | * Convert a number to a percentage 247 | * @method 248 | * @private 249 | * @param {number} num A number from 0 - 1 250 | * @return {string} The percentage with % sign 251 | */ 252 | function percentage(num) 253 | { 254 | return Math.round(num * 10000) / 100 + "%"; 255 | } 256 | 257 | /** 258 | * Make a filesize in bytes readable 259 | * @method filesize 260 | * @private 261 | * @param {int} fileSizeInBytes The bytes 262 | * @return {string} The readable filesize 263 | */ 264 | function filesize(fileSizeInBytes) 265 | { 266 | var i = -1; 267 | var byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']; 268 | do { 269 | fileSizeInBytes = fileSizeInBytes / 1024; 270 | i++; 271 | } while (fileSizeInBytes > 1024); 272 | return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i]; 273 | } 274 | 275 | /** 276 | * The amount of padding in the output report 277 | * @property {int} PADDING 278 | * @static 279 | * @readOnly 280 | * @default 4 281 | */ 282 | var PADDING = 4; 283 | 284 | /** 285 | * Generate an ASCII table 286 | * @method tableLayout 287 | * @private 288 | * @param {Array} header Collection of header labels 289 | * @param {Array} entries Collection of array entries 290 | * @return {String} The full ASCII table 291 | */ 292 | p.tableLayout = function(entries, headers) 293 | { 294 | var finalStr = ""; 295 | var layout = {}; 296 | var label; 297 | 298 | // Set the minimum values 299 | for (i = 0; i < headers.length; ++i) 300 | { 301 | label = headers[i]; 302 | layout[label] = label.length + PADDING; 303 | } 304 | 305 | // Measure the layout for prettier display 306 | for (i = 0; i < entries.length; ++i) 307 | { 308 | for (label in layout) 309 | { 310 | if (label == "contains") continue; 311 | 312 | layout[label] = Math.max( 313 | layout[label], 314 | String(entries[i][label]).length + PADDING 315 | ); 316 | } 317 | } 318 | 319 | // Show the table headers 320 | res = ""; 321 | var total = 0; 322 | for (label in layout) 323 | { 324 | if (label == "contains") continue; 325 | res += fontLayout(label, layout[label]); 326 | total += layout[label]; 327 | } 328 | finalStr += res + "\n"; 329 | 330 | // Place a divider 331 | var hr = ""; 332 | for (i = 0; i < total; ++i) 333 | { 334 | hr += "-"; 335 | } 336 | 337 | finalStr += hr + "\n"; 338 | 339 | // Prep the entries 340 | for (i = 0; i < entries.length; ++i) 341 | { 342 | res = ""; 343 | for (label in layout) 344 | { 345 | if (label == "contains") continue; 346 | res += fontLayout(String(entries[i][label]), layout[label]); 347 | } 348 | finalStr += res + "\n"; 349 | } 350 | 351 | finalStr += hr + "\n"; 352 | return "\n" + finalStr; 353 | }; 354 | 355 | /** 356 | * Trace out a title fancy style 357 | * @method logTitle 358 | * @private 359 | * @param {string} str The title name 360 | */ 361 | var logTitle = function(str) 362 | { 363 | var hr = ""; 364 | var size = str.length + PADDING; 365 | while(hr.length < size) 366 | { 367 | hr += "-"; 368 | } 369 | 370 | fl.trace("\n+" + hr + "+"); 371 | fl.trace("| " + str + " |"); 372 | fl.trace("+" + hr + "+"); 373 | }; 374 | 375 | /** 376 | * Prepend string with text based on a fixed length 377 | * @method fontLayout 378 | * @private 379 | * @param {String} str The string to pad 380 | * @param {int} len The length of the output string 381 | * @return {string} The formatted output string 382 | */ 383 | var fontLayout = function(str, len) 384 | { 385 | while(str.length < len) 386 | { 387 | str = str + " "; 388 | } 389 | return str; 390 | }; 391 | 392 | // Run command 393 | new FilesizeReport(); 394 | 395 | }()); -------------------------------------------------------------------------------- /src/JSFLLibraries/XULWindow.jsfl: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a XUL panel in JSFL. The object can be set up and then used to invoke a XUL panel without writing an actual XUL file. 3 | * The XML can be generated purely in JSFL and passed in, or you can use convenience methods to add various controls without having 4 | * to writing XML strings in JSFL. 5 | * 6 | *

For full-on XUL reference, the Mozilla document is here: 7 | * https://developer.mozilla.org/en/XUL_Reference. Note that Flash supports a very small subset of this.

8 | * 9 | **/ 10 | 11 | /** 12 | * Constructor 13 | * @param title The title of the window. This is only used if you own XML string is not passed in to the create 14 | * method. 15 | * @param buttons The remaining arguments are Strings of types of buttons to include. For convenience, use the static 16 | * constants XULWindow.ACCEPT and XULWindow.CANCEL. This is only used if you own XML string is not passed in to 17 | * the create method. 18 | **/ 19 | var XULWindow = function(title) 20 | { 21 | this._title = title || "Options"; 22 | 23 | var buttons = []; 24 | var iLen = arguments.length; 25 | for (var i = 1; i < iLen; i++) 26 | { 27 | buttons.push(arguments[i]); 28 | } 29 | 30 | this._columns = false; 31 | this._xml = ''; 32 | this._tab = 1; 33 | 34 | /** 35 | * Create muliple columns 36 | * @method columns 37 | * @param {int} cols* The flex amounts of columns 38 | * @return {XULWindow} The instance, for chaining 39 | */ 40 | this.columns = function() 41 | { 42 | this._columns = arguments; 43 | return this; 44 | }; 45 | 46 | /** 47 | * Creates a XUL window panel. It is attached to the current document 48 | * 49 | * @param xul A String of raw XUL to create. If null, the window will be created according to the various control 50 | * creation methods. If not null, any other method calls would be ignored and the passed-in XUL is used. 51 | * @return An object containing information about the user input. The object will have a property called dismiss 52 | * that contains the name of the button that was clicked (either XULWindow.ACCEPT or 53 | * <code>XULWindow.CANCEL). It will 54 | * also have other properties, named after the various controls' id values. The value contained by these properties 55 | * will be the value associated with the input of the control. 56 | **/ 57 | this.create = function(xul) 58 | { 59 | var doc = fl.getDocumentDOM(); 60 | if (!doc) 61 | { 62 | alert("There is no open document. XUL Windows need to be attached to a document. [XULWindow::create()]"); 63 | return; 64 | } 65 | 66 | var xulFilePath = fl.configURI + escape(this._title) + ".xul"; 67 | 68 | // If there's no XUL supplied 69 | if (!xul) 70 | { 71 | // Create the dialog 72 | xul = ''; 73 | 74 | // Create the column layout 75 | if (this._columns && this._columns.length > 1) 76 | { 77 | xul += ''; 78 | 79 | // Add the columns 80 | for (var i = 0; i < this._columns.length; i++) 81 | { 82 | xul += ''; 83 | } 84 | 85 | // Add the closing layout plus the dialog contents 86 | xul += '' + this._xml + ''; 87 | } 88 | else 89 | { 90 | // Just add the contents 91 | xul += this._xml; 92 | } 93 | xul += ''; 94 | } 95 | 96 | FLfile.write(xulFilePath, xul); 97 | var options = fl.getDocumentDOM().xmlPanel(xulFilePath); 98 | FLfile.remove(xulFilePath); 99 | 100 | return options; 101 | }; 102 | 103 | /** 104 | * Creates a check box control with a label. 105 | * @method addCheckbox 106 | * @param label String for the label next to the check box. 107 | * @param id String for the id of the checkbox. This is the name of the property on the returned object with data in it 108 | * @param checked Whether initially checked or not. 109 | * @return {XULWindow} The instance, for chaining 110 | */ 111 | this.addCheckbox = function(label, id, checked) 112 | { 113 | this._xml += ''; 114 | return this; 115 | }; 116 | 117 | /** 118 | * Start a row element 119 | * @method openRow 120 | * @return {XULWindow} The instance, for chaining 121 | */ 122 | this.openRow = function() 123 | { 124 | if (!this._columns) 125 | { 126 | alert("XULWindow::openRow() requires XULWindow::columns() to be set."); 127 | return this; 128 | } 129 | this._xml += ''; 130 | return this; 131 | }; 132 | 133 | /** 134 | * Close a row element 135 | * @method closeRow 136 | * @return {XULWindow} The instance, for chaining 137 | */ 138 | this.closeRow = function() 139 | { 140 | if (!this._columns) 141 | { 142 | alert("XULWindow::closeRow() requires XULWindow::columns() to be set."); 143 | return this; 144 | } 145 | this._xml += ''; 146 | return this; 147 | }; 148 | 149 | /** 150 | * Add a spacer 151 | * @method addSpacer 152 | * @return {XULWindow} The instance, for chaining 153 | */ 154 | this.addSpacer = function() 155 | { 156 | this._xml += ''; 157 | return this; 158 | }; 159 | 160 | /** 161 | * Add a separator 162 | * @method addSeparator 163 | * @return {XULWindow} The instance, for chaining 164 | */ 165 | this.addSeparator = function() 166 | { 167 | this._xml += ''; 168 | return this; 169 | }; 170 | 171 | /** 172 | * Opens an <hbox> nodes so that all controls added after this method call are contained within the hbox. Should 173 | * be ultimately terminated with closeHBox 174 | * 175 | * @see #closeHBox 176 | * @return {XULWindow} The instance, for chaining 177 | */ 178 | this.openHBox = function() 179 | { 180 | this._xml += ""; 181 | return this; 182 | }; 183 | 184 | /** 185 | * Closes a previously open <hbox>, enclosing all intermediate controls within an hbox node. 186 | * 187 | * @see #openHBox 188 | * @return {XULWindow} The instance, for chaining 189 | */ 190 | this.closeHBox = function() 191 | { 192 | this._xml += ""; 193 | return this; 194 | }; 195 | 196 | /** 197 | * Opens an <vbox> nodes so that all controls added after this method call are contained within the vbox. Should 198 | * be ultimately terminated with closeVBox 199 | * @method openVBox 200 | * @see #closeVBox 201 | * @return {XULWindow} The instance, for chaining 202 | */ 203 | this.openVBox = function() 204 | { 205 | this._xml += ""; 206 | return this; 207 | }; 208 | 209 | /** 210 | * Closes a previously open <vbox>, enclosing all intermediate controls within an vbox node. 211 | * @method closeVBox 212 | * @see #openVBox 213 | * @return {XULWindow} The instance, for chaining 214 | */ 215 | this.closeVBox = function() 216 | { 217 | this._xml += ""; 218 | return this; 219 | }; 220 | 221 | /** 222 | * @private 223 | * Adds a button to the panel, but I assume there might be more involved in getting one to do something than we can realistically 224 | * accomplish here. 225 | * @method addButton 226 | * @return {XULWindow} The instance, for chaining 227 | */ 228 | this.addButton = function(label, id, accessKey, autocheck) 229 | { 230 | this._xml += '