A Claude Code plugin for hand-drawn-style 2D animation made as code. Scenes are deterministic
Canvas 2D modules (renderFrame(t)), headless Chromium draws the frames, and ffmpeg encodes them.
| Skill | What it does |
|---|---|
/animation:rotoscope <clip> <work dir> |
Copies a reference clip drawing by drawing: traces each distinct drawing to vector paths, renders them through the ink.js brush engine, measures every drawing against its source (XOR against a codec-noise floor, SSIM, edge-band SSIM, paper color), fits per-shot brush overrides, and reviews 1:1 crops. Each run appends to a learnings file, and a retro step promotes recurring findings into the defaults. |
/animation:setup |
Checks the prerequisites (ffmpeg with libx264, ffprobe, Node, playwright-core with Chromium, the pinned numpy and opencv, and the playwright_core option when set) and prints a PASS/FAIL/INFO table with one remedy line per failure. Check-only: every prerequisite is external. |
/animation:learn-style <work dir> <pack dir> |
Measures a rotoscope work directory into a style pack: palette and tone ramp, the seven style knobs, and statistic bands (edge softness, stroke and gap widths, edge roughness, gray inside the ink, boil of the frame and caption, holds on 1s/2s/3s). Then proves the pack by authoring a new scene with ink.js and checking its render against the bands. |
/animation:produce <production dir> |
From a brief and one or more style packs, writes pre-production boards and stops for approval. After that, a shot list, scenes, rendered frames, a delivered file, and a review against the pack. shots.json is the cut list inkstats.py --cuts reads. |
styles/<name>/ holds one style: style.json (measured statistics, bands, knobs, brush defaults)
and STYLE.md (the style in words, with its credit and validation). Packs hold statistics only,
never a source's frames or traces.
| Pack | Style |
|---|---|
woodcut-ink |
Two-tone brushed ink on cream with carved gouges, a soft edge and boil on 3s. A study of @shfred0's clip; no untraced scene has passed its current check yet. |
scripts/ holds what every skill renders with: ink.js (the deterministic brush engine: capsule
dabs, ink fills, strokes, splatter, dashed lines, paper grain), render.html (loads a scene module
named by ?scene=), render.py (the one render entry point: serves a scene, captures each drawing
or every frame at a given fps through capture.mjs, writes render.json, and encodes), decode.py
(reads a video, a frame folder or a work dir back into frames), prereq.py (the prerequisite
probe), and inkstats.py (style statistics of any film, a video, a frame folder or a rotoscope work
dir, with --pack to check it against a style pack; --cuts takes a comma list or a produce shots.json), produce.py (the production directory: boards, the approval digest, shots.json, and the review command), and woodcut_marks.py (a test helper for the woodcut-ink authoring residuals: synthetic caption and frame drawings inside the frozen bands, not a render or measuring entry point).
/animation:setup checks each of these and prints one remedy line per missing one. Verified on
Linux only; Windows and macOS are untested by hand (the scripts use no shell and open text as
UTF-8, but no run there has been recorded).
- Python 3.12 or later (numpy 2.5 requires it), with numpy and opencv at the versions pinned in
requirements.txt: run every script throughuv run --with-requirements ${CLAUDE_PLUGIN_ROOT}/requirements.txt python .... The pins keep the statistics and the shipped regression reproducible byte for byte. ffmpegat or abovescripts/prereq.py'sFFMPEG_MIN(the first release with-fps_mode), with thelibx264encoder, andffprobe, on PATH.- Node and playwright-core with Chromium.
capture.mjslooks in theplaywright_coreplugin option (passed asrender.py --playwright-core), then the working directory, then a playwright-cli install on PATH, and prints the remedy when none is found.
The shfred0 study is the calibration target: every drawing within the measure.py target (rotoscope
reference/method.md, Target). Its per-shot override file ships as a fixture (parameters only; the
clip and traces are not shipped). With the clip and an empty directory:
uv run --with-requirements plugins/animation/requirements.txt python \
plugins/animation/skills/rotoscope/scripts/regress.py <shfred0.mp4> <empty work dir>It exits 0 only when every drawing passes and the encoded replica passes the woodcut-ink pack.
regress.py --synthetic <empty dir> needs no unshipped input: it renders, encodes and re-traces the
committed fixtures/synthetic.js.
Generated from this plugin's .claude-plugin/plugin.json. Every option Claude Code
will prompt for when the plugin is enabled, with the environment variable each hook
reads it from.
| Option | Type | Default | Environment variable | Description |
|---|---|---|---|---|
playwright_core |
directory | (none) | CLAUDE_PLUGIN_OPTION_PLAYWRIGHT_CORE |
The playwright-core package directory, or a folder holding node_modules/playwright-core, to render with. Leave unset to use the working directory's install or a playwright-cli install on PATH. |
Three supported routes, in the order most people want them:
-
Interactively. Claude Code prompts for declared options when you enable the plugin. To change them later:
/plugin configure animation@<marketplace>. -
Headless. Repeat
--configfor each option. Replace<marketplace>with the marketplace you installed this plugin from:claude plugin install animation@<marketplace> -s <scope> --config playwright_core=<value>
The same command reconfigures a plugin that is already installed: it prints
already installedand still writes the value. The short-circuit message is about the install, not the config write. Do notclaude plugin uninstallto reconfigure: uninstalling drops this plugin's whole storedpluginConfigsentry, resetting every option in the table above to its default.-sdefaults touser, so pass the scopeclaude plugin listreports for this plugin. The verified-version record lives in the plugin-reconfiguration convention.The value is stored immediately; the session you are in does not change. Hooks are handed their
CLAUDE_PLUGIN_OPTION_*when the session starts, so start a fresh Claude Code session before expecting new behavior. A check run in the old session still reports the old value, and that is not a failed write. -
By hand, in settings. Add the value under
pluginConfigsin your user settings (~/.claude/settings.json):{ "pluginConfigs": { "animation@<marketplace>": { "options": { "playwright_core": <value> } } } }Plugin option values are read from user,
--settings, and managed settings only, not from a project's.claude/settings.json. To vary behavior per repository, enable or disable the plugin in that project'senabledPluginsinstead of setting an option there.
Do not set the CLAUDE_PLUGIN_OPTION_* variables yourself. They are how Claude Code
hands a configured value to a hook process; the value comes from the routes above.
- User configuration: the
userConfigschema and theCLAUDE_PLUGIN_OPTION_<KEY>export - Plugin install options: the
--configflag's reference entry - Plugins and skills settings:
enabledPlugins,extraKnownMarketplaces,pluginConfigs - Settings files and who they affect: user vs project vs local precedence
- Manage installed plugins: enabling, disabling,
/plugin list