@newkrok/three-particles 2.6.3 → 2.6.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.
Files changed (39) hide show
  1. package/dist/index.d.ts +1782 -7
  2. package/dist/index.js +1578 -6
  3. package/dist/index.js.map +1 -0
  4. package/dist/three-particles.min.js +1 -2
  5. package/dist/three-particles.min.js.map +1 -0
  6. package/package.json +5 -7
  7. package/dist/bundle-report.json +0 -1
  8. package/dist/index.d.ts.map +0 -1
  9. package/dist/js/effects/three-particles/index.d.ts +0 -7
  10. package/dist/js/effects/three-particles/index.d.ts.map +0 -1
  11. package/dist/js/effects/three-particles/index.js +0 -6
  12. package/dist/js/effects/three-particles/shaders/particle-system-fragment-shader.glsl.d.ts +0 -3
  13. package/dist/js/effects/three-particles/shaders/particle-system-fragment-shader.glsl.d.ts.map +0 -1
  14. package/dist/js/effects/three-particles/shaders/particle-system-fragment-shader.glsl.js +0 -71
  15. package/dist/js/effects/three-particles/shaders/particle-system-vertex-shader.glsl.d.ts +0 -3
  16. package/dist/js/effects/three-particles/shaders/particle-system-vertex-shader.glsl.d.ts.map +0 -1
  17. package/dist/js/effects/three-particles/shaders/particle-system-vertex-shader.glsl.js +0 -37
  18. package/dist/js/effects/three-particles/three-particles-bezier.d.ts +0 -5
  19. package/dist/js/effects/three-particles/three-particles-bezier.d.ts.map +0 -1
  20. package/dist/js/effects/three-particles/three-particles-bezier.js +0 -62
  21. package/dist/js/effects/three-particles/three-particles-curves.d.ts +0 -108
  22. package/dist/js/effects/three-particles/three-particles-curves.d.ts.map +0 -1
  23. package/dist/js/effects/three-particles/three-particles-curves.js +0 -62
  24. package/dist/js/effects/three-particles/three-particles-enums.d.ts +0 -115
  25. package/dist/js/effects/three-particles/three-particles-enums.d.ts.map +0 -1
  26. package/dist/js/effects/three-particles/three-particles-enums.js +0 -1
  27. package/dist/js/effects/three-particles/three-particles-modifiers.d.ts +0 -73
  28. package/dist/js/effects/three-particles/three-particles-modifiers.d.ts.map +0 -1
  29. package/dist/js/effects/three-particles/three-particles-modifiers.js +0 -168
  30. package/dist/js/effects/three-particles/three-particles-utils.d.ts +0 -159
  31. package/dist/js/effects/three-particles/three-particles-utils.d.ts.map +0 -1
  32. package/dist/js/effects/three-particles/three-particles-utils.js +0 -302
  33. package/dist/js/effects/three-particles/three-particles.d.ts +0 -107
  34. package/dist/js/effects/three-particles/three-particles.d.ts.map +0 -1
  35. package/dist/js/effects/three-particles/three-particles.js +0 -972
  36. package/dist/js/effects/three-particles/types.d.ts +0 -1223
  37. package/dist/js/effects/three-particles/types.d.ts.map +0 -1
  38. package/dist/js/effects/three-particles/types.js +0 -1
  39. package/dist/three-particles.min.js.LICENSE.txt +0 -6
@@ -1,972 +0,0 @@
1
- import { ObjectUtils } from '@newkrok/three-utils';
2
- import * as THREE from 'three';
3
- import { Gyroscope } from 'three/examples/jsm/misc/Gyroscope.js';
4
- import { FBM } from 'three-noise/build/three-noise.module.js';
5
- import ParticleSystemFragmentShader from './shaders/particle-system-fragment-shader.glsl.js';
6
- import ParticleSystemVertexShader from './shaders/particle-system-vertex-shader.glsl.js';
7
- import { removeBezierCurveFunction } from './three-particles-bezier.js';
8
- import { applyModifiers } from './three-particles-modifiers.js';
9
- import { calculateRandomPositionAndVelocityOnBox, calculateRandomPositionAndVelocityOnCircle, calculateRandomPositionAndVelocityOnCone, calculateRandomPositionAndVelocityOnRectangle, calculateRandomPositionAndVelocityOnSphere, calculateValue, getCurveFunctionFromConfig, isLifeTimeCurve, createDefaultParticleTexture, } from './three-particles-utils.js';
10
- export * from './types.js';
11
- let _particleSystemId = 0;
12
- let createdParticleSystems = [];
13
- // Pre-allocated objects for updateParticleSystemInstance to avoid GC pressure
14
- const _lastWorldPositionSnapshot = new THREE.Vector3();
15
- const _distanceStep = { x: 0, y: 0, z: 0 };
16
- const _tempPosition = { x: 0, y: 0, z: 0 };
17
- const _modifierParams = {
18
- delta: 0,
19
- generalData: null,
20
- normalizedConfig: null,
21
- attributes: null,
22
- particleLifetimePercentage: 0,
23
- particleIndex: 0,
24
- };
25
- /**
26
- * Mapping of blending mode string identifiers to Three.js blending constants.
27
- *
28
- * Used for converting serialized particle system configurations (e.g., from JSON)
29
- * to actual Three.js blending mode constants.
30
- *
31
- * @example
32
- * ```typescript
33
- * import { blendingMap } from '@newkrok/three-particles';
34
- *
35
- * // Convert string to Three.js constant
36
- * const blending = blendingMap['THREE.AdditiveBlending'];
37
- * // blending === THREE.AdditiveBlending
38
- * ```
39
- */
40
- export const blendingMap = {
41
- 'THREE.NoBlending': THREE.NoBlending,
42
- 'THREE.NormalBlending': THREE.NormalBlending,
43
- 'THREE.AdditiveBlending': THREE.AdditiveBlending,
44
- 'THREE.SubtractiveBlending': THREE.SubtractiveBlending,
45
- 'THREE.MultiplyBlending': THREE.MultiplyBlending,
46
- };
47
- /**
48
- * Returns a deep copy of the default particle system configuration.
49
- *
50
- * This is useful when you want to start with default settings and modify specific properties
51
- * without affecting the internal default configuration object.
52
- *
53
- * @returns A new object containing all default particle system settings
54
- *
55
- * @example
56
- * ```typescript
57
- * import { getDefaultParticleSystemConfig, createParticleSystem } from '@newkrok/three-particles';
58
- *
59
- * // Get default config and modify it
60
- * const config = getDefaultParticleSystemConfig();
61
- * config.emission.rateOverTime = 100;
62
- * config.startColor.min = { r: 1, g: 0, b: 0 };
63
- *
64
- * const { instance } = createParticleSystem(config);
65
- * scene.add(instance);
66
- * ```
67
- */
68
- export const getDefaultParticleSystemConfig = () => JSON.parse(JSON.stringify(DEFAULT_PARTICLE_SYSTEM_CONFIG));
69
- const DEFAULT_PARTICLE_SYSTEM_CONFIG = {
70
- transform: {
71
- position: new THREE.Vector3(),
72
- rotation: new THREE.Vector3(),
73
- scale: new THREE.Vector3(1, 1, 1),
74
- },
75
- duration: 5.0,
76
- looping: true,
77
- startDelay: 0,
78
- startLifetime: 5.0,
79
- startSpeed: 1.0,
80
- startSize: 1.0,
81
- startOpacity: 1.0,
82
- startRotation: 0.0,
83
- startColor: {
84
- min: { r: 1.0, g: 1.0, b: 1.0 },
85
- max: { r: 1.0, g: 1.0, b: 1.0 },
86
- },
87
- gravity: 0.0,
88
- simulationSpace: "LOCAL" /* SimulationSpace.LOCAL */,
89
- maxParticles: 100.0,
90
- emission: {
91
- rateOverTime: 10.0,
92
- rateOverDistance: 0.0,
93
- bursts: [],
94
- },
95
- shape: {
96
- shape: "SPHERE" /* Shape.SPHERE */,
97
- sphere: {
98
- radius: 1.0,
99
- radiusThickness: 1.0,
100
- arc: 360.0,
101
- },
102
- cone: {
103
- angle: 25.0,
104
- radius: 1.0,
105
- radiusThickness: 1.0,
106
- arc: 360.0,
107
- },
108
- circle: {
109
- radius: 1.0,
110
- radiusThickness: 1.0,
111
- arc: 360.0,
112
- },
113
- rectangle: {
114
- rotation: { x: 0.0, y: 0.0 }, // TODO: add z rotation
115
- scale: { x: 1.0, y: 1.0 },
116
- },
117
- box: {
118
- scale: { x: 1.0, y: 1.0, z: 1.0 },
119
- emitFrom: "VOLUME" /* EmitFrom.VOLUME */,
120
- },
121
- },
122
- map: undefined,
123
- renderer: {
124
- blending: THREE.NormalBlending,
125
- discardBackgroundColor: false,
126
- backgroundColorTolerance: 1.0,
127
- backgroundColor: { r: 1.0, g: 1.0, b: 1.0 },
128
- transparent: true,
129
- depthTest: true,
130
- depthWrite: false,
131
- },
132
- velocityOverLifetime: {
133
- isActive: false,
134
- linear: {
135
- x: 0,
136
- y: 0,
137
- z: 0,
138
- },
139
- orbital: {
140
- x: 0,
141
- y: 0,
142
- z: 0,
143
- },
144
- },
145
- sizeOverLifetime: {
146
- isActive: false,
147
- lifetimeCurve: {
148
- type: "BEZIER" /* LifeTimeCurve.BEZIER */,
149
- scale: 1,
150
- bezierPoints: [
151
- { x: 0, y: 0, percentage: 0 },
152
- { x: 1, y: 1, percentage: 1 },
153
- ],
154
- },
155
- },
156
- colorOverLifetime: {
157
- isActive: false,
158
- r: {
159
- type: "BEZIER" /* LifeTimeCurve.BEZIER */,
160
- scale: 1,
161
- bezierPoints: [
162
- { x: 0, y: 1, percentage: 0 },
163
- { x: 1, y: 1, percentage: 1 },
164
- ],
165
- },
166
- g: {
167
- type: "BEZIER" /* LifeTimeCurve.BEZIER */,
168
- scale: 1,
169
- bezierPoints: [
170
- { x: 0, y: 1, percentage: 0 },
171
- { x: 1, y: 1, percentage: 1 },
172
- ],
173
- },
174
- b: {
175
- type: "BEZIER" /* LifeTimeCurve.BEZIER */,
176
- scale: 1,
177
- bezierPoints: [
178
- { x: 0, y: 1, percentage: 0 },
179
- { x: 1, y: 1, percentage: 1 },
180
- ],
181
- },
182
- },
183
- opacityOverLifetime: {
184
- isActive: false,
185
- lifetimeCurve: {
186
- type: "BEZIER" /* LifeTimeCurve.BEZIER */,
187
- scale: 1,
188
- bezierPoints: [
189
- { x: 0, y: 0, percentage: 0 },
190
- { x: 1, y: 1, percentage: 1 },
191
- ],
192
- },
193
- },
194
- rotationOverLifetime: {
195
- isActive: false,
196
- min: 0.0,
197
- max: 0.0,
198
- },
199
- noise: {
200
- isActive: false,
201
- useRandomOffset: false,
202
- strength: 1.0,
203
- frequency: 0.5,
204
- octaves: 1,
205
- positionAmount: 1.0,
206
- rotationAmount: 0.0,
207
- sizeAmount: 0.0,
208
- },
209
- textureSheetAnimation: {
210
- tiles: new THREE.Vector2(1.0, 1.0),
211
- timeMode: "LIFETIME" /* TimeMode.LIFETIME */,
212
- fps: 30.0,
213
- startFrame: 0,
214
- },
215
- };
216
- const createFloat32Attributes = ({ geometry, propertyName, maxParticles, factory, }) => {
217
- const array = new Float32Array(maxParticles);
218
- if (typeof factory === 'function') {
219
- for (let i = 0; i < maxParticles; i++) {
220
- array[i] = factory(undefined, i);
221
- }
222
- }
223
- else {
224
- array.fill(factory);
225
- }
226
- geometry.setAttribute(propertyName, new THREE.BufferAttribute(array, 1));
227
- };
228
- const calculatePositionAndVelocity = (generalData, { shape, sphere, cone, circle, rectangle, box }, startSpeed, position, velocity) => {
229
- const calculatedStartSpeed = calculateValue(generalData.particleSystemId, startSpeed, generalData.normalizedLifetimePercentage);
230
- switch (shape) {
231
- case "SPHERE" /* Shape.SPHERE */:
232
- calculateRandomPositionAndVelocityOnSphere(position, generalData.wrapperQuaternion, velocity, calculatedStartSpeed, sphere);
233
- break;
234
- case "CONE" /* Shape.CONE */:
235
- calculateRandomPositionAndVelocityOnCone(position, generalData.wrapperQuaternion, velocity, calculatedStartSpeed, cone);
236
- break;
237
- case "CIRCLE" /* Shape.CIRCLE */:
238
- calculateRandomPositionAndVelocityOnCircle(position, generalData.wrapperQuaternion, velocity, calculatedStartSpeed, circle);
239
- break;
240
- case "RECTANGLE" /* Shape.RECTANGLE */:
241
- calculateRandomPositionAndVelocityOnRectangle(position, generalData.wrapperQuaternion, velocity, calculatedStartSpeed, rectangle);
242
- break;
243
- case "BOX" /* Shape.BOX */:
244
- calculateRandomPositionAndVelocityOnBox(position, generalData.wrapperQuaternion, velocity, calculatedStartSpeed, box);
245
- break;
246
- }
247
- };
248
- const destroyParticleSystem = (particleSystem) => {
249
- createdParticleSystems = createdParticleSystems.filter(({ particleSystem: savedParticleSystem, wrapper, generalData: { particleSystemId }, }) => {
250
- if (savedParticleSystem !== particleSystem &&
251
- wrapper !== particleSystem) {
252
- return true;
253
- }
254
- removeBezierCurveFunction(particleSystemId);
255
- savedParticleSystem.geometry.dispose();
256
- if (Array.isArray(savedParticleSystem.material))
257
- savedParticleSystem.material.forEach((material) => material.dispose());
258
- else
259
- savedParticleSystem.material.dispose();
260
- if (savedParticleSystem.parent)
261
- savedParticleSystem.parent.remove(savedParticleSystem);
262
- return false;
263
- });
264
- };
265
- /**
266
- * Creates a new particle system with the specified configuration.
267
- *
268
- * This is the primary function for instantiating particle effects. It handles the complete
269
- * setup of a particle system including geometry creation, material configuration, shader setup,
270
- * and initialization of all particle properties.
271
- *
272
- * @param config - Configuration object for the particle system. If not provided, uses default settings.
273
- * See {@link ParticleSystemConfig} for all available options.
274
- * @param externalNow - Optional custom timestamp in milliseconds. If not provided, uses `Date.now()`.
275
- * Useful for synchronized particle systems or testing.
276
- *
277
- * @returns A {@link ParticleSystem} object containing:
278
- * - `instance`: The THREE.Object3D that should be added to your scene
279
- * - `resumeEmitter()`: Function to resume particle emission
280
- * - `pauseEmitter()`: Function to pause particle emission
281
- * - `dispose()`: Function to clean up resources and remove the particle system
282
- *
283
- * @example
284
- * ```typescript
285
- * import { createParticleSystem, updateParticleSystems } from '@newkrok/three-particles';
286
- *
287
- * // Create a basic particle system with default settings
288
- * const { instance, dispose } = createParticleSystem();
289
- * scene.add(instance);
290
- *
291
- * // Create a custom fire effect
292
- * const fireEffect = createParticleSystem({
293
- * duration: 2.0,
294
- * looping: true,
295
- * startLifetime: { min: 0.5, max: 1.5 },
296
- * startSpeed: { min: 2, max: 4 },
297
- * startSize: { min: 0.5, max: 1.5 },
298
- * startColor: {
299
- * min: { r: 1.0, g: 0.3, b: 0.0 },
300
- * max: { r: 1.0, g: 0.8, b: 0.0 }
301
- * },
302
- * emission: { rateOverTime: 50 },
303
- * shape: {
304
- * shape: Shape.CONE,
305
- * cone: { angle: 10, radius: 0.2 }
306
- * }
307
- * });
308
- * scene.add(fireEffect.instance);
309
- *
310
- * // In your animation loop
311
- * function animate(time) {
312
- * updateParticleSystems({ now: time, delta: deltaTime, elapsed: elapsedTime });
313
- * renderer.render(scene, camera);
314
- * }
315
- *
316
- * // Clean up when done
317
- * fireEffect.dispose();
318
- * ```
319
- *
320
- * @see {@link updateParticleSystems} - Required function to call in your animation loop
321
- * @see {@link ParticleSystemConfig} - Complete configuration options
322
- */
323
- export const createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow) => {
324
- const now = externalNow || Date.now();
325
- const generalData = {
326
- particleSystemId: _particleSystemId++,
327
- normalizedLifetimePercentage: 0,
328
- distanceFromLastEmitByDistance: 0,
329
- lastWorldPosition: new THREE.Vector3(-99999),
330
- currentWorldPosition: new THREE.Vector3(-99999),
331
- worldPositionChange: new THREE.Vector3(),
332
- worldQuaternion: new THREE.Quaternion(),
333
- wrapperQuaternion: new THREE.Quaternion(),
334
- lastWorldQuaternion: new THREE.Quaternion(-99999),
335
- worldEuler: new THREE.Euler(),
336
- gravityVelocity: new THREE.Vector3(0, 0, 0),
337
- startValues: {},
338
- linearVelocityData: undefined,
339
- orbitalVelocityData: undefined,
340
- lifetimeValues: {},
341
- creationTimes: [],
342
- noise: {
343
- isActive: false,
344
- strength: 0,
345
- noisePower: 0,
346
- positionAmount: 0,
347
- rotationAmount: 0,
348
- sizeAmount: 0,
349
- },
350
- isEnabled: true,
351
- };
352
- const normalizedConfig = ObjectUtils.deepMerge(DEFAULT_PARTICLE_SYSTEM_CONFIG, config, { applyToFirstObject: false, skippedProperties: [] });
353
- let particleMap = normalizedConfig.map || createDefaultParticleTexture();
354
- const { transform, duration, looping, startDelay, startLifetime, startSpeed, startSize, startRotation, startColor, startOpacity, gravity, simulationSpace, maxParticles, emission, shape, renderer, noise, velocityOverLifetime, onUpdate, onComplete, textureSheetAnimation, } = normalizedConfig;
355
- if (typeof renderer?.blending === 'string')
356
- renderer.blending = blendingMap[renderer.blending];
357
- const startPositions = Array.from({ length: maxParticles }, () => new THREE.Vector3());
358
- const velocities = Array.from({ length: maxParticles }, () => new THREE.Vector3());
359
- generalData.creationTimes = Array.from({ length: maxParticles }, () => 0);
360
- // Free list for O(1) inactive particle lookup (stack, top = end of array)
361
- const freeList = Array.from({ length: maxParticles }, (_, i) => maxParticles - 1 - i);
362
- if (velocityOverLifetime.isActive) {
363
- generalData.linearVelocityData = Array.from({ length: maxParticles }, () => ({
364
- speed: new THREE.Vector3(velocityOverLifetime.linear.x
365
- ? calculateValue(generalData.particleSystemId, velocityOverLifetime.linear.x, 0)
366
- : 0, velocityOverLifetime.linear.y
367
- ? calculateValue(generalData.particleSystemId, velocityOverLifetime.linear.y, 0)
368
- : 0, velocityOverLifetime.linear.z
369
- ? calculateValue(generalData.particleSystemId, velocityOverLifetime.linear.z, 0)
370
- : 0),
371
- valueModifiers: {
372
- x: isLifeTimeCurve(velocityOverLifetime.linear.x || 0)
373
- ? getCurveFunctionFromConfig(generalData.particleSystemId, velocityOverLifetime.linear.x)
374
- : undefined,
375
- y: isLifeTimeCurve(velocityOverLifetime.linear.y || 0)
376
- ? getCurveFunctionFromConfig(generalData.particleSystemId, velocityOverLifetime.linear.y)
377
- : undefined,
378
- z: isLifeTimeCurve(velocityOverLifetime.linear.z || 0)
379
- ? getCurveFunctionFromConfig(generalData.particleSystemId, velocityOverLifetime.linear.z)
380
- : undefined,
381
- },
382
- }));
383
- generalData.orbitalVelocityData = Array.from({ length: maxParticles }, () => ({
384
- speed: new THREE.Vector3(velocityOverLifetime.orbital.x
385
- ? calculateValue(generalData.particleSystemId, velocityOverLifetime.orbital.x, 0)
386
- : 0, velocityOverLifetime.orbital.y
387
- ? calculateValue(generalData.particleSystemId, velocityOverLifetime.orbital.y, 0)
388
- : 0, velocityOverLifetime.orbital.z
389
- ? calculateValue(generalData.particleSystemId, velocityOverLifetime.orbital.z, 0)
390
- : 0),
391
- valueModifiers: {
392
- x: isLifeTimeCurve(velocityOverLifetime.orbital.x || 0)
393
- ? getCurveFunctionFromConfig(generalData.particleSystemId, velocityOverLifetime.orbital.x)
394
- : undefined,
395
- y: isLifeTimeCurve(velocityOverLifetime.orbital.y || 0)
396
- ? getCurveFunctionFromConfig(generalData.particleSystemId, velocityOverLifetime.orbital.y)
397
- : undefined,
398
- z: isLifeTimeCurve(velocityOverLifetime.orbital.z || 0)
399
- ? getCurveFunctionFromConfig(generalData.particleSystemId, velocityOverLifetime.orbital.z)
400
- : undefined,
401
- },
402
- positionOffset: new THREE.Vector3(),
403
- }));
404
- }
405
- const startValueKeys = [
406
- 'startSize',
407
- 'startOpacity',
408
- ];
409
- startValueKeys.forEach((key) => {
410
- generalData.startValues[key] = Array.from({ length: maxParticles }, () => calculateValue(generalData.particleSystemId, normalizedConfig[key], 0));
411
- });
412
- generalData.startValues.startColorR = Array.from({ length: maxParticles }, () => 0);
413
- generalData.startValues.startColorG = Array.from({ length: maxParticles }, () => 0);
414
- generalData.startValues.startColorB = Array.from({ length: maxParticles }, () => 0);
415
- const lifetimeValueKeys = [
416
- 'rotationOverLifetime',
417
- ];
418
- lifetimeValueKeys.forEach((key) => {
419
- const value = normalizedConfig[key];
420
- if (value.isActive)
421
- generalData.lifetimeValues[key] = Array.from({ length: maxParticles }, () => THREE.MathUtils.randFloat(value.min, value.max));
422
- });
423
- generalData.noise = {
424
- isActive: noise.isActive,
425
- strength: noise.strength,
426
- noisePower: 0.15 * noise.strength,
427
- positionAmount: noise.positionAmount,
428
- rotationAmount: noise.rotationAmount,
429
- sizeAmount: noise.sizeAmount,
430
- sampler: noise.isActive
431
- ? new FBM({
432
- seed: Math.random(),
433
- scale: noise.frequency,
434
- octaves: noise.octaves,
435
- })
436
- : undefined,
437
- offsets: noise.useRandomOffset
438
- ? Array.from({ length: maxParticles }, () => Math.random() * 100)
439
- : undefined,
440
- };
441
- // Initialize burst states if bursts are configured
442
- if (emission.bursts && emission.bursts.length > 0) {
443
- generalData.burstStates = emission.bursts.map(() => ({
444
- cyclesExecuted: 0,
445
- lastCycleTime: 0,
446
- probabilityPassed: false,
447
- }));
448
- }
449
- const material = new THREE.ShaderMaterial({
450
- uniforms: {
451
- elapsed: {
452
- value: 0.0,
453
- },
454
- map: {
455
- value: particleMap,
456
- },
457
- tiles: {
458
- value: textureSheetAnimation.tiles,
459
- },
460
- fps: {
461
- value: textureSheetAnimation.fps,
462
- },
463
- useFPSForFrameIndex: {
464
- value: textureSheetAnimation.timeMode === "FPS" /* TimeMode.FPS */,
465
- },
466
- backgroundColor: {
467
- value: renderer.backgroundColor,
468
- },
469
- discardBackgroundColor: {
470
- value: renderer.discardBackgroundColor,
471
- },
472
- backgroundColorTolerance: {
473
- value: renderer.backgroundColorTolerance,
474
- },
475
- },
476
- vertexShader: ParticleSystemVertexShader,
477
- fragmentShader: ParticleSystemFragmentShader,
478
- transparent: renderer.transparent,
479
- blending: renderer.blending,
480
- depthTest: renderer.depthTest,
481
- depthWrite: renderer.depthWrite,
482
- });
483
- const geometry = new THREE.BufferGeometry();
484
- for (let i = 0; i < maxParticles; i++)
485
- calculatePositionAndVelocity(generalData, shape, startSpeed, startPositions[i], velocities[i]);
486
- const positionArray = new Float32Array(maxParticles * 3);
487
- for (let i = 0; i < maxParticles; i++) {
488
- positionArray[i * 3] = startPositions[i].x;
489
- positionArray[i * 3 + 1] = startPositions[i].y;
490
- positionArray[i * 3 + 2] = startPositions[i].z;
491
- }
492
- geometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3));
493
- createFloat32Attributes({
494
- geometry,
495
- propertyName: 'isActive',
496
- maxParticles,
497
- factory: 0,
498
- });
499
- createFloat32Attributes({
500
- geometry,
501
- propertyName: 'lifetime',
502
- maxParticles,
503
- factory: 0,
504
- });
505
- createFloat32Attributes({
506
- geometry,
507
- propertyName: 'startLifetime',
508
- maxParticles,
509
- factory: () => calculateValue(generalData.particleSystemId, startLifetime, 0) * 1000,
510
- });
511
- createFloat32Attributes({
512
- geometry,
513
- propertyName: 'startFrame',
514
- maxParticles,
515
- factory: () => textureSheetAnimation.startFrame
516
- ? calculateValue(generalData.particleSystemId, textureSheetAnimation.startFrame, 0)
517
- : 0,
518
- });
519
- createFloat32Attributes({
520
- geometry,
521
- propertyName: 'opacity',
522
- maxParticles,
523
- factory: () => calculateValue(generalData.particleSystemId, startOpacity, 0),
524
- });
525
- createFloat32Attributes({
526
- geometry,
527
- propertyName: 'rotation',
528
- maxParticles,
529
- factory: () => calculateValue(generalData.particleSystemId, startRotation, 0),
530
- });
531
- createFloat32Attributes({
532
- geometry,
533
- propertyName: 'size',
534
- maxParticles,
535
- factory: (_, index) => generalData.startValues.startSize[index],
536
- });
537
- createFloat32Attributes({
538
- geometry,
539
- propertyName: 'rotation',
540
- maxParticles,
541
- factory: 0,
542
- });
543
- const colorRandomRatio = Math.random();
544
- createFloat32Attributes({
545
- geometry,
546
- propertyName: 'colorR',
547
- maxParticles,
548
- factory: () => startColor.min.r +
549
- colorRandomRatio * (startColor.max.r - startColor.min.r),
550
- });
551
- createFloat32Attributes({
552
- geometry,
553
- propertyName: 'colorG',
554
- maxParticles,
555
- factory: () => startColor.min.g +
556
- colorRandomRatio * (startColor.max.g - startColor.min.g),
557
- });
558
- createFloat32Attributes({
559
- geometry,
560
- propertyName: 'colorB',
561
- maxParticles,
562
- factory: () => startColor.min.b +
563
- colorRandomRatio * (startColor.max.b - startColor.min.b),
564
- });
565
- createFloat32Attributes({
566
- geometry,
567
- propertyName: 'colorA',
568
- maxParticles,
569
- factory: 0,
570
- });
571
- const deactivateParticle = (particleIndex) => {
572
- geometry.attributes.isActive.array[particleIndex] = 0;
573
- geometry.attributes.colorA.array[particleIndex] = 0;
574
- geometry.attributes.colorA.needsUpdate = true;
575
- freeList.push(particleIndex);
576
- };
577
- const activateParticle = ({ particleIndex, activationTime, position, }) => {
578
- geometry.attributes.isActive.array[particleIndex] = 1;
579
- generalData.creationTimes[particleIndex] = activationTime;
580
- if (generalData.noise.offsets)
581
- generalData.noise.offsets[particleIndex] = Math.random() * 100;
582
- const colorRandomRatio = Math.random();
583
- geometry.attributes.colorR.array[particleIndex] =
584
- startColor.min.r +
585
- colorRandomRatio * (startColor.max.r - startColor.min.r);
586
- geometry.attributes.colorR.needsUpdate = true;
587
- geometry.attributes.colorG.array[particleIndex] =
588
- startColor.min.g +
589
- colorRandomRatio * (startColor.max.g - startColor.min.g);
590
- geometry.attributes.colorG.needsUpdate = true;
591
- geometry.attributes.colorB.array[particleIndex] =
592
- startColor.min.b +
593
- colorRandomRatio * (startColor.max.b - startColor.min.b);
594
- geometry.attributes.colorB.needsUpdate = true;
595
- generalData.startValues.startColorR[particleIndex] =
596
- geometry.attributes.colorR.array[particleIndex];
597
- generalData.startValues.startColorG[particleIndex] =
598
- geometry.attributes.colorG.array[particleIndex];
599
- generalData.startValues.startColorB[particleIndex] =
600
- geometry.attributes.colorB.array[particleIndex];
601
- geometry.attributes.startFrame.array[particleIndex] =
602
- textureSheetAnimation.startFrame
603
- ? calculateValue(generalData.particleSystemId, textureSheetAnimation.startFrame, 0)
604
- : 0;
605
- geometry.attributes.startFrame.needsUpdate = true;
606
- geometry.attributes.startLifetime.array[particleIndex] =
607
- calculateValue(generalData.particleSystemId, startLifetime, generalData.normalizedLifetimePercentage) * 1000;
608
- geometry.attributes.startLifetime.needsUpdate = true;
609
- generalData.startValues.startSize[particleIndex] = calculateValue(generalData.particleSystemId, startSize, generalData.normalizedLifetimePercentage);
610
- geometry.attributes.size.array[particleIndex] =
611
- generalData.startValues.startSize[particleIndex];
612
- geometry.attributes.size.needsUpdate = true;
613
- generalData.startValues.startOpacity[particleIndex] = calculateValue(generalData.particleSystemId, startOpacity, generalData.normalizedLifetimePercentage);
614
- geometry.attributes.colorA.array[particleIndex] =
615
- generalData.startValues.startOpacity[particleIndex];
616
- geometry.attributes.colorA.needsUpdate = true;
617
- geometry.attributes.rotation.array[particleIndex] = calculateValue(generalData.particleSystemId, startRotation, generalData.normalizedLifetimePercentage);
618
- geometry.attributes.rotation.needsUpdate = true;
619
- if (normalizedConfig.rotationOverLifetime.isActive)
620
- generalData.lifetimeValues.rotationOverLifetime[particleIndex] =
621
- THREE.MathUtils.randFloat(normalizedConfig.rotationOverLifetime.min, normalizedConfig.rotationOverLifetime.max);
622
- calculatePositionAndVelocity(generalData, shape, startSpeed, startPositions[particleIndex], velocities[particleIndex]);
623
- const positionIndex = Math.floor(particleIndex * 3);
624
- geometry.attributes.position.array[positionIndex] =
625
- position.x + startPositions[particleIndex].x;
626
- geometry.attributes.position.array[positionIndex + 1] =
627
- position.y + startPositions[particleIndex].y;
628
- geometry.attributes.position.array[positionIndex + 2] =
629
- position.z + startPositions[particleIndex].z;
630
- geometry.attributes.position.needsUpdate = true;
631
- if (generalData.linearVelocityData) {
632
- generalData.linearVelocityData[particleIndex].speed.set(normalizedConfig.velocityOverLifetime.linear.x
633
- ? calculateValue(generalData.particleSystemId, normalizedConfig.velocityOverLifetime.linear.x, 0)
634
- : 0, normalizedConfig.velocityOverLifetime.linear.y
635
- ? calculateValue(generalData.particleSystemId, normalizedConfig.velocityOverLifetime.linear.y, 0)
636
- : 0, normalizedConfig.velocityOverLifetime.linear.z
637
- ? calculateValue(generalData.particleSystemId, normalizedConfig.velocityOverLifetime.linear.z, 0)
638
- : 0);
639
- }
640
- if (generalData.orbitalVelocityData) {
641
- generalData.orbitalVelocityData[particleIndex].speed.set(normalizedConfig.velocityOverLifetime.orbital.x
642
- ? calculateValue(generalData.particleSystemId, normalizedConfig.velocityOverLifetime.orbital.x, 0)
643
- : 0, normalizedConfig.velocityOverLifetime.orbital.y
644
- ? calculateValue(generalData.particleSystemId, normalizedConfig.velocityOverLifetime.orbital.y, 0)
645
- : 0, normalizedConfig.velocityOverLifetime.orbital.z
646
- ? calculateValue(generalData.particleSystemId, normalizedConfig.velocityOverLifetime.orbital.z, 0)
647
- : 0);
648
- generalData.orbitalVelocityData[particleIndex].positionOffset.set(startPositions[particleIndex].x, startPositions[particleIndex].y, startPositions[particleIndex].z);
649
- }
650
- geometry.attributes.lifetime.array[particleIndex] = 0;
651
- geometry.attributes.lifetime.needsUpdate = true;
652
- applyModifiers({
653
- delta: 0,
654
- generalData,
655
- normalizedConfig,
656
- attributes: particleSystem.geometry.attributes,
657
- particleLifetimePercentage: 0,
658
- particleIndex,
659
- });
660
- };
661
- let particleSystem = new THREE.Points(geometry, material);
662
- particleSystem.position.copy(transform.position);
663
- particleSystem.rotation.x = THREE.MathUtils.degToRad(transform.rotation.x);
664
- particleSystem.rotation.y = THREE.MathUtils.degToRad(transform.rotation.y);
665
- particleSystem.rotation.z = THREE.MathUtils.degToRad(transform.rotation.z);
666
- particleSystem.scale.copy(transform.scale);
667
- const calculatedCreationTime = now + calculateValue(generalData.particleSystemId, startDelay) * 1000;
668
- let wrapper;
669
- if (normalizedConfig.simulationSpace === "WORLD" /* SimulationSpace.WORLD */) {
670
- wrapper = new Gyroscope();
671
- wrapper.add(particleSystem);
672
- }
673
- const instanceData = {
674
- particleSystem,
675
- wrapper,
676
- elapsedUniform: material.uniforms.elapsed,
677
- generalData,
678
- onUpdate,
679
- onComplete,
680
- creationTime: calculatedCreationTime,
681
- lastEmissionTime: calculatedCreationTime,
682
- duration,
683
- looping,
684
- simulationSpace,
685
- gravity,
686
- emission,
687
- normalizedConfig,
688
- iterationCount: 0,
689
- velocities,
690
- freeList,
691
- deactivateParticle,
692
- activateParticle,
693
- };
694
- createdParticleSystems.push(instanceData);
695
- const resumeEmitter = () => (generalData.isEnabled = true);
696
- const pauseEmitter = () => (generalData.isEnabled = false);
697
- const dispose = () => destroyParticleSystem(particleSystem);
698
- const update = (cycleData) => updateParticleSystemInstance(instanceData, cycleData);
699
- return {
700
- instance: wrapper || particleSystem,
701
- resumeEmitter,
702
- pauseEmitter,
703
- dispose,
704
- update,
705
- };
706
- };
707
- /**
708
- * Updates all active particle systems created with {@link createParticleSystem}.
709
- *
710
- * This function must be called once per frame in your animation loop to animate all particles.
711
- * It handles particle emission, movement, lifetime tracking, modifier application, and cleanup
712
- * of expired particle systems.
713
- *
714
- * @param cycleData - Object containing timing information for the current frame:
715
- * - `now`: Current timestamp in milliseconds (typically from `performance.now()` or `Date.now()`)
716
- * - `delta`: Time elapsed since the last frame in seconds
717
- * - `elapsed`: Total time elapsed since the animation started in seconds
718
- *
719
- * @example
720
- * ```typescript
721
- * import { createParticleSystem, updateParticleSystems } from '@newkrok/three-particles';
722
- *
723
- * const { instance } = createParticleSystem({
724
- * // your config
725
- * });
726
- * scene.add(instance);
727
- *
728
- * // Animation loop
729
- * let lastTime = 0;
730
- * let elapsedTime = 0;
731
- *
732
- * function animate(currentTime) {
733
- * requestAnimationFrame(animate);
734
- *
735
- * const delta = (currentTime - lastTime) / 1000; // Convert to seconds
736
- * elapsedTime += delta;
737
- * lastTime = currentTime;
738
- *
739
- * // Update all particle systems
740
- * updateParticleSystems({
741
- * now: currentTime,
742
- * delta: delta,
743
- * elapsed: elapsedTime
744
- * });
745
- *
746
- * renderer.render(scene, camera);
747
- * }
748
- *
749
- * animate(0);
750
- * ```
751
- *
752
- * @example
753
- * ```typescript
754
- * // Using Three.js Clock for timing
755
- * import * as THREE from 'three';
756
- * import { updateParticleSystems } from '@newkrok/three-particles';
757
- *
758
- * const clock = new THREE.Clock();
759
- *
760
- * function animate() {
761
- * requestAnimationFrame(animate);
762
- *
763
- * const delta = clock.getDelta();
764
- * const elapsed = clock.getElapsedTime();
765
- *
766
- * updateParticleSystems({
767
- * now: performance.now(),
768
- * delta: delta,
769
- * elapsed: elapsed
770
- * });
771
- *
772
- * renderer.render(scene, camera);
773
- * }
774
- * ```
775
- *
776
- * @see {@link createParticleSystem} - Creates particle systems to be updated
777
- * @see {@link CycleData} - Timing data structure
778
- */
779
- const updateParticleSystemInstance = (props, { now, delta, elapsed }) => {
780
- const { onUpdate, generalData, onComplete, particleSystem, wrapper, elapsedUniform, creationTime, lastEmissionTime, duration, looping, emission, normalizedConfig, iterationCount, velocities, freeList, deactivateParticle, activateParticle, simulationSpace, gravity, } = props;
781
- const lifetime = now - creationTime;
782
- const normalizedLifetime = lifetime % (duration * 1000);
783
- generalData.normalizedLifetimePercentage = Math.max(Math.min(normalizedLifetime / (duration * 1000), 1), 0);
784
- const { lastWorldPosition, currentWorldPosition, worldPositionChange, lastWorldQuaternion, worldQuaternion, worldEuler, gravityVelocity, isEnabled, } = generalData;
785
- if (wrapper?.parent)
786
- generalData.wrapperQuaternion.copy(wrapper.parent.quaternion);
787
- _lastWorldPositionSnapshot.copy(lastWorldPosition);
788
- elapsedUniform.value = elapsed;
789
- particleSystem.getWorldPosition(currentWorldPosition);
790
- if (lastWorldPosition.x !== -99999) {
791
- worldPositionChange.set(currentWorldPosition.x - lastWorldPosition.x, currentWorldPosition.y - lastWorldPosition.y, currentWorldPosition.z - lastWorldPosition.z);
792
- }
793
- if (isEnabled) {
794
- generalData.distanceFromLastEmitByDistance += worldPositionChange.length();
795
- }
796
- particleSystem.getWorldPosition(lastWorldPosition);
797
- particleSystem.getWorldQuaternion(worldQuaternion);
798
- if (lastWorldQuaternion.x === -99999 ||
799
- lastWorldQuaternion.x !== worldQuaternion.x ||
800
- lastWorldQuaternion.y !== worldQuaternion.y ||
801
- lastWorldQuaternion.z !== worldQuaternion.z) {
802
- worldEuler.setFromQuaternion(worldQuaternion);
803
- lastWorldQuaternion.copy(worldQuaternion);
804
- gravityVelocity.set(lastWorldPosition.x, lastWorldPosition.y + gravity, lastWorldPosition.z);
805
- particleSystem.worldToLocal(gravityVelocity);
806
- }
807
- const creationTimes = generalData.creationTimes;
808
- const attributes = particleSystem.geometry.attributes;
809
- const isActiveArr = attributes.isActive.array;
810
- const startLifetimeArr = attributes.startLifetime.array;
811
- const positionArr = attributes.position.array;
812
- const lifetimeArr = attributes.lifetime.array;
813
- const creationTimesLength = creationTimes.length;
814
- let positionNeedsUpdate = false;
815
- let lifetimeNeedsUpdate = false;
816
- _modifierParams.delta = delta;
817
- _modifierParams.generalData = generalData;
818
- _modifierParams.normalizedConfig = normalizedConfig;
819
- _modifierParams.attributes = attributes;
820
- for (let index = 0; index < creationTimesLength; index++) {
821
- if (isActiveArr[index]) {
822
- const particleLifetime = now - creationTimes[index];
823
- if (particleLifetime > startLifetimeArr[index]) {
824
- deactivateParticle(index);
825
- }
826
- else {
827
- const velocity = velocities[index];
828
- velocity.x -= gravityVelocity.x * delta;
829
- velocity.y -= gravityVelocity.y * delta;
830
- velocity.z -= gravityVelocity.z * delta;
831
- if (gravity !== 0 ||
832
- velocity.x !== 0 ||
833
- velocity.y !== 0 ||
834
- velocity.z !== 0 ||
835
- worldPositionChange.x !== 0 ||
836
- worldPositionChange.y !== 0 ||
837
- worldPositionChange.z !== 0) {
838
- const positionIndex = index * 3;
839
- if (simulationSpace === "WORLD" /* SimulationSpace.WORLD */) {
840
- positionArr[positionIndex] -= worldPositionChange.x;
841
- positionArr[positionIndex + 1] -= worldPositionChange.y;
842
- positionArr[positionIndex + 2] -= worldPositionChange.z;
843
- }
844
- positionArr[positionIndex] += velocity.x * delta;
845
- positionArr[positionIndex + 1] += velocity.y * delta;
846
- positionArr[positionIndex + 2] += velocity.z * delta;
847
- positionNeedsUpdate = true;
848
- }
849
- lifetimeArr[index] = particleLifetime;
850
- lifetimeNeedsUpdate = true;
851
- _modifierParams.particleLifetimePercentage =
852
- particleLifetime / startLifetimeArr[index];
853
- _modifierParams.particleIndex = index;
854
- applyModifiers(_modifierParams);
855
- }
856
- }
857
- }
858
- if (positionNeedsUpdate)
859
- attributes.position.needsUpdate = true;
860
- if (lifetimeNeedsUpdate)
861
- attributes.lifetime.needsUpdate = true;
862
- if (isEnabled && (looping || lifetime < duration * 1000)) {
863
- const emissionDelta = now - lastEmissionTime;
864
- const neededParticlesByTime = emission.rateOverTime
865
- ? Math.floor(calculateValue(generalData.particleSystemId, emission.rateOverTime, generalData.normalizedLifetimePercentage) *
866
- (emissionDelta / 1000))
867
- : 0;
868
- const rateOverDistance = emission.rateOverDistance
869
- ? calculateValue(generalData.particleSystemId, emission.rateOverDistance, generalData.normalizedLifetimePercentage)
870
- : 0;
871
- const neededParticlesByDistance = rateOverDistance > 0 && generalData.distanceFromLastEmitByDistance > 0
872
- ? Math.floor(generalData.distanceFromLastEmitByDistance / (1 / rateOverDistance))
873
- : 0;
874
- const useDistanceStep = neededParticlesByDistance > 0;
875
- if (useDistanceStep) {
876
- _distanceStep.x =
877
- (currentWorldPosition.x - _lastWorldPositionSnapshot.x) /
878
- neededParticlesByDistance;
879
- _distanceStep.y =
880
- (currentWorldPosition.y - _lastWorldPositionSnapshot.y) /
881
- neededParticlesByDistance;
882
- _distanceStep.z =
883
- (currentWorldPosition.z - _lastWorldPositionSnapshot.z) /
884
- neededParticlesByDistance;
885
- }
886
- let neededParticles = neededParticlesByTime + neededParticlesByDistance;
887
- if (rateOverDistance > 0 && neededParticlesByDistance >= 1) {
888
- generalData.distanceFromLastEmitByDistance = 0;
889
- }
890
- // Process burst emissions
891
- if (emission.bursts && generalData.burstStates) {
892
- const bursts = emission.bursts;
893
- const burstStates = generalData.burstStates;
894
- const currentIterationTime = normalizedLifetime;
895
- for (let i = 0; i < bursts.length; i++) {
896
- const burst = bursts[i];
897
- const state = burstStates[i];
898
- const burstTimeMs = burst.time * 1000;
899
- const cycles = burst.cycles ?? 1;
900
- const intervalMs = (burst.interval ?? 0) * 1000;
901
- const probability = burst.probability ?? 1;
902
- // Check if we've looped and need to reset burst states
903
- if (looping &&
904
- currentIterationTime < burstTimeMs &&
905
- state.cyclesExecuted > 0) {
906
- state.cyclesExecuted = 0;
907
- state.lastCycleTime = 0;
908
- state.probabilityPassed = false;
909
- }
910
- // Check if all cycles for this burst have been executed
911
- if (state.cyclesExecuted >= cycles)
912
- continue;
913
- // Calculate the time for the next cycle
914
- const nextCycleTime = burstTimeMs + state.cyclesExecuted * intervalMs;
915
- // Check if it's time for the next cycle
916
- if (currentIterationTime >= nextCycleTime) {
917
- // On first cycle, determine if probability check passes
918
- if (state.cyclesExecuted === 0) {
919
- state.probabilityPassed = Math.random() < probability;
920
- }
921
- // Only emit if probability check passed
922
- if (state.probabilityPassed) {
923
- const burstCount = Math.floor(calculateValue(generalData.particleSystemId, burst.count, generalData.normalizedLifetimePercentage));
924
- neededParticles += burstCount;
925
- }
926
- state.cyclesExecuted++;
927
- state.lastCycleTime = currentIterationTime;
928
- }
929
- }
930
- }
931
- if (neededParticles > 0) {
932
- let generatedParticlesByDistanceNeeds = 0;
933
- for (let i = 0; i < neededParticles; i++) {
934
- if (freeList.length === 0)
935
- break;
936
- const particleIndex = freeList.pop();
937
- _tempPosition.x = 0;
938
- _tempPosition.y = 0;
939
- _tempPosition.z = 0;
940
- if (useDistanceStep &&
941
- generatedParticlesByDistanceNeeds < neededParticlesByDistance) {
942
- _tempPosition.x = _distanceStep.x * generatedParticlesByDistanceNeeds;
943
- _tempPosition.y = _distanceStep.y * generatedParticlesByDistanceNeeds;
944
- _tempPosition.z = _distanceStep.z * generatedParticlesByDistanceNeeds;
945
- generatedParticlesByDistanceNeeds++;
946
- }
947
- activateParticle({
948
- particleIndex,
949
- activationTime: now,
950
- position: _tempPosition,
951
- });
952
- props.lastEmissionTime = now;
953
- }
954
- }
955
- if (onUpdate)
956
- onUpdate({
957
- particleSystem,
958
- delta,
959
- elapsed,
960
- lifetime,
961
- normalizedLifetime,
962
- iterationCount: iterationCount + 1,
963
- });
964
- }
965
- else if (onComplete)
966
- onComplete({
967
- particleSystem,
968
- });
969
- };
970
- export const updateParticleSystems = (cycleData) => {
971
- createdParticleSystems.forEach((props) => updateParticleSystemInstance(props, cycleData));
972
- };