minimojs 1.0.0-alpha.22 → 1.0.0-alpha.24
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 +4 -1
- package/dist/internal/AssetSystem.js +40 -8
- package/dist/internal/RenderSystem.js +104 -21
- package/dist/minimo-ambient.d.ts +299 -0
- package/dist/minimo-ambient.js +1445 -0
- package/dist/minimo.d.ts +96 -26
- package/dist/minimo.js +270 -86
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,7 +87,9 @@ class DemoScene implements IScene {
|
|
|
87
87
|
game.start(new DemoScene());
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
Note:
|
|
90
|
+
Note: text defaults to `monospace`, so a game that names no font downloads none. To use a web font, declare it in your HTML, register it with `game.requireFont(...)`, and set it as a `TextSprite`'s `fontFamily`.
|
|
91
|
+
|
|
92
|
+
Load images in `game.onPreload`, once, at boot. Only a game with many scenes that each carry many images of their own, such as one with many levels, should give scenes an `onPreload()` so each loads its images when the player reaches it; see `examples/level-loading/`.
|
|
91
93
|
|
|
92
94
|
## Examples
|
|
93
95
|
|
|
@@ -102,6 +104,7 @@ Note: `drawText()` uses `"Press Start 2P", monospace`. Load the font in your HTM
|
|
|
102
104
|
- `examples/draw-sprite/`
|
|
103
105
|
- `examples/pseudo-3d-racer/`
|
|
104
106
|
- `examples/animations/`
|
|
107
|
+
- `examples/level-loading/`
|
|
105
108
|
|
|
106
109
|
Run locally from the `minimojs` directory:
|
|
107
110
|
|
|
@@ -67,20 +67,52 @@ export class AssetSystem {
|
|
|
67
67
|
}
|
|
68
68
|
return total;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Loads every queued image that is not loaded yet, settling all of them
|
|
72
|
+
* before returning instead of stopping at the first failure.
|
|
73
|
+
*
|
|
74
|
+
* Loaded images leave the queue; failed ones stay in it, so a retry loads
|
|
75
|
+
* only them. Returns the keys that failed, empty when all loaded.
|
|
76
|
+
*/
|
|
70
77
|
async loadQueuedImages(onProgress) {
|
|
71
78
|
const entries = [...this._queuedImages.entries()].filter(([key]) => !this._loadedImages.has(key));
|
|
72
79
|
const total = entries.length;
|
|
73
|
-
let
|
|
80
|
+
let settled = 0;
|
|
81
|
+
const failed = [];
|
|
74
82
|
onProgress?.(0, total, null);
|
|
75
|
-
if (total === 0)
|
|
76
|
-
|
|
83
|
+
if (total === 0) {
|
|
84
|
+
this._queuedImages.clear();
|
|
85
|
+
return failed;
|
|
86
|
+
}
|
|
77
87
|
await Promise.all(entries.map(async ([key, src]) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
try {
|
|
89
|
+
const image = await this.loadImage(src);
|
|
90
|
+
this._loadedImages.set(key, image);
|
|
91
|
+
this.bumpImageVersion(key);
|
|
92
|
+
this._queuedImages.delete(key);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
failed.push(key);
|
|
96
|
+
}
|
|
97
|
+
settled += 1;
|
|
98
|
+
onProgress?.(settled, total, key);
|
|
83
99
|
}));
|
|
100
|
+
for (const key of [...this._queuedImages.keys()]) {
|
|
101
|
+
if (this._loadedImages.has(key)) {
|
|
102
|
+
this._queuedImages.delete(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return failed;
|
|
106
|
+
}
|
|
107
|
+
/** Returns the source registered for an image key, if any. */
|
|
108
|
+
getImageSource(key) {
|
|
109
|
+
return this._imageSources.get(key);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Drops queued images that were not loaded, so a later load does not pick
|
|
113
|
+
* them up. Their keys may be registered again.
|
|
114
|
+
*/
|
|
115
|
+
discardQueuedImages() {
|
|
84
116
|
this._queuedImages.clear();
|
|
85
117
|
}
|
|
86
118
|
/** @internal */
|
|
@@ -6,6 +6,8 @@ export class RenderSystem {
|
|
|
6
6
|
/** @internal */
|
|
7
7
|
this._lastAppliedPageBackground = undefined;
|
|
8
8
|
/** @internal */
|
|
9
|
+
this._bootInk = null;
|
|
10
|
+
/** @internal */
|
|
9
11
|
this._transitionScratchCanvas = null;
|
|
10
12
|
/** @internal */
|
|
11
13
|
this._transitionScratchContext = null;
|
|
@@ -199,7 +201,7 @@ export class RenderSystem {
|
|
|
199
201
|
}
|
|
200
202
|
for (const entry of options.textEntries) {
|
|
201
203
|
ctx.save();
|
|
202
|
-
ctx.font = `${entry.fontSize}px
|
|
204
|
+
ctx.font = `${entry.fontSize}px monospace`;
|
|
203
205
|
ctx.fillStyle = entry.color;
|
|
204
206
|
ctx.textAlign = entry.centered ? "center" : "left";
|
|
205
207
|
ctx.textBaseline = entry.centered ? "middle" : "top";
|
|
@@ -263,35 +265,114 @@ export class RenderSystem {
|
|
|
263
265
|
}
|
|
264
266
|
ctx.restore();
|
|
265
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Draws the boot loading screen: the game's background and one thin
|
|
270
|
+
* progress line across its middle, with no text, so it needs no font.
|
|
271
|
+
*/
|
|
266
272
|
renderLoadingScreen(options) {
|
|
267
273
|
const ctx = options.context;
|
|
268
274
|
const W = options.canvas.width;
|
|
269
275
|
const H = options.canvas.height;
|
|
270
|
-
const
|
|
271
|
-
const safeLoaded = Math.max(0, Math.min(options.loaded, safeTotal));
|
|
272
|
-
const progress = safeTotal === 0 ? 1 : safeLoaded / safeTotal;
|
|
273
|
-
const barWidth = Math.min(420, Math.max(220, Math.floor(W * 0.46)));
|
|
274
|
-
const barHeight = 18;
|
|
275
|
-
const barX = Math.round((W - barWidth) / 2);
|
|
276
|
-
const barY = Math.round(H / 2 + 24);
|
|
276
|
+
const progress = this.clampProgress(options.progress);
|
|
277
277
|
this.applyPageBackground(options.pageBackground);
|
|
278
278
|
this.paintCanvasBackground(ctx, W, H, options.background ?? "#101820", options.backgroundGradient);
|
|
279
|
+
// The line is redrawn on every settled image; read the background once.
|
|
280
|
+
const inkKey = `${options.background}|${options.backgroundGradient?.from}|${options.backgroundGradient?.to}`;
|
|
281
|
+
if (this._bootInk === null || this._bootInk.key !== inkKey) {
|
|
282
|
+
this._bootInk = { key: inkKey, ink: this.pickLoadingInk(options.canvas) };
|
|
283
|
+
}
|
|
284
|
+
const ink = this._bootInk.ink;
|
|
285
|
+
const lineWidth = Math.round(Math.min(320, Math.max(160, W * 0.4)));
|
|
286
|
+
const x = Math.round((W - lineWidth) / 2);
|
|
287
|
+
const y = Math.round(H / 2);
|
|
279
288
|
ctx.save();
|
|
280
|
-
ctx.
|
|
281
|
-
ctx.
|
|
282
|
-
ctx.
|
|
283
|
-
ctx.
|
|
284
|
-
ctx.
|
|
285
|
-
ctx.
|
|
286
|
-
ctx.
|
|
287
|
-
ctx.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
289
|
+
ctx.lineWidth = RenderSystem.LOADING_STROKE;
|
|
290
|
+
ctx.lineCap = "round";
|
|
291
|
+
ctx.strokeStyle = ink;
|
|
292
|
+
ctx.globalAlpha = 0.22;
|
|
293
|
+
ctx.beginPath();
|
|
294
|
+
ctx.moveTo(x, y);
|
|
295
|
+
ctx.lineTo(x + lineWidth, y);
|
|
296
|
+
ctx.stroke();
|
|
297
|
+
if (progress > 0) {
|
|
298
|
+
ctx.globalAlpha = 1;
|
|
299
|
+
ctx.beginPath();
|
|
300
|
+
ctx.moveTo(x, y);
|
|
301
|
+
ctx.lineTo(x + lineWidth * progress, y);
|
|
302
|
+
ctx.stroke();
|
|
303
|
+
}
|
|
293
304
|
ctx.restore();
|
|
294
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Draws the frozen scene being left and, over its center, a ring whose
|
|
308
|
+
* outline fills with the share of images loaded. The ring has no fill.
|
|
309
|
+
*/
|
|
310
|
+
renderSceneLoading(options) {
|
|
311
|
+
const ctx = options.context;
|
|
312
|
+
const W = options.canvas.width;
|
|
313
|
+
const H = options.canvas.height;
|
|
314
|
+
this.applyPageBackground(options.pageBackground);
|
|
315
|
+
ctx.clearRect(0, 0, W, H);
|
|
316
|
+
ctx.drawImage(options.snapshot, 0, 0, W, H);
|
|
317
|
+
if (!options.showIndicator)
|
|
318
|
+
return;
|
|
319
|
+
const progress = this.clampProgress(options.progress);
|
|
320
|
+
const radius = Math.round(Math.min(44, Math.max(20, Math.min(W, H) * 0.06)));
|
|
321
|
+
const start = -Math.PI / 2;
|
|
322
|
+
const isDarkInk = options.ink !== "#ffffff";
|
|
323
|
+
ctx.save();
|
|
324
|
+
ctx.lineWidth = RenderSystem.LOADING_STROKE;
|
|
325
|
+
ctx.lineCap = "round";
|
|
326
|
+
ctx.strokeStyle = options.ink;
|
|
327
|
+
// A soft halo in the opposite tone keeps the ring legible over busy art.
|
|
328
|
+
ctx.shadowColor = isDarkInk ? "rgba(255, 255, 255, 0.5)" : "rgba(0, 0, 0, 0.5)";
|
|
329
|
+
ctx.shadowBlur = 6;
|
|
330
|
+
ctx.globalAlpha = 0.25;
|
|
331
|
+
ctx.beginPath();
|
|
332
|
+
ctx.arc(W / 2, H / 2, radius, 0, Math.PI * 2);
|
|
333
|
+
ctx.stroke();
|
|
334
|
+
if (progress > 0) {
|
|
335
|
+
ctx.globalAlpha = 1;
|
|
336
|
+
ctx.beginPath();
|
|
337
|
+
ctx.arc(W / 2, H / 2, radius, start, start + Math.PI * 2 * progress);
|
|
338
|
+
ctx.stroke();
|
|
339
|
+
}
|
|
340
|
+
ctx.restore();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Chooses white or near-black loading ink from the average brightness at the
|
|
344
|
+
* center of `source`. Falls back to white when the pixels cannot be read,
|
|
345
|
+
* which happens when a cross-origin image has tainted the canvas.
|
|
346
|
+
*/
|
|
347
|
+
pickLoadingInk(source) {
|
|
348
|
+
const size = Math.max(1, Math.min(96, source.width, source.height));
|
|
349
|
+
const x = Math.floor((source.width - size) / 2);
|
|
350
|
+
const y = Math.floor((source.height - size) / 2);
|
|
351
|
+
try {
|
|
352
|
+
const ctx = source.getContext("2d");
|
|
353
|
+
if (!ctx)
|
|
354
|
+
return "#ffffff";
|
|
355
|
+
const data = ctx.getImageData(x, y, size, size).data;
|
|
356
|
+
let luminance = 0;
|
|
357
|
+
let weight = 0;
|
|
358
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
359
|
+
const alpha = data[i + 3] / 255;
|
|
360
|
+
luminance +=
|
|
361
|
+
alpha * (0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]);
|
|
362
|
+
weight += alpha;
|
|
363
|
+
}
|
|
364
|
+
if (weight === 0)
|
|
365
|
+
return "#ffffff";
|
|
366
|
+
return luminance / weight / 255 > 0.6 ? "#101820" : "#ffffff";
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return "#ffffff";
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** @internal */
|
|
373
|
+
clampProgress(progress) {
|
|
374
|
+
return Number.isFinite(progress) ? Math.max(0, Math.min(1, progress)) : 0;
|
|
375
|
+
}
|
|
295
376
|
/** @internal */
|
|
296
377
|
drawFadeTransition(ctx, transition, width, height, progress) {
|
|
297
378
|
const firstHalf = progress < 0.5;
|
|
@@ -908,3 +989,5 @@ export class RenderSystem {
|
|
|
908
989
|
}
|
|
909
990
|
/** @internal */
|
|
910
991
|
RenderSystem.MAX_SURFACE_CACHE_ENTRIES = 256;
|
|
992
|
+
/** @internal Stroke of the boot line and the scene ring, which match. */
|
|
993
|
+
RenderSystem.LOADING_STROKE = 3;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { Game, Sprite } from "./minimo.js";
|
|
2
|
+
/**
|
|
3
|
+
* The weather and light of an {@link Ambient}.
|
|
4
|
+
*
|
|
5
|
+
* Every number runs from `0` to `1`, except {@link AmbientState.wind}, which
|
|
6
|
+
* runs from `-1` to `1`. Change it with {@link Ambient.set},
|
|
7
|
+
* {@link Ambient.to}, or {@link Ambient.preset}; read it from
|
|
8
|
+
* {@link Ambient.state}.
|
|
9
|
+
*/
|
|
10
|
+
export interface AmbientState {
|
|
11
|
+
/** Wind direction and strength: `1` is strong wind to the right, `-1` to the left. */
|
|
12
|
+
wind: number;
|
|
13
|
+
/** How irregularly the wind blows around {@link AmbientState.wind}. */
|
|
14
|
+
gusts: number;
|
|
15
|
+
/** Rain intensity. Rain splashes on the ambient's surfaces. */
|
|
16
|
+
rain: number;
|
|
17
|
+
/** Snow intensity. With {@link Ambient.snowSettles}, snow builds up on surfaces. */
|
|
18
|
+
snow: number;
|
|
19
|
+
/** Fog: drifting bands of mist and a haze over the scene. */
|
|
20
|
+
fog: number;
|
|
21
|
+
/** Lightning frequency. Flashes never exceed the safe rate, whatever the value. */
|
|
22
|
+
storm: number;
|
|
23
|
+
/** How dark the scene is: `1` is deep night. */
|
|
24
|
+
darkness: number;
|
|
25
|
+
/** How strongly {@link AmbientState.tintColor} colors the scene: `1` lays it at 50% over it. */
|
|
26
|
+
tint: number;
|
|
27
|
+
/** How many stars show in the sky. Clouds from rain, snow, and fog hide them. */
|
|
28
|
+
stars: number;
|
|
29
|
+
/** Hex color of the tint, such as `"#1b2a55"`. Interpolated in RGB by {@link Ambient.to}. */
|
|
30
|
+
tintColor: string;
|
|
31
|
+
}
|
|
32
|
+
/** The numeric fields of {@link AmbientState}. */
|
|
33
|
+
export type AmbientField = Exclude<keyof AmbientState, "tintColor">;
|
|
34
|
+
/** A partial state for {@link Ambient.set} and {@link Ambient.to}. */
|
|
35
|
+
export type AmbientChange = Partial<AmbientState>;
|
|
36
|
+
/**
|
|
37
|
+
* A named weather for {@link Ambient.preset}. Presets are geometric only, so
|
|
38
|
+
* every one works in any game without art.
|
|
39
|
+
*/
|
|
40
|
+
export type AmbientPreset = "clear" | "drizzle" | "rain" | "storm" | "snow" | "blizzard" | "mist" | "windy";
|
|
41
|
+
/** A horizontal line in world space that rain splashes on and snow settles on. */
|
|
42
|
+
export interface AmbientLine {
|
|
43
|
+
/** World X of the line's left end. */
|
|
44
|
+
left: number;
|
|
45
|
+
/** World X of the line's right end. */
|
|
46
|
+
right: number;
|
|
47
|
+
/** World Y of the line. */
|
|
48
|
+
y: number;
|
|
49
|
+
}
|
|
50
|
+
/** A rectangle in world space where no rain, snow, or falling drift is drawn. */
|
|
51
|
+
export interface AmbientRect {
|
|
52
|
+
left: number;
|
|
53
|
+
top: number;
|
|
54
|
+
right: number;
|
|
55
|
+
bottom: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Something rain splashes on and snow settles on. A sprite counts by the top
|
|
59
|
+
* edge of its collision body, and is followed if it moves.
|
|
60
|
+
*/
|
|
61
|
+
export type AmbientSurface = Sprite | AmbientLine;
|
|
62
|
+
/**
|
|
63
|
+
* A covered area, such as a cave, a roof, or an awning. A sprite counts by its
|
|
64
|
+
* collision body, and is followed if it moves.
|
|
65
|
+
*/
|
|
66
|
+
export type AmbientShelter = Sprite | AmbientRect;
|
|
67
|
+
/** Options for {@link Ambient}. Every field is optional. */
|
|
68
|
+
export interface AmbientOptions {
|
|
69
|
+
/** Sprite layer of the surface drawn behind the play field. Default `5`. */
|
|
70
|
+
backLayer?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Sprite layer of the surface drawn in front of the play field: the nearest
|
|
73
|
+
* particles, splashes, settled snow, haze, darkness, and flashes. Put the
|
|
74
|
+
* HUD above it. Default `900`.
|
|
75
|
+
*/
|
|
76
|
+
frontLayer?: number;
|
|
77
|
+
/** Share of particles that are nearer than the play field and drawn in front of it. Default `0.25`. */
|
|
78
|
+
front?: number;
|
|
79
|
+
/** Seed for reproducible weather. Omit it for different weather on every load. */
|
|
80
|
+
seed?: number;
|
|
81
|
+
/** Hard cap on rain, snow, and drift particles together. Lowered on weak devices. Default `900`. */
|
|
82
|
+
maxParticles?: number;
|
|
83
|
+
/** Wind speed in pixels per second at `wind: 1`. Default `600`. */
|
|
84
|
+
windStrength?: number;
|
|
85
|
+
/** Initial {@link Ambient.surfaces}. */
|
|
86
|
+
surfaces?: AmbientSurface[];
|
|
87
|
+
/** Initial {@link Ambient.shelters}. */
|
|
88
|
+
shelters?: AmbientShelter[];
|
|
89
|
+
/** Initial {@link Ambient.snowSettles}. Default `false`. */
|
|
90
|
+
snowSettles?: boolean;
|
|
91
|
+
/** Deepest settled snow, in pixels. Default `16`. */
|
|
92
|
+
maxSnowDepth?: number;
|
|
93
|
+
/** Hex color of the fog. Default `"#dde5ee"`; a sand color makes a sandstorm. */
|
|
94
|
+
fogColor?: string;
|
|
95
|
+
/** Hex color of rain. Default `"#c8d6ea"`. */
|
|
96
|
+
rainColor?: string;
|
|
97
|
+
/** Hex color of snow, falling and settled. Default `"#ffffff"`. */
|
|
98
|
+
snowColor?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Forces reduced motion on or off. By default it follows the player's
|
|
101
|
+
* `prefers-reduced-motion` setting: fewer particles and softer lightning.
|
|
102
|
+
*/
|
|
103
|
+
reducedMotion?: boolean;
|
|
104
|
+
/** Initial state. Fields left out start at `0`, and `tintColor` at `"#1b2a55"`. */
|
|
105
|
+
state?: AmbientChange;
|
|
106
|
+
}
|
|
107
|
+
/** A lightning strike, passed to {@link Ambient.onLightning}. */
|
|
108
|
+
export interface AmbientStrike {
|
|
109
|
+
/** `0` is overhead, `1` is far away. Use it to delay and soften the thunder. */
|
|
110
|
+
distance: number;
|
|
111
|
+
/** Screen X of the bolt, in pixels. */
|
|
112
|
+
x: number;
|
|
113
|
+
}
|
|
114
|
+
/** A glowing dot, the art of a drift that uses no image: fireflies, embers, dust. */
|
|
115
|
+
export interface AmbientDot {
|
|
116
|
+
/** Hex color of the dot, such as `"#fff6a0"`. */
|
|
117
|
+
color: string;
|
|
118
|
+
/** Radius of the solid core, in pixels. Default `2`. */
|
|
119
|
+
radius?: number;
|
|
120
|
+
/** Radius of the soft halo around the core, in pixels. Default `0`. */
|
|
121
|
+
glow?: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* How a drift moves.
|
|
125
|
+
*
|
|
126
|
+
* - `"fall"`: sways and tumbles downward, like leaves, petals, or ash. Rests a
|
|
127
|
+
* moment where it lands on a surface.
|
|
128
|
+
* - `"rise"`: floats upward, like embers or bubbles.
|
|
129
|
+
* - `"hover"`: wanders slowly in place, like fireflies or dust.
|
|
130
|
+
*/
|
|
131
|
+
export type AmbientDriftMotion = "fall" | "rise" | "hover";
|
|
132
|
+
/** Options for {@link Ambient.addDrift}. Give either `images` or `dot`. */
|
|
133
|
+
export interface AmbientDriftOptions {
|
|
134
|
+
/** How the drift moves. */
|
|
135
|
+
motion: AmbientDriftMotion;
|
|
136
|
+
/** Image keys the game loaded or created; each particle picks one. */
|
|
137
|
+
images?: string[];
|
|
138
|
+
/** A glowing dot, for drifts that need no image. */
|
|
139
|
+
dot?: AmbientDot;
|
|
140
|
+
/** Density from `0` to `1`, like the state fields. Default `0.5`. */
|
|
141
|
+
amount?: number;
|
|
142
|
+
/**
|
|
143
|
+
* Size of an image particle, as the pixel length of its longest side, or a
|
|
144
|
+
* `[min, max]` range. Nearer particles are drawn larger. Default `[16, 32]`.
|
|
145
|
+
* Ignored for dots, which use their own radius.
|
|
146
|
+
*/
|
|
147
|
+
size?: number | [number, number];
|
|
148
|
+
/** Speed multiplier. Default `1`. */
|
|
149
|
+
speed?: number;
|
|
150
|
+
/** How much images tumble and turn: `0` keeps them upright. Default `1` for `"fall"`, `0` otherwise. */
|
|
151
|
+
spin?: number;
|
|
152
|
+
/** Side-to-side swing. Default `1`. */
|
|
153
|
+
sway?: number;
|
|
154
|
+
/** How much the wind carries the drift, `0` for none, as underwater. Default `1` for `"fall"`, `0.5` otherwise. */
|
|
155
|
+
wind?: number;
|
|
156
|
+
/** Brightness flicker from `0` to `1`. Default `0`. */
|
|
157
|
+
twinkle?: number;
|
|
158
|
+
/**
|
|
159
|
+
* Draw the drift above the darkness, so it glows at night. Default `true`
|
|
160
|
+
* for a dot with a glow, `false` otherwise.
|
|
161
|
+
*/
|
|
162
|
+
lit?: boolean;
|
|
163
|
+
/** Fade out while rising instead of leaving through the top. Default `true` for dots, `false` for images. */
|
|
164
|
+
fade?: boolean;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A stream of leaves, petals, embers, bubbles, fireflies, or dust, created by
|
|
168
|
+
* {@link Ambient.addDrift}. It shares the ambient's wind and parallax.
|
|
169
|
+
*/
|
|
170
|
+
export declare class AmbientDrift {
|
|
171
|
+
/** How the drift moves. */
|
|
172
|
+
get motion(): AmbientDriftMotion;
|
|
173
|
+
/** Density from `0` to `1`. Setting it stops a running {@link AmbientDrift.to}. */
|
|
174
|
+
get amount(): number;
|
|
175
|
+
set amount(value: number);
|
|
176
|
+
/**
|
|
177
|
+
* Changes the density over `durationMs` milliseconds. The promise resolves
|
|
178
|
+
* when the amount arrives, or when a later change takes over.
|
|
179
|
+
*/
|
|
180
|
+
to(amount: number, durationMs: number): Promise<void>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Weather and atmosphere for a game: rain, snow, wind, fog, lightning,
|
|
184
|
+
* drifting leaves or fireflies, and the light of the hour.
|
|
185
|
+
*
|
|
186
|
+
* One shared state of intensities drives every effect, and every effect reads
|
|
187
|
+
* the same wind. All particles are painted into two canvas-sized surfaces, one
|
|
188
|
+
* behind the play field and one in front of it, so the module adds two sprites
|
|
189
|
+
* to the game whatever the weather.
|
|
190
|
+
*
|
|
191
|
+
* The ambient never moves a game sprite. The wind reaches the game only through
|
|
192
|
+
* {@link Ambient.windX}, which the game applies where it wants.
|
|
193
|
+
*
|
|
194
|
+
* Call {@link Ambient.update} once per frame. An ambient survives
|
|
195
|
+
* {@link Game.reset}: it puts its surfaces back on its next update and keeps
|
|
196
|
+
* its weather, so one ambient can serve every scene. Creating a new one in a
|
|
197
|
+
* scene's `onCreate` also works.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* const ambient = new Ambient(game, { surfaces: [ground], seed: 7 });
|
|
202
|
+
* ambient.preset("rain");
|
|
203
|
+
*
|
|
204
|
+
* // in onUpdate(dt)
|
|
205
|
+
* ambient.update(dt);
|
|
206
|
+
*
|
|
207
|
+
* // later: a storm rolls in over four seconds
|
|
208
|
+
* await ambient.to({ rain: 1, storm: 0.6, wind: 0.5 }, 4000);
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export declare class Ambient {
|
|
212
|
+
/**
|
|
213
|
+
* Things rain splashes on and snow settles on: sprites, by the top of their
|
|
214
|
+
* collision body, or {@link AmbientLine}s in world space.
|
|
215
|
+
*/
|
|
216
|
+
surfaces: AmbientSurface[];
|
|
217
|
+
/**
|
|
218
|
+
* Covered areas where no rain, snow, or falling drift is drawn: sprites, by
|
|
219
|
+
* their collision body, or {@link AmbientRect}s in world space.
|
|
220
|
+
*/
|
|
221
|
+
shelters: AmbientShelter[];
|
|
222
|
+
/** When `true`, snow builds up on surfaces while it snows and melts when it stops. */
|
|
223
|
+
snowSettles: boolean;
|
|
224
|
+
/** Softer lightning and fewer particles. Follows the player's system setting unless the game sets it. */
|
|
225
|
+
reducedMotion: boolean;
|
|
226
|
+
/**
|
|
227
|
+
* Called on every lightning strike, for thunder, a camera shake, or a
|
|
228
|
+
* gameplay reaction.
|
|
229
|
+
*/
|
|
230
|
+
onLightning: ((strike: AmbientStrike) => void) | null;
|
|
231
|
+
/**
|
|
232
|
+
* Creates an ambient and adds its two surfaces to the game.
|
|
233
|
+
*
|
|
234
|
+
* @param game - The game to draw into.
|
|
235
|
+
* @param options - Layers, surfaces, shelters, seed, colors, and initial state.
|
|
236
|
+
*/
|
|
237
|
+
constructor(game: Game, options?: AmbientOptions);
|
|
238
|
+
/** The current state. Read-only: change it with {@link Ambient.set}, {@link Ambient.to}, or {@link Ambient.preset}. */
|
|
239
|
+
get state(): Readonly<AmbientState>;
|
|
240
|
+
/**
|
|
241
|
+
* The wind at this frame, gusts included, in pixels per second. Positive
|
|
242
|
+
* blows right. This is the only way the weather reaches the game's sprites:
|
|
243
|
+
*
|
|
244
|
+
* ```ts
|
|
245
|
+
* player.vx += ambient.windX * 0.3 * dt;
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
get windX(): number;
|
|
249
|
+
/** `true` while any {@link Ambient.to} or {@link Ambient.preset} transition is running. */
|
|
250
|
+
get isTransitioning(): boolean;
|
|
251
|
+
/** Changes state fields at once. Fields left out keep their value. */
|
|
252
|
+
set(change: AmbientChange): void;
|
|
253
|
+
/**
|
|
254
|
+
* Changes state fields over `durationMs` milliseconds with an ease in and
|
|
255
|
+
* out. Fields left out keep their value and any transition they are in.
|
|
256
|
+
*
|
|
257
|
+
* A field that is already moving starts from wherever it is, so a new call
|
|
258
|
+
* can interrupt an old one without a jump. The promise resolves when every
|
|
259
|
+
* field arrives, or when later calls have taken all of them over.
|
|
260
|
+
*/
|
|
261
|
+
to(change: AmbientChange, durationMs: number): Promise<void>;
|
|
262
|
+
/**
|
|
263
|
+
* Moves to a named weather. The preset's fields are set, and the other
|
|
264
|
+
* weather fields (`wind`, `gusts`, `rain`, `snow`, `fog`, `storm`) go to `0`.
|
|
265
|
+
* Light fields are left alone, because weather and time of day are
|
|
266
|
+
* independent.
|
|
267
|
+
*
|
|
268
|
+
* @param name - The preset.
|
|
269
|
+
* @param durationMs - Transition length in milliseconds. `0`, the default, changes at once.
|
|
270
|
+
*/
|
|
271
|
+
preset(name: AmbientPreset, durationMs?: number): Promise<void>;
|
|
272
|
+
/**
|
|
273
|
+
* Adds a stream of drifting things: leaves, petals, embers, bubbles,
|
|
274
|
+
* fireflies, or dust. Images must already be loaded or created.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* const leaves = ambient.addDrift({ images: ["leaf-red", "leaf-gold"], motion: "fall", amount: 0.5 });
|
|
279
|
+
* const fireflies = ambient.addDrift({ dot: { color: "#fff6a0", glow: 8 }, motion: "hover", twinkle: 1 });
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
addDrift(options: AmbientDriftOptions): AmbientDrift;
|
|
283
|
+
/** Removes a drift at once. Fade it out first with `drift.to(0, ms)` for a gentle exit. */
|
|
284
|
+
removeDrift(drift: AmbientDrift): void;
|
|
285
|
+
/** Removes all settled snow at once. */
|
|
286
|
+
clearSnow(): void;
|
|
287
|
+
/**
|
|
288
|
+
* Depth of settled snow in pixels on a surface at world X, `0` where there
|
|
289
|
+
* is none. Useful for footprints, or for slowing a player in deep snow.
|
|
290
|
+
*/
|
|
291
|
+
snowDepthAt(surface: AmbientSurface, worldX: number): number;
|
|
292
|
+
/**
|
|
293
|
+
* Advances the weather by `dt` seconds. Call it once per frame from the
|
|
294
|
+
* scene's `onUpdate`. Transition promises resolve from here.
|
|
295
|
+
*/
|
|
296
|
+
update(dt: number): void;
|
|
297
|
+
/** Removes both surfaces from the game and resolves every pending transition. The ambient does nothing afterwards. */
|
|
298
|
+
destroy(): void;
|
|
299
|
+
}
|