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.
|
@@ -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.
|
|
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",
|