API Docs for: 0.3.0
Show:

File: modules/Graphic/SpriteSheet.js

/**
 * @module Graphic
 * @namespace Graphic
 */

var TW = TW || {};
define(['../Utils/clone'], function(clone) {

	TW.Graphic = TW.Graphic || {};


	/**
	 * The spritesheet class provides a model to describe animations from an image called spriteSheet.
	 *
	 * @class SpriteSheet
	 * @constructor
	 * @param {Image} image represents the image on which the SpriteSheet coordinate will be applied.
	 * @param {Object} config represents the object which provides the description of each animation.
	 *
	 *
	 *     var mySpriteSheet = new SpriteSheet(image, config);
	 *
	 *  config object represents the raw configuration of the spriteSheet.
	 *  Please see below the synthax of a spriteSheet :
	 *
	 *  The SpriteSheet in tumbleweed work on JSON objects.
	 *  Inside of these JSON objects, there is a description of all or just one animation.
	 *  In the previous example, config is a full description of the animation.
	 *  Here is how tumbleweed's spritesheets are configured :
	 *
	 *  First of all let's define the structure of our SpriteSheet object :
	 *
	 *     {}
	 *
	 *  As you can see it is only an empty JSON object.
	 *  This object can handle some informations about the animation.
	 *
	 * ### Setting default values
	 *
	 *     default : {}
	 *
	 *  The default object can handle default values. It is useful to make some constants in the spriteSheet.
	 *  For example if you want to define 5 constants (x = 10, y = 30, w = 50, h = 60, framerate = 25) You must
	 *  proceed like this :
	 *
	 *     default : {
     *          x : 10,
     *          y : 30,
     *          w : 50,
     *          h : 60,
     *          framerate : 25
     *     }
	 *
	 *
	 * ### Setting animations
	 *
	 *  Each animation is composed by frames and can also define a framerate value which override the framerate
	 *  from default values.
	 *  Here is an important tip, in some animations you may don't want to use the default values. Then you just
	 *  Have to redefine them inside of the animation.
	 *  To create an animation named 'walk' which have framerate set to 12 you must proceed like this :
	 *
	 *     walk : {
     *          framerate: 12,
     *          frames : []
     *     }
	 *
	 *  Note that there is an entry in you walk animation called frames. This entry must contain each frame of the
	 *  walk animation.
	 *
	 * ### Setting frames
	 *
	 *  Each animation contain some frames. It works like a flipbook, each frame are displayed one
	 *  after another, tumbleweed will wait 1/framerate seconds to display the next frame.
	 *  Let's imagine that your walk animation is made of three frames inside of your SpriteSheet.
	 *  The first one will have the coordinate : `x = 0, y = 0, w = 50, h = 50`
	 *  The second one will have the coordinate : `x = 50, y = 0, w = 50, h = 50`
	 *  And finally the third one will have the coordinate : `x = 0, y = 50, w = 50, h = 50`
	 *
	 *  Let's see below what will be the result of these frame inside of our walk animation object :
	 *
	 *     walk : {
     *          framerate: 12,
     *          frames : [
     *              { x: 0, y: 0, w: 50, h: 50 },
     *              { x: 50, y: 0, w: 50, h: 50 },
     *              { x: 0, y: 50, w: 50, h: 50 }
     *          ]
     *     }
	 *
	 * Let's wrap it inside of our config object :
	 *
	 *     var config = {
     *          default: {
     *              x: 0,
     *              y: 0,
     *              w: 50,
     *              h: 50,
     *              framerate: 25
     *          },
     *          walk: {
     *              framerate: 12,
     *              frames: [
     *                  {x:0, y:0, w: 50, h: 50},
     *                  {x:50, y:0, w:50, h:50},
     *                  {x:0, y:50, w:50, h:50}
     *              ]
     *          }
     *     };
	 *
	 * Now you have a walk animation which contain 3 frames which will be displayed with a framerate of 12.
	 * You have the basics to build your own animations.
	 * In the following parts i will describe how to make animation's reference and how you can do
	 * transformations on them.
	 *
	 * ### Animation's reference
	 *
	 *  Sometimes you can need to specify another animation which is a copy of another animation but with some
	 *  transformations on it, the typical case will be an animation of walking to right and another animation which
	 *  is walking to left.
	 *  Frames are the same except that they must be reverted horizontally.
	 *  To make it we will introduce a new entity which is the flip flags.
	 *  Flip flags allow you to flip images from an animation. You can either flip them by the x axis
	 *  (horizontal flip) or by the y axis (vertical flip).
	 *
	 *  To illustrate it we will improve our config object which contain the walk animation.
	 *  Now we want 2 walk animation (walk_left and walk_right).
	 *  Initially we will consider that our previous definition of the walk animation was equivalent to the
	 *  walk_left animation.
	 *
	 *  Now let's see now how looks like our config object :
	 *
	 *     var config = {
     *          default: {
     *              x: 0,
     *              y: 0,
     *              w: 50,
     *              h: 50,
     *              framerate: 25
     *          },
     *          walk_left: {
     *              framerate: 12,
     *              frames: [
     *                  {x:0, y:0, w: 50, h: 50 },
     *                  {x:50, y:0, w:50, h:50 },
     *                  {x:0, y:50, w:50, h:50 }
     *              ]
     *          },
     *          walk_right: {                           //This is our new animation entry : walk_right
     *              framerate: 12,                      //The framerate is the same than walk_left
     *              frames: [
     *                  {x:0, y:0, w:50, h:50},         //The frames are also the same than walk_left
     *                  {x:50, y:0, w:50, h:50},
     *                  {x:0, y:50, w:50, h:50}
     *              ],
     *              flipX: true,                       //Flip_x true indicate that all the frames must be
     *                                                  //horizontally flipped before being draw.
     *          }
     *     };
	 *
	 *  There's one annoying thing in the previous definition, as you can see, the frames of the walk_left animation
	 *  and the frames of the walk_right animation are duplicated.
	 *  There's one way to solve this problem : the alias flag.
	 *
	 * ### Alias flag
	 *
	 *  Alias flag allows you to define an animation by referencing another, it's quite useful when an animation has
	 *  the same frames than another. And we're actually in this case.
	 *  Using the alias flag, this is what will be your config object :
	 *
	 *     var config = {
     *          default: {
     *              x: 0,
     *              y: 0,
     *              w: 50,
     *              h: 50,
     *              framerate: 25
     *          },
     *          walk_left: {
     *              framerate: 12,
     *              frames: [
     *                  {x:0, y:0, w: 50, h: 50 },
     *                  {x:50, y:0, w:50, h:50 },
     *                  {x:0, y:50, w:50, h:50 }
     *              ]
     *          },
     *          walk_right: {               //This is our new animation entry : walk_right
     *              framerate: 12,          //The framerate is the same than walk_left
     *              alias: "walk_left",     //by declaring walk_left as alias,
     *                                      // walk_right will share it's frames with walk_left.
     *              flipX: true,           //Flip_x true indicate that all the frames must be
     *                                      //horizontally flipped before being draw.
     *          }
     *     };
	 *
	 * ## define frames more quickly
	 *
	 *  It's possible to define many frames in one line, if these frames follow or if they are identical.
	 *
	 *  If all frames are on the same line, or the same collumn, we can just specify the number of frames we want,
	 *  and the direction of repetition. No direction means we want to copy and repeat the frame.
	 *
	 *  Repeat a frame can be useful in some case.
	 *  For example if you want to wait more than one cycle to go on the next frame.
	 *
	 *  - `nbFrames` is the number of frames we want
	 *  - `way` is the direction we want to move for the next frames. If not defined, it's a repetition.
	 *    It can take 4 values :
	 *
	 *     - "LEFT",
	 *     - "RIGHT"
	 *     - "UP"
	 *     - "DOWN"
	 *
	 * Example :
	 *
	 *
	 *     var config = {
     *          default: {
     *              x: 0,
     *              y: 0,
     *              w: 50,
     *              h: 50,
     *              framerate: 25
     *          },
     *          walk_left: {
     *              framerate: 12,
     *              frames: [
     *                  {x:0, y:0, w: 50, h: 50 },
     *                  {x:50, y:0, w:50, h:50 },
     *                  {x:0, y:50, w:50, h:50, nbFrames: 5 }                  //This frame will be duplicated 5 times.
     *                  {x:0, y:100, w:50, h:50, nbFrames: 5, way: "RIGHT" }   //We take 5 frames, moving on the right.
     *              ]
     *          },
     *          walk_right: {
     *              framerate: 12,
     *              alias: "walk_left",
     *              flipX: true
     *          }
     *     };
	 *
	 *  Now let me introduce you the last feature which allows you to reverse the frames order of an animation.
	 *  There's some case where you will need to reverse the frames of an animation, especially when you animation
	 *  is an alias of another.
	 *  Let's took our previous example, now, i want to add two moonwalk animation (moonwalk_left, moonwalk_right).
	 *  To make them i will apply to them respectively an alias of the walk_left and walk_right, and then, i will apply
	 *  to moonwalk_right and moonwalk_right the reverse flag which will reverse the frames that the animation contains.
	 *
	 *     var config = {
     *          default: {
     *              x: 0,
     *              y: 0,
     *              w: 50,
     *              h: 50,
     *              framerate: 25
     *          },
     *          walk_left: {
     *              framerate: 12,
     *              frames: [
     *                  {x:0, y:0, w: 50, h: 50 },
     *                  {x:50, y:0, w:50, h:50 },
     *                  {x:0, y:50, w:50, h:50, nbFrames: 5 },                 //This frame will be duplicated 5 times.
     *                  {x:0, y:100, w:50, h:50, nbFrames: 5, way: "RIGHT" }   //We take 5 frames, moving on the right.
     *              ]
     *          },
     *          walk_right: {
     *              framerate: 12,
     *              alias: "walk_left",
     *              flipX: true
     *          },
     *          moonwalk_left: {
     *              framerate: 12,
     *              alias: "walk_right",
     *              reverse: true           //We set our moonwalk_left animation to be reversed.
     *          },
     *          moonwalk_right: {
     *              framerate: 12,
     *              alias: "walk_left",
     *              reverse: true           //We set out moonwalk_right animation to be reversed.
     *          }
     *     };
	 *
	 * ## Hotpoint
	 *
	 * As it's possible to define frames of any size, two frames in an animation can be of differents sizes.
	 * However, this fact hides a problem : if the element must change size, how to enlarge the sprite ?
	 * by the right or the left ? Up or bottom ?
	 *
	 * Usually, we can use the `centerPoint`. If it's defined in the up-left corner,
	 * the sprite will grow toward bottom and right. If it's defined on the center, the sprite will grow from all sides.
	 *
	 * But it's not always the best choice. So, you can redefine the centerPoint for the transition
	 * with the `hotpoint` param. It can take these following values :
	 *
	 *  - `TOP-LEFT`
	 *  - `TOP-CENTER`
	 *  - `TOP-RIGHT`
	 *  - `CENTER-LEFT`
	 *  - `CENTER-CENTER`
	 *  - `CENTER-RIGHT`
	 *  - `BOTTOM-LEFT`
	 *  - `BOTTOM-CENTER`
	 *  - `BOTTOM-RIGHT`
	 *
	 *
	 * Example:
	 *
	 *     grow_up: {
     *          framerate: 12,
     *          frames: [
     *              {x:0, y:0, w:50, h:50 },
     *              {x:50, y:0, w:50, h:150, hotpoint: "BOTTOM-CENTER" }
     *              //the height grow from 50 to 150
     *              //but the center bottom point will not move.
     *          ]
     *     }
	 *
	 */
	function SpriteSheet(image, config) {

		/**
		 * image used by the srpitesheet.
		 *
		 * @property {Image} image
		 */
		this.image = image;

		this.listAnimation = {};



		this.config = clone(config);

		for (var a in this.config) {
			if (a !== "default") {
				if (this.config[a].alias) {
					if (this.listAnimation[this.config[a].alias]) {
						this.config[a].frames = clone(this.listAnimation[this.config[a].alias].frames);
					}
				}
				this.developAnimationFrames(this.config[a]);
				if (this.config[a].reverse && this.config[a].reverse === true) {
					this.config[a].frames.reverse();
				}
			}
			this.listAnimation[a] = this.config[a];
		}
	}

	/**
	 * This function is used internally by the SpriteSheet class to apply the default values in the frames.
	 * @method applyDefaultValuesToFrames
	 * @param {Object} animation object which contains the description of the animation to apply default values.
	 * @private
	 */
	SpriteSheet.prototype.applyDefaultValuesToFrames = function(animation) {
		if (!this.config['default']) {
			return;
		}
		if (this.config['default'].framerate) {
			if (!animation.framerate) {
				animation.framerate = this.config['default'].framerate;
			}
		}
		if (this.config['default'].flipX) {
			if (!animation.flipX) {
				animation.flipX = this.config['default'].flipX;
			}
		}
		if (this.config['default'].flipY) {
			if (!animation.flipY) {
				animation.flipY = this.config['default'].flipY;
			}
		}
		if (this.config['default'].reverse) {
			if (!animation.reverse) {
				animation.reverse = this.config['default'].reverse;
			}
		}
		if (!animation.frames) {
			return;
		}
		for (var i = 0; i < animation.frames.length; i++) {
			if (this.config['default'].x) {
				if (!animation.frames[i].x) {
					animation.frames[i].x = this.config['default'].x;
				}
			}
			if (this.config['default'].y) {
				if (!animation.frames[i].y) {
					animation.frames[i].y = this.config['default'].y;
				}
			}
			if (this.config['default'].w) {
				if (!animation.frames[i].w) {
					animation.frames[i].w = this.config['default'].w;
				}
			}
			if (this.config['default'].h) {
				if (!animation.frames[i].h) {
					animation.frames[i].h = this.config['default'].h;
				}
			}
			if (this.config['default'].nbFrames) {
				if (!animation.frames[i].nbFrames) {
					animation.frames[i].nbFrames = this.config['default'].nbFrames;
				}
			}
		}
	};

	/**
	 * This function is private and have the aim to autoincrement each frame duplicated in order to generate animation.
	 * @param {Object} frame frame which will be transformed
	 * @method _applyFrameIncrementation
	 * @private
	 */
	SpriteSheet.prototype._applyFrameIncrementation = function(frame) {
		if (!frame.way) {
			return;
		}
		switch (frame.way) {
			case "LEFT" :
				frame.x -= frame.w;
				if (frame.x < 0) {
					frame.x = this.image.width - frame.w;
					frame.y += frame.h;
				}
				break;
			case "RIGHT" :
				frame.x += frame.w;
				if (frame.x + frame.w > this.image.width) {
					frame.x = 0;
					frame.y += frame.h;
				}
				break;
			case "UP" :
				frame.y -= frame.h;
				if (frame.y < 0) {
					frame.x += frame.w;
					frame.y = this.image.height - frame.h;
				}
				break;
			case "DOWN" :
				frame.y += frame.h;
				if (frame.y + frame.h > this.image.height) {
					frame.x += frame.w;
					frame.y = 0;
				}
				break;
		}
		//delete frame.way;
	};

	SpriteSheet.prototype._setLitteralHotPoint = function(frame, stringHotpoint) {
		var hotPoint = {
			x: 0,
			y: 0
		};

		switch (stringHotpoint) {
			case "LEFT-TOP":
				hotPoint.x = 0;
				hotPoint.y = 0;
				break;
			case "CENTER-TOP":
				hotPoint.x = frame.w / 2;
				hotPoint.y = 0;
				break;
			case "RIGHT-TOP":
				hotPoint.x = frame.w;
				hotPoint.y = 0;
				break;
			case "LEFT-CENTER":
				hotPoint.x = 0;
				hotPoint.y = frame.h / 2;
				break;
			case "CENTER-CENTER":
				hotPoint.x = frame.w / 2;
				hotPoint.y = frame.h / 2;
				break;
			case "RIGHT-CENTER":
				hotPoint.x = frame.w;
				hotPoint.y = frame.h / 2;
				break;
			case "LEFT-BOTTOM":
				hotPoint.x = 0;
				hotPoint.y = frame.h;
				break;
			case "CENTER-BOTTOM":
				hotPoint.x = frame.w / 2;
				hotPoint.y = frame.h;
				break;
			case "RIGHT-BOTTOM":
				hotPoint.x = frame.w;
				hotPoint.y = frame.h;
				break;
		}
		frame.hotpoint = hotPoint;
	};

	/**
	 * The _applyHotPoint is private and set some parameters about the hot points.
	 * @method _applyHotPoint
	 * @param animationEntry
	 * @param frames
	 * @private
	 */
	SpriteSheet.prototype._applyHotPoint = function(animationEntry, frames) {
		var xHotPoint;
		var yHotPoint;

		if (animationEntry.hotpoint) {
			for (var i = 0; i < frames.length; i++) {
				if (typeof animationEntry.hotpoint === "string") {
					this._setLitteralHotPoint(frames[i], animationEntry.hotpoint);
				} else {
					if (!animationEntry.hotpoint.x || !animationEntry.hotpoint.y ||
					    isNaN(animationEntry.hotpoint.x) || isNaN(animationEntry.hotpoint.y)) {
						return;
					}
					xHotPoint = animationEntry.hotpoint.x;
					yHotPoint = animationEntry.hotpoint.y;
					frames[i].hotpoint = {x: xHotPoint, y: yHotPoint};
				}
			}
		}
	};

	/**
	 * This function is private and have the aim to clone an animation entry.
	 * @private
	 * @method developAnimationFrames
	 * @param {Object} animationEntry
	 */
	SpriteSheet.prototype.developAnimationFrames = function(animationEntry) {
		var newFrames = [];
		var offset = 0;
		var frameClone;

		this.applyDefaultValuesToFrames(animationEntry);
		if (!animationEntry.frames) {
			return;
		}
		for (var i = 0; i < animationEntry.frames.length; i++) {
			if (animationEntry.frames[i].nbFrames && animationEntry.frames[i].nbFrames >= 1) {
				for (var j = 0; j < animationEntry.frames[i].nbFrames; j++) {
					if (j === 0) {
						frameClone = clone(animationEntry.frames[i]);
					} else {
						frameClone = clone(newFrames[offset - 1]);
					}
					if (j > 0) {
						this._applyFrameIncrementation(frameClone);
					}
					newFrames.push(frameClone);
					delete newFrames[offset].nbFrames;
					offset++;
				}
			} else {
				newFrames.push(clone(animationEntry.frames[i]));
			}
		}
		this._applyHotPoint(animationEntry, newFrames);
		animationEntry.frames = newFrames;
	};

	/**
	 * addAnimation method allows you to add an animation to the current SpriteSheet object.
	 * Note that if you add an animation with the same name than a previous one, the older will be overwritted.
	 *
	 * @method addAnimation
	 * @param {String} name it is the name of the animation
	 * @param {Object} config it is an object which contains the description of the name animation.
	 */
	SpriteSheet.prototype.addAnimation = function(name, config) {
		this.config[name] = clone(config);
		if (this.config[name].alias) {
			if (this.listAnimation[this.config[name].alias]) {
				this.config[name].frames = clone(this.listAnimation[this.config[name].alias].frames);
			}
		}
		this.developAnimationFrames(this.config[name]);
		this.listAnimation[name] = this.config[name];
	};

	/**
	 * rmAnimation method allows you to suppress an animation from the current SpriteSheet object.
	 *
	 * @method rmAnimation
	 * @param {String} name it is the name of the animation to delete
	 * @return {Boolean} the rmAnimation returns true if the name animation have been successfuly suppressed
	 * from the current SpriteSheet object.
	 */
	SpriteSheet.prototype.rmAnimation = function(name) {
		if (this.config[name] && this.listAnimation[name]) {
			delete this.config[name];
			delete this.listAnimation[name];
			return true;
		} else {
			return false;
		}
	};

	/**
	 * getAnimation method allows you to get a description of the name animation.
	 *
	 * @method getAnimation
	 * @param {String} name it is the name of the animation which you want to get the description
	 * @return {Object} the getAnimation method returns the description of the name animation on success.
	 * Otherwise it returns null
	 */
	SpriteSheet.prototype.getAnimation = function(name) {
		if (this.config[name]) {
			return clone(this.config[name]);
		} else {
			return null;
		}
	};


	/**
	 * setAnimation method allows you to set a new animation inside of the current SpriteSheet object.
	 *
	 * @method setAnimation
	 * @param {String} name it is the name of the new animation to be set.
	 * @param {Object} config it is the config object which describe the name animation.
	 */
	SpriteSheet.prototype.setAnimation = function(name, config) {
		this.addAnimation(name, config);
	};

	/**
	 * getListAnim method allows you to get the list of the animation contained inside of the
	 * current SpriteSheet object.
	 *
	 * @method getListAnim
	 * @return {Object} returns an object containing the animations of the SpriteSheet object.
	 */
	SpriteSheet.prototype.getListAnimation = function() {
		return this.config;
	};


	TW.Graphic.SpriteSheet = SpriteSheet;
	return SpriteSheet;
});