Effect kinds
Define an effect kind for SVG filters, with a stage that paints its effects.
An effect kind fixes what an effect of that kind supplies and what paints it. This page builds
svgFilter for the SVG backend: an effect supplies filter
primitives from its params, and a stage turns them into a <filter> around the actor.
The kind and its stage
import { function useId(): stringuseId, type type ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | null | undefinedRepresents all of the things React can render.
Where
{@link
ReactElement
}
only represents JSX, `ReactNode` represents everything that can be rendered.ReactNode } from "react";
import {
function defineEffectKind<const TBackend extends string, const TName extends string, K extends EffectKindTypes, const TRaster extends EffectRaster = {
readonly kind: "inline";
}, const TApplies extends EffectApplication = EffectApplication>(backend: BackendRef<TBackend>, spec: EffectKindDeclaration<K, TName, TRaster, TApplies>): EffectKind<TBackend, TName, K, TRaster, TApplies>
Define a kind of effect for a backend: what an author supplies for one
shape of effect, and how it lowers to what a stage or host reads. The stage
that paints it is the binding's, paired by the backend.
`types` names the type slots (`spec`, `runtime`) in terms of `this["params"]`:
export const pixiFilter = defineEffectKind(pixi, {
name: "filter",
types: effectKindTypes<PixiFilterTypes>(),
* applies: "actor",
lower: (spec) => (typeof spec.filter === "function" ? filterFromClass(spec.filter) : spec.filter),
});defineEffectKind,
const effectKindTypes: <K extends EffectKindTypes>() => EffectKindTypesTag<K>Names a kind's type slots (`spec`, `runtime`, in terms of `this["params"]`) for `defineEffectKind`.effectKindTypes,
const readEffectDefinition: (binding: EffectBinding | undefined) => AnyEffectDefinition | undefinedThe definition a binding carries; `undefined` for a bare wire id (JSON that crossed IPC).readEffectDefinition,
const resolveEffectParams: (effect: Pick<ActiveEffect, "binding" | "params" | "effectType">) => Record<string, unknown>The params render code reads for an `ActiveEffect`, however it was minted:
every declared param the effect leaves out takes its declared default, and
every typed one decodes (a colour string becomes a `Color`), the way
`ctx.apply` and the factories treat them. Params nothing declares pass
through. Returns `effect.params` itself when nothing changes.
Stages and hosts call it before render code runs, so an editor that mints
effects from `runtime.resolveEffectBinding(...)` gets the same values a
scene does.resolveEffectParams,
type EffectKindTypes,
type interface EffectStage<TNode = unknown>How a backend paints effects around an actor's content, in the binding's
node type (JSX for React). The element resolver decides geometry; the stage
decides pixels. A backend pairs each stage with the kinds it paints
(`createBackend(ref, { stages: [{ kinds, stage }] })`) and orders its stages
from those kinds' `raster.kind`: offscreen stages wrap the content first
(they need the subtree rendered before they run), inline stages wrap
outside them.EffectStage,
} from "@motionactor/core";
import { const svg: BackendRef<"svg">svg } from "./svg";
/** One SVG filter primitive, as data. */
export type type SvgPrimitive = {
readonly type: "feGaussianBlur";
readonly stdDeviation: number;
readonly result?: string;
} | {
readonly type: "feTurbulence";
readonly baseFrequency: number;
readonly numOctaves: number;
readonly result: string;
} | {
readonly type: "feDisplacementMap";
readonly in: string;
readonly in2: string;
readonly scale: number;
} | {
readonly type: "feMerge";
readonly inputs: readonly string[];
}
One SVG filter primitive, as data.SvgPrimitive =
| {
readonly type: "feGaussianBlur"type: "feGaussianBlur";
readonly stdDeviation: numberstdDeviation: number;
readonly result?: string | undefinedresult?: string;
}
| {
readonly type: "feTurbulence"type: "feTurbulence";
readonly baseFrequency: numberbaseFrequency: number;
readonly numOctaves: numbernumOctaves: number;
readonly result: stringresult: string;
}
| {
readonly type: "feDisplacementMap"type: "feDisplacementMap";
readonly in: stringin: string;
readonly in2: stringin2: string;
readonly scale: numberscale: number;
}
| { readonly type: "feMerge"type: "feMerge"; readonly inputs: readonly string[]inputs: readonly string[] };
/** The primitives of one filter, from an effect's params. */
export type type SvgPrimitives<P> = (params: P) => readonly SvgPrimitive[]The primitives of one filter, from an effect's params.SvgPrimitives<function (type parameter) P in type SvgPrimitives<P>P> = (params: Pparams: function (type parameter) P in type SvgPrimitives<P>P) => readonly type SvgPrimitive = {
readonly type: "feGaussianBlur";
readonly stdDeviation: number;
readonly result?: string;
} | {
readonly type: "feTurbulence";
readonly baseFrequency: number;
readonly numOctaves: number;
readonly result: string;
} | {
readonly type: "feDisplacementMap";
readonly in: string;
readonly in2: string;
readonly scale: number;
} | {
readonly type: "feMerge";
readonly inputs: readonly string[];
}
One SVG filter primitive, as data.SvgPrimitive[];
/** What an `svgFilter` effect supplies beside its params. */
export interface interface SvgFilterSpec<P>What an `svgFilter` effect supplies beside its params.SvgFilterSpec<function (type parameter) P in SvgFilterSpec<P>P> {
readonly SvgFilterSpec<P>.primitives: SvgPrimitives<P>primitives: type SvgPrimitives<P> = (params: P) => readonly SvgPrimitive[]The primitives of one filter, from an effect's params.SvgPrimitives<function (type parameter) P in SvgFilterSpec<P>P>;
}
export interface SvgFilterTypes extends EffectKindTypes {
readonly SvgFilterTypes.spec: SvgFilterSpec<this["params"]>spec: interface SvgFilterSpec<P>What an `svgFilter` effect supplies beside its params.SvgFilterSpec<this["params"]>;
readonly SvgFilterTypes.runtime: SvgPrimitives<this["params"]>runtime: type SvgPrimitives<P> = (params: P) => readonly SvgPrimitive[]The primitives of one filter, from an effect's params.SvgPrimitives<this["params"]>;
}
const const element: (primitive: SvgPrimitive, key: number) => JSX.Elementelement = (primitive: SvgPrimitiveprimitive: type SvgPrimitive = {
readonly type: "feGaussianBlur";
readonly stdDeviation: number;
readonly result?: string;
} | {
readonly type: "feTurbulence";
readonly baseFrequency: number;
readonly numOctaves: number;
readonly result: string;
} | {
readonly type: "feDisplacementMap";
readonly in: string;
readonly in2: string;
readonly scale: number;
} | {
readonly type: "feMerge";
readonly inputs: readonly string[];
}
One SVG filter primitive, as data.SvgPrimitive, key: numberkey: number) => {
switch (primitive: SvgPrimitiveprimitive.type: "feGaussianBlur" | "feTurbulence" | "feDisplacementMap" | "feMerge"type) {
case "feGaussianBlur":
return (
<React.JSX.IntrinsicElements.feGaussianBlur: React.SVGProps<SVGFEGaussianBlurElement>feGaussianBlur
React.Attributes.key?: React.Key | null | undefinedkey={key: numberkey}
React.SVGAttributes<SVGFEGaussianBlurElement>.stdDeviation?: string | number | undefinedstdDeviation={primitive: {
readonly type: "feGaussianBlur";
readonly stdDeviation: number;
readonly result?: string;
}
primitive.stdDeviation: numberstdDeviation}
React.SVGAttributes<SVGFEGaussianBlurElement>.result?: string | undefinedresult={primitive: {
readonly type: "feGaussianBlur";
readonly stdDeviation: number;
readonly result?: string;
}
primitive.result?: string | undefinedresult}
/>
);
case "feTurbulence":
return (
<React.JSX.IntrinsicElements.feTurbulence: React.SVGProps<SVGFETurbulenceElement>feTurbulence
React.Attributes.key?: React.Key | null | undefinedkey={key: numberkey}
React.SVGAttributes<SVGFETurbulenceElement>.baseFrequency?: string | number | undefinedbaseFrequency={primitive: {
readonly type: "feTurbulence";
readonly baseFrequency: number;
readonly numOctaves: number;
readonly result: string;
}
primitive.baseFrequency: numberbaseFrequency}
React.SVGAttributes<SVGFETurbulenceElement>.numOctaves?: string | number | undefinednumOctaves={primitive: {
readonly type: "feTurbulence";
readonly baseFrequency: number;
readonly numOctaves: number;
readonly result: string;
}
primitive.numOctaves: numbernumOctaves}
React.SVGAttributes<T>.result?: string | undefinedresult={primitive: {
readonly type: "feTurbulence";
readonly baseFrequency: number;
readonly numOctaves: number;
readonly result: string;
}
primitive.result: stringresult}
/>
);
case "feDisplacementMap":
return (
<React.JSX.IntrinsicElements.feDisplacementMap: React.SVGProps<SVGFEDisplacementMapElement>feDisplacementMap
React.Attributes.key?: React.Key | null | undefinedkey={key: numberkey}
React.SVGAttributes<SVGFEDisplacementMapElement>.in?: string | undefinedin={primitive: {
readonly type: "feDisplacementMap";
readonly in: string;
readonly in2: string;
readonly scale: number;
}
primitive.in: stringin}
React.SVGAttributes<SVGFEDisplacementMapElement>.in2?: string | number | undefinedin2={primitive: {
readonly type: "feDisplacementMap";
readonly in: string;
readonly in2: string;
readonly scale: number;
}
primitive.in2: stringin2}
React.SVGAttributes<SVGFEDisplacementMapElement>.scale?: string | number | undefinedscale={primitive: {
readonly type: "feDisplacementMap";
readonly in: string;
readonly in2: string;
readonly scale: number;
}
primitive.scale: numberscale}
/>
);
case "feMerge":
return (
<React.JSX.IntrinsicElements.feMerge: React.SVGProps<SVGFEMergeElement>feMerge React.Attributes.key?: React.Key | null | undefinedkey={key: numberkey}>
{primitive: {
readonly type: "feMerge";
readonly inputs: readonly string[];
}
primitive.inputs: readonly string[]inputs.ReadonlyArray<string>.map<JSX.Element>(callbackfn: (value: string, index: number, array: readonly string[]) => JSX.Element, thisArg?: any): JSX.Element[]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((input: stringinput, i: numberi) => (
<React.JSX.IntrinsicElements.feMergeNode: React.SVGProps<SVGFEMergeNodeElement>feMergeNode React.Attributes.key?: React.Key | null | undefinedkey={i: numberi} React.SVGAttributes<T>.in?: string | undefinedin={input: stringinput} />
))}
</React.JSX.IntrinsicElements.feMerge: React.SVGProps<SVGFEMergeElement>feMerge>
);
}
};
function function Filtered(props: {
primitives: readonly SvgPrimitive[];
children: ReactNode;
}): JSX.Element
Filtered(props: {
primitives: readonly SvgPrimitive[];
children: ReactNode;
}
props: {
primitives: readonly SvgPrimitive[]primitives: readonly type SvgPrimitive = {
readonly type: "feGaussianBlur";
readonly stdDeviation: number;
readonly result?: string;
} | {
readonly type: "feTurbulence";
readonly baseFrequency: number;
readonly numOctaves: number;
readonly result: string;
} | {
readonly type: "feDisplacementMap";
readonly in: string;
readonly in2: string;
readonly scale: number;
} | {
readonly type: "feMerge";
readonly inputs: readonly string[];
}
One SVG filter primitive, as data.SvgPrimitive[];
children: ReactNodechildren: type ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | null | undefinedRepresents all of the things React can render.
Where
{@link
ReactElement
}
only represents JSX, `ReactNode` represents everything that can be rendered.ReactNode;
}) {
const const id: stringid = function useId(): stringuseId();
return (
<>
<React.JSX.IntrinsicElements.filter: React.SVGProps<SVGFilterElement>filter React.SVGAttributes<SVGFilterElement>.id?: string | undefinedid={const id: stringid} React.SVGAttributes<SVGFilterElement>.x?: string | number | undefinedx="-50%" React.SVGAttributes<SVGFilterElement>.y?: string | number | undefinedy="-50%" React.SVGAttributes<SVGFilterElement>.width?: string | number | undefinedwidth="200%" React.SVGAttributes<SVGFilterElement>.height?: string | number | undefinedheight="200%">
{props: {
primitives: readonly SvgPrimitive[];
children: ReactNode;
}
props.primitives: readonly SvgPrimitive[]primitives.ReadonlyArray<SvgPrimitive>.map<JSX.Element>(callbackfn: (value: SvgPrimitive, index: number, array: readonly SvgPrimitive[]) => JSX.Element, thisArg?: any): JSX.Element[]Calls a defined callback function on each element of an array, and returns an array that contains the results.map(const element: (primitive: SvgPrimitive, key: number) => JSX.Elementelement)}
</React.JSX.IntrinsicElements.filter: React.SVGProps<SVGFilterElement>filter>
<React.JSX.IntrinsicElements.g: React.SVGProps<SVGGElement>g React.SVGAttributes<SVGGElement>.filter?: string | undefinedfilter={`url(#${const id: stringid})`}>{props: {
primitives: readonly SvgPrimitive[];
children: ReactNode;
}
props.children: ReactNodechildren}</React.JSX.IntrinsicElements.g: React.SVGProps<SVGGElement>g>
</>
);
}
/** Each effect gets its own filter, wrapped around the ones before it. */
export const const svgFilterStage: EffectStage<ReactNode>Each effect gets its own filter, wrapped around the ones before it.svgFilterStage: interface EffectStage<TNode = unknown>How a backend paints effects around an actor's content, in the binding's
node type (JSX for React). The element resolver decides geometry; the stage
decides pixels. A backend pairs each stage with the kinds it paints
(`createBackend(ref, { stages: [{ kinds, stage }] })`) and orders its stages
from those kinds' `raster.kind`: offscreen stages wrap the content first
(they need the subtree rendered before they run), inline stages wrap
outside them.EffectStage<type ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | null | undefinedRepresents all of the things React can render.
Where
{@link
ReactElement
}
only represents JSX, `ReactNode` represents everything that can be rendered.ReactNode> = {
EffectStage<React.ReactNode>.wrap: (context: EffectStageContext, node: ReactNode) => ReactNodeA property, not a method: its node input is checked contravariantly.wrap: ({ effects: readonly ActiveEffect[]The actor's applied effects this stage owns, in application order.effects }, node: ReactNodenode) =>
effects: readonly ActiveEffect[]The actor's applied effects this stage owns, in application order.effects.ReadonlyArray<ActiveEffect>.reduce<string | number | bigint | boolean | Iterable<ReactNode> | Promise<AwaitedReactNode> | JSX.Element | null | undefined>(callbackfn: (previousValue: string | number | bigint | boolean | Iterable<ReactNode> | Promise<AwaitedReactNode> | JSX.Element | null | undefined, currentValue: ActiveEffect, currentIndex: number, array: readonly ActiveEffect[]) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<AwaitedReactNode> | JSX.Element | null | undefined, initialValue: string | ... 7 more ... | undefined): string | ... 7 more ... | undefined (+2 overloads)Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((inner: ReactNodeinner, effect: ActiveEffecteffect) => {
const const definition: AnyEffectDefinition | undefineddefinition = function readEffectDefinition(binding: EffectBinding | undefined): AnyEffectDefinition | undefinedThe definition a binding carries; `undefined` for a bare wire id (JSON that crossed IPC).readEffectDefinition(effect: ActiveEffecteffect.ActiveEffect.binding?: EffectBinding<string, string, unknown, EffectRaster> | undefinedThe effect it applies, by wire id `{ backend, name }`; carries its definition when built in-process.binding);
const const primitives: SvgPrimitives<Record<string, unknown>> | undefinedprimitives = const definition: AnyEffectDefinition | undefineddefinition && const svgFilter: EffectKind<"svg", "filter", SvgFilterTypes, {
readonly kind: "inline";
}, "actor">
svgFilter.EffectKind<"svg", "filter", SvgFilterTypes, { readonly kind: "inline"; }, "actor">.runtimeOf(definition: AnyEffectDefinition): SvgPrimitives<Record<string, unknown>> | undefinedWhat this kind's stage or host runs for `definition`: its spec, lowered
when it was defined. `undefined` for another kind's definition.runtimeOf(const definition: AnyEffectDefinitiondefinition);
if (!const primitives: SvgPrimitives<Record<string, unknown>> | undefinedprimitives) return inner: ReactNodeinner;
return (
<function Filtered(props: {
primitives: readonly SvgPrimitive[];
children: ReactNode;
}): JSX.Element
Filtered primitives: readonly SvgPrimitive[]primitives={const primitives: (params: Record<string, unknown>) => readonly SvgPrimitive[]primitives(function resolveEffectParams(effect: Pick<ActiveEffect, "binding" | "params" | "effectType">): Record<string, unknown>The params render code reads for an `ActiveEffect`, however it was minted:
every declared param the effect leaves out takes its declared default, and
every typed one decodes (a colour string becomes a `Color`), the way
`ctx.apply` and the factories treat them. Params nothing declares pass
through. Returns `effect.params` itself when nothing changes.
Stages and hosts call it before render code runs, so an editor that mints
effects from `runtime.resolveEffectBinding(...)` gets the same values a
scene does.resolveEffectParams(effect: ActiveEffecteffect))}>
{inner: ReactNodeinner}
</function Filtered(props: {
primitives: readonly SvgPrimitive[];
children: ReactNode;
}): JSX.Element
Filtered>
);
}, node: ReactNodenode),
};
export const const svgFilter: EffectKind<"svg", "filter", SvgFilterTypes, {
readonly kind: "inline";
}, "actor">
svgFilter = defineEffectKind<"svg", "filter", SvgFilterTypes, {
readonly kind: "inline";
}, "actor">(backend: BackendRef<"svg">, spec: EffectKindDeclaration<SvgFilterTypes, "filter", {
readonly kind: "inline";
}, "actor">): EffectKind<"svg", "filter", SvgFilterTypes, {
readonly kind: "inline";
}, "actor">
Define a kind of effect for a backend: what an author supplies for one
shape of effect, and how it lowers to what a stage or host reads. The stage
that paints it is the binding's, paired by the backend.
`types` names the type slots (`spec`, `runtime`) in terms of `this["params"]`:
export const pixiFilter = defineEffectKind(pixi, {
name: "filter",
types: effectKindTypes<PixiFilterTypes>(),
* applies: "actor",
lower: (spec) => (typeof spec.filter === "function" ? filterFromClass(spec.filter) : spec.filter),
});defineEffectKind(const svg: BackendRef<"svg">svg, {
EffectKindDeclaration<SvgFilterTypes, "filter", { readonly kind: "inline"; }, "actor">.name: "filter"Unique among the backend's kinds.name: "filter",
EffectKindDeclaration<SvgFilterTypes, "filter", { readonly kind: "inline"; }, "actor">.types: EffectKindTypesTag<SvgFilterTypes>The kind's type slots: `effectKindTypes<PixiFilterTypes>()`.types: effectKindTypes<SvgFilterTypes>(): EffectKindTypesTag<SvgFilterTypes>Names a kind's type slots (`spec`, `runtime`, in terms of `this["params"]`) for `defineEffectKind`.effectKindTypes<SvgFilterTypes>(),
EffectKindDeclaration<SvgFilterTypes, "filter", { readonly kind: "inline"; }, "actor">.applies: "actor"What effects of the kind apply to unless they declare a `target`, kept literal (actor `fx` reads it).applies: "actor",
EffectKindDeclaration<SvgFilterTypes, "filter", { readonly kind: "inline"; }, "actor">.lower?: ((spec: SvgFilterSpec<Record<string, unknown>>) => SvgPrimitives<Record<string, unknown>>) | undefinedThe kind's spec lowered to its runtime form, once at definition time.
Absent, the spec is the runtime form: `K["spec"]` must then be `K["runtime"]`.
`native` is the kind's bound here: one `lower` serves every effect's class.lower: (spec: SvgFilterSpec<Record<string, unknown>>spec) => spec: SvgFilterSpec<Record<string, unknown>>spec.SvgFilterSpec<Record<string, unknown>>.primitives: SvgPrimitives<Record<string, unknown>>primitives,
});
types names two slots in terms of the effect’s own params. spec is what defineEffect takes
beside its name and params, and runtime is what the stage reads. lower turns one into the
other once, when the effect is defined. Here the effect gives a primitives function and the
stage calls it, so lower only unwraps it. The stage reads it back with
svgFilter.runtimeOf(definition), typed by the kind, which is undefined for another kind’s
effect.
A stage is one function, wrap(context, node). It gets the actor’s content and the effects it
owns, and returns the content wrapped in their treatment. The DOM style stage wraps it in a
styled element, and the Pixi filter stage in a container with filters. This one gives each
effect its own <filter> and <g>, so two filters on one actor stack in the order they were
applied. Filtered is a component so it can call useId, which keeps filter ids unique when
the same scene renders twice on a page.
The kind names no stage. The backend pairs them, stages: [{ kinds: [svgFilter], stage: svgFilterStage }], so the same kind could paint through a Vue stage under another binding. Here
both live in one React file for brevity; a backend shared across bindings keeps the kind in a
framework-free module. The ref in svg.ts stays React-free, and a scene that applies SVG
filters imports ./svg-filter.
A node a view binds to an actor has no wrapper to render.
An inline stage that can paint on one anyway adds node to its pairing: given the same
context, it returns the transform, filter, opacity and clipPath the node folds into its
own style, or unsupported naming what it would need. The DOM style stage has one. A binding
rejects an effect whose stage has none.
Effects
Every effect of the kind is a defineEffect call, typed from its params. The code beside the
figure is its scene: blur is one primitive, halo merges a blurred copy under the dot, and
wobble displaces the dot by a noise field. primitives returns SvgPrimitive[], so a
misspelled primitive or a missing in2 fails the type check.
Each effect registers beside its definition, keyed "svg:<name>", so every actor reads
dot.fx.blur(). filter.play drives progress from 0 to 1 as it does for a DOM effect, and
filter.progress.tween(0) brings it back. See Effects.
Raster
A kind also declares a raster. Inline, the default, paints over the actor as it is drawn.
Offscreen means the stage needs the actor’s pixels first, so offscreen stages wrap the content
before inline ones do. An SVG filter is inline: the browser rasterizes the filtered group
itself. Two kinds can share a stage if they agree on its raster; one kind pairs with one stage.
Effects that arrive by name
An effect a scene imports carries its definition, and readEffectDefinition returns it. An
effect that crossed a process boundary as { backend: "svg", name: "halo" } has lost it.
groupEffectsByStage rebinds it from the effect registry, where registerEffects([halo]) put it
when its module was imported, and skips it when nothing registered that wire id. A host that
reads effects itself, without a stage, rebinds the same way with registeredEffect(binding) from
@motionactor/core/internal.