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.