Source: framework/components/animation/component.js

Object.assign(pc, function () {
    /**
     * @component Animation
     * @constructor
     * @name pc.AnimationComponent
     * @classdesc The Animation Component allows an Entity to playback animations on models
     * @description Create a new AnimationComponent
     * @param {pc.AnimationComponentSystem} system The {@link pc.ComponentSystem} that created this Component
     * @param {pc.Entity} entity The Entity that this Component is attached to
     * @extends pc.Component
     * @property {Number} speed Speed multiplier for animation play back speed. 1.0 is playback at normal speed, 0.0 pauses the animation
     * @property {Boolean} loop If true the animation will restart from the beginning when it reaches the end
     * @property {Boolean} activate If true the first animation asset will begin playing when the scene is loaded
     * @property {pc.Asset[]} assets The array of animation assets - can also be an array of asset ids.
     * @property {Number} currentTime Get or Set the current time position (in seconds) of the animation
     * @property {Number} duration Get the duration in seconds of the current animation.
     */
    var AnimationComponent = function (system, entity) {
        pc.Component.call(this, system, entity);

        this.animationsIndex = { };

        // Handle changes to the 'animations' value
        this.on('set_animations', this.onSetAnimations, this);
        // Handle changes to the 'assets' value
        this.on('set_assets', this.onSetAssets, this);
        // Handle changes to the 'loop' value
        this.on('set_loop', this.onSetLoop, this);
    };
    AnimationComponent.prototype = Object.create(pc.Component.prototype);
    AnimationComponent.prototype.constructor = AnimationComponent;

    Object.assign(AnimationComponent.prototype, {
        /**
         * @function
         * @name pc.AnimationComponent#play
         * @description Start playing an animation
         * @param {String} name The name of the animation asset to begin playing.
         * @param {Number} [blendTime] The time in seconds to blend from the current
         * animation state to the start of the animation being set.
         */
        play: function (name, blendTime) {
            if (!this.data.animations[name]) {
                console.error(pc.string.format("Trying to play animation '{0}' which doesn't exist", name));
                return;
            }

            if (!this.enabled || !this.entity.enabled) {
                return;
            }

            blendTime = blendTime || 0;

            var data = this.data;

            data.prevAnim = data.currAnim;
            data.currAnim = name;

            if (data.model) {
                data.blending = blendTime > 0 && data.prevAnim;
                if (data.blending) {
                    // Blend from the current time of the current animation to the start of
                    // the newly specified animation over the specified blend time period.
                    data.blendTime = blendTime;
                    data.blendTimeRemaining = blendTime;
                    data.fromSkel.animation = data.animations[data.prevAnim];
                    data.fromSkel.addTime(data.skeleton._time);
                    data.toSkel.animation = data.animations[data.currAnim];
                } else {
                    data.skeleton.animation = data.animations[data.currAnim];
                }
            }

            data.playing = true;
        },

        /**
         * @function
         * @name pc.AnimationComponent#getAnimation
         * @description Return an animation
         * @param {String} name The name of the animation asset
         * @returns {pc.Animation} An Animation
         */
        getAnimation: function (name) {
            return this.data.animations[name];
        },

        setModel: function (model) {
            var data = this.data;
            if (model) {
                // Create skeletons
                var graph = model.getGraph();
                data.fromSkel = new pc.Skeleton(graph);
                data.toSkel = new pc.Skeleton(graph);
                data.skeleton = new pc.Skeleton(graph);
                data.skeleton.looping = data.loop;
                data.skeleton.setGraph(graph);
            }
            data.model = model;

            // Reset the current animation on the new model
            if (data.animations && data.currAnim && data.animations[data.currAnim]) {
                this.play(data.currAnim);
            }
        },

        loadAnimationAssets: function (ids) {
            if (!ids || !ids.length)
                return;

            var self = this;
            var assets = this.system.app.assets;
            var i, l = ids.length;

            var onAssetReady = function (asset) {
                self.animations[asset.name] = asset.resource;
                self.animationsIndex[asset.id] = asset.name;
                /* eslint-disable no-self-assign */
                self.animations = self.animations; // assigning ensures set_animations event is fired
                /* eslint-enable no-self-assign */
            };

            var onAssetAdd = function (asset) {
                asset.off('change', self.onAssetChanged, self);
                asset.on('change', self.onAssetChanged, self);

                asset.off('remove', self.onAssetRemoved, self);
                asset.on('remove', self.onAssetRemoved, self);

                if (asset.resource) {
                    onAssetReady(asset);
                } else {
                    asset.once('load', onAssetReady, self);
                    if (self.enabled && self.entity.enabled)
                        assets.load(asset);
                }
            };

            for (i = 0; i < l; i++) {
                var asset = assets.get(ids[i]);
                if (asset) {
                    onAssetAdd(asset);
                } else {
                    assets.on('add:' + ids[i], onAssetAdd);
                }
            }
        },

        onAssetChanged: function (asset, attribute, newValue, oldValue) {
            if (attribute === 'resource') {
                // replace old animation with new one
                if (newValue) {
                    this.animations[asset.name] = newValue;
                    this.animationsIndex[asset.id] = asset.name;

                    if (this.data.currAnim === asset.name) {
                        // restart animation
                        if (this.data.playing && this.data.enabled && this.entity.enabled)
                            this.play(asset.name, 0);
                    }
                } else {
                    delete this.animations[asset.name];
                    delete this.animationsIndex[asset.id];
                }
            }
        },

        onAssetRemoved: function (asset) {
            asset.off('remove', this.onAssetRemoved, this);

            if (this.animations && this.animations[asset.name]) {
                delete this.animations[asset.name];
                delete this.animationsIndex[asset.id];

                if (this.data.currAnim === asset.name)
                    this._stopCurrentAnimation();
            }
        },

        _stopCurrentAnimation: function () {
            this.data.currAnim = null;
            this.data.playing = false;
            if (this.data.skeleton) {
                this.data.skeleton.currentTime = 0;
                this.data.skeleton.animation = null;
            }
        },

        onSetAnimations: function (name, oldValue, newValue) {
            var data = this.data;

            // If we have animations _and_ a model, we can create the skeletons
            var modelComponent = this.entity.model;
            if (modelComponent) {
                var m = modelComponent.model;
                if (m && m !== data.model) {
                    this.entity.animation.setModel(m);
                }
            }

            if (!data.currAnim && data.activate && data.enabled && this.entity.enabled) {
                for (var animName in data.animations) {
                    // Set the first loaded animation as the current
                    this.play(animName, 0);
                    break;
                }
            }
        },

        onSetAssets: function (name, oldValue, newValue) {
            if (oldValue && oldValue.length) {
                for (var i = 0; i < oldValue.length; i++) {
                    // unsubscribe from change event for old assets
                    if (oldValue[i]) {
                        var asset = this.system.app.assets.get(oldValue[i]);
                        if (asset) {
                            asset.off('change', this.onAssetChanged, this);
                            asset.off('remove', this.onAssetRemoved, this);

                            var animName = this.animationsIndex[asset.id];

                            if (this.data.currAnim === animName)
                                this._stopCurrentAnimation();

                            delete this.animations[animName];
                            delete this.animationsIndex[asset.id];
                        }
                    }
                }
            }

            var ids = newValue.map(function (value) {
                return (value instanceof pc.Asset) ? value.id : value;
            });

            this.loadAnimationAssets(ids);
        },

        onSetLoop: function (name, oldValue, newValue) {
            if (this.data.skeleton) {
                this.data.skeleton.looping = this.data.loop;
            }
        },

        onSetCurrentTime: function (name, oldValue, newValue) {
            this.data.skeleton.currentTime = newValue;
            this.data.skeleton.addTime(0); // update
            this.data.skeleton.updateGraph();
        },

        onEnable: function () {
            pc.Component.prototype.onEnable.call(this);

            // load assets if they're not loaded
            var assets = this.data.assets;
            var registry = this.system.app.assets;
            if (assets) {
                for (var i = 0, len = assets.length; i < len; i++) {
                    var asset = assets[i];
                    if (!(asset instanceof pc.Asset))
                        asset = registry.get(asset);

                    if (asset && !asset.resource)
                        registry.load(asset);
                }
            }

            if (this.data.activate && !this.data.currAnim) {
                for (var animName in this.data.animations) {
                    this.play(animName, 0);
                    break;
                }
            }
        },

        onBeforeRemove: function () {
            for (var i = 0; i < this.assets.length; i++) {
                var asset = this.system.app.assets.get(this.assets[i]);
                if (!asset) continue;

                asset.off('change', this.onAssetChanged, this);
                asset.off('remove', this.onAssetRemoved, this);
            }

            delete this.data.animation;
            delete this.data.skeleton;
            delete this.data.fromSkel;
            delete this.data.toSkel;
        }
    });

    Object.defineProperties(AnimationComponent.prototype, {
        currentTime: {
            get: function () {
                return this.data.skeleton._time;
            },
            set: function (currentTime) {
                this.data.skeleton.currentTime = currentTime;
                this.data.skeleton.addTime(0);
                this.data.skeleton.updateGraph();
            }
        },

        duration: {
            get: function () {
                return this.data.animations[this.data.currAnim].duration;
            }
        }
    });

    return {
        AnimationComponent: AnimationComponent
    };
}());