HTML動畫 request animation frame

在網頁中,實現動畫無外乎兩種方式。
1. CSS3 方式,也就是利用瀏覽器對CSS3 的原生支持實現動畫;
2. 腳本方式,經過間隔一段時間用JavaScript 來修改頁面元素樣式來實現動畫。
接下來咱們就分別介紹這兩種方式的原理,讓你們先對這兩種方式有一個直觀認識,瞭解各自的優缺點。javascript

CSS3 的方式下,開發者通常在css 中定義一些包含CSS3 transition 語法的規則。在某些特定狀況下,讓這些規則發生做用,因而瀏覽器就會將這些規則應用於指定的DOM元素上,產生動畫的效果。這種方式毫無疑問運行效率要比腳本方式高,由於瀏覽器原生支持,省去了JavaScript 的解釋執行負擔,有的瀏覽器(好比Chrome 瀏覽器)甚至還能夠充分利用GPU 加速的優點,進一步加強了動畫渲染的性能。不過CSS3 的方式並不是完美,也有很多缺點。
首先, CSS3 Transition 對一個動畫規則的定義是基於時間和速度曲線( Speed Curve)的規則。換句話來講,就是CSS3 的動畫過程要描述成「在什麼時間範圍內,以什麼樣的運動節奏完成動畫」 。css

<!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 節奏來完成「取消操做」的動畫,這並不合理。html

          時間和速度曲線的不合理是CSS3 先天的屬性,更讓開發者頭疼的就是開發CSS3 規則的過程,尤爲是對transition-duration 時間很短的動畫調試,由於CSS3 的transition 過程老是一閃而過,捕捉不到中間狀態,只能一遍一遍用肉眼去檢驗動畫效果,用CSS3作過複雜動畫的開發者確定都深有體會。雖然CSS3 有這樣一些缺點,可是由於其無與倫比的性能,用來處理一些簡單的動畫仍是不錯的選擇。java

       相對於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 ,啓動了動畫過程性能

相關文章
相關標籤/搜索