minimojs 1.0.0-alpha.15 → 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 +22 -1
- package/dist/internal/PhysicsSystem.js +12 -12
- package/dist/internal/RenderSystem.js +99 -0
- package/dist/minimo.d.ts +150 -0
- package/dist/minimo.js +194 -0
- package/package.json +1 -1
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:
|
|
@@ -37,12 +37,12 @@ export class PhysicsSystem {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
overlap(a, b) {
|
|
40
|
-
const halfWidthA = a.
|
|
41
|
-
const halfHeightA = a.
|
|
42
|
-
const halfWidthB = b.
|
|
43
|
-
const halfHeightB = b.
|
|
44
|
-
return (Math.abs(a.
|
|
45
|
-
Math.abs(a.
|
|
40
|
+
const halfWidthA = a.bodyDisplayWidth / 2;
|
|
41
|
+
const halfHeightA = a.bodyDisplayHeight / 2;
|
|
42
|
+
const halfWidthB = b.bodyDisplayWidth / 2;
|
|
43
|
+
const halfHeightB = b.bodyDisplayHeight / 2;
|
|
44
|
+
return (Math.abs(a.bodyCenterX - b.bodyCenterX) < halfWidthA + halfWidthB &&
|
|
45
|
+
Math.abs(a.bodyCenterY - b.bodyCenterY) < halfHeightA + halfHeightB);
|
|
46
46
|
}
|
|
47
47
|
overlapAny(listA, listB) {
|
|
48
48
|
for (const a of listA) {
|
|
@@ -100,12 +100,12 @@ export class PhysicsSystem {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
getCollisionResolution(a, b) {
|
|
103
|
-
const halfWidthA = a.
|
|
104
|
-
const halfHeightA = a.
|
|
105
|
-
const halfWidthB = b.
|
|
106
|
-
const halfHeightB = b.
|
|
107
|
-
const dx = b.
|
|
108
|
-
const dy = b.
|
|
103
|
+
const halfWidthA = a.bodyDisplayWidth / 2;
|
|
104
|
+
const halfHeightA = a.bodyDisplayHeight / 2;
|
|
105
|
+
const halfWidthB = b.bodyDisplayWidth / 2;
|
|
106
|
+
const halfHeightB = b.bodyDisplayHeight / 2;
|
|
107
|
+
const dx = b.bodyCenterX - a.bodyCenterX;
|
|
108
|
+
const dy = b.bodyCenterY - a.bodyCenterY;
|
|
109
109
|
const overlapX = halfWidthA + halfWidthB - Math.abs(dx);
|
|
110
110
|
const overlapY = halfHeightA + halfHeightB - Math.abs(dy);
|
|
111
111
|
if (overlapX <= 0 || overlapY <= 0) {
|
|
@@ -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
|
}
|
|
@@ -196,6 +200,60 @@ export class RenderSystem {
|
|
|
196
200
|
ctx.fillText(entry.text, entry.x, entry.y);
|
|
197
201
|
ctx.restore();
|
|
198
202
|
}
|
|
203
|
+
if (options.debugBodies) {
|
|
204
|
+
this.drawBodyDebugOverlay(ctx, sorted, options.scrollX, options.scrollY);
|
|
205
|
+
}
|
|
206
|
+
if (options.debugInputAreas) {
|
|
207
|
+
this.drawInputDebugOverlay(ctx, sorted, options.scrollX, options.scrollY);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
drawBodyDebugOverlay(ctx, sprites, scrollX, scrollY) {
|
|
211
|
+
ctx.save();
|
|
212
|
+
ctx.setLineDash([6, 4]);
|
|
213
|
+
ctx.lineWidth = 2;
|
|
214
|
+
for (const sprite of sprites) {
|
|
215
|
+
const halfWidth = sprite.bodyDisplayWidth / 2;
|
|
216
|
+
const halfHeight = sprite.bodyDisplayHeight / 2;
|
|
217
|
+
const centerX = sprite.ignoreScroll ? sprite.bodyCenterX : sprite.bodyCenterX - scrollX;
|
|
218
|
+
const centerY = sprite.ignoreScroll ? sprite.bodyCenterY : sprite.bodyCenterY - scrollY;
|
|
219
|
+
const left = Math.round(centerX - halfWidth) + 0.5;
|
|
220
|
+
const top = Math.round(centerY - halfHeight) + 0.5;
|
|
221
|
+
const width = Math.max(1, Math.round(sprite.bodyDisplayWidth) - 1);
|
|
222
|
+
const height = Math.max(1, Math.round(sprite.bodyDisplayHeight) - 1);
|
|
223
|
+
const stroke = sprite.isStatic
|
|
224
|
+
? 'rgba(0, 217, 255, 0.95)'
|
|
225
|
+
: 'rgba(255, 77, 109, 0.95)';
|
|
226
|
+
const fill = sprite.isStatic
|
|
227
|
+
? 'rgba(0, 217, 255, 0.12)'
|
|
228
|
+
: 'rgba(255, 77, 109, 0.12)';
|
|
229
|
+
ctx.strokeStyle = stroke;
|
|
230
|
+
ctx.fillStyle = fill;
|
|
231
|
+
ctx.fillRect(left, top, width, height);
|
|
232
|
+
ctx.strokeRect(left, top, width, height);
|
|
233
|
+
}
|
|
234
|
+
ctx.restore();
|
|
235
|
+
}
|
|
236
|
+
drawInputDebugOverlay(ctx, sprites, scrollX, scrollY) {
|
|
237
|
+
ctx.save();
|
|
238
|
+
ctx.setLineDash([3, 5]);
|
|
239
|
+
ctx.lineWidth = 2;
|
|
240
|
+
for (const sprite of sprites) {
|
|
241
|
+
const halfWidth = sprite.displayWidth / 2;
|
|
242
|
+
const halfHeight = sprite.displayHeight / 2;
|
|
243
|
+
const centerX = sprite.ignoreScroll ? sprite.renderX : sprite.renderX - scrollX;
|
|
244
|
+
const centerY = sprite.ignoreScroll ? sprite.renderY : sprite.renderY - scrollY;
|
|
245
|
+
const left = Math.round(centerX - halfWidth) + 0.5;
|
|
246
|
+
const top = Math.round(centerY - halfHeight) + 0.5;
|
|
247
|
+
const width = Math.max(1, Math.round(sprite.displayWidth) - 1);
|
|
248
|
+
const height = Math.max(1, Math.round(sprite.displayHeight) - 1);
|
|
249
|
+
const stroke = "rgba(255, 196, 0, 0.98)";
|
|
250
|
+
const fill = "rgba(255, 196, 0, 0.10)";
|
|
251
|
+
ctx.strokeStyle = stroke;
|
|
252
|
+
ctx.fillStyle = fill;
|
|
253
|
+
ctx.fillRect(left, top, width, height);
|
|
254
|
+
ctx.strokeRect(left, top, width, height);
|
|
255
|
+
}
|
|
256
|
+
ctx.restore();
|
|
199
257
|
}
|
|
200
258
|
renderLoadingScreen(options) {
|
|
201
259
|
const ctx = options.context;
|
|
@@ -294,6 +352,9 @@ export class RenderSystem {
|
|
|
294
352
|
ctx.restore();
|
|
295
353
|
}
|
|
296
354
|
getRenderSurface(sprite) {
|
|
355
|
+
if (this.isDrawSprite(sprite)) {
|
|
356
|
+
return this.getDrawSurface(sprite);
|
|
357
|
+
}
|
|
297
358
|
const cacheKey = sprite.getRenderCacheKey();
|
|
298
359
|
const cached = this._surfaceCache.get(cacheKey);
|
|
299
360
|
if (cached)
|
|
@@ -413,9 +474,47 @@ export class RenderSystem {
|
|
|
413
474
|
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
414
475
|
return canvas;
|
|
415
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
|
+
}
|
|
416
512
|
isTextSprite(sprite) {
|
|
417
513
|
return "text" in sprite && "fontFamily" in sprite && "fontSize" in sprite;
|
|
418
514
|
}
|
|
515
|
+
isDrawSprite(sprite) {
|
|
516
|
+
return "redraw" in sprite && "_surface" in sprite && "_surfaceCtx" in sprite;
|
|
517
|
+
}
|
|
419
518
|
isImageSprite(sprite) {
|
|
420
519
|
return "imageKey" in sprite && "setTexture" in sprite;
|
|
421
520
|
}
|
package/dist/minimo.d.ts
CHANGED
|
@@ -252,6 +252,52 @@ export declare abstract class BaseSprite {
|
|
|
252
252
|
* Effective rendered/collision height in pixels.
|
|
253
253
|
*/
|
|
254
254
|
get displayHeight(): number;
|
|
255
|
+
/**
|
|
256
|
+
* Optional logical body width used by physics helpers and collision checks.
|
|
257
|
+
*
|
|
258
|
+
* When `null` (default), MinimoJS uses the sprite's visual {@link BaseSprite.width}.
|
|
259
|
+
* When set, this value is scaled by {@link BaseSprite.scale} the same way as the
|
|
260
|
+
* visual sprite size.
|
|
261
|
+
*/
|
|
262
|
+
bodyWidth: number | null;
|
|
263
|
+
/**
|
|
264
|
+
* Optional logical body height used by physics helpers and collision checks.
|
|
265
|
+
*
|
|
266
|
+
* When `null` (default), MinimoJS uses the sprite's visual {@link BaseSprite.height}.
|
|
267
|
+
* When set, this value is scaled by {@link BaseSprite.scale} the same way as the
|
|
268
|
+
* visual sprite size.
|
|
269
|
+
*/
|
|
270
|
+
bodyHeight: number | null;
|
|
271
|
+
/**
|
|
272
|
+
* Horizontal body offset, in local sprite pixels before scale is applied.
|
|
273
|
+
*
|
|
274
|
+
* Positive values move the collision body to the right of the sprite's rendered center.
|
|
275
|
+
* Negative values move it to the left.
|
|
276
|
+
*/
|
|
277
|
+
bodyOffsetX: number;
|
|
278
|
+
/**
|
|
279
|
+
* Vertical body offset, in local sprite pixels before scale is applied.
|
|
280
|
+
*
|
|
281
|
+
* Positive values move the collision body downward relative to the sprite's
|
|
282
|
+
* rendered center. Negative values move it upward.
|
|
283
|
+
*/
|
|
284
|
+
bodyOffsetY: number;
|
|
285
|
+
/**
|
|
286
|
+
* Effective collision-body width in pixels after applying {@link BaseSprite.scale}.
|
|
287
|
+
*/
|
|
288
|
+
get bodyDisplayWidth(): number;
|
|
289
|
+
/**
|
|
290
|
+
* Effective collision-body height in pixels after applying {@link BaseSprite.scale}.
|
|
291
|
+
*/
|
|
292
|
+
get bodyDisplayHeight(): number;
|
|
293
|
+
/**
|
|
294
|
+
* Resolved body center X used internally by physics helpers and collision checks.
|
|
295
|
+
*/
|
|
296
|
+
get bodyCenterX(): number;
|
|
297
|
+
/**
|
|
298
|
+
* Resolved body center Y used internally by physics helpers and collision checks.
|
|
299
|
+
*/
|
|
300
|
+
get bodyCenterY(): number;
|
|
255
301
|
/**
|
|
256
302
|
* CSS text color used when rendering this sprite.
|
|
257
303
|
*
|
|
@@ -425,6 +471,96 @@ export declare class ImageSprite extends BaseSprite {
|
|
|
425
471
|
private getResolvedImage;
|
|
426
472
|
private assertTextureAvailable;
|
|
427
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
|
+
}
|
|
428
564
|
/**
|
|
429
565
|
* A renderable text actor that participates in the same animation/effects
|
|
430
566
|
* pipeline as regular sprites.
|
|
@@ -982,6 +1118,20 @@ export declare class Game {
|
|
|
982
1118
|
from: string;
|
|
983
1119
|
to: string;
|
|
984
1120
|
} | null;
|
|
1121
|
+
/**
|
|
1122
|
+
* When `true`, MinimoJS draws every sprite's collision body as an overlay.
|
|
1123
|
+
*
|
|
1124
|
+
* This is a runtime debugging aid only. It does not change collisions,
|
|
1125
|
+
* input, rendering order, or physics behavior.
|
|
1126
|
+
*/
|
|
1127
|
+
debugBodies: boolean;
|
|
1128
|
+
/**
|
|
1129
|
+
* When `true`, MinimoJS draws every sprite's input area as an overlay.
|
|
1130
|
+
*
|
|
1131
|
+
* This is a runtime debugging aid only. It does not change input behavior,
|
|
1132
|
+
* physics, or rendering.
|
|
1133
|
+
*/
|
|
1134
|
+
debugInputAreas: boolean;
|
|
985
1135
|
/**
|
|
986
1136
|
* Background color for the full web page (`document.body`).
|
|
987
1137
|
* Set to any valid CSS color string. Default: `null` (engine leaves page background unchanged).
|
package/dist/minimo.js
CHANGED
|
@@ -53,6 +53,36 @@ export class BaseSprite {
|
|
|
53
53
|
this.scale = 1;
|
|
54
54
|
/** @internal */
|
|
55
55
|
this._renderData = null;
|
|
56
|
+
/**
|
|
57
|
+
* Optional logical body width used by physics helpers and collision checks.
|
|
58
|
+
*
|
|
59
|
+
* When `null` (default), MinimoJS uses the sprite's visual {@link BaseSprite.width}.
|
|
60
|
+
* When set, this value is scaled by {@link BaseSprite.scale} the same way as the
|
|
61
|
+
* visual sprite size.
|
|
62
|
+
*/
|
|
63
|
+
this.bodyWidth = null;
|
|
64
|
+
/**
|
|
65
|
+
* Optional logical body height used by physics helpers and collision checks.
|
|
66
|
+
*
|
|
67
|
+
* When `null` (default), MinimoJS uses the sprite's visual {@link BaseSprite.height}.
|
|
68
|
+
* When set, this value is scaled by {@link BaseSprite.scale} the same way as the
|
|
69
|
+
* visual sprite size.
|
|
70
|
+
*/
|
|
71
|
+
this.bodyHeight = null;
|
|
72
|
+
/**
|
|
73
|
+
* Horizontal body offset, in local sprite pixels before scale is applied.
|
|
74
|
+
*
|
|
75
|
+
* Positive values move the collision body to the right of the sprite's rendered center.
|
|
76
|
+
* Negative values move it to the left.
|
|
77
|
+
*/
|
|
78
|
+
this.bodyOffsetX = 0;
|
|
79
|
+
/**
|
|
80
|
+
* Vertical body offset, in local sprite pixels before scale is applied.
|
|
81
|
+
*
|
|
82
|
+
* Positive values move the collision body downward relative to the sprite's
|
|
83
|
+
* rendered center. Negative values move it upward.
|
|
84
|
+
*/
|
|
85
|
+
this.bodyOffsetY = 0;
|
|
56
86
|
/**
|
|
57
87
|
* CSS text color used when rendering this sprite.
|
|
58
88
|
*
|
|
@@ -179,6 +209,44 @@ export class BaseSprite {
|
|
|
179
209
|
const safeScale = Number.isFinite(this.scale) ? this.scale : 1;
|
|
180
210
|
return this.height * Math.max(0, safeScale);
|
|
181
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Effective collision-body width in pixels after applying {@link BaseSprite.scale}.
|
|
214
|
+
*/
|
|
215
|
+
get bodyDisplayWidth() {
|
|
216
|
+
const safeScale = Number.isFinite(this.scale) ? this.scale : 1;
|
|
217
|
+
const baseWidth = typeof this.bodyWidth === "number" &&
|
|
218
|
+
Number.isFinite(this.bodyWidth) &&
|
|
219
|
+
this.bodyWidth > 0
|
|
220
|
+
? this.bodyWidth
|
|
221
|
+
: this.width;
|
|
222
|
+
return baseWidth * Math.max(0, safeScale);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Effective collision-body height in pixels after applying {@link BaseSprite.scale}.
|
|
226
|
+
*/
|
|
227
|
+
get bodyDisplayHeight() {
|
|
228
|
+
const safeScale = Number.isFinite(this.scale) ? this.scale : 1;
|
|
229
|
+
const baseHeight = typeof this.bodyHeight === "number" &&
|
|
230
|
+
Number.isFinite(this.bodyHeight) &&
|
|
231
|
+
this.bodyHeight > 0
|
|
232
|
+
? this.bodyHeight
|
|
233
|
+
: this.height;
|
|
234
|
+
return baseHeight * Math.max(0, safeScale);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Resolved body center X used internally by physics helpers and collision checks.
|
|
238
|
+
*/
|
|
239
|
+
get bodyCenterX() {
|
|
240
|
+
const safeScale = Number.isFinite(this.scale) ? this.scale : 1;
|
|
241
|
+
return this.renderX + this.bodyOffsetX * Math.max(0, safeScale);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolved body center Y used internally by physics helpers and collision checks.
|
|
245
|
+
*/
|
|
246
|
+
get bodyCenterY() {
|
|
247
|
+
const safeScale = Number.isFinite(this.scale) ? this.scale : 1;
|
|
248
|
+
return this.renderY + this.bodyOffsetY * Math.max(0, safeScale);
|
|
249
|
+
}
|
|
182
250
|
getAnchorOffsetX() {
|
|
183
251
|
return 0;
|
|
184
252
|
}
|
|
@@ -315,6 +383,112 @@ export class ImageSprite extends BaseSprite {
|
|
|
315
383
|
}
|
|
316
384
|
}
|
|
317
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;
|
|
318
492
|
/**
|
|
319
493
|
* A renderable text actor that participates in the same animation/effects
|
|
320
494
|
* pipeline as regular sprites.
|
|
@@ -1053,6 +1227,20 @@ export class Game {
|
|
|
1053
1227
|
* ```
|
|
1054
1228
|
*/
|
|
1055
1229
|
this.backgroundGradient = null;
|
|
1230
|
+
/**
|
|
1231
|
+
* When `true`, MinimoJS draws every sprite's collision body as an overlay.
|
|
1232
|
+
*
|
|
1233
|
+
* This is a runtime debugging aid only. It does not change collisions,
|
|
1234
|
+
* input, rendering order, or physics behavior.
|
|
1235
|
+
*/
|
|
1236
|
+
this.debugBodies = false;
|
|
1237
|
+
/**
|
|
1238
|
+
* When `true`, MinimoJS draws every sprite's input area as an overlay.
|
|
1239
|
+
*
|
|
1240
|
+
* This is a runtime debugging aid only. It does not change input behavior,
|
|
1241
|
+
* physics, or rendering.
|
|
1242
|
+
*/
|
|
1243
|
+
this.debugInputAreas = false;
|
|
1056
1244
|
/**
|
|
1057
1245
|
* Background color for the full web page (`document.body`).
|
|
1058
1246
|
* Set to any valid CSS color string. Default: `null` (engine leaves page background unchanged).
|
|
@@ -2458,6 +2646,7 @@ export class Game {
|
|
|
2458
2646
|
// Private — rendering
|
|
2459
2647
|
// -------------------------------------------------------------------------
|
|
2460
2648
|
/** @internal */ _onLoopFrameCallback(dt, dtMs) {
|
|
2649
|
+
this._renderSystem.beginDynamicSurfacePass();
|
|
2461
2650
|
if (this._transitionSystem.isActive) {
|
|
2462
2651
|
this._transitionSystem.update(dtMs);
|
|
2463
2652
|
if (this._transitionSystem.isActive) {
|
|
@@ -2584,6 +2773,8 @@ export class Game {
|
|
|
2584
2773
|
background: this.background,
|
|
2585
2774
|
backgroundGradient: this.backgroundGradient,
|
|
2586
2775
|
pageBackground: this.pageBackground,
|
|
2776
|
+
debugBodies: this.debugBodies,
|
|
2777
|
+
debugInputAreas: this.debugInputAreas,
|
|
2587
2778
|
});
|
|
2588
2779
|
}
|
|
2589
2780
|
/** @internal */ _renderTransition() {
|
|
@@ -2600,6 +2791,7 @@ export class Game {
|
|
|
2600
2791
|
});
|
|
2601
2792
|
}
|
|
2602
2793
|
/** @internal */ _captureFrameSnapshot() {
|
|
2794
|
+
this._renderSystem.beginDynamicSurfacePass();
|
|
2603
2795
|
return this._renderSystem.captureFrame({
|
|
2604
2796
|
canvas: this._canvas,
|
|
2605
2797
|
context: this._ctx,
|
|
@@ -2615,6 +2807,8 @@ export class Game {
|
|
|
2615
2807
|
background: this.background,
|
|
2616
2808
|
backgroundGradient: this.backgroundGradient,
|
|
2617
2809
|
pageBackground: this.pageBackground,
|
|
2810
|
+
debugBodies: this.debugBodies,
|
|
2811
|
+
debugInputAreas: this.debugInputAreas,
|
|
2618
2812
|
});
|
|
2619
2813
|
}
|
|
2620
2814
|
/** @internal */
|
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.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",
|