Case study - Lasciarsi coinvolgere da HTML5 Canvas

Derek Detweiler
Derek Detweiler

Introduzione

La scorsa primavera (2010) mi sono interessata al supporto in rapida crescita di HTML5 e alle tecnologie correlate. All'epoca, io e un amico ci sfidavamo a vicenda in gare di sviluppo di giochi di due settimane per affinare le nostre capacità di programmazione e di sviluppo e per dare vita alle nostre idee di gioco. Ho quindi iniziato a incorporare elementi HTML5 nei video della mia competizione per capire meglio come funzionavano e poter svolgere operazioni quasi impossibili con le specifiche HTML precedenti.

Tra le tante nuove funzionalità in HTML5, il maggiore supporto del tag canvas mi ha offerto un'entusiasmante opportunità di implementare opere d'arte interattive usando JavaScript. Questo mi ha portato a provare a implementare un rompicapo che ora si chiama Entanglement. Avevo già creato un prototipo utilizzando il retro delle tessere Settlers di Catan, quindi, utilizzandolo come un progetto, ci sono tre parti essenziali per modellare il riquadro esagonale sulla tela HTML5 per il gioco sul web: disegnare l'esagono, disegnare i percorsi e ruotare il riquadro. Di seguito viene spiegato nel dettaglio come li ho realizzati nel formato attuale.

Disegnare l'esagono

Nella versione originale di Entanglement ho utilizzato diversi metodi di disegno su tela per disegnare l'esagono, ma la forma attuale del gioco utilizza drawImage() per disegnare texture ritagliate da un foglio sprite.

Foglio sprite di riquadri
Foglio sprite di riquadri

Ho combinato le immagini in un unico file, in modo che si tratti di una sola richiesta al server anziché, in questo caso, dieci. Per disegnare un esagono scelto sulla tela, devi prima raccogliere gli strumenti: tela, contesto e immagine.

Per creare un canvas, è sufficiente il tag canvas nel documento HTML, in questo modo:

<canvas id="myCanvas"></canvas>

Gli do un ID in modo da poterlo inserire nel nostro script:

var cvs = document.getElementById('myCanvas');

In secondo luogo, dobbiamo scegliere il contesto 2D della tela per iniziare a disegnare:

var ctx = cvs.getContext('2d');

Infine, abbiamo bisogno dell'immagine. Se il nome è "tiles.png" nella stessa cartella della pagina web, possiamo recuperarlo:

var img = new Image();
img.src = 'tiles.png';

Ora che abbiamo i tre componenti, possiamo utilizzare ctx.drawImage() per disegnare il singolo esagono che vogliamo dal foglio sprite al canvas:

ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

In questo caso, stiamo utilizzando il quarto esagono da sinistra nella riga superiore. Inoltre, lo disegneremo sul canvas nell'angolo in alto a sinistra, mantenendo le stesse dimensioni dell'originale. Supponendo che gli esagoni siano larghi 400 pixel e alti 346 pixel, complessivamente l'aspetto sarà simile al seguente:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';
var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

Abbiamo copiato correttamente parte dell'immagine sulla tela con questo risultato:

Piastrella esagonale
Riquadro esagonale

Creazione di percorsi

Ora che il nostro esagono è disegnato sulla tela, vediamo disegnarci sopra alcune linee. Per prima cosa, osserveremo alcune geometrie relative all'esagono. Vogliamo due estremità di linea per lato, ognuna delle quali termina a 1/4 dalle estremità lungo ciascun bordo e a 1/2 di distanza l'una dall'altra, in questo modo:

Punti finali linea su riquadro esagonale
Endpoint delle linee su riquadro esagonale

Vogliamo anche una curva accattivante, quindi, utilizzando un po' di prove ed errori, ho scoperto che, se creo una retta perpendicolare dal bordo di ogni endpoint, l'intersezione di ogni coppia di endpoint intorno a un determinato angolo dell'esagono crea un bel punto di controllo di Bezier per gli endpoint dati:

Punti di controllo su riquadro esagonale
Punti di controllo su un riquadro esagonale

Ora, mappamo sia gli endpoint che i punti di controllo su un piano cartesiano corrispondente alla nostra immagine canvas e siamo pronti a tornare al codice. Per semplificare, iniziamo con una riga. Inizieremo tracciando un percorso dall'endpoint in alto a sinistra all'endpoint in basso a destra. Con la nostra precedente immagine esagonale di 400 x 346, l'endpoint principale avrà una dimensione di 150 pixel e 0 pixel in basso (150, 0). Il punto di controllo sarà (150, 86). L'endpoint del bordo inferiore è (250, 346) con un punto di controllo di (250, 260):

Coordinate per la prima curva di Bézier
Coordinate per la prima curva di Bézier

Con le coordinate in mano, ora siamo pronti per iniziare a disegnare. Inizieremo da zero con ctx.beginPath(), quindi passeremo al primo endpoint utilizzando:

ctx.moveTo(pointX1,pointY1);

Possiamo quindi tracciare la linea stessa utilizzando ctx.bezierCurveTo() nel seguente modo:

ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);

Poiché vogliamo che la linea abbia un bordo piacevole, tracciamo il percorso due volte utilizzando ogni volta una larghezza e un colore diversi. Il colore verrà impostato utilizzando la proprietà ctx.impattoStyle e la larghezza verrà impostata utilizzando ctx.lineLarghezza. Complessivamente, la prima riga sarà simile a questa:

var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.beginPath();
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

Ora abbiamo un riquadro esagonale con la prima linea serpeggiante:

Linea solitaria su riquadro esagonale
Linea solitaria su riquadro esagonale

Inserendo le coordinate per gli altri 10 endpoint e i corrispondenti punti di controllo della curva di Bezier, possiamo ripetere i passaggi precedenti e creare un riquadro simile al seguente:

Riquadro esagonale completato.
Riquadro esagonale completato

Rotazione della tela

Una volta ottenuto il riquadro, vogliamo poterlo girare in modo da poter seguire percorsi diversi nel gioco. A questo scopo, usiamo ctx.translate() e ctx.rotate(). Il riquadro deve ruotare attorno al centro, quindi il primo passaggio è spostare il punto di riferimento della tela al centro del riquadro esagonale. A questo scopo, utilizziamo:

ctx.translate(originX, originY);

dove originX corrisponderà a metà della larghezza del riquadro esagonale, mentre originX sarà metà dell'altezza, ottenendo come segue:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);

Ora possiamo ruotare il riquadro con il nostro nuovo punto centrale. Poiché un esagono ha sei lati, devi ruotarlo di un multiplo di Math.PI diviso per 3. Faremo tutto in modo semplice e faremo un solo giro in senso orario utilizzando:

ctx.rotate(Math.PI / 3);

Tuttavia, poiché l'esagono e le linee utilizzano le vecchie coordinate (0,0) come origine, una volta completata la rotazione, devi traslare prima di disegnare. In tutto, ora abbiamo:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

Inserendo la traslazione e la rotazione precedenti prima del codice di rendering, viene eseguito il rendering del riquadro ruotato:

Riquadro esagonale ruotato
Riquadro esagonale ruotato

Riepilogo

Sopra ho evidenziato alcune delle funzionalità offerte da HTML5 mediante il tag canvas, tra cui il rendering delle immagini, il disegno di curve di Bézier e la rotazione del canvas. L'utilizzo del tag canvas HTML5 e dei suoi strumenti di disegno JavaScript per Entanglement si è rivelato un'esperienza piacevole e non vedo l'ora di scoprire le tante nuove applicazioni e giochi creati da altri con questa tecnologia aperta ed emergente.

Riferimento codice

Di seguito sono riportati tutti gli esempi di codice forniti sopra come riferimento:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

ctx.beginPath();
var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 250;
pointY1 = 0;
controlX1 = 250;
controlY1 = 86;
controlX2 = 150;
controlY2 = 86;
pointX2 = 75;
pointY2 = 43;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 150;
pointY1 = 346;
controlX1 = 150;
controlY1 = 260;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 43;
controlX1 = 250;
controlY1 = 86;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 130;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 25;
pointY1 = 130;
controlX1 = 100;
controlY1 = 173;
controlX2 = 100;
controlY2 = 173;
pointX2 = 25;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 303;
controlX1 = 250;
controlY1 = 260;
controlX2 = 150;
controlY2 = 260;
pointX2 = 75;
pointY2 = 303;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();