minimojs 1.0.0-alpha.24 → 1.0.0-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/internal/InputSystem.js +51 -17
- package/dist/internal/RenderSystem.js +33 -5
- package/dist/minimo-cards.js +4 -1
- package/dist/minimo.d.ts +7 -7
- package/dist/minimo.js +18 -6
- package/package.json +1 -1
|
@@ -29,6 +29,16 @@ export class InputSystem {
|
|
|
29
29
|
this._touchPointers = new Map();
|
|
30
30
|
/** @internal */
|
|
31
31
|
this._primaryTouchId = null;
|
|
32
|
+
/**
|
|
33
|
+
* The primary touch after it lifted, kept until the mouse or a new touch
|
|
34
|
+
* takes over, so the pointer stays where the finger left the screen instead
|
|
35
|
+
* of jumping to the mouse's last position.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
this._releasedTouch = null;
|
|
40
|
+
/** @internal True on the frame the primary touch ended in a `touchcancel`. */
|
|
41
|
+
this._pointerCancelled = false;
|
|
32
42
|
}
|
|
33
43
|
get pointerX() {
|
|
34
44
|
return this._pointerX;
|
|
@@ -52,16 +62,19 @@ export class InputSystem {
|
|
|
52
62
|
this._mouseY = p.y;
|
|
53
63
|
this._mouseDown = true;
|
|
54
64
|
this._mousePressed = true;
|
|
65
|
+
this._releasedTouch = null;
|
|
55
66
|
this.syncPrimaryPointer();
|
|
56
67
|
});
|
|
57
68
|
this.canvas.addEventListener("mouseup", () => {
|
|
58
69
|
this._mouseDown = false;
|
|
70
|
+
this._releasedTouch = null;
|
|
59
71
|
this.syncPrimaryPointer();
|
|
60
72
|
});
|
|
61
73
|
this.canvas.addEventListener("mousemove", (e) => {
|
|
62
74
|
const p = this.getCanvasPoint(e.clientX, e.clientY);
|
|
63
75
|
this._mouseX = p.x;
|
|
64
76
|
this._mouseY = p.y;
|
|
77
|
+
this._releasedTouch = null;
|
|
65
78
|
this.syncPrimaryPointer();
|
|
66
79
|
});
|
|
67
80
|
this.canvas.addEventListener("touchstart", (e) => {
|
|
@@ -76,27 +89,17 @@ export class InputSystem {
|
|
|
76
89
|
this._primaryTouchId = touch.identifier;
|
|
77
90
|
}
|
|
78
91
|
}
|
|
92
|
+
this._releasedTouch = null;
|
|
93
|
+
this._pointerCancelled = false;
|
|
79
94
|
this.syncPrimaryPointer();
|
|
80
95
|
e.preventDefault();
|
|
81
96
|
}, { passive: false });
|
|
82
97
|
this.canvas.addEventListener("touchend", (e) => {
|
|
83
|
-
|
|
84
|
-
this._touchPointers.delete(touch.identifier);
|
|
85
|
-
if (this._primaryTouchId === touch.identifier) {
|
|
86
|
-
this._primaryTouchId = null;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
this.syncPrimaryPointer();
|
|
98
|
+
this.endTouches(e, false);
|
|
90
99
|
e.preventDefault();
|
|
91
100
|
}, { passive: false });
|
|
92
101
|
this.canvas.addEventListener("touchcancel", (e) => {
|
|
93
|
-
|
|
94
|
-
this._touchPointers.delete(touch.identifier);
|
|
95
|
-
if (this._primaryTouchId === touch.identifier) {
|
|
96
|
-
this._primaryTouchId = null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
this.syncPrimaryPointer();
|
|
102
|
+
this.endTouches(e, true);
|
|
100
103
|
e.preventDefault();
|
|
101
104
|
}, { passive: false });
|
|
102
105
|
this.canvas.addEventListener("touchmove", (e) => {
|
|
@@ -131,6 +134,9 @@ export class InputSystem {
|
|
|
131
134
|
isPointerPressed() {
|
|
132
135
|
return this._pointerPressed;
|
|
133
136
|
}
|
|
137
|
+
isPointerCancelled() {
|
|
138
|
+
return this._pointerCancelled;
|
|
139
|
+
}
|
|
134
140
|
isPointerDownOverSprite(sprite) {
|
|
135
141
|
if (!sprite)
|
|
136
142
|
return false;
|
|
@@ -185,11 +191,38 @@ export class InputSystem {
|
|
|
185
191
|
for (const pointer of this._touchPointers.values()) {
|
|
186
192
|
pointer.pressed = false;
|
|
187
193
|
}
|
|
194
|
+
if (this._releasedTouch)
|
|
195
|
+
this._releasedTouch.pressed = false;
|
|
196
|
+
this._pointerCancelled = false;
|
|
188
197
|
this.syncPrimaryPointer();
|
|
189
198
|
}
|
|
190
199
|
resetPressedState() {
|
|
191
200
|
this.clearFramePressedState();
|
|
192
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Removes the touches an event ended. When the primary one lifts, it is kept
|
|
204
|
+
* as the released touch at the point it lifted, carrying its frame-press flag
|
|
205
|
+
* so a tap that starts and ends between two frames still reads as pressed.
|
|
206
|
+
*
|
|
207
|
+
* @internal
|
|
208
|
+
*/
|
|
209
|
+
endTouches(e, cancelled) {
|
|
210
|
+
for (const touch of Array.from(e.changedTouches)) {
|
|
211
|
+
const pointer = this._touchPointers.get(touch.identifier);
|
|
212
|
+
this._touchPointers.delete(touch.identifier);
|
|
213
|
+
if (this._primaryTouchId !== touch.identifier || !pointer)
|
|
214
|
+
continue;
|
|
215
|
+
this._primaryTouchId = null;
|
|
216
|
+
if (!cancelled) {
|
|
217
|
+
const p = this.getCanvasPoint(touch.clientX, touch.clientY);
|
|
218
|
+
pointer.x = p.x;
|
|
219
|
+
pointer.y = p.y;
|
|
220
|
+
}
|
|
221
|
+
this._releasedTouch = pointer;
|
|
222
|
+
this._pointerCancelled = cancelled;
|
|
223
|
+
}
|
|
224
|
+
this.syncPrimaryPointer();
|
|
225
|
+
}
|
|
193
226
|
/** @internal */
|
|
194
227
|
getCanvasPoint(clientX, clientY) {
|
|
195
228
|
const rect = this.canvas.getBoundingClientRect();
|
|
@@ -257,9 +290,10 @@ export class InputSystem {
|
|
|
257
290
|
}
|
|
258
291
|
}
|
|
259
292
|
this._primaryTouchId = null;
|
|
260
|
-
|
|
261
|
-
this.
|
|
293
|
+
const released = this._releasedTouch;
|
|
294
|
+
this._pointerX = released ? released.x : this._mouseX;
|
|
295
|
+
this._pointerY = released ? released.y : this._mouseY;
|
|
262
296
|
this._pointerDown = this._mouseDown;
|
|
263
|
-
this._pointerPressed = this._mousePressed;
|
|
297
|
+
this._pointerPressed = this._mousePressed || (released?.pressed ?? false);
|
|
264
298
|
}
|
|
265
299
|
}
|
|
@@ -304,18 +304,21 @@ export class RenderSystem {
|
|
|
304
304
|
ctx.restore();
|
|
305
305
|
}
|
|
306
306
|
/**
|
|
307
|
-
* Draws the frozen scene being left
|
|
308
|
-
* outline fills with the share of images loaded.
|
|
307
|
+
* Draws the frozen scene being left, dimmed so it reads as paused, and over
|
|
308
|
+
* its center a ring whose outline fills with the share of images loaded.
|
|
309
|
+
* The ring has no fill.
|
|
309
310
|
*/
|
|
310
311
|
renderSceneLoading(options) {
|
|
311
312
|
const ctx = options.context;
|
|
312
313
|
const W = options.canvas.width;
|
|
313
314
|
const H = options.canvas.height;
|
|
315
|
+
const reveal = this.clampProgress(options.reveal);
|
|
314
316
|
this.applyPageBackground(options.pageBackground);
|
|
315
317
|
ctx.clearRect(0, 0, W, H);
|
|
316
318
|
ctx.drawImage(options.snapshot, 0, 0, W, H);
|
|
317
|
-
if (
|
|
319
|
+
if (reveal === 0)
|
|
318
320
|
return;
|
|
321
|
+
this.dimScene(ctx, W, H, reveal);
|
|
319
322
|
const progress = this.clampProgress(options.progress);
|
|
320
323
|
const radius = Math.round(Math.min(44, Math.max(20, Math.min(W, H) * 0.06)));
|
|
321
324
|
const start = -Math.PI / 2;
|
|
@@ -327,18 +330,41 @@ export class RenderSystem {
|
|
|
327
330
|
// A soft halo in the opposite tone keeps the ring legible over busy art.
|
|
328
331
|
ctx.shadowColor = isDarkInk ? "rgba(255, 255, 255, 0.5)" : "rgba(0, 0, 0, 0.5)";
|
|
329
332
|
ctx.shadowBlur = 6;
|
|
330
|
-
ctx.globalAlpha = 0.25;
|
|
333
|
+
ctx.globalAlpha = 0.25 * reveal;
|
|
331
334
|
ctx.beginPath();
|
|
332
335
|
ctx.arc(W / 2, H / 2, radius, 0, Math.PI * 2);
|
|
333
336
|
ctx.stroke();
|
|
334
337
|
if (progress > 0) {
|
|
335
|
-
ctx.globalAlpha =
|
|
338
|
+
ctx.globalAlpha = reveal;
|
|
336
339
|
ctx.beginPath();
|
|
337
340
|
ctx.arc(W / 2, H / 2, radius, start, start + Math.PI * 2 * progress);
|
|
338
341
|
ctx.stroke();
|
|
339
342
|
}
|
|
340
343
|
ctx.restore();
|
|
341
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Chooses the ring's color against the frozen frame as it looks once fully
|
|
347
|
+
* dimmed, since that is what the ring is drawn over.
|
|
348
|
+
*/
|
|
349
|
+
pickSceneLoadingInk(snapshot) {
|
|
350
|
+
const dimmed = document.createElement("canvas");
|
|
351
|
+
dimmed.width = snapshot.width;
|
|
352
|
+
dimmed.height = snapshot.height;
|
|
353
|
+
const ctx = dimmed.getContext("2d");
|
|
354
|
+
if (!ctx)
|
|
355
|
+
return "#ffffff";
|
|
356
|
+
ctx.drawImage(snapshot, 0, 0);
|
|
357
|
+
this.dimScene(ctx, dimmed.width, dimmed.height, 1);
|
|
358
|
+
return this.pickLoadingInk(dimmed);
|
|
359
|
+
}
|
|
360
|
+
/** @internal */
|
|
361
|
+
dimScene(ctx, width, height, reveal) {
|
|
362
|
+
ctx.save();
|
|
363
|
+
ctx.globalAlpha = RenderSystem.SCENE_LOADING_DIM * reveal;
|
|
364
|
+
ctx.fillStyle = "#000000";
|
|
365
|
+
ctx.fillRect(0, 0, width, height);
|
|
366
|
+
ctx.restore();
|
|
367
|
+
}
|
|
342
368
|
/**
|
|
343
369
|
* Chooses white or near-black loading ink from the average brightness at the
|
|
344
370
|
* center of `source`. Falls back to white when the pixels cannot be read,
|
|
@@ -991,3 +1017,5 @@ export class RenderSystem {
|
|
|
991
1017
|
RenderSystem.MAX_SURFACE_CACHE_ENTRIES = 256;
|
|
992
1018
|
/** @internal Stroke of the boot line and the scene ring, which match. */
|
|
993
1019
|
RenderSystem.LOADING_STROKE = 3;
|
|
1020
|
+
/** @internal Opacity of the black veil over a scene that is loading the next one. */
|
|
1021
|
+
RenderSystem.SCENE_LOADING_DIM = 0.4;
|
package/dist/minimo-cards.js
CHANGED
|
@@ -658,7 +658,10 @@ export class CardTable {
|
|
|
658
658
|
return;
|
|
659
659
|
}
|
|
660
660
|
this._press = null;
|
|
661
|
-
|
|
661
|
+
// A cancelled touch was interrupted, not released: return the cards, tap nothing.
|
|
662
|
+
if (game.isPointerCancelled())
|
|
663
|
+
this.cancelDrag();
|
|
664
|
+
else if (this._drag)
|
|
662
665
|
this.finishDrag();
|
|
663
666
|
else if (Math.hypot(x - press.x, y - press.y) < this.dragThreshold)
|
|
664
667
|
this.onTap?.(press.card, press.pile);
|
package/dist/minimo.d.ts
CHANGED
|
@@ -201,10 +201,10 @@ export interface IScene {
|
|
|
201
201
|
* `game.onPreload`. Images that are already loaded are skipped, so replaying
|
|
202
202
|
* a scene loads nothing.
|
|
203
203
|
*
|
|
204
|
-
* When images are pending, the scene being left freezes under a
|
|
205
|
-
* ring until they load, and only then is this scene created.
|
|
206
|
-
* that, `reset()` and `transitionTo()` may return before this
|
|
207
|
-
* continue in `onCreate()`, not in the code after the call.
|
|
204
|
+
* When images are pending, the scene being left freezes, dimmed, under a
|
|
205
|
+
* progress ring until they load, and only then is this scene created.
|
|
206
|
+
* Because of that, `reset()` and `transitionTo()` may return before this
|
|
207
|
+
* scene exists: continue in `onCreate()`, not in the code after the call.
|
|
208
208
|
*
|
|
209
209
|
* @example
|
|
210
210
|
* ```ts
|
|
@@ -1064,7 +1064,7 @@ export interface CollisionInfo {
|
|
|
1064
1064
|
* scenes that each carry many assets of their own — typically a game with many
|
|
1065
1065
|
* levels, whose player does not reach every level in a session — should give
|
|
1066
1066
|
* scenes an {@link IScene.onPreload}. Entering such a scene loads its images
|
|
1067
|
-
* first, with the scene being left frozen under a progress ring, so `reset()`
|
|
1067
|
+
* first, with the scene being left frozen and dimmed under a progress ring, so `reset()`
|
|
1068
1068
|
* and `transitionTo()` may return before the new scene's `onCreate()` runs.
|
|
1069
1069
|
*
|
|
1070
1070
|
* ---
|
|
@@ -2292,7 +2292,7 @@ export declare class Game {
|
|
|
2292
2292
|
* can rebuild synchronously.
|
|
2293
2293
|
*
|
|
2294
2294
|
* The exception is a scene whose {@link IScene.onPreload} registers images
|
|
2295
|
-
* that are not loaded yet. Then the current scene freezes under a progress
|
|
2295
|
+
* that are not loaded yet. Then the current scene freezes and dims under a progress
|
|
2296
2296
|
* ring, nothing is cleared until the images load, and `reset()` returns
|
|
2297
2297
|
* before the scene is rebuilt; its `onCreate()` runs once they have loaded.
|
|
2298
2298
|
* A `reset()` called while a scene is loading is ignored.
|
|
@@ -2325,7 +2325,7 @@ export declare class Game {
|
|
|
2325
2325
|
* While the transition is active, gameplay updates are paused.
|
|
2326
2326
|
*
|
|
2327
2327
|
* If the target scene's {@link IScene.onPreload} registers images that are
|
|
2328
|
-
* not loaded yet, the current scene first freezes under a progress ring
|
|
2328
|
+
* not loaded yet, the current scene first freezes and dims under a progress ring
|
|
2329
2329
|
* until they load, and the transition plays after that. `transitionTo()`
|
|
2330
2330
|
* then returns before the target scene exists; continue in its
|
|
2331
2331
|
* `onCreate()` or in `onComplete`. If the player declines to retry failed
|
package/dist/minimo.js
CHANGED
|
@@ -22,8 +22,10 @@ import { TextSystem } from "./internal/TextSystem.js";
|
|
|
22
22
|
import { TrailSystem } from "./internal/TrailSystem.js";
|
|
23
23
|
import { TransitionSystem } from "./internal/TransitionSystem.js";
|
|
24
24
|
import { TimerSystem } from "./internal/TimerSystem.js";
|
|
25
|
-
/** How long a scene may load before its progress ring appears, so a fast load shows
|
|
25
|
+
/** How long a scene may load before it dims and its progress ring appears, so a fast load shows neither. */
|
|
26
26
|
const SCENE_LOADING_INDICATOR_DELAY_MS = 250;
|
|
27
|
+
/** How long the dimming and the ring take to fade in once they appear. */
|
|
28
|
+
const SCENE_LOADING_FADE_MS = 200;
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
28
30
|
// EmojiSprite
|
|
29
31
|
// ---------------------------------------------------------------------------
|
|
@@ -1103,7 +1105,7 @@ export class BackgroundLayer {
|
|
|
1103
1105
|
* scenes that each carry many assets of their own — typically a game with many
|
|
1104
1106
|
* levels, whose player does not reach every level in a session — should give
|
|
1105
1107
|
* scenes an {@link IScene.onPreload}. Entering such a scene loads its images
|
|
1106
|
-
* first, with the scene being left frozen under a progress ring, so `reset()`
|
|
1108
|
+
* first, with the scene being left frozen and dimmed under a progress ring, so `reset()`
|
|
1107
1109
|
* and `transitionTo()` may return before the new scene's `onCreate()` runs.
|
|
1108
1110
|
*
|
|
1109
1111
|
* ---
|
|
@@ -1814,6 +1816,15 @@ export class Game {
|
|
|
1814
1816
|
isPointerPressed() {
|
|
1815
1817
|
return this._inputSystem.isPointerPressed();
|
|
1816
1818
|
}
|
|
1819
|
+
/**
|
|
1820
|
+
* True on the frame the primary touch ended in a `touchcancel` rather than
|
|
1821
|
+
* lifting, so a module can drop the gesture instead of reading it as a tap.
|
|
1822
|
+
*
|
|
1823
|
+
* @internal
|
|
1824
|
+
*/
|
|
1825
|
+
isPointerCancelled() {
|
|
1826
|
+
return this._inputSystem.isPointerCancelled();
|
|
1827
|
+
}
|
|
1817
1828
|
/**
|
|
1818
1829
|
* Returns `true` while any active pointer is held down over the target sprite.
|
|
1819
1830
|
* Works with both mouse input and multiple simultaneous touches.
|
|
@@ -2580,7 +2591,7 @@ export class Game {
|
|
|
2580
2591
|
* can rebuild synchronously.
|
|
2581
2592
|
*
|
|
2582
2593
|
* The exception is a scene whose {@link IScene.onPreload} registers images
|
|
2583
|
-
* that are not loaded yet. Then the current scene freezes under a progress
|
|
2594
|
+
* that are not loaded yet. Then the current scene freezes and dims under a progress
|
|
2584
2595
|
* ring, nothing is cleared until the images load, and `reset()` returns
|
|
2585
2596
|
* before the scene is rebuilt; its `onCreate()` runs once they have loaded.
|
|
2586
2597
|
* A `reset()` called while a scene is loading is ignored.
|
|
@@ -2654,7 +2665,7 @@ export class Game {
|
|
|
2654
2665
|
* While the transition is active, gameplay updates are paused.
|
|
2655
2666
|
*
|
|
2656
2667
|
* If the target scene's {@link IScene.onPreload} registers images that are
|
|
2657
|
-
* not loaded yet, the current scene first freezes under a progress ring
|
|
2668
|
+
* not loaded yet, the current scene first freezes and dims under a progress ring
|
|
2658
2669
|
* until they load, and the transition plays after that. `transitionTo()`
|
|
2659
2670
|
* then returns before the target scene exists; continue in its
|
|
2660
2671
|
* `onCreate()` or in `onComplete`. If the player declines to retry failed
|
|
@@ -2873,7 +2884,8 @@ export class Game {
|
|
|
2873
2884
|
pageBackground: this.pageBackground,
|
|
2874
2885
|
snapshot: load.snapshot,
|
|
2875
2886
|
progress: load.progress,
|
|
2876
|
-
|
|
2887
|
+
reveal: (performance.now() - load.startedAt - SCENE_LOADING_INDICATOR_DELAY_MS) /
|
|
2888
|
+
SCENE_LOADING_FADE_MS,
|
|
2877
2889
|
ink: load.ink,
|
|
2878
2890
|
});
|
|
2879
2891
|
}
|
|
@@ -2915,7 +2927,7 @@ export class Game {
|
|
|
2915
2927
|
snapshot.getContext("2d")?.drawImage(this._canvas, 0, 0);
|
|
2916
2928
|
const load = {
|
|
2917
2929
|
snapshot,
|
|
2918
|
-
ink: this._renderSystem.
|
|
2930
|
+
ink: this._renderSystem.pickSceneLoadingInk(snapshot),
|
|
2919
2931
|
progress: 0,
|
|
2920
2932
|
startedAt: performance.now(),
|
|
2921
2933
|
};
|
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.26",
|
|
4
4
|
"description": "MinimoJS v1 — ultra-minimal, flat, deterministic 2D web game engine. Image-first sprites with emoji as a zero-asset alternative, rAF loop, TypeScript-first, LLM-friendly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/minimo.js",
|