NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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:

Kolo štěstí - Základní konstrukce jazyka JavaScript

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í
localhost

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:

Kolo štěstí
localhost

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í:

Kolo štěstí
localhost

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

 

Předchozí článek
Řešené úlohy k 29. lekci JavaScriptu
Všechny články v sekci
Základní konstrukce jazyka JavaScript
Přeskočit článek
(nedoporučujeme)
Nejčastější chyby JS začátečníků, děláš je také?
Článek pro vás napsal Neaktivní uživatel
Avatar
Uživatelské hodnocení:
985 hlasů
Tento uživatelský účet již není aktivní na základě žádosti jeho majitele.
Aktivity