September discount week
Pouze tento týden sleva až 80 % na e-learning týkající se MS Office
50 % bodů zdarma na online výuku díky naší Slevové akci!

Lekce 18 - JS requestAnimationFrame - Za lepší vykreslování

V předešlém cvičení, Řešené úlohy k 17. lekci JavaScriptu, jsme si procvičili nabyté zkušenosti z předchozích lekcí.

Spolu s HTML5 přišlo nové javascriptové API - requestAnimationFrame(). Jedná se o technologii, která nám umožňuje plynuleji a s vyšším výkonem vykreslovat animace v prohlížeči a my si ji v tomto tutoriálu představíme.

Staré řešení

Pokud jste někdy zkoušeli napsat např. nějakou jednoduchou hru v prohlížeči, jistě vaše hlavní smyčka vypadala nějak takto.

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

Naše dosavadní animace vypadaly velmi podobně, pouze jsme spojili posouvání a vykreslování do jedné funkce. Alternativní vykreslovací smyčka může vypadat i např. takto:

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

Kód funguje, dokonce si můžeme i omezit FPS. Co je na něm ale špatně?

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

Problém výše uvedeného kódu je ten, že prohlížeč jej vykonává, i když se uživatel na danou stránku zrovna nedívá (má překliknuto na jinou záložku, případně je prohlížeč minimalizovaný). Prohlížeč Google Chrome tyto situace řeší omezením takovýchto smyček pouze na 1 FPS. To je ale pouze jeho dobrovolné chování a v ostatních prohlížečích to tak vůbec být nemusí, takže na mobilních zařízeních může docházet zbytečně k poklesu výkonu a vybíjení baterie.

requestAnimationFrame() to řeší!

S použitím funkce requestAnimationFrame() místo setTimeout() bude náš kód vypadat velmi podobně:

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

Tuto funkci vymyslela Mozilla a později ji převzal a vylepšil tým WebKitu. Pomocí funkce se může vykreslovat CSS, DOM elementy, WebGL nebo na Canvas.

Výhody

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

requestAnimationFrame() nám zajistí, že všechny naše animace se budou vykreslovat najednou, s vyšším výkonem a nižší spotřebou baterie.

Pokud v prohlížeči překliknete na jinou stránku, nebo prohlížeč minimalizujete, vykreslování se zastaví, aby se šetřil výkon a bude se pokračovat, jakmile bude stránka opět viditelná.

Mohli jste si také všimnout, že u použití requestAnimationFrame() jsme nikde nenastavovali počet FPS. Počet FPS závisí na konkrétní implementaci v prohlížeči a může být tedy v různých prohlížečích odlišný, dále závisí na výkonu PC nebo mobilního zařízení a také na obnovovací frekvenci monitoru.

Nejčastěji se setkáte s rychlostí 60 FPS, chybou je však na to v aplikaci spoléhat.

Demo

Ukážeme si jednoduchou aplikaci, ve které se čtverec pohybuje po Canvas a implementujeme jí jak pomocí setInterval(), tak pomocí requestAnimationFrame().

Řešení pomocí setInterval()

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();
    }

    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);
    }

    setInterval(smycka, 1000 / 60);
};

Výsledek:

Animace pomocí setInterval
localhost

Řešení pomocí requestAnimationFrame() a setInterval()

Všimněte se, že v příkladu jsou použity dvě smyčky:

  • setInterval() pro logiku a
  • requestAnimationFrame() pro vykreslování.

Pokud bychom použili jen requestAnimationFrame(), mohlo by se stát, že na některých sestavách poběží animace rychleji než v prvním příkladu. Šetříme tedy výkon při vykreslení a spouštíme stále stejně jen logiku:

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();
    }

    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(prekresli);
    }

    setInterval(smycka, 1000 / 60);
    prekresli();
};

Výsledek:

Animace pomocí requestAnimati­onFrame a setInterval
localhost

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

V předchozím příkladu jsme si ukázali, že rychlost volání requestAnimationFrame() nemáme tak úplně pod kontrolou. Záleží totiž na konkrétním zařízení, jaká má nastavená FPS (to může být 60, ale klidně i 144). Navíc kód 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í requestAnimationFrame() se o nějaký čas odsune.

Následují příklad poběží na všech zařízeních stejně rychle a to proto, že si měří čas 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ě každé opakování smycka() 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:

window.onload = function() {
    let platno = document.querySelector('#platno');
    let kontext = platno.getContext('2d');
    let zakladniIntervalOpakovani = 1000 / 60;
    let casPoslednihoOpakovani = 0;
    let dobaOdPoslednihoOpakovani = 0;
    let ctverec = {
        x: 25,
        y: 25,
        rychlostX: -2,
        rychlostY: 2,
        strana: 50,
        barva: 'black',
        zakladniRychlost: 2,
        upravaRychlosti: 1
    };

    function smycka() {
        // for (let i = 0; i < 100000000; i++) {}
        upravRychlost();
        posun();
        prekresli();
        requestAnimationFrame(smycka);
    }

    // Pokud je requestAnimationFrame() rychlejší nebo pomalejší
    // než předpokládáme, přizpůsobíme posun tak, aby byl vždy stejný.
    function upravRychlost() {
        if (casPoslednihoOpakovani) {
            dobaOdPoslednihoOpakovani = Date.now() - casPoslednihoOpakovani;
            ctverec.upravaRychlosti = dobaOdPoslednihoOpakovani / zakladniIntervalOpakovani;
        }
        casPoslednihoOpakovani = Date.now();
    }

    function posun() {
        let posunX = ctverec.rychlostX * ctverec.upravaRychlosti;
        let posunY = ctverec.rychlostY * ctverec.upravaRychlosti;
        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 posun mimo plátno
        if (ctverec.x < 0) ctverec.x = 0;
        else if (ctverec.x + ctverec.strana > platno.width) ctverec.x = platno.width - ctverec.strana;
        if (ctverec.y < 0) ctverec.y = 0;
        else if (ctverec.y + ctverec.strana > platno.height) ctverec.y = platno.height - ctverec.strana;
    }

    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);
    }

    smycka();
};

Výsledek:

Animace pomocí requestAnimati­onFrame
localhost

Jak vidíme, problematika optimalizovaného vykreslování není obecně jednoduchá, protože při velkém zaseknutí se může rychlost zvýšit tak, že čtverec vyjede mimo obrazovku.

Ukázka s nižší rychlostí

U předchozího příkladu odkomentujeme for cyklus, který v něm simuluje zpomalení programu. Rychlost posunu ale zůstane stejná:

function smycka() {
    for (let i = 0; i < 100000000; i++) {}
    upravRychlost();
    posun();
    prekresli();
    requestAnimationFrame(smycka);
}

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ů s setInterval() a uvidíte, že se animace na rozdíl od posledního řešení zpomalí.

Doufám, že vám byla lekce užitečná a že od teď budete pro animace v prohlížeči používat requestAnimationFrame() :)


 

Stáhnout

Staženo 91x (4.92 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript

 

Předchozí článek
Řešené úlohy k 17. lekci JavaScriptu
Všechny články v sekci
Základní konstrukce jazyka JavaScript
Článek pro vás napsal Neaktivní uživatel
Avatar
Jak se ti líbí článek?
13 hlasů
Tento uživatelský účet již není aktivní na základě žádosti jeho majitele.
Aktivity (17)

 

 

Komentáře

Avatar
Pavel Vosyka
Člen
Avatar
Pavel Vosyka:7.2.2014 22:18

Díky! To je mi novinka, určitě to použiju. Chtěl jsem to porovnat v chrome, ale jak už jsi říkal, on si i ten loop teda nějak pozastavuje, takže rozdíl jsem nezpozoroval. Tak jsem zapl IE11 a tam to neběží :D .. Alespoň mě to donutí nainstalovat jiné prohlížeče.. :) (mám teď zrovna čistou instalaci..)

Odpovědět
7.2.2014 22:18
"nikdy nepiš nic 2x"
Avatar
Odpovídá na Pavel Vosyka
Neaktivní uživatel:7.2.2014 22:45

Díky za pozitivní ohlas. Ale k tomu IE. IE11 to 100% umí, chyba je tedy někde v kódu nebo na mé straně. V Dartu jsem si zkoušel udělat jednoduchý pingpong s pomocí právě requestAnimati­onFrame a normálně to funguje (http://grelek.maweb.eu/pingpong (prosím, je to pouze demo)).

BTW - IE 11 a už ani IE 10 nejsou nezbytně špatné prohlížeče.

Editováno 7.2.2014 22:46
Odpovědět
7.2.2014 22:45
Neaktivní uživatelský účet
Avatar
dyžon
Člen
Avatar
dyžon:7.10.2018 9:33

super tutorial,
jsem samouk a tohle je výborná lekce pro začátek.
díky moc.

 
Odpovědět
7.10.2018 9:33
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
Patrik Pastor:28.3.2019 22:58

vysvetli mi prosim nekdo, tuto cast koud?:

function posun() {
if (ctverec.x + ctverec.sirka + ctverec.rychlost * ctverec.smerX > platno.width)
ctverec.smerX = -1;
if (ctverec.x + ctverec.rychlost * ctverec.smerX < 0)
ctverec.smerX = 1;
if (ctverec.y + ctverec.vyska + ctverec.rychlost * ctverec.smerY > platno.height)
ctverec.smerY = -1;
if (ctverec.y + ctverec.rychlost * ctverec.smerY < 0)
ctverec.smerY = 1;

 
Odpovědět
28.3.2019 22:58
Avatar
Patrik Pastor:30.3.2019 18:40

cau, chtel bych se zeptat, kdyz pouziju funkci requestAnimati­onFrame(smycka) ... pro jeste pod touto funkci (smycka()) jeste jednou spustim tuto funkci smycka(), jinymi slovy, proc se spousti dvakrat, kdyz uz ji mam v argumentu req...frame(smyc­ka)? to nestaci, musi se spoustet Jeste jednou? diky za odpoved

 
Odpovědět
30.3.2019 18:40
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 5 zpráv z 5.