/** * @module PIXI Animation * @namespace springroll.pixi * @requires Core, PIXI Display, Animation */ (function(undefined) { "use strict"; var Sprite = include('PIXI.Sprite'), Texture = include("PIXI.Texture"); /** * A class similar to PIXI.extras.MovieClip, but made to play animations _exclusively_ using * the Animator, with data exported by the BitmapMovieClip exporter. * * Format for AdvancedMovieClip data (the same as BitmapMovieClip): * * { * fps:30, * labels: * { * animStart:0, * animStart_loop:15 * }, * origin:{ x: 20, y:30 }, * frames: * [ * { * name:"myAnim#", * min:1, * max:20, * digits:4 * } * ], * scale:1 * } * * The example object describes a 30 fps animation that is 20 frames long, and was originally * myAnim0001.png->myAnim0020.png, with frame labels on the first and 16th frames. 'digits' is * optional, and defaults to 4. * * @class AdvancedMovieClip * @extends PIXI.Sprite * @constructor * @param {Object} [data] Initialization data * @param {int} [data.fps] Framerate to play the movieclip at. Omitting this will use the * current framerate. * @param {Object} [data.labels] A dictionary of the labels in the movieclip to assist in * playing animations. * @param {Object} [data.origin={x:0,y:0}] The origin of the movieclip. * @param {Array} [data.frames] An array of frame sequences to pull from the texture atlas. * @param {String} [data.frames.name] The name to use for the frame sequence. This should * include a "#" to be replaced with the image number. * @param {int} [data.frames.min] The first frame number in the frame sequence. * @param {int} [data.frames.max] The last frame number in the frame sequence. * @param {int} [data.frames.digits=4] The maximum number of digits in the names of the frames, * e.g. myAnim0001 has 4 digits. * @param {Number} [data.scale=1] The scale at which the art was exported, e.g. a scale of 1.4 * means the art was increased in size to 140% before exporting * and should be scaled back down before drawing to the screen. * @param {springroll.pixi.TextureAtlas} [atlas] A TextureAtlas to pull frames from. If omitted, * frames are pulled from Pixi's global texture * cache. */ var AdvancedMovieClip = function(data, atlas) { Sprite.call(this); //==== Public properties ===== /** * The current frame of the movieclip. * @property currentFrame * @type Number * @default 0 * @readonly */ this.currentFrame = 0; //==== Private properties ===== /** * The speed at which the AdvancedMovieClip should play. * * @property _framerate * @type {Number} * @default 0 * @private */ this._framerate = 0; /** * The total time in seconds for the animation. * * @property _duration * @type Number * @default 0 * @private */ this._duration = 0; /** * The time elapsed from frame 0 in seconds. * @property _t * @type Number * @default 0 * @private */ this._t = 0; /** * An array of frame labels. * @property _labels * @type Array * @private */ this._labels = 0; /** * An array of event labels. * @property _events * @type Array * @private */ this._events = 0; /** * The array of Textures that are the MovieClip's frames. * @property _textures * @private * @type Array */ this._textures = null; if (data) { this.init(data, atlas); } }; var p = extend(AdvancedMovieClip, Sprite); var s = Sprite.prototype; /** * The speed at which the AdvancedMovieClip should play. * @property framerate * @type {Number} * @default 0 */ Object.defineProperty(p, 'framerate', { get: function() { return this._framerate; }, set: function(value) { if (value > 0) { this._framerate = value; this._duration = value ? this._textures.length / value : 0; } else this._framerate = this._duration = 0; } }); /** * When the BitmapMovieClip is framerate independent, this is the time elapsed from frame 0 in * seconds. * @property elapsedTime * @type Number * @default 0 * @public */ Object.defineProperty(p, 'elapsedTime', { get: function() { return this._t; }, set: function(value) { this._t = value; if (this._t > this._duration) this._t = this._duration; //add a tiny amount to stop floating point errors in their tracks this.currentFrame = Math.floor(this._t * this._framerate + 0.0000001); if (this.currentFrame >= this._textures.length) this.currentFrame = this._textures.length - 1; this.texture = this._textures[this.currentFrame] || Texture.EMPTY; } }); /** * (Read-Only) The total number of frames in the timeline * @property totalFrames * @type Int * @default 0 * @readOnly */ Object.defineProperty(p, 'totalFrames', { get: function() { return this._textures.length; } }); //==== Public Methods ===== /** * Advances this movie clip to the specified position or label. * @method gotoAndStop * @param {String|Number} positionOrLabel The animation or frame name to go to. */ p.gotoAndStop = function(positionOrLabel) { var pos = null; if (typeof positionOrLabel == "string") { var labels = this._labels; for (var i = 0, len = labels.length; i < len; ++i) { if (labels[i].label == positionOrLabel) { pos = labels[i].position; break; } } } else pos = positionOrLabel; if (pos === null) return; if (pos >= this._textures.length) pos = this._textures.length - 1; this.currentFrame = pos; if (this._framerate > 0) this._t = pos / this._framerate; else this._t = 0; this.texture = this._textures[pos] || Texture.EMPTY; }; /** * Advances the playhead. This occurs automatically each tick by default. * @param [time] {Number} The amount of time in milliseconds to advance by. * @method advance */ p.advance = function(time) { if (this._framerate > 0 && time) { this._t += time * 0.001; //milliseconds -> seconds if (this._t > this._duration) this._t = this._duration; //add a tiny amount to stop floating point errors in their tracks this.currentFrame = Math.floor(this._t * this._framerate + 0.0000001); if (this.currentFrame >= this._textures.length) this.currentFrame = this._textures.length - 1; this.texture = this._textures[this.currentFrame] || Texture.EMPTY; } }; /** * Returns a sorted list of the labels defined on this AdvancedMovieClip. * @method getLabels * @return {Array[Object]} A sorted array of objects with label and position (aka frame) * properties. */ p.getLabels = function() { return this._labels; }; /** * Returns a sorted list of the labels which can be played with Animator. * @method getEvents * @return {Array} A sorted array of objects with label, length and position (aka frame) * properties. */ p.getEvents = function() { return this._events; }; /** * Returns the name of the label on or immediately before the current frame. * @method getCurrentLabel * @return {String} The name of the current label or null if there is no label. */ p.getCurrentLabel = function() { var labels = this._labels; var current = null; for (var i = 0, len = labels.length; i < len; ++i) { if (labels[i].position <= this.currentFrame) current = labels[i].label; else break; } return current; }; /** * Initializes or re-initializes the AdvancedMovieClip. * @method init * @param {Object} data Initialization data * @param {int} [data.fps] Framerate to play the movieclip at. Omitting this will use the * current framerate. * @param {Object} [data.labels] A dictionary of the labels in the movieclip to assist in * playing animations. * @param {Object} [data.origin={x:0,y:0}] The origin of the movieclip. * @param {Array} [data.frames] An array of frame sequences to pull from the texture atlas. * @param {String} [data.frames.name] The name to use for the frame sequence. This should * include a "#" to be replaced with the image number. * @param {int} [data.frames.min] The first frame number in the frame sequence. * @param {int} [data.frames.max] The last frame number in the frame sequence. * @param {int} [data.frames.digits=4] The maximum number of digits in the names of the frames, * e.g. myAnim0001 has 4 digits. * @param {Number} [data.scale=1] The scale at which the art was exported, e.g. a scale of 1.4 * means the art was increased in size to 140% before exporting * and should be scaled back down before drawing to the screen. * @param {springroll.pixi.TextureAtlas} [atlas] A TextureAtlas to pull frames from. If omitted, * frames are pulled from Pixi's global texture * cache. */ p.init = function(data, atlas) { //collect the frame labels var labels = this._labels = []; var events = this._events = []; var name; if (data.labels) { var positions = {}, position; for (name in data.labels) { var label = { label: name, position: data.labels[name], length: 1 }; positions[name] = label.position; // Exclude animation-end tags if (!/_(loop|stop)$/.test(name)) { events.push(label); } labels.push(label); } // Calculate the lengths for all the event labels var start = null; for (var j = 0; j < events.length; j++) { var event = events[j]; start = event.position; event.length = positions[name + '_stop'] - start || positions[name + '_loop'] - start || 0; } labels.sort(labelSorter); events.sort(labelSorter); } //collect the frames this._textures = []; var index; for (var i = 0; i < data.frames.length; ++i) { var frameSet = data.frames[i]; name = frameSet.name; index = name.lastIndexOf("/"); //strip off any folder structure included in the name if (index >= 0) name = name.substring(index + 1); if (atlas) { atlas.getFrames( name, frameSet.min, frameSet.max, frameSet.digits, this._textures ); } else { getFrames( name, frameSet.min, frameSet.max, frameSet.digits, this._textures ); } } //set up the framerate if (data.fps) this.framerate = data.fps; else if (this._framerate) this.framerate = this._framerate; if (data.origin) { this.pivot.x = data.origin.x; this.pivot.y = data.origin.y; } else { this.pivot.x = this.pivot.y = 0; } this.gotoAndStop(0); }; function labelSorter(a, b) { return a.position - b.position; } function getFrames(name, numberMin, numberMax, maxDigits, outArray) { if (maxDigits === undefined) maxDigits = 4; if (maxDigits < 0) maxDigits = 0; if (!outArray) outArray = []; //set up strings to add the correct number of zeros ahead of time to avoid creating even more strings. var zeros = []; //preceding zeroes array var compares = []; //powers of 10 array for determining how many preceding zeroes to use var i, c; for (i = 1; i < maxDigits; ++i) { var s = ""; c = 1; for (var j = 0; j < i; ++j) { s += "0"; c *= 10; } zeros.unshift(s); compares.push(c); } var compareLength = compares.length; //the length of the compare //the previous Texture, so we can place the same object in multiple times to control //animation rate var prevTex; var len; var fromFrame = Texture.fromFrame; for (i = numberMin, len = numberMax; i <= len; ++i) { var num = null; //calculate the number of preceding zeroes needed, then create the full number string. for (c = 0; c < compareLength; ++c) { if (i < compares[c]) { num = zeros[c] + i; break; } } if (!num) num = i.toString(); //If the texture doesn't exist, use the previous texture - this should allow us to use //fewer textures that are in fact the same, if those textures were removed before //making the spritesheet var texName = name.replace("#", num); var tex = fromFrame(texName, true); if (tex) prevTex = tex; if (prevTex) outArray.push(prevTex); } return outArray; } /** * Copies the labels, textures, origin, and framerate from another AdvancedMovieClip. * The labels and textures are copied by reference, instead of a deep copy. * @method copyFrom * @param {AdvancedMovieClip} other The movieclip to copy data from. */ p.copyFrom = function(other) { this._textures = other._textures; this._labels = other._labels; this._events = other._events; this.pivot.x = other.pivot.x; this.pivot.y = other.pivot.y; this._framerate = other._framerate; this._duration = other._duration; }; /** * Destroys the AdvancedMovieClip. * @method destroy */ p.destroy = function() { this._labels = this._events = null; s.destroy.call(this); }; namespace("springroll.pixi").AdvancedMovieClip = AdvancedMovieClip; }());