@cosmoledo/gleam 1.0.0
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/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/gleam.d.ts +1195 -0
- package/dist/gleam.esm.js +3801 -0
- package/dist/gleam.esm.js.map +7 -0
- package/dist/gleam.js +3824 -0
- package/dist/gleam.js.map +7 -0
- package/dist/gleam.min.js +8 -0
- package/dist/gleam.min.js.map +7 -0
- package/package.json +71 -0
package/dist/gleam.js
ADDED
|
@@ -0,0 +1,3824 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var Gleam = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
22
|
+
|
|
23
|
+
// src/index.ts
|
|
24
|
+
var index_exports = {};
|
|
25
|
+
__export(index_exports, {
|
|
26
|
+
Animator: () => Animator,
|
|
27
|
+
AudioBase: () => AudioBase,
|
|
28
|
+
CANVAS_TYPES: () => CANVAS_TYPES,
|
|
29
|
+
CONTROLLER_KEYS: () => CONTROLLER_KEYS,
|
|
30
|
+
CanvasManager: () => CanvasManager,
|
|
31
|
+
Color: () => Color,
|
|
32
|
+
Controller: () => Controller,
|
|
33
|
+
ControllerCursor: () => ControllerCursor,
|
|
34
|
+
EASINGS: () => EASINGS,
|
|
35
|
+
EventSystem: () => EventSystem,
|
|
36
|
+
Game: () => Game,
|
|
37
|
+
Gameloop: () => Gameloop,
|
|
38
|
+
KEYBOARD_KEYS: () => KEYBOARD_KEYS,
|
|
39
|
+
Keyboard: () => Keyboard,
|
|
40
|
+
Music: () => Music,
|
|
41
|
+
POINTER_KEYS: () => POINTER_KEYS,
|
|
42
|
+
Particle: () => Particle,
|
|
43
|
+
Pointer: () => Pointer,
|
|
44
|
+
Polygon: () => Polygon,
|
|
45
|
+
Projectile: () => Projectile,
|
|
46
|
+
Rect: () => Rect,
|
|
47
|
+
SHAKE_TYPES: () => SHAKE_TYPES,
|
|
48
|
+
Screenshake: () => Screenshake,
|
|
49
|
+
Settings: () => Settings,
|
|
50
|
+
Sound: () => Sound,
|
|
51
|
+
Vec2: () => Vec2,
|
|
52
|
+
applyFilterOnCanvas: () => applyFilterOnCanvas,
|
|
53
|
+
approxEqual: () => approxEqual,
|
|
54
|
+
changeColor: () => changeColor,
|
|
55
|
+
chunk: () => chunk,
|
|
56
|
+
clamp: () => clamp,
|
|
57
|
+
cloneGrid: () => cloneGrid,
|
|
58
|
+
colorShifter: () => colorShifter,
|
|
59
|
+
compact: () => compact,
|
|
60
|
+
convert1DTo2D: () => convert1DTo2D,
|
|
61
|
+
convert2DTo1D: () => convert2DTo1D,
|
|
62
|
+
createNewCanvas: () => createNewCanvas,
|
|
63
|
+
debounce: () => debounce,
|
|
64
|
+
deepClone: () => deepClone,
|
|
65
|
+
defineMethod: () => defineMethod,
|
|
66
|
+
delay: () => delay,
|
|
67
|
+
doWhilePressed: () => doWhilePressed,
|
|
68
|
+
easeIn: () => easeIn,
|
|
69
|
+
easeInOut: () => easeInOut,
|
|
70
|
+
easeOut: () => easeOut,
|
|
71
|
+
generateGrid: () => generateGrid,
|
|
72
|
+
getCanvasConstruct: () => getCanvasConstruct,
|
|
73
|
+
getElement: () => getElement,
|
|
74
|
+
getFactorial: () => getFactorial,
|
|
75
|
+
getUsedColors: () => getUsedColors,
|
|
76
|
+
hex2rgb: () => hex2rgb,
|
|
77
|
+
hue2rgb: () => hue2rgb,
|
|
78
|
+
initCSSVariables: () => initCSSVariables,
|
|
79
|
+
isNumeric: () => isNumeric,
|
|
80
|
+
isTouchPrimary: () => isTouchPrimary,
|
|
81
|
+
linear: () => linear,
|
|
82
|
+
loadBunch: () => loadBunch,
|
|
83
|
+
loadCanvas: () => loadCanvas,
|
|
84
|
+
loadImage: () => loadImage,
|
|
85
|
+
loadImageFromJson: () => loadImageFromJson,
|
|
86
|
+
loadJson: () => loadJson,
|
|
87
|
+
loadJsonCommented: () => loadJsonCommented,
|
|
88
|
+
loadText: () => loadText,
|
|
89
|
+
map: () => map,
|
|
90
|
+
mapUnclamped: () => mapUnclamped,
|
|
91
|
+
prepareLanguage: () => prepareLanguage,
|
|
92
|
+
rafLoop: () => rafLoop,
|
|
93
|
+
random2Pi: () => random2Pi,
|
|
94
|
+
randomBetweenFloat: () => randomBetweenFloat,
|
|
95
|
+
randomBetweenInt: () => randomBetweenInt,
|
|
96
|
+
randomBoolean: () => randomBoolean,
|
|
97
|
+
randomHex: () => randomHex,
|
|
98
|
+
randomItem: () => randomItem,
|
|
99
|
+
randomRgb: () => randomRgb,
|
|
100
|
+
randomSign: () => randomSign,
|
|
101
|
+
remove: () => remove,
|
|
102
|
+
replaceCharAt: () => replaceCharAt,
|
|
103
|
+
rgb2Int: () => rgb2Int,
|
|
104
|
+
rgb2hex: () => rgb2hex,
|
|
105
|
+
rotateHue: () => rotateHue,
|
|
106
|
+
roundTo: () => roundTo,
|
|
107
|
+
safeLoad: () => safeLoad,
|
|
108
|
+
setDisplay: () => setDisplay,
|
|
109
|
+
setVisibility: () => setVisibility,
|
|
110
|
+
shuffle: () => shuffle,
|
|
111
|
+
splitSpriteSheet: () => splitSpriteSheet,
|
|
112
|
+
styleElement: () => styleElement,
|
|
113
|
+
threshold: () => threshold,
|
|
114
|
+
throttle: () => throttle,
|
|
115
|
+
throttleByKey: () => throttleByKey,
|
|
116
|
+
toDegrees: () => toDegrees,
|
|
117
|
+
toDotted: () => toDotted,
|
|
118
|
+
toHHMMSS: () => toHHMMSS,
|
|
119
|
+
toRadians: () => toRadians,
|
|
120
|
+
urlBasename: () => urlBasename,
|
|
121
|
+
validateUrl: () => validateUrl,
|
|
122
|
+
waitForEvent: () => waitForEvent,
|
|
123
|
+
wrapDegrees: () => wrapDegrees,
|
|
124
|
+
wrapRadians: () => wrapRadians,
|
|
125
|
+
wrapValue: () => wrapValue
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// src/utilities/Functions.ts
|
|
129
|
+
function debounce(callback, delay2) {
|
|
130
|
+
let timer;
|
|
131
|
+
return (...args) => {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
timer = setTimeout(() => callback(...args), delay2);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function delay(time) {
|
|
137
|
+
return new Promise((res) => setTimeout(res, time));
|
|
138
|
+
}
|
|
139
|
+
function isTouchPrimary() {
|
|
140
|
+
return matchMedia("(pointer: coarse)").matches;
|
|
141
|
+
}
|
|
142
|
+
function rafLoop(tick) {
|
|
143
|
+
let lastTime = 0;
|
|
144
|
+
let running = true;
|
|
145
|
+
let handle = 0;
|
|
146
|
+
const wrapped = (now) => {
|
|
147
|
+
const dt = lastTime === 0 ? 0 : (now - lastTime) / 1e3;
|
|
148
|
+
lastTime = now;
|
|
149
|
+
tick(dt);
|
|
150
|
+
if (running) {
|
|
151
|
+
handle = requestAnimationFrame(wrapped);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
handle = requestAnimationFrame(wrapped);
|
|
155
|
+
return () => {
|
|
156
|
+
running = false;
|
|
157
|
+
cancelAnimationFrame(handle);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function throttle(callback, delay2 = 1e3) {
|
|
161
|
+
let lastCalled = -Infinity;
|
|
162
|
+
let callCount = 0;
|
|
163
|
+
return () => {
|
|
164
|
+
callCount++;
|
|
165
|
+
const now = performance.now();
|
|
166
|
+
if (now - lastCalled > delay2) {
|
|
167
|
+
lastCalled = now;
|
|
168
|
+
const count = callCount;
|
|
169
|
+
callCount = 0;
|
|
170
|
+
callback(count);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function throttleByKey(callback, delay2 = 1e3) {
|
|
175
|
+
const wrappers = /* @__PURE__ */ new Map();
|
|
176
|
+
return (key, ...args) => {
|
|
177
|
+
let wrapper = wrappers.get(key);
|
|
178
|
+
if (!wrapper) {
|
|
179
|
+
let latest;
|
|
180
|
+
const throttled = throttle(
|
|
181
|
+
(count) => callback(count, ...latest),
|
|
182
|
+
delay2
|
|
183
|
+
);
|
|
184
|
+
wrapper = (...a) => {
|
|
185
|
+
latest = a;
|
|
186
|
+
throttled();
|
|
187
|
+
};
|
|
188
|
+
wrappers.set(key, wrapper);
|
|
189
|
+
}
|
|
190
|
+
wrapper(...args);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function urlBasename(path) {
|
|
194
|
+
const url = new URL(path, "http://_/");
|
|
195
|
+
const base = url.pathname.split("/").pop();
|
|
196
|
+
const dot = base.lastIndexOf(".");
|
|
197
|
+
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
198
|
+
if (!stem) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
return decodeURIComponent(stem);
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/core/EventSystem.ts
|
|
209
|
+
var _EventSystem = class _EventSystem {
|
|
210
|
+
static addEventListener(eventName, callback, options = {}) {
|
|
211
|
+
if (options.signal?.aborted) {
|
|
212
|
+
return function dispose2() {
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const id = ++this.nextId;
|
|
216
|
+
function dispose() {
|
|
217
|
+
const bucket2 = _EventSystem.eventListener[eventName];
|
|
218
|
+
if (!bucket2?.delete(id)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (bucket2.size === 0) {
|
|
222
|
+
delete _EventSystem.eventListener[eventName];
|
|
223
|
+
}
|
|
224
|
+
if (listener.options.signal) {
|
|
225
|
+
listener.options.signal.removeEventListener("abort", dispose);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const listener = {
|
|
229
|
+
callback,
|
|
230
|
+
dispose,
|
|
231
|
+
options: { once: options.once ?? false, signal: options.signal }
|
|
232
|
+
};
|
|
233
|
+
let bucket = this.eventListener[eventName];
|
|
234
|
+
if (!bucket) {
|
|
235
|
+
bucket = /* @__PURE__ */ new Map();
|
|
236
|
+
this.eventListener[eventName] = bucket;
|
|
237
|
+
}
|
|
238
|
+
bucket.set(id, listener);
|
|
239
|
+
if (options.signal) {
|
|
240
|
+
options.signal.addEventListener("abort", dispose, { once: true });
|
|
241
|
+
}
|
|
242
|
+
return dispose;
|
|
243
|
+
}
|
|
244
|
+
static dispatchEvent(eventName, ...params) {
|
|
245
|
+
const bucket = this.eventListener[eventName];
|
|
246
|
+
if (!bucket) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const maxId = this.nextId;
|
|
250
|
+
bucket.forEach((entry, id) => {
|
|
251
|
+
if (id > maxId) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (entry.options.once) {
|
|
255
|
+
entry.dispose();
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
entry.callback(...params);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
261
|
+
this.logListenerError(`${eventName}:${msg}`, eventName, err);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
__publicField(_EventSystem, "eventListener", {});
|
|
267
|
+
__publicField(_EventSystem, "logListenerError", throttleByKey(
|
|
268
|
+
(count, eventName, err) => {
|
|
269
|
+
const suffix = count > 1 ? ` (x${count} since last log)` : "";
|
|
270
|
+
console.error(
|
|
271
|
+
`EventSystem listener for "${eventName}" threw${suffix}:`,
|
|
272
|
+
err
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
));
|
|
276
|
+
// Monotonic counter — each listener gets an id assigned at registration.
|
|
277
|
+
// `dispatchEvent` captures the value at start as a boundary so listeners
|
|
278
|
+
// registered mid-dispatch (id > maxId) are deferred to the next dispatch.
|
|
279
|
+
__publicField(_EventSystem, "nextId", 0);
|
|
280
|
+
var EventSystem = _EventSystem;
|
|
281
|
+
|
|
282
|
+
// src/audio/AudioBase.ts
|
|
283
|
+
var MEDIA_ERROR_CODES = {
|
|
284
|
+
1: "ABORTED",
|
|
285
|
+
2: "NETWORK",
|
|
286
|
+
3: "DECODE",
|
|
287
|
+
4: "SRC_NOT_SUPPORTED"
|
|
288
|
+
};
|
|
289
|
+
var AudioBase = class {
|
|
290
|
+
constructor(enabled = true) {
|
|
291
|
+
__publicField(this, "songs", /* @__PURE__ */ new Map());
|
|
292
|
+
__publicField(this, "_enabled");
|
|
293
|
+
__publicField(this, "registered", false);
|
|
294
|
+
this._enabled = enabled;
|
|
295
|
+
EventSystem.addEventListener("gameloopStopped", () => this.stop());
|
|
296
|
+
}
|
|
297
|
+
get enabled() {
|
|
298
|
+
return this._enabled;
|
|
299
|
+
}
|
|
300
|
+
set enabled(value) {
|
|
301
|
+
this._enabled = value;
|
|
302
|
+
if (!value) {
|
|
303
|
+
this.stop();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
register(defaultVolume = 1, ...songs) {
|
|
307
|
+
this.throwOnBadVolume(defaultVolume, "defaultVolume");
|
|
308
|
+
if (this.registered) {
|
|
309
|
+
throw new Error("register() can only be called once per instance");
|
|
310
|
+
}
|
|
311
|
+
this.registered = true;
|
|
312
|
+
songs.forEach((song) => {
|
|
313
|
+
if (typeof song === "string") {
|
|
314
|
+
song = { name: urlBasename(song) ?? song, path: song };
|
|
315
|
+
} else if (song.volume !== void 0) {
|
|
316
|
+
this.throwOnBadVolume(song.volume, `Volume of "${song.name}"`);
|
|
317
|
+
}
|
|
318
|
+
const audio = new window.Audio();
|
|
319
|
+
audio.addEventListener("error", () => {
|
|
320
|
+
const err = audio.error;
|
|
321
|
+
const reason = err ? `${MEDIA_ERROR_CODES[err.code] ?? err.code}${err.message ? `: ${err.message}` : ""}` : "unknown";
|
|
322
|
+
console.error(
|
|
323
|
+
`Failed to load audio "${song.name}" from "${audio.src}": ${reason}`
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
audio.preload = "auto";
|
|
327
|
+
audio.src = song.path;
|
|
328
|
+
audio.id = song.name;
|
|
329
|
+
audio.volume = song.volume ?? defaultVolume;
|
|
330
|
+
audio.defaultVolume = audio.volume;
|
|
331
|
+
this.songs.set(song.name, audio);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
stop() {
|
|
335
|
+
}
|
|
336
|
+
throwOnBadVolume(volume, name) {
|
|
337
|
+
if (!Number.isFinite(volume)) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
name + " is invalid, it has to be in range of 0 to 1"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (volume < 0) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
name + " has to be above 0. What's a negative volume anyway?"
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (volume > 1) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
name + " has to be lower or equal to 1! If you need a louder volume, you need to update the audio file itself."
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/utilities/Number.ts
|
|
356
|
+
function approxEqual(a, b, epsilon = 1e-9) {
|
|
357
|
+
return Math.abs(a - b) <= epsilon;
|
|
358
|
+
}
|
|
359
|
+
function clamp(value, min, max) {
|
|
360
|
+
return Math.min(max, Math.max(min, value));
|
|
361
|
+
}
|
|
362
|
+
function mapUnclamped(value, low1, high1, low2, high2) {
|
|
363
|
+
if (low1 === high1) {
|
|
364
|
+
return low2;
|
|
365
|
+
}
|
|
366
|
+
return low2 + (high2 - low2) * (value - low1) / (high1 - low1);
|
|
367
|
+
}
|
|
368
|
+
function map(value, low1, high1, low2, high2) {
|
|
369
|
+
return clamp(
|
|
370
|
+
mapUnclamped(value, low1, high1, low2, high2),
|
|
371
|
+
Math.min(low2, high2),
|
|
372
|
+
Math.max(low2, high2)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
function threshold(value, cutoff) {
|
|
376
|
+
if (Math.abs(value) < cutoff) {
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
function toDotted(value) {
|
|
382
|
+
return Math.round(value).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
|
383
|
+
}
|
|
384
|
+
function wrapValue(value, min, max) {
|
|
385
|
+
if (approxEqual(min, max)) {
|
|
386
|
+
throw new RangeError(`wrapValue: min and max must differ (got ${min})`);
|
|
387
|
+
}
|
|
388
|
+
if (min > max) {
|
|
389
|
+
[min, max] = [max, min];
|
|
390
|
+
}
|
|
391
|
+
const range = max - min;
|
|
392
|
+
return ((value - min) % range + range) % range + min;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/utilities/Easing.ts
|
|
396
|
+
function easeIn(t) {
|
|
397
|
+
return t * t;
|
|
398
|
+
}
|
|
399
|
+
function easeInOut(t) {
|
|
400
|
+
return t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t);
|
|
401
|
+
}
|
|
402
|
+
function easeOut(t) {
|
|
403
|
+
return t * (2 - t);
|
|
404
|
+
}
|
|
405
|
+
function linear(t) {
|
|
406
|
+
return t;
|
|
407
|
+
}
|
|
408
|
+
var EASINGS = {
|
|
409
|
+
"ease-in": easeIn,
|
|
410
|
+
"ease-in-out": easeInOut,
|
|
411
|
+
"ease-out": easeOut,
|
|
412
|
+
"linear": linear
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// src/utilities/Array.ts
|
|
416
|
+
function chunk(array, maxLength) {
|
|
417
|
+
if (!Number.isFinite(maxLength) || maxLength < 1) {
|
|
418
|
+
throw new RangeError(
|
|
419
|
+
`chunk: maxLength must be a finite number >= 1 (got ${maxLength})`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const result = [];
|
|
423
|
+
let part = [];
|
|
424
|
+
array.forEach((item, i) => {
|
|
425
|
+
part.push(item);
|
|
426
|
+
if (part.length === maxLength || i === array.length - 1) {
|
|
427
|
+
result.push(part);
|
|
428
|
+
part = [];
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
function randomItem(array) {
|
|
434
|
+
if (array.length === 0) {
|
|
435
|
+
throw new Error("randomItem called on empty array");
|
|
436
|
+
}
|
|
437
|
+
return array[Math.random() * array.length | 0];
|
|
438
|
+
}
|
|
439
|
+
function remove(arr, item) {
|
|
440
|
+
const index = arr.indexOf(item);
|
|
441
|
+
if (index >= 0) {
|
|
442
|
+
arr.splice(index, 1);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function shuffle(arr) {
|
|
446
|
+
const result = arr.slice();
|
|
447
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
448
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
449
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
450
|
+
}
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/audio/Music.ts
|
|
455
|
+
var Music = class extends AudioBase {
|
|
456
|
+
constructor() {
|
|
457
|
+
super(...arguments);
|
|
458
|
+
__publicField(this, "last", null);
|
|
459
|
+
__publicField(this, "current", null);
|
|
460
|
+
__publicField(this, "next", null);
|
|
461
|
+
__publicField(this, "fadeCancel", null);
|
|
462
|
+
}
|
|
463
|
+
get isPlaying() {
|
|
464
|
+
return !!this.fadeCancel || this.current instanceof window.Audio && !this.current.paused;
|
|
465
|
+
}
|
|
466
|
+
get enabled() {
|
|
467
|
+
return super.enabled;
|
|
468
|
+
}
|
|
469
|
+
set enabled(value) {
|
|
470
|
+
super.enabled = value;
|
|
471
|
+
if (value && !this.isPlaying) {
|
|
472
|
+
this.fade();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
fade(name = null, fadeTime = 1e3, easing = {
|
|
476
|
+
cur: "ease-in",
|
|
477
|
+
next: "ease-out"
|
|
478
|
+
}) {
|
|
479
|
+
if (fadeTime <= 0) {
|
|
480
|
+
throw new Error(`fadeTime must be > 0, got ${fadeTime}`);
|
|
481
|
+
}
|
|
482
|
+
if (this.songs.size === 0) {
|
|
483
|
+
throw new Error("No music registered!");
|
|
484
|
+
}
|
|
485
|
+
if (name && !this.songs.has(name)) {
|
|
486
|
+
throw new Error(`Music "${name}" not registered!`);
|
|
487
|
+
}
|
|
488
|
+
if (!this.enabled) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (this.fadeCancel) {
|
|
492
|
+
console.warn("Stopping current music fading.");
|
|
493
|
+
this.fadeCancel();
|
|
494
|
+
this.fadeCancel = null;
|
|
495
|
+
this.current?.stop();
|
|
496
|
+
this.current = this.next;
|
|
497
|
+
this.next = null;
|
|
498
|
+
}
|
|
499
|
+
if (this.songs.size === 1) {
|
|
500
|
+
console.info("Only one music registered, playing that looped.");
|
|
501
|
+
this.current = this.songs.values().next().value;
|
|
502
|
+
this.current.loop = true;
|
|
503
|
+
this.current.play();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (name) {
|
|
507
|
+
this.next = this.songs.get(name);
|
|
508
|
+
} else {
|
|
509
|
+
this.next = this.getRandom();
|
|
510
|
+
if (!this.next) {
|
|
511
|
+
console.warn(
|
|
512
|
+
"Not enough songs to pick a fresh one, replaying the previous one."
|
|
513
|
+
);
|
|
514
|
+
this.next = this.last;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
this.next.onended = () => {
|
|
518
|
+
this.fade();
|
|
519
|
+
};
|
|
520
|
+
this.next.volume = 0;
|
|
521
|
+
this.next.play();
|
|
522
|
+
console.log(`Start fading to music: "${this.next.id}"`);
|
|
523
|
+
if (this.current) {
|
|
524
|
+
this.current.onended = () => void 0;
|
|
525
|
+
}
|
|
526
|
+
const curEase = EASINGS[easing.cur];
|
|
527
|
+
const nextEase = EASINGS[easing.next];
|
|
528
|
+
const fadeTimeSeconds = fadeTime / 1e3;
|
|
529
|
+
let time = 0;
|
|
530
|
+
const curStartVolume = this.current?.volume ?? 0;
|
|
531
|
+
this.fadeCancel = rafLoop((dt) => {
|
|
532
|
+
time += dt / fadeTimeSeconds;
|
|
533
|
+
if (this.current) {
|
|
534
|
+
this.current.volume = curStartVolume * (1 - clamp(curEase(time), 0, 1));
|
|
535
|
+
}
|
|
536
|
+
this.next.volume = this.next.defaultVolume * clamp(nextEase(time), 0, 1);
|
|
537
|
+
if (time >= 1) {
|
|
538
|
+
this.fadeCancel?.();
|
|
539
|
+
this.fadeCancel = null;
|
|
540
|
+
if (this.current) {
|
|
541
|
+
this.current.stop();
|
|
542
|
+
this.current.volume = this.current.defaultVolume;
|
|
543
|
+
}
|
|
544
|
+
this.next.volume = this.next.defaultVolume;
|
|
545
|
+
this.last = this.current;
|
|
546
|
+
this.current = this.next;
|
|
547
|
+
this.next = null;
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
stop() {
|
|
552
|
+
super.stop();
|
|
553
|
+
this.fadeCancel?.();
|
|
554
|
+
this.fadeCancel = null;
|
|
555
|
+
this.current?.stop();
|
|
556
|
+
this.next?.stop();
|
|
557
|
+
this.last = this.current;
|
|
558
|
+
this.current = this.next;
|
|
559
|
+
this.next = null;
|
|
560
|
+
}
|
|
561
|
+
getRandom() {
|
|
562
|
+
const allSongs = Array.from(this.songs.values());
|
|
563
|
+
if (this.last) {
|
|
564
|
+
remove(allSongs, this.last);
|
|
565
|
+
}
|
|
566
|
+
if (this.current) {
|
|
567
|
+
remove(allSongs, this.current);
|
|
568
|
+
}
|
|
569
|
+
if (allSongs.length === 0) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
return randomItem(allSongs);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/audio/Sound.ts
|
|
577
|
+
var Sound = class extends AudioBase {
|
|
578
|
+
constructor() {
|
|
579
|
+
super(...arguments);
|
|
580
|
+
__publicField(this, "currentSounds", []);
|
|
581
|
+
}
|
|
582
|
+
play(name) {
|
|
583
|
+
if (this.songs.size === 0) {
|
|
584
|
+
throw new Error("No sounds registered!");
|
|
585
|
+
}
|
|
586
|
+
if (!this.songs.has(name)) {
|
|
587
|
+
throw new Error(`Sound "${name}" not registered!`);
|
|
588
|
+
}
|
|
589
|
+
if (!this.enabled) {
|
|
590
|
+
return Promise.resolve();
|
|
591
|
+
}
|
|
592
|
+
const newSound = this.songs.get(name).clone();
|
|
593
|
+
this.currentSounds.push(newSound);
|
|
594
|
+
const cleanup = () => remove(this.currentSounds, newSound);
|
|
595
|
+
newSound.onended = cleanup;
|
|
596
|
+
newSound.onerror = () => {
|
|
597
|
+
cleanup();
|
|
598
|
+
console.error(
|
|
599
|
+
`Playback failed for Sound "${name}", reason: ${newSound.error?.message ?? "unknown"}`
|
|
600
|
+
);
|
|
601
|
+
};
|
|
602
|
+
return newSound.play().catch((err) => {
|
|
603
|
+
cleanup();
|
|
604
|
+
throw err;
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
stop() {
|
|
608
|
+
super.stop();
|
|
609
|
+
this.currentSounds.forEach((audio) => audio.stop());
|
|
610
|
+
this.currentSounds.length = 0;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// src/utilities/Math.ts
|
|
615
|
+
var NUMERIC_PATTERN = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
|
|
616
|
+
function isNumeric(value) {
|
|
617
|
+
if (typeof value === "number") {
|
|
618
|
+
return Number.isFinite(value);
|
|
619
|
+
}
|
|
620
|
+
if (typeof value === "string") {
|
|
621
|
+
return NUMERIC_PATTERN.test(value.trim());
|
|
622
|
+
}
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
function random2Pi() {
|
|
626
|
+
return Math.random() * Math.PI * 2;
|
|
627
|
+
}
|
|
628
|
+
function randomBetweenFloat(min, max) {
|
|
629
|
+
if (min > max) {
|
|
630
|
+
[min, max] = [max, min];
|
|
631
|
+
}
|
|
632
|
+
return min + Math.random() * (max - min);
|
|
633
|
+
}
|
|
634
|
+
function randomBetweenInt(min, max) {
|
|
635
|
+
if (min > max) {
|
|
636
|
+
[min, max] = [max, min];
|
|
637
|
+
}
|
|
638
|
+
return Math.floor(min + Math.random() * (max - min + 1));
|
|
639
|
+
}
|
|
640
|
+
function randomBoolean() {
|
|
641
|
+
return Math.random() >= 0.5;
|
|
642
|
+
}
|
|
643
|
+
function randomSign() {
|
|
644
|
+
return randomBoolean() ? 1 : -1;
|
|
645
|
+
}
|
|
646
|
+
var warnInvalidTime = throttle((count) => {
|
|
647
|
+
console.warn(
|
|
648
|
+
`toHHMMSS() received invalid input (NaN, Infinity, or negative) x${count} since last warning; returning "00:00".`
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
function toHHMMSS(time) {
|
|
652
|
+
if (!Number.isFinite(time) || time < 0) {
|
|
653
|
+
warnInvalidTime();
|
|
654
|
+
return "00:00";
|
|
655
|
+
}
|
|
656
|
+
time = Math.floor(time);
|
|
657
|
+
const h = Math.floor(time / 3600);
|
|
658
|
+
const m = Math.floor((time - h * 3600) / 60);
|
|
659
|
+
const s = time - h * 3600 - m * 60;
|
|
660
|
+
const hours = h < 10 ? "0" + h : "" + h;
|
|
661
|
+
const minutes = m < 10 ? "0" + m : "" + m;
|
|
662
|
+
const seconds = s < 10 ? "0" + s : "" + s;
|
|
663
|
+
if (h === 0) {
|
|
664
|
+
return `${minutes}:${seconds}`;
|
|
665
|
+
}
|
|
666
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
667
|
+
}
|
|
668
|
+
function toDegrees(radians) {
|
|
669
|
+
return radians * 180 / Math.PI;
|
|
670
|
+
}
|
|
671
|
+
function toRadians(degrees) {
|
|
672
|
+
return degrees * Math.PI / 180;
|
|
673
|
+
}
|
|
674
|
+
function wrapRadians(angle) {
|
|
675
|
+
return wrapValue(angle, -Math.PI, Math.PI);
|
|
676
|
+
}
|
|
677
|
+
function wrapDegrees(angle) {
|
|
678
|
+
return wrapValue(angle, -180, 180);
|
|
679
|
+
}
|
|
680
|
+
function roundTo(number, digitsAfterPoint) {
|
|
681
|
+
const power = Math.pow(10, digitsAfterPoint);
|
|
682
|
+
return Math.round(number * power) / power;
|
|
683
|
+
}
|
|
684
|
+
var factorialsCache = { 0: 1 };
|
|
685
|
+
var largestCachedFactorial = 0;
|
|
686
|
+
var factorialOverflowAt = Infinity;
|
|
687
|
+
function getFactorial(n) {
|
|
688
|
+
const intN = Math.floor(n);
|
|
689
|
+
if (intN < 0 || !Number.isFinite(intN)) {
|
|
690
|
+
throw new Error("getFactorial requires non-negative integer");
|
|
691
|
+
}
|
|
692
|
+
if (intN >= factorialOverflowAt) {
|
|
693
|
+
return Infinity;
|
|
694
|
+
}
|
|
695
|
+
if (factorialsCache[intN] !== void 0) {
|
|
696
|
+
return factorialsCache[intN];
|
|
697
|
+
}
|
|
698
|
+
let result = factorialsCache[largestCachedFactorial];
|
|
699
|
+
for (let i = largestCachedFactorial + 1; i <= intN; i++) {
|
|
700
|
+
result *= i;
|
|
701
|
+
if (!Number.isFinite(result)) {
|
|
702
|
+
factorialOverflowAt = i;
|
|
703
|
+
return Infinity;
|
|
704
|
+
}
|
|
705
|
+
factorialsCache[i] = result;
|
|
706
|
+
largestCachedFactorial = i;
|
|
707
|
+
}
|
|
708
|
+
return result;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/utilities/Color.ts
|
|
712
|
+
function rgb2hex(red, green, blue) {
|
|
713
|
+
return "#" + (16777216 + rgb2Int(red, green, blue)).toString(16).slice(1);
|
|
714
|
+
}
|
|
715
|
+
function rgb2Int(red, green, blue, alpha) {
|
|
716
|
+
if (alpha === void 0) {
|
|
717
|
+
return red << 16 | green << 8 | blue;
|
|
718
|
+
}
|
|
719
|
+
const a = Math.round(alpha * 255);
|
|
720
|
+
return (red << 24 | green << 16 | blue << 8 | a) >>> 0;
|
|
721
|
+
}
|
|
722
|
+
function hex2rgb(hex) {
|
|
723
|
+
return hex.replace(
|
|
724
|
+
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
|
|
725
|
+
(_, r, g, b) => "#" + r + r + g + g + b + b
|
|
726
|
+
).substring(1).match(/.{2}/g).map((x) => parseInt(x, 16));
|
|
727
|
+
}
|
|
728
|
+
function hue2rgb(p, q, t) {
|
|
729
|
+
if (t < 0) {
|
|
730
|
+
t += 1;
|
|
731
|
+
}
|
|
732
|
+
if (t > 1) {
|
|
733
|
+
t -= 1;
|
|
734
|
+
}
|
|
735
|
+
if (t < 1 / 6) {
|
|
736
|
+
return p + (q - p) * 6 * t;
|
|
737
|
+
}
|
|
738
|
+
if (t < 1 / 2) {
|
|
739
|
+
return q;
|
|
740
|
+
}
|
|
741
|
+
if (t < 2 / 3) {
|
|
742
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
743
|
+
}
|
|
744
|
+
return p;
|
|
745
|
+
}
|
|
746
|
+
function randomHex() {
|
|
747
|
+
return "#" + Math.random().toString(16).slice(2, 6);
|
|
748
|
+
}
|
|
749
|
+
function randomRgb(min = 0, max = 255) {
|
|
750
|
+
return [
|
|
751
|
+
randomBetweenInt(min, max),
|
|
752
|
+
randomBetweenInt(min, max),
|
|
753
|
+
randomBetweenInt(min, max)
|
|
754
|
+
];
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/color/Color.ts
|
|
758
|
+
var Color = class _Color {
|
|
759
|
+
constructor(r, g, b, a) {
|
|
760
|
+
__publicField(this, "_r");
|
|
761
|
+
__publicField(this, "_g");
|
|
762
|
+
__publicField(this, "_b");
|
|
763
|
+
__publicField(this, "_alpha", 1);
|
|
764
|
+
this.set(r, g, b, a);
|
|
765
|
+
}
|
|
766
|
+
static fromHex(hex) {
|
|
767
|
+
let cleanHex = hex.replace("#", "").toUpperCase();
|
|
768
|
+
if (!/^[0-9A-F]+$/.test(cleanHex)) {
|
|
769
|
+
throw new Error(`Invalid hex color: ${hex}`);
|
|
770
|
+
}
|
|
771
|
+
if (cleanHex.length === 3 || cleanHex.length === 4) {
|
|
772
|
+
cleanHex = cleanHex.split("").map((c) => c + c).join("");
|
|
773
|
+
}
|
|
774
|
+
if (cleanHex.length !== 6 && cleanHex.length !== 8) {
|
|
775
|
+
throw new Error(`Invalid hex color: ${hex}`);
|
|
776
|
+
}
|
|
777
|
+
const r = parseInt(cleanHex.slice(0, 2), 16);
|
|
778
|
+
const g = parseInt(cleanHex.slice(2, 4), 16);
|
|
779
|
+
const b = parseInt(cleanHex.slice(4, 6), 16);
|
|
780
|
+
if (cleanHex.length === 8) {
|
|
781
|
+
const a = parseInt(cleanHex.slice(6, 8), 16);
|
|
782
|
+
return new _Color(r, g, b, a / 255);
|
|
783
|
+
}
|
|
784
|
+
return new _Color(r, g, b);
|
|
785
|
+
}
|
|
786
|
+
static fromHSL(h, s, l, a = 1) {
|
|
787
|
+
const hNorm = wrapValue(h, 0, 360) / 360;
|
|
788
|
+
const sNorm = s / 100;
|
|
789
|
+
const lNorm = l / 100;
|
|
790
|
+
let r, g, b;
|
|
791
|
+
if (sNorm === 0) {
|
|
792
|
+
r = g = b = lNorm;
|
|
793
|
+
} else {
|
|
794
|
+
const q = lNorm < 0.5 ? lNorm * (1 + sNorm) : lNorm + sNorm - lNorm * sNorm;
|
|
795
|
+
const p = 2 * lNorm - q;
|
|
796
|
+
r = hue2rgb(p, q, hNorm + 1 / 3);
|
|
797
|
+
g = hue2rgb(p, q, hNorm);
|
|
798
|
+
b = hue2rgb(p, q, hNorm - 1 / 3);
|
|
799
|
+
}
|
|
800
|
+
return new _Color(r * 255, g * 255, b * 255, a);
|
|
801
|
+
}
|
|
802
|
+
get r() {
|
|
803
|
+
return this._r;
|
|
804
|
+
}
|
|
805
|
+
get g() {
|
|
806
|
+
return this._g;
|
|
807
|
+
}
|
|
808
|
+
get b() {
|
|
809
|
+
return this._b;
|
|
810
|
+
}
|
|
811
|
+
get alpha() {
|
|
812
|
+
return this._alpha;
|
|
813
|
+
}
|
|
814
|
+
set(r, g, b, a) {
|
|
815
|
+
this._r = clamp(r, 0, 255);
|
|
816
|
+
this._g = clamp(g, 0, 255);
|
|
817
|
+
this._b = clamp(b, 0, 255);
|
|
818
|
+
if (a !== void 0) {
|
|
819
|
+
const clamped = clamp(a, 0, 1);
|
|
820
|
+
this._alpha = approxEqual(clamped, 0) ? 0 : approxEqual(clamped, 1) ? 1 : clamped;
|
|
821
|
+
}
|
|
822
|
+
return this;
|
|
823
|
+
}
|
|
824
|
+
applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9) {
|
|
825
|
+
return this.set(
|
|
826
|
+
this.r * m1 + this.g * m2 + this.b * m3,
|
|
827
|
+
this.r * m4 + this.g * m5 + this.b * m6,
|
|
828
|
+
this.r * m7 + this.g * m8 + this.b * m9
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
brightness(factor) {
|
|
832
|
+
return this.set(this.r * factor, this.g * factor, this.b * factor);
|
|
833
|
+
}
|
|
834
|
+
contrast(factor) {
|
|
835
|
+
const midtone = 127.5;
|
|
836
|
+
return this.set(
|
|
837
|
+
midtone + (this.r - midtone) * factor,
|
|
838
|
+
midtone + (this.g - midtone) * factor,
|
|
839
|
+
midtone + (this.b - midtone) * factor
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
grayscale(value = 1) {
|
|
843
|
+
const m1 = 0.2126 + 0.7874 * (1 - value);
|
|
844
|
+
const m2 = 0.7152 - 0.7152 * (1 - value);
|
|
845
|
+
const m3 = 0.0722 - 0.0722 * (1 - value);
|
|
846
|
+
const m4 = 0.2126 - 0.2126 * (1 - value);
|
|
847
|
+
const m5 = 0.7152 + 0.2848 * (1 - value);
|
|
848
|
+
const m6 = 0.0722 - 0.0722 * (1 - value);
|
|
849
|
+
const m7 = 0.2126 - 0.2126 * (1 - value);
|
|
850
|
+
const m8 = 0.7152 - 0.7152 * (1 - value);
|
|
851
|
+
const m9 = 0.0722 + 0.9278 * (1 - value);
|
|
852
|
+
return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
|
|
853
|
+
}
|
|
854
|
+
hueRotate(radians) {
|
|
855
|
+
const cos = Math.cos(radians);
|
|
856
|
+
const sin = Math.sin(radians);
|
|
857
|
+
const m1 = 0.213 + cos * 0.787 - sin * 0.213;
|
|
858
|
+
const m2 = 0.715 - cos * 0.715 - sin * 0.715;
|
|
859
|
+
const m3 = 0.072 - cos * 0.072 + sin * 0.928;
|
|
860
|
+
const m4 = 0.213 - cos * 0.213 + sin * 0.143;
|
|
861
|
+
const m5 = 0.715 + cos * 0.285 + sin * 0.14;
|
|
862
|
+
const m6 = 0.072 - cos * 0.072 - sin * 0.283;
|
|
863
|
+
const m7 = 0.213 - cos * 0.213 - sin * 0.787;
|
|
864
|
+
const m8 = 0.715 - cos * 0.715 + sin * 0.715;
|
|
865
|
+
const m9 = 0.072 + cos * 0.928 + sin * 0.072;
|
|
866
|
+
return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
|
|
867
|
+
}
|
|
868
|
+
invert(factor = 1) {
|
|
869
|
+
return this.set(
|
|
870
|
+
this.r * (1 - factor) + (255 - this.r) * factor,
|
|
871
|
+
this.g * (1 - factor) + (255 - this.g) * factor,
|
|
872
|
+
this.b * (1 - factor) + (255 - this.b) * factor
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
mix(other, amount) {
|
|
876
|
+
const inv = 1 - amount;
|
|
877
|
+
return this.set(
|
|
878
|
+
this.r * inv + other.r * amount,
|
|
879
|
+
this.g * inv + other.g * amount,
|
|
880
|
+
this.b * inv + other.b * amount,
|
|
881
|
+
this.alpha * inv + other.alpha * amount
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
round() {
|
|
885
|
+
return this.set(
|
|
886
|
+
Math.round(this.r),
|
|
887
|
+
Math.round(this.g),
|
|
888
|
+
Math.round(this.b)
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
saturate(value = 1) {
|
|
892
|
+
const m1 = 0.213 + 0.787 * value;
|
|
893
|
+
const m2 = 0.715 - 0.715 * value;
|
|
894
|
+
const m3 = 0.072 - 0.072 * value;
|
|
895
|
+
const m4 = 0.213 - 0.213 * value;
|
|
896
|
+
const m5 = 0.715 + 0.285 * value;
|
|
897
|
+
const m6 = 0.072 - 0.072 * value;
|
|
898
|
+
const m7 = 0.213 - 0.213 * value;
|
|
899
|
+
const m8 = 0.715 - 0.715 * value;
|
|
900
|
+
const m9 = 0.072 + 0.928 * value;
|
|
901
|
+
return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
|
|
902
|
+
}
|
|
903
|
+
sepia(value = 1) {
|
|
904
|
+
const m1 = 0.393 + 0.607 * (1 - value);
|
|
905
|
+
const m2 = 0.769 - 0.769 * (1 - value);
|
|
906
|
+
const m3 = 0.189 - 0.189 * (1 - value);
|
|
907
|
+
const m4 = 0.349 - 0.349 * (1 - value);
|
|
908
|
+
const m5 = 0.686 + 0.314 * (1 - value);
|
|
909
|
+
const m6 = 0.168 - 0.168 * (1 - value);
|
|
910
|
+
const m7 = 0.272 - 0.272 * (1 - value);
|
|
911
|
+
const m8 = 0.534 - 0.534 * (1 - value);
|
|
912
|
+
const m9 = 0.131 + 0.869 * (1 - value);
|
|
913
|
+
return this.applyMatrix(m1, m2, m3, m4, m5, m6, m7, m8, m9);
|
|
914
|
+
}
|
|
915
|
+
shade(percent) {
|
|
916
|
+
const target = percent < 0 ? 0 : 255;
|
|
917
|
+
const p = Math.abs(percent);
|
|
918
|
+
return this.set(
|
|
919
|
+
this.r + (target - this.r) * p,
|
|
920
|
+
this.g + (target - this.g) * p,
|
|
921
|
+
this.b + (target - this.b) * p
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
toHex() {
|
|
925
|
+
const r = Math.round(this.r).toString(16).padStart(2, "0");
|
|
926
|
+
const g = Math.round(this.g).toString(16).padStart(2, "0");
|
|
927
|
+
const b = Math.round(this.b).toString(16).padStart(2, "0");
|
|
928
|
+
const rgb = `#${r}${g}${b}`;
|
|
929
|
+
if (this.alpha === 1) {
|
|
930
|
+
return rgb;
|
|
931
|
+
}
|
|
932
|
+
const a = Math.round(this.alpha * 255);
|
|
933
|
+
return `${rgb}${a.toString(16).padStart(2, "0")}`;
|
|
934
|
+
}
|
|
935
|
+
toHSL() {
|
|
936
|
+
const { h, s, l } = this.toHSLObject();
|
|
937
|
+
const cssH = Math.round(h);
|
|
938
|
+
const cssS = Math.round(s);
|
|
939
|
+
const cssL = Math.round(l);
|
|
940
|
+
return this.alpha === 1 ? `hsl(${cssH}, ${cssS}%, ${cssL}%)` : `hsla(${cssH}, ${cssS}%, ${cssL}%, ${this.alpha.toFixed(2)})`;
|
|
941
|
+
}
|
|
942
|
+
toHSLObject() {
|
|
943
|
+
const r = this.r / 255;
|
|
944
|
+
const g = this.g / 255;
|
|
945
|
+
const b = this.b / 255;
|
|
946
|
+
const max = Math.max(r, g, b);
|
|
947
|
+
const min = Math.min(r, g, b);
|
|
948
|
+
const l = (max + min) / 2;
|
|
949
|
+
let h = 0;
|
|
950
|
+
let s = 0;
|
|
951
|
+
if (max !== min) {
|
|
952
|
+
const d = max - min;
|
|
953
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
954
|
+
switch (max) {
|
|
955
|
+
case r:
|
|
956
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
957
|
+
break;
|
|
958
|
+
case g:
|
|
959
|
+
h = (b - r) / d + 2;
|
|
960
|
+
break;
|
|
961
|
+
case b:
|
|
962
|
+
h = (r - g) / d + 4;
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
h /= 6;
|
|
966
|
+
}
|
|
967
|
+
return { h: h * 360, s: s * 100, l: l * 100, a: this.alpha };
|
|
968
|
+
}
|
|
969
|
+
toRGB() {
|
|
970
|
+
const r = Math.round(this.r);
|
|
971
|
+
const g = Math.round(this.g);
|
|
972
|
+
const b = Math.round(this.b);
|
|
973
|
+
return this.alpha === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${this.alpha.toFixed(2)})`;
|
|
974
|
+
}
|
|
975
|
+
clone() {
|
|
976
|
+
return new _Color(this.r, this.g, this.b, this.alpha);
|
|
977
|
+
}
|
|
978
|
+
equals(other, compareAlpha = true) {
|
|
979
|
+
return approxEqual(this.r, other.r) && approxEqual(this.g, other.g) && approxEqual(this.b, other.b) && (!compareAlpha || approxEqual(this.alpha, other.alpha));
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// src/color/ColorShifter.ts
|
|
984
|
+
var SATURATE = 2;
|
|
985
|
+
var HUE_ROTATE = 3;
|
|
986
|
+
var BRIGHTNESS = 4;
|
|
987
|
+
var CONTRAST = 5;
|
|
988
|
+
function colorShifter(rgb) {
|
|
989
|
+
const color = new Color(rgb[0], rgb[1], rgb[2]);
|
|
990
|
+
const solver = new Solver(color);
|
|
991
|
+
const result = solver.solve();
|
|
992
|
+
return result.filter.replace("filter: ", "").replace(";", "");
|
|
993
|
+
}
|
|
994
|
+
var Solver = class {
|
|
995
|
+
constructor(target) {
|
|
996
|
+
__publicField(this, "reusedColor");
|
|
997
|
+
__publicField(this, "target");
|
|
998
|
+
__publicField(this, "targetHSL");
|
|
999
|
+
this.target = target;
|
|
1000
|
+
this.targetHSL = target.toHSLObject();
|
|
1001
|
+
this.reusedColor = new Color(0, 0, 0);
|
|
1002
|
+
}
|
|
1003
|
+
css(filters) {
|
|
1004
|
+
const [invert, sepia, saturate, hueRotate, brightness, contrast] = filters;
|
|
1005
|
+
return `filter: invert(${Math.round(invert)}%) sepia(${Math.round(sepia)}%) saturate(${Math.round(saturate)}%) hue-rotate(${Math.round(hueRotate * 3.6)}deg) brightness(${Math.round(brightness)}%) contrast(${Math.round(contrast)}%);`;
|
|
1006
|
+
}
|
|
1007
|
+
loss(filters) {
|
|
1008
|
+
const [invert, sepia, saturate, hueRotate, brightness, contrast] = filters;
|
|
1009
|
+
const color = this.reusedColor;
|
|
1010
|
+
color.set(0, 0, 0);
|
|
1011
|
+
color.invert(invert / 100);
|
|
1012
|
+
color.sepia(sepia / 100);
|
|
1013
|
+
color.saturate(saturate / 100);
|
|
1014
|
+
color.hueRotate(hueRotate / 100 * Math.PI * 2);
|
|
1015
|
+
color.brightness(brightness / 100);
|
|
1016
|
+
color.contrast(contrast / 100);
|
|
1017
|
+
const colorHSL = color.toHSLObject();
|
|
1018
|
+
return Math.abs(color.r - this.target.r) + Math.abs(color.g - this.target.g) + Math.abs(color.b - this.target.b) + // h is cyclic 0-360; wrap diff into ±180 then scale to 0-100 to keep loss balance with the other terms
|
|
1019
|
+
Math.abs(wrapDegrees(colorHSL.h - this.targetHSL.h)) / 1.8 + Math.abs(colorHSL.s - this.targetHSL.s) + Math.abs(colorHSL.l - this.targetHSL.l);
|
|
1020
|
+
}
|
|
1021
|
+
solve() {
|
|
1022
|
+
const result = this.solveNarrow(this.solveWide());
|
|
1023
|
+
return {
|
|
1024
|
+
values: result.values,
|
|
1025
|
+
loss: result.loss,
|
|
1026
|
+
filter: this.css(result.values)
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
solveNarrow(wide) {
|
|
1030
|
+
const A = wide.loss;
|
|
1031
|
+
const c = 2;
|
|
1032
|
+
const A1 = A + 1;
|
|
1033
|
+
const a = [
|
|
1034
|
+
0.25 * A1,
|
|
1035
|
+
0.25 * A1,
|
|
1036
|
+
A1,
|
|
1037
|
+
0.25 * A1,
|
|
1038
|
+
0.2 * A1,
|
|
1039
|
+
0.2 * A1
|
|
1040
|
+
];
|
|
1041
|
+
return this.spsa(A, a, c, wide.values, 500);
|
|
1042
|
+
}
|
|
1043
|
+
solveWide() {
|
|
1044
|
+
const A = 5;
|
|
1045
|
+
const c = 15;
|
|
1046
|
+
const a = [60, 180, 18e3, 600, 1.2, 1.2];
|
|
1047
|
+
let best = {
|
|
1048
|
+
loss: Infinity,
|
|
1049
|
+
values: [50, 20, 3750, 50, 100, 100]
|
|
1050
|
+
};
|
|
1051
|
+
for (let i = 0; best.loss > 25 && i < 3; i++) {
|
|
1052
|
+
const initial = [50, 20, 3750, 50, 100, 100];
|
|
1053
|
+
const result = this.spsa(A, a, c, initial, 1e3);
|
|
1054
|
+
if (result.loss < best.loss) {
|
|
1055
|
+
best = result;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return best;
|
|
1059
|
+
}
|
|
1060
|
+
spsa(A, a, c, values, iters) {
|
|
1061
|
+
values = values.slice();
|
|
1062
|
+
const alpha = 1;
|
|
1063
|
+
const gamma = 0.16666666666666666;
|
|
1064
|
+
let best = values.slice();
|
|
1065
|
+
let bestLoss = Infinity;
|
|
1066
|
+
const deltas = [0, 0, 0, 0, 0, 0];
|
|
1067
|
+
const highArgs = [0, 0, 0, 0, 0, 0];
|
|
1068
|
+
const lowArgs = [0, 0, 0, 0, 0, 0];
|
|
1069
|
+
for (let k = 0; k < iters; k++) {
|
|
1070
|
+
const ck = c / Math.pow(k + 1, gamma);
|
|
1071
|
+
for (let i = 0; i < 6; i++) {
|
|
1072
|
+
deltas[i] = Math.random() > 0.5 ? 1 : -1;
|
|
1073
|
+
highArgs[i] = values[i] + ck * deltas[i];
|
|
1074
|
+
lowArgs[i] = values[i] - ck * deltas[i];
|
|
1075
|
+
}
|
|
1076
|
+
const lossDiff = this.loss(highArgs) - this.loss(lowArgs);
|
|
1077
|
+
for (let i = 0; i < 6; i++) {
|
|
1078
|
+
const g = lossDiff / (2 * ck) * deltas[i];
|
|
1079
|
+
const ak = a[i] / Math.pow(A + k + 1, alpha);
|
|
1080
|
+
values[i] = fix(values[i] - ak * g, i);
|
|
1081
|
+
}
|
|
1082
|
+
const loss = this.loss(values);
|
|
1083
|
+
if (loss < bestLoss) {
|
|
1084
|
+
best = values.slice();
|
|
1085
|
+
bestLoss = loss;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return { values: best, loss: bestLoss };
|
|
1089
|
+
function fix(value, idx) {
|
|
1090
|
+
let max = 100;
|
|
1091
|
+
if (idx === SATURATE) {
|
|
1092
|
+
max = 7500;
|
|
1093
|
+
} else if (idx === BRIGHTNESS || idx === CONTRAST) {
|
|
1094
|
+
max = 200;
|
|
1095
|
+
}
|
|
1096
|
+
if (idx === HUE_ROTATE) {
|
|
1097
|
+
value = wrapValue(value, 0, max);
|
|
1098
|
+
} else if (value < 0) {
|
|
1099
|
+
value = 0;
|
|
1100
|
+
} else if (value > max) {
|
|
1101
|
+
value = max;
|
|
1102
|
+
}
|
|
1103
|
+
return value;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
// src/math/Rect.ts
|
|
1109
|
+
var Rect = class _Rect {
|
|
1110
|
+
constructor(x = 0, y = 0, w = 0, h = 0) {
|
|
1111
|
+
__publicField(this, "_h", 0);
|
|
1112
|
+
__publicField(this, "_w", 0);
|
|
1113
|
+
__publicField(this, "_x", 0);
|
|
1114
|
+
__publicField(this, "_y", 0);
|
|
1115
|
+
__publicField(this, "_sides");
|
|
1116
|
+
__publicField(this, "sideIsDirty", true);
|
|
1117
|
+
this.set(x, y, w, h);
|
|
1118
|
+
}
|
|
1119
|
+
static fromBoundingClientRect(rect) {
|
|
1120
|
+
if (rect instanceof HTMLElement) {
|
|
1121
|
+
rect = rect.getBoundingClientRect();
|
|
1122
|
+
}
|
|
1123
|
+
return new _Rect(rect.left, rect.top, rect.width, rect.height);
|
|
1124
|
+
}
|
|
1125
|
+
static fromPolygon(polygon) {
|
|
1126
|
+
if (polygon.points.length === 0) {
|
|
1127
|
+
throw new Error("Supplied polygon has no points!");
|
|
1128
|
+
}
|
|
1129
|
+
let minX = Infinity;
|
|
1130
|
+
let minY = Infinity;
|
|
1131
|
+
let maxX = -Infinity;
|
|
1132
|
+
let maxY = -Infinity;
|
|
1133
|
+
polygon.points.forEach((point) => {
|
|
1134
|
+
if (point.x < minX) {
|
|
1135
|
+
minX = point.x;
|
|
1136
|
+
}
|
|
1137
|
+
if (point.x > maxX) {
|
|
1138
|
+
maxX = point.x;
|
|
1139
|
+
}
|
|
1140
|
+
if (point.y < minY) {
|
|
1141
|
+
minY = point.y;
|
|
1142
|
+
}
|
|
1143
|
+
if (point.y > maxY) {
|
|
1144
|
+
maxY = point.y;
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
return new _Rect(minX, minY, maxX - minX, maxY - minY);
|
|
1148
|
+
}
|
|
1149
|
+
get h() {
|
|
1150
|
+
return this._h;
|
|
1151
|
+
}
|
|
1152
|
+
set h(value) {
|
|
1153
|
+
this._h = value;
|
|
1154
|
+
this.sideIsDirty = true;
|
|
1155
|
+
}
|
|
1156
|
+
get w() {
|
|
1157
|
+
return this._w;
|
|
1158
|
+
}
|
|
1159
|
+
set w(value) {
|
|
1160
|
+
this._w = value;
|
|
1161
|
+
this.sideIsDirty = true;
|
|
1162
|
+
}
|
|
1163
|
+
get x() {
|
|
1164
|
+
return this._x;
|
|
1165
|
+
}
|
|
1166
|
+
set x(value) {
|
|
1167
|
+
this._x = value;
|
|
1168
|
+
this.sideIsDirty = true;
|
|
1169
|
+
}
|
|
1170
|
+
get y() {
|
|
1171
|
+
return this._y;
|
|
1172
|
+
}
|
|
1173
|
+
set y(value) {
|
|
1174
|
+
this._y = value;
|
|
1175
|
+
this.sideIsDirty = true;
|
|
1176
|
+
}
|
|
1177
|
+
get sides() {
|
|
1178
|
+
if (this.sideIsDirty) {
|
|
1179
|
+
this._sides = {
|
|
1180
|
+
bottom: this.y + this.h,
|
|
1181
|
+
centerPos: new Vec2(
|
|
1182
|
+
this.x + this.w * 0.5,
|
|
1183
|
+
this.y + this.h * 0.5
|
|
1184
|
+
),
|
|
1185
|
+
halfSize: new Vec2(this.w * 0.5, this.h * 0.5),
|
|
1186
|
+
right: this.x + this.w
|
|
1187
|
+
};
|
|
1188
|
+
this.sideIsDirty = false;
|
|
1189
|
+
}
|
|
1190
|
+
return this._sides;
|
|
1191
|
+
}
|
|
1192
|
+
inflate(delta) {
|
|
1193
|
+
this.x -= delta;
|
|
1194
|
+
this.y -= delta;
|
|
1195
|
+
this.w += 2 * delta;
|
|
1196
|
+
this.h += 2 * delta;
|
|
1197
|
+
this.sideIsDirty = true;
|
|
1198
|
+
return this;
|
|
1199
|
+
}
|
|
1200
|
+
round() {
|
|
1201
|
+
this.x = Math.round(this.x);
|
|
1202
|
+
this.y = Math.round(this.y);
|
|
1203
|
+
this.sideIsDirty = true;
|
|
1204
|
+
return this;
|
|
1205
|
+
}
|
|
1206
|
+
set(x = 0, y = 0, w, h) {
|
|
1207
|
+
if (typeof x === "number") {
|
|
1208
|
+
this.x = x;
|
|
1209
|
+
this.y = y;
|
|
1210
|
+
} else {
|
|
1211
|
+
if ("w" in x) {
|
|
1212
|
+
this.w = x.w;
|
|
1213
|
+
this.h = x.h;
|
|
1214
|
+
}
|
|
1215
|
+
this.x = x.x;
|
|
1216
|
+
this.y = x.y;
|
|
1217
|
+
}
|
|
1218
|
+
if (w !== void 0) {
|
|
1219
|
+
this.w = w;
|
|
1220
|
+
}
|
|
1221
|
+
if (h !== void 0) {
|
|
1222
|
+
this.h = h;
|
|
1223
|
+
}
|
|
1224
|
+
this.sideIsDirty = true;
|
|
1225
|
+
return this;
|
|
1226
|
+
}
|
|
1227
|
+
collide(rect) {
|
|
1228
|
+
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
|
+
}
|
|
1230
|
+
collideFull(rect) {
|
|
1231
|
+
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
|
+
}
|
|
1233
|
+
collidePoint(vec) {
|
|
1234
|
+
return this.x <= vec.x && vec.x <= this.x + this.w && this.y <= vec.y && vec.y <= this.y + this.h;
|
|
1235
|
+
}
|
|
1236
|
+
collideSide(rect) {
|
|
1237
|
+
const dx = this.x + this.w * 0.5 - (rect.x + rect.w * 0.5);
|
|
1238
|
+
const dy = this.y + this.h * 0.5 - (rect.y + rect.h * 0.5);
|
|
1239
|
+
const width = (this.w + rect.w) * 0.5;
|
|
1240
|
+
const height = (this.h + rect.h) * 0.5;
|
|
1241
|
+
const crossWidth = width * dy;
|
|
1242
|
+
const crossHeight = height * dx;
|
|
1243
|
+
let collision = "none";
|
|
1244
|
+
if (Math.abs(dx) <= width && Math.abs(dy) <= height) {
|
|
1245
|
+
if (crossWidth > crossHeight) {
|
|
1246
|
+
collision = crossWidth > -crossHeight ? "bottom" : "left";
|
|
1247
|
+
} else {
|
|
1248
|
+
collision = crossWidth > -crossHeight ? "right" : "top";
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return collision;
|
|
1252
|
+
}
|
|
1253
|
+
pos() {
|
|
1254
|
+
return new Vec2(this.x, this.y);
|
|
1255
|
+
}
|
|
1256
|
+
size() {
|
|
1257
|
+
return new Vec2(this.w, this.h);
|
|
1258
|
+
}
|
|
1259
|
+
toString() {
|
|
1260
|
+
return `Rect [x: ${this.x}, y: ${this.y}, w: ${this.w}, h: ${this.h}]`;
|
|
1261
|
+
}
|
|
1262
|
+
clone() {
|
|
1263
|
+
return new _Rect(this.x, this.y, this.w, this.h);
|
|
1264
|
+
}
|
|
1265
|
+
equals(other, withSize = true) {
|
|
1266
|
+
let output = approxEqual(this.x, other.x) && approxEqual(this.y, other.y);
|
|
1267
|
+
if (output && withSize) {
|
|
1268
|
+
output = approxEqual(this.w, other.w) && approxEqual(this.h, other.h);
|
|
1269
|
+
}
|
|
1270
|
+
return output;
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
// src/math/Vec2.ts
|
|
1275
|
+
var Operation = {
|
|
1276
|
+
Add: 1,
|
|
1277
|
+
Sub: 2,
|
|
1278
|
+
Mult: 3,
|
|
1279
|
+
Div: 4,
|
|
1280
|
+
Rem: 5,
|
|
1281
|
+
Equal: 6,
|
|
1282
|
+
Mod: 7
|
|
1283
|
+
};
|
|
1284
|
+
var warnZeroNormalize = throttle(
|
|
1285
|
+
(count) => console.warn(
|
|
1286
|
+
`Vec2.normalize() called on zero vector x${count} since last warning; returning zero.`
|
|
1287
|
+
)
|
|
1288
|
+
);
|
|
1289
|
+
var warnNonFinite = throttle(
|
|
1290
|
+
(count) => console.warn(
|
|
1291
|
+
`Vec2 operation produced non-finite value x${count} since last warning; check for zero divisor.`
|
|
1292
|
+
)
|
|
1293
|
+
);
|
|
1294
|
+
var Vec2 = class _Vec2 {
|
|
1295
|
+
constructor(x = 0, y) {
|
|
1296
|
+
__publicField(this, "x", 0);
|
|
1297
|
+
__publicField(this, "y", 0);
|
|
1298
|
+
this.calculate(Operation.Equal, x, y);
|
|
1299
|
+
}
|
|
1300
|
+
static fromAngle(rad, scaleX = 1, scaleY = scaleX) {
|
|
1301
|
+
return new _Vec2(Math.cos(rad) * scaleX, Math.sin(rad) * scaleY);
|
|
1302
|
+
}
|
|
1303
|
+
set(x, y) {
|
|
1304
|
+
return this.calculate(Operation.Equal, x, y);
|
|
1305
|
+
}
|
|
1306
|
+
abs() {
|
|
1307
|
+
return this.map(Math.abs);
|
|
1308
|
+
}
|
|
1309
|
+
add(x, y) {
|
|
1310
|
+
return this.calculate(Operation.Add, x, y);
|
|
1311
|
+
}
|
|
1312
|
+
ceil() {
|
|
1313
|
+
return this.map(Math.ceil);
|
|
1314
|
+
}
|
|
1315
|
+
clamp(x, y = x) {
|
|
1316
|
+
this.x = clamp(this.x, x[0], x[1]);
|
|
1317
|
+
this.y = clamp(this.y, y[0], y[1]);
|
|
1318
|
+
return this;
|
|
1319
|
+
}
|
|
1320
|
+
div(x, y) {
|
|
1321
|
+
return this.calculate(Operation.Div, x, y);
|
|
1322
|
+
}
|
|
1323
|
+
floor() {
|
|
1324
|
+
return this.map(Math.floor);
|
|
1325
|
+
}
|
|
1326
|
+
map(callback) {
|
|
1327
|
+
this.x = callback(this.x, 0);
|
|
1328
|
+
this.y = callback(this.y, 1);
|
|
1329
|
+
return this;
|
|
1330
|
+
}
|
|
1331
|
+
mod(x, y) {
|
|
1332
|
+
return this.calculate(Operation.Mod, x, y);
|
|
1333
|
+
}
|
|
1334
|
+
mult(x, y) {
|
|
1335
|
+
return this.calculate(Operation.Mult, x, y);
|
|
1336
|
+
}
|
|
1337
|
+
negate() {
|
|
1338
|
+
return this.mult(-1);
|
|
1339
|
+
}
|
|
1340
|
+
normalize() {
|
|
1341
|
+
const length = this.length();
|
|
1342
|
+
if (approxEqual(length, 0)) {
|
|
1343
|
+
warnZeroNormalize();
|
|
1344
|
+
return this;
|
|
1345
|
+
}
|
|
1346
|
+
return this.map((value) => value / length);
|
|
1347
|
+
}
|
|
1348
|
+
normalizeManhattan() {
|
|
1349
|
+
const length = this.lengthManhattan();
|
|
1350
|
+
if (approxEqual(length, 0)) {
|
|
1351
|
+
return this;
|
|
1352
|
+
}
|
|
1353
|
+
return this.map((value) => value / length);
|
|
1354
|
+
}
|
|
1355
|
+
rem(x, y) {
|
|
1356
|
+
return this.calculate(Operation.Rem, x, y);
|
|
1357
|
+
}
|
|
1358
|
+
round() {
|
|
1359
|
+
this.x = Math.round(this.x);
|
|
1360
|
+
this.y = Math.round(this.y);
|
|
1361
|
+
return this;
|
|
1362
|
+
}
|
|
1363
|
+
sub(x, y) {
|
|
1364
|
+
return this.calculate(Operation.Sub, x, y);
|
|
1365
|
+
}
|
|
1366
|
+
angle(other) {
|
|
1367
|
+
if (!other) {
|
|
1368
|
+
return Math.atan2(this.y, this.x);
|
|
1369
|
+
}
|
|
1370
|
+
return Math.atan2(other.y - this.y, other.x - this.x);
|
|
1371
|
+
}
|
|
1372
|
+
distance(other) {
|
|
1373
|
+
return Math.sqrt(
|
|
1374
|
+
Math.pow(other.x - this.x, 2) + Math.pow(other.y - this.y, 2)
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
distanceManhattan(other) {
|
|
1378
|
+
return Math.abs(other.x - this.x) + Math.abs(other.y - this.y);
|
|
1379
|
+
}
|
|
1380
|
+
dotProduct(other) {
|
|
1381
|
+
return this.x * other.x + this.y * other.y;
|
|
1382
|
+
}
|
|
1383
|
+
isValid() {
|
|
1384
|
+
return Number.isFinite(this.x) && Number.isFinite(this.y);
|
|
1385
|
+
}
|
|
1386
|
+
length() {
|
|
1387
|
+
return Math.sqrt(this.x * this.x + this.y * this.y);
|
|
1388
|
+
}
|
|
1389
|
+
lengthManhattan() {
|
|
1390
|
+
return Math.abs(this.x) + Math.abs(this.y);
|
|
1391
|
+
}
|
|
1392
|
+
max() {
|
|
1393
|
+
return Math.max(this.x, this.y);
|
|
1394
|
+
}
|
|
1395
|
+
min() {
|
|
1396
|
+
return Math.min(this.x, this.y);
|
|
1397
|
+
}
|
|
1398
|
+
toArray() {
|
|
1399
|
+
return [this.x, this.y];
|
|
1400
|
+
}
|
|
1401
|
+
toRectAddPos(x, y) {
|
|
1402
|
+
return this.concat(true, x, y);
|
|
1403
|
+
}
|
|
1404
|
+
toRectAddSize(width, height) {
|
|
1405
|
+
return this.concat(false, width, height);
|
|
1406
|
+
}
|
|
1407
|
+
toString() {
|
|
1408
|
+
return `Vec2 [x: ${this.x}, y: ${this.y}]`;
|
|
1409
|
+
}
|
|
1410
|
+
clone() {
|
|
1411
|
+
return new _Vec2(this.x, this.y);
|
|
1412
|
+
}
|
|
1413
|
+
equals(x, y) {
|
|
1414
|
+
const [x2, y2] = this.getValues(x, y);
|
|
1415
|
+
return approxEqual(this.x, x2) && approxEqual(this.y, y2);
|
|
1416
|
+
}
|
|
1417
|
+
calculate(operation, x, y) {
|
|
1418
|
+
const [x2, y2] = this.getValues(x, y);
|
|
1419
|
+
switch (operation) {
|
|
1420
|
+
case Operation.Add:
|
|
1421
|
+
this.x += x2;
|
|
1422
|
+
this.y += y2;
|
|
1423
|
+
break;
|
|
1424
|
+
case Operation.Sub:
|
|
1425
|
+
this.x -= x2;
|
|
1426
|
+
this.y -= y2;
|
|
1427
|
+
break;
|
|
1428
|
+
case Operation.Mult:
|
|
1429
|
+
this.x *= x2;
|
|
1430
|
+
this.y *= y2;
|
|
1431
|
+
break;
|
|
1432
|
+
case Operation.Div:
|
|
1433
|
+
this.x /= x2;
|
|
1434
|
+
this.y /= y2;
|
|
1435
|
+
break;
|
|
1436
|
+
case Operation.Rem:
|
|
1437
|
+
this.x %= x2;
|
|
1438
|
+
this.y %= y2;
|
|
1439
|
+
break;
|
|
1440
|
+
case Operation.Equal:
|
|
1441
|
+
this.x = x2;
|
|
1442
|
+
this.y = y2;
|
|
1443
|
+
break;
|
|
1444
|
+
case Operation.Mod:
|
|
1445
|
+
this.x = (this.x % x2 + x2) % x2;
|
|
1446
|
+
this.y = (this.y % y2 + y2) % y2;
|
|
1447
|
+
break;
|
|
1448
|
+
default:
|
|
1449
|
+
operation;
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
if (!this.isValid()) {
|
|
1453
|
+
warnNonFinite();
|
|
1454
|
+
}
|
|
1455
|
+
return this;
|
|
1456
|
+
}
|
|
1457
|
+
concat(first, x, y) {
|
|
1458
|
+
const [x2, y2] = this.getValues(x, y);
|
|
1459
|
+
if (first) {
|
|
1460
|
+
return new Rect(x2, y2, this.x, this.y);
|
|
1461
|
+
}
|
|
1462
|
+
return new Rect(this.x, this.y, x2, y2);
|
|
1463
|
+
}
|
|
1464
|
+
getValues(x, y) {
|
|
1465
|
+
let inputX = 0;
|
|
1466
|
+
let inputY = 0;
|
|
1467
|
+
if (typeof x === "number") {
|
|
1468
|
+
inputX = x;
|
|
1469
|
+
if (y === void 0) {
|
|
1470
|
+
inputY = x;
|
|
1471
|
+
} else {
|
|
1472
|
+
inputY = y;
|
|
1473
|
+
}
|
|
1474
|
+
} else {
|
|
1475
|
+
if (y === void 0) {
|
|
1476
|
+
inputX = x.x;
|
|
1477
|
+
inputY = x.y;
|
|
1478
|
+
} else {
|
|
1479
|
+
throw new Error("When x is a Vector2, y must be omitted!");
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return [inputX, inputY];
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
// src/core/Settings.ts
|
|
1487
|
+
var LOCAL_STORAGE_KEY = "gleam";
|
|
1488
|
+
var Settings = class {
|
|
1489
|
+
static get localStorage() {
|
|
1490
|
+
return this._localStorage;
|
|
1491
|
+
}
|
|
1492
|
+
static init(overrides, game) {
|
|
1493
|
+
if (this.initialized) {
|
|
1494
|
+
throw new Error("Settings.init called twice");
|
|
1495
|
+
}
|
|
1496
|
+
Object.assign(this, overrides);
|
|
1497
|
+
if (!(Number.isFinite(this.fps) && this.fps > 0)) {
|
|
1498
|
+
throw new Error(`Settings.fps must be > 0, got ${this.fps}`);
|
|
1499
|
+
}
|
|
1500
|
+
if (this.debug) {
|
|
1501
|
+
window.game = game;
|
|
1502
|
+
}
|
|
1503
|
+
if (this.warnBeforeClose) {
|
|
1504
|
+
window.addEventListener(
|
|
1505
|
+
"beforeunload",
|
|
1506
|
+
(event) => {
|
|
1507
|
+
this.triedToClose?.();
|
|
1508
|
+
event.preventDefault();
|
|
1509
|
+
event.returnValue = true;
|
|
1510
|
+
return "Are you sure?";
|
|
1511
|
+
},
|
|
1512
|
+
false
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
this._localStorage.language = navigator.language.split("-")[0] || "en";
|
|
1516
|
+
const storage = localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
1517
|
+
if (storage) {
|
|
1518
|
+
try {
|
|
1519
|
+
const parsed = JSON.parse(storage);
|
|
1520
|
+
Object.assign(this._localStorage, parsed);
|
|
1521
|
+
} catch (_e) {
|
|
1522
|
+
console.error(
|
|
1523
|
+
"Couldn't parse local storage! Will be cleaned now.",
|
|
1524
|
+
storage
|
|
1525
|
+
);
|
|
1526
|
+
localStorage.removeItem(LOCAL_STORAGE_KEY);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
this.initialized = true;
|
|
1530
|
+
}
|
|
1531
|
+
static setLocalStorage(key, value) {
|
|
1532
|
+
this._localStorage[key] = value;
|
|
1533
|
+
localStorage.setItem(
|
|
1534
|
+
LOCAL_STORAGE_KEY,
|
|
1535
|
+
JSON.stringify(this._localStorage)
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
__publicField(Settings, "antialias", false);
|
|
1540
|
+
__publicField(Settings, "autoloop", true);
|
|
1541
|
+
__publicField(Settings, "backgroundColor", "#444");
|
|
1542
|
+
__publicField(Settings, "debug", false);
|
|
1543
|
+
__publicField(Settings, "doNotClear", false);
|
|
1544
|
+
__publicField(Settings, "enableResize", true);
|
|
1545
|
+
__publicField(Settings, "font", "Arial");
|
|
1546
|
+
__publicField(Settings, "fps", 1 / 60);
|
|
1547
|
+
__publicField(Settings, "triedToClose");
|
|
1548
|
+
__publicField(Settings, "useClearRect", true);
|
|
1549
|
+
__publicField(Settings, "warnBeforeClose", false);
|
|
1550
|
+
__publicField(Settings, "initialized", false);
|
|
1551
|
+
// Only mutated via `setLocalStorage` (typed) and via the localStorage
|
|
1552
|
+
// round-trip in `init()` — since `setLocalStorage` is the sole writer of
|
|
1553
|
+
// the persisted blob, the parsed payload is trusted to match the schema.
|
|
1554
|
+
__publicField(Settings, "_localStorage", {
|
|
1555
|
+
language: ""
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
// src/utilities/DOM.ts
|
|
1559
|
+
function getElement(query, parent = document) {
|
|
1560
|
+
const el = parent.querySelector(query);
|
|
1561
|
+
if (!el) {
|
|
1562
|
+
throw new Error(`Element not found: ${query}`);
|
|
1563
|
+
}
|
|
1564
|
+
return el;
|
|
1565
|
+
}
|
|
1566
|
+
function styleElement(element, styles) {
|
|
1567
|
+
Object.assign(element.style, styles);
|
|
1568
|
+
}
|
|
1569
|
+
function setDisplay(element, active) {
|
|
1570
|
+
element.style.display = active ? "" : "none";
|
|
1571
|
+
}
|
|
1572
|
+
function setVisibility(element, active) {
|
|
1573
|
+
element.style.visibility = active ? "" : "hidden";
|
|
1574
|
+
}
|
|
1575
|
+
function initCSSVariables() {
|
|
1576
|
+
const root = getElement(":root");
|
|
1577
|
+
return {
|
|
1578
|
+
root,
|
|
1579
|
+
get(name) {
|
|
1580
|
+
return getComputedStyle(root).getPropertyValue("--" + name);
|
|
1581
|
+
},
|
|
1582
|
+
set(name, value) {
|
|
1583
|
+
root.style.setProperty("--" + name, value);
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function doWhilePressed(querySelector, callback, delay2 = 200) {
|
|
1588
|
+
const element = getElement(querySelector);
|
|
1589
|
+
let intervalId;
|
|
1590
|
+
let activePointerId = null;
|
|
1591
|
+
function start(event) {
|
|
1592
|
+
if (activePointerId !== null) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
activePointerId = event.pointerId;
|
|
1596
|
+
element.setPointerCapture(activePointerId);
|
|
1597
|
+
clearInterval(intervalId);
|
|
1598
|
+
callback();
|
|
1599
|
+
intervalId = setInterval(() => callback(), delay2);
|
|
1600
|
+
}
|
|
1601
|
+
function stop(event) {
|
|
1602
|
+
if (event.pointerId !== activePointerId) {
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
clearInterval(intervalId);
|
|
1606
|
+
element.releasePointerCapture(event.pointerId);
|
|
1607
|
+
activePointerId = null;
|
|
1608
|
+
}
|
|
1609
|
+
element.addEventListener("pointerdown", start);
|
|
1610
|
+
element.addEventListener("pointerup", stop);
|
|
1611
|
+
element.addEventListener("pointercancel", stop);
|
|
1612
|
+
function dispose() {
|
|
1613
|
+
element.removeEventListener("pointerdown", start);
|
|
1614
|
+
element.removeEventListener("pointerup", stop);
|
|
1615
|
+
element.removeEventListener("pointercancel", stop);
|
|
1616
|
+
clearInterval(intervalId);
|
|
1617
|
+
if (activePointerId !== null) {
|
|
1618
|
+
element.releasePointerCapture(activePointerId);
|
|
1619
|
+
activePointerId = null;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return dispose;
|
|
1623
|
+
}
|
|
1624
|
+
async function waitForEvent(element, type, signal) {
|
|
1625
|
+
if (signal?.aborted) {
|
|
1626
|
+
throw signal.reason;
|
|
1627
|
+
}
|
|
1628
|
+
return new Promise((resolve, reject) => {
|
|
1629
|
+
element.addEventListener(type, () => resolve(), {
|
|
1630
|
+
once: true,
|
|
1631
|
+
signal
|
|
1632
|
+
});
|
|
1633
|
+
signal?.addEventListener("abort", () => reject(signal.reason), {
|
|
1634
|
+
once: true
|
|
1635
|
+
});
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/utilities/Grid.ts
|
|
1640
|
+
function cloneGrid(grid) {
|
|
1641
|
+
return grid.map((row) => row.slice());
|
|
1642
|
+
}
|
|
1643
|
+
function convert1DTo2D(index, width) {
|
|
1644
|
+
return {
|
|
1645
|
+
x: index % width,
|
|
1646
|
+
y: index / width | 0
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
function convert2DTo1D(indexX, indexY, width) {
|
|
1650
|
+
return indexX + width * indexY;
|
|
1651
|
+
}
|
|
1652
|
+
function generateGrid(height, width, valueOrFactory) {
|
|
1653
|
+
const isFactory = typeof valueOrFactory === "function";
|
|
1654
|
+
return Array.from(
|
|
1655
|
+
{ length: height },
|
|
1656
|
+
(_, y) => Array.from(
|
|
1657
|
+
{ length: width },
|
|
1658
|
+
(_2, x) => isFactory ? valueOrFactory(x, y) : valueOrFactory
|
|
1659
|
+
)
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// src/utilities/Prototype.ts
|
|
1664
|
+
function defineMethod(proto, name, value) {
|
|
1665
|
+
Object.defineProperty(proto, name, {
|
|
1666
|
+
value,
|
|
1667
|
+
configurable: true,
|
|
1668
|
+
writable: true
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// src/loader/UrlLoaders.ts
|
|
1673
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1674
|
+
function validateUrl(url) {
|
|
1675
|
+
const schemeMatch = url.trim().match(/^([a-z][a-z0-9+.-]*):/i);
|
|
1676
|
+
if (!schemeMatch) {
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
1680
|
+
if (["blob", "data", "http", "https"].includes(scheme)) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
throw new Error(`Invalid URL protocol: ${url}`);
|
|
1684
|
+
}
|
|
1685
|
+
function safeLoad(promise, url, operationName) {
|
|
1686
|
+
let timeoutId;
|
|
1687
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1688
|
+
timeoutId = setTimeout(() => {
|
|
1689
|
+
reject(
|
|
1690
|
+
new Error(
|
|
1691
|
+
`Timeout (${DEFAULT_TIMEOUT_MS}ms) when loading ${operationName}: ${url}`
|
|
1692
|
+
)
|
|
1693
|
+
);
|
|
1694
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
1695
|
+
});
|
|
1696
|
+
return Promise.race([promise, timeoutPromise]).catch((error) => {
|
|
1697
|
+
clearTimeout(timeoutId);
|
|
1698
|
+
const errorMessage = error?.message || String(error);
|
|
1699
|
+
console.error(
|
|
1700
|
+
`${operationName} failed on ${url}
|
|
1701
|
+
${errorMessage}`
|
|
1702
|
+
);
|
|
1703
|
+
throw error;
|
|
1704
|
+
}).finally(() => clearTimeout(timeoutId));
|
|
1705
|
+
}
|
|
1706
|
+
async function loadImage(url) {
|
|
1707
|
+
validateUrl(url);
|
|
1708
|
+
return safeLoad(
|
|
1709
|
+
new Promise((resolve, reject) => {
|
|
1710
|
+
const image = new Image();
|
|
1711
|
+
image.onload = () => resolve(image);
|
|
1712
|
+
image.onerror = () => reject(new Error(`Image failed to load: ${url}`));
|
|
1713
|
+
image.crossOrigin = "anonymous";
|
|
1714
|
+
image.src = url;
|
|
1715
|
+
}),
|
|
1716
|
+
url,
|
|
1717
|
+
"image"
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
async function loadCanvas(url) {
|
|
1721
|
+
validateUrl(url);
|
|
1722
|
+
return safeLoad(
|
|
1723
|
+
(async () => {
|
|
1724
|
+
const image = await loadImage(url);
|
|
1725
|
+
const cc = createNewCanvas(image.width, image.height);
|
|
1726
|
+
cc.canvas.id = url;
|
|
1727
|
+
cc.context.drawImage(image, 0, 0);
|
|
1728
|
+
return cc.canvas;
|
|
1729
|
+
})(),
|
|
1730
|
+
url,
|
|
1731
|
+
"canvas"
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
async function loadText(url) {
|
|
1735
|
+
validateUrl(url);
|
|
1736
|
+
return safeLoad(
|
|
1737
|
+
(async () => {
|
|
1738
|
+
const response = await fetch(url);
|
|
1739
|
+
if (!response.ok) {
|
|
1740
|
+
throw new Error(
|
|
1741
|
+
`HTTP ${response.status}: Failed to load text from ${url}
|
|
1742
|
+
Status: ${response.status} ${response.statusText}`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
return response.text();
|
|
1746
|
+
})(),
|
|
1747
|
+
url,
|
|
1748
|
+
"text"
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
async function loadJson(url) {
|
|
1752
|
+
validateUrl(url);
|
|
1753
|
+
return safeLoad(
|
|
1754
|
+
loadText(url).then((text) => JSON.parse(text)),
|
|
1755
|
+
url,
|
|
1756
|
+
"JSON"
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
async function loadJsonCommented(url) {
|
|
1760
|
+
validateUrl(url);
|
|
1761
|
+
return safeLoad(
|
|
1762
|
+
loadText(url).then((text) => {
|
|
1763
|
+
const lines = text.split("\n").filter((line) => {
|
|
1764
|
+
const trimmed = line.trim();
|
|
1765
|
+
if (!trimmed || trimmed.startsWith("//")) {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
return true;
|
|
1769
|
+
}).join("\n");
|
|
1770
|
+
return JSON.parse(lines);
|
|
1771
|
+
}),
|
|
1772
|
+
url,
|
|
1773
|
+
"JSON (commented)"
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
async function loadImageFromJson(baseUrl, filenameOrJson, jsonInput = false) {
|
|
1777
|
+
if (!baseUrl.endsWith("/")) {
|
|
1778
|
+
baseUrl += "/";
|
|
1779
|
+
}
|
|
1780
|
+
let json;
|
|
1781
|
+
if (jsonInput) {
|
|
1782
|
+
try {
|
|
1783
|
+
json = JSON.parse(filenameOrJson);
|
|
1784
|
+
} catch (parseError) {
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
`Failed to parse inline JSON: ${parseError.message}`
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
} else {
|
|
1790
|
+
const url = baseUrl + filenameOrJson + ".json";
|
|
1791
|
+
json = await loadJson(url);
|
|
1792
|
+
}
|
|
1793
|
+
if (!json || !json.options?.file) {
|
|
1794
|
+
const source = jsonInput ? "inline JSON" : `${baseUrl}${filenameOrJson}.json`;
|
|
1795
|
+
throw new Error(
|
|
1796
|
+
`JSON missing 'options.file' property
|
|
1797
|
+
Source: ${source}`
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
const canvas = await loadCanvas(baseUrl + json.options.file);
|
|
1801
|
+
const sprites = {};
|
|
1802
|
+
if (!json.sprites || !Array.isArray(json.sprites)) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
`JSON missing 'sprites' array
|
|
1805
|
+
Source: ${baseUrl}${filenameOrJson}.json`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
json.sprites.forEach((sprite, i) => {
|
|
1809
|
+
if (sprite.x === void 0 || sprite.y === void 0 || !sprite.w || !sprite.h || !sprite.name) {
|
|
1810
|
+
throw new Error(
|
|
1811
|
+
`Invalid sprite data at index ${i}: ${JSON.stringify(sprite, null, 2)}`
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
const newSprite = canvas.subImage(
|
|
1815
|
+
sprite.x,
|
|
1816
|
+
sprite.y,
|
|
1817
|
+
sprite.w,
|
|
1818
|
+
sprite.h
|
|
1819
|
+
);
|
|
1820
|
+
for (const key in sprite) {
|
|
1821
|
+
newSprite.dataset[key] = String(sprite[key]);
|
|
1822
|
+
}
|
|
1823
|
+
sprites[sprite.name] = newSprite;
|
|
1824
|
+
});
|
|
1825
|
+
return sprites;
|
|
1826
|
+
}
|
|
1827
|
+
async function loadBunch(bunch) {
|
|
1828
|
+
const output = {};
|
|
1829
|
+
const keys = Object.keys(bunch);
|
|
1830
|
+
return Promise.all(keys.map((k) => bunch[k])).then((datas) => {
|
|
1831
|
+
keys.forEach((key, i) => {
|
|
1832
|
+
output[key] = datas[i];
|
|
1833
|
+
});
|
|
1834
|
+
return output;
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/prototypes/HTMLCanvasElement.ts
|
|
1839
|
+
defineMethod(HTMLCanvasElement.prototype, "hasAnyColor", function() {
|
|
1840
|
+
const pixels = this.getContext("2d").getImageData(
|
|
1841
|
+
0,
|
|
1842
|
+
0,
|
|
1843
|
+
this.width,
|
|
1844
|
+
this.height
|
|
1845
|
+
).data;
|
|
1846
|
+
return pixels.some((pixel) => pixel !== 0);
|
|
1847
|
+
});
|
|
1848
|
+
defineMethod(HTMLCanvasElement.prototype, "getPixelAt", function(x, y, output = "integer") {
|
|
1849
|
+
let r = 0;
|
|
1850
|
+
let g = 0;
|
|
1851
|
+
let b = 0;
|
|
1852
|
+
let a = 0;
|
|
1853
|
+
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
|
1854
|
+
const data = this.getContext("2d").getImageData(x, y, 1, 1).data;
|
|
1855
|
+
r = data[0];
|
|
1856
|
+
g = data[1];
|
|
1857
|
+
b = data[2];
|
|
1858
|
+
a = data[3];
|
|
1859
|
+
}
|
|
1860
|
+
switch (output) {
|
|
1861
|
+
case "array":
|
|
1862
|
+
return [r, g, b, a];
|
|
1863
|
+
case "json":
|
|
1864
|
+
return { r, g, b, a };
|
|
1865
|
+
case "string":
|
|
1866
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
1867
|
+
case "integer":
|
|
1868
|
+
default:
|
|
1869
|
+
return rgb2Int(r, g, b, a / 255);
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
defineMethod(
|
|
1873
|
+
HTMLCanvasElement.prototype,
|
|
1874
|
+
"replaceColors",
|
|
1875
|
+
function(replacements) {
|
|
1876
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
1877
|
+
for (const from in replacements) {
|
|
1878
|
+
const [fr, fg, fb] = hex2rgb(from);
|
|
1879
|
+
const [tr, tg, tb] = hex2rgb(replacements[from]);
|
|
1880
|
+
lookup.set(rgb2Int(fr, fg, fb), [tr, tg, tb]);
|
|
1881
|
+
}
|
|
1882
|
+
const context = this.getContext("2d");
|
|
1883
|
+
const image = context.getImageData(0, 0, this.width, this.height);
|
|
1884
|
+
const { data } = image;
|
|
1885
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1886
|
+
if (data[i + 3] === 0) {
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
const replacement = lookup.get(
|
|
1890
|
+
rgb2Int(data[i], data[i + 1], data[i + 2])
|
|
1891
|
+
);
|
|
1892
|
+
if (replacement !== void 0) {
|
|
1893
|
+
data[i] = replacement[0];
|
|
1894
|
+
data[i + 1] = replacement[1];
|
|
1895
|
+
data[i + 2] = replacement[2];
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
context.putImageData(image, 0, 0);
|
|
1899
|
+
return this;
|
|
1900
|
+
}
|
|
1901
|
+
);
|
|
1902
|
+
defineMethod(
|
|
1903
|
+
HTMLCanvasElement.prototype,
|
|
1904
|
+
"rotateBy",
|
|
1905
|
+
function(radians) {
|
|
1906
|
+
const diam = Math.ceil(
|
|
1907
|
+
Math.sqrt(this.width * this.width + this.height * this.height)
|
|
1908
|
+
);
|
|
1909
|
+
const cc = createNewCanvas(diam, diam);
|
|
1910
|
+
cc.context.translate(diam * 0.5, diam * 0.5);
|
|
1911
|
+
cc.context.rotate(radians);
|
|
1912
|
+
cc.context.drawImage(this, -this.width * 0.5, -this.height * 0.5);
|
|
1913
|
+
cc.context.translate(-diam * 0.5, -diam * 0.5);
|
|
1914
|
+
return cc.canvas;
|
|
1915
|
+
}
|
|
1916
|
+
);
|
|
1917
|
+
defineMethod(
|
|
1918
|
+
HTMLCanvasElement.prototype,
|
|
1919
|
+
"rotateByAligned",
|
|
1920
|
+
function(radians) {
|
|
1921
|
+
const cc = createNewCanvas(this.width, this.height);
|
|
1922
|
+
cc.context.translate(this.width * 0.5, this.height * 0.5);
|
|
1923
|
+
cc.context.rotate(radians);
|
|
1924
|
+
cc.context.translate(-this.width * 0.5, -this.height * 0.5);
|
|
1925
|
+
cc.context.drawImage(this, 0, 0);
|
|
1926
|
+
return cc.canvas;
|
|
1927
|
+
}
|
|
1928
|
+
);
|
|
1929
|
+
defineMethod(
|
|
1930
|
+
HTMLCanvasElement.prototype,
|
|
1931
|
+
"autoCrop",
|
|
1932
|
+
function() {
|
|
1933
|
+
const topLeft = {
|
|
1934
|
+
x: this.width,
|
|
1935
|
+
y: this.height,
|
|
1936
|
+
update(x, y) {
|
|
1937
|
+
this.x = Math.min(this.x, x);
|
|
1938
|
+
this.y = Math.min(this.y, y);
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
const bottomRight = {
|
|
1942
|
+
x: 0,
|
|
1943
|
+
y: 0,
|
|
1944
|
+
update(x, y) {
|
|
1945
|
+
this.x = Math.max(this.x, x);
|
|
1946
|
+
this.y = Math.max(this.y, y);
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
const context = this.getContext("2d");
|
|
1950
|
+
const imageData = context.getImageData(0, 0, this.width, this.height);
|
|
1951
|
+
for (let y = 0; y < this.height; y++) {
|
|
1952
|
+
for (let x = 0; x < this.width; x++) {
|
|
1953
|
+
const alpha = imageData.data[convert2DTo1D(x * 4, y * 4, this.width) + 3];
|
|
1954
|
+
if (alpha !== 0) {
|
|
1955
|
+
topLeft.update(x, y);
|
|
1956
|
+
bottomRight.update(x, y);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
if (topLeft.x > bottomRight.x) {
|
|
1961
|
+
return this.clone();
|
|
1962
|
+
}
|
|
1963
|
+
const width = bottomRight.x - topLeft.x + 1;
|
|
1964
|
+
const height = bottomRight.y - topLeft.y + 1;
|
|
1965
|
+
return this.subImage(topLeft.x, topLeft.y, width, height);
|
|
1966
|
+
}
|
|
1967
|
+
);
|
|
1968
|
+
defineMethod(
|
|
1969
|
+
HTMLCanvasElement.prototype,
|
|
1970
|
+
"scaleBy",
|
|
1971
|
+
function(scaleX = 1, scaleY = scaleX) {
|
|
1972
|
+
if (scaleX <= 0 || scaleY <= 0) {
|
|
1973
|
+
throw new Error(
|
|
1974
|
+
`scaleBy requires positive scale factors, got: ${scaleX} x ${scaleY}`
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
const cc = createNewCanvas(this.width * scaleX, this.height * scaleY);
|
|
1978
|
+
cc.context.scale(scaleX, scaleY);
|
|
1979
|
+
cc.context.drawImage(this, 0, 0);
|
|
1980
|
+
return cc.canvas;
|
|
1981
|
+
}
|
|
1982
|
+
);
|
|
1983
|
+
defineMethod(
|
|
1984
|
+
HTMLCanvasElement.prototype,
|
|
1985
|
+
"resize",
|
|
1986
|
+
function(size, isWidth = true) {
|
|
1987
|
+
if (isWidth) {
|
|
1988
|
+
return this.scaleBy(size / this.width);
|
|
1989
|
+
}
|
|
1990
|
+
return this.scaleBy(size / this.height);
|
|
1991
|
+
}
|
|
1992
|
+
);
|
|
1993
|
+
defineMethod(
|
|
1994
|
+
HTMLCanvasElement.prototype,
|
|
1995
|
+
"flipX",
|
|
1996
|
+
function(offsetX = 0) {
|
|
1997
|
+
const cc = createNewCanvas(this.width, this.height);
|
|
1998
|
+
cc.context.translate(this.width + offsetX, 0);
|
|
1999
|
+
cc.context.scale(-1, 1);
|
|
2000
|
+
cc.context.drawImage(this, 0, 0);
|
|
2001
|
+
return cc.canvas;
|
|
2002
|
+
}
|
|
2003
|
+
);
|
|
2004
|
+
defineMethod(
|
|
2005
|
+
HTMLCanvasElement.prototype,
|
|
2006
|
+
"flipY",
|
|
2007
|
+
function(offsetY = 0) {
|
|
2008
|
+
const cc = createNewCanvas(this.width, this.height);
|
|
2009
|
+
cc.context.translate(0, this.height + offsetY);
|
|
2010
|
+
cc.context.scale(1, -1);
|
|
2011
|
+
cc.context.drawImage(this, 0, 0);
|
|
2012
|
+
return cc.canvas;
|
|
2013
|
+
}
|
|
2014
|
+
);
|
|
2015
|
+
defineMethod(
|
|
2016
|
+
HTMLCanvasElement.prototype,
|
|
2017
|
+
"subImage",
|
|
2018
|
+
function(x, y, w, h) {
|
|
2019
|
+
if (w === void 0) {
|
|
2020
|
+
w = this.width;
|
|
2021
|
+
}
|
|
2022
|
+
if (h === void 0) {
|
|
2023
|
+
h = this.height;
|
|
2024
|
+
}
|
|
2025
|
+
const cc = createNewCanvas(w, h);
|
|
2026
|
+
cc.context.drawImage(this, x, y, w, h, 0, 0, w, h);
|
|
2027
|
+
return cc.canvas;
|
|
2028
|
+
}
|
|
2029
|
+
);
|
|
2030
|
+
defineMethod(
|
|
2031
|
+
HTMLCanvasElement.prototype,
|
|
2032
|
+
"clone",
|
|
2033
|
+
function() {
|
|
2034
|
+
const cc = createNewCanvas(this.width, this.height);
|
|
2035
|
+
cc.canvas.id = this.id;
|
|
2036
|
+
cc.context.drawImage(this, 0, 0);
|
|
2037
|
+
for (const key in this.dataset) {
|
|
2038
|
+
cc.canvas.dataset[key] = this.dataset[key];
|
|
2039
|
+
}
|
|
2040
|
+
return cc.canvas;
|
|
2041
|
+
}
|
|
2042
|
+
);
|
|
2043
|
+
defineMethod(
|
|
2044
|
+
HTMLCanvasElement.prototype,
|
|
2045
|
+
"toImage",
|
|
2046
|
+
function() {
|
|
2047
|
+
return loadImage(this.toDataURL());
|
|
2048
|
+
}
|
|
2049
|
+
);
|
|
2050
|
+
|
|
2051
|
+
// src/utilities/Canvas.ts
|
|
2052
|
+
function createNewCanvas(width, height, antialias = Settings.antialias) {
|
|
2053
|
+
const canvas = document.createElement("canvas");
|
|
2054
|
+
canvas.width = width;
|
|
2055
|
+
canvas.height = height;
|
|
2056
|
+
const context = canvas.getContext("2d");
|
|
2057
|
+
context.imageSmoothingEnabled = antialias;
|
|
2058
|
+
return {
|
|
2059
|
+
canvas,
|
|
2060
|
+
context
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
function getCanvasConstruct(selector) {
|
|
2064
|
+
const canvas = getElement(selector);
|
|
2065
|
+
const context = canvas.getContext("2d");
|
|
2066
|
+
return {
|
|
2067
|
+
canvas,
|
|
2068
|
+
context
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
function applyFilterOnCanvas(image, filter, width = image.width, height = image.height) {
|
|
2072
|
+
const cc = createNewCanvas(width, height);
|
|
2073
|
+
cc.context.filter = filter;
|
|
2074
|
+
cc.context.drawImage(image, 0, 0);
|
|
2075
|
+
cc.context.filter = "none";
|
|
2076
|
+
return cc.canvas;
|
|
2077
|
+
}
|
|
2078
|
+
function rotateHue(image, hue, width, height) {
|
|
2079
|
+
return applyFilterOnCanvas(
|
|
2080
|
+
image,
|
|
2081
|
+
"hue-rotate(" + hue + "deg)",
|
|
2082
|
+
width,
|
|
2083
|
+
height
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
function changeColor(context, oriImg, newColor) {
|
|
2087
|
+
context.save();
|
|
2088
|
+
context.clearRect(0, 0, oriImg.width, oriImg.height);
|
|
2089
|
+
context.globalCompositeOperation = "source-over";
|
|
2090
|
+
context.drawImage(oriImg, 0, 0, oriImg.width, oriImg.height);
|
|
2091
|
+
context.globalCompositeOperation = "color";
|
|
2092
|
+
context.fillStyle = newColor;
|
|
2093
|
+
context.fillRect(0, 0, oriImg.width, oriImg.height);
|
|
2094
|
+
context.globalCompositeOperation = "destination-in";
|
|
2095
|
+
context.drawImage(oriImg, 0, 0, oriImg.width, oriImg.height);
|
|
2096
|
+
context.restore();
|
|
2097
|
+
}
|
|
2098
|
+
function splitSpriteSheet(img, elementsX, elementsY) {
|
|
2099
|
+
if (img.width % elementsX !== 0 || img.height % elementsY !== 0) {
|
|
2100
|
+
throw new Error(
|
|
2101
|
+
`SpriteSheet doesn't divide evenly: ${img.width}x${img.height} / ${elementsX}x${elementsY}`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
const sizeX = img.width / elementsX;
|
|
2105
|
+
const sizeY = img.height / elementsY;
|
|
2106
|
+
const sprites = [];
|
|
2107
|
+
Array.from({ length: elementsY }).forEach((_, row) => {
|
|
2108
|
+
Array.from({ length: elementsX }).forEach((_2, col) => {
|
|
2109
|
+
sprites.push(img.subImage(col * sizeX, row * sizeY, sizeX, sizeY));
|
|
2110
|
+
});
|
|
2111
|
+
});
|
|
2112
|
+
return sprites;
|
|
2113
|
+
}
|
|
2114
|
+
function getUsedColors(image, pixelAmount = 1, removeLowerThan = 0, removeHigherThan = 0) {
|
|
2115
|
+
const data = image.getContext("2d").getImageData(0, 0, image.width, image.height).data;
|
|
2116
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2117
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
2118
|
+
const key = rgb2Int(data[i], data[i + 1], data[i + 2]);
|
|
2119
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2120
|
+
}
|
|
2121
|
+
if (pixelAmount < 1 || removeLowerThan > 0 || removeHigherThan > 0) {
|
|
2122
|
+
counts.forEach((value, key) => {
|
|
2123
|
+
const newAmount = value * pixelAmount | 0;
|
|
2124
|
+
if (newAmount === 0 || removeLowerThan > 0 && newAmount < removeLowerThan || removeHigherThan > 0 && newAmount > removeHigherThan) {
|
|
2125
|
+
counts.delete(key);
|
|
2126
|
+
} else {
|
|
2127
|
+
counts.set(key, newAmount);
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
const result = /* @__PURE__ */ new Map();
|
|
2132
|
+
counts.forEach((count, rgbInt) => {
|
|
2133
|
+
result.set("#" + (16777216 + rgbInt).toString(16).slice(1), count);
|
|
2134
|
+
});
|
|
2135
|
+
return result;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// src/content/Animator.ts
|
|
2139
|
+
var _Animator = class _Animator {
|
|
2140
|
+
/**
|
|
2141
|
+
* We store rendered images in a cache.
|
|
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.
|
|
2147
|
+
*/
|
|
2148
|
+
constructor(entity, namespace) {
|
|
2149
|
+
__publicField(this, "active", true);
|
|
2150
|
+
__publicField(this, "image");
|
|
2151
|
+
__publicField(this, "imageId", 0);
|
|
2152
|
+
__publicField(this, "lookLeft", false);
|
|
2153
|
+
__publicField(this, "onEnd");
|
|
2154
|
+
__publicField(this, "size", new Vec2());
|
|
2155
|
+
__publicField(this, "animations", []);
|
|
2156
|
+
__publicField(this, "currentAnimation", 0);
|
|
2157
|
+
__publicField(this, "entity");
|
|
2158
|
+
__publicField(this, "lastPlayed");
|
|
2159
|
+
__publicField(this, "namespace");
|
|
2160
|
+
__publicField(this, "onFrame");
|
|
2161
|
+
__publicField(this, "playVersion", 0);
|
|
2162
|
+
__publicField(this, "timer", 0);
|
|
2163
|
+
this.entity = entity;
|
|
2164
|
+
this.namespace = namespace;
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Drop cached rendered sprites. Pass a namespace to evict only that prefix; omit to clear all.
|
|
2168
|
+
*/
|
|
2169
|
+
static clearSpriteCache(namespace) {
|
|
2170
|
+
if (namespace === void 0) {
|
|
2171
|
+
_Animator.spriteCache.clear();
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const prefix = `${namespace}.`;
|
|
2175
|
+
_Animator.spriteCache.forEach((_, key) => {
|
|
2176
|
+
if (key.startsWith(prefix)) {
|
|
2177
|
+
_Animator.spriteCache.delete(key);
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
get current() {
|
|
2182
|
+
return this.animations[this.currentAnimation];
|
|
2183
|
+
}
|
|
2184
|
+
draw(context, offset = new Vec2()) {
|
|
2185
|
+
context.drawImage(
|
|
2186
|
+
this.image,
|
|
2187
|
+
this.entity.pos.x + offset.x - this.size.x,
|
|
2188
|
+
this.entity.pos.y + offset.y
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
update(dt) {
|
|
2192
|
+
if (!this.active) {
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
this.timer += dt;
|
|
2196
|
+
if (this.timer <= this.current.timing) {
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
this.timer -= this.current.timing;
|
|
2200
|
+
this.imageId++;
|
|
2201
|
+
if (this.imageId >= this.current.sprites.length) {
|
|
2202
|
+
const onEnd = this.onEnd;
|
|
2203
|
+
this.onEnd = void 0;
|
|
2204
|
+
this.onFrame = void 0;
|
|
2205
|
+
this.imageId = 0;
|
|
2206
|
+
const versionBefore = this.playVersion;
|
|
2207
|
+
onEnd?.();
|
|
2208
|
+
if (this.playVersion !== versionBefore) {
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (this.lastPlayed) {
|
|
2212
|
+
this.play(this.lastPlayed);
|
|
2213
|
+
this.lastPlayed = void 0;
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
if (this.current.sprites.length === 1) {
|
|
2217
|
+
this.active = false;
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
if (this.onFrame?.[this.imageId]) {
|
|
2222
|
+
const cb = this.onFrame[this.imageId];
|
|
2223
|
+
delete this.onFrame[this.imageId];
|
|
2224
|
+
const versionBefore = this.playVersion;
|
|
2225
|
+
cb();
|
|
2226
|
+
if (this.playVersion !== versionBefore) {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
if (this.active) {
|
|
2231
|
+
this.setImage();
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
add(name, sprites, timing, defaultAnim = false) {
|
|
2235
|
+
if (defaultAnim && this.animations.some((anim) => anim.default)) {
|
|
2236
|
+
console.error("Only one default animation allowed!");
|
|
2237
|
+
}
|
|
2238
|
+
if (this.animations.some((anim) => anim.name === name)) {
|
|
2239
|
+
console.error("Duplicate animation name!");
|
|
2240
|
+
}
|
|
2241
|
+
this.animations.push({
|
|
2242
|
+
default: defaultAnim,
|
|
2243
|
+
name,
|
|
2244
|
+
sprites,
|
|
2245
|
+
timing
|
|
2246
|
+
});
|
|
2247
|
+
if (defaultAnim) {
|
|
2248
|
+
this.play(name);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
addAnimation(anim, defaultAnim = false) {
|
|
2252
|
+
this.add(
|
|
2253
|
+
anim.name,
|
|
2254
|
+
anim.sprites,
|
|
2255
|
+
anim.timing,
|
|
2256
|
+
anim.default || defaultAnim
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
drawRotated(context, angle, offset = new Vec2()) {
|
|
2260
|
+
const x = this.entity.pos.x + offset.x;
|
|
2261
|
+
const y = this.entity.pos.y + offset.y;
|
|
2262
|
+
const w = this.image.width * 0.75;
|
|
2263
|
+
const h = this.image.height * 0.5;
|
|
2264
|
+
context.setTransform(1, 0, 0, 1, x + w - this.size.x, y + h);
|
|
2265
|
+
context.rotate(angle);
|
|
2266
|
+
context.drawImage(this.image, -w, -h);
|
|
2267
|
+
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
2268
|
+
}
|
|
2269
|
+
play(name, onEnd, onFrame) {
|
|
2270
|
+
const index = this.animations.findIndex(
|
|
2271
|
+
(anim) => anim.name === name
|
|
2272
|
+
);
|
|
2273
|
+
if (index < 0) {
|
|
2274
|
+
throw new Error(`Animator.play: animation "${name}" not found`);
|
|
2275
|
+
}
|
|
2276
|
+
this.playVersion++;
|
|
2277
|
+
const myVersion = this.playVersion;
|
|
2278
|
+
this.imageId = 0;
|
|
2279
|
+
this.timer = 0;
|
|
2280
|
+
this.currentAnimation = index;
|
|
2281
|
+
this.setImage();
|
|
2282
|
+
const prevOnEnd = this.onEnd;
|
|
2283
|
+
this.onEnd = onEnd;
|
|
2284
|
+
this.onFrame = onFrame;
|
|
2285
|
+
prevOnEnd?.();
|
|
2286
|
+
if (this.playVersion !== myVersion) {
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
this.active = true;
|
|
2290
|
+
}
|
|
2291
|
+
playIfNot(name, onEnd, onFrame) {
|
|
2292
|
+
if (!this.isPlaying(name)) {
|
|
2293
|
+
this.play(name, onEnd, onFrame);
|
|
2294
|
+
return true;
|
|
2295
|
+
}
|
|
2296
|
+
return false;
|
|
2297
|
+
}
|
|
2298
|
+
playNextOnce(name) {
|
|
2299
|
+
this.lastPlayed = name;
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Play `name` once, then return to the previously-playing animation. Calling with the currently-playing name re-loops it indefinitely (lastPlayed restores to itself).
|
|
2303
|
+
*/
|
|
2304
|
+
playOnce(name, onEnd, onFrame) {
|
|
2305
|
+
this.lastPlayed = this.current?.name;
|
|
2306
|
+
this.play(name, onEnd, onFrame);
|
|
2307
|
+
}
|
|
2308
|
+
randomTimer() {
|
|
2309
|
+
this.timer = randomBetweenFloat(0, this.current.timing);
|
|
2310
|
+
}
|
|
2311
|
+
removeAllAnimations() {
|
|
2312
|
+
this.animations.length = 0;
|
|
2313
|
+
this.active = true;
|
|
2314
|
+
this.onEnd = void 0;
|
|
2315
|
+
this.onFrame = void 0;
|
|
2316
|
+
_Animator.clearSpriteCache(this.namespace);
|
|
2317
|
+
}
|
|
2318
|
+
reset() {
|
|
2319
|
+
const defaultAnim = this.animations.find(
|
|
2320
|
+
(anim) => anim.default
|
|
2321
|
+
);
|
|
2322
|
+
if (defaultAnim) {
|
|
2323
|
+
this.play(defaultAnim.name);
|
|
2324
|
+
this.active = true;
|
|
2325
|
+
} else {
|
|
2326
|
+
this.active = false;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
isPlaying(name) {
|
|
2330
|
+
return this.current && this.current.name === name;
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Update `image` and `size` from the current sprite. Assumes uniform sprite size within an animation. Caches rendered canvases per (animation, frame, lookLeft) — if you mutate `entity.flipX` after first render, call `removeAllAnimations()` or recreate the Animator to invalidate.
|
|
2334
|
+
*/
|
|
2335
|
+
setImage() {
|
|
2336
|
+
const animation = this.current;
|
|
2337
|
+
const sprite = animation.sprites[this.imageId];
|
|
2338
|
+
this.size.set(sprite.width, sprite.height);
|
|
2339
|
+
if (this.entity.flipX === void 0) {
|
|
2340
|
+
this.entity.flipX = this.size.x;
|
|
2341
|
+
}
|
|
2342
|
+
const key = `${this.namespace}.${animation.name}`;
|
|
2343
|
+
let cache = _Animator.spriteCache.get(key);
|
|
2344
|
+
if (!cache) {
|
|
2345
|
+
cache = { unflipped: [], flipped: [] };
|
|
2346
|
+
_Animator.spriteCache.set(key, cache);
|
|
2347
|
+
}
|
|
2348
|
+
const bucket = this.lookLeft ? cache.flipped : cache.unflipped;
|
|
2349
|
+
if (!bucket[this.imageId]) {
|
|
2350
|
+
const cc = createNewCanvas(this.size.x * 2, this.size.y);
|
|
2351
|
+
cc.context.translate(
|
|
2352
|
+
this.size.x + (this.lookLeft ? this.entity.flipX : 0),
|
|
2353
|
+
0
|
|
2354
|
+
);
|
|
2355
|
+
if (this.lookLeft) {
|
|
2356
|
+
cc.context.scale(-1, 1);
|
|
2357
|
+
}
|
|
2358
|
+
cc.context.drawImage(sprite, 0, 0);
|
|
2359
|
+
bucket[this.imageId] = cc.canvas;
|
|
2360
|
+
}
|
|
2361
|
+
this.image = bucket[this.imageId];
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
__publicField(_Animator, "spriteCache", /* @__PURE__ */ new Map());
|
|
2365
|
+
var Animator = _Animator;
|
|
2366
|
+
|
|
2367
|
+
// src/content/Particle.ts
|
|
2368
|
+
var Particle = class {
|
|
2369
|
+
constructor(pos, color, size = 2) {
|
|
2370
|
+
__publicField(this, "color");
|
|
2371
|
+
__publicField(this, "lifetime", 0);
|
|
2372
|
+
__publicField(this, "maxLifeTime");
|
|
2373
|
+
__publicField(this, "pos");
|
|
2374
|
+
__publicField(this, "size");
|
|
2375
|
+
__publicField(this, "vel");
|
|
2376
|
+
__publicField(this, "_rect");
|
|
2377
|
+
this.pos = pos.clone();
|
|
2378
|
+
this.color = color;
|
|
2379
|
+
this.size = size;
|
|
2380
|
+
this._rect = pos.toRectAddSize(size);
|
|
2381
|
+
this.vel = Vec2.fromAngle(
|
|
2382
|
+
random2Pi(),
|
|
2383
|
+
randomBetweenInt(50, 150),
|
|
2384
|
+
randomBetweenInt(50, 150)
|
|
2385
|
+
);
|
|
2386
|
+
this.maxLifeTime = 0.5 + Math.random();
|
|
2387
|
+
}
|
|
2388
|
+
get alive() {
|
|
2389
|
+
return this.lifetime < this.maxLifeTime;
|
|
2390
|
+
}
|
|
2391
|
+
get rect() {
|
|
2392
|
+
return this._rect;
|
|
2393
|
+
}
|
|
2394
|
+
draw(context, offset = new Vec2()) {
|
|
2395
|
+
context.fillStyle = this.color;
|
|
2396
|
+
context.drawCircle(
|
|
2397
|
+
{
|
|
2398
|
+
x: this.pos.x + offset.x,
|
|
2399
|
+
y: this.pos.y + offset.y
|
|
2400
|
+
},
|
|
2401
|
+
this.size,
|
|
2402
|
+
"fill"
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
update(dt) {
|
|
2406
|
+
this.lifetime += dt;
|
|
2407
|
+
this.pos.x += this.vel.x * dt;
|
|
2408
|
+
this.pos.y += this.vel.y * dt;
|
|
2409
|
+
this._rect.set(this.pos.x, this.pos.y);
|
|
2410
|
+
}
|
|
2411
|
+
resetLifetime() {
|
|
2412
|
+
this.lifetime -= this.maxLifeTime;
|
|
2413
|
+
}
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
// src/content/Projectile.ts
|
|
2417
|
+
var Projectile = class {
|
|
2418
|
+
constructor(pos, image, vel = new Vec2()) {
|
|
2419
|
+
__publicField(this, "maxLifetime", Infinity);
|
|
2420
|
+
__publicField(this, "payload");
|
|
2421
|
+
__publicField(this, "speed", 1200);
|
|
2422
|
+
__publicField(this, "image");
|
|
2423
|
+
__publicField(this, "lifetime", 0);
|
|
2424
|
+
__publicField(this, "pos");
|
|
2425
|
+
__publicField(this, "rotation", 0);
|
|
2426
|
+
__publicField(this, "vel");
|
|
2427
|
+
__publicField(this, "_rect");
|
|
2428
|
+
__publicField(this, "originalImage");
|
|
2429
|
+
this.pos = pos.clone();
|
|
2430
|
+
this.originalImage = image;
|
|
2431
|
+
this.vel = vel.clone();
|
|
2432
|
+
this._rect = pos.toRectAddSize(image.width, image.height);
|
|
2433
|
+
this.rebuildRotation();
|
|
2434
|
+
}
|
|
2435
|
+
get alive() {
|
|
2436
|
+
return this.lifetime < this.maxLifetime;
|
|
2437
|
+
}
|
|
2438
|
+
get rect() {
|
|
2439
|
+
return this._rect;
|
|
2440
|
+
}
|
|
2441
|
+
draw(context, offset = new Vec2()) {
|
|
2442
|
+
context.drawImage(
|
|
2443
|
+
this.image,
|
|
2444
|
+
this.pos.x + offset.x,
|
|
2445
|
+
this.pos.y + offset.y
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
update(dt) {
|
|
2449
|
+
this.lifetime += dt;
|
|
2450
|
+
this.pos.x += this.vel.x * this.speed * dt;
|
|
2451
|
+
this.pos.y += this.vel.y * this.speed * dt;
|
|
2452
|
+
this._rect.set(this.pos.x, this.pos.y);
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Allocates a fresh rotated canvas on each call — no internal cache.
|
|
2456
|
+
* Caching by quantized rotation could be a feature when projectiles need to re-aim every tick (homing/seeking).
|
|
2457
|
+
*/
|
|
2458
|
+
rebuildRotation() {
|
|
2459
|
+
this.rotation = Math.atan2(this.vel.y, this.vel.x);
|
|
2460
|
+
this.image = this.originalImage.rotateBy(this.rotation);
|
|
2461
|
+
this._rect.w = this.image.width;
|
|
2462
|
+
this._rect.h = this.image.height;
|
|
2463
|
+
}
|
|
2464
|
+
remove() {
|
|
2465
|
+
this.lifetime = this.maxLifetime * 2;
|
|
2466
|
+
}
|
|
2467
|
+
};
|
|
2468
|
+
|
|
2469
|
+
// src/core/CanvasManager.ts
|
|
2470
|
+
var CANVAS_TYPES = {
|
|
2471
|
+
ANY: /* @__PURE__ */ Symbol("any"),
|
|
2472
|
+
DEFAULT: /* @__PURE__ */ Symbol("default"),
|
|
2473
|
+
BACKGROUND: /* @__PURE__ */ Symbol("background"),
|
|
2474
|
+
MAIN: /* @__PURE__ */ Symbol("main")
|
|
2475
|
+
};
|
|
2476
|
+
var CanvasManager = class {
|
|
2477
|
+
constructor() {
|
|
2478
|
+
__publicField(this, "canvasBoundingClientRect");
|
|
2479
|
+
__publicField(this, "canvasHolder", {});
|
|
2480
|
+
__publicField(this, "ratio", 1);
|
|
2481
|
+
__publicField(this, "resizedSize", new Vec2());
|
|
2482
|
+
__publicField(this, "mainHolder");
|
|
2483
|
+
}
|
|
2484
|
+
get canvas() {
|
|
2485
|
+
return this.mainHolder.canvas;
|
|
2486
|
+
}
|
|
2487
|
+
get canvasContext() {
|
|
2488
|
+
return this.mainHolder.context;
|
|
2489
|
+
}
|
|
2490
|
+
get height() {
|
|
2491
|
+
return this.canvas.height;
|
|
2492
|
+
}
|
|
2493
|
+
set height(height) {
|
|
2494
|
+
this.canvas.height = height;
|
|
2495
|
+
}
|
|
2496
|
+
get size() {
|
|
2497
|
+
return new Vec2(this.width, this.height);
|
|
2498
|
+
}
|
|
2499
|
+
get width() {
|
|
2500
|
+
return this.canvas.width;
|
|
2501
|
+
}
|
|
2502
|
+
set width(width) {
|
|
2503
|
+
this.canvas.width = width;
|
|
2504
|
+
}
|
|
2505
|
+
finishSetup() {
|
|
2506
|
+
if (this.mainHolder) {
|
|
2507
|
+
throw new Error("Already set up.");
|
|
2508
|
+
}
|
|
2509
|
+
const mainCanvas = Object.values(this.canvasHolder).filter(
|
|
2510
|
+
(holder) => holder.type === CANVAS_TYPES.MAIN
|
|
2511
|
+
);
|
|
2512
|
+
if (mainCanvas.length === 0) {
|
|
2513
|
+
throw new Error("No main canvas defined!");
|
|
2514
|
+
}
|
|
2515
|
+
if (mainCanvas.length > 1) {
|
|
2516
|
+
throw new Error("Multiple main canvas defined!");
|
|
2517
|
+
}
|
|
2518
|
+
this.mainHolder = mainCanvas[0];
|
|
2519
|
+
if (this.width === 0 || this.height === 0) {
|
|
2520
|
+
throw new Error("Main canvas has zero width or height.");
|
|
2521
|
+
}
|
|
2522
|
+
this.canvasBoundingClientRect = this.canvas.getBoundingClientRect();
|
|
2523
|
+
if (Settings.enableResize) {
|
|
2524
|
+
EventSystem.addEventListener("resized", () => this.resize());
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
resize() {
|
|
2528
|
+
const windowRatio = window.innerHeight / window.innerWidth;
|
|
2529
|
+
Object.values(this.canvasHolder).forEach((ch) => {
|
|
2530
|
+
if (!ch.resize) {
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
const canvasRatio = ch.canvas.height / ch.canvas.width;
|
|
2534
|
+
let width;
|
|
2535
|
+
let height;
|
|
2536
|
+
if (windowRatio < canvasRatio) {
|
|
2537
|
+
height = window.innerHeight;
|
|
2538
|
+
width = height / canvasRatio;
|
|
2539
|
+
} else {
|
|
2540
|
+
width = window.innerWidth;
|
|
2541
|
+
height = width * canvasRatio;
|
|
2542
|
+
}
|
|
2543
|
+
if (ch.canvas === this.canvas) {
|
|
2544
|
+
this.resizedSize = new Vec2(width, height);
|
|
2545
|
+
this.ratio = width / this.width;
|
|
2546
|
+
}
|
|
2547
|
+
ch.canvas.style.width = width + "px";
|
|
2548
|
+
ch.canvas.style.height = height + "px";
|
|
2549
|
+
});
|
|
2550
|
+
this.canvasBoundingClientRect = this.canvas.getBoundingClientRect();
|
|
2551
|
+
}
|
|
2552
|
+
setFontSize(size, font = Settings.font) {
|
|
2553
|
+
this.canvasContext.font = `${size}px "${font}"`;
|
|
2554
|
+
}
|
|
2555
|
+
setupCanvas(canvasType, selector, resize = true) {
|
|
2556
|
+
if (!document.querySelector(selector)) {
|
|
2557
|
+
throw new Error("Canvas '" + selector + "' does not exist!");
|
|
2558
|
+
}
|
|
2559
|
+
if (this.canvasHolder[selector]) {
|
|
2560
|
+
throw new Error(`Canvas "${selector}" was already registered!`);
|
|
2561
|
+
}
|
|
2562
|
+
const newCanvas = Object.assign(
|
|
2563
|
+
{},
|
|
2564
|
+
getCanvasConstruct(selector),
|
|
2565
|
+
{
|
|
2566
|
+
id: selector,
|
|
2567
|
+
resize,
|
|
2568
|
+
type: canvasType
|
|
2569
|
+
}
|
|
2570
|
+
);
|
|
2571
|
+
newCanvas.context.fillStyle = "white";
|
|
2572
|
+
newCanvas.context.strokeStyle = "white";
|
|
2573
|
+
newCanvas.context.font = "12px Arial";
|
|
2574
|
+
this.canvasHolder[selector] = newCanvas;
|
|
2575
|
+
return newCanvas;
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
|
|
2579
|
+
// src/core/Gameloop.ts
|
|
2580
|
+
var MAX_DT_SECONDS = 0.25;
|
|
2581
|
+
var MAX_STEPS_PER_FRAME = 5;
|
|
2582
|
+
var Gameloop = class {
|
|
2583
|
+
constructor(game) {
|
|
2584
|
+
__publicField(this, "levelTime", 0);
|
|
2585
|
+
__publicField(this, "_isLooping", false);
|
|
2586
|
+
__publicField(this, "accumulator", 0);
|
|
2587
|
+
__publicField(this, "game");
|
|
2588
|
+
__publicField(this, "stop", false);
|
|
2589
|
+
this.game = game;
|
|
2590
|
+
}
|
|
2591
|
+
get isLooping() {
|
|
2592
|
+
return this._isLooping;
|
|
2593
|
+
}
|
|
2594
|
+
startLoop() {
|
|
2595
|
+
if (this._isLooping && this.stop) {
|
|
2596
|
+
throw new Error(
|
|
2597
|
+
'Gameloop teardown is pending; wait for the "gameloopStopped" event before restarting.'
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
this.stop = false;
|
|
2601
|
+
this.looper();
|
|
2602
|
+
}
|
|
2603
|
+
stopLoop() {
|
|
2604
|
+
this.stop = true;
|
|
2605
|
+
}
|
|
2606
|
+
draw(context) {
|
|
2607
|
+
if (!Settings.doNotClear) {
|
|
2608
|
+
if (Settings.useClearRect) {
|
|
2609
|
+
context.clearRect(
|
|
2610
|
+
0,
|
|
2611
|
+
0,
|
|
2612
|
+
this.game.canman.canvas.width,
|
|
2613
|
+
this.game.canman.canvas.height
|
|
2614
|
+
);
|
|
2615
|
+
} else {
|
|
2616
|
+
context.fillStyle = Settings.backgroundColor;
|
|
2617
|
+
context.fillRect(
|
|
2618
|
+
0,
|
|
2619
|
+
0,
|
|
2620
|
+
this.game.canman.canvas.width,
|
|
2621
|
+
this.game.canman.canvas.height
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
this.game.draw(context);
|
|
2626
|
+
}
|
|
2627
|
+
looper() {
|
|
2628
|
+
if (this._isLooping) {
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
this._isLooping = true;
|
|
2632
|
+
const context = this.game.canman.canvasContext;
|
|
2633
|
+
const stopLoop = rafLoop((dt) => {
|
|
2634
|
+
if (this.stop) {
|
|
2635
|
+
this._isLooping = false;
|
|
2636
|
+
EventSystem.dispatchEvent("gameloopStopped");
|
|
2637
|
+
console.log("Simulation stopped.");
|
|
2638
|
+
stopLoop();
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
if (dt > MAX_DT_SECONDS) {
|
|
2642
|
+
this.accumulator = 0;
|
|
2643
|
+
} else {
|
|
2644
|
+
this.accumulator += dt;
|
|
2645
|
+
}
|
|
2646
|
+
let steps = MAX_STEPS_PER_FRAME;
|
|
2647
|
+
while (steps-- > 0 && this.accumulator >= Settings.fps) {
|
|
2648
|
+
this.game.update(Settings.fps);
|
|
2649
|
+
this.accumulator -= Settings.fps;
|
|
2650
|
+
this.levelTime += Settings.fps * 1e3;
|
|
2651
|
+
}
|
|
2652
|
+
this.draw(context);
|
|
2653
|
+
});
|
|
2654
|
+
console.log("Simulation started.");
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
|
|
2658
|
+
// src/input/Keyboard.ts
|
|
2659
|
+
var KEYBOARD_KEYS = {
|
|
2660
|
+
KEY_0: "Digit0",
|
|
2661
|
+
KEY_1: "Digit1",
|
|
2662
|
+
KEY_2: "Digit2",
|
|
2663
|
+
KEY_3: "Digit3",
|
|
2664
|
+
KEY_4: "Digit4",
|
|
2665
|
+
KEY_5: "Digit5",
|
|
2666
|
+
KEY_6: "Digit6",
|
|
2667
|
+
KEY_7: "Digit7",
|
|
2668
|
+
KEY_8: "Digit8",
|
|
2669
|
+
KEY_9: "Digit9",
|
|
2670
|
+
KEY_A: "KeyA",
|
|
2671
|
+
KEY_B: "KeyB",
|
|
2672
|
+
KEY_C: "KeyC",
|
|
2673
|
+
KEY_D: "KeyD",
|
|
2674
|
+
KEY_DOWN: "ArrowDown",
|
|
2675
|
+
KEY_E: "KeyE",
|
|
2676
|
+
KEY_ENTER: "Enter",
|
|
2677
|
+
KEY_ESCAPE: "Escape",
|
|
2678
|
+
KEY_F: "KeyF",
|
|
2679
|
+
KEY_G: "KeyG",
|
|
2680
|
+
KEY_H: "KeyH",
|
|
2681
|
+
KEY_I: "KeyI",
|
|
2682
|
+
KEY_J: "KeyJ",
|
|
2683
|
+
KEY_K: "KeyK",
|
|
2684
|
+
KEY_L: "KeyL",
|
|
2685
|
+
KEY_LEFT: "ArrowLeft",
|
|
2686
|
+
KEY_M: "KeyM",
|
|
2687
|
+
KEY_N: "KeyN",
|
|
2688
|
+
KEY_O: "KeyO",
|
|
2689
|
+
KEY_P: "KeyP",
|
|
2690
|
+
KEY_Q: "KeyQ",
|
|
2691
|
+
KEY_R: "KeyR",
|
|
2692
|
+
KEY_RIGHT: "ArrowRight",
|
|
2693
|
+
KEY_S: "KeyS",
|
|
2694
|
+
KEY_SPACE: "Space",
|
|
2695
|
+
KEY_T: "KeyT",
|
|
2696
|
+
KEY_TAB: "Tab",
|
|
2697
|
+
KEY_U: "KeyU",
|
|
2698
|
+
KEY_UP: "ArrowUp",
|
|
2699
|
+
KEY_V: "KeyV",
|
|
2700
|
+
KEY_W: "KeyW",
|
|
2701
|
+
KEY_X: "KeyX",
|
|
2702
|
+
KEY_Y: "KeyY",
|
|
2703
|
+
KEY_Z: "KeyZ"
|
|
2704
|
+
};
|
|
2705
|
+
var Keyboard = class {
|
|
2706
|
+
constructor(game) {
|
|
2707
|
+
__publicField(this, "keys", {});
|
|
2708
|
+
const keyEvent = (event) => {
|
|
2709
|
+
const code = event.code;
|
|
2710
|
+
const pressed = event.type === "keydown";
|
|
2711
|
+
this.keys[code] = pressed;
|
|
2712
|
+
if (Settings.debug && code === KEYBOARD_KEYS.KEY_ESCAPE && pressed) {
|
|
2713
|
+
game.gameloop.stopLoop();
|
|
2714
|
+
}
|
|
2715
|
+
EventSystem.dispatchEvent(
|
|
2716
|
+
"inputKeyboard",
|
|
2717
|
+
this.keys,
|
|
2718
|
+
code,
|
|
2719
|
+
pressed
|
|
2720
|
+
);
|
|
2721
|
+
};
|
|
2722
|
+
window.addEventListener("keydown", keyEvent, false);
|
|
2723
|
+
window.addEventListener("keyup", keyEvent, false);
|
|
2724
|
+
window.addEventListener("blur", () => this.reset(), false);
|
|
2725
|
+
EventSystem.addEventListener("gameloopStopped", () => this.reset());
|
|
2726
|
+
}
|
|
2727
|
+
reset() {
|
|
2728
|
+
for (const key in this.keys) {
|
|
2729
|
+
this.keys[key] = false;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
stopPress(code) {
|
|
2733
|
+
this.keys[code] = false;
|
|
2734
|
+
}
|
|
2735
|
+
isPressed(code) {
|
|
2736
|
+
return !!this.keys[code];
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
|
|
2740
|
+
// src/input/Pointer.ts
|
|
2741
|
+
var POINTER_KEYS = {
|
|
2742
|
+
LEFT: 0,
|
|
2743
|
+
MIDDLE: 1,
|
|
2744
|
+
RIGHT: 2,
|
|
2745
|
+
PREV: 3,
|
|
2746
|
+
FORWARD: 4
|
|
2747
|
+
};
|
|
2748
|
+
var Pointer = class {
|
|
2749
|
+
constructor(game) {
|
|
2750
|
+
__publicField(this, "hasMoved", false);
|
|
2751
|
+
__publicField(this, "lastEvent", null);
|
|
2752
|
+
__publicField(this, "posReal", new Vec2());
|
|
2753
|
+
__publicField(this, "posRealLast", new Vec2());
|
|
2754
|
+
__publicField(this, "posScaled", new Vec2());
|
|
2755
|
+
__publicField(this, "posScaledLast", new Vec2());
|
|
2756
|
+
__publicField(this, "pressed", []);
|
|
2757
|
+
__publicField(this, "game");
|
|
2758
|
+
this.game = game;
|
|
2759
|
+
const pointerMoveEvent = (event) => {
|
|
2760
|
+
if (event.target === this.game.canman.canvas) {
|
|
2761
|
+
event.preventDefault();
|
|
2762
|
+
}
|
|
2763
|
+
this.lastEvent = event;
|
|
2764
|
+
this.hasMoved = true;
|
|
2765
|
+
this.update(event);
|
|
2766
|
+
EventSystem.dispatchEvent("inputPointer", this);
|
|
2767
|
+
};
|
|
2768
|
+
const pointerStateChangeEvent = (event) => {
|
|
2769
|
+
if (event.target === this.game.canman.canvas) {
|
|
2770
|
+
event.preventDefault();
|
|
2771
|
+
}
|
|
2772
|
+
this.lastEvent = event;
|
|
2773
|
+
this.pressed[event.button] = event.type === "pointerdown";
|
|
2774
|
+
EventSystem.dispatchEvent("inputPointer", this);
|
|
2775
|
+
};
|
|
2776
|
+
window.addEventListener("pointermove", pointerMoveEvent, false);
|
|
2777
|
+
window.addEventListener("pointerdown", pointerStateChangeEvent, false);
|
|
2778
|
+
window.addEventListener("pointerup", pointerStateChangeEvent, false);
|
|
2779
|
+
window.addEventListener("blur", () => this.reset(), false);
|
|
2780
|
+
document.addEventListener(
|
|
2781
|
+
"contextmenu",
|
|
2782
|
+
(event) => {
|
|
2783
|
+
event.preventDefault();
|
|
2784
|
+
},
|
|
2785
|
+
false
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
reset() {
|
|
2789
|
+
this.pressed.length = 0;
|
|
2790
|
+
}
|
|
2791
|
+
update(event) {
|
|
2792
|
+
this.posRealLast.set(this.posReal.x, this.posReal.y);
|
|
2793
|
+
this.posReal.set(event.clientX, event.clientY);
|
|
2794
|
+
this.posScaledLast.set(this.posScaled.x, this.posScaled.y);
|
|
2795
|
+
this.posScaled.set(
|
|
2796
|
+
clamp(
|
|
2797
|
+
(event.clientX - this.game.canman.canvasBoundingClientRect.left) / this.game.canman.canvasBoundingClientRect.width * this.game.canman.width | 0,
|
|
2798
|
+
0,
|
|
2799
|
+
this.game.canman.width
|
|
2800
|
+
),
|
|
2801
|
+
clamp(
|
|
2802
|
+
(event.clientY - this.game.canman.canvasBoundingClientRect.top) / this.game.canman.canvasBoundingClientRect.height * this.game.canman.height | 0,
|
|
2803
|
+
0,
|
|
2804
|
+
this.game.canman.height
|
|
2805
|
+
)
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
};
|
|
2809
|
+
|
|
2810
|
+
// src/localization/Translator.ts
|
|
2811
|
+
defineMethod(window, "t", function() {
|
|
2812
|
+
throw new Error("Call 'prepareLanguage' first!");
|
|
2813
|
+
});
|
|
2814
|
+
var logMissingKey = throttleByKey((count, key) => {
|
|
2815
|
+
const suffix = count > 1 ? ` (x${count} since last log)` : "";
|
|
2816
|
+
console.warn(`"${key}" has no translation${suffix}`);
|
|
2817
|
+
});
|
|
2818
|
+
var logMissingLanguage = throttleByKey(
|
|
2819
|
+
(count, current, fallback) => {
|
|
2820
|
+
const suffix = count > 1 ? ` (x${count} since last log)` : "";
|
|
2821
|
+
console.warn(
|
|
2822
|
+
`Language "${current}" not found, falling back to "${fallback}"${suffix}`
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
);
|
|
2826
|
+
function prepareLanguage(languages, defaultLanguage = "en") {
|
|
2827
|
+
if (!languages[defaultLanguage]) {
|
|
2828
|
+
throw new Error(
|
|
2829
|
+
`Default language is defined as "${defaultLanguage}" but isn't supplied!`
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
const allKeys = /* @__PURE__ */ new Set();
|
|
2833
|
+
for (const lang in languages) {
|
|
2834
|
+
for (const key in languages[lang]) {
|
|
2835
|
+
allKeys.add(key);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
const sortedKeys = [...allKeys].sort();
|
|
2839
|
+
for (const lang in languages) {
|
|
2840
|
+
sortedKeys.forEach((key) => {
|
|
2841
|
+
if (languages[lang][key] === void 0) {
|
|
2842
|
+
console.error(`"${lang}" misses "${key}"`);
|
|
2843
|
+
}
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
defineMethod(window, "t", function(key) {
|
|
2847
|
+
const currentLang = Settings.localStorage.language;
|
|
2848
|
+
let language = languages[currentLang];
|
|
2849
|
+
if (!language) {
|
|
2850
|
+
logMissingLanguage(currentLang, currentLang, defaultLanguage);
|
|
2851
|
+
language = languages[defaultLanguage];
|
|
2852
|
+
}
|
|
2853
|
+
if (language[key] === void 0) {
|
|
2854
|
+
logMissingKey(key, key);
|
|
2855
|
+
return key;
|
|
2856
|
+
}
|
|
2857
|
+
return language[key];
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
// src/prototypes/Audio.ts
|
|
2862
|
+
defineMethod(
|
|
2863
|
+
HTMLAudioElement.prototype,
|
|
2864
|
+
"clone",
|
|
2865
|
+
function() {
|
|
2866
|
+
const node = this.cloneNode(true);
|
|
2867
|
+
node.volume = this.volume;
|
|
2868
|
+
return node;
|
|
2869
|
+
}
|
|
2870
|
+
);
|
|
2871
|
+
defineMethod(HTMLAudioElement.prototype, "stop", function() {
|
|
2872
|
+
this.pause();
|
|
2873
|
+
this.currentTime = 0;
|
|
2874
|
+
if (this.defaultVolume !== void 0) {
|
|
2875
|
+
this.volume = this.defaultVolume;
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
|
|
2879
|
+
// src/prototypes/CanvasRenderingContext2D.ts
|
|
2880
|
+
defineMethod(
|
|
2881
|
+
CanvasRenderingContext2D.prototype,
|
|
2882
|
+
"fillBar",
|
|
2883
|
+
function(rect, amount, c1 = "white", c2 = "black") {
|
|
2884
|
+
this.fillStyle = c1;
|
|
2885
|
+
this.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
2886
|
+
this.fillStyle = c2;
|
|
2887
|
+
this.fillRect(rect.x, rect.y, rect.w * amount, rect.h);
|
|
2888
|
+
}
|
|
2889
|
+
);
|
|
2890
|
+
defineMethod(
|
|
2891
|
+
CanvasRenderingContext2D.prototype,
|
|
2892
|
+
"fillFramedBar",
|
|
2893
|
+
function(rect, amount = 0.8, padding = 4, colors = ["white", "black", "red"]) {
|
|
2894
|
+
this.fillStyle = colors[0];
|
|
2895
|
+
this.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
2896
|
+
this.fillStyle = colors[1];
|
|
2897
|
+
this.fillRect(
|
|
2898
|
+
rect.x + padding,
|
|
2899
|
+
rect.y + padding,
|
|
2900
|
+
rect.w - padding * 2,
|
|
2901
|
+
rect.h - padding * 2
|
|
2902
|
+
);
|
|
2903
|
+
if (amount > 0) {
|
|
2904
|
+
this.fillStyle = colors[2];
|
|
2905
|
+
this.fillRect(
|
|
2906
|
+
rect.x + padding,
|
|
2907
|
+
rect.y + padding,
|
|
2908
|
+
amount * (rect.w - padding * 2),
|
|
2909
|
+
rect.h - padding * 2
|
|
2910
|
+
);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
);
|
|
2914
|
+
defineMethod(
|
|
2915
|
+
CanvasRenderingContext2D.prototype,
|
|
2916
|
+
"drawCircle",
|
|
2917
|
+
function(vecPos, rad, mode, amount = 1) {
|
|
2918
|
+
this.beginPath();
|
|
2919
|
+
this.arc(vecPos.x, vecPos.y, rad, 0, amount * 2 * Math.PI);
|
|
2920
|
+
this[mode]();
|
|
2921
|
+
}
|
|
2922
|
+
);
|
|
2923
|
+
defineMethod(
|
|
2924
|
+
CanvasRenderingContext2D.prototype,
|
|
2925
|
+
"drawRect",
|
|
2926
|
+
function(...args) {
|
|
2927
|
+
let x;
|
|
2928
|
+
let y;
|
|
2929
|
+
let w;
|
|
2930
|
+
let h;
|
|
2931
|
+
let mode;
|
|
2932
|
+
if (typeof args[0] === "number") {
|
|
2933
|
+
[x, y, w, h, mode] = args;
|
|
2934
|
+
} else {
|
|
2935
|
+
[{ x, y, w, h }, mode] = args;
|
|
2936
|
+
}
|
|
2937
|
+
this.beginPath();
|
|
2938
|
+
this.rect(x, y, w, h);
|
|
2939
|
+
this[mode]();
|
|
2940
|
+
}
|
|
2941
|
+
);
|
|
2942
|
+
defineMethod(
|
|
2943
|
+
CanvasRenderingContext2D.prototype,
|
|
2944
|
+
"drawRoundRect",
|
|
2945
|
+
function(...args) {
|
|
2946
|
+
let x;
|
|
2947
|
+
let y;
|
|
2948
|
+
let w;
|
|
2949
|
+
let h;
|
|
2950
|
+
let mode;
|
|
2951
|
+
let options;
|
|
2952
|
+
if (typeof args[0] === "number") {
|
|
2953
|
+
[x, y, w, h, mode, options] = args;
|
|
2954
|
+
} else {
|
|
2955
|
+
[{ x, y, w, h }, mode, options] = args;
|
|
2956
|
+
}
|
|
2957
|
+
const { padding = 0, radius = 16 } = options ?? {};
|
|
2958
|
+
x += padding;
|
|
2959
|
+
y += padding;
|
|
2960
|
+
w -= padding * 2;
|
|
2961
|
+
h -= padding * 2;
|
|
2962
|
+
this.beginPath();
|
|
2963
|
+
this.roundRect(x, y, w, h, radius);
|
|
2964
|
+
this[mode]();
|
|
2965
|
+
}
|
|
2966
|
+
);
|
|
2967
|
+
defineMethod(
|
|
2968
|
+
CanvasRenderingContext2D.prototype,
|
|
2969
|
+
"strokeDottedRect",
|
|
2970
|
+
function(rect) {
|
|
2971
|
+
this.setLineDash([15, 15]);
|
|
2972
|
+
this.lineWidth = 5;
|
|
2973
|
+
this.beginPath();
|
|
2974
|
+
this.moveTo(rect.x, rect.y);
|
|
2975
|
+
this.lineTo(rect.x + rect.w, rect.y);
|
|
2976
|
+
this.lineTo(rect.x + rect.w, rect.y + rect.h);
|
|
2977
|
+
this.lineTo(rect.x, rect.y + rect.h);
|
|
2978
|
+
this.lineTo(rect.x, rect.y);
|
|
2979
|
+
this.stroke();
|
|
2980
|
+
}
|
|
2981
|
+
);
|
|
2982
|
+
defineMethod(
|
|
2983
|
+
CanvasRenderingContext2D.prototype,
|
|
2984
|
+
"strokeLine",
|
|
2985
|
+
function(x1, y1, x2, y2) {
|
|
2986
|
+
this.beginPath();
|
|
2987
|
+
this.moveTo(x1, y1);
|
|
2988
|
+
this.lineTo(x2, y2);
|
|
2989
|
+
this.stroke();
|
|
2990
|
+
this.closePath();
|
|
2991
|
+
}
|
|
2992
|
+
);
|
|
2993
|
+
defineMethod(
|
|
2994
|
+
CanvasRenderingContext2D.prototype,
|
|
2995
|
+
"drawPolygon",
|
|
2996
|
+
function(sides, rect, mode) {
|
|
2997
|
+
const rad = Math.min(rect.w, rect.h) * 0.5;
|
|
2998
|
+
const Xcenter = rect.x + rect.w * 0.5;
|
|
2999
|
+
const Ycenter = rect.y + rect.h * 0.5;
|
|
3000
|
+
this.beginPath();
|
|
3001
|
+
this.moveTo(Xcenter + rad, Ycenter);
|
|
3002
|
+
for (let i = 1; i <= sides; i++) {
|
|
3003
|
+
this.lineTo(
|
|
3004
|
+
Math.round(Xcenter + rad * Math.cos(i * 2 * Math.PI / sides)),
|
|
3005
|
+
Math.round(Ycenter + rad * Math.sin(i * 2 * Math.PI / sides))
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
this.closePath();
|
|
3009
|
+
this[mode]();
|
|
3010
|
+
}
|
|
3011
|
+
);
|
|
3012
|
+
defineMethod(
|
|
3013
|
+
CanvasRenderingContext2D.prototype,
|
|
3014
|
+
"drawTriangle",
|
|
3015
|
+
function(rect, mode) {
|
|
3016
|
+
this.beginPath();
|
|
3017
|
+
this.moveTo(rect.x, rect.y);
|
|
3018
|
+
this.lineTo(rect.x + rect.w, rect.y);
|
|
3019
|
+
this.lineTo(rect.x + rect.w * 0.5, rect.y + rect.h);
|
|
3020
|
+
this[mode]();
|
|
3021
|
+
}
|
|
3022
|
+
);
|
|
3023
|
+
defineMethod(
|
|
3024
|
+
CanvasRenderingContext2D.prototype,
|
|
3025
|
+
"writeText",
|
|
3026
|
+
function(text, x, y, measureTextOffset = 0.5) {
|
|
3027
|
+
this.fillText(
|
|
3028
|
+
text,
|
|
3029
|
+
x - this.measureText(text).width * measureTextOffset,
|
|
3030
|
+
y
|
|
3031
|
+
);
|
|
3032
|
+
}
|
|
3033
|
+
);
|
|
3034
|
+
defineMethod(
|
|
3035
|
+
CanvasRenderingContext2D.prototype,
|
|
3036
|
+
"writeMultilineText",
|
|
3037
|
+
function(text, x, y, width, lineOffset = 50, maxAttempts = 50) {
|
|
3038
|
+
const words = text.split(" ");
|
|
3039
|
+
let attempts = 0;
|
|
3040
|
+
while (words.length > 0 && attempts < maxAttempts) {
|
|
3041
|
+
let count = words.length;
|
|
3042
|
+
for (let i = 1; i <= words.length; i++) {
|
|
3043
|
+
const textWidth = this.measureText(
|
|
3044
|
+
words.slice(0, i).join(" ")
|
|
3045
|
+
).width;
|
|
3046
|
+
if (textWidth > width) {
|
|
3047
|
+
count = Math.max(1, i - 1);
|
|
3048
|
+
break;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
this.writeText(words.slice(0, count).join(" "), x, y, 0);
|
|
3052
|
+
words.splice(0, count);
|
|
3053
|
+
y += lineOffset;
|
|
3054
|
+
attempts++;
|
|
3055
|
+
}
|
|
3056
|
+
if (attempts >= maxAttempts) {
|
|
3057
|
+
console.error("maxAttempts reached", attempts);
|
|
3058
|
+
}
|
|
3059
|
+
return attempts < maxAttempts;
|
|
3060
|
+
}
|
|
3061
|
+
);
|
|
3062
|
+
defineMethod(
|
|
3063
|
+
CanvasRenderingContext2D.prototype,
|
|
3064
|
+
"drawImageRotated",
|
|
3065
|
+
function(image, x, y, radians) {
|
|
3066
|
+
this.save();
|
|
3067
|
+
this.translate(x + image.width * 0.5, y + image.height * 0.5);
|
|
3068
|
+
this.rotate(radians);
|
|
3069
|
+
this.drawImage(image, -image.width * 0.5, -image.height * 0.5);
|
|
3070
|
+
this.restore();
|
|
3071
|
+
}
|
|
3072
|
+
);
|
|
3073
|
+
defineMethod(
|
|
3074
|
+
CanvasRenderingContext2D.prototype,
|
|
3075
|
+
"generateColor",
|
|
3076
|
+
function(size, color) {
|
|
3077
|
+
const cc = createNewCanvas(size, size);
|
|
3078
|
+
cc.context.strokeStyle = color;
|
|
3079
|
+
cc.context.lineWidth = 6;
|
|
3080
|
+
cc.context.drawRoundRect(0, 0, size, size, "stroke", {
|
|
3081
|
+
padding: cc.context.lineWidth,
|
|
3082
|
+
radius: 15
|
|
3083
|
+
});
|
|
3084
|
+
const data = cc.context.getImageData(0, 0, size, size).data;
|
|
3085
|
+
const allColors = [];
|
|
3086
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
3087
|
+
if (data[i + 3]) {
|
|
3088
|
+
allColors.push([i / 4 % size, i / 4 / size | 0]);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
const colors = [];
|
|
3092
|
+
allColors.forEach((c) => {
|
|
3093
|
+
if (c[0] > size * 0.5) {
|
|
3094
|
+
colors.push(c);
|
|
3095
|
+
}
|
|
3096
|
+
});
|
|
3097
|
+
for (let i = allColors.length - 1; i >= 0; i--) {
|
|
3098
|
+
if (allColors[i][0] <= size * 0.5) {
|
|
3099
|
+
colors.push(allColors[i]);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
const drawPartialRoundRect = (rect, amount, offsetX = 0, offsetY = 0) => {
|
|
3103
|
+
this.drawImage(cc.canvas, rect.x + offsetX, rect.y + offsetY);
|
|
3104
|
+
this.fillStyle = "white";
|
|
3105
|
+
for (let i = 0; i < Math.min(amount * colors.length, colors.length); i++) {
|
|
3106
|
+
this.fillRect(
|
|
3107
|
+
rect.x + colors[i][0] + offsetX,
|
|
3108
|
+
rect.y + colors[i][1] + offsetY,
|
|
3109
|
+
1,
|
|
3110
|
+
1
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
return { colors, image: cc.canvas, drawPartialRoundRect };
|
|
3115
|
+
}
|
|
3116
|
+
);
|
|
3117
|
+
|
|
3118
|
+
// src/prototypes/index.ts
|
|
3119
|
+
defineMethod(
|
|
3120
|
+
HTMLImageElement.prototype,
|
|
3121
|
+
"subImage",
|
|
3122
|
+
HTMLCanvasElement.prototype.subImage
|
|
3123
|
+
);
|
|
3124
|
+
|
|
3125
|
+
// src/core/Game.ts
|
|
3126
|
+
var Game = class {
|
|
3127
|
+
constructor(settingOverrides = {}) {
|
|
3128
|
+
__publicField(this, "canman", new CanvasManager());
|
|
3129
|
+
__publicField(this, "gameloop");
|
|
3130
|
+
__publicField(this, "keyboard");
|
|
3131
|
+
__publicField(this, "pointer");
|
|
3132
|
+
__publicField(this, "initialized", false);
|
|
3133
|
+
Settings.init(settingOverrides, this);
|
|
3134
|
+
history.scrollRestoration = "manual";
|
|
3135
|
+
this.gameloop = new Gameloop(this);
|
|
3136
|
+
this.keyboard = new Keyboard(this);
|
|
3137
|
+
this.pointer = new Pointer(this);
|
|
3138
|
+
}
|
|
3139
|
+
draw(_context) {
|
|
3140
|
+
throw new Error("Override draw function!");
|
|
3141
|
+
}
|
|
3142
|
+
update(_dt) {
|
|
3143
|
+
throw new Error("Override update function!");
|
|
3144
|
+
}
|
|
3145
|
+
async init() {
|
|
3146
|
+
throw new Error(
|
|
3147
|
+
"Override init() and start the game via preInit() \u2014 do not call init() directly."
|
|
3148
|
+
);
|
|
3149
|
+
}
|
|
3150
|
+
async preInit(doInit = true) {
|
|
3151
|
+
if (this.initialized) {
|
|
3152
|
+
throw new Error(
|
|
3153
|
+
"preInit() may only be called once per Game instance."
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
this.initialized = true;
|
|
3157
|
+
this.canman.finishSetup();
|
|
3158
|
+
window.addEventListener(
|
|
3159
|
+
"resize",
|
|
3160
|
+
debounce(() => EventSystem.dispatchEvent("resized"), 250),
|
|
3161
|
+
false
|
|
3162
|
+
);
|
|
3163
|
+
this.gameloop.levelTime = 0;
|
|
3164
|
+
if (doInit) {
|
|
3165
|
+
await this.init();
|
|
3166
|
+
}
|
|
3167
|
+
EventSystem.dispatchEvent("resized");
|
|
3168
|
+
if (Settings.autoloop) {
|
|
3169
|
+
this.gameloop.startLoop();
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
|
|
3174
|
+
// src/effects/Screenshake.ts
|
|
3175
|
+
var SHAKE_TYPES = {
|
|
3176
|
+
NORMAL: {
|
|
3177
|
+
step: 3,
|
|
3178
|
+
update(updateCss, time) {
|
|
3179
|
+
const tr = `rotate(${randomBetweenFloat(-2, 2) * time}deg)`;
|
|
3180
|
+
updateCss("transform", tr);
|
|
3181
|
+
updateCss("webkitTransform", tr);
|
|
3182
|
+
updateCss("filter", `blur(${time * 5}px)`);
|
|
3183
|
+
}
|
|
3184
|
+
},
|
|
3185
|
+
FAST: {
|
|
3186
|
+
step: 15,
|
|
3187
|
+
update(updateCss, time) {
|
|
3188
|
+
updateCss("filter", `blur(${time * 3}px)`);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
var Screenshake = class {
|
|
3193
|
+
constructor(element) {
|
|
3194
|
+
__publicField(this, "isShaking", false);
|
|
3195
|
+
__publicField(this, "shakeType", SHAKE_TYPES.NORMAL);
|
|
3196
|
+
__publicField(this, "style");
|
|
3197
|
+
this.style = element.style;
|
|
3198
|
+
}
|
|
3199
|
+
/** Best-effort style restore: each key the shake writes is snapshotted on first write and rewritten when the shake ends or is disposed. */
|
|
3200
|
+
shake(shakeType = SHAKE_TYPES.NORMAL) {
|
|
3201
|
+
if (this.isShaking) {
|
|
3202
|
+
return null;
|
|
3203
|
+
}
|
|
3204
|
+
this.isShaking = true;
|
|
3205
|
+
this.shakeType = shakeType;
|
|
3206
|
+
let timer = 1;
|
|
3207
|
+
const originalValue = /* @__PURE__ */ new Map();
|
|
3208
|
+
const updateCssProxy = (key, value) => {
|
|
3209
|
+
if (!originalValue.has(key)) {
|
|
3210
|
+
originalValue.set(key, this.style[key]);
|
|
3211
|
+
}
|
|
3212
|
+
this.style[key] = value;
|
|
3213
|
+
};
|
|
3214
|
+
const stopLoop = rafLoop((dt) => {
|
|
3215
|
+
this.shakeType.update(updateCssProxy, timer);
|
|
3216
|
+
timer -= this.shakeType.step * dt;
|
|
3217
|
+
if (timer <= 0) {
|
|
3218
|
+
dispose();
|
|
3219
|
+
}
|
|
3220
|
+
});
|
|
3221
|
+
let alive = true;
|
|
3222
|
+
const dispose = () => {
|
|
3223
|
+
if (!alive) {
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
alive = false;
|
|
3227
|
+
stopLoop();
|
|
3228
|
+
originalValue.forEach((value, key) => {
|
|
3229
|
+
this.style[key] = value;
|
|
3230
|
+
});
|
|
3231
|
+
this.isShaking = false;
|
|
3232
|
+
};
|
|
3233
|
+
return dispose;
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3236
|
+
|
|
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
|
+
// src/input/Controller.ts
|
|
3279
|
+
var CONTROLLER_KEYS = {
|
|
3280
|
+
A: 0,
|
|
3281
|
+
B: 1,
|
|
3282
|
+
X: 2,
|
|
3283
|
+
Y: 3,
|
|
3284
|
+
LB: 4,
|
|
3285
|
+
RB: 5,
|
|
3286
|
+
LT: 6,
|
|
3287
|
+
RT: 7,
|
|
3288
|
+
SELECT: 8,
|
|
3289
|
+
START: 9,
|
|
3290
|
+
LEFT_STICK: 10,
|
|
3291
|
+
RIGHT_STICK: 11,
|
|
3292
|
+
UP: 12,
|
|
3293
|
+
DOWN: 13,
|
|
3294
|
+
LEFT: 14,
|
|
3295
|
+
RIGHT: 15,
|
|
3296
|
+
GUIDE: 16
|
|
3297
|
+
};
|
|
3298
|
+
var AXIS_THRESHOLD = 0.3;
|
|
3299
|
+
var Controller = class {
|
|
3300
|
+
constructor(game) {
|
|
3301
|
+
__publicField(this, "buttons", []);
|
|
3302
|
+
__publicField(this, "cursors", []);
|
|
3303
|
+
__publicField(this, "axes", []);
|
|
3304
|
+
__publicField(this, "index", -1);
|
|
3305
|
+
__publicField(this, "lastTime", 0);
|
|
3306
|
+
if (!("getGamepads" in navigator)) {
|
|
3307
|
+
console.error("Controller not supported!");
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
for (let i = 0; i < 2; i++) {
|
|
3311
|
+
this.cursors.push(new ControllerCursor(this, game, i));
|
|
3312
|
+
}
|
|
3313
|
+
window.addEventListener("gamepadconnected", (event) => {
|
|
3314
|
+
this.index = event.gamepad.index;
|
|
3315
|
+
console.log("Gamepad connected:", event.gamepad.id);
|
|
3316
|
+
EventSystem.dispatchEvent(
|
|
3317
|
+
"inputControllerConnected",
|
|
3318
|
+
event.gamepad
|
|
3319
|
+
);
|
|
3320
|
+
this.vibrate();
|
|
3321
|
+
});
|
|
3322
|
+
window.addEventListener(
|
|
3323
|
+
"gamepaddisconnected",
|
|
3324
|
+
(event) => {
|
|
3325
|
+
if (this.index === event.gamepad.index) {
|
|
3326
|
+
this.index = -1;
|
|
3327
|
+
console.log(
|
|
3328
|
+
"Our Gamepad was disconnected:",
|
|
3329
|
+
event.gamepad.index
|
|
3330
|
+
);
|
|
3331
|
+
EventSystem.dispatchEvent("inputControllerDisconnected");
|
|
3332
|
+
} else {
|
|
3333
|
+
console.log(
|
|
3334
|
+
"Different Gamepad was disconnected:",
|
|
3335
|
+
event.gamepad.index
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
);
|
|
3340
|
+
window.addEventListener("blur", () => this.reset(), false);
|
|
3341
|
+
}
|
|
3342
|
+
draw(context) {
|
|
3343
|
+
if (this.index < 0) {
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
this.cursors.forEach((cursor) => cursor.draw(context));
|
|
3347
|
+
}
|
|
3348
|
+
update(dt) {
|
|
3349
|
+
this.cursors.forEach((cursor) => cursor.update(dt));
|
|
3350
|
+
const gp = this.getGamepad();
|
|
3351
|
+
if (!gp || this.lastTime === gp.timestamp) {
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
this.lastTime = gp.timestamp;
|
|
3355
|
+
this.buttons = gp.buttons.map(
|
|
3356
|
+
(button) => button.pressed
|
|
3357
|
+
);
|
|
3358
|
+
this.axes.length = 0;
|
|
3359
|
+
const axes = gp.axes;
|
|
3360
|
+
for (let i = 0; i + 1 < axes.length; i += 2) {
|
|
3361
|
+
this.axes.push(new Vec2(axes[i], axes[i + 1]));
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
reset() {
|
|
3365
|
+
this.buttons.length = 0;
|
|
3366
|
+
this.axes.length = 0;
|
|
3367
|
+
}
|
|
3368
|
+
vibrate() {
|
|
3369
|
+
const gp = this.getGamepad();
|
|
3370
|
+
if (!gp) {
|
|
3371
|
+
return false;
|
|
3372
|
+
}
|
|
3373
|
+
const vibrator = gp.vibrationActuator;
|
|
3374
|
+
if (vibrator) {
|
|
3375
|
+
vibrator.playEffect("dual-rumble", {
|
|
3376
|
+
duration: 400,
|
|
3377
|
+
weakMagnitude: 1,
|
|
3378
|
+
startDelay: 0,
|
|
3379
|
+
strongMagnitude: 1
|
|
3380
|
+
});
|
|
3381
|
+
}
|
|
3382
|
+
return !!vibrator;
|
|
3383
|
+
}
|
|
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
|
+
getGamepad() {
|
|
3393
|
+
return this.index < 0 ? null : navigator.getGamepads()[this.index] ?? null;
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
|
|
3397
|
+
// src/math/Polygon.ts
|
|
3398
|
+
var warnZeroEdge = throttle((count) => {
|
|
3399
|
+
console.warn(
|
|
3400
|
+
`Polygon.collide: found a zero-edge polygon \u2014 no collision possible. (called x${count} since last trace)`
|
|
3401
|
+
);
|
|
3402
|
+
});
|
|
3403
|
+
function pointDirection(xfrom, yfrom, xto, yto) {
|
|
3404
|
+
return Math.atan2(yto - yfrom, xto - xfrom);
|
|
3405
|
+
}
|
|
3406
|
+
function intervalDistance(minA, maxA, minB, maxB) {
|
|
3407
|
+
return minA < minB ? minB - maxA : minA - maxB;
|
|
3408
|
+
}
|
|
3409
|
+
function projectPolygon(axis, polygon, bounds) {
|
|
3410
|
+
let dotProduct = axis.dotProduct(polygon.points[0]);
|
|
3411
|
+
bounds.x = dotProduct;
|
|
3412
|
+
bounds.y = dotProduct;
|
|
3413
|
+
for (let i = 1; i < polygon.points.length; i++) {
|
|
3414
|
+
dotProduct = polygon.points[i].dotProduct(axis);
|
|
3415
|
+
if (dotProduct < bounds.x) {
|
|
3416
|
+
bounds.x = dotProduct;
|
|
3417
|
+
} else if (dotProduct > bounds.y) {
|
|
3418
|
+
bounds.y = dotProduct;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
var Polygon = class _Polygon {
|
|
3423
|
+
constructor(...points) {
|
|
3424
|
+
__publicField(this, "_center", new Vec2());
|
|
3425
|
+
__publicField(this, "_points", []);
|
|
3426
|
+
__publicField(this, "edges", []);
|
|
3427
|
+
this.addPoints(...points);
|
|
3428
|
+
}
|
|
3429
|
+
/**
|
|
3430
|
+
* `angle` is the simplification threshold in radians: vertices whose turn
|
|
3431
|
+
* angle wraps to within ±`angle` of straight are dropped.
|
|
3432
|
+
*/
|
|
3433
|
+
static fromCanvas(canvas, detail, angle) {
|
|
3434
|
+
detail = Math.max(2, detail);
|
|
3435
|
+
const w = canvas.width;
|
|
3436
|
+
const h = canvas.height;
|
|
3437
|
+
const data = canvas.getContext("2d").getImageData(0, 0, w, h).data;
|
|
3438
|
+
const vertexX = [0];
|
|
3439
|
+
const vertexY = [0];
|
|
3440
|
+
const vertexK = [1];
|
|
3441
|
+
let numPoints = 0;
|
|
3442
|
+
let fy = -1;
|
|
3443
|
+
let lx = 0;
|
|
3444
|
+
let ly = 0;
|
|
3445
|
+
for (let tx = 0; tx < w; tx += detail) {
|
|
3446
|
+
for (let ty = 0; ty < h; ty += 1) {
|
|
3447
|
+
if (data[(ty * w + tx) * 4 + 3] !== 0) {
|
|
3448
|
+
vertexX[numPoints] = tx;
|
|
3449
|
+
vertexY[numPoints] = ty;
|
|
3450
|
+
vertexK[numPoints] = 1;
|
|
3451
|
+
numPoints++;
|
|
3452
|
+
if (fy < 0) {
|
|
3453
|
+
fy = ty;
|
|
3454
|
+
}
|
|
3455
|
+
lx = tx;
|
|
3456
|
+
ly = ty;
|
|
3457
|
+
break;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
for (let ty = 0; ty < h; ty += detail) {
|
|
3462
|
+
for (let tx = w - 1; tx >= 0; tx -= 1) {
|
|
3463
|
+
if (data[(ty * w + tx) * 4 + 3] !== 0 && ty > ly) {
|
|
3464
|
+
vertexX[numPoints] = tx;
|
|
3465
|
+
vertexY[numPoints] = ty;
|
|
3466
|
+
vertexK[numPoints] = 1;
|
|
3467
|
+
numPoints++;
|
|
3468
|
+
lx = tx;
|
|
3469
|
+
ly = ty;
|
|
3470
|
+
break;
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
for (let tx = w - 1; tx >= 0; tx -= detail) {
|
|
3475
|
+
for (let ty = h - 1; ty >= 0; ty -= 1) {
|
|
3476
|
+
if (data[(ty * w + tx) * 4 + 3] !== 0 && tx < lx) {
|
|
3477
|
+
vertexX[numPoints] = tx;
|
|
3478
|
+
vertexY[numPoints] = ty;
|
|
3479
|
+
vertexK[numPoints] = 1;
|
|
3480
|
+
numPoints++;
|
|
3481
|
+
lx = tx;
|
|
3482
|
+
ly = ty;
|
|
3483
|
+
break;
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
for (let ty = h - 1; ty >= 0; ty -= detail) {
|
|
3488
|
+
for (let tx = 0; tx < w; tx += 1) {
|
|
3489
|
+
if (data[(ty * w + tx) * 4 + 3] !== 0 && ty < ly && ty > fy) {
|
|
3490
|
+
vertexX[numPoints] = tx;
|
|
3491
|
+
vertexY[numPoints] = ty;
|
|
3492
|
+
vertexK[numPoints] = 1;
|
|
3493
|
+
numPoints++;
|
|
3494
|
+
lx = tx;
|
|
3495
|
+
ly = ty;
|
|
3496
|
+
break;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
if (numPoints < 3) {
|
|
3501
|
+
throw new Error(
|
|
3502
|
+
`Polygon.fromCanvas: scan produced "${numPoints}" vertices (need >=3). Canvas may be empty or detail (${detail}) too large.`
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
for (let i = 0; i < numPoints; i++) {
|
|
3506
|
+
const a = i;
|
|
3507
|
+
const b = (i + 1) % numPoints;
|
|
3508
|
+
const c = (i + 2) % numPoints;
|
|
3509
|
+
const ang1 = pointDirection(
|
|
3510
|
+
vertexX[a],
|
|
3511
|
+
vertexY[a],
|
|
3512
|
+
vertexX[b],
|
|
3513
|
+
vertexY[b]
|
|
3514
|
+
);
|
|
3515
|
+
const ang2 = pointDirection(
|
|
3516
|
+
vertexX[b],
|
|
3517
|
+
vertexY[b],
|
|
3518
|
+
vertexX[c],
|
|
3519
|
+
vertexY[c]
|
|
3520
|
+
);
|
|
3521
|
+
if (Math.abs(wrapRadians(ang1 - ang2)) <= angle) {
|
|
3522
|
+
vertexK[b] = 0;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
const points = [];
|
|
3526
|
+
for (let i = 0; i < numPoints; i++) {
|
|
3527
|
+
if (vertexK[i] === 1) {
|
|
3528
|
+
points.push(new Vec2(vertexX[i], vertexY[i]));
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
return new _Polygon(...points);
|
|
3532
|
+
}
|
|
3533
|
+
static fromEdges(edges, size) {
|
|
3534
|
+
const s = size instanceof Vec2 ? size : new Vec2(size, size);
|
|
3535
|
+
const rad = Math.min(s.x, s.y) * 0.5;
|
|
3536
|
+
const Xcenter = s.x * 0.5;
|
|
3537
|
+
const Ycenter = s.y * 0.5;
|
|
3538
|
+
const points = [];
|
|
3539
|
+
for (let i = 1; i <= edges; i++) {
|
|
3540
|
+
points.push(
|
|
3541
|
+
new Vec2(
|
|
3542
|
+
Math.round(
|
|
3543
|
+
Xcenter + rad * Math.cos(i * 2 * Math.PI / edges)
|
|
3544
|
+
),
|
|
3545
|
+
Math.round(
|
|
3546
|
+
Ycenter + rad * Math.sin(i * 2 * Math.PI / edges)
|
|
3547
|
+
)
|
|
3548
|
+
)
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
return new _Polygon(...points);
|
|
3552
|
+
}
|
|
3553
|
+
static fromRect(rect) {
|
|
3554
|
+
return new _Polygon(
|
|
3555
|
+
new Vec2(rect.x, rect.y),
|
|
3556
|
+
new Vec2(rect.x + rect.w, rect.y),
|
|
3557
|
+
new Vec2(rect.x + rect.w, rect.y + rect.h),
|
|
3558
|
+
new Vec2(rect.x, rect.y + rect.h)
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
get center() {
|
|
3562
|
+
return this._center;
|
|
3563
|
+
}
|
|
3564
|
+
get points() {
|
|
3565
|
+
return this._points;
|
|
3566
|
+
}
|
|
3567
|
+
draw(context, offset = new Vec2()) {
|
|
3568
|
+
if (this._points.length === 0) {
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
context.beginPath();
|
|
3572
|
+
context.moveTo(
|
|
3573
|
+
offset.x + this._points[0].x | 0,
|
|
3574
|
+
offset.y + this._points[0].y | 0
|
|
3575
|
+
);
|
|
3576
|
+
for (let i = 1; i < this._points.length; i++) {
|
|
3577
|
+
context.lineTo(
|
|
3578
|
+
offset.x + this._points[i].x | 0,
|
|
3579
|
+
offset.y + this._points[i].y | 0
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
context.closePath();
|
|
3583
|
+
context.stroke();
|
|
3584
|
+
}
|
|
3585
|
+
addPoint(x, y) {
|
|
3586
|
+
this._points.push(new Vec2(x, y));
|
|
3587
|
+
this.update();
|
|
3588
|
+
return this;
|
|
3589
|
+
}
|
|
3590
|
+
addPoints(...points) {
|
|
3591
|
+
points.forEach((point) => this._points.push(point.clone()));
|
|
3592
|
+
this.update();
|
|
3593
|
+
return this;
|
|
3594
|
+
}
|
|
3595
|
+
offset(x = 0, y = 0) {
|
|
3596
|
+
this._points.forEach((point) => point.add(x, y));
|
|
3597
|
+
this.update();
|
|
3598
|
+
return this;
|
|
3599
|
+
}
|
|
3600
|
+
rotate(angle, pos = this.center) {
|
|
3601
|
+
if (!angle) {
|
|
3602
|
+
return this;
|
|
3603
|
+
}
|
|
3604
|
+
const cos = Math.cos(angle);
|
|
3605
|
+
const sin = Math.sin(angle);
|
|
3606
|
+
this._points.forEach((point) => {
|
|
3607
|
+
const dx = point.x - pos.x;
|
|
3608
|
+
const dy = point.y - pos.y;
|
|
3609
|
+
point.set(dx * cos - dy * sin + pos.x, dx * sin + dy * cos + pos.y);
|
|
3610
|
+
});
|
|
3611
|
+
this.update();
|
|
3612
|
+
return this;
|
|
3613
|
+
}
|
|
3614
|
+
collide(otherPolygon, velocity = new Vec2()) {
|
|
3615
|
+
const result = {
|
|
3616
|
+
intersect: true,
|
|
3617
|
+
minimumTranslationVector: new Vec2(),
|
|
3618
|
+
willIntersect: true
|
|
3619
|
+
};
|
|
3620
|
+
const edgeCountA = this.edges.length;
|
|
3621
|
+
const edgeCountB = otherPolygon.edges.length;
|
|
3622
|
+
if (edgeCountA === 0 || edgeCountB === 0) {
|
|
3623
|
+
warnZeroEdge();
|
|
3624
|
+
return {
|
|
3625
|
+
intersect: false,
|
|
3626
|
+
minimumTranslationVector: new Vec2(),
|
|
3627
|
+
willIntersect: false
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
let minDistance = Infinity;
|
|
3631
|
+
let translationAxis = new Vec2();
|
|
3632
|
+
let edge;
|
|
3633
|
+
for (let edgeIndex = 0; edgeIndex < edgeCountA + edgeCountB; edgeIndex++) {
|
|
3634
|
+
if (edgeIndex < edgeCountA) {
|
|
3635
|
+
edge = this.edges[edgeIndex];
|
|
3636
|
+
} else {
|
|
3637
|
+
edge = otherPolygon.edges[edgeIndex - edgeCountA];
|
|
3638
|
+
}
|
|
3639
|
+
const axis = new Vec2(-edge.y, edge.x);
|
|
3640
|
+
axis.normalize();
|
|
3641
|
+
const boundsA = new Vec2();
|
|
3642
|
+
const boundsB = new Vec2();
|
|
3643
|
+
projectPolygon(axis, this, boundsA);
|
|
3644
|
+
projectPolygon(axis, otherPolygon, boundsB);
|
|
3645
|
+
if (intervalDistance(boundsA.x, boundsA.y, boundsB.x, boundsB.y) > 0) {
|
|
3646
|
+
result.intersect = false;
|
|
3647
|
+
}
|
|
3648
|
+
const velocityProjection = axis.dotProduct(velocity);
|
|
3649
|
+
if (velocityProjection < 0) {
|
|
3650
|
+
boundsA.x += velocityProjection;
|
|
3651
|
+
} else {
|
|
3652
|
+
boundsA.y += velocityProjection;
|
|
3653
|
+
}
|
|
3654
|
+
let distance = intervalDistance(
|
|
3655
|
+
boundsA.x,
|
|
3656
|
+
boundsA.y,
|
|
3657
|
+
boundsB.x,
|
|
3658
|
+
boundsB.y
|
|
3659
|
+
);
|
|
3660
|
+
if (distance > 0) {
|
|
3661
|
+
result.willIntersect = false;
|
|
3662
|
+
}
|
|
3663
|
+
if (!result.intersect && !result.willIntersect) {
|
|
3664
|
+
break;
|
|
3665
|
+
}
|
|
3666
|
+
distance = Math.abs(distance);
|
|
3667
|
+
if (distance < minDistance) {
|
|
3668
|
+
minDistance = distance;
|
|
3669
|
+
translationAxis = axis.clone();
|
|
3670
|
+
const d = this.center.clone().sub(otherPolygon.center);
|
|
3671
|
+
if (d.dotProduct(translationAxis) < 0) {
|
|
3672
|
+
translationAxis.negate();
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
if (result.willIntersect) {
|
|
3677
|
+
result.minimumTranslationVector = translationAxis.mult(minDistance);
|
|
3678
|
+
}
|
|
3679
|
+
return result;
|
|
3680
|
+
}
|
|
3681
|
+
clone() {
|
|
3682
|
+
return new _Polygon(...this._points);
|
|
3683
|
+
}
|
|
3684
|
+
update() {
|
|
3685
|
+
if (this._points.length === 0) {
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
let p1;
|
|
3689
|
+
let p2;
|
|
3690
|
+
this.edges.length = 0;
|
|
3691
|
+
for (let i = 0; i < this._points.length; i++) {
|
|
3692
|
+
p1 = this._points[i];
|
|
3693
|
+
if (i + 1 >= this._points.length) {
|
|
3694
|
+
p2 = this._points[0];
|
|
3695
|
+
} else {
|
|
3696
|
+
p2 = this._points[i + 1];
|
|
3697
|
+
}
|
|
3698
|
+
this.edges.push(p2.clone().sub(p1));
|
|
3699
|
+
}
|
|
3700
|
+
let totalX = 0;
|
|
3701
|
+
let totalY = 0;
|
|
3702
|
+
this.points.forEach((point) => {
|
|
3703
|
+
totalX += point.x;
|
|
3704
|
+
totalY += point.y;
|
|
3705
|
+
});
|
|
3706
|
+
this.center.set(
|
|
3707
|
+
totalX / this._points.length,
|
|
3708
|
+
totalY / this._points.length
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
};
|
|
3712
|
+
|
|
3713
|
+
// src/utilities/Json.ts
|
|
3714
|
+
function deepClone(obj) {
|
|
3715
|
+
return cloneInternal(obj, /* @__PURE__ */ new WeakMap());
|
|
3716
|
+
}
|
|
3717
|
+
function cloneInternal(obj, hash) {
|
|
3718
|
+
if (Object(obj) !== obj || obj instanceof Function) {
|
|
3719
|
+
return obj;
|
|
3720
|
+
}
|
|
3721
|
+
const o = obj;
|
|
3722
|
+
if (hash.has(o)) {
|
|
3723
|
+
return hash.get(o);
|
|
3724
|
+
}
|
|
3725
|
+
if (obj instanceof Date) {
|
|
3726
|
+
const cloned = new Date(obj.getTime());
|
|
3727
|
+
hash.set(o, cloned);
|
|
3728
|
+
return cloned;
|
|
3729
|
+
}
|
|
3730
|
+
if (obj instanceof RegExp) {
|
|
3731
|
+
const cloned = new RegExp(obj.source, obj.flags);
|
|
3732
|
+
cloned.lastIndex = obj.lastIndex;
|
|
3733
|
+
hash.set(o, cloned);
|
|
3734
|
+
return cloned;
|
|
3735
|
+
}
|
|
3736
|
+
if (obj instanceof Map) {
|
|
3737
|
+
const cloned = /* @__PURE__ */ new Map();
|
|
3738
|
+
hash.set(o, cloned);
|
|
3739
|
+
obj.forEach((v, k) => {
|
|
3740
|
+
cloned.set(cloneInternal(k, hash), cloneInternal(v, hash));
|
|
3741
|
+
});
|
|
3742
|
+
return cloned;
|
|
3743
|
+
}
|
|
3744
|
+
if (obj instanceof Set) {
|
|
3745
|
+
const cloned = /* @__PURE__ */ new Set();
|
|
3746
|
+
hash.set(o, cloned);
|
|
3747
|
+
obj.forEach((v) => {
|
|
3748
|
+
cloned.add(cloneInternal(v, hash));
|
|
3749
|
+
});
|
|
3750
|
+
return cloned;
|
|
3751
|
+
}
|
|
3752
|
+
if (Array.isArray(obj)) {
|
|
3753
|
+
const cloned = [];
|
|
3754
|
+
hash.set(o, cloned);
|
|
3755
|
+
obj.forEach((item, i) => {
|
|
3756
|
+
cloned[i] = cloneInternal(item, hash);
|
|
3757
|
+
});
|
|
3758
|
+
return cloned;
|
|
3759
|
+
}
|
|
3760
|
+
if (obj instanceof ArrayBuffer) {
|
|
3761
|
+
const cloned = obj.slice(0);
|
|
3762
|
+
hash.set(o, cloned);
|
|
3763
|
+
return cloned;
|
|
3764
|
+
}
|
|
3765
|
+
if (obj instanceof DataView) {
|
|
3766
|
+
const buf = obj.buffer.slice(
|
|
3767
|
+
obj.byteOffset,
|
|
3768
|
+
obj.byteOffset + obj.byteLength
|
|
3769
|
+
);
|
|
3770
|
+
const cloned = new DataView(buf);
|
|
3771
|
+
hash.set(o, cloned);
|
|
3772
|
+
return cloned;
|
|
3773
|
+
}
|
|
3774
|
+
if (ArrayBuffer.isView(obj)) {
|
|
3775
|
+
const cloned = obj.slice();
|
|
3776
|
+
hash.set(o, cloned);
|
|
3777
|
+
return cloned;
|
|
3778
|
+
}
|
|
3779
|
+
const result = Object.create(Object.getPrototypeOf(o));
|
|
3780
|
+
hash.set(o, result);
|
|
3781
|
+
Reflect.ownKeys(o).forEach((key) => {
|
|
3782
|
+
const descriptor = Object.getOwnPropertyDescriptor(o, key);
|
|
3783
|
+
if ("value" in descriptor) {
|
|
3784
|
+
descriptor.value = cloneInternal(descriptor.value, hash);
|
|
3785
|
+
Object.defineProperty(result, key, descriptor);
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
const snapshot = cloneInternal(
|
|
3789
|
+
o[key],
|
|
3790
|
+
hash
|
|
3791
|
+
);
|
|
3792
|
+
Object.defineProperty(result, key, {
|
|
3793
|
+
value: snapshot,
|
|
3794
|
+
writable: true,
|
|
3795
|
+
enumerable: descriptor.enumerable,
|
|
3796
|
+
configurable: descriptor.configurable
|
|
3797
|
+
});
|
|
3798
|
+
});
|
|
3799
|
+
return result;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// src/utilities/String.ts
|
|
3803
|
+
function compact(str) {
|
|
3804
|
+
return str.trim().replace(/\s+/g, " ");
|
|
3805
|
+
}
|
|
3806
|
+
function replaceCharAt(str, index, char) {
|
|
3807
|
+
const codePoints = Array.from(str);
|
|
3808
|
+
if (index < 0 || index >= codePoints.length) {
|
|
3809
|
+
throw new Error(
|
|
3810
|
+
`replaceCharAt index out of range: ${index} (length ${codePoints.length})`
|
|
3811
|
+
);
|
|
3812
|
+
}
|
|
3813
|
+
const charPoints = Array.from(char);
|
|
3814
|
+
if (charPoints.length !== 1) {
|
|
3815
|
+
throw new Error(
|
|
3816
|
+
`replaceCharAt requires a single character, got length ${charPoints.length}`
|
|
3817
|
+
);
|
|
3818
|
+
}
|
|
3819
|
+
codePoints[index] = char;
|
|
3820
|
+
return codePoints.join("");
|
|
3821
|
+
}
|
|
3822
|
+
return __toCommonJS(index_exports);
|
|
3823
|
+
})();
|
|
3824
|
+
//# sourceMappingURL=gleam.js.map
|