How the Ink Theater Engine Enables Hand-Drawn Doodle Animations in OpenMontage
The Ink Theater engine combines deterministic pure-function geometry with GSAP timeline integration to generate wobbly, hand-drawn SVG strokes and animate them via inverse kinematics and motion-capture data.
In the OpenMontage repository, the Ink Theater engine provides a self-contained framework for creating organic, sketch-like motion graphics. By treating every primitive—from variable-width ink paths to inverse-kinematics rigs—as deterministic functions of the timeline playhead, the system enables fully reproducible hand-drawn doodle animations that can be precisely scrubbed and sequenced.
Deterministic Architecture for Seek-Safe Animation
The core philosophy of the Ink Theater engine is seek-safe reproducibility. Every primitive—whether a stroke, boil effect, spring ease, or IK calculation—is implemented as a pure function of geometry or the GSAP timeline playhead. This design guarantees that frame N can be regenerated from the timestamp alone, with no hidden clocks or Math.random calls at render time.
According to the engine header in ink-theater/ink-theater.js (lines 1-6), this deterministic approach ensures that animations remain consistent when scrubbing backwards or jumping to specific timestamps, a critical requirement for HyperFrames used throughout OpenMontage.
Generating Organic Ink Strokes with inkPath
The inkPath and inkRibbon functions transform simple polylines into variable-width, wobbly SVG paths that mimic ink on paper. The wobble effect is created using a seeded PRNG (rng) combined with perpendicular jitter (wobblePts), ensuring the randomness is deterministic and repeatable.
To draw a hand-drawn line between two points:
const line = InkTheater.inkPath([[100, 200], [300, 200]], { wobble: 1.6 });
mount.appendChild(InkTheater.el('path', {
d: line,
stroke: '#000',
'stroke-width': 4
}));
As implemented in ink-theater/ink-theater.js (lines 88-93), the function accepts a step parameter for controlling point density and a wobble parameter for adjusting the jitter intensity, allowing fine-tuning of the sketch aesthetic.
Variable-Width Ribbons
For strokes that taper like real ink brushes, inkRibbon generates paths with variable width along the trajectory. This creates the illusion of pressure-sensitive drawing without requiring complex brush physics simulations.
Creating the "Living Sketch" Boil Effect
The boil effect simulates the low-frame-rate line noise characteristic of hand-drawn cel animation. Rather than using continuous random noise—which would break determinism—the engine drives an SVG <feTurbulence> filter through stepped animation of its seed attribute.
The boil function bakes these steps directly onto the GSAP timeline:
const turb = document.getElementById('boilFilter');
InkTheater.boil(turb, tl, { duration: 8, fps: 9 });
According to the implementation in ink-theater/ink-theater.js (lines 15-24), the function accepts a timeline (tl), target turbulence element, duration, and target frame rate. By stepping the turbulence seed at discrete intervals rather than every frame, the effect maintains the "hand-drawn" jitter while staying perfectly scrubbable.
Mathematical Animation Primitives
Closed-Form Spring Easings
The springEase function provides anticipatory and overshoot curves commonly used for pop-in effects, such as speech balloons emerging from a mascot. Unlike physics simulations that carry hidden state, these easings are pure functions of normalized progress (p), returning values based solely on the input parameter.
The implementation in ink-theater/ink-theater.js (lines 27-38) uses closed-form mathematical expressions to simulate elastic motion without iterative solvers, ensuring frame-perfect consistency at any playback speed.
FABRIK Inverse Kinematics
For rigging the mascot's limbs, the engine includes a fabrik solver implementing Forward And Backward Reaching Inverse Kinematics. This 2D IK system runs each frame based only on the current target position, keeping the pose completely deterministic.
As found in ink-theater/ink-theater.js (lines 63-71), the solver adjusts joint chains to reach target coordinates while maintaining bone lengths, enabling the doodle mascot to point, wave, or manipulate objects with fluid, hand-drawn motion.
Parametric Contraption Parts
The engine includes a library of reusable low-tech contraption parts—including cranks, gauges, and hoppers—implemented as plain SVG groups. These components reside in the parts object (lines 94-106 of ink-theater/ink-theater.js) and can be animated using the same deterministic primitives, enabling complex "ink-world" machinery with minimal code.
Real Motion Capture with Ink Puppet
The ink-puppet.js module bridges the gap between doodle aesthetics and professional animation by applying BVH motion-capture clips to the hand-drawn mascot. The system reads baked animation data from ink-theater/mocap/catalog.json and maps it to the SVG rig.
Creating and Revealing the Puppet
const puppet = InkPuppet.create(mount, { cx: 960, ground: 900 });
puppet.drawIn(tl, { start: 0.3 });
The create function (lines 33-71 of ink-theater/ink-puppet.js) constructs the SVG rig with specified center and ground plane coordinates. The drawIn method animates each limb using stroke-dash offsets, creating the illusion of the doodle drawing itself into existence.
Choreographing Mocap Clips
Once revealed, the puppet performs real-world motion using the choreograph method:
InkPuppet.choreograph(tl, puppet, [
{ clip: 'wave', dur: 3 },
{ clip: 'walk', dur: 4 }
], { start: 2 });
As implemented in lines 74-99 of ink-theater/ink-puppet.js, this function sequences named clips (stored in window.INK_CLIPS) such as walk, wave, or twist according to the timeline, enabling complex performances while maintaining the sketch-like visual style.
Complete Integration Example
Putting these layers together creates a fully animated scene:
// 1️⃣ set up the engine
const tl = gsap.timeline();
const mount = document.getElementById('mount');
// 2️⃣ draw a doodle path
const doodle = InkTheater.inkPath([[0,0],[200,0],[200,150]], { wobble: 1.2 });
mount.appendChild(InkTheater.el('path',{
d: doodle,
stroke: '#000',
'stroke-width': 4
}));
// 3️⃣ add a boiling line-noise filter (optional)
InkTheater.boil(document.querySelector('#boilFilter'), tl, { duration: 8, fps: 9 });
// 4️⃣ create a puppet and animate it
const puppet = InkPuppet.create(mount,{ cx: 960, ground: 900 });
puppet.drawIn(tl,{ start: 0.3 });
InkPuppet.choreograph(tl, puppet, [
{ clip: 'wave', dur: 3 },
{ clip: 'walk', dur: 4 }
], { start: 2 });
The result is a stick-figure that draws itself, then performs real-world motion while surrounding lines retain the wobbly, ink-on-paper aesthetic. All timing is driven by a single GSAP timeline, making the animation fully deterministic and scrubbable.
Summary
- Deterministic Design: All primitives in the Ink Theater engine are pure functions of the GSAP timeline playhead, enabling frame-accurate seeking and reproducible renders.
- Wobble Generation: The
inkPathfunction uses seeded PRNG and perpendicular jitter to create variable-width strokes that mimic hand-drawn ink lines. - Boil Animation: Stepped turbulence seed animation creates living line noise at controlled frame rates without breaking timeline scrubbing.
- ** Mathematical Rigor**: Closed-form spring easings and FABRIK inverse kinematics provide physics-like motion without hidden state or iterative approximations.
- Mocap Integration: The Ink Puppet system applies professional BVH motion-capture data to doodle-style rigs, bridging traditional animation and vector graphics.
- Modular Parts: Reusable contraption components allow rapid construction of complex mechanical scenes using consistent deterministic animation rules.
Frequently Asked Questions
What makes Ink Theater animations "deterministic"?
Unlike conventional animation systems that rely on Math.random or continuous physics simulations at render time, every Ink Theater primitive is a pure function of the GSAP timeline position. This means frame 100 will look identical whether you arrive there by playing forward from frame 0 or scrubbing backward from frame 200, because no hidden state exists between frames.
How does the boil effect create hand-drawn aesthetics without random flickering?
The boil effect uses SVG <feTurbulence> filters with seeds that change at discrete intervals controlled by the timeline (typically 8-12 FPS). By stepping the seed rather than randomizing it continuously, the line noise appears to vibrate like hand-drawn animation while remaining perfectly synchronized to the scrubbable timeline.
Can I use custom motion-capture clips with Ink Puppet?
Yes, the system reads from ink-theater/mocap/catalog.json and exposes clips via window.INK_CLIPS. You can import custom BVH data into this catalog, following the schema used by existing clips like walk and wave. The choreograph method will then play your custom motions using the standard puppet rig.
Does the engine support real-time interaction or only pre-baked timelines?
While optimized for GSAP timeline integration, the deterministic nature of the engine means you can drive animations by updating timeline progress in real-time. Since fabrik solves based on the current target and inkPath generates from seed-based functions, you can update target positions or regenerate paths interactively without breaking the hand-drawn aesthetic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →