minimojs 1.0.0-alpha.4 → 1.0.0-alpha.41

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 (71) hide show
  1. package/README.md +58 -20
  2. package/dist/internal/AnimationSystem.js +345 -0
  3. package/dist/internal/AssetSystem.d.ts +6 -0
  4. package/dist/internal/AssetSystem.js +281 -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/DesignNamingCheck.d.ts +1 -0
  10. package/dist/internal/DesignNamingCheck.js +7 -0
  11. package/dist/internal/ExplosionSystem.d.ts +1 -0
  12. package/dist/internal/ExplosionSystem.js +540 -0
  13. package/dist/internal/InputSystem.d.ts +1 -0
  14. package/dist/internal/InputSystem.js +292 -0
  15. package/dist/internal/LoopSystem.d.ts +1 -0
  16. package/dist/internal/LoopSystem.js +61 -0
  17. package/dist/internal/PhysicsSystem.d.ts +1 -0
  18. package/dist/internal/PhysicsSystem.js +174 -0
  19. package/dist/internal/RenderSystem.d.ts +1 -0
  20. package/dist/internal/RenderSystem.js +1021 -0
  21. package/dist/internal/SoundSystem.d.ts +39 -0
  22. package/dist/internal/SoundSystem.js +292 -0
  23. package/dist/internal/SpriteSystem.d.ts +1 -0
  24. package/dist/internal/SpriteSystem.js +32 -0
  25. package/dist/internal/TextSystem.d.ts +1 -0
  26. package/dist/internal/TextSystem.js +16 -0
  27. package/dist/internal/TimerSystem.d.ts +1 -0
  28. package/dist/internal/TimerSystem.js +43 -0
  29. package/dist/internal/TrailSystem.d.ts +1 -0
  30. package/dist/internal/TrailSystem.js +116 -0
  31. package/dist/internal/TransitionSystem.d.ts +1 -0
  32. package/dist/internal/TransitionSystem.js +74 -0
  33. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.d.ts +1 -0
  34. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.js +198 -0
  35. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.d.ts +1 -0
  36. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.js +120 -0
  37. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.d.ts +1 -0
  38. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.js +599 -0
  39. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.d.ts +1 -0
  40. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.js +13 -0
  41. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.d.ts +1 -0
  42. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.js +447 -0
  43. package/dist/minimo-ambient.d.ts +299 -0
  44. package/dist/minimo-ambient.js +1445 -0
  45. package/dist/minimo-arcaderacer.d.ts +1431 -0
  46. package/dist/minimo-arcaderacer.js +2060 -0
  47. package/dist/minimo-cards.d.ts +359 -0
  48. package/dist/minimo-cards.js +819 -0
  49. package/dist/minimo-dice.d.ts +206 -0
  50. package/dist/minimo-dice.js +719 -0
  51. package/dist/minimo-fire.d.ts +282 -0
  52. package/dist/minimo-fire.js +930 -0
  53. package/dist/minimo-format.d.ts +632 -0
  54. package/dist/minimo-format.js +669 -0
  55. package/dist/minimo-leaderboard.d.ts +199 -0
  56. package/dist/minimo-leaderboard.js +566 -0
  57. package/dist/minimo-multiplayer.d.ts +387 -0
  58. package/dist/minimo-multiplayer.js +1323 -0
  59. package/dist/minimo.d.ts +1690 -233
  60. package/dist/minimo.js +2587 -892
  61. package/package.json +10 -3
  62. package/dist/animations.js +0 -30
  63. package/dist/audio.js +0 -17
  64. package/dist/game.js +0 -1105
  65. package/dist/input.js +0 -185
  66. package/dist/internal-types.js +0 -4
  67. package/dist/physics.js +0 -10
  68. package/dist/render.js +0 -75
  69. package/dist/sprite.js +0 -149
  70. package/dist/timers.js +0 -23
  71. /package/dist/{pointer-info.js → internal/AnimationSystem.d.ts} +0 -0
@@ -0,0 +1,39 @@
1
+ /** Options for one play of a loaded sound. */
2
+ export interface PlaySoundOptions {
3
+ /** Repeat the sound until it is stopped. Default `false`. */
4
+ loop?: boolean;
5
+ /** Volume from `0`, silent, to `1`, as recorded. Default `1`. */
6
+ volume?: number;
7
+ }
8
+ /** Where a {@link SoundPlayback} is: playing, paused, or stopped for good. */
9
+ export type SoundPlaybackState = "playing" | "paused" | "stopped";
10
+ /**
11
+ * One play of a loaded sound, returned by `game.playSound`. Keep it to pause,
12
+ * resume, or stop that play, such as the game's music:
13
+ *
14
+ * ```ts
15
+ * const music = game.playSound("theme", { loop: true, volume: 0.6 });
16
+ * music.pause();
17
+ * music.resume();
18
+ * music.stop();
19
+ * ```
20
+ *
21
+ * A stopped play cannot be resumed; call `game.playSound` again.
22
+ */
23
+ export declare class SoundPlayback {
24
+ /** The key the sound was loaded under. */
25
+ readonly key: string;
26
+ /** Whether this play repeats until stopped. */
27
+ readonly loop: boolean;
28
+ /** `"playing"`, `"paused"`, or `"stopped"`. A one-shot play stops by itself when it ends. */
29
+ get state(): SoundPlaybackState;
30
+ /** This play's volume, from `0` to `1`. Takes effect immediately. */
31
+ get volume(): number;
32
+ set volume(value: number);
33
+ /** Pauses the play where it is. Does nothing unless it is playing. */
34
+ pause(): void;
35
+ /** Continues a paused play from where it paused. Does nothing unless it is paused. */
36
+ resume(): void;
37
+ /** Stops the play for good. */
38
+ stop(): void;
39
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * One play of a loaded sound, returned by `game.playSound`. Keep it to pause,
3
+ * resume, or stop that play, such as the game's music:
4
+ *
5
+ * ```ts
6
+ * const music = game.playSound("theme", { loop: true, volume: 0.6 });
7
+ * music.pause();
8
+ * music.resume();
9
+ * music.stop();
10
+ * ```
11
+ *
12
+ * A stopped play cannot be resumed; call `game.playSound` again.
13
+ */
14
+ export class SoundPlayback {
15
+ /** @internal */
16
+ constructor(
17
+ /** The key the sound was loaded under. */
18
+ key,
19
+ /** Whether this play repeats until stopped. */
20
+ loop, volume,
21
+ /** @internal */
22
+ _system,
23
+ /** @internal */
24
+ _buffer) {
25
+ this.key = key;
26
+ this.loop = loop;
27
+ this._system = _system;
28
+ this._buffer = _buffer;
29
+ /** @internal */
30
+ this._state = "playing";
31
+ /** @internal */
32
+ this._source = null;
33
+ /** @internal */
34
+ this._gain = null;
35
+ /** Seconds into the sound where the current source started, or where it was paused. @internal */
36
+ this._offset = 0;
37
+ /** The context time at which the current source started. @internal */
38
+ this._startedAt = 0;
39
+ this._volume = clampVolume(volume);
40
+ if (!_system || !_buffer) {
41
+ this._state = "stopped";
42
+ return;
43
+ }
44
+ this._startSource();
45
+ }
46
+ /** `"playing"`, `"paused"`, or `"stopped"`. A one-shot play stops by itself when it ends. */
47
+ get state() {
48
+ return this._state;
49
+ }
50
+ /** This play's volume, from `0` to `1`. Takes effect immediately. */
51
+ get volume() {
52
+ return this._volume;
53
+ }
54
+ set volume(value) {
55
+ this._volume = clampVolume(value);
56
+ if (this._gain) {
57
+ this._gain.gain.value = this._volume;
58
+ }
59
+ }
60
+ /** Pauses the play where it is. Does nothing unless it is playing. */
61
+ pause() {
62
+ if (this._state !== "playing" || !this._system || !this._buffer)
63
+ return;
64
+ const elapsed = this._system.context.currentTime - this._startedAt;
65
+ const position = this._offset + Math.max(0, elapsed);
66
+ this._offset = this.loop
67
+ ? position % this._buffer.duration
68
+ : Math.min(position, this._buffer.duration);
69
+ this._stopSource();
70
+ this._state = "paused";
71
+ }
72
+ /** Continues a paused play from where it paused. Does nothing unless it is paused. */
73
+ resume() {
74
+ if (this._state !== "paused")
75
+ return;
76
+ this._state = "playing";
77
+ this._startSource();
78
+ }
79
+ /** Stops the play for good. */
80
+ stop() {
81
+ if (this._state === "stopped")
82
+ return;
83
+ this._stopSource();
84
+ this._state = "stopped";
85
+ this._system?.forget(this);
86
+ }
87
+ /** @internal */
88
+ _startSource() {
89
+ const system = this._system;
90
+ const buffer = this._buffer;
91
+ if (!system || !buffer)
92
+ return;
93
+ const ctx = system.context;
94
+ const gain = ctx.createGain();
95
+ gain.gain.value = this._volume;
96
+ gain.connect(ctx.destination);
97
+ const source = ctx.createBufferSource();
98
+ source.buffer = buffer;
99
+ source.loop = this.loop;
100
+ source.connect(gain);
101
+ source.onended = () => {
102
+ if (this._source !== source)
103
+ return;
104
+ this._source = null;
105
+ this._gain = null;
106
+ gain.disconnect();
107
+ this._state = "stopped";
108
+ system.forget(this);
109
+ };
110
+ this._source = source;
111
+ this._gain = gain;
112
+ this._startedAt = ctx.currentTime;
113
+ source.start(0, this._offset);
114
+ system.remember(this);
115
+ }
116
+ /** @internal */
117
+ _stopSource() {
118
+ const source = this._source;
119
+ const gain = this._gain;
120
+ this._source = null;
121
+ this._gain = null;
122
+ if (source) {
123
+ source.onended = null;
124
+ try {
125
+ source.stop();
126
+ }
127
+ catch {
128
+ // A source that already ended throws on some browsers.
129
+ }
130
+ source.disconnect();
131
+ }
132
+ gain?.disconnect();
133
+ }
134
+ }
135
+ function clampVolume(value) {
136
+ if (!Number.isFinite(value))
137
+ return 1;
138
+ return Math.min(1, Math.max(0, value));
139
+ }
140
+ /** The events that count as the player's first gesture, which lets a page play sound. */
141
+ const UNLOCK_EVENTS = ["pointerdown", "touchend", "mousedown", "keydown"];
142
+ /** @internal */
143
+ export class SoundSystem {
144
+ constructor() {
145
+ /** @internal */
146
+ this._audioCtx = null;
147
+ /** @internal */
148
+ this._playing = new Set();
149
+ /** Whether the page hid while sound could play, so showing it again resumes. @internal */
150
+ this._suspendedByHiding = false;
151
+ }
152
+ sound(freq, durationMs) {
153
+ const ctx = this.getAudioContext();
154
+ const oscillator = ctx.createOscillator();
155
+ const gain = ctx.createGain();
156
+ oscillator.type = "square";
157
+ oscillator.frequency.setValueAtTime(freq, ctx.currentTime);
158
+ gain.gain.setValueAtTime(0.08, ctx.currentTime);
159
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + durationMs / 1000);
160
+ oscillator.connect(gain);
161
+ gain.connect(ctx.destination);
162
+ oscillator.start(ctx.currentTime);
163
+ oscillator.stop(ctx.currentTime + durationMs / 1000);
164
+ }
165
+ soundSequence(...tones) {
166
+ if (tones.length === 0)
167
+ return;
168
+ const ctx = this.getAudioContext();
169
+ let startTime = ctx.currentTime;
170
+ for (const [freq, durationMs] of tones) {
171
+ if (!Number.isFinite(freq) || !Number.isFinite(durationMs))
172
+ continue;
173
+ if (freq <= 0 || durationMs <= 0)
174
+ continue;
175
+ const durationSec = durationMs / 1000;
176
+ const oscillator = ctx.createOscillator();
177
+ const gain = ctx.createGain();
178
+ oscillator.type = "square";
179
+ oscillator.frequency.setValueAtTime(freq, startTime);
180
+ gain.gain.setValueAtTime(0.08, startTime);
181
+ gain.gain.exponentialRampToValueAtTime(0.001, startTime + durationSec);
182
+ oscillator.connect(gain);
183
+ gain.connect(ctx.destination);
184
+ oscillator.start(startTime);
185
+ oscillator.stop(startTime + durationSec);
186
+ startTime += durationSec;
187
+ }
188
+ }
189
+ /**
190
+ * Fetches and decodes an audio file. Used by the asset system to load the
191
+ * sounds a game queues, so a file the browser cannot decode fails while the
192
+ * game loads, with every other file that failed.
193
+ */
194
+ async decode(src) {
195
+ const response = await fetch(src);
196
+ if (!response.ok) {
197
+ throw new Error(`MinimoJS: Failed to load sound "${src}" (HTTP ${response.status}).`);
198
+ }
199
+ const data = await response.arrayBuffer();
200
+ const ctx = this.getAudioContext();
201
+ // The callback form, because older Safari returns no promise.
202
+ return new Promise((resolve, reject) => {
203
+ const result = ctx.decodeAudioData(data, resolve, reject);
204
+ result?.catch?.(reject);
205
+ });
206
+ }
207
+ /**
208
+ * Plays a decoded sound. Before the player's first gesture the page cannot
209
+ * play sound: a looping play waits and starts with that gesture, while a
210
+ * one-shot play is dropped, so effects queued meanwhile do not all sound at
211
+ * once. A dropped play is returned already stopped.
212
+ */
213
+ play(key, buffer, options = {}) {
214
+ const loop = options.loop === true;
215
+ const volume = options.volume ?? 1;
216
+ const ctx = this.getAudioContext();
217
+ if (!loop && ctx.state !== "running") {
218
+ return new SoundPlayback(key, loop, volume, null, null);
219
+ }
220
+ return new SoundPlayback(key, loop, volume, this, buffer);
221
+ }
222
+ /** Stops every play of a loaded sound. Synthesized tones are not affected. */
223
+ stopAll() {
224
+ for (const playback of [...this._playing]) {
225
+ playback.stop();
226
+ }
227
+ }
228
+ /** @internal */
229
+ get context() {
230
+ return this.getAudioContext();
231
+ }
232
+ /** @internal */
233
+ remember(playback) {
234
+ this._playing.add(playback);
235
+ }
236
+ /** @internal */
237
+ forget(playback) {
238
+ this._playing.delete(playback);
239
+ }
240
+ /** @internal */
241
+ getAudioContext() {
242
+ if (!this._audioCtx) {
243
+ this._audioCtx = new AudioContext();
244
+ this.listenForUnlockAndVisibility();
245
+ }
246
+ const ctx = this._audioCtx;
247
+ if (ctx.state === "suspended" && !this._suspendedByHiding) {
248
+ void ctx.resume().catch(() => { });
249
+ }
250
+ return ctx;
251
+ }
252
+ /**
253
+ * Resumes the context on the player's first gesture, which is what lets a
254
+ * browser play sound, and pauses all sound while the page is hidden.
255
+ *
256
+ * @internal
257
+ */
258
+ listenForUnlockAndVisibility() {
259
+ if (typeof window === "undefined")
260
+ return;
261
+ const unlock = () => {
262
+ const ctx = this._audioCtx;
263
+ if (!ctx || this._suspendedByHiding)
264
+ return;
265
+ if (ctx.state === "running") {
266
+ for (const type of UNLOCK_EVENTS)
267
+ window.removeEventListener(type, unlock, true);
268
+ return;
269
+ }
270
+ void ctx.resume().catch(() => { });
271
+ };
272
+ for (const type of UNLOCK_EVENTS)
273
+ window.addEventListener(type, unlock, true);
274
+ if (typeof document === "undefined")
275
+ return;
276
+ document.addEventListener("visibilitychange", () => {
277
+ const ctx = this._audioCtx;
278
+ if (!ctx)
279
+ return;
280
+ if (document.visibilityState === "hidden") {
281
+ if (ctx.state === "running") {
282
+ this._suspendedByHiding = true;
283
+ void ctx.suspend().catch(() => { });
284
+ }
285
+ }
286
+ else if (this._suspendedByHiding) {
287
+ this._suspendedByHiding = false;
288
+ void ctx.resume().catch(() => { });
289
+ }
290
+ });
291
+ }
292
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ /** @internal */
2
+ export class SpriteSystem {
3
+ constructor(
4
+ /** @internal */ animationSystem,
5
+ /** @internal */ game) {
6
+ this.animationSystem = animationSystem;
7
+ this.game = game;
8
+ /** @internal */
9
+ this._sprites = [];
10
+ }
11
+ add(sprite) {
12
+ sprite._setGame(this.game);
13
+ this._sprites.push(sprite);
14
+ return sprite;
15
+ }
16
+ destroySprite(sprite) {
17
+ const idx = this._sprites.indexOf(sprite);
18
+ if (idx !== -1) {
19
+ this._sprites.splice(idx, 1);
20
+ }
21
+ this.animationSystem.clearSpriteAnimations(sprite);
22
+ }
23
+ getSprites() {
24
+ return [...this._sprites];
25
+ }
26
+ getMutableSprites() {
27
+ return this._sprites;
28
+ }
29
+ clearAll() {
30
+ this._sprites = [];
31
+ }
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ /** @internal */
2
+ export class TextSystem {
3
+ constructor() {
4
+ /** @internal */
5
+ this._entries = [];
6
+ }
7
+ drawText(text, x, y, fontSize, color = "#ffffff", centered = false) {
8
+ this._entries.push({ text, x, y, fontSize, color, centered });
9
+ }
10
+ getEntries() {
11
+ return this._entries;
12
+ }
13
+ clear() {
14
+ this._entries = [];
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ /** @internal */
2
+ export class TimerSystem {
3
+ constructor() {
4
+ /** @internal */
5
+ this._timers = [];
6
+ /** @internal */
7
+ this._timerIdCounter = 0;
8
+ }
9
+ update(dtMs) {
10
+ const timers = this._timers;
11
+ const toRemove = new Set();
12
+ const toFire = [];
13
+ for (const timer of timers) {
14
+ timer.elapsed += dtMs;
15
+ if (timer.elapsed >= timer.delayMs) {
16
+ toFire.push(timer);
17
+ if (timer.repeat) {
18
+ timer.elapsed -= timer.delayMs;
19
+ }
20
+ else {
21
+ toRemove.add(timer.id);
22
+ }
23
+ }
24
+ }
25
+ if (toRemove.size > 0) {
26
+ this._timers = timers.filter((t) => !toRemove.has(t.id));
27
+ }
28
+ for (const timer of toFire) {
29
+ timer.callback();
30
+ }
31
+ }
32
+ addTimer(delayMs, repeat, callback) {
33
+ const id = ++this._timerIdCounter;
34
+ this._timers.push({ id, delayMs, elapsed: 0, repeat, callback });
35
+ return id;
36
+ }
37
+ clearTimer(id) {
38
+ this._timers = this._timers.filter((t) => t.id !== id);
39
+ }
40
+ clearAll() {
41
+ this._timers = [];
42
+ }
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,116 @@
1
+ const DEFAULT_OPTIONS = {
2
+ durationMs: 300,
3
+ spacingMs: 40,
4
+ fadeMs: 180,
5
+ alpha: 0.45,
6
+ scaleTo: 0.8,
7
+ };
8
+ /** @internal */
9
+ export class TrailSystem {
10
+ constructor() {
11
+ /** @internal */
12
+ this._emitters = [];
13
+ /** @internal */
14
+ this._ghosts = [];
15
+ }
16
+ animateTrail(sprite, options, onComplete) {
17
+ const durationMs = this.sanitizeDuration(options?.durationMs, DEFAULT_OPTIONS.durationMs);
18
+ const spacingMs = this.sanitizeDuration(options?.spacingMs, DEFAULT_OPTIONS.spacingMs);
19
+ const fadeMs = this.sanitizeDuration(options?.fadeMs, DEFAULT_OPTIONS.fadeMs);
20
+ const alpha = this.sanitizeAlpha(options?.alpha, DEFAULT_OPTIONS.alpha);
21
+ const scaleTo = this.sanitizeScale(options?.scaleTo, DEFAULT_OPTIONS.scaleTo);
22
+ const emitters = this._emitters.filter((entry) => entry.sprite !== sprite);
23
+ emitters.push({
24
+ sprite,
25
+ elapsedMs: 0,
26
+ durationMs,
27
+ timeUntilNextEmitMs: 0,
28
+ spacingMs,
29
+ fadeMs,
30
+ alpha,
31
+ scaleTo,
32
+ onComplete,
33
+ });
34
+ this._emitters = emitters;
35
+ }
36
+ update(dtMs, getSnapshot) {
37
+ const emitters = this._emitters;
38
+ const toRemove = [];
39
+ for (let i = 0; i < emitters.length; i++) {
40
+ const emitter = emitters[i];
41
+ emitter.elapsedMs += dtMs;
42
+ emitter.timeUntilNextEmitMs -= dtMs;
43
+ while (emitter.timeUntilNextEmitMs <= 0 && emitter.elapsedMs <= emitter.durationMs) {
44
+ const snapshot = getSnapshot(emitter.sprite);
45
+ this._ghosts.push({
46
+ canvas: snapshot.canvas,
47
+ layer: snapshot.layer,
48
+ ignoreScroll: snapshot.ignoreScroll,
49
+ x: snapshot.x,
50
+ y: snapshot.y,
51
+ rotation: snapshot.rotation,
52
+ alpha: snapshot.alpha * emitter.alpha,
53
+ flipX: snapshot.flipX,
54
+ flipY: snapshot.flipY,
55
+ drawWidth: snapshot.drawWidth,
56
+ drawHeight: snapshot.drawHeight,
57
+ ageMs: 0,
58
+ fadeMs: emitter.fadeMs,
59
+ startAlpha: snapshot.alpha * emitter.alpha,
60
+ baseDrawWidth: snapshot.drawWidth,
61
+ baseDrawHeight: snapshot.drawHeight,
62
+ scaleTo: emitter.scaleTo,
63
+ });
64
+ emitter.timeUntilNextEmitMs += emitter.spacingMs;
65
+ }
66
+ if (emitter.elapsedMs >= emitter.durationMs) {
67
+ toRemove.push(i);
68
+ emitter.onComplete?.();
69
+ }
70
+ }
71
+ for (let i = toRemove.length - 1; i >= 0; i--) {
72
+ emitters.splice(toRemove[i], 1);
73
+ }
74
+ const ghosts = this._ghosts;
75
+ for (let i = ghosts.length - 1; i >= 0; i--) {
76
+ const ghost = ghosts[i];
77
+ ghost.ageMs += dtMs;
78
+ const t = Math.min(ghost.ageMs / ghost.fadeMs, 1);
79
+ const scale = 1 + (ghost.scaleTo - 1) * t;
80
+ ghost.alpha = ghost.startAlpha * (1 - t);
81
+ ghost.drawWidth = ghost.baseDrawWidth * scale;
82
+ ghost.drawHeight = ghost.baseDrawHeight * scale;
83
+ if (t >= 1) {
84
+ ghosts.splice(i, 1);
85
+ }
86
+ }
87
+ }
88
+ getRenderEntries() {
89
+ return this._ghosts;
90
+ }
91
+ clearSpriteTrails(sprite) {
92
+ this._emitters = this._emitters.filter((entry) => entry.sprite !== sprite);
93
+ }
94
+ clearAll() {
95
+ this._emitters = [];
96
+ this._ghosts = [];
97
+ }
98
+ /** @internal */
99
+ sanitizeDuration(value, fallback) {
100
+ if (!Number.isFinite(value))
101
+ return fallback;
102
+ return Math.max(1, value);
103
+ }
104
+ /** @internal */
105
+ sanitizeAlpha(value, fallback) {
106
+ if (!Number.isFinite(value))
107
+ return fallback;
108
+ return Math.max(0, Math.min(1, value));
109
+ }
110
+ /** @internal */
111
+ sanitizeScale(value, fallback) {
112
+ if (!Number.isFinite(value))
113
+ return fallback;
114
+ return Math.max(0, value);
115
+ }
116
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
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
+ /** @internal */
8
+ this._active = null;
9
+ }
10
+ get isActive() {
11
+ return this._active !== null;
12
+ }
13
+ start(fromCanvas, toCanvas, width, height, options, onComplete) {
14
+ const type = options.type;
15
+ const durationMs = this.sanitizeDuration(options.durationMs, DEFAULT_DURATION_MS);
16
+ const color = typeof options.color === "string" && options.color.trim()
17
+ ? options.color
18
+ : type === "flash"
19
+ ? "#ffffff"
20
+ : "#000000";
21
+ const centerX = this.sanitizeCoordinate(options.centerX, width / 2);
22
+ const centerY = this.sanitizeCoordinate(options.centerY, height / 2);
23
+ const pixelSize = this.sanitizePixelSize(options.pixelSize, DEFAULT_PIXEL_SIZE);
24
+ this._active = {
25
+ fromCanvas,
26
+ toCanvas,
27
+ type,
28
+ progress: 0,
29
+ color,
30
+ direction: options.direction ?? DEFAULT_DIRECTION,
31
+ centerX,
32
+ centerY,
33
+ pixelSize,
34
+ elapsedMs: 0,
35
+ durationMs,
36
+ onComplete,
37
+ };
38
+ }
39
+ update(dtMs) {
40
+ if (!this._active)
41
+ return;
42
+ this._active.elapsedMs += dtMs;
43
+ this._active.progress = Math.max(0, Math.min(1, this._active.elapsedMs / this._active.durationMs));
44
+ if (this._active.progress >= 1) {
45
+ const onComplete = this._active.onComplete;
46
+ this._active = null;
47
+ onComplete?.();
48
+ }
49
+ }
50
+ getRenderEntry() {
51
+ return this._active;
52
+ }
53
+ clear() {
54
+ this._active = null;
55
+ }
56
+ /** @internal */
57
+ sanitizeDuration(value, fallback) {
58
+ if (!Number.isFinite(value))
59
+ return fallback;
60
+ return Math.max(1, value);
61
+ }
62
+ /** @internal */
63
+ sanitizeCoordinate(value, fallback) {
64
+ if (!Number.isFinite(value))
65
+ return fallback;
66
+ return value;
67
+ }
68
+ /** @internal */
69
+ sanitizePixelSize(value, fallback) {
70
+ if (!Number.isFinite(value))
71
+ return fallback;
72
+ return Math.max(2, Math.round(value));
73
+ }
74
+ }