JS动画基础

最近看到了一篇介绍JS动画的好文章,就把文章进行翻译总结后放在这里。

动画基础

JS动画实质上就是通过连续的改变DOM元素的styles或者canvas对象来达到动的效果。

整个动画过程可以拆分成几小部分,每部分通过定时器timer控制,由于timer的时间间隔非常短,所以看起来就是一个连续的动画。

1
2
3
4
var id = setInterval(function() {
/* show the current frame */
if (/* finished */) clearInterval(id)
}, 10)

代码中,帧和帧之间的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的函数,表明了progressdelta的映射关系。

step(delta)

function, 以delta作为参数

比如:

1
2
3
function step(delta) {
elem.style.height = 100*delta + '%'
}

总结上面的主要参数:

  • delay就是setInterval的第二个参数
  • duration指整个动画完成需要的时间
  • progress指经过了多长时间,取值0~1之间
  • delta计算动画的当前进度,前提是已知当前的时间
  • step将当前的动画进度赋给元素

一个普通动画的例子

将上面所讲的几个参数融合到一个动画函数里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function animate(opts) {

var start = new Date

var id = setInterval(function() {
var timePassed = new Date - start
var progress = timePassed / opts.duration

if (progress > 1) progress = 1

var delta = opts.delta(progress)
opts.step(delta)

if (progress == 1) {
clearInterval(id)
}
}, opts.delay || 10)

}

上例中的opts需要包含如下属性:

  • delay
  • duration
  • function delta
  • function step

在上例的基础上,写一个动画函数move

1
2
3
4
5
6
7
8
9
10
11
12
13
function move(element, delta, duration) {
var to = 500

animate({
delay: 10,
duration: duration || 1000, // 1 sec by default
delta: delta,
step: function(delta) {
element.style.left = to*delta + "px"
}
})

}

调用:

1
2
3
<div onclick="move(this.children[0], function(p) {return p})" class="example_path">
<div class="example_block"></div>
</div>

delta = function(p) {return p}表示动画进度在时间上是匀速的

效果是demo中的第一种情况

delta函数

动画根据给定的规则随时间变化,而这个规则由delta函数来实现。

不同的delta函数让动画的速度,准确度以及其他参数有多种表现形式,函数里通常是一个数学公式。

下面举几个非常常用的公式:

注:下面所有delta函数的对应效果均可在demo中查看。

linear delta

1
2
3
function linear(progress) {
return progress
}

数学图形如下:

横轴是progress,纵轴是delta(progress)。动画的速度是固定值,也就是匀速运动。

power of n

deltaprogress的n次方

1
2
3
function quad(progress) {
return Math.pow(progress, 2)
}

Circ: a piece of circle

1
2
3
function circ(progress) {
return 1 - Math.sin(Math.acos(progress)) //Math.acos()返回一个数的反余弦
}

Back: the bow function 弯曲函数

这个函数会先后退,然后加速前进

与之前的函数不同,这个函数依赖一个额外的参数x,叫做弹性系数

1
2
3
function back(progress, x) {
return Math.pow(progress, 2) * ((x + 1) * progress - x)
}

x = 1.5时的图形

Bounce 弹跳

这个效果可以想象成一个球掉到地板上,然后弹跳的效果

在达到一个目标点时停止弹跳

1
2
3
4
5
6
7
function bounce(progress) {
for(var a = 0, b = 1, result; 1; a += b, b /= 2) {
if (progress >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2)
}
}
}

Elastic 弹性

这个函数也有一个额外的参数x,用来定义初始范围

1
2
3
function elastic(progress, x) {
return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*x/3*progress)
}

x = 1.5时的数学图形为

Reverse functions 逆函数

直接使用上面总结的各种delta函数,均属于easeIn模式。

然而,有时候需要以time-reversed mode的方式来展示动画,这种叫做easeOut模式,通过“time-reversing”delta方式来控制。

easeOut

在这个模式下,delta需要做转换:deltaEaseOut = 1 - delta(1 - progress),然后将deltaEaseOut作为delta参数传入move函数。

举一个bounceeaseOut模式下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function bounce(progress) {
for(var a = 0, b = 1, result; 1; a += b, b /= 2) {
if (progress >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2);
}
}
}

function makeEaseOut(delta) {
return function(progress) {
return 1 - delta(1 - progress)
}
}

var bounceEaseOut = makeEaseOut(bounce);

我们通过下图来看一下easeOut是如何变化的:

红线是easeIn(normal),绿线是easeOut(time-reversed)

  • easeIn的情况下,开始会在底部缓慢弹跳,然后弹到最高处
  • easeOut模式下,会在瞬间弹至顶端,然后缓慢在底部弹跳

可以看出easeOut会造成time-reversed,即时间反转。

easeInOut

另外一种情况就是在动画的开始和结束都出现delta定义的效果,这种就是easeInOut

转换过程如下:

1
2
3
4
5
6
7
8
9
10
function makeEaseInOut(delta) {  
return function(progress) {
if (progress < .5) //动画的前半段
return delta(2*progress) / 2;
else //动画的后半段
return (2 - delta(2*(1-progress))) / 2;
}
}

bounceEaseInOut = makeEaseInOut(bounce);

上面的变换过程把easeIneaseOut结合到了一起。

总结:

  • easeIn 最基本的表现:开始缓慢,然后加速
  • easeOut 表现为time-reversed,开始很快,然后越来越慢
  • easeInOut 上面两种情况的混合,动画被拆分为两半,第一部分是easeIn,第二部分是easeOut

改变step

可以任意改变step来做各种动画,opcity,width,height,color…都可以做动画。

Color highlight

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function highlight(elem) {
var from = [255,0,0], to = [255,255,255]
animate({
delay: 10,
duration: 1000,
delta: linear,
step: function(delta) {
elem.style.backgroundColor = 'rgb(' +
Math.max(Math.min(parseInt((delta * (to[0]-from[0])) + from[0], 10), 255), 0) + ',' +
Math.max(Math.min(parseInt((delta * (to[1]-from[1])) + from[1], 10), 255), 0) + ',' +
Math.max(Math.min(parseInt((delta * (to[2]-from[2])) + from[2], 10), 255), 0) + ')';

}
});

}

CSS transitions

可以通过CSS transitions来实现一些简单的动画效果。

然而跟JS相比还是有局限性:

  • 在JS里,可以通过设置delta来使动画呈现不同的变化,CSS中也即transition-timing-function,然而CSS只支持3次Bézier曲线
  • 只有CSS属性才能做为动画,当然比较好的地方在于,多个属性都可以同时设置动画

所以如果是比较简单的动画可以采用CSS的方式来实现,比较省事儿

优化提示

较多的定时器会造成CPU过多的消耗

如果需要同时运行多个动画,比如下着的雪花,将他们放在一个单独的定时器里。

因为每个定时器都会对页面进行重绘,如果只有单独的一个重绘,那么浏览器效率会更高。

一个动画框架通常只有一个单独的setInterval,在它上面运行所有帧。

帮助浏览器去渲染

浏览器控制渲染树,元素之间互相依赖。

如果动画元素在DOM很深层的地方,那么其他元素就会依赖于其几何构造和位置。尽管动画没有移动他们,但浏览器不得不使用更多的额外计算。

解决方案:

  1. 将原本放在DOM里的动画元素直接放到开始的body下,需要将其设置成绝对定位
  2. 加动画
  3. 将其重新放回DOM中

这样可以减少动画的不稳定性,同时降低CPU的计算。

本文的demo示例均可在这里查看。