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,5 +1,7 @@
1
1
  /*
2
- LittleJS - Release build include file
2
+ LittleJS - Release Build
3
+ MIT License - Copyright 2021 Frank Force
4
+
3
5
  - This file is used for release builds in place of engineDebug.js
4
6
  - Debug functionality will be disabled to lower size and increase performance
5
7
  */
@@ -26,15 +28,16 @@ const debugCircle = ()=> {}
26
28
  const debugPoint = ()=> {}
27
29
  const debugLine = ()=> {}
28
30
  const debugAABB = ()=> {}
31
+ const debugText = ()=> {}
29
32
  const debugClear = ()=> {}
30
33
  const debugSaveCanvas = ()=> {}
31
34
  /**
32
- * LittleJS Utility Classes and Functions
33
- * <br> - General purpose math library
34
- * <br> - Vector2 - fast, simple, easy 2D vector class
35
- * <br> - Color - holds a rgba color with some math functions
36
- * <br> - Timer - tracks time automatically
37
- * @namespace Utilities
35
+ * LittleJS Utility Classes and Functions
36
+ * <br> - General purpose math library
37
+ * <br> - Vector2 - fast, simple, easy 2D vector class
38
+ * <br> - Color - holds a rgba color with some math functions
39
+ * <br> - Timer - tracks time automatically
40
+ * @namespace Utilities
38
41
  */
39
42
 
40
43
  'use strict';
@@ -44,23 +47,12 @@ const debugSaveCanvas = ()=> {}
44
47
  * @memberof Utilities */
45
48
  const PI = Math.PI;
46
49
 
47
- /** True if running a Chromium based browser
48
- * @const
49
- * @memberof Utilities */
50
- const isChrome = window['chrome'];
51
-
52
50
  /** Returns absoulte value of value passed in
53
51
  * @param {Number} value
54
52
  * @return {Number}
55
53
  * @memberof Utilities */
56
54
  const abs = (a)=> a < 0 ? -a : a;
57
55
 
58
- /** Returns the sign of value passed in
59
- * @param {Number} value
60
- * @return {Number}
61
- * @memberof Utilities */
62
- const sign = (a)=> a < 0 ? -1 : 1;
63
-
64
56
  /** Returns lowest of two values passed in
65
57
  * @param {Number} valueA
66
58
  * @param {Number} valueB
@@ -75,54 +67,54 @@ const min = (a, b)=> a < b ? a : b;
75
67
  * @memberof Utilities */
76
68
  const max = (a, b)=> a > b ? a : b;
77
69
 
70
+ /** Returns the sign of value passed in (also returns 1 if 0)
71
+ * @param {Number} value
72
+ * @return {Number}
73
+ * @memberof Utilities */
74
+ const sign = (a)=> a < 0 ? -1 : 1;
75
+
78
76
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
79
77
  * @param {Number} dividend
80
- * @param {Number} divisor
78
+ * @param {Number} [divisor=1]
81
79
  * @return {Number}
82
80
  * @memberof Utilities */
83
- const mod = (a, b)=> ((a % b) + b) % b;
81
+ const mod = (a, b=1)=> ((a % b) + b) % b;
84
82
 
85
83
  /** Clamps the value beween max and min
86
84
  * @param {Number} value
87
- * @param {Number} [max=1]
88
85
  * @param {Number} [min=0]
86
+ * @param {Number} [max=1]
89
87
  * @return {Number}
90
88
  * @memberof Utilities */
91
- const clamp = (v, max=1, min=0)=> (ASSERT(max > min), v < min ? min : v > max ? max : v);
89
+ const clamp = (v, min=0, max=1)=> v < min ? min : v > max ? max : v;
92
90
 
93
91
  /** Returns what percentage the value is between max and min
94
92
  * @param {Number} value
95
- * @param {Number} [max=1]
96
93
  * @param {Number} [min=0]
94
+ * @param {Number} [max=1]
97
95
  * @return {Number}
98
96
  * @memberof Utilities */
99
- const percent = (v, max=1, min=0)=> max-min ? clamp((v-min) / (max-min)) : 0;
97
+ const percent = (v, min=0, max=1)=> max-min ? clamp((v-min) / (max-min)) : 0;
100
98
 
101
99
  /** Linearly interpolates the percent value between max and min
102
100
  * @param {Number} percent
103
- * @param {Number} [max=1]
104
101
  * @param {Number} [min=0]
102
+ * @param {Number} [max=1]
105
103
  * @return {Number}
106
104
  * @memberof Utilities */
107
- const lerp = (p, max=1, min=0)=> min + clamp(p) * (max-min);
105
+ const lerp = (p, min=0, max=1)=> min + clamp(p) * (max-min);
108
106
 
109
- /** Formats seconds to 00:00 style for display purposes
110
- * @param {Number} t - time in seconds
111
- * @return {String}
112
- * @memberof Utilities */
113
- const formatTime = (t)=> (t/60|0)+':'+(t%60<10?'0':'')+(t%60|0);
114
-
115
- /** Returns the nearest power of two not less then the value
107
+ /** Applies smoothstep function to the percentage value
116
108
  * @param {Number} value
117
109
  * @return {Number}
118
110
  * @memberof Utilities */
119
- const nearestPowerOfTwo = (v)=> 2**Math.ceil(Math.log2(v));
111
+ const smoothStep = (p)=> p * p * (3 - 2 * p);
120
112
 
121
- /** Applies smoothstep function to the percentage value
113
+ /** Returns the nearest power of two not less then the value
122
114
  * @param {Number} value
123
115
  * @return {Number}
124
116
  * @memberof Utilities */
125
- const smoothStep = (p)=> p * p * (3 - 2 * p);
117
+ const nearestPowerOfTwo = (v)=> 2**Math.ceil(Math.log2(v));
126
118
 
127
119
  /** Returns true if two axis aligned bounding boxes are overlapping
128
120
  * @param {Vector2} pointA - Center of box A
@@ -141,6 +133,12 @@ const isOverlapping = (pA, sA, pB, sB)=> abs(pA.x - pB.x)*2 < sA.x + sB.x & abs(
141
133
  * @memberof Utilities */
142
134
  const wave = (frequency=1, amplitude=1, t=time)=> amplitude/2 * (1 - Math.cos(t*frequency*2*PI));
143
135
 
136
+ /** Formats seconds to mm:ss style for display purposes
137
+ * @param {Number} t - time in seconds
138
+ * @return {String}
139
+ * @memberof Utilities */
140
+ const formatTime = (t)=> (t/60|0)+':'+(t%60<10?'0':'')+(t%60|0);
141
+
144
142
  ///////////////////////////////////////////////////////////////////////////////
145
143
 
146
144
  /** Random global functions
@@ -163,7 +161,7 @@ const randInt = (a=1, b=0)=> rand(a,b)|0;
163
161
  /** Randomly returns either -1 or 1
164
162
  * @return {Number}
165
163
  * @memberof Random */
166
- const randSign = ()=> (rand(2)|0)*2-1;
164
+ const randSign = ()=> (rand(2)|0) * 2 - 1;
167
165
 
168
166
  /** Returns a random Vector2 within a circular shape
169
167
  * @param {Number} [radius=1]
@@ -199,25 +197,46 @@ let randSeed = 1;
199
197
  const randSeeded = (a=1, b=0)=>
200
198
  {
201
199
  randSeed ^= randSeed << 13; randSeed ^= randSeed >>> 17; randSeed ^= randSeed << 5; // xorshift
202
- return b + (a-b)*abs(randSeed % 1e9)/1e9;
200
+ return b + (a-b) * abs(randSeed % 1e9) / 1e9;
203
201
  }
204
202
 
205
203
  ///////////////////////////////////////////////////////////////////////////////
206
204
 
207
- /** Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
208
- * @param {Number} [x=0]
209
- * @param {Number} [y=0]
210
- * @return {Vector2}
211
- * @memberof Utilities */
205
+ /**
206
+ * Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
207
+ * @param {Number} [x=0]
208
+ * @param {Number} [y=0]
209
+ * @return {Vector2}
210
+ * @example
211
+ * let a = vec2(0, 1); // vector with coordinates (0, 1)
212
+ * let b = vec2(a); // copy a into b
213
+ * a = vec2(5); // set a to (5, 5)
214
+ * b = vec2(); // set b to (0, 0)
215
+ * @memberof Utilities
216
+ */
212
217
  const vec2 = (x=0, y)=> x.x == undefined ? new Vector2(x, y == undefined? x : y) : new Vector2(x.x, x.y);
213
218
 
214
- /** 2D Vector object with vector math library */
219
+ /**
220
+ * 2D Vector object with vector math library
221
+ * <br> - Functions do not change this so they can be chained together
222
+ * @example
223
+ * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
224
+ * let b = new Vector2; // vector with coordinates (0, 0)
225
+ * let c = vec2(4, 2); // use the vec2 function to make a Vector2
226
+ * let d = a.add(b).scale(5); // operators can be chained
227
+ */
215
228
  class Vector2
216
229
  {
217
230
  /** Create a 2D vector with the x and y passed in, can also be created with vec2()
218
- * @param {Number} [x=0] - x axis position
219
- * @param {Number} [y=0] - y axis position */
220
- constructor(x=0, y=0) { this.x = x; this.y = y; }
231
+ * @param {Number} [x=0] - X axis location
232
+ * @param {Number} [y=0] - Y axis location */
233
+ constructor(x=0, y=0)
234
+ {
235
+ /** @property {Number} - X axis location */
236
+ this.x = x;
237
+ /** @property {Number} - Y axis location */
238
+ this.y = y;
239
+ }
221
240
 
222
241
  /** Returns a new vector that is a copy of this
223
242
  * @return {Vector2} */
@@ -308,10 +327,6 @@ class Vector2
308
327
  * @return {Vector2} */
309
328
  invert() { return new Vector2(this.y, -this.x); }
310
329
 
311
- /** Returns a copy of this vector with the axies flipped
312
- * @return {Vector2} */
313
- flip() { return new Vector2(this.y, this.x); }
314
-
315
330
  /** Returns a copy of this vector with each axis floored
316
331
  * @return {Vector2} */
317
332
  floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
@@ -334,15 +349,31 @@ class Vector2
334
349
 
335
350
  ///////////////////////////////////////////////////////////////////////////////
336
351
 
337
- /** Color object (red, green, blue, alpha) with some helpful functions */
352
+ /**
353
+ * Color object (red, green, blue, alpha) with some helpful functions
354
+ * @example
355
+ * let a = new Color; // white
356
+ * let b = new Color(1, 0, 0); // red
357
+ * let c = new Color(0, 0, 0, 0); // transparent black
358
+ */
338
359
  class Color
339
360
  {
340
361
  /** Create a color with the components passed in, white by default
341
- * @param {Number} [r=1] - red
342
- * @param {Number} [g=1] - green
343
- * @param {Number} [b=1] - blue
344
- * @param {Number} [a=1] - alpha */
345
- constructor(r=1, g=1, b=1, a=1) { this.r=r; this.g=g; this.b=b; this.a=a; }
362
+ * @param {Number} [red=1]
363
+ * @param {Number} [green=1]
364
+ * @param {Number} [blue=1]
365
+ * @param {Number} [alpha=1] */
366
+ constructor(r=1, g=1, b=1, a=1)
367
+ {
368
+ /** @property {Number} - Red */
369
+ this.r = r;
370
+ /** @property {Number} - Green */
371
+ this.g = g;
372
+ /** @property {Number} - Blue */
373
+ this.b = b;
374
+ /** @property {Number} - Alpha */
375
+ this.a = a;
376
+ }
346
377
 
347
378
  /** Returns a new color that is a copy of this
348
379
  * @return {Color} */
@@ -439,7 +470,15 @@ class Color
439
470
 
440
471
  ///////////////////////////////////////////////////////////////////////////////
441
472
 
442
- /** Timer object tracks how long has passed since it was set */
473
+ /**
474
+ * Timer object tracks how long has passed since it was set
475
+ * @example
476
+ * let a = new Timer; // creates a timer that is not set
477
+ * a.set(3); // sets the timer to 3 seconds
478
+ *
479
+ * let b = new Timer(1); // creates a timer with 1 second left
480
+ * b.unset(); // unsets the timer
481
+ */
443
482
  class Timer
444
483
  {
445
484
  /** Create a timer object set time passed in
@@ -465,17 +504,17 @@ class Timer
465
504
  * @return {Boolean} */
466
505
  elapsed() { return time > this.time; }
467
506
 
468
- /** Get how long since elapsed, returns 0 if not set
507
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
469
508
  * @return {Number} */
470
509
  get() { return this.isSet()? time - this.time : 0; }
471
510
 
472
511
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
473
512
  * @return {Number} */
474
- getPercent() { return this.isSet()? percent(this.time - time, 0, this.setTime) : 0; }
513
+ getPercent() { return this.isSet()? percent(this.time - time, this.setTime, 0) : 0; }
475
514
  }
476
515
  /**
477
- * LittleJS Engine Settings
478
- * @namespace Settings
516
+ * LittleJS Engine Settings
517
+ * @namespace Settings
479
518
  */
480
519
 
481
520
  'use strict';
@@ -483,35 +522,27 @@ class Timer
483
522
  ///////////////////////////////////////////////////////////////////////////////
484
523
  // Display settings
485
524
 
486
- /** The max width of the canvas, centered if window is larger
487
- * @default
488
- * @memberof Settings */
489
- const maxWidth = 1920;
490
-
491
- /** The max height of the canvas, centered if window is larger
492
- * @default
493
- * @memberof Settings */
494
- const maxHeight = 1200; // up to 1080p and 16:10
495
-
496
- /** Fixed witdh, if enabled cavnvas size never changes
525
+ /** The max size of the canvas, centered if window is larger
526
+ * @type {Vector2}
497
527
  * @default
498
528
  * @memberof Settings */
499
- let fixedWidth = 0;
529
+ let canvasMaxSize = vec2(1920, 1200);
500
530
 
501
- /** Fixed height, if enabled cavnvas size never changes
531
+ /** Fixed size of the canvas, if enabled cavnvas size never changes
532
+ * @type {Vector2}
502
533
  * @default
503
534
  * @memberof Settings */
504
- let fixedHeight = 0;
535
+ let canvasFixedSize = vec2();
505
536
 
506
- /** Fit to canvas to window by adding space on top or bottom if necessary
537
+ /** Disables anti aliasing for pixel art if true
507
538
  * @default
508
539
  * @memberof Settings */
509
- let fixedFitToWindow = 1;
540
+ let cavasPixelated = 1;
510
541
 
511
542
  /** Default font used for text rendering
512
543
  * @default
513
544
  * @memberof Settings */
514
- let defaultFont = 'arial';
545
+ let fontDefault = 'arial';
515
546
 
516
547
  ///////////////////////////////////////////////////////////////////////////////
517
548
  // Tile sheet settings
@@ -520,17 +551,12 @@ let defaultFont = 'arial';
520
551
  * @type {Vector2}
521
552
  * @default
522
553
  * @memberof Settings */
523
- const defaultTileSize = vec2(16);
554
+ let tileSizeDefault = vec2(16);
524
555
 
525
556
  /** Prevent tile bleeding from neighbors in pixels
526
557
  * @default
527
558
  * @memberof Settings */
528
- const tileBleedShrinkFix = .3;
529
-
530
- /** Use crisp pixels for pixel art if true
531
- * @default
532
- * @memberof Settings */
533
- let pixelated = 1;
559
+ let tileFixBleedScale = .3;
534
560
 
535
561
  ///////////////////////////////////////////////////////////////////////////////
536
562
  // Object settings
@@ -539,43 +565,48 @@ let pixelated = 1;
539
565
  * @type {Vector2}
540
566
  * @default
541
567
  * @memberof Settings */
542
- const defaultObjectSize = vec2(1);
568
+ let objectDefaultSize = vec2(1);
543
569
 
544
570
  /** Default object mass for collison calcuations (how heavy objects are)
545
571
  * @default
546
572
  * @memberof Settings */
547
- const defaultObjectMass = 1;
573
+ let objectDefaultMass = 1;
548
574
 
549
575
  /** How much to slow velocity by each frame (0-1)
550
576
  * @default
551
577
  * @memberof Settings */
552
- const defaultObjectDamping = .99;
578
+ let objectDefaultDamping = .99;
553
579
 
554
580
  /** How much to slow angular velocity each frame (0-1)
555
581
  * @default
556
582
  * @memberof Settings */
557
- const defaultObjectAngleDamping = .99;
583
+ let objectDefaultAngleDamping = .99;
558
584
 
559
585
  /** How much to bounce when a collision occurs (0-1)
560
586
  * @default
561
587
  * @memberof Settings */
562
- const defaultObjectElasticity = 0;
588
+ let objectDefaultElasticity = 0;
563
589
 
564
590
  /** How much to slow when touching (0-1)
565
591
  * @default
566
592
  * @memberof Settings */
567
- const defaultObjectFriction = .8;
593
+ let objectDefaultFriction = .8;
568
594
 
569
595
  /** Clamp max speed to avoid fast objects missing collisions
570
596
  * @default
571
597
  * @memberof Settings */
572
- const maxObjectSpeed = 1;
598
+ let objectMaxSpeed = 1;
573
599
 
574
600
  /** How much gravity to apply to objects along the Y axis, negative is down
575
601
  * @default
576
602
  * @memberof Settings */
577
603
  let gravity = 0;
578
604
 
605
+ /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
606
+ * @default
607
+ * @memberof Settings */
608
+ let particleEmitRateScale = 1;
609
+
579
610
  ///////////////////////////////////////////////////////////////////////////////
580
611
  // Camera settings
581
612
 
@@ -588,7 +619,7 @@ let cameraPos = vec2();
588
619
  /** Scale of camera in world space
589
620
  * @default
590
621
  * @memberof Settings */
591
- let cameraScale = max(defaultTileSize.x, defaultTileSize.y);
622
+ let cameraScale = max(tileSizeDefault.x, tileSizeDefault.y);
592
623
 
593
624
  ///////////////////////////////////////////////////////////////////////////////
594
625
  // WebGL settings
@@ -596,7 +627,7 @@ let cameraScale = max(defaultTileSize.x, defaultTileSize.y);
596
627
  /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
597
628
  * @default
598
629
  * @memberof Settings */
599
- const glEnable = 1;
630
+ let glEnable = 1;
600
631
 
601
632
  /** Fixes slow rendering in some browsers by not compositing the WebGL canvas
602
633
  * @default
@@ -609,45 +640,62 @@ let glOverlay = 1;
609
640
  /** Should gamepads be allowed
610
641
  * @default
611
642
  * @memberof Settings */
612
- const gamepadsEnable = 1;
643
+ let gamepadsEnable = 1;
644
+
645
+ /** If true, the dpad input is also routed to the left analog stick (for better accessability)
646
+ * @default
647
+ * @memberof Settings */
648
+ let gamepadDirectionEmulateStick = 1;
649
+
650
+ /** If true the WASD keys are also routed to the direction keys (for better accessability)
651
+ * @default
652
+ * @memberof Settings */
653
+ let inputWASDEmulateDirection = 1;
654
+
655
+ /** True if touch gamepad should appear on mobile devices
656
+ * <br> - Supports left analog stick, 4 face buttons and start button (button 9)
657
+ * <br> - Must be set by end of gameInit to be activated
658
+ * @default
659
+ * @memberof Settings */
660
+ let touchGamepadEnable = 0;
613
661
 
614
- /** If true touch input is routed to mouse functions
662
+ /** Size of virutal gamepad for touch devices in pixels
615
663
  * @default
616
664
  * @memberof Settings */
617
- const touchInputEnable = 1;
665
+ let touchGamepadSize = 80;
618
666
 
619
- /** Allow players to use dpad as analog stick
667
+ /** Transparency of touch gamepad overlay
620
668
  * @default
621
669
  * @memberof Settings */
622
- const copyGamepadDirectionToStick = 1;
670
+ let touchGamepadAlpha = .3;
623
671
 
624
- /** allow players to use WASD as direction keys
672
+ /** Allow vibration hardware if it exists
625
673
  * @default
626
674
  * @memberof Settings */
627
- const copyWASDToDpad = 1;
675
+ let vibrateEnable = 1;
628
676
 
629
677
  ///////////////////////////////////////////////////////////////////////////////
630
678
  // Audio settings
631
679
 
632
- /** All audio code can be disabled and removed from build
680
+ /** Volume scale to apply to all sound, music and speech
633
681
  * @default
634
682
  * @memberof Settings */
635
- const soundEnable = 1;
683
+ let soundVolume = .5;
636
684
 
637
- /** Volume scale to apply to all sound, music and speech
685
+ /** All audio code can be disabled and removed from build
638
686
  * @default
639
687
  * @memberof Settings */
640
- let audioVolume = .5;
688
+ let soundEnable = 1;
641
689
 
642
690
  /** Default range where sound no longer plays
643
691
  * @default
644
692
  * @memberof Settings */
645
- const defaultSoundRange = 30;
693
+ let soundDefaultRange = 30;
646
694
 
647
695
  /** Default range percent to start tapering off sound (0-1)
648
696
  * @default
649
697
  * @memberof Settings */
650
- const defaultSoundTaper = .7;
698
+ let soundDefaultTaper = .7;
651
699
 
652
700
  ///////////////////////////////////////////////////////////////////////////////
653
701
  // Medals settings
@@ -655,27 +703,27 @@ const defaultSoundTaper = .7;
655
703
  /** How long to show medals for in seconds
656
704
  * @default
657
705
  * @memberof Settings */
658
- const medalDisplayTime = 5;
706
+ let medalDisplayTime = 5;
659
707
 
660
708
  /** How quickly to slide on/off medals in seconds
661
709
  * @default
662
710
  * @memberof Settings */
663
- const medalDisplaySlideTime = .5;
711
+ let medalDisplaySlideTime = .5;
664
712
 
665
713
  /** Width of medal display
666
714
  * @default
667
715
  * @memberof Settings */
668
- const medalDisplayWidth = 640;
716
+ let medalDisplayWidth = 640;
669
717
 
670
718
  /** Height of medal display
671
719
  * @default
672
720
  * @memberof Settings */
673
- const medalDisplayHeight = 99;
721
+ let medalDisplayHeight = 80;
674
722
 
675
723
  /** Size of icon in medal display
676
724
  * @default
677
725
  * @memberof Settings */
678
- const medalDisplayIconSize = 80;
726
+ let medalDisplayIconSize = 50;
679
727
  /*
680
728
  LittleJS - The Tiny JavaScript Game Engine That Can!
681
729
  MIT License - Copyright 2021 Frank Force
@@ -700,21 +748,21 @@ const medalDisplayIconSize = 80;
700
748
  const engineName = 'LittleJS';
701
749
 
702
750
  /** Version of engine */
703
- const engineVersion = '1.1.2';
751
+ const engineVersion = '1.1.3';
704
752
 
705
753
  /** Frames per second to update objects
706
754
  * @default */
707
- const FPS = 60;
755
+ const frameRate = 60;
708
756
 
709
757
  /** How many seconds each frame lasts, engine uses a fixed time step
710
758
  * @default 1/60 */
711
- const timeDelta = 1/FPS;
759
+ const timeDelta = 1/frameRate;
712
760
 
713
761
  /** Array containing all engine objects */
714
762
  let engineObjects = [];
715
763
 
716
- /** Array containing only objects that are set to collide with other objects (for optimization) */
717
- let engineCollideObjects = [];
764
+ /** Array containing only objects that are set to collide with other objects this frame (for optimization) */
765
+ let engineObjectsCollide = [];
718
766
 
719
767
  /** Current update frame, used to calculate time */
720
768
  let frame = 0;
@@ -729,36 +777,40 @@ let timeReal = 0;
729
777
  let paused = 0;
730
778
 
731
779
  // Engine internal variables not exposed to documentation
732
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, debugFPS = 0,
733
- shrinkTilesX, shrinkTilesY, drawCount, tileImageSize, tileImageSizeInverse;
780
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, tileImageSize, tileImageFixBleed;
781
+
782
+ // Engine stat tracking, if showWatermark is true
783
+ let averageFPS, drawCount;
784
+
785
+ // css text used for elements created by engine
786
+ const styleBody = 'margin:0;overflow:hidden;background:#000' +
787
+ ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
788
+ const styleCanvas = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
734
789
 
735
790
  ///////////////////////////////////////////////////////////////////////////////
736
791
 
737
792
  /** Start up LittleJS engine with your callback functions
738
- * @param {Function} gameInit - Called once after the engine starts up, setup the game
739
- * @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
740
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
741
- * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
742
- * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
743
- * @param {String} tileImageSource - Tile image to use, everything starts when the image is finished loading
793
+ * @param {Function} gameInit - Called once after the engine starts up, setup the game
794
+ * @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
795
+ * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
796
+ * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
797
+ * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
798
+ * @param {String} [tileImageSource] - Tile image to use, everything starts when the image is finished loading
744
799
  */
745
800
  function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, tileImageSource)
746
801
  {
747
- // init engine when tiles load
748
- tileImage.onload = ()=>
802
+ // init engine when tiles load or fail to load
803
+ tileImage.onerror = tileImage.onload = ()=>
749
804
  {
750
805
  // save tile image info
751
- tileImageSizeInverse = vec2(1).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
806
+ tileImageFixBleed = vec2(tileFixBleedScale).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
752
807
  debug && (tileImage.onload=()=>ASSERT(1)); // tile sheet can not reloaded
753
- shrinkTilesX = tileBleedShrinkFix/tileImageSize.x;
754
- shrinkTilesY = tileBleedShrinkFix/tileImageSize.y;
755
808
 
756
809
  // setup html
810
+ document.body.style = styleBody;
757
811
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
758
- document.body.style = 'margin:0;overflow:hidden;background:#000';
759
- mainCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)' +
760
- (pixelated ? ';image-rendering:crisp-edges;image-rendering:pixelated' : ''); // pixelated rendering
761
812
  mainContext = mainCanvas.getContext('2d');
813
+ mainCanvas.style = styleCanvas;
762
814
 
763
815
  // init stuff and start engine
764
816
  debugInit();
@@ -766,10 +818,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
766
818
 
767
819
  // create overlay canvas for hud to appear above gl canvas
768
820
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
769
- overlayCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
770
821
  overlayContext = overlayCanvas.getContext('2d');
771
-
822
+ overlayCanvas.style = styleCanvas;
823
+
772
824
  gameInit();
825
+ touchGamepadCreate();
773
826
  engineUpdate();
774
827
  };
775
828
 
@@ -782,7 +835,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
782
835
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
783
836
  frameTimeLastMS = frameTimeMS;
784
837
  if (debug || showWatermark)
785
- debugFPS = lerp(.05, 1e3/(frameTimeDeltaMS||1), debugFPS);
838
+ averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
786
839
  if (debug)
787
840
  frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
788
841
  timeReal += frameTimeDeltaMS / 1e3;
@@ -808,7 +861,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
808
861
  }
809
862
 
810
863
  // update multiple frames if necessary in case of slow framerate
811
- for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / FPS)
864
+ for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
812
865
  {
813
866
  // update game and objects
814
867
  inputUpdate();
@@ -825,45 +878,39 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
825
878
  frameTimeBufferMS += deltaSmooth;
826
879
  }
827
880
 
828
- if (fixedWidth)
881
+ if (canvasFixedSize.x)
829
882
  {
830
883
  // clear set fixed size
831
- mainCanvas.width = fixedWidth;
832
- mainCanvas.height = fixedHeight;
884
+ mainCanvas.width = canvasFixedSize.x;
885
+ mainCanvas.height = canvasFixedSize.y;
833
886
 
834
- if (fixedFitToWindow)
887
+ // fit to window by adding space on top or bottom if necessary
888
+ const aspect = innerWidth / innerHeight;
889
+ const fixedAspect = mainCanvas.width / mainCanvas.height;
890
+ mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
891
+ mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
892
+ if (glCanvas)
835
893
  {
836
- // fit to window by adding space on top or bottom if necessary
837
- const aspect = innerWidth / innerHeight;
838
- const fixedAspect = fixedWidth / fixedHeight;
839
- mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
840
- mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
841
- if (glCanvas)
842
- {
843
- glCanvas.style.width = mainCanvas.style.width;
844
- glCanvas.style.height = mainCanvas.style.height;
845
- }
894
+ glCanvas.style.width = mainCanvas.style.width;
895
+ glCanvas.style.height = mainCanvas.style.height;
846
896
  }
847
897
  }
848
898
  else
849
899
  {
850
900
  // clear and set size to same as window
851
- mainCanvas.width = min(innerWidth, maxWidth);
852
- mainCanvas.height = min(innerHeight, maxHeight);
901
+ mainCanvas.width = min(innerWidth, canvasMaxSize.x);
902
+ mainCanvas.height = min(innerHeight, canvasMaxSize.y);
853
903
  }
854
904
 
855
- // save canvas size and clear overlay canvas
856
- mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
857
- mainContext.imageSmoothingEnabled = !pixelated; // disable smoothing for pixel art
858
-
859
905
  // render sort then render while removing destroyed objects
860
- glPreRender(mainCanvas.width, mainCanvas.height);
906
+ enginePreRender();
861
907
  gameRender();
862
908
  engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
863
909
  for (const o of engineObjects)
864
910
  o.destroyed || o.render();
865
911
  gameRenderPost();
866
912
  medalsRender();
913
+ touchGamepadRender();
867
914
  debugRender();
868
915
  glCopyToContext(mainContext);
869
916
 
@@ -875,7 +922,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
875
922
  overlayContext.font = '1em monospace';
876
923
  overlayContext.fillStyle = '#000';
877
924
  const text = engineName + ' ' + 'v' + engineVersion + ' / '
878
- + drawCount + ' / ' + engineObjects.length + ' / ' + debugFPS.toFixed(1);
925
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
926
+ + ' ' + (glEnable ? 'GL' : '2D') ;
879
927
  overlayContext.fillText(text, mainCanvas.width-3, 3);
880
928
  overlayContext.fillStyle = '#fff';
881
929
  overlayContext.fillText(text, mainCanvas.width-2, 2);
@@ -887,12 +935,27 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
887
935
  tileImageSource ? tileImage.src = tileImageSource : tileImage.onload();
888
936
  }
889
937
 
938
+ // called by engine to setup render system
939
+ function enginePreRender()
940
+ {
941
+ // save canvas size and clear canvases
942
+ mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
943
+
944
+ // disable smoothing for pixel art
945
+ mainContext.imageSmoothingEnabled = !cavasPixelated;
946
+
947
+ // setup gl rendering if enabled
948
+ glPreRender(mainCanvas.width, mainCanvas.height, cameraPos.x, cameraPos.y, cameraScale);
949
+ }
890
950
 
891
951
  ///////////////////////////////////////////////////////////////////////////////
892
952
 
893
953
  /** Calls update on each engine object (recursively if child), removes destroyed objects, and updated time */
894
954
  function engineObjectsUpdate()
895
955
  {
956
+ // get list of solid objects for physics optimzation
957
+ engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
958
+
896
959
  // recursive object update
897
960
  const updateObject = (o)=>
898
961
  {
@@ -908,42 +971,38 @@ function engineObjectsUpdate()
908
971
 
909
972
  // remove destroyed objects
910
973
  engineObjects = engineObjects.filter(o=>!o.destroyed);
911
- engineCollideObjects = engineCollideObjects.filter(o=>!o.destroyed);
912
974
 
913
975
  // increment frame and update time
914
- time = ++frame / FPS;
976
+ time = ++frame / frameRate;
915
977
  }
916
978
 
917
- /** Detroy and remove all objects that are not persistent or descendants of a persistent object */
979
+ /** Destroy and remove all objects */
918
980
  function engineObjectsDestroy()
919
981
  {
920
982
  for (const o of engineObjects)
921
- o.persistent || o.parent || o.destroy();
983
+ o.parent || o.destroy();
922
984
  engineObjects = engineObjects.filter(o=>!o.destroyed);
923
985
  }
924
986
 
925
987
  /** Triggers a callback for each object within a given area
926
- * @param {Vector2} [pos] - Center of test area
927
- * @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
928
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
988
+ * @param {Vector2} [pos] - Center of test area
989
+ * @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
990
+ * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
929
991
  * @param {Array} [objects=engineObjects] - List of objects to check */
930
992
  function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
931
993
  {
932
- if (!pos)
994
+ if (!pos) // all objects
933
995
  {
934
- // all objects
935
996
  for (const o of objects)
936
997
  callbackFunction(o);
937
998
  }
938
- else if (size.x != undefined)
999
+ else if (size.x != undefined) // bounding box test
939
1000
  {
940
- // aabb test
941
1001
  for (const o of objects)
942
1002
  isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
943
1003
  }
944
- else
1004
+ else // circle test
945
1005
  {
946
- // circle test
947
1006
  const sizeSquared = size*size;
948
1007
  for (const o of objects)
949
1008
  pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
@@ -956,51 +1015,83 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
956
1015
  'use strict';
957
1016
 
958
1017
  /**
959
- * LittleJS Object Base Object Class
960
- * <br> - Base object class used by the engine
961
- * <br> - Automatically adds self to object list
962
- * <br> - Will be updated and rendered each frame
963
- * <br> - Renders as a sprite from a tilesheet by default
964
- * <br> - Can have color and addtive color applied
965
- * <br> - 2d Physics and collision system
966
- * <br> - Sorted by renderOrder
967
- * <br> - Objects can have children attached
968
- * <br> - Parents are updated before children, and set child transform
969
- * <br> - Call destroy() to get rid of objects
1018
+ * LittleJS Object Base Object Class
1019
+ * <br> - Base object class used by the engine
1020
+ * <br> - Automatically adds self to object list
1021
+ * <br> - Will be updated and rendered each frame
1022
+ * <br> - Renders as a sprite from a tilesheet by default
1023
+ * <br> - Can have color and addtive color applied
1024
+ * <br> - 2d Physics and collision system
1025
+ * <br> - Sorted by renderOrder
1026
+ * <br> - Objects can have children attached
1027
+ * <br> - Parents are updated before children, and set child transform
1028
+ * <br> - Call destroy() to get rid of objects
1029
+ * <br>
1030
+ * <br>The physics system used by objects is simple and fast with some caveats...
1031
+ * <br> - Collision uses the axis aligned size, the object's rotation angle is only for rendering
1032
+ * <br> - Objects are guaranteed to not intersect tile collision from physics
1033
+ * <br> - If an object starts or is moved inside tile collision, it will not collide with that tile
1034
+ * <br> - Collision for objects can be set to be solid to block other objects
1035
+ * <br> - Objects may get pushed into overlapping other solid objects, if so they will push away
1036
+ * <br> - Solid objects are more performance intensive and should be used sparingly
1037
+ * @example
1038
+ * // create an engine object, normally you would first extend the class with your own
1039
+ * const pos = vec2(2,3);
1040
+ * const object = new EngineObject(pos);
970
1041
  */
971
1042
  class EngineObject
972
1043
  {
973
- /**
974
- * Create an engine object and adds it to the list of objects
975
- * @param {Vector2} [position=new Vector2(0,0)] - World space position of the object
976
- * @param {Vector2} [size=defaultObjectSize] - World space size of the object
977
- * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
978
- * @param {Vector2} [tileSize=defaultTileSize] - Size of tile in source pixels
979
- * @param {Number} [angle=0] - Angle to rotate the object
980
- * @param {Color} [color] - Color to apply to tile when rendered
1044
+ /** Create an engine object and adds it to the list of objects
1045
+ * @param {Vector2} [position=new Vector2(0,0)] - World space position of the object
1046
+ * @param {Vector2} [size=objectDefaultSize] - World space size of the object
1047
+ * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1048
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
1049
+ * @param {Number} [angle=0] - Angle to rotate the object
1050
+ * @param {Color} [color] - Color to apply to tile when rendered
1051
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
981
1052
  */
982
- constructor(pos=vec2(), size=defaultObjectSize, tileIndex=-1, tileSize=defaultTileSize, angle=0, color)
1053
+ constructor(pos=vec2(), size=objectDefaultSize, tileIndex=-1, tileSize=tileSizeDefault, angle=0, color, renderOrder=0)
983
1054
  {
984
1055
  // set passed in params
985
1056
  ASSERT(pos && pos.x != undefined && size.x != undefined); // ensure pos and size are vec2s
1057
+
1058
+ /** @property {Vector2} - World space position of the object */
986
1059
  this.pos = pos.copy();
1060
+ /** @property {Vector2} - World space width and height of the object */
987
1061
  this.size = size;
1062
+ /** @property {Vector2} - Size of object used for drawing, uses size if not set */
1063
+ this.drawSize;
1064
+ /** @property {Number} - Tile to use to render object, untextured if -1 */
988
1065
  this.tileIndex = tileIndex;
1066
+ /** @property {Vector2} - Size of tile in source pixels */
989
1067
  this.tileSize = tileSize;
1068
+ /** @property {Number} - Angle to rotate the object */
990
1069
  this.angle = angle;
1070
+ /** @property {Color} - Color to apply when rendered */
991
1071
  this.color = color;
1072
+ /** @property {Color} - Additive color to apply when rendered */
1073
+ this.additiveColor;
1074
+
1075
+ // set object defaults
1076
+ /** @property {Number} [mass=objectDefaultMass] - How heavy the object is */
1077
+ this.mass = objectDefaultMass;
1078
+ /** @property {Number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
1079
+ this.damping = objectDefaultDamping;
1080
+ /** @property {Number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
1081
+ this.angleDamping = objectDefaultAngleDamping;
1082
+ /** @property {Number} [elasticity=objectDefaultElasticity] - How bouncy the object is when colliding (0-1) */
1083
+ this.elasticity = objectDefaultElasticity;
1084
+ /** @property {Number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
1085
+ this.friction = objectDefaultFriction;
1086
+ /** @property {Number} [gravityScale=1] - How much to scale gravity by for this object */
1087
+ this.gravityScale = 1;
1088
+ /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1089
+ this.renderOrder = renderOrder;
992
1090
 
993
- // set physics defaults
994
- this.mass = defaultObjectMass;
995
- this.damping = defaultObjectDamping;
996
- this.angleDamping = defaultObjectAngleDamping;
997
- this.elasticity = defaultObjectElasticity;
998
- this.friction = defaultObjectFriction;
999
-
1000
- // init other object stuff
1091
+ // init other internal object stuff
1001
1092
  this.spawnTime = time;
1002
- this.velocity = vec2(this.collideSolidObjects = this.renderOrder = this.angleVelocity = 0);
1003
- this.collideTiles = this.gravityScale = 1;
1093
+ this.velocity = vec2(this.collideSolidObjects = this.angleVelocity = 0);
1094
+ this.collideTiles = 1;
1004
1095
  this.children = [];
1005
1096
 
1006
1097
  // add to list of objects
@@ -1020,8 +1111,8 @@ class EngineObject
1020
1111
  }
1021
1112
 
1022
1113
  // limit max speed to prevent missing collisions
1023
- this.velocity.x = clamp(this.velocity.x, maxObjectSpeed, -maxObjectSpeed);
1024
- this.velocity.y = clamp(this.velocity.y, maxObjectSpeed, -maxObjectSpeed);
1114
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1115
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1025
1116
 
1026
1117
  // apply physics
1027
1118
  const oldPos = this.pos.copy();
@@ -1050,14 +1141,14 @@ class EngineObject
1050
1141
  {
1051
1142
  // check collisions against solid objects
1052
1143
  const epsilon = 1e-3; // necessary to push slightly outside of the collision
1053
- for (const o of engineCollideObjects)
1144
+ for (const o of engineObjectsCollide)
1054
1145
  {
1055
1146
  // non solid objects don't collide with eachother
1056
- if (!this.isSolid & !o.isSolid || o.destroyed || o.parent)
1147
+ if (!this.isSolid & !o.isSolid || o.destroyed || o.parent || o == this)
1057
1148
  continue;
1058
1149
 
1059
1150
  // check collision
1060
- if (!isOverlapping(this.pos, this.size, o.pos, o.size) || o == this)
1151
+ if (!isOverlapping(this.pos, this.size, o.pos, o.size))
1061
1152
  continue;
1062
1153
 
1063
1154
  // pass collision to objects
@@ -1069,7 +1160,7 @@ class EngineObject
1069
1160
  // if already was touching, try to push away
1070
1161
  const deltaPos = oldPos.subtract(o.pos);
1071
1162
  const length = deltaPos.length();
1072
- const pushAwayAccel = .001; // push away if alread overlapping
1163
+ const pushAwayAccel = .001; // push away if already overlapping
1073
1164
  const velocity = length < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
1074
1165
  this.velocity = this.velocity.add(velocity);
1075
1166
  if (o.mass) // push away if not fixed
@@ -1080,16 +1171,15 @@ class EngineObject
1080
1171
  }
1081
1172
 
1082
1173
  // check for collision
1083
- const sx = this.size.x + o.size.x;
1084
- const sy = this.size.y + o.size.y;
1085
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sy + gravity; // prefer to push up if small delta
1086
- const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sy;
1087
- const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sx;
1174
+ const sizeBoth = this.size.add(o.size);
1175
+ const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity; // prefer to push up if small delta
1176
+ const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
1177
+ const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
1088
1178
 
1089
1179
  if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
1090
1180
  {
1091
1181
  // push outside object collision
1092
- this.pos.y = o.pos.y + (sy/2 + epsilon) * sign(oldPos.y - o.pos.y);
1182
+ this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
1093
1183
  if (o.groundObject && wasMovingDown || !o.mass)
1094
1184
  {
1095
1185
  // set ground object if landed on something
@@ -1112,15 +1202,14 @@ class EngineObject
1112
1202
 
1113
1203
  // lerp betwen elastic or inelastic based on elasticity
1114
1204
  const elasticity = max(this.elasticity, o.elasticity);
1115
- this.velocity.y = lerp(elasticity, elastic0, inelastic);
1116
- o.velocity.y = lerp(elasticity, elastic1, inelastic);
1205
+ this.velocity.y = lerp(elasticity, inelastic, elastic0);
1206
+ o.velocity.y = lerp(elasticity, inelastic, elastic1);
1117
1207
  }
1118
- debugPhysics && smallStepUp && (abs(oldPos.x - o.pos.x)*2 > sx) && console.log('stepUp', oldPos.y - o.pos.y);
1119
1208
  }
1120
1209
  if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision
1121
1210
  {
1122
1211
  // push outside collision
1123
- this.pos.x = o.pos.x + (sx/2 + epsilon) * sign(oldPos.x - o.pos.x);
1212
+ this.pos.x = o.pos.x + (sizeBoth.x/2 + epsilon) * sign(oldPos.x - o.pos.x);
1124
1213
  if (o.mass)
1125
1214
  {
1126
1215
  // inelastic collision
@@ -1134,13 +1223,12 @@ class EngineObject
1134
1223
 
1135
1224
  // lerp betwen elastic or inelastic based on elasticity
1136
1225
  const elasticity = max(this.elasticity, o.elasticity);
1137
- this.velocity.x = lerp(elasticity, elastic0, inelastic);
1138
- o.velocity.x = lerp(elasticity, elastic1, inelastic);
1226
+ this.velocity.x = lerp(elasticity, inelastic, elastic0);
1227
+ o.velocity.x = lerp(elasticity, inelastic, elastic1);
1139
1228
  }
1140
1229
  else // bounce if other object is fixed
1141
1230
  this.velocity.x *= -this.elasticity;
1142
1231
  }
1143
-
1144
1232
  debugPhysics && debugAABB(this.pos, this.size, o.pos, o.size, '#f0f');
1145
1233
  }
1146
1234
  }
@@ -1149,8 +1237,6 @@ class EngineObject
1149
1237
  // check collision against tiles
1150
1238
  if (tileCollisionTest(this.pos, this.size, this))
1151
1239
  {
1152
- //debugPhysics && debugRect(this.pos, this.size, '#ff0');
1153
-
1154
1240
  // if already was stuck in collision, don't do anything
1155
1241
  // this should not happen unless something starts in collision
1156
1242
  if (!tileCollisionTest(oldPos, this.size, this))
@@ -1207,19 +1293,19 @@ class EngineObject
1207
1293
 
1208
1294
  /** Called to check if a tile collision should be resolved
1209
1295
  * @param {Number} tileData - the value of the tile at the position
1210
- * @param {Vector2} pos - tile where the collision occured
1211
- * @return {Boolean} true if the collision should be resolved */
1296
+ * @param {Vector2} pos - tile where the collision occured
1297
+ * @return {Boolean} - true if the collision should be resolved */
1212
1298
  collideWithTile(tileData, pos) { return tileData > 0; }
1213
1299
 
1214
1300
  /** Called to check if a tile raycast hit
1215
1301
  * @param {Number} tileData - the value of the tile at the position
1216
- * @param {Vector2} pos - tile where the raycast is
1217
- * @return {Boolean} true if the raycast should hit */
1302
+ * @param {Vector2} pos - tile where the raycast is
1303
+ * @return {Boolean} - true if the raycast should hit */
1218
1304
  collideWithTileRaycast(tileData, pos) { return tileData > 0; }
1219
1305
 
1220
1306
  /** Called to check if a tile raycast hit
1221
1307
  * @param {EngineObject} object - the object to test against
1222
- * @return {Boolean} true if the collision should be resolved
1308
+ * @return {Boolean} - true if the collision should be resolved
1223
1309
  */
1224
1310
  collideWithObject(o) { return 1; }
1225
1311
 
@@ -1263,41 +1349,42 @@ class EngineObject
1263
1349
 
1264
1350
  /** Set how this object collides
1265
1351
  * @param {boolean} [collideSolidObjects=0] - Does it collide with solid objects
1266
- * @param {boolean} [isSolid=0] - Does it collide with and block other objects (expensive in large numbers)
1267
- * @param {boolean} [collideTiles=1] - Does it collide with the tile collision */
1352
+ * @param {boolean} [isSolid=0] - Does it collide with and block other objects (expensive in large numbers)
1353
+ * @param {boolean} [collideTiles=1] - Does it collide with the tile collision */
1268
1354
  setCollision(collideSolidObjects=0, isSolid=0, collideTiles=1)
1269
1355
  {
1270
1356
  ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1271
1357
 
1272
- // track collidable objects in separate list
1273
- if (collideSolidObjects && !this.collideSolidObjects)
1274
- {
1275
- ASSERT(!engineCollideObjects.includes(this));
1276
- engineCollideObjects.push(this);
1277
- }
1278
- else if (!collideSolidObjects && this.collideSolidObjects)
1279
- {
1280
- ASSERT(engineCollideObjects.includes(this))
1281
- engineCollideObjects.splice(engineCollideObjects.indexOf(this), 1);
1282
- }
1283
-
1284
1358
  this.collideSolidObjects = collideSolidObjects;
1285
1359
  this.isSolid = isSolid;
1286
1360
  this.collideTiles = collideTiles;
1287
1361
  }
1288
1362
  }
1289
1363
  /**
1290
- * LittleJS Drawing System
1291
- * <br> - Hybrid with both Canvas2D and WebGL available
1292
- * <br> - Super fast tile sheet rendering with WebGL
1293
- * <br> - Can apply rotation, mirror, color and additive color
1294
- * <br> - Many useful utility functions
1295
- * @namespace Draw
1364
+ * LittleJS Drawing System
1365
+ * <br> - Hybrid with both Canvas2D and WebGL available
1366
+ * <br> - Super fast tile sheet rendering with WebGL
1367
+ * <br> - Can apply rotation, mirror, color and additive color
1368
+ * <br> - Many useful utility functions
1369
+ * <br>
1370
+ * <br>LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
1371
+ * <br>There are 3 canvas/contexts available to draw to...
1372
+ * <br> - mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
1373
+ * <br> - glCanvas - Used by the accelerated WebGL batch rendering system.
1374
+ * <br> - overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
1375
+ * <br>
1376
+ * <br>The WebGL rendering system is very fast with some caveats...
1377
+ * <br> - The default setup supports only 1 tile sheet, to support more call glCreateTexture and glSetTexture
1378
+ * <br> - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
1379
+ * <br> - Group additive rendering together using renderOrder to mitigate this issue
1380
+ * <br>
1381
+ * <br>The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
1382
+ * @namespace Draw
1296
1383
  */
1297
1384
 
1298
1385
  'use strict';
1299
1386
 
1300
- /** Main tilesheet to use for batch rendering system
1387
+ /** Tile sheet for batch rendering system
1301
1388
  * @type {Image}
1302
1389
  * @memberof Draw */
1303
1390
  const tileImage = new Image();
@@ -1322,7 +1409,7 @@ let overlayCanvas;
1322
1409
  * @memberof Draw */
1323
1410
  let overlayContext;
1324
1411
 
1325
- /** The size of the main canvas (and other secondary canvases: overlayCanvas and glCanvas)
1412
+ /** The size of the main canvas (and other secondary canvases)
1326
1413
  * @type {Vector2}
1327
1414
  * @memberof Draw */
1328
1415
  let mainCanvasSize = vec2();
@@ -1341,42 +1428,39 @@ const screenToWorld = (screenPos)=>
1341
1428
  const worldToScreen = (worldPos)=>
1342
1429
  worldPos.subtract(cameraPos).multiply(vec2(cameraScale,-cameraScale)).add(mainCanvasSize.scale(.5)).subtract(vec2(.5));
1343
1430
 
1344
- /** Draw textured tile centered on pos
1345
- * @param {Vector2} pos - Center of the tile
1346
- * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile
1347
- * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1348
- * @param {Vector2} [tileSize=defaultTileSize] - Tile size in source pixels
1349
- * @param {Color} [color=new Color(1,1,1)]
1350
- * @param {Number} [angle=0]
1351
- * @param {Boolean} [mirror=0]
1352
- * @param {Color} [additiveColor=new Color(0,0,0,0)]
1431
+ /** Draw textured tile centered in world space, with color applied if using WebGL
1432
+ * @param {Vector2} pos - Center of the tile in world space
1433
+ * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile in world space, width and height
1434
+ * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1435
+ * @param {Vector2} [tileSize=tileSizeDefault] - Tile size in source pixels
1436
+ * @param {Color} [color=new Color(1,1,1)] - Color to modulate with
1437
+ * @param {Number} [angle=0] - Angle to rotate by
1438
+ * @param {Boolean} [mirror=0] - If true image is flipped along the Y axis
1439
+ * @param {Color} [additiveColor=new Color(0,0,0,0)] - Additive color to be applied
1440
+ * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1353
1441
  * @memberof Draw */
1354
- function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=defaultTileSize, color=new Color, angle=0, mirror,
1355
- additiveColor=new Color(0,0,0,0))
1442
+ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color, angle=0, mirror,
1443
+ additiveColor=new Color(0,0,0,0), useWebGL=glEnable)
1356
1444
  {
1357
1445
  showWatermark && ++drawCount;
1358
- if (glEnable)
1446
+ if (glEnable && useWebGL)
1359
1447
  {
1360
- if (tileIndex < 0)
1448
+ if (tileIndex < 0 || !tileImage.width)
1361
1449
  {
1362
- // if negative tile index, force untextured
1450
+ // if negative tile index or image not found, force untextured
1363
1451
  glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
1364
1452
  }
1365
1453
  else
1366
1454
  {
1367
1455
  // calculate uvs and render
1368
- const cols = tileImage.width / tileSize.x |0;
1369
- const uvSizeX = tileSize.x * tileImageSizeInverse.x;
1370
- const uvSizeY = tileSize.y * tileImageSizeInverse.y;
1456
+ const cols = tileImageSize.x / tileSize.x |0;
1457
+ const uvSizeX = tileSize.x / tileImageSize.x;
1458
+ const uvSizeY = tileSize.y / tileImageSize.y;
1371
1459
  const uvX = (tileIndex%cols)*uvSizeX, uvY = (tileIndex/cols|0)*uvSizeY;
1372
-
1373
- // shrink uvs to prevent bleeding
1374
- const shrinkTilesX = tileBleedShrinkFix * tileImageSizeInverse.x;
1375
- const shrinkTilesY = tileBleedShrinkFix * tileImageSizeInverse.y;
1376
1460
 
1377
1461
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
1378
- uvX + shrinkTilesX, uvY + shrinkTilesY,
1379
- uvX - shrinkTilesX + uvSizeX, uvY - shrinkTilesX + uvSizeY,
1462
+ uvX + tileImageFixBleed.x, uvY + tileImageFixBleed.y,
1463
+ uvX - tileImageFixBleed.x + uvSizeX, uvY - tileImageFixBleed.y + uvSizeY,
1380
1464
  color.rgbaInt(), additiveColor.rgbaInt());
1381
1465
  }
1382
1466
  }
@@ -1394,11 +1478,11 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=defaultTileSize, col
1394
1478
  else
1395
1479
  {
1396
1480
  // calculate uvs and render
1397
- const cols = tileImage.width / tileSize.x |0;
1398
- const sX = (tileIndex%cols)*tileSize.x + tileBleedShrinkFix;
1399
- const sY = (tileIndex/cols|0)*tileSize.y + tileBleedShrinkFix;
1400
- const sWidth = tileSize.x - 2*tileBleedShrinkFix;
1401
- const sHeight = tileSize.y - 2*tileBleedShrinkFix;
1481
+ const cols = tileImageSize.x / tileSize.x |0;
1482
+ const sX = (tileIndex%cols)*tileSize.x + tileFixBleedScale;
1483
+ const sY = (tileIndex/cols|0)*tileSize.y + tileFixBleedScale;
1484
+ const sWidth = tileSize.x - 2*tileFixBleedScale;
1485
+ const sHeight = tileSize.y - 2*tileFixBleedScale;
1402
1486
  context.globalAlpha = color.a; // only alpha is supported
1403
1487
  context.drawImage(tileImage, sX, sY, sWidth, sHeight, -.5, -.5, 1, 1);
1404
1488
  }
@@ -1406,41 +1490,44 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=defaultTileSize, col
1406
1490
  }
1407
1491
  }
1408
1492
 
1409
- /** Draw colored untextured rect centered on pos
1493
+ /** Draw colored rect centered on pos
1410
1494
  * @param {Vector2} pos
1411
1495
  * @param {Vector2} [size=new Vector2(1,1)]
1412
1496
  * @param {Color} [color=new Color(1,1,1)]
1413
1497
  * @param {Number} [angle=0]
1498
+ * @param {Boolean} [useWebGL=glEnable]
1414
1499
  * @memberof Draw */
1415
- function drawRect(pos, size, color, angle)
1500
+ function drawRect(pos, size, color, angle, useWebGL)
1416
1501
  {
1417
- drawTile(pos, size, -1, defaultTileSize, color, angle);
1502
+ drawTile(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1418
1503
  }
1419
1504
 
1420
1505
  /** Draw textured tile centered on pos in screen space
1421
- * @param {Vector2} pos - Center of the tile
1422
- * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile
1423
- * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1424
- * @param {Vector2} [tileSize=defaultTileSize] - Tile size in source pixels
1506
+ * @param {Vector2} pos - Center of the tile
1507
+ * @param {Vector2} [size=new Vector2(1,1)] - Size of the tile
1508
+ * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1509
+ * @param {Vector2} [tileSize=tileSizeDefault] - Tile size in source pixels
1425
1510
  * @param {Color} [color=new Color]
1426
1511
  * @param {Number} [angle=0]
1427
1512
  * @param {Boolean} [mirror=0]
1428
1513
  * @param {Color} [additiveColor=new Color(0,0,0,0)]
1514
+ * @param {Boolean} [useWebGL=glEnable]
1429
1515
  * @memberof Draw */
1430
- function drawTileScreenSpace(pos, size=vec2(1), tileIndex, tileSize, color, angle, mirror, additiveColor)
1516
+ function drawTileScreenSpace(pos, size=vec2(1), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL)
1431
1517
  {
1432
- drawTile(screenToWorld(pos), size.scale(1/cameraScale), tileIndex, tileSize, color, angle, mirror, additiveColor);
1518
+ drawTile(screenToWorld(pos), size.scale(1/cameraScale), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL);
1433
1519
  }
1434
1520
 
1435
- /** Draw colored untextured rectangle in screen space
1521
+ /** Draw colored rectangle in screen space
1436
1522
  * @param {Vector2} pos
1437
1523
  * @param {Vector2} [size=new Vector2(1,1)]
1438
1524
  * @param {Color} [color=new Color(1,1,1)]
1439
1525
  * @param {Number} [angle=0]
1526
+ * @param {Boolean} [useWebGL=glEnable]
1440
1527
  * @memberof Draw */
1441
- function drawRectScreenSpace(pos, size, color, angle)
1528
+ function drawRectScreenSpace(pos, size, color, angle, useWebGL)
1442
1529
  {
1443
- drawTileScreenSpace(pos, size, -1, defaultTileSize, color, angle);
1530
+ drawTileSrceenSpace(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1444
1531
  }
1445
1532
 
1446
1533
  /** Draw colored line between two points
@@ -1448,15 +1535,16 @@ function drawRectScreenSpace(pos, size, color, angle)
1448
1535
  * @param {Vector2} posB
1449
1536
  * @param {Number} [thickness=.1]
1450
1537
  * @param {Color} [color=new Color(1,1,1)]
1538
+ * @param {Boolean} [useWebGL=glEnable]
1451
1539
  * @memberof Draw */
1452
- function drawLine(posA, posB, thickness=.1, color)
1540
+ function drawLine(posA, posB, thickness=.1, color, useWebGL)
1453
1541
  {
1454
1542
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
1455
1543
  const size = vec2(thickness, halfDelta.length()*2);
1456
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle());
1544
+ drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), 0, 0, useWebGL);
1457
1545
  }
1458
1546
 
1459
- /** Draw directly to a 2d canvas context in world space (bipass webgl)
1547
+ /** Draw directly to a 2d canvas context in world space
1460
1548
  * @param {Vector2} pos
1461
1549
  * @param {Vector2} size
1462
1550
  * @param {Number} angle
@@ -1472,7 +1560,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
1472
1560
  context.save();
1473
1561
  context.translate(pos.x+.5|0, pos.y-.5|0);
1474
1562
  context.rotate(angle);
1475
- context.scale(mirror?-size.x:size.x, size.y);
1563
+ context.scale(mirror ? -size.x : size.x, size.y);
1476
1564
  drawFunction(context);
1477
1565
  context.restore();
1478
1566
  }
@@ -1486,7 +1574,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
1486
1574
  * @param {Color} [lineColor=new Color(0,0,0)]
1487
1575
  * @param {String} [textAlign='center']
1488
1576
  * @memberof Draw */
1489
- function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=defaultFont)
1577
+ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
1490
1578
  {
1491
1579
  pos = worldToScreen(pos);
1492
1580
  overlayContext.font = size*cameraScale + 'px '+ font;
@@ -1502,12 +1590,13 @@ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new
1502
1590
  overlayContext.fillText(text, pos.x, pos.y);
1503
1591
  }
1504
1592
 
1505
- /** Enable additive or regular blend mode
1593
+ /** Enable normal or additive blend mode
1506
1594
  * @param {Boolean} [additive=0]
1595
+ * @param {Boolean} [useWebGL=glEnable]
1507
1596
  * @memberof Draw */
1508
- function setBlendMode(additive)
1597
+ function setBlendMode(additive, useWebGL=glEnable)
1509
1598
  {
1510
- if (glEnable)
1599
+ if (glEnable && useWebGL)
1511
1600
  glSetBlendMode(additive);
1512
1601
  else
1513
1602
  mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
@@ -1541,11 +1630,12 @@ function toggleFullscreen()
1541
1630
  }
1542
1631
  }
1543
1632
  /**
1544
- * LittleJS Input System
1545
- * <br> - Tracks key down, pressed, and released
1546
- * <br> - Also tracks mouse buttons, position, and wheel
1547
- * <br> - Supports multiple gamepads
1548
- * @namespace Input
1633
+ * LittleJS Input System
1634
+ * <br> - Tracks key down, pressed, and released
1635
+ * <br> - Also tracks mouse buttons, position, and wheel
1636
+ * <br> - Supports multiple gamepads
1637
+ * <br> - Virtual gamepad for touch devices with touchGamepadSize
1638
+ * @namespace Input
1549
1639
  */
1550
1640
 
1551
1641
  'use strict';
@@ -1573,7 +1663,7 @@ const keyWasReleased = (key, device=0)=> inputData[device] && inputData[device][
1573
1663
 
1574
1664
  /** Clears all input
1575
1665
  * @memberof Input */
1576
- const clearInput = ()=> inputData[0] = [];
1666
+ const clearInput = ()=> inputData = [[]];
1577
1667
 
1578
1668
  /** Returns true if mouse button is down
1579
1669
  * @param {Number} button
@@ -1609,7 +1699,7 @@ let mouseWheel = 0;
1609
1699
 
1610
1700
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
1611
1701
  * @memberof Input */
1612
- let usingGamepad = 0;
1702
+ let isUsingGamepad = 0;
1613
1703
 
1614
1704
  /** Returns true if gamepad button is down
1615
1705
  * @param {Number} button
@@ -1642,7 +1732,9 @@ const gamepadStick = (stick, gamepad=0)=> stickData[gamepad] ? stickData[gamepa
1642
1732
  ///////////////////////////////////////////////////////////////////////////////
1643
1733
  // Input update called by engine
1644
1734
 
1645
- const inputData = [[]];
1735
+ // store input as a bit field for each key: 1 = isDown, 2 = wasPressed, 4 = wasReleased
1736
+ // mouse and keyboard are stored together in device 0, gamepads are in devices > 0
1737
+ let inputData = [[]];
1646
1738
 
1647
1739
  function inputUpdate()
1648
1740
  {
@@ -1668,34 +1760,38 @@ function inputUpdatePost()
1668
1760
  ///////////////////////////////////////////////////////////////////////////////
1669
1761
  // Keyboard event handlers
1670
1762
 
1671
- onkeydown = e=>
1763
+ onkeydown = (e)=>
1672
1764
  {
1673
1765
  if (debug && e.target != document.body) return;
1674
- e.repeat || (inputData[usingGamepad = 0][remapKeyCode(e.keyCode)] = 3);
1766
+ e.repeat || (inputData[isUsingGamepad = 0][remapKeyCode(e.keyCode)] = 3);
1675
1767
  debug || e.preventDefault();
1676
1768
  }
1677
- onkeyup = e=>
1769
+ onkeyup = (e)=>
1678
1770
  {
1679
1771
  if (debug && e.target != document.body) return;
1680
1772
  inputData[0][remapKeyCode(e.keyCode)] = 4;
1681
1773
  }
1682
- const remapKeyCode = c=> copyWASDToDpad ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
1774
+ const remapKeyCode = (c)=> inputWASDEmulateDirection ? c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
1683
1775
 
1684
1776
  ///////////////////////////////////////////////////////////////////////////////
1685
1777
  // Mouse event handlers
1686
1778
 
1687
- onmousedown = e=> {inputData[usingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
1688
- onmouseup = e=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
1689
- onmousemove = e=>
1779
+ onmousedown = (e)=> {inputData[isUsingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
1780
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
1781
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
1782
+ onwheel = (e)=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
1783
+ oncontextmenu = (e)=> !1; // prevent right click menu
1784
+
1785
+ // convert a mouse or touch event position to screen space
1786
+ const mouseToScreen = (mousePos)=>
1690
1787
  {
1691
- // convert mouse pos to canvas space
1692
- if (!mainCanvas) return;
1788
+ if (!mainCanvas)
1789
+ return vec2(); // fix bug that can occur if user clicks before page loads
1790
+
1693
1791
  const rect = mainCanvas.getBoundingClientRect();
1694
- mousePosScreen.x = mainCanvasSize.x * percent(e.x, rect.right, rect.left);
1695
- mousePosScreen.y = mainCanvasSize.y * percent(e.y, rect.bottom, rect.top);
1792
+ return mainCanvasSize.multiply(
1793
+ vec2(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom)));
1696
1794
  }
1697
- onwheel = e=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
1698
- oncontextmenu = e=> !1; // prevent right click menu
1699
1795
 
1700
1796
  ///////////////////////////////////////////////////////////////////////////////
1701
1797
  // Gamepad input
@@ -1703,6 +1799,21 @@ oncontextmenu = e=> !1; // prevent right click menu
1703
1799
  const stickData = [];
1704
1800
  function gamepadsUpdate()
1705
1801
  {
1802
+ if (touchGamepadEnable && touchGamepadTimer.isSet())
1803
+ {
1804
+ // read virtual analog stick
1805
+ const sticks = stickData[0] || (stickData[0] = []);
1806
+ sticks[0] = vec2(touchGamepadStick.x, -touchGamepadStick.y); // flip vertical
1807
+
1808
+ // read virtual gamepad buttons
1809
+ const data = inputData[1] || (inputData[1] = []);
1810
+ for (let i=10; i--;)
1811
+ {
1812
+ const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
1813
+ data[j] = touchGamepadButtons[i] ? 1 + 2*!gamepadIsDown(j,0) : 4*gamepadIsDown(j,0);
1814
+ }
1815
+ }
1816
+
1706
1817
  if (!gamepadsEnable || !navigator.getGamepads || !document.hasFocus() && !debug)
1707
1818
  return;
1708
1819
 
@@ -1720,8 +1831,8 @@ function gamepadsUpdate()
1720
1831
  // read clamp dead zone of analog sticks
1721
1832
  const deadZone = .3, deadZoneMax = .8;
1722
1833
  const applyDeadZone = (v)=>
1723
- v > deadZone ? percent( v, deadZoneMax, deadZone) :
1724
- v < -deadZone ? -percent(-v, deadZoneMax, deadZone) : 0;
1834
+ v > deadZone ? percent( v, deadZone, deadZoneMax) :
1835
+ v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
1725
1836
 
1726
1837
  // read analog sticks
1727
1838
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
@@ -1732,10 +1843,11 @@ function gamepadsUpdate()
1732
1843
  {
1733
1844
  const button = gamepad.buttons[j];
1734
1845
  data[j] = button.pressed ? 1 + 2*!gamepadIsDown(j,i) : 4*gamepadIsDown(j,i);
1735
- usingGamepad |= !i && button.pressed;
1846
+ isUsingGamepad |= !i && button.pressed;
1847
+ touchGamepadEnable && touchGamepadTimer.unset(); // disable touch gamepad if using real gamepad
1736
1848
  }
1737
-
1738
- if (copyGamepadDirectionToStick)
1849
+
1850
+ if (gamepadDirectionEmulateStick)
1739
1851
  {
1740
1852
  // copy dpad to left analog stick when pressed
1741
1853
  const dpad = vec2(gamepadIsDown(15,i) - gamepadIsDown(14,i), gamepadIsDown(12,i) - gamepadIsDown(13,i));
@@ -1746,18 +1858,31 @@ function gamepadsUpdate()
1746
1858
  }
1747
1859
  }
1748
1860
 
1861
+ ///////////////////////////////////////////////////////////////////////////////
1862
+
1863
+ /** Pulse the vibration hardware if it exists
1864
+ * @param {Number} [pattern=100] - a single value in miliseconds or vibration interval array
1865
+ * @memberof Input */
1866
+ const vibrate = (pattern)=> vibrateEnable && Navigator.vibrate && Navigator.vibrate(pattern);
1867
+
1868
+ /** Cancel any ongoing vibration
1869
+ * @memberof Input */
1870
+ const vibrateStop = ()=> vibrate(0);
1871
+
1749
1872
  ///////////////////////////////////////////////////////////////////////////////
1750
1873
  // Touch input
1751
1874
 
1752
1875
  /** True if a touch device has been detected
1753
1876
  * @const {boolean}
1754
1877
  * @memberof Input */
1755
- const isTouchDevice = touchInputEnable && window.ontouchstart !== undefined;
1878
+ const isTouchDevice = window.ontouchstart !== undefined;
1879
+
1880
+ // try to enable touch mouse
1756
1881
  if (isTouchDevice)
1757
1882
  {
1758
1883
  // handle all touch events the same way
1759
1884
  let wasTouching, hadTouchInput;
1760
- ontouchstart = ontouchmove = ontouchend = e=>
1885
+ ontouchstart = ontouchmove = ontouchend = (e)=>
1761
1886
  {
1762
1887
  e.button = 0; // all touches are left click
1763
1888
 
@@ -1782,31 +1907,158 @@ if (isTouchDevice)
1782
1907
  return !e.cancelable;
1783
1908
  }
1784
1909
  }
1910
+
1911
+ ///////////////////////////////////////////////////////////////////////////////
1912
+ // touch gamepad, virtual on screen gamepad emulator for touch devices
1913
+
1914
+ // touch input internal variables
1915
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2(), touchGamepadAnalog = 1;
1916
+
1917
+ // create the touch gamepad, called automatically by the engine
1918
+ function touchGamepadCreate()
1919
+ {
1920
+ if (!touchGamepadEnable || !isTouchDevice)
1921
+ return;
1922
+
1923
+ ontouchstart = ontouchmove = ontouchend = (e)=>
1924
+ {
1925
+ if (!touchGamepadEnable)
1926
+ return;
1927
+
1928
+ // clear touch gamepad input
1929
+ touchGamepadStick = vec2();
1930
+ touchGamepadButtons = [];
1931
+
1932
+ const touching = e.touches.length;
1933
+ if (touching)
1934
+ {
1935
+ touchGamepadTimer.isSet() || zzfx(0) ; // fix mobile audio, force it to play a sound the first time
1936
+
1937
+ // set that gamepad is active
1938
+ isUsingGamepad = 1;
1939
+ touchGamepadTimer.set();
1940
+
1941
+ if (paused)
1942
+ {
1943
+ // touch anywhere to press start when paused
1944
+ touchGamepadButtons[9] = 1;
1945
+ return;
1946
+ }
1947
+ }
1948
+
1949
+ // get center of left and right sides
1950
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
1951
+ const buttonCenter = mainCanvasSize.subtract(vec2(touchGamepadSize, touchGamepadSize));
1952
+ const startCenter = mainCanvasSize.scale(.5);
1953
+
1954
+ // check each touch point
1955
+ for (const touch of e.touches)
1956
+ {
1957
+ const touchPos = mouseToScreen(vec2(touch.clientX, touch.clientY));
1958
+ if (touchPos.distance(stickCenter) < touchGamepadSize)
1959
+ {
1960
+ // virtual analog stick
1961
+ if (touchGamepadAnalog)
1962
+ touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
1963
+ else
1964
+ {
1965
+ // 8 way dpad
1966
+ const angle = touchPos.subtract(stickCenter).angle();
1967
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
1968
+ }
1969
+ }
1970
+ else if (touchPos.distance(buttonCenter) < touchGamepadSize)
1971
+ {
1972
+ // virtual face buttons
1973
+ const button = touchPos.subtract(buttonCenter).direction();
1974
+ touchGamepadButtons[button] = 1;
1975
+ }
1976
+ else if (touchPos.distance(startCenter) < touchGamepadSize)
1977
+ {
1978
+ // virtual start button in center
1979
+ touchGamepadButtons[9] = 1;
1980
+ }
1981
+ }
1982
+ }
1983
+ }
1984
+
1985
+ // render the touch gamepad, called automatically by the engine
1986
+ function touchGamepadRender()
1987
+ {
1988
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet())
1989
+ return;
1990
+
1991
+ // fade off when not touching or paused
1992
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
1993
+ if (!alpha || paused)
1994
+ return;
1995
+
1996
+ // setup the canvas
1997
+ overlayContext.save();
1998
+ overlayContext.globalAlpha = alpha*touchGamepadAlpha;
1999
+ overlayContext.strokeStyle = '#fff';
2000
+ overlayContext.lineWidth = 3;
2001
+
2002
+ // draw left analog stick
2003
+ overlayContext.fillStyle = gamepadStick(0).length() > 0 ? '#fff' : '#000';
2004
+ overlayContext.beginPath();
2005
+ overlayContext.arc(touchGamepadSize, mainCanvasSize.y-touchGamepadSize, touchGamepadSize/2, 0,9);
2006
+ overlayContext.fill();
2007
+ overlayContext.stroke();
2008
+
2009
+ // draw right face buttons
2010
+ const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2011
+ for (let i=4; i--;)
2012
+ {
2013
+ const pos = rightCenter.add((new Vector2).setAngle(i*PI/2, touchGamepadSize/2));
2014
+ overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2015
+ overlayContext.beginPath();
2016
+ overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
2017
+ overlayContext.fill();
2018
+ overlayContext.stroke();
2019
+ }
2020
+
2021
+ // set canvas back to normal
2022
+ overlayContext.restore();
2023
+ }
1785
2024
  /**
1786
- * LittleJS Audio System
1787
- * <br> - ZzFX Sound Effects and ZzFXM Music
1788
- * <br> - Caches sounds and music for fast playback
1789
- * <br> - Can attenuate and apply stereo panning to sounds
1790
- * <br> - Ability to play mp3, ogg, and wave files
1791
- * <br> - Speech synthesis wrapper functions
1792
- * @namespace Audio
2025
+ * LittleJS Audio System
2026
+ * <br> - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a>
2027
+ * <br> - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a>
2028
+ * <br> - Caches sounds and music for fast playback
2029
+ * <br> - Can attenuate and apply stereo panning to sounds
2030
+ * <br> - Ability to play mp3, ogg, and wave files
2031
+ * <br> - Speech synthesis wrapper functions
1793
2032
  */
1794
2033
 
1795
2034
  'use strict';
1796
2035
 
1797
- /** Sound Object - Stores a zzfx sound for later use and can be played positionally */
2036
+ /**
2037
+ * Sound Object - Stores a zzfx sound for later use and can be played positionally
2038
+ * <br>
2039
+ * <br><b><a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a></b>
2040
+ * @example
2041
+ * // create a sound
2042
+ * const sound_example = new Sound([.5,.5]);
2043
+ *
2044
+ * // play the sound
2045
+ * sound_example.play();
2046
+ */
1798
2047
  class Sound
1799
2048
  {
1800
2049
  /** Create a sound object and cache the zzfx samples for later use
1801
2050
  * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
1802
- * @param {Number} [range=defaultSoundRange] - World space max range of sound, will not play if camera is farther away
1803
- * @param {Number} [taper=defaultSoundTaper] - At what percentage of range should it start tapering off
2051
+ * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2052
+ * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
1804
2053
  */
1805
- constructor(zzfxSound, range=defaultSoundRange, taper=defaultSoundTaper)
2054
+ constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
1806
2055
  {
1807
2056
  if (!soundEnable) return;
1808
2057
 
2058
+ /** @property {Number} - World space max range of sound, will not play if camera is farther away */
1809
2059
  this.range = range;
2060
+
2061
+ /** @property {Number} - At what percentage of range should it start tapering off */
1810
2062
  this.taper = taper;
1811
2063
 
1812
2064
  // get randomness from sound parameters
@@ -1840,7 +2092,7 @@ class Sound
1840
2092
  return; // out of range
1841
2093
 
1842
2094
  // attenuate volume by distance
1843
- volume *= percent(lengthSquared**.5, range*this.taper, range);
2095
+ volume *= percent(lengthSquared**.5, range, range*this.taper);
1844
2096
  }
1845
2097
 
1846
2098
  // get pan from screen space coords
@@ -1866,7 +2118,36 @@ class Sound
1866
2118
  }
1867
2119
  }
1868
2120
 
1869
- /** Music Object - Stores a zzfx music track for later use */
2121
+ /**
2122
+ * Music Object - Stores a zzfx music track for later use
2123
+ * <br>
2124
+ * <br><b><a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a></b>
2125
+ * @example
2126
+ * // create some music
2127
+ * const music_example = new Music(
2128
+ * [
2129
+ * [ // instruments
2130
+ * [,0,400] // simple note
2131
+ * ],
2132
+ * [ // patterns
2133
+ * [ // pattern 1
2134
+ * [ // channel 0
2135
+ * 0, -1, // instrument 0, left speaker
2136
+ * 1, 0, 9, 1 // channel notes
2137
+ * ],
2138
+ * [ // channel 1
2139
+ * 0, 1, // instrument 1, right speaker
2140
+ * 0, 12, 17, -1 // channel notes
2141
+ * ]
2142
+ * ],
2143
+ * ],
2144
+ * [0, 0, 0, 0], // sequence, play pattern 0 four times
2145
+ * 90 // BPM
2146
+ * ]);
2147
+ *
2148
+ * // play the music
2149
+ * music_example.play();
2150
+ */
1870
2151
  class Music
1871
2152
  {
1872
2153
  /** Create a music object and cache the zzfx music samples for later use
@@ -1903,7 +2184,7 @@ function playAudioFile(url, volume=1, loop=1)
1903
2184
  if (!soundEnable) return;
1904
2185
 
1905
2186
  const audio = new Audio(url);
1906
- audio.volume = audioVolume * volume;
2187
+ audio.volume = soundVolume * volume;
1907
2188
  audio.loop = loop;
1908
2189
  audio.play();
1909
2190
  return audio;
@@ -1928,7 +2209,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
1928
2209
  // build utterance and speak
1929
2210
  const utterance = new SpeechSynthesisUtterance(text);
1930
2211
  utterance.lang = language;
1931
- utterance.volume = 2*volume*audioVolume;
2212
+ utterance.volume = 2*volume*soundVolume;
1932
2213
  utterance.rate = rate;
1933
2214
  utterance.pitch = pitch;
1934
2215
  speechSynthesis.speak(utterance);
@@ -1937,7 +2218,7 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
1937
2218
 
1938
2219
  /** Stop all queued speech
1939
2220
  * @memberof Audio */
1940
- const stopSpeech = ()=> speechSynthesis && speechSynthesis.cancel();
2221
+ const speakStop = ()=> speechSynthesis && speechSynthesis.cancel();
1941
2222
 
1942
2223
  /** Get frequency of a note on a musical scale
1943
2224
  * @param {Number} semitoneOffset - How many semitones away from the root note
@@ -1968,6 +2249,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
1968
2249
  if (!audioContext)
1969
2250
  audioContext = new (window.AudioContext||webkitAudioContext);
1970
2251
 
2252
+ // fix stalled audio
2253
+ audioContext.resume();
2254
+
2255
+ // prevent sounds from building up if they can't be played
2256
+ if (audioContext.state != 'running')
2257
+ return;
2258
+
1971
2259
  // create buffer and source
1972
2260
  const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, zzfxR),
1973
2261
  source = audioContext.createBufferSource();
@@ -1978,11 +2266,18 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
1978
2266
  source.playbackRate.value = rate;
1979
2267
  source.loop = loop;
1980
2268
 
1981
- // create pan and gain nodes
1982
- source
1983
- .connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, 1, -1)}))
1984
- .connect(new GainNode(audioContext, {'gain':audioVolume*volume}))
1985
- .connect(audioContext.destination);
2269
+ // create and connect gain node (createGain is more widley spported then GainNode construtor)
2270
+ const gainNode = audioContext.createGain();
2271
+ gainNode.gain.value = soundVolume*volume;
2272
+ gainNode.connect(audioContext.destination);
2273
+
2274
+ // connect source to gain
2275
+ (
2276
+ window.StereoPannerNode ? // create pan node if possible
2277
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)}))
2278
+ : source
2279
+ )
2280
+ .connect(gainNode);
1986
2281
 
1987
2282
  // play and return sound
1988
2283
  source.start();
@@ -1993,6 +2288,8 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
1993
2288
  // ZzFXMicro - Zuper Zmall Zound Zynth - v1.1.8 by Frank Force
1994
2289
 
1995
2290
  /** Generate and play a ZzFX sound
2291
+ * <br>
2292
+ * <br><b><a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a></b>
1996
2293
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
1997
2294
  * @return {Array} - Array of audio samples
1998
2295
  * @memberof Audio */
@@ -2049,7 +2346,7 @@ function zzfxG
2049
2346
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
2050
2347
  : 1) *
2051
2348
  sign(s)*(abs(s)**shapeCurve) * // curve 0=square, 2=pointy
2052
- volume * audioVolume * ( // envelope
2349
+ volume * soundVolume * ( // envelope
2053
2350
  i < attack ? i/attack : // attack
2054
2351
  i < attack + decay ? // decay
2055
2352
  1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
@@ -2195,27 +2492,31 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2195
2492
  return [leftChannelBuffer, rightChannelBuffer];
2196
2493
  }
2197
2494
  /**
2198
- * LittleJS Tile Layer System
2199
- * <br> - Caches arrays of tiles to offscreen canvas for fast rendering
2200
- * <br> - Unlimted numbers of layers, allocates canvases as needed
2201
- * <br> - Interfaces with EngineObject for collision
2202
- * <br> - Collision layer is separate from visible layers
2203
- * <br> - Tile layers can be drawn to using their context with canvas2d
2204
- * <br> - It is recommended to have a visible layer that matches the collision
2205
- * @namespace TileLayer
2495
+ * LittleJS Tile Layer System
2496
+ * <br> - Caches arrays of tiles to off screen canvas for fast rendering
2497
+ * <br> - Unlimted numbers of layers, allocates canvases as needed
2498
+ * <br> - Interfaces with EngineObject for collision
2499
+ * <br> - Collision layer is separate from visible layers
2500
+ * <br> - It is recommended to have a visible layer that matches the collision
2501
+ * <br> - Tile layers can be drawn to using their context with canvas2d
2502
+ * <br> - Drawn directly to the main canvas without using WebGL
2503
+ * @namespace TileCollision
2206
2504
  */
2207
2505
 
2208
2506
  'use strict';
2209
2507
 
2210
- ///////////////////////////////////////////////////////////////////////////////
2211
- // Tile Collision
2508
+ /** The tile collision layer array, use setTileCollisionData and getTileCollisionData to access
2509
+ * @memberof TileCollision */
2510
+ let tileCollision = [];
2212
2511
 
2213
- // Internal variables not exposed to documentation
2214
- let tileCollision = [], tileCollisionSize = vec2();
2512
+ /** Size of the tile collision layer
2513
+ * @type {Vector2}
2514
+ * @memberof TileCollision */
2515
+ let tileCollisionSize = vec2();
2215
2516
 
2216
2517
  /** Clear and initialize tile collision
2217
2518
  * @param {Vector2} size
2218
- * @memberof TileLayer */
2519
+ * @memberof TileCollision */
2219
2520
  function initTileCollision(size)
2220
2521
  {
2221
2522
  tileCollisionSize = size;
@@ -2227,14 +2528,14 @@ function initTileCollision(size)
2227
2528
  /** Set tile collision data
2228
2529
  * @param {Vector2} pos
2229
2530
  * @param {Number} [data=0]
2230
- * @memberof TileLayer */
2531
+ * @memberof TileCollision */
2231
2532
  const setTileCollisionData = (pos, data=0)=>
2232
2533
  pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
2233
2534
 
2234
2535
  /** Get tile collision data
2235
2536
  * @param {Vector2} pos
2236
2537
  * @return {Number}
2237
- * @memberof TileLayer */
2538
+ * @memberof TileCollision */
2238
2539
  const getTileCollisionData = (pos)=>
2239
2540
  pos.arrayCheck(tileCollisionSize) ? tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] : 0;
2240
2541
 
@@ -2243,13 +2544,13 @@ const getTileCollisionData = (pos)=>
2243
2544
  * @param {Vector2} [size=new Vector2(1,1)]
2244
2545
  * @param {EngineObject} [object]
2245
2546
  * @return {Boolean}
2246
- * @memberof TileLayer */
2547
+ * @memberof TileCollision */
2247
2548
  function tileCollisionTest(pos, size=vec2(), object)
2248
2549
  {
2249
- const minX = max(Math.floor(pos.x - size.x/2), 0);
2250
- const minY = max(Math.floor(pos.y - size.y/2), 0);
2251
- const maxX = min(pos.x + size.x/2, tileCollisionSize.x-1);
2252
- const maxY = min(pos.y + size.y/2, tileCollisionSize.y-1);
2550
+ const minX = max(pos.x - size.x/2|0, 0);
2551
+ const minY = max(pos.y - size.y/2|0, 0);
2552
+ const maxX = min(pos.x + size.x/2, tileCollisionSize.x);
2553
+ const maxY = min(pos.y + size.y/2, tileCollisionSize.y);
2253
2554
  for (let y = minY; y < maxY; ++y)
2254
2555
  for (let x = minX; x < maxX; ++x)
2255
2556
  {
@@ -2264,7 +2565,7 @@ function tileCollisionTest(pos, size=vec2(), object)
2264
2565
  * @param {Vector2} posEnd
2265
2566
  * @param {EngineObject} [object]
2266
2567
  * @return {Vector2}
2267
- * @memberof TileLayer */
2568
+ * @memberof TileCollision */
2268
2569
  function tileCollisionRaycast(posStart, posEnd, object)
2269
2570
  {
2270
2571
  // test if a ray collides with tiles from start to end
@@ -2298,23 +2599,32 @@ function tileCollisionRaycast(posStart, posEnd, object)
2298
2599
  ///////////////////////////////////////////////////////////////////////////////
2299
2600
  // Tile Layer Rendering System
2300
2601
 
2301
- // Reuse canvas autmatically when destroyed
2302
- const tileLayerCanvasCache = [];
2303
-
2304
- /** Tile layer data object stores info about how to render a tile */
2602
+ /**
2603
+ * Tile layer data object stores info about how to render a tile
2604
+ * @example
2605
+ * // create tile layer data with tile index 0 and random orientation and color
2606
+ * const tileIndex = 0;
2607
+ * const direction = randInt(4)
2608
+ * const mirror = randInt(2);
2609
+ * const color = randColor();
2610
+ * const data = new TileLayerData(tileIndex, direction, mirror, color);
2611
+ */
2305
2612
  class TileLayerData
2306
2613
  {
2307
- /** Create a tile layer data object
2308
- * @param {Number} [tile] - The tile to use, untextured if undefined
2309
- * @param {Number} [direction=0] - Integer direction of tile, in 90 degree increments
2310
- * @param {Boolean} [mirror=0] - If the tile should be mirrored along the x axis
2311
- * @param {Color} [color=new Color(1,1,1)] - Color of the tile
2312
- */
2614
+ /** Create a tile layer data object, one for each tile in a TileLayer
2615
+ * @param {Number} [tile] - The tile to use, untextured if undefined
2616
+ * @param {Number} [direction=0] - Integer direction of tile, in 90 degree increments
2617
+ * @param {Boolean} [mirror=0] - If the tile should be mirrored along the x axis
2618
+ * @param {Color} [color=new Color(1,1,1)] - Color of the tile */
2313
2619
  constructor(tile, direction=0, mirror=0, color=new Color)
2314
2620
  {
2621
+ /** @property {Number} - The tile to use, untextured if undefined */
2315
2622
  this.tile = tile;
2623
+ /** @property {Number} - Integer direction of tile, in 90 degree increments */
2316
2624
  this.direction = direction;
2625
+ /** @property {Boolean} - If the tile should be mirrored along the x axis */
2317
2626
  this.mirror = mirror;
2627
+ /** @property {Color} - Color of the tile */
2318
2628
  this.color = color;
2319
2629
  }
2320
2630
 
@@ -2322,44 +2632,49 @@ class TileLayerData
2322
2632
  clear() { this.tile = this.direction = this.mirror = 0; color = new Color; }
2323
2633
  }
2324
2634
 
2325
- /** Tile layer object - cached rendering system for tile layers */
2635
+ /**
2636
+ * Tile layer object - cached rendering system for tile layers
2637
+ * <br> - Each Tile layer is rendered to an off screen canvas
2638
+ * <br> - To allow dynamic modifications, layers are rendered using canvas 2d
2639
+ * <br> - Some devices like mobile phones are limited to 4k texture resolution
2640
+ * <br> - So with 16x16 tiles this limits layers to 256x256 on mobile devices
2641
+ * @extends EngineObject
2642
+ * @example
2643
+ * // create tile collision and visible tile layer
2644
+ * initTileCollision(vec2(200,100));
2645
+ * const tileLayer = new TileLayer();
2646
+ */
2326
2647
  class TileLayer extends EngineObject
2327
2648
  {
2328
- /** Create a tile layer data object
2649
+ /** Create a tile layer object
2329
2650
  * @param {Vector2} [position=new Vector2(0,0)] - World space position
2330
- * @param {Vector2} [size=defaultObjectSize] - World space size
2331
- * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this in world space
2332
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2651
+ * @param {Vector2} [size=objectDefaultSize] - World space size
2652
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2653
+ * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2654
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2333
2655
  */
2334
- constructor(pos, size, scale=vec2(1), renderOrder=0)
2656
+ constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2335
2657
  {
2336
- super(pos, size);
2658
+ super(pos, size, -1, tileSize, 0, undefined, renderOrder);
2337
2659
 
2338
- // create new canvas if necessary
2339
- this.canvas = tileLayerCanvasCache.length ? tileLayerCanvasCache.pop() : document.createElement('canvas');
2660
+ /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
2661
+ this.canvas = document.createElement('canvas');
2662
+ /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
2340
2663
  this.context = this.canvas.getContext('2d');
2664
+ /** @property {Vector2} - How much to scale this layer when rendered */
2341
2665
  this.scale = scale;
2342
- this.tileSize = defaultTileSize.copy();
2343
- this.renderOrder = renderOrder;
2344
- this.flushGLBeforeRender = 1;
2666
+ /** @property {Boolean} [isOverlay=0] - If true this layer will render to overlay canvas and appear above all objects */
2667
+ this.isOverlay;
2345
2668
 
2346
2669
  // init tile data
2347
2670
  this.data = [];
2348
2671
  for (let j = this.size.area(); j--;)
2349
2672
  this.data.push(new TileLayerData());
2350
2673
  }
2351
-
2352
- /** Destroy this tile layer */
2353
- destroy()
2354
- {
2355
- // add canvas back to the cache
2356
- tileLayerCanvasCache.push(this.canvas);
2357
- super.destroy();
2358
- }
2359
2674
 
2360
2675
  /** Set data at a given position in the array
2361
- * @param {Vector2} position - Local position in array
2362
- * @param {TileLayerData} data - Data to set
2676
+ * @param {Vector2} position - Local position in array
2677
+ * @param {TileLayerData} data - Data to set
2363
2678
  * @param {Boolean} [redraw=0] - Force the tile to redraw if true */
2364
2679
  setData(layerPos, data, redraw)
2365
2680
  {
@@ -2384,50 +2699,48 @@ class TileLayer extends EngineObject
2384
2699
  {
2385
2700
  ASSERT(mainContext != this.context); // must call redrawEnd() after drawing tiles
2386
2701
 
2387
- // flush and copy gl canvas because tile canvas does not use gl
2388
- this.flushGLBeforeRender && glEnable && glCopyToContext(mainContext);
2702
+ // flush and copy gl canvas because tile canvas does not use webgl
2703
+ glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
2389
2704
 
2390
- // draw the entire cached level onto the main canvas
2705
+ // draw the entire cached level onto the canvas
2391
2706
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
2392
- mainContext.drawImage
2707
+ (this.isOverlay ? overlayContext : mainContext).drawImage
2393
2708
  (
2394
2709
  this.canvas, pos.x, pos.y,
2395
2710
  cameraScale*this.size.x*this.scale.x, cameraScale*this.size.y*this.scale.y
2396
2711
  );
2397
2712
  }
2398
2713
 
2399
- /** Draw all the tile data to an offscreen canvas using webgl if possible */
2714
+ /** Draw all the tile data to an offscreen canvas
2715
+ * - This may be slow in some browsers
2716
+ */
2400
2717
  redraw()
2401
2718
  {
2402
- this.redrawStart();
2719
+ this.redrawStart(1);
2403
2720
  this.drawAllTileData();
2404
2721
  this.redrawEnd();
2405
2722
  }
2406
2723
 
2407
2724
  /** Call to start the redraw process
2408
- * @param {Boolean} [clear=1] - Should it clear the canvas before drawing */
2409
- redrawStart(clear = 1)
2725
+ * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
2726
+ redrawStart(clear = 0)
2410
2727
  {
2411
- // clear and set size
2412
- const width = this.size.x * this.tileSize.x;
2413
- const height = this.size.y * this.tileSize.y;
2414
2728
  if (clear)
2415
2729
  {
2416
- this.canvas.width = width;
2417
- this.canvas.height = height;
2730
+ // clear and set size
2731
+ this.canvas.width = this.size.x * this.tileSize.x;
2732
+ this.canvas.height = this.size.y * this.tileSize.y;
2418
2733
  }
2419
2734
 
2420
2735
  // save current render settings
2421
- this.savedRenderSettings = [mainCanvasSize, mainCanvas, mainContext, cameraScale, cameraPos];
2736
+ this.savedRenderSettings = [mainCanvas, mainContext, cameraPos, cameraScale];
2422
2737
 
2423
- // set camera transform for renering
2424
- cameraScale = this.tileSize.x;
2425
- cameraPos = this.size.scale(.5);
2738
+ // use normal rendering system to render the tiles
2426
2739
  mainCanvas = this.canvas;
2427
2740
  mainContext = this.context;
2428
- mainContext.imageSmoothingEnabled = !pixelated; // disable smoothing for pixel art
2429
- mainCanvasSize = vec2(width, height);
2430
- glPreRender(width, height);
2741
+ cameraPos = this.size.scale(.5);
2742
+ cameraScale = this.tileSize.x;
2743
+ enginePreRender();
2431
2744
  }
2432
2745
 
2433
2746
  /** Call to end the redraw process */
@@ -2438,7 +2751,7 @@ class TileLayer extends EngineObject
2438
2751
  //debugSaveCanvas(this.canvas);
2439
2752
 
2440
2753
  // set stuff back to normal
2441
- [mainCanvasSize, mainCanvas, mainContext, cameraScale, cameraPos] = this.savedRenderSettings;
2754
+ [mainCanvas, mainContext, cameraPos, cameraScale] = this.savedRenderSettings;
2442
2755
  }
2443
2756
 
2444
2757
  /** Draw the tile at a given position
@@ -2466,13 +2779,13 @@ class TileLayer extends EngineObject
2466
2779
  this.drawTileData(vec2(x,y));
2467
2780
  }
2468
2781
 
2469
- /** Draw directly to the 2d canvas in world space (bipass webgl)
2782
+ /** Draw directly to the 2D canvas in world space (bipass webgl)
2470
2783
  * @param {Vector2} pos
2471
2784
  * @param {Vector2} size
2472
- * @param {Number} angle
2473
- * @param {Boolean} mirror
2785
+ * @param {Number} [angle=0]
2786
+ * @param {Boolean} [mirror=0]
2474
2787
  * @param {Function} drawFunction */
2475
- drawCanvas2D(pos, size, angle, mirror, drawFunction)
2788
+ drawCanvas2D(pos, size, angle=0, mirror, drawFunction)
2476
2789
  {
2477
2790
  const context = this.context;
2478
2791
  context.save();
@@ -2480,7 +2793,7 @@ class TileLayer extends EngineObject
2480
2793
  size = size.multiply(this.tileSize);
2481
2794
  context.translate(pos.x, this.canvas.height - pos.y);
2482
2795
  context.rotate(angle);
2483
- context.scale(mirror?-size.x:size.x, size.y);
2796
+ context.scale(mirror ? -size.x : size.x, size.y);
2484
2797
  drawFunction(context);
2485
2798
  context.restore();
2486
2799
  }
@@ -2489,11 +2802,11 @@ class TileLayer extends EngineObject
2489
2802
  * @param {Vector2} pos
2490
2803
  * @param {Vector2} [size=new Vector2(1,1)]
2491
2804
  * @param {Number} [tileIndex=-1]
2492
- * @param {Vector2} [tileSize=defaultTileSize]
2805
+ * @param {Vector2} [tileSize=tileSizeDefault]
2493
2806
  * @param {Color} [color=new Color(1,1,1)]
2494
2807
  * @param {Number} [angle=0]
2495
2808
  * @param {Boolean} [mirror=0] */
2496
- drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=defaultTileSize, color=new Color, angle=0, mirror)
2809
+ drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color, angle, mirror)
2497
2810
  {
2498
2811
  this.drawCanvas2D(pos, size, angle, mirror, (context)=>
2499
2812
  {
@@ -2506,7 +2819,7 @@ class TileLayer extends EngineObject
2506
2819
  else
2507
2820
  {
2508
2821
  const cols = tileImage.width/tileSize.x;
2509
- context.globalAlpha = color.a; // full color not supported in this mode
2822
+ context.globalAlpha = color.a; // only alpha, no color, is supported in this mode
2510
2823
  context.drawImage(tileImage,
2511
2824
  (tileIndex%cols)*tileSize.x, (tileIndex/cols|0)*tileSize.x,
2512
2825
  tileSize.x, tileSize.y, -.5, -.5, 1, 1);
@@ -2519,7 +2832,7 @@ class TileLayer extends EngineObject
2519
2832
  * @param {Vector2} [size=new Vector2(1,1)]
2520
2833
  * @param {Color} [color=new Color(1,1,1)]
2521
2834
  * @param {Number} [angle=0] */
2522
- drawRect(pos, size, color, angle) { this.drawTile(pos, size, -1, 0, color, angle, 0); }
2835
+ drawRect(pos, size, color, angle) { this.drawTile(pos, size, -1, 0, color, angle); }
2523
2836
  }
2524
2837
  /*
2525
2838
  LittleJS Particle System
@@ -2532,37 +2845,50 @@ class TileLayer extends EngineObject
2532
2845
 
2533
2846
  /**
2534
2847
  * Particle Emitter - Spawns particles with the given settings
2848
+ * @extends EngineObject
2849
+ * @example
2850
+ * // create a particle emitter
2851
+ * let pos = vec2(2,3);
2852
+ * let particleEmiter = new ParticleEmitter
2853
+ * (
2854
+ * pos, 1, 0, 500, PI, // pos, emitSize, emitTime, emitRate, emiteCone
2855
+ * 0, vec2(16), // tileIndex, tileSize
2856
+ * new Color, new Color(0,0,0), // colorStartA, colorStartB
2857
+ * new Color(1,1,1,0), new Color(0,0,0,0), // colorEndA, colorEndB
2858
+ * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
2859
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
2860
+ * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
2861
+ * );
2535
2862
  */
2536
2863
  class ParticleEmitter extends EngineObject
2537
2864
  {
2538
- /**
2539
- * Create a particle system with the given settings
2540
- * @param {Vector2} position - World space position of the emitter
2541
- * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
2542
- * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
2543
- * @param {Number} [emitRate=100] - How many particles per second to spawn
2544
- * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
2545
- * @param {Number} [tileIndex=-1] - Index into tile sheet, if <0 no texture is applied
2546
- * @param {Number} [tileSize=defaultTileSize] - Tile size for particles
2547
- * @param {Color} [colorStartA=new Color(1,1,1)] - Color at start of life 1, randomized between start colors
2548
- * @param {Color} [colorStartB=new Color(1,1,1)] - Color at start of life 2, randomized between start colors
2549
- * @param {Color} [colorEndA=new Color(1,1,1,0)] - Color at end of life 1, randomized between end colors
2550
- * @param {Color} [colorEndB=new Color(1,1,1,0)] - Color at end of life 2, randomized between end colors
2551
- * @param {Number} [particleTime=.5] - How long particles live
2552
- * @param {Number} [sizeStart=.1] - How big are particles at start
2553
- * @param {Number} [sizeEnd=1] - How big are particles at end
2554
- * @param {Number} [speed=.1] - How fast are particles when spawned
2555
- * @param {Number} [angleSpeed=.05] - How fast are particles rotating
2556
- * @param {Number} [damping=1] - How much to dampen particle speed
2557
- * @param {Number} [angleDamping=1] - How much to dampen particle angular speed
2558
- * @param {Number} [gravityScale=0] - How much does gravity effect particles
2559
- * @param {Number} [particleConeAngle=PI] - Cone for start particle angle
2560
- * @param {Number} [fadeRate=.1] - How quick to fade in particles at start/end in percent of life
2561
- * @param {Number} [randomness=.2] - Apply extra randomness percent
2562
- * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
2563
- * @param {Boolean} [additive=0] - Should particles use addtive blend
2564
- * @param {Boolean} [randomColorLinear=0] - Should color be randomized linearly or across each component
2565
- * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
2865
+ /** Create a particle system with the given settings
2866
+ * @param {Vector2} position - World space position of the emitter
2867
+ * @param {Number} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
2868
+ * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
2869
+ * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
2870
+ * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
2871
+ * @param {Number} [tileIndex=-1] - Index into tile sheet, if <0 no texture is applied
2872
+ * @param {Number} [tileSize=tileSizeDefault] - Tile size for particles
2873
+ * @param {Color} [colorStartA=new Color(1,1,1)] - Color at start of life 1, randomized between start colors
2874
+ * @param {Color} [colorStartB=new Color(1,1,1)] - Color at start of life 2, randomized between start colors
2875
+ * @param {Color} [colorEndA=new Color(1,1,1,0)] - Color at end of life 1, randomized between end colors
2876
+ * @param {Color} [colorEndB=new Color(1,1,1,0)] - Color at end of life 2, randomized between end colors
2877
+ * @param {Number} [particleTime=.5] - How long particles live
2878
+ * @param {Number} [sizeStart=.1] - How big are particles at start
2879
+ * @param {Number} [sizeEnd=1] - How big are particles at end
2880
+ * @param {Number} [speed=.1] - How fast are particles when spawned
2881
+ * @param {Number} [angleSpeed=.05] - How fast are particles rotating
2882
+ * @param {Number} [damping=1] - How much to dampen particle speed
2883
+ * @param {Number} [angleDamping=1] - How much to dampen particle angular speed
2884
+ * @param {Number} [gravityScale=0] - How much does gravity effect particles
2885
+ * @param {Number} [particleConeAngle=PI] - Cone for start particle angle
2886
+ * @param {Number} [fadeRate=.1] - How quick to fade in particles at start/end in percent of life
2887
+ * @param {Number} [randomness=.2] - Apply extra randomness percent
2888
+ * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
2889
+ * @param {Boolean} [additive=0] - Should particles use addtive blend
2890
+ * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
2891
+ * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
2566
2892
  */
2567
2893
  constructor
2568
2894
  (
@@ -2572,7 +2898,7 @@ class ParticleEmitter extends EngineObject
2572
2898
  emitRate = 100,
2573
2899
  emitConeAngle = PI,
2574
2900
  tileIndex = -1,
2575
- tileSize = defaultTileSize,
2901
+ tileSize = tileSizeDefault,
2576
2902
  colorStartA = new Color,
2577
2903
  colorStartB = new Color,
2578
2904
  colorEndA = new Color(1,1,1,0),
@@ -2594,37 +2920,61 @@ class ParticleEmitter extends EngineObject
2594
2920
  renderOrder = additive ? 1e9 : 0
2595
2921
  )
2596
2922
  {
2597
- super(pos, new Vector2, tileIndex, tileSize);
2923
+ super(pos, new Vector2, tileIndex, tileSize, 0, undefined, renderOrder);
2598
2924
 
2599
2925
  // emitter settings
2926
+ /** @property {Number} - World space size of the emitter (float for circle diameter, vec2 for rect) */
2600
2927
  this.emitSize = emitSize
2928
+ /** @property {Number} - How long to stay alive (0 is forever) */
2601
2929
  this.emitTime = emitTime;
2930
+ /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
2602
2931
  this.emitRate = emitRate;
2932
+ /** @property {Number} - Local angle to apply velocity to particles from emitter */
2603
2933
  this.emitConeAngle = emitConeAngle;
2604
2934
 
2605
2935
  // color settings
2936
+ /** @property {Color} - Color at start of life 1, randomized between start colors */
2606
2937
  this.colorStartA = colorStartA;
2938
+ /** @property {Color} - Color at start of life 2, randomized between start colors */
2607
2939
  this.colorStartB = colorStartB;
2940
+ /** @property {Color} - Color at end of life 1, randomized between end colors */
2608
2941
  this.colorEndA = colorEndA;
2942
+ /** @property {Color} - Color at end of life 2, randomized between end colors */
2609
2943
  this.colorEndB = colorEndB;
2944
+ /** @property {Boolean} - Should color be randomized linearly or across each component */
2610
2945
  this.randomColorLinear = randomColorLinear;
2611
2946
 
2612
2947
  // particle settings
2948
+ /** @property {Number} - How long particles live */
2613
2949
  this.particleTime = particleTime;
2950
+ /** @property {Number} - How big are particles at start */
2614
2951
  this.sizeStart = sizeStart;
2952
+ /** @property {Number} - How big are particles at end */
2615
2953
  this.sizeEnd = sizeEnd;
2954
+ /** @property {Number} - How fast are particles when spawned */
2616
2955
  this.speed = speed;
2956
+ /** @property {Number} - How fast are particles rotating */
2617
2957
  this.angleSpeed = angleSpeed;
2958
+ /** @property {Number} - How much to dampen particle speed */
2618
2959
  this.damping = damping;
2960
+ /** @property {Number} - How much to dampen particle angular speed */
2619
2961
  this.angleDamping = angleDamping;
2962
+ /** @property {Number} - How much does gravity effect particles */
2620
2963
  this.gravityScale = gravityScale;
2964
+ /** @property {Number} - Cone for start particle angle */
2621
2965
  this.particleConeAngle = particleConeAngle;
2966
+ /** @property {Number} - How quick to fade in particles at start/end in percent of life */
2622
2967
  this.fadeRate = fadeRate;
2968
+ /** @property {Number} - Apply extra randomness percent */
2623
2969
  this.randomness = randomness;
2970
+ /** @property {Number} - Do particles collide against tiles */
2624
2971
  this.collideTiles = collideTiles;
2972
+ /** @property {Number} - Should particles use addtive blend */
2625
2973
  this.additive = additive;
2626
- this.renderOrder = renderOrder;
2627
- this.trailScale =
2974
+ /** @property {Number} - If set the partile is drawn as a trail, stretched in the drection of velocity */
2975
+ this.trailScale = 0;
2976
+
2977
+ // internal variables
2628
2978
  this.emitTimeBuffer = 0;
2629
2979
  }
2630
2980
 
@@ -2638,9 +2988,9 @@ class ParticleEmitter extends EngineObject
2638
2988
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
2639
2989
  {
2640
2990
  // emit particles
2641
- if (this.emitRate)
2991
+ if (this.emitRate * particleEmitRateScale)
2642
2992
  {
2643
- const rate = 1/this.emitRate;
2993
+ const rate = 1/this.emitRate/particleEmitRateScale;
2644
2994
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
2645
2995
  this.emitParticle();
2646
2996
  }
@@ -2667,34 +3017,34 @@ class ParticleEmitter extends EngineObject
2667
3017
  const randomizeScale = (v)=> v + v*rand(randomness, -randomness);
2668
3018
 
2669
3019
  // randomize particle settings
2670
- const particleTime = randomizeScale(this.particleTime);
2671
- const sizeStart = randomizeScale(this.sizeStart);
2672
- const sizeEnd = randomizeScale(this.sizeEnd);
2673
- const speed = randomizeScale(this.speed);
2674
- const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
2675
- const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
2676
- const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
2677
- const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3020
+ const particleTime = randomizeScale(this.particleTime);
3021
+ const sizeStart = randomizeScale(this.sizeStart);
3022
+ const sizeEnd = randomizeScale(this.sizeEnd);
3023
+ const speed = randomizeScale(this.speed);
3024
+ const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
3025
+ const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
3026
+ const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3027
+ const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
2678
3028
 
2679
3029
  // build particle settings
2680
- particle.colorStart = colorStart;
2681
- particle.colorEndDelta = colorEnd.subtract(colorStart);
2682
- particle.velocity = (new Vector2).setAngle(this.angle + coneAngle, speed);
2683
- particle.angleVelocity = angleSpeed;
2684
- particle.lifeTime = particleTime;
2685
- particle.sizeStart = sizeStart;
2686
- particle.sizeEndDelta = sizeEnd - sizeStart;
2687
- particle.fadeRate = this.fadeRate;
2688
- particle.damping = this.damping;
2689
- particle.angleDamping = this.angleDamping;
2690
- particle.elasticity = this.elasticity;
2691
- particle.friction = this.friction;
2692
- particle.gravityScale = this.gravityScale;
2693
- particle.collideTiles = this.collideTiles;
2694
- particle.additive = this.additive;
2695
- particle.renderOrder = this.renderOrder;
2696
- particle.trailScale = this.trailScale;
2697
- particle.mirror = rand()<.5;
3030
+ particle.colorStart = colorStart;
3031
+ particle.colorEndDelta = colorEnd.subtract(colorStart);
3032
+ particle.velocity = (new Vector2).setAngle(this.angle + coneAngle, speed);
3033
+ particle.angleVelocity = angleSpeed;
3034
+ particle.lifeTime = particleTime;
3035
+ particle.sizeStart = sizeStart;
3036
+ particle.sizeEndDelta = sizeEnd - sizeStart;
3037
+ particle.fadeRate = this.fadeRate;
3038
+ particle.damping = this.damping;
3039
+ particle.angleDamping = this.angleDamping;
3040
+ particle.elasticity = this.elasticity;
3041
+ particle.friction = this.friction;
3042
+ particle.gravityScale = this.gravityScale;
3043
+ particle.collideTiles = this.collideTiles;
3044
+ particle.additive = this.additive;
3045
+ particle.renderOrder = this.renderOrder;
3046
+ particle.trailScale = this.trailScale;
3047
+ particle.mirror = rand()<.5;
2698
3048
 
2699
3049
  // setup callbacks for particles
2700
3050
  particle.destroyCallback = this.particleDestroyCallback;
@@ -2711,15 +3061,16 @@ class ParticleEmitter extends EngineObject
2711
3061
  ///////////////////////////////////////////////////////////////////////////////
2712
3062
  /**
2713
3063
  * Particle Object - Created automatically by Particle Emitters
3064
+ * @extends EngineObject
2714
3065
  */
2715
3066
  class Particle extends EngineObject
2716
3067
  {
2717
3068
  /**
2718
3069
  * Create a particle with the given settings
2719
3070
  * @param {Vector2} position - World space position of the particle
2720
- * @param {Number} [tileIndex=-1] - Tile to use to render, untextured if -1
2721
- * @param {Vector2} [tileSize=defaultTileSize] - Size of tile in source pixels
2722
- * @param {Number} [angle=0] - Angle to rotate the particle
3071
+ * @param {Number} [tileIndex=-1] - Tile to use to render, untextured if -1
3072
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
3073
+ * @param {Number} [angle=0] - Angle to rotate the particle
2723
3074
  */
2724
3075
  constructor(pos, tileIndex, tileSize, angle) { super(pos, new Vector2, tileIndex, tileSize, angle); }
2725
3076
 
@@ -2766,11 +3117,11 @@ class Particle extends EngineObject
2766
3117
  }
2767
3118
  }
2768
3119
  /**
2769
- * LittleJS Medal System
2770
- * <br> - Tracks and displays medals
2771
- * <br> - Saves medals to local storage
2772
- * <br> - Newgrounds and OS13k integration
2773
- * @namespace Medals
3120
+ * LittleJS Medal System
3121
+ * <br> - Tracks and displays medals
3122
+ * <br> - Saves medals to local storage
3123
+ * <br> - Newgrounds and OS13k integration
3124
+ * @namespace Medals
2774
3125
  */
2775
3126
 
2776
3127
  'use strict';
@@ -2789,32 +3140,42 @@ let medalsPreventUnlock;
2789
3140
  let newgrounds;
2790
3141
 
2791
3142
  // Engine internal variables not exposed to documentation
2792
- let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimer;
3143
+ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
2793
3144
 
2794
3145
  ///////////////////////////////////////////////////////////////////////////////
2795
3146
 
2796
3147
  /** Initialize medals with a save name used for storage
2797
- * <br> - Checks if medals are unlocked
2798
3148
  * <br> - Call this after creating all medals
3149
+ * <br> - Checks if medals are unlocked
2799
3150
  * @param {String} saveName
2800
3151
  * @memberof Medals */
2801
3152
  function medalsInit(saveName)
2802
3153
  {
2803
3154
  // check if medals are unlocked
2804
3155
  medalsSaveName = saveName;
2805
- debugMedals || medals.forEach(medal=> medal.unlocked = localStorage[medal.storageKey()]);
3156
+ debugMedals || medals.forEach(medal=> medal.unlocked = 1 || localStorage[medal.storageKey()]);
2806
3157
  }
2807
3158
 
2808
- /** Medal Object - Tracks an unlockable medal */
3159
+ /**
3160
+ * Medal Object - Tracks an unlockable medal
3161
+ * @example
3162
+ * // create a medal
3163
+ * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
3164
+ *
3165
+ * // initialize medals
3166
+ * medalsInit('Example Game');
3167
+ *
3168
+ * // unlock the medal
3169
+ * medal_example.unlock();
3170
+ */
2809
3171
  class Medal
2810
3172
  {
2811
- /**
2812
- * Create an medal object and adds it to the list of medals
2813
- * @param {Number} id - The unique identifier of the medal
2814
- * @param {String} name - Name of the medal
2815
- * @param {String} [description] - Description of the medal
2816
- * @param {String} [icon='🏆'] - Icon for the medal
2817
- * @param {String} [src] - Image location for the medal
3173
+ /** Create an medal object and adds it to the list of medals
3174
+ * @param {Number} id - The unique identifier of the medal
3175
+ * @param {String} name - Name of the medal
3176
+ * @param {String} [description] - Description of the medal
3177
+ * @param {String} [icon='🏆'] - Icon for the medal
3178
+ * @param {String} [src] - Image location for the medal
2818
3179
  */
2819
3180
  constructor(id, name, description='', icon='🏆', src)
2820
3181
  {
@@ -2825,13 +3186,9 @@ class Medal
2825
3186
  this.name = name;
2826
3187
  this.description = description;
2827
3188
  this.icon = icon;
2828
-
3189
+ this.image = new Image();
2829
3190
  if (src)
2830
- {
2831
- // load image
2832
- this.image = new Image();
2833
3191
  this.image.src = src;
2834
- }
2835
3192
  }
2836
3193
 
2837
3194
  /** Unlocks a medal if not already unlocked */
@@ -2841,7 +3198,7 @@ class Medal
2841
3198
  return;
2842
3199
 
2843
3200
  // save the medal
2844
- ASSERT(medalsSaveName); // game name must be set
3201
+ ASSERT(medalsSaveName); // save name must be set
2845
3202
  localStorage[this.storageKey()] = this.unlocked = 1;
2846
3203
  medalsDisplayQueue.push(this);
2847
3204
 
@@ -2856,27 +3213,28 @@ class Medal
2856
3213
  render(hidePercent=0)
2857
3214
  {
2858
3215
  const context = overlayContext;
2859
- const x = overlayCanvas.width - medalDisplayWidth;
3216
+ const width = min(medalDisplayWidth, mainCanvas.width);
3217
+ const x = overlayCanvas.width - width;
2860
3218
  const y = -medalDisplayHeight*hidePercent;
2861
3219
 
2862
3220
  // draw containing rect and clip to that region
2863
3221
  context.save();
2864
3222
  context.beginPath();
2865
3223
  context.fillStyle = '#ddd'
2866
- context.fill(context.rect(x, y, medalDisplayWidth, medalDisplayHeight));
2867
- context.strokeStyle = context.fillStyle = '#000';
2868
- context.lineWidth = 2;
3224
+ context.fill(context.rect(x, y, width, medalDisplayHeight));
3225
+ context.strokeStyle = '#000';
3226
+ context.lineWidth = 3;
2869
3227
  context.stroke();
2870
3228
  context.clip();
2871
3229
 
3230
+ // draw the icon and text
2872
3231
  this.renderIcon(x+15+medalDisplayIconSize/2, y+medalDisplayHeight/2);
2873
-
2874
- // draw the text
2875
3232
  context.textAlign = 'left';
2876
- context.font = '3em '+ defaultFont;
2877
- context.fillText(this.name, x+medalDisplayIconSize+25, y+35);
2878
- context.font = '1.5em '+ defaultFont;
2879
- context.restore(context.fillText(this.description, x+medalDisplayIconSize+25, y+70));
3233
+ context.font = '38px '+ fontDefault;
3234
+ context.fillText(this.name, x+medalDisplayIconSize+30, y+28);
3235
+ context.font = '24px '+ fontDefault;
3236
+ context.fillText(this.description, x+medalDisplayIconSize+30, y+60);
3237
+ context.restore();
2880
3238
  }
2881
3239
 
2882
3240
  /** Render the icon for a medal
@@ -2888,21 +3246,18 @@ class Medal
2888
3246
  {
2889
3247
  // draw the image or icon
2890
3248
  const context = overlayContext;
3249
+ context.fillStyle = '#000';
2891
3250
  context.textAlign = 'center';
2892
3251
  context.textBaseline = 'middle';
2893
- context.font = size*.6 + 'px '+ defaultFont;
2894
- context.fillStyle = '#000';
2895
- if (this.image)
3252
+ context.font = size*.7 + 'px '+ fontDefault;
3253
+ if (this.image.src)
2896
3254
  context.drawImage(this.image, x-size/2, y-size/2, size, size);
2897
3255
  else
2898
3256
  context.fillText(this.icon, x, y); // show icon if there is no image
2899
3257
  }
2900
3258
 
2901
3259
  // Get local storage key used by the medal
2902
- storageKey()
2903
- {
2904
- return medalsSaveName + '_medal_' + this.id;
2905
- }
3260
+ storageKey() { return medalsSaveName + '_' + this.id; }
2906
3261
  }
2907
3262
 
2908
3263
  // engine automatically renders medals
@@ -2913,11 +3268,11 @@ function medalsRender()
2913
3268
 
2914
3269
  // update first medal in queue
2915
3270
  const medal = medalsDisplayQueue[0];
2916
- const time = timeReal - medalsDisplayTimer;
2917
- if (!medalsDisplayTimer)
2918
- medalsDisplayTimer = timeReal;
3271
+ const time = timeReal - medalsDisplayTimeLast;
3272
+ if (!medalsDisplayTimeLast)
3273
+ medalsDisplayTimeLast = timeReal;
2919
3274
  else if (time > medalDisplayTime)
2920
- medalsDisplayQueue.shift(medalsDisplayTimer = 0);
3275
+ medalsDisplayQueue.shift(medalsDisplayTimeLast = 0);
2921
3276
  else
2922
3277
  {
2923
3278
  // slide on/off medals
@@ -2931,14 +3286,19 @@ function medalsRender()
2931
3286
 
2932
3287
  ///////////////////////////////////////////////////////////////////////////////
2933
3288
 
2934
- /** Newgrounds API wrapper object */
3289
+ /**
3290
+ * Newgrounds API wrapper object
3291
+ * @example
3292
+ * // create a newgrounds object, replace the app id and cipher with your own
3293
+ * const app_id = '53123:1ZuSTQ9l';
3294
+ * const cipher = 'enF0vGH@Mj/FRASKL23Q==';
3295
+ * newgrounds = new Newgrounds(app_id, cipher);
3296
+ */
2935
3297
  class Newgrounds
2936
3298
  {
2937
- /**
2938
- * Create a newgrounds object
2939
- * @param {Number} app_id - The newgrounds App ID
2940
- * @param {String} [cipher] - The encryption Key (AES-128/Base64)
2941
- */
3299
+ /** Create a newgrounds object
3300
+ * @param {Number} app_id - The newgrounds App ID
3301
+ * @param {String} [cipher] - The encryption Key (AES-128/Base64) */
2942
3302
  constructor(app_id, cipher)
2943
3303
  {
2944
3304
  ASSERT(!newgrounds && app_id);
@@ -2966,7 +3326,6 @@ class Newgrounds
2966
3326
  if (medal)
2967
3327
  {
2968
3328
  // copy newgrounds medal data
2969
- medal.image = new Image();
2970
3329
  medal.image.src = newgroundsMedal['icon'];
2971
3330
  medal.name = newgroundsMedal['name'];
2972
3331
  medal.description = newgroundsMedal['description'];
@@ -2990,7 +3349,7 @@ class Newgrounds
2990
3349
  unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, 1); }
2991
3350
 
2992
3351
  /** Send message to post score
2993
- * @param {Number} id - The scoreboard id
3352
+ * @param {Number} id - The scoreboard id
2994
3353
  * @param {Number} value - The score value */
2995
3354
  postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, 1); }
2996
3355
 
@@ -2998,21 +3357,21 @@ class Newgrounds
2998
3357
  logView() { return this.call('App.logView', {'host':this.host}, 1); }
2999
3358
 
3000
3359
  /** Get scores from a scoreboard
3001
- * @param {Number} id - The scoreboard id
3002
- * @param {String} [user=0] - A user's id or name
3360
+ * @param {Number} id - The scoreboard id
3361
+ * @param {String} [user=0] - A user's id or name
3003
3362
  * @param {Number} [social=0] - If true, only social scores will be loaded
3004
- * @param {Number} [skip=0] - Number of scores to skip before start
3363
+ * @param {Number} [skip=0] - Number of scores to skip before start
3005
3364
  * @param {Number} [limit=10] - Number of scores to include in the list
3006
- * @return {Object} - The response JSON object
3365
+ * @return {Object} - The response JSON object
3007
3366
  */
3008
3367
  getScores(id, user=0, social=0, skip=0, limit=10)
3009
3368
  { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
3010
3369
 
3011
3370
  /** Send a message to call a component of the Newgrounds API
3012
- * @param {String} component - Name of the component
3371
+ * @param {String} component - Name of the component
3013
3372
  * @param {Object} [parameters=0] - Parameters to use for call
3014
- * @param {Boolean} [async=0] - If true, wait for response before continuing (will cause stall)
3015
- * @return {Object} - The response JSON object
3373
+ * @param {Boolean} [async=0] - If true, don't wait for response before continuing (avoid stall)
3374
+ * @return {Object} - The response JSON object
3016
3375
  */
3017
3376
  call(component, parameters=0, async=0)
3018
3377
  {
@@ -3056,14 +3415,14 @@ class Newgrounds
3056
3415
 
3057
3416
  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));
3058
3417
  /**
3059
- * LittleJS WebGL Interface
3060
- * <br> - All webgl used by the engine is wrapped up here
3061
- * <br> - Can be disabled with glEnable to revert to 2D canvas rendering
3062
- * <br> - Batches sprite rendering on GPU for incredibly fast performance
3063
- * <br> - Sprite transform math is done in the shader where possible
3064
- * <br> - For normal stuff you won't need to call any functions in this file
3065
- * <br> - For advanced stuff there are helper functions to create shaders, textures, etc
3066
- * @namespace WebGL
3418
+ * LittleJS WebGL Interface
3419
+ * <br> - All webgl used by the engine is wrapped up here
3420
+ * <br> - For normal stuff you won't need to see or call anything in this file
3421
+ * <br> - For advanced stuff there are helper functions to create shaders, textures, etc
3422
+ * <br> - Can be disabled with glEnable to revert to 2D canvas rendering
3423
+ * <br> - Batches sprite rendering on GPU for incredibly fast performance
3424
+ * <br> - Sprite transform math is done in the shader where possible
3425
+ * @namespace WebGL
3067
3426
  */
3068
3427
 
3069
3428
  'use strict';
@@ -3084,7 +3443,7 @@ let glContext;
3084
3443
  let glTileTexture;
3085
3444
 
3086
3445
  // WebGL internal variables not exposed to documentation
3087
- let glActiveTexture, glShader, glPositionData, glColorData, glBatchCount, glDirty, glAdditive;
3446
+ let glActiveTexture, glShader, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
3088
3447
 
3089
3448
  ///////////////////////////////////////////////////////////////////////////////
3090
3449
 
@@ -3095,19 +3454,16 @@ function glInit()
3095
3454
 
3096
3455
  // create the canvas and tile texture
3097
3456
  glCanvas = document.createElement('canvas');
3098
- glContext = glCanvas.getContext('webgl', {antialias:!pixelated});
3457
+ glContext = glCanvas.getContext('webgl');
3458
+ glCanvas.style = styleCanvas;
3099
3459
  glTileTexture = glCreateTexture(tileImage);
3100
3460
 
3101
- if (glOverlay)
3102
- {
3103
- // firefox is much faster without copying the gl buffer so we just overlay it with some tradeoffs
3104
- document.body.appendChild(glCanvas);
3105
- glCanvas.style = mainCanvas.style.cssText;
3106
- }
3461
+ // some browsers are much faster without copying the gl buffer so we just overlay it instead
3462
+ glOverlay && document.body.appendChild(glCanvas);
3107
3463
 
3108
3464
  // setup vertex and fragment shaders
3109
3465
  glShader = glCreateProgram(
3110
- 'precision lowp float;'+ // use lowp for better performance
3466
+ 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3111
3467
  'uniform mat4 m;'+ // transform matrix
3112
3468
  'attribute float a;'+ // angle
3113
3469
  'attribute vec2 p,s,t;'+ // position, size, uv
@@ -3119,7 +3475,7 @@ function glInit()
3119
3475
  'v=t;d=c;e=b;'+ // pass stuff to fragment shader
3120
3476
  '}' // end of shader
3121
3477
  ,
3122
- 'precision lowp float;'+ // use lowp for better performance
3478
+ 'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3123
3479
  'varying vec2 v;'+ // uv
3124
3480
  'varying vec4 d,e;'+ // color, additiveColor
3125
3481
  'uniform sampler2D j;'+ // texture
@@ -3135,6 +3491,7 @@ function glInit()
3135
3491
  glColorData = new Uint32Array(glVertexData);
3136
3492
 
3137
3493
  // setup the vertex data array
3494
+ let offset = glBatchCount = 0;
3138
3495
  const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
3139
3496
  {
3140
3497
  const location = glContext.getAttribLocation(glShader, name);
@@ -3142,7 +3499,6 @@ function glInit()
3142
3499
  glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3143
3500
  offset += size*typeSize;
3144
3501
  }
3145
- let offset = glDirty = glBatchCount = 0;
3146
3502
  initVertexAttribArray('a', gl_FLOAT, 4, 1); // angle
3147
3503
  initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3148
3504
  initVertexAttribArray('s', gl_FLOAT, 4, 2); // size
@@ -3158,14 +3514,8 @@ function glSetBlendMode(additive)
3158
3514
  {
3159
3515
  if (!glEnable) return;
3160
3516
 
3161
- if (additive != glAdditive)
3162
- glFlush();
3163
-
3164
3517
  // setup blending
3165
3518
  glAdditive = additive;
3166
- const destBlend = additive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
3167
- glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
3168
- glContext.enable(gl_BLEND);
3169
3519
  }
3170
3520
 
3171
3521
  /** Set the WebGl texture, not normally necessary unless multiple tile sheets are used
@@ -3175,7 +3525,8 @@ function glSetBlendMode(additive)
3175
3525
  function glSetTexture(texture=glTileTexture)
3176
3526
  {
3177
3527
  if (!glEnable) return;
3178
-
3528
+
3529
+ // must flush cache with the old texture to set a new one
3179
3530
  if (texture != glActiveTexture)
3180
3531
  glFlush();
3181
3532
 
@@ -3240,36 +3591,35 @@ function glCreateBuffer(bufferType, size, usage)
3240
3591
  return buffer;
3241
3592
  }
3242
3593
 
3243
- /** Create WebGL texture from an image
3594
+ /** Create WebGL texture from an image and set the texture settings
3244
3595
  * @param {Image} image
3245
3596
  * @return {WebGLTexture}
3246
3597
  * @memberof WebGL */
3247
3598
  function glCreateTexture(image)
3248
3599
  {
3249
- if (!glEnable) return;
3600
+ if (!glEnable || !image || !image.width) return;
3250
3601
 
3251
3602
  // build the texture
3252
3603
  const texture = glContext.createTexture();
3253
3604
  glContext.bindTexture(gl_TEXTURE_2D, texture);
3254
3605
  glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
3255
-
3606
+
3256
3607
  // use point filtering for pixelated rendering
3257
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, pixelated ? gl_NEAREST : gl_LINEAR);
3258
- glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, pixelated ? gl_NEAREST : gl_LINEAR);
3608
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, cavasPixelated ? gl_NEAREST : gl_LINEAR);
3609
+ glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, cavasPixelated ? gl_NEAREST : gl_LINEAR);
3259
3610
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE);
3260
3611
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE);
3261
3612
  return texture;
3262
3613
  }
3263
3614
 
3264
3615
  // called automatically by engine before render
3265
- function glPreRender(width, height)
3616
+ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3266
3617
  {
3267
3618
  if (!glEnable) return;
3268
3619
 
3269
3620
  // clear and set to same size as main canvas
3270
- glCanvas.width = width;
3271
- glCanvas.height = height;
3272
- glContext.viewport(0, 0, width, height);
3621
+ glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3622
+ glContext.clear(gl_COLOR_BUFFER_BIT);
3273
3623
 
3274
3624
  // set up the shader
3275
3625
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
@@ -3284,7 +3634,7 @@ function glPreRender(width, height)
3284
3634
  sx, 0, 0, 0,
3285
3635
  0, sy, 0, 0,
3286
3636
  1, 1, -1, 1,
3287
- -1-sx*cameraPos.x, -1-sy*cameraPos.y, 0, 0
3637
+ -1-sx*cameraX, -1-sy*cameraY, 0, 0
3288
3638
  ])
3289
3639
  );
3290
3640
  }
@@ -3295,11 +3645,16 @@ function glFlush()
3295
3645
  {
3296
3646
  if (!glEnable || !glBatchCount) return;
3297
3647
 
3648
+ const destBlend = glBatchAdditive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
3649
+ glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
3650
+ glContext.enable(gl_BLEND);
3651
+
3298
3652
  // draw all the sprites in the batch and reset the buffer
3299
3653
  glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
3300
3654
  glPositionData.subarray(0, glBatchCount * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT));
3301
3655
  glContext.drawArrays(gl_TRIANGLES, 0, glBatchCount * gl_VERTICES_PER_QUAD);
3302
3656
  glBatchCount = 0;
3657
+ glBatchAdditive = glAdditive;
3303
3658
  }
3304
3659
 
3305
3660
  /** Draw any sprites still in the buffer, copy to main canvas and clear
@@ -3308,29 +3663,38 @@ function glFlush()
3308
3663
  * @memberof WebGL */
3309
3664
  function glCopyToContext(context, forceDraw)
3310
3665
  {
3311
- if (!glEnable || !glDirty) return;
3666
+ if (!glEnable || !glBatchCount) return;
3312
3667
 
3313
3668
  glFlush();
3669
+
3670
+ // do not draw in overlay mode because the canvas is visible
3314
3671
  if (!glOverlay || forceDraw)
3315
- {
3316
- // do not draw/clear in overlay mode because the canvas is visible
3317
- context.drawImage(glCanvas, 0, glAdditive = glDirty = 0);
3318
- glContext.clear(gl_COLOR_BUFFER_BIT);
3319
- }
3672
+ context.drawImage(glCanvas, 0, 0);
3320
3673
  }
3321
3674
 
3322
- // Draw a sprite with the given parameters, used internally by draw functions
3323
- function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=0xffffffff, rgbaAdditive=0x00000000)
3675
+ /** Add a sprite to the gl draw list, used by all gl draw functions
3676
+ * @param x
3677
+ * @param y
3678
+ * @param sizeX
3679
+ * @param sizeY
3680
+ * @param angle
3681
+ * @param uv0X
3682
+ * @param uv0Y
3683
+ * @param uv1X
3684
+ * @param uv1Y
3685
+ * @param [rgba=0xffffffff]
3686
+ * @param [rgbaAdditive=0]
3687
+ * @memberof WebGL */
3688
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffffff, rgbaAdditive=0)
3324
3689
  {
3325
3690
  if (!glEnable) return;
3326
-
3327
- // flush if there is no room for more verts
3328
- if (glBatchCount == gl_MAX_BATCH)
3691
+
3692
+ // flush if there is no room for more verts or if different blend mode
3693
+ if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3329
3694
  glFlush();
3330
3695
 
3331
3696
  // setup 2 triangles to form a quad
3332
3697
  let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3333
- glDirty = 1;
3334
3698
 
3335
3699
  // vertex 0
3336
3700
  glPositionData[offset++] = angle;