@jayf0x/fluidity-js 0.2.5 → 0.2.6
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 +140 -54
- package/CONTRIBUTING.md +32 -1
- package/dist/index.js +109 -106
- package/llms.txt +32 -38
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# fluidity-js — Agent Guide
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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,
|
|
26
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
78
|
-
splat(x: number, y: number,
|
|
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
|
-
##
|
|
97
|
+
## Normalized prop API
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
108
|
-
| `useFluid` creates a **fresh** `<canvas>`
|
|
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
|
|
111
|
-
| Display shader outputs **premultiplied alpha** | Transparent canvas compositing correctness |
|
|
112
|
-
|
|
|
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
|
-
##
|
|
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
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
|
144
|
-
|
|
|
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
|
-
|
|
|
147
|
-
| npm
|
|
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
|
|
76
|
+
Use the GitHub issue templates. Pick the template that matches your issue type and add the appropriate `type:*`, `domain:*`, and `effort:*` labels.
|