minimojs 1.0.0-alpha.13 → 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.
@@ -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;
@@ -3,6 +3,8 @@ export class RenderSystem {
3
3
  constructor() {
4
4
  this._surfaceCache = new Map();
5
5
  this._lastAppliedPageBackground = undefined;
6
+ this._transitionScratchCanvas = null;
7
+ this._transitionScratchContext = null;
6
8
  }
7
9
  clearSpriteCache() {
8
10
  this._surfaceCache.clear();
@@ -42,10 +44,57 @@ export class RenderSystem {
42
44
  };
43
45
  }
44
46
  render(options) {
47
+ this.applyPageBackground(options.pageBackground);
48
+ this.renderScene(options);
49
+ }
50
+ captureFrame(options) {
51
+ const snapshot = document.createElement("canvas");
52
+ snapshot.width = options.canvas.width;
53
+ snapshot.height = options.canvas.height;
54
+ const snapshotContext = snapshot.getContext("2d");
55
+ if (!snapshotContext) {
56
+ throw new Error("MinimoJS: Could not acquire a transition snapshot context.");
57
+ }
58
+ this.renderScene({
59
+ ...options,
60
+ canvas: snapshot,
61
+ context: snapshotContext,
62
+ });
63
+ return snapshot;
64
+ }
65
+ renderScreenTransition(options) {
66
+ const ctx = options.context;
67
+ const width = options.canvas.width;
68
+ const height = options.canvas.height;
69
+ const transition = options.transition;
70
+ const progress = this.easeTransitionProgress(transition.progress);
71
+ this.applyPageBackground(options.pageBackground);
72
+ ctx.clearRect(0, 0, width, height);
73
+ switch (transition.type) {
74
+ case "fade":
75
+ this.drawFadeTransition(ctx, transition, width, height, progress);
76
+ return;
77
+ case "wipe":
78
+ this.drawWipeTransition(ctx, transition, width, height, progress);
79
+ return;
80
+ case "slide":
81
+ this.drawSlideTransition(ctx, transition, width, height, progress);
82
+ return;
83
+ case "iris":
84
+ this.drawIrisTransition(ctx, transition, width, height, progress);
85
+ return;
86
+ case "pixelate":
87
+ this.drawPixelateTransition(ctx, transition, width, height, progress);
88
+ return;
89
+ case "flash":
90
+ this.drawFlashTransition(ctx, transition, width, height, progress);
91
+ return;
92
+ }
93
+ }
94
+ renderScene(options) {
45
95
  const ctx = options.context;
46
96
  const W = options.canvas.width;
47
97
  const H = options.canvas.height;
48
- this.applyPageBackground(options.pageBackground);
49
98
  this.paintCanvasBackground(ctx, W, H, options.background, options.backgroundGradient);
50
99
  const backgrounds = [...options.backgroundLayers].sort((a, b) => a.layer - b.layer);
51
100
  for (const layer of backgrounds) {
@@ -177,6 +226,73 @@ export class RenderSystem {
177
226
  ctx.strokeRect(barX, barY, barWidth, barHeight);
178
227
  ctx.restore();
179
228
  }
229
+ drawFadeTransition(ctx, transition, width, height, progress) {
230
+ const firstHalf = progress < 0.5;
231
+ const localT = firstHalf ? progress / 0.5 : (progress - 0.5) / 0.5;
232
+ ctx.drawImage(firstHalf ? transition.fromCanvas : transition.toCanvas, 0, 0, width, height);
233
+ ctx.save();
234
+ ctx.globalAlpha = firstHalf ? localT : 1 - localT;
235
+ ctx.fillStyle = transition.color;
236
+ ctx.fillRect(0, 0, width, height);
237
+ ctx.restore();
238
+ }
239
+ drawWipeTransition(ctx, transition, width, height, progress) {
240
+ ctx.drawImage(transition.fromCanvas, 0, 0, width, height);
241
+ const clip = this.getScreenWipeClipRect(width, height, transition.direction, progress);
242
+ if (clip.width <= 0 || clip.height <= 0)
243
+ return;
244
+ ctx.save();
245
+ ctx.beginPath();
246
+ ctx.rect(clip.x, clip.y, clip.width, clip.height);
247
+ ctx.clip();
248
+ ctx.drawImage(transition.toCanvas, 0, 0, width, height);
249
+ ctx.restore();
250
+ }
251
+ drawSlideTransition(ctx, transition, width, height, progress) {
252
+ const offset = this.getSlideOffset(transition.direction, width, height, progress);
253
+ ctx.drawImage(transition.fromCanvas, offset.fromX, offset.fromY, width, height);
254
+ ctx.drawImage(transition.toCanvas, offset.toX, offset.toY, width, height);
255
+ }
256
+ drawIrisTransition(ctx, transition, width, height, progress) {
257
+ ctx.drawImage(transition.fromCanvas, 0, 0, width, height);
258
+ const maxRadius = Math.max(Math.hypot(transition.centerX, transition.centerY), Math.hypot(width - transition.centerX, transition.centerY), Math.hypot(transition.centerX, height - transition.centerY), Math.hypot(width - transition.centerX, height - transition.centerY));
259
+ ctx.save();
260
+ ctx.beginPath();
261
+ ctx.arc(transition.centerX, transition.centerY, maxRadius * progress, 0, Math.PI * 2);
262
+ ctx.clip();
263
+ ctx.drawImage(transition.toCanvas, 0, 0, width, height);
264
+ ctx.restore();
265
+ }
266
+ drawPixelateTransition(ctx, transition, width, height, progress) {
267
+ const firstHalf = progress < 0.5;
268
+ const localT = firstHalf ? progress / 0.5 : (progress - 0.5) / 0.5;
269
+ const source = firstHalf ? transition.fromCanvas : transition.toCanvas;
270
+ const pixelSize = firstHalf
271
+ ? this.lerp(1, transition.pixelSize, localT)
272
+ : this.lerp(transition.pixelSize, 1, localT);
273
+ this.drawPixelatedCanvas(ctx, source, width, height, pixelSize);
274
+ }
275
+ drawFlashTransition(ctx, transition, width, height, progress) {
276
+ const swapPoint = 0.28;
277
+ const overlayPeak = 0.52;
278
+ const source = progress < swapPoint ? transition.fromCanvas : transition.toCanvas;
279
+ let alpha = 0;
280
+ if (progress < swapPoint) {
281
+ alpha = progress / Math.max(0.001, swapPoint);
282
+ }
283
+ else if (progress < overlayPeak) {
284
+ alpha = 1;
285
+ }
286
+ else {
287
+ alpha = 1 - (progress - overlayPeak) / Math.max(0.001, 1 - overlayPeak);
288
+ }
289
+ ctx.drawImage(source, 0, 0, width, height);
290
+ ctx.save();
291
+ ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
292
+ ctx.fillStyle = transition.color;
293
+ ctx.fillRect(0, 0, width, height);
294
+ ctx.restore();
295
+ }
180
296
  getRenderSurface(sprite) {
181
297
  const cacheKey = sprite.getRenderCacheKey();
182
298
  const cached = this._surfaceCache.get(cacheKey);
@@ -341,6 +457,42 @@ export class RenderSystem {
341
457
  }
342
458
  }
343
459
  }
460
+ getScreenWipeClipRect(width, height, direction, progress) {
461
+ switch (direction) {
462
+ case "left-to-right":
463
+ return {
464
+ x: 0,
465
+ y: 0,
466
+ width: width * progress,
467
+ height,
468
+ };
469
+ case "right-to-left": {
470
+ const clipWidth = width * progress;
471
+ return {
472
+ x: width - clipWidth,
473
+ y: 0,
474
+ width: clipWidth,
475
+ height,
476
+ };
477
+ }
478
+ case "top-to-bottom":
479
+ return {
480
+ x: 0,
481
+ y: 0,
482
+ width,
483
+ height: height * progress,
484
+ };
485
+ case "bottom-to-top": {
486
+ const clipHeight = height * progress;
487
+ return {
488
+ x: 0,
489
+ y: height - clipHeight,
490
+ width,
491
+ height: clipHeight,
492
+ };
493
+ }
494
+ }
495
+ }
344
496
  asEmojiSprite(sprite) {
345
497
  if ("sprite" in sprite && "size" in sprite) {
346
498
  return sprite;
@@ -501,6 +653,88 @@ export class RenderSystem {
501
653
  }
502
654
  return Math.max(scaleX, scaleY);
503
655
  }
656
+ easeTransitionProgress(t) {
657
+ const clamped = Math.max(0, Math.min(1, t));
658
+ return clamped * clamped * (3 - 2 * clamped);
659
+ }
660
+ getSlideOffset(direction, width, height, progress) {
661
+ switch (direction) {
662
+ case "left-to-right":
663
+ return {
664
+ fromX: width * progress,
665
+ fromY: 0,
666
+ toX: width * (progress - 1),
667
+ toY: 0,
668
+ };
669
+ case "right-to-left":
670
+ return {
671
+ fromX: -width * progress,
672
+ fromY: 0,
673
+ toX: width * (1 - progress),
674
+ toY: 0,
675
+ };
676
+ case "top-to-bottom":
677
+ return {
678
+ fromX: 0,
679
+ fromY: height * progress,
680
+ toX: 0,
681
+ toY: height * (progress - 1),
682
+ };
683
+ case "bottom-to-top":
684
+ return {
685
+ fromX: 0,
686
+ fromY: -height * progress,
687
+ toX: 0,
688
+ toY: height * (1 - progress),
689
+ };
690
+ }
691
+ }
692
+ drawPixelatedCanvas(ctx, source, width, height, pixelSize) {
693
+ const roundedPixelSize = Math.max(1, Math.round(pixelSize));
694
+ if (roundedPixelSize <= 1) {
695
+ ctx.drawImage(source, 0, 0, width, height);
696
+ return;
697
+ }
698
+ const scaledWidth = Math.max(1, Math.ceil(width / roundedPixelSize));
699
+ const scaledHeight = Math.max(1, Math.ceil(height / roundedPixelSize));
700
+ const scratch = this.getTransitionScratchSurface(scaledWidth, scaledHeight);
701
+ const scratchCtx = this._transitionScratchContext;
702
+ if (!scratchCtx) {
703
+ ctx.drawImage(source, 0, 0, width, height);
704
+ return;
705
+ }
706
+ scratch.width = scaledWidth;
707
+ scratch.height = scaledHeight;
708
+ scratchCtx.clearRect(0, 0, scaledWidth, scaledHeight);
709
+ scratchCtx.imageSmoothingEnabled = false;
710
+ scratchCtx.drawImage(source, 0, 0, scaledWidth, scaledHeight);
711
+ ctx.save();
712
+ ctx.imageSmoothingEnabled = false;
713
+ ctx.drawImage(scratch, 0, 0, scaledWidth, scaledHeight, 0, 0, width, height);
714
+ ctx.restore();
715
+ }
716
+ getTransitionScratchSurface(width, height) {
717
+ if (!this._transitionScratchCanvas) {
718
+ this._transitionScratchCanvas = document.createElement("canvas");
719
+ this._transitionScratchContext = this._transitionScratchCanvas.getContext("2d");
720
+ if (!this._transitionScratchContext) {
721
+ throw new Error("MinimoJS: Could not acquire a transition scratch context.");
722
+ }
723
+ }
724
+ if (this._transitionScratchCanvas.width !== width ||
725
+ this._transitionScratchCanvas.height !== height) {
726
+ this._transitionScratchCanvas.width = width;
727
+ this._transitionScratchCanvas.height = height;
728
+ this._transitionScratchContext = this._transitionScratchCanvas.getContext("2d");
729
+ if (!this._transitionScratchContext) {
730
+ throw new Error("MinimoJS: Could not acquire a transition scratch context.");
731
+ }
732
+ }
733
+ return this._transitionScratchCanvas;
734
+ }
735
+ lerp(from, to, t) {
736
+ return from + (to - from) * t;
737
+ }
504
738
  applyPageBackground(pageBackground) {
505
739
  if (!document.body)
506
740
  return;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ const DEFAULT_DURATION_MS = 420;
2
+ const DEFAULT_DIRECTION = "left-to-right";
3
+ const DEFAULT_PIXEL_SIZE = 28;
4
+ /** @internal */
5
+ export class TransitionSystem {
6
+ constructor() {
7
+ this._active = null;
8
+ }
9
+ get isActive() {
10
+ return this._active !== null;
11
+ }
12
+ start(fromCanvas, toCanvas, width, height, options, onComplete) {
13
+ const type = options.type;
14
+ const durationMs = this.sanitizeDuration(options.durationMs, DEFAULT_DURATION_MS);
15
+ const color = typeof options.color === "string" && options.color.trim()
16
+ ? options.color
17
+ : type === "flash"
18
+ ? "#ffffff"
19
+ : "#000000";
20
+ const centerX = this.sanitizeCoordinate(options.centerX, width / 2);
21
+ const centerY = this.sanitizeCoordinate(options.centerY, height / 2);
22
+ const pixelSize = this.sanitizePixelSize(options.pixelSize, DEFAULT_PIXEL_SIZE);
23
+ this._active = {
24
+ fromCanvas,
25
+ toCanvas,
26
+ type,
27
+ progress: 0,
28
+ color,
29
+ direction: options.direction ?? DEFAULT_DIRECTION,
30
+ centerX,
31
+ centerY,
32
+ pixelSize,
33
+ elapsedMs: 0,
34
+ durationMs,
35
+ onComplete,
36
+ };
37
+ }
38
+ update(dtMs) {
39
+ if (!this._active)
40
+ return;
41
+ this._active.elapsedMs += dtMs;
42
+ this._active.progress = Math.max(0, Math.min(1, this._active.elapsedMs / this._active.durationMs));
43
+ if (this._active.progress >= 1) {
44
+ const onComplete = this._active.onComplete;
45
+ this._active = null;
46
+ onComplete?.();
47
+ }
48
+ }
49
+ getRenderEntry() {
50
+ return this._active;
51
+ }
52
+ clear() {
53
+ this._active = null;
54
+ }
55
+ sanitizeDuration(value, fallback) {
56
+ if (!Number.isFinite(value))
57
+ return fallback;
58
+ return Math.max(1, value);
59
+ }
60
+ sanitizeCoordinate(value, fallback) {
61
+ if (!Number.isFinite(value))
62
+ return fallback;
63
+ return value;
64
+ }
65
+ sanitizePixelSize(value, fallback) {
66
+ if (!Number.isFinite(value))
67
+ return fallback;
68
+ return Math.max(2, Math.round(value));
69
+ }
70
+ }
package/dist/minimo.d.ts CHANGED
@@ -111,6 +111,10 @@ export interface IntegrateOptions {
111
111
  * Wipe mode used by {@link Game.animateWipe}.
112
112
  */
113
113
  export type WipeMode = "reveal" | "cover";
114
+ /**
115
+ * Full-screen transition type used by {@link Game.transitionTo}.
116
+ */
117
+ export type ScreenTransitionType = "fade" | "wipe" | "slide" | "iris" | "pixelate" | "flash";
114
118
  /**
115
119
  * Optional tuning values for {@link Game.animateWipe}.
116
120
  *
@@ -126,6 +130,29 @@ export interface WipeOptions {
126
130
  /** Whether the original sprite is destroyed for `mode: "cover"`. */
127
131
  destroySprite?: boolean;
128
132
  }
133
+ /**
134
+ * Optional tuning values for {@link Game.transitionTo}.
135
+ *
136
+ * MinimoJS scene transitions operate on frozen snapshots of the outgoing and
137
+ * incoming scenes. The target scene is created immediately, then both
138
+ * snapshots are composited for the duration of the effect.
139
+ */
140
+ export interface SceneTransitionOptions {
141
+ /** Required transition style. */
142
+ type: ScreenTransitionType;
143
+ /** Total transition duration in **milliseconds**. */
144
+ durationMs?: number;
145
+ /** Optional color used by `fade` and `flash`. */
146
+ color?: string;
147
+ /** Direction used by `wipe` and `slide`. */
148
+ direction?: FlowDirection;
149
+ /** Optional iris center X position in canvas pixels. */
150
+ centerX?: number;
151
+ /** Optional iris center Y position in canvas pixels. */
152
+ centerY?: number;
153
+ /** Maximum pixel block size used by `pixelate`. */
154
+ pixelSize?: number;
155
+ }
129
156
  /**
130
157
  * Optional tuning values for {@link Game.animateTrail}.
131
158
  *
@@ -1025,6 +1052,10 @@ export declare class Game {
1025
1052
  * Currently active scene object, if any.
1026
1053
  */
1027
1054
  get currentScene(): IScene | null;
1055
+ /**
1056
+ * Returns `true` while a full-screen scene transition is playing.
1057
+ */
1058
+ get isTransitioning(): boolean;
1028
1059
  /**
1029
1060
  * Creates a new MinimoJS game instance.
1030
1061
  *
@@ -2027,6 +2058,25 @@ export declare class Game {
2027
2058
  * ```
2028
2059
  */
2029
2060
  reset(scene?: IScene): void;
2061
+ /**
2062
+ * Changes to a new scene using a full-screen transition between frozen scene
2063
+ * snapshots.
2064
+ *
2065
+ * `transitionTo()` does not change the behavior of {@link Game.reset}. It
2066
+ * captures the current scene as a snapshot, rebuilds the target scene
2067
+ * immediately, captures the new scene as another snapshot, and then animates
2068
+ * between those two images for the requested duration.
2069
+ *
2070
+ * While the transition is active, gameplay updates are paused.
2071
+ *
2072
+ * If the game loop is not running yet, this falls back to an immediate
2073
+ * {@link Game.reset}.
2074
+ *
2075
+ * @param scene - Target scene to make active.
2076
+ * @param options - Transition style and optional tuning values.
2077
+ * @param onComplete - Optional callback invoked when the transition finishes.
2078
+ */
2079
+ transitionTo(scene: IScene, options: SceneTransitionOptions, onComplete?: () => void): void;
2030
2080
  /**
2031
2081
  * Starts the `requestAnimationFrame` game loop.
2032
2082
  * Safe to call multiple times — does nothing if already running.
package/dist/minimo.js CHANGED
@@ -20,6 +20,7 @@ import { SoundSystem } from "./internal/SoundSystem.js";
20
20
  import { SpriteSystem } from "./internal/SpriteSystem.js";
21
21
  import { TextSystem } from "./internal/TextSystem.js";
22
22
  import { TrailSystem } from "./internal/TrailSystem.js";
23
+ import { TransitionSystem } from "./internal/TransitionSystem.js";
23
24
  import { TimerSystem } from "./internal/TimerSystem.js";
24
25
  // ---------------------------------------------------------------------------
25
26
  // Sprite
@@ -972,6 +973,12 @@ export class Game {
972
973
  get currentScene() {
973
974
  return this._currentScene;
974
975
  }
976
+ /**
977
+ * Returns `true` while a full-screen scene transition is playing.
978
+ */
979
+ get isTransitioning() {
980
+ return this._transitionSystem.isActive;
981
+ }
975
982
  // -------------------------------------------------------------------------
976
983
  // Constructor
977
984
  // -------------------------------------------------------------------------
@@ -1120,6 +1127,7 @@ export class Game {
1120
1127
  this._renderSystem = new RenderSystem();
1121
1128
  this._explosionSystem = new ExplosionSystem();
1122
1129
  this._trailSystem = new TrailSystem();
1130
+ this._transitionSystem = new TransitionSystem();
1123
1131
  this._loopSystem = new LoopSystem(this._onLoopFrameCallback.bind(this));
1124
1132
  this._loopSystem.onCreate = this._invokeCreate.bind(this);
1125
1133
  this._inputSystem.bindInputEvents();
@@ -2291,6 +2299,7 @@ export class Game {
2291
2299
  if (scene !== undefined) {
2292
2300
  this._currentScene = scene;
2293
2301
  }
2302
+ this._transitionSystem.clear();
2294
2303
  this._backgroundSystem.clearAll();
2295
2304
  this._spriteSystem.clearAll();
2296
2305
  this._timerSystem.clearAll();
@@ -2303,6 +2312,38 @@ export class Game {
2303
2312
  this.scrollY = 0;
2304
2313
  this._loopSystem.invokeCreate();
2305
2314
  }
2315
+ /**
2316
+ * Changes to a new scene using a full-screen transition between frozen scene
2317
+ * snapshots.
2318
+ *
2319
+ * `transitionTo()` does not change the behavior of {@link Game.reset}. It
2320
+ * captures the current scene as a snapshot, rebuilds the target scene
2321
+ * immediately, captures the new scene as another snapshot, and then animates
2322
+ * between those two images for the requested duration.
2323
+ *
2324
+ * While the transition is active, gameplay updates are paused.
2325
+ *
2326
+ * If the game loop is not running yet, this falls back to an immediate
2327
+ * {@link Game.reset}.
2328
+ *
2329
+ * @param scene - Target scene to make active.
2330
+ * @param options - Transition style and optional tuning values.
2331
+ * @param onComplete - Optional callback invoked when the transition finishes.
2332
+ */
2333
+ transitionTo(scene, options, onComplete) {
2334
+ if (!this._loopSystem.isRunning || !this._hasCompletedPreload) {
2335
+ this.reset(scene);
2336
+ onComplete?.();
2337
+ return;
2338
+ }
2339
+ if (this._transitionSystem.isActive) {
2340
+ return;
2341
+ }
2342
+ const fromCanvas = this._captureFrameSnapshot();
2343
+ this.reset(scene);
2344
+ const toCanvas = this._captureFrameSnapshot();
2345
+ this._transitionSystem.start(fromCanvas, toCanvas, this.width, this.height, options, onComplete);
2346
+ }
2306
2347
  // -------------------------------------------------------------------------
2307
2348
  // Loop control
2308
2349
  // -------------------------------------------------------------------------
@@ -2417,14 +2458,44 @@ export class Game {
2417
2458
  // Private — rendering
2418
2459
  // -------------------------------------------------------------------------
2419
2460
  /** @internal */ _onLoopFrameCallback(dt, dtMs) {
2461
+ if (this._transitionSystem.isActive) {
2462
+ this._transitionSystem.update(dtMs);
2463
+ if (this._transitionSystem.isActive) {
2464
+ this._renderTransition();
2465
+ }
2466
+ else {
2467
+ this._render();
2468
+ }
2469
+ this._inputSystem.clearFramePressedState();
2470
+ this._textSystem.clear();
2471
+ return;
2472
+ }
2420
2473
  this._timerSystem.update(dtMs);
2474
+ if (this._transitionSystem.isActive) {
2475
+ this._renderTransition();
2476
+ this._inputSystem.clearFramePressedState();
2477
+ this._textSystem.clear();
2478
+ return;
2479
+ }
2421
2480
  this._animationSystem.update(dtMs);
2422
2481
  this._explosionSystem.update(dt, dtMs);
2423
2482
  this._physicsSystem.update(this._spriteSystem.getMutableSprites(), dt);
2483
+ if (this._transitionSystem.isActive) {
2484
+ this._renderTransition();
2485
+ this._inputSystem.clearFramePressedState();
2486
+ this._textSystem.clear();
2487
+ return;
2488
+ }
2424
2489
  if (this._currentScene?.onUpdate)
2425
2490
  this._currentScene.onUpdate(dt);
2426
2491
  else if (this.onUpdate)
2427
2492
  this.onUpdate(dt);
2493
+ if (this._transitionSystem.isActive) {
2494
+ this._renderTransition();
2495
+ this._inputSystem.clearFramePressedState();
2496
+ this._textSystem.clear();
2497
+ return;
2498
+ }
2428
2499
  this._trailSystem.update(dtMs, this._renderSystem.getSpriteRenderSnapshot.bind(this._renderSystem));
2429
2500
  this._render();
2430
2501
  this._inputSystem.clearFramePressedState();
@@ -2515,6 +2586,37 @@ export class Game {
2515
2586
  pageBackground: this.pageBackground,
2516
2587
  });
2517
2588
  }
2589
+ /** @internal */ _renderTransition() {
2590
+ const transition = this._transitionSystem.getRenderEntry();
2591
+ if (!transition) {
2592
+ this._render();
2593
+ return;
2594
+ }
2595
+ this._renderSystem.renderScreenTransition({
2596
+ canvas: this._canvas,
2597
+ context: this._ctx,
2598
+ pageBackground: this.pageBackground,
2599
+ transition,
2600
+ });
2601
+ }
2602
+ /** @internal */ _captureFrameSnapshot() {
2603
+ return this._renderSystem.captureFrame({
2604
+ canvas: this._canvas,
2605
+ context: this._ctx,
2606
+ backgroundLayers: this._backgroundSystem.getMutableLayers(),
2607
+ resolveImage: this._assetSystem.getImage.bind(this._assetSystem),
2608
+ sprites: this._spriteSystem.getMutableSprites(),
2609
+ trails: this._trailSystem.getRenderEntries(),
2610
+ explosions: this._explosionSystem.getRenderEntries(),
2611
+ wipes: this._explosionSystem.getWipeEntries(),
2612
+ textEntries: this._textSystem.getEntries(),
2613
+ scrollX: this.scrollX,
2614
+ scrollY: this.scrollY,
2615
+ background: this.background,
2616
+ backgroundGradient: this.backgroundGradient,
2617
+ pageBackground: this.pageBackground,
2618
+ });
2619
+ }
2518
2620
  /** @internal */
2519
2621
  _renderLoadingScreen(loaded, total, currentKey) {
2520
2622
  this._renderSystem.renderLoadingScreen({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimojs",
3
- "version": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.14",
4
4
  "description": "MinimoJS v1 — ultra-minimal, flat, deterministic 2D web game engine. Emoji-only sprites, rAF loop, TypeScript-first, LLM-friendly.",
5
5
  "type": "module",
6
6
  "main": "dist/minimo.js",