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