API reference
Platform utilities
Shipped helpers that sit below the main tutorial path but are load-bearing in real
applications. All seven are part of the stable platform surface. Most come from
@fundamental-engine/dom; headlessHost and
allocateAttention come from @fundamental-engine/core.
withFlip
The FLIP (First, Last, Invert, Play) animation primitive. Measures where elements sit, runs your DOM mutation, then translates each element from its old position to its new one and releases the offset under a transition — so re-sorts and re-parents read as travel, not teleportation.
Respects prefers-reduced-motion: reduce: the mutation still runs, only the
animation is skipped.
withFlip(elements, mutate, opts?)-
elementsis called twice — before and aftermutate— so a callback that re-queries the DOM naturally covers reparenting. Elements that only appear after the mutation (no first rect) are left alone. opts.axis-
'both'(default) — animatestranslate(dx, dy).'y'— animatestranslateY(dy)only, for single-column lists where horizontal delta is noise. opts.exclude-
(el: HTMLElement) => boolean— elements the mutation moved but that should NOT be translated (e.g. a tile whose size class changed, settled a different way).
import { withFlip } from '@fundamental-engine/dom';
// re-sort a card list with animated travel
withFlip(
() => [...grid.querySelectorAll('.card')] as HTMLElement[],
() => grid.append(...sortedCards), // the actual DOM mutation
{ axis: 'y', duration: 400 }
); allocateAttention
A conserved attention budget for a page. When any body is engaged (hovered,
focused, tapped), allocateAttention computes a per-body effective-strength
multiplier that boosts the engaged body while proportionally starving the others — because
the total is invariant, the field cannot emphasise two things at once.
The model is feed-forward on demand: idle bodies (nothing engaged) always receive a multiplier of 1, so a field that opts in is unchanged until something is actually engaged.
allocateAttention(inputs, opts?)-
Takes an array of
{ strength, on }items (one per body) and returns a parallel array of multipliers the integrator folds into each body's force magnitude. opts.beta-
Engagement multiplier β — how much harder an engaged body competes. Default
2. The engaged body's effective strength isstrength × (1 + β) × kwherek = ΣS / ΣMnormalises the total. opts.lo/opts.hi- Multiplier clamp floor / ceiling. Defaults:
0.25/3.
import { allocateAttention } from '@fundamental-engine/core';
// compute multipliers for three bodies on hover of the first
const muls = allocateAttention(
[
{ strength: 0.8, on: true }, // hovered
{ strength: 0.6, on: false },
{ strength: 0.4, on: false },
],
{ beta: 2 }
);
// muls[0] > 1 (competing harder), muls[1] + muls[2] < 1 (starved) threadOverlay
An absolutely-positioned, aria-hidden, pointer-events-none SVG overlay that
draws cubic-bezier center-to-center threads from a source element to one or
more targets. The invisible-fields example pages (Evidence, Backlog, Dependencies) all use
this pattern; threadOverlay is the common core extracted so callers write only
what to connect, not how the SVG lifecycle or geometry math works.
Geometry is sampled at draw time — callers must re-draw after layout changes (FLIP re-sorts, batch reveals, container resize).
threadOverlay(host, opts?)-
Creates the overlay and prepends it into
host(lazily on firstdraw). The host must be a containing block (position: relativeor similar). .draw(from, targets, opts?)-
Draws one bezier per target from the centre of
from. Marksfromwith.litand each target with.cited. Replaces the previous draw. Passopts.colorto set--threadon the overlay so CSS can style the stroke. .clear()- Empties the overlay's paths and removes every
lit/citedmark. .destroy()clear()+ remove the SVG from the host. A laterdraw()recreates it.
import { threadOverlay } from '@fundamental-engine/dom';
const overlay = threadOverlay(containerEl, { className: 'my-threads' });
card.addEventListener('pointerenter', () => {
const related = [...document.querySelectorAll('.related-card')] as HTMLElement[];
overlay.draw(card, related, { color: 'var(--accent)' });
});
card.addEventListener('pointerleave', () => overlay.clear()); CSS contract: style .my-threads path { stroke: var(--thread, #fff4); fill: none; }.
QualityGovernor
Detects sustained frame-budget overruns and emits a tier signal (0–3) so
the caller can adapt render quality without a hard cutoff. The <field-root>
element runtime uses it internally; expose it directly when building a custom platform loop.
- Tier semantics
-
0= full quality (default).1= simplify effects — drop heatmap, reduce overlay.2= minimal — switchrenderto'dots', halve particle cap.3= paused — suspend the field loop entirely. new QualityGovernor(budgetMs?)- Default budget:
16.67ms (60 fps). Pass8.33for a 120 fps budget. .feed(durationMs)-
Feed one rAF frame duration. Returns the new tier when it changes,
undefinedwhen stable. Escalation: 10 frames above 20 ms → tier 1; 5 frames above 33 ms → tier 2; 3 frames above 50 ms → tier 3. Recovery requires 30 clean frames to avoid thrashing. .reset()- Reset to tier 0 and clear streak counters. Call on
visibilitychangeto avoid false escalation on tab switches.
import { QualityGovernor } from '@fundamental-engine/dom';
const gov = new QualityGovernor(16.67);
let prev = performance.now();
function loop(now: number) {
const dur = now - prev; prev = now;
const tier = gov.feed(dur);
if (tier !== undefined) {
// tier changed — adapt render
field.setQualityTier(tier);
}
field.tick();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
document.addEventListener('visibilitychange', () => {
if (document.hidden) gov.reset();
}); textBodies
Annotates a text element with per-line or per-word body geometry. It
measures the element's rendered line boxes via Range.getClientRects(), emits one
absolutely-positioned, aria-hidden, pointer-events:none span per
box, and declares each span as a data-field-visual-for representation of the
source element — so the field's per-line/per-word geometry flows through the existing
[data-body] contract without a new engine surface.
This is box geometry, not glyph contours. True glyph-outline sampling is the
planned next slice of the textBodies roadmap and will slot in as a finer
granularity behind the same API.
textBodies(el, field, opts?)-
Returns a disposer. Calling it removes the spans and tears down any observer. Call
field.rescan()after annotating (and after disposing) so the engine picks up the new geometry. opts.granularity-
'line'(default) — one span per rendered line box.'word'— one span per word fragment box.'box'— rawgetClientRects()boxes. opts.observe-
true— wire a debouncedResizeObserveron the source that re-measures spans on resize. The observer is torn down by the disposer. - Lifecycle note
-
textBodies()is idempotent — re-calling disposes the previous span set first, then re-measures. Callfield.rescan()after both annotate and dispose.
import { textBodies } from '@fundamental-engine/dom';
import { createField } from '@fundamental-engine/vanilla';
const field = createField(canvas);
const heading = document.querySelector('h1')!;
const dispose = textBodies(heading, field, {
granularity: 'word',
observe: true, // re-annotate on resize
});
field.rescan(); // pick up the new spans immediately
// to teardown:
// dispose(); field.rescan(); headlessHost
The reference FieldHost for
non-DOM, non-visual consumers: an agent reading the field as a salience
substrate, a native sidecar, a Node.js service, or a deterministic test. Where
browserHost() binds the engine to window / document /
requestAnimationFrame, headlessHost() binds it to nothing — an
abstract volume the caller sets, a no-op scan root (bodies come via addBody, not
[data-body]), and a manual tick loop the caller drives.
Pair with render: 'none' (the signals-first default). The full simulation
runs and writes its signals; no DOM is touched.
headlessHost({ width, height })-
Returns a
HeadlessHost— aFieldHostwith one extra method:.tick(). host.tick()-
Advance one engine frame manually. Call per agent turn, on a schedule, or in a test loop.
In the browser context
tick()replacesrequestAnimationFrame. - Reading signals
-
Register bodies with
field.addBody({ …, onFeedback: (ch) => read(ch.density) })for per-body channels, or usefield.sampleScalar(x, y)/field.readParticles()for global sampling.
import { headlessHost } from '@fundamental-engine/core';
import { createField } from '@fundamental-engine/vanilla';
const host = headlessHost({ width: 1920, height: 1080 });
const field = createField(undefined, { host, render: 'none' });
const densities: number[] = [];
field.addBody({
tokens: ['attract'],
rect: () => ({ x: 500, y: 400, width: 200, height: 100 }),
onFeedback: (ch) => densities.push(ch.density),
});
// run 10 frames
for (let i = 0; i < 10; i++) host.tick();