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

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): void
Set 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.

Was this page helpful?