因爲最近作了一些頁面的動畫效果,以前經驗很少,此次作的過程當中碰到些問題,加之很早前就閱讀過一篇很好介紹動畫的博客《關於動畫,你須要知道的》,來自十年蹤影,因此就思考了一些關於動畫的基本原理的問題,好比本文這個。這個問題要簡單也能夠很是簡單,好比前面提到那篇博客裏就有一個比較好的解釋,本文提供的是另一種更詳細地方式,但願對有須要的人有所價值。css
在客觀的物體運動中,以勻速直線運動爲例,咱們能夠同時用速度與時間曲線或位移與時間曲線來描述物體的運動:html
不論是用速度與時間的關係仍是位移與時間的關係來描述客觀物體的直線運動,物體的狀態都是一致的,這是由於客觀物體的運動老是沿着人沒法改變的客觀時間軸進行變化,在時間軸上的任意一點,總有特定的速度以及位移與之對應。git
而在網頁動畫中,雖然它也呈現爲運動,可是咱們不能用客觀物體的運動規律去描述它。我認爲緣由主要是動畫的本質不是運動,僅僅是基於定時器對元素狀態進行的瞬間改變。以一個簡單的元素進行水平勻速偏移的動畫效果爲例,要實現這個動畫,只要用一個定時器在一個固定的時間間隔,從新設置元素的x軸偏移量便可,大概用圖能夠描述以下:github
圖中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="width: 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>
在瀏覽器中運行以上代碼,動畫效果確定是跟預期一致的,並且動畫的實際執行時間也與規定的時長相差很小:post
至於爲何不徹底等於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="width: 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跟貝塞爾曲線工具,要實現不一樣的動畫速度控制效果,就變成一件特別容易的事情了。
最後,但願這篇文章能幫助到一些朋友更好理解動畫的原理以及動畫速度控制的正確方式。