1. 程式人生 > >HTML動畫 request animation frame

HTML動畫 request animation frame

oct 啟動 界面 當前時間 規則 cti 時鐘 req eas

在網頁中,實現動畫無外乎兩種方式。
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