Concepts
How the field thinks.
Fundamental is a relational field runtime for the DOM. Elements are bodies in one shared field context — they bend the field; its density bends them back. A renderer-agnostic core computes the field; a platform layer binds it to the DOM. The visible canvas is one render surface, not the whole system.
Bodies
A body is any element you mark with data-body. Its value is
one or more force tokens, space-separated — they compose, so a single element
can attract and swirl at once. The engine re-reads each body's bounding box every frame,
so moving the element moves the force.
<a
data-body="attract swirl" // forces compose, space-separated
data-strength="0.9"
data-range="320"
data-color="#4da3ff"
data-when="active" // gate: only acts while engaged
data-feedback // opt into density write-back
>link</a>
Common attributes — data-strength, data-range,
data-color, data-spin, data-angle — are read by the
forces that use them; each force ignores the ones it doesn't. See the
attribute reference for the full matrix.
<field-root> only hosts the
field; applyRecipe() frozen · @fundamental-engine/platform and bindData() frozen · @fundamental-engine/platform adapt it to recipes and data; and the
<field-cell> frames lower down are just isolated single-force microscopes. The
system is the markup, not the components.
The shared field context
Bodies, particles, relationships, measurements, metrics, feedback, and overlays all share one
field context. The visible particle layer is a conserved pool (~130 at default density) drawn
on a full-viewport <canvas> — one render surface among several (SVG overlays
and DOM feedback are others). Particles are one agent type, not the whole substrate.
The conserved pool carries a central law: nothing is created from nothing —
matter is moved, captured, and released; a sink that swallows particles must later release
them, so the count stays constant. That is what makes the field feel physical rather than
decorative.
The platform layer
@fundamental-engine/core (the core) computes the field against plain data and stays renderer-agnostic.
@fundamental-engine/platform binds it to the DOM through six registries — measurement
(frame-stable geometry), state (typed element state), feedback
(CSS variables + thresholded events), relationships (the native href/aria/data
graph), visual bindings (visual ↔ semantic pairing), and overlays
(render layers) — all driven by one frame scheduler with explicit phases:
Reads happen in the read phase and writes in the write phase, so geometry reads never thrash
against DOM writes. Since the runtime-unification phase the platform is the default for
<field-root>; the core engine still renders the canvas. See the
platform layer for the registries, scheduler, and lint.
The DOM ⇄ field runtime binding
The rarest property of the system is a two-way binding between layout and a physics runtime. Fundamental is not a particle background: those are one-way (a canvas reacts to the mouse). Here the DOM is the runtime's geometry, and the runtime writes back into layout and type — a shared field context, not a decorative layer. Two flows, on the scheduler's read and write phases:
// read phase — DOM → field (platform MeasurementRegistry, once per frame, per body)
cx = (rect.left + rect.width / 2) * DPR
cy = (rect.top + rect.height / 2) * DPR
// write phase — field → DOM (platform FeedbackRegistry, per data-feedback body)
element.style.setProperty('--field-density', density) // d ∈ [0, 1]
// (the compact --d is written too, as an alias) Read (geometry in): any DOM change — reflow, transform, resize — is an
instantaneous change to the force geometry, so animating the DOM animates the runtime for free.
Write (feedback out): the field samples how much matter pooled on each
data-feedback body and writes it back as --field-density (with the
compact --d as an alias).
Reciprocity & --field-density
Reciprocity is the payoff. Opt a body in with data-feedback and the FeedbackRegistry
eases the gathered density into a CSS custom property --field-density ∈ [0, 1] on the
element (the compact --d is written alongside it as an
alias). Your styles turn that number into anything — weight, glow, scale, color:
.headline {
/* the field writes --field-density; CSS turns density into weight + glow.
--d is a compact legacy alias; new code should prefer --field-density. */
font-variation-settings: 'wght' calc(380 + var(--field-density, 0) * 420);
text-shadow: 0 0 calc(var(--field-density, 0) * 40px) rgba(77, 163, 255, var(--field-density, 0));
} The value is low-pass filtered (a ~0.2s time constant), so it eases rather than jitters. This is the sanctioned way to make type react — words gain weight where matter collects. (The engine never assembles particles into letterforms; words glow and grow, they are not drawn from dots.)
Metrics, and the lanes that stay separate
Density is one channel. The field also computes higher-level metrics — attention,
priority, coherence, entropy — and writes each back as a --field-<metric> custom
property. A recipe declares which metrics it tracks; the runtime writes
them. Below, real elements carry a runtime token (data-body="attract") and a supplied
priority; applyRecipe() writes it back as --field-priority,
and the CSS reads it — no canvas, no cursor needed:
Primary — ship the runtime
Secondary — write the guide
Later — tidy the changelog
data-body="attract" + supplied priority → applyRecipe() →
--field-priority → this element
Each row supplies a different value, so the field's feedback is visible at a glance — the readout shows what the field actually wrote back. The vocabulary works because each word lives in exactly one lane, and the lanes never bleed into each other:
- Concept describes (priority, polarity, binding) — it is never executed.
- Token executes (
attract,gravity) — the only thing adata-bodyruns. - Metric measures (
--field-priority,--field-attention) — state the field writes back, not a force. - Diagnostic explains (potential, topology, causality) — it inspects behavior, it does not cause it.
- Condition activates (
data-when="active") — when a token is allowed to act.
So: a concept is not a token, a token is not a metric, and a diagnostic is not behavior. The recipe model composes these lanes without ever inventing a new force.
The orthogonal axes
The vocabulary stays small by separating four independent dimensions:
- Forces — what a body does to nearby matter (attract, swirl, gravity, lens…). They compose. Reference →
- Conditions — when a force acts, via
data-when(active, fast, slow, hot, cool, scrolling). Reference → - Formations — a single global bias applied to every particle at once (ambient, wells, lanes, scatter, accretion). Reference →
- Render modes — the same physics drawn differently. Matter/structure: dots, trails, links, metaballs, voronoi, streamlines, field-lines, heatmap. Diagnostics (all shipped): force-vectors, contours, potential, energy, topology, inspector, causality, prediction. Diagnostics →
The natural model: four fields
Fundamental does not copy physics into the interface — it translates the four fundamental fields into interface behavior. Every interface already has priority, polarity, binding, and transformation:
- Gravity → priority, convergence, hierarchy (
gravity, potential). - Electromagnetic → polarity, signal, field lines, flow (
charge,magnetism,propagate,fieldflow). - Strong → binding, cohesion, structure (
cohesion,link,crystallize). - Weak → transformation, decay, release (
morph,memorydecay).
Natural fields are conceptual; engine primitives are translations; canonical forces
(attract, repel, …) are designed verbs, not natural translations. The
full model is on the Natural Fields page.
See the primitives move
Each frame below is a live <field-cell> running one force — the
lightweight demo surface with its own small particle pool, not the page field. Move your
cursor through a frame to push the matter around.
Agents: particles, elements, events
A force doesn't only push particles — it produces an influence at a location,
and whatever sits there decides how to consume it. A particle consumes it as
motion (the lightest agent); a DOM element consumes it as a transform offset
with an anchor tether; an event sink consumes a density threshold by firing a
CustomEvent you can listen for. One influence, three kinds of consumer — so the
field can drive real app behaviour, not just pixels.
Fundamental calls this the Field Agent Consumption Model. A single DOM body can
play several roles at once — force source, density receiver, force target, and event host. Its
capture-hold-release behaviour (the sink token, data-absorb,
data-max, --load) is the Body Matter Interaction submodel —
sink is the token; absorb is concept language, not a runtime token.
blackhole, galaxy,
tornado — are presets that expand to several
co-located bodies, one primitive each. The engine never grows to add a preset.
Engine-stepped agents, tagged matter, and the scalar field
Three engine capabilities turn the swarm from a backdrop into a population you can program. They live in the core, so they work under any renderer — and the Three.js layer binds a mesh over each one.
- Engine-stepped agents —
addAgent(spec)experimental adds a participant the integrator moves (vs an agent that samples the field and integrates itself). It lives in the conserved pool, so it feels every force the swarm feels — body forces and the particle-levelhunt/align/cohesion— and each step itsreport(p)fires so an external object (a mesh, a label) can follow it.maxSpeedclamps it; it edge-bounces rather than wrapping; it's counted byparticleCount()but excluded fromreadParticles(). - Matter tagging — a body stamps the matter it emits with
data-speciesand acts selectively viadata-affects(a species set). Matter outside the set feels no force and is skipped in the density sample, so several ecologies — pollen, seeds, spores — share one field with no cross-talk. Tags are read once at scan, allocation-free per frame. - The scalar field —
sampleScalar(x, y)experimental returns the smooth diffused density ∈ [0, 1] at a point (the heatmap grid, bilinear-sampled). Its gradient stays meaningful right at a source — unlike a nearest-body readout — so a creature can forage by gradient, always reading which way is "more". Enable it withcreateField({ heatmap: true }); it runs even underrender: 'none'.
These extend the consumption model rather than adding forces: an agent is a particle the engine
drives, tagging scopes which matter a force sees, and the scalar is a read channel —
no new data-body verb. The FieldHandle reference has
the exact signatures.