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 betweenIts exports also name types the module doesn't export: its internal types.
Classes
ProjectionSystem
The Environment's Projection system.
Extends
Context.Service<ProjectionSystem,Shape>
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
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
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
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
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
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
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