hz-particles 1.0.11 → 1.0.13
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 +165 -228
- package/dist-lib/hz-particles-r3f.cjs +3 -3
- package/dist-lib/hz-particles-r3f.mjs +313 -333
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/jguyet/particle-system/blob/main/LICENSE)
|
|
5
5
|
[](https://caniuse.com/webgpu)
|
|
6
6
|
|
|
7
|
-
A high-performance WebGPU particle engine for the web.
|
|
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.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
@@ -15,6 +15,7 @@ A high-performance WebGPU particle engine for the web.
|
|
|
15
15
|
- **Scene Serialization** — Save/load particle configurations as JSON
|
|
16
16
|
- **Multiple Emitter Shapes** — Sphere, cube, cylinder, circle, square emission patterns
|
|
17
17
|
- **Real-time Physics** — Gravity, attractors, drag, velocity, and lifetime management
|
|
18
|
+
- **React Three Fiber Component** — Drop-in `HZParticlesFX` component with preset support
|
|
18
19
|
- **3D Object Support** — Static 3D objects alongside particle systems
|
|
19
20
|
|
|
20
21
|
## Requirements
|
|
@@ -28,18 +29,36 @@ Check browser support: [caniuse.com/webgpu](https://caniuse.com/webgpu)
|
|
|
28
29
|
|
|
29
30
|
## Installation
|
|
30
31
|
|
|
32
|
+
### WebGPU
|
|
33
|
+
|
|
31
34
|
```bash
|
|
32
35
|
npm install hz-particles
|
|
33
36
|
```
|
|
34
37
|
|
|
38
|
+
```javascript
|
|
39
|
+
import { initWebGPU, ParticleSystemManager } from 'hz-particles';
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### React Three Fiber
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install hz-particles
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
import { HZParticlesFX } from 'hz-particles/r3f';
|
|
50
|
+
```
|
|
51
|
+
|
|
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.
|
|
53
|
+
|
|
35
54
|
## Quick Start
|
|
36
55
|
|
|
37
|
-
###
|
|
56
|
+
### WebGPU
|
|
38
57
|
|
|
39
58
|
```javascript
|
|
40
59
|
import { initWebGPU, ParticleSystemManager } from 'hz-particles';
|
|
41
60
|
|
|
42
|
-
// 1. Setup WebGPU context
|
|
61
|
+
// 1. Setup WebGPU context
|
|
43
62
|
const canvas = document.getElementById('webgpu-canvas');
|
|
44
63
|
canvas.width = 800;
|
|
45
64
|
canvas.height = 600;
|
|
@@ -54,16 +73,10 @@ const systemId = manager.createParticleSystem({
|
|
|
54
73
|
particleCount: 1000,
|
|
55
74
|
});
|
|
56
75
|
|
|
57
|
-
// 4.
|
|
76
|
+
// 4. Initialize compute pipeline
|
|
58
77
|
const system = manager.getActiveSystem();
|
|
59
78
|
await system.initComputePipeline(device);
|
|
60
79
|
|
|
61
|
-
// Optional: Load a texture
|
|
62
|
-
const response = await fetch('particle-texture.png');
|
|
63
|
-
const blob = await response.blob();
|
|
64
|
-
const imageBitmap = await createImageBitmap(blob);
|
|
65
|
-
await system.setTexture(imageBitmap);
|
|
66
|
-
|
|
67
80
|
// 5. Render loop
|
|
68
81
|
let lastTime = performance.now();
|
|
69
82
|
|
|
@@ -72,10 +85,8 @@ function render() {
|
|
|
72
85
|
const deltaTime = (currentTime - lastTime) / 1000;
|
|
73
86
|
lastTime = currentTime;
|
|
74
87
|
|
|
75
|
-
// Update physics
|
|
76
88
|
manager.updateAllSystems(deltaTime);
|
|
77
89
|
|
|
78
|
-
// Render
|
|
79
90
|
const commandEncoder = device.createCommandEncoder();
|
|
80
91
|
const renderPass = commandEncoder.beginRenderPass({
|
|
81
92
|
colorAttachments: [{
|
|
@@ -96,144 +107,99 @@ function render() {
|
|
|
96
107
|
render();
|
|
97
108
|
```
|
|
98
109
|
|
|
99
|
-
###
|
|
100
|
-
|
|
101
|
-
If you already have a WebGPU device and context (e.g., from an existing project or framework), you can skip `initWebGPU()` and use the library directly:
|
|
102
|
-
|
|
103
|
-
```javascript
|
|
104
|
-
import { ParticleSystemManager, createRenderTextures, createDepthTexture } from 'hz-particles';
|
|
105
|
-
|
|
106
|
-
// Assume you already have these from your existing WebGPU setup
|
|
107
|
-
const device = yourExistingDevice;
|
|
108
|
-
const context = yourExistingContext;
|
|
109
|
-
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
110
|
-
const canvas = yourExistingCanvas;
|
|
111
|
-
|
|
112
|
-
// Create particle system manager with your device
|
|
113
|
-
const manager = new ParticleSystemManager(device);
|
|
114
|
-
|
|
115
|
-
// Create a particle system
|
|
116
|
-
const systemId = manager.createParticleSystem({
|
|
117
|
-
maxParticles: 10000,
|
|
118
|
-
particleCount: 1000,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Initialize and use as shown in Option 1
|
|
122
|
-
const system = manager.getActiveSystem();
|
|
123
|
-
await system.initComputePipeline(device);
|
|
124
|
-
|
|
125
|
-
// Your render loop...
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
This approach is ideal for integrating into existing WebGPU applications, frameworks, or when you need custom WebGPU initialization.
|
|
129
|
-
|
|
130
|
-
## React Three Fiber Integration
|
|
110
|
+
### React Three Fiber
|
|
131
111
|
|
|
132
|
-
|
|
112
|
+
**Minimal example with a preset file:**
|
|
133
113
|
|
|
134
114
|
```jsx
|
|
135
|
-
import {
|
|
136
|
-
import
|
|
137
|
-
import { initWebGPU, ParticleSystemManager } from 'hz-particles';
|
|
138
|
-
|
|
139
|
-
function ParticleLayer() {
|
|
140
|
-
const canvasRef = useRef();
|
|
141
|
-
const engineRef = useRef();
|
|
142
|
-
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
let animationId;
|
|
145
|
-
|
|
146
|
-
async function init() {
|
|
147
|
-
// Create dedicated WebGPU canvas
|
|
148
|
-
const canvas = canvasRef.current;
|
|
149
|
-
canvas.width = window.innerWidth;
|
|
150
|
-
canvas.height = window.innerHeight;
|
|
151
|
-
const { device, context, format } = await initWebGPU(canvas);
|
|
152
|
-
|
|
153
|
-
// Setup particle system
|
|
154
|
-
const manager = new ParticleSystemManager(device);
|
|
155
|
-
manager.createParticleSystem({ particleCount: 1000 });
|
|
156
|
-
const system = manager.getActiveSystem();
|
|
157
|
-
await system.initComputePipeline(device);
|
|
158
|
-
|
|
159
|
-
engineRef.current = { device, context, manager, system };
|
|
160
|
-
|
|
161
|
-
// Render loop
|
|
162
|
-
let lastTime = performance.now();
|
|
163
|
-
function render() {
|
|
164
|
-
const { device, context, manager, system } = engineRef.current;
|
|
165
|
-
const currentTime = performance.now();
|
|
166
|
-
const deltaTime = (currentTime - lastTime) / 1000;
|
|
167
|
-
lastTime = currentTime;
|
|
168
|
-
|
|
169
|
-
manager.updateAllSystems(deltaTime);
|
|
170
|
-
|
|
171
|
-
const commandEncoder = device.createCommandEncoder();
|
|
172
|
-
const renderPass = commandEncoder.beginRenderPass({
|
|
173
|
-
colorAttachments: [{
|
|
174
|
-
view: context.getCurrentTexture().createView(),
|
|
175
|
-
clearValue: { r: 0, g: 0, b: 0, a: 0 }, // Transparent
|
|
176
|
-
loadOp: 'clear',
|
|
177
|
-
storeOp: 'store',
|
|
178
|
-
}],
|
|
179
|
-
});
|
|
180
|
-
system.render(renderPass);
|
|
181
|
-
renderPass.end();
|
|
182
|
-
device.queue.submit([commandEncoder.finish()]);
|
|
183
|
-
|
|
184
|
-
animationId = requestAnimationFrame(render);
|
|
185
|
-
}
|
|
186
|
-
render();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
init();
|
|
190
|
-
|
|
191
|
-
return () => {
|
|
192
|
-
if (animationId) cancelAnimationFrame(animationId);
|
|
193
|
-
};
|
|
194
|
-
}, []);
|
|
115
|
+
import { HZParticlesFX } from 'hz-particles/r3f';
|
|
116
|
+
import explosionPreset from './presets/explosion.json';
|
|
195
117
|
|
|
118
|
+
function Scene() {
|
|
196
119
|
return (
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
style={{
|
|
201
|
-
position: 'absolute',
|
|
202
|
-
top: 0,
|
|
203
|
-
left: 0,
|
|
204
|
-
width: '100%',
|
|
205
|
-
height: '100%',
|
|
206
|
-
pointerEvents: 'none',
|
|
207
|
-
}}
|
|
120
|
+
<HZParticlesFX
|
|
121
|
+
preset={explosionPreset}
|
|
122
|
+
position={[0, 1, 0]}
|
|
208
123
|
/>
|
|
209
124
|
);
|
|
210
125
|
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Complete example with all key props:**
|
|
129
|
+
|
|
130
|
+
```jsx
|
|
131
|
+
import { useRef, useState } from 'react';
|
|
132
|
+
import { HZParticlesFX } from 'hz-particles/r3f';
|
|
133
|
+
import explosionPreset from './presets/explosion.json';
|
|
134
|
+
|
|
135
|
+
function Scene() {
|
|
136
|
+
const targetRef = useRef();
|
|
137
|
+
const [resetKey, setResetKey] = useState(0);
|
|
211
138
|
|
|
212
|
-
// Usage in App
|
|
213
|
-
function App() {
|
|
214
139
|
return (
|
|
215
|
-
|
|
216
|
-
{
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
140
|
+
<>
|
|
141
|
+
<mesh ref={targetRef} position={[0, 1, 0]}>
|
|
142
|
+
<boxGeometry />
|
|
143
|
+
<meshStandardMaterial />
|
|
144
|
+
</mesh>
|
|
145
|
+
|
|
146
|
+
<HZParticlesFX
|
|
147
|
+
preset={explosionPreset}
|
|
148
|
+
positionRef={targetRef}
|
|
149
|
+
scale={1.5}
|
|
150
|
+
resetKey={resetKey}
|
|
151
|
+
onComplete={() => console.log('effect finished')}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
<button onClick={() => setResetKey(k => k + 1)}>
|
|
155
|
+
Replay
|
|
156
|
+
</button>
|
|
157
|
+
</>
|
|
228
158
|
);
|
|
229
159
|
}
|
|
230
160
|
```
|
|
231
161
|
|
|
162
|
+
## Props Reference
|
|
163
|
+
|
|
164
|
+
The `HZParticlesFX` component accepts the following props:
|
|
165
|
+
|
|
166
|
+
| Prop | Type | Default | Description |
|
|
167
|
+
|------|------|---------|-------------|
|
|
168
|
+
| `preset` | `SceneData \| string` | — | Particle preset — pass a JSON object or a URL/path to a JSON file |
|
|
169
|
+
| `position` | `[number, number, number]` | `[0, 0, 0]` | Static world-space position of the effect |
|
|
170
|
+
| `positionRef` | `React.RefObject<Object3D>` | — | Attach the effect to a scene object; position tracks the ref each frame |
|
|
171
|
+
| `autoPlay` | `boolean` | `true` | Start playing immediately when mounted |
|
|
172
|
+
| `visible` | `boolean` | `true` | Show or hide the effect without unmounting |
|
|
173
|
+
| `scale` | `number` | `1` | Uniform scale applied to the entire effect |
|
|
174
|
+
| `resetKey` | `number` | `0` | Increment to reset and respawn the particle system |
|
|
175
|
+
| `onComplete` | `() => void` | — | Callback fired when the effect finishes playing |
|
|
176
|
+
|
|
177
|
+
## Preset Configuration
|
|
178
|
+
|
|
179
|
+
Presets are plain JSON files that describe a particle scene. Key fields:
|
|
180
|
+
|
|
181
|
+
| Field | Type | Description |
|
|
182
|
+
|-------|------|-------------|
|
|
183
|
+
| `particleCount` | `number` | Number of active particles |
|
|
184
|
+
| `lifetime` | `number` | Particle lifetime in seconds |
|
|
185
|
+
| `emissionRate` | `number` | Particles emitted per second |
|
|
186
|
+
| `emissionShape` | `string` | Emitter shape: `sphere`, `cube`, `cylinder`, `circle`, `square` |
|
|
187
|
+
| `particleSize` | `number` | Base size of each particle |
|
|
188
|
+
| `colors` | `string[]` | Array of hex color values interpolated over lifetime |
|
|
189
|
+
| `fadeIn` | `number` | Opacity fade-in duration (0–1, fraction of lifetime) |
|
|
190
|
+
| `fadeOut` | `number` | Opacity fade-out start (0–1, fraction of lifetime) |
|
|
191
|
+
| `bloom` | `boolean` | Enable bloom glow on particles |
|
|
192
|
+
| `gravity` | `number` | Gravity strength applied to particles |
|
|
193
|
+
| `damping` | `number` | Velocity damping factor (0 = no drag, 1 = full stop) |
|
|
194
|
+
| `emissionTrailEnabled` | `boolean` | Enable particle trail rendering |
|
|
195
|
+
| `emissionTrailDuration` | `number` | How long trail segments persist in seconds |
|
|
196
|
+
| `emissionTrailWidth` | `number` | Width of the emission trail |
|
|
197
|
+
|
|
232
198
|
## API Reference
|
|
233
199
|
|
|
234
200
|
### `initWebGPU(canvas)`
|
|
235
201
|
|
|
236
|
-
Convenience helper to initialize WebGPU context and device.
|
|
202
|
+
Convenience helper to initialize a WebGPU context and device.
|
|
237
203
|
|
|
238
204
|
```typescript
|
|
239
205
|
async function initWebGPU(canvas: HTMLCanvasElement): Promise<{
|
|
@@ -245,21 +211,17 @@ async function initWebGPU(canvas: HTMLCanvasElement): Promise<{
|
|
|
245
211
|
```
|
|
246
212
|
|
|
247
213
|
**Parameters:**
|
|
248
|
-
- `canvas` (required) — Canvas element
|
|
214
|
+
- `canvas` (required) — Canvas element. Set `canvas.width` and `canvas.height` before calling.
|
|
249
215
|
|
|
250
|
-
**Returns:** Object with WebGPU device, canvas context, texture format, and canvas element
|
|
216
|
+
**Returns:** Object with WebGPU device, canvas context, texture format, and canvas element.
|
|
251
217
|
|
|
252
|
-
**Throws:**
|
|
253
|
-
- Error if `canvas` is not provided
|
|
254
|
-
- Error if WebGPU is not supported
|
|
255
|
-
|
|
256
|
-
**Note:** This function does NOT handle canvas sizing. You should set `canvas.width` and `canvas.height` before calling `initWebGPU()`.
|
|
218
|
+
**Throws:** Error if `canvas` is missing or WebGPU is not supported.
|
|
257
219
|
|
|
258
220
|
---
|
|
259
221
|
|
|
260
222
|
### `ParticleSystem`
|
|
261
223
|
|
|
262
|
-
Core particle system class managing
|
|
224
|
+
Core particle system class managing simulation and rendering.
|
|
263
225
|
|
|
264
226
|
```typescript
|
|
265
227
|
class ParticleSystem {
|
|
@@ -267,29 +229,22 @@ class ParticleSystem {
|
|
|
267
229
|
}
|
|
268
230
|
```
|
|
269
231
|
|
|
270
|
-
**
|
|
271
|
-
- `
|
|
272
|
-
- `
|
|
273
|
-
- `maxParticles` (number, default 10000) — Maximum particle buffer size
|
|
274
|
-
- `particleCount` (number, default 100) — Initial active particle count
|
|
232
|
+
**Config options:**
|
|
233
|
+
- `maxParticles` (number, default `10000`) — Maximum particle buffer size
|
|
234
|
+
- `particleCount` (number, default `100`) — Initial active particle count
|
|
275
235
|
|
|
276
236
|
**Key Methods:**
|
|
277
237
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
- `setGravity(value: number)` — Set gravity strength (default 9.8).
|
|
289
|
-
|
|
290
|
-
- `setAttractor(strength: number, position: [number, number, number])` — Set attractor point with strength and 3D position.
|
|
291
|
-
|
|
292
|
-
- `render(renderPass: GPURenderPassEncoder)` — Render particles to the current render pass.
|
|
238
|
+
| Method | Description |
|
|
239
|
+
|--------|-------------|
|
|
240
|
+
| `initComputePipeline(device)` | Initialize GPU compute and render pipelines. Must be called before rendering. |
|
|
241
|
+
| `setTexture(imageBitmap)` | Set particle texture from an `ImageBitmap`. |
|
|
242
|
+
| `setGLBModel(arrayBuffer)` | Use GLB model geometry as particle shape. Automatically extracts textures. |
|
|
243
|
+
| `updateParticles(deltaTime)` | Execute a physics simulation step on the GPU. |
|
|
244
|
+
| `spawnParticles()` | Emit new particles according to emitter configuration. |
|
|
245
|
+
| `setGravity(value)` | Set gravity strength (default `9.8`). |
|
|
246
|
+
| `setAttractor(strength, position)` | Set an attractor point with strength and 3D position `[x, y, z]`. |
|
|
247
|
+
| `render(renderPass)` | Render particles to the current render pass. |
|
|
293
248
|
|
|
294
249
|
---
|
|
295
250
|
|
|
@@ -305,23 +260,17 @@ class ParticleSystemManager {
|
|
|
305
260
|
|
|
306
261
|
**Key Methods:**
|
|
307
262
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
- `getSystemsList(): Array<{name: string, id: number, index: number, isActive: boolean}>` — Get list of all systems with metadata.
|
|
321
|
-
|
|
322
|
-
- `duplicateActiveSystem(): number` — Clone active system. Returns new system ID.
|
|
323
|
-
|
|
324
|
-
- `async replaceSystems(sceneData: object): boolean` — Load scene from serialized data. Returns success status.
|
|
263
|
+
| Method | Description |
|
|
264
|
+
|--------|-------------|
|
|
265
|
+
| `createParticleSystem(config?)` | Create a new particle system. Returns system ID. |
|
|
266
|
+
| `getActiveSystem()` | Get the currently active `ParticleSystem` instance. |
|
|
267
|
+
| `getActiveConfig()` | Get the active system configuration object. |
|
|
268
|
+
| `setActiveSystem(index)` | Switch active system by index. Returns success status. |
|
|
269
|
+
| `removeSystem(index)` | Remove a system by index. Returns success status. |
|
|
270
|
+
| `updateAllSystems(deltaTime)` | Update physics for all systems. |
|
|
271
|
+
| `getSystemsList()` | Get list of all systems with `name`, `id`, `index`, `isActive`. |
|
|
272
|
+
| `duplicateActiveSystem()` | Clone the active system. Returns new system ID. |
|
|
273
|
+
| `replaceSystems(sceneData)` | Load a scene from serialized data. Returns success status. |
|
|
325
274
|
|
|
326
275
|
---
|
|
327
276
|
|
|
@@ -342,11 +291,6 @@ async function parseGLB(arrayBuffer: ArrayBuffer): Promise<{
|
|
|
342
291
|
}>
|
|
343
292
|
```
|
|
344
293
|
|
|
345
|
-
**Parameters:**
|
|
346
|
-
- `arrayBuffer` — GLB file as ArrayBuffer
|
|
347
|
-
|
|
348
|
-
**Returns:** Parsed geometry and animation data
|
|
349
|
-
|
|
350
294
|
---
|
|
351
295
|
|
|
352
296
|
### `GLBAnimator`
|
|
@@ -359,42 +303,22 @@ class GLBAnimator {
|
|
|
359
303
|
|
|
360
304
|
currentTime: number
|
|
361
305
|
playing: boolean
|
|
362
|
-
speed: number
|
|
363
|
-
loop: boolean
|
|
306
|
+
speed: number // default 1.0
|
|
307
|
+
loop: boolean // default true
|
|
364
308
|
}
|
|
365
309
|
```
|
|
366
310
|
|
|
367
311
|
**Key Methods:**
|
|
368
312
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
- `getAnimationNames(): string[]` — Get list of available animation clip names.
|
|
376
|
-
|
|
377
|
-
---
|
|
378
|
-
|
|
379
|
-
### Secondary Exports
|
|
380
|
-
|
|
381
|
-
The following modules are also exported for advanced usage:
|
|
382
|
-
|
|
383
|
-
- **`ParticleEmitter`** — Emission shape configuration (sphere, cube, cylinder, circle, square)
|
|
384
|
-
- **`ParticlePhysics`** — Physics simulation parameter management
|
|
385
|
-
- **`ParticleTextureManager`** — Texture loading utilities
|
|
386
|
-
- **`Objects3DManager`** — 3D object scene management
|
|
387
|
-
- **`saveScene(manager)`** — Export scene as JSON file download
|
|
388
|
-
- **`loadScene(event)`** — Load scene from file input event
|
|
389
|
-
- **`extractGLBTexture(arrayBuffer)`** — Extract base color texture from GLB file
|
|
390
|
-
- **Shader exports** — WGSL shader code for compute and rendering pipelines
|
|
391
|
-
- **Geometry helpers** — Primitive shape generators (cube, sphere, etc.)
|
|
392
|
-
- **Render pipeline creators** — Low-level WebGPU pipeline construction
|
|
313
|
+
| Method | Description |
|
|
314
|
+
|--------|-------------|
|
|
315
|
+
| `setRestPose(positions, normals)` | Set the T-pose/bind pose for animation. |
|
|
316
|
+
| `update(deltaTime)` | Advance animation. Returns `{ positions, normals, changed }`. |
|
|
317
|
+
| `setAnimation(index)` | Switch to animation clip by index. |
|
|
318
|
+
| `getAnimationNames()` | Get list of available animation clip names. |
|
|
393
319
|
|
|
394
320
|
## GLB Models & Animation
|
|
395
321
|
|
|
396
|
-
Load and animate 3D models:
|
|
397
|
-
|
|
398
322
|
```javascript
|
|
399
323
|
import { parseGLB, GLBAnimator } from 'hz-particles';
|
|
400
324
|
|
|
@@ -425,20 +349,16 @@ if (glbData.animationData) {
|
|
|
425
349
|
|
|
426
350
|
## Scene Save/Load
|
|
427
351
|
|
|
428
|
-
Serialize and restore particle configurations:
|
|
429
|
-
|
|
430
352
|
```javascript
|
|
431
353
|
import { saveScene, loadScene } from 'hz-particles';
|
|
432
354
|
|
|
433
355
|
// Save current scene
|
|
434
|
-
|
|
435
|
-
button.addEventListener('click', () => {
|
|
356
|
+
document.getElementById('save-button').addEventListener('click', () => {
|
|
436
357
|
saveScene(manager); // Downloads scene.json
|
|
437
358
|
});
|
|
438
359
|
|
|
439
360
|
// Load scene from file input
|
|
440
|
-
|
|
441
|
-
input.addEventListener('change', async (event) => {
|
|
361
|
+
document.getElementById('file-input').addEventListener('change', async (event) => {
|
|
442
362
|
const success = await loadScene(event, manager);
|
|
443
363
|
if (success) {
|
|
444
364
|
console.log('Scene loaded successfully');
|
|
@@ -446,28 +366,45 @@ input.addEventListener('change', async (event) => {
|
|
|
446
366
|
});
|
|
447
367
|
```
|
|
448
368
|
|
|
449
|
-
## Online Editor
|
|
450
|
-
|
|
451
|
-
Try the interactive particle editor at `http://localhost:8110/editor` when running the Docker container (`docker-compose up` from the repository root)
|
|
452
|
-
|
|
453
|
-
The editor provides a visual interface for:
|
|
454
|
-
- Real-time particle system configuration
|
|
455
|
-
- Emitter shape selection and tuning
|
|
456
|
-
- GLB model import and animation control
|
|
457
|
-
- Scene preset library
|
|
458
|
-
- Export/import scene JSON
|
|
459
|
-
|
|
460
369
|
## TypeScript
|
|
461
370
|
|
|
462
|
-
Type declarations are not yet included in this package. For TypeScript projects,
|
|
371
|
+
Type declarations are not yet included in this package. For TypeScript projects, suppress the import error with:
|
|
463
372
|
|
|
464
373
|
```typescript
|
|
465
374
|
// @ts-ignore
|
|
466
375
|
import { ParticleSystem, initWebGPU } from 'hz-particles';
|
|
376
|
+
// @ts-ignore
|
|
377
|
+
import { HZParticlesFX } from 'hz-particles/r3f';
|
|
467
378
|
```
|
|
468
379
|
|
|
469
380
|
Community-contributed type definitions are welcome via PR.
|
|
470
381
|
|
|
382
|
+
## Secondary Exports
|
|
383
|
+
|
|
384
|
+
The following modules are also exported for advanced usage:
|
|
385
|
+
|
|
386
|
+
- **`ParticleEmitter`** — Emission shape configuration (sphere, cube, cylinder, circle, square)
|
|
387
|
+
- **`ParticlePhysics`** — Physics simulation parameter management
|
|
388
|
+
- **`ParticleTextureManager`** — Texture loading utilities
|
|
389
|
+
- **`Objects3DManager`** — 3D object scene management
|
|
390
|
+
- **`saveScene(manager)`** — Export scene as JSON file download
|
|
391
|
+
- **`loadScene(event)`** — Load scene from file input event
|
|
392
|
+
- **`extractGLBTexture(arrayBuffer)`** — Extract base color texture from GLB file
|
|
393
|
+
- **Shader exports** — WGSL shader code for compute and rendering pipelines
|
|
394
|
+
- **Geometry helpers** — Primitive shape generators (cube, sphere, etc.)
|
|
395
|
+
- **Render pipeline creators** — Low-level WebGPU pipeline construction
|
|
396
|
+
|
|
397
|
+
## Online Editor
|
|
398
|
+
|
|
399
|
+
Try the interactive particle editor at `http://localhost:8110/editor` when running the Docker container (`docker-compose up` from the repository root).
|
|
400
|
+
|
|
401
|
+
The editor provides a visual interface for:
|
|
402
|
+
- Real-time particle system configuration
|
|
403
|
+
- Emitter shape selection and tuning
|
|
404
|
+
- GLB model import and animation control
|
|
405
|
+
- Scene preset library
|
|
406
|
+
- Export/import scene JSON
|
|
407
|
+
|
|
471
408
|
## Contributing
|
|
472
409
|
|
|
473
410
|
Contributions are welcome! Please follow these steps:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const ke=require("react/jsx-runtime"),o=require("react"),Ne=require("@react-three/fiber"),$t=require("three"),xt=require("hz-particles");function Kt(n){const i=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(n){for(const l in n)if(l!=="default"){const T=Object.getOwnPropertyDescriptor(n,l);Object.defineProperty(i,l,T.get?T:{enumerable:!0,get:()=>n[l]})}}return i.default=n,Object.freeze(i)}const c=Kt($t);function vt(n){return Math.abs(Math.sin(n*12.9898)*43758.5453)%1}function en(n){const i=Math.sin(n*54321.67)*43758.5453%1;return i<0?i+1:i}function gt(n,i,l,T){if(l<=0)return 0;const d=i/l;let X=n.particleSize??.5;if(n.randomSize){const O=n.minSize??.1,Z=n.maxSize??.5;X=O+(Z-O)*en(l)}const Y=Math.max(.01,Math.min(10,n.sizeLifetimeSpeed??1));if(n.fadeSizeEnabled&&(X*=1-Math.pow(d,1/Y)),n.increaseSizeEnabled&&(X*=1+Math.pow(d,1/Y)),n.pulseEnabled){const O=n.pulseAmplitude??.5,Z=n.pulseFrequency??1,re=(n.pulsePhaseRandom??0)*vt(T)*Math.PI*2;X*=1+O*Math.sin(i*Z*Math.PI*2+re)}return Math.max(0,X)}function bt(n,i,l,T){if(l<=0)return 0;let d=n.opacity??1;const X=i/l;if(n.fadeEnabled&&(d*=Math.max(0,1-X)),n.pulseEnabled&&n.pulseOpacity){const Y=n.pulseAmplitude??.5,O=n.pulseFrequency??1,J=(n.pulsePhaseRandom??0)*vt(T)*Math.PI*2;d*=Math.max(0,1+Y*Math.sin(i*O*Math.PI*2+J))}return Math.max(0,Math.min(1,d))}function Ot(n,i){return i===1?n:{...n,systems:n.systems.map(l=>({...l,particleSize:(l.particleSize??.5)*i,minSize:l.minSize!=null?l.minSize*i:void 0,maxSize:l.maxSize!=null?l.maxSize*i:void 0,particleSpeed:(l.particleSpeed??1)*i,minSpeed:l.minSpeed!=null?l.minSpeed*i:void 0,maxSpeed:l.maxSpeed!=null?l.maxSpeed*i:void 0,emissionTrailWidth:l.emissionTrailWidth!=null?l.emissionTrailWidth*i:void 0}))}}function tn(n){let i=0;for(const l of n.systems)i+=l.maxParticles??1e4;return i}function nn({preset:n,position:i,positionRef:l,autoPlay:T=!0,visible:d=!0,scale:X=1,resetKey:Y=0,onComplete:O}){var Ve;const[Z,J]=o.useState(typeof n=="string"?null:n);o.useEffect(()=>{typeof n=="string"?(J(null),xt.fetchPreset(n).then(J).catch(a=>{console.error("[HZParticlesFX] Failed to fetch preset:",a)})):J(n)},[n]);const{camera:re,gl:Qe}=Ne.useThree(),fe=o.useRef(null);if(!fe.current){const a=(Ve=Qe.backend)==null?void 0:Ve.device;a&&(fe.current=a)}const ze=fe.current,Ae=o.useRef(null),ye=o.useRef(null),Re=o.useRef(!1),C=o.useRef(!1),h=o.useMemo(()=>Z?Ot(Z,X):null,[Z,X]),Ye=o.useMemo(()=>h?tn(h):0,[h]),Je=o.useMemo(()=>i?i instanceof c.Vector3?i.clone():new c.Vector3(...i):new c.Vector3(0,0,0),[i]),Pe=o.useMemo(()=>(h==null?void 0:h.systems.some(a=>a.emissionTrailEnabled))??!1,[h]),He=o.useMemo(()=>new c.PlaneGeometry(1,1),[]),_e=o.useMemo(()=>{const a=document.createElement("canvas");a.width=64,a.height=64;const V=a.getContext("2d");V.clearRect(0,0,64,64);const E=V.createRadialGradient(32,32,0,32,32,32);E.addColorStop(0,"rgba(255,255,255,1)"),E.addColorStop(.7,"rgba(255,255,255,1)"),E.addColorStop(1,"rgba(255,255,255,0)"),V.fillStyle=E,V.beginPath(),V.arc(32,32,32,0,Math.PI*2),V.fill();const M=new c.CanvasTexture(a);M.flipY=!1;const _=new c.MeshBasicMaterial({color:16777215,transparent:!0,depthWrite:!1,alphaTest:.01,map:M,vertexColors:!0,side:c.DoubleSide,blending:c.AdditiveBlending});return _.needsUpdate=!0,_},[]),t=64,D=100,Me=o.useRef(null),H=o.useRef(0),p=512,b=o.useRef({buf:new Float32Array(p*4),head:0,count:0,elapsed:0}),{trailGeo:$,trailPosAttr:oe,trailColAttr:y}=o.useMemo(()=>{const a=D*t*6,V=new Float32Array(a*3),E=new Float32Array(a*3),M=new c.BufferGeometry,_=new c.BufferAttribute(V,3);_.setUsage(c.DynamicDrawUsage),M.setAttribute("position",_);const de=new c.BufferAttribute(E,3);return de.setUsage(c.DynamicDrawUsage),M.setAttribute("color",de),M.setDrawRange(0,0),{trailGeo:M,trailPosAttr:_,trailColAttr:de}},[]),U=o.useMemo(()=>new c.MeshBasicMaterial({vertexColors:!0,transparent:!0,depthWrite:!1,side:c.DoubleSide,blending:c.AdditiveBlending}),[]);o.useEffect(()=>{if(!ze||!h)return;const a=new xt.ParticleSystemManager(ze);return ye.current=a,C.current=!1,a.replaceSystems(h).then(()=>{T&&d&&(a.respawnAllSystems(),C.current=!0)}).catch(V=>{console.error("[HZParticlesFX] replaceSystems FAILED:",V)}),()=>{typeof a.destroy=="function"&&a.destroy(),ye.current=null}},[ze,h]),o.useEffect(()=>{const a=ye.current;a&&T&&d&&!C.current&&(a.respawnAllSystems(),C.current=!0,Re.current=!1)},[T,d]);const Se=o.useRef(Y);o.useEffect(()=>{if(Y===Se.current)return;Se.current=Y;const a=b.current;a.head=0,a.count=0,a.elapsed=0,$.setDrawRange(0,0),z.current=null;const V=ye.current;V&&(V.respawnAllSystems(),C.current=!0,Re.current=!1)},[Y]);const xe=o.useMemo(()=>new c.Matrix4,[]),ce=o.useMemo(()=>new c.Vector3,[]),Te=o.useMemo(()=>new c.Vector3,[]),ge=o.useMemo(()=>new c.Quaternion,[]),K=o.useMemo(()=>new c.Vector3,[]),ae=o.useMemo(()=>new c.Vector3(0,0,1),[]),qe=o.useMemo(()=>new c.Color,[]),z=o.useRef(null);return Ne.useFrame((a,V)=>{if(!d||!Ae.current||!ye.current)return;const E=ye.current,M=(l==null?void 0:l.current)??Je,_=V>1?V/1e3:V,de=z.current,$e=de&&_>0,Ht=$e?(M.x-de.x)/_:0,_t=$e?(M.y-de.y)/_:0,qt=$e?(M.z-de.z)/_:0;z.current={x:M.x,y:M.y,z:M.z};for(const{system:s,config:r}of E.particleSystems)r.emissionTrailEnabled?(s.setSimulationTransform({position:[M.x,M.y,M.z],velocity:[Ht,_t,qt]}),r.shapeTranslationX=0,r.shapeTranslationY=0,r.shapeTranslationZ=0):(r.shapeTranslationX=M.x,r.shapeTranslationY=M.y,r.shapeTranslationZ=M.z);E.updateAllSystems(_);for(const{system:s}of E.particleSystems)s.readbackAndProcessParticles();if(Pe){const s=b.current;s.elapsed+=_;const r=s.head*4;s.buf[r]=M.x,s.buf[r+1]=M.y,s.buf[r+2]=M.z,s.buf[r+3]=s.elapsed,s.head=(s.head+1)%p,s.count<p&&s.count++}const Ce=Ae.current;let je=0;for(let s=0;s<E.particleSystems.length;s++){const{system:r,config:x}=E.particleSystems[s];if(x.emissionTrailEnabled)continue;const G=r.particleData,pe=r.activeParticles;if(!G||pe<=0)continue;const B=r._simPosition,A=x.emissionTrailEnabled&&B?B[0]:0,N=x.emissionTrailEnabled&&B?B[1]:0,De=x.emissionTrailEnabled&&B?B[2]:0;for(let ie=0;ie<pe;ie++){const ee=ie*8,Ke=G[ee+0]+A,me=G[ee+1]+N,et=G[ee+2]+De;let be=G[ee+3],ve=G[ee+4],L=G[ee+5];const Ee=G[ee+6],we=G[ee+7];if(Ee>=we||we<=0)continue;const We=Ee/we;if(x.colorTransitionEnabled){const I=x.startColor??[1,0,0],k=x.endColor??[0,0,1];be=I[0]+(k[0]-I[0])*We,ve=I[1]+(k[1]-I[1])*We,L=I[2]+(k[2]-I[2])*We}const he=x.bloomIntensity??1;be=Math.min(1,be*he),ve=Math.min(1,ve*he),L=Math.min(1,L*he);const v=bt(x,Ee,we,ie);be*=v,ve*=v,L*=v;const q=gt(x,Ee,we,ie);if(!(q<=0)){if(ce.set(Ke,me,et),Te.subVectors(re.position,ce).normalize(),ge.setFromUnitVectors(ae,Te),x.velocityStretchEnabled&&r.particleVelocities){const I=ie*4,k=r.particleVelocities[I],Ie=r.particleVelocities[I+1],Fe=r.particleVelocities[I+2],Xe=Math.sqrt(k*k+Ie*Ie+Fe*Fe);if(Xe>.001){const te=x.velocityStretchFactor??1,Q=q*(1+Xe*te);K.set(q,Q,1),Te.set(k,Ie,Fe).normalize(),ge.setFromUnitVectors(ae,Te);const tt=(Q-q)*.5;ce.addScaledVector(Te,tt)}else K.set(q,q,1)}else K.set(q,q,1);xe.compose(ce,ge,K),Ce.setMatrixAt(je,xe),qe.setRGB(be,ve,L),Ce.setColorAt(je,qe),je++}}}Ce.count=je,Ce.instanceMatrix.needsUpdate=!0,Ce.instanceColor&&(Ce.instanceColor.needsUpdate=!0);const wt=je>0||E.particleSystems.some(({system:s})=>s.emitting||s.activeParticles>0);if(Re.current&&!wt&&(C.current=!1,O==null||O()),Re.current=wt,Pe&&Me.current){const s=oe.array,r=y.array;let x=0;for(let pe=0;pe<E.particleSystems.length;pe++){const{system:B,config:A}=E.particleSystems[pe];if(!A.emissionTrailEnabled||A.glbModelEnabled||A.textureEnabled)continue;const N=B.particleData,De=B.particleVelocities,ie=B.activeParticles;if(!N||!De||ie<=0)continue;const ee=A.emissionTrailDuration??1,Ke=A.emissionTrailWidth??.3,me=B._simPosition,et=me?me[0]:0,be=me?me[1]:0,ve=me?me[2]:0,L=B._simVelocity,Ee=L?L[0]:0,we=L?L[1]:0,We=L?L[2]:0,he=b.current,v=he.buf,q=he.count,I=he.head,k=he.elapsed,Ie=q>=2;let Fe=k;if(Ie){const te=((I-q)%p+p)%p;Fe=k-v[te*4+3]}const Xe=k;for(let te=0;te<ie&&!(x+t*6>D*t*6);te++){const Q=te*8,tt=N[Q+6],Ue=N[Q+7];if(tt>=Ue||Ue<=0)continue;const nt=Math.min(Xe,Ue),st=nt/Ue;let Ze=N[Q+3],Ge=N[Q+4],Le=N[Q+5];if(A.colorTransitionEnabled){const e=A.startColor??[1,0,0],f=A.endColor??[0,0,1];Ze=e[0]+(f[0]-e[0])*st,Ge=e[1]+(f[1]-e[1])*st,Le=e[2]+(f[2]-e[2])*st}const rt=A.bloomIntensity??1;if(Ze=Math.min(1,Ze*rt),Ge=Math.min(1,Ge*rt),Le=Math.min(1,Le*rt),gt(A,nt,Ue,te)<=0)continue;const zt=Ke*.5,ot=bt(A,nt,Ue,te);if(ot<.01)continue;const ct=te*4,at=De[ct],it=De[ct+1],lt=De[ct+2],jt=Math.sqrt(at*at+it*it+lt*lt)>.05,At=Math.min(ee,Xe,Fe);if(At<.001)continue;const Wt=(e,f)=>{if(!Ie){f[0]=et-Ee*e,f[1]=be-we*e,f[2]=ve-We*e;return}const S=k-e;for(let j=0;j<q-1;j++){const ne=((I-1-j)%p+p)%p,W=((I-2-j)%p+p)%p,ue=v[ne*4+3],g=v[W*4+3];if(S>=g&&S<=ue){const w=ue!==g?(S-g)/(ue-g):0;f[0]=v[W*4]+(v[ne*4]-v[W*4])*w,f[1]=v[W*4+1]+(v[ne*4+1]-v[W*4+1])*w,f[2]=v[W*4+2]+(v[ne*4+2]-v[W*4+2])*w;return}}const m=((I-q)%p+p)%p;f[0]=v[m*4],f[1]=v[m*4+1],f[2]=v[m*4+2]},Xt=re.position.x,Zt=re.position.y,Gt=re.position.z,u=new Array((t+1)*3);for(let e=0;e<=t;e++){const S=e/t*At,m=[0,0,0];Wt(S,m),jt?(u[e*3]=N[Q]-at*S+m[0],u[e*3+1]=N[Q+1]-it*S+m[1],u[e*3+2]=N[Q+2]-lt*S+m[2]):(u[e*3]=m[0],u[e*3+1]=m[1],u[e*3+2]=m[2])}const le=new Array((t+1)*3);let Rt=0,Pt=0,Tt=0;for(let e=0;e<=t;e++){let f,S,m;e===0?(f=u[3]-u[0],S=u[4]-u[1],m=u[5]-u[2]):e===t?(f=u[e*3]-u[(e-1)*3],S=u[e*3+1]-u[(e-1)*3+1],m=u[e*3+2]-u[(e-1)*3+2]):(f=u[(e+1)*3]-u[(e-1)*3],S=u[(e+1)*3+1]-u[(e-1)*3+1],m=u[(e+1)*3+2]-u[(e-1)*3+2]);let j=Math.sqrt(f*f+S*S+m*m);j<1e-8&&(f=0,S=0,m=1,j=1),f/=j,S/=j,m/=j;const ne=Xt-u[e*3],W=Zt-u[e*3+1],ue=Gt-u[e*3+2];let g=S*ue-m*W,w=m*ne-f*ue,F=f*W-S*ne,se=Math.sqrt(g*g+w*w+F*F);se<1e-8&&(g=-m,w=0,F=f,se=Math.sqrt(g*g+w*w+F*F)),se<1e-8&&(g=0,w=1,F=0,se=1),g/=se,w/=se,F/=se,e>0&&g*Rt+w*Pt+F*Tt<0&&(g=-g,w=-w,F=-F),Rt=g,Pt=w,Tt=F,le[e*3]=g,le[e*3+1]=w,le[e*3+2]=F}for(let e=0;e<t;e++){const f=e/t,S=(e+1)/t,m=u[e*3],j=u[e*3+1],ne=u[e*3+2],W=u[(e+1)*3],ue=u[(e+1)*3+1],g=u[(e+1)*3+2],w=le[e*3],F=le[e*3+1],se=le[e*3+2],ut=le[(e+1)*3],Vt=le[(e+1)*3+1],Ct=le[(e+1)*3+2],Be=zt*(1-f),Oe=zt*(1-S),ft=ot*(1-f)*(1-f),dt=ot*(1-S)*(1-S),Lt=m+w*Be,kt=j+F*Be,Yt=ne+se*Be,Dt=m-w*Be,Et=j-F*Be,It=ne-se*Be,Ft=W+ut*Oe,Ut=ue+Vt*Oe,Bt=g+Ct*Oe,Nt=W-ut*Oe,Qt=ue-Vt*Oe,Jt=g-Ct*Oe,pt=Ze*ft,mt=Ge*ft,ht=Le*ft,yt=Ze*dt,Mt=Ge*dt,St=Le*dt,R=x*3,P=x*3;s[R]=Lt,s[R+1]=kt,s[R+2]=Yt,r[P]=pt,r[P+1]=mt,r[P+2]=ht,s[R+3]=Dt,s[R+4]=Et,s[R+5]=It,r[P+3]=pt,r[P+4]=mt,r[P+5]=ht,s[R+6]=Ft,s[R+7]=Ut,s[R+8]=Bt,r[P+6]=yt,r[P+7]=Mt,r[P+8]=St,s[R+9]=Dt,s[R+10]=Et,s[R+11]=It,r[P+9]=pt,r[P+10]=mt,r[P+11]=ht,s[R+12]=Nt,s[R+13]=Qt,s[R+14]=Jt,r[P+12]=yt,r[P+13]=Mt,r[P+14]=St,s[R+15]=Ft,s[R+16]=Ut,s[R+17]=Bt,r[P+15]=yt,r[P+16]=Mt,r[P+17]=St,x+=6}}}const G=H.current;if(x<G){const pe=x*3,B=G*3;for(let A=pe;A<B;A++)s[A]=0,r[A]=0}H.current=x,$.setDrawRange(0,x),oe.needsUpdate=!0,y.needsUpdate=!0}}),Z?ke.jsxs(ke.Fragment,{children:[ke.jsx("instancedMesh",{ref:a=>{Ae.current=a,a&&(a.count=0)},args:[He,_e,Ye],frustumCulled:!1,renderOrder:100,visible:d}),Pe&&ke.jsx("mesh",{ref:Me,geometry:$,material:U,frustumCulled:!1,renderOrder:99,visible:d})]}):null}const sn=`
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const Oe=require("react/jsx-runtime"),r=require("react"),je=require("@react-three/fiber"),Vt=require("three"),Xe=require("hz-particles");function Dt(e){const u=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const f in e)if(f!=="default"){const A=Object.getOwnPropertyDescriptor(e,f);Object.defineProperty(u,f,A.get?A:{enumerable:!0,get:()=>e[f]})}}return u.default=e,Object.freeze(u)}const c=Dt(Vt);function ke(e){return Math.abs(Math.sin(e*12.9898)*43758.5453)%1}function Et(e){const u=Math.sin(e*54321.67)*43758.5453%1;return u<0?u+1:u}function xt(e,u,f,A){if(f<=0)return 0;const p=u/f;let _=e.particleSize??.5;if(e.randomSize){const q=e.minSize??.1,W=e.maxSize??.5;_=q+(W-q)*Et(f)}const Z=Math.max(.01,Math.min(10,e.sizeLifetimeSpeed??1));if(e.fadeSizeEnabled&&(_*=1-Math.pow(p,1/Z)),e.increaseSizeEnabled&&(_*=1+Math.pow(p,1/Z)),e.pulseEnabled){const q=e.pulseAmplitude??.5,W=e.pulseFrequency??1,te=(e.pulsePhaseRandom??0)*ke(A)*Math.PI*2;_*=1+q*Math.sin(u*W*Math.PI*2+te)}return Math.max(0,_)}function St(e,u,f,A){if(f<=0)return 0;let p=e.opacity??1;const _=u/f;if(e.fadeEnabled&&(p*=Math.max(0,1-_)),e.pulseEnabled&&e.pulseOpacity){const Z=e.pulseAmplitude??.5,q=e.pulseFrequency??1,J=(e.pulsePhaseRandom??0)*ke(A)*Math.PI*2;p*=Math.max(0,1+Z*Math.sin(u*q*Math.PI*2+J))}return Math.max(0,Math.min(1,p))}function gt(e,u){return u===1?e:{...e,systems:e.systems.map(f=>({...f,particleSize:(f.particleSize??.5)*u,minSize:f.minSize!=null?f.minSize*u:void 0,maxSize:f.maxSize!=null?f.maxSize*u:void 0,particleSpeed:(f.particleSpeed??1)*u,minSpeed:f.minSpeed!=null?f.minSpeed*u:void 0,maxSpeed:f.maxSpeed!=null?f.maxSpeed*u:void 0,emissionTrailWidth:(f.emissionTrailWidth??.3)*u}))}}function Ft(e){let u=0;for(const f of e.systems)u+=f.maxParticles??1e4;return u}function It({preset:e,position:u,positionRef:f,autoPlay:A=!0,visible:p=!0,scale:_=1,resetKey:Z=0,onComplete:q}){var Ce;const[W,J]=r.useState(typeof e=="string"?null:e);r.useEffect(()=>{typeof e=="string"?(J(null),Xe.fetchPreset(e).then(J).catch(a=>{console.error("[HZParticlesFX] Failed to fetch preset:",a)})):J(e)},[e]);const{camera:te,gl:_e}=je.useThree(),ae=r.useRef(null);if(!ae.current){const a=(Ce=_e.backend)==null?void 0:Ce.device;a&&(ae.current=a)}const Re=ae.current,Pe=r.useRef(null),fe=r.useRef(null),Te=r.useRef(!1),E=r.useRef(!1),d=r.useMemo(()=>W?gt(W,_):null,[W,_]),Fe=r.useMemo(()=>(d==null?void 0:d.systems.every(a=>a.emissionTrailEnabled))??!1,[d]),We=r.useMemo(()=>d?d.systems.every(a=>a.emissionTrailEnabled)?1:Ft(d):0,[d]),He=r.useMemo(()=>u?u instanceof c.Vector3?u.clone():new c.Vector3(...u):new c.Vector3(0,0,0),[u]),de=r.useMemo(()=>(d==null?void 0:d.systems.some(a=>a.emissionTrailEnabled))??!1,[d]),Ie=r.useMemo(()=>new c.PlaneGeometry(1,1),[]),s=r.useMemo(()=>{const a=document.createElement("canvas");a.width=64,a.height=64;const z=a.getContext("2d");z.clearRect(0,0,64,64);const F=z.createRadialGradient(32,32,0,32,32,32);F.addColorStop(0,"rgba(255,255,255,1)"),F.addColorStop(.7,"rgba(255,255,255,1)"),F.addColorStop(1,"rgba(255,255,255,0)"),z.fillStyle=F,z.beginPath(),z.arc(32,32,32,0,Math.PI*2),z.fill();const x=new c.CanvasTexture(a);x.flipY=!1;const B=new c.MeshBasicMaterial({color:16777215,transparent:!0,depthWrite:!1,alphaTest:.01,map:x,vertexColors:!0,side:c.DoubleSide,blending:c.AdditiveBlending});return B.needsUpdate=!0,B},[]),h=8,pe=r.useRef(null),j=r.useRef(0),y=512,b=r.useRef({buf:new Float32Array(y*4),head:0,count:0,elapsed:0}),{trailGeo:$,trailPosAttr:ne,trailColAttr:M}=r.useMemo(()=>{const z=8*h*6,F=new Float32Array(z*3),x=new Float32Array(z*3),B=new c.BufferGeometry,le=new c.BufferAttribute(F,3);le.setUsage(c.DynamicDrawUsage),B.setAttribute("position",le);const ye=new c.BufferAttribute(x,3);return ye.setUsage(c.DynamicDrawUsage),B.setAttribute("color",ye),B.setDrawRange(0,0),{trailGeo:B,trailPosAttr:le,trailColAttr:ye}},[]),U=r.useMemo(()=>new c.MeshBasicMaterial({color:16777215,vertexColors:!0,transparent:!0,depthWrite:!1,side:c.DoubleSide,blending:c.AdditiveBlending}),[]);r.useEffect(()=>{if(!Re||!d)return;const a=new Xe.ParticleSystemManager(Re);return fe.current=a,E.current=!1,a.replaceSystems(d).then(()=>{A&&p&&(a.respawnAllSystems(),E.current=!0)}).catch(z=>{console.error("[HZParticlesFX] replaceSystems FAILED:",z)}),()=>{typeof a.destroy=="function"&&a.destroy(),fe.current=null}},[Re,d]),r.useEffect(()=>{const a=fe.current;a&&A&&p&&!E.current&&(a.respawnAllSystems(),E.current=!0,Te.current=!1)},[A,p]);const ie=r.useRef(null),me=r.useRef(Z);r.useEffect(()=>{if(Z===me.current)return;me.current=Z;const a=b.current;a.head=0,a.count=0,a.elapsed=0,$.setDrawRange(0,0),ie.current=null;const z=fe.current;z&&(z.respawnAllSystems(),E.current=!0,Te.current=!1)},[Z]);const he=r.useMemo(()=>new c.Matrix4,[]),Ue=r.useMemo(()=>new c.Vector3,[]),se=r.useMemo(()=>new c.Vector3,[]),re=r.useMemo(()=>new c.Quaternion,[]),X=r.useMemo(()=>new c.Vector3,[]),Be=r.useMemo(()=>new c.Vector3(0,0,1),[]),R=r.useMemo(()=>new c.Color,[]);return je.useFrame((a,z)=>{if(!p||!Pe.current||!fe.current)return;const F=fe.current,x=(f==null?void 0:f.current)??He,B=z>1?z/1e3:z,le=ie.current,ye=le&&B>0,vt=ye?(x.x-le.x)/B:0,bt=ye?(x.y-le.y)/B:0,wt=ye?(x.z-le.z)/B:0;ie.current={x:x.x,y:x.y,z:x.z};for(const{system:n,config:o}of F.particleSystems)o.emissionTrailEnabled?(n.setSimulationTransform({position:[x.x,x.y,x.z],velocity:[vt,bt,wt]}),o.shapeTranslationX=0,o.shapeTranslationY=0,o.shapeTranslationZ=0):(o.shapeTranslationX=x.x,o.shapeTranslationY=x.y,o.shapeTranslationZ=x.z);F.updateAllSystems(B);for(const{system:n}of F.particleSystems)n.readbackAndProcessParticles();if(de){const n=b.current;n.elapsed+=B;const o=n.head*4;n.buf[o]=x.x,n.buf[o+1]=x.y,n.buf[o+2]=x.z,n.buf[o+3]=n.elapsed,n.head=(n.head+1)%y,n.count<y&&n.count++}const Me=Pe.current;let xe=0;for(let n=0;n<F.particleSystems.length;n++){const{system:o,config:C}=F.particleSystems[n];if(C.emissionTrailEnabled)continue;const k=o.particleData,qe=o.activeParticles;if(!(!k||qe<=0))for(let ue=0;ue<qe;ue++){const L=ue*8,Se=k[L+0],Ve=k[L+1],O=k[L+2];let ge=k[L+3],ve=k[L+4],G=k[L+5];const be=k[L+6],K=k[L+7];if(be>=K||K<=0)continue;const w=be/K;if(C.colorTransitionEnabled){const I=C.startColor??[1,0,0],oe=C.endColor??[0,0,1];ge=I[0]+(oe[0]-I[0])*w,ve=I[1]+(oe[1]-I[1])*w,G=I[2]+(oe[2]-I[2])*w}const we=C.bloomIntensity??1;ge=Math.min(1,ge*we),ve=Math.min(1,ve*we),G=Math.min(1,G*we);const ze=St(C,be,K,ue);ge*=ze,ve*=ze,G*=ze;const Y=xt(C,be,K,ue);if(!(Y<=0)){if(Ue.set(Se,Ve,O),se.subVectors(te.position,Ue).normalize(),re.setFromUnitVectors(Be,se),C.velocityStretchEnabled&&o.particleVelocities){const I=ue*4,oe=o.particleVelocities[I],De=o.particleVelocities[I+1],P=o.particleVelocities[I+2],ce=Math.sqrt(oe*oe+De*De+P*P);if(ce>.001){const Ge=C.velocityStretchFactor??1,i=Y*(1+ce*Ge);X.set(Y,i,1),se.set(oe,De,P).normalize(),re.setFromUnitVectors(Be,se);const Ee=(i-Y)*.5;Ue.addScaledVector(se,Ee)}else X.set(Y,Y,1)}else X.set(Y,Y,1);he.compose(Ue,re,X),Me.setMatrixAt(xe,he),R.setRGB(ge,ve,G),Me.setColorAt(xe,R),xe++}}}Me.count=xe,Me.visible=xe>0,xe>0&&(Me.instanceMatrix.needsUpdate=!0,Me.instanceColor&&(Me.instanceColor.needsUpdate=!0));const Le=xe>0||F.particleSystems.some(({system:n})=>n.emitting||n.activeParticles>0);if(Te.current&&!Le&&(E.current=!1,q==null||q()),Te.current=Le,de&&pe.current){const n=ne.array,o=M.array;let C=0;const k=te.position.x,qe=te.position.y,ue=te.position.z;for(let Se=0;Se<F.particleSystems.length;Se++){const{system:Ve,config:O}=F.particleSystems[Se];if(!O.emissionTrailEnabled||!Ve.emitting&&Ve.activeParticles<=0)continue;const ge=O.emissionTrailDuration??1,ve=O.emissionTrailWidth??.3,G=O.bloomIntensity??1,be=ve*.5,K=b.current,w=K.buf,we=K.count,ze=K.head,Y=K.elapsed;if(we<2)continue;const I=((ze-we)%y+y)%y,oe=Y-w[I*4+3],De=Math.min(ge,oe);if(De<.001)continue;const P=O.startColor??[1,1,1],ce=O.colorTransitionEnabled?O.endColor??[1,1,1]:P,Ge=(t,m)=>{const g=Y-t;for(let S=0;S<we-1;S++){const T=((ze-1-S)%y+y)%y,H=((ze-2-S)%y+y)%y,ee=w[T*4+3],N=w[H*4+3];if(g>=N&&g<=ee){const v=ee!==N?(g-N)/(ee-N):0;m[0]=w[H*4]+(w[T*4]-w[H*4])*v,m[1]=w[H*4+1]+(w[T*4+1]-w[H*4+1])*v,m[2]=w[H*4+2]+(w[T*4+2]-w[H*4+2])*v;return}}m[0]=w[I*4],m[1]=w[I*4+1],m[2]=w[I*4+2]},i=[x.x,x.y,x.z],Ee=[0,0,0];for(let t=1;t<=h;t++){const m=t/h*De;Ge(m,Ee),i.push(Ee[0],Ee[1],Ee[2])}const Ye=i[0]-i[h*3],Ne=i[1]-i[h*3+1],Qe=i[2]-i[h*3+2];if(Ye*Ye+Ne*Ne+Qe*Qe<1e-6)continue;let Je=0,$e=0,Ke=0;const Ae=[];for(let t=0;t<=h;t++){let m,g,S;t===0?(m=i[3]-i[0],g=i[4]-i[1],S=i[5]-i[2]):t===h?(m=i[t*3]-i[(t-1)*3],g=i[t*3+1]-i[(t-1)*3+1],S=i[t*3+2]-i[(t-1)*3+2]):(m=i[(t+1)*3]-i[(t-1)*3],g=i[(t+1)*3+1]-i[(t-1)*3+1],S=i[(t+1)*3+2]-i[(t-1)*3+2]);let T=Math.sqrt(m*m+g*g+S*S);T<1e-8&&(m=0,g=0,S=1,T=1),m/=T,g/=T,S/=T;const H=k-i[t*3],ee=qe-i[t*3+1],N=ue-i[t*3+2];let v=g*N-S*ee,V=S*H-m*N,D=m*ee-g*H,Q=Math.sqrt(v*v+V*V+D*D);Q<1e-8&&(v=-S,V=0,D=m,Q=Math.sqrt(v*v+V*V+D*D)),Q<1e-8&&(v=0,V=1,D=0,Q=1),v/=Q,V/=Q,D/=Q,t>0&&v*Je+V*$e+D*Ke<0&&(v=-v,V=-V,D=-D),Je=v,$e=V,Ke=D,Ae.push(v,V,D)}for(let t=0;t<h;t++){const m=t/h,g=(t+1)/h,S=be*(1-m),T=be*(1-g),H=(1-m)*(1-m),ee=(1-g)*(1-g),N=Math.min(1,(P[0]+(ce[0]-P[0])*m)*G)*H,v=Math.min(1,(P[1]+(ce[1]-P[1])*m)*G)*H,V=Math.min(1,(P[2]+(ce[2]-P[2])*m)*G)*H,D=Math.min(1,(P[0]+(ce[0]-P[0])*g)*G)*ee,Q=Math.min(1,(P[1]+(ce[1]-P[1])*g)*G)*ee,Ze=Math.min(1,(P[2]+(ce[2]-P[2])*g)*G)*ee,et=i[t*3],tt=i[t*3+1],nt=i[t*3+2],st=i[(t+1)*3],rt=i[(t+1)*3+1],ot=i[(t+1)*3+2],ct=Ae[t*3],at=Ae[t*3+1],it=Ae[t*3+2],lt=Ae[(t+1)*3],ut=Ae[(t+1)*3+1],ft=Ae[(t+1)*3+2],zt=et+ct*S,At=tt+at*S,Rt=nt+it*S,dt=et-ct*S,pt=tt-at*S,mt=nt-it*S,ht=st+lt*T,yt=rt+ut*T,Mt=ot+ft*T,Pt=st-lt*T,Tt=rt-ut*T,Ct=ot-ft*T,l=C*3;n[l]=zt,n[l+1]=At,n[l+2]=Rt,o[l]=N,o[l+1]=v,o[l+2]=V,n[l+3]=dt,n[l+4]=pt,n[l+5]=mt,o[l+3]=N,o[l+4]=v,o[l+5]=V,n[l+6]=ht,n[l+7]=yt,n[l+8]=Mt,o[l+6]=D,o[l+7]=Q,o[l+8]=Ze,n[l+9]=dt,n[l+10]=pt,n[l+11]=mt,o[l+9]=N,o[l+10]=v,o[l+11]=V,n[l+12]=Pt,n[l+13]=Tt,n[l+14]=Ct,o[l+12]=D,o[l+13]=Q,o[l+14]=Ze,n[l+15]=ht,n[l+16]=yt,n[l+17]=Mt,o[l+15]=D,o[l+16]=Q,o[l+17]=Ze,C+=6}}const L=j.current;if(C<L){const Se=C*3,Ve=L*3;for(let O=Se;O<Ve;O++)n[O]=0,o[O]=0}j.current=C,$.setDrawRange(0,C),ne.needsUpdate=!0,M.needsUpdate=!0}}),W?Oe.jsxs(Oe.Fragment,{children:[Oe.jsx("instancedMesh",{ref:a=>{Pe.current=a,a&&(a.count=0,a.visible=!Fe)},args:[Ie,s,We],frustumCulled:!1,renderOrder:100,visible:p&&!Fe}),de&&Oe.jsx("mesh",{ref:pe,geometry:$,material:U,frustumCulled:!1,renderOrder:99,visible:p})]}):null}const Ut=`
|
|
2
2
|
attribute vec4 aColor;
|
|
3
3
|
varying vec4 vColor;
|
|
4
4
|
void main() {
|
|
5
5
|
vColor = aColor;
|
|
6
6
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
7
7
|
}
|
|
8
|
-
`,
|
|
8
|
+
`,Bt=`
|
|
9
9
|
varying vec4 vColor;
|
|
10
10
|
void main() {
|
|
11
11
|
gl_FragColor = vColor;
|
|
12
12
|
}
|
|
13
|
-
`;function
|
|
13
|
+
`;function Ot({positionRef:e,duration:u=1,width:f=.3,color:A=[1,1,1],maxPoints:p=256,minDistance:_=.05,opacity:Z=1,blending:q=c.AdditiveBlending,visible:W=!0}){const{camera:J}=je.useThree(),te=r.useRef(null),_e=r.useRef({positions:new Float32Array(p*3),times:new Float32Array(p),head:0,count:0,lastPos:new c.Vector3(1/0,1/0,1/0),elapsed:0}),{geometry:ae,posAttr:Re,colorAttr:Pe,indexBuf:fe}=r.useMemo(()=>{const de=p*2,Ie=new Float32Array(de*3),s=new Float32Array(de*4),h=new c.BufferGeometry,pe=new c.BufferAttribute(Ie,3);pe.setUsage(c.DynamicDrawUsage),h.setAttribute("position",pe);const j=new c.BufferAttribute(s,4);j.setUsage(c.DynamicDrawUsage),h.setAttribute("aColor",j);const y=(p-1)*6,b=new Uint32Array(y);for(let ne=0;ne<p-1;ne++){const M=ne*2,U=ne*6;b[U]=M,b[U+1]=M+1,b[U+2]=M+2,b[U+3]=M+1,b[U+4]=M+3,b[U+5]=M+2}const $=new c.BufferAttribute(b,1);return h.setIndex($),h.setDrawRange(0,0),{geometry:h,posAttr:pe,colorAttr:j,indexBuf:$}},[p]),Te=r.useMemo(()=>new c.ShaderMaterial({vertexShader:Ut,fragmentShader:Bt,transparent:!0,depthWrite:!1,side:c.DoubleSide,blending:q}),[q]),E=r.useMemo(()=>new c.Vector3,[]),d=r.useMemo(()=>new c.Vector3,[]),Fe=r.useMemo(()=>new c.Vector3,[]),We=r.useMemo(()=>new c.Vector3(0,1,0),[]),He=r.useMemo(()=>new c.Vector3,[]);return je.useFrame((de,Ie)=>{if(!te.current)return;const s=_e.current;if(s.elapsed+=Ie,!W){s.count=0,s.head=0,s.lastPos.set(1/0,1/0,1/0),ae.setDrawRange(0,0);return}const h=e.current;if(!h)return;if(s.lastPos.distanceTo(h)>=_){const M=(s.head+s.count)%p;s.positions[M*3]=h.x,s.positions[M*3+1]=h.y,s.positions[M*3+2]=h.z,s.times[M]=s.elapsed,s.count<p?s.count++:s.head=(s.head+1)%p,s.lastPos.copy(h)}for(;s.count>0;){const M=s.head;if(s.elapsed-s.times[M]>u)s.head=(s.head+1)%p,s.count--;else break}if(s.count<2){ae.setDrawRange(0,0);return}const j=Re.array,y=Pe.array;let b=null;const $=s.count;for(let M=0;M<$;M++){const U=(s.head+M)%p,ie=s.positions[U*3],me=s.positions[U*3+1],he=s.positions[U*3+2],se=1-(s.elapsed-s.times[U])/u;if(M<$-1){const Ce=(s.head+M+1)%p;E.set(s.positions[Ce*3]-ie,s.positions[Ce*3+1]-me,s.positions[Ce*3+2]-he);const a=E.length();a>1e-6?E.divideScalar(a):b&&E.copy(b)}else b&&E.copy(b);Fe.set(J.position.x-ie,J.position.y-me,J.position.z-he),d.crossVectors(E,Fe);let re=d.length();re<1e-6&&(He.crossVectors(E,We),d.copy(He),re=d.length(),re<1e-6&&(d.set(1,0,0),re=1)),d.divideScalar(re);const X=f*se,Be=Z*se*se,R=M*2;j[R*3]=ie+d.x*X,j[R*3+1]=me+d.y*X,j[R*3+2]=he+d.z*X,j[(R+1)*3]=ie-d.x*X,j[(R+1)*3+1]=me-d.y*X,j[(R+1)*3+2]=he-d.z*X,y[R*4]=A[0],y[R*4+1]=A[1],y[R*4+2]=A[2],y[R*4+3]=Be,y[(R+1)*4]=A[0],y[(R+1)*4+1]=A[1],y[(R+1)*4+2]=A[2],y[(R+1)*4+3]=Be,b=b||new c.Vector3,b.copy(E)}const ne=($-1)*6;ae.setDrawRange(0,ne),Re.needsUpdate=!0,Pe.needsUpdate=!0}),Oe.jsx("mesh",{ref:te,geometry:ae,material:Te,frustumCulled:!1,renderOrder:101,visible:W})}Object.defineProperty(exports,"fetchPreset",{enumerable:!0,get:()=>Xe.fetchPreset});exports.HZParticlesFX=It;exports.HZTrailRibbon=Ot;exports.computeParticleOpacity=St;exports.computeParticleSize=xt;exports.particleHash=ke;exports.scalePreset=gt;
|