UNPKG

node-easel

Version:

node wrapper for EaselJS, utilizing node-canvas.

460 lines (426 loc) 14.5 kB
/* * SpriteSheet * 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. */ // namespace: this.createjs = this.createjs||{}; (function() { /** * Encapsulates the properties and methods associated with a sprite sheet. A sprite sheet is a series of images (usually * animation frames) combined into a larger image (or images). For example, an animation consisting of eight 100x100 * images could be combined into a single 400x200 sprite sheet (4 frames across by 2 high). * * The data passed to the SpriteSheet constructor defines three critical pieces of information:<ol> * <li> The image or images to use.</li> * <li> The positions of individual image frames. This data can be represented in one of two ways: * As a regular grid of sequential, equal-sized frames, or as individually defined, variable sized frames arranged in * an irregular (non-sequential) fashion.</li> * <li> Likewise, animations can be represented in two ways: As a series of sequential frames, defined by a start and * end frame [0,3], or as a list of frames [0,1,2,3].</li> * </OL> * * <h4>SpriteSheet Format</h4> * * data = { * * // DEFINING IMAGES: * // list of images or image URIs to use. SpriteSheet can handle preloading. * // the order dictates their index value for frame definition. * images: [image1, "path/to/image2.png"], * * // DEFINING FRAMES: * // the simple way to define frames, only requires frame size because frames are consecutive: * // define frame width/height, and optionally the frame count and registration point x/y. * // if count is omitted, it will be calculated automatically based on image dimensions. * frames: {width:64, height:64, count:20, regX: 32, regY:64}, * * // OR, the complex way that defines individual rects for frames. * // The 5th value is the image index per the list defined in "images" (defaults to 0). * frames: [ * // x, y, width, height, imageIndex, regX, regY * [0,0,64,64,0,32,64], * [64,0,96,64,0] * ], * * // DEFINING ANIMATIONS: * * // simple animation definitions. Define a consecutive range of frames. * // also optionally define a "next" animation name for sequencing. * // setting next to false makes it pause when it reaches the end. * animations: { * // start, end, next, frequency * run: [0,8], * jump: [9,12,"run",2], * stand: 13 * } * * // the complex approach which specifies every frame in the animation by index. * animations: { * run: { * frames: [1,2,3,3,2,1] * }, * jump: { * frames: [1,4,5,6,1], * next: "run", * frequency: 2 * }, * stand: { frames: [7] } * } * * // the above two approaches can be combined, you can also use a single frame definition: * animations: { * run: [0,8,true,2], * jump: { * frames: [8,9,10,9,8], * next: "run", * frequency: 2 * }, * stand: 7 * } * } * * <h4>Example</h4> * To define a simple sprite sheet, with a single image "sprites.jpg" arranged in a regular 50x50 grid with two * animations, "run" looping from frame 0-4 inclusive, and "jump" playing from frame 5-8 and sequencing back to run: * * var data = { * images: ["sprites.jpg"], * frames: {width:50, height:50}, * animations: {run:[0,4], jump:[5,8,"run"]} * }; * var animation = new createjs.BitmapAnimation(data); * animation.gotoAndPlay("run"); * * @class SpriteSheet * @constructor * @param data * @uses EventDispatcher **/ var SpriteSheet = function(data) { this.initialize(data); } var p = SpriteSheet.prototype; // events: /** * Dispatched when all images are loaded. Note that this only fires if the images * were not fully loaded when the sprite sheet was initialized. You should check the complete property * to prior to adding a listener. Ex. * <pre><code>var sheet = new SpriteSheet(data); * if (!sheet.complete) { * &nbsp; // not preloaded, listen for onComplete: * &nbsp; sheet.addEventListener("complete", handler); * }</code></pre> * @event complete * @param {Object} target The object that dispatched the event. * @param {String} type The event type. * @since 0.6.0 */ // public properties: /** * Read-only property indicating whether all images are finished loading. * @property complete * @type Boolean **/ p.complete = true; /** * The onComplete callback is called when all images are loaded. Note that this only fires if the images * were not fully loaded when the sprite sheet was initialized. You should check the complete property * to prior to adding an onComplete handler. Ex. * <pre><code>var sheet = new SpriteSheet(data); * if (!sheet.complete) { * &nbsp; // not preloaded, listen for onComplete: * &nbsp; sheet.onComplete = handler; * }</code></pre> * @property onComplete * @type Function * @deprecated In favour of the "complete" event. Will be removed in a future version. **/ p.onComplete = null; // mix-ins: // EventDispatcher methods: p.addEventListener = null; p.removeEventListener = null; p.removeAllEventListeners = null; p.dispatchEvent = null; p.hasEventListener = null; p._listeners = null; createjs.EventDispatcher.initialize(p); // inject EventDispatcher methods. // private properties: /** * @property _animations * @protected **/ p._animations = null; /** * @property _frames * @protected **/ p._frames = null; /** * @property _images * @protected **/ p._images = null; /** * @property _data * @protected **/ p._data = null; /** * @property _loadCount * @protected **/ p._loadCount = 0; // only used for simple frame defs: /** * @property _frameHeight * @protected **/ p._frameHeight = 0; /** * @property _frameWidth * @protected **/ p._frameWidth = 0; /** * @property _numFrames * @protected **/ p._numFrames = 0; /** * @property _regX * @protected **/ p._regX = 0; /** * @property _regY * @protected **/ p._regY = 0; // constructor: /** * @method initialize * @protected **/ p.initialize = function(data) { var i,l,o,a; if (data == null) { return; } // parse images: if (data.images && (l=data.images.length) > 0) { a = this._images = []; for (i=0; i<l; i++) { var img = data.images[i]; if (typeof img == "string") { var src = img; img = new Image(); img.src = src; } a.push(img); if (!img.getContext && !img.complete) { this._loadCount++; this.complete = false; (function(o) { img.onload = function() { o._handleImageLoad(); } })(this); } } } // parse frames: if (data.frames == null) { // nothing } else if (data.frames instanceof Array) { this._frames = []; a = data.frames; for (i=0,l=a.length;i<l;i++) { var arr = a[i]; this._frames.push({image:this._images[arr[4]?arr[4]:0], rect:new createjs.Rectangle(arr[0],arr[1],arr[2],arr[3]), regX:arr[5]||0, regY:arr[6]||0 }); } } else { o = data.frames; this._frameWidth = o.width; this._frameHeight = o.height; this._regX = o.regX||0; this._regY = o.regY||0; this._numFrames = o.count; if (this._loadCount == 0) { this._calculateFrames(); } } // parse animations: if ((o=data.animations) != null) { this._animations = []; this._data = {}; var name; for (name in o) { var anim = {name:name}; var obj = o[name]; if (typeof obj == "number") { // single frame a = anim.frames = [obj]; } else if (obj instanceof Array) { // simple if (obj.length == 1) { anim.frames = [obj[0]]; } else { anim.frequency = obj[3]; anim.next = obj[2]; a = anim.frames = []; for (i=obj[0];i<=obj[1];i++) { a.push(i); } } } else { // complex anim.frequency = obj.frequency; anim.next = obj.next; var frames = obj.frames; a = anim.frames = (typeof frames == "number") ? [frames] : frames.slice(0); } anim.next = (a.length < 2 || anim.next == false) ? null : (anim.next == null || anim.next == true) ? name : anim.next; if (!anim.frequency) { anim.frequency = 1; } this._animations.push(name); this._data[name] = anim; } } } // public methods: /** * Returns the total number of frames in the specified animation, or in the whole sprite * sheet if the animation param is omitted. * @method getNumFrames * @param {String} animation The name of the animation to get a frame count for. * @return {Number} The number of frames in the animation, or in the entire sprite sheet if the animation param is omitted. */ p.getNumFrames = function(animation) { if (animation == null) { return this._frames ? this._frames.length : this._numFrames; } else { var data = this._data[animation]; if (data == null) { return 0; } else { return data.frames.length; } } } /** * Returns an array of all available animation names as strings. * @method getAnimations * @return {Array} an array of animation names available on this sprite sheet. **/ p.getAnimations = function() { return this._animations.slice(0); } /** * Returns an object defining the specified animation. The returned object has a * frames property containing an array of the frame id's in the animation, a frequency * property indicating the advance frequency for this animation, a name property, * and a next property, which specifies the default next animation. If the animation * loops, the name and next property will be the same. * @method getAnimation * @param {String} name The name of the animation to get. * @return {Object} a generic object with frames, frequency, name, and next properties. **/ p.getAnimation = function(name) { return this._data[name]; } /** * Returns an object specifying the image and source rect of the specified frame. The returned object * has an image property holding a reference to the image object in which the frame is found, * and a rect property containing a Rectangle instance which defines the boundaries for the * frame within that image. * @method getFrame * @param {Number} frameIndex The index of the frame. * @return {Object} a generic object with image and rect properties. Returns null if the frame does not exist, or the image is not fully loaded. **/ p.getFrame = function(frameIndex) { var frame; if (this.complete && this._frames && (frame=this._frames[frameIndex])) { return frame; } return null; }; /** * Returns a Rectangle instance defining the bounds of the specified frame relative to the origin. For example, a * 90 x 70 frame with a regX of 50 and a regY of 40 would return a rectangle with [x=-50, y=-40, width=90, height=70]. * @method getFrameBounds * @param {Number} frameIndex The index of the frame. * @return {Rectangle} A Rectangle instance. Returns null if the frame does not exist, or the image is not fully loaded. **/ p.getFrameBounds = function(frameIndex) { var frame = this.getFrame(frameIndex); return frame ? new createjs.Rectangle(-frame.regX, -frame.regY, frame.rect.width, frame.rect.height) : null; }; /** * Returns a string representation of this object. * @method toString * @return {String} a string representation of the instance. **/ p.toString = function() { return "[SpriteSheet]"; } /** * Returns a clone of the SpriteSheet instance. * @method clone * @return {SpriteSheet} a clone of the SpriteSheet instance. **/ p.clone = function() { // TODO: there isn't really any reason to clone SpriteSheet instances, because they can be reused. var o = new SpriteSheet(); o.complete = this.complete; o._animations = this._animations; o._frames = this._frames; o._images = this._images; o._data = this._data; o._frameHeight = this._frameHeight; o._frameWidth = this._frameWidth; o._numFrames = this._numFrames; o._loadCount = this._loadCount; return o; } // private methods: /** * @method _handleImageLoad * @protected **/ p._handleImageLoad = function() { if (--this._loadCount == 0) { this._calculateFrames(); this.complete = true; this.onComplete&&this.onComplete(); this.dispatchEvent("complete"); } } /** * @method _calculateFrames * @protected **/ p._calculateFrames = function() { if (this._frames || this._frameWidth == 0) { return; } this._frames = []; var ttlFrames = 0; var fw = this._frameWidth; var fh = this._frameHeight; for (var i=0,imgs = this._images; i<imgs.length; i++) { var img = imgs[i]; var cols = (img.width+1)/fw|0; var rows = (img.height+1)/fh|0; var ttl = this._numFrames>0 ? Math.min(this._numFrames-ttlFrames,cols*rows) : cols*rows; for (var j=0;j<ttl;j++) { this._frames.push({image:img, rect:new createjs.Rectangle(j%cols*fw,(j/cols|0)*fh,fw,fh), regX:this._regX, regY:this._regY }); } ttlFrames += ttl; } this._numFrames = ttlFrames; } createjs.SpriteSheet = SpriteSheet; }());