pixi-particles-engine 0.1.8 → 0.1.10

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 (71) hide show
  1. package/dist/cjs/behaviours/rotation-curve-behaviour.d.ts +21 -0
  2. package/dist/cjs/behaviours/rotation-curve-behaviour.d.ts.map +1 -0
  3. package/dist/cjs/behaviours/rotation-curve-behaviour.js +26 -0
  4. package/dist/cjs/behaviours/rotation-curve-behaviour.js.map +1 -0
  5. package/dist/cjs/behaviours/spawn-behaviours/circle-spawn-behaviour.d.ts +12 -5
  6. package/dist/cjs/behaviours/spawn-behaviours/circle-spawn-behaviour.d.ts.map +1 -1
  7. package/dist/cjs/behaviours/spawn-behaviours/circle-spawn-behaviour.js +15 -5
  8. package/dist/cjs/behaviours/spawn-behaviours/circle-spawn-behaviour.js.map +1 -1
  9. package/dist/cjs/behaviours/spawn-behaviours/rectangle-spawn-behaviour.d.ts +15 -5
  10. package/dist/cjs/behaviours/spawn-behaviours/rectangle-spawn-behaviour.d.ts.map +1 -1
  11. package/dist/cjs/behaviours/spawn-behaviours/rectangle-spawn-behaviour.js +36 -7
  12. package/dist/cjs/behaviours/spawn-behaviours/rectangle-spawn-behaviour.js.map +1 -1
  13. package/dist/cjs/behaviours/static-behaviours/static-rotation-behaviour.d.ts +41 -3
  14. package/dist/cjs/behaviours/static-behaviours/static-rotation-behaviour.d.ts.map +1 -1
  15. package/dist/cjs/behaviours/static-behaviours/static-rotation-behaviour.js +46 -5
  16. package/dist/cjs/behaviours/static-behaviours/static-rotation-behaviour.js.map +1 -1
  17. package/dist/cjs/index.d.ts +1 -0
  18. package/dist/cjs/index.d.ts.map +1 -1
  19. package/dist/cjs/index.js +1 -0
  20. package/dist/cjs/index.js.map +1 -1
  21. package/dist/cjs/px-particle.d.ts +7 -2
  22. package/dist/cjs/px-particle.d.ts.map +1 -1
  23. package/dist/cjs/px-particle.js +17 -4
  24. package/dist/cjs/px-particle.js.map +1 -1
  25. package/dist/esm/behaviours/rotation-curve-behaviour.d.ts +21 -0
  26. package/dist/esm/behaviours/rotation-curve-behaviour.d.ts.map +1 -0
  27. package/dist/esm/behaviours/rotation-curve-behaviour.js +22 -0
  28. package/dist/esm/behaviours/rotation-curve-behaviour.js.map +1 -0
  29. package/dist/esm/behaviours/spawn-behaviours/circle-spawn-behaviour.d.ts +12 -5
  30. package/dist/esm/behaviours/spawn-behaviours/circle-spawn-behaviour.d.ts.map +1 -1
  31. package/dist/esm/behaviours/spawn-behaviours/circle-spawn-behaviour.js +15 -5
  32. package/dist/esm/behaviours/spawn-behaviours/circle-spawn-behaviour.js.map +1 -1
  33. package/dist/esm/behaviours/spawn-behaviours/rectangle-spawn-behaviour.d.ts +15 -5
  34. package/dist/esm/behaviours/spawn-behaviours/rectangle-spawn-behaviour.d.ts.map +1 -1
  35. package/dist/esm/behaviours/spawn-behaviours/rectangle-spawn-behaviour.js +36 -7
  36. package/dist/esm/behaviours/spawn-behaviours/rectangle-spawn-behaviour.js.map +1 -1
  37. package/dist/esm/behaviours/static-behaviours/static-rotation-behaviour.d.ts +41 -3
  38. package/dist/esm/behaviours/static-behaviours/static-rotation-behaviour.d.ts.map +1 -1
  39. package/dist/esm/behaviours/static-behaviours/static-rotation-behaviour.js +46 -5
  40. package/dist/esm/behaviours/static-behaviours/static-rotation-behaviour.js.map +1 -1
  41. package/dist/esm/index.d.ts +1 -0
  42. package/dist/esm/index.d.ts.map +1 -1
  43. package/dist/esm/index.js +1 -0
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/px-particle.d.ts +7 -2
  46. package/dist/esm/px-particle.d.ts.map +1 -1
  47. package/dist/esm/px-particle.js +17 -4
  48. package/dist/esm/px-particle.js.map +1 -1
  49. package/package.json +2 -1
  50. package/src/behaviour.ts +74 -0
  51. package/src/behaviours/alpha-behaviour.ts +29 -0
  52. package/src/behaviours/alpha-curve-behaviour.ts +34 -0
  53. package/src/behaviours/curved-behaviour/curve-key-frame.ts +23 -0
  54. package/src/behaviours/curved-behaviour/curve-sampler.ts +92 -0
  55. package/src/behaviours/movement-behaviours/gravity-behaviour.ts +48 -0
  56. package/src/behaviours/movement-behaviours/movement-curve-behaviour.ts +39 -0
  57. package/src/behaviours/movement-behaviours/radial-burst-behaviour.ts +57 -0
  58. package/src/behaviours/rotation-curve-behaviour.ts +32 -0
  59. package/src/behaviours/scale-curve-behaviour.ts +36 -0
  60. package/src/behaviours/spawn-behaviours/circle-spawn-behaviour.ts +37 -0
  61. package/src/behaviours/spawn-behaviours/rectangle-spawn-behaviour.ts +60 -0
  62. package/src/behaviours/static-behaviours/static-rotation-behaviour.ts +99 -0
  63. package/src/behaviours/static-behaviours/static-scale-behaviour.ts +21 -0
  64. package/src/emitter.ts +517 -0
  65. package/src/index.ts +19 -0
  66. package/src/px-particle.ts +125 -0
  67. package/src/texture-provider.ts +66 -0
  68. package/src/texture-providers/animated-texture-provider.ts +146 -0
  69. package/src/texture-providers/single-texture-provider.ts +22 -0
  70. package/src/texture-providers/weighted-texture-provider.ts +52 -0
  71. package/src/utils.ts +13 -0
package/src/emitter.ts ADDED
@@ -0,0 +1,517 @@
1
+ import { ParticleContainer, ParticleContainerOptions, ParticleProperties, Ticker } from "pixi.js";
2
+ import { Behaviour } from "./behaviour";
3
+ import { TextureProvider } from "./texture-provider";
4
+ import { PxParticle } from "./px-particle";
5
+
6
+ /**
7
+ * How this emitter produces particles over time:
8
+ * - "rate": continuously emits at a fixed particles-per-second
9
+ * - "wave": emits discrete bursts ("waves") every interval
10
+ * - "manual": user code calls emitBurst/emitWave; no ticker hookup
11
+ */
12
+ export type EmissionMode = "rate" | "wave" | "manual";
13
+
14
+ /**
15
+ * Configuration passed to {@link Emitter}.
16
+ *
17
+ * Notes:
18
+ * - In "rate" mode, you must provide `ratePerSecond`
19
+ * - In "wave" mode, you should provide `waveInterval` and `particlesPerWave`
20
+ * - In "manual" mode, the emitter will NOT auto-update; you trigger emission yourself
21
+ */
22
+ export interface EmitterOptions {
23
+ /**
24
+ * Options forwarded to PixiJS ParticleContainer.
25
+ * You can set blendMode, position, etc.
26
+ *
27
+ * NOTE: dynamicProperties are computed automatically based on behaviours + texture provider.
28
+ */
29
+ containerOptions?: ParticleContainerOptions;
30
+
31
+ /** Maximum number of particles alive at the same time. Also defines the pool size. */
32
+ maxParticles: number;
33
+
34
+ /** Emission strategy (rate / wave / manual). */
35
+ mode: EmissionMode;
36
+
37
+ /**
38
+ * Particles per second when mode === "rate".
39
+ */
40
+ ratePerSecond?: number;
41
+
42
+ /**
43
+ * Seconds between waves when mode === "wave".
44
+ * If omitted, a default is used.
45
+ */
46
+ waveInterval?: number;
47
+
48
+ /**
49
+ * How many particles are spawned each wave when mode === "wave".
50
+ * If omitted, defaults to 1.
51
+ */
52
+ particlesPerWave?: number;
53
+
54
+ /** Particle lifetime in seconds. Each particle chooses a random value in [min, max]. */
55
+ lifetime: { min: number; max: number };
56
+
57
+ /**
58
+ * If true, the emitter automatically emits according to its mode.
59
+ * If false, particles can still be spawned using manual methods.
60
+ */
61
+ emitting?: boolean;
62
+
63
+ /**
64
+ * Clamp delta-time to avoid huge simulation jumps (tab pause, breakpoint, slow frame, etc).
65
+ * This prevents spawning a massive burst and "teleporting" particles.
66
+ */
67
+ maxDeltaSeconds?: number;
68
+
69
+ /**
70
+ * Behaviours are modular systems applied to particles.
71
+ * Typical responsibilities:
72
+ * - initialize properties at spawn (velocity, scale, alpha, etc)
73
+ * - update properties each frame (curves, gravity, drag, etc)
74
+ *
75
+ * Behaviours can also declare "requires" to enable ParticleContainer dynamicProperties.
76
+ */
77
+ behaviours?: Behaviour[];
78
+
79
+ /**
80
+ * If true, particles are added at index 0 (behind existing children).
81
+ * Useful for layering (e.g. smoke behind sparks).
82
+ */
83
+ addAtBack?: boolean;
84
+
85
+ /**
86
+ * Optional custom ticker (e.g. your app ticker).
87
+ * If omitted, uses Ticker.shared.
88
+ */
89
+ ticker?: Ticker;
90
+ }
91
+
92
+ /**
93
+ * Internally we keep behaviours in a stable sorted array:
94
+ * - prio: behaviour priority (lower runs earlier)
95
+ * - order: insertion order to break ties (stable ordering)
96
+ */
97
+ type SortedBehaviour = { b: Behaviour; order: number; prio: number };
98
+
99
+ /**
100
+ * ParticleContainer dynamicProperties config.
101
+ * Pixi uses this to know which particle attributes must be re-uploaded to GPU each frame.
102
+ */
103
+ type DynamicProps = ParticleProperties & Record<string, boolean>;
104
+
105
+ /**
106
+ * Emitter is a ParticleContainer that owns a pool of {@link PxParticle} instances.
107
+ *
108
+ * Core responsibilities:
109
+ * - Keep a pool of particles to avoid allocations during gameplay.
110
+ * - Emit particles according to the selected EmissionMode.
111
+ * - Update active particles each tick (movement, behaviours, textures).
112
+ * - Recycle dead particles back into the pool.
113
+ */
114
+ export class Emitter extends ParticleContainer {
115
+ /** Pool capacity / maximum concurrent particles. */
116
+ private maxParticles: number;
117
+
118
+ /** Current emission mode. */
119
+ private mode: EmissionMode;
120
+
121
+ /** Particles per second for mode="rate". */
122
+ public ratePerSecond?: number;
123
+
124
+ /** Wave interval (seconds) for mode="wave". */
125
+ public waveInterval?: number;
126
+
127
+ /** Particles per wave for mode="wave". */
128
+ public particlesPerWave?: number;
129
+
130
+ /** Lifetime range (seconds) used for each spawned particle. */
131
+ private lifetime: { min: number; max: number };
132
+
133
+ /**
134
+ * If true, emission is automatic (rate/wave).
135
+ * If false, update still runs but no new particles are spawned.
136
+ */
137
+ public emitting?: boolean;
138
+
139
+ /** Delta clamp to keep simulation stable on long frames. */
140
+ private maxDeltaSeconds: number;
141
+
142
+ /** Whether new particles should be added behind existing particles. */
143
+ public addAtBack?: boolean;
144
+
145
+ /** Ticker driving updates when not in manual mode. */
146
+ private ticker: Ticker;
147
+
148
+ /** Responsible for choosing textures and (optionally) updating animated textures. */
149
+ private textureProvider: TextureProvider;
150
+
151
+ /** Inactive particle pool (reused). */
152
+ private readonly pool: PxParticle[] = [];
153
+
154
+ /** Active particles currently simulated and rendered. */
155
+ private readonly active: PxParticle[] = [];
156
+
157
+ /**
158
+ * Sorted behaviours (priority + stable insertion order).
159
+ * Behaviours are applied in order every frame.
160
+ */
161
+ private behaviours: SortedBehaviour[] = [];
162
+ private nextBehaviourOrder = 0;
163
+
164
+ /** Accumulator used to compute spawn counts in rate mode. */
165
+ private emitAcc = 0;
166
+
167
+ /** Accumulator used to track time between waves in wave mode. */
168
+ private waveAcc = 0;
169
+
170
+ private updateEmitterBound?: (ticker: Ticker) => void;
171
+ private tickerAttached = false;
172
+
173
+ constructor(options: EmitterOptions, textureProvider: TextureProvider) {
174
+ super({
175
+ label: "Emitter",
176
+ ...options.containerOptions,
177
+
178
+ /**
179
+ * Important optimization:
180
+ * We enable only the dynamic properties that are actually needed
181
+ * (based on behaviours + texture provider requirements).
182
+ */
183
+ dynamicProperties: Emitter.computeDynamicProperties(options.behaviours ?? [], textureProvider),
184
+ });
185
+
186
+ this.maxParticles = options.maxParticles;
187
+ this.mode = options.mode;
188
+ this.ratePerSecond = options.ratePerSecond;
189
+ this.waveInterval = options.waveInterval;
190
+ this.particlesPerWave = options.particlesPerWave;
191
+ this.lifetime = options.lifetime;
192
+ this.emitting = options.emitting;
193
+ this.maxDeltaSeconds = options.maxDeltaSeconds ?? 0.1;
194
+ this.addAtBack = options.addAtBack;
195
+
196
+ this.textureProvider = textureProvider;
197
+
198
+ // Register behaviours (sorted by priority + stable order).
199
+ if (options.behaviours) {
200
+ for (const behaviour of options.behaviours) {
201
+ this.addBehaviour(behaviour);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Pre-allocate particles upfront:
207
+ * - avoids runtime allocations / GC spikes
208
+ * - allows "maxParticles" to be a hard cap
209
+ *
210
+ * Each pooled particle gets an initial texture (required by Pixi Particle).
211
+ * The actual texture may be replaced on spawn by the provider.
212
+ */
213
+ for (let i = 0; i < this.maxParticles; i++) {
214
+ const p = new PxParticle({ texture: this.textureProvider.initialTexture() });
215
+ this.pool.push(p);
216
+ }
217
+
218
+ /**
219
+ * In manual mode we do NOT attach to a ticker.
220
+ * User code calls emitBurst/emitWave and also needs to call updateEmitter manually
221
+ * (or you can provide a separate public update method if you prefer).
222
+ *
223
+ * NOTE: currently updateEmitter is still public and can be called manually.
224
+ */
225
+ this.ticker = options.ticker ?? Ticker.shared;
226
+ if (this.mode !== "manual") {
227
+ this.attachTicker();
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Adds a behaviour and inserts it into the sorted execution order.
233
+ * Priority controls order; ties are resolved by insertion order.
234
+ */
235
+ private addBehaviour(b: Behaviour): this {
236
+ this.behaviours.push({
237
+ b,
238
+ order: this.nextBehaviourOrder++,
239
+ prio: b.priority ?? 0,
240
+ });
241
+
242
+ // lower priority runs earlier
243
+ this.behaviours.sort((a, c) => a.prio - c.prio || a.order - c.order);
244
+
245
+ // Optional init hook for behaviour to cache references or precompute curves.
246
+ b.init?.(this);
247
+ return this;
248
+ }
249
+
250
+ /**
251
+ * Ticker callback: advances simulation and handles emission.
252
+ *
253
+ * The emitter uses ticker.deltaMS (milliseconds between frames) converted to seconds.
254
+ * We clamp dt to maxDeltaSeconds to avoid large jumps and excessive spawning.
255
+ */
256
+ public updateEmitter(ticker: Ticker): void {
257
+ const maxDt = this.maxDeltaSeconds;
258
+ let dt = ticker.deltaMS / 1000;
259
+
260
+ // Clamp dt for stability (e.g. tab in background).
261
+ if (dt > maxDt) dt = maxDt;
262
+ if (dt <= 0) return;
263
+
264
+ // Spawn new particles if enabled.
265
+ if (this.emitting) this.emitParticles(dt);
266
+
267
+ /**
268
+ * Update particles & kill dead ones.
269
+ * Iterate backwards so we can remove by index safely.
270
+ */
271
+ for (let i = this.active.length - 1; i >= 0; i--) {
272
+ const p = this.active[i];
273
+
274
+ // Texture provider may animate / swap textures per frame.
275
+ this.textureProvider.update?.(p, dt);
276
+
277
+ // Base integrator: apply velocity and angular velocity.
278
+ // Behaviours can also modify velocity, position, rotation, etc.
279
+ p.age += dt;
280
+ p.x += p.vx * dt;
281
+ p.y += p.vy * dt;
282
+ p.rotation += p.angleV * dt;
283
+
284
+ // Apply behaviours in sorted order.
285
+ for (const entry of this.behaviours) entry.b.update?.(p, dt, this);
286
+
287
+ // Kill particle if it exceeded its lifetime.
288
+ if (p.age >= p.life) {
289
+ this.killAtIndex(i);
290
+ }
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Emits particles according to the emitter's mode.
296
+ * This method is called automatically each tick when emitting=true.
297
+ */
298
+ private emitParticles(dt: number): void {
299
+ if (this.mode === "rate") {
300
+ const rate = this.ratePerSecond ?? 0;
301
+ if (rate <= 0) return;
302
+
303
+ /**
304
+ * Accumulate fractional time and convert into "how many particles should we emit".
305
+ * This produces stable emission even with variable frame rates.
306
+ */
307
+ this.emitAcc += dt;
308
+ const want = Math.floor(this.emitAcc * rate);
309
+ if (want <= 0) return;
310
+
311
+ // Keep the remainder time after spawning `want` particles.
312
+ this.emitAcc -= want / rate;
313
+
314
+ for (let i = 0; i < want; i++) this.spawnOne();
315
+ return;
316
+ }
317
+
318
+ if (this.mode === "wave") {
319
+ const interval = this.waveInterval ?? 0.25;
320
+ const perWave = this.particlesPerWave ?? 1;
321
+ if (interval <= 0 || perWave <= 0) return;
322
+
323
+ // Emit full waves when the accumulated time crosses the interval.
324
+ this.waveAcc += dt;
325
+ while (this.waveAcc >= interval) {
326
+ this.waveAcc -= interval;
327
+ for (let i = 0; i < perWave; i++) this.spawnOne();
328
+ }
329
+ return;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Spawns a single particle from the pool.
335
+ *
336
+ * IMPORTANT:
337
+ * - If the pool is empty, spawning is skipped (hard cap).
338
+ * - Texture is selected by TextureProvider at spawn time.
339
+ * - Behaviours' onSpawn hooks initialize per-particle state.
340
+ */
341
+ private spawnOne(): void {
342
+ const p = this.pool.pop();
343
+ if (!p) return;
344
+
345
+ if (this.textureProvider.textureForSpawn) p.texture = this.textureProvider.textureForSpawn(p);
346
+
347
+ p.onSpawn();
348
+
349
+ // Allow behaviours to initialize the particle for this spawn.
350
+ for (const behaviour of this.behaviours) behaviour.b.onSpawn?.(p, this);
351
+
352
+ // Add particle to the container (front or back).
353
+ if (this.addAtBack) {
354
+ this.addParticleAt(p, 0);
355
+ } else {
356
+ this.addParticle(p);
357
+ }
358
+
359
+ // Randomize lifetime in [min, max].
360
+ const { min, max } = this.lifetime;
361
+ p.life = min + Math.random() * Math.max(0, max - min);
362
+
363
+ this.active.push(p);
364
+ }
365
+
366
+ /**
367
+ * Kills and recycles a particle at a given index in the active list.
368
+ *
369
+ * Order of operations:
370
+ * 1) behaviour kill hooks (reverse order, mirroring teardown)
371
+ * 2) provider kill hook
372
+ * 3) reset particle instance
373
+ * 4) remove from container + active list
374
+ * 5) return to pool
375
+ */
376
+ private killAtIndex(activeIndex: number): void {
377
+ const p = this.active[activeIndex];
378
+
379
+ // Call behaviours' teardown in reverse to match common init/apply ordering expectations.
380
+ for (let i = this.behaviours.length - 1; i >= 0; i--) {
381
+ this.behaviours[i].b.onKill?.(p, this);
382
+ }
383
+
384
+ this.textureProvider.onKill?.(p);
385
+
386
+ p.onKill();
387
+
388
+ this.removeParticle(p);
389
+
390
+ /**
391
+ * Remove from active list using "swap remove":
392
+ * - O(1)
393
+ * - does not preserve ordering (fine for particle sims)
394
+ */
395
+ const last = this.active.length - 1;
396
+ if (activeIndex !== last) this.active[activeIndex] = this.active[last];
397
+ this.active.pop();
398
+
399
+ this.pool.push(p);
400
+ }
401
+
402
+ /**
403
+ * Immediately kills all active particles and resets emission accumulators.
404
+ * Useful when restarting an effect or changing scenes.
405
+ */
406
+ public clearParticles(): void {
407
+ this.emitAcc = 0;
408
+ this.waveAcc = 0;
409
+
410
+ while (this.active.length > 0) {
411
+ this.killAtIndex(this.active.length - 1);
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Manual emission helpers.
417
+ * Only relevant if mode === "manual" or emitting === false.
418
+ */
419
+ public emitBurst(count: number): void {
420
+ for (let i = 0; i < count; i++) this.spawnOne();
421
+ }
422
+
423
+ public emitWave(): void {
424
+ const n = this.particlesPerWave ?? 0;
425
+ for (let i = 0; i < n; i++) this.spawnOne();
426
+ }
427
+
428
+ /**
429
+ * Computes ParticleContainer dynamicProperties from:
430
+ * - TextureProvider.requires
431
+ * - each Behaviour.requires
432
+ *
433
+ * Keeping these minimal is important for performance:
434
+ * enabling extra dynamic properties can increase per-frame GPU uploads.
435
+ */
436
+ private static computeDynamicProperties(behaviours: Behaviour[], provider: TextureProvider): DynamicProps {
437
+ const props: DynamicProps = {
438
+ position: false,
439
+ rotation: false,
440
+ vertex: false,
441
+ uvs: false,
442
+ color: false,
443
+ };
444
+
445
+ // Texture provider may require updates like uvs (animated textures) or vertex.
446
+ if (provider.requires) {
447
+ for (const key in provider.requires) {
448
+ props[key] = true;
449
+ }
450
+ }
451
+
452
+ // Behaviours declare which GPU-updated properties they touch over time.
453
+ for (const b of behaviours) {
454
+ if (!b.requires) continue;
455
+
456
+ for (const key in b.requires) {
457
+ props[key] = true;
458
+ }
459
+ }
460
+
461
+ return props;
462
+ }
463
+
464
+ /**
465
+ * Attaches the emitter update loop to the configured ticker.
466
+ */
467
+ private attachTicker(): void {
468
+ if (this.tickerAttached) return;
469
+ if (!this.updateEmitterBound) {
470
+ this.updateEmitterBound = this.updateEmitter.bind(this);
471
+ }
472
+
473
+ if (!this.ticker) {
474
+ this.ticker = Ticker.shared;
475
+ }
476
+
477
+ this.ticker.add(this.updateEmitterBound);
478
+ this.tickerAttached = true;
479
+ }
480
+
481
+ /**
482
+ * Detaches the emitter update loop from the ticker.
483
+ */
484
+ private detachTicker(): void {
485
+ if (!this.tickerAttached) return;
486
+ if (!this.ticker || !this.updateEmitterBound) return;
487
+ this.ticker.remove(this.updateEmitterBound);
488
+ this.tickerAttached = false;
489
+ }
490
+
491
+ /**
492
+ * Changes the emission mode at runtime.
493
+ *
494
+ * - Switching to "manual" detaches the ticker.
495
+ * - Switching to "rate" or "wave" attaches the ticker.
496
+ *
497
+ */
498
+ public setMode(mode: EmissionMode): void {
499
+ if (this.mode === mode) return;
500
+
501
+ const wasManual = this.mode === "manual";
502
+ const willBeManual = mode === "manual";
503
+
504
+ this.mode = mode;
505
+
506
+ if (!wasManual && willBeManual) {
507
+ this.detachTicker();
508
+ } else if (wasManual && !willBeManual) {
509
+ this.attachTicker();
510
+ }
511
+ }
512
+
513
+ public override destroy(options?: any): void {
514
+ this.detachTicker();
515
+ super.destroy(options);
516
+ }
517
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export * from "./behaviour";
2
+ export * from "./emitter";
3
+ export * from "./texture-provider";
4
+ export * from "./behaviours/alpha-behaviour";
5
+ export * from "./behaviours/alpha-curve-behaviour";
6
+ export * from "./behaviours/scale-curve-behaviour";
7
+ export * from "./behaviours/curved-behaviour/curve-key-frame";
8
+ export * from "./behaviours/curved-behaviour/curve-sampler";
9
+ export * from "./behaviours/movement-behaviours/gravity-behaviour";
10
+ export * from "./behaviours/movement-behaviours/movement-curve-behaviour";
11
+ export * from "./behaviours/movement-behaviours/radial-burst-behaviour";
12
+ export * from "./behaviours/spawn-behaviours/circle-spawn-behaviour";
13
+ export * from "./behaviours/spawn-behaviours/rectangle-spawn-behaviour";
14
+ export * from "./behaviours/static-behaviours/static-rotation-behaviour";
15
+ export * from "./behaviours/static-behaviours/static-scale-behaviour";
16
+ export * from "./behaviours/rotation-curve-behaviour";
17
+ export * from "./texture-providers/animated-texture-provider";
18
+ export * from "./texture-providers/single-texture-provider";
19
+ export * from "./texture-providers/weighted-texture-provider";
@@ -0,0 +1,125 @@
1
+ import { Particle, ParticleOptions } from "pixi.js";
2
+
3
+ /**
4
+ * Internal state used by AnimatedTextureProvider.
5
+ *
6
+ * `t` tracks elapsed time (seconds) inside the animation.
7
+ * This allows providers to compute frame index based on lifetime progression.
8
+ */
9
+ export type AnimatedParticleState = {
10
+ t: number; // seconds progressed in animated texture (sprite sheet)
11
+ };
12
+
13
+ /**
14
+ * PxParticle extends PixiJS {@link Particle} with simulation state.
15
+ *
16
+ * Why this exists:
17
+ * - Pixi's Particle handles rendering efficiently.
18
+ * - We extend it to attach simulation data (velocity, lifetime, etc).
19
+ * - Instances are pooled and reused by the Emitter.
20
+ *
21
+ * IMPORTANT:
22
+ * Particles are never destroyed during runtime.
23
+ * They are recycled via onKill() and reused via onSpawn().
24
+ */
25
+ export class PxParticle extends Particle {
26
+ /** Seconds since spawn. */
27
+ public age = 0;
28
+
29
+ /** Total lifetime in seconds (randomized per spawn by the emitter). */
30
+ public life = 1;
31
+
32
+ /** Velocity in pixels per second (X axis). */
33
+ public vx = 0;
34
+
35
+ /** Velocity in pixels per second (Y axis). */
36
+ public vy = 0;
37
+
38
+ /** Base angular velocity in radians per second. */
39
+ private _angleVBase = 0;
40
+
41
+ /** Base angular velocity scale for angleVBase */
42
+ private _angleVScale = 1;
43
+
44
+ /**
45
+ * Optional provider-specific animation state.
46
+ * Used by AnimatedTextureProvider (if present).
47
+ */
48
+ public animatedParticleState?: AnimatedParticleState;
49
+
50
+ constructor(options: ParticleOptions) {
51
+ // Center anchor by default
52
+ options.anchorX = 0.5;
53
+ options.anchorY = 0.5;
54
+
55
+ super(options);
56
+
57
+ // Start in pooled/inactive state.
58
+ this.onKill();
59
+ }
60
+
61
+ public set angleVBase(base: number) {
62
+ this._angleVBase = base;
63
+ }
64
+
65
+ public set angleVScale(scale: number) {
66
+ this._angleVScale = scale;
67
+ }
68
+
69
+ public get angleV(): number {
70
+ return this._angleVBase * this._angleVScale;
71
+ }
72
+
73
+ /**
74
+ * Resets the particle into pooled (inactive) state.
75
+ *
76
+ * Called when:
77
+ * - The particle exceeds its lifetime
78
+ * - The emitter clears particles
79
+ *
80
+ * This must leave the particle in a clean state so it can safely be reused.
81
+ */
82
+ public onKill(): void {
83
+ this.age = 0;
84
+ this.life = 1;
85
+
86
+ this.vx = 0;
87
+ this.vy = 0;
88
+ this._angleVBase = 0;
89
+ this._angleVScale = 1;
90
+
91
+ this.animatedParticleState = undefined;
92
+ }
93
+
94
+ /**
95
+ * Prepares the particle for active simulation.
96
+ *
97
+ * Called when:
98
+ * - The emitter spawns this particle from the pool
99
+ *
100
+ * Resets all visual and simulation state to defaults.
101
+ * Behaviours will then modify properties as needed.
102
+ */
103
+ public onSpawn(): void {
104
+ this.age = 0;
105
+ this.life = 1;
106
+
107
+ this.vx = 0;
108
+ this.vy = 0;
109
+ this._angleVBase = 0;
110
+ this._angleVScale = 1;
111
+
112
+ this.animatedParticleState = undefined;
113
+
114
+ // Reset visual state
115
+ this.alpha = 1;
116
+ this.tint = 0xffffff;
117
+
118
+ this.rotation = 0;
119
+ this.scaleX = 1;
120
+ this.scaleY = 1;
121
+
122
+ this.x = 0;
123
+ this.y = 0;
124
+ }
125
+ }