这是一篇学习笔记,参考十年踪迹的博客原文:《关于动画,你需要知道的》。本篇不记录任何动画原理相关的东西,只记录我在学习之后的实现。
《关于动画,你需要知道的》这篇文章帮助我完全理解了动画的本质,在它的基础上,我简单实现了一个动画库,可以:
- 自定义动画时长
- 自定义动画的ease函数,类似css动画的animation-timing-function
- 自定义动画的迭代次数,类似css动画的animation-iteration-count
- 自定义动画的direciton,类似css动画的animation-direction
- 控制动画暂停和继续,类似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
129let _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
2this.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
33export 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();
}
};