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