JavaScript 動畫可以處理 CSS 無法處理的事情。
例如,沿著複雜路徑移動,使用與貝茲曲線不同的計時函數,或在畫布上進行動畫。
使用 setInterval
動畫可以實作為一系列的畫面,通常是對 HTML/CSS 屬性的微小變更。
例如,將 style.left
從 0px
變更為 100px
會移動元素。如果我們在 setInterval
中增加它,以微小的延遲(例如每秒 50 次)變更 2px
,那麼它看起來就會很順暢。這與電影中的原理相同:每秒 24 個畫面就足以讓它看起來很順暢。
偽程式碼可以如下所示
let timer = setInterval(function() {
if (animation complete) clearInterval(timer);
else increase style.left by 2px
}, 20); // change by 2px every 20ms, about 50 frames per second
更完整的動畫範例
let start = Date.now(); // remember start time
let timer = setInterval(function() {
// how much time passed from the start?
let timePassed = Date.now() - start;
if (timePassed >= 2000) {
clearInterval(timer); // finish the animation after 2 seconds
return;
}
// draw the animation at the moment timePassed
draw(timePassed);
}, 20);
// as timePassed goes from 0 to 2000
// left gets values from 0px to 400px
function draw(timePassed) {
train.style.left = timePassed / 5 + 'px';
}
按一下以查看示範
<!DOCTYPE HTML>
<html>
<head>
<style>
#train {
position: relative;
cursor: pointer;
}
</style>
</head>
<body>
<img id="train" src="https://js.cx/clipart/train.gif">
<script>
train.onclick = function() {
let start = Date.now();
let timer = setInterval(function() {
let timePassed = Date.now() - start;
train.style.left = timePassed / 5 + 'px';
if (timePassed > 2000) clearInterval(timer);
}, 20);
}
</script>
</body>
</html>
使用 requestAnimationFrame
想像一下我們同時執行多個動畫。
如果我們分別執行它們,那麼即使每個動畫都有 setInterval(..., 20)
,瀏覽器仍必須比每 20ms
重新繪製更頻繁。
這是因為它們有不同的開始時間,所以「每 20ms」在不同的動畫之間有所不同。間隔沒有對齊。因此,我們將在 20ms
內執行多個獨立執行。
換句話說,這
setInterval(function() {
animate1();
animate2();
animate3();
}, 20)
…比三個獨立呼叫更輕量
setInterval(animate1, 20); // independent animations
setInterval(animate2, 20); // in different places of the script
setInterval(animate3, 20);
這些多個獨立的重新繪製應該組合在一起,以使瀏覽器更容易重新繪製,從而減少 CPU 負載並看起來更順暢。
還有一件事需要記住。有時 CPU 會過載,或者有其他原因導致重新繪製的頻率較低(例如當瀏覽器標籤隱藏時),因此我們真的不應該每 20ms
執行一次。
但是我們如何在 JavaScript 中知道這一點?有一個規範 動畫時序 提供了 requestAnimationFrame
函數。它解決了所有這些問題,甚至更多。
語法
let requestId = requestAnimationFrame(callback)
這將安排 callback
函數在瀏覽器想要執行動畫時最接近的時間執行。
如果我們在 callback
中對元素進行更改,則它們將與其他 requestAnimationFrame
回呼和 CSS 動畫組合在一起。因此,將進行一次幾何重新計算和重新繪製,而不是多次。
返回的值 requestId
可用於取消呼叫
// cancel the scheduled execution of callback
cancelAnimationFrame(requestId);
callback
獲得一個參數 - 從頁面載入開始經過的時間(以毫秒為單位)。也可以通過呼叫 performance.now() 來獲取此時間。
通常 callback
會很快執行,除非 CPU 過載或筆電電池幾乎耗盡,或者有其他原因。
以下程式碼顯示了 requestAnimationFrame
前 10 次執行的時間。通常為 10-20ms
<script>
let prev = performance.now();
let times = 0;
requestAnimationFrame(function measure(time) {
document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
prev = time;
if (times++ < 10) requestAnimationFrame(measure);
})
</script>
結構化動畫
現在,我們可以根據 requestAnimationFrame
製作一個更通用的動畫函數
function animate({timing, draw, duration}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction goes from 0 to 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// calculate the current animation state
let progress = timing(timeFraction)
draw(progress); // draw it
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
函數 animate
接受 3 個參數,這些參數基本上描述了動畫
持續時間
-
動畫的總時間。例如,
1000
。 timing(timeFraction)
-
計時函數,例如 CSS 屬性
transition-timing-function
,它會取得已過時間的分數(開始時為0
,結束時為1
),並傳回動畫完成度(例如貝茲曲線上的y
)。例如,線性函數表示動畫會以相同的速度均勻進行
function linear(timeFraction) { return timeFraction; }
其圖表:
這就像
transition-timing-function: linear
。下方顯示更多有趣的變體。 draw(progress)
-
取得動畫完成狀態並繪製的函數。值
progress=0
表示動畫開始狀態,而progress=1
表示結束狀態。這是實際繪製動畫的函數。
它可以移動元素
function draw(progress) { train.style.left = progress + 'px'; }
…或執行其他任何動作,我們可以以任何方式動畫化任何事物。
讓我們使用我們的函數,將元素 width
從 0
動畫化到 100%
。
按一下元素以查看示範
function animate({duration, draw, timing}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
let progress = timing(timeFraction)
draw(progress);
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
progress {
width: 5%;
}
</style>
<script src="animate.js"></script>
</head>
<body>
<progress id="elem"></progress>
<script>
elem.onclick = function() {
animate({
duration: 1000,
timing: function(timeFraction) {
return timeFraction;
},
draw: function(progress) {
elem.style.width = progress * 100 + '%';
}
});
};
</script>
</body>
</html>
其程式碼
animate({
duration: 1000,
timing(timeFraction) {
return timeFraction;
},
draw(progress) {
elem.style.width = progress * 100 + '%';
}
});
與 CSS 動畫不同,我們可以在這裡建立任何計時函數和任何繪製函數。計時函數不受貝茲曲線限制。而且 draw
可以超越屬性,建立新元素,例如煙火動畫或類似的東西。
計時函數
我們在上面看到了最簡單的線性計時函數。
讓我們看看更多。我們將嘗試使用不同的計時函數進行移動動畫,以了解它們如何運作。
n 的次方
如果我們想要加速動畫,我們可以在次方 n
中使用 progress
。
例如,拋物線
function quad(timeFraction) {
return Math.pow(timeFraction, 2)
}
圖表
實際操作(按一下以啟用)
…或三次曲線,甚至更大的 n
。增加次方會讓它加速更快。
以下是次方 5
中 progress
的圖表
實際操作
弧線
函數
function circ(timeFraction) {
return 1 - Math.sin(Math.acos(timeFraction));
}
圖表
後退:射箭
此函數會執行「射箭」。我們先「拉弓弦」,然後「射擊」。
與先前的函數不同,它取決於額外的參數 x
,即「彈性係數」。「拉弓弦」的距離由它定義。
程式碼
function back(x, timeFraction) {
return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x)
}
x = 1.5
的圖表
對於動畫,我們使用它與 x
的特定值。x = 1.5
的範例
彈跳
想像我們正在掉一個球。它掉下來,然後彈跳幾次並停止。
bounce
函數執行相同的動作,但順序相反:「彈跳」會立即開始。它使用幾個特殊係數來執行此動作
function bounce(timeFraction) {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
實際操作
彈性動畫
另一個「彈性」函數,它接受一個額外的參數 x
作為「初始範圍」。
function elastic(x, timeFraction) {
return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction)
}
x=1.5
的圖形:
x=1.5
的動作
反轉:ease*
因此,我們有一組計時函數。它們的直接應用稱為「easeIn」。
有時我們需要以相反的順序顯示動畫。這是使用「easeOut」轉換來完成的。
easeOut
在「easeOut」模式中,timing
函數會放入包裝函數 timingEaseOut
timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction)
換句話說,我們有一個「轉換」函數 makeEaseOut
,它會取得一個「常規」計時函數並回傳其周圍的包裝函數
// accepts a timing function, returns the transformed variant
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
例如,我們可以取得上面描述的 bounce
函數並套用它
let bounceEaseOut = makeEaseOut(bounce);
然後彈跳不會在動畫的開始,而是在動畫的結尾。看起來更好
#brick {
width: 40px;
height: 20px;
background: #EE6B47;
position: relative;
cursor: pointer;
}
#path {
outline: 1px solid #E8C48E;
width: 540px;
height: 20px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="https://js.cx/libs/animate.js"></script>
</head>
<body>
<div id="path">
<div id="brick"></div>
</div>
<script>
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
function bounce(timeFraction) {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
let bounceEaseOut = makeEaseOut(bounce);
brick.onclick = function() {
animate({
duration: 3000,
timing: bounceEaseOut,
draw: function(progress) {
brick.style.left = progress * 500 + 'px';
}
});
};
</script>
</body>
</html>
在這裡,我們可以看到轉換如何改變函數的行為
如果一開始就有動畫效果,例如彈跳,它將會在結尾顯示。
在上面的圖表中,常規彈跳 為紅色,easeOut 彈跳 為藍色。
- 常規彈跳 - 物件在底部彈跳,然後在結尾急劇跳到頂部。
- 在
easeOut
之後 - 它會先跳到頂部,然後在那裡彈跳。
easeInOut
我們也可以在動畫的開始和結尾顯示效果。轉換稱為「easeInOut」。
給定計時函數,我們會這樣計算動畫狀態
if (timeFraction <= 0.5) { // first half of the animation
return timing(2 * timeFraction) / 2;
} else { // second half of the animation
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
包裝函數程式碼
function makeEaseInOut(timing) {
return function(timeFraction) {
if (timeFraction < .5)
return timing(2 * timeFraction) / 2;
else
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
}
bounceEaseInOut = makeEaseInOut(bounce);
在動作中,bounceEaseInOut
#brick {
width: 40px;
height: 20px;
background: #EE6B47;
position: relative;
cursor: pointer;
}
#path {
outline: 1px solid #E8C48E;
width: 540px;
height: 20px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="https://js.cx/libs/animate.js"></script>
</head>
<body>
<div id="path">
<div id="brick"></div>
</div>
<script>
function makeEaseInOut(timing) {
return function(timeFraction) {
if (timeFraction < .5)
return timing(2 * timeFraction) / 2;
else
return (2 - timing(2 * (1 - timeFraction))) / 2;
}
}
function bounce(timeFraction) {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
let bounceEaseInOut = makeEaseInOut(bounce);
brick.onclick = function() {
animate({
duration: 3000,
timing: bounceEaseInOut,
draw: function(progress) {
brick.style.left = progress * 500 + 'px';
}
});
};
</script>
</body>
</html>
「easeInOut」轉換將兩個圖表合併為一個:easeIn
(常規)用於動畫的前半段,easeOut
(反轉)用於後半段。
如果我們比較 circ
計時函數的 easeIn
、easeOut
和 easeInOut
的圖表,效果會很明顯
- 紅色是
circ
(easeIn
)的常規變體。 - 綠色 –
easeOut
。 - 藍色 –
easeInOut
。
正如我們所見,動畫前半段的圖形是縮小的 easeIn
,而後半段是縮小的 easeOut
。因此,動畫以相同的效果開始和結束。
更有趣的「繪製」
我們可以做一些事,而不是移動元素。我們只需要撰寫適當的 draw
。
以下是動畫「彈跳」文字輸入
textarea {
display: block;
border: 1px solid #BBB;
color: #444;
font-size: 110%;
}
button {
margin-top: 10px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="https://js.cx/libs/animate.js"></script>
</head>
<body>
<textarea id="textExample" rows="5" cols="60">He took his vorpal sword in hand:
Long time the manxome foe he sought—
So rested he by the Tumtum tree,
And stood awhile in thought.
</textarea>
<button onclick="animateText(textExample)">Run the animated typing!</button>
<script>
function animateText(textArea) {
let text = textArea.value;
let to = text.length,
from = 0;
animate({
duration: 5000,
timing: bounce,
draw: function(progress) {
let result = (to - from) * progress + from;
textArea.value = text.slice(0, Math.ceil(result))
}
});
}
function bounce(timeFraction) {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
</script>
</body>
</html>
摘要
對於 CSS 無法妥善處理的動畫,或需要嚴格控制的動畫,JavaScript 可以提供協助。JavaScript 動畫應透過 requestAnimationFrame
實作。內建方法允許設定一個回呼函式,在瀏覽器準備重新繪製時執行。通常會很快,但確切時間取決於瀏覽器。
當頁面在背景中時,完全沒有重新繪製,因此回呼不會執行:動畫將暫停,並且不會消耗資源。這很棒。
以下是設定大多數動畫的輔助函式 animate
function animate({timing, draw, duration}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction goes from 0 to 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// calculate the current animation state
let progress = timing(timeFraction);
draw(progress); // draw it
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
選項
duration
– 總動畫時間(以毫秒為單位)。timing
– 計算動畫進度的函式。取得從 0 到 1 的時間分數,傳回動畫進度,通常從 0 到 1。draw
– 繪製動畫的函式。
我們當然可以改善它,增加更多花俏的功能,但 JavaScript 動畫並非每天都會應用。它們用於執行一些有趣且非標準的事情。因此,您可以在需要時新增所需的功能。
JavaScript 動畫可以使用任何計時函式。我們介紹了許多範例和轉換,讓它們更具通用性。與 CSS 不同,我們在此不受貝茲曲線的限制。
draw
也是如此:我們可以動畫任何東西,而不仅仅是 CSS 屬性。
留言
<code>
標籤,若要插入多行,請將它們包在<pre>
標籤中,若要插入超過 10 行,請使用沙盒 (plnkr、jsbin、codepen…)