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

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): void
Publish `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(): void
Retract 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>): void
Publish 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.
@version16.8.0@see{@link https://react.dev/reference/react/useRef}
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.
@version16.8.0@see{@link https://react.dev/reference/react/useRef}
useRef
<HTMLSpanElement>(null);
useChannelPublication<number>(handle: PublishableChannel<number> | null | undefined, produce: () => number | null, options?: ChannelPublicationOptions<number> | undefined): void
Publish 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 | null
The current value of the ref.
current
?.HTMLElement.offsetWidth: number | undefined
The **`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> | undefined
Allows 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).
@see{@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}
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): void
Publish `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): void
Publish `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:

  1. for a recorded channel, the value recorded at the latest frame at or before this one,
  2. the latest publication,
  3. 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]: () => 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
().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(): boolean
Whether 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.

Was this page helpful?