Source: framework/components/sprite/sprite-animation-clip.js

Object.assign(pc, function () {

    /**
     * @constructor
     * @name pc.SpriteAnimationClip
     * @classdesc Handles playing of sprite animations and loading of relevant sprite assets.
     * @param {pc.SpriteComponent} component The sprite component managing this clip.
     * @param {Object} data Data for the new animation clip.
     * @param {Number} [data.fps] Frames per second for the animation clip.
     * @param {Object} [data.loop] Whether to loop the animation clip.
     * @param {String} [data.name] The name of the new animation clip.
     * @param {Number} [data.spriteAsset] The id of the sprite asset that this clip will play.
     * @property {Number} spriteAsset The id of the sprite asset used to play the animation.
     * @property {pc.Sprite} sprite The current sprite used to play the animation.
     * @property {Number} frame The index of the frame of the {@link pc.Sprite} currently being rendered.
     * @property {Number} time The current time of the animation in seconds.
     * @property {Number} duration The total duration of the animation in seconds.
     * @property {Boolean} isPlaying Whether the animation is currently playing.
     * @property {Boolean} isPaused Whether the animation is currently paused.
     */
    var SpriteAnimationClip = function (component, data) {
        this._component = component;

        this._frame = 0;
        this._sprite = null;
        this._spriteAsset = null;
        this.spriteAsset = data.spriteAsset;

        this.name = data.name;
        this.fps = data.fps || 0;
        this.loop = data.loop || false;

        this._playing = false;
        this._paused = false;

        this._time = 0;

        pc.events.attach(this);
    };

    Object.assign(SpriteAnimationClip.prototype, {
        // When sprite asset is added bind it
        _onSpriteAssetAdded: function (asset) {
            this._component.system.app.assets.off('add:' + asset.id, this._onSpriteAssetAdded, this);
            if (this._spriteAsset === asset.id) {
                this._bindSpriteAsset(asset);
            }
        },

        // Hook up event handlers on sprite asset
        _bindSpriteAsset: function (asset) {
            asset.on("load", this._onSpriteAssetLoad, this);
            asset.on("remove", this._onSpriteAssetRemove, this);

            if (asset.resource) {
                this._onSpriteAssetLoad(asset);
            } else {
                this._component.system.app.assets.load(asset);
            }
        },

        _unbindSpriteAsset: function (asset) {
            asset.off("load", this._onSpriteAssetLoad, this);
            asset.off("remove", this._onSpriteAssetRemove, this);

            // unbind atlas
            if (asset.resource && asset.resource.atlas) {
                this._component.system.app.assets.off('load:' + asset.data.textureAtlasAsset, this._onTextureAtlasLoad, this);
            }
        },

        // When sprite asset is loaded make sure the texture atlas asset is loaded too
        // If so then set the sprite, otherwise wait for the atlas to be loaded first
        _onSpriteAssetLoad: function (asset) {
            if (!asset.resource) {
                this.sprite = null;
            } else {
                if (!asset.resource.atlas) {
                    var atlasAssetId = asset.data.textureAtlasAsset;
                    var assets = this._component.system.app.assets;
                    assets.off('load:' + atlasAssetId, this._onTextureAtlasLoad, this);
                    assets.once('load:' + atlasAssetId, this._onTextureAtlasLoad, this);
                } else {
                    this.sprite = asset.resource;
                }
            }
        },

        // When atlas is loaded try to reset the sprite asset
        _onTextureAtlasLoad: function (atlasAsset) {
            var spriteAsset = this._spriteAsset;
            if (spriteAsset instanceof pc.Asset) {
                this._onSpriteAssetLoad(spriteAsset);
            } else {
                this._onSpriteAssetLoad(this._component.system.app.assets.get(spriteAsset));
            }
        },

        _onSpriteAssetRemove: function (asset) {
            this.sprite = null;
        },

        // If the meshes are re-created make sure
        // we update them in the mesh instance
        _onSpriteMeshesChange: function () {
            if (this._component.currentClip === this) {
                this._component._showFrame(this.frame);
            }
        },

        // Update frame if ppu changes for 9-sliced sprites
        _onSpritePpuChanged: function () {
            if (this._component.currentClip === this) {
                if (this.sprite.renderMode !== pc.SPRITE_RENDERMODE_SIMPLE) {
                    this._component._showFrame(this.frame);
                }
            }
        },

        /**
         * @private
         * @function
         * @name pc.SpriteAnimationClip#_update
         * @param {Number} dt The delta time
         * @description Advances the animation looping if necessary
         */
        _update: function (dt) {
            if (this.fps === 0) return;
            if (!this._playing || this._paused || !this._sprite) return;

            var dir = this.fps < 0 ? -1 : 1;
            var time = this._time + dt * this._component.speed * dir;
            var duration = this.duration;
            var end = (time > duration || time < 0);

            this._setTime(time);

            var frame = this.frame;
            if (this._sprite) {
                frame = Math.floor(this._sprite.frameKeys.length * this._time / duration);
            } else {
                frame = 0;
            }

            if (frame !== this._frame) {
                this._setFrame(frame);
            }

            if (end) {
                if (this.loop) {
                    this.fire('loop');
                    this._component.fire('loop', this);
                } else {
                    this._playing = false;
                    this._paused = false;
                    this.fire('end');
                    this._component.fire('end', this);
                }
            }
        },

        _setTime: function (value) {
            this._time = value;
            var duration = this.duration;
            if (this._time < 0) {
                if (this.loop) {
                    this._time = this._time % duration + duration;
                } else {
                    this._time = 0;
                }
            } else if (this._time > duration) {
                if (this.loop) {
                    this._time = this._time % duration;
                } else {
                    this._time = duration;
                }
            }
        },

        _setFrame: function (value) {
            if (this._sprite) {
                // clamp frame
                this._frame = pc.math.clamp(value, 0, this._sprite.frameKeys.length - 1);
            } else {
                this._frame = value;
            }

            if (this._component.currentClip === this) {
                this._component._showFrame(this._frame);
            }
        },

        _destroy: function () {
            // remove sprite
            if (this._sprite) {
                this.sprite = null;
            }

            // remove sprite asset
            if (this._spriteAsset) {
                this.spriteAsset = null;
            }
        },

        /**
         * @function
         * @name pc.SpriteAnimationClip#play
         * @description Plays the animation. If it's already playing then this does nothing.
         */
        play: function () {
            if (this._playing)
                return;

            this._playing = true;
            this._paused = false;
            this.frame = 0;

            this.fire('play');
            this._component.fire('play', this);
        },

        /**
         * @function
         * @name pc.SpriteAnimationClip#pause
         * @description Pauses the animation.
         */
        pause: function () {
            if (!this._playing || this._paused)
                return;

            this._paused = true;

            this.fire('pause');
            this._component.fire('pause', this);
        },

        /**
         * @function
         * @name pc.SpriteAnimationClip#resume
         * @description Resumes the paused animation.
         */
        resume: function () {
            if (!this._paused) return;

            this._paused = false;
            this.fire('resume');
            this._component.fire('resume', this);
        },

        /**
         * @function
         * @name pc.SpriteAnimationClip#stop
         * @description Stops the animation and resets the animation to the first frame.
         */
        stop: function () {
            if (!this._playing) return;

            this._playing = false;
            this._paused = false;
            this._time = 0;
            this.frame = 0;

            this.fire('stop');
            this._component.fire('stop', this);
        }
    });

    Object.defineProperty(SpriteAnimationClip.prototype, "spriteAsset", {
        get: function () {
            return this._spriteAsset;
        },
        set: function (value) {
            var assets = this._component.system.app.assets;
            var id = value;

            if (value instanceof pc.Asset) {
                id = value.id;
            }

            if (this._spriteAsset !== id) {
                if (this._spriteAsset) {
                    // clean old event listeners
                    var prev = assets.get(this._spriteAsset);
                    if (prev) {
                        this._unbindSpriteAsset(prev);
                    }
                }

                this._spriteAsset = id;

                // bind sprite asset
                if (this._spriteAsset) {
                    var asset = assets.get(this._spriteAsset);
                    if (!asset) {
                        this.sprite = null;
                        assets.on('add:' + this._spriteAsset, this._onSpriteAssetAdded, this);
                    } else {
                        this._bindSpriteAsset(asset);
                    }
                } else {
                    this.sprite = null;
                }
            }
        }
    });

    Object.defineProperty(SpriteAnimationClip.prototype, "sprite", {
        get: function () {
            return this._sprite;
        },
        set: function (value) {
            if (this._sprite) {
                this._sprite.off('set:meshes', this._onSpriteMeshesChange, this);
                this._sprite.off('set:pixelsPerUnit', this._onSpritePpuChanged, this);
                this._sprite.off('set:atlas', this._onSpriteMeshesChange, this);
                if (this._sprite.atlas) {
                    this._sprite.atlas.off('set:texture', this._onSpriteMeshesChange, this);
                }
            }

            this._sprite = value;

            if (this._sprite) {
                this._sprite.on('set:meshes', this._onSpriteMeshesChange, this);
                this._sprite.on('set:pixelsPerUnit', this._onSpritePpuChanged, this);
                this._sprite.on('set:atlas', this._onSpriteMeshesChange, this);

                if (this._sprite.atlas) {
                    this._sprite.atlas.on('set:texture', this._onSpriteMeshesChange, this);
                }
            }

            if (this._component.currentClip === this) {
                var mi;

                // if we are clearing the sprite clear old mesh instance parameters
                if (!value || !value.atlas) {
                    mi = this._component._meshInstance;
                    if (mi) {
                        mi.deleteParameter('texture_emissiveMap');
                        mi.deleteParameter('texture_opacityMap');
                    }

                    this._component._hideModel();
                } else {
                    // otherwise show sprite

                    // update texture
                    if (value.atlas.texture) {
                        mi = this._component._meshInstance;
                        if (mi) {
                            mi.setParameter('texture_emissiveMap', value.atlas.texture);
                            mi.setParameter('texture_opacityMap', value.atlas.texture);
                        }

                        if (this._component.enabled && this._component.entity.enabled) {
                            this._component._showModel();
                        }
                    }

                    // if we have a time then force update
                    // frame based on the time (check if fps is not 0 otherwise time will be Infinity)
                    if (this.time && this.fps) {
                        this.time = this.time;
                    } else {
                        // if we don't have a time
                        // then force update frame counter
                        this.frame = this.frame;
                    }
                }
            }
        }
    });

    Object.defineProperty(SpriteAnimationClip.prototype, "frame", {
        get: function () {
            return this._frame;
        },

        set: function (value) {
            this._setFrame(value);

            // update time to start of frame
            var fps = this.fps || Number.MIN_VALUE;
            this._setTime(this._frame / fps);
        }
    });

    Object.defineProperty(SpriteAnimationClip.prototype, "isPlaying", {
        get: function () {
            return this._playing;
        }
    });

    Object.defineProperty(SpriteAnimationClip.prototype, "isPaused", {
        get: function () {
            return this._paused;
        }
    });

    Object.defineProperty(SpriteAnimationClip.prototype, "duration", {
        get: function () {
            if (this._sprite) {
                var fps = this.fps || Number.MIN_VALUE;
                return this._sprite.frameKeys.length / Math.abs(fps);
            }
            return 0;
        }
    });

    Object.defineProperty(SpriteAnimationClip.prototype, "time", {
        get: function () {
            return this._time;
        },
        set: function (value) {
            this._setTime(value);

            if (this._sprite) {
                this.frame = Math.min(this._sprite.frameKeys.length - 1, Math.floor(this._time * Math.abs(this.fps)));
            } else {
                this.frame = 0;
            }
        }
    });

    return {
        SpriteAnimationClip: SpriteAnimationClip
    };
}());


// Events Documentation

/**
 * @event
 * @name pc.SpriteAnimationClip#play
 * @description Fired when the clip starts playing
 */

/**
 * @event
 * @name pc.SpriteAnimationClip#pause
 * @description Fired when the clip is paused.
 */

/**
 * @event
 * @name pc.SpriteAnimationClip#resume
 * @description Fired when the clip is resumed.
 */

/**
 * @event
 * @name pc.SpriteAnimationClip#stop
 * @description Fired when the clip is stopped.
 */

/**
 * @event
 * @name pc.SpriteAnimationClip#end
 * @description Fired when the clip stops playing because it reached its ending.
 */

/**
 * @event
 * @name pc.SpriteAnimationClip#loop
 * @description Fired when the clip reached the end of its current loop.
 */