Scrollytelling cinématographique
Architecture de base (non négociable)
- Un seul pin, une seule timeline. Le hero (ou le chapitre vedette) épingle un conteneur qui dure N viewport-heights et une progression 0→1 dirige tout. S'il y a plus d'une animation de scroll en compétition, c'est un site web normal avec des effets : ce n'est pas ça.
- Jamais de scroll-jacking. L'utilisateur contrôle la vitesse avec son scroll natif ; l'animation se "scrube" (
scrub: true). Il est interdit de détourner la molette ou de déplacer le scroll à sa place. - Une seule boucle d'animation. Lenis intégré dans le ticker de GSAP (
autoRaf: false) et ScrollTrigger synchronisé ; ou scroll natif avec scrub. Deux boucles en compétition sont la cause n°1 de jank et ne se corrigent pas en optimisant les assets. - Le texte vit dans le DOM, superposé au canvas, avec des calques par progression. Ne jamais rendre de copie à l'intérieur du canvas.
Séquence d'images (technique type Apple)
- Source préférée : vidéo réelle → images avec
ffmpeg(fps=12..24, échelle à la résolution d'écran). Exporter en WebP q≈68-75 et affiner en regardant le résultat, pas par intuition. - Maximum ~90-120 images ; sur mobile servir la moitié ou une version 720p.
- Chargement avec fenêtre glissante : garder ±6-10 images autour de l'actuelle ; ne jamais décoder la séquence complète (une séquence 1080p complète tue iOS Safari).
- Préchargement avec
IntersectionObserver(rootMargin ~800px),requestIdleCallbackpour le décodage etfetchPriority="low"sauf pour le hero. - Canvas avec
devicePixelRatioplafonné à 1.5 etcover/containcalculé ; ne dessiner que les images décodées ; si l'image exacte manque, utiliser la plus proche déjà chargée (ne jamais peindre en noir). - Cache immuable (
Cache-Control: public, max-age=31536000, immutable) avec nom de fichier versionné. - Fallback avec
prefers-reduced-motionet connexion lente : poster statique + chapitres normaux sans pin. - Alternative plus simple quand aucun post-traitement n'est nécessaire : échanger la
srcd'une<img>par image (fiable dans tous les navigateurs, sans canvas).
Vidéo scrubbing (si utilisation de <video>)
currentTimepiloté par la progression dans une seulerequestAnimationFrame, avec lissage ; coalescer les seeks (Safari accumule une file d'attente).- Charger en tant que blob pour qu'elle soit 100% seekable ; encoder avec un GOP court (keyframe tous les quelques frames) sinon le scrub hoquette.
- Jamais d'autoplay : c'est le scroll qui la déplace.
Chapitres et overlays
- Beats définis par des plages de progression (10%, 35%, 60%, 85%...), avec fade in/out et entrée alternant les côtés pour le rythme.
- Chaque chapitre conserve son texte dans le DOM dans un ordre sémantique correct pour le SEO.
- Galeries horizontales : pin de la section +
xlié à la progression ; les cartes se mettent à l'échelle vers le centre.
Performance et vérification
- Animer uniquement
transform,opacityetfilter;will-changeponctuel et retiré à la fin. - Mesurer les images avec
requestAnimationFrameen local et sur mobile réel si possible ; objectif 60fps sur un ordinateur portable moyen. - La capture headless peut lire le canvas noir (buffer non préservé) : vérifier par le DOM (styles calculés, éléments, état exposé dans
window.debug) et avec le mode réduit activé. - Tester à 375/768/1280/1440 ; le redimensionnement d'un navigateur de bureau ne reproduit pas le crash mémoire d'iOS : le mentionner dans le rapport QA.
Structure de données recommandée
Un tableau ordonné de "plans" : chaque plan est { tipo: 'frames' | 'overlay' | 'galeria', ... }. Réorganiser le film = réorganiser la liste. Les images sont référencées par motif (/seq/plano-01/0001.webp).