Effects
An effect is a timed record on an actor that the actor's backend paints, with typed params a scene can tween.
An effect changes how an actor looks without changing the actor. Applying one records it on the timeline next to the actor, with its own params, and the backend that paints the actor reads the record and draws the treatment. The actor’s own state is never touched, so removing the effect leaves nothing to undo.
The card’s renderer draws a plain panel. The glow comes from the DOM backend’s style stage,
which reads card.effects on each frame and adds a CSS filter around the card. play fades
the glow in, a tween on spread widens it, and detach ends it.
Define
defineEffect(kind, { name, params, ...contribution }) declares an effect once. The kind
comes from the backend package and fixes three things: which backend paints the effect, which
stage paints it, and what the contribution is. Params are signals, and the contribution is
typed from them, so no separate params type exists to keep in sync.
import { const describeEffect: (source: AnyEffectDefinition | EffectBinding) => EffectDescriptionWhat an editor needs to label and offer an effect: its kind's name, what
it applies to, and per param reflection's `kind`, the declared `type`,
`range`, `options` and `unit`, and the JSON `default`. `params: {}` for an
effect that declares none. A binding must carry its definition; resolve a
bare wire id through the render runtime (`runtime.describeEffect(id)`).describeEffect } from "@motionactor/core";
import { const glow: EffectDefinition<"dom", "glow", {
readonly color: TypedFieldSignal<Color, string | Color>;
readonly spread: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}, EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, unknown, DomStyleRender<EffectParamValues<{
readonly color: TypedFieldSignal<Color, string | Color>;
readonly spread: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}>>, false, "actor">
glow } from "./effect-glow.scene";
const glow: EffectDefinition<"dom", "glow", {
readonly color: TypedFieldSignal<Color, string | Color>;
readonly spread: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}, EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, unknown, DomStyleRender<EffectParamValues<{
readonly color: TypedFieldSignal<Color, string | Color>;
readonly spread: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}>>, false, "actor">
glow.EffectDefinitionFields<"dom", "glow", { readonly color: TypedFieldSignal<Color, string | Color>; readonly spread: ActorFieldSignal<number>; readonly progress: ActorFieldSignal<...>; }, EffectKind<...>, DomStyleRender<...>>.name: "glow"Unique among its backend's effects: with the backend's name, the wire identity.name; // "glow"
function describeEffect(source: AnyEffectDefinition | EffectBinding): EffectDescriptionWhat an editor needs to label and offer an effect: its kind's name, what
it applies to, and per param reflection's `kind`, the declared `type`,
`range`, `options` and `unit`, and the JSON `default`. `params: {}` for an
effect that declares none. A binding must carry its definition; resolve a
bare wire id through the render runtime (`runtime.describeEffect(id)`).describeEffect(const glow: EffectDefinition<"dom", "glow", {
readonly color: TypedFieldSignal<Color, string | Color>;
readonly spread: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}, EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, unknown, DomStyleRender<EffectParamValues<{
readonly color: TypedFieldSignal<Color, string | Color>;
readonly spread: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}>>, false, "actor">
glow).EffectDescription.params: Record<string, EffectParamDescription>params.EffectParamDescriptionWhat an editor needs to offer one param: reflection's kind and type facts, and the JSON default.spread;
// { kind: "number", default: 16 }
describeEffect is what an editor reads to build a control for each param: its kind, its
declared type, its range and its default.
| Kind | Import | Contribution | Painted by |
|---|---|---|---|
domStyle |
@motionactor/dom |
style(params, { box }) → { filter, translateX, rotate, scale, opacity, clipPath, overlays, … } |
The DOM style stage, inline |
glShader |
@motionactor/dom |
A WebGL shader with uniforms(params, frame) |
The DOM canvas stage, offscreen |
canvasPaint |
@motionactor/dom |
paint(context2d, ctx) |
The DOM canvas stage, offscreen |
pixiFilter |
@motionactor/pixi |
A Pixi filter class and update(filter, params) |
The Pixi filter stage |
threePass |
@motionactor/three |
A postprocessing pass and update(pass, params) |
The Three.js host’s composer |
threeMaterial |
@motionactor/three |
material: { apply, update, remove } |
The Three.js host |
Several domStyle effects on one actor fold together: filters append, translations and
rotations add.
Overlays
A domStyle effect draws over its actor with overlays. They’re data built with overlay.*,
so the same effect paints under React, Vue and Svelte:
import { function defineEffect<const TName extends string, const P extends EffectParamDeclarations, TKind extends AnyEffectKind, TTarget = unknown, const TTime extends boolean = false, TNative = unknown, const TApplies extends EffectApplication | undefined = undefined>(kind: TKind, spec: EffectSpec<TName, P, TTarget, TTime> & {
readonly params: NoPlaybackParams;
readonly applies?: TApplies;
} & EffectNativeSite<TNative> & EffectKindSpec<EffectKindTypesOf<TKind>, EffectRenderParams<P, TTime>, TTarget, TNative>): EffectDefinition<EffectKindBackendOf<TKind>, TName, P, TKind, TTarget, EffectKindRuntime<EffectKindTypesOf<TKind>, EffectRenderParams<P, TTime>, TTarget, TNative>, TTime, EffectDefinitionApplies<TApplies, TTarget, TKind>>
Define an effect: one value of one kind, on the kind's backend. The spec is
the core fields plus the kind's own, typed from `params`:
export const glow = defineEffect(domStyle, {
name: "glow",
params: { color: signal.of(colorType, color("#22d3ee")), spread: signal(18) },
style: ({ color, spread }) => ({ filter: `drop-shadow(0 0 ${spread}px ${color})` }),
});
declare module "@motionactor/core" { interface ActorEffects { "dom:glow": typeof glow } }
registerEffects([glow]);
card.fx.glow({ spread: 24 });defineEffect, 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";
import { const domStyle: EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that styles its actor's element with CSS:
export const blur = defineEffect(domStyle, {
name: "blur",
params: { strength: signal(8), progress: signal.of(unitInterval, 0) },
style: ({ strength, progress }) => ({ filter: `blur(${strength * progress}px)` }),
});
Framework-free: each binding's DOM backend pairs it with a stage that paints
`domStyleLayers` in its own elements.domStyle, const overlay: {
div: (props?: Readonly<{
style?: StyleRecord;
data?: Readonly<Record<string, string>>;
mount?: OverlayMount;
}> | undefined, children?: readonly OverlayElement[]) => OverlayElement;
svg: (props?: OverlaySvgProps | undefined, children?: readonly OverlayElement[]) => OverlayElement;
g: (props?: OverlaySvgProps | undefined, children?: readonly OverlayElement[]) => OverlayElement;
rect: (props: OverlaySvgProps) => OverlayElement;
circle: (props: OverlaySvgProps) => OverlayElement;
line: (props: OverlaySvgProps) => OverlayElement;
path: (props: OverlaySvgProps) => OverlayElement;
}
The overlay builders:
overlays: [overlay.svg({ attrs: { viewBox } }, [overlay.rect({ attrs: { strokeWidth: 2 } })])]overlay } from "@motionactor/dom";
export const const ring: EffectDefinition<"dom", "ring", {
readonly width: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}, EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, unknown, DomStyleRender<EffectParamValues<{
readonly width: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}>>, false, "actor">
ring = defineEffect<"ring", {
readonly width: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}, EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, unknown, false, unknown, undefined>(kind: EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, spec: EffectSpec<"ring", {
readonly width: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}, unknown, false> & {
...;
} & DomStyleSpec<...>): EffectDefinition<...>
Define an effect: one value of one kind, on the kind's backend. The spec is
the core fields plus the kind's own, typed from `params`:
export const glow = defineEffect(domStyle, {
name: "glow",
params: { color: signal.of(colorType, color("#22d3ee")), spread: signal(18) },
style: ({ color, spread }) => ({ filter: `drop-shadow(0 0 ${spread}px ${color})` }),
});
declare module "@motionactor/core" { interface ActorEffects { "dom:glow": typeof glow } }
registerEffects([glow]);
card.fx.glow({ spread: 24 });defineEffect(const domStyle: EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that styles its actor's element with CSS:
export const blur = defineEffect(domStyle, {
name: "blur",
params: { strength: signal(8), progress: signal.of(unitInterval, 0) },
style: ({ strength, progress }) => ({ filter: `blur(${strength * progress}px)` }),
});
Framework-free: each binding's DOM backend pairs it with a stage that paints
`domStyleLayers` in its own elements.domStyle, {
EffectSpec<"ring", { readonly width: ActorFieldSignal<number>; readonly progress: ActorFieldSignal<number>; }, unknown, false>.name: "ring"Unique among the backend's effects. With the backend's name, the effect's wire identity.name: "ring",
params: {
readonly width: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
} & NoPlaybackParams
Every param the kind's contribution reads, declared once as `signal(default)` or `signal.of(type, default)`.params: { width: ActorFieldSignal<number>width: 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(2), progress: ActorFieldSignal<number>progress: 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) },
style: DomStyleRender<EffectParamValues<{
readonly width: ActorFieldSignal<number>;
readonly progress: ActorFieldSignal<number>;
}>>
The effect's contribution to its actor's element, pure in its params.style: ({ width: numberwidth, progress: numberprogress }, { box: EffectStageBox | undefinedbox }) => ({
overlays?: readonly OverlayElement[] | undefinedPainted over the actor's content, inside its box, by whichever binding paints the actor.overlays: [
const overlay: {
div: (props?: Readonly<{
style?: StyleRecord;
data?: Readonly<Record<string, string>>;
mount?: OverlayMount;
}> | undefined, children?: readonly OverlayElement[]) => OverlayElement;
svg: (props?: OverlaySvgProps | undefined, children?: readonly OverlayElement[]) => OverlayElement;
g: (props?: OverlaySvgProps | undefined, children?: readonly OverlayElement[]) => OverlayElement;
rect: (props: OverlaySvgProps) => OverlayElement;
circle: (props: OverlaySvgProps) => OverlayElement;
line: (props: OverlaySvgProps) => OverlayElement;
path: (props: OverlaySvgProps) => OverlayElement;
}
The overlay builders:
overlays: [overlay.svg({ attrs: { viewBox } }, [overlay.rect({ attrs: { strokeWidth: 2 } })])]overlay.svg: (props?: OverlaySvgProps | undefined, children?: readonly OverlayElement[]) => OverlayElementsvg(
{ style?: StyleRecord | undefinedstyle: { StandardLonghandProperties<string | number, string & {}>.position?: Property.Position | undefinedThis feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.
**Syntax**: `static | relative | absolute | sticky | fixed`
**Initial value**: `static`
| Chrome | Firefox | Safari | Edge | IE |
| :----: | :-----: | :----: | :----: | :---: |
| **1** | **1** | **1** | **12** | **4** |position: "absolute", StandardShorthandProperties<string | number, string & {}>.inset?: Property.Inset<string | number> | undefinedThis feature is well established and works across many devices and browser versions. It’s been available across browsers since April 2021.
**Syntax**: `<'top'>{1,4}`
| Chrome | Firefox | Safari | Edge | IE |
| :----: | :-----: | :------: | :----: | :-: |
| **87** | **66** | **14.1** | **87** | No |inset: 0 }, attrs?: Readonly<{
viewBox?: string;
preserveAspectRatio?: string;
transform?: string;
x?: number;
y?: number;
width?: number;
height?: number;
rx?: number;
ry?: number;
cx?: number;
cy?: number;
r?: number;
x1?: number;
y1?: number;
x2?: number;
y2?: number;
d?: string;
fill?: string;
opacity?: number;
stroke?: string;
strokeWidth?: number;
strokeLinecap?: "butt" | "round" | "square";
strokeLinejoin?: "miter" | "round" | "bevel";
strokeDasharray?: string;
strokeDashoffset?: number;
strokeOpacity?: number;
}> | undefined
attrs: { viewBox?: string | undefinedviewBox: `0 0 ${box: EffectStageBox | undefinedbox?.width: number | undefinedwidth ?? 0} ${box: EffectStageBox | undefinedbox?.height: number | undefinedheight ?? 0}` } },
[const overlay: {
div: (props?: Readonly<{
style?: StyleRecord;
data?: Readonly<Record<string, string>>;
mount?: OverlayMount;
}> | undefined, children?: readonly OverlayElement[]) => OverlayElement;
svg: (props?: OverlaySvgProps | undefined, children?: readonly OverlayElement[]) => OverlayElement;
g: (props?: OverlaySvgProps | undefined, children?: readonly OverlayElement[]) => OverlayElement;
rect: (props: OverlaySvgProps) => OverlayElement;
circle: (props: OverlaySvgProps) => OverlayElement;
line: (props: OverlaySvgProps) => OverlayElement;
path: (props: OverlaySvgProps) => OverlayElement;
}
The overlay builders:
overlays: [overlay.svg({ attrs: { viewBox } }, [overlay.rect({ attrs: { strokeWidth: 2 } })])]overlay.rect: (props: OverlaySvgProps) => OverlayElementrect({ attrs?: Readonly<{
viewBox?: string;
preserveAspectRatio?: string;
transform?: string;
x?: number;
y?: number;
width?: number;
height?: number;
rx?: number;
ry?: number;
cx?: number;
cy?: number;
r?: number;
x1?: number;
y1?: number;
x2?: number;
y2?: number;
d?: string;
fill?: string;
opacity?: number;
stroke?: string;
strokeWidth?: number;
strokeLinecap?: "butt" | "round" | "square";
strokeLinejoin?: "miter" | "round" | "bevel";
strokeDasharray?: string;
strokeDashoffset?: number;
strokeOpacity?: number;
}> | undefined
attrs: { x?: number | undefinedx: 1, y?: number | undefinedy: 1, width?: number | undefinedwidth: (box: EffectStageBox | undefinedbox?.width: number | undefinedwidth ?? 2) - 2, height?: number | undefinedheight: (box: EffectStageBox | undefinedbox?.height: number | undefinedheight ?? 2) - 2, fill?: string | undefinedfill: "none", stroke?: string | undefinedstroke: "#f86e42", strokeWidth?: number | undefinedstrokeWidth: width: numberwidth, opacity?: number | undefinedopacity: progress: numberprogress } })],
),
],
}),
});
A painter that draws imperatively, such as a WebGL shader, attaches to an overlay element with
overlayMount(id, props, painter). The painter starts once and receives each later frame’s props
while the id holds. It is disposed when the element goes. Declare the painter at module level:
a mount with a different painter restarts.
Install
The scene above installs glow beside its definition, in two statements. The ActorEffects
entry, keyed "<backend>:<name>", types the name. registerEffects([glow]) makes it
callable. Importing the module installs the effect. A type-only import runs no registration, so
the call throws, naming the effect.
- A later registration at the same backend and name replaces the earlier one. A hot reload or a second module loader re-registers without error.
- Two
ActorEffectsentries for one key with different definitions are a type error. registerEffectsthrows for an actor effect namedfor.- An effect that cannot apply to an actor, a layer effect or one that needs target data,
registers too. It resolves by wire id but stays off
fx; apply it to a target withctx.apply.
A module imported only for its registration is a side effect. Under "sideEffects": false a
bundler drops it, and the call throws. A package that ships effect modules, or wraps copied
ones, lists them and the entries that import them:
{ "sideEffects": ["./src/**/effect.ts", "./dist/index.mjs", "./dist/index.cjs"] }
Call by name
fx is a getter on Actor, so every actor, child field and slot occupant has the same menu:
each installed effect whose name one backend defines. Params come first, playback second.
const glowing = card.fx.glow(); // handle, default params
const wide = card.fx.glow({ spread: 24 }); // handle
yield* card.fx.glow({ duration: 0.6 }); // played, default params
yield* card.fx.glow({ spread: 24 }, { duration: 0.6, easing: Easing.outCubic }); // played
- Without playback, the effect applies at the cursor and the call returns its handle.
- With playback, it applies when the generator first runs, then plays to its end and holds
there. Under
ctx.delaythat is when the delay ends. - One object with
durationis playback, so no effect param may be nameddurationoreasing. - An effect with a required param has no
()or(playback)form.
On the handle:
handle.play({ duration, easing? })tweens a param namedprogressfrom 0 to 1. An effect without aprogressparam just waits out the duration.- Every param is a signal, so
handle.spread.tween(48, 0.5)andhandle.color.set(...)work as they do on an actor. A param given as aderivedhas no signal on the handle. handle.detach()ends the record at the cursor. Frames before it still show the effect.
One name on two backends
A name two installed backends define leaves the menu: card.fx.glow is a type error, and at
runtime the call throws, naming both. fx.for(backend) selects one:
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, function defineEffect<const TName extends string, const P extends EffectParamDeclarations, TKind extends AnyEffectKind, TTarget = unknown, const TTime extends boolean = false, TNative = unknown, const TApplies extends EffectApplication | undefined = undefined>(kind: TKind, spec: EffectSpec<TName, P, TTarget, TTime> & {
readonly params: NoPlaybackParams;
readonly applies?: TApplies;
} & EffectNativeSite<TNative> & EffectKindSpec<EffectKindTypesOf<TKind>, EffectRenderParams<P, TTime>, TTarget, TNative>): EffectDefinition<EffectKindBackendOf<TKind>, TName, P, TKind, TTarget, EffectKindRuntime<EffectKindTypesOf<TKind>, EffectRenderParams<P, TTime>, TTarget, TNative>, TTime, EffectDefinitionApplies<TApplies, TTarget, TKind>>
Define an effect: one value of one kind, on the kind's backend. The spec is
the core fields plus the kind's own, typed from `params`:
export const glow = defineEffect(domStyle, {
name: "glow",
params: { color: signal.of(colorType, color("#22d3ee")), spread: signal(18) },
style: ({ color, spread }) => ({ filter: `drop-shadow(0 0 ${spread}px ${color})` }),
});
declare module "@motionactor/core" { interface ActorEffects { "dom:glow": typeof glow } }
registerEffects([glow]);
card.fx.glow({ spread: 24 });defineEffect, class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor, function registerEffects(definitions: readonly AnyEffectDefinition[]): voidInstall effects by wire id. Call it in the effect's own module, after its
`defineEffect(...)` (and beside its `ActorEffects` augmentation when it
applies to an actor), so importing the effect installs it.
Every effect registers. One that can apply to an actor is also on every
actor's `fx`; one that cannot (a layer application, or one needing target
data) stays off `fx` and applies with `ctx.apply` on a target. A later
registration at a wire id replaces the earlier one. An actor effect named
`for` throws: `fx.for` is taken.registerEffects, 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";
import { const dom: BackendRef<"dom">The DOM backend's identity, at its own framework-free entry
(`@motionactor/renderers/dom-ref`). It lives here and not in
`@motionactor/dom`: DOM renderers are written in packages the DOM host
depends on (`@motionactor/primitives`, `@motionactor/media`), and the
contract already treats DOM specially (only DOM renderers bind `refs`).
It is not on the floor's root, so no binding entry re-exports a `dom` that
is not the backend: `@motionactor/dom` re-exports it as the ref, and its
`/react`, `/vue` and `/svelte` entries export their backends as `dom`.dom, const domStyle: EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that styles its actor's element with CSS:
export const blur = defineEffect(domStyle, {
name: "blur",
params: { strength: signal(8), progress: signal.of(unitInterval, 0) },
style: ({ strength, progress }) => ({ filter: `blur(${strength * progress}px)` }),
});
Framework-free: each binding's DOM backend pairs it with a stage that paints
`domStyleLayers` in its own elements.domStyle } from "@motionactor/dom";
import { const pixi: BackendRef<"pixi">The Pixi backend's identity, and the only place its name is spelled. This
entry pulls in no React, no Pixi and no host, so a scene file, an effect or
a renderer package can name the backend without mounting it.pixi, const pixiFilter: EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that is one Pixi filter on its actor (or a registered
target container). The short form is a pixi-filters class whose options and
props are the params, by name:
export const glow = defineEffect(pixiFilter, {
name: "glow",
params: { distance: signal(10), outerStrength: signal(4), color: signal<ColorSource>(0xffffff) },
filter: GlowFilter,
});
The full form, `filter: { create, update, destroy? }`, translates params the
filter does not take by name.pixiFilter } from "@motionactor/pixi";
import { class BlurFilterThe BlurFilter applies a Gaussian blur to an object.
The strength of the blur can be set for the x-axis and y-axis separately.BlurFilter } from "pixi.js";
const const domGlow: EffectDefinition<"dom", "glow", {
readonly spread: ActorFieldSignal<number>;
}, EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, unknown, DomStyleRender<EffectParamValues<{
readonly spread: ActorFieldSignal<number>;
}>>, false, "actor">
domGlow = defineEffect<"glow", {
readonly spread: ActorFieldSignal<number>;
}, EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, unknown, false, unknown, undefined>(kind: EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, spec: EffectSpec<"glow", {
readonly spread: ActorFieldSignal<number>;
}, unknown, false> & {
readonly params: NoPlaybackParams;
readonly applies?: undefined;
} & DomStyleSpec<EffectParamValues<{
readonly spread: ActorFieldSignal<number>;
}>>): EffectDefinition<...>
Define an effect: one value of one kind, on the kind's backend. The spec is
the core fields plus the kind's own, typed from `params`:
export const glow = defineEffect(domStyle, {
name: "glow",
params: { color: signal.of(colorType, color("#22d3ee")), spread: signal(18) },
style: ({ color, spread }) => ({ filter: `drop-shadow(0 0 ${spread}px ${color})` }),
});
declare module "@motionactor/core" { interface ActorEffects { "dom:glow": typeof glow } }
registerEffects([glow]);
card.fx.glow({ spread: 24 });defineEffect(const domStyle: EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that styles its actor's element with CSS:
export const blur = defineEffect(domStyle, {
name: "blur",
params: { strength: signal(8), progress: signal.of(unitInterval, 0) },
style: ({ strength, progress }) => ({ filter: `blur(${strength * progress}px)` }),
});
Framework-free: each binding's DOM backend pairs it with a stage that paints
`domStyleLayers` in its own elements.domStyle, {
EffectSpec<"glow", { readonly spread: ActorFieldSignal<number>; }, unknown, false>.name: "glow"Unique among the backend's effects. With the backend's name, the effect's wire identity.name: "glow",
params: {
readonly spread: ActorFieldSignal<number>;
} & NoPlaybackParams
Every param the kind's contribution reads, declared once as `signal(default)` or `signal.of(type, default)`.params: { spread: ActorFieldSignal<number>spread: 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(16) },
style: DomStyleRender<EffectParamValues<{
readonly spread: ActorFieldSignal<number>;
}>>
The effect's contribution to its actor's element, pure in its params.style: ({ spread: numberspread }) => ({ filter?: string | readonly string[] | undefinedCSS filter functions, appended after the folded hue/contrast/brightness.filter: `drop-shadow(0 0 ${spread: numberspread}px #f86e42)` }),
});
const const pixiGlow: EffectDefinition<"pixi", "glow", {
readonly strength: ActorFieldSignal<number>;
}, EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">, unknown, PixiFilterLifecycle<EffectParamValues<{
readonly strength: ActorFieldSignal<number>;
}>>, false, "actor">
pixiGlow = defineEffect<"glow", {
readonly strength: ActorFieldSignal<number>;
}, EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">, unknown, false, BlurFilter, undefined>(kind: EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">, spec: EffectSpec<"glow", {
readonly strength: ActorFieldSignal<number>;
}, unknown, false> & ({
readonly params: NoPlaybackParams;
readonly applies?: undefined;
} & PixiFilterSpec<...>)): EffectDefinition<...>
Define an effect: one value of one kind, on the kind's backend. The spec is
the core fields plus the kind's own, typed from `params`:
export const glow = defineEffect(domStyle, {
name: "glow",
params: { color: signal.of(colorType, color("#22d3ee")), spread: signal(18) },
style: ({ color, spread }) => ({ filter: `drop-shadow(0 0 ${spread}px ${color})` }),
});
declare module "@motionactor/core" { interface ActorEffects { "dom:glow": typeof glow } }
registerEffects([glow]);
card.fx.glow({ spread: 24 });defineEffect(const pixiFilter: EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that is one Pixi filter on its actor (or a registered
target container). The short form is a pixi-filters class whose options and
props are the params, by name:
export const glow = defineEffect(pixiFilter, {
name: "glow",
params: { distance: signal(10), outerStrength: signal(4), color: signal<ColorSource>(0xffffff) },
filter: GlowFilter,
});
The full form, `filter: { create, update, destroy? }`, translates params the
filter does not take by name.pixiFilter, {
EffectSpec<"glow", { readonly strength: ActorFieldSignal<number>; }, unknown, false>.name: "glow"Unique among the backend's effects. With the backend's name, the effect's wire identity.name: "glow",
params: {
readonly strength: ActorFieldSignal<number>;
} & NoPlaybackParams
Every param the kind's contribution reads, declared once as `signal(default)` or `signal.of(type, default)`.params: { strength: ActorFieldSignal<number>strength: 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(8) },
filter: PixiFilterClass<EffectParamValues<{
readonly strength: ActorFieldSignal<number>;
}>, Filter & BlurFilter>
filter: class BlurFilterThe BlurFilter applies a Gaussian blur to an object.
The strength of the blur can be set for the x-axis and y-axis separately.BlurFilter,
update: (filter: Filter & BlurFilter, params: EffectParamValues<{
readonly strength: ActorFieldSignal<number>;
}>) => void
update: (filter: Filter & BlurFilterfilter, { strength: numberstrength }) => {
filter: Filter & BlurFilterfilter.BlurFilter.strength: numberSets the strength of both the blurX and blurY properties simultaneously.
Controls the overall intensity of the Gaussian blur effect.strength = strength: numberstrength;
},
});
declare module "@motionactor/core" {
interface ActorEffects {
"dom:glow": typeof const domGlow: EffectDefinition<"dom", "glow", {
readonly spread: ActorFieldSignal<number>;
}, EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, unknown, DomStyleRender<EffectParamValues<{
readonly spread: ActorFieldSignal<number>;
}>>, false, "actor">
domGlow;
"pixi:glow": typeof const pixiGlow: EffectDefinition<"pixi", "glow", {
readonly strength: ActorFieldSignal<number>;
}, EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">, unknown, PixiFilterLifecycle<EffectParamValues<{
readonly strength: ActorFieldSignal<number>;
}>>, false, "actor">
pixiGlow;
}
}
function registerEffects(definitions: readonly AnyEffectDefinition[]): voidInstall effects by wire id. Call it in the effect's own module, after its
`defineEffect(...)` (and beside its `ActorEffects` augmentation when it
applies to an actor), so importing the effect installs it.
Every effect registers. One that can apply to an actor is also on every
actor's `fx`; one that cannot (a layer application, or one needing target
data) stays off `fx` and applies with `ctx.apply` on a target. A later
registration at a wire id replaces the earlier one. An actor effect named
`for` throws: `fx.for` is taken.registerEffects([const domGlow: EffectDefinition<"dom", "glow", {
readonly spread: ActorFieldSignal<number>;
}, EffectKind<"dom", "style", DomStyleTypes, {
readonly kind: "inline";
}, "actor">, unknown, DomStyleRender<EffectParamValues<{
readonly spread: ActorFieldSignal<number>;
}>>, false, "actor">
domGlow, const pixiGlow: EffectDefinition<"pixi", "glow", {
readonly strength: ActorFieldSignal<number>;
}, EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">, unknown, PixiFilterLifecycle<EffectParamValues<{
readonly strength: ActorFieldSignal<number>;
}>>, false, "actor">
pixiGlow]);
export 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: LayoutActor<ActorEventMap>card = ctx: SceneContextctx.CompileContext.spawn<LayoutActor<ActorEventMap>>(definition: ActorClass<LayoutActor<ActorEventMap>>, overrides?: {
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;
... 20 more ...;
parent?: undefined;
} | undefined, options?: {
...;
} | undefined): LayoutActor<...> (+1 overload)
spawn(class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor);
// @ts-expect-error: dom and pixi both define glow
const card: LayoutActor<ActorEventMap>card.Actor<ActorEventMap>.fx: ActorFxInstalled effects by name: `yield* this.fx.burnDissolve({ noiseScale: 7 }, { duration: 0.6 })`
plays one; `this.fx.glow({ spread: 24 })` keeps its handle. A name two
installed backends define is selected with `this.fx.for(dom).glow(...)`.
Applies only once spawned; the layer's backend is checked when bound.fx.glow();
yield* const card: LayoutActor<ActorEventMap>card.Actor<ActorEventMap>.fx: ActorFxInstalled effects by name: `yield* this.fx.burnDissolve({ noiseScale: 7 }, { duration: 0.6 })`
plays one; `this.fx.glow({ spread: 24 })` keeps its handle. A name two
installed backends define is selected with `this.fx.for(dom).glow(...)`.
Applies only once spawned; the layer's backend is checked when bound.fx.for<"dom">(backend: BackendRef<"dom">): FxFor<"dom">The effects `backend` defines, clashing names included: `card.fx.for(dom).glow(...)`.for(const dom: BackendRef<"dom">The DOM backend's identity, at its own framework-free entry
(`@motionactor/renderers/dom-ref`). It lives here and not in
`@motionactor/dom`: DOM renderers are written in packages the DOM host
depends on (`@motionactor/primitives`, `@motionactor/media`), and the
contract already treats DOM specially (only DOM renderers bind `refs`).
It is not on the floor's root, so no binding entry re-exports a `dom` that
is not the backend: `@motionactor/dom` re-exports it as the ref, and its
`/react`, `/vue` and `/svelte` entries export their backends as `dom`.dom).glow: (params: EffectFactoryInput<{
readonly spread: ActorFieldSignal<number>;
}, false>, playback: EffectPlaybackOptions) => Generator<Frame, void, unknown> (+3 overloads)
Apply when iterated and play it once.glow({ spread?: EffectParamInitial<ActorFieldSignal<number>> | undefinedspread: 24 }, { EffectPlaybackOptions.duration: numberduration: 0.4 });
}).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 });
Checked when it paints
TypeScript does not know which backend paints an actor: layer(dom).spawn(Card) has the same
menu as any actor. In every build, an effect for another backend throws before it paints,
naming the effect, the actor and both backends. On an actor in a declared layer, the call throws
as the scene compiles. Selecting with for(backend) does not skip the check.
Effect "glow" on card "card" is for backend "pixi", but the default layer is painted by "dom"
An offscreen effect (glShader, canvasPaint) applies to a child a view binds only when its
target contract clips, clips: true, so it paints inside the child’s rect and nowhere else.
The binding refuses any other with a BoundElementError, in every build, including one that
starts later. Bindings shows one.
Explicit targets
ctx.apply(target, effect(params?)) is the primitive under fx, and returns the same handle.
A scene uses it for what is not an actor, such as a target factory’s text range or mask, and
for a definition picked at runtime, such as one from an editor palette:
const highlight = ctx.apply(article.targets.range("lead"), textHighlight());
yield* highlight.play({ duration: 0.4 });
What a frame reports
actor.effects lists the effects active at the frame, in the order they were applied:
import { const scene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
}>
scene } from "./effect-glow.scene";
using using compiled: CompiledScene<{
card: Card;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled = const scene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
}>
scene.SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
card: Card;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 1280, SceneViewport.height: numberheight: 720 }, fps: numberfps: 30 });
const [const fx: ActiveEffectfx] = using compiled: CompiledScene<{
card: Card;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
card: Card;
}>
at(45).FrameState<{ ...; }>.scene: {
card: ResolvedActorOfInstance<Card> | undefined;
}
scene.card: ResolvedActorOfInstance<Card> | undefinedcard!.effects: readonly ActiveEffect[]effects;
const fx: ActiveEffectfx?.ActiveEffect.effectType: stringeffectType; // "glow"
const fx: ActiveEffectfx?.ActiveEffect.params: Record<string, unknown>params.unknownspread; // 48
const fx: ActiveEffectfx?.ActiveEffect.params: Record<string, unknown>params.unknownprogress; // 1
const fx: ActiveEffectfx?.ActiveEffect.progress: numberprogress; // 0.857, how far through the application frame 45 is
using compiled: CompiledScene<{
card: Card;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
compiled.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
card: Card;
}>
at(60).FrameState<{ ...; }>.scene: {
card: ResolvedActorOfInstance<Card> | undefined;
}
scene.card: ResolvedActorOfInstance<Card> | undefinedcard?.effects: readonly ActiveEffect[] | undefinedeffects; // [], detached at frame 51
fx.progress measures the application itself, from the call to detach, and is 1 for an
effect that is never detached. The progress param is the one play drives, so a style can
fade in once and hold.
Pixi and Three.js
The same pattern works on the canvas backends. A kind whose contribution names a class infers
the type of the native object it creates, so update gets a typed filter or pass:
import { function defineEffect<const TName extends string, const P extends EffectParamDeclarations, TKind extends AnyEffectKind, TTarget = unknown, const TTime extends boolean = false, TNative = unknown, const TApplies extends EffectApplication | undefined = undefined>(kind: TKind, spec: EffectSpec<TName, P, TTarget, TTime> & {
readonly params: NoPlaybackParams;
readonly applies?: TApplies;
} & EffectNativeSite<TNative> & EffectKindSpec<EffectKindTypesOf<TKind>, EffectRenderParams<P, TTime>, TTarget, TNative>): EffectDefinition<EffectKindBackendOf<TKind>, TName, P, TKind, TTarget, EffectKindRuntime<EffectKindTypesOf<TKind>, EffectRenderParams<P, TTime>, TTarget, TNative>, TTime, EffectDefinitionApplies<TApplies, TTarget, TKind>>
Define an effect: one value of one kind, on the kind's backend. The spec is
the core fields plus the kind's own, typed from `params`:
export const glow = defineEffect(domStyle, {
name: "glow",
params: { color: signal.of(colorType, color("#22d3ee")), spread: signal(18) },
style: ({ color, spread }) => ({ filter: `drop-shadow(0 0 ${spread}px ${color})` }),
});
declare module "@motionactor/core" { interface ActorEffects { "dom:glow": typeof glow } }
registerEffects([glow]);
card.fx.glow({ spread: 24 });defineEffect, 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";
import { const pixiFilter: EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that is one Pixi filter on its actor (or a registered
target container). The short form is a pixi-filters class whose options and
props are the params, by name:
export const glow = defineEffect(pixiFilter, {
name: "glow",
params: { distance: signal(10), outerStrength: signal(4), color: signal<ColorSource>(0xffffff) },
filter: GlowFilter,
});
The full form, `filter: { create, update, destroy? }`, translates params the
filter does not take by name.pixiFilter } from "@motionactor/pixi";
import { const threePass: EffectKind<"three", "pass", ThreePassTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that is a postprocessing `Effect` in the layer's
composer. No stage: the three host (`ThreeSceneEffectsWrapper`) collects
these into its `EffectComposer`.
export const noise = defineEffect(threePass, {
name: "noise",
params: { premultiply: signal(false), opacity: signal(0.4) },
pass: NoiseEffect,
options: { blendFunction: BlendFunction.SCREEN },
update: (effect, { premultiply, opacity }) => { … }, // effect: NoiseEffect
});
or, for a pass that needs the scene or selection to build,
`pass: { scope, create, update, destroy }`.threePass } from "@motionactor/three";
import { class BlurFilterThe BlurFilter applies a Gaussian blur to an object.
The strength of the blur can be set for the x-axis and y-axis separately.BlurFilter } from "pixi.js";
import { class BloomEffectA bloom effect.BloomEffect } from "postprocessing";
export const const blur: EffectDefinition<"pixi", "blur", {
readonly strength: ActorFieldSignal<number>;
}, EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">, unknown, PixiFilterLifecycle<EffectParamValues<{
readonly strength: ActorFieldSignal<number>;
}>>, false, "actor">
blur = defineEffect<"blur", {
readonly strength: ActorFieldSignal<number>;
}, EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">, unknown, false, BlurFilter, undefined>(kind: EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">, spec: EffectSpec<"blur", {
readonly strength: ActorFieldSignal<number>;
}, unknown, false> & ({
readonly params: NoPlaybackParams;
readonly applies?: undefined;
} & PixiFilterSpec<...>)): EffectDefinition<...>
Define an effect: one value of one kind, on the kind's backend. The spec is
the core fields plus the kind's own, typed from `params`:
export const glow = defineEffect(domStyle, {
name: "glow",
params: { color: signal.of(colorType, color("#22d3ee")), spread: signal(18) },
style: ({ color, spread }) => ({ filter: `drop-shadow(0 0 ${spread}px ${color})` }),
});
declare module "@motionactor/core" { interface ActorEffects { "dom:glow": typeof glow } }
registerEffects([glow]);
card.fx.glow({ spread: 24 });defineEffect(const pixiFilter: EffectKind<"pixi", "filter", PixiFilterTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that is one Pixi filter on its actor (or a registered
target container). The short form is a pixi-filters class whose options and
props are the params, by name:
export const glow = defineEffect(pixiFilter, {
name: "glow",
params: { distance: signal(10), outerStrength: signal(4), color: signal<ColorSource>(0xffffff) },
filter: GlowFilter,
});
The full form, `filter: { create, update, destroy? }`, translates params the
filter does not take by name.pixiFilter, {
EffectSpec<"blur", { readonly strength: ActorFieldSignal<number>; }, unknown, false>.name: "blur"Unique among the backend's effects. With the backend's name, the effect's wire identity.name: "blur",
params: {
readonly strength: ActorFieldSignal<number>;
} & NoPlaybackParams
Every param the kind's contribution reads, declared once as `signal(default)` or `signal.of(type, default)`.params: { strength: ActorFieldSignal<number>strength: 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(8) },
filter: PixiFilterClass<EffectParamValues<{
readonly strength: ActorFieldSignal<number>;
}>, Filter & BlurFilter>
filter: class BlurFilterThe BlurFilter applies a Gaussian blur to an object.
The strength of the blur can be set for the x-axis and y-axis separately.BlurFilter,
update: (filter: Filter & BlurFilter, params: EffectParamValues<{
readonly strength: ActorFieldSignal<number>;
}>) => void
update: (filter: Filter & BlurFilterfilter, { strength: numberstrength }) => {
filter: Filter & BlurFilterfilter.BlurFilter.strength: numberSets the strength of both the blurX and blurY properties simultaneously.
Controls the overall intensity of the Gaussian blur effect.strength = strength: numberstrength;
},
});
export const const bloom: EffectDefinition<"three", "bloom", {
readonly intensity: ActorFieldSignal<number>;
}, EffectKind<"three", "pass", ThreePassTypes, {
readonly kind: "inline";
}, "actor">, unknown, ThreePassLifecycle<EffectParamValues<{
readonly intensity: ActorFieldSignal<number>;
}>, Effect>, false, "actor">
bloom = defineEffect<"bloom", {
readonly intensity: ActorFieldSignal<number>;
}, EffectKind<"three", "pass", ThreePassTypes, {
readonly kind: "inline";
}, "actor">, unknown, false, BloomEffect, undefined>(kind: EffectKind<"three", "pass", ThreePassTypes, {
readonly kind: "inline";
}, "actor">, spec: EffectSpec<"bloom", {
readonly intensity: ActorFieldSignal<number>;
}, unknown, false> & ({
readonly params: NoPlaybackParams;
readonly applies?: undefined;
} & ThreePassSpec<...>)): EffectDefinition<...>
Define an effect: one value of one kind, on the kind's backend. The spec is
the core fields plus the kind's own, typed from `params`:
export const glow = defineEffect(domStyle, {
name: "glow",
params: { color: signal.of(colorType, color("#22d3ee")), spread: signal(18) },
style: ({ color, spread }) => ({ filter: `drop-shadow(0 0 ${spread}px ${color})` }),
});
declare module "@motionactor/core" { interface ActorEffects { "dom:glow": typeof glow } }
registerEffects([glow]);
card.fx.glow({ spread: 24 });defineEffect(const threePass: EffectKind<"three", "pass", ThreePassTypes, {
readonly kind: "inline";
}, "actor">
The kind of effect that is a postprocessing `Effect` in the layer's
composer. No stage: the three host (`ThreeSceneEffectsWrapper`) collects
these into its `EffectComposer`.
export const noise = defineEffect(threePass, {
name: "noise",
params: { premultiply: signal(false), opacity: signal(0.4) },
pass: NoiseEffect,
options: { blendFunction: BlendFunction.SCREEN },
update: (effect, { premultiply, opacity }) => { … }, // effect: NoiseEffect
});
or, for a pass that needs the scene or selection to build,
`pass: { scope, create, update, destroy }`.threePass, {
name: "bloom"name: "bloom",
params: {
intensity: ActorFieldSignal<number>;
}
params: { intensity: ActorFieldSignal<number>intensity: 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.5) },
pass: typeof BloomEffectpass: class BloomEffectA bloom effect.BloomEffect,
ThreePassFromClass<EffectParamValues<{ readonly intensity: ActorFieldSignal<number>; }>, Effect & BloomEffect>.update: (effect: Effect & BloomEffect, params: EffectParamValues<{
readonly intensity: ActorFieldSignal<number>;
}>, context: ThreeComposerEffectContext) => void
`effect` is the instance `pass` builds. A property, not a method, so a wrong annotation is an error.update: (pass: Effect & BloomEffectpass, { intensity: numberintensity }) => {
pass: Effect & BloomEffectpass.BloomEffect.intensity: numberThe bloom intensity.intensity = intensity: numberintensity;
},
});
Effects by wire id
The record carries the definition of an effect the scene imported. An app that names effects
without their definitions, such as an editor palette or a process that receives
{ backend: "dom", name: "glow" } over IPC, imports their modules. A render runtime resolves
every registered effect of its own backends:
import "./glow";
import { function createRenderRuntime<THost = unknown, TNode = unknown>({ backends, renderers, default: requestedDefault, }: CreateRenderRuntimeInput<THost, TNode>): RenderRuntime<THost, TNode>Compose backends (hosts, primitives, stages) and the renderers painted on
them. Each renderer routes to the backend it names, so one list can hold
DOM, three and redraw renderers side by side. Effects by wire id resolve
from the one effect registry (`registerEffects`), limited to these
backends: importing an effect installs it.
Backends are keyed by name, so a ref from a second copy of a backend
package in one bundle still finds its backend; two backends with one name
are an error.createRenderRuntime } from "@motionactor/renderers";
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";
export const const runtime: RenderRuntime<React.FC<ReactBackendHostProps>, React.ReactNode>runtime = createRenderRuntime<React.FC<ReactBackendHostProps>, React.ReactNode>({ backends, renderers, default: requestedDefault, }: CreateRenderRuntimeInput<React.FC<ReactBackendHostProps>, React.ReactNode>): RenderRuntime<React.FC<ReactBackendHostProps>, React.ReactNode>Compose backends (hosts, primitives, stages) and the renderers painted on
them. Each renderer routes to the backend it names, so one list can hold
DOM, three and redraw renderers side by side. Effects by wire id resolve
from the one effect registry (`registerEffects`), limited to these
backends: importing an effect installs it.
Backends are keyed by name, so a ref from a second copy of a backend
package in one bundle still finds its backend; two backends with one name
are an error.createRenderRuntime({ backends: readonly AnyBackend<React.FC<ReactBackendHostProps>, React.ReactNode>[]backends: [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] });
const runtime: RenderRuntime<React.FC<ReactBackendHostProps>, React.ReactNode>runtime.function allAvailableEffects(): EffectWireId[]Every registered effect of this runtime's backends, as wire ids, backend by backend.allAvailableEffects(); // [{ backend: "dom", name: "glow" }]
const runtime: RenderRuntime<React.FC<ReactBackendHostProps>, React.ReactNode>runtime.function resolveEffectBinding(id: EffectWireId): EffectBinding | undefinedA binding for the effect a wire id names, carrying its definition: what an
editor puts on an `ActiveEffect` it mints. `undefined` if none.resolveEffectBinding({ EffectWireId<string, string>.backend: stringbackend: "dom", EffectWireId<string, string>.name: stringname: "glow" });
const runtime: RenderRuntime<React.FC<ReactBackendHostProps>, React.ReactNode>runtime.function describeEffect(effect: AnyEffectDefinition | EffectWireId): EffectDescription | undefinedWhat an editor needs to offer an effect's params, derived from its
declarations (`describeEffect` in core). `undefined` for a wire id
`resolveEffect` does not resolve.describeEffect({ backend: stringbackend: "dom", name: stringname: "glow" });
A runtime without the dom backend resolves none of them.