Lekce 30 - Metoda requestAnimationFrame() pro lepší vykreslování v JS
V předešlém cvičení, Řešené úlohy k 29. lekci JavaScriptu, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
V rámci HTML5 byla v JavaScriptu představena nová metoda
requestAnimationFrame()
pro práci s animacemi. Tato metoda
umožňuje efektivnější a plynulejší vykreslování
animací v prohlížeči. V tomto tutoriálu se
podíváme na její použití a výhody.
Metoda
requestAnimationFrame()
Metoda requestAnimationFrame()
je zásadním prvkem pro
vytváření účinných animací na webových stránkách. Na rozdíl od
funkcí setTimeout()
a setInterval()
, které
spouštějí kód po určitém časovém intervalu, metoda
requestAnimationFrame()
synchronizuje animace s
obnovovacím cyklem prohlížeče. Tím se snižuje zbytečné
vykreslování a minimalizují se vizuální chyby, což je důležité u
animací s vysokým rozlišením nebo na zařízeních s různou
obnovovací frekvencí. Pomocí této metody vývojáři dosahují
plynulých animací, které zároveň šetří výkon procesoru (CPU) a
spotřebu energie.
Obnovovací frekvence se vyjadřuje jako počet snímků za sekundu (FPS – Frames Per Second), což je měřítko udávající, kolik snímků je vykresleno na obrazovce za jednu sekundu. Vyšší FPS znamená, že zobrazení bude plynulejší. Rychlost snímkování je důležitá pro hladký vizuální zážitek ve videohrách, filmu a videu. Nízké FPS může vést k nežádoucím efektům jako je rozmazání obrazu.
Počet FPS může být v různých prohlížečích odlišný, závisí také na výkonu PC nebo mobilního zařízení a na obnovovací frekvenci monitoru. Nejčastěji se setkáme s rychlostí 60 FPS, chybou je však na to v aplikaci spoléhat.
Kolo štěstí
Ukážeme si jednoduchou animaci, ve které vytvoříme kolo štěstí.
Nejprve si ji implementujeme pomocí funkce setInterval()
jak jsme
zvyklí, posléze její kód upravíme tak, aby používal modernější způsob
pomocí requestAnimationFrame()
.
Animaci vytvoříme tentokrát na plátno, kde budeme v určitém intervalu celé plátno mazat a znovu vykreslovat.
Kolo štěstí pomocí funkce
setInterval()
Stáhneme si obrázek níže a vložíme jej do nového projektu:

Do těla HTML souboru pak obrázek a plátno přidáme:
<!DOCTYPE html> <html lang="cs-cz"> <head> <meta charset="UTF-8" /> <title>Kolo štěstí</title> <script src="kolo.js"></script> </head> <body> <img src="kolo.png" id="kolo" /> <canvas id="platno" width="500" height="500"></canvas> </body> </html>
V projektu vytvoříme soubor kolo.js
a plátno s obrázkem si
načteme do proměnných:
let platno; let kontext; let obrazek; let otoceni = 0; document.addEventListener("DOMContentLoaded", function() { platno = document.getElementById("platno"); kontext = platno.getContext("2d"); obrazek = document.getElementById("kolo"); obrazek.style.display = "none"; });
K proměnným jsme přidali ještě proměnnou otoceni
, kde
máme uložený úhel otočení nastavený na hodnotu 0
. Obrázek
jsme na stránce skryli.
V uvedeném kódu jsme původní obrázek pro ilustraci skryli
pomocí CSS vlastnosti style.display
. Stejného efektu bychom
dosáhli také zavoláním metody removeChild(obrazek)
na rodiči,
tedy na document.body
.
Funkce prekresli()
Dále v souboru vytvoříme funkci prekresli()
a umístíme ji
na konec našeho JavaScriptového souboru, mimo metodu
addEventListener()
:
function prekresli() { kontext.clearRect(0, 0, platno.width, platno.height); kontext.save(); kontext.translate(platno.width / 2, platno.height / 2); kontext.rotate(otoceni); kontext.drawImage(obrazek, -obrazek.width / 2, -obrazek.height / 2); kontext.restore(); otoceni += (2 * Math.PI) / 360; }
V této funkci nejprve mažeme plátno metodou clearRect()
a
kontext ukládáme metodou save()
. Poté ho metodou
translate()
zarovnáváme na střed a otáčíme o úhel uložený
v proměnné voláním kontext.rotate(otoceni)
. Na takto upravený
kontext vykreslujeme obrázek se středem zarovnaným do středu plátna. Toho
dosahujeme nastavením jeho souřadnic na zápornou hodnotu poloviny jeho
šířky a výšky. Tento přístup zajistí, že kód bude fungovat i při
změně velikosti plátna nebo obrázku. Po vykreslení obnovujeme původní
stav kontextu metodou restore()
a zvýšujeme hodnotu proměnné
otoceni
o jeden stupeň, což je v radiánech
(2 * Math.PI) / 360
.
Nyní se vrátíme do těla metody addEventListener()
a nejprve
zde zavoláme funkci prekresli()
, abychom nečekali na uplynutí
prvního intervalu. Poté nastavíme opakované volání vykreslovací funkce s
krátkým intervalem. Na konec metody doplníme tedy následující dva
řádky:
prekresli(); setInterval(prekresli, 20);
Plýtvání výkonem počítače
Kód funguje, dokonce si můžeme omezit FPS změnou frekvence animace v
metodě setInterval()
. Jeho problémem ale je, že ho prohlížeč
vykonává, i když se uživatel na danou stránku zrovna
nedívá - má překliknuto na jinou záložku nebo je okno
prohlížeče minimalizované. Google Chrome tyto situace řeší omezením
takovýchto smyček pouze na 1 FPS. Je to však jeho dobrovolné chování a v
ostatních prohlížečích nebo na mobilních zařízeních to tak vůbec být
nemusí.
Abychom spuštěnou animaci mohli přerušit, například když uživatel
opustí záložku prohlížeče, musíme nejprve nahradit původní volání
funkce setInterval()
jejím uložením do proměnné:
let interval = setInterval(prekresli, 20);
Samotného přerušení animace pak docílíme pomocí funkce
clearInterval()
. Do těla již vytvořeného posluchače
addEventListener()
vložíme druhý s následujícím kódem:
window.addEventListener("beforeunload", function() { clearInterval(interval); });
Funkce clearInterval()
jako parametr bere naší proměnnou
obsahující spuštěnou animaci. Protože chceme animaci ukončit při
opuštění stránky, voláme tuto funkci v obsluze události
beforeunload
.
Výsledek:
Kolo štěstí s metodou
requestAnimationFrame()
V případě, že budeme chtít použít metodu
requestAnimationFrame()
, náš kód bude vypadat podobně:
let platno; let kontext; let obrazek; let otoceni = 0; document.addEventListener("DOMContentLoaded", function() { platno = document.getElementById("platno"); kontext = platno.getContext("2d"); obrazek = document.getElementById("kolo"); obrazek.style.display = "none"; // Start animace requestAnimationFrame(prekresli); }); function prekresli() { kontext.clearRect(0, 0, 500, 500); kontext.save(); kontext.translate(250, 250); kontext.rotate(otoceni); kontext.drawImage(obrazek, -obrazek.width / 2, -obrazek.height / 2); kontext.restore(); otoceni += (2 * Math.PI) / 360; // Zavolání dalšího snímku requestAnimationFrame(prekresli); }
Funkci setInterval()
jsme nahradili voláním
requestAnimationFrame()
. Abychom dosáhli efektu animace, museli
jsme upravit i funkci prekresli()
, kam jsme na její konec přidali
rekurzivní volání. Funkce prekresli()
tak opakovaně volá sama
sebe, čímž vytváří efekt animace. Volání clearInterval()
už není potřeba, protože requestAnimationFrame()
si
vykreslování při přepnutí na jinou záložku nebo minimalizaci okna
koriguje automaticky.
Výstup v prohlížeči:
Takto jednoduše zajistíme, že naše animace budou synchronizovány s vykreslovacím cyklem prohlížeče a výkon procesoru a grafické karty bude využit efektivně. Snížíme také spotřebu baterie. Pokud nyní v prohlížeči překlikneme na jinou stránku nebo prohlížeč minimalizujeme, vykreslování se zastaví, aby se šetřil výkon. Animace bude pokračovat, jakmile bude stránka opět viditelná.
Metodě requestAnimationFrame()
jsme nikde nenastavovali počet
FPS. Obnovovací frekvenci animace v tomto případě
řídí prohlížeč automaticky na základě vlastního
obnovovacího cyklu.
Úprava rychlosti animace
V předchozím příkladu však nemáme rychlost volání animace úplně pod
kontrolou. Záleží totiž na konkrétním zařízení a jeho obnovovací
frekvenci. I když má FPS ve většině případů hodnotu 60, může mít
klidně i 144. Navíc záleží i na výpočetní složitosti naší aplikace.
Snadno se nám může stát, že se nějaký kód bude vykonávat příliš
dlouho a volaní metody requestAnimationFrame()
se o nějaký čas
odsune.
Vývojáři webových aplikací tedy často narazí na výzvu, jak zajistit, aby animace běžely konzistentně napříč různými zařízeními s rozdílnými obnovovacími frekvencemi a s rozdílným výkonem. Klasické metody mohou vést k různým rychlostem animace v závislosti na těchto faktorech. Řešením je doplnit do kódu výpočet pro úpravu rychlosti. Tento přístup umožňuje animaci běžet s konzistentní rychlostí nezávisle na FPS prohlížeče nebo zátěži systému.
K našim stávajícím proměnným přidáme čtyři nové:
let platno; let kontext; let obrazek; let otoceni = 0; let zakladniIntervalOpakovani = 1000 / 60; let casPoslednihoOpakovani = 0; let dobaOdPoslednihoOpakovani = 0; let upravaRychlosti = 1;
Pojďme si vysvětlit, k čemu nám jednotlivé proměnné budou sloužit:
zakladniIntervalOpakovani
- Ukládá ideální čas mezi snímky (při 60 FPS je to 1000/60 ms), tedy čas, kterého se našimi úpravami budeme snažit dosáhnout.casPoslednihoOpakovani
- Uchovává čas posledního vykreslení snímku.dobaOdPoslednihoOpakovani
- Uchovává uplynulý čas od posledního snímku.upravaRychlosti
- Hodnota upravující rychlost animace.
Do kódu (mimo addEventListener()
a funkci
prekresli()
) doplníme funkci sloužící k úpravě rychlosti:
function upravRychlost() { if (casPoslednihoOpakovani) { dobaOdPoslednihoOpakovani = Date.now() - casPoslednihoOpakovani; upravaRychlosti = dobaOdPoslednihoOpakovani / zakladniIntervalOpakovani; } casPoslednihoOpakovani = Date.now(); }
V podmínce se ptáme, jestli už proběhl alespoň jeden cyklus animace. V
případě, že je v proměnné casPoslednihoOpakovani
uložena
výchozí hodnota, tedy nula, přeskočí se výpočet rozdílu času a do
proměnné se dosadí aktuální čas.
V případě, že animace již probíhá, vypočítá se časový rozdíl
mezi aktuálním časem a časem posledního snímku. Tento rozdíl pak slouží
k úpravě rychlosti animace. Aby byla přizpůsobena aktuálnímu výkonu
systému nebo obnovovací frekvenci - dělíme jej hodnotou v
zakladniIntervalOpakovani
a ukládáme do proměnné
upravaRychlosti
. Tu pak použijeme ke zrychlení nebo zpomalení
animace.
Ve funkci prekresli()
přepíšeme způsob otáčení. Původní
změnu otáčení o jeden stupeň v radiánech ještě vynásobíme naší
proměnnou upravaRychlosti
:
otoceni += ((2 * Math.PI) / 360) * upravaRychlosti;
Do funkce prekresli()
doplníme na její začátek volání
funkce upravRychlost()
. Níže přidáme také výpis aktuálního
FPS. Upravená funkce vypadá takto:
function prekresli() { upravRychlost(); kontext.clearRect(0, 0, platno.width, platno.height); kontext.save(); kontext.translate(platno.width / 2, platno.height / 2); kontext.rotate(otoceni); kontext.drawImage(obrazek, -obrazek.width / 2, -obrazek.height / 2); kontext.restore(); otoceni += ((2 * Math.PI) / 360) * upravaRychlosti; kontext.font = '16px Arial'; kontext.fillStyle = 'black'; kontext.fillText('FPS: ' + Math.round(1000 / dobaOdPoslednihoOpakovani), 10, 20); requestAnimationFrame(prekresli); }
Hodnota FPS je vypočítána jako počet milisekund, které uplynuly od
posledního snímku (dobaOdPoslednihoOpakovani
), převedených na
snímky za sekundu (1000 ms = 1 sekunda). Výsledek je zaokrouhlený na celé
číslo a vykreslený na souřadnice (10
, 20
) na
plátně pomocí nastaveného písma a barvy.
Tím jsme docílili toho, že animace poběží na všech zařízeních stejnou rychlostí:
Snížení rychlosti aplikace
Zpomalení programu může být běžným problémem v interaktivních
aplikacích, zejména když jsou na stránce prováděny náročné výpočty
nebo operace. Zkusme si zpomalení našeho programu simulovat. Uvidíme, jak
metoda requestAnimationFrame()
zachovává plynulost animace, i
když samotný program běží pomaleji.
Na začátek funkce prekresli()
přidáme následující řádek
s for
cyklem, který způsobí zpomalení programu. Rychlost posunu
ale zůstane stejná:
for (let i = 0; i < 10000000; i++) {}
Tuto ukázku si vyzkoušejte u sebe, aby zbytečně nezpomalovala tuto stránku se všemi dalšími příklady. Můžete si zkusit zpomalovací cyklus přidat i do předchozích příkladů a uvidíte, že se animace na rozdíl od posledního řešení zpomalí.
V příští lekci, Nejčastější chyby JS začátečníků, děláš je také?, si ukážeme nejčastější chyby
začátečníků v JavaScriptu, např. ohledně pojmenování kolekcí,
Boolean
výrazů a DRY.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 697x (617.19 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript