Audit finding from video production: handwriting rendered as serif everywhere because the woff2 we downloaded was a Google-Fonts css2 subset MISSING basic-latin (ASCII) glyphs — not because 'SVG text can't use webfonts' (the old README claim was a misdiagnosis; prior demos were silently serif). Fix: embed the FULL font. Bundles ink-theater/assets/patrickhand.ttf and corrects README + skill guidance (HTML overlay divs + full TTF).
6.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 — 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: "wave_hello" }, { clip: "dab" }, { clip: "jumping" }, { clip: "zombie" }
], { start: 3.7 });
Deterministic + seek-safe (pose is a pure function of each segment's local time). Add a motion by dropping any BVH through the converter — free CMU mocap has thousands (walk/run/dance/…). This is what Meta's Animated Drawings does, but here it stays vector, white-ink, with a draw-on reveal (Animated Drawings is raster, humanoid-only, and has no reveal — see the eval notes). Provenance of the bundled example clips: mocap/NOTE.md.
Demos
examples/mocap-figure/— the pencil figure draws itself, then waves / dabs / jumps / walks via real mocap.examples/reel.html— capabilities reel (each primitive, labeled).examples/momentum.html— "Momentum" story (springs + rig + ink + handwriting).