@fundamental-engine/elements 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,9 +49,10 @@ each frame — style with them (`var(--field-density)`, etc.) to make content re
49
49
  ## `<field-root>` attributes
50
50
 
51
51
  `accent` · `density` · `waves` · `render` (`dots` / `trails` / `links` / `streamlines` / `metaballs` /
52
- `voronoi`) · `palette` (`ours` / `heatmap` / `infrared` / `spectrum`) · `mass` · `attention` ·
53
- `causality`. The engine ships 16 render modes in all (including `field-lines`, `heatmap`, and the
54
- diagnostics); the others are reached through `setRender()` / the core, not this attribute.
52
+ `voronoi` / `knockout` / `redshift` / `blackbody` / `depth`) · `palette` (`ours` / `heatmap` /
53
+ `infrared` / `spectrum`) · `mass` · `attention` · `causality`. The engine ships 20 render modes in
54
+ all (including `field-lines`, `heatmap`, and the diagnostics); the others are reached through
55
+ `setRender()` / the core, not this attribute.
55
56
 
56
57
  ## Methods — the `FieldHandle`, proxied onto the element
57
58
 
@@ -95,6 +96,14 @@ const field = mountField({ render: 'trails', accent: '#2dd4bf' });
95
96
  formation inside a frame), separate from the page-wide `<field-root>`. It runs a deliberately
96
97
  simplified in-frame model, not the canonical engine math.
97
98
 
99
+ > **Boundary statement — `<field-cell>` is a demo pool, not the engine.** A cell owns its own
100
+ > particle pool, canvas, and lifecycle, seeded with the host's local `Math.random()`. It carries
101
+ > **none of the core guarantees**: no determinism, no snapshot/causal replay, no cross-plane
102
+ > conformance, and no physics parity with `@fundamental-engine/core` or the Swift/Kotlin ports. Its
103
+ > numbers are illustrative, not a source of truth — never assert against a cell in a conformance or
104
+ > replay test. When you need the real engine (deterministic, replayable, parity-held), use
105
+ > `<field-root>` or `createField()`, not `<field-cell>`.
106
+
98
107
  ## Framework use
99
108
 
100
109
  The custom elements work unchanged in React, Vue, Svelte, Solid, Angular, or plain HTML — register once
@@ -84,7 +84,7 @@
84
84
  "declarations": [
85
85
  {
86
86
  "kind": "class",
87
- "description": "`<field-cell force=\"swirl\" color=\"#2dd4bf\">` — an in-frame field surface (§25.1).\n\nA standalone field sized to its container that renders *one* force, with its own\nlightweight particle pool, its own pointer interaction, and a lifecycle that pauses\nwhen off-screen. It is **not** the §-engine: it's a lighter \"demo/poster\" engine,\ncompletely separate from the page `<field-root>` and the core field loop.\n\n- `ResizeObserver` re-fits the canvas (DPR-aware) and rebuilds the pool.\n- `IntersectionObserver` gates the rAF loop — paused when off-screen.\n- Honours `prefers-reduced-motion`: renders one static frame, no animation.",
87
+ "description": "`<field-cell force=\"swirl\" color=\"#2dd4bf\">` — an in-frame field surface (§25.1).\n\nA standalone field sized to its container that renders *one* force, with its own\nlightweight particle pool, its own pointer interaction, and a lifecycle that pauses\nwhen off-screen. It is **not** the §-engine: it's a lighter \"demo/poster\" engine,\ncompletely separate from the page `<field-root>` and the core field loop.\n\n- `ResizeObserver` re-fits the canvas (DPR-aware) and rebuilds the pool.\n- `IntersectionObserver` gates the rAF loop — paused when off-screen.\n- Honours `prefers-reduced-motion`: renders one static frame, no animation.\n\n**Budgets & isolation (shadow-dom.md §31.19).** Each cell owns its *own* pool, so\nmany cells on a docs page never share or starve one budget. Hard caps keep that\nbudget cheap: `max-particles` clamps the pool ceiling (over both the auto-size and an\nexplicit `count`), and `fps` throttles the animation loop to a target framerate. Both\ncaps are enforced per instance — a saturating cell cannot spill particles or frame\ncost into a neighbour.",
88
88
  "name": "FieldCell",
89
89
  "members": [
90
90
  {
@@ -177,6 +177,16 @@
177
177
  "privacy": "private",
178
178
  "default": "null"
179
179
  },
180
+ {
181
+ "kind": "field",
182
+ "name": "lastStepTs",
183
+ "type": {
184
+ "text": "number"
185
+ },
186
+ "privacy": "private",
187
+ "default": "0",
188
+ "description": "rAF timestamp of the last rendered frame — drives the `fps` throttle (§31.19)."
189
+ },
180
190
  {
181
191
  "kind": "field",
182
192
  "name": "onPointerMove",
@@ -199,7 +209,7 @@
199
209
  "kind": "field",
200
210
  "name": "tick",
201
211
  "type": {
202
- "text": "() => void"
212
+ "text": "(ts?: number) => void"
203
213
  },
204
214
  "privacy": "private",
205
215
  "readonly": true
@@ -231,6 +241,24 @@
231
241
  "description": "particle count; `0` (default) means auto-size to the frame area.",
232
242
  "readonly": true
233
243
  },
244
+ {
245
+ "kind": "field",
246
+ "name": "maxParticles",
247
+ "type": {
248
+ "text": "number"
249
+ },
250
+ "description": "Hard ceiling on the pool size (§31.19 scoped local-cell budget); `0` (default) means\nno cap. Clamps *both* the auto-size and an explicit `count`, so a cell can never\nexceed its declared budget no matter how large its frame grows.",
251
+ "readonly": true
252
+ },
253
+ {
254
+ "kind": "field",
255
+ "name": "fps",
256
+ "type": {
257
+ "text": "number"
258
+ },
259
+ "description": "Target frames per second for the animation loop (§31.19); `0` (default) means run at\nthe display's native rAF cadence. A positive value throttles the loop so a page full\nof demo cells stays cheap — each cell keeps its own frame budget.",
260
+ "readonly": true
261
+ },
234
262
  {
235
263
  "kind": "method",
236
264
  "name": "isPrefersReducedMotion",
@@ -241,6 +269,36 @@
241
269
  }
242
270
  }
243
271
  },
272
+ {
273
+ "kind": "method",
274
+ "name": "rafNow",
275
+ "privacy": "private",
276
+ "return": {
277
+ "type": {
278
+ "text": "number"
279
+ }
280
+ },
281
+ "description": "A monotonic timestamp for the fps throttle, in ms (rAF-timestamp compatible)."
282
+ },
283
+ {
284
+ "kind": "method",
285
+ "name": "shouldRenderFrame",
286
+ "privacy": "private",
287
+ "return": {
288
+ "type": {
289
+ "text": "boolean"
290
+ }
291
+ },
292
+ "parameters": [
293
+ {
294
+ "name": "now",
295
+ "type": {
296
+ "text": "number"
297
+ }
298
+ }
299
+ ],
300
+ "description": "The per-cell frame-budget gate (§31.19). With no `fps` (0), every frame renders. With a\npositive `fps`, a frame renders only once its `1000/fps` ms interval has elapsed since the\nlast render; renders advance `lastStepTs`. Each cell keeps its own budget — one cell's rate\nnever affects another's."
301
+ },
244
302
  {
245
303
  "kind": "method",
246
304
  "name": "fit",
@@ -261,7 +319,7 @@
261
319
  "text": "void"
262
320
  }
263
321
  },
264
- "description": "Build the particle pool sized to the frame area."
322
+ "description": "Build the particle pool sized to the frame area, capped by the cell's budget (§31.19)."
265
323
  },
266
324
  {
267
325
  "kind": "method",
@@ -320,6 +378,20 @@
320
378
  "text": "number"
321
379
  },
322
380
  "description": "Number of particles in the cell's pool."
381
+ },
382
+ {
383
+ "name": "max-particles",
384
+ "type": {
385
+ "text": "number"
386
+ },
387
+ "description": "Hard ceiling on the pool size (§31.19); caps both the auto-size and an explicit `count`."
388
+ },
389
+ {
390
+ "name": "fps",
391
+ "type": {
392
+ "text": "number"
393
+ },
394
+ "description": "Target frames per second for the animation loop (§31.19); throttles rAF so many cells stay cheap."
323
395
  }
324
396
  ],
325
397
  "superclass": {
@@ -368,7 +440,7 @@
368
440
  "privacy": "private",
369
441
  "static": true,
370
442
  "readonly": true,
371
- "default": "[ { key: 'density', attr: 'density', read: (el) => el.density }, { key: 'waves', attr: 'waves', read: (el) => el.waves }, { key: 'depth', attr: 'depth', read: (el) => el.depth }, { key: 'background', attr: 'background', read: (el) => el.background }, { key: 'render', attr: 'render', read: (el) => el.renderMode }, { key: 'overlay', attr: 'overlay', read: (el) => el.overlay }, { key: 'palette', attr: 'palette', read: (el) => el.palette }, { key: 'mass', attr: 'mass', read: (el) => el.mass }, { key: 'attention', attr: 'attention', read: (el) => el.attention }, { key: 'causality', attr: 'causality', read: (el) => el.causality }, { key: 'heatmap', attr: 'heatmap', read: (el) => el.heatmap }, { key: 'dprCap', attr: 'dpr-cap', read: (el) => el.dprCap }, { key: 'gridWarp', attr: 'grid-warp', read: (el) => el.gridWarp }, { key: 'gridIntensity', attr: 'grid-intensity', read: (el) => el.gridIntensity }, { key: 'theme', attr: 'theme', read: (el) => el.theme }, { key: 'gradientCool', attr: 'gradient-cool', read: (el) => el.gradientCool }, { key: 'gradientWarm', attr: 'gradient-warm', read: (el) => el.gradientWarm }, { key: 'waveBaseline', attr: 'wave-baseline', read: (el) => el.waveBaseline }, { key: 'waveStyle', attr: 'wave-style', read: (el) => el.waveStyle }, { key: 'waveCenter', attr: 'wave-center', read: (el) => el.waveCenter }, { key: 'separation', attr: 'separation', read: (el) => el.separation }, { key: 'integrator', attr: 'integrator', read: (el) => el.integrator }, ]",
443
+ "default": "[ { key: 'density', attr: 'density', read: (el) => el.density }, { key: 'waves', attr: 'waves', read: (el) => el.waves }, { key: 'depth', attr: 'depth', read: (el) => el.depth }, { key: 'background', attr: 'background', read: (el) => el.background }, { key: 'render', attr: 'render', read: (el) => el.renderMode }, { key: 'overlay', attr: 'overlay', read: (el) => el.overlay }, { key: 'palette', attr: 'palette', read: (el) => el.palette }, { key: 'mass', attr: 'mass', read: (el) => el.mass }, { key: 'attention', attr: 'attention', read: (el) => el.attention }, { key: 'causality', attr: 'causality', read: (el) => el.causality }, { key: 'heatmap', attr: 'heatmap', read: (el) => el.heatmap }, { key: 'dprCap', attr: 'dpr-cap', read: (el) => el.dprCap }, { key: 'gridWarp', attr: 'grid-warp', read: (el) => el.gridWarp }, { key: 'gridIntensity', attr: 'grid-intensity', read: (el) => el.gridIntensity }, { key: 'theme', attr: 'theme', read: (el) => el.theme }, { key: 'gradientCool', attr: 'gradient-cool', read: (el) => el.gradientCool }, { key: 'gradientWarm', attr: 'gradient-warm', read: (el) => el.gradientWarm }, { key: 'waveBaseline', attr: 'wave-baseline', read: (el) => el.waveBaseline }, { key: 'waveStyle', attr: 'wave-style', read: (el) => el.waveStyle }, { key: 'waveCenter', attr: 'wave-center', read: (el) => el.waveCenter }, { key: 'separation', attr: 'separation', read: (el) => el.separation }, { key: 'ambientOrbit', attr: 'ambient-orbit', read: (el) => el.ambientOrbit }, { key: 'ambientWander', attr: 'ambient-wander', read: (el) => el.ambientWander }, { key: 'integrator', attr: 'integrator', read: (el) => el.integrator }, ]",
372
444
  "description": "The engine options `<field-root>` forwards to `createBrowserField`, as ONE declarative table —\nthe single source of truth for the option object built in `start()`, so a new forwarded\n`FieldOption` can never be silently dropped from forwarding the way `depth` once was. `accent`\n(raw passthrough so a `palette` with no `accent` adopts the palette stop) and\n`overlayCanvas`/`feedbackSink` (managed internally) are special-cased in `start()` and absent here.\n\n`observedAttributes` stays an explicit literal below — the Custom-Elements-Manifest analyzer reads\nit statically and can't enumerate a computed array — but the `option-attrs-observed` test pins it\nto this table (every `attr` here must be observed), so the two lists can't drift apart."
373
445
  },
374
446
  {
@@ -425,6 +497,16 @@
425
497
  "privacy": "private",
426
498
  "default": "true"
427
499
  },
500
+ {
501
+ "kind": "field",
502
+ "name": "fieldActiveMarked",
503
+ "type": {
504
+ "text": "boolean"
505
+ },
506
+ "privacy": "private",
507
+ "default": "false",
508
+ "description": "SSR pre-registration queue bookkeeping: true while this element counts as a live field, so a\nrebuild (start() after a destroy) does not double-count the active-field tally (#683)."
509
+ },
428
510
  {
429
511
  "kind": "field",
430
512
  "name": "platformRuntime",
@@ -466,14 +548,14 @@
466
548
  "type": {
467
549
  "text": "boolean"
468
550
  },
469
- "description": "draw the background Currents (§24).",
551
+ "description": "draw the background Currents (§24). OPT-IN (#979, mirrors the core default): the attribute\nmust be PRESENT (and not `\"false\"`) to build the waves — same semantics as `attention` /\n`causality` / `mass`. Absent = the bare field, no carrier waves.",
470
552
  "readonly": true
471
553
  },
472
554
  {
473
555
  "kind": "field",
474
556
  "name": "renderMode",
475
557
  "type": {
476
- "text": "'dots' | 'trails' | 'links' | 'metaballs' | 'voronoi' | 'streamlines' | 'flow' | 'none'"
558
+ "text": "| 'dots'\n | 'trails'\n | 'links'\n | 'metaballs'\n | 'voronoi'\n | 'streamlines'\n | 'flow'\n | 'knockout'\n | 'redshift'\n | 'blackbody'\n | 'depth'\n | 'none'"
477
559
  },
478
560
  "description": "render mode (§20.6); the DEFAULT is `none` (#538) — the signals-only engine: simulate + feed back,\nnever draw (#297). Set `render=\"dots\"` (or another drawing mode) to get a visible surface.",
479
561
  "readonly": true
@@ -586,13 +668,31 @@
586
668
  "description": "`separation` — particle-to-particle separation force strength ∈ [0,1]; undefined if absent/invalid.",
587
669
  "readonly": true
588
670
  },
671
+ {
672
+ "kind": "field",
673
+ "name": "ambientOrbit",
674
+ "type": {
675
+ "text": "number | undefined"
676
+ },
677
+ "description": "`ambient-orbit` — DECLARED resting-formation swirl on attract (#978); undefined (engine default\n0.1, the historical hardcoded value) if absent/invalid. `0` is valid (a purely radial attract).",
678
+ "readonly": true
679
+ },
680
+ {
681
+ "kind": "field",
682
+ "name": "ambientWander",
683
+ "type": {
684
+ "text": "number | undefined"
685
+ },
686
+ "description": "`ambient-wander` — DECLARED resting-formation drift (#978); undefined (engine default 1.0, the\nhistorical hardcoded value) if absent/invalid. `0` is valid (a still resting field).",
687
+ "readonly": true
688
+ },
589
689
  {
590
690
  "kind": "field",
591
691
  "name": "integrator",
592
692
  "type": {
593
693
  "text": "IntegratorMode | undefined"
594
694
  },
595
- "description": "`integrator` — the integration scheme (substrate doc 04 §Step 3); `'fixed'` opts into the\nframe-rate-independent integrator, anything else (incl. absent) is the default `'legacy'`.",
695
+ "description": "`integrator` — the integration scheme (substrate doc 04 §Step 3); `'fixed'` opts into the\nframe-rate-independent integrator, `'velocity-verlet'` into the second-order Verlet scheme\n(#659); anything else (incl. absent) is the default `'legacy'`.",
596
696
  "readonly": true
597
697
  },
598
698
  {
@@ -940,7 +1040,7 @@
940
1040
  {
941
1041
  "name": "mode",
942
1042
  "type": {
943
- "text": "'dots' | 'trails' | 'links' | 'metaballs' | 'voronoi' | 'streamlines' | 'flow' | 'none'"
1043
+ "text": "| 'dots'\n | 'trails'\n | 'links'\n | 'metaballs'\n | 'voronoi'\n | 'streamlines'\n | 'flow'\n | 'knockout'\n | 'redshift'\n | 'blackbody'\n | 'depth'\n | 'none'"
944
1044
  }
945
1045
  }
946
1046
  ],
@@ -1529,6 +1629,17 @@
1529
1629
  ],
1530
1630
  "description": "pause/resume drawing (the simulation keeps running) — the element also does this automatically\nfrom its IntersectionObserver, so a manual call is for explicit control."
1531
1631
  },
1632
+ {
1633
+ "kind": "method",
1634
+ "name": "ensureOverlayCanvas",
1635
+ "privacy": "private",
1636
+ "return": {
1637
+ "type": {
1638
+ "text": "HTMLCanvasElement | null"
1639
+ }
1640
+ },
1641
+ "description": "Field Surfaces: lazily create + attach the front overlay surface (light DOM — the shadow host is\nz-index:0, behind content). A fixed, full-viewport, click-through, mix-blend canvas above content;\ncore sizes its backing store and draws the overlay mode onto it. Created ONCE, on the first overlay\nthat actually goes active (#676) — core invokes this as `overlayCanvasProvider` — and reused across\nrebuilds. Idempotent: returns the existing canvas on every later call, never a second element."
1642
+ },
1532
1643
  {
1533
1644
  "kind": "method",
1534
1645
  "name": "start",
@@ -1609,6 +1720,12 @@
1609
1720
  {
1610
1721
  "name": "separation"
1611
1722
  },
1723
+ {
1724
+ "name": "ambient-orbit"
1725
+ },
1726
+ {
1727
+ "name": "ambient-wander"
1728
+ },
1612
1729
  {
1613
1730
  "name": "integrator"
1614
1731
  },
@@ -1791,6 +1908,20 @@
1791
1908
  "module": "src/index.ts"
1792
1909
  }
1793
1910
  },
1911
+ {
1912
+ "name": "ambient-orbit",
1913
+ "inheritedFrom": {
1914
+ "name": "FieldField",
1915
+ "module": "src/index.ts"
1916
+ }
1917
+ },
1918
+ {
1919
+ "name": "ambient-wander",
1920
+ "inheritedFrom": {
1921
+ "name": "FieldField",
1922
+ "module": "src/index.ts"
1923
+ }
1924
+ },
1794
1925
  {
1795
1926
  "name": "integrator",
1796
1927
  "inheritedFrom": {
@@ -1823,7 +1954,7 @@
1823
1954
  "privacy": "private",
1824
1955
  "static": true,
1825
1956
  "readonly": true,
1826
- "default": "[ { key: 'density', attr: 'density', read: (el) => el.density }, { key: 'waves', attr: 'waves', read: (el) => el.waves }, { key: 'depth', attr: 'depth', read: (el) => el.depth }, { key: 'background', attr: 'background', read: (el) => el.background }, { key: 'render', attr: 'render', read: (el) => el.renderMode }, { key: 'overlay', attr: 'overlay', read: (el) => el.overlay }, { key: 'palette', attr: 'palette', read: (el) => el.palette }, { key: 'mass', attr: 'mass', read: (el) => el.mass }, { key: 'attention', attr: 'attention', read: (el) => el.attention }, { key: 'causality', attr: 'causality', read: (el) => el.causality }, { key: 'heatmap', attr: 'heatmap', read: (el) => el.heatmap }, { key: 'dprCap', attr: 'dpr-cap', read: (el) => el.dprCap }, { key: 'gridWarp', attr: 'grid-warp', read: (el) => el.gridWarp }, { key: 'gridIntensity', attr: 'grid-intensity', read: (el) => el.gridIntensity }, { key: 'theme', attr: 'theme', read: (el) => el.theme }, { key: 'gradientCool', attr: 'gradient-cool', read: (el) => el.gradientCool }, { key: 'gradientWarm', attr: 'gradient-warm', read: (el) => el.gradientWarm }, { key: 'waveBaseline', attr: 'wave-baseline', read: (el) => el.waveBaseline }, { key: 'waveStyle', attr: 'wave-style', read: (el) => el.waveStyle }, { key: 'waveCenter', attr: 'wave-center', read: (el) => el.waveCenter }, { key: 'separation', attr: 'separation', read: (el) => el.separation }, { key: 'integrator', attr: 'integrator', read: (el) => el.integrator }, ]",
1957
+ "default": "[ { key: 'density', attr: 'density', read: (el) => el.density }, { key: 'waves', attr: 'waves', read: (el) => el.waves }, { key: 'depth', attr: 'depth', read: (el) => el.depth }, { key: 'background', attr: 'background', read: (el) => el.background }, { key: 'render', attr: 'render', read: (el) => el.renderMode }, { key: 'overlay', attr: 'overlay', read: (el) => el.overlay }, { key: 'palette', attr: 'palette', read: (el) => el.palette }, { key: 'mass', attr: 'mass', read: (el) => el.mass }, { key: 'attention', attr: 'attention', read: (el) => el.attention }, { key: 'causality', attr: 'causality', read: (el) => el.causality }, { key: 'heatmap', attr: 'heatmap', read: (el) => el.heatmap }, { key: 'dprCap', attr: 'dpr-cap', read: (el) => el.dprCap }, { key: 'gridWarp', attr: 'grid-warp', read: (el) => el.gridWarp }, { key: 'gridIntensity', attr: 'grid-intensity', read: (el) => el.gridIntensity }, { key: 'theme', attr: 'theme', read: (el) => el.theme }, { key: 'gradientCool', attr: 'gradient-cool', read: (el) => el.gradientCool }, { key: 'gradientWarm', attr: 'gradient-warm', read: (el) => el.gradientWarm }, { key: 'waveBaseline', attr: 'wave-baseline', read: (el) => el.waveBaseline }, { key: 'waveStyle', attr: 'wave-style', read: (el) => el.waveStyle }, { key: 'waveCenter', attr: 'wave-center', read: (el) => el.waveCenter }, { key: 'separation', attr: 'separation', read: (el) => el.separation }, { key: 'ambientOrbit', attr: 'ambient-orbit', read: (el) => el.ambientOrbit }, { key: 'ambientWander', attr: 'ambient-wander', read: (el) => el.ambientWander }, { key: 'integrator', attr: 'integrator', read: (el) => el.integrator }, ]",
1827
1958
  "description": "The engine options `<field-root>` forwards to `createBrowserField`, as ONE declarative table —\nthe single source of truth for the option object built in `start()`, so a new forwarded\n`FieldOption` can never be silently dropped from forwarding the way `depth` once was. `accent`\n(raw passthrough so a `palette` with no `accent` adopts the palette stop) and\n`overlayCanvas`/`feedbackSink` (managed internally) are special-cased in `start()` and absent here.\n\n`observedAttributes` stays an explicit literal below — the Custom-Elements-Manifest analyzer reads\nit statically and can't enumerate a computed array — but the `option-attrs-observed` test pins it\nto this table (every `attr` here must be observed), so the two lists can't drift apart.",
1828
1959
  "inheritedFrom": {
1829
1960
  "name": "FieldField",
@@ -1908,6 +2039,20 @@
1908
2039
  "module": "src/index.ts"
1909
2040
  }
1910
2041
  },
2042
+ {
2043
+ "kind": "field",
2044
+ "name": "fieldActiveMarked",
2045
+ "type": {
2046
+ "text": "boolean"
2047
+ },
2048
+ "privacy": "private",
2049
+ "default": "false",
2050
+ "description": "SSR pre-registration queue bookkeeping: true while this element counts as a live field, so a\nrebuild (start() after a destroy) does not double-count the active-field tally (#683).",
2051
+ "inheritedFrom": {
2052
+ "name": "FieldField",
2053
+ "module": "src/index.ts"
2054
+ }
2055
+ },
1911
2056
  {
1912
2057
  "kind": "field",
1913
2058
  "name": "platformRuntime",
@@ -1965,7 +2110,7 @@
1965
2110
  "type": {
1966
2111
  "text": "boolean"
1967
2112
  },
1968
- "description": "draw the background Currents (§24).",
2113
+ "description": "draw the background Currents (§24). OPT-IN (#979, mirrors the core default): the attribute\nmust be PRESENT (and not `\"false\"`) to build the waves — same semantics as `attention` /\n`causality` / `mass`. Absent = the bare field, no carrier waves.",
1969
2114
  "readonly": true,
1970
2115
  "inheritedFrom": {
1971
2116
  "name": "FieldField",
@@ -1976,7 +2121,7 @@
1976
2121
  "kind": "field",
1977
2122
  "name": "renderMode",
1978
2123
  "type": {
1979
- "text": "'dots' | 'trails' | 'links' | 'metaballs' | 'voronoi' | 'streamlines' | 'flow' | 'none'"
2124
+ "text": "| 'dots'\n | 'trails'\n | 'links'\n | 'metaballs'\n | 'voronoi'\n | 'streamlines'\n | 'flow'\n | 'knockout'\n | 'redshift'\n | 'blackbody'\n | 'depth'\n | 'none'"
1980
2125
  },
1981
2126
  "description": "render mode (§20.6); the DEFAULT is `none` (#538) — the signals-only engine: simulate + feed back,\nnever draw (#297). Set `render=\"dots\"` (or another drawing mode) to get a visible surface.",
1982
2127
  "readonly": true,
@@ -2141,13 +2286,39 @@
2141
2286
  "module": "src/index.ts"
2142
2287
  }
2143
2288
  },
2289
+ {
2290
+ "kind": "field",
2291
+ "name": "ambientOrbit",
2292
+ "type": {
2293
+ "text": "number | undefined"
2294
+ },
2295
+ "description": "`ambient-orbit` — DECLARED resting-formation swirl on attract (#978); undefined (engine default\n0.1, the historical hardcoded value) if absent/invalid. `0` is valid (a purely radial attract).",
2296
+ "readonly": true,
2297
+ "inheritedFrom": {
2298
+ "name": "FieldField",
2299
+ "module": "src/index.ts"
2300
+ }
2301
+ },
2302
+ {
2303
+ "kind": "field",
2304
+ "name": "ambientWander",
2305
+ "type": {
2306
+ "text": "number | undefined"
2307
+ },
2308
+ "description": "`ambient-wander` — DECLARED resting-formation drift (#978); undefined (engine default 1.0, the\nhistorical hardcoded value) if absent/invalid. `0` is valid (a still resting field).",
2309
+ "readonly": true,
2310
+ "inheritedFrom": {
2311
+ "name": "FieldField",
2312
+ "module": "src/index.ts"
2313
+ }
2314
+ },
2144
2315
  {
2145
2316
  "kind": "field",
2146
2317
  "name": "integrator",
2147
2318
  "type": {
2148
2319
  "text": "IntegratorMode | undefined"
2149
2320
  },
2150
- "description": "`integrator` — the integration scheme (substrate doc 04 §Step 3); `'fixed'` opts into the\nframe-rate-independent integrator, anything else (incl. absent) is the default `'legacy'`.",
2321
+ "description": "`integrator` — the integration scheme (substrate doc 04 §Step 3); `'fixed'` opts into the\nframe-rate-independent integrator, `'velocity-verlet'` into the second-order Verlet scheme\n(#659); anything else (incl. absent) is the default `'legacy'`.",
2151
2322
  "readonly": true,
2152
2323
  "inheritedFrom": {
2153
2324
  "name": "FieldField",
@@ -2591,7 +2762,7 @@
2591
2762
  {
2592
2763
  "name": "mode",
2593
2764
  "type": {
2594
- "text": "'dots' | 'trails' | 'links' | 'metaballs' | 'voronoi' | 'streamlines' | 'flow' | 'none'"
2765
+ "text": "| 'dots'\n | 'trails'\n | 'links'\n | 'metaballs'\n | 'voronoi'\n | 'streamlines'\n | 'flow'\n | 'knockout'\n | 'redshift'\n | 'blackbody'\n | 'depth'\n | 'none'"
2595
2766
  }
2596
2767
  }
2597
2768
  ],
@@ -3304,6 +3475,21 @@
3304
3475
  "module": "src/index.ts"
3305
3476
  }
3306
3477
  },
3478
+ {
3479
+ "kind": "method",
3480
+ "name": "ensureOverlayCanvas",
3481
+ "privacy": "private",
3482
+ "return": {
3483
+ "type": {
3484
+ "text": "HTMLCanvasElement | null"
3485
+ }
3486
+ },
3487
+ "description": "Field Surfaces: lazily create + attach the front overlay surface (light DOM — the shadow host is\nz-index:0, behind content). A fixed, full-viewport, click-through, mix-blend canvas above content;\ncore sizes its backing store and draws the overlay mode onto it. Created ONCE, on the first overlay\nthat actually goes active (#676) — core invokes this as `overlayCanvasProvider` — and reused across\nrebuilds. Idempotent: returns the existing canvas on every later call, never a second element.",
3488
+ "inheritedFrom": {
3489
+ "name": "FieldField",
3490
+ "module": "src/index.ts"
3491
+ }
3492
+ },
3307
3493
  {
3308
3494
  "kind": "method",
3309
3495
  "name": "start",
@@ -3757,6 +3943,140 @@
3757
3943
  }
3758
3944
  }
3759
3945
  ]
3946
+ },
3947
+ {
3948
+ "kind": "javascript-module",
3949
+ "path": "src/preregistration-queue.ts",
3950
+ "declarations": [
3951
+ {
3952
+ "kind": "function",
3953
+ "name": "installPreRegistrationQueue",
3954
+ "return": {
3955
+ "type": {
3956
+ "text": "void"
3957
+ }
3958
+ },
3959
+ "description": "Install the capturing document listeners — idempotent, SSR-guarded. Called from the custom\nelement's constructor so it runs on the client before the element upgrades its peers, and never on\nthe server (no `document`). Capturing (`{ capture: true }`) so the queue sees the event on the way\ndown, independent of any later-added bubble-phase field listener."
3960
+ },
3961
+ {
3962
+ "kind": "function",
3963
+ "name": "markFieldActive",
3964
+ "return": {
3965
+ "type": {
3966
+ "text": "void"
3967
+ }
3968
+ },
3969
+ "description": "Mark that a field has become live. The first live field is what makes subsequent registration\nevents skip the queue and reach the field directly. Paired with markFieldInactive."
3970
+ },
3971
+ {
3972
+ "kind": "function",
3973
+ "name": "markFieldInactive",
3974
+ "return": {
3975
+ "type": {
3976
+ "text": "void"
3977
+ }
3978
+ },
3979
+ "description": "Mark that a live field has torn down. When the last field goes away the queue resumes buffering, so\nan element that (re)connects during a field-less window is captured for the next field that boots."
3980
+ },
3981
+ {
3982
+ "kind": "function",
3983
+ "name": "flushPreRegistrationQueue",
3984
+ "return": {
3985
+ "type": {
3986
+ "text": "void"
3987
+ }
3988
+ },
3989
+ "description": "Replay every buffered registration event on its source element, then clear the buffer. Called by a\nfield immediately after it wires its body-event listeners, so the events bubble (composed) to the\ndocument and the field registers them through its normal, idempotent path. Safe to call with an\nempty queue (the common client-only case) — it does nothing."
3990
+ },
3991
+ {
3992
+ "kind": "function",
3993
+ "name": "pendingRegistrationCount",
3994
+ "return": {
3995
+ "type": {
3996
+ "text": "number"
3997
+ }
3998
+ },
3999
+ "description": "Test-only: the number of events currently buffered (0 once drained)."
4000
+ },
4001
+ {
4002
+ "kind": "function",
4003
+ "name": "resetPreRegistrationQueue",
4004
+ "return": {
4005
+ "type": {
4006
+ "text": "void"
4007
+ }
4008
+ },
4009
+ "description": "Test-only: reset all module state (queue, active-field count, install flag) between tests."
4010
+ },
4011
+ {
4012
+ "kind": "function",
4013
+ "name": "isBuffering",
4014
+ "return": {
4015
+ "type": {
4016
+ "text": "boolean"
4017
+ }
4018
+ },
4019
+ "description": "Whether the queue is currently buffering (no field live). Test/introspection helper."
4020
+ }
4021
+ ],
4022
+ "exports": [
4023
+ {
4024
+ "kind": "js",
4025
+ "name": "installPreRegistrationQueue",
4026
+ "declaration": {
4027
+ "name": "installPreRegistrationQueue",
4028
+ "module": "src/preregistration-queue.ts"
4029
+ }
4030
+ },
4031
+ {
4032
+ "kind": "js",
4033
+ "name": "markFieldActive",
4034
+ "declaration": {
4035
+ "name": "markFieldActive",
4036
+ "module": "src/preregistration-queue.ts"
4037
+ }
4038
+ },
4039
+ {
4040
+ "kind": "js",
4041
+ "name": "markFieldInactive",
4042
+ "declaration": {
4043
+ "name": "markFieldInactive",
4044
+ "module": "src/preregistration-queue.ts"
4045
+ }
4046
+ },
4047
+ {
4048
+ "kind": "js",
4049
+ "name": "flushPreRegistrationQueue",
4050
+ "declaration": {
4051
+ "name": "flushPreRegistrationQueue",
4052
+ "module": "src/preregistration-queue.ts"
4053
+ }
4054
+ },
4055
+ {
4056
+ "kind": "js",
4057
+ "name": "pendingRegistrationCount",
4058
+ "declaration": {
4059
+ "name": "pendingRegistrationCount",
4060
+ "module": "src/preregistration-queue.ts"
4061
+ }
4062
+ },
4063
+ {
4064
+ "kind": "js",
4065
+ "name": "resetPreRegistrationQueue",
4066
+ "declaration": {
4067
+ "name": "resetPreRegistrationQueue",
4068
+ "module": "src/preregistration-queue.ts"
4069
+ }
4070
+ },
4071
+ {
4072
+ "kind": "js",
4073
+ "name": "isBuffering",
4074
+ "declaration": {
4075
+ "name": "isBuffering",
4076
+ "module": "src/preregistration-queue.ts"
4077
+ }
4078
+ }
4079
+ ]
3760
4080
  }
3761
4081
  ]
3762
4082
  }
@@ -11,11 +11,20 @@ import { HTMLElementBase } from './base.ts';
11
11
  * - `IntersectionObserver` gates the rAF loop — paused when off-screen.
12
12
  * - Honours `prefers-reduced-motion`: renders one static frame, no animation.
13
13
  *
14
+ * **Budgets & isolation (shadow-dom.md §31.19).** Each cell owns its *own* pool, so
15
+ * many cells on a docs page never share or starve one budget. Hard caps keep that
16
+ * budget cheap: `max-particles` clamps the pool ceiling (over both the auto-size and an
17
+ * explicit `count`), and `fps` throttles the animation loop to a target framerate. Both
18
+ * caps are enforced per instance — a saturating cell cannot spill particles or frame
19
+ * cost into a neighbour.
20
+ *
14
21
  * @summary A standalone, in-frame demo field that renders one force with its own
15
22
  * particle pool, in-view-gated. Registered as `<field-cell>`.
16
23
  * @attr {string} force - The single force token rendered: `attract` | `repel` | `swirl` | `gravity` | `stream` | `buoyancy` | `tether`.
17
24
  * @attr {string} color - Accent color (hex) for the cell's particles.
18
25
  * @attr {number} count - Number of particles in the cell's pool.
26
+ * @attr {number} max-particles - Hard ceiling on the pool size (§31.19); caps both the auto-size and an explicit `count`.
27
+ * @attr {number} fps - Target frames per second for the animation loop (§31.19); throttles rAF so many cells stay cheap.
19
28
  */
20
29
  export declare class FieldCell extends HTMLElementBase {
21
30
  static readonly observedAttributes: string[];
@@ -31,6 +40,8 @@ export declare class FieldCell extends HTMLElementBase {
31
40
  /** cursor in frame-local CSS px, or null when absent. */
32
41
  private cursorX;
33
42
  private cursorY;
43
+ /** rAF timestamp of the last rendered frame — drives the `fps` throttle (§31.19). */
44
+ private lastStepTs;
34
45
  private readonly onPointerMove;
35
46
  private readonly onPointerLeave;
36
47
  private readonly tick;
@@ -41,13 +52,34 @@ export declare class FieldCell extends HTMLElementBase {
41
52
  get color(): string;
42
53
  /** particle count; `0` (default) means auto-size to the frame area. */
43
54
  get count(): number;
44
- attributeChangedCallback(): void;
55
+ /**
56
+ * Hard ceiling on the pool size (§31.19 scoped local-cell budget); `0` (default) means
57
+ * no cap. Clamps *both* the auto-size and an explicit `count`, so a cell can never
58
+ * exceed its declared budget no matter how large its frame grows.
59
+ */
60
+ get maxParticles(): number;
61
+ /**
62
+ * Target frames per second for the animation loop (§31.19); `0` (default) means run at
63
+ * the display's native rAF cadence. A positive value throttles the loop so a page full
64
+ * of demo cells stays cheap — each cell keeps its own frame budget.
65
+ */
66
+ get fps(): number;
67
+ attributeChangedCallback(name: string): void;
45
68
  connectedCallback(): void;
46
69
  disconnectedCallback(): void;
47
70
  private isPrefersReducedMotion;
71
+ /** A monotonic timestamp for the fps throttle, in ms (rAF-timestamp compatible). */
72
+ private rafNow;
73
+ /**
74
+ * The per-cell frame-budget gate (§31.19). With no `fps` (0), every frame renders. With a
75
+ * positive `fps`, a frame renders only once its `1000/fps` ms interval has elapsed since the
76
+ * last render; renders advance `lastStepTs`. Each cell keeps its own budget — one cell's rate
77
+ * never affects another's.
78
+ */
79
+ private shouldRenderFrame;
48
80
  /** Fit the canvas to the element box (DPR-aware) and rebuild the pool. */
49
81
  private fit;
50
- /** Build the particle pool sized to the frame area. */
82
+ /** Build the particle pool sized to the frame area, capped by the cell's budget (§31.19). */
51
83
  private buildPool;
52
84
  private start;
53
85
  private stop;
@@ -1 +1 @@
1
- {"version":3,"file":"field-cell.d.ts","sourceRoot":"","sources":["../src/field-cell.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAa5C;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,SAAU,SAAQ,eAAe;IAC5C,MAAM,CAAC,QAAQ,CAAC,kBAAkB,WAA+B;IAEjE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,GAAG,CAAyC;IAEpD,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,oBAAoB,CAAC,CAAuB;IAEpD,0DAA0D;IAC1D,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,IAAI,CAAK;IAEjB,yDAAyD;IACzD,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,OAAO,CAAuB;IAEtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAa;IAC5C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;;IA2BlC,gDAAgD;IAChD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,iBAAiB;IACjB,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,uEAAuE;IACvE,IAAI,KAAK,IAAI,MAAM,CAGlB;IAED,wBAAwB,IAAI,IAAI;IAKhC,iBAAiB,IAAI,IAAI;IAqBzB,oBAAoB,IAAI,IAAI;IAU5B,OAAO,CAAC,sBAAsB;IAO9B,0EAA0E;IAC1E,OAAO,CAAC,GAAG;IAmBX,uDAAuD;IACvD,OAAO,CAAC,SAAS;IAejB,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,IAAI;IAKZ,kCAAkC;IAClC,OAAO,CAAC,IAAI;CAwDb;AAMD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,YAAY,EAAE,SAAS,CAAC;KACzB;CACF"}
1
+ {"version":3,"file":"field-cell.d.ts","sourceRoot":"","sources":["../src/field-cell.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAa5C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,SAAU,SAAQ,eAAe;IAC5C,MAAM,CAAC,QAAQ,CAAC,kBAAkB,WAAuD;IAEzF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,GAAG,CAAyC;IAEpD,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,oBAAoB,CAAC,CAAuB;IAEpD,0DAA0D;IAC1D,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,IAAI,CAAK;IAEjB,yDAAyD;IACzD,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,OAAO,CAAuB;IAEtC,qFAAqF;IACrF,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAa;IAC5C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;;IA8B7C,gDAAgD;IAChD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,iBAAiB;IACjB,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,uEAAuE;IACvE,IAAI,KAAK,IAAI,MAAM,CAGlB;IAED;;;;OAIG;IACH,IAAI,YAAY,IAAI,MAAM,CAGzB;IAED;;;;OAIG;IACH,IAAI,GAAG,IAAI,MAAM,CAGhB;IAED,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAU5C,iBAAiB,IAAI,IAAI;IAqBzB,oBAAoB,IAAI,IAAI;IAU5B,OAAO,CAAC,sBAAsB;IAO9B,oFAAoF;IACpF,OAAO,CAAC,MAAM;IAMd;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAYzB,0EAA0E;IAC1E,OAAO,CAAC,GAAG;IAmBX,6FAA6F;IAC7F,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,KAAK;IAWb,OAAO,CAAC,IAAI;IAKZ,kCAAkC;IAClC,OAAO,CAAC,IAAI;CAwDb;AAMD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,YAAY,EAAE,SAAS,CAAC;KACzB;CACF"}