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

LayoutActor

The base class for anything drawn on the stage. It adds a box, a transform, opacity, compositing and a measured size.

LayoutActor extends Actor with the groups every drawn thing needs, so a class that extends it has a box and can fade, scale, rotate and blend without declaring any of that.

import { class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor, 
function layoutGroup(overrides?: LayoutGroupOverrides): {
    x: ActorFieldSignal<LayoutValue>;
    y: ActorFieldSignal<LayoutValue>;
    width: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
    height: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<...>>;
} & SignalGroupDefinition
The canonical `layout` group: x/y/width/height signals with defaults filled in. Subclasses of `LayoutActor` redeclare `layout = layoutGroup({...})` to override defaults without restating the untouched axes.
layoutGroup
,
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 BadgeBadge extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { Badge.label: ActorFieldSignal<string>label = 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
("Ready");
Badge.layout: {
    x: ActorFieldSignal<LayoutValue>;
    y: ActorFieldSignal<LayoutValue>;
    width: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
    height: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
} & SignalGroupDefinition
layout
=
function layoutGroup(overrides?: LayoutGroupOverrides): {
    x: ActorFieldSignal<LayoutValue>;
    y: ActorFieldSignal<LayoutValue>;
    width: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
    height: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<...>>;
} & SignalGroupDefinition
The canonical `layout` group: x/y/width/height signals with defaults filled in. Subclasses of `LayoutActor` redeclare `layout = layoutGroup({...})` to override defaults without restating the untouched axes.
layoutGroup
({ LayoutGroupOverrides.width?: LayoutValue | undefinedwidth: 160, LayoutGroupOverrides.height?: LayoutValue | undefinedheight: 48 });
}

Redeclare layout with layoutGroup to change the default size. Axes you leave out keep their defaults.

Groups

Group Fields Defaults
layout x, y, width, height 0
transform scale, rotation (degrees) 1, 0
appearance opacity 1
shape radius (px) 0
compositing blend, isolate, clip "source-over", false, null
flexItem alignSelf "auto"
gridItem gridColumn, gridRow null

Each field is a signal: read it, set it or tween it.

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
,
const Easing: {
    readonly linear: EasingFunction;
    readonly inQuad: EasingFunction;
    readonly outQuad: EasingFunction;
    readonly inOutQuad: EasingFunction;
    readonly inCubic: EasingFunction;
    readonly outCubic: EasingFunction;
    readonly inOutCubic: EasingFunction;
    readonly inQuart: EasingFunction;
    readonly outQuart: EasingFunction;
    readonly inOutQuart: EasingFunction;
    readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
    readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
    readonly in: (easing: EasingFunction) => EasingFunction;
    readonly out: (easing: EasingFunction) => EasingFunction;
    readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing
, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor } from "@motionactor/core";
class class BadgeBadge extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {} createScene<void, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>, 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 badge: Badgebadge = ctx: SceneContextctx.
CompileContext.spawn<Badge>(definition: ActorClass<Badge>, overrides?: {
    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;
    ... 21 more ...;
    parent?: undefined;
} | undefined, options?: {
    ...;
} | undefined): Badge (+1 overload)
spawn
(class BadgeBadge, {
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
: 160,
height?: Readonly<{
    kind: "layout.percent";
    value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
    kind: "layout.percent";
    value: number;
}>> | undefined
height
: 48 } });
yield* ctx: SceneContextctx.CompileContext.all(...generators: Generator<Frame, unknown, unknown>[]): Generator<Frame, void, unknown>all( const badge: Badgebadge.
LayoutActor<{}>.layout: {
    x: ActorFieldSignal<LayoutValue>;
    y: ActorFieldSignal<LayoutValue>;
    width: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
    height: TypedFieldSignal<number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>, number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>>;
} & SignalGroupDefinition
layout
.x: ActorFieldSignal<LayoutValue>x.LiveSignal<LayoutValue>.tween(target: LayoutValue, options: number | SignalTweenOptions<LayoutValue>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(400, 0.6),
const badge: Badgebadge.
LayoutActor<{}>.transform: {
    scale: TypedFieldSignal<number, number>;
    rotation: TypedFieldSignal<number, number>;
} & SignalGroupDefinition
transform
.rotation: TypedFieldSignal<number, number>rotation.function tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>
Tween to target over authored seconds — must yield*
tween
(-8, {
SignalTweenOptions<number>.duration: numberduration: 0.6, SignalTweenOptions<number>.easing?: EasingFunction | undefinedeasing:
const Easing: {
    readonly linear: EasingFunction;
    readonly inQuad: EasingFunction;
    readonly outQuad: EasingFunction;
    readonly inOutQuad: EasingFunction;
    readonly inCubic: EasingFunction;
    readonly outCubic: EasingFunction;
    readonly inOutCubic: EasingFunction;
    readonly inQuart: EasingFunction;
    readonly outQuart: EasingFunction;
    readonly inOutQuart: EasingFunction;
    readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
    readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
    readonly in: (easing: EasingFunction) => EasingFunction;
    readonly out: (easing: EasingFunction) => EasingFunction;
    readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing
.outCubic: EasingFunctionoutCubic,
}), ); const badge: Badgebadge.
LayoutActor<{}>.appearance: {
    opacity: TypedFieldSignal<number, number>;
} & SignalGroupDefinition
appearance
.opacity: TypedFieldSignal<number, number>opacity.function set(value: number): void
Set value instantly at current cursor (no yield needed)
set
(0.5);
}).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });

shape.radius rounds the corners of the actor’s box and clips what it draws to them; effects that need the box’s outline get it too.

flexItem and gridItem are read by the actor’s host when the host lays its children out as a flex row or a grid. They do nothing on a root actor.

Compositing

By default an actor’s children paint straight into its parent, one after another. Setting any compositing field away from its default makes the actor a group: it and its subtree are drawn into one layer, which is then composited onto the parent.

  • blend is a CSS blend mode such as "multiply" or "screen", applied to the group.
  • isolate: true makes a group with nothing else changed, so the subtree’s own blend modes stay inside it.
  • clip clips the group to the actor’s box, with that corner radius in pixels.

A group also changes what appearance.opacity means. On a group it fades the finished layer, so overlapping children no longer show through one another. Resolved actors carry these values as compositing, which is what backends read.

Authored and resolved layout

layout is what you write. It takes numbers, which are pixels, and percentage strings such as "50%". localLayout is what the scene resolves it to: always numbers, with percentages of a root actor resolved against the viewport and those of a hosted actor against its host’s box. worldLayout adds the hosts’ positions, so it is the box on the stage.

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
, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor } from "@motionactor/core";
class class BadgeBadge extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {} const
const definition: SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
    badge: Badge;
}>
definition
=
createScene<{
    badge: Badge;
}, {}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
    badge: Badge;
}>, options?: SceneDefinitionOptions<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>> | undefined): SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
    badge: Badge;
}>
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 badge: Badgebadge = ctx: SceneContextctx.
CompileContext.spawn<Badge>(definition: ActorClass<Badge>, overrides?: {
    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;
    ... 21 more ...;
    parent?: undefined;
} | undefined, options?: {
    ...;
} | undefined): Badge (+1 overload)
spawn
(class BadgeBadge, {
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
: { x?: SignalInputValue<LayoutValue> | undefinedx: "50%", y?: SignalInputValue<LayoutValue> | undefinedy: "25%",
width?: Readonly<{
    kind: "layout.percent";
    value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
    kind: "layout.percent";
    value: number;
}>> | undefined
width
: "10%",
height?: Readonly<{
    kind: "layout.percent";
    value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
    kind: "layout.percent";
    value: number;
}>> | undefined
height
: 40 },
}); const badge: Badgebadge.LayoutActor<{}>.localLayout(): ResolvedLayoutBoxlocalLayout(); // { x: 960, y: 270, width: 192, height: 40 } yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(1); return { badge: Badgebadge }; }, { SceneDefinitionOptions<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>.inputs?: {} | undefinedinputs: {} }, ); const
const compiled: CompiledScene<{
    badge: Badge;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
=
const definition: SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
    badge: Badge;
}>
definition
.
SceneDefinition<{}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { badge: Badge; }>.build(args: SceneBuildInput<{}>): CompiledScene<{
    badge: Badge;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build
({
viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, inputs?: HostInputArgs<{}> | undefinedinputs: {}, fps: numberfps: 30, });
const compiled: CompiledScene<{
    badge: Badge;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
.
CompiledScene<{ badge: Badge; }, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
    badge: Badge;
}>
at
(0).
FrameState<{ badge: Badge; }>.scene: {
    badge: ResolvedActorOfInstance<Badge> | undefined;
}
scene
.badge: ResolvedActorOfInstance<Badge> | undefinedbadge?.
layout: {
    x: LayoutValue;
    y: LayoutValue;
    width: number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>;
    height: number | `${number}%` | Readonly<{
        kind: "layout.percent";
        value: number;
    }>;
}
layout
.x: LayoutValue | undefinedx; // "50%"
const compiled: CompiledScene<{
    badge: Badge;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled
.
CompiledScene<{ badge: Badge; }, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
    badge: Badge;
}>
at
(0).
FrameState<{ badge: Badge; }>.scene: {
    badge: ResolvedActorOfInstance<Badge> | undefined;
}
scene
.badge: ResolvedActorOfInstance<Badge> | undefinedbadge?.localLayout: Readonly<Rect>localLayout.x: number | undefinedx; // 960

A built scene resolves against the viewport it was built for. A new stage size is a new build.

bounds() is the same as localLayout().

Measured size

Some boxes are known only after the browser lays them out: a paragraph that wraps, an item a flex row places. Read the box in a derived:

import { 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 CaptionCaption extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { Caption.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
("");
Caption.underline: DerivedDefinition<number, unknown>underline = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => 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.width: numberwidth);
}

The read asks the DOM backend to measure the element of an actor its host lays out by CSS and publish the box to the actor’s measuredBox channel. Until the box is published, the read returns the box layout computes from the actor’s own x, y, width and height. For every frame that has a measured box, the actor’s resolved localLayout is that box instead of the authored one. The channel is recorded, so a seek back returns the box measured at that frame. See Determinism.

Transitions

*transition(apply, options) applies a change at the cursor and, for duration seconds, has the renderer morph the view from the frame before the change into the view after it:

import { 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 CalendarCalendar extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor { Calendar.view: ActorFieldSignal<"month" | "week">view = signal<"month" | "week">(initial: "month" | "week"): ActorFieldSignal<"month" | "week"> (+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
<"month" | "week">("month");
*Calendar.showWeek(): Generator<number, void, unknown>showWeek() { yield* this.LayoutActor<{}>.transition(apply: () => void, options: TransitionOptions): Generator<Frame, void, unknown>
Apply a mutation and project the change: the tree one frame before the mutation morphs into the tree after it, matched by shared-element name. ```ts yield* this.transition(() => this.view.set("week"), { duration: 0.5 }); yield* this.transition(() => this.items.reorder(next), { duration: 1 }); ```
transition
(() => this.Calendar.view: ActorFieldSignal<"month" | "week">view.LiveSignal<"month" | "week">.set(value: "month" | "week"): void
Set value instantly at current cursor (no yield needed)
set
("week"), {
TransitionOptions.duration: number
Authored seconds for the transition.
duration
: 0.5,
}); } }

The change itself lands on one frame: view is "week" from the cursor on. The morph is drawn from transitionState, which every LayoutActor carries. See Transitions.

Was this page helpful?