小话前端动画

前言

在如今的web时代,网页中的动画已经成为web交互中必不可少的要素,小到一个按钮的点击,大到一个炫酷的特效,各路神圣大显神通,展示了web动画的无限可能,进而动画也就成了前端开发者不得不认真了解的一个领域。

就目前来看,前端实现动画就有六种方式:

  • css的transition
  • css的animation
  • javascript利用setTimeout手写
  • requestAnimationFrames
  • SVG
  • Canvas

以上六种中,SVG由于CSS3的出现使用得越来越少了,而Canvas又是一个很庞大的领域,作为h5如今力压flash的王牌,canvas需要一定的额外学习成本。因此这篇文章将暂时不探讨这两种方式。

而剩下的四种方式,习惯上我们更容易想到,用transition和animation做比较,用setTimeout和requestAnimationFrames做比较,因此我们也已这样的角度,分别探究一下这四种方式的各个方面

CSS阵营

为什么css动画高效

分层与合成

在浏览器渲染原理中我们了解到,浏览器的渲染过程有回流和重绘。但是完整的一帧画面,实际上包括三个过程:重排、重绘、合成

Chrome中对于合成技术用上三个字可以概括:分层,分块,合成

为什么要引入分层与合成:当页面需要实现一些复杂的动画效果,从渲染树直接生成目标画面的话,由于涉及到的元素太多,一个微小的变化都可能引起页面的重排和重绘,严重影响页面的渲染效率。

为了解决这个问题,Chrome引入了分层与合成机制。

何为分层与合成

对于这个概念,了解过学习过photoshop的就不难理解,你可以把一个网页想象成多个图片叠加在一起形成的,类似于ps中的图层的概念,而Chrome的合成器就是将这些图片合成最终显示的页面。

在此过程中,将页面分为多个图层的过程就称为分层,图层合并的过程称为合成。

Chrome如何实现分层与合成

当渲染树生成时,渲染引擎会根据树的一些特点将其转换为层树,层树中每个节点对应的一个图层,因此绘制阶段并不是绘制真正的页面,而是绘制指令组合成为一个列表。

那么什么情况下,渲染引擎会为元素创建新图层?

  1. 拥有层叠上下文
  • 文档根元素(<html>);
  • position 值为 absolute或 relative且 z-index 值不为 auto 的元素;
  • position 值为 fixed(固定定位)或 sticky(粘滞定位)的元素
  • flex (flexbox) 容器的子元素,且 z-index 值不为 auto;
  • grid (grid) 容器的子元素,且 z-index 值不为 auto;
  • opacity 属性值小于 1 的元素(参见 the specification for opacity);
  • mix-blend-mode 属性值不为 normal 的元素;
  • 以下任意属性值不为 none 的元素:
    transform
    filter
    perspective
    clip-path
    mask / mask-image / mask-border
  • isolation 属性值为 isolate 的元素;
  • -webkit-overflow-scrolling 属性值为 touch 的元素;
  • will-change 值设定了任一属性而该属性在 non-initial 值时会创建层叠上下文的元素
  • contain 属性值为 layout、paint 或包含它们其中之一的合成值(比如 contain: strict、contain: content)的元素。
  1. 需要裁剪的地方
    例如:文字内容比较多,超出了显示区域。


那么所谓的合成,其实也就可以理解为,原本需要由浏览器计算并绘制完成的完整页面,然后交给GPU显示,变成了浏览器只绘制渲染层页面,此时交给GPU的页面是不完整的,剩下的合成层需要由GPU计算并绘制,并最终显示到屏幕上。

所以就可以理解为什么css3中动画、fixed元素(好好理解一下,get到那个点),canvas/video/iframe等等都交给了合成层由GPU去绘制

而对于合成操作,实在合成线程上完成且由GPU计算的,这意味着在进行合成操作时,是不会影响到主线程的,这也是css动画高效的一个主要原因。

分块

如果说分层是从宏观上提升了渲染效率,那么分块则是从微观层面提升了渲染效率。

通常情况下,页面内容远大于屏幕大小。如果等所有图层都生成完毕再合成,会让合成图片的时间变得更久。

因此合成线程会将每个图层分为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以加速页面显示速度。不过,有时即使只绘制优先级高的图块,也要耗费不少时间,因为中间涉及到纹理上传,从计算机内存.上传到GPU内存的操作会比较慢。

为了解决这个问题,Chrome 在首次合成图块的时候使用一个低分辨率的图片。

分辨率减少-半,纹理就减少了四分之三。首次展示页面时,展示低分辨率图片,然后合成器继续绘制正常比例的页面内容,当绘制完成后,再替换掉当前显示的低分辨率内容。

利用分层技术优化代码

如果要对某个元素做几何形状变化、透明度变换或者缩放,可以使用will-change告诉渲染引擎,如下:

1
.box { will-change: transform, opacity; }

will-change会提前告诉渲染引擎box元素要做几何变换和透明度变换操作,这是渲染引擎会将该元素单独实现一层,等变换发生时,渲染引擎会通过合成线程直接处理变换,此变换不涉及主线程,故效率高。

注意点: will-change会让渲染引擎为该元素准备独立层,占用的内存也会大大增加,因为从层树开始,后续每个阶段都会多一个层结构,都需要额外内存。

transition、transform、animation

假设我们将一个页面元素的高度从100px渐变到200px

transition做动画时两个线程的工作情况是:对于浏览器来说,元素的高度一直在变化,因此这个动画的每一帧都需要主线程对元素进行布局,绘制成位图,将位图交由GPU线程,GPU将位图绘制到屏幕。两个线程来回切换工作,即使是移动十几个像素,主线程也需要对元素布局很多次,这部分的工作消耗相当大,相对较慢,这也是transition动画经常卡顿的原因。

1
2
3
4
5
6
7
8
div {
height: 100px;
transition: height 1s linear;
}

div:hover {
height: 200px;
}

在这里插入图片描述

使用transform时浏览器的工作情况是:主线程对元素进行布局,绘制成位图,交由GPU线程,transform执行的整个过程都在GPU进程执行绘制,两个线程来回切换的情况不多,而且transform不会触发浏览器的重新排版,不会影响周围的布局,这也意味着这种情况的动画比transition流畅一些。

1
2
3
4
5
6
7
div {
transform: scale(0.5);
transition: transform 1s linear;
}
div:hover {
transform: scale(1.0);
}

而animation几乎做到了transform那样的性能,只不过从一定程度上来讲,animation要更消耗性能,由于关键帧的引入,使得animation足以应付很多复杂的动画效果

与 transition 不同的是,keyframes提供更多的控制,尤其是时间轴的控制,这点让css animation更加强大,使得flash的部分动画效果可以由css直接控制完成,而这一切,仅仅只需要几行代码,也因此诞生了大量(比起flash来说算是大量了)基于css的animation tools,用来取代flash的动画部分。

JS阵营

众所周知,现在我们的大部分屏幕刷新率都是60hz(别跟我提什么120hz电竞屏,切图仔不吃这一套)。在通常情况下,当动画能够到达60帧时就能够给人以流畅的体验,于是就出现了setTimeout和setInterval的动画实现方案,即每16ms执行一次回调函数,在回调函数中像素级别地修改目标元素的而样式,手动控制动画的开始和停止。

可能有人会觉得,那我把函数的时间间隔设置短一点,是不是就可以把帧率提得更高了呢?

然而。。。。

相当一部分的浏览器的显示频率是16.7ms, 就是上图第一行的节奏,表现就是“我和你一步两步三步四步往前走……”。如果我们火力搞猛一点,例如搞个10ms setTimeout,就会是下面一行的模样——每第三个图形都无法绘制(红色箭头指示),表现就是“我和你一步两步 坑 四步往前走……”。

也就是说如果你设置的间隔和用户屏幕刷新率不是整除型匹配,这样做反而会产生卡顿。

那么使用setTimeout有没有其他问题呢?

当然有,学习了浏览器的事件循环event loop机制我们可以回想到,setTimeout和setInterval在浏览器中是宏任务,而一次event loop循环中,只有浏览器执行完主线程任务和微任务队列的任务,才会从宏队列中拿出setTimeout和setInterval回调函数中的任务去执行。这意味着,如果在16ms内浏览器没能执行完主线程和微队列上的任务,那么就无法保证每16ms稳定执行setTime和setInterval的回调函数,无法做到60帧的帧率,那么自然我们的动画就发生了卡顿。

requestAnimationFrames

针对上述两个问题,raf都分别有自己的解决方案

首先对于第一个刷新间隔的问题:
requestAnimationFrame所做的事情很简单,跟着浏览器的绘制走,如果浏览设备绘制间隔是16.7ms,那就这个间隔绘制;如果浏览设备绘制间隔是10ms, 就10ms绘制。这样就不会存在过度绘制的问题,动画不会掉帧,自然流畅的说~~

而对于第二个问题:
浏览器(如页面)每次要重绘,就会通知requestAnimationFrame:老铁,我要重绘了,你可以执行你内部的函数了!

这是资源非常高效的一种利用方式。怎么讲呢?

  • 就算很多个raf,浏览器只要通知一次就可以了。而setTimeout貌似是多个独立绘制。
  • 页面最小化了,或者被Tab切换关灯了。页面是不会重绘的,自然,raf的执行也就停止了。页面绘制全部停止,资源高效利用。

raf的优势

优秀的兼容性

虽然说目前对于IE8和IE9,开发者们已经几乎不放什么心思在做他们的兼容性上了,但是你不得不承认的是,这货如今依然有大量的用户和机构在使用。

对于一些动画,可以使用CSS3实现,但是由于要考虑兼容性,不得不用setTimeout再去实现一遍。然而,raf表示:这都不是个事儿。

requestAnimationFrame跟setTimeout非常类似,都是单回调,用法也类似。

1
2
var handle = setTimeout(renderLoop, PERIOD);
var handle = requestAnimationFrame(renderLoop);

而requestAnimationFrame调用一次只会重绘一次动画

一些css不能做到的动画

使用CSS3动画可以改变高宽,方位,角度,透明度等等。但是,就像六道带土也有弱点一样,CSS3动画也有属性鞭长莫及。比方说scrollTop值。如果我们希望返回顶部是个平滑滚动效果,就目前而言,CSS3似乎是无能为力的。此时,还是要JS出马

css支持的动画效果有限

由于CSS3动画的贝塞尔曲线是一个标准3次方曲线(详见:animation-timing-function
),因此,只能是:Linear, Sine, Quad, Cubic, Expo等,但对于Back, Bounce等缓动则只可观望而不可亵玩焉。

像这些复杂的动画,css3是无法实现的

注:事实上,css3已经有cubic-bezier属性,可以设置贝塞尔曲线;

而关于缓动(Tween),有下面这些形式:

  • Linear:无缓动效果
  • Quadratic:二次方的缓动(t^2)
  • Cubic:三次方的缓动(t^3)
  • Quartic:四次方的缓动(t^4)
  • Quintic:五次方的缓动(t^5)
  • Sinusoidal:正弦曲线的缓动(sin(t))
  • Exponential:指数曲线的缓动(2^t)
  • Circular:圆形曲线的缓动(sqrt(1-t^2))
  • Elastic:指数衰减的正弦曲线缓动
  • 超过范围的三次方缓动((s+1)t^3 – st^2)
  • 指数衰减的反弹缓动

每个效果都分三个缓动方式,分别是:

  • easeIn:从0开始加速的缓动;
  • easeOut:减速到0的缓动;
  • easeInOut:前半段从0开始加速,后半段减速到0的缓动。

Tween算法:

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/*
* Tween.js
* t: current time(当前时间)
* b: beginning value(初始值)
* c: change in value(变化量)
* d: duration(持续时间)
*/
var Tween = {
Linear: function(t, b, c, d) { return c*t/d + b; },
Quad: {
easeIn: function(t, b, c, d) {
return c * (t /= d) * t + b;
},
easeOut: function(t, b, c, d) {
return -c *(t /= d)*(t-2) + b;
},
easeInOut: function(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t + b;
return -c / 2 * ((--t) * (t-2) - 1) + b;
}
},
Cubic: {
easeIn: function(t, b, c, d) {
return c * (t /= d) * t * t + b;
},
easeOut: function(t, b, c, d) {
return c * ((t = t/d - 1) * t * t + 1) + b;
},
easeInOut: function(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t*t + b;
return c / 2*((t -= 2) * t * t + 2) + b;
}
},
Quart: {
easeIn: function(t, b, c, d) {
return c * (t /= d) * t * t*t + b;
},
easeOut: function(t, b, c, d) {
return -c * ((t = t/d - 1) * t * t*t - 1) + b;
},
easeInOut: function(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
return -c / 2 * ((t -= 2) * t * t*t - 2) + b;
}
},
Quint: {
easeIn: function(t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
},
easeOut: function(t, b, c, d) {
return c * ((t = t/d - 1) * t * t * t * t + 1) + b;
},
easeInOut: function(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b;
return c / 2*((t -= 2) * t * t * t * t + 2) + b;
}
},
Sine: {
easeIn: function(t, b, c, d) {
return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
},
easeOut: function(t, b, c, d) {
return c * Math.sin(t/d * (Math.PI/2)) + b;
},
easeInOut: function(t, b, c, d) {
return -c / 2 * (Math.cos(Math.PI * t/d) - 1) + b;
}
},
Expo: {
easeIn: function(t, b, c, d) {
return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
},
easeOut: function(t, b, c, d) {
return (t==d) ? b + c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
},
easeInOut: function(t, b, c, d) {
if (t==0) return b;
if (t==d) return b+c;
if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
}
},
Circ: {
easeIn: function(t, b, c, d) {
return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
},
easeOut: function(t, b, c, d) {
return c * Math.sqrt(1 - (t = t/d - 1) * t) + b;
},
easeInOut: function(t, b, c, d) {
if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
}
},
Elastic: {
easeIn: function(t, b, c, d, a, p) {
var s;
if (t==0) return b;
if ((t /= d) == 1) return b + c;
if (typeof p == "undefined") p = d * .3;
if (!a || a < Math.abs(c)) {
s = p / 4;
a = c;
} else {
s = p / (2 * Math.PI) * Math.asin(c / a);
}
return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
},
easeOut: function(t, b, c, d, a, p) {
var s;
if (t==0) return b;
if ((t /= d) == 1) return b + c;
if (typeof p == "undefined") p = d * .3;
if (!a || a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = p/(2*Math.PI) * Math.asin(c/a);
}
return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b);
},
easeInOut: function(t, b, c, d, a, p) {
var s;
if (t==0) return b;
if ((t /= d / 2) == 2) return b+c;
if (typeof p == "undefined") p = d * (.3 * 1.5);
if (!a || a < Math.abs(c)) {
a = c;
s = p / 4;
} else {
s = p / (2 *Math.PI) * Math.asin(c / a);
}
if (t < 1) return -.5 * (a * Math.pow(2, 10* (t -=1 )) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p ) * .5 + c + b;
}
},
Back: {
easeIn: function(t, b, c, d, s) {
if (typeof s == "undefined") s = 1.70158;
return c * (t /= d) * t * ((s + 1) * t - s) + b;
},
easeOut: function(t, b, c, d, s) {
if (typeof s == "undefined") s = 1.70158;
return c * ((t = t/d - 1) * t * ((s + 1) * t + s) + 1) + b;
},
easeInOut: function(t, b, c, d, s) {
if (typeof s == "undefined") s = 1.70158;
if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
return c / 2*((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
}
},
Bounce: {
easeIn: function(t, b, c, d) {
return c - Tween.Bounce.easeOut(d-t, 0, c, d) + b;
},
easeOut: function(t, b, c, d) {
if ((t /= d) < (1 / 2.75)) {
return c * (7.5625 * t * t) + b;
} else if (t < (2 / 2.75)) {
return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
} else if (t < (2.5 / 2.75)) {
return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
} else {
return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
}
},
easeInOut: function(t, b, c, d) {
if (t < d / 2) {
return Tween.Bounce.easeIn(t * 2, 0, c, d) * .5 + b;
} else {
return Tween.Bounce.easeOut(t * 2 - d, 0, c, d) * .5 + c * .5 + b;
}
}
}
}
Math.tween = Tween;

使用方法(一个例子):

1
2
3
4
5
6
7
8
9
10
function startAnimation() {
let start = 0, during = 100;
let _fun = function () {
start ++;
let width = Tween.Quad.easeInOut(start, 0, 1000, during);
box.style.width = width + 'px';
if (start < during) requestAnimationFrame(_fun);
};
_fun();
}
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2015-2021 AURORA_ZXH
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信