motion-video · Create Verifiable Animations with Code
This skill solves a very specific problem: small models (Flash / mini / turbo class) can write CSS and Canvas, but the animations they produce often "look like they were written, but don't actually move"—intermediate states are skipped, easing is fake, key elements appear at other times, and the ending stops halfway. The model itself can't tell because it has no eyes.
So the approach here is not "teach it to write better-looking code," but to turn animation into an engineering artifact that can be judged by machines:
- Specification first, code second — translate aesthetic judgment into numerical values in tables
- Every frame must be a pure function of
t— structurally force it to write intermediate states - Let scripts handle numbers — Bézier curves, springs, stagger offsets, contrast ratios — always use tools, no mental math
- Use a deterministic renderer for frame-by-frame output — same code, same
t, always the same pixel - Machine verification — numerical layer first, pixel layer second
- Only after the machine says "structure is fine" does a human look at composition
The gap is not bridged by a single magic phrase. It's bridged by these six things turning "can't think it through" into "discoverable and fixable." Where the gap comes from, what can be filled, and what cannot, see references/delta-vs-opus.md.
Quality First: Only Cut Wasted Overhead, Never Validation
This overrides the ironclad rules below. The goal of this skill is to produce Opus-grade pieces, not the cheapest ones. So the criterion for "should I do this?" is not whether it's fast, but whether it makes it better.
Only one type of overhead can be cut: repetitive, ineffective, or resource-contending overhead. Verification density, rendering quality, frame rate, motion blur—these are never cut.
Allowed to Cut (waste; cutting doesn't add or subtract quality)
| Waste | How to Cut |
|---|---|
| Launching 4 Chrome instances contending for CPU | Serial (faster than parallel in practice); render.mjs has a built-in single-instance lock |
| A 20KB spec document per chapter | One storyboard.md covering everything |
Repeatedly calling render --still for single frames | Extract 6–8 time points at once |
| Repeatedly re-analyzing the same asset | Analyze once, write results into storyboard |
| Writing each frame PNG to disk then reading it back | Already enabled optimizeForSpeed; for large projects use image2pipe (cinema-grade.md §9) |
| Verifying the same thing three times "to be safe" | Two verifications (numerical + pixel) are sufficient; a third adds no information |
Forbidden to Cut (cutting reduces quality)
| Don't Cut | Reason |
|---|---|
Full audit, including processing each jump/flat individually | Last checkpoint before delivery; skipping means no verification |
verify --idempotent | If idempotency is broken, all subsequent verifications are unreliable |
rigcheck (every time a character or pose changes) | Bone misalignment can only be caught by numbers, not eyes |
| Motion blur on delivery version | The difference between "with shutter" and "without" is a whole grade; it's not a nice-to-have |
| 60fps for fast motion | Fast limbs will stutter at 30fps; this is a defect, not a trade-off |
| Extracting frames for review (multiple time points + local zoom) | 1 frame can't reveal mid-segment problems; at least 6 frames + 2–3× zoom on key parts |
| Re-running after a fix (rather than "it should be fine now") | If you didn't re-run, you didn't fix it |
One-Sentence Judgement
Cutting waiting time is okay; cutting opportunities to find errors is not.
A 70-minute render is unacceptable, not because "70 minutes is too long," but because 85% of that 70 minutes was 4 processes contending for CPU—waiting bought no quality. But spending 4× more rendering time for a 180° shutter buys a tangible improvement in visual feel—that spend is mandatory.
Correct Speed-Up Order
- First kill waste: concurrent → serial, merge duplicate startups, delete redundant documents, batch frame extraction
- Then optimize implementation: screenshot parameters, multi-tab in a single process, pipe to encoder without writing to disk
- Only then consider reducing verification — and this step should almost never happen; if it does, steps 1 and 2 weren't done properly
Ironclad Rules
Violating any of these makes the output non-verifiable; redo it.
- No code without specifications. Produce the shot table and timeline table first (STEP 1). Every element in the table must have
three time intervals:
enter/active/exit. Missing one means you haven't thought through when it exists. But when the user gives only a single sentence ("make something impressive" "show me what you can do" "go all out"), do not ask them follow-up questions — fill in the four slots (motif, meta-narrative, form, tier) yourself and write them intostoryboard.md, then start working. See references/wish-mode.md. - Every visual state is a pure function of
seek(t). ForbidrequestAnimationFrame,Date.now(),performance.now(),Math.random(), auto-increment counters, and self-running CSS@keyframes/transition. The same code at the sametmust always render the same pixel. - All numerical values must come from scripts; mental math is forbidden. Curve control points, spring parameters, stagger offsets,
coordinate transformations, contrast ratios, CSS
linear()sequences—everything must be produced byease.mjs. Before any such number appears in your response, there must be a tool output. See below "Numbers go to tools, judgement stays with you." - After any change, run
verifyandrender + audit.FAILis not deliverable. For eachWARN, provide either "fix" or "this is by design because…" Do not skipverifyand render the full piece—that's spending an extra 8 seconds just to get a vague "the animation didn't move" with no clue why. - Delivery triad: final piece (mp4/webm/gif) + contact sheet (-sheet.png) + audit conclusion.
- If unsure about aesthetics, offer options, don't guess. Two color schemes, two rhythms—let the user choose. Better than delivering a mediocre version.
- Don't wait for assets; draw them yourself. The deliverable must not contain phrases like "suggest inserting an image," "place image here," or "wait for assets." The rendering pipeline only uses HTML/CSS/SVG/Canvas—all visual elements must be generated by code. When you need lighting/textures/graphics/icons, first check references/asset-recipes.md (20 categories of ready-to-use snippets that can be pasted directly: glow, sweep light, grain, glass card, neon, grid, abstract illustration, particles, wireframe sphere, charts, city silhouette, glitch, scan lines, icon set, light beam, depth of field, fabric texture, bokeh, pseudo-3D), and if not there, write the SVG path yourself. Forbidden to use emoji or Unicode symbols as graphics. Every frame must have at least three layers (background gradient/grid → main subject → atmospheric grain/bokeh), at least 2 custom visual elements, and at least 1 depth cue (shadow/gradient/overlay/grain). A frame with only one layer, only text and color blocks = fail.
- Prioritize existing assets; prohibit rebuilding. If the workspace already contains assembled skeletons/specs/components/design files,
you must adopt the entire set without re-estimating a single value. Use
rigcheck.mjs --refto prove you used the original values. Incident record: a rebuild changedhead.posfrom[-6.8,-182.5]to[0,-248](65.9px off),torso.pvfrom[160,160]to[160,291](131px off), resulting in the head floating to the left of the body, and "visual inspection" didn't catch it—because the human eye reads it as "a character." Such errors must be caught with numbers. - Asset whitelist. Only use assets explicitly listed in
storyboard.md. Other files in the workspace (previous versions, unrelated character images, configs from other projects) must not be touched, even if they seem usable. When a new asset is needed, add a line to the storyboard explaining its purpose, then look for it. - Don't lower verification standards, and don't render concurrently.
auditreporting WARN is not "deliverable": hard jumps and flat frames must be eliminated or explained one by one, or redo.verify/rigcheckreporting FAIL means stop immediately. Only onerender.mjsprocess at a time—the script has a single-instance lock; concurrency just wastes CPU. To parallelize, use multiple tabs in a single Chrome instance (see cinema-grade.md §9), not multiple processes.
Numbers Go to Tools, Judgement Stays with You
Models of this class (Flash / mini / turbo) share a common tendency: they'd rather estimate than call a tool. They think mental math is zero-cost and tools make you wait. This cost calculation is wrong. Let's break it down with numbers:
| Mental Math | Run ease.mjs | |
|---|---|---|
| Direct time | 0 seconds | ~0.3–0.5 seconds |
| Probability of getting it right the first time | 30–40% (guess one Bézier control point wrong, and the entire curve changes character) | 100% |
| Cost of being wrong | Render 90 frames ≈ 8 seconds + review, and you likely won't even see the error—it'll just "not feel smooth" | 0 |
| Expected cost | 0 + 0.5 × (8 + 30) ≈ 19 seconds, with silent quality degradation | 0.5 seconds |
The 0.5 seconds "saved" by mental math is the most expensive kind of saving. So this is a hard requirement, not a suggestion:
The following numbers must always come from tool output; never hand-write inferred values in your response:
| What you need | What to run |
|---|---|
| Properties of any Bézier (overshoot, plateau, sampling table) | node scripts/ease.mjs cubic 0.22 1 0.36 1 |
| Spring parameters (how much bounce, how long to settle, whether it stops mid-way) | node scripts/ease.mjs spring 120 14 1 1.2 |
Compress a curve into CSS linear() | node scripts/ease.mjs css-linear --preset silkIn |
| Stagger offsets for a group of elements, check if they exceed total duration | node scripts/ease.mjs stagger 7 2.4 0.5 |
| Whether text contrast meets requirements | node scripts/ease.mjs contrast "#0b0e14" "#6d8bbd" |
| Generate readable grayscale steps from a base color | node scripts/ease.mjs ramp "#0b0e14" 5 |
Self-Check Three Questions (ask yourself before delivery; if you can't answer, it's a violation):
- Can every curve parameter in my response be traced back to a command output?
- Did I write down an estimated number like "around 0.3" anywhere? Yes → delete it, run the command.
- When I say "this curve is very smooth," is it because I checked the ASCII plot and overshoot percentage from
ease.mjs, or because I guessed?
It also wants to verify itself, so use the verifier to reassure it: verify.mjs reads real computed styles,
not guesses. When it thinks "let me just confirm," the correct action is to run verify, not simulate in its head.
Environment
Zero npm dependencies. Requires Chrome or Edge (already on the machine) + ffmpeg + ffprobe.
ffprobe is only used by audit to verify resolution/duration. Running without it will not silently pass:
if --width/--height/--duration is declared with ffprobe missing, it FAILs; if undeclared, it WARNS and prints
"Resolution unknown (not verified)."
node scripts/render.mjs --check # Print chrome / ffmpeg / node / WebSocket capabilities
If chrome shows NOT FOUND, specify with --chrome <path>, or install a browser first.
What to Do When There's No Image Understanding
Some models (e.g., text-only inference versions) cannot read_image. This chain's judgement is entirely numerical, not visual,
so it can still be executed: ease (parameters) → verify (numerical trajectory + idempotency) → audit (pixel statistics; conclusions are numbers)
→ rigcheck (geometry assertions).
- What's possible: structure, motion, geometry, readability
- What's not possible: composition quality, color harmony, style consistency, visual clipping
The latter must be handled by someone who can see images or a visual model. But you can delay it to the end:
use numbers to catch all detectable errors first, then present 6–8 frames (--still batch extract + --guides to overlay safe zones) to a human all at once.
Document Index (Read on Demand, Not All at Once)
| File | When to Read |
|---|---|
| references/wish-mode.md | Read this first when the user gives only one sentence: four slots, chapter derivation, default value table, no interviews in wish mode |
| references/spec-and-prompt.md | When writing the spec table in STEP 1; when you need a reproducible prompt template; when dealing with models that "resist using tools" |
| references/code-patterns.md | When writing code in STEP 2: seek(t) paradigm, easing library, keys() keyframe table, spring(f,z), SVG/Canvas, 20 modern CSS effects |
| references/asset-recipes.md | First place to look when a frame lacks elements: glow, sweep light, grain, glass, neon, grid, illustration, particles, wireframe sphere, charts, icon sets, bokeh, pseudo-3D |
| references/rig-characters.md | When a character needs to move in the frame: parts + parent-child transform chain + pose table + how to map the 12 principles to fields |
| references/cinema-grade.md | Before finalizing: motion blur, encoding parameters (anti-banding), vignette, HUD system, whip pan, frame rate selection, long-form organization, parallelization for speed |
| references/review-checklist.md | STEP 5 human visual spot-check + pre-delivery checklist |
| references/failure-modes.md | When audit/verify errors occur, look up the symptom |
| references/delta-vs-opus.md | When you want to know "how far can this system really compensate?" or need to decide whether to switch models |
First, Route: Wish Mode or Specification Mode
| What the Input Looks Like | Which Path to Take |
|---|---|
| Provides most of size/duration/content/style; or is responding to your follow-up questions | Specification Mode: STEP 0 ask questions → STEP 1 spec table → … |
| One sentence + authorizing tone ("make something impressive" "show me what you can do" "like that kind of piece" "go all out") | Wish Mode: Skip STEP 0, no follow-up questions allowed. First scan the workspace for motifs, fill in the four slots yourself, then proceed from STEP 1. See wish-mode.md |
The criterion is simple: Did they leave things unclear meaning "wait for me to ask" or "you decide"? A tone like "you figure it out/go all out/show me" is the latter.
Wish mode still requires writing storyboard.md — the difference is that you write the table, not ask for it.
Workflow
STEP 0 · Clarify Three Things (Specification Mode only; skip for Wish Mode)
- Use case: Presentation intro / Social short video / Web hero animation / Explanatory clip / Data growth animation / GIF
- Size and duration: See default values in table below, unless user specifies otherwise
- Emotion: Stable (corporate/finance), Fast (tech/product launch), Soft (lifestyle/brand), Hard (gaming/sports)
| Use Case | Size | Duration | Frame Rate | Format |
|---|---|---|---|---|
| Intro / Product Launch | 1920x1080 | 3–6s | 30 | mp4 |
| Social Portrait | 1080x1920 | 5–15s | 30 | mp4 |
| Web Hero / Banner | 1920x800 or 1440x600 | 2–4s, loopable | 30 | webp / apng (for transparency); webm requires alpha verification first |
| Data Animation / Chart | 1920x1080 | 3–8s | 30 | mp4 |
| Looping Motion Graphic | 800x800 / 1080x1080 | 1–3s, must loop seamlessly | 15–20 | gif / webp |
| Explanatory Clip | 1920x1080 | 2–5s per shot, assembled later | 30 | mp4 |
Exceeding 10 seconds or 3 shots: split into multiple scene files, render and verify each individually, then concatenate with ffmpeg.
Don't let a single seek() handle 30 seconds—nobody will be able to debug it.
STEP 1 · Write Specifications (The Only Allowed Creative Freedom)
Produce storyboard.md, two things are mandatory:
① Shot Table
| # | Time | What the Frame Is Doing | Main Motion | End State |
|---|---|---|---|---|
| 1 | 0.00–1.10s | Title lines float up character by character | y: +96→0, 75ms stagger per character | Title in place |
| 2 | 1.10–2.40s | Progress bar fills in + three dots pop out | scaleX: 0→1 (ease-in-out) | Bar at edge, dots aligned |
② Element Timeline Table (one row per element; this is the sole basis for code)
| Element | enter | active | exit | Property | Curve |
|---|---|---|---|---|---|
| #title span | 0.15 + i*0.075 | till end | — | translateY 96→0, opacity 0→1 | easeOutCubic |
| #bar b | 0.30 | 0.30–2.40 | — | scaleX 0→1 | easeInOutCubic |
| .dots i | 1.50 + i*0.15 | till end | — | scale 0→1 | easeOutBack |
If you can't write this table, you haven't thought it through. Don't skip it and write code; you'll only end up with "first and last frames only."
The "Curve" column isn't just a name. After naming the curve, immediately run the tool once to pin down "what this curve actually does at your duration," then fill in the row with numerical details:
node scripts/ease.mjs cubic 0.33 1 0.68 1 # easeOutCubic → check plateau/overshoot
node scripts/ease.mjs stagger 6 2.4 0.7 # 6 title characters in 2.4s, 0.7s each → stagger and overflow
So the two rows in the table become:
| Element | enter | active | exit | Property | Curve (Tool Confirmed) |
|---|---|---|---|---|---|
| #title span | 0.15 + i*0.075 | till end | — | translateY 96→0, opacity 0→1 | cubic-bezier(0.33,1,0.68,1), overshoot 0%, plateau after 0.75 → compress duration to under 0.7s |
| .dots i | 1.50 + i*0.15 | till end | — | scale 0→1 | spring(120,14) overshoot 7.1%, stabilizes in 0.67s → allocate 0.75s segment |
Criteria for a good curve (the tool will tell you directly): overshoot ≤ 15%, no mid-segment plateau, spring stabilization time ≤ segment length. If any criterion is not met, change parameters and re-run, rather than "write it like this first and see how it renders."
Template and more patterns: references/spec-and-prompt.md
STEP 2 · Implementation
Start with a skeleton, don't write the entire scene at once. Skeleton = background + one element that can move + a working seek contract.
window.scene = {
duration: 3.0, fps: 30, width: 1920, height: 1080, ready: true,
seek(t) {
// Each element: translate the row from the timeline table into 3 lines of code
// k = curve(seg(t, enter, enter + duration)) → use k to interpolate property
},
};
Must inline CSS and JS. Don't reference external CDNs (they will fail as if offline during rendering).
Use system fonts ("Segoe UI", "Microsoft YaHei", Arial).
Available patterns (DOM/CSS/SVG/Canvas/GSAP/Web Animations, easing library, multi-segment orchestration, character-by-character text,
path stroke, number scroll, mask transition, CSS linear(), filter/blend-mode/mask usage):
references/code-patterns.md
When you need to draw something (lighting, textures, graphics, icons, charts, illustrations, pseudo-3D), first consult
references/asset-recipes.md. It has 20 categories of zero-dependency snippets,
all of which can be pasted directly into a single-file HTML and accept seek(t) driving. Don't build from scratch by intuition, and don't wait for assets.
STEP 3 · Numerical Verification (Check Numbers First, Don't Rush to Render)
This is the first link in the "verify more" chain: takes seconds, doesn't require encoding, and can pinpoint specific elements.
# See the actual trajectory of each element: who's moving, how far, where it's stuck
node scripts/verify.mjs --html scene.html --select "#bar b,#title span" --props transform,opacity --steps 31
# Verify that "the frame is determined solely by t" (same t called twice must give identical results)
node scripts/verify.mjs --html scene.html --select ".item" --idempotent
It will give you answers like:
⚠ #b[0] · sx — Stopped mid-way 0–2s then resumed changing→ interpolation was broken or omitted hereℹ #card[0] · (all) — This element had no changes at all across the whole segment→ selector didn't match / style not written into DOM✖ seek is not idempotent: 13 spots where same t returned different results→ didseekuse cumulative state
Proceed only after the numerical layer passes. Don't render the full piece when numbers are wrong—you'll only get a blurry "the animation doesn't seem to move" and have to reverse-engineer the cause from pixels. Let numbers speak first.
STEP 4 · Rendering + Pixel Verification
First, only render keyframes—fast and sufficient to judge composition:
node scripts/render.mjs --html scene.html --still 0.2,1.0,2.2 --out out/stills
Look at these images (use read_image yourself; don't skip). If composition is OK, render the full piece:
node scripts/render.mjs --html scene.html --out out/v.mp4 --fps 30 --sheet
node scripts/audit.mjs --video out/v.mp4 --fps 30 --duration 3 --width 1920 --height 1080
audit provides: pixel change curve, rhythm bar, and graded conclusions. Fix based on the conclusions,
fix only the one named issue per round, then re-run audit immediately. Maximum 5 rounds;
if still FAIL at round 5, go back to STEP 1 and change the specification (indicating the spec itself was wrong).
Common error lookup: references/failure-modes.md
STEP 5 · Human Visual Spot-Check
The machine only guarantees "structure is correct". These things require you to look at the images:
- Is text overflowing / clipped / blending into the background?
- Is the visual center of gravity where expected (rule of thirds / centered / center of mass of element group)?
- Is contrast sufficient (gray text at 40% opacity on a dark background is invisible)?
- Are entry/exit points clean (no elements starting to move outside the frame, or suddenly disappearing inside)?
Checklist: references/review-checklist.md
STEP 6 · Delivery
# Add one more render for the required format
node scripts/render.mjs --html scene.html --out out/v.webm --format webm
node scripts/render.mjs --html scene.html --out out/v.gif --format gif --fps 15
Deliver: final piece path, contact sheet, the verdict lines from audit, and storyboard.md.
If audit has WARN but you judge it's by design, explicitly state which one and why.
Final version must be re-rendered with motion blur.
node scripts/render.mjs --html scene.html --out out/final.mp4 --fps 30 --motion-blur 180
This is the boundary between "looks smooth" and "looks like a screen recording." During iteration, you can temporarily disable it to save time,
but the delivery version must have shutter—skipping it is quality degradation. After enabling, re-run audit to verify duration and frame count aren't changed.
For fast motion (character actions, whip pans, particles), also use --fps 60.
Quick Command Reference
# Health check
node scripts/render.mjs --check
# Math (curves / springs / stagger / contrast) — mental math forbidden
node scripts/ease.mjs cubic 0.22 1 0.36 1
node scripts/ease.mjs spring 120 14 1 1.2
node scripts/ease.mjs css-linear --preset silkIn
node scripts/ease.mjs stagger 7 2.4 0.5
node scripts/ease.mjs contrast "#0b0e14" "#6d8bbd"
node scripts/ease.mjs ramp "#0b0e14" 5
node scripts/ease.mjs presets
# Skeletal geometry check (mandatory when characters are present; must use --ref to align with existing rig)
node scripts/rigcheck.mjs --rig rig/ander.rig.json
node scripts/rigcheck.mjs --rig lib/rig.json --ref rig/ander.rig.json
node scripts/rigcheck.mjs --rig rig.json --pose '{"head":{"rot":0.2}}' --assert
# SKILL.md frontmatter check (run after editing the skill to prevent "entire skill fails to load")
node scripts/lint-skill.mjs
# Numerical verification (seconds-level, can pinpoint elements)
node scripts/verify.mjs --html scene.html --select "#bar b" --props transform,opacity --steps 31
node scripts/verify.mjs --html scene.html --select ".item" --idempotent
node scripts/verify.mjs --html scene.html --all --json
# Keyframes (for visual review, fastest)
node scripts/render.mjs --html scene.html --still 0,0.5,1.5,2.9 --out out/stills
# Keyframes + composition guides (100px grid / 80px safe zone / rule of thirds / center cross)
node scripts/render.mjs --html scene.html --still 1.6 --guides --out out/guides
# Full piece + contact sheet
node scripts/render.mjs --html scene.html --out out/v.mp4 --fps 30 --sheet
# Final delivery version with motion blur (180° shutter = cinematic standard; 4× render time, use only for final)
node scripts/render.mjs --html scene.html --out out/final.mp4 --fps 30 --motion-blur 180
# 60fps only for fast motion (character actions, whip pans, particles)
node scripts/render.mjs --html scene.html --out out/final.mp4 --fps 60 --motion-blur 180
# 2x supersampling (sharper, 3–4× slower)
node scripts/render.mjs --html scene.html --out out/v.mp4 --scale 2
# Transparent background: webp / apng / png are reliable
node scripts/render.mjs --html scene.html --out out/v.webp --format webp --alpha
node scripts/render.mjs --html scene.html --out out/v.apng --format apng --alpha
# Webm alpha depends on ffmpeg build. Local tests show VP8/VP9 both degrade to alpha-less;
# script reads back the first frame to verify — if no transparent pixels, errors out and tells you which format to use instead
node scripts/render.mjs --html scene.html --out out/v.webm --format webm --alpha
# Looping motion graphic
node scripts/render.mjs --html scene.html --out out/v.gif --format gif --fps 15
# Pixel verification
node scripts/audit.mjs --video out/v.mp4 --fps 30 --duration 3 --width 1920 --height 1080
node scripts/audit.mjs --video out/v.gif --loop # Loop seam check
node scripts/audit.mjs --video out/v.mp4 --json out/audit.json
render.mjs parameters: --html --out --fps --duration --width --height --scale --format(mp4|webm|gif|webp|apng|png) --still --sheet --alpha --file --keep-frames --chrome --check --quiet
verify.mjs parameters: --html --select --props --steps --idempotent --all --json --tolerance
audit.mjs parameters: --video --fps --duration --width --height --json --loop --verbose