API reference
The FieldHandle
Every entry point — createField frozen · @fundamental-engine/core, mountField, the
FieldField class, the <field-root> element (proxied), and
the React onReady — hands you a FieldHandle. Use it to drive the
field after it mounts.
How you get one
Every entry point returns the same handle — pick the one that matches your stack. With the web component it's also proxied onto the element, so the element is the handle.
import '@fundamental-engine/elements';
// the <field-root> element proxies the entire FieldHandle:
const field = document.querySelector('field-root');
field.setFormation('wells'); import { FieldField } from '@fundamental-engine/react';
// onReady hands you the live handle once the field mounts:
<FieldField onReady={(field) => field.setPalette('heatmap')} /> import { createField } from '@fundamental-engine/vanilla'; // wires the browser host
const field = createField(canvas); // ← createField returns the handle
field.setRender('links'); Methods
-
scan() - Re-scan the document for [data-body] bodies after a DOM change.
-
rescan() - Alias of scan().
-
setAccent(hex) - Recolor the travelling accent.
-
setPalette(name | hex[]) - Swap the accent color template live.
-
setFormation(name) - Switch the global formation.
-
setAttention(on) - Toggle conserved attention live (one finite strength budget).
-
setCausality(on) - Toggle cross-boundary causality live (density spills to neighbours).
-
setHeatmap(on) - Toggle the density heatmap layer live (a glow of where matter pools).
-
setDprCap(cap) / opt dprCap - Backing-store DPR ceiling (#410) — the dominant fill-rate lever. Effective DPR = min(devicePixelRatio, dprCap), default 2; capping at ~1.5 buys ~1.8x headroom on retina for a small softening. As a FieldOptions key (createField / <field-root dpr-cap>) or the runtime setDprCap setter (re-sizes immediately). Shipped-but-unfrozen.
-
setRender(mode) - Switch the underlay render mode (behind content): dots / trails / links / metaballs / voronoi / streamlines / flow.
-
setOverlay(mode | mode[]) - Field Surfaces: render overlay reading(s) in front of content — one reading or an additive stack (the readings compose). The vocabulary: streamlines / force-vectors / field-lines / grid / temperature / energy / path / data, or off. Pairs with setRender.
-
setBackground(mode) - Switch the substrate live: 'transparent' clears to transparent so the underlay composites over light content; 'opaque' restores the near-black substrate. Additive.
-
threads(list | null) - Wire glowing connector lines between an engaged set, or clear with null.
-
burst(x, y, hex?) - A one-shot shove + heat near a point, optionally tinting the matter.
-
flowTo(x, y, opts?) - Place/move a dynamic flow focus the field bends toward — pulls matter in and curves the streamlines. Retarget it each frame to follow the pointer, an element, or a path. opts: { strength?, radius? }.
-
clearFlow() - Remove the flow focus — the field relaxes back to its bodies-only shape.
-
seed(atoms) - Bind a data record to each base particle, round-robin. Each record's weight ∈ [0,1] scales that particle's mass + size. Re-applied across resize/density rebuilds.
-
atomAt(x, y) - The seeded record on the nearest particle to (x, y) within ~24 px, or null. For hover-to-inspect.
-
focusAt(x, y) - Hold + highlight the nearest seeded particle; return its record — the dwell affordance before a click. Returns null if no particle is in range.
-
clearFocus() - Release the focused particle; it resumes drifting.
-
particleCount()experimental - Live size of the particle pool. Use for external budget monitors or debug overlays without walking the particle array. Shipped-but-unfrozen.
-
energy()experimental - Per-frame energy snapshot: { kinetic, thermal, total, count }. Forwards to energyReport() without requiring a reference to the internal particle array. Shipped-but-unfrozen.
-
readParticles(out) - Copy live particle state into a caller-owned Float32Array (stride 5: x, y, z, heat, size — z is the optional depth lane, 0 in a flat field); returns the count written = min(particleCount(), floor(out.length/5)). Zero-alloc and read-only — the render-agnostic swarm read-out an alternative surface (e.g. @fundamental-engine/three) draws from. Shipped-but-unfrozen; the stride may widen (a color lane) before 1.0.
-
readParticleIds(out) - Copy each live particle's stable id into a Uint32Array, parallel to readParticles (same order, same agent skip), so ids[i] is the identity of the particle at stride offset i*5. Lets a host track a seeded entity across frames and key its own opaque payload off the id. Zero-alloc, read-only. Shipped-but-unfrozen.
-
sample(x, y) - The net field force a still test particle would feel at (x, y), as { x, y } in field-pixel space — every visible body superposed (wells, dipole structure, flow bias). Pure and read-only, samplable at any resolution; the seam external visualizers consume for vector grids, streamline tubes, or mesh displacement. Shipped-but-unfrozen.
-
sampleScalar(x, y) - The smooth diffused density scalar ∈ [0,1] at (x, y) — the heatmap grid, bilinear-sampled, so its gradient stays meaningful at a source (forage-by-gradient), unlike a nearest-body readout. Requires the heatmap layer (createField({ heatmap: true }) / setHeatmap(true)); returns 0 when off. Read-only, updated each frame including under render:none. Shipped-but-unfrozen.
-
sampleGradient(x, y) - The gradient ∇ {x,y} of the density field at (x, y) — direction + steepness (1/px) of increasing matter density. The analytic companion to sampleScalar, off the same diffused heatmap grid, so it stays non-degenerate at a source (a real uphill slope where a nearest-body density flattens to zero) — the cue reliable forage-/flee-by-gradient steers by. Requires the heatmap layer; returns { x: 0, y: 0 } when off or empty. Pure, read-only, maintained under render:none. Shipped-but-unfrozen.
-
grid(name) - Open a named host-authorable ScalarGrid — the engine field-buffer primitive (the same one diffuse/memory/propagate run on), promoted to a public surface for application fields the simulation composes with (a scent map, a wear/desire-path layer, a goal attractor). { sample, deposit, gradient, decay, clear } in field px. Created on first access, kept viewport-sized, advanced each frame by its mode (wave… = wave, memory… = slow decay, else diffuse); a same-named force shares the buffer. Shipped-but-unfrozen.
-
on(type, cb)experimental - Subscribe to a discrete field event — the host-agnostic push bus, for reacting to occurrences instead of polling feedback channels each frame. Returns an unsubscribe fn; plain data, no DOM. Events: absorb / release — a sink body captured / let go of matter (rising / falling edge of accretion), { body, count }. Lazy: a type with no listener costs nothing. (contact / settle / enter·exit are the next slice, #441.) Shipped-but-unfrozen.
-
addAgent(spec) - Add an engine-stepped agent — a participant the integrator MOVES (vs sample(), where you integrate yourself). It lives in the particle pool, so it feels every force the swarm feels (body forces AND particle-level hunt/align/cohesion); each step its report(p) fires so an external transform (a THREE.Object3D) follows it. spec: { x, y, z?, mass?, maxSpeed?, species?, report }. maxSpeed caps it, species lets tagged bodies (data-affects) steer it selectively; it edge-bounces (not wraps) and is excluded from readParticles. Returns { particle, remove() }. The creatures primitive @fundamental-engine/three’s layer.addAgent binds over. Shipped-but-unfrozen.
-
addBody(spec) - Add a programmatic body (no DOM) from a spec — the sanctioned alternative to the [data-body] scan for a non-DOM host (Three.js mesh, native view). { tokens, strength?, range?, spin?, angle?, color?, rect:()=>box, data?, onFeedback? }; rect() samples the box in field px each frame. The body carries a data record and takes per-body feedback (channels demuxed from the global sink); survives rescan. Returns { data, channels, set(params), remove() } — set({ strength?, range?, angle?, spin?, color? }) mutates the force params live on the measure cadence (no rescan, no remove+re-add; a token change still needs remove+addBody). Shipped-but-unfrozen.
-
addField(name, sampler) - Register a named field CHANNEL — an external scalar field the engine samples on its own read path (terrain height, soil moisture, a heat map). The open INPUT analog of the render surfaces (setRender/setOverlay are bundled output layers): instead of bolting a parallel grid alongside the field, hand it a pull-based sampler (x, y) => number and read it back through sampleField, so a consumer queries ONE field, not two. The sampler is called on demand (never cached) — keep it cheap. Returns a FieldChannelHandle { name, set(sampler), remove() } to swap the sampler live or unregister. (Force coupling — a force reading a channel as a potential — is a separate opt-in; this is the read substrate.) Shipped-but-unfrozen.
-
sampleField(name, x, y) - Sample a channel registered with addField at (x, y) in field-pixel space; returns 0 for an unregistered channel. Pure, read-only. Shipped-but-unfrozen.
-
scrollV()experimental - The engine's eased page-scroll velocity — the same EMA the scrolling condition gate reads: (prev × 0.7) + (|Δscroll| × 0.3) per frame. Units are px/frame at the display refresh rate (refresh-rate dependent — roughly half on 120 Hz; may normalize to px/ms before 1.0). Mirrored to --field-scroll-v on :root by the platform runtime. Pull-based: read on demand, don't poll in tight loops. Shipped-but-unfrozen.
-
setVisible(on) - Element-level visibility hint: setVisible(false) skips all draw work (render + overlay) each frame while the simulation and its feedback signals stay live — scrollV(), --d, --load, capture events keep flowing. Distinct from the tab-level pause (visibilitychange already stops the loop entirely). <field-root> wires it automatically from an IntersectionObserver on the host. Shipped-but-unfrozen.
-
destroy() - Stop the loop and release listeners.
On the element. When you use
<field-root>, these are proxied onto
the element itself — document.querySelector('field-root').setFormation('wells').
Diagnostics
Two read-only accessors give external tools — debug overlays, the DataConsole, Inspector panels — access to engine state that is otherwise private to the engine:
particleCount() → numberexperimental- Live size of the particle pool (
store.sizeforwarded). Use for budget monitors that need the count without walking the array (whichinspectBudgetdoes internally). energy() → { kinetic, thermal, total, count }experimental- Per-frame energy snapshot. Forwards to
energyReport(store.particles)— the function exists in@fundamental-engine/core/diagnostics/energy; this accessor exposes it without requiring a reference to the internal particle array.
Shipped-but-unfrozen. Both accessors ship today and are safe to use. They are not part
of the frozen
0.x contract — their signatures may refine before 1.0 as the
FieldPerf surface is designed. See the performance
docs for the full observability gap analysis.
A worked example
Drive the field from your own code — recolor it, reshape it, thread engaged elements together, react to events, and clean up:
const field = document.querySelector('field-root');
// recolor + reshape the whole field at runtime
field.setPalette('infrared');
field.setFormation('accretion');
field.setRender('metaballs');
// glowing threads between engaged elements (real Element refs, not selectors)
const a = document.querySelector('#node-a');
const b = document.querySelector('#node-b');
field.threads([{ a, b, color: '#4da3ff' }]);
// a one-shot shove + heat at the cursor
addEventListener('click', (e) => field.burst(e.clientX, e.clientY));
// re-scan after a route change adds or removes [data-body] elements
field.scan();
// release the loop + listeners when you're done
field.destroy(); import { FieldField } from '@fundamental-engine/react';
export function Background() {
// onReady hands you the same handle once the field mounts
return (
<FieldField
onReady={(field) => {
field.setPalette('infrared');
field.setFormation('accretion');
field.setRender('metaballs');
const a = document.querySelector('#node-a');
const b = document.querySelector('#node-b');
field.threads([{ a, b, color: '#4da3ff' }]);
addEventListener('click', (e) => field.burst(e.clientX, e.clientY));
}}
/>
);
} import { createField } from '@fundamental-engine/vanilla';
const field = createField(canvas); // the handle
// recolor + reshape the whole field at runtime
field.setPalette('infrared');
field.setFormation('accretion');
field.setRender('metaballs');
// glowing threads between engaged elements (real Element refs)
const a = document.querySelector('#node-a');
const b = document.querySelector('#node-b');
field.threads([{ a, b, color: '#4da3ff' }]);
// a one-shot shove + heat at the cursor
addEventListener('click', (e) => field.burst(e.clientX, e.clientY));
field.scan(); // re-scan after the DOM changes
field.destroy(); // release the loop + listeners