minimojs 1.0.0-alpha.12 → 1.0.0-alpha.14

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.
@@ -1,4 +1,4 @@
1
- const DEFAULT_OPTIONS = {
1
+ const DEFAULT_EXPLODE_OPTIONS = {
2
2
  rows: 3,
3
3
  cols: 3,
4
4
  durationMs: 500,
@@ -8,43 +8,299 @@ const DEFAULT_OPTIONS = {
8
8
  fade: true,
9
9
  destroySprite: true,
10
10
  };
11
+ const DEFAULT_ASSEMBLE_OPTIONS = {
12
+ rows: 3,
13
+ cols: 3,
14
+ durationMs: 500,
15
+ speed: 260,
16
+ gravityY: 900,
17
+ spin: 220,
18
+ fade: true,
19
+ };
20
+ const DEFAULT_DISINTEGRATE_OPTIONS = {
21
+ rows: 4,
22
+ cols: 4,
23
+ durationMs: 420,
24
+ direction: "left-to-right",
25
+ distance: 42,
26
+ spin: 120,
27
+ fade: true,
28
+ destroySprite: true,
29
+ };
30
+ const DEFAULT_INTEGRATE_OPTIONS = {
31
+ rows: 4,
32
+ cols: 4,
33
+ durationMs: 420,
34
+ direction: "left-to-right",
35
+ distance: 42,
36
+ spin: 120,
37
+ fade: true,
38
+ };
39
+ const DEFAULT_WIPE_OPTIONS = {
40
+ mode: "reveal",
41
+ direction: "left-to-right",
42
+ durationMs: 360,
43
+ destroySprite: true,
44
+ };
11
45
  /** @internal */
12
46
  export class ExplosionSystem {
13
47
  constructor() {
14
- this._explosions = [];
48
+ this._pieceEffects = [];
49
+ this._wipes = [];
15
50
  }
16
51
  animateExplode(sprite, glyphCanvas, options, onComplete) {
17
- this.clearSpriteExplosions(sprite);
18
- const rows = this.sanitizeCount(options?.rows, DEFAULT_OPTIONS.rows);
19
- const cols = this.sanitizeCount(options?.cols, DEFAULT_OPTIONS.cols);
20
- const durationMs = this.sanitizeDuration(options?.durationMs, DEFAULT_OPTIONS.durationMs);
21
- const speed = this.sanitizeNonNegative(options?.speed, DEFAULT_OPTIONS.speed);
22
- const gravityY = this.sanitizeNumber(options?.gravityY, DEFAULT_OPTIONS.gravityY);
23
- const spin = this.sanitizeNonNegative(options?.spin, DEFAULT_OPTIONS.spin);
24
- const fade = options?.fade ?? DEFAULT_OPTIONS.fade;
25
- const destroySprite = options?.destroySprite ?? DEFAULT_OPTIONS.destroySprite;
26
- const originalVisible = sprite.visible;
27
- const renderData = sprite._renderData;
28
- const deformScaleX = this.getSafeScale(renderData?.scaleX);
29
- const deformScaleY = this.getSafeScale(renderData?.scaleY);
30
- const pivotX = this.getSafePivot(renderData?.pivotX);
31
- const pivotY = this.getSafePivot(renderData?.pivotY);
32
- const offsetX = this.getSafeNumber(renderData?.offsetX, 0);
33
- const offsetY = this.getSafeNumber(renderData?.offsetY, 0);
34
- const alphaMultiplier = this.getSafeNumber(renderData?.alphaMultiplier, 1);
35
- const baseDisplayWidth = sprite.displayWidth;
36
- const baseDisplayHeight = sprite.displayHeight;
37
- const pivotOffsetX = (pivotX - 0.5) * baseDisplayWidth * (1 - deformScaleX);
38
- const pivotOffsetY = (pivotY - 0.5) * baseDisplayHeight * (1 - deformScaleY);
39
- const signedScaleX = this.getSafeScale(sprite.scale) * deformScaleX * (sprite.flipX ? -1 : 1);
40
- const signedScaleY = this.getSafeScale(sprite.scale) * deformScaleY * (sprite.flipY ? -1 : 1);
41
- const absScaleX = Math.abs(signedScaleX);
42
- const absScaleY = Math.abs(signedScaleY);
43
- const rotationRad = (sprite.rotation * Math.PI) / 180;
44
- const baseX = sprite.renderX + pivotOffsetX + offsetX;
45
- const baseY = sprite.renderY + pivotOffsetY + offsetY;
46
- const baseAlpha = Math.max(0, Math.min(1, sprite.alpha * alphaMultiplier));
52
+ const context = this.buildPieceSpriteContext(sprite, glyphCanvas);
53
+ this.spawnRadialEffect("explode", sprite, glyphCanvas, context, {
54
+ rows: this.sanitizeCount(options?.rows, DEFAULT_EXPLODE_OPTIONS.rows),
55
+ cols: this.sanitizeCount(options?.cols, DEFAULT_EXPLODE_OPTIONS.cols),
56
+ durationMs: this.sanitizeDuration(options?.durationMs, DEFAULT_EXPLODE_OPTIONS.durationMs),
57
+ speed: this.sanitizeNonNegative(options?.speed, DEFAULT_EXPLODE_OPTIONS.speed),
58
+ gravityY: this.sanitizeNumber(options?.gravityY, DEFAULT_EXPLODE_OPTIONS.gravityY),
59
+ spin: this.sanitizeNonNegative(options?.spin, DEFAULT_EXPLODE_OPTIONS.spin),
60
+ fade: options?.fade ?? DEFAULT_EXPLODE_OPTIONS.fade,
61
+ }, false, options?.destroySprite === false, options?.destroySprite === false ? sprite.visible : false, onComplete);
62
+ }
63
+ animateAssemble(sprite, glyphCanvas, options, onComplete) {
64
+ const context = this.buildPieceSpriteContext(sprite, glyphCanvas);
65
+ this.spawnRadialEffect("assemble", sprite, glyphCanvas, context, {
66
+ rows: this.sanitizeCount(options?.rows, DEFAULT_ASSEMBLE_OPTIONS.rows),
67
+ cols: this.sanitizeCount(options?.cols, DEFAULT_ASSEMBLE_OPTIONS.cols),
68
+ durationMs: this.sanitizeDuration(options?.durationMs, DEFAULT_ASSEMBLE_OPTIONS.durationMs),
69
+ speed: this.sanitizeNonNegative(options?.speed, DEFAULT_ASSEMBLE_OPTIONS.speed),
70
+ gravityY: this.sanitizeNumber(options?.gravityY, DEFAULT_ASSEMBLE_OPTIONS.gravityY),
71
+ spin: this.sanitizeNonNegative(options?.spin, DEFAULT_ASSEMBLE_OPTIONS.spin),
72
+ fade: options?.fade ?? DEFAULT_ASSEMBLE_OPTIONS.fade,
73
+ }, true, true, true, onComplete);
74
+ }
75
+ animateDisintegrate(sprite, glyphCanvas, options, onComplete) {
76
+ const context = this.buildPieceSpriteContext(sprite, glyphCanvas);
77
+ this.spawnDirectionalEffect("disintegrate", sprite, glyphCanvas, context, {
78
+ rows: this.sanitizeCount(options?.rows, DEFAULT_DISINTEGRATE_OPTIONS.rows),
79
+ cols: this.sanitizeCount(options?.cols, DEFAULT_DISINTEGRATE_OPTIONS.cols),
80
+ durationMs: this.sanitizeDuration(options?.durationMs, DEFAULT_DISINTEGRATE_OPTIONS.durationMs),
81
+ direction: options?.direction ?? DEFAULT_DISINTEGRATE_OPTIONS.direction,
82
+ distance: this.sanitizeNonNegative(options?.distance, DEFAULT_DISINTEGRATE_OPTIONS.distance),
83
+ spin: this.sanitizeNonNegative(options?.spin, DEFAULT_DISINTEGRATE_OPTIONS.spin),
84
+ fade: options?.fade ?? DEFAULT_DISINTEGRATE_OPTIONS.fade,
85
+ }, false, options?.destroySprite === false, false, onComplete);
86
+ }
87
+ animateIntegrate(sprite, glyphCanvas, options, onComplete) {
88
+ const context = this.buildPieceSpriteContext(sprite, glyphCanvas);
89
+ this.spawnDirectionalEffect("integrate", sprite, glyphCanvas, context, {
90
+ rows: this.sanitizeCount(options?.rows, DEFAULT_INTEGRATE_OPTIONS.rows),
91
+ cols: this.sanitizeCount(options?.cols, DEFAULT_INTEGRATE_OPTIONS.cols),
92
+ durationMs: this.sanitizeDuration(options?.durationMs, DEFAULT_INTEGRATE_OPTIONS.durationMs),
93
+ direction: options?.direction ?? DEFAULT_INTEGRATE_OPTIONS.direction,
94
+ distance: this.sanitizeNonNegative(options?.distance, DEFAULT_INTEGRATE_OPTIONS.distance),
95
+ spin: this.sanitizeNonNegative(options?.spin, DEFAULT_INTEGRATE_OPTIONS.spin),
96
+ fade: options?.fade ?? DEFAULT_INTEGRATE_OPTIONS.fade,
97
+ }, true, true, true, onComplete);
98
+ }
99
+ animateWipe(sprite, snapshot, options, onComplete) {
100
+ this.clearSpriteEffects(sprite);
101
+ const mode = options?.mode ?? DEFAULT_WIPE_OPTIONS.mode;
102
+ const durationMs = this.sanitizeDuration(options?.durationMs, DEFAULT_WIPE_OPTIONS.durationMs);
103
+ const direction = options?.direction ?? DEFAULT_WIPE_OPTIONS.direction;
104
+ const destroySprite = options?.destroySprite ?? DEFAULT_WIPE_OPTIONS.destroySprite;
105
+ const restoreVisibility = mode === "reveal";
106
+ sprite.visible = false;
107
+ this._wipes.push({
108
+ sprite,
109
+ canvas: snapshot.canvas,
110
+ x: snapshot.x,
111
+ y: snapshot.y,
112
+ rotation: snapshot.rotation,
113
+ alpha: snapshot.alpha,
114
+ flipX: snapshot.flipX,
115
+ flipY: snapshot.flipY,
116
+ drawWidth: snapshot.drawWidth,
117
+ drawHeight: snapshot.drawHeight,
118
+ layer: snapshot.layer,
119
+ ignoreScroll: snapshot.ignoreScroll,
120
+ mode,
121
+ direction,
122
+ progress: mode === "reveal" ? 0 : 1,
123
+ elapsedMs: 0,
124
+ durationMs,
125
+ restoreVisibility,
126
+ finalVisible: mode === "reveal",
127
+ onComplete,
128
+ });
129
+ }
130
+ update(_dt, dtMs) {
131
+ const pieceToRemove = [];
132
+ for (let i = 0; i < this._pieceEffects.length; i++) {
133
+ const effect = this._pieceEffects[i];
134
+ effect.elapsedMs += dtMs;
135
+ const t = Math.min(effect.elapsedMs / effect.durationMs, 1);
136
+ for (const piece of effect.pieces) {
137
+ const localT = t <= piece.delayT ? 0 : Math.min(1, (t - piece.delayT) / (1 - piece.delayT));
138
+ const eased = this.easePieceProgress(effect.mode, localT);
139
+ piece.x = this.lerp(piece.startX, piece.endX, eased);
140
+ piece.y = this.lerp(piece.startY, piece.endY, eased);
141
+ piece.rotation = this.lerp(piece.startRotation, piece.endRotation, eased);
142
+ piece.alpha = this.lerp(piece.startAlpha, piece.endAlpha, eased);
143
+ }
144
+ if (t >= 1) {
145
+ pieceToRemove.push(i);
146
+ if (effect.restoreVisibility) {
147
+ effect.sprite.visible = effect.finalVisible;
148
+ }
149
+ effect.onComplete?.();
150
+ }
151
+ }
152
+ for (let i = pieceToRemove.length - 1; i >= 0; i--) {
153
+ this._pieceEffects.splice(pieceToRemove[i], 1);
154
+ }
155
+ const wipeToRemove = [];
156
+ for (let i = 0; i < this._wipes.length; i++) {
157
+ const wipe = this._wipes[i];
158
+ wipe.elapsedMs += dtMs;
159
+ const t = Math.min(wipe.elapsedMs / wipe.durationMs, 1);
160
+ wipe.progress = wipe.mode === "reveal" ? t : 1 - t;
161
+ if (t >= 1) {
162
+ wipeToRemove.push(i);
163
+ if (wipe.restoreVisibility) {
164
+ wipe.sprite.visible = wipe.finalVisible;
165
+ }
166
+ wipe.onComplete?.();
167
+ }
168
+ }
169
+ for (let i = wipeToRemove.length - 1; i >= 0; i--) {
170
+ this._wipes.splice(wipeToRemove[i], 1);
171
+ }
172
+ }
173
+ getRenderEntries() {
174
+ return this._pieceEffects;
175
+ }
176
+ getWipeEntries() {
177
+ return this._wipes;
178
+ }
179
+ clearAll() {
180
+ for (const effect of this._pieceEffects) {
181
+ if (effect.restoreVisibility) {
182
+ effect.sprite.visible = effect.finalVisible;
183
+ }
184
+ }
185
+ for (const wipe of this._wipes) {
186
+ if (wipe.restoreVisibility) {
187
+ wipe.sprite.visible = wipe.finalVisible;
188
+ }
189
+ }
190
+ this._pieceEffects = [];
191
+ this._wipes = [];
192
+ }
193
+ spawnRadialEffect(mode, sprite, glyphCanvas, context, options, forceHideSprite, restoreVisibility, finalVisible, onComplete) {
194
+ this.clearSpriteEffects(sprite);
195
+ if (forceHideSprite || restoreVisibility) {
196
+ sprite.visible = false;
197
+ }
198
+ const durationSeconds = options.durationMs / 1000;
199
+ const pieces = this.buildPieceGrid(glyphCanvas, context, options.rows, options.cols, (_row, _col, sx, sy, sw, sh, centerX, centerY) => {
200
+ const velocity = this.getRadialVelocity(centerX, centerY);
201
+ const distance = options.speed * durationSeconds * (0.65 + Math.random() * 0.7);
202
+ const targetX = context.baseX + centerX + velocity.x * distance;
203
+ const targetY = context.baseY +
204
+ centerY +
205
+ velocity.y * distance +
206
+ 0.5 * options.gravityY * durationSeconds * durationSeconds;
207
+ const assembledX = context.baseX + centerX;
208
+ const assembledY = context.baseY + centerY;
209
+ const startRotation = mode === "explode"
210
+ ? context.rotation
211
+ : context.rotation + (Math.random() * 2 - 1) * options.spin * durationSeconds;
212
+ const endRotation = mode === "explode"
213
+ ? context.rotation + (Math.random() * 2 - 1) * options.spin * durationSeconds
214
+ : context.rotation;
215
+ return {
216
+ sx,
217
+ sy,
218
+ sw,
219
+ sh,
220
+ assembledX,
221
+ assembledY,
222
+ offsetX: targetX - assembledX,
223
+ offsetY: targetY - assembledY,
224
+ startRotation,
225
+ endRotation,
226
+ delayT: 0,
227
+ fade: options.fade,
228
+ };
229
+ }, mode);
230
+ this._pieceEffects.push({
231
+ sprite,
232
+ canvas: glyphCanvas,
233
+ layer: context.layer,
234
+ ignoreScroll: context.ignoreScroll,
235
+ pieces,
236
+ elapsedMs: 0,
237
+ durationMs: options.durationMs,
238
+ mode,
239
+ restoreVisibility,
240
+ finalVisible,
241
+ onComplete,
242
+ });
243
+ }
244
+ spawnDirectionalEffect(mode, sprite, glyphCanvas, context, options, forceHideSprite, restoreVisibility, finalVisible, onComplete) {
245
+ this.clearSpriteEffects(sprite);
246
+ if (forceHideSprite || restoreVisibility) {
247
+ sprite.visible = false;
248
+ }
249
+ const durationSeconds = options.durationMs / 1000;
250
+ const maxDelayT = 0.58;
251
+ const inPlace = options.direction === "in-place";
252
+ const directionalFlow = inPlace
253
+ ? "left-to-right"
254
+ : options.direction;
255
+ const directionVector = inPlace ? { x: 0, y: 0 } : this.getDirectionVector(directionalFlow);
256
+ const perpendicular = inPlace ? { x: 0, y: 0 } : { x: -directionVector.y, y: directionVector.x };
257
+ const pieces = this.buildPieceGrid(glyphCanvas, context, options.rows, options.cols, (row, col, sx, sy, sw, sh, centerX, centerY) => {
258
+ const baseDistance = inPlace ? 0 : options.distance * (0.45 + Math.random() * 0.75);
259
+ const sidewaysDistance = inPlace ? 0 : options.distance * 0.22 * (Math.random() * 2 - 1);
260
+ const offsetX = directionVector.x * baseDistance + perpendicular.x * sidewaysDistance;
261
+ const offsetY = directionVector.y * baseDistance + perpendicular.y * sidewaysDistance;
262
+ const assembledX = context.baseX + centerX;
263
+ const assembledY = context.baseY + centerY;
264
+ const delayT = inPlace
265
+ ? Math.random() * (maxDelayT + 0.05)
266
+ : this.getDirectionalProgress(row, col, options.rows, options.cols, directionalFlow) *
267
+ maxDelayT +
268
+ Math.random() * 0.05;
269
+ const rotationOffset = inPlace
270
+ ? 0
271
+ : (Math.random() * 2 - 1) * options.spin * durationSeconds;
272
+ return {
273
+ sx,
274
+ sy,
275
+ sw,
276
+ sh,
277
+ assembledX,
278
+ assembledY,
279
+ offsetX,
280
+ offsetY,
281
+ startRotation: mode === "disintegrate" ? context.rotation : context.rotation + rotationOffset,
282
+ endRotation: mode === "disintegrate" ? context.rotation + rotationOffset : context.rotation,
283
+ delayT: Math.min(maxDelayT + 0.05, delayT),
284
+ fade: options.fade,
285
+ };
286
+ }, mode);
287
+ this._pieceEffects.push({
288
+ sprite,
289
+ canvas: glyphCanvas,
290
+ layer: context.layer,
291
+ ignoreScroll: context.ignoreScroll,
292
+ pieces,
293
+ elapsedMs: 0,
294
+ durationMs: options.durationMs,
295
+ mode,
296
+ restoreVisibility,
297
+ finalVisible,
298
+ onComplete,
299
+ });
300
+ }
301
+ buildPieceGrid(glyphCanvas, context, rows, cols, builder, mode) {
47
302
  const pieces = [];
303
+ const rotationRad = (context.rotation * Math.PI) / 180;
48
304
  for (let row = 0; row < rows; row++) {
49
305
  const sy = Math.floor((row * glyphCanvas.height) / rows);
50
306
  const nextSy = Math.floor(((row + 1) * glyphCanvas.height) / rows);
@@ -53,102 +309,142 @@ export class ExplosionSystem {
53
309
  const sx = Math.floor((col * glyphCanvas.width) / cols);
54
310
  const nextSx = Math.floor(((col + 1) * glyphCanvas.width) / cols);
55
311
  const sw = Math.max(1, nextSx - sx);
56
- const localX = (sx + sw / 2 - glyphCanvas.width / 2) * signedScaleX;
57
- const localY = (sy + sh / 2 - glyphCanvas.height / 2) * signedScaleY;
312
+ const localX = (sx + sw / 2 - glyphCanvas.width / 2) * context.signedScaleX;
313
+ const localY = (sy + sh / 2 - glyphCanvas.height / 2) * context.signedScaleY;
58
314
  const rotated = this.rotate(localX, localY, rotationRad);
59
- const velocity = this.getPieceVelocity(rotated.x, rotated.y, speed);
315
+ const generated = builder(row, col, sx, sy, sw, sh, rotated.x, rotated.y);
316
+ const startX = mode === "explode" || mode === "disintegrate"
317
+ ? generated.assembledX
318
+ : generated.assembledX + generated.offsetX;
319
+ const startY = mode === "explode" || mode === "disintegrate"
320
+ ? generated.assembledY
321
+ : generated.assembledY + generated.offsetY;
322
+ const endX = mode === "explode" || mode === "disintegrate"
323
+ ? generated.assembledX + generated.offsetX
324
+ : generated.assembledX;
325
+ const endY = mode === "explode" || mode === "disintegrate"
326
+ ? generated.assembledY + generated.offsetY
327
+ : generated.assembledY;
328
+ const startAlpha = mode === "explode" || mode === "disintegrate"
329
+ ? context.baseAlpha
330
+ : generated.fade
331
+ ? 0
332
+ : context.baseAlpha;
333
+ const endAlpha = mode === "explode" || mode === "disintegrate"
334
+ ? generated.fade
335
+ ? 0
336
+ : context.baseAlpha
337
+ : context.baseAlpha;
60
338
  pieces.push({
61
- x: baseX + rotated.x,
62
- y: baseY + rotated.y,
63
- rotation: sprite.rotation,
64
- alpha: baseAlpha,
65
- flipX: sprite.flipX,
66
- flipY: sprite.flipY,
67
- sx,
68
- sy,
69
- sw,
70
- sh,
71
- drawWidth: sw * absScaleX,
72
- drawHeight: sh * absScaleY,
73
- vx: velocity.x,
74
- vy: velocity.y,
75
- angularVelocity: (Math.random() * 2 - 1) * spin,
339
+ x: startX,
340
+ y: startY,
341
+ rotation: generated.startRotation,
342
+ alpha: startAlpha,
343
+ flipX: context.flipX,
344
+ flipY: context.flipY,
345
+ sx: generated.sx,
346
+ sy: generated.sy,
347
+ sw: generated.sw,
348
+ sh: generated.sh,
349
+ drawWidth: generated.sw * context.absScaleX,
350
+ drawHeight: generated.sh * context.absScaleY,
351
+ startX,
352
+ startY,
353
+ endX,
354
+ endY,
355
+ startRotation: generated.startRotation,
356
+ endRotation: generated.endRotation,
357
+ startAlpha,
358
+ endAlpha,
359
+ delayT: generated.delayT,
76
360
  });
77
361
  }
78
362
  }
79
- if (!destroySprite) {
80
- sprite.visible = false;
81
- }
82
- this._explosions.push({
83
- sprite,
84
- canvas: glyphCanvas,
363
+ return pieces;
364
+ }
365
+ buildPieceSpriteContext(sprite, glyphCanvas) {
366
+ const renderData = sprite._renderData;
367
+ const deformScaleX = this.getSafeScale(renderData?.scaleX);
368
+ const deformScaleY = this.getSafeScale(renderData?.scaleY);
369
+ const pivotX = this.getSafePivot(renderData?.pivotX);
370
+ const pivotY = this.getSafePivot(renderData?.pivotY);
371
+ const offsetX = this.getSafeNumber(renderData?.offsetX, 0);
372
+ const offsetY = this.getSafeNumber(renderData?.offsetY, 0);
373
+ const alphaMultiplier = this.getSafeNumber(renderData?.alphaMultiplier, 1);
374
+ const baseDisplayWidth = sprite.displayWidth;
375
+ const baseDisplayHeight = sprite.displayHeight;
376
+ const pivotOffsetX = (pivotX - 0.5) * baseDisplayWidth * (1 - deformScaleX);
377
+ const pivotOffsetY = (pivotY - 0.5) * baseDisplayHeight * (1 - deformScaleY);
378
+ const signedScaleX = this.getSafeScale(sprite.scale) * deformScaleX * (sprite.flipX ? -1 : 1);
379
+ const signedScaleY = this.getSafeScale(sprite.scale) * deformScaleY * (sprite.flipY ? -1 : 1);
380
+ return {
85
381
  layer: sprite.layer,
86
382
  ignoreScroll: sprite.ignoreScroll,
87
- pieces,
88
- elapsedMs: 0,
89
- durationMs,
90
- gravityY,
91
- fade,
92
- baseAlpha,
93
- restoreVisibility: !destroySprite,
94
- originalVisible,
95
- onComplete,
96
- });
383
+ baseX: sprite.renderX + pivotOffsetX + offsetX,
384
+ baseY: sprite.renderY + pivotOffsetY + offsetY,
385
+ rotation: sprite.rotation,
386
+ baseAlpha: Math.max(0, Math.min(1, sprite.alpha * alphaMultiplier)),
387
+ flipX: sprite.flipX,
388
+ flipY: sprite.flipY,
389
+ signedScaleX,
390
+ signedScaleY,
391
+ absScaleX: Math.abs(signedScaleX),
392
+ absScaleY: Math.abs(signedScaleY),
393
+ };
97
394
  }
98
- update(dt, dtMs) {
99
- const explosions = this._explosions;
100
- const toRemove = [];
101
- for (let i = 0; i < explosions.length; i++) {
102
- const explosion = explosions[i];
103
- explosion.elapsedMs += dtMs;
104
- const t = Math.min(explosion.elapsedMs / explosion.durationMs, 1);
105
- for (const piece of explosion.pieces) {
106
- piece.vy += explosion.gravityY * dt;
107
- piece.x += piece.vx * dt;
108
- piece.y += piece.vy * dt;
109
- piece.rotation += piece.angularVelocity * dt;
110
- piece.alpha = explosion.fade
111
- ? explosion.baseAlpha * (1 - t)
112
- : explosion.baseAlpha;
113
- }
114
- if (t >= 1) {
115
- toRemove.push(i);
116
- if (explosion.restoreVisibility) {
117
- explosion.sprite.visible = explosion.originalVisible;
395
+ clearSpriteEffects(sprite) {
396
+ const remainingPieces = [];
397
+ for (const effect of this._pieceEffects) {
398
+ if (effect.sprite === sprite) {
399
+ if (effect.restoreVisibility) {
400
+ effect.sprite.visible = effect.finalVisible;
118
401
  }
119
- explosion.onComplete?.();
120
402
  }
121
- }
122
- for (let i = toRemove.length - 1; i >= 0; i--) {
123
- explosions.splice(toRemove[i], 1);
124
- }
125
- }
126
- getRenderEntries() {
127
- return this._explosions;
128
- }
129
- clearAll() {
130
- for (const explosion of this._explosions) {
131
- if (explosion.restoreVisibility) {
132
- explosion.sprite.visible = explosion.originalVisible;
403
+ else {
404
+ remainingPieces.push(effect);
133
405
  }
134
406
  }
135
- this._explosions = [];
136
- }
137
- clearSpriteExplosions(sprite) {
138
- const remaining = [];
139
- for (const explosion of this._explosions) {
140
- if (explosion.sprite === sprite) {
141
- if (explosion.restoreVisibility) {
142
- explosion.sprite.visible = explosion.originalVisible;
407
+ this._pieceEffects = remainingPieces;
408
+ const remainingWipes = [];
409
+ for (const wipe of this._wipes) {
410
+ if (wipe.sprite === sprite) {
411
+ if (wipe.restoreVisibility) {
412
+ wipe.sprite.visible = wipe.finalVisible;
143
413
  }
144
414
  }
145
415
  else {
146
- remaining.push(explosion);
416
+ remainingWipes.push(wipe);
147
417
  }
148
418
  }
149
- this._explosions = remaining;
419
+ this._wipes = remainingWipes;
420
+ }
421
+ getDirectionalProgress(row, col, rows, cols, direction) {
422
+ const rowProgress = rows <= 1 ? 0 : row / (rows - 1);
423
+ const colProgress = cols <= 1 ? 0 : col / (cols - 1);
424
+ switch (direction) {
425
+ case "left-to-right":
426
+ return colProgress;
427
+ case "right-to-left":
428
+ return 1 - colProgress;
429
+ case "top-to-bottom":
430
+ return rowProgress;
431
+ case "bottom-to-top":
432
+ return 1 - rowProgress;
433
+ }
150
434
  }
151
- getPieceVelocity(x, y, speed) {
435
+ getDirectionVector(direction) {
436
+ switch (direction) {
437
+ case "left-to-right":
438
+ return { x: 1, y: 0 };
439
+ case "right-to-left":
440
+ return { x: -1, y: 0 };
441
+ case "top-to-bottom":
442
+ return { x: 0, y: 1 };
443
+ case "bottom-to-top":
444
+ return { x: 0, y: -1 };
445
+ }
446
+ }
447
+ getRadialVelocity(x, y) {
152
448
  const magnitude = Math.hypot(x, y);
153
449
  let dirX = 0;
154
450
  let dirY = 0;
@@ -162,12 +458,18 @@ export class ExplosionSystem {
162
458
  dirY = Math.sin(angle);
163
459
  }
164
460
  const jitterAngle = (Math.random() - 0.5) * (Math.PI / 2);
165
- const jittered = this.rotate(dirX, dirY, jitterAngle);
166
- const pieceSpeed = speed * (0.65 + Math.random() * 0.7);
167
- return {
168
- x: jittered.x * pieceSpeed,
169
- y: jittered.y * pieceSpeed,
170
- };
461
+ return this.rotate(dirX, dirY, jitterAngle);
462
+ }
463
+ easePieceProgress(mode, t) {
464
+ const clamped = Math.max(0, Math.min(1, t));
465
+ switch (mode) {
466
+ case "explode":
467
+ case "disintegrate":
468
+ return 1 - (1 - clamped) * (1 - clamped);
469
+ case "assemble":
470
+ case "integrate":
471
+ return clamped * clamped * (3 - 2 * clamped);
472
+ }
171
473
  }
172
474
  rotate(x, y, radians) {
173
475
  const cos = Math.cos(radians);
@@ -177,6 +479,9 @@ export class ExplosionSystem {
177
479
  y: x * sin + y * cos,
178
480
  };
179
481
  }
482
+ lerp(from, to, t) {
483
+ return from + (to - from) * t;
484
+ }
180
485
  getSafeScale(value) {
181
486
  if (!Number.isFinite(value))
182
487
  return 1;
@@ -27,6 +27,9 @@ export class LoopSystem {
27
27
  set onCreate(callback) {
28
28
  this._onCreate = callback;
29
29
  }
30
+ get isRunning() {
31
+ return this._running;
32
+ }
30
33
  start() {
31
34
  if (this._running)
32
35
  return;