minimojs 1.0.0-alpha.16 → 1.0.0-alpha.18

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/README.md CHANGED
@@ -40,17 +40,35 @@ npm install minimojs
40
40
  ## Quick Start
41
41
 
42
42
  ```ts
43
- import { Game, Sprite, type IScene } from "minimojs";
43
+ import { DrawSprite, Game, Sprite, type IScene } from "minimojs";
44
44
 
45
45
  const game = new Game(720, 1280);
46
46
  game.gravityY = 980;
47
47
 
48
+ class MeterSprite extends DrawSprite {
49
+ public value = 0.5;
50
+
51
+ constructor(x: number, y: number) {
52
+ super(160, 28, x, y);
53
+ }
54
+
55
+ redraw(ctx: CanvasRenderingContext2D) {
56
+ ctx.fillStyle = "#1a2236";
57
+ ctx.fillRect(0, 0, this.width, this.height);
58
+ ctx.fillStyle = "#7ce7ff";
59
+ ctx.fillRect(0, 0, this.width * this.value, this.height);
60
+ }
61
+ }
62
+
48
63
  class DemoScene implements IScene {
49
64
  private player: Sprite | null = null;
65
+ private meter: MeterSprite | null = null;
50
66
 
51
67
  onCreate() {
52
68
  this.player = game.add(new Sprite("🐢", 360, 640, 48));
53
69
  this.player.gravityScale = 1;
70
+ this.meter = game.add(new MeterSprite(360, 80));
71
+ this.meter.ignoreScroll = true;
54
72
  }
55
73
 
56
74
  onUpdate(dt: number) {
@@ -61,6 +79,7 @@ class DemoScene implements IScene {
61
79
  else this.player.vx = 0;
62
80
 
63
81
  if (game.isKeyPressed(" ")) this.player.vy = -600;
82
+ if (this.meter) this.meter.value = Math.abs(this.player.vx) / 200;
64
83
  game.drawText("MinimoJS", 10, 10, 16);
65
84
  }
66
85
  }
@@ -80,6 +99,8 @@ Note: `drawText()` uses `"Press Start 2P", monospace`. Load the font in your HTM
80
99
  - `examples/background-desert/`
81
100
  - `examples/scene-lab/`
82
101
  - `examples/image-sprite-sad-plush/`
102
+ - `examples/draw-sprite/`
103
+ - `examples/pseudo-3d-racer/`
83
104
  - `examples/animations/`
84
105
 
85
106
  Run locally from the `minimojs` directory:
@@ -1,12 +1,19 @@
1
1
  /** @internal */
2
2
  export class AnimationSystem {
3
3
  constructor() {
4
+ /** @internal */
4
5
  this._animations = [];
6
+ /** @internal */
5
7
  this._deformAnimations = [];
8
+ /** @internal */
6
9
  this._motionAnimations = [];
10
+ /** @internal */
7
11
  this._shakeAnimations = [];
12
+ /** @internal */
8
13
  this._blinkAnimations = [];
14
+ /** @internal */
9
15
  this._flickerAnimations = [];
16
+ /** @internal */
10
17
  this._renderDataSprites = new Set();
11
18
  }
12
19
  update(dtMs) {
@@ -211,31 +218,37 @@ export class AnimationSystem {
211
218
  this._flickerAnimations = [];
212
219
  this._renderDataSprites.clear();
213
220
  }
221
+ /** @internal */
214
222
  sanitizeScale(value) {
215
223
  if (!Number.isFinite(value))
216
224
  return 1;
217
225
  return Math.max(0, value);
218
226
  }
227
+ /** @internal */
219
228
  sanitizeDuration(value) {
220
229
  if (!Number.isFinite(value))
221
230
  return 1;
222
231
  return Math.max(1, value);
223
232
  }
233
+ /** @internal */
224
234
  sanitizeNonNegative(value) {
225
235
  if (!Number.isFinite(value))
226
236
  return 0;
227
237
  return Math.max(0, value);
228
238
  }
239
+ /** @internal */
229
240
  sanitizePivot(value) {
230
241
  if (!Number.isFinite(value))
231
242
  return 0.5;
232
243
  return Math.max(0, Math.min(1, value));
233
244
  }
245
+ /** @internal */
234
246
  replaceMotionAnimation(entry) {
235
247
  const animations = this._motionAnimations.filter((a) => a.sprite !== entry.sprite);
236
248
  animations.push(entry);
237
249
  this._motionAnimations = animations;
238
250
  }
251
+ /** @internal */
239
252
  rebuildRenderData(dtMs) {
240
253
  const next = new Map();
241
254
  const nextSprites = new Set();
@@ -1,8 +1,11 @@
1
1
  /** @internal */
2
2
  export class AssetSystem {
3
3
  constructor() {
4
+ /** @internal */
4
5
  this._queuedImages = new Map();
6
+ /** @internal */
5
7
  this._loadedImages = new Map();
8
+ /** @internal */
6
9
  this._imageSources = new Map();
7
10
  }
8
11
  queueImage(key, src) {
@@ -51,6 +54,7 @@ export class AssetSystem {
51
54
  }));
52
55
  this._queuedImages.clear();
53
56
  }
57
+ /** @internal */
54
58
  loadImage(src) {
55
59
  return new Promise((resolve, reject) => {
56
60
  const image = new Image();
@@ -1,6 +1,7 @@
1
1
  /** @internal */
2
2
  export class BackgroundSystem {
3
3
  constructor() {
4
+ /** @internal */
4
5
  this._layers = [];
5
6
  }
6
7
  add(layer) {
@@ -1,8 +1,10 @@
1
1
  /** @internal */
2
2
  export class CanvasSystem {
3
- constructor(canvas) {
3
+ constructor(/** @internal */ canvas) {
4
4
  this.canvas = canvas;
5
+ /** @internal */
5
6
  this.onResize = () => this.applyResponsiveCanvasLayout();
7
+ /** @internal */
6
8
  this.onViewportChange = () => this.applyResponsiveCanvasLayout();
7
9
  }
8
10
  initialize() {
@@ -22,6 +24,7 @@ export class CanvasSystem {
22
24
  window.visualViewport?.addEventListener("resize", this.onViewportChange);
23
25
  window.visualViewport?.addEventListener("scroll", this.onViewportChange);
24
26
  }
27
+ /** @internal */
25
28
  applyResponsiveCanvasLayout() {
26
29
  if (!document.body)
27
30
  return;
@@ -45,7 +45,9 @@ const DEFAULT_WIPE_OPTIONS = {
45
45
  /** @internal */
46
46
  export class ExplosionSystem {
47
47
  constructor() {
48
+ /** @internal */
48
49
  this._pieceEffects = [];
50
+ /** @internal */
49
51
  this._wipes = [];
50
52
  }
51
53
  animateExplode(sprite, glyphCanvas, options, onComplete) {
@@ -190,6 +192,7 @@ export class ExplosionSystem {
190
192
  this._pieceEffects = [];
191
193
  this._wipes = [];
192
194
  }
195
+ /** @internal */
193
196
  spawnRadialEffect(mode, sprite, glyphCanvas, context, options, forceHideSprite, restoreVisibility, finalVisible, onComplete) {
194
197
  this.clearSpriteEffects(sprite);
195
198
  if (forceHideSprite || restoreVisibility) {
@@ -241,6 +244,7 @@ export class ExplosionSystem {
241
244
  onComplete,
242
245
  });
243
246
  }
247
+ /** @internal */
244
248
  spawnDirectionalEffect(mode, sprite, glyphCanvas, context, options, forceHideSprite, restoreVisibility, finalVisible, onComplete) {
245
249
  this.clearSpriteEffects(sprite);
246
250
  if (forceHideSprite || restoreVisibility) {
@@ -298,6 +302,7 @@ export class ExplosionSystem {
298
302
  onComplete,
299
303
  });
300
304
  }
305
+ /** @internal */
301
306
  buildPieceGrid(glyphCanvas, context, rows, cols, builder, mode) {
302
307
  const pieces = [];
303
308
  const rotationRad = (context.rotation * Math.PI) / 180;
@@ -362,6 +367,7 @@ export class ExplosionSystem {
362
367
  }
363
368
  return pieces;
364
369
  }
370
+ /** @internal */
365
371
  buildPieceSpriteContext(sprite, glyphCanvas) {
366
372
  const renderData = sprite._renderData;
367
373
  const deformScaleX = this.getSafeScale(renderData?.scaleX);
@@ -392,6 +398,7 @@ export class ExplosionSystem {
392
398
  absScaleY: Math.abs(signedScaleY),
393
399
  };
394
400
  }
401
+ /** @internal */
395
402
  clearSpriteEffects(sprite) {
396
403
  const remainingPieces = [];
397
404
  for (const effect of this._pieceEffects) {
@@ -418,6 +425,7 @@ export class ExplosionSystem {
418
425
  }
419
426
  this._wipes = remainingWipes;
420
427
  }
428
+ /** @internal */
421
429
  getDirectionalProgress(row, col, rows, cols, direction) {
422
430
  const rowProgress = rows <= 1 ? 0 : row / (rows - 1);
423
431
  const colProgress = cols <= 1 ? 0 : col / (cols - 1);
@@ -432,6 +440,7 @@ export class ExplosionSystem {
432
440
  return 1 - rowProgress;
433
441
  }
434
442
  }
443
+ /** @internal */
435
444
  getDirectionVector(direction) {
436
445
  switch (direction) {
437
446
  case "left-to-right":
@@ -444,6 +453,7 @@ export class ExplosionSystem {
444
453
  return { x: 0, y: -1 };
445
454
  }
446
455
  }
456
+ /** @internal */
447
457
  getRadialVelocity(x, y) {
448
458
  const magnitude = Math.hypot(x, y);
449
459
  let dirX = 0;
@@ -460,6 +470,7 @@ export class ExplosionSystem {
460
470
  const jitterAngle = (Math.random() - 0.5) * (Math.PI / 2);
461
471
  return this.rotate(dirX, dirY, jitterAngle);
462
472
  }
473
+ /** @internal */
463
474
  easePieceProgress(mode, t) {
464
475
  const clamped = Math.max(0, Math.min(1, t));
465
476
  switch (mode) {
@@ -471,6 +482,7 @@ export class ExplosionSystem {
471
482
  return clamped * clamped * (3 - 2 * clamped);
472
483
  }
473
484
  }
485
+ /** @internal */
474
486
  rotate(x, y, radians) {
475
487
  const cos = Math.cos(radians);
476
488
  const sin = Math.sin(radians);
@@ -479,39 +491,47 @@ export class ExplosionSystem {
479
491
  y: x * sin + y * cos,
480
492
  };
481
493
  }
494
+ /** @internal */
482
495
  lerp(from, to, t) {
483
496
  return from + (to - from) * t;
484
497
  }
498
+ /** @internal */
485
499
  getSafeScale(value) {
486
500
  if (!Number.isFinite(value))
487
501
  return 1;
488
502
  return Math.max(0, value);
489
503
  }
504
+ /** @internal */
490
505
  getSafePivot(value) {
491
506
  if (!Number.isFinite(value))
492
507
  return 0.5;
493
508
  return Math.max(0, Math.min(1, value));
494
509
  }
510
+ /** @internal */
495
511
  getSafeNumber(value, fallback) {
496
512
  if (!Number.isFinite(value))
497
513
  return fallback;
498
514
  return value;
499
515
  }
516
+ /** @internal */
500
517
  sanitizeCount(value, fallback) {
501
518
  if (!Number.isFinite(value))
502
519
  return fallback;
503
520
  return Math.max(1, Math.round(value));
504
521
  }
522
+ /** @internal */
505
523
  sanitizeDuration(value, fallback) {
506
524
  if (!Number.isFinite(value))
507
525
  return fallback;
508
526
  return Math.max(1, value);
509
527
  }
528
+ /** @internal */
510
529
  sanitizeNonNegative(value, fallback) {
511
530
  if (!Number.isFinite(value))
512
531
  return fallback;
513
532
  return Math.max(0, value);
514
533
  }
534
+ /** @internal */
515
535
  sanitizeNumber(value, fallback) {
516
536
  if (!Number.isFinite(value))
517
537
  return fallback;
@@ -1,19 +1,33 @@
1
1
  /** @internal */
2
2
  export class InputSystem {
3
- constructor(game, canvas) {
3
+ constructor(
4
+ /** @internal */ game,
5
+ /** @internal */ canvas) {
4
6
  this.game = game;
5
7
  this.canvas = canvas;
8
+ /** @internal */
6
9
  this._keysDown = new Set();
10
+ /** @internal */
7
11
  this._keysPressed = new Set();
12
+ /** @internal */
8
13
  this._pointerDown = false;
14
+ /** @internal */
9
15
  this._pointerPressed = false;
16
+ /** @internal */
10
17
  this._pointerX = 0;
18
+ /** @internal */
11
19
  this._pointerY = 0;
20
+ /** @internal */
12
21
  this._mouseDown = false;
22
+ /** @internal */
13
23
  this._mousePressed = false;
24
+ /** @internal */
14
25
  this._mouseX = 0;
26
+ /** @internal */
15
27
  this._mouseY = 0;
28
+ /** @internal */
16
29
  this._touchPointers = new Map();
30
+ /** @internal */
17
31
  this._primaryTouchId = null;
18
32
  }
19
33
  get pointerX() {
@@ -176,6 +190,7 @@ export class InputSystem {
176
190
  resetPressedState() {
177
191
  this.clearFramePressedState();
178
192
  }
193
+ /** @internal */
179
194
  getCanvasPoint(clientX, clientY) {
180
195
  const rect = this.canvas.getBoundingClientRect();
181
196
  const scaleX = this.canvas.width / rect.width;
@@ -185,6 +200,7 @@ export class InputSystem {
185
200
  y: (clientY - rect.top) * scaleY,
186
201
  };
187
202
  }
203
+ /** @internal */
188
204
  isScreenPointOverSprite(x, y, sprite) {
189
205
  const halfWidth = sprite.displayWidth / 2;
190
206
  const halfHeight = sprite.displayHeight / 2;
@@ -192,6 +208,7 @@ export class InputSystem {
192
208
  const drawY = sprite.ignoreScroll ? sprite.renderY : sprite.renderY - this.game.scrollY;
193
209
  return Math.abs(x - drawX) <= halfWidth && Math.abs(y - drawY) <= halfHeight;
194
210
  }
211
+ /** @internal */
195
212
  collectPointers() {
196
213
  const pointers = [];
197
214
  if (this._mouseDown) {
@@ -214,6 +231,7 @@ export class InputSystem {
214
231
  }
215
232
  return pointers;
216
233
  }
234
+ /** @internal */
217
235
  syncPrimaryPointer() {
218
236
  if (this._primaryTouchId !== null) {
219
237
  const primaryTouch = this._touchPointers.get(this._primaryTouchId);
@@ -1,12 +1,18 @@
1
1
  /** @internal */
2
2
  export class LoopSystem {
3
- constructor(onFrame) {
3
+ constructor(/** @internal */ onFrame) {
4
4
  this.onFrame = onFrame;
5
+ /** @internal */
5
6
  this._rafId = null;
7
+ /** @internal */
6
8
  this._lastTimestamp = null;
9
+ /** @internal */
7
10
  this._running = false;
11
+ /** @internal */
8
12
  this._hasCreated = false;
13
+ /** @internal */
9
14
  this._onCreate = null;
15
+ /** @internal */
10
16
  this.loop = (timestamp) => {
11
17
  if (!this._running)
12
18
  return;
@@ -1,8 +1,11 @@
1
1
  /** @internal */
2
2
  export class PhysicsSystem {
3
3
  constructor() {
4
+ /** @internal */
4
5
  this._gravityX = 0;
6
+ /** @internal */
5
7
  this._gravityY = 0;
8
+ /** @internal */
6
9
  this._enabled = false;
7
10
  }
8
11
  get gravityX() {
@@ -94,11 +97,13 @@ export class PhysicsSystem {
94
97
  }
95
98
  return null;
96
99
  }
100
+ /** @internal */
97
101
  assertEnabled() {
98
102
  if (!this._enabled) {
99
103
  throw new Error("MinimoJS: collide() and collideAny() require game.physics = true.");
100
104
  }
101
105
  }
106
+ /** @internal */
102
107
  getCollisionResolution(a, b) {
103
108
  const halfWidthA = a.bodyDisplayWidth / 2;
104
109
  const halfHeightA = a.bodyDisplayHeight / 2;