@newkrok/three-particles 2.14.1 → 2.15.1

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
@@ -64,12 +64,18 @@ const effect = {
64
64
  // Your effect configuration here
65
65
  // It can be empty to use default settings
66
66
  };
67
- const { instance } = createParticleSystem(effect);
68
- scene.add(instance);
67
+ const system = createParticleSystem(effect);
68
+ scene.add(system.instance);
69
69
 
70
70
  // Update the particle system in your animation loop
71
71
  // Pass the current time, delta time, and elapsed time
72
72
  updateParticleSystems({now, delta, elapsed});
73
+
74
+ // Update configuration at runtime without recreating the system
75
+ system.updateConfig({
76
+ gravity: -9.8,
77
+ forceFields: [{ type: 'DIRECTIONAL', direction: { x: 1, y: 0, z: 0 }, strength: 5 }],
78
+ });
73
79
  ```
74
80
 
75
81
  # Usage with React Three Fiber
package/dist/index.d.ts CHANGED
@@ -1849,6 +1849,39 @@ type ParticleSystem = {
1849
1849
  pauseEmitter: () => void;
1850
1850
  dispose: () => void;
1851
1851
  update: (cycleData: CycleData) => void;
1852
+ /**
1853
+ * Updates the particle system configuration at runtime without recreating the system.
1854
+ *
1855
+ * System-level properties (gravity, force fields, noise, emission rates, color/size/opacity
1856
+ * over lifetime curves) take effect immediately for all particles.
1857
+ * Per-particle spawn properties (startColor, startSize, startSpeed, startLifetime, etc.)
1858
+ * only affect newly emitted particles — already-alive particles retain their original values.
1859
+ *
1860
+ * @param config - A partial configuration object. Only the provided properties will be updated;
1861
+ * all other settings remain unchanged.
1862
+ *
1863
+ * @remarks
1864
+ * Structural properties that are set at creation time cannot be changed at runtime:
1865
+ * `maxParticles`, `renderer.rendererType`, `shape`, and `map` (texture).
1866
+ * Passing these will update the internal config but have no visible effect since the
1867
+ * geometry and material are pre-allocated.
1868
+ *
1869
+ * @example
1870
+ * ```typescript
1871
+ * const system = createParticleSystem(config);
1872
+ *
1873
+ * // Change wind direction in real time
1874
+ * system.updateConfig({
1875
+ * forceFields: [{ type: ForceFieldType.DIRECTIONAL, direction: { x: 1, y: 0, z: 0 }, strength: 5 }],
1876
+ * });
1877
+ *
1878
+ * // Gradually change color of new particles
1879
+ * system.updateConfig({
1880
+ * startColor: { min: { r: 1, g: 0, b: 0 }, max: { r: 1, g: 0.5, b: 0 } },
1881
+ * });
1882
+ * ```
1883
+ */
1884
+ updateConfig: (config: Partial<ParticleSystemConfig>) => void;
1852
1885
  };
1853
1886
  /**
1854
1887
  * Data representing the current cycle of the particle system's update loop.
package/dist/index.js CHANGED
@@ -1171,6 +1171,10 @@ var _particleSystemId = 0;
1171
1171
  var createdParticleSystems = [];
1172
1172
  var _subEmitterPosition = new THREE4.Vector3();
1173
1173
  var _lastWorldPositionSnapshot = new THREE4.Vector3();
1174
+ var _localForceFieldPos = new THREE4.Vector3();
1175
+ var _localForceFieldDir = new THREE4.Vector3();
1176
+ var _inverseQuat = new THREE4.Quaternion();
1177
+ var _localForceFields = [];
1174
1178
  new THREE4.Vector3();
1175
1179
  new THREE4.Vector3();
1176
1180
  new THREE4.Vector3();
@@ -1184,6 +1188,16 @@ var _modifierParams = {
1184
1188
  particleLifetimePercentage: 0,
1185
1189
  particleIndex: 0
1186
1190
  };
1191
+ var toVector3 = (v, fallback) => v ? new THREE4.Vector3(v.x ?? 0, v.y ?? 0, v.z ?? 0) : fallback.clone();
1192
+ var normalizeForceFields = (rawForceFields) => (rawForceFields ?? []).map((ff) => ({
1193
+ isActive: ff.isActive ?? true,
1194
+ type: ff.type ?? "POINT" /* POINT */,
1195
+ position: toVector3(ff.position, new THREE4.Vector3(0, 0, 0)),
1196
+ direction: toVector3(ff.direction, new THREE4.Vector3(0, 1, 0)).normalize(),
1197
+ strength: ff.strength ?? 1,
1198
+ range: Math.max(0, ff.range ?? Infinity),
1199
+ falloff: ff.falloff ?? "LINEAR" /* LINEAR */
1200
+ }));
1187
1201
  var blendingMap = {
1188
1202
  "THREE.NoBlending": THREE4.NoBlending,
1189
1203
  "THREE.NormalBlending": THREE4.NormalBlending,
@@ -1507,16 +1521,7 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
1507
1521
  subEmitters,
1508
1522
  forceFields: rawForceFields
1509
1523
  } = normalizedConfig;
1510
- const toVector3 = (v, fallback) => v ? new THREE4.Vector3(v.x ?? 0, v.y ?? 0, v.z ?? 0) : fallback.clone();
1511
- const normalizedForceFields = (rawForceFields ?? []).map((ff) => ({
1512
- isActive: ff.isActive ?? true,
1513
- type: ff.type ?? "POINT" /* POINT */,
1514
- position: toVector3(ff.position, new THREE4.Vector3(0, 0, 0)),
1515
- direction: toVector3(ff.direction, new THREE4.Vector3(0, 1, 0)).normalize(),
1516
- strength: ff.strength ?? 1,
1517
- range: Math.max(0, ff.range ?? Infinity),
1518
- falloff: ff.falloff ?? "LINEAR" /* LINEAR */
1519
- }));
1524
+ const normalizedForceFields = normalizeForceFields(rawForceFields);
1520
1525
  if (typeof renderer?.blending === "string")
1521
1526
  renderer.blending = blendingMap[renderer.blending];
1522
1527
  const startPositions = Array.from(
@@ -1952,44 +1957,45 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
1952
1957
  if (generalData.noise.offsets)
1953
1958
  generalData.noise.offsets[particleIndex] = Math.random() * 100;
1954
1959
  const colorRandomRatio2 = Math.random();
1955
- aColorR.array[particleIndex] = startColor.min.r + colorRandomRatio2 * (startColor.max.r - startColor.min.r);
1960
+ const cfgStartColor = normalizedConfig.startColor;
1961
+ aColorR.array[particleIndex] = cfgStartColor.min.r + colorRandomRatio2 * (cfgStartColor.max.r - cfgStartColor.min.r);
1956
1962
  aColorR.needsUpdate = true;
1957
- aColorG.array[particleIndex] = startColor.min.g + colorRandomRatio2 * (startColor.max.g - startColor.min.g);
1963
+ aColorG.array[particleIndex] = cfgStartColor.min.g + colorRandomRatio2 * (cfgStartColor.max.g - cfgStartColor.min.g);
1958
1964
  aColorG.needsUpdate = true;
1959
- aColorB.array[particleIndex] = startColor.min.b + colorRandomRatio2 * (startColor.max.b - startColor.min.b);
1965
+ aColorB.array[particleIndex] = cfgStartColor.min.b + colorRandomRatio2 * (cfgStartColor.max.b - cfgStartColor.min.b);
1960
1966
  aColorB.needsUpdate = true;
1961
1967
  generalData.startValues.startColorR[particleIndex] = aColorR.array[particleIndex];
1962
1968
  generalData.startValues.startColorG[particleIndex] = aColorG.array[particleIndex];
1963
1969
  generalData.startValues.startColorB[particleIndex] = aColorB.array[particleIndex];
1964
- aStartFrame.array[particleIndex] = textureSheetAnimation.startFrame ? calculateValue(
1970
+ aStartFrame.array[particleIndex] = normalizedConfig.textureSheetAnimation.startFrame ? calculateValue(
1965
1971
  generalData.particleSystemId,
1966
- textureSheetAnimation.startFrame,
1972
+ normalizedConfig.textureSheetAnimation.startFrame,
1967
1973
  0
1968
1974
  ) : 0;
1969
1975
  aStartFrame.needsUpdate = true;
1970
1976
  aStartLifetime.array[particleIndex] = calculateValue(
1971
1977
  generalData.particleSystemId,
1972
- startLifetime,
1978
+ normalizedConfig.startLifetime,
1973
1979
  generalData.normalizedLifetimePercentage
1974
1980
  ) * 1e3;
1975
1981
  aStartLifetime.needsUpdate = true;
1976
1982
  generalData.startValues.startSize[particleIndex] = calculateValue(
1977
1983
  generalData.particleSystemId,
1978
- startSize,
1984
+ normalizedConfig.startSize,
1979
1985
  generalData.normalizedLifetimePercentage
1980
1986
  );
1981
1987
  aSize.array[particleIndex] = generalData.startValues.startSize[particleIndex];
1982
1988
  aSize.needsUpdate = true;
1983
1989
  generalData.startValues.startOpacity[particleIndex] = calculateValue(
1984
1990
  generalData.particleSystemId,
1985
- startOpacity,
1991
+ normalizedConfig.startOpacity,
1986
1992
  generalData.normalizedLifetimePercentage
1987
1993
  );
1988
1994
  aColorA.array[particleIndex] = generalData.startValues.startOpacity[particleIndex];
1989
1995
  aColorA.needsUpdate = true;
1990
1996
  aRotation.array[particleIndex] = calculateValue(
1991
1997
  generalData.particleSystemId,
1992
- startRotation,
1998
+ normalizedConfig.startRotation,
1993
1999
  generalData.normalizedLifetimePercentage
1994
2000
  );
1995
2001
  aRotation.needsUpdate = true;
@@ -2010,8 +2016,8 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
2010
2016
  );
2011
2017
  calculatePositionAndVelocity(
2012
2018
  generalData,
2013
- shape,
2014
- startSpeed,
2019
+ normalizedConfig.shape,
2020
+ normalizedConfig.startSpeed,
2015
2021
  startPositions[particleIndex],
2016
2022
  velocities[particleIndex]
2017
2023
  );
@@ -2406,12 +2412,53 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
2406
2412
  for (const sub of instances) sub.update(cycleData);
2407
2413
  }
2408
2414
  };
2415
+ const updateConfig = (partialConfig) => {
2416
+ ObjectUtils.deepMerge(instanceData.normalizedConfig, partialConfig, {
2417
+ applyToFirstObject: true,
2418
+ skippedProperties: []
2419
+ });
2420
+ const cfg = instanceData.normalizedConfig;
2421
+ if (partialConfig.gravity !== void 0) {
2422
+ instanceData.gravity = cfg.gravity;
2423
+ generalData.lastWorldQuaternion.x = -99999;
2424
+ }
2425
+ if (partialConfig.duration !== void 0)
2426
+ instanceData.duration = cfg.duration;
2427
+ if (partialConfig.looping !== void 0) instanceData.looping = cfg.looping;
2428
+ if (partialConfig.simulationSpace !== void 0)
2429
+ instanceData.simulationSpace = cfg.simulationSpace;
2430
+ if (partialConfig.emission !== void 0)
2431
+ instanceData.emission = cfg.emission;
2432
+ if (partialConfig.forceFields !== void 0) {
2433
+ instanceData.normalizedForceFields = normalizeForceFields(
2434
+ cfg.forceFields
2435
+ );
2436
+ }
2437
+ if (partialConfig.noise !== void 0) {
2438
+ const n = cfg.noise;
2439
+ generalData.noise = {
2440
+ isActive: n.isActive,
2441
+ strength: n.strength,
2442
+ noisePower: 0.15 * n.strength,
2443
+ positionAmount: n.positionAmount,
2444
+ rotationAmount: n.rotationAmount,
2445
+ sizeAmount: n.sizeAmount,
2446
+ sampler: n.isActive ? new FBM({
2447
+ seed: Math.random(),
2448
+ scale: n.frequency,
2449
+ octaves: n.octaves
2450
+ }) : void 0,
2451
+ offsets: n.useRandomOffset ? generalData.noise.offsets ?? Array.from({ length: maxParticles }, () => Math.random() * 100) : void 0
2452
+ };
2453
+ }
2454
+ };
2409
2455
  return {
2410
2456
  instance: wrapper || particleSystem,
2411
2457
  resumeEmitter,
2412
2458
  pauseEmitter,
2413
2459
  dispose,
2414
- update
2460
+ update,
2461
+ updateConfig
2415
2462
  };
2416
2463
  };
2417
2464
  var updateParticleSystemInstance = (props, { now, delta, elapsed }) => {
@@ -2484,6 +2531,37 @@ var updateParticleSystemInstance = (props, { now, delta, elapsed }) => {
2484
2531
  );
2485
2532
  particleSystem.worldToLocal(gravityVelocity);
2486
2533
  }
2534
+ if (hasForceFields) {
2535
+ _inverseQuat.copy(worldQuaternion).invert();
2536
+ _localForceFields.length = normalizedForceFields.length;
2537
+ for (let i = 0; i < normalizedForceFields.length; i++) {
2538
+ const src = normalizedForceFields[i];
2539
+ let dst = _localForceFields[i];
2540
+ if (!dst) {
2541
+ dst = {
2542
+ isActive: true,
2543
+ type: "POINT" /* POINT */,
2544
+ position: new THREE4.Vector3(),
2545
+ direction: new THREE4.Vector3(),
2546
+ strength: 0,
2547
+ range: 0,
2548
+ falloff: "LINEAR" /* LINEAR */
2549
+ };
2550
+ _localForceFields[i] = dst;
2551
+ }
2552
+ dst.isActive = src.isActive;
2553
+ dst.type = src.type;
2554
+ dst.strength = src.strength;
2555
+ dst.range = src.range;
2556
+ dst.falloff = src.falloff;
2557
+ _localForceFieldPos.copy(src.position);
2558
+ particleSystem.worldToLocal(_localForceFieldPos);
2559
+ dst.position.copy(_localForceFieldPos);
2560
+ _localForceFieldDir.copy(src.direction);
2561
+ _localForceFieldDir.applyQuaternion(_inverseQuat);
2562
+ dst.direction.copy(_localForceFieldDir);
2563
+ }
2564
+ }
2487
2565
  const creationTimes = generalData.creationTimes;
2488
2566
  const isActiveArr = ma.isActive.array;
2489
2567
  const startLifetimeArr = ma.startLifetime.array;
@@ -2511,7 +2589,7 @@ var updateParticleSystemInstance = (props, { now, delta, elapsed }) => {
2511
2589
  if (hasForceFields) {
2512
2590
  applyForceFields({
2513
2591
  particleSystemId: generalData.particleSystemId,
2514
- forceFields: normalizedForceFields,
2592
+ forceFields: _localForceFields,
2515
2593
  velocity,
2516
2594
  positionArr,
2517
2595
  positionIndex: index * 3,