学习动画的基本原理

这是一篇学习笔记,参考十年踪迹的博客原文:《关于动画,你需要知道的》。本篇不记录任何动画原理相关的东西,只记录我在学习之后的实现。

《关于动画,你需要知道的》这篇文章帮助我完全理解了动画的本质,在它的基础上,我简单实现了一个动画库,可以:

  1. 自定义动画时长
  2. 自定义动画的ease函数,类似css动画的animation-timing-function
  3. 自定义动画的迭代次数,类似css动画的animation-iteration-count
  4. 自定义动画的direciton,类似css动画的animation-direction
  5. 控制动画暂停和继续,类似css动画的animation-play-state

下面是它的完整实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
let _continueStart = Symbol('continueStart');

export default class Animation {
constructor({
duration,
iterationCount = 1,
ease = Animation.EASE.LINEAR,
direction = Animation.DIRECTION.NORMAL,
onProgress = Animation.noop
} = {}) {
this.id = Animation.nextId();
this.duration = duration;
this.ease = ease;
this.iterationCount = iterationCount;
this.direction = direction;
this.onProgress = onProgress;
this.playState = null;
this.promise = null;
}

start() {
this.promise = new Promise((resolve, reject) => {
this.playState = Animation.playState.RUNNING;
this.startTime = Date.now();//记录动画每次迭代的开始时间
this.progress = 0;//记录动画每次迭代的进程
let _iterationCount = 0;//累计动画迭代次数

// 设置动画方向:只有在normal 和 alternate时,方向才是正向的
let isPositiveDirection = this.direction === Animation.DIRECTION.NORMAL || this.direction === Animation.DIRECTION.ALTERNATE;

let iterationFinished = () => {
if (this.iterationCount === 'infinite') return false;
return _iterationCount >= this.iterationCount;
};

this[_continueStart] = () => {
if( this.playState === Animation.playState.DESTROYED ) {
return reject(new Error('animation destroyed...'));
}

let currentTime = Date.now();
this.progress = Math.min(1.0, (currentTime - this.startTime) / this.duration);
this.onProgress(this.ease(isPositiveDirection ? this.progress : (1 - this.progress)));

if (this.progress >= 1.0) {
// 累加动画迭代次数
_iterationCount += 1;
}

let isLastIteration = iterationFinished();
let goon = this.playState === Animation.playState.RUNNING;

// 本次动画迭代结束
if (this.progress >= 1.0) {
if (!isLastIteration) {
// 重置动画的状态
this.progress = 0;
this.startTime = currentTime;

// 只有alternate和alternate_reverse时,动画才会在结束点切换方向
if (this.direction === Animation.DIRECTION.ALTERNATE || this.direction === Animation.DIRECTION.ALTERNATE_REVERSE) {
isPositiveDirection = !isPositiveDirection;
}
} else {
// 动画完全结束回调
resolve(this);
goon = false;
}
}

goon && requestAnimationFrame(this[_continueStart]);
};

requestAnimationFrame(this[_continueStart]);
});

return this.promise;
}

//销毁
destroy() {
this.playState = Animation.playState.DESTROYED;
}

//恢复
resume() {
if(this.playState === Animation.playState.PAUSED) {
this.startTime = (Date.now() * 100000 - this.duration * this.progress * 100000)/100000;
this.playState = Animation.playState.RUNNING;
this[_continueStart]();
}
}

//暂停
pause() {
if(this.playState === Animation.playState.RUNNING) {
this.playState = Animation.playState.PAUSED;
}
}

static DIRECTION = {
NORMAL: 'normal',
REVERSE: 'reverse',
ALTERNATE: 'alternate',
ALTERNATE_REVERSE: 'alternate_reverse'
}

static playState = {
RUNNING: Symbol('running'),
DESTROYED: Symbol('destroyed'),
PAUSED: Symbol('paused')
}

static EASE = {
LINEAR: function (p) {
return p;
}
}

static noop() {
}

static nextId = (function() {
let id = 0;
return function () {
return ++id;
}
})()
};

我写了一个demo,可以点击这里查看,看看这个动画库的实际效果。

这个库有2个小要点,分别是动画暂停恢复的实现以及动画运行方向的实现。动画暂停恢复,我采用的是一个比较简单的办法,就是在每次动画定时器执行的时候,都把当前的progress给存起来,当动画恢复的时候,根据当前时间和暂停时的progress,反算动画的startTime,从而保证动画定时器恢复的时候,动画进程能够接着上次暂停的进度继续。

动画运行方向的实现,利用的是动画状态关于时间的对称性,假如一个动画的时间为T,那这个动画反向运行一段时间t之后的动画状态应该等于这个动画正向运行T-t时的动画状态,假设动画正向的动画进程为Pz,则Pz = (T - t) / T = 1 - t / T,假设动画反向的动画进程为Pf,则Pf = Pz = 1 - t / T,也就是下面代码的核心原理:

1
2
this.progress = Math.min(1.0, (currentTime - this.startTime) / this.duration);
this.onProgress(this.ease(isPositiveDirection ? this.progress : (1 - this.progress)));

其它实践

利用一个简单的动画队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export default class {
constructor(){
this.tasks = [];
}

push (...tasks) {
this.tasks.push(...tasks);
return this;
}

run() {
if (this.tasks.length === 0) return;

let _run = () => {
if (this.tasks.length === 0) return;


let task = this.tasks.shift();
let ret = task();

if (ret instanceof Promise) {
ret.then(() => {
_run();
});
} else {
_run();
}
};


_run();
}
};

就能做出有序列的动画,点击这里查看,以及十年踪迹博客中提到的带阻尼效果的运动小球效果