Source: core/Tween.js

import Class from './Class';

function now() {
    return +new Date();
}

/**
 * Tween类提供缓动功能。
 * @class  Tween
 * @param {Object} target 缓动对象。
 * @param {Object} fromProps 对象缓动的起始属性集合。
 * @param {Object} toProps 对象缓动的目标属性集合。
 * @param {TweenParams} params 缓动参数。可包含Tween类所有可写属性。
 * @property {Object} target 缓动目标。只读属性。
 * @property {number} duration 缓动总时长。单位毫秒。
 * @property {number} delay 缓动延迟时间。单位毫秒。
 * @property {boolean} paused 缓动是否暂停。默认为false。
 * @property {boolean} loop 缓动是否循环。默认为false。
 * @property {boolean} reverse 缓动是否反转播放。默认为false。
 * @property {number} repeat 缓动重复的次数。默认为0。
 * @property {number} repeatDelay 缓动重复的延迟时长。单位为毫秒。
 * @property {Function} ease 缓动变化函数。默认为null。
 * @property {number} time 缓动已进行的时长。单位毫秒。只读属性。
 * @property {Function} onStart 缓动开始回调函数。它接受1个参数:tween。默认值为null。
 * @property {Function} onUpdate 缓动更新回调函数。它接受2个参数:ratio和tween。默认值为null。
 * @property {Function} onComplete 缓动结束回调函数。它接受1个参数:tween。默认值为null。
 * @see {@link https://hiloteam.github.io/Hilo/docs/api-zh/symbols/Tween.html}
 * @example
 * Hilo.Tween.to(node, {
 *     x:100,
 *     y:20
 * }, {
 *     duration:1000,
 *     delay:500,
 *     ease:Hilo3d.Tween.Ease.Quad.EaseIn,
 *     onComplete:function(){
 *         console.log('complete');
 *     }
 * });
 */
const Tween = Class.create(/** @lends Tween.prototype */ {
    constructor(target, fromProps, toProps, params) {
        const me = this;

        me.target = target;
        me._startTime = 0;
        me._seekTime = 0;
        me._pausedTime = 0;
        me._pausedStartTime = 0;
        me._reverseFlag = 1;
        me._repeatCount = 0;

        // no fromProps if pass 3 arguments
        if (arguments.length === 3) {
            params = toProps;
            toProps = fromProps;
            fromProps = null;
        }

        for (const p in params) me[p] = params[p];
        me._fromProps = fromProps;
        me._toProps = toProps;

        // for old version compatiblity
        if (!params.duration && params.time) {
            me.duration = params.time || 0;
            me.time = 0;
        }
    },

    target: null,
    duration: 1000,
    delay: 0,
    paused: false,
    loop: false,
    reverse: false,
    repeat: 0,
    repeatDelay: 0,
    ease: null,
    time: 0, // ready only

    isStart: false,
    isComplete: false,
    onStart: null,
    onUpdate: null,
    onComplete: null,

    setProps(fromProps, toProps) {
        const me = this;
        const target = me.target;
        const propNames = fromProps || toProps;
        const from = me._fromProps = {};
        const to = me._toProps = {};

        fromProps = fromProps || target;
        toProps = toProps || target;

        for (const p in propNames) {
            to[p] = toProps[p] || 0;
            target[p] = from[p] = fromProps[p] || 0;
        }
        return me;
    },

    /**
     * 启动缓动动画的播放。
     * @memberOf Tween.prototype
     * @method start
     * @returns {Tween} Tween变换本身。可用于链式调用。
     */
    start() {
        const me = this;
        me._startTime = now() + me.delay;
        me._seekTime = 0;
        me._pausedTime = 0;
        me._reverseFlag = 1;
        me._repeatCount = 0;
        me.paused = false;
        me.isStart = false;
        me.isComplete = false;
        Tween.add(me);
        return me;
    },

    /**
     * 停止缓动动画的播放。
     * @memberOf Tween.prototype
     * @method stop
     * @returns {Tween} Tween变换本身。可用于链式调用。
     */
    stop() {
        Tween.remove(this);
        return this;
    },

    /**
     * 暂停缓动动画的播放。
     * @memberOf Tween.prototype
     * @method pause
     * @returns {Tween} Tween变换本身。可用于链式调用。
     */
    pause() {
        const me = this;
        me.paused = true;
        me._pausedStartTime = now();
        return me;
    },

    /**
     * 恢复缓动动画的播放。
     * @memberOf Tween.prototype
     * @method resume
     * @returns {Tween} Tween变换本身。可用于链式调用。
     */
    resume() {
        const me = this;
        me.paused = false;
        if (me._pausedStartTime) me._pausedTime += now() - me._pausedStartTime;
        me._pausedStartTime = 0;
        return me;
    },

    /**
     * 跳转Tween到指定的时间。
     * @memberOf Tween.prototype
     * @method seek
     * @param {number} time 指定要跳转的时间。取值范围为:0 - duraion。
     * @param {boolean} pause 是否暂停。
     * @returns {Tween} Tween变换本身。可用于链式调用。
     */
    seek(time, pause) {
        const me = this;
        const current = now();
        me._startTime = current;
        me._seekTime = time;
        me._pausedTime = 0;
        if (pause !== undefined) me.paused = pause;
        me._update(current, true);
        Tween.add(me);
        return me;
    },

    /**
     * 连接下一个Tween变换。其开始时间根据delay值不同而不同。当delay值为字符串且以'+'或'-'开始时,Tween的开始时间从当前变换结束点计算,否则以当前变换起始点计算。
     * @memberOf Tween.prototype
     * @method link
     * @param {Tween} tween 要连接的Tween变换。
     * @returns {Tween} 下一个Tween。可用于链式调用。
     */
    link(tween) {
        const me = this;
        let delay = tween.delay;
        const startTime = me._startTime;

        let plus;
        let minus;
        if (typeof delay === 'string') {
            plus = delay.indexOf('+') === 0;
            minus = delay.indexOf('-') === 0;
            delay = plus || minus ? Number(delay.substr(1)) * (plus ? 1 : -1) : Number(delay);
        }
        tween.delay = delay;
        tween._startTime = plus || minus ? startTime + me.duration + delay : startTime + delay;

        me._next = tween;
        Tween.remove(tween);
        return tween;
    },

    /**
     * Tween类的内部渲染方法。
     * @private
     */
    _render(ratio) {
        const me = this;
        const target = me.target;
        const fromProps = me._fromProps;
        for (let p in fromProps) {
            target[p] = fromProps[p] + (me._toProps[p] - fromProps[p]) * ratio;
        }
    },

    /**
     * Tween类的内部更新方法。
     * @private
     */
    _update(time, forceUpdate) {
        const me = this;
        if (me.paused && !forceUpdate) return false;
        if (me.isComplete) return true;

        // elapsed time
        const elapsed = time - me._startTime - me._pausedTime + me._seekTime;
        if (elapsed < 0) return false;

        // elapsed ratio
        let ratio = elapsed / me.duration;
        let callback;
        if (ratio <= 0) {
            ratio = 0;
        } else if (ratio >= 1) {
            ratio = 1;
        }
        let easeRatio = me.ease ? me.ease(ratio) : ratio;

        if (me.reverse && me.isStart) {
            // backward
            if (me._reverseFlag < 0) {
                ratio = 1 - ratio;
                easeRatio = 1 - easeRatio;
            }
            // forward
            if (ratio < 1e-7) {
                // repeat complete or not loop
                if ((me.repeat > 0 && me._repeatCount++ >= me.repeat) || (me.repeat === 0 && !me.loop)) {
                    me.isComplete = true;
                } else {
                    me._startTime = now();
                    me._pausedTime = 0;
                    me._reverseFlag *= -1;
                }
            }
        }

        // start callback
        if (!me.isStart) {
            me.setProps(me._fromProps, me._toProps);
            me.isStart = true;
            if (me.onStart) {
                me.onStart.call(me, me);
            }
        }
        me.time = elapsed;

        // render & update callback
        me._render(easeRatio);
        callback = me.onUpdate;
        if (callback) {
            callback.call(me, easeRatio, me);
        }

        // check if complete
        if (ratio >= 1) {
            if (me.reverse) {
                me._startTime = now();
                me._pausedTime = 0;
                me._reverseFlag *= -1;
            } else if (me.loop || me.repeat > 0 && me._repeatCount++ < me.repeat) {
                me._startTime = now() + me.repeatDelay;
                me._pausedTime = 0;
            } else {
                me.isComplete = true;
            }
        }

        // next tween
        const next = me._next;
        if (next && next.time <= 0) {
            const nextStartTime = next._startTime;
            if (nextStartTime > 0 && nextStartTime <= time) {
                // parallel tween
                next._render(ratio);
                next.time = elapsed;
                Tween.add(next);
            } else if (me.isComplete && (nextStartTime < 0 || nextStartTime > time)) {
                // next tween
                next.start();
            }
        }

        // complete
        if (me.isComplete) {
            callback = me.onComplete;
            if (callback) {
                callback.call(me, me);
            }
            return true;
        }

        return false;
    },

    Statics: /** @lends Tween */ {
        _tweens: [],

        /**
         * 更新所有Tween实例。
         * @memberOf Tween
         * @method tick
         * @returns {Tween} Tween
         */
        tick() {
            const tweens = Tween._tweens;
            let tween;
            let i;
            const len = tweens.length;

            for (i = 0; i < len; i++) {
                tween = tweens[i];
                if (tween && tween._update(now())) {
                    tweens.splice(i, 1);
                    i--;
                }
            }
            return Tween;
        },

        /**
         * 添加Tween实例。
         * @memberOf Tween
         * @param {Tween} tween 要添加的Tween对象。
         * @returns {Tween} Tween。
         */
        add(tween) {
            const tweens = Tween._tweens;
            if (tweens.indexOf(tween) === -1) tweens.push(tween);
            return Tween;
        },


        /**
         * 删除Tween实例。
         * @param {Tween|any|any[]} tweenOrTarget 要删除的Tween对象或target对象或要删除的一组对象。
         * @returns {Tween} Tween。
         */
        remove(tweenOrTarget) {
            let i; let
                l;
            if (tweenOrTarget instanceof Array) {
                for (i = 0, l = tweenOrTarget.length; i < l; i++) {
                    Tween.remove(tweenOrTarget[i]);
                }
                return Tween;
            }

            const tweens = Tween._tweens;
            if (tweenOrTarget instanceof Tween) {
                i = tweens.indexOf(tweenOrTarget);
                if (i > -1) tweens.splice(i, 1);
            } else {
                for (i = 0; i < tweens.length; i++) {
                    if (tweens[i].target === tweenOrTarget) {
                        tweens.splice(i, 1);
                        i--;
                    }
                }
            }

            return Tween;
        },


        /**
         * 删除所有Tween实例。
         * @returns {Tween} Tween。
         */
        removeAll() {
            Tween._tweens.length = 0;
            return Tween;
        },

        /**
         * 创建一个缓动动画,让目标对象从开始属性变换到目标属性。
         * @memberOf Tween
         * @method fromTo
         * @param {Object|Array} target 缓动目标对象或缓动目标数组。
         * @param {Object} fromProps 缓动目标对象的开始属性。
         * @param {Object} toProps 缓动目标对象的目标属性。
         * @param {TweenParams} params 缓动动画的参数。
         * @returns {Tween|Array} 一个Tween实例对象或Tween实例数组。
         */
        fromTo(target, fromProps, toProps, params) {
            params = params || {};
            const isArray = target instanceof Array;
            target = isArray ? target : [target];

            let tween;
            let i;
            const stagger = params.stagger;
            const tweens = [];
            for (i = 0; i < target.length; i++) {
                tween = new Tween(target[i], fromProps, toProps, params);
                if (stagger) {
                    tween.delay = (params.delay || 0) + (i * stagger || 0);
                }
                tween.start();
                tweens.push(tween);
            }

            return isArray ? tweens : tween;
        },


        /**
         * 创建一个缓动动画,让目标对象从当前属性变换到目标属性。
         * @memberOf Tween
         * @method to
         * @param {Object|Array} target 缓动目标对象或缓动目标数组。
         * @param {Object} toProps 缓动目标对象的目标属性。
         * @param {TweenParams} params 缓动动画的参数。
         * @returns {Tween|Array} 一个Tween实例对象或Tween实例数组。
         */
        to(target, toProps, params) {
            return Tween.fromTo(target, null, toProps, params);
        },

        /**
         * 创建一个缓动动画,让目标对象从指定的起始属性变换到当前属性。
         * @memberOf Tween
         * @method from
         * @param {Object|Array} target 缓动目标对象或缓动目标数组。
         * @param {Object} fromProps 缓动目标对象的初始属性。
         * @param {TweenParams} params 缓动动画的参数。
         * @returns {Tween|Array} 一个Tween实例对象或Tween实例数组。
         */
        from(target, fromProps, params) {
            return Tween.fromTo(target, fromProps, null, params);
        }
    }

});


/* eslint-disable no-return-assign, no-cond-assign */

function createEase(obj, easeInFn, easeOutFn, easeInOutFn, easeNoneFn) {
    obj = obj || {};
    if (easeInFn) {
        obj.EaseIn = easeInFn;
    }

    if (easeOutFn) {
        obj.EaseOut = easeOutFn;
    }

    if (easeInOutFn) {
        obj.EaseInOut = easeInOutFn;
    }

    if (easeNoneFn) {
        obj.EaseNone = easeNoneFn;
    }

    return obj;
}

const Linear = createEase(null, null, null, null, (k) => {
    return k;
});

const Quad = createEase(null,
    (k) => {
        return k * k;
    },

    (k) => {
        return -k * (k - 2);
    },

    (k) => {
        return ((k *= 2) < 1) ? 0.5 * k * k : -0.5 * (--k * (k - 2) - 1);
    });

const Cubic = createEase(null,
    (k) => {
        return k * k * k;
    },

    (k) => {
        return --k * k * k + 1;
    },

    (k) => {
        return ((k *= 2) < 1) ? 0.5 * k * k * k : 0.5 * ((k -= 2) * k * k + 2);
    });

const Quart = createEase(null,
    (k) => {
        return k * k * k * k;
    },

    (k) => {
        return -(--k * k * k * k - 1);
    },

    (k) => {
        return ((k *= 2) < 1) ? 0.5 * k * k * k * k : -0.5 * ((k -= 2) * k * k * k - 2);
    });

const Quint = createEase(null,
    (k) => {
        return k * k * k * k * k;
    },

    (k) => {
        return (k -= 1) * k * k * k * k + 1;
    },

    (k) => {
        return ((k *= 2) < 1) ? 0.5 * k * k * k * k * k : 0.5 * ((k -= 2) * k * k * k * k + 2);
    });

let math = Math;
let PI = math.PI; let HALF_PI = PI * 0.5;
let sin = math.sin; let cos = math.cos;
let pow = math.pow; let
    sqrt = math.sqrt;

const Sine = createEase(null,
    (k) => {
        return -cos(k * HALF_PI) + 1;
    },

    (k) => {
        return sin(k * HALF_PI);
    },

    (k) => {
        return -0.5 * (cos(PI * k) - 1);
    });

const Expo = createEase(null,
    (k) => {
        return k === 0 ? 0 : pow(2, 10 * (k - 1));
    },

    (k) => {
        return k === 1 ? 1 : -pow(2, -10 * k) + 1;
    },

    (k) => {
        if (k === 0 || k === 1) return k;
        if ((k *= 2) < 1) return 0.5 * pow(2, 10 * (k - 1));
        return 0.5 * (-pow(2, -10 * (k - 1)) + 2);
    });

const Circ = createEase(null,
    (k) => {
        return -(sqrt(1 - k * k) - 1);
    },

    (k) => {
        return sqrt(1 - (--k * k));
    },

    (k) => {
        if ((k /= 0.5) < 1) return -0.5 * (sqrt(1 - k * k) - 1);
        return 0.5 * (sqrt(1 - (k -= 2) * k) + 1);
    });

const Elastic = createEase(
    {
        a: 1,
        p: 0.4,
        s: 0.1,

        config(amplitude, period) {
            Elastic.a = amplitude;
            Elastic.p = period;
            Elastic.s = period / (2 * PI) * Math.asin(1 / amplitude) || 0;
        }
    },

    (k) => {
        return -(Elastic.a * pow(2, 10 * (k -= 1)) * sin((k - Elastic.s) * (2 * PI) / Elastic.p));
    },

    (k) => {
        return (Elastic.a * pow(2, -10 * k) * sin((k - Elastic.s) * (2 * PI) / Elastic.p) + 1);
    },

    (k) => {
        return ((k *= 2) < 1) ? -0.5 * (Elastic.a * pow(2, 10 * (k -= 1)) * sin((k - Elastic.s) * (2 * PI) / Elastic.p))
            : Elastic.a * pow(2, -10 * (k -= 1)) * sin((k - Elastic.s) * (2 * PI) / Elastic.p) * 0.5 + 1;
    }
);

const Back = createEase(
    {
        o: 1.70158,
        s: 2.59491,

        config(overshoot) {
            Back.o = overshoot;
            Back.s = overshoot * 1.525;
        }
    },

    (k) => {
        return k * k * ((Back.o + 1) * k - Back.o);
    },

    (k) => {
        return (k -= 1) * k * ((Back.o + 1) * k + Back.o) + 1;
    },

    (k) => {
        return ((k *= 2) < 1) ? 0.5 * (k * k * ((Back.s + 1) * k - Back.s)) : 0.5 * ((k -= 2) * k * ((Back.s + 1) * k + Back.s) + 2);
    }
);

const Bounce = createEase(null,
    (k) => {
        return 1 - Bounce.EaseOut(1 - k);
    },

    (k) => {
        if ((k /= 1) < 0.36364) {
            return 7.5625 * k * k;
        } if (k < 0.72727) {
            return 7.5625 * (k -= 0.54545) * k + 0.75;
        } if (k < 0.90909) {
            return 7.5625 * (k -= 0.81818) * k + 0.9375;
        }
        return 7.5625 * (k -= 0.95455) * k + 0.984375;
    },

    (k) => {
        return k < 0.5 ? Bounce.EaseIn(k * 2) * 0.5 : Bounce.EaseOut(k * 2 - 1) * 0.5 + 0.5;
    });
/* eslint-enable no-return-assign, no-cond-assign */

/**
 * Ease类包含为Tween类提供各种缓动功能的函数。
 * @memberOf Tween
 * @property {TweenEaseNoneObject} Linear 线性匀速缓动函数
 * @property {TweenEaseObject} Quad 二次缓动函数
 * @property {TweenEaseObject} Cubic 三次缓动函数。
 * @property {TweenEaseObject} Quart 四次缓动函数。
 * @property {TweenEaseObject} Quint 五次缓动函数。
 * @property {TweenEaseObject} Sine 正弦缓动函数。
 * @property {TweenEaseObject} Expo 指数缓动函数。
 * @property {TweenEaseObject} Circ 圆形缓动函数。
 * @property {TweenEaseObject} Elastic 弹性缓动函数。
 * @property {TweenEaseObject} Back 向后缓动函数。
 * @property {TweenEaseObject} Bounce 弹跳缓动函数。
 * @see  {@link https://hiloteam.github.io/Hilo/docs/api-zh/symbols/Ease.html}
 */
Tween.Ease = {
    Linear,
    Quad,
    Cubic,
    Quart,
    Quint,
    Sine,
    Expo,
    Circ,
    Elastic,
    Back,
    Bounce
};


export default Tween;


/**
 * @interface TweenParams
 * @property {number} duration
 * @property {number|String} [delay]
 * @property {Function} [ease]
 * @property {Function} [onStart]
 * @property {Function} [onComplete]
 * @property {Function} [onUpdate]
 * @property {boolean} [loop=false]
 * @property {boolean} [reverse=false]
 * @property {number} [repeat=0]
 */

/**
 * @interface TweenEaseObject
 * @property {Function} EaseIn
 * @property {Function} EaseOut
 * @property {Function} EaseInOut
 */

/**
 * @interface TweenEaseNoneObject
 * @property {Function} EaseNone
 */