Tvoříme slider obrázků pomocí HTML5 canvasu

Na internetu najdete milion a jedna verzí různých sliderů obrázků, které jsou implementovány napříč spektrem technologií a frameworků. Technologie dozrála do doby, kdy k vytvoření slideru nepotřebujete žádné knihovny, jen JavaScript a canvas, jen vanilla JavaScript.
Nálepky:
Komu je seriál určen
Tímto článkem začínáme seriál, který má dvě cílové skupiny:
- ti, kdo chtějí mít pěkný slider obrázků
- ti, kdo si chtějí něco s canvasem vyzkoušet
Cíl seriálu
Tento mini seriál v pár článcích provede čtenáře tvorbou slideru za pomoci canvasu. Nejprve se podíváme na samotné animace, kde bude brán zřetel hlavně na jednoduchost. Následně z animací sestavíme jednoduchý slider, který bude použitelný na webové prezentaci. Dalším cílem bude přepsání kódu tak, aby mohly animace běžet souběžně.
Implementace
Do psaní jsem se pouštěl s následujícími požadavky:
- Fade in na začátku
- Animace pohybu
- Cross fade mezi obrázky
Canvas
Prvně potřebujeme canvas, tak si jej přidáme do dokumentu.
<canvas id="myCanvas">No canvas MSG.</canvas>
Více informací o canvasu naleznete na MDN: Canvas, nebo zde na zdrojáku: Tag canvas.
Fade in
Fade animace je asi nejznámější. Tento přechod implementují snad všechny JS frameworky, které jsou zaměřeny mimo jiné i na UI. Z tohoto důvodu tímto přechodem začneme. Co to vlastně fade je? Není to nic jiného, než změna průhlednosti v čase.
Abychom mohli tuto animaci provést, potřebujeme znát tři věci: obrázek, canvas a délku animace. Vytvoříme si proto třídu, ve které si připravíme metodu pro nastavení konfigurace. Je to sice pro fade efekt značný overkill, ale s touto animací pouze začínáme.
var Anim = function() {
this.timeLog = [];
}
Anim.prototype.configure = function(imgUri,animDuration,canvas) {
this.imgUri = imgUri;
this.animDuration = animDuration;
this.canvas = document.getElementById(canvas);
return;
}
Samotný fade můžeme řešit pomocí property globalAplha, který se nám nabízí v 2d kontextu canvasu. Jak už napovídá název, bude mít něco společného s průhledností. Hned ze začátku tu mám menší varování: property globalAlpha opravdu není globální! (Toho využiji při cross Fade animaci.)
Samotná fade animace je smyčka, s těmito kroky:
- Vyčistíme canvas
- Nastavíme globalAplha
- Vykreslíme obrázek
Vyčištění canvasu
K tomu nám slouží metoda clearRect (mimo jíné), ta očekává 4 parametry : startX,startY,dX,dY.
Nastavení globalAlpha
Zde není co řešit, property je float, prostě jí nastavíme hodnotu v rozmezí 0-1.
Vykreslení obrázku
K vykreslení obrázku do kontextu slouží metoda drawImage(), ta přijímá 9 parametrů:
- Instance obrázku (html element)
- 4 parametry pro obrázek (startX,startY,dx,dy)
- 4 parametry pro kontext (startX,startY,dx,dy)
Řízení běhu
K řízení běhu použijeme funkci requestAnimationFrame, která se postará o volání dalšího kroku animace ve chvíli, kdy je to možné. Jejímu popisu se věnuje MDN.
Internet Explorer implementuje tuto funkci pouze ve verzi 10+, proto si musíme ještě napsat fallback pollyfill pro IE9 a starší. Existuje několik hotových polyfillů, já jsem si ale napsal svůj, z jednoho prostého důvodu: Nechci mít jako parametr dTime vysoké hodnoty unix timestamp, lépe se potom čte průběh animace při debugování. Samozřejmě nesmíme zapomenout na prefixy různých prohlížečů.
function(w){
"use strict";
w.requestAnimationFrame = requestAnimationFrame || mozRequestAnimationFrame
|| webkitRequestAnimationFrame || msRequestAnimationFrame ||
(function(){
var startTime = new Date().getTime();
return function(callback) {
var time = new Date().getTime();
var dTime = time - startTime;
window.setTimeout(callback.bind(callback,dTime),17)
return;
}
}())
}(window))
Číslo 17 není magická kontanta, velká část zobrazovacích zařízení funguje s frekvencí 60Hz, z tohoto důvodu je ideální tick animace 16.6ms, ale setTimeout přijímá integer.
Sestavení kódu
Naše třída má zatím pouze metodu pro předání konfigurace a konstruktor. Nyní si připravíme canvas, nastavíme mu globalAlpha na 0 a načteme si obrázek.
Anim.prototype.run = function() {
this.ctx = this.canvas.getContext('2d');
this.ctx.globalAlpha = 0;
this.img = new Image();
this.img.src = this.imgUri;
this.img.onload = this._beforeFade.bind(this,null);
return;
}
Předchozí metoda má nastavený callback pro onload event, ten směruje na metodu _beforeFade. Tato metoda zavolá requestAnimationFrame, abychom měli relativní čas začátku animace, a poté spustí samotnou animaci.
Anim.prototype._beforeFade = function(startTime) {
if(!startTime)
requestAnimationFrame(this._beforeFade.bind(this))
this.animStart = startTime;
requestAnimationFrame(this._fade.bind(this,this._afterFade.bind(this)))
return;
}
Samotná animace je pak pouze rekurzivní volání metody, která vyčistí canvas, nastaví globalAlpha a vykreslí obrázek.
Anim.prototype._fade = function(callback,dTime) {
this.timeLog.push(new Date().getTime());
var alpha = 1/this.animDuration * (dTime - this.animStart);
if(alpha > 1) alpha = 1;
this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);
this.ctx.globalAlpha = alpha;
this.ctx.drawImage(this.img,0,0,this.img.width,this.img.height
,0,0,this.canvas.width,this.canvas.height)
if(alpha == 1) {
callback();
} else {
requestAnimationFrame(this._fade.bind(this,callback))
}
return;
}
Nakonec se zavolá callback, který vypíše statistiku animace.
Anim.prototype._afterFade = function() {
var log = document.getElementById("log");
var tmpl = [];
var timeLog = this.timeLog;
tmpl.push("Počet kroků animace: ",timeLog.length,"<br/>");
tmpl.push("Čas animace v s: ", (timeLog[timeLog.length-1]-timeLog[0])/1000,"<br/>");
tmpl.push("Prum. fps: ",timeLog.length/((timeLog[timeLog.length-1]-timeLog[0])/1000));
log.innerHTML = tmpl.join("");
return;
}
Závěr a zdrojový kód
Tímto máme napsaný jednoduchou fade-in animaci za použití canvasu – podívejte se na výsledný kód. V dalším dílu se podíváme na animaci pohybu a zoomu a vytvoříme nelineární animaci.
paradni tutorial, jen tak dal :)
Ahoj,
1) v polyfillu se volá callback s časem, který odpovídá rozdílu mezi zavoláním rAF a inicializací polyfillu. Neměl by ten parametr spíš odpovídat rozdílu aktuálního času a toho inicializačního? Protože díky němu pak můžeme vyrovnat nerovnoměrnost časového kroku (časování rAF si řídí prohlížeč sám); v aktuálním řešení je „zpoždění“ způsobené
setTimeout
ignorováno.2) co přesně na
globalAlpha
není globální? Nastavená průhlednost se přeci aplikuje na všechny pixely ve všech operacích nad kontextem…1) Je pravda, že tento polyfill není úplný, ale pro potřeby těchto animací mi přijde naprosto dostačující. Je otázka, o kolik se bude lišit chyba třeba po 10^6 iteracích. Zkusím to a napíšu výsledek.
2) Od globální property by jsem očekával:
ctx.fillRect(...) ctx.globalAlpha = 0.5; // teď by tedy měl mít obdélník 50% průhlednost.
Místo toho se globalAlpha aplikuje pouze na následující operace s kontextem. (Jinak by ani nešel řešit třeba cross-fade efekt)
To by stejnou logikou mohl člověk očekávat, že po zavolání
ctx.fillStyle = "red"
se dříve černý obdélník přebarví na červenou. Vlastnosti (tuším, že tak se česky překládá „property“) jen nastavují parametry pro následné operace (metody), ale samy od sebe nic nevykreslují…Myslím, že tohle je spor o chápání slova globální. Soouhlasím s tím, že věta použitá v článku je nepřesná a matoucí. Autor upozorňuje, že nastavení globalAlpha nezmění průhlednost celého aktuálního canvasu (naopak by to tak bylo, kdybychom nastavili opacity pomocí CSS pro celý canvas), ake týká se následujících vykreslovacích metod. Z té použité formulace to není přesně znát.
Chápu, že se zde spíše jedná o slovíčkaření, ale to klíčové slovo global mě upřímně na začátku hodně zmátlo. protože to opravdu nemění globálně všemu průhlednost.
Mění to průhlednost odteď všemu, dokud neřeknu dost. :)
MDN říká: Alpha value that is applied to shapes and images before they are composited onto the canvas. Default 1.0 (opaque).
Ta property se opravdu jmenuje špatně, nazval bych ji fillAlpha, protože to je to, co opravdu dělá. Od globalAlpha by jsem očekával spíše následují: vezmi buffer co máš, a pixel po pixelu přepočítej průhledost.
O to nejde, jde o jitter – hlavne pri pohybu objektu (napr. slide zleva doprava) staci milisekundy k tomu aby nam prestal pohyb pripadat plynuly – a par milisekund je v prostredi kde treba jedno jadro uz vytezuje flash a k tomu browser ceka na disk kvuli nejakemu cache requestu opravdu lehke nabrat.
Nějak mě uniká, proč se používá canvas a ne nějaký normální html tag třeba IMG nebo obrázek v pozadí na DIV. Má canvas nějaké výhody ?
Canvas má v JavaScriptu vlastní API, které umožňuje získat kontext, který může být 2D (
CanvasRenderingContext2D
) nebo 3D (WebGLRenderingContext
)Jak už napsal Pavel, canvas je předurčen pro práci s grafikou, ať už pro práci v 2d režimu pro slider, nebo pro hry v 3d režimu například Unreal engine.
Tak nevím, zkusil jsem odkaz na „výsledný kód“ na PC (Chrome) a vše bylo OK. Pak jsem ale zkusil totéž na tabletu (iPad) a Safari ani Chrome se nechytali – zobrazili jen border okolo kanvasu a nic víc. Takže hezké, ale prakticky nepoužitelné…
Díky za upozornění, bude fajn si v dalších dílech odzkoušet i funkci na tabletech. Celkem věřím, že pokud je tam nyní nějaký problém, tak bude překonatelný.
Skvělý článek, moc jsi mi pomohl, ale jen tě chci upozornit, že (ale to samozřejmě je velice snadno přehlédnutelné) jsi napsal globalAplha místo globalAlpha.
Díky za reakci i za nahlášení chybky.
Jak jsem právě zjistil při ladění v IE9 (když jsem se divil, proč se animace nekoná, zato procesor jede na 100% ;), polyfill chybně volá callback funkci s parametrem dTime (rozdíl časů), správně ji má volat s aktuálním časem (viz definice funkce requestAnimationFrame).
Také volat new Date().getTime() mi přijde zbytečné, stačí Date.now().
Jinak díky za velice užitečný článek.