最近看到了一篇介绍JS动画的好文章,就把文章进行翻译总结后放在这里。
动画基础
JS动画实质上就是通过连续的改变DOM元素的styles
或者canvas对象来达到动的效果。
整个动画过程可以拆分成几小部分,每部分通过定时器timer
控制,由于timer
的时间间隔非常短,所以看起来就是一个连续的动画。
1 | var id = setInterval(function() { |
代码中,帧和帧之间的delay是10ms
,即每秒100帧
多数JS框架中,默认是10-15ms
,较少的delay会让动画看起来更流畅更平滑,当然这是要求浏览器能够足够快,以满足按时完成每步的动画。
如果一个动画需要很多计算,CPU会100%load,那么反应会变得非常迟钝,这种情况下,就需要增加delay的值了。举个例子,40ms
相当于每秒25帧,已经接近相机标准24帧了。
动画代码重构
为了让animation更具一般化,需要下面的这些参数:
delay
帧之间的时间,单位ms
duration
整个动画持续的时间,单位ms
start
动画开始的时间,start = new Date
下面就是动画过程的核心部分了,每帧都需要计算的参数:
timePassed
从动画开始经过的时间,单位ms
,通常范围是0~duration
之间,可能在浏览器timer不准确的情况下,偶尔会超过duration
的值。
progress
小数,表示动画已经进行了多久,计算方式timePassed/duration
,通常范围是0~1之间。比如progress = 0.5
表示已经过去了duration
的一般时间了。
delta(progress)
function,返回当前动画的progress。比如,让属性height
从0%增大到100%:
mapping:
progress = 0
->height = 0%
progress = 0.2
->height = 20%
progress = 0.5
->height = 50%
progress = 0.8
->height = 80%
progress = 1
->height = 100%
有时我们想让动画不是线性变换,而是开始慢,然后快,那么也可以是下面的情况
mapping:
progress = 0
->height = 0%
progress = 0.2
->height = 4%
progress = 0.5
->height = 25%
progress = 0.8
->height = 64%
progress = 1
->height = 100%
delta(progress)
就是一个将时间进度progress
映射到动画进度delta
的函数,表明了progress
和delta
的映射关系。
step(delta)
function, 以delta
作为参数
比如:
1 | function step(delta) { |
总结上面的主要参数:
delay
就是setInterval
的第二个参数duration
指整个动画完成需要的时间progress
指经过了多长时间,取值0~1之间delta
计算动画的当前进度,前提是已知当前的时间step
将当前的动画进度赋给元素
一个普通动画的例子
将上面所讲的几个参数融合到一个动画函数里
1 | function animate(opts) { |
上例中的opts
需要包含如下属性:
delay
duration
- function
delta
- function
step
在上例的基础上,写一个动画函数move
1 | function move(element, delta, duration) { |
调用:
1 | <div onclick="move(this.children[0], function(p) {return p})" class="example_path"> |
delta = function(p) {return p}
表示动画进度在时间上是匀速的
效果是demo中的第一种情况
delta
函数
动画根据给定的规则随时间变化,而这个规则由delta
函数来实现。
不同的delta
函数让动画的速度,准确度以及其他参数有多种表现形式,函数里通常是一个数学公式。
下面举几个非常常用的公式:
注:下面所有delta函数的对应效果均可在demo中查看。
linear delta
1 | function linear(progress) { |
数学图形如下:
横轴是progress
,纵轴是delta(progress)
。动画的速度是固定值,也就是匀速运动。
power of n
delta
是progress
的n次方
1 | function quad(progress) { |
Circ: a piece of circle
1 | function circ(progress) { |
Back: the bow function 弯曲函数
这个函数会先后退,然后加速前进
与之前的函数不同,这个函数依赖一个额外的参数x,叫做弹性系数
1 | function back(progress, x) { |
x = 1.5
时的图形
Bounce 弹跳
这个效果可以想象成一个球掉到地板上,然后弹跳的效果
在达到一个目标点时停止弹跳
1 | function bounce(progress) { |
Elastic 弹性
这个函数也有一个额外的参数x
,用来定义初始范围
1 | function elastic(progress, x) { |
在x = 1.5
时的数学图形为
Reverse functions 逆函数
直接使用上面总结的各种delta
函数,均属于easeIn
模式。
然而,有时候需要以time-reversed mode的方式来展示动画,这种叫做easeOut
模式,通过“time-reversing”delta
方式来控制。
easeOut
在这个模式下,delta需要做转换:deltaEaseOut = 1 - delta(1 - progress)
,然后将deltaEaseOut
作为delta
参数传入move
函数。
举一个bounce
在easeOut
模式下的例子:
1 | function bounce(progress) { |
我们通过下图来看一下easeOut
是如何变化的:
红线是easeIn(normal),绿线是easeOut(time-reversed)
- easeIn的情况下,开始会在底部缓慢弹跳,然后弹到最高处
- easeOut模式下,会在瞬间弹至顶端,然后缓慢在底部弹跳
可以看出easeOut
会造成time-reversed,即时间反转。
easeInOut
另外一种情况就是在动画的开始和结束都出现delta
定义的效果,这种就是easeInOut
。
转换过程如下:
1 | function makeEaseInOut(delta) { |
上面的变换过程把easeIn
和easeOut
结合到了一起。
总结:
- easeIn 最基本的表现:开始缓慢,然后加速
- easeOut 表现为time-reversed,开始很快,然后越来越慢
- easeInOut 上面两种情况的混合,动画被拆分为两半,第一部分是easeIn,第二部分是easeOut
改变step
可以任意改变step
来做各种动画,opcity,width,height,color…都可以做动画。
Color highlight
1 | function highlight(elem) { |
CSS transitions
可以通过CSS transitions来实现一些简单的动画效果。
然而跟JS相比还是有局限性:
- 在JS里,可以通过设置
delta
来使动画呈现不同的变化,CSS中也即transition-timing-function
,然而CSS只支持3次Bézier曲线 - 只有CSS属性才能做为动画,当然比较好的地方在于,多个属性都可以同时设置动画
所以如果是比较简单的动画可以采用CSS的方式来实现,比较省事儿
优化提示
较多的定时器会造成CPU过多的消耗
如果需要同时运行多个动画,比如下着的雪花,将他们放在一个单独的定时器里。
因为每个定时器都会对页面进行重绘,如果只有单独的一个重绘,那么浏览器效率会更高。
一个动画框架通常只有一个单独的setInterval
,在它上面运行所有帧。
帮助浏览器去渲染
浏览器控制渲染树,元素之间互相依赖。
如果动画元素在DOM很深层的地方,那么其他元素就会依赖于其几何构造和位置。尽管动画没有移动他们,但浏览器不得不使用更多的额外计算。
解决方案:
- 将原本放在DOM里的动画元素直接放到开始的
body
下,需要将其设置成绝对定位 - 加动画
- 将其重新放回DOM中
这样可以减少动画的不稳定性,同时降低CPU的计算。
本文的demo示例均可在这里查看。