---
title: Actor
description: The base class. An actor owns state, the commands that change it over time, and hooks for entering and leaving a scene.
---

An actor is a class whose fields are [signals](/docs/reactivity/signal) and whose methods are
generators. The methods are commands: a scene calls them while it compiles, and every tween
inside them is recorded at the cursor. `Actor` has no box. Anything drawn on the stage
extends [`LayoutActor`](/docs/actors/layout-actor); extend `Actor` for a clock, a camera, a sound
manager or a controller.

```ts twoslash title="counter.ts"
import { Actor, Easing, signal } from "@motionactor/core";

export class Counter extends Actor {
  static type = "counter";

  count = signal(0);

  *countTo({ target, duration = 1 }: {
    target: number;
    duration?: number;
  }) {
    yield* this.count.tween(target, {
      duration,
      easing: Easing.outCubic,
    });
  }

  reset() {
    this.count.set(0);
  }
}
```

A command that `yield*`s a tween takes that tween's time. A plain method that only calls `set`
takes none: the value changes at the cursor. Give a command more than one number as an
object, as `countTo` does. Positional numbers read as `countTo(240, 2)` at the call site and
are easy to swap.

`static type` names the class in reflection, in `findActorByType` and to editors. Without it
the class name is used.

## On every instance

| Member | |
| --- | --- |
| `id` | Set at spawn: the `id` option, or a generated one |
| `cursor`, `cursorSeconds` | The scene's cursor while it compiles |
| `sceneFps` | The frame rate of the build the actor runs in |
| `runtime` | Timing verbs, as on `ctx`, plus `fork` and `loop`. See [Inside an actor](/docs/runtime/scene-orchestration#inside-an-actor) |
| `parent(Class?)` | The actor's host, if it has one. See [Children](/docs/runtime/children#reading-the-owner) |
| `enter(params?)`, `exit(params?)` | Run the lifecycle hooks below |
| `emit` | Raises the actor's events |
| `fx` | Installed effects by name: `fx.glow(params?, playback?)`, and `fx.for(backend)` for a name two backends define. See [Effects](/docs/rendering/effects#call-by-name) |

They work only while the actor is in a scene. On an instance made with `new`, outside a
scene script, the methods throw and the cursor fields are `undefined`.

## Lifecycle

| Hook | Runs | Takes time |
| --- | --- | --- |
| `onInit()` | At spawn, after the overrides are applied and children exist | No, it is synchronous |
| `*onEnter(params)` | When something calls `enter(params)` | Yes |
| `*onExit(params)` | When something calls `exit(params)` | Yes |

`onInit` is where an actor fills its own [slots](/docs/runtime/slots) from its inputs. Spawning
does not run `onEnter`. Call `enter()`, or spawn with the `enter` option, when the entrance
should play.

`exit()` runs the actor's `onExit` and, at the same time, the `onExit` of every actor it
hosts. The whole subtree despawns when the longest of them ends:

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

class Caption extends LayoutActor {
  *onExit() {
    yield* this.appearance.opacity.tween(0, 1);
  }
}

class Card extends LayoutActor {
  caption = child(Caption);

  *onEnter() {
    yield* this.appearance.opacity.tween(1, 0.5);
  }

  *onExit() {
    yield* this.layout.y.tween(40, 0.5);
  }
}

const compiled = createScene(function* (ctx) {
  const card = ctx.spawn(Card, { appearance: { opacity: 0 } });
  yield* card.enter();
  ctx.cursor; // 15
  yield* card.exit();
  ctx.cursor; // 45: the caption's second outlasts the card's half
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

compiled.at(44).actors.size; // 2
compiled.at(45).actors.size; // 0
```

A host paces its children's exits by calling their `exit()` inside its own `onExit`, before
that hook first waits. Each child's `onExit` then runs when the host runs the generator
`exit()` returned; the children it does not call start together once the hook first waits.
Awaiting a child's exit resumes when that child's `onExit` ends. The child stays in the scene
until the whole subtree despawns.

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

class Caption extends LayoutActor {
  *onExit() {
    yield* this.appearance.opacity.tween(0, 0.5);
  }
}

class Panel extends LayoutActor {
  *onExit() {
    yield* this.layout.y.tween(40, 0.5);
  }
}

class Card extends Panel {
  caption = child(Caption);

  *onExit() {
    const caption = this.caption.exit(); // before the first wait
    yield* super.onExit();
    yield* caption; // the caption fades after the card slides
  }
}
```

Calling `exit()` on a child that is already exiting on its own, after the hook first waited,
throws and names the child. So does calling a child's `exit()` and never running it.

## Events

An actor declares the events it raises as a type parameter. `emit.<name>(payload)` is a
generator that records the event at the cursor, and actors that
[subscribe](/docs/reactivity/events-and-subscribe) to it run their handlers there.

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

class Timer extends Actor<{ finished: { at: number } }> {
  remaining = signal(3);

  *run() {
    yield* this.remaining.tween(0, 3);
    yield* this.emit.finished({ at: this.cursorSeconds });
  }
}
```

## Drawn from the frame

`static frameDriven = true` marks a class whose renderer draws from the frame number rather
than from the actor's state, such as a shader keyed to time. Export then never treats two of
its frames as the same picture. See [Capture and export](/docs/rendering/capture-and-export#export).
