Animation.ts 5.51 KB
/**
 * Animation main class, dispatch and manage all animation controllers
 *
 */
// TODO Additive animation
// http://iosoteric.com/additive-animations-animatewithduration-in-ios-8/
// https://developer.apple.com/videos/wwdc2014/#236

import Eventful from '../core/Eventful';
import requestAnimationFrame from './requestAnimationFrame';
import Animator from './Animator';
import Clip from './Clip';

export function getTime() {
    return new Date().getTime();
}

interface Stage {
    update?: () => void
}

interface AnimationOption {
    stage?: Stage
}
/**
 * @example
 *     const animation = new Animation();
 *     const obj = {
 *         x: 100,
 *         y: 100
 *     };
 *     animation.animate(node.position)
 *         .when(1000, {
 *             x: 500,
 *             y: 500
 *         })
 *         .when(2000, {
 *             x: 100,
 *             y: 100
 *         })
 *         .start();
 */

export default class Animation extends Eventful {

    stage: Stage

    // Use linked list to store clip
    private _head: Clip
    private _tail: Clip

    private _running = false

    private _time = 0
    private _pausedTime = 0
    private _pauseStart = 0

    private _paused = false;

    constructor(opts?: AnimationOption) {
        super();

        opts = opts || {};

        this.stage = opts.stage || {};
    }

    /**
     * Add clip
     */
    addClip(clip: Clip) {
        if (clip.animation) {
            // Clip has been added
            this.removeClip(clip);
        }

        if (!this._head) {
            this._head = this._tail = clip;
        }
        else {
            this._tail.next = clip;
            clip.prev = this._tail;
            clip.next = null;
            this._tail = clip;
        }
        clip.animation = this;
    }
    /**
     * Add animator
     */
    addAnimator(animator: Animator<any>) {
        animator.animation = this;
        const clip = animator.getClip();
        if (clip) {
            this.addClip(clip);
        }
    }
    /**
     * Delete animation clip
     */
    removeClip(clip: Clip) {
        if (!clip.animation) {
            return;
        }
        const prev = clip.prev;
        const next = clip.next;
        if (prev) {
            prev.next = next;
        }
        else {
            // Is head
            this._head = next;
        }
        if (next) {
            next.prev = prev;
        }
        else {
            // Is tail
            this._tail = prev;
        }
        clip.next = clip.prev = clip.animation = null;
    }

    /**
     * Delete animation clip
     */
    removeAnimator(animator: Animator<any>) {
        const clip = animator.getClip();
        if (clip) {
            this.removeClip(clip);
        }
        animator.animation = null;
    }

    update(notTriggerFrameAndStageUpdate?: boolean) {
        const time = getTime() - this._pausedTime;
        const delta = time - this._time;
        let clip = this._head;

        while (clip) {
            // Save the nextClip before step.
            // So the loop will not been affected if the clip is removed in the callback
            const nextClip = clip.next;
            let finished = clip.step(time, delta);
            if (finished) {
                clip.ondestroy();
                this.removeClip(clip);
                clip = nextClip;
            }
            else {
                clip = nextClip;
            }
        }

        this._time = time;

        if (!notTriggerFrameAndStageUpdate) {

            // 'frame' should be triggered before stage, because upper application
            // depends on the sequence (e.g., echarts-stream and finish
            // event judge)
            this.trigger('frame', delta);

            this.stage.update && this.stage.update();
        }
    }

    _startLoop() {
        const self = this;

        this._running = true;

        function step() {
            if (self._running) {
                requestAnimationFrame(step);
                !self._paused && self.update();
            }
        }

        requestAnimationFrame(step);
    }

    /**
     * Start animation.
     */
    start() {
        if (this._running) {
            return;
        }

        this._time = getTime();
        this._pausedTime = 0;

        this._startLoop();
    }

    /**
     * Stop animation.
     */
    stop() {
        this._running = false;
    }

    /**
     * Pause animation.
     */
    pause() {
        if (!this._paused) {
            this._pauseStart = getTime();
            this._paused = true;
        }
    }

    /**
     * Resume animation.
     */
    resume() {
        if (this._paused) {
            this._pausedTime += getTime() - this._pauseStart;
            this._paused = false;
        }
    }

    /**
     * Clear animation.
     */
    clear() {
        let clip = this._head;

        while (clip) {
            let nextClip = clip.next;
            clip.prev = clip.next = clip.animation = null;
            clip = nextClip;
        }

        this._head = this._tail = null;
    }

    /**
     * Whether animation finished.
     */
    isFinished() {
        return this._head == null;
    }

    /**
     * Creat animator for a target, whose props can be animated.
     */
    // TODO Gap
    animate<T>(target: T, options: {
        loop?: boolean  // Whether loop animation
    }) {
        options = options || {};

        // Start animation loop
        this.start();

        const animator = new Animator(
            target,
            options.loop
        );

        this.addAnimator(animator);

        return animator;
    }
}