/** 
 | 
 * 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; 
 | 
    } 
 | 
} 
 |