由于最近做了一些页面的动画效果,之前经验不多,这次做的过程中碰到些问题,加之很早前就阅读过一篇很好介绍动画的博客《关于动画,你需要知道的》,来自十年踪迹,所以就思考了一些关于动画的基本原理的问题,比如本文这个。这个问题要简单也可以非常简单,比如前面提到那篇博客里就有一个比较好的解释,本文提供的是另外一种更详细地方式,希望对有需要的人有所价值。
在客观的物体运动中,以匀速直线运动为例,我们可以同时用速度与时间曲线或位移与时间曲线来描述物体的运动:
不管是用速度与时间的关系还是位移与时间的关系来描述客观物体的直线运动,物体的状态都是一致的,这是因为客观物体的运动总是沿着人无法改变的客观时间轴进行变化,在时间轴上的任意一点,总有特定的速度以及位移与之对应。
而在网页动画中,虽然它也呈现为运动,但是我们不能用客观物体的运动规律去描述它。我认为原因主要是动画的本质不是运动,仅仅是基于定时器对元素状态进行的瞬间改变。以一个简单的元素进行水平匀速偏移的动画效果为例,要实现这个动画,只要用一个定时器在一个固定的时间间隔,重新设置元素的x轴偏移量即可,大概用图可以描述如下:
图中t1~t6代表定时器回调函数执行的时刻。在这个效果中,元素的偏移位置将在定时器每次执行的时刻发生变化,而在相邻的两个执行时刻之间,元素的偏移位置是不变的。我们看到的动画,仅仅是因为定时器间隔时间太短,从视觉上感知不到这段时间的过程,如果将定时器间隔加到足够长,我们就能看到元素在间隔时间内的状态了。
正因动画不是运动,所以我们在尝试理解一些动画过程的时候,不能用运动规律去思考。比如我们该如何去理解动画停止那一刻的状态?还以前面提到的这个动画效果为例,当把定时器清掉的时候,动画瞬间停止,对于元素而言,它的动画速度将骤变为0,如果我们类比到客观的物体运动,总是会想当然地以为元素的动画也应该先有个减速的过程才能停止下来,要是这样想,就没办法理解元素动画停止时骤停的原理了。但是当我们从动画的本质去思考这个问题的时候,就很好理解了,因为定时器是元素在动画过程中发生状态改变的唯一要素,当定时器不起作用的时候,就没有外在的力量去改变元素的状态了,它还怎么能动呢?
尽管动画不是运动,我们还是希望找到一个方式,能够很好的控制动画的快慢,以便打造更加流畅,更加逼近客观世界的动画效果。当提到快慢,就很容易想到速度,因为在客观物体运动中,速度就是用来描述运动快慢的要素。而且用速度的规律来控制动画的快慢,看起来也很好理解和实现。将前面的的例子再具体一点,假如我们想实现一个元素在1秒内往右匀速偏移120px的动画效果,那么只要用定时器控制元素每次往右偏移固定的量即可,这里面定时器每次执行给元素添加的偏移量,就是我们用来控制动画的速度。如果我们以16ms作为定时器的间隔,那么这个动画的速度可以通过: 120px / (1000ms / 16ms) 求得(约等于 2px),也就是说只要定时器每次执行的时候将元素往右偏移2px就能实现我们要的效果。简单代码实现如下:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div id="box" style=" 100px;height: 100px;background-color: goldenrod"> </div> <br> <button type="button" onclick="start()">开始</button> </body> <script> var box = document.getElementById('box'); function start() { var duration = 1000;//动画时长 var s = 120;//总的偏移量 var cur_s = 0;//当前偏移总量 var p = 16;//定时器间隔 var speed = s / (duration / p);//速度 var count = 0; var start_time = new Date().getTime(); var timer = setInterval(function(){ if(cur_s >= s) { clearInterval(timer); console.log('动画运行时间(ms): ' + (new Date().getTime() - start_time)); return; } count++; cur_s = speed * count; box.style.transform = 'translateX(' + cur_s + 'px)'; },p); } </script> </html>
在浏览器中运行以上代码,动画效果肯定是跟预期一致的,而且动画的实际执行时间也与规定的时长相差很小:
至于为什么不完全等于1000ms,那是因为多的那20多毫秒都耗费在了代码执行上。
通过这个例子,看起来,我们用速度去控制动画的思路还比较可行。事实上,这种思路是很有局限性的,我不是说它不行,只是说局限性,就是只能用于小部分的场合,而不能适用更广泛的动画效果中。为什么呢,原因有多个方面。
先从定时器说起。
定时器给了我们一种通过代码的方式来管理时间轴,但是这个时间轴与客观时间轴是有差别的。假如我们把一个动画的定时器间隔放大,放大到1000ms,让这个定时器执行10次,定时器执行的真实时间间隔会等于1000ms吗?
<script> var start = new Date().getTime(),count = 1; var timer = setInterval(function(){ var end = new Date().getTime(); console.log('第' + count++ + '次执行,间隔:' + (end - start)); start = end; if(count == 11) { clearInterval(timer); } },1000); </script>
以上代码模拟了一个动画,并且放大了动画的时间间隔,如果把它拿到浏览器中执行,我们会得到下面类似的结果:
从这个结果可以看出,虽然定时器的间隔设置为了1000ms,但是实际的执行间隔却只能说在1000左右浮动。这是很正常的,假如我们把操作系统的时间看成是客观的时间轴,那么浏览器里面定时器构建的时间轴只能是一个尽可能的接近客观时间轴的模拟时间轴。操作系统的状态,浏览器的状态,定时器内外代码的执行时间都会影响这根时间轴与客观时间轴的差距,只考虑浏览器内部,定时器内外的代码执行时间越长,这其中的差距越大。因为上面的代码是在一个很简单的网页中测试出来的,所以定时器的实际间隔与客观时间的偏差很小,要是一个页面内容比较多的时候,这个偏差一定会比现在的大。
时间轴的不稳定性,会直接导致速度的不稳定性,也就是说匀速运动都无法达到理想状态,更别说其它复杂的变速运动了。
单从这点来说,不管用什么方式控制运动,都会存在这个问题,所以它还并不能完全说明速度控制动画的根本问题所在。这个根本问题在于无法确保动画能够按照规定的时长完成。在上面的例子的基础上,我们想办法把定时器的时间轴与客观时间差的偏差放大,这个不难办到,只要在定时器执行过程中,加入一些耗时任务即可,代码如下:
<script> var start = new Date().getTime(), prev = start, count = 1; //在动画模拟的第2和第3秒之间插入一个耗时任务 setTimeout(function () { var i = 0; var cur = new Date().getTime(); console.log('耗时任务开始,距动画开始时间:' + (cur-start)); while (++i < 3000000000); var cur2 = new Date().getTime(); console.log('耗时任务结束,距动画开始时间:' + (cur2-start) + ',耗时:' + (cur2-cur)); }, 2400); //模拟一个动画 var timer = setInterval(function () { var end = new Date().getTime(); console.log('第' + count++ + '次执行,间隔:' + (end - prev)); prev = end; if (count == 11) { clearInterval(timer); } }, 1000); </script>
把以上代码在浏览器中运行,我们可以得到下面的类似结果:
根据以上结果中的时间范围,我们把这个例子的整个过程转换为时间轴示意图的话,就能看得更清晰了:
在这个图中,忽略了临界点之间的微小差距,因为只要观察那些大的差距,就能发现问题。结合前面的代码跟示意图,我们能看出:
由于有耗时任务的加入,导致动画的实际执行总时间接近于12s,比规定的动画时长多出整整2s。
虽然说从上面的图中也能看到另外一个问题,就是动画第三次执行的时间间隔被延长为3.67,而第四次执行的时间间隔被缩短为0.33s,会导致动画在这个时间段左右会看到不连贯不流畅的效果,但是这个问题不管用什么样的方式都会存在,只要有其它耗时任务在处理,动画定时器的回调就必须排队等待耗时任务完成才能执行。
无法控制动画在规定时长内完成,是不能用速度与时间的关系去实现动画的最重要的原因。
综上所述,为什么不用速度去控制动画有两个原因:
一是因为动画的时间轴的不稳定性(耗时任务会加大这种不稳定性),导致速度的变化规律很难把握。即使是匀速动画,我们也要考虑定时器的间隔,动画的偏移量,动画的时长三个参数才能计算出一个平均速度。如果是变速动画呢,比如我们想要一个动画先加速再匀速后减速,这种动画快慢的控制要求显然就无法轻易实现了。
二就是因为无法控制动画时长。
那么用什么样的方式来控制动画,就能够达到我们想要的轻易控制动画速度的目标呢?
用偏移量(位移)跟时间的关系吗?显然也是不行的,因为仅仅是单纯的速度控制改变为位移控制,并不会从根本上解决问题,因为速度与时间的关系还有位移与时间的关系是等价的。
速度无法控制动画时长的原因在于,由于已知的动画偏移量跟动画时长,导致动画定时器的执行次数也是固定的!所以只要某些次数定时器的实际执行时间超过理想的执行间隔,就会拉长动画时间轴跟客观时间轴的差距,就像上面示意图所看到的那样。
真正能解决动画时长的控制问题在于我们一定要用客观时间轴去控制动画。这个能做到吗?当然是可以的,来看看正确实现一个动画的方式,还是以前面那个小方块往右移动的动画为例,代码修改如下:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div id="box" style=" 100px;height: 100px;background-color: goldenrod"> </div> <br> <button type="button" onclick="start()">开始</button> </body> <script> var box = document.getElementById('box'); function start() { var duration = 1000;//动画时长 var s = 120;//总的偏移量 var start_time = Date.now(); var timer = setInterval(function(){ //percent表示动画的进程 var percent = (Date.now() - start_time) / duration; if(percent >= 1.0) { percent = 1; clearInterval(timer); console.log('动画运行时间(ms): ' + (new Date().getTime() - start_time)); } box.style.transform = 'translateX(' + (Math.floor(s * percent)) + 'px)'; },16); } </script> </html>
实际执行结果如下:
接下来我们总结下这个方式的做法。首先在代码中可以看到,我们用这种方式
引入了动画进程的概念,通过动画进程来控制动画的完成:
由于percent这个动画进程,我们是基于客观时间轴得出的,这样就能保证动画一定能够在规定的时间内完成,不会再出现速度控制动画时,动画执行时间被延长的问题了。(当然如果在动画执行过程中,我们加入一个非常耗时的任务的话,不管什么动画都无法在规定时间内完成)。
接着我们在处理动画的偏移量的时候,就只需要将总的偏移量 * 动画进程 就得到当前执行时刻的偏移量了。
最后当动画进程为1的时候,动画结束,并且元素被设置为了动画规定的总的偏移量。
总的来说,这个方式就是把位移与时间的关系,转换成了偏移量与动画进程的关系。通过动画进程,同时控制动画时长和动画偏移的完成度。
更重要的是,偏移量与动画进程的关系,可以经由客观的运动规律推导出来:
比如上面的例子中,由于是匀速动画,所以它的规律是:
动画进程 p = t / T; (t = Date.now() – start_time ; T = duration)
偏移量 Sp = S * p; (S为总的偏移量;Sp为当前偏移量)
如果是其它的动画,比如匀加速动画,匀减速动画,圆周动画,我们也能得到类似的规律。而且所有动画效果动画进程计算方式都是一样的,唯一不同的是偏移量跟动画进程的关系而已:
匀加速:Sp=S * P2
匀减速:Sp=S * P * (2−P)
圆周x轴: Sp=S * cos(ω * P)
圆周y轴: Sp=S * sin(ω * P)
(以上四种关系的推导我也没有仔细研究,早先的数学知识忘了不少,感兴趣的可以去研究《关于动画,你需要知道的》)
同一个动画,应用以上不同的规律,就可以看到不同速度的动画变化效果,最终就实现了我们想要用动画模拟现实世界物体运动的目的。
再研究这些偏移量跟动画进程的关系,我们发现,总的偏移量S在这个关系中,仅仅是一个参数的作用,当把S去掉时,我们就得到一个跟S完全无关,仅仅跟动画进程有关系的方程:
匀速:ep = p
匀加速:ep= P2
匀减速:ep= P * (2−P)
圆周x轴: ep= cos(ω * P)
圆周y轴: ep= sin(ω * P)
用一个函数来表示以上所有规律就是:ep = E(P),P∈[0,1],P代表动画进程,ep代表偏移量的完成百分比。需要再补充的是,这个关系还必须满足一个条件就是当P=0的时候,ep 必须为0;P=1的时候,ep必须为1。这个应该很好理解了,因为P=0和P=1,以及ep=0和ep=1分别代表动画的开始跟结束状态。
也就是说,只要找到一个函数满足上一段文字的所有条件,比如前面的那些,那么这个函数就可以作为我们控制动画快慢的方法。这个函数
就是所谓的动画算子ease。下面的这些函数图像都可以作为动画的算子:
有了这个规律,就赋予了动画效果控制无限的可能性,因为能满足前面那些条件的函数是无穷的。而这些看起来无穷尽的函数,我们能够轻松地通过贝塞尔曲线工具绘制出来,并且在css里面我们可以直接把这个工具的参数直接应用于transition跟animation里面。js里面也有bezier-easing 库可以使用这个工具的参数,然后应用到我们用js写的动画里面。比如:
<script> var box = document.getElementById('box'); function start() { var duration = 1000;//动画时长 var s = 120;//总的偏移量 var start_time = Date.now(); var easing = BezierEasing(0.86, 0, 0.07, 1); var timer = setInterval(function(){ //percent表示动画的进程 var percent = (Date.now() - start_time) / duration; if(percent >= 1.0) { percent = 1; clearInterval(timer); console.log('动画运行时间(ms): ' + (new Date().getTime() - start_time)); } box.style.transform = 'translateX(' + (Math.floor(s * easing(percent))) + 'px)'; },16); } </script>
总之,有了ease跟贝塞尔曲线工具,要实现不同的动画速度控制效果,就变成一件特别容易的事情了。
最后,希望这篇文章能帮助到一些朋友更好理解动画的原理以及动画速度控制的正确方式。