# How the Ink Theater Engine Enables Hand-Drawn Doodle Animations in OpenMontage

> Discover how the Ink Theater engine crafts hand-drawn doodle animations in OpenMontage using SVG strokes, inverse kinematics, and motion capture data. Learn more!

- Repository: [Calesthio/OpenMontage](https://github.com/calesthio/OpenMontage)
- Tags: internals
- Published: 2026-08-30

---

**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`](https://github.com/calesthio/OpenMontage/blob/main/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:

```javascript
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`](https://github.com/calesthio/OpenMontage/blob/main/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:

```javascript
const turb = document.getElementById('boilFilter');
InkTheater.boil(turb, tl, { duration: 8, fps: 9 });

```

According to the implementation in [`ink-theater/ink-theater.js`](https://github.com/calesthio/OpenMontage/blob/main/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`](https://github.com/calesthio/OpenMontage/blob/main/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`](https://github.com/calesthio/OpenMontage/blob/main/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`](https://github.com/calesthio/OpenMontage/blob/main/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`](https://github.com/calesthio/OpenMontage/blob/main/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`](https://github.com/calesthio/OpenMontage/blob/main/ink-theater/mocap/catalog.json) and maps it to the SVG rig.

### Creating and Revealing the Puppet

```javascript
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`](https://github.com/calesthio/OpenMontage/blob/main/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:

```javascript
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`](https://github.com/calesthio/OpenMontage/blob/main/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:

```javascript
// 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 `inkPath` function 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`](https://github.com/calesthio/OpenMontage/blob/main/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.