W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
JavaScript 動畫可以處理 CSS 無法處理的事情。
例如,沿著具有與 Bezier 曲線不同的時序函數(shù)的復(fù)雜路徑移動,或者實現(xiàn)畫布上的動畫。
從 HTML/CSS 的角度來看,動畫是 style 屬性的逐漸變化。例如,將 style.left
從 0px
變化到 100px
可以移動元素。
如果我們用 setInterval
每秒做 50 次小變化,看起來會更流暢。電影也是這樣的原理:每秒 24 幀或更多幀足以使其看起來流暢。
偽代碼如下:
let delay = 1000 / 50; // 每秒 50 幀
let timer = setInterval(function() {
if (animation complete) clearInterval(timer);
else increase style.left
}, delay)
更完整的動畫示例:
let start = Date.now(); // 保存開始時間
let timer = setInterval(function() {
// 距開始過了多長時間
let timePassed = Date.now() - start;
if (timePassed >= 2000) {
clearInterval(timer); // 2 秒后結(jié)束動畫
return;
}
// 在 timePassed 時刻繪制動畫
draw(timePassed);
}, 20);
// 隨著 timePassed 從 0 增加到 2000
// 將 left 的值從 0px 增加到 400px
function draw(timePassed) {
train.style.left = timePassed / 5 + 'px';
}
假設(shè)我們有幾個同時運行的動畫。
如果我們單獨運行它們,每個都有自己的 setInterval(..., 20)
,那么瀏覽器必須以比 20ms
更頻繁的速度重繪。
每個 setInterval
每 20ms
觸發(fā)一次,但它們相互獨立,因此 20ms
內(nèi)將有多個獨立運行的重繪。
這幾個獨立的重繪應(yīng)該組合在一起,以使瀏覽器更加容易處理。
換句話說,像下面這樣:
setInterval(function() {
animate1();
animate2();
animate3();
}, 20)
……比這樣更好:
setInterval(animate1, 20);
setInterval(animate2, 20);
setInterval(animate3, 20);
還有一件事需要記住。有時當 CPU 過載時,或者有其他原因需要降低重繪頻率。例如,如果瀏覽器選項卡被隱藏,那么繪圖完全沒有意義。
有一個標準動畫時序提供了 requestAnimationFrame
函數(shù)。
它解決了所有這些問題,甚至更多其它的問題。
語法:
let requestId = requestAnimationFrame(callback);
這會讓 callback
函數(shù)在瀏覽器每次重繪的最近時間運行。
如果我們對 callback
中的元素進行變化,這些變化將與其他 requestAnimationFrame
回調(diào)和 CSS 動畫組合在一起。因此,只會有一次幾何重新計算和重繪,而不是多次。
返回值 requestId
可用來取消回調(diào):
// 取消回調(diào)的周期執(zhí)行
cancelAnimationFrame(requestId);
callback
得到一個參數(shù) —— 從頁面加載開始經(jīng)過的毫秒數(shù)。這個時間也可通過調(diào)用 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>
現(xiàn)在我們可以在 requestAnimationFrame
基礎(chǔ)上創(chuàng)建一個更通用的動畫函數(shù):
function animate({timing, draw, duration}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction 從 0 增加到 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// 計算當前動畫狀態(tài)
let progress = timing(timeFraction);
draw(progress); // 繪制
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
animate
函數(shù)接受 3 個描述動畫的基本參數(shù):
?duration
?
動畫總時間,比如 ?1000
?。
?timing(timeFraction)
?
時序函數(shù),類似 CSS 屬性 transition-timing-function
,傳入一個已過去的時間與總時間之比的小數(shù)(0
代表開始,1
代表結(jié)束),返回動畫完成度(類似 Bezier 曲線中的 y
)。
例如,線性函數(shù)意味著動畫以相同的速度均勻地進行:
function linear(timeFraction) {
return timeFraction;
}
圖像如下:
它類似于 transition-timing-function: linear
。后文有更多有趣的變體。
?draw(progress)
?
獲取動畫完成狀態(tài)并繪制的函數(shù)。值 progress = 0
表示開始動畫狀態(tài),progress = 1
表示結(jié)束狀態(tài)。
這是實際繪制動畫的函數(shù)。
它可以移動元素:
function draw(progress) {
train.style.left = progress + 'px';
}
……或者做任何其他事情,我們可以以任何方式為任何事物制作動畫。
讓我們使用我們的函數(shù)將元素的 width
從 0
變化為 100%
。
它的代碼如下:
animate({
duration: 1000,
timing(timeFraction) {
return timeFraction;
},
draw(progress) {
elem.style.width = progress * 100 + '%';
}
});
與 CSS 動畫不同,我們可以在這里設(shè)計任何時序函數(shù)和任何繪圖函數(shù)。時序函數(shù)不受 Bezier 曲線的限制。并且 draw
不局限于操作 CSS 屬性,還可以為類似煙花動畫或其他動畫創(chuàng)建新元素。
上文我們看到了最簡單的線性時序函數(shù)。
讓我們看看更多。我們將嘗試使用不同時序函數(shù)的移動動畫來查看它們的工作原理。
如果我們想加速動畫,我們可以讓 progress
為 n
次冪。
例如,拋物線:
function quad(timeFraction) {
return Math.pow(timeFraction, 2)
}
圖像如下:
……或者三次曲線甚至使用更大的 n
。增大冪會讓動畫加速得更快。
下面是 progress
為 5
次冪的圖像:
函數(shù):
function circ(timeFraction) {
return 1 - Math.sin(Math.acos(timeFraction));
}
圖像:
此函數(shù)執(zhí)行“弓箭射擊”。首先,我們“拉弓弦”,然后“射擊”。
與以前的函數(shù)不同,它取決于附加參數(shù) x
,即“彈性系數(shù)”?!袄摇钡木嚯x由它定義。
代碼如下:
function back(x, timeFraction) {
return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x);
}
x = 1.5
時的圖像:
在動畫中我們使用特定的 x
值。下面是 x = 1.5
時的例子:
想象一下,我們正在拋球。球落下之后,彈跳幾次然后停下來。
bounce
函數(shù)也是如此,但順序相反:“bouncing”立即啟動。它使用了幾個特殊的系數(shù):
function bounce(timeFraction) {
for (let a = 0, b = 1, result; 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)
}
}
}
另一個“伸縮”函數(shù)接受附加參數(shù) 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
時的演示
我們有一組時序函數(shù)。它們的直接應(yīng)用稱為“easeIn”。
有時我們需要以相反的順序顯示動畫。這是通過“easeOut”變換完成的。
在“easeOut”模式中,我們將 timing
函數(shù)封裝到 timingEaseOut
中:
timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction);
換句話說,我們有一個“變換”函數(shù) makeEaseOut
,它接受一個“常規(guī)”時序函數(shù) timing
并返回一個封裝器,里面封裝了 timing
函數(shù):
// 接受時序函數(shù),返回變換后的變體
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
例如,我們可以使用上面描述的 bounce
函數(shù):
let bounceEaseOut = makeEaseOut(bounce);
這樣,彈跳不會在動畫開始時執(zhí)行,而是在動畫結(jié)束時。這樣看起來更好:
在這里,我們可以看到變換如何改變函數(shù)的行為:
如果在開始時有動畫效果,比如彈跳 —— 那么它將在最后顯示。
上圖中常規(guī)彈跳為紅色,easeOut 彈跳為藍色。
easeOut
? 變換之后 —— 物體跳到頂部之后,在那里彈跳。我們還可以在動畫的開頭和結(jié)尾都顯示效果。該變換稱為“easeInOut”。
給定時序函數(shù),我們按下面的方式計算動畫狀態(tài):
if (timeFraction <= 0.5) { // 動畫前半部分
return timing(2 * timeFraction) / 2;
} else { // 動畫后半部分
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
演示如下:
“easeInOut” 變換將兩個圖像連接成一個:動畫的前半部分為“easeIn”(常規(guī)),后半部分為“easeOut”(反向)。
如果我們比較 circ
時序函數(shù)的 easeIn
、easeOut
和 easeInOut
的圖像,就可以清楚地看到效果:
circ
?(?easeIn
?)的常規(guī)變體。easeOut
?。easeInOut
?。正如我們所看到的,動畫前半部分的圖形是縮小的“easeIn”,后半部分是縮小的“easeOut”。結(jié)果是動畫以相同的效果開始和結(jié)束。
除了移動元素,我們還可以做其他事情。我們所需要的只是寫出合適的 ?draw
?。
這是動畫形式的“彈跳”文字輸入:
JavaScript 動畫應(yīng)該通過 requestAnimationFrame
實現(xiàn)。該內(nèi)建方法允許設(shè)置回調(diào)函數(shù),以便在瀏覽器準備重繪時運行。那通常很快,但確切的時間取決于瀏覽器。
當頁面在后臺時,根本沒有重繪,因此回調(diào)將不會運行:動畫將被暫停并且不會消耗資源。那很棒。
這是設(shè)置大多數(shù)動畫的 helper 函數(shù) animate
:
function animate({timing, draw, duration}) {
let start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction 從 0 增加到 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// 計算當前動畫狀態(tài)
let progress = timing(timeFraction);
draw(progress); // 繪制
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
參數(shù):
duration
? —— 動畫運行的總毫秒數(shù)。timing
? —— 計算動畫進度的函數(shù)。獲取從 0 到 1 的小數(shù)時間,返回動畫進度,通常也是從 0 到 1。draw
? —— 繪制動畫的函數(shù)。當然我們可以改進它,增加更多花里胡哨的東西,但 JavaScript 動畫不是經(jīng)常用到。它們用于做一些有趣和不標準的事情。因此,您大可在必要時再添加所需的功能。
JavaScript 動畫可以使用任何時序函數(shù)。我們介紹了很多例子和變換,使它們更加通用。與 CSS 不同,我們不僅限于 Bezier 曲線。
?draw
? 也是如此:我們可以將任何東西動畫化,而不僅僅是 CSS 屬性。
做一個彈跳的球。
為了達到反彈效果,我們可以在帶有 position:relative
屬性的區(qū)域內(nèi),給小球使用 top
和 position:absolute
CSS 屬性。
field 區(qū)域的底部坐標是 field.clientHeight
。top
屬性給出了球頂部的坐標,在最底部時達到 field.clientHeight - ball.clientHeight
。
因此,我們將 top
從 0
變化到 field.clientHeight - ball.clientHeight
來設(shè)置動畫。
現(xiàn)在為了獲得“彈跳”效果,我們可以在 easeOut
模式下使用時序函數(shù) bounce
。
這是動畫的最終代碼:
let to = field.clientHeight - ball.clientHeight;
animate({
duration: 2000,
timing: makeEaseOut(bounce),
draw(progress) {
ball.style.top = to * progress + 'px'
}
});
讓球向右移動。
編寫動畫代碼。終止時球到左側(cè)的距離是 100px
。
從前一個任務(wù) 為彈跳的球設(shè)置動畫 的答案開始。
在任務(wù) 為彈跳的球設(shè)置動畫 中,我們只有一個需要添加動畫的屬性?,F(xiàn)在多了一個 elem.style.left
。
水平坐標由另一個定律改變:它不會“反彈”,而是逐漸增加使球逐漸向右移動。
我們可以為它多寫一個 animate
。
至于時序函數(shù),我們可以使用 linear
,但像 makeEaseOut(quad)
這樣的函數(shù)看起來要好得多。
代碼:
let height = field.clientHeight - ball.clientHeight;
let width = 100;
// 設(shè)置 top 動畫(彈跳)
animate({
duration: 2000,
timing: makeEaseOut(bounce),
draw: function(progress) {
ball.style.top = height * progress + 'px'
}
});
// 設(shè)置 left 動畫(向右移動)
animate({
duration: 2000,
timing: makeEaseOut(quad),
draw: function(progress) {
ball.style.left = width * progress + "px"
}
});
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: