Scene orchestration

Recording authored intent — spawning actors, consuming time, and composing concurrent work.

This layer is what runs during compile. Everything here writes to the timeline record; nothing here renders.

createScene

Compiles a generator into a CompiledScene. The generator receives a CompileContext (ctx) and returns typed scene exports.

const compiled = createScene(
  function* (ctx) {
    const card = ctx.spawn(Card, { id: "card" });

    yield* card.appearance.opacity.tween(1, 0.3);
    yield* ctx.wait(1);
    yield* card.layout.x.tween(200, 0.5);

    return { card };
  },
  { fps: 60 },
);

fps defaults to 30. It is the only place authored seconds become frames, so it belongs with the scene rather than with the renderer.

ctx

// Spawn an actor instance
ctx.spawn(Card, { id: "card" });

// Wait in authored time (seconds)
yield* ctx.wait(1.5);

// Run generators concurrently — finishes when the longest finishes
yield* ctx.all(
  card.appearance.opacity.tween(1, 0.3),
  label.appearance.opacity.tween(1, 0.3),
);

// Run with a stagger delay between each start
yield* ctx.sequence(0.05, ...items.map((i) => i.enter()));

// Offset one generator without waiting for it first
yield* ctx.delay(0.2, card.enter());

// Scoped sub-context — the cursor resets to the scope start when it ends
yield* ctx.scope(function* (scope) {
  yield* scope.wait(0.5);
});

ctx.cursor and ctx.cursorSeconds read the current authoring position, in frames and in seconds respectively.

frame 0·0.00slevels → [0.00, 0.00, 0.00, 0.00, 0.00]

Five bars raised with ctx.all, dropped, then raised again with ctx.sequence(0.08, …). Scrub to the second rise to see the offset: all starts everything at the cursor, sequence offsets each start by the delay.

ctx.all(...) — a
0.3s
ctx.all(...) — b
0.3s
ctx.sequence(0.05, …) — 1
12f
ctx.sequence(0.05, …) — 2
12f
ctx.sequence(0.05, …) — 3
12f
30 fps
0s·0f
1s·30f

all starts everything at the current cursor. sequence offsets each start by the stagger and finishes when the last one does.

actor.runtime

The same primitives are available inside actor methods as this.runtime, plus two that only make sense from inside an actor.

class Panel extends LayoutActor {
  *enter() {
    yield* this.runtime.all(this.header.enter(), this.body.enter());
  }

  *startPolling() {
    // fire-and-forget: does not consume authored time
    this.runtime.fork(function* (this: Panel) {
      yield* this.refresh();
    });

    // repeats until the actor is despawned
    this.runtime.loop(function* (this: Panel) {
      yield* this.pulse();
      yield* this.runtime.wait(2);
    });
  }
}
HelperConsumes authored time
all, sequence, wait, scopeyes
fork, loopno

createComposition

Like createScene, plus a ctx.stage surface for composition-level concerns. It returns a CompiledComposition, which extends CompiledScene — everything on the compiled scene page applies.

const comp = createComposition(function* (ctx) {
  const slide = ctx.spawn(Slide);
  yield* ctx.wait(3);
  return { slide };
});

Scene exports

Return the actors callers need. This is the supported way to reach into a scene: typed, refactor-safe, and free of string lookups.

return { card, caption: card.caption };
const { card, caption } = compiled.at(frame).scene;

Use findActor / getActor only for actors that were not exported.

On this page