文章已同步至個人Blog:Benjamin - 專注前端開發和使用者體驗
相關概念:繪製頻率、螢幕重新整理頻率、硬體加速、60fps
繪製頻率:
頁面上每一幀變化都是系統繪製出來的(GPU或者CPU)【參考瀏覽器渲染原理】。但這種繪製又和PC遊戲的繪製不同,它的最高繪製頻率受限於顯示器的重新整理頻率(而非顯示卡),所以大多數情況下最高的繪製頻率只能是每秒60幀(frame per second,以下用fps簡稱),對應於顯示器的60Hz。60fps是一個最理想的狀態,在日常對頁面效能的測試中,60fps也是一個重要的指標,the closer the better。
重新整理頻率: 影象在螢幕上更新的速度,也即螢幕上的影象每秒鐘出現的次數,它的單位是赫茲(Hz)。重新整理頻率越高,螢幕上影象閃爍感就越小,穩定性也就越高,換言之對視力的保護也越好。一般人的眼睛、不容易察覺75Hz以上重新整理頻率帶來的閃爍感,因此最好能將您顯示卡重新整理頻率調到75Hz以上。要注意的是,並不是所有的顯示卡都能夠在最大解析度下達到70Hz以上的重新整理頻率(這個效能取決於顯示卡上RAMDAC的速度),而且顯示器也可能因為頻寬不夠而不能達到要求。影響重新整理率最主要的還是顯示器的頻寬。
顯示器頻寬是顯示器視訊放大器通頻頻寬度的簡稱,指電子槍每秒鐘在螢幕上掃過的最大總畫素數,以MHz(兆赫茲)為單位。 頻寬的值越大,顯示器效能越好。
硬體加速: 硬體有三個處理器,CPU、GPU和APU(不是加速處理器是聲音處理器)。他們通過PCI/AGP/PCIE匯流排交換資料。今天,GPU已經不再侷限於3D圖形處理了,GPU通用計算技術發展已經引起業界不少的關注,事實也證明在浮點運算、平行計算等部分計算方面,GPU可以提供數十倍乃至於上百倍於CPU的效能。
60Hz和60fps是什麼關係
沒有任何關係。fps代表GPU渲染畫面的頻率,Hz代表顯示器重新整理螢幕的頻率。一幅靜態圖片,你可以說這副圖片的fps是0幀/秒,但絕對不能說此時螢幕的重新整理率是0Hz,也就是說重新整理率不隨影象內容的變化而變化。遊戲也好瀏覽器也好,我們談到掉幀,是指GPU渲染畫面頻率降低。比如跌落到30fps甚至20fps,但因為視覺暫留原理,我們看到的畫面仍然是運動和連貫的。
PS: 以下示例在Chrome環境中執行
一、CSS動畫
1.1 Transitions動畫
例項:
<style type="text/css"> .animate { width: 200px; height: 200px; margin: 0 auto; border-radius: 50%; background-color: #f00; line-height: 200px; border-radius: 50%; text-align: center; color: #fff; font-size: 20px; } .animate-transition { transition : transform 2s linear; -moz-transition : -moz-transform 2s linear; -webkit-transition : -webkit-transform 2s linear; -o-transition : -o-transform 2s linear; } .animate-transition:hover { cursor: pointer; transform : rotate(360deg); -moz-transform : rotate(360deg); -webkit-transform : rotate(360deg); -o-transform : rotate(360deg); } </style> <div class="animate animate-transition">Transition Animation</div>
1.2 Keyframes animation
Keyframes animation通過定義多個關鍵幀以及定義每個關鍵幀中的元素的屬性值來實現更為複雜的動畫效果。 例項:
<style type="text/css"> .animate-keyframes { -webkit-animation: frames 2s linear infinite; } .animate-keyframes:hover { cursor: pointer; -webkit-animation: none; } @-webkit-keyframes frames { 0% { background-color: #f00; -webkit-transform: rotate(0deg); } 100% { background-color: #f00; -webkit-transform: rotate(360deg); } } </style> <div class="animate animate-keyframes">keyframes animation</div>
1.3 CSS動畫優缺點
優點: 簡單、高效 宣告式的 不依賴與主執行緒,採用硬體加速(GPU) 簡單的控制keyframe animation 播放和暫停 缺點: 不能動態的修改或定義動畫內容 不同的動畫無法實現同步 多個動畫彼此無法堆疊 另: 1) CSS3 transition強制硬體加速會加大GPU消耗,高負荷情形下將導致執行不流暢。這種情況在移動裝置上尤為明顯。(特殊情況下,比如當資料在瀏覽器主執行緒和排版執行緒之間傳遞產生的瓶頸也會導致不流暢)。某些CSS屬性,比如transform和opacity,則不受這些瓶頸影響。Adobe在這裡精心總結了這些問題。詳細請戳 transition的相容性問題是個詬病,IE10+及現代瀏覽器,使用起來會造成很多不便。 由於transition並不是由JavaScript原生控制(而僅僅是由JavaScript觸發),瀏覽器無法獲知如何與控制這些transition的JavaScript程式碼同步地優化他們。 2) keyframes animation 的動畫曲線會應用到所有變化的屬性上,而且手寫比較複雜的動畫,寫起來就是噩夢。
二、SVG動畫
2.1 例項
<div class="animate-svg"> <svg id="svgAnimation" ns="http://www.w3.org/2000/svg" version="1.1" width="200" height="200"> <g transform="translate(100,100)"> <g> <rect width="200" height="200" rx="100" ry="100" fill="red" transform="translate(-100,-100)"></rect> <text x="-60" y="-0" font-size="20" fill="white" >SVG Animation</text> <!-- Add ease-in-out and infinite iterations to this animation and the code --> <animateTransform attributeName="transform" attributeType="xml" type="rotate" from="0" to="360" dur="3s" repeatCount="indefinite">SVG Animation</animateTransform> </g> </g> </svg> </div>
2.2 SVG動畫優缺點
優點:
1) 向量圖形,不受畫素影響——SVG的這個特性使得它在不同的平臺或者媒體下表現良好,無論螢幕解析度如何
2) SVG對動畫的支援較好,其DOM結構可以被其特定語法或者Javascript控制,從而輕鬆的實現動畫
3) Javascript可以完全控制SVG Dom 元素
4) SVG的結構是XML,其可訪問性(盲文、聲音朗讀等)、可操作性、可程式設計性、可被CSS樣式化完勝Canvas。另外,其支援 ARIA 屬性,使其如虎添翼。
缺點:
1) DOM比正常的圖形慢,而且如果其結點多而雜,就更慢。
2) SVG 畫點報表什麼的,還行;在網頁遊戲前,就束手無策了;當然可以結合 Canvas + SVG實現。
3) 不能動態的修改動畫內容
4) 不能與HTML內容整合
5) 整個SVG作為一個動畫
6) 瀏覽器相容性問題,IE8-以及Android 2.3預設瀏覽器是不支援SVG
三、Javascript動畫
3.1 jQuery動畫
jQuery動畫使用setInterval實現:
// 用例 $("#div").animate({}); // 原始碼 jQuery.fx.timer = function( timer ) { if ( timer() && jQuery.timers.push( timer ) && !timerId ) { timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); } }; jQuery.fx.interval = 13;
How to determine the best “framerate” (setInterval delay) to use in a JavaScript animation loop?
優點: 易用,低效,相容好;
缺點:
1) setInterval多個間隔可能會被跳過
2) setInterval多個間隔可能比預期小
3) 不同瀏覽器的精度量級不同:
4) jQuery 無法解決頻繁觸發 Layout 導致的抽動。
3.2 requestAnimationFrame
例項:
<style type="text/css"> .animate-RAF { } .animate-input { margin-top: 10px; text-align: center; } </style> <div id="animate-RAF" class="animate animate-RAF">RAF Animation</div> <div class="animate-input"><input type="button" id="btn_start" value="Start" style="width:100px;height:30px"></div> <script type="text/javascript"> var animate_raf = document.getElementById("animate-RAF"), btn_start = document.getElementById("btn_start"), frameid = null; function frame(time) { animate_raf.style['-webkit-transform'] = 'rotate('+ Math.cos(time/1000)*360 +'deg)'; frameid = requestAnimationFrame(frame); } // bind Event btn_start.addEventListener("click", function(event) { var val = this.value; if(val === "Start") { frameid = requestAnimationFrame(frame); this.value = "Pause"; }else { this.value = "Start"; cancelAnimationFrame(frameid); } }, false); </script>
優點:
1) 在每次瀏覽器更新頁面時,能獲取通知並執行應用。 簡單理解為,RAF能在每個16.7ms間執行一次我們們的函式,不多不少。
2) 最小化的消耗資源,RAF在頁面被切換或瀏覽器最小化時,會暫停執行,等頁面再次關注時,繼續執行動畫。
3) 相比 CSS 動畫有更好的掌控,能合理降低CPU的使用。
缺點:
1) 無法控制執行時間,執行時間由系統根據螢幕重新整理時間決定
2) 瀏覽器相容性問題,IE10+及現代瀏覽器,低版本瀏覽器建議降級處理,使用setInterval或setTimeout
3.3 Canvas
優點:
1) 畫2D圖形時,頁面渲染效能比較高
2) 頁面渲染效能受圖形複雜度影響小
3) 渲染效能只受圖形的解析度的影響
4) 畫出來的圖形可以直接儲存為 .png 或者 .jpg的圖形
5) 最適合於畫光柵影象(如遊戲和不規則幾何圖形等),編輯圖片還有其他基於畫素的圖形操作。
缺點:
1) 整個就是一張圖,無論你往上畫什麼東西——沒有DOM 結點可供操作
2) 沒有實現動畫的API,你必須依靠定時器和其他事件來更新Canvas
3) 對文字的渲染支援是比較差
4) 對要求有高可訪問性(盲文、聲音朗讀等)頁面,比較困難
5) 對互動要求高的(比如TIBCO的很多產品)的介面,不建議使用Canvas
3.4 WebGL
WebGL是一種3D繪圖示準,這種繪圖技術標準允許把JavaScript和OpenGL ES 2.0結合在一起,通過增加OpenGL ES 2.0的一個JavaScript繫結,WebGL可以為HTML5 Canvas提供硬體3D加速渲染,這樣Web開發人員就可以藉助系統顯示卡來在瀏覽器裡更流暢地展示3D場景和模型了,還能建立複雜的導航和資料視覺化。顯然,WebGL技術標準免去了開發網頁專用渲染外掛的麻煩,可被用於建立具有複雜3D結構的網站頁面,甚至可以用來設計3D網頁遊戲等等。
瀏覽器支援:
Internet Explorer 11+
Google Chrome 9+
Firefox 4+
Opera 12+
Safari 5.1+
3.5 Problems
1) 不同的api有不同的模型動畫
2) 不必要的維護來支援多種方式實現同樣的事情
3) Web開發人員需要學習多種實現技術
4) Javascript不容易設定宣告式動畫
四、Flash動畫(過時)
五、Web Animations 1.0 ——A new general purpose animation model
2) JavaScript implementation of the Web Animations API
Web Animations API為CSS和SVG動畫提供了單一介面。旨在通過提供更好的效能、更好的控制時間線和播放、靈活、統一的Javascript程式設計介面,使做一些事情更容易。
當前狀態:
Specification at First Public Working Draft: www.w3.org/TR/web-animations
Chrome: CSS Transitions & Animations rewritten on top of the Web Animations model JavaScript API in development behind a flag
Firefox & Safari: Started implementation
IE: No public signals
例項一:A simple example
var web_animation_1 = document.getElementById("web_animation_1"); web_animation_1.addEventListener('click', function() { web_animation_1.animate([{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],{ duration: 2 }); }, false);
例項二:More complex timing
var web_animation_2 = document.getElementById("web_animation_2"); web_animation_2.addEventListener('click', function() { web_animation_2.animate([{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],{ direction: 'alternate', duration: 1, iterations: Infinity, easing: 'ease-in-out', playbackRate: 2 }); }, false);
例項三:Without the syntactic sugar
var web_animation_3 = document.getElementById("web_animation_3"); web_animation_3.addEventListener('click', function() { var obj = new Animation(web_animation_3,[{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],{ duration: 2 }); document.timeline.play(obj); }, false);
例項四:Parallel animation grouping
var web_animation_4 = document.getElementById("web_animation_4"), parItem1 = document.getElementById("parItem1"), parItem2 = document.getElementById("parItem2"), parItem3 = document.getElementById("parItem3"); web_animation_4.addEventListener('click', function() { var obj = new ParGroup([ new Animation(parItem1, [{width: '0px'}, {width: '500px'}], 1), new Animation(parItem2, [{width: '0px'}, {width: '700px'}], 1), new Animation(parItem3, [{width: '0px'}, {width: '200px'}], 1), ]) document.timeline.play(obj); }, false);
例項五:Sequential animation grouping
var web_animation_5 = document.getElementById("web_animation_5"), seqItem1 = document.getElementById("seqItem1"), seqItem2 = document.getElementById("seqItem2"), seqItem3 = document.getElementById("seqItem3"); web_animation_5.addEventListener('click', function() { var obj = new SeqGroup([ new Animation(seqItem1, [{width: '0px'}, {width: '200px'}], 1), new Animation(seqItem2, [{width: '0px'}, {width: '300px'}], 1), new Animation(seqItem3, [{width: '0px'}, {width: '200px'}], 1), ]) document.timeline.play(obj); }, false);
例項六:Nested grouped animations
var web_animation_6 = document.getElementById("web_animation_6"), outerSeqItem1 = document.getElementById("outerSeqItem1"), outerSeqItem2 = document.getElementById("outerSeqItem2"), innerParItem1 = document.getElementById("innerParItem1"), innerParItem2 = document.getElementById("innerParItem2"), innerParItem3 = document.getElementById("innerParItem3"); web_animation_6.addEventListener('click', function() { var parobj = new ParGroup([ new Animation(innerParItem1, [{width: '0px'}, {width: '300px'}], 1), new Animation(innerParItem2, [{width: '0px'}, {width: '300px'}], 1), new Animation(innerParItem3, [{width: '0px'}, {width: '300px'}], 1), ]); var seqobj = new SeqGroup([ new Animation(outerSeqItem1, [{width: '0px'}, {width: '200px'}], 1), parobj, new Animation(outerSeqItem2, [{width: '0px'}, {width: '200px'}], 1), ]); document.timeline.play(seqobj); }, false);
例項七:Path animations
var web_animation_7 = document.getElementById("web_animation_7"); web_animation_7.addEventListener('click', function() { var obj = new Animation(web_animation_7, new PathAnimationEffect( 'M 100 200 ' + 'C 200 100 300 0 400 100 ' + 'C 500 200 600 300 700 200 ' + 'C 800 100 900 100 900 100', 'auto-rotate'), { duration: 2, direction: 'alternate', easing: 'ease-in-out', iterations: Infinity, }); document.timeline.play(obj); }, false);
例項八:Custom animation effects
function customAnimationEffect(timeFraction, iteration, target) { web_animation_8.innerHTML = 'timeFraction: ' + timeFraction.toFixed(2) + '\n' + 'iteration: ' + iteration; } var web_animation_8 = document.getElementById("web_animation_8"); var obj = new Animation(null, {sample: customAnimationEffect}, { duration: 2, direction: 'alternate', easing: 'ease-in-out', iterations: Infinity, }); var customPlayer = document.timeline.play(obj); window.addEventListener('slideenter', function(event) { if (event.slide == customSlide) { customPlayer.currentTime = 0; } }, false);
例項九:綜合例項
(function(document) { 'use strict'; var Animations = {}, player, controls = document.getElementById('animate-controls'); Animations.targets = { path: document.getElementById('path'), ballContainer: document.getElementById('animate-ball-container'), ball: document.getElementById('animate-ball') }; Animations.keyframeMove = new Animation(Animations.targets.ballContainer, [{ offset: 0, transform: 'translate(0,0)' }, { offset: 1, transform: 'translate(600,0)' }], { duration: 2000 }); Animations.keyframeSpinRoll = new Animation(Animations.targets.ball, [{ transform: 'rotate(950deg)' }], { duration: 2000 }); Animations.motionpathBounce = new Animation(Animations.targets.ballContainer, new MotionPathEffect("M25,25 " + "a150,100 0 0,1 300,0 " + "a75,50 0 0,1 150,0 " + "a35,20 0 0,1 70,0 " + "a2,1 0 0,1 35,0 " + "h45"), { duration: 2500 }); Animations.keyframeSpinBounce = new Animation(Animations.targets.ball, [{ transform: 'rotate(950deg)' }], { duration: 2500 }); Animations.animationGroupRoll = new AnimationGroup([Animations.keyframeMove, Animations.keyframeSpinRoll], { easing: 'ease-out' }); Animations.animationGroupBounce = new AnimationGroup([Animations.motionpathBounce, Animations.keyframeSpinBounce], { easing: 'ease-out' }); controls.addEventListener('click', function(event) { if (event.target) { var targetElement = event.target; switch (targetElement.id) { case 'keyframe-start': player = document.timeline.play(Animations.animationGroupRoll); break; case 'motionpath-start': player = document.timeline.play(Animations.animationGroupBounce); break; case 'pause': player.pause(); break; case 'cancel': player.cancel(); break; case 'play': player.play(); break; case 'reverse': player.reverse() } } }) })(document);
六、現行相容性方案
1) 頁面增強動畫建議使用CSS動畫
2) 複雜動畫互動建議使用RAF及setInterval 或setTimeout優雅降級處理
3) 推薦動畫庫Velocity.js、GreenSock:
Velocity.js是一款動畫切換的jQuery外掛,它重新實現了jQuery的$.animate()方法從而加快動畫切換的速度。Velocity.js只有7k的大小,它不僅包含了$.animate()的所有功能,並且還包含了顏色切換、轉換(transform)、迴圈、緩動、CSS切換、Scroll功能,它是jQuery、 jQuery UI、CSS變換 在動畫方面的最佳組合。
Velocity.js支援IE8+、Chrome、Firefox等瀏覽器,並支援Andriod以及IOS。
Velocity.js在內部實現中使用了jQuery的$.queue()方法,因此它比 jQuery的$.animate()、$.fade()、$.delay()方法更加流暢,其效能也高於CSS的animation屬性。
GreenSock:GSAP v12平臺
非常快的速度:效能是非常重要的,尤其是在移動裝置上。GSAP不斷優化,以保證互動專案的快速響應、高效率及平滑,你可以從這裡檢視動畫效果測試。
異想天開的強勁:內建眾多引擎的功能,如動畫色彩、貝塞爾曲線、CSS樣式屬性、Flash濾鏡、陣列等等,定義不同的回撥,可以通過幀或者秒定義運動。
相容性:Flash,HTML5,jQuery,Canvas,CSS,新瀏覽器,舊瀏覽器,RequireJS,EaseIJS,移動裝置等等-GSAP都可以很好的與他們相容,你可以選擇你熟悉的工具來使用。
Javascript,AS3/AS2:選擇適合你的語言來完成動畫。
輕量與可擴充套件性:模組化與外掛式的結構保持了核心引擎的輕量,TweenLite包非常小(基本上低於7kb)。
沒有依賴:GSAP沒有基於第三方工具來構建(雖然它將jQuery作為選擇器),因此能保證最短的載入時間與最大化效能。
高等序列:不用受限於線性序列,可以重疊動畫序列,你可以通過精確時間控制,靈活地使用最少的程式碼實現動畫。
良好的技術支援:可以通過論壇反饋,會有專家和資深活躍使用者回答問題。
任何物件都可以實現動畫:是的,任何,不用預定義的屬性,任何物件的任意數字屬性都可以實現動畫,如果這些屬性(如顏色,濾鏡,非數值屬性等)需要處理,外掛可以實現。如果沒有,我們可以實現一個。
重寫管理:GSAP幫助防止動畫引擎的衝突以及高階選項的設定。 易於學習:文件、教程、 示例、學習指南、論壇,還有很多學習資源,非常地豐富。
許可證:除商業用途意外,GSAP完全免費。
參考連結:
CSS Will Change Module Level 1
CSS Will Change Module Level 1 (中文版本)
CSS vs. JS Animation: Which is Faster?
CSS animations and transitions performance: looking inside the browser
SVG or Canvas? СHoosing Between the Two
Exploring the Web Animations API
Everything You Need to Know About the CSS will-change Property