animation/Clip.js

import Easing from './easing';

function noop () {}
/**
 * @constructor
 * @alias clay.animation.Clip
 * @param {Object} [opts]
 * @param {Object} [opts.target]
 * @param {number} [opts.life]
 * @param {number} [opts.delay]
 * @param {number} [opts.gap]
 * @param {number} [opts.playbackRate]
 * @param {boolean|number} [opts.loop] If loop is a number, it indicate the loop count of animation
 * @param {string|Function} [opts.easing]
 * @param {Function} [opts.onframe]
 * @param {Function} [opts.onfinish]
 * @param {Function} [opts.onrestart]
 */
var Clip = function (opts) {

    opts = opts || {};

    /**
     * @type {string}
     */
    this.name = opts.name || '';

    /**
     * @type {Object}
     */
    this.target = opts.target;

    /**
     * @type {number}
     */
    this.life = opts.life || 1000;

    /**
     * @type {number}
     */
    this.delay = opts.delay || 0;

    /**
     * @type {number}
     */
    this.gap = opts.gap || 0;

    /**
     * @type {number}
     */
    this.playbackRate = opts.playbackRate || 1;


    this._initialized = false;

    this._elapsedTime = 0;

    this._loop = opts.loop == null ? false : opts.loop;
    this.setLoop(this._loop);

    if (opts.easing != null) {
        this.setEasing(opts.easing);
    }

    /**
     * @type {Function}
     */
    this.onframe = opts.onframe || noop;

    /**
     * @type {Function}
     */
    this.onfinish = opts.onfinish || noop;

    /**
     * @type {Function}
     */
    this.onrestart = opts.onrestart || noop;

    this._paused = false;
};

Clip.prototype = {

    gap: 0,

    life: 0,

    delay: 0,

    /**
     * @param {number|boolean} loop
     */
    setLoop: function (loop) {
        this._loop = loop;
        if (loop) {
            if (typeof loop === 'number') {
                this._loopRemained = loop;
            }
            else {
                this._loopRemained = Infinity;
            }
        }
    },

    /**
     * @param {string|Function} easing
     */
    setEasing: function (easing) {
        if (typeof(easing) === 'string') {
            easing = Easing[easing];
        }
        this.easing = easing;
    },

    /**
     * @param  {number} time
     * @return {string}
     */
    step: function (time, deltaTime, silent) {
        if (!this._initialized) {
            this._startTime = time + this.delay;
            this._initialized = true;
        }
        if (this._currentTime != null) {
            deltaTime = time - this._currentTime;
        }
        this._currentTime = time;

        if (this._paused) {
            return 'paused';
        }

        if (time < this._startTime) {
            return;
        }

        // PENDIGN Sync ?
        this._elapse(time, deltaTime);

        var percent = Math.min(this._elapsedTime / this.life, 1);

        if (percent < 0) {
            return;
        }

        var schedule;
        if (this.easing) {
            schedule = this.easing(percent);
        }
        else {
            schedule = percent;
        }

        if (!silent) {
            this.fire('frame', schedule);
        }

        if (percent === 1) {
            if (this._loop && this._loopRemained > 0) {
                this._restartInLoop(time);
                this._loopRemained--;
                return 'restart';
            }
            else {
                // Mark this clip to be deleted
                // In the animation.update
                this._needsRemove = true;

                return 'finish';
            }
        }
        else {
            return null;
        }
    },

    /**
     * @param  {number} time
     * @return {string}
     */
    setTime: function (time) {
        return this.step(time + this._startTime);
    },

    restart: function (time) {
        // If user leave the page for a while, when he gets back
        // All clips may be expired and all start from the beginning value(position)
        // It is clearly wrong, so we use remainder to add a offset

        var remainder = 0;
        // Remainder ignored if restart is invoked manually
        if (time) {
            this._elapse(time);
            remainder = this._elapsedTime % this.life;
        }
        time = time || Date.now();

        this._startTime = time - remainder + this.delay;
        this._elapsedTime = 0;

        this._needsRemove = false;
        this._paused = false;
    },

    getElapsedTime: function () {
        return this._elapsedTime;
    },

    _restartInLoop: function (time) {
        this._startTime = time + this.gap;
        this._elapsedTime = 0;
    },

    _elapse: function (time, deltaTime) {
        this._elapsedTime += deltaTime * this.playbackRate;
    },

    fire: function (eventType, arg) {
        var eventName = 'on' + eventType;
        if (this[eventName]) {
            this[eventName](this.target, arg);
        }
    },

    clone: function () {
        var clip = new this.constructor();
        clip.name = this.name;
        clip._loop = this._loop;
        clip._loopRemained = this._loopRemained;

        clip.life = this.life;
        clip.gap = this.gap;
        clip.delay = this.delay;

        return clip;
    },
    /**
     * Pause the clip.
     */
    pause: function () {
        this._paused = true;
    },

    /**
     * Resume the clip.
     */
    resume: function () {
        this._paused = false;
    }
};
Clip.prototype.constructor = Clip;

export default Clip;