minimojs 1.0.0-alpha.16 → 1.0.0-alpha.17

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:
@@ -5,10 +5,14 @@ export class RenderSystem {
5
5
  this._lastAppliedPageBackground = undefined;
6
6
  this._transitionScratchCanvas = null;
7
7
  this._transitionScratchContext = null;
8
+ this._dynamicSurfacePassId = 0;
8
9
  }
9
10
  clearSpriteCache() {
10
11
  this._surfaceCache.clear();
11
12
  }
13
+ beginDynamicSurfacePass() {
14
+ this._dynamicSurfacePassId += 1;
15
+ }
12
16
  getSpriteGlyphCanvasForEffects(sprite) {
13
17
  return this.getRenderSurface(sprite);
14
18
  }
@@ -348,6 +352,9 @@ export class RenderSystem {
348
352
  ctx.restore();
349
353
  }
350
354
  getRenderSurface(sprite) {
355
+ if (this.isDrawSprite(sprite)) {
356
+ return this.getDrawSurface(sprite);
357
+ }
351
358
  const cacheKey = sprite.getRenderCacheKey();
352
359
  const cached = this._surfaceCache.get(cacheKey);
353
360
  if (cached)
@@ -467,9 +474,47 @@ export class RenderSystem {
467
474
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
468
475
  return canvas;
469
476
  }
477
+ getDrawSurface(sprite) {
478
+ const width = Math.max(1, Math.round(sprite.width));
479
+ const height = Math.max(1, Math.round(sprite.height));
480
+ let surface = sprite._surface;
481
+ if (!surface) {
482
+ surface = document.createElement("canvas");
483
+ sprite._surface = surface;
484
+ }
485
+ const sizeChanged = surface.width !== width || surface.height !== height;
486
+ if (sprite.frozen && !sizeChanged && surface.width > 0 && surface.height > 0) {
487
+ return surface;
488
+ }
489
+ if (!sprite.frozen &&
490
+ sprite._lastRedrawPassId === this._dynamicSurfacePassId &&
491
+ surface.width === width &&
492
+ surface.height === height) {
493
+ return surface;
494
+ }
495
+ if (sizeChanged) {
496
+ surface.width = width;
497
+ surface.height = height;
498
+ }
499
+ else {
500
+ // Resetting width clears pixels and restores the default 2D context state.
501
+ surface.width = width;
502
+ }
503
+ const ctx = surface.getContext("2d");
504
+ if (!ctx) {
505
+ throw new Error("MinimoJS: Could not acquire a DrawSprite rendering context.");
506
+ }
507
+ sprite._surfaceCtx = ctx;
508
+ sprite.redraw(ctx);
509
+ sprite._lastRedrawPassId = this._dynamicSurfacePassId;
510
+ return surface;
511
+ }
470
512
  isTextSprite(sprite) {
471
513
  return "text" in sprite && "fontFamily" in sprite && "fontSize" in sprite;
472
514
  }
515
+ isDrawSprite(sprite) {
516
+ return "redraw" in sprite && "_surface" in sprite && "_surfaceCtx" in sprite;
517
+ }
473
518
  isImageSprite(sprite) {
474
519
  return "imageKey" in sprite && "setTexture" in sprite;
475
520
  }
package/dist/minimo.d.ts CHANGED
@@ -471,6 +471,96 @@ export declare class ImageSprite extends BaseSprite {
471
471
  private getResolvedImage;
472
472
  private assertTextureAvailable;
473
473
  }
474
+ /**
475
+ * A renderable sprite backed by a per-instance canvas that is repainted on demand.
476
+ *
477
+ * `DrawSprite` is useful for procedural shapes, gauges, charts, minimaps, and
478
+ * HUD widgets whose appearance changes often and is easier to express with
479
+ * Canvas 2D drawing commands than with emoji, text, or image swaps.
480
+ *
481
+ * Treat `DrawSprite` as a specialized tool, not the default sprite type.
482
+ * Because it may execute custom Canvas 2D drawing code repeatedly, overusing it
483
+ * can affect game performance much more than regular {@link Sprite},
484
+ * {@link ImageSprite}, or {@link TextSprite} instances.
485
+ *
486
+ * Prefer the other sprite types whenever they can express the same result more
487
+ * simply. Reach for `DrawSprite` only when you truly need procedural drawing or
488
+ * custom per-sprite canvas rendering that the built-in sprite types cannot
489
+ * provide cleanly.
490
+ *
491
+ * MinimoJS creates and owns an internal canvas for each `DrawSprite` instance.
492
+ * Before every engine render that needs this sprite's surface, the engine:
493
+ *
494
+ * 1. Resolves the sprite's internal canvas size
495
+ * 2. Optionally clears and repaints that internal canvas
496
+ * 3. Draws the resulting canvas like any other sprite surface
497
+ *
498
+ * By default, `DrawSprite` is live-rendered: the engine clears the internal
499
+ * canvas and calls {@link DrawSprite.redraw} every frame.
500
+ *
501
+ * Set {@link DrawSprite.frozen} to `true` if you want to keep and reuse the
502
+ * last rendered canvas contents without repainting on each frame. This is
503
+ * useful for shapes or procedural art that you only want to draw once.
504
+ *
505
+ * While `frozen` is `true`, MinimoJS reuses the existing surface exactly as-is:
506
+ * it does not clear the canvas and does not call {@link DrawSprite.redraw}
507
+ * again, unless the surface does not exist yet or its size had to be rebuilt.
508
+ *
509
+ * This means freezing is a rendering optimization and content-preservation
510
+ * flag, not a separate caching system. You can switch `frozen` on or off at
511
+ * runtime whenever it makes sense for your sprite.
512
+ *
513
+ * The local drawing coordinate system uses the sprite surface itself:
514
+ * - `(0, 0)` is the top-left corner of the internal canvas
515
+ * - `width` / `height` match the sprite's logical size before `scale`
516
+ * - draw centered content yourself if you want the visual origin in the middle
517
+ *
518
+ * Override {@link DrawSprite.redraw} in a subclass, or assign your own method
519
+ * on an instance if you prefer an inline style in JavaScript.
520
+ */
521
+ export declare class DrawSprite extends BaseSprite {
522
+ private static _nextSurfaceId;
523
+ /**
524
+ * When `false` (default), MinimoJS clears this sprite's internal canvas and
525
+ * calls {@link DrawSprite.redraw} on every render pass.
526
+ *
527
+ * When `true`, MinimoJS keeps and reuses the existing canvas contents without
528
+ * clearing or repainting them again, unless the internal surface does not yet
529
+ * exist or had to be resized.
530
+ *
531
+ * Use this when your procedural drawing becomes static after its first paint,
532
+ * or when you want explicit manual control over when the sprite is redrawn by
533
+ * toggling `frozen` at runtime.
534
+ */
535
+ frozen: boolean;
536
+ private readonly _surfaceId;
537
+ private readonly _width;
538
+ private readonly _height;
539
+ get width(): number;
540
+ get height(): number;
541
+ /**
542
+ * Creates a new dynamic canvas-backed sprite.
543
+ *
544
+ * @param width - Internal canvas width in pixels. Minimum `1`.
545
+ * @param height - Internal canvas height in pixels. Minimum `1`.
546
+ * @param x - Initial X position in world space (center), in pixels. Default: `0`.
547
+ * @param y - Initial Y position in world space (center), in pixels. Default: `0`.
548
+ */
549
+ constructor(width: number, height: number, x?: number, y?: number);
550
+ /**
551
+ * Called by the engine whenever the sprite's internal surface must be repainted.
552
+ *
553
+ * The provided context is already cleared and reset to the default 2D canvas
554
+ * state for the current surface size. MinimoJS repaints each `DrawSprite` at
555
+ * most once per render pass while it is not {@link DrawSprite.frozen}, so
556
+ * repeated snapshot reads during the same frame reuse the already-redrawn
557
+ * surface.
558
+ *
559
+ * @param ctx - The sprite's internal 2D drawing context.
560
+ */
561
+ redraw(_ctx: CanvasRenderingContext2D): void;
562
+ getRenderCacheKey(): string;
563
+ }
474
564
  /**
475
565
  * A renderable text actor that participates in the same animation/effects
476
566
  * pipeline as regular sprites.
package/dist/minimo.js CHANGED
@@ -383,6 +383,112 @@ export class ImageSprite extends BaseSprite {
383
383
  }
384
384
  }
385
385
  }
386
+ /**
387
+ * A renderable sprite backed by a per-instance canvas that is repainted on demand.
388
+ *
389
+ * `DrawSprite` is useful for procedural shapes, gauges, charts, minimaps, and
390
+ * HUD widgets whose appearance changes often and is easier to express with
391
+ * Canvas 2D drawing commands than with emoji, text, or image swaps.
392
+ *
393
+ * Treat `DrawSprite` as a specialized tool, not the default sprite type.
394
+ * Because it may execute custom Canvas 2D drawing code repeatedly, overusing it
395
+ * can affect game performance much more than regular {@link Sprite},
396
+ * {@link ImageSprite}, or {@link TextSprite} instances.
397
+ *
398
+ * Prefer the other sprite types whenever they can express the same result more
399
+ * simply. Reach for `DrawSprite` only when you truly need procedural drawing or
400
+ * custom per-sprite canvas rendering that the built-in sprite types cannot
401
+ * provide cleanly.
402
+ *
403
+ * MinimoJS creates and owns an internal canvas for each `DrawSprite` instance.
404
+ * Before every engine render that needs this sprite's surface, the engine:
405
+ *
406
+ * 1. Resolves the sprite's internal canvas size
407
+ * 2. Optionally clears and repaints that internal canvas
408
+ * 3. Draws the resulting canvas like any other sprite surface
409
+ *
410
+ * By default, `DrawSprite` is live-rendered: the engine clears the internal
411
+ * canvas and calls {@link DrawSprite.redraw} every frame.
412
+ *
413
+ * Set {@link DrawSprite.frozen} to `true` if you want to keep and reuse the
414
+ * last rendered canvas contents without repainting on each frame. This is
415
+ * useful for shapes or procedural art that you only want to draw once.
416
+ *
417
+ * While `frozen` is `true`, MinimoJS reuses the existing surface exactly as-is:
418
+ * it does not clear the canvas and does not call {@link DrawSprite.redraw}
419
+ * again, unless the surface does not exist yet or its size had to be rebuilt.
420
+ *
421
+ * This means freezing is a rendering optimization and content-preservation
422
+ * flag, not a separate caching system. You can switch `frozen` on or off at
423
+ * runtime whenever it makes sense for your sprite.
424
+ *
425
+ * The local drawing coordinate system uses the sprite surface itself:
426
+ * - `(0, 0)` is the top-left corner of the internal canvas
427
+ * - `width` / `height` match the sprite's logical size before `scale`
428
+ * - draw centered content yourself if you want the visual origin in the middle
429
+ *
430
+ * Override {@link DrawSprite.redraw} in a subclass, or assign your own method
431
+ * on an instance if you prefer an inline style in JavaScript.
432
+ */
433
+ export class DrawSprite extends BaseSprite {
434
+ get width() {
435
+ return this._width;
436
+ }
437
+ get height() {
438
+ return this._height;
439
+ }
440
+ /**
441
+ * Creates a new dynamic canvas-backed sprite.
442
+ *
443
+ * @param width - Internal canvas width in pixels. Minimum `1`.
444
+ * @param height - Internal canvas height in pixels. Minimum `1`.
445
+ * @param x - Initial X position in world space (center), in pixels. Default: `0`.
446
+ * @param y - Initial Y position in world space (center), in pixels. Default: `0`.
447
+ */
448
+ constructor(width, height, x = 0, y = 0) {
449
+ super();
450
+ /** @internal */
451
+ this._surface = null;
452
+ /** @internal */
453
+ this._surfaceCtx = null;
454
+ /** @internal */
455
+ this._lastRedrawPassId = -1;
456
+ /**
457
+ * When `false` (default), MinimoJS clears this sprite's internal canvas and
458
+ * calls {@link DrawSprite.redraw} on every render pass.
459
+ *
460
+ * When `true`, MinimoJS keeps and reuses the existing canvas contents without
461
+ * clearing or repainting them again, unless the internal surface does not yet
462
+ * exist or had to be resized.
463
+ *
464
+ * Use this when your procedural drawing becomes static after its first paint,
465
+ * or when you want explicit manual control over when the sprite is redrawn by
466
+ * toggling `frozen` at runtime.
467
+ */
468
+ this.frozen = false;
469
+ this._surfaceId = DrawSprite._nextSurfaceId++;
470
+ this._width = Math.max(1, Math.round(Number.isFinite(width) ? width : 1));
471
+ this._height = Math.max(1, Math.round(Number.isFinite(height) ? height : 1));
472
+ this.x = x;
473
+ this.y = y;
474
+ }
475
+ /**
476
+ * Called by the engine whenever the sprite's internal surface must be repainted.
477
+ *
478
+ * The provided context is already cleared and reset to the default 2D canvas
479
+ * state for the current surface size. MinimoJS repaints each `DrawSprite` at
480
+ * most once per render pass while it is not {@link DrawSprite.frozen}, so
481
+ * repeated snapshot reads during the same frame reuse the already-redrawn
482
+ * surface.
483
+ *
484
+ * @param ctx - The sprite's internal 2D drawing context.
485
+ */
486
+ redraw(_ctx) { }
487
+ getRenderCacheKey() {
488
+ return `draw:${this._surfaceId}`;
489
+ }
490
+ }
491
+ DrawSprite._nextSurfaceId = 1;
386
492
  /**
387
493
  * A renderable text actor that participates in the same animation/effects
388
494
  * pipeline as regular sprites.
@@ -2540,6 +2646,7 @@ export class Game {
2540
2646
  // Private — rendering
2541
2647
  // -------------------------------------------------------------------------
2542
2648
  /** @internal */ _onLoopFrameCallback(dt, dtMs) {
2649
+ this._renderSystem.beginDynamicSurfacePass();
2543
2650
  if (this._transitionSystem.isActive) {
2544
2651
  this._transitionSystem.update(dtMs);
2545
2652
  if (this._transitionSystem.isActive) {
@@ -2684,6 +2791,7 @@ export class Game {
2684
2791
  });
2685
2792
  }
2686
2793
  /** @internal */ _captureFrameSnapshot() {
2794
+ this._renderSystem.beginDynamicSurfacePass();
2687
2795
  return this._renderSystem.captureFrame({
2688
2796
  canvas: this._canvas,
2689
2797
  context: this._ctx,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimojs",
3
- "version": "1.0.0-alpha.16",
3
+ "version": "1.0.0-alpha.17",
4
4
  "description": "MinimoJS v1 — ultra-minimal, flat, deterministic 2D web game engine. Emoji-only sprites, rAF loop, TypeScript-first, LLM-friendly.",
5
5
  "type": "module",
6
6
  "main": "dist/minimo.js",