minimojs 1.0.0-alpha.2 → 1.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +82 -285
  2. package/dist/internal/AnimationSystem.js +345 -0
  3. package/dist/internal/AssetSystem.d.ts +1 -0
  4. package/dist/internal/AssetSystem.js +101 -0
  5. package/dist/internal/BackgroundSystem.d.ts +1 -0
  6. package/dist/internal/BackgroundSystem.js +26 -0
  7. package/dist/internal/CanvasSystem.d.ts +1 -0
  8. package/dist/internal/CanvasSystem.js +51 -0
  9. package/dist/internal/ExplosionSystem.d.ts +1 -0
  10. package/dist/internal/ExplosionSystem.js +540 -0
  11. package/dist/internal/InputSystem.d.ts +1 -0
  12. package/dist/internal/InputSystem.js +265 -0
  13. package/dist/internal/LoopSystem.d.ts +1 -0
  14. package/dist/internal/LoopSystem.js +61 -0
  15. package/dist/internal/PhysicsSystem.d.ts +1 -0
  16. package/dist/internal/PhysicsSystem.js +174 -0
  17. package/dist/internal/RenderSystem.d.ts +1 -0
  18. package/dist/internal/RenderSystem.js +910 -0
  19. package/dist/internal/SoundSystem.d.ts +1 -0
  20. package/dist/internal/SoundSystem.js +55 -0
  21. package/dist/internal/SpriteSystem.d.ts +1 -0
  22. package/dist/internal/SpriteSystem.js +32 -0
  23. package/dist/internal/TextSystem.d.ts +1 -0
  24. package/dist/internal/TextSystem.js +16 -0
  25. package/dist/internal/TimerSystem.d.ts +1 -0
  26. package/dist/internal/TimerSystem.js +43 -0
  27. package/dist/internal/TrailSystem.d.ts +1 -0
  28. package/dist/internal/TrailSystem.js +116 -0
  29. package/dist/internal/TransitionSystem.d.ts +1 -0
  30. package/dist/internal/TransitionSystem.js +74 -0
  31. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.d.ts +1 -0
  32. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.js +198 -0
  33. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.d.ts +1 -0
  34. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.js +120 -0
  35. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.d.ts +1 -0
  36. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.js +599 -0
  37. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.d.ts +1 -0
  38. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.js +13 -0
  39. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.d.ts +1 -0
  40. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.js +447 -0
  41. package/dist/minimo-arcaderacer.d.ts +1431 -0
  42. package/dist/minimo-arcaderacer.js +2060 -0
  43. package/dist/minimo.d.ts +1412 -162
  44. package/dist/minimo.js +2083 -816
  45. package/package.json +3 -2
  46. package/dist/animations.js +0 -30
  47. package/dist/audio.js +0 -17
  48. package/dist/game.js +0 -1105
  49. package/dist/input.js +0 -185
  50. package/dist/internal-types.js +0 -4
  51. package/dist/physics.js +0 -10
  52. package/dist/render.js +0 -75
  53. package/dist/sprite.js +0 -149
  54. package/dist/timers.js +0 -23
  55. /package/dist/{pointer-info.js → internal/AnimationSystem.d.ts} +0 -0
@@ -0,0 +1,910 @@
1
+ /** @internal */
2
+ export class RenderSystem {
3
+ constructor() {
4
+ /** @internal */
5
+ this._surfaceCache = new Map();
6
+ /** @internal */
7
+ this._lastAppliedPageBackground = undefined;
8
+ /** @internal */
9
+ this._transitionScratchCanvas = null;
10
+ /** @internal */
11
+ this._transitionScratchContext = null;
12
+ /** @internal */
13
+ this._dynamicSurfacePassId = 0;
14
+ }
15
+ clearSpriteCache() {
16
+ this._surfaceCache.clear();
17
+ }
18
+ beginDynamicSurfacePass() {
19
+ this._dynamicSurfacePassId += 1;
20
+ }
21
+ getSpriteGlyphCanvasForEffects(sprite) {
22
+ return this.getRenderSurface(sprite);
23
+ }
24
+ getSpriteRenderSnapshot(sprite) {
25
+ const canvas = this.getRenderSurface(sprite);
26
+ const renderData = sprite._renderData;
27
+ const deformScaleX = this.getSafeRenderScale(renderData?.scaleX);
28
+ const deformScaleY = this.getSafeRenderScale(renderData?.scaleY);
29
+ const pivotX = this.getSafePivot(renderData?.pivotX);
30
+ const pivotY = this.getSafePivot(renderData?.pivotY);
31
+ const offsetX = this.getSafeNumber(renderData?.offsetX, 0);
32
+ const offsetY = this.getSafeNumber(renderData?.offsetY, 0);
33
+ const alphaMultiplier = this.getSafeNumber(renderData?.alphaMultiplier, 1);
34
+ const safeRenderScale = this.getSafeRenderScale(sprite.scale);
35
+ const finalScaleX = safeRenderScale * deformScaleX;
36
+ const finalScaleY = safeRenderScale * deformScaleY;
37
+ const baseDisplayWidth = sprite.displayWidth;
38
+ const baseDisplayHeight = sprite.displayHeight;
39
+ const pivotOffsetX = (pivotX - 0.5) * baseDisplayWidth * (1 - deformScaleX);
40
+ const pivotOffsetY = (pivotY - 0.5) * baseDisplayHeight * (1 - deformScaleY);
41
+ return {
42
+ canvas,
43
+ x: sprite.renderX + pivotOffsetX + offsetX,
44
+ y: sprite.renderY + pivotOffsetY + offsetY,
45
+ rotation: sprite.rotation,
46
+ alpha: Math.max(0, Math.min(1, sprite.alpha * alphaMultiplier)),
47
+ flipX: sprite.flipX,
48
+ flipY: sprite.flipY,
49
+ drawWidth: canvas.width * Math.abs(finalScaleX),
50
+ drawHeight: canvas.height * Math.abs(finalScaleY),
51
+ layer: sprite.layer,
52
+ ignoreScroll: sprite.ignoreScroll,
53
+ };
54
+ }
55
+ render(options) {
56
+ this.applyPageBackground(options.pageBackground);
57
+ this.renderScene(options);
58
+ }
59
+ captureFrame(options) {
60
+ const snapshot = document.createElement("canvas");
61
+ snapshot.width = options.canvas.width;
62
+ snapshot.height = options.canvas.height;
63
+ const snapshotContext = snapshot.getContext("2d");
64
+ if (!snapshotContext) {
65
+ throw new Error("MinimoJS: Could not acquire a transition snapshot context.");
66
+ }
67
+ this.renderScene({
68
+ ...options,
69
+ canvas: snapshot,
70
+ context: snapshotContext,
71
+ });
72
+ return snapshot;
73
+ }
74
+ renderScreenTransition(options) {
75
+ const ctx = options.context;
76
+ const width = options.canvas.width;
77
+ const height = options.canvas.height;
78
+ const transition = options.transition;
79
+ const progress = this.easeTransitionProgress(transition.progress);
80
+ this.applyPageBackground(options.pageBackground);
81
+ ctx.clearRect(0, 0, width, height);
82
+ switch (transition.type) {
83
+ case "fade":
84
+ this.drawFadeTransition(ctx, transition, width, height, progress);
85
+ return;
86
+ case "wipe":
87
+ this.drawWipeTransition(ctx, transition, width, height, progress);
88
+ return;
89
+ case "slide":
90
+ this.drawSlideTransition(ctx, transition, width, height, progress);
91
+ return;
92
+ case "iris":
93
+ this.drawIrisTransition(ctx, transition, width, height, progress);
94
+ return;
95
+ case "pixelate":
96
+ this.drawPixelateTransition(ctx, transition, width, height, progress);
97
+ return;
98
+ case "flash":
99
+ this.drawFlashTransition(ctx, transition, width, height, progress);
100
+ return;
101
+ }
102
+ }
103
+ /** @internal */
104
+ renderScene(options) {
105
+ const ctx = options.context;
106
+ const W = options.canvas.width;
107
+ const H = options.canvas.height;
108
+ this.paintCanvasBackground(ctx, W, H, options.background, options.backgroundGradient);
109
+ const backgrounds = [...options.backgroundLayers].sort((a, b) => a.layer - b.layer);
110
+ for (const layer of backgrounds) {
111
+ if (!layer.visible)
112
+ continue;
113
+ const image = options.resolveImage(layer.imageKey);
114
+ if (!image)
115
+ continue;
116
+ this.drawBackgroundLayer(ctx, options.canvas, layer, image);
117
+ }
118
+ const sorted = [...options.sprites].sort((a, b) => a.layer - b.layer);
119
+ const trailEntries = [...options.trails].sort((a, b) => a.layer - b.layer);
120
+ for (const ghost of trailEntries) {
121
+ ctx.save();
122
+ ctx.globalAlpha = Math.max(0, Math.min(1, ghost.alpha));
123
+ const drawX = ghost.ignoreScroll ? ghost.x : ghost.x - options.scrollX;
124
+ const drawY = ghost.ignoreScroll ? ghost.y : ghost.y - options.scrollY;
125
+ ctx.translate(drawX, drawY);
126
+ if (ghost.rotation !== 0) {
127
+ ctx.rotate((ghost.rotation * Math.PI) / 180);
128
+ }
129
+ if (ghost.flipX || ghost.flipY) {
130
+ ctx.scale(ghost.flipX ? -1 : 1, ghost.flipY ? -1 : 1);
131
+ }
132
+ ctx.drawImage(ghost.canvas, -ghost.drawWidth / 2, -ghost.drawHeight / 2, ghost.drawWidth, ghost.drawHeight);
133
+ ctx.restore();
134
+ }
135
+ for (const sprite of sorted) {
136
+ if (!sprite.visible)
137
+ continue;
138
+ ctx.save();
139
+ const snapshot = this.getSpriteRenderSnapshot(sprite);
140
+ ctx.globalAlpha = snapshot.alpha;
141
+ const drawX = Math.round(snapshot.ignoreScroll ? snapshot.x : snapshot.x - options.scrollX);
142
+ const drawY = Math.round(snapshot.ignoreScroll ? snapshot.y : snapshot.y - options.scrollY);
143
+ ctx.translate(drawX, drawY);
144
+ if (snapshot.rotation !== 0) {
145
+ ctx.rotate((snapshot.rotation * Math.PI) / 180);
146
+ }
147
+ if (snapshot.flipX || snapshot.flipY) {
148
+ ctx.scale(snapshot.flipX ? -1 : 1, snapshot.flipY ? -1 : 1);
149
+ }
150
+ ctx.shadowColor = "transparent";
151
+ ctx.shadowBlur = 0;
152
+ ctx.shadowOffsetX = 0;
153
+ ctx.shadowOffsetY = 0;
154
+ ctx.drawImage(snapshot.canvas, -snapshot.drawWidth / 2, -snapshot.drawHeight / 2, snapshot.drawWidth, snapshot.drawHeight);
155
+ ctx.restore();
156
+ }
157
+ const wipeEntries = [...options.wipes].sort((a, b) => a.layer - b.layer);
158
+ for (const entry of wipeEntries) {
159
+ const visibleProgress = Math.max(0, Math.min(1, entry.progress));
160
+ if (visibleProgress <= 0)
161
+ continue;
162
+ ctx.save();
163
+ ctx.globalAlpha = entry.alpha;
164
+ const drawX = Math.round(entry.ignoreScroll ? entry.x : entry.x - options.scrollX);
165
+ const drawY = Math.round(entry.ignoreScroll ? entry.y : entry.y - options.scrollY);
166
+ ctx.translate(drawX, drawY);
167
+ if (entry.rotation !== 0) {
168
+ ctx.rotate((entry.rotation * Math.PI) / 180);
169
+ }
170
+ if (entry.flipX || entry.flipY) {
171
+ ctx.scale(entry.flipX ? -1 : 1, entry.flipY ? -1 : 1);
172
+ }
173
+ const clip = this.getWipeClipRect(entry.drawWidth, entry.drawHeight, entry.direction, visibleProgress);
174
+ if (clip.width > 0 && clip.height > 0) {
175
+ ctx.beginPath();
176
+ ctx.rect(clip.x, clip.y, clip.width, clip.height);
177
+ ctx.clip();
178
+ ctx.drawImage(entry.canvas, -entry.drawWidth / 2, -entry.drawHeight / 2, entry.drawWidth, entry.drawHeight);
179
+ }
180
+ ctx.restore();
181
+ }
182
+ const explosionEntries = [...options.explosions].sort((a, b) => a.layer - b.layer);
183
+ for (const entry of explosionEntries) {
184
+ for (const piece of entry.pieces) {
185
+ ctx.save();
186
+ ctx.globalAlpha = Math.max(0, Math.min(1, piece.alpha));
187
+ const drawX = entry.ignoreScroll ? piece.x : piece.x - options.scrollX;
188
+ const drawY = entry.ignoreScroll ? piece.y : piece.y - options.scrollY;
189
+ ctx.translate(drawX, drawY);
190
+ if (piece.rotation !== 0) {
191
+ ctx.rotate((piece.rotation * Math.PI) / 180);
192
+ }
193
+ if (piece.flipX || piece.flipY) {
194
+ ctx.scale(piece.flipX ? -1 : 1, piece.flipY ? -1 : 1);
195
+ }
196
+ ctx.drawImage(entry.canvas, piece.sx, piece.sy, piece.sw, piece.sh, -piece.drawWidth / 2, -piece.drawHeight / 2, piece.drawWidth, piece.drawHeight);
197
+ ctx.restore();
198
+ }
199
+ }
200
+ for (const entry of options.textEntries) {
201
+ ctx.save();
202
+ ctx.font = `${entry.fontSize}px "Press Start 2P", monospace`;
203
+ ctx.fillStyle = entry.color;
204
+ ctx.textAlign = entry.centered ? "center" : "left";
205
+ ctx.textBaseline = entry.centered ? "middle" : "top";
206
+ ctx.fillText(entry.text, entry.x, entry.y);
207
+ ctx.restore();
208
+ }
209
+ if (options.debugBodies) {
210
+ this.drawBodyDebugOverlay(ctx, sorted, options.scrollX, options.scrollY);
211
+ }
212
+ if (options.debugInputAreas) {
213
+ this.drawInputDebugOverlay(ctx, sorted, options.scrollX, options.scrollY);
214
+ }
215
+ }
216
+ /** @internal */
217
+ drawBodyDebugOverlay(ctx, sprites, scrollX, scrollY) {
218
+ ctx.save();
219
+ ctx.setLineDash([6, 4]);
220
+ ctx.lineWidth = 2;
221
+ for (const sprite of sprites) {
222
+ const halfWidth = sprite.bodyDisplayWidth / 2;
223
+ const halfHeight = sprite.bodyDisplayHeight / 2;
224
+ const centerX = sprite.ignoreScroll ? sprite.bodyCenterX : sprite.bodyCenterX - scrollX;
225
+ const centerY = sprite.ignoreScroll ? sprite.bodyCenterY : sprite.bodyCenterY - scrollY;
226
+ const left = Math.round(centerX - halfWidth) + 0.5;
227
+ const top = Math.round(centerY - halfHeight) + 0.5;
228
+ const width = Math.max(1, Math.round(sprite.bodyDisplayWidth) - 1);
229
+ const height = Math.max(1, Math.round(sprite.bodyDisplayHeight) - 1);
230
+ const stroke = sprite.isStatic
231
+ ? 'rgba(0, 217, 255, 0.95)'
232
+ : 'rgba(255, 77, 109, 0.95)';
233
+ const fill = sprite.isStatic
234
+ ? 'rgba(0, 217, 255, 0.12)'
235
+ : 'rgba(255, 77, 109, 0.12)';
236
+ ctx.strokeStyle = stroke;
237
+ ctx.fillStyle = fill;
238
+ ctx.fillRect(left, top, width, height);
239
+ ctx.strokeRect(left, top, width, height);
240
+ }
241
+ ctx.restore();
242
+ }
243
+ /** @internal */
244
+ drawInputDebugOverlay(ctx, sprites, scrollX, scrollY) {
245
+ ctx.save();
246
+ ctx.setLineDash([3, 5]);
247
+ ctx.lineWidth = 2;
248
+ for (const sprite of sprites) {
249
+ const halfWidth = sprite.displayWidth / 2;
250
+ const halfHeight = sprite.displayHeight / 2;
251
+ const centerX = sprite.ignoreScroll ? sprite.renderX : sprite.renderX - scrollX;
252
+ const centerY = sprite.ignoreScroll ? sprite.renderY : sprite.renderY - scrollY;
253
+ const left = Math.round(centerX - halfWidth) + 0.5;
254
+ const top = Math.round(centerY - halfHeight) + 0.5;
255
+ const width = Math.max(1, Math.round(sprite.displayWidth) - 1);
256
+ const height = Math.max(1, Math.round(sprite.displayHeight) - 1);
257
+ const stroke = "rgba(255, 196, 0, 0.98)";
258
+ const fill = "rgba(255, 196, 0, 0.10)";
259
+ ctx.strokeStyle = stroke;
260
+ ctx.fillStyle = fill;
261
+ ctx.fillRect(left, top, width, height);
262
+ ctx.strokeRect(left, top, width, height);
263
+ }
264
+ ctx.restore();
265
+ }
266
+ renderLoadingScreen(options) {
267
+ const ctx = options.context;
268
+ const W = options.canvas.width;
269
+ const H = options.canvas.height;
270
+ const safeTotal = Math.max(0, options.total);
271
+ const safeLoaded = Math.max(0, Math.min(options.loaded, safeTotal));
272
+ const progress = safeTotal === 0 ? 1 : safeLoaded / safeTotal;
273
+ const barWidth = Math.min(420, Math.max(220, Math.floor(W * 0.46)));
274
+ const barHeight = 18;
275
+ const barX = Math.round((W - barWidth) / 2);
276
+ const barY = Math.round(H / 2 + 24);
277
+ this.applyPageBackground(options.pageBackground);
278
+ this.paintCanvasBackground(ctx, W, H, options.background ?? "#101820", options.backgroundGradient);
279
+ ctx.save();
280
+ ctx.fillStyle = "rgba(8, 18, 28, 0.76)";
281
+ ctx.fillRect(0, 0, W, H);
282
+ ctx.font = `26px "Press Start 2P", monospace`;
283
+ ctx.textAlign = "center";
284
+ ctx.textBaseline = "middle";
285
+ ctx.fillStyle = "#ffffff";
286
+ ctx.fillText("Loading...", W / 2, H / 2 - 28);
287
+ ctx.fillStyle = "rgba(255, 255, 255, 0.16)";
288
+ ctx.fillRect(barX, barY, barWidth, barHeight);
289
+ ctx.fillStyle = "#f7fbff";
290
+ ctx.fillRect(barX, barY, Math.round(barWidth * progress), barHeight);
291
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.35)";
292
+ ctx.strokeRect(barX, barY, barWidth, barHeight);
293
+ ctx.restore();
294
+ }
295
+ /** @internal */
296
+ drawFadeTransition(ctx, transition, width, height, progress) {
297
+ const firstHalf = progress < 0.5;
298
+ const localT = firstHalf ? progress / 0.5 : (progress - 0.5) / 0.5;
299
+ ctx.drawImage(firstHalf ? transition.fromCanvas : transition.toCanvas, 0, 0, width, height);
300
+ ctx.save();
301
+ ctx.globalAlpha = firstHalf ? localT : 1 - localT;
302
+ ctx.fillStyle = transition.color;
303
+ ctx.fillRect(0, 0, width, height);
304
+ ctx.restore();
305
+ }
306
+ /** @internal */
307
+ drawWipeTransition(ctx, transition, width, height, progress) {
308
+ ctx.drawImage(transition.fromCanvas, 0, 0, width, height);
309
+ const clip = this.getScreenWipeClipRect(width, height, transition.direction, progress);
310
+ if (clip.width <= 0 || clip.height <= 0)
311
+ return;
312
+ ctx.save();
313
+ ctx.beginPath();
314
+ ctx.rect(clip.x, clip.y, clip.width, clip.height);
315
+ ctx.clip();
316
+ ctx.drawImage(transition.toCanvas, 0, 0, width, height);
317
+ ctx.restore();
318
+ }
319
+ /** @internal */
320
+ drawSlideTransition(ctx, transition, width, height, progress) {
321
+ const offset = this.getSlideOffset(transition.direction, width, height, progress);
322
+ ctx.drawImage(transition.fromCanvas, offset.fromX, offset.fromY, width, height);
323
+ ctx.drawImage(transition.toCanvas, offset.toX, offset.toY, width, height);
324
+ }
325
+ /** @internal */
326
+ drawIrisTransition(ctx, transition, width, height, progress) {
327
+ ctx.drawImage(transition.fromCanvas, 0, 0, width, height);
328
+ 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));
329
+ ctx.save();
330
+ ctx.beginPath();
331
+ ctx.arc(transition.centerX, transition.centerY, maxRadius * progress, 0, Math.PI * 2);
332
+ ctx.clip();
333
+ ctx.drawImage(transition.toCanvas, 0, 0, width, height);
334
+ ctx.restore();
335
+ }
336
+ /** @internal */
337
+ drawPixelateTransition(ctx, transition, width, height, progress) {
338
+ const firstHalf = progress < 0.5;
339
+ const localT = firstHalf ? progress / 0.5 : (progress - 0.5) / 0.5;
340
+ const source = firstHalf ? transition.fromCanvas : transition.toCanvas;
341
+ const pixelSize = firstHalf
342
+ ? this.lerp(1, transition.pixelSize, localT)
343
+ : this.lerp(transition.pixelSize, 1, localT);
344
+ this.drawPixelatedCanvas(ctx, source, width, height, pixelSize);
345
+ }
346
+ /** @internal */
347
+ drawFlashTransition(ctx, transition, width, height, progress) {
348
+ const swapPoint = 0.28;
349
+ const overlayPeak = 0.52;
350
+ const source = progress < swapPoint ? transition.fromCanvas : transition.toCanvas;
351
+ let alpha = 0;
352
+ if (progress < swapPoint) {
353
+ alpha = progress / Math.max(0.001, swapPoint);
354
+ }
355
+ else if (progress < overlayPeak) {
356
+ alpha = 1;
357
+ }
358
+ else {
359
+ alpha = 1 - (progress - overlayPeak) / Math.max(0.001, 1 - overlayPeak);
360
+ }
361
+ ctx.drawImage(source, 0, 0, width, height);
362
+ ctx.save();
363
+ ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
364
+ ctx.fillStyle = transition.color;
365
+ ctx.fillRect(0, 0, width, height);
366
+ ctx.restore();
367
+ }
368
+ /** @internal */
369
+ getRenderSurface(sprite) {
370
+ if (this.isDrawSprite(sprite)) {
371
+ return this.getDrawSurface(sprite);
372
+ }
373
+ const cacheKey = sprite.getRenderCacheKey();
374
+ const cached = this._surfaceCache.get(cacheKey);
375
+ if (cached) {
376
+ this._surfaceCache.delete(cacheKey);
377
+ this._surfaceCache.set(cacheKey, cached);
378
+ return cached;
379
+ }
380
+ const surface = this.isTextSprite(sprite)
381
+ ? this.createTextSurface(sprite)
382
+ : this.isImageSprite(sprite)
383
+ ? this.createImageSurface(sprite)
384
+ : this.createEmojiSurface(this.asEmojiSprite(sprite));
385
+ this._surfaceCache.set(cacheKey, surface);
386
+ this.trimSurfaceCache();
387
+ return surface;
388
+ }
389
+ /** @internal */
390
+ trimSurfaceCache() {
391
+ while (this._surfaceCache.size > RenderSystem.MAX_SURFACE_CACHE_ENTRIES) {
392
+ const oldestKey = this._surfaceCache.keys().next().value;
393
+ if (oldestKey === undefined) {
394
+ break;
395
+ }
396
+ this._surfaceCache.delete(oldestKey);
397
+ }
398
+ }
399
+ /** @internal */
400
+ createEmojiSurface(sprite) {
401
+ const size = Math.max(1, Math.round(sprite.size));
402
+ const color = sprite.color;
403
+ const glyphCanvas = document.createElement("canvas");
404
+ const boxSize = Math.max(2, Math.ceil(size * 2));
405
+ glyphCanvas.width = boxSize;
406
+ glyphCanvas.height = boxSize;
407
+ const glyphCtx = glyphCanvas.getContext("2d");
408
+ if (!glyphCtx) {
409
+ throw new Error("MinimoJS: Could not acquire a glyph rendering context.");
410
+ }
411
+ glyphCtx.clearRect(0, 0, boxSize, boxSize);
412
+ glyphCtx.shadowColor = "transparent";
413
+ glyphCtx.shadowBlur = 0;
414
+ glyphCtx.shadowOffsetX = 0;
415
+ glyphCtx.shadowOffsetY = 0;
416
+ glyphCtx.fillStyle = color;
417
+ glyphCtx.font = `${size}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
418
+ glyphCtx.textAlign = "center";
419
+ glyphCtx.textBaseline = "middle";
420
+ glyphCtx.fillText(sprite.sprite, boxSize / 2, boxSize / 2);
421
+ return glyphCanvas;
422
+ }
423
+ /** @internal */
424
+ createTextSurface(sprite) {
425
+ const canvas = document.createElement("canvas");
426
+ const ctx = canvas.getContext("2d");
427
+ if (!ctx) {
428
+ throw new Error("MinimoJS: Could not acquire a text rendering context.");
429
+ }
430
+ const lines = this.layoutTextLines(ctx, sprite);
431
+ const measuredWidth = Math.max(1, Math.ceil(lines.reduce((maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width), 0)));
432
+ const lineHeightPx = Math.max(1, Math.ceil(sprite.fontSize * sprite.lineHeight));
433
+ const contentHeight = Math.max(1, lineHeightPx * Math.max(lines.length, 1));
434
+ const borderPad = Math.max(0, sprite.borderWidth) * 2;
435
+ const strokePad = Math.max(0, sprite.strokeWidth) * 2;
436
+ const totalWidth = Math.max(1, Math.ceil(sprite.fixedWidth > 0
437
+ ? sprite.fixedWidth
438
+ : measuredWidth + sprite.paddingX * 2 + borderPad + strokePad));
439
+ const totalHeight = Math.max(1, Math.ceil(sprite.fixedHeight > 0
440
+ ? sprite.fixedHeight
441
+ : contentHeight + sprite.paddingY * 2 + borderPad + strokePad));
442
+ sprite._measuredWidth = totalWidth;
443
+ sprite._measuredHeight = totalHeight;
444
+ canvas.width = totalWidth;
445
+ canvas.height = totalHeight;
446
+ const drawCtx = canvas.getContext("2d");
447
+ if (!drawCtx) {
448
+ throw new Error("MinimoJS: Could not acquire a text rendering context.");
449
+ }
450
+ this.applyTextStyle(drawCtx, sprite);
451
+ if (sprite.backgroundColor !== null) {
452
+ drawCtx.fillStyle = sprite.backgroundColor;
453
+ this.fillRoundedRect(drawCtx, 0, 0, totalWidth, totalHeight, sprite.cornerRadius);
454
+ }
455
+ if (sprite.borderColor !== null && sprite.borderWidth > 0) {
456
+ drawCtx.strokeStyle = sprite.borderColor;
457
+ drawCtx.lineWidth = sprite.borderWidth;
458
+ this.strokeRoundedRect(drawCtx, sprite.borderWidth / 2, sprite.borderWidth / 2, totalWidth - sprite.borderWidth, totalHeight - sprite.borderWidth, sprite.cornerRadius);
459
+ }
460
+ drawCtx.fillStyle = sprite.color;
461
+ drawCtx.textAlign = sprite.textAlign;
462
+ drawCtx.textBaseline = "middle";
463
+ if (sprite.strokeColor !== null && sprite.strokeWidth > 0) {
464
+ drawCtx.strokeStyle = sprite.strokeColor;
465
+ drawCtx.lineWidth = sprite.strokeWidth;
466
+ drawCtx.lineJoin = "round";
467
+ drawCtx.miterLimit = 2;
468
+ }
469
+ const contentLeft = sprite.paddingX + sprite.borderWidth + sprite.strokeWidth;
470
+ const contentRight = totalWidth - sprite.paddingX - sprite.borderWidth - sprite.strokeWidth;
471
+ const contentCenter = totalWidth / 2;
472
+ const availableContentHeight = Math.max(1, totalHeight - sprite.paddingY * 2 - borderPad - strokePad);
473
+ const contentOffsetY = Math.max(0, Math.floor((availableContentHeight - contentHeight) / 2));
474
+ const startY = sprite.paddingY +
475
+ sprite.borderWidth +
476
+ sprite.strokeWidth +
477
+ contentOffsetY +
478
+ lineHeightPx / 2;
479
+ const drawX = sprite.textAlign === "left"
480
+ ? contentLeft
481
+ : sprite.textAlign === "right"
482
+ ? contentRight
483
+ : contentCenter;
484
+ for (let i = 0; i < lines.length; i++) {
485
+ if (sprite.strokeColor !== null && sprite.strokeWidth > 0) {
486
+ drawCtx.strokeText(lines[i], drawX, startY + i * lineHeightPx);
487
+ }
488
+ drawCtx.fillText(lines[i], drawX, startY + i * lineHeightPx);
489
+ }
490
+ return canvas;
491
+ }
492
+ /** @internal */
493
+ createImageSurface(sprite) {
494
+ const image = sprite.game?.getImage(sprite.imageKey);
495
+ if (!image) {
496
+ throw new Error(`MinimoJS: Image '${sprite.imageKey}' is not loaded.`);
497
+ }
498
+ const canvas = document.createElement("canvas");
499
+ canvas.width = Math.max(1, image instanceof HTMLImageElement ? image.naturalWidth || image.width : image.width);
500
+ canvas.height = Math.max(1, image instanceof HTMLImageElement ? image.naturalHeight || image.height : image.height);
501
+ const ctx = canvas.getContext("2d");
502
+ if (!ctx) {
503
+ throw new Error("MinimoJS: Could not acquire an image rendering context.");
504
+ }
505
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
506
+ ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
507
+ return canvas;
508
+ }
509
+ /** @internal */
510
+ getDrawSurface(sprite) {
511
+ const width = Math.max(1, Math.round(sprite.width));
512
+ const height = Math.max(1, Math.round(sprite.height));
513
+ let surface = sprite._surface;
514
+ if (!surface) {
515
+ surface = document.createElement("canvas");
516
+ sprite._surface = surface;
517
+ }
518
+ const sizeChanged = surface.width !== width || surface.height !== height;
519
+ if (sprite.frozen && !sizeChanged && surface.width > 0 && surface.height > 0) {
520
+ return surface;
521
+ }
522
+ if (!sprite.frozen &&
523
+ sprite._lastRedrawPassId === this._dynamicSurfacePassId &&
524
+ surface.width === width &&
525
+ surface.height === height) {
526
+ return surface;
527
+ }
528
+ if (sizeChanged) {
529
+ surface.width = width;
530
+ surface.height = height;
531
+ }
532
+ else {
533
+ // Resetting width clears pixels and restores the default 2D context state.
534
+ surface.width = width;
535
+ }
536
+ const ctx = surface.getContext("2d");
537
+ if (!ctx) {
538
+ throw new Error("MinimoJS: Could not acquire a DrawSprite rendering context.");
539
+ }
540
+ sprite._surfaceCtx = ctx;
541
+ sprite.redraw(ctx);
542
+ sprite._lastRedrawPassId = this._dynamicSurfacePassId;
543
+ return surface;
544
+ }
545
+ /** @internal */
546
+ isTextSprite(sprite) {
547
+ return "text" in sprite && "fontFamily" in sprite && "fontSize" in sprite;
548
+ }
549
+ /** @internal */
550
+ isDrawSprite(sprite) {
551
+ return "redraw" in sprite && "_surface" in sprite && "_surfaceCtx" in sprite;
552
+ }
553
+ /** @internal */
554
+ isImageSprite(sprite) {
555
+ return "imageKey" in sprite && "setTexture" in sprite;
556
+ }
557
+ /** @internal */
558
+ getWipeClipRect(drawWidth, drawHeight, direction, progress) {
559
+ const halfWidth = drawWidth / 2;
560
+ const halfHeight = drawHeight / 2;
561
+ switch (direction) {
562
+ case "left-to-right":
563
+ return {
564
+ x: -halfWidth,
565
+ y: -halfHeight,
566
+ width: drawWidth * progress,
567
+ height: drawHeight,
568
+ };
569
+ case "right-to-left": {
570
+ const width = drawWidth * progress;
571
+ return {
572
+ x: halfWidth - width,
573
+ y: -halfHeight,
574
+ width,
575
+ height: drawHeight,
576
+ };
577
+ }
578
+ case "top-to-bottom":
579
+ return {
580
+ x: -halfWidth,
581
+ y: -halfHeight,
582
+ width: drawWidth,
583
+ height: drawHeight * progress,
584
+ };
585
+ case "bottom-to-top": {
586
+ const height = drawHeight * progress;
587
+ return {
588
+ x: -halfWidth,
589
+ y: halfHeight - height,
590
+ width: drawWidth,
591
+ height,
592
+ };
593
+ }
594
+ }
595
+ }
596
+ /** @internal */
597
+ getScreenWipeClipRect(width, height, direction, progress) {
598
+ switch (direction) {
599
+ case "left-to-right":
600
+ return {
601
+ x: 0,
602
+ y: 0,
603
+ width: width * progress,
604
+ height,
605
+ };
606
+ case "right-to-left": {
607
+ const clipWidth = width * progress;
608
+ return {
609
+ x: width - clipWidth,
610
+ y: 0,
611
+ width: clipWidth,
612
+ height,
613
+ };
614
+ }
615
+ case "top-to-bottom":
616
+ return {
617
+ x: 0,
618
+ y: 0,
619
+ width,
620
+ height: height * progress,
621
+ };
622
+ case "bottom-to-top": {
623
+ const clipHeight = height * progress;
624
+ return {
625
+ x: 0,
626
+ y: height - clipHeight,
627
+ width,
628
+ height: clipHeight,
629
+ };
630
+ }
631
+ }
632
+ }
633
+ /** @internal */
634
+ asEmojiSprite(sprite) {
635
+ if ("sprite" in sprite && "size" in sprite) {
636
+ return sprite;
637
+ }
638
+ throw new Error("MinimoJS: Unsupported sprite type for emoji rendering.");
639
+ }
640
+ /** @internal */
641
+ layoutTextLines(ctx, sprite) {
642
+ this.applyTextStyle(ctx, sprite);
643
+ const rawLines = sprite.text.split("\n");
644
+ const availableTextWidth = this.getAvailableTextWidth(sprite);
645
+ if (availableTextWidth <= 0) {
646
+ return rawLines.length > 0 ? rawLines : [""];
647
+ }
648
+ const wrappedLines = [];
649
+ for (const rawLine of rawLines) {
650
+ const words = rawLine.split(/\s+/).filter((word) => word.length > 0);
651
+ if (words.length === 0) {
652
+ wrappedLines.push("");
653
+ continue;
654
+ }
655
+ let currentLine = words[0];
656
+ for (let i = 1; i < words.length; i++) {
657
+ const candidate = `${currentLine} ${words[i]}`;
658
+ if (ctx.measureText(candidate).width <= availableTextWidth) {
659
+ currentLine = candidate;
660
+ }
661
+ else {
662
+ wrappedLines.push(currentLine);
663
+ currentLine = words[i];
664
+ }
665
+ }
666
+ wrappedLines.push(currentLine);
667
+ }
668
+ return wrappedLines.length > 0 ? wrappedLines : [""];
669
+ }
670
+ /** @internal */
671
+ getAvailableTextWidth(sprite) {
672
+ const borderPad = Math.max(0, sprite.borderWidth) * 2;
673
+ const strokePad = Math.max(0, sprite.strokeWidth) * 2;
674
+ const limits = [];
675
+ if (sprite.maxWidth > 0) {
676
+ limits.push(sprite.maxWidth - sprite.paddingX * 2 - borderPad - strokePad);
677
+ }
678
+ if (sprite.fixedWidth > 0) {
679
+ limits.push(sprite.fixedWidth - sprite.paddingX * 2 - borderPad - strokePad);
680
+ }
681
+ if (limits.length === 0) {
682
+ return 0;
683
+ }
684
+ return Math.max(1, Math.floor(Math.min(...limits)));
685
+ }
686
+ /** @internal */
687
+ applyTextStyle(ctx, sprite) {
688
+ ctx.font = `${sprite.fontWeight} ${sprite.fontSize}px ${sprite.fontFamily}`;
689
+ ctx.shadowColor = "transparent";
690
+ ctx.shadowBlur = 0;
691
+ ctx.shadowOffsetX = 0;
692
+ ctx.shadowOffsetY = 0;
693
+ }
694
+ /** @internal */
695
+ fillRoundedRect(ctx, x, y, width, height, radius) {
696
+ this.buildRoundedRectPath(ctx, x, y, width, height, radius);
697
+ ctx.fill();
698
+ }
699
+ /** @internal */
700
+ strokeRoundedRect(ctx, x, y, width, height, radius) {
701
+ this.buildRoundedRectPath(ctx, x, y, width, height, radius);
702
+ ctx.stroke();
703
+ }
704
+ /** @internal */
705
+ buildRoundedRectPath(ctx, x, y, width, height, radius) {
706
+ const safeRadius = Math.max(0, Math.min(radius, width / 2, height / 2));
707
+ ctx.beginPath();
708
+ if (safeRadius <= 0) {
709
+ ctx.rect(x, y, width, height);
710
+ return;
711
+ }
712
+ ctx.moveTo(x + safeRadius, y);
713
+ ctx.lineTo(x + width - safeRadius, y);
714
+ ctx.quadraticCurveTo(x + width, y, x + width, y + safeRadius);
715
+ ctx.lineTo(x + width, y + height - safeRadius);
716
+ ctx.quadraticCurveTo(x + width, y + height, x + width - safeRadius, y + height);
717
+ ctx.lineTo(x + safeRadius, y + height);
718
+ ctx.quadraticCurveTo(x, y + height, x, y + height - safeRadius);
719
+ ctx.lineTo(x, y + safeRadius);
720
+ ctx.quadraticCurveTo(x, y, x + safeRadius, y);
721
+ ctx.closePath();
722
+ }
723
+ /** @internal */
724
+ getSafeRenderScale(value) {
725
+ if (!Number.isFinite(value))
726
+ return 1;
727
+ const safeValue = value;
728
+ return Math.max(0, safeValue);
729
+ }
730
+ /** @internal */
731
+ getSafePivot(value) {
732
+ if (!Number.isFinite(value))
733
+ return 0.5;
734
+ const safeValue = value;
735
+ return Math.max(0, Math.min(1, safeValue));
736
+ }
737
+ /** @internal */
738
+ getSafeNumber(value, fallback) {
739
+ if (!Number.isFinite(value))
740
+ return fallback;
741
+ return value;
742
+ }
743
+ /** @internal */
744
+ paintCanvasBackground(ctx, width, height, background, backgroundGradient) {
745
+ ctx.clearRect(0, 0, width, height);
746
+ if (backgroundGradient !== null) {
747
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
748
+ gradient.addColorStop(0, backgroundGradient.from);
749
+ gradient.addColorStop(1, backgroundGradient.to);
750
+ ctx.fillStyle = gradient;
751
+ ctx.fillRect(0, 0, width, height);
752
+ }
753
+ else if (background !== null) {
754
+ ctx.fillStyle = background;
755
+ ctx.fillRect(0, 0, width, height);
756
+ }
757
+ }
758
+ /** @internal */
759
+ drawBackgroundLayer(ctx, canvas, layer, image) {
760
+ const destX = Math.round(layer.x);
761
+ const destY = Math.round(layer.y);
762
+ const destW = this.getPositiveLength(layer.width, canvas.width);
763
+ const destH = this.getPositiveLength(layer.height, canvas.height);
764
+ const alpha = Math.max(0, Math.min(1, layer.alpha));
765
+ ctx.save();
766
+ ctx.globalAlpha = alpha;
767
+ if (layer.fit === "none") {
768
+ ctx.drawImage(image, destX, destY);
769
+ ctx.restore();
770
+ return;
771
+ }
772
+ if (layer.fit === "stretch") {
773
+ ctx.drawImage(image, destX, destY, destW, destH);
774
+ ctx.restore();
775
+ return;
776
+ }
777
+ const imageW = Math.max(1, image instanceof HTMLImageElement ? image.naturalWidth || image.width : image.width);
778
+ const imageH = Math.max(1, image instanceof HTMLImageElement ? image.naturalHeight || image.height : image.height);
779
+ const scale = this.getBackgroundScale(layer.fit, destW, destH, imageW, imageH);
780
+ const drawW = imageW * scale;
781
+ const drawH = imageH * scale;
782
+ const drawX = destX + (destW - drawW) / 2;
783
+ const drawY = destY + (destH - drawH) / 2;
784
+ if (layer.fit === "cover") {
785
+ ctx.beginPath();
786
+ ctx.rect(destX, destY, destW, destH);
787
+ ctx.clip();
788
+ }
789
+ ctx.drawImage(image, drawX, drawY, drawW, drawH);
790
+ ctx.restore();
791
+ }
792
+ /** @internal */
793
+ getPositiveLength(value, fallback) {
794
+ if (!Number.isFinite(value) || value <= 0)
795
+ return fallback;
796
+ return Math.round(value);
797
+ }
798
+ /** @internal */
799
+ getBackgroundScale(fit, destW, destH, imageW, imageH) {
800
+ const scaleX = destW / imageW;
801
+ const scaleY = destH / imageH;
802
+ if (fit === "contain") {
803
+ return Math.min(scaleX, scaleY);
804
+ }
805
+ return Math.max(scaleX, scaleY);
806
+ }
807
+ /** @internal */
808
+ easeTransitionProgress(t) {
809
+ const clamped = Math.max(0, Math.min(1, t));
810
+ return clamped * clamped * (3 - 2 * clamped);
811
+ }
812
+ /** @internal */
813
+ getSlideOffset(direction, width, height, progress) {
814
+ switch (direction) {
815
+ case "left-to-right":
816
+ return {
817
+ fromX: width * progress,
818
+ fromY: 0,
819
+ toX: width * (progress - 1),
820
+ toY: 0,
821
+ };
822
+ case "right-to-left":
823
+ return {
824
+ fromX: -width * progress,
825
+ fromY: 0,
826
+ toX: width * (1 - progress),
827
+ toY: 0,
828
+ };
829
+ case "top-to-bottom":
830
+ return {
831
+ fromX: 0,
832
+ fromY: height * progress,
833
+ toX: 0,
834
+ toY: height * (progress - 1),
835
+ };
836
+ case "bottom-to-top":
837
+ return {
838
+ fromX: 0,
839
+ fromY: -height * progress,
840
+ toX: 0,
841
+ toY: height * (1 - progress),
842
+ };
843
+ }
844
+ }
845
+ /** @internal */
846
+ drawPixelatedCanvas(ctx, source, width, height, pixelSize) {
847
+ const roundedPixelSize = Math.max(1, Math.round(pixelSize));
848
+ if (roundedPixelSize <= 1) {
849
+ ctx.drawImage(source, 0, 0, width, height);
850
+ return;
851
+ }
852
+ const scaledWidth = Math.max(1, Math.ceil(width / roundedPixelSize));
853
+ const scaledHeight = Math.max(1, Math.ceil(height / roundedPixelSize));
854
+ const scratch = this.getTransitionScratchSurface(scaledWidth, scaledHeight);
855
+ const scratchCtx = this._transitionScratchContext;
856
+ if (!scratchCtx) {
857
+ ctx.drawImage(source, 0, 0, width, height);
858
+ return;
859
+ }
860
+ scratch.width = scaledWidth;
861
+ scratch.height = scaledHeight;
862
+ scratchCtx.clearRect(0, 0, scaledWidth, scaledHeight);
863
+ scratchCtx.imageSmoothingEnabled = false;
864
+ scratchCtx.drawImage(source, 0, 0, scaledWidth, scaledHeight);
865
+ ctx.save();
866
+ ctx.imageSmoothingEnabled = false;
867
+ ctx.drawImage(scratch, 0, 0, scaledWidth, scaledHeight, 0, 0, width, height);
868
+ ctx.restore();
869
+ }
870
+ /** @internal */
871
+ getTransitionScratchSurface(width, height) {
872
+ if (!this._transitionScratchCanvas) {
873
+ this._transitionScratchCanvas = document.createElement("canvas");
874
+ this._transitionScratchContext = this._transitionScratchCanvas.getContext("2d");
875
+ if (!this._transitionScratchContext) {
876
+ throw new Error("MinimoJS: Could not acquire a transition scratch context.");
877
+ }
878
+ }
879
+ if (this._transitionScratchCanvas.width !== width ||
880
+ this._transitionScratchCanvas.height !== height) {
881
+ this._transitionScratchCanvas.width = width;
882
+ this._transitionScratchCanvas.height = height;
883
+ this._transitionScratchContext = this._transitionScratchCanvas.getContext("2d");
884
+ if (!this._transitionScratchContext) {
885
+ throw new Error("MinimoJS: Could not acquire a transition scratch context.");
886
+ }
887
+ }
888
+ return this._transitionScratchCanvas;
889
+ }
890
+ /** @internal */
891
+ lerp(from, to, t) {
892
+ return from + (to - from) * t;
893
+ }
894
+ /** @internal */
895
+ applyPageBackground(pageBackground) {
896
+ if (!document.body)
897
+ return;
898
+ if (this._lastAppliedPageBackground === pageBackground)
899
+ return;
900
+ if (pageBackground === null) {
901
+ document.body.style.removeProperty("background");
902
+ }
903
+ else {
904
+ document.body.style.background = pageBackground;
905
+ }
906
+ this._lastAppliedPageBackground = pageBackground;
907
+ }
908
+ }
909
+ /** @internal */
910
+ RenderSystem.MAX_SURFACE_CACHE_ENTRIES = 256;