Nous allons parcourir les fonctionnalités 2D offertes par le Canvas HTML 5 à travers une série d’exercices qui suivront un fil rouge : représenter un système solaire.
Prérequis
- Connaissances de base en HTML / CSS / JavaScript
- Un éditeur de texte (Visual Studio Code, Sublime Text ou encore Notepad++)
- Un navigateur Web
Partie 1 – Premier dessin
De quoi s’agit-il ?
Mais avant de commencer, de quoi parlons nous exactement avec ce Canvas ? Il s’agit d’une balise HTML introduite avec HTML 5 et qui se matérialise par une surface, au sein de la page Web, sur laquelle il est possible de dessiner en 2D mais également en 3D avec l’API WebGL. Seuls les aspects 2D seront abordés ici.
Mise en place du canvas
- Commencez par créer un document
index.html
avec une structure HTML de base :
<!DOCTYPE html>
<html>
<head>
<title>Canvas HTML 5</title>
</head>
<body>
</body>
</html>
- Ajoutez une balise
canvas
dans la sectionbody
de la page :
...
<body>
<canvas></canvas>
</body>
...
- Ouvrez le document
index.html
dans votre navigateur. Vous devriez… ne rien voir. Par défaut, la surface de dessin du canvas est transparente.
- Ouvrez les outils de développement intégrés à votre navigateur (Ctrl+Shit+i ou F12). Dans l’onglet « Eléments », survolez la balise canvas avec votre souris et vous devriez voir la surface apparaître.

Note : Vous pouvez constater que le canvas possède des dimensions par défaut de 300 x 150 pixels. Ceci aura sont importance pour plus tard.
Dessine-moi un… carré ?
Vous allez réaliser votre premier dessin sur une page Web, et pour cela, quoi de mieux qu’un bon vieux carré ! C’est simple et efficace pour s’assurer que tout fonctionne bien avant d’aller plus loin.
- Créez un dossier
js
à la racine de votre site web.
- Créez, dans ce dossier, un fichier
draw.js
- Liez ce fichier à votre document
index.html
en ajoutant une balisescript
à la fin dubody
de votre page :
...
<body>
<canvas></canvas>
<script src="js/draw.js></script>
</body>
...
- Editez le fichier
draw.js
- Première chose, il va falloir récupérer l’élément HTML représenté par la balise <canvas> :
const canvas = document.querySelector("canvas");
Avoir accès à l’élément HTML n’est pas suffisant pour pouvoir dessiner. En effet, le canvas est composé de deux parties :
- l’élément HTML qui est intégré à la page et auquel vous pouvez associer un style CSS
- le contexte graphique au sein duquel sera réalisé le dessin et qui sera ensuite affiché au sein de l’élément HTML cité précédemment.
Pour pouvoir dessiner, il va donc falloir récupérer ce fameux contexte graphique :
const context = canvas.getContext("2d");
Note 1 : respectez bien la casse pour le nom du contexte « 2d » à récupérer. Si vous mettez un majuscule, cela ne fonctionnera pas.
Note 2 : Ici nous récupérons le contexte graphique permettant de réaliser des dessins en 2D. Il serait possible, comme annoncé plus haut, d’obtenir un environnement pour modéliser des scènes 3D. Nous utiliserions alors le contexte « webgl ».
- Une fois le contexte récupérer, nous allons pouvoir utiliser les primitives de dessin associées pour tracer le carré :
// Starts the drawing of the shape
context.beginPath();
// Prepares the drawing of a rectangle starting at (0 ; 0) with 100 pixels width and 100 pixels height
context.rect(0, 0, 100, 100);
// Sets the color of filling
context.fillStyle = "#FF0000";
// Fills the square
context.fill();
- Enregitrez tous les fichiers et actualisez la page de votre navigateur. Vous devriez voir apparaître un carré rouge dans l’angle supérieur gauche de la page Web.

Dimensionnement du canvas
Vous l’avez vu précédemment, par défaut le canvas fait 300 pixels de large par 150 pixels de haut. Comme tout élément HTML, il est possible de définir la taille d’un canvas via une feuille de style.
Nous allons à présent faire en sorte que le canvas couvre la totalité de la page.
- Créez un dossier
css
à la racine de votre site et ajoutez-y un fichierstyle.css
- Associez le fichier
style.css
au documentindex.html
:
...
<head>
...
<link rel="stylesheet" href="css/style.css" />
...
</head>
...
- Editez le fichier style.css.
- Nous allons commencer par supprimer les marges de base du body et faire en sorte que ce dernier occupe tout l’espace disponible sur la page :
body {
margin: 0;
width: 100vw;
height: 100vh;
}
- Puis nous allons indiquer au canvas d’occuper tout l’espace disponible de son parent (ici le body) :
canvas {
width: 100%;
height: 100%;
}
- Enregistrez tous les fichiers et actualisez la page Web. Oups ?

Votre carré ne ressemble plus vraiment à un carré, n’est-ce pas ? D’une part, c’est devenu un rectangle et d’autre part, ses contours sont comme flous. Et dire que tout ceci est normal. si, si, vous allez comprendre.
Si, comme plus tôt dans cet exercice, vous survolez la balise canvas dans les éléments qui constituent la page, vous verrez que la canvas couvre bien toute la surface de la page. Donc sur ce point, le CSS a fait son travail.
Mais comment cela a été dit précédemment, le canvas est constitué de deux parties : l’élément HTML et le contexte graphique. Or le fait de redimensionner l’un, n’affecte pas les dimensions de l’autre. Par ailleurs, le contexte graphique est étiré pour couvrir toute la surface de l’élément HTML.
Si l’on reprend la capture d’écran ci-dessus, l’élément HTML du canvas mesure 861 par 675 pixels, et le contexte graphique est resté sur ses dimensions initiales, à savoir 300 x 150 pixels. De manière à recouvrir l’intégralité de l’élément HTML, le contexte graphique est donc étiré environ 3 fois en largeur et 5 fois en hauteur. De fait, notre carré est devenu un rectangle.
L’étirement conduit également à une perte de résolution et à des points qui chevauchent plusieurs pixels, ce qui explique les bords flous de la forme.
Pour résoudre tous ces problèmes, il suffit d’appliquer les même dimensions à l’élément HTML et au contexte graphique .
- Ajoutez les lignes suivantes dans le fichier
draw.js
, juste avant la récupération du contexte :
...
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
...
Note : Les attributs
width
etheight
du canvas définissent la taille du contexte graphique. Les attributclientWidth
etclientHeight
, communs à tous les éléments HTML, fournissent les dimensions de l’élément HTML dans la page.
- Actualisez la page. Le carré redevient carré.
Oui mais… que se passe-t-il si vous redimensionnez la fenêtre de votre navigateur ? Le rectangle s’en trouve à nouveau déformé. En effet, le redimensionnement du contexte graphique n’intervient, actuellement, qu’au début du script JS qui n’est exécuté qu’une seule fois : au chargement de la page.
Si les dimensions du canvas viennent à changer par la suite, la mise à jour des dimensions du contexte graphique ne sera pas faite.
- Créez une méthode
resize
qui actualisera les dimensions du contexte graphique du canvas :
function resize()
{
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
- Appelez la méthode
resize
juste avant la récupération du contexte.
- Ajoutez un gestionnaire d’événement
resize
à l’objetwindow
qui appellera la méthode précédente :
window.addEventListener("resize", () => {
resize();
});
- Enregistrez tous les fichiers, actualisez la page et redimensionnez la fenêtre de votre navigateur. Tada ! Le carré… a disparu !
Et oui, redimensionner le contexte graphique conduit à une réinitialisation de se dernier, et donc à un effacement des dessins précédemment réalisés. Donc, après un redimensionnement, il faut penser à retracer le dessin.
- Créez une méthode
drawSquare
qui reprendra le code permettant de dessiner le carré.
- Appelez cette méthode à la fin de la méthode
resize
- Actualisez la page du navigateur. Cette fois-ci, le carré reste à l’écran et conserve ses dimensions lors des redimensionnements de la fenêtre du navigateur.
Voici le code que vous devriez obtenir à l’issue de ce premier exercice :
// Gets the canvas HTMLElement from the DOM
const canvas = document.querySelector("canvas");
// Gets the 2D graphical context of the canvas
const context = canvas.getContext("2d");
// Draws a red square
function drawSquare()
{
context.beginPath();
context.rect(0, 0, 100, 100);
context.fillStyle = "#ff0000";
context.fill();
}
// Resizes the canvas and redraws the scene
function resize()
{
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
drawSquare();
}
// Adds a listener on resize events to resize the canvas
window.addEventListener("resize", () => {
resize();
});
// Starts by resizing the canvas and so, drawing the scene
resize();
Partie 2 – Représentation du système solaire
Modélisation du système solaire
Nous allons pouvoir passer à l’étape suivante qui consiste en la représentation graphique du système solaire. Pour cela, vous pourrez utiliser le fichier celestial-body.js
qui contient une modélisation partielle du système solaire sous la forme d’objets célestes tournant les uns autours des autres :

Étudions les attributs d’un corps céleste tel que nous les décrit le diagramme UML ci-dessus :
- Un corps céleste possède un nom (
name
) qui permettra d’identifier lequel représente le soleil, la Terre, … - Il possède également un rayon (
radius
) qui déterminera la taille du cercle utilisé pour le représenté sur le dessin - La couleur du cercle sera déterminé par l’attribut
color
- Chaque corps céleste peut être le satellite d’un autre. L’attribut distance détermine alors la distance qui sépare un satellite de son « parent ».
- Téléchargez le fichier
celestial-body.js
et placez-le dans le dossierjs
de votre site :
- Editez le fichier index.html et ajoutez un lien vers le script celestial-body.js en veillant à ce que ce dernier soit bien placé avant le lien vers le script draw.js :
<script src="js/celestial-body.js"></script>
<script src="js/draw.js"></script>
Note : Aujourd’hui, l’utilisation des modules ES6 simplifie grandement la gestion des dépendances et évite au développeur d’avoir à toutes les intégrer au fichier HTML. Malheureusement, cela nécessite d’utiliser un serveur Web or l’objectif de cet exercice est de vous permettre de travailler uniquement à partir de vos fichiers et d’un navigateur, sans autre élément.
Le fichier celestial-body.js fournit un objet solarSystem
qui décrit partiellement le système solaire à l’aide d’objets de type CelestialBody
.
Editez votre fichier draw.js
- Affichez dans la console du navigateur le contenu de
solarSystem
pour bien comprendre comment se dernier est constitué :
console.log(solarSystem);
- Enregistrez vos fichiers et actualisez la page de votre navigateur.
- Rendez-vous dans l’onglet console des outils de développement. Vous devriez obtenir quelque chose comme ceci :

{ sun: CelestialBody }
afin de déployer la structure de l’objet. Vous verrez alors que solarSystem
possède un attribut sun. Ce dernier est un CelestialBody
qui possède 4 satellites : Mercure, Venus, la Terre et Mars. La terre possède elle-même 1 satellite : la Lune.Vous trouverez également d’autres attributs que ceux listés dans la modélisation UML précédente. N’en tenez pas compte pour le moment, il seront utiles lors de la troisième partie de cette exercice.
Dessin d’un objet céleste
- Vous allez à présent créer une fonction
drawCelestialBody
qui prendra en paramètre l’objet céleste à dessiner :
function drawCelestialBody(celestialBody)
{
}
Les corps célestes seront représentés à l’aide de cercles dont la taille sera définit par l’attribut radius
. Pour tracer un cercle, vous devrez utiliser la méthode arc
du contexte graphique :
context.arc(centerX, centerY, radius, startAngle, endAngle, counterClockWise);
centerX
etcenterY
correspondent aux coordonnées du centre du cercle à dessinerradius
est le rayon du cerclestartAngle
etendAngle
fournissent quant à eux le point de départ et d’arrivée de l’arc de cercle à tracer, sous la forme d’angle exprimés en radians. Pour un cercle complet, il faudra donc partir de l’angle 0 et terminer à l’angle 2π (« deux fois PI »).- Enfin,
antiClockWise
indique si le tracé doit être réalisé dans le sens trigonométrique (true
) ou dans le sens des aiguilles d’une montre (false
). Par défaut, ce paramètre vauttrue
.
Ainsi, dans le cadre de notre fonction drawCelestialBody, nous obtenons :
function drawCelestialBody(celestialBody)
{
// Starts the drawing
context.beginPath();
// Prepare the drawing of a complete circle
context.arc(0, 0, celestialBody.radius, 0, 2 * Math.PI);
// Sets the filling color
context.fillStyle = celestialBody.color;
// Fills the circle
context.fill();
}
- Remplacez l’appel de la méthode
drawSquare
dansresize
, par l’appel dedrawCelestialBody
en lui passant en paramètre l’attributsun
desolarSystem
:
function resize()
{
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
drawCelestialBody(solarSystem.sun);
}
- Enregistrez tous vos fichiers et actualisez la page Web. Vous devriez apercevoir un quart de soleil coincé dans l’angle supérieur gauche de votre page Web :

En informatique, la coordonnée (0 ; 0) est située dans l’angle supérieur gauche du support que ce dernier soit un écran, une image, ou, comme ici, un canvas.
Pour centrer le soleil dans la page, une première solution serait d’indiquer les coordonnées du centre du canvas à la méthode arc. Ainsi le centre du cercle se trouverait bien au centre de la page.
- Effectuez la modification suivante dans la méthode drawCelestialBody :
arc(canvas.width / 2, canvas.height / 2, celestialBody.radius, 0, 2 * Math.PI);
- Enregistrez la modification et actualiser la page de votre navigateur. Le soleil devrait être centré.
L’inconvénient de cette méthode est que drawCelestialBody
détermine l’endroit où dessiner le corps céleste à partir de coordonnées absolues, ce qui va poser problème lorsque nous allons devoir dessiner les satellites.
Les transformations : translation
Une autre solution, consiste à utiliser les transformations du canvas, qui permettent de déplacer le repère et l’emplacement de la coordonnée (0 ; 0) :

- Annulez les modifications de la méthode
drawCelestialBody
pour que le cercle ait de nouveau pour centre la coordonnée (0 ; 0).
- Créez une nouvelle méthode
drawSolarSystem
qui déplacera le repère au centre du canvas et appellera ensuite la méthodedrawCelestialBody
:
function drawSolarSystem()
{
//Moves the coordinate system to the center of the canvas
context.translate(canvas.width / 2, canvas.height / 2);
//Draws the solar system starting with the sun
drawCelestialBody(solarSystem.sun);
}
- Modifiez la méthode
resize
pour que cette dernière appelledrawSolarSystem
au lieu dedrawCelestialBody
.
- Enregistrez et actualisez la page de votre navigateur. Le soleil se trouve toujours au centre de la page.
Attention : les transformations du systèmes de coordonnées persistent dans le temps et se cumulent à chaque appel. Pour éviter cela, il est important de borner les transformations avec des appels aux méthodes
save
etrestore
dont les buts respectifs sont de mémoriser la configuration actuelle du contexte graphique et de la rétablir.
- Effectuez une sauvegarde du contexte graphique avant l’appel de la méthode
translate
. Puis restaurez le contexte dans son état initial à la fin de la méthodedrawSolarSystem
:
function drawSolarSystem()
{
//Saves the context
context.save();
//Moves the coordinate system to the center of the canvas
context.translate(canvas.width / 2, canvas.height / 2);
//Draws the solar system starting with the sun
drawCelestialBody(solarSystem.sun);
//Restores the context to its states at the previous call of save
context.restore();
}
Recursivité
Nous sommes à présent en mesure de dessiner un corps céleste. reste à dessiner ses satellites qui sont, eux-mêmes, des corps céleste. Nous allons donc utiliser la même méthode drawCelestialBody
pour les représenter.
- Modifiez la méthode
drawCelestialBody
pour que cette dernière dessiner le corps céleste fournit en paramètre, puis dessine ses satellites s’il en possède.
function drawCelestialBody(celestialBody)
{
// Starts the drawing
context.beginPath();
// Prepare the drawing of a complete circle
context.arc(0, 0, celestialBody.radius, 0, 2 * Math.PI);
// Sets the filling color
context.fillStyle = celestialBody.color;
// Fills the circle
context.fill();
// Draws each satellite of the celestial body
celestialBody.satellites.forEach((satellite) => {
drawCelestialBody(satellite);
});
}
- Enregistrez les modification et recharger la page Web. Oh ! Une pokeball !
Vous en conviendrez, ce n’est pas le résultat escompté. Cela vient du fait que toutes les planètes sont dessiner aux même coordonnées que le soleil. Nous allons apporter une petite correction pour régler ce problème.
Chaque corps céleste possède un attribut distance
qui indique la distance qui le sépare de son parent. Cette donnée va nous permettre de positionner les satellites relativement par rapport à leur parent. Et, comme nous l’avons vu plus tôt, les transformations du systèmes de coordonnées sont cumulables. Cela va rendre les choses très simples.
- Modifiez la méthode
drawCelestialBody
pour que cette dernière déplace le repère de coordonnées de la distance qui sépare le corps céleste à dessiner de son parent.
function drawCelestialBody(celestialBody)
{
// Saves the context in its current state
context.save();
// Translates the coordinate system with the vector (celestialBody.distance ; 0)
context.translate(celestialBody.distance, 0);
// Starts the drawing
context.beginPath();
// Prepare the drawing of a complete circle
context.arc(0, 0, celestialBody.radius, 0, 2 * Math.PI);
// Sets the filling color
context.fillStyle = celestialBody.color;
// Fills the circle
context.fill();
// Draws each satellite of the celestial body
celestialBody.satellites.forEach((satellite) => {
drawCelestialBody(satellite);
});
// Restores the context on its initial state
context.restore();
}
Ca commence à ressembler à quelque chose.
- Modifiez la couleur d’arrière-plan de la page, dans le fichier
style.css
, pour que ce soit un peu plus immersif :
body {
margin: 0;
width: 100vw;
height: 100vh;
background-color: #1a1a1a;
}
Nous allons ajouter le tracé de l’orbite de chaque satellite. Pour cela nous utiliserons toujours la méthode arc
, mais au lieu d’utiliser fillStyle
et fill
pour remplir le cercle, nous utiliserons strokeStyle
et stroke
pour tracé le contour du cercle.
- Créez une méthode
drawOrbit
qui tracera l’orbite du corps céleste fourni en paramètre :
function drawOrbit(celestialBody)
{
// Starts the drawing
context.beginPath();
// Prepare the drawing of a complete circle
context.arc(0, 0, celestialBody.distance, 0, 2 * Math.PI);
// Sets the outline color of the circle
context.strokeStyle = "#333333";
// Draws the outline of the circle
context.stroke();
}
- Appelez la méthode
drawOrbit
dans la méthodedrawCelestialBody
, juste avant le dessin du satellite :
function drawCelestialBody(celestialBody)
{
// Saves the context in its current state
context.save();
// Translates the coordinate system with the vector (celestialBody.distance ; 0)
context.translate(celestialBody.distance, 0);
// Starts the drawing
context.beginPath();
// Prepare the drawing of a complete circle
context.arc(0, 0, celestialBody.radius, 0, 2 * Math.PI);
// Sets the filling color
context.fillStyle = celestialBody.color;
// Fills the circle
context.fill();
// Draws each satellite of the celestial body
celestialBody.satellites.forEach((satellite) => {
drawOrbit(satellite);
drawCelestialBody(satellite);
});
// Restores the context on its initial state
context.restore();
}
Vous devriez obtenir un système solaire partiel comme ci-dessous :

Voyons à présent comment animer tout cela !
Partie 3 – Animation
Une animation, comme un film, c’est avant tout une série d’images légèrement différentes qui affichées l’une après l’autre vont donner l’illusion d’un mouvement.
Animer notre système solaire va donc consister à le dessiner, puis faire évoluer la position des planètes, dessiner à nouveau, mettre à jour la position des planètes, …
Pour la mise à jour de la position des planètes, tout est prêt. Vous vous souvenez, un peu plus tôt, il vous était demandé d’ignorer certains attributs de CelestialBody. Il vont prendre tout leur sens dès à présent :

Parmi les nouveaux attributs, on retrouve :
rotationAngle
etrotationSpeed
qui décrivent respectivement l’angle et la vitesse de rotation de l’astre sur lui-même.orbitalAngle
etorbitalSpeed
représentent quant à eux l’angle et la vitesse de rotation du satellite autour de son parent.- La méthode
update
qui met à jour les positions angulaires des objets célestes en fonction du temps écoulé. Cette méthode est récursive et met à jour la position des satellites de l’objet.
Animation par intervalle
Comme vu précédemment, pour animer le dessin du système solaire, il va falloir régulièrement appeler la méthode drawSolarSystem
puis mettre à jour la position des planètes en appelant la méthode update
.
setInterval
crée un déclencheur qui exécute un traitement à intervalle régulier.
- Dans le fichier
draw.js
, créez une méthodeanimate
qui, toutes les 50ms, appelle la méthodedrawSolarSystem
suivi de la méthodeupdate
desolarSystem.sun
:
function animate()
{
// Executes the callback each 50ms
setInterval(() => {
// Draws the solar system
drawSolarSystem();
// Updates celestial bodies position
solarSystem.sun.update(50);
}, 50);
}
Pour rappel, la méthode
update
deCelestialBody
est récursive. Appeler la méthodeupdate
desolarSystem.sun
, mettra à jour la position du soleil, puis de ses satellites, des satellites de ses satellites, et ainsi de suite.
Ajoutez un appel à la fonction animate
à la fin de votre script.
L’animation est prête, reste à prendre en compte la rotation des satellites lors du dessin de ces derniers.
Les transformations : rotation
Nous avons vu précédemment comment déplacer le système de coordonnées pour dessine des objets les uns par rapport aux autres. Il est également possible de faire de même avec des rotations du repère. De cette façon, nous nous économisons des calculs à base de cosinus et de sinus pour faire tourner un objet autour d’un autre. Et surtout, il devient possible de faire pivoter des objets complexes comme une image. En effet, les primitives de dessin pour les images ne permettent de représenter ces dernières que horizontalement. Comment, dans ces conditions, la faire pivoter ? En pivotant le repère du canvas !

- Nous allons procéder de même pour faire tourner les satellites autour de leur parent.
- Modifiez la méthode
drawCelestialBody
pour faire pivoter le repère avant de réaliser la translation :
function drawCelestialBody(celestialBody)
{
// Saves the context in its current state
context.save();
// Rotates the coordinate system
context.rotate(celestialBody.orbitalAngle);
// Translates the coordinate system with the vector (celestialBody.distance ; 0)
context.translate(celestialBody.distance, 0);
...
}
- Enregistrez les modifications et actualisez la page de votre navigateur :

Ok, ça tourne, mais il manque quelque chose : effacer le canvas entre deux dessins. Pour cela, nous allons utiliser la méthode clearRect
du contexte graphique :
context.clearRect(x, y, width, height);
Cette méthode efface la zone définie par le rectangle de largeur width
, de hauteur height
et ayant son angle supérieur gauche aux coordonnées (x
; y
).
- Modifiez la méthode
animate
afin d’effacer le canvas entre deux dessins :
function animate()
{
// Executes the callback each 50ms
setInterval(() => {
// Clears the canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Draws the solar system
drawSolarSystem();
// Updates celestial bodies position
solarSystem.sun.update(50);
}, 50);
}
- Enregistrez les modifications et actualisez la page. Ce devrait-être beaucoup mieux.
Animation par requestAnimationFrame
L’animation actuelle fonctionne à partir d’un dessin réaliser à intervalle régulier. Ceci peut poser problème car nous ne savons pas combien de temps sera nécessaire à la réalisation du dit dessin. Le risque, lorsque l’on veut animer une scène avec un grand nombre d’images par seconde, est d’avoir un temps de rendu plus long que l’intervalle fixé entre deux images. Et là, c’est le drame.
Pour palier à ce problème, la méthode requestAnimationFrame
permet d’effectuer un traitement aussi souvent que possible tout en garantissant que le traitement précédent est terminé avant de lancer le suivant.
L’inconvénient sera qu’il faudra déterminer le temps qui s’est écoulé depuis le dernier dessin pour pouvoir mettre à jour convenablement la position des planètes. Pour cela, nous utiliserons performance.now()
qui retourne le nombre de millisecondes écoulées depuis l’ouverture de la page Web.
- Modifiez la méthode
animate
pour utiliserrequestAnimationFrame
au lieu desetInterval
et calculez le temps écoulé entre chaque animation pour mettre à jour convenablement la position des planètes :
function animate(lastUpdateTime)
{
// Gets the number of milliseconds elapsed from the beginning of the program
const now = performance.now();
// Computes the elpased time from the last update.
// If lastUpdateTime is equel to 0, it is the first frame, so update is not required.
const elapsedTime = lastUpdateTime === 0 ? 0 : now - lastUpdateTime;
// Clears the canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Draws the solar system
drawSolarSystem();
// Updates celestial bodies position
solarSystem.sun.update(elapsedTime);
// Requests a new frame as soon as possible
requestAnimationFrame(() => { animate(now) });
}
Enregistrez, actualisez et admirez la fluidité !
Partie 4 – Patterns, masques et dégradés
Chargement des textures
Si la représentation du système solaire, dans cet exercice, n’a pas vocation à être fidèle, tout cela reste très schématique. Voyons si nous ne pouvons pas faire mieux en intégrant quelques textures.
Pour cela, nous allons utiliser les Patterns
que fournit le canvas pour texturer des formes. Mais avant cela, il nous faut des textures !
- Créez un dossier
img
à la racine de votre site.
- Télécharger les fichiers suivants et placez-les dans le dossier
img
précédemment créé :
Il est important de bien conserver le nom des images téléchargées pour la suite de l’exercice.
Le processus de chargement des textures est déjà intégré à CelestialBody
. Toutefois, il va être nécessaire de déclencher celui-ci lorsque votre page Web sera chargée.
- Dans
draw.js
, ajoutez un gestionnaire d’événementsload
à l’objetwindow
qui initialisera les textures des corps célestes puis redimensionnera le canvas et démarrera l’animation :
window.addEventListener("load", () => {
solarSystem.sun.initTexture().then(() => {
resize();
animate();
});
});
initTexture
est une méthode asynchrone qui retourne une promesse. Deux notations sont possibles : celle indiquée précédemment et celle ci-dessous qui a ma préférence (n’oubliez pas leasync
) :
window.addEventListener("load", async () => {
await solarSystem.sun.initTexture()
resize();
animate();
});
initTexture
est récursive et initialise la texture du corps céleste et de ses éventuels satellites. Un seul appel pour le soleil est donc nécessaire pour initialiser toutes les textures du systèmes.
Important : les méthodes
resize
etanimate
seront appelées sitôt que la page aura terminée de charger tous ses éléments. Il n’est donc plus nécessaire d’appeler ces méthodes à la fin du script. Pensez bien à retirer les retirer.
- Pour vérifiez que les textures sont bien chargées, ajoutez la ligne suivante à la fin du gestionnaire d’événements précédent :
console.log(solarSystem.sun.texture);
- Enregistrez vos fichiers, actualisez la page Web et vérifiez dans la console de développement que vous n’avez pas d’erreur et que vous obtenez un affichage similaire à celui-ci :

- Vous pouvez retirer le
console.log
précédent.
Les patterns
Les textures étant chargée, il va falloir maintenant les appliquer aux disques qui représente le soleil et les planètes du système solaire.
Actuellement, nous utilisons une couleur unie pour définir le style de remplissage des formes :
context.fillStyle = "#FF0000";
Pour appliquer une texture comme style de remplissage, nous allons devoir créer un motif (Pattern) à partir de cette image, et c’est ce motif qui sera ensuite appliqué à la forme.
- Modifiez la section de code de
drawCelestialBody
qui définit la couleur de remplissage des cercles :
context.beginPath();
context.arc(0, 0, celestialBody.radius, 0, 2 * Math.PI);
const pattern = context.createPattern(celestialBody.texture, "no-repeat");
context.fillStyle = pattern;
context.fill();
- Enregistrez et actualisez la page.
On dirait bien que les planètes ont disparues ! Cela vient du fait que les textures utilisées sont plus grandes que les disques dessinés. Et comme les textures représentent des objets ronds, nous passons à coté :

Comme vous pouvez le voir sur l’illustration ci-dessus, la pattern n’est pas automatiquement redimensionné pour couvrir l’objet. Il va donc falloir traiter cela nous-mêmes et, là encore, les transformations vont nous être très utiles.
Première chose à faire : définir les coefficients d’échelle à appliquer à la texture pour qu’elle fasse la même taille que le disque à remplir :
const coefEchelle = (celestialBody.radius * 2) / celestialBody.texture.width;
Malheureusement, nous ne pouvons pas directement appliquer le coefficient d’échelle au pattern. Il va falloir l’appliquer temporairement au contexte graphique, appliquer le pattern puis revenir à l’échelle initiale. Cela peut sembler étrange, mais une fois que l’on a compris le truc, tout s’éclaire :




Voyons ce que cela donne dans notre cas :
// Prepares the drawing of the circle
context.beginPath();
context.arc(0, 0, celestialBody.radius, 0, 2 * Math.PI);
// Creates the pattern and sets the fill style with it
const pattern = context.createPattern(celestialBody.texture, "no-repeat");
context.fillStyle = pattern;
// Computes the scale required to apply the pattern at the good dimensions
const coefEchelle = (celestialBody.radius * 2) / celestialBody.texture.width;
// Saves the current context
context.save()
// Translates the coordinate system to the top right corner of the circle
context.translate(-celestialBody.radius, -celestialBody.radius);
// Sets the scales of the coordinate system horizontally and vertically
context.scale(coefEchelle, coefEchelle);
// Fills the previously prepared circle with the scaled pattern
context.fill();
// Restores the coordinate system to its initial state
context.restore();
- Modifiez le code de la méthode
drawCelestialBody
à partir des informations précédentes.
- Enregistrez les modifications et actualisez la page web. Tada !

Rotation des planètes
Dans la partie précédente, nous avions mis en place la rotation orbitale des planètes. Mais nous ne nous étions pas occupé de leur rotation sur leur propre axe. Si cela ne se voyait pas avec des disque de couleur unie, avec des textures, ce n’est plus possible !
La classe CelestialBody
possède un attribut rotationAngle
qui détermine l’angle de rotation de la planète sur elle-même.
- Modifiez la méthode
drawCelestialBody
afin d’intégrer cette rotation supplémentaire.
Masques
Parfait, notre système solaire est animé de manière cohérente. Il manque encore une petite chose, non des moindres, qui apportera une touche de réalisme incroyable : les ombres.
En effet, nos planètes tournent autour du soleil mais elles ne sont pas affectées par la lumière qui leur parvient. Avec une bibliothèque 3D comme Three.js ou Babylon.js, ce serait très simple à mettre en place. Mais, ici, nous allons gérer tout cela en 2D et manuellement.
Pour cela, nous allons utiliser une fonctionnalité très puissante du Canvas HTML5 : les masques.
Le principe est simple : nous traçons le contour d’une forme puis nous appelons la méthode clip du contexte graphique. A partir de là, seuls les tracés contenus à l’intérieur de la forme précédente apparaîtront sur le dessin final :

save
) et tracé du contour du masque
clip
).

restore
). Ici, nous allons utilisé les masques pour donner l’impression qu’une face de la planète est éclairée et une autre plongée dans l’obscurité :

Pour obtenir ce résultat, nous allons précéder en quatre étapes :


clip
)

restore
) avant de poursuivre le dessin.- Modifiez la méthode
drawCelestialBody
pour appliquer une ombre aux corps célestes qui le nécessitent (CelestialBody
possède l’attributhasShadow
qui est àtrue
si la planète projette une ombre).
- Enregistrez et testez. Vous devriez obtenir quelques chose comme ceci :

Niveau code, voici ce que vous devriez avoir :
function drawCelestialBody(celestialBody)
{
//Saves the context in its current state [*0]
context.save();
context.rotate(celestialBody.orbitalAngle);
//Translates the coordinate system with the vector (celestialBody.distance ; 0)
context.translate(celestialBody.distance, 0);
if(celestialBody.hasShadow)
{
//Draws a black disque which will be the shadowed part of the planet
context.beginPath();
context.arc(0, 0, celestialBody.radius, 0, 2 * Math.PI);
context.fillStyle = "#000000";
context.fill();
//Saves the current context [*1]
context.save();
//Prepares the drawing of the mask
context.beginPath();
context.arc(-celestialBody.radius * 2, 0, celestialBody.radius * 2, 0, 2 * Math.PI);
//Create a mask from the previous prepared drawing
context.clip();
}
//Starts the drawing
context.beginPath();
//Prepare the drawing of a complete circle
context.arc(0, 0, celestialBody.radius, 0, 2 * Math.PI);
//Creates a pattern from the texture of the celestial body
const pattern = context.createPattern(celestialBody.texture, "no-repeat");
const coef = (celestialBody.radius * 2) / celestialBody.texture.width;
//Saves the current context [*2]
context.save();
//Rotates the celestial body on its own axis
context.rotate(celestialBody.rotationAngle);
//Moves and scales the coordinate system to apply the pattern
context.translate(-celestialBody.radius, -celestialBody.radius);
context.scale(coef, coef);
//Sets the filling color
context.fillStyle = pattern; //celestialBody.color;
//Fills the circle
context.fill();
//Restores the context [*2]
context.restore();
if(celestialBody.hasShadow)
{
//Restores the context and disable the mask [*1]
context.restore();
}
//Draws each satellite of the celestial body
celestialBody.satellites.forEach((satellite) => {
drawOrbit(satellite);
drawCelestialBody(satellite);
});
//Restores the context on its initial state [*0]
context.restore();
}
Nous allons ajouter un dernier effet qui permettra de rendre moins net la séparation entre la face éclairée et la partie à l’ombre des planètes.
Dégradés
Le canvas permet de réaliser des dégradés linéaires et radiaux :


Dans notre cas, nous allons utiliser un dégradé radial pour appliquer un effet d’ombre progressive sur la bordure de la zone éclairée de la planète :

La création d’un dégradé se fait en deux temps :
- On commence par déterminer les coordonnées de début et de fin du dégradé (plus les rayons dans les cas d’un dégradé radial) :
context.createLinearGradient
oucontext.createRadialGradient
- On définit les teintes par lesquelles passera le dégradé :
gradient.addColorStop
Pour obtenir le résultat ci-dessus, nous allons cumulé masque et dégradé radial. Nous allons procédé en quatre étapes :




- Modifiez la méthode
drawCelestialBody
pour les planètes qui possèdent une ombre.
- Enregistrez votre fichier et actualisez votre page Web.
Votre système devrait ressembler à cela :

Pour terminer et ajouter la touche ultime, nous allons appliquer un dégradé CSS au niveau de l’arrière plan. En effet, nous avons affecté une teinte unie plutôt dans l’exercice. Or, avec un dégradé qui s’assombrit au fur et à mesure que l’on s’éloigne du soleil, le résultat sera top !
- Dans le fichier style.css, remplacez la propriété background-color du body par la ligne suivante :
background: radial-gradient(#1a1a1a, #000000);
- Enregistrez tous vos fichiers une dernière fois et admirez le travail !
Félicitations ! Vous êtes parvenu à la fin de cet exercice. Vous avez parcouru les principales fonctionnalités du Canvas HTML 5. Il en reste d’autres comme les filtres et les compositions qui permettent d’aller encore plus loin.