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

Actor

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 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; extend Actor for a clock, a camera, a sound manager or a controller.

import { class Actor<TEvents extends ActorEventMap = {}>Actor, 
const Easing: {
    readonly linear: EasingFunction;
    readonly inQuad: EasingFunction;
    readonly outQuad: EasingFunction;
    readonly inOutQuad: EasingFunction;
    readonly inCubic: EasingFunction;
    readonly outCubic: EasingFunction;
    readonly inOutCubic: EasingFunction;
    readonly inQuart: EasingFunction;
    readonly outQuart: EasingFunction;
    readonly inOutQuart: EasingFunction;
    readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
    readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
    readonly in: (easing: EasingFunction) => EasingFunction;
    readonly out: (easing: EasingFunction) => EasingFunction;
    readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing
,
function signal<T>(): RequiredSignal<T> (+1 overload)
module signal
A timeline value on an actor, and an input to every spawn of it. Without an initial value the signal is required: `ctx.spawn` must supply it.
signal
} from "@motionactor/core";
export class class CounterCounter extends class Actor<TEvents extends ActorEventMap = {}>Actor { static Counter.type: stringtype = "counter"; Counter.count: ActorFieldSignal<number>count = signal<number>(initial: number): ActorFieldSignal<number> (+1 overload)
A timeline value on an actor, and an input to every spawn of it. Without an initial value the signal is required: `ctx.spawn` must supply it.
signal
(0);
*
Counter.countTo({ target, duration }: {
    target: number;
    duration?: number;
}): Generator<number, void, unknown>
countTo
({ target: numbertarget, duration: numberduration = 1 }: {
target: numbertarget: number; duration?: number | undefinedduration?: number; }) { yield* this.Counter.count: ActorFieldSignal<number>count.LiveSignal<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(target: numbertarget, {
SignalTweenOptions<number>.duration: numberduration, SignalTweenOptions<number>.easing?: EasingFunction | undefinedeasing:
const Easing: {
    readonly linear: EasingFunction;
    readonly inQuad: EasingFunction;
    readonly outQuad: EasingFunction;
    readonly inOutQuad: EasingFunction;
    readonly inCubic: EasingFunction;
    readonly outCubic: EasingFunction;
    readonly inOutCubic: EasingFunction;
    readonly inQuart: EasingFunction;
    readonly outQuart: EasingFunction;
    readonly inOutQuart: EasingFunction;
    readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
    readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
    readonly in: (easing: EasingFunction) => EasingFunction;
    readonly out: (easing: EasingFunction) => EasingFunction;
    readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing
.outCubic: EasingFunctionoutCubic,
}); } Counter.reset(): voidreset() { this.Counter.count: ActorFieldSignal<number>count.LiveSignal<number>.set(value: number): void
Set value instantly at current cursor (no yield needed)
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
parent(Class?) The actor’s host, if it has one. See Children
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

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 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:

import { function child<TBlueprint extends ActorClass<object>, TOwner extends object = object>(blueprint: TBlueprint, ...[overrides]: ChildOverridesArg<TBlueprint, TOwner>): ChildField<TBlueprint, TOwner>child, function createScene<TSceneExports, const I extends SceneInputs = Readonly<Record<never, SceneInput>>, const L extends SceneLayers = Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<I, L, TSceneExports>, options?: SceneDefinitionOptions<I, L>): SceneDefinition<I, L, TSceneExports>
Declare a scene. Nothing compiles until a host builds it: `scene.build({ viewport, fps, inputs })` runs the script as `(ctx, inputs, viewport, layers)` and returns the compiled scene. `ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn` places it in a declared one. `ctx.stage` stages another scene on this scene's clock, and a staged scene may stage again.
createScene
, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor } from "@motionactor/core";
class class CaptionCaption extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { *Caption.onExit(): Generator<number, void, unknown>onExit() { yield* this.
LayoutActor<{}>.appearance: {
    opacity: TypedFieldSignal<number, number>;
} & SignalGroupDefinition
appearance
.opacity: TypedFieldSignal<number, number>opacity.function tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(0, 1);
} } class class CardCard extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { Card.caption: ChildField<typeof Caption, object>caption = child<typeof Caption, object>(blueprint: typeof Caption, overrides?: ChildOverrides<typeof Caption, object> | undefined): ChildField<typeof Caption, object>child(class CaptionCaption); *Card.onEnter(): Generator<number, void, unknown>onEnter() { yield* this.
LayoutActor<{}>.appearance: {
    opacity: TypedFieldSignal<number, number>;
} & SignalGroupDefinition
appearance
.opacity: TypedFieldSignal<number, number>opacity.function tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(1, 0.5);
} *Card.onExit(): Generator<number, void, unknown>onExit() { yield* this.
LayoutActor<{}>.layout: {
    x: ActorFieldSignal<LayoutValue>;
    y: ActorFieldSignal<LayoutValue>;
    width: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
    height: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
} & SignalGroupDefinition
layout
.y: ActorFieldSignal<LayoutValue>y.LiveSignal<LayoutValue>.tween(target: LayoutValue, options: number | SignalTweenOptions<LayoutValue>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(40, 0.5);
} } const const compiled: CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>compiled = createScene<void, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>, options?: SceneDefinitionOptions<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>> | undefined): SceneDefinition<...>
Declare a scene. Nothing compiles until a host builds it: `scene.build({ viewport, fps, inputs })` runs the script as `(ctx, inputs, viewport, layers)` and returns the compiled scene. `ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn` places it in a declared one. `ctx.stage` stages another scene on this scene's clock, and a staged scene may stage again.
createScene
(function* (ctx: SceneContextctx) {
const const card: Cardcard = ctx: SceneContextctx.
CompileContext.spawn<Card>(definition: ActorClass<Card>, overrides?: {
    measuredBox?: undefined;
    transform?: {
        scale?: SignalInputValue<number> | undefined;
        rotation?: SignalInputValue<number> | undefined;
    } | undefined;
    id?: undefined;
    onExit?: undefined;
    layout?: {
        x?: SignalInputValue<LayoutValue> | undefined;
        y?: SignalInputValue<LayoutValue> | undefined;
        width?: Readonly<{
            kind: "layout.percent";
            value: number;
        }> | SignalInputValue<number | `${number}%` | Readonly<...>> | undefined;
        height?: Readonly<...> | ... 1 more ... | undefined;
    } | undefined;
    ... 18 more ...;
    caption?: {
        ...;
    } | undefined;
} | undefined, options?: {
    ...;
} | undefined): Card (+1 overload)
spawn
(class CardCard, {
appearance?: {
    opacity?: SignalInputValue<number> | undefined;
} | undefined
appearance
: { opacity?: SignalInputValue<number> | undefinedopacity: 0 } });
yield* const card: Cardcard.Actor<{}>.enter(_params?: unknown): Generator<Frame, void, unknown>enter(); ctx: SceneContextctx.CompileContext.cursor: numbercursor; // 15 yield* const card: Cardcard.Actor<{}>.exit(_params?: unknown): Generator<Frame, void, unknown>exit(); ctx: SceneContextctx.CompileContext.cursor: numbercursor; // 45: the caption's second outlasts the card's half }).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 }); const compiled: CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>compiled.CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<void>at(44).FrameState<void>.actors: ReadonlyMap<string, ResolvedActor>actors.ReadonlyMap<string, ResolvedActor>.size: numbersize; // 2 const compiled: CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>compiled.CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<void>at(45).FrameState<void>.actors: ReadonlyMap<string, ResolvedActor>actors.ReadonlyMap<string, ResolvedActor>.size: numbersize; // 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.

import { function child<TBlueprint extends ActorClass<object>, TOwner extends object = object>(blueprint: TBlueprint, ...[overrides]: ChildOverridesArg<TBlueprint, TOwner>): ChildField<TBlueprint, TOwner>child, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor } from "@motionactor/core";

class class CaptionCaption extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
  *Caption.onExit(): Generator<number, void, unknown>onExit() {
    yield* this.
LayoutActor<{}>.appearance: {
    opacity: TypedFieldSignal<number, number>;
} & SignalGroupDefinition
appearance
.opacity: TypedFieldSignal<number, number>opacity.function tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(0, 0.5);
} } class class PanelPanel extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { *Panel.onExit(): Generator<number, void, unknown>onExit() { yield* this.
LayoutActor<{}>.layout: {
    x: ActorFieldSignal<LayoutValue>;
    y: ActorFieldSignal<LayoutValue>;
    width: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
    height: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
} & SignalGroupDefinition
layout
.y: ActorFieldSignal<LayoutValue>y.LiveSignal<LayoutValue>.tween(target: LayoutValue, options: number | SignalTweenOptions<LayoutValue>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(40, 0.5);
} } class class CardCard extends class PanelPanel { Card.caption: ChildField<typeof Caption, object>caption = child<typeof Caption, object>(blueprint: typeof Caption, overrides?: ChildOverrides<typeof Caption, object> | undefined): ChildField<typeof Caption, object>child(class CaptionCaption); *Card.onExit(): Generator<number, void, unknown>onExit() { const const caption: Generator<number, void, unknown>caption = this.Card.caption: ChildField<typeof Caption, object>caption.Actor<{}>.exit(_params?: unknown): Generator<Frame, void, unknown>exit(); // before the first wait yield* super.Panel.onExit(): Generator<number, void, unknown>onExit(); yield* const caption: Generator<number, void, unknown>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 to it run their handlers there.

import { class Actor<TEvents extends ActorEventMap = {}>Actor, 
function signal<T>(): RequiredSignal<T> (+1 overload)
module signal
A timeline value on an actor, and an input to every spawn of it. Without an initial value the signal is required: `ctx.spawn` must supply it.
signal
} from "@motionactor/core";
class class TimerTimer extends class Actor<TEvents extends ActorEventMap = {}>Actor<{
finished: {
    at: number;
}
finished
: { at: numberat: number } }> {
Timer.remaining: ActorFieldSignal<number>remaining = signal<number>(initial: number): ActorFieldSignal<number> (+1 overload)
A timeline value on an actor, and an input to every spawn of it. Without an initial value the signal is required: `ctx.spawn` must supply it.
signal
(3);
*Timer.run(): Generator<number, void, unknown>run() { yield* this.Timer.remaining: ActorFieldSignal<number>remaining.LiveSignal<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(0, 3);
yield* this.
Actor<{ finished: { at: number; }; }>.emit: ActorEmitter<{
    finished: {
        at: number;
    };
}>
emit
.
finished: (payload: {
    at: number;
}) => Generator<Frame, void, unknown>
finished
({ at: numberat: this.Actor<{ finished: { at: number; }; }>.cursorSeconds: number
Compile cursor expressed in authored seconds (cursor / scene fps).
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.

Was this page helpful?