hz-particles 1.2.0 → 1.3.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/README.md CHANGED
@@ -1,59 +1,239 @@
1
1
  # hz-particles
2
2
 
3
+ **Prompt-generated, real-time WebGPU FX for Three.js and React Three Fiber.**
4
+
5
+ Design particle effects in the HZ editor — by hand or **from a prompt** — export them as
6
+ `JSON` / `.hzfx` presets, and render them **faithfully** in your app with the *same* WebGPU
7
+ engine the editor uses. What you design is exactly what renders.
8
+
3
9
  [![npm version](https://img.shields.io/npm/v/hz-particles.svg)](https://www.npmjs.com/package/hz-particles)
4
10
  [![license](https://img.shields.io/npm/l/hz-particles.svg)](https://github.com/jguyet/particle-system/blob/main/LICENSE)
5
11
  [![WebGPU](https://img.shields.io/badge/WebGPU-required-blue.svg)](https://caniuse.com/webgpu)
6
12
 
7
- A high-performance WebGPU particle engine for the web. Use it as a standalone WebGPU renderer or as a plug-and-play React Three Fiber component.
13
+ ```tsx
14
+ import { HZFaithfulFX } from 'hz-particles/r3f';
15
+ import firePreset from './fire.json';
16
+
17
+ export function Scene() {
18
+ return <HZFaithfulFX preset={firePreset} position={[0, 1, 0]} />;
19
+ }
20
+ ```
21
+
22
+ <!-- TODO: fill in the real public URLs (see PR / message) -->
23
+ - **Prompt FX generator / editor:** https://YOUR-SITE
24
+ - **Live demo:** https://YOUR-SITE/demo
25
+ - **Docs:** https://YOUR-SITE/docs
26
+ - **NPM:** https://www.npmjs.com/package/hz-particles
27
+
28
+ ## Why hz-particles?
29
+
30
+ Most AI/VFX tools generate **videos** or **spritesheets**. hz-particles generates **real-time,
31
+ editable FX presets** that run inside your Three.js / R3F scene.
32
+
33
+ - **Not a video** — effects are simulated in real time, on the GPU, every frame.
34
+ - **Not a screenshot** — presets are editable `JSON` / `.hzfx` files you can re-tune or re-prompt.
35
+ - **Not a separate renderer** — the R3F component renders the *same engine* as the editor, so
36
+ there's no "looked great in the editor, renders wrong in-game" gap.
37
+ - **Game-ready** — moving emitters, trails, depth occlusion, bloom, GLB-mesh particles.
38
+
39
+ > **Text-to-realtime-FX, not text-to-video.** The HZ editor turns a prompt into an editable
40
+ > preset; this package is the runtime that renders it in your app.
41
+
42
+ The full pipeline:
43
+
44
+ ```
45
+ HZ editor (manual or prompt) → preset (JSON / .hzfx) → faithful WebGPU runtime → Three.js / R3F
46
+ ```
8
47
 
9
48
  ## Features
10
49
 
11
50
  - **WebGPU Compute Shaders** — GPU-accelerated particle physics simulation
12
- - **GPU Instancing** — Efficient rendering of thousands of particles
13
- - **GLB Model Support** — Use 3D models as particle shapes with automatic texture extraction
14
- - **Skeletal Animations** Animated GLB models with full animation control
15
- - **Scene Serialization** Save/load particle configurations as JSON
16
- - **Multiple Emitter Shapes** — Sphere, cube, cylinder, circle, square emission patterns
17
- - **Real-time Physics** — Gravity, attractors, drag, velocity, and lifetime management
18
- - **React Three Fiber Component** — `HZFaithfulFX` (engine-faithful: the real overlay inline on three's WebGPURenderer), with preset support
19
- - **3D Object Support** — Static 3D objects alongside particle systems
51
+ - **GPU Instancing** — efficient rendering of thousands of particles
52
+ - **Engine-faithful R3F component** — `HZFaithfulFX` runs the real overlay inline on three's
53
+ `WebGPURenderer`: shader shapes, noise, per-system blending, multi-pass bloom, faithful trails,
54
+ depth occlusion. Editor parity by construction.
55
+ - **Faithful trails & moving emitters** — comet trails that follow an arbitrary path (ball trails)
56
+ - **GLB model support** — use 3D models as particle shapes, with automatic texture extraction
57
+ - **Skeletal animations** — animated GLB models with full animation control
58
+ - **20+ emitter shapes** — volumes & surfaces: sphere, cube, cylinder, cone, torus, capsule,
59
+ frustum, hemisphere, disc, annulus, arc, spiral, polygon, `cubeSurface`, `sphereSurface`,
60
+ `boxFrame`, and more
61
+ - **Real-time physics** — gravity, attractors, drag, velocity, lifetime
62
+ - **Serialized presets** — self-contained `.hzfx` binary package (textures + GLB inlined) or plain JSON
63
+ - **3D object support** — static 3D objects alongside particle systems
20
64
 
21
65
  ## Requirements
22
66
 
23
- **WebGPU-compatible browser required:**
24
- - Chrome/Edge 113+ (stable)
67
+ **A WebGPU-capable browser is required:**
68
+ - Chrome / Edge 113+ (stable)
25
69
  - Firefox Nightly (experimental)
26
- - Safari Technology Preview (experimental)
70
+ - Safari Technology Preview / Safari 18+ (experimental)
27
71
 
28
72
  Check browser support: [caniuse.com/webgpu](https://caniuse.com/webgpu)
29
73
 
30
74
  ## Installation
31
75
 
32
- ### WebGPU
33
-
34
76
  ```bash
35
77
  npm install hz-particles
36
78
  ```
37
79
 
38
80
  ```javascript
39
- import { initWebGPU, ParticleSystemManager } from 'hz-particles';
81
+ // React Three Fiber (recommended)
82
+ import { HZFaithfulFX } from 'hz-particles/r3f';
83
+
84
+ // Standalone WebGPU / overlay
85
+ import { initHzFxOverlay, ParticleSystemManager } from 'hz-particles';
40
86
  ```
41
87
 
42
- ### React Three Fiber
88
+ For R3F usage you also need the peers: `react`, `three` (with its WebGPU backend), and
89
+ `@react-three/fiber`.
43
90
 
44
- ```bash
45
- npm install hz-particles
91
+ ## Usage modes
92
+
93
+ Pick the entry point that matches how much control you need:
94
+
95
+ ### 1. Drop-in R3F FX — `<HZFaithfulFX />`
96
+ The fastest path. Give it a preset, drop it in your scene, done. Engine-faithful by construction.
97
+
98
+ ### 2. Overlay / inline renderer — `initHzFxOverlay()`
99
+ For non-React apps, custom render loops, post-processing pipelines, or when you want to host many
100
+ coexisting effects and drive the camera yourself. Same faithful engine, full control.
101
+
102
+ ### 3. Low-level particle engine — `ParticleSystemManager` / `ParticleSystem`
103
+ Build your own engine on top of the raw compute + render pipelines.
104
+
105
+ ## Quick Start (React Three Fiber)
106
+
107
+ `<HZFaithfulFX>` runs the **real engine** inline on three's `WebGPURenderer`: shader-drawn shapes,
108
+ noise, per-system blending, multi-pass bloom, faithful trails, and depth occlusion. Pass a
109
+ `position` for a static FX, or a `positionRef` for a moving one (e.g. a ball trail) — the overlay
110
+ pulls the ref each frame.
111
+
112
+ Your R3F `Canvas` must use a **WebGPU** renderer. With `@react-three/fiber` v9 and `three/webgpu`:
113
+
114
+ ```tsx
115
+ import { Canvas } from '@react-three/fiber';
116
+ import * as THREE from 'three/webgpu';
117
+ import { useRef } from 'react';
118
+ import { HZFaithfulFX } from 'hz-particles/r3f';
119
+ import firePreset from './fire.json';
120
+ import trailPreset from './trail.json';
121
+
122
+ export function App() {
123
+ const ballRef = useRef<THREE.Vector3>(null); // updated by your app each frame
124
+
125
+ return (
126
+ <Canvas
127
+ // Provide a WebGPURenderer instead of the default WebGLRenderer:
128
+ gl={async (props) => {
129
+ const renderer = new THREE.WebGPURenderer(props as any);
130
+ await renderer.init();
131
+ return renderer;
132
+ }}
133
+ camera={{ position: [0, 2, 6], fov: 50 }}
134
+ >
135
+ <HZFaithfulFX preset={firePreset} position={[0, 1, 0]} /> {/* static FX */}
136
+ <HZFaithfulFX preset={trailPreset} positionRef={ballRef} scale={1.5} /> {/* ball trail */}
137
+ </Canvas>
138
+ );
139
+ }
46
140
  ```
47
141
 
48
- ```javascript
142
+ > **Important integration note —** `<HZFaithfulFX>` **drives rendering**: its `useFrame` calls
143
+ > `gl.render` itself, so R3F stops auto-rendering. This is what guarantees the FX composites on
144
+ > top of your scene with correct depth occlusion.
145
+ >
146
+ > - Use `<HZFaithfulFX>` when it can own the frame (the common case).
147
+ > - If your host owns its own render loop (custom RAF, a post-processing / EffectComposer
148
+ > pipeline), use [`initHzFxOverlay()`](#engine-faithful-overlay) directly instead so *you* stay
149
+ > in control of when the scene and the FX render.
150
+ >
151
+ > Re-trigger an effect by remounting it (change its `key`).
152
+
153
+ ### Loading `.hzfx` presets
154
+
155
+ The `preset` prop is a plain `SceneData` object. JSON presets can be imported directly (above).
156
+ For the binary `.hzfx` package, fetch it with `fetchPreset` (auto-detects `.hzfx` vs JSON):
157
+
158
+ ```tsx
159
+ import { useEffect, useState } from 'react';
160
+ import { fetchPreset } from 'hz-particles/r3f';
49
161
  import { HZFaithfulFX } from 'hz-particles/r3f';
162
+
163
+ function Fire() {
164
+ const [preset, setPreset] = useState(null);
165
+ useEffect(() => { fetchPreset('/fx/fire.hzfx').then(setPreset); }, []);
166
+ return preset ? <HZFaithfulFX preset={preset} position={[0, 1, 0]} /> : null;
167
+ }
50
168
  ```
51
169
 
52
- > **Important:** Your R3F `Canvas` must use `WebGPURenderer`. Pass `gl={{ powerPreference: 'high-performance' }}` and configure Three.js to use its WebGPU backend, or use `@react-three/fiber` with a WebGPU-capable renderer setup.
170
+ ### Props reference
53
171
 
54
- ## Quick Start
172
+ | Prop | Type | Default | Description |
173
+ |------|------|---------|-------------|
174
+ | `preset` | `SceneData` | — | Particle preset (JSON object exported from the editor) |
175
+ | `position` | `[number, number, number]` | `[0, 0, 0]` | Static emitter world position (when `positionRef` is omitted) |
176
+ | `positionRef` | `RefObject<Vector3 \| null>` | — | Moving emitter (e.g. a ball trail): the overlay reads this ref every frame |
177
+ | `scale` | `number` | `1` | Uniform scale applied to the preset (sizes, speeds, emission, trail) |
178
+ | `active` | `boolean` | `true` | When false, a moving emitter pauses (no new trail) |
179
+ | `renderPriority` | `number` | `1` | `useFrame` priority. This component drives rendering, so keep it > 0 |
180
+ | `noOcclusion` | `boolean` | `false` | Skip scene-depth occlusion (particles never hidden behind geometry) |
55
181
 
56
- ### WebGPU
182
+ The `hz-particles/r3f` entry also exports `HZTrailRibbon` (a lightweight ribbon-trail mesh driven
183
+ by a `positionRef`) and the helpers `scalePreset`, `computeParticleSize`, `computeParticleOpacity`.
184
+
185
+ ## Engine-faithful overlay
186
+
187
+ `initHzFxOverlay()` is **the same render loop the HZ previews and editor use** — shader-drawn
188
+ shapes, per-system blending, noise distortion, GLB-mesh particles + animation, multi-pass bloom,
189
+ and faithful trails. It's the recommended way to drop HZ effects into any app (vanilla JS,
190
+ TypeScript, or a React app with its own render loop — one core, no per-framework reimplementation).
191
+
192
+ ```javascript
193
+ import { initHzFxOverlay, makeThreeSceneDepth } from 'hz-particles';
194
+
195
+ // OVERLAY mode: pass a canvas — the overlay owns its WebGPU context and renders on a
196
+ // transparent background. Stack it above your scene with pointer-events: none.
197
+ const fx = await initHzFxOverlay(canvas);
198
+ await fx.loadPreset(preset); // or fx.setEmitters([{ preset, position }])
199
+
200
+ function frame(dt) {
201
+ fx.setCamera(proj, view, [cx, cy, cz]); // column-major matrices + camera world pos
202
+ fx.render(dt);
203
+ }
204
+
205
+ // INLINE mode (share your own WebGPU renderer, e.g. three's WebGPURenderer):
206
+ const fx2 = await initHzFxOverlay(
207
+ { device, context, canvas },
208
+ { getSceneDepth: makeThreeSceneDepth(renderer) } // one-line occlusion behind your geometry
209
+ );
210
+ // each frame, AFTER renderer.render(scene, camera): fx2.setCamera(...); fx2.render(dt);
211
+ ```
212
+
213
+ **Coexisting groups & moving emitters** — one overlay hosts many FX at once. `addEmitter(preset, pos)`
214
+ adds a static group; `addMovingEmitter(preset, { getPosition })` adds a moving one (e.g. a ball trail)
215
+ whose comet trail follows the path you supply (the overlay *pulls* `getPosition()` each frame; return
216
+ `null` to pause). Both return `{ remove() }`.
217
+
218
+ | Method | Description |
219
+ | --- | --- |
220
+ | `setCamera(proj, view, pos)` / `setCameraMVP(mvp, pos)` | Drive the camera (column-major). |
221
+ | `loadPreset(preset, pos?)` / `setEmitters([...])` | (Re)build emitters. |
222
+ | `addEmitter(preset, pos?)` → `{ remove }` | Add a STATIC group that coexists with others. |
223
+ | `addMovingEmitter(preset, { getPosition })` → `{ setPosition, remove }` | Add a MOVING group (ball trail). |
224
+ | `render(dt)` | Simulate + render one frame (inline: call after your scene render). |
225
+ | `resize()` | Recreate render textures (also auto-detected from the canvas). |
226
+ | `clearCaches()` | Drop cached bind groups after a `replaceSystems`/`addSystems` that reuses ids. |
227
+ | `trackHistoryGroup(ids, getPosition)` → `{ remove }` | Give existing manager systems a moving trail (history only). |
228
+ | `destroy()` | Release GPU resources. |
229
+
230
+ Options: `{ getSceneDepth, autoRespawn, manager }`. Pass `manager` to **share an existing
231
+ `ParticleSystemManager`** so your app keeps owning it while the overlay is the single render path.
232
+
233
+ ## Advanced: standalone WebGPU engine
234
+
235
+ If you want to build your own renderer on the raw engine (no overlay, no R3F), drive the
236
+ `ParticleSystemManager` / `ParticleSystem` directly:
57
237
 
58
238
  ```javascript
59
239
  import { initWebGPU, ParticleSystemManager } from 'hz-particles';
@@ -64,31 +244,25 @@ canvas.width = 800;
64
244
  canvas.height = 600;
65
245
  const { device, context, format } = await initWebGPU(canvas);
66
246
 
67
- // 2. Create particle system manager
247
+ // 2. Create manager + a particle system
68
248
  const manager = new ParticleSystemManager(device);
249
+ manager.createParticleSystem({ maxParticles: 10000, particleCount: 1000 });
69
250
 
70
- // 3. Create a particle system
71
- const systemId = manager.createParticleSystem({
72
- maxParticles: 10000,
73
- particleCount: 1000,
74
- });
75
-
76
- // 4. Initialize compute pipeline
251
+ // 3. Initialize the compute pipeline
77
252
  const system = manager.getActiveSystem();
78
253
  await system.initComputePipeline(device);
79
254
 
80
- // 5. Render loop
255
+ // 4. Render loop
81
256
  let lastTime = performance.now();
82
-
83
257
  function render() {
84
- const currentTime = performance.now();
85
- const deltaTime = (currentTime - lastTime) / 1000;
86
- lastTime = currentTime;
258
+ const now = performance.now();
259
+ const dt = (now - lastTime) / 1000;
260
+ lastTime = now;
87
261
 
88
- manager.updateAllSystems(deltaTime);
262
+ manager.updateAllSystems(dt);
89
263
 
90
- const commandEncoder = device.createCommandEncoder();
91
- const renderPass = commandEncoder.beginRenderPass({
264
+ const encoder = device.createCommandEncoder();
265
+ const pass = encoder.beginRenderPass({
92
266
  colorAttachments: [{
93
267
  view: context.getCurrentTexture().createView(),
94
268
  clearValue: { r: 0, g: 0, b: 0, a: 1 },
@@ -96,81 +270,40 @@ function render() {
96
270
  storeOp: 'store',
97
271
  }],
98
272
  });
99
-
100
- system.render(renderPass);
101
- renderPass.end();
102
- device.queue.submit([commandEncoder.finish()]);
273
+ system.render(pass);
274
+ pass.end();
275
+ device.queue.submit([encoder.finish()]);
103
276
 
104
277
  requestAnimationFrame(render);
105
278
  }
106
-
107
279
  render();
108
280
  ```
109
281
 
110
- ### React Three Fiber
111
-
112
- `<HZFaithfulFX>` runs the **real engine** (the overlay) inline on three's `WebGPURenderer`: shader
113
- shapes, noise, per-system blending, multi-pass bloom, faithful trails + depth occlusion. What you
114
- design in the editor is exactly what renders. Pass a `position` for a static FX, or a `positionRef`
115
- for a moving one (a ball trail) — the overlay pulls the ref each frame.
282
+ ## Preset configuration
116
283
 
117
- ```jsx
118
- import { useRef } from 'react';
119
- import { HZFaithfulFX } from 'hz-particles/r3f';
120
- import firePreset from './presets/fire.json';
121
-
122
- function Scene() {
123
- const ballRef = useRef(null); // THREE.Vector3, updated by your app each frame
124
- return (
125
- <>
126
- <HZFaithfulFX preset={firePreset} position={[0, 1, 0]} /> {/* static FX */}
127
- <HZFaithfulFX preset={trailPreset} positionRef={ballRef} scale={1.5} /> {/* ball trail */}
128
- </>
129
- );
130
- }
131
- ```
132
-
133
- `<HZFaithfulFX>` **drives rendering** (its `useFrame` calls `gl.render` itself, so R3F stops
134
- auto-rendering). If your host owns its own render loop (custom RAF, post-processing pipeline), use
135
- `initHzFxOverlay()` + `makeThreeSceneDepth()` directly (see above). Re-trigger an effect by remounting
136
- it (change its `key`).
137
-
138
- ## Props Reference
139
-
140
- The `HZFaithfulFX` component accepts the following props:
141
-
142
- | Prop | Type | Default | Description |
143
- |------|------|---------|-------------|
144
- | `preset` | `SceneData` | — | Particle preset (JSON exported from the editor) |
145
- | `position` | `[number, number, number]` | `[0, 0, 0]` | Static emitter world position (used when `positionRef` is omitted) |
146
- | `positionRef` | `React.RefObject<Vector3 \| null>` | — | Moving emitter (e.g. a ball trail): the overlay reads this ref every frame |
147
- | `scale` | `number` | `1` | Uniform scale applied to the preset (sizes, speeds, emission, trail) |
148
- | `active` | `boolean` | `true` | When false, a moving emitter pauses (no new trail) |
149
- | `renderPriority` | `number` | `1` | `useFrame` priority. This component drives rendering, so keep it > 0 |
150
- | `noOcclusion` | `boolean` | `false` | Skip scene-depth occlusion (particles never hidden behind geometry) |
151
-
152
- ## Preset Configuration
153
-
154
- Presets are plain JSON files that describe a particle scene. Key fields:
284
+ Presets are plain `SceneData`. A few key per-system fields:
155
285
 
156
286
  | Field | Type | Description |
157
287
  |-------|------|-------------|
158
288
  | `particleCount` | `number` | Number of active particles |
159
289
  | `lifetime` | `number` | Particle lifetime in seconds |
160
290
  | `emissionRate` | `number` | Particles emitted per second |
161
- | `emissionShape` | `string` | Emitter shape: `sphere`, `cube`, `cylinder`, `circle`, `square` |
291
+ | `emissionShape` | `string` | Emitter shape (default `cube`). Volumes: `cube`/`box`, `sphere`, `cylinder`, `cone`, `torus`, `capsule`, `frustum`, `hemisphere`, `spiral`. Flat: `circle`, `square`, `rectangle`, `disc`, `annulus`, `arc`, `polygon`, `plain`, `line`. Surfaces: `cubeSurface`, `sphereSurface`, `boxFrame` |
162
292
  | `particleSize` | `number` | Base size of each particle |
163
- | `colors` | `string[]` | Array of hex color values interpolated over lifetime |
293
+ | `colors` | `string[]` | Hex colors interpolated over lifetime |
164
294
  | `fadeIn` | `number` | Opacity fade-in duration (0–1, fraction of lifetime) |
165
295
  | `fadeOut` | `number` | Opacity fade-out start (0–1, fraction of lifetime) |
166
296
  | `bloom` | `boolean` | Enable bloom glow on particles |
167
297
  | `gravity` | `number` | Gravity strength applied to particles |
168
298
  | `damping` | `number` | Velocity damping factor (0 = no drag, 1 = full stop) |
169
299
  | `emissionTrailEnabled` | `boolean` | Enable particle trail rendering |
170
- | `emissionTrailDuration` | `number` | How long trail segments persist in seconds |
300
+ | `emissionTrailDuration` | `number` | How long trail segments persist (seconds) |
171
301
  | `emissionTrailWidth` | `number` | Width of the emission trail |
172
302
 
173
- ## API Reference
303
+ `serializeSystemConfig(config)` is the single source of truth for the full set of serializable
304
+ fields (used by `saveScene` and the editor's code export).
305
+
306
+ ## API reference
174
307
 
175
308
  ### `initWebGPU(canvas)`
176
309
 
@@ -185,14 +318,7 @@ async function initWebGPU(canvas: HTMLCanvasElement): Promise<{
185
318
  }>
186
319
  ```
187
320
 
188
- **Parameters:**
189
- - `canvas` (required) — Canvas element. Set `canvas.width` and `canvas.height` before calling.
190
-
191
- **Returns:** Object with WebGPU device, canvas context, texture format, and canvas element.
192
-
193
- **Throws:** Error if `canvas` is missing or WebGPU is not supported.
194
-
195
- ---
321
+ Set `canvas.width` / `canvas.height` before calling. Throws if WebGPU is unsupported.
196
322
 
197
323
  ### `ParticleSystem`
198
324
 
@@ -204,25 +330,19 @@ class ParticleSystem {
204
330
  }
205
331
  ```
206
332
 
207
- **Config options:**
208
- - `maxParticles` (number, default `10000`) — Maximum particle buffer size
209
- - `particleCount` (number, default `100`) — Initial active particle count
210
-
211
- **Key Methods:**
333
+ **Config:** `maxParticles` (default `10000`), `particleCount` (default `100`).
212
334
 
213
335
  | Method | Description |
214
336
  |--------|-------------|
215
- | `initComputePipeline(device)` | Initialize GPU compute and render pipelines. Must be called before rendering. |
337
+ | `initComputePipeline(device)` | Initialize GPU compute and render pipelines. Call before rendering. |
216
338
  | `setTexture(imageBitmap)` | Set particle texture from an `ImageBitmap`. |
217
- | `setGLBModel(arrayBuffer)` | Use GLB model geometry as particle shape. Automatically extracts textures. |
339
+ | `setGLBModel(arrayBuffer)` | Use GLB model geometry as particle shape (auto-extracts textures). |
218
340
  | `updateParticles(deltaTime)` | Execute a physics simulation step on the GPU. |
219
341
  | `spawnParticles()` | Emit new particles according to emitter configuration. |
220
342
  | `setGravity(value)` | Set gravity strength (default `9.8`). |
221
- | `setAttractor(strength, position)` | Set an attractor point with strength and 3D position `[x, y, z]`. |
343
+ | `setAttractor(strength, position)` | Set an attractor point `[x, y, z]`. |
222
344
  | `render(renderPass)` | Render particles to the current render pass. |
223
345
 
224
- ---
225
-
226
346
  ### `ParticleSystemManager`
227
347
 
228
348
  Manages multiple particle systems within a single scene.
@@ -233,25 +353,21 @@ class ParticleSystemManager {
233
353
  }
234
354
  ```
235
355
 
236
- **Key Methods:**
237
-
238
356
  | Method | Description |
239
357
  |--------|-------------|
240
358
  | `createParticleSystem(config?)` | Create a new particle system. Returns system ID. |
241
- | `getActiveSystem()` | Get the currently active `ParticleSystem` instance. |
359
+ | `getActiveSystem()` | Get the currently active `ParticleSystem`. |
242
360
  | `getActiveConfig()` | Get the active system configuration object. |
243
- | `setActiveSystem(index)` | Switch active system by index. Returns success status. |
244
- | `removeSystem(index)` | Remove a system by index. Returns success status. |
361
+ | `setActiveSystem(index)` | Switch active system by index. |
362
+ | `removeSystem(index)` | Remove a system by index. |
245
363
  | `updateAllSystems(deltaTime)` | Update physics for all systems. |
246
- | `getSystemsList()` | Get list of all systems with `name`, `id`, `index`, `isActive`. |
364
+ | `getSystemsList()` | List all systems (`name`, `id`, `index`, `isActive`). |
247
365
  | `duplicateActiveSystem()` | Clone the active system. Returns new system ID. |
248
- | `replaceSystems(sceneData)` | Load a scene from serialized data. Returns success status. |
249
-
250
- ---
366
+ | `replaceSystems(sceneData)` | Load a scene from serialized data. |
251
367
 
252
368
  ### `parseGLB(arrayBuffer)`
253
369
 
254
- Parse GLB binary format and extract geometry data.
370
+ Parse GLB binary and extract geometry.
255
371
 
256
372
  ```typescript
257
373
  async function parseGLB(arrayBuffer: ArrayBuffer): Promise<{
@@ -266,16 +382,13 @@ async function parseGLB(arrayBuffer: ArrayBuffer): Promise<{
266
382
  }>
267
383
  ```
268
384
 
269
- ---
270
-
271
385
  ### `GLBAnimator`
272
386
 
273
- Handles skeletal animation playback for animated GLB models.
387
+ Skeletal animation playback for animated GLB models.
274
388
 
275
389
  ```typescript
276
390
  class GLBAnimator {
277
391
  constructor(animationData: object)
278
-
279
392
  currentTime: number
280
393
  playing: boolean
281
394
  speed: number // default 1.0
@@ -283,164 +396,95 @@ class GLBAnimator {
283
396
  }
284
397
  ```
285
398
 
286
- **Key Methods:**
287
-
288
399
  | Method | Description |
289
400
  |--------|-------------|
290
- | `setRestPose(positions, normals)` | Set the T-pose/bind pose for animation. |
401
+ | `setRestPose(positions, normals)` | Set the T-pose/bind pose. |
291
402
  | `update(deltaTime)` | Advance animation. Returns `{ positions, normals, changed }`. |
292
403
  | `setAnimation(index)` | Switch to animation clip by index. |
293
- | `getAnimationNames()` | Get list of available animation clip names. |
404
+ | `getAnimationNames()` | List available animation clip names. |
294
405
 
295
- ## GLB Models & Animation
406
+ ## GLB models & animation
296
407
 
297
408
  ```javascript
298
409
  import { parseGLB, GLBAnimator } from 'hz-particles';
299
410
 
300
- // Load GLB file
301
- const response = await fetch('model.glb');
302
- const arrayBuffer = await response.arrayBuffer();
411
+ const arrayBuffer = await (await fetch('model.glb')).arrayBuffer();
303
412
  const glbData = await parseGLB(arrayBuffer);
304
413
 
305
- // Use as particle shape
306
- await system.setGLBModel(arrayBuffer);
414
+ await system.setGLBModel(arrayBuffer); // use as particle shape
307
415
 
308
- // Setup animation (if model has animations)
309
416
  if (glbData.animationData) {
310
417
  const animator = new GLBAnimator(glbData.animationData);
311
418
  animator.setRestPose(glbData.positions, glbData.normals);
312
419
  animator.playing = true;
313
- animator.loop = true;
314
- animator.speed = 1.0;
315
420
 
316
- // In render loop:
421
+ // in the render loop:
317
422
  const { positions, normals, changed } = animator.update(deltaTime);
318
- if (changed) {
319
- // Update particle system with new geometry
320
- // (See full API documentation for geometry update methods)
321
- }
322
- }
323
- ```
324
-
325
- ## Engine-faithful Integration (overlay renderer)
326
-
327
- `initHzFxOverlay()` is **the same render loop the HZ previews and editor use** — shader-drawn
328
- shapes, per-system blending, noise distortion, GLB mesh particles + animation, multi-pass bloom,
329
- and faithful trails. It's the recommended way to drop HZ effects into any app (vanilla JS,
330
- TypeScript, or React — one core, no per-framework reimplementation).
331
-
332
- ```javascript
333
- import { initHzFxOverlay, makeThreeSceneDepth } from 'hz-particles';
334
-
335
- // OVERLAY mode: pass a canvas — the overlay owns its WebGPU context and renders on a
336
- // transparent background. Stack it above your scene with pointer-events: none.
337
- const fx = await initHzFxOverlay(canvas);
338
- await fx.loadPreset(preset); // or fx.setEmitters([{ preset, position }])
339
-
340
- function frame(dt) {
341
- fx.setCamera(proj, view, [cx, cy, cz]); // column-major matrices + camera world pos
342
- fx.render(dt);
423
+ if (changed) { /* push updated geometry to the system */ }
343
424
  }
344
-
345
- // INLINE mode (share your own WebGPU renderer, e.g. three's WebGPURenderer):
346
- const fx2 = await initHzFxOverlay(
347
- { device, context, canvas },
348
- { getSceneDepth: makeThreeSceneDepth(renderer) } // one-line occlusion behind your geometry
349
- );
350
- // each frame, AFTER renderer.render(scene, camera): fx2.setCamera(...); fx2.render(dt);
351
425
  ```
352
426
 
353
- **Coexisting groups & moving emitters** — one overlay hosts many FX at once. `addEmitter(preset, pos)`
354
- adds a static group; `addMovingEmitter(preset, { getPosition })` adds a moving one (e.g. a ball trail)
355
- whose comet trail follows the path you supply (the overlay *pulls* `getPosition()` each frame; return
356
- `null` to pause). Both return `{ remove() }`.
357
-
358
- | Method | Description |
359
- | --- | --- |
360
- | `setCamera(proj, view, pos)` / `setCameraMVP(mvp, pos)` | Drive the camera (column-major). |
361
- | `loadPreset(preset, pos?)` / `setEmitters([...])` | (Re)build emitters. |
362
- | `addEmitter(preset, pos?)` → `{ remove }` | Add a STATIC group that coexists with others. |
363
- | `addMovingEmitter(preset, { getPosition })` → `{ setPosition, remove }` | Add a MOVING group (ball trail). |
364
- | `render(dt)` | Simulate + render one frame (inline: call after your scene render). |
365
- | `resize()` | Recreate render textures (also auto-detected from the canvas). |
366
- | `clearCaches()` | Drop cached bind groups after a `replaceSystems`/`addSystems` that reuses ids. |
367
- | `trackHistoryGroup(ids, getPosition)` → `{ remove }` | Give existing manager systems a moving trail (history only). |
368
- | `destroy()` | Release GPU resources. |
369
-
370
- Options: `{ getSceneDepth, autoRespawn, manager }`. Pass `manager` to **share an existing
371
- `ParticleSystemManager`** so your app keeps owning it while the overlay is the single render path.
372
-
373
- ## Scene Save/Load
427
+ ## Scene save / load
374
428
 
375
429
  ```javascript
376
430
  import { saveScene, loadScene } from 'hz-particles';
377
431
 
378
- // Save current scene
379
- document.getElementById('save-button').addEventListener('click', () => {
380
- saveScene(manager); // Downloads a self-contained .hzfx package (or .hzfx → JSON on Alt-click)
381
- });
432
+ // Save (downloads a self-contained .hzfx package; Alt-click → JSON):
433
+ saveButton.addEventListener('click', () => saveScene(manager));
382
434
 
383
- // Load scene from file input
384
- document.getElementById('file-input').addEventListener('change', async (event) => {
385
- const success = await loadScene(event, manager);
386
- if (success) {
387
- console.log('Scene loaded successfully');
388
- }
435
+ // Load from a file input (.hzfx or JSON, auto-detected):
436
+ fileInput.addEventListener('change', async (e) => {
437
+ if (await loadScene(e, manager)) console.log('Scene loaded');
389
438
  });
390
439
  ```
391
440
 
392
441
  ## TypeScript
393
442
 
394
- Type declarations **are shipped** — the package's `types` entry points at `dist-lib/hz-particles.d.ts`
395
- (and `dist-lib/hz-particles-r3f.d.ts` for the `hz-particles/r3f` subpath), so imports are fully typed
396
- out of the box:
443
+ Type declarations **are shipped** — the package's `types` entry points at
444
+ `dist-lib/hz-particles.d.ts` (and `dist-lib/hz-particles-r3f.d.ts` for the `hz-particles/r3f`
445
+ subpath), so imports are fully typed out of the box:
397
446
 
398
447
  ```typescript
399
448
  import { initHzFxOverlay, ParticleSystemManager, serializeSystemConfig } from 'hz-particles';
400
449
  import { HZFaithfulFX } from 'hz-particles/r3f';
401
450
  ```
402
451
 
403
- ## Secondary Exports
452
+ ## Secondary exports
404
453
 
405
- The following modules are also exported for advanced usage:
454
+ Also exported for advanced usage:
406
455
 
407
- - **`ParticleEmitter`** — Emission shape configuration (sphere, cube, cylinder, circle, square)
408
- - **`ParticlePhysics`** — Physics simulation parameter management
409
- - **`ParticleTextureManager`** — Texture loading utilities
456
+ - **`fetchPreset(url)`** — load a preset from a URL (`.hzfx` or JSON, auto-detected)
457
+ - **`packHZFX` / `unpackHZFX` / `isHZFX`** — build/read the `.hzfx` binary package format
458
+ - **`serializeSystemConfig(config)`** — single source of truth for a system's serializable fields
459
+ - **`saveScene(manager)` / `loadScene(event)`** — export/import a scene (`.hzfx` or JSON)
460
+ - **`ParticleEmitter`** — emission-shape configuration
461
+ - **`ParticlePhysics`** — physics parameter management
462
+ - **`ParticleTextureManager`** — texture loading utilities
410
463
  - **`Objects3DManager`** — 3D object scene management
411
- - **`saveScene(manager)`** — Export scene as a self-contained `.hzfx` package (or JSON) file download
412
- - **`loadScene(event)`** Load scene from file input event (`.hzfx` or JSON, auto-detected)
413
- - **`serializeSystemConfig(config)`** — Single source of truth for a system's serializable config fields (shared by `saveScene` + the editor's code export; add binary assets yourself)
414
- - **`initHzFxOverlay(target, options?)`** — Engine-faithful overlay/inline renderer (see above)
415
- - **`packHZFX` / `unpackHZFX` / `isHZFX`** — Build/read the `.hzfx` binary package format
416
- - **`extractGLBTexture(arrayBuffer)`** — Extract base color texture from GLB file
417
- - **Shader exports** — WGSL shader code for compute and rendering pipelines
418
- - **Geometry helpers** — Primitive shape generators (cube, sphere, etc.)
419
- - **Render pipeline creators** — Low-level WebGPU pipeline construction
420
-
421
- ## Online Editor
422
-
423
- Try the interactive particle editor at `http://localhost:8110/editor` when running the Docker container (`docker-compose up` from the repository root).
424
-
425
- The editor provides a visual interface for:
426
- - Real-time particle system configuration
427
- - Emitter shape selection and tuning
428
- - GLB model import and animation control
429
- - Scene preset library
430
- - Export/import scene JSON
464
+ - **`extractGLBTexture(arrayBuffer)`** — extract base color texture from a GLB
465
+ - **Shader / geometry / pipeline helpers** low-level WGSL and pipeline construction
431
466
 
432
- ## Contributing
467
+ ## Online editor
468
+
469
+ Generate effects from a prompt — or build them by hand — in the HZ editor, then export a preset and
470
+ render it with this package.
471
+
472
+ <!-- TODO: replace with the public editor URL -->
473
+ - Hosted editor & prompt FX generator: https://YOUR-SITE
474
+ - Local: `http://localhost:8110/editor` (run `docker-compose up` from the repo root)
433
475
 
434
- Contributions are welcome! Please follow these steps:
476
+ The editor provides real-time configuration, emitter-shape tuning, GLB import + animation control, a
477
+ preset library, and `JSON` / `.hzfx` export.
478
+
479
+ ## Contributing
435
480
 
436
481
  1. Fork the repository
437
482
  2. Create a feature branch (`git checkout -b feature/amazing-feature`)
438
- 3. Commit your changes (`git commit -m 'Add amazing feature'`)
439
- 4. Push to the branch (`git push origin feature/amazing-feature`)
440
- 5. Open a Pull Request
483
+ 3. Commit your changes
484
+ 4. Push and open a Pull Request
441
485
 
442
486
  ## License
443
487
 
444
- MIT License - see [LICENSE](LICENSE) file for details.
488
+ MIT License see [LICENSE](LICENSE).
445
489
 
446
490
  Copyright (c) 2025 HZ