---
type: reference
title: "CAMDOM — Generator-Based Animation Engine"
description: "Deep-dive into the custom tweening motor: generator worklets, frame-accurate timing, composable primitives, real usage patterns, and potential as standalone library."
tags: [camdom, animation, reanimated, worklets, generators, tweening, engine, react-native]
timestamp: "2026-07-20"
---

# CAMDOM — Generator-Based Animation Engine

A generator-based animation motor built on React Native Reanimated worklets. 164 lines of code. Used across 5+ components. Potential candidate for extraction as a standalone library.

## 1. Core Insight

Traditional React Native animation libraries (Reanimated's `withTiming`, `withSpring`, Moti, React Native Reanimated Layout Animations) use **configuration objects** — you describe *what* should happen, and the library figures out *when*.

The CAMDOM engine inverts this: you write **generators** that describe *sequential logic*, and the engine drives them frame-by-frame on the UI thread. The result is code that reads like synchronous imperative logic but runs asynchronously inside Reanimated worklets.

```typescript
// Traditional: declarative config
withTiming(value, { duration: 600, easing: Easing.inOut(Easing.ease) })

// CAMDOM: imperative generator
yield* timing(value, { to: 1, duration: 600 })
```

The difference is **control flow**. With config objects, you can't easily express "wait for this, then do that in parallel with this other thing, but only after this condition becomes true." With generators, that's just code.

## 2. Architecture

### 2.1 The Frame Loop

```
useFrameCallback (Reanimated UI thread)
    │
    ▼
gen.value.next(timeSincePreviousFrame)
    │
    ▼
Generator yields → engine captures delta time
    │
    ▼
Next frame: generator resumes with accurate delta
```

The `useAnimation` hook registers a `useFrameCallback` on the Reanimated UI thread. Each frame, it calls `gen.value.next(ts)` where `ts` is the milliseconds since the last frame. The generator receives this delta via `yield` and uses it to advance its internal clock.

**Key property:** Animations are **frame-accurate**, not time-accurate. If a frame takes 33ms instead of 16ms, the next `yield` delivers the correct delta. There's no drift.

### 2.2 Generator Delegation

The engine uses JavaScript's `yield*` (generator delegation) as its composition mechanism. This is the critical design decision that makes the engine composable.

```typescript
function* parallel(...inputs: Inputs) {
  "worklet";
  const iterators = inputs.map((input) => materializeGenerator(input));
  let isDone = false;
  let timeSinceFirstFrame = 0;
  while (!isDone) {
    const done = [];
    for (const iterator of iterators) {
      const val = iterator.next(timeSinceFirstFrame);
      done.push(val.done);
    }
    isDone = done.every((d) => d);
    if (!isDone) {
      timeSinceFirstFrame = yield;
    }
  }
}
```

When you write `yield* parallel(timingA, timingB)`, JavaScript delegates the outer generator's `yield` calls to the inner generators. The `parallel` primitive advances all iterators each frame, and yields back to the caller when it needs the next frame's delta. This is **zero-cost composition** — no arrays of callbacks, no promise chains, no event emitters.

### 2.3 Materialization

```typescript
const materializeGenerator = (input: Input) => {
  "worklet";
  return typeof input === "function" ? input() : input;
};
```

Generators can be passed as **factory functions** (`() => generator()`) or **live generators** (`generator()`). Factory functions are used when a generator needs to be re-created (e.g., in `parallel` where each iterator needs its own state). This is a subtle but important ergonomic choice.

## 3. Primitive Reference

### 3.1 `timeSincePreviousFrame()`

```typescript
export function* timeSincePreviousFrame() {
  "worklet";
  const time: number = yield;
  return time;
}
```

The atomic primitive. Yields once, receives the frame delta, returns it. Every other primitive builds on this.

**Why it exists:** Directly accessing `useFrameCallback`'s `timeSincePreviousFrame` parameter would couple every animation to the hook. By making it a generator, animations become pure functions that can be composed, nested, and tested independently of the rendering pipeline.

### 3.2 `timing(value, config?)`

```typescript
export function* timing(value: SharedValue<number>, rawConfig?: TimingConfig) {
  "worklet";
  const from = value.value;
  const { to, easing, duration } = { ...defaultTimingConfig, ...rawConfig };
  const start: number = yield;
  const end = start + duration;
  for (let current = start; current < end; ) {
    const progress = easing((current - start) / duration);
    const val = interpolate(progress, [0, 1], [from, to]);
    value.value = val;
    current += yield* timeSincePreviousFrame();
  }
  value.value = to;
}
```

**Behavior:**
- Reads `from` from the current SharedValue (not a config parameter)
- Yields once to capture the start time
- Loops frame-by-frame, computing easing progress and interpolating
- Snaps to final value on completion (no floating-point drift)
- Uses `yield* timeSincePreviousFrame()` — frame-accurate

**Default config:**
```typescript
{ to: 1, easing: Easing.inOut(Easing.ease), duration: 600 }
```

**Design decision:** `from` is read from `value.value` at generator start, not passed as a parameter. This means the animation always starts from the current state — you can't accidentally create a jump. If you need to set a specific start value, set `value.value` before yielding.

### 3.3 `wait(duration)`

```typescript
export function* wait(duration = 1000) {
  "worklet";
  const from: number = yield;
  const to = from + duration;
  for (let current = from; current < to; ) {
    current += yield* timeSincePreviousFrame();
  }
}
```

A dead zone. Accumulates frame deltas until the target duration has elapsed. Used for delays in sequences and as the offset mechanism in `stagger`.

### 3.4 `waitUntil(value, invert?)`

```typescript
export function* waitUntil(value: SharedValue<boolean>, invert = false) {
  "worklet";
  while (invert ? value.value : !value.value) {
    yield;
  }
}
```

**The reactive primitive.** Blocks the generator until a SharedValue becomes `true` (or `false` if `invert = true`). Each frame it checks the condition and yields if not met.

**Usage in CAMDOM:** Every component uses this to wait for menu open/close state:
```typescript
yield* waitUntil(isOpened, !to);  // wait for the toggle to flip
```

This is what enables the **reactive animation pattern** — animations don't fire on a trigger, they *wait* for state changes. The generator is alive the entire time the component is mounted, ready to respond.

### 3.5 `parallel(...inputs)`

```typescript
export function* parallel(...inputs: Inputs) {
  "worklet";
  const iterators = inputs.map((input) => materializeGenerator(input));
  let isDone = false;
  let timeSinceFirstFrame = 0;
  while (!isDone) {
    const done = [];
    for (const iterator of iterators) {
      const val = iterator.next(timeSinceFirstFrame);
      done.push(val.done);
    }
    isDone = done.every((d) => d);
    if (!isDone) {
      timeSinceFirstFrame = yield;
    }
  }
}
```

Runs N generators simultaneously. All receive the same frame delta. Completes when ALL generators are done.

**The elegance:** Because generators maintain their own local state (closures over `start`, `current`, `from`), each iterator in `parallel` is fully independent. They share only the frame delta. This is why you can nest `parallel` inside `parallel` without conflicts.

### 3.6 `stagger(delay, ...inputs)`

```typescript
export function* stagger(delay: number, ...inputs: Inputs) {
  "worklet";
  const iterators = inputs.map((input, index) => {
    return (function* () {
      yield* wait(delay * index);
      yield* materializeGenerator(input);
    })();
  });
  yield* parallel(...iterators);
}
```

**The composition primitive.** For each input, creates a new generator that waits `delay * index` milliseconds, then runs the actual animation. All staggered generators are then run in parallel.

**Usage in CAMDOM:**
```typescript
yield* stagger(duration / 2, timing(transition, { to, duration }));
```

When used with a single timing, this is functionally identical to `timing()` — the stagger adds 0ms delay for index 0. But when the lateral menu renders multiple children, each can have its own `stagger` with different delays, creating cascading entrance animations.

## 4. The Hook: `useAnimation`

```typescript
export const useAnimation = <S extends AnimationState>(
  input: Animation<S> | (() => Generator),
  pause?: SharedValue<boolean>,
) => {
  const offset = useSharedValue(0);
  const { animation, state } =
    typeof input === "function" ? { animation: input, state: {} as S } : input;
  const values = useSharedValues(state);
  const gen = useSharedValue<null | Generator>(null);
  useFrameCallback(({ timeSincePreviousFrame: ts }) => {
    if (gen.value === null) {
      gen.value = animation(values);
    }
    if (pause?.value) {
      offset.value += ts ?? 0;
    } else {
      if (gen.value.next) {
        gen.value.next(ts);
      } else {
        gen.value = animation(values);
        gen.value.next(ts);
      }
    }
  });
  return values;
};
```

**Key behaviors:**

1. **Lazy initialization:** Generator is created on first frame, not on mount. Avoids timing issues with SharedValue initialization.

2. **Auto-restart:** When the generator completes (`gen.value.next` is undefined), it's automatically re-created from the factory. This enables the `while (true)` loop pattern used in CAMDOM components — the animation is always alive, waiting for the next state change.

3. **Pause support:** When `pause` SharedValue is `true`, frame deltas are accumulated in `offset` instead of being passed to the generator. This effectively freezes the animation without losing the generator's state.

4. **Automatic cleanup:** `useSharedValues` registers an effect that calls `cancelAnimation` on all SharedValues when the component unmounts.

5. **Return value:** Returns the `AnimationValues` object — a record of SharedValues that the animation writes to and the component reads via `useAnimatedStyle`.

## 5. Real Usage Patterns

### 5.1 The Menu Toggle Pattern (All Lateral Menu Components)

Every lateral menu component (BillyBoyVerticalLogo, BuyAComdomLateralText, LateralMenu, MenuIcon, CamdomLogo, UnlockYourPleasure) uses the same animation structure:

```typescript
const animation = makeAnimation(
  function* ({ isOpened, display, transition }) {
    "worklet";
    let to = 1;

    while (true) {
      // 1. Wait for state change
      yield* waitUntil(isOpened, !to);

      // 2. Show element (before animation starts)
      if (isOpened.value) {
        display.value = true;
      }

      // 3. Animate with stagger
      yield* stagger(duration / 2, timing(transition, { to, duration }));

      // 4. Hide element (after animation completes)
      if (!isOpened.value) {
        display.value = false;
      }

      // 5. Flip direction for next toggle
      to = to === 1 ? 0 : 1;
    }
  },
  { isOpened: false, display: false, transition: 0 },
);
```

**The pattern:**
1. Wait for a boolean SharedValue to flip
2. Set display=true (show the element)
3. Animate opacity 0→1 or 1→0
4. Set display=false (hide the element after fade-out)
5. Flip the target direction
6. Loop back to step 1

**Why `while (true)`:** The generator never exits. It's always alive, always waiting. When `isOpened` flips, the generator wakes up, runs the animation, and goes back to sleep. This is fundamentally different from event-driven animations — there's no "trigger" to miss.

### 5.2 The Component Integration

```typescript
// 1. Create animation definition (module-level, shared across instances)
const animation = makeAnimation(generatorFn, initialState);

// 2. In the component, instantiate
const { isOpened, display, transition } = useAnimation(animation);

// 3. Drive state from React
useEffect(() => {
  isOpened.value = isShowingLateralMenu;
}, [isShowingLateralMenu, isOpened]);

// 4. Read state for rendering
const style = useAnimatedStyle(() => ({
  opacity: transition.value,
  display: display.value ? "flex" : "none",
}));
```

**The separation:** Animation logic (generator) is defined at module level and shared. State (SharedValues) is instantiated per-component. React drives the input (`isOpened`), the generator handles the output (`transition`, `display`), and `useAnimatedStyle` bridges them to rendering.

## 6. Comparison with Other Approaches

| Approach | Composition | State | Frame Accuracy | Interruptibility |
|---|---|---|---|---|
| **Reanimated `withTiming`** | Config objects | Implicit | Time-based | Limited (new animation overwrites) |
| **Moti** | Props-based | Prop-driven | Time-based | Good (key-based) |
| **React Native Layout Animations** | Entering/Exiting | Layout-triggered | Frame-based | Poor (can't interrupt) |
| **CAMDOM generators** | `yield*` delegation | Generator-local | Frame-accurate (delta) | Natural (generator state preserved) |

**The advantage:** Generator-based composition is **orthogonal** to the animation parameters. You can nest `parallel(stagger(100, timing(a), timing(b)), wait(500), timing(c))` without any additional API surface. Each primitive does one thing; composition is free.

**The disadvantage:** Generators are unfamiliar to most React Native developers. The learning curve is steeper than prop-based APIs. Debugging is harder — you can't inspect generator state from React DevTools.

## 7. Potential as Standalone Library

### 7.1 What Would a Library Version Add?

The current implementation is 164 lines. A production library would need:

**Core primitives (already exist):**
- `timing`, `wait`, `waitUntil`, `parallel`, `stagger`

**Missing primitives:**
- `sequence(...inputs)` — run generators sequentially (trivial: `for (const input of inputs) yield* input`)
- `spring(value, config?)` — spring physics using Reanimated's `withSpring` internals
- `repeat(count, generator)` — repeat N times or infinitely
- `condition(predicate, then, else?)` — branching inside generators
- `chain(...generators)` — sequence with shared state
- `cancel()` — cooperative cancellation inside a generator
- `race(...inputs)` — complete when first generator finishes

**Missing features:**
- **TypeScript generics** — stronger typing for animation state
- **Dev mode warnings** — detect generators that never yield, detect stalled animations
- **Performance monitoring** — frame time tracking, jank detection
- **Persistence** — save/restore animation state for screen transitions
- **Gesture integration** — `fromGesture(gestureHandler)` as a generator source
- **Scroll-driven** — `fromScroll(scrollView)` as a frame source

### 7.2 Package Architecture

```
react-native-generator-animations/
├── src/
│   ├── primitives/
│   │   ├── timing.ts
│   │   ├── spring.ts
│   │   ├── wait.ts
│   │   ├── waitUntil.ts
│   │   ├── parallel.ts
│   │   ├── stagger.ts
│   │   ├── sequence.ts
│   │   ├── repeat.ts
│   │   └── race.ts
│   ├── core/
│   │   ├── useAnimation.ts
│   │   ├── makeAnimation.ts
│   │   └── materializeGenerator.ts
│   ├── integration/
│   │   ├── fromGesture.ts
│   │   ├── fromScroll.ts
│   │   └── fromValue.ts
│   └── index.ts
├── examples/
│   ├── menu-toggle.tsx
│   ├── card-stack.tsx
│   └── scroll-parallax.tsx
└── package.json
```

### 7.3 Naming

Potential names:
- **gen-motion** — generator + motion
- **yield-motion** — the yield keyword as brand
- **worklet-sequences** — descriptive
- **camdom-engine** — origin story

### 7.4 What Makes It Different from Existing Libs

| Library | Paradigm | Composition | UI Thread |
|---|---|---|---|
| Reanimated | Config objects | Method chaining | Yes |
| Moti | React props | Prop spreading | Yes |
| Animated API | Value listeners | Nested callbacks | No (bridge) |
| **Generator Engine** | Generator functions | `yield*` delegation | Yes (worklet) |

The unique value proposition: **imperative control flow on the UI thread**. No other React Native animation library gives you `for` loops, `if/else`, and `while` inside an animation sequence. Generators do.

### 7.5 Open Source Viability

**Strengths:**
- 164 lines — trivially auditable
- Zero dependencies beyond Reanimated
- Frame-accurate — technically superior to time-based approaches
- Composable — `yield*` is a language feature, not a library API
- Proven in production (CAMDOM, 300K+ downloads, 30+ countries)

**Weaknesses:**
- Small surface area — competitors have more primitives out of the box
- Generator unfamiliarity — most JS developers don't use generators daily
- No spring physics yet — would need Reanimated's spring internals
- Debugging opacity — generator state is invisible to dev tools

**Opportunity:** The React Native animation ecosystem is fragmented. Reanimated is powerful but verbose. Moti is ergonomic but limited. A generator-based approach occupies a unique niche: **composable imperative animations on the UI thread**. If documented well with examples, it could find a audience among developers who've felt the limits of config-based animation.

## 8. The Philosophical Design

The deepest insight in this engine is that **animations are sequential logic, not declarative state**.

When you write `yield* waitUntil(isOpened)`, you're expressing *intent*: "wait here until the user does something." When you write `yield* stagger(100, timing(a), timing(b))`, you're expressing *choreography*: "these things happen in sequence, with this offset."

Traditional animation libraries force you to express choreography through configuration:
```typescript
// Reanimated: nested config hell
withSequence(
  withDelay(100, withTiming(val1, { duration: 300 })),
  withDelay(200, withTiming(val2, { duration: 300 }))
)
```

Generator-based choreography:
```typescript
// CAMDOM: sequential logic
yield* wait(100);
yield* parallel(timing(val1, { duration: 300 }), timing(val2, { duration: 300 }));
```

The second version is not just shorter — it's **legible**. You can read the timeline left-to-right, top-to-bottom. The control flow is explicit. And because generators can contain `if/else`, `for`, and `while`, you can express conditional choreography without additional API surface:

```typescript
if (shouldAnimateIn) {
  yield* timing(value, { to: 1, duration: 300 });
} else {
  value.value = 1; // instant snap
}

yield* wait(200);
yield* parallel(timing(a), timing(b));
```

No config-based library can express that without adding a `condition()` primitive. With generators, it's just JavaScript.

---
*Source: condom-app/utils/Animations.ts (164 lines) + component usage analysis*
*See also: [[camdom-architecture]], [[camdom-design-system]]*
