@fundamental-engine/vanilla 0.4.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zach Shallbetter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @fundamental-engine/vanilla
2
+
3
+ **The framework-free door to [`@fundamental-engine/core`](../core)** — a reciprocal DOM-physics field as a typed
4
+ `FieldField` class and the imperative `mountField()`. Elements you mark with `data-body` become forces;
5
+ the single background field reacts to them, and its density reacts back. No custom-element
6
+ registration, no framework dependency, no import side effects.
7
+
8
+ → Live manual, Lab, and gallery at **[fundamental-engine.com](https://fundamental-engine.com)**.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm i @fundamental-engine/vanilla
14
+ ```
15
+
16
+ The only dependency is the zero-dependency core plus [`@fundamental-engine/platform`](../platform) (which supplies
17
+ the browser host). Reach for this from plain TypeScript, or any stack where you want to drive the field
18
+ by hand.
19
+
20
+ ## The class
21
+
22
+ ```ts
23
+ import { FieldField } from '@fundamental-engine/vanilla';
24
+
25
+ const field = new FieldField({ accent: '#4da3ff', render: 'dots' });
26
+ field.setFormation('wells');
27
+ field.burst(window.innerWidth / 2, 200);
28
+ // field.scan(); // re-pick-up bodies after a DOM change
29
+ // field.destroy(); // stop the loop and remove the managed canvas
30
+ ```
31
+
32
+ `new FieldField()` builds a fixed, full-viewport canvas behind your page and starts the engine on it.
33
+ It takes every `FieldOptions` value, implements the full `FieldHandle` surface, and exposes the
34
+ `canvas` it runs on — the same engine the `<field-root>` custom element and the React `<FieldField>`
35
+ wrap.
36
+
37
+ ### Options (`FieldOptions`)
38
+
39
+ | Option | Type | Effect |
40
+ |---|---|---|
41
+ | `accent` | `string` | base hue (any CSS color) |
42
+ | `density` | `number` | particle density multiplier |
43
+ | `render` | `'dots' \| 'trails' \| 'links' \| 'metaballs' \| 'voronoi' \| 'streamlines'` | underlay render method |
44
+ | `palette` | `string \| string[]` | named palette (`ours` / `heatmap` / `infrared` / `spectrum`) or colors |
45
+ | `waves` | `boolean` | wave propagation |
46
+ | `mass` | `boolean` | first-class mass in the integrator |
47
+ | `attention` · `causality` · `heatmap` | `boolean` | diagnostics |
48
+ | `canvas` | `HTMLCanvasElement` | drive a canvas you own (the field won't create/remove one) |
49
+
50
+ ### Methods (`FieldHandle`)
51
+
52
+ | Method | Use |
53
+ |---|---|
54
+ | `scan()` / `rescan()` | re-read `[data-body]` elements after the DOM changes |
55
+ | `setAccent(hex)` · `setPalette(p)` | recolor live |
56
+ | `setFormation(name)` | arrange particles into a named formation |
57
+ | `setRender(mode)` · `setOverlay(mode)` | underlay (behind content) / overlay (in front) |
58
+ | `setAttention(on)` · `setCausality(on)` · `setHeatmap(on)` | toggle diagnostics |
59
+ | `burst(x, y, hex?)` · `flowTo(x, y)` · `clearFlow()` | impulses and a movable focus |
60
+ | `threads(list)` | draw relationship threads between bodies |
61
+ | `destroy()` | stop the engine (and remove the managed canvas, if it created one) |
62
+
63
+ > **Client only.** The field is a browser effect: `new FieldField()` (and `mountField()`) touch
64
+ > `document` right away and throw a clear error during server-side rendering. In Next.js, Astro,
65
+ > SvelteKit, and similar, construct it on the client — inside `useEffect`, `onMount`, or a "client
66
+ > only" boundary.
67
+
68
+ Drive a `<canvas>` you own instead by passing it — then the field never creates or removes a canvas,
69
+ and `destroy()` only stops the engine:
70
+
71
+ ```ts
72
+ const field = new FieldField({ canvas: myCanvas, density: 1.2 });
73
+ ```
74
+
75
+ ## The function
76
+
77
+ If you prefer a plain factory over a class, `mountField()` returns the bare `FieldHandle`:
78
+
79
+ ```ts
80
+ import { mountField } from '@fundamental-engine/vanilla';
81
+
82
+ const field = mountField({ render: 'trails' });
83
+ // field.destroy() also removes the canvas it created.
84
+ ```
85
+
86
+ To run the engine on a `<canvas>` with no managed wrapper at all, the host-bundled `createField` is
87
+ re-exported (this one supplies `browserHost()` for you, unlike the core primitive):
88
+
89
+ ```ts
90
+ import { createField } from '@fundamental-engine/vanilla';
91
+
92
+ const field = createField(document.querySelector('canvas')!, { accent: '#2dd4bf' });
93
+ ```
94
+
95
+ ## Marking bodies — the `data-body` vocabulary
96
+
97
+ | Attribute | Meaning |
98
+ |---|---|
99
+ | `data-body="attract"` | the force token (`attract`, `gravity`, `charge`, `sink`, …) |
100
+ | `data-strength` | how hard it bends the field |
101
+ | `data-range` | radius of influence, in px |
102
+ | `data-feedback` | opt in to receiving `--field-*` variables back |
103
+ | `data-absorb` / `data-max` | for `sink` bodies: accretion load and capacity |
104
+
105
+ Call `field.scan()` after adding new `[data-body]` elements so the engine picks them up.
106
+
107
+ ## Catalog
108
+
109
+ For building UI around the field (a force picker, a legend), the catalog data is re-exported so you
110
+ need no second install: `FORCES`, `FORMATIONS`, `CONDITIONS`, `PALETTE`.
111
+
112
+ ```ts
113
+ import { FORCES, FORMATIONS } from '@fundamental-engine/vanilla';
114
+ ```
115
+
116
+ ## Recipes & data binding
117
+
118
+ To apply a named recipe over your markup or bind data to the field, use `applyRecipe()` / `bindData()`
119
+ from [`@fundamental-engine/platform`](../platform); browse all 64 recipes at
120
+ [`/docs/gallery`](https://fundamental-engine.com/docs/gallery).
121
+
122
+ ## Related
123
+
124
+ [`@fundamental-engine/core`](../core) · [`@fundamental-engine/platform`](../platform) · [`@fundamental-engine/elements`](../elements)
125
+ · [`@fundamental-engine/react`](../react) · the [documentation map](../../docs/README.md).
126
+
127
+ ## License
128
+
129
+ MIT © Zach Shallbetter
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `FieldField` — the reciprocal DOM-physics field as a typed class, for plain TypeScript
3
+ * apps that want object-oriented ergonomics without a framework or a custom element.
4
+ *
5
+ * `new FieldField()` builds a managed, full-viewport canvas and starts the engine on it;
6
+ * pass `{ canvas }` to drive a `<canvas>` you own instead. The class implements the full
7
+ * `FieldHandle` surface, so an instance is type-compatible anywhere a handle is expected,
8
+ * and it exposes the `canvas` it renders to.
9
+ *
10
+ * ```ts
11
+ * import { FieldField } from '@fundamental-engine/vanilla';
12
+ *
13
+ * const field = new FieldField({ accent: '#4da3ff', render: 'dots' });
14
+ * field.setFormation('wells');
15
+ * field.burst(window.innerWidth / 2, 200);
16
+ * // field.scan(); field.destroy();
17
+ * ```
18
+ */
19
+ import { type AtomPayload, type FieldHandle, type FieldOptions, type ThreadLink, type FlowOptions } from '@fundamental-engine/core';
20
+ export interface FieldFieldInit extends FieldOptions {
21
+ /** drive a `<canvas>` you own; when omitted, a managed full-viewport canvas is created
22
+ * (and removed again by `destroy()`). */
23
+ canvas?: HTMLCanvasElement;
24
+ /** where to append the managed canvas; ignored when you pass your own `canvas`. */
25
+ target?: HTMLElement;
26
+ }
27
+ export declare class FieldField implements FieldHandle {
28
+ /** the `<canvas>` the field renders to — the one created for you, or the one you passed. */
29
+ readonly canvas: HTMLCanvasElement;
30
+ private readonly field;
31
+ /** did this instance create the canvas (and so should remove it on `destroy()`)? */
32
+ private readonly managed;
33
+ constructor(init?: FieldFieldInit);
34
+ /** (re)scan the document for `[data-body]` bodies after a layout change. */
35
+ scan(): void;
36
+ /** alias of `scan`. */
37
+ rescan(): void;
38
+ /** recolor the travelling accent (§9). */
39
+ setAccent(hex: string): void;
40
+ /** swap the accent's color template live: a built-in name or custom hex stops (§9). */
41
+ setPalette(palette: string | readonly string[]): void;
42
+ /** switch the global formation (§7). */
43
+ setFormation(name: string): void;
44
+ /** toggle conserved attention (§2.4) live — one finite strength budget. */
45
+ setAttention(on: boolean): void;
46
+ /** toggle cross-boundary causality (Concept 4) live — density spills to neighbours. */
47
+ setCausality(on: boolean): void;
48
+ /** toggle the density heatmap layer (field-systems H1) live. */
49
+ setHeatmap(on: boolean): void;
50
+ /** switch the underlay render mode (§20.6) live. */
51
+ setRender(mode: Parameters<FieldHandle['setRender']>[0]): void;
52
+ /** render a field-structure visualization on the overlay surface (in front of content). */
53
+ setOverlay(mode: Parameters<FieldHandle['setOverlay']>[0]): void;
54
+ /** wire glowing connector lines between a set, or clear with null (§10). */
55
+ threads(list: ThreadLink[] | null): void;
56
+ /** a discrete one-shot: shove + heat matter near (x, y), optionally tinting it (§11). */
57
+ burst(x: number, y: number, hex?: string): void;
58
+ /** place/move a dynamic flow focus the field bends toward — pulls matter, curves the streamlines. */
59
+ flowTo(x: number, y: number, opts?: FlowOptions): void;
60
+ /** remove the flow focus. */
61
+ clearFlow(): void;
62
+ seed(atoms: readonly AtomPayload[]): void;
63
+ atomAt(x: number, y: number): AtomPayload | null;
64
+ focusAt(x: number, y: number): AtomPayload | null;
65
+ clearFocus(): void;
66
+ particleCount(): number;
67
+ /** copy live particle state into `out` (stride 5: x, y, z, heat, size); returns the count written. */
68
+ readParticles(out: Float32Array): number;
69
+ energy(): {
70
+ kinetic: number;
71
+ thermal: number;
72
+ total: number;
73
+ count: number;
74
+ };
75
+ /** sample the live field at `(x, y)` — the net force vector a still test particle would feel. */
76
+ sample(x: number, y: number): {
77
+ x: number;
78
+ y: number;
79
+ };
80
+ scrollV(): number;
81
+ setVisible(on: boolean): void;
82
+ setBackground(mode: Parameters<FieldHandle['setBackground']>[0]): void;
83
+ /** stop the loop, release listeners, and remove the canvas if this instance made it. */
84
+ destroy(): void;
85
+ }
86
+ //# sourceMappingURL=field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../src/field.ts"],"names":[],"mappings":"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"}
package/dist/field.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * `FieldField` — the reciprocal DOM-physics field as a typed class, for plain TypeScript
3
+ * apps that want object-oriented ergonomics without a framework or a custom element.
4
+ *
5
+ * `new FieldField()` builds a managed, full-viewport canvas and starts the engine on it;
6
+ * pass `{ canvas }` to drive a `<canvas>` you own instead. The class implements the full
7
+ * `FieldHandle` surface, so an instance is type-compatible anywhere a handle is expected,
8
+ * and it exposes the `canvas` it renders to.
9
+ *
10
+ * ```ts
11
+ * import { FieldField } from '@fundamental-engine/vanilla';
12
+ *
13
+ * const field = new FieldField({ accent: '#4da3ff', render: 'dots' });
14
+ * field.setFormation('wells');
15
+ * field.burst(window.innerWidth / 2, 200);
16
+ * // field.scan(); field.destroy();
17
+ * ```
18
+ */
19
+ import {} from '@fundamental-engine/core';
20
+ import { createBrowserField } from '@fundamental-engine/platform';
21
+ import { makeFieldCanvas, assertBrowser } from "./mount.js";
22
+ export class FieldField {
23
+ /** the `<canvas>` the field renders to — the one created for you, or the one you passed. */
24
+ canvas;
25
+ field;
26
+ /** did this instance create the canvas (and so should remove it on `destroy()`)? */
27
+ managed;
28
+ constructor(init = {}) {
29
+ assertBrowser(); // browser-only: fail loudly during SSR instead of a cryptic crash
30
+ const { canvas, target, ...opts } = init;
31
+ this.managed = !canvas;
32
+ this.canvas = canvas ?? makeFieldCanvas(target);
33
+ this.field = createBrowserField(this.canvas, opts);
34
+ }
35
+ /** (re)scan the document for `[data-body]` bodies after a layout change. */
36
+ scan() {
37
+ this.field.scan();
38
+ }
39
+ /** alias of `scan`. */
40
+ rescan() {
41
+ this.field.rescan();
42
+ }
43
+ /** recolor the travelling accent (§9). */
44
+ setAccent(hex) {
45
+ this.field.setAccent(hex);
46
+ }
47
+ /** swap the accent's color template live: a built-in name or custom hex stops (§9). */
48
+ setPalette(palette) {
49
+ this.field.setPalette(palette);
50
+ }
51
+ /** switch the global formation (§7). */
52
+ setFormation(name) {
53
+ this.field.setFormation(name);
54
+ }
55
+ /** toggle conserved attention (§2.4) live — one finite strength budget. */
56
+ setAttention(on) {
57
+ this.field.setAttention(on);
58
+ }
59
+ /** toggle cross-boundary causality (Concept 4) live — density spills to neighbours. */
60
+ setCausality(on) {
61
+ this.field.setCausality(on);
62
+ }
63
+ /** toggle the density heatmap layer (field-systems H1) live. */
64
+ setHeatmap(on) {
65
+ this.field.setHeatmap(on);
66
+ }
67
+ /** switch the underlay render mode (§20.6) live. */
68
+ setRender(mode) {
69
+ this.field.setRender(mode);
70
+ }
71
+ /** render a field-structure visualization on the overlay surface (in front of content). */
72
+ setOverlay(mode) {
73
+ this.field.setOverlay(mode);
74
+ }
75
+ /** wire glowing connector lines between a set, or clear with null (§10). */
76
+ threads(list) {
77
+ this.field.threads(list);
78
+ }
79
+ /** a discrete one-shot: shove + heat matter near (x, y), optionally tinting it (§11). */
80
+ burst(x, y, hex) {
81
+ this.field.burst(x, y, hex);
82
+ }
83
+ /** place/move a dynamic flow focus the field bends toward — pulls matter, curves the streamlines. */
84
+ flowTo(x, y, opts) {
85
+ this.field.flowTo(x, y, opts);
86
+ }
87
+ /** remove the flow focus. */
88
+ clearFlow() {
89
+ this.field.clearFlow();
90
+ }
91
+ seed(atoms) {
92
+ this.field.seed(atoms);
93
+ }
94
+ atomAt(x, y) {
95
+ return this.field.atomAt(x, y);
96
+ }
97
+ focusAt(x, y) {
98
+ return this.field.focusAt(x, y);
99
+ }
100
+ clearFocus() {
101
+ this.field.clearFocus();
102
+ }
103
+ particleCount() {
104
+ return this.field.particleCount();
105
+ }
106
+ /** copy live particle state into `out` (stride 5: x, y, z, heat, size); returns the count written. */
107
+ readParticles(out) {
108
+ return this.field.readParticles(out);
109
+ }
110
+ energy() {
111
+ return this.field.energy();
112
+ }
113
+ /** sample the live field at `(x, y)` — the net force vector a still test particle would feel. */
114
+ sample(x, y) {
115
+ return this.field.sample(x, y);
116
+ }
117
+ scrollV() {
118
+ return this.field.scrollV();
119
+ }
120
+ setVisible(on) {
121
+ this.field.setVisible(on);
122
+ }
123
+ setBackground(mode) {
124
+ this.field.setBackground(mode);
125
+ }
126
+ /** stop the loop, release listeners, and remove the canvas if this instance made it. */
127
+ destroy() {
128
+ this.field.destroy();
129
+ if (this.managed)
130
+ this.canvas.remove();
131
+ }
132
+ }
133
+ //# sourceMappingURL=field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field.js","sourceRoot":"","sources":["../src/field.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAA4F,MAAM,0BAA0B,CAAC;AACpI,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAU5D,MAAM,OAAO,UAAU;IACrB,4FAA4F;IACnF,MAAM,CAAoB;IAClB,KAAK,CAAc;IACpC,oFAAoF;IACnE,OAAO,CAAU;IAElC,YAAY,OAAuB,EAAE;QACnC,aAAa,EAAE,CAAC,CAAC,kEAAkE;QACnF,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;QACzC,IAAI,CAAC,OAAO,GAAG,CAAC,MAAM,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC;QAChD,IAAI,CAAC,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACrD,CAAC;IAED,4EAA4E;IAC5E,IAAI;QACF,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;IACD,uBAAuB;IACvB,MAAM;QACJ,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;IACtB,CAAC;IACD,0CAA0C;IAC1C,SAAS,CAAC,GAAW;QACnB,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IACD,uFAAuF;IACvF,UAAU,CAAC,OAAmC;QAC5C,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC;IACD,wCAAwC;IACxC,YAAY,CAAC,IAAY;QACvB,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IACD,2EAA2E;IAC3E,YAAY,CAAC,EAAW;QACtB,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC;IACD,uFAAuF;IACvF,YAAY,CAAC,EAAW;QACtB,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC;IACD,gEAAgE;IAChE,UAAU,CAAC,EAAW;QACpB,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;IACD,oDAAoD;IACpD,SAAS,CAAC,IAA6C;QACrD,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IACD,2FAA2F;IAC3F,UAAU,CAAC,IAA8C;QACvD,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IACD,4EAA4E;IAC5E,OAAO,CAAC,IAAyB;QAC/B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IACD,yFAAyF;IACzF,KAAK,CAAC,CAAS,EAAE,CAAS,EAAE,GAAY;QACtC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9B,CAAC;IACD,qGAAqG;IACrG,MAAM,CAAC,CAAS,EAAE,CAAS,EAAE,IAAkB;QAC7C,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IAChC,CAAC;IACD,6BAA6B;IAC7B,SAAS;QACP,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;IACzB,CAAC;IACD,IAAI,CAAC,KAA6B;QAChC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IACD,MAAM,CAAC,CAAS,EAAE,CAAS;QACzB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,CAAC,CAAS,EAAE,CAAS;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,UAAU;QACR,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;IAC1B,CAAC;IACD,aAAa;QACX,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;IACpC,CAAC;IACD,sGAAsG;IACtG,aAAa,CAAC,GAAiB;QAC7B,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACvC,CAAC;IACD,MAAM;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;IAC7B,CAAC;IACD,iGAAiG;IACjG,MAAM,CAAC,CAAS,EAAE,CAAS;QACzB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO;QACL,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;IAC9B,CAAC;IACD,UAAU,CAAC,EAAW;QACpB,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;IACD,aAAa,CAAC,IAAiD;QAC7D,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IACD,wFAAwF;IACxF,OAAO;QACL,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACrB,IAAI,IAAI,CAAC,OAAO;YAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;IACzC,CAAC;CACF"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@fundamental-engine/vanilla` — the framework-free TypeScript door to the reciprocal field.
3
+ *
4
+ * The same engine the `<field-root>` custom element and the React `<FieldField>` wrap,
5
+ * exposed as a typed `FieldField` class and the imperative `mountField()` / `createField()`,
6
+ * with **no** custom-element registration and **no** framework dependency. This package has
7
+ * no side effects: importing it never defines a custom element. Reach for it from plain
8
+ * TypeScript, or any stack where you want to drive the field by hand.
9
+ *
10
+ * Spec: `docs/engine-reference/forces-system.md`.
11
+ */
12
+ export { FieldField } from './field.ts';
13
+ export type { FieldFieldInit } from './field.ts';
14
+ export { mountField, makeFieldCanvas } from './mount.ts';
15
+ export type { MountOptions } from './mount.ts';
16
+ export { createBrowserField as createField, browserHost } from '@fundamental-engine/platform';
17
+ export type { FieldHandle, FieldOptions, ThreadLink, Particle, Body, Force, Formation, Vec2, } from '@fundamental-engine/core';
18
+ export { FORCES, FORMATIONS, CONDITIONS, PALETTE } from '@fundamental-engine/core';
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK/C,OAAO,EAAE,kBAAkB,IAAI,WAAW,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC9F,YAAY,EACV,WAAW,EACX,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,SAAS,EACT,IAAI,GACL,MAAM,0BAA0B,CAAC;AAIlC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@fundamental-engine/vanilla` — the framework-free TypeScript door to the reciprocal field.
3
+ *
4
+ * The same engine the `<field-root>` custom element and the React `<FieldField>` wrap,
5
+ * exposed as a typed `FieldField` class and the imperative `mountField()` / `createField()`,
6
+ * with **no** custom-element registration and **no** framework dependency. This package has
7
+ * no side effects: importing it never defines a custom element. Reach for it from plain
8
+ * TypeScript, or any stack where you want to drive the field by hand.
9
+ *
10
+ * Spec: `docs/engine-reference/forces-system.md`.
11
+ */
12
+ export { FieldField } from "./field.js";
13
+ export { mountField, makeFieldCanvas } from "./mount.js";
14
+ // The engine entry, wired to the browser host (core is renderer-agnostic and requires a host).
15
+ // `createBrowserField` = `createField` + `browserHost()`; re-exported here as `createField` so the
16
+ // framework-free door stays a one-liner. `browserHost` is re-exported for custom wiring.
17
+ export { createBrowserField as createField, browserHost } from '@fundamental-engine/platform';
18
+ // The catalog data a vanilla UI commonly reads — the force list, formations, `data-when`
19
+ // gates, and the palette — so a force picker or legend needs no second install.
20
+ export { FORCES, FORMATIONS, CONDITIONS, PALETTE } from '@fundamental-engine/core';
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGzD,+FAA+F;AAC/F,mGAAmG;AACnG,yFAAyF;AACzF,OAAO,EAAE,kBAAkB,IAAI,WAAW,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAY9F,yFAAyF;AACzF,gFAAgF;AAChF,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `mountField` — the framework-free imperative mount (no custom element, no framework).
3
+ *
4
+ * Creates a fixed, full-viewport canvas behind the page, starts the engine on it, and
5
+ * returns the `FieldHandle` (whose `destroy()` also removes the canvas it made). It is the
6
+ * same engine the `<field-root>` custom element and the React `<FieldField>` wrap, for
7
+ * plain scripts and imperative mounts: `const field = mountField(); field.scan()`.
8
+ *
9
+ * This is the canonical home of the imperative mount; `@fundamental-engine/elements` re-exports it.
10
+ * For object-oriented ergonomics (and driving a canvas you own), see the `FieldField` class.
11
+ */
12
+ import { type FieldHandle, type FieldOptions } from '@fundamental-engine/core';
13
+ export interface MountOptions extends FieldOptions {
14
+ /** where to append the canvas; defaults to `document.body`. */
15
+ target?: HTMLElement;
16
+ }
17
+ /**
18
+ * Throw a clear error when there is no DOM (server-side render or build step). The field is a
19
+ * browser-only, client-side effect; construct it on the client (a `useEffect` / `onMount` /
20
+ * "client only" boundary), never during SSR. Call before any `document`/`window` access.
21
+ */
22
+ export declare function assertBrowser(): void;
23
+ /**
24
+ * Create the fixed, full-viewport, decorative canvas the managed mounts run on. Internal —
25
+ * shared by `mountField` and the `FieldField` class so the styling lives in one place.
26
+ */
27
+ export declare function makeFieldCanvas(target?: HTMLElement): HTMLCanvasElement;
28
+ /** Mount and start the field; returns the handle. `destroy()` also removes the canvas. */
29
+ export declare function mountField(opts?: MountOptions): FieldHandle;
30
+ //# sourceMappingURL=mount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAoB,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGjG,MAAM,WAAW,YAAa,SAAQ,YAAY;IAChD,+DAA+D;IAC/D,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAOpC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,GAAE,WAA2B,GAAG,iBAAiB,CAMtF;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,IAAI,GAAE,YAAiB,GAAG,WAAW,CAY/D"}
package/dist/mount.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `mountField` — the framework-free imperative mount (no custom element, no framework).
3
+ *
4
+ * Creates a fixed, full-viewport canvas behind the page, starts the engine on it, and
5
+ * returns the `FieldHandle` (whose `destroy()` also removes the canvas it made). It is the
6
+ * same engine the `<field-root>` custom element and the React `<FieldField>` wrap, for
7
+ * plain scripts and imperative mounts: `const field = mountField(); field.scan()`.
8
+ *
9
+ * This is the canonical home of the imperative mount; `@fundamental-engine/elements` re-exports it.
10
+ * For object-oriented ergonomics (and driving a canvas you own), see the `FieldField` class.
11
+ */
12
+ import { FIELD_CANVAS_CSS } from '@fundamental-engine/core';
13
+ import { createBrowserField } from '@fundamental-engine/platform';
14
+ /**
15
+ * Throw a clear error when there is no DOM (server-side render or build step). The field is a
16
+ * browser-only, client-side effect; construct it on the client (a `useEffect` / `onMount` /
17
+ * "client only" boundary), never during SSR. Call before any `document`/`window` access.
18
+ */
19
+ export function assertBrowser() {
20
+ if (typeof document === 'undefined' || typeof window === 'undefined') {
21
+ throw new Error('field-ui: the field runs in the browser only. Create it on the client (inside ' +
22
+ 'useEffect / onMount / a "client only" boundary), not during server-side rendering.');
23
+ }
24
+ }
25
+ /**
26
+ * Create the fixed, full-viewport, decorative canvas the managed mounts run on. Internal —
27
+ * shared by `mountField` and the `FieldField` class so the styling lives in one place.
28
+ */
29
+ export function makeFieldCanvas(target = document.body) {
30
+ const canvas = document.createElement('canvas');
31
+ canvas.setAttribute('aria-hidden', 'true'); // decorative field (§18 a11y)
32
+ canvas.style.cssText = FIELD_CANVAS_CSS; // single source of truth (core/surface.ts)
33
+ target.appendChild(canvas);
34
+ return canvas;
35
+ }
36
+ /** Mount and start the field; returns the handle. `destroy()` also removes the canvas. */
37
+ export function mountField(opts = {}) {
38
+ assertBrowser();
39
+ const { target = document.body, ...fieldOpts } = opts;
40
+ const canvas = makeFieldCanvas(target);
41
+ const field = createBrowserField(canvas, fieldOpts);
42
+ return {
43
+ ...field,
44
+ destroy: () => {
45
+ field.destroy();
46
+ canvas.remove();
47
+ },
48
+ };
49
+ }
50
+ //# sourceMappingURL=mount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,gBAAgB,EAAuC,MAAM,0BAA0B,CAAC;AACjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAOlE;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CACb,gFAAgF;YAC9E,oFAAoF,CACvF,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,SAAsB,QAAQ,CAAC,IAAI;IACjE,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,8BAA8B;IAC1E,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,gBAAgB,CAAC,CAAC,2CAA2C;IACpF,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,UAAU,CAAC,OAAqB,EAAE;IAChD,aAAa,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,SAAS,EAAE,GAAG,IAAI,CAAC;IACtD,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,kBAAkB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpD,OAAO;QACL,GAAG,KAAK;QACR,OAAO,EAAE,GAAG,EAAE;YACZ,KAAK,CAAC,OAAO,EAAE,CAAC;YAChB,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@fundamental-engine/vanilla",
3
+ "version": "0.4.0",
4
+ "description": "Framework-free TypeScript wrapper for Fundamental — the reciprocal DOM-physics field as a typed FieldField class + mountField(), with no custom-element registration and no framework dependency.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Zach Shallbetter <hi@zachshallbetter.com> (https://zachshallbetter.com)",
8
+ "homepage": "https://fundamental-engine.com",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/zachshallbetter/fundamental-engine.git",
12
+ "directory": "packages/vanilla"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/zachshallbetter/fundamental-engine/issues"
16
+ },
17
+ "keywords": [
18
+ "Fundamental",
19
+ "typescript",
20
+ "vanilla",
21
+ "particles",
22
+ "physics",
23
+ "canvas",
24
+ "field",
25
+ "reciprocal",
26
+ "dom"
27
+ ],
28
+ "sideEffects": false,
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "main": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js"
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "dependencies": {
50
+ "@fundamental-engine/platform": "0.4.0",
51
+ "@fundamental-engine/core": "0.4.0"
52
+ },
53
+ "devDependencies": {
54
+ "typescript": "^5.9.3"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc -p tsconfig.json",
58
+ "typecheck": "tsc -p tsconfig.json --noEmit",
59
+ "test": "node --test"
60
+ }
61
+ }