JS如何做实现动画序列播放
提示
前言:我们知道,一般的浏览器的刷新率为60Hz,也就是说,1秒钟就会刷新60次,相当于大概每过16.6ms浏览器会渲染一帧画面。我们想看到流畅连贯的动画,我们一般希望动画间隔时间为16.6ms。 当我们想要实现下面这个动画,可以有以下几种方法 实现模块滑动 (opens new window)
# CSS动画
# transition
transition的使用相当的简单,但其也有着极大的限制性,即其本身没法在网页加载时自动发生,需要事件触发,其次它是一次性除非再次触发不然不能重复发生,再者它不能定义中间状态,而且一条语句也只能只能定义一个属性的变化,例:
.trans {
transition: left 5s linear 0s;
}
2
3
# animation
animation相比transtion强大了不少,其可以通过js控制动画的开始、暂停,起始、结束位置,也可以定义中间状态
.ani{
animation: left 5s linear;
}
@keyframes left {
from {transform: translateX(0px)}
to {transform: translateX(300px)}
}
2
3
4
5
6
7
8
# js实现
通常我们js实现滑动等动画,都会采用循环或是计时器的方式来进行。例如:
const box = document.querySelector('.box');
let boxLeft = 0;
const animation = () => {
boxLeft+=1;
box.style.left = boxLeft + 'px';
if(boxLeft<=300) {
setTimeout(animation,16.6);
}
}
2
3
4
5
6
7
8
9
# 小结
JS相比CSS而言,CSS动画我们是定义不同时间的状态,然后它进行补间动画,而JS实现为了保证其过程流畅,是帧动画,CSS可以实现简单的动画效果,但对于一些复杂的运动,则需要通过JS进行呈现。对于动画的卡顿,浏览器为了提升动画的性能,为了在动画的每一帧的过程中不必每次都重新绘制整个页面。在特定方式下可以触发生成一个合成层,合成层拥有单独的 GraphicsLayer。需要进行动画的元素包含在这个合成层之下,这样动画的每一帧只需要去重新绘制这个 Graphics Layer 即可,从而达到提升动画性能的目的。
那么一个元素什么时候会触发创建一个 Graphics Layer 层?从目前来说,满足以下任意情况便会创建层:
- 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层)
- 硬件加速的插件,比如 flash 等等
- 使用加速视频解码的
- 3D 或者 硬件加速的 2D Canvas 元素
- 3D 或透视变换(perspective、transform) 的 CSS 属性
- 对自己的 opacity 做 CSS 动画或使用一个动画变换的元素
- 拥有加速 CSS 过滤器的元素
- 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
- 元素有一个 z-index 较低且包含一个复合层的兄弟元素
通常来说,我们希望我们的动画得到 GPU 硬件加速,所以我们会利用类似 transform: translate3d() 这样的方式生成一个 Graphics Layer 层。Graphics Layer 虽好,但不是越多越好,每一帧的渲染内核都会去遍历计算当前所有的 Graphics Layer ,并计算他们下一帧的重绘区域,所以过量的 Graphics Layer 计算也会给渲染造成性能影响。
# 动画序列库实现
在我们日常需求库的过程中,我们难免会遇到一些需求,它要求A元素在n秒后向上移动后再向左滑行等进行多段动画的播放,这类相对复杂的运动通常我们会想到用动画序列的方法来处理。所谓的动画序列,也就是说可以在上一段动画播放结束之后进行下一段动画的播放,这样可以方便用多段动画实现各种不同的复杂效果。而我们不难想到,要实现这个目的,将动画接口实现成 Promise 是一个非常好的方案 例:
var a1 = new Animator(1000, function(p){
var tx = 500 * p;
box.style.transform = 'translateX('
+ tx + 'px)';
});
var a2 = new Animator(1000, function(p){
var ty = 400 * p;
box.style.transform = 'translate(100px,'
+ ty + 'px)';
});
var a3 = new Animator(1000, function(p){
var tx = 300 * (1-p);
box.style.transform = 'translate('
+ tx + 'px, 100px)';
});
var a4 = new Animator(1000, function(p){
var ty = 200 * (1-p);
box.style.transform = 'translateY('
+ ty + 'px)';
});
box.addEventListener('click', async function(){
await a1.animate();
await a2.animate();
await a3.animate();
await a4.animate();
});
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
我们可以看到,一个物体的运动通过async await的加持下,其序列运动就变得非常简单。下面我们通过了解animator库,来实现这样的promise动画的实现。
# 具体实现
在这里,我们用到了requestAnimationFrame(请求动画帧),它将告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。window.requestAnimationFrame(fn)我们只需要传入回调函数fn,它则会返回一个一个整数请求 ID 用于取消动画的执行,当下次重绘时去执行传入的fn函数
首先,我们需要对其使用的方法进行polyfill,当然自带babel插件的话可以让其帮忙做这件事。
function nowtime(){
if(typeof performance !== 'undefined' && performance.now){
return performance.now();
}
return Date.now ? Date.now() : (new Date()).getTime();
}
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());
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
- 这里兼容了requestAnimationFrame,当浏览器不支持时使用定时器来代替其功能。
- 时间的获取方面,对于requestAnimationFrame,它回调的参数 timestamp 是一个 DOMHighResTimeStamp 对象,它比 Date 的计时要更精确(可以精确到纳秒)。因此获取时间我们优先使用 performance.now(),如果浏览器不支持 performance.now(),我们再降级使用 Date.now()。
接下来我们看看具体实现:
class Animator {
constructor(duration, update, easing) {
this.duration = duration
this.update = update
this.easing = easing
}
animate(startTime) {
startTime = startTime || 0
const duration = this.duration,
update = this.update,
easing = this.easing,
self = this
return new Promise(((resolve, reject) => {
let qId = 0
function step(timestamp) {
startTime = startTime || timestamp
const p = Math.min(1.0, (timestamp - startTime) / duration)
update.call(self, easing ? easing(p) : p, p)
if(p < 1.0) {
qId = requestAnimationFrame(step)
} else {
resolve(startTime + duration)
}
}
self.cancel = function () {
cancelAnimationFrame(qId)
update.call(self, 0, 0)
resolve(startTime + duration)
}
qId = requestAnimationFrame(step)
}))
}
ease(easing) {
return new Animator(this.duration, this.update, easing)
}
}
module.exports = Animator
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
Animator 构造的时候可以传三个参数:
- 第一个是动画的总时长(duration)
- 第二个是动画每一帧的 update 事件,在这里可以改变元素的属性,从而实现动画
- 第三个参数是 easing, 也就是动画运动的贝塞尔曲线。
其中第二个参数 update 事件回调提供两个参数,一是 ep,是经过 easing 之后的动画进程,二是 p,是不经过 easing 的动画进程,ep 和 p 的值都是从 0 开始,到 1 结束。我们从而可以通过时间的进程去改变运动的轨迹。
Animator 有一个 animate 的对象方法,它返回一个 promise,当动画播放完成时,它的 promise 被 resolve,使用者还可以在 promise resolve 前调用 cancel 方法,这样它的 promise 会被 reject。
Animator搭配上 async/await 代码,时序动画的实现简洁且优雅,扩展性强,是后续实现复杂动画的优选。