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.

HTML
<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.

Data attributes are the contract. You make semantic HTML participate in the field by adding attributes to it — there is no special body element. <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:

discover → read → compute → state → write → render

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:

Per frame
// 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:

CSS
.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 priorityapplyRecipe()--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 a data-body runs.
  • 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:

  • Forceswhat a body does to nearby matter (attract, swirl, gravity, lens…). They compose. Reference →
  • Conditionswhen 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, memory decay).

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.

These are posters, not the detector. A cell uses a simplified centre-of-frame model so the motion reads at a glance. To fire a particle into the canonical force math and check it against its law, open the Lab.

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.

Composites, not new code. Bigger ideas — 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 agentsaddAgent(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-level hunt/align/cohesion — and each step its report(p) fires so an external object (a mesh, a label) can follow it. maxSpeed clamps it; it edge-bounces rather than wrapping; it's counted by particleCount() but excluded from readParticles().
  • Matter tagging — a body stamps the matter it emits with data-species and acts selectively via data-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 fieldsampleScalar(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 with createField({ heatmap: true }); it runs even under render: '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.