動畫現在基本是web站點必備項,各大JS框架都封裝瞭相應的代碼。QWrap也內置瞭一個實現,支持常見的動畫效果和算子及隊列播放,這裡有示例。JS動畫的實現有多種方式,各自有哪些區別,QWrap使用的哪一種?本文閑聊這個話題。
一、使用瀏覽器的定時器(setInterval/setTimeout)。這種動畫實現方式很早就被大傢廣泛使用,原理簡單,兼容性好。最核心的原理就是利用定時器,在一定時間內(duration),以一定的間隔(frameTime)執行動畫函數(callback)。在動畫函數裡可以改變元素大小、位置(css屬性值);可以改變元素滾動條位置(scrollTop/scrollLeft);甚至用來改變文本內容(倒計時)。
動畫間隔決定瞭動畫的每秒幀數(FPS),一般的,FPS越高,動畫就表現得越流暢,FPS偏低,動畫就會不流暢、卡頓。JQuery中,動畫間隔默認為13ms,也就是說理想狀態下,動畫的每秒幀數是70多。實際上,由於JS定時器精度問題,間隔不可能太小;在計算機資源占用比較大時,這個間隔也沒辦法得到保證;更為嚴重的是,新一點的瀏覽器在頁面不可見時(例如切換到其他tab,瀏覽器被最小化),會自動提高定時器執行間隔,firefox5開始,setInterval的間隔在瀏覽器最小化之後至少被提高到1000ms。
動畫時長 = 播放總幀數 * 幀間隔平均值。由於幀間隔不可控,可能被提高到1000ms甚至更高,那麼實現動畫時面臨兩個選擇:要保證播放總幀數,動畫時長就會增加;要保證動畫時長,就必須犧牲掉總幀數。實際上我們一般采用第二種方式,也就是丟幀保時的策略來實現動畫,來看一個簡單的例子:
<script>
var timerId, startTime, frameTime = 13, dur = 3 * 1000;
function animFun(time) {
var per = Math.min(1.0, (new Date – startTime) / dur);
if(per >= 1) {
clearTimeout(timerId);
} else {
document.getElementById("animated").style.left = Math.round(500 * per) + "px";
}
}
function start() {
startTime = new Date;
timerId = setInterval(animFun, frameTime);
}
</script>
<p id="animated" onclick="start()" style="position: absolute; left: 0px; padding: 50px;background: crimson; color: white">Click Me</p>
在動畫開始時記錄一個初始時間,動畫函數裡用當前時間減去初始時間,得到的時間差除以總時長,可以得到動畫執行進度(per)。再根據per去改變元素的css屬性值,就實現瞭一個最簡單的動畫。
上面例子中,方塊運動是勻速的,平淡無奇。想要運動軌跡更有趣,就需要引入動畫算子。動畫算子是一個函數,把動畫進度per轉換為另外一個值,在上面例子基礎上改進下:
<script>
var timerId, startTime, frameTime = 13, dur = 3 * 1000;
function bounceOut(p) {
if (p < (1 / 2.75)) {
return (7.5625 * p * p);
} else if (p < (2 / 2.75)) {
return (7.5625 * (p -= (1.5 / 2.75)) * p + 0.75);
} else if (p < (2.5 / 2.75)) {
return (7.5625 * (p -= (2.25 / 2.75)) * p + 0.9375);
}
return (7.5625 * (p -= (2.625 / 2.75)) * p + 0.984375);
}
function animFun(time) {
var per = Math.min(1.0, (new Date – startTime) / dur);
if(per >= 1) {
clearTimeout(timerId);
} else {
document.getElementById("animated").style.left = Math.round(500 * bounceOut(per)) + "px";
}
}
function start() {
startTime = new Date;
timerId = setInterval(animFun, frameTime);
}
</script>
<p id="animated" onclick="start()" style="position: absolute; left: 0px; padding: 50px;background: crimson; color: white">Click Me</p>
這下方塊運動就有趣多瞭,上面例子中的bounceOut就是一個算子。有些動畫組件算子可能會接受更多參數(運動距離、時間等),但是QWrap中,算子都隻需要傳per。這裡有QWrap內置算子的演示。
二、W3C有一份WindowAnimationTiming interface規范,也可以用來實現動畫。它的核心方法是requestAnimactionFrame和cancelRequestAnimationFrame。各大瀏覽器新版都有實現,這部分內容我之前介紹過,不瞭解的同學可以點過去看,看完記得再回來。
可以看到,W3C這份規范提供的動畫沒有幀間隔時間這個概念,也就是何時觸發下一幀完全由瀏覽器控制。其它方面跟setInterval動畫幾乎一樣,上面的算子也可以直接拿來用。下面說下我遇到的幾個坑:
首先是firefox:在11之前的某個版本開始,firefox實現瞭mozRequestAnimationFrame,卻沒有提供對應的mozCancelRequestAnimationFrame,那時網上有文章會提到通過“註冊、移除moz私有的beforepaint事件”來模擬這個事件。坑爹的是,firefox11開始有瞭mozCancelRequestAnimationFrame,但老方案直接拋異常。
webkit下也有坑:某個詭異的webKit版本下,webkitRequestAnimationFrame沒有給回調函數傳time參數,更神奇的是一些webkit居然傳遞錯誤格式的time。為此,在webkit下我們通常不用參數裡的time,改為自己new Date。
綜上,由於各瀏覽器對標準實現的不一致和bug,最終我們在QWrap中並沒有使用基於原生動畫函數的版本。不過也可以像下面這樣變通使用,來繞過那些坑:
<script>
var timerId, startTime, dur = 3 * 1000,
requestAnimationFrame = window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
window.oRequestAnimationFrame;
if(!requestAnimationFrame) { alert('你的瀏覽器不支持requestAnimationFrame!'); }
function animFun(time) {
if(!timerId) return false;
var per = Math.min(1.0, (new Date – startTime) / dur);
if(per >= 1) {
timerId = null;
} else {
document.getElementById("animated").style.left = Math.round(500 * per) + "px";
requestAnimationFrame(animFun);
}
}
function start() {
startTime = new Date;
timerId = 1;
requestAnimationFrame(animFun);
}
</script>
<p id="animated" onclick="start()" style="position: absolute; left: 0px; padding: 50px;background: crimson; color: white">Click Me</p>
三、CSS3動畫。css3中的Transition可以用來平滑改變css屬性值。簡而言之,給元素設置下面這樣的css樣式後,再改變transition-property指定屬性的值,瀏覽器會自動處理剩下的事情:
transition-property :* //需要動畫的css屬性,如height,all表示全部屬性
transition-duration:*//持續時間,duration,單位s
transition-delay:* //延遲時間,單位s
transition-timing-function:*//動畫算子,有ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier
通常,我們在css裡寫好transition規則,再改變元素class就可以擁有css3動畫瞭。但是為瞭使用更方便,我們也可以把這個過程封裝成為js組件。監聽動畫執行結束,也需要依賴js。
具體的代碼就不貼瞭,後面例子裡都有,這裡說一些需要關註的地方。本來,檢測瀏覽器是否支持css3動畫可以通過創建臨時元素,判斷'transitionProperty' in el.style是否為true就可以瞭;但是,萬惡的瀏覽器廠商前綴(就是那些-webkit-、 -moz-、-o-、-ms-),使這個過程變得復雜多瞭。最坑爹的是IE,css裡可以用“-ms-transition-property”,js中對應的style,卻是“el.style.msTransitionProperty”。QWrap在設置css的函數中,會先調用StringH.camelize函數處理屬性名,會把“-ms-xxx-ooo”變成“MsXxxOoo”,IE不認!其他瀏覽器則無論首字母是否大小都支持。
要監聽動畫結束,webkit需要給動畫元素綁定webkitTransitionEnd事件,firefox是transitionend,opera是oTransitionEnd,IE是MSTransitionEnd。firefox支持標準事件名,其他瀏覽器都是“小寫前綴+TransitionEnd”,唯獨IE的ms全都要大寫!
好在,隨著瀏覽器的發展,大傢逐漸開始支持標準寫法,慢慢不再需要寫前綴瞭。我這邊測試,最新的IE10 for Win7 Pre Release版也支持瞭標準寫法,所以js需要優先嘗試無前綴的用法。詳細的情況可以去CanIUse看。
— 分割線 —
幾種動畫實現都大概介紹瞭下,都可以封裝成JS組件,有一致的接口方便調用。那麼實際項目中該如何選擇呢?
從瀏覽器支持度來看,除瞭第一種方案,其餘方案都是沒辦法在低版本IE使用的。如果需要支持更多瀏覽器,可以采用方案一(QWrap);或者優先使用方案二,方案一作為降級(JQuery)。如果是面向移動平臺,可以使用方案三(Zepto)。
移動平臺上能用css3動畫就不要用requestAnimationFrame,因為iOS6才開始支持它。使用css3的transition+transform,渲染效率比動畫callback裡改變css屬性值高很多。另外,iOS上移動或縮放頁面時,操作的是touch開始時的截屏,頁面是靜止不動的。那麼松開手指,隻有css3動畫能連續播放,其他兩種方案因為都是根據時間差計算進度,會明顯的跳躍到某幀。
css3動畫有一些功能上的缺失,它不支持在播放每一幀時觸發callback,也就是沒辦法監控播放進度,也沒辦法暫停和恢復動畫。但是css3動畫組件可以把Transform封裝進去,提供一些很贊的新功能:DEMO。更復雜的動畫,或者有暫停播放的需求,通過CSS3 Animation可以實現,不過我們暫時很少用到,所以沒做封裝。
而setInterval和requestAnimationFrame實現的動畫,在改變css屬性時需要額外做更多事情,例如backgroundColor需要轉化為R、G、B三個數值來分別變換;其它有單位的屬性值,如height:100px,也需要轉化為{value:100, unit:'px'}這樣的形式,再對value進行變換。但是,由於每個屬性都可以應用不同的算子,可以組合出獨特的運動軌跡,DEMO。
前面說到在頁面不可見時,瀏覽器會自動提高setInterval的間隔。那對於requestAnimationFrame和css3動畫,瀏覽器會怎麼處理呢?
(chrome23)
(firefox16)
這個測試頁面引入瞭三個iframe,對應本文三種動畫實現,duration都是2s。16s的時間內,動畫應該執行8個周期。頁面處於不可見時:chrome下,用requestAnimationFrame實現的動畫完全被停止播放;其他動畫執行周期都有一定程度下降。測試地址在這裡,大傢可以自己試試~