littlejsengine 1.16.1 → 1.16.2

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