HTML5:Canvas e animazioni

Nel precedente post sul Canvas ho descritto l'utilizzo del nuovo tag HTML5 e le API JavaScript che permettono di programmarlo. Per riassumere quello che abbiamo discusso nel precedente post:
- il Canvas fornisce una superficie di disegno immediato
- gli sviluppatori Web utilizzano le API JS esposte dal Canvas per disegnare varie primitive grafiche direttamente sulla superfice del Canvas stesso

Cercherò ora di introdurre le modalità con cui è possibile gestire con JS delle animazioni sfruttando il context 2D del Canvas. La gestione delle animazioni con il Canvas può essere effettuata attraverso la modifica diretta del suo contenuto, sfruttando delle funzioni JS richiamate ad intervalli specifici, con una frequenza determinata in base al frame rate che si vuole ottenere nella gestione del ridisegno del movimento. Si procede in pratica ad intervalli con:

- cancellare l'area canvas coinvolta

- ridisegnare l'elemento nella nuova posizione

Di seguito un semplice esempio di animazione dove abbiamo una pagina contenente un canvas e una funzione draw che ogni 10 millisecondi ridisegna il canvas, producendo un'animazione di scorrimento di una immagine.

Il processo di avvio dell'intervallo di ridisegno, viene eseguito solo dopo il load dell'immagine per avere tutti gli elementi, scaricati localmente e quindi utilizzabili per l'esecuzione dell'animazione.

<!DOCTYPE html>

<html lang="en">
<head>
<title title="test anim"></title>
<meta charset="utf-8" />

   <script language="javascript" type="text/javascript">
var myCanvas, ctx;
var x1=10,y1=10, distance=5;
var img;
var imgWidth;
var imgHeight;

  window.addEventListener("DOMContentLoaded", Main, false);

function Main(){

myCanvas = document.getElementById("myCanvas");

if (myCanvas.getContext) {
ctx = myCanvas.getContext("2d");
img = new Image();

img.addEventListener("load", function(){

imgWidth = 100;
imgHeight = 100;

lastFrameTime = new Date().getTime();

setInterval(draw, 16);
}, false);

img.src = "IELogo.png";
}
}

   
function draw() {

y1 += distance;

// make image show up again when it falls of the page
if (y1 > (ctx.canvas.height - imgHeight)) {
y1 = 10;
}

ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillText("Sample Animation", x1, 10);
ctx.drawImage(img, x1, y1, imgWidth, imgHeight);

}

</script>
</head>
<body>

<canvas id="myCanvas" width="500" height="500">
Canvas is not supported.
</canvas>
</body>
</html>

 Come si vede dal codice contenuto nella funzione draw, viene usata una variabile per mantenere la posizione dell'ultimo ridisegno all'interno del canvas e, dopo aver cancellato il canvas, con le API del context 2D si ridisegna l'elemento nella nuova posizione. Richiamando la funzione draw con un intervallo di 16 millisecondi , si produce una animazione con teoricamente 60 frame al secondo, dipendente ovviamente dalla velocità con cui il browser esegue la funzione di ridisegno. E' anche possibile applicare delle trasformazioni usando le API specifiche del canvas (rotate, translate, transform ) durante un'animazione per gestire il funzionamento della stessa.

Oltre alla precedente modalità di cancellazione e riscrittura, le API del canvas offrono la possibilità di gestire lo stato del contenuto prima di applicare un nuovo disegno. Attraverso i metodi save e restore è possibile gestire lo stato e salvarlo prima di applicare una trasformazione e con save, riportarlo nello stato precedente, funzione molto utile nel caso in cui si debba, ad esempio, ridisegnare più elementi in una animazione. Ad esempio se si devono disegnare più elementi in sequenza si può utilizzare questa funzionalità:

 for(var a=1;i<10;i++)
{
ctx.save();
ctx.translate(a,a);
drawSomething();
ctx.restore();
}

Nel caso di IE9 nelle animazioni di immagini, il ridisegno delle stesse sfrutta le capacità della GPU e consente di ottenere delle per eccellenti, sfruttando intervalli di ridisegno che consentono di ottenere un elevatissimo numero di frame al secondo con una eccellente esperienza utente ed un ottima fluidità nelle animazioni. Un bel esempio di animazioni lo trovate nel Test Drive Center come ad esempio l'applicazione fishIETank e Galactic . Anche nel già citato Canvas Pad trovate un esempio di animazione utile per approfondire l'utilizzo del canvas per questo tipo di operazione.

Una delle fasi dell'animazione è quello di rendere una immagine (o una cella singola animazione) nella posizione specifica e ci sono due principali tecniche di calcolo della posizione specifica. Queste tecniche di animazione sono:
- Animazione frame-based
- Animazioni time-based

Nell'animazione frame-based oggetti vengono spostati lo stesso numero di pixel in ogni aggiornamento. Nelle time-based lo spostamento è basato sul tempo e vengono spostati lo stesso numero di pixel per unità di tempo. Ognuno dei due approcci ha vantaggi e svantaggi ed è più idoneo in determinati scenari. 

 L'animazione frame-based è semplice da implementare ma è fortemente dipendente dalla velocità del PC così come lo spazio percorso dall'animazione non è costante e per tanto questo tipo di tecnica è poco usata nei giochi e di più per animazioni che non hanno dei percorsi di dimensione predefinita in una specifica unità di tempo. Di seguito un semplice esempio di frame-base animation, dove ctx è il context 2d del canvas, che in questo caso cancella completamente il canvas perchè abbiamo un unico oggetto presente nell'animazione:

var distance=5 lastFrameTime = 0;
setInterval(drawTimeBased, 16);

function drawFrameBased() {

y1 += distance;

ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, x1, y1, imgWidth, imgHeight);
}

Di seguito abbiamo un esempio di questo tipo di animazione  applicata ad un canvas sovrapposto ad un video, che illustra la flessibilità con cui il canvas può essere usato anche per gestire animazioni in overlay ad un video, come ad esempio banner pubblicitari o altri elementi interattivi:

  <!DOCTYPE html>
<html>
<head>

</head>
<body>

<video id="myVideo" src="fish.mp4" height="480" width="640"autoplay controls loop>
Video is not supported.
</video>

<canvas id="myCanvas" height="480" width="640" style="left: 0px; top: 0px; position: absolute;" >
Canvas is not supported
</canvas>
<script language="javascript">
window.addEventListener("load", Main, false);
var y1 = 10, distance = 1;
var customText = "Canvas text on top of video";
var ctx;
var msg;
function Main() {

        msg = document.getElementById("msg");
myCanvas = document.getElementById("myCanvas");
if (myCanvas.getContext) {
ctx = myCanvas.getContext("2d");
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 5;
ctx.shadowColor = "grey";
ctx.fillStyle = "black";
ctx.font = '35px "Segoe UI" bold';

            setInterval(draw, 16);
}

    }
function draw() {

        if (distance < 0) {
distance = 0;
}
y1 += distance;

        // make text show up again when it falls of the page
if (y1 > (ctx.canvas.height - 35)) {
y1 = 10;
}

        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        ctx.fillText(customText, 60, y1);

}
 

</script>
</body>
</html>

L'Animazione time-based è più complicata da implementare. In questo tipo di animazione la distanza per spostare una specifica immagine è calcolato su l'unità di tempo. Questo tipo di animazione non è dipendente della macchina e può essere usata nei giochi. Nei giochi in genere tutti i giocatori hanno la stessa quantità di tempo da spendere per il certo livello, a prescindere macchina utilizzata per giocare una partita - quindi abbiamo bisogno di animazione indipendente macchina. La fluidità dell'animazione è dipendente dalle performance del PC su cui si esegue ma le distanze sono costanti nell'unità di tempo. Ecco di seguito un esempio di codice di utilizzo di un'animazione time-based:

var speed = 250, lastFrameTime = 0;
setInterval(drawTimeBased, 16);

function drawTimeBased() {
// time since last frame
var now = new Date().getTime();
dt = (now - lastFrameTime) / 1000;
lastFrameTime = now;

y2 += speed * dt;

ctx.clearRect(x2, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, x2, y2, imgWidth, imgHeight);
}

In questo caso l'oggetto non viene spostato di una distanza predeterminata ad ogni intervallo di ridisegno dell'animazione, ma viene invece calcolato lo spostamento, in base al tempo trascorso dal precedente ciclo di rendering.
Ricordando che la distanza è uguale velocità moltiplicata per il tempo, che la velocità è una costante che abbiamo fissato da qualche parte nel codice (250 pixel nel nostro caso), e che  dt è calcolato in ciascun ciclo, possiamo facilmente calcolare la distanza necessaria per muovere il nostro oggetto in ogni step dell'animazione. Tutto quello che dobbiamo fare è moltiplicare la velocità per il delta del tempo, ottenendo i pixel da applicare al movimento.

Di seguito un semplice esempio delle due tipologie di animazione applicate sullo stesso canvas a coordinate differenti:

<!DOCTYPE html>

<html lang="en">

  <head>
<title title="test anim"></title>
<meta charset="utf-8" />

  <script language="javascript" type="text/javascript">
var myCanvas, ctx;
var x1=10,y1=10,x2=200,y2=10, distance=5, speed = 250;
var img;
var imgWidth;
var imgHeight;

var lastFrameTime = 0;
  

  window.addEventListener("load", Main, false);

function Main(){

myCanvas = document.getElementById("myCanvas");

if (myCanvas.getContext) {
ctx = myCanvas.getContext("2d");
img = new Image();

img.addEventListener("load", function(){

imgWidth = 100;
imgHeight = 100;

lastFrameTime = new Date().getTime();

setInterval(draw, 16);
}, false);

img.src = "IELogo.png";
}
}

function draw() {
drawFrameBased();
drawTimeBased();
}

function drawFrameBased() {

y1 += distance;

// make image show up again when it falls of the page
if (y1 > (ctx.canvas.height - imgHeight)) {
y1 = 10;
}

ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillText("Frame Based Animation", x1, 10);
ctx.drawImage(img, x1, y1, imgWidth, imgHeight);

}

function drawTimeBased() {

// time since last frame
var now = new Date().getTime();
dt = (now - lastFrameTime) / 1000;
lastFrameTime = now;

y2 += speed * dt;

// make image show up again when it falls of the page
if (y2 > (ctx.canvas.height - imgHeight)) {
y2 = 10;
}

ctx.clearRect(x2, 0, ctx.canvas.width-(x1+imgWidth), ctx.canvas.height);
ctx.fillText("Time Based Animation", x2, 10);
ctx.drawImage(img, x2, y2, imgWidth, imgHeight);

}

  </script>
</head>
<body>
<canvas id="myCanvas" width="500" height="500">
Canvas is not supported.
</canvas>
</body>
</html>

 Come potete vedere nell'esempio precedente le due animazioni vengono eseguite assieme ed in questo caso, viene cancellato prima del ridisegno solo la porzione del canvas che contiene la posizione dell'elemento su cui si esegue l'animazione, utilizzando il metododo del context clearRect  utilizzando come parametri le specifiche coodinate e dimensioni .

Nel caso di animazioni complesse con elementi che durante il movimento , simulano anche altri tipi di movimento come ad esempio l'applicazione FishIETank che simula un acquario con i pesci che si muovono nello spazio e che cambiano direzione e muovono la coda per simulare il nuoto:

o come il bellissimo esempio\gioco implementato in HTML5 Pirates Love Daises :

Dove ci sono vari personaggi animati su canvas.

Per gestire animazioni di questo tipo e garantire le migliori performance possibili, sfruttando in particolare su IE9 l'accelerazione grafica, si utilizzano genericamente delle tecniche ( sprite animation ) che accorpano in un unica immagine gli stati degli elementi durante le animazioni, che vengono suddivisi in celle con delle specifiche coordinate, utilizzate poi nelle funzioni che ridisegnano gli oggetti.

 Ad esempio nel caso dell'acquario del FishIETank abbiamo, una immagine di background che viene applicata al canvas e una immagine in cui sono contenuti tutti gli stati necessari all'animazione dei pesci e le diverse "razze" di pesci che vengono poi divise in celle che permettono attraverso le coordinate definite di ridisegnare una specifica cella sul canvas:

   
 

 Di seguito un esempio della funzione che sintetizza l'utilizzo delle immagini all'interno dell'animazione:

<img id="imageStrip" src="fishstrip.png" style="display: none" tabIndex="-1">

// rif allo strip di img
imageStrip = document.getElementById('imageStrip');

ctx.drawImage (

// informazioni source image

FishImageStrip, // source
fishW * cell, // sX della cella
fishH * species, // SY della cella
fishW, // larghezza della cella
fishH, // altezza della cella

// Canvas destinazione
x, // X dove disegnare il pesce
y, // Y dove disegnare il pesce
fishW, // larghezza
fishH, // altezza
);

 Come si vede nel codice precedente, viene caricata l'intera immagine è poi utilizzate le coordinate della cella per indicare la porzione di immagine (cella) da disegnare nel canvas di destinazione ad una specifica coordinata, con delle dimensioni definite.

 Il particolare nell'applicazione FishIETank dell'acquario viene essenzialmente:

1-Caricato ed impostato lo sfondo dell'acquario

2- caricata l'intera sprite image

3- applicare al canvas le trasformazioni per disegnare il pesce, salvando prima lo stato del canvas, per riapplicarlo ripetendo poi le operazioni per ogni pesce

Questa demo di acquario, utilizza una animazione Time-Based. Di seguito un esempio semplificato della funzione di disegno dei pesci:

var myCanvas = document.getElementById('myCanvas');
var ctx = myCanvas.getContext("2d");

ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.transform(flip, 0, 0, 1, 0, 0);
ctx.drawImage(imageStrip, fishW * cell, fishH * species, fishW, fishH, -fishW / 2, -fishH / 2, fishW, fishH);
scale = nextScale;
ctx.restore();

Proprio come ho spiegato prima, viene salvato lo stato del canvas, applicata la trasformazione e disegnata la cella corretta in base allo stato dell'animazione e quindi ripristinato lo stato. Viene applicata anche una trasformazione personalizzata dove il primo parametro (flip) è 1 o -1 serve per eventualmente riflettere pesce in una direzione diversa in base alla direzione che il pesce ha nell'acquario. Le trasformazioni nel canvas in IE9 vengono svolte sfruttando completamente la GPU con l'accelerazione grafica offerta dal browser, garantendo delle perfomance eccellenti nell'animazione.

Approfondendo il funzionamento dell'applicazione nel test drive di IE9, trovate di seguito una porzione del codice della applicazione fishIETank che potete anche trovare online :

//set up the canvas
var tempCtx = document.createElement("canvas");
tempCtx.id = "canvas1";
tempCtx.setAttribute('width', WIDTH);
tempCtx.setAttribute('height', HEIGHT);
tempCtx.setAttribute('tabIndex', -1);
document.body.insertBefore(tempCtx, document.body.firstChild);

            var tempCtx3 = document.createElement("canvas");
tempCtx3.id = "background";
tempCtx3.setAttribute('width', WIDTH);
tempCtx3.setAttribute('height', HEIGHT);
tempCtx3.setAttribute('tabIndex', -1);
document.body.insertBefore(tempCtx3, document.body.firstChild);

            ctx = tempCtx.getContext("2d");

            ctx3 = tempCtx3.getContext("2d");

            //draw the background
backgroundImage = document.getElementById('backgroundImage');
drawBackground();

            //create the fish
imageStrip = document.getElementById('imageStrip');
createFish(startFish);

            //start animation
setInterval(function () { draw(); }, 16);

window.addEventListener("resize", OnWindowResize, false);

Vengono creati i canvas utilizzati nella parte grafica, impostato il background dell'acquario e creati gli oggetti che rappresentano i pesci ed avviato l'intervallo che chiama la funzione che si occupa di ridisegnare l'acquario e implementare quindi le animazioni. La funzione draw essenzialmente effettua un ciclo nella collezione di oggetti che rappresentano i pesci, creati nella funzione createFish e che consarvano lo stato in cui si trovano all'interno dell'acquario e chiama il metodo swim, che fa essenzialmente ridisegnare l'acquario:

function draw() {
//clear the canvas
ctx.clearRect(0, 0, WIDTH, HEIGHT);

            //set velocity of fish as a function of FPS
var fps = fpsMeter.meterFps;

power = Math.min(fps, 60);
if(isNaN(power)) power = 1;
//velocity = 100 + 100 * (power * power / 3600); //exponential curve between 100-200
velocity = Math.floor((power * power * .5) / 3) < 1 ? 1 : Math.floor((power * power * .5) / 3); //exponential curve between 1 and 600.

            // Draw each fish
for (var fishie in fish) {
fish[fishie].swim();
}
  

    //draw fpsometer with the current number of fish
fpsMeter.Draw(fish.length);
}

 

la funzione swim, essenzialmente ridisegna il pesce dopo aver calcolato le coordinate della posizione del pesce. Il cuore nella funzione lo vedete di seguito:

//draw the fish
//locate the fish
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale); // make the fish bigger or smaller depending on how far away it is.
ctx.transform(flip, 0, 0, 1, 0, 0); //make the fish face the way he's swimming.
ctx.drawImage(imageStrip, fishW * cell, fishH * species, fishW, fishH, -fishW / 2, -fishH / 2, fishW, fishH); //draw the fish
ctx.save();
scale = nextScale // increment scale for next time
ctx.restore();
ctx.restore();

//increment to next state
x = nextX;
y = nextY;
z = nextZ;
if (cell >= cellCount-1 || cell <= 0) { cellReverse = cellReverse * -1; } //go through each cell in the animation
cell = cell + 1 * cellReverse; //go back down once we hit the end of the animation

Che come descritto prima, essenzialmente salva il contesto del canvas, posiziona e disegna il pesce e ripristina lo stato e incrementa le variabili per il successivo ridisegno. Nella demo FishIETank potete vedere ed apporfondire il resto dell'esempio di animazione con il canvas realizzata.