Visit Mozilla.org

Tutoriel canvas:Formes géométriques

Un article de MDC.


Sommaire

[modifier] La grille

Avant de commencer à dessiner, il faut aborder le sujet de la grille du canevas ou de l'espace de coordonnées. Le modèle HTML de la page précédente avait un élément canvas de 150 pixels de large et 150 pixels de haut. L'image ci-contre montre la grille par défaut en surimpression. Normalement, 1 unité sur la grille correspond à 1 pixel sur le canevas. L'origine des axes est placée dans le coin en haut à gauche (coordonnées (0,0)). Tous les éléments sont placés relativement à cette origine. Ainsi, la position du coin en haut à gauche du carré bleu est définie à x pixels depuis la gauche et y pixels depuis le haut (coordonnées (x,y)).

Plus loin dans ce tutoriel, nous verrons comment effectuer une translation de cette origine vers une position différente, effectuer une rotation de la grille et même la changer d'échelle. Pour l'instant nous la conserverons à sa position par défaut.

[modifier] Formes géométriques

Contrairement à SVG, canvas ne permet de créer qu'une forme primitive - le rectangle. Toutes les autres formes doivent être créés en combinant un ou plusieurs chemins. Heureusement, nous disposons d'une collection de fonctions de création de chemins qui permettent de composer des formes très complexes.

[modifier] Rectangles

Pour commencer, occupons-nous du rectangle. Trois fonctions existent pour dessiner des rectangles sur le canevas :

fillRect(x,y,width,height) : Dessine un rectangle plein
strokeRect(x,y,width,height) : Dessine un rectangle vide
clearRect(x,y,width,height) : Efface la zone spécifiée et la rend entièrement transparente

Chacune de ces trois fonctions prend les mêmes paramètres. x et y spécifient la position sur le canevas (relativement à l'origine) du coin en haut à gauche du rectangle. width et height définissent respectivement la largeur et la hauteur. Voyons maintenant ces fonctions en action.

Le code ci-dessous est celui de la fonction draw() de la page précédente, auquel ont été ajoutées les trois fonctions qui viennent d'être présentées.

[modifier] Exemple de forme rectangulaire

Afficher cet exemple

function draw(){
  var canvas = document.getElementById('tutorial');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    ctx.fillRect(25,25,100,100);
    ctx.clearRect(45,45,60,60);
    ctx.strokeRect(50,50,50,50);
  }
}
Le résultat devrait ressemble à l'image sur la droite. La fonction fillRect dessine un grand carré noir de 100x100 pixels. La fonction clearRect enlève un carré de 60x60 pixels du centre, et finalement la fonction strokeRect dessine un encadré rectangulaire de 50x50 pixels à l'intérieur du rectangle supprimé.

Dans les pages qui suivent, nous verrons deux méthodes alternatives à la fonction clearRect et nous verront également comment changer la couleur et le style de ligne des formes affichées.

Contrairement aux fonctions de chemin que nous verrons dans la section suivante, les trois fonctions de rectangle dessinent immédiatement sur le canevas.

[modifier] Dessin de chemins

Pour créer des formes à l'aide de chemins, quelques étapes supplémentaires sont nécessaires.

beginPath()
closePath()
stroke()
fill()

La première étape dans la création d'un chemin est d'appeler la méthode beginPath. En interne, les chemins sont stockés sous la forme d'une liste de sous-chemins (lignes, arcs, etc.) qui mis ensemble forment un dessin. Chaque fois que cette méthode est appelée, la liste est remise à zéro et l'on peut commencer à dessiner de nouvelles formes.

La seconde étape est d'appeler les méthodes spécifiant les chemins qui doivent être réellement dessinés. Nous les verrons rapidement.

La troisième étape, facultative, est d'appeler la méthode closePath. Celle-ci essaie de fermer le chemin en dessinant une ligne droite depuis le point courant vers le point de départ. Si la forme a déjà été fermée ou s'in n'y a qu'un point dans la liste, cette fonction ne fait rien.

La dernière étape est d'appeler les méthodes stroke et/ou fill. L'appel à une de ces deux fonctions dessinera la forme sur le canevas. stroke est utilisée pour dessiner le bord d'une forme, tandis que fill permet de créer une forme pleine.

Note : Lors de l'appel à la méthode fill, toute forme ouverte sera automatiquement fermée et il n'est pas nécessaire d'utiliser la méthode closePath.

Le code pour dessiner une forme simple (un triangle) ressemblera donc à ceci :

ctx.beginPath();
ctx.moveTo(75,50);
ctx.lineTo(100,75);
ctx.lineTo(100,25);
ctx.fill();

[modifier] moveTo

Une fonction très utile, qui ne dessine en fait rien, mais fait partie de la liste de fonctions de chemin décrite ci-dessus est la fonction moveTo. Il est probablement plus facile de penser à celle-ci comme à un lever de pinceau ou de crayon depuis un endroit sur une feuille de papier et son repositionnement à un autre endroit.

moveTo(x, y)

La fonction moveTo prend deux arguments - x et y, - qui sont les coordonnées du nouveau point de départ.

Lorsque le canevas est initialisé ou que la méthode beginPath est appelée, le point de départ est défini aux coordonnées (0,0). Dans la plupart des cas, on utilisera la méthode moveTo pour placer le point de départ à un autre endroit. La fonction moveTo peut également être utilisée pour dessiner deux chemins non reliés entre-eux. Regardez par exemple le visage souriant sur la droite. Les endroits où la médhode moveTo a été utilisée sont marqués (les lignes rouges).

Pour essayer ceci par vous-mêmes, vous pouvez utiliser le bout de code ci-dessous. Collez-le simplement dans la fonction draw vue précédemment.

[modifier] Exemple d'utilisation de moveTo

Afficher cet exemple

ctx.beginPath();
ctx.arc(75,75,50,0,Math.PI*2,true); // Cercle extérieur
ctx.moveTo(110,75);
ctx.arc(75,75,35,0,Math.PI,false);   // Bouche (sens des aiguilles d'une montre)
ctx.moveTo(65,65);
ctx.arc(60,65,5,0,Math.PI*2,true);  // Œil gauche
ctx.moveTo(95,65);
ctx.arc(90,65,5,0,Math.PI*2,true);  // Œil droit
ctx.stroke();

Note : enlevez les appels à moveTo pour voir les lignes de connexion.
Note : pour une description de la fonction arc et de ses paramètres, voir plus loin.

[modifier] Lignes

Pour dessiner des lignes droites, on utilise la méthode lineTo.

lineTo(x, y)

Cette méthode prend deux arguments - x et y, - qui sont les coordonnées de l'extrémité finale de la ligne. Le point de départ est dépendant des chemins dessinés précédemment, puisque le dernier point d'un chemin est le point de départ du suivant, etc. Le point de départ peut également être changé à l'aide de la méthode moveTo.

[modifier] Exemple d'utilisation de lineTo

Dans l'exemple ci-dessous, deux triangles sont dessinés, un rempli et l'autre vide. (Le résultat peut être vu dans l'image de droite). Pour commencer, la méthode beginPath est appelée pour débuter un nouveau chemin. La méthode moveTo est ensuite utilisée pour déplacer le point de départ vers la position désirée. Enfin, deux lignes sont dessinées pour former deux côtés du triangle.

Vous remarquerez la différence entre le triangle plein et le triangle vide. Elle est due, comme mentionné ci-dessus, à la fermeture automatique des formes lorsqu'un chemin est rempli avec la méthode fill. Si la fermeture n'avait pas été faite explicitement pour le triangle vide, seules deux lignes auraient été dessinées et pas le triangle complet.

Afficher cet exemple

// Triangle plein
ctx.beginPath();
ctx.moveTo(25,25);
ctx.lineTo(105,25);
ctx.lineTo(25,105);
ctx.fill();

// Triangle vide
ctx.beginPath();
ctx.moveTo(125,125);
ctx.lineTo(125,45);
ctx.lineTo(45,125);
ctx.closePath();
ctx.stroke();

[modifier] Arcs

Pour dessiner des arcs ou des cercles, on utilise la méthode arc. La spécification décrit également la méthode arcTo, qui est reconnue par Safari, mais n'a pas été implémentée dans les navigateurs actuels basés sur Gecko.

arc(x, y, rayon, angleDepart, angleFin, sensInverse)

Cette méthode prend cinq paramètres : x et y sont les coordonnées du centre du cercle, tandis que rayon est son rayon. Les paramètres angleDepart et angleFin définissent les points de départ et d'arrivée de l'arc en radians. Ceux-ci sont mesurés à partir de l'axe x. Le paramètre sensInverse est une valeur booléenne qui, lorsqu'elle est mise à true, dessine l'arc dans le sens inverse des aiguilles d'une montre. Autrement, le dessin se fait dans le sens des aiguilles d'une montre.

Attention : dans les versions beta de Firefox 1.5, le sens du dernier paramètre était inversé. La version finale se comporte comme indiqué ci-dessus. Tous les scripts qui utilisaient cette méthode dans sa forme précédente devront être mis à jour pour fonctionner avec la version finale.

Note : les angles dans la fonction arc sont mesurés en radians et non en degrés. Pour convertir des degrés en radians, vous pouvez utiliser l'expression JavaScript suivante : var radians = (Math.PI/180)*degres.

[modifier] Exemple d'utilisation d'arc

L'exemple suivant est un peu plus complexe que ceux que vous avez pu voir jusqu'à présent. Il dessine 12 arcs différents, chacun avec des angles et des remplissages différents. S'il avait été écrit de la même façon que le visage présenté plus haut, la liste d'instructions serait devenue très longue, et de plus il aurait été nécessaire de connaître chaque point de départ. Pour les arcs de 90, 180 et 270 degrés comme ceux qui sont utilisés ici, ce ne serait pas un problème, mais pour des angles plus complexes cela deviendrait beaucoup trop compliqué.

Les deux boucles for servent à parcourir les lignes et colonnes d'arcs. Pour chaque arc, un nouveau chemin est commencé à l'aide de beginPath. Ensuite, tous les paramètres sont présentés suis la forme de variables afin qu'il soit plus facile de comprendre ce qui se passe. Normalement une simple ligne suffirait. Les coordonnées x et y devraient être faciles à comprendre, et rayon ainsi que angleDepart sont fixés. L'angle de fin (angleFin) commence à 180 degrés (première colonne) et augmente de 90 degrés à chaque étape pour finir par former un cercle complet (dernière colonne). La déclaration du paramètre sensInverse fait en sorte que les premiers et troisième arcs soient dessinés dans le sens des aiguilles d'une montre et les deuxième et quatrième dans le sens inverse. Enfin, la condition if rend la moitié supérieure vide, et la partie inférieure pleine.

Afficher cet exemple

for (i=0; i<4; i++) {
  for(j=0; j<3; j++) {
    ctx.beginPath();
    var x           = 25+j*50;               // coordonnée x
    var y           = 25+i*50;               // coordonnée y
    var rayon       = 20;                    // rayon de l'arc
    var angleDepart = 0;                     // point de départ sur le cercle
    var angleFin    = Math.PI+(Math.PI*j)/2; // point final sur le cercle
    var sensInverse = i%2==0 ? false : true; // sens des aiguilles ou inverse

    ctx.arc(x, y, rayon, angleDepart, angleFin, sensInverse);

    if (i>1) {
      ctx.fill();
    } else {
      ctx.stroke();
    }
  }
}

[modifier] Courbes de Bézier et courbes quadratiques

Il existe un autre type de chemins, les courbes de Bézier, disponible dans ses variétés cubiques et quadratiques. Ces courbes sont généralement utilisées pour dessiner des formes organiques complexes.


quadraticCurveTo(cp1x, cp1y, x, y) // Inutilisable dans Firefox 1.5 (voir comment contourner cela ci-dessous)
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)


La différence entre les deux est visible sur l'image de droite. Une courbe de Bézier quadratique a un point de départ et un point d'arrivée (les points bleus), mais uniquement un point de contrôle (le point rouge). Une courbe de Bézier cubique dispose par contre de deux points de contrôle.

Les paramètres x et y dans ces deux méthodes sont les coordonnées du point d'arrivée. cp1x et cp1y sont les coordonnées du premier point de contrôle, tandis que cp2x et cp2y sont les coordonnées du second point de contrôle.

L'utilisation de courbes de Bézier quadratiques et cubiques peut s'avérer relativement complexe, car contrairement aux logiciels de dessin vectoriel comme Adobe Illustrator, on ne dispose pas d'un retour visuel direct sur ce que l'on est en train de faire. Cela rend le dessin de formes complexes assez difficile. Dans l'exemple suivant, nous dessinerons quelques formes organiques simples, mais si vous avez le temps, et surtout la patience, des formes beaucoup plus complexes peuvent être créées.

Il n'y a rien de très difficile dans ces exemples. Dans les deux cas nous voyons une succession de courbes dessinées qui résultent finalement en une forme complète.

[modifier] Exemple d'utilisation de quadraticCurveTo

Afficher cet exemple

// Exemple de courbes quadratriques
ctx.beginPath();
ctx.moveTo(75,25);
ctx.quadraticCurveTo(25,25,25,62.5);
ctx.quadraticCurveTo(25,100,50,100);
ctx.quadraticCurveTo(50,120,30,125);
ctx.quadraticCurveTo(60,120,65,100);
ctx.quadraticCurveTo(125,100,125,62.5);
ctx.quadraticCurveTo(125,25,75,25);
ctx.stroke();

Il est possible de convertir n'importe quelle courbe de Bézier quadratique en une courbe de Bézier cubique en calculant correctement les deux points de contrôle de la courbe cubique depuis le point de contrôle unique de la courbe quadratique. Cependant, l'inverse n'est PAS vrai. Une conversion exacte d'une courbe de Bézier cubique en courbe de Bézier quadratique n'est possible que si le terme cubique est zéro. Dans les autre cas, une méthode de subdivision est utilisée pour faire l'approximation d'une courbe de Bézier cubique à l'aide de plusieurs courbes de Bézier quadratiques.

[modifier] Exemple d'utilisation de bezierCurveTo

Afficher cet exemple

// Exemple de courbes de Bézier
ctx.beginPath();
ctx.moveTo(75,40);
ctx.bezierCurveTo(75,37,70,25,50,25);
ctx.bezierCurveTo(20,25,20,62.5,20,62.5);
ctx.bezierCurveTo(20,80,40,102,75,120);
ctx.bezierCurveTo(110,102,130,80,130,62.5);
ctx.bezierCurveTo(130,62.5,130,25,100,25);
ctx.bezierCurveTo(85,25,75,37,75,40);
ctx.fill();

[modifier] Contournement du bug de quadraticCurveTo() dans Firefox 1.5

L'implémentation de quadatricCurveTo() disponible dans Firefox 1.5 est incorrecte. En effet, la fonction ne dessine PAS une courbe quadratique, étant donné qu'elle appelle simplement la même fonction de courbe cubique que celle utilisée par bezierCurveTo() en donnant deux fois la même coordonnée (x,y) pour les deux points de contrôle. Par conséquent, quadraticCurveTo() produira des résultats incorrects. Si vous avez besoin d'utiliser quadraticCurveTo(), vous devrez convertir votre courbe de Bézier quadratique en une courbe cubique par vous-même et utiliser la fonction bezierCurveTo() qui fonctionne correctement.

var currentX, currentY;  // dernières coordonnées x,y envoyées à lineTo/moveTo/bezierCurveTo ou quadraticCurveToFixed()

function quadraticCurveToFixed( cpx, cpy, x, y ) {
  /*
   Pour les équations ci-dessouss, les préfixes de noms de variables suivants sont utilisés :
     qp0 est le point de départ de la courbe quadratique (il doit s'agir du dernier point envoyé à moveTo(), lineTo(), ou bezierCurveTo() ).
     qp1 est le point de contrôle de la courbe quadratique (il s'agit des cpx,cpy que vous auriez envoyés à quadraticCurveTo() ).
     qp2 est le point d'arrivée de la courbe quadratique (ce sont les arguments x,y que vous auriez envoyés à quadraticCurveTo() ).
   Ces points seront convertis pour calculer les deux points de contrôle nécessaire pour une courbe cubique (les points de départ et d'arrivée ne changeant pas).

   Les équations pour les deux points de contrôle cubiques sont :
     cp0=qp0 and cp3=qp2
     cp1 = qp0 + 2/3 *(qp1-qp0)
     cp2 = cp1 + 1/3 *(qp2-qp0) 

   Dans le code ci-dessous, il faut calculer les termes x et y pour chaque point séparément. 

    cp1x = qp0x + 2.0/3.0*(qp1x - qp0x);
    cp1y = qp0y + 2.0/3.0*(qp1y - qp0y);
    cp2x = cp1x + (qp2x - qp0x)/3.0;
    cp2y = cp1y + (qp2y - qp0y)/3.0;

   Nous allons maintenant : 
     a) remplacer les variables qp0x et qp0y par currentX et currentY (que *vous* devez conserver entre chaque moveTo/lineTo/bezierCurveTo)
     b) remplacer les variables qp1x et qp1y par cpx et cpy (qui auraient été envoyées à quadraticCurveTo)
     c) remplacer les variables qp2x et qp2y par x et y.
   ce qui nous donne ceci : 
  */
  var cp1x = currentX + 2.0/3.0*(cpx - currentX);
  var cp1y = currentY + 2.0/3.0*(cpy - currentY);
  var cp2x = cp1x + (x - currentX)/3.0;
  var cp2y = cp1y + (y - currentY)/3.0;

  // on peut maintenant appeler la fonction de courbe de Bézier cubique 
  bezierCurveTo( cp1x, cp1y, cp2x, cp2y, x, y );

  currentX = x;
  currentY = y;
}

[modifier] Rectangles

À côté des trois méthodes vues précédemment qui permettent de dessiner des formes rectangulaires directement sur le canevas, il existe également une méthode rect qui ajoute un chemin rectangulaire à un chemin existant.

rect(x, y, largeur, hauteur)

Cette méthode prend quatre arguments. Les paramètres x et y définissent les coordonnées du coin en haut à gauche du nouveau chemin rectangulaire, largeur et hauteur définissent la largeur et la hauteur du rectangle.

Lorsque cette méthode est exécutée, la méthode moveTo est automatiquement appelée avec les paramètres (0,0) (c'est-à-dire qu'elle réinitialise le point de départ à sa position par défaut).

[modifier] Combinaisons

Dans tous les exemples de cette page, un seul type de fonction a été utilisé par forme. Cependant, il n'y a absolument aucune limitation à la quantité de type de chemins qui peuvent être utilisés pour créer une forme. Dans ce dernier exemple, toutes les fonctions de tracer de chemin sont combinées afin de réaliser un ensemble de personnages de jeux très connus.

[modifier] Exemple

Nous n'irons pas dans les détails de ce script complet, mais les choses les plus importantes à noter sont la fonction roundedRect et l'utilisation de la propriété fillStyle. Il peut être très utile de définir vos propres fonctions pour dessiner des formes complexes, et cela peut vous faire gagner beaucoup de temps. La longueur du script suivant a ainsi pu être réduite de moitié.
La propriété fillStyle sera examinée plus en détail plus loin dans ce tutoriel. Elle est utilisée ici pour changer la couleur noire de remplissage par défaut par le blanc, puis à nouveau en noir.

Afficher cet exemple

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  roundedRect(ctx,12,12,150,150,15);
  roundedRect(ctx,19,19,150,150,9);
  roundedRect(ctx,53,53,49,33,10);
  roundedRect(ctx,53,119,49,16,6);
  roundedRect(ctx,135,53,49,33,10);
  roundedRect(ctx,135,119,25,49,10);

  ctx.beginPath();
  ctx.arc(37,37,13,Math.PI/7,-Math.PI/7,true);
  ctx.lineTo(31,37);
  ctx.fill();
  for(i=0;i<8;i++){
    ctx.fillRect(51+i*16,35,4,4);
  }
  for(i=0;i<6;i++){
    ctx.fillRect(115,51+i*16,4,4);
  }
  for(i=0;i<8;i++){
    ctx.fillRect(51+i*16,99,4,4);
  }
  ctx.beginPath();
  ctx.moveTo(83,116);
  ctx.lineTo(83,102);
  ctx.bezierCurveTo(83,94,89,88,97,88);
  ctx.bezierCurveTo(105,88,111,94,111,102);
  ctx.lineTo(111,116);
  ctx.lineTo(106.333,111.333);
  ctx.lineTo(101.666,116);
  ctx.lineTo(97,111.333);
  ctx.lineTo(92.333,116);
  ctx.lineTo(87.666,111.333);
  ctx.lineTo(83,116);
  ctx.fill();
  ctx.fillStyle = "white";
  ctx.beginPath();
  ctx.moveTo(91,96);
  ctx.bezierCurveTo(88,96,87,99,87,101);
  ctx.bezierCurveTo(87,103,88,106,91,106);
  ctx.bezierCurveTo(94,106,95,103,95,101);
  ctx.bezierCurveTo(95,99,94,96,91,96);
  ctx.moveTo(103,96);
  ctx.bezierCurveTo(100,96,99,99,99,101);
  ctx.bezierCurveTo(99,103,100,106,103,106);
  ctx.bezierCurveTo(106,106,107,103,107,101);
  ctx.bezierCurveTo(107,99,106,96,103,96);
  ctx.fill();
  ctx.fillStyle = "black";
  ctx.beginPath();
  ctx.arc(101,102,2,0,Math.PI*2,true);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(89,102,2,0,Math.PI*2,true);
  ctx.fill();
}
function roundedRect(ctx,x,y,width,height,radius){
  ctx.beginPath();
  ctx.moveTo(x,y+radius);
  ctx.lineTo(x,y+height-radius);
  ctx.quadraticCurveTo(x,y+height,x+radius,y+height);
  ctx.lineTo(x+width-radius,y+height);
  ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
  ctx.lineTo(x+width,y+radius);
  ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
  ctx.lineTo(x+radius,y);
  ctx.quadraticCurveTo(x,y,x,y+radius);
  ctx.stroke();
}