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