IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

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:

Animations using setInterval
localhost

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:

Animations using requestAnimati­onFrame
localhost

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:

Animations using requestAnimati­onFrame
localhost

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é 3x (3.91 kB)
Aplikácia je vrátane zdrojových kódov v jazyku JavaScript

 

Predchádzajúci článok
Riešené úlohy k 28. lekcii JavaScriptu
Všetky články v sekcii
Základné konštrukcie jazyka JavaScript
Preskočiť článok
(neodporúčame)
Najčastejšie chyby JS začiatočníkov, robíš ich tiež?
Článok pre vás napísal Neaktivní uživatel
Avatar
Užívateľské hodnotenie:
4 hlasov
Tento uživatelský účet již není aktivní na základě žádosti jeho majitele.
Aktivity