Licensing (finding 1): - Mocap library is now 100% CMU-sourced (free for any use). Replaced the 3 Meta/FAIR clips with CMU equivalents: wave=141_16, shuffle=77_29 (creep); dropped the un-CMU "dab", added twist=141_12. Deleted the 4 committed FAIR BVHs (dab/jumping/wave_hello/zombie). - Ship Patrick Hand's SIL OFL license (assets/OFL.txt) beside the bundled font — OFL permits embedding/redistribution; this is the required attribution. - Add THIRD_PARTY_NOTICES.md (Patrick Hand OFL + CMU) and rewrite mocap/NOTE.md to drop the "verify before commercial use" caveat (no longer applicable). Stale clip names (finding 2): - Update the /ink-art command files (.claude/.github/.cursor/.codex), character-design-director, and the creative skills to the current catalog (wave/twist/…), and point to add-motion.mjs. - ink-puppet.js: console.warn on an unknown clip name instead of silent dead-time. Examples (finding 3): - Remove broken standalone reel.html/momentum.html (they referenced a missing ink-theater.js and momentum shipped the subset font the fix warns against). - Keep mocap-figure/ as the canonical self-contained, lintable example; refresh its bundled clips.js/ink-puppet.js; add examples/README.md with the lint path.
7.2 KiB
Ink Theater
A deterministic, seek-safe engine for hand-drawn "moving art" — a minimalist black-ink-on-white world where a deadpan mascot physically performs an abstract idea by operating absurd low-tech contraptions. Built for OpenMontage's atelier path and rendered through HyperFrames (HTML/SVG/CSS + one paused GSAP timeline → MP4).
Inspired by Ian's 小黑 / Xiaohei illustration skill (MIT — credit Ian for the technique); this is an original, generic, English, motion-first engine, not a copy.
Why it exists
The illustration style is simple enough that the illustration IS the animation — no diffusion model needed. Vector shapes + math give you the whole thing: free, deterministic, infinitely editable, and the character genuinely acts out the concept. This engine turns the research findings (memory: project_ink_atelier_animation, deep-research on vector/physics/metaphor foundations) into reusable primitives.
The five capabilities (ink-theater.js, global InkTheater)
| Module | What it does | Key API |
|---|---|---|
| ink strokes | Confident hand-drawn lines — variable-width brush ribbons + wobbled centerlines | inkPath(pts, opt), inkRibbon(pts, {width,taper,seed}) |
| boil | Seek-safe hand-drawn line "boil" — steps a feTurbulence seed off the timeline (~9fps), NOT SMIL |
boil(turbEl, tl, {duration,fps}) |
| spring physics | Closed-form damped-spring eases (anticipation/overshoot/settle) — pure functions of progress, seek-safe | springEase({stiffness,damping,mass}), ease.{settle,overshoot,bouncy,soft} |
| rig / IK | 2D FABRIK inverse kinematics + a riggable mascot whose arms reach a target | fabrik(lengths,origin,target), mascot({x,y,scale}) → .reachL/.reachR([x,y]) |
| contraption grammar | Parametric composable machine parts | parts.{crank,gauge,hopper,slot,lever,box} |
Determinism (HyperFrames render contract)
Every frame must be reproducible from time alone. This engine obeys that:
- Closed-form springs —
springEaseevaluates an analytic damped-oscillator step response, so any progresspmaps deterministically (no numeric integration, no accumulated state). - Seek-safe boil — driven by a GSAP stepped-seed tween on the timeline, never SMIL / render-time clocks.
- IK-follow via
onUpdate— pose the arm from a target whose position is set by the timeline; GSAP firesonUpdateon seek, so it's pure-function-of-time. - Seeded PRNG (
rng) for all "random-looking" wobble — no runtimeMath.random. - No
repeat:-1(finite counts only), animate only transforms/opacity/attrs.
⚠ The font gotcha (the REAL root cause)
Custom handwriting rendered as serif in every render for a long time. The cause was not SVG-vs-HTML — it was a font-subset trap: grabbing one woff2 from the Google Fonts css2 API (grep … | head -1) returns a single unicode-range subset (often cyrillic / vietnamese / latin-ext) that is missing basic-latin (ASCII). So every English word silently falls back to serif — while the renderer still logs Fonts: 1 loaded. (This means earlier demos whose captions "looked handwritten" were actually serif.)
Fix (verified): embed the full font file — the TrueType, or a woff2 that actually covers basic-latin:
@font-face { font-family: "InkHand"; src: url("assets/patrickhand.ttf") format("truetype"); font-display: block; }
A working Patrick Hand TTF ships at ink-theater/assets/patrickhand.ttf (SIL Open Font License — the license ships beside it as assets/OFL.txt; see THIRD_PARTY_NOTICES.md) — copy it into your project's assets/ and use font-family: "InkHand". It renders real handwriting on normal HTML overlay <div>s (verified — put caption divs over the SVG scene). Don't hot-link Google Fonts (a render-time network fetch breaks determinism); a local @font-face file is auto-inlined by the compiler at build time.
Note: HyperFrames also pre-bundles ~18 fonts (none are handwriting) — see
hyperframes-creative/references/typography.md. For handwriting you must embed your own full font as above.
Usage in a HyperFrames project
- Copy
ink-theater.jsinto the project root;<script src="ink-theater.js">after gsap. - Build the scene programmatically into a mount
<g>, keep node refs. - Apply
filter="url(#boil)"to ink groups; callInkTheater.boil(...)once. - Captions = HTML overlay divs (see gotcha).
- Register one
gsap.timeline({paused:true})onwindow.__timelines["<id>"].
Ink Puppet — real mocap on a hand-drawn figure (recommended for characters)
The right way to animate a doodle character (walk / dance / wave / jump) is not hand-tuned math — it is real motion-capture retargeted onto a stick figure. An agent should only choose the character and choreograph named moves; it must never hand-tune motion. Two pieces:
mocap/bvh2clip.mjs— offline converter: a 3D BVH mocap file → a compact 2D "clip" (per-frame joint tracks, hips-relative pose + root motion, scaled to a fixed figure height). Run once per motion; bundle clips withclips.js.ink-puppet.js— runtime: builds the stick figure, plays clips, exposes a declarative choreography API:
var p = InkPuppet.create(mount, { cx: 960, ground: 902, boil: "boil" });
p.drawIn(tl, { start: 0.4 }); // pencil sketches the figure limb-by-limb
InkPuppet.choreograph(tl, p, [ // then plays named mocap clips — zero hand-tuning
{ clip: "walk" }, { clip: "dance_spin" }, { clip: "kick" }, { clip: "wave" }
], { start: 3.7 });
// speak — comic balloon tethered to the mouth (HTML text = webfont works)
InkTheater.balloon(tl, { into: fxGroup, overlay: htmlOverlay, at: 5, dur: 2, text: "hello!", boil: "boil" });
Deterministic + seek-safe (pose is a pure function of each segment's local time).
The action library (mocap/catalog.json) ships 12 varied moves the agent picks by name — locomotion (walk, run, climb, march, shuffle), action (jump, kick), posture (sit), gesture (wave), dance (dance_spin, dance_glide, twist). Every clip is CMU-sourced (free for any use). Read the catalog and pick moves that fit the story — don't loop one clip.
Extend it in one command (self-extending, no code changes) — the converter auto-maps fair1 / CMU / Mixamo skeletons:
node mocap/add-motion.mjs backflip 05_20 dance "a backflip" # CMU id, or a URL, or a local .bvh
Free CMU mocap (una-dinosauria/cmu-mocap) has thousands. This is what Meta's Animated Drawings does, but here it stays vector, white-ink, with a draw-on reveal (AD is raster, humanoid-only, no reveal). Provenance (all clips CMU, free for any use): mocap/NOTE.md · THIRD_PARTY_NOTICES.md.
Speech balloons — InkTheater.balloon(tl, opts)
Comic balloon that grows from the mouth, with HTML overlay text (so the webfont applies). opts: into (an SVG <g>), overlay (an HTML div), at, dur, text, mouth:[x,y], center:[x,y], w, size, boil.
Demos
examples/mocap-figure/— the pencil figure draws itself, then walks / runs / dances / kicks / sits / waves via real CMU mocap. Self-contained and lintable (npx hyperframes lint ink-theater/examples/mocap-figure); seeexamples/README.md.