littlejsengine 1.6.6 → 1.7.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 (59) hide show
  1. package/README.md +76 -53
  2. package/build/littlejs.d.ts +154 -136
  3. package/build/littlejs.esm.js +461 -305
  4. package/build/littlejs.esm.min.js +1 -1
  5. package/build/littlejs.js +452 -291
  6. package/build/littlejs.min.js +1 -1
  7. package/build/littlejs.release.js +2317 -2168
  8. package/examples/breakout/game.js +5 -1
  9. package/examples/breakout/index.html +5 -5
  10. package/examples/breakoutTutorial/README.md +6 -6
  11. package/examples/breakoutTutorial/game.js +3 -0
  12. package/examples/breakoutTutorial/index.html +3 -3
  13. package/examples/electron/build.js +105 -0
  14. package/examples/electron/game.js +4 -2
  15. package/examples/electron/index.html +13 -2
  16. package/examples/electron/package.json +5 -3
  17. package/examples/empty/game.js +3 -1
  18. package/examples/empty/index.html +1 -1
  19. package/examples/js13k/build.bat +2 -0
  20. package/examples/js13k/build.js +110 -0
  21. package/examples/js13k/game.js +109 -0
  22. package/examples/js13k/index.html +18 -0
  23. package/examples/js13k/tiles.png +0 -0
  24. package/examples/module/game.js +9 -3
  25. package/examples/module/index.html +2 -2
  26. package/examples/particles/index.html +7 -13
  27. package/examples/platformer/game.js +5 -2
  28. package/examples/platformer/gameEffects.js +12 -12
  29. package/examples/platformer/gamePlayer.js +2 -2
  30. package/examples/platformer/index.html +8 -8
  31. package/examples/puzzle/game.js +5 -3
  32. package/examples/puzzle/index.html +4 -4
  33. package/examples/starter/build.bat +2 -78
  34. package/examples/starter/build.js +109 -0
  35. package/examples/starter/game.js +4 -1
  36. package/examples/starter/index.html +15 -18
  37. package/examples/stress/index.html +5 -7
  38. package/examples/typescript/build.bat +2 -14
  39. package/examples/typescript/build.js +31 -0
  40. package/examples/typescript/game.js +94 -89
  41. package/examples/typescript/game.ts +9 -3
  42. package/examples/typescript/index.html +2 -2
  43. package/package.json +2 -2
  44. package/src/engine.js +3 -3
  45. package/src/engineAudio.js +44 -16
  46. package/src/engineBuild.bat +2 -132
  47. package/src/engineBuild.js +156 -0
  48. package/src/engineDebug.js +25 -15
  49. package/src/engineDraw.js +62 -53
  50. package/src/engineExport.js +7 -12
  51. package/src/engineInput.js +27 -35
  52. package/src/engineObject.js +15 -13
  53. package/src/engineParticles.js +6 -6
  54. package/src/engineRelease.js +1 -3
  55. package/src/engineSettings.js +1 -0
  56. package/src/engineTileLayer.js +31 -18
  57. package/src/engineUtilities.js +161 -93
  58. package/src/engineWebGL.js +63 -27
  59. package/examples/electron/build.bat +0 -52
@@ -1,10 +1,12 @@
1
+ // LittleJS - MIT License - Copyright 2021 Frank Force
2
+
1
3
  /**
2
4
  * LittleJS Debug System
3
- * - Press ~ to show debug overlay with mouse pick
5
+ * - Press Esc to show debug overlay with mouse pick
4
6
  * - Number keys toggle debug functions
5
7
  * - +/- apply time scale
6
8
  * - Debug primitive rendering
7
- * - Save a 2d canvas as an image
9
+ * - Save a 2d canvas as a png image
8
10
  * @namespace Debug
9
11
  */
10
12
 
@@ -34,12 +36,6 @@ const debugPointSize = .5;
34
36
  * @memberof Debug */
35
37
  let showWatermark = 1;
36
38
 
37
- /** True if god mode is enabled, handle this however you want
38
- * @type {Boolean}
39
- * @default
40
- * @memberof Debug */
41
- let godMode = 0;
42
-
43
39
  /** Key code used to toggle debug mode, Esc by default
44
40
  * @type {Boolean}
45
41
  * @default
@@ -144,11 +140,27 @@ function debugClear() { debugPrimitives = []; }
144
140
  /** Save a canvas to disk
145
141
  * @param {HTMLCanvasElement} canvas
146
142
  * @param {String} [filename]
143
+ * @param {String} [type='image/png']
144
+ * @memberof Debug */
145
+ function debugSaveCanvas(canvas, filename=engineName, type='image/png')
146
+ { debugSaveDataURL(canvas.toDataURL(type), filename); }
147
+
148
+ /** Save a text file to disk
149
+ * @param {String} text
150
+ * @param {String} [filename]
151
+ * @param {String} [type='text/plain']
147
152
  * @memberof Debug */
148
- function debugSaveCanvas(canvas, filename = engineName + '.png')
153
+ function debugSaveText(text, filename=engineName, type='text/plain')
154
+ { debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
155
+
156
+ /** Save a data url to disk
157
+ * @param {String} dataURL
158
+ * @param {String} filename
159
+ * @memberof Debug */
160
+ function debugSaveDataURL(dataURL, filename)
149
161
  {
150
- downloadLink.download = 'screenshot.png';
151
- downloadLink.href = canvas.toDataURL('image/png').replace('image/png','image/octet-stream');
162
+ downloadLink.download = filename;
163
+ downloadLink.href = dataURL;
152
164
  downloadLink.click();
153
165
  }
154
166
 
@@ -180,7 +192,7 @@ function debugUpdate()
180
192
  if (keyWasPressed(51)) // 3
181
193
  debugGamepads = !debugGamepads;
182
194
  if (keyWasPressed(52)) // 4
183
- godMode = !godMode;
195
+ debugRaycast = !debugRaycast;
184
196
  if (keyWasPressed(53)) // 5
185
197
  debugTakeScreenshot = 1;
186
198
  //if (keyWasPressed(54)) // 6
@@ -359,8 +371,8 @@ function debugRender()
359
371
  overlayContext.fillText('2: Debug Particles', x, y += h);
360
372
  overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
361
373
  overlayContext.fillText('3: Debug Gamepads', x, y += h);
362
- overlayContext.fillStyle = godMode ? '#f00' : '#fff';
363
- overlayContext.fillText('4: God Mode', x, y += h);
374
+ overlayContext.fillStyle = debugRaycast ? '#f00' : '#fff';
375
+ overlayContext.fillText('4: Debug Raycasts', x, y += h);
364
376
  overlayContext.fillStyle = '#fff';
365
377
  overlayContext.fillText('5: Save Screenshot', x, y += h);
366
378
 
@@ -385,19 +397,20 @@ function debugRender()
385
397
  {
386
398
  overlayContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
387
399
  overlayContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
388
- overlayContext.fillText(godMode ? 'God Mode' : '', x, y += h);
400
+ overlayContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
389
401
  overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
390
402
  }
391
403
 
392
404
  overlayContext.restore();
393
405
  }
394
- }
406
+ }
395
407
  /**
396
408
  * LittleJS Utility Classes and Functions
397
409
  * - General purpose math library
398
410
  * - Vector2 - fast, simple, easy 2D vector class
399
411
  * - Color - holds a rgba color with some math functions
400
412
  * - Timer - tracks time automatically
413
+ * - RandomGenerator - seeded random number generator
401
414
  * @namespace Utilities
402
415
  */
403
416
 
@@ -413,34 +426,34 @@ const PI = Math.PI;
413
426
  * @param {Number} value
414
427
  * @return {Number}
415
428
  * @memberof Utilities */
416
- function abs(a) { return a < 0 ? -a : a; }
429
+ function abs(value) { return Math.abs(value); }
417
430
 
418
431
  /** Returns lowest of two values passed in
419
432
  * @param {Number} valueA
420
433
  * @param {Number} valueB
421
434
  * @return {Number}
422
435
  * @memberof Utilities */
423
- function min(a, b) { return a < b ? a : b; }
436
+ function min(valueA, valueB) { return Math.min(valueA, valueB); }
424
437
 
425
438
  /** Returns highest of two values passed in
426
439
  * @param {Number} valueA
427
440
  * @param {Number} valueB
428
441
  * @return {Number}
429
442
  * @memberof Utilities */
430
- function max(a, b) { return a > b ? a : b; }
443
+ function max(valueA, valueB) { return Math.max(valueA, valueB); }
431
444
 
432
445
  /** Returns the sign of value passed in (also returns 1 if 0)
433
446
  * @param {Number} value
434
447
  * @return {Number}
435
448
  * @memberof Utilities */
436
- function sign(a) { return a < 0 ? -1 : 1; }
449
+ function sign(value) { return Math.sign(value); }
437
450
 
438
451
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
439
452
  * @param {Number} dividend
440
453
  * @param {Number} [divisor=1]
441
454
  * @return {Number}
442
455
  * @memberof Utilities */
443
- function mod(a, b=1) { return ((a % b) + b) % b; }
456
+ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
444
457
 
445
458
  /** Clamps the value beween max and min
446
459
  * @param {Number} value
@@ -448,47 +461,83 @@ function mod(a, b=1) { return ((a % b) + b) % b; }
448
461
  * @param {Number} [max=1]
449
462
  * @return {Number}
450
463
  * @memberof Utilities */
451
- function clamp(v, min=0, max=1)
452
- { return v < min ? min : v > max ? max : v; }
464
+ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
453
465
 
454
- /** Returns what percentage the value is between max and min
466
+ /** Returns what percentage the value is between valueA and valueB
455
467
  * @param {Number} value
456
- * @param {Number} [min=0]
457
- * @param {Number} [max=1]
468
+ * @param {Number} valueA
469
+ * @param {Number} valueB
458
470
  * @return {Number}
459
471
  * @memberof Utilities */
460
- function percent(v, min=0, max=1)
461
- { return max-min ? clamp((v-min) / (max-min)) : 0; }
472
+ function percent(value, valueA, valueB)
473
+ { return valueB-valueA ? clamp((value-valueA) / (valueB-valueA)) : 0; }
462
474
 
463
- /** Linearly interpolates the percent value between max and min
475
+ /** Linearly interpolates between values passed in using percent
464
476
  * @param {Number} percent
465
- * @param {Number} [min=0]
466
- * @param {Number} [max=1]
477
+ * @param {Number} valueA
478
+ * @param {Number} valueB
467
479
  * @return {Number}
468
480
  * @memberof Utilities */
469
- function lerp(p, min=0, max=1){ return min + clamp(p) * (max-min); }
481
+ function lerp(percent, valueA, valueB) { return valueA + clamp(percent) * (valueB-valueA); }
482
+
483
+ /** Returns signed wrapped distance between the two values passed in
484
+ * @param {Number} valueA
485
+ * @param {Number} valueB
486
+ * @param {Number} [wrapSize=1]
487
+ * @returns {Number}
488
+ * @memberof Utilities */
489
+ function distanceWrap(valueA, valueB, wrapSize=1)
490
+ { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
491
+
492
+ /** Linearly interpolates between values passed in with wrappping
493
+ * @param {Number} percent
494
+ * @param {Number} valueA
495
+ * @param {Number} valueB
496
+ * @param {Number} [wrapSize=1]
497
+ * @returns {Number}
498
+ * @memberof Utilities */
499
+ function lerpWrap(percent, valueA, valueB, wrapSize=1)
500
+ { return valueB + clamp(percent) * distanceWrap(valueA, valueB, wrapSize); }
501
+
502
+ /** Returns signed wrapped distance between the two angles passed in
503
+ * @param {Number} angleA
504
+ * @param {Number} angleB
505
+ * @returns {Number}
506
+ * @memberof Utilities */
507
+ function distanceAngle(angleA, angleB) { distanceWrap(angleA, angleB, 2*PI); }
508
+
509
+ /** Linearly interpolates between the angles passed in with wrappping
510
+ * @param {Number} percent
511
+ * @param {Number} angleA
512
+ * @param {Number} angleB
513
+ * @returns {Number}
514
+ * @memberof Utilities */
515
+ function lerpAngle(percent, angleA, angleB) { return lerpWrap(percent, angleA, angleB, 2*PI); }
470
516
 
471
517
  /** Applies smoothstep function to the percentage value
472
- * @param {Number} value
518
+ * @param {Number} percent
473
519
  * @return {Number}
474
520
  * @memberof Utilities */
475
- function smoothStep(p) { return p * p * (3 - 2 * p); }
521
+ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
476
522
 
477
523
  /** Returns the nearest power of two not less then the value
478
524
  * @param {Number} value
479
525
  * @return {Number}
480
526
  * @memberof Utilities */
481
- function nearestPowerOfTwo(v) { return 2**Math.ceil(Math.log2(v)); }
527
+ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
482
528
 
483
529
  /** Returns true if two axis aligned bounding boxes are overlapping
484
530
  * @param {Vector2} pointA - Center of box A
485
531
  * @param {Vector2} sizeA - Size of box A
486
532
  * @param {Vector2} pointB - Center of box B
487
- * @param {Vector2} [sizeB] - Size of box B
533
+ * @param {Vector2} sizeB - Size of box B
488
534
  * @return {Boolean} - True if overlapping
489
535
  * @memberof Utilities */
490
- function isOverlapping(pA, sA, pB, sB)
491
- { return abs(pA.x - pB.x)*2 < sA.x + sB.x && abs(pA.y - pB.y)*2 < sA.y + sB.y; }
536
+ function isOverlapping(pointA, sizeA, pointB, sizeB)
537
+ {
538
+ return abs(pointA.x - pointB.x)*2 < sizeA.x + sizeB.x
539
+ && abs(pointA.y - pointB.y)*2 < sizeA.y + sizeB.y;
540
+ }
492
541
 
493
542
  /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
494
543
  * @param {Number} [frequency=1] - Frequency of the wave in Hz
@@ -515,20 +564,26 @@ function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
515
564
  * @param {Number} [valueB=0]
516
565
  * @return {Number}
517
566
  * @memberof Random */
518
- function rand(a=1, b=0) { return b + (a-b)*Math.random(); }
567
+ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
519
568
 
520
569
  /** Returns a floored random value the two values passed in
521
- * @param {Number} [valueA=1]
570
+ * @param {Number} valueA
522
571
  * @param {Number} [valueB=0]
523
572
  * @return {Number}
524
573
  * @memberof Random */
525
- function randInt(a=1, b=0) { return rand(a,b)|0; }
574
+ function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
526
575
 
527
576
  /** Randomly returns either -1 or 1
528
577
  * @return {Number}
529
578
  * @memberof Random */
530
579
  function randSign() { return randInt(2) * 2 - 1; }
531
580
 
581
+ /** Returns a random Vector2 with the passed in length
582
+ * @param {Number} [length=1]
583
+ * @return {Vector2}
584
+ * @memberof Random */
585
+ function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
586
+
532
587
  /** Returns a random Vector2 within a circular shape
533
588
  * @param {Number} [radius=1]
534
589
  * @param {Number} [minRadius=0]
@@ -537,44 +592,62 @@ function randSign() { return randInt(2) * 2 - 1; }
537
592
  function randInCircle(radius=1, minRadius=0)
538
593
  { return radius > 0 ? randVector(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
539
594
 
540
- /** Returns a random Vector2 with the passed in length
541
- * @param {Number} [length=1]
542
- * @return {Vector2}
543
- * @memberof Random */
544
- function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
545
-
546
595
  /** Returns a random color between the two passed in colors, combine components if linear
547
596
  * @param {Color} [colorA=Color()]
548
597
  * @param {Color} [colorB=Color(0,0,0,1)]
549
598
  * @param {Boolean} [linear]
550
599
  * @return {Color}
551
600
  * @memberof Random */
552
- function randColor(cA = new Color, cB = new Color(0,0,0,1), linear)
553
- { return linear ? cA.lerp(cB, rand()) : new Color(rand(cA.r,cB.r),rand(cA.g,cB.g),rand(cA.b,cB.b),rand(cA.a,cB.a)); }
554
-
555
- /** Seed used by the randSeeded function
556
- * @type {Number}
557
- * @default
558
- * @memberof Random */
559
- let randSeed = 1;
601
+ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear)
602
+ {
603
+ return linear ? colorA.lerp(colorB, rand()) :
604
+ new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
605
+ }
560
606
 
561
- /** Set seed used by the randSeeded function, should not be 0
562
- * @param {Number} seed
563
- * @memberof Random */
564
- function setRandSeed(seed) { randSeed = seed; }
607
+ ///////////////////////////////////////////////////////////////////////////////
565
608
 
566
- /** Returns a seeded random value between the two values passed in using randSeed
567
- * @param {Number} [valueA=1]
568
- * @param {Number} [valueB=0]
569
- * @return {Number}
570
- * @memberof Random */
571
- function randSeeded(a=1, b=0)
609
+ /**
610
+ * Seeded random number generator
611
+ * - Can be used to create a deterministic random number sequence
612
+ * @example
613
+ * let r = new RandomGenerator(123); // random number generator with seed 123
614
+ * let a = r.rand(); // random value between 0 and 1
615
+ * let b = r.randInt(10); // random integer between 0 and 9
616
+ * r.seed = 123; // reset the seed
617
+ * let c = r.rand(); // the same value as a
618
+ */
619
+ class RandomGenerator
572
620
  {
573
- // xorshift algorithm
574
- randSeed ^= randSeed << 13;
575
- randSeed ^= randSeed >>> 17;
576
- randSeed ^= randSeed << 5;
577
- return b + (a-b) * abs(randSeed % 1e9) / 1e9;
621
+ /** Create a random number generator with the seed passed in
622
+ * @param {Number} seed - Starting seed */
623
+ constructor(seed)
624
+ {
625
+ /** @property {Number} - random seed */
626
+ this.seed = seed;
627
+ }
628
+
629
+ /** Returns a seeded random value between the two values passed in
630
+ * @param {Number} [valueA=1]
631
+ * @param {Number} [valueB=0]
632
+ * @return {Number} */
633
+ float(valueA=1, valueB=0)
634
+ {
635
+ // xorshift algorithm
636
+ this.seed ^= this.seed << 13;
637
+ this.seed ^= this.seed >>> 17;
638
+ this.seed ^= this.seed << 5;
639
+ return valueB + (valueA - valueB) * abs(this.seed % 1e9) / 1e9;
640
+ }
641
+
642
+ /** Returns a floored seeded random value the two values passed in
643
+ * @param {Number} valueA
644
+ * @param {Number} [valueB=0]
645
+ * @return {Number} */
646
+ int(valueA, valueB=0) { return Math.floor(this.rand(valueA, valueB)); }
647
+
648
+ /** Randomly returns either -1 or 1 deterministically
649
+ * @return {Number} */
650
+ sign() { return this.randInt(2) * 2 - 1; }
578
651
  }
579
652
 
580
653
  ///////////////////////////////////////////////////////////////////////////////
@@ -596,7 +669,7 @@ function vec2(x=0, y)
596
669
 
597
670
  /**
598
671
  * Check if object is a valid Vector2
599
- * @param {Vector2} vector
672
+ * @param {Vector2} v
600
673
  * @return {Boolean}
601
674
  * @memberof Utilities
602
675
  */
@@ -629,27 +702,27 @@ class Vector2
629
702
  copy() { return new Vector2(this.x, this.y); }
630
703
 
631
704
  /** Returns a copy of this vector plus the vector passed in
632
- * @param {Vector2} vector
705
+ * @param {Vector2} v - other vector
633
706
  * @return {Vector2} */
634
707
  add(v) { ASSERT(isVector2(v)); return new Vector2(this.x + v.x, this.y + v.y); }
635
708
 
636
709
  /** Returns a copy of this vector minus the vector passed in
637
- * @param {Vector2} vector
710
+ * @param {Vector2} v - other vector
638
711
  * @return {Vector2} */
639
712
  subtract(v) { ASSERT(isVector2(v)); return new Vector2(this.x - v.x, this.y - v.y); }
640
713
 
641
714
  /** Returns a copy of this vector times the vector passed in
642
- * @param {Vector2} vector
715
+ * @param {Vector2} v - other vector
643
716
  * @return {Vector2} */
644
717
  multiply(v) { ASSERT(isVector2(v)); return new Vector2(this.x * v.x, this.y * v.y); }
645
718
 
646
719
  /** Returns a copy of this vector divided by the vector passed in
647
- * @param {Vector2} vector
720
+ * @param {Vector2} v - other vector
648
721
  * @return {Vector2} */
649
722
  divide(v) { ASSERT(isVector2(v)); return new Vector2(this.x / v.x, this.y / v.y); }
650
723
 
651
724
  /** Returns a copy of this vector scaled by the vector passed in
652
- * @param {Number} scale
725
+ * @param {Number} s - scale
653
726
  * @return {Vector2} */
654
727
  scale(s) { ASSERT(!isVector2(s)); return new Vector2(this.x * s, this.y * s); }
655
728
 
@@ -662,12 +735,12 @@ class Vector2
662
735
  lengthSquared() { return this.x**2 + this.y**2; }
663
736
 
664
737
  /** Returns the distance from this vector to vector passed in
665
- * @param {Vector2} vector
738
+ * @param {Vector2} v - other vector
666
739
  * @return {Number} */
667
740
  distance(v) { return this.distanceSquared(v)**.5; }
668
741
 
669
742
  /** Returns the distance squared from this vector to vector passed in
670
- * @param {Vector2} vector
743
+ * @param {Vector2} v - other vector
671
744
  * @return {Number} */
672
745
  distanceSquared(v) { return (this.x - v.x)**2 + (this.y - v.y)**2; }
673
746
 
@@ -682,12 +755,12 @@ class Vector2
682
755
  clampLength(length=1) { const l = this.length(); return l > length ? this.scale(length/l) : this; }
683
756
 
684
757
  /** Returns the dot product of this and the vector passed in
685
- * @param {Vector2} vector
758
+ * @param {Vector2} v - other vector
686
759
  * @return {Number} */
687
760
  dot(v) { ASSERT(isVector2(v)); return this.x*v.x + this.y*v.y; }
688
761
 
689
762
  /** Returns the cross product of this and the vector passed in
690
- * @param {Vector2} vector
763
+ * @param {Vector2} v - other vector
691
764
  * @return {Number} */
692
765
  cross(v) { ASSERT(isVector2(v)); return this.x*v.y - this.y*v.x; }
693
766
 
@@ -699,12 +772,17 @@ class Vector2
699
772
  * @param {Number} [angle=0]
700
773
  * @param {Number} [length=1]
701
774
  * @return {Vector2} */
702
- setAngle(a=0, length=1) { this.x = length*Math.sin(a); this.y = length*Math.cos(a); return this; }
775
+ setAngle(angle=0, length=1)
776
+ { this.x = length*Math.sin(angle); this.y = length*Math.cos(angle); return this; }
703
777
 
704
778
  /** Returns copy of this vector rotated by the angle passed in
705
779
  * @param {Number} angle
706
780
  * @return {Vector2} */
707
- rotate(a) { const c = Math.cos(a), s = Math.sin(a); return new Vector2(this.x*c-this.y*s, this.x*s+this.y*c); }
781
+ rotate(angle)
782
+ {
783
+ const c = Math.cos(angle), s = Math.sin(angle);
784
+ return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
785
+ }
708
786
 
709
787
  /** Returns the integer direction of this vector, corrosponding to multiples of 90 degree rotation (0-3)
710
788
  * @return {Number} */
@@ -723,10 +801,11 @@ class Vector2
723
801
  area() { return abs(this.x * this.y); }
724
802
 
725
803
  /** Returns a new vector that is p percent between this and the vector passed in
726
- * @param {Vector2} vector
804
+ * @param {Vector2} v - other vector
727
805
  * @param {Number} percent
728
806
  * @return {Vector2} */
729
- lerp(v, p) { ASSERT(isVector2(v)); return this.add(v.subtract(this).scale(clamp(p))); }
807
+ lerp(v, percent)
808
+ { ASSERT(isVector2(v)); return this.add(v.subtract(this).scale(clamp(percent))); }
730
809
 
731
810
  /** Returns true if this vector is within the bounds of an array size passed in
732
811
  * @param {Vector2} arraySize
@@ -744,10 +823,10 @@ class Vector2
744
823
 
745
824
  /**
746
825
  * Create a color object with RGBA values
747
- * @param {Number} [r=1]
748
- * @param {Number} [g=1]
749
- * @param {Number} [b=1]
750
- * @param {Number} [a=1]
826
+ * @param {Number} [r=1] - red
827
+ * @param {Number} [g=1] - green
828
+ * @param {Number} [b=1] - blue
829
+ * @param {Number} [a=1] - alpha
751
830
  * @return {Color}
752
831
  * @memberof Utilities
753
832
  */
@@ -755,10 +834,10 @@ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
755
834
 
756
835
  /**
757
836
  * Create a color object with HSLA values
758
- * @param {Number} [h=0]
759
- * @param {Number} [s=0]
760
- * @param {Number} [l=1]
761
- * @param {Number} [a=1]
837
+ * @param {Number} [h=0] - hue
838
+ * @param {Number} [s=0] - saturation
839
+ * @param {Number} [l=1] - lightness
840
+ * @param {Number} [a=1] - alpha
762
841
  * @return {Color}
763
842
  * @memberof Utilities
764
843
  */
@@ -775,11 +854,11 @@ function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
775
854
  */
776
855
  class Color
777
856
  {
778
- /** Create a color with the components passed in, white by default
779
- * @param {Number} [red=1]
780
- * @param {Number} [green=1]
781
- * @param {Number} [blue=1]
782
- * @param {Number} [alpha=1] */
857
+ /** Create a color with the rgba components passed in, white by default
858
+ * @param {Number} [r=1] - red
859
+ * @param {Number} [g=1] - green
860
+ * @param {Number} [b=1] - blue
861
+ * @param {Number} [a=1] - alpha*/
783
862
  constructor(r=1, g=1, b=1, a=1)
784
863
  {
785
864
  /** @property {Number} - Red */
@@ -797,22 +876,22 @@ class Color
797
876
  copy() { return new Color(this.r, this.g, this.b, this.a); }
798
877
 
799
878
  /** Returns a copy of this color plus the color passed in
800
- * @param {Color} color
879
+ * @param {Color} c - other color
801
880
  * @return {Color} */
802
881
  add(c) { return new Color(this.r+c.r, this.g+c.g, this.b+c.b, this.a+c.a); }
803
882
 
804
883
  /** Returns a copy of this color minus the color passed in
805
- * @param {Color} color
884
+ * @param {Color} c - other color
806
885
  * @return {Color} */
807
886
  subtract(c) { return new Color(this.r-c.r, this.g-c.g, this.b-c.b, this.a-c.a); }
808
887
 
809
888
  /** Returns a copy of this color times the color passed in
810
- * @param {Color} color
889
+ * @param {Color} c - other color
811
890
  * @return {Color} */
812
891
  multiply(c) { return new Color(this.r*c.r, this.g*c.g, this.b*c.b, this.a*c.a); }
813
892
 
814
893
  /** Returns a copy of this color divided by the color passed in
815
- * @param {Color} color
894
+ * @param {Color} c - other color
816
895
  * @return {Color} */
817
896
  divide(c) { return new Color(this.r/c.r, this.g/c.g, this.b/c.b, this.a/c.a); }
818
897
 
@@ -820,23 +899,24 @@ class Color
820
899
  * @param {Number} scale
821
900
  * @param {Number} [alphaScale=scale]
822
901
  * @return {Color} */
823
- scale(s, a=s) { return new Color(this.r*s, this.g*s, this.b*s, this.a*a); }
902
+ scale(scale, alphaScale=scale)
903
+ { return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
824
904
 
825
905
  /** Returns a copy of this color clamped to the valid range between 0 and 1
826
906
  * @return {Color} */
827
907
  clamp() { return new Color(clamp(this.r), clamp(this.g), clamp(this.b), clamp(this.a)); }
828
908
 
829
909
  /** Returns a new color that is p percent between this and the color passed in
830
- * @param {Color} color
910
+ * @param {Color} c - other color
831
911
  * @param {Number} percent
832
912
  * @return {Color} */
833
- lerp(c, p) { return this.add(c.subtract(this).scale(clamp(p))); }
913
+ lerp(c, percent) { return this.add(c.subtract(this).scale(clamp(percent))); }
834
914
 
835
915
  /** Sets this color given a hue, saturation, lightness, and alpha
836
- * @param {Number} [hue=0]
837
- * @param {Number} [saturation=0]
838
- * @param {Number} [lightness=1]
839
- * @param {Number} [alpha=1]
916
+ * @param {Number} [h=0] - hue
917
+ * @param {Number} [s=0] - saturation
918
+ * @param {Number} [l=1] - lightness
919
+ * @param {Number} [a=1] - alpha
840
920
  * @return {Color} */
841
921
  setHSLA(h=0, s=0, l=1, a=1)
842
922
  {
@@ -977,14 +1057,15 @@ class Timer
977
1057
 
978
1058
  /** Returns this timer expressed as a string
979
1059
  * @return {String} */
980
- toString() { if (debug) { return this.unset() ? 'unset' : Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ); }}
1060
+ toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
981
1061
 
982
1062
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
983
1063
  * @return {Number} */
984
1064
  valueOf() { return this.get(); }
985
- }
1065
+ }
986
1066
  /**
987
1067
  * LittleJS Engine Settings
1068
+ * - All settings for the engine are here
988
1069
  * @namespace Settings
989
1070
  */
990
1071
 
@@ -1231,7 +1312,7 @@ let medalDisplayIconSize = 50;
1231
1312
  * @type {Boolean}
1232
1313
  * @default 0
1233
1314
  * @memberof Settings */
1234
- let medalsPreventUnlock;
1315
+ let medalsPreventUnlock;
1235
1316
  /**
1236
1317
  * LittleJS Object System
1237
1318
  */
@@ -1240,12 +1321,12 @@ let medalsPreventUnlock;
1240
1321
 
1241
1322
  /**
1242
1323
  * LittleJS Object Base Object Class
1243
- * - Base object class used by the engine
1324
+ * - Top level object class used by the engine
1244
1325
  * - Automatically adds self to object list
1245
1326
  * - Will be updated and rendered each frame
1246
1327
  * - Renders as a sprite from a tilesheet by default
1247
1328
  * - Can have color and addtive color applied
1248
- * - 2d Physics and collision system
1329
+ * - 2D Physics and collision system
1249
1330
  * - Sorted by renderOrder
1250
1331
  * - Objects can have children attached
1251
1332
  * - Parents are updated before children, and set child transform
@@ -1312,7 +1393,7 @@ class EngineObject
1312
1393
  /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1313
1394
  this.renderOrder = renderOrder;
1314
1395
  /** @property {Vector2} [velocity=Vector2()] - Velocity of the object */
1315
- this.velocity = new Vector2();
1396
+ this.velocity = vec2();
1316
1397
  /** @property {Number} [angleVelocity=0] - Angular velocity of the object */
1317
1398
  this.angleVelocity = 0;
1318
1399
 
@@ -1372,15 +1453,17 @@ class EngineObject
1372
1453
  for (const o of engineObjectsCollide)
1373
1454
  {
1374
1455
  // non solid objects don't collide with eachother
1375
- if (!this.isSolid & !o.isSolid || o.destroyed || o.parent || o == this)
1456
+ if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o == this)
1376
1457
  continue;
1377
1458
 
1378
1459
  // check collision
1379
1460
  if (!isOverlapping(this.pos, this.size, o.pos, o.size))
1380
1461
  continue;
1381
1462
 
1382
- // pass collision to objects
1383
- if (!this.collideWithObject(o) | !o.collideWithObject(this))
1463
+ // notify objects of collision and check if should be resolved
1464
+ const collide1 = this.collideWithObject(o);
1465
+ const collide2 = o.collideWithObject(this);
1466
+ if (!collide1 || !collide2)
1384
1467
  continue;
1385
1468
 
1386
1469
  if (isOverlapping(oldPos, this.size, o.pos, o.size))
@@ -1405,7 +1488,7 @@ class EngineObject
1405
1488
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
1406
1489
  const elasticity = max(this.elasticity, o.elasticity);
1407
1490
 
1408
- if (smallStepUp | isBlockedY | !isBlockedX) // resolve y collision
1491
+ if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
1409
1492
  {
1410
1493
  // push outside object collision
1411
1494
  this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
@@ -1434,7 +1517,7 @@ class EngineObject
1434
1517
  o.velocity.y = lerp(elasticity, inelastic, elastic1);
1435
1518
  }
1436
1519
  }
1437
- if (!smallStepUp & isBlockedX) // resolve x collision
1520
+ if (!smallStepUp && isBlockedX) // resolve x collision
1438
1521
  {
1439
1522
  // push outside collision
1440
1523
  this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
@@ -1469,9 +1552,9 @@ class EngineObject
1469
1552
  if (!tileCollisionTest(oldPos, this.size, this))
1470
1553
  {
1471
1554
  // test which side we bounced off (or both if a corner)
1472
- const isBlockedY = tileCollisionTest(new Vector2(oldPos.x, this.pos.y), this.size, this);
1473
- const isBlockedX = tileCollisionTest(new Vector2(this.pos.x, oldPos.y), this.size, this);
1474
- if (isBlockedY | !isBlockedX)
1555
+ const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
1556
+ const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
1557
+ if (isBlockedY || !isBlockedX)
1475
1558
  {
1476
1559
  // set if landed on ground
1477
1560
  this.groundObject = wasMovingDown;
@@ -1534,7 +1617,7 @@ class EngineObject
1534
1617
  * @param {EngineObject} object - the object to test against
1535
1618
  * @return {Boolean} - true if the collision should be resolved
1536
1619
  */
1537
- collideWithObject(o) { return 1; }
1620
+ collideWithObject(object) { return 1; }
1538
1621
 
1539
1622
  /** How long since the object was created
1540
1623
  * @return {Number} */
@@ -1542,7 +1625,7 @@ class EngineObject
1542
1625
 
1543
1626
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
1544
1627
  * @param {Vector2} acceleration */
1545
- applyAcceleration(a) { if (this.mass) this.velocity = this.velocity.add(a); }
1628
+ applyAcceleration(acceleration) { if (this.mass) this.velocity = this.velocity.add(acceleration); }
1546
1629
 
1547
1630
  /** Apply force to this object (adjust velocity, affected by mass)
1548
1631
  * @param {Vector2} force */
@@ -1607,12 +1690,13 @@ class EngineObject
1607
1690
  return text;
1608
1691
  }
1609
1692
  }
1610
- }
1693
+ }
1611
1694
  /**
1612
1695
  * LittleJS Drawing System
1613
- * - Hybrid with both Canvas2D and WebGL available
1696
+ * - Hybrid system with both Canvas2D and WebGL available
1614
1697
  * - Super fast tile sheet rendering with WebGL
1615
1698
  * - Can apply rotation, mirror, color and additive color
1699
+ * - Font rendering system with built in engine font
1616
1700
  * - Many useful utility functions
1617
1701
  *
1618
1702
  * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
@@ -1622,7 +1706,6 @@ class EngineObject
1622
1706
  * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
1623
1707
  *
1624
1708
  * The WebGL rendering system is very fast with some caveats...
1625
- * - The default setup supports only 1 tile sheet, to support more call glCreateTexture and glSetTexture
1626
1709
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
1627
1710
  * - Group additive rendering together using renderOrder to mitigate this issue
1628
1711
  *
@@ -1666,25 +1749,29 @@ const tileImage = new Image;
1666
1749
  let tileImageSize, tileImageFixBleed, drawCount;
1667
1750
 
1668
1751
  /** Convert from screen to world space coordinates
1669
- * - if calling outside of render, you may need to manually set mainCanvasSize
1670
1752
  * @param {Vector2} screenPos
1671
1753
  * @return {Vector2}
1672
1754
  * @memberof Draw */
1673
1755
  function screenToWorld(screenPos)
1674
1756
  {
1675
- ASSERT(mainCanvasSize.x && mainCanvasSize.y, 'mainCanvasSize is invalid');
1676
- return screenPos.add(vec2(.5)).subtract(mainCanvasSize.scale(.5)).multiply(vec2(1/cameraScale,-1/cameraScale)).add(cameraPos);
1757
+ return new Vector2
1758
+ (
1759
+ (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale + cameraPos.x,
1760
+ (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale + cameraPos.y
1761
+ );
1677
1762
  }
1678
1763
 
1679
1764
  /** Convert from world to screen space coordinates
1680
- * - if calling outside of render, you may need to manually set mainCanvasSize
1681
1765
  * @param {Vector2} worldPos
1682
1766
  * @return {Vector2}
1683
1767
  * @memberof Draw */
1684
1768
  function worldToScreen(worldPos)
1685
1769
  {
1686
- ASSERT(mainCanvasSize.x && mainCanvasSize.y, 'mainCanvasSize is invalid');
1687
- return worldPos.subtract(cameraPos).multiply(vec2(cameraScale,-cameraScale)).add(mainCanvasSize.scale(.5)).subtract(vec2(.5));
1770
+ return new Vector2
1771
+ (
1772
+ (worldPos.x - cameraPos.x) * cameraScale + mainCanvasSize.x/2 - .5,
1773
+ (worldPos.y - cameraPos.y) * -cameraScale + mainCanvasSize.y/2 - .5
1774
+ );
1688
1775
  }
1689
1776
 
1690
1777
  /** Draw textured tile centered in world space, with color applied if using WebGL
@@ -1697,13 +1784,21 @@ function worldToScreen(worldPos)
1697
1784
  * @param {Boolean} [mirror=0] - If true image is flipped along the Y axis
1698
1785
  * @param {Color} [additiveColor=Color(0,0,0,0)] - Additive color to be applied
1699
1786
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1787
+ * @param {Boolean} [screenSpace=0] - If true the pos and size are in screen space
1700
1788
  * @memberof Draw */
1701
- function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color, angle=0, mirror,
1702
- additiveColor=new Color(0,0,0,0), useWebGL=glEnable)
1789
+ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color,
1790
+ angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace)
1703
1791
  {
1704
1792
  showWatermark && ++drawCount;
1793
+
1705
1794
  if (glEnable && useWebGL)
1706
1795
  {
1796
+ if (screenSpace)
1797
+ {
1798
+ // convert to world space
1799
+ pos = screenToWorld(pos);
1800
+ size = size.scale(1/cameraScale);
1801
+ }
1707
1802
  if (tileIndex < 0 || !tileImage.width)
1708
1803
  {
1709
1804
  // if negative tile index or image not found, force untextured
@@ -1745,7 +1840,7 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1745
1840
  context.globalAlpha = color.a; // only alpha is supported
1746
1841
  context.drawImage(tileImage, sX, sY, sWidth, sHeight, -.5, -.5, 1, 1);
1747
1842
  }
1748
- });
1843
+ }, undefined, screenSpace);
1749
1844
  }
1750
1845
  }
1751
1846
 
@@ -1755,38 +1850,30 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1755
1850
  * @param {Color} [color=Color()]
1756
1851
  * @param {Number} [angle=0]
1757
1852
  * @param {Boolean} [useWebGL=glEnable]
1853
+ * @param {Boolean} [screenSpace=0]
1758
1854
  * @memberof Draw */
1759
- function drawRect(pos, size, color, angle, useWebGL)
1760
- {
1761
- drawTile(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1762
- }
1763
-
1764
- /** Draw textured tile centered on pos in screen space
1765
- * @param {Vector2} pos - Center of the tile
1766
- * @param {Vector2} [size=Vector2(1,1)] - Size of the tile
1767
- * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1768
- * @param {Vector2} [tileSize=tileSizeDefault] - Tile size in source pixels
1769
- * @param {Color} [color=Color()]
1770
- * @param {Number} [angle=0]
1771
- * @param {Boolean} [mirror=0]
1772
- * @param {Color} [additiveColor=Color(0,0,0,0)]
1773
- * @param {Boolean} [useWebGL=glEnable]
1774
- * @memberof Draw */
1775
- function drawTileScreenSpace(pos, size=vec2(1), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL)
1776
- {
1777
- drawTile(screenToWorld(pos), size.scale(1/cameraScale), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL);
1778
- }
1855
+ function drawRect(pos, size, color, angle, useWebGL, screenSpace)
1856
+ { drawTile(pos, size, -1, tileSizeDefault, color, angle, 0, undefined, useWebGL, screenSpace); }
1779
1857
 
1780
- /** Draw colored rectangle in screen space
1781
- * @param {Vector2} pos
1782
- * @param {Vector2} [size=Vector2(1,1)]
1858
+ /** Draw colored polygon using passed in points
1859
+ * @param {Array} points - Array of Vector2 points
1783
1860
  * @param {Color} [color=Color()]
1784
- * @param {Number} [angle=0]
1785
1861
  * @param {Boolean} [useWebGL=glEnable]
1862
+ * @param {Boolean} [screenSpace=0]
1786
1863
  * @memberof Draw */
1787
- function drawRectScreenSpace(pos, size, color, angle, useWebGL)
1864
+ function drawPoly(points, color=new Color, useWebGL=glEnable, screenSpace)
1788
1865
  {
1789
- drawTileScreenSpace(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1866
+ if (useWebGL)
1867
+ glDrawPoints(screenSpace ? points.map(screenToWorld) : points, color.rgbaInt());
1868
+ else
1869
+ {
1870
+ // draw using canvas
1871
+ mainContext.fillStyle = color;
1872
+ mainContext.beginPath();
1873
+ for (const point of screenSpace ? points : points.map(worldToScreen))
1874
+ mainContext.lineTo(point.x, point.y);
1875
+ mainContext.fill();
1876
+ }
1790
1877
  }
1791
1878
 
1792
1879
  /** Draw colored line between two points
@@ -1795,12 +1882,13 @@ function drawRectScreenSpace(pos, size, color, angle, useWebGL)
1795
1882
  * @param {Number} [thickness=.1]
1796
1883
  * @param {Color} [color=Color()]
1797
1884
  * @param {Boolean} [useWebGL=glEnable]
1885
+ * @param {Boolean} [screenSpace=0]
1798
1886
  * @memberof Draw */
1799
1887
  function drawLine(posA, posB, thickness=.1, color, useWebGL)
1800
1888
  {
1801
1889
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
1802
1890
  const size = vec2(thickness, halfDelta.length()*2);
1803
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL);
1891
+ drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace);
1804
1892
  }
1805
1893
 
1806
1894
  /** Draw directly to a 2d canvas context in world space
@@ -1810,12 +1898,16 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL)
1810
1898
  * @param {Boolean} mirror
1811
1899
  * @param {Function} drawFunction
1812
1900
  * @param {CanvasRenderingContext2D} [context=mainContext]
1901
+ * @param {Boolean} [screenSpace=0]
1813
1902
  * @memberof Draw */
1814
- function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainContext)
1903
+ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainContext, screenSpace)
1815
1904
  {
1816
- // create canvas transform from world space to screen space
1817
- pos = worldToScreen(pos);
1818
- size = size.scale(cameraScale);
1905
+ if (!screenSpace)
1906
+ {
1907
+ // create canvas transform from world space to screen space
1908
+ pos = worldToScreen(pos);
1909
+ size = size.scale(cameraScale);
1910
+ }
1819
1911
  context.save();
1820
1912
  context.translate(pos.x+.5|0, pos.y+.5|0);
1821
1913
  context.rotate(angle);
@@ -1922,6 +2014,17 @@ class FontImage
1922
2014
  this.context = context;
1923
2015
  }
1924
2016
 
2017
+ /** Draw text in world space using the image font
2018
+ * @param {String} text
2019
+ * @param {Vector2} pos
2020
+ * @param {Number} [scale=.25]
2021
+ * @param {Boolean} [center]
2022
+ */
2023
+ drawText(text, pos, scale=1, center)
2024
+ {
2025
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
2026
+ }
2027
+
1925
2028
  /** Draw text in screen space using the image font
1926
2029
  * @param {String} text
1927
2030
  * @param {Vector2} pos
@@ -1959,17 +2062,6 @@ class FontImage
1959
2062
 
1960
2063
  context.restore();
1961
2064
  }
1962
-
1963
- /** Draw text in world space using the image font
1964
- * @param {String} text
1965
- * @param {Vector2} pos
1966
- * @param {Number} [scale=.25]
1967
- * @param {Boolean} [center]
1968
- */
1969
- drawText(text, pos, scale=1, center)
1970
- {
1971
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
1972
- }
1973
2065
  }
1974
2066
 
1975
2067
  ///////////////////////////////////////////////////////////////////////////////
@@ -1992,13 +2084,13 @@ function toggleFullscreen()
1992
2084
  else if (document.body.requestFullscreen)
1993
2085
  document.body.requestFullscreen();
1994
2086
  }
1995
-
2087
+
1996
2088
  /**
1997
2089
  * LittleJS Input System
1998
- * - Tracks key down, pressed, and released
1999
- * - Also tracks mouse buttons, position, and wheel
2000
- * - Supports multiple gamepads
2001
- * - Virtual gamepad for touch devices with touchGamepadSize
2090
+ * - Tracks keyboard down, pressed, and released
2091
+ * - Tracks mouse buttons, position, and wheel
2092
+ * - Tracks multiple analog gamepads
2093
+ * - Virtual gamepad for touch devices
2002
2094
  * @namespace Input
2003
2095
  */
2004
2096
 
@@ -2270,40 +2362,32 @@ if (isTouchDevice)
2270
2362
  let wasTouching, mouseDown = onmousedown, mouseUp = onmouseup;
2271
2363
  onmousedown = onmouseup = ()=> 0;
2272
2364
 
2273
- // setup touch input
2274
- ontouchstart = (e)=>
2365
+ // handle all touch events the same way
2366
+ ontouchstart = ontouchmove = ontouchend = (e)=>
2275
2367
  {
2276
- // fix mobile audio, force it to play a sound on first touch
2277
- zzfx(0);
2278
-
2279
- // handle all touch events the same way
2280
- ontouchstart = ontouchmove = ontouchend = (e)=>
2281
- {
2282
- e.button = 0; // all touches are left click
2283
-
2284
- // check if touching and pass to mouse events
2285
- const touching = e.touches.length;
2286
- if (touching)
2287
- {
2288
- // set event pos and pass it along
2289
- e.x = e.touches[0].clientX;
2290
- e.y = e.touches[0].clientY;
2291
- wasTouching ? onmousemove(e) : mouseDown(e);
2292
- }
2293
- else if (wasTouching)
2294
- mouseUp(e);
2368
+ e.button = 0; // all touches are left click
2295
2369
 
2296
- // set was touching
2297
- wasTouching = touching;
2370
+ // fix stalled audio on mobile
2371
+ if (soundEnable)
2372
+ audioContext ? audioContext.resume() : zzfx(0);
2298
2373
 
2299
- // must return true so the document will get focus
2300
- return true;
2374
+ // check if touching and pass to mouse events
2375
+ const touching = e.touches.length;
2376
+ if (touching)
2377
+ {
2378
+ // set event pos and pass it along
2379
+ e.x = e.touches[0].clientX;
2380
+ e.y = e.touches[0].clientY;
2381
+ wasTouching ? onmousemove(e) : mouseDown(e);
2301
2382
  }
2383
+ else if (wasTouching)
2384
+ mouseUp(e);
2302
2385
 
2303
- // try to create touch game pad
2304
- touchGamepadEnable && touchGamepadCreate();
2386
+ // set was touching
2387
+ wasTouching = touching;
2305
2388
 
2306
- return ontouchstart(e);
2389
+ // must return true so the document will get focus
2390
+ return true;
2307
2391
  }
2308
2392
  }
2309
2393
 
@@ -2314,13 +2398,13 @@ if (isTouchDevice)
2314
2398
  let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2315
2399
 
2316
2400
  // create the touch gamepad, called automatically by the engine
2317
- function touchGamepadCreate()
2401
+ if (touchGamepadEnable)
2318
2402
  {
2319
2403
  // touch input internal variables
2320
2404
  touchGamepadButtons = [];
2321
2405
  touchGamepadStick = vec2();
2322
2406
 
2323
- let touchHandler = ontouchstart;
2407
+ const touchHandler = ontouchstart;
2324
2408
  ontouchstart = ontouchmove = ontouchend = (e)=>
2325
2409
  {
2326
2410
  // clear touch gamepad input
@@ -2426,7 +2510,7 @@ function touchGamepadRender()
2426
2510
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2427
2511
  for (let i=4; i--;)
2428
2512
  {
2429
- const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize/2));
2513
+ const pos = rightCenter.add(vec2().setAngle(i*PI/2, touchGamepadSize/2));
2430
2514
  overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2431
2515
  overlayContext.beginPath();
2432
2516
  overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
@@ -2436,15 +2520,15 @@ function touchGamepadRender()
2436
2520
 
2437
2521
  // set canvas back to normal
2438
2522
  overlayContext.restore();
2439
- }
2523
+ }
2440
2524
  /**
2441
2525
  * LittleJS Audio System
2442
- * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - Sound Effect Generator
2443
- * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - Music System
2526
+ * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
2527
+ * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
2444
2528
  * - Caches sounds and music for fast playback
2445
2529
  * - Can attenuate and apply stereo panning to sounds
2446
2530
  * - Ability to play mp3, ogg, and wave files
2447
- * - Speech synthesis wrapper functions
2531
+ * - Speech synthesis functions
2448
2532
  * @namespace Audio
2449
2533
  */
2450
2534
 
@@ -2478,12 +2562,15 @@ class Sound
2478
2562
  /** @property {Number} - At what percentage of range should it start tapering off */
2479
2563
  this.taper = taper;
2480
2564
 
2481
- // get randomness from sound parameters
2482
- this.randomness = zzfxSound[1] || 0;
2483
- zzfxSound[1] = 0;
2565
+ /** @property {Number} - How much to randomize frequency each time sound plays */
2566
+ this.randomness = 0;
2484
2567
 
2485
- // generate sound now for fast playback
2486
- this.cachedSamples = zzfxG(...zzfxSound);
2568
+ if (zzfxSound)
2569
+ {
2570
+ // generate zzfx sound now for fast playback
2571
+ this.randomness = zzfxSound[1] || (zzfxSound[1] = 0);
2572
+ this.cachedSamples = zzfxSound && zzfxG(...zzfxSound);
2573
+ }
2487
2574
  }
2488
2575
 
2489
2576
  /** Play the sound
@@ -2495,7 +2582,7 @@ class Sound
2495
2582
  */
2496
2583
  play(pos, volume=1, pitch=1, randomnessScale=1)
2497
2584
  {
2498
- if (!soundEnable) return;
2585
+ if (!soundEnable || !this.cachedSamples) return;
2499
2586
 
2500
2587
  let pan;
2501
2588
  if (pos)
@@ -2528,12 +2615,36 @@ class Sound
2528
2615
  * @return {AudioBufferSourceNode} - The audio, can be used to stop sound later
2529
2616
  */
2530
2617
  playNote(semitoneOffset, pos, volume)
2618
+ { return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
2619
+ }
2620
+
2621
+ /**
2622
+ * Sound Wave Object - Stores a wave sound for later use and can be played positionally
2623
+ */
2624
+ class SoundWave extends Sound
2625
+ {
2626
+ /** Create a sound object and cache the wave file for later use
2627
+ * @param {String} waveFilename - Filename of wave file to load
2628
+ * @param {Number} [randomness=.05] - How much to randomize frequency each time sound plays
2629
+ * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2630
+ * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
2631
+ */
2632
+ constructor(waveFilename, randomness=.05, range, taper)
2531
2633
  {
2634
+ super(0, range, taper);
2635
+ this.randomness = randomness;
2636
+
2532
2637
  if (!soundEnable) return;
2638
+ if (!soundWaveDecoderContext)
2639
+ soundDecoderContext = new AudioContext;
2533
2640
 
2534
- return this.play(pos, volume, 2**(semitoneOffset/12), 0);
2641
+ fetch(waveFilename)
2642
+ .then(response => response.arrayBuffer())
2643
+ .then(arrayBuffer => soundWaveDecoderContext.decodeAudioData(arrayBuffer))
2644
+ .then(audioBuffer => this.cachedSamples = audioBuffer.getChannelData(0));
2535
2645
  }
2536
2646
  }
2647
+ let soundDecoderContext; // audio context used only to decode audio files
2537
2648
 
2538
2649
  /**
2539
2650
  * Music Object - Stores a zzfx music track for later use
@@ -2676,16 +2787,17 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2676
2787
  {
2677
2788
  if (!soundEnable) return;
2678
2789
 
2679
- // create audio context
2790
+ // create audio context if needed
2680
2791
  if (!audioContext)
2681
2792
  audioContext = new AudioContext;
2682
2793
 
2683
- // fix stalled audio
2684
- audioContext.resume();
2685
-
2686
2794
  // prevent sounds from building up if they can't be played
2687
2795
  if (audioContext.state != 'running')
2796
+ {
2797
+ // fix stalled audio
2798
+ audioContext.resume();
2688
2799
  return;
2800
+ }
2689
2801
 
2690
2802
  // create buffer and source
2691
2803
  const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, zzfxR),
@@ -2877,7 +2989,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2877
2989
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
2878
2990
 
2879
2991
  // check if there are more channels
2880
- hasMore |= !!patterns[patternIndex][channelIndex];
2992
+ hasMore ||= !!patterns[patternIndex][channelIndex];
2881
2993
 
2882
2994
  // get next offset, use the length of first channel
2883
2995
  nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - !notFirstBeat) * beatLength;
@@ -2890,7 +3002,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2890
3002
 
2891
3003
  // stop if end, different instrument or new note
2892
3004
  stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
2893
- instrument != (patternChannel[0] || 0) | note | 0;
3005
+ instrument != (patternChannel[0] || 0) || note | 0;
2894
3006
 
2895
3007
  // fill buffer with samples for previous beat, most cpu intensive part
2896
3008
  for (j = 0; j < beatLength && notFirstBeat;
@@ -2934,7 +3046,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2934
3046
  }
2935
3047
 
2936
3048
  return [leftChannelBuffer, rightChannelBuffer];
2937
- }
3049
+ }
2938
3050
  /**
2939
3051
  * LittleJS Tile Layer System
2940
3052
  * - Caches arrays of tiles to off screen canvas for fast rendering
@@ -3004,12 +3116,12 @@ function tileCollisionTest(pos, size=vec2(), object)
3004
3116
  for (let x = minX; x < maxX; ++x)
3005
3117
  {
3006
3118
  const tileData = tileCollision[y*tileCollisionSize.x+x];
3007
- if (tileData && (!object || object.collideWithTile(tileData, new Vector2(x, y))))
3119
+ if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
3008
3120
  return 1;
3009
3121
  }
3010
3122
  }
3011
3123
 
3012
- /** Return the center of tile if any that is hit (this does not return the exact hit point)
3124
+ /** Return the center of tile if any that is hit (does not return the exact intersection)
3013
3125
  * @param {Vector2} posStart
3014
3126
  * @param {Vector2} posEnd
3015
3127
  * @param {EngineObject} [object]
@@ -3018,28 +3130,41 @@ function tileCollisionTest(pos, size=vec2(), object)
3018
3130
  function tileCollisionRaycast(posStart, posEnd, object)
3019
3131
  {
3020
3132
  // test if a ray collides with tiles from start to end
3021
- // todo: a way to get the exact hit point, it must still register as inside the hit tile
3022
- const posDelta = (posEnd = posEnd.floor()).subtract(posStart = posStart.floor());
3023
- const dx = abs(posDelta.x), dy = -abs(posDelta.y);
3024
- const sx = sign(posDelta.x), sy = sign(posDelta.y);
3025
-
3026
- for (let x = posStart.x, y = posStart.y, e = dx + dy;;)
3133
+ // todo: a way to get the exact hit point, it must still be inside the hit tile
3134
+ const delta = posEnd.subtract(posStart);
3135
+ const totalLength = delta.length();
3136
+ const normalizedDelta = delta.normalize();
3137
+ const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
3138
+ const flooredPosStart = posStart.floor();
3139
+
3140
+ // setup iteration variables
3141
+ let pos = flooredPosStart;
3142
+ let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
3143
+ let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
3144
+
3145
+ while (1)
3027
3146
  {
3028
- const tileData = getTileCollisionData(vec2(x,y));
3029
- if (tileData && (object ? object.collideWithTileRaycast(tileData, new Vector2(x, y)) : tileData > 0))
3147
+ // check for tile collision
3148
+ const tileData = getTileCollisionData(pos);
3149
+ if (tileData && (!object || object.collideWithTile(tileData, pos)))
3030
3150
  {
3031
- debugRaycast && debugLine(posStart, posEnd, '#f00',.02, 1);
3032
- debugRaycast && debugPoint(new Vector2(x+.5, y+.5), '#ff0', 1);
3033
- return new Vector2(x+.5, y+.5);
3151
+ debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
3152
+ debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
3153
+ return pos.add(vec2(.5));
3034
3154
  }
3035
3155
 
3036
- // update Bresenham line drawing algorithm
3037
- if (x == posEnd.x & y == posEnd.y) break;
3038
- const e2 = 2*e;
3039
- if (e2 >= dy) e += dy, x += sx;
3040
- if (e2 <= dx) e += dx, y += sy;
3156
+ // check if past the end
3157
+ if (xi > totalLength && yi > totalLength)
3158
+ break;
3159
+
3160
+ // get coordinates of the next tile to check
3161
+ if (xi > yi)
3162
+ pos.y += sign(delta.y), yi += unit.y;
3163
+ else
3164
+ pos.x += sign(delta.x), xi += unit.x;
3041
3165
  }
3042
- debugRaycast && debugLine(posStart, posEnd, '#00f',.02, 1);
3166
+
3167
+ debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
3043
3168
  }
3044
3169
 
3045
3170
  ///////////////////////////////////////////////////////////////////////////////
@@ -3282,7 +3407,7 @@ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1)
3282
3407
  * @param {Number} [angle=0] */
3283
3408
  drawRect(pos, size, color, angle)
3284
3409
  { this.drawTile(pos, size, -1, 0, color, angle); }
3285
- }
3410
+ }
3286
3411
  /**
3287
3412
  * LittleJS Particle System
3288
3413
  */
@@ -3370,7 +3495,7 @@ class ParticleEmitter extends EngineObject
3370
3495
  localSpace
3371
3496
  )
3372
3497
  {
3373
- super(pos, new Vector2, tileIndex, tileSize, angle, undefined, renderOrder);
3498
+ super(pos, vec2(), tileIndex, tileSize, angle, undefined, renderOrder);
3374
3499
 
3375
3500
  // emitter settings
3376
3501
  /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
@@ -3437,7 +3562,7 @@ class ParticleEmitter extends EngineObject
3437
3562
  this.parent && super.update();
3438
3563
 
3439
3564
  // update emitter
3440
- if (!this.emitTime | this.getAliveTime() <= this.emitTime)
3565
+ if (!this.emitTime || this.getAliveTime() <= this.emitTime)
3441
3566
  {
3442
3567
  // emit particles
3443
3568
  if (this.emitRate * particleEmitRateScale)
@@ -3459,7 +3584,7 @@ class ParticleEmitter extends EngineObject
3459
3584
  {
3460
3585
  // spawn a particle
3461
3586
  let pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3462
- (new Vector2(rand(-.5,.5), rand(-.5,.5)))
3587
+ vec2(rand(-.5,.5), rand(-.5,.5))
3463
3588
  .multiply(this.emitSize).rotate(this.angle) // box emitter
3464
3589
  : randInCircle(this.emitSize/2); // circle emitter
3465
3590
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
@@ -3489,7 +3614,7 @@ class ParticleEmitter extends EngineObject
3489
3614
  // build particle settings
3490
3615
  particle.colorStart = colorStart;
3491
3616
  particle.colorEndDelta = colorEnd.subtract(colorStart);
3492
- particle.velocity = (new Vector2).setAngle(velocityAngle, speed);
3617
+ particle.velocity = vec2().setAngle(velocityAngle, speed);
3493
3618
  particle.angleVelocity = angleSpeed;
3494
3619
  particle.lifeTime = particleTime;
3495
3620
  particle.sizeStart = sizeStart;
@@ -3534,7 +3659,7 @@ class Particle extends EngineObject
3534
3659
  * @param {Number} [angle=0] - Angle to rotate the particle
3535
3660
  */
3536
3661
  constructor(pos, tileIndex, tileSize, angle)
3537
- { super(pos, new Vector2, tileIndex, tileSize, angle); }
3662
+ { super(pos, vec2(), tileIndex, tileSize, angle); }
3538
3663
 
3539
3664
  /** Render the particle, automatically called each frame, sorted by renderOrder */
3540
3665
  render()
@@ -3542,7 +3667,7 @@ class Particle extends EngineObject
3542
3667
  // modulate size and color
3543
3668
  const p = min((time - this.spawnTime) / this.lifeTime, 1);
3544
3669
  const radius = this.sizeStart + p * this.sizeEndDelta;
3545
- const size = new Vector2(radius, radius);
3670
+ const size = vec2(radius);
3546
3671
  const fadeRate = this.fadeRate/2;
3547
3672
  const color = new Color(
3548
3673
  this.colorStart.r + p * this.colorEndDelta.r,
@@ -3588,7 +3713,7 @@ class Particle extends EngineObject
3588
3713
  this.destroyed = 1;
3589
3714
  }
3590
3715
  }
3591
- }
3716
+ }
3592
3717
  /**
3593
3718
  * LittleJS Medal System
3594
3719
  * - Tracks and displays medals
@@ -3909,7 +4034,7 @@ class Newgrounds
3909
4034
  // end of Crypto-JS
3910
4035
  ///////////////////////////////////////////////////////////////////////////////
3911
4036
  }
3912
- }
4037
+ }
3913
4038
  /**
3914
4039
  * LittleJS WebGL Interface
3915
4040
  * - All webgl used by the engine is wrapped up here
@@ -4115,8 +4240,8 @@ function glFlush()
4115
4240
 
4116
4241
  // draw all the sprites in the batch and reset the buffer
4117
4242
  glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
4118
- glPositionData.subarray(0, glBatchCount * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT));
4119
- glContext.drawArrays(gl_TRIANGLES, 0, glBatchCount * gl_VERTICES_PER_QUAD);
4243
+ glPositionData.subarray(0, glBatchCount * gl_INDICIES_PER_VERT));
4244
+ glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount);
4120
4245
  glBatchCount = 0;
4121
4246
  glBatchAdditive = glAdditive;
4122
4247
  }
@@ -4137,39 +4262,75 @@ function glCopyToContext(context, forceDraw)
4137
4262
  }
4138
4263
 
4139
4264
  /** Add a sprite to the gl draw list, used by all gl draw functions
4140
- * @param x
4141
- * @param y
4142
- * @param sizeX
4143
- * @param sizeY
4144
- * @param angle
4145
- * @param uv0X
4146
- * @param uv0Y
4147
- * @param uv1X
4148
- * @param uv1Y
4149
- * @param rgba
4150
- * @param [rgbaAdditive=0]
4265
+ * @param {Number} x
4266
+ * @param {Number} y
4267
+ * @param {Number} sizeX
4268
+ * @param {Number} sizeY
4269
+ * @param {Number} angle
4270
+ * @param {Number} uv0X
4271
+ * @param {Number} uv0Y
4272
+ * @param {Number} uv1X
4273
+ * @param {Number} uv1Y
4274
+ * @param {Number} rgba
4275
+ * @param {Number} [rgbaAdditive=0]
4151
4276
  * @memberof WebGL */
4152
4277
  function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
4153
4278
  {
4154
- // flush if there is no room for more verts or if different blend mode
4155
- if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
4279
+ // flush if there is not enough room or if different blend mode
4280
+ const vertCount = 6;
4281
+ if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
4156
4282
  glFlush();
4157
4283
 
4158
4284
  // prepare to create the verts from size and angle
4159
4285
  const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
4160
4286
  const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
4161
-
4162
- // setup 2 triangles to form a quad
4163
- for(let i=6, offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT; i--;)
4287
+ const positionData =
4288
+ [
4289
+ x-cx+sy, y+cy+sx, uv0X, uv0Y,
4290
+ x-cx-sy, y-cy+sx, uv0X, uv1Y,
4291
+ x+cx+sy, y+cy-sx, uv1X, uv0Y,
4292
+ x+cx-sy, y-cy-sx, uv1X, uv1Y,
4293
+ ];
4294
+
4295
+ // setup 2 triangle strip quad
4296
+ for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
4164
4297
  {
4165
- const a = i-4&&i>1, b = i-5&&i-2&&i-1;
4166
- glPositionData[offset++] = x + (a?-cx:cx) + (b?sy:-sy);
4167
- glPositionData[offset++] = y + (b?cy:-cy) + (a?sx:-sx);
4168
- glPositionData[offset++] = a ? uv0X : uv1X;
4169
- glPositionData[offset++] = b ? uv0Y : uv1Y;
4298
+ let j = clamp(i-1, 0, 3)*4; // degenerate tri at ends
4299
+ glPositionData[offset++] = positionData[j++];
4300
+ glPositionData[offset++] = positionData[j++];
4301
+ glPositionData[offset++] = positionData[j++];
4302
+ glPositionData[offset++] = positionData[j++];
4170
4303
  glColorData[offset++] = rgba;
4171
4304
  glColorData[offset++] = rgbaAdditive;
4172
4305
  }
4306
+ glBatchCount += vertCount;
4307
+ }
4308
+
4309
+ /** Add a convex polygon to the gl draw list
4310
+ * @param {Array} points - Array of Vector2 points
4311
+ * @param {Number} rgba - Color of the polygon
4312
+ * @memberof WebGL */
4313
+ function glDrawPoints(points, rgba)
4314
+ {
4315
+ // flush if there is not enough room or if different blend mode
4316
+ const vertCount = points.length + 2;
4317
+ if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
4318
+ glFlush();
4319
+
4320
+ // setup triangle strip from list of points
4321
+ for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
4322
+ {
4323
+ const j = clamp(i-1, 0, vertCount-3); // degenerate tri at ends
4324
+ const h = j>>1;
4325
+ const point = points[j%2? h : vertCount-3-h];
4326
+ glPositionData[offset++] = point.x;
4327
+ glPositionData[offset++] = point.y;
4328
+ glPositionData[offset++] = 0; // uvx
4329
+ glPositionData[offset++] = 0; // uvy
4330
+ glColorData[offset++] = 0; // nothing to tint
4331
+ glColorData[offset++] = rgba; // apply rgba via additive
4332
+ }
4333
+ glBatchCount += vertCount;
4173
4334
  }
4174
4335
 
4175
4336
  ///////////////////////////////////////////////////////////////////////////////
@@ -4263,14 +4424,14 @@ function glRenderPostProcess()
4263
4424
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
4264
4425
  glContext.uniform1f(uniformLocation('iTime'), time);
4265
4426
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
4266
- glContext.drawArrays(gl_TRIANGLES, 0, 3);
4427
+ glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 3);
4267
4428
  }
4268
4429
 
4269
4430
  ///////////////////////////////////////////////////////////////////////////////
4270
4431
  // store gl constants as integers so their name doesn't use space in minifed
4271
4432
  const
4272
4433
  gl_ONE = 1,
4273
- gl_TRIANGLES = 4,
4434
+ gl_TRIANGLE_STRIP = 5,
4274
4435
  gl_SRC_ALPHA = 770,
4275
4436
  gl_ONE_MINUS_SRC_ALPHA = 771,
4276
4437
  gl_BLEND = 3042,
@@ -4301,9 +4462,9 @@ gl_UNPACK_FLIP_Y_WEBGL = 37440,
4301
4462
  // constants for batch rendering
4302
4463
  gl_VERTICES_PER_QUAD = 6,
4303
4464
  gl_INDICIES_PER_VERT = 6,
4304
- gl_MAX_BATCH = 1<<16,
4465
+ gl_MAX_BATCH = 1e5,
4305
4466
  gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2, // vec2 * 2 + (char * 4) * 2
4306
- gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE;
4467
+ gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTEX_BYTE_STRIDE;
4307
4468
  /**
4308
4469
  * LittleJS - The Tiny JavaScript Game Engine That Can!
4309
4470
  * MIT License - Copyright 2021 Frank Force
@@ -4336,7 +4497,7 @@ const engineName = 'LittleJS';
4336
4497
  * @type {String}
4337
4498
  * @default
4338
4499
  * @memberof Engine */
4339
- const engineVersion = '1.6.6';
4500
+ const engineVersion = '1.7.01';
4340
4501
 
4341
4502
  /** Frames per second to update objects
4342
4503
  * @type {Number}
@@ -4426,7 +4587,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4426
4587
  // set canvas style
4427
4588
  const styleCanvas = 'position:absolute;' +
4428
4589
  'top:50%;left:50%;transform:translate(-50%,-50%);' + // center the canvas
4429
- (canvasPixelated?'image-rendering:pixelated':''); // set pixelated rendering
4590
+ (canvasPixelated?'image-rendering:pixelated':''); // set pixelated rendering
4430
4591
  (glCanvas||mainCanvas).style = mainCanvas.style = overlayCanvas.style = styleCanvas;
4431
4592
 
4432
4593
  gameInit();
@@ -4434,7 +4595,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4434
4595
  };
4435
4596
 
4436
4597
  // frame time tracking
4437
- let frameTimeLastMS = 0, frameTimeBufferMS, averageFPS;
4598
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
4438
4599
 
4439
4600
  // main update loop
4440
4601
  function engineUpdate(frameTimeMS=0)
@@ -4625,11 +4786,11 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
4625
4786
  for (const o of objects)
4626
4787
  pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
4627
4788
  }
4628
- }
4629
-
4789
+ }
4790
+
4630
4791
  /**
4631
4792
  * LittleJS Module Export
4632
- * - Export engine as a module with extra functions where necessary
4793
+ * - Export engine as a module with functions where necessary
4633
4794
  */
4634
4795
 
4635
4796
  /** Set position of camera in world space
@@ -4810,18 +4971,13 @@ function setMedalDisplayIconSize(size) { medalDisplayIconSize = size; }
4810
4971
  /** Set to stop medals from being unlockable
4811
4972
  * @param {Boolean} preventUnlock
4812
4973
  * @memberof Settings */
4813
- function setMedalsPreventUnlock(prevent) { medalsPreventUnlock = prevent; }
4974
+ function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUnlock; }
4814
4975
 
4815
4976
  /** Set if watermark with FPS should be shown
4816
4977
  * @param {Boolean} show
4817
4978
  * @memberof Debug */
4818
4979
  function setShowWatermark(show) { showWatermark = show; }
4819
4980
 
4820
- /** Set if god mode is enabled
4821
- * @param {Boolean} enable
4822
- * @memberof Debug */
4823
- function setGodMode(enable) { godMode = enable; }
4824
-
4825
4981
  /** Set key code used to toggle debug mode, Esc by default
4826
4982
  * @param {Number} key
4827
4983
  * @memberof Debug */
@@ -4866,7 +5022,6 @@ export {
4866
5022
  setMedalDisplayIconSize,
4867
5023
  setMedalsPreventUnlock,
4868
5024
  setShowWatermark,
4869
- setGodMode,
4870
5025
  setDebugKey,
4871
5026
 
4872
5027
  // Settings
@@ -4909,7 +5064,6 @@ export {
4909
5064
  // Globals
4910
5065
  debug,
4911
5066
  showWatermark,
4912
- godMode,
4913
5067
 
4914
5068
  // Debug
4915
5069
  ASSERT,
@@ -4931,6 +5085,10 @@ export {
4931
5085
  mod,
4932
5086
  clamp,
4933
5087
  percent,
5088
+ distanceWrap,
5089
+ lerpWrap,
5090
+ distanceAngle,
5091
+ lerpAngle,
4934
5092
  lerp,
4935
5093
  smoothStep,
4936
5094
  nearestPowerOfTwo,
@@ -4945,11 +5103,9 @@ export {
4945
5103
  randInCircle,
4946
5104
  randVector,
4947
5105
  randColor,
4948
- randSeed,
4949
- setRandSeed,
4950
- randSeeded,
4951
5106
 
4952
5107
  // Utility Classes
5108
+ RandomGenerator,
4953
5109
  Vector2,
4954
5110
  Color,
4955
5111
  Timer,
@@ -5065,4 +5221,4 @@ export {
5065
5221
  engineObjectsUpdate,
5066
5222
  engineObjectsDestroy,
5067
5223
  engineObjectsCallback,
5068
- };
5224
+ };