littlejsengine 1.1.2 → 1.1.3

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 (94) hide show
  1. package/README.md +55 -0
  2. package/build.bat +2 -1
  3. package/engine/engine.all.js +1320 -930
  4. package/engine/engine.all.min.js +1 -1
  5. package/engine/engine.all.release.js +965 -601
  6. package/engine/engine.js +71 -60
  7. package/engine/engineAudio.js +80 -22
  8. package/engine/engineBuild.bat +2 -2
  9. package/engine/engineDebug.js +65 -36
  10. package/engine/engineDraw.js +71 -56
  11. package/engine/engineInput.js +178 -28
  12. package/engine/engineMedals.js +66 -58
  13. package/engine/engineObject.js +86 -71
  14. package/engine/engineParticles.js +101 -63
  15. package/engine/engineRelease.js +4 -1
  16. package/engine/engineSettings.js +62 -53
  17. package/engine/engineTileLayer.js +99 -83
  18. package/engine/engineUtilities.js +95 -59
  19. package/engine/engineWebGL.js +53 -48
  20. package/examples/breakout/game.js +1 -1
  21. package/examples/breakout/gameObjects.js +2 -2
  22. package/examples/breakout/index.html +7 -5
  23. package/examples/platformer/game.js +7 -4
  24. package/examples/platformer/gameEffects.js +8 -11
  25. package/examples/platformer/gameLevel.js +15 -8
  26. package/examples/platformer/gameObjects.js +2 -2
  27. package/examples/platformer/gamePlayer.js +6 -6
  28. package/examples/platformer/index.html +10 -8
  29. package/examples/puzzle/game.js +3 -3
  30. package/examples/puzzle/index.html +6 -4
  31. package/examples/stress/index.html +7 -5
  32. package/game.js +10 -10
  33. package/index.html +17 -15
  34. package/package.json +4 -7
  35. package/tiles.png +0 -0
  36. package/LittleJS.zip +0 -0
  37. package/docs/Audio.html +0 -2264
  38. package/docs/Color.html +0 -2506
  39. package/docs/Debug.html +0 -2929
  40. package/docs/Draw.html +0 -3854
  41. package/docs/EngineObject.html +0 -2506
  42. package/docs/Input.html +0 -2478
  43. package/docs/Medal.html +0 -992
  44. package/docs/Medals.html +0 -624
  45. package/docs/Music.html +0 -569
  46. package/docs/Newgrounds.html +0 -1384
  47. package/docs/Particle.html +0 -567
  48. package/docs/ParticleEmitter.html +0 -1537
  49. package/docs/Random.html +0 -1770
  50. package/docs/Settings.html +0 -2885
  51. package/docs/Sound.html +0 -998
  52. package/docs/TileLayer.html +0 -3211
  53. package/docs/TileLayerData.html +0 -569
  54. package/docs/Timer.html +0 -1168
  55. package/docs/Utilities.html +0 -3266
  56. package/docs/Vector2.html +0 -3956
  57. package/docs/WebGL.html +0 -1842
  58. package/docs/engine.js.html +0 -428
  59. package/docs/engineAudio.js.html +0 -567
  60. package/docs/engineDebug.js.html +0 -721
  61. package/docs/engineDraw.js.html +0 -409
  62. package/docs/engineInput.js.html +0 -397
  63. package/docs/engineMedals.js.html +0 -445
  64. package/docs/engineObject.js.html +0 -492
  65. package/docs/engineParticles.js.html +0 -399
  66. package/docs/engineSettings.js.html +0 -358
  67. package/docs/engineTileLayer.js.html +0 -482
  68. package/docs/engineUtilities.js.html +0 -600
  69. package/docs/engineWebGL.js.html +0 -507
  70. package/docs/fonts/MavenPro-Regular.ttf +0 -0
  71. package/docs/fonts/Montserrat-Regular.ttf +0 -0
  72. package/docs/fonts/Muli-Black.ttf +0 -0
  73. package/docs/fonts/OFL-hind.txt +0 -93
  74. package/docs/fonts/OFL-montserrat.txt +0 -93
  75. package/docs/global.html +0 -1635
  76. package/docs/index.html +0 -243
  77. package/docs/scripts/fix-code-block.js +0 -53
  78. package/docs/scripts/fix-navbar.js +0 -25
  79. package/docs/scripts/linenumber.js +0 -25
  80. package/docs/scripts/misc.js +0 -217
  81. package/docs/scripts/resize.js +0 -85
  82. package/docs/scripts/search.js +0 -83
  83. package/docs/scripts/third-party/Apache-License-2.0.txt +0 -202
  84. package/docs/scripts/third-party/fuse.js +0 -9
  85. package/docs/scripts/third-party/lang-css.js +0 -2
  86. package/docs/scripts/third-party/prettify.js +0 -28
  87. package/docs/static/favicon.png +0 -0
  88. package/docs/styles/clean-jsdoc-theme-base.css +0 -393
  89. package/docs/styles/clean-jsdoc-theme-dark.css +0 -324
  90. package/docs/styles/clean-jsdoc-theme-light.css +0 -319
  91. package/docs/styles/reset.css +0 -346
  92. package/docs/styles/third-party/ionicons.min.css +0 -11
  93. package/docs/styles/third-party/prettify-jsdoc.css +0 -111
  94. package/docs/styles/third-party/prettify-tomorrow.css +0 -5
@@ -1,9 +1,16 @@
1
+ /*
2
+ LittleJS - Debug Build
3
+ MIT License - Copyright 2021 Frank Force
4
+ */
5
+
1
6
  /**
2
- * LittleJS Medal System
3
- * <br> - Debug overlay with mouse pick
4
- * <br> - Debug primitive rendering
5
- * <br> - Save screenshots to disk
6
- * @namespace Debug
7
+ * LittleJS Debug System
8
+ * <br> - Press ~ to show debug overlay with mouse pick
9
+ * <br> - Number keys toggle debug functions
10
+ * <br> - +/- apply time scale
11
+ * <br> - Debug primitive rendering
12
+ * <br> - Save a 2d canvas as an image
13
+ * @namespace Debug
7
14
  */
8
15
 
9
16
  'use strict';
@@ -34,7 +41,7 @@ let showWatermark = 1;
34
41
  let godMode = 0;
35
42
 
36
43
  // Engine internal variables not exposed to documentation
37
- let debugPrimitives = [], debugOverlay = 0, debugPhysics = 0, debugRaycast = 0,
44
+ let debugPrimitives = [], debugOverlay = 0, debugPhysics = 0, debugRaycast = 0,
38
45
  debugParticles = 0, debugGamepads = 0, debugMedals = 0, debugTakeScreenshot, downloadLink;
39
46
 
40
47
  ///////////////////////////////////////////////////////////////////////////////
@@ -156,22 +163,25 @@ const debugUpdate = ()=>
156
163
 
157
164
  if (keyWasPressed(192)) // ~
158
165
  debugOverlay = !debugOverlay;
159
- if (keyWasPressed(49)) // 1
160
- debugPhysics = !debugPhysics, debugParticles = 0;
161
- if (keyWasPressed(50)) // 2
162
- debugParticles = !debugParticles, debugPhysics = 0;
163
- if (keyWasPressed(51)) // 3
164
- godMode = !godMode;
165
- if (keyWasPressed(53)) // 5
166
- debugTakeScreenshot = 1;
167
- //if (keyWasPressed(54)) // 6
168
- // debugToggleParticleEditor();
169
- if (keyWasPressed(55)) // 7
170
- debugGamepads = !debugGamepads;
171
- //if (keyWasPressed(56)) // 8
172
- //if (keyWasPressed(57)) // 9
173
- if (keyWasPressed(48)) // 0
174
- showWatermark = !showWatermark;
166
+ if (debugOverlay)
167
+ {
168
+ if (keyWasPressed(49)) // 1
169
+ debugPhysics = !debugPhysics, debugParticles = 0;
170
+ if (keyWasPressed(50)) // 2
171
+ debugParticles = !debugParticles, debugPhysics = 0;
172
+ if (keyWasPressed(51)) // 3
173
+ godMode = !godMode;
174
+ if (keyWasPressed(53)) // 5
175
+ debugTakeScreenshot = 1;
176
+ //if (keyWasPressed(54)) // 6
177
+ // debugToggleParticleEditor();
178
+ if (keyWasPressed(55)) // 7
179
+ debugGamepads = !debugGamepads;
180
+ //if (keyWasPressed(56)) // 8
181
+ //if (keyWasPressed(57)) // 9
182
+ if (keyWasPressed(48)) // 0
183
+ showWatermark = !showWatermark;
184
+ }
175
185
  }
176
186
 
177
187
  const debugRender = ()=>
@@ -224,12 +234,24 @@ const debugRender = ()=>
224
234
 
225
235
  if (debugOverlay)
226
236
  {
237
+ // mouse pick
238
+ let bestDistance = Infinity, bestObject;
227
239
  for (const o of engineObjects)
228
240
  {
229
- if (o.canvas)
241
+ if (o.canvas || o.destroyed)
230
242
  continue; // skip tile layers
231
243
 
232
244
  const size = o.size.copy();
245
+ if (!size.x || !size.y)
246
+ continue;
247
+
248
+ const distance = mousePos.distanceSquared(o.pos);
249
+ if (distance < bestDistance)
250
+ {
251
+ bestDistance = distance;
252
+ bestObject = o
253
+ }
254
+
233
255
  size.x = max(size.x, .2);
234
256
  size.y = max(size.y, .2);
235
257
 
@@ -244,18 +266,6 @@ const debugRender = ()=>
244
266
  drawRect(o.pos, size.scale(.8), o.parent ? new Color(1,1,1,.5) : new Color(0,0,0,.8));
245
267
  o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0,0,1,.5));
246
268
  }
247
-
248
- // mouse pick
249
- let bestDistance = Infinity, bestObject;
250
- for (const o of engineObjects)
251
- {
252
- const distance = mousePos.distanceSquared(o.pos);
253
- if (distance < bestDistance)
254
- {
255
- bestDistance = distance;
256
- bestObject = o
257
- }
258
- }
259
269
 
260
270
  if (bestObject)
261
271
  {
@@ -275,7 +285,8 @@ const debugRender = ()=>
275
285
  pos = pos.add(height), ...args);
276
286
  drawText('vel = ' + printVec2(bestObject.velocity), pos = pos.add(height), ...args);
277
287
  drawText('size = ' + printVec2(bestObject.size), pos = pos.add(height), ...args);
278
- drawText('collision = ' + getTileCollisionData(mousePos), pos = mousePos.subtract(height), ...args);
288
+ drawText('type = ' + ( bestObject.constructor.name), pos = mousePos.subtract(height), ...args);
289
+ drawText('collision = ' + getTileCollisionData(mousePos), pos = mousePos.subtract(height.scale(2)), ...args);
279
290
  mainContext = saveContext;
280
291
  }
281
292
 
@@ -361,6 +372,24 @@ const debugRender = ()=>
361
372
  //overlayContext.fillText('6: Particle Editor', x, y += h);
362
373
  overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
363
374
  overlayContext.fillText('7: Debug Gamepads', x, y += h);
375
+
376
+ let keysPressed = '';
377
+ for(const i in inputData[0])
378
+ {
379
+ if (i && keyIsDown(i, 0))
380
+ keysPressed += i + ' ' ;
381
+ }
382
+ keysPressed && overlayContext.fillText('Keys Down: ' + keysPressed, x, y += h);
383
+
384
+ let buttonsPressed = '';
385
+ if (inputData[1])
386
+ for(const i in inputData[1])
387
+ {
388
+ if (i && keyIsDown(i, 1))
389
+ buttonsPressed += i + ' ' ;
390
+ }
391
+ buttonsPressed && overlayContext.fillText('Gamepad: ' + buttonsPressed, x, y += h);
392
+
364
393
  }
365
394
  else
366
395
  {
@@ -565,12 +594,12 @@ const debugParticleSettings =
565
594
  ['renderOrder'],
566
595
  ];
567
596
  /**
568
- * LittleJS Utility Classes and Functions
569
- * <br> - General purpose math library
570
- * <br> - Vector2 - fast, simple, easy 2D vector class
571
- * <br> - Color - holds a rgba color with some math functions
572
- * <br> - Timer - tracks time automatically
573
- * @namespace Utilities
597
+ * LittleJS Utility Classes and Functions
598
+ * <br> - General purpose math library
599
+ * <br> - Vector2 - fast, simple, easy 2D vector class
600
+ * <br> - Color - holds a rgba color with some math functions
601
+ * <br> - Timer - tracks time automatically
602
+ * @namespace Utilities
574
603
  */
575
604
 
576
605
  'use strict';
@@ -580,23 +609,12 @@ const debugParticleSettings =
580
609
  * @memberof Utilities */
581
610
  const PI = Math.PI;
582
611
 
583
- /** True if running a Chromium based browser
584
- * @const
585
- * @memberof Utilities */
586
- const isChrome = window['chrome'];
587
-
588
612
  /** Returns absoulte value of value passed in
589
613
  * @param {Number} value
590
614
  * @return {Number}
591
615
  * @memberof Utilities */
592
616
  const abs = (a)=> a < 0 ? -a : a;
593
617
 
594
- /** Returns the sign of value passed in
595
- * @param {Number} value
596
- * @return {Number}
597
- * @memberof Utilities */
598
- const sign = (a)=> a < 0 ? -1 : 1;
599
-
600
618
  /** Returns lowest of two values passed in
601
619
  * @param {Number} valueA
602
620
  * @param {Number} valueB
@@ -611,54 +629,54 @@ const min = (a, b)=> a < b ? a : b;
611
629
  * @memberof Utilities */
612
630
  const max = (a, b)=> a > b ? a : b;
613
631
 
632
+ /** Returns the sign of value passed in (also returns 1 if 0)
633
+ * @param {Number} value
634
+ * @return {Number}
635
+ * @memberof Utilities */
636
+ const sign = (a)=> a < 0 ? -1 : 1;
637
+
614
638
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
615
639
  * @param {Number} dividend
616
- * @param {Number} divisor
640
+ * @param {Number} [divisor=1]
617
641
  * @return {Number}
618
642
  * @memberof Utilities */
619
- const mod = (a, b)=> ((a % b) + b) % b;
643
+ const mod = (a, b=1)=> ((a % b) + b) % b;
620
644
 
621
645
  /** Clamps the value beween max and min
622
646
  * @param {Number} value
623
- * @param {Number} [max=1]
624
647
  * @param {Number} [min=0]
648
+ * @param {Number} [max=1]
625
649
  * @return {Number}
626
650
  * @memberof Utilities */
627
- const clamp = (v, max=1, min=0)=> (ASSERT(max > min), v < min ? min : v > max ? max : v);
651
+ const clamp = (v, min=0, max=1)=> v < min ? min : v > max ? max : v;
628
652
 
629
653
  /** Returns what percentage the value is between max and min
630
654
  * @param {Number} value
631
- * @param {Number} [max=1]
632
655
  * @param {Number} [min=0]
656
+ * @param {Number} [max=1]
633
657
  * @return {Number}
634
658
  * @memberof Utilities */
635
- const percent = (v, max=1, min=0)=> max-min ? clamp((v-min) / (max-min)) : 0;
659
+ const percent = (v, min=0, max=1)=> max-min ? clamp((v-min) / (max-min)) : 0;
636
660
 
637
661
  /** Linearly interpolates the percent value between max and min
638
662
  * @param {Number} percent
639
- * @param {Number} [max=1]
640
663
  * @param {Number} [min=0]
664
+ * @param {Number} [max=1]
641
665
  * @return {Number}
642
666
  * @memberof Utilities */
643
- const lerp = (p, max=1, min=0)=> min + clamp(p) * (max-min);
644
-
645
- /** Formats seconds to 00:00 style for display purposes
646
- * @param {Number} t - time in seconds
647
- * @return {String}
648
- * @memberof Utilities */
649
- const formatTime = (t)=> (t/60|0)+':'+(t%60<10?'0':'')+(t%60|0);
667
+ const lerp = (p, min=0, max=1)=> min + clamp(p) * (max-min);
650
668
 
651
- /** Returns the nearest power of two not less then the value
669
+ /** Applies smoothstep function to the percentage value
652
670
  * @param {Number} value
653
671
  * @return {Number}
654
672
  * @memberof Utilities */
655
- const nearestPowerOfTwo = (v)=> 2**Math.ceil(Math.log2(v));
673
+ const smoothStep = (p)=> p * p * (3 - 2 * p);
656
674
 
657
- /** Applies smoothstep function to the percentage value
675
+ /** Returns the nearest power of two not less then the value
658
676
  * @param {Number} value
659
677
  * @return {Number}
660
678
  * @memberof Utilities */
661
- const smoothStep = (p)=> p * p * (3 - 2 * p);
679
+ const nearestPowerOfTwo = (v)=> 2**Math.ceil(Math.log2(v));
662
680
 
663
681
  /** Returns true if two axis aligned bounding boxes are overlapping
664
682
  * @param {Vector2} pointA - Center of box A
@@ -677,6 +695,12 @@ const isOverlapping = (pA, sA, pB, sB)=> abs(pA.x - pB.x)*2 < sA.x + sB.x & abs(
677
695
  * @memberof Utilities */
678
696
  const wave = (frequency=1, amplitude=1, t=time)=> amplitude/2 * (1 - Math.cos(t*frequency*2*PI));
679
697
 
698
+ /** Formats seconds to mm:ss style for display purposes
699
+ * @param {Number} t - time in seconds
700
+ * @return {String}
701
+ * @memberof Utilities */
702
+ const formatTime = (t)=> (t/60|0)+':'+(t%60<10?'0':'')+(t%60|0);
703
+
680
704
  ///////////////////////////////////////////////////////////////////////////////
681
705
 
682
706
  /** Random global functions
@@ -699,7 +723,7 @@ const randInt = (a=1, b=0)=> rand(a,b)|0;
699
723
  /** Randomly returns either -1 or 1
700
724
  * @return {Number}
701
725
  * @memberof Random */
702
- const randSign = ()=> (rand(2)|0)*2-1;
726
+ const randSign = ()=> (rand(2)|0) * 2 - 1;
703
727
 
704
728
  /** Returns a random Vector2 within a circular shape
705
729
  * @param {Number} [radius=1]
@@ -735,25 +759,46 @@ let randSeed = 1;
735
759
  const randSeeded = (a=1, b=0)=>
736
760
  {
737
761
  randSeed ^= randSeed << 13; randSeed ^= randSeed >>> 17; randSeed ^= randSeed << 5; // xorshift
738
- return b + (a-b)*abs(randSeed % 1e9)/1e9;
762
+ return b + (a-b) * abs(randSeed % 1e9) / 1e9;
739
763
  }
740
764
 
741
765
  ///////////////////////////////////////////////////////////////////////////////
742
766
 
743
- /** Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
744
- * @param {Number} [x=0]
745
- * @param {Number} [y=0]
746
- * @return {Vector2}
747
- * @memberof Utilities */
767
+ /**
768
+ * Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
769
+ * @param {Number} [x=0]
770
+ * @param {Number} [y=0]
771
+ * @return {Vector2}
772
+ * @example
773
+ * let a = vec2(0, 1); // vector with coordinates (0, 1)
774
+ * let b = vec2(a); // copy a into b
775
+ * a = vec2(5); // set a to (5, 5)
776
+ * b = vec2(); // set b to (0, 0)
777
+ * @memberof Utilities
778
+ */
748
779
  const vec2 = (x=0, y)=> x.x == undefined ? new Vector2(x, y == undefined? x : y) : new Vector2(x.x, x.y);
749
780
 
750
- /** 2D Vector object with vector math library */
781
+ /**
782
+ * 2D Vector object with vector math library
783
+ * <br> - Functions do not change this so they can be chained together
784
+ * @example
785
+ * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
786
+ * let b = new Vector2; // vector with coordinates (0, 0)
787
+ * let c = vec2(4, 2); // use the vec2 function to make a Vector2
788
+ * let d = a.add(b).scale(5); // operators can be chained
789
+ */
751
790
  class Vector2
752
791
  {
753
792
  /** Create a 2D vector with the x and y passed in, can also be created with vec2()
754
- * @param {Number} [x=0] - x axis position
755
- * @param {Number} [y=0] - y axis position */
756
- constructor(x=0, y=0) { this.x = x; this.y = y; }
793
+ * @param {Number} [x=0] - X axis location
794
+ * @param {Number} [y=0] - Y axis location */
795
+ constructor(x=0, y=0)
796
+ {
797
+ /** @property {Number} - X axis location */
798
+ this.x = x;
799
+ /** @property {Number} - Y axis location */
800
+ this.y = y;
801
+ }
757
802
 
758
803
  /** Returns a new vector that is a copy of this
759
804
  * @return {Vector2} */
@@ -844,10 +889,6 @@ class Vector2
844
889
  * @return {Vector2} */
845
890
  invert() { return new Vector2(this.y, -this.x); }
846
891
 
847
- /** Returns a copy of this vector with the axies flipped
848
- * @return {Vector2} */
849
- flip() { return new Vector2(this.y, this.x); }
850
-
851
892
  /** Returns a copy of this vector with each axis floored
852
893
  * @return {Vector2} */
853
894
  floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
@@ -870,15 +911,31 @@ class Vector2
870
911
 
871
912
  ///////////////////////////////////////////////////////////////////////////////
872
913
 
873
- /** Color object (red, green, blue, alpha) with some helpful functions */
914
+ /**
915
+ * Color object (red, green, blue, alpha) with some helpful functions
916
+ * @example
917
+ * let a = new Color; // white
918
+ * let b = new Color(1, 0, 0); // red
919
+ * let c = new Color(0, 0, 0, 0); // transparent black
920
+ */
874
921
  class Color
875
922
  {
876
923
  /** Create a color with the components passed in, white by default
877
- * @param {Number} [r=1] - red
878
- * @param {Number} [g=1] - green
879
- * @param {Number} [b=1] - blue
880
- * @param {Number} [a=1] - alpha */
881
- constructor(r=1, g=1, b=1, a=1) { this.r=r; this.g=g; this.b=b; this.a=a; }
924
+ * @param {Number} [red=1]
925
+ * @param {Number} [green=1]
926
+ * @param {Number} [blue=1]
927
+ * @param {Number} [alpha=1] */
928
+ constructor(r=1, g=1, b=1, a=1)
929
+ {
930
+ /** @property {Number} - Red */
931
+ this.r = r;
932
+ /** @property {Number} - Green */
933
+ this.g = g;
934
+ /** @property {Number} - Blue */
935
+ this.b = b;
936
+ /** @property {Number} - Alpha */
937
+ this.a = a;
938
+ }
882
939
 
883
940
  /** Returns a new color that is a copy of this
884
941
  * @return {Color} */
@@ -975,7 +1032,15 @@ class Color
975
1032
 
976
1033
  ///////////////////////////////////////////////////////////////////////////////
977
1034
 
978
- /** Timer object tracks how long has passed since it was set */
1035
+ /**
1036
+ * Timer object tracks how long has passed since it was set
1037
+ * @example
1038
+ * let a = new Timer; // creates a timer that is not set
1039
+ * a.set(3); // sets the timer to 3 seconds
1040
+ *
1041
+ * let b = new Timer(1); // creates a timer with 1 second left
1042
+ * b.unset(); // unsets the timer
1043
+ */
979
1044
  class Timer
980
1045
  {
981
1046
  /** Create a timer object set time passed in
@@ -1001,17 +1066,17 @@ class Timer
1001
1066
  * @return {Boolean} */
1002
1067
  elapsed() { return time > this.time; }
1003
1068
 
1004
- /** Get how long since elapsed, returns 0 if not set
1069
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1005
1070
  * @return {Number} */
1006
1071
  get() { return this.isSet()? time - this.time : 0; }
1007
1072
 
1008
1073
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1009
1074
  * @return {Number} */
1010
- getPercent() { return this.isSet()? percent(this.time - time, 0, this.setTime) : 0; }
1075
+ getPercent() { return this.isSet()? percent(this.time - time, this.setTime, 0) : 0; }
1011
1076
  }
1012
1077
  /**
1013
- * LittleJS Engine Settings
1014
- * @namespace Settings
1078
+ * LittleJS Engine Settings
1079
+ * @namespace Settings
1015
1080
  */
1016
1081
 
1017
1082
  'use strict';
@@ -1019,35 +1084,27 @@ class Timer
1019
1084
  ///////////////////////////////////////////////////////////////////////////////
1020
1085
  // Display settings
1021
1086
 
1022
- /** The max width of the canvas, centered if window is larger
1023
- * @default
1024
- * @memberof Settings */
1025
- const maxWidth = 1920;
1026
-
1027
- /** The max height of the canvas, centered if window is larger
1028
- * @default
1029
- * @memberof Settings */
1030
- const maxHeight = 1200; // up to 1080p and 16:10
1031
-
1032
- /** Fixed witdh, if enabled cavnvas size never changes
1087
+ /** The max size of the canvas, centered if window is larger
1088
+ * @type {Vector2}
1033
1089
  * @default
1034
1090
  * @memberof Settings */
1035
- let fixedWidth = 0;
1091
+ let canvasMaxSize = vec2(1920, 1200);
1036
1092
 
1037
- /** Fixed height, if enabled cavnvas size never changes
1093
+ /** Fixed size of the canvas, if enabled cavnvas size never changes
1094
+ * @type {Vector2}
1038
1095
  * @default
1039
1096
  * @memberof Settings */
1040
- let fixedHeight = 0;
1097
+ let canvasFixedSize = vec2();
1041
1098
 
1042
- /** Fit to canvas to window by adding space on top or bottom if necessary
1099
+ /** Disables anti aliasing for pixel art if true
1043
1100
  * @default
1044
1101
  * @memberof Settings */
1045
- let fixedFitToWindow = 1;
1102
+ let cavasPixelated = 1;
1046
1103
 
1047
1104
  /** Default font used for text rendering
1048
1105
  * @default
1049
1106
  * @memberof Settings */
1050
- let defaultFont = 'arial';
1107
+ let fontDefault = 'arial';
1051
1108
 
1052
1109
  ///////////////////////////////////////////////////////////////////////////////
1053
1110
  // Tile sheet settings
@@ -1056,17 +1113,12 @@ let defaultFont = 'arial';
1056
1113
  * @type {Vector2}
1057
1114
  * @default
1058
1115
  * @memberof Settings */
1059
- const defaultTileSize = vec2(16);
1116
+ let tileSizeDefault = vec2(16);
1060
1117
 
1061
1118
  /** Prevent tile bleeding from neighbors in pixels
1062
1119
  * @default
1063
1120
  * @memberof Settings */
1064
- const tileBleedShrinkFix = .3;
1065
-
1066
- /** Use crisp pixels for pixel art if true
1067
- * @default
1068
- * @memberof Settings */
1069
- let pixelated = 1;
1121
+ let tileFixBleedScale = .3;
1070
1122
 
1071
1123
  ///////////////////////////////////////////////////////////////////////////////
1072
1124
  // Object settings
@@ -1075,43 +1127,48 @@ let pixelated = 1;
1075
1127
  * @type {Vector2}
1076
1128
  * @default
1077
1129
  * @memberof Settings */
1078
- const defaultObjectSize = vec2(1);
1130
+ let objectDefaultSize = vec2(1);
1079
1131
 
1080
1132
  /** Default object mass for collison calcuations (how heavy objects are)
1081
1133
  * @default
1082
1134
  * @memberof Settings */
1083
- const defaultObjectMass = 1;
1135
+ let objectDefaultMass = 1;
1084
1136
 
1085
1137
  /** How much to slow velocity by each frame (0-1)
1086
1138
  * @default
1087
1139
  * @memberof Settings */
1088
- const defaultObjectDamping = .99;
1140
+ let objectDefaultDamping = .99;
1089
1141
 
1090
1142
  /** How much to slow angular velocity each frame (0-1)
1091
1143
  * @default
1092
1144
  * @memberof Settings */
1093
- const defaultObjectAngleDamping = .99;
1145
+ let objectDefaultAngleDamping = .99;
1094
1146
 
1095
1147
  /** How much to bounce when a collision occurs (0-1)
1096
1148
  * @default
1097
1149
  * @memberof Settings */
1098
- const defaultObjectElasticity = 0;
1150
+ let objectDefaultElasticity = 0;
1099
1151
 
1100
1152
  /** How much to slow when touching (0-1)
1101
1153
  * @default
1102
1154
  * @memberof Settings */
1103
- const defaultObjectFriction = .8;
1155
+ let objectDefaultFriction = .8;
1104
1156
 
1105
1157
  /** Clamp max speed to avoid fast objects missing collisions
1106
1158
  * @default
1107
1159
  * @memberof Settings */
1108
- const maxObjectSpeed = 1;
1160
+ let objectMaxSpeed = 1;
1109
1161
 
1110
1162
  /** How much gravity to apply to objects along the Y axis, negative is down
1111
1163
  * @default
1112
1164
  * @memberof Settings */
1113
1165
  let gravity = 0;
1114
1166
 
1167
+ /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
1168
+ * @default
1169
+ * @memberof Settings */
1170
+ let particleEmitRateScale = 1;
1171
+
1115
1172
  ///////////////////////////////////////////////////////////////////////////////
1116
1173
  // Camera settings
1117
1174
 
@@ -1124,7 +1181,7 @@ let cameraPos = vec2();
1124
1181
  /** Scale of camera in world space
1125
1182
  * @default
1126
1183
  * @memberof Settings */
1127
- let cameraScale = max(defaultTileSize.x, defaultTileSize.y);
1184
+ let cameraScale = max(tileSizeDefault.x, tileSizeDefault.y);
1128
1185
 
1129
1186
  ///////////////////////////////////////////////////////////////////////////////
1130
1187
  // WebGL settings
@@ -1132,7 +1189,7 @@ let cameraScale = max(defaultTileSize.x, defaultTileSize.y);
1132
1189
  /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
1133
1190
  * @default
1134
1191
  * @memberof Settings */
1135
- const glEnable = 1;
1192
+ let glEnable = 1;
1136
1193
 
1137
1194
  /** Fixes slow rendering in some browsers by not compositing the WebGL canvas
1138
1195
  * @default
@@ -1145,45 +1202,62 @@ let glOverlay = 1;
1145
1202
  /** Should gamepads be allowed
1146
1203
  * @default
1147
1204
  * @memberof Settings */
1148
- const gamepadsEnable = 1;
1205
+ let gamepadsEnable = 1;
1206
+
1207
+ /** If true, the dpad input is also routed to the left analog stick (for better accessability)
1208
+ * @default
1209
+ * @memberof Settings */
1210
+ let gamepadDirectionEmulateStick = 1;
1211
+
1212
+ /** If true the WASD keys are also routed to the direction keys (for better accessability)
1213
+ * @default
1214
+ * @memberof Settings */
1215
+ let inputWASDEmulateDirection = 1;
1216
+
1217
+ /** True if touch gamepad should appear on mobile devices
1218
+ * <br> - Supports left analog stick, 4 face buttons and start button (button 9)
1219
+ * <br> - Must be set by end of gameInit to be activated
1220
+ * @default
1221
+ * @memberof Settings */
1222
+ let touchGamepadEnable = 0;
1149
1223
 
1150
- /** If true touch input is routed to mouse functions
1224
+ /** Size of virutal gamepad for touch devices in pixels
1151
1225
  * @default
1152
1226
  * @memberof Settings */
1153
- const touchInputEnable = 1;
1227
+ let touchGamepadSize = 80;
1154
1228
 
1155
- /** Allow players to use dpad as analog stick
1229
+ /** Transparency of touch gamepad overlay
1156
1230
  * @default
1157
1231
  * @memberof Settings */
1158
- const copyGamepadDirectionToStick = 1;
1232
+ let touchGamepadAlpha = .3;
1159
1233
 
1160
- /** allow players to use WASD as direction keys
1234
+ /** Allow vibration hardware if it exists
1161
1235
  * @default
1162
1236
  * @memberof Settings */
1163
- const copyWASDToDpad = 1;
1237
+ let vibrateEnable = 1;
1164
1238
 
1165
1239
  ///////////////////////////////////////////////////////////////////////////////
1166
1240
  // Audio settings
1167
1241
 
1168
- /** All audio code can be disabled and removed from build
1242
+ /** Volume scale to apply to all sound, music and speech
1169
1243
  * @default
1170
1244
  * @memberof Settings */
1171
- const soundEnable = 1;
1245
+ let soundVolume = .5;
1172
1246
 
1173
- /** Volume scale to apply to all sound, music and speech
1247
+ /** All audio code can be disabled and removed from build
1174
1248
  * @default
1175
1249
  * @memberof Settings */
1176
- let audioVolume = .5;
1250
+ let soundEnable = 1;
1177
1251
 
1178
1252
  /** Default range where sound no longer plays
1179
1253
  * @default
1180
1254
  * @memberof Settings */
1181
- const defaultSoundRange = 30;
1255
+ let soundDefaultRange = 30;
1182
1256
 
1183
1257
  /** Default range percent to start tapering off sound (0-1)
1184
1258
  * @default
1185
1259
  * @memberof Settings */
1186
- const defaultSoundTaper = .7;
1260
+ let soundDefaultTaper = .7;
1187
1261
 
1188
1262
  ///////////////////////////////////////////////////////////////////////////////
1189
1263
  // Medals settings
@@ -1191,409 +1265,168 @@ const defaultSoundTaper = .7;
1191
1265
  /** How long to show medals for in seconds
1192
1266
  * @default
1193
1267
  * @memberof Settings */
1194
- const medalDisplayTime = 5;
1268
+ let medalDisplayTime = 5;
1195
1269
 
1196
1270
  /** How quickly to slide on/off medals in seconds
1197
1271
  * @default
1198
1272
  * @memberof Settings */
1199
- const medalDisplaySlideTime = .5;
1273
+ let medalDisplaySlideTime = .5;
1200
1274
 
1201
1275
  /** Width of medal display
1202
1276
  * @default
1203
1277
  * @memberof Settings */
1204
- const medalDisplayWidth = 640;
1278
+ let medalDisplayWidth = 640;
1205
1279
 
1206
1280
  /** Height of medal display
1207
1281
  * @default
1208
1282
  * @memberof Settings */
1209
- const medalDisplayHeight = 99;
1283
+ let medalDisplayHeight = 80;
1210
1284
 
1211
1285
  /** Size of icon in medal display
1212
1286
  * @default
1213
1287
  * @memberof Settings */
1214
- const medalDisplayIconSize = 80;
1288
+ let medalDisplayIconSize = 50;
1215
1289
  /*
1216
- LittleJS - The Tiny JavaScript Game Engine That Can!
1217
- MIT License - Copyright 2021 Frank Force
1218
-
1219
- Engine Features
1220
- - Object oriented system with base class engine object
1221
- - Base class object handles update, physics, collision, rendering, etc
1222
- - Engine helper classes and functions like Vector2, Color, and Timer
1223
- - Super fast rendering system for tile sheets
1224
- - Sound effects audio with zzfx and music with zzfxm
1225
- - Input processing system with gamepad and touchscreen support
1226
- - Tile layer rendering and collision system
1227
- - Particle effect system
1228
- - Medal system tracks and displays achievements
1229
- - Debug tools and debug rendering system
1230
- - Call engineInit() to start it up!
1290
+ LittleJS Object System
1231
1291
  */
1232
1292
 
1233
1293
  'use strict';
1234
1294
 
1235
- /** Name of engine */
1236
- const engineName = 'LittleJS';
1237
-
1238
- /** Version of engine */
1239
- const engineVersion = '1.1.2';
1240
-
1241
- /** Frames per second to update objects
1242
- * @default */
1243
- const FPS = 60;
1244
-
1245
- /** How many seconds each frame lasts, engine uses a fixed time step
1246
- * @default 1/60 */
1247
- const timeDelta = 1/FPS;
1248
-
1249
- /** Array containing all engine objects */
1250
- let engineObjects = [];
1251
-
1252
- /** Array containing only objects that are set to collide with other objects (for optimization) */
1253
- let engineCollideObjects = [];
1254
-
1255
- /** Current update frame, used to calculate time */
1256
- let frame = 0;
1257
-
1258
- /** Current engine time since start in seconds, derived from frame */
1259
- let time = 0;
1260
-
1261
- /** Actual clock time since start in seconds (not affected by pause or frame rate clamping) */
1262
- let timeReal = 0;
1263
-
1264
- /** Is the game paused? Causes time and objects to not be updated. */
1265
- let paused = 0;
1266
-
1267
- // Engine internal variables not exposed to documentation
1268
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, debugFPS = 0,
1269
- shrinkTilesX, shrinkTilesY, drawCount, tileImageSize, tileImageSizeInverse;
1270
-
1271
- ///////////////////////////////////////////////////////////////////////////////
1272
-
1273
- /** Start up LittleJS engine with your callback functions
1274
- * @param {Function} gameInit - Called once after the engine starts up, setup the game
1275
- * @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
1276
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
1277
- * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
1278
- * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
1279
- * @param {String} tileImageSource - Tile image to use, everything starts when the image is finished loading
1295
+ /**
1296
+ * LittleJS Object Base Object Class
1297
+ * <br> - Base object class used by the engine
1298
+ * <br> - Automatically adds self to object list
1299
+ * <br> - Will be updated and rendered each frame
1300
+ * <br> - Renders as a sprite from a tilesheet by default
1301
+ * <br> - Can have color and addtive color applied
1302
+ * <br> - 2d Physics and collision system
1303
+ * <br> - Sorted by renderOrder
1304
+ * <br> - Objects can have children attached
1305
+ * <br> - Parents are updated before children, and set child transform
1306
+ * <br> - Call destroy() to get rid of objects
1307
+ * <br>
1308
+ * <br>The physics system used by objects is simple and fast with some caveats...
1309
+ * <br> - Collision uses the axis aligned size, the object's rotation angle is only for rendering
1310
+ * <br> - Objects are guaranteed to not intersect tile collision from physics
1311
+ * <br> - If an object starts or is moved inside tile collision, it will not collide with that tile
1312
+ * <br> - Collision for objects can be set to be solid to block other objects
1313
+ * <br> - Objects may get pushed into overlapping other solid objects, if so they will push away
1314
+ * <br> - Solid objects are more performance intensive and should be used sparingly
1315
+ * @example
1316
+ * // create an engine object, normally you would first extend the class with your own
1317
+ * const pos = vec2(2,3);
1318
+ * const object = new EngineObject(pos);
1280
1319
  */
1281
- function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, tileImageSource)
1320
+ class EngineObject
1282
1321
  {
1283
- // init engine when tiles load
1284
- tileImage.onload = ()=>
1322
+ /** Create an engine object and adds it to the list of objects
1323
+ * @param {Vector2} [position=new Vector2(0,0)] - World space position of the object
1324
+ * @param {Vector2} [size=objectDefaultSize] - World space size of the object
1325
+ * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1326
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
1327
+ * @param {Number} [angle=0] - Angle to rotate the object
1328
+ * @param {Color} [color] - Color to apply to tile when rendered
1329
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
1330
+ */
1331
+ constructor(pos=vec2(), size=objectDefaultSize, tileIndex=-1, tileSize=tileSizeDefault, angle=0, color, renderOrder=0)
1285
1332
  {
1286
- // save tile image info
1287
- tileImageSizeInverse = vec2(1).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
1288
- debug && (tileImage.onload=()=>ASSERT(1)); // tile sheet can not reloaded
1289
- shrinkTilesX = tileBleedShrinkFix/tileImageSize.x;
1290
- shrinkTilesY = tileBleedShrinkFix/tileImageSize.y;
1333
+ // set passed in params
1334
+ ASSERT(pos && pos.x != undefined && size.x != undefined); // ensure pos and size are vec2s
1291
1335
 
1292
- // setup html
1293
- document.body.appendChild(mainCanvas = document.createElement('canvas'));
1294
- document.body.style = 'margin:0;overflow:hidden;background:#000';
1295
- mainCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)' +
1296
- (pixelated ? ';image-rendering:crisp-edges;image-rendering:pixelated' : ''); // pixelated rendering
1297
- mainContext = mainCanvas.getContext('2d');
1336
+ /** @property {Vector2} - World space position of the object */
1337
+ this.pos = pos.copy();
1338
+ /** @property {Vector2} - World space width and height of the object */
1339
+ this.size = size;
1340
+ /** @property {Vector2} - Size of object used for drawing, uses size if not set */
1341
+ this.drawSize;
1342
+ /** @property {Number} - Tile to use to render object, untextured if -1 */
1343
+ this.tileIndex = tileIndex;
1344
+ /** @property {Vector2} - Size of tile in source pixels */
1345
+ this.tileSize = tileSize;
1346
+ /** @property {Number} - Angle to rotate the object */
1347
+ this.angle = angle;
1348
+ /** @property {Color} - Color to apply when rendered */
1349
+ this.color = color;
1350
+ /** @property {Color} - Additive color to apply when rendered */
1351
+ this.additiveColor;
1352
+
1353
+ // set object defaults
1354
+ /** @property {Number} [mass=objectDefaultMass] - How heavy the object is */
1355
+ this.mass = objectDefaultMass;
1356
+ /** @property {Number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
1357
+ this.damping = objectDefaultDamping;
1358
+ /** @property {Number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
1359
+ this.angleDamping = objectDefaultAngleDamping;
1360
+ /** @property {Number} [elasticity=objectDefaultElasticity] - How bouncy the object is when colliding (0-1) */
1361
+ this.elasticity = objectDefaultElasticity;
1362
+ /** @property {Number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
1363
+ this.friction = objectDefaultFriction;
1364
+ /** @property {Number} [gravityScale=1] - How much to scale gravity by for this object */
1365
+ this.gravityScale = 1;
1366
+ /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1367
+ this.renderOrder = renderOrder;
1298
1368
 
1299
- // init stuff and start engine
1300
- debugInit();
1301
- glInit();
1369
+ // init other internal object stuff
1370
+ this.spawnTime = time;
1371
+ this.velocity = vec2(this.collideSolidObjects = this.angleVelocity = 0);
1372
+ this.collideTiles = 1;
1373
+ this.children = [];
1302
1374
 
1303
- // create overlay canvas for hud to appear above gl canvas
1304
- document.body.appendChild(overlayCanvas = document.createElement('canvas'));
1305
- overlayCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
1306
- overlayContext = overlayCanvas.getContext('2d');
1375
+ // add to list of objects
1376
+ engineObjects.push(this);
1377
+ }
1378
+
1379
+ /** Update the object transform and physics, called automatically by engine once each frame */
1380
+ update()
1381
+ {
1382
+ const parent = this.parent;
1383
+ if (parent)
1384
+ {
1385
+ // copy parent pos/angle
1386
+ this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
1387
+ this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
1388
+ return;
1389
+ }
1307
1390
 
1308
- gameInit();
1309
- engineUpdate();
1310
- };
1391
+ // limit max speed to prevent missing collisions
1392
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1393
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1311
1394
 
1312
- // main update loop
1313
- const engineUpdate = (frameTimeMS=0)=>
1314
- {
1315
- requestAnimationFrame(engineUpdate);
1395
+ // apply physics
1396
+ const oldPos = this.pos.copy();
1397
+ this.pos.x += this.velocity.x = this.damping * this.velocity.x;
1398
+ this.pos.y += this.velocity.y = this.damping * this.velocity.y + gravity * this.gravityScale;
1399
+ this.angle += this.angleVelocity *= this.angleDamping;
1316
1400
 
1317
- // update time keeping
1318
- let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
1319
- frameTimeLastMS = frameTimeMS;
1320
- if (debug || showWatermark)
1321
- debugFPS = lerp(.05, 1e3/(frameTimeDeltaMS||1), debugFPS);
1322
- if (debug)
1323
- frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
1324
- timeReal += frameTimeDeltaMS / 1e3;
1325
- frameTimeBufferMS = min(frameTimeBufferMS + !paused * frameTimeDeltaMS, 50); // clamp incase of slow framerate
1401
+ // physics sanity checks
1402
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1403
+ ASSERT(this.damping >= 0 && this.damping <= 1);
1326
1404
 
1327
- if (paused)
1405
+ if (!this.mass) // do not update collision for fixed objects
1406
+ return;
1407
+
1408
+ const wasMovingDown = this.velocity.y < 0;
1409
+ if (this.groundObject)
1328
1410
  {
1329
- // do post update even when paused
1330
- inputUpdate();
1331
- debugUpdate();
1332
- gameUpdatePost();
1333
- inputUpdatePost();
1411
+ // apply friction in local space of ground object
1412
+ const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
1413
+ this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
1414
+ this.groundObject = 0;
1415
+ //debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
1334
1416
  }
1335
- else
1417
+
1418
+ if (this.collideSolidObjects)
1336
1419
  {
1337
- // apply time delta smoothing, improves smoothness of framerate in some browsers
1338
- let deltaSmooth = 0;
1339
- if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
1340
- {
1341
- // force an update each frame if time is close enough (not just a fast refresh rate)
1342
- deltaSmooth = frameTimeBufferMS;
1343
- frameTimeBufferMS = 0;
1344
- }
1345
-
1346
- // update multiple frames if necessary in case of slow framerate
1347
- for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / FPS)
1348
- {
1349
- // update game and objects
1350
- inputUpdate();
1351
- gameUpdate();
1352
- engineObjectsUpdate();
1353
-
1354
- // do post update
1355
- debugUpdate();
1356
- gameUpdatePost();
1357
- inputUpdatePost();
1358
- }
1359
-
1360
- // add the time smoothing back in
1361
- frameTimeBufferMS += deltaSmooth;
1362
- }
1363
-
1364
- if (fixedWidth)
1365
- {
1366
- // clear set fixed size
1367
- mainCanvas.width = fixedWidth;
1368
- mainCanvas.height = fixedHeight;
1369
-
1370
- if (fixedFitToWindow)
1371
- {
1372
- // fit to window by adding space on top or bottom if necessary
1373
- const aspect = innerWidth / innerHeight;
1374
- const fixedAspect = fixedWidth / fixedHeight;
1375
- mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
1376
- mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
1377
- if (glCanvas)
1378
- {
1379
- glCanvas.style.width = mainCanvas.style.width;
1380
- glCanvas.style.height = mainCanvas.style.height;
1381
- }
1382
- }
1383
- }
1384
- else
1385
- {
1386
- // clear and set size to same as window
1387
- mainCanvas.width = min(innerWidth, maxWidth);
1388
- mainCanvas.height = min(innerHeight, maxHeight);
1389
- }
1390
-
1391
- // save canvas size and clear overlay canvas
1392
- mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
1393
- mainContext.imageSmoothingEnabled = !pixelated; // disable smoothing for pixel art
1394
-
1395
- // render sort then render while removing destroyed objects
1396
- glPreRender(mainCanvas.width, mainCanvas.height);
1397
- gameRender();
1398
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
1399
- for (const o of engineObjects)
1400
- o.destroyed || o.render();
1401
- gameRenderPost();
1402
- medalsRender();
1403
- debugRender();
1404
- glCopyToContext(mainContext);
1405
-
1406
- if (showWatermark)
1407
- {
1408
- // update fps
1409
- overlayContext.textAlign = 'right';
1410
- overlayContext.textBaseline = 'top';
1411
- overlayContext.font = '1em monospace';
1412
- overlayContext.fillStyle = '#000';
1413
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
1414
- + drawCount + ' / ' + engineObjects.length + ' / ' + debugFPS.toFixed(1);
1415
- overlayContext.fillText(text, mainCanvas.width-3, 3);
1416
- overlayContext.fillStyle = '#fff';
1417
- overlayContext.fillText(text, mainCanvas.width-2, 2);
1418
- drawCount = 0;
1419
- }
1420
- }
1421
-
1422
- // set tile image source to load the image and start the engine
1423
- tileImageSource ? tileImage.src = tileImageSource : tileImage.onload();
1424
- }
1425
-
1426
-
1427
- ///////////////////////////////////////////////////////////////////////////////
1428
-
1429
- /** Calls update on each engine object (recursively if child), removes destroyed objects, and updated time */
1430
- function engineObjectsUpdate()
1431
- {
1432
- // recursive object update
1433
- const updateObject = (o)=>
1434
- {
1435
- if (!o.destroyed)
1436
- {
1437
- o.update();
1438
- for (const child of o.children)
1439
- updateObject(child);
1440
- }
1441
- }
1442
- for (const o of engineObjects)
1443
- o.parent || updateObject(o);
1444
-
1445
- // remove destroyed objects
1446
- engineObjects = engineObjects.filter(o=>!o.destroyed);
1447
- engineCollideObjects = engineCollideObjects.filter(o=>!o.destroyed);
1448
-
1449
- // increment frame and update time
1450
- time = ++frame / FPS;
1451
- }
1452
-
1453
- /** Detroy and remove all objects that are not persistent or descendants of a persistent object */
1454
- function engineObjectsDestroy()
1455
- {
1456
- for (const o of engineObjects)
1457
- o.persistent || o.parent || o.destroy();
1458
- engineObjects = engineObjects.filter(o=>!o.destroyed);
1459
- }
1460
-
1461
- /** Triggers a callback for each object within a given area
1462
- * @param {Vector2} [pos] - Center of test area
1463
- * @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
1464
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
1465
- * @param {Array} [objects=engineObjects] - List of objects to check */
1466
- function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
1467
- {
1468
- if (!pos)
1469
- {
1470
- // all objects
1471
- for (const o of objects)
1472
- callbackFunction(o);
1473
- }
1474
- else if (size.x != undefined)
1475
- {
1476
- // aabb test
1477
- for (const o of objects)
1478
- isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
1479
- }
1480
- else
1481
- {
1482
- // circle test
1483
- const sizeSquared = size*size;
1484
- for (const o of objects)
1485
- pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
1486
- }
1487
- }
1488
- /*
1489
- LittleJS Object System
1490
- */
1491
-
1492
- 'use strict';
1493
-
1494
- /**
1495
- * LittleJS Object Base Object Class
1496
- * <br> - Base object class used by the engine
1497
- * <br> - Automatically adds self to object list
1498
- * <br> - Will be updated and rendered each frame
1499
- * <br> - Renders as a sprite from a tilesheet by default
1500
- * <br> - Can have color and addtive color applied
1501
- * <br> - 2d Physics and collision system
1502
- * <br> - Sorted by renderOrder
1503
- * <br> - Objects can have children attached
1504
- * <br> - Parents are updated before children, and set child transform
1505
- * <br> - Call destroy() to get rid of objects
1506
- */
1507
- class EngineObject
1508
- {
1509
- /**
1510
- * Create an engine object and adds it to the list of objects
1511
- * @param {Vector2} [position=new Vector2(0,0)] - World space position of the object
1512
- * @param {Vector2} [size=defaultObjectSize] - World space size of the object
1513
- * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1514
- * @param {Vector2} [tileSize=defaultTileSize] - Size of tile in source pixels
1515
- * @param {Number} [angle=0] - Angle to rotate the object
1516
- * @param {Color} [color] - Color to apply to tile when rendered
1517
- */
1518
- constructor(pos=vec2(), size=defaultObjectSize, tileIndex=-1, tileSize=defaultTileSize, angle=0, color)
1519
- {
1520
- // set passed in params
1521
- ASSERT(pos && pos.x != undefined && size.x != undefined); // ensure pos and size are vec2s
1522
- this.pos = pos.copy();
1523
- this.size = size;
1524
- this.tileIndex = tileIndex;
1525
- this.tileSize = tileSize;
1526
- this.angle = angle;
1527
- this.color = color;
1528
-
1529
- // set physics defaults
1530
- this.mass = defaultObjectMass;
1531
- this.damping = defaultObjectDamping;
1532
- this.angleDamping = defaultObjectAngleDamping;
1533
- this.elasticity = defaultObjectElasticity;
1534
- this.friction = defaultObjectFriction;
1535
-
1536
- // init other object stuff
1537
- this.spawnTime = time;
1538
- this.velocity = vec2(this.collideSolidObjects = this.renderOrder = this.angleVelocity = 0);
1539
- this.collideTiles = this.gravityScale = 1;
1540
- this.children = [];
1541
-
1542
- // add to list of objects
1543
- engineObjects.push(this);
1544
- }
1545
-
1546
- /** Update the object transform and physics, called automatically by engine once each frame */
1547
- update()
1548
- {
1549
- const parent = this.parent;
1550
- if (parent)
1551
- {
1552
- // copy parent pos/angle
1553
- this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
1554
- this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
1555
- return;
1556
- }
1557
-
1558
- // limit max speed to prevent missing collisions
1559
- this.velocity.x = clamp(this.velocity.x, maxObjectSpeed, -maxObjectSpeed);
1560
- this.velocity.y = clamp(this.velocity.y, maxObjectSpeed, -maxObjectSpeed);
1561
-
1562
- // apply physics
1563
- const oldPos = this.pos.copy();
1564
- this.pos.x += this.velocity.x = this.damping * this.velocity.x;
1565
- this.pos.y += this.velocity.y = this.damping * this.velocity.y + gravity * this.gravityScale;
1566
- this.angle += this.angleVelocity *= this.angleDamping;
1567
-
1568
- // physics sanity checks
1569
- ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1570
- ASSERT(this.damping >= 0 && this.damping <= 1);
1571
-
1572
- if (!this.mass) // do not update collision for fixed objects
1573
- return;
1574
-
1575
- const wasMovingDown = this.velocity.y < 0;
1576
- if (this.groundObject)
1577
- {
1578
- // apply friction in local space of ground object
1579
- const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
1580
- this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
1581
- this.groundObject = 0;
1582
- //debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
1583
- }
1584
-
1585
- if (this.collideSolidObjects)
1586
- {
1587
- // check collisions against solid objects
1588
- const epsilon = 1e-3; // necessary to push slightly outside of the collision
1589
- for (const o of engineCollideObjects)
1420
+ // check collisions against solid objects
1421
+ const epsilon = 1e-3; // necessary to push slightly outside of the collision
1422
+ for (const o of engineObjectsCollide)
1590
1423
  {
1591
1424
  // non solid objects don't collide with eachother
1592
- if (!this.isSolid & !o.isSolid || o.destroyed || o.parent)
1425
+ if (!this.isSolid & !o.isSolid || o.destroyed || o.parent || o == this)
1593
1426
  continue;
1594
1427
 
1595
1428
  // check collision
1596
- if (!isOverlapping(this.pos, this.size, o.pos, o.size) || o == this)
1429
+ if (!isOverlapping(this.pos, this.size, o.pos, o.size))
1597
1430
  continue;
1598
1431
 
1599
1432
  // pass collision to objects
@@ -1605,7 +1438,7 @@ class EngineObject
1605
1438
  // if already was touching, try to push away
1606
1439
  const deltaPos = oldPos.subtract(o.pos);
1607
1440
  const length = deltaPos.length();
1608
- const pushAwayAccel = .001; // push away if alread overlapping
1441
+ const pushAwayAccel = .001; // push away if already overlapping
1609
1442
  const velocity = length < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
1610
1443
  this.velocity = this.velocity.add(velocity);
1611
1444
  if (o.mass) // push away if not fixed
@@ -1616,16 +1449,15 @@ class EngineObject
1616
1449
  }
1617
1450
 
1618
1451
  // check for collision
1619
- const sx = this.size.x + o.size.x;
1620
- const sy = this.size.y + o.size.y;
1621
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sy + gravity; // prefer to push up if small delta
1622
- const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sy;
1623
- const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sx;
1452
+ const sizeBoth = this.size.add(o.size);
1453
+ const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity; // prefer to push up if small delta
1454
+ const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
1455
+ const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
1624
1456
 
1625
1457
  if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
1626
1458
  {
1627
1459
  // push outside object collision
1628
- this.pos.y = o.pos.y + (sy/2 + epsilon) * sign(oldPos.y - o.pos.y);
1460
+ this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
1629
1461
  if (o.groundObject && wasMovingDown || !o.mass)
1630
1462
  {
1631
1463
  // set ground object if landed on something
@@ -1648,15 +1480,14 @@ class EngineObject
1648
1480
 
1649
1481
  // lerp betwen elastic or inelastic based on elasticity
1650
1482
  const elasticity = max(this.elasticity, o.elasticity);
1651
- this.velocity.y = lerp(elasticity, elastic0, inelastic);
1652
- o.velocity.y = lerp(elasticity, elastic1, inelastic);
1483
+ this.velocity.y = lerp(elasticity, inelastic, elastic0);
1484
+ o.velocity.y = lerp(elasticity, inelastic, elastic1);
1653
1485
  }
1654
- debugPhysics && smallStepUp && (abs(oldPos.x - o.pos.x)*2 > sx) && console.log('stepUp', oldPos.y - o.pos.y);
1655
1486
  }
1656
1487
  if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision
1657
1488
  {
1658
1489
  // push outside collision
1659
- this.pos.x = o.pos.x + (sx/2 + epsilon) * sign(oldPos.x - o.pos.x);
1490
+ this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
1660
1491
  if (o.mass)
1661
1492
  {
1662
1493
  // inelastic collision
@@ -1670,13 +1501,12 @@ class EngineObject
1670
1501
 
1671
1502
  // lerp betwen elastic or inelastic based on elasticity
1672
1503
  const elasticity = max(this.elasticity, o.elasticity);
1673
- this.velocity.x = lerp(elasticity, elastic0, inelastic);
1674
- o.velocity.x = lerp(elasticity, elastic1, inelastic);
1504
+ this.velocity.x = lerp(elasticity, inelastic, elastic0);
1505
+ o.velocity.x = lerp(elasticity, inelastic, elastic1);
1675
1506
  }
1676
1507
  else // bounce if other object is fixed
1677
1508
  this.velocity.x *= -this.elasticity;
1678
1509
  }
1679
-
1680
1510
  debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f0f');
1681
1511
  }
1682
1512
  }
@@ -1685,8 +1515,6 @@ class EngineObject
1685
1515
  // check collision against tiles
1686
1516
  if (tileCollisionTest(this.pos, this.size, this))
1687
1517
  {
1688
- //debugPhysics && debugRect(this.pos, this.size, '#ff0');
1689
-
1690
1518
  // if already was stuck in collision, don't do anything
1691
1519
  // this should not happen unless something starts in collision
1692
1520
  if (!tileCollisionTest(oldPos, this.size, this))
@@ -1743,19 +1571,19 @@ class EngineObject
1743
1571
 
1744
1572
  /** Called to check if a tile collision should be resolved
1745
1573
  * @param {Number} tileData - the value of the tile at the position
1746
- * @param {Vector2} pos - tile where the collision occured
1747
- * @return {Boolean} true if the collision should be resolved */
1574
+ * @param {Vector2} pos - tile where the collision occured
1575
+ * @return {Boolean} - true if the collision should be resolved */
1748
1576
  collideWithTile(tileData, pos) { return tileData > 0; }
1749
1577
 
1750
1578
  /** Called to check if a tile raycast hit
1751
1579
  * @param {Number} tileData - the value of the tile at the position
1752
- * @param {Vector2} pos - tile where the raycast is
1753
- * @return {Boolean} true if the raycast should hit */
1580
+ * @param {Vector2} pos - tile where the raycast is
1581
+ * @return {Boolean} - true if the raycast should hit */
1754
1582
  collideWithTileRaycast(tileData, pos) { return tileData > 0; }
1755
1583
 
1756
1584
  /** Called to check if a tile raycast hit
1757
1585
  * @param {EngineObject} object - the object to test against
1758
- * @return {Boolean} true if the collision should be resolved
1586
+ * @return {Boolean} - true if the collision should be resolved
1759
1587
  */
1760
1588
  collideWithObject(o) { return 1; }
1761
1589
 
@@ -1799,41 +1627,42 @@ class EngineObject
1799
1627
 
1800
1628
  /** Set how this object collides
1801
1629
  * @param {boolean} [collideSolidObjects=0] - Does it collide with solid objects
1802
- * @param {boolean} [isSolid=0] - Does it collide with and block other objects (expensive in large numbers)
1803
- * @param {boolean} [collideTiles=1] - Does it collide with the tile collision */
1630
+ * @param {boolean} [isSolid=0] - Does it collide with and block other objects (expensive in large numbers)
1631
+ * @param {boolean} [collideTiles=1] - Does it collide with the tile collision */
1804
1632
  setCollision(collideSolidObjects=0, isSolid=0, collideTiles=1)
1805
1633
  {
1806
1634
  ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1807
1635
 
1808
- // track collidable objects in separate list
1809
- if (collideSolidObjects && !this.collideSolidObjects)
1810
- {
1811
- ASSERT(!engineCollideObjects.includes(this));
1812
- engineCollideObjects.push(this);
1813
- }
1814
- else if (!collideSolidObjects && this.collideSolidObjects)
1815
- {
1816
- ASSERT(engineCollideObjects.includes(this))
1817
- engineCollideObjects.splice(engineCollideObjects.indexOf(this), 1);
1818
- }
1819
-
1820
1636
  this.collideSolidObjects = collideSolidObjects;
1821
1637
  this.isSolid = isSolid;
1822
1638
  this.collideTiles = collideTiles;
1823
1639
  }
1824
1640
  }
1825
1641
  /**
1826
- * LittleJS Drawing System
1827
- * <br> - Hybrid with both Canvas2D and WebGL available
1828
- * <br> - Super fast tile sheet rendering with WebGL
1829
- * <br> - Can apply rotation, mirror, color and additive color
1830
- * <br> - Many useful utility functions
1831
- * @namespace Draw
1642
+ * LittleJS Drawing System
1643
+ * <br> - Hybrid with both Canvas2D and WebGL available
1644
+ * <br> - Super fast tile sheet rendering with WebGL
1645
+ * <br> - Can apply rotation, mirror, color and additive color
1646
+ * <br> - Many useful utility functions
1647
+ * <br>
1648
+ * <br>LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
1649
+ * <br>There are 3 canvas/contexts available to draw to...
1650
+ * <br> - mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
1651
+ * <br> - glCanvas - Used by the accelerated WebGL batch rendering system.
1652
+ * <br> - overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
1653
+ * <br>
1654
+ * <br>The WebGL rendering system is very fast with some caveats...
1655
+ * <br> - The default setup supports only 1 tile sheet, to support more call glCreateTexture and glSetTexture
1656
+ * <br> - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
1657
+ * <br> - Group additive rendering together using renderOrder to mitigate this issue
1658
+ * <br>
1659
+ * <br>The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
1660
+ * @namespace Draw
1832
1661
  */
1833
1662
 
1834
1663
  'use strict';
1835
1664
 
1836
- /** Main tilesheet to use for batch rendering system
1665
+ /** Tile sheet for batch rendering system
1837
1666
  * @type {Image}
1838
1667
  * @memberof Draw */
1839
1668
  const tileImage = new Image();
@@ -1858,7 +1687,7 @@ let overlayCanvas;
1858
1687
  * @memberof Draw */
1859
1688
  let overlayContext;
1860
1689
 
1861
- /** The size of the main canvas (and other secondary canvases: overlayCanvas and glCanvas)
1690
+ /** The size of the main canvas (and other secondary canvases)
1862
1691
  * @type {Vector2}
1863
1692
  * @memberof Draw */
1864
1693
  let mainCanvasSize = vec2();
@@ -1877,42 +1706,39 @@ const screenToWorld = (screenPos)=>
1877
1706
  const worldToScreen = (worldPos)=>
1878
1707
  worldPos.subtract(cameraPos).multiply(vec2(cameraScale,-cameraScale)).add(mainCanvasSize.scale(.5)).subtract(vec2(.5));
1879
1708
 
1880
- /** Draw textured tile centered on pos
1881
- * @param {Vector2} pos - Center of the tile
1882
- * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile
1883
- * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1884
- * @param {Vector2} [tileSize=defaultTileSize] - Tile size in source pixels
1885
- * @param {Color} [color=new Color(1,1,1)]
1886
- * @param {Number} [angle=0]
1887
- * @param {Boolean} [mirror=0]
1888
- * @param {Color} [additiveColor=new Color(0,0,0,0)]
1709
+ /** Draw textured tile centered in world space, with color applied if using WebGL
1710
+ * @param {Vector2} pos - Center of the tile in world space
1711
+ * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile in world space, width and height
1712
+ * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1713
+ * @param {Vector2} [tileSize=tileSizeDefault] - Tile size in source pixels
1714
+ * @param {Color} [color=new Color(1,1,1)] - Color to modulate with
1715
+ * @param {Number} [angle=0] - Angle to rotate by
1716
+ * @param {Boolean} [mirror=0] - If true image is flipped along the Y axis
1717
+ * @param {Color} [additiveColor=new Color(0,0,0,0)] - Additive color to be applied
1718
+ * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1889
1719
  * @memberof Draw */
1890
- function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=defaultTileSize, color=new Color, angle=0, mirror,
1891
- additiveColor=new Color(0,0,0,0))
1720
+ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color, angle=0, mirror,
1721
+ additiveColor=new Color(0,0,0,0), useWebGL=glEnable)
1892
1722
  {
1893
1723
  showWatermark && ++drawCount;
1894
- if (glEnable)
1724
+ if (glEnable && useWebGL)
1895
1725
  {
1896
- if (tileIndex < 0)
1726
+ if (tileIndex < 0 || !tileImage.width)
1897
1727
  {
1898
- // if negative tile index, force untextured
1728
+ // if negative tile index or image not found, force untextured
1899
1729
  glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
1900
1730
  }
1901
1731
  else
1902
1732
  {
1903
1733
  // calculate uvs and render
1904
- const cols = tileImage.width / tileSize.x |0;
1905
- const uvSizeX = tileSize.x * tileImageSizeInverse.x;
1906
- const uvSizeY = tileSize.y * tileImageSizeInverse.y;
1734
+ const cols = tileImageSize.x / tileSize.x |0;
1735
+ const uvSizeX = tileSize.x / tileImageSize.x;
1736
+ const uvSizeY = tileSize.y / tileImageSize.y;
1907
1737
  const uvX = (tileIndex%cols)*uvSizeX, uvY = (tileIndex/cols|0)*uvSizeY;
1908
-
1909
- // shrink uvs to prevent bleeding
1910
- const shrinkTilesX = tileBleedShrinkFix * tileImageSizeInverse.x;
1911
- const shrinkTilesY = tileBleedShrinkFix * tileImageSizeInverse.y;
1912
1738
 
1913
1739
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
1914
- uvX + shrinkTilesX, uvY + shrinkTilesY,
1915
- uvX - shrinkTilesX + uvSizeX, uvY - shrinkTilesX + uvSizeY,
1740
+ uvX + tileImageFixBleed.x, uvY + tileImageFixBleed.y,
1741
+ uvX - tileImageFixBleed.x + uvSizeX, uvY - tileImageFixBleed.y + uvSizeY,
1916
1742
  color.rgbaInt(), additiveColor.rgbaInt());
1917
1743
  }
1918
1744
  }
@@ -1930,11 +1756,11 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=defaultTileSize, col
1930
1756
  else
1931
1757
  {
1932
1758
  // calculate uvs and render
1933
- const cols = tileImage.width / tileSize.x |0;
1934
- const sX = (tileIndex%cols)*tileSize.x + tileBleedShrinkFix;
1935
- const sY = (tileIndex/cols|0)*tileSize.y + tileBleedShrinkFix;
1936
- const sWidth = tileSize.x - 2*tileBleedShrinkFix;
1937
- const sHeight = tileSize.y - 2*tileBleedShrinkFix;
1759
+ const cols = tileImageSize.x / tileSize.x |0;
1760
+ const sX = (tileIndex%cols)*tileSize.x + tileFixBleedScale;
1761
+ const sY = (tileIndex/cols|0)*tileSize.y + tileFixBleedScale;
1762
+ const sWidth = tileSize.x - 2*tileFixBleedScale;
1763
+ const sHeight = tileSize.y - 2*tileFixBleedScale;
1938
1764
  context.globalAlpha = color.a; // only alpha is supported
1939
1765
  context.drawImage(tileImage, sX, sY, sWidth, sHeight, -.5, -.5, 1, 1);
1940
1766
  }
@@ -1942,41 +1768,44 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=defaultTileSize, col
1942
1768
  }
1943
1769
  }
1944
1770
 
1945
- /** Draw colored untextured rect centered on pos
1771
+ /** Draw colored rect centered on pos
1946
1772
  * @param {Vector2} pos
1947
1773
  * @param {Vector2} [size=new Vector2(1,1)]
1948
1774
  * @param {Color} [color=new Color(1,1,1)]
1949
1775
  * @param {Number} [angle=0]
1776
+ * @param {Boolean} [useWebGL=glEnable]
1950
1777
  * @memberof Draw */
1951
- function drawRect(pos, size, color, angle)
1778
+ function drawRect(pos, size, color, angle, useWebGL)
1952
1779
  {
1953
- drawTile(pos, size, -1, defaultTileSize, color, angle);
1780
+ drawTile(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1954
1781
  }
1955
1782
 
1956
1783
  /** Draw textured tile centered on pos in screen space
1957
- * @param {Vector2} pos - Center of the tile
1958
- * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile
1959
- * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1960
- * @param {Vector2} [tileSize=defaultTileSize] - Tile size in source pixels
1784
+ * @param {Vector2} pos - Center of the tile
1785
+ * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile
1786
+ * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1787
+ * @param {Vector2} [tileSize=tileSizeDefault] - Tile size in source pixels
1961
1788
  * @param {Color} [color=new Color]
1962
1789
  * @param {Number} [angle=0]
1963
1790
  * @param {Boolean} [mirror=0]
1964
1791
  * @param {Color} [additiveColor=new Color(0,0,0,0)]
1792
+ * @param {Boolean} [useWebGL=glEnable]
1965
1793
  * @memberof Draw */
1966
- function drawTileScreenSpace(pos, size=vec2(1), tileIndex, tileSize, color, angle, mirror, additiveColor)
1794
+ function drawTileScreenSpace(pos, size=vec2(1), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL)
1967
1795
  {
1968
- drawTile(screenToWorld(pos), size.scale(1/cameraScale), tileIndex, tileSize, color, angle, mirror, additiveColor);
1796
+ drawTile(screenToWorld(pos), size.scale(1/cameraScale), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL);
1969
1797
  }
1970
1798
 
1971
- /** Draw colored untextured rectangle in screen space
1799
+ /** Draw colored rectangle in screen space
1972
1800
  * @param {Vector2} pos
1973
1801
  * @param {Vector2} [size=new Vector2(1,1)]
1974
1802
  * @param {Color} [color=new Color(1,1,1)]
1975
1803
  * @param {Number} [angle=0]
1804
+ * @param {Boolean} [useWebGL=glEnable]
1976
1805
  * @memberof Draw */
1977
- function drawRectScreenSpace(pos, size, color, angle)
1806
+ function drawRectScreenSpace(pos, size, color, angle, useWebGL)
1978
1807
  {
1979
- drawTileScreenSpace(pos, size, -1, defaultTileSize, color, angle);
1808
+ drawTileSrceenSpace(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1980
1809
  }
1981
1810
 
1982
1811
  /** Draw colored line between two points
@@ -1984,15 +1813,16 @@ function drawRectScreenSpace(pos, size, color, angle)
1984
1813
  * @param {Vector2} posB
1985
1814
  * @param {Number} [thickness=.1]
1986
1815
  * @param {Color} [color=new Color(1,1,1)]
1816
+ * @param {Boolean} [useWebGL=glEnable]
1987
1817
  * @memberof Draw */
1988
- function drawLine(posA, posB, thickness=.1, color)
1818
+ function drawLine(posA, posB, thickness=.1, color, useWebGL)
1989
1819
  {
1990
1820
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
1991
1821
  const size = vec2(thickness, halfDelta.length()*2);
1992
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle());
1822
+ drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), 0, 0, useWebGL);
1993
1823
  }
1994
1824
 
1995
- /** Draw directly to a 2d canvas context in world space (bipass webgl)
1825
+ /** Draw directly to a 2d canvas context in world space
1996
1826
  * @param {Vector2} pos
1997
1827
  * @param {Vector2} size
1998
1828
  * @param {Number} angle
@@ -2008,7 +1838,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
2008
1838
  context.save();
2009
1839
  context.translate(pos.x+.5|0, pos.y-.5|0);
2010
1840
  context.rotate(angle);
2011
- context.scale(mirror?-size.x:size.x, size.y);
1841
+ context.scale(mirror ? -size.x : size.x, size.y);
2012
1842
  drawFunction(context);
2013
1843
  context.restore();
2014
1844
  }
@@ -2022,7 +1852,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
2022
1852
  * @param {Color} [lineColor=new Color(0,0,0)]
2023
1853
  * @param {String} [textAlign='center']
2024
1854
  * @memberof Draw */
2025
- function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=defaultFont)
1855
+ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
2026
1856
  {
2027
1857
  pos = worldToScreen(pos);
2028
1858
  overlayContext.font = size*cameraScale + 'px '+ font;
@@ -2038,12 +1868,13 @@ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new
2038
1868
  overlayContext.fillText(text, pos.x, pos.y);
2039
1869
  }
2040
1870
 
2041
- /** Enable additive or regular blend mode
1871
+ /** Enable normal or additive blend mode
2042
1872
  * @param {Boolean} [additive=0]
1873
+ * @param {Boolean} [useWebGL=glEnable]
2043
1874
  * @memberof Draw */
2044
- function setBlendMode(additive)
1875
+ function setBlendMode(additive, useWebGL=glEnable)
2045
1876
  {
2046
- if (glEnable)
1877
+ if (glEnable && useWebGL)
2047
1878
  glSetBlendMode(additive);
2048
1879
  else
2049
1880
  mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
@@ -2077,11 +1908,12 @@ function toggleFullscreen()
2077
1908
  }
2078
1909
  }
2079
1910
  /**
2080
- * LittleJS Input System
2081
- * <br> - Tracks key down, pressed, and released
2082
- * <br> - Also tracks mouse buttons, position, and wheel
2083
- * <br> - Supports multiple gamepads
2084
- * @namespace Input
1911
+ * LittleJS Input System
1912
+ * <br> - Tracks key down, pressed, and released
1913
+ * <br> - Also tracks mouse buttons, position, and wheel
1914
+ * <br> - Supports multiple gamepads
1915
+ * <br> - Virtual gamepad for touch devices with touchGamepadSize
1916
+ * @namespace Input
2085
1917
  */
2086
1918
 
2087
1919
  'use strict';
@@ -2109,7 +1941,7 @@ const keyWasReleased = (key, device=0)=> inputData[device] && inputData[device][
2109
1941
 
2110
1942
  /** Clears all input
2111
1943
  * @memberof Input */
2112
- const clearInput = ()=> inputData[0] = [];
1944
+ const clearInput = ()=> inputData = [[]];
2113
1945
 
2114
1946
  /** Returns true if mouse button is down
2115
1947
  * @param {Number} button
@@ -2145,7 +1977,7 @@ let mouseWheel = 0;
2145
1977
 
2146
1978
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
2147
1979
  * @memberof Input */
2148
- let usingGamepad = 0;
1980
+ let isUsingGamepad = 0;
2149
1981
 
2150
1982
  /** Returns true if gamepad button is down
2151
1983
  * @param {Number} button
@@ -2178,7 +2010,9 @@ const gamepadStick = (stick, gamepad=0)=> stickData[gamepad] ? stickData[gamepa
2178
2010
  ///////////////////////////////////////////////////////////////////////////////
2179
2011
  // Input update called by engine
2180
2012
 
2181
- const inputData = [[]];
2013
+ // store input as a bit field for each key: 1 = isDown, 2 = wasPressed, 4 = wasReleased
2014
+ // mouse and keyboard are stored together in device 0, gamepads are in devices > 0
2015
+ let inputData = [[]];
2182
2016
 
2183
2017
  function inputUpdate()
2184
2018
  {
@@ -2204,34 +2038,38 @@ function inputUpdatePost()
2204
2038
  ///////////////////////////////////////////////////////////////////////////////
2205
2039
  // Keyboard event handlers
2206
2040
 
2207
- onkeydown = e=>
2041
+ onkeydown = (e)=>
2208
2042
  {
2209
2043
  if (debug && e.target != document.body) return;
2210
- e.repeat || (inputData[usingGamepad = 0][remapKeyCode(e.keyCode)] = 3);
2044
+ e.repeat || (inputData[isUsingGamepad = 0][remapKeyCode(e.keyCode)] = 3);
2211
2045
  debug || e.preventDefault();
2212
2046
  }
2213
- onkeyup = e=>
2047
+ onkeyup = (e)=>
2214
2048
  {
2215
2049
  if (debug && e.target != document.body) return;
2216
2050
  inputData[0][remapKeyCode(e.keyCode)] = 4;
2217
2051
  }
2218
- const remapKeyCode = c=> copyWASDToDpad ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
2052
+ const remapKeyCode = (c)=> inputWASDEmulateDirection ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
2219
2053
 
2220
2054
  ///////////////////////////////////////////////////////////////////////////////
2221
2055
  // Mouse event handlers
2222
2056
 
2223
- onmousedown = e=> {inputData[usingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
2224
- onmouseup = e=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2225
- onmousemove = e=>
2057
+ onmousedown = (e)=> {inputData[isUsingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
2058
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2059
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2060
+ onwheel = (e)=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
2061
+ oncontextmenu = (e)=> !1; // prevent right click menu
2062
+
2063
+ // convert a mouse or touch event position to screen space
2064
+ const mouseToScreen = (mousePos)=>
2226
2065
  {
2227
- // convert mouse pos to canvas space
2228
- if (!mainCanvas) return;
2066
+ if (!mainCanvas)
2067
+ return vec2(); // fix bug that can occur if user clicks before page loads
2068
+
2229
2069
  const rect = mainCanvas.getBoundingClientRect();
2230
- mousePosScreen.x = mainCanvasSize.x * percent(e.x, rect.right, rect.left);
2231
- mousePosScreen.y = mainCanvasSize.y * percent(e.y, rect.bottom, rect.top);
2070
+ return mainCanvasSize.multiply(
2071
+ vec2(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom)));
2232
2072
  }
2233
- onwheel = e=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
2234
- oncontextmenu = e=> !1; // prevent right click menu
2235
2073
 
2236
2074
  ///////////////////////////////////////////////////////////////////////////////
2237
2075
  // Gamepad input
@@ -2239,6 +2077,21 @@ oncontextmenu = e=> !1; // prevent right click menu
2239
2077
  const stickData = [];
2240
2078
  function gamepadsUpdate()
2241
2079
  {
2080
+ if (touchGamepadEnable && touchGamepadTimer.isSet())
2081
+ {
2082
+ // read virtual analog stick
2083
+ const sticks = stickData[0] || (stickData[0] = []);
2084
+ sticks[0] = vec2(touchGamepadStick.x, -touchGamepadStick.y); // flip vertical
2085
+
2086
+ // read virtual gamepad buttons
2087
+ const data = inputData[1] || (inputData[1] = []);
2088
+ for (let i=10; i--;)
2089
+ {
2090
+ const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
2091
+ data[j] = touchGamepadButtons[i] ? 1 + 2*!gamepadIsDown(j,0) : 4*gamepadIsDown(j,0);
2092
+ }
2093
+ }
2094
+
2242
2095
  if (!gamepadsEnable || !navigator.getGamepads || !document.hasFocus() && !debug)
2243
2096
  return;
2244
2097
 
@@ -2256,8 +2109,8 @@ function gamepadsUpdate()
2256
2109
  // read clamp dead zone of analog sticks
2257
2110
  const deadZone = .3, deadZoneMax = .8;
2258
2111
  const applyDeadZone = (v)=>
2259
- v > deadZone ? percent( v, deadZoneMax, deadZone) :
2260
- v < -deadZone ? -percent(-v, deadZoneMax, deadZone) : 0;
2112
+ v > deadZone ? percent( v, deadZone, deadZoneMax) :
2113
+ v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
2261
2114
 
2262
2115
  // read analog sticks
2263
2116
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
@@ -2268,10 +2121,11 @@ function gamepadsUpdate()
2268
2121
  {
2269
2122
  const button = gamepad.buttons[j];
2270
2123
  data[j] = button.pressed ? 1 + 2*!gamepadIsDown(j,i) : 4*gamepadIsDown(j,i);
2271
- usingGamepad |= !i && button.pressed;
2124
+ isUsingGamepad |= !i && button.pressed;
2125
+ touchGamepadEnable && touchGamepadTimer.unset(); // disable touch gamepad if using real gamepad
2272
2126
  }
2273
-
2274
- if (copyGamepadDirectionToStick)
2127
+
2128
+ if (gamepadDirectionEmulateStick)
2275
2129
  {
2276
2130
  // copy dpad to left analog stick when pressed
2277
2131
  const dpad = vec2(gamepadIsDown(15,i) - gamepadIsDown(14,i), gamepadIsDown(12,i) - gamepadIsDown(13,i));
@@ -2282,18 +2136,31 @@ function gamepadsUpdate()
2282
2136
  }
2283
2137
  }
2284
2138
 
2139
+ ///////////////////////////////////////////////////////////////////////////////
2140
+
2141
+ /** Pulse the vibration hardware if it exists
2142
+ * @param {Number} [pattern=100] - a single value in miliseconds or vibration interval array
2143
+ * @memberof Input */
2144
+ const vibrate = (pattern)=> vibrateEnable && Navigator.vibrate && Navigator.vibrate(pattern);
2145
+
2146
+ /** Cancel any ongoing vibration
2147
+ * @memberof Input */
2148
+ const vibrateStop = ()=> vibrate(0);
2149
+
2285
2150
  ///////////////////////////////////////////////////////////////////////////////
2286
2151
  // Touch input
2287
2152
 
2288
2153
  /** True if a touch device has been detected
2289
2154
  * @const {boolean}
2290
2155
  * @memberof Input */
2291
- const isTouchDevice = touchInputEnable && window.ontouchstart !== undefined;
2156
+ const isTouchDevice = window.ontouchstart !== undefined;
2157
+
2158
+ // try to enable touch mouse
2292
2159
  if (isTouchDevice)
2293
2160
  {
2294
2161
  // handle all touch events the same way
2295
2162
  let wasTouching, hadTouchInput;
2296
- ontouchstart = ontouchmove = ontouchend = e=>
2163
+ ontouchstart = ontouchmove = ontouchend = (e)=>
2297
2164
  {
2298
2165
  e.button = 0; // all touches are left click
2299
2166
 
@@ -2318,57 +2185,184 @@ if (isTouchDevice)
2318
2185
  return !e.cancelable;
2319
2186
  }
2320
2187
  }
2321
- /**
2322
- * LittleJS Audio System
2323
- * <br> - ZzFX Sound Effects and ZzFXM Music
2324
- * <br> - Caches sounds and music for fast playback
2325
- * <br> - Can attenuate and apply stereo panning to sounds
2326
- * <br> - Ability to play mp3, ogg, and wave files
2327
- * <br> - Speech synthesis wrapper functions
2328
- * @namespace Audio
2329
- */
2330
2188
 
2331
- 'use strict';
2189
+ ///////////////////////////////////////////////////////////////////////////////
2190
+ // touch gamepad, virtual on screen gamepad emulator for touch devices
2332
2191
 
2333
- /** Sound Object - Stores a zzfx sound for later use and can be played positionally */
2334
- class Sound
2192
+ // touch input internal variables
2193
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2(), touchGamepadAnalog = 1;
2194
+
2195
+ // create the touch gamepad, called automatically by the engine
2196
+ function touchGamepadCreate()
2335
2197
  {
2336
- /** Create a sound object and cache the zzfx samples for later use
2337
- * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
2338
- * @param {Number} [range=defaultSoundRange] - World space max range of sound, will not play if camera is farther away
2339
- * @param {Number} [taper=defaultSoundTaper] - At what percentage of range should it start tapering off
2340
- */
2341
- constructor(zzfxSound, range=defaultSoundRange, taper=defaultSoundTaper)
2198
+ if (!touchGamepadEnable || !isTouchDevice)
2199
+ return;
2200
+
2201
+ ontouchstart = ontouchmove = ontouchend = (e)=>
2342
2202
  {
2343
- if (!soundEnable) return;
2203
+ if (!touchGamepadEnable)
2204
+ return;
2344
2205
 
2345
- this.range = range;
2346
- this.taper = taper;
2206
+ // clear touch gamepad input
2207
+ touchGamepadStick = vec2();
2208
+ touchGamepadButtons = [];
2209
+
2210
+ const touching = e.touches.length;
2211
+ if (touching)
2212
+ {
2213
+ touchGamepadTimer.isSet() || zzfx(0) ; // fix mobile audio, force it to play a sound the first time
2347
2214
 
2348
- // get randomness from sound parameters
2349
- this.randomness = zzfxSound[1] || 0;
2350
- zzfxSound[1] = 0;
2215
+ // set that gamepad is active
2216
+ isUsingGamepad = 1;
2217
+ touchGamepadTimer.set();
2351
2218
 
2352
- // generate sound now for fast playback
2353
- this.cachedSamples = zzfxG(...zzfxSound);
2354
- }
2219
+ if (paused)
2220
+ {
2221
+ // touch anywhere to press start when paused
2222
+ touchGamepadButtons[9] = 1;
2223
+ return;
2224
+ }
2225
+ }
2355
2226
 
2356
- /** Play the sound
2357
- * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
2358
- * @param {Number} [volume=1] - How much to scale volume by (in addition to range fade)
2359
- * @param {Number} [pitch=1] - How much to scale pitch by (also adjusted by this.randomness)
2360
- * @param {Number} [randomnessScale=1] - How much to scale randomness
2361
- * @return {AudioBufferSourceNode} - The audio, can be used to stop sound later
2362
- */
2363
- play(pos, volume=1, pitch=1, randomnessScale=1)
2364
- {
2365
- if (!soundEnable) return;
2227
+ // get center of left and right sides
2228
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2229
+ const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
2230
+ const startCenter = mainCanvasSize.scale(.5);
2366
2231
 
2367
- let pan = 0;
2368
- if (pos)
2232
+ // check each touch point
2233
+ for (const touch of e.touches)
2369
2234
  {
2370
- const range = this.range;
2371
- if (range)
2235
+ const touchPos = mouseToScreen(vec2(touch.clientX, touch.clientY));
2236
+ if (touchPos.distance(stickCenter) < touchGamepadSize)
2237
+ {
2238
+ // virtual analog stick
2239
+ if (touchGamepadAnalog)
2240
+ touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
2241
+ else
2242
+ {
2243
+ // 8 way dpad
2244
+ const angle = touchPos.subtract(stickCenter).angle();
2245
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2246
+ }
2247
+ }
2248
+ else if (touchPos.distance(buttonCenter) < touchGamepadSize)
2249
+ {
2250
+ // virtual face buttons
2251
+ const button = touchPos.subtract(buttonCenter).direction();
2252
+ touchGamepadButtons[button] = 1;
2253
+ }
2254
+ else if (touchPos.distance(startCenter) < touchGamepadSize)
2255
+ {
2256
+ // virtual start button in center
2257
+ touchGamepadButtons[9] = 1;
2258
+ }
2259
+ }
2260
+ }
2261
+ }
2262
+
2263
+ // render the touch gamepad, called automatically by the engine
2264
+ function touchGamepadRender()
2265
+ {
2266
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet())
2267
+ return;
2268
+
2269
+ // fade off when not touching or paused
2270
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
2271
+ if (!alpha || paused)
2272
+ return;
2273
+
2274
+ // setup the canvas
2275
+ overlayContext.save();
2276
+ overlayContext.globalAlpha = alpha*touchGamepadAlpha;
2277
+ overlayContext.strokeStyle = '#fff';
2278
+ overlayContext.lineWidth = 3;
2279
+
2280
+ // draw left analog stick
2281
+ overlayContext.fillStyle = gamepadStick(0).length() > 0 ? '#fff' : '#000';
2282
+ overlayContext.beginPath();
2283
+ overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize/2, 0,9);
2284
+ overlayContext.fill();
2285
+ overlayContext.stroke();
2286
+
2287
+ // draw right face buttons
2288
+ const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2289
+ for (let i=4; i--;)
2290
+ {
2291
+ const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize/2));
2292
+ overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2293
+ overlayContext.beginPath();
2294
+ overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
2295
+ overlayContext.fill();
2296
+ overlayContext.stroke();
2297
+ }
2298
+
2299
+ // set canvas back to normal
2300
+ overlayContext.restore();
2301
+ }
2302
+ /**
2303
+ * LittleJS Audio System
2304
+ * <br> - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a>
2305
+ * <br> - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a>
2306
+ * <br> - Caches sounds and music for fast playback
2307
+ * <br> - Can attenuate and apply stereo panning to sounds
2308
+ * <br> - Ability to play mp3, ogg, and wave files
2309
+ * <br> - Speech synthesis wrapper functions
2310
+ */
2311
+
2312
+ 'use strict';
2313
+
2314
+ /**
2315
+ * Sound Object - Stores a zzfx sound for later use and can be played positionally
2316
+ * <br>
2317
+ * <br><b><a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a></b>
2318
+ * @example
2319
+ * // create a sound
2320
+ * const sound_example = new Sound([.5,.5]);
2321
+ *
2322
+ * // play the sound
2323
+ * sound_example.play();
2324
+ */
2325
+ class Sound
2326
+ {
2327
+ /** Create a sound object and cache the zzfx samples for later use
2328
+ * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
2329
+ * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2330
+ * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
2331
+ */
2332
+ constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
2333
+ {
2334
+ if (!soundEnable) return;
2335
+
2336
+ /** @property {Number} - World space max range of sound, will not play if camera is farther away */
2337
+ this.range = range;
2338
+
2339
+ /** @property {Number} - At what percentage of range should it start tapering off */
2340
+ this.taper = taper;
2341
+
2342
+ // get randomness from sound parameters
2343
+ this.randomness = zzfxSound[1] || 0;
2344
+ zzfxSound[1] = 0;
2345
+
2346
+ // generate sound now for fast playback
2347
+ this.cachedSamples = zzfxG(...zzfxSound);
2348
+ }
2349
+
2350
+ /** Play the sound
2351
+ * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
2352
+ * @param {Number} [volume=1] - How much to scale volume by (in addition to range fade)
2353
+ * @param {Number} [pitch=1] - How much to scale pitch by (also adjusted by this.randomness)
2354
+ * @param {Number} [randomnessScale=1] - How much to scale randomness
2355
+ * @return {AudioBufferSourceNode} - The audio, can be used to stop sound later
2356
+ */
2357
+ play(pos, volume=1, pitch=1, randomnessScale=1)
2358
+ {
2359
+ if (!soundEnable) return;
2360
+
2361
+ let pan = 0;
2362
+ if (pos)
2363
+ {
2364
+ const range = this.range;
2365
+ if (range)
2372
2366
  {
2373
2367
  // apply range based fade
2374
2368
  const lengthSquared = cameraPos.distanceSquared(pos);
@@ -2376,7 +2370,7 @@ class Sound
2376
2370
  return; // out of range
2377
2371
 
2378
2372
  // attenuate volume by distance
2379
- volume *= percent(lengthSquared**.5, range*this.taper, range);
2373
+ volume *= percent(lengthSquared**.5, range, range*this.taper);
2380
2374
  }
2381
2375
 
2382
2376
  // get pan from screen space coords
@@ -2402,7 +2396,36 @@ class Sound
2402
2396
  }
2403
2397
  }
2404
2398
 
2405
- /** Music Object - Stores a zzfx music track for later use */
2399
+ /**
2400
+ * Music Object - Stores a zzfx music track for later use
2401
+ * <br>
2402
+ * <br><b><a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a></b>
2403
+ * @example
2404
+ * // create some music
2405
+ * const music_example = new Music(
2406
+ * [
2407
+ * [ // instruments
2408
+ * [,0,400] // simple note
2409
+ * ],
2410
+ * [ // patterns
2411
+ * [ // pattern 1
2412
+ * [ // channel 0
2413
+ * 0, -1, // instrument 0, left speaker
2414
+ * 1, 0, 9, 1 // channel notes
2415
+ * ],
2416
+ * [ // channel 1
2417
+ * 0, 1, // instrument 1, right speaker
2418
+ * 0, 12, 17, -1 // channel notes
2419
+ * ]
2420
+ * ],
2421
+ * ],
2422
+ * [0, 0, 0, 0], // sequence, play pattern 0 four times
2423
+ * 90 // BPM
2424
+ * ]);
2425
+ *
2426
+ * // play the music
2427
+ * music_example.play();
2428
+ */
2406
2429
  class Music
2407
2430
  {
2408
2431
  /** Create a music object and cache the zzfx music samples for later use
@@ -2439,7 +2462,7 @@ function playAudioFile(url, volume=1, loop=1)
2439
2462
  if (!soundEnable) return;
2440
2463
 
2441
2464
  const audio = new Audio(url);
2442
- audio.volume = audioVolume * volume;
2465
+ audio.volume = soundVolume * volume;
2443
2466
  audio.loop = loop;
2444
2467
  audio.play();
2445
2468
  return audio;
@@ -2464,7 +2487,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
2464
2487
  // build utterance and speak
2465
2488
  const utterance = new SpeechSynthesisUtterance(text);
2466
2489
  utterance.lang = language;
2467
- utterance.volume = 2*volume*audioVolume;
2490
+ utterance.volume = 2*volume*soundVolume;
2468
2491
  utterance.rate = rate;
2469
2492
  utterance.pitch = pitch;
2470
2493
  speechSynthesis.speak(utterance);
@@ -2473,7 +2496,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
2473
2496
 
2474
2497
  /** Stop all queued speech
2475
2498
  * @memberof Audio */
2476
- const stopSpeech = ()=> speechSynthesis && speechSynthesis.cancel();
2499
+ const speakStop = ()=> speechSynthesis && speechSynthesis.cancel();
2477
2500
 
2478
2501
  /** Get frequency of a note on a musical scale
2479
2502
  * @param {Number} semitoneOffset - How many semitones away from the root note
@@ -2504,6 +2527,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2504
2527
  if (!audioContext)
2505
2528
  audioContext = new (window.AudioContext||webkitAudioContext);
2506
2529
 
2530
+ // fix stalled audio
2531
+ audioContext.resume();
2532
+
2533
+ // prevent sounds from building up if they can't be played
2534
+ if (audioContext.state != 'running')
2535
+ return;
2536
+
2507
2537
  // create buffer and source
2508
2538
  const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, zzfxR),
2509
2539
  source = audioContext.createBufferSource();
@@ -2514,11 +2544,18 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2514
2544
  source.playbackRate.value = rate;
2515
2545
  source.loop = loop;
2516
2546
 
2517
- // create pan and gain nodes
2518
- source
2519
- .connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, 1, -1)}))
2520
- .connect(new GainNode(audioContext, {'gain':audioVolume*volume}))
2521
- .connect(audioContext.destination);
2547
+ // create and connect gain node (createGain is more widley spported then GainNode construtor)
2548
+ const gainNode = audioContext.createGain();
2549
+ gainNode.gain.value = soundVolume*volume;
2550
+ gainNode.connect(audioContext.destination);
2551
+
2552
+ // connect source to gain
2553
+ (
2554
+ window.StereoPannerNode ? // create pan node if possible
2555
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)}))
2556
+ : source
2557
+ )
2558
+ .connect(gainNode);
2522
2559
 
2523
2560
  // play and return sound
2524
2561
  source.start();
@@ -2529,6 +2566,8 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2529
2566
  // ZzFXMicro - Zuper Zmall Zound Zynth - v1.1.8 by Frank Force
2530
2567
 
2531
2568
  /** Generate and play a ZzFX sound
2569
+ * <br>
2570
+ * <br><b><a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a></b>
2532
2571
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
2533
2572
  * @return {Array} - Array of audio samples
2534
2573
  * @memberof Audio */
@@ -2585,7 +2624,7 @@ function zzfxG
2585
2624
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
2586
2625
  : 1) *
2587
2626
  sign(s)*(abs(s)**shapeCurve) * // curve 0=square, 2=pointy
2588
- volume * audioVolume * ( // envelope
2627
+ volume * soundVolume * ( // envelope
2589
2628
  i < attack ? i/attack : // attack
2590
2629
  i < attack + decay ? // decay
2591
2630
  1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
@@ -2731,27 +2770,31 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2731
2770
  return [leftChannelBuffer, rightChannelBuffer];
2732
2771
  }
2733
2772
  /**
2734
- * LittleJS Tile Layer System
2735
- * <br> - Caches arrays of tiles to offscreen canvas for fast rendering
2736
- * <br> - Unlimted numbers of layers, allocates canvases as needed
2737
- * <br> - Interfaces with EngineObject for collision
2738
- * <br> - Collision layer is separate from visible layers
2739
- * <br> - Tile layers can be drawn to using their context with canvas2d
2740
- * <br> - It is recommended to have a visible layer that matches the collision
2741
- * @namespace TileLayer
2773
+ * LittleJS Tile Layer System
2774
+ * <br> - Caches arrays of tiles to off screen canvas for fast rendering
2775
+ * <br> - Unlimted numbers of layers, allocates canvases as needed
2776
+ * <br> - Interfaces with EngineObject for collision
2777
+ * <br> - Collision layer is separate from visible layers
2778
+ * <br> - It is recommended to have a visible layer that matches the collision
2779
+ * <br> - Tile layers can be drawn to using their context with canvas2d
2780
+ * <br> - Drawn directly to the main canvas without using WebGL
2781
+ * @namespace TileCollision
2742
2782
  */
2743
2783
 
2744
2784
  'use strict';
2745
2785
 
2746
- ///////////////////////////////////////////////////////////////////////////////
2747
- // Tile Collision
2786
+ /** The tile collision layer array, use setTileCollisionData and getTileCollisionData to access
2787
+ * @memberof TileCollision */
2788
+ let tileCollision = [];
2748
2789
 
2749
- // Internal variables not exposed to documentation
2750
- let tileCollision = [], tileCollisionSize = vec2();
2790
+ /** Size of the tile collision layer
2791
+ * @type {Vector2}
2792
+ * @memberof TileCollision */
2793
+ let tileCollisionSize = vec2();
2751
2794
 
2752
2795
  /** Clear and initialize tile collision
2753
2796
  * @param {Vector2} size
2754
- * @memberof TileLayer */
2797
+ * @memberof TileCollision */
2755
2798
  function initTileCollision(size)
2756
2799
  {
2757
2800
  tileCollisionSize = size;
@@ -2763,14 +2806,14 @@ function initTileCollision(size)
2763
2806
  /** Set tile collision data
2764
2807
  * @param {Vector2} pos
2765
2808
  * @param {Number} [data=0]
2766
- * @memberof TileLayer */
2809
+ * @memberof TileCollision */
2767
2810
  const setTileCollisionData = (pos, data=0)=>
2768
2811
  pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
2769
2812
 
2770
2813
  /** Get tile collision data
2771
2814
  * @param {Vector2} pos
2772
2815
  * @return {Number}
2773
- * @memberof TileLayer */
2816
+ * @memberof TileCollision */
2774
2817
  const getTileCollisionData = (pos)=>
2775
2818
  pos.arrayCheck(tileCollisionSize) ? tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] : 0;
2776
2819
 
@@ -2779,13 +2822,13 @@ const getTileCollisionData = (pos)=>
2779
2822
  * @param {Vector2} [size=new Vector2(1,1)]
2780
2823
  * @param {EngineObject} [object]
2781
2824
  * @return {Boolean}
2782
- * @memberof TileLayer */
2825
+ * @memberof TileCollision */
2783
2826
  function tileCollisionTest(pos, size=vec2(), object)
2784
2827
  {
2785
- const minX = max(Math.floor(pos.x - size.x/2), 0);
2786
- const minY = max(Math.floor(pos.y - size.y/2), 0);
2787
- const maxX = min(pos.x + size.x/2, tileCollisionSize.x-1);
2788
- const maxY = min(pos.y + size.y/2, tileCollisionSize.y-1);
2828
+ const minX = max(pos.x - size.x/2|0, 0);
2829
+ const minY = max(pos.y - size.y/2|0, 0);
2830
+ const maxX = min(pos.x + size.x/2, tileCollisionSize.x);
2831
+ const maxY = min(pos.y + size.y/2, tileCollisionSize.y);
2789
2832
  for (let y = minY; y < maxY; ++y)
2790
2833
  for (let x = minX; x < maxX; ++x)
2791
2834
  {
@@ -2800,7 +2843,7 @@ function tileCollisionTest(pos, size=vec2(), object)
2800
2843
  * @param {Vector2} posEnd
2801
2844
  * @param {EngineObject} [object]
2802
2845
  * @return {Vector2}
2803
- * @memberof TileLayer */
2846
+ * @memberof TileCollision */
2804
2847
  function tileCollisionRaycast(posStart, posEnd, object)
2805
2848
  {
2806
2849
  // test if a ray collides with tiles from start to end
@@ -2834,23 +2877,32 @@ function tileCollisionRaycast(posStart, posEnd, object)
2834
2877
  ///////////////////////////////////////////////////////////////////////////////
2835
2878
  // Tile Layer Rendering System
2836
2879
 
2837
- // Reuse canvas autmatically when destroyed
2838
- const tileLayerCanvasCache = [];
2839
-
2840
- /** Tile layer data object stores info about how to render a tile */
2880
+ /**
2881
+ * Tile layer data object stores info about how to render a tile
2882
+ * @example
2883
+ * // create tile layer data with tile index 0 and random orientation and color
2884
+ * const tileIndex = 0;
2885
+ * const direction = randInt(4)
2886
+ * const mirror = randInt(2);
2887
+ * const color = randColor();
2888
+ * const data = new TileLayerData(tileIndex, direction, mirror, color);
2889
+ */
2841
2890
  class TileLayerData
2842
2891
  {
2843
- /** Create a tile layer data object
2844
- * @param {Number} [tile] - The tile to use, untextured if undefined
2845
- * @param {Number} [direction=0] - Integer direction of tile, in 90 degree increments
2846
- * @param {Boolean} [mirror=0] - If the tile should be mirrored along the x axis
2847
- * @param {Color} [color=new Color(1,1,1)] - Color of the tile
2848
- */
2892
+ /** Create a tile layer data object, one for each tile in a TileLayer
2893
+ * @param {Number} [tile] - The tile to use, untextured if undefined
2894
+ * @param {Number} [direction=0] - Integer direction of tile, in 90 degree increments
2895
+ * @param {Boolean} [mirror=0] - If the tile should be mirrored along the x axis
2896
+ * @param {Color} [color=new Color(1,1,1)] - Color of the tile */
2849
2897
  constructor(tile, direction=0, mirror=0, color=new Color)
2850
2898
  {
2899
+ /** @property {Number} - The tile to use, untextured if undefined */
2851
2900
  this.tile = tile;
2901
+ /** @property {Number} - Integer direction of tile, in 90 degree increments */
2852
2902
  this.direction = direction;
2903
+ /** @property {Boolean} - If the tile should be mirrored along the x axis */
2853
2904
  this.mirror = mirror;
2905
+ /** @property {Color} - Color of the tile */
2854
2906
  this.color = color;
2855
2907
  }
2856
2908
 
@@ -2858,44 +2910,49 @@ class TileLayerData
2858
2910
  clear() { this.tile = this.direction = this.mirror = 0; color = new Color; }
2859
2911
  }
2860
2912
 
2861
- /** Tile layer object - cached rendering system for tile layers */
2913
+ /**
2914
+ * Tile layer object - cached rendering system for tile layers
2915
+ * <br> - Each Tile layer is rendered to an off screen canvas
2916
+ * <br> - To allow dynamic modifications, layers are rendered using canvas 2d
2917
+ * <br> - Some devices like mobile phones are limited to 4k texture resolution
2918
+ * <br> - So with 16x16 tiles this limits layers to 256x256 on mobile devices
2919
+ * @extends EngineObject
2920
+ * @example
2921
+ * // create tile collision and visible tile layer
2922
+ * initTileCollision(vec2(200,100));
2923
+ * const tileLayer = new TileLayer();
2924
+ */
2862
2925
  class TileLayer extends EngineObject
2863
2926
  {
2864
- /** Create a tile layer data object
2927
+ /** Create a tile layer object
2865
2928
  * @param {Vector2} [position=new Vector2(0,0)] - World space position
2866
- * @param {Vector2} [size=defaultObjectSize] - World space size
2867
- * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this in world space
2868
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2929
+ * @param {Vector2} [size=objectDefaultSize] - World space size
2930
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2931
+ * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2932
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2869
2933
  */
2870
- constructor(pos, size, scale=vec2(1), renderOrder=0)
2934
+ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2871
2935
  {
2872
- super(pos, size);
2936
+ super(pos, size, -1, tileSize, 0, undefined, renderOrder);
2873
2937
 
2874
- // create new canvas if necessary
2875
- this.canvas = tileLayerCanvasCache.length ? tileLayerCanvasCache.pop() : document.createElement('canvas');
2938
+ /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
2939
+ this.canvas = document.createElement('canvas');
2940
+ /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
2876
2941
  this.context = this.canvas.getContext('2d');
2942
+ /** @property {Vector2} - How much to scale this layer when rendered */
2877
2943
  this.scale = scale;
2878
- this.tileSize = defaultTileSize.copy();
2879
- this.renderOrder = renderOrder;
2880
- this.flushGLBeforeRender = 1;
2944
+ /** @property {Boolean} [isOverlay=0] - If true this layer will render to overlay canvas and appear above all objects */
2945
+ this.isOverlay;
2881
2946
 
2882
2947
  // init tile data
2883
2948
  this.data = [];
2884
2949
  for (let j = this.size.area(); j--;)
2885
2950
  this.data.push(new TileLayerData());
2886
2951
  }
2887
-
2888
- /** Destroy this tile layer */
2889
- destroy()
2890
- {
2891
- // add canvas back to the cache
2892
- tileLayerCanvasCache.push(this.canvas);
2893
- super.destroy();
2894
- }
2895
2952
 
2896
2953
  /** Set data at a given position in the array
2897
- * @param {Vector2} position - Local position in array
2898
- * @param {TileLayerData} data - Data to set
2954
+ * @param {Vector2} position - Local position in array
2955
+ * @param {TileLayerData} data - Data to set
2899
2956
  * @param {Boolean} [redraw=0] - Force the tile to redraw if true */
2900
2957
  setData(layerPos, data, redraw)
2901
2958
  {
@@ -2920,50 +2977,48 @@ class TileLayer extends EngineObject
2920
2977
  {
2921
2978
  ASSERT(mainContext != this.context); // must call redrawEnd() after drawing tiles
2922
2979
 
2923
- // flush and copy gl canvas because tile canvas does not use gl
2924
- this.flushGLBeforeRender && glEnable && glCopyToContext(mainContext);
2980
+ // flush and copy gl canvas because tile canvas does not use webgl
2981
+ glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
2925
2982
 
2926
- // draw the entire cached level onto the main canvas
2983
+ // draw the entire cached level onto the canvas
2927
2984
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
2928
- mainContext.drawImage
2985
+ (this.isOverlay ? overlayContext : mainContext).drawImage
2929
2986
  (
2930
2987
  this.canvas, pos.x, pos.y,
2931
2988
  cameraScale*this.size.x*this.scale.x, cameraScale*this.size.y*this.scale.y
2932
2989
  );
2933
2990
  }
2934
2991
 
2935
- /** Draw all the tile data to an offscreen canvas using webgl if possible */
2992
+ /** Draw all the tile data to an offscreen canvas
2993
+ * - This may be slow in some browsers
2994
+ */
2936
2995
  redraw()
2937
2996
  {
2938
- this.redrawStart();
2997
+ this.redrawStart(1);
2939
2998
  this.drawAllTileData();
2940
2999
  this.redrawEnd();
2941
3000
  }
2942
3001
 
2943
3002
  /** Call to start the redraw process
2944
- * @param {Boolean} [clear=1] - Should it clear the canvas before drawing */
2945
- redrawStart(clear = 1)
3003
+ * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
3004
+ redrawStart(clear = 0)
2946
3005
  {
2947
- // clear and set size
2948
- const width = this.size.x * this.tileSize.x;
2949
- const height = this.size.y * this.tileSize.y;
2950
3006
  if (clear)
2951
3007
  {
2952
- this.canvas.width = width;
2953
- this.canvas.height = height;
3008
+ // clear and set size
3009
+ this.canvas.width = this.size.x * this.tileSize.x;
3010
+ this.canvas.height = this.size.y * this.tileSize.y;
2954
3011
  }
2955
3012
 
2956
3013
  // save current render settings
2957
- this.savedRenderSettings = [mainCanvasSize, mainCanvas, mainContext, cameraScale, cameraPos];
3014
+ this.savedRenderSettings = [mainCanvas, mainContext, cameraPos, cameraScale];
2958
3015
 
2959
- // set camera transform for renering
2960
- cameraScale = this.tileSize.x;
2961
- cameraPos = this.size.scale(.5);
3016
+ // use normal rendering system to render the tiles
2962
3017
  mainCanvas = this.canvas;
2963
3018
  mainContext = this.context;
2964
- mainContext.imageSmoothingEnabled = !pixelated; // disable smoothing for pixel art
2965
- mainCanvasSize = vec2(width, height);
2966
- glPreRender(width, height);
3019
+ cameraPos = this.size.scale(.5);
3020
+ cameraScale = this.tileSize.x;
3021
+ enginePreRender();
2967
3022
  }
2968
3023
 
2969
3024
  /** Call to end the redraw process */
@@ -2974,7 +3029,7 @@ class TileLayer extends EngineObject
2974
3029
  //debugSaveCanvas(this.canvas);
2975
3030
 
2976
3031
  // set stuff back to normal
2977
- [mainCanvasSize, mainCanvas, mainContext, cameraScale, cameraPos] = this.savedRenderSettings;
3032
+ [mainCanvas, mainContext, cameraPos, cameraScale] = this.savedRenderSettings;
2978
3033
  }
2979
3034
 
2980
3035
  /** Draw the tile at a given position
@@ -3002,13 +3057,13 @@ class TileLayer extends EngineObject
3002
3057
  this.drawTileData(vec2(x,y));
3003
3058
  }
3004
3059
 
3005
- /** Draw directly to the 2d canvas in world space (bipass webgl)
3060
+ /** Draw directly to the 2D canvas in world space (bipass webgl)
3006
3061
  * @param {Vector2} pos
3007
3062
  * @param {Vector2} size
3008
- * @param {Number} angle
3009
- * @param {Boolean} mirror
3063
+ * @param {Number} [angle=0]
3064
+ * @param {Boolean} [mirror=0]
3010
3065
  * @param {Function} drawFunction */
3011
- drawCanvas2D(pos, size, angle, mirror, drawFunction)
3066
+ drawCanvas2D(pos, size, angle=0, mirror, drawFunction)
3012
3067
  {
3013
3068
  const context = this.context;
3014
3069
  context.save();
@@ -3016,7 +3071,7 @@ class TileLayer extends EngineObject
3016
3071
  size = size.multiply(this.tileSize);
3017
3072
  context.translate(pos.x, this.canvas.height - pos.y);
3018
3073
  context.rotate(angle);
3019
- context.scale(mirror?-size.x:size.x, size.y);
3074
+ context.scale(mirror ? -size.x : size.x, size.y);
3020
3075
  drawFunction(context);
3021
3076
  context.restore();
3022
3077
  }
@@ -3025,11 +3080,11 @@ class TileLayer extends EngineObject
3025
3080
  * @param {Vector2} pos
3026
3081
  * @param {Vector2} [size=new Vector2(1,1)]
3027
3082
  * @param {Number} [tileIndex=-1]
3028
- * @param {Vector2} [tileSize=defaultTileSize]
3083
+ * @param {Vector2} [tileSize=tileSizeDefault]
3029
3084
  * @param {Color} [color=new Color(1,1,1)]
3030
3085
  * @param {Number} [angle=0]
3031
3086
  * @param {Boolean} [mirror=0] */
3032
- drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=defaultTileSize, color=new Color, angle=0, mirror)
3087
+ drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color, angle, mirror)
3033
3088
  {
3034
3089
  this.drawCanvas2D(pos, size, angle, mirror, (context)=>
3035
3090
  {
@@ -3042,7 +3097,7 @@ class TileLayer extends EngineObject
3042
3097
  else
3043
3098
  {
3044
3099
  const cols = tileImage.width/tileSize.x;
3045
- context.globalAlpha = color.a; // full color not supported in this mode
3100
+ context.globalAlpha = color.a; // only alpha, no color, is supported in this mode
3046
3101
  context.drawImage(tileImage,
3047
3102
  (tileIndex%cols)*tileSize.x, (tileIndex/cols|0)*tileSize.x,
3048
3103
  tileSize.x, tileSize.y, -.5, -.5, 1, 1);
@@ -3055,7 +3110,7 @@ class TileLayer extends EngineObject
3055
3110
  * @param {Vector2} [size=new Vector2(1,1)]
3056
3111
  * @param {Color} [color=new Color(1,1,1)]
3057
3112
  * @param {Number} [angle=0] */
3058
- drawRect(pos, size, color, angle) { this.drawTile(pos, size, -1, 0, color, angle, 0); }
3113
+ drawRect(pos, size, color, angle) { this.drawTile(pos, size, -1, 0, color, angle); }
3059
3114
  }
3060
3115
  /*
3061
3116
  LittleJS Particle System
@@ -3068,37 +3123,50 @@ class TileLayer extends EngineObject
3068
3123
 
3069
3124
  /**
3070
3125
  * Particle Emitter - Spawns particles with the given settings
3126
+ * @extends EngineObject
3127
+ * @example
3128
+ * // create a particle emitter
3129
+ * let pos = vec2(2,3);
3130
+ * let particleEmiter = new ParticleEmitter
3131
+ * (
3132
+ * pos, 1, 0, 500, PI, // pos, emitSize, emitTime, emitRate, emiteCone
3133
+ * 0, vec2(16), // tileIndex, tileSize
3134
+ * new Color, new Color(0,0,0), // colorStartA, colorStartB
3135
+ * new Color(1,1,1,0), new Color(0,0,0,0), // colorEndA, colorEndB
3136
+ * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
3137
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
3138
+ * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
3139
+ * );
3071
3140
  */
3072
3141
  class ParticleEmitter extends EngineObject
3073
3142
  {
3074
- /**
3075
- * Create a particle system with the given settings
3076
- * @param {Vector2} position - World space position of the emitter
3077
- * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
3078
- * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
3079
- * @param {Number} [emitRate=100] - How many particles per second to spawn
3080
- * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
3081
- * @param {Number} [tileIndex=-1] - Index into tile sheet, if <0 no texture is applied
3082
- * @param {Number} [tileSize=defaultTileSize] - Tile size for particles
3083
- * @param {Color} [colorStartA=new Color(1,1,1)] - Color at start of life 1, randomized between start colors
3084
- * @param {Color} [colorStartB=new Color(1,1,1)] - Color at start of life 2, randomized between start colors
3085
- * @param {Color} [colorEndA=new Color(1,1,1,0)] - Color at end of life 1, randomized between end colors
3086
- * @param {Color} [colorEndB=new Color(1,1,1,0)] - Color at end of life 2, randomized between end colors
3087
- * @param {Number} [particleTime=.5] - How long particles live
3088
- * @param {Number} [sizeStart=.1] - How big are particles at start
3089
- * @param {Number} [sizeEnd=1] - How big are particles at end
3090
- * @param {Number} [speed=.1] - How fast are particles when spawned
3091
- * @param {Number} [angleSpeed=.05] - How fast are particles rotating
3092
- * @param {Number} [damping=1] - How much to dampen particle speed
3093
- * @param {Number} [angleDamping=1] - How much to dampen particle angular speed
3094
- * @param {Number} [gravityScale=0] - How much does gravity effect particles
3095
- * @param {Number} [particleConeAngle=PI] - Cone for start particle angle
3096
- * @param {Number} [fadeRate=.1] - How quick to fade in particles at start/end in percent of life
3097
- * @param {Number} [randomness=.2] - Apply extra randomness percent
3098
- * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
3099
- * @param {Boolean} [additive=0] - Should particles use addtive blend
3100
- * @param {Boolean} [randomColorLinear=0] - Should color be randomized linearly or across each component
3101
- * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
3143
+ /** Create a particle system with the given settings
3144
+ * @param {Vector2} position - World space position of the emitter
3145
+ * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
3146
+ * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
3147
+ * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
3148
+ * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
3149
+ * @param {Number} [tileIndex=-1] - Index into tile sheet, if <0 no texture is applied
3150
+ * @param {Number} [tileSize=tileSizeDefault] - Tile size for particles
3151
+ * @param {Color} [colorStartA=new Color(1,1,1)] - Color at start of life 1, randomized between start colors
3152
+ * @param {Color} [colorStartB=new Color(1,1,1)] - Color at start of life 2, randomized between start colors
3153
+ * @param {Color} [colorEndA=new Color(1,1,1,0)] - Color at end of life 1, randomized between end colors
3154
+ * @param {Color} [colorEndB=new Color(1,1,1,0)] - Color at end of life 2, randomized between end colors
3155
+ * @param {Number} [particleTime=.5] - How long particles live
3156
+ * @param {Number} [sizeStart=.1] - How big are particles at start
3157
+ * @param {Number} [sizeEnd=1] - How big are particles at end
3158
+ * @param {Number} [speed=.1] - How fast are particles when spawned
3159
+ * @param {Number} [angleSpeed=.05] - How fast are particles rotating
3160
+ * @param {Number} [damping=1] - How much to dampen particle speed
3161
+ * @param {Number} [angleDamping=1] - How much to dampen particle angular speed
3162
+ * @param {Number} [gravityScale=0] - How much does gravity effect particles
3163
+ * @param {Number} [particleConeAngle=PI] - Cone for start particle angle
3164
+ * @param {Number} [fadeRate=.1] - How quick to fade in particles at start/end in percent of life
3165
+ * @param {Number} [randomness=.2] - Apply extra randomness percent
3166
+ * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
3167
+ * @param {Boolean} [additive=0] - Should particles use addtive blend
3168
+ * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
3169
+ * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
3102
3170
  */
3103
3171
  constructor
3104
3172
  (
@@ -3108,7 +3176,7 @@ class ParticleEmitter extends EngineObject
3108
3176
  emitRate = 100,
3109
3177
  emitConeAngle = PI,
3110
3178
  tileIndex = -1,
3111
- tileSize = defaultTileSize,
3179
+ tileSize = tileSizeDefault,
3112
3180
  colorStartA = new Color,
3113
3181
  colorStartB = new Color,
3114
3182
  colorEndA = new Color(1,1,1,0),
@@ -3130,37 +3198,61 @@ class ParticleEmitter extends EngineObject
3130
3198
  renderOrder = additive ? 1e9 : 0
3131
3199
  )
3132
3200
  {
3133
- super(pos, new Vector2, tileIndex, tileSize);
3201
+ super(pos, new Vector2, tileIndex, tileSize, 0, undefined, renderOrder);
3134
3202
 
3135
3203
  // emitter settings
3204
+ /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
3136
3205
  this.emitSize = emitSize
3206
+ /** @property {Number} - How long to stay alive (0 is forever) */
3137
3207
  this.emitTime = emitTime;
3208
+ /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
3138
3209
  this.emitRate = emitRate;
3210
+ /** @property {Number} - Local angle to apply velocity to particles from emitter */
3139
3211
  this.emitConeAngle = emitConeAngle;
3140
3212
 
3141
3213
  // color settings
3214
+ /** @property {Color} - Color at start of life 1, randomized between start colors */
3142
3215
  this.colorStartA = colorStartA;
3216
+ /** @property {Color} - Color at start of life 2, randomized between start colors */
3143
3217
  this.colorStartB = colorStartB;
3218
+ /** @property {Color} - Color at end of life 1, randomized between end colors */
3144
3219
  this.colorEndA = colorEndA;
3220
+ /** @property {Color} - Color at end of life 2, randomized between end colors */
3145
3221
  this.colorEndB = colorEndB;
3222
+ /** @property {Boolean} - Should color be randomized linearly or across each component */
3146
3223
  this.randomColorLinear = randomColorLinear;
3147
3224
 
3148
3225
  // particle settings
3226
+ /** @property {Number} - How long particles live */
3149
3227
  this.particleTime = particleTime;
3228
+ /** @property {Number} - How big are particles at start */
3150
3229
  this.sizeStart = sizeStart;
3230
+ /** @property {Number} - How big are particles at end */
3151
3231
  this.sizeEnd = sizeEnd;
3232
+ /** @property {Number} - How fast are particles when spawned */
3152
3233
  this.speed = speed;
3234
+ /** @property {Number} - How fast are particles rotating */
3153
3235
  this.angleSpeed = angleSpeed;
3236
+ /** @property {Number} - How much to dampen particle speed */
3154
3237
  this.damping = damping;
3238
+ /** @property {Number} - How much to dampen particle angular speed */
3155
3239
  this.angleDamping = angleDamping;
3240
+ /** @property {Number} - How much does gravity effect particles */
3156
3241
  this.gravityScale = gravityScale;
3242
+ /** @property {Number} - Cone for start particle angle */
3157
3243
  this.particleConeAngle = particleConeAngle;
3244
+ /** @property {Number} - How quick to fade in particles at start/end in percent of life */
3158
3245
  this.fadeRate = fadeRate;
3246
+ /** @property {Number} - Apply extra randomness percent */
3159
3247
  this.randomness = randomness;
3248
+ /** @property {Number} - Do particles collide against tiles */
3160
3249
  this.collideTiles = collideTiles;
3250
+ /** @property {Number} - Should particles use addtive blend */
3161
3251
  this.additive = additive;
3162
- this.renderOrder = renderOrder;
3163
- this.trailScale =
3252
+ /** @property {Number} - If set the partile is drawn as a trail, stretched in the drection of velocity */
3253
+ this.trailScale = 0;
3254
+
3255
+ // internal variables
3164
3256
  this.emitTimeBuffer = 0;
3165
3257
  }
3166
3258
 
@@ -3174,9 +3266,9 @@ class ParticleEmitter extends EngineObject
3174
3266
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
3175
3267
  {
3176
3268
  // emit particles
3177
- if (this.emitRate)
3269
+ if (this.emitRate * particleEmitRateScale)
3178
3270
  {
3179
- const rate = 1/this.emitRate;
3271
+ const rate = 1/this.emitRate/particleEmitRateScale;
3180
3272
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
3181
3273
  this.emitParticle();
3182
3274
  }
@@ -3203,34 +3295,34 @@ class ParticleEmitter extends EngineObject
3203
3295
  const randomizeScale = (v)=> v + v*rand(randomness, -randomness);
3204
3296
 
3205
3297
  // randomize particle settings
3206
- const particleTime = randomizeScale(this.particleTime);
3207
- const sizeStart = randomizeScale(this.sizeStart);
3208
- const sizeEnd = randomizeScale(this.sizeEnd);
3209
- const speed = randomizeScale(this.speed);
3210
- const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3211
- const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3212
- const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3213
- const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3298
+ const particleTime = randomizeScale(this.particleTime);
3299
+ const sizeStart = randomizeScale(this.sizeStart);
3300
+ const sizeEnd = randomizeScale(this.sizeEnd);
3301
+ const speed = randomizeScale(this.speed);
3302
+ const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3303
+ const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3304
+ const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3305
+ const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3214
3306
 
3215
3307
  // build particle settings
3216
- particle.colorStart = colorStart;
3217
- particle.colorEndDelta = colorEnd.subtract(colorStart);
3218
- particle.velocity = (new Vector2).setAngle(this.angle + coneAngle, speed);
3219
- particle.angleVelocity = angleSpeed;
3220
- particle.lifeTime = particleTime;
3221
- particle.sizeStart = sizeStart;
3222
- particle.sizeEndDelta = sizeEnd - sizeStart;
3223
- particle.fadeRate = this.fadeRate;
3224
- particle.damping = this.damping;
3225
- particle.angleDamping = this.angleDamping;
3226
- particle.elasticity = this.elasticity;
3227
- particle.friction = this.friction;
3228
- particle.gravityScale = this.gravityScale;
3229
- particle.collideTiles = this.collideTiles;
3230
- particle.additive = this.additive;
3231
- particle.renderOrder = this.renderOrder;
3232
- particle.trailScale = this.trailScale;
3233
- particle.mirror = rand()<.5;
3308
+ particle.colorStart = colorStart;
3309
+ particle.colorEndDelta = colorEnd.subtract(colorStart);
3310
+ particle.velocity = (new Vector2).setAngle(this.angle + coneAngle, speed);
3311
+ particle.angleVelocity = angleSpeed;
3312
+ particle.lifeTime = particleTime;
3313
+ particle.sizeStart = sizeStart;
3314
+ particle.sizeEndDelta = sizeEnd - sizeStart;
3315
+ particle.fadeRate = this.fadeRate;
3316
+ particle.damping = this.damping;
3317
+ particle.angleDamping = this.angleDamping;
3318
+ particle.elasticity = this.elasticity;
3319
+ particle.friction = this.friction;
3320
+ particle.gravityScale = this.gravityScale;
3321
+ particle.collideTiles = this.collideTiles;
3322
+ particle.additive = this.additive;
3323
+ particle.renderOrder = this.renderOrder;
3324
+ particle.trailScale = this.trailScale;
3325
+ particle.mirror = rand()<.5;
3234
3326
 
3235
3327
  // setup callbacks for particles
3236
3328
  particle.destroyCallback = this.particleDestroyCallback;
@@ -3247,15 +3339,16 @@ class ParticleEmitter extends EngineObject
3247
3339
  ///////////////////////////////////////////////////////////////////////////////
3248
3340
  /**
3249
3341
  * Particle Object - Created automatically by Particle Emitters
3342
+ * @extends EngineObject
3250
3343
  */
3251
3344
  class Particle extends EngineObject
3252
3345
  {
3253
3346
  /**
3254
3347
  * Create a particle with the given settings
3255
3348
  * @param {Vector2} position - World space position of the particle
3256
- * @param {Number} [tileIndex=-1] - Tile to use to render, untextured if -1
3257
- * @param {Vector2} [tileSize=defaultTileSize] - Size of tile in source pixels
3258
- * @param {Number} [angle=0] - Angle to rotate the particle
3349
+ * @param {Number} [tileIndex=-1] - Tile to use to render, untextured if -1
3350
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
3351
+ * @param {Number} [angle=0] - Angle to rotate the particle
3259
3352
  */
3260
3353
  constructor(pos, tileIndex, tileSize, angle) { super(pos, new Vector2, tileIndex, tileSize, angle); }
3261
3354
 
@@ -3302,11 +3395,11 @@ class Particle extends EngineObject
3302
3395
  }
3303
3396
  }
3304
3397
  /**
3305
- * LittleJS Medal System
3306
- * <br> - Tracks and displays medals
3307
- * <br> - Saves medals to local storage
3308
- * <br> - Newgrounds and OS13k integration
3309
- * @namespace Medals
3398
+ * LittleJS Medal System
3399
+ * <br> - Tracks and displays medals
3400
+ * <br> - Saves medals to local storage
3401
+ * <br> - Newgrounds and OS13k integration
3402
+ * @namespace Medals
3310
3403
  */
3311
3404
 
3312
3405
  'use strict';
@@ -3325,32 +3418,42 @@ let medalsPreventUnlock;
3325
3418
  let newgrounds;
3326
3419
 
3327
3420
  // Engine internal variables not exposed to documentation
3328
- let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimer;
3421
+ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
3329
3422
 
3330
3423
  ///////////////////////////////////////////////////////////////////////////////
3331
3424
 
3332
3425
  /** Initialize medals with a save name used for storage
3333
- * <br> - Checks if medals are unlocked
3334
3426
  * <br> - Call this after creating all medals
3427
+ * <br> - Checks if medals are unlocked
3335
3428
  * @param {String} saveName
3336
3429
  * @memberof Medals */
3337
3430
  function medalsInit(saveName)
3338
3431
  {
3339
3432
  // check if medals are unlocked
3340
3433
  medalsSaveName = saveName;
3341
- debugMedals || medals.forEach(medal=> medal.unlocked = localStorage[medal.storageKey()]);
3434
+ debugMedals || medals.forEach(medal=> medal.unlocked = 1 || localStorage[medal.storageKey()]);
3342
3435
  }
3343
3436
 
3344
- /** Medal Object - Tracks an unlockable medal */
3437
+ /**
3438
+ * Medal Object - Tracks an unlockable medal
3439
+ * @example
3440
+ * // create a medal
3441
+ * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
3442
+ *
3443
+ * // initialize medals
3444
+ * medalsInit('Example Game');
3445
+ *
3446
+ * // unlock the medal
3447
+ * medal_example.unlock();
3448
+ */
3345
3449
  class Medal
3346
3450
  {
3347
- /**
3348
- * Create an medal object and adds it to the list of medals
3349
- * @param {Number} id - The unique identifier of the medal
3350
- * @param {String} name - Name of the medal
3351
- * @param {String} [description] - Description of the medal
3352
- * @param {String} [icon='🏆'] - Icon for the medal
3353
- * @param {String} [src] - Image location for the medal
3451
+ /** Create an medal object and adds it to the list of medals
3452
+ * @param {Number} id - The unique identifier of the medal
3453
+ * @param {String} name - Name of the medal
3454
+ * @param {String} [description] - Description of the medal
3455
+ * @param {String} [icon='🏆'] - Icon for the medal
3456
+ * @param {String} [src] - Image location for the medal
3354
3457
  */
3355
3458
  constructor(id, name, description='', icon='🏆', src)
3356
3459
  {
@@ -3361,13 +3464,9 @@ class Medal
3361
3464
  this.name = name;
3362
3465
  this.description = description;
3363
3466
  this.icon = icon;
3364
-
3467
+ this.image = new Image();
3365
3468
  if (src)
3366
- {
3367
- // load image
3368
- this.image = new Image();
3369
3469
  this.image.src = src;
3370
- }
3371
3470
  }
3372
3471
 
3373
3472
  /** Unlocks a medal if not already unlocked */
@@ -3377,7 +3476,7 @@ class Medal
3377
3476
  return;
3378
3477
 
3379
3478
  // save the medal
3380
- ASSERT(medalsSaveName); // game name must be set
3479
+ ASSERT(medalsSaveName); // save name must be set
3381
3480
  localStorage[this.storageKey()] = this.unlocked = 1;
3382
3481
  medalsDisplayQueue.push(this);
3383
3482
 
@@ -3392,27 +3491,28 @@ class Medal
3392
3491
  render(hidePercent=0)
3393
3492
  {
3394
3493
  const context = overlayContext;
3395
- const x = overlayCanvas.width - medalDisplayWidth;
3494
+ const width = min(medalDisplayWidth, mainCanvas.width);
3495
+ const x = overlayCanvas.width - width;
3396
3496
  const y = -medalDisplayHeight*hidePercent;
3397
3497
 
3398
3498
  // draw containing rect and clip to that region
3399
3499
  context.save();
3400
3500
  context.beginPath();
3401
3501
  context.fillStyle = '#ddd'
3402
- context.fill(context.rect(x, y, medalDisplayWidth, medalDisplayHeight));
3403
- context.strokeStyle = context.fillStyle = '#000';
3404
- context.lineWidth = 2;
3502
+ context.fill(context.rect(x, y, width, medalDisplayHeight));
3503
+ context.strokeStyle = '#000';
3504
+ context.lineWidth = 3;
3405
3505
  context.stroke();
3406
3506
  context.clip();
3407
3507
 
3508
+ // draw the icon and text
3408
3509
  this.renderIcon(x+15+medalDisplayIconSize/2, y+medalDisplayHeight/2);
3409
-
3410
- // draw the text
3411
3510
  context.textAlign = 'left';
3412
- context.font = '3em '+ defaultFont;
3413
- context.fillText(this.name, x+medalDisplayIconSize+25, y+35);
3414
- context.font = '1.5em '+ defaultFont;
3415
- context.restore(context.fillText(this.description, x+medalDisplayIconSize+25, y+70));
3511
+ context.font = '38px '+ fontDefault;
3512
+ context.fillText(this.name, x+medalDisplayIconSize+30, y+28);
3513
+ context.font = '24px '+ fontDefault;
3514
+ context.fillText(this.description, x+medalDisplayIconSize+30, y+60);
3515
+ context.restore();
3416
3516
  }
3417
3517
 
3418
3518
  /** Render the icon for a medal
@@ -3424,21 +3524,18 @@ class Medal
3424
3524
  {
3425
3525
  // draw the image or icon
3426
3526
  const context = overlayContext;
3527
+ context.fillStyle = '#000';
3427
3528
  context.textAlign = 'center';
3428
3529
  context.textBaseline = 'middle';
3429
- context.font = size*.6 + 'px '+ defaultFont;
3430
- context.fillStyle = '#000';
3431
- if (this.image)
3530
+ context.font = size*.7 + 'px '+ fontDefault;
3531
+ if (this.image.src)
3432
3532
  context.drawImage(this.image, x-size/2, y-size/2, size, size);
3433
3533
  else
3434
3534
  context.fillText(this.icon, x, y); // show icon if there is no image
3435
3535
  }
3436
3536
 
3437
3537
  // Get local storage key used by the medal
3438
- storageKey()
3439
- {
3440
- return medalsSaveName + '_medal_' + this.id;
3441
- }
3538
+ storageKey() { return medalsSaveName + '_' + this.id; }
3442
3539
  }
3443
3540
 
3444
3541
  // engine automatically renders medals
@@ -3449,11 +3546,11 @@ function medalsRender()
3449
3546
 
3450
3547
  // update first medal in queue
3451
3548
  const medal = medalsDisplayQueue[0];
3452
- const time = timeReal - medalsDisplayTimer;
3453
- if (!medalsDisplayTimer)
3454
- medalsDisplayTimer = timeReal;
3549
+ const time = timeReal - medalsDisplayTimeLast;
3550
+ if (!medalsDisplayTimeLast)
3551
+ medalsDisplayTimeLast = timeReal;
3455
3552
  else if (time > medalDisplayTime)
3456
- medalsDisplayQueue.shift(medalsDisplayTimer = 0);
3553
+ medalsDisplayQueue.shift(medalsDisplayTimeLast = 0);
3457
3554
  else
3458
3555
  {
3459
3556
  // slide on/off medals
@@ -3467,14 +3564,19 @@ function medalsRender()
3467
3564
 
3468
3565
  ///////////////////////////////////////////////////////////////////////////////
3469
3566
 
3470
- /** Newgrounds API wrapper object */
3567
+ /**
3568
+ * Newgrounds API wrapper object
3569
+ * @example
3570
+ * // create a newgrounds object, replace the app id and cipher with your own
3571
+ * const app_id = '53123:1ZuSTQ9l';
3572
+ * const cipher = 'enF0vGH@Mj/FRASKL23Q==';
3573
+ * newgrounds = new Newgrounds(app_id, cipher);
3574
+ */
3471
3575
  class Newgrounds
3472
3576
  {
3473
- /**
3474
- * Create a newgrounds object
3475
- * @param {Number} app_id - The newgrounds App ID
3476
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
3477
- */
3577
+ /** Create a newgrounds object
3578
+ * @param {Number} app_id - The newgrounds App ID
3579
+ * @param {String} [cipher] - The encryption Key (AES-128/Base64) */
3478
3580
  constructor(app_id, cipher)
3479
3581
  {
3480
3582
  ASSERT(!newgrounds && app_id);
@@ -3502,7 +3604,6 @@ class Newgrounds
3502
3604
  if (medal)
3503
3605
  {
3504
3606
  // copy newgrounds medal data
3505
- medal.image = new Image();
3506
3607
  medal.image.src = newgroundsMedal['icon'];
3507
3608
  medal.name = newgroundsMedal['name'];
3508
3609
  medal.description = newgroundsMedal['description'];
@@ -3526,7 +3627,7 @@ class Newgrounds
3526
3627
  unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, 1); }
3527
3628
 
3528
3629
  /** Send message to post score
3529
- * @param {Number} id - The scoreboard id
3630
+ * @param {Number} id - The scoreboard id
3530
3631
  * @param {Number} value - The score value */
3531
3632
  postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, 1); }
3532
3633
 
@@ -3534,21 +3635,21 @@ class Newgrounds
3534
3635
  logView() { return this.call('App.logView', {'host':this.host}, 1); }
3535
3636
 
3536
3637
  /** Get scores from a scoreboard
3537
- * @param {Number} id - The scoreboard id
3538
- * @param {String} [user=0] - A user's id or name
3638
+ * @param {Number} id - The scoreboard id
3639
+ * @param {String} [user=0] - A user's id or name
3539
3640
  * @param {Number} [social=0] - If true, only social scores will be loaded
3540
- * @param {Number} [skip=0] - Number of scores to skip before start
3641
+ * @param {Number} [skip=0] - Number of scores to skip before start
3541
3642
  * @param {Number} [limit=10] - Number of scores to include in the list
3542
- * @return {Object} - The response JSON object
3643
+ * @return {Object} - The response JSON object
3543
3644
  */
3544
3645
  getScores(id, user=0, social=0, skip=0, limit=10)
3545
3646
  { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
3546
3647
 
3547
3648
  /** Send a message to call a component of the Newgrounds API
3548
- * @param {String} component - Name of the component
3649
+ * @param {String} component - Name of the component
3549
3650
  * @param {Object} [parameters=0] - Parameters to use for call
3550
- * @param {Boolean} [async=0] - If true, wait for response before continuing (will cause stall)
3551
- * @return {Object} - The response JSON object
3651
+ * @param {Boolean} [async=0] - If true, don't wait for response before continuing (avoid stall)
3652
+ * @return {Object} - The response JSON object
3552
3653
  */
3553
3654
  call(component, parameters=0, async=0)
3554
3655
  {
@@ -3592,14 +3693,14 @@ class Newgrounds
3592
3693
 
3593
3694
  const CryptoJS=()=>eval(Function("[M='GBMGXz^oVYPPKKbB`agTXU|LxPc_ZBcMrZvCr~wyGfWrwk@ATqlqeTp^N?p{we}jIpEnB_sEr`l?YDkDhWhprc|Er|XETG?pTl`e}dIc[_N~}fzRycIfpW{HTolvoPB_FMe_eH~BTMx]yyOhv?biWPCGc]kABencBhgERHGf{OL`Dj`c^sh@canhy[secghiyotcdOWgO{tJIE^JtdGQRNSCrwKYciZOa]Y@tcRATYKzv|sXpboHcbCBf`}SKeXPFM|RiJsSNaIb]QPc[D]Jy_O^XkOVTZep`ONmntLL`Qz~UupHBX_Ia~WX]yTRJIxG`ioZ{fefLJFhdyYoyLPvqgH?b`[TMnTwwfzDXhfM?rKs^aFr|nyBdPmVHTtAjXoYUloEziWDCw_suyYT~lSMksI~ZNCS[Bex~j]Vz?kx`gdYSEMCsHpjbyxQvw|XxX_^nQYue{sBzVWQKYndtYQMWRef{bOHSfQhiNdtR{o?cUAHQAABThwHPT}F{VvFmgN`E@FiFYS`UJmpQNM`X|tPKHlccT}z}k{sACHL?Rt@MkWplxO`ASgh?hBsuuP|xD~LSH~KBlRs]t|l|_tQAroDRqWS^SEr[sYdPB}TAROtW{mIkE|dWOuLgLmJrucGLpebrAFKWjikTUzS|j}M}szasKOmrjy[?hpwnEfX[jGpLt@^v_eNwSQHNwtOtDgWD{rk|UgASs@mziIXrsHN_|hZuxXlPJOsA^^?QY^yGoCBx{ekLuZzRqQZdsNSx@ezDAn{XNj@fRXIwrDX?{ZQHwTEfu@GhxDOykqts|n{jOeZ@c`dvTY?e^]ATvWpb?SVyg]GC?SlzteilZJAL]mlhLjYZazY__qcVFYvt@|bIQnSno@OXyt]OulzkWqH`rYFWrwGs`v|~XeTsIssLrbmHZCYHiJrX}eEzSssH}]l]IhPQhPoQ}rCXLyhFIT[clhzYOvyHqigxmjz`phKUU^TPf[GRAIhNqSOdayFP@FmKmuIzMOeoqdpxyCOwCthcLq?n`L`tLIBboNn~uXeFcPE{C~mC`h]jUUUQe^`UqvzCutYCgct|SBrAeiYQW?X~KzCz}guXbsUw?pLsg@hDArw?KeJD[BN?GD@wgFWCiHq@Ypp_QKFixEKWqRp]oJFuVIEvjDcTFu~Zz]a{IcXhWuIdMQjJ]lwmGQ|]g~c]Hl]pl`Pd^?loIcsoNir_kikBYyg?NarXZEGYspt_vLBIoj}LI[uBFvm}tbqvC|xyR~a{kob|HlctZslTGtPDhBKsNsoZPuH`U`Fqg{gKnGSHVLJ^O`zmNgMn~{rsQuoymw^JY?iUBvw_~mMr|GrPHTERS[MiNpY[Mm{ggHpzRaJaoFomtdaQ_?xuTRm}@KjU~RtPsAdxa|uHmy}n^i||FVL[eQAPrWfLm^ndczgF~Nk~aplQvTUpHvnTya]kOenZlLAQIm{lPl@CCTchvCF[fI{^zPkeYZTiamoEcKmBMfZhk_j_~Fjp|wPVZlkh_nHu]@tP|hS@^G^PdsQ~f[RqgTDqezxNFcaO}HZhb|MMiNSYSAnQWCDJukT~e|OTgc}sf[cnr?fyzTa|EwEtRG|I~|IO}O]S|rp]CQ}}DWhSjC_|z|oY|FYl@WkCOoPuWuqr{fJu?Brs^_EBI[@_OCKs}?]O`jnDiXBvaIWhhMAQDNb{U`bqVR}oqVAvR@AZHEBY@depD]OLh`kf^UsHhzKT}CS}HQKy}Q~AeMydXPQztWSSzDnghULQgMAmbWIZ|lWWeEXrE^EeNoZApooEmrXe{NAnoDf`m}UNlRdqQ@jOc~HLOMWs]IDqJHYoMziEedGBPOxOb?[X`KxkFRg@`mgFYnP{hSaxwZfBQqTm}_?RSEaQga]w[vxc]hMne}VfSlqUeMo_iqmd`ilnJXnhdj^EEFifvZyxYFRf^VaqBhLyrGlk~qowqzHOBlOwtx?i{m~`n^G?Yxzxux}b{LSlx]dS~thO^lYE}bzKmUEzwW^{rPGhbEov[Plv??xtyKJshbG`KuO?hjBdS@Ru}iGpvFXJRrvOlrKN?`I_n_tplk}kgwSXuKylXbRQ]]?a|{xiT[li?k]CJpwy^o@ebyGQrPfF`aszGKp]baIx~H?ElETtFh]dz[OjGl@C?]VDhr}OE@V]wLTc[WErXacM{We`F|utKKjgllAxvsVYBZ@HcuMgLboFHVZmi}eIXAIFhS@A@FGRbjeoJWZ_NKd^oEH`qgy`q[Tq{x?LRP|GfBFFJV|fgZs`MLbpPYUdIV^]mD@FG]pYAT^A^RNCcXVrPsgk{jTrAIQPs_`mD}rOqAZA[}RETFz]WkXFTz_m{N@{W@_fPKZLT`@aIqf|L^Mb|crNqZ{BVsijzpGPEKQQZGlApDn`ruH}cvF|iXcNqK}cxe_U~HRnKV}sCYb`D~oGvwG[Ca|UaybXea~DdD~LiIbGRxJ_VGheI{ika}KC[OZJLn^IBkPrQj_EuoFwZ}DpoBRcK]Q}?EmTv~i_Tul{bky?Iit~tgS|o}JL_VYcCQdjeJ_MfaA`FgCgc[Ii|CBHwq~nbJeYTK{e`CNstKfTKPzw{jdhp|qsZyP_FcugxCFNpKitlR~vUrx^NrSVsSTaEgnxZTmKc`R|lGJeX}ccKLsQZQhsFkeFd|ckHIVTlGMg`~uPwuHRJS_CPuN_ogXe{Ba}dO_UBhuNXby|h?JlgBIqMKx^_u{molgL[W_iavNQuOq?ap]PGB`clAicnl@k~pA?MWHEZ{HuTLsCpOxxrKlBh]FyMjLdFl|nMIvTHyGAlPogqfZ?PlvlFJvYnDQd}R@uAhtJmDfe|iJqdkYr}r@mEjjIetDl_I`TELfoR|qTBu@Tic[BaXjP?dCS~MUK[HPRI}OUOwAaf|_}HZzrwXvbnNgltjTwkBE~MztTQhtRSWoQHajMoVyBBA`kdgK~h`o[J`dm~pm]tk@i`[F~F]DBlJKklrkR]SNw@{aG~Vhl`KINsQkOy?WhcqUMTGDOM_]bUjVd|Yh_KUCCgIJ|LDIGZCPls{RzbVWVLEhHvWBzKq|^N?DyJB|__aCUjoEgsARki}j@DQXS`RNU|DJ^a~d{sh_Iu{ONcUtSrGWW@cvUjefHHi}eSSGrNtO?cTPBShLqzwMVjWQQCCFB^culBjZHEK_{dO~Q`YhJYFn]jq~XSnG@[lQr]eKrjXpG~L^h~tDgEma^AUFThlaR{xyuP@[^VFwXSeUbVetufa@dX]CLyAnDV@Bs[DnpeghJw^?UIana}r_CKGDySoRudklbgio}kIDpA@McDoPK?iYcG?_zOmnWfJp}a[JLR[stXMo?_^Ng[whQlrDbrawZeSZ~SJstIObdDSfAA{MV}?gNunLOnbMv_~KFQUAjIMj^GkoGxuYtYbGDImEYiwEMyTpMxN_LSnSMdl{bg@dtAnAMvhDTBR_FxoQgANniRqxd`pWv@rFJ|mWNWmh[GMJz_Nq`BIN@KsjMPASXORcdHjf~rJfgZYe_uulzqM_KdPlMsuvU^YJuLtofPhGonVOQxCMuXliNvJIaoC?hSxcxKVVxWlNs^ENDvCtSmO~WxI[itnjs^RDvI@KqG}YekaSbTaB]ki]XM@[ZnDAP~@|BzLRgOzmjmPkRE@_sobkT|SszXK[rZN?F]Z_u}Yue^[BZgLtR}FHzWyxWEX^wXC]MJmiVbQuBzkgRcKGUhOvUc_bga|Tx`KEM`JWEgTpFYVeXLCm|mctZR@uKTDeUONPozBeIkrY`cz]]~WPGMUf`MNUGHDbxZuO{gmsKYkAGRPqjc|_FtblEOwy}dnwCHo]PJhN~JoteaJ?dmYZeB^Xd?X^pOKDbOMF@Ugg^hETLdhwlA}PL@_ur|o{VZosP?ntJ_kG][g{Zq`Tu]dzQlSWiKfnxDnk}KOzp~tdFstMobmy[oPYjyOtUzMWdjcNSUAjRuqhLS@AwB^{BFnqjCmmlk?jpn}TksS{KcKkDboXiwK]qMVjm~V`LgWhjS^nLGwfhAYrjDSBL_{cRus~{?xar_xqPlArrYFd?pHKdMEZzzjJpfC?Hv}mAuIDkyBxFpxhstTx`IO{rp}XGuQ]VtbHerlRc_LFGWK[XluFcNGUtDYMZny[M^nVKVeMllQI[xtvwQnXFlWYqxZZFp_|]^oWX[{pOMpxXxvkbyJA[DrPzwD|LW|QcV{Nw~U^dgguSpG]ClmO@j_TENIGjPWwgdVbHganhM?ema|dBaqla|WBd`poj~klxaasKxGG^xbWquAl~_lKWxUkDFagMnE{zHug{b`A~IYcQYBF_E}wiA}K@yxWHrZ{[d~|ARsYsjeNWzkMs~IOqqp[yzDE|WFrivsidTcnbHFRoW@XpAV`lv_zj?B~tPCppRjgbbDTALeFaOf?VcjnKTQMLyp{NwdylHCqmo?oelhjWuXj~}{fpuX`fra?GNkDiChYgVSh{R[BgF~eQa^WVz}ATI_CpY?g_diae]|ijH`TyNIF}|D_xpmBq_JpKih{Ba|sWzhnAoyraiDvk`h{qbBfsylBGmRH}DRPdryEsSaKS~tIaeF[s]I~xxHVrcNe@Jjxa@jlhZueLQqHh_]twVMqG_EGuwyab{nxOF?`HCle}nBZzlTQjkLmoXbXhOtBglFoMz?eqre`HiE@vNwBulglmQjj]DB@pPkPUgA^sjOAUNdSu_`oAzar?n?eMnw{{hYmslYi[TnlJD'",...']charCodeAtUinyxpf',"for(;e<10359;c[e++]=p-=128,A=A?p-A&&A:p==34&&p)for(p=1;p<128;y=f.map((n,x)=>(U=r[n]*2+1,U=Math.log(U/(h-U)),t-=a[x]*U,U/500)),t=~-h/(1+Math.exp(t))|1,i=o%h<t,o=o%h+(i?t:h-t)*(o>>17)-!i*t,f.map((n,x)=>(U=r[n]+=(i*h/2-r[n]<<13)/((C[n]+=C[n]<5)+1/20)>>13,a[x]+=y[x]*(i-t/h))),p=p*2+i)for(f='010202103203210431053105410642065206541'.split(t=0).map((n,x)=>(U=0,[...n].map((n,x)=>(U=U*997+(c[e-n]|0)|0)),h*32-1&U*997+p+!!A*129)*12+x);o<h*32;o=o*64|M.charCodeAt(d++)&63);for(C=String.fromCharCode(...c);r=/[\0-#?@\\\\~]/.exec(C);)with(C.split(r))C=join(shift());return C")([],[],1<<17,[0,0,0,0,0,0,0,0,0,0,0,0],new Uint16Array(51e6).fill(1<<15),new Uint8Array(51e6),0,0,0,0));
3594
3695
  /**
3595
- * LittleJS WebGL Interface
3596
- * <br> - All webgl used by the engine is wrapped up here
3597
- * <br> - Can be disabled with glEnable to revert to 2D canvas rendering
3598
- * <br> - Batches sprite rendering on GPU for incredibly fast performance
3599
- * <br> - Sprite transform math is done in the shader where possible
3600
- * <br> - For normal stuff you won't need to call any functions in this file
3601
- * <br> - For advanced stuff there are helper functions to create shaders, textures, etc
3602
- * @namespace WebGL
3696
+ * LittleJS WebGL Interface
3697
+ * <br> - All webgl used by the engine is wrapped up here
3698
+ * <br> - For normal stuff you won't need to see or call anything in this file
3699
+ * <br> - For advanced stuff there are helper functions to create shaders, textures, etc
3700
+ * <br> - Can be disabled with glEnable to revert to 2D canvas rendering
3701
+ * <br> - Batches sprite rendering on GPU for incredibly fast performance
3702
+ * <br> - Sprite transform math is done in the shader where possible
3703
+ * @namespace WebGL
3603
3704
  */
3604
3705
 
3605
3706
  'use strict';
@@ -3620,7 +3721,7 @@ let glContext;
3620
3721
  let glTileTexture;
3621
3722
 
3622
3723
  // WebGL internal variables not exposed to documentation
3623
- let glActiveTexture, glShader, glPositionData, glColorData, glBatchCount, glDirty, glAdditive;
3724
+ let glActiveTexture, glShader, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3624
3725
 
3625
3726
  ///////////////////////////////////////////////////////////////////////////////
3626
3727
 
@@ -3631,19 +3732,16 @@ function glInit()
3631
3732
 
3632
3733
  // create the canvas and tile texture
3633
3734
  glCanvas = document.createElement('canvas');
3634
- glContext = glCanvas.getContext('webgl', {antialias:!pixelated});
3735
+ glContext = glCanvas.getContext('webgl');
3736
+ glCanvas.style = styleCanvas;
3635
3737
  glTileTexture = glCreateTexture(tileImage);
3636
3738
 
3637
- if (glOverlay)
3638
- {
3639
- // firefox is much faster without copying the gl buffer so we just overlay it with some tradeoffs
3640
- document.body.appendChild(glCanvas);
3641
- glCanvas.style = mainCanvas.style.cssText;
3642
- }
3739
+ // some browsers are much faster without copying the gl buffer so we just overlay it instead
3740
+ glOverlay && document.body.appendChild(glCanvas);
3643
3741
 
3644
3742
  // setup vertex and fragment shaders
3645
3743
  glShader = glCreateProgram(
3646
- 'precision lowp float;'+ // use lowp for better performance
3744
+ 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3647
3745
  'uniform mat4 m;'+ // transform matrix
3648
3746
  'attribute float a;'+ // angle
3649
3747
  'attribute vec2 p,s,t;'+ // position, size, uv
@@ -3655,7 +3753,7 @@ function glInit()
3655
3753
  'v=t;d=c;e=b;'+ // pass stuff to fragment shader
3656
3754
  '}' // end of shader
3657
3755
  ,
3658
- 'precision lowp float;'+ // use lowp for better performance
3756
+ 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3659
3757
  'varying vec2 v;'+ // uv
3660
3758
  'varying vec4 d,e;'+ // color, additiveColor
3661
3759
  'uniform sampler2D j;'+ // texture
@@ -3671,6 +3769,7 @@ function glInit()
3671
3769
  glColorData = new Uint32Array(glVertexData);
3672
3770
 
3673
3771
  // setup the vertex data array
3772
+ let offset = glBatchCount = 0;
3674
3773
  const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3675
3774
  {
3676
3775
  const location = glContext.getAttribLocation(glShader, name);
@@ -3678,7 +3777,6 @@ function glInit()
3678
3777
  glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3679
3778
  offset += size*typeSize;
3680
3779
  }
3681
- let offset = glDirty = glBatchCount = 0;
3682
3780
  initVertexAttribArray('a', gl_FLOAT, 4, 1); // angle
3683
3781
  initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3684
3782
  initVertexAttribArray('s', gl_FLOAT, 4, 2); // size
@@ -3694,14 +3792,8 @@ function glSetBlendMode(additive)
3694
3792
  {
3695
3793
  if (!glEnable) return;
3696
3794
 
3697
- if (additive != glAdditive)
3698
- glFlush();
3699
-
3700
3795
  // setup blending
3701
3796
  glAdditive = additive;
3702
- const destBlend = additive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
3703
- glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
3704
- glContext.enable(gl_BLEND);
3705
3797
  }
3706
3798
 
3707
3799
  /** Set the WebGl texture, not normally necessary unless multiple tile sheets are used
@@ -3711,7 +3803,8 @@ function glSetBlendMode(additive)
3711
3803
  function glSetTexture(texture=glTileTexture)
3712
3804
  {
3713
3805
  if (!glEnable) return;
3714
-
3806
+
3807
+ // must flush cache with the old texture to set a new one
3715
3808
  if (texture != glActiveTexture)
3716
3809
  glFlush();
3717
3810
 
@@ -3776,36 +3869,35 @@ function glCreateBuffer(bufferType, size, usage)
3776
3869
  return buffer;
3777
3870
  }
3778
3871
 
3779
- /** Create WebGL texture from an image
3872
+ /** Create WebGL texture from an image and set the texture settings
3780
3873
  * @param {Image} image
3781
3874
  * @return {WebGLTexture}
3782
3875
  * @memberof WebGL */
3783
3876
  function glCreateTexture(image)
3784
3877
  {
3785
- if (!glEnable) return;
3878
+ if (!glEnable || !image || !image.width) return;
3786
3879
 
3787
3880
  // build the texture
3788
3881
  const texture = glContext.createTexture();
3789
3882
  glContext.bindTexture(gl_TEXTURE_2D, texture);
3790
3883
  glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
3791
-
3884
+
3792
3885
  // use point filtering for pixelated rendering
3793
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, pixelated ? gl_NEAREST : gl_LINEAR);
3794
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, pixelated ? gl_NEAREST : gl_LINEAR);
3886
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, cavasPixelated ? gl_NEAREST : gl_LINEAR);
3887
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, cavasPixelated ? gl_NEAREST : gl_LINEAR);
3795
3888
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE);
3796
3889
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE);
3797
3890
  return texture;
3798
3891
  }
3799
3892
 
3800
3893
  // called automatically by engine before render
3801
- function glPreRender(width, height)
3894
+ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3802
3895
  {
3803
3896
  if (!glEnable) return;
3804
3897
 
3805
3898
  // clear and set to same size as main canvas
3806
- glCanvas.width = width;
3807
- glCanvas.height = height;
3808
- glContext.viewport(0, 0, width, height);
3899
+ glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3900
+ glContext.clear(gl_COLOR_BUFFER_BIT);
3809
3901
 
3810
3902
  // set up the shader
3811
3903
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
@@ -3820,7 +3912,7 @@ function glPreRender(width, height)
3820
3912
  sx, 0, 0, 0,
3821
3913
  0, sy, 0, 0,
3822
3914
  1, 1, -1, 1,
3823
- -1-sx*cameraPos.x, -1-sy*cameraPos.y, 0, 0
3915
+ -1-sx*cameraX, -1-sy*cameraY, 0, 0
3824
3916
  ])
3825
3917
  );
3826
3918
  }
@@ -3831,11 +3923,16 @@ function glFlush()
3831
3923
  {
3832
3924
  if (!glEnable || !glBatchCount) return;
3833
3925
 
3926
+ const destBlend = glBatchAdditive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
3927
+ glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
3928
+ glContext.enable(gl_BLEND);
3929
+
3834
3930
  // draw all the sprites in the batch and reset the buffer
3835
3931
  glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
3836
3932
  glPositionData.subarray(0, glBatchCount * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT));
3837
3933
  glContext.drawArrays(gl_TRIANGLES, 0, glBatchCount * gl_VERTICES_PER_QUAD);
3838
3934
  glBatchCount = 0;
3935
+ glBatchAdditive = glAdditive;
3839
3936
  }
3840
3937
 
3841
3938
  /** Draw any sprites still in the buffer, copy to main canvas and clear
@@ -3844,29 +3941,38 @@ function glFlush()
3844
3941
  * @memberof WebGL */
3845
3942
  function glCopyToContext(context, forceDraw)
3846
3943
  {
3847
- if (!glEnable || !glDirty) return;
3944
+ if (!glEnable || !glBatchCount) return;
3848
3945
 
3849
3946
  glFlush();
3947
+
3948
+ // do not draw in overlay mode because the canvas is visible
3850
3949
  if (!glOverlay || forceDraw)
3851
- {
3852
- // do not draw/clear in overlay mode because the canvas is visible
3853
- context.drawImage(glCanvas, 0, glAdditive = glDirty = 0);
3854
- glContext.clear(gl_COLOR_BUFFER_BIT);
3855
- }
3950
+ context.drawImage(glCanvas, 0, 0);
3856
3951
  }
3857
3952
 
3858
- // Draw a sprite with the given parameters, used internally by draw functions
3859
- function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=0xffffffff, rgbaAdditive=0x00000000)
3953
+ /** Add a sprite to the gl draw list, used by all gl draw functions
3954
+ * @param x
3955
+ * @param y
3956
+ * @param sizeX
3957
+ * @param sizeY
3958
+ * @param angle
3959
+ * @param uv0X
3960
+ * @param uv0Y
3961
+ * @param uv1X
3962
+ * @param uv1Y
3963
+ * @param [rgba=0xffffffff]
3964
+ * @param [rgbaAdditive=0]
3965
+ * @memberof WebGL */
3966
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffffff, rgbaAdditive=0)
3860
3967
  {
3861
3968
  if (!glEnable) return;
3862
-
3863
- // flush if there is no room for more verts
3864
- if (glBatchCount == gl_MAX_BATCH)
3969
+
3970
+ // flush if there is no room for more verts or if different blend mode
3971
+ if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3865
3972
  glFlush();
3866
3973
 
3867
3974
  // setup 2 triangles to form a quad
3868
3975
  let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3869
- glDirty = 1;
3870
3976
 
3871
3977
  // vertex 0
3872
3978
  glPositionData[offset++] = angle;
@@ -3943,3 +4049,287 @@ gl_VERTICES_PER_QUAD = 6,
3943
4049
  gl_INDICIES_PER_VERT = 9,
3944
4050
  gl_MAX_BATCH = 1<<16,
3945
4051
  gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char * 4) * 2
4052
+ /*
4053
+ LittleJS - The Tiny JavaScript Game Engine That Can!
4054
+ MIT License - Copyright 2021 Frank Force
4055
+
4056
+ Engine Features
4057
+ - Object oriented system with base class engine object
4058
+ - Base class object handles update, physics, collision, rendering, etc
4059
+ - Engine helper classes and functions like Vector2, Color, and Timer
4060
+ - Super fast rendering system for tile sheets
4061
+ - Sound effects audio with zzfx and music with zzfxm
4062
+ - Input processing system with gamepad and touchscreen support
4063
+ - Tile layer rendering and collision system
4064
+ - Particle effect system
4065
+ - Medal system tracks and displays achievements
4066
+ - Debug tools and debug rendering system
4067
+ - Call engineInit() to start it up!
4068
+ */
4069
+
4070
+ 'use strict';
4071
+
4072
+ /** Name of engine */
4073
+ const engineName = 'LittleJS';
4074
+
4075
+ /** Version of engine */
4076
+ const engineVersion = '1.1.3';
4077
+
4078
+ /** Frames per second to update objects
4079
+ * @default */
4080
+ const frameRate = 60;
4081
+
4082
+ /** How many seconds each frame lasts, engine uses a fixed time step
4083
+ * @default 1/60 */
4084
+ const timeDelta = 1/frameRate;
4085
+
4086
+ /** Array containing all engine objects */
4087
+ let engineObjects = [];
4088
+
4089
+ /** Array containing only objects that are set to collide with other objects this frame (for optimization) */
4090
+ let engineObjectsCollide = [];
4091
+
4092
+ /** Current update frame, used to calculate time */
4093
+ let frame = 0;
4094
+
4095
+ /** Current engine time since start in seconds, derived from frame */
4096
+ let time = 0;
4097
+
4098
+ /** Actual clock time since start in seconds (not affected by pause or frame rate clamping) */
4099
+ let timeReal = 0;
4100
+
4101
+ /** Is the game paused? Causes time and objects to not be updated. */
4102
+ let paused = 0;
4103
+
4104
+ // Engine internal variables not exposed to documentation
4105
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, tileImageSize, tileImageFixBleed;
4106
+
4107
+ // Engine stat tracking, if showWatermark is true
4108
+ let averageFPS, drawCount;
4109
+
4110
+ // css text used for elements created by engine
4111
+ const styleBody = 'margin:0;overflow:hidden;background:#000' +
4112
+ ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
4113
+ const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
4114
+
4115
+ ///////////////////////////////////////////////////////////////////////////////
4116
+
4117
+ /** Start up LittleJS engine with your callback functions
4118
+ * @param {Function} gameInit - Called once after the engine starts up, setup the game
4119
+ * @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
4120
+ * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
4121
+ * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
4122
+ * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
4123
+ * @param {String} [tileImageSource] - Tile image to use, everything starts when the image is finished loading
4124
+ */
4125
+ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, tileImageSource)
4126
+ {
4127
+ // init engine when tiles load or fail to load
4128
+ tileImage.onerror = tileImage.onload = ()=>
4129
+ {
4130
+ // save tile image info
4131
+ tileImageFixBleed = vec2(tileFixBleedScale).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
4132
+ debug && (tileImage.onload=()=>ASSERT(1)); // tile sheet can not reloaded
4133
+
4134
+ // setup html
4135
+ document.body.style = styleBody;
4136
+ document.body.appendChild(mainCanvas = document.createElement('canvas'));
4137
+ mainContext = mainCanvas.getContext('2d');
4138
+ mainCanvas.style = styleCanvas;
4139
+
4140
+ // init stuff and start engine
4141
+ debugInit();
4142
+ glInit();
4143
+
4144
+ // create overlay canvas for hud to appear above gl canvas
4145
+ document.body.appendChild(overlayCanvas = document.createElement('canvas'));
4146
+ overlayContext = overlayCanvas.getContext('2d');
4147
+ overlayCanvas.style = styleCanvas;
4148
+
4149
+ gameInit();
4150
+ touchGamepadCreate();
4151
+ engineUpdate();
4152
+ };
4153
+
4154
+ // main update loop
4155
+ const engineUpdate = (frameTimeMS=0)=>
4156
+ {
4157
+ requestAnimationFrame(engineUpdate);
4158
+
4159
+ // update time keeping
4160
+ let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
4161
+ frameTimeLastMS = frameTimeMS;
4162
+ if (debug || showWatermark)
4163
+ averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
4164
+ if (debug)
4165
+ frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
4166
+ timeReal += frameTimeDeltaMS / 1e3;
4167
+ frameTimeBufferMS = min(frameTimeBufferMS + !paused * frameTimeDeltaMS, 50); // clamp incase of slow framerate
4168
+
4169
+ if (paused)
4170
+ {
4171
+ // do post update even when paused
4172
+ inputUpdate();
4173
+ debugUpdate();
4174
+ gameUpdatePost();
4175
+ inputUpdatePost();
4176
+ }
4177
+ else
4178
+ {
4179
+ // apply time delta smoothing, improves smoothness of framerate in some browsers
4180
+ let deltaSmooth = 0;
4181
+ if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
4182
+ {
4183
+ // force an update each frame if time is close enough (not just a fast refresh rate)
4184
+ deltaSmooth = frameTimeBufferMS;
4185
+ frameTimeBufferMS = 0;
4186
+ }
4187
+
4188
+ // update multiple frames if necessary in case of slow framerate
4189
+ for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
4190
+ {
4191
+ // update game and objects
4192
+ inputUpdate();
4193
+ gameUpdate();
4194
+ engineObjectsUpdate();
4195
+
4196
+ // do post update
4197
+ debugUpdate();
4198
+ gameUpdatePost();
4199
+ inputUpdatePost();
4200
+ }
4201
+
4202
+ // add the time smoothing back in
4203
+ frameTimeBufferMS += deltaSmooth;
4204
+ }
4205
+
4206
+ if (canvasFixedSize.x)
4207
+ {
4208
+ // clear set fixed size
4209
+ mainCanvas.width = canvasFixedSize.x;
4210
+ mainCanvas.height = canvasFixedSize.y;
4211
+
4212
+ // fit to window by adding space on top or bottom if necessary
4213
+ const aspect = innerWidth / innerHeight;
4214
+ const fixedAspect = mainCanvas.width / mainCanvas.height;
4215
+ mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
4216
+ mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
4217
+ if (glCanvas)
4218
+ {
4219
+ glCanvas.style.width = mainCanvas.style.width;
4220
+ glCanvas.style.height = mainCanvas.style.height;
4221
+ }
4222
+ }
4223
+ else
4224
+ {
4225
+ // clear and set size to same as window
4226
+ mainCanvas.width = min(innerWidth, canvasMaxSize.x);
4227
+ mainCanvas.height = min(innerHeight, canvasMaxSize.y);
4228
+ }
4229
+
4230
+ // render sort then render while removing destroyed objects
4231
+ enginePreRender();
4232
+ gameRender();
4233
+ engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
4234
+ for (const o of engineObjects)
4235
+ o.destroyed || o.render();
4236
+ gameRenderPost();
4237
+ medalsRender();
4238
+ touchGamepadRender();
4239
+ debugRender();
4240
+ glCopyToContext(mainContext);
4241
+
4242
+ if (showWatermark)
4243
+ {
4244
+ // update fps
4245
+ overlayContext.textAlign = 'right';
4246
+ overlayContext.textBaseline = 'top';
4247
+ overlayContext.font = '1em monospace';
4248
+ overlayContext.fillStyle = '#000';
4249
+ const text = engineName + ' ' + 'v' + engineVersion + ' / '
4250
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
4251
+ + ' ' + (glEnable ? 'GL' : '2D') ;
4252
+ overlayContext.fillText(text, mainCanvas.width-3, 3);
4253
+ overlayContext.fillStyle = '#fff';
4254
+ overlayContext.fillText(text, mainCanvas.width-2, 2);
4255
+ drawCount = 0;
4256
+ }
4257
+ }
4258
+
4259
+ // set tile image source to load the image and start the engine
4260
+ tileImageSource ? tileImage.src = tileImageSource : tileImage.onload();
4261
+ }
4262
+
4263
+ // called by engine to setup render system
4264
+ function enginePreRender()
4265
+ {
4266
+ // save canvas size and clear canvases
4267
+ mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
4268
+
4269
+ // disable smoothing for pixel art
4270
+ mainContext.imageSmoothingEnabled = !cavasPixelated;
4271
+
4272
+ // setup gl rendering if enabled
4273
+ glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
4274
+ }
4275
+
4276
+ ///////////////////////////////////////////////////////////////////////////////
4277
+
4278
+ /** Calls update on each engine object (recursively if child), removes destroyed objects, and updated time */
4279
+ function engineObjectsUpdate()
4280
+ {
4281
+ // get list of solid objects for physics optimzation
4282
+ engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
4283
+
4284
+ // recursive object update
4285
+ const updateObject = (o)=>
4286
+ {
4287
+ if (!o.destroyed)
4288
+ {
4289
+ o.update();
4290
+ for (const child of o.children)
4291
+ updateObject(child);
4292
+ }
4293
+ }
4294
+ for (const o of engineObjects)
4295
+ o.parent || updateObject(o);
4296
+
4297
+ // remove destroyed objects
4298
+ engineObjects = engineObjects.filter(o=>!o.destroyed);
4299
+
4300
+ // increment frame and update time
4301
+ time = ++frame / frameRate;
4302
+ }
4303
+
4304
+ /** Destroy and remove all objects */
4305
+ function engineObjectsDestroy()
4306
+ {
4307
+ for (const o of engineObjects)
4308
+ o.parent || o.destroy();
4309
+ engineObjects = engineObjects.filter(o=>!o.destroyed);
4310
+ }
4311
+
4312
+ /** Triggers a callback for each object within a given area
4313
+ * @param {Vector2} [pos] - Center of test area
4314
+ * @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
4315
+ * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
4316
+ * @param {Array} [objects=engineObjects] - List of objects to check */
4317
+ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
4318
+ {
4319
+ if (!pos) // all objects
4320
+ {
4321
+ for (const o of objects)
4322
+ callbackFunction(o);
4323
+ }
4324
+ else if (size.x != undefined) // bounding box test
4325
+ {
4326
+ for (const o of objects)
4327
+ isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
4328
+ }
4329
+ else // circle test
4330
+ {
4331
+ const sizeSquared = size*size;
4332
+ for (const o of objects)
4333
+ pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
4334
+ }
4335
+ }