Authoring across surfaces

Four surfaces, one contract. A body authored in plain HTML, as a web component, in React, or in a Three.js scene is the same body in the field — each is a different way to reach the same [data-body] frozen · attribute contract contract. Pick the surface your stack uses; Fundamental does not ask you to adopt a framework.

A body, four ways

Each of these authors one attractor of strength 0.8. They produce identical bodies in the engine.

<!-- the page is the medium; any element can be a body -->
<canvas id="field"></canvas>

<article data-body="attract" data-strength="0.8">
  Pulls matter inward
</article>

<script type="module">
  import { createField } from '@fundamental-engine/vanilla'; // wires the browser host for you
  // mounts the field and scans the page for [data-body] elements
  createField(document.getElementById('field'));
</script>
The same body — attract, strength 0.8 — across all four surfaces. The Three.js door uses layer.addBody(mesh, spec) instead of data-body markup.

Live

These cards carry real data-body attributes, so the page's field reacts to them right now. Move your pointer near one and watch the field gather around it.

attractpulls matter inward
repelpushes matter away
swirlorbits matter around

The shared contract

Every surface writes the same data-* attributes. This is the contract:

AttributeMeaningExample
data-bodyforce tokens, space-separatedattract repel swirl
data-strengthsource mass / pull (default 0.5)0.8
data-rangereach in px (default 280)220
data-preseta named bundle of virtual bodiesdipole
data-intentauthored intent, compiled to forcesgather
data-field-rolesemantic role → a default forcesource, sink, anchor

Element consumers — react to the field

The contract above makes an element a force source. An element can instead consume the field — react to the matter around it (the element side of the Body Matter Interaction model). Each pairs with data-move so the engine may relocate or collapse the node.

HTML
<!-- capture: a chip that collapses into a sink when the field pulls it in -->
<span class="chip" data-move data-dock>tag</span>

<!-- relocate: an element that teleports to its paired exit through a warp throat -->
<div data-move data-warp data-pair="#exit" data-twist="0.5" data-scale="0.8">in</div>
<div id="exit"></div>

<!-- emit: clone this template node as the body emits matter -->
<button data-body="spawn" data-emit>
  <template><span class="spark">·</span></template>
</button>
AttributeBehaviorExample
data-dockcollapse into a sink when captured; fires field:captured / field:releaseddata-move data-dock
data-warpteleport to a paired body through a warp throat; fires field:relocateddata-warp data-pair="#exit" data-twist="0.5" data-scale="0.8"
data-emitclone a decorative template node as matter is emitteddata-emit

data-warp needs a paired body (data-pair) to relocate to — a single body can't show relocation. See the agent consumption model for the captured / relocated / emitted events.

Reading the field — CSS feedback channels

The engine writes CSS custom properties onto elements during the write phase of every frame. Add data-feedback to receive them; consume them in CSS to make an element visually react. No polling, no event listeners — the engine is a CSS property writer.

<!-- 1. Add data-feedback to receive --d and the other engine vars -->
<h1 data-body="attract" data-feedback class="hero-mass">
  The field is here
</h1>
--d is the canonical density var ∈ [0,1]. Scale it before driving properties — the raw fraction is small by design.

The full set of feedback channels — --d, --load, --lit, --entropy, --coherence, --temperature — is in the writeback reference. Sink accretion is a common pattern — --load fills from 0 (empty) to 1 (saturated):

<!-- a sink body; data-absorb starts accretion, data-max caps it -->
<ul data-body="sink" data-absorb data-max="40" data-feedback class="queue">
  <!-- items dock here when captured -->
</ul>

Correctness note: drive reactions off --d, not --field-density. When a pattern's metric pipeline runs, it also writes --field-density as a pattern output — so on pattern elements, --field-density holds the pattern metric, not the engine's raw particle density. --d is always the engine value.

Conditional forces — data-when

A force can activate only while a named condition holds, by adding data-when to the body element. The condition is evaluated by the engine each frame; the force is multiplied by 0 when the condition is false, 1 when true. No JavaScript required.

HTML
<!-- data-when gates the force on a named condition -->
<nav data-body="attract" data-when="active">
  <!-- force activates only while this element is hovered / focused -->
</nav>

<!-- 'slow' = --field-scroll-v < 2 (reading pace); force sleeps during fast scroll -->
<section data-body="swirl" data-strength="0.5" data-when="slow">
  ...
</section>

<!-- combine data-hot + data-when: hover triggers, condition gates -->
<button data-hot data-body="attract" data-spin="1" data-when="active">
  hover me
</button>

Built-in conditions: active (the element is hovered/focused), slow (--field-scroll-v < 2 — reading pace), idle (no pointer activity in the last second), visible (intersection > 0.1). The full list is on the conditions reference.

Formations — setFormation + data-formation

A formation shapes where the field's particles want to be — a global geometry imposed on top of the local force field. Formations are page-scope and transition smoothly.

<!-- divide the page into named sections; a formation snaps particles to each -->
<section data-formation="arc">
  <h2>chapter one</h2>
</section>

<section data-formation="ring">
  <h2>chapter two</h2>
</section>
Declarative: scroll drives formation changes. Imperative: setFormation() switches on any event.

The built-in formations — arc, ring, wells, cascade, drift, and more — are on the formations reference. Formations coexist with local body forces; the field blends both.

Beneath the surfaces: the platform

The surfaces above author bodies. When you need the page itself to participate — to measure elements, hold state, and write feedback on a disciplined loop — reach for createFieldPlatform() frozen · @fundamental-engine/dom from @fundamental-engine/dom directly. It is framework-agnostic; the same code runs under any surface.

TypeScript
import { createFieldPlatform } from '@fundamental-engine/dom';

// the substrate beneath every surface: measure → state → write, on one scheduler
const platform = createFieldPlatform(document.querySelector('article'));
const card = document.querySelector('[data-body]');

platform.measure.register(card);                          // read phase
platform.feedback.bind(card, { attention: '--field-attention' }); // write phase
platform.on('compute', () => {                            // compute phase
  const m = platform.measure.for(card);
  if (m) platform.state.set(card, 'attention', m.visibilityRatio);
});

const tick = (t) => { platform.tick(t); requestAnimationFrame(tick); };
requestAnimationFrame(tick);
The measure → state → write loop, on the six-phase scheduler. See the Reading Field demo for a full page built on it.