minimojs 1.0.0-alpha.40 → 1.0.0-alpha.42
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/DesignNamingCheck.js +7 -1
- package/dist/internal/RenderSystem.js +28 -5
- package/dist/minimo-format.d.ts +81 -11
- package/dist/minimo-format.js +72 -2
- package/dist/minimo-multiplayer.d.ts +19 -6
- package/dist/minimo-multiplayer.js +19 -6
- package/dist/minimo-outline.d.ts +106 -0
- package/dist/minimo-outline.js +299 -0
- package/dist/minimo.d.ts +1 -1
- package/dist/minimo.js +51 -2
- package/package.json +1 -1
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
// object, except `id` and `type`, exists on its runtime class under the same
|
|
3
3
|
// name and with a compatible type (X-002). Designs use the runtime's names so
|
|
4
4
|
// agents learn one vocabulary for code and files. The build fails when a
|
|
5
|
-
// property is misnamed; a property that must be named differently
|
|
5
|
+
// property is misnamed; a property that must be named differently is
|
|
6
6
|
// declared here as an explicit exception.
|
|
7
|
+
//
|
|
8
|
+
// The one exception is a decoration's properties, `DesignOutline`'s
|
|
9
|
+
// `outlineWidth` and `outlineColor` (X-023). They belong to the optional
|
|
10
|
+
// `outline` module, not to the sprite, which the core never gives an outline
|
|
11
|
+
// of its own; in code they are `outlines.set(sprite, { width, color })`, and
|
|
12
|
+
// the prefix says which module reads them in a file.
|
|
7
13
|
export {};
|
|
@@ -478,12 +478,26 @@ export class RenderSystem {
|
|
|
478
478
|
return this.getDrawSurface(sprite);
|
|
479
479
|
}
|
|
480
480
|
const cacheKey = sprite.getRenderCacheKey();
|
|
481
|
-
const
|
|
482
|
-
if (
|
|
483
|
-
this.
|
|
484
|
-
|
|
481
|
+
const effect = sprite._surfaceEffect;
|
|
482
|
+
if (effect === null)
|
|
483
|
+
return this.getBaseSurface(sprite, cacheKey);
|
|
484
|
+
// The effect's result shares the bounded cache with the base surfaces,
|
|
485
|
+
// under a key of its own, so the base stays as it was for every other
|
|
486
|
+
// sprite that looks the same.
|
|
487
|
+
const effectKey = `${cacheKey}\u0000effect:${effect.key}`;
|
|
488
|
+
const cached = this.takeCachedSurface(effectKey);
|
|
489
|
+
if (cached)
|
|
490
|
+
return cached;
|
|
491
|
+
const surface = effect.apply(this.getBaseSurface(sprite, cacheKey));
|
|
492
|
+
this._surfaceCache.set(effectKey, surface);
|
|
493
|
+
this.trimSurfaceCache();
|
|
494
|
+
return surface;
|
|
495
|
+
}
|
|
496
|
+
/** @internal The sprite's own surface, before any effect. */
|
|
497
|
+
getBaseSurface(sprite, cacheKey) {
|
|
498
|
+
const cached = this.takeCachedSurface(cacheKey);
|
|
499
|
+
if (cached)
|
|
485
500
|
return cached;
|
|
486
|
-
}
|
|
487
501
|
const surface = this.isTextSprite(sprite)
|
|
488
502
|
? this.createTextSurface(sprite)
|
|
489
503
|
: this.isImageSprite(sprite)
|
|
@@ -493,6 +507,15 @@ export class RenderSystem {
|
|
|
493
507
|
this.trimSurfaceCache();
|
|
494
508
|
return surface;
|
|
495
509
|
}
|
|
510
|
+
/** @internal A cached surface, marked as the most recently used. */
|
|
511
|
+
takeCachedSurface(key) {
|
|
512
|
+
const cached = this._surfaceCache.get(key);
|
|
513
|
+
if (cached) {
|
|
514
|
+
this._surfaceCache.delete(key);
|
|
515
|
+
this._surfaceCache.set(key, cached);
|
|
516
|
+
}
|
|
517
|
+
return cached;
|
|
518
|
+
}
|
|
496
519
|
/** @internal */
|
|
497
520
|
trimSurfaceCache() {
|
|
498
521
|
while (this._surfaceCache.size > RenderSystem.MAX_SURFACE_CACHE_ENTRIES) {
|
package/dist/minimo-format.d.ts
CHANGED
|
@@ -191,6 +191,24 @@ export interface DesignSpriteBase extends DesignObjectBase {
|
|
|
191
191
|
/** Vertical offset of the body's center from the sprite's center, in pixels, before scale. Default: `0`. */
|
|
192
192
|
bodyOffsetY?: number;
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* The outline an `image`, `emoji`, or `text` object can have, drawn by the
|
|
196
|
+
* optional `outline` module around the object's visible shape (X-023). The
|
|
197
|
+
* scene creates the module, `new Outline(game)`, before creating a design
|
|
198
|
+
* object with an outline, and a design holding either property is version 3.
|
|
199
|
+
* See {@link designDecorations}.
|
|
200
|
+
*/
|
|
201
|
+
export interface DesignOutline {
|
|
202
|
+
/**
|
|
203
|
+
* Width of the outline in the object's own pixels, before scale, so it
|
|
204
|
+
* grows with the object; `0` draws none. At most `64`. The outline is drawn
|
|
205
|
+
* around the object and leaves its size, body, and hit area as they are.
|
|
206
|
+
* Default: `0`.
|
|
207
|
+
*/
|
|
208
|
+
outlineWidth?: number;
|
|
209
|
+
/** CSS color of the outline, which may be translucent, such as `"rgba(0, 0, 0, 0.5)"`. Default: `"#ffffff"`. */
|
|
210
|
+
outlineColor?: string;
|
|
211
|
+
}
|
|
194
212
|
/**
|
|
195
213
|
* An `ImageSprite` in a design file.
|
|
196
214
|
*
|
|
@@ -199,7 +217,7 @@ export interface DesignSpriteBase extends DesignObjectBase {
|
|
|
199
217
|
* { "id": "a93e51b4-6c0d-4f2a-8b17-3e5d9c2f0a61", "name": "play", "type": "image", "imageKey": "btn-play", "x": 360, "y": 700 }
|
|
200
218
|
* ```
|
|
201
219
|
*/
|
|
202
|
-
export interface DesignImage extends DesignSpriteBase {
|
|
220
|
+
export interface DesignImage extends DesignSpriteBase, DesignOutline {
|
|
203
221
|
/** Always `"image"`. */
|
|
204
222
|
type: "image";
|
|
205
223
|
/** Required. The key of an image declared in one of the game's asset files. */
|
|
@@ -213,7 +231,7 @@ export interface DesignImage extends DesignSpriteBase {
|
|
|
213
231
|
* { "id": "0b9a7c3e-2d4f-4e61-a8c5-7f1e3d2b6a90", "type": "emoji", "sprite": "⭐", "size": 64, "x": 200, "y": 300 }
|
|
214
232
|
* ```
|
|
215
233
|
*/
|
|
216
|
-
export interface DesignEmoji extends DesignSpriteBase {
|
|
234
|
+
export interface DesignEmoji extends DesignSpriteBase, DesignOutline {
|
|
217
235
|
/** Always `"emoji"`. */
|
|
218
236
|
type: "emoji";
|
|
219
237
|
/** Required. The emoji to draw. */
|
|
@@ -229,7 +247,7 @@ export interface DesignEmoji extends DesignSpriteBase {
|
|
|
229
247
|
* { "id": "5c2e8f1a-9b3d-4a7e-b6c0-1d4f8e2a7c35", "type": "text", "text": "GAME OVER", "fontSize": 64, "x": 360, "y": 400 }
|
|
230
248
|
* ```
|
|
231
249
|
*/
|
|
232
|
-
export interface DesignText extends DesignSpriteBase {
|
|
250
|
+
export interface DesignText extends DesignSpriteBase, DesignOutline {
|
|
233
251
|
/** Always `"text"`. */
|
|
234
252
|
type: "text";
|
|
235
253
|
/** Required. The text to draw. */
|
|
@@ -335,8 +353,9 @@ export type DesignObject = DesignImage | DesignEmoji | DesignText | DesignBackgr
|
|
|
335
353
|
* {@link Design} and creates with `game.createObject` and
|
|
336
354
|
* `game.createObjectList`. A design without pages is a {@link DesignFileV1},
|
|
337
355
|
* one flat list of objects; a design divided into pages is a
|
|
338
|
-
* {@link DesignFileV2}; a design holding objects of an optional module
|
|
339
|
-
*
|
|
356
|
+
* {@link DesignFileV2}; a design holding objects of an optional module or
|
|
357
|
+
* properties of a decoration, such as an outline, with or without pages, is a
|
|
358
|
+
* {@link DesignFileV3}. All three are current: a
|
|
340
359
|
* design is written in the lowest version that holds it, so it also loads in
|
|
341
360
|
* an older MinimoJS whenever it can.
|
|
342
361
|
*/
|
|
@@ -395,10 +414,11 @@ export interface DesignFileV2 {
|
|
|
395
414
|
pages: DesignPage[];
|
|
396
415
|
}
|
|
397
416
|
/**
|
|
398
|
-
* A design file holding objects of an optional module, {@link DesignModule}
|
|
399
|
-
*
|
|
400
|
-
* `
|
|
401
|
-
*
|
|
417
|
+
* A design file holding objects of an optional module, {@link DesignModule},
|
|
418
|
+
* or properties of a decoration, such as the {@link DesignOutline} the
|
|
419
|
+
* `outline` module draws. Like the earlier versions, it has either a flat list
|
|
420
|
+
* of `objects` or `pages`, never both; it is version 3 only because an engine
|
|
421
|
+
* older than either cannot read it.
|
|
402
422
|
*
|
|
403
423
|
* @example
|
|
404
424
|
* ```json
|
|
@@ -415,7 +435,7 @@ export interface DesignFileV2 {
|
|
|
415
435
|
export interface DesignFileV3 {
|
|
416
436
|
/** Always `"minimo-design"`. */
|
|
417
437
|
format: "minimo-design";
|
|
418
|
-
/** `3`: the design holds objects of an optional module. */
|
|
438
|
+
/** `3`: the design holds objects of an optional module or properties of a decoration. */
|
|
419
439
|
version: 3;
|
|
420
440
|
/** The objects of a design without pages, in file order. Present exactly when `pages` is not. */
|
|
421
441
|
objects?: DesignObject[];
|
|
@@ -541,6 +561,55 @@ export interface DesignModuleSchema {
|
|
|
541
561
|
* ```
|
|
542
562
|
*/
|
|
543
563
|
export declare const designModules: Readonly<Record<string, DesignModuleSchema>>;
|
|
564
|
+
/**
|
|
565
|
+
* What an optional module adds to the objects of a design that are not its
|
|
566
|
+
* own, such as an outline the `outline` module draws around an image, for the
|
|
567
|
+
* engine and for tools such as an editor. See {@link designDecorations}.
|
|
568
|
+
*/
|
|
569
|
+
export interface DesignDecorationSchema {
|
|
570
|
+
/** The module's id, such as `"outline"`. */
|
|
571
|
+
readonly module: string;
|
|
572
|
+
/** The module's file, next to `minimo.js`, such as `"minimo-outline.js"`. */
|
|
573
|
+
readonly file: string;
|
|
574
|
+
/**
|
|
575
|
+
* The class the module exports, which a scene creates with
|
|
576
|
+
* `new Class(game)` before creating a design object that uses it, such as
|
|
577
|
+
* `"Outline"`.
|
|
578
|
+
*/
|
|
579
|
+
readonly className: string;
|
|
580
|
+
/** The types of design object that take the decoration's properties. */
|
|
581
|
+
readonly types: readonly DesignObjectType[];
|
|
582
|
+
/**
|
|
583
|
+
* The properties of those types that belong to the module, as
|
|
584
|
+
* {@link designObjectProperties} lists them with their types and defaults.
|
|
585
|
+
* A design holding any of them is version 3.
|
|
586
|
+
*/
|
|
587
|
+
readonly properties: readonly string[];
|
|
588
|
+
/**
|
|
589
|
+
* The number property that turns the decoration on when it is above `0`,
|
|
590
|
+
* such as `"outlineWidth"`. Only then does creating the object need the
|
|
591
|
+
* module.
|
|
592
|
+
*/
|
|
593
|
+
readonly enabledBy: string;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* The optional modules that decorate design objects of the core's types,
|
|
597
|
+
* by id. Unlike a {@link DesignModule}, a decoration adds no object: it is a
|
|
598
|
+
* look of an `image`, `emoji`, or `text` object, held in properties of that
|
|
599
|
+
* object, which the module draws.
|
|
600
|
+
*
|
|
601
|
+
* A tool that draws a design creates the module, with `new Class(game)`,
|
|
602
|
+
* before creating an object whose `enabledBy` property is above `0`.
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* ```ts
|
|
606
|
+
* for (const schema of Object.values(designDecorations)) {
|
|
607
|
+
* const used = design.objects.some((object) => (object[schema.enabledBy] ?? 0) > 0);
|
|
608
|
+
* if (used) console.log(`${schema.className} from ${schema.file}`);
|
|
609
|
+
* }
|
|
610
|
+
* ```
|
|
611
|
+
*/
|
|
612
|
+
export declare const designDecorations: Readonly<Record<string, DesignDecorationSchema>>;
|
|
544
613
|
/**
|
|
545
614
|
* The data of a design file, `*.design.json`, validated: its objects, its
|
|
546
615
|
* pages, and searches on them. A `Design` needs no game and creates nothing; pass its
|
|
@@ -596,7 +665,8 @@ export declare class Design {
|
|
|
596
665
|
* a `data` that is not a JSON object, a page without a UUID `id` or with
|
|
597
666
|
* a missing, padded, or repeated `name`, and a `module` object in a design
|
|
598
667
|
* older than version 3 or with a module, kind, property, or option that
|
|
599
|
-
* {@link designModules} does not list
|
|
668
|
+
* {@link designModules} does not list, and a property of a decoration, such
|
|
669
|
+
* as `outlineWidth`, in a design older than version 3.
|
|
600
670
|
*
|
|
601
671
|
* @param json - The content of a `*.design.json` file. See {@link DesignFile}.
|
|
602
672
|
*/
|
package/dist/minimo-format.js
CHANGED
|
@@ -142,14 +142,20 @@ const SPRITE_SPECS = {
|
|
|
142
142
|
bodyOffsetY: spec("number", 0),
|
|
143
143
|
...DATA,
|
|
144
144
|
};
|
|
145
|
+
const OUTLINE_SPECS = {
|
|
146
|
+
outlineWidth: spec("number", 0),
|
|
147
|
+
outlineColor: spec("string", "#ffffff"),
|
|
148
|
+
};
|
|
145
149
|
const IMAGE_SPECS = {
|
|
146
150
|
imageKey: requiredString(),
|
|
147
151
|
...SPRITE_SPECS,
|
|
152
|
+
...OUTLINE_SPECS,
|
|
148
153
|
};
|
|
149
154
|
const EMOJI_SPECS = {
|
|
150
155
|
sprite: requiredString(),
|
|
151
156
|
size: spec("number", 32),
|
|
152
157
|
...SPRITE_SPECS,
|
|
158
|
+
...OUTLINE_SPECS,
|
|
153
159
|
};
|
|
154
160
|
const TEXT_SPECS = {
|
|
155
161
|
text: requiredString(),
|
|
@@ -173,6 +179,7 @@ const TEXT_SPECS = {
|
|
|
173
179
|
strokeWidth: spec("number", 0),
|
|
174
180
|
...SPRITE_SPECS,
|
|
175
181
|
color: spec("string", "#ffffff"),
|
|
182
|
+
...OUTLINE_SPECS,
|
|
176
183
|
};
|
|
177
184
|
const BACKGROUND_SPECS = {
|
|
178
185
|
imageKey: requiredString(),
|
|
@@ -283,6 +290,62 @@ export const designModules = Object.freeze({
|
|
|
283
290
|
sizeOption: null,
|
|
284
291
|
}),
|
|
285
292
|
});
|
|
293
|
+
/**
|
|
294
|
+
* The optional modules that decorate design objects of the core's types,
|
|
295
|
+
* by id. Unlike a {@link DesignModule}, a decoration adds no object: it is a
|
|
296
|
+
* look of an `image`, `emoji`, or `text` object, held in properties of that
|
|
297
|
+
* object, which the module draws.
|
|
298
|
+
*
|
|
299
|
+
* A tool that draws a design creates the module, with `new Class(game)`,
|
|
300
|
+
* before creating an object whose `enabledBy` property is above `0`.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* for (const schema of Object.values(designDecorations)) {
|
|
305
|
+
* const used = design.objects.some((object) => (object[schema.enabledBy] ?? 0) > 0);
|
|
306
|
+
* if (used) console.log(`${schema.className} from ${schema.file}`);
|
|
307
|
+
* }
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
export const designDecorations = Object.freeze({
|
|
311
|
+
outline: Object.freeze({
|
|
312
|
+
module: "outline",
|
|
313
|
+
file: "minimo-outline.js",
|
|
314
|
+
className: "Outline",
|
|
315
|
+
types: Object.freeze(["image", "emoji", "text"]),
|
|
316
|
+
properties: Object.freeze(Object.keys(OUTLINE_SPECS)),
|
|
317
|
+
enabledBy: "outlineWidth",
|
|
318
|
+
}),
|
|
319
|
+
});
|
|
320
|
+
/**
|
|
321
|
+
* The first property of `object` that belongs to a decoration, or `null`.
|
|
322
|
+
*
|
|
323
|
+
* @internal
|
|
324
|
+
*/
|
|
325
|
+
export function findDecorationProperty(object) {
|
|
326
|
+
const fields = object;
|
|
327
|
+
for (const schema of Object.values(designDecorations)) {
|
|
328
|
+
if (!schema.types.includes(object.type))
|
|
329
|
+
continue;
|
|
330
|
+
for (const name of schema.properties) {
|
|
331
|
+
if (name in fields)
|
|
332
|
+
return name;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The decorations whose `enabledBy` property is above `0` in `object`.
|
|
339
|
+
*
|
|
340
|
+
* @internal
|
|
341
|
+
*/
|
|
342
|
+
export function enabledDecorations(object) {
|
|
343
|
+
const fields = object;
|
|
344
|
+
return Object.values(designDecorations).filter((schema) => {
|
|
345
|
+
const value = fields[schema.enabledBy];
|
|
346
|
+
return schema.types.includes(object.type) && typeof value === "number" && value > 0;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
286
349
|
/** The properties every module object takes, whatever its module. */
|
|
287
350
|
const MODULE_OBJECT_FIELDS = new Set(["id", "type", "module", "kind", "options", "name", "tags", "data"]);
|
|
288
351
|
/** Checks a module object, already checked as a `module`, against its module's schema. */
|
|
@@ -437,7 +500,7 @@ function parseDesignFile(json) {
|
|
|
437
500
|
}
|
|
438
501
|
// Every version is current, so none is migrated: version 1 is a design
|
|
439
502
|
// without pages, version 2 one divided into pages, and version 3 either,
|
|
440
|
-
// holding objects of an optional module.
|
|
503
|
+
// holding objects of an optional module or properties of a decoration.
|
|
441
504
|
for (const field of Object.keys(json)) {
|
|
442
505
|
if (!DESIGN_FILE_FIELDS[version].has(field)) {
|
|
443
506
|
const hint = field === "pages" ? ` A design with pages has "version" 2.` : field === "objects" ? ` In version 2, objects belong to "pages".` : "";
|
|
@@ -459,6 +522,12 @@ function parseDesignFile(json) {
|
|
|
459
522
|
if (object.type === "module" && version < 3) {
|
|
460
523
|
throw fail(`${at} is a "module" object, which only a design with "version" 3 holds, so that an older MinimoJS refuses the design by its version.`);
|
|
461
524
|
}
|
|
525
|
+
if (version < 3) {
|
|
526
|
+
const decorated = findDecorationProperty(object);
|
|
527
|
+
if (decorated !== null) {
|
|
528
|
+
throw fail(`${at} sets "${decorated}", which only a design with "version" 3 holds, so that an older MinimoJS refuses the design by its version.`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
462
531
|
const id = object.id.toLowerCase();
|
|
463
532
|
const first = whereById.get(id);
|
|
464
533
|
if (first !== undefined) {
|
|
@@ -573,7 +642,8 @@ export class Design {
|
|
|
573
642
|
* a `data` that is not a JSON object, a page without a UUID `id` or with
|
|
574
643
|
* a missing, padded, or repeated `name`, and a `module` object in a design
|
|
575
644
|
* older than version 3 or with a module, kind, property, or option that
|
|
576
|
-
* {@link designModules} does not list
|
|
645
|
+
* {@link designModules} does not list, and a property of a decoration, such
|
|
646
|
+
* as `outlineWidth`, in a design older than version 3.
|
|
577
647
|
*
|
|
578
648
|
* @param json - The content of a `*.design.json` file. See {@link DesignFile}.
|
|
579
649
|
*/
|
|
@@ -293,7 +293,8 @@ export declare class Room {
|
|
|
293
293
|
*
|
|
294
294
|
* {@link Multiplayer.lobby} shows the lobby over the game, in HTML: creating a
|
|
295
295
|
* room, joining one by its code, sharing its link, and waiting for the others
|
|
296
|
-
* until the host starts. A player who
|
|
296
|
+
* until the host starts. A player who opened the room's link lands in it as
|
|
297
|
+
* soon as the game calls it.
|
|
297
298
|
*
|
|
298
299
|
* ```js
|
|
299
300
|
* import { Multiplayer } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo-multiplayer.js";
|
|
@@ -309,15 +310,27 @@ export declare class Room {
|
|
|
309
310
|
* const choice = await room.showResults(); // "rematch" or "leave"
|
|
310
311
|
* ```
|
|
311
312
|
*
|
|
312
|
-
* A
|
|
313
|
-
*
|
|
314
|
-
*
|
|
313
|
+
* A player who opens a room's link has already chosen to play online and
|
|
314
|
+
* which room to join. So a game asks {@link Multiplayer.pendingRoom} as soon
|
|
315
|
+
* as its assets are loaded, before showing anything. When a room is pending,
|
|
316
|
+
* it skips every screen that only leads to a room or that the host decides,
|
|
317
|
+
* such as a title screen, menus, playing alone or online, creating or joining,
|
|
318
|
+
* the mode, the difficulty, the rules, or the level, and shows only the
|
|
319
|
+
* choices that are the player's own, such as a character or a color, before
|
|
320
|
+
* {@link Multiplayer.lobby} or after it:
|
|
315
321
|
*
|
|
316
322
|
* ```js
|
|
317
323
|
* const pending = await mp.pendingRoom();
|
|
318
|
-
* if (pending)
|
|
319
|
-
*
|
|
324
|
+
* if (pending) {
|
|
325
|
+
* await chooseCharacter(); // only the player's own choices
|
|
326
|
+
* room = await mp.lobby(); // that room's lobby, or its game in progress
|
|
327
|
+
* } else showWelcome(); // Play online, then mp.lobby() to create or join
|
|
320
328
|
* ```
|
|
329
|
+
*
|
|
330
|
+
* What every player must share is the host's choice and travels in the
|
|
331
|
+
* room's state. A player returning to a room after a reload (`invited` is
|
|
332
|
+
* false) is asked again only what the game could not keep. When the player
|
|
333
|
+
* closes the lobby, show the welcome screen.
|
|
321
334
|
*/
|
|
322
335
|
export declare class Multiplayer {
|
|
323
336
|
/**
|
|
@@ -757,7 +757,8 @@ export class Room {
|
|
|
757
757
|
*
|
|
758
758
|
* {@link Multiplayer.lobby} shows the lobby over the game, in HTML: creating a
|
|
759
759
|
* room, joining one by its code, sharing its link, and waiting for the others
|
|
760
|
-
* until the host starts. A player who
|
|
760
|
+
* until the host starts. A player who opened the room's link lands in it as
|
|
761
|
+
* soon as the game calls it.
|
|
761
762
|
*
|
|
762
763
|
* ```js
|
|
763
764
|
* import { Multiplayer } from "https://cdn.jsdelivr.net/npm/minimojs@<version>/dist/minimo-multiplayer.js";
|
|
@@ -773,15 +774,27 @@ export class Room {
|
|
|
773
774
|
* const choice = await room.showResults(); // "rematch" or "leave"
|
|
774
775
|
* ```
|
|
775
776
|
*
|
|
776
|
-
* A
|
|
777
|
-
*
|
|
778
|
-
*
|
|
777
|
+
* A player who opens a room's link has already chosen to play online and
|
|
778
|
+
* which room to join. So a game asks {@link Multiplayer.pendingRoom} as soon
|
|
779
|
+
* as its assets are loaded, before showing anything. When a room is pending,
|
|
780
|
+
* it skips every screen that only leads to a room or that the host decides,
|
|
781
|
+
* such as a title screen, menus, playing alone or online, creating or joining,
|
|
782
|
+
* the mode, the difficulty, the rules, or the level, and shows only the
|
|
783
|
+
* choices that are the player's own, such as a character or a color, before
|
|
784
|
+
* {@link Multiplayer.lobby} or after it:
|
|
779
785
|
*
|
|
780
786
|
* ```js
|
|
781
787
|
* const pending = await mp.pendingRoom();
|
|
782
|
-
* if (pending)
|
|
783
|
-
*
|
|
788
|
+
* if (pending) {
|
|
789
|
+
* await chooseCharacter(); // only the player's own choices
|
|
790
|
+
* room = await mp.lobby(); // that room's lobby, or its game in progress
|
|
791
|
+
* } else showWelcome(); // Play online, then mp.lobby() to create or join
|
|
784
792
|
* ```
|
|
793
|
+
*
|
|
794
|
+
* What every player must share is the host's choice and travels in the
|
|
795
|
+
* room's state. A player returning to a room after a reload (`invited` is
|
|
796
|
+
* false) is asked again only what the game could not keep. When the player
|
|
797
|
+
* closes the lobby, show the welcome screen.
|
|
785
798
|
*/
|
|
786
799
|
export class Multiplayer {
|
|
787
800
|
/**
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Game, Sprite } from "./minimo.js";
|
|
2
|
+
/** How {@link Outline.set} outlines a sprite. */
|
|
3
|
+
export interface OutlineOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Width of the outline in the sprite's own pixels, before
|
|
6
|
+
* {@link Sprite.scale}, so it grows and shrinks with the sprite. `0`
|
|
7
|
+
* removes the outline. A width above {@link Outline.MAX_WIDTH} draws that
|
|
8
|
+
* width.
|
|
9
|
+
*/
|
|
10
|
+
width: number;
|
|
11
|
+
/**
|
|
12
|
+
* CSS color of the outline, such as `"#fff5df"`, which may be translucent,
|
|
13
|
+
* such as `"rgba(0, 0, 0, 0.5)"`. Default `"#ffffff"`.
|
|
14
|
+
*/
|
|
15
|
+
color?: string;
|
|
16
|
+
}
|
|
17
|
+
/** The outline a sprite has, as {@link Outline.get} reports it. */
|
|
18
|
+
export interface OutlineStyle {
|
|
19
|
+
/** Width in the sprite's own pixels, before scale. */
|
|
20
|
+
readonly width: number;
|
|
21
|
+
/** CSS color. */
|
|
22
|
+
readonly color: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Draws a solid outline around the visible shape of image, emoji, and text
|
|
26
|
+
* sprites, so a character or an answer stands out from what is behind it.
|
|
27
|
+
*
|
|
28
|
+
* The outline follows the shape's transparent edges, not its box, and is
|
|
29
|
+
* part of the sprite's own picture: it moves, turns, flips, scales, fades,
|
|
30
|
+
* and changes layer with the sprite, and it shows in trails, wipes, and
|
|
31
|
+
* explosions of it. Only what is drawn grows. The sprite's size, collision
|
|
32
|
+
* body, the area that takes taps, and its pivot stay as they were.
|
|
33
|
+
*
|
|
34
|
+
* Each outline is built once and kept, so moving or turning a sprite costs
|
|
35
|
+
* nothing more. A new texture, new text, or a new width or color builds it
|
|
36
|
+
* again. Sprites that look the same share one outline.
|
|
37
|
+
*
|
|
38
|
+
* {@link DrawSprite}s, and the sprites made from it such as a flame or a die,
|
|
39
|
+
* are not outlined: they are redrawn every frame, which would rebuild the
|
|
40
|
+
* outline every frame.
|
|
41
|
+
*
|
|
42
|
+
* Create the module in the scene's `onCreate()`. It needs no `update()`.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { Outline } from "./minimo-outline.js";
|
|
47
|
+
*
|
|
48
|
+
* onCreate() {
|
|
49
|
+
* const outlines = new Outline(game);
|
|
50
|
+
* const frog = game.add(new ImageSprite(game, "frog", 360, 640));
|
|
51
|
+
* outlines.set(frog, { width: 4, color: "#fff5df" });
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* ## In a design
|
|
56
|
+
*
|
|
57
|
+
* An `image`, `emoji`, or `text` object in a design takes an outline through
|
|
58
|
+
* two properties, which Studio shows among the object's properties:
|
|
59
|
+
*
|
|
60
|
+
* - `outlineWidth`: the width in the object's own pixels. `0`, the default,
|
|
61
|
+
* draws none.
|
|
62
|
+
* - `outlineColor`: its CSS color. Default `"#ffffff"`.
|
|
63
|
+
*
|
|
64
|
+
* ```json
|
|
65
|
+
* { "id": "…", "type": "image", "name": "frog", "imageKey": "frog",
|
|
66
|
+
* "x": 360, "y": 640, "outlineWidth": 4, "outlineColor": "#fff5df" }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* A design holding either property has `"version": 3`. The outline is a look
|
|
70
|
+
* of the object, not an object of its own: there is no `module` object for it.
|
|
71
|
+
* Create the module before the design's objects, or creating an object whose
|
|
72
|
+
* `outlineWidth` is above `0` throws an error naming it:
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* onCreate() {
|
|
76
|
+
* this.outlines = new Outline(game);
|
|
77
|
+
* const screen = game.createObjectList(menu.objects);
|
|
78
|
+
* const frog = screen.findByName("frog");
|
|
79
|
+
* // Change it while the game plays, such as on the chosen answer:
|
|
80
|
+
* this.outlines.set(frog, { width: 6, color: "#ffd84a" });
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare class Outline {
|
|
85
|
+
/** The widest outline drawn, in the sprite's own pixels. */
|
|
86
|
+
static readonly MAX_WIDTH = 64;
|
|
87
|
+
/** The game whose sprites it outlines. */
|
|
88
|
+
readonly game: Game;
|
|
89
|
+
constructor(game: Game);
|
|
90
|
+
/**
|
|
91
|
+
* Outlines `sprite`, or changes its outline. A width of `0` removes it.
|
|
92
|
+
* @param sprite - An {@link ImageSprite}, {@link EmojiSprite}, or {@link TextSprite}, or a subclass of one.
|
|
93
|
+
* @param options - The width, and the color, white by default.
|
|
94
|
+
*/
|
|
95
|
+
set(sprite: Sprite, options: OutlineOptions): void;
|
|
96
|
+
/**
|
|
97
|
+
* The outline `sprite` has, or `null` when it has none.
|
|
98
|
+
* @param sprite - Any sprite.
|
|
99
|
+
*/
|
|
100
|
+
get(sprite: Sprite): OutlineStyle | null;
|
|
101
|
+
/**
|
|
102
|
+
* Removes the outline of `sprite`. Nothing happens when it has none.
|
|
103
|
+
* @param sprite - Any sprite.
|
|
104
|
+
*/
|
|
105
|
+
remove(sprite: Sprite): void;
|
|
106
|
+
}
|
|
@@ -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
|
@@ -13,7 +13,7 @@ import type { DesignObject } from "./minimo-format.js";
|
|
|
13
13
|
export { Design, parseAssetFile } 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 { AssetFile, DesignBackground, DesignEmoji, DesignFile, DesignFileV1, DesignFileV2, DesignFileV3, DesignImage, DesignModule, DesignObject, DesignObjectBase, DesignObjectType, DesignOutline, DesignPage, DesignSpriteBase, DesignText, ImageAsset, SoundAsset, UnknownAsset, } from "./minimo-format.js";
|
|
17
17
|
/**
|
|
18
18
|
* Optional tuning values for {@link Game.animateExplode}.
|
|
19
19
|
*
|
package/dist/minimo.js
CHANGED
|
@@ -22,7 +22,7 @@ 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";
|
|
25
|
+
import { Design, designDecorations, designModules, enabledDecorations, findFirst, parseAssetFile, parseDesignObject, } from "./minimo-format.js";
|
|
26
26
|
export { Design, parseAssetFile } 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. */
|
|
@@ -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`
|
|
@@ -1884,6 +1895,7 @@ export class Game {
|
|
|
1884
1895
|
}
|
|
1885
1896
|
const obj = parseDesignObject(data, "the object", "design object");
|
|
1886
1897
|
this._assertDesignImageLoaded(obj);
|
|
1898
|
+
this._assertDesignDecorationsReady(obj);
|
|
1887
1899
|
const created = this._buildDesignObject(obj);
|
|
1888
1900
|
if (options.add !== false)
|
|
1889
1901
|
this._addDesignObject(created);
|
|
@@ -1922,8 +1934,10 @@ export class Game {
|
|
|
1922
1934
|
throw new Error("MinimoJS: createObjectList() takes a list of object data, such as design.objects or design.findAllByTag(\"enemy\").");
|
|
1923
1935
|
}
|
|
1924
1936
|
const parsed = objects.map((data, index) => parseDesignObject(data, `objects[${index}]`, "design object"));
|
|
1925
|
-
for (const obj of parsed)
|
|
1937
|
+
for (const obj of parsed) {
|
|
1926
1938
|
this._assertDesignImageLoaded(obj);
|
|
1939
|
+
this._assertDesignDecorationsReady(obj);
|
|
1940
|
+
}
|
|
1927
1941
|
const created = parsed.map((obj) => this._buildDesignObject(obj));
|
|
1928
1942
|
if (options.add !== false) {
|
|
1929
1943
|
for (const obj of created)
|
|
@@ -1997,6 +2011,31 @@ export class Game {
|
|
|
1997
2011
|
if (!this._moduleBuilders.has(module))
|
|
1998
2012
|
this._moduleBuilders.set(module, builder);
|
|
1999
2013
|
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Makes {@link Game.createObject} and {@link Game.createObjectList} hand
|
|
2016
|
+
* the design objects that use the decoration `module`, such as an outline,
|
|
2017
|
+
* to `builder` once built. A module calls it when it is created; the first
|
|
2018
|
+
* module created for an id keeps it until {@link Game.reset} clears it.
|
|
2019
|
+
*
|
|
2020
|
+
* @internal
|
|
2021
|
+
*/
|
|
2022
|
+
_registerDesignDecoration(module, builder) {
|
|
2023
|
+
if (!this._decorationBuilders.has(module))
|
|
2024
|
+
this._decorationBuilders.set(module, builder);
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Checks that every decoration `obj` turns on has its module created, before
|
|
2028
|
+
* anything is built, so a list with one such object creates nothing.
|
|
2029
|
+
*
|
|
2030
|
+
* @internal
|
|
2031
|
+
*/
|
|
2032
|
+
_assertDesignDecorationsReady(obj) {
|
|
2033
|
+
for (const schema of enabledDecorations(obj)) {
|
|
2034
|
+
if (this._decorationBuilders.has(schema.module))
|
|
2035
|
+
continue;
|
|
2036
|
+
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.`);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2000
2039
|
/** @internal */
|
|
2001
2040
|
_buildModuleObject(obj) {
|
|
2002
2041
|
const builder = this._moduleBuilders.get(obj.module);
|
|
@@ -2062,8 +2101,17 @@ export class Game {
|
|
|
2062
2101
|
created.setTexture(value);
|
|
2063
2102
|
continue;
|
|
2064
2103
|
}
|
|
2104
|
+
// A decoration's properties, such as `outlineWidth`, are its module's,
|
|
2105
|
+
// not the sprite's: the module applies them below.
|
|
2106
|
+
if (DECORATION_PROPERTIES.has(field))
|
|
2107
|
+
continue;
|
|
2065
2108
|
target[field] = value;
|
|
2066
2109
|
}
|
|
2110
|
+
if (created instanceof Sprite) {
|
|
2111
|
+
for (const schema of enabledDecorations(obj)) {
|
|
2112
|
+
this._decorationBuilders.get(schema.module)?.(created, obj);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2067
2115
|
return created;
|
|
2068
2116
|
}
|
|
2069
2117
|
/** @internal */
|
|
@@ -3167,6 +3215,7 @@ export class Game {
|
|
|
3167
3215
|
this._inputSystem.resetPressedState();
|
|
3168
3216
|
this._buildersByTag.clear();
|
|
3169
3217
|
this._moduleBuilders.clear();
|
|
3218
|
+
this._decorationBuilders.clear();
|
|
3170
3219
|
this.scrollX = 0;
|
|
3171
3220
|
this.scrollY = 0;
|
|
3172
3221
|
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.42",
|
|
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",
|