À propos de ce cours
Chapitre 15 — Animations et graphismes avec Canvas · 15.3 Animer un dessin Canvas (requestAnimationFrame)
• écrire une boucle d'animation : effacer, mettre à jour, dessiner ;
• utiliser le temps écoulé (
dt) pour une vitesse indépendante de l'écran ;• faire rebondir un objet sur les bords ;
• animer de nombreux objets avec des classes, puis mettre en pause.
1. Le principe : redessiner toute la scène à chaque image
Le canvas ne se souvient pas des formes. Pour déplacer une balle, on ne la « bouge » pas : on efface tout, on met à jour sa position, puis on la redessine, environ 60 fois par seconde.

Figure 1 — La boucle d'animation.
2. Une balle qui rebondit
📄 l3_balle.html
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"></head>
<body>
<canvas id="jeu" width="600" height="300" style="border: 1px solid #ccc; background: #fafafa"></canvas>
<p><label><input type="checkbox" id="trainee"> Laisser une traînée</label></p>
<script>
const canvas = document.querySelector("#jeu");
const ctx = canvas.getContext("2d");
// État de la balle — vitesses en PIXELS PAR SECONDE
const balle = { x: 80, y: 60, rayon: 18, vx: 260, vy: 180, couleur: "#e53935" };
let tempsPrecedent = null;
function mettreAJour(dt) { // dt = secondes écoulées
balle.x += balle.vx * dt;
balle.y += balle.vy * dt;
// Rebonds sur les bords : on inverse la vitesse et on replace la balle
if (balle.x + balle.rayon > canvas.width) { balle.x = canvas.width - balle.rayon; balle.vx *= -1; }
if (balle.x - balle.rayon < 0) { balle.x = balle.rayon; balle.vx *= -1; }
if (balle.y + balle.rayon > canvas.height) { balle.y = canvas.height - balle.rayon; balle.vy *= -1; }
if (balle.y - balle.rayon < 0) { balle.y = balle.rayon; balle.vy *= -1; }
}
function dessiner() {
if (document.querySelector("#trainee").checked) {
ctx.fillStyle = "rgba(250, 250, 250, 0.15)"; // efface partiellement
ctx.fillRect(0, 0, canvas.width, canvas.height);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height); // efface tout
}
ctx.beginPath();
ctx.arc(balle.x, balle.y, balle.rayon, 0, Math.PI * 2);
ctx.fillStyle = balle.couleur;
ctx.fill();
}
// LA BOUCLE D'ANIMATION : mettre à jour → dessiner → redemander une image
function boucle(temps) {
const dt = tempsPrecedent === null ? 0 : (temps - tempsPrecedent) / 1000;
tempsPrecedent = temps;
mettreAJour(Math.min(dt, 0.05)); // limite si l'onglet a été mis en pause
dessiner();
requestAnimationFrame(boucle);
}
requestAnimationFrame(boucle);
</script>
</body>
</html>

Capture après 1,2 s
- Les vitesses sont en pixels par seconde et multipliées par
dt(en secondes) : la balle va aussi vite sur un écran 60 Hz que sur un écran 144 Hz. dtest limité à 0,05 s : si l'onglet a été caché, le navigateur a suspendu la boucle et le premierdtserait énorme (la balle traverserait les murs).- Au rebond, on inverse la vitesse et on replace la balle contre le bord : sinon elle peut rester « coincée » dans le mur.
- La mise à jour et le dessin sont dans deux fonctions séparées : on peut modifier l'un sans toucher à l'autre.
En cochant « Laisser une traînée », on remplace clearRect par un rectangle semi-transparent : les anciennes images s'estompent progressivement.

Effet de traînée
3. Beaucoup d'objets : des classes
Pour un feu d'artifice, chaque particule a sa position, sa vitesse et sa durée de vie : c'est le cas idéal pour une classe (chapitre 16).
📄 l3_particules.html
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"></head>
<body>
<canvas id="jeu" width="600" height="300" style="background: #0b1026"></canvas>
<p><button id="pause">Pause</button> <span id="infos"></span></p>
<script>
const canvas = document.querySelector("#jeu");
const ctx = canvas.getContext("2d");
const GRAVITE = 300; // px/s²
// Une classe pour chaque particule (voir chapitre 16)
class Particule {
constructor(x, y, couleur) {
const angle = Math.random() * Math.PI * 2;
const vitesse = 60 + Math.random() * 180;
this.x = x;
this.y = y;
this.vx = Math.cos(angle) * vitesse;
this.vy = Math.sin(angle) * vitesse;
this.vie = 1 + Math.random(); // secondes restantes
this.couleur = couleur;
}
mettreAJour(dt) {
this.vy += GRAVITE * dt;
this.x += this.vx * dt;
this.y += this.vy * dt;
this.vie -= dt;
}
dessiner(ctx) {
ctx.globalAlpha = Math.max(this.vie, 0) / 2;
ctx.fillStyle = this.couleur;
ctx.fillRect(this.x, this.y, 3, 3);
ctx.globalAlpha = 1;
}
get morte() {
return this.vie <= 0 || this.y > canvas.height;
}
}
let particules = [];
function feuDArtifice() {
const couleur = `hsl(${Math.random() * 360}, 90%, 60%)`;
const x = 80 + Math.random() * 440;
const y = 60 + Math.random() * 100;
for (let i = 0; i < 120; i++) particules.push(new Particule(x, y, couleur));
}
let idAnimation = null;
let tempsPrecedent = null;
let prochainFeu = 0;
function boucle(temps) {
const dt = tempsPrecedent === null ? 0 : Math.min((temps - tempsPrecedent) / 1000, 0.05);
tempsPrecedent = temps;
prochainFeu -= dt;
if (prochainFeu <= 0) { feuDArtifice(); prochainFeu = 0.6; }
particules.forEach((p) => p.mettreAJour(dt));
particules = particules.filter((p) => !p.morte); // on supprime les mortes
ctx.fillStyle = "rgba(11, 16, 38, 0.3)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
particules.forEach((p) => p.dessiner(ctx));
document.querySelector("#infos").textContent =
`${particules.length} particules · ${dt ? Math.round(1 / dt) : 0} images/s`;
idAnimation = requestAnimationFrame(boucle);
}
function demarrer() { tempsPrecedent = null; idAnimation = requestAnimationFrame(boucle); }
function arreter() { cancelAnimationFrame(idAnimation); idAnimation = null; }
document.querySelector("#pause").addEventListener("click", (ev) => {
if (idAnimation) { arreter(); ev.target.textContent = "Reprendre"; }
else { demarrer(); ev.target.textContent = "Pause"; }
});
demarrer();
</script>
</body>
</html>

Capture après 2,5 s : environ 240 particules à 60 images par seconde
- Chaque particule sait se
mettreAJour(dt)et sedessiner(ctx): la boucle reste courte. - Les particules « mortes » sont retirées du tableau avec
filter, sinon le tableau grossirait sans fin. requestAnimationFramerenvoie un identifiant :cancelAnimationFrame(id)arrête la boucle (bouton Pause).- La gravité modifie la vitesse (
vy += GRAVITE × dt), la vitesse modifie la position : c'est le principe de toute simulation physique simple.
• Boucle : calculer
dt → mettre à jour → effacer et dessiner → requestAnimationFrame(boucle). • Vitesses en px/s multipliées par
dt; limiter dt après une pause de l'onglet.• Rebond : inverser la vitesse et replacer l'objet contre le bord.
• Nombreux objets : une classe avec
mettreAJour et dessiner; supprimer les objets inutiles.•
cancelAnimationFrame(id) pour arrêter.🛠 Activité 3 — Un aquarium animé
Mission : Créez un aquarium : des poissons nagent de gauche à droite, font demi-tour au bord, et des bulles montent depuis le fond.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Quelles données doit contenir un poisson ? Écrivez la classe
PoissonavecmettreAJour(dt)etdessiner(ctx). Comment dessinez-vous un poisson tourné vers la gauche ou vers la droite (indice :scale(-1, 1)) ? - Créez 8 poissons de tailles et de vitesses aléatoires. Si vous oubliez
clearRect, que voyez-vous ? - Ajoutez des bulles qui naissent au fond, montent en ondulant (
Math.sin) et disparaissent en haut. Comment éviter que le tableau des bulles grossisse indéfiniment ? Affichez sa taille pour le vérifier. - Remplacez
x += vitesse * dtparx += 2. Testez sur deux écrans différents, ou simulez un écran lent en ajoutant une boucle qui ralentit chaque image. Que constatez-vous ? Concluez. - Ajoutez un bouton Pause. Après 10 secondes de pause, reprenez : les poissons font-ils un bond ? Pourquoi ? Comment l'éviter ?
- Mesurez les images par seconde avec 10, 100, puis 2 000 poissons. À partir de combien l'animation devient-elle saccadée sur votre ordinateur ?
Je vérifie / je rends :
- ☐ Le fichier
aquarium.html. - ☐ Le tableau de vos mesures (nombre de poissons → images par seconde).
← 15.2 Dessiner des graphismes avec l'API <canvas>15.4 Interagir avec le canvas : souris, clavier et collisions →