#

API Documentation for: NEXT
Show: Inherited Protected Private Deprecated

# File:easeljs\display\SpriteSheet.js

    /*
     * 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.
     */

    /**
     * @module EaselJS
     */

    // namespace:
    this.createjs = this.createjs||{};

    (function() {
    	"use strict";

    // constructor:
    	/**
    	 * 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:
    	 * <ol>
    	 * 	<li> The source image or images to use.</li>
    	 * 	<li> The positions of individual image frames.</li>
    	 * 	<li> Sequences of frames that form named animations. Optional.</li>
    	 * 	<li> The target playback framerate. Optional.</li>
    	 * </ol>
    	 * <h3>SpriteSheet Format</h3>
    	 * SpriteSheets are an object with two required properties (`images` and `frames`), and two optional properties
    	 * (`framerate` and `animations`). This makes them easy to define in javascript code, or in JSON.
    	 *
    	 * <h4>images</h4>
    	 * An array of source images. Images can be either an HTMlimage
    	 * instance, or a uri to an image. The former is recommended to control preloading.
    	 *
    	 * 	images: [image1, "path/to/image2.png"],
    	 *
    	 * <h4>frames</h4>
    	 * Defines the individual frames. There are two supported formats for frame data:
    	 * When all of the frames are the same size (in a grid), use an object with `width`, `height`, `regX`, `regY`,
    	 * and `count` properties.
    	 *
    	 * <ul>
    	 *  <li>`width` & `height` are required and specify the dimensions of the frames</li>
    	 *  <li>`regX` & `regY` indicate the registration point or "origin" of the frames</li>
    	 *  <li>`spacing` indicate the spacing between frames</li>
    	 *  <li>`margin` specify the margin around the image(s)</li>
    	 *  <li>`count` allows you to specify the total number of frames in the spritesheet; if omitted, this will
    	 *  be calculated based on the dimensions of the source images and the frames. Frames will be assigned
    	 *  indexes based on their position in the source images (left to right, top to bottom).</li>
    	 * </ul>
    	 *
    	 *  	frames: {width:64, height:64, count:20, regX: 32, regY:64, spacing:0, margin:0}
    	 *
    	 * If the frames are of different sizes, use an array of frame definitions. Each definition is itself an array
    	 * with 4 required and 3 optional entries, in the order:
    	 *
    	 * <ul>
    	 *  <li>The first four, `x`, `y`, `width`, and `height` are required and define the frame rectangle.</li>
    	 *  <li>The fifth, `imageIndex`, specifies the index of the source image (defaults to 0)</li>
    	 *  <li>The last two, `regX` and `regY` specify the registration point of the frame</li>
    	 * </ul>
    	 *
    	 * 	frames: [
    	 * 		// x, y, width, height, imageIndex*, regX*, regY*
    	 * 		[64, 0, 96, 64],
    	 * 		[0, 0, 64, 64, 1, 32, 32]
    	 * 		// etc.
    	 * 	]
    	 *
    	 * <h4>animations</h4>
    	 * Optional. An object defining sequences of frames to play as named animations. Each property corresponds to an
    	 * animation of the same name. Each animation must specify the frames to play, and may
    	 * also include a relative playback `speed` (ex. 2 would playback at double speed, 0.5 at half), and
    	 * the name of the `next` animation to sequence to after it completes.
    	 *
    	 * There are three formats supported for defining the frames in an animation, which can be mixed and matched as appropriate:
    	 * <ol>
    	 * 	<li>for a single frame animation, you can simply specify the frame index
    	 *
    	 * 		animations: {
    	 * 			sit: 7
    	 * 		}
    	 *
    	 * </li>
    	 * <li>
    	 *      for an animation of consecutive frames, you can use an array with two required, and two optional entries
    	 * 		in the order: `start`, `end`, `next`, and `speed`. This will play the frames from start to end inclusive.
    	 *
    	 * 		animations: {
    	 * 			// start, end, next*, speed*
    	 * 			run: [0, 8],
    	 * 			jump: [9, 12, "run", 2]
    	 * 		}
    	 *
    	 *  </li>
    	 *  <li>
    	 *     for non-consecutive frames, you can use an object with a `frames` property defining an array of frame
    	 *     indexes to play in order. The object can also specify `next` and `speed` properties.
    	 *
    	 * 		animations: {
    	 * 			walk: {
    	 * 				frames: [1,2,3,3,2,1]
    	 * 			},
    	 * 			shoot: {
    	 * 				frames: [1,4,5,6],
    	 * 				next: "walk",
    	 * 				speed: 0.5
    	 * 			}
    	 * 		}
    	 *
    	 *  </li>
    	 * </ol>
    	 * <strong>Note:</strong> the `speed` property was added in EaselJS 0.7.0. Earlier versions had a `frequency`
    	 * property instead, which was the inverse of `speed`. For example, a value of "4" would be 1/4 normal speed in
    	 * earlier versions, but is 4x normal speed in EaselJS 0.7.0+.
    	 *
    	 * <h4>framerate</h4>
    	 * Optional. Indicates the default framerate to play this spritesheet at in frames per second. See
    	 * {{#crossLink "SpriteSheet/framerate:property"}}{{/crossLink}} for more information.
    	 *
    	 * 		framerate: 20
    	 *
    	 * Note that the Sprite framerate will only work if the stage update method is provided with the {{#crossLink "Ticker/tick:event"}}{{/crossLink}}
    	 * event generated by the {{#crossLink "Ticker"}}{{/crossLink}}.
    	 *
    	 * 		createjs.Ticker.on("tick", handleTick);
    	 * 		function handleTick(event) {
    	 *			stage.update(event);
    	 *		}
    	 *
    	 * <h3>Example</h3>
    	 * To define a simple sprite sheet, with a single image "sprites.jpg" arranged in a regular 50x50 grid with three
    	 * animations: "stand" showing the first frame, "run" looping frame 1-5 inclusive, and "jump" playing frame 6-8 and
    	 * sequencing back to run.
    	 *
    	 * 		var data = {
    	 * 			images: ["sprites.jpg"],
    	 * 			frames: {width:50, height:50},
    	 * 			animations: {
    	 * 				stand:0,
    	 * 				run:[1,5],
    	 * 				jump:[6,8,"run"]
    	 * 			}
    	 * 		};
    	 * 		var spriteSheet = new createjs.SpriteSheet(data);
    	 * 		var animation = new createjs.Sprite(spriteSheet, "run");
    	 *
    	 * <h3>Generating SpriteSheet Images</h3>
    	 * Spritesheets can be created manually by combining images in PhotoShop, and specifying the frame size or
    	 * coordinates manually, however there are a number of tools that facilitate this.
    	 * <ul>
    	 *     <li>Exporting SpriteSheets or HTML5 content from Flash Pro supports the EaselJS SpriteSheet format.</li>
    	 *     <li>The popular <a href="https://www.codeandweb.com/texturepacker/easeljs" target="_blank">Texture Packer</a> has
    	 *     EaselJS support.
    	 *     <li>SWF animations in Flash can be exported to SpriteSheets using <a href="http://createjs.com/zoe" target="_blank"></a></li>
    	 * </ul>
    	 *
    	 * <h3>Cross Origin Issues</h3>
    	 * <strong>Warning:</strong> Images loaded cross-origin will throw cross-origin security errors when interacted with
    	 * using:
    	 * <ul>
    	 *     <li>a mouse</li>
    	 *     <li>methods such as {{#crossLink "Container/getObjectUnderPoint"}}{{/crossLink}}</li>
    	 *     <li>Filters (see {{#crossLink "Filter"}}{{/crossLink}})</li>
    	 *     <li>caching (see {{#crossLink "DisplayObject/cache"}}{{/crossLink}})</li>
    	 * </ul>
    	 * You can get around this by setting `crossOrigin` property on your images before passing them to EaselJS, or
    	 * setting the `crossOrigin` property on PreloadJS' LoadQueue or LoadItems.
    	 *
    	 * 		var image = new Image();
    	 * 		img.crossOrigin="Anonymous";
    	 * 		img.src = "http://server-with-CORS-support.com/path/to/image.jpg";
    	 *
    	 * If you pass string paths to SpriteSheets, they will not work cross-origin. The server that stores the image must
    	 * support cross-origin requests, or this will not work. For more information, check out
    	 * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS" target="_blank">CORS overview on MDN</a>.
    	 *
    	 * @class SpriteSheet
    	 * @constructor
    	 * @param {Object} data An object describing the SpriteSheet data.
    	 * @extends EventDispatcher
    	 **/
    	function SpriteSheet(data) {
    		this.EventDispatcher_constructor();

    		// public properties:
    		/**
    		 * Indicates whether all images are finished loading.
    		 * @property complete
    		 * @type Boolean
    		 * @readonly
    		 **/
    		this.complete = true;

    		/**
    		 * Specifies the framerate to use by default for Sprite instances using the SpriteSheet. See the Sprite class
    		 * {{#crossLink "Sprite/framerate:property"}}{{/crossLink}} for more information.
    		 * @property framerate
    		 * @type Number
    		 **/
    		this.framerate = 0;

    		// private properties:
    		/**
    		 * @property _animations
    		 * @protected
    		 * @type Array
    		 **/
    		this._animations = null;

    		/**
    		 * @property _frames
    		 * @protected
    		 * @type Array
    		 **/
    		this._frames = null;

    		/**
    		 * @property _images
    		 * @protected
    		 * @type Array
    		 **/
    		this._images = null;

    		/**
    		 * @property _data
    		 * @protected
    		 * @type Object
    		 **/
    		this._data = null;

    		/**
    		 * @property _loadCount
    		 * @protected
    		 * @type Number
    		 **/
    		this._loadCount = 0;

    		// only used for simple frame defs:
    		/**
    		 * @property _frameHeight
    		 * @protected
    		 * @type Number
    		 **/
    		this._frameHeight = 0;

    		/**
    		 * @property _frameWidth
    		 * @protected
    		 * @type Number
    		 **/
    		this._frameWidth = 0;

    		/**
    		 * @property _numFrames
    		 * @protected
    		 * @type Number
    		 **/
    		this._numFrames = 0;

    		/**
    		 * @property _regX
    		 * @protected
    		 * @type Number
    		 **/
    		this._regX = 0;

    		/**
    		 * @property _regY
    		 * @protected
    		 * @type Number
    		 **/
    		this._regY = 0;

    		/**
    		 * @property _spacing
    		 * @protected
    		 * @type Number
    		 **/
    		this._spacing = 0;

    		/**
    		 * @property _margin
    		 * @protected
    		 * @type Number
    		 **/
    		this._margin = 0;

    		// setup:
    		this._parseData(data);
    	}
    	var p = createjs.extend(SpriteSheet, createjs.EventDispatcher);

    	// TODO: deprecated
    	// p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details.

    // 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.
    	 *
    	 * 	var sheet = new SpriteSheet(data);
    	 * 	if (!sheet.complete) {
    	 * 		// not preloaded, listen for the complete event:
    	 * 		sheet.addEventListener("complete", handler);
    	 * 	}
    	 *
    	 * @event complete
    	 * @param {Object} target The object that dispatched the event.
    	 * @param {String} type The event type.
    	 * @since 0.6.0
    	 */

    	/**
    	 * Dispatched when getFrame is called with a valid frame index. This is primarily intended for use by {{#crossLink "SpriteSheetBuilder"}}{{/crossLink}}
    	 * when doing on-demand rendering.
    	 * @event getframe
    	 * @param {Number} index The frame index.
    	 * @param {Object} frame The frame object that getFrame will return.
    	 */

    // getter / setters:
    	/**
    	 * Use the {{#crossLink "SpriteSheet/animations:property"}}{{/crossLink}} property instead.
    	 * @method getAnimations
    	 * @return {Array}
    	 * @deprecated
    	 **/
    	p.getAnimations = function() {
    		return this._animations.slice();
    	};

    	/**
    	 * Returns an array of all available animation names available on this sprite sheet as strings.
    	 * @property animations
    	 * @type {Array}
    	 * @readonly
    	 **/
    	try {
    		Object.defineProperties(p, {
    			animations: { get: p.getAnimations }
    		});
    	} catch (e) {}

    // public methods:
    	/**
    	 * Returns the total number of frames in the specified animation, or in the whole sprite
    	 * sheet if the animation param is omitted. Returns 0 if the spritesheet relies on calculated frame counts, and
    	 * the images have not been fully loaded.
    	 * @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 || 0;
    		} else {
    			var data = this._data[animation];
    			if (data == null) { return 0; }
    			else { return data.frames.length; }
    		}
    	};

    	/**
    	 * Returns an object defining the specified animation. The returned object contains:<UL>
    	 * 	<li>frames: an array of the frame ids in the animation</li>
    	 * 	<li>speed: the playback speed for this animation</li>
    	 * 	<li>name: the name of the animation</li>
    	 * 	<li>next: the default animation to play next. If the animation loops, the name and next property will be the
    	 * 	same.</li>
    	 * </UL>
    	 * @method getAnimation
    	 * @param {String} name The name of the animation to get.
    	 * @return {Object} a generic object with frames, speed, 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:<UL>
    	 * 	<li>an image property holding a reference to the image object in which the frame is found</li>
    	 * 	<li>a rect property containing a Rectangle instance which defines the boundaries for the frame within that
    	 * 	image.</li>
    	 * 	<li> A regX and regY property corresponding to the regX/Y values for the frame.
    	 * </UL>
    	 * @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.
    	 **/
    	p.getFrame = function(frameIndex) {
    		var frame;
    		if (this._frames && (frame=this._frames[frameIndex])) { return frame; }
    		return null;
    	};

    	/**
    	 * Returns a {{#crossLink "Rectangle"}}{{/crossLink}} 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:
    	 *
    	 * 	[x=-50, y=-40, width=90, height=70]
    	 *
    	 * @method getFrameBounds
    	 * @param {Number} frameIndex The index of the frame.
    	 * @param {Rectangle} [rectangle] A Rectangle instance to copy the values into. By default a new instance is created.
    	 * @return {Rectangle} A Rectangle instance. Returns null if the frame does not exist, or the image is not fully loaded.
    	 **/
    	p.getFrameBounds = function(frameIndex, rectangle) {
    		var frame = this.getFrame(frameIndex);
    		return frame ? (rectangle||new createjs.Rectangle()).setValues(-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]";
    	};

    	/**
    	 * SpriteSheet cannot be cloned. A SpriteSheet can be shared by multiple Sprite instances without cloning it.
    	 * @method clone
    	 **/
    	p.clone = function() {
    		throw("SpriteSheet cannot be cloned.")
    	};

    // private methods:
    	/**
    	 * @method _parseData
    	 * @param {Object} data An object describing the SpriteSheet data.
    	 * @protected
    	 **/
    	p._parseData = function(data) {
    		var i,l,o,a;
    		if (data == null) { return; }

    		this.framerate = data.framerate||0;

    		// 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 = document.createElement("img");
    					img.src = src;
    				}
    				a.push(img);
    				if (!img.getContext && !img.naturalWidth) {
    					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._spacing = o.spacing||0;
    			this._margin = o.margin||0;
    			this._numFrames = o.count;
    			if (this._loadCount == 0) { this._calculateFrames(); }
    		}

    		// parse animations:
    		this._animations = [];
    		if ((o=data.animations) != null) {
    			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.speed = obj[3];
    						anim.next = obj[2];
    						a = anim.frames = [];
    						for (i=obj[0];i<=obj[1];i++) {
    							a.push(i);
    						}
    					}
    				} else { // complex
    					anim.speed = obj.speed;
    					anim.next = obj.next;
    					var frames = obj.frames;
    					a = anim.frames = (typeof frames == "number") ? [frames] : frames.slice(0);
    				}
    				if (anim.next === true || anim.next === undefined) { anim.next = name; } // loop
    				if (anim.next === false || (a.length < 2 && anim.next == name)) { anim.next = null; } // stop
    				if (!anim.speed) { anim.speed = 1; }
    				this._animations.push(name);
    				this._data[name] = anim;
    			}
    		}
    	};

    	/**
    	 * @method _handleImageLoad
    	 * @protected
    	 **/
    	p._handleImageLoad = function() {
    		if (--this._loadCount == 0) {
    			this._calculateFrames();
    			this.complete = true;
    			this.dispatchEvent("complete");
    		}
    	};

    	/**
    	 * @method _calculateFrames
    	 * @protected
    	 **/
    	p._calculateFrames = function() {
    		if (this._frames || this._frameWidth == 0) { return; }

    		this._frames = [];

    		var maxFrames = this._numFrames || 100000; // if we go over this, something is wrong.
    		var frameCount = 0, frameWidth = this._frameWidth, frameHeight = this._frameHeight;
    		var spacing = this._spacing, margin = this._margin;

    		imgLoop:
    		for (var i=0, imgs=this._images; i<imgs.length; i++) {
    			var img = imgs[i], imgW = img.width, imgH = img.height;

    			var y = margin;
    			while (y <= imgH-margin-frameHeight) {
    				var x = margin;
    				while (x <= imgW-margin-frameWidth) {
    					if (frameCount >= maxFrames) { break imgLoop; }
    					frameCount++;
    					this._frames.push({
    							image: img,
    							rect: new createjs.Rectangle(x, y, frameWidth, frameHeight),
    							regX: this._regX,
    							regY: this._regY
    						});
    					x += frameWidth+spacing;
    				}
    				y += frameHeight+spacing;
    			}
    		}
    		this._numFrames = frameCount;
    	};

    	createjs.SpriteSheet = createjs.promote(SpriteSheet, "EventDispatcher");
    }());