requestAnimationFrame筆記總結
一.什麼是requestAnimationFrame
首先看張圖:
相當一部分的瀏覽器的顯示頻率是16.5ms,就是上圖第一行的節奏,表現就是「我和你一步兩步三步四步往前走……」。如果我們火力搞猛一點,例如搞個10ms setTimeout,就會是下面一行的模樣——每第三個圖形都無法繪製(紅色箭頭指示),表現就是「我和你一步兩步 坑 四步往前走……」。
什麼意思呢?
可以這樣理解:國慶北京高速,最多每16.7通過一輛車,結果,突然插入一批setTimeout的軍車,強行要10s通過。顯然,這是超負荷的,要想順利進行,只能讓第三輛車直接消失(正如顯示繪製第三幀的丟失)。然,這是不現實的,於是就有了會堵車!
同樣的,顯示器16.7刷新間隔之前發生了其他繪製請求(setTimeout),導致所有第三幀丟失,繼而導致動畫斷續顯示(堵車的感覺),這就是過度繪製帶來的問題。不僅如此,這種計時器頻率的降低也會對電池使用壽命造成負面影響,並會降低其他應用的性能。
這也是為何setTimeout的定時器推薦最小使用16.7ms的原因(16.7 = 1000/60,每秒60幀)。
還不明白,我們再來看:
假設屏幕是每隔16.7ms刷新一次,而setTimeout每隔10ms設置圖像向左移動1px,就會出現如下的繪製過程:
第0ms: 屏幕未刷新,等待中,setTimeout也未執行,等待中;
第10ms:屏幕未刷新,等待中,setTimeout開始執行並設置圖像屬性left=1px;
第16.7ms:屏幕刷新,屏幕上的圖像向左移動了1px,setTimeout未執行,繼續等待中;
第20ms:屏幕為刷新,等待中,setTimeout開始執行並設置left=2px;
第30ms: 屏幕未刷新,等待中,setTimeout開始執行並設置left=3px;
第33.4ms: 屏幕開始刷新,屏幕上的圖像向左移動了3px, setTimeout未執行,繼續等待中;
。。。
可以看到,屏幕並沒有展示left=2px的那一幀畫面,圖像直接從1px的位置跳到了3px的位置,這就是丟幀的現象,這種現象就會引起動畫卡頓。
而requestAnimationFrame就是為了解決上述問題出現的:該介面以瀏覽器的顯示頻率來作為其動畫動作的頻率,比如瀏覽器每10ms刷新一次,動畫回調也每10ms調用一次,這樣就不會存在過度繪製的問題,動畫不會掉幀,自然流暢。
二.requestAnimationFrame的使用
1. 首先,獲取requestAnimationFrame介面,由於該介面存在兼容性問題,故而需要做兼容處理:
簡單兼容可以這樣做:
window.requestAnimationFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000/60); }})();
但是還是存在問題,並不是所有的設備的繪製時間間隔是1000/60ms,以及上面並沒有cancel相關方法,所以,就有了下面這份更全面的兼容方法:
(function() { var lastTime = 0; var vendors = [webkit, moz]; //如果window.requestAnimationFrame為undefined先嘗試瀏覽器前綴是否兼容 for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x] + RequestAnimationFrame]; window.cancelAnimationFrame = window[vendors[x] + CancelAnimationFrame] ||//webkit中此取消方法的名字變了 window[vendors[x] + CancelRequestAnimationFrame]; } //如果仍然不兼容,則使用setTimeOut進行兼容操作 if(!window.requestAnimationFrame) { window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; } } if(!window.cancelAnimationFrame) { window.cancelAnimationFrame = function(id) { clearTimeout(id); } }})();
上述的代碼是由Opera瀏覽器的技術師Erik M?ller設計的,使得更好得兼容各種瀏覽器,但基本上他的代碼就是判斷使用4ms還是16ms的延遲,來最佳匹配60fps。
2.貝塞爾曲線
在具體進行動畫效果之前,我們先來了解一下貝塞爾曲線,下面的例子中會用到這個。
相信很多同學都知道「貝塞爾曲線」這個詞,我們在很多地方都能經常看到。但是,可能並不是每位同學都清楚地知道,到底什麼是「貝塞爾曲線」,又是什麼特點讓它有這麼高的知名度。
貝塞爾曲線的數學基礎是早在 1912 年就廣為人知的伯恩斯坦多項式。但直到 1959 年,當時就職於雪鐵龍的法國數學家 Paul de Casteljau 才開始對它進行圖形化應用的嘗試,並提出了一種數值穩定的 de Casteljau 演算法。然而貝塞爾曲線的得名,卻是由於 1962 年另一位就職於雷諾的法國工程師 Pierre Bézier 的廣泛宣傳。他使用這種只需要很少的控制點就能夠生成複雜平滑曲線的方法,來輔助汽車車體的工業設計。
正是因為控制簡便卻具有極強的描述能力,貝塞爾曲線在工業設計領域迅速得到了廣泛的應用。不僅如此,在計算機圖形學領域,尤其是矢量圖形學,貝塞爾曲線也佔有重要的地位。今天我們最常見的一些矢量繪圖軟體,如 Flash、Illustrator、CorelDraw 等,無一例外都提供了繪製貝塞爾曲線的功能。甚至像 Photoshop 這樣的點陣圖編輯軟體,也把貝塞爾曲線作為僅有的矢量繪製工具(鋼筆工具)包含其中。
貝塞爾曲線在 web 開發領域同樣佔有一席之地。CSS3 新增了 transition-timing-function 屬性,它的取值就可以設置為一個三次貝塞爾曲線方程。在此之前,也有不少 JavaScript 動畫庫使用貝塞爾曲線來實現美觀逼真的緩動效果。
下面我們就通過例子來了解一下如何用 de Casteljau 演算法繪製一條貝塞爾曲線。
在平面內任選 3 個不共線的點,依次用線段連接。
在第一條線段上任選一個點 D。計算該點到線段起點的距離 AD,與該線段總長 AB 的比例。
根據上一步得到的比例,從第二條線段上找出對應的點 E,使得 AD:AB= BE:BC。
連接這兩點 DE。
從新的線段 DE 上再次找出相同比例的點 F,使得 DF:DE= AD:AB= BE:BC。
到這裡,我們就確定了貝塞爾曲線上的一個點 F。接下來,請稍微回想一下中學所學的極限知識,讓選取的點 D 在第一條線段上從起點 A 移動到終點 B,找出所有的貝塞爾曲線上的點 F。所有的點找出來之後,我們也得到了這條貝塞爾曲線。
你實在想像不出這個過程,沒關係,看動畫!
回過頭來看這條貝塞爾曲線,為了確定曲線上的一個點,需要進行兩輪取點的操作,因此我們稱得到的貝塞爾曲線為二次曲線(這樣記憶很直觀,但曲線的次數其實是由前面提到的伯恩斯坦多項式決定的)。
當控制點個數為 4 時,情況是怎樣的?
步驟都是相同的,只不過我們每確定一個貝塞爾曲線上的點,要進行三輪取點操作。如圖,AE:AB= BF:BC= CG:CD= EH:EF= FI:FG= HJ:HI,其中點 J 就是最終得到的貝塞爾曲線上的一個點。
這樣我們得到的是一條三次貝塞爾曲線。
看過了二次和三次曲線,更高次的貝塞爾曲線大家應該也知道要怎麼畫了吧。那麼比二次曲線更簡單的一次(線性)貝塞爾曲線存在嗎?長什麼樣?根據前面的介紹,只要稍作思考,想必你也能猜出來了。哈!就是一條直線~
能畫曲線也能畫直線,是不是很厲害?要繪製更複雜的曲線,控制點的增加也僅僅是線性的。這一特點使其不光在工業設計領域大展拳腳,就連數學基礎不好的人也可以比較容易地掌握,比如大多數平面美術設計師們。
上面介紹的內容並不足以展示貝塞爾曲線的真正威力。推廣到三維空間的貝塞爾曲面,以及更進一步的非均勻有理 B 樣條(NURBS),早已成為當今計算機輔助設計(CAD)的行業標準,不論是我們平常用到的各種產品,還是在電影院看到的精彩大片,都少不了它們的功勞。
通過上面的貝塞爾曲線,我們能夠很容易模擬動畫的快慢。
Web中有很多領域都用到了貝塞爾曲線:CSS3動畫、svg、canvas,下面我們再來看一張圖:
看看有點嚇人吧。。。
別怕,先來了解一下Tween:
- Linear:無緩動效果
- Quadratic:二次方的緩動(t^2)
- Cubic:三次方的緩動(t^3)
- Quartic:四次方的緩動(t^4)
- Quintic:五次方的緩動(t^5)
- Sinusoidal:正弦曲線的緩動(sin(t))
- Exponential:指數曲線的緩動(2^t)
- Circular:圓形曲線的緩動(sqrt(1-t^2))
- Elastic:指數衰減的正弦曲線緩動
- 超過範圍的三次方緩動((s+1)*t^3 – s*t^2)
- 指數衰減的反彈緩動
每個效果都分三個緩動方式,分別是(可採用後面的邪惡記憶法幫助記憶):
- easeIn:從0開始加速的緩動,想像OOXX進去,探路要花時間,因此肯定是先慢後快的;
- easeOut:減速到0的緩動,想像OOXX出來,肯定定先快後慢的,以防掉出來;
- easeInOut:前半段從0開始加速,後半段減速到0的緩動,想像OOXX進進出出,先慢後快然後再慢。
每周動畫效果都有其自身的演算法。我們都知道jQuery UI中就有緩動,As腳本也內置了緩動,其中的運動演算法都是一致的,下面是相應的演算法(之後例子中用到的tween.js文件):
/* * Tween.js * t: current time(當前時間) * b: beginning value(初始值) * c: change in value(變化量) * d: duration(持續時間)*/var Tween = { Linear: function(t, b, c, d) { return c*t/d + b; }, Quad: { easeIn: function(t, b, c, d) { return c * (t /= d) * t + b; }, easeOut: function(t, b, c, d) { return -c *(t /= d)*(t-2) + b; }, easeInOut: function(t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t + b; return -c / 2 * ((--t) * (t-2) - 1) + b; } }, Cubic: { easeIn: function(t, b, c, d) { return c * (t /= d) * t * t + b; }, easeOut: function(t, b, c, d) { return c * ((t = t/d - 1) * t * t + 1) + b; }, easeInOut: function(t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t*t + b; return c / 2*((t -= 2) * t * t + 2) + b; } }, Quart: { easeIn: function(t, b, c, d) { return c * (t /= d) * t * t*t + b; }, easeOut: function(t, b, c, d) { return -c * ((t = t/d - 1) * t * t*t - 1) + b; }, easeInOut: function(t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b; return -c / 2 * ((t -= 2) * t * t*t - 2) + b; } }, Quint: { easeIn: function(t, b, c, d) { return c * (t /= d) * t * t * t * t + b; }, easeOut: function(t, b, c, d) { return c * ((t = t/d - 1) * t * t * t * t + 1) + b; }, easeInOut: function(t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b; return c / 2*((t -= 2) * t * t * t * t + 2) + b; } }, Sine: { easeIn: function(t, b, c, d) { return -c * Math.cos(t/d * (Math.PI/2)) + c + b; }, easeOut: function(t, b, c, d) { return c * Math.sin(t/d * (Math.PI/2)) + b; }, easeInOut: function(t, b, c, d) { return -c / 2 * (Math.cos(Math.PI * t/d) - 1) + b; } }, Expo: { easeIn: function(t, b, c, d) { return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; }, easeOut: function(t, b, c, d) { return (t==d) ? b + c : c * (-Math.pow(2, -10 * t/d) + 1) + b; }, easeInOut: function(t, b, c, d) { if (t==0) return b; if (t==d) return b+c; if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b; return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; } }, Circ: { easeIn: function(t, b, c, d) { return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; }, easeOut: function(t, b, c, d) { return c * Math.sqrt(1 - (t = t/d - 1) * t) + b; }, easeInOut: function(t, b, c, d) { if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; } }, Elastic: { easeIn: function(t, b, c, d, a, p) { var s; if (t==0) return b; if ((t /= d) == 1) return b + c; if (typeof p == "undefined") p = d * .3; if (!a || a < Math.abs(c)) { s = p / 4; a = c; } else { s = p / (2 * Math.PI) * Math.asin(c / a); } return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; }, easeOut: function(t, b, c, d, a, p) { var s; if (t==0) return b; if ((t /= d) == 1) return b + c; if (typeof p == "undefined") p = d * .3; if (!a || a < Math.abs(c)) { a = c; s = p / 4; } else { s = p/(2*Math.PI) * Math.asin(c/a); } return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b); }, easeInOut: function(t, b, c, d, a, p) { var s; if (t==0) return b; if ((t /= d / 2) == 2) return b+c; if (typeof p == "undefined") p = d * (.3 * 1.5); if (!a || a < Math.abs(c)) { a = c; s = p / 4; } else { s = p / (2 *Math.PI) * Math.asin(c / a); } if (t < 1) return -.5 * (a * Math.pow(2, 10* (t -=1 )) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p ) * .5 + c + b; } }, Back: { easeIn: function(t, b, c, d, s) { if (typeof s == "undefined") s = 1.70158; return c * (t /= d) * t * ((s + 1) * t - s) + b; }, easeOut: function(t, b, c, d, s) { if (typeof s == "undefined") s = 1.70158; return c * ((t = t/d - 1) * t * ((s + 1) * t + s) + 1) + b; }, easeInOut: function(t, b, c, d, s) { if (typeof s == "undefined") s = 1.70158; if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; return c / 2*((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; } }, Bounce: { easeIn: function(t, b, c, d) { return c - Tween.Bounce.easeOut(d-t, 0, c, d) + b; }, easeOut: function(t, b, c, d) { if ((t /= d) < (1 / 2.75)) { return c * (7.5625 * t * t) + b; } else if (t < (2 / 2.75)) { return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b; } else if (t < (2.5 / 2.75)) { return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b; } else { return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b; } }, easeInOut: function(t, b, c, d) { if (t < d / 2) { return Tween.Bounce.easeIn(t * 2, 0, c, d) * .5 + b; } else { return Tween.Bounce.easeOut(t * 2 - d, 0, c, d) * .5 + c * .5 + b; } } }}Math.tween = Tween;
我們可以通過調用上面的方法獲得動畫中某個變化的參數,以模擬動畫的演變。
3.接下來,我們就可以去使用它了,以下是一個彈性球的例子:
demo地址:
yaodebian/ballBounce準備好以上這些目錄:
index.html:
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width_= , initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>ballBounce</title> <link rel="stylesheet" href="index.css"> <script src="jquery.min.js"></script> <script src="requestAnimation.js"></script> <script src="tween.js"></script> <script src="index.js"></script></head> <body> <div id="main"> <h1 class="headerTit">requestAnimationFrame實現皮球落地的緩動效果實例頁面</h1> <div class="demo"> <h4 class="demoTit">向上拖動小球然後放下</h4> <ball></ball> <shadow></shadow> </div> </div></body> </html>
index.css:
.headerTit { text-align: center;}.demo { height: 450px; position: relative;}.demoTit { text-align: center; font-size: 110%;}ball { width: 100px; height: 100px; border-radius: 100px; position: absolute; left: 50%; top: 335px; margin-left: -50px; background-color: #34538b; background-image: -webkit-radial-gradient(100px 100px at 50px 20px, #a0b3d6, #34538b); background-image: -moz-radial-gradient(100px 100px at 50px 20px, #a0b3d6, #34538b); background-image: radial-gradient(100px 100px at 50px 20px, #a0b3d6, #34538b); cursor: pointer; z-index: 1;}shadow { position: absolute; width: 100px; height: 30px; position: absolute; left: 50%; bottom: 5px; margin-left: -50px; background-image: -webkit-radial-gradient(ellipse closest-side, rgba(0,0,0,.75), rgba(0,0,0,0)); background-image: -moz-radial-gradient(ellipse closest-side, rgba(0,0,0,.75), rgba(0,0,0,0)); background-image: radial-gradient(ellipse closest-side, rgba(0,0,0,.75), rgba(0,0,0,0));}
requestAnimation.js:即剛剛我們做的requestAnimationFrame兼容處理
tween.js:上面貝塞爾曲線中涉及到的tween.js
截圖如下:
三.RequestAnimationFrame相對於setTimeout的優點
1. 使得動畫更加流暢,防止動畫失幀情況;
2. Cpu節能:
使用setTimeout實現的動畫,當頁面被隱藏或最小化時,setTimeout 仍然在後台執行動畫任務,由於此時頁面處於不可見或不可用狀態,刷新動畫是沒有意義的,完全是浪費CPU資源。而requestAnimationFrame則完全不同,當頁面處理未激活的狀態下,該頁面的屏幕刷新任務也會被系統暫停,因此跟著系統步伐走的requestAnimationFrame也會停止渲染,當頁面被激活時,動畫就從上次停留的地方繼續執行,有效節省了CPU開銷。
3.函數節流:在高頻率事件(resize,scroll等)中,為了防止在一個刷新間隔內發生多次函數執行,使用requestAnimationFrame可保證每個刷新間隔內,函數只被執行一次(這個可以自己測試一下,在每次回調函數中列印出當前的毫秒數:getTime()),這樣既能保證流暢性,也能更好的節省函數執行的開銷。一個刷新間隔內函數執行多次是沒有意義的,因為顯示器每16.7ms刷新一次,多次繪製並不會在屏幕上體現出來。
參考文章:
CSS3動畫那麼強,requestAnimationFrame還有毛線用? " 張鑫旭-鑫空間-鑫生活如何使用Tween.js各類原生動畫運動緩動演算法 " 張鑫旭-鑫空間-鑫生活深入理解 requestAnimationFrame - WEB前端 - 伯樂在線HTML5探秘:用requestAnimationFrame優化Web動畫 - WEB駭客貝塞爾曲線掃盲 - CSDN博客
推薦閱讀:
※雅思寫作筆記——(二)prediction題型 例1-2
※雅思寫作筆記——(四)influence題型 例3-1
※新概念第二冊第一節錯題集
※elasticsearch安裝筆記
※R上課的筆記