import { shared } from '../../core/ticker';
import { UPDATE_PRIORITY } from '../../core/const';
import Sprite from '../../core/sprites/Sprite';
import Texture from '../../core/textures/Texture';
/**
* @typedef FrameObject
* @type {object}
* @property {Tiny.Texture} texture - The {@link Tiny.Texture} of the frame
* @property {number} time - the duration of the frame in ms
*/
/**
* An AnimatedSprite is a simple way to display an animation depicted by a list of textures.
*
* ```js
* let alienImages = ['image_sequence_01.png','image_sequence_02.png','image_sequence_03.png','image_sequence_04.png'];
* let textureArray = [];
*
* for (let i=0; i < 4; i++) {
* let texture = Tiny.Texture.fromImage(alienImages[i]);
* textureArray.push(texture);
* // you can also set the duration for any frame, as follows:
* // textureArray.push({
* // texture,
* // time: 4 * 10
* // });
* };
*
* let mc = new Tiny.AnimatedSprite(textureArray);
* ```
*
* @class
* @extends Tiny.Sprite
* @memberof Tiny
*/
export default class AnimatedSprite extends Sprite {
/**
* @param {Tiny.Texture[]|FrameObject[]} textures - an array of {@link Tiny.Texture} or frame objects that make up the animation
* @param {boolean} [autoUpdate=true] - Whether use Tiny.ticker.shared to auto update animation time.
*/
constructor(textures, autoUpdate) {
super(textures[0] instanceof Texture ? textures[0] : textures[0].texture);
/**
* @private
*/
this._textures = null;
/**
* @private
*/
this._durations = null;
this.textures = textures;
/**
* `true` uses Tiny.ticker.shared to auto update animation time.
*
* @type {boolean}
* @default true
* @private
*/
this._autoUpdate = autoUpdate !== false;
/**
* The speed that the AnimatedSprite will play at. Higher is faster, lower is slower
*
* @member {number}
* @default 1
*/
this.animationSpeed = 1;
/**
* Whether or not the animate sprite repeats after playing.
*
* @member {boolean}
* @default true
*/
this.loop = true;
/**
* Update anchor to [Texture's defaultAnchor]{@link Tiny.Texture#defaultAnchor} when frame changes.
*
* Useful with [sprite sheet animations]{@link Tiny.Spritesheet#animations} created with tools.
* Changing anchor for each frame allows to pin sprite origin to certain moving feature
* of the frame (e.g. left foot).
*
* Note: Enabling this will override any previously set `anchor` on each frame change.
*
* @version 1.4.0
* @member {boolean}
* @default false
*/
this.updateAnchor = false;
/**
* Function to call when a AnimatedSprite finishes playing
*
* @member {function}
*/
this.onComplete = null;
/**
* Function to call when a AnimatedSprite changes which texture is being rendered
*
* @member {function}
*/
this.onFrameChange = null;
/**
* Function to call when 'loop' is true, and an AnimatedSprite is played and loops around to start again
*
* @version 1.2.0
* @member {function}
*/
this.onLoop = null;
/**
* Elapsed time since animation has been started, used internally to display current texture
*
* @member {number}
* @private
*/
this._currentTime = 0;
/**
* Indicates if the AnimatedSprite is currently playing
*
* @member {boolean}
* @readonly
*/
this.playing = false;
}
/**
* Stops the AnimatedSprite
*/
stop() {
if (!this.playing) {
return;
}
this.playing = false;
if (this._autoUpdate) {
shared.remove(this._update, this);
}
}
/**
* Plays the AnimatedSprite
*/
play() {
if (this.playing) {
return;
}
this.playing = true;
if (this._autoUpdate) {
shared.add(this._update, this, UPDATE_PRIORITY.HIGH);
}
}
/**
* Stops the AnimatedSprite and goes to a specific frame
*
* @param {number} frameNumber - frame index to stop at
*/
gotoAndStop(frameNumber) {
this.stop();
const previousFrame = this.currentFrame;
this._currentTime = frameNumber;
if (previousFrame !== this.currentFrame) {
this.updateTexture();
}
}
/**
* Goes to a specific frame and begins playing the AnimatedSprite
*
* @param {number} frameNumber - frame index to start at
*/
gotoAndPlay(frameNumber) {
const previousFrame = this.currentFrame;
this._currentTime = frameNumber;
if (previousFrame !== this.currentFrame) {
this.updateTexture();
}
this.play();
}
/**
* Updates the object transform for rendering.
*
* @private
* @param {number} deltaTime - Time since last tick.
*/
_update(deltaTime) {
const elapsed = this.animationSpeed * deltaTime;
const previousFrame = this.currentFrame;
if (this._durations !== null) {
let lag = this._currentTime % 1 * this._durations[this.currentFrame];
lag += elapsed / 60 * 1000;
while (lag < 0) {
this._currentTime--;
lag += this._durations[this.currentFrame];
}
const sign = Math.sign(this.animationSpeed * deltaTime);
this._currentTime = Math.floor(this._currentTime);
while (lag >= this._durations[this.currentFrame]) {
lag -= this._durations[this.currentFrame] * sign;
this._currentTime += sign;
}
this._currentTime += lag / this._durations[this.currentFrame];
} else {
this._currentTime += elapsed;
}
if (this._currentTime < 0 && !this.loop) {
this.gotoAndStop(0);
if (this.onComplete) {
this.onComplete();
}
} else if (this._currentTime >= this._textures.length && !this.loop) {
this.gotoAndStop(this._textures.length - 1);
if (this.onComplete) {
this.onComplete();
}
} else if (previousFrame !== this.currentFrame) {
if (this.loop && this.onLoop) {
if (this.animationSpeed > 0 && this.currentFrame < previousFrame) {
this.onLoop();
} else if (this.animationSpeed < 0 && this.currentFrame > previousFrame) {
this.onLoop();
}
}
this.updateTexture();
}
}
/**
* Updates the displayed texture to match the current frame index
*
* @private
*/
updateTexture() {
this._texture = this._textures[this.currentFrame];
this._textureID = -1;
this.cachedTint = 0xFFFFFF;
if (this.updateAnchor) {
this._anchor.copy(this._texture.defaultAnchor);
}
if (this.onFrameChange) {
this.onFrameChange(this.currentFrame);
}
}
/**
* Reverse the Animation
*
* @version 1.0.2
*/
reverse() {
this._currentTime = this._textures.length - this.currentFrame - 1;
this._textures.reverse();
}
/**
* Stops the AnimatedSprite and destroys it
*
* @param {object|boolean} [options] - Options parameter. A boolean will act as if all options have been set to that value
* @param {boolean} [options.children=false] - if set to true, all the children will have their destroy method called as well. 'options' will be passed on to those calls.
* @param {boolean} [options.texture=false] - Should it destroy the current texture of the sprite as well
* @param {boolean} [options.baseTexture=false] - Should it destroy the base texture of the sprite as well
*/
destroy(options) {
this.stop();
super.destroy(options);
}
/**
* A short hand way of creating a movieclip from an array of frame ids
*
* @static
* @param {string[]} frames - The array of frames ids the movieclip will use as its texture frames
* @return {AnimatedSprite} The new animated sprite with the specified frames.
*/
static fromFrames(frames) {
const textures = [];
for (let i = 0; i < frames.length; ++i) {
textures.push(Texture.fromFrame(frames[i]));
}
return new AnimatedSprite(textures);
}
/**
* A short hand way of creating a movieclip from an array of image ids
*
* @static
* @param {string[]} images - the array of image urls the movieclip will use as its texture frames
* @return {AnimatedSprite} The new animate sprite with the specified images as frames.
*/
static fromImages(images) {
const textures = [];
for (let i = 0; i < images.length; ++i) {
textures.push(Texture.fromImage(images[i]));
}
return new AnimatedSprite(textures);
}
/**
* totalFrames is the total number of frames in the AnimatedSprite. This is the same as number of textures assigned to the AnimatedSprite.
*
* @readonly
* @member {number}
* @default 0
*/
get totalFrames() {
return this._textures.length;
}
/**
* The array of textures used for this AnimatedSprite
*
* @member {Tiny.Texture[]}
*/
get textures() {
return this._textures;
}
set textures(value) {
if (value[0] instanceof Texture) {
this._textures = value;
this._durations = null;
} else {
this._textures = [];
this._durations = [];
for (let i = 0; i < value.length; i++) {
this._textures.push(value[i].texture);
this._durations.push(value[i].time);
}
}
this.gotoAndPlay(0);
this.updateTexture();
}
/**
* The AnimatedSprites current frame index
*
* @member {number}
* @readonly
*/
get currentFrame() {
let currentFrame = Math.floor(this._currentTime) % this._textures.length;
if (currentFrame < 0) {
currentFrame += this._textures.length;
}
return currentFrame;
}
}