Lekce 28 - Metoda requestAnimationFrame() pro lepší vykreslování v JS
V předešlém cvičení, Řešené úlohy k 27. 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" hidden /> <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 kontext; let obrazek; let otoceni = 0; document.addEventListener("DOMContentLoaded", function() { const platno = document.getElementById("platno"); kontext = platno.getContext("2d"); obrazek = document.getElementById("kolo"); });
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 pomocí atributu hidden.
Kdybychom na atribut hidden zapomněli, obrázek
bychom mohli skrýt pomocí CSS vlastnosti
obrazek.style.display = "none";. Stejného efektu bychom dosáhli
také zavoláním metody removeChild(obrazek) na rodiči, tedy na
document.body.
Rotace
Abychom mohli objekt na plátně otáčet, využijeme transformace 2D kontextu canvasu. Canvas neotáčí přímo objekt, ale souřadnicový systém, ve kterém se objekt vykresluje.
Použijeme následující metody kontextu:
save()– Uloží aktuální stav kontextu (včetně transformací).translate()– Posune počátek souřadnic. Před vykreslením kola posuneme počátek do jeho středu, aby se rotace prováděla kolem středu kola.rotate()– Otočí souřadnicový systém kolem aktuálního počátku. Úhel se zadává v radiánech. Díky předchozímu posunu se kolo bude otáčet kolem svého středu.restore()– Obnoví původní stav kontextu. Tím se zruší posun i rotace, aby neovlivnily další kreslení.
Funkce prekresli()
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 vymažeme celé plátno pomocí metody
clearRect() a uložíme aktuální stav kontextu metodou
save(). Následně kontext pomocí metody translate()
přesuneme do středu plátna a otočíme ho o úhel uložený v proměnné
otoceni voláním kontext.rotate(otoceni). Na takto transformovaný
kontext vykreslíme obrázek tak, aby jeho střed ležel ve středu plátna,
čehož dosáhneme posunutím jeho souřadnic o zápornou polovinu jeho šířky
a výšky. Tento postup zajistí, že vykreslování bude správně fungovat i
při změně rozměrů plátna nebo obrázku. Po vykreslení obnovíme původní
stav kontextu metodou restore() a zvýšíme hodnotu proměnné
otoceni o jeden stupeň, který je vyjádřen v radiánech jako
(2 * Math.PI) / 360.
Nyní se vrátíme do těla obslužné funkce události
addEventListener() a nejprve zde jednorázově zavoláme funkci
prekresli(), aby se první vykreslení provedlo okamžitě a nebylo
nutné čekat na uplynutí intervalu. Následně, pomocí metody
setInterval() zajistíme její opakované volání, které bude
řídit průběh animace. Na konec této části kódu proto doplníme
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é:
const 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 kontext; let obrazek; let otoceni = 0; document.addEventListener("DOMContentLoaded", function() { const 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 kontext; let obrazek; let otoceni = 0; const 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 866x (618.71 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript

