Tvoříme slider obrázků pomocí HTML5 canvasu – animace pohybu, zoom a nelineární animace

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:
Předchozí díl byl letmým úvodem do canvasu, popsali jsme si základy práce a vytvořili jsme jednoduchou fade-in animaci.
V tomto druhém díle musím prvně vyřešit oprávněné námitky k fallback polyfillu funkce requestAnimationFrame od čtenářů předchozího dílu. Poté se podíváme na animaci pohybu, zoomu a nelineární animaci.
RequestAnimationFrame
Odchylka, způsobená prodlevami při vysokém vytížení prohlížeče (málo výkonné zařízení, flash, příliš mnoho js …), může vyústit v neplynulost animace, proto bude lepší použít polyfill, který s tímto počítá. Použil jsem proto mírně upravený polyfill od Paula Irishe.
Pohyb
Nejelegantnější způsob, jak řešit pohyb, je změna středu. Na rozdíl od umísťování výřezu nemusíme řešit poměry stran canvasu a obrázku. Co vlastně chceme od této animace? Aby se střed obrázku startX, startY změnil za dTime na endX, endY.
Abychom mohli tuto animaci provést, potřebujeme znát následující:
- Souřadnice počátku startX, startY
- Souřadnice konce endX, endY
- Délku animace duration
var config = [
2220 // startX
, 1700 // startY
, 1820 // endX
, 1850 // endY
, 3000 // duration[ms]
];
Kód pro tuto animaci bude velmi podobný předchozímu, jen se změní konstruktor a metoda, která vykresluje obrázek.
Konstruktor
Jediná změna oproti předchozímu příkladu je, že místo předávání duration předáme pole o 5 parametrech.
Anim.prototype.configure = function(imgUri,config,canvas) {
this.imgUri = imgUri;
this.config = config;
this.canvas = document.getElementById(canvas);
return;
}
Animace
V předchozím díle jsem zvolil nešťastně názvy metod (preFade, Fade, postFade), proto je přejmenuji na preAnim, anim, postAnim.
Metoda pro vykreslení má jako paramerty callback a dTime, který nám předává funkce requestAnimationFrame. Pro další příklady (zejména nelineární animace) chceme, aby tato metoda byla funkcí času. To by nám tento jednoduchý příklad komplikovalo, proto si tuto hodnotu dopočítáme přímo v této metodě:
var fTime = (dTime - this.animStart) / this.config[4];
Teď již potřebujeme pouze dopočítat souřadnice pro funkci drawImage canvasu.
Funkce drawImage nepracuje se středem, proto musíme dopočítat souřadnice pro vykreslování, což není nic jiného než počátek + přírůstek – rozměr canvasu / 2.
Počátek jsme předali konstruktoru, přírůstek je funkce času rozdílu počátku a konce, rozměr canvasu taky víme.
var coords = [
this.config[0] - this.canvas.width / 2 + (this.config[0] - this.config[2]) * fTime
, this.config[1] - this.canvas.height / 2 + (this.config[1] - this.config[3]) * fTime
];
Celá metoda potom bude vypadat takto:
Anim.prototype._anim = function (callback, dTime) {
this.timeLog.push(new Date().getTime());
var fTime = (dTime - this.animStart) / this.config[4];
if (fTime > 1) {
fTime = 1;
}
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
var coords = [
this.config[0] - this.canvas.width / 2 + (this.config[0] - this.config[2]) * fTime
, this.config[1] - this.canvas.height / 2 + (this.config[1] - this.config[3]) * fTime
];
this.ctx.drawImage(
this.img
, coords[0], coords[1]
, this.canvas.width, this.canvas.height
, 0, 0
, this.canvas.width, this.canvas.height
);
if (fTime == 1) {
callback();
} else {
requestAnimationFrame(this._anim.bind(this, callback));
}
return;
}
Zdrojový kód
Na výsledný kód se můžete podívat zde.
Zoom
Zoom je další z jednoduchých animací, na kterou se v tomto díle podíváme. Co je to Zoom? Je to změna měřítka v čase.
Abychom mohli tuto animaci provést, potřebujeme znát následující:
- Souřadnice středu obrázku centerX, centerY
- Počáteční měřítko startScale
- Koncové měřítko endScale
- Dobu animace duration
var config = [
2220 // centerX
, 1700 // centerY
, 1 // startScale
, 1.5 // endScale
, 3000 // duration[ms]
];
Konstruktor se nám nezmění, pouze budou jiné prvky pole, které předáváme.
Animace
Předchozí animace vykreslovaly obrázek v měřítku 1:1, proto byl třetí a čtvrtý parametr pro funkci drawImage rozměr canvasu. Při této animaci již nelze toto použít, proto bude mít pole coords 4 prvky, a to: startX, startY, sumX, sumY.
Výpočet počátku je jednoduchý, jako základ použijeme předchozí příklad a přidáme korekci měřítka.
var zoom = this.config[2] + fTime * (this.config[3] - this.config[2]);
Celá metoda poté bude vypadat takto:
Anim.prototype._anim = function (callback,dTime) {
this.timeLog.push(new Date().getTime());
var fTime = (dTime - this.animStart) / this.config[4];
if (fTime > 1) {
fTime = 1;
}
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
var zoom = this.config[2]+ fTime*(this.config[3] - this.config[2]);
var coords = [
this.config[0] - 1.5*this.canvas.width + (this.canvas.width*zoom)
, this.config[1] - 1.5*this.canvas.height + (this.canvas.height*zoom)
, this.canvas.width - 2*(this.canvas.width*zoom - this.canvas.width)
, this.canvas.height - 2*(this.canvas.height*zoom - this.canvas.height)
];
this.ctx.drawImage(
this.img
, coords[0], coords[1]
, coords[2], coords[3]
, 0, 0
, this.canvas.width, this.canvas.height);
if (fTime == 1) {
callback();
} else {
requestAnimationFrame(this._anim.bind(this, callback))
}
return;
}
Zdrojový kód
Na výsledný kód se můžete podívat zde.
Nelineární animace
V předchozích dvou příkladech jsme změnu stavu počítali jako funkci času s intervalem <0,1>. Není nic jednoduššího než předat naší třídě funkci, která se postará o výpočet potřebného.
Použijme například rovnici:
y = 0.1 (11 - cos x)
Využijeme interval <0,π>, proto x z rovnice bude náš relativní čas fTime * π. (Goniometrické funkce v Math knihovně pracují s radiány). Funkce, kterou předáme v konfiguraci, proto bude vypadat takto:
function (fTime) {
return 0.1 * (11 - Math.cos(Math.PI * fTime));
}
Výsledná metoda bude prakticky stejná jako v předchozím případě, jen se změní výpočet měřítka.
if (typeof this.config[2] === 'function') {
zoom = this.config[2](fTime);
} else {
zoom = this.config[2] + fTime * (this.config[3] - this.config[2]);
}
Takto napsaná animace působí plynulejším dojmem, na začátku zrychluje a na konci brzdí. Navíc nám tento callback umožňuje provádět prakticky jakékoli animace, stačí si jen připravit potřebnou funkci.
Zdrojový kód
Na výsledný kód se můžete podívat zde.
Závěrem
Z předchozího útržku kódu je jasné, že se při každé iteraci zbytečně kontroluje typ třetího indexu pole (počítáme od nuly), při předání callbacku je další parametr null
. Navíc takto napsaný skript neumožňuje spouštět více animací.
Takto předané konfigurace se zbavíme v příštím díle, kdy si začneme připravovat kód pro slider, tím rovnou vyřešíme problém s typovou kontrolou v iteraci a zbavíme se do očí bijícího null
.
Tie ukazky nefunguju na mojom iPade.
Podle dokumentace by mělo Safari umět vše, co je v sc, je tam k dispozici nějaké chybové hlášení?
Chybová hláška nie ja žiadna, zobrazi sa obdĺžnik s červeným rámom (ako na desktope), ale bez obsahu, teda bez obrázku. Po chvílke vypíše počet krokov animácie 355, čas anim: 3sec, proste ako na klasickom desktope (FF), ale obrázok nevidím žiaden. To isté robí aj iPhone IOS 7.0.5, Safari. A práve som skúsil aj Chrome pre IOS, výsledok ten istý.
Tvári sa to, že to niečo robilo, ale samotný obrázok nie je vidno.
Kolega to zkusil na iPhonu a taky mu to tam neběží. Michale, zkus připravit verzi, která bude podrobněji logovat, co se dějě, ať to rozlouskneme. Doporučuji hlavně zkontrolovat, zda proběhne načtení obrázku (to by mohlo na mobilu trvat logicky déle, ale kolegovi se stránka spustila hned, jakoby se na načtení obrázku nečekalo), tj.
this.img.onload = this._beforeAnim.bind(this,null);
a následně co se vlastně děje okolodrawImage
(zda se to vůbec volá, případně co to vykresluje, zda to není jen bílý prázdný pixel).Uvidíme, přidám nějakou testovací ukázku do příštího dílu. vypadá to ale, že Safari vadí nastavení src před definováním event listeneru.
Ve Firefox 27 je animace dosti priserna – mam na mysli tento odkaz http://ukazky.zdrojak.cz/slider/02/nonlinear.html
V Chrome vse ok, jeste dodam, ze testovano na OS Windows 7
Zkusil jsem to teď na buildu 27.0.1 a vše je v pořádku. Otázkou také je, co znamená příšerná, z toho nevyvodím, co se může dít.
Ted jsem to znovu zkusil v 27.0.1 a cela animace neprobiha plynule, ale tese se obraz nahoru dolu o nekolik pixelu, navic obrazek behem animace nedrzi v tom ohraniceni. Bliz uz ti to asi popsat nedokazu ;)
Tento článek mi připomněl výbornou sérii článků o kinetickém skrolování v javascriptu:
http://ariya.ofilabs.com/2013/08/javascript-kinetic-scrolling-part-1.html
http://ariya.ofilabs.com/2013/11/javascript-kinetic-scrolling-part-2.html
http://ariya.ofilabs.com/2013/12/javascript-kinetic-scrolling-part-3.html
http://ariya.ofilabs.com/2013/12/javascript-kinetic-scrolling-part-4.html
http://ariya.ofilabs.com/2014/01/javascript-kinetic-scrolling-part-5-cover-flow-effect.html
Technologie dozrála natolik, že nepotřebujete nejen knihovny a frameworky, ale prakticky ani JavaScript, ani vanilla ani jiný, dnes máme CSS Animations, případně (na jednodušší věci) Transitions.