@cosmoledo/gleam 1.0.1 → 1.0.2
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 +20 -2
- package/dist/gleam.d.ts +526 -62
- package/dist/gleam.esm.js +358 -81
- package/dist/gleam.esm.js.map +3 -3
- package/dist/gleam.js +358 -81
- package/dist/gleam.js.map +3 -3
- package/dist/gleam.min.js +7 -7
- package/dist/gleam.min.js.map +4 -4
- package/package.json +13 -11
package/dist/gleam.js
CHANGED
|
@@ -37,6 +37,8 @@ var Gleam = (() => {
|
|
|
37
37
|
Gameloop: () => Gameloop,
|
|
38
38
|
KEYBOARD_KEYS: () => KEYBOARD_KEYS,
|
|
39
39
|
Keyboard: () => Keyboard,
|
|
40
|
+
MAX_DT_SECONDS: () => MAX_DT_SECONDS,
|
|
41
|
+
MAX_STEPS_PER_FRAME: () => MAX_STEPS_PER_FRAME,
|
|
40
42
|
Music: () => Music,
|
|
41
43
|
POINTER_KEYS: () => POINTER_KEYS,
|
|
42
44
|
Particle: () => Particle,
|
|
@@ -207,6 +209,9 @@ var Gleam = (() => {
|
|
|
207
209
|
|
|
208
210
|
// src/core/EventSystem.ts
|
|
209
211
|
var _EventSystem = class _EventSystem {
|
|
212
|
+
/**
|
|
213
|
+
* Register a listener for `eventName`. Returns a dispose function — the primary teardown path. Multiple disposers (returned, `once`, `signal.abort`) are idempotent. Use {@link EventSystemOptions} for `once` and `signal` behavior.
|
|
214
|
+
*/
|
|
210
215
|
static addEventListener(eventName, callback, options = {}) {
|
|
211
216
|
if (options.signal?.aborted) {
|
|
212
217
|
return function dispose2() {
|
|
@@ -241,6 +246,7 @@ var Gleam = (() => {
|
|
|
241
246
|
}
|
|
242
247
|
return dispose;
|
|
243
248
|
}
|
|
249
|
+
/** Synchronously fire `eventName` with the typed payload. Listeners are invoked in registration order; nested dispatches and self-disposing listeners are handled safely. */
|
|
244
250
|
static dispatchEvent(eventName, ...params) {
|
|
245
251
|
const bucket = this.eventListener[eventName];
|
|
246
252
|
if (!bucket) {
|
|
@@ -288,21 +294,25 @@ var Gleam = (() => {
|
|
|
288
294
|
};
|
|
289
295
|
var AudioBase = class {
|
|
290
296
|
constructor(enabled = true) {
|
|
297
|
+
/** Registered audio elements keyed by name. Subclasses read this; mutate via {@link register}. */
|
|
291
298
|
__publicField(this, "songs", /* @__PURE__ */ new Map());
|
|
292
299
|
__publicField(this, "_enabled");
|
|
293
300
|
__publicField(this, "registered", false);
|
|
294
301
|
this._enabled = enabled;
|
|
295
302
|
EventSystem.addEventListener("gameloopStopped", () => this.stop());
|
|
296
303
|
}
|
|
304
|
+
/** Whether playback is permitted. Setting to `false` invokes {@link stop} immediately. */
|
|
297
305
|
get enabled() {
|
|
298
306
|
return this._enabled;
|
|
299
307
|
}
|
|
308
|
+
/** Setting to `false` invokes {@link stop} immediately; subclasses (notably {@link Music}) may re-start playback when flipped back to `true`. */
|
|
300
309
|
set enabled(value) {
|
|
301
310
|
this._enabled = value;
|
|
302
311
|
if (!value) {
|
|
303
312
|
this.stop();
|
|
304
313
|
}
|
|
305
314
|
}
|
|
315
|
+
/** Load and register one or more audio files. Each entry may be a bare URL string (the file's basename becomes the name) or a {@link RegisterData} object. Per-song volume falls back to `defaultVolume`. **Call once per instance** — throws on a second invocation, on non-finite volume, or on volume outside `[0, 1]`. Load failures are logged to `console.error` but don't throw. */
|
|
306
316
|
register(defaultVolume = 1, ...songs) {
|
|
307
317
|
this.throwOnBadVolume(defaultVolume, "defaultVolume");
|
|
308
318
|
if (this.registered) {
|
|
@@ -331,6 +341,7 @@ var Gleam = (() => {
|
|
|
331
341
|
this.songs.set(song.name, audio);
|
|
332
342
|
});
|
|
333
343
|
}
|
|
344
|
+
/** Base hook called when {@link enabled} flips to `false` and on `"gameloopStopped"`. The default is a no-op; {@link Sound} and {@link Music} override it to cut playback. Subclass overrides should call `super.stop()`. */
|
|
334
345
|
stop() {
|
|
335
346
|
}
|
|
336
347
|
throwOnBadVolume(volume, name) {
|
|
@@ -409,7 +420,7 @@ var Gleam = (() => {
|
|
|
409
420
|
"ease-in": easeIn,
|
|
410
421
|
"ease-in-out": easeInOut,
|
|
411
422
|
"ease-out": easeOut,
|
|
412
|
-
|
|
423
|
+
linear
|
|
413
424
|
};
|
|
414
425
|
|
|
415
426
|
// src/utilities/Array.ts
|
|
@@ -460,18 +471,24 @@ var Gleam = (() => {
|
|
|
460
471
|
__publicField(this, "next", null);
|
|
461
472
|
__publicField(this, "fadeCancel", null);
|
|
462
473
|
}
|
|
474
|
+
/** `true` while a fade is in progress OR the current track is actively playing. */
|
|
463
475
|
get isPlaying() {
|
|
464
476
|
return !!this.fadeCancel || this.current instanceof window.Audio && !this.current.paused;
|
|
465
477
|
}
|
|
478
|
+
/** Whether music playback is permitted (inherited from {@link AudioBase}). */
|
|
466
479
|
get enabled() {
|
|
467
480
|
return super.enabled;
|
|
468
481
|
}
|
|
482
|
+
/** Flipping from `false` to `true` while no music is playing auto-starts a fade-in to a random track. */
|
|
469
483
|
set enabled(value) {
|
|
470
484
|
super.enabled = value;
|
|
471
485
|
if (value && !this.isPlaying) {
|
|
472
486
|
this.fade();
|
|
473
487
|
}
|
|
474
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Cross-fade to `name` (or a random unplayed track when `null`) over `fadeTime` ms. `easing.cur` controls the outgoing track's volume curve, `easing.next` the incoming one. Cancels any in-progress fade. No-op when disabled. Throws on `fadeTime <= 0`, an empty registry, or an unknown `name`. When the new track ends, the next fade fires automatically — call {@link stop} to break the cycle.
|
|
491
|
+
*/
|
|
475
492
|
fade(name = null, fadeTime = 1e3, easing = {
|
|
476
493
|
cur: "ease-in",
|
|
477
494
|
next: "ease-out"
|
|
@@ -548,6 +565,7 @@ var Gleam = (() => {
|
|
|
548
565
|
}
|
|
549
566
|
});
|
|
550
567
|
}
|
|
568
|
+
/** Stop everything immediately: cancels any in-flight fade, halts current and next tracks, and breaks the auto-cycle chain. Restart via {@link fade} or by flipping {@link enabled}. */
|
|
551
569
|
stop() {
|
|
552
570
|
super.stop();
|
|
553
571
|
this.fadeCancel?.();
|
|
@@ -579,6 +597,7 @@ var Gleam = (() => {
|
|
|
579
597
|
super(...arguments);
|
|
580
598
|
__publicField(this, "currentSounds", []);
|
|
581
599
|
}
|
|
600
|
+
/** Play the registered sound `name` once. Returns a promise that resolves when playback starts (or immediately if `enabled` is `false`) and rejects on autoplay/permission errors. Throws synchronously if no sounds are registered or `name` is unknown. Each call allocates a clone, so concurrent plays of the same name overlap. */
|
|
582
601
|
play(name) {
|
|
583
602
|
if (this.songs.size === 0) {
|
|
584
603
|
throw new Error("No sounds registered!");
|
|
@@ -604,6 +623,7 @@ var Gleam = (() => {
|
|
|
604
623
|
throw err;
|
|
605
624
|
});
|
|
606
625
|
}
|
|
626
|
+
/** Stop and forget every currently-playing clone. Also calls the base-class teardown. */
|
|
607
627
|
stop() {
|
|
608
628
|
super.stop();
|
|
609
629
|
this.currentSounds.forEach((audio) => audio.stop());
|
|
@@ -763,6 +783,7 @@ var Gleam = (() => {
|
|
|
763
783
|
__publicField(this, "_alpha", 1);
|
|
764
784
|
this.set(r, g, b, a);
|
|
765
785
|
}
|
|
786
|
+
/** Parse `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa` (case-insensitive, `#` optional). Throws on any other shape or on non-hex characters. */
|
|
766
787
|
static fromHex(hex) {
|
|
767
788
|
let cleanHex = hex.replace("#", "").toUpperCase();
|
|
768
789
|
if (!/^[0-9A-F]+$/.test(cleanHex)) {
|
|
@@ -783,6 +804,7 @@ var Gleam = (() => {
|
|
|
783
804
|
}
|
|
784
805
|
return new _Color(r, g, b);
|
|
785
806
|
}
|
|
807
|
+
/** Build from HSL(A). `h` in **degrees** (wraps mod 360), `s`/`l` in percent `[0, 100]`, `a` in `[0, 1]`. The degree convention matches CSS — note that {@link hueRotate} uses radians instead. */
|
|
786
808
|
static fromHSL(h, s, l, a = 1) {
|
|
787
809
|
const hNorm = wrapValue(h, 0, 360) / 360;
|
|
788
810
|
const sNorm = s / 100;
|
|
@@ -799,18 +821,23 @@ var Gleam = (() => {
|
|
|
799
821
|
}
|
|
800
822
|
return new _Color(r * 255, g * 255, b * 255, a);
|
|
801
823
|
}
|
|
824
|
+
/** Red channel, `[0, 255]`. Read-only; mutate via {@link set} or any chainable transform. */
|
|
802
825
|
get r() {
|
|
803
826
|
return this._r;
|
|
804
827
|
}
|
|
828
|
+
/** Green channel, `[0, 255]`. Read-only; mutate via {@link set} or any chainable transform. */
|
|
805
829
|
get g() {
|
|
806
830
|
return this._g;
|
|
807
831
|
}
|
|
832
|
+
/** Blue channel, `[0, 255]`. Read-only; mutate via {@link set} or any chainable transform. */
|
|
808
833
|
get b() {
|
|
809
834
|
return this._b;
|
|
810
835
|
}
|
|
836
|
+
/** Alpha channel, `[0, 1]`. Read-only; mutate via {@link set} (pass the fourth arg). */
|
|
811
837
|
get alpha() {
|
|
812
838
|
return this._alpha;
|
|
813
839
|
}
|
|
840
|
+
/** Primary mutator — every other transform on this class routes through it. Clamps `r`/`g`/`b` to `[0, 255]` and `a` to `[0, 1]`; alpha is snapped to exact `0` or `1` when within `approxEqual` tolerance so equality checks stay clean. Returns `this` for chaining. */
|
|
814
841
|
set(r, g, b, a) {
|
|
815
842
|
this._r = clamp(r, 0, 255);
|
|
816
843
|
this._g = clamp(g, 0, 255);
|
|
@@ -821,6 +848,7 @@ var Gleam = (() => {
|
|
|
821
848
|
}
|
|
822
849
|
return this;
|
|
823
850
|
}
|
|
851
|
+
/** Apply a 3×3 RGB color matrix in row-major order (`m1..m9`). Alpha is unchanged. Used by {@link grayscale}, {@link hueRotate}, {@link saturate}, {@link sepia}. Mutates and returns `this`. */
|
|
824
852
|
applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9) {
|
|
825
853
|
return this.set(
|
|
826
854
|
this.r * m1 + this.g * m2 + this.b * m3,
|
|
@@ -828,9 +856,11 @@ var Gleam = (() => {
|
|
|
828
856
|
this.r * m7 + this.g * m8 + this.b * m9
|
|
829
857
|
);
|
|
830
858
|
}
|
|
859
|
+
/** Multiply each channel by `factor`. `factor < 1` darkens, `factor > 1` brightens (clamped at 255). Mutates and returns `this`. */
|
|
831
860
|
brightness(factor) {
|
|
832
861
|
return this.set(this.r * factor, this.g * factor, this.b * factor);
|
|
833
862
|
}
|
|
863
|
+
/** Push each channel away from `127.5` (the midtone) by `factor`. `factor < 1` flattens contrast, `> 1` increases it, `0` collapses every channel to gray. Mutates and returns `this`. */
|
|
834
864
|
contrast(factor) {
|
|
835
865
|
const midtone = 127.5;
|
|
836
866
|
return this.set(
|
|
@@ -839,6 +869,7 @@ var Gleam = (() => {
|
|
|
839
869
|
midtone + (this.b - midtone) * factor
|
|
840
870
|
);
|
|
841
871
|
}
|
|
872
|
+
/** Desaturate via the standard luminance-preserving matrix. `value` in `[0, 1]`: `0` is a no-op, `1` is full grayscale. Mutates and returns `this`. */
|
|
842
873
|
grayscale(value = 1) {
|
|
843
874
|
const m1 = 0.2126 + 0.7874 * (1 - value);
|
|
844
875
|
const m2 = 0.7152 - 0.7152 * (1 - value);
|
|
@@ -851,6 +882,7 @@ var Gleam = (() => {
|
|
|
851
882
|
const m9 = 0.0722 + 0.9278 * (1 - value);
|
|
852
883
|
return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
|
|
853
884
|
}
|
|
885
|
+
/** Rotate hue by `radians` (use `Math.PI / 2` etc.). Unlike {@link fromHSL}, this takes radians, not degrees. Mutates and returns `this`. */
|
|
854
886
|
hueRotate(radians) {
|
|
855
887
|
const cos = Math.cos(radians);
|
|
856
888
|
const sin = Math.sin(radians);
|
|
@@ -865,6 +897,7 @@ var Gleam = (() => {
|
|
|
865
897
|
const m9 = 0.072 + cos * 0.928 + sin * 0.072;
|
|
866
898
|
return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
|
|
867
899
|
}
|
|
900
|
+
/** Interpolate each channel toward its inverse (`255 - c`). `factor` in `[0, 1]`: `0` is unchanged, `1` is fully inverted. Mutates and returns `this`. */
|
|
868
901
|
invert(factor = 1) {
|
|
869
902
|
return this.set(
|
|
870
903
|
this.r * (1 - factor) + (255 - this.r) * factor,
|
|
@@ -872,6 +905,7 @@ var Gleam = (() => {
|
|
|
872
905
|
this.b * (1 - factor) + (255 - this.b) * factor
|
|
873
906
|
);
|
|
874
907
|
}
|
|
908
|
+
/** Linear blend toward `other`. `amount` in `[0, 1]`: `0` keeps `this`, `1` becomes `other`. Mixes alpha too. Mutates and returns `this`. */
|
|
875
909
|
mix(other, amount) {
|
|
876
910
|
const inv = 1 - amount;
|
|
877
911
|
return this.set(
|
|
@@ -881,6 +915,7 @@ var Gleam = (() => {
|
|
|
881
915
|
this.alpha * inv + other.alpha * amount
|
|
882
916
|
);
|
|
883
917
|
}
|
|
918
|
+
/** Round each RGB channel to the nearest integer. Alpha is untouched. Mutates and returns `this`. */
|
|
884
919
|
round() {
|
|
885
920
|
return this.set(
|
|
886
921
|
Math.round(this.r),
|
|
@@ -888,6 +923,7 @@ var Gleam = (() => {
|
|
|
888
923
|
Math.round(this.b)
|
|
889
924
|
);
|
|
890
925
|
}
|
|
926
|
+
/** Saturation matrix. `value` typically in `[0, 2]`: `0` desaturates to grayscale (same as `grayscale(1)`), `1` is a no-op, `> 1` oversaturates. Mutates and returns `this`. */
|
|
891
927
|
saturate(value = 1) {
|
|
892
928
|
const m1 = 0.213 + 0.787 * value;
|
|
893
929
|
const m2 = 0.715 - 0.715 * value;
|
|
@@ -900,6 +936,7 @@ var Gleam = (() => {
|
|
|
900
936
|
const m9 = 0.072 + 0.928 * value;
|
|
901
937
|
return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
|
|
902
938
|
}
|
|
939
|
+
/** Sepia matrix. `value` in `[0, 1]`: `0` is unchanged, `1` is full sepia. Mutates and returns `this`. */
|
|
903
940
|
sepia(value = 1) {
|
|
904
941
|
const m1 = 0.393 + 0.607 * (1 - value);
|
|
905
942
|
const m2 = 0.769 - 0.769 * (1 - value);
|
|
@@ -912,6 +949,7 @@ var Gleam = (() => {
|
|
|
912
949
|
const m9 = 0.131 + 0.869 * (1 - value);
|
|
913
950
|
return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
|
|
914
951
|
}
|
|
952
|
+
/** Tint or shade. `percent` in `[-1, 1]`: negative shades toward black, positive tints toward white, magnitude is the amount. Mutates and returns `this`. */
|
|
915
953
|
shade(percent) {
|
|
916
954
|
const target = percent < 0 ? 0 : 255;
|
|
917
955
|
const p = Math.abs(percent);
|
|
@@ -921,6 +959,7 @@ var Gleam = (() => {
|
|
|
921
959
|
this.b + (target - this.b) * p
|
|
922
960
|
);
|
|
923
961
|
}
|
|
962
|
+
/** CSS hex string. `#rrggbb` when alpha is exactly `1`, `#rrggbbaa` otherwise. Channels are rounded. */
|
|
924
963
|
toHex() {
|
|
925
964
|
const r = Math.round(this.r).toString(16).padStart(2, "0");
|
|
926
965
|
const g = Math.round(this.g).toString(16).padStart(2, "0");
|
|
@@ -932,6 +971,7 @@ var Gleam = (() => {
|
|
|
932
971
|
const a = Math.round(this.alpha * 255);
|
|
933
972
|
return `${rgb}${a.toString(16).padStart(2, "0")}`;
|
|
934
973
|
}
|
|
974
|
+
/** CSS HSL string. `hsl(h, s%, l%)` when alpha is exactly `1`, `hsla(...)` otherwise. Hue is in degrees. */
|
|
935
975
|
toHSL() {
|
|
936
976
|
const { h, s, l } = this.toHSLObject();
|
|
937
977
|
const cssH = Math.round(h);
|
|
@@ -939,6 +979,7 @@ var Gleam = (() => {
|
|
|
939
979
|
const cssL = Math.round(l);
|
|
940
980
|
return this.alpha === 1 ? `hsl(${cssH}, ${cssS}%, ${cssL}%)` : `hsla(${cssH}, ${cssS}%, ${cssL}%, ${this.alpha.toFixed(2)})`;
|
|
941
981
|
}
|
|
982
|
+
/** HSL(A) components as numbers — see {@link HSLObject}. Use when you need to compute against the values rather than render them as a string. */
|
|
942
983
|
toHSLObject() {
|
|
943
984
|
const r = this.r / 255;
|
|
944
985
|
const g = this.g / 255;
|
|
@@ -966,15 +1007,18 @@ var Gleam = (() => {
|
|
|
966
1007
|
}
|
|
967
1008
|
return { h: h * 360, s: s * 100, l: l * 100, a: this.alpha };
|
|
968
1009
|
}
|
|
1010
|
+
/** CSS RGB string. `rgb(r, g, b)` when alpha is exactly `1`, `rgba(r, g, b, a)` otherwise. Channels are rounded. */
|
|
969
1011
|
toRGB() {
|
|
970
1012
|
const r = Math.round(this.r);
|
|
971
1013
|
const g = Math.round(this.g);
|
|
972
1014
|
const b = Math.round(this.b);
|
|
973
1015
|
return this.alpha === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${this.alpha.toFixed(2)})`;
|
|
974
1016
|
}
|
|
1017
|
+
/** New `Color` with the same channels. */
|
|
975
1018
|
clone() {
|
|
976
1019
|
return new _Color(this.r, this.g, this.b, this.alpha);
|
|
977
1020
|
}
|
|
1021
|
+
/** Approximate equality (within `approxEqual` tolerance). Pass `compareAlpha: false` to ignore the alpha channel. */
|
|
978
1022
|
equals(other, compareAlpha = true) {
|
|
979
1023
|
return approxEqual(this.r, other.r) && approxEqual(this.g, other.g) && approxEqual(this.b, other.b) && (!compareAlpha || approxEqual(this.alpha, other.alpha));
|
|
980
1024
|
}
|
|
@@ -1116,12 +1160,14 @@ var Gleam = (() => {
|
|
|
1116
1160
|
__publicField(this, "sideIsDirty", true);
|
|
1117
1161
|
this.set(x, y, w, h);
|
|
1118
1162
|
}
|
|
1163
|
+
/** Build from an `HTMLElement` (via `getBoundingClientRect`) or a `DOMRect`. */
|
|
1119
1164
|
static fromBoundingClientRect(rect) {
|
|
1120
1165
|
if (rect instanceof HTMLElement) {
|
|
1121
1166
|
rect = rect.getBoundingClientRect();
|
|
1122
1167
|
}
|
|
1123
1168
|
return new _Rect(rect.left, rect.top, rect.width, rect.height);
|
|
1124
1169
|
}
|
|
1170
|
+
/** Axis-aligned bounding box of a polygon's points. Throws if the polygon has no points. */
|
|
1125
1171
|
static fromPolygon(polygon) {
|
|
1126
1172
|
if (polygon.points.length === 0) {
|
|
1127
1173
|
throw new Error("Supplied polygon has no points!");
|
|
@@ -1146,34 +1192,43 @@ var Gleam = (() => {
|
|
|
1146
1192
|
});
|
|
1147
1193
|
return new _Rect(minX, minY, maxX - minX, maxY - minY);
|
|
1148
1194
|
}
|
|
1195
|
+
/** Height. */
|
|
1149
1196
|
get h() {
|
|
1150
1197
|
return this._h;
|
|
1151
1198
|
}
|
|
1199
|
+
/** Height. */
|
|
1152
1200
|
set h(value) {
|
|
1153
1201
|
this._h = value;
|
|
1154
1202
|
this.sideIsDirty = true;
|
|
1155
1203
|
}
|
|
1204
|
+
/** Width. */
|
|
1156
1205
|
get w() {
|
|
1157
1206
|
return this._w;
|
|
1158
1207
|
}
|
|
1208
|
+
/** Width. */
|
|
1159
1209
|
set w(value) {
|
|
1160
1210
|
this._w = value;
|
|
1161
1211
|
this.sideIsDirty = true;
|
|
1162
1212
|
}
|
|
1213
|
+
/** Top-left x. */
|
|
1163
1214
|
get x() {
|
|
1164
1215
|
return this._x;
|
|
1165
1216
|
}
|
|
1217
|
+
/** Top-left x. */
|
|
1166
1218
|
set x(value) {
|
|
1167
1219
|
this._x = value;
|
|
1168
1220
|
this.sideIsDirty = true;
|
|
1169
1221
|
}
|
|
1222
|
+
/** Top-left y. */
|
|
1170
1223
|
get y() {
|
|
1171
1224
|
return this._y;
|
|
1172
1225
|
}
|
|
1226
|
+
/** Top-left y. */
|
|
1173
1227
|
set y(value) {
|
|
1174
1228
|
this._y = value;
|
|
1175
1229
|
this.sideIsDirty = true;
|
|
1176
1230
|
}
|
|
1231
|
+
/** Derived sides/center/halfSize. Lazily recomputed after any `x`/`y`/`w`/`h` change. */
|
|
1177
1232
|
get sides() {
|
|
1178
1233
|
if (this.sideIsDirty) {
|
|
1179
1234
|
this._sides = {
|
|
@@ -1189,6 +1244,7 @@ var Gleam = (() => {
|
|
|
1189
1244
|
}
|
|
1190
1245
|
return this._sides;
|
|
1191
1246
|
}
|
|
1247
|
+
/** Grow on every side by `delta` (`x`/`y` shift in, `w`/`h` grow by `2*delta`). Pass a negative value to shrink. Mutates and returns `this`. */
|
|
1192
1248
|
inflate(delta) {
|
|
1193
1249
|
this.x -= delta;
|
|
1194
1250
|
this.y -= delta;
|
|
@@ -1197,12 +1253,16 @@ var Gleam = (() => {
|
|
|
1197
1253
|
this.sideIsDirty = true;
|
|
1198
1254
|
return this;
|
|
1199
1255
|
}
|
|
1256
|
+
/** Round `x` and `y` to the nearest integer. `w`/`h` are unchanged. Mutates and returns `this`. */
|
|
1200
1257
|
round() {
|
|
1201
1258
|
this.x = Math.round(this.x);
|
|
1202
1259
|
this.y = Math.round(this.y);
|
|
1203
1260
|
this.sideIsDirty = true;
|
|
1204
1261
|
return this;
|
|
1205
1262
|
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Replace fields. The first arg may be a `Vector4` (sets all four), a `Vector2` (sets `x`/`y` only, unless explicit `w`/`h` follow), or `x` as a number with separate `y`/`w`/`h`. Mutates and returns `this`.
|
|
1265
|
+
*/
|
|
1206
1266
|
set(x = 0, y = 0, w, h) {
|
|
1207
1267
|
if (typeof x === "number") {
|
|
1208
1268
|
this.x = x;
|
|
@@ -1224,15 +1284,19 @@ var Gleam = (() => {
|
|
|
1224
1284
|
this.sideIsDirty = true;
|
|
1225
1285
|
return this;
|
|
1226
1286
|
}
|
|
1287
|
+
/** AABB-vs-AABB overlap test (inclusive of touching edges). */
|
|
1227
1288
|
collide(rect) {
|
|
1228
1289
|
return this.x <= rect.x + rect.w && this.x + this.w >= rect.x && this.y <= rect.y + rect.h && this.y + this.h >= rect.y;
|
|
1229
1290
|
}
|
|
1291
|
+
/** `true` when `rect` is fully inside `this`. */
|
|
1230
1292
|
collideFull(rect) {
|
|
1231
1293
|
return rect.x + rect.w <= this.x + this.w && rect.x >= this.x && rect.y >= this.y && rect.y + rect.h <= this.y + this.h;
|
|
1232
1294
|
}
|
|
1295
|
+
/** `true` when `vec` lies inside `this` (inclusive of edges). */
|
|
1233
1296
|
collidePoint(vec) {
|
|
1234
1297
|
return this.x <= vec.x && vec.x <= this.x + this.w && this.y <= vec.y && vec.y <= this.y + this.h;
|
|
1235
1298
|
}
|
|
1299
|
+
/** Side of `this` that `rect` overlaps from, or `"none"` if disjoint. Useful for picking a bounce axis. */
|
|
1236
1300
|
collideSide(rect) {
|
|
1237
1301
|
const dx = this.x + this.w * 0.5 - (rect.x + rect.w * 0.5);
|
|
1238
1302
|
const dy = this.y + this.h * 0.5 - (rect.y + rect.h * 0.5);
|
|
@@ -1250,18 +1314,23 @@ var Gleam = (() => {
|
|
|
1250
1314
|
}
|
|
1251
1315
|
return collision;
|
|
1252
1316
|
}
|
|
1317
|
+
/** Top-left corner as a new `Vec2`. */
|
|
1253
1318
|
pos() {
|
|
1254
1319
|
return new Vec2(this.x, this.y);
|
|
1255
1320
|
}
|
|
1321
|
+
/** Width and height as a new `Vec2`. */
|
|
1256
1322
|
size() {
|
|
1257
1323
|
return new Vec2(this.w, this.h);
|
|
1258
1324
|
}
|
|
1325
|
+
/** Debug string like `"Rect [x: 0, y: 0, w: 10, h: 20]"`. */
|
|
1259
1326
|
toString() {
|
|
1260
1327
|
return `Rect [x: ${this.x}, y: ${this.y}, w: ${this.w}, h: ${this.h}]`;
|
|
1261
1328
|
}
|
|
1329
|
+
/** New `Rect` with the same values. */
|
|
1262
1330
|
clone() {
|
|
1263
1331
|
return new _Rect(this.x, this.y, this.w, this.h);
|
|
1264
1332
|
}
|
|
1333
|
+
/** Approximate equality. Pass `withSize: false` to compare position only. */
|
|
1265
1334
|
equals(other, withSize = true) {
|
|
1266
1335
|
let output = approxEqual(this.x, other.x) && approxEqual(this.y, other.y);
|
|
1267
1336
|
if (output && withSize) {
|
|
@@ -1293,25 +1362,31 @@ var Gleam = (() => {
|
|
|
1293
1362
|
);
|
|
1294
1363
|
var Vec2 = class _Vec2 {
|
|
1295
1364
|
constructor(x = 0, y) {
|
|
1365
|
+
/** Horizontal component. */
|
|
1296
1366
|
__publicField(this, "x", 0);
|
|
1367
|
+
/** Vertical component. */
|
|
1297
1368
|
__publicField(this, "y", 0);
|
|
1298
1369
|
this.calculate(Operation.Equal, x, y);
|
|
1299
1370
|
}
|
|
1371
|
+
/** Unit vector at angle `rad` (radians), scaled per-axis. `scaleY` defaults to `scaleX`. */
|
|
1300
1372
|
static fromAngle(rad, scaleX = 1, scaleY = scaleX) {
|
|
1301
1373
|
return new _Vec2(Math.cos(rad) * scaleX, Math.sin(rad) * scaleY);
|
|
1302
1374
|
}
|
|
1303
1375
|
set(x, y) {
|
|
1304
1376
|
return this.calculate(Operation.Equal, x, y);
|
|
1305
1377
|
}
|
|
1378
|
+
/** Set each component to its absolute value. Mutates and returns `this`. */
|
|
1306
1379
|
abs() {
|
|
1307
1380
|
return this.map(Math.abs);
|
|
1308
1381
|
}
|
|
1309
1382
|
add(x, y) {
|
|
1310
1383
|
return this.calculate(Operation.Add, x, y);
|
|
1311
1384
|
}
|
|
1385
|
+
/** Round each component up. Mutates and returns `this`. */
|
|
1312
1386
|
ceil() {
|
|
1313
1387
|
return this.map(Math.ceil);
|
|
1314
1388
|
}
|
|
1389
|
+
/** Clamp each axis to its `[min, max]` range. `y` defaults to `x`. Mutates and returns `this`. */
|
|
1315
1390
|
clamp(x, y = x) {
|
|
1316
1391
|
this.x = clamp(this.x, x[0], x[1]);
|
|
1317
1392
|
this.y = clamp(this.y, y[0], y[1]);
|
|
@@ -1320,9 +1395,19 @@ var Gleam = (() => {
|
|
|
1320
1395
|
div(x, y) {
|
|
1321
1396
|
return this.calculate(Operation.Div, x, y);
|
|
1322
1397
|
}
|
|
1398
|
+
/** Round each component down. Mutates and returns `this`. */
|
|
1323
1399
|
floor() {
|
|
1324
1400
|
return this.map(Math.floor);
|
|
1325
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Apply `callback` to each component (`index` is `0` for x, `1` for y). Mutates and returns `this`.
|
|
1404
|
+
*
|
|
1405
|
+
* @example
|
|
1406
|
+
* ```ts
|
|
1407
|
+
* new Vec2(3.6, -2.1).map(Math.trunc); // Vec2 { x: 3, y: -2 }
|
|
1408
|
+
* new Vec2(2, 5).map((v, i) => v * (i + 1)); // Vec2 { x: 2, y: 10 }
|
|
1409
|
+
* ```
|
|
1410
|
+
*/
|
|
1326
1411
|
map(callback) {
|
|
1327
1412
|
this.x = callback(this.x, 0);
|
|
1328
1413
|
this.y = callback(this.y, 1);
|
|
@@ -1334,9 +1419,11 @@ var Gleam = (() => {
|
|
|
1334
1419
|
mult(x, y) {
|
|
1335
1420
|
return this.calculate(Operation.Mult, x, y);
|
|
1336
1421
|
}
|
|
1422
|
+
/** Flip the sign of both components (same as `mult(-1)`). Mutates and returns `this`. */
|
|
1337
1423
|
negate() {
|
|
1338
1424
|
return this.mult(-1);
|
|
1339
1425
|
}
|
|
1426
|
+
/** Scale to unit length. Zero-length vectors are left untouched and warn (throttled). Mutates and returns `this`. */
|
|
1340
1427
|
normalize() {
|
|
1341
1428
|
const length = this.length();
|
|
1342
1429
|
if (approxEqual(length, 0)) {
|
|
@@ -1345,6 +1432,7 @@ var Gleam = (() => {
|
|
|
1345
1432
|
}
|
|
1346
1433
|
return this.map((value) => value / length);
|
|
1347
1434
|
}
|
|
1435
|
+
/** Scale so `|x| + |y| === 1`. Zero-length vectors are left untouched. Mutates and returns `this`. */
|
|
1348
1436
|
normalizeManhattan() {
|
|
1349
1437
|
const length = this.lengthManhattan();
|
|
1350
1438
|
if (approxEqual(length, 0)) {
|
|
@@ -1355,6 +1443,7 @@ var Gleam = (() => {
|
|
|
1355
1443
|
rem(x, y) {
|
|
1356
1444
|
return this.calculate(Operation.Rem, x, y);
|
|
1357
1445
|
}
|
|
1446
|
+
/** Round each component to the nearest integer. Mutates and returns `this`. */
|
|
1358
1447
|
round() {
|
|
1359
1448
|
this.x = Math.round(this.x);
|
|
1360
1449
|
this.y = Math.round(this.y);
|
|
@@ -1363,38 +1452,48 @@ var Gleam = (() => {
|
|
|
1363
1452
|
sub(x, y) {
|
|
1364
1453
|
return this.calculate(Operation.Sub, x, y);
|
|
1365
1454
|
}
|
|
1455
|
+
/** Angle in radians. No arg: angle of `this` from origin. With `other`: angle from `this` toward `other`. */
|
|
1366
1456
|
angle(other) {
|
|
1367
1457
|
if (!other) {
|
|
1368
1458
|
return Math.atan2(this.y, this.x);
|
|
1369
1459
|
}
|
|
1370
1460
|
return Math.atan2(other.y - this.y, other.x - this.x);
|
|
1371
1461
|
}
|
|
1462
|
+
/** Euclidean distance to `other`. */
|
|
1372
1463
|
distance(other) {
|
|
1373
1464
|
return Math.sqrt(
|
|
1374
1465
|
Math.pow(other.x - this.x, 2) + Math.pow(other.y - this.y, 2)
|
|
1375
1466
|
);
|
|
1376
1467
|
}
|
|
1468
|
+
/** Manhattan distance (`|dx| + |dy|`) to `other`. */
|
|
1377
1469
|
distanceManhattan(other) {
|
|
1378
1470
|
return Math.abs(other.x - this.x) + Math.abs(other.y - this.y);
|
|
1379
1471
|
}
|
|
1472
|
+
/** Dot product with `other`. */
|
|
1380
1473
|
dotProduct(other) {
|
|
1381
1474
|
return this.x * other.x + this.y * other.y;
|
|
1382
1475
|
}
|
|
1476
|
+
/** `true` when both components are finite (rules out `NaN` and `±Infinity`). */
|
|
1383
1477
|
isValid() {
|
|
1384
1478
|
return Number.isFinite(this.x) && Number.isFinite(this.y);
|
|
1385
1479
|
}
|
|
1480
|
+
/** Euclidean magnitude (`sqrt(x² + y²)`). */
|
|
1386
1481
|
length() {
|
|
1387
1482
|
return Math.sqrt(this.x * this.x + this.y * this.y);
|
|
1388
1483
|
}
|
|
1484
|
+
/** Manhattan magnitude (`|x| + |y|`). */
|
|
1389
1485
|
lengthManhattan() {
|
|
1390
1486
|
return Math.abs(this.x) + Math.abs(this.y);
|
|
1391
1487
|
}
|
|
1488
|
+
/** Larger of the two components. */
|
|
1392
1489
|
max() {
|
|
1393
1490
|
return Math.max(this.x, this.y);
|
|
1394
1491
|
}
|
|
1492
|
+
/** Smaller of the two components. */
|
|
1395
1493
|
min() {
|
|
1396
1494
|
return Math.min(this.x, this.y);
|
|
1397
1495
|
}
|
|
1496
|
+
/** Tuple `[x, y]`. */
|
|
1398
1497
|
toArray() {
|
|
1399
1498
|
return [this.x, this.y];
|
|
1400
1499
|
}
|
|
@@ -1404,9 +1503,11 @@ var Gleam = (() => {
|
|
|
1404
1503
|
toRectAddSize(width, height) {
|
|
1405
1504
|
return this.concat(false, width, height);
|
|
1406
1505
|
}
|
|
1506
|
+
/** Debug string like `"Vec2 [x: 1, y: 2]"`. */
|
|
1407
1507
|
toString() {
|
|
1408
1508
|
return `Vec2 [x: ${this.x}, y: ${this.y}]`;
|
|
1409
1509
|
}
|
|
1510
|
+
/** New `Vec2` with the same components. */
|
|
1410
1511
|
clone() {
|
|
1411
1512
|
return new _Vec2(this.x, this.y);
|
|
1412
1513
|
}
|
|
@@ -1486,9 +1587,11 @@ var Gleam = (() => {
|
|
|
1486
1587
|
// src/core/Settings.ts
|
|
1487
1588
|
var LOCAL_STORAGE_KEY = "gleam";
|
|
1488
1589
|
var Settings = class {
|
|
1590
|
+
/** Read-only view of the persisted localStorage blob. Writes go through {@link setLocalStorage}. */
|
|
1489
1591
|
static get localStorage() {
|
|
1490
1592
|
return this._localStorage;
|
|
1491
1593
|
}
|
|
1594
|
+
/** One-time setup — called by `Game`'s constructor with the overrides passed to `super()`. Validates {@link fps}, loads the persisted localStorage blob, derives `language` from `navigator.language`, and wires the close-warning handler if {@link warnBeforeClose}. Throws if called twice or if `fps` isn't a finite positive number. */
|
|
1492
1595
|
static init(overrides, game) {
|
|
1493
1596
|
if (this.initialized) {
|
|
1494
1597
|
throw new Error("Settings.init called twice");
|
|
@@ -1528,6 +1631,7 @@ var Gleam = (() => {
|
|
|
1528
1631
|
}
|
|
1529
1632
|
this.initialized = true;
|
|
1530
1633
|
}
|
|
1634
|
+
/** Typed setter for the persisted localStorage blob. Writes both in-memory and to actual `localStorage` (under a single JSON key — `"gleam"`). The only supported way to mutate persisted state. */
|
|
1531
1635
|
static setLocalStorage(key, value) {
|
|
1532
1636
|
this._localStorage[key] = value;
|
|
1533
1637
|
localStorage.setItem(
|
|
@@ -1536,16 +1640,27 @@ var Gleam = (() => {
|
|
|
1536
1640
|
);
|
|
1537
1641
|
}
|
|
1538
1642
|
};
|
|
1643
|
+
/** Enable smoothing on the main canvas context. Default `false` for crisp pixel art. */
|
|
1539
1644
|
__publicField(Settings, "antialias", false);
|
|
1645
|
+
/** Start the gameloop automatically after `init()` resolves. Disable to drive `gameloop.startLoop()` manually. Default `true`. */
|
|
1540
1646
|
__publicField(Settings, "autoloop", true);
|
|
1647
|
+
/** CSS color used when {@link useClearRect} is `false`. Default `"#444"`. */
|
|
1541
1648
|
__publicField(Settings, "backgroundColor", "#444");
|
|
1649
|
+
/** Debug mode: assigns the `Game` instance to `window.game` and lets {@link Keyboard} Escape stop the loop. Default `false`. */
|
|
1542
1650
|
__publicField(Settings, "debug", false);
|
|
1651
|
+
/** Skip the per-frame canvas clear. Use for trail/decay effects where you manage clearing yourself. Default `false`. */
|
|
1543
1652
|
__publicField(Settings, "doNotClear", false);
|
|
1653
|
+
/** Stretch the main canvas to fill the window on resize while preserving its aspect ratio. Default `true`. */
|
|
1544
1654
|
__publicField(Settings, "enableResize", true);
|
|
1655
|
+
/** Default font family for `canman.setFontSize`. Default `"Arial"`. */
|
|
1545
1656
|
__publicField(Settings, "font", "Arial");
|
|
1657
|
+
/** **Seconds per fixed step**, not frames per second — `1 / 60` = 60 Hz, `1 / 30` = 30 Hz. Must be finite and `> 0` or {@link init} throws. */
|
|
1546
1658
|
__publicField(Settings, "fps", 1 / 60);
|
|
1659
|
+
/** Callback invoked from the `beforeunload` handler when {@link warnBeforeClose} is `true`. Useful for "are you sure?" autosave logic. */
|
|
1547
1660
|
__publicField(Settings, "triedToClose");
|
|
1661
|
+
/** Clear the canvas with `clearRect` (transparent) when `true`, or `fillRect` with {@link backgroundColor} when `false`. Default `true`. */
|
|
1548
1662
|
__publicField(Settings, "useClearRect", true);
|
|
1663
|
+
/** Show a browser "are you sure?" dialog on tab close. Required for {@link triedToClose} to fire. Default `false`. */
|
|
1549
1664
|
__publicField(Settings, "warnBeforeClose", false);
|
|
1550
1665
|
__publicField(Settings, "initialized", false);
|
|
1551
1666
|
// Only mutated via `setLocalStorage` (typed) and via the localStorage
|
|
@@ -2138,19 +2253,20 @@ var Gleam = (() => {
|
|
|
2138
2253
|
// src/content/Animator.ts
|
|
2139
2254
|
var _Animator = class _Animator {
|
|
2140
2255
|
/**
|
|
2141
|
-
*
|
|
2142
|
-
* So if you have the same entity multiple times, the images get computed once and then shared.
|
|
2143
|
-
* This reduces overhead and frees time up for other important computations.
|
|
2144
|
-
*
|
|
2145
|
-
* `namespace` is the cache key prefix for these rendered images.
|
|
2146
|
-
* So pass a different key for different Animators; otherwise, you will see wrong images.
|
|
2256
|
+
* Bind to `entity` and stamp `namespace` as the prefix for cache keys (`${namespace}.${animationName}`). **Use distinct namespaces for animators whose sprite sets differ** — sharing a namespace across mismatched sprite sets serves the wrong cached frames.
|
|
2147
2257
|
*/
|
|
2148
2258
|
constructor(entity, namespace) {
|
|
2259
|
+
/** When `false`, {@link update} is a no-op. Set automatically to `false` after a single-frame animation finishes and via {@link reset} when no default animation exists. */
|
|
2149
2260
|
__publicField(this, "active", true);
|
|
2261
|
+
/** Current rendered frame (after flip processing). Pulled from the cache by {@link setImage} every time the frame advances. */
|
|
2150
2262
|
__publicField(this, "image");
|
|
2263
|
+
/** Index of the current frame within the current animation's `sprites` array. */
|
|
2151
2264
|
__publicField(this, "imageId", 0);
|
|
2265
|
+
/** Flip the rendered sprite horizontally. Caches a separate "flipped" bucket so toggling is cheap. */
|
|
2152
2266
|
__publicField(this, "lookLeft", false);
|
|
2267
|
+
/** One-shot callback that fires when the current animation's last frame finishes. Cleared after firing. */
|
|
2153
2268
|
__publicField(this, "onEnd");
|
|
2269
|
+
/** Current sprite's `(width, height)`. Updated by {@link setImage}. */
|
|
2154
2270
|
__publicField(this, "size", new Vec2());
|
|
2155
2271
|
__publicField(this, "animations", []);
|
|
2156
2272
|
__publicField(this, "currentAnimation", 0);
|
|
@@ -2178,9 +2294,11 @@ var Gleam = (() => {
|
|
|
2178
2294
|
}
|
|
2179
2295
|
});
|
|
2180
2296
|
}
|
|
2297
|
+
/** The currently-playing animation. `undefined` if no animations have been added yet. */
|
|
2181
2298
|
get current() {
|
|
2182
2299
|
return this.animations[this.currentAnimation];
|
|
2183
2300
|
}
|
|
2301
|
+
/** Blit the current cached frame at `entity.pos + offset`, shifted left by `size.x` to compensate for the 2×-wide cache canvas. */
|
|
2184
2302
|
draw(context, offset = new Vec2()) {
|
|
2185
2303
|
context.drawImage(
|
|
2186
2304
|
this.image,
|
|
@@ -2188,6 +2306,7 @@ var Gleam = (() => {
|
|
|
2188
2306
|
this.entity.pos.y + offset.y
|
|
2189
2307
|
);
|
|
2190
2308
|
}
|
|
2309
|
+
/** Advance the timer; when it crosses `current.timing`, step to the next frame, fire any `onFrame[index]` callback, and on rollover fire `onEnd` and queue `lastPlayed` (set by {@link playOnce}). No-op when {@link active} is `false`. */
|
|
2191
2310
|
update(dt) {
|
|
2192
2311
|
if (!this.active) {
|
|
2193
2312
|
return;
|
|
@@ -2231,6 +2350,7 @@ var Gleam = (() => {
|
|
|
2231
2350
|
this.setImage();
|
|
2232
2351
|
}
|
|
2233
2352
|
}
|
|
2353
|
+
/** Register a new animation. Logs an error (but still registers) if `defaultAnim` is true while another default already exists, or if `name` collides with an existing animation. Auto-plays the new animation when `defaultAnim` is `true`. */
|
|
2234
2354
|
add(name, sprites, timing, defaultAnim = false) {
|
|
2235
2355
|
if (defaultAnim && this.animations.some((anim) => anim.default)) {
|
|
2236
2356
|
console.error("Only one default animation allowed!");
|
|
@@ -2248,6 +2368,7 @@ var Gleam = (() => {
|
|
|
2248
2368
|
this.play(name);
|
|
2249
2369
|
}
|
|
2250
2370
|
}
|
|
2371
|
+
/** Convenience wrapper around {@link add} that takes a packed {@link SpriteAnimation}. `defaultAnim` is OR'd with `anim.default`. */
|
|
2251
2372
|
addAnimation(anim, defaultAnim = false) {
|
|
2252
2373
|
this.add(
|
|
2253
2374
|
anim.name,
|
|
@@ -2256,6 +2377,7 @@ var Gleam = (() => {
|
|
|
2256
2377
|
anim.default || defaultAnim
|
|
2257
2378
|
);
|
|
2258
2379
|
}
|
|
2380
|
+
/** Draw the current frame rotated by `angle` radians around a sprite-relative pivot (75% width, 50% height). Uses `setTransform` and resets the transform on exit. */
|
|
2259
2381
|
drawRotated(context, angle, offset = new Vec2()) {
|
|
2260
2382
|
const x = this.entity.pos.x + offset.x;
|
|
2261
2383
|
const y = this.entity.pos.y + offset.y;
|
|
@@ -2266,6 +2388,7 @@ var Gleam = (() => {
|
|
|
2266
2388
|
context.drawImage(this.image, -w, -h);
|
|
2267
2389
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
2268
2390
|
}
|
|
2391
|
+
/** Switch to the named animation, rewinding timer and frame index. Optionally register `onEnd` (fires after the last frame) and `onFrame` (frame-indexed callbacks). Any previous {@link Animator.onEnd} fires before the new one is set. Throws if `name` isn't registered. */
|
|
2269
2392
|
play(name, onEnd, onFrame) {
|
|
2270
2393
|
const index = this.animations.findIndex(
|
|
2271
2394
|
(anim) => anim.name === name
|
|
@@ -2288,6 +2411,7 @@ var Gleam = (() => {
|
|
|
2288
2411
|
}
|
|
2289
2412
|
this.active = true;
|
|
2290
2413
|
}
|
|
2414
|
+
/** {@link play} only if `name` isn't already the current animation. Returns `true` if it started a new playback, `false` if it was already playing. */
|
|
2291
2415
|
playIfNot(name, onEnd, onFrame) {
|
|
2292
2416
|
if (!this.isPlaying(name)) {
|
|
2293
2417
|
this.play(name, onEnd, onFrame);
|
|
@@ -2295,6 +2419,7 @@ var Gleam = (() => {
|
|
|
2295
2419
|
}
|
|
2296
2420
|
return false;
|
|
2297
2421
|
}
|
|
2422
|
+
/** Queue an animation to play once the current one finishes. Pass `undefined` to cancel the queue. Used internally by {@link playOnce}. */
|
|
2298
2423
|
playNextOnce(name) {
|
|
2299
2424
|
this.lastPlayed = name;
|
|
2300
2425
|
}
|
|
@@ -2305,9 +2430,11 @@ var Gleam = (() => {
|
|
|
2305
2430
|
this.lastPlayed = this.current?.name;
|
|
2306
2431
|
this.play(name, onEnd, onFrame);
|
|
2307
2432
|
}
|
|
2433
|
+
/** Randomize the frame timer to a value in `[0, current.timing)`. Useful when spawning many instances of the same animation to break phase lockstep. */
|
|
2308
2434
|
randomTimer() {
|
|
2309
2435
|
this.timer = randomBetweenFloat(0, this.current.timing);
|
|
2310
2436
|
}
|
|
2437
|
+
/** Drop every registered animation and clear this instance's cached frames. {@link active} resets to `true`; pending callbacks are cleared. */
|
|
2311
2438
|
removeAllAnimations() {
|
|
2312
2439
|
this.animations.length = 0;
|
|
2313
2440
|
this.active = true;
|
|
@@ -2315,6 +2442,7 @@ var Gleam = (() => {
|
|
|
2315
2442
|
this.onFrame = void 0;
|
|
2316
2443
|
_Animator.clearSpriteCache(this.namespace);
|
|
2317
2444
|
}
|
|
2445
|
+
/** Switch back to the default animation if one was registered (marked via `defaultAnim`); otherwise just stop animating ({@link active} = `false`). */
|
|
2318
2446
|
reset() {
|
|
2319
2447
|
const defaultAnim = this.animations.find(
|
|
2320
2448
|
(anim) => anim.default
|
|
@@ -2326,6 +2454,7 @@ var Gleam = (() => {
|
|
|
2326
2454
|
this.active = false;
|
|
2327
2455
|
}
|
|
2328
2456
|
}
|
|
2457
|
+
/** `true` when `name` matches the currently-playing animation. */
|
|
2329
2458
|
isPlaying(name) {
|
|
2330
2459
|
return this.current && this.current.name === name;
|
|
2331
2460
|
}
|
|
@@ -2364,15 +2493,63 @@ var Gleam = (() => {
|
|
|
2364
2493
|
__publicField(_Animator, "spriteCache", /* @__PURE__ */ new Map());
|
|
2365
2494
|
var Animator = _Animator;
|
|
2366
2495
|
|
|
2496
|
+
// src/content/ControllerCursor.ts
|
|
2497
|
+
var ControllerCursor = class {
|
|
2498
|
+
/**
|
|
2499
|
+
* @param controller Gamepad input source.
|
|
2500
|
+
* @param crosshair Drawn at each cursor position via `CanvasRenderingContext2D.drawImage`. The image's top-left is the draw origin — center the visible reticle inside the image, or offset the anchors to compensate.
|
|
2501
|
+
* @param sticks Anchor positions, one per stick to track. Cloned at construction so caller mutation is harmless.
|
|
2502
|
+
* @param range Max pixel deflection from anchor at full stick. Default `80`.
|
|
2503
|
+
*/
|
|
2504
|
+
constructor(controller, crosshair, sticks, range = 80) {
|
|
2505
|
+
__publicField(this, "controller");
|
|
2506
|
+
__publicField(this, "crosshair");
|
|
2507
|
+
__publicField(this, "range");
|
|
2508
|
+
__publicField(this, "sticks", []);
|
|
2509
|
+
this.controller = controller;
|
|
2510
|
+
this.crosshair = crosshair;
|
|
2511
|
+
this.range = range;
|
|
2512
|
+
sticks.forEach(
|
|
2513
|
+
(stick) => this.sticks.push({ pos: stick.clone(), offset: new Vec2() })
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
/** Draw a crosshair at `anchor + offset` for each tracked stick. */
|
|
2517
|
+
draw(context) {
|
|
2518
|
+
this.sticks.forEach((stick) => {
|
|
2519
|
+
context.drawImage(
|
|
2520
|
+
this.crosshair,
|
|
2521
|
+
stick.pos.x + stick.offset.x,
|
|
2522
|
+
stick.pos.y + stick.offset.y
|
|
2523
|
+
);
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
/** Pull fresh stick state via {@link Controller.poll} and smooth each crosshair's offset toward `stickAxis * range`. Frame-rate independent — 50 ms to cover half the remaining distance. */
|
|
2527
|
+
update(dt) {
|
|
2528
|
+
const alpha = 1 - Math.pow(0.5, dt / 0.05);
|
|
2529
|
+
this.controller.poll().forEach((axi, index) => {
|
|
2530
|
+
const offset = this.sticks[index].offset;
|
|
2531
|
+
offset.x += (axi.x * this.range - offset.x) * alpha;
|
|
2532
|
+
offset.y += (axi.y * this.range - offset.y) * alpha;
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
|
|
2367
2537
|
// src/content/Particle.ts
|
|
2368
2538
|
var Particle = class {
|
|
2369
2539
|
constructor(pos, color, size = 2) {
|
|
2540
|
+
/** CSS color string passed to `context.fillStyle` in {@link draw}. */
|
|
2370
2541
|
__publicField(this, "color");
|
|
2542
|
+
/** Accumulated time (seconds) since spawn or last {@link resetLifetime}. */
|
|
2371
2543
|
__publicField(this, "lifetime", 0);
|
|
2544
|
+
/** Lifetime cap in seconds, randomized to `[0.5, 1.5]` at construction. */
|
|
2372
2545
|
__publicField(this, "maxLifeTime");
|
|
2546
|
+
/** Top-left position. Cloned from the constructor arg so the caller's `Vec2` isn't aliased. */
|
|
2373
2547
|
__publicField(this, "pos");
|
|
2548
|
+
/** Circle radius (pixels). */
|
|
2374
2549
|
__publicField(this, "size");
|
|
2550
|
+
/** Velocity in px/s. Seeded randomly by the constructor (random angle, random magnitude per axis). */
|
|
2375
2551
|
__publicField(this, "vel");
|
|
2552
|
+
/** Backing storage for the {@link rect} getter. Subclasses can read it; the public-facing accessor is {@link rect}. */
|
|
2376
2553
|
__publicField(this, "_rect");
|
|
2377
2554
|
this.pos = pos.clone();
|
|
2378
2555
|
this.color = color;
|
|
@@ -2385,12 +2562,15 @@ var Gleam = (() => {
|
|
|
2385
2562
|
);
|
|
2386
2563
|
this.maxLifeTime = 0.5 + Math.random();
|
|
2387
2564
|
}
|
|
2565
|
+
/** `false` once {@link lifetime} reaches {@link maxLifeTime}. Owners typically filter dead particles out of their list each frame, or call {@link resetLifetime} to recycle. */
|
|
2388
2566
|
get alive() {
|
|
2389
2567
|
return this.lifetime < this.maxLifeTime;
|
|
2390
2568
|
}
|
|
2569
|
+
/** Read-only AABB tracking `pos` and the particle's `size`. Recomputed each {@link update}. */
|
|
2391
2570
|
get rect() {
|
|
2392
2571
|
return this._rect;
|
|
2393
2572
|
}
|
|
2573
|
+
/** Fill a circle at `pos + offset` using {@link color}. `offset` is useful for shifting by a camera/world transform without mutating `pos`. */
|
|
2394
2574
|
draw(context, offset = new Vec2()) {
|
|
2395
2575
|
context.fillStyle = this.color;
|
|
2396
2576
|
context.drawCircle(
|
|
@@ -2402,12 +2582,14 @@ var Gleam = (() => {
|
|
|
2402
2582
|
"fill"
|
|
2403
2583
|
);
|
|
2404
2584
|
}
|
|
2585
|
+
/** Integrate lifetime, position, and bounding rect. */
|
|
2405
2586
|
update(dt) {
|
|
2406
2587
|
this.lifetime += dt;
|
|
2407
2588
|
this.pos.x += this.vel.x * dt;
|
|
2408
2589
|
this.pos.y += this.vel.y * dt;
|
|
2409
2590
|
this._rect.set(this.pos.x, this.pos.y);
|
|
2410
2591
|
}
|
|
2592
|
+
/** Recycle a dead particle by subtracting `maxLifeTime` from `lifetime` — preserves any overshoot so a pool of pre-allocated particles can stay phase-stable across loops. Note this doesn't re-randomize `vel` or `pos`; mutate those externally if you want a fresh trajectory. */
|
|
2411
2593
|
resetLifetime() {
|
|
2412
2594
|
this.lifetime -= this.maxLifeTime;
|
|
2413
2595
|
}
|
|
@@ -2416,14 +2598,23 @@ var Gleam = (() => {
|
|
|
2416
2598
|
// src/content/Projectile.ts
|
|
2417
2599
|
var Projectile = class {
|
|
2418
2600
|
constructor(pos, image, vel = new Vec2()) {
|
|
2601
|
+
/** Seconds after which {@link alive} flips to `false`. Defaults to `Infinity` — no natural expiry. */
|
|
2419
2602
|
__publicField(this, "maxLifetime", Infinity);
|
|
2603
|
+
/** Caller-supplied data. Typed via the class generic so consumers can read `projectile.payload` without casting. */
|
|
2420
2604
|
__publicField(this, "payload");
|
|
2605
|
+
/** Magnitude multiplier applied to `vel` each update: `pos += vel * speed * dt`. Pass a unit-length `vel` to make this read as "pixels per second". */
|
|
2421
2606
|
__publicField(this, "speed", 1200);
|
|
2607
|
+
/** Current pre-rotated sprite. Re-baked by {@link rebuildRotation} from the un-rotated `originalImage`. */
|
|
2422
2608
|
__publicField(this, "image");
|
|
2609
|
+
/** Accumulated time (seconds). Drives the {@link alive} check against {@link maxLifetime}. */
|
|
2423
2610
|
__publicField(this, "lifetime", 0);
|
|
2611
|
+
/** Top-left position. Cloned from the constructor arg so the caller's `Vec2` isn't aliased. */
|
|
2424
2612
|
__publicField(this, "pos");
|
|
2613
|
+
/** Current sprite rotation in radians, kept in sync with `vel` by {@link rebuildRotation}. */
|
|
2425
2614
|
__publicField(this, "rotation", 0);
|
|
2615
|
+
/** Velocity direction vector. Multiplied by {@link speed} each update — pass a unit vector for `speed`-as-px-per-second semantics. Cloned from the constructor arg. */
|
|
2426
2616
|
__publicField(this, "vel");
|
|
2617
|
+
/** Backing storage for the {@link rect} getter. Subclasses can read it; mutate via `pos`/{@link rebuildRotation} instead of touching it directly. */
|
|
2427
2618
|
__publicField(this, "_rect");
|
|
2428
2619
|
__publicField(this, "originalImage");
|
|
2429
2620
|
this.pos = pos.clone();
|
|
@@ -2432,12 +2623,15 @@ var Gleam = (() => {
|
|
|
2432
2623
|
this._rect = pos.toRectAddSize(image.width, image.height);
|
|
2433
2624
|
this.rebuildRotation();
|
|
2434
2625
|
}
|
|
2626
|
+
/** `false` once {@link lifetime} reaches {@link maxLifetime}. Owners typically filter dead projectiles out of their list each frame. */
|
|
2435
2627
|
get alive() {
|
|
2436
2628
|
return this.lifetime < this.maxLifetime;
|
|
2437
2629
|
}
|
|
2630
|
+
/** Read-only AABB tracking `pos` and the (rotated) image size. Recomputed in {@link update} and {@link rebuildRotation}. */
|
|
2438
2631
|
get rect() {
|
|
2439
2632
|
return this._rect;
|
|
2440
2633
|
}
|
|
2634
|
+
/** Blit the pre-rotated image at `pos + offset`. `offset` is useful for shifting by a camera/world transform without mutating `pos`. */
|
|
2441
2635
|
draw(context, offset = new Vec2()) {
|
|
2442
2636
|
context.drawImage(
|
|
2443
2637
|
this.image,
|
|
@@ -2445,6 +2639,7 @@ var Gleam = (() => {
|
|
|
2445
2639
|
this.pos.y + offset.y
|
|
2446
2640
|
);
|
|
2447
2641
|
}
|
|
2642
|
+
/** Integrate motion and advance lifetime. Doesn't re-bake the rotation — call {@link rebuildRotation} after mutating `vel`. */
|
|
2448
2643
|
update(dt) {
|
|
2449
2644
|
this.lifetime += dt;
|
|
2450
2645
|
this.pos.x += this.vel.x * this.speed * dt;
|
|
@@ -2452,8 +2647,7 @@ var Gleam = (() => {
|
|
|
2452
2647
|
this._rect.set(this.pos.x, this.pos.y);
|
|
2453
2648
|
}
|
|
2454
2649
|
/**
|
|
2455
|
-
* Allocates a fresh rotated canvas
|
|
2456
|
-
* Caching by quantized rotation could be a feature when projectiles need to re-aim every tick (homing/seeking).
|
|
2650
|
+
* Re-bake the sprite to match the current `vel` direction (rotation = `atan2(vel.y, vel.x)`) and update `rect` to the new bounds. Allocates a fresh rotated canvas every call — no internal cache, so heavy re-aiming (homing/seeking) is a candidate for adding quantized caching.
|
|
2457
2651
|
*/
|
|
2458
2652
|
rebuildRotation() {
|
|
2459
2653
|
this.rotation = Math.atan2(this.vel.y, this.vel.x);
|
|
@@ -2461,6 +2655,7 @@ var Gleam = (() => {
|
|
|
2461
2655
|
this._rect.w = this.image.width;
|
|
2462
2656
|
this._rect.h = this.image.height;
|
|
2463
2657
|
}
|
|
2658
|
+
/** Force {@link alive} to `false` immediately (sets `lifetime` past `maxLifetime`). Use when the projectile should die on collision/impact, not from natural expiry. */
|
|
2464
2659
|
remove() {
|
|
2465
2660
|
this.lifetime = this.maxLifetime * 2;
|
|
2466
2661
|
}
|
|
@@ -2468,40 +2663,56 @@ var Gleam = (() => {
|
|
|
2468
2663
|
|
|
2469
2664
|
// src/core/CanvasManager.ts
|
|
2470
2665
|
var CANVAS_TYPES = {
|
|
2666
|
+
/** Catch-all tag for canvases without a specific role. */
|
|
2471
2667
|
ANY: /* @__PURE__ */ Symbol("any"),
|
|
2668
|
+
/** Generic placeholder tag — distinct from `ANY` so consumers can differentiate. */
|
|
2472
2669
|
DEFAULT: /* @__PURE__ */ Symbol("default"),
|
|
2670
|
+
/** Background canvas (drawn behind the main one). */
|
|
2473
2671
|
BACKGROUND: /* @__PURE__ */ Symbol("background"),
|
|
2672
|
+
/** Primary render target. Exactly one canvas must be registered with this type before {@link CanvasManager.finishSetup}. */
|
|
2474
2673
|
MAIN: /* @__PURE__ */ Symbol("main")
|
|
2475
2674
|
};
|
|
2476
2675
|
var CanvasManager = class {
|
|
2477
2676
|
constructor() {
|
|
2677
|
+
/** Cached `getBoundingClientRect()` of the main canvas. Refreshed in {@link resize}. Used to map pointer client coords into canvas space. */
|
|
2478
2678
|
__publicField(this, "canvasBoundingClientRect");
|
|
2679
|
+
/** Registry of every {@link setupCanvas}-registered canvas, keyed by selector. */
|
|
2479
2680
|
__publicField(this, "canvasHolder", {});
|
|
2681
|
+
/** Display-to-buffer scale factor after the last {@link resize} (`displayWidth / bufferWidth`). `1` until the first resize. */
|
|
2480
2682
|
__publicField(this, "ratio", 1);
|
|
2683
|
+
/** Display (CSS-pixel) size of the main canvas after the last {@link resize}. Independent of the buffer dimensions in {@link width}/{@link height}. */
|
|
2481
2684
|
__publicField(this, "resizedSize", new Vec2());
|
|
2482
2685
|
__publicField(this, "mainHolder");
|
|
2483
2686
|
}
|
|
2687
|
+
/** Main canvas element (the one registered with `CANVAS_TYPES.MAIN`). */
|
|
2484
2688
|
get canvas() {
|
|
2485
2689
|
return this.mainHolder.canvas;
|
|
2486
2690
|
}
|
|
2691
|
+
/** Main canvas 2D rendering context. */
|
|
2487
2692
|
get canvasContext() {
|
|
2488
2693
|
return this.mainHolder.context;
|
|
2489
2694
|
}
|
|
2695
|
+
/** Main canvas **buffer** height (the drawing surface, not the CSS display size). */
|
|
2490
2696
|
get height() {
|
|
2491
2697
|
return this.canvas.height;
|
|
2492
2698
|
}
|
|
2699
|
+
/** Main canvas **buffer** height (the drawing surface, not the CSS display size). */
|
|
2493
2700
|
set height(height) {
|
|
2494
2701
|
this.canvas.height = height;
|
|
2495
2702
|
}
|
|
2703
|
+
/** Main canvas buffer dimensions as a new `Vec2`. */
|
|
2496
2704
|
get size() {
|
|
2497
2705
|
return new Vec2(this.width, this.height);
|
|
2498
2706
|
}
|
|
2707
|
+
/** Main canvas **buffer** width (the drawing surface, not the CSS display size). */
|
|
2499
2708
|
get width() {
|
|
2500
2709
|
return this.canvas.width;
|
|
2501
2710
|
}
|
|
2711
|
+
/** Main canvas **buffer** width (the drawing surface, not the CSS display size). */
|
|
2502
2712
|
set width(width) {
|
|
2503
2713
|
this.canvas.width = width;
|
|
2504
2714
|
}
|
|
2715
|
+
/** Finalize the canvas registry. Called once by `Game.preInit()`. Validates that exactly one `CANVAS_TYPES.MAIN` canvas is registered and that its buffer is non-zero, caches its bounding rect, and wires the `"resized"` listener if `Settings.enableResize`. Throws on duplicate calls or invalid registry state. */
|
|
2505
2716
|
finishSetup() {
|
|
2506
2717
|
if (this.mainHolder) {
|
|
2507
2718
|
throw new Error("Already set up.");
|
|
@@ -2524,6 +2735,7 @@ var Gleam = (() => {
|
|
|
2524
2735
|
EventSystem.addEventListener("resized", () => this.resize());
|
|
2525
2736
|
}
|
|
2526
2737
|
}
|
|
2738
|
+
/** Rescale every opt-in canvas (`holder.resize === true`) to fit the window while preserving its buffer aspect ratio. Updates `style.width`/`style.height` only — buffer dimensions don't change. Refreshes {@link canvasBoundingClientRect}, {@link resizedSize}, and {@link ratio} from the main canvas. */
|
|
2527
2739
|
resize() {
|
|
2528
2740
|
const windowRatio = window.innerHeight / window.innerWidth;
|
|
2529
2741
|
Object.values(this.canvasHolder).forEach((ch) => {
|
|
@@ -2549,10 +2761,12 @@ var Gleam = (() => {
|
|
|
2549
2761
|
});
|
|
2550
2762
|
this.canvasBoundingClientRect = this.canvas.getBoundingClientRect();
|
|
2551
2763
|
}
|
|
2764
|
+
/** Set the main context's `font` to `${size}px "${font}"`. Defaults the family to `Settings.font`. */
|
|
2552
2765
|
setFontSize(size, font = Settings.font) {
|
|
2553
2766
|
this.canvasContext.font = `${size}px "${font}"`;
|
|
2554
2767
|
}
|
|
2555
|
-
|
|
2768
|
+
/** Register a canvas at `selector` with the given role tag. Initializes its context (`fillStyle`/`strokeStyle` = white, font = `12px Arial`) and returns the {@link CanvasHolder}. `resize` defaults to `Settings.enableResize`. Throws if the selector doesn't match an element or has already been registered. */
|
|
2769
|
+
setupCanvas(canvasType, selector, resize = Settings.enableResize) {
|
|
2556
2770
|
if (!document.querySelector(selector)) {
|
|
2557
2771
|
throw new Error("Canvas '" + selector + "' does not exist!");
|
|
2558
2772
|
}
|
|
@@ -2581,6 +2795,7 @@ var Gleam = (() => {
|
|
|
2581
2795
|
var MAX_STEPS_PER_FRAME = 5;
|
|
2582
2796
|
var Gameloop = class {
|
|
2583
2797
|
constructor(game) {
|
|
2798
|
+
/** Simulation time in milliseconds. Advances by `Settings.fps * 1000` per update step, so it reflects simulated time, not wall-clock — paused/dropped frames don't add. Use this for time-driven spawning, animations, etc. */
|
|
2584
2799
|
__publicField(this, "levelTime", 0);
|
|
2585
2800
|
__publicField(this, "_isLooping", false);
|
|
2586
2801
|
__publicField(this, "accumulator", 0);
|
|
@@ -2588,9 +2803,11 @@ var Gleam = (() => {
|
|
|
2588
2803
|
__publicField(this, "stop", false);
|
|
2589
2804
|
this.game = game;
|
|
2590
2805
|
}
|
|
2806
|
+
/** `true` while the rAF callback is registered. Goes `false` only after the final frame fires the `"gameloopStopped"` event. */
|
|
2591
2807
|
get isLooping() {
|
|
2592
2808
|
return this._isLooping;
|
|
2593
2809
|
}
|
|
2810
|
+
/** Begin the rAF loop. Throws if {@link stopLoop} was called but teardown hasn't completed yet — wait for the `"gameloopStopped"` event before restarting. */
|
|
2594
2811
|
startLoop() {
|
|
2595
2812
|
if (this._isLooping && this.stop) {
|
|
2596
2813
|
throw new Error(
|
|
@@ -2600,6 +2817,7 @@ var Gleam = (() => {
|
|
|
2600
2817
|
this.stop = false;
|
|
2601
2818
|
this.looper();
|
|
2602
2819
|
}
|
|
2820
|
+
/** Request that the loop stop on its next tick. Asynchronous — the loop tears down on the following frame and dispatches `"gameloopStopped"` when done. */
|
|
2603
2821
|
stopLoop() {
|
|
2604
2822
|
this.stop = true;
|
|
2605
2823
|
}
|
|
@@ -2657,53 +2875,98 @@ var Gleam = (() => {
|
|
|
2657
2875
|
|
|
2658
2876
|
// src/input/Keyboard.ts
|
|
2659
2877
|
var KEYBOARD_KEYS = {
|
|
2878
|
+
/** Digit row `0`. */
|
|
2660
2879
|
KEY_0: "Digit0",
|
|
2880
|
+
/** Digit row `1`. */
|
|
2661
2881
|
KEY_1: "Digit1",
|
|
2882
|
+
/** Digit row `2`. */
|
|
2662
2883
|
KEY_2: "Digit2",
|
|
2884
|
+
/** Digit row `3`. */
|
|
2663
2885
|
KEY_3: "Digit3",
|
|
2886
|
+
/** Digit row `4`. */
|
|
2664
2887
|
KEY_4: "Digit4",
|
|
2888
|
+
/** Digit row `5`. */
|
|
2665
2889
|
KEY_5: "Digit5",
|
|
2890
|
+
/** Digit row `6`. */
|
|
2666
2891
|
KEY_6: "Digit6",
|
|
2892
|
+
/** Digit row `7`. */
|
|
2667
2893
|
KEY_7: "Digit7",
|
|
2894
|
+
/** Digit row `8`. */
|
|
2668
2895
|
KEY_8: "Digit8",
|
|
2896
|
+
/** Digit row `9`. */
|
|
2669
2897
|
KEY_9: "Digit9",
|
|
2898
|
+
/** Letter `A`. */
|
|
2670
2899
|
KEY_A: "KeyA",
|
|
2900
|
+
/** Letter `B`. */
|
|
2671
2901
|
KEY_B: "KeyB",
|
|
2902
|
+
/** Letter `C`. */
|
|
2672
2903
|
KEY_C: "KeyC",
|
|
2904
|
+
/** Letter `D`. */
|
|
2673
2905
|
KEY_D: "KeyD",
|
|
2906
|
+
/** Down arrow. */
|
|
2674
2907
|
KEY_DOWN: "ArrowDown",
|
|
2908
|
+
/** Letter `E`. */
|
|
2675
2909
|
KEY_E: "KeyE",
|
|
2910
|
+
/** Enter / Return. */
|
|
2676
2911
|
KEY_ENTER: "Enter",
|
|
2912
|
+
/** Escape. Note: in `Settings.debug` mode this stops the gameloop. */
|
|
2677
2913
|
KEY_ESCAPE: "Escape",
|
|
2914
|
+
/** Letter `F`. */
|
|
2678
2915
|
KEY_F: "KeyF",
|
|
2916
|
+
/** Letter `G`. */
|
|
2679
2917
|
KEY_G: "KeyG",
|
|
2918
|
+
/** Letter `H`. */
|
|
2680
2919
|
KEY_H: "KeyH",
|
|
2920
|
+
/** Letter `I`. */
|
|
2681
2921
|
KEY_I: "KeyI",
|
|
2922
|
+
/** Letter `J`. */
|
|
2682
2923
|
KEY_J: "KeyJ",
|
|
2924
|
+
/** Letter `K`. */
|
|
2683
2925
|
KEY_K: "KeyK",
|
|
2926
|
+
/** Letter `L`. */
|
|
2684
2927
|
KEY_L: "KeyL",
|
|
2928
|
+
/** Left arrow. */
|
|
2685
2929
|
KEY_LEFT: "ArrowLeft",
|
|
2930
|
+
/** Letter `M`. */
|
|
2686
2931
|
KEY_M: "KeyM",
|
|
2932
|
+
/** Letter `N`. */
|
|
2687
2933
|
KEY_N: "KeyN",
|
|
2934
|
+
/** Letter `O`. */
|
|
2688
2935
|
KEY_O: "KeyO",
|
|
2936
|
+
/** Letter `P`. */
|
|
2689
2937
|
KEY_P: "KeyP",
|
|
2938
|
+
/** Letter `Q`. */
|
|
2690
2939
|
KEY_Q: "KeyQ",
|
|
2940
|
+
/** Letter `R`. */
|
|
2691
2941
|
KEY_R: "KeyR",
|
|
2942
|
+
/** Right arrow. */
|
|
2692
2943
|
KEY_RIGHT: "ArrowRight",
|
|
2944
|
+
/** Letter `S`. */
|
|
2693
2945
|
KEY_S: "KeyS",
|
|
2946
|
+
/** Space bar. */
|
|
2694
2947
|
KEY_SPACE: "Space",
|
|
2948
|
+
/** Letter `T`. */
|
|
2695
2949
|
KEY_T: "KeyT",
|
|
2950
|
+
/** Tab. */
|
|
2696
2951
|
KEY_TAB: "Tab",
|
|
2952
|
+
/** Letter `U`. */
|
|
2697
2953
|
KEY_U: "KeyU",
|
|
2954
|
+
/** Up arrow. */
|
|
2698
2955
|
KEY_UP: "ArrowUp",
|
|
2956
|
+
/** Letter `V`. */
|
|
2699
2957
|
KEY_V: "KeyV",
|
|
2958
|
+
/** Letter `W`. */
|
|
2700
2959
|
KEY_W: "KeyW",
|
|
2960
|
+
/** Letter `X`. */
|
|
2701
2961
|
KEY_X: "KeyX",
|
|
2962
|
+
/** Letter `Y`. */
|
|
2702
2963
|
KEY_Y: "KeyY",
|
|
2964
|
+
/** Letter `Z`. */
|
|
2703
2965
|
KEY_Z: "KeyZ"
|
|
2704
2966
|
};
|
|
2705
2967
|
var Keyboard = class {
|
|
2706
2968
|
constructor(game) {
|
|
2969
|
+
/** Live map of `KeyboardEvent.code` → pressed state. Codes only appear after the key has been touched at least once; missing codes read as `undefined` (use {@link isPressed} for a safe `boolean`). */
|
|
2707
2970
|
__publicField(this, "keys", {});
|
|
2708
2971
|
const keyEvent = (event) => {
|
|
2709
2972
|
const code = event.code;
|
|
@@ -2724,14 +2987,17 @@ var Gleam = (() => {
|
|
|
2724
2987
|
window.addEventListener("blur", () => this.reset(), false);
|
|
2725
2988
|
EventSystem.addEventListener("gameloopStopped", () => this.reset());
|
|
2726
2989
|
}
|
|
2990
|
+
/** Mark every tracked key as released. Called automatically on `window` blur and on `gameloopStopped`. */
|
|
2727
2991
|
reset() {
|
|
2728
2992
|
for (const key in this.keys) {
|
|
2729
2993
|
this.keys[key] = false;
|
|
2730
2994
|
}
|
|
2731
2995
|
}
|
|
2996
|
+
/** Mark a single key as released — used to consume a press so subsequent ticks don't re-trigger one-shot actions while the key is still held. */
|
|
2732
2997
|
stopPress(code) {
|
|
2733
2998
|
this.keys[code] = false;
|
|
2734
2999
|
}
|
|
3000
|
+
/** `true` when `code` is currently held. Safe for untouched keys (returns `false` rather than `undefined`). */
|
|
2735
3001
|
isPressed(code) {
|
|
2736
3002
|
return !!this.keys[code];
|
|
2737
3003
|
}
|
|
@@ -2739,20 +3005,32 @@ var Gleam = (() => {
|
|
|
2739
3005
|
|
|
2740
3006
|
// src/input/Pointer.ts
|
|
2741
3007
|
var POINTER_KEYS = {
|
|
3008
|
+
/** Primary button (left for right-handers). */
|
|
2742
3009
|
LEFT: 0,
|
|
3010
|
+
/** Middle button / wheel click. */
|
|
2743
3011
|
MIDDLE: 1,
|
|
3012
|
+
/** Secondary button (right for right-handers). */
|
|
2744
3013
|
RIGHT: 2,
|
|
3014
|
+
/** "Back" side button (browser back). */
|
|
2745
3015
|
PREV: 3,
|
|
3016
|
+
/** "Forward" side button. */
|
|
2746
3017
|
FORWARD: 4
|
|
2747
3018
|
};
|
|
2748
3019
|
var Pointer = class {
|
|
2749
3020
|
constructor(game) {
|
|
3021
|
+
/** Dirty bit set to `true` on every move and never cleared by the engine — flip it back to `false` after reading to detect "moved since last check". */
|
|
2750
3022
|
__publicField(this, "hasMoved", false);
|
|
3023
|
+
/** Last raw `PointerEvent` received. `null` until any pointer event fires. Use for properties not surfaced as Vec2/booleans (pressure, pointerType, etc.). */
|
|
2751
3024
|
__publicField(this, "lastEvent", null);
|
|
3025
|
+
/** Viewport-space coordinates (`event.clientX/Y` — CSS pixels relative to the page). */
|
|
2752
3026
|
__publicField(this, "posReal", new Vec2());
|
|
3027
|
+
/** Previous tick's {@link posReal}. Subtract for a per-frame delta. */
|
|
2753
3028
|
__publicField(this, "posRealLast", new Vec2());
|
|
3029
|
+
/** Canvas-space coordinates, mapped from the bounding rect into the main canvas's pixel buffer and clamped to its size. This is the position to use for in-game logic. */
|
|
2754
3030
|
__publicField(this, "posScaled", new Vec2());
|
|
3031
|
+
/** Previous tick's {@link posScaled}. */
|
|
2755
3032
|
__publicField(this, "posScaledLast", new Vec2());
|
|
3033
|
+
/** Per-button pressed state. Index with {@link POINTER_KEYS} (e.g. `pressed[POINTER_KEYS.LEFT]`). Sparse — unindexed entries are `undefined`, not `false`. */
|
|
2756
3034
|
__publicField(this, "pressed", []);
|
|
2757
3035
|
__publicField(this, "game");
|
|
2758
3036
|
this.game = game;
|
|
@@ -2785,6 +3063,7 @@ var Gleam = (() => {
|
|
|
2785
3063
|
false
|
|
2786
3064
|
);
|
|
2787
3065
|
}
|
|
3066
|
+
/** Clear all pressed-button state. Called automatically on `window` blur so held buttons don't stay "pressed" forever when focus is lost. */
|
|
2788
3067
|
reset() {
|
|
2789
3068
|
this.pressed.length = 0;
|
|
2790
3069
|
}
|
|
@@ -3125,9 +3404,13 @@ var Gleam = (() => {
|
|
|
3125
3404
|
// src/core/Game.ts
|
|
3126
3405
|
var Game = class {
|
|
3127
3406
|
constructor(settingOverrides = {}) {
|
|
3407
|
+
/** Canvas registry + 2D context exposure. Register canvases here from the constructor (`canman.setupCanvas(CANVAS_TYPES.MAIN, "#game")`) before calling {@link preInit}. */
|
|
3128
3408
|
__publicField(this, "canman", new CanvasManager());
|
|
3409
|
+
/** The fixed-step driver. Started automatically by `preInit` when `Settings.autoloop` is `true`. */
|
|
3129
3410
|
__publicField(this, "gameloop");
|
|
3411
|
+
/** Live keyboard state. See {@link Keyboard}. */
|
|
3130
3412
|
__publicField(this, "keyboard");
|
|
3413
|
+
/** Live pointer (mouse / pen / touch) state. See {@link Pointer}. */
|
|
3131
3414
|
__publicField(this, "pointer");
|
|
3132
3415
|
__publicField(this, "initialized", false);
|
|
3133
3416
|
Settings.init(settingOverrides, this);
|
|
@@ -3136,17 +3419,21 @@ var Gleam = (() => {
|
|
|
3136
3419
|
this.keyboard = new Keyboard(this);
|
|
3137
3420
|
this.pointer = new Pointer(this);
|
|
3138
3421
|
}
|
|
3422
|
+
/** Render the current frame. Called by {@link Gameloop} after the canvas is cleared. Subclasses must override — the default throws. */
|
|
3139
3423
|
draw(_context) {
|
|
3140
3424
|
throw new Error("Override draw function!");
|
|
3141
3425
|
}
|
|
3426
|
+
/** Advance the simulation by `dt` seconds (= `Settings.fps`). Called by {@link Gameloop} zero-or-more times per frame depending on real-time accumulation. Subclasses must override — the default throws. */
|
|
3142
3427
|
update(_dt) {
|
|
3143
3428
|
throw new Error("Override update function!");
|
|
3144
3429
|
}
|
|
3430
|
+
/** One-time setup hook (assets, world build) invoked by {@link preInit}. Can be `async`; the loop waits for it to resolve before starting. **Do not call directly** — kick off via {@link preInit} from the constructor. Subclasses must override — the default throws. */
|
|
3145
3431
|
async init() {
|
|
3146
3432
|
throw new Error(
|
|
3147
3433
|
"Override init() and start the game via preInit() \u2014 do not call init() directly."
|
|
3148
3434
|
);
|
|
3149
3435
|
}
|
|
3436
|
+
/** Finalise engine wiring and start the loop. Call once from the subclass constructor *after* registering canvases. Steps: `canman.finishSetup()` → install debounced `window.resize` → reset `gameloop.levelTime` → `await this.init()` (if `doInit`) → dispatch `"resized"` → start the loop if `Settings.autoloop`. Throws if called twice. Pass `doInit: false` to skip the `init()` await (useful for tests). */
|
|
3150
3437
|
async preInit(doInit = true) {
|
|
3151
3438
|
if (this.initialized) {
|
|
3152
3439
|
throw new Error(
|
|
@@ -3173,8 +3460,11 @@ var Gleam = (() => {
|
|
|
3173
3460
|
|
|
3174
3461
|
// src/effects/Screenshake.ts
|
|
3175
3462
|
var SHAKE_TYPES = {
|
|
3463
|
+
/** ~0.33 s wobble combining a small random rotation with a blur fall-off. */
|
|
3176
3464
|
NORMAL: {
|
|
3465
|
+
/** Decay rate — see {@link ShakeType.step}. */
|
|
3177
3466
|
step: 3,
|
|
3467
|
+
/** Per-frame mutator — see {@link ShakeType.update}. */
|
|
3178
3468
|
update(updateCss, time) {
|
|
3179
3469
|
const tr = `rotate(${randomBetweenFloat(-2, 2) * time}deg)`;
|
|
3180
3470
|
updateCss("transform", tr);
|
|
@@ -3182,8 +3472,11 @@ var Gleam = (() => {
|
|
|
3182
3472
|
updateCss("filter", `blur(${time * 5}px)`);
|
|
3183
3473
|
}
|
|
3184
3474
|
},
|
|
3475
|
+
/** ~0.07 s impact: blur-only fall-off, no rotation. */
|
|
3185
3476
|
FAST: {
|
|
3477
|
+
/** Decay rate — see {@link ShakeType.step}. */
|
|
3186
3478
|
step: 15,
|
|
3479
|
+
/** Per-frame mutator — see {@link ShakeType.update}. */
|
|
3187
3480
|
update(updateCss, time) {
|
|
3188
3481
|
updateCss("filter", `blur(${time * 3}px)`);
|
|
3189
3482
|
}
|
|
@@ -3196,7 +3489,7 @@ var Gleam = (() => {
|
|
|
3196
3489
|
__publicField(this, "style");
|
|
3197
3490
|
this.style = element.style;
|
|
3198
3491
|
}
|
|
3199
|
-
/**
|
|
3492
|
+
/** Start a shake of the given `shakeType`. Returns a dispose function that stops the shake early and restores every CSS key it touched, or `null` if a shake is already active on this instance. Auto-stops and restores when the timer reaches zero. */
|
|
3200
3493
|
shake(shakeType = SHAKE_TYPES.NORMAL) {
|
|
3201
3494
|
if (this.isShaking) {
|
|
3202
3495
|
return null;
|
|
@@ -3234,72 +3527,48 @@ var Gleam = (() => {
|
|
|
3234
3527
|
}
|
|
3235
3528
|
};
|
|
3236
3529
|
|
|
3237
|
-
// src/input/ControllerCursor.ts
|
|
3238
|
-
var SPEED = 600;
|
|
3239
|
-
var CROSSHAIR;
|
|
3240
|
-
(async () => CROSSHAIR = await loadImage(
|
|
3241
|
-
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAilBMVEUAAAAaGhoZGRkZGRkbGxsZGRkaGhoWFhYgICAZGRkZGRkZGRkZGRkZGRkZGRkYGBgZGRkZGRkbGxsaGhoZGRkZGRkZGRkZGRkZGRkaGhoZGRkZGRkaGhoZGRkZGRkZGRkYGBgZGRkZGRkbGxsZGRkYGBgZGRkYGBgaGhoYGBgYGBgYGBgZGRkaGhqj+rsBAAAALXRSTlMA9fvsBINBCwfhZTnS2sByMcUZE8oe1TUqubeppJOLhlDyeiPOrp57X19dSkdeSFeiAAAELElEQVR42sSY2XaqQBBFD20jozKJI3E2Tun//72bKgmXZBmjLbT7KQ9J2HSdrqoFtMiCw2I2SUKvI0THC5PJbHEIMhjBOc/7HXWVTn9+dtAmbtdOhLqJSOyui3YYvXXUXXTeRmicfN9TNaxtYUf+shtbVtxd+pFdbC1Vo7fPm335Xe3Z0/VyI1FiWSiRm+V6WrPYNXcMwbSq8KCqcCXwMyWDKiXTAE3Q7auSMEpB/C7ApFGoSvpdPEs6q5IVA/hbgImrvM5SPIOMVorxfBe4U4BxfU8xq0hCmyAsM+1L4BEBlvd7ZeV0oyBtoQjvJIEHBRh58hQhbKlV/Uv4xNwBNAQYZy4uYdRIwsclRkkMaAowcXKJ8AceQy4UYfmApkCFbyli8VAZ3EIRwzGeF8B4qIjCxd04E0W8O2hCAM67IiYO7iQb8PEfgQYEmCOXYZDhLvIexyZAcwIIONK9/K735+d7YzQpgLHHBtkdBRtw/HI0K4B8yFVw/sz/pKxWswJVsiYubiILfv8MzQsg4zMoJG6x4PrnaEMAOedgcbP/cv7HaEcAY74LN7pySr9gBWhLAIFFL5j+GgCef0e0J4Ajz8bfYmBz/0WbAuCubOMqgaAL4LQr4NBVEMHVAoQUgDHaFcCYYhBeK0KkPvHRtgB8ek505QbQ/pugfQEktCun+MmMahObEIgpazP8oKs+mcOEAOb0rC6+Qy3Ac8wIOB41A3wjIKkTzAjgRE8LUGdK24I0JSBp55mixuhyBY0IVFdxhP/sKAHSnICkFOxQkeseAAtoH0GOL/Y0JF2TAi4N/j2+oEy8waQA3ij13yIYmxWIOYY1mxBmBRDSqdfqEZkWiDh31RgQqWmBVNBAqDaxAUwLYFDtZgn9ZF7A/to/HD4L8wJceQfAmVZB17yAS8vhuVwPpjAvwBN4Xq4i61cIrMu1hLrA8hUCS+oEQKY+2bxCYENPzhBQBuUrBKTFi9lBKbXFKwSwVUod+JNE8RqBgj9XzKgPGheoeuEMEx6FuoyEGAFPDMQJTwIfeuR9/t6QQw+fp0Go3wbcoWKGrn4jCOHpj6K1WtmWZa/UWn8ceejo74OhiCmEsQj198IOBDUiPZSwLPpjof0PlBL/mrd3GwiBIAiiAgRYCOcMfOzNP70TpNBCr4kAaX8zPVX+B6IluJ4luKIlSDbhOpZzms5lrMEmTI7hfoz3O/bgGEYX0f17s/07uYjCq3ib5y27iv1jxJ9jXpDwkowXpbws940Jb814c8rbcx5Q+IiGh1Q8puNBpY9qeVjt43o+sOAjGz+04mM7P7jko1s/vObjew8wcITDQywc4/Egk0e5OMzmcT4PNHqkk0OtHuv1YLNHuz3c7vF+Lzh4xcNLLgWajxedvOpVILt53a9AeCxQPguk1wLtt0B8blC/C+T3L/T/P3gDDpik2UVvAAAAAElFTkSuQmCC"
|
|
3242
|
-
))();
|
|
3243
|
-
var ControllerCursor = class {
|
|
3244
|
-
constructor(controller, game, axisId) {
|
|
3245
|
-
__publicField(this, "axisId");
|
|
3246
|
-
__publicField(this, "controller");
|
|
3247
|
-
__publicField(this, "game");
|
|
3248
|
-
__publicField(this, "pos");
|
|
3249
|
-
this.controller = controller;
|
|
3250
|
-
this.game = game;
|
|
3251
|
-
this.axisId = axisId;
|
|
3252
|
-
this.pos = game.canman.size.mult(0.5);
|
|
3253
|
-
}
|
|
3254
|
-
get centerPos() {
|
|
3255
|
-
return this.pos.clone().add(CROSSHAIR.width * 0.5, CROSSHAIR.height * 0.5);
|
|
3256
|
-
}
|
|
3257
|
-
draw(context) {
|
|
3258
|
-
context.drawImage(CROSSHAIR, this.pos.x, this.pos.y);
|
|
3259
|
-
}
|
|
3260
|
-
update(dt) {
|
|
3261
|
-
const step = this.controller.stick(this.axisId).mult(SPEED * dt);
|
|
3262
|
-
if (step.length() === 0) {
|
|
3263
|
-
return;
|
|
3264
|
-
}
|
|
3265
|
-
this.pos.x = clamp(
|
|
3266
|
-
this.pos.x + step.x,
|
|
3267
|
-
0,
|
|
3268
|
-
this.game.canman.width - CROSSHAIR.width
|
|
3269
|
-
);
|
|
3270
|
-
this.pos.y = clamp(
|
|
3271
|
-
this.pos.y + step.y,
|
|
3272
|
-
0,
|
|
3273
|
-
this.game.canman.height - CROSSHAIR.height
|
|
3274
|
-
);
|
|
3275
|
-
}
|
|
3276
|
-
};
|
|
3277
|
-
|
|
3278
3530
|
// src/input/Controller.ts
|
|
3279
3531
|
var CONTROLLER_KEYS = {
|
|
3532
|
+
/** Bottom face button — `A` on Xbox, `×` on PlayStation. */
|
|
3280
3533
|
A: 0,
|
|
3534
|
+
/** Right face button — `B` on Xbox, `○` on PlayStation. */
|
|
3281
3535
|
B: 1,
|
|
3536
|
+
/** Left face button — `X` on Xbox, `□` on PlayStation. */
|
|
3282
3537
|
X: 2,
|
|
3538
|
+
/** Top face button — `Y` on Xbox, `△` on PlayStation. */
|
|
3283
3539
|
Y: 3,
|
|
3540
|
+
/** Left bumper / shoulder. */
|
|
3284
3541
|
LB: 4,
|
|
3542
|
+
/** Right bumper / shoulder. */
|
|
3285
3543
|
RB: 5,
|
|
3544
|
+
/** Left trigger. The digital pressed-state lives here; the analog value is on the underlying `Gamepad.buttons[6].value`. */
|
|
3286
3545
|
LT: 6,
|
|
3546
|
+
/** Right trigger. The digital pressed-state lives here; the analog value is on the underlying `Gamepad.buttons[7].value`. */
|
|
3287
3547
|
RT: 7,
|
|
3548
|
+
/** Back / Select / Share. */
|
|
3288
3549
|
SELECT: 8,
|
|
3550
|
+
/** Start / Options / Menu. */
|
|
3289
3551
|
START: 9,
|
|
3552
|
+
/** Left stick click (L3). */
|
|
3290
3553
|
LEFT_STICK: 10,
|
|
3554
|
+
/** Right stick click (R3). */
|
|
3291
3555
|
RIGHT_STICK: 11,
|
|
3556
|
+
/** D-pad up. */
|
|
3292
3557
|
UP: 12,
|
|
3558
|
+
/** D-pad down. */
|
|
3293
3559
|
DOWN: 13,
|
|
3560
|
+
/** D-pad left. */
|
|
3294
3561
|
LEFT: 14,
|
|
3562
|
+
/** D-pad right. */
|
|
3295
3563
|
RIGHT: 15,
|
|
3564
|
+
/** Guide / Home / PS button. Not exposed by all browsers. */
|
|
3296
3565
|
GUIDE: 16
|
|
3297
3566
|
};
|
|
3298
|
-
var
|
|
3567
|
+
var DEADZONE = 0.25;
|
|
3299
3568
|
var Controller = class {
|
|
3300
|
-
constructor(
|
|
3569
|
+
constructor() {
|
|
3570
|
+
/** Pressed-state per button, indexed in the same order as the underlying `Gamepad.buttons`. Index with {@link CONTROLLER_KEYS}. Updated by {@link poll}; empty until the first non-cached poll. */
|
|
3301
3571
|
__publicField(this, "buttons", []);
|
|
3302
|
-
__publicField(this, "cursors", []);
|
|
3303
3572
|
__publicField(this, "axes", []);
|
|
3304
3573
|
__publicField(this, "index", -1);
|
|
3305
3574
|
__publicField(this, "lastTime", 0);
|
|
@@ -3307,11 +3576,11 @@ var Gleam = (() => {
|
|
|
3307
3576
|
console.error("Controller not supported!");
|
|
3308
3577
|
return;
|
|
3309
3578
|
}
|
|
3310
|
-
for (let i = 0; i < 2; i++) {
|
|
3311
|
-
this.cursors.push(new ControllerCursor(this, game, i));
|
|
3312
|
-
}
|
|
3313
3579
|
window.addEventListener("gamepadconnected", (event) => {
|
|
3314
3580
|
this.index = event.gamepad.index;
|
|
3581
|
+
for (let i = 0; i < event.gamepad.axes.length / 2; i++) {
|
|
3582
|
+
this.axes.push(new Vec2());
|
|
3583
|
+
}
|
|
3315
3584
|
console.log("Gamepad connected:", event.gamepad.id);
|
|
3316
3585
|
EventSystem.dispatchEvent(
|
|
3317
3586
|
"inputControllerConnected",
|
|
@@ -3328,6 +3597,7 @@ var Gleam = (() => {
|
|
|
3328
3597
|
"Our Gamepad was disconnected:",
|
|
3329
3598
|
event.gamepad.index
|
|
3330
3599
|
);
|
|
3600
|
+
this.reset();
|
|
3331
3601
|
EventSystem.dispatchEvent("inputControllerDisconnected");
|
|
3332
3602
|
} else {
|
|
3333
3603
|
console.log(
|
|
@@ -3339,32 +3609,35 @@ var Gleam = (() => {
|
|
|
3339
3609
|
);
|
|
3340
3610
|
window.addEventListener("blur", () => this.reset(), false);
|
|
3341
3611
|
}
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
return;
|
|
3345
|
-
}
|
|
3346
|
-
this.cursors.forEach((cursor) => cursor.draw(context));
|
|
3347
|
-
}
|
|
3348
|
-
update(dt) {
|
|
3349
|
-
this.cursors.forEach((cursor) => cursor.update(dt));
|
|
3612
|
+
/** Read the current gamepad state and return one {@link Vec2} per stick pair with a circular deadzone applied (`0.25` inner radius, output magnitude clamped to `[0, 1]`). The returned array (and each `Vec2` in it) is reused across calls — clone if you need to retain. Also refreshes {@link buttons}. Returns the cached array unchanged when the gamepad timestamp hasn't advanced. */
|
|
3613
|
+
poll() {
|
|
3350
3614
|
const gp = this.getGamepad();
|
|
3351
3615
|
if (!gp || this.lastTime === gp.timestamp) {
|
|
3352
|
-
return;
|
|
3616
|
+
return this.axes;
|
|
3353
3617
|
}
|
|
3354
3618
|
this.lastTime = gp.timestamp;
|
|
3355
3619
|
this.buttons = gp.buttons.map(
|
|
3356
3620
|
(button) => button.pressed
|
|
3357
3621
|
);
|
|
3358
|
-
this.axes.length
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3622
|
+
for (let i = 0; i < this.axes.length; i++) {
|
|
3623
|
+
this.axes[i].set(gp.axes[i * 2], gp.axes[i * 2 + 1]);
|
|
3624
|
+
const mag = this.axes[i].length();
|
|
3625
|
+
if (mag < DEADZONE) {
|
|
3626
|
+
this.axes[i].set(0, 0);
|
|
3627
|
+
} else {
|
|
3628
|
+
this.axes[i].mult(
|
|
3629
|
+
(Math.min(1, mag) - DEADZONE) / (1 - DEADZONE) / mag
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3362
3632
|
}
|
|
3633
|
+
return this.axes;
|
|
3363
3634
|
}
|
|
3635
|
+
/** Clear {@link buttons} and the cached stick axes. Called automatically on `window` blur and on gamepad disconnect. */
|
|
3364
3636
|
reset() {
|
|
3365
3637
|
this.buttons.length = 0;
|
|
3366
3638
|
this.axes.length = 0;
|
|
3367
3639
|
}
|
|
3640
|
+
/** Trigger a 400 ms full-strength dual-rumble pulse. Returns `false` when no gamepad is connected or the pad has no `vibrationActuator`; `true` when the effect was dispatched. */
|
|
3368
3641
|
vibrate() {
|
|
3369
3642
|
const gp = this.getGamepad();
|
|
3370
3643
|
if (!gp) {
|
|
@@ -3381,14 +3654,6 @@ var Gleam = (() => {
|
|
|
3381
3654
|
}
|
|
3382
3655
|
return !!vibrator;
|
|
3383
3656
|
}
|
|
3384
|
-
stick(index) {
|
|
3385
|
-
if (this.index < 0 || index >= this.axes.length) {
|
|
3386
|
-
return new Vec2();
|
|
3387
|
-
}
|
|
3388
|
-
return this.axes[index].clone().map((value) => threshold(value, AXIS_THRESHOLD)).map(
|
|
3389
|
-
(value) => Math.sign(value) * map(Math.abs(value), AXIS_THRESHOLD, 1, 0, 1)
|
|
3390
|
-
);
|
|
3391
|
-
}
|
|
3392
3657
|
getGamepad() {
|
|
3393
3658
|
return this.index < 0 ? null : navigator.getGamepads()[this.index] ?? null;
|
|
3394
3659
|
}
|
|
@@ -3427,8 +3692,9 @@ var Gleam = (() => {
|
|
|
3427
3692
|
this.addPoints(...points);
|
|
3428
3693
|
}
|
|
3429
3694
|
/**
|
|
3430
|
-
* `angle` is the simplification threshold in radians: vertices whose turn
|
|
3431
|
-
*
|
|
3695
|
+
* Trace an outline around the opaque pixels of `canvas` via four directional sweeps (top, right, bottom, left). `detail` is the pixel stride (≥ 2) between scanline samples — higher = faster but coarser. `angle` is the simplification threshold in radians: vertices whose turn angle wraps to within ±`angle` of straight are dropped. Throws if fewer than 3 vertices survive.
|
|
3696
|
+
*
|
|
3697
|
+
* **Convex shapes only.** The sweep ignores anything an outer-perimeter ray can't reach: holes (donuts), inward bays (a "C" opening sideways), or any row/column with multiple disjoint opaque spans. For those inputs the result is either a broken polygon or simply the outer hull, and {@link Polygon.collide} relies on convexity anyway.
|
|
3432
3698
|
*/
|
|
3433
3699
|
static fromCanvas(canvas, detail, angle) {
|
|
3434
3700
|
detail = Math.max(2, detail);
|
|
@@ -3530,6 +3796,7 @@ var Gleam = (() => {
|
|
|
3530
3796
|
}
|
|
3531
3797
|
return new _Polygon(...points);
|
|
3532
3798
|
}
|
|
3799
|
+
/** Regular convex polygon with `edges` vertices, inscribed in a bounding box of `size` (number = square). */
|
|
3533
3800
|
static fromEdges(edges, size) {
|
|
3534
3801
|
const s = size instanceof Vec2 ? size : new Vec2(size, size);
|
|
3535
3802
|
const rad = Math.min(s.x, s.y) * 0.5;
|
|
@@ -3550,6 +3817,7 @@ var Gleam = (() => {
|
|
|
3550
3817
|
}
|
|
3551
3818
|
return new _Polygon(...points);
|
|
3552
3819
|
}
|
|
3820
|
+
/** Polygon from the four corners of `rect`. */
|
|
3553
3821
|
static fromRect(rect) {
|
|
3554
3822
|
return new _Polygon(
|
|
3555
3823
|
new Vec2(rect.x, rect.y),
|
|
@@ -3558,12 +3826,15 @@ var Gleam = (() => {
|
|
|
3558
3826
|
new Vec2(rect.x, rect.y + rect.h)
|
|
3559
3827
|
);
|
|
3560
3828
|
}
|
|
3829
|
+
/** Centroid (mean of vertex positions). Recomputed whenever the vertex set changes. */
|
|
3561
3830
|
get center() {
|
|
3562
3831
|
return this._center;
|
|
3563
3832
|
}
|
|
3833
|
+
/** Read-only view of the current vertex list. Use `addPoint`/`offset`/`rotate` to mutate. */
|
|
3564
3834
|
get points() {
|
|
3565
3835
|
return this._points;
|
|
3566
3836
|
}
|
|
3837
|
+
/** Stroke the polygon to `context`, shifted by `offset`. Coordinates are truncated to integers (via `| 0`) for crisp lines. */
|
|
3567
3838
|
draw(context, offset = new Vec2()) {
|
|
3568
3839
|
if (this._points.length === 0) {
|
|
3569
3840
|
return;
|
|
@@ -3582,21 +3853,25 @@ var Gleam = (() => {
|
|
|
3582
3853
|
context.closePath();
|
|
3583
3854
|
context.stroke();
|
|
3584
3855
|
}
|
|
3856
|
+
/** Append a single vertex at `(x, y)`. Mutates and returns `this`. */
|
|
3585
3857
|
addPoint(x, y) {
|
|
3586
3858
|
this._points.push(new Vec2(x, y));
|
|
3587
3859
|
this.update();
|
|
3588
3860
|
return this;
|
|
3589
3861
|
}
|
|
3862
|
+
/** Append cloned copies of every passed vertex. Mutates and returns `this`. */
|
|
3590
3863
|
addPoints(...points) {
|
|
3591
3864
|
points.forEach((point) => this._points.push(point.clone()));
|
|
3592
3865
|
this.update();
|
|
3593
3866
|
return this;
|
|
3594
3867
|
}
|
|
3868
|
+
/** Translate every vertex by `(x, y)`. Mutates and returns `this`. */
|
|
3595
3869
|
offset(x = 0, y = 0) {
|
|
3596
3870
|
this._points.forEach((point) => point.add(x, y));
|
|
3597
3871
|
this.update();
|
|
3598
3872
|
return this;
|
|
3599
3873
|
}
|
|
3874
|
+
/** Rotate by `angle` radians around `pos` (defaults to the centroid). Mutates and returns `this`. */
|
|
3600
3875
|
rotate(angle, pos = this.center) {
|
|
3601
3876
|
if (!angle) {
|
|
3602
3877
|
return this;
|
|
@@ -3611,6 +3886,7 @@ var Gleam = (() => {
|
|
|
3611
3886
|
this.update();
|
|
3612
3887
|
return this;
|
|
3613
3888
|
}
|
|
3889
|
+
/** SAT collision against `otherPolygon`. **Both polygons must be convex** — SAT silently misses collisions for concave shapes. Pass a non-zero `velocity` to also compute whether the polygons would intersect after that displacement. Warns and returns a no-collision result if either polygon has zero edges. */
|
|
3614
3890
|
collide(otherPolygon, velocity = new Vec2()) {
|
|
3615
3891
|
const result = {
|
|
3616
3892
|
intersect: true,
|
|
@@ -3678,6 +3954,7 @@ var Gleam = (() => {
|
|
|
3678
3954
|
}
|
|
3679
3955
|
return result;
|
|
3680
3956
|
}
|
|
3957
|
+
/** New `Polygon` with the same vertices. */
|
|
3681
3958
|
clone() {
|
|
3682
3959
|
return new _Polygon(...this._points);
|
|
3683
3960
|
}
|