29. diel - Metóda requestAnimationFrame() pre lepšie vykresľovanie v JS
V predchádzajúcom cvičení, Riešené úlohy k 28. lekcii JavaScriptu, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.
V rámci HTML5 bola v JavaScripte predstavená nová metóda
requestAnimationFrame()
pre prácu s animáciami. Táto metóda
umožňuje efektívnejšie a plynulejšie vykresľovanie
animácií v prehliadači. V tomto tutoriále sa
pozrieme na jej použitie a výhody.
Metóda
requestAnimationFrame()
Metóda requestAnimationFrame()
je zásadným prvkom pre
vytváranie účinných animácií na webových stránkach. Na rozdiel od
funkcií setTimeout()
a setInterval()
, ktoré
spúšťajú kód po určitom časovom intervale, metóda
requestAnimationFrame()
synchronizuje animácie s
obnovovacím cyklom prehliadača. Tým sa znižuje zbytočné
vykresľovanie a minimalizujú sa vizuálne chyby, čo je dôležité pri
animáciách s vysokým rozlíšením alebo na zariadeniach s rôznou
obnovovacou frekvenciou. Pomocou tejto metódy vývojári dosahujú
plynulé animácie, ktoré zároveň šetria výkon CPU a
energiu.
Obnovovacia frekvencia sa vyjadruje ako počet snímok za sekundu (FPS – Frames Per Second), čo je merítko, ktoré udáva, koľko snímok je vykreslených na obrazovke za jednu sekundu. Vyššia FPS znamená, že zobrazenie bude plynulejšie. Rýchlosť snímkovania je dôležitá pre hladký vizuálny zážitok vo videohrách, filme a videu. Nízke FPS môže viesť k nežiaducim efektom ako je rozmazanie obrazu.
Poďme si teraz porovnať, ako by sme postupovali podľa doterajších znalostí a porovnajme ich s novými možnosťami.
Staré riešenie
Predstavme si, že tvoríme nejakú jednoduchú hru v prehliadači. Pomocou
funkcie setInterval()
by hlavná slučka aplikácie vyzerala
takto:
setInterval(function() { shift(); draw(); }, 1000 / FPS);
Pomocou funkcie shift()
aktualizujeme pozíciu objektov v hre a
funkciou draw()
ich zobrazíme. Interval je vypočítaný ako 1000
milisekúnd vydelených počtom snímok za sekundu. Tým zaisťujeme, že
aktualizácia animácie prebieha v požadovanom tempe definovanom premennou
FPS
, kvôli plynulému zobrazeniu a efektívnemu využitiu zdrojov.
Naše doterajšie animácie vyzerali veľmi podobne, iba sme spojili posúvanie
a vykresľovanie do jednej funkcie.
Alternatívna vykresľovacia slučka s funkciou setTimeout()
vyzerá takto:
function loop() { shift(); draw(); setTimeout(loop, 1000 / FPS); } loop(); // Initiates an animation loop
Tieto ukážky kódu demonštrujú dve tradičné techniky na implementáciu
animačnej slučky v JavaScripte, ktoré sa používajú na tvorbu hier v
prehliadači. Funkcia setInterval()
je nastavená tak, aby
opakovane volala funkcia shift()
a draw()
v intervale
určenom počtom snímok za sekundu (FPS). Alternatívne riešenie používa
funkciu setTimeout()
, ktorá rekurzívne volá funkciu
loop()
, čo taktiež umožňuje aktualizáciu polohy a vykreslenie
hry.
Plytvanie výkonom počítača
Kód teda funguje, dokonca si môžeme aj obmedziť FPS. Jeho problémom je, že ho prehliadač vykonáva, aj keď sa užívateľ na danú stránku práve nepozerá. Má prekliknuté na inú záložku alebo je okno prehliadača minimalizované. Google Chrome tieto situácie rieši obmedzením takýchto slučiek iba na 1 FPS. Je to však jeho dobrovoľné správanie a v ostatných prehliadačoch alebo na mobilných zariadeniach to tak vôbec byť nemusí.
Riešenie pomocou
metódy requestAnimationFrame()
Ak použijeme metódu requestAnimationFrame()
namiesto funkcie
setTimeout()
, bude náš kód vyzerať veľmi podobne:
function loop() { shift(); draw(); requestAnimationFrame(loop); } requestAnimationFrame(loop);
Takto jednoducho zaistíme, že naše animácie budú synchronizované s vykresľovacím cyklom prehliadača a výkon CPU a GPU bude využitý efektívne, znížime aj spotrebu batérie. Pokiaľ teraz v prehliadači preklikneme na inú stránku alebo prehliadač minimalizujeme, vykresľovanie sa zastaví, aby sa šetril výkon. Animácia bude pokračovať, akonáhle bude stránka opäť viditeľná.
Metóde requestAnimationFrame()
sme nikde nenastavovali počet
FPS. Obnovovaciu frekvenciu animácie v tomto prípade
riadi prehliadač automaticky na základe vlastného
obnovovacieho cyklu.
Počet FPS môže byť v rôznych prehliadačoch odlišný, závisí aj od výkonu PC alebo mobilného zariadenia a od obnovovacej frekvencie monitora. Najčastejšie sa stretneme s rýchlosťou 60 FPS, chybou je však na to v aplikácii spoliehať.
Ukážková animácia
Ukážeme si jednoduchú aplikáciu, v ktorej sa štvorec pohybuje po
plátne. Implementujeme ju ako pomocou funkcie setInterval()
, tak
pomocou metódy requestAnimationFrame()
.
V HTML súbore si pripravíme plátno:
<body> <canvas id="canvas" width="500" height="300"></canvas> </body>
Riešenie s funkciou
setInterval()
Z JavaScriptu v udalosti onload
získame element plátna a
nastavíme mu kontext. Ďalej si vytvoríme prvý objekt reprezentujúci
štvorec. Všetky informácie potrebné na definíciu a manipuláciu so
štvorcom tak budeme mať uložené na jednom mieste. Objekt square
má vlastnosti určujúce jeho pozíciu (x
, y
) ,
rýchlosť (speedX
X , speedY
), veľkosť
(side
) a farbu (color
). V kóde si ešte pripravíme
funkciu pre vykresľovaciu slučku:
window.onload = function() { let canvas = document.querySelector('#canvas'); let context = canvas.getContext('2d'); let square = { x: 25, y: 25, speedX: -2, speedY: 2, side: 50, color: 'red' }; function loop() { shift(); redraw(); } // Here we add the function shift() and redraw() };
Teraz doplníme funkciu shift()
, v ktorej budeme nastavovať
novú pozíciu štvorca na vykreslenie:
function shift() { if (square.x + square.side + square.speedX > canvas.width) square.speedX *= -1; else if (square.x + square.speedX < 0) square.speedX *= -1; if (square.y + square.side + square.speedY > canvas.height) square.speedY *= -1; else if (square.y + square.speedY < 0) square.speedY *= -1; square.x += square.speedX; square.y += square.speedY; }
V podmienkach kontrolujeme, či sa štvorec nepriblížil k jednému z
okrajov plátna. Pokiaľ áno, vynásobíme pomocou operátora *=
jeho rýchlosť v danom smere -1
, čím tento pohyb otočíme.
Nakoniec z parametrov speedX
a speedY
vypočítame
novú pozíciu.
V kóde sme si mohli všimnúť notáciu square.x
.
Tá odkazuje na prístup k vlastnosti x
objektu square
v JavaScripte.
V našej ukážke zostáva doplniť vykresľovaciu funkciu a nastaviť interval vykresľovania:
function redraw() { context.clearRect(0, 0, canvas.width, canvas.height); context.fillStyle = square.color; context.fillRect(square.x, square.y, square.side, square.side); } setInterval(loop, 1000 / 60);
Vo funkcii redraw()
najprv vymažeme celé plátno, potom
nastavíme farbu štvorca a vykreslíme ho na novej pozícii. Interval animácie
sme nastavili na hodnotu 1000 / 60
.
Ukážme si výsledok v prehliadači:
Riešenie s metódou
requestAnimationFrame()
V uvedenom príklade teraz nahradíme funkciu setInterval()
metódou requestAnimationFrame()
:
window.onload = function() { let canvas = document.querySelector('#canvas'); let context = canvas.getContext('2d'); let square = { x: 25, y: 25, speedX: -2, speedY: 2, side: 50, color: 'red' }; function loop() { shift(); redraw(); requestAnimationFrame(loop); // We have added this line } function shift() { if (square.x + square.side + square.speedX > canvas.width) square.speedX *= -1; else if (square.x + square.speedX < 0) square.speedX *= -1; if (square.y + square.side + square.speedY > canvas.height) square.speedY *= -1; else if (square.y + square.speedY < 0) square.speedY *= -1; square.x += square.speedX; square.y += square.speedY; } function redraw() { context.clearRect(0, 0, canvas.width, canvas.height); context.fillStyle = square.color; context.fillRect(square.x, square.y, square.side, square.side); } requestAnimationFrame(loop); // We have edited this line };
Výsledok:
Riešenie
s metódou requestAnimationFrame()
a úpravou rýchlosti
V predchádzajúcom príklade však nemáme rýchlosť volania animácie
úplne pod kontrolou. Záleží totiž na konkrétnom zariadení a jeho
obnovovacej frekvencii. Aj keď má FPS vo väčšine prípadov hodnotu
60
, môže mať pokojne aj 144
. Navyše záleží aj
na výpočtovej zložitosti našej aplikácie. Ľahko sa nám môže stať, že
sa nejaký kód bude vykonávať príliš dlho a volanie metódy
requestAnimationFrame()
sa o nejaký čas odsunie.
Vývojári webových aplikácií teda často narazia na výzvu, ako zabezpečiť, aby animácie bežali konzistentne naprieč rôznymi zariadeniami s rozdielnymi obnovovacími frekvenciami a výkonnosťami. Klasické metódy môžu viesť k rôznym rýchlostiam animácie v závislosti od týchto faktorov. Riešením je doplniť do kódu výpočet na úpravu rýchlosti. Tento prístup umožňuje animáciu bežať s konzistentnou rýchlosťou nezávisle od FPS prehliadača alebo záťaži systému.
Doplnenie premenných a vlastností štvorca
Ukážeme si teda ešte implementáciu animácie, ktorá dynamicky upravuje
rýchlosť objektov na základe času, ktorý uplynul od posledného
vykreslenia. To docielime tým, že si určíme akýsi chcený interval
opakovania (baseRecurrenceInterval
) a následne pri každom
opakovaní meriame čas ubehnutý od posledného behu. Pokiaľ je čas kratší,
než chcený interval, tak podľa pomeru
timeSinceLastRecurrence / baseRecurrenceInterval
znížime
rýchlosť posunu a naopak. Nastavíme aj maximálnu rýchlosť posunu, aby sme
pri veľkom zaseknutí aplikácie zabránili možnosti, že štvorec vyjde mimo
obrazovku.
Do pôvodného kódu doplníme spomínané premenné a dve nové vlastnosti
objektu square
:
window.onload = function() { let canvas = document.querySelector('#canvas'); let context = canvas.getContext('2d'); let baseRecurrenceInterval = 1000 / 60; let lastRecurrenceTime = 0; let timeSinceLastRecurrence = 0; let maxSpeed = 6; let square = { x: 25, y: 25, speedX: -2, speedXY: 2, side: 50, color: 'red', baseSpeed: 2, speedAdjustment: 1 }; // ... };
Funkcie pre kontrolu rýchlosti
Do funkcie loop()
pridáme volanie funkcie
adjustSpeed()
, ktorú tiež doplníme:
function loop() { adjustSpeed(); shift(); redraw(); requestAnimationFrame(loop); } function adjustSpeed() { if (lastRecurrenceTime) { timeSinceLastRecurrence = Date.now() - lastRecurrenceTime; square.speedAdjustment = timeSinceLastRecurrence / baseRecurrenceInterval; } lastRecurrenceTime = Date.now(); }
Vo funkcii adjustSpeed()
kontrolujeme, či má premenná
lastRecurrenceTime
inú hodnotu ako 0
. Ak áno,
vypočítame dobu, ktorá uplynula od poslednej aktualizácie. Na základe tohto
časového intervalu a základného intervalu aktualizácie nastavíme novú
rýchlosť vykreslenia. Nakoniec do premennej lastRecurrenceTime
uložíme aktuálny čas, aby mohol byť použitý pre ďalšiu úpravu
rýchlosti.
Úprava výpočtu posunu a výpis FPS
Ďalej upravíme výpočet posunu. Do výpočtu zahrnieme novú vlastnosť
štvorca speedAdjustment
, zaistíme, aby nebola prekročená
maximálna rýchlosť pohybu a pridáme kontrolu pozície štvorca, aby sa
nedostal mimo obrazovky:
function shift() { let shiftX = square.speedX * square.speedAdjustment; let shiftY = square.speedY * square.speedAdjustment; // Limiting the speed to the maximum value shiftX = Math.min(shiftX, maxSpeed); shiftY = Math.min(shiftY, maxSpeed); if (square.x + square.side + shiftX > canvas.width) square.speedX *= -1; else if (square.x + shiftX < 0) square.speedX *= -1; if (square.y + square.side + shiftY > canvas.height) square.speedY *= -1; else if (square.y + shiftY < 0) square.speedY *= -1; square.x += shiftX; square.y += shiftY; // We will not allow the square to go outside the canvas square.x = Math.max(0, Math.min(square.x, canvas.width - square.side)); square.y = Math.max(0, Math.min(square.y, canvas.height - square.side)); }
Nakoniec vo funkcii redraw()
doplníme výpis meraného FPS:
function redraw() { context.clearRect(0, 0, canvas.width, canvas.height); context.fillStyle = square.color; context.fillRect(square.x, square.y, square.side, square.side); context.font = '12px Arial'; context.fillText('FPS: ' + Math.round(1000 / timeSinceLastRecurrence ), 5, 10); }
Výsledok:
Ukážka s nižšou rýchlosťou
Spomalenie programu môže byť bežným problémom v interaktívnych
aplikáciách, najmä keď sú na stránke vykonávané náročné výpočty
alebo operácie. Skúsme si spomalenie nášho programu simulovať. Uvidíme,
ako metóda requestAnimationFrame()
zachováva plynulosť
animácie, aj keď samotný program beží pomalšie.
Na začiatok funkcie loop()
pridáme nasledujúci riadok s
for
cyklom, ktorý spôsobí spomalenie programu. Rýchlosť posunu
ale zostane rovnaká:
for (let i = 0; i < 1000000; i++) {}
Túto ukážku si vyskúšajte pri sebe, aby zbytočne nespomaľovala túto stránku so všetkými ďalšími príkladmi. Môžete si skúsiť spomaľovací cyklus pridať aj do predchádzajúcich príkladov a uvidíte, že sa animácia na rozdiel od posledného riešenia spomalí.
V budúcej lekcii, Najčastejšie chyby JS začiatočníkov, robíš ich tiež? , si ukážeme najčastejšie chyby
začiatočníkov v JavaScripte, napr. ohľadom pomenovania kolekcií,
Boolean
výrazov a DRY.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 4x (3.91 kB)
Aplikácia je vrátane zdrojových kódov v jazyku JavaScript