HTML動畫 request animation frame
在網頁中,實現動畫無外乎兩種方式。
1. CSS3 方式,也就是利用瀏覽器對CSS3 的原生支持實現動畫;
2. 腳本方式,通過間隔一段時間用JavaScript 來修改頁面元素樣式來實現動畫。
接下來我們就分別介紹這兩種方式的原理,讓大家先對這兩種方式有一個直觀認識,了解各自的優缺點。
CSS3 的方式下,開發者一般在css 中定義一些包含CSS3 transition 語法的規則。在某些特定情況下,讓這些規則發生作用,於是瀏覽器就會將這些規則應用於指定的DOM元素上,產生動畫的效果。這種方式毫無疑問運行效率要比腳本方式高,因為瀏覽器原生支持,省去了JavaScript 的解釋執行負擔,有的瀏覽器(比如Chrome 瀏覽器)甚至還可以充分利用GPU 加速的優勢,進一步增強了動畫渲染的性能。不過CSS3 的方式並非完美,也有不少缺點。
首先, CSS3 Transition 對一個動畫規則的定義是基於時間和速度曲線( Speed Curve)的規則。換句話來說,就是CSS3 的動畫過程要描述成“在什麽時間範圍內,以什麽樣的運動節奏完成動畫” 。
<!DOCTYPE html> <html> <head> <style> .sample { background: red; position: absolute; left: 0px; width: 100px; height: 100px; transition-property: left; transition-duration: 0.5s; transition-timing-function: ease } .sample:hover { left: 420px; } </style> </head> <body> <div class="sample" /> </body> </html>
在上面的例子中, sample 類的元素定義了這樣的動畫屬性:“ left 屬性會在0.2 秒內以ease 速度曲線完成動畫” 。transition 只定義了動畫涉及的屬性、時間和速度曲線,並不定義需要修改的具體值。sample 類的left 屬性默認值為0 ,當鼠標移到sample 類元素上時, left 屬性就擁有新的值420px 。這時候transition 定義的規則發生作用,讓left 屬性以ease 速度曲線在0.2 秒
的時間完成從0 變成420px 的轉化過程,這個過程中,用戶看到的就是sample 類元素向右移動420 個像素的動畫過程。
因為CSS3 定義動畫的方式是基於時間和速度曲線,可能不利於動畫的流暢,因為動畫是可能會被中途打斷的,在上面的例子中,鼠標移到sample 類元素上的時候開始動畫,但是在0.2 秒的動畫時間內,用戶的鼠標可能會移出這個sample 類元素,這時候CSS3 還會以ease 速度曲線的節奏讓sample 類元素回到原位。從用戶體驗角度來說,中途sample 類元素回到原位的動作,語義上是“取消操作”的含義,但卻依然以同樣的時間和ease 節奏來完成“取消操作”的動畫,這並不合理。
時間和速度曲線的不合理是CSS3 先天的屬性,更讓開發者頭疼的就是開發CSS3 規則的過程,尤其是對transition-duration 時間很短的動畫調試,因為CSS3 的transition 過程總是一閃而過,捕捉不到中間狀態,只能一遍一遍用肉眼去檢驗動畫效果,用CSS3做過復雜動畫的開發者肯定都深有體會。雖然CSS3 有這樣一些缺點,但是因為其無與倫比的性能,用來處理一些簡單的動畫還是不錯的選擇。
相對於CSS3 方式,腳本方式最大的好處就是更強的靈活度,開發者可以任意控制動畫的時間長度,也可以控制每個時間點上元素渲染出來的樣式,可以更容易做出豐富的動畫效果。腳本方式的缺點也很明顯,動畫過程通過JavaScript 實現,不是瀏覽器原生支持,消耗的計算資源更多。如果處理不當,動畫可能會出現卡頓滯後現象,本來使用動畫是為了創造更好的用戶體驗,如果出現卡頓,反而對用戶體驗帶來不好的影響。最原始的腳本方式就是利用setlnterval 或者setTimeout 來實現,每隔一段時間一個指定的函數被執行來修改界面的內容或者樣式,從而達到動畫的效果。
<!DOCTYPE html> <html> <head> <style> #sample { position: absolute; background: red; width: 100px; height: 100px; } </style> </head> <body> <div id="sample" /> <script type="text/javascript"> var animatedElement = document.getElementById("sample"); var left = 0; var timer; var ANIMATION_INTERVAL = 16; timer = setInterval(function() { left += 10; animatedElement.style.left = left + "px"; if ( left >= 400 ) { clearInterval(timer); } }, ANIMATION_INTERVAL); </script> </body> </html>
在上面的例子中,有一個常量ANIMATION INTERVAL 定義為16 , setlnterval 以這個常量為間隔,每16 毫秒計算一次sample 元素的left 值,每次都根據時間推移按比例增加left 的值,直到left 大於400 。為什麽要選擇16 毫秒呢?因為每秒渲染60 幀(也叫60fps, 60 Frame Per Second)會給用戶帶來足夠流暢的視覺體驗,一秒鐘有1000 毫秒, 1000 /60 =16 ,也就是說,如果我們做到每16 毫秒去渲染一次畫面,就能夠達到比較流暢的動畫效果。對於簡單的動畫, setlnterval 方式勉強能夠及格,但是對於稍微復雜一些的動畫,腳本方式就頂不住了,比如渲染一幀要花去超過32 毫秒的時間,那麽還用16 毫秒一個間隔的方式肯定不行。實際上,因為一幀渲染要占用網頁線程32 毫秒,會導致setlnterval根本無法以16 毫秒間隔調用渲染函數,這就產生了明顯的動畫滯後感,原本一秒鐘完成的動畫現在要花兩秒鐘完成,所以這種原始的setlnterval 方式是肯定不適合復雜的動畫的。
出現上面問題的本質原因是setlnterval 和setTimeout 並不能保證在指定時間間隔或者延遲的情況下準時調用指定函數。所以可以換一個思路,當指定函數調用的時候,根據逝去的時間計算當前這一幀應該顯示成什麽樣子,這樣即使因為瀏覽器渲染主線程忙碌導致一幀渲染時間超過16 毫秒,在後續幀誼染時至少內容不會因此滯後,即使達不倒60fps 的效果,也能保證動畫在指定時間內完成。
<!DOCTYPE html> <html> <head> <style> #sample { position: absolute; background: red; width: 100px; height: 100px; } </style> </head> <body> <div id="sample" /> <script type="text/javascript"> var lastTimeStamp = new Date().getTime(); function raf(fn) { var currTimeStamp = new Date().getTime(); var delay = Math.max(0, 16 - (currTimeStamp - lastTimeStamp)); var handle = setTimeout(function(){ fn(currTimeStamp); }, delay); lastTimeStamp = currTimeStamp; return handle; } var left = 0; var animatedElement = document.getElementById("sample"); var startTimestamp = new Date().getTime(); function render(timestamp) { left += (timestamp - startTimestamp) / 16; animatedElement.style.left = left + ‘px‘; if (left < 400) { raf(render); } } raf(render); </script> </body> </html>
在上面定義的raf 中,接受的fn 函數參數是真正的渲染過程, raf 只是協調渲染的節奏。raf 盡量以每隔16 毫秒的速度去調用傳遞的fn 參數,如果發現上一次被調用時間和這一次被調用時間相差不足16 毫秒,就會保持16 毫秒一次的渲染間隔繼續,如果發現
兩次調用時間間隔已經超出了16 毫秒,就會在下一次時鐘周期立刻調用fn 。上面的render 函數中根據當前時間和開始動畫的時間差來計算sample 元素的left 屬性,這樣無論render 函數何時被調用,總能夠渲染出正確的結果。
最後,我們將render 作為參數傳遞給raf ,啟動了動畫過程
HTML動畫 request animation frame