minimojs 1.0.0-alpha.41 → 1.0.0-alpha.43
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/AssetSystem.d.ts +2 -0
- package/dist/internal/AssetSystem.js +145 -5
- package/dist/internal/DesignNamingCheck.js +7 -1
- package/dist/internal/RenderSystem.js +28 -5
- package/dist/minimo-format.d.ts +225 -14
- package/dist/minimo-format.js +261 -8
- package/dist/minimo-leaderboard.d.ts +3 -3
- package/dist/minimo-leaderboard.js +3 -3
- package/dist/minimo-multiplayer.d.ts +8 -8
- package/dist/minimo-multiplayer.js +9 -9
- package/dist/minimo-outline.d.ts +106 -0
- package/dist/minimo-outline.js +299 -0
- package/dist/minimo.d.ts +13 -2
- package/dist/minimo.js +64 -4
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { DrawSprite, EmojiSprite, Game, ImageSprite, TextSprite } from "./minimo.js";
|
|
2
|
+
/** The color of an outline whose options name none. */
|
|
3
|
+
const DEFAULT_COLOR = "#ffffff";
|
|
4
|
+
/** A pixel at least this opaque is inside the shape the outline goes around. */
|
|
5
|
+
const INSIDE_ALPHA = 128;
|
|
6
|
+
/**
|
|
7
|
+
* Draws a solid outline around the visible shape of image, emoji, and text
|
|
8
|
+
* sprites, so a character or an answer stands out from what is behind it.
|
|
9
|
+
*
|
|
10
|
+
* The outline follows the shape's transparent edges, not its box, and is
|
|
11
|
+
* part of the sprite's own picture: it moves, turns, flips, scales, fades,
|
|
12
|
+
* and changes layer with the sprite, and it shows in trails, wipes, and
|
|
13
|
+
* explosions of it. Only what is drawn grows. The sprite's size, collision
|
|
14
|
+
* body, the area that takes taps, and its pivot stay as they were.
|
|
15
|
+
*
|
|
16
|
+
* Each outline is built once and kept, so moving or turning a sprite costs
|
|
17
|
+
* nothing more. A new texture, new text, or a new width or color builds it
|
|
18
|
+
* again. Sprites that look the same share one outline.
|
|
19
|
+
*
|
|
20
|
+
* {@link DrawSprite}s, and the sprites made from it such as a flame or a die,
|
|
21
|
+
* are not outlined: they are redrawn every frame, which would rebuild the
|
|
22
|
+
* outline every frame.
|
|
23
|
+
*
|
|
24
|
+
* Create the module in the scene's `onCreate()`. It needs no `update()`.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { Outline } from "./minimo-outline.js";
|
|
29
|
+
*
|
|
30
|
+
* onCreate() {
|
|
31
|
+
* const outlines = new Outline(game);
|
|
32
|
+
* const frog = game.add(new ImageSprite(game, "frog", 360, 640));
|
|
33
|
+
* outlines.set(frog, { width: 4, color: "#fff5df" });
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* ## In a design
|
|
38
|
+
*
|
|
39
|
+
* An `image`, `emoji`, or `text` object in a design takes an outline through
|
|
40
|
+
* two properties, which Studio shows among the object's properties:
|
|
41
|
+
*
|
|
42
|
+
* - `outlineWidth`: the width in the object's own pixels. `0`, the default,
|
|
43
|
+
* draws none.
|
|
44
|
+
* - `outlineColor`: its CSS color. Default `"#ffffff"`.
|
|
45
|
+
*
|
|
46
|
+
* ```json
|
|
47
|
+
* { "id": "…", "type": "image", "name": "frog", "imageKey": "frog",
|
|
48
|
+
* "x": 360, "y": 640, "outlineWidth": 4, "outlineColor": "#fff5df" }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* A design holding either property has `"version": 3`. The outline is a look
|
|
52
|
+
* of the object, not an object of its own: there is no `module` object for it.
|
|
53
|
+
* Create the module before the design's objects, or creating an object whose
|
|
54
|
+
* `outlineWidth` is above `0` throws an error naming it:
|
|
55
|
+
*
|
|
56
|
+
* ```ts
|
|
57
|
+
* onCreate() {
|
|
58
|
+
* this.outlines = new Outline(game);
|
|
59
|
+
* const screen = game.createObjectList(menu.objects);
|
|
60
|
+
* const frog = screen.findByName("frog");
|
|
61
|
+
* // Change it while the game plays, such as on the chosen answer:
|
|
62
|
+
* this.outlines.set(frog, { width: 6, color: "#ffd84a" });
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export class Outline {
|
|
67
|
+
constructor(game) {
|
|
68
|
+
if (!(game instanceof Game)) {
|
|
69
|
+
throw new Error("MinimoJS: new Outline(game) needs the game, such as new Outline(game) in a scene's onCreate().");
|
|
70
|
+
}
|
|
71
|
+
this.game = game;
|
|
72
|
+
game._registerDesignDecoration("outline", (sprite, obj) => this._fromDesign(sprite, obj));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Outlines `sprite`, or changes its outline. A width of `0` removes it.
|
|
76
|
+
* @param sprite - An {@link ImageSprite}, {@link EmojiSprite}, or {@link TextSprite}, or a subclass of one.
|
|
77
|
+
* @param options - The width, and the color, white by default.
|
|
78
|
+
*/
|
|
79
|
+
set(sprite, options) {
|
|
80
|
+
checkSprite(sprite, "set");
|
|
81
|
+
if (options === null || typeof options !== "object") {
|
|
82
|
+
throw new Error("MinimoJS: Outline.set(sprite, options) needs options such as { width: 4, color: \"#ffffff\" }.");
|
|
83
|
+
}
|
|
84
|
+
const { width, color = DEFAULT_COLOR } = options;
|
|
85
|
+
if (typeof width !== "number" || !Number.isFinite(width) || width < 0) {
|
|
86
|
+
throw new Error(`MinimoJS: An outline's width is a number of pixels of at least 0, found ${String(width)}.`);
|
|
87
|
+
}
|
|
88
|
+
if (typeof color !== "string" || color.trim() === "") {
|
|
89
|
+
throw new Error(`MinimoJS: An outline's color is a CSS color, such as "#ffffff", found ${String(color)}.`);
|
|
90
|
+
}
|
|
91
|
+
if (width === 0) {
|
|
92
|
+
this.remove(sprite);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const current = sprite._surfaceEffect;
|
|
96
|
+
if (current !== null && !(current instanceof OutlineEffect)) {
|
|
97
|
+
throw new Error("MinimoJS: This sprite already has another module's effect, and a sprite takes one at a time.");
|
|
98
|
+
}
|
|
99
|
+
sprite._surfaceEffect = new OutlineEffect(Math.min(width, Outline.MAX_WIDTH), color);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The outline `sprite` has, or `null` when it has none.
|
|
103
|
+
* @param sprite - Any sprite.
|
|
104
|
+
*/
|
|
105
|
+
get(sprite) {
|
|
106
|
+
const effect = sprite?._surfaceEffect;
|
|
107
|
+
return effect instanceof OutlineEffect ? { width: effect.width, color: effect.color } : null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Removes the outline of `sprite`. Nothing happens when it has none.
|
|
111
|
+
* @param sprite - Any sprite.
|
|
112
|
+
*/
|
|
113
|
+
remove(sprite) {
|
|
114
|
+
if (sprite?._surfaceEffect instanceof OutlineEffect)
|
|
115
|
+
sprite._surfaceEffect = null;
|
|
116
|
+
}
|
|
117
|
+
/** @internal Applies a design object's outline to the sprite built for it. */
|
|
118
|
+
_fromDesign(sprite, obj) {
|
|
119
|
+
const outline = obj;
|
|
120
|
+
this.set(sprite, { width: outline.outlineWidth ?? 0, color: outline.outlineColor ?? DEFAULT_COLOR });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** The widest outline drawn, in the sprite's own pixels. */
|
|
124
|
+
Outline.MAX_WIDTH = 64;
|
|
125
|
+
function checkSprite(sprite, method) {
|
|
126
|
+
if (sprite instanceof DrawSprite) {
|
|
127
|
+
throw new Error(`MinimoJS: Outline.${method}() does not outline a DrawSprite, or a sprite made from one such as a flame or a die: it is redrawn every frame, so its outline would be rebuilt every frame. Outline an ImageSprite, EmojiSprite, or TextSprite.`);
|
|
128
|
+
}
|
|
129
|
+
if (!(sprite instanceof ImageSprite || sprite instanceof EmojiSprite || sprite instanceof TextSprite)) {
|
|
130
|
+
throw new Error(`MinimoJS: Outline.${method}() outlines an ImageSprite, EmojiSprite, or TextSprite, found ${describe(sprite)}.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function describe(value) {
|
|
134
|
+
if (value === null)
|
|
135
|
+
return "null";
|
|
136
|
+
if (typeof value === "object")
|
|
137
|
+
return value.constructor?.name ?? "an object";
|
|
138
|
+
return typeof value;
|
|
139
|
+
}
|
|
140
|
+
/** The outline as the core applies it to a sprite's surface. */
|
|
141
|
+
class OutlineEffect {
|
|
142
|
+
constructor(width, color) {
|
|
143
|
+
this.width = width;
|
|
144
|
+
this.color = color;
|
|
145
|
+
this.key = `outline:${width}:${color}`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Returns a new canvas, larger than `base` by the outline's width rounded up
|
|
149
|
+
* on every side, holding the outline with `base` drawn over it.
|
|
150
|
+
*/
|
|
151
|
+
apply(base) {
|
|
152
|
+
const pad = Math.ceil(this.width);
|
|
153
|
+
const width = base.width + pad * 2;
|
|
154
|
+
const height = base.height + pad * 2;
|
|
155
|
+
const mask = createCanvas(width, height);
|
|
156
|
+
const maskCtx = context(mask);
|
|
157
|
+
if (!drawDistanceMask(maskCtx, base, pad, this.width)) {
|
|
158
|
+
drawStampedMask(maskCtx, base, pad, this.width);
|
|
159
|
+
}
|
|
160
|
+
// Keep the mask's coverage, painted in the outline's color, so a
|
|
161
|
+
// translucent color stays as translucent where outlines overlap.
|
|
162
|
+
maskCtx.globalCompositeOperation = "source-in";
|
|
163
|
+
maskCtx.fillStyle = this.color;
|
|
164
|
+
maskCtx.fillRect(0, 0, width, height);
|
|
165
|
+
const result = createCanvas(width, height);
|
|
166
|
+
const ctx = context(result);
|
|
167
|
+
ctx.drawImage(mask, 0, 0);
|
|
168
|
+
ctx.drawImage(base, pad, pad);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function createCanvas(width, height) {
|
|
173
|
+
const canvas = document.createElement("canvas");
|
|
174
|
+
canvas.width = Math.max(1, width);
|
|
175
|
+
canvas.height = Math.max(1, height);
|
|
176
|
+
return canvas;
|
|
177
|
+
}
|
|
178
|
+
function context(canvas) {
|
|
179
|
+
const ctx = canvas.getContext("2d");
|
|
180
|
+
if (!ctx)
|
|
181
|
+
throw new Error("MinimoJS: Could not acquire an outline rendering context.");
|
|
182
|
+
return ctx;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Draws into `ctx` the coverage of an outline `width` pixels wide around the
|
|
186
|
+
* pixels of `base` at least half opaque, from each pixel's distance to the
|
|
187
|
+
* nearest of them, so the outline is round and smooth at its edge and faint
|
|
188
|
+
* haze around an image does not grow one. Returns `false`, having drawn
|
|
189
|
+
* nothing, when the pixels cannot be read.
|
|
190
|
+
*/
|
|
191
|
+
function drawDistanceMask(ctx, base, pad, width) {
|
|
192
|
+
const w = ctx.canvas.width;
|
|
193
|
+
const h = ctx.canvas.height;
|
|
194
|
+
let pixels;
|
|
195
|
+
try {
|
|
196
|
+
ctx.drawImage(base, pad, pad);
|
|
197
|
+
pixels = ctx.getImageData(0, 0, w, h);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
ctx.clearRect(0, 0, w, h);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const source = pixels.data;
|
|
204
|
+
const distance = new Float64Array(w * h);
|
|
205
|
+
for (let i = 0; i < w * h; i++) {
|
|
206
|
+
distance[i] = source[i * 4 + 3] >= INSIDE_ALPHA ? 0 : Infinity;
|
|
207
|
+
}
|
|
208
|
+
squaredDistanceTransform(distance, w, h);
|
|
209
|
+
const out = ctx.createImageData(w, h);
|
|
210
|
+
const data = out.data;
|
|
211
|
+
for (let i = 0; i < w * h; i++) {
|
|
212
|
+
// A pixel next to the shape, one pixel away, is fully covered by a
|
|
213
|
+
// 1-pixel outline; one diagonal to it partly.
|
|
214
|
+
const coverage = Math.max(0, Math.min(1, width + 1 - Math.sqrt(distance[i])));
|
|
215
|
+
data[i * 4 + 3] = Math.round(coverage * 255);
|
|
216
|
+
}
|
|
217
|
+
ctx.clearRect(0, 0, w, h);
|
|
218
|
+
ctx.putImageData(out, 0, 0);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Turns `grid`, holding `0` inside the shape and `Infinity` outside, into the
|
|
223
|
+
* squared distance from each cell to the nearest cell inside, by columns and
|
|
224
|
+
* then by rows (Felzenszwalb and Huttenlocher's exact transform).
|
|
225
|
+
*/
|
|
226
|
+
function squaredDistanceTransform(grid, width, height) {
|
|
227
|
+
const size = Math.max(width, height);
|
|
228
|
+
const f = new Float64Array(size);
|
|
229
|
+
const d = new Float64Array(size);
|
|
230
|
+
const v = new Int32Array(size);
|
|
231
|
+
const z = new Float64Array(size + 1);
|
|
232
|
+
for (let x = 0; x < width; x++) {
|
|
233
|
+
for (let y = 0; y < height; y++)
|
|
234
|
+
f[y] = grid[y * width + x];
|
|
235
|
+
transform1D(f, height, d, v, z);
|
|
236
|
+
for (let y = 0; y < height; y++)
|
|
237
|
+
grid[y * width + x] = d[y];
|
|
238
|
+
}
|
|
239
|
+
for (let y = 0; y < height; y++) {
|
|
240
|
+
for (let x = 0; x < width; x++)
|
|
241
|
+
f[x] = grid[y * width + x];
|
|
242
|
+
transform1D(f, width, d, v, z);
|
|
243
|
+
for (let x = 0; x < width; x++)
|
|
244
|
+
grid[y * width + x] = d[x];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function transform1D(f, n, d, v, z) {
|
|
248
|
+
let k = -1;
|
|
249
|
+
for (let q = 0; q < n; q++) {
|
|
250
|
+
if (f[q] === Infinity)
|
|
251
|
+
continue;
|
|
252
|
+
if (k < 0) {
|
|
253
|
+
k = 0;
|
|
254
|
+
v[0] = q;
|
|
255
|
+
z[0] = -Infinity;
|
|
256
|
+
z[1] = Infinity;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
let s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
|
|
260
|
+
while (s <= z[k]) {
|
|
261
|
+
k--;
|
|
262
|
+
if (k < 0)
|
|
263
|
+
break;
|
|
264
|
+
s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
|
|
265
|
+
}
|
|
266
|
+
k++;
|
|
267
|
+
v[k] = q;
|
|
268
|
+
z[k] = k === 0 ? -Infinity : s;
|
|
269
|
+
z[k + 1] = Infinity;
|
|
270
|
+
}
|
|
271
|
+
if (k < 0) {
|
|
272
|
+
for (let q = 0; q < n; q++)
|
|
273
|
+
d[q] = Infinity;
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
let j = 0;
|
|
277
|
+
for (let q = 0; q < n; q++) {
|
|
278
|
+
while (z[j + 1] < q)
|
|
279
|
+
j++;
|
|
280
|
+
const offset = q - v[j];
|
|
281
|
+
d[q] = offset * offset + f[v[j]];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Draws the outline's coverage by stamping `base` around rings of every radius
|
|
286
|
+
* up to `width`, for an image whose pixels cannot be read, such as one from
|
|
287
|
+
* another origin. It needs no pixels, but faint haze around an image adds up
|
|
288
|
+
* into a visible outline.
|
|
289
|
+
*/
|
|
290
|
+
function drawStampedMask(ctx, base, pad, width) {
|
|
291
|
+
for (let radius = width; radius > 0; radius -= 1) {
|
|
292
|
+
const steps = Math.max(8, Math.ceil(2 * Math.PI * radius));
|
|
293
|
+
for (let i = 0; i < steps; i++) {
|
|
294
|
+
const angle = (i / steps) * Math.PI * 2;
|
|
295
|
+
ctx.drawImage(base, pad + Math.cos(angle) * radius, pad + Math.sin(angle) * radius);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
ctx.drawImage(base, pad, pad);
|
|
299
|
+
}
|
package/dist/minimo.d.ts
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
import { SoundPlayback } from "./internal/SoundSystem.js";
|
|
11
11
|
import type { PlaySoundOptions } from "./internal/SoundSystem.js";
|
|
12
12
|
import type { DesignObject } from "./minimo-format.js";
|
|
13
|
-
export { Design, parseAssetFile } from "./minimo-format.js";
|
|
13
|
+
export { Design, parseAssetFile, parseTextureAtlas, frameKey } from "./minimo-format.js";
|
|
14
14
|
export { SoundPlayback } from "./internal/SoundSystem.js";
|
|
15
15
|
export type { PlaySoundOptions, SoundPlaybackState } from "./internal/SoundSystem.js";
|
|
16
|
-
export type { AssetFile, DesignBackground, DesignEmoji, DesignFile, DesignFileV1, DesignFileV2, DesignFileV3, DesignImage, DesignModule, DesignObject, DesignObjectBase, DesignObjectType, DesignPage, DesignSpriteBase, DesignText, ImageAsset, SoundAsset, UnknownAsset, } from "./minimo-format.js";
|
|
16
|
+
export type { AssetEntry, AssetFile, AtlasAsset, DesignBackground, DesignEmoji, DesignFile, DesignFileV1, DesignFileV2, DesignFileV3, DesignImage, DesignModule, DesignObject, DesignObjectBase, DesignObjectType, DesignOutline, DesignPage, DesignSpriteBase, DesignText, ImageAsset, SoundAsset, SpritesheetAsset, TextureAtlas, TextureFrame, UnknownAsset, } from "./minimo-format.js";
|
|
17
17
|
/**
|
|
18
18
|
* Optional tuning values for {@link Game.animateExplode}.
|
|
19
19
|
*
|
|
@@ -1596,6 +1596,17 @@ export declare class Game {
|
|
|
1596
1596
|
* - An entry of a type this engine does not support is skipped, with a
|
|
1597
1597
|
* console warning naming it, so the game still loads every asset it can.
|
|
1598
1598
|
*
|
|
1599
|
+
* A `spritesheet` or `atlas` entry loads its image under its key, and its
|
|
1600
|
+
* atlas JSON with it, and once both are in makes each frame an image of its
|
|
1601
|
+
* own, keyed `<key>/<frame>`, such as `"frog/0"` or `"pond/lotus"`. A frame
|
|
1602
|
+
* key works wherever an image key does: {@link ImageSprite}, backgrounds,
|
|
1603
|
+
* designs, and {@link Game.getImage}. A trimmed atlas frame is restored to
|
|
1604
|
+
* its untrimmed size, so it measures and sits as the picture it came from.
|
|
1605
|
+
* A frame key cannot be used by any other image, sound, or texture. A sheet
|
|
1606
|
+
* whose frames cannot be made, such as an atlas that names a rectangle
|
|
1607
|
+
* outside its image, fails with its key, as a missing image does, and the
|
|
1608
|
+
* console says why. See {@link SpritesheetAsset} and {@link AtlasAsset}.
|
|
1609
|
+
*
|
|
1599
1610
|
* @param assets - The content of a `*.assets.json` file. See {@link AssetFile}.
|
|
1600
1611
|
*
|
|
1601
1612
|
* @example
|
package/dist/minimo.js
CHANGED
|
@@ -22,8 +22,8 @@ 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
|
-
import { Design, designModules, findFirst, parseAssetFile, parseDesignObject } from "./minimo-format.js";
|
|
26
|
-
export { Design, parseAssetFile } from "./minimo-format.js";
|
|
25
|
+
import { ASSET_TYPES, Design, designDecorations, designModules, enabledDecorations, findFirst, parseAssetFile, parseDesignObject, } from "./minimo-format.js";
|
|
26
|
+
export { Design, parseAssetFile, parseTextureAtlas, frameKey } from "./minimo-format.js";
|
|
27
27
|
export { SoundPlayback } from "./internal/SoundSystem.js";
|
|
28
28
|
/** How long a scene may load before it dims and its progress ring appears, so a fast load shows neither. */
|
|
29
29
|
const SCENE_LOADING_INDICATOR_DELAY_MS = 250;
|
|
@@ -60,6 +60,14 @@ export class Sprite {
|
|
|
60
60
|
this.scale = 1;
|
|
61
61
|
/** @internal */
|
|
62
62
|
this._renderData = null;
|
|
63
|
+
/**
|
|
64
|
+
* A change an optional module makes to how this sprite looks, such as the
|
|
65
|
+
* `outline` module's outline, applied to its drawn surface. `null` draws
|
|
66
|
+
* the surface as it is. See {@link SpriteSurfaceEffect}.
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
this._surfaceEffect = null;
|
|
63
71
|
/**
|
|
64
72
|
* Optional logical body width used by physics helpers and collision checks.
|
|
65
73
|
*
|
|
@@ -946,6 +954,8 @@ const DESIGN_OBJECT_CLASSES = {
|
|
|
946
954
|
text: TextSprite,
|
|
947
955
|
background: BackgroundLayer,
|
|
948
956
|
};
|
|
957
|
+
/** Every property of a design object that belongs to a decoration's module. */
|
|
958
|
+
const DECORATION_PROPERTIES = new Set(Object.values(designDecorations).flatMap((schema) => schema.properties));
|
|
949
959
|
function describeDesignObject(obj) {
|
|
950
960
|
return obj.name === undefined ? `the object ${obj.id}` : `the object "${obj.name}" (${obj.id})`;
|
|
951
961
|
}
|
|
@@ -1388,6 +1398,7 @@ export class Game {
|
|
|
1388
1398
|
/** @internal */ this._currentScene = null;
|
|
1389
1399
|
/** @internal */ this._buildersByTag = new Map();
|
|
1390
1400
|
/** @internal */ this._moduleBuilders = new Map();
|
|
1401
|
+
/** @internal */ this._decorationBuilders = new Map();
|
|
1391
1402
|
/**
|
|
1392
1403
|
* Horizontal scroll offset of the world camera, in pixels.
|
|
1393
1404
|
* The canvas viewport is shifted left by `scrollX` — sprites with higher `x`
|
|
@@ -1766,6 +1777,17 @@ export class Game {
|
|
|
1766
1777
|
* - An entry of a type this engine does not support is skipped, with a
|
|
1767
1778
|
* console warning naming it, so the game still loads every asset it can.
|
|
1768
1779
|
*
|
|
1780
|
+
* A `spritesheet` or `atlas` entry loads its image under its key, and its
|
|
1781
|
+
* atlas JSON with it, and once both are in makes each frame an image of its
|
|
1782
|
+
* own, keyed `<key>/<frame>`, such as `"frog/0"` or `"pond/lotus"`. A frame
|
|
1783
|
+
* key works wherever an image key does: {@link ImageSprite}, backgrounds,
|
|
1784
|
+
* designs, and {@link Game.getImage}. A trimmed atlas frame is restored to
|
|
1785
|
+
* its untrimmed size, so it measures and sits as the picture it came from.
|
|
1786
|
+
* A frame key cannot be used by any other image, sound, or texture. A sheet
|
|
1787
|
+
* whose frames cannot be made, such as an atlas that names a rectangle
|
|
1788
|
+
* outside its image, fails with its key, as a missing image does, and the
|
|
1789
|
+
* console says why. See {@link SpritesheetAsset} and {@link AtlasAsset}.
|
|
1790
|
+
*
|
|
1769
1791
|
* @param assets - The content of a `*.assets.json` file. See {@link AssetFile}.
|
|
1770
1792
|
*
|
|
1771
1793
|
* @example
|
|
@@ -1784,7 +1806,7 @@ export class Game {
|
|
|
1784
1806
|
if (!isFirstLoad)
|
|
1785
1807
|
return;
|
|
1786
1808
|
for (const entry of file.assets) {
|
|
1787
|
-
if (
|
|
1809
|
+
if (!ASSET_TYPES.includes(entry.type)) {
|
|
1788
1810
|
console.warn(`MinimoJS: Skipped the asset "${entry.key}", whose type "${entry.type}" this version of MinimoJS does not support.`);
|
|
1789
1811
|
}
|
|
1790
1812
|
}
|
|
@@ -1884,6 +1906,7 @@ export class Game {
|
|
|
1884
1906
|
}
|
|
1885
1907
|
const obj = parseDesignObject(data, "the object", "design object");
|
|
1886
1908
|
this._assertDesignImageLoaded(obj);
|
|
1909
|
+
this._assertDesignDecorationsReady(obj);
|
|
1887
1910
|
const created = this._buildDesignObject(obj);
|
|
1888
1911
|
if (options.add !== false)
|
|
1889
1912
|
this._addDesignObject(created);
|
|
@@ -1922,8 +1945,10 @@ export class Game {
|
|
|
1922
1945
|
throw new Error("MinimoJS: createObjectList() takes a list of object data, such as design.objects or design.findAllByTag(\"enemy\").");
|
|
1923
1946
|
}
|
|
1924
1947
|
const parsed = objects.map((data, index) => parseDesignObject(data, `objects[${index}]`, "design object"));
|
|
1925
|
-
for (const obj of parsed)
|
|
1948
|
+
for (const obj of parsed) {
|
|
1926
1949
|
this._assertDesignImageLoaded(obj);
|
|
1950
|
+
this._assertDesignDecorationsReady(obj);
|
|
1951
|
+
}
|
|
1927
1952
|
const created = parsed.map((obj) => this._buildDesignObject(obj));
|
|
1928
1953
|
if (options.add !== false) {
|
|
1929
1954
|
for (const obj of created)
|
|
@@ -1997,6 +2022,31 @@ export class Game {
|
|
|
1997
2022
|
if (!this._moduleBuilders.has(module))
|
|
1998
2023
|
this._moduleBuilders.set(module, builder);
|
|
1999
2024
|
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Makes {@link Game.createObject} and {@link Game.createObjectList} hand
|
|
2027
|
+
* the design objects that use the decoration `module`, such as an outline,
|
|
2028
|
+
* to `builder` once built. A module calls it when it is created; the first
|
|
2029
|
+
* module created for an id keeps it until {@link Game.reset} clears it.
|
|
2030
|
+
*
|
|
2031
|
+
* @internal
|
|
2032
|
+
*/
|
|
2033
|
+
_registerDesignDecoration(module, builder) {
|
|
2034
|
+
if (!this._decorationBuilders.has(module))
|
|
2035
|
+
this._decorationBuilders.set(module, builder);
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Checks that every decoration `obj` turns on has its module created, before
|
|
2039
|
+
* anything is built, so a list with one such object creates nothing.
|
|
2040
|
+
*
|
|
2041
|
+
* @internal
|
|
2042
|
+
*/
|
|
2043
|
+
_assertDesignDecorationsReady(obj) {
|
|
2044
|
+
for (const schema of enabledDecorations(obj)) {
|
|
2045
|
+
if (this._decorationBuilders.has(schema.module))
|
|
2046
|
+
continue;
|
|
2047
|
+
throw new Error(`MinimoJS: ${describeDesignObject(obj)} sets "${schema.enabledBy}", which the ${schema.module} module draws, but this scene has not created the ${schema.module} module. Create it in the scene's onCreate(), such as with new ${schema.className}(game), before creating the design's objects.`);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2000
2050
|
/** @internal */
|
|
2001
2051
|
_buildModuleObject(obj) {
|
|
2002
2052
|
const builder = this._moduleBuilders.get(obj.module);
|
|
@@ -2062,8 +2112,17 @@ export class Game {
|
|
|
2062
2112
|
created.setTexture(value);
|
|
2063
2113
|
continue;
|
|
2064
2114
|
}
|
|
2115
|
+
// A decoration's properties, such as `outlineWidth`, are its module's,
|
|
2116
|
+
// not the sprite's: the module applies them below.
|
|
2117
|
+
if (DECORATION_PROPERTIES.has(field))
|
|
2118
|
+
continue;
|
|
2065
2119
|
target[field] = value;
|
|
2066
2120
|
}
|
|
2121
|
+
if (created instanceof Sprite) {
|
|
2122
|
+
for (const schema of enabledDecorations(obj)) {
|
|
2123
|
+
this._decorationBuilders.get(schema.module)?.(created, obj);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2067
2126
|
return created;
|
|
2068
2127
|
}
|
|
2069
2128
|
/** @internal */
|
|
@@ -3167,6 +3226,7 @@ export class Game {
|
|
|
3167
3226
|
this._inputSystem.resetPressedState();
|
|
3168
3227
|
this._buildersByTag.clear();
|
|
3169
3228
|
this._moduleBuilders.clear();
|
|
3229
|
+
this._decorationBuilders.clear();
|
|
3170
3230
|
this.scrollX = 0;
|
|
3171
3231
|
this.scrollY = 0;
|
|
3172
3232
|
this._loopSystem.invokeCreate();
|
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.43",
|
|
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",
|