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

Events and subscribe

An actor announces what happened with a typed event; subscribe attaches another actor's response.

An actor emits an event to say what happened, without knowing who is listening. subscribe attaches a response from another actor. The emitter keeps no list of listeners, so the same actor can drive different responses in different scenes.

import { class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor } from "@motionactor/core";

type 
type ButtonEvents = {
    pressed: {
        via: "key" | "pointer";
    };
}
ButtonEvents
= {
pressed: {
    via: "key" | "pointer";
}
pressed
: { via: "key" | "pointer"via: "key" | "pointer" };
}; export class class ButtonButton extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor<
type ButtonEvents = {
    pressed: {
        via: "key" | "pointer";
    };
}
ButtonEvents
> {
*Button.press(via: "key" | "pointer"): Generator<number, void, unknown>press(via: "key" | "pointer"via: "key" | "pointer") { yield* this.
LayoutActor<ButtonEvents>.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.6, 0.1);
yield* this.Actor<ButtonEvents>.emit: ActorEmitter<ButtonEvents>emit.
pressed: (payload: {
    via: "key" | "pointer";
}) => Generator<Frame, void, unknown>
pressed
({ via: "key" | "pointer"via });
yield* this.
LayoutActor<ButtonEvents>.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.1);
} }

The type parameter of Actor or LayoutActor is the event map: each key is an event name and its value is the payload. this.emit is typed from it, so this.emit.pressed takes exactly { via } and an unknown event name does not compile.

import {
  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,
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
,
function subscribe<TSubscriber extends object, TEvents extends ActorEventMap>(subscriber: TSubscriber, source: {
    id: string;
    emit: ActorEmitter<TEvents>;
}, handlers: ActorEventHandlers<TSubscriber, TEvents>): void
subscribe
,
} from "@motionactor/core"; import { class ButtonButton } from "./button"; class class FormForm extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { Form.sentVia: ActorFieldSignal<string>sentVia = signal<string>(initial: string): ActorFieldSignal<string> (+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
("");
*Form.submit(via: string): Generator<number, void, unknown>submit(via: stringvia: string) { 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.5, 1);
this.Form.sentVia: ActorFieldSignal<string>sentVia.LiveSignal<string>.set(value: string): void
Set value instantly at current cursor (no yield needed)
set
(via: stringvia);
} } const
const compiled: CompiledScene<{
    form: Form;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
=
createScene<{
    form: Form;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
    form: Form;
}>, 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 button: Buttonbutton = ctx: SceneContextctx.
CompileContext.spawn<Button>(definition: ActorClass<Button>, overrides?: {
    measuredBox?: undefined;
    transform?: {
        scale?: SignalInputValue<number> | undefined;
        rotation?: SignalInputValue<number> | undefined;
    } | undefined;
    id?: 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;
    ... 19 more ...;
    press?: undefined;
} | undefined, options?: {
    ...;
} | undefined): Button (+1 overload)
spawn
(class ButtonButton);
const const form: Formform = ctx: SceneContextctx.
CompileContext.spawn<Form>(definition: ActorClass<Form>, overrides?: {
    measuredBox?: undefined;
    transform?: {
        scale?: SignalInputValue<number> | undefined;
        rotation?: SignalInputValue<number> | undefined;
    } | undefined;
    id?: undefined;
    sentVia?: SignalInputValue<string> | undefined;
    submit?: undefined;
    layout?: {
        x?: SignalInputValue<LayoutValue> | undefined;
        y?: SignalInputValue<LayoutValue> | undefined;
        width?: Readonly<{
            kind: "layout.percent";
            value: number;
        }> | SignalInputValue<...> | undefined;
        height?: Readonly<...> | ... 1 more ... | undefined;
    } | undefined;
    ... 18 more ...;
    parent?: undefined;
} | undefined, options?: {
    ...;
} | undefined): Form (+1 overload)
spawn
(class FormForm);
subscribe<Form, ButtonEvents>(subscriber: Form, source: {
    id: string;
    emit: ActorEmitter<ButtonEvents>;
}, handlers: ActorEventHandlers<Form, ButtonEvents>): void
subscribe
(const form: Formform, const button: Buttonbutton, {
*
pressed?: ((this: Form, payload: {
    via: "key" | "pointer";
}) => void | Generator<Frame, void, unknown>) | undefined
pressed
({ via: "key" | "pointer"via }) {
yield* this.Form.submit(via: string): Generator<number, void, unknown>submit(via: "key" | "pointer"via); }, }); yield* const button: Buttonbutton.Button.press(via: "key" | "pointer"): Generator<number, void, unknown>press("key"); return { form: Formform }; }).
SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
    form: Form;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build
({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const compiled: CompiledScene<{
    form: Form;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
.CompiledScene<{ ...; }, CompiledLayersOf<...>>.totalDuration: numbertotalDuration; // 33
const compiled: CompiledScene<{
    form: Form;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
.
CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
    form: Form;
}>
at
(33).
FrameState<{ ...; }>.scene: {
    form: ResolvedActorOfInstance<Form> | undefined;
}
scene
.form: ResolvedActorOfInstance<Form> | undefinedform?.sentVia: string | undefinedsentVia; // "key"

Emitting

yield* this.emit.name(payload) takes no time. It records the event at the cursor and starts every handler subscribed to it, then the emitter carries on. Each emit is also kept in compiled.events, as an actor-event entry with the actor, the name, the payload and the frame.

Subscribing

subscribe(subscriber, source, handlers) takes the actor that responds, the actor whose events it hears, and a handler per event. Handlers are optional per event, and inside one this is the subscriber.

A plain function handler runs at the emit, before the emitter continues. A generator handler runs on a branch of its own from the emit frame. The emitter does not wait for it: above, the button’s press is over at frame 6 while the form’s submit runs to frame 33, and the scene lasts until the last branch ends. At a frame the handler shares with the emitter, the handler steps first (reads across branches). A handler stops when its subscriber exits.

Call subscribe while the scene compiles: in the scene script, in a command, or in a host’s onInit to wire up its children. Anywhere else it throws Actor subscriptions can only be created while attached to a scene. A subscription hears only the events emitted after it was made.

Who changes what

The source states what happened and the subscriber decides what it means for its own state. A dock emits itemActivated, and the desktop that owns the windows opens one:

subscribe<Desktop, DockEvents>(subscriber: Desktop, source: {
    id: string;
    emit: ActorEmitter<DockEvents>;
}, handlers: ActorEventHandlers<Desktop, DockEvents>): void
subscribe
(const desktop: Desktopdesktop, const dock: Dockdock, {
*
itemActivated?: ((this: Desktop, payload: {
    appId: string;
}) => void | Generator<Frame, void, unknown>) | undefined
itemActivated
({ appId: stringappId }) {
yield* this.Desktop.openWindow(appId: string): Generator<number, void, unknown>openWindow(appId: stringappId); }, });

Had the dock set desktop.windows itself, it would also have to know every rule that comes with opening a window, such as focusing it and moving it to the front. With the event, those rules stay in Desktop, and a dock in a scene without a desktop still works.

Sound uses the same mechanism. Actors emit events and know nothing about audio, and the scene decides which events play which sounds with bindSounds, a subscription made by the audio manager.

Was this page helpful?