/**
* A powerful, easy-to-use Sprite animation library for HTML5 Canvas.
*
* @author Isaac Sukin (IceCreamYou)
* @license MIT License: http://opensource.org/licenses/mit-license.php
* @ignore
*/
// BEGIN SPRITE MAP LIBRARY ===================================================
(function() {
/**
* Manage multiple sprite animations in the same sprite sheet.
*
* All methods except SpriteMap#clone are chainable (they return the SpriteMap
* instance).
*
* @constructor
* Creates a new SpriteMap instance.
*
* @param {String} src
* The image to draw, in the form of one of the following:
*
* - The file path of the base image
* - An Image
* - A Canvas
* @param {Object} animations
* A map (Object) where the keys are the names of animation sequences and the
* values are maps (Objects) specifying the starting and ending frames of the
* relevant animation sequence. All properties are optional:
*
* - startRow: The row at which to start the animation sequence. Defaults to
* 0 (zero) - the first row.
* - startCol: The column at which to start the animation sequence. Defaults
* to 0 (zero) - the first column.
* - endRow: The row at which to end the animation sequence. Defaults to the
* last row.
* - endCol: The column at which to end the animation sequence. Defaults to
* the last column.
* - squeeze: Determines which frames are included in the animation loop. If
* set to true, frames are constrained within startCol and endCol,
* regardless of the row. If set to false (the default), frames will run to
* the last column in the Sprite and then loop back to the first column on
* the next row in the Sprite until reaching the last frame in the loop.
* More details on how this work are documented in the {@link Sprite}
* constructor.
* - flipped: An object with "horizontal" and "vertical" properties
* (both Booleans) indicating whether the Sprite should be drawn flipped
* along the horizontal or vertical axes.
*
* Alternatively, instead of the inner values being Objects with the
* properties specified above, they can be Arrays that hold the same values
* (in the same order). This is less clear to read, but more concise to
* write.
* @param {Object} options
* This parameter is the same as the options parameter for the {@link Sprite}
* class.
*/
function SpriteMap(src, animations, options) {
var origPIC = typeof options.postInitCallback == 'function' ? options.postInitCallback : null;
var t = this;
options.postInitCallback = function(sprite) {
t.sprite = sprite;
t.baseImage = sprite.image;
t.cachedImages = {'00': t.baseImage};
t.maps = {};
for (var name in animations) {
if (animations.hasOwnProperty(name)) {
t.set(name, animations[name]);
}
}
if (origPIC) {
origPIC.apply(this, arguments);
}
};
this.sprite = new Sprite(src, options);
this.sprite.spriteMap = this;
}
SpriteMap.prototype = {
/**
* Add or modify an animation sequence.
*
* @param {String} name
* The name of the sequence.
* @param {Object/Array} [options]
* Specifies the frames of the animation sequence. If an Array is passed,
* the values should be included in the order below.
* @param {Number} [options.startRow=0]
* The index of the sequence's starting row.
* @param {Number} [options.startCol=0]
* The index of the sequence's starting column.
* @param {Number} [options.endRow]
* The index of the sequence's ending row. Defaults to the Sprite's last
* row.
* @param {Number} [options.endCol]
* The index of the sequence's ending column. Defaults to the Sprite's last
* column.
* @param {Boolean} [options.squeeze=false]
* Determines which frames are included in the animation loop. If set to
* true, frames are constrained within startCol and endCol, regardless of
* the row. If set to false (the default), frames will run to the last
* column in the Sprite and then loop back to the first column on the next
* row in the Sprite until reaching the last frame in the loop. More
* details on how this work are documented in the {@link Sprite}
* constructor.
* @param {Object} [options.flipped={horizontal: false, vertical: false}]
* An object with "horizontal" and "vertical" properties (both Booleans)
* indicating whether the Sprite should be drawn flipped along the
* horizontal or vertical axes.
* @param {Boolean} [options.flipped.horizontal=false]
* @param {Boolean} [options.flipped.vertical=false]
*/
set: function(name, options) {
if (options instanceof Array) {
options = {
startRow: options[0],
startCol: options[1],
endRow: options[2],
endCol: options[3],
squeeze: options[4],
flipped: options[5]
};
}
this.maps[name] = {
startRow: typeof options.startRow !== 'undefined' ? options.startRow : 0,
startCol: typeof options.startCol !== 'undefined' ? options.startCol : 0,
endRow: typeof options.endRow !== 'undefined' ? options.endRow : this.sprite.rows-1,
endCol: typeof options.endCol !== 'undefined' ? options.endCol : this.sprite.cols-1,
squeeze: typeof options.squeeze !== 'undefined' ? options.squeeze : false,
flipped: typeof options.flipped !== 'undefined' ? options.flipped : {horizontal: false, vertical: false}
};
// Pre-render the flipped versions of the image we know we'll need to use.
var f = this.maps[name].flipped,
key = (f.horizontal ? '1' : '0') + (f.vertical ? '1' : '0');
if (typeof this.sprite.cachedImages[key] === 'undefined') {
this.sprite.cachedImages[key] = this.sprite._prerender(this.baseImage, f);
}
return this;
},
/**
* Remove an animation sequence.
*
* @param {String} name
* The animation sequence to remove.
*/
unset: function(name) {
if (this.maps.hasOwnProperty(name)) {
delete this.maps[name];
}
return this;
},
/**
* Switch the active animation sequence.
*
* @param {String} name
* The name of the animation sequence to switch to.
* @param {Boolean} [restartIfInUse=false]
* A Boolean indicating whether to restart the animation sequence if the
* specified sequence is already in use.
*/
use: function(name, restartIfInUse) {
if (this.activeLoop == name && !restartIfInUse) {
return this;
}
/**
* @property {String} activeLoop
* The name of the active animation sequence.
*/
this.activeLoop = name;
var m = this.maps[name];
this.sprite.setLoop(m.startRow, m.startCol, m.endRow, m.endCol, m.squeeze, m.flipped);
return this;
},
/**
* Start the animation sequence.
*
* @param {String} [name]
* The name of the animation sequence to start. If not given, defaults to
* the active animation sequence. If no animation sequence is active, the
* default sequence is to show the whole sprite sheet.
*/
start: function(name) {
if (name) {
this.use(name);
}
this.sprite.startLoop();
return this;
},
/**
* Stop the currently running animation sequence.
*/
stop: function() {
this.sprite.stop();
return this;
},
/**
* Reset the active animation sequence to the first frame.
*
* If the sequence is running when SpriteMap#reset() is called, it will still
* be running afterwards, so usually SpriteMap#stop() is called first.
*/
reset: function() {
this.sprite.reset();
return this;
},
/**
* Run an animation sequence once.
*
* @param {Function} [callback]
* A function to call after the animation sequence is done running.
* @param {Sprite} [callback.sprite]
* The Sprite that was animated. Its "spriteMap" property holds the parent
* SpriteMap.
* @param {String} [name]
* The name of the animation sequence to start. If not given, defaults to
* the active animation sequence. If no animation sequence is active, the
* default sequence is to show the whole sprite sheet.
*/
runOnce: function(callback, name) {
if (name) {
this.use(name);
}
this.sprite.runLoop(callback);
return this;
},
/**
* Draw the sprite's current frame.
*
* @param {CanvasRenderingContext2D} ctx
* The canvas graphics context onto which the sprite should be drawn.
* @param {Number} x
* The x-coordinate of the canvas graphics context at which the upper-left
* corner of the Sprite should be drawn. This is usually (but not always)
* the horizontal distance in pixels from the left side of the canvas.
* @param {Number} y
* The y-coordinate of the canvas graphics context at which the upper-left
* corner of the Sprite should be drawn. This is usually (but not always)
* the vertical distance in pixels from the top of the canvas.
* @param {Number} [w]
* The width of the image when drawn onto the canvas. Defaults to the
* Sprite's projected width, which in turn defaults to the frame width.
* @param {Number} [h]
* The height of the image when drawn onto the canvas. Defaults to the
* Sprite's projected height, which in turn defaults to the frame height.
*/
draw: function(ctx, x, y, w, h) {
this.sprite.draw(ctx, x, y, w, h);
return this;
},
/**
* Clone the SpriteMap.
*
* @return {SpriteMap}
* A SpriteMap instance that is identical to the current instance.
*/
clone: function() {
return new SpriteMap(this.sprite.sourceFile, this.maps, this.sprite);
}
};
this.SpriteMap = SpriteMap;
}).call(this);
// END SPRITE MAP LIBRARY =====================================================
// BEGIN SPRITE ANIMATION LIBRARY =============================================
(function() {
/**
* Support sprite animation.
*
* - Animations are always run left-to-right, top-to-bottom.
* - All frames in the loop are assumed to be the same size.
* - Rows and columns (the row, col, startRow, startCol, endRow, and endCol
* properties) are zero-indexed, while frame number starts at 1. Usually
* frame 1 will have row and column values (0, 0).
* - Use {@link SpriteMap}s to maintain multiple loops in the same image.
* - This class assumes that the properties passed in make sense (i.e. the
* starting cell occurs before the ending cell, the image has nonzero
* dimensions, etc.). Otherwise behavior is undefined.
* - All public methods that do not exist to get specific values return `this`
* (and therefore are chainable).
*
* @constructor
* Creates a new Sprite instance.
*
* @param {String} src
* The image to draw, in the form of one of the following:
*
* - The file path of the base image
* - An Image
* - A Canvas
* @param {Object} [options]
* An object whose properties affect how the sprite is animated. Each of the
* properties will be attached to the Sprite object directly, along with
* other calculated properties. It is best to call Sprite#getFrame() if you
* need information about the currently displayed frame. You can read other
* properties if you need to, but it is strongly recommended not to set
* properties directly because the resulting behavior is undefined.
* @param {Number} [options.frameW]
* The width of each frame of the sprite. Defaults to the image width.
* @param {Number} [options.frameH]
* The height of each frame of the sprite. Defaults to the image height.
* @param {Number} [options.projectedW]
* The width of each frame when it is displayed on the canvas (allowing you
* to scale the frame). Defaults to the frame width.
* @param {Number} [options.projectedH]
* The height of each frame when it is displayed on the canvas (allowing you
* to scale the frame). Defaults to the frame height.
* @param {Number} [options.startRow=0]
* The row at which the animation loop should start.
* @param {Number} [options.startCol=0]
* The column at which the animation loop should start.
* @param {Number} [options.endRow]
* The row at which the animation loop should stop. Animations will run from
* (startRow, startCol) to (endRow, endCol), inclusive. Defaults to the last
* row in the image.
* @param {Number} [options.endCol]
* The column at which the animation loop should stop. Animations will run
* from (startRow, startCol) to (endRow, endCol), inclusive. Defaults to the
* last column in the image.
* @param {Boolean} [options.squeeze=false]
* By default, animation loops are assumed to run all the way to the end of
* each row before continuing at the start of the next row. For example, a
* valid arrangement of loops in an image might look like this:
*
* AAAA
* AABB
* BBBC
* CCDD
* DDDD
*
* In this example, the "C" loop starts at (2, 2) and ends at (3, 1).
* However, if the squeeze option is set to true, loops will be contained
* inside startCol and endCol. For example, a valid arrangement of loops in
* an image might look like this:
*
* AABB
* AABB
* AACC
* DDCC
*
* Now the "C" loop starts at (2, 2) and ends at (3, 3) and all its frames
* occur within the box formed by those coordinates.
* @param {Number} [options.interval=125]
* The delay in milliseconds before switching frames when running the
* animation loop.
* @param {Boolean} [options.useTimer=true]
* If true, Sprite animation loops rely on setInterval() to update their
* frames regularly (this is the default). If false, the Sprite will rely on
* being drawn as the "tick" that lets it update its frames. This can be
* slightly less accurate than using a timer (assuming the sprite gets drawn
* on every canvas repaint; otherwise it can be a lot less accurate, and in
* any case it can be up to 15ms off on Windows) but it is more
* performance-friendly and also ensures that frames will never skip if the
* sprite is not drawn.
* @param {Boolean} [options.advanceFramesManually=false]
* If options.useTimer is false and this setting is true, frames will not be
* advanced automatically and must be advanced manually instead (i.e. using
* Sprite#nextFrame() or Sprite#changeFrame()).
* @param {Object} [options.flipped={horizontal: false, vertical: false}]
* An object with "horizontal" and "vertical" properties (both Booleans)
* indicating whether the Sprite should be drawn flipped along the horizontal
* or vertical axes.
* @param {Function} [options.postInitCallback]
* A function that will run at the end of the initialization process (if the
* source image has not been loaded before, this will be after the image has
* been fully loaded asynchronously). If the source image was not pre-loaded
* and you draw() the Sprite before this callback is invoked, nothing will be
* drawn because the image won't be loaded yet.
* @param {Sprite} [options.postInitCallback.sprite]
* The Sprite that was loaded.
*/
function Sprite(src, options) {
// String image file path
if (typeof src == 'string') {
this.sourceFile = src;
var cachedImage = Sprite.getImageFromCache(src);
if (cachedImage) { // cached
this._init(cachedImage, options);
}
else { // not cached
var image = new Image(), t = this;
image.onload = function() {
t._init(this, options);
};
image._src = src;
image.src = src;
Sprite.saveImageToCache(src, image);
}
}
// Image
else if (src instanceof HTMLImageElement || src instanceof Image) {
if (!src.src) {
return;
}
this.sourceFile = src._src || src.src;
if (src.complete || (src.width && src.height)) { // loaded
Sprite.saveImageToCache(this.sourceFile, src);
this._init(src, options);
}
else { // not loaded
if (src._src) { // We've already tried to draw this one
return; // The onload callback will initialize the sprite when it runs
}
var o = src.onload, t = this;
src.onload = function() {
if (typeof o == 'function') { // don't overwrite any existing handler
o();
}
Sprite.saveImageToCache(t.sourceFile, src);
t._init(this, options);
};
}
}
// Canvas
else if (src instanceof HTMLCanvasElement) {
this._init(src, options);
}
}
Sprite.prototype = {
// Calculates and stores initial values based on a loaded image.
_init: function(img, options) {
this.width = img.width;
this.height = img.height;
this.frameW = options.frameW || img.width;
this.frameH = options.frameH || img.height;
this.projectedW = options.projectedW || this.frameW;
this.projectedH = options.projectedH || this.frameH;
this.rows = Math.floor(this.height / this.frameH);
this.cols = Math.floor(this.width / this.frameW);
this.startRow = options.startRow || 0;
this.startCol = options.startCol || 0;
this.endRow = (typeof options.endRow === 'undefined' ? this.rows-1 : options.endRow);
this.endCol = (typeof options.endCol === 'undefined' ? this.cols-1 : options.endCol);
this.row = this.startRow;
this.col = this.startCol;
this.frame = 1;
this.squeeze = options.squeeze || false;
this.interval = (typeof options.interval === 'undefined' ? 125 : options.interval);
this.useTimer = (typeof options.useTimer === 'undefined' ? true : options.useTimer);
this.advanceFramesManually = options.advanceFramesManually || false;
this.lastFrameUpdateTime = 0;
this.flipped = options.flipped || {horizontal: false, vertical: false};
this.flipped.horizontal = this.flipped.horizontal || false;
this.flipped.vertical = this.flipped.vertical || false;
this.cachedImages = {'00': img};
var f = this.flipped,
key = (f.horizontal ? '1' : '0') + (f.vertical ? '1' : '0');
if (typeof this.cachedImages[key] === 'undefined') {
this.cachedImages[key] = this._prerender(img, f);
}
this.image = this.cachedImages[key];
this._runOnce = false;
if (this.squeeze) {
this.cols = this.endCol - this.startCol + 1;
}
this.numFrames = this.getNumFrames();
if (options.postInitCallback) {
options.postInitCallback(this);
}
},
/**
* Pre-render the image onto a canvas.
*
* Canvases typically draw faster than images, especially when flipped.
*
* @return {HTMLCanvasElement} The prerendered flipped image.
* @ignore
*/
_prerender: function(image, flipped) {
var tempCanvas = document.createElement('canvas');
tempCanvas.width = this.width;
tempCanvas.height = this.height;
var tempContext = tempCanvas.getContext('2d');
if (flipped.horizontal || flipped.vertical) {
tempContext.translate(flipped.horizontal ? tempCanvas.width : 0,
flipped.vertical ? tempCanvas.height : 0);
tempContext.scale(flipped.horizontal ? -1 : 1,
flipped.vertical ? -1 : 1);
}
tempContext.drawImage(image, 0, 0);
return tempCanvas;
},
/**
* Draws the sprite.
*
* @param {CanvasRenderingContext2D} ctx
* The canvas graphics context onto which the sprite should be drawn.
* @param {Number} x
* The x-coordinate of the canvas graphics context at which the upper-left
* corner of the Sprite should be drawn. This is usually (but not always)
* the horizontal distance in pixels from the left side of the canvas.
* @param {Number} y
* The y-coordinate of the canvas graphics context at which the upper-left
* corner of the Sprite should be drawn. This is usually (but not always)
* the vertical distance in pixels from the top of the canvas.
* @param {Number} [w]
* The width of the image when drawn onto the canvas. Defaults to the
* Sprite's projected width, which in turn defaults to the frame width.
* @param {Number} [h]
* The height of the image when drawn onto the canvas. Defaults to the
* Sprite's projected height, which in turn defaults to the frame height.
*/
draw: function(ctx, x, y, w, h) {
try {
ctx.save();
w = w || this.projectedW;
h = h || this.projectedH;
var xOffset = this.col * this.frameW,
yOffset = this.row * this.frameH;
if (this.flipped.horizontal) {
xOffset = this.width - xOffset - this.frameW;
}
if (this.flipped.vertical) {
yOffset = this.height - yOffset - this.frameH;
}
ctx.drawImage(
this.image, // image
xOffset, // image x-offset
yOffset, // image y-offset
this.frameW, // image slice width
this.frameH, // image slice height
x, // canvas x-position
y, // canvas y-position
w, // slice width on canvas
h // slice height on canvas
);
ctx.restore();
} catch(e) {
if (console && console.error) {
// Usually the reason you would get an error here is if you tried to
// draw() an image before it was fully loaded. That's an ignore-able
// error, because if you're animating, the image will just pop in when
// it loads.
console.error(e);
}
}
if (!this.useTimer && !this.advanceFramesManually &&
Date.now() - this.lastFrameUpdateTime > this.interval) {
this.nextFrame();
}
return this;
},
/**
* Reset the animation to its first frame.
*
* Usually you will want to call Sprite#stopLoop() immediately before
* Sprite#reset(); otherwise the animation will keep running (if it was
* running already).
*/
reset: function() {
this.row = this.startRow, this.col = this.startCol, this.frame = 1;
this.lastFrameUpdateTime = 0;
return this;
},
/**
* Move forward or backward a specified number of frames.
*
* @param {Number} delta
* The number of frames by which to move forward or backward (negative
* values move backward).
*/
changeFrame: function(delta) {
this.frame += delta;
this.setFrame(this.frame);
return this;
},
/**
* Moves to a specific frame in the animation loop.
*
* This function supports passing either a frame number or row and column
* coordinates as parameters. Frames outside of the accepted range will
* overflow/underflow.
*
* You may want to call Sprite#stopLoop() immediately before
* Sprite#setFrame(); otherwise the animation will keep running (if it was
* running already).
*
* @param {Number} row
* The row of the frame to which to switch.
* @param {Number} col
* The column of the frame to which to switch.
*/
setFrame: function(row, col) {
if (typeof col !== 'undefined') {
this.row = row, this.col = col;
if (this.squeeze) {
this.frame = this.cols * (this.row - this.startRow + 1) -
(this.endCol - this.col);
}
else {
this.frame = this.cols * (this.row - this.startRow + 1) -
(this.cols - (this.col + 1)) - (this.startCol);
}
}
else {
var props = this.frameNumberToRowCol(row);
this.frame = props.frame, this.row = props.row, this.col = props.col;
}
this.lastFrameUpdateTime = Date.now();
return this;
},
/**
* Sets the range of frames over which the sprite should loop.
*
* @param {Number} startRow
* The row of the frame at which animation should start.
* @param {Number} startCol
* The column of the frame at which animation should start.
* @param {Number} [endRow]
* The row of the frame at which animation should end. Defaults to the last
* row in the image.
* @param {Number} [endCol]
* The column of the frame at which animation should end. Defaults to the
* last column in the image.
* @param {Boolean} [squeeze=false]
* A Boolean determining whether startCol and endCol define a box within
* which to find frames for this animation, or whether frames from any
* column can be used (after startCol in startRow and before endCol in
* endRow). For more information on how this works, see the documentation
* for the {@link Sprite} constructor.
* @param {Object} [flipped]
* An object with "horizontal" and "vertical" properties (both Booleans)
* indicating whether the Sprite should be drawn flipped along the
* horizontal or vertical axes. Defaults to the flipped setting for the
* current animation sequence.
*/
setLoop: function(startRow, startCol, endRow, endCol, squeeze, flipped) {
this.stopLoop();
if (endRow === null || typeof endRow === 'undefined') {
endRow = this.rows-1;
}
if (endCol === null || typeof endCol === 'undefined') {
endCol = this.cols-1;
}
if (typeof squeeze !== 'undefined') {
this.squeeze = squeeze;
}
if (typeof flipped !== 'undefined') {
this.flipped = flipped;
var f = this.flipped,
key = (f.horizontal ? '1' : '0') + (f.vertical ? '1' : '0');
if (typeof this.cachedImages[key] === 'undefined') {
this.cachedImages[key] = this._prerender(this.image, f);
}
this.image = this.cachedImages[key];
}
this.startRow = startRow, this.startCol = startCol,
this.endRow = endRow, this.endCol = endCol;
this.reset();
this.numFrames = this.getNumFrames();
return this;
},
/**
* Starts running a new animation loop.
*
* Usually this function will be called without parameters since it defaults
* to using the sprite's settings defined at instantiation time. In cases
* where the frames that should be used in an animation change, this function
* takes the same parameters as Sprite#setLoop() for convenience; using these
* parameters is equivalent to calling sprite.setLoop(params).startLoop().
*
* @param {Number} [startRow]
* The row of the frame at which animation should start. Defaults to the
* starting row of the current animation sequence.
* @param {Number} [startCol]
* The column of the frame at which animation should start. Defaults to the
* starting column of the current animation sequence.
* @param {Number} [endRow]
* The row of the frame at which animation should end. Defaults to the
* ending row of the current animation sequence unless startRow and
* startCol are specified, in which case it defaults to the last row in the
* image.
* @param {Number} [endCol]
* The column of the frame at which animation should end. Defaults to the
* ending column of the current animation sequence unless startRow and
* startCol are specified, in which case it defaults to the last column in
* the image.
* @param {Boolean} [squeeze]
* A Boolean determining whether startCol and endCol define a box within
* which to find frames for this animation, or whether frames from any
* column can be used (after startCol in startRow and before endCol in
* endRow). For more information on how this works, see the documentation
* for the {@link Sprite} constructor. Defaults to the squeeze setting for
* the current animation sequence unless startRow and startCol are
* specified, in which case it defaults to false.
* @param {Object} [flipped]
* An object with "horizontal" and "vertical" properties (both Booleans)
* indicating whether the Sprite should be drawn flipped along the
* horizontal or vertical axes. Defaults to the flipped setting for the
* current animation sequence.
*/
startLoop: function(startRow, startCol, endRow, endCol, squeeze, flipped) {
if (typeof startRow !== 'undefined' && typeof startCol !== 'undefined') {
this.setLoop(startRow, startCol, endRow, endCol, squeeze, flipped);
}
this.lastFrameUpdateTime = Date.now();
if (this.interval && this.useTimer) {
var t = this;
this.intervalID = setInterval(function() { t.nextFrame(); }, this.interval);
}
return this;
},
/**
* Stops running the animation loop.
*/
stopLoop: function() {
this.lastFrameUpdateTime = 0;
if (this.intervalID) {
clearInterval(this.intervalID);
}
return this;
},
/**
* Runs the animation loop once.
*
* The loop concludes at the final frame and does not reset to the first
* frame. Use the callback function to reset it if you need that behavior.
*
* Usually this function will be called without parameters since it defaults
* to using the sprite's settings defined at instantiation time. In cases
* where the frames that should be used in an animation change, this function
* takes the same parameters as Sprite#setLoop() for convenience; using these
* parameters is equivalent to calling sprite.setLoop(params).startLoop().
*
* @param {Function} [callback]
* A callback function to run after the loop has completed, or a falsey
* value to skip this argument.
* @param {Sprite} [callback.sprite]
* The Sprite that was animated.
* @param {Number} [startRow]
* The row of the frame at which animation should start. Defaults to the
* starting row of the current animation sequence.
* @param {Number} [startCol]
* The column of the frame at which animation should start. Defaults to the
* starting column of the current animation sequence.
* @param {Number} [endRow]
* The row of the frame at which animation should end. Defaults to the
* ending row of the current animation sequence unless startRow and
* startCol are specified, in which case it defaults to the last row in the
* image.
* @param {Number} [endCol]
* The column of the frame at which animation should end. Defaults to the
* ending column of the current animation sequence unless startRow and
* startCol are specified, in which case it defaults to the last column in
* the image.
* @param {Boolean} [squeeze]
* A Boolean determining whether startCol and endCol define a box within
* which to find frames for this animation, or whether frames from any
* column can be used (after startCol in startRow and before endCol in
* endRow). For more information on how this works, see the documentation
* for the {@link Sprite} constructor. Defaults to the squeeze setting for
* the current animation sequence unless startRow and startCol are
* specified, in which case it defaults to false.
* @param {Object} [flipped]
* An object with "horizontal" and "vertical" properties (both Booleans)
* indicating whether the Sprite should be drawn flipped along the
* horizontal or vertical axes. Defaults to the flipped setting for the
* current animation sequence unless startRow and startCol are specified,
* in which case it defaults to {horizontal: false, vertical: false}.
*/
runLoop: function(callback, startRow, startCol, endRow, endCol, squeeze, flipped) {
this.runLoopCallback = callback || function() {};
this._runOnce = true;
Array.prototype.shift.call(arguments);
this.startLoop.apply(this, arguments);
return this;
},
/**
* Goes back one frame in the animation loop.
*
* This is equivalent to Sprite#changeFrame(-1). It is provided as a
* convenience and to complement Sprite#nextFrame().
*/
prevFrame: function() {
changeFrame(-1);
return this;
},
/**
* Advances one frame in the animation loop.
*
* This is equivalent to (but more efficient than) Sprite#changeFrame(1).
*/
nextFrame: function() {
this.col++;
this.frame++;
if (this.row == this.endRow && this.col > this.endCol) {
if (this._runOnce) {
this.stopLoop();
this._runOnce = false;
this.runLoopCallback(this);
}
else {
this.reset();
}
}
else if (this.squeeze && this.col > this.endCol) {
this.col = this.startCol;
this.row++;
}
else if (!this.squeeze && this.col >= this.cols) {
this.col = 0;
this.row++;
}
this.lastFrameUpdateTime = Date.now();
return this;
},
/**
* Returns an object containing the current "row," "col," and "frame" number.
*
* Row and Col are zero-indexed; frame is 1-indexed.
*/
getFrame: function() {
return {row: this.row, col: this.col, frame: this.frame};
},
/**
* Returns the total number of frames in the current animation loop.
*/
getNumFrames: function() {
if (this.squeeze) {
return (this.endRow - this.startRow + 1) * (this.endCol - this.startCol + 1);
}
return (this.endRow - this.startRow) * this.cols - this.startCol + this.endCol + 1;
},
* Converts a frame number to row and column numbers.
*
* @param {Number} frame
* The frame number to convert.
*
* @return {Object}
* An object containing the 'frame' number and the corresponding 'row' and
* 'col' properties.
*/
frameNumberToRowCol: function(frame) {
var row, col;
frame = ((frame + this.numFrames) % this.numFrames) || this.numFrames; // over-/under-flow
if (this.squeeze) {
row = this.startRow + Math.floor((frame - 1) / this.cols);
col = (frame - 1) % this.cols + this.startCol;
}
else {
row = this.startRow + Math.floor((frame + this.startCol - 1) / this.cols);
col = (frame + this.startCol - 1) % this.cols;
}
return {frame: frame, row: row, col: col};
},
/**
* Clone the Sprite (return an identical copy).
*/
clone: function() {
return new Sprite(this.sourceFile, this);
}
};
this.Sprite = Sprite;
}).call(this);
// END SPRITE ANIMATION LIBRARY ===============================================
// BEGIN IMAGE CACHE HELPERS ==================================================
/**
* Override these functions to provide alternative cache implementations.
* @ignore
*/
(function() {
var images = {}; // Image cache
/**
* Get an image from the cache.
*
* @param {String} src
* The file path of the image.
*
* @return {Image}
* The Image object associated with the file or null if the Image object
* has not yet been cached.
*
* @static
*/
Sprite.getImageFromCache = function(src) {
return images[src] ? images[src] : null;
};
/**
* Save an image to the cache.
*
* @param {String} src
* The file path of the image.
* @param {Image} image
* The Image object to cache.
*
* @static
*/
Sprite.saveImageToCache = function(src, image) {
images[src] = image;
};
/**
* Preload a list of images asynchronously.
*
* @param {String[]} files
* An Array of paths to images to preload.
* @param {Object} [options]
* A map of options for this function.
* @param {Function} [options.finishCallback]
* A function to run when all images have finished loading.
* @param {Number} [options.finishCallback.numLoaded]
* The number of images that were preloaded.
* @param {Function} [options.itemCallback]
* A function to run when an image has finished loading.
* @param {String} [options.itemCallback.filepath]
* The file path of the loaded image.
* @param {Number} [options.itemCallback.numLoaded]
* The number of images that have been loaded so far (including the current
* one).
* @param {Number} [options.itemCallback.numImages]
* The total number of images to load.
*
* @static
*/
Sprite.preloadImages = function(files, options) {
var l = files.length, m = -1, src, image;
var notifyLoaded = function(itemCallback, src) {
m++;
if (typeof itemCallback == 'function') {
itemCallback(src, m, l);
}
if (m == l && typeof options.finishCallback == 'function') {
options.finishCallback(l);
}
};
notifyLoaded();
var onload = function() {
Sprite.saveImageToCache(this._src, this);
notifyLoaded(options.itemCallback, this._src);
};
while (files.length) {
src = files.pop();
image = new Image();
image._num = l-files.length;
image._src = src;
image.onload = onload;
image.src = src;
}
};
}).call(this);
// END IMAGE CACHE HELPERS ====================================================