Actor
The base class. An actor owns state, the commands that change it over time, and hooks for entering and leaving a scene.
An actor is a class whose fields are signals and whose methods are
generators. The methods are commands: a scene calls them while it compiles, and every tween
inside them is recorded at the cursor. Actor has no box. Anything drawn on the stage
extends LayoutActor; extend Actor for a clock, a camera, a sound
manager or a controller.
import { class Actor<TEvents extends ActorEventMap = {}>Actor, 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, 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 CounterCounter extends class Actor<TEvents extends ActorEventMap = {}>Actor {
static Counter.type: stringtype = "counter";
Counter.count: ActorFieldSignal<number>count = 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);
*Counter.countTo({ target, duration }: {
target: number;
duration?: number;
}): Generator<number, void, unknown>
countTo({ target: numbertarget, duration: numberduration = 1 }: {
target: numbertarget: number;
duration?: number | undefinedduration?: number;
}) {
yield* this.Counter.count: ActorFieldSignal<number>count.LiveSignal<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(target: numbertarget, {
SignalTweenOptions<number>.duration: numberduration,
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,
});
}
Counter.reset(): voidreset() {
this.Counter.count: ActorFieldSignal<number>count.LiveSignal<number>.set(value: number): voidSet value instantly at current cursor (no yield needed)set(0);
}
}
A command that yield*s a tween takes that tween’s time. A plain method that only calls set
takes none: the value changes at the cursor. Give a command more than one number as an
object, as countTo does. Positional numbers read as countTo(240, 2) at the call site and
are easy to swap.
static type names the class in reflection, in findActorByType and to editors. Without it
the class name is used.
On every instance
| Member | |
|---|---|
id |
Set at spawn: the id option, or a generated one |
cursor, cursorSeconds |
The scene’s cursor while it compiles |
sceneFps |
The frame rate of the build the actor runs in |
runtime |
Timing verbs, as on ctx, plus fork and loop. See Inside an actor |
parent(Class?) |
The actor’s host, if it has one. See Children |
enter(params?), exit(params?) |
Run the lifecycle hooks below |
emit |
Raises the actor’s events |
fx |
Installed effects by name: fx.glow(params?, playback?), and fx.for(backend) for a name two backends define. See Effects |
They work only while the actor is in a scene. On an instance made with new, outside a
scene script, the methods throw and the cursor fields are undefined.
Lifecycle
| Hook | Runs | Takes time |
|---|---|---|
onInit() |
At spawn, after the overrides are applied and children exist | No, it is synchronous |
*onEnter(params) |
When something calls enter(params) |
Yes |
*onExit(params) |
When something calls exit(params) |
Yes |
onInit is where an actor fills its own slots from its inputs. Spawning
does not run onEnter. Call enter(), or spawn with the enter option, when the entrance
should play.
exit() runs the actor’s onExit and, at the same time, the onExit of every actor it
hosts. The whole subtree despawns when the longest of them ends:
import { function child<TBlueprint extends ActorClass<object>, TOwner extends object = object>(blueprint: TBlueprint, ...[overrides]: ChildOverridesArg<TBlueprint, TOwner>): ChildField<TBlueprint, TOwner>child, 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 CaptionCaption extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
*Caption.onExit(): Generator<number, void, unknown>onExit() {
yield* this.LayoutActor<{}>.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, 1);
}
}
class class CardCard extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Card.caption: ChildField<typeof Caption, object>caption = child<typeof Caption, object>(blueprint: typeof Caption, overrides?: ChildOverrides<typeof Caption, object> | undefined): ChildField<typeof Caption, object>child(class CaptionCaption);
*Card.onEnter(): Generator<number, void, unknown>onEnter() {
yield* this.LayoutActor<{}>.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.5);
}
*Card.onExit(): Generator<number, void, unknown>onExit() {
yield* this.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.y: ActorFieldSignal<LayoutValue>y.LiveSignal<LayoutValue>.tween(target: LayoutValue, options: number | SignalTweenOptions<LayoutValue>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(40, 0.5);
}
}
const const compiled: CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>compiled = 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 card: Cardcard = ctx: SceneContextctx.CompileContext.spawn<Card>(definition: ActorClass<Card>, overrides?: {
measuredBox?: undefined;
transform?: {
scale?: SignalInputValue<number> | undefined;
rotation?: SignalInputValue<number> | undefined;
} | undefined;
id?: undefined;
onExit?: undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<...>> | undefined;
height?: Readonly<...> | ... 1 more ... | undefined;
} | undefined;
... 18 more ...;
caption?: {
...;
} | undefined;
} | undefined, options?: {
...;
} | undefined): Card (+1 overload)
spawn(class CardCard, { appearance?: {
opacity?: SignalInputValue<number> | undefined;
} | undefined
appearance: { opacity?: SignalInputValue<number> | undefinedopacity: 0 } });
yield* const card: Cardcard.Actor<{}>.enter(_params?: unknown): Generator<Frame, void, unknown>enter();
ctx: SceneContextctx.CompileContext.cursor: numbercursor; // 15
yield* const card: Cardcard.Actor<{}>.exit(_params?: unknown): Generator<Frame, void, unknown>exit();
ctx: SceneContextctx.CompileContext.cursor: numbercursor; // 45: the caption's second outlasts the card's half
}).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 });
const compiled: CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>compiled.CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<void>at(44).FrameState<void>.actors: ReadonlyMap<string, ResolvedActor>actors.ReadonlyMap<string, ResolvedActor>.size: numbersize; // 2
const compiled: CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>compiled.CompiledScene<void, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<void>at(45).FrameState<void>.actors: ReadonlyMap<string, ResolvedActor>actors.ReadonlyMap<string, ResolvedActor>.size: numbersize; // 0
A host paces its children’s exits by calling their exit() inside its own onExit, before
that hook first waits. Each child’s onExit then runs when the host runs the generator
exit() returned; the children it does not call start together once the hook first waits.
Awaiting a child’s exit resumes when that child’s onExit ends. The child stays in the scene
until the whole subtree despawns.
import { function child<TBlueprint extends ActorClass<object>, TOwner extends object = object>(blueprint: TBlueprint, ...[overrides]: ChildOverridesArg<TBlueprint, TOwner>): ChildField<TBlueprint, TOwner>child, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor } from "@motionactor/core";
class class CaptionCaption extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
*Caption.onExit(): Generator<number, void, unknown>onExit() {
yield* this.LayoutActor<{}>.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.5);
}
}
class class PanelPanel extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
*Panel.onExit(): Generator<number, void, unknown>onExit() {
yield* this.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.y: ActorFieldSignal<LayoutValue>y.LiveSignal<LayoutValue>.tween(target: LayoutValue, options: number | SignalTweenOptions<LayoutValue>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(40, 0.5);
}
}
class class CardCard extends class PanelPanel {
Card.caption: ChildField<typeof Caption, object>caption = child<typeof Caption, object>(blueprint: typeof Caption, overrides?: ChildOverrides<typeof Caption, object> | undefined): ChildField<typeof Caption, object>child(class CaptionCaption);
*Card.onExit(): Generator<number, void, unknown>onExit() {
const const caption: Generator<number, void, unknown>caption = this.Card.caption: ChildField<typeof Caption, object>caption.Actor<{}>.exit(_params?: unknown): Generator<Frame, void, unknown>exit(); // before the first wait
yield* super.Panel.onExit(): Generator<number, void, unknown>onExit();
yield* const caption: Generator<number, void, unknown>caption; // the caption fades after the card slides
}
}
Calling exit() on a child that is already exiting on its own, after the hook first waited,
throws and names the child. So does calling a child’s exit() and never running it.
Events
An actor declares the events it raises as a type parameter. emit.<name>(payload) is a
generator that records the event at the cursor, and actors that
subscribe to it run their handlers there.
import { class Actor<TEvents extends ActorEventMap = {}>Actor, 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 TimerTimer extends class Actor<TEvents extends ActorEventMap = {}>Actor<{ finished: {
at: number;
}
finished: { at: numberat: number } }> {
Timer.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(3);
*Timer.run(): Generator<number, void, unknown>run() {
yield* this.Timer.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, 3);
yield* this.Actor<{ finished: { at: number; }; }>.emit: ActorEmitter<{
finished: {
at: number;
};
}>
emit.finished: (payload: {
at: number;
}) => Generator<Frame, void, unknown>
finished({ at: numberat: this.Actor<{ finished: { at: number; }; }>.cursorSeconds: numberCompile cursor expressed in authored seconds (cursor / scene fps).cursorSeconds });
}
}
Drawn from the frame
static frameDriven = true marks a class whose renderer draws from the frame number rather
than from the actor’s state, such as a shader keyed to time. Export then never treats two of
its frames as the same picture. See Capture and export.