Reactivity
Where each of an actor's values comes from, and the primitive that declares it.
An actor declares its values as fields, and the primitive behind each field says where the
value comes from. The scene writes signals. A derived is computed from other reads each
time a frame resolves. A channel is published from outside the scene while frames play,
such as the box a renderer measures. Events carry an actor’s intent to the
actors that subscribe, and a watch runs code when one of the actor’s own values changes.
import {
function derived<TThis, T>(fn: (this: TThis) => T): DerivedDefinition<T, TThis>derived,
function group<T extends Record<string, unknown>>(value: T): T & SignalGroupDefinitiongroup,
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 watch<TThis, TValue>(source: WatchSource<TValue, TThis>, handler: WatchHandler<TValue, TThis>): WatchDefinition<TValue, TThis>watch,
} from "@motionactor/core";
type type PlayerEvents = {
ended: {
at: number;
};
}
PlayerEvents = { ended: {
at: number;
}
ended: { at: numberat: number } };
export class class PlayerPlayer extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor<type PlayerEvents = {
ended: {
at: number;
};
}
PlayerEvents> {
// Written by the scene.
Player.time: ActorFieldSignal<number>time = 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);
Player.length: ActorFieldSignal<number>length = 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(10);
Player.controls: {
visible: ActorFieldSignal<boolean>;
} & SignalGroupDefinition
controls = group<{
visible: ActorFieldSignal<boolean>;
}>(value: {
visible: ActorFieldSignal<boolean>;
}): {
visible: ActorFieldSignal<boolean>;
} & SignalGroupDefinition
group({ visible: ActorFieldSignal<boolean>visible: signal<boolean>(initial: boolean): ActorFieldSignal<boolean> (+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(true) });
// Computed when a frame resolves.
Player.progress: DerivedDefinition<number, unknown>progress = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => this.Player.time: () => numbertime() / this.Player.length: () => numberlength());
Player.thumbX: DerivedDefinition<number, unknown>thumbX = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => this.Player.progress: DerivedDefinition
() => number
progress() * this.LayoutActor<PlayerEvents>.localLayout(): ResolvedLayoutBoxlocalLayout().Rect.width: numberwidth);
// Runs while the scene compiles.
Player.hideControls: WatchDefinition<boolean, unknown>hideControls = watch<unknown, boolean>(source: WatchSource<boolean, unknown>, handler: WatchHandler<boolean, unknown>): WatchDefinition<boolean, unknown>watch(
() => this.Player.time: () => numbertime() > 0,
(playing: booleanplaying) => this.Player.controls: {
visible: ActorFieldSignal<boolean>;
} & SignalGroupDefinition
controls.visible: ActorFieldSignal<boolean>visible.LiveSignal<boolean>.set(value: boolean): voidSet value instantly at current cursor (no yield needed)set(!playing: booleanplaying),
);
*Player.play(): Generator<number, void, unknown>play() {
yield* this.Player.time: ActorFieldSignal<number>time.LiveSignal<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(this.Player.length: () => numberlength(), this.Player.length: () => numberlength());
yield* this.Actor<PlayerEvents>.emit: ActorEmitter<PlayerEvents>emit.ended: (payload: {
at: number;
}) => Generator<Frame, void, unknown>
ended({ at: numberat: this.Player.time: () => numbertime() });
}
}
Choosing a primitive
Ask in order and stop at the first yes.
| Question | Primitive |
|---|---|
| Is it computed from other values and never set? | derived |
| Does the host publish it while frames play, such as a measured box? | channel |
| Is it one nested object whose leaves animate separately? | reactiveSignal |
| Does it change how the actor looks, such as a shake or a glow? | an effect |
| Does the scene set or animate it? | signal |
Two primitives hold no values. group names a set of fields, as
layout names x, y, width and height. Events
tell other actors that something happened.
A signal’s history is in the scene: every set and tween is recorded at a frame, and
seeking replays the record. A channel has no history in the scene, only what was published.
A value the scene cannot know, such as the width a browser lays a label out at, has to be a
channel. Modelled as a signal it would be fixed at compile time and wrong in any other
viewport.
Who writes a value
An actor writes its own signals. Another actor that wants a change emits an event and lets the owner decide what the change means:
subscribe<Video, ButtonEvents>(subscriber: Video, source: {
id: string;
emit: ActorEmitter<ButtonEvents>;
}, handlers: ActorEventHandlers<Video, ButtonEvents>): void
subscribe(const video: Videovideo, const button: Buttonbutton, {
*pressed?: ((this: Video, payload: {
via: "key" | "pointer";
}) => void | Generator<Frame, void, unknown>) | undefined
pressed() {
yield* this.Video.restart(): Generator<number, void, unknown>restart();
},
});
Video keeps every rule about its own time in restart. The button knows nothing about
videos, so the same button can restart a video in one scene and submit a form in another.