fireworks-engine 1.0.0 → 1.0.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 +31 -0
- package/index.d.ts +14 -2
- package/package.json +2 -2
- package/src/burstEvents.js +19 -0
- package/src/burstSpawn.js +34 -8
- package/src/presets.js +11 -2
- package/src/renderer.js +3 -0
- package/src/rocketLaunch.js +6 -1
- package/src/runtime.js +9 -4
- package/src/runtimeLifecycle.js +2 -0
- package/src/update.js +3 -0
package/README.md
CHANGED
|
@@ -52,6 +52,37 @@ Always call `destroy()` when the owning UI unmounts.
|
|
|
52
52
|
fireworks.destroy();
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Burst notifications
|
|
56
|
+
|
|
57
|
+
Pass `onBurst` when creating the engine to synchronize another effect with each
|
|
58
|
+
logical explosion:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const fireworks = createFireworksEngine({
|
|
62
|
+
onBurst: ({ x, y }) => {
|
|
63
|
+
// Trigger an already prepared effect at these canvas coordinates.
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The callback runs synchronously after the first visual component is created,
|
|
69
|
+
once per logical burst, including rocket detonations and cascade children.
|
|
70
|
+
Queued bursts notify on materialization, not enqueue, and particle batches,
|
|
71
|
+
trails, and later twinkles do not produce additional notifications. Flash-only
|
|
72
|
+
and twinkle-only bursts also notify; a twinkle cloud notifies when created even
|
|
73
|
+
if its twinkles appear later. Completely empty bursts do not notify. This is a
|
|
74
|
+
component-creation signal, not a pixel visibility or audio hardware timestamp.
|
|
75
|
+
|
|
76
|
+
`onBurst` is constructor-only: `start`, launch methods, and `updateOptions` neither
|
|
77
|
+
replace nor reset it. Exceptions in the callback are isolated from rendering.
|
|
78
|
+
Calling `stop()` or `destroy()` from the callback cancels the rest of that run;
|
|
79
|
+
starting a replacement effect leaves the new run intact. Ordinary additive
|
|
80
|
+
launches remain part of the current run. Call `destroy()` when the owner unmounts.
|
|
81
|
+
|
|
82
|
+
TypeScript exports `FireworksBurstEvent`, `FireworksBurstCallback`, and
|
|
83
|
+
`FireworksEngineCreationOptions`; existing `FireworksEngineOptions` still work
|
|
84
|
+
for both construction and start/update configuration.
|
|
85
|
+
|
|
55
86
|
## Public API
|
|
56
87
|
|
|
57
88
|
Primary exports:
|
package/index.d.ts
CHANGED
|
@@ -189,6 +189,18 @@ export interface FireworksRocketLaunchOptions extends FireworksBurstOverrideOpti
|
|
|
189
189
|
rocketTailEmbers?: FireworksRocketTailEmbersOptions;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
export interface FireworksBurstEvent {
|
|
193
|
+
x: number;
|
|
194
|
+
y: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type FireworksBurstCallback = (event: FireworksBurstEvent) => void;
|
|
198
|
+
|
|
199
|
+
/** Options accepted only when creating an engine. */
|
|
200
|
+
export interface FireworksEngineCreationOptions extends FireworksEngineOptions {
|
|
201
|
+
onBurst?: FireworksBurstCallback | null;
|
|
202
|
+
}
|
|
203
|
+
|
|
192
204
|
export interface FireworksEngineOptions extends FireworksRocketLaunchOptions {
|
|
193
205
|
canvas?: FireworksCanvasOptions;
|
|
194
206
|
runtime?: FireworksRuntimeOptions;
|
|
@@ -506,7 +518,7 @@ export interface FireworksBurstRequest {
|
|
|
506
518
|
export interface FireworksPresetConfig extends FireworksEngineOptions { }
|
|
507
519
|
|
|
508
520
|
export declare class FireworksEngine {
|
|
509
|
-
constructor(options?:
|
|
521
|
+
constructor(options?: FireworksEngineCreationOptions);
|
|
510
522
|
|
|
511
523
|
documentRef: Document | null | undefined;
|
|
512
524
|
windowRef: Window | null | undefined;
|
|
@@ -677,7 +689,7 @@ export interface FireworksRocketExplicitTarget {
|
|
|
677
689
|
* @param options Initial engine configuration.
|
|
678
690
|
* @returns A new fireworks engine instance.
|
|
679
691
|
*/
|
|
680
|
-
export declare function createFireworksEngine(options?:
|
|
692
|
+
export declare function createFireworksEngine(options?: FireworksEngineCreationOptions): FireworksEngine;
|
|
681
693
|
|
|
682
694
|
/**
|
|
683
695
|
* Resolves a preset name to a fully expanded engine configuration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fireworks-engine",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"license": "BSD-2-Clause",
|
|
5
5
|
"description": "Reusable canvas fireworks runtime package for standalone integrations and demos.",
|
|
6
6
|
"author": "coinexplain",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"smoke": "node --check src/index.js && node --input-type=module -e \"const mod = await import('./src/index.js'); if (typeof mod.createFireworksEngine !== 'function' || typeof mod.resolveFireworksEnginePreset !== 'function' || typeof mod.default !== 'function') throw new Error('package smoke failed'); console.log('package smoke ok');\"",
|
|
43
43
|
"typecheck": "tsc --noEmit --project tsconfig.types.json",
|
|
44
|
-
"test": "npm run smoke && npm run typecheck"
|
|
44
|
+
"test": "npm run smoke && npm run typecheck && node --test test/burst-events.test.js"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"typescript": "^5.9.3"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// A stop invalidates every stack/timer belonging to the previous run. Additive
|
|
2
|
+
// launches deliberately keep the generation, including cascade children.
|
|
3
|
+
export function isCurrentRun(engine, generation) {
|
|
4
|
+
return engine.runGeneration === generation;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createBurstEvent(engine, x, y) {
|
|
8
|
+
return { generation: engine.runGeneration, x, y, notified: false };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function notifyBurst(engine, burst) {
|
|
12
|
+
if (burst.notified || !isCurrentRun(engine, burst.generation)) return;
|
|
13
|
+
burst.notified = true;
|
|
14
|
+
try {
|
|
15
|
+
engine.onBurstCallback?.({ x: burst.x, y: burst.y });
|
|
16
|
+
} catch {
|
|
17
|
+
// Consumer code must not interrupt rendering or lifecycle cleanup.
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/burstSpawn.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createBurstEvent, isCurrentRun, notifyBurst } from './burstEvents.js';
|
|
1
2
|
import {
|
|
2
3
|
BURST_FLASH_ATTACK_UNIT,
|
|
3
4
|
DEFAULT_BURST_SINGLE_COLOR,
|
|
@@ -252,6 +253,17 @@ function createBurstRequests(x, y, options, flashOptions = options) {
|
|
|
252
253
|
return requests;
|
|
253
254
|
}
|
|
254
255
|
|
|
256
|
+
function spawnBurstExtras(engine, burst, flashOptions, cloudOptions) {
|
|
257
|
+
if (spawnBurstCenterFlash(engine, burst.x, burst.y, flashOptions)) {
|
|
258
|
+
notifyBurst(engine, burst);
|
|
259
|
+
}
|
|
260
|
+
if (!isCurrentRun(engine, burst.generation)) return false;
|
|
261
|
+
if (spawnBurstTwinkleCloud(engine, burst.x, burst.y, cloudOptions)) {
|
|
262
|
+
notifyBurst(engine, burst);
|
|
263
|
+
}
|
|
264
|
+
return isCurrentRun(engine, burst.generation);
|
|
265
|
+
}
|
|
266
|
+
|
|
255
267
|
function enqueueBurst(engine, x, y, options = {}) {
|
|
256
268
|
if (!engine.running) {
|
|
257
269
|
return false;
|
|
@@ -262,10 +274,11 @@ function enqueueBurst(engine, x, y, options = {}) {
|
|
|
262
274
|
resolveBurstCurrentConfig(engine),
|
|
263
275
|
);
|
|
264
276
|
const requests = createBurstRequests(x, y, burstOptions, options);
|
|
277
|
+
const burst = createBurstEvent(engine, x, y);
|
|
278
|
+
requests.forEach((request) => { request.burstEvent = burst; });
|
|
265
279
|
|
|
266
280
|
if (requests.length === 0) {
|
|
267
|
-
|
|
268
|
-
spawnBurstTwinkleCloud(engine, x, y, burstOptions);
|
|
281
|
+
spawnBurstExtras(engine, burst, options, burstOptions);
|
|
269
282
|
return true;
|
|
270
283
|
}
|
|
271
284
|
|
|
@@ -554,6 +567,7 @@ function spawnBurstParticles(engine, request, startIndex, count) {
|
|
|
554
567
|
}
|
|
555
568
|
|
|
556
569
|
export function drainBurstQueue(engine) {
|
|
570
|
+
const generation = engine.runGeneration;
|
|
557
571
|
let remainingBudget = engine.maxBurstParticlesPerFrame;
|
|
558
572
|
|
|
559
573
|
while (remainingBudget > 0 && engine.burstQueue.length > 0) {
|
|
@@ -567,14 +581,22 @@ export function drainBurstQueue(engine) {
|
|
|
567
581
|
|
|
568
582
|
const spawnCount = Math.min(remainingBudget, remainingParticles);
|
|
569
583
|
if (request.spawnBurstCenterFlash && !request.flashSpawned) {
|
|
570
|
-
spawnBurstCenterFlash(engine, request.x, request.y, request.flashOptions ?? request.options);
|
|
571
584
|
request.flashSpawned = true;
|
|
585
|
+
if (spawnBurstCenterFlash(engine, request.x, request.y, request.flashOptions ?? request.options)) {
|
|
586
|
+
notifyBurst(engine, request.burstEvent);
|
|
587
|
+
}
|
|
588
|
+
if (!isCurrentRun(engine, generation)) return;
|
|
572
589
|
}
|
|
573
590
|
if (request.spawnBurstTwinkleCloud && !request.twinkleCloudSpawned) {
|
|
574
|
-
spawnBurstTwinkleCloud(engine, request.x, request.y, request.options);
|
|
575
591
|
request.twinkleCloudSpawned = true;
|
|
592
|
+
if (spawnBurstTwinkleCloud(engine, request.x, request.y, request.options)) {
|
|
593
|
+
notifyBurst(engine, request.burstEvent);
|
|
594
|
+
}
|
|
595
|
+
if (!isCurrentRun(engine, generation)) return;
|
|
576
596
|
}
|
|
577
597
|
spawnBurstParticles(engine, request, request.nextIndex, spawnCount);
|
|
598
|
+
notifyBurst(engine, request.burstEvent);
|
|
599
|
+
if (!isCurrentRun(engine, generation)) return;
|
|
578
600
|
request.nextIndex += spawnCount;
|
|
579
601
|
remainingBudget -= spawnCount;
|
|
580
602
|
|
|
@@ -600,12 +622,14 @@ export function launchBurst(engine, x, y, options = {}) {
|
|
|
600
622
|
normalizedOptions,
|
|
601
623
|
resolveBurstCurrentConfig(engine),
|
|
602
624
|
);
|
|
603
|
-
|
|
604
|
-
|
|
625
|
+
const burst = createBurstEvent(engine, x, y);
|
|
626
|
+
if (!spawnBurstExtras(engine, burst, normalizedOptions, normalizedOptions)) return true;
|
|
605
627
|
const requests = createBurstRequests(x, y, burstOptions);
|
|
606
|
-
|
|
628
|
+
for (const request of requests) {
|
|
607
629
|
spawnBurstParticles(engine, request, 0, request.totalCount);
|
|
608
|
-
|
|
630
|
+
notifyBurst(engine, burst);
|
|
631
|
+
if (!isCurrentRun(engine, burst.generation)) return true;
|
|
632
|
+
}
|
|
609
633
|
return true;
|
|
610
634
|
}
|
|
611
635
|
|
|
@@ -669,6 +693,7 @@ function spawnBurstCenterFlash(engine, x, y, options = {}) {
|
|
|
669
693
|
color,
|
|
670
694
|
innerStop,
|
|
671
695
|
});
|
|
696
|
+
return true;
|
|
672
697
|
}
|
|
673
698
|
|
|
674
699
|
function spawnBurstTwinkleCloud(engine, x, y, options = {}) {
|
|
@@ -761,4 +786,5 @@ function spawnBurstTwinkleCloud(engine, x, y, options = {}) {
|
|
|
761
786
|
spawnSpread,
|
|
762
787
|
twinkles,
|
|
763
788
|
});
|
|
789
|
+
return true;
|
|
764
790
|
}
|
package/src/presets.js
CHANGED
|
@@ -346,6 +346,7 @@ function runAchievementUnlock(engine, targetX, targetY, options) {
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
function runSnowfall(engine, _targetX, _targetY, options) {
|
|
349
|
+
const generation = engine.runGeneration;
|
|
349
350
|
const flakesPerWave = 6;
|
|
350
351
|
const waveDelayMs = 1500;
|
|
351
352
|
|
|
@@ -355,6 +356,7 @@ function runSnowfall(engine, _targetX, _targetY, options) {
|
|
|
355
356
|
const x = randomRange(0, stageWidth);
|
|
356
357
|
const y = -10 - randomRange(0, 40);
|
|
357
358
|
engine.burst(x, y, options);
|
|
359
|
+
if (engine.runGeneration !== generation) return;
|
|
358
360
|
}
|
|
359
361
|
|
|
360
362
|
scheduleTimeout(engine, () => {
|
|
@@ -372,6 +374,7 @@ function runSnowfall(engine, _targetX, _targetY, options) {
|
|
|
372
374
|
}
|
|
373
375
|
|
|
374
376
|
function runLeafFall(engine, _targetX, _targetY, options) {
|
|
377
|
+
const generation = engine.runGeneration;
|
|
375
378
|
const leavesPerWave = 5;
|
|
376
379
|
const waveDelayMs = 1200;
|
|
377
380
|
|
|
@@ -381,6 +384,7 @@ function runLeafFall(engine, _targetX, _targetY, options) {
|
|
|
381
384
|
const x = randomRange(0, stageWidth);
|
|
382
385
|
const y = -16 - randomRange(0, 60);
|
|
383
386
|
engine.burst(x, y, options);
|
|
387
|
+
if (engine.runGeneration !== generation) return;
|
|
384
388
|
}
|
|
385
389
|
|
|
386
390
|
scheduleTimeout(engine, () => {
|
|
@@ -398,6 +402,7 @@ function runLeafFall(engine, _targetX, _targetY, options) {
|
|
|
398
402
|
}
|
|
399
403
|
|
|
400
404
|
function runAmbientSparkle(engine, targetX, targetY, options) {
|
|
405
|
+
const generation = engine.runGeneration;
|
|
401
406
|
const pulseDelayMs = 8000;
|
|
402
407
|
|
|
403
408
|
const launchPulse = () => {
|
|
@@ -405,6 +410,7 @@ function runAmbientSparkle(engine, targetX, targetY, options) {
|
|
|
405
410
|
const centerY = Number.isFinite(targetY) ? targetY : engine.height * 0.5;
|
|
406
411
|
|
|
407
412
|
engine.launchRocketAt(centerX, centerY, options);
|
|
413
|
+
if (engine.runGeneration !== generation) return;
|
|
408
414
|
engine.launchRocketAt(
|
|
409
415
|
centerX,
|
|
410
416
|
centerY,
|
|
@@ -415,6 +421,7 @@ function runAmbientSparkle(engine, targetX, targetY, options) {
|
|
|
415
421
|
}),
|
|
416
422
|
);
|
|
417
423
|
|
|
424
|
+
if (engine.runGeneration !== generation) return;
|
|
418
425
|
scheduleTimeout(engine, () => {
|
|
419
426
|
if (!engine.running) {
|
|
420
427
|
return;
|
|
@@ -430,6 +437,7 @@ function runAmbientSparkle(engine, targetX, targetY, options) {
|
|
|
430
437
|
}
|
|
431
438
|
|
|
432
439
|
function runConfettiPop(engine, _targetX, _targetY, options) {
|
|
440
|
+
const generation = engine.runGeneration;
|
|
433
441
|
const stageWidth = engine.width;
|
|
434
442
|
const stageHeight = engine.height;
|
|
435
443
|
const cannons = [
|
|
@@ -437,7 +445,7 @@ function runConfettiPop(engine, _targetX, _targetY, options) {
|
|
|
437
445
|
{ x: 1.01, y: 0.5, directionDeg: 135 },
|
|
438
446
|
];
|
|
439
447
|
|
|
440
|
-
|
|
448
|
+
for (const { x, y, directionDeg } of cannons) {
|
|
441
449
|
engine.confetti(
|
|
442
450
|
stageWidth * x,
|
|
443
451
|
stageHeight * y,
|
|
@@ -447,7 +455,8 @@ function runConfettiPop(engine, _targetX, _targetY, options) {
|
|
|
447
455
|
},
|
|
448
456
|
}),
|
|
449
457
|
);
|
|
450
|
-
|
|
458
|
+
if (engine.runGeneration !== generation) return true;
|
|
459
|
+
}
|
|
451
460
|
|
|
452
461
|
return true;
|
|
453
462
|
}
|
package/src/renderer.js
CHANGED
|
@@ -800,6 +800,7 @@ function drawFormParticle(engine, particle) {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
export function loop(engine, now) {
|
|
803
|
+
const generation = engine.runGeneration;
|
|
803
804
|
if (!engine.running || !engine.ctx) {
|
|
804
805
|
return;
|
|
805
806
|
}
|
|
@@ -817,6 +818,7 @@ export function loop(engine, now) {
|
|
|
817
818
|
|
|
818
819
|
ctx.clearRect(0, 0, engine.width, engine.height);
|
|
819
820
|
updateRockets(engine, dt);
|
|
821
|
+
if (engine.runGeneration !== generation) return;
|
|
820
822
|
updateParticles(engine, dt);
|
|
821
823
|
updateFlashes(engine, dt);
|
|
822
824
|
updateTwinkleClouds(engine, dt);
|
|
@@ -854,6 +856,7 @@ export function loop(engine, now) {
|
|
|
854
856
|
}
|
|
855
857
|
|
|
856
858
|
drainBurstQueue(engine);
|
|
859
|
+
if (engine.runGeneration !== generation) return;
|
|
857
860
|
clearRenderCachesWhenIdle(engine);
|
|
858
861
|
|
|
859
862
|
const requestAnimationFrameFn = getRequestAnimationFrame(engine);
|
package/src/rocketLaunch.js
CHANGED
|
@@ -317,6 +317,7 @@ function launchRocketWithExplicitTarget(engine, options, explicitTarget) {
|
|
|
317
317
|
return false;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
const generation = engine.runGeneration;
|
|
320
321
|
const wasRunning = engine.running;
|
|
321
322
|
if (!startLoop(engine)) {
|
|
322
323
|
return false;
|
|
@@ -324,7 +325,7 @@ function launchRocketWithExplicitTarget(engine, options, explicitTarget) {
|
|
|
324
325
|
|
|
325
326
|
createRocket(engine, options, explicitTarget);
|
|
326
327
|
|
|
327
|
-
if (!wasRunning) {
|
|
328
|
+
if (engine.runGeneration === generation && !wasRunning) {
|
|
328
329
|
engine.finishWhenDone();
|
|
329
330
|
}
|
|
330
331
|
|
|
@@ -622,10 +623,12 @@ function createRocket(engine, options = {}, explicitTarget = null) {
|
|
|
622
623
|
}
|
|
623
624
|
|
|
624
625
|
export function launchRocket(engine, options = {}) {
|
|
626
|
+
const generation = engine.runGeneration;
|
|
625
627
|
const count = options.count ?? 1;
|
|
626
628
|
|
|
627
629
|
for (let index = 0; index < count; index += 1) {
|
|
628
630
|
createRocket(engine, options);
|
|
631
|
+
if (engine.runGeneration !== generation) return;
|
|
629
632
|
}
|
|
630
633
|
}
|
|
631
634
|
|
|
@@ -659,6 +662,7 @@ export function launchRocketFromTo(engine, startX, startY, targetX, targetY, opt
|
|
|
659
662
|
}
|
|
660
663
|
|
|
661
664
|
export function scheduleCascadeBursts(engine, rocket) {
|
|
665
|
+
const generation = engine.runGeneration;
|
|
662
666
|
const cascade = rocket.cascade ?? {};
|
|
663
667
|
const cascadeMode = resolveRocketCascadeMode(cascade.mode);
|
|
664
668
|
const cascadeBurstCount = cascade.bursts ?? 0;
|
|
@@ -693,6 +697,7 @@ export function scheduleCascadeBursts(engine, rocket) {
|
|
|
693
697
|
distance,
|
|
694
698
|
);
|
|
695
699
|
launchRocketFromTo(engine, originX, originY, x, y, childLaunchOptions);
|
|
700
|
+
if (engine.runGeneration !== generation) return;
|
|
696
701
|
}
|
|
697
702
|
return;
|
|
698
703
|
}
|
package/src/runtime.js
CHANGED
|
@@ -285,6 +285,8 @@ function mergePresetLaunchOptions(presetOptions = {}, launchOverrides = {}) {
|
|
|
285
285
|
|
|
286
286
|
export class FireworksEngine {
|
|
287
287
|
constructor(options = {}) {
|
|
288
|
+
this.runGeneration = 0;
|
|
289
|
+
this.onBurstCallback = typeof options?.onBurst === 'function' ? options.onBurst : null;
|
|
288
290
|
const normalizedOptions = normalizeGroupedPublicOptions(options);
|
|
289
291
|
this.explicitLaunchOptions = extractExplicitLaunchOptions(options);
|
|
290
292
|
const engineConfig = buildEngineConfig(normalizedOptions);
|
|
@@ -420,6 +422,7 @@ export class FireworksEngine {
|
|
|
420
422
|
}
|
|
421
423
|
|
|
422
424
|
launchPresetAt(name, targetX, targetY, options = {}) {
|
|
425
|
+
const generation = this.runGeneration;
|
|
423
426
|
if (shouldSkipHiddenLaunch(this)) {
|
|
424
427
|
return false;
|
|
425
428
|
}
|
|
@@ -452,7 +455,7 @@ export class FireworksEngine {
|
|
|
452
455
|
? launchPreset.run(this, targetX, targetY, launchOptions)
|
|
453
456
|
: false;
|
|
454
457
|
launched = launched || nextLaunched;
|
|
455
|
-
if (!nextLaunched) {
|
|
458
|
+
if (this.runGeneration !== generation || !nextLaunched) {
|
|
456
459
|
break;
|
|
457
460
|
}
|
|
458
461
|
}
|
|
@@ -485,7 +488,7 @@ export class FireworksEngine {
|
|
|
485
488
|
for (let index = 0; index < count; index += 1) {
|
|
486
489
|
const nextLaunched = launchRocketAt(this, targetX, targetY, launchOptions);
|
|
487
490
|
launched = launched || nextLaunched;
|
|
488
|
-
if (!nextLaunched) {
|
|
491
|
+
if (this.runGeneration !== generation || !nextLaunched) {
|
|
489
492
|
break;
|
|
490
493
|
}
|
|
491
494
|
}
|
|
@@ -623,8 +626,9 @@ export class FireworksEngine {
|
|
|
623
626
|
// Note: Renderer logic is decoupled; use engine directly in renderer for performance.
|
|
624
627
|
|
|
625
628
|
finishWhenDone() {
|
|
629
|
+
const generation = this.runGeneration;
|
|
626
630
|
const check = () => {
|
|
627
|
-
if (!this.running) {
|
|
631
|
+
if (this.runGeneration !== generation || !this.running) {
|
|
628
632
|
return;
|
|
629
633
|
}
|
|
630
634
|
|
|
@@ -639,7 +643,7 @@ export class FireworksEngine {
|
|
|
639
643
|
this.finishLifecycleNotified = true;
|
|
640
644
|
invokeLifecycleHook(this.activeLifecycleHooks.onFinish, this);
|
|
641
645
|
}
|
|
642
|
-
this.stop(false);
|
|
646
|
+
if (this.runGeneration === generation) this.stop(false);
|
|
643
647
|
return;
|
|
644
648
|
}
|
|
645
649
|
|
|
@@ -728,6 +732,7 @@ export class FireworksEngine {
|
|
|
728
732
|
}
|
|
729
733
|
|
|
730
734
|
stop(clearCanvas = true) {
|
|
735
|
+
this.runGeneration += 1;
|
|
731
736
|
const wasRunning = this.running;
|
|
732
737
|
this.running = false;
|
|
733
738
|
lifecycle.clearPendingStart(this);
|
package/src/runtimeLifecycle.js
CHANGED
|
@@ -160,10 +160,12 @@ export function removeResizeObserver(engine) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
export function scheduleTimeout(engine, callback, delay = 0) {
|
|
163
|
+
const generation = engine.runGeneration;
|
|
163
164
|
const schedule = typeof engine.setTimeoutRef === 'function' ? engine.setTimeoutRef : setTimeout;
|
|
164
165
|
|
|
165
166
|
let timeoutId = null;
|
|
166
167
|
const runWhenVisible = () => {
|
|
168
|
+
if (engine.runGeneration !== generation) return;
|
|
167
169
|
if (timeoutId !== null) {
|
|
168
170
|
engine.timeoutIds.delete(timeoutId);
|
|
169
171
|
}
|
package/src/update.js
CHANGED
|
@@ -90,6 +90,7 @@ function randomBetween(min, max) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function updateRockets(engine, dt) {
|
|
93
|
+
const generation = engine.runGeneration;
|
|
93
94
|
const rockets = engine.rockets;
|
|
94
95
|
let writeIndex = 0;
|
|
95
96
|
|
|
@@ -124,7 +125,9 @@ export function updateRockets(engine, dt) {
|
|
|
124
125
|
rocket.x = burstX;
|
|
125
126
|
rocket.y = burstY;
|
|
126
127
|
launchBurst(engine, burstX, burstY, rocket.burst);
|
|
128
|
+
if (engine.runGeneration !== generation) return;
|
|
127
129
|
scheduleCascadeBursts(engine, rocket);
|
|
130
|
+
if (engine.runGeneration !== generation) return;
|
|
128
131
|
continue;
|
|
129
132
|
}
|
|
130
133
|
|