Motion graphiques de défilement
Une timeline canvas pilotée par la position de défilement : une scène épinglée, des bobines qui sont des fonctions pures de la progression, et des légendes que le dessin n’a jamais le droit de toucher. Cette méthode anime le tambour d’histoire de la page d’accueil d’ImuTechz, le tambour R&D et le film du parcours du fondateur. Elle alimente également la démo à cinq bobines dans assets/demo.html.
Toute la technique tient en une phrase. La position de défilement est la seule horloge : associez-la à p ∈ [0, 1] et faites en sorte que chaque pixel, légende et rotation soit une fonction de p.
À quoi ressemble un bon résultat
- Ça défile comme une vidéo. Faites défiler vers l’avant et le film se joue. Revenez en arrière et il rembobine, image par image. S’arrêter à mi-chemin laisse une image qui a du sens.
- La page n’est jamais détournée. La molette, le trackpad, le tactile et les touches déplacent la page exactement comme partout ailleurs. La section est simplement haute.
- Les mots sont toujours lisibles. Les légendes sont du vrai texte DOM sur une carte. Le dessin se dispose dans l’espace laissé par les légendes, et le canvas est découpé autour d’elles comme filet de sécurité.
- C’est un contrôle, pas seulement un spectacle. Les faces d’un tambour d’histoire sont des liens. Son rail, ses boutons fléchés, les touches fléchées et le glissement cherchent en faisant défiler la page, de sorte que le défilement et le tambour ne peuvent jamais diverger.
- Une page au repos ne coûte rien. Aucun requestAnimationFrame ne s’exécute une fois que p s’est stabilisé ou après que la section a quitté la fenêtre.
- Le mouvement réduit est respecté. Pas de lissage, pas de rotation intermédiaire du tambour et aucune animation d’entrée des légendes. Tout le contenu est toujours présent.
- Ça tient de 360 à 1920 px de large, en clair et en sombre, à DPR 1 et 2.
Règles strictes
- p vient de la position de défilement, jamais d’une horloge. N’utilisez pas
performance.now(),Date, d’animations CSS ousetIntervalà l’intérieur d’une bobine. Le lissage (un lerp vers la cible p) est autorisé car il converge vers la même image. - Une bobine est une fonction pure
draw(ctx, t, env). Elle ne conserve aucun état entre les images et n’appelle jamaisMath.random(); utilisez unhash(i)seedé à la place. Le même (t, env) doit peindre les mêmes pixels. - Épinglez avec CSS, pas JS. Utilisez une piste haute (
position: relative; height: N×vh) contenant une scèneposition: sticky; top: 0; height: 100svh. Ne mettez jamaisposition: fixedsur la scène, ne verrouillez jamais le body, et ne mettez jamaisoverflow: hidden|auto|clipsur un ancêtre de la piste. - Ne capturez jamais l’entrée. N’utilisez pas
preventDefaultsur la molette ou le tactile, pas deoverflow: hiddensur html ou body, et pas d’accrochage au défilement sur la piste. Seul un tambour focalisé peut gérer les touches fléchées, et il le fait en faisant défiler la page. - Cherchez en défilant. Chaque bouton, élément du rail, touche et ancre modifie
window.scrollY. Ne définissez jamais p ou une rotation directement. Utilisezbehavior: "instant"pour les sauts programmatiques qui ne doivent pas s’animer, car"auto"obéit au CSSscroll-behavior: smoothde la page. - Le texte vit dans le DOM. Les légendes, titres et liens sont du HTML. Le texte du canvas est limité aux étiquettes décoratives comme les années, les repères d’axe et les noms d’étapes.
- Mesurez les légendes à chaque image et gardez le dessin à l’écart. Chaque légende et élément HUD est une zone d’exclusion (
data-keep). Les bobines se disposent à l’intérieur deenv.free, et le moteur découpe les zones d’exclusion avec un clip pair-impair. - N’affichez jamais deux légendes à la fois. Les fenêtres de légende se trouvent à l’intérieur de leur bobine avec un espace à chaque limite, et les bobines font un fondu croisé dans cet espace.
- La boucle s’arrête au repos. Démarrez rAF au défilement ou au redimensionnement uniquement lorsque la section est proche de la fenêtre (IntersectionObserver), et arrêtez-la lorsque p est égal à la cible.
- Le canvas est dimensionné à la scène à
min(devicePixelRatio, 2). AppelezsetTransform(dpr, …)une fois par image et effacez-le à chaque image. - Les couleurs proviennent des propriétés personnalisées CSS. Ne codez pas en dur une palette mode clair dans une bobine. Relisez la palette lors du changement de
prefers-color-scheme. - Respectez
prefers-reduced-motion: reduce. N’utilisez pas de lissage, faites en sorte que le tambour s’arrête face à face et désactivez les animations d’entrée des légendes. Les images suivent toujours le défilement, car c’est l’utilisateur qui les contrôle.
Ordre de construction
Faites-le dans cet ordre. Chaque étape est testable avant la suivante.
- D’abord le contenu. Écrivez les chapitres : un accroche, un titre, une ou deux phrases et un lien optionnel pour chacun. Choisissez le nombre de bobines (3 à 7) et donnez à chacune environ 80 à 100 svh de défilement.
- Piste et scène. Ajoutez la section haute avec une scène sticky à 100svh à l’intérieur, et vérifiez qu’elle s’épingle sans aucun JS (voir layout-and-captions).
- Le moteur de défilement. Câblez
ScrollMotion.scruboufilm(voir engine) et loggez p. Vérifiez qu’il lit 0 au début de l’épingle et 1 à la fin, et qu’il se déplace dans les deux sens. - Légendes et HUD dans le DOM. Placez-les par point de rupture, marquez-les
data-keep, et laissez le moteur piloter leur opacité à partir de p. - Bobines, une par une. Écrivez
draw(ctx, t, env)à l’intérieur deenv.free(voir drawing), et défilez-la dans les deux sens avant d’écrire la suivante. - Grammaire de mouvement. Ajoutez les enveloppes d’entrée, de maintien et de sortie, les décalages, les temps d’arrêt et les fondus croisés (voir motion-grammar).
- Interactivité, s’il s’agit d’un tambour. Ajoutez les liens des faces, le rail, les boutons précédent et suivant, les touches et le glissement, tous cherchant par défilement.
- Passage performance et accessibilité. Couvrez la boucle au repos, le DPR, le mouvement réduit, l’ordre de focus, le texte du lecteur d’écran et le mode sombre (voir performance-and-a11y).
- Vérifiez à 360, 390, 414, 768, 1024, 1280, 1440 et 1920 avec le script sans tête (voir verification). Ne considérez pas la tâche comme terminée tant que tous les tests ne passent pas.
Listes de décision
Film canvas ou transformations DOM/CSS ?
- Beaucoup de petites pièces mobiles, lignes à dessiner, particules ou graphiques → film canvas (
ScrollMotion.film). - Une poignée de cartes, d’images ou de panneaux de texte → transformations DOM pilotées par
scrub(le tambour en est un). - Maillages 3D réels ou shaders → cette compétence ne suffit pas. Utilisez WebGL, mais respectez les règles 1 à 5 et 9 à 12.
Mode épingle ou vue ?
- La section doit retenir l’écran pendant que son histoire se joue →
mode: "pin"(piste haute et scène sticky). - L’effet suit le contenu pendant qu’il défile, comme pour les marquees ou le parallaxe →
mode: "view", sans épingle.
Quelle hauteur pour la piste ?
- Utilisez
n × 80–100svh + 100svhpour un film etn × 70vh + 100vhpour un tambour. Si un testeur dit que ça va « trop vite », augmentez la hauteur. Ne ralentissez jamais en capturant la molette.
Où placer les légendes ?
- Desktop (≥ 760 px) : à côté du dessin (carte gauche ou droite) ou comme une large carte en bas. Téléphones : toujours une feuille en bas, avec le dessin au-dessus.
- Avec un en-tête de site fixe, définissez
--sm-nav-hpour que le HUD et le tambour le dégagent.
Faut-il le tambour ?
- Utilisez le tambour lorsque les chapitres sont des choses que l’utilisateur peut vouloir atteindre (pages, produits, sections). Sinon, utilisez le film seul.
Modes de défaillance et correctifs
| Symptôme | Cause | Correctif |
|---|---|---|
| La scène défile au lieu de s’épingler | Un ancêtre a overflow: hidden/auto/clip, ou la scène n’a pas de top | Supprimez le débordement (utilisez overflow-x: clip sur le body si nécessaire) et définissez top: 0 |
| L’animation ne se joue que dans un sens, ou saute en remontant | État accumulé entre les images, ou une horloge ou Math.random dans une bobine | Faites de la bobine une fonction pure de t ; utilisez hash(i) pour l’aléatoire |
| Le dessin passe sous la légende | Bobine disposée sur toute la scène | Disposez dans env.free ; gardez data-keep sur la légende ; laissez le moteur découper |
| Boîte de légende mesurée au mauvais endroit en milieu de fondu | Utilisation de getBoundingClientRect (inclut la transformation) | Mesurez les boîtes de mise en page (offsetLeft/Top/Width/Height, voir boxIn) |
| Deux légendes se chevauchent à une limite | Les fenêtres de légende se touchent | Fondu des légendes à 1 − cross et apparition à cross ; les bobines font un fondu croisé dans l’espace |
| Les ventilateurs tournent sur une page au repos | La boucle rAF ne s’arrête jamais | Arrêtez quand cur === target ; verrouillez avec IntersectionObserver |
| Canvas flou sur les téléphones / lent sur 4K | Canvas à la taille CSS, ou DPR non plafonné | Utilisez width = W × min(dpr, 2) et setTransform(dpr, 0, 0, dpr, 0, 0) |
| Le tambour et la page divergent après avoir cliqué sur le rail | Le contrôle a défini la rotation directement | Chaque contrôle appelle scrollToProgress ; la rotation ne vient jamais que de p |
| Le glissement saccade, puis revient en arrière | La page a scroll-behavior: smooth, donc scrollTo(x, y) s’anime | Utilisez scrollTo({ top, behavior: "instant" }) pendant le glissement |
| Cliquer sur une face après un glissement ouvre son lien | Le clic qui termine un glissement se déclenche | Avalez un clic en phase de capture après un glissement qui a bougé de plus de 4 px |
| Saut de 100vh sur mobile lorsque la barre d’URL se cache | vh sur la scène | Utilisez 100svh pour la scène ; calculez p avec la propre hauteur de la scène |
| Le mode sombre montre un dessin en mode clair | Couleurs codées en dur | Lisez les jetons --sm-* via readPalette ; relisez lors du changement de schéma |
| Les faces arrière du tambour apparaissent | Absence de backface-visibility: hidden | Définissez-le, avec le préfixe -webkit-, sur chaque face |
| Vignettes des faces du tambour dessinées à la mauvaise taille | Utilisation du rectangle projeté d’une face transformée en 3D | Dimensionnez par clientWidth/clientHeight |
Fichiers
- references/engine.md : p, la boucle de défilement,
film(), la recherche, le hook React. - references/drawing.md : le contrat de bobine, la configuration du canvas, les routes, les pop-ins, les particules et les palettes.
- references/layout-and-captions.md : CSS de la piste et de la scène, zones d’exclusion, rectangle libre, le clip, la disposition du tambour.
- references/motion-grammar.md : enveloppes, décalages, temps d’arrêt, fondus croisés, rythme et assouplissements.
- references/performance-and-a11y.md : la boucle au repos, le coût de mise en page, le mouvement réduit, le clavier, le focus et les lecteurs d’écran.
- references/verification.md : les huit largeurs, le script Brave/Chrome sans tête et la validation manuelle.
- assets/demo.html : autonome, zéro dépendance, double-clic pour lancer. Cinq bobines et le tambour d’histoire.
- assets/scroll-motion.js : le moteur (
scrub,film,drum,scrollToProgresset les helpers), UMD. - assets/reels.js : cinq bobines d’exemple (orbite, chemin, pile, vague, fermeture) et
still()pour les vignettes. - assets/scroll-motion.css : styles du film et du tambour, jetons clair et sombre.
- assets/ScrollFilm.jsx : le wrapper React pour le film, plus le hook
useScrub. - assets/StoryDrum.jsx : le tambour d’histoire React, plus un
DrumPreviewà défilement automatique. assets/preview-orbit.jpg,assets/preview-path.jpg,assets/preview-close.jpg: captures de la démo à 1440 × 900.
Commencez par la démo : ouvrez-la, regardez la source et remplacez le contenu. Le câblage en bas du fichier fait environ 60 lignes.