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