À propos de ce cours
Chapitre 15 — Animations et graphismes avec Canvas · 15.4 Interagir avec le canvas : souris, clavier et collisions
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.
• 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.

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
- 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: noneempêche le téléphone de faire défiler la page pendant qu'on dessine.pointerupest écouté surwindow: 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
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
- On compare les carrés des distances : c'est plus rapide que
Math.sqrtet 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.
• 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) :
- 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) ?
- É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 ? - Déplacez d'abord le panier directement dans
keydown, puis avec unSetde touches lu dans la boucle. Décrivez la différence ressentie. - Quel test de collision utilisez-vous entre une étoile (cercle) et le panier (rectangle) ? Écrivez-le et testez-le avec
console.logsur quelques cas faciles à vérifier. - Une étoile très rapide traverse parfois le panier sans être attrapée. Pourquoi ? Proposez une solution.
- 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 →