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

reactiveSignal

One nested value, written leaf by leaf or as a whole.

A reactiveSignal holds one nested value, such as a playback model or a list of markers. Every leaf is a signal of its own with set, tween and transition, so separate parts of the value can move on separate schedules, and the actor still has one field to read.

import { class Actor<TEvents extends ActorEventMap = {}>Actor, function reactiveSignal<T>(initial: T): ReactiveField<T>reactiveSignal } from "@motionactor/core";

export class class TimelineTimeline extends class Actor<TEvents extends ActorEventMap = {}>Actor {
  
Timeline.playback: ReactiveField<{
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}>
playback
=
reactiveSignal<{
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}>(initial: {
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}): ReactiveField<{
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}>
reactiveSignal
({
time: numbertime: 0,
selection: {
    start: number;
    end: number;
}
selection
: { start: numberstart: 0, end: numberend: 0 },
markers: {
    at: number;
    label: string;
}[]
markers
: [{ at: numberat: 1, label: stringlabel: "Beat" }],
}); }
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
} from "@motionactor/core";
import { class TimelineTimeline } from "./timeline"; const
const compiled: CompiledScene<{
    timeline: Timeline;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
=
createScene<{
    timeline: Timeline;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
    timeline: Timeline;
}>, 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 timeline: Timelinetimeline = ctx: SceneContextctx.
CompileContext.spawn<Timeline>(definition: ActorClass<Timeline>, overrides?: {
    playback?: {
        time: number;
        selection: {
            start: number;
            end: number;
        };
        markers: {
            at: number;
            label: string;
        }[];
    } | undefined;
    id?: undefined;
    cursor?: undefined;
    cursorSeconds?: undefined;
    sceneFps?: undefined;
    onInit?: undefined;
    onEnter?: undefined;
    onExit?: undefined;
    enter?: undefined;
    exit?: undefined;
    parent?: undefined;
} | undefined, options?: {
    id?: string;
} | undefined): Timeline (+1 overload)
spawn
(class TimelineTimeline);
const {
const playback: ReactiveField<{
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}>
playback
} = const timeline: Timelinetimeline;
const playback: ReactiveField<{
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}>
playback
.
markers: ReactiveArray<{
    at: number;
    label: string;
}>
markers
.
ReactiveCollectionMethods<{ at: number; label: string; }>.push(value: {
    at: number;
    label: string;
}): void
push
({ at: numberat: 2, label: stringlabel: "Drop" });
yield* ctx: SceneContextctx.CompileContext.all(...generators: Generator<Frame, unknown, unknown>[]): Generator<Frame, void, unknown>all(
const playback: ReactiveField<{
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}>
playback
.time: ReactiveScalar<number>time.ReactiveScalar<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>tween(4, 1),
const playback: ReactiveField<{
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}>
playback
.
selection: ReactiveObject<{
    start: number;
    end: number;
}>
selection
.end: ReactiveScalar<number>end.ReactiveScalar<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>tween(2, 1),
); return { timeline: Timelinetimeline }; }).
SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
    timeline: Timeline;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build
({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const compiled: CompiledScene<{
    timeline: Timeline;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
.
CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
    timeline: Timeline;
}>
at
(15).
FrameState<{ ...; }>.scene: {
    timeline: ResolvedActorOfInstance<Timeline> | undefined;
}
scene
.timeline: ResolvedActorOfInstance<Timeline> | undefinedtimeline?.
playback: {
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
} | undefined
playback
;
// { time: 2, selection: { start: 0, end: 1 }, // markers: [{ at: 1, label: "Beat" }, // { at: 2, label: "Drop" }] }

The value

The initial value fixes the shape. It may hold plain objects, arrays, numbers, strings, booleans and null. Anything else, such as a Date, a Map or a class instance, throws when it is declared or written.

A leaf is a number, string, boolean or null, and an array that holds only numbers counts as one leaf, so [x, y] tweens as a point. Leaves are called to read: playback.time(). An object or array node is not callable; read it with peek(), or read its leaves.

Writing

Node Methods
Leaf set, tween, transition, as on a signal
Object set, transition
Array set, transition, push, replaceAt, removeAt, and [i] for an element

playback.markers[0].label.set("Intro") writes one leaf of one element.

A node’s transition takes a function of a draft, the eased progress and the start value. Change the draft in place, or return a new value:

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
} from "@motionactor/core";
import { class TimelineTimeline } from "./timeline"; const
const compiled: CompiledScene<{
    timeline: Timeline;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
=
createScene<{
    timeline: Timeline;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
    timeline: Timeline;
}>, 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 timeline: Timelinetimeline = ctx: SceneContextctx.
CompileContext.spawn<Timeline>(definition: ActorClass<Timeline>, overrides?: {
    playback?: {
        time: number;
        selection: {
            start: number;
            end: number;
        };
        markers: {
            at: number;
            label: string;
        }[];
    } | undefined;
    id?: undefined;
    cursor?: undefined;
    cursorSeconds?: undefined;
    sceneFps?: undefined;
    onInit?: undefined;
    onEnter?: undefined;
    onExit?: undefined;
    enter?: undefined;
    exit?: undefined;
    parent?: undefined;
} | undefined, options?: {
    id?: string;
} | undefined): Timeline (+1 overload)
spawn
(class TimelineTimeline);
yield* const timeline: Timelinetimeline.
Timeline.playback: ReactiveField<{
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}>
playback
.
ReactiveStructuredNode<{ time: number; selection: { start: number; end: number; }; markers: { at: number; label: string; }[]; }>.transition(fn: (draft: {
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}, progress: number, from: {
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}) => void | {
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}, options: number | SignalTransitionOptions<{
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}>): Generator<Frame, void, unknown>
transition
((
draft: {
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}
draft
, progress: numberprogress) => {
draft: {
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}
draft
.time: numbertime = 4 * progress: numberprogress;
draft: {
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}
draft
.
selection: {
    start: number;
    end: number;
}
selection
.end: numberend = 4 * progress: numberprogress;
}, 1); return { timeline: Timelinetimeline }; }).
SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
    timeline: Timeline;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build
({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const compiled: CompiledScene<{
    timeline: Timeline;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
.
CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
    timeline: Timeline;
}>
at
(15).
FrameState<{ ...; }>.scene: {
    timeline: ResolvedActorOfInstance<Timeline> | undefined;
}
scene
.timeline: ResolvedActorOfInstance<Timeline> | undefinedtimeline?.
playback: {
    time: number;
    selection: {
        start: number;
        end: number;
    };
    markers: {
        at: number;
        label: string;
    }[];
}
playback
.
selection: {
    start: number;
    end: number;
} | undefined
selection
; // { start: 0, end: 2 }

Use a node’s transition when several leaves change as one step. It is one write, so the leaves cannot drift apart.

Writes in the same frame

Writes that start on the same frame compose when their paths are disjoint, as time and selection.end do above. Two animated writes whose paths overlap have no single answer: the same leaf twice, or a node and a leaf inside it, like selection and selection.end. The second write throws while the scene compiles, with an error that names both paths:

Conflicting reactive signal writes for Timeline-1:playback at frame 0:
time conflicts with time

A write that starts on a later frame takes over the path from that frame.

When to use one

A flat value is a signal. A list that only ever changes as a whole is also a signal, written with transition. Reach for reactiveSignal when leaves of one value animate separately. A set of unrelated values under one name is a group of signals, which each stay a spawn input.

A reactiveSignal field takes a plain value as a spawn input. A derived input throws.

Was this page helpful?