Capturer une animation en GIF comparatif
Les défauts et améliorations d'une animation ne se voient pas sur une simple capture d'écran, et il est difficile de les juger à l'oral. Il faut la filmer : une même animation, avant et après modification (ou activée/désactivée), capturée image par image sur la même échelle de temps, juxtaposée en GIF, pour que l'utilisateur puisse trancher d'un coup d'œil.
Le principe central est le suivant : c'est nous qui avançons le temps, pas l'horloge réelle. Le navigateur headless peut être lent à dessiner (surtout en rendu logiciel), n'affichant que quelques images par seconde. Avec une horloge réelle, chaque capture s'arrête à un moment différent, et les deux variantes ne correspondent jamais. On fait donc en sorte que le temps dans la page n'avance que lorsqu'on lui dit « avance de 40 ms », puis on capture une image, on avance à nouveau, on recapture. Ainsi, quelle que soit la lenteur de la machine, la n-ième image correspond toujours à n × 40 ms, les deux variantes correspondent image par image, et deux captures d'une même variante sont identiques (testé : 75 images identiques octet par octet).
1. Réfléchir d'abord : le GIF pour l'humain, les chiffres pour la vérification
Le GIF est adapté à la validation, à la comparaison et à l'insertion dans de la documentation, mais pas pour trouver les défauts. Exemple réel : une main qui se lève lentement, le genou reste plié à 18° pendant une seconde – cela a été détecté en lisant les angles image par image, pas dans le GIF. Donc :
- D'abord, enregistrer les valeurs clés image par image (angles, positions, hauteurs, transparence, glissement du pied), une fois pour chaque variante (activée/désactivée), et vérifier si les chiffres sont corrects ;
- Ensuite, capturer le GIF pour validation humaine.
La méthode pour enregistrer les valeurs est décrite dans references/hook.md (écrire une fonction de capture dans la page, qui renvoie les valeurs de chaque image en même temps que la capture visuelle).
2. Choisir l'horloge
| Horloge | Quand l'utiliser | Faut-il modifier la page ? |
|---|---|---|
shim (par défaut) | Presque toujours : transitions CSS, animations CSS, element.animate(), animations JS avec rAF, canvas, three.js, PixiJS, Live2D | Non. clock-shim.js est injecté avant les scripts de la page, remplace performance.now, Date.now, requestAnimationFrame, setTimeout/setInterval, et Math.random avec une graine fixe ; les animations CSS/WAAPI sont figées et alignées sur le même instant |
hook | Pour capturer des valeurs depuis un modèle (frame par frame), ou si l'application a déjà une boucle pas à pas et qu'on veut éviter la capture d'écran du navigateur | Écrire window.__capture(...) dans la page, voir references/hook.md |
Ne pas utiliser l'horloge virtuelle Chrome intégrée (Emulation.setVirtualTimePolicy) : testé, les timestamps rAF et performance.now ne correspondent pas (compteurs négatifs), et les animations ne suivent pas le pas donné.
Ce que l'horloge fictive ne contrôle pas : les animations dans les Web Workers, OffscreenCanvas dessiné dans un worker, <video>, new Date() (seulement Date.now est remplacé), crypto.getRandomValues. Si vous rencontrez ces cas, passez à hook ou remplacez-les dans la page de test.
3. Étapes
- Préparer la page et les variantes. Utilisez de préférence des paramètres d'URL pour les variantes (
?v=off,?v=on), la page les lit et bascule ; ou utilisez--setuppour exécuter du JS de basculement avant la capture. Placez les éléments locaux (modèles, pages de test) en dehors du dépôt git, supprimez-les après utilisation. - Lancer la page. Utilisez
file:///…pour les fichiers locaux ; pour les projets nécessitant un build (Vite, etc.), lancez un serveur de développement sur un port libre (ne touchez pas aux fenêtres des autres sessions sur la même machine). Vite optimise les dépendances au premier chargement d'une nouvelle page, puis la recharge automatiquement – si la capture se fait en plein milieu, une erreur « window.xxx is undefined » peut survenir : ouvrez d'abord la page dans un navigateur pour la préchauffer, ou relancez la capture après l'erreur. - Capturer.
node <skill>/scripts/record.mjs --out frames --fps 25 --seconds 3 --clip "#stage" \ --ready "window.model != null" --setup "window.model.motion('TapBody', 0)" \ --variant "off=http://127.0.0.1:5173/lab.html?v=off" --variant "on=http://127.0.0.1:5173/lab.html"--clip: ne capture que la zone définie (sélecteur CSS) ; sans cela, capture toute la fenêtre (taille définie par--width,--height).--ready: expression indiquant que le chargement (modèle, textures asynchrones) est terminé. Pendant l'attente, l'horloge fictive ne tourne pas : si elle tournait, le moment de début et les nombres aléatoires utilisés différeraient entre deux captures (testé). Si la page utilisesetTimeoutpour son chargement, ajoutez--ready-advance 1.--wait: temps fictif (en ms) à faire avancer après le chargement pour stabiliser les animations d'entrée, les ressorts, etc. ;--setups'exécute après, puis la capture commence immédiatement.--setuppeut contenir desawait, la valeur de retour est ignorée (les objets three.js sont trop grands pour être renvoyés via CDP).- Le script lance lui-même Chrome headless (répertoire de configuration dédié), et le ferme après la capture. Ne pas laisser de processus résiduels.
- Par défaut, utilise le rendu GPU (
--render gpu) ; un message « Rendu WebGL : … » s'affiche au début. Si le message mentionne SwiftShader, le GPU n'est pas utilisé, le CPU sera saturé (plusieurs cœurs) – surtout visible avec three.js/VRM. Sur les machines sans GPU, ou pour comparer avec des captures antérieures en rendu logiciel, ajoutez--render cpu. - Les rendus GPU et logiciel produisent des bords différents : les comparaisons image par image ne se font qu'au sein d'un même type de rendu.
- D'abord la feuille de contact, ensuite le GIF.
python <skill>/scripts/make_gif.py frames --variants off,on --fps 25 --width 300 --title "Hiyori · caresse" --sheet sheet.png --out compare.gif--sheetproduit une PNG avec plusieurs images (par défaut 4 images réparties uniformément,--pick 0,10,20pour spécifier) : vous vérifiez d'abord cette feuille – l'animation se déroule correctement, les variantes sont alignées, le cadrage est bon – avant de donner le GIF à l'utilisateur. Le GIF utilise une palette commune pour toutes les images, évitant les changements de couleur intempestifs. Les légendes sont écrites dans une bande blanche au-dessus de l'image, sans chevaucher le contenu. - Remettre à l'utilisateur : envoyez le GIF via un outil de partage de fichiers (quelques images représentatives, le reste dit où il se trouve dans un dossier) ; précisez ce que montrent les parties gauche et droite, quelle version est enregistrée, et ce qui n'a pas été vérifié (par ex. « rendu logiciel, bords légèrement plus épais qu'avec le GPU »).
4. Comment choisir les paramètres
- 25 images par seconde, durée 2–4 secondes ; 300 pixels de large par vignette. Une image côte à côte fait environ 0,6–3 Mo (plus pour les personnages et les dégradés).
- La durée de l'action dépend de l'action elle-même : avec
hook, vous pouvez utiliser la durée du clip plus une marge (l'élan résiduel après l'arrêt doit aussi être capturé). - Cadrage basé sur la boîte englobante : pour les personnages 3D, utilisez
Box3.setFromObject(scene)pour inclure la tête ; se baser uniquement sur le crâne peut couper les grands chapeaux ou les antennes (testé). Laissez aussi un peu d'espace sous les pieds. - Chaque variante ne doit différer que par l'élément à comparer : même modèle, même caméra, même action, même attente. Les parties aléatoires sont déjà contrôlées par la graine fixe, mais si la page appelle
Math.randomun nombre différent de fois selon la variante, les séquences suivantes seront décalées – gardez-le en tête.
5. Procédure pour différents types de pages
- Page web (CSS, WAAPI, rAF) : pas de modification nécessaire. Pour capturer des interactions (ouverture de menu, survol), déclenchez-les dans
--setup(ajout de classe,dispatchEvent). Testé : compteurs rAF, transitions CSS déclenchées après 50 ms, animations CSS avec keyframes,element.animate()fonctionnent correctement avec l'horloge fictive ; deux variantes avec des courbes d'assouplissement différentes sont alignées image par image. - PixiJS / Live2D : pas de modification nécessaire. Le ticker Pixi utilise rAF et
performance.now, donc suit l'horloge fictive ; la physique aussi. Le runtime Cubism Live2D (live2dcubismcore.min.js) doit être chargé via une balise<script>normale avant les modules, car le moteur le vérifie au moment de l'import – s'il n'est pas trouvé, il lance l'erreur « Could not find Cubism runtime ». Testé : 75 images de Hiyori (physique activée/désactivée) identiques octet par octet. - three.js / VRM : pas de modification nécessaire. La boucle rAF suit l'horloge fictive (testé : le chargement d'un VRM desktop pet fonctionne directement, les différences avant/après sont visibles). Pour lire les angles des os image par image, utilisez
hook(voirreferences/hook.md). - Jeux, autres applications canvas : idem. Si l'application utilise
setTimeoutpour sa boucle de jeu, cela tombe aussi sous le contrôle de l'horloge fictive.
6. Licences et confidentialité
- Les GIF provenant de modèles ou d'assets créés par d'autres personnes sont soumis à leur licence. S'ils interdisent la redistribution (ex. certaines clauses VN3), vous pouvez les montrer mais ne les placez pas dans un dépôt public ou une publication. Si la licence autorise l'utilisation avec attribution, mentionnez l'auteur lors de la publication.
- Placez les GIF et les images dans un dossier local (hors du dépôt git, ou dans un dossier temporaire dédié) et supprimez les pages de test après utilisation.
7. Relations avec les autres skills
- Capture d'écran statique, multi-viewports, mode sombre :
frontend-verification(sonshot.mjsutilise l'horloge réelle ; pour les animations, utilisez ce skill). - Vidéo promotionnelle pour le public, avec musique :
motion-showreel. - Rendre l'animation des personnages naturelle (couches de mouvement, physique, clips d'action) :
live2d-vrm-animation.
8. Origine et maintenance
- Origine : Cirra (desktop pet, RedCarpG/cirra-deskpet) – le 2026-11-10, en réalisant des personnages 3D avec couches de mouvement, hanches/jambes, cheveux balayés, pour montrer les différences avant/après au client ; la méthode est décrite dans
docs/vrm-motion.md§11–§12 etdocs/vrm-spring-force.md§8 (« Comment vérifier »). Les leçons sont dans le dépôt privélessons/animation-capture.md(la partie 3D est aussi danslessons/threejs-vrm-animation.md). - Source :
RedCarpG/redcarpg-skillsdansskills/animation-capture/. Le dossier~/.claude/skills/est une copie locale installée – ne pas modifier directement. - Comment modifier :
- Modifiez dans
redcarpg-skills, via PR ; incrémentez la version dansmanifest.json, validez avecnode scripts/validate.mjs, puis créez un tag (animation-capture-v<version>) et réinstallez la copie locale. - Après avoir modifié les scripts, effectuez une capture sur une page CSS, une page Live2D ou three.js pour vérifier que les deux variantes correspondent image par image, et qu'une même variante capturée deux fois est identique (comparaison avec
ImageChops.differencede Pillow). - Mettez à jour les fichiers
lessonssi la procédure ou les écueils changent ; les cas de test sont dansevals/. - Ajoutez une ligne dans l'historique des modifications ci-dessous.
- Modifiez dans
- Historique des modifications :
- 2026-11-12 : v1.0.0, publié dans
redcarpg-skills. Chemins locaux modifiés, contenu inchangé. - 2026-11-10, Cirra : Première version.
record.mjs(horloges shim et hook),clock-shim.js,make_gif.py. Testé sur pages web (transitions CSS, animations CSS, WAAPI, rAF), Live2D (Hiyori, physique activée/désactivée), three.js VRM (couche de mouvement activée/désactivée). - 2026-11-10, Cirra : Test a révélé que les animations CSS/WAAPI lancées au chargement de la page avancent de quelques ms en temps réel avant que l'horloge fictive ne prenne le relais, créant un décalage de 16,6 ms entre deux variantes ; les transitions déclenchées par timer attendent la fin de cette étape (50 ms devient 80 ms). Correction : les animations vues pour la première fois partent de 0, et à chaque exécution de timer, rAF ou début d'étape, on revérifie. Résultat : une transition de 600 ms déclenchée à 50 ms atteint exactement 8,0 px à 80 ms, et la 0e image des deux variantes est identique. Deux cas de test (comparaison d'assouplissement web, déformation de balle rebondissante) passent tous avec et sans le skill ; le skill économise le temps de réécriture des scripts de capture et évite les pièges déjà rencontrés.
- 2026-11-10, Cirra : Capture GIF comparatif de balancement de cheveux (force centrifuge, traînée) pour Yumeka, Sendagaya Shino ; personnage 3D avec plateau tournant (placé dans un Group rotatif) et
setIntervalsous horloge fictive, sans code de capture dans la page. - 2026-11-10 : Conformément aux nouvelles règles de rédaction, la section « Maintenance » devient « Origine et maintenance ».
- 2026-11-10, Cirra : L'utilisateur a constaté que Chrome consommait beaucoup de CPU lors de la capture 3D.
record.mjspasse par défaut en rendu GPU (--render gpu;--render cpureste SwiftShader), affiche le moteur utilisé ; la suppression du répertoire de configuration à la sortie de Chrome peut échouer (EPERM), désormais ignoré silencieusement si impossible. Testé : sur RTX 3060 Ti, la charge de shader par image passe de 154 ms à 8 ms ; deux captures en rendu GPU : 10 images identiques octet par octet ; rendu GPU et logiciel diffèrent image par image (bords) – ne pas mélanger.
- 2026-11-12 : v1.0.0, publié dans