Tu vois passer des réalisations de dingues faîtes avec l'API canvas et tu te demandes comment ça fonctionne ? 🤔
Je te propose dans cet article de parcourir les fondamentaux de canvas, pour que tu puisses avoir les connaissances essentielles pour te lancer dans la création de jeux et d'animations avec l'API canvas !
Lorsque j'ai entamé mon exploration de l'API Canvas, mon objectif initial était de créer des animations similaires à celle-ci (tu peux cliquer sur le fond pour déclencher l'animation) :
Cependant, je me suis rapidement rendu compte que je manquais de compréhension quant au fonctionnement réel de canvas.
Avant d'essayer de créer des animations ou des jeux, il est crucial de comprendre les bases de canvas : qu'est-ce que canvas, comment ça marche, comment dessiner des formes simples et les animer, comment ajouter des évènements etc.
Je te propose d'aborder tous ces sujets dans cet article. 🙂
C'est quoi canvas au juste ?
Avant d'essayer de faire quoi que ce soit avec canvas, il est crucial de comprendre ce qu'est canvas.
Canvas est une balise HTML (l'élément <canvas>
) qui va te permettre de créer une surface de dessin sur une page Web.
Tu vas pouvoir dessiner sur cette surface en te servant de l'API canvas.
L'API canvas est un ensemble de méthodes et de propriétés JavaScript qui vont te permettre de dessiner des formes, du texte, des images etc.
Derrière le mot canvas se cache en réalité deux choses :
- la balise HTML
<canvas>
qui génère une surface de dessin sur la page Web - l'API canvas qui te permet de dessiner sur cette surface
Tu peux générer avec l'exemple ci-dessous 100 ronds dont la taille, la couleur, les positions en x et en y sont déterminées de façon aléatoire :
Les ronds ne sont pas des éléments HTML, ce sont des pixels dessinés sur la surface de dessin. Il n'est donc pas possible de les cibler avec du CSS ou du JavaScript (avec un querySelector()
).
Il ne faut donc pas essayer de créer des formes avec des balises HTML, puisque les formes dessinées avec canvas ne sont pas des éléments du DOM.
La balise HTML <canvas>
sert seulement à créer une surface de dessin, le code que l'on peut mettre à l'intérieur ne sera d'ailleurs pas affiché sur la page.
Une précision importante : mettre du contenu HTML dans la balise canvas peut quand même être utile pour deux raisons :
- pour afficher un message d'erreur si le navigateur ne supporte pas canvas
- pour mettre du contenu alternatif pour les lecteurs d'écran et les robots d'indexation
La surface de dessin créée avec la balise canvas est une surface dont tu peux changer la taille librement : pour lui donner la largeur et la hauteur du viewport par exemple.
De plus, il est tout à fait possible d'avoir sur une même page une surface de dessin et d'autres éléments HTML :
Tout ce qui est dessiné sur la surface de dessin l'est via l'utilisation de l'API canvas.
Le rendu de ce qui est dessiné est un rendu bitmap (comme les jpeg, les png..), même le texte que tu peux dessiner sur ton canvas aura un rendu bitmap.
Une image bitmap, également connue sous le nom d'image matricielle, est constituée d'une grille de pixels, chaque pixel étant associé à une couleur. En revanche, une image vectorielle est composée de formes géométriques telles que des cercles, des carrés ou des lignes.
C'est pourquoi, lorsqu'une image bitmap est agrandie, les pixels deviennent visibles, tandis qu'une image vectorielle peut être agrandie indéfiniment sans perte de qualité, car les formes géométriques sont définies mathématiquement.
Mais alors ça ne serait pas mieux d'avoir des images vectorielles pour dessiner des formes sur le canvas ? 🤔
Si canvas ne permet pas de dessiner des formes vectorielles, c'est parce qu'il est conçu pour permettre le dessin et la manipulation d'images (de pixels) de façon flexible et dynamique.
Pour des raisons de performances : les opérations sur les bitmaps sont souvent plus rapides que celles sur des éléments vectoriels complexes, et de flexibilité : les bitmaps permettent une plus grande variété d'effets visuels et d'opérations graphiques que les vecteurs dans de nombreux cas.
Comment faire pour dessiner dans la balise canvas ?
Pour comprendre comment l'API canvas s'utilise pour dessiner, je vais prendre un exemple très simple : le dessin d'un carré.
Pour cela, il faut d'abord créé une balise <canvas>
dans le HTML :
<canvas></canvas>
La balise canvas est un élément HTML, et donc un élément du DOM qui peut être ciblé avec du JavaScript :
// Récupération de la balise canvas
const canvas = document.querySelector('canvas');
// Récupération du contexte de rendu de la balise canvas
const ctx = canvas.getContext('2d');
Dans le code ci-dessus, on commence par récupérer la balise canvas avec un document.querySelector('canvas')
.
À ce stade, il ne faut pas hésiter à faire un console.dir(canvas)
pour voir les propriétés et méthodes de l'objet canvas.
Tu verras par exemple la présence des propriétés width
et height
pour définir la largeur et la hauteur de la surface de dessin.
Sur l'objet canvas se trouve la méthode getContext()
qui permet de récupérer le contexte de rendu de la balise canvas. Cette méthode retourne un objet (CanvasRenderingContext2D) qui va permettre de dessiner sur le canvas car il contient toutes les méthodes et propriétés pour dessiner des formes, du texte, des images etc.
Maintenant que l'on a récupéré le contexte de la surface de dessin, nous pouvons dessiner notre carré en utilisant les méthodes de ce contexte (n'hésites pas à faire un console.dir(ctx)
pour voir les méthodes et propriétés disponibles) :
// Dessin d'un carré
ctx.fillRect(10, 10, 100, 100);
La méthode fillRect()
permet de dessiner directement un rectangle sur le canvas, elle prend 4 paramètres :
- la position en x dans la surface de dessin du coin supérieur gauche du rectangle
- la position en y dans la surface de dessin du coin supérieur gauche du rectangle
- la largeur du rectangle
- la hauteur du rectangle
Il existe d'autres méthodes pour dessiner des cercles, des lignes, des courbes de Bézier, des images etc.
Un exemple avec le dessin d'un cercle :
// Dessin d'un cercle
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fill();
Voici un exemple (j'ai mis des commentaires dans le fichier JS) avec le dessin de plusieurs formes :
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="/styles.css" /> </head> <body> <canvas></canvas> <script src="/app.js"></script> </body> </html>
Ce que tu dois retenir : le dessin passe par la création d'un contexte, ce sont les propriétés et les méthodes de ce contexte (CanvasRenderingContext2D) qui vont te permettre de dessiner.
Comment faire des animations avec canvas ?
Maintenant tu te demandes peut-être comment est-ce qu'on fait pour faire des animations sur des formes, ou comment un personnage dans un jeu peut se déplacer sur la surface de dessin.
Contrairement à ce que l'on pourrait penser, il n'y a pas de méthode dans l'objet CanvasRenderingContext2D pour animer des éléments. Il existe des méthodes pour réaliser des transformations (rotate()
, translate()
), mais rien qui ne permet de réaliser des animations.
D'ailleurs les méthodes comme
rotate()
outranslate()
permettent de faire des transformations sur la surface de dessin elle-même mais pas sur les éléments dessinés. Même avec l'API canvas, il n'est pas possible de récupérer une forme dessinée pour la déplacer ou lui faire faire une rotation.
Mais comment faire alors ?? 🤔
Pour comprendre, je te propose de reprendre notre carré là où on l'a laissé et l'animer.
Pour faire une animation sur notre carré, il faut le dessiner de façon répétée en changeant sa position dans le système de coordonnées du canvas.
La position du carré devra être actualisée à une certaine fréquence pour donner l'illusion d'un mouvement.
Si on découpe cela en étapes, cela donne ceci :
- on dessine le carré une première fois (avec la méthode
fillRect()
) - on le dessine à nouveau mais en le décalant de 1px dans une certaine direction
On répète cela autant de fois qu'on le souhaite, par exemple sur une durée de 3 secondes, à raison de 60 fois par seconde pour avoir une animation fluide.
On va donc déplacer notre carré de 180px à la fin des 3 secondes.
Pour répéter le dessin du carré, on peut se servir de la méthode setInterval()
qui va nous permettre d'exécuter une fonction de façon répétée à intervalle régulier.
On peut donc avoir ceci :
// par défaut la position en x du carré est 100
let x = 100;
function drawSquare() {
// on dessine le carré à la position x (la première fois x = 100)
ctx.fillRect(x, 100, 100, 100);
// on ajoute 1 à la position x
x += 1;
}
// on répète l'exécution de la fonction drawSquare toutes les 16,66 millisecondes
setInterval(drawSquare, 1000 / 60);
Ici 1000 / 60 est le temps en millisecondes entre chaque exécution de la fonction drawSquare()
.
Pourquoi 1000 / 60 ? Parce que 60 images par seconde est un taux de rafraichissement standard pour une animation. Ce qui donne une exécution de la fonction toutes les 16,66 millisecondes.
Mais setInterval()
n'est pas la meilleure méthode pour faire des animations.
Pour des animations fluides, il est préférable d'utiliser la méthode requestAnimationFrame()
.
Pourquoi ? Car requestAnimationFrame()
est généralement plus performant et est conçue pour s'exécuter juste avant le prochain rafraîchissement de l'écran, ce qui garantit des animations affichées au bon moment.
Il existe d'autres raisons pour lesquelles requestAnimationFrame()
est préférable à setInterval()
mais ce n'est pas l'objet de l'article.
Modifions le code pour utiliser requestAnimationFrame()
:
let x = 100;
function drawSquare() {
ctx.fillRect(x, 100, 100, 100);
x += 1;
requestAnimationFrame(drawSquare);
}
requestAnimationFrame(drawSquare);
Dans le code ci-dessus, la méthode requestAnimationFrame()
est utilisée une première fois pour exécuter la fonction drawSquare()
en callback. Cette fonction va dessiner le carré à la position x (qui au début vaut 100), puis ajouter 1 à la position x.
Enfin, la méthode requestAnimationFrame()
est utilisée à nouveau, mais cette fois dans la fonction drawSquare()
, ce qui créé une boucle d'animation infinie.
Sauf que.. on va obtenir ceci :
Pourquoi ce comportement ? Car le carré est constamment dessiné, ce qui fait que l'on obtient cette longue trainée noire.
Dans l'ordre, voila ce qu'il se passe pour le moment :
- la méthode
requestAnimationFrame
est exécutée une première fois et demande au navigateur de dessiner le carré une première fois à la position en x à 100 - la méthode
requestAnimationFrame
est exécutée une nouvelle fois (au prochain rafraichissement de l'écran) et demande au navigateur de dessiner à nouveau le carré à la position en x à 101 - la méthode
requestAnimationFrame
est exécutée une nouvelle fois (au prochain rafraichissement de l'écran) et demande au navigateur de dessiner à nouveau le carré à la position en x à 102 - etc etc..
À aucun moment on ne précise au navigateur de supprimer le carré dessiné précédemment.
Cela peut paraitre bizarre mais pour éviter cela il faut qu'avant chaque nouveau dessin de la position actualisée du carré, effacer tout le canvas.
Il faut donc faire ceci à la place :
- la méthode
requestAnimationFrame
est exécutée une première fois, elle efface tout le canvas (même si rien n'est dessiné dessus), et dessine le carré une première fois à la position en x à 100 - la méthode
requestAnimationFrame
est exécutée une nouvelle fois (au prochain rafraichissement de l'écran), elle efface tout le canvas, et dessine à nouveau le carré à la position en x à 101 - etc etc..
La fonction drawSquare()
va être exécutée 60 fois par seconde, c'est cela qui donne l'animation.
Un effet de bord à prendre en compte lorsqu'on utilise la méthode requestAnimationFrame()
Il est important de préciser que la méthode requestAnimationFrame()
est executée à la fréquence de rafraichissement du moniteur, et non à une fréquence fixe.
Donc sur un moniteur de 120hz, la méthode drawSquare()
sera executée 120 fois par seconde.
Et c'est un problème, deux utilisateurs avec des moniteurs de fréquences différentes auront une expérience différente de l'animation.
Ce problème existe sur certains jeux vidéos, c'est le cas du jeu Resident Evil 2 par exemple : Voir la vidéo comparant Resident Evil 2 - PAL 50Hz vs PAL 60Hz
Attention car 60hz ne veut pas forcément dire 60fps.
Mais dans le cas d'une utilisation de requestAnimationFrame()
, on peut être sûr que l'animation sera de 60 images par seconde sur un moniteur de 60hz.
Il existe diverses façons de contrôler cela afin d'avoir la même animation sur différents moniteurs, mais cela nous ferait sortir du cadre de cet article.
La gestion des évènements
Maintenant que l'on sait animer des formes sur le canvas, voyons comment interagir avec ces formes en utlisant les évènements du clavier ou de la souris.
Comme exemple nous allons reprendre notre carré et le faire bouger dans la surface de dessin en utilisant les touches du clavier.
On va garder l'utilisation de la méthode requestAnimationFrame()
pour créer notre boucle d'animation, sauf que cette fois on va prendre en compte dans cette boucle, les évènements du clavier pour déplacer notre carré.
Lorsqu'on écoute en JavaScript l'évènement keydown
, il est possible de récupérer le code de la touche sur laquelle l'utilisateur a appuyé. En fonction du code de la touche, on pourra alors déplacer notre carré dans la direction souhaitée.
Si l'utilisateur appuie sur la touche "Z", on déplace le carré vers le haut, si l'utilisateur appuie sur la touche "S", on déplace le carré vers le bas etc.
Aller plus loin avec la création de jeux
Si on veut aller plus loin et commencer à implémenter la base d'un jeu de plateforme, on va devoir intégrer des notions supplémentaires comme :
- la gravité : un objet doit pouvoir chuter
- l'élasticité : un objet peut avoir une certaine capacité à rebondir
- la friction : un objet qui touche le sol et rebondit ne pourra pas le faire indéfiniment
- la gestion des collisions : un objet ne doit pas pouvoir traverser une plateforme
- la vélocité : un objet doit pouvoir se déplacer à une certaine vitesse et dans une certaine direction
Ce genre de choses se gère avec du calcul de position dans la boucle d'animation.
Voici un exemple avec la création du début d'un jeu de plateforme qui prend déjà en compte :
- une gestion de la gravité : le carré tombe
- une gestion des collisions : le carré ne peut pas traverser le sol, ni les murs, et peut sauter sur la plateforme
Cet exemple intègre d'autres notions plus avancées (vélocité, accélération..) qui ne concernent pas l'API canvas, mais qui sont nécessaires pour la création de jeux.
Une notion cruciale : les chemins et les sous-chemins
J'aborde cette notion à la fin car elle est assez complexe à saisir lorsqu'on débute avec l'API canvas, mais son importance est cruciale pour la suite de ton apprentissage.
Nous avons vu qu'il est possible d'utiliser la méthode fillRect()
pour dessiner un carré.
Cette méthode est une méthode de commodité, elle permet de dessiner un rectangle plein sans avoir à créer un sous-chemin.
Ok mais c'est quoi un chemin/sous-chemin ? 🤔
Un chemin (path) est un ensemble de sous-chemins (subpaths). Les sous-chemins sont des suites de points de coordonnées qui vont former une ou plusieurs formes.
Il est important de préciser qu'un chemin peut être vide, c'est-à-dire ne pas contenir de sous-chemins. En effet dès lors qu'un contexte est créé, comme avec getContext('2d')
, un chemin est automatiquement créé, mais ce dernier est vide.
Lorsqu'on dessine une forme, par exemple un cercle avec la méthode arc()
, on crée un sous-chemin dans le chemin actuel :
const canvas = document.querySelector('canvas');
// La création d'un contexte créé un chemin vide
const ctx = canvas.getContext('2d');
// Création d'un sous-chemin dans le chemin courant avec la méthode arc()
ctx.arc(100, 100, 50, 0, Math.PI * 2);
Avec le code ci-dessus, on a créé un sous-chemin dans le chemin courant, mais à ce stade, rien ne sera visible sur le canvas.
Pour dessiner le cercle, on pourra utiliser par exemple :
- la méthode
fill()
qui permet de remplir le chemin courant avec la couleur de remplissage actuelle (celle définie avec la propriétéfillStyle
) - la méthode
stroke()
qui permet de dessiner le contour du chemin courant avec la couleur de contour actuelle (celle définie avec la propriétéstrokeStyle
)
On comprend donc que les méthodes fill()
et stroke()
ne dessinent pas directement une forme, elles dessinent le chemin courant, c'est une nuance importante à comprendre.
Si le chemin courant contient plusieurs sous-chemins, fill()
et stroke()
vont s'appliquer à tous les sous-chemins.
Parfois, il te faudra créer un nouveau chemin, mais pourquoi faire ? Après tout on peut dessiner plusieurs formes dans un même chemin.
Créer un nouveau chemin te sera utile pour : dessiner des formes avec des styles de remplissage et de contour différents.
En effet, comment faire pour avoir des carrés rouges et des cercles bleus dans un même chemin ? C'est impossible, car le style de remplissage (fill()
) et de contour (stroke()
) s'applique à tout le chemin.
Pour créer un nouveau chemin, on utilise la méthode beginPath()
, qui va vider le chemin courant et créer un nouveau chemin vide.
J'ai conscience que la notion de chemins et de sous-chemins est complexe à saisir, si cela peut t'aider, voici une illustration pour mieux comprendre :
Pour bien comprendre cette notion, il te faudra comprendre parfaitement le fonctionnement des méthodes suivantes :
moveTo()
: pour déplacer le point de départ d'un sous-cheminlineTo()
: pour dessiner une ligne droite entre le point de départ du sous-chemin et un autre pointclosePath()
: pour fermer le sous-chemin actuel (mais sans créer un nouveau chemin)beginPath()
: pour créer un nouveau chemin en vidant la liste des sous-chemins
Alors n'hésites pas à créer des formes, des lignes avec ces méthodes et bien comprendre ce qu'elles font.
Conclusion
Nous avons vu dans cet article les bases de canvas, à savoir :
- ce qu'est canvas
- comment dessiner des formes simples
- comment animer des formes
- comment interagir avec des formes
- la notion de chemins et de sous-chemins
Cet article est un bon point de départ pour commencer à créer des animations et des jeux avec canvas, il te faudra bien sûr approfondir tes connaissances sur chacun des sujets abordés pour être à l'aise avec l'API canvas.
Je te remercie de m'avoir lu, n'hésites pas à me donner ton avis sur Twitter ou Linkedin !
Si tu souhaites être prévenu de la sortie des prochains articles et contenu du site, tu trouveras le formulaire d'inscription à ma newsletter un peu plus bas.
À bientôt, Seb.