core-vfx 0.0.3 → 0.0.4

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
@@ -27,16 +27,16 @@ npm install three @react-three/fiber react
27
27
  ## Quick Start
28
28
 
29
29
  ```tsx
30
- import { Canvas } from '@react-three/fiber';
31
- import { VFXParticles, Appearance, EmitterShape } from 'r3f-vfx';
32
- import * as THREE from 'three/webgpu';
30
+ import { Canvas } from '@react-three/fiber'
31
+ import { VFXParticles, Appearance, EmitterShape } from 'r3f-vfx'
32
+ import * as THREE from 'three/webgpu'
33
33
 
34
34
  function App() {
35
35
  return (
36
36
  <Canvas>
37
37
  <VFXParticles debug />
38
38
  </Canvas>
39
- );
39
+ )
40
40
  }
41
41
  ```
42
42
 
@@ -114,8 +114,8 @@ The main particle system component.
114
114
 
115
115
  ```ts
116
116
  interface StretchConfig {
117
- factor: number; // Stretch multiplier
118
- maxStretch: number; // Maximum stretch amount
117
+ factor: number // Stretch multiplier
118
+ maxStretch: number // Maximum stretch amount
119
119
  }
120
120
  ```
121
121
 
@@ -127,9 +127,9 @@ interface StretchConfig {
127
127
 
128
128
  ```ts
129
129
  interface TurbulenceConfig {
130
- intensity: number; // Turbulence strength
131
- frequency: number; // Noise scale
132
- speed: number; // Animation speed
130
+ intensity: number // Turbulence strength
131
+ frequency: number // Noise scale
132
+ speed: number // Animation speed
133
133
  }
134
134
  ```
135
135
 
@@ -142,11 +142,11 @@ interface TurbulenceConfig {
142
142
 
143
143
  ```ts
144
144
  interface AttractorConfig {
145
- position: [x, y, z];
146
- strength: number; // Positive = attract, negative = repel
147
- radius?: number; // 0 = infinite range
148
- type?: 'point' | 'vortex';
149
- axis?: [x, y, z]; // Vortex rotation axis
145
+ position: [x, y, z]
146
+ strength: number // Positive = attract, negative = repel
147
+ radius?: number // 0 = infinite range
148
+ type?: 'point' | 'vortex'
149
+ axis?: [x, y, z] // Vortex rotation axis
150
150
  }
151
151
  ```
152
152
 
@@ -158,11 +158,11 @@ interface AttractorConfig {
158
158
 
159
159
  ```ts
160
160
  interface CollisionConfig {
161
- plane: { y: number }; // Plane Y position
162
- bounce?: number; // Bounce factor (0-1)
163
- friction?: number; // Horizontal friction
164
- die?: boolean; // Kill on collision
165
- sizeBasedGravity?: number; // Gravity multiplier by size
161
+ plane: { y: number } // Plane Y position
162
+ bounce?: number // Bounce factor (0-1)
163
+ friction?: number // Horizontal friction
164
+ die?: boolean // Kill on collision
165
+ sizeBasedGravity?: number // Gravity multiplier by size
166
166
  }
167
167
  ```
168
168
 
@@ -180,10 +180,10 @@ All curves use Bezier spline format:
180
180
  ```ts
181
181
  interface CurveData {
182
182
  points: Array<{
183
- pos: [x, y]; // Position (x: 0-1 progress, y: value)
184
- handleIn?: [x, y]; // Bezier handle in (offset)
185
- handleOut?: [x, y]; // Bezier handle out (offset)
186
- }>;
183
+ pos: [x, y] // Position (x: 0-1 progress, y: value)
184
+ handleIn?: [x, y] // Bezier handle in (offset)
185
+ handleOut?: [x, y] // Bezier handle out (offset)
186
+ }>
187
187
  }
188
188
  ```
189
189
 
@@ -205,21 +205,21 @@ interface CurveData {
205
205
  | `alphaTestNode` | `NodeFunction` | Alpha test/discard |
206
206
 
207
207
  ```ts
208
- type NodeFunction = (data: ParticleData, defaultColor?: Node) => Node;
208
+ type NodeFunction = (data: ParticleData, defaultColor?: Node) => Node
209
209
 
210
210
  interface ParticleData {
211
- progress: Node; // 0 → 1 over lifetime
212
- lifetime: Node; // 1 → 0 over lifetime
213
- position: Node; // vec3 world position
214
- velocity: Node; // vec3 velocity
215
- size: Node; // float size
216
- rotation: Node; // vec3 rotation
217
- colorStart: Node; // vec3 start color
218
- colorEnd: Node; // vec3 end color
219
- color: Node; // vec3 interpolated color
220
- intensifiedColor: Node; // color × intensity
221
- shapeMask: Node; // float alpha mask
222
- index: Node; // particle index
211
+ progress: Node // 0 → 1 over lifetime
212
+ lifetime: Node // 1 → 0 over lifetime
213
+ position: Node // vec3 world position
214
+ velocity: Node // vec3 velocity
215
+ size: Node // float size
216
+ rotation: Node // vec3 rotation
217
+ colorStart: Node // vec3 start color
218
+ colorEnd: Node // vec3 end color
219
+ color: Node // vec3 interpolated color
220
+ intensifiedColor: Node // color × intensity
221
+ shapeMask: Node // float alpha mask
222
+ index: Node // particle index
223
223
  }
224
224
  ```
225
225
 
@@ -232,8 +232,8 @@ interface ParticleData {
232
232
 
233
233
  ```ts
234
234
  interface FlipbookConfig {
235
- rows: number;
236
- columns: number;
235
+ rows: number
236
+ columns: number
237
237
  }
238
238
  ```
239
239
 
@@ -276,13 +276,13 @@ Decoupled emitter component that links to a VFXParticles system.
276
276
 
277
277
  ```ts
278
278
  interface VFXEmitterAPI {
279
- emit(): boolean; // Emit at current position
280
- burst(count?: number): boolean; // Burst emit
281
- start(): void; // Start auto-emission
282
- stop(): void; // Stop auto-emission
283
- isEmitting: boolean; // Current state
284
- getParticleSystem(): ParticleAPI;
285
- group: THREE.Group; // The group element
279
+ emit(): boolean // Emit at current position
280
+ burst(count?: number): boolean // Burst emit
281
+ start(): void // Start auto-emission
282
+ stop(): void // Stop auto-emission
283
+ isEmitting: boolean // Current state
284
+ getParticleSystem(): ParticleAPI
285
+ group: THREE.Group // The group element
286
286
  }
287
287
  ```
288
288
 
@@ -292,13 +292,13 @@ Programmatic emitter control.
292
292
 
293
293
  ```tsx
294
294
  function MyComponent() {
295
- const { emit, burst, start, stop } = useVFXEmitter('sparks');
295
+ const { emit, burst, start, stop } = useVFXEmitter('sparks')
296
296
 
297
297
  const handleClick = () => {
298
- burst([0, 1, 0], 100, { colorStart: ['#ff0000'] });
299
- };
298
+ burst([0, 1, 0], 100, { colorStart: ['#ff0000'] })
299
+ }
300
300
 
301
- return <mesh onClick={handleClick}>...</mesh>;
301
+ return <mesh onClick={handleClick}>...</mesh>
302
302
  }
303
303
  ```
304
304
 
@@ -310,18 +310,18 @@ interface UseVFXEmitterResult {
310
310
  position?: [x, y, z],
311
311
  count?: number,
312
312
  overrides?: SpawnOverrides
313
- ): boolean;
313
+ ): boolean
314
314
  burst(
315
315
  position?: [x, y, z],
316
316
  count?: number,
317
317
  overrides?: SpawnOverrides
318
- ): boolean;
319
- start(): boolean;
320
- stop(): boolean;
321
- clear(): boolean;
322
- isEmitting(): boolean;
323
- getUniforms(): Record<string, { value: unknown }>;
324
- getParticles(): ParticleAPI;
318
+ ): boolean
319
+ start(): boolean
320
+ stop(): boolean
321
+ clear(): boolean
322
+ isEmitting(): boolean
323
+ getUniforms(): Record<string, { value: unknown }>
324
+ getParticles(): ParticleAPI
325
325
  }
326
326
  ```
327
327
 
@@ -330,17 +330,17 @@ interface UseVFXEmitterResult {
330
330
  Zustand store for managing particle systems.
331
331
 
332
332
  ```ts
333
- const store = useVFXStore();
333
+ const store = useVFXStore()
334
334
 
335
335
  // Access registered particle systems
336
- const sparks = store.getParticles('sparks');
337
- sparks?.spawn(0, 0, 0, 50);
336
+ const sparks = store.getParticles('sparks')
337
+ sparks?.spawn(0, 0, 0, 50)
338
338
 
339
339
  // Store methods
340
- store.emit('sparks', { x: 0, y: 0, z: 0, count: 20 });
341
- store.start('sparks');
342
- store.stop('sparks');
343
- store.clear('sparks');
340
+ store.emit('sparks', { x: 0, y: 0, z: 0, count: 20 })
341
+ store.start('sparks')
342
+ store.stop('sparks')
343
+ store.clear('sparks')
344
344
  ```
345
345
 
346
346
  ## Examples
@@ -388,9 +388,9 @@ store.clear('sparks');
388
388
  ### 3D Geometry Particles
389
389
 
390
390
  ```tsx
391
- import { BoxGeometry } from 'three/webgpu';
391
+ import { BoxGeometry } from 'three/webgpu'
392
392
 
393
- <VFXParticles
393
+ ;<VFXParticles
394
394
  geometry={new BoxGeometry(1, 1, 1)}
395
395
  maxParticles={500}
396
396
  size={[0.1, 0.2]}
@@ -404,7 +404,7 @@ import { BoxGeometry } from 'three/webgpu';
404
404
  ]}
405
405
  shadow={true}
406
406
  lighting={Lighting.STANDARD}
407
- />;
407
+ />
408
408
  ```
409
409
 
410
410
  ### Turbulent Smoke
@@ -464,7 +464,7 @@ import type {
464
464
  TurbulenceConfig,
465
465
  CollisionConfig,
466
466
  AttractorConfig,
467
- } from 'r3f-vfx';
467
+ } from 'r3f-vfx'
468
468
  ```
469
469
 
470
470
  ## License
package/dist/index.d.ts CHANGED
@@ -250,6 +250,8 @@ declare const DEFAULT_LINEAR_CURVE: {
250
250
  handleOut?: undefined;
251
251
  })[];
252
252
  };
253
+ declare const createDefaultCurveTexture: () => THREE.DataTexture;
254
+ declare const loadCurveTextureFromPath: (path: string, existingTexture?: THREE.DataTexture) => Promise<THREE.DataTexture>;
253
255
 
254
256
  type ParticleStorageArrays = {
255
257
  positions: StorageBufferNode;
@@ -257,11 +259,18 @@ type ParticleStorageArrays = {
257
259
  lifetimes: StorageBufferNode;
258
260
  fadeRates: StorageBufferNode;
259
261
  particleSizes: StorageBufferNode;
260
- particleRotations: StorageBufferNode;
261
- particleColorStarts: StorageBufferNode;
262
- particleColorEnds: StorageBufferNode;
262
+ particleRotations: StorageBufferNode | null;
263
+ particleColorStarts: StorageBufferNode | null;
264
+ particleColorEnds: StorageBufferNode | null;
263
265
  };
264
266
  type ParticleUniforms = Record<string, Node>;
267
+ type ShaderFeatures = {
268
+ turbulence: boolean;
269
+ attractors: boolean;
270
+ collision: boolean;
271
+ rotation: boolean;
272
+ perParticleColor: boolean;
273
+ };
265
274
  type MaterialOptions = {
266
275
  alphaMap: THREE.Texture | null;
267
276
  flipbook: {
@@ -299,9 +308,9 @@ declare const createSpawnCompute: (storage: ParticleStorageArrays, uniforms: Par
299
308
 
300
309
  /**
301
310
  * Creates the update compute shader that simulates particle physics each frame.
302
- * Handles gravity, turbulence, attractors, collision, rotation, and lifetime.
311
+ * Features can be disabled to generate a simpler/faster shader.
303
312
  */
304
- declare const createUpdateCompute: (storage: ParticleStorageArrays, uniforms: ParticleUniforms, curveTexture: THREE.DataTexture, maxParticles: number) => THREE.ComputeNode;
313
+ declare const createUpdateCompute: (storage: ParticleStorageArrays, uniforms: ParticleUniforms, curveTexture: THREE.DataTexture, maxParticles: number, features?: Partial<ShaderFeatures>) => THREE.ComputeNode;
305
314
 
306
315
  /**
307
316
  * Creates the particle material (either SpriteNodeMaterial or MeshNodeMaterial).
@@ -309,4 +318,4 @@ declare const createUpdateCompute: (storage: ParticleStorageArrays, uniforms: Pa
309
318
  */
310
319
  declare const createParticleMaterial: (storage: ParticleStorageArrays, uniforms: ParticleUniforms, curveTexture: THREE.DataTexture, options: MaterialOptions) => THREE.SpriteNodeMaterial | THREE.MeshBasicNodeMaterial | THREE.MeshStandardNodeMaterial | THREE.MeshPhysicalNodeMaterial;
311
320
 
312
- export { Appearance, type AttractorConfig, AttractorType, type BaseParticleProps, Blending, CURVE_RESOLUTION, type CollisionConfig, type CoreState, type CurveData, type CurvePoint, DEFAULT_LINEAR_CURVE, Easing, EmitterShape, type FlipbookConfig, type FrictionConfig, Lighting, MAX_ATTRACTORS, type MaterialOptions, type ParticleData, type ParticleStorageArrays, type ParticleUniforms, type Rotation3DInput, type StretchConfig, type TurbulenceConfig, axisToNumber, bakeCurveToArray, coreStore, createCombinedCurveTexture, createInitCompute, createParticleMaterial, createSpawnCompute, createUpdateCompute, easingToType, evaluateBezierSegment, hexToRgb, lifetimeToFadeRate, sampleCurveAtX, selectColor, toRange, toRotation3D };
321
+ export { Appearance, type AttractorConfig, AttractorType, type BaseParticleProps, Blending, CURVE_RESOLUTION, type CollisionConfig, type CoreState, type CurveData, type CurvePoint, DEFAULT_LINEAR_CURVE, Easing, EmitterShape, type FlipbookConfig, type FrictionConfig, Lighting, MAX_ATTRACTORS, type MaterialOptions, type ParticleData, type ParticleStorageArrays, type ParticleUniforms, type Rotation3DInput, type ShaderFeatures, type StretchConfig, type TurbulenceConfig, axisToNumber, bakeCurveToArray, coreStore, createCombinedCurveTexture, createDefaultCurveTexture, createInitCompute, createParticleMaterial, createSpawnCompute, createUpdateCompute, easingToType, evaluateBezierSegment, hexToRgb, lifetimeToFadeRate, loadCurveTextureFromPath, sampleCurveAtX, selectColor, toRange, toRotation3D };
package/dist/index.js CHANGED
@@ -301,8 +301,14 @@ var lifetimeToFadeRate = (seconds) => 1 / seconds;
301
301
  import * as THREE2 from "three/webgpu";
302
302
  var evaluateBezierSegment = (t, p0, p1, h0Out, h1In) => {
303
303
  const cp0 = p0;
304
- const cp1 = [p0[0] + ((h0Out == null ? void 0 : h0Out[0]) || 0), p0[1] + ((h0Out == null ? void 0 : h0Out[1]) || 0)];
305
- const cp2 = [p1[0] + ((h1In == null ? void 0 : h1In[0]) || 0), p1[1] + ((h1In == null ? void 0 : h1In[1]) || 0)];
304
+ const cp1 = [
305
+ p0[0] + ((h0Out == null ? void 0 : h0Out[0]) || 0),
306
+ p0[1] + ((h0Out == null ? void 0 : h0Out[1]) || 0)
307
+ ];
308
+ const cp2 = [
309
+ p1[0] + ((h1In == null ? void 0 : h1In[0]) || 0),
310
+ p1[1] + ((h1In == null ? void 0 : h1In[1]) || 0)
311
+ ];
306
312
  const cp3 = p1;
307
313
  const mt = 1 - t;
308
314
  const mt2 = mt * mt;
@@ -403,10 +409,68 @@ var createCombinedCurveTexture = (sizeCurve, opacityCurve, velocityCurve, rotati
403
409
  };
404
410
  var DEFAULT_LINEAR_CURVE = {
405
411
  points: [
406
- { pos: [0, 1], handleOut: [0.33, 0] },
407
- { pos: [1, 0], handleIn: [-0.33, 0] }
412
+ {
413
+ pos: [0, 1],
414
+ handleOut: [0.33, 0]
415
+ },
416
+ {
417
+ pos: [1, 0],
418
+ handleIn: [-0.33, 0]
419
+ }
408
420
  ]
409
421
  };
422
+ var createDefaultCurveTexture = () => {
423
+ const rgba = new Float32Array(CURVE_RESOLUTION * 4);
424
+ for (let i = 0; i < CURVE_RESOLUTION; i++) {
425
+ const value = 1 - i / (CURVE_RESOLUTION - 1);
426
+ rgba[i * 4] = value;
427
+ rgba[i * 4 + 1] = value;
428
+ rgba[i * 4 + 2] = value;
429
+ rgba[i * 4 + 3] = value;
430
+ }
431
+ const tex = new THREE2.DataTexture(
432
+ rgba,
433
+ CURVE_RESOLUTION,
434
+ 1,
435
+ THREE2.RGBAFormat,
436
+ THREE2.FloatType
437
+ );
438
+ tex.minFilter = THREE2.LinearFilter;
439
+ tex.magFilter = THREE2.LinearFilter;
440
+ tex.wrapS = THREE2.ClampToEdgeWrapping;
441
+ tex.needsUpdate = true;
442
+ return tex;
443
+ };
444
+ var loadCurveTextureFromPath = async (path, existingTexture) => {
445
+ const response = await fetch(path);
446
+ if (!response.ok) {
447
+ throw new Error(`Failed to load curve texture: HTTP ${response.status}`);
448
+ }
449
+ const buffer = await response.arrayBuffer();
450
+ const rgba = new Float32Array(buffer);
451
+ if (rgba.length !== CURVE_RESOLUTION * 4) {
452
+ throw new Error(
453
+ `Invalid curve texture size: expected ${CURVE_RESOLUTION * 4}, got ${rgba.length}`
454
+ );
455
+ }
456
+ if (existingTexture && existingTexture.image.data) {
457
+ existingTexture.image.data.set(rgba);
458
+ existingTexture.needsUpdate = true;
459
+ return existingTexture;
460
+ }
461
+ const tex = new THREE2.DataTexture(
462
+ rgba,
463
+ CURVE_RESOLUTION,
464
+ 1,
465
+ THREE2.RGBAFormat,
466
+ THREE2.FloatType
467
+ );
468
+ tex.minFilter = THREE2.LinearFilter;
469
+ tex.magFilter = THREE2.LinearFilter;
470
+ tex.wrapS = THREE2.ClampToEdgeWrapping;
471
+ tex.needsUpdate = true;
472
+ return tex;
473
+ };
410
474
 
411
475
  // src/shaders/helpers.ts
412
476
  var selectColor = (idx, c0, c1, c2, c3, c4, c5, c6, c7) => {
@@ -432,22 +496,27 @@ var selectColor = (idx, c0, c1, c2, c3, c4, c5, c6, c7) => {
432
496
  import { Fn, float, vec3, instanceIndex } from "three/tsl";
433
497
  var createInitCompute = (storage, maxParticles) => {
434
498
  return Fn(() => {
499
+ var _a, _b, _c;
435
500
  const position = storage.positions.element(instanceIndex);
436
501
  const velocity = storage.velocities.element(instanceIndex);
437
502
  const lifetime = storage.lifetimes.element(instanceIndex);
438
503
  const fadeRate = storage.fadeRates.element(instanceIndex);
439
504
  const particleSize = storage.particleSizes.element(instanceIndex);
440
- const particleRotation = storage.particleRotations.element(instanceIndex);
441
- const colorStart = storage.particleColorStarts.element(instanceIndex);
442
- const colorEnd = storage.particleColorEnds.element(instanceIndex);
505
+ const particleRotation = (_a = storage.particleRotations) == null ? void 0 : _a.element(instanceIndex);
506
+ const colorStart = (_b = storage.particleColorStarts) == null ? void 0 : _b.element(instanceIndex);
507
+ const colorEnd = (_c = storage.particleColorEnds) == null ? void 0 : _c.element(instanceIndex);
443
508
  position.assign(vec3(0, -1e3, 0));
444
509
  velocity.assign(vec3(0, 0, 0));
445
510
  lifetime.assign(float(0));
446
511
  fadeRate.assign(float(0));
447
512
  particleSize.assign(float(0));
448
- particleRotation.assign(vec3(0, 0, 0));
449
- colorStart.assign(vec3(1, 1, 1));
450
- colorEnd.assign(vec3(1, 1, 1));
513
+ if (particleRotation) {
514
+ particleRotation.assign(vec3(0, 0, 0));
515
+ }
516
+ if (colorStart && colorEnd) {
517
+ colorStart.assign(vec3(1, 1, 1));
518
+ colorEnd.assign(vec3(1, 1, 1));
519
+ }
451
520
  })().compute(maxParticles);
452
521
  };
453
522
 
@@ -478,14 +547,15 @@ var createSpawnCompute = (storage, uniforms, maxParticles) => {
478
547
  idx.greaterThanEqual(startIdx).or(idx.lessThan(endIdx))
479
548
  );
480
549
  If(inRange, () => {
550
+ var _a, _b, _c;
481
551
  const position = storage.positions.element(instanceIndex2);
482
552
  const velocity = storage.velocities.element(instanceIndex2);
483
553
  const lifetime = storage.lifetimes.element(instanceIndex2);
484
554
  const fadeRate = storage.fadeRates.element(instanceIndex2);
485
555
  const particleSize = storage.particleSizes.element(instanceIndex2);
486
- const particleRotation = storage.particleRotations.element(instanceIndex2);
487
- const pColorStart = storage.particleColorStarts.element(instanceIndex2);
488
- const pColorEnd = storage.particleColorEnds.element(instanceIndex2);
556
+ const particleRotation = (_a = storage.particleRotations) == null ? void 0 : _a.element(instanceIndex2);
557
+ const pColorStart = (_b = storage.particleColorStarts) == null ? void 0 : _b.element(instanceIndex2);
558
+ const pColorEnd = (_c = storage.particleColorEnds) == null ? void 0 : _c.element(instanceIndex2);
489
559
  const particleSeed = idx.add(seed);
490
560
  const randDirX = hash(particleSeed.add(333));
491
561
  const randDirY = hash(particleSeed.add(444));
@@ -571,9 +641,7 @@ var createSpawnCompute = (storage, uniforms, maxParticles) => {
571
641
  const coneLocalX = coneR.mul(cos(theta));
572
642
  const coneLocalY = coneH.mul(cos(coneAngle));
573
643
  const coneLocalZ = coneR.mul(sin(theta));
574
- const conePos = rotateToEmitDir(
575
- vec32(coneLocalX, coneLocalY, coneLocalZ)
576
- );
644
+ const conePos = rotateToEmitDir(vec32(coneLocalX, coneLocalY, coneLocalZ));
577
645
  const diskR = surfaceOnly.greaterThan(0.5).select(
578
646
  radiusOuter,
579
647
  mix(radiusInner, radiusOuter, sqrt(randRadius))
@@ -630,61 +698,61 @@ var createSpawnCompute = (storage, uniforms, maxParticles) => {
630
698
  const startPosLength = shapeOffset.length();
631
699
  const startPosDir = startPosLength.greaterThan(1e-3).select(shapeOffset.div(startPosLength), vec32(0, 0, 0));
632
700
  const dir = useStartPosAsDir.select(startPosDir, randomDir);
633
- const randomSpeed = mix(
634
- uniforms.speedMin,
635
- uniforms.speedMax,
636
- randSpeed
637
- );
701
+ const randomSpeed = mix(uniforms.speedMin, uniforms.speedMax, randSpeed);
638
702
  const normalVelocity = dir.mul(randomSpeed);
639
703
  velocity.assign(
640
704
  useAttractToCenter.select(attractVelocity, normalVelocity)
641
705
  );
642
706
  const randomSize = mix(uniforms.sizeMin, uniforms.sizeMax, randSize);
643
707
  particleSize.assign(randomSize);
644
- const rotX = mix(
645
- uniforms.rotationMinX,
646
- uniforms.rotationMaxX,
647
- randRotationX
648
- );
649
- const rotY = mix(
650
- uniforms.rotationMinY,
651
- uniforms.rotationMaxY,
652
- randRotationY
653
- );
654
- const rotZ = mix(
655
- uniforms.rotationMinZ,
656
- uniforms.rotationMaxZ,
657
- randRotationZ
658
- );
659
- particleRotation.assign(vec32(rotX, rotY, rotZ));
660
- const startColorIdx = floor(
661
- randColorStart.mul(uniforms.colorStartCount)
662
- );
663
- const selectedStartColor = selectColor(
664
- startColorIdx,
665
- uniforms.colorStart0,
666
- uniforms.colorStart1,
667
- uniforms.colorStart2,
668
- uniforms.colorStart3,
669
- uniforms.colorStart4,
670
- uniforms.colorStart5,
671
- uniforms.colorStart6,
672
- uniforms.colorStart7
673
- );
674
- pColorStart.assign(selectedStartColor);
675
- const endColorIdx = floor(randColorEnd.mul(uniforms.colorEndCount));
676
- const selectedEndColor = selectColor(
677
- endColorIdx,
678
- uniforms.colorEnd0,
679
- uniforms.colorEnd1,
680
- uniforms.colorEnd2,
681
- uniforms.colorEnd3,
682
- uniforms.colorEnd4,
683
- uniforms.colorEnd5,
684
- uniforms.colorEnd6,
685
- uniforms.colorEnd7
686
- );
687
- pColorEnd.assign(selectedEndColor);
708
+ if (particleRotation) {
709
+ const rotX = mix(
710
+ uniforms.rotationMinX,
711
+ uniforms.rotationMaxX,
712
+ randRotationX
713
+ );
714
+ const rotY = mix(
715
+ uniforms.rotationMinY,
716
+ uniforms.rotationMaxY,
717
+ randRotationY
718
+ );
719
+ const rotZ = mix(
720
+ uniforms.rotationMinZ,
721
+ uniforms.rotationMaxZ,
722
+ randRotationZ
723
+ );
724
+ particleRotation.assign(vec32(rotX, rotY, rotZ));
725
+ }
726
+ if (pColorStart && pColorEnd) {
727
+ const startColorIdx = floor(
728
+ randColorStart.mul(uniforms.colorStartCount)
729
+ );
730
+ const selectedStartColor = selectColor(
731
+ startColorIdx,
732
+ uniforms.colorStart0,
733
+ uniforms.colorStart1,
734
+ uniforms.colorStart2,
735
+ uniforms.colorStart3,
736
+ uniforms.colorStart4,
737
+ uniforms.colorStart5,
738
+ uniforms.colorStart6,
739
+ uniforms.colorStart7
740
+ );
741
+ pColorStart.assign(selectedStartColor);
742
+ const endColorIdx = floor(randColorEnd.mul(uniforms.colorEndCount));
743
+ const selectedEndColor = selectColor(
744
+ endColorIdx,
745
+ uniforms.colorEnd0,
746
+ uniforms.colorEnd1,
747
+ uniforms.colorEnd2,
748
+ uniforms.colorEnd3,
749
+ uniforms.colorEnd4,
750
+ uniforms.colorEnd5,
751
+ uniforms.colorEnd6,
752
+ uniforms.colorEnd7
753
+ );
754
+ pColorEnd.assign(selectedEndColor);
755
+ }
688
756
  lifetime.assign(float2(1));
689
757
  });
690
758
  })().compute(maxParticles);
@@ -703,13 +771,22 @@ import {
703
771
  instanceIndex as instanceIndex3,
704
772
  mx_noise_vec3
705
773
  } from "three/tsl";
706
- var createUpdateCompute = (storage, uniforms, curveTexture, maxParticles) => {
774
+ var DEFAULT_FEATURES = {
775
+ turbulence: true,
776
+ attractors: true,
777
+ collision: true,
778
+ rotation: true,
779
+ perParticleColor: true
780
+ };
781
+ var createUpdateCompute = (storage, uniforms, curveTexture, maxParticles, features = {}) => {
782
+ const f = __spreadValues(__spreadValues({}, DEFAULT_FEATURES), features);
707
783
  return Fn3(() => {
784
+ var _a;
708
785
  const position = storage.positions.element(instanceIndex3);
709
786
  const velocity = storage.velocities.element(instanceIndex3);
710
787
  const lifetime = storage.lifetimes.element(instanceIndex3);
711
788
  const fadeRate = storage.fadeRates.element(instanceIndex3);
712
- const particleRotation = storage.particleRotations.element(instanceIndex3);
789
+ const particleRotation = f.rotation ? (_a = storage.particleRotations) == null ? void 0 : _a.element(instanceIndex3) : null;
713
790
  const particleSize = storage.particleSizes.element(instanceIndex3);
714
791
  const dt = uniforms.deltaTime;
715
792
  If2(lifetime.greaterThan(0), () => {
@@ -753,139 +830,143 @@ var createUpdateCompute = (storage, uniforms, curveTexture, maxParticles) => {
753
830
  return float3(1).sub(currentIntensity.mul(0.9));
754
831
  })()
755
832
  );
756
- const turbIntensity = uniforms.turbulenceIntensity;
757
- const turbFreq = uniforms.turbulenceFrequency;
758
- const turbTime = uniforms.turbulenceTime;
759
- If2(turbIntensity.greaterThan(1e-3), () => {
760
- const noisePos = position.mul(turbFreq).add(vec33(turbTime, turbTime.mul(0.7), turbTime.mul(1.3)));
761
- const eps = float3(0.01);
762
- const nPosX = mx_noise_vec3(noisePos.add(vec33(eps, 0, 0)));
763
- const nNegX = mx_noise_vec3(noisePos.sub(vec33(eps, 0, 0)));
764
- const nPosY = mx_noise_vec3(noisePos.add(vec33(0, eps, 0)));
765
- const nNegY = mx_noise_vec3(noisePos.sub(vec33(0, eps, 0)));
766
- const nPosZ = mx_noise_vec3(noisePos.add(vec33(0, 0, eps)));
767
- const nNegZ = mx_noise_vec3(noisePos.sub(vec33(0, 0, eps)));
768
- const dFx_dy = nPosY.x.sub(nNegY.x).div(eps.mul(2));
769
- const dFx_dz = nPosZ.x.sub(nNegZ.x).div(eps.mul(2));
770
- const dFy_dx = nPosX.y.sub(nNegX.y).div(eps.mul(2));
771
- const dFy_dz = nPosZ.y.sub(nNegZ.y).div(eps.mul(2));
772
- const dFz_dx = nPosX.z.sub(nNegX.z).div(eps.mul(2));
773
- const dFz_dy = nPosY.z.sub(nNegY.z).div(eps.mul(2));
774
- const curlX = dFz_dy.sub(dFy_dz);
775
- const curlY = dFx_dz.sub(dFz_dx);
776
- const curlZ = dFy_dx.sub(dFx_dy);
777
- const curl = vec33(curlX, curlY, curlZ);
778
- velocity.addAssign(curl.mul(turbIntensity).mul(uniforms.deltaTime));
779
- });
780
- const attractorCount = uniforms.attractorCount;
781
- const applyAttractor = (aPos, aStrength, aRadius, aType, aAxis) => {
782
- If2(aStrength.abs().greaterThan(1e-3), () => {
783
- const toAttractor = aPos.sub(position);
784
- const dist = toAttractor.length();
785
- const safeDist = dist.max(0.01);
786
- const direction = toAttractor.div(safeDist);
787
- const falloff = aRadius.greaterThan(1e-3).select(
788
- float3(1).sub(dist.div(aRadius)).max(0),
789
- // Linear falloff within radius
790
- float3(1).div(safeDist.mul(safeDist).add(1))
791
- // Inverse square falloff (softened)
833
+ if (f.turbulence) {
834
+ const turbIntensity = uniforms.turbulenceIntensity;
835
+ const turbFreq = uniforms.turbulenceFrequency;
836
+ const turbTime = uniforms.turbulenceTime;
837
+ If2(turbIntensity.greaterThan(1e-3), () => {
838
+ const noisePos = position.mul(turbFreq).add(vec33(turbTime, turbTime.mul(0.7), turbTime.mul(1.3)));
839
+ const eps = float3(0.01);
840
+ const nPosX = mx_noise_vec3(noisePos.add(vec33(eps, 0, 0)));
841
+ const nNegX = mx_noise_vec3(noisePos.sub(vec33(eps, 0, 0)));
842
+ const nPosY = mx_noise_vec3(noisePos.add(vec33(0, eps, 0)));
843
+ const nNegY = mx_noise_vec3(noisePos.sub(vec33(0, eps, 0)));
844
+ const nPosZ = mx_noise_vec3(noisePos.add(vec33(0, 0, eps)));
845
+ const nNegZ = mx_noise_vec3(noisePos.sub(vec33(0, 0, eps)));
846
+ const dFx_dy = nPosY.x.sub(nNegY.x).div(eps.mul(2));
847
+ const dFx_dz = nPosZ.x.sub(nNegZ.x).div(eps.mul(2));
848
+ const dFy_dx = nPosX.y.sub(nNegX.y).div(eps.mul(2));
849
+ const dFy_dz = nPosZ.y.sub(nNegZ.y).div(eps.mul(2));
850
+ const dFz_dx = nPosX.z.sub(nNegX.z).div(eps.mul(2));
851
+ const dFz_dy = nPosY.z.sub(nNegY.z).div(eps.mul(2));
852
+ const curl = vec33(
853
+ dFz_dy.sub(dFy_dz),
854
+ dFx_dz.sub(dFz_dx),
855
+ dFy_dx.sub(dFx_dy)
856
+ );
857
+ velocity.addAssign(curl.mul(turbIntensity).mul(uniforms.deltaTime));
858
+ });
859
+ }
860
+ if (f.attractors) {
861
+ const attractorCount = uniforms.attractorCount;
862
+ const applyAttractor = (aPos, aStrength, aRadius, aType, aAxis) => {
863
+ If2(aStrength.abs().greaterThan(1e-3), () => {
864
+ const toAttractor = aPos.sub(position);
865
+ const dist = toAttractor.length();
866
+ const safeDist = dist.max(0.01);
867
+ const direction = toAttractor.div(safeDist);
868
+ const falloff = aRadius.greaterThan(1e-3).select(
869
+ float3(1).sub(dist.div(aRadius)).max(0),
870
+ float3(1).div(safeDist.mul(safeDist).add(1))
871
+ );
872
+ const force = aType.lessThan(0.5).select(
873
+ direction.mul(aStrength).mul(falloff),
874
+ (() => {
875
+ const tangent = vec33(
876
+ aAxis.y.mul(toAttractor.z).sub(aAxis.z.mul(toAttractor.y)),
877
+ aAxis.z.mul(toAttractor.x).sub(aAxis.x.mul(toAttractor.z)),
878
+ aAxis.x.mul(toAttractor.y).sub(aAxis.y.mul(toAttractor.x))
879
+ );
880
+ const tangentLen = tangent.length().max(1e-3);
881
+ return tangent.div(tangentLen).mul(aStrength).mul(falloff);
882
+ })()
883
+ );
884
+ velocity.addAssign(force.mul(uniforms.deltaTime));
885
+ });
886
+ };
887
+ If2(attractorCount.greaterThan(0), () => {
888
+ applyAttractor(
889
+ uniforms.attractor0Pos,
890
+ uniforms.attractor0Strength,
891
+ uniforms.attractor0Radius,
892
+ uniforms.attractor0Type,
893
+ uniforms.attractor0Axis
792
894
  );
793
- const force = aType.lessThan(0.5).select(
794
- // Point attractor: force along direction to attractor
795
- direction.mul(aStrength).mul(falloff),
796
- // Vortex: force perpendicular to both (toAttractor) and (axis)
797
- // cross(axis, toAttractor) gives tangent direction
798
- (() => {
799
- const tangent = vec33(
800
- aAxis.y.mul(toAttractor.z).sub(aAxis.z.mul(toAttractor.y)),
801
- aAxis.z.mul(toAttractor.x).sub(aAxis.x.mul(toAttractor.z)),
802
- aAxis.x.mul(toAttractor.y).sub(aAxis.y.mul(toAttractor.x))
803
- );
804
- const tangentLen = tangent.length().max(1e-3);
805
- return tangent.div(tangentLen).mul(aStrength).mul(falloff);
806
- })()
895
+ });
896
+ If2(attractorCount.greaterThan(1), () => {
897
+ applyAttractor(
898
+ uniforms.attractor1Pos,
899
+ uniforms.attractor1Strength,
900
+ uniforms.attractor1Radius,
901
+ uniforms.attractor1Type,
902
+ uniforms.attractor1Axis
807
903
  );
808
- velocity.addAssign(force.mul(uniforms.deltaTime));
809
904
  });
810
- };
811
- If2(attractorCount.greaterThan(0), () => {
812
- applyAttractor(
813
- uniforms.attractor0Pos,
814
- uniforms.attractor0Strength,
815
- uniforms.attractor0Radius,
816
- uniforms.attractor0Type,
817
- uniforms.attractor0Axis
905
+ If2(attractorCount.greaterThan(2), () => {
906
+ applyAttractor(
907
+ uniforms.attractor2Pos,
908
+ uniforms.attractor2Strength,
909
+ uniforms.attractor2Radius,
910
+ uniforms.attractor2Type,
911
+ uniforms.attractor2Axis
912
+ );
913
+ });
914
+ If2(attractorCount.greaterThan(3), () => {
915
+ applyAttractor(
916
+ uniforms.attractor3Pos,
917
+ uniforms.attractor3Strength,
918
+ uniforms.attractor3Radius,
919
+ uniforms.attractor3Type,
920
+ uniforms.attractor3Axis
921
+ );
922
+ });
923
+ }
924
+ position.addAssign(velocity.mul(dt).mul(speedScale));
925
+ if (f.collision) {
926
+ If2(uniforms.collisionEnabled.greaterThan(0.5), () => {
927
+ const planeY = uniforms.collisionPlaneY;
928
+ const bounce = uniforms.collisionBounce;
929
+ const friction = uniforms.collisionFriction;
930
+ const shouldDie = uniforms.collisionDie;
931
+ If2(position.y.lessThan(planeY), () => {
932
+ If2(shouldDie.greaterThan(0.5), () => {
933
+ lifetime.assign(float3(0));
934
+ position.y.assign(float3(-1e3));
935
+ }).Else(() => {
936
+ position.y.assign(planeY);
937
+ velocity.y.assign(velocity.y.abs().mul(bounce));
938
+ velocity.x.mulAssign(friction);
939
+ velocity.z.mulAssign(friction);
940
+ });
941
+ });
942
+ });
943
+ }
944
+ if (particleRotation) {
945
+ const idx = float3(instanceIndex3);
946
+ const rotSpeedX = mix2(
947
+ uniforms.rotationSpeedMinX,
948
+ uniforms.rotationSpeedMaxX,
949
+ hash2(idx.add(8888))
818
950
  );
819
- });
820
- If2(attractorCount.greaterThan(1), () => {
821
- applyAttractor(
822
- uniforms.attractor1Pos,
823
- uniforms.attractor1Strength,
824
- uniforms.attractor1Radius,
825
- uniforms.attractor1Type,
826
- uniforms.attractor1Axis
951
+ const rotSpeedY = mix2(
952
+ uniforms.rotationSpeedMinY,
953
+ uniforms.rotationSpeedMaxY,
954
+ hash2(idx.add(9999))
827
955
  );
828
- });
829
- If2(attractorCount.greaterThan(2), () => {
830
- applyAttractor(
831
- uniforms.attractor2Pos,
832
- uniforms.attractor2Strength,
833
- uniforms.attractor2Radius,
834
- uniforms.attractor2Type,
835
- uniforms.attractor2Axis
956
+ const rotSpeedZ = mix2(
957
+ uniforms.rotationSpeedMinZ,
958
+ uniforms.rotationSpeedMaxZ,
959
+ hash2(idx.add(10101))
836
960
  );
837
- });
838
- If2(attractorCount.greaterThan(3), () => {
839
- applyAttractor(
840
- uniforms.attractor3Pos,
841
- uniforms.attractor3Strength,
842
- uniforms.attractor3Radius,
843
- uniforms.attractor3Type,
844
- uniforms.attractor3Axis
961
+ const rotSpeedCurveSample = texture(
962
+ curveTexture,
963
+ vec2(progress, float3(0.5))
964
+ ).w;
965
+ const rotSpeedMultiplier = uniforms.rotationSpeedCurveEnabled.greaterThan(0.5).select(rotSpeedCurveSample, float3(1));
966
+ particleRotation.addAssign(
967
+ vec33(rotSpeedX, rotSpeedY, rotSpeedZ).mul(uniforms.deltaTime).mul(rotSpeedMultiplier)
845
968
  );
846
- });
847
- position.addAssign(velocity.mul(dt).mul(speedScale));
848
- If2(uniforms.collisionEnabled.greaterThan(0.5), () => {
849
- const planeY = uniforms.collisionPlaneY;
850
- const bounce = uniforms.collisionBounce;
851
- const friction = uniforms.collisionFriction;
852
- const shouldDie = uniforms.collisionDie;
853
- If2(position.y.lessThan(planeY), () => {
854
- If2(shouldDie.greaterThan(0.5), () => {
855
- lifetime.assign(float3(0));
856
- position.y.assign(float3(-1e3));
857
- }).Else(() => {
858
- position.y.assign(planeY);
859
- velocity.y.assign(velocity.y.abs().mul(bounce));
860
- velocity.x.mulAssign(friction);
861
- velocity.z.mulAssign(friction);
862
- });
863
- });
864
- });
865
- const idx = float3(instanceIndex3);
866
- const rotSpeedX = mix2(
867
- uniforms.rotationSpeedMinX,
868
- uniforms.rotationSpeedMaxX,
869
- hash2(idx.add(8888))
870
- );
871
- const rotSpeedY = mix2(
872
- uniforms.rotationSpeedMinY,
873
- uniforms.rotationSpeedMaxY,
874
- hash2(idx.add(9999))
875
- );
876
- const rotSpeedZ = mix2(
877
- uniforms.rotationSpeedMinZ,
878
- uniforms.rotationSpeedMaxZ,
879
- hash2(idx.add(10101))
880
- );
881
- const rotSpeedCurveSample = texture(
882
- curveTexture,
883
- vec2(progress, float3(0.5))
884
- ).w;
885
- const rotSpeedMultiplier = uniforms.rotationSpeedCurveEnabled.greaterThan(0.5).select(rotSpeedCurveSample, float3(1));
886
- particleRotation.addAssign(
887
- vec33(rotSpeedX, rotSpeedY, rotSpeedZ).mul(uniforms.deltaTime).mul(rotSpeedMultiplier)
888
- );
969
+ }
889
970
  lifetime.subAssign(fadeRate.mul(uniforms.deltaTime));
890
971
  If2(lifetime.lessThanEqual(0), () => {
891
972
  lifetime.assign(float3(0));
@@ -920,6 +1001,7 @@ import {
920
1001
  clamp
921
1002
  } from "three/tsl";
922
1003
  var createParticleMaterial = (storage, uniforms, curveTexture, options) => {
1004
+ var _a, _b, _c, _d;
923
1005
  const {
924
1006
  alphaMap,
925
1007
  flipbook,
@@ -937,13 +1019,13 @@ var createParticleMaterial = (storage, uniforms, curveTexture, options) => {
937
1019
  } = options;
938
1020
  const lifetime = storage.lifetimes.element(instanceIndex4);
939
1021
  const particleSize = storage.particleSizes.element(instanceIndex4);
940
- const particleRotation = storage.particleRotations.element(instanceIndex4);
941
- const pColorStart = storage.particleColorStarts.element(instanceIndex4);
942
- const pColorEnd = storage.particleColorEnds.element(instanceIndex4);
1022
+ const particleRotation = (_b = (_a = storage.particleRotations) == null ? void 0 : _a.element(instanceIndex4)) != null ? _b : vec34(0, 0, 0);
1023
+ const pColorStart = (_c = storage.particleColorStarts) == null ? void 0 : _c.element(instanceIndex4);
1024
+ const pColorEnd = (_d = storage.particleColorEnds) == null ? void 0 : _d.element(instanceIndex4);
943
1025
  const particlePos = storage.positions.element(instanceIndex4);
944
1026
  const particleVel = storage.velocities.element(instanceIndex4);
945
1027
  const progress = float4(1).sub(lifetime);
946
- const currentColor = mix3(pColorStart, pColorEnd, progress);
1028
+ const currentColor = pColorStart && pColorEnd ? mix3(pColorStart, pColorEnd, progress) : mix3(uniforms.colorStart0, uniforms.colorEnd0, progress);
947
1029
  const intensifiedColor = currentColor.mul(uniforms.intensity);
948
1030
  const curveSample = texture2(curveTexture, vec22(progress, float4(0.5)));
949
1031
  const sizeMultiplier = uniforms.fadeSizeCurveEnabled.greaterThan(0.5).select(
@@ -959,9 +1041,7 @@ var createParticleMaterial = (storage, uniforms, curveTexture, options) => {
959
1041
  const rows = float4(flipbook.rows || 1);
960
1042
  const columns = float4(flipbook.columns || 1);
961
1043
  const totalFrames = rows.mul(columns);
962
- const frameIndex = floor2(
963
- progress.mul(totalFrames).min(totalFrames.sub(1))
964
- );
1044
+ const frameIndex = floor2(progress.mul(totalFrames).min(totalFrames.sub(1)));
965
1045
  const col = mod(frameIndex, columns);
966
1046
  const row = floor2(frameIndex.div(columns));
967
1047
  const scaledUV = uv().div(vec22(columns, rows));
@@ -991,20 +1071,19 @@ var createParticleMaterial = (storage, uniforms, curveTexture, options) => {
991
1071
  }
992
1072
  }
993
1073
  const baseOpacity = opacityMultiplier.mul(shapeMask).mul(lifetime.greaterThan(1e-3).select(float4(1), float4(0)));
994
- const particleData = {
1074
+ const particleData = __spreadProps(__spreadValues(__spreadValues({
995
1075
  progress,
996
1076
  lifetime,
997
1077
  position: particlePos,
998
1078
  velocity: particleVel,
999
1079
  size: particleSize,
1000
- rotation: particleRotation,
1001
- colorStart: pColorStart,
1002
- colorEnd: pColorEnd,
1080
+ rotation: particleRotation
1081
+ }, pColorStart && { colorStart: pColorStart }), pColorEnd && { colorEnd: pColorEnd }), {
1003
1082
  color: currentColor,
1004
1083
  intensifiedColor,
1005
1084
  shapeMask,
1006
1085
  index: instanceIndex4
1007
- };
1086
+ });
1008
1087
  let finalOpacity = opacityNode ? baseOpacity.mul(
1009
1088
  typeof opacityNode === "function" ? opacityNode(particleData) : opacityNode
1010
1089
  ) : baseOpacity;
@@ -1071,10 +1150,7 @@ var createParticleMaterial = (storage, uniforms, curveTexture, options) => {
1071
1150
  const velDir = particleVel.div(velLen).mul(axisSign);
1072
1151
  const localAxis = axisIndex.lessThan(0.5).select(
1073
1152
  vec34(1, 0, 0),
1074
- axisIndex.lessThan(1.5).select(
1075
- vec34(0, 1, 0),
1076
- vec34(0, 0, 1)
1077
- )
1153
+ axisIndex.lessThan(1.5).select(vec34(0, 1, 0), vec34(0, 0, 1))
1078
1154
  );
1079
1155
  const dotProduct = localAxis.dot(velDir).clamp(-1, 1);
1080
1156
  const crossProduct = localAxis.cross(velDir);
@@ -1092,10 +1168,7 @@ var createParticleMaterial = (storage, uniforms, curveTexture, options) => {
1092
1168
  const kCrossV = rotAxis.cross(v);
1093
1169
  const rotatedByAxis = needsRotation.select(
1094
1170
  v.mul(cosAngleVal).add(kCrossV.mul(sinAngleVal)).add(rotAxis.mul(kDotV.mul(oneMinusCos))),
1095
- dotProduct.lessThan(-0.99).select(
1096
- v.negate(),
1097
- v
1098
- )
1171
+ dotProduct.lessThan(-0.99).select(v.negate(), v)
1099
1172
  );
1100
1173
  rotatedPos = rotatedByAxis;
1101
1174
  } else {
@@ -1178,6 +1251,7 @@ export {
1178
1251
  bakeCurveToArray,
1179
1252
  coreStore,
1180
1253
  createCombinedCurveTexture,
1254
+ createDefaultCurveTexture,
1181
1255
  createInitCompute,
1182
1256
  createParticleMaterial,
1183
1257
  createSpawnCompute,
@@ -1186,6 +1260,7 @@ export {
1186
1260
  evaluateBezierSegment,
1187
1261
  hexToRgb,
1188
1262
  lifetimeToFadeRate,
1263
+ loadCurveTextureFromPath,
1189
1264
  sampleCurveAtX,
1190
1265
  selectColor,
1191
1266
  toRange,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-vfx",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {