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

Interagir avec le canvas : souris, clavier et collisions

Interagir avec le canvas : souris, clavier et collisions. Dans cette leçon du cours JavaScript, vous apprendrez à : convertir la position de la souris ou du doigt en coordonnées du canvas ; dessiner à...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Interagir avec le canvas : souris, clavier et collisions. Dans cette leçon du cours JavaScript, vous apprendrez à : convertir la position de la souris ou du doigt en coordonnées du canvas ; dessiner à la souris avec les événements pointer ; gérer le clavier pour un déplacement fluide ; détecter les collisions entre points, rectangles et cercles.

Chapitre 15 — Animations et graphismes avec Canvas · 15.4 Interagir avec le canvas : souris, clavier et collisions

ℹ️ À retenir
Nouvelle leçon : un dessin animé devient une application ou un jeu dès que l'utilisateur peut agir dessus. Ces techniques sont indispensables pour le TP.
🎯 Objectifs de la leçon
• convertir la position de la souris ou du doigt en coordonnées du canvas ;
• dessiner à la souris avec les événements pointer;
• gérer le clavier pour un déplacement fluide ;
• détecter les collisions entre points, rectangles et cercles.

Conversion des coordonnées et tests de collision
Figure 1 — Les deux outils de l'interaction.

1. La souris et le doigt : les événements pointer

Les événements pointerdown, pointermove et pointerup fonctionnent avec la souris, le stylet et le toucher. Leurs coordonnées clientX/clientY sont celles de la fenêtre : il faut les convertir dans le repère du canvas, en tenant compte de sa position et de sa taille affichée.

📄 l4_dessin.html

<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"></head>
<body>
  <p>
    <input type="color" id="couleur" value="#1565c0">
    <input type="range" id="taille" min="2" max="40" value="8">
    <button id="effacer">Effacer</button>
  </p>
  <!-- Affiché à 100 % de la largeur : la taille CSS diffère de la taille en pixels -->
  <canvas id="toile" width="600" height="300" style="width: 100%; max-width: 900px; border: 1px solid #ccc; touch-action: none"></canvas>
  <script>
    const canvas = document.querySelector("#toile");
    const ctx = canvas.getContext("2d");
    ctx.lineCap = "round";
    ctx.lineJoin = "round";

    // Convertit la position de la souris (pixels de l'écran) en coordonnées du canvas
    function positionDans(ev) {
      const rect = canvas.getBoundingClientRect();
      return {
        x: (ev.clientX - rect.left) * (canvas.width / rect.width),
        y: (ev.clientY - rect.top) * (canvas.height / rect.height),
      };
    }

    let enTrainDeDessiner = false;
    let dernier = null;

    // Événements « pointer » : souris, stylet ET doigt
    canvas.addEventListener("pointerdown", (ev) => {
      enTrainDeDessiner = true;
      dernier = positionDans(ev);
    });
    canvas.addEventListener("pointermove", (ev) => {
      if (!enTrainDeDessiner) return;
      const p = positionDans(ev);
      ctx.strokeStyle = document.querySelector("#couleur").value;
      ctx.lineWidth = document.querySelector("#taille").value;
      ctx.beginPath();
      ctx.moveTo(dernier.x, dernier.y);
      ctx.lineTo(p.x, p.y);
      ctx.stroke();
      dernier = p;
    });
    window.addEventListener("pointerup", () => (enTrainDeDessiner = false));

    document.querySelector("#effacer").addEventListener("click", () =>
      ctx.clearRect(0, 0, canvas.width, canvas.height));
  </script>
</body>
</html>

Tracés réalisés en simulant la souris pendant le test de l'exemple
Tracés réalisés en simulant la souris pendant le test de l'exemple

  • Sans la multiplication par canvas.width / rect.width, le trait serait décalé dès que le canvas est affiché plus grand ou plus petit que sa taille réelle.
  • touch-action: none empêche le téléphone de faire défiler la page pendant qu'on dessine.
  • pointerup est écouté sur window: si on relâche le bouton hors du canvas, le tracé s'arrête quand même.

2. Le clavier : mémoriser les touches enfoncées

Déplacer un objet directement dans keydown donne un mouvement saccadé (le clavier répète la touche après un délai) et empêche les diagonales. La bonne méthode : mémoriser les touches enfoncées dans un Set, et déplacer l'objet dans la boucle d'animation.

📄 l4_clavier.html

<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"></head>
<body>
  <canvas id="jeu" width="600" height="300" style="background: #102027"></canvas>
  <p>Flèches pour se déplacer, espace pour changer de couleur.</p>
  <script>
    const canvas = document.querySelector("#jeu");
    const ctx = canvas.getContext("2d");
    const vaisseau = { x: 300, y: 150, taille: 24, vitesse: 240, couleur: "#4fc3f7" };

    // On mémorise les touches ENFONCÉES : mouvement fluide et diagonales
    const touches = new Set();
    window.addEventListener("keydown", (ev) => {
      if (ev.key.startsWith("Arrow") || ev.key === " ") ev.preventDefault(); // pas de défilement
      touches.add(ev.key);
      if (ev.key === " " && !ev.repeat) {
        vaisseau.couleur = `hsl(${Math.random() * 360}, 80%, 65%)`;
      }
    });
    window.addEventListener("keyup", (ev) => touches.delete(ev.key));

    function mettreAJour(dt) {
      if (touches.has("ArrowLeft"))  vaisseau.x -= vaisseau.vitesse * dt;
      if (touches.has("ArrowRight")) vaisseau.x += vaisseau.vitesse * dt;
      if (touches.has("ArrowUp"))    vaisseau.y -= vaisseau.vitesse * dt;
      if (touches.has("ArrowDown"))  vaisseau.y += vaisseau.vitesse * dt;
      // Rester dans le canvas
      vaisseau.x = Math.max(vaisseau.taille, Math.min(canvas.width - vaisseau.taille, vaisseau.x));
      vaisseau.y = Math.max(vaisseau.taille, Math.min(canvas.height - vaisseau.taille, vaisseau.y));
    }

    function dessiner() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      const { x, y, taille } = vaisseau;
      ctx.beginPath();
      ctx.moveTo(x, y - taille);
      ctx.lineTo(x + taille * 0.8, y + taille);
      ctx.lineTo(x - taille * 0.8, y + taille);
      ctx.closePath();
      ctx.fillStyle = vaisseau.couleur;
      ctx.fill();
    }

    let tPrec = null;
    function boucle(t) {
      const dt = tPrec === null ? 0 : Math.min((t - tPrec) / 1000, 0.05);
      tPrec = t;
      mettreAJour(dt);
      dessiner();
      requestAnimationFrame(boucle);
    }
    requestAnimationFrame(boucle);
  </script>
</body>
</html>

Après 0,3 s avec → et ↑ enfoncées en même temps, puis Espace
Après 0,3 s avec → et ↑ enfoncées en même temps, puis Espace

3. Les collisions

Le canvas ne sait pas qu'il contient des formes : c'est à nous de garder les formes dans un tableau et de tester nous-mêmes si un point ou une autre forme les touche. Faites glisser les formes dans l'exemple :

📄 l4_collisions.html

<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"></head>
<body>
  <canvas id="scene" width="600" height="300" style="border: 1px solid #ccc"></canvas>
  <p>Faites glisser les formes : elles deviennent rouges quand elles se touchent.</p>
  <script>
    const canvas = document.querySelector("#scene");
    const ctx = canvas.getContext("2d");

    const formes = [
      { type: "cercle", x: 120, y: 150, r: 50 },
      { type: "cercle", x: 300, y: 110, r: 35 },
      { type: "rect", x: 400, y: 150, l: 120, h: 80 },
      { type: "rect", x: 200, y: 200, l: 90, h: 60 },
    ];

    // ---- Tests de collision ----
    const pointDansCercle = (px, py, c) => (px - c.x) ** 2 + (py - c.y) ** 2 <= c.r ** 2;
    const pointDansRect = (px, py, r) => px >= r.x && px <= r.x + r.l && py >= r.y && py <= r.y + r.h;
    const cercleCercle = (a, b) => (a.x - b.x) ** 2 + (a.y - b.y) ** 2 <= (a.r + b.r) ** 2;
    const rectRect = (a, b) =>
      a.x < b.x + b.l && a.x + a.l > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
    function cercleRect(c, r) {
      // point du rectangle le plus proche du centre du cercle
      const px = Math.max(r.x, Math.min(c.x, r.x + r.l));
      const py = Math.max(r.y, Math.min(c.y, r.y + r.h));
      return pointDansCercle(px, py, c);
    }
    function collision(a, b) {
      if (a.type === "cercle" && b.type === "cercle") return cercleCercle(a, b);
      if (a.type === "rect" && b.type === "rect") return rectRect(a, b);
      return a.type === "cercle" ? cercleRect(a, b) : cercleRect(b, a);
    }
    const contient = (f, px, py) => f.type === "cercle" ? pointDansCercle(px, py, f) : pointDansRect(px, py, f);

    function dessiner() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      for (const f of formes) {
        const touche = formes.some((g) => g !== f && collision(f, g));
        ctx.fillStyle = touche ? "rgba(229, 57, 53, 0.7)" : "rgba(30, 136, 229, 0.6)";
        ctx.beginPath();
        if (f.type === "cercle") ctx.arc(f.x, f.y, f.r, 0, Math.PI * 2);
        else ctx.rect(f.x, f.y, f.l, f.h);
        ctx.fill();
      }
    }

    // ---- Glisser-déposer ----
    let saisie = null;
    function pos(ev) {
      const r = canvas.getBoundingClientRect();
      return { x: (ev.clientX - r.left) * canvas.width / r.width, y: (ev.clientY - r.top) * canvas.height / r.height };
    }
    canvas.addEventListener("pointerdown", (ev) => {
      const p = pos(ev);
      const f = formes.findLast((f) => contient(f, p.x, p.y)); // la plus « au-dessus »
      if (f) saisie = { forme: f, dx: p.x - f.x, dy: p.y - f.y };
    });
    canvas.addEventListener("pointermove", (ev) => {
      if (!saisie) return;
      const p = pos(ev);
      saisie.forme.x = p.x - saisie.dx;
      saisie.forme.y = p.y - saisie.dy;
      dessiner(); // pas de boucle : on ne redessine que quand quelque chose change
    });
    window.addEventListener("pointerup", () => (saisie = null));
    dessiner();
  </script>
</body>
</html>

Le cercle a été glissé sur le petit rectangle : les deux deviennent rouges
Le cercle a été glissé sur le petit rectangle : les deux deviennent rouges

  • On compare les carrés des distances : c'est plus rapide que Math.sqrt et donne le même résultat.
  • Pour savoir quelle forme est sous la souris, on parcourt le tableau à l'envers (findLast) : la dernière dessinée est au-dessus.
  • Ici, pas de boucle d'animation : on ne redessine que quand quelque chose change. C'est suffisant pour un éditeur, et économe.
ℹ️ À retenir
• Position dans le canvas : (clientX − rect.left) × canvas.width / rect.width (idem en y).
• Événements pointer…: souris, stylet et toucher ; touch-action: none sur mobile.
• Clavier : un Set des touches enfoncées, lu dans la boucle ; preventDefault pour les flèches.
• Collisions : point/rectangle, cercle/cercle (distance²), rectangle/rectangle, cercle/rectangle (point le plus proche).

🛠 Activité 4 — Le jeu « attrape les étoiles »

Mission : Un panier en bas de l'écran, déplacé au clavier et à la souris, doit attraper des étoiles qui tombent. Une étoile ratée fait perdre une vie.

Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :

  1. Quels objets votre jeu contient-il ? Quelles formes géométriques utilisez-vous pour tester les collisions (le dessin peut être plus détaillé que la forme de collision) ?
  2. Écrivez la conversion des coordonnées de la souris. Testez en réduisant la fenêtre avec le canvas à width: 100%: le panier suit-il toujours exactement la souris ?
  3. Déplacez d'abord le panier directement dans keydown, puis avec un Set de touches lu dans la boucle. Décrivez la différence ressentie.
  4. Quel test de collision utilisez-vous entre une étoile (cercle) et le panier (rectangle) ? Écrivez-le et testez-le avec console.log sur quelques cas faciles à vérifier.
  5. Une étoile très rapide traverse parfois le panier sans être attrapée. Pourquoi ? Proposez une solution.
  6. Ajoutez le score, trois vies et un écran « Partie terminée ». Où placez-vous la variable qui indique si le jeu est en cours ou fini ?

Je vérifie / je rends :

  • ☐ Le jeu fonctionnel.
  • ☐ Vos réponses aux questions 1, 5 et 6.

← 15.3 Animer un dessin Canvas (requestAnimationFrame)15.5 TP 15 : Dessin et animation avec Canvas →