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