ProjectionSystem

The Projection system: what runs the DOM's layout animations, one per Environment a binding builds. A binding registers each layout element with it, and when a layout changes it measures their boxes, draws each where it was with a transform, and animates it to where it is now; elements that share a layoutId fly one into the other. This module is its key and its types, which every element's code reads: the engine itself is DomProjectionSystem's, so a page that never lays out ships none of it. toJSON() gives its tree as plain data, for a diagnostic view. A server render builds none.

import * as ProjectionSystem from "@wardic/dom/ProjectionSystem"

const system = yield* ProjectionSystem.ProjectionSystem
const node = system.register({ id: "card", element: card, transition: () => Option.none() })
system.animateLayout(() => expand(card)) // measured before and after, and animated between

Its exports also name types the module doesn't export: its internal types.

Classes

ProjectionSystem

The Environment's Projection system.

Extends

Interfaces

AnimateLayoutOptions

animateLayout's options.

Properties

from?
readonly from?: "call" | "lastPass";

Where the change animates from: the boxes at the call ("call", the default), or the last Measure pass's ("lastPass"), for a change a framework applied after the call, such as a write Solid 2 held, which no pass has seen yet. With "lastPass" the call takes no Measurements of its own: the pass it asks for compares with the boxes the last one kept.

onSettled?
readonly onSettled?: () => void;

Called once the change has settled: every layout animation it started has ended, however; at once after its pass if it started none.

Returns

void


CorrectorInfo

What an app's scale corrector is handed beside the value: the projection node's fields Motion's correctors read, as Motion names them. target is the box the node is drawn into this frame, treeScale its ancestors' scale, projectionDelta its delta.

Properties

projectionDelta
readonly projectionDelta: Delta;

Its delta, from its layout box to target.

target
readonly target: Box;

The box the node is drawn into this frame.

treeScale
readonly treeScale: Point;

Its ancestors' scale, multiplied together.


DragHandle

How a draggable layout node is dragged: through its projection, not its own x and y, which stay its animations', drawn on top. A binding's drag calls these, so the element follows the pointer and flies to its place at the release. Part of ProjectionNodeHandle.

Extended by

Properties

dragEnd
readonly dragEnd: (velocity?) => void;

End the drag: from the loop's next tick, the drag target's distance from where the layout puts the node springs to zero on each axis (a Release offset), starting at velocity (px/s; at rest when omitted) on top of the node's layout animation.

Parameters
velocity?

any

Returns

void

dragMove
readonly dragMove: (delta) => void;

The live pointer translate from the grab point — the node tracks grabBox + delta. Re-derives the dragged group into the sink at once, so the gate's next flush paints it.

Parameters
delta

Point

Returns

void

dragStart
readonly dragStart: () => void;

Begin a drag: capture the grab-time slot box as the pointer anchor, from where a stopped release left the node; hold the node active. A layout animation running at the grab runs on under the drag.

Returns

void

resumeRelease
readonly resumeRelease: () => void;

A press that ends with no drag: start a stopped release again, from rest.

Returns

void

stopRelease
readonly stopRelease: () => void;

A grab (a press on the element): stop a running release where it is, as Motion's drag stops its offset at the press. A drag that follows continues from there.

Returns

void


NodeHandle

Handle returned from ProjectionSystem.register.

Extends

Properties

dispose
readonly dispose: () => void;

Unregisters the node, ending an animation in flight.

Returns

void

dragEnd
readonly dragEnd: (velocity?) => void;

End the drag: from the loop's next tick, the drag target's distance from where the layout puts the node springs to zero on each axis (a Release offset), starting at velocity (px/s; at rest when omitted) on top of the node's layout animation.

Parameters
velocity?

any

Returns

void

Inherited from

DragHandle.dragEnd

dragMove
readonly dragMove: (delta) => void;

The live pointer translate from the grab point — the node tracks grabBox + delta. Re-derives the dragged group into the sink at once, so the gate's next flush paints it.

Parameters
delta

Point

Returns

void

Inherited from

DragHandle.dragMove

dragOffset
readonly dragOffset: () => Point;

How far the node paints from its slot while dragged (the drag target minus the slot, per axis, leaving out a layout animation the grab left running); { x: 0, y: 0 } otherwise, the release included.

Returns

Point

dragStart
readonly dragStart: () => void;

Begin a drag: capture the grab-time slot box as the pointer anchor, from where a stopped release left the node; hold the node active. A layout animation running at the grab runs on under the drag.

Returns

void

Inherited from

DragHandle.dragStart

layoutBox
readonly layoutBox: () => Option<Box>;

The node's slot (its layout box) in the Scroll-stable frame, as of its last Measure pass; none before the first. A plain read: no layout read.

Returns

Option<Box>

layoutVersion
readonly layoutVersion: () => number;

A count that rises with every Measure pass round that measures the node (a blocked one included); 0 before the first. A Reorder group waits on it after a reorder, as on Motion's former layoutVersion counter (removed in motion-dom 13.4.5).

Returns

number

notify
readonly notify: () => void;

Asks for a measure at the end of the task, however many ask: this node's layoutDependency changed, say.

Returns

void

resumeRelease
readonly resumeRelease: () => void;

A press that ends with no drag: start a stopped release again, from rest.

Returns

void

Inherited from

DragHandle.resumeRelease

setLifted
readonly setLifted: (on) => void;

A Keyboard reorder's lift: true raises the node as a drag does (2) while it's lifted, through the layout animations its steps start; false (its drop or cancel) leaves it raised (1) until its last one lands, as a drag's release does, or drops the raise now at rest.

Parameters
on

boolean

Returns

void

stopRelease
readonly stopRelease: () => void;

A grab (a press on the element): stop a running release where it is, as Motion's drag stops its offset at the press. A drag that follows continues from there.

Returns

void

Inherited from

DragHandle.stopRelease


RegisterNodeOptions

Options to register a layout element as a projection node.

Properties

crossfade?
readonly crossfade?: () => boolean;

Whether a handover onto this node crossfades (Motion's layoutCrossfade), read as each begins: false hides its Followers as it starts, and it moves at its own opacity. A Switch blends as ever. Left out, every handover crossfades.

Returns

boolean

dragTransition?
readonly dragTransition?: () => Option<DragTransition>;

The node's drag transition, read at each release: what its Release offset runs on, or None for Motion's, DEFAULT_DRAG_TRANSITION.

Returns

Option<DragTransition>

element
readonly element: HTMLElement;

The live DOM element to measure + project.

enteredFrom?
readonly enteredFrom?: any;

Where a presence child taking over from its leaver starts: the leaver's box in the Root frame, as it painted (ProjectionSystem.rootBoxOf). The node's first measure moves it from there to its own box, as a layout change, with no handover. A live twin to crossfade from comes first; this comes before a Switch seed.

fadeFrom?
readonly fadeFrom?: number;

Where the handover its first measure begins starts its fade: a keyed twin back while its leaver crossfades out goes on from the factor that leaver had faded to (ProjectionSystem.dropLeaver). Omitted, 0.

heldTransform?
readonly heldTransform?: () => string;

The transform a Measure pass holds the element at (Motion's transformTemplate(values, "")): its Transform template's with no transform of Wardic's, so a static part of it, such as centring, is in the boxes it flies between. Read at each pass. Without one, the pass clears it.

Returns

string

id
readonly id: string;

The element's id in the flush gate: the engine writes its projection there, beside its animated values.

layoutAnchor?
readonly layoutAnchor?: any;

Where in its parent the node is anchored, as Motion's layoutAnchor, from 0 to 1 on each axis: its top-left, { x: 0, y: 0 }, by default. Its box is kept relative to that point while its parent animates. false keeps it absolute.

layoutId?
readonly layoutId?: string;

Its shared-element key: nodes with the same layoutId are twins, and one flies into the other, the newest leading and the others following, crossfading. A twin removed in the same task as its twin arrives hands it where it was. Left out for a plain layout node.

layoutMode?
readonly layoutMode?: () => LayoutMode;

The node's Layout mode, read at each layout animation's start: which parts of a layout change it animates. Default true, position and size.

Returns

LayoutMode

layoutRoot?
readonly layoutRoot?: boolean;

Marks the element a layout root, as Motion's layoutRoot: it never animates its own layout change, landing on it at once, and its subtree's projections stop at it, so a change outside it doesn't move what's inside.

layoutScroll?
readonly layoutScroll?: boolean;

Marks the element a scroll container, as Motion's layoutScroll: its scroll is read at each measure and taken out of its descendants' boxes, so a scroll between two layout changes doesn't animate them; as a position: fixed scroll root, its style is read at every measure. Every other ancestor that can scroll is taken out too, unmarked.

onBeforeMeasure?
readonly onBeforeMeasure?: () => void;

Called as each Measure pass that measures the node begins (Motion's onBeforeLayoutMeasure): in its write phase, before its transform is held for the read, blocked passes included. A throw is reported, and the pass goes on.

Returns

void

onLayoutAnimationComplete?
readonly onLayoutAnimationComplete?: () => Option<() => void>;

The app's onLayoutAnimationComplete, read when it fires: at the frame where a layout animation of this node lands on its own. Not for one interrupted by a restart, finished early by a window resize, or cut off by an unmount; not for a Follower. None when the app has none.

Returns

Option<() => void>

onLayoutAnimationStart?
readonly onLayoutAnimationStart?: () => Option<() => void>;

The app's onLayoutAnimationStart, read when it fires: at the frame where a layout animation of this node takes its first step, once per animation (restarts in one task are one). Not for a node that is a Follower: a handover is its Lead's. None when the app has none.

Returns

Option<() => void>

onMeasure?
readonly onMeasure?: (box, prev) => void;

Called with the node's box each Measure pass measures it (Motion's onLayoutMeasure), after the read: in its Scroll-stable frame, Motion's page layout box, and the box before it, none at its first. Blocked passes included. A throw is reported, and the pass goes on.

Parameters
box

Box

prev

Option<Box>

Returns

void

onReleaseEnd?
readonly onReleaseEnd?: () => Option<() => void>;

The app's onDragTransitionEnd (Motion's), read when it fires: once a drag's release (its Release offset) comes to rest on its own, after that frame's flush; for a drag released in place at rest, which starts none, from a microtask after the release. Not for one a grab stopped and a drag took over, nor one an unmount or a resize ended. None when the app has none.

Returns

Option<() => void>

ownOpacity?
readonly ownOpacity?: () => number;

The element's own opacity (ownOpacityOf): what a Switch blends the leaver's opacity into, read each frame of one, and a leaver's when it's already off the page. Default 1.

Returns

number

ownRotate?
readonly ownRotate?: () => number;

The element's own rotate in degrees, which the renderer weaves into its projection (ownRotateOf): what a handover blends a twin's rotation into, read each frame of one. Default 0.

Returns

number

ownScale?
readonly ownScale?: () => Point;

The element's own scale per axis: its scale times its scaleX or scaleY, which the renderer draws on top of its projection (ownScaleOf). Read at each handover, and each frame a Follower is derived. Default { x: 1, y: 1 }.

Returns

Point

ownTranslate?
readonly ownTranslate?: () => Point;

The element's own translate: its x plus translateX and y plus translateY, in page pixels, which the renderer adds to its projection's (ownTranslateOf). Read where its own scale is. Default { x: 0, y: 0 }.

Returns

Point

path?
readonly path?: () => Option<Path>;

The element's Path, read at each layout animation's start: a flight of 20 px or more curves its centre along it, from where it starts to its box, its size still mixed straight, as Motion's interpolateProjection. None, or left out, and every flight is straight.

Returns

Option<Path>

raiseWhileDragged?
readonly raiseWhileDragged?: boolean;

Raises the node while it's dragged: from the drag's start until its release settles, at once for a release in place, its projection carries a raise the renderer draws as z-index, so it paints above its siblings, as Motion's Reorder items do; then one last write drops it. It's 2 while dragged and 1 once released, so the item under the pointer paints above one flying back. A Reorder item sets it.

reducedMotion?
readonly reducedMotion?: () => boolean;

Whether reduced motion applies to the node now, read at each layout animation's (re)start: a binding's setting for its subtree with the user's preference (Wardic's WardicConfig). Default: the system's reducedMotion.

Returns

boolean

respectReducedMotion?
readonly respectReducedMotion?: () => boolean;

Whether the node's layout animations honour reduced motion, read at each (re)start: false animates them whatever the system's reducedMotion says (a binding's per-transition opt-out). Default true.

Returns

boolean

skipAnimations?
readonly skipAnimations?: () => boolean;

Whether the node's animations are skipped now (a binding's skipAnimations), read at each layout animation's (re)start and each drag release: it lands at once, whatever reduced motion says, and a release lands on its slot. Default: never.

Returns

boolean

transition
readonly transition: () => Option<any>;

The node's layout transition, read at each layout animation's (re)start: a spring or a tween, or None for Motion's default, DEFAULT_LAYOUT_TRANSITION.

Returns

Option<any>


ScaleCorrector

An app's scale corrector (Motion's addScaleCorrector entry), given to a provider by the value key it corrects (borderRadius, --card-radius): correct turns the element's value into the one to write while its projection scales it, written under applyTo's keys when given, else its own.

Properties

applyTo?
readonly applyTo?: readonly string[];

The keys the corrected value is written under: its own key when left out.

correct
readonly correct: (latest, info) => string | number;

The value to write while the projection scales the element, from its own value, latest, and the node's fields.

Parameters
latest

string | number

info

CorrectorInfo

Returns

string | number


Tree

ProjectionSystem.toJSON's tree.

Properties

_id
readonly _id: "ProjectionSystem";

What it is, as Effect's Inspectable names it.

nodes
readonly nodes: readonly TreeNode[];

Every node, in the order it was registered.


TreeNode

One node in a Tree.

Properties

animating
readonly animating: boolean;

A layout animation in flight.

dragging
readonly dragging: boolean;

A drag in progress.

id
readonly id: string;

The id its binding registered it with.

layoutId
readonly layoutId: string | undefined;

Its layoutId, if it has one.

layoutRoot
readonly layoutRoot: boolean;

Whether it's a layout root.

lead
readonly lead: string | undefined;

A Follower's Lead's id.

leaving
readonly leaving: boolean;

Kept by a presence while it leaves.

parent
readonly parent: string | undefined;

Its parent's id, as the last Measure pass found it by DOM ancestry; undefined for a root.

releasing
readonly releasing: boolean;

A drag's release, running on its Release offset.

tag
readonly tag: string;

Its element's tag, lower-case.

twin
readonly twin: "lead" | "follower" | undefined;

A layoutId twin's part: the Lead, the newest live one, or a Follower; undefined for none.

Type Aliases

DragTransition

type DragTransition = Omit<Machine.InertiaOptions, "min" | "max">;

A drag's release (Motion's dragTransition): the inertia a node's Release offset runs on, with both edges at its slot, so it springs back there on the bounce. Times in seconds. Its edges are the slot's, never its own.


LayoutTransition

type LayoutTransition = object & Machine.SpringOptions | object & Machine.TweenOptions;

A layout animation's transition: a spring, its fields and its delay; or a tween, its duration, ease and delay. A repeat means nothing for a flight that lands once. A tween with no duration or ease takes the tween's own defaults, 0.3 s and linear (a binding fills Motion's easeOut first).


Options

type Options = ProjectionSystemOptions;

What a binding hands its engine: whether reduced motion applies now, and the app's scale correctors.


Services

type Services = ProjectionServices;

The services the engine needs from its Environment.


Shape

type Shape = Omit<ProjectionSystem, "dispose" | "onPassEnd">;

The engine: what a binding registers its layout elements with.


Sink

type Sink = ProjectionSink;

What the engine writes each node's projection through: a FlushGate's write and flushPass.

Variables

DEFAULT_DRAG_TRANSITION

const DEFAULT_DRAG_TRANSITION: DragTransition;

Motion's drag transition (framer-motion 13.4.0, VisualElementDragControls.startAnimation), with its default dragElastic: a 0.75 s time constant, power 0.8, the bounce 200 / 40, at rest within 1 px and 10 px/s. A node's Release offset runs on it when it's given none.


DEFAULT_LAYOUT_TRANSITION

const DEFAULT_LAYOUT_TRANSITION: LayoutTransition;

Motion's default layout transition (motion-dom 13.3.0, defaultLayoutTransition): a 0.45 s tween, eased cubic-bezier(0.4, 0, 0.1, 1), so a layout animation lands on time.

Functions

dragBoundsWithin()

function dragBoundsWithin(
   element, 
   container, 
   mapPoint?
): Bounds;

The bounds on element's x/y that keep it inside container (Motion's element dragConstraints): element measured without its own transform (its layout box, as Motion's projection measures it), container as it renders. With mapPoint (Motion's transformPagePoint), each box's corners are mapped through it first, as Motion's measurePageBox maps them: a container inside a zoomed parent bounds the element in its own pixels.

Parameters

element

HTMLElement

container

Element

mapPoint?

(point) => Point

Returns

Bounds


measureLayoutBox()

function measureLayoutBox(el): Bounds;

Measures el's box where the layout puts it: its own transform is cleared for the read and put back, so an animation running on it doesn't move the reading. Its ancestors' transforms still count. The box is in the viewport: x, y, width, height, and its edges.

Parameters

el

HTMLElement

Returns

Bounds

Example

const box = measureLayoutBox(card)

See

https://wardic.dev/reference/solid/layout/measure-layout-box