/*
* SpriteSheetBuilder
* Visit http://createjs.com/ for documentation, updates and examples.
*
* Copyright (c) 2010 gskinner.com, inc.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* @module EaselJS
*/
// namespace:
this.createjs = this.createjs||{};
(function() {
"use strict";
// constructor:
/**
* The SpriteSheetBuilder allows you to generate {{#crossLink "SpriteSheet"}}{{/crossLink}} instances at run time
* from any display object. This can allow you to maintain your assets as vector graphics (for low file size), and
* render them at run time as SpriteSheets for better performance.
*
* SpriteSheets can be built either synchronously, or asynchronously, so that large SpriteSheets can be generated
* without locking the UI.
*
* Note that the "images" used in the generated SpriteSheet are actually canvas elements, and that they will be
* sized to the nearest power of 2 up to the value of {{#crossLink "SpriteSheetBuilder/maxWidth:property"}}{{/crossLink}}
* or {{#crossLink "SpriteSheetBuilder/maxHeight:property"}}{{/crossLink}}.
* @class SpriteSheetBuilder
* @param {Number} [framerate=0] The {{#crossLink "SpriteSheet/framerate:property"}}{{/crossLink}} of
* {{#crossLink "SpriteSheet"}}{{/crossLink}} instances that are created.
* @extends EventDispatcher
* @constructor
**/
function SpriteSheetBuilder(framerate) {
this.EventDispatcher_constructor();
// public properties:
/**
* The maximum width for the images (not individual frames) in the generated SpriteSheet. It is recommended to
* use a power of 2 for this value (ex. 1024, 2048, 4096). If the frames cannot all fit within the max
* dimensions, then additional images will be created as needed.
* @property maxWidth
* @type Number
* @default 2048
*/
this.maxWidth = 2048;
/**
* The maximum height for the images (not individual frames) in the generated SpriteSheet. It is recommended to
* use a power of 2 for this value (ex. 1024, 2048, 4096). If the frames cannot all fit within the max
* dimensions, then additional images will be created as needed.
* @property maxHeight
* @type Number
* @default 2048
**/
this.maxHeight = 2048;
/**
* The SpriteSheet that was generated. This will be null before a build is completed successfully.
* @property spriteSheet
* @type SpriteSheet
**/
this.spriteSheet = null;
/**
* The scale to apply when drawing all frames to the SpriteSheet. This is multiplied against any scale specified
* in the addFrame call. This can be used, for example, to generate a SpriteSheet at run time that is tailored
* to the a specific device resolution (ex. tablet vs mobile).
* @property scale
* @type Number
* @default 1
**/
this.scale = 1;
/**
* The padding to use between frames. This is helpful to preserve antialiasing on drawn vector content.
* @property padding
* @type Number
* @default 1
**/
this.padding = 1;
/**
* A number from 0.01 to 0.99 that indicates what percentage of time the builder can use. This can be
* thought of as the number of seconds per second the builder will use. For example, with a timeSlice value of 0.3,
* the builder will run 20 times per second, using approximately 15ms per build (30% of available time, or 0.3s per second).
* Defaults to 0.3.
* @property timeSlice
* @type Number
* @default 0.3
**/
this.timeSlice = 0.3;
/**
* A value between 0 and 1 that indicates the progress of a build, or -1 if a build has not
* been initiated.
* @property progress
* @type Number
* @default -1
* @readonly
*/
this.progress = -1;
/**
* A {{#crossLink "SpriteSheet/framerate:property"}}{{/crossLink}} value that will be passed to new {{#crossLink "SpriteSheet"}}{{/crossLink}} instances that are
* created. If no framerate is specified (or it is 0), then SpriteSheets will use the {{#crossLink "Ticker"}}{{/crossLink}}
* framerate.
* @property framerate
* @type Number
* @default 0
*/
this.framerate = framerate || 0;
// private properties:
/**
* @property _frames
* @protected
* @type Array
**/
this._frames = [];
/**
* @property _animations
* @protected
* @type Array
**/
this._animations = {};
/**
* @property _data
* @protected
* @type Array
**/
this._data = null;
/**
* @property _nextFrameIndex
* @protected
* @type Number
**/
this._nextFrameIndex = 0;
/**
* @property _index
* @protected
* @type Number
**/
this._index = 0;
/**
* @property _timerID
* @protected
* @type Number
**/
this._timerID = null;
/**
* @property _scale
* @protected
* @type Number
**/
this._scale = 1;
}
var p = createjs.extend(SpriteSheetBuilder, createjs.EventDispatcher);
/**
* <strong>REMOVED</strong>. Removed in favor of using `MySuperClass_constructor`.
* See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}}
* for details.
*
* There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance.
*
* @method initialize
* @protected
* @deprecated
*/
// p.initialize = function() {}; // searchable for devs wondering where it is.
// constants:
SpriteSheetBuilder.ERR_DIMENSIONS = "frame dimensions exceed max spritesheet dimensions";
SpriteSheetBuilder.ERR_RUNNING = "a build is already running";
// events:
/**
* Dispatched when a build completes.
* @event complete
* @param {Object} target The object that dispatched the event.
* @param {String} type The event type.
* @since 0.6.0
*/
/**
* Dispatched when an asynchronous build has progress.
* @event progress
* @param {Object} target The object that dispatched the event.
* @param {String} type The event type.
* @param {Number} progress The current progress value (0-1).
* @since 0.6.0
*/
// public methods:
/**
* Adds a frame to the {{#crossLink "SpriteSheet"}}{{/crossLink}}. Note that the frame will not be drawn until you
* call {{#crossLink "SpriteSheetBuilder/build"}}{{/crossLink}} method. The optional setup params allow you to have
* a function run immediately before the draw occurs. For example, this allows you to add a single source multiple
* times, but manipulate it or its children to change it to generate different frames.
*
* Note that the source's transformations (x, y, scale, rotate, alpha) will be ignored, except for regX/Y. To apply
* transforms to a source object and have them captured in the SpriteSheet, simply place it into a {{#crossLink "Container"}}{{/crossLink}}
* and pass in the Container as the source.
* @method addFrame
* @param {DisplayObject} source The source {{#crossLink "DisplayObject"}}{{/crossLink}} to draw as the frame.
* @param {Rectangle} [sourceRect] A {{#crossLink "Rectangle"}}{{/crossLink}} defining the portion of the
* source to draw to the frame. If not specified, it will look for a `getBounds` method, bounds property, or
* `nominalBounds` property on the source to use. If one is not found, the frame will be skipped.
* @param {Number} [scale=1] Optional. The scale to draw this frame at. Default is 1.
* @param {Function} [setupFunction] A function to call immediately before drawing this frame. It will be called with two parameters: the source, and setupData.
* @param {Object} [setupData] Arbitrary setup data to pass to setupFunction as the second parameter.
* @return {Number} The index of the frame that was just added, or null if a sourceRect could not be determined.
**/
p.addFrame = function(source, sourceRect, scale, setupFunction, setupData) {
if (this._data) { throw SpriteSheetBuilder.ERR_RUNNING; }
var rect = sourceRect||source.bounds||source.nominalBounds;
if (!rect&&source.getBounds) { rect = source.getBounds(); }
if (!rect) { return null; }
scale = scale||1;
return this._frames.push({source:source, sourceRect:rect, scale:scale, funct:setupFunction, data:setupData, index:this._frames.length, height:rect.height*scale})-1;
};
/**
* Adds an animation that will be included in the created {{#crossLink "SpriteSheet"}}{{/crossLink}}.
* @method addAnimation
* @param {String} name The name for the animation.
* @param {Array} frames An array of frame indexes that comprise the animation. Ex. [3,6,5] would describe an animation
* that played frame indexes 3, 6, and 5 in that order.
* @param {String} [next] Specifies the name of the animation to continue to after this animation ends. You can
* also pass false to have the animation stop when it ends. By default it will loop to the start of the same animation.
* @param {Number} [speed] Specifies a frame advance speed for this animation. For example, a value of 0.5 would
* cause the animation to advance every second tick. Note that earlier versions used `frequency` instead, which had
* the opposite effect.
**/
p.addAnimation = function(name, frames, next, speed) {
if (this._data) { throw SpriteSheetBuilder.ERR_RUNNING; }
this._animations[name] = {frames:frames, next:next, speed:speed};
};
/**
* This will take a {{#crossLink "MovieClip"}}{{/crossLink}} instance, and add its frames and labels to this
* builder. Labels will be added as an animation running from the label index to the next label. For example, if
* there is a label named "foo" at frame 0 and a label named "bar" at frame 10, in a MovieClip with 15 frames, it
* will add an animation named "foo" that runs from frame index 0 to 9, and an animation named "bar" that runs from
* frame index 10 to 14.
*
* Note that this will iterate through the full MovieClip with {{#crossLink "MovieClip/actionsEnabled:property"}}{{/crossLink}}
* set to `false`, ending on the last frame.
* @method addMovieClip
* @param {MovieClip} source The source MovieClip instance to add to the SpriteSheet.
* @param {Rectangle} [sourceRect] A {{#crossLink "Rectangle"}}{{/crossLink}} defining the portion of the source to
* draw to the frame. If not specified, it will look for a {{#crossLink "DisplayObject/getBounds"}}{{/crossLink}}
* method, `frameBounds` Array, `bounds` property, or `nominalBounds` property on the source to use. If one is not
* found, the MovieClip will be skipped.
* @param {Number} [scale=1] The scale to draw the movie clip at.
* @param {Function} [setupFunction] A function to call immediately before drawing each frame. It will be called
* with three parameters: the source, setupData, and the frame index.
* @param {Object} [setupData] Arbitrary setup data to pass to setupFunction as the second parameter.
* @param {Function} [labelFunction] This method will be called for each MovieClip label that is added with four
* parameters: the label name, the source MovieClip instance, the starting frame index (in the movieclip timeline)
* and the end index. It must return a new name for the label/animation, or `false` to exclude the label.
**/
p.addMovieClip = function(source, sourceRect, scale, setupFunction, setupData, labelFunction) {
if (this._data) { throw SpriteSheetBuilder.ERR_RUNNING; }
var rects = source.frameBounds;
var rect = sourceRect||source.bounds||source.nominalBounds;
if (!rect&&source.getBounds) { rect = source.getBounds(); }
if (!rect && !rects) { return; }
var i, l, baseFrameIndex = this._frames.length;
var duration = source.timeline.duration;
for (i=0; i<duration; i++) {
var r = (rects&&rects[i]) ? rects[i] : rect;
this.addFrame(source, r, scale, this._setupMovieClipFrame, {i:i, f:setupFunction, d:setupData});
}
var labels = source.timeline._labels;
var lbls = [];
for (var n in labels) {
lbls.push({index:labels[n], label:n});
}
if (lbls.length) {
lbls.sort(function(a,b){ return a.index-b.index; });
for (i=0,l=lbls.length; i<l; i++) {
var label = lbls[i].label;
var start = baseFrameIndex+lbls[i].index;
var end = baseFrameIndex+((i == l-1) ? duration : lbls[i+1].index);
var frames = [];
for (var j=start; j<end; j++) { frames.push(j); }
if (labelFunction) {
label = labelFunction(label, source, start, end);
if (!label) { continue; }
}
this.addAnimation(label, frames, true); // for now, this loops all animations.
}
}
};
/**
* Builds a {{#crossLink "SpriteSheet"}}{{/crossLink}} instance based on the current frames.
* @method build
* @return {SpriteSheet} The created SpriteSheet instance, or null if a build is already running or an error
* occurred.
**/
p.build = function() {
if (this._data) { throw SpriteSheetBuilder.ERR_RUNNING; }
this._startBuild();
while (this._drawNext()) {}
this._endBuild();
return this.spriteSheet;
};
/**
* Asynchronously builds a {{#crossLink "SpriteSheet"}}{{/crossLink}} instance based on the current frames. It will
* run 20 times per second, using an amount of time defined by `timeSlice`. When it is complete it will call the
* specified callback.
* @method buildAsync
* @param {Number} [timeSlice] Sets the timeSlice property on this instance.
**/
p.buildAsync = function(timeSlice) {
if (this._data) { throw SpriteSheetBuilder.ERR_RUNNING; }
this.timeSlice = timeSlice;
this._startBuild();
var _this = this;
this._timerID = setTimeout(function() { _this._run(); }, 50-Math.max(0.01, Math.min(0.99, this.timeSlice||0.3))*50);
};
/**
* Stops the current asynchronous build.
* @method stopAsync
**/
p.stopAsync = function() {
clearTimeout(this._timerID);
this._data = null;
};
/**
* SpriteSheetBuilder instances cannot be cloned.
* @method clone
**/
p.clone = function() {
throw("SpriteSheetBuilder cannot be cloned.");
};
/**
* Returns a string representation of this object.
* @method toString
* @return {String} a string representation of the instance.
**/
p.toString = function() {
return "[SpriteSheetBuilder]";
};
// private methods:
/**
* @method _startBuild
* @protected
**/
p._startBuild = function() {
var pad = this.padding||0;
this.progress = 0;
this.spriteSheet = null;
this._index = 0;
this._scale = this.scale;
var dataFrames = [];
this._data = {
images: [],
frames: dataFrames,
framerate: this.framerate,
animations: this._animations // TODO: should we "clone" _animations in case someone adds more animations after a build?
};
var frames = this._frames.slice();
frames.sort(function(a,b) { return (a.height<=b.height) ? -1 : 1; });
if (frames[frames.length-1].height+pad*2 > this.maxHeight) { throw SpriteSheetBuilder.ERR_DIMENSIONS; }
var y=0, x=0;
var img = 0;
while (frames.length) {
var o = this._fillRow(frames, y, img, dataFrames, pad);
if (o.w > x) { x = o.w; }
y += o.h;
if (!o.h || !frames.length) {
var canvas = createjs.createCanvas?createjs.createCanvas():document.createElement("canvas");
canvas.width = this._getSize(x,this.maxWidth);
canvas.height = this._getSize(y,this.maxHeight);
this._data.images[img] = canvas;
if (!o.h) {
x=y=0;
img++;
}
}
}
};
/**
* @method _setupMovieClipFrame
* @protected
* @return {Number} The width & height of the row.
**/
p._setupMovieClipFrame = function(source, data) {
var ae = source.actionsEnabled;
source.actionsEnabled = false;
source.gotoAndStop(data.i);
source.actionsEnabled = ae;
data.f&&data.f(source, data.d, data.i);
};
/**
* @method _getSize
* @protected
* @return {Number} The width & height of the row.
**/
p._getSize = function(size,max) {
var pow = 4;
while (Math.pow(2,++pow) < size){}
return Math.min(max,Math.pow(2,pow));
};
/**
* @method _fillRow
* @param {Array} frames
* @param {Number} y
* @param {HTMLImageElement} img
* @param {Object} dataFrames
* @param {Number} pad
* @protected
* @return {Number} The width & height of the row.
**/
p._fillRow = function(frames, y, img, dataFrames, pad) {
var w = this.maxWidth;
var maxH = this.maxHeight;
y += pad;
var h = maxH-y;
var x = pad;
var height = 0;
for (var i=frames.length-1; i>=0; i--) {
var frame = frames[i];
var sc = this._scale*frame.scale;
var rect = frame.sourceRect;
var source = frame.source;
var rx = Math.floor(sc*rect.x-pad);
var ry = Math.floor(sc*rect.y-pad);
var rh = Math.ceil(sc*rect.height+pad*2);
var rw = Math.ceil(sc*rect.width+pad*2);
if (rw > w) { throw SpriteSheetBuilder.ERR_DIMENSIONS; }
if (rh > h || x+rw > w) { continue; }
frame.img = img;
frame.rect = new createjs.Rectangle(x,y,rw,rh);
height = height || rh;
frames.splice(i,1);
dataFrames[frame.index] = [x,y,rw,rh,img,Math.round(-rx+sc*source.regX-pad),Math.round(-ry+sc*source.regY-pad)];
x += rw;
}
return {w:x, h:height};
};
/**
* @method _endBuild
* @protected
**/
p._endBuild = function() {
this.spriteSheet = new createjs.SpriteSheet(this._data);
this._data = null;
this.progress = 1;
this.dispatchEvent("complete");
};
/**
* @method _run
* @protected
**/
p._run = function() {
var ts = Math.max(0.01, Math.min(0.99, this.timeSlice||0.3))*50;
var t = (new Date()).getTime()+ts;
var complete = false;
while (t > (new Date()).getTime()) {
if (!this._drawNext()) { complete = true; break; }
}
if (complete) {
this._endBuild();
} else {
var _this = this;
this._timerID = setTimeout(function() { _this._run(); }, 50-ts);
}
var p = this.progress = this._index/this._frames.length;
if (this.hasEventListener("progress")) {
var evt = new createjs.Event("progress");
evt.progress = p;
this.dispatchEvent(evt);
}
};
/**
* @method _drawNext
* @protected
* @return Boolean Returns false if this is the last draw.
**/
p._drawNext = function() {
var frame = this._frames[this._index];
var sc = frame.scale*this._scale;
var rect = frame.rect;
var sourceRect = frame.sourceRect;
var canvas = this._data.images[frame.img];
var ctx = canvas.getContext("2d");
frame.funct&&frame.funct(frame.source, frame.data);
ctx.save();
ctx.beginPath();
ctx.rect(rect.x, rect.y, rect.width, rect.height);
ctx.clip();
ctx.translate(Math.ceil(rect.x-sourceRect.x*sc), Math.ceil(rect.y-sourceRect.y*sc));
ctx.scale(sc,sc);
frame.source.draw(ctx); // display object will draw itself.
ctx.restore();
return (++this._index) < this._frames.length;
};
createjs.SpriteSheetBuilder = createjs.promote(SpriteSheetBuilder, "EventDispatcher");
}());