Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

Animer un dessin Canvas (requestAnimationFrame)

Animer un dessin Canvas (requestAnimationFrame). Dans cette leçon du cours JavaScript, vous apprendrez à : écrire une boucle d'animation : effacer, mettre à jour, dessiner ; utiliser le temps écoulé (...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Animer un dessin Canvas (requestAnimationFrame). Dans cette leçon du cours JavaScript, vous apprendrez à : é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.

Chapitre 15 — Animations et graphismes avec Canvas · 15.3 Animer un dessin Canvas (requestAnimationFrame)

🎯 Objectifs de la leçon
• é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.

Les quatre étapes de la boucle d'animation
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
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.
  • dt est limité à 0,05 s : si l'onglet a été caché, le navigateur a suspendu la boucle et le premier dt serait é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.

Balle avec traînée
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
Capture après 2,5 s : environ 240 particules à 60 images par seconde

  • Chaque particule sait se mettreAJour(dt) et se dessiner(ctx): la boucle reste courte.
  • Les particules « mortes » sont retirées du tableau avec filter, sinon le tableau grossirait sans fin.
  • requestAnimationFrame renvoie 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.
ℹ️ À retenir
• 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) :

  1. Quelles données doit contenir un poisson ? Écrivez la classe Poisson avec mettreAJour(dt) et dessiner(ctx). Comment dessinez-vous un poisson tourné vers la gauche ou vers la droite (indice : scale(-1, 1)) ?
  2. Créez 8 poissons de tailles et de vitesses aléatoires. Si vous oubliez clearRect, que voyez-vous ?
  3. 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.
  4. Remplacez x += vitesse * dt par x += 2. Testez sur deux écrans différents, ou simulez un écran lent en ajoutant une boucle qui ralentit chaque image. Que constatez-vous ? Concluez.
  5. Ajoutez un bouton Pause. Après 10 secondes de pause, reprenez : les poissons font-ils un bond ? Pourquoi ? Comment l'éviter ?
  6. 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 →