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');

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() → number experimental
Live size of the particle pool (store.size forwarded). Use for budget monitors that need the count without walking the array (which inspectBudget does 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();