Case study: imbarazzo con HTML5 Canvas

Derek Detweiler
Derek Detweiler

Introduzione

La scorsa primavera (2010) mi sono interessato al supporto in rapida crescita per HTML5 e le tecnologie correlate. All'epoca, io e un amico ci sfidavamo in competizioni di sviluppo di giochi di due settimane per perfezionare le nostre competenze di programmazione e sviluppo e dare vita alle idee di giochi che ci proponevamo a vicenda. Di conseguenza, ho iniziato a incorporare elementi HTML5 nei miei lavori per comprendere meglio il loro funzionamento ed eseguire operazioni quasi impossibili con le specifiche HTML precedenti.

Tra le molte nuove funzionalità di HTML5, il crescente supporto per il tag canvas mi ha offerto un'opportunità entusiasmante per implementare l'arte interattiva utilizzando JavaScript, il che mi ha portato a provare a implementare un puzzle game ora chiamato Entanglement. Avevo già creato un prototipo utilizzando il retro delle tessere di Catan, quindi, utilizzandolo come una sorta di progetto, ci sono tre componenti essenziali per realizzare la tessera esagonale sulla tela HTML5 per il gioco sul web: disegnare l'esagono, disegnare i percorsi e ruotare la tessera. Di seguito viene descritto nel dettaglio come ho realizzato ciascuno di questi elementi nella loro forma attuale.

Disegno dell'esagono

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

Spritesheet di riquadri
Sprite sheet dei riquadri

Ho combinato le immagini in un unico file, quindi è stata inviata una sola richiesta al server anziché dieci, in questo caso. Per disegnare un esagono scelto sul canvas, dobbiamo prima raccogliere i nostri strumenti: canvas, contesto e immagine.

Per creare un canvas, tutto ciò di cui abbiamo bisogno è il tag canvas nel nostro documento HTML, come segue:

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

Assegno un ID in modo da poterlo inserire nel nostro script:

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

In secondo luogo, dobbiamo acquisire il contesto 2D per la tela per poter iniziare a disegnare:

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

Infine, ci serve l'immagine. Se si chiama "tiles.png" e si trova nella stessa cartella della nostra 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 ci interessa dallo sprite sheet alla tela:

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

In questo caso, utilizziamo il quarto esagono a sinistra nella riga superiore. Inoltre, lo disegneremo sulla tela nell'angolo in alto a sinistra, mantenendo le stesse dimensioni dell'originale. Supponendo che gli esagoni siano larghi 400 pixel e alti 346 pixel, il risultato sarà simile a questo:

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 nella tela con il seguente risultato:

Riquadro esagonale
Riquadro esagonale

Percorsi di disegno

Ora che abbiamo disegnato l'esagono sulla tela, vogliamo tracciare alcune linee. Per prima cosa, esamineremo la geometria del riquadro esagonale. Vogliamo due estremità della linea per lato, ciascuna a 1/4 dalle estremità lungo ciascun bordo e 1/2 del bordo una dall'altra, come segue:

Punti finali della linea nel riquadro esagonale
Punti di fine linea nel riquadro esagonale

Vogliamo anche una bella curva, quindi, con un po' di tentativi ed errori, ho scoperto che, se creo una linea perpendicolare dal bordo a ciascun endpoint, l'intersezione di ogni coppia di endpoint intorno a un determinato angolo dell'esagono crea un punto di controllo bezier per i punti finali dati:

Punti di controllo nella scheda esagonale
Punti di controllo nel riquadro esagonale

Ora mappiamo sia gli endpoint sia i punti di controllo a un piano cartesiano corrispondente all'immagine della tela e siamo pronti a tornare al codice. Per semplicità, inizieremo con una riga. Inizieremo disegnando un percorso dall'endpoint in alto a sinistra a quello in basso a destra. Poiché la nostra precedente immagine esagonale era di 400 x 346, l'endpoint superiore sarà di 150 pixel di larghezza e 0 pixel di altezza, in forma abbreviata (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 della prima curva di Bézier
Coordinate per la prima curva di Bezier

Ora che abbiamo le coordinate, possiamo iniziare a disegnare. Inizieremo da zero con ctx.beginPath() e poi passeremo al primo endpoint utilizzando:

ctx.moveTo(pointX1,pointY1);

Possiamo quindi disegnare la linea stessa utilizzando ctx.bezierCurveTo() come segue:

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

Poiché vogliamo che la linea abbia un bel bordo, eseguiremo la traccia di questo percorso due volte utilizzando ogni volta una larghezza e un colore diversi. Il colore verrà impostato utilizzando la proprietà ctx.strokeStyle e la larghezza verrà impostata utilizzando ctx.lineWidth. Nel complesso, il disegno della prima riga sarà simile al seguente:

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 riga che si snoda al suo interno:

Riga solitaria su riquadro esagonale
Linea singola su riquadro esagonale

Inserendo le coordinate degli altri 10 endpoint e i punti di controllo della curva di Bézier corrispondenti, possiamo ripetere i passaggi precedenti e creare un riquadro simile al seguente:

Riquadro esagonale completato.
Riquadro esagonale completato

Ruotare la tela

Una volta che abbiamo la nostra tessera, vogliamo poterla girare in modo da poter scegliere diversi percorsi nel gioco. Per farlo utilizzando Canvas, utilizziamo ctx.translate() e ctx.rotate(). Vogliamo che la scheda ruoti attorno al suo centro, quindi il primo passaggio consiste nel spostare il punto di riferimento della tela al centro della scheda esagonale. A questo scopo utilizziamo:

ctx.translate(originX, originY);

Dove originX sarà la metà della larghezza del riquadro esagonale e originY sarà la metà dell'altezza, ottenendo:

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

Ora possiamo ruotare il riquadro con il nuovo punto centrale. Poiché un esagono ha sei lati, dovremo ruotarlo per un multiplo di Math.PI diviso per 3. Per semplificare, utilizzeremo un singolo 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, al termine della rotazione dovremo eseguire nuovamente la traslazione prima di disegnare. Quindi, ora abbiamo:

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

Se inserisci la traduzione e la rotazione riportate sopra prima del codice di rendering, il riquadro viene visualizzato ruotato:

Riquadro esagonale ruotato
Riquadro esagonale ruotato

Riepilogo

Sopra ho evidenziato alcune delle funzionalità offerte da HTML5 tramite 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 relativi strumenti di disegno JavaScript per Entanglement è stata un'esperienza piacevole e non vedo l'ora di scoprire le molte nuove applicazioni e nuovi giochi che altri creeranno con questa tecnologia aperta ed emergente.

Riferimento del codice

Tutti gli esempi di codice forniti sopra sono combinati di seguito 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();