hz-particles 1.0.14 → 1.2.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.
@@ -12,7 +12,8 @@ export interface ParticleSystemConfig {
12
12
  lifetime?: number;
13
13
  emissionRate?: number;
14
14
  emissionDuration?: number;
15
- emissionShape?: 'cube' | 'sphere' | 'square' | 'circle' | 'cylinder' | 'point';
15
+ emissionDurationInfinite?: boolean;
16
+ emissionShape?: 'cube' | 'sphere' | 'square' | 'circle' | 'cylinder' | 'plain' | 'cone' | 'torus' | 'line' | 'hemisphere' | 'disc' | 'annulus' | 'capsule' | 'arc' | 'spiral' | 'frustum' | 'cubeSurface' | 'sphereSurface' | 'boxFrame' | 'polygon' | 'rectangle' | 'point';
16
17
  particleSize?: number;
17
18
  particleSpeed?: number;
18
19
  particleColor?: [number, number, number];
@@ -24,6 +25,7 @@ export interface ParticleSystemConfig {
24
25
  randomColors?: number[][];
25
26
  textureEnabled?: boolean;
26
27
  textureType?: 'image' | 'glb';
28
+ textureImageData?: string;
27
29
  glbModelEnabled?: boolean;
28
30
  glbFileName?: string | null;
29
31
  glbAnimated?: boolean;
@@ -33,7 +35,9 @@ export interface ParticleSystemConfig {
33
35
  useGlbTexture?: boolean;
34
36
  glbHasTexture?: boolean;
35
37
  particleShape?: 'square' | 'circle' | 'triangle' | 'diamond' | 'star' | 'hexagon' | 'ring' | 'heart' | 'cross' | 'spark' | 'leaf' | 'capsule' | 'crescent' | 'line' | 'curved-line';
36
- particleShapeRotation?: number;
38
+ particleShapeRotationX?: number;
39
+ particleShapeRotationY?: number;
40
+ particleShapeRotationZ?: number;
37
41
  pulseEnabled?: boolean;
38
42
  pulseAmplitude?: number;
39
43
  pulseFrequency?: number;
@@ -47,19 +51,18 @@ export interface ParticleSystemConfig {
47
51
  attractorEnabled?: boolean;
48
52
  attractorStrength?: number;
49
53
  attractorPosition?: [number, number, number];
50
- shapeRotationX?: number;
51
- shapeRotationY?: number;
52
- shapeRotationZ?: number;
53
- shapeTranslationX?: number;
54
- shapeTranslationY?: number;
55
- shapeTranslationZ?: number;
54
+ emissionRotationX?: number;
55
+ emissionRotationY?: number;
56
+ emissionRotationZ?: number;
57
+ emissionPositionX?: number;
58
+ emissionPositionY?: number;
59
+ emissionPositionZ?: number;
56
60
  overrideXVelocity?: boolean;
57
61
  overrideYVelocity?: boolean;
58
62
  overrideZVelocity?: boolean;
59
63
  xVelocity?: number;
60
64
  yVelocity?: number;
61
65
  zVelocity?: number;
62
- cubeLength?: number;
63
66
  outerLength?: number;
64
67
  innerLength?: number;
65
68
  outerRadius?: number;
@@ -68,6 +71,8 @@ export interface ParticleSystemConfig {
68
71
  squareInnerSize?: number;
69
72
  circleOuterRadius?: number;
70
73
  circleInnerRadius?: number;
74
+ rectangleWidth?: number;
75
+ rectangleHeight?: number;
71
76
  circleVelocityDirection?: string;
72
77
  cylinderOuterRadius?: number;
73
78
  cylinderInnerRadius?: number;
@@ -81,6 +86,12 @@ export interface ParticleSystemConfig {
81
86
  aspectRatio?: number;
82
87
  velocityStretchEnabled?: boolean;
83
88
  velocityStretchFactor?: number;
89
+ blendMode?: 'normal' | 'additive';
90
+ noiseDistortEnabled?: boolean;
91
+ noiseTilingX?: number;
92
+ noiseTilingY?: number;
93
+ noiseSpeed?: number;
94
+ noiseAmplitude?: number;
84
95
  randomSize?: boolean;
85
96
  minSize?: number;
86
97
  maxSize?: number;
@@ -97,6 +108,7 @@ export interface ParticleSystemConfig {
97
108
  emissionTrailMinDistance?: number;
98
109
  emissionTrailMaxPoints?: number;
99
110
  emissionTrailSegments?: number;
111
+ emissionTrailMode?: 'ribbon' | 'particles';
100
112
  emissionTrailShape?: 'straight' | 'zigzag' | 'sine' | 'spiral';
101
113
  emissionTrailShapeAmplitude?: number;
102
114
  emissionTrailShapeFrequency?: number;
@@ -106,7 +118,9 @@ export interface ParticleSystemConfig {
106
118
  followSystemId?: number | null;
107
119
  hidden?: boolean;
108
120
  oneOnlyMode?: boolean;
121
+ script?: string;
109
122
  glbModelData?: string;
123
+ displayUnit?: 'm' | 'cm';
110
124
  onAppearanceChange?: ((config: ParticleSystemConfig) => void) | null;
111
125
  onColorChange?: ((config: ParticleSystemConfig) => void) | null;
112
126
  onSizeChange?: ((config: ParticleSystemConfig) => void) | null;
@@ -186,6 +200,8 @@ export interface BindGroupLayouts {
186
200
 
187
201
  export interface RenderPipelines {
188
202
  particlePipeline: GPURenderPipeline;
203
+ particleAdditivePipeline: GPURenderPipeline;
204
+ particleDepthWritePipeline: GPURenderPipeline;
189
205
  blurPipeline: GPURenderPipeline;
190
206
  compositePipeline: GPURenderPipeline;
191
207
  directRenderPipeline: GPURenderPipeline;
@@ -243,6 +259,8 @@ export class ParticleSystem {
243
259
  activeParticles: number;
244
260
  emitting: boolean;
245
261
  currentEmissionTime: number;
262
+ /** true after destroy() — GPU buffers are gone, all mutating methods become no-ops. */
263
+ destroyed: boolean;
246
264
  particleData: Float32Array;
247
265
  particleVelocities: Float32Array;
248
266
  instanceBuffer: GPUBuffer;
@@ -290,6 +308,7 @@ export class ParticleSystem {
290
308
  outParticleData: Float32Array,
291
309
  outVelocities: Float32Array
292
310
  ): Promise<PhysicsReadbackResult>;
311
+ setScript(source: string | null): void;
293
312
  destroy(): void;
294
313
  respawnParticle(oldIndex: number, newIndex: number): void;
295
314
  updateBuffers(): void;
@@ -307,6 +326,9 @@ export class ParticleSystemManager {
307
326
  activeSystemIndex: number;
308
327
  systemCounter: number;
309
328
  onSystemCreated: ((systemId: number, config: ParticleSystemConfig) => void) | null;
329
+ /** false while replaceSystems()/addSystems() rebuild GPU resources — hosts
330
+ * driving their own render loop must skip frames while !ready. */
331
+ ready: boolean;
310
332
 
311
333
  createParticleSystem(config?: ParticleSystemConfig): number;
312
334
  getSystemById(id: number): SystemEntry | null;
@@ -320,7 +342,8 @@ export class ParticleSystemManager {
320
342
  getSystemsList(): SystemInfo[];
321
343
  duplicateActiveSystem(): number;
322
344
  replaceSystems(sceneData: SceneData): Promise<boolean>;
323
- addSystems(sceneData: SceneData, positionOffset?: [number, number, number]): Promise<boolean>;
345
+ /** Returns the ids of the systems it created (race-proof handle), or false on error. */
346
+ addSystems(sceneData: SceneData, positionOffset?: [number, number, number]): Promise<number[] | false>;
324
347
  }
325
348
 
326
349
  export class ParticleEmitter {
@@ -387,6 +410,13 @@ export class ParticlePhysics {
387
410
  instanceBuffer: GPUBuffer,
388
411
  velocityBuffer: GPUBuffer
389
412
  ): Promise<PhysicsReadbackResult>;
413
+ readbackForScript(
414
+ activeParticles: number,
415
+ outParticleData: Float32Array,
416
+ outVelocities: Float32Array,
417
+ instanceBuffer: GPUBuffer,
418
+ velocityBuffer: GPUBuffer
419
+ ): Promise<PhysicsReadbackResult>;
390
420
  destroy(): void;
391
421
  setDamping(dampingValue: number): void;
392
422
  setGravity(gravityValue: number): void;
@@ -483,6 +513,23 @@ export class Objects3DManager {
483
513
  ): void;
484
514
  }
485
515
 
516
+ export class ParticleScript {
517
+ constructor(source: string, config: ParticleSystemConfig, maxParticles: number);
518
+ compile(source: string): void;
519
+ execute(
520
+ deltaTime: number,
521
+ time: number,
522
+ frame: number,
523
+ totalFrames: number,
524
+ particleData: Float32Array,
525
+ velocityData: Float32Array,
526
+ count: number
527
+ ): boolean;
528
+ snapshotConfig(config: ParticleSystemConfig): void;
529
+ restoreConfig(config: ParticleSystemConfig): void;
530
+ destroy(): void;
531
+ }
532
+
486
533
  // ─── WebGPU Utilities ────────────────────────────────────────────────────────
487
534
 
488
535
  export function initWebGPU(canvas: HTMLCanvasElement): Promise<WebGPUContext>;
@@ -572,6 +619,127 @@ export function saveScene(particleSystemManager: ParticleSystemManager): void;
572
619
 
573
620
  export function loadScene(event: Event): Promise<SceneData>;
574
621
 
622
+ /** THE single source of truth for the serializable (non-binary) fields of a system config.
623
+ * saveScene, the exported standalone code, and editor undo snapshots all build their
624
+ * per-system object from this so the field set can never drift. Binary assets
625
+ * (textureImageData / glbModelData) are added by the caller. */
626
+ export function serializeSystemConfig(config: ParticleSystemConfig): Record<string, unknown>;
627
+
575
628
  // ─── Preset Fetching ─────────────────────────────────────────────────────────
576
629
 
577
630
  export function fetchPreset(url: string): Promise<SceneData>;
631
+
632
+ // ─── .hzfx — self-contained binary effect package ────────────────────────────
633
+
634
+ export interface PackHZFXOptions {
635
+ /** Transcode embedded textures to WebP (browser only — needs OffscreenCanvas). */
636
+ textureFormat?: 'keep' | 'webp';
637
+ /** WebP quality 0..1 (default 0.85). */
638
+ quality?: number;
639
+ }
640
+
641
+ /** Pack a SceneData into a self-contained .hzfx binary package (config + assets). */
642
+ export function packHZFX(sceneData: SceneData, opts?: PackHZFXOptions): Promise<ArrayBuffer>;
643
+
644
+ /** Unpack a .hzfx package back into a SceneData the engine consumes. */
645
+ export function unpackHZFX(arrayBuffer: ArrayBuffer): SceneData;
646
+
647
+ /** True if the buffer starts with the HZFX magic. */
648
+ export function isHZFX(arrayBuffer: ArrayBuffer): boolean;
649
+
650
+ // ─── Engine-faithful FX overlay/inline renderer ──────────────────────────────
651
+
652
+ export interface HzFxEmitter {
653
+ preset: SceneData;
654
+ position?: [number, number, number];
655
+ }
656
+
657
+ export interface HzFxOverlayOptions {
658
+ /** Inline mode only — return the host scene's depth texture (or a
659
+ * depth-only view) each frame to occlude particles behind host geometry. */
660
+ getSceneDepth?: () => GPUTexture | { view: GPUTextureView; multisampled?: boolean } | null;
661
+ /** Respawn all systems when everything is dead (preview-style looping). Default true. */
662
+ autoRespawn?: boolean;
663
+ /** When true (default), the effect's saved group `loop` flag overrides autoRespawn.
664
+ * Hosts owning their own respawn timeline (the editor) pass false. */
665
+ respectEffectLoop?: boolean;
666
+ /** Share an existing ParticleSystemManager instead of creating one — lets a host
667
+ * (e.g. the hz-editor) keep all its UI/config wiring while the overlay is the single
668
+ * render path. The overlay then owns updateAllSystems/GLB-update/trail/draw each render(). */
669
+ manager?: ParticleSystemManager;
670
+ }
671
+
672
+ export interface HzFxOverlay {
673
+ manager: ParticleSystemManager;
674
+ device: GPUDevice;
675
+ context: GPUCanvasContext;
676
+ /** Column-major 4×4 matrices (e.g. three camera.projectionMatrix.elements /
677
+ * camera.matrixWorldInverse.elements) + camera world position. */
678
+ setCamera(
679
+ projectionMatrix: Float32Array | number[],
680
+ viewMatrix: Float32Array | number[],
681
+ position?: [number, number, number] | { x: number; y: number; z: number },
682
+ ): void;
683
+ setCameraMVP(
684
+ mvp: Float32Array | number[],
685
+ position?: [number, number, number] | { x: number; y: number; z: number },
686
+ ): void;
687
+ setEmitters(emitters: HzFxEmitter[]): Promise<void>;
688
+ loadPreset(preset: SceneData, position?: [number, number, number]): Promise<void>;
689
+ /** Add a preset as a STATIC group at `position` (e.g. a map FX, a splash). Returns a
690
+ * handle to remove exactly these systems. Any number coexist in the same overlay —
691
+ * prefer this over reaching into `manager.addSystems` directly. */
692
+ addEmitter(preset: SceneData, position?: [number, number, number]): Promise<{ remove(): void }>;
693
+ /** Add a preset as a MOVING emitter (e.g. a ball trail); the trail echoes follow
694
+ * the recorded path. Two ways to drive its position:
695
+ * - PULL (recommended): pass `{ getPosition }` — the overlay reads it during its
696
+ * own render(), once/frame at the right time, so the host never pushes per-frame
697
+ * (no timing race vs the host loop). Return null to pause the emitter.
698
+ * - PUSH: call the returned setPosition() each frame (for simple hosts). */
699
+ addMovingEmitter(
700
+ preset: SceneData,
701
+ opts?: { getPosition?: () => [number, number, number] | { x: number; y: number; z: number } | null },
702
+ ): Promise<{
703
+ setPosition(p: [number, number, number] | { x: number; y: number; z: number }): void;
704
+ remove(): void;
705
+ }>;
706
+ /** Simulate + render one frame (deltaTime in seconds). Inline mode: call AFTER the host scene render. */
707
+ render(deltaTime: number): void;
708
+ resize(): void;
709
+ /** Drop cached per-system bind groups. Call after a replaceSystems/addSystems that may
710
+ * reuse a system id with a new system object (stale appearance/texture otherwise). */
711
+ clearCaches(): void;
712
+ /** Track existing manager systems (by id) as a path-history group so their trails use the
713
+ * same curved `hasPathHistory` path as moving emitters — history ONLY (the host keeps
714
+ * owning position/rotation/visibility/respawn). Used by the editor's simulate-move. */
715
+ trackHistoryGroup(
716
+ ids: Iterable<number>,
717
+ getPosition: () => [number, number, number] | { x: number; y: number; z: number } | null,
718
+ ): { remove(): void };
719
+ destroy(): void;
720
+ }
721
+
722
+ /**
723
+ * Engine-faithful FX rendering for host applications — the same render loop as
724
+ * the HZ Particles previews (library pipelines: shader shapes, per-system
725
+ * blending, GLB meshes, multi-pass bloom), driven by the host camera.
726
+ *
727
+ * Overlay mode: pass a canvas (own transparent WebGPU context, stack it above
728
+ * your scene). Inline mode: pass { device, context, canvas } from your own
729
+ * WebGPU renderer and call render() after your scene render each frame.
730
+ */
731
+ export function initHzFxOverlay(
732
+ target: HTMLCanvasElement | { device: GPUDevice; context: GPUCanvasContext; canvas?: HTMLCanvasElement },
733
+ options?: HzFxOverlayOptions,
734
+ ): Promise<HzFxOverlay>;
735
+
736
+ /**
737
+ * Build a `getSceneDepth` callback (for initHzFxOverlay inline mode) from a three.js
738
+ * WebGPURenderer, so the overlay occludes particles behind host geometry. Duck-typed —
739
+ * pass the renderer; no three import. Handles tone-mapping/sRGB internal framebuffers and
740
+ * returns null (no occlusion, FX stay visible) when the scene depth isn't reachable
741
+ * (e.g. a PostProcessing pass owns the output). Pass the result as `options.getSceneDepth`.
742
+ */
743
+ export function makeThreeSceneDepth(
744
+ gl: unknown,
745
+ ): () => (GPUTexture | { view: GPUTextureView; multisampled?: boolean } | null);