littlejsengine 1.12.6 → 1.13.1

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 (83) hide show
  1. package/dist/littlejs.d.ts +387 -177
  2. package/dist/littlejs.esm.js +5934 -5294
  3. package/dist/littlejs.esm.min.js +4 -1
  4. package/dist/littlejs.js +5887 -5263
  5. package/dist/littlejs.min.js +4 -1
  6. package/dist/littlejs.release.js +5426 -4829
  7. package/examples/box2d/game.js +1 -1
  8. package/examples/box2d/gameObjects.js +7 -6
  9. package/examples/box2d/index.html +1 -1
  10. package/examples/box2d/scenes.js +7 -7
  11. package/examples/breakout/game.js +1 -1
  12. package/examples/breakout/gameObjects.js +2 -2
  13. package/examples/breakout/index.html +1 -1
  14. package/examples/breakoutTutorial/README.md +2 -2
  15. package/examples/breakoutTutorial/game.js +1 -1
  16. package/examples/breakoutTutorial/index.html +1 -1
  17. package/examples/empty/index.html +1 -1
  18. package/examples/htmlMenu/index.html +1 -1
  19. package/examples/index.html +396 -153
  20. package/examples/module/game.js +1 -1
  21. package/examples/module/index.html +1 -1
  22. package/examples/platformer/game.js +4 -4
  23. package/examples/platformer/gameCharacter.js +6 -6
  24. package/examples/platformer/gameEffects.js +74 -57
  25. package/examples/platformer/gameLevel.js +61 -70
  26. package/examples/platformer/gameObjects.js +4 -4
  27. package/examples/platformer/index.html +1 -1
  28. package/examples/puzzle/index.html +1 -1
  29. package/examples/shorts/base.html +23 -3
  30. package/examples/shorts/box2dCar.js +8 -12
  31. package/examples/shorts/flappyGame.js +52 -0
  32. package/examples/shorts/helloWorld.js +6 -3
  33. package/examples/shorts/hillGlideGame.js +56 -0
  34. package/examples/shorts/landerGame.js +32 -0
  35. package/examples/shorts/maze.js +47 -0
  36. package/examples/shorts/medals.js +42 -0
  37. package/examples/shorts/nineSlice.js +21 -0
  38. package/examples/shorts/piano.js +52 -0
  39. package/examples/shorts/platformer.js +24 -20
  40. package/examples/shorts/{pong.js → pongGame.js} +1 -1
  41. package/examples/shorts/raycasting.js +71 -0
  42. package/examples/shorts/slidingPuzzle.js +42 -0
  43. package/examples/shorts/spaceGame.js +56 -0
  44. package/examples/shorts/starfield.js +17 -0
  45. package/examples/shorts/tileLayer.js +3 -5
  46. package/examples/shorts/tiles.png +0 -0
  47. package/examples/shorts/tiltedView.js +8 -7
  48. package/examples/shorts/topDown.js +18 -26
  49. package/examples/shorts/uiSystem.js +4 -7
  50. package/examples/starter/build.bat +6 -1
  51. package/examples/starter/game.js +2 -2
  52. package/examples/starter/index.html +23 -13
  53. package/examples/stress/index.html +2 -3
  54. package/examples/style.css +136 -0
  55. package/examples/typescript/build.js +1 -2
  56. package/examples/typescript/game.js +2 -2
  57. package/examples/typescript/game.ts +1 -1
  58. package/examples/typescript/index.html +1 -1
  59. package/examples/uiSystem/game.js +8 -24
  60. package/examples/uiSystem/index.html +1 -1
  61. package/package.json +1 -1
  62. package/plugins/box2d.js +8 -8
  63. package/plugins/drawUtilities.js +126 -0
  64. package/plugins/newgrounds.js +1 -1
  65. package/plugins/pluginExport.js +7 -1
  66. package/plugins/postProcess.js +2 -2
  67. package/plugins/uiSystem.js +153 -65
  68. package/plugins/zzfxm.js +1 -1
  69. package/reference.md +10 -10
  70. package/src/engine.js +50 -29
  71. package/src/engineAudio.js +14 -5
  72. package/src/engineBuild.bat +6 -1
  73. package/src/engineBuild.js +20 -5
  74. package/src/engineDebug.js +53 -26
  75. package/src/engineDraw.js +108 -57
  76. package/src/engineExport.js +21 -9
  77. package/src/engineInput.js +109 -37
  78. package/src/engineObject.js +68 -67
  79. package/src/engineParticles.js +26 -9
  80. package/src/engineSettings.js +15 -16
  81. package/src/engineTileLayer.js +312 -153
  82. package/src/engineUtilities.js +182 -130
  83. package/src/engineWebGL.js +47 -36
@@ -22,19 +22,17 @@ const PI = Math.PI;
22
22
  * @memberof Utilities */
23
23
  function abs(value) { return Math.abs(value); }
24
24
 
25
- /** Returns lowest of two values passed in
26
- * @param {number} valueA
27
- * @param {number} valueB
25
+ /** Returns lowest value passed in
26
+ * @param {...number} values
28
27
  * @return {number}
29
28
  * @memberof Utilities */
30
- function min(valueA, valueB) { return Math.min(valueA, valueB); }
29
+ function min(...values) { return Math.min(...values); }
31
30
 
32
- /** Returns highest of two values passed in
33
- * @param {number} valueA
34
- * @param {number} valueB
31
+ /** Returns highest value passed in
32
+ * @param {...number} values
35
33
  * @return {number}
36
34
  * @memberof Utilities */
37
- function max(valueA, valueB) { return Math.max(valueA, valueB); }
35
+ function max(...values) { return Math.max(...values); }
38
36
 
39
37
  /** Returns the sign of value passed in
40
38
  * @param {number} value
@@ -67,12 +65,17 @@ function percent(value, valueA, valueB)
67
65
  { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
68
66
 
69
67
  /** Linearly interpolates between values passed in using percent
70
- * @param {number} percent
71
68
  * @param {number} valueA
72
69
  * @param {number} valueB
70
+ * @param {number} percent
73
71
  * @return {number}
74
72
  * @memberof Utilities */
75
- function lerp(percent, valueA, valueB) { return valueA + clamp(percent) * (valueB-valueA); }
73
+ function lerp(valueA, valueB, percent)
74
+ {
75
+ if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
76
+ console.warn('lerp() parameter order changed! use lerp(start, end, p)');
77
+ return valueA + clamp(percent) * (valueB-valueA);
78
+ }
76
79
 
77
80
  /** Returns signed wrapped distance between the two values passed in
78
81
  * @param {number} valueA
@@ -84,14 +87,18 @@ function distanceWrap(valueA, valueB, wrapSize=1)
84
87
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
85
88
 
86
89
  /** Linearly interpolates between values passed in with wrapping
87
- * @param {number} percent
88
90
  * @param {number} valueA
89
91
  * @param {number} valueB
92
+ * @param {number} percent
90
93
  * @param {number} [wrapSize]
91
94
  * @returns {number}
92
95
  * @memberof Utilities */
93
- function lerpWrap(percent, valueA, valueB, wrapSize=1)
94
- { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
96
+ function lerpWrap(valueA, valueB, percent, wrapSize=1)
97
+ {
98
+ if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
99
+ console.warn('lerpWrap() parameter order changed! use lerpWrap(start, end, p)');
100
+ return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize);
101
+ }
95
102
 
96
103
  /** Returns signed wrapped distance between the two angles passed in
97
104
  * @param {number} angleA
@@ -101,12 +108,12 @@ function lerpWrap(percent, valueA, valueB, wrapSize=1)
101
108
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
102
109
 
103
110
  /** Linearly interpolates between the angles passed in with wrapping
104
- * @param {number} percent
105
111
  * @param {number} angleA
106
112
  * @param {number} angleB
113
+ * @param {number} percent
107
114
  * @returns {number}
108
115
  * @memberof Utilities */
109
- function lerpAngle(percent, angleA, angleB) { return lerpWrap(percent, angleA, angleB, 2*PI); }
116
+ function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
110
117
 
111
118
  /** Applies smoothstep function to the percentage value
112
119
  * @param {number} percent
@@ -114,6 +121,12 @@ function lerpAngle(percent, angleA, angleB) { return lerpWrap(percent, angleA, a
114
121
  * @memberof Utilities */
115
122
  function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
116
123
 
124
+ /** Checks if the value passed in is a power of two
125
+ * @param {number} value
126
+ * @return {boolean}
127
+ * @memberof Utilities */
128
+ function isPowerOfTwo(value) { return !(value & (value - 1)); }
129
+
117
130
  /** Returns the nearest power of two not less then the value
118
131
  * @param {number} value
119
132
  * @return {number}
@@ -199,6 +212,14 @@ async function fetchJSON(url)
199
212
  return response.json();
200
213
  }
201
214
 
215
+ /**
216
+ * Check if object is a valid number, not NaN or undefined, but it may be infinite
217
+ * @param {any} n
218
+ * @return {boolean}
219
+ * @memberof Utilities
220
+ */
221
+ function isNumber(n) { return typeof n == 'number' && !isNaN(n); }
222
+
202
223
  ///////////////////////////////////////////////////////////////////////////////
203
224
 
204
225
  /** Random global functions
@@ -219,6 +240,12 @@ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valu
219
240
  * @memberof Random */
220
241
  function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
221
242
 
243
+ /** Randomly returns true or false given the chance of true passed in
244
+ * @param {number} [chance]
245
+ * @return {boolean}
246
+ * @memberof Random */
247
+ function randBool(chance=.5) { return rand() < chance; }
248
+
222
249
  /** Randomly returns either -1 or 1
223
250
  * @return {number}
224
251
  * @memberof Random */
@@ -228,7 +255,7 @@ function randSign() { return randInt(2) * 2 - 1; }
228
255
  * @param {number} [length]
229
256
  * @return {Vector2}
230
257
  * @memberof Random */
231
- function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
258
+ function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
232
259
 
233
260
  /** Returns a random Vector2 within a circular shape
234
261
  * @param {number} [radius]
@@ -236,7 +263,7 @@ function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length
236
263
  * @return {Vector2}
237
264
  * @memberof Random */
238
265
  function randInCircle(radius=1, minRadius=0)
239
- { return radius > 0 ? randVector(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
266
+ { return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
240
267
 
241
268
  /** Returns a random color between the two passed in colors, combine components if linear
242
269
  * @param {Color} [colorA=(1,1,1,1)]
@@ -265,8 +292,8 @@ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
265
292
  class RandomGenerator
266
293
  {
267
294
  /** Create a random number generator with the seed passed in
268
- * @param {number} seed - Starting seed */
269
- constructor(seed)
295
+ * @param {number} [seed] - Starting seed or engine default seed */
296
+ constructor(seed = 123456789)
270
297
  {
271
298
  /** @property {number} - random seed */
272
299
  this.seed = seed;
@@ -291,6 +318,11 @@ class RandomGenerator
291
318
  * @return {number} */
292
319
  int(valueA, valueB=0) { return Math.floor(this.float(valueA, valueB)); }
293
320
 
321
+ /** Randomly returns true or false given the chance of true passed in
322
+ * @param {number} [chance]
323
+ * @return {boolean} */
324
+ bool(chance=.5) { return this.float() < chance; }
325
+
294
326
  /** Randomly returns either -1 or 1 deterministically
295
327
  * @return {number} */
296
328
  sign() { return this.float() > .5 ? 1 : -1; }
@@ -300,11 +332,22 @@ class RandomGenerator
300
332
  * @param {number} [valueB]
301
333
  * @return {number} */
302
334
  floatSign(valueA=1, valueB=0) { return this.float(valueA, valueB) * this.sign(); }
335
+
336
+ /** Returns a random angle between -PI and PI
337
+ * @return {number} */
338
+ angle() { return this.float(-PI, PI); }
339
+
340
+ /** Returns a seeded vec2 with size between the two values passed in
341
+ * @param {number} valueA
342
+ * @param {number} [valueB]
343
+ * @return {Vector2} */
344
+ vec2(valueA=1, valueB=0)
345
+ { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
303
346
  }
304
347
 
305
348
  ///////////////////////////////////////////////////////////////////////////////
306
349
 
307
- /**
350
+ /**
308
351
  * Create a 2d vector, can take 1 or 2 scalar values
309
352
  * @param {number} [x]
310
353
  * @param {number} [y] - if y is undefined, x is used for both
@@ -315,7 +358,7 @@ class RandomGenerator
315
358
  * b = vec2(); // set b to (0, 0)
316
359
  * @memberof Utilities
317
360
  */
318
- function vec2(x=0, y) { return new Vector2(x, y == undefined? x : y); }
361
+ function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
319
362
 
320
363
  /**
321
364
  * Check if object is a valid Vector2
@@ -325,6 +368,15 @@ function vec2(x=0, y) { return new Vector2(x, y == undefined? x : y); }
325
368
  */
326
369
  function isVector2(v) { return v instanceof Vector2; }
327
370
 
371
+ // vector2 asserts
372
+ function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v) && v.isValid(), 'Vector2 is invalid.', v); }
373
+ function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n); }
374
+ function ASSERT_VECTOR2_NORMAL(v)
375
+ {
376
+ ASSERT_VECTOR2_VALID(v);
377
+ ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
378
+ }
379
+
328
380
  /**
329
381
  * 2D Vector object with vector math library
330
382
  * - Functions do not change this so they can be chained together
@@ -345,7 +397,7 @@ class Vector2
345
397
  this.x = x;
346
398
  /** @property {number} - Y axis location */
347
399
  this.y = y;
348
- ASSERT(this.isValid());
400
+ ASSERT(this.isValid(), 'Constructed Vector2 is invalid.', this);
349
401
  }
350
402
 
351
403
  /** Sets values of this vector and returns self
@@ -356,7 +408,6 @@ class Vector2
356
408
  {
357
409
  this.x = x;
358
410
  this.y = y;
359
- ASSERT(this.isValid());
360
411
  return this;
361
412
  }
362
413
 
@@ -367,47 +418,27 @@ class Vector2
367
418
  /** Returns a copy of this vector plus the vector passed in
368
419
  * @param {Vector2} v - other vector
369
420
  * @return {Vector2} */
370
- add(v)
371
- {
372
- ASSERT(isVector2(v));
373
- return new Vector2(this.x + v.x, this.y + v.y);
374
- }
421
+ add(v) { return new Vector2(this.x + v.x, this.y + v.y);}
375
422
 
376
423
  /** Returns a copy of this vector minus the vector passed in
377
424
  * @param {Vector2} v - other vector
378
425
  * @return {Vector2} */
379
- subtract(v)
380
- {
381
- ASSERT(isVector2(v));
382
- return new Vector2(this.x - v.x, this.y - v.y);
383
- }
426
+ subtract(v) { return new Vector2(this.x - v.x, this.y - v.y); }
384
427
 
385
428
  /** Returns a copy of this vector times the vector passed in
386
429
  * @param {Vector2} v - other vector
387
430
  * @return {Vector2} */
388
- multiply(v)
389
- {
390
- ASSERT(isVector2(v));
391
- return new Vector2(this.x * v.x, this.y * v.y);
392
- }
431
+ multiply(v) { return new Vector2(this.x * v.x, this.y * v.y); }
393
432
 
394
433
  /** Returns a copy of this vector divided by the vector passed in
395
434
  * @param {Vector2} v - other vector
396
435
  * @return {Vector2} */
397
- divide(v)
398
- {
399
- ASSERT(isVector2(v));
400
- return new Vector2(this.x / v.x, this.y / v.y);
401
- }
436
+ divide(v) { return new Vector2(this.x / v.x, this.y / v.y); }
402
437
 
403
438
  /** Returns a copy of this vector scaled by the vector passed in
404
439
  * @param {number} s - scale
405
440
  * @return {Vector2} */
406
- scale(s)
407
- {
408
- ASSERT(!isVector2(s));
409
- return new Vector2(this.x * s, this.y * s);
410
- }
441
+ scale(s) { return new Vector2(this.x * s, this.y * s); }
411
442
 
412
443
  /** Returns the length of this vector
413
444
  * @return {number} */
@@ -420,20 +451,12 @@ class Vector2
420
451
  /** Returns the distance from this vector to vector passed in
421
452
  * @param {Vector2} v - other vector
422
453
  * @return {number} */
423
- distance(v)
424
- {
425
- ASSERT(isVector2(v));
426
- return this.distanceSquared(v)**.5;
427
- }
454
+ distance(v) { return this.distanceSquared(v)**.5; }
428
455
 
429
456
  /** Returns the distance squared from this vector to vector passed in
430
457
  * @param {Vector2} v - other vector
431
458
  * @return {number} */
432
- distanceSquared(v)
433
- {
434
- ASSERT(isVector2(v));
435
- return (this.x - v.x)**2 + (this.y - v.y)**2;
436
- }
459
+ distanceSquared(v) { return (this.x - v.x)**2 + (this.y - v.y)**2; }
437
460
 
438
461
  /** Returns a new vector in same direction as this one with the length passed in
439
462
  * @param {number} [length]
@@ -456,20 +479,19 @@ class Vector2
456
479
  /** Returns the dot product of this and the vector passed in
457
480
  * @param {Vector2} v - other vector
458
481
  * @return {number} */
459
- dot(v)
460
- {
461
- ASSERT(isVector2(v));
462
- return this.x*v.x + this.y*v.y;
463
- }
482
+ dot(v) { return this.x*v.x + this.y*v.y; }
464
483
 
465
484
  /** Returns the cross product of this and the vector passed in
466
485
  * @param {Vector2} v - other vector
467
486
  * @return {number} */
468
- cross(v)
469
- {
470
- ASSERT(isVector2(v));
471
- return this.x*v.y - this.y*v.x;
472
- }
487
+ cross(v) { return this.x*v.y - this.y*v.x; }
488
+
489
+ /** Returns a copy this vector reflected by the surface normal
490
+ * @param {Vector2} normal - surface normal (should be normalized)
491
+ * @param {number} restitution - how much to bounce, 1 is perfect bounce, 0 is no bounce
492
+ * @return {Vector2} */
493
+ reflect(normal, restitution=1)
494
+ { return this.subtract(normal.scale((1+restitution)*this.dot(normal))); }
473
495
 
474
496
  /** Returns the clockwise angle of this vector, up is angle 0
475
497
  * @return {number} */
@@ -481,6 +503,8 @@ class Vector2
481
503
  * @return {Vector2} */
482
504
  setAngle(angle=0, length=1)
483
505
  {
506
+ ASSERT_NUMBER_VALID(angle);
507
+ ASSERT_NUMBER_VALID(length);
484
508
  this.x = length*Math.sin(angle);
485
509
  this.y = length*Math.cos(angle);
486
510
  return this;
@@ -490,7 +514,8 @@ class Vector2
490
514
  * @param {number} angle
491
515
  * @return {Vector2} */
492
516
  rotate(angle)
493
- {
517
+ {
518
+ ASSERT_NUMBER_VALID(angle);
494
519
  const c = Math.cos(-angle), s = Math.sin(-angle);
495
520
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
496
521
  }
@@ -500,8 +525,11 @@ class Vector2
500
525
  * @param {number} [length] */
501
526
  setDirection(direction, length=1)
502
527
  {
528
+ ASSERT_NUMBER_VALID(direction);
529
+ ASSERT_NUMBER_VALID(length);
503
530
  direction = mod(direction, 4);
504
- ASSERT(direction==0 || direction==1 || direction==2 || direction==3);
531
+ ASSERT(direction==0 || direction==1 || direction==2 || direction==3,
532
+ 'Vector2.setDirection() direction must be an integer between 0 and 3.');
505
533
  return vec2(direction%2 ? direction-1 ? -length : length : 0,
506
534
  direction%2 ? 0 : direction ? -length : length);
507
535
  }
@@ -515,10 +543,20 @@ class Vector2
515
543
  * @return {Vector2} */
516
544
  invert() { return new Vector2(this.y, -this.x); }
517
545
 
546
+ /** Returns a copy of this vector absolute values
547
+ * @return {Vector2} */
548
+ abs() { return new Vector2(abs(this.x), abs(this.y)); }
549
+
518
550
  /** Returns a copy of this vector with each axis floored
519
551
  * @return {Vector2} */
520
552
  floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
521
553
 
554
+ /** Returns new vec2 with modded values
555
+ * @param {number} [divisor]
556
+ * @return {Vector2} */
557
+ mod(divisor=1)
558
+ { return new Vector2(mod(this.x, divisor), mod(this.y, divisor)); }
559
+
522
560
  /** Returns the area this vector covers as a rectangle
523
561
  * @return {number} */
524
562
  area() { return abs(this.x * this.y); }
@@ -529,35 +567,36 @@ class Vector2
529
567
  * @return {Vector2} */
530
568
  lerp(v, percent)
531
569
  {
532
- ASSERT(isVector2(v));
533
- return this.add(v.subtract(this).scale(clamp(percent)));
570
+ ASSERT_VECTOR2_VALID(v);
571
+ ASSERT_NUMBER_VALID(percent);
572
+ const p = clamp(percent);
573
+ return new Vector2(v.x*p + this.x*(1-p), v.y*p + this.y*(1-p));
534
574
  }
535
575
 
536
576
  /** Returns true if this vector is within the bounds of an array size passed in
537
577
  * @param {Vector2} arraySize
538
578
  * @return {boolean} */
539
579
  arrayCheck(arraySize)
540
- {
541
- ASSERT(isVector2(arraySize));
542
- return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y;
543
- }
580
+ { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
544
581
 
545
582
  /** Returns this vector expressed as a string
546
583
  * @param {number} digits - precision to display
547
584
  * @return {string} */
548
585
  toString(digits=3)
549
586
  {
587
+ ASSERT_NUMBER_VALID(digits);
550
588
  if (debug)
551
- return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
589
+ {
590
+ if (this.isValid())
591
+ return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
592
+ else
593
+ return `(${this.x}, ${this.y})`;
594
+ }
552
595
  }
553
596
 
554
597
  /** Checks if this is a valid vector
555
598
  * @return {boolean} */
556
- isValid()
557
- {
558
- return typeof this.x == 'number' && !isNaN(this.x)
559
- && typeof this.y == 'number' && !isNaN(this.y);
560
- }
599
+ isValid() { return isNumber(this.x) && isNumber(this.y); }
561
600
  }
562
601
 
563
602
  ///////////////////////////////////////////////////////////////////////////////
@@ -592,13 +631,16 @@ function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
592
631
  */
593
632
  function isColor(c) { return c instanceof Color; }
594
633
 
634
+ // color asserts
635
+ function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c) && c.isValid(), 'Color is invalid.', c); }
636
+
595
637
  /**
596
638
  * Color object (red, green, blue, alpha) with some helpful functions
597
639
  * @example
598
640
  * let a = new Color; // white
599
641
  * let b = new Color(1, 0, 0); // red
600
642
  * let c = new Color(0, 0, 0, 0); // transparent black
601
- * let d = rgb(0, 0, 1); // blue using rgb color
643
+ * let d = rgb(0, 0, 1); // blue using rgb color
602
644
  * let e = hsl(.3, 1, .5); // green using hsl color
603
645
  */
604
646
  class Color
@@ -618,7 +660,7 @@ class Color
618
660
  this.b = b;
619
661
  /** @property {number} - Alpha */
620
662
  this.a = a;
621
- ASSERT(this.isValid());
663
+ ASSERT(this.isValid(), 'Constructed Color is invalid.', this);
622
664
  }
623
665
 
624
666
  /** Sets values of this color and returns self
@@ -633,7 +675,6 @@ class Color
633
675
  this.g = g;
634
676
  this.b = b;
635
677
  this.a = a;
636
- ASSERT(this.isValid());
637
678
  return this;
638
679
  }
639
680
 
@@ -644,38 +685,22 @@ class Color
644
685
  /** Returns a copy of this color plus the color passed in
645
686
  * @param {Color} c - other color
646
687
  * @return {Color} */
647
- add(c)
648
- {
649
- ASSERT(isColor(c));
650
- return new Color(this.r+c.r, this.g+c.g, this.b+c.b, this.a+c.a);
651
- }
688
+ add(c) { return new Color(this.r+c.r, this.g+c.g, this.b+c.b, this.a+c.a); }
652
689
 
653
690
  /** Returns a copy of this color minus the color passed in
654
691
  * @param {Color} c - other color
655
692
  * @return {Color} */
656
- subtract(c)
657
- {
658
- ASSERT(isColor(c));
659
- return new Color(this.r-c.r, this.g-c.g, this.b-c.b, this.a-c.a);
660
- }
693
+ subtract(c) { return new Color(this.r-c.r, this.g-c.g, this.b-c.b, this.a-c.a); }
661
694
 
662
695
  /** Returns a copy of this color times the color passed in
663
696
  * @param {Color} c - other color
664
697
  * @return {Color} */
665
- multiply(c)
666
- {
667
- ASSERT(isColor(c));
668
- return new Color(this.r*c.r, this.g*c.g, this.b*c.b, this.a*c.a);
669
- }
698
+ multiply(c) { return new Color(this.r*c.r, this.g*c.g, this.b*c.b, this.a*c.a); }
670
699
 
671
700
  /** Returns a copy of this color divided by the color passed in
672
701
  * @param {Color} c - other color
673
702
  * @return {Color} */
674
- divide(c)
675
- {
676
- ASSERT(isColor(c));
677
- return new Color(this.r/c.r, this.g/c.g, this.b/c.b, this.a/c.a);
678
- }
703
+ divide(c) { return new Color(this.r/c.r, this.g/c.g, this.b/c.b, this.a/c.a); }
679
704
 
680
705
  /** Returns a copy of this color scaled by the value passed in, alpha can be scaled separately
681
706
  * @param {number} scale
@@ -694,8 +719,14 @@ class Color
694
719
  * @return {Color} */
695
720
  lerp(c, percent)
696
721
  {
697
- ASSERT(isColor(c));
698
- return this.add(c.subtract(this).scale(clamp(percent)));
722
+ ASSERT_COLOR_VALID(c);
723
+ ASSERT_NUMBER_VALID(percent);
724
+ const p = clamp(percent);
725
+ return new Color(
726
+ c.r*p + this.r*(1-p),
727
+ c.g*p + this.g*(1-p),
728
+ c.b*p + this.b*(1-p),
729
+ c.a*p + this.a*(1-p));
699
730
  }
700
731
 
701
732
  /** Sets this color given a hue, saturation, lightness, and alpha
@@ -718,7 +749,7 @@ class Color
718
749
  this.g = f(p, q, h);
719
750
  this.b = f(p, q, h - 1/3);
720
751
  this.a = a;
721
- ASSERT(this.isValid());
752
+ ASSERT_COLOR_VALID(this);
722
753
  return this;
723
754
  }
724
755
 
@@ -730,20 +761,19 @@ class Color
730
761
  const g = clamp(this.g);
731
762
  const b = clamp(this.b);
732
763
  const a = clamp(this.a);
733
- const max = Math.max(r, g, b);
734
- const min = Math.min(r, g, b);
735
- const l = (max + min) / 2;
736
-
764
+ const maxC = max(r, g, b);
765
+ const minC = min(r, g, b);
766
+ const l = (maxC + minC) / 2;
737
767
  let h = 0, s = 0;
738
- if (max != min)
768
+ if (maxC != minC)
739
769
  {
740
- let d = max - min;
741
- s = l > .5 ? d / (2 - max - min) : d / (max + min);
742
- if (r == max)
770
+ let d = maxC - minC;
771
+ s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
772
+ if (r == maxC)
743
773
  h = (g - b) / d + (g < b ? 6 : 0);
744
- else if (g == max)
774
+ else if (g == maxC)
745
775
  h = (b - r) / d + 2;
746
- else if (b == max)
776
+ else if (b == maxC)
747
777
  h = (r - g) / d + 4;
748
778
  }
749
779
  return [h / 6, s, l, a];
@@ -755,6 +785,8 @@ class Color
755
785
  * @return {Color} */
756
786
  mutate(amount=.05, alphaAmount=0)
757
787
  {
788
+ ASSERT_NUMBER_VALID(amount);
789
+ ASSERT_NUMBER_VALID(alphaAmount);
758
790
  return new Color
759
791
  (
760
792
  this.r + rand(amount, -amount),
@@ -768,7 +800,10 @@ class Color
768
800
  * @param {boolean} [useAlpha] - if alpha should be included in result
769
801
  * @return {string} */
770
802
  toString(useAlpha = true)
771
- {
803
+ {
804
+ ASSERT(typeof useAlpha == 'boolean', 'Use alpha boolean is invalid.', useAlpha);
805
+ if (debug && !this.isValid())
806
+ return `#000`;
772
807
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
773
808
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
774
809
  }
@@ -778,7 +813,7 @@ class Color
778
813
  * @return {Color} */
779
814
  setHex(hex)
780
815
  {
781
- ASSERT(typeof hex == 'string' && hex[0] == '#');
816
+ ASSERT(typeof hex == 'string' && hex[0] == '#', 'Color hex code must be a string starting with #');
782
817
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
783
818
 
784
819
  if (hex.length < 6)
@@ -798,7 +833,7 @@ class Color
798
833
  this.a = hex.length == 9 ? fromHex(7) : 1;
799
834
  }
800
835
 
801
- ASSERT(this.isValid());
836
+ ASSERT_COLOR_VALID(this);
802
837
  return this;
803
838
  }
804
839
 
@@ -816,11 +851,8 @@ class Color
816
851
  /** Checks if this is a valid color
817
852
  * @return {boolean} */
818
853
  isValid()
819
- {
820
- return typeof this.r == 'number' && !isNaN(this.r)
821
- && typeof this.g == 'number' && !isNaN(this.g)
822
- && typeof this.b == 'number' && !isNaN(this.b)
823
- && typeof this.a == 'number' && !isNaN(this.a);
854
+ {
855
+ return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a);
824
856
  }
825
857
  }
826
858
 
@@ -832,11 +864,21 @@ class Color
832
864
  * @memberof Utilities */
833
865
  const WHITE = rgb();
834
866
 
867
+ /** Color - Clear White #ffffff with 0 alpha
868
+ * @type {Color}
869
+ * @memberof Utilities */
870
+ const CLEAR_WHITE = rgb(1,1,1,0);
871
+
835
872
  /** Color - Black #000000
836
873
  * @type {Color}
837
874
  * @memberof Utilities */
838
875
  const BLACK = rgb(0,0,0);
839
876
 
877
+ /** Color - Clear Black #000000 with 0 alpha
878
+ * @type {Color}
879
+ * @memberof Utilities */
880
+ const CLEAR_BLACK = rgb(0,0,0,0);
881
+
840
882
  /** Color - Gray #808080
841
883
  * @type {Color}
842
884
  * @memberof Utilities */
@@ -897,18 +939,28 @@ class Timer
897
939
  {
898
940
  /** Create a timer object set time passed in
899
941
  * @param {number} [timeLeft] - How much time left before the timer elapses in seconds */
900
- constructor(timeLeft) { this.time = timeLeft == undefined ? undefined : time + timeLeft; this.setTime = timeLeft; }
942
+ constructor(timeLeft)
943
+ {
944
+ ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
945
+ this.time = timeLeft === undefined ? undefined : time + timeLeft;
946
+ this.setTime = timeLeft;
947
+ }
901
948
 
902
949
  /** Set the timer with seconds passed in
903
950
  * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
904
- set(timeLeft=0) { this.time = time + timeLeft; this.setTime = timeLeft; }
951
+ set(timeLeft=0)
952
+ {
953
+ ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
954
+ this.time = time + timeLeft;
955
+ this.setTime = timeLeft;
956
+ }
905
957
 
906
958
  /** Unset the timer */
907
959
  unset() { this.time = undefined; }
908
960
 
909
961
  /** Returns true if set
910
962
  * @return {boolean} */
911
- isSet() { return this.time != undefined; }
963
+ isSet() { return this.time !== undefined; }
912
964
 
913
965
  /** Returns true if set and has not elapsed
914
966
  * @return {boolean} */
@@ -932,5 +984,5 @@ class Timer
932
984
 
933
985
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
934
986
  * @return {number} */
935
- valueOf() { return this.get(); }
987
+ valueOf() { return this.get(); }
936
988
  }