css

幀動畫的多種實現方式與效能對比

Web動畫形式首先我們來了解一下Web有哪些動畫形式以上各種動畫形式都可以製作出一種型別的動畫,那就是幀動畫,...

>

Web動畫形式

首先我們來了解一下Web有哪些動畫形式

1. CSS3動畫
    Transform(變形)
    Transition(過渡)
    Animation(動畫)
2. JS動畫(操作DOM、修改CSS屬性值)
3. Canvas動畫
4. SVG動畫
5. 以Three.js為首的3D動畫
複製程式碼

以上各種動畫形式都可以製作出一種型別的動畫,那就是 幀動畫 ,也叫序列幀動畫,定格動畫,逐幀動畫等,這裏我們統一用幀動畫來表述。

img

應用場景

幀動畫一般用來實現稍微複雜一點的動畫效果,同時希望動畫更細膩,設計師更自由的發揮。他可以定義到每一個時間刻度上的展現內容,我們一般用幀動畫來做頁面的Loading,小人物,小物體元素的簡單動畫。我們想象中的幀動畫應該有以下幾個特點:

  1. 可以自由控制播放、暫停和停止
  2. 可以控制播放次數,播放速度
  3. 可以新增互動,在播放完成後新增事件
  4. 瀏覽器相容性好

素材準備

幀動畫的素材一般是先由設計師在PS中的時間軸上設計好了,然後匯出圖片給前端人員,PS製作時間軸動畫一般是用來製作稍微簡單的動畫,操作簡單,方便。

或者是由設計師在AE的時間軸進行設計,因為AE內建了更豐富的動作效果,比如轉換,翻轉之類的,AE可以幫助我們實現更復雜的效果,然後再匯出圖片給前端人員。

這裏幀動畫素材的要求,每一幀的圖片最好是偶數寬高,偶數張,最好周圍能有一些留白。

實現方案

將目前想到的解決方案梳理如下圖,同時我們將對每種方案進行詳細介紹。

img

一、GIF圖

我們可以將上面製作的幀動畫匯出成GIF圖,GIF圖會連續播放,無法暫停,它往往用來實現小細節動畫,成本較低、使用方便。但其缺點也是很明顯的:

  1. 畫質上,gif 支援顏色少(最大256色)、Alpha 透明度支援差,影象鋸齒毛邊比較嚴重;
  2. 互動上,不能直接控制播放、暫停、播放次數,靈活性差;
  3. 效能上,gif 會引起頁面週期性的 繪畫 ,效能較差。

二、CSS3幀動畫

CSS3幀動畫是我們今天需要重點介紹的方案,最核心的是利用CSS3中 Animation動畫 ,確切的說是使用 animation-timing-function 的階梯函式 steps(number_of_steps, direction)來實現逐幀動畫的連續播放。

幀動畫的實現原理是不斷切換視覺內圖片內容,利用視覺滯留生理現象來實現連續播放的動畫效果,下面我們來介紹製作CSS3幀動畫的幾種方案。

(1)連續切換動畫圖片地址src(不推薦)

我們將圖片放到元素的背景中( background-image ),通過更改 background-image 的值實現幀的切換。但是這種方式會有以下幾個缺點,所以該方案不推薦。

  • 多張圖片會帶來多個 HTTP 請求
  • 每張圖片首次載入會造成圖片切換時的閃爍
  • 不利於檔案的管理

(2)連續切換雪碧圖位置(推薦)

我們將所有的幀動畫圖片合併成一張雪碧圖,通過改變 background-position 的值來實現動畫幀切換。分兩步進行:

步驟一:將動畫幀合併爲雪碧圖,雪碧圖的要求可以看上面 素材準備 ,比如下面這張幀動畫雪碧圖,共20幀。

img

步驟二:使用steps階梯函式切換雪碧圖位置

先看寫法一:

<div class="sprite"></div>

.sprite {
    width: 300px;
    height: 300px;
    background-repeat: no-repeat;
    background-image: url(frame.png);
    animation: frame 1s steps(1,end) both infinite;
}
@keyframes frame {
    0% {background-position: 0 0;}
    5% {background-position: -300px 0;}
    10% {background-position: -600px 0;}
    15% {background-position: -900px 0;}
    20% {background-position: -1200px 0;}
    25% {background-position: -1500px 0;}
    30% {background-position: -1800px 0;}
    35% {background-position: -2100px 0;}
    40% {background-position: -2400px 0;}
    45% {background-position: -2700px 0;}
    50% {background-position: -3000px 0;}
    55% {background-position: -3300px 0;}
    60% {background-position: -3600px 0;}
    65% {background-position: -3900px 0;}
    70% {background-position: -4200px 0;}
    75% {background-position: -4500px 0;}
    80% {background-position: -4800px 0;}
    85% {background-position: -5100px 0;}
    90% {background-position: -5400px 0;}
    95% {background-position: -5700px 0;}
    100% {background-position: -6000px 0;}
}

針對以上動畫有疑問?

問題一:既然都詳細定義關鍵幀了,是不是可以不用steps函式了,直接定義linear變化不就好了嗎?

animation: frame 10s linear both infinite;

如果我們定義成這樣,動畫是不會階梯狀,一步一步執行的,而是會連續的變化背景圖位置,是移動的效果,而不是切換的效果,如下圖:

img

問題二:不是應該設定為20步嗎,怎麼變成了1?

這裏我們先來了解下 animation-timing-function 屬性。

CSS animation-timing-function 屬性定義CSS動畫在每一動畫週期中執行的節奏。對於關鍵幀動畫來說,timing function作用於一個關鍵幀週期而非整個動畫週期,即從關鍵幀開始開始,到關鍵幀結束結束。

timing-function 作用於每兩個關鍵幀之間,而不是整個動畫。

接著我們來了解下steps() 函式:

  • steps 函式指定了一個階躍函式,它接受兩個引數。
  • 第一個引數接受一個整數值,表示兩個關鍵幀之間分幾步完成。
  • 第二個引數有兩個值< start > or < end >。預設值為< end > 。
  • step-start 等同於 step(1, start)。step-end 等同於 step(1, end)。

綜上我們可以知道,因為我們詳細定義了一個關鍵幀週期,從開始到結束,每兩個關鍵幀之間分 1 步展示完,也就是說0% ~ 5%之間變化一次,5% ~ 10%變化一次,所以我們這樣寫才能達到想要的效果。

再看寫法二:

<div class="sprite"></div>

.sprite {
    width: 300px;
    height: 300px;
    background-repeat: no-repeat;
    background-image: url(frame.png);
    animation: frame 1s steps(20) both infinite;
}
@keyframes frame {
    0% {background-position: 0 0;}//可省略
    100% {background-position: -6000px 0;}
}

這裏我們定義了關鍵幀的開始和結束,也就是定義了一個關鍵幀週期,但因為我們沒有詳細的定義每一幀的展示,所以我們要將0%~100%這個區間分成20步來階段性展示。

也可以換成關鍵字的寫法,還可以只定義最後一幀,因為預設第一幀就是初始位置。

@keyframes frame {
    from {background-position: 0 0;}//可省略
    to {background-position: -6000px 0;}
}

(3)連續移動雪碧圖位置(移動端推薦)

跟第二種基本一致,只是切換雪碧圖的位置過程換成了 transform:translate3d() 來實現,不過要加多一層 overflow: hidden; 的容器包裹,這裏我們以只定義初始和結束幀為例,使用 transform 可以開啟GPU加速,提高機器渲染效果,還能有效解決移動端幀動畫抖動的問題。

<div class="sprite-wp">
    <div class="sprite"></div>
</div>

.sprite-wp {
    width: 300px;
    height: 300px;
    overflow: hidden;
}
.sprite {
    width: 6000px;
    height: 300px;
    will-change: transform;
    background: url(frame.png) no-repeat center;
    animation: frame 1s steps(20) both infinite;
}
@keyframes frame {
	0% {transform: translate3d(0,0,0);}
    100% {transform: translate3d(-6000px,0,0);}
}

三、JS幀動畫

(1)通過JS來控制img的src屬性切換(不推薦)

和上面CSS3幀動畫裏面切換元素 background-image 屬性一樣,會存在多個請求等問題,所以該方案我們不推薦,但是這是一種解決思路。

(2)通過JS來控制Canvas影象繪製

通過Canvas製作幀動畫的原理是用drawImage方法將圖片繪製到Canvas上,不斷擦除和重繪就能得到我們想要的效果。

<canvas id="canvas" width="300" height="300"></canvas>

(function () {
    var timer = null,
        canvas = document.getElementById("canvas"),
        context = canvas.getContext('2d'),
        img = new Image(),
        width = 300,
        height = 300,
        k = 20,
        i = 0;
    img.src = "frame.png";

    function drawImg() {
        context.clearRect(0, 0, width, height);
        i++;
        if (i == k) {
            i = 0;
        }
        context.drawImage(img, i * width, 0, width, height, 0, 0, width, height);
    }
    img.onload = function () {
        timer = setInterval(drawImg, 50);
    }
})();

上面是通過改變裁剪影象的X座標位置來實現動畫效果的,也可以通過改變畫布上放置影象的座標位置實現,如下: context.drawImage(img, 0, 0, width*k, height,-i*width,0,width*k,height);

(3)通過JS來控制CSS屬性值變化

這種方式和前面CSS3幀動畫一樣,有三種方式,一種是通過JS切換元素背景圖片地址 background-image ,一種是通過JS切換元素背景圖片定位 background-position ,最後一種是通過JS移動元素 transform:translate3d() ,第一種不做介紹,因為同樣會存在多個請求等問題,不推薦使用,這裏實現後面兩種。

  • 切換元素背景圖片位置 background-position
.sprite {
    width: 300px;
    height: 300px;
    background: url(frame.png) no-repeat 0 0;
}

<div class="sprite" id="sprite"></div>

(function(){
    var sprite = document.getElementById("sprite"),
	    picWidth = 300,
	    k = 20,
	    i = 0,
	    timer = null;
    // 重置背景圖片位置
    sprite.style = "background-position: 0 0";
    // 改變背景圖位置
    function changePosition(){
        sprite.style = "background-position: "+(-picWidth*i)+"px 0";
        i++;
        if(i == k){
            i = 0;
        }
    }
    timer = setInterval(changePosition, 50);
})();
  • 移動元素背景圖片位置 transform:translate3d()
.sprite-wp {
   width: 300px;
    height: 300px;
    overflow: hidden;
}
.sprite {
    width: 6000px;
    height: 300px;
    will-change: transform;
    background: url(frame.png) no-repeat center;
}

<div class="sprite-wp">
    <div class="sprite" id="sprite"></div>
</div>

(function () {
    var sprite = document.getElementById("sprite"),
        picWidth = 300,
        k = 20,
        i = 0,
        timer = null;
    // 重置背景圖片位置
    sprite.style = "transform: translate3d(0,0,0)";
    // 改變背景圖移動
    function changePosition() {
        sprite.style = "transform: translate3d(" + (-picWidth * i) + "px,0,0)";
        i++;
        if (i == k) {
            i = 0;
        }
    }
    timer = setInterval(changePosition, 50);
})();

主執行緒和排版執行緒

在現代瀏覽器中,渲染頁面所要負責的執行緒主要有兩個:主執行緒和排版執行緒。

主執行緒

  • 執行 JS
  • 計算 HTML 元素的 CSS 樣式
  • 佈局頁面
  • 把頁面元素繪製成一個或多個位圖
  • 把這些點陣圖移交給排版執行緒

在瀏覽器開始渲染頁面,或者長時間執行某個 JS時,主執行緒會一直在忙碌狀態,此時對於使用者的任何輸入或是操作都不會有所響應。

排版執行緒

  • 通過 GPU 渲染點陣圖,並顯示在螢幕上
  • 向主執行緒請求更新點陣圖的可見部分或即將可見的部分
  • 判斷出當前頁面處於可見的部分
  • 判斷出即將通過頁面滾動而可見的部分
  • 隨著使用者滾動頁面來移動這些部分

排版執行緒對於使用者的操作保持快速的響應,普遍的幀率是每秒 60 幀的速度去渲染重新整理,顯示器是會以一定的頻率來重新整理顯示器,頻率是赫茲(Hz)。

Transtion

下面我們在網頁中實現一個元素的高度變化的動畫,滑鼠懸浮在元素上動畫啟動,直至完成,我們來了解一下瀏覽器的兩個執行緒是如何協同工作的:

<style>
#foo {
  height: 100px;
  width: 100px;
  background: red;
  transition: height 1s linear;
}
#foo:hover {
  height: 200px;
}
</style>

圖中橘黃色部分代表操作相對較慢,消耗較大;藍色部分代表操作相對較快,消耗較小

img

從上圖我們可以看到,瀏覽器的兩個執行緒在來回地切換工作,而且橘黃色出現次數較多,這意味著瀏覽器需要處理相當多的工作。

對於瀏覽器而言,由於元素的高度一直在變化,因此這個動畫的每一幀中,都需要重新佈局 ——> 繪製頁面 ——> 將新的點陣圖載入到 GPU 中 ——> 顯示。而其中載入到 GPU 是一個相對緩慢的操作。

Transform

經過上面的實驗,我們對 transition 屬性有了比較好的瞭解;同時我們對上述動畫效能也有一個瞭解。接著我需要在網頁中實現一個元素的大小變化動畫,滑鼠懸浮在元素上動畫啟動,直至完成:

<style>
#bar {
  height: 100px;
  width: 100px;
  background: red;
  transition: transform 1s linear;
}
#bar:hover {
  transform: scale(2);
}
</style>

img

由此我們可以看到,兩個執行緒來回切換的情況並不多,橘黃色部分出現的次數也較少,藍色部分居絕大部分,這意味著這個動畫效果相較於上面的要流暢很多。

在定義中, transform 是不會使瀏覽器產生重新排版的,因此 transform 不會影響原本的佈局,以及周圍的元素。它會將定義的元素作為一個整體進行縮放、移動或旋轉等。

基於 transform 這類的特性,瀏覽器在渲染頁面時可以節省很多不必要的開支,例如重新佈局和將點陣圖傳給 GPU 等工作,這樣就使得動畫更有效率。

所以,我們在選擇動畫方式時,應該優先選擇 transform 的實現方案。

方案總結

總結以上幾種方案,我們可以看到GIF圖有一定的優點也有缺點,所以這種方式是看情況選擇使用的,選擇符合實際場景的方案就是最好的方案。

同時我們最常用的是CSS3幀動畫,因為通過CSS就可以實現,效果也很好;如果希望新增更靈活豐富的互動就可以採用JS幀動畫的解決方案了。

通過上節的擴充套件閱讀,我們瞭解到, transform 的實現方案,在渲染效能上要優於 background-position 的實現方案,那其他實現方式效能如何呢,我們來比較一下。

測試環境:

系統:Windows 10 專業版
處理器:Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz 3.41GHz
RAM: 8.00GB
瀏覽器:Chrome 67.0

如果測試結果出現偏差,可能與測試環境變化有關。

img

img

這兩個FPS meter截圖是來自CSS3幀動畫,可以看到他們都能達到60FPS的流暢動畫效果,同時後者是改變背景圖位移 transform:translate3d() ,具有GPU加速效果。

img

img

img

這三個FPS meter截圖是來自JS幀動畫,分別是Canvas繪製,改變 background-position ,改變 transform:translate3d() ,可以看到JS幀動畫的FPS都只有20左右,這個數值的FPS會給人感覺一定的卡頓和不舒適感,同時也看到,他們的波線或多或少有一定的不穩定性,這同樣會給人卡頓的感覺,而且不難看出,使用了transform 3D屬性具有GPU加速效果,在裝置上表現相對會好一點。

綜上我們可以對各方案動畫效能簡單排序:

GPU 硬體加速CSS3動畫 > 非硬體加速CSS3動畫 > GPU 硬體加速Javascript 動畫 > 非硬體加速Javascript 動畫

tips:使用 will-change 可以在元素屬性真正發生變化之前提前做好對應準備

注意事項

素材:動畫圖片寬高最好是偶數,總幀數最好是偶數,圖片拼接處最好有一定的留白。

適配:移動端適配最好不用rem,因為rem的計算會造成小數四捨五入,造成一定的抖動效果,建議直接用px作為單位,同時輔助以scale(zoom)媒體查詢進行適配。如果使用rem適配,試試使用 transform 的方案,抖動問題可以得到優化解決。

Facebook Profile photo
Written by Nat
This is the author box. A short description about the author of this article. Could be their website link, what they like to read about and so on. Profile