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): voidSet 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.