IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
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 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 CPU a energii.

Obnovovací frekvence se vyjadřuje jako počet snímků za sekundu (FPS – Frames Per Second), což je měřítko, které udává, 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.

Pojďme si nyní porovnat, jak bychom postupovali podle dosavadních znalostí a srovnejme je s novými možnostmi.

Staré řešení

Představme si, že tvoříme nějakou jednoduchou hru v prohlížeči. Pomocí funkce setInterval() by hlavní smyčka aplikace vypadala takto:

setInterval(function() {
    posun();
    vykresli();
}, 1000 / FPS);

Pomocí funkce posun() aktualizujeme pozici objektů ve hře a funkcí vykresli() je zobrazíme. Interval je vypočten jako 1000 milisekund děleno počtem snímků za sekundu. Tím zajišťujeme, že aktualizace animace probíhá v požadovaném tempu definovaném proměnnou FPS, kvůli plynulému zobrazení a efektivnímu využití zdrojů. Naše dosavadní animace vypadaly velmi podobně, pouze jsme spojili posouvání a vykreslování do jedné funkce.

Alternativní vykreslovací smyčka s funkcí setTimeout() vypadá takto:

function smycka() {
    posun();
    vykresli();
    setTimeout(smycka, 1000 / FPS);
}
smycka(); // Iniciuje animační smyčku

Tyto ukázky kódu demonstrují dvě tradiční techniky pro implementaci animační smyčky v JavaScriptu, které se používají pro tvorbu her v prohlížeči. Funkce setInterval() je nastavena tak, aby opakovaně volala funkce posun() a vykresli() v intervalu určeném počtem snímků za sekundu (FPS). Alternativní řešení používá funkci setTimeout(), která rekurzivně volá funkci smycka(), což také umožňuje aktualizaci polohy a vykreslení hry.

Plýtvání výkonem počítače

Kód tedy funguje, dokonce si můžeme i omezit FPS. Jeho problémem 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í.

Řešení pomocí metody requestAnimationFrame()

Použijeme-li metodu requestAnimationFrame() místo funkce setTimeout(), bude náš kód vypadat velmi podobně:

function smycka() {
    posun();
    vykresli();
    requestAnimationFrame(smycka);
}
requestAnimationFrame(smycka);

Takto jednoduše zajistíme, že naše animace budou synchronizovány s vykreslovacím cyklem prohlížeče a výkon CPU a GPU 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.

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.

Ukázková animace

Ukážeme si jednoduchou aplikaci, ve které se čtverec pohybuje po plátně. Implementujeme ji jak pomocí funkce setInterval(), tak pomocí metody requestAnimationFrame().

V HTML souboru si připravíme plátno:

<body>
    <canvas id="platno" width="500" height="300"></canvas>
</body>

Řešení s funkcí setInterval()

Z JavaScriptu v události onload získáme element plátna a nastavíme mu kontext. Dále si vytvoříme první objekt reprezentující čtverec. Všechny informace potřebné pro definici a manipulaci se čtvercem tak budeme mít uloženy na jednom místě. Objekt ctverec má vlastnosti určující jeho pozici (x, y) , rychlost (rychlostX, rychlostY), velikost (strana) a barvu (barva). V kódu si ještě připravíme funkci pro vykreslovací smyčku:

window.onload = function() {
    let platno = document.querySelector('#platno');
    let kontext = platno.getContext('2d');
    let ctverec = {
        x: 25,
        y: 25,
        rychlostX: -2,
        rychlostY: 2,
        strana: 50,
        barva: 'red'
    };

    function smycka() {
        posun();
        prekresli();
    }

    // Sem přidáme funkci posun() a překresli()
};

Nyní doplníme funkci posun(), v níž budeme nastavovat novou pozici čtverce k vykreslení:

function posun() {
    if (ctverec.x + ctverec.strana + ctverec.rychlostX > platno.width) ctverec.rychlostX *= -1;
    else if (ctverec.x + ctverec.rychlostX < 0) ctverec.rychlostX *= -1;

    if (ctverec.y + ctverec.strana + ctverec.rychlostY > platno.height) ctverec.rychlostY *= -1;
    else if (ctverec.y + ctverec.rychlostY < 0) ctverec.rychlostY *= -1;

    ctverec.x += ctverec.rychlostX;
    ctverec.y += ctverec.rychlostY;
}

V podmínkách kontrolujeme, zda se čtverec nepřiblížil k jednomu z okrajů plátna. Pokud ano, vynásobíme pomocí operátoru *= jeho rychlost v daném směru -1, čímž tento pohyb otočíme. Nakonec z parametrů rychlostX a rychlostY vypočítáme novou pozici.

V kódu jsme si mohli všimnout notace ctverec.x. Ta odkazuje na přístup k vlastnosti x objektu ctverec v JavaScriptu.

V naší ukázce zbývá doplnit vykreslovací funkci a nastavit interval vykreslování:

function prekresli() {
    kontext.clearRect(0, 0, platno.width, platno.height);
    kontext.fillStyle = ctverec.barva;
    kontext.fillRect(ctverec.x, ctverec.y, ctverec.strana, ctverec.strana);
}

setInterval(smycka, 1000 / 60);

Ve funkci prekresli() nejprve vymažeme celé plátno, poté nastavíme barvu čtverce a vykreslíme jej na nové pozici. Interval animace jsme nastavili na hodnotu 1000 / 60.

Ukažme si výsledek v prohlížeči:

Animace pomocí setInterval
localhost

Řešení s metodou requestAnimationFrame()

V uvedeném příkladu nyní nahradíme funkci setInterval() metodou requestAnimationFrame():

window.onload = function() {
    let platno = document.querySelector('#platno');
    let kontext = platno.getContext('2d');
    let ctverec = {
        x: 25,
        y: 25,
        rychlostX: -2,
        rychlostY: 2,
        strana: 50,
        barva: 'red'
    };

    function smycka() {
        posun();
        prekresli();
        requestAnimationFrame(smycka); // Tento řádek jsme doplnili
    }

    function posun() {
        if (ctverec.x + ctverec.strana + ctverec.rychlostX > platno.width) ctverec.rychlostX *= -1;
        else if (ctverec.x + ctverec.rychlostX < 0) ctverec.rychlostX *= -1;

        if (ctverec.y + ctverec.strana + ctverec.rychlostY > platno.height) ctverec.rychlostY *= -1;
        else if (ctverec.y + ctverec.rychlostY < 0) ctverec.rychlostY *= -1;

        ctverec.x += ctverec.rychlostX;
        ctverec.y += ctverec.rychlostY;
    }

    function prekresli() {
        kontext.clearRect(0, 0, platno.width, platno.height);
        kontext.fillStyle = ctverec.barva;
        kontext.fillRect(ctverec.x, ctverec.y, ctverec.strana, ctverec.strana);
    }

    requestAnimationFrame(smycka); // Tento řádek jsme upravili
};

Výsledek:

Animace pomocí requestAnimati­onFrame
localhost

Řešení s metodou requestAnimationFrame() a úpravou rychlosti

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 výkonnostmi. 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.

Doplnění proměnných a vlastností čtverce

Ukážeme si tedy ještě implementaci animace, která dynamicky upravuje rychlost objektů na základě času, který uplynul od posledního vykreslení. Toho docílíme tím, že si určíme jakýsi chtěný interval opakování (zakladniIntervalOpakovani) a následně při každém opakování měříme čas uběhnutý od posledního běhu. Pokud je čas kratší, než chtěný interval, tak podle poměru dobaOdPoslednihoOpakovani / zakladniIntervalOpakovani snížíme rychlost posunu a naopak. Nastavíme také maximální rychlost posunu, abychom při velkém zaseknutí aplikace zabránili možnosti, že čtverec vyjede mimo obrazovku.

Do původního kódu doplníme zmíněné proměnné a dvě nové vlastnosti objektu ctverec:

window.onload = function() {
    let platno = document.querySelector('#platno');
    let kontext = platno.getContext('2d');

    let zakladniIntervalOpakovani = 1000 / 60;
    let casPoslednihoOpakovani = 0;
    let dobaOdPoslednihoOpakovani = 0;
    let maxRychlost = 6;

    let ctverec = {
        x: 25,
        y: 25,
        rychlostX: -2,
        rychlostY: 2,
        strana: 50,
        barva: 'red',
        zakladniRychlost: 2,
        upravaRychlosti: 1
    };

    // ...
};

Funkce pro kontrolu rychlosti

Do funkce smycka() přidáme volání funkce upravRychlost(), kterou také doplníme:

function smycka() {
    upravRychlost();
    posun();
    prekresli();
    requestAnimationFrame(smycka);
}

function upravRychlost() {
    if (casPoslednihoOpakovani) {
        dobaOdPoslednihoOpakovani = Date.now() - casPoslednihoOpakovani;
        ctverec.upravaRychlosti = dobaOdPoslednihoOpakovani / zakladniIntervalOpakovani;
    }
    casPoslednihoOpakovani = Date.now();
}

Ve funkci upravRychlost() kontrolujeme, zda má proměnná casPoslednihoOpakovani jinou hodnotu než 0. Pokud ano, vypočítáme dobu, která uplynula od poslední aktualizace. Na základě tohoto časového intervalu a základního intervalu aktualizace nastavíme novou rychlost vykreslení. Nakonec do proměnné casPoslednihoOpakovani uložíme aktuální čas, aby mohl být použit pro další úpravu rychlosti.

Úprava výpočtu posunu a výpis FPS

Dále upravíme výpočet posunu. Do výpočtu zahrneme novou vlastnost čtverce upravaRychlosti, zajistíme, aby nebyla překročena maximální rychlost pohybu a přidáme kontrolu pozice čtverce, aby se nedostal mimo obrazovku:

function posun() {
    let posunX = ctverec.rychlostX * ctverec.upravaRychlosti;
    let posunY = ctverec.rychlostY * ctverec.upravaRychlosti;

    // Omezení rychlosti na maximální hodnotu
    posunX = Math.min(posunX, maxRychlost);
    posunY = Math.min(posunY, maxRychlost);

    if (ctverec.x + ctverec.strana + posunX > platno.width) ctverec.rychlostX *= -1;
    else if (ctverec.x + posunX < 0) ctverec.rychlostX *= -1;
    if (ctverec.y + ctverec.strana + posunY > platno.height) ctverec.rychlostY *= -1;
    else if (ctverec.y + posunY < 0) ctverec.rychlostY *= -1;
    ctverec.x += posunX;
    ctverec.y += posunY;

    // Nedovolíme čtverci vyjet mimo plátno
    ctverec.x = Math.max(0, Math.min(ctverec.x, platno.width - ctverec.strana));
    ctverec.y = Math.max(0, Math.min(ctverec.y, platno.height - ctverec.strana));
}

Nakonec ve funkci prekresli() doplníme výpis měřeného FPS:

function prekresli() {
    kontext.clearRect(0, 0, platno.width, platno.height);
    kontext.fillStyle = ctverec.barva;
    kontext.fillRect(ctverec.x, ctverec.y, ctverec.strana, ctverec.strana);
    kontext.font = '12px Arial';
    kontext.fillText('FPS: ' + Math.round(1000 / dobaOdPoslednihoOpakovani), 5, 10);
}

Výsledek:

Animace pomocí requestAnimati­onFrame
localhost

Ukázka s nižší rychlostí

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 smycka() 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 < 1000000; 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 509x (4.41 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript

 

Předchozí článek
Řešené úlohy k 27. 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í:
604 hlasů
Tento uživatelský účet již není aktivní na základě žádosti jeho majitele.
Aktivity