由於最近做了一些頁面的動畫效果,之前經驗不多,這次做的過程中碰到些問題,加之很早前就閱讀過一篇很好介紹動畫的博客《關於動畫,你需要知道的》,來自十年蹤跡,所以就思考了一些關於動畫的基本原理的問題,例如本文這個。這個問題要簡單也可以非常簡單,例如前面提到那篇部落格裡就有一個比較好的解釋,本文提供的是另一種更詳細地方式,希望對有需要的人有所價值。
在客觀的物體運動中,以勻速直線運動為例,我們可以同時用速度與時間曲線或位移與時間曲線來描述物體的運動:
不管是用速度與時間的關係還是位移與時間的關係來描述客觀物體的直線運動,物體的狀態都是一致的,這是因為客觀物體的運動總是沿著人無法改變的客觀時間軸進行變化,在時間軸上的任意一點,總有特定的速度以及位移與之對應。
而在網頁動畫中,雖然它也呈現為運動,但是我們不能用客觀物體的運動規律去描述它。我認為原因主要是動畫的本質不是運動,只是基於定時器對元素狀態進行的瞬間改變。以一個簡單的元素進行水平勻速偏移的動畫效果為例,要實現這個動畫,只要用一個定時器在一個固定的時間間隔,重新設定元素的x軸偏移量即可,大概用圖可以描述如下:
圖中t1~t6代表定時器回呼函數執行的時刻。在這個效果中,元素的偏移位置將在定時器每次執行的時刻發生變化,而在相鄰的兩個執行時刻之間,元素的偏移位置是不變的。我們看到的動畫,僅僅是因為定時器間隔時間太短,從視覺上感知不到這段時間的過程,如果將定時器間隔加到足夠長,我們就能看到元素在間隔時間內的狀態了。
正因動畫不是運動,所以我們在嘗試理解一些動畫過程的時候,不能用運動規律去思考。例如我們該如何去理解動畫停止那一刻的狀態?也以前面提到的這個動畫效果為例,當把定時器清除的時候,動畫瞬間停止,對於元素而言,它的動畫速度將驟變為0,如果我們類比到客觀的物體運動,總是會想當然地以為元素的動畫也應該先有個減速的過程才能停止下來,要是這樣想,就沒辦法理解元素動畫停止時驟停的原理了。但是當我們從動畫的本質去思考這個問題的時候,就很好理解了,因為定時器是元素在動畫過程中發生狀態改變的唯一要素,當定時器不起作用的時候,就沒有外在的力量去改變元素的狀態了,它又怎麼動?
儘管動畫不是運動,我們還是希望找到一個方式,能夠很好的控制動畫的快慢,以便打造更加流暢,更加逼近客觀世界的動畫效果。當提到快慢,就很容易想到速度,因為在客觀物體運動中,速度就是用來描述運動快慢的要素。而且用速度的規律來控制動畫的快慢,看起來也很好理解和實現。將前面的例子再具體一點,假如我們想實現一個元素在1秒內向右勻速偏移120px的動畫效果,那麼只要用定時器控制元素每次往右偏移固定的量即可,這裡面定時器每次執行給元素的偏移量,就是我們用來控制動畫的速度。如果我們以16ms作為定時器的間隔,那麼這個動畫的速度可以通過: 120px / (1000ms / 16ms) 求得(約等於2px),也就是說只要定時器每次執行的時候將元素往右偏移2px就能實現我們要的效果。簡單程式碼實作如下:
<span style="color: #0000ff"><span style="color: #ff00ff">doctype html</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">html </span><span style="color: #ff0000">lang</span><span style="color: #0000ff">="en"</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">head</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">meta </span><span style="color: #ff0000">charset</span><span style="color: #0000ff">="UTF-8"</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">title</span><span style="color: #0000ff">></span>Document<span style="color: #0000ff"></span><span style="color: #800000">title</span><span style="color: #0000ff">></span> <span style="color: #0000ff"></span><span style="color: #800000">head</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">body</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">div </span><span style="color: #ff0000">id</span><span style="color: #0000ff">="box"</span><span style="color: #ff0000"> style</span><span style="color: #0000ff">="width: 100px;height: 100px;background-color: goldenrod"</span><span style="color: #0000ff">></span> <span style="color: #0000ff"></span><span style="color: #800000">div</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">br</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">button </span><span style="color: #ff0000">type</span><span style="color: #0000ff">="button"</span><span style="color: #ff0000"> onclick</span><span style="color: #0000ff">="start()"</span><span style="color: #0000ff">></span>开始<span style="color: #0000ff"></span><span style="color: #800000">button</span><span style="color: #0000ff">></span> <span style="color: #0000ff"></span><span style="color: #800000">body</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">script</span><span style="color: #0000ff">></span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> box </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> document.getElementById(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">box</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">); </span><span style="color: #0000ff; background-color: #f5f5f5">function</span><span style="color: #000000; background-color: #f5f5f5"> start() { </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> duration </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">1000</span><span style="color: #000000; background-color: #f5f5f5">;</span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">动画时长</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> s </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">120</span><span style="color: #000000; background-color: #f5f5f5">;</span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">总的偏移量</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> cur_s </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">0</span><span style="color: #000000; background-color: #f5f5f5">;</span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">当前偏移总量</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> p </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">16</span><span style="color: #000000; background-color: #f5f5f5">;</span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">定时器间隔</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> speed </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> s </span><span style="color: #000000; background-color: #f5f5f5">/</span><span style="color: #000000; background-color: #f5f5f5"> (duration </span><span style="color: #000000; background-color: #f5f5f5">/</span><span style="color: #000000; background-color: #f5f5f5"> p);</span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">速度</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> count </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">0</span><span style="color: #000000; background-color: #f5f5f5">; </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> start_time </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime(); </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> timer </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> setInterval(</span><span style="color: #0000ff; background-color: #f5f5f5">function</span><span style="color: #000000; background-color: #f5f5f5">(){ </span><span style="color: #0000ff; background-color: #f5f5f5">if</span><span style="color: #000000; background-color: #f5f5f5">(cur_s </span><span style="color: #000000; background-color: #f5f5f5">>=</span><span style="color: #000000; background-color: #f5f5f5"> s) { clearInterval(timer); console.log(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">动画运行时间(ms): </span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (</span><span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime() </span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5"> start_time)); </span><span style="color: #0000ff; background-color: #f5f5f5">return</span><span style="color: #000000; background-color: #f5f5f5">; } count</span><span style="color: #000000; background-color: #f5f5f5">++</span><span style="color: #000000; background-color: #f5f5f5">; cur_s </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> speed </span><span style="color: #000000; background-color: #f5f5f5">*</span><span style="color: #000000; background-color: #f5f5f5"> count; box.style.transform </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">translateX(</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> cur_s </span><span style="color: #000000; background-color: #f5f5f5">+</span> <span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">px)</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">; },p); } </span><span style="color: #0000ff"></span><span style="color: #800000">script</span><span style="color: #0000ff">></span> <span style="color: #0000ff"></span><span style="color: #800000">html</span><span style="color: #0000ff">></span></span></span></span></span></span></span></span></span></span></span>
在浏览器中运行以上代码,动画效果肯定是跟预期一致的,而且动画的实际执行时间也与规定的时长相差很小:
至于为什么不完全等于1000ms,那是因为多的那20多毫秒都耗费在了代码执行上。
通过这个例子,看起来,我们用速度去控制动画的思路还比较可行。事实上,这种思路是很有局限性的,我不是说它不行,只是说局限性,就是只能用于小部分的场合,而不能适用更广泛的动画效果中。为什么呢,原因有多个方面。
先从定时器说起。
定时器给了我们一种通过代码的方式来管理时间轴,但是这个时间轴与客观时间轴是有差别的。假如我们把一个动画的定时器间隔放大,放大到1000ms,让这个定时器执行10次,定时器执行的真实时间间隔会等于1000ms吗?
<span style="color: #0000ff"><span style="color: #800000">script</span><span style="color: #0000ff">></span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> start </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime(),count </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">1</span><span style="color: #000000; background-color: #f5f5f5">; </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> timer </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> setInterval(</span><span style="color: #0000ff; background-color: #f5f5f5">function</span><span style="color: #000000; background-color: #f5f5f5">(){ </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> end </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime(); console.log(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">第</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> count</span><span style="color: #000000; background-color: #f5f5f5">++</span> <span style="color: #000000; background-color: #f5f5f5">+</span> <span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">次执行,间隔:</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (end </span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5"> start)); start </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> end; </span><span style="color: #0000ff; background-color: #f5f5f5">if</span><span style="color: #000000; background-color: #f5f5f5">(count </span><span style="color: #000000; background-color: #f5f5f5">==</span> <span style="color: #000000; background-color: #f5f5f5">11</span><span style="color: #000000; background-color: #f5f5f5">) { clearInterval(timer); } },</span><span style="color: #000000; background-color: #f5f5f5">1000</span><span style="color: #000000; background-color: #f5f5f5">); </span><span style="color: #0000ff"></span><span style="color: #800000">script</span><span style="color: #0000ff">></span></span>
以上代码模拟了一个动画,并且放大了动画的时间间隔,如果把它拿到浏览器中执行,我们会得到下面类似的结果:
从这个结果可以看出,虽然定时器的间隔设置为了1000ms,但是实际的执行间隔却只能说在1000左右浮动。这是很正常的,假如我们把操作系统的时间看成是客观的时间轴,那么浏览器里面定时器构建的时间轴只能是一个尽可能的接近客观时间轴的模拟时间轴。操作系统的状态,浏览器的状态,定时器内外代码的执行时间都会影响这根时间轴与客观时间轴的差距,只考虑浏览器内部,定时器内外的代码执行时间越长,这其中的差距越大。因为上面的代码是在一个很简单的网页中测试出来的,所以定时器的实际间隔与客观时间的偏差很小,要是一个页面内容比较多的时候,这个偏差一定会比现在的大。
时间轴的不稳定性,会直接导致速度的不稳定性,也就是说匀速运动都无法达到理想状态,更别说其它复杂的变速运动了。
单从这点来说,不管用什么方式控制运动,都会存在这个问题,所以它还并不能完全说明速度控制动画的根本问题所在。这个根本问题在于无法确保动画能够按照规定的时长完成。在上面的例子的基础上,我们想办法把定时器的时间轴与客观时间差的偏差放大,这个不难办到,只要在定时器执行过程中,加入一些耗时任务即可,代码如下:
<span style="color: #0000ff"><span style="color: #800000">script</span><span style="color: #0000ff">></span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> start </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime(), prev </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> start, count </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">1</span><span style="color: #000000; background-color: #f5f5f5">; </span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">在动画模拟的第2和第3秒之间插入一个耗时任务</span> <span style="color: #000000; background-color: #f5f5f5"> setTimeout(</span><span style="color: #0000ff; background-color: #f5f5f5">function</span><span style="color: #000000; background-color: #f5f5f5"> () { </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> i </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">0</span><span style="color: #000000; background-color: #f5f5f5">; </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> cur </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime(); console.log(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">耗时任务开始,距动画开始时间:</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (cur</span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5">start)); </span><span style="color: #0000ff; background-color: #f5f5f5">while</span><span style="color: #000000; background-color: #f5f5f5"> (</span><span style="color: #000000; background-color: #f5f5f5">++</span><span style="color: #000000; background-color: #f5f5f5">i </span><span style="color: #000000; background-color: #f5f5f5"> <span style="color: #000000; background-color: #f5f5f5">3000000000</span><span style="color: #000000; background-color: #f5f5f5">); </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> cur2 </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime(); console.log(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">耗时任务结束,距动画开始时间:</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (cur2</span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5">start) </span><span style="color: #000000; background-color: #f5f5f5">+</span> <span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">,耗时:</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (cur2</span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5">cur)); }, </span><span style="color: #000000; background-color: #f5f5f5">2400</span><span style="color: #000000; background-color: #f5f5f5">); </span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">模拟一个动画</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> timer </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> setInterval(</span><span style="color: #0000ff; background-color: #f5f5f5">function</span><span style="color: #000000; background-color: #f5f5f5"> () { </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> end </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime(); console.log(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">第</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> count</span><span style="color: #000000; background-color: #f5f5f5">++</span> <span style="color: #000000; background-color: #f5f5f5">+</span> <span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">次执行,间隔:</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (end </span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5"> prev)); prev </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> end; </span><span style="color: #0000ff; background-color: #f5f5f5">if</span><span style="color: #000000; background-color: #f5f5f5"> (count </span><span style="color: #000000; background-color: #f5f5f5">==</span> <span style="color: #000000; background-color: #f5f5f5">11</span><span style="color: #000000; background-color: #f5f5f5">) { clearInterval(timer); } }, </span><span style="color: #000000; background-color: #f5f5f5">1000</span><span style="color: #000000; background-color: #f5f5f5">); </span><span style="color: #0000ff"></span><span style="color: #800000">script</span><span style="color: #0000ff">></span></span></span>
把以上代码在浏览器中运行,我们可以得到下面的类似结果:
根据以上结果中的时间范围,我们把这个例子的整个过程转换为时间轴示意图的话,就能看得更清晰了:
在这个图中,忽略了临界点之间的微小差距,因为只要观察那些大的差距,就能发现问题。结合前面的代码跟示意图,我们能看出:
由于有耗时任务的加入,导致动画的实际执行总时间接近于12s,比规定的动画时长多出整整2s。
虽然说从上面的图中也能看到另外一个问题,就是动画第三次执行的时间间隔被延长为3.67,而第四次执行的时间间隔被缩短为0.33s,会导致动画在这个时间段左右会看到不连贯不流畅的效果,但是这个问题不管用什么样的方式都会存在,只要有其它耗时任务在处理,动画定时器的回调就必须排队等待耗时任务完成才能执行。
无法控制动画在规定时长内完成,是不能用速度与时间的关系去实现动画的最重要的原因。
综上所述,为什么不用速度去控制动画有两个原因:
一是因为动画的时间轴的不稳定性(耗时任务会加大这种不稳定性),导致速度的变化规律很难把握。即使是匀速动画,我们也要考虑定时器的间隔,动画的偏移量,动画的时长三个参数才能计算出一个平均速度。如果是变速动画呢,比如我们想要一个动画先加速再匀速后减速,这种动画快慢的控制要求显然就无法轻易实现了。
二就是因为无法控制动画时长。
那么用什么样的方式来控制动画,就能够达到我们想要的轻易控制动画速度的目标呢?
用偏移量(位移)跟时间的关系吗?显然也是不行的,因为仅仅是单纯的速度控制改变为位移控制,并不会从根本上解决问题,因为速度与时间的关系还有位移与时间的关系是等价的。
速度无法控制动画时长的原因在于,由于已知的动画偏移量跟动画时长,导致动画定时器的执行次数也是固定的!所以只要某些次数定时器的实际执行时间超过理想的执行间隔,就会拉长动画时间轴跟客观时间轴的差距,就像上面示意图所看到的那样。
真正能解决动画时长的控制问题在于我们一定要用客观时间轴去控制动画。这个能做到吗?当然是可以的,来看看正确实现一个动画的方式,还是以前面那个小方块往右移动的动画为例,代码修改如下:
<span style="color: #0000ff"><span style="color: #ff00ff">doctype html</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">html </span><span style="color: #ff0000">lang</span><span style="color: #0000ff">="en"</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">head</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">meta </span><span style="color: #ff0000">charset</span><span style="color: #0000ff">="UTF-8"</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">title</span><span style="color: #0000ff">></span>Document<span style="color: #0000ff"></span><span style="color: #800000">title</span><span style="color: #0000ff">></span> <span style="color: #0000ff"></span><span style="color: #800000">head</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">body</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">div </span><span style="color: #ff0000">id</span><span style="color: #0000ff">="box"</span><span style="color: #ff0000"> style</span><span style="color: #0000ff">="width: 100px;height: 100px;background-color: goldenrod"</span><span style="color: #0000ff">></span> <span style="color: #0000ff"></span><span style="color: #800000">div</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">br</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">button </span><span style="color: #ff0000">type</span><span style="color: #0000ff">="button"</span><span style="color: #ff0000"> onclick</span><span style="color: #0000ff">="start()"</span><span style="color: #0000ff">></span>开始<span style="color: #0000ff"></span><span style="color: #800000">button</span><span style="color: #0000ff">></span> <span style="color: #0000ff"></span><span style="color: #800000">body</span><span style="color: #0000ff">></span> <span style="color: #0000ff"><span style="color: #800000">script</span><span style="color: #0000ff">></span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> box </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> document.getElementById(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">box</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">); </span><span style="color: #0000ff; background-color: #f5f5f5">function</span><span style="color: #000000; background-color: #f5f5f5"> start() { </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> duration </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">1000</span><span style="color: #000000; background-color: #f5f5f5">;</span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">动画时长</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> s </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">120</span><span style="color: #000000; background-color: #f5f5f5">;</span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">总的偏移量</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> start_time </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> Date.now(); </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> timer </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> setInterval(</span><span style="color: #0000ff; background-color: #f5f5f5">function</span><span style="color: #000000; background-color: #f5f5f5">(){ </span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">percent表示动画的进程</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> percent </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> (Date.now() </span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5"> start_time) </span><span style="color: #000000; background-color: #f5f5f5">/</span><span style="color: #000000; background-color: #f5f5f5"> duration; </span><span style="color: #0000ff; background-color: #f5f5f5">if</span><span style="color: #000000; background-color: #f5f5f5">(percent </span><span style="color: #000000; background-color: #f5f5f5">>=</span> <span style="color: #000000; background-color: #f5f5f5">1.0</span><span style="color: #000000; background-color: #f5f5f5">) { percent </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">1</span><span style="color: #000000; background-color: #f5f5f5">; clearInterval(timer); console.log(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">动画运行时间(ms): </span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (</span><span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime() </span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5"> start_time)); } box.style.transform </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">translateX(</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (Math.floor(s </span><span style="color: #000000; background-color: #f5f5f5">*</span><span style="color: #000000; background-color: #f5f5f5"> percent)) </span><span style="color: #000000; background-color: #f5f5f5">+</span> <span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">px)</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">; },</span><span style="color: #000000; background-color: #f5f5f5">16</span><span style="color: #000000; background-color: #f5f5f5">); } </span><span style="color: #0000ff"></span><span style="color: #800000">script</span><span style="color: #0000ff">></span> <span style="color: #0000ff"></span><span style="color: #800000">html</span><span style="color: #0000ff">></span></span></span></span></span></span></span></span></span></span></span>
實際執行結果如下:
接下來我們總結下這個方式的做法。首先在程式碼中可以看到,我們用這種方式
引入了動畫進程的概念,透過動畫進程來控制動畫的完成:
由於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写的动画里面。比如:
<span style="color: #0000ff"><span style="color: #800000">script</span><span style="color: #0000ff">></span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> box </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> document.getElementById(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">box</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">); </span><span style="color: #0000ff; background-color: #f5f5f5">function</span><span style="color: #000000; background-color: #f5f5f5"> start() { </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> duration </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">1000</span><span style="color: #000000; background-color: #f5f5f5">;</span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">动画时长</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> s </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">120</span><span style="color: #000000; background-color: #f5f5f5">;</span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">总的偏移量</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> start_time </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> Date.now(); </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> easing </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> BezierEasing(</span><span style="color: #000000; background-color: #f5f5f5">0.86</span><span style="color: #000000; background-color: #f5f5f5">, </span><span style="color: #000000; background-color: #f5f5f5">0</span><span style="color: #000000; background-color: #f5f5f5">, </span><span style="color: #000000; background-color: #f5f5f5">0.07</span><span style="color: #000000; background-color: #f5f5f5">, </span><span style="color: #000000; background-color: #f5f5f5">1</span><span style="color: #000000; background-color: #f5f5f5">); </span><span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> timer </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> setInterval(</span><span style="color: #0000ff; background-color: #f5f5f5">function</span><span style="color: #000000; background-color: #f5f5f5">(){ </span><span style="color: #008000; background-color: #f5f5f5">//</span><span style="color: #008000; background-color: #f5f5f5">percent表示动画的进程</span> <span style="color: #0000ff; background-color: #f5f5f5">var</span><span style="color: #000000; background-color: #f5f5f5"> percent </span><span style="color: #000000; background-color: #f5f5f5">=</span><span style="color: #000000; background-color: #f5f5f5"> (Date.now() </span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5"> start_time) </span><span style="color: #000000; background-color: #f5f5f5">/</span><span style="color: #000000; background-color: #f5f5f5"> duration; </span><span style="color: #0000ff; background-color: #f5f5f5">if</span><span style="color: #000000; background-color: #f5f5f5">(percent </span><span style="color: #000000; background-color: #f5f5f5">>=</span> <span style="color: #000000; background-color: #f5f5f5">1.0</span><span style="color: #000000; background-color: #f5f5f5">) { percent </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">1</span><span style="color: #000000; background-color: #f5f5f5">; clearInterval(timer); console.log(</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">动画运行时间(ms): </span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (</span><span style="color: #0000ff; background-color: #f5f5f5">new</span><span style="color: #000000; background-color: #f5f5f5"> Date().getTime() </span><span style="color: #000000; background-color: #f5f5f5">-</span><span style="color: #000000; background-color: #f5f5f5"> start_time)); } box.style.transform </span><span style="color: #000000; background-color: #f5f5f5">=</span> <span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">translateX(</span><span style="color: #000000; background-color: #f5f5f5">'</span> <span style="color: #000000; background-color: #f5f5f5">+</span><span style="color: #000000; background-color: #f5f5f5"> (Math.floor(s </span><span style="color: #000000; background-color: #f5f5f5">*</span><span style="color: #000000; background-color: #f5f5f5"> easing(percent))) </span><span style="color: #000000; background-color: #f5f5f5">+</span> <span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">px)</span><span style="color: #000000; background-color: #f5f5f5">'</span><span style="color: #000000; background-color: #f5f5f5">; },</span><span style="color: #000000; background-color: #f5f5f5">16</span><span style="color: #000000; background-color: #f5f5f5">); } </span><span style="color: #0000ff"></span><span style="color: #800000">script</span><span style="color: #0000ff">></span></span>
总之,有了ease跟贝塞尔曲线工具,要实现不同的动画速度控制效果,就变成一件特别容易的事情了。
最后,希望这篇文章能帮助到一些朋友更好理解动画的原理以及动画速度控制的正确方式。