littlejsengine 1.16.1 → 1.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/littlejs.d.ts +91 -78
- package/dist/littlejs.esm.js +287 -272
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +279 -266
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +184 -175
- package/examples/box2d/gameObjects.js +5 -5
- package/examples/electron/index.html +2 -2
- package/examples/logo.png +0 -0
- package/examples/logo2.png +0 -0
- package/examples/platformer/gameEffects.js +1 -1
- package/examples/shorts/base.html +2 -1
- package/examples/starter/index.html +3 -2
- package/package.json +1 -1
- package/plugins/uiSystem.js +1 -1
- package/reference.md +5 -5
- package/src/engine.js +16 -28
- package/src/engineAudio.js +2 -4
- package/src/engineBuild.js +1 -0
- package/src/engineDebug.js +96 -94
- package/src/engineDraw.js +8 -28
- package/src/engineExport.js +8 -6
- package/src/engineInput.js +3 -6
- package/src/engineMath.js +1201 -0
- package/src/engineMedals.js +4 -4
- package/src/engineObject.js +1 -2
- package/src/engineRelease.js +1 -3
- package/src/engineSettings.js +2 -2
- package/src/engineTileLayer.js +7 -9
- package/src/engineUtilities.js +34 -1179
- package/src/engineWebGL.js +7 -10
package/src/engineUtilities.js
CHANGED
|
@@ -10,256 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
/** The value of PI
|
|
14
|
-
* @type {number}
|
|
15
|
-
* @default Math.PI
|
|
16
|
-
* @memberof Utilities */
|
|
17
|
-
const PI = Math.PI;
|
|
18
|
-
|
|
19
|
-
/** Returns absolute value of value passed in
|
|
20
|
-
* @param {number} value
|
|
21
|
-
* @return {number}
|
|
22
|
-
* @memberof Utilities */
|
|
23
|
-
const abs = Math.abs;
|
|
24
|
-
|
|
25
|
-
/** Returns floored value of value passed in
|
|
26
|
-
* @param {number} value
|
|
27
|
-
* @return {number}
|
|
28
|
-
* @memberof Utilities */
|
|
29
|
-
const floor = Math.floor;
|
|
30
|
-
|
|
31
|
-
/** Returns ceiled value of value passed in
|
|
32
|
-
* @param {number} value
|
|
33
|
-
* @return {number}
|
|
34
|
-
* @memberof Utilities */
|
|
35
|
-
const ceil = Math.ceil;
|
|
36
|
-
|
|
37
|
-
/** Returns rounded value passed in
|
|
38
|
-
* @param {number} value
|
|
39
|
-
* @return {number}
|
|
40
|
-
* @memberof Utilities */
|
|
41
|
-
const round = Math.round;
|
|
42
|
-
|
|
43
|
-
/** Returns lowest value passed in
|
|
44
|
-
* @param {...number} values
|
|
45
|
-
* @return {number}
|
|
46
|
-
* @memberof Utilities */
|
|
47
|
-
const min = Math.min;
|
|
48
|
-
|
|
49
|
-
/** Returns highest value passed in
|
|
50
|
-
* @param {...number} values
|
|
51
|
-
* @return {number}
|
|
52
|
-
* @memberof Utilities */
|
|
53
|
-
const max = Math.max;
|
|
54
|
-
|
|
55
|
-
/** Returns the sign of value passed in
|
|
56
|
-
* @param {number} value
|
|
57
|
-
* @return {number}
|
|
58
|
-
* @memberof Utilities */
|
|
59
|
-
const sign = Math.sign;
|
|
60
|
-
|
|
61
|
-
/** Returns hypotenuse of values passed in
|
|
62
|
-
* @param {...number} values
|
|
63
|
-
* @return {number}
|
|
64
|
-
* @memberof Utilities */
|
|
65
|
-
const hypot = Math.hypot;
|
|
66
|
-
|
|
67
|
-
/** Returns log2 of value passed in
|
|
68
|
-
* @param {number} value
|
|
69
|
-
* @return {number}
|
|
70
|
-
* @memberof Utilities */
|
|
71
|
-
const log2 = Math.log2;
|
|
72
|
-
|
|
73
|
-
/** Returns sin of value passed in
|
|
74
|
-
* @param {number} value
|
|
75
|
-
* @return {number}
|
|
76
|
-
* @memberof Utilities */
|
|
77
|
-
const sin = Math.sin;
|
|
78
|
-
|
|
79
|
-
/** Returns cos of value passed in
|
|
80
|
-
* @param {number} value
|
|
81
|
-
* @return {number}
|
|
82
|
-
* @memberof Utilities */
|
|
83
|
-
const cos = Math.cos;
|
|
84
|
-
|
|
85
|
-
/** Returns tan of value passed in
|
|
86
|
-
* @param {number} value
|
|
87
|
-
* @return {number}
|
|
88
|
-
* @memberof Utilities */
|
|
89
|
-
const tan = Math.tan;
|
|
90
|
-
|
|
91
|
-
/** Returns atan2 of values passed in
|
|
92
|
-
* @param {number} y
|
|
93
|
-
* @param {number} x
|
|
94
|
-
* @return {number}
|
|
95
|
-
* @memberof Utilities */
|
|
96
|
-
const atan2 = Math.atan2;
|
|
97
|
-
|
|
98
|
-
/** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
|
|
99
|
-
* @param {number} dividend
|
|
100
|
-
* @param {number} [divisor]
|
|
101
|
-
* @return {number}
|
|
102
|
-
* @memberof Utilities */
|
|
103
|
-
function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
|
|
104
|
-
|
|
105
|
-
/** Clamps the value between max and min
|
|
106
|
-
* @param {number} value
|
|
107
|
-
* @param {number} [min]
|
|
108
|
-
* @param {number} [max]
|
|
109
|
-
* @return {number}
|
|
110
|
-
* @memberof Utilities */
|
|
111
|
-
function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
|
|
112
|
-
|
|
113
|
-
/** Returns what percentage the value is between valueA and valueB
|
|
114
|
-
* @param {number} value
|
|
115
|
-
* @param {number} valueA
|
|
116
|
-
* @param {number} valueB
|
|
117
|
-
* @return {number}
|
|
118
|
-
* @memberof Utilities */
|
|
119
|
-
function percent(value, valueA, valueB)
|
|
120
|
-
{ return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
|
|
121
|
-
|
|
122
|
-
/** Linearly interpolates between values passed in using percent
|
|
123
|
-
* @param {number} valueA
|
|
124
|
-
* @param {number} valueB
|
|
125
|
-
* @param {number} percent
|
|
126
|
-
* @return {number}
|
|
127
|
-
* @memberof Utilities */
|
|
128
|
-
function lerp(valueA, valueB, percent)
|
|
129
|
-
{ return valueA + clamp(percent) * (valueB-valueA); }
|
|
130
|
-
|
|
131
|
-
/** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
|
|
132
|
-
* A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
|
|
133
|
-
* @param {number} value
|
|
134
|
-
* @param {number} percentA
|
|
135
|
-
* @param {number} percentB
|
|
136
|
-
* @param {number} lerpA
|
|
137
|
-
* @param {number} lerpB
|
|
138
|
-
* @return {number}
|
|
139
|
-
* @memberof Utilities */
|
|
140
|
-
function percentLerp(value, percentA, percentB, lerpA, lerpB)
|
|
141
|
-
{ return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
|
|
142
|
-
|
|
143
|
-
/** Returns signed wrapped distance between the two values passed in
|
|
144
|
-
* @param {number} valueA
|
|
145
|
-
* @param {number} valueB
|
|
146
|
-
* @param {number} [wrapSize]
|
|
147
|
-
* @return {number}
|
|
148
|
-
* @memberof Utilities */
|
|
149
|
-
function distanceWrap(valueA, valueB, wrapSize=1)
|
|
150
|
-
{ const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
|
|
151
|
-
|
|
152
|
-
/** Linearly interpolates between values passed in with wrapping
|
|
153
|
-
* @param {number} valueA
|
|
154
|
-
* @param {number} valueB
|
|
155
|
-
* @param {number} percent
|
|
156
|
-
* @param {number} [wrapSize]
|
|
157
|
-
* @return {number}
|
|
158
|
-
* @memberof Utilities */
|
|
159
|
-
function lerpWrap(valueA, valueB, percent, wrapSize=1)
|
|
160
|
-
{ return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
|
|
161
|
-
|
|
162
|
-
/** Returns signed wrapped distance between the two angles passed in
|
|
163
|
-
* @param {number} angleA
|
|
164
|
-
* @param {number} angleB
|
|
165
|
-
* @return {number}
|
|
166
|
-
* @memberof Utilities */
|
|
167
|
-
function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
|
|
168
|
-
|
|
169
|
-
/** Linearly interpolates between the angles passed in with wrapping
|
|
170
|
-
* @param {number} angleA
|
|
171
|
-
* @param {number} angleB
|
|
172
|
-
* @param {number} percent
|
|
173
|
-
* @return {number}
|
|
174
|
-
* @memberof Utilities */
|
|
175
|
-
function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
|
|
176
|
-
|
|
177
|
-
/** Applies smoothstep function to the percentage value
|
|
178
|
-
* @param {number} percent
|
|
179
|
-
* @return {number}
|
|
180
|
-
* @memberof Utilities */
|
|
181
|
-
function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
|
|
182
|
-
|
|
183
|
-
/** Checks if the value passed in is a power of two
|
|
184
|
-
* @param {number} value
|
|
185
|
-
* @return {boolean}
|
|
186
|
-
* @memberof Utilities */
|
|
187
|
-
function isPowerOfTwo(value) { return !(value & (value - 1)); }
|
|
188
|
-
|
|
189
|
-
/** Returns the nearest power of two not less than the value
|
|
190
|
-
* @param {number} value
|
|
191
|
-
* @return {number}
|
|
192
|
-
* @memberof Utilities */
|
|
193
|
-
function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
|
|
194
|
-
|
|
195
|
-
/** Returns true if two axis aligned bounding boxes are overlapping
|
|
196
|
-
* this can be used for simple collision detection between objects
|
|
197
|
-
* @param {Vector2} posA - Center of box A
|
|
198
|
-
* @param {Vector2} sizeA - Size of box A
|
|
199
|
-
* @param {Vector2} posB - Center of box B
|
|
200
|
-
* @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
|
|
201
|
-
* @return {boolean} - True if overlapping
|
|
202
|
-
* @memberof Utilities */
|
|
203
|
-
function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
204
|
-
{
|
|
205
|
-
const dx = (posA.x - posB.x)*2;
|
|
206
|
-
const dy = (posA.y - posB.y)*2;
|
|
207
|
-
const sx = sizeA.x + sizeB.x;
|
|
208
|
-
const sy = sizeA.y + sizeB.y;
|
|
209
|
-
return dx >= -sx && dx < sx && dy >= -sy && dy < sy;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Returns true if a line segment is intersecting an axis aligned box
|
|
213
|
-
* @param {Vector2} start - Start of raycast
|
|
214
|
-
* @param {Vector2} end - End of raycast
|
|
215
|
-
* @param {Vector2} pos - Center of box
|
|
216
|
-
* @param {Vector2} size - Size of box
|
|
217
|
-
* @return {boolean} - True if intersecting
|
|
218
|
-
* @memberof Utilities */
|
|
219
|
-
function isIntersecting(start, end, pos, size)
|
|
220
|
-
{
|
|
221
|
-
// Liang-Barsky algorithm
|
|
222
|
-
const boxMin = pos.subtract(size.scale(.5));
|
|
223
|
-
const boxMax = boxMin.add(size);
|
|
224
|
-
const delta = end.subtract(start);
|
|
225
|
-
const a = start.subtract(boxMin);
|
|
226
|
-
const b = start.subtract(boxMax);
|
|
227
|
-
const p = [-delta.x, delta.x, -delta.y, delta.y];
|
|
228
|
-
const q = [a.x, -b.x, a.y, -b.y];
|
|
229
|
-
let tMin = 0, tMax = 1;
|
|
230
|
-
for (let i = 4; i--;)
|
|
231
|
-
{
|
|
232
|
-
if (p[i])
|
|
233
|
-
{
|
|
234
|
-
const t = q[i] / p[i];
|
|
235
|
-
if (p[i] < 0)
|
|
236
|
-
{
|
|
237
|
-
if (t > tMax) return false;
|
|
238
|
-
tMin = max(t, tMin);
|
|
239
|
-
}
|
|
240
|
-
else
|
|
241
|
-
{
|
|
242
|
-
if (t < tMin) return false;
|
|
243
|
-
tMax = min(t, tMax);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
else if (q[i] < 0)
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
|
|
254
|
-
* @param {number} [frequency] - Frequency of the wave in Hz
|
|
255
|
-
* @param {number} [amplitude] - Amplitude (max height) of the wave
|
|
256
|
-
* @param {number} [t=time] - Value to use for time of the wave
|
|
257
|
-
* @param {number} [offset] - Value to use for time offset of the wave
|
|
258
|
-
* @return {number} - Value waving between 0 and amplitude
|
|
259
|
-
* @memberof Utilities */
|
|
260
|
-
function wave(frequency=1, amplitude=1, t=time, offset=0)
|
|
261
|
-
{ return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
|
|
262
|
-
|
|
263
13
|
/** Formats seconds to mm:ss style for display purposes
|
|
264
14
|
* @param {number} t - time in seconds
|
|
265
15
|
* @return {string}
|
|
@@ -283,943 +33,48 @@ async function fetchJSON(url)
|
|
|
283
33
|
return response.json();
|
|
284
34
|
}
|
|
285
35
|
|
|
286
|
-
/**
|
|
287
|
-
* Check if object is a valid number, not NaN or undefined, but it may be infinite
|
|
288
|
-
* @param {any} n
|
|
289
|
-
* @return {boolean}
|
|
290
|
-
* @memberof Utilities */
|
|
291
|
-
function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Check if object is a valid string or can be converted to one
|
|
295
|
-
* @param {any} s
|
|
296
|
-
* @return {boolean}
|
|
297
|
-
* @memberof Utilities */
|
|
298
|
-
function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Check if object is an array
|
|
302
|
-
* @param {any} a
|
|
303
|
-
* @return {boolean}
|
|
304
|
-
* @memberof Utilities */
|
|
305
|
-
function isArray(a) { return Array.isArray(a); }
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* @callback LineTestFunction - Checks if a position is colliding
|
|
309
|
-
* @param {Vector2} pos
|
|
310
|
-
* @memberof Draw
|
|
311
|
-
*/
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Casts a ray and returns position of the first collision found, or undefined if none are found
|
|
315
|
-
* @param {Vector2} posStart
|
|
316
|
-
* @param {Vector2} posEnd
|
|
317
|
-
* @param {LineTestFunction} testFunction - Check if colliding
|
|
318
|
-
* @param {Vector2} [normal] - Optional vector to store the normal
|
|
319
|
-
* @return {Vector2|undefined} - Position of the collision or undefined if none found
|
|
320
|
-
* @memberof Utilities */
|
|
321
|
-
function lineTest(posStart, posEnd, testFunction, normal)
|
|
322
|
-
{
|
|
323
|
-
ASSERT(isVector2(posStart), 'posStart must be a vec2');
|
|
324
|
-
ASSERT(isVector2(posEnd), 'posEnd must be a vec2');
|
|
325
|
-
ASSERT(typeof testFunction === 'function', 'testFunction must be a function');
|
|
326
|
-
ASSERT(!normal || isVector2(normal), 'normal must be a vec2');
|
|
327
|
-
|
|
328
|
-
// get ray direction and length
|
|
329
|
-
const dx = posEnd.x - posStart.x;
|
|
330
|
-
const dy = posEnd.y - posStart.y;
|
|
331
|
-
const totalLength = hypot(dx, dy);
|
|
332
|
-
if (!totalLength)
|
|
333
|
-
return;
|
|
334
|
-
|
|
335
|
-
// current integer cell we are in
|
|
336
|
-
const pos = posStart.floor();
|
|
337
|
-
|
|
338
|
-
// normalize ray direction
|
|
339
|
-
const dirX = dx / totalLength;
|
|
340
|
-
const dirY = dy / totalLength;
|
|
341
|
-
|
|
342
|
-
// step direction in grid
|
|
343
|
-
const stepX = sign(dirX);
|
|
344
|
-
const stepY = sign(dirY);
|
|
345
|
-
|
|
346
|
-
// distance along the ray to cross one full cell in X or Y
|
|
347
|
-
const tDeltaX = dirX ? abs(1 / dirX) : Infinity;
|
|
348
|
-
const tDeltaY = dirY ? abs(1 / dirY) : Infinity;
|
|
349
|
-
|
|
350
|
-
// distance along the ray from start to the first grid boundary
|
|
351
|
-
const nextGridX = stepX > 0 ? pos.x + 1 : pos.x;
|
|
352
|
-
const nextGridY = stepY > 0 ? pos.y + 1 : pos.y;
|
|
353
|
-
const tMaxX = dirX ? (nextGridX - posStart.x) / dirX : Infinity;
|
|
354
|
-
const tMaxY = dirY ? (nextGridY - posStart.y) / dirY : Infinity;
|
|
355
|
-
|
|
356
|
-
// use line drawing algorithm to test for collisions
|
|
357
|
-
let t = 0, tX = tMaxX, tY = tMaxY, wasX = tDeltaX < tDeltaY;
|
|
358
|
-
while (t < totalLength)
|
|
359
|
-
{
|
|
360
|
-
if (testFunction(pos))
|
|
361
|
-
{
|
|
362
|
-
// set hit point
|
|
363
|
-
const hitPos = vec2(posStart.x + dirX*t, posStart.y + dirY*t);
|
|
364
|
-
|
|
365
|
-
// move inside of tile if on positive edge
|
|
366
|
-
const e = 1e-9;
|
|
367
|
-
if (wasX)
|
|
368
|
-
{
|
|
369
|
-
if (stepX < 0)
|
|
370
|
-
hitPos.x -= e;
|
|
371
|
-
}
|
|
372
|
-
if (stepY < 0)
|
|
373
|
-
hitPos.y -= e;
|
|
374
|
-
|
|
375
|
-
// set normal
|
|
376
|
-
if (normal)
|
|
377
|
-
wasX ? normal.set(-stepX,0) : normal.set(0,-stepY);
|
|
378
|
-
return hitPos;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// advance to the next grid boundary
|
|
382
|
-
if (wasX = tX < tY)
|
|
383
|
-
{
|
|
384
|
-
pos.x += stepX;
|
|
385
|
-
t = tX;
|
|
386
|
-
tX += tDeltaX;
|
|
387
|
-
}
|
|
388
|
-
else
|
|
389
|
-
{
|
|
390
|
-
pos.y += stepY;
|
|
391
|
-
t = tY;
|
|
392
|
-
tY += tDeltaY;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
398
|
-
|
|
399
|
-
/** Random global functions
|
|
400
|
-
* @namespace Random */
|
|
401
|
-
|
|
402
|
-
/** Returns a random value between the two values passed in
|
|
403
|
-
* @param {number} [valueA]
|
|
404
|
-
* @param {number} [valueB]
|
|
405
|
-
* @return {number}
|
|
406
|
-
* @memberof Random */
|
|
407
|
-
function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
|
|
408
|
-
|
|
409
|
-
/** Returns a floored random value between the two values passed in
|
|
410
|
-
* The upper bound is exclusive. (If 2 is passed in, result will be 0 or 1)
|
|
411
|
-
* @param {number} valueA
|
|
412
|
-
* @param {number} [valueB]
|
|
413
|
-
* @return {number}
|
|
414
|
-
* @memberof Random */
|
|
415
|
-
function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
|
|
416
|
-
|
|
417
|
-
/** Randomly returns true or false given the chance of true passed in
|
|
418
|
-
* @param {number} [chance]
|
|
419
|
-
* @return {boolean}
|
|
420
|
-
* @memberof Random */
|
|
421
|
-
function randBool(chance=.5) { return rand() < chance; }
|
|
422
|
-
|
|
423
|
-
/** Randomly returns either -1 or 1
|
|
424
|
-
* @return {number}
|
|
425
|
-
* @memberof Random */
|
|
426
|
-
function randSign() { return randInt(2) * 2 - 1; }
|
|
427
|
-
|
|
428
|
-
/** Returns a random Vector2 with the passed in length
|
|
429
|
-
* @param {number} [length]
|
|
430
|
-
* @return {Vector2}
|
|
431
|
-
* @memberof Random */
|
|
432
|
-
function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
|
|
433
|
-
|
|
434
|
-
/** Returns a random Vector2 within a circular shape
|
|
435
|
-
* @param {number} [radius]
|
|
436
|
-
* @param {number} [minRadius]
|
|
437
|
-
* @return {Vector2}
|
|
438
|
-
* @memberof Random */
|
|
439
|
-
function randInCircle(radius=1, minRadius=0)
|
|
440
|
-
{ return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
|
|
441
|
-
|
|
442
|
-
/** Returns a random color between the two passed in colors, combine components if linear
|
|
443
|
-
* @param {Color} [colorA=(1,1,1,1)]
|
|
444
|
-
* @param {Color} [colorB=(0,0,0,1)]
|
|
445
|
-
* @param {boolean} [linear]
|
|
446
|
-
* @return {Color}
|
|
447
|
-
* @memberof Random */
|
|
448
|
-
function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
|
|
449
|
-
{
|
|
450
|
-
return linear ? colorA.lerp(colorB, rand()) :
|
|
451
|
-
new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Seeded random number generator
|
|
458
|
-
* - Can be used to create a deterministic random number sequence
|
|
459
|
-
* @memberof Engine
|
|
460
|
-
* @example
|
|
461
|
-
* let r = new RandomGenerator(123); // random number generator with seed 123
|
|
462
|
-
* let a = r.float(); // random value between 0 and 1
|
|
463
|
-
* let b = r.int(10); // random integer between 0 and 9
|
|
464
|
-
* r.seed = 123; // reset the seed
|
|
465
|
-
* let c = r.float(); // the same value as a
|
|
466
|
-
*/
|
|
467
|
-
class RandomGenerator
|
|
468
|
-
{
|
|
469
|
-
/** Create a random number generator with the seed passed in
|
|
470
|
-
* @param {number} [seed] - Starting seed or engine default seed */
|
|
471
|
-
constructor(seed = 123456789)
|
|
472
|
-
{
|
|
473
|
-
/** @property {number} - random seed */
|
|
474
|
-
this.seed = seed;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/** Returns a seeded random value between the two values passed in
|
|
478
|
-
* @param {number} [valueA]
|
|
479
|
-
* @param {number} [valueB]
|
|
480
|
-
* @return {number} */
|
|
481
|
-
float(valueA=1, valueB=0)
|
|
482
|
-
{
|
|
483
|
-
// xorshift algorithm
|
|
484
|
-
this.seed ^= this.seed << 13;
|
|
485
|
-
this.seed ^= this.seed >>> 17;
|
|
486
|
-
this.seed ^= this.seed << 5;
|
|
487
|
-
return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/** Returns a floored seeded random value the two values passed in
|
|
491
|
-
* @param {number} valueA
|
|
492
|
-
* @param {number} [valueB]
|
|
493
|
-
* @return {number} */
|
|
494
|
-
int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
|
|
495
|
-
|
|
496
|
-
/** Randomly returns true or false given the chance of true passed in
|
|
497
|
-
* @param {number} [chance]
|
|
498
|
-
* @return {boolean} */
|
|
499
|
-
bool(chance=.5) { return this.float() < chance; }
|
|
500
|
-
|
|
501
|
-
/** Randomly returns either -1 or 1 deterministically
|
|
502
|
-
* @return {number} */
|
|
503
|
-
sign() { return this.float() > .5 ? 1 : -1; }
|
|
504
|
-
|
|
505
|
-
/** Returns a seeded random value between the two values passed in with a random sign
|
|
506
|
-
* @param {number} [valueA]
|
|
507
|
-
* @param {number} [valueB]
|
|
508
|
-
* @return {number} */
|
|
509
|
-
floatSign(valueA=1, valueB=0) { return this.float(valueA, valueB) * this.sign(); }
|
|
510
|
-
|
|
511
|
-
/** Returns a random angle between -PI and PI
|
|
512
|
-
* @return {number} */
|
|
513
|
-
angle() { return this.float(-PI, PI); }
|
|
514
|
-
|
|
515
|
-
/** Returns a seeded vec2 with size between the two values passed in
|
|
516
|
-
* @param {number} valueA
|
|
517
|
-
* @param {number} [valueB]
|
|
518
|
-
* @return {Vector2} */
|
|
519
|
-
vec2(valueA=1, valueB=0)
|
|
520
|
-
{ return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
|
|
521
|
-
|
|
522
|
-
/** Returns a random color between the two passed in colors, combine components if linear
|
|
523
|
-
* @param {Color} [colorA=(1,1,1,1)]
|
|
524
|
-
* @param {Color} [colorB=(0,0,0,1)]
|
|
525
|
-
* @param {boolean} [linear]
|
|
526
|
-
* @return {Color} */
|
|
527
|
-
randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
|
|
528
|
-
{
|
|
529
|
-
return linear ? colorA.lerp(colorB, this.float()) :
|
|
530
|
-
new Color(
|
|
531
|
-
this.float(colorA.r,colorB.r),
|
|
532
|
-
this.float(colorA.g,colorB.g),
|
|
533
|
-
this.float(colorA.b,colorB.b),
|
|
534
|
-
this.float(colorA.a,colorB.a));
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/** Returns a new color that has each component randomly adjusted
|
|
538
|
-
* @param {Color} color
|
|
539
|
-
* @param {number} [amount]
|
|
540
|
-
* @param {number} [alphaAmount]
|
|
541
|
-
* @return {Color} */
|
|
542
|
-
mutateColor(color, amount=.05, alphaAmount=0)
|
|
543
|
-
{
|
|
544
|
-
ASSERT_NUMBER_VALID(amount);
|
|
545
|
-
ASSERT_NUMBER_VALID(alphaAmount);
|
|
546
|
-
return new Color
|
|
547
|
-
(
|
|
548
|
-
color.r + this.float(amount, -amount),
|
|
549
|
-
color.g + this.float(amount, -amount),
|
|
550
|
-
color.b + this.float(amount, -amount),
|
|
551
|
-
color.a + this.float(alphaAmount, -alphaAmount)
|
|
552
|
-
).clamp();
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Create a 2d vector, can take 1 or 2 scalar values
|
|
560
|
-
* @param {number} [x]
|
|
561
|
-
* @param {number} [y] - if y is undefined, x is used for both
|
|
562
|
-
* @return {Vector2}
|
|
563
|
-
* @example
|
|
564
|
-
* let a = vec2(0, 1); // vector with coordinates (0, 1)
|
|
565
|
-
* a = vec2(5); // set a to (5, 5)
|
|
566
|
-
* b = vec2(); // set b to (0, 0)
|
|
567
|
-
* @memberof Utilities */
|
|
568
|
-
function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Check if object is a valid Vector2
|
|
572
|
-
* @param {any} v
|
|
573
|
-
* @return {boolean}
|
|
574
|
-
* @memberof Utilities */
|
|
575
|
-
function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
|
|
576
|
-
|
|
577
|
-
// vector2 asserts
|
|
578
|
-
function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), 'Vector2 is invalid.', v); }
|
|
579
|
-
function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n); }
|
|
580
|
-
function ASSERT_VECTOR2_NORMAL(v)
|
|
581
|
-
{
|
|
582
|
-
ASSERT_VECTOR2_VALID(v);
|
|
583
|
-
ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* 2D Vector object with vector math library
|
|
588
|
-
* - Functions do not change this so they can be chained together
|
|
589
|
-
* @memberof Engine
|
|
590
|
-
* @example
|
|
591
|
-
* let a = new Vector2(2, 3); // vector with coordinates (2, 3)
|
|
592
|
-
* let b = new Vector2; // vector with coordinates (0, 0)
|
|
593
|
-
* let c = vec2(4, 2); // use the vec2 function to make a Vector2
|
|
594
|
-
* let d = a.add(b).scale(5); // operators can be chained
|
|
595
|
-
*/
|
|
596
|
-
class Vector2
|
|
597
|
-
{
|
|
598
|
-
/** Create a 2D vector with the x and y passed in, can also be created with vec2()
|
|
599
|
-
* @param {number} [x] - X axis location
|
|
600
|
-
* @param {number} [y] - Y axis location */
|
|
601
|
-
constructor(x=0, y=0)
|
|
602
|
-
{
|
|
603
|
-
/** @property {number} - X axis location */
|
|
604
|
-
this.x = x;
|
|
605
|
-
/** @property {number} - Y axis location */
|
|
606
|
-
this.y = y;
|
|
607
|
-
ASSERT(this.isValid(), 'Constructed Vector2 is invalid.', this);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/** Sets values of this vector and returns self
|
|
611
|
-
* @param {number} [x] - X axis location
|
|
612
|
-
* @param {number} [y] - Y axis location
|
|
613
|
-
* @return {Vector2} */
|
|
614
|
-
set(x=0, y=0)
|
|
615
|
-
{
|
|
616
|
-
this.x = x;
|
|
617
|
-
this.y = y;
|
|
618
|
-
ASSERT_VECTOR2_VALID(this);
|
|
619
|
-
return this;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/** Sets this vector from another vector and returns self
|
|
623
|
-
* @param {Vector2} v - other vector
|
|
624
|
-
* @return {Vector2} */
|
|
625
|
-
setFrom(v) { return this.set(v.x, v.y); }
|
|
626
|
-
|
|
627
|
-
/** Returns a new vector that is a copy of this
|
|
628
|
-
* @return {Vector2} */
|
|
629
|
-
copy() { return new Vector2(this.x, this.y); }
|
|
630
|
-
|
|
631
|
-
/** Returns a copy of this vector plus the vector passed in
|
|
632
|
-
* @param {Vector2} v - other vector
|
|
633
|
-
* @return {Vector2} */
|
|
634
|
-
add(v) { return new Vector2(this.x + v.x, this.y + v.y);}
|
|
635
|
-
|
|
636
|
-
/** Returns a copy of this vector minus the vector passed in
|
|
637
|
-
* @param {Vector2} v - other vector
|
|
638
|
-
* @return {Vector2} */
|
|
639
|
-
subtract(v) { return new Vector2(this.x - v.x, this.y - v.y); }
|
|
640
|
-
|
|
641
|
-
/** Returns a copy of this vector times the vector passed in
|
|
642
|
-
* @param {Vector2} v - other vector
|
|
643
|
-
* @return {Vector2} */
|
|
644
|
-
multiply(v) { return new Vector2(this.x * v.x, this.y * v.y); }
|
|
645
|
-
|
|
646
|
-
/** Returns a copy of this vector divided by the vector passed in
|
|
647
|
-
* @param {Vector2} v - other vector
|
|
648
|
-
* @return {Vector2} */
|
|
649
|
-
divide(v) { return new Vector2(this.x / v.x, this.y / v.y); }
|
|
650
|
-
|
|
651
|
-
/** Returns a copy of this vector scaled by the vector passed in
|
|
652
|
-
* @param {number} s - scale
|
|
653
|
-
* @return {Vector2} */
|
|
654
|
-
scale(s) { return new Vector2(this.x * s, this.y * s); }
|
|
655
|
-
|
|
656
|
-
/** Returns the length of this vector
|
|
657
|
-
* @return {number} */
|
|
658
|
-
length() { return this.lengthSquared()**.5; }
|
|
659
|
-
|
|
660
|
-
/** Returns the length of this vector squared
|
|
661
|
-
* @return {number} */
|
|
662
|
-
lengthSquared() { return this.x**2 + this.y**2; }
|
|
663
|
-
|
|
664
|
-
/** Returns the distance from this vector to vector passed in
|
|
665
|
-
* @param {Vector2} v - other vector
|
|
666
|
-
* @return {number} */
|
|
667
|
-
distance(v) { return this.distanceSquared(v)**.5; }
|
|
668
|
-
|
|
669
|
-
/** Returns the distance squared from this vector to vector passed in
|
|
670
|
-
* @param {Vector2} v - other vector
|
|
671
|
-
* @return {number} */
|
|
672
|
-
distanceSquared(v) { return (this.x - v.x)**2 + (this.y - v.y)**2; }
|
|
673
|
-
|
|
674
|
-
/** Returns a new vector in same direction as this one with the length passed in
|
|
675
|
-
* @param {number} [length]
|
|
676
|
-
* @return {Vector2} */
|
|
677
|
-
normalize(length=1)
|
|
678
|
-
{
|
|
679
|
-
const l = this.length();
|
|
680
|
-
return l ? this.scale(length/l) : new Vector2(0, length);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/** Returns a new vector clamped to length passed in
|
|
684
|
-
* @param {number} [length]
|
|
685
|
-
* @return {Vector2} */
|
|
686
|
-
clampLength(length=1)
|
|
687
|
-
{
|
|
688
|
-
const l = this.length();
|
|
689
|
-
return l > length ? this.scale(length/l) : this.copy();
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/** Returns the dot product of this and the vector passed in
|
|
693
|
-
* @param {Vector2} v - other vector
|
|
694
|
-
* @return {number} */
|
|
695
|
-
dot(v) { return this.x*v.x + this.y*v.y; }
|
|
696
|
-
|
|
697
|
-
/** Returns the cross product of this and the vector passed in
|
|
698
|
-
* @param {Vector2} v - other vector
|
|
699
|
-
* @return {number} */
|
|
700
|
-
cross(v) { return this.x*v.y - this.y*v.x; }
|
|
701
|
-
|
|
702
|
-
/** Returns a copy this vector reflected by the surface normal
|
|
703
|
-
* @param {Vector2} normal - surface normal (should be normalized)
|
|
704
|
-
* @param {number} restitution - how much to bounce, 1 is perfect bounce, 0 is no bounce
|
|
705
|
-
* @return {Vector2} */
|
|
706
|
-
reflect(normal, restitution=1)
|
|
707
|
-
{ return this.subtract(normal.scale((1+restitution)*this.dot(normal))); }
|
|
708
|
-
|
|
709
|
-
/** Returns the clockwise angle of this vector, up is angle 0
|
|
710
|
-
* @return {number} */
|
|
711
|
-
angle() { return atan2(this.x, this.y); }
|
|
712
|
-
|
|
713
|
-
/** Sets this vector with clockwise angle and length passed in
|
|
714
|
-
* @param {number} [angle]
|
|
715
|
-
* @param {number} [length]
|
|
716
|
-
* @return {Vector2} */
|
|
717
|
-
setAngle(angle=0, length=1)
|
|
718
|
-
{
|
|
719
|
-
ASSERT_NUMBER_VALID(angle);
|
|
720
|
-
ASSERT_NUMBER_VALID(length);
|
|
721
|
-
this.x = length*sin(angle);
|
|
722
|
-
this.y = length*cos(angle);
|
|
723
|
-
return this;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/** Returns copy of this vector rotated by the clockwise angle passed in
|
|
727
|
-
* @param {number} angle
|
|
728
|
-
* @return {Vector2} */
|
|
729
|
-
rotate(angle)
|
|
730
|
-
{
|
|
731
|
-
ASSERT_NUMBER_VALID(angle);
|
|
732
|
-
const c = cos(-angle), s = sin(-angle);
|
|
733
|
-
return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/** Sets this this vector to point in the specified integer direction (0-3), corresponding to multiples of 90 degree rotation
|
|
737
|
-
* @param {number} [direction]
|
|
738
|
-
* @param {number} [length]
|
|
739
|
-
* @return {Vector2} */
|
|
740
|
-
setDirection(direction, length=1)
|
|
741
|
-
{
|
|
742
|
-
ASSERT_NUMBER_VALID(direction);
|
|
743
|
-
ASSERT_NUMBER_VALID(length);
|
|
744
|
-
direction = mod(direction, 4);
|
|
745
|
-
ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
|
|
746
|
-
'Vector2.setDirection() direction must be an integer between 0 and 3.');
|
|
747
|
-
|
|
748
|
-
this.x = direction%2 ? direction-1 ? -length : length : 0;
|
|
749
|
-
this.y = direction%2 ? 0 : direction ? -length : length;
|
|
750
|
-
return this;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/** Returns the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
|
|
754
|
-
* @return {number} */
|
|
755
|
-
direction()
|
|
756
|
-
{ return abs(this.x) > abs(this.y) ? this.x < 0 ? 3 : 1 : this.y < 0 ? 2 : 0; }
|
|
757
|
-
|
|
758
|
-
/** Returns a copy of this vector with absolute values
|
|
759
|
-
* @return {Vector2} */
|
|
760
|
-
abs() { return new Vector2(abs(this.x), abs(this.y)); }
|
|
761
|
-
|
|
762
|
-
/** Returns a copy of this vector with each axis floored
|
|
763
|
-
* @return {Vector2} */
|
|
764
|
-
floor() { return new Vector2(floor(this.x), floor(this.y)); }
|
|
765
|
-
|
|
766
|
-
/** Returns new vec2 with modded values
|
|
767
|
-
* @param {number} [divisor]
|
|
768
|
-
* @return {Vector2} */
|
|
769
|
-
mod(divisor=1)
|
|
770
|
-
{ return new Vector2(mod(this.x, divisor), mod(this.y, divisor)); }
|
|
771
|
-
|
|
772
|
-
/** Returns the area this vector covers as a rectangle
|
|
773
|
-
* @return {number} */
|
|
774
|
-
area() { return abs(this.x * this.y); }
|
|
775
|
-
|
|
776
|
-
/** Returns true if this vector is (0,0)
|
|
777
|
-
* @return {boolean} */
|
|
778
|
-
isZero() { return !this.x && !this.y; }
|
|
779
|
-
|
|
780
|
-
/** Returns a new vector that is p percent between this and the vector passed in
|
|
781
|
-
* @param {Vector2} v - other vector
|
|
782
|
-
* @param {number} percent
|
|
783
|
-
* @return {Vector2} */
|
|
784
|
-
lerp(v, percent)
|
|
785
|
-
{
|
|
786
|
-
ASSERT_VECTOR2_VALID(v);
|
|
787
|
-
ASSERT_NUMBER_VALID(percent);
|
|
788
|
-
const p = clamp(percent);
|
|
789
|
-
return new Vector2(v.x*p + this.x*(1-p), v.y*p + this.y*(1-p));
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/** Returns true if this vector is within the bounds of an array size passed in
|
|
793
|
-
* @param {Vector2} arraySize
|
|
794
|
-
* @return {boolean} */
|
|
795
|
-
arrayCheck(arraySize)
|
|
796
|
-
{ return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
|
|
797
|
-
|
|
798
|
-
/** Returns this vector expressed as a string
|
|
799
|
-
* @param {number} digits - precision to display
|
|
800
|
-
* @return {string} */
|
|
801
|
-
toString(digits=3)
|
|
802
|
-
{
|
|
803
|
-
ASSERT_NUMBER_VALID(digits);
|
|
804
|
-
if (this.isValid())
|
|
805
|
-
return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
|
|
806
|
-
else
|
|
807
|
-
return `(${this.x}, ${this.y})`;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
/** Checks if this is a valid vector
|
|
811
|
-
* @return {boolean} */
|
|
812
|
-
isValid() { return isNumber(this.x) && isNumber(this.y); }
|
|
813
|
-
}
|
|
814
|
-
|
|
815
36
|
///////////////////////////////////////////////////////////////////////////////
|
|
816
37
|
|
|
817
|
-
/**
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
* @memberof Utilities
|
|
825
|
-
*/
|
|
826
|
-
function rgb(r, g, b, a) { return new Color(r, g, b, a); }
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* Create a color object with HSLA values, white by default
|
|
830
|
-
* @param {number} [h=0] - hue
|
|
831
|
-
* @param {number} [s=0] - saturation
|
|
832
|
-
* @param {number} [l=1] - lightness
|
|
833
|
-
* @param {number} [a=1] - alpha
|
|
834
|
-
* @return {Color}
|
|
835
|
-
* @memberof Utilities */
|
|
836
|
-
function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Check if object is a valid Color
|
|
840
|
-
* @param {any} c
|
|
841
|
-
* @return {boolean}
|
|
842
|
-
* @memberof Utilities */
|
|
843
|
-
function isColor(c) { return c instanceof Color && c.isValid(); }
|
|
844
|
-
|
|
845
|
-
// color asserts
|
|
846
|
-
function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
|
|
38
|
+
/** Save a text file to disk
|
|
39
|
+
* @param {string} text
|
|
40
|
+
* @param {string} [filename]
|
|
41
|
+
* @param {string} [type]
|
|
42
|
+
* @memberof Utilities */
|
|
43
|
+
function saveText(text, filename='text', type='text/plain')
|
|
44
|
+
{ saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
|
|
847
45
|
|
|
848
|
-
/**
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
|
|
854
|
-
* let c = new Color(0, 0, 0, 0); // transparent black
|
|
855
|
-
* let d = rgb(0, 0, 1); // blue using rgb color
|
|
856
|
-
* let e = hsl(.3, 1, .5); // green using hsl color
|
|
857
|
-
*/
|
|
858
|
-
class Color
|
|
46
|
+
/** Save a canvas to disk
|
|
47
|
+
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
48
|
+
* @param {string} [filename]
|
|
49
|
+
* @param {string} [type]
|
|
50
|
+
* @memberof Utilities */
|
|
51
|
+
function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
859
52
|
{
|
|
860
|
-
|
|
861
|
-
* @param {number} [r] - red
|
|
862
|
-
* @param {number} [g] - green
|
|
863
|
-
* @param {number} [b] - blue
|
|
864
|
-
* @param {number} [a] - alpha*/
|
|
865
|
-
constructor(r=1, g=1, b=1, a=1)
|
|
866
|
-
{
|
|
867
|
-
/** @property {number} - Red */
|
|
868
|
-
this.r = r;
|
|
869
|
-
/** @property {number} - Green */
|
|
870
|
-
this.g = g;
|
|
871
|
-
/** @property {number} - Blue */
|
|
872
|
-
this.b = b;
|
|
873
|
-
/** @property {number} - Alpha */
|
|
874
|
-
this.a = a;
|
|
875
|
-
ASSERT(this.isValid(), 'Constructed Color is invalid.', this);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
/** Sets values of this color and returns self
|
|
879
|
-
* @param {number} [r] - red
|
|
880
|
-
* @param {number} [g] - green
|
|
881
|
-
* @param {number} [b] - blue
|
|
882
|
-
* @param {number} [a] - alpha
|
|
883
|
-
* @return {Color} */
|
|
884
|
-
set(r=1, g=1, b=1, a=1)
|
|
885
|
-
{
|
|
886
|
-
this.r = r;
|
|
887
|
-
this.g = g;
|
|
888
|
-
this.b = b;
|
|
889
|
-
this.a = a;
|
|
890
|
-
ASSERT_COLOR_VALID(this);
|
|
891
|
-
return this;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/** Sets this color from another color and returns self
|
|
895
|
-
* @param {Color} c - other color
|
|
896
|
-
* @return {Color} */
|
|
897
|
-
setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
|
|
898
|
-
|
|
899
|
-
/** Returns a new color that is a copy of this
|
|
900
|
-
* @return {Color} */
|
|
901
|
-
copy() { return new Color(this.r, this.g, this.b, this.a); }
|
|
902
|
-
|
|
903
|
-
/** Returns a copy of this color plus the color passed in
|
|
904
|
-
* @param {Color} c - other color
|
|
905
|
-
* @return {Color} */
|
|
906
|
-
add(c) { return new Color(this.r+c.r, this.g+c.g, this.b+c.b, this.a+c.a); }
|
|
907
|
-
|
|
908
|
-
/** Returns a copy of this color minus the color passed in
|
|
909
|
-
* @param {Color} c - other color
|
|
910
|
-
* @return {Color} */
|
|
911
|
-
subtract(c) { return new Color(this.r-c.r, this.g-c.g, this.b-c.b, this.a-c.a); }
|
|
912
|
-
|
|
913
|
-
/** Returns a copy of this color times the color passed in
|
|
914
|
-
* @param {Color} c - other color
|
|
915
|
-
* @return {Color} */
|
|
916
|
-
multiply(c) { return new Color(this.r*c.r, this.g*c.g, this.b*c.b, this.a*c.a); }
|
|
917
|
-
|
|
918
|
-
/** Returns a copy of this color divided by the color passed in
|
|
919
|
-
* @param {Color} c - other color
|
|
920
|
-
* @return {Color} */
|
|
921
|
-
divide(c) { return new Color(this.r/c.r, this.g/c.g, this.b/c.b, this.a/c.a); }
|
|
922
|
-
|
|
923
|
-
/** Returns a copy of this color scaled by the value passed in, alpha can be scaled separately
|
|
924
|
-
* @param {number} scale
|
|
925
|
-
* @param {number} [alphaScale=scale]
|
|
926
|
-
* @return {Color} */
|
|
927
|
-
scale(scale, alphaScale=scale)
|
|
928
|
-
{ return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
|
|
929
|
-
|
|
930
|
-
/** Returns a copy of this color clamped to the valid range between 0 and 1
|
|
931
|
-
* @return {Color} */
|
|
932
|
-
clamp() { return new Color(clamp(this.r), clamp(this.g), clamp(this.b), clamp(this.a)); }
|
|
933
|
-
|
|
934
|
-
/** Returns a new color that is p percent between this and the color passed in
|
|
935
|
-
* @param {Color} c - other color
|
|
936
|
-
* @param {number} percent
|
|
937
|
-
* @return {Color} */
|
|
938
|
-
lerp(c, percent)
|
|
939
|
-
{
|
|
940
|
-
ASSERT_COLOR_VALID(c);
|
|
941
|
-
ASSERT_NUMBER_VALID(percent);
|
|
942
|
-
const p = clamp(percent);
|
|
943
|
-
return new Color(
|
|
944
|
-
c.r*p + this.r*(1-p),
|
|
945
|
-
c.g*p + this.g*(1-p),
|
|
946
|
-
c.b*p + this.b*(1-p),
|
|
947
|
-
c.a*p + this.a*(1-p));
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/** Sets this color given a hue, saturation, lightness, and alpha
|
|
951
|
-
* @param {number} [h] - hue
|
|
952
|
-
* @param {number} [s] - saturation
|
|
953
|
-
* @param {number} [l] - lightness
|
|
954
|
-
* @param {number} [a] - alpha
|
|
955
|
-
* @return {Color} */
|
|
956
|
-
setHSLA(h=0, s=0, l=1, a=1)
|
|
957
|
-
{
|
|
958
|
-
h = mod(h,1);
|
|
959
|
-
s = clamp(s);
|
|
960
|
-
l = clamp(l);
|
|
961
|
-
const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
|
|
962
|
-
f = (p, q, t)=>
|
|
963
|
-
(t = mod(t,1))*6 < 1 ? p+(q-p)*6*t :
|
|
964
|
-
t*2 < 1 ? q :
|
|
965
|
-
t*3 < 2 ? p+(q-p)*(4-t*6) : p;
|
|
966
|
-
this.r = f(p, q, h + 1/3);
|
|
967
|
-
this.g = f(p, q, h);
|
|
968
|
-
this.b = f(p, q, h - 1/3);
|
|
969
|
-
this.a = a;
|
|
970
|
-
ASSERT_COLOR_VALID(this);
|
|
971
|
-
return this;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
/** Returns this color expressed in hsla format
|
|
975
|
-
* @return {Array<number>} */
|
|
976
|
-
HSLA()
|
|
977
|
-
{
|
|
978
|
-
const r = clamp(this.r);
|
|
979
|
-
const g = clamp(this.g);
|
|
980
|
-
const b = clamp(this.b);
|
|
981
|
-
const a = clamp(this.a);
|
|
982
|
-
const maxC = max(r, g, b);
|
|
983
|
-
const minC = min(r, g, b);
|
|
984
|
-
const l = (maxC + minC) / 2;
|
|
985
|
-
let h = 0, s = 0;
|
|
986
|
-
if (maxC !== minC)
|
|
987
|
-
{
|
|
988
|
-
let d = maxC - minC;
|
|
989
|
-
s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
|
|
990
|
-
if (r === maxC)
|
|
991
|
-
h = (g - b) / d + (g < b ? 6 : 0);
|
|
992
|
-
else if (g === maxC)
|
|
993
|
-
h = (b - r) / d + 2;
|
|
994
|
-
else if (b === maxC)
|
|
995
|
-
h = (r - g) / d + 4;
|
|
996
|
-
}
|
|
997
|
-
return [h / 6, s, l, a];
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
/** Returns a new color that has each component randomly adjusted
|
|
1001
|
-
* @param {number} [amount]
|
|
1002
|
-
* @param {number} [alphaAmount]
|
|
1003
|
-
* @return {Color} */
|
|
1004
|
-
mutate(amount=.05, alphaAmount=0)
|
|
1005
|
-
{
|
|
1006
|
-
ASSERT_NUMBER_VALID(amount);
|
|
1007
|
-
ASSERT_NUMBER_VALID(alphaAmount);
|
|
1008
|
-
return new Color
|
|
1009
|
-
(
|
|
1010
|
-
this.r + rand(amount, -amount),
|
|
1011
|
-
this.g + rand(amount, -amount),
|
|
1012
|
-
this.b + rand(amount, -amount),
|
|
1013
|
-
this.a + rand(alphaAmount, -alphaAmount)
|
|
1014
|
-
).clamp();
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/** Returns this color expressed as a hex color code
|
|
1018
|
-
* @param {boolean} [useAlpha] - if alpha should be included in result
|
|
1019
|
-
* @return {string} */
|
|
1020
|
-
toString(useAlpha = true)
|
|
1021
|
-
{
|
|
1022
|
-
if (debug && !this.isValid())
|
|
1023
|
-
return `#000`;
|
|
1024
|
-
const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
|
|
1025
|
-
return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/** Set this color from a hex code
|
|
1029
|
-
* @param {string} hex - html hex code
|
|
1030
|
-
* @return {Color} */
|
|
1031
|
-
setHex(hex)
|
|
1032
|
-
{
|
|
1033
|
-
ASSERT(isString(hex), 'Color hex code must be a string');
|
|
1034
|
-
ASSERT(hex[0] === '#', 'Color hex code must start with #');
|
|
1035
|
-
ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
|
|
1036
|
-
|
|
1037
|
-
if (hex.length < 6)
|
|
1038
|
-
{
|
|
1039
|
-
const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
|
|
1040
|
-
this.r = fromHex(1);
|
|
1041
|
-
this.g = fromHex(2);
|
|
1042
|
-
this.b = fromHex(3);
|
|
1043
|
-
this.a = hex.length === 5 ? fromHex(4) : 1;
|
|
1044
|
-
}
|
|
1045
|
-
else
|
|
1046
|
-
{
|
|
1047
|
-
const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
|
|
1048
|
-
this.r = fromHex(1);
|
|
1049
|
-
this.g = fromHex(3);
|
|
1050
|
-
this.b = fromHex(5);
|
|
1051
|
-
this.a = hex.length === 9 ? fromHex(7) : 1;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
ASSERT_COLOR_VALID(this);
|
|
1055
|
-
return this;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
/** Returns this color expressed as 32 bit RGBA value
|
|
1059
|
-
* @return {number} */
|
|
1060
|
-
rgbaInt()
|
|
53
|
+
if (canvas instanceof OffscreenCanvas)
|
|
1061
54
|
{
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
55
|
+
// copy to temporary canvas and save
|
|
56
|
+
const saveCanvas = document.createElement('canvas');
|
|
57
|
+
saveCanvas.width = canvas.width;
|
|
58
|
+
saveCanvas.height = canvas.height;
|
|
59
|
+
saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
|
60
|
+
saveDataURL(saveCanvas.toDataURL(type), filename);
|
|
1067
61
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
* @return {boolean} */
|
|
1071
|
-
isValid()
|
|
1072
|
-
{ return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a); }
|
|
62
|
+
else
|
|
63
|
+
saveDataURL(canvas.toDataURL(type), filename);
|
|
1073
64
|
}
|
|
1074
65
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
* @type {Color}
|
|
1080
|
-
* @memberof Utilities */
|
|
1081
|
-
const WHITE = debugProtectConstant(rgb());
|
|
1082
|
-
|
|
1083
|
-
/** Color - Clear White #757474ff with 0 alpha
|
|
1084
|
-
* @type {Color}
|
|
1085
|
-
* @memberof Utilities */
|
|
1086
|
-
const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
|
|
1087
|
-
|
|
1088
|
-
/** Color - Black #000000
|
|
1089
|
-
* @type {Color}
|
|
1090
|
-
* @memberof Utilities */
|
|
1091
|
-
const BLACK = debugProtectConstant(rgb(0,0,0));
|
|
1092
|
-
|
|
1093
|
-
/** Color - Clear Black #000000 with 0 alpha
|
|
1094
|
-
* @type {Color}
|
|
1095
|
-
* @memberof Utilities */
|
|
1096
|
-
const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
|
|
1097
|
-
|
|
1098
|
-
/** Color - Gray #808080
|
|
1099
|
-
* @type {Color}
|
|
1100
|
-
* @memberof Utilities */
|
|
1101
|
-
const GRAY = debugProtectConstant(rgb(.5,.5,.5));
|
|
1102
|
-
|
|
1103
|
-
/** Color - Red #ff0000
|
|
1104
|
-
* @type {Color}
|
|
1105
|
-
* @memberof Utilities */
|
|
1106
|
-
const RED = debugProtectConstant(rgb(1,0,0));
|
|
1107
|
-
|
|
1108
|
-
/** Color - Orange #ff8000
|
|
1109
|
-
* @type {Color}
|
|
66
|
+
/** Save a data url to disk
|
|
67
|
+
* @param {string} url
|
|
68
|
+
* @param {string} [filename]
|
|
69
|
+
* @param {number} [revokeTime] - how long before revoking the url
|
|
1110
70
|
* @memberof Utilities */
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
/** Color - Yellow #ffff00
|
|
1114
|
-
* @type {Color}
|
|
1115
|
-
* @memberof Utilities */
|
|
1116
|
-
const YELLOW = debugProtectConstant(rgb(1,1,0));
|
|
1117
|
-
|
|
1118
|
-
/** Color - Green #00ff00
|
|
1119
|
-
* @type {Color}
|
|
1120
|
-
* @memberof Utilities */
|
|
1121
|
-
const GREEN = debugProtectConstant(rgb(0,1,0));
|
|
1122
|
-
|
|
1123
|
-
/** Color - Cyan #00ffff
|
|
1124
|
-
* @type {Color}
|
|
1125
|
-
* @memberof Utilities */
|
|
1126
|
-
const CYAN = debugProtectConstant(rgb(0,1,1));
|
|
1127
|
-
|
|
1128
|
-
/** Color - Blue #0000ff
|
|
1129
|
-
* @type {Color}
|
|
1130
|
-
* @memberof Utilities */
|
|
1131
|
-
const BLUE = debugProtectConstant(rgb(0,0,1));
|
|
1132
|
-
|
|
1133
|
-
/** Color - Purple #8000ff
|
|
1134
|
-
* @type {Color}
|
|
1135
|
-
* @memberof Utilities */
|
|
1136
|
-
const PURPLE = debugProtectConstant(rgb(.5,0,1));
|
|
1137
|
-
|
|
1138
|
-
/** Color - Magenta #ff00ff
|
|
1139
|
-
* @type {Color}
|
|
1140
|
-
* @memberof Utilities */
|
|
1141
|
-
const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
1142
|
-
|
|
1143
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
1144
|
-
|
|
1145
|
-
/**
|
|
1146
|
-
* Timer object tracks how long has passed since it was set
|
|
1147
|
-
* @memberof Engine
|
|
1148
|
-
* @example
|
|
1149
|
-
* let a = new Timer; // creates a timer that is not set
|
|
1150
|
-
* a.set(3); // sets the timer to 3 seconds
|
|
1151
|
-
*
|
|
1152
|
-
* let b = new Timer(1); // creates a timer with 1 second left
|
|
1153
|
-
* b.unset(); // unset the timer
|
|
1154
|
-
*/
|
|
1155
|
-
class Timer
|
|
71
|
+
function saveDataURL(url, filename='download', revokeTime)
|
|
1156
72
|
{
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const globalTime = this.getGlobalTime();
|
|
1165
|
-
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
1166
|
-
this.setTime = timeLeft;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
/** Set the timer with seconds passed in
|
|
1170
|
-
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
1171
|
-
set(timeLeft=0)
|
|
1172
|
-
{
|
|
1173
|
-
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
1174
|
-
const globalTime = this.getGlobalTime();
|
|
1175
|
-
this.time = globalTime + timeLeft;
|
|
1176
|
-
this.setTime = timeLeft;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
/** Set if the timer should keep running even when the game is paused
|
|
1180
|
-
* @param {boolean} [useRealTime] */
|
|
1181
|
-
setUseRealTime(useRealTime=true)
|
|
1182
|
-
{
|
|
1183
|
-
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
1184
|
-
this.useRealTime = useRealTime;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
/** Unset the timer */
|
|
1188
|
-
unset() { this.time = undefined; }
|
|
1189
|
-
|
|
1190
|
-
/** Returns true if set
|
|
1191
|
-
* @return {boolean} */
|
|
1192
|
-
isSet() { return this.time !== undefined; }
|
|
1193
|
-
|
|
1194
|
-
/** Returns true if set and has not elapsed
|
|
1195
|
-
* @return {boolean} */
|
|
1196
|
-
active() { return this.getGlobalTime() < this.time; }
|
|
1197
|
-
|
|
1198
|
-
/** Returns true if set and elapsed
|
|
1199
|
-
* @return {boolean} */
|
|
1200
|
-
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
1201
|
-
|
|
1202
|
-
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
1203
|
-
* @return {number} */
|
|
1204
|
-
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
1205
|
-
|
|
1206
|
-
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
1207
|
-
* @return {number} */
|
|
1208
|
-
getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
|
|
1209
|
-
|
|
1210
|
-
/** Get the time this timer was set to, returns 0 if not set
|
|
1211
|
-
* @return {number} */
|
|
1212
|
-
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
1213
|
-
|
|
1214
|
-
/** Get the current global time this timer is based on
|
|
1215
|
-
* @return {number} */
|
|
1216
|
-
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
1217
|
-
|
|
1218
|
-
/** Returns this timer expressed as a string
|
|
1219
|
-
* @return {string} */
|
|
1220
|
-
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
1221
|
-
|
|
1222
|
-
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
1223
|
-
* @return {number} */
|
|
1224
|
-
valueOf() { return this.get(); }
|
|
73
|
+
// create link for saving screenshots
|
|
74
|
+
const link = document.createElement('a');
|
|
75
|
+
link.download = filename;
|
|
76
|
+
link.href = url;
|
|
77
|
+
link.click();
|
|
78
|
+
if (revokeTime !== undefined)
|
|
79
|
+
setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
|
|
1225
80
|
}
|