Zach郵件跟我說,上Stack Overflow這類的論壇,他經常碰到一些關於JavaScript控制CSS 動畫的問題,又提供給我幾個例子。我很久就打算寫一些關於這方面的文章,所以很高興讓Zach提出來並促使我寫了這個教程。
有時候WEB開發人員認為CSS的動畫比JavaScript的動畫更難理解。雖然CSS動畫有其局限性,但它的性能比大多數JavaScript庫更加高效,因為它可以借助硬件加速啊!其效果絕對可以超出我們的預期。
CSS animations和transitions再加上點JavaScript就可以實現硬件加速動畫,而且其交互效果比大多數JavaScript庫更高效。
So,讓我們快點開始吧!小伙伴們都等不及了!
注意:Animations(動畫)和Transitions(過渡)是不同的
CSS Transitions(過渡)被應用於元素指定的屬性變化時,該屬性經過一段時間逐漸的過渡到最終需要的值;而
CSS Animations(動畫)只是在應用時執行之前定義好的操作,它提供更細粒度的控制。
在這篇文章中,我們將分別針對上述內容進行講解。
控制CSS Transition(過渡)
在編程論壇中,關於transition(過渡)的觸發和暫停有無數的疑問。使用JavaScript可以很容易的解決這些疑問。
觸發元素的transiton(過渡),切換元素的類名可以觸發該元素的transition(過渡)
暫停元素的transition(過渡), 在你想要暫停過渡點,用
getComputedStyle和getPropertyValue獲取該元素相應的CSS屬性值,然后設置該元素的對應的CSS屬性等於你剛才獲取到的CSS屬性值。
以下是該方法的一個例子。
執行效果:
http://cdpn.io/GokAm
同樣的技術可以用在更高級的方法上。下面的例子也是通過改變類名來觸發元素的transition(過渡),但這次可以跟蹤當前的縮放率。
執行效果:http://cdpn.io/FIlHe
注意我們這次改變的是background-size的值。有許多不同的CSS屬性可以應用到過渡和動畫中,
這些屬性
通常具有數值或顏色值。關於CSS transitions(過渡),Rodney Rehm也寫了一篇非常不錯的文章,
這里可以訪問到。
使用CSS“回調函數”
一些最有用但鮮為人知javascript技巧,就是利用監聽Dom事件控制CSS transitions(過渡)和animations(動畫)。如:與animations(動畫)相關的animationEnd,animationStart和animationIteration;與transitions(過渡)相關的transitonEnd。你可能已經猜到它們是做什么的。這些動畫事件分別是在元素的動畫結束時,開始時,或者完成一次迭代時觸發。
目前使用這些事件還需要添加瀏覽器前綴,所以在這個演示中,我們使用由Craig Buckler開發的叫PrefixedEvent的方法。該方法的參數有element(元素),type(類型)和callback(回調)來實現跨瀏覽器的兼容。這里是他的一篇文章
使用JavaScript捕獲CSS animations(動畫)。這里是
另一篇關於通過判斷動畫名稱來判斷觸發哪個事件。
這個演示想實現當鼠標懸浮時停止動畫,並放大心型圖案。
執行效果:http://cdpn.io/rmDdx
純CSS版本在鼠標懸停時會跳一下,除非你在恰當的時機鼠標移上去,不然它會在擴大到最終懸停狀態之前先跳到一個特定狀態。JavaScript版本就非常流暢,它在應用新的放大狀態之前先讓動畫完成,這樣鼠標懸停時就不會跳動。
控制CSS Animation(動畫)
就像我們剛剛了解到的,我們可以看到與元素動畫相關的事件:animationStart,animationIteration,animationEnd。但是如果我們想改變CSS animation(動畫)執行過程中的動畫,還需要一點技巧!
animation-play-state屬性
當你想在動畫執行過程中暫停,並且接下來讓動畫接着執行。這時CSS的animation-play-state屬性是非常有用的。你可以可以通過JavaScript像這樣更改CSS(注意你的前綴):
|
1
2
|
element.style.webkitAnimationPlayState =
"paused"
;
element.style.webkitAnimationPlayState =
"running"
;
|
然而當使用animation-play-state讓CSS 動畫暫停時,動畫中的元素變形也會以相同的方式被阻止。你不能使這種變形暫停在某個狀態,使它變形,使它恢復,更不用期望它能從新的變形狀態中恢復到流暢運行。為了實現這些控制,我們需要做一些更復雜的工作。
獲取當前keyvalue的百分比
不幸的是,在這個階段沒有辦法獲得當前CSS動畫關鍵幀的“完成百分比”。最好的獲取近似值的方法是使用setInterval 函數在動畫過程中迭代100次。它的本質是:動畫持續的時間(單位是毫秒)/100。例如,如果動畫時長4秒,則得到的setInterval的執行時間是每40毫秒(4000 / 100)。
這種做法很不理想,因為函數實際運行頻率要遠少於每40毫秒。我發現將它設為39毫秒更准確。但這個也不是好實現,因為它依賴於瀏覽器,並非所有瀏覽器下都能得到很完美效果。
獲取當前動畫的CSS屬性值
在理想的情況下,我們選擇一個使用CSS動畫的元素,刪除該元素當前動畫再給它添加個新的動畫,讓它可以從當前狀態開始新的動畫。但是現實情況卻很復雜。
下面我們就有一個演示,用來測試獲取和改變CSS動畫”中間流”的技術。該動畫讓一個元素沿一個圓形路徑移動,起始位置在圓形的頂部中心(或稱為“十二點”)位置。當按鈕被單擊時,元素的起始位置變成元素當前移動到的位置。元素會沿着之前相同的路徑繼續移動,只是現在“起始”的位置變成了你按下按鈕時元素移動到的位置。通過在動畫的第一關鍵幀把元素的顏色變成紅色,來表示元素動畫起始點位置發生了改變。
執行效果:http://cdpn.io/GwBJa
相同的概念用在
StackOverflow的這個例子中。
我們需要很深入才能完成!我們要進入的樣式表本身找到原有動畫。
你可以用document.styleSheets來獲取與頁面關聯的樣式表的集合,然后通過for循環取得具體的樣式表。以下是如何使用JavaScript來找到一個特定動畫值的CSSKeyFrameRules對象:
|
1
2
3
4
5
6
7
8
9
10
11
|
function
findKeyframesRule(rule){
var
ss = document.styleSheets;
for
(
var
i = 0;i < ss.length;++i){
for
(
var
j = 0;j<ss[i].cssRules.length;++j){
if
(ss[i].cssRules[j].type == window.CSSRule.WEBKIT_KEYFRAMES_RULE && ss[i].cssRules[j].name == rule){
return
ss[i].cssRules[j];
}
}
}
return
null
;
}
|
我們一旦調用上面的函數(例如 var keyframes= findKeyframesRule(anim)),就可以通過keyframes.cssRules.length獲得該對象的動畫長度(這個動畫中關鍵幀的總數量)。然后使用JavaScript的.map方法把獲得到的每個關鍵幀值上的“%”過濾掉,這樣JavaScript就可以把這些值作為數字使用。
|
1
2
3
4
5
6
7
8
9
10
11
12
|
// Makes an array of the current percent values
// in the animation
var
keyframeString = [];
for
(
var
i = 0; i < length; i ++)
{
keyframeString.push(keyframes[i].keyText);
}
// Removes all the % values from the array so
// the getClosest function can perform calculations
var
keys = keyframeString.map(
function
(str) {
return
str.replace(
'%'
,
''
);
});
|
這里keys是一個包含所有動畫關鍵幀數值的數組。
改變實際的動畫(終於!)
在循環動畫演示過程中,我們需要兩個變量:一個用來跟蹤從最近的起始位置開始移動了多少度,另一個用來跟蹤從原來的起始位置開始移動了多少度。我們可以使用setInterval函數(在環形移動度數時消耗的時間)改變第一個變量。然后我們可以使用下面的代碼,當單擊該按鈕時更新第二個變量。
|
1
2
3
4
5
|
totalCurrentPercent += currentPercent;
// Since it's in percent it shouldn't ever be over 100
if
(totalCurrentPercent > 100) {
totalCurrentPercent -= 100;
}
|
然后我們可以使用以下函數,在之前我們獲得的關鍵幀數組里,找出與當前總百分比值最接近的關鍵幀值。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function
getClosest(keyframe) {
// curr stands for current keyframe
var
curr = keyframe[0];
var
diff = Math.abs (totalCurrentPercent - curr);
for
(
var
val = 0, j = keyframe.length; val < j; val++) {
var
newdiff = Math.abs(totalCurrentPercent - keyframe[val]);
// If the difference between the current percent and the iterated
// keyframe is smaller, take the new difference and keyframe
if
(newdiff < diff) {
diff = newdiff;
curr = keyframe[val];
}
}
return
curr;
}
|
要獲得新動畫第一關鍵幀的位置值,我們可以使用JavaScript的.IndexOf方法。然后我們根據這個值,刪除原來的關鍵幀定義,重新定義該關鍵幀。
|
1
2
3
|
for
(
var
i = 0, j = keyframeString.length; i < j; i ++) {
keyframes.deleteRule(keyframeString[i]);
}
|
接下來,我們需要把圓的度數值轉換成相應的百分比值。我們可以通過第一關鍵幀的位置值與3.6簡單的相乘得到(因為10 0 * 3.6 = 360)。
最后,我們基於上面獲得變量創建新的規則。每個規則之間有45度的差值,是因為我們在繞圈過程中擁有八個不同的關鍵幀,360(一個圓的度數)除以8是45。
|
1
2
3
4
5
6
7
8
9
10
11
|
// Prefix here as needed
keyframes.insertRule(
"0% {
-webkit-transform: translate(100px,100px) rotate("
+ (multiplier + 0) +
"deg)
translate(-100px,-100px) rotate("
+ (multiplier + 0) +
"deg);
}"
);
keyframes.insertRule(
"13% {
-webkit-transform: translate(100px,100px) rotate("
+ (multiplier + 45) +
"deg)
translate(-100px,-100px) rotate("
+ (multiplier + 45) +
"deg);
}"
);
...continued...
|
然后我們通過setInterval重置當前百分比值來使它可以再次運行。注意上面使用的是WebKit前綴,為了使它兼容更多的瀏覽器,我們需要做一些UA的嗅探來確定采用哪個前綴:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Gets the browser prefix
var
browserPrefix;
navigator.sayswho= (
function
(){
var
N = navigator.appName, ua = navigator.userAgent, tem;
var
M = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
if
(M && (tem = ua.match(/version\/([\.\d]+)/i))!=
null
) M[2] = tem[1];
M = M? [M[1], M[2]]: [N, navigator.appVersion,
'-?'
];
M = M[0];
if
(M ==
"Chrome"
) { browserPrefix =
"webkit"
; }
if
(M ==
"Firefox"
) { browserPrefix =
"moz"
; }
if
(M ==
"Safari"
) { browserPrefix =
"webkit"
; }
if
(M ==
"MSIE"
) { browserPrefix =
"ms"
; }
})();
|
如果你想進一步研究,可以訪問Russell Uresti在StackOverflow上的帖子和相應的案例。
Animations(動畫)轉成Transitions(過渡)
正如我們所看到的,使用JavaScript可以很方便的操作CSS transitions(過渡)。如果使用CSS animations(動畫)最終沒能得到想要的結果,你可以試着把它變成css transitions(過渡)來實現。從CSS代碼來看他們大約有相同的代碼量,但使用transiton可以更容易地設置和編輯。
將CSS animations(動畫)轉換成CSS transitions(過渡)的最大問題是,當我們把animation-iteration轉換成與之等效的transition命令時,Transitons(過渡)沒有直接等效命令。
關於我們的旋轉演示,有一個小技巧就是用x來分別乘以transition-duration和rotation(譯者:分別包括X軸和Y軸的旋轉值)。然后你需要使用樣式類來觸發這個動畫,因為如果你在元素上直接改變這些屬性,將不會有過渡效果。你需要給元素添加類名來觸發過渡(模擬動畫)。
在我們的例子中,我們在頁面加載時實現:
執行效果:http://cdpn.io/IdlHx
利用CSS矩陣
你也通過CSSMatrix來操作CSS animations(動畫)。比如:
|
1
2
|
var
translated3D =
new
WebKitCSSMatrix(window.getComputedStyle(elem,
null
).webkitTransform);
|
但是這個過程可能有些混亂,尤其對於那些剛剛開始使用CSS animations(動畫)的。
重置CSS animations(動畫)
實現這個技巧的方法可以從
CSS Tricks找到。
動動腦筋
在開始編碼前,就思考和規划過渡或動畫如何執行,應該是減少你的問題和達到你想要的效果的最佳途徑。好過你在遇到問題時google搜索解決方案!雖然在這篇文章中總結的技術和技巧,不一定是你在項目中創建動畫的最佳方案,但值得你嘗試着了解一下(師兄我也只能幫到這里了……)。
比如這個小例子僅通過html和css就解決了問題,你可能開始會想着使用JavaScript去解決。
我們想讓一個不停旋轉的圖形當鼠標懸停時反方向旋轉。你可能跳過這篇文章講解的內容直接使用animationIteration事件來實現這個動畫。然而,一個更有效更好的方案是是使用CSS和添加內容元素。
技巧是獲取旋轉圖形旋轉速度x,當鼠標懸停時,讓其父元素以2倍x的速度反方向旋轉(在相同的位置)。兩個方向的旋轉相互作用,最終得到想要的反向旋轉的效果。
執行效果:
http://cdpn.io/sFnyD
相同的概念用在
StackOverflow的這個例子中。
相關鏈接
你可能會感興趣的相關東東。
Animo.js – 用於管理CSS動畫的強大小工具
Thank God We Have A Specification! – Smashing Magazine上的關於transition技巧的文章
總結
1. getComputedStyle 對於操作CSS transitions(過渡)很有幫助
2. transitionEnd及其相關事件對於使用JavaScript操作CSS transitions(過渡)和animations(動畫)非常有幫助
3. 通過使用JavaScript獲取樣式表可以更改當前CSS animation的值,但操作比較復雜。
4. 通常情況下使用JavaScript操作CSS transitions(過渡)比操作CSS animations(動畫)要更容易。
5. 處理CSS矩陣比較痛苦,尤其對於初學者來說。
6. 思考應該做什么和規划如何做,是動畫編碼的關鍵。
