Skip to content
MotionActor
Esc
↑↓navigate↵open⌘Jpreview
On this page

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(...) — 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);
    });
  }
}
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.

Was this page helpful?