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): voidSet 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.
blendis a CSS blend mode such as"multiply"or"screen", applied to the group.isolate: truemakes a group with nothing else changed, so the subtree’s own blend modes stay inside it.clipclips 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]: () => 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.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"): voidSet value instantly at current cursor (no yield needed)set("week"), {
TransitionOptions.duration: numberAuthored 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.