---
title: Compiled scenes
description: Turning a frame number into state, finding actors in it, caching it and reflecting it for editors.
---

A compiled scene is the record a [scene script](/docs/runtime/scene-orchestration) wrote: what
its actors did and when. `scene` holds what the script returned: the actor objects it
authored with, which after the build only identify their actors. A write or a lifecycle call
on one throws, and its build owns it, so a renderer binds it only against that build
(`actor.<tag> of={…}`). `at(frame)` replays the record to the frame you ask for and returns a
`FrameState`, whose `state.scene` holds snapshots of the same actors at that frame: their
values, but no handle.

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

export class Title extends LayoutActor {
  static type = "title";
  text = signal("");
}

export const compiled = createScene(function* (ctx) {
  const title = ctx.spawn(
    Title,
    { text: "Launch day", appearance: { opacity: 0 } },
    { id: "title" },
  );
  yield* title.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1);
  return { title };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

compiled.fps; // 30
compiled.totalDuration; // 45

const state = compiled.at(30);
state.scene.title?.text; // "Launch day"
state.camera.zoom; // 1
```

| `CompiledScene` | |
| --- | --- |
| `fps` | Frames per second the scene compiled at |
| `totalDuration` | Length in frames |
| `at(frame, options?)` | The `FrameState` at that frame |
| `scene` | What the script returned, as the build's actor handles: page markup binds one ([page bindings](/docs/rendering/renderers#page-bindings)) |
| `layers` | One handle per declared layer, typed by its backend: page markup addresses one with `InLayer` |
| `layersOf(scene)` | The handles on a staged scene's layers, by the definition staged, at any depth |
| `placements` | Every staged scene at any depth, stager first: `path`, `parentPath`, `depth`, `startFrame`, `endFrame` |
| `reflect(frame?, options?)` | The frame's actors described for an editor |
| `events`, `operations` | The raw record, for tools |
| `cells` | The build's publications, for a host: `subscribe(fn)` and `version()` move when one changes, `resetRun()` starts a new run |
| `[Symbol.dispose]()` | Releases the build; see [below](#releasing-a-build) |

A frame past the end resolves to the final state, so a host that overshoots shows the last
frame. A frame before 0 resolves with no actors.

## Frame state

| `FrameState` | |
| --- | --- |
| `frame` | The frame it resolves |
| `scene` | The scene's exports, each actor resolved |
| `actors` | Every live actor, as a `Map` from id |
| `layers` | The frame's layers in paint order |
| `camera` | `centerX`, `centerY`, `zoom`, `rotation`, `originX`, `originY` |

An actor that has not spawned yet, or has despawned, is absent from `actors` and resolves to
`undefined` in `scene`.

`camera` comes from the scene's camera actor, such as [`Camera2D`](/docs/actors/camera2d), and
is identity when there is none.

### Looking up actors

The scene's exports are the typed way in. For an actor the scene did not return, look it up
by class. The class carries the type, so the result needs no cast, and a subclass instance
matches too:

```ts twoslash
// @filename: title.scene.ts
import { createScene, LayoutActor, signal } from "@motionactor/core";
export class Title extends LayoutActor {
  static type = "title";
  text = signal("");
}
export const compiled = createScene(function* (ctx) {
  const title = ctx.spawn(Title, { text: "Launch day", appearance: { opacity: 0 } }, { id: "title" });
  yield* title.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1);
  return { title };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
// @filename: lookup.ts
// ---cut---
import { compiled, Title } from "./title.scene";

const state = compiled.at(30);

state.findActor(Title)?.text; // first match
state.findActors(Title); // every match
state.getActor("title", Title); // by id, if it is a Title
state.actor("title"); // by id, untyped
state.findActorByType("title"); // by static type, untyped
```

`findActorByType` matches the `static type` of the class and its parents. It is for code that
has a type name but not the class, such as an inspector.

## Resolved actors

Every lookup and export resolves to an actor's state at the frame. Its fields appear under
their own names, typed by the class, beside these:

| Key | Holds |
| --- | --- |
| `id`, `type` | The actor's id and its class's `static type` |
| `parentId`, `slotName` | Its host and the slot it is in; `null` for a root actor |
| `slotChildren` | The ids in each of its slots, in hosted order |
| `localLayout`, `worldLayout` | Its box relative to its parent and to the stage |
| `compositing` | Opacity, blend mode, isolation and clip, ready for a backend |
| `effects` | The effects active on it |
| `layer` | The id of the layer it paints in: its key, or `key#2` when the build declares the key twice; `null` for the default |
| `spawnFrame`, `despawnFrame` | When it entered and left |
| `signals` | Every field's value by name, channels included |
| `channels` | A handle per [channel](/docs/reactivity/channel): its value when called, `write`, `unpublish` and demand |

## Values from the host

Percentages resolve against the viewport the scene was built for; a new size is a new build.
A value the host measures goes into a
[channel](/docs/reactivity/channel), written through the resolved actor's handle:

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

class Label extends LayoutActor {
  text = signal("");
  available = channel({ default: 0 });
  fits = derived(() => this.text().length * 30 <= this.available());
}

const compiled = createScene(function* (ctx) {
  const label = ctx.spawn(Label, { text: "Launch day" }, { id: "label" });
  yield* ctx.wait(1);
  return { label };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

compiled.at(0).scene.label?.fits; // false
compiled.at(0).scene.label?.channels.available.write(600);
compiled.at(0).scene.label?.fits; // true
```

The publication applies to every later call until it is replaced or removed.

## Caching

`at` keeps each actor's resolved values and revisits only the actors whose values can have
changed: a timeline they read moves, a channel they read is published, or an effect on them is
active. An actor that holds is the same object in the next frame, and a repeat of the last
frame with nothing due is the same `FrameState`. `frame.resolution` counts the actors visited.
`at(frame, { cache: false })` resolves afresh and keeps nothing.

```ts twoslash
// @filename: title.scene.ts
import { createScene, LayoutActor, signal } from "@motionactor/core";
export class Title extends LayoutActor {
  static type = "title";
  text = signal("");
}
export const compiled = createScene(function* (ctx) {
  const title = ctx.spawn(Title, { text: "Launch day", appearance: { opacity: 0 } }, { id: "title" });
  yield* title.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1);
  return { title };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
// @filename: cache.ts
// ---cut---
import { compiled } from "./title.scene";

compiled.at(10) === compiled.at(10); // true
compiled.at(11).resolution; // { visited: 1, actors: 1 }
compiled.at(10, { cache: false }) === compiled.at(10); // false
```

## Reflection

`reflect(frame)` describes the frame's actors for an editor that holds no classes: each one's
fields with their current values, its groups and its commands.

```ts twoslash
// @filename: title.scene.ts
import { createScene, LayoutActor, signal } from "@motionactor/core";
export class Title extends LayoutActor {
  static type = "title";
  text = signal("");
}
export const compiled = createScene(function* (ctx) {
  const title = ctx.spawn(Title, { text: "Launch day", appearance: { opacity: 0 } }, { id: "title" });
  yield* title.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1);
  return { title };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
// @filename: inspect.ts
// ---cut---
import { compiled } from "./title.scene";

const title = compiled.reflect(15).getActor("title");
title?.groups.map((group) => group.key);
// ["appearance", "compositing", "flexItem", "gridItem",
//  "layout", "transform", "transitionState"]
title?.fields.find((field) => field.path === "text")?.currentValue;
// "Launch day"
```

`reflectActor(Class, overrides)` reflects one actor without a scene, and
`reflectBlueprint(Class)` reflects a class before anything spawns it. Commands and their
parameters come from [`@command`](/docs/actors/command).

## Releasing a build

A compiled scene owns its cells: their values, publications and recorded frames. Disposing it
releases them:

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

class Card extends LayoutActor {}

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

After that, `at` throws `This compiled scene was disposed: build it again to resolve its
frames.`, and a write through any of its handles throws. Outside a block, call
`compiled[Symbol.dispose]()`. A [composition build](/docs/rendering/compositions) is disposed the
same way.
