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