littlejsengine 1.8.9 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +17 -17
  2. package/build/littlejs.d.ts +352 -288
  3. package/build/littlejs.esm.js +695 -658
  4. package/build/littlejs.esm.min.js +1 -1
  5. package/build/littlejs.js +694 -658
  6. package/build/littlejs.min.js +1 -1
  7. package/build/littlejs.release.js +629 -594
  8. package/examples/breakout/game.js +4 -4
  9. package/examples/breakout/index.html +3 -3
  10. package/examples/breakoutTutorial/index.html +2 -2
  11. package/examples/electron/game.js +1 -1
  12. package/examples/electron/index.html +2 -2
  13. package/examples/favicon.png +0 -0
  14. package/examples/js13k/build.js +1 -1
  15. package/examples/js13k/index.html +13 -13
  16. package/examples/module/game.js +1 -1
  17. package/examples/module/index.html +1 -1
  18. package/examples/particles/index.html +1 -1
  19. package/examples/platformer/game.js +6 -6
  20. package/examples/platformer/gameCharacter.js +293 -0
  21. package/examples/platformer/gameEffects.js +8 -5
  22. package/examples/platformer/gameObjects.js +13 -13
  23. package/examples/platformer/gamePlayer.js +9 -293
  24. package/examples/platformer/index.html +7 -6
  25. package/examples/puzzle/game.js +3 -2
  26. package/examples/puzzle/index.html +2 -2
  27. package/examples/starter/build.js +1 -1
  28. package/examples/starter/game.js +2 -2
  29. package/examples/starter/index.html +13 -13
  30. package/examples/stress/index.html +35 -27
  31. package/examples/typescript/index.html +1 -1
  32. package/index.d.ts +2094 -0
  33. package/package.json +1 -1
  34. package/src/engine.js +28 -28
  35. package/src/engineAudio.js +57 -57
  36. package/src/engineDebug.js +66 -65
  37. package/src/engineDraw.js +64 -73
  38. package/src/engineExport.js +1 -0
  39. package/src/engineInput.js +57 -40
  40. package/src/engineMedals.js +32 -29
  41. package/src/engineObject.js +42 -27
  42. package/src/engineParticles.js +98 -72
  43. package/src/engineRelease.js +1 -1
  44. package/src/engineSettings.js +22 -22
  45. package/src/engineTileLayer.js +66 -60
  46. package/src/engineUtilities.js +49 -49
  47. package/src/engineWebGL.js +112 -135
  48. package/src/jsconfig.json +10 -0
@@ -11,7 +11,7 @@
11
11
 
12
12
 
13
13
  let showWatermark = 0;
14
- let debugKey = 0;
14
+ let debugKey = '';
15
15
  const debug = 0;
16
16
  const debugOverlay = 0;
17
17
  const debugPhysics = 0;
@@ -79,15 +79,15 @@ function sign(value) { return Math.sign(value); }
79
79
 
80
80
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
81
81
  * @param {Number} dividend
82
- * @param {Number} [divisor=1]
82
+ * @param {Number} [divisor]
83
83
  * @return {Number}
84
84
  * @memberof Utilities */
85
85
  function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
86
86
 
87
87
  /** Clamps the value beween max and min
88
88
  * @param {Number} value
89
- * @param {Number} [min=0]
90
- * @param {Number} [max=1]
89
+ * @param {Number} [min]
90
+ * @param {Number} [max]
91
91
  * @return {Number}
92
92
  * @memberof Utilities */
93
93
  function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
@@ -112,7 +112,7 @@ function lerp(percent, valueA, valueB) { return valueA + clamp(percent) * (value
112
112
  /** Returns signed wrapped distance between the two values passed in
113
113
  * @param {Number} valueA
114
114
  * @param {Number} valueB
115
- * @param {Number} [wrapSize=1]
115
+ * @param {Number} [wrapSize]
116
116
  * @returns {Number}
117
117
  * @memberof Utilities */
118
118
  function distanceWrap(valueA, valueB, wrapSize=1)
@@ -122,7 +122,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
122
122
  * @param {Number} percent
123
123
  * @param {Number} valueA
124
124
  * @param {Number} valueB
125
- * @param {Number} [wrapSize=1]
125
+ * @param {Number} [wrapSize]
126
126
  * @returns {Number}
127
127
  * @memberof Utilities */
128
128
  function lerpWrap(percent, valueA, valueB, wrapSize=1)
@@ -133,7 +133,7 @@ function lerpWrap(percent, valueA, valueB, wrapSize=1)
133
133
  * @param {Number} angleB
134
134
  * @returns {Number}
135
135
  * @memberof Utilities */
136
- function distanceAngle(angleA, angleB) { distanceWrap(angleA, angleB, 2*PI); }
136
+ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
137
137
 
138
138
  /** Linearly interpolates between the angles passed in with wrappping
139
139
  * @param {Number} percent
@@ -156,11 +156,11 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
156
156
  function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
157
157
 
158
158
  /** Returns true if two axis aligned bounding boxes are overlapping
159
- * @param {Vector2} pointA - Center of box A
160
- * @param {Vector2} sizeA - Size of box A
161
- * @param {Vector2} pointB - Center of box B
162
- * @param {Vector2} sizeB - Size of box B
163
- * @return {Boolean} - True if overlapping
159
+ * @param {Vector2} pointA - Center of box A
160
+ * @param {Vector2} sizeA - Size of box A
161
+ * @param {Vector2} pointB - Center of box B
162
+ * @param {Vector2} sizeB - Size of box B
163
+ * @return {Boolean} - True if overlapping
164
164
  * @memberof Utilities */
165
165
  function isOverlapping(pointA, sizeA, pointB, sizeB)
166
166
  {
@@ -169,10 +169,10 @@ function isOverlapping(pointA, sizeA, pointB, sizeB)
169
169
  }
170
170
 
171
171
  /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
172
- * @param {Number} [frequency=1] - Frequency of the wave in Hz
173
- * @param {Number} [amplitude=1] - Amplitude (max height) of the wave
174
- * @param {Number} [t=time] - Value to use for time of the wave
175
- * @return {Number} - Value waving between 0 and amplitude
172
+ * @param {Number} [frequency] - Frequency of the wave in Hz
173
+ * @param {Number} [amplitude] - Amplitude (max height) of the wave
174
+ * @param {Number} [t=time] - Value to use for time of the wave
175
+ * @return {Number} - Value waving between 0 and amplitude
176
176
  * @memberof Utilities */
177
177
  function wave(frequency=1, amplitude=1, t=time)
178
178
  { return amplitude/2 * (1 - Math.cos(t*frequency*2*PI)); }
@@ -189,15 +189,15 @@ function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
189
189
  * @namespace Random */
190
190
 
191
191
  /** Returns a random value between the two values passed in
192
- * @param {Number} [valueA=1]
193
- * @param {Number} [valueB=0]
192
+ * @param {Number} [valueA]
193
+ * @param {Number} [valueB]
194
194
  * @return {Number}
195
195
  * @memberof Random */
196
196
  function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
197
197
 
198
198
  /** Returns a floored random value the two values passed in
199
199
  * @param {Number} valueA
200
- * @param {Number} [valueB=0]
200
+ * @param {Number} [valueB]
201
201
  * @return {Number}
202
202
  * @memberof Random */
203
203
  function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
@@ -208,14 +208,14 @@ function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
208
208
  function randSign() { return randInt(2) * 2 - 1; }
209
209
 
210
210
  /** Returns a random Vector2 with the passed in length
211
- * @param {Number} [length=1]
211
+ * @param {Number} [length]
212
212
  * @return {Vector2}
213
213
  * @memberof Random */
214
214
  function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
215
215
 
216
216
  /** Returns a random Vector2 within a circular shape
217
- * @param {Number} [radius=1]
218
- * @param {Number} [minRadius=0]
217
+ * @param {Number} [radius]
218
+ * @param {Number} [minRadius]
219
219
  * @return {Vector2}
220
220
  * @memberof Random */
221
221
  function randInCircle(radius=1, minRadius=0)
@@ -227,7 +227,7 @@ function randInCircle(radius=1, minRadius=0)
227
227
  * @param {Boolean} [linear]
228
228
  * @return {Color}
229
229
  * @memberof Random */
230
- function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear)
230
+ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
231
231
  {
232
232
  return linear ? colorA.lerp(colorB, rand()) :
233
233
  new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
@@ -256,8 +256,8 @@ class RandomGenerator
256
256
  }
257
257
 
258
258
  /** Returns a seeded random value between the two values passed in
259
- * @param {Number} [valueA=1]
260
- * @param {Number} [valueB=0]
259
+ * @param {Number} [valueA]
260
+ * @param {Number} [valueB]
261
261
  * @return {Number} */
262
262
  float(valueA=1, valueB=0)
263
263
  {
@@ -270,7 +270,7 @@ class RandomGenerator
270
270
 
271
271
  /** Returns a floored seeded random value the two values passed in
272
272
  * @param {Number} valueA
273
- * @param {Number} [valueB=0]
273
+ * @param {Number} [valueB]
274
274
  * @return {Number} */
275
275
  int(valueA, valueB=0) { return Math.floor(this.float(valueA, valueB)); }
276
276
 
@@ -283,8 +283,8 @@ class RandomGenerator
283
283
 
284
284
  /**
285
285
  * Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
286
- * @param {(Number|Vector2)} [x=0]
287
- * @param {Number} [y=0]
286
+ * @param {(Number|Vector2)} [x]
287
+ * @param {Number} [y]
288
288
  * @return {Vector2}
289
289
  * @example
290
290
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
@@ -294,15 +294,15 @@ class RandomGenerator
294
294
  * @memberof Utilities
295
295
  */
296
296
  function vec2(x=0, y)
297
- { return x.x == undefined ? new Vector2(x, y == undefined? x : y) : new Vector2(x.x, x.y); }
297
+ { return typeof x === 'number'? new Vector2(x, y == undefined? x : y) : new Vector2(x.x, x.y); }
298
298
 
299
299
  /**
300
300
  * Check if object is a valid Vector2
301
- * @param {Vector2} v
301
+ * @param {any} v
302
302
  * @return {Boolean}
303
303
  * @memberof Utilities
304
304
  */
305
- function isVector2(v) { return !isNaN(v.x) && !isNaN(v.y); }
305
+ function isVector2(v) { return typeof v === 'object' && typeof v.x === 'number' && typeof v.y === 'number'; }
306
306
 
307
307
  /**
308
308
  * 2D Vector object with vector math library
@@ -316,8 +316,8 @@ function isVector2(v) { return !isNaN(v.x) && !isNaN(v.y); }
316
316
  class Vector2
317
317
  {
318
318
  /** Create a 2D vector with the x and y passed in, can also be created with vec2()
319
- * @param {Number} [x=0] - X axis location
320
- * @param {Number} [y=0] - Y axis location */
319
+ * @param {Number} [x] - X axis location
320
+ * @param {Number} [y] - Y axis location */
321
321
  constructor(x=0, y=0)
322
322
  {
323
323
  /** @property {Number} - X axis location */
@@ -374,12 +374,12 @@ class Vector2
374
374
  distanceSquared(v) { return (this.x - v.x)**2 + (this.y - v.y)**2; }
375
375
 
376
376
  /** Returns a new vector in same direction as this one with the length passed in
377
- * @param {Number} [length=1]
377
+ * @param {Number} [length]
378
378
  * @return {Vector2} */
379
379
  normalize(length=1) { const l = this.length(); return l ? this.scale(length/l) : new Vector2(0, length); }
380
380
 
381
381
  /** Returns a new vector clamped to length passed in
382
- * @param {Number} [length=1]
382
+ * @param {Number} [length]
383
383
  * @return {Vector2} */
384
384
  clampLength(length=1) { const l = this.length(); return l > length ? this.scale(length/l) : this; }
385
385
 
@@ -398,8 +398,8 @@ class Vector2
398
398
  angle() { return Math.atan2(this.x, this.y); }
399
399
 
400
400
  /** Sets this vector with angle and length passed in
401
- * @param {Number} [angle=0]
402
- * @param {Number} [length=1]
401
+ * @param {Number} [angle]
402
+ * @param {Number} [length]
403
403
  * @return {Vector2} */
404
404
  setAngle(angle=0, length=1)
405
405
  { this.x = length*Math.sin(angle); this.y = length*Math.cos(angle); return this; }
@@ -484,10 +484,10 @@ function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
484
484
  class Color
485
485
  {
486
486
  /** Create a color with the rgba components passed in, white by default
487
- * @param {Number} [r=1] - red
488
- * @param {Number} [g=1] - green
489
- * @param {Number} [b=1] - blue
490
- * @param {Number} [a=1] - alpha*/
487
+ * @param {Number} [r] - red
488
+ * @param {Number} [g] - green
489
+ * @param {Number} [b] - blue
490
+ * @param {Number} [a] - alpha*/
491
491
  constructor(r=1, g=1, b=1, a=1)
492
492
  {
493
493
  /** @property {Number} - Red */
@@ -542,10 +542,10 @@ class Color
542
542
  lerp(c, percent) { return this.add(c.subtract(this).scale(clamp(percent))); }
543
543
 
544
544
  /** Sets this color given a hue, saturation, lightness, and alpha
545
- * @param {Number} [h=0] - hue
546
- * @param {Number} [s=0] - saturation
547
- * @param {Number} [l=1] - lightness
548
- * @param {Number} [a=1] - alpha
545
+ * @param {Number} [h] - hue
546
+ * @param {Number} [s] - saturation
547
+ * @param {Number} [l] - lightness
548
+ * @param {Number} [a] - alpha
549
549
  * @return {Color} */
550
550
  setHSLA(h=0, s=0, l=1, a=1)
551
551
  {
@@ -591,8 +591,8 @@ class Color
591
591
  }
592
592
 
593
593
  /** Returns a new color that has each component randomly adjusted
594
- * @param {Number} [amount=.05]
595
- * @param {Number} [alphaAmount=0]
594
+ * @param {Number} [amount]
595
+ * @param {Number} [alphaAmount]
596
596
  * @return {Color} */
597
597
  mutate(amount=.05, alphaAmount=0)
598
598
  {
@@ -606,9 +606,9 @@ class Color
606
606
  }
607
607
 
608
608
  /** Returns this color expressed as a hex color code
609
- * @param {Boolean} [useAlpha=1] - if alpha should be included in result
609
+ * @param {Boolean} [useAlpha] - if alpha should be included in result
610
610
  * @return {String} */
611
- toString(useAlpha = 1)
611
+ toString(useAlpha = true)
612
612
  {
613
613
  const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
614
614
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
@@ -657,7 +657,7 @@ class Timer
657
657
  constructor(timeLeft) { this.time = timeLeft == undefined ? undefined : time + timeLeft; this.setTime = timeLeft; }
658
658
 
659
659
  /** Set the timer with seconds passed in
660
- * @param {Number} [timeLeft=0] - How much time left before the timer is elapsed in seconds */
660
+ * @param {Number} [timeLeft] - How much time left before the timer is elapsed in seconds */
661
661
  set(timeLeft=0) { this.time = time + timeLeft; this.setTime = timeLeft; }
662
662
 
663
663
  /** Unset the timer */
@@ -734,7 +734,7 @@ let canvasFixedSize = vec2();
734
734
  * @type {Boolean}
735
735
  * @default
736
736
  * @memberof Settings */
737
- let canvasPixelated = 1;
737
+ let canvasPixelated = true;
738
738
 
739
739
  /** Default font used for text rendering
740
740
  * @type {String}
@@ -746,7 +746,7 @@ let fontDefault = 'arial';
746
746
  * @type {Boolean}
747
747
  * @default
748
748
  * @memberof Settings */
749
- let showSplashScreen = 0;
749
+ let showSplashScreen = false;
750
750
 
751
751
  ///////////////////////////////////////////////////////////////////////////////
752
752
  // WebGL settings
@@ -755,13 +755,13 @@ let showSplashScreen = 0;
755
755
  * @type {Boolean}
756
756
  * @default
757
757
  * @memberof Settings */
758
- let glEnable = 1;
758
+ let glEnable = true;
759
759
 
760
760
  /** Fixes slow rendering in some browsers by not compositing the WebGL canvas
761
761
  * @type {Boolean}
762
762
  * @default
763
763
  * @memberof Settings */
764
- let glOverlay = 1;
764
+ let glOverlay = true;
765
765
 
766
766
  ///////////////////////////////////////////////////////////////////////////////
767
767
  // Tile sheet settings
@@ -785,7 +785,7 @@ let tileFixBleedScale = .3;
785
785
  * @type {Boolean}
786
786
  * @default
787
787
  * @memberof Settings */
788
- let enablePhysicsSolver = 1;
788
+ let enablePhysicsSolver = true;
789
789
 
790
790
  /** Default object mass for collison calcuations (how heavy objects are)
791
791
  * @type {Number}
@@ -807,7 +807,7 @@ let objectDefaultAngleDamping = 1;
807
807
 
808
808
  /** How much to bounce when a collision occurs (0-1)
809
809
  * @type {Number}
810
- * @default 0
810
+ * @default
811
811
  * @memberof Settings */
812
812
  let objectDefaultElasticity = 0;
813
813
 
@@ -825,7 +825,7 @@ let objectMaxSpeed = 1;
825
825
 
826
826
  /** How much gravity to apply to objects along the Y axis, negative is down
827
827
  * @type {Number}
828
- * @default 0
828
+ * @default
829
829
  * @memberof Settings */
830
830
  let gravity = 0;
831
831
 
@@ -842,33 +842,33 @@ let particleEmitRateScale = 1;
842
842
  * @type {Boolean}
843
843
  * @default
844
844
  * @memberof Settings */
845
- let gamepadsEnable = 1;
845
+ let gamepadsEnable = true;
846
846
 
847
847
  /** If true, the dpad input is also routed to the left analog stick (for better accessability)
848
848
  * @type {Boolean}
849
849
  * @default
850
850
  * @memberof Settings */
851
- let gamepadDirectionEmulateStick = 1;
851
+ let gamepadDirectionEmulateStick = true;
852
852
 
853
853
  /** If true the WASD keys are also routed to the direction keys (for better accessability)
854
854
  * @type {Boolean}
855
855
  * @default
856
856
  * @memberof Settings */
857
- let inputWASDEmulateDirection = 1;
857
+ let inputWASDEmulateDirection = true;
858
858
 
859
859
  /** True if touch gamepad should appear on mobile devices
860
860
  * - Supports left analog stick, 4 face buttons and start button (button 9)
861
861
  * - Must be set by end of gameInit to be activated
862
862
  * @type {Boolean}
863
- * @default 0
863
+ * @default
864
864
  * @memberof Settings */
865
- let touchGamepadEnable = 0;
865
+ let touchGamepadEnable = false;
866
866
 
867
867
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
868
868
  * @type {Boolean}
869
869
  * @default
870
870
  * @memberof Settings */
871
- let touchGamepadAnalog = 1;
871
+ let touchGamepadAnalog = true;
872
872
 
873
873
  /** Size of virutal gamepad for touch devices in pixels
874
874
  * @type {Number}
@@ -886,7 +886,7 @@ let touchGamepadAlpha = .3;
886
886
  * @type {Boolean}
887
887
  * @default
888
888
  * @memberof Settings */
889
- let vibrateEnable = 1;
889
+ let vibrateEnable = true;
890
890
 
891
891
  ///////////////////////////////////////////////////////////////////////////////
892
892
  // Audio settings
@@ -895,7 +895,7 @@ let vibrateEnable = 1;
895
895
  * @type {Boolean}
896
896
  * @default
897
897
  * @memberof Settings */
898
- let soundEnable = 1;
898
+ let soundEnable = true;
899
899
 
900
900
  /** Volume scale to apply to all sound, music and speech
901
901
  * @type {Number}
@@ -944,9 +944,9 @@ let medalDisplayIconSize = 50;
944
944
 
945
945
  /** Set to stop medals from being unlockable (like if cheats are enabled)
946
946
  * @type {Boolean}
947
- * @default 0
947
+ * @default
948
948
  * @memberof Settings */
949
- let medalsPreventUnlock;
949
+ let medalsPreventUnlock = false;
950
950
 
951
951
  ///////////////////////////////////////////////////////////////////////////////
952
952
  // Setters for global variables
@@ -1017,12 +1017,12 @@ function setEnablePhysicsSolver(enable) { enablePhysicsSolver = enable; }
1017
1017
  function setObjectDefaultMass(mass) { objectDefaultMass = mass; }
1018
1018
 
1019
1019
  /** Set how much to slow velocity by each frame
1020
- * @param {Number} damping
1020
+ * @param {Number} damp
1021
1021
  * @memberof Settings */
1022
1022
  function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
1023
1023
 
1024
1024
  /** Set how much to slow angular velocity each frame
1025
- * @param {Number} damping
1025
+ * @param {Number} damp
1026
1026
  * @memberof Settings */
1027
1027
  function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
1028
1028
 
@@ -1042,9 +1042,9 @@ function setObjectDefaultFriction(friction) { objectDefaultFriction = friction;
1042
1042
  function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
1043
1043
 
1044
1044
  /** Set how much gravity to apply to objects along the Y axis
1045
- * @param {Number} gravity
1045
+ * @param {Number} newGravity
1046
1046
  * @memberof Settings */
1047
- function setGravity(g) { gravity = g; }
1047
+ function setGravity(newGravity) { gravity = newGravity; }
1048
1048
 
1049
1049
  /** Set to scales emit rate of particles
1050
1050
  * @param {Number} scale
@@ -1142,7 +1142,7 @@ function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUn
1142
1142
  function setShowWatermark(show) { showWatermark = show; }
1143
1143
 
1144
1144
  /** Set key code used to toggle debug mode, Esc by default
1145
- * @param {Number} key
1145
+ * @param {String} key
1146
1146
  * @memberof Debug */
1147
1147
  function setDebugKey(key) { debugKey = key; }
1148
1148
  /**
@@ -1179,27 +1179,25 @@ function setDebugKey(key) { debugKey = key; }
1179
1179
  class EngineObject
1180
1180
  {
1181
1181
  /** Create an engine object and adds it to the list of objects
1182
- * @param {Vector2} [pos=Vector2()] - World space position of the object
1183
- * @param {Vector2} [size=Vector2(1,1)] - World space size of the object
1184
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
1185
- * @param {Number} [angle=0] - Angle the object is rotated by
1186
- * @param {Color} [color=Color()] - Color to apply to tile when rendered
1187
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
1182
+ * @param {Vector2} [pos=(0,0)] - World space position of the object
1183
+ * @param {Vector2} [size=(1,1)] - World space size of the object
1184
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
1185
+ * @param {Number} [angle] - Angle the object is rotated by
1186
+ * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
1187
+ * @param {Number} [renderOrder] - Objects sorted by renderOrder before being rendered
1188
1188
  */
1189
1189
  constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color, renderOrder=0)
1190
1190
  {
1191
1191
  // set passed in params
1192
- ASSERT(isVector2(pos) && isVector2(size)); // ensure pos and size are vec2s
1193
- ASSERT(typeof tileInfo !== 'number' || !tileInfo); // prevent old style calls
1194
- ASSERT(!(renderOrder instanceof Color)); // prevent old style calls
1195
- // to fix old calls, replace with tile(tileIndex, tileSize)
1192
+ ASSERT(isVector2(pos) && isVector2(size), 'ensure pos and size are vec2s');
1193
+ ASSERT(typeof tileInfo !== 'number' || !tileInfo, 'old style tile setup');
1196
1194
 
1197
1195
  /** @property {Vector2} - World space position of the object */
1198
1196
  this.pos = pos.copy();
1199
1197
  /** @property {Vector2} - World space width and height of the object */
1200
1198
  this.size = size;
1201
1199
  /** @property {Vector2} - Size of object used for drawing, uses size if not set */
1202
- this.drawSize;
1200
+ this.drawSize = undefined;
1203
1201
  /** @property {TileInfo} - Tile info to render object (undefined is untextured) */
1204
1202
  this.tileInfo = tileInfo;
1205
1203
  /** @property {Number} - Angle to rotate the object */
@@ -1207,9 +1205,11 @@ class EngineObject
1207
1205
  /** @property {Color} - Color to apply when rendered */
1208
1206
  this.color = color;
1209
1207
  /** @property {Color} - Additive color to apply when rendered */
1210
- this.additiveColor;
1208
+ this.additiveColor = undefined;
1209
+ /** @property {Boolean} - Should it flip along y axis when rendered */
1210
+ this.mirror = false;
1211
1211
 
1212
- // set object defaults
1212
+ // physical properties
1213
1213
  /** @property {Number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
1214
1214
  this.mass = objectDefaultMass;
1215
1215
  /** @property {Number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
@@ -1220,19 +1220,34 @@ class EngineObject
1220
1220
  this.elasticity = objectDefaultElasticity;
1221
1221
  /** @property {Number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
1222
1222
  this.friction = objectDefaultFriction;
1223
- /** @property {Number} [gravityScale=1] - How much to scale gravity by for this object */
1223
+ /** @property {Number} - How much to scale gravity by for this object */
1224
1224
  this.gravityScale = 1;
1225
- /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1225
+ /** @property {Number} - Objects are sorted by render order */
1226
1226
  this.renderOrder = renderOrder;
1227
- /** @property {Vector2} [velocity=Vector2()] - Velocity of the object */
1227
+ /** @property {Vector2} - Velocity of the object */
1228
1228
  this.velocity = vec2();
1229
- /** @property {Number} [angleVelocity=0] - Angular velocity of the object */
1229
+ /** @property {Number} - Angular velocity of the object */
1230
1230
  this.angleVelocity = 0;
1231
-
1232
- // init other internal object stuff
1231
+ /** @property {Number} - Track when object was created */
1233
1232
  this.spawnTime = time;
1233
+ /** @property {Array} - List of children of this object */
1234
1234
  this.children = [];
1235
+
1236
+ // parent child system
1237
+ /** @property {EngineObject} - Parent of object if in local space */
1238
+ this.parent = undefined;
1239
+ /** @property {Vector2} - Local position if child */
1240
+ this.localPos = vec2();
1241
+ /** @property {Number} - Local angle if child */
1242
+ this.localAngle = 0;
1243
+
1244
+ // collision flags
1245
+ /** @property {Boolean} - Object collides with the tile collision */
1235
1246
  this.collideTiles = false;
1247
+ /** @property {Boolean} - Object collides with solid objects */
1248
+ this.collideSolidObjects = false;
1249
+ /** @property {Boolean} - Object collides with and blocks other objects */
1250
+ this.isSolid = false;
1236
1251
 
1237
1252
  // add to list of objects
1238
1253
  engineObjects.push(this);
@@ -1449,7 +1464,7 @@ class EngineObject
1449
1464
  * @param {EngineObject} object - the object to test against
1450
1465
  * @return {Boolean} - true if the collision should be resolved
1451
1466
  */
1452
- collideWithObject(object) { return 1; }
1467
+ collideWithObject(object) { return true; }
1453
1468
 
1454
1469
  /** How long since the object was created
1455
1470
  * @return {Number} */
@@ -1469,8 +1484,8 @@ class EngineObject
1469
1484
 
1470
1485
  /** Attaches a child to this with a given local transform
1471
1486
  * @param {EngineObject} child
1472
- * @param {Vector2} [localPos=Vector2()]
1473
- * @param {Number} [localAngle=0] */
1487
+ * @param {Vector2} [localPos=(0,0)]
1488
+ * @param {Number} [localAngle] */
1474
1489
  addChild(child, localPos=vec2(), localAngle=0)
1475
1490
  {
1476
1491
  ASSERT(!child.parent && !this.children.includes(child));
@@ -1490,12 +1505,12 @@ class EngineObject
1490
1505
  }
1491
1506
 
1492
1507
  /** Set how this object collides
1493
- * @param {Boolean} [collideSolidObjects=1] - Does it collide with solid objects
1494
- * @param {Boolean} [isSolid=1] - Does it collide with and block other objects (expensive in large numbers)
1495
- * @param {Boolean} [collideTiles=1] - Does it collide with the tile collision */
1496
- setCollision(collideSolidObjects=1, isSolid=1, collideTiles=1)
1508
+ * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects
1509
+ * @param {Boolean} [isSolid] - Does it collide with and block other objects (expensive in large numbers)
1510
+ * @param {Boolean} [collideTiles] - Does it collide with the tile collision */
1511
+ setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true)
1497
1512
  {
1498
- ASSERT(collideSolidObjects || !isSolid); // solid objects must be set to collide
1513
+ ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
1499
1514
 
1500
1515
  this.collideSolidObjects = collideSolidObjects;
1501
1516
  this.isSolid = isSolid;
@@ -1586,9 +1601,9 @@ let drawCount;
1586
1601
  * Create a tile info object
1587
1602
  * - This can take vecs or floats for easier use and conversion
1588
1603
  * - If an index is passed in, the tile size and index will determine the position
1589
- * @param {(Number|Vector2)} [pos=Vector2()] - Top left corner of tile in pixels or index
1590
- * @param {(Number|Vector2)} [size=tileSizeDefault] - Size of tile in pixels
1591
- * @param {Number} [textureIndex=0] - Texture index to use
1604
+ * @param {(Number|Vector2)} [pos=(0,0)] - Top left corner of tile in pixels or index
1605
+ * @param {(Number|Vector2)} [size=tileSizeDefault] - Size of tile in pixels
1606
+ * @param {Number} [textureIndex] - Texture index to use
1592
1607
  * @return {TileInfo}
1593
1608
  * @example
1594
1609
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -1600,14 +1615,14 @@ let drawCount;
1600
1615
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1601
1616
  {
1602
1617
  // if size is a number, make it a vector
1603
- if (size.x == undefined)
1618
+ if (typeof size === 'number')
1604
1619
  {
1605
1620
  ASSERT(size > 0);
1606
1621
  size = vec2(size);
1607
1622
  }
1608
1623
 
1609
1624
  // if pos is a number, use it as a tile index
1610
- if (pos.x == undefined)
1625
+ if (typeof pos === 'number')
1611
1626
  {
1612
1627
  const textureInfo = textureInfos[textureIndex];
1613
1628
  if (textureInfo)
@@ -1629,9 +1644,9 @@ function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1629
1644
  class TileInfo
1630
1645
  {
1631
1646
  /** Create a tile info object
1632
- * @param {Vector2} [pos=Vector2()] - Top left corner of tile in pixels
1647
+ * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
1633
1648
  * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
1634
- * @param {Number} [textureIndex=0] - Texture index to use
1649
+ * @param {Number} [textureIndex] - Texture index to use
1635
1650
  */
1636
1651
  constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1637
1652
  {
@@ -1660,10 +1675,13 @@ class TileInfo
1660
1675
  /** Texture Info - Stores info about each texture */
1661
1676
  class TextureInfo
1662
1677
  {
1663
- // create a TextureInfo, called automatically by the engine
1678
+ /**
1679
+ * Create a TextureInfo, called automatically by the engine
1680
+ * @param {HTMLImageElement} image
1681
+ */
1664
1682
  constructor(image)
1665
1683
  {
1666
- /** @property {CanvasImageSource} - image source */
1684
+ /** @property {HTMLImageElement} - image source */
1667
1685
  this.image = image;
1668
1686
  /** @property {Vector2} - size of the image */
1669
1687
  this.size = vec2(image.width, image.height);
@@ -1703,26 +1721,24 @@ function worldToScreen(worldPos)
1703
1721
  }
1704
1722
 
1705
1723
  /** Draw textured tile centered in world space, with color applied if using WebGL
1706
- * @param {Vector2} pos - Center of the tile in world space
1707
- * @param {Vector2} [size=Vector2(1,1)] - Size of the tile in world space
1708
- * @param {TileInfo}[tileInfo] - Tile info to use, untextured if undefined
1709
- * @param {Vector2} [tileSize=tileSizeDefault] - Tile size in source pixels
1710
- * @param {Color} [color=Color()] - Color to modulate with
1711
- * @param {Number} [angle=0] - Angle to rotate by
1712
- * @param {Boolean} [mirror=0] - If true image is flipped along the Y axis
1713
- * @param {Color} [additiveColor=Color(0,0,0,0)] - Additive color to be applied
1714
- * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1715
- * @param {Boolean} [screenSpace=0] - If true the pos and size are in screen space
1716
- * @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
1724
+ * @param {Vector2} pos - Center of the tile in world space
1725
+ * @param {Vector2} [size=(1,1)] - Size of the tile in world space
1726
+ * @param {TileInfo}[tileInfo] - Tile info to use, untextured if undefined
1727
+ * @param {Color} [color=(1,1,1,1)] - Color to modulate with
1728
+ * @param {Number} [angle] - Angle to rotate by
1729
+ * @param {Boolean} [mirror] - If true image is flipped along the Y axis
1730
+ * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
1731
+ * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1732
+ * @param {Boolean} [screenSpace=false] - If true the pos and size are in screen space
1733
+ * @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
1717
1734
  * @memberof Draw */
1718
1735
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
1719
1736
  angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
1720
1737
  {
1721
- ASSERT(!context || !useWebGL); // context only supported in canvas 2D mode
1722
- ASSERT(typeof tileInfo !== 'number' || !tileInfo); // prevent old style calls
1723
- // to fix old calls, replace with tile(tileIndex, tileSize)
1738
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
1739
+ ASSERT(typeof tileInfo !== 'number' || !tileInfo,
1740
+ 'this is an old style calls, to fix replace it with tile(tileIndex, tileSize)');
1724
1741
 
1725
- showWatermark && ++drawCount;
1726
1742
  const textureInfo = tileInfo && tileInfo.getTextureInfo();
1727
1743
  if (useWebGL)
1728
1744
  {
@@ -1756,6 +1772,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
1756
1772
  else
1757
1773
  {
1758
1774
  // normal canvas 2D rendering method (slower)
1775
+ showWatermark && ++drawCount;
1759
1776
  drawCanvas2D(pos, size, angle, mirror, (context)=>
1760
1777
  {
1761
1778
  if (textureInfo)
@@ -1781,51 +1798,40 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
1781
1798
 
1782
1799
  /** Draw colored rect centered on pos
1783
1800
  * @param {Vector2} pos
1784
- * @param {Vector2} [size=Vector2(1,1)]
1785
- * @param {Color} [color=Color()]
1786
- * @param {Number} [angle=0]
1801
+ * @param {Vector2} [size=(1,1)]
1802
+ * @param {Color} [color=(1,1,1,1)]
1803
+ * @param {Number} [angle]
1787
1804
  * @param {Boolean} [useWebGL=glEnable]
1788
- * @param {Boolean} [screenSpace=0]
1805
+ * @param {Boolean} [screenSpace=false]
1789
1806
  * @param {CanvasRenderingContext2D} [context]
1790
1807
  * @memberof Draw */
1791
1808
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
1792
1809
  {
1793
- drawTile(pos, size, undefined, color, angle, 0, undefined, useWebGL, screenSpace, context);
1810
+ drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
1794
1811
  }
1795
1812
 
1796
1813
  /** Draw colored polygon using passed in points
1797
1814
  * @param {Array} points - Array of Vector2 points
1798
- * @param {Color} [color=Color()]
1799
- * @param {Boolean} [useWebGL=glEnable]
1800
- * @param {Boolean} [screenSpace=0]
1801
- * @param {CanvasRenderingContext2D} [context]
1815
+ * @param {Color} [color=(1,1,1,1)]
1816
+ * @param {Boolean} [screenSpace=false]
1817
+ * @param {CanvasRenderingContext2D} [context=mainContext]
1802
1818
  * @memberof Draw */
1803
- function drawPoly(points, color=new Color, useWebGL=glEnable, screenSpace, context)
1819
+ function drawPoly(points, color=new Color, screenSpace, context=mainContext)
1804
1820
  {
1805
- ASSERT(!context || !useWebGL); // context only supported in canvas 2D mode
1806
-
1807
- if (useWebGL)
1808
- glDrawPoints(screenSpace ? points.map(screenToWorld) : points, color.rgbaInt());
1809
- else
1810
- {
1811
- // draw using canvas
1812
- if (!context)
1813
- context = mainContext;
1814
- context.fillStyle = color;
1815
- context.beginPath();
1816
- for (const point of screenSpace ? points : points.map(worldToScreen))
1817
- context.lineTo(point.x, point.y);
1818
- context.fill();
1819
- }
1821
+ context.fillStyle = color.toString();
1822
+ context.beginPath();
1823
+ for (const point of screenSpace ? points : points.map(worldToScreen))
1824
+ context.lineTo(point.x, point.y);
1825
+ context.fill();
1820
1826
  }
1821
1827
 
1822
1828
  /** Draw colored line between two points
1823
1829
  * @param {Vector2} posA
1824
1830
  * @param {Vector2} posB
1825
- * @param {Number} [thickness=.1]
1826
- * @param {Color} [color=Color()]
1831
+ * @param {Number} [thickness]
1832
+ * @param {Color} [color=(1,1,1,1)]
1827
1833
  * @param {Boolean} [useWebGL=glEnable]
1828
- * @param {Boolean} [screenSpace=0]
1834
+ * @param {Boolean} [screenSpace=false]
1829
1835
  * @param {CanvasRenderingContext2D} [context]
1830
1836
  * @memberof Draw */
1831
1837
  function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
@@ -1841,7 +1847,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
1841
1847
  * @param {Number} angle
1842
1848
  * @param {Boolean} mirror
1843
1849
  * @param {Function} drawFunction
1844
- * @param {Boolean} [screenSpace=0]
1850
+ * @param {Boolean} [screenSpace=false]
1845
1851
  * @param {CanvasRenderingContext2D} [context=mainContext]
1846
1852
  * @memberof Draw */
1847
1853
  function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
@@ -1861,13 +1867,13 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
1861
1867
  }
1862
1868
 
1863
1869
  /** Enable normal or additive blend mode
1864
- * @param {Boolean} [additive=0]
1870
+ * @param {Boolean} [additive]
1865
1871
  * @param {Boolean} [useWebGL=glEnable]
1866
1872
  * @param {CanvasRenderingContext2D} [context=mainContext]
1867
1873
  * @memberof Draw */
1868
1874
  function setBlendMode(additive, useWebGL=glEnable, context)
1869
1875
  {
1870
- ASSERT(!context || !useWebGL); // context only supported in canvas 2D mode
1876
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
1871
1877
  if (useWebGL)
1872
1878
  glAdditive = additive;
1873
1879
  else
@@ -1882,11 +1888,11 @@ function setBlendMode(additive, useWebGL=glEnable, context)
1882
1888
  * Automatically splits new lines into rows
1883
1889
  * @param {String} text
1884
1890
  * @param {Vector2} pos
1885
- * @param {Number} [size=1]
1886
- * @param {Color} [color=Color()]
1887
- * @param {Number} [lineWidth=0]
1888
- * @param {Color} [lineColor=Color(0,0,0)]
1889
- * @param {String} [textAlign='center']
1891
+ * @param {Number} [size]
1892
+ * @param {Color} [color=(1,1,1,1)]
1893
+ * @param {Number} [lineWidth]
1894
+ * @param {Color} [lineColor=(0,0,0,1)]
1895
+ * @param {CanvasTextAlign} [textAlign='center']
1890
1896
  * @param {String} [font=fontDefault]
1891
1897
  * @param {CanvasRenderingContext2D} [context=overlayContext]
1892
1898
  * @memberof Draw */
@@ -1899,19 +1905,19 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
1899
1905
  * Automatically splits new lines into rows
1900
1906
  * @param {String} text
1901
1907
  * @param {Vector2} pos
1902
- * @param {Number} [size=1]
1903
- * @param {Color} [color=Color()]
1904
- * @param {Number} [lineWidth=0]
1905
- * @param {Color} [lineColor=Color(0,0,0)]
1906
- * @param {String} [textAlign='center']
1908
+ * @param {Number} [size]
1909
+ * @param {Color} [color=(1,1,1,1)]
1910
+ * @param {Number} [lineWidth]
1911
+ * @param {Color} [lineColor=(0,0,0,1)]
1912
+ * @param {CanvasTextAlign} [textAlign]
1907
1913
  * @param {String} [font=fontDefault]
1908
1914
  * @param {CanvasRenderingContext2D} [context=overlayContext]
1909
1915
  * @memberof Draw */
1910
1916
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
1911
1917
  {
1912
- context.fillStyle = color;
1918
+ context.fillStyle = color.toString();
1913
1919
  context.lineWidth = lineWidth;
1914
- context.strokeStyle = lineColor;
1920
+ context.strokeStyle = lineColor.toString();
1915
1921
  context.textAlign = textAlign;
1916
1922
  context.font = size + 'px '+ font;
1917
1923
  context.textBaseline = 'middle';
@@ -1945,9 +1951,9 @@ let engineFontImage;
1945
1951
  class FontImage
1946
1952
  {
1947
1953
  /** Create an image font
1948
- * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
1949
- * @param {Vector2} [tileSize=vec2(8)] - Size of the font source tiles
1950
- * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1954
+ * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
1955
+ * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
1956
+ * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
1951
1957
  * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
1952
1958
  */
1953
1959
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
@@ -1976,7 +1982,7 @@ class FontImage
1976
1982
  /** Draw text in screen space using the image font
1977
1983
  * @param {String} text
1978
1984
  * @param {Vector2} pos
1979
- * @param {Number} [scale=4]
1985
+ * @param {Number} [scale]
1980
1986
  * @param {Boolean} [center]
1981
1987
  */
1982
1988
  drawTextScreen(text, pos, scale=4, center)
@@ -1994,7 +2000,7 @@ class FontImage
1994
2000
  for(let j=line.length; j--;)
1995
2001
  {
1996
2002
  // draw each character
1997
- let charCode = line[j].charCodeAt();
2003
+ let charCode = line[j].charCodeAt(0);
1998
2004
  if (charCode < 32 || charCode > 127)
1999
2005
  charCode = 127; // unknown character
2000
2006
 
@@ -2018,7 +2024,7 @@ class FontImage
2018
2024
  /** Returns true if fullscreen mode is active
2019
2025
  * @return {Boolean}
2020
2026
  * @memberof Draw */
2021
- function isFullscreen() { return document.fullscreenElement; }
2027
+ function isFullscreen() { return !!document.fullscreenElement; }
2022
2028
 
2023
2029
  /** Toggle fullsceen mode
2024
2030
  * @memberof Draw */
@@ -2044,28 +2050,37 @@ function toggleFullscreen()
2044
2050
 
2045
2051
 
2046
2052
  /** Returns true if device key is down
2047
- * @param {Number} key
2048
- * @param {Number} [device=0]
2053
+ * @param {String|Number} key
2054
+ * @param {Number} [device]
2049
2055
  * @return {Boolean}
2050
2056
  * @memberof Input */
2051
2057
  function keyIsDown(key, device=0)
2052
- { return inputData[device] && inputData[device][key] & 1; }
2058
+ {
2059
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
2060
+ return inputData[device] && !!(inputData[device][key] & 1);
2061
+ }
2053
2062
 
2054
2063
  /** Returns true if device key was pressed this frame
2055
- * @param {Number} key
2056
- * @param {Number} [device=0]
2064
+ * @param {String|Number} key
2065
+ * @param {Number} [device]
2057
2066
  * @return {Boolean}
2058
2067
  * @memberof Input */
2059
2068
  function keyWasPressed(key, device=0)
2060
- { return inputData[device] && inputData[device][key] & 2 ? 1 : 0; }
2069
+ {
2070
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
2071
+ return inputData[device] && !!(inputData[device][key] & 2);
2072
+ }
2061
2073
 
2062
2074
  /** Returns true if device key was released this frame
2063
- * @param {Number} key
2064
- * @param {Number} [device=0]
2075
+ * @param {String|Number} key
2076
+ * @param {Number} [device]
2065
2077
  * @return {Boolean}
2066
2078
  * @memberof Input */
2067
2079
  function keyWasReleased(key, device=0)
2068
- { return inputData[device] && inputData[device][key] & 4 ? 1 : 0; }
2080
+ {
2081
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
2082
+ return inputData[device] && !!(inputData[device][key] & 4);
2083
+ }
2069
2084
 
2070
2085
  /** Clears all input
2071
2086
  * @memberof Input */
@@ -2110,16 +2125,16 @@ let mouseWheel = 0;
2110
2125
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
2111
2126
  * @type {Boolean}
2112
2127
  * @memberof Input */
2113
- let isUsingGamepad = 0;
2128
+ let isUsingGamepad = false;
2114
2129
 
2115
2130
  /** Prevents input continuing to the default browser handling (false by default)
2116
2131
  * @type {Boolean}
2117
2132
  * @memberof Input */
2118
- let preventDefaultInput = 0;
2133
+ let preventDefaultInput = false;
2119
2134
 
2120
2135
  /** Returns true if gamepad button is down
2121
2136
  * @param {Number} button
2122
- * @param {Number} [gamepad=0]
2137
+ * @param {Number} [gamepad]
2123
2138
  * @return {Boolean}
2124
2139
  * @memberof Input */
2125
2140
  function gamepadIsDown(button, gamepad=0)
@@ -2127,7 +2142,7 @@ function gamepadIsDown(button, gamepad=0)
2127
2142
 
2128
2143
  /** Returns true if gamepad button was pressed
2129
2144
  * @param {Number} button
2130
- * @param {Number} [gamepad=0]
2145
+ * @param {Number} [gamepad]
2131
2146
  * @return {Boolean}
2132
2147
  * @memberof Input */
2133
2148
  function gamepadWasPressed(button, gamepad=0)
@@ -2135,7 +2150,7 @@ function gamepadWasPressed(button, gamepad=0)
2135
2150
 
2136
2151
  /** Returns true if gamepad button was released
2137
2152
  * @param {Number} button
2138
- * @param {Number} [gamepad=0]
2153
+ * @param {Number} [gamepad]
2139
2154
  * @return {Boolean}
2140
2155
  * @memberof Input */
2141
2156
  function gamepadWasReleased(button, gamepad=0)
@@ -2143,7 +2158,7 @@ function gamepadWasReleased(button, gamepad=0)
2143
2158
 
2144
2159
  /** Returns gamepad stick value
2145
2160
  * @param {Number} stick
2146
- * @param {Number} [gamepad=0]
2161
+ * @param {Number} [gamepad]
2147
2162
  * @return {Vector2}
2148
2163
  * @memberof Input */
2149
2164
  function gamepadStick(stick, gamepad=0)
@@ -2186,9 +2201,10 @@ function inputUpdatePost()
2186
2201
  if (debug && e.target != document.body) return;
2187
2202
  if (!e.repeat)
2188
2203
  {
2189
- inputData[isUsingGamepad = 0][e.which] = 3;
2204
+ isUsingGamepad = false;
2205
+ inputData[0][e.code] = 3;
2190
2206
  if (inputWASDEmulateDirection)
2191
- inputData[0][remapKey(e.which)] = 3;
2207
+ inputData[0][remapKey(e.code)] = 3;
2192
2208
  }
2193
2209
  preventDefaultInput && e.preventDefault();
2194
2210
  }
@@ -2196,26 +2212,29 @@ function inputUpdatePost()
2196
2212
  onkeyup = (e)=>
2197
2213
  {
2198
2214
  if (debug && e.target != document.body) return;
2199
- inputData[0][e.which] = 4;
2215
+ inputData[0][e.code] = 4;
2200
2216
  if (inputWASDEmulateDirection)
2201
- inputData[0][remapKey(e.which)] = 4;
2217
+ inputData[0][remapKey(e.code)] = 4;
2202
2218
  }
2203
2219
 
2204
2220
  // handle remapping wasd keys to directions
2205
2221
  function remapKey(c)
2206
- {
2222
+ {
2207
2223
  return inputWASDEmulateDirection ?
2208
- c==87?38 : c==83?40 : c==65?37 : c==68?39 : c : c;
2224
+ c == 'KeyW' ? 'ArrowUp' :
2225
+ c == 'KeyS' ? 'ArrowDown' :
2226
+ c == 'KeyA' ? 'ArrowLeft' :
2227
+ c == 'KeyD' ? 'ArrowRight' : c : c;
2209
2228
  }
2210
2229
  }
2211
2230
 
2212
2231
  ///////////////////////////////////////////////////////////////////////////////
2213
2232
  // Mouse event handlers
2214
2233
 
2215
- onmousedown = (e)=> {inputData[isUsingGamepad = 0][e.button] = 3; onmousemove(e); e.button && e.preventDefault();}
2234
+ onmousedown = (e)=> {isUsingGamepad = false; inputData[0][e.button] = 3; mousePosScreen = mouseToScreen(e); e.button && e.preventDefault();}
2216
2235
  onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2217
2236
  onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2218
- onwheel = (e)=> e.ctrlKey || (mouseWheel = sign(e.deltaY));
2237
+ onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2219
2238
  oncontextmenu = (e)=> false; // prevent right click menu
2220
2239
 
2221
2240
  // convert a mouse or touch event position to screen space
@@ -2253,7 +2272,7 @@ function gamepadsUpdate()
2253
2272
  for (let i=10; i--;)
2254
2273
  {
2255
2274
  const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
2256
- data[j] = touchGamepadButtons[i] ? 1 + 2*!gamepadIsDown(j,0) : 4*gamepadIsDown(j,0);
2275
+ data[j] = touchGamepadButtons[i] ? gamepadIsDown(j,0) ? 1 : 3 : gamepadIsDown(j,0) ? 4 : 0;
2257
2276
  }
2258
2277
  }
2259
2278
  }
@@ -2285,18 +2304,23 @@ function gamepadsUpdate()
2285
2304
  for (let j = gamepad.buttons.length; j--;)
2286
2305
  {
2287
2306
  const button = gamepad.buttons[j];
2288
- data[j] = button.pressed ? 1 + 2*!gamepadIsDown(j,i) : 4*gamepadIsDown(j,i);
2289
- isUsingGamepad |= !i && button.pressed;
2290
- touchGamepadEnable && touchGamepadTimer.unset(); // disable touch gamepad if using real gamepad
2307
+ const wasDown = gamepadIsDown(j,i);
2308
+ data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
2309
+ isUsingGamepad ||= !i && button.pressed;
2291
2310
  }
2292
2311
 
2293
2312
  if (gamepadDirectionEmulateStick)
2294
2313
  {
2295
2314
  // copy dpad to left analog stick when pressed
2296
- const dpad = vec2(gamepadIsDown(15,i) - gamepadIsDown(14,i), gamepadIsDown(12,i) - gamepadIsDown(13,i));
2315
+ const dpad = vec2(
2316
+ (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
2317
+ (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
2297
2318
  if (dpad.lengthSquared())
2298
2319
  sticks[0] = dpad.clampLength();
2299
2320
  }
2321
+
2322
+ // disable touch gamepad if using real gamepad
2323
+ touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
2300
2324
  }
2301
2325
  }
2302
2326
  }
@@ -2304,9 +2328,9 @@ function gamepadsUpdate()
2304
2328
  ///////////////////////////////////////////////////////////////////////////////
2305
2329
 
2306
2330
  /** Pulse the vibration hardware if it exists
2307
- * @param {Number} [pattern=100] - a single value in miliseconds or vibration interval array
2331
+ * @param {Number|Array} [pattern] - single value in ms or vibration interval array
2308
2332
  * @memberof Input */
2309
- function vibrate(pattern)
2333
+ function vibrate(pattern=100)
2310
2334
  { vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2311
2335
 
2312
2336
  /** Cancel any ongoing vibration
@@ -2324,29 +2348,28 @@ const isTouchDevice = window.ontouchstart !== undefined;
2324
2348
  if (isTouchDevice)
2325
2349
  {
2326
2350
  // override mouse events
2327
- let wasTouching, mouseDown = onmousedown, mouseUp = onmouseup;
2351
+ let wasTouching;
2328
2352
  onmousedown = onmouseup = ()=> 0;
2329
2353
 
2330
2354
  // handle all touch events the same way
2331
2355
  ontouchstart = ontouchmove = ontouchend = (e)=>
2332
2356
  {
2333
- e.button = 0; // all touches are left click
2334
-
2335
2357
  // fix stalled audio on mobile
2336
2358
  if (soundEnable)
2337
2359
  audioContext ? audioContext.resume() : zzfx(0);
2338
2360
 
2339
2361
  // check if touching and pass to mouse events
2340
2362
  const touching = e.touches.length;
2363
+ const button = 0; // all touches are left mouse button
2341
2364
  if (touching)
2342
2365
  {
2343
2366
  // set event pos and pass it along
2344
- e.x = e.touches[0].clientX;
2345
- e.y = e.touches[0].clientY;
2346
- wasTouching ? onmousemove(e) : mouseDown(e);
2367
+ const p = vec2(e.touches[0].clientX, e.touches[0].clientY);
2368
+ mousePosScreen = mouseToScreen(p);
2369
+ wasTouching ? isUsingGamepad = false : inputData[0][button] = 3;
2347
2370
  }
2348
2371
  else if (wasTouching)
2349
- mouseUp(e);
2372
+ inputData[0][button] = inputData[0][button] & 2 | 4;
2350
2373
 
2351
2374
  // set was touching
2352
2375
  wasTouching = touching;
@@ -2427,8 +2450,8 @@ function createTouchGamepad()
2427
2450
  }
2428
2451
 
2429
2452
  // call default touch handler and set to using gamepad
2430
- touchHandler(e);
2431
- isUsingGamepad = 1;
2453
+ touchHandler.bind(window)(e);
2454
+ isUsingGamepad = true;
2432
2455
 
2433
2456
  // must return true so the document will get focus
2434
2457
  return true;
@@ -2442,7 +2465,7 @@ function touchGamepadRender()
2442
2465
  return;
2443
2466
 
2444
2467
  // fade off when not touching or paused
2445
- const alpha = percent(touchGamepadTimer, 4, 3);
2468
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
2446
2469
  if (!alpha || paused)
2447
2470
  return;
2448
2471
 
@@ -2546,13 +2569,13 @@ class Sound
2546
2569
 
2547
2570
  /** Play the sound
2548
2571
  * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
2549
- * @param {Number} [volume=1] - How much to scale volume by (in addition to range fade)
2550
- * @param {Number} [pitch=1] - How much to scale pitch by (also adjusted by this.randomness)
2551
- * @param {Number} [randomnessScale=1] - How much to scale randomness
2552
- * @param {Boolean} [loop=0] - Should the sound loop
2572
+ * @param {Number} [volume] - How much to scale volume by (in addition to range fade)
2573
+ * @param {Number} [pitch] - How much to scale pitch by (also adjusted by this.randomness)
2574
+ * @param {Number} [randomnessScale] - How much to scale randomness
2575
+ * @param {Boolean} [loop] - Should the sound loop
2553
2576
  * @return {AudioBufferSourceNode} - The audio source node
2554
2577
  */
2555
- play(pos, volume=1, pitch=1, randomnessScale=1, loop=0)
2578
+ play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
2556
2579
  {
2557
2580
  if (!soundEnable || !this.sampleChannels) return;
2558
2581
 
@@ -2585,8 +2608,13 @@ class Sound
2585
2608
  {
2586
2609
  if (this.source)
2587
2610
  this.source.stop();
2588
- this.source = 0;
2611
+ this.source = undefined;
2589
2612
  }
2613
+
2614
+ /** Get source of most recent instance of this sound that was played
2615
+ * @return {AudioBufferSourceNode}
2616
+ */
2617
+ getSource() { return this.source; }
2590
2618
 
2591
2619
  /** Play the sound as a note with a semitone offset
2592
2620
  * @param {Number} semitoneOffset - How many semitones to offset pitch
@@ -2602,12 +2630,7 @@ class Sound
2602
2630
  */
2603
2631
  getDuration()
2604
2632
  { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
2605
-
2606
- /** Check if the last instance of this sound is playing
2607
- * @return {Boolean} - True if the sound is playing
2608
- */
2609
- isPlaying() { return this.source && !this.source.ended; }
2610
-
2633
+
2611
2634
  /** Check if sound is loading, for sounds fetched from a url
2612
2635
  * @return {Boolean} - True if sound is loading and not ready to play
2613
2636
  */
@@ -2628,13 +2651,13 @@ class SoundWave extends Sound
2628
2651
  {
2629
2652
  /** Create a sound object and cache the wave file for later use
2630
2653
  * @param {String} filename - Filename of audio file to load
2631
- * @param {Number} [randomness=0] - How much to randomize frequency each time sound plays
2654
+ * @param {Number} [randomness] - How much to randomize frequency each time sound plays
2632
2655
  * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2633
2656
  * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
2634
2657
  */
2635
2658
  constructor(filename, randomness=0, range, taper)
2636
2659
  {
2637
- super(0, range, taper);
2660
+ super(undefined, range, taper);
2638
2661
  this.randomness = randomness;
2639
2662
 
2640
2663
  if (!soundEnable) return;
@@ -2688,11 +2711,11 @@ let soundDecoderContext; // audio context used only to decode audio files
2688
2711
  class Music extends Sound
2689
2712
  {
2690
2713
  /** Create a music object and cache the zzfx music samples for later use
2691
- * @param {Array} zzfxMusic - Array of zzfx music parameters
2714
+ * @param {[Array, Array, Array, Number]} zzfxMusic - Array of zzfx music parameters
2692
2715
  */
2693
2716
  constructor(zzfxMusic)
2694
2717
  {
2695
- super();
2718
+ super(undefined);
2696
2719
 
2697
2720
  if (!soundEnable) return;
2698
2721
  this.randomness = 0;
@@ -2705,17 +2728,17 @@ class Music extends Sound
2705
2728
  * @param {Boolean} [loop=1] - True if the music should loop
2706
2729
  * @return {AudioBufferSourceNode} - The audio source node
2707
2730
  */
2708
- playMusic(volume, loop = 1)
2709
- { return super.play(0, volume, 1, 1, loop); }
2731
+ playMusic(volume, loop = false)
2732
+ { return super.play(undefined, volume, 1, 1, loop); }
2710
2733
  }
2711
2734
 
2712
2735
  /** Play an mp3, ogg, or wav audio from a local file or url
2713
2736
  * @param {String} url - Location of sound file to play
2714
- * @param {Number} [volume=1] - How much to scale volume by
2715
- * @param {Boolean} [loop=1] - True if the music should loop
2737
+ * @param {Number} [volume] - How much to scale volume by
2738
+ * @param {Boolean} [loop] - True if the music should loop
2716
2739
  * @return {HTMLAudioElement} - The audio element for this sound
2717
2740
  * @memberof Audio */
2718
- function playAudioFile(url, volume=1, loop=1)
2741
+ function playAudioFile(url, volume=1, loop=false)
2719
2742
  {
2720
2743
  if (!soundEnable) return;
2721
2744
 
@@ -2729,9 +2752,9 @@ function playAudioFile(url, volume=1, loop=1)
2729
2752
  /** Speak text with passed in settings
2730
2753
  * @param {String} text - The text to speak
2731
2754
  * @param {String} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
2732
- * @param {Number} [volume=1] - How much to scale volume by
2733
- * @param {Number} [rate=1] - How quickly to speak
2734
- * @param {Number} [pitch=1] - How much to change the pitch by
2755
+ * @param {Number} [volume] - How much to scale volume by
2756
+ * @param {Number} [rate] - How quickly to speak
2757
+ * @param {Number} [pitch] - How much to change the pitch by
2735
2758
  * @return {SpeechSynthesisUtterance} - The utterance that was spoken
2736
2759
  * @memberof Audio */
2737
2760
  function speak(text, language='', volume=1, rate=1, pitch=1)
@@ -2758,7 +2781,7 @@ function speakStop() {speechSynthesis && speechSynthesis.cancel();}
2758
2781
 
2759
2782
  /** Get frequency of a note on a musical scale
2760
2783
  * @param {Number} semitoneOffset - How many semitones away from the root note
2761
- * @param {Number} [rootNoteFrequency=220] - Frequency at semitone offset 0
2784
+ * @param {Number} [rootFrequency=220] - Frequency at semitone offset 0
2762
2785
  * @return {Number} - The frequency of the note
2763
2786
  * @memberof Audio */
2764
2787
  function getNoteFrequency(semitoneOffset, rootFrequency=220)
@@ -2772,14 +2795,14 @@ let audioContext;
2772
2795
 
2773
2796
  /** Play cached audio samples with given settings
2774
2797
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
2775
- * @param {Number} [volume=1] - How much to scale volume by
2776
- * @param {Number} [rate=1] - The playback rate to use
2777
- * @param {Number} [pan=0] - How much to apply stereo panning
2778
- * @param {Boolean} [loop=0] - True if the sound should loop when it reaches the end
2798
+ * @param {Number} [volume] - How much to scale volume by
2799
+ * @param {Number} [rate] - The playback rate to use
2800
+ * @param {Number} [pan] - How much to apply stereo panning
2801
+ * @param {Boolean} [loop] - True if the sound should loop when it reaches the end
2779
2802
  * @param {Number} [sampleRate=44100] - Sample rate for the sound
2780
2803
  * @return {AudioBufferSourceNode} - The audio node of the sound played
2781
2804
  * @memberof Audio */
2782
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0, sampleRate=zzfxR)
2805
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
2783
2806
  {
2784
2807
  if (!soundEnable) return;
2785
2808
 
@@ -2819,7 +2842,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0, sampleRate
2819
2842
  }
2820
2843
 
2821
2844
  ///////////////////////////////////////////////////////////////////////////////
2822
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.0 by Frank Force
2845
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
2823
2846
 
2824
2847
  /** Generate and play a ZzFX sound
2825
2848
  *
@@ -2835,27 +2858,27 @@ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
2835
2858
  const zzfxR = 44100;
2836
2859
 
2837
2860
  /** Generate samples for a ZzFX sound
2838
- * @param {Number} [volume=1] - Volume scale (percent)
2839
- * @param {Number} [randomness=.05] - How much to randomize frequency (percent Hz)
2840
- * @param {Number} [frequency=220] - Frequency of sound (Hz)
2841
- * @param {Number} [attack=0] - Attack time, how fast sound starts (seconds)
2842
- * @param {Number} [sustain=0] - Sustain time, how long sound holds (seconds)
2843
- * @param {Number} [release=.1] - Release time, how fast sound fades out (seconds)
2844
- * @param {Number} [shape=0] - Shape of the sound wave
2845
- * @param {Number} [shapeCurve=1] - Squarenes of wave (0=square, 1=normal, 2=pointy)
2846
- * @param {Number} [slide=0] - How much to slide frequency (kHz/s)
2847
- * @param {Number} [deltaSlide=0] - How much to change slide (kHz/s/s)
2848
- * @param {Number} [pitchJump=0] - Frequency of pitch jump (Hz)
2849
- * @param {Number} [pitchJumpTime=0] - Time of pitch jump (seconds)
2850
- * @param {Number} [repeatTime=0] - Resets some parameters periodically (seconds)
2851
- * @param {Number} [noise=0] - How much random noise to add (percent)
2852
- * @param {Number} [modulation=0] - Frequency of modulation wave, negative flips phase (Hz)
2853
- * @param {Number} [bitCrush=0] - Resamples at a lower frequency in (samples*100)
2854
- * @param {Number} [delay=0] - Overlap sound with itself for reverb and flanger effects (seconds)
2855
- * @param {Number} [sustainVolume=1] - Volume level for sustain (percent)
2856
- * @param {Number} [decay=0] - Decay time, how long to reach sustain after attack (seconds)
2857
- * @param {Number} [tremolo=0] - Trembling effect, rate controlled by repeat time (precent)
2858
- * @param {Number} [filter=0] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
2861
+ * @param {Number} [volume] - Volume scale (percent)
2862
+ * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
2863
+ * @param {Number} [frequency] - Frequency of sound (Hz)
2864
+ * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
2865
+ * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
2866
+ * @param {Number} [release] - Release time, how fast sound fades out (seconds)
2867
+ * @param {Number} [shape] - Shape of the sound wave
2868
+ * @param {Number} [shapeCurve] - Squarenes of wave (0=square, 1=normal, 2=pointy)
2869
+ * @param {Number} [slide] - How much to slide frequency (kHz/s)
2870
+ * @param {Number} [deltaSlide] - How much to change slide (kHz/s/s)
2871
+ * @param {Number} [pitchJump] - Frequency of pitch jump (Hz)
2872
+ * @param {Number} [pitchJumpTime] - Time of pitch jump (seconds)
2873
+ * @param {Number} [repeatTime] - Resets some parameters periodically (seconds)
2874
+ * @param {Number} [noise] - How much random noise to add (percent)
2875
+ * @param {Number} [modulation] - Frequency of modulation wave, negative flips phase (Hz)
2876
+ * @param {Number} [bitCrush] - Resamples at a lower frequency in (samples*100)
2877
+ * @param {Number} [delay] - Overlap sound with itself for reverb and flanger effects (seconds)
2878
+ * @param {Number} [sustainVolume] - Volume level for sustain (percent)
2879
+ * @param {Number} [decay] - Decay time, how long to reach sustain after attack (seconds)
2880
+ * @param {Number} [tremolo] - Trembling effect, rate controlled by repeat time (precent)
2881
+ * @param {Number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
2859
2882
  * @return {Array} - Array of audio samples
2860
2883
  * @memberof Audio
2861
2884
  */
@@ -2903,7 +2926,7 @@ function zzfxG
2903
2926
  if (!(++c%(bitCrush*100|0))) // bit crush
2904
2927
  {
2905
2928
  s = shape? shape>1? shape>2? shape>3? // wave shape
2906
- Math.sin(t*t) : // 4 noise
2929
+ Math.sin(t**3) : // 4 noise
2907
2930
  clamp(Math.tan(t),1,-1): // 3 tan
2908
2931
  1-(2*t/PI2%2+2)%2: // 2 saw
2909
2932
  1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
@@ -2960,7 +2983,7 @@ function zzfxG
2960
2983
  * @param {Array} instruments - Array of ZzFX sound paramaters
2961
2984
  * @param {Array} patterns - Array of pattern data
2962
2985
  * @param {Array} sequence - Array of pattern indexes
2963
- * @param {Number} [BPM=125] - Playback speed of the song in BPM
2986
+ * @param {Number} [BPM] - Playback speed of the song in BPM
2964
2987
  * @return {Array} - Left and right channel sample data
2965
2988
  * @memberof Audio */
2966
2989
  function zzfxM(instruments, patterns, sequence, BPM = 125)
@@ -2999,10 +3022,10 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2999
3022
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
3000
3023
 
3001
3024
  // check if there are more channels
3002
- hasMore ||= !!patterns[patternIndex][channelIndex];
3025
+ hasMore |= patterns[patternIndex][channelIndex]&&1;
3003
3026
 
3004
3027
  // get next offset, use the length of first channel
3005
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - !notFirstBeat) * beatLength;
3028
+ nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
3006
3029
  // for each beat in pattern, plus one extra if end of sequence
3007
3030
  isSequenceEnd = sequenceIndex == sequence.length - 1;
3008
3031
  for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
@@ -3018,7 +3041,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
3018
3041
  for (j = 0; j < beatLength && notFirstBeat;
3019
3042
 
3020
3043
  // fade off attenuation at end of beat if stopping note, prevents clicking
3021
- j++ > beatLength - 99 && stop ? attenuation += (attenuation < 1) / 99 : 0
3044
+ j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
3022
3045
  ) {
3023
3046
  // copy sample to stereo buffers with panning
3024
3047
  sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
@@ -3094,7 +3117,7 @@ function initTileCollision(size)
3094
3117
 
3095
3118
  /** Set tile collision data
3096
3119
  * @param {Vector2} pos
3097
- * @param {Number} [data=0]
3120
+ * @param {Number} [data]
3098
3121
  * @memberof TileCollision */
3099
3122
  function setTileCollisionData(pos, data=0)
3100
3123
  {
@@ -3112,7 +3135,7 @@ function getTileCollisionData(pos)
3112
3135
 
3113
3136
  /** Check if collision with another object should occur
3114
3137
  * @param {Vector2} pos
3115
- * @param {Vector2} [size=Vector2(1,1)]
3138
+ * @param {Vector2} [size=(1,1)]
3116
3139
  * @param {EngineObject} [object]
3117
3140
  * @return {Boolean}
3118
3141
  * @memberof TileCollision */
@@ -3127,7 +3150,7 @@ function tileCollisionTest(pos, size=vec2(), object)
3127
3150
  {
3128
3151
  const tileData = tileCollision[y*tileCollisionSize.x+x];
3129
3152
  if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
3130
- return 1;
3153
+ return true;
3131
3154
  }
3132
3155
  }
3133
3156
 
@@ -3193,11 +3216,11 @@ function tileCollisionRaycast(posStart, posEnd, object)
3193
3216
  class TileLayerData
3194
3217
  {
3195
3218
  /** Create a tile layer data object, one for each tile in a TileLayer
3196
- * @param {Number} [tile] - The tile to use, untextured if undefined
3197
- * @param {Number} [direction=0] - Integer direction of tile, in 90 degree increments
3198
- * @param {Boolean} [mirror=0] - If the tile should be mirrored along the x axis
3199
- * @param {Color} [color=Color()] - Color of the tile */
3200
- constructor(tile, direction=0, mirror=0, color=new Color())
3219
+ * @param {Number} [tile] - The tile to use, untextured if undefined
3220
+ * @param {Number} [direction] - Integer direction of tile, in 90 degree increments
3221
+ * @param {Boolean} [mirror] - If the tile should be mirrored along the x axis
3222
+ * @param {Color} [color] - Color of the tile */
3223
+ constructor(tile, direction=0, mirror=false, color=new Color())
3201
3224
  {
3202
3225
  /** @property {Number} - The tile to use, untextured if undefined */
3203
3226
  this.tile = tile;
@@ -3210,7 +3233,7 @@ class TileLayerData
3210
3233
  }
3211
3234
 
3212
3235
  /** Set this tile to clear, it will not be rendered */
3213
- clear() { this.tile = this.direction = this.mirror = 0; color = new Color; }
3236
+ clear() { this.tile = this.direction = 0; this.mirror = false; this.color = new Color; }
3214
3237
  }
3215
3238
 
3216
3239
  /**
@@ -3227,25 +3250,25 @@ class TileLayerData
3227
3250
  */
3228
3251
  class TileLayer extends EngineObject
3229
3252
  {
3230
- /** Create a tile layer object
3231
- * @param {Vector2} [position=Vector2()] - World space position
3232
- * @param {Vector2} [size=tileCollisionSize] - World space size
3233
- * @param {TileInfo} [tileInfo] - Tile info for layer
3234
- * @param {Vector2} [scale=Vector2(1,1)] - How much to scale this layer when rendered
3235
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
3253
+ /** Create a tile layer object
3254
+ * @param {Vector2} [position=(0,0)] - World space position
3255
+ * @param {Vector2} [size=tileCollisionSize] - World space size
3256
+ * @param {TileInfo} [tileInfo] - Tile info for layer
3257
+ * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
3258
+ * @param {Number} [renderOrder] - Objects are sorted by renderOrder
3236
3259
  */
3237
- constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderOrder=0)
3260
+ constructor(position, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderOrder=0)
3238
3261
  {
3239
- super(pos, size, tileInfo, 0, undefined, renderOrder);
3262
+ super(position, size, tileInfo, 0, undefined, renderOrder);
3240
3263
 
3241
- /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3264
+ /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3242
3265
  this.canvas = document.createElement('canvas');
3243
3266
  /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3244
3267
  this.context = this.canvas.getContext('2d');
3245
- /** @property {Vector2} - How much to scale this layer when rendered */
3268
+ /** @property {Vector2} - How much to scale this layer when rendered */
3246
3269
  this.scale = scale;
3247
- /** @property {Boolean} [isOverlay=0] - If true this layer will render to overlay canvas and appear above all objects */
3248
- this.isOverlay;
3270
+ /** @property {Boolean} - If true this layer will render to overlay canvas and appear above all objects */
3271
+ this.isOverlay = false;
3249
3272
 
3250
3273
  // init tile data
3251
3274
  this.data = [];
@@ -3254,10 +3277,10 @@ constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderO
3254
3277
  }
3255
3278
 
3256
3279
  /** Set data at a given position in the array
3257
- * @param {Vector2} position - Local position in array
3258
- * @param {TileLayerData} data - Data to set
3259
- * @param {Boolean} [redraw=0] - Force the tile to redraw if true */
3260
- setData(layerPos, data, redraw)
3280
+ * @param {Vector2} layerPos - Local position in array
3281
+ * @param {TileLayerData} data - Data to set
3282
+ * @param {Boolean} [redraw] - Force the tile to redraw if true */
3283
+ setData(layerPos, data, redraw=false)
3261
3284
  {
3262
3285
  if (layerPos.arrayCheck(this.size))
3263
3286
  {
@@ -3278,7 +3301,7 @@ constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderO
3278
3301
  // Render the tile layer, called automatically by the engine
3279
3302
  render()
3280
3303
  {
3281
- ASSERT(mainContext != this.context); // must call redrawEnd() after drawing tiles
3304
+ ASSERT(mainContext != this.context, 'must call redrawEnd() after drawing tiles');
3282
3305
 
3283
3306
  // flush and copy gl canvas because tile canvas does not use webgl
3284
3307
  glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
@@ -3293,33 +3316,38 @@ constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderO
3293
3316
  }
3294
3317
 
3295
3318
  /** Draw all the tile data to an offscreen canvas
3296
- * - This may be slow in some browsers
3297
- */
3319
+ * - This may be slow in some browsers but only needs to be done once */
3298
3320
  redraw()
3299
3321
  {
3300
- this.redrawStart(1);
3301
- this.drawAllTileData();
3322
+ this.redrawStart(true);
3323
+ for (let x = this.size.x; x--;)
3324
+ for (let y = this.size.y; y--;)
3325
+ this.drawTileData(vec2(x,y), false);
3302
3326
  this.redrawEnd();
3303
3327
  }
3304
3328
 
3305
3329
  /** Call to start the redraw process
3306
- * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
3307
- redrawStart(clear = 0)
3330
+ * - This can be used to manually update small parts of the level
3331
+ * @param {Boolean} [clear] - Should it clear the canvas before drawing */
3332
+ redrawStart(clear=false)
3308
3333
  {
3309
3334
  // save current render settings
3335
+ /** @type {[HTMLCanvasElement, CanvasRenderingContext2D, Vector2, Vector2, number]} */
3310
3336
  this.savedRenderSettings = [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale];
3311
3337
 
3312
- // hack: use normal rendering system to render the tiles
3338
+ // use webgl rendering system to render the tiles if enabled
3339
+ // this works by temporally taking control of the rendering system
3313
3340
  mainCanvas = this.canvas;
3314
3341
  mainContext = this.context;
3342
+ mainCanvasSize = this.size.multiply(this.tileInfo.size);
3315
3343
  cameraPos = this.size.scale(.5);
3316
3344
  cameraScale = this.tileInfo.size.x;
3317
3345
 
3318
3346
  if (clear)
3319
3347
  {
3320
3348
  // clear and set size
3321
- mainCanvas.width = this.size.x * this.tileInfo.size.x;
3322
- mainCanvas.height = this.size.y * this.tileInfo.size.y;
3349
+ mainCanvas.width = mainCanvasSize.x;
3350
+ mainCanvas.height = mainCanvasSize.y;
3323
3351
  }
3324
3352
 
3325
3353
  // begin a new render for the tile canvas
@@ -3329,40 +3357,41 @@ constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderO
3329
3357
  /** Call to end the redraw process */
3330
3358
  redrawEnd()
3331
3359
  {
3332
- ASSERT(mainContext == this.context); // must call redrawStart() before drawing tiles
3333
- glEnable && glCopyToContext(mainContext, 1);
3360
+ ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3361
+ glEnable && glCopyToContext(mainContext, true);
3334
3362
  //debugSaveCanvas(this.canvas);
3335
3363
 
3336
3364
  // set stuff back to normal
3337
3365
  [mainCanvas, mainContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
3338
3366
  }
3339
3367
 
3340
- /** Draw the tile at a given position
3341
- * @param {Vector2} layerPos */
3342
- drawTileData(layerPos)
3368
+ /** Draw the tile at a given position in the tile grid
3369
+ * This can be used to clear out tiles when they are destroyed
3370
+ * Tiles can also be redrawn if isinde a redrawStart/End block
3371
+ * @param {Vector2} layerPos
3372
+ * @param {Boolean} [clear] - should the old tile be cleared out
3373
+ */
3374
+ drawTileData(layerPos, clear=true)
3343
3375
  {
3344
- // first clear out where the tile was
3345
- const pos = layerPos.floor().add(this.pos).add(vec2(.5));
3346
- this.drawCanvas2D(pos, vec2(1), 0, 0, (context)=>context.clearRect(-.5, -.5, 1, 1));
3376
+ // clear out where the tile was, for full opaque tiles this can be skipped
3377
+ const s = this.tileInfo.size;
3378
+ if (clear)
3379
+ {
3380
+ const pos = layerPos.multiply(s);
3381
+ this.context.clearRect(pos.x, this.canvas.height-pos.y, s.x, -s.y);
3382
+ }
3347
3383
 
3348
3384
  // draw the tile if not undefined
3349
3385
  const d = this.getData(layerPos);
3350
3386
  if (d.tile != undefined)
3351
3387
  {
3352
- ASSERT(mainContext == this.context); // must call redrawStart() before drawing tiles
3353
- const tileInfo = tile(d.tile, this.tileInfo.size, this.tileInfo.textureIndex);
3388
+ const pos = this.pos.add(layerPos).add(vec2(.5));
3389
+ ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3390
+ const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex);
3354
3391
  drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
3355
3392
  }
3356
3393
  }
3357
3394
 
3358
- /** Draw all the tiles in this layer */
3359
- drawAllTileData()
3360
- {
3361
- for (let x = this.size.x; x--;)
3362
- for (let y = this.size.y; y--;)
3363
- this.drawTileData(vec2(x,y));
3364
- }
3365
-
3366
3395
  /** Draw directly to the 2D canvas in world space (bipass webgl)
3367
3396
  * @param {Vector2} pos
3368
3397
  * @param {Vector2} size
@@ -3382,11 +3411,11 @@ constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderO
3382
3411
  context.restore();
3383
3412
  }
3384
3413
 
3385
- /** Draw a tile directly onto the layer canvas
3414
+ /** Draw a tile directly onto the layer canvas in world space
3386
3415
  * @param {Vector2} pos
3387
- * @param {Vector2} [size=Vector2(1,1)]
3416
+ * @param {Vector2} [size=(1,1)]
3388
3417
  * @param {TileInfo} [tileInfo]
3389
- * @param {Color} [color=Color()]
3418
+ * @param {Color} [color=(1,1,1,1)]
3390
3419
  * @param {Number} [angle=0]
3391
3420
  * @param {Boolean} [mirror=0] */
3392
3421
  drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle, mirror)
@@ -3411,13 +3440,13 @@ constructor(pos, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderO
3411
3440
  });
3412
3441
  }
3413
3442
 
3414
- /** Draw a rectangle directly onto the layer canvas
3443
+ /** Draw a rectangle directly onto the layer canvas in world space
3415
3444
  * @param {Vector2} pos
3416
- * @param {Vector2} [size=Vector2(1,1)]
3417
- * @param {Color} [color=Color()]
3445
+ * @param {Vector2} [size=(1,1)]
3446
+ * @param {Color} [color=(1,1,1,1)]
3418
3447
  * @param {Number} [angle=0] */
3419
3448
  drawRect(pos, size, color, angle)
3420
- { this.drawTile(pos, size, -1, 0, color, angle); }
3449
+ { this.drawTile(pos, size, undefined, color, angle); }
3421
3450
  }
3422
3451
  /**
3423
3452
  * LittleJS Particle System
@@ -3446,36 +3475,36 @@ class ParticleEmitter extends EngineObject
3446
3475
  {
3447
3476
  /** Create a particle system with the given settings
3448
3477
  * @param {Vector2} position - World space position of the emitter
3449
- * @param {Number} [angle=0] - Angle to emit the particles
3450
- * @param {Number|Vector2} [emitSize=0] - World space size of the emitter (float for circle diameter, vec2 for rect)
3451
- * @param {Number} [emitTime=0] - How long to stay alive (0 is forever)
3452
- * @param {Number} [emitRate=100] - How many particles per second to spawn, does not emit if 0
3453
- * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
3478
+ * @param {Number} [angle] - Angle to emit the particles
3479
+ * @param {Number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
3480
+ * @param {Number} [emitTime] - How long to stay alive (0 is forever)
3481
+ * @param {Number} [emitRate] - How many particles per second to spawn, does not emit if 0
3482
+ * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
3454
3483
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
3455
- * @param {Color} [colorStartA=Color()] - Color at start of life 1, randomized between start colors
3456
- * @param {Color} [colorStartB=Color()] - Color at start of life 2, randomized between start colors
3457
- * @param {Color} [colorEndA=Color(1,1,1,0)] - Color at end of life 1, randomized between end colors
3458
- * @param {Color} [colorEndB=Color(1,1,1,0)] - Color at end of life 2, randomized between end colors
3459
- * @param {Number} [particleTime=.5] - How long particles live
3460
- * @param {Number} [sizeStart=.1] - How big are particles at start
3461
- * @param {Number} [sizeEnd=1] - How big are particles at end
3462
- * @param {Number} [speed=.1] - How fast are particles when spawned
3463
- * @param {Number} [angleSpeed=.05] - How fast are particles rotating
3464
- * @param {Number} [damping=1] - How much to dampen particle speed
3465
- * @param {Number} [angleDamping=1] - How much to dampen particle angular speed
3466
- * @param {Number} [gravityScale=0] - How much does gravity effect particles
3467
- * @param {Number} [particleConeAngle=PI] - Cone for start particle angle
3468
- * @param {Number} [fadeRate=.1] - How quick to fade in particles at start/end in percent of life
3469
- * @param {Number} [randomness=.2] - Apply extra randomness percent
3470
- * @param {Boolean} [collideTiles=0] - Do particles collide against tiles
3471
- * @param {Boolean} [additive=0] - Should particles use addtive blend
3472
- * @param {Boolean} [randomColorLinear=1] - Should color be randomized linearly or across each component
3473
- * @param {Number} [renderOrder=0] - Render order for particles (additive is above other stuff by default)
3474
- * @param {Boolean} [localSpace=0] - Should it be in local space of emitter (world space is default)
3484
+ * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
3485
+ * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
3486
+ * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
3487
+ * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
3488
+ * @param {Number} [particleTime] - How long particles live
3489
+ * @param {Number} [sizeStart] - How big are particles at start
3490
+ * @param {Number} [sizeEnd] - How big are particles at end
3491
+ * @param {Number} [speed] - How fast are particles when spawned
3492
+ * @param {Number} [angleSpeed] - How fast are particles rotating
3493
+ * @param {Number} [damping] - How much to dampen particle speed
3494
+ * @param {Number} [angleDamping] - How much to dampen particle angular speed
3495
+ * @param {Number} [gravityScale] - How much gravity effect particles
3496
+ * @param {Number} [particleConeAngle] - Cone for start particle angle
3497
+ * @param {Number} [fadeRate] - How quick to fade particles at start/end in percent of life
3498
+ * @param {Number} [randomness] - Apply extra randomness percent
3499
+ * @param {Boolean} [collideTiles] - Do particles collide against tiles
3500
+ * @param {Boolean} [additive] - Should particles use addtive blend
3501
+ * @param {Boolean} [randomColorLinear] - Should color be randomized linearly or across each component
3502
+ * @param {Number} [renderOrder] - Render order for particles (additive is above other stuff by default)
3503
+ * @param {Boolean} [localSpace] - Should it be in local space of emitter (world space is default)
3475
3504
  */
3476
3505
  constructor
3477
3506
  (
3478
- pos,
3507
+ position,
3479
3508
  angle,
3480
3509
  emitSize = 0,
3481
3510
  emitTime = 0,
@@ -3497,14 +3526,14 @@ class ParticleEmitter extends EngineObject
3497
3526
  particleConeAngle = PI,
3498
3527
  fadeRate = .1,
3499
3528
  randomness = .2,
3500
- collideTiles,
3501
- additive,
3502
- randomColorLinear = 1,
3529
+ collideTiles = false,
3530
+ additive = false,
3531
+ randomColorLinear = true,
3503
3532
  renderOrder = additive ? 1e9 : 0,
3504
- localSpace
3533
+ localSpace = false
3505
3534
  )
3506
3535
  {
3507
- super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
3536
+ super(position, vec2(), tileInfo, angle, undefined, renderOrder);
3508
3537
 
3509
3538
  // emitter settings
3510
3539
  /** @property {Number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
@@ -3553,14 +3582,17 @@ class ParticleEmitter extends EngineObject
3553
3582
  this.randomness = randomness;
3554
3583
  /** @property {Boolean} - Do particles collide against tiles */
3555
3584
  this.collideTiles = collideTiles;
3556
- /** @property {Number} - Should particles use addtive blend */
3585
+ /** @property {Boolean} - Should particles use addtive blend */
3557
3586
  this.additive = additive;
3558
3587
  /** @property {Boolean} - Should it be in local space of emitter */
3559
- this.localSpace = localSpace;
3560
- /** @property {Number} - If set the partile is drawn as a trail, stretched in the drection of velocity */
3588
+ this.localSpace = localSpace;
3589
+ /** @property {Number} - If non zero the partile is drawn as a trail, stretched in the drection of velocity */
3561
3590
  this.trailScale = 0;
3562
-
3563
- // internal variables
3591
+ /** @property {Function} - Callback when particle is destroyed */
3592
+ this.particleDestroyCallback = undefined;
3593
+ /** @property {Function} - Callback when particle is created */
3594
+ this.particleCreateCallback = undefined;
3595
+ /** @property {Number} - Track particle emit time */
3564
3596
  this.emitTimeBuffer = 0;
3565
3597
  }
3566
3598
 
@@ -3592,18 +3624,16 @@ class ParticleEmitter extends EngineObject
3592
3624
  emitParticle()
3593
3625
  {
3594
3626
  // spawn a particle
3595
- let pos = this.emitSize.x != undefined ? // check if vec2 was used for size
3596
- vec2(rand(-.5,.5), rand(-.5,.5))
3597
- .multiply(this.emitSize).rotate(this.angle) // box emitter
3598
- : randInCircle(this.emitSize/2); // circle emitter
3627
+ let pos = typeof this.emitSize === 'number' ? // check if number was used
3628
+ randInCircle(this.emitSize/2) // circle emitter
3629
+ : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
3630
+ .multiply(this.emitSize).rotate(this.angle)
3599
3631
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
3600
3632
  if (!this.localSpace)
3601
3633
  {
3602
3634
  pos = this.pos.add(pos);
3603
3635
  angle += this.angle;
3604
3636
  }
3605
-
3606
- const particle = new Particle(pos, this.tileInfo, this.tileSize, angle);
3607
3637
 
3608
3638
  // randomness scales each paremeter by a percentage
3609
3639
  const randomness = this.randomness;
@@ -3619,30 +3649,21 @@ class ParticleEmitter extends EngineObject
3619
3649
  const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
3620
3650
  const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
3621
3651
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
3622
-
3623
- // build particle settings
3624
- particle.colorStart = colorStart;
3625
- particle.colorEndDelta = colorEnd.subtract(colorStart);
3626
- particle.velocity = vec2().setAngle(velocityAngle, speed);
3627
- particle.angleVelocity = angleSpeed;
3628
- particle.lifeTime = particleTime;
3629
- particle.sizeStart = sizeStart;
3630
- particle.sizeEndDelta = sizeEnd - sizeStart;
3631
- particle.fadeRate = this.fadeRate;
3632
- particle.damping = this.damping;
3633
- particle.angleDamping = this.angleDamping;
3634
- particle.elasticity = this.elasticity;
3635
- particle.friction = this.friction;
3636
- particle.gravityScale = this.gravityScale;
3637
- particle.collideTiles = this.collideTiles;
3638
- particle.additive = this.additive;
3639
- particle.renderOrder = this.renderOrder;
3640
- particle.trailScale = this.trailScale;
3641
- particle.mirror = randInt(2);
3642
- particle.localSpaceEmitter = this.localSpace && this;
3643
-
3644
- // setup callbacks for particles
3645
- particle.destroyCallback = this.particleDestroyCallback;
3652
+
3653
+ // build particle
3654
+ const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
3655
+ particle.velocity = vec2().setAngle(velocityAngle, speed);
3656
+ particle.fadeRate = this.fadeRate;
3657
+ particle.damping = this.damping;
3658
+ particle.angleDamping = this.angleDamping;
3659
+ particle.elasticity = this.elasticity;
3660
+ particle.friction = this.friction;
3661
+ particle.gravityScale = this.gravityScale;
3662
+ particle.collideTiles = this.collideTiles;
3663
+ particle.renderOrder = this.renderOrder;
3664
+ particle.mirror = !!randInt(2);
3665
+
3666
+ // call particle create callaback
3646
3667
  this.particleCreateCallback && this.particleCreateCallback(particle);
3647
3668
 
3648
3669
  // return the newly created particle
@@ -3661,13 +3682,47 @@ class ParticleEmitter extends EngineObject
3661
3682
  class Particle extends EngineObject
3662
3683
  {
3663
3684
  /**
3664
- * Create a particle with the given settings
3665
- * @param {Vector2} position - World space position of the particle
3666
- * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
3667
- * @param {Number} [angle=0] - Angle to rotate the particle
3685
+ * Create a particle with the given shis.colorStart = undefined;ettings
3686
+ * @param {Vector2} position - World space position of the particle
3687
+ * @param {TileInfo} [tileInfo] - Tile info to render particles
3688
+ * @param {Number} [angle] - Angle to rotate the particle
3689
+ * @param {Color} [colorStart] - Color at start of life
3690
+ * @param {Color} [colorEnd] - Color at end of life
3691
+ * @param {Number} [lifeTime] - How long to live for
3692
+ * @param {Number} [sizeStart] - Angle to rotate the particle
3693
+ * @param {Number} [sizeEnd] - Angle to rotate the particle
3694
+ * @param {Number} [fadeRate] - Angle to rotate the particle
3695
+ * @param {Boolean} [additive] - Angle to rotate the particle
3696
+ * @param {Number} [trailScale] - If a trail, how long to make it
3697
+ * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
3698
+ * @param {Function} [destroyCallback] - Called when particle dies
3668
3699
  */
3669
- constructor(pos, tileInfo, angle)
3670
- { super(pos, vec2(), tileInfo, angle); }
3700
+ constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
3701
+ )
3702
+ {
3703
+ super(position, vec2(), tileInfo, angle);
3704
+
3705
+ /** @property {Color} - Color at start of life */
3706
+ this.colorStart = colorStart;
3707
+ /** @property {Color} - Calculated change in color */
3708
+ this.colorEndDelta = colorEnd.subtract(colorStart);
3709
+ /** @property {Number} - How long to live for */
3710
+ this.lifeTime = lifeTime;
3711
+ /** @property {Number} - Size at start of life */
3712
+ this.sizeStart = sizeStart;
3713
+ /** @property {Number} - Calculated change in size */
3714
+ this.sizeEndDelta = sizeEnd - sizeStart;
3715
+ /** @property {Number} - How quick to fade in/out */
3716
+ this.fadeRate = fadeRate;
3717
+ /** @property {Boolean} - Is it additive */
3718
+ this.additive = additive;
3719
+ /** @property {Number} - If a trail, how long to make it */
3720
+ this.trailScale = trailScale;
3721
+ /** @property {ParticleEmitter} - Parent emitter if local space */
3722
+ this.localSpaceEmitter = localSpaceEmitter;
3723
+ /** @property {Function} - Called when particle dies */
3724
+ this.destroyCallback = destroyCallback;
3725
+ }
3671
3726
 
3672
3727
  /** Render the particle, automatically called each frame, sorted by renderOrder */
3673
3728
  render()
@@ -3685,7 +3740,7 @@ class Particle extends EngineObject
3685
3740
  (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
3686
3741
 
3687
3742
  // draw the particle
3688
- this.additive && setBlendMode(1);
3743
+ this.additive && setBlendMode(true);
3689
3744
 
3690
3745
  let pos = this.pos, angle = this.angle;
3691
3746
  if (this.localSpaceEmitter)
@@ -3775,7 +3830,7 @@ class Medal
3775
3830
  * @param {Number} id - The unique identifier of the medal
3776
3831
  * @param {String} name - Name of the medal
3777
3832
  * @param {String} [description] - Description of the medal
3778
- * @param {String} [icon='🏆'] - Icon for the medal
3833
+ * @param {String} [icon] - Icon for the medal
3779
3834
  * @param {String} [src] - Image location for the medal
3780
3835
  */
3781
3836
  constructor(id, name, description='', icon='🏆', src)
@@ -3798,14 +3853,14 @@ class Medal
3798
3853
  return;
3799
3854
 
3800
3855
  // save the medal
3801
- ASSERT(medalsSaveName); // save name must be set
3856
+ ASSERT(medalsSaveName, 'save name must be set');
3802
3857
  localStorage[this.storageKey()] = this.unlocked = 1;
3803
3858
  medalsDisplayQueue.push(this);
3804
3859
  newgrounds && newgrounds.unlockMedal(this.id);
3805
3860
  }
3806
3861
 
3807
3862
  /** Render a medal
3808
- * @param {Number} [hidePercent=0] - How much to slide the medal off screen
3863
+ * @param {Number} [hidePercent] - How much to slide the medal off screen
3809
3864
  */
3810
3865
  render(hidePercent=0)
3811
3866
  {
@@ -3817,25 +3872,25 @@ class Medal
3817
3872
  // draw containing rect and clip to that region
3818
3873
  context.save();
3819
3874
  context.beginPath();
3820
- context.fillStyle = new Color(.9,.9,.9);
3821
- context.strokeStyle = new Color(0,0,0);
3875
+ context.fillStyle = rgb(.9,.9,.9).toString();
3876
+ context.strokeStyle = rgb(0,0,0).toString();
3822
3877
  context.lineWidth = 3;
3823
- context.fill(context.rect(x, y, width, medalDisplaySize.y));
3878
+ context.rect(x, y, width, medalDisplaySize.y);
3879
+ context.fill();
3824
3880
  context.stroke();
3825
3881
  context.clip();
3826
3882
 
3827
3883
  // draw the icon and text
3828
3884
  this.renderIcon(vec2(x+15+medalDisplayIconSize/2, y+medalDisplaySize.y/2));
3829
3885
  const pos = vec2(x+medalDisplayIconSize+30, y+28);
3830
- drawTextScreen(this.name, pos, 38, new Color(0,0,0), 0, 0, 'left');
3886
+ drawTextScreen(this.name, pos, 38, new Color(0,0,0), 0, undefined, 'left');
3831
3887
  pos.y += 32;
3832
- drawTextScreen(this.description, pos, 24, new Color(0,0,0), 0, 0, 'left');
3888
+ drawTextScreen(this.description, pos, 24, new Color(0,0,0), 0, undefined, 'left');
3833
3889
  context.restore();
3834
3890
  }
3835
3891
 
3836
3892
  /** Render the icon for a medal
3837
- * @param {Number} x - Screen space X position
3838
- * @param {Number} y - Screen space Y position
3893
+ * @param {Vector2} pos - Screen space position
3839
3894
  * @param {Number} [size=medalDisplayIconSize] - Screen space size
3840
3895
  */
3841
3896
  renderIcon(pos, size=medalDisplayIconSize)
@@ -3863,7 +3918,10 @@ function medalsRender()
3863
3918
  if (!medalsDisplayTimeLast)
3864
3919
  medalsDisplayTimeLast = timeReal;
3865
3920
  else if (time > medalDisplayTime)
3866
- medalsDisplayQueue.shift(medalsDisplayTimeLast = 0);
3921
+ {
3922
+ medalsDisplayTimeLast = 0;
3923
+ medalsDisplayQueue.shift();
3924
+ }
3867
3925
  else
3868
3926
  {
3869
3927
  // slide on/off medals
@@ -3903,8 +3961,8 @@ class Newgrounds
3903
3961
  * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher */
3904
3962
  constructor(app_id, cipher, cryptoJS)
3905
3963
  {
3906
- ASSERT(!newgrounds && app_id); // can only be one newgrounds object
3907
- ASSERT(!cipher || cryptoJS); // must provide cryptojs if there is a cipher
3964
+ ASSERT(!newgrounds && app_id>0, 'there can only be one newgrounds object');
3965
+ ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
3908
3966
 
3909
3967
  this.app_id = app_id;
3910
3968
  this.cipher = cipher;
@@ -3947,39 +4005,39 @@ class Newgrounds
3947
4005
  debugMedals && console.log(this.scoreboards);
3948
4006
 
3949
4007
  const keepAliveMS = 5 * 60 * 1e3;
3950
- setInterval(()=>this.call('Gateway.ping', 0, 1), keepAliveMS);
4008
+ setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
3951
4009
  }
3952
4010
 
3953
4011
  /** Send message to unlock a medal by id
3954
4012
  * @param {Number} id - The medal id */
3955
- unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, 1); }
4013
+ unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
3956
4014
 
3957
4015
  /** Send message to post score
3958
4016
  * @param {Number} id - The scoreboard id
3959
4017
  * @param {Number} value - The score value */
3960
- postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, 1); }
4018
+ postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
3961
4019
 
3962
4020
  /** Get scores from a scoreboard
3963
- * @param {Number} id - The scoreboard id
3964
- * @param {String} [user=0] - A user's id or name
3965
- * @param {Number} [social=0] - If true, only social scores will be loaded
3966
- * @param {Number} [skip=0] - Number of scores to skip before start
3967
- * @param {Number} [limit=10] - Number of scores to include in the list
3968
- * @return {Object} - The response JSON object
4021
+ * @param {Number} id - The scoreboard id
4022
+ * @param {String} [user] - A user's id or name
4023
+ * @param {Number} [social] - If true, only social scores will be loaded
4024
+ * @param {Number} [skip] - Number of scores to skip before start
4025
+ * @param {Number} [limit] - Number of scores to include in the list
4026
+ * @return {Object} - The response JSON object
3969
4027
  */
3970
- getScores(id, user=0, social=0, skip=0, limit=10)
4028
+ getScores(id, user, social=0, skip=0, limit=10)
3971
4029
  { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
3972
4030
 
3973
4031
  /** Send message to log a view */
3974
- logView() { return this.call('App.logView', {'host':this.host}, 1); }
4032
+ logView() { return this.call('App.logView', {'host':this.host}, true); }
3975
4033
 
3976
4034
  /** Send a message to call a component of the Newgrounds API
3977
- * @param {String} component - Name of the component
3978
- * @param {Object} [parameters=0] - Parameters to use for call
3979
- * @param {Boolean} [async=0] - If true, don't wait for response before continuing (avoid stall)
3980
- * @return {Object} - The response JSON object
4035
+ * @param {String} component - Name of the component
4036
+ * @param {Object} [parameters] - Parameters to use for call
4037
+ * @param {Boolean} [async] - If true, don't wait for response before continuing
4038
+ * @return {Object} - The response JSON object
3981
4039
  */
3982
- call(component, parameters=0, async=0)
4040
+ call(component, parameters, async=false)
3983
4041
  {
3984
4042
  const call = {'component':component, 'parameters':parameters};
3985
4043
  if (this.cipher)
@@ -4014,7 +4072,7 @@ class Newgrounds
4014
4072
  return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
4015
4073
  }
4016
4074
  }
4017
- /**
4075
+ /**
4018
4076
  * LittleJS WebGL Interface
4019
4077
  * - All webgl used by the engine is wrapped up here
4020
4078
  * - For normal stuff you won't need to see or call anything in this file
@@ -4033,13 +4091,13 @@ class Newgrounds
4033
4091
  * @memberof WebGL */
4034
4092
  let glCanvas;
4035
4093
 
4036
- /** 2d context for glCanvas
4037
- * @type {WebGLRenderingContext}
4094
+ /** 2d context for glCanvas
4095
+ * @type {WebGL2RenderingContext}
4038
4096
  * @memberof WebGL */
4039
4097
  let glContext;
4040
4098
 
4041
4099
  // WebGL internal variables not exposed to documentation
4042
- let glActiveTexture, glShader, glArrayBuffer, glVertexData, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
4100
+ let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive;
4043
4101
 
4044
4102
  ///////////////////////////////////////////////////////////////////////////////
4045
4103
 
@@ -4055,73 +4113,89 @@ function glInit()
4055
4113
 
4056
4114
  // setup vertex and fragment shaders
4057
4115
  glShader = glCreateProgram(
4058
- '#version 300 es\n' + // specify GLSL ES version
4059
- 'precision highp float;'+ // use highp for better accuracy
4060
- 'uniform mat4 m;'+ // transform matrix
4061
- 'in vec4 p,c,a;'+ // position, uv, color, additiveColor
4062
- 'out vec4 v,d,e;'+ // return uv, color, additiveColor
4063
- 'void main(){'+ // shader entry point
4064
- 'gl_Position=m*vec4(p.xy,1,1);'+ // transform position
4065
- 'v=p;d=c;e=a;'+ // pass stuff to fragment shader
4066
- '}' // end of shader
4116
+ '#version 300 es\n' + // specify GLSL ES version
4117
+ 'precision highp float;'+ // use highp for better accuracy
4118
+ 'uniform mat4 m;'+ // transform matrix
4119
+ 'in vec2 g;'+ // geometry
4120
+ 'in vec4 p,u,c,a;'+ // position/size, uvs, color, additiveColor
4121
+ 'in float r;'+ // rotation
4122
+ 'out vec2 v;'+ // return uv, color, additiveColor
4123
+ 'out vec4 d,e;'+ // return uv, color, additiveColor
4124
+ 'void main(){'+ // shader entry point
4125
+ 'vec2 s=(g-.5)*p.zw;'+ // get size offset
4126
+ 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
4127
+ 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
4128
+ 'd=c;e=a;'+ // pass colors to fragment shader
4129
+ '}' // end of shader
4067
4130
  ,
4068
- '#version 300 es\n' + // specify GLSL ES version
4069
- 'precision highp float;'+ // use highp for better accuracy
4070
- 'in vec4 v,d,e;'+ // position, uv, color, additiveColor
4071
- 'uniform sampler2D s;'+ // texture
4072
- 'out vec4 c;'+ // out color
4073
- 'void main(){'+ // shader entry point
4074
- 'c=texture(s,v.zw)*d+e;'+ // modulate texture by color plus additive
4075
- '}' // end of shader
4131
+ '#version 300 es\n' + // specify GLSL ES version
4132
+ 'precision highp float;'+ // use highp for better accuracy
4133
+ 'in vec2 v;'+ // uv
4134
+ 'in vec4 d,e;'+ // color, additiveColor
4135
+ 'uniform sampler2D s;'+ // texture
4136
+ 'out vec4 c;'+ // out color
4137
+ 'void main(){'+ // shader entry point
4138
+ 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
4139
+ '}' // end of shader
4076
4140
  );
4077
4141
 
4078
4142
  // init buffers
4079
- glVertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
4080
- glPositionData = new Float32Array(glVertexData);
4081
- glColorData = new Uint32Array(glVertexData);
4143
+ const glInstanceData = new ArrayBuffer(gl_INSTANCE_BUFFER_SIZE);
4144
+ glPositionData = new Float32Array(glInstanceData);
4145
+ glColorData = new Uint32Array(glInstanceData);
4082
4146
  glArrayBuffer = glContext.createBuffer();
4083
- glBatchCount = 0;
4147
+ glGeometryBuffer = glContext.createBuffer();
4148
+
4149
+ // create the geometry buffer, triangle strip square
4150
+ const geometry = new Float32Array([glInstanceCount=0,0,1,0,0,1,1,1]);
4151
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glGeometryBuffer);
4152
+ glContext.bufferData(gl_ARRAY_BUFFER, geometry, gl_STATIC_DRAW);
4084
4153
  }
4085
4154
 
4086
4155
  // Setup render each frame, called automatically by engine
4087
4156
  function glPreRender()
4088
4157
  {
4089
4158
  // clear and set to same size as main canvas
4090
- glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
4159
+ glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4091
4160
  glContext.clear(gl_COLOR_BUFFER_BIT);
4092
4161
 
4093
4162
  // set up the shader
4094
4163
  glContext.useProgram(glShader);
4095
4164
  glContext.activeTexture(gl_TEXTURE0);
4096
4165
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
4097
- glContext.bindBuffer(gl_ARRAY_BUFFER, glArrayBuffer);
4098
- glContext.bufferData(gl_ARRAY_BUFFER, gl_VERTEX_BUFFER_SIZE, gl_DYNAMIC_DRAW);
4099
- glAdditive = 0;
4100
-
4166
+
4101
4167
  // set vertex attributes
4102
- let offset = 0;
4103
- const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
4168
+ let offset = glAdditive = glBatchAdditive = 0;
4169
+ let initVertexAttribArray = (name, type, typeSize, size)=>
4104
4170
  {
4105
4171
  const location = glContext.getAttribLocation(glShader, name);
4172
+ const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
4173
+ const divisor = typeSize && 1; // only if not geometry
4174
+ const normalize = typeSize==1; // only if color
4106
4175
  glContext.enableVertexAttribArray(location);
4107
- glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
4176
+ glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
4177
+ glContext.vertexAttribDivisor(location, divisor);
4108
4178
  offset += size*typeSize;
4109
4179
  }
4110
- initVertexAttribArray('p', gl_FLOAT, 4, 4); // position & texture
4111
- initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
4112
- initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
4180
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glGeometryBuffer);
4181
+ initVertexAttribArray('g', gl_FLOAT, 0, 2); // geometry
4182
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glArrayBuffer);
4183
+ glContext.bufferData(gl_ARRAY_BUFFER, gl_INSTANCE_BUFFER_SIZE, gl_DYNAMIC_DRAW);
4184
+ initVertexAttribArray('p', gl_FLOAT, 4, 4); // position & size
4185
+ initVertexAttribArray('u', gl_FLOAT, 4, 4); // texture coords
4186
+ initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4); // color
4187
+ initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4); // additiveColor
4188
+ initVertexAttribArray('r', gl_FLOAT, 4, 1); // rotation
4113
4189
 
4114
4190
  // build the transform matrix
4115
- const sx = 2 * cameraScale / mainCanvas.width;
4116
- const sy = 2 * cameraScale / mainCanvas.height;
4117
- const cx = -1 - sx*cameraPos.x;
4118
- const cy = -1 - sy*cameraPos.y;
4119
- glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), 0,
4191
+ const s = vec2(2*cameraScale).divide(mainCanvasSize);
4192
+ const p = vec2(-1).subtract(cameraPos.multiply(s));
4193
+ glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
4120
4194
  new Float32Array([
4121
- sx, 0, 0, 0,
4122
- 0, sy, 0, 0,
4123
- 1, 1, -1, 1,
4124
- cx, cy, 0, 0
4195
+ s.x, 0, 0, 0,
4196
+ 0, s.y, 0, 0,
4197
+ 1, 1, 1, 1,
4198
+ p.x, p.y, 0, 0
4125
4199
  ])
4126
4200
  );
4127
4201
  }
@@ -4142,7 +4216,7 @@ function glSetTexture(texture)
4142
4216
 
4143
4217
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
4144
4218
  * @param {String} source
4145
- * @param type
4219
+ * @param {Number} type
4146
4220
  * @return {WebGLShader}
4147
4221
  * @memberof WebGL */
4148
4222
  function glCompileShader(source, type)
@@ -4159,8 +4233,8 @@ function glCompileShader(source, type)
4159
4233
  }
4160
4234
 
4161
4235
  /** Create WebGL program with given shaders
4162
- * @param {WebGLShader} vsSource
4163
- * @param {WebGLShader} fsSource
4236
+ * @param {String} vsSource
4237
+ * @param {String} fsSource
4164
4238
  * @return {WebGLProgram}
4165
4239
  * @memberof WebGL */
4166
4240
  function glCreateProgram(vsSource, fsSource)
@@ -4178,7 +4252,7 @@ function glCreateProgram(vsSource, fsSource)
4178
4252
  }
4179
4253
 
4180
4254
  /** Create WebGL texture from an image and init the texture settings
4181
- * @param {Image} image
4255
+ * @param {HTMLImageElement} image
4182
4256
  * @return {WebGLTexture}
4183
4257
  * @memberof WebGL */
4184
4258
  function glCreateTexture(image)
@@ -4188,7 +4262,7 @@ function glCreateTexture(image)
4188
4262
  glContext.bindTexture(gl_TEXTURE_2D, texture);
4189
4263
  if (image)
4190
4264
  glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
4191
-
4265
+
4192
4266
  // use point filtering for pixelated rendering
4193
4267
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
4194
4268
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
@@ -4203,29 +4277,31 @@ function glCreateTexture(image)
4203
4277
  * @memberof WebGL */
4204
4278
  function glFlush()
4205
4279
  {
4206
- if (!glBatchCount) return;
4280
+ if (!glInstanceCount) return;
4207
4281
 
4208
4282
  const destBlend = glBatchAdditive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
4209
4283
  glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
4210
4284
  glContext.enable(gl_BLEND);
4211
4285
 
4212
4286
  // draw all the sprites in the batch and reset the buffer
4213
- glContext.bufferSubData(gl_ARRAY_BUFFER, 0, glVertexData);
4214
- glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount);
4215
- glBatchCount = 0;
4287
+ glContext.bufferSubData(gl_ARRAY_BUFFER, 0, glPositionData);
4288
+ glContext.drawArraysInstanced(gl_TRIANGLE_STRIP, 0, 4, glInstanceCount);
4289
+ if (showWatermark)
4290
+ drawCount += glInstanceCount;
4291
+ glInstanceCount = 0;
4216
4292
  glBatchAdditive = glAdditive;
4217
4293
  }
4218
4294
 
4219
4295
  /** Draw any sprites still in the buffer, copy to main canvas and clear
4220
4296
  * @param {CanvasRenderingContext2D} context
4221
- * @param {Boolean} [forceDraw=0]
4297
+ * @param {Boolean} [forceDraw]
4222
4298
  * @memberof WebGL */
4223
- function glCopyToContext(context, forceDraw)
4299
+ function glCopyToContext(context, forceDraw=false)
4224
4300
  {
4225
- if (!glBatchCount && !forceDraw) return;
4226
-
4301
+ if (!glInstanceCount && !forceDraw) return;
4302
+
4227
4303
  glFlush();
4228
-
4304
+
4229
4305
  // do not draw in overlay mode because the canvas is visible
4230
4306
  if (!glOverlay || forceDraw)
4231
4307
  context.drawImage(glCanvas, 0, 0);
@@ -4246,61 +4322,25 @@ function glCopyToContext(context, forceDraw)
4246
4322
  * @memberof WebGL */
4247
4323
  function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
4248
4324
  {
4249
- // flush if there is not enough room or if different blend mode
4250
- const vertCount = 6;
4251
- if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
4252
- glFlush();
4253
-
4254
- // prepare to create the verts from size and angle
4255
- const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
4256
- const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
4257
- const positionData =
4258
- [
4259
- x-cx+sy, y+cy+sx, uv0X, uv0Y,
4260
- x-cx-sy, y-cy+sx, uv0X, uv1Y,
4261
- x+cx+sy, y+cy-sx, uv1X, uv0Y,
4262
- x+cx-sy, y-cy-sx, uv1X, uv1Y,
4263
- ];
4264
-
4265
- // setup 2 triangle strip quad
4266
- for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
4267
- {
4268
- const j = clamp(i-1, 0, 3)*4; // degenerate tri at ends
4269
- glPositionData[offset++] = positionData[j+0];
4270
- glPositionData[offset++] = positionData[j+1];
4271
- glPositionData[offset++] = positionData[j+2];
4272
- glPositionData[offset++] = positionData[j+3];
4273
- glColorData[offset++] = rgba;
4274
- glColorData[offset++] = rgbaAdditive;
4275
- }
4276
- glBatchCount += vertCount;
4277
- }
4325
+ ASSERT(typeof rgba == 'number' && typeof rgbaAdditive == 'number', 'invalid color');
4278
4326
 
4279
- /** Add a convex polygon to the gl draw list
4280
- * @param {Array} points - Array of Vector2 points
4281
- * @param {Number} rgba - Color of the polygon
4282
- * @memberof WebGL */
4283
- function glDrawPoints(points, rgba)
4284
- {
4285
4327
  // flush if there is not enough room or if different blend mode
4286
- const vertCount = points.length + 2;
4287
- if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
4328
+ if (glInstanceCount >= gl_MAX_INSTANCES-1 || glBatchAdditive != glAdditive)
4288
4329
  glFlush();
4289
-
4290
- // setup triangle strip from list of points
4291
- for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
4292
- {
4293
- const j = clamp(i-1, 0, vertCount-3); // degenerate tri at ends
4294
- const h = j>>1;
4295
- const point = points[j%2? h : vertCount-3-h];
4296
- glPositionData[offset++] = point.x;
4297
- glPositionData[offset++] = point.y;
4298
- glPositionData[offset++] = 0; // uvx
4299
- glPositionData[offset++] = 0; // uvy
4300
- glColorData[offset++] = 0; // nothing to tint
4301
- glColorData[offset++] = rgba; // apply rgba via additive
4302
- }
4303
- glBatchCount += vertCount;
4330
+
4331
+ let offset = glInstanceCount * gl_INDICIES_PER_INSTANCE;
4332
+ glPositionData[offset++] = x;
4333
+ glPositionData[offset++] = y;
4334
+ glPositionData[offset++] = sizeX;
4335
+ glPositionData[offset++] = sizeY;
4336
+ glPositionData[offset++] = uv0X;
4337
+ glPositionData[offset++] = uv0Y;
4338
+ glPositionData[offset++] = uv1X;
4339
+ glPositionData[offset++] = uv1Y;
4340
+ glColorData[offset++] = rgba;
4341
+ glColorData[offset++] = rgbaAdditive;
4342
+ glPositionData[offset++] = angle;
4343
+ glInstanceCount++;
4304
4344
  }
4305
4345
 
4306
4346
  ///////////////////////////////////////////////////////////////////////////////
@@ -4312,9 +4352,9 @@ let glPostShader, glPostArrayBuffer, glPostTexture, glPostIncludeOverlay;
4312
4352
  * @param {String} shaderCode
4313
4353
  * @param {Boolean} includeOverlay
4314
4354
  * @memberof WebGL */
4315
- function glInitPostProcess(shaderCode, includeOverlay)
4355
+ function glInitPostProcess(shaderCode, includeOverlay=false)
4316
4356
  {
4317
- ASSERT(!glPostShader); // can only have 1 post effects shader
4357
+ ASSERT(!glPostShader, 'can only have 1 post effects shader');
4318
4358
 
4319
4359
  if (!shaderCode) // default shader pass through
4320
4360
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
@@ -4325,14 +4365,14 @@ function glInitPostProcess(shaderCode, includeOverlay)
4325
4365
  'precision highp float;'+ // use highp for better accuracy
4326
4366
  'in vec2 p;'+ // position
4327
4367
  'void main(){'+ // shader entry point
4328
- 'gl_Position=vec4(p,1,1);'+ // set position
4368
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
4329
4369
  '}' // end of shader
4330
4370
  ,
4331
4371
  '#version 300 es\n' + // specify GLSL ES version
4332
4372
  'precision highp float;'+ // use highp for better accuracy
4333
4373
  'uniform sampler2D iChannel0;'+ // input texture
4334
4374
  'uniform vec3 iResolution;'+ // size of output texture
4335
- 'uniform float iTime;'+ // time passed
4375
+ 'uniform float iTime;'+ // time
4336
4376
  'out vec4 c;'+ // out color
4337
4377
  '\n' + shaderCode + '\n'+ // insert custom shader code
4338
4378
  'void main(){'+ // shader entry point
@@ -4343,11 +4383,13 @@ function glInitPostProcess(shaderCode, includeOverlay)
4343
4383
 
4344
4384
  // create buffer and texture
4345
4385
  glPostArrayBuffer = glContext.createBuffer();
4346
- glPostTexture = glCreateTexture();
4386
+ glPostTexture = glCreateTexture(undefined);
4347
4387
  glPostIncludeOverlay = includeOverlay;
4348
4388
 
4349
4389
  // hide the original 2d canvas
4350
4390
  mainCanvas.style.visibility = 'hidden';
4391
+ if (glPostIncludeOverlay)
4392
+ overlayCanvas.style.visibility = 'hidden';
4351
4393
  }
4352
4394
 
4353
4395
  // Render the post processing shader, called automatically by the engine
@@ -4368,21 +4410,14 @@ function glRenderPostProcess()
4368
4410
  glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
4369
4411
  }
4370
4412
 
4371
- if (glPostIncludeOverlay)
4372
- {
4373
- // copy overlay canvas so it will be included in post processing
4374
- mainContext.drawImage(overlayCanvas, 0, 0);
4375
-
4376
- // clear overlay canvas
4377
- overlayCanvas.width = mainCanvas.width;
4378
- }
4413
+ // copy overlay canvas so it will be included in post processing
4414
+ glPostIncludeOverlay && mainContext.drawImage(overlayCanvas, 0, 0);
4379
4415
 
4380
4416
  // setup shader program to draw one triangle
4381
4417
  glContext.useProgram(glPostShader);
4418
+ glContext.bindBuffer(gl_ARRAY_BUFFER, glGeometryBuffer);
4419
+ glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, 1);
4382
4420
  glContext.disable(gl_BLEND);
4383
- glContext.bindBuffer(gl_ARRAY_BUFFER, glPostArrayBuffer);
4384
- glContext.bufferData(gl_ARRAY_BUFFER, new Float32Array([-3,1,1,-3,1,1]), gl_STATIC_DRAW);
4385
- glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, true);
4386
4421
 
4387
4422
  // set textures, pass in the 2d canvas and gl canvas in separate texture channels
4388
4423
  glContext.activeTexture(gl_TEXTURE0);
@@ -4393,19 +4428,19 @@ function glRenderPostProcess()
4393
4428
  const vertexByteStride = 8;
4394
4429
  const pLocation = glContext.getAttribLocation(glPostShader, 'p');
4395
4430
  glContext.enableVertexAttribArray(pLocation);
4396
- glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, 0, vertexByteStride, 0);
4431
+ glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, false, vertexByteStride, 0);
4397
4432
 
4398
4433
  // set uniforms and draw
4399
4434
  const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
4400
4435
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
4401
4436
  glContext.uniform1f(uniformLocation('iTime'), time);
4402
4437
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
4403
- glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 3);
4438
+ glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 4);
4404
4439
  }
4405
4440
 
4406
4441
  ///////////////////////////////////////////////////////////////////////////////
4407
4442
  // store gl constants as integers so their name doesn't use space in minifed
4408
- const
4443
+ const
4409
4444
  gl_ONE = 1,
4410
4445
  gl_TRIANGLE_STRIP = 5,
4411
4446
  gl_SRC_ALPHA = 770,
@@ -4427,17 +4462,17 @@ gl_TEXTURE0 = 33984,
4427
4462
  gl_ARRAY_BUFFER = 34962,
4428
4463
  gl_STATIC_DRAW = 35044,
4429
4464
  gl_DYNAMIC_DRAW = 35048,
4430
- gl_FRAGMENT_SHADER = 35632,
4465
+ gl_FRAGMENT_SHADER = 35632,
4431
4466
  gl_VERTEX_SHADER = 35633,
4432
4467
  gl_COMPILE_STATUS = 35713,
4433
4468
  gl_LINK_STATUS = 35714,
4434
4469
  gl_UNPACK_FLIP_Y_WEBGL = 37440,
4435
4470
 
4436
4471
  // constants for batch rendering
4437
- gl_INDICIES_PER_VERT = 6,
4438
- gl_MAX_BATCH = 1e5,
4439
- gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2, // vec2 * 2 + (char * 4) * 2
4440
- gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTEX_BYTE_STRIDE;
4472
+ gl_INDICIES_PER_INSTANCE = 11,
4473
+ gl_MAX_INSTANCES = 1e4,
4474
+ gl_INSTANCE_BYTE_STRIDE = gl_INDICIES_PER_INSTANCE * 4, // 11 * 4
4475
+ gl_INSTANCE_BUFFER_SIZE = gl_MAX_INSTANCES * gl_INSTANCE_BYTE_STRIDE;
4441
4476
  /**
4442
4477
  * LittleJS - The Tiny JavaScript Game Engine That Can!
4443
4478
  * MIT License - Copyright 2021 Frank Force
@@ -4470,7 +4505,7 @@ const engineName = 'LittleJS';
4470
4505
  * @type {String}
4471
4506
  * @default
4472
4507
  * @memberof Engine */
4473
- const engineVersion = '1.8.9';
4508
+ const engineVersion = '1.9.1';
4474
4509
 
4475
4510
  /** Frames per second to update objects
4476
4511
  * @type {Number}
@@ -4511,14 +4546,14 @@ let timeReal = 0;
4511
4546
 
4512
4547
  /** Is the game paused? Causes time and objects to not be updated
4513
4548
  * @type {Boolean}
4514
- * @default 0
4549
+ * @default false
4515
4550
  * @memberof Engine */
4516
- let paused = 0;
4551
+ let paused = false;
4517
4552
 
4518
4553
  /** Set if game is paused
4519
- * @param {Boolean} paused
4554
+ * @param {Boolean} isPaused
4520
4555
  * @memberof Engine */
4521
- function setPaused(_paused) { paused = _paused; }
4556
+ function setPaused(isPaused) { paused = isPaused; }
4522
4557
 
4523
4558
  // Frame time tracking
4524
4559
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
@@ -4535,7 +4570,7 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
4535
4570
  * @memberof Engine */
4536
4571
  function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=['tiles.png'])
4537
4572
  {
4538
- ASSERT(Array.isArray(imageSources)); // pass in images as array
4573
+ ASSERT(Array.isArray(imageSources), 'pass in images as array');
4539
4574
 
4540
4575
  // internal update loop for engine
4541
4576
  function engineUpdate(frameTimeMS=0)
@@ -4545,12 +4580,12 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4545
4580
  frameTimeLastMS = frameTimeMS;
4546
4581
  if (debug || showWatermark)
4547
4582
  averageFPS = lerp(.05, averageFPS, 1e3/(frameTimeDeltaMS||1));
4548
- const debugSpeedUp = debug && keyIsDown(107); // +
4549
- const debugSpeedDown = debug && keyIsDown(109); // -
4583
+ const debugSpeedUp = debug && keyIsDown('Equal'); // +
4584
+ const debugSpeedDown = debug && keyIsDown('Minus'); // -
4550
4585
  if (debug) // +/- to speed/slow time
4551
4586
  frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
4552
4587
  timeReal += frameTimeDeltaMS / 1e3;
4553
- frameTimeBufferMS += !paused * frameTimeDeltaMS;
4588
+ frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
4554
4589
  if (!debugSpeedUp)
4555
4590
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp incase of slow framerate
4556
4591
 
@@ -4660,7 +4695,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4660
4695
  'user-select:none;' + // prevent mobile hold to select
4661
4696
  '-webkit-user-select:none;' + // compatibility for ios
4662
4697
  '-webkit-touch-callout:none'; // compatibility for ios
4663
- document.body.style = styleBody;
4698
+ document.body.style.cssText = styleBody;
4664
4699
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
4665
4700
  mainContext = mainCanvas.getContext('2d');
4666
4701
 
@@ -4673,10 +4708,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4673
4708
  overlayContext = overlayCanvas.getContext('2d');
4674
4709
 
4675
4710
  // set canvas style
4676
- const styleCanvas =
4677
- 'position:absolute;' + // position
4711
+ const styleCanvas = 'position:absolute;' + // position
4678
4712
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center
4679
- (glCanvas||mainCanvas).style = mainCanvas.style = overlayCanvas.style = styleCanvas;
4713
+ (glCanvas||mainCanvas).style.cssText = mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
4680
4714
 
4681
4715
  // create promises for loading images
4682
4716
  const promises = imageSources.map((src, textureIndex)=>
@@ -4696,13 +4730,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4696
4730
  showSplashScreen && promises.push(new Promise(resolve =>
4697
4731
  {
4698
4732
  let t = 0;
4699
- console.log(`LittleJS Engine v${engineVersion}`);
4733
+ console.log(`${engineName} Engine v${engineVersion}`);
4700
4734
  updateSplash();
4701
4735
  function updateSplash()
4702
4736
  {
4703
4737
  clearInput();
4704
4738
  drawEngineSplashScreen(t+=.01);
4705
- t>1 ? resolve() : setTimeout(updateSplash,16);
4739
+ t>1 ? resolve() : setTimeout(updateSplash, 16);
4706
4740
  }
4707
4741
  }));
4708
4742
 
@@ -4763,7 +4797,7 @@ function engineObjectsDestroy()
4763
4797
 
4764
4798
  /** Triggers a callback for each object within a given area
4765
4799
  * @param {Vector2} [pos] - Center of test area
4766
- * @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
4800
+ * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
4767
4801
  * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
4768
4802
  * @param {Array} [objects=engineObjects] - List of objects to check
4769
4803
  * @memberof Engine */
@@ -4774,7 +4808,7 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
4774
4808
  for (const o of objects)
4775
4809
  callbackFunction(o);
4776
4810
  }
4777
- else if (size.x != undefined) // bounding box test
4811
+ else if (typeof size === 'object') // bounding box test
4778
4812
  {
4779
4813
  for (const o of objects)
4780
4814
  isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
@@ -4792,23 +4826,23 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
4792
4826
 
4793
4827
  function drawEngineSplashScreen(t)
4794
4828
  {
4795
- const x = mainContext;
4796
- const w = mainCanvas.width = innerWidth;
4797
- const h = mainCanvas.height = innerHeight;
4829
+ const x = overlayContext;
4830
+ const w = overlayCanvas.width = innerWidth;
4831
+ const h = overlayCanvas.height = innerHeight;
4832
+
4798
4833
  {
4799
4834
  // background
4800
4835
  const p3 = percent(t, 1, .8);
4801
4836
  const p4 = percent(t, 0, .5);
4802
4837
  const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,Math.hypot(w,h)*.7);
4803
- g.addColorStop(0,hsl(0,0,lerp(p4,0,p3/2),p3));
4804
- g.addColorStop(1,hsl(0,0,0,p3));
4838
+ g.addColorStop(0,hsl(0,0,lerp(p4,0,p3/2),p3).toString());
4839
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
4805
4840
  x.save();
4806
4841
  x.fillStyle = g;
4807
4842
  x.fillRect(0,0,w,h);
4808
4843
  }
4809
4844
 
4810
4845
  // draw LittleJS logo...
4811
-
4812
4846
  const rect = (X, Y, W, H, C)=>
4813
4847
  {
4814
4848
  x.beginPath();
@@ -4833,7 +4867,7 @@ function drawEngineSplashScreen(t)
4833
4867
  C ? x.fill() : x.stroke();
4834
4868
  };
4835
4869
  const color = (c=0, l=0) =>
4836
- hsl([.98,.3,.57,.14][c%4]-10,.8,[0,.3,.5,.8,.9][l]);
4870
+ hsl([.98,.3,.57,.14][c%4]-10,.8,[0,.3,.5,.8,.9][l]).toString();
4837
4871
  const alpha = wave(1,1,t);
4838
4872
  const p = percent(alpha, .1, .5);
4839
4873
 
@@ -4843,7 +4877,7 @@ function drawEngineSplashScreen(t)
4843
4877
  x.scale(size,size);
4844
4878
  x.translate(-40,-35);
4845
4879
  x.lineJoin = x.lineCap = 'round';
4846
- x.lineWidth = 1+p;
4880
+ x.lineWidth = .1 + p*1.9;
4847
4881
 
4848
4882
  // drawing effect
4849
4883
  const p2 = percent(alpha,.1,1);
@@ -4868,7 +4902,7 @@ function drawEngineSplashScreen(t)
4868
4902
 
4869
4903
  // little stack
4870
4904
  rect(37,14,9,6,color(3,2));
4871
- rect(37,14,4,6,color(3,3));
4905
+ rect(37,14,4.5,6,color(3,3));
4872
4906
  rect(37,14,9,6);
4873
4907
 
4874
4908
  // big stack
@@ -4912,7 +4946,8 @@ function drawEngineSplashScreen(t)
4912
4946
  x.lineTo(53+(1+i*2.9)*p,40);
4913
4947
  x.lineTo(53+(4+i*3.5)*p,54);
4914
4948
  x.fillStyle = color(0,i%2+2);
4915
- x.fill() || i%2 && x.stroke();
4949
+ x.fill();
4950
+ i%2 && x.stroke();
4916
4951
  }
4917
4952
 
4918
4953
  // wheels
@@ -4935,7 +4970,7 @@ function drawEngineSplashScreen(t)
4935
4970
  x.font = '900 16px arial';
4936
4971
  x.textAlign = 'center';
4937
4972
  x.textBaseline = 'top';
4938
- x.lineWidth = 1+p*3;
4973
+ x.lineWidth = .1+p*3.9;
4939
4974
  let w2 = 0;
4940
4975
  for (let i=0; i<s.length; ++i)
4941
4976
  w2 += x.measureText(s[i]).width;