watch
Runs a handler while the scene compiles, each time a value the actor reads changes.
watch(source, handler) keeps a rule next to the state it depends on. source reads some of
the actor’s values and returns one result. Whenever that result changes, the handler runs
with the new and the previous result, and anything it writes is recorded at that frame like
any other write in the scene.
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, function watch<TThis, TValue>(source: WatchSource<TValue, TThis>, handler: WatchHandler<TValue, TThis>): WatchDefinition<TValue, TThis>watch } from "@motionactor/core";
export class class SearchSearch extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Search.query: ActorFieldSignal<string>query = 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("");
Search.open: ActorFieldSignal<boolean>open = signal<boolean>(initial: boolean): ActorFieldSignal<boolean> (+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(false);
Search.sync: WatchDefinition<boolean, unknown>sync = watch<unknown, boolean>(source: WatchSource<boolean, unknown>, handler: WatchHandler<boolean, unknown>): WatchDefinition<boolean, unknown>watch(
() => this.Search.query: () => stringquery().String.length: numberReturns the length of a String object.length > 0,
(hasQuery: booleanhasQuery) => this.Search.open: ActorFieldSignal<boolean>open.LiveSignal<boolean>.set(value: boolean): voidSet value instantly at current cursor (no yield needed)set(hasQuery: booleanhasQuery),
);
}
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 SearchSearch } from "./search";
const const compiled: CompiledScene<{
search: Search;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled = createScene<{
search: Search;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
search: Search;
}>, 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 search: Searchsearch = ctx: SceneContextctx.CompileContext.spawn<Search>(definition: ActorClass<Search>, overrides?: {
measuredBox?: undefined;
query?: SignalInputValue<string> | undefined;
open?: SignalInputValue<boolean> | 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;
... 20 more ...;
parent?: undefined;
} | undefined, options?: {
...;
} | undefined): Search (+1 overload)
spawn(class SearchSearch);
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(0.5);
const search: Searchsearch.Search.query: ActorFieldSignal<string>query.LiveSignal<string>.set(value: string): voidSet value instantly at current cursor (no yield needed)set("ca");
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(0.5);
const search: Searchsearch.Search.query: ActorFieldSignal<string>query.LiveSignal<string>.set(value: string): voidSet value instantly at current cursor (no yield needed)set("cat");
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(0.5);
return { search: Searchsearch };
}).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
search: Search;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const compiled: CompiledScene<{
search: Search;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
search: Search;
}>
at(0).FrameState<{ ...; }>.scene: {
search: ResolvedActorOfInstance<Search> | undefined;
}
scene.search: ResolvedActorOfInstance<Search> | undefinedsearch?.open: boolean | undefinedopen; // false
const compiled: CompiledScene<{
search: Search;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
search: Search;
}>
at(15).FrameState<{ ...; }>.scene: {
search: ResolvedActorOfInstance<Search> | undefined;
}
scene.search: ResolvedActorOfInstance<Search> | undefinedsearch?.open: boolean | undefinedopen; // true
The scene only writes query, and open follows. Typing the third letter changes the query
but not the source’s result, which was already true, so the handler does not run again.
When it runs
A watch is registered when its actor spawns. The source runs then, and its result is the
starting point; the handler does not run for it. Plain objects and arrays are compared by
their contents, so a source that returns { ...this.bag.peek() } changes only when the data
does. Anything else is compared with Object.is.
The compiler checks the source when a signal it read is written, and on every frame where a
value it read can change: each frame of a tween, and the frame of a later write. A watch on
level() > 50 while level tweens from 0 to 100 runs on the frame level passes 50. A
source that reads runtime.frame() or runtime.timeSeconds() is checked on every frame.
A handler’s writes are checked too, so one watch can set off another. The chain has to settle; after 100 rounds at one frame, compiling throws.
A handler that takes time
A generator handler animates. JavaScript has no generator arrows, so give the function a
this type:
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, 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, function watch<TThis, TValue>(source: WatchSource<TValue, TThis>, handler: WatchHandler<TValue, TThis>): WatchDefinition<TValue, TThis>watch } from "@motionactor/core";
class class BadgeBadge extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Badge.active: ActorFieldSignal<boolean>active = signal<boolean>(initial: boolean): ActorFieldSignal<boolean> (+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(false);
Badge.fade: WatchDefinition<boolean, Badge>fade = watch<Badge, boolean>(source: WatchSource<boolean, Badge>, handler: WatchHandler<boolean, Badge>): WatchDefinition<boolean, Badge>watch(
() => this.Badge.active: () => booleanactive(),
function* (this: Badgethis: class BadgeBadge, active: booleanactive) {
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(active: booleanactive ? 1 : 0.2, 1);
},
);
}
const const compiled: CompiledScene<{
badge: Badge;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled = createScene<{
badge: Badge;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
badge: Badge;
}>, 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?: {
measuredBox?: undefined;
transform?: {
scale?: SignalInputValue<number> | undefined;
rotation?: SignalInputValue<number> | undefined;
} | undefined;
id?: undefined;
appearance?: {
opacity?: SignalInputValue<number> | undefined;
} | undefined;
active?: SignalInputValue<boolean> | undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
width?: Readonly<...> | ... 1 more ... | undefined;
height?: Readonly<...> | ... 1 more ... | undefined;
} | undefined;
... 17 more ...;
parent?: undefined;
} | undefined, options?: {
...;
} | undefined): Badge (+1 overload)
spawn(class BadgeBadge, { appearance?: {
opacity?: SignalInputValue<number> | undefined;
} | undefined
appearance: { opacity?: SignalInputValue<number> | undefinedopacity: 0 } });
const badge: Badgebadge.Badge.active: ActorFieldSignal<boolean>active.LiveSignal<boolean>.set(value: boolean): voidSet value instantly at current cursor (no yield needed)set(true);
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(0.5);
const badge: Badgebadge.Badge.active: ActorFieldSignal<boolean>active.LiveSignal<boolean>.set(value: boolean): voidSet value instantly at current cursor (no yield needed)set(false);
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(1.5);
return { badge: Badgebadge };
}).SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
badge: Badge;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1920, SceneViewport.height: numberheight: 1080 }, fps: numberfps: 30 });
const compiled: CompiledScene<{
badge: Badge;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
badge: Badge;
}>
at(15).FrameState<{ ...; }>.scene: {
badge: ResolvedActorOfInstance<Badge> | undefined;
}
scene.badge: ResolvedActorOfInstance<Badge> | undefinedbadge?.appearance: {
opacity: number;
}
appearance.opacity: number | undefinedopacity; // 0.5
const compiled: CompiledScene<{
badge: Badge;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
badge: Badge;
}>
at(30).FrameState<{ ...; }>.scene: {
badge: ResolvedActorOfInstance<Badge> | undefined;
}
scene.badge: ResolvedActorOfInstance<Badge> | undefinedbadge?.appearance: {
opacity: number;
}
appearance.opacity: number | undefinedopacity; // 0.35
The handler runs on a branch of its own from the frame of the change, and the script does not
wait for it. When the result changes again before the branch finishes, the running branch
stops and a new one starts from the current value. Here the fade in is halfway at 0.5 when
active turns false, and the fade out starts from there. A handler still running when the
script ends keeps the scene going until it finishes.
A handler started by a write steps when the branch that wrote pauses, at the same frame. One started by a frame of a tween steps before every other branch at that frame, so the script reads what it wrote there.
What it cannot read
The source runs while the scene compiles, so it can read only what the scene writes. A source
that reads a channel, or a derived that reads one, throws, and so
does runtime.viewport(): neither has a value until a host resolves a frame. Compute from
them in a derived.
runtime.frame() throws inside a handler. Use this.cursor for the frame or
this.cursorSeconds for the time in seconds.
To act on something another actor does, subscribe to its events.