Étude de cas : s'emmêler avec le canevas HTML5

Derek Detweiler
Derek Detweiler

Introduction

Au printemps dernier (2010), je me suis intéressé à la prise en charge croissante du HTML5 et des technologies associées. À l'époque, un ami et moi-même nous défiions mutuellement lors de compétitions de développement de jeux de deux semaines pour affiner nos compétences en programmation et en développement, et donner vie aux idées de jeux que nous nous proposions constamment. J'ai donc naturellement commencé à intégrer des éléments HTML5 dans mes participations à des concours pour mieux comprendre leur fonctionnement et pouvoir réaliser des choses qui étaient presque impossibles avec les spécifications HTML précédentes.

Parmi les nombreuses nouvelles fonctionnalités de HTML5, la prise en charge croissante de la balise canvas m'a sans frais une opportunité passionnante d'implémenter de l'art interactif à l'aide de JavaScript, ce qui m'a conduit à essayer d'implémenter un jeu de puzzle appelé désormais Entanglement. J'avais déjà créé un prototype à l'aide du dos des tuiles de Settlers of Catan. En utilisant cela comme plan, il existe trois éléments essentiels pour façonner la tuile hexagonale sur le canevas HTML5 pour le jeu sur le Web: dessiner l'hexagone, dessiner les chemins et faire pivoter la tuile. Ce qui suit décrit en détail comment j'ai réalisé chacune de ces tâches sous leur forme actuelle.

Dessiner l'hexagone

Dans la version originale d'Entanglement, j'ai utilisé plusieurs méthodes de dessin de canevas pour dessiner l'hexagone, mais la forme actuelle du jeu utilise drawImage() pour dessiner des textures découpées à partir d'une feuille d'éléments graphiques.

Feuille de sprites des cartes
Feuille de sprites de tuiles

J'ai combiné les images dans un seul fichier. Il n'y a donc qu'une seule requête envoyée au serveur au lieu de dix dans ce cas. Pour dessiner un hexagone choisi sur le canevas, nous devons d'abord rassembler nos outils: canevas, contexte et image.

Pour créer un canevas, il suffit d'ajouter la balise canvas dans notre document HTML, comme suit:

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

Je lui attribue un ID pour pouvoir l'extraire dans notre script:

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

Deuxièmement, nous devons récupérer le contexte 2D du canevas pour pouvoir commencer à dessiner:

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

Enfin, nous avons besoin de l'image. S'il est nommé "tiles.png" dans le même dossier que notre page Web, nous pouvons l'obtenir en procédant comme suit:

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

Maintenant que nous avons les trois composants, nous pouvons utiliser ctx.drawImage() pour dessiner l'hexagone unique souhaité à partir de la feuille de sprites sur le canevas:

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

Dans ce cas, nous utilisons le quatrième hexagone à gauche de la ligne supérieure. Nous allons également le dessiner sur le canevas en haut à gauche, en conservant la même taille que l'original. En supposant que les hexagones mesurent 400 pixels de large et 346 pixels de haut, l'ensemble ressemble à ceci:

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);

Nous avons réussi à copier une partie de l'image sur le canevas, comme suit:

Carte hexagonale
Carreau hexagonal

Dessiner des tracés

Maintenant que nous avons dessiné notre hexagone sur le canevas, nous voulons y dessiner quelques lignes. Commençons par examiner la géométrie de la carte hexagonale. Nous voulons deux extrémités de ligne par côté, chacune se terminant à 1/4 de la longueur de chaque bord et à 1/2 du bord l'une de l'autre, comme suit:

Points de terminaison de ligne sur une carte hexagonale
Points de terminaison de ligne sur une carte hexagonale

Nous voulons également une belle courbe. Après quelques essais et erreurs, j'ai constaté que si je traçais une ligne perpendiculaire à partir du bord à chaque point de terminaison, l'intersection de chaque paire de points de terminaison autour d'un angle donné de l'hexagone constitue un bon point de contrôle Bézier pour les points de terminaison donnés:

Points de contrôle sur une carte hexagonale
Points de contrôle sur une carte hexagonale

Nous mappons maintenant les points de terminaison et les points de contrôle sur un plan cartésien correspondant à notre image de canevas. Nous sommes maintenant prêts à revenir au code. Pour simplifier, nous allons commencer par une seule ligne. Nous allons commencer par dessiner un tracé du point de terminaison en haut à gauche au point de terminaison en bas à droite. Étant donné que notre image d'hexagone précédente mesure 400 x 346, notre point de terminaison supérieur aura une largeur de 150 pixels et une hauteur de 0 pixel, soit (150, 0). Son point de contrôle sera (150, 86). Le point de terminaison de l'arête inférieure est (250, 346) avec un point de contrôle de (250, 260):

Coordonnées de la première courbe de Bézier
Coordonnées de la première courbe de Bézier

Maintenant que nous avons nos coordonnées en main, nous sommes prêts à commencer à dessiner. Nous allons repartir à zéro avec ctx.beginPath() et passer au premier point de terminaison à l'aide de:

ctx.moveTo(pointX1,pointY1);

Nous pouvons ensuite dessiner la ligne elle-même à l'aide de ctx.bezierCurveTo() comme suit:

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

Comme nous voulons que la ligne ait une belle bordure, nous allons tracer ce tracé deux fois en utilisant une largeur et une couleur différentes à chaque fois. La couleur sera définie à l'aide de la propriété ctx.strokeStyle et la largeur à l'aide de ctx.lineWidth. Dans l'ensemble, le dessin de la première ligne se présente comme suit:

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();

Nous avons maintenant une carte hexagonale avec la première ligne qui serpente:

Ligne solitaire sur une carte hexagonale
Ligne solitaire sur une tuile hexagonale

En saisissant les coordonnées des 10 autres points de terminaison ainsi que les points de contrôle de la courbe de Bézier correspondants, nous pouvons répéter les étapes ci-dessus et créer une carte semblable à celle-ci:

Carte hexagonale terminée.
Carreau hexagonal terminé

Faire pivoter le canevas

Une fois que nous avons notre carte, nous voulons pouvoir la faire pivoter pour que différents chemins puissent être empruntés dans le jeu. Pour ce faire à l'aide d'un canevas, nous utilisons ctx.translate() et ctx.rotate(). Nous voulons que la carte tourne autour de son centre. La première étape consiste donc à déplacer le point de référence du canevas au centre de la carte hexagonale. Pour ce faire, nous utilisons:

ctx.translate(originX, originY);

Où originX correspond à la moitié de la largeur de la carte hexagonale et originY à la moitié de la hauteur, ce qui nous donne:

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

Nous pouvons maintenant faire pivoter la carte avec notre nouveau point central. Étant donné qu'un hexagone comporte six côtés, nous allons le faire pivoter d'un multiple de Math.PI divisé par trois. Nous allons faire simple et effectuer un seul tour dans le sens des aiguilles d'une montre à l'aide de:

ctx.rotate(Math.PI / 3);

Toutefois, comme notre hexagone et nos lignes utilisent les anciennes coordonnées (0,0) comme origine, une fois la rotation terminée, nous souhaitons revenir en arrière avant de dessiner. Nous avons donc maintenant:

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

En plaçant la traduction et la rotation ci-dessus avant notre code de rendu, il affiche désormais la carte pivotée:

Bloc hexagonal pivoté
Carreau hexagonal pivoté

Résumé

J'ai mis en avant ci-dessus quelques-unes des fonctionnalités offertes par HTML5 à l'aide de la balise canvas, y compris le rendu d'images, le dessin de courbes de Bézier et la rotation du canevas. L'utilisation de la balise HTML5 canvas et de ses outils de dessin JavaScript pour Entanglement s'est avérée être une expérience agréable. J'attends avec impatience les nombreuses nouvelles applications et les nombreux nouveaux jeux que d'autres créeront avec cette technologie ouverte et émergente.

Référence du code

Tous les exemples de code fournis ci-dessus sont combinés ci-dessous à titre de référence:

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();