Source: animation/Animation.js

import Class from '../core/Class';
import math from '../math/math';
import EventMixin from '../core/EventMixin';
import log from '../utils/log';

/**
 * 动画类
 * @class
 * @mixes EventMixin
 * @fires end 动画完全结束事件
 * @fires loopEnd 动画每次循环结束事件
 */
const Animation = Class.create(/** @lends Animation.prototype */{
    Statics: {
        _anims: [],
        /**
         * tick
         * @memberOf Animation
         * @method tick
         * @param {Number} dt 一帧时间
         */
        tick(dt) {
            this._anims.forEach(anim => anim.tick(dt));
        }
    },
    Mixes: EventMixin,
    /**
     * @default true
     * @type {boolean}
     */
    isAnimation: true,
    /**
     * @default Animation
     * @type {string}
     */
    className: 'Animation',
    /**
     * 动画是否暂停
     * @default false
     * @type {boolean}
     */
    paused: false,
    /**
     * 动画当前播放次数
     * @default 0
     * @type {number}
     */
    currentLoopCount: 0,
    /**
     * 动画需要播放的次数,默认值为 Infinity 表示永远循环
     * @default Infinity
     * @type {number}
     */
    loop: Infinity,
    /**
     * 动画当前时间
     * @default 0
     * @type {number}
     */
    currentTime: 0,
    /**
     * 动画播放速度
     * @default 1
     * @type {number}
     */
    timeScale: 1,
    /**
     * 动画开始时间
     * @default 0
     * @type {number}
     */
    startTime: 0,
    /**
     * 动画结束时间,初始化后会根据 AnimationStates 来自动获取,也可以通过 play 来改变
     * @default 0
     * @type {number}
     */
    endTime: 0,
    /**
     * 动画整体的最小时间,初始化后会根据 AnimationStates 来自动获取
     * @default 0
     * @type {number}
     */
    clipStartTime: 0,
    /**
     * 动画整体的最大时间,初始化后会根据 AnimationStates 来自动获取
     * @default 0
     * @type {number}
     */
    clipEndTime: 0,
    nodeNameMap: null,
    _rootNode: null,
    /**
     * 动画根节点,不指定根节点将无法正常播放动画
     * @default null
     * @type {Node}
     */
    rootNode: {
        get() {
            return this._rootNode;
        },
        set(value) {
            this._rootNode = value;
            this._initNodeNameMap();
        }
    },
    _animStatesList: null,
    /**
     * 动画状态列表
     * @default []
     * @type {AnimationStates[]}
     */
    animStatesList: {
        get() {
            return this._animStatesList;
        },
        set(value) {
            this._animStatesList = value;
            this._initClipTime();
        }
    },
    /**
     * AnimationId集合
     * @type {Object}
     */
    validAnimationIds: null,
    /**
     * @constructs
     * @param {Object} [parmas] 创建对象的属性参数。可包含此类的所有属性。
     */
    constructor(parmas) {
        /**
         * @type {string}
         */
        this.id = math.generateUUID(this.className);
        /**
         * 动画剪辑列表,{ name: { start: 0, end: 1} },play的时候可以通过name来播放某段剪辑
         * @default {}
         * @type {Object}
         */
        this.clips = {};
        this._animStatesList = [];
        Object.assign(this, parmas);
    },
    /**
     * 添加动画剪辑
     * @param {string} name 剪辑名字
     * @param {number} start 动画开始时间
     * @param {number} end 动画结束时间
     * @param {AnimationStates[]} animStatesList 动画帧列表
     */
    addClip(name, start, end, animStatesList) {
        this.clips[name] = {
            start,
            end,
            animStatesList
        };
    },
    /**
     * 移除动画剪辑
     * @param {string} name 需要移除的剪辑名字
     */
    removeClip(name) {
        this.clips[name] = null;
    },
    /**
     * 获取动画列表的时间信息
     * @param  {AnimationStates[]} animStatesList 动画列表
     * @return {Object} result {startTime, endTime} 时间信息
     */
    getAnimStatesListTimeInfo(animStatesList) {
        let endTime = 0;
        let startTime = Infinity;
        animStatesList.forEach((animStates) => {
            endTime = Math.max(animStates.keyTime[animStates.keyTime.length - 1], endTime);
            startTime = Math.min(animStates.keyTime[0], startTime);
        });

        return {
            startTime,
            endTime
        };
    },
    /**
     * 初始化 clip time
     * @private
     */
    _initClipTime() {
        const timeInfo = this.getAnimStatesListTimeInfo(this.animStatesList);
        this.clipStartTime = 0;
        this.clipEndTime = timeInfo.endTime;
    },
    /**
     * 初始化 node name map
     */
    _initNodeNameMap() {
        if (this._rootNode) {
            const map = this.nodeNameMap = {};
            this._rootNode.traverse((child) => {
                map[child.animationId] = child;

                // fix smd animation bug
                const originName = child.name;
                if (originName !== undefined && !map[originName]) {
                    map[originName] = child;
                }
            }, false);
        }
    },
    /**
     * tick
     * @param  {Number} dt
     */
    tick(dt) {
        if (this.paused) {
            return;
        }
        this.currentTime += dt / 1000 * this.timeScale;

        // 当前动画结束
        if (this.currentTime >= this.endTime) {
            this.currentLoopCount++;

            // 渲染最后一帧
            this.currentTime = this.endTime;
            this.updateAnimStates();
            this.fire('loopEnd');

            // 动画完全结束
            if (!this.loop || this.currentLoopCount >= this.loop) {
                this.stop();
                this.fire('end');
            } else {
                this.currentTime = this.startTime;
            }
        } else {
            this.updateAnimStates();
        }
    },
    /**
     * 更新动画状态
     * @return {Animation} this
     */
    updateAnimStates() {
        this.animStatesList.forEach((animStates) => {
            animStates.updateNodeState(this.currentTime, this.nodeNameMap[animStates.nodeName]);
        });

        return this;
    },
    /**
     * 播放动画(剪辑)
     * @param {number|string} [startOrClipName=0] 动画开始时间,或者动画剪辑名字
     * @param {number} [end=this.clipEndTime] 动画结束时间,如果是剪辑的话不需要传
     */
    play(startOrClipName, end) {
        let start;
        if (typeof startOrClipName === 'string') {
            const clip = this.clips[startOrClipName];
            if (clip) {
                start = clip.start;
                end = clip.end;
                if (clip.animStatesList) {
                    this.animStatesList = clip.animStatesList;
                    this._initClipTime();
                }
            } else {
                log.warn('no this animation clip name:' + startOrClipName);
            }
        } else {
            start = startOrClipName;
        }

        if (start === undefined) {
            start = this.clipStartTime;
        }
        if (end === undefined) {
            end = this.clipEndTime;
        }

        this.endTime = Math.min(end, this.clipEndTime);
        this.startTime = Math.min(start, this.endTime);
        this.currentTime = this.startTime;
        this.currentLoopCount = 0;

        // 先移除,然后再插入
        this.stop();
        this.paused = false;
        Animation._anims.push(this);
    },
    /**
     * 停止动画,这个会将动画从Ticker中移除,需要重新调用play才能再次播放
     */
    stop() {
        this.paused = true;
        const anims = Animation._anims;
        const index = anims.indexOf(this);
        if (index !== -1) {
            anims.splice(index, 1);
        }
    },
    /**
     * 暂停动画,这个不会将动画从Ticker中移除
     */
    pause() {
        this.paused = true;
    },
    /**
     * 恢复动画播放,只能针对 pause 暂停后恢复
     */
    resume() {
        this.paused = false;
    },
    /**
     * clone动画
     * @param {Node} rootNode 目标动画根节点
     * @return {Animation} clone的动画对象
     */
    clone(rootNode) {
        const anim = new this.constructor({
            rootNode,
            animStatesList: this.animStatesList,
            timeScale: this.timeScale,
            loop: this.loop,
            paused: this.paused,
            currentTime: this.currentTime,
            startTime: this.startTime,
            endTime: this.endTime,
            clips: this.clips
        });
        if (!this.paused) {
            anim.play();
        }
        return anim;
    }
});

export default Animation;