Creating actors
One actor from its state to its renderer, with signals for what the scene writes, derived values for what gets drawn and commands for motion.
This page builds a Toast: a notification that fades in, counts down with a shrinking bar,
fades out and announces that it was dismissed. Each step links to the reference page for the
primitive it uses.
State
import {
function derived<TThis, T>(fn: (this: TThis) => T): DerivedDefinition<T, TThis>derived,
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,
type type Duration = numberDuration,
} from "@motionactor/core";
type type ToastEvents = {
dismissed: {
message: string;
};
}
ToastEvents = {
dismissed: {
message: string;
}
dismissed: { message: stringmessage: string };
};
export class class ToastToast extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor<type ToastEvents = {
dismissed: {
message: string;
};
}
ToastEvents> {
Toast.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: 360, LayoutGroupOverrides.height?: LayoutValue | undefinedheight: 64 });
Toast.message: RequiredSignal<string>message = signal<string>(): RequiredSignal<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<string>();
Toast.tone: ActorFieldSignal<"error" | "info">tone = signal<"error" | "info">(initial: "error" | "info"): ActorFieldSignal<"error" | "info"> (+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<"info" | "error">("info");
Toast.remaining: ActorFieldSignal<number>remaining = 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(1);
Toast.barWidth: DerivedDefinition<number, unknown>barWidth = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => this.Toast.remaining: () => numberremaining() * this.LayoutActor<ToastEvents>.localLayout(): ResolvedLayoutBoxlocalLayout().Rect.width: numberwidth);
*Toast.show({ hold }: {
hold: Duration;
}): Generator<number, void, unknown>
show({ hold: numberhold }: { hold: numberhold: type Duration = numberDuration }) {
yield* this.LayoutActor<ToastEvents>.appearance: {
opacity: TypedFieldSignal<number, number>;
} & SignalGroupDefinition
appearance.opacity: TypedFieldSignal<number, number>opacity.function tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(1, 0.2);
yield* this.Toast.remaining: ActorFieldSignal<number>remaining.LiveSignal<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(0, hold: numberhold);
yield* this.LayoutActor<ToastEvents>.appearance: {
opacity: TypedFieldSignal<number, number>;
} & SignalGroupDefinition
appearance.opacity: TypedFieldSignal<number, number>opacity.function tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(0, 0.2);
yield* this.Actor<ToastEvents>.emit: ActorEmitter<ToastEvents>emit.dismissed: (payload: {
message: string;
}) => Generator<Frame, void, unknown>
dismissed({ message: stringmessage: this.Toast.message: () => stringmessage() });
}
}
Every field the scene writes is a signal, and every signal is also a
spawn input. message has no initial value, so a spawn without one is a type error.
LayoutActor already declares the layout, transform, appearance, compositing,
flexItem and gridItem groups. layoutGroup redeclares layout with a default box of
360 by 64, and the other axes keep their defaults. LayoutActor
lists the groups and how authored layout becomes the resolved box.
State says what the toast is doing, in its own terms. remaining runs from 1 to 0 and knows
nothing about pixels, and tone names a state whose color the renderer picks. A renderer on
any backend can decide what those look like.
Values to draw
barWidth is a derived: it reads remaining and the resolved box
at the frame being resolved. The renderer reads the finished number. Geometry computed on the
actor is the same on every backend, and a test can assert it without drawing anything.
Commands
show is a command, a generator method the scene runs with yield*. Its durations are
seconds, and it takes an object, so a call reads toast.show({ hold: 2 }). Give a command an
object parameter whenever it takes more than one primitive value.
The last step emits dismissed. The toast does not know who is listening; a scene that
should react to it subscribes. To show show in an
editor’s command list with its parameters, label it with command.
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 ToastToast } from "./toast";
export const const compiled: CompiledScene<{
toast: Toast;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled = createScene<{
toast: Toast;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
toast: Toast;
}>, 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 toast: Toasttoast = ctx: SceneContextctx.CompileContext.spawn<Toast>(definition: ActorClass<Toast>, overrides: {
message: SignalInputValue<string>;
appearance?: {
opacity?: SignalInputValue<number> | undefined;
} | undefined;
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<...> | ... 1 more ... | undefined;
} | undefined;
... 23 more ...;
parent?: undefined;
}, options?: {
...;
} | undefined): Toast (+1 overload)
spawn(class ToastToast, {
message: SignalInputValue<string>message: "Saved",
appearance?: {
opacity?: SignalInputValue<number> | undefined;
} | undefined
appearance: { opacity?: SignalInputValue<number> | undefinedopacity: 0 },
});
yield* const toast: Toasttoast.Toast.show({ hold }: {
hold: Duration;
}): Generator<number, void, unknown>
show({ hold: numberhold: 2 });
return { toast: Toasttoast };
}).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
toast: Toast;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const compiled: CompiledScene<{
toast: Toast;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.totalDuration: numbertotalDuration; // 72
const compiled: CompiledScene<{
toast: Toast;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
toast: Toast;
}>
at(36).FrameState<{ ...; }>.scene: {
toast: ResolvedActorOfInstance<Toast> | undefined;
}
scene.toast: ResolvedActorOfInstance<Toast> | undefinedtoast?.barWidth: number | undefinedbarWidth; // 180
The fade in takes 6 frames, the countdown 60 and the fade out 6. At frame 36 the countdown is halfway, so the bar is half of 360.
Drawing it
A renderer turns the resolved toast into what one backend draws:
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 { class ToastToast } from "./toast";
export const const toastRenderer: BackendRendererDefinition<readonly [typeof Toast], "dom">toastRenderer = 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 Toast>(blueprint: typeof Toast, render: ActorRendererForBlueprints<readonly [typeof Toast], "dom">, options?: ActorRendererRegistrationOptions) => BackendRendererDefinition<readonly [typeof Toast], "dom"> (+1 overload)render(class ToastToast, ({ 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;
message: string;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
...;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
barWidth: number;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor }) => (
<React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div React.HTMLAttributes<HTMLDivElement>.role?: React.AriaRole | undefinedrole="status" React.HTMLAttributes<HTMLDivElement>.className?: string | undefinedclassName="toast" data-tone: "error" | "info"data-tone={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;
message: string;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
...;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
barWidth: number;
} & {
...;
}
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;
message: string;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
...;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
barWidth: number;
}
signals.tone: "error" | "info"tone}>
{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;
message: string;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
...;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
barWidth: number;
} & {
...;
}
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;
message: string;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
...;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
barWidth: number;
}
signals.message: stringmessage}
<React.JSX.IntrinsicElements.i: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>i React.HTMLAttributes<T>.className?: string | undefinedclassName="toast-bar" React.HTMLAttributes<HTMLElement>.style?: React.CSSProperties | undefinedstyle={{ StandardLonghandProperties<string | number, string & {}>.width?: Property.Width<string | number> | undefinedThis feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.
**Syntax**: `auto | <length-percentage [0,∞]> | min-content | max-content | fit-content | fit-content(<length-percentage [0,∞]>) | <calc-size()> | <anchor-size()>`
**Initial value**: `auto`
| Chrome | Firefox | Safari | Edge | IE |
| :----: | :-----: | :----: | :----: | :---: |
| **1** | **1** | **1** | **12** | **4** |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;
message: string;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
...;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
barWidth: number;
} & {
...;
}
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;
message: string;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
...;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
barWidth: number;
}
signals.barWidth: numberbarWidth }} />
</React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>
));
actor.signals holds each field’s value at this frame, typed from the class. The DOM backend
has already sized and placed the element from the resolved box, so the renderer only fills
it. Class names stay static; values that change per frame go through style, and states
such as tone go through attributes a stylesheet can match.
List the renderer in a render runtime to use it. Renderers covers
matching, registries and the alternative for DOM actors, a render method on the class.
Parts
An actor with parts of its own declares them as fields. A child is a
fixed part that every toast has, such as an icon. A slot is a place the
scene fills, such as an action button that only some toasts show.
Actor families meant for reuse across projects ship through the component library. An actor that one composition needs lives next to that composition.