---
title: Scene orchestration
description: A scene script runs once, moving a cursor through time while it spawns actors and records their motion.
mode: wide
---

A scene is a generator function. `createScene` declares it; each `scene.build({ viewport, fps })`
runs it once, and every verb it calls writes to a timeline record at the current position of a cursor. A
`yield*` moves the cursor forward. Nothing here draws: a host later asks the compiled scene
for a frame and gets back the state the record describes.

```ts twoslash title="card.scene.ts"
import { createScene, LayoutActor, signal } from "@motionactor/core";

class Card extends LayoutActor {
  label = signal("");
}

export const compiled = createScene(function* (ctx) {
  const card = ctx.spawn(Card, {
    label: "Hello",
    appearance: { opacity: 0 },
  });

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

  return { card };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

compiled.totalDuration; // 54
compiled.at(9).scene.card?.appearance.opacity; // 1
```

Durations are seconds. A build converts them to frames at the `fps` its host passes, rounding
each one to the nearest frame. Built at 30 fps, 0.3 s is 9 frames, the wait is 30 and the slide is 15, so the scene is 54 frames long.

## The cursor

`ctx.cursor` is the cursor in frames and `ctx.cursorSeconds` the same position in seconds.
Every timing verb is a generator that starts its branches at the cursor and moves it on when
you `yield*` it:

| Verb | Branches start | Cursor moves by |
| --- | --- | --- |
| `wait(s)` | | `s` |
| `all(...branches)` | All at the cursor | The longest branch |
| `sequence(d, ...branches)` | Branch `i` at `i × d` | Until the last branch ends |
| `chain(...branches)` | Each when the one before ends | The sum |
| `delay(d, branch)` | After `d` | `d` plus the branch |

```ts twoslash
import { createScene, LayoutActor } from "@motionactor/core";

class Dot extends LayoutActor {}

createScene(function* (ctx) {
  const dots = [0, 1, 2].map(() => ctx.spawn(Dot));

  yield* ctx.all(...dots.map((d) => d.layout.y.tween(100, 0.5)));
  ctx.cursor; // 15

  yield* ctx.sequence(
    0.1,
    ...dots.map((d) => d.layout.y.tween(0, 0.5)),
  );
  ctx.cursor; // 36: the third starts at 21 and ends at 36

  yield* ctx.chain(...dots.map((d) => d.layout.x.tween(50, 0.2)));
  ctx.cursor; // 54

  yield* ctx.delay(0.5, dots[0].layout.x.tween(0, 1));
  ctx.cursor; // 99
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

A branch is any generator: a tween, an actor method, another verb. Nesting them is how a
scene builds a timeline.

<StaggerScene />

Five bars raised with `ctx.all`, dropped, then raised again with `ctx.sequence(0.08, …)`.
The track beside `raise` shows both: five starts on one frame under `all`, a staircase under
`sequence`.

### Scopes

`ctx.scope(fn)` runs `fn` with a child context and moves the cursor as `fn` does. Actors
spawned through the child despawn when it ends, so a scope suits something that exists for
one beat of the scene. It returns what `fn` returns:

```ts twoslash
import { createScene, LayoutActor } from "@motionactor/core";

class Flash extends LayoutActor {}

createScene(function* (ctx) {
  const peak = yield* ctx.scope(function* (beat) {
    const flash = beat.spawn(Flash);
    yield* flash.appearance.opacity.tween(0, 1);
    return beat.cursor;
  });

  peak; // 30
  ctx.cursor; // 30, and the flash has despawned
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

## Spawning

`ctx.spawn(Class, overrides, { id })` creates an actor at the cursor and returns it at once,
taking no time. Overrides set its initial values and are required when the class declares a
field with no default. Without an `id` the actor gets a generated one such as `Card-2`.

Give the options an `enter` and spawn returns a generator instead: it spawns, then runs the
actor's [`onEnter`](/docs/actors/actor#lifecycle) with `enter` as its params, and returns the
actor when that finishes.

```ts twoslash
import { createScene, Easing, LayoutActor } from "@motionactor/core";

class Card extends LayoutActor {
  *onEnter({ rise = 40 }: { rise?: number } = {}) {
    this.layout.y.set(rise);
    yield* this.runtime.all(
      this.layout.y.tween(0, { duration: 0.5, easing: Easing.outCubic }),
      this.appearance.opacity.tween(1, 0.5),
    );
  }
}

createScene(function* (ctx) {
  const card = yield* ctx.spawn(
    Card,
    { appearance: { opacity: 0 } },
    { id: "card", enter: { rise: 80 } },
  );
  ctx.cursor; // 15
  return { card };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

## Inside an actor

An actor method is a generator too, and `this.runtime` gives it the same `all`, `chain`,
`sequence`, `delay` and `wait`. They work while the actor is in a scene; called on an actor
that is not, they throw "Actor instances can only coordinate generators while attached to a
scene."

The runtime adds two verbs that start work without moving anyone's cursor:

| Verb | Runs | Ends |
| --- | --- | --- |
| `fork(fn)` | `fn` once, from the cursor | When `fn` returns |
| `loop(fn)` | `fn` again each time it returns | When the scene ends |
| `loop({ iterations }, fn)` | `fn` that many times | After the last time |

Both take an optional name first. Starting a fork or a loop under a name the actor already
runs replaces the old one: it starts nothing new, though a tween already under way plays to
its end.

```ts twoslash
import { createScene, LayoutActor } from "@motionactor/core";

class Beacon extends LayoutActor {
  *beat() {
    yield* this.appearance.opacity.tween(0.4, 0.5);
    yield* this.appearance.opacity.tween(1, 0.5);
  }

  calm() {
    this.runtime.loop("pulse", function* (this: Beacon) {
      yield* this.beat();
      yield* this.runtime.wait(1);
    });
  }

  alarm() {
    this.runtime.loop("pulse", function* (this: Beacon) {
      yield* this.beat();
    });
  }
}

createScene(function* (ctx) {
  const beacon = ctx.spawn(Beacon);
  beacon.calm();
  yield* ctx.wait(4);
  beacon.alarm();
  yield* ctx.wait(3);
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

A scene lasts until its script returns or until its last finite fork or loop ends, whichever
is later. An endless loop never lengthens it.

## Reads across branches

The script, every branch of `all` and `sequence`, every fork, loop and generator handler
step together in frame order. A read sees every write made at an earlier frame, whichever
branch made it. At a frame two branches share, work a branch started steps before that
branch, so the script reads what its forks and handlers wrote at that frame. A tween takes
its start value the same way, at the frame its branch reaches it.

```ts twoslash
import { createScene, LayoutActor, signal } from "@motionactor/core";

class Counter extends LayoutActor {
  count = signal(0);
}

createScene(function* (ctx) {
  const counter = ctx.spawn(Counter);

  yield* ctx.all(
    (function* () {
      yield* ctx.wait(1);
      counter.count.set(counter.count() + 1); // 11: the other branch wrote 10 at frame 15
    })(),
    (function* () {
      yield* ctx.wait(0.5);
      counter.count.set(10);
    })(),
  );

  counter.runtime.fork(function* () {
    yield* counter.runtime.wait(1);
    counter.count.set(20);
  });
  yield* ctx.wait(1);
  counter.count(); // 20: the fork wrote at frame 60 and stepped first
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

## Inputs and layers

Give `createScene` `inputs` or `layers` to declare what a host supplies. The script
receives the input values, the viewport and the layer handles after `ctx`, and compiles
each time a host calls `build`:

```ts twoslash title="poster.scene.ts"
import { createScene, input, LayoutActor, signal } from "@motionactor/core";
import { image, text } from "@motionactor/schema";

class Poster extends LayoutActor {
  source = signal.of(image);
  caption = signal("");
}

export const poster = createScene(
  function* (ctx, { hero, title }, { width }) {
    const card = ctx.spawn(Poster, {
      source: hero,
      caption: title,
      layout: { x: width * 0.1 },
    });
    yield* ctx.wait(2);
    return { card };
  },
  {
    inputs: {
      hero: input.of(image),
      title: input.of(text, "Launch day"),
    },
  },
);

const compiled = poster.build({
  viewport: { width: 1920, height: 1080 },
  inputs: { hero: image.file("hero.jpg") },
  fps: 30,
});
compiled.at(0).scene.card?.layout.x; // 192
```

`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 that is missing or wrong.
An input is a plain value: a different one is a new `build`.
`reflectSceneInputs(poster)` lists the declared inputs for an editor. Layers are on
[Backends and layers](/docs/rendering/backends-and-layers#layers).

## Staging scenes

Every scene's `ctx` has `stage`, which runs a scene as one part of a longer timeline.
`stage` takes a script, or a definition with its inputs, and returns the scene's exports:

```ts twoslash
import {
  createScene,
  input,
  LayoutActor,
  signal,
} from "@motionactor/core";
import { text } from "@motionactor/schema";

class Title extends LayoutActor {
  text = signal("");
}

const intro = createScene(
  function* (ctx, { title }) {
    ctx.spawn(Title, { text: title });
    yield* ctx.wait(1);
  },
  { inputs: { title: input.of(text, "Hi") } },
);

const film = createScene(function* (ctx) {
  yield* ctx.stage(intro, { id: "intro", inputs: { title: "Hello" } });
  yield* ctx.stage(
    function* (scene) {
      yield* scene.wait(2);
    },
    { id: "outro" },
  );
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

film.totalDuration; // 90
film.placements.map((p) => [p.sceneId, p.startFrame, p.endFrame]);
// [["intro", 0, 30], ["outro", 30, 90]]
```

Each stage runs in its stager's scope, so its actors leave when it ends. Actors spawned on the
stager's own `ctx` stay for its whole timeline, which is where a music bed goes. Two scenes one
stager stages cannot overlap: staging two under `ctx.all` throws, naming both and their frames.

A staged definition runs on the built scene's clock, and its length in seconds holds.

A staged scene stages in turn. `placements` lists every staged scene at any depth, stager
first: each has a `path` (`"a/leaf"`), its stager's `parentPath` (`null` at the top) and its
`depth` (1 at the top). A definition already on stage above itself throws, and so does a second
stage with one `path`; give each stage under one stager its own `id`.

```ts twoslash
import { createScene } from "@motionactor/core";
// ---cut---
const leaf = createScene(function* (ctx) {
  yield* ctx.wait(1);
}, { inputs: {} });
const chapter = createScene(function* (ctx) {
  yield* ctx.wait(0.5);
  yield* ctx.stage(leaf, { id: "leaf" });
}, { inputs: {} });
const book = createScene(function* (ctx) {
  yield* ctx.stage(chapter, { id: "a" });
  yield* ctx.stage(chapter, { id: "b" });
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

book.placements.map((p) => [p.path, p.depth, p.startFrame, p.endFrame]);
// [["a", 1, 0, 45], ["a/leaf", 2, 15, 45], ["b", 1, 45, 90], ["b/leaf", 2, 60, 90]]
```

The host can pass the scenes to stage as well: a definition's `build` takes `scenes`, and
its script reads them from `ctx.scenes`. Only the built scene lists them; a staged scene's
`ctx.scenes` is empty.

```ts twoslash
import { createScene, input } from "@motionactor/core";
import { Audio } from "@motionactor/media";
import { audio } from "@motionactor/schema";

const intro = createScene(function* (ctx) {
  yield* ctx.wait(1);
}, { inputs: {} });
const outro = intro;
const theme = audio.file("theme.mp3");
// ---cut---
const video = createScene(
  function* (ctx, { music }) {
    const bed = ctx.spawn(Audio, {
      content: { source: music },
      volume: 0.5,
      fade: { in: 2, out: 3 },
    });
    bed.loop();
    for (const scene of ctx.scenes) yield* ctx.stage(scene);
  },
  { inputs: { music: input.of(audio) } },
);

video.build({
  viewport: { width: 1920, height: 1080 },
  inputs: { music: theme },
  scenes: [
    { id: "intro", definition: intro },
    { id: "outro", definition: outro },
  ],
  fps: 30,
});
```

### Actor inputs

`input.of(Blueprint)` declares an input that is an actor. The scene that stages the
definition passes one of its own actors, and the script receives that actor: its fields stay
live, so whatever the stager does to it reaches the staged scene, across cuts.

```ts twoslash
import {
  channel,
  createScene,
  derived,
  input,
  LayoutActor,
  signal,
} from "@motionactor/core";
import { text } from "@motionactor/schema";

class Narrator extends LayoutActor {
  volume = signal(1);
  loudness = channel({ default: 0 });
}

class Meter extends LayoutActor {
  level = signal(0);
}

const outro = createScene(
  function* (ctx, { title, narrator }) {
    ctx.spawn(Meter, { level: derived(() => narrator.loudness() * 2) });
    ctx.spawn(Meter, { level: narrator.volume });
    yield* ctx.wait(1);
    return { title };
  },
  { inputs: { title: input.of(text, "Outro"), narrator: input.of(Narrator) } },
);

const video = createScene(function* (ctx) {
  const narrator = ctx.spawn(Narrator);
  narrator.volume.animate("fade", { to: 0, duration: 2 });
  yield* ctx.stage(outro, { inputs: { narrator, title: "One" } });
  yield* ctx.stage(outro, { inputs: { narrator, title: "Two" } });
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

- `ctx.stage` checks the actor: one of another blueprint, a value that is not a spawned actor,
  or an actor of another build throws, naming the input. A subclass of the blueprint passes.
- An actor input takes no default and has no host form. A host has no actor to pass, so
  `build` on such a definition is a type error and throws `scene: input "narrator" takes a
  Narrator actor, which only a stager can pass, at ctx.stage`.
- `reflectSceneInputs(outro)` reports it as `{ name: "narrator", actor: "Narrator", required: true }`:
  an `actor` instead of a `type`, so an editor shows no form for it.

## Exports

What the script returns is the scene's public surface. Each actor in it resolves, at every
frame, to that actor's state, typed by its class:

```ts twoslash
import { createScene, LayoutActor, signal } from "@motionactor/core";

class Card extends LayoutActor {
  label = signal("");
}

const compiled = createScene(function* (ctx) {
  const card = ctx.spawn(Card, { label: "Hello" });
  yield* ctx.wait(1);
  return { card };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
// ---cut---
const { card } = compiled.at(15).scene;
card?.label; // "Hello"
card?.layout.x; // 0
```

Return what callers need. A lookup by class or id, on
[Compiled scenes](/docs/runtime/compiled-scene#looking-up-actors), is for the actors you did not
return. `compiled.scene` holds the same exports as the live handles the script returned:
`compiled.scene.card` names the card in every frame, and page markup binds it
([page bindings](/docs/rendering/renderers#page-bindings)).
