Animace JavaScriptu zvládnou věci, které CSS nezvládnou.
Například pohyb po složité cestě s funkcí časování odlišnou od Bézierových křivek nebo animací na plátně.
Použití setInterval
Animaci lze implementovat jako sekvenci snímků – obvykle malé změny vlastností HTML/CSS.
Například změna style.left
od 0px
do 100px
přesune prvek. A pokud jej zvýšíme v setInterval
, mění se o 2px
s malým zpožděním, například 50krát za sekundu, pak vypadá hladce. To je stejný princip jako v kině:24 snímků za sekundu stačí, aby to vypadalo hladce.
Pseudokód může vypadat takto:
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
Úplnější příklad animace:
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';
}
Klikněte pro ukázku:
Resultindex.html<!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>
Použití requestAnimationFrame
Představme si, že běží několik animací současně.
Pokud je spustíme samostatně, pak i když každý má setInterval(..., 20)
, pak by prohlížeč musel překreslovat mnohem častěji než každých 20ms
.
Je to proto, že mají odlišný čas začátku, takže „každých 20 ms“ se mezi různými animacemi liší. Intervaly nejsou zarovnány. Takže budeme mít několik nezávislých běhů v rámci 20ms
.
Jinými slovy, toto:
setInterval(function() {
animate1();
animate2();
animate3();
}, 20)
…Je lehčí než tři nezávislá volání:
setInterval(animate1, 20); // independent animations
setInterval(animate2, 20); // in different places of the script
setInterval(animate3, 20);
Těchto několik nezávislých překreslení by mělo být seskupeno, aby bylo překreslování pro prohlížeč snazší, a tudíž méně zatěžovalo CPU a vypadalo plynuleji.
Je třeba mít na paměti ještě jednu věc. Někdy je CPU přetíženo nebo existují jiné důvody pro méně časté překreslování (například když je karta prohlížeče skrytá), takže bychom to opravdu neměli spouštět každých 20ms
.
Ale jak o tom v JavaScriptu víme? Existuje specifikace Časování animace, která poskytuje funkci requestAnimationFrame
. Řeší všechny tyto problémy a ještě více.
Syntaxe:
let requestId = requestAnimationFrame(callback)
To naplánuje callback
funkci spustit v nejbližším čase, kdy chce prohlížeč provést animaci.
Pokud provedeme změny v prvcích v callback
pak budou seskupeny s ostatními requestAnimationFrame
zpětná volání a s animacemi CSS. Takže bude jeden přepočet geometrie a překreslení místo mnoha.
Vrácená hodnota requestId
lze použít ke zrušení hovoru:
// cancel the scheduled execution of callback
cancelAnimationFrame(requestId);
callback
dostane jeden argument – čas, který uplynul od začátku načítání stránky v milisekundách. Tento čas lze také získat voláním performance.now().
Obvykle callback
běží velmi brzy, pokud není přetížený procesor nebo baterie notebooku téměř vybitá nebo existuje jiný důvod.
Níže uvedený kód ukazuje čas mezi prvními 10 spuštěními pro requestAnimationFrame
. Obvykle je to 10–20 ms:
<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>
Strukturovaná animace
Nyní můžeme vytvořit univerzálnější animační funkci založenou na 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);
}
});
}
Funkce animate
přijímá 3 parametry, které v podstatě popisují animaci:
duration
-
Celková doba animace. Třeba
1000
. timing(timeFraction)
-
Funkce časování, jako je vlastnost CSS
transition-timing-function
to dostane zlomek času, který uplynul (0
na začátku1
na konci) a vrátí dokončení animace (jakoy
na Bezierově křivce).Například lineární funkce znamená, že animace probíhá rovnoměrně se stejnou rychlostí:
function linear(timeFraction) { return timeFraction; }
Jeho graf:
To je jako
transition-timing-function: linear
. Níže jsou uvedeny zajímavější varianty. draw(progress)
-
Funkce, která převezme stav dokončení animace a vykreslí jej. Hodnota
progress=0
označuje počáteční stav animace aprogress=1
– koncový stav.Toto je funkce, která ve skutečnosti vykresluje animaci.
Může přesunout prvek:
function draw(progress) { train.style.left = progress + 'px'; }
…Nebo udělejte cokoli jiného, můžeme animovat cokoli, jakýmkoli způsobem.
Pojďme animovat prvek width
z 0
na 100%
pomocí naší funkce.
Klikněte na prvek pro ukázku:
Resultanimate.jsindex.htmlfunction 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>
Kód:
animate({
duration: 1000,
timing(timeFraction) {
return timeFraction;
},
draw(progress) {
elem.style.width = progress * 100 + '%';
}
});
Na rozdíl od CSS animace zde můžeme udělat libovolnou funkci časování a jakoukoli kreslicí funkci. Funkce časování není omezena Bézierovými křivkami. A draw
může jít za hranice vlastností, vytvářet nové prvky, jako je animace ohňostroje nebo tak něco.
Funkce časování
Nejjednodušší funkci lineárního časování jsme viděli výše.
Podívejme se na více z nich. Vyzkoušíme si animace pohybu s různými funkcemi časování, abychom viděli, jak fungují.
Síla n
Pokud chceme animaci zrychlit, můžeme použít progress
v mocnině n
.
Například parabolická křivka:
function quad(timeFraction) {
return Math.pow(timeFraction, 2)
}
Graf:
Podívejte se v akci (klikněte pro aktivaci):
…Nebo kubická křivka nebo ještě větší n
. Zvýšením výkonu se zrychlí.
Zde je graf pro progress
v mocnině 5
:
V akci:
Oblouk
Funkce:
function circ(timeFraction) {
return 1 - Math.sin(Math.acos(timeFraction));
}
Graf:
Záda:střelba z luku
Tato funkce provádí „střelbu z luku“. Nejprve „natáhneme tětivu“ a poté „vystřelíme“.
Na rozdíl od předchozích funkcí závisí na dodatečném parametru x
, „koeficient pružnosti“. Je jím definována vzdálenost „natažení tětivy“.
Kód:
function back(x, timeFraction) {
return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x)
}
Graf pro x = 1.5
:
Pro animaci jej používáme s konkrétní hodnotou x
. Příklad pro x = 1.5
:
Odskok
Představte si, že pouštíme míč. Spadne dolů, pak se několikrát odrazí a zastaví se.
bounce
funkce dělá totéž, ale v opačném pořadí:„odrážení“ se spustí okamžitě. K tomu používá několik speciálních koeficientů:
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)
}
}
}
V akci:
Elastická animace
Další „elastická“ funkce, která přijímá další parametr x
pro „počáteční rozsah“.
function elastic(x, timeFraction) {
return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction)
}
Graf pro x=1.5
:
V akci pro x=1.5
:
Zvrat:snadnost*
Máme tedy sbírku časovacích funkcí. Jejich přímá aplikace se nazývá „easeIn“.
Někdy potřebujeme ukázat animaci v opačném pořadí. To se provádí pomocí transformace „easeOut“.
easeOut
V režimu „easeOut“ timing
funkce je vložena do obalu timingEaseOut
:
timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction)
Jinými slovy, máme „transformační“ funkci makeEaseOut
která přebírá „běžnou“ funkci časování a vrací kolem ní obal:
// accepts a timing function, returns the transformed variant
function makeEaseOut(timing) {
return function(timeFraction) {
return 1 - timing(1 - timeFraction);
}
}
Například můžeme vzít bounce
výše popsanou funkci a použijte ji:
let bounceEaseOut = makeEaseOut(bounce);
Odskok pak nebude na začátku, ale na konci animace. Vypadá ještě lépe:
Resultsstyle.cssindex.html#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>
Zde můžeme vidět, jak transformace změní chování funkce:
Pokud je na začátku efekt animace, jako je poskakování – zobrazí se na konci.
V grafu výše má pravidelný odraz červenou barvu a mírný odraz je modrý.
- Pravidelný odraz – objekt se odrazí zespodu a na konci prudce vyskočí nahoru.
- Po
easeOut
– nejprve vyskočí na vrchol, pak se tam odrazí.
easeInOut
Můžeme také ukázat efekt jak na začátku, tak na konci animace. Transformace se nazývá „easeInOut“.
Vzhledem k funkci časování vypočítáme stav animace takto:
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;
}
Kód obálky:
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);
V akci, 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>
Transformace „easeInOut“ spojuje dva grafy do jednoho:easeIn
(běžné) pro první polovinu animace a easeOut
(obrácené) – pro druhou část.
Efekt je jasně vidět, pokud porovnáme grafy easeIn
, easeOut
a easeInOut
z circ
funkce časování:
- Červená je běžná varianta
circ
(easeIn
). - Zelená –
easeOut
. - Modrá –
easeInOut
.
Jak vidíme, graf první poloviny animace je zmenšený easeIn
a druhá polovina je zmenšená easeOut
. V důsledku toho se animace spustí a skončí se stejným efektem.
Zajímavější „kreslení“
Místo přesunu prvku můžeme udělat něco jiného. Vše, co potřebujeme, je napsat správný draw
.
Zde je animovaný „skákající“ text:
Resultsstyle.cssindex.htmltextarea {
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>
Shrnutí
U animací, se kterými si CSS neumí dobře poradit, nebo u těch, které vyžadují přísnou kontrolu, může pomoci JavaScript. Animace JavaScriptu by měly být implementovány pomocí requestAnimationFrame
. Tato vestavěná metoda umožňuje nastavit funkci zpětného volání, která se spustí, když bude prohlížeč připravovat překreslení. Obvykle je to velmi brzy, ale přesný čas závisí na prohlížeči.
Když je stránka na pozadí, nedochází k žádnému překreslování, takže zpětné volání se nespustí:animace bude pozastavena a nebude spotřebovávat zdroje. To je skvělé.
Zde je pomocník animate
funkce pro nastavení většiny animací:
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);
}
});
}
Možnosti:
duration
– celkový čas animace v ms.timing
– funkce pro výpočet průběhu animace. Získá časový zlomek od 0 do 1, vrátí průběh animace, obvykle od 0 do 1.draw
– funkce pro kreslení animace.
Určitě bychom to mohli vylepšit, přidat další zvonky a píšťalky, ale JavaScriptové animace se nepoužívají na denní bázi. Jsou zvyklí dělat něco zajímavého a nestandardního. Takže byste chtěli přidat funkce, které potřebujete, když je potřebujete.
Animace JavaScriptu mohou používat jakoukoli funkci časování. Probrali jsme spoustu příkladů a transformací, aby byly ještě univerzálnější. Na rozdíl od CSS zde nejsme omezeni na Bezierovy křivky.
Totéž se týká draw
:můžeme animovat cokoli, nejen vlastnosti CSS.