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