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.
Inputs
A scene can declare typed inputs that its host supplies, like Remotion’s input props. With
inputs, createScene returns a definition that compiles once per build, not on import:
const intro = createScene(
function* (ctx, { hero, title }, { width }) {
ctx.spawn(Poster, { source: hero, caption: title, layout: { x: width * 0.1 } });
yield* ctx.wait(2);
},
{ inputs: { hero: input.of(image), title: input.of(text, "Launch day") } },
);
const compiled = intro.build({ inputs: { hero }, viewport: { width: 1920, height: 1080 } });
input.of(type) is required and input.of(type, fallback) is not, as with signal.of.
build checks each value against its type and names the input when one is missing or
wrong. reflectSceneInputs(intro) lists the declared inputs for an editor.
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.
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(...) — actx.all(...) — bctx.sequence(0.05, …) — 1ctx.sequence(0.05, …) — 2ctx.sequence(0.05, …) — 3all 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);
});
}
}
| Helper | Consumes authored time |
|---|---|
all, sequence, wait, scope |
yes |
fork, loop |
no |
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 };
});
ctx.stage places scenes one after another on the composition’s timeline. It takes a
generator, or a createScene definition with its inputs:
yield* ctx.stage(intro, { id: "intro", inputs: { hero } });
With inputs in its options, createComposition returns a definition like createScene
does. A host builds it with a viewport, its inputs, and the scenes ctx.scenes lists. Actors
spawned on ctx live across every staged scene, so a music bed plays and fades over the
whole video:
const video = createComposition(
function* (ctx, { music }) {
ctx.spawn(Audio, { content: { source: music }, volume: 0.5, fade: { in: 2, out: 3 } }).loop();
for (const scene of ctx.scenes) yield* ctx.stage(scene);
},
{ fps: 30, inputs: { music: input.of(audio) } },
);
video.build({
viewport: { width: 1920, height: 1080 },
inputs: { music },
scenes: [{ id: "intro", definition: intro, inputs: { hero } }, { id: "outro", definition: outro }],
});
A staged definition runs on the composition’s clock: its own fps is ignored, and its length in
seconds holds.
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.