Communitygithub.com

Animation Capture

Records moving content frame-by-frame at fixed intervals and produces side-by-side comparison GIFs or frame grids for review, before/after comparison, PRs, and documentation.

What is Animation Capture?

This skill captures deterministic frames from web pages, canvas 2D, three.js/VRM, PixiJS, and Live2D scenes by controlling time with a fake clock instead of real time. It uses a headless Chrome browser with a clock shim (`clock-shim.js`) that overrides `performance.now`, `Date.now`, `requestAnimationFrame`, and `setTimeout`/`setInterval`, and fixes `Math.random` with a seed — producing the exact same frame sequence across runs regardless of rendering speed. The `record.mjs` script takes variants (e.g., before/after, on/off) defined via URL parameters or setup JS, captures frames only after the page is ready, and outputs organized frame sets. The `make_gif.py` script creates side-by-side GIFs with a shared color palette, labels, and optional preview sheets. The workflow emphasizes first recording numerical values (angles, positions) per frame to verify correctness, then capturing the GIF for visual approval. Chrome's built-in virtual time is avoided due to unreliable timestamps. The skill supports both GPU (default) and software rendering modes, and includes lessons on handling Vite page refreshes, Live2D runtime loading, and waiting for async assets without advancing the fake clock. Output GIFs respect model licenses and are kept out of version control.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/RedCarpG/redcarpg-skills/tree/HEAD/skills/animation-capture

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Record Animation as Comparison GIF

Problems and improvements in animation cannot be seen from a single screenshot, and words alone won't convince a user. Record it: the same animation, before and after (or on/off), frame by frame along the same timeline, placed side by side into a single GIF. The user can decide at a glance.

The core is only this: Time is advanced by us, not by the real clock. A headless browser renders slowly (especially with software rendering), drawing maybe a dozen frames per second. Using real time means each frame lands at a different point, and the left and right sides never match. So we make time in the page advance only when we say "go forward 40 ms," then take a screenshot, advance again, and screenshot again. No matter how slow the machine, frame n is always at n × 40 ms. Two variants match frame for frame, and recording the same variant twice produces identical results (tested: all 75 frames match byte-for-byte).

1. Think First: GIFs for People, Numbers for Verification

GIFs are for approval, comparison, and documentation—not for finding bugs. Real example: when slowly raising a hand, the knee stayed bent at 18° for a whole second. This was caught by logging angles per frame, but invisible in the GIF. So:

  1. First, log key numerical values per frame (angle, position, height, opacity, foot sliding) for both before and after variants, and check if the values are correct.
  2. Once the numbers are right, record the GIF for human approval.

See references/hook.md for how to log values (write a capture function on the page that reports frame data together with the screenshot).

2. Choosing a Clock

ClockWhen to UsePage Needs Modification?
shim (default)Almost always: CSS transitions, CSS animations, element.animate(), rAF-based JS animations, canvas, three.js, PixiJS, Live2DNo. clock-shim.js is injected before page scripts, overriding performance.now, Date.now, requestAnimationFrame, setTimeout/setInterval; Math.random is seeded. CSS/WAAPI animations are frozen and advanced to the same time each frame.
hookWhen you need to read values from the model (numerical per-frame data) while recording, or if the app already has a loop that advances by fixed steps and you want to skip browser screenshotsYes: the page must implement window.__capture(...). See references/hook.md.

Do not use Chrome's built-in virtual time (Emulation.setVirtualTimePolicy): we tested it; rAF timestamps and performance.now were inconsistent (counter went negative), and animations did not advance by the given step.

What the fake clock does not cover: animations in Web Workers, OffscreenCanvas drawn in workers, <video>, new Date() (only Date.now is overridden), crypto.getRandomValues. If you encounter these, switch to hook or replace them in your test page.

3. Steps

  1. Prepare the page and variants. Ideally, toggle variants via URL parameters (?v=off, ?v=on); the page reads the parameter to decide the state. Alternatively, use --setup to run JS before recording to switch state. Keep local-only assets (third-party models, test pages) outside git and delete them when done.
  2. Start the page. Local files use file:///… directly. For projects that need bundling (Vite, etc.), start a dev server on a free port (other sessions on the same machine might be using test ports—don't touch their windows). Vite refreshes the page once on first open (optimizing dependencies): if the page gets reloaded mid-recording, you'll see "window.xxx is undefined." Warm it up with a browser first, or rerun after the error.
  3. Record.
    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: crop to that region (CSS selector). If omitted, captures the full window (use --width, --height to set size).
    • --ready: models and textures load asynchronously; provide an expression that evaluates to true when ready. While waiting, the fake clock does not advance: if it did, each load would finish at a different real time, meaning the start frame and random state would differ between runs, and two recordings would not match (tested). For pages that rely on setTimeout to become ready, add --ready-advance 1.
    • --wait: after ready, advance the fake clock by this many milliseconds (e.g., to let entrance animations or springs settle). --setup runs after that, and recording begins immediately after it finishes. --setup can await; the return value is discarded (three.js objects are too large for CDP transfer).
    • The script starts its own headless Chrome (separate config directory), records, and shuts it down automatically—don't leave browsers open.
    • Default rendering is GPU (--render gpu). The script prints "WebGL renderer: …" at the start. If it says "SwiftShader," the GPU is not being used; CPU usage will spike to full cores when recording three.js/VRM scenes, and users will see Chrome permanently hogging CPU. On machines without a GPU, or when comparing with recordings previously made with software rendering, use --render cpu.
    • GPU and software rendering produce different pixel edges. Per-frame comparison should only be done within the same rendering mode.
  4. Review with a sheet first, then produce the GIF.
    python <skill>/scripts/make_gif.py frames --variants Off,On --fps 25 --width 300 --title "Hiyori · Pet Head" --sheet sheet.png --out compare.gif
    
    --sheet lays out a few frames (default: evenly spaced 4 frames; use --pick 0,10,20 to specify) into a single PNG. Review this image yourself first: confirm the animation works, left and right match, and the crop didn't cut anything off. Then give the GIF to the user. The GIF uses a shared color palette for all frames—no flashing colors. Labels are drawn in a white bar above the image, not overlaid on content.
  5. Deliver to the user: Send GIFs via a file-sharing tool (send a few representative ones; say where the rest are). Clearly explain what each left and right side shows, which version was recorded, and what was not verified (e.g., "this used software rendering, edges are slightly thicker than with GPU rendering").

4. How to Choose Parameters

  • 25 fps, 2–4 seconds per clip, single frame 300 px wide. Two frames side by side ≈ 0.6–3 MB (more with characters and gradients).
  • Clip length is determined by the action itself: in hook mode, use the action clip length plus a little tail (record the settle after stopping).
  • Frame the shot by bounding box. For 3D characters, use Box3.setFromObject(scene) to include the top of the head—basing it on the skull only will crop big helmets or antenna hairs (tested). Also leave a little space below the feet.
  • Variants should differ only in the one aspect being compared: same model, same camera, same motion, same wait time. The fake clock already seeds Math.random, but if different variants call Math.random a different number of times, the sequences will diverge later—be aware of this.

5. Approaches for Different Page Types

  • Web pages (CSS, WAAPI, rAF): No page modifications needed. For interactive triggers (opening menus, hover), call them in --setup (add class, dispatchEvent). Tested: rAF counters, CSS transitions with 50ms delay, CSS keyframe animations, element.animate() all work correctly under the fake clock; two easing curve variants match frame for frame.
  • PixiJS / Live2D: No page modification needed. Pixi's ticker uses rAF and performance.now, so it follows the fake clock; physics also compute on this time. Live2D's Cubism runtime (live2dcubismcore.min.js) must be loaded via a regular <script> tag before the module that imports it; the engine checks for its existence at import time and throws "Could not find Cubism runtime" if it's missing. Tested: Hiyori with physics on/off, two recordings of 75 frames each, byte-identical.
  • three.js / VRM: No page modification needed. The rAF loop follows the fake clock (tested: VRM desk-pet type pages record directly, all differences between toggled states are visible). To read bone angles per frame, switch to hook—see references/hook.md.
  • Games, other canvas apps: Same as above. If the app drives its game loop with setTimeout, that is also under the fake clock's control.

6. Licensing and Privacy

  • For GIFs captured from third-party models or assets, check their license. If redistribution is prohibited (e.g., some VN3 terms), you may only view the GIF and show effects to others—do not commit it to a public repository or publish it. If the license requires attribution, credit the author when publishing.
  • Keep GIFs and frames out of git (use a dedicated local folder in the project or a temp directory). Delete test pages when done.

7. Relationship to Other Skills

  • Static interface screenshots, multi-viewport, dark mode: frontend-verification (its shot.mjs takes real-time screenshots; use this skill for animations).
  • Promotional reels with music as MP4: motion-showreel.
  • Making character animation itself look natural (overlays, physics, motion clips): live2d-vrm-animation.

8. Origin and Maintenance

  • Origin: Cirra (desk pet, RedCarpG/cirra-deskpet) 2026-11-10—while creating a 3D character with overlay layers, hip/leg, and hair swing, the author needed before/after comparisons for the user. The method was developed in that repo's docs/vrm-motion.md §11–§12 and docs/vrm-spring-force.md §8 ("How to Verify"). Further experience is recorded in the private repo's lessons/animation-capture.md (the 3D character section is also in lessons/threejs-vrm-animation.md).
  • Source: RedCarpG/redcarpg-skills skills/animation-capture/. The copy in ~/.claude/skills/ is an installed artifact; do not edit it directly.
  • How to modify:
    • Edit in redcarpg-skills, follow PR workflow; bump the version in manifest.json, run node scripts/validate.mjs, then tag (tag animation-capture-v<version>), and reinstall the local copy.
    • After changing scripts, record on a CSS page, a Live2D page, and a three.js/VRM page, confirming two variants match frame for frame; also confirm that recording the same variant twice produces identical frames (compare with Pillow's ImageChops.difference).
    • If the method or pitfalls change, update the lessons files accordingly; test cases live in evals/.
    • Add a line to the changelog below.
  • Changelog:
    • 2026-11-12: 1.0.0—moved into redcarpg-skills as public release. Removed local absolute paths; content unchanged.
    • 2026-11-10, Cirra: Initial version. record.mjs (shim and hook clocks), clock-shim.js, make_gif.py. Tested on web pages (CSS transitions, CSS animations, WAAPI, rAF), Live2D (Hiyori, physics on/off), three.js VRM (overlay on/off).
    • 2026-11-10, Cirra: During testing, discovered that CSS/WAAPI animations starting at page load had already run for a few real milliseconds before the fake clock took over—causing a 16.6 ms discrepancy between variants. Timer-triggered transitions didn't get caught until that step finished (50 ms became 80 ms). Fixed: animations seen for the first time are started from 0; timers, rAF, and each step's beginning all re-check. After fix, a 600 ms transition triggered at 50 ms advanced to exactly 8.0 px at 80 ms, and both variants had identical frame 0. Two test cases (easing comparison on a web page, bouncing ball squash/stretch) passed with and without this skill—the model would have gotten these right anyway; the skill saves the effort of rewriting record scripts each time and covers these already-trodden pitfalls.
    • 2026-11-10, Cirra: Recorded hair swing (centrifugal force, trailing) comparison GIFs with Yumeka and Sendagaya Shino. For 3D characters, used a turntable (place character in a rotatable Group) driven by the fake clock's setInterval, no record code on the page.
    • 2026-11-10: Following new writing conventions, renamed "Maintenance" to "Origin and Maintenance."
    • 2026-11-10, Cirra: User reported high CPU usage when recording 3D scenes. Changed record.mjs to default to GPU rendering (--render gpu; --render cpu still uses SwiftShader), printing which renderer is in use at the start. Deleting the Chrome config directory before exit used to cause EPERM; now it silently skips if deletion fails. Tested: RTX 3060 Ti — per-frame shader load dropped from 154 ms to 8 ms; two GPU-rendered recordings of 10 frames were byte-identical; GPU and software rendering frames differ (edges), so they must not be mixed for comparison.

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Related Skills