Repository

js/Mobilizing/misc/Animation.js

import * as _Math from '../core/util/Math';
import Time from '../time/Time';
import Component from '../core/Component';

/**
* Fired when the animation starts
* @event start
* @param {Object} target The target object
*/
const EVT_START = 'start';

/**
* Fired each time the animation is updated
* @event update
* @param {Object} target The target object
*/
const EVT_UPDATE = 'update';

/**
* Fired when each time the animation is repeated once it reached the end if repeat is greater than 1
* @event restart
* @param {Object} target The target object
* @param {Number} direction The current direction
*/
const EVT_RESTART = 'restart';

/**
* Fired when the animation is stopped
* @event stop
* @param {Object} target The target object
*/
const EVT_STOP = 'stop';

/**
* Fired when the animation is resumed
* @event resume
* @param {Object} target The target object
*/
const EVT_RESUME = 'resume';

/**
* Fired when the animation reaches the end and no repetition is pending
* @event finish
* @param {Object} target The target object
*/
const EVT_FINISH = 'finish';

/**
* The Animation class provides a simple way to tween object properties
* @extends Component
* @example
*    //TODO
*/
export default class Animation extends Component {
    /**
    * @param {Object} params The parameters object
    * @param {Object} params.target The object whose propoerties are to be animated
    * @param {Object} [params.from] An object indicating the start values of the properties to animate, defaults to the values of the target
    * @param {Object} params.to An object indicating the finish values of the properties to animate
    * @param {Number} params.duration The animation duration in milliseconds
    * @param {Function} [params.easing=Animation.Easing.linear] An easing function to use
    * @param {Number} [params.repeat=0] The number of times the animation should be repeated, set to Infinity to repeat indefinately
    * @param {Boolean} [params.yoyo=false] If set to true and repeat is greater than 1, the animation will play in reverse once it reached the end
    * @param {Number} [params.delay=0] The number of milliseconds to wait for before starting the animation
    * @param {Time} [params.time] The Time instance to use for this Animation Component
    * @param {Function} [params.onStart] A callback to be called when the animation starts
    * @param {Function} [params.onUpdate] A callback to be called each time the animation is updated
    * @param {Function} [params.onRestart] A callback to be called each time the animation is repeated once it reached the end if repeat is greater than 1
    * @param {Function} [params.onStop] A callback to be called when the animation is stopped
    * @param {Function} [params.onResume] A callback to be called when the animation is resumed
    * @param {Function} [params.onFinish] A callback to be called when the animation reaches the end and no repetition is pending
    */
    constructor({
        target = undefined,
        from = null,
        to = null,
        duration = null,
        easing = Animation.Easing.linear,
        repeat = 0,
        yoyo = false,
        delay = 0,
        onStart = null,
        onUpdate = null,
        onRestart = null,
        onStop = null,
        onResume = null,
        onFinish = null,
    } = {}) {
        super(...arguments);

        this.target = target;
        this.from = from;
        this.to = to;
        this.duration = duration;
        this.easing = easing;
        this.repeat = repeat;
        this.yoyo = yoyo;
        this.delay = delay;
        this.onStart = onStart;
        this.onUpdate = onUpdate;
        this.onRestart = onRestart;
        this.onStop = onStop;
        this.onResume = onResume;
        this.onFinish = onFinish;

        this._time = new Time();
        this._timesPlayed = 0;
        this._direction = 1;

        // bind custom callbacks to events
        if (this.onStart) {
            this.events.on(EVT_START, this.onStart);
        }
        if (this.onUpdate) {
            this.events.on(EVT_UPDATE, this.onUpdate);
        }
        if (this.onRestart) {
            this.events.on(EVT_RESTART, this.onRestart);
        }
        if (this.onStop) {
            this.events.on(EVT_STOP, this.onStop);
        }
        if (this.onResume) {
            this.events.on(EVT_RESUME, this.onResume);
        }
        if (this.onFinish) {
            this.events.on(EVT_FINISH, this.onFinish);
        }
    }

    /**
    * Setup
    */
    setup() {
        const context = this.context;

        context.addComponent(this._time);
        this._time.setup();
        this._time.on();

        this._isPlaying = false;
    }

    /**
    * Play the animation
    */
    play() {
        this._isPlaying = true;
        this._time.reset();

        if (!this.from) {
            // fill in the start values from the target
            this.from = {};

            Object.keys(this.to).forEach((prop) => {
                this.from[prop] = this.target[prop];
            });
        }

        this.events.trigger(EVT_START, this.target);
    }

    /**
    * Stop the animation
    */
    stop() {
        this._isPlaying = false;

        this.events.trigger(EVT_STOP, this.target);
    }

    /**
    * Resume the animation
    */
    resume() {
    }

    /**
    * Rewind the animation back to its starting values
    */
    rewind() {
        this.update(0);
    }

    /**
    * Update the properties according to the elapsed time
    */
    update(time) {
        if (!this._isPlaying) {
            return;
        }

        const t = (typeof time !== "undefined" ? time : (this._time.getAbsoluteDelta())) / this.duration;

        if (t >= 1) {
            Object.keys(this.from).forEach((prop) => {
                this.target[prop] = this.to[prop];
            });

            if (this._timesPlayed++ < this.repeat) {
                if (this.yoyo) {
                    this._direction *= -1;
                }
                this._time.reset();

                this.events.trigger(EVT_RESTART, this.target, this._direction);
            }
            else {
                this._isPlaying = false;
                this.events.trigger(EVT_FINISH, this.target);
            }
        }
        else {
            for (const prop in this.from) {
                if (this._direction < 1) {
                    this.target[prop] = _Math.map(this.easing(t), 0, 1, this.to[prop], this.from[prop]);
                }
                else {
                    this.target[prop] = _Math.map(this.easing(t), 0, 1, this.from[prop], this.to[prop]);
                }
            }
            this.events.trigger(EVT_UPDATE, this.target);
        }
    }

    /**
    * Chain another animation once this one is finished
    */
    chain(animation) {
        this.events.on(EVT_FINISH, () => {
            animation.play();
        });
    }

}

// credits: https://gist.github.com/gre/1650294
Animation.Easing = {
    "linear": function (t) {
        return t;
    },
    "easeInQuad": function (t) {
        return Math.pow(t, 2);
    },
    "easeOutQuad": function (t) {
        return 1 - Math.abs(Math.pow(t - 1, 2));
    },
    "easeInOutQuad": function (t) {
        if (t < 0.5) {
            return Animation.Easing.easeInQuad(t * 2) / 2;
        }
        return Animation.Easing.easeOutQuad(t * 2 - 1) / 2 + 0.5;
    },
    "easeInCubic": function (t) {
        return Math.pow(t, 3);
    },
    "easeOutCubic": function (t) {
        return 1 - Math.abs(Math.pow(t - 1, 3));
    },
    "easeInOutCubic": function (t) {
        if (t < 0.5) {
            return Animation.Easing.easeInCubic(t * 2) / 2;
        }
        return Animation.Easing.easeOutCubic(t * 2 - 1) / 2 + 0.5;
    },
    "easeInQuart": function (t) {
        return Math.pow(t, 4);
    },
    "easeOutQuart": function (t) {
        return 1 - Math.abs(Math.pow(t - 1, 4));
    },
    "easeInOutQuart": function (t) {
        if (t < 0.5) {
            return Animation.Easing.easeInQuart(t * 2) / 2;
        }
        return Animation.Easing.easeOutQuart(t * 2 - 1) / 2 + 0.5;
    },
    "easeInQuint": function (t) {
        return Math.pow(t, 5);
    },
    "easeOutQuint": function (t) {
        return 1 - Math.abs(Math.pow(t - 1, 5));
    },
    "easeInOutQuint": function (t) {
        if (t < 0.5) {
            return Animation.Easing.easeInQuint(t * 2) / 2;
        }
        return Animation.Easing.easeOutQuint(t * 2 - 1) / 2 + 0.5;
    },
    "easeInSin": function (t) {
        return 1 + Math.sin(Math.PI / 2 * t - Math.PI / 2);
    },
    "easeOutSin": function (t) {
        return Math.sin(Math.PI / 2 * t);
    },
    "easeInOutSin": function (t) {
        return (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2;
    },
    "easeInElastic": function (t) {
        return (0.04 - 0.04 / t) * Math.sin(25 * t) + 1;
    },
    "easeOutElastic": function (t) {
        return 0.04 * t / (--t) * Math.sin(25 * t);
    },
    "easeInOutElastic": function (t) {
        if ((t -= 0.5) < 0) {
            return (0.01 + 0.01 / t) * Math.sin(50 * t);
        }

        return (0.02 - 0.01 / t) * Math.sin(50 * t) + 1;
    },
    "easeInBounce": function (t) {
        return 1 - Animation.Easing.easeOutBounce(1 - t);
    },
    "easeOutBounce": function (t) {
        if (t < (1 / 2.75)) {
            return 7.5625 * t * t;
        }
        else if (t < (2 / 2.75)) {
            return 7.5625 * (t -= (1.5 / 2.75)) * t + 0.75;
        }
        else if (t < (2.5 / 2.75)) {
            return 7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375;
        }

        return 7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375;
    },
    "easeInOutBounce": function (t) {
        if (t < 0.5) {
            return Animation.Easing.easeInBounce(t * 2) * 0.5;
        }
        return Animation.Easing.easeOutBounce(t * 2 - 1) * 0.5 + 0.5;
    }

};