@jayf0x/fluidity-js 0.2.5 → 0.2.7

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/AGENTS.md CHANGED
@@ -1,13 +1,6 @@
1
- # AGENTS.mdfluidity-js
1
+ # fluidity-jsAgent Guide
2
2
 
3
- > Universal agent instructions for GitHub Copilot, Cursor, GPT-4, Gemini, and all AI coding assistants.
4
- > Claude Code users: see [CLAUDE.md](./CLAUDE.md) for deeper architecture notes.
5
-
6
- ---
7
-
8
- ## What this repo is
9
-
10
- `@jayf0x/fluidity-js` — a WebGPU-first fluid simulation library for React. Implements a real-time Navier-Stokes solver (advection, divergence, pressure, vorticity confinement) that renders onto a `<canvas>` element. Falls back automatically to WebGL2 → WebGL1. Runs the heavy simulation loop in a Web Worker via OffscreenCanvas by default.
3
+ WebGPU-first fluid simulation React library. Real-time Navier-Stokes solver (advection, divergence, pressure, vorticity confinement) on a `<canvas>`. Automatic fallback: WebGPU → WebGL2 → WebGL1. Simulation runs in a Web Worker via OffscreenCanvas by default.
11
4
 
12
5
  Two components: `<FluidText>` (text as obstacle/background) and `<FluidImage>` (bitmap as obstacle/background).
13
6
 
@@ -15,29 +8,32 @@ Two components: `<FluidText>` (text as obstacle/background) and `<FluidImage>` (
15
8
 
16
9
  ---
17
10
 
18
- ## Repo layout (critical paths)
11
+ ## Repo layout
19
12
 
20
13
  ```
21
14
  src/
22
15
  index.ts ← public exports
23
- globals.d.ts ← ambient global types (FluidConfig, FluidHandle…)
16
+ globals.d.ts ← ambient global types (FluidConfig, FluidHandle, FluidBaseProps…) — no import needed
17
+ index.d.ts ← public module declarations for consumers
24
18
  core/
25
- config.ts ← DEFAULT_CONFIG, presets, mergeConfig
26
- gl-utils.ts ← WebGL context + FBO helpers
19
+ config.ts ← DEFAULT_CONFIG, DEFAULT_CONFIG_TEXT, DEFAULT_PROPS_*, DEFAULT_QUALITY,
20
+ PRESETS, PROP_RANGES, mergeConfig, normalizeConfig
21
+ gl-utils.ts ← WebGL context + FBO helpers, Program class, createBlit
27
22
  gpu-utils.ts ← WebGPU helpers
28
23
  shaders.ts ← GLSL shader strings
29
24
  wgsl-shaders.ts ← WGSL shader strings
30
- simulation.ts ← FluidSimulation class (dual WebGPU/WebGL)
25
+ simulation.ts ← FluidSimulation class (dual WebGPU/WebGL); use FluidSimulation.create() for WebGPU-first
31
26
  textures.ts ← texture creation for text + image modes
32
27
  worker/index.ts ← Web Worker message handler
33
28
  fluid-controller.ts ← worker vs main-thread abstraction
34
29
  react/
35
- FluidText.tsx ← React component
36
- FluidImage.tsx ← React component
30
+ FluidText.tsx ← React component (forwardRef)
31
+ FluidImage.tsx ← React component (forwardRef), imageSize prop
37
32
  useFluid.ts ← core hook: canvas lifecycle + controller
38
- tests/ ← Vitest + jsdom (83 tests)
39
- demo/ ← standalone Vite 5 demo site (NOT the library)
33
+ tests/ ← Vitest + jsdom
34
+ demo/ ← standalone Vite 5 demo site (NOT the library; uses alias to src/)
40
35
  dist/ ← built output (do not edit)
36
+ backlog.md ← prioritised bug/feature backlog
41
37
  ```
42
38
 
43
39
  ---
@@ -45,19 +41,21 @@ dist/ ← built output (do not edit)
45
41
  ## Commands
46
42
 
47
43
  ```bash
48
- # Run tests (library root, Node 16 compat)
44
+ # Run tests (library root)
49
45
  bun test:claude # preferred: runs vitest, tails last 8 lines
50
46
 
51
47
  # Build library
52
- bun build
48
+ bun build # → dist/
53
49
 
54
50
  # Demo (requires Node 20)
55
51
  cd demo
56
52
  PATH=/Users/me/.nvm/versions/node/v20.19.6/bin:$PATH bun dev
57
53
  PATH=/Users/me/.nvm/versions/node/v20.19.6/bin:$PATH bun build
58
- PATH=/Users/me/.nvm/versions/node/v20.19.6/bin:$PATH bun deploy # → gh-pages
54
+ PATH=/Users/me/.nvm/versions/node/v20.19.6/bin:$PATH bun deploy # → gh-pages
59
55
  ```
60
56
 
57
+ **Node version:** library root uses Node 16+ deps (vite@4, vitest@0.34); demo requires Node 20 (Vite 5).
58
+
61
59
  ---
62
60
 
63
61
  ## Public API
@@ -69,17 +67,25 @@ PATH=/Users/me/.nvm/versions/node/v20.19.6/bin:$PATH bun deploy # → gh-pages
69
67
  <FluidImage src="/hero.jpg" algorithm="aurora" pixelRatio={1} simResolution={0.5} />
70
68
  ```
71
69
 
70
+ All `FluidConfig` fields are **flat optional props** on both components — there is no nested `config` object. `FluidBaseProps extends Partial<FluidConfig>` to avoid duplicate field declarations.
71
+
72
72
  ### FluidHandle ref
73
73
 
74
74
  ```ts
75
75
  interface FluidHandle {
76
76
  reset(): void;
77
- move(opts: { x: number; y: number; strength?: number }): void;
78
- splat(x: number, y: number, vx: number, vy: number, strength?: number): void;
77
+ move(x: number, y: number, strength?: number): void;
78
+ splat(x: number, y: number, velocityX: number, velocityY: number, strength?: number): void;
79
79
  updateConfig(config: Partial<FluidConfig>): void;
80
80
  }
81
81
  ```
82
82
 
83
+ `splat()` writes directly to velocity+density FBOs — safe to call multiple times per frame. `move()` goes through the mouse-state machine (one splat per sim step, last-write-wins).
84
+
85
+ ### Quality props
86
+
87
+ `pixelRatio` and `simResolution` are flat top-level props (both `number`, range `0.1–1`). They map to internal `FluidQuality = { dpr, sim }` at the controller boundary — `FluidQuality` is an internal type, not a component prop.
88
+
83
89
  ### Algorithms
84
90
  `'standard'` · `'glass'` · `'ink'` · `'aurora'` · `'ripple'`
85
91
 
@@ -88,15 +94,24 @@ interface FluidHandle {
88
94
 
89
95
  ---
90
96
 
91
- ## Code style / conventions
97
+ ## Normalized prop API
92
98
 
93
- - **TypeScript strict** no `any`, no non-null assertions without a comment
94
- - **No file extensions** on imports within `src/` (e.g. `from './config'`)
95
- Exception: worker import keeps `.js?worker&inline` for Vite query string
96
- - **No default exports** from library modules; named exports only
97
- - **No comments** unless the *why* is non-obvious (hidden constraint, invariant, workaround)
98
- - **Formatting:** Prettier with `@trivago/prettier-plugin-sort-imports`
99
- - **Tests:** All 83 must pass before any commit. Add tests for new behaviour.
99
+ Seven simulation props accept a **normalized `0–1` value** instead of raw physics units. `normalizeConfig` (in `src/core/config.ts`) maps them to physics range before the sim receives them. Values outside `[0, 1]` pass through unchanged as raw physics overrides.
100
+
101
+ Normalized fields: `densityDissipation`, `velocityDissipation`, `splatRadius`, `splatForce`, `specularExp`, `shine`, `warpStrength`.
102
+
103
+ `DEFAULT_CONFIG` and presets store normalized values; `normalizeConfig` converts them to physics. See `PROP_RANGES` in `src/core/config.ts` for the exact min/max mappings.
104
+
105
+ ---
106
+
107
+ ## Simulation pipeline (per frame, `simulation.ts#step`)
108
+
109
+ 1. Advect velocity (obstacle mask zeroes inside text/image)
110
+ 2. Advect density
111
+ 3. Curl → vorticity confinement
112
+ 4. Splat — mouse move OR direct `splat()` calls → velocity + density FBOs
113
+ 5. Divergence → pressure solve (N iterations) → gradient subtract
114
+ 6. Display pass: 5 texture units bound (`uTexture`, `uObstacle`, `uBackground`, `uCoverage`, `uVelocity`); uniforms: `uAlgorithm`, `uWarpStrength`, `uWaterColor`, `uGlowColor`, `uRefraction`, `uSpecularExp`, `uShine`
100
115
 
101
116
  ---
102
117
 
@@ -104,45 +119,116 @@ interface FluidHandle {
104
119
 
105
120
  | Rule | Reason |
106
121
  |------|--------|
107
- | `transferControlToOffscreen` called at most once per canvas | Irreversible — double-mount in StrictMode would crash |
108
- | `useFluid` creates a **fresh** `<canvas>` element each mount inside a container `<div>` | StrictMode safety |
122
+ | `transferControlToOffscreen` called at most once per canvas | Irreversible — double-mount in React StrictMode crashes |
123
+ | `useFluid` creates a **fresh** `<canvas>` each mount inside a container `<div>`, removes on cleanup | StrictMode safety |
109
124
  | `createBlit` sets up vertex buffers **once** | Performance — never call `vertexAttribPointer` per draw |
110
- | Worker `.terminate()` always deferred 50ms after `postMessage({type:'destroy'})` | Lets the worker flush its destroy sequence |
111
- | Display shader outputs **premultiplied alpha** | Transparent canvas compositing correctness |
112
- | Coverage texture ref: `text` mode → `coverageTex === obstacleTex`; guard against double-delete | Memory safety |
125
+ | Worker `.terminate()` always deferred 50 ms after `postMessage({type:'destroy'})` | Lets the worker flush its destroy sequence |
126
+ | Display shader outputs **premultiplied alpha** (`vec4(color * alpha, alpha)`) | Transparent canvas compositing correctness |
127
+ | Text mode: `coverageTex === obstacleTex` (same ref); guard against double-delete in `#disposeTextures` | Memory safety |
128
+ | Background colour samples masked by coverage: `mix(uWaterColor, texture2D(uBackground, uv).rgb, coverage)` | Prevents CSS `backgroundColor` contamination in transparent areas |
113
129
 
114
130
  ---
115
131
 
116
- ## What agents MUST NOT do
132
+ ## Critical implementation notes
133
+
134
+ ### React StrictMode + OffscreenCanvas
135
+
136
+ `transferControlToOffscreen` is irreversible. StrictMode double-mounts → double transfer → crash. Fix: `useFluid.ts` creates a fresh `<canvas>` per mount; `FluidController` has a try/catch fallback to main-thread mode.
137
+
138
+ ### Worker destroy pattern
139
+
140
+ ```ts
141
+ destroy() {
142
+ const worker = this.#worker;
143
+ this.#worker = null; // null first — prevents double-use
144
+ worker.postMessage({ type: 'destroy' });
145
+ setTimeout(() => worker.terminate(), 50); // captured ref is safe
146
+ }
147
+ ```
148
+
149
+ ### Transparent canvas
150
+
151
+ WebGL context: `alpha: true`. `gl.clearColor(0,0,0,0)`. Coverage texture (`uCoverage`) = binary mask of content area → empty space is transparent → CSS `backgroundColor` prop shows through.
152
+
153
+ - Text mode: `coverageTex === obstacleTex`
154
+ - Image mode: separate white-rect coverage texture
117
155
 
118
- - **Do not** edit files under `dist/` — these are build artefacts
119
- - **Do not** add `// eslint-disable` or `@ts-ignore` without explaining why in the PR
120
- - **Do not** introduce new peer dependencies without updating `peerDependencies` in `package.json`
121
- - **Do not** call `vertexAttribPointer` inside the render loop
122
- - **Do not** call `transferControlToOffscreen` more than once per canvas element
123
- - **Do not** use `window.*` APIs directly inside `worker/index.ts` (worker context)
124
- - **Do not** commit if `bun test:claude` fails
125
- - **Do not** change the module format from ESM to CJS — consumers depend on tree-shaking
126
- - **Do not** modify `CLAUDE.md` unless explicitly asked — it is the authoritative agent guide
156
+ ### DPR-aware sizing
157
+
158
+ All canvas sizing multiplies `clientWidth/clientHeight` by `window.devicePixelRatio * clampedDprRef.current`. The ResizeObserver reads a ref (not a closure) so it always uses the current `pixelRatio`. Mouse/splat CSS-pixel coordinates are multiplied by `#dpr` before normalising to UV space.
159
+
160
+ ### Preset + config reactivity
161
+
162
+ `useEffect([preset, configKey])` calls `updateConfig(mergeConfig(configProps, preset))` whenever any flat config prop or preset changes. `configKey = JSON.stringify(configProps)` is the effect dep.
163
+
164
+ ### Quality reactivity
165
+
166
+ `useEffect([pixelRatio, simResolution])` updates `clampedDprRef`, calls `controller.updateQuality(...)`, then `controller.resize(w * newDpr, h * newDpr)`. Compares against `prevQualityRef` to skip first mount and unchanged values.
167
+
168
+ ### backgroundSrc bitmap lifecycle
169
+
170
+ Main thread loads bitmap via `loadImageBitmap()`, transfers to worker zero-copy via `postMessage([bitmap])`. Worker stores in `#backgroundBitmap`. Old bitmap `.close()`d on change; `.close()` again on destroy.
171
+
172
+ ---
173
+
174
+ ## Code style
175
+
176
+ - **TypeScript strict** — no `any`, no non-null assertions without an explanatory comment
177
+ - **No file extensions** on imports within `src/` (e.g. `from './config'`)
178
+ Exception: worker import keeps `.js?worker&inline` (Vite query string must be adjacent to the path)
179
+ - **Named exports only** from library modules; no default exports
180
+ - **No comments** unless the *why* is non-obvious
181
+ - **Formatting:** Prettier with `@trivago/prettier-plugin-sort-imports`
182
+
183
+ ---
184
+
185
+ ## What agents must not do
186
+
187
+ - Edit files under `dist/` — build artefacts only
188
+ - Add `// eslint-disable` or `@ts-ignore` without a comment explaining why
189
+ - Introduce new peer dependencies without updating `peerDependencies` in `package.json`
190
+ - Call `vertexAttribPointer` inside the render loop
191
+ - Call `transferControlToOffscreen` more than once per canvas element
192
+ - Use `window.*` APIs inside `worker/index.ts` (worker context has no `window`)
193
+ - Commit if `bun test:claude` fails — all tests must pass
194
+ - Change the module format from ESM to CJS — consumers depend on tree-shaking
195
+ - Import types from `globals.d.ts` — they are globally ambient after install
127
196
 
128
197
  ---
129
198
 
130
199
  ## Testing notes
131
200
 
132
- - Tests run under jsdom with a WebGL mock (`tests/setup.js`). `navigator.gpu` is absent — all tests exercise the WebGL path.
133
- - `FluidText`/`FluidImage` are `forwardRef` ExoticComponents — check `$$typeof`, not `typeof`.
201
+ Tests run under jsdom with a WebGL mock (`tests/setup.js`). `navigator.gpu` is absent — all tests exercise the WebGL path.
202
+
203
+ - `FluidText`/`FluidImage` are `forwardRef` ExoticComponents — check `$$typeof`, not `typeof`
134
204
  - React component mocks require `.ts` extension: `vi.mock('../../src/fluid-controller.ts', ...)`
135
205
  - Worker mock: `vi.mock('../src/worker/index.ts?worker&inline', ...)`
206
+ - Run with `bun test:claude`; add tests for any new behaviour
207
+
208
+ ---
209
+
210
+ ## GitHub issue labels
211
+
212
+ Every issue carries three label groups. Use all three when filing or picking up work.
213
+
214
+ **`type:*`** — `type:bug` · `type:feature` · `type:improvement` · `type:docs` · `type:refactor`
215
+
216
+ **`domain:*`** — `domain:core` · `domain:render` · `domain:physics` · `domain:react` · `domain:dx`
217
+
218
+ **`effort:*`** — `effort:1` (trivial, <30 min) · `effort:2` (few hours) · `effort:3` (half–full day) · `effort:4` (multi-day) · `effort:5` (architectural)
219
+
220
+ Full label reference: [CONTRIBUTING.md § Labels](./CONTRIBUTING.md#labels)
136
221
 
137
222
  ---
138
223
 
139
224
  ## Where to find more
140
225
 
141
- | Resource | Path |
142
- |----------|------|
143
- | Deep architecture guide (Claude-specific) | [CLAUDE.md](./CLAUDE.md) |
144
- | Prioritised bug/feature backlog | [backlog.md](./backlog.md) |
226
+ | Resource | Path / URL |
227
+ |----------|------------|
228
+ | Simulation config defaults + presets | [src/core/config.ts](./src/core/config.ts) |
229
+ | Ambient type declarations | [src/globals.d.ts](./src/globals.d.ts) |
230
+ | Bug/feature backlog | [backlog.md](./backlog.md) |
145
231
  | Version history | [changelog.md](./changelog.md) |
146
- | Live demo source | [demo/src/examples/](./demo/src/examples/) |
147
- | npm package | https://www.npmjs.com/package/@jayf0x/fluidity-js |
232
+ | Demo examples | [demo/src/examples/](./demo/src/examples/) |
233
+ | npm | https://www.npmjs.com/package/@jayf0x/fluidity-js |
148
234
  | Live demo | https://jayf0x.github.io/fluidity |
package/CONTRIBUTING.md CHANGED
@@ -40,6 +40,37 @@ PATH=/Users/me/.nvm/versions/node/v20.19.6/bin:$PATH bun dev
40
40
  | Fix React lifecycle | `src/react/useFluid.ts`, `src/fluid-controller.ts` |
41
41
  | Fix worker communication | `src/worker/index.ts`, `src/fluid-controller.ts` |
42
42
 
43
+ ## Labels
44
+
45
+ Every issue carries three label groups:
46
+
47
+ ### `type:*`
48
+ | Label | Use for |
49
+ |-------|---------|
50
+ | `type:bug` | Something broken |
51
+ | `type:feature` | New prop, algorithm, or API |
52
+ | `type:improvement` | Perf, visual quality, or correctness enhancement |
53
+ | `type:docs` | Documentation fix |
54
+ | `type:refactor` | Code restructure, no behaviour change |
55
+
56
+ ### `domain:*`
57
+ | Label | Scope |
58
+ |-------|-------|
59
+ | `domain:core` | Props, config, public API |
60
+ | `domain:render` | Shaders, FBOs, display pass |
61
+ | `domain:physics` | Fluid solver, pressure, advection |
62
+ | `domain:react` | React components and hooks |
63
+ | `domain:dx` | Build, packaging, DX |
64
+
65
+ ### `effort:*`
66
+ | Label | Meaning |
67
+ |-------|---------|
68
+ | `effort:1` | Trivial — under 30 min |
69
+ | `effort:2` | Small — a few hours |
70
+ | `effort:3` | Medium — half to full day |
71
+ | `effort:4` | Large — multi-day |
72
+ | `effort:5` | Major — architectural change |
73
+
43
74
  ## Reporting issues
44
75
 
45
- Use the GitHub issue templates include a minimal repro snippet and your browser/renderer info.
76
+ Use the GitHub issue templates. Pick the template that matches your issue type and add the appropriate `type:*`, `domain:*`, and `effort:*` labels.
package/dist/globals.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  type FluidAlgorithm = 'standard' | 'glass' | 'ink' | 'aurora' | 'ripple';
4
4
 
5
5
  /** RGB tuple (values 0–1) or a CSS hex string (#RGB, #RRGGBB, #RRGGBBAA — alpha stripped). */
6
- type FluidColor = [number, number, number] | `#${string}`;
6
+ type FluidColor = [number, number, number] | `#${string}` | string;
7
7
 
8
8
  /** Internal quality object used by FluidController and FluidSimulation. */
9
9
  interface FluidQuality {