channel
A value the host publishes while frames play, such as a measured width, read when a frame resolves.
A channel holds a value the scene cannot author: the width a browser gives a label, a value a
host measures while the scene plays. An actor declares it and computes from it in a
derived:
import { function channel<TValue>(options: {
readonly default: TValue;
readonly record?: boolean;
readonly equals?: (previous: TValue, next: TValue) => boolean;
}): ChannelDefinition<TValue> & {
readonly hasDefault: true;
readonly default: TValue;
} (+1 overload)
A channel is a writable, scope-aware cell carrying an actor-channel brand.
Declared as an actor field it lives in the spawning build's store (keyed
`${actorId}:${name}` by the spawn walk, like a signal); declared free (in
`buildScene`, a script, or module top) it keys by its scope, in its build's
store or the module's, and is shared by reference. Either way resolution
reads it live.
The static return type stays `ChannelDefinition<TValue>` so the
`isChannelDefinition` predicate is unchanged; at runtime the handle is a
live cell.channel, function derived<TThis, T>(fn: (this: TThis) => T): DerivedDefinition<T, TThis>derived, 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 } from "@motionactor/core";
export class class MeterMeter extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Meter.level: ActorFieldSignal<number>level = 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.5);
Meter.trackWidth: ChannelDefinition<number> & {
readonly hasDefault: true;
readonly default: number;
}
trackWidth = channel<number>(options: {
readonly default: number;
readonly record?: boolean;
readonly equals?: ((previous: number, next: number) => boolean) | undefined;
}): ChannelDefinition<number> & {
readonly hasDefault: true;
readonly default: number;
} (+1 overload)
A channel is a writable, scope-aware cell carrying an actor-channel brand.
Declared as an actor field it lives in the spawning build's store (keyed
`${actorId}:${name}` by the spawn walk, like a signal); declared free (in
`buildScene`, a script, or module top) it keys by its scope, in its build's
store or the module's, and is shared by reference. Either way resolution
reads it live.
The static return type stays `ChannelDefinition<TValue>` so the
`isChannelDefinition` predicate is unchanged; at runtime the handle is a
live cell.channel({ default: numberdefault: 400 });
Meter.fillWidth: DerivedDefinition<number, unknown>fillWidth = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => this.Meter.level: () => numberlevel() * this.Meter.trackWidth: () => numbertrackWidth());
}
The host writes it through the resolved actor’s handle, and the same compiled frame resolves to the 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 MeterMeter } from "./meter";
const const compiled: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled = createScene<{
meter: Meter;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
meter: Meter;
}>, 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 meter: Metermeter = ctx: SceneContextctx.CompileContext.spawn<Meter>(definition: ActorClass<Meter>, overrides?: {
id?: undefined;
measuredBox?: undefined;
level?: SignalInputValue<number> | undefined;
trackWidth?: undefined;
fillWidth?: undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
height?: Readonly<...> | ... 1 more ... | undefined;
} | undefined;
... 19 more ...;
parent?: undefined;
} | undefined, options?: {
...;
} | undefined): Meter (+1 overload)
spawn(class MeterMeter, {}, { id?: string | undefinedid: "meter" });
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(1);
return { meter: Metermeter };
}).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const compiled: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
meter: Meter;
}>
at(0).FrameState<{ ...; }>.scene: {
meter: ResolvedActorOfInstance<Meter> | undefined;
}
scene.meter: ResolvedActorOfInstance<Meter> | undefinedmeter?.fillWidth: number | undefinedfillWidth; // 200
const const trackWidth: ResolvedChannelHandle<number> | undefinedtrackWidth = const compiled: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
meter: Meter;
}>
at(0).FrameState<{ ...; }>.scene: {
meter: ResolvedActorOfInstance<Meter> | undefined;
}
scene.meter: ResolvedActorOfInstance<Meter> | undefinedmeter?.channels: ActorClassChannels<Omit<Meter, keyof Actor<any>>>channels.trackWidth: ResolvedChannelHandle<number> | undefinedtrackWidth;
const trackWidth: ResolvedChannelHandle<number> | undefinedtrackWidth?.ResolvedChannelHandle<number>.write(value: number, frame?: number): voidPublish `value`. `frame` is the frame the producer's scene host committed;
a `record: true` channel files the value under it, so a later seek reads
the value observed at that frame. Without one the write is live only.write(800);
const compiled: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
meter: Meter;
}>
at(0).FrameState<{ ...; }>.scene: {
meter: ResolvedActorOfInstance<Meter> | undefined;
}
scene.meter: ResolvedActorOfInstance<Meter> | undefinedmeter?.fillWidth: number | undefinedfillWidth; // 400
const trackWidth: ResolvedChannelHandle<number> | undefinedtrackWidth?.ResolvedChannelHandle<number>.unpublish(): voidRetract the live publication: the producer unmounted or stopped. A recorded channel keeps its run log.unpublish();
const compiled: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
meter: Meter;
}>
at(0).FrameState<{ ...; }>.scene: {
meter: ResolvedActorOfInstance<Meter> | undefined;
}
scene.meter: ResolvedActorOfInstance<Meter> | undefinedmeter?.fillWidth: number | undefinedfillWidth; // 200
The handle is the only way to write a channel. The scene script never writes one: write
inside a generator throws, because a script authors the timeline and a channel has none.
Publishing from a renderer
A renderer reads channel values from actor.signals, like any other field. The component it
returns publishes with useChannelPublication(handle, produce):
import {
function channel<TValue>(options: {
readonly default: TValue;
readonly record?: boolean;
readonly equals?: (previous: TValue, next: TValue) => boolean;
}): ChannelDefinition<TValue> & {
readonly hasDefault: true;
readonly default: TValue;
} (+1 overload)
A channel is a writable, scope-aware cell carrying an actor-channel brand.
Declared as an actor field it lives in the spawning build's store (keyed
`${actorId}:${name}` by the spawn walk, like a signal); declared free (in
`buildScene`, a script, or module top) it keys by its scope, in its build's
store or the module's, and is shared by reference. Either way resolution
reads it live.
The static return type stays `ChannelDefinition<TValue>` so the
`isChannelDefinition` predicate is unchanged; at runtime the handle is a
live cell.channel,
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,
type interface ResolvedChannelHandle<T>Handle to a channel cell on a resolved actor (`actor.channels.<name>`), and
the only way a binding reaches the channel: read (call or `peek`), publish
(`write`), retract (`unpublish`), and the demand a gated producer follows.
A renderer reads the resolved value from `actor.signals.<name>`. Timeline
authoring (set/tween/play/animate/transition) is not available on resolved
handles — author against the live actor inside a scene generator.ResolvedChannelHandle,
} from "@motionactor/core";
import { const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom } from "@motionactor/dom/react";
import { function useChannelPublication<T>(handle: PublishableChannel<T> | null | undefined, produce: () => T | null, options?: ChannelPublicationOptions<T>): voidPublish a renderer-produced value into a channel after every commit and
retract it on unmount. The single backend-agnostic publish tail shared by
measured-box publication and cross-renderer surface producers — the producer
only supplies the handle and a `produce()` that reads post-commit state.
Under a scene host the publication also runs on every commit of the host,
at the frame it committed, whether or not the caller rendered.useChannelPublication } from "@motionactor/renderers/react";
import { function useRef<T>(initialValue: T): React.RefObject<T> (+2 overloads)`useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
(`initialValue`). The returned object will persist for the full lifetime of the component.
Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
value around similar to how you’d use instance fields in classes.useRef } from "react";
export class class LabelLabel extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Label.text: ActorFieldSignal<string>text = 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("");
Label.textWidth: ChannelDefinition<number> & {
readonly hasDefault: true;
readonly default: number;
}
textWidth = channel<number>(options: {
readonly default: number;
readonly record?: boolean;
readonly equals?: ((previous: number, next: number) => boolean) | undefined;
}): ChannelDefinition<number> & {
readonly hasDefault: true;
readonly default: number;
} (+1 overload)
A channel is a writable, scope-aware cell carrying an actor-channel brand.
Declared as an actor field it lives in the spawning build's store (keyed
`${actorId}:${name}` by the spawn walk, like a signal); declared free (in
`buildScene`, a script, or module top) it keys by its scope, in its build's
store or the module's, and is shared by reference. Either way resolution
reads it live.
The static return type stays `ChannelDefinition<TValue>` so the
`isChannelDefinition` predicate is unchanged; at runtime the handle is a
live cell.channel({ default: numberdefault: 0, record?: boolean | undefinedrecord: true });
}
function function LabelText(props: {
text: string;
width: ResolvedChannelHandle<number>;
}): JSX.Element
LabelText(props: {
text: string;
width: ResolvedChannelHandle<number>;
}
props: { text: stringtext: string; width: ResolvedChannelHandle<number>width: interface ResolvedChannelHandle<T>Handle to a channel cell on a resolved actor (`actor.channels.<name>`), and
the only way a binding reaches the channel: read (call or `peek`), publish
(`write`), retract (`unpublish`), and the demand a gated producer follows.
A renderer reads the resolved value from `actor.signals.<name>`. Timeline
authoring (set/tween/play/animate/transition) is not available on resolved
handles — author against the live actor inside a scene generator.ResolvedChannelHandle<number> }) {
const const ref: React.RefObject<HTMLSpanElement | null>ref = useRef<HTMLSpanElement>(initialValue: HTMLSpanElement | null): React.RefObject<HTMLSpanElement | null> (+2 overloads)`useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
(`initialValue`). The returned object will persist for the full lifetime of the component.
Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
value around similar to how you’d use instance fields in classes.useRef<HTMLSpanElement>(null);
useChannelPublication<number>(handle: PublishableChannel<number> | null | undefined, produce: () => number | null, options?: ChannelPublicationOptions<number> | undefined): voidPublish a renderer-produced value into a channel after every commit and
retract it on unmount. The single backend-agnostic publish tail shared by
measured-box publication and cross-renderer surface producers — the producer
only supplies the handle and a `produce()` that reads post-commit state.
Under a scene host the publication also runs on every commit of the host,
at the frame it committed, whether or not the caller rendered.useChannelPublication(props: {
text: string;
width: ResolvedChannelHandle<number>;
}
props.width: ResolvedChannelHandle<number>width, () => const ref: React.RefObject<HTMLSpanElement | null>ref.React.RefObject<HTMLSpanElement | null>.current: HTMLSpanElement | nullThe current value of the ref.current?.HTMLElement.offsetWidth: number | undefinedThe **`offsetWidth`** read-only property of the HTMLElement interface returns the layout width of an element as an integer.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/offsetWidth)offsetWidth ?? null);
return <React.JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>span React.RefAttributes<HTMLSpanElement>.ref?: React.Ref<HTMLSpanElement> | undefinedAllows getting a ref to the component instance.
Once the component unmounts, React will set `ref.current` to `null`
(or call the ref with `null` if you passed a callback ref).ref={const ref: React.RefObject<HTMLSpanElement | null>ref}>{props: {
text: string;
width: ResolvedChannelHandle<number>;
}
props.text: stringtext}</React.JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>span>;
}
export const const label: BackendRendererDefinition<readonly [typeof Label], "dom">label = const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom.render: <typeof Label>(blueprint: typeof Label, render: ActorRendererForBlueprints<readonly [typeof Label], "dom">, options?: ActorRendererRegistrationOptions) => BackendRendererDefinition<readonly [typeof Label], "dom"> (+1 overload)render(class LabelLabel, ({ actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 7 more ...;
textWidth: number | undefined;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor }) => (
<function LabelText(props: {
text: string;
width: ResolvedChannelHandle<number>;
}): JSX.Element
LabelText text: stringtext={actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 7 more ...;
textWidth: number | undefined;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor.signals: Readonly<Record<string, unknown>> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 7 more ...;
textWidth: number | undefined;
}
signals.text: stringtext} width: ResolvedChannelHandle<number>width={actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 7 more ...;
textWidth: number | undefined;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor.channels: Readonly<Record<string, ResolvedChannelHandle<any>>> & ActorClassChannels<Omit<Label, keyof Actor<any>>>channels.textWidth: ResolvedChannelHandle<number>textWidth} />
));
After each commit it calls produce and writes the result with the frame the scene renderer
committed; null skips the commit. It removes the publication when the component unmounts.
The renderer function itself calls no hooks, so the publication lives in the component.
The Vue and Svelte bindings export their own useChannelPublication.
Declaring
| Declaration | Before anything is published |
|---|---|
channel({ default }) |
Reads return the default |
channel<T>() |
The resolved actor leaves the field out, and a read throws |
Had trackWidth no default, reading it before a publication would throw
Channel "meter:trackWidth" cannot be read before publication for frame 0. Declare a
default when a frame can resolve before the producer has run, as on the first paint.
Two options change how publications are kept. record: true keeps a value per frame,
described below. equals(previous, next) decides when a write is no
change: an equal write leaves the value and the publication version alone, so cached frames
stay valid. The measured box on every LayoutActor uses it to ignore movement under half a
pixel.
Where a channel lives
A channel lives where it is declared:
| Declared | Lives in | Shared by |
|---|---|---|
| As an actor field | The actor, in the build that spawned it | Everything holding the actor |
| In a scene or composition script | That build | Everything in the build holding the reference |
| At module scope | The module | Every build that reads it |
Every compile is its own build. Two builds of one definition each get their own channel, even for actors with the same id, and writing one leaves the other alone:
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 MeterMeter } from "./meter";
const const gauge: SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
meter: Meter;
}>
gauge = createScene<{
meter: Meter;
}, {}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
meter: Meter;
}>, options?: SceneDefinitionOptions<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>> | undefined): SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
meter: Meter;
}>
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 meter: Metermeter = ctx: SceneContextctx.CompileContext.spawn<Meter>(definition: ActorClass<Meter>, overrides?: {
id?: undefined;
measuredBox?: undefined;
level?: SignalInputValue<number> | undefined;
trackWidth?: undefined;
fillWidth?: undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
height?: Readonly<...> | ... 1 more ... | undefined;
} | undefined;
... 19 more ...;
parent?: undefined;
} | undefined, options?: {
...;
} | undefined): Meter (+1 overload)
spawn(class MeterMeter, {}, { id?: string | undefinedid: "meter" });
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(1);
return { meter: Metermeter };
},
{ SceneDefinitionOptions<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>.inputs?: {} | undefinedinputs: {} },
);
const const viewport: {
width: number;
height: number;
}
viewport = { width: numberwidth: 1280, height: numberheight: 720 };
const const left: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
left = const gauge: SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
meter: Meter;
}>
gauge.SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { meter: Meter; }>.build(args: SceneBuildInput<{}>): CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ inputs?: HostInputArgs<{}> | undefinedinputs: {}, viewport: SceneViewportviewport, fps: numberfps: 30 });
const const right: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
right = const gauge: SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
meter: Meter;
}>
gauge.SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { meter: Meter; }>.build(args: SceneBuildInput<{}>): CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ inputs?: HostInputArgs<{}> | undefinedinputs: {}, viewport: SceneViewportviewport, fps: numberfps: 30 });
const left: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
left.CompiledScene<{ meter: Meter; }, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
meter: Meter;
}>
at(0).FrameState<{ meter: Meter; }>.scene: {
meter: ResolvedActorOfInstance<Meter> | undefined;
}
scene.meter: ResolvedActorOfInstance<Meter> | undefinedmeter?.channels: ActorClassChannels<Omit<Meter, keyof Actor<any>>>channels.trackWidth: ResolvedChannelHandle<number>trackWidth.ResolvedChannelHandle<number>.write(value: number, frame?: number): voidPublish `value`. `frame` is the frame the producer's scene host committed;
a `record: true` channel files the value under it, so a later seek reads
the value observed at that frame. Without one the write is live only.write(800);
const left: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
left.CompiledScene<{ meter: Meter; }, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
meter: Meter;
}>
at(0).FrameState<{ meter: Meter; }>.scene: {
meter: ResolvedActorOfInstance<Meter> | undefined;
}
scene.meter: ResolvedActorOfInstance<Meter> | undefinedmeter?.fillWidth: number | undefinedfillWidth; // 400
const right: CompiledScene<{
meter: Meter;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
right.CompiledScene<{ meter: Meter; }, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
meter: Meter;
}>
at(0).FrameState<{ meter: Meter; }>.scene: {
meter: ResolvedActorOfInstance<Meter> | undefined;
}
scene.meter: ResolvedActorOfInstance<Meter> | undefinedmeter?.fillWidth: number | undefinedfillWidth; // 200
A module channel is the one value every build reads. A compiled scene reads it live, so a write after the compile reaches every frame resolved after it:
import { function channel<TValue>(options: {
readonly default: TValue;
readonly record?: boolean;
readonly equals?: (previous: TValue, next: TValue) => boolean;
}): ChannelDefinition<TValue> & {
readonly hasDefault: true;
readonly default: TValue;
} (+1 overload)
A channel is a writable, scope-aware cell carrying an actor-channel brand.
Declared as an actor field it lives in the spawning build's store (keyed
`${actorId}:${name}` by the spawn walk, like a signal); declared free (in
`buildScene`, a script, or module top) it keys by its scope, in its build's
store or the module's, and is shared by reference. Either way resolution
reads it live.
The static return type stays `ChannelDefinition<TValue>` so the
`isChannelDefinition` predicate is unchanged; at runtime the handle is a
live cell.channel, 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, function derived<TThis, T>(fn: (this: TThis) => T): DerivedDefinition<T, TThis>derived, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor } from "@motionactor/core";
const const accent: ChannelDefinition<number> & {
readonly hasDefault: true;
readonly default: number;
}
accent = channel<number>(options: {
readonly default: number;
readonly record?: boolean;
readonly equals?: ((previous: number, next: number) => boolean) | undefined;
}): ChannelDefinition<number> & {
readonly hasDefault: true;
readonly default: number;
} (+1 overload)
A channel is a writable, scope-aware cell carrying an actor-channel brand.
Declared as an actor field it lives in the spawning build's store (keyed
`${actorId}:${name}` by the spawn walk, like a signal); declared free (in
`buildScene`, a script, or module top) it keys by its scope, in its build's
store or the module's, and is shared by reference. Either way resolution
reads it live.
The static return type stays `ChannelDefinition<TValue>` so the
`isChannelDefinition` predicate is unchanged; at runtime the handle is a
live cell.channel({ default: numberdefault: 1 });
class class BarBar extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Bar.glow: DerivedDefinition<number, unknown>glow = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => const accent: () => numberaccent() * 2);
}
const const compiled: CompiledScene<{
bar: Bar;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled = createScene<{
bar: Bar;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
bar: Bar;
}>, 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 bar: Barbar = ctx: SceneContextctx.CompileContext.spawn<Bar>(definition: ActorClass<Bar>, overrides?: {
measuredBox?: undefined;
glow?: undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
height?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<...> | undefined;
} | undefined;
... 20 more ...;
parent?: undefined;
} | undefined, options?: {
...;
} | undefined): Bar (+1 overload)
spawn(class BarBar);
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(1);
return { bar: Barbar };
}).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
bar: Bar;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const compiled: CompiledScene<{
bar: Bar;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
bar: Bar;
}>
at(0).FrameState<{ ...; }>.scene: {
bar: ResolvedActorOfInstance<Bar> | undefined;
}
scene.bar: ResolvedActorOfInstance<Bar> | undefinedbar?.glow: number | undefinedglow; // 2
const accent: ChannelDefinition<number> & {
readonly hasDefault: true;
readonly default: number;
}
accent.ChannelDefinition<number>.write(value: number, frame?: number): voidPublish `value`; a `record` channel files it under `frame`, the frame its host committed.write(5);
const compiled: CompiledScene<{
bar: Bar;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
bar: Bar;
}>
at(0).FrameState<{ ...; }>.scene: {
bar: ResolvedActorOfInstance<Bar> | undefined;
}
scene.bar: ResolvedActorOfInstance<Bar> | undefinedbar?.glow: number | undefinedglow; // 10
A build’s channels go with it. Once it is disposed, with using or [Symbol.dispose](), its
handles throw on write and at throws; see
Compiled scenes.
Which value a read gets
A read takes the first of these that has a value:
- for a recorded channel, the value recorded at the latest frame at or before this one,
- the latest publication,
- the declared default.
With none of them, the read throws. An actor’s measuredBox is the one exception, described
below.
Recorded channels
A channel without record holds one value, the latest. That suits a pointer or a sensor,
which are live. It is wrong for a value that depends on the frame, such as a measured box
that changes as an actor animates: after scrubbing back, every frame would read the box from
the last frame painted.
record: true keeps a log. A write with a frame, handle.write(value, frame), is stored at
that frame, and a read at frame N returns the value at the latest recorded frame at or before
N. useChannelPublication passes the committed frame for you. Publishing at the same frame
again replaces the entry. A write without a frame only updates the latest value, which reads
fall back to before the first recorded frame.
Demand
Producing a value can be expensive. Measuring the DOM forces a layout. A producer can publish
only while something reads the channel, and reading is what asks for it: a derived that
calls one of the build’s channels while a frame resolves, or a call to
actor.channels.<name>(). The request stays until the run is reset. The runtime’s own reads,
such as layout checking for a measured box, do not ask, and neither does reading a module
channel: nothing would clear its request.
| Call | Side |
|---|---|
actor.channels.<name>.hasDemand() |
Producer: whether a read asked for the value this run |
actor.channels.<name>.subscribeDemand(fn) |
Producer: called when demand starts and when it stops |
Measured boxes
Every LayoutActor declares a recorded channel, measuredBox, exported as MEASURED_BOX:
the box a DOM renderer measured for the actor. Layout uses it in place of the authored box
for an actor placed by CSS flow.
A derived can read it before anything is measured. The read asks the DOM backend to
measure, and until the box is published it returns the box layout computes: the actor’s x,
y, width and height against its parent’s box, or its layer’s.
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, function derived<TThis, T>(fn: (this: TThis) => T): DerivedDefinition<T, TThis>derived, 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 } from "@motionactor/core";
class class WaveformWaveform extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Waveform.level: ActorFieldSignal<number>level = 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.5);
Waveform.bar: DerivedDefinition<number, unknown>bar = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => this.Waveform.level: () => numberlevel() * this.LayoutActor<{}>[MEASURED_BOX]: () => RectLayout uses the channel literally named `MEASURED_BOX` to source a
measured-box override for this actor's local layout. The field name must
match the `MEASURED_BOX` constant.
Half-pixel equality: the renderer republishes this box on every commit, and
`getBoundingClientRect` jitters below a pixel under a scaled ancestor. The
tolerance is declared here, once, so no producer has to carry it.measuredBox().Rect.height: numberheight);
}
const const compiled: CompiledScene<{
wave: Waveform;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled = createScene<{
wave: Waveform;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
wave: Waveform;
}>, 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 wave: Waveformwave = ctx: SceneContextctx.CompileContext.spawn<Waveform>(definition: ActorClass<Waveform>, overrides?: {
measuredBox?: undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
height?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<...> | undefined;
} | undefined;
... 22 more ...;
parent?: undefined;
} | undefined, options?: {
...;
} | undefined): Waveform (+1 overload)
spawn(class WaveformWaveform, { layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
height?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
} | undefined
layout: { width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined
width: 200, height?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined
height: 80 } });
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(1);
return { wave: Waveformwave };
}).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
wave: Waveform;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const const wave: ResolvedActorOfInstance<Waveform> | undefinedwave = const compiled: CompiledScene<{
wave: Waveform;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
wave: Waveform;
}>
at(0).FrameState<{ ...; }>.scene: {
wave: ResolvedActorOfInstance<Waveform> | undefined;
}
scene.wave: ResolvedActorOfInstance<Waveform> | undefinedwave;
const wave: ResolvedActorOfInstance<Waveform> | undefinedwave?.bar: number | undefinedbar; // 40, from the layout box
const wave: ResolvedActorOfInstance<Waveform> | undefinedwave?.channels: ActorClassChannels<Omit<Waveform, keyof Actor<any>>>channels.[MEASURED_BOX]: ResolvedChannelHandle<Rect>Layout uses the channel literally named `MEASURED_BOX` to source a
measured-box override for this actor's local layout. The field name must
match the `MEASURED_BOX` constant.
Half-pixel equality: the renderer republishes this box on every commit, and
`getBoundingClientRect` jitters below a pixel under a scaled ancestor. The
tolerance is declared here, once, so no producer has to carry it.measuredBox.ResolvedChannelHandle<Rect>.hasDemand(): booleanWhether a read asked for the value this run: a scene's read during
resolution or a call to this handle. Sticky until the run resets.hasDemand(); // true: the DOM backend measures it
The script runs before anything is measured, so reading measuredBox there throws. Read it
in a derived.
Channels and watches
A watch runs while the scene compiles, before anything is published, so
a watch whose source reads a channel throws. Compute from a channel in a
derived, which runs at every resolve.