littlejsengine 1.13.4 → 1.14.4

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 (58) hide show
  1. package/README.md +4 -3
  2. package/dist/littlejs.d.ts +395 -249
  3. package/dist/littlejs.esm.js +1550 -754
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +1528 -744
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +1504 -720
  8. package/examples/box2d/game.js +4 -4
  9. package/examples/box2d/gameObjects.js +1 -1
  10. package/examples/breakout/game.js +30 -25
  11. package/examples/breakoutTutorial/README.md +1 -1
  12. package/examples/breakoutTutorial/game.js +1 -1
  13. package/examples/electron/build.js +1 -1
  14. package/examples/electron/index.html +2 -2
  15. package/examples/empty/game.js +1 -1
  16. package/examples/htmlMenu/index.html +7 -7
  17. package/examples/index.html +208 -97
  18. package/examples/platformer/gameEffects.js +20 -25
  19. package/examples/platformer/gameLevel.js +6 -1
  20. package/examples/shorts/base.html +1 -1
  21. package/examples/shorts/flappyGame.js +2 -1
  22. package/examples/shorts/helloWorld.js +1 -1
  23. package/examples/shorts/music.js +106 -0
  24. package/examples/shorts/parallax.js +71 -0
  25. package/examples/shorts/piano.js +7 -8
  26. package/examples/shorts/postProcess.js +25 -8
  27. package/examples/shorts/shapes.js +12 -18
  28. package/examples/shorts/song.mp3 +0 -0
  29. package/examples/shorts/uiSystem.js +15 -8
  30. package/examples/starter/index.html +2 -2
  31. package/examples/stress/index.html +14 -7
  32. package/examples/style.css +14 -4
  33. package/examples/typescript/build.js +1 -1
  34. package/examples/uiSystem/game.js +11 -11
  35. package/package.json +1 -1
  36. package/plugins/box2d.js +12 -10
  37. package/plugins/drawUtilities.js +5 -5
  38. package/plugins/newgrounds.js +6 -6
  39. package/plugins/pluginExport.js +1 -1
  40. package/plugins/uiSystem.js +103 -79
  41. package/plugins/zzfxm.js +10 -10
  42. package/reference.md +94 -56
  43. package/src/engine.js +28 -24
  44. package/src/engineAudio.js +284 -109
  45. package/src/engineBuild.js +11 -11
  46. package/src/engineDebug.js +29 -29
  47. package/src/engineDraw.js +285 -112
  48. package/src/engineExport.js +28 -16
  49. package/src/engineInput.js +57 -67
  50. package/src/engineMedals.js +25 -25
  51. package/src/engineObject.js +28 -25
  52. package/src/engineParticles.js +59 -59
  53. package/src/engineRelease.js +1 -1
  54. package/src/engineSettings.js +20 -13
  55. package/src/engineTileLayer.js +34 -35
  56. package/src/engineUtilities.js +66 -59
  57. package/src/engineWebGL.js +466 -66
  58. /package/examples/shorts/{playSound.js → sound.js} +0 -0
@@ -3,10 +3,10 @@
3
3
 
4
4
  'use strict';
5
5
 
6
- /**
6
+ /**
7
7
  * LittleJS - The Tiny Fast JavaScript Game Engine
8
8
  * MIT License - Copyright 2021 Frank Force
9
- *
9
+ *
10
10
  * Engine Features
11
11
  * - Object oriented system with base class engine object
12
12
  * - Base class object handles update, physics, collision, rendering, etc
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.13.4';
36
+ const engineVersion = '1.14.4';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -123,7 +123,7 @@ function engineAddPlugin(updateFunction, renderFunction)
123
123
  * // Basic engine startup
124
124
  * engineInit(
125
125
  * () => { console.log('Game initialized!'); }, // gameInit
126
- * () => { updatePlayer(); }, // gameUpdate
126
+ * () => { updateGameLogic(); }, // gameUpdate
127
127
  * () => { updateUI(); }, // gameUpdatePost
128
128
  * () => { drawBackground(); }, // gameRender
129
129
  * () => { drawHUD(); }, // gameRenderPost
@@ -149,7 +149,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
149
149
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
150
150
 
151
151
  // disable smoothing for pixel art
152
- overlayContext.imageSmoothingEnabled =
152
+ overlayContext.imageSmoothingEnabled =
153
153
  mainContext.imageSmoothingEnabled = !tilesPixelated;
154
154
 
155
155
  // setup gl rendering if enabled
@@ -197,7 +197,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
197
197
  deltaSmooth = frameTimeBufferMS;
198
198
  frameTimeBufferMS = 0;
199
199
  }
200
-
200
+
201
201
  // update multiple frames if necessary in case of slow framerate
202
202
  for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
203
203
  {
@@ -242,15 +242,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
242
242
  overlayContext.textBaseline = 'top';
243
243
  overlayContext.font = '1em monospace';
244
244
  overlayContext.fillStyle = '#000';
245
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
245
+ const text = engineName + ' ' + 'v' + engineVersion + ' / '
246
246
  + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
247
247
  + (glEnable ? ' GL' : ' 2D') ;
248
248
  overlayContext.fillText(text, mainCanvas.width-3, 3);
249
249
  overlayContext.fillStyle = '#fff';
250
250
  overlayContext.fillText(text, mainCanvas.width-2, 2);
251
251
  }
252
- if (debug || showWatermark)
253
- drawCount = 0;
252
+ drawCount = 0;
254
253
  }
255
254
  requestAnimationFrame(engineUpdate);
256
255
  }
@@ -258,13 +257,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
258
257
  function updateCanvas()
259
258
  {
260
259
  if (headlessMode) return;
261
-
260
+
262
261
  if (canvasFixedSize.x)
263
262
  {
264
263
  // clear canvas and set fixed size
265
264
  mainCanvas.width = canvasFixedSize.x;
266
265
  mainCanvas.height = canvasFixedSize.y;
267
-
266
+
268
267
  // fit to window by adding space on top or bottom if necessary
269
268
  const aspect = innerWidth / innerHeight;
270
269
  const fixedAspect = mainCanvas.width / mainCanvas.height;
@@ -274,16 +273,21 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
274
273
  else
275
274
  {
276
275
  // clear canvas and set size to same as window
277
- mainCanvas.width = min(innerWidth, canvasMaxSize.x);
276
+ mainCanvas.width = min(innerWidth, canvasMaxSize.x);
278
277
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
279
278
  }
280
-
279
+
281
280
  // clear overlay canvas and set size
282
281
  overlayCanvas.width = mainCanvas.width;
283
282
  overlayCanvas.height = mainCanvas.height;
284
283
 
285
284
  // save canvas size
286
285
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
286
+
287
+ // set default line join and cap
288
+ const lineJoin = 'round', lineCap = 'round';
289
+ mainContext.lineJoin = overlayContext.lineJoin = lineJoin;
290
+ mainContext.lineCap = overlayContext.lineCap = lineCap;
287
291
  }
288
292
 
289
293
  // wait for gameInit to load
@@ -296,15 +300,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
296
300
  return startEngine();
297
301
 
298
302
  // setup html
299
- const styleRoot =
303
+ const styleRoot =
300
304
  'margin:0;' + // fill the window
301
305
  'overflow:hidden;' + // no scroll bars
302
306
  'background:#000;' + // set background color
303
307
  'user-select:none;' + // prevent hold to select
304
308
  '-webkit-user-select:none;' + // compatibility for ios
305
- (!touchInputEnable ? '' : // no touch css settings
306
309
  'touch-action:none;' + // prevent mobile pinch to resize
307
- '-webkit-touch-callout:none');// compatibility for ios
310
+ '-webkit-touch-callout:none';// compatibility for ios
308
311
  rootElement.style.cssText = styleRoot;
309
312
  drawCanvas = mainCanvas = document.createElement('canvas');
310
313
  rootElement.appendChild(mainCanvas);
@@ -321,7 +324,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
321
324
  rootElement.appendChild(overlayCanvas);
322
325
  overlayContext = overlayCanvas.getContext('2d');
323
326
 
324
- // set canvas style
327
+ // set canvases
325
328
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
326
329
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
327
330
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
@@ -330,17 +333,18 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
330
333
  setCanvasPixelated(canvasPixelated);
331
334
  setOverlayCanvasPixelated(overlayCanvasPixelated);
332
335
  updateCanvas();
336
+ glPreRender();
333
337
 
334
338
  // create offscreen canvas for image processing
335
339
  workCanvas = new OffscreenCanvas(256, 256);
336
340
  workContext = workCanvas.getContext('2d', { willReadFrequently: true });
337
-
341
+
338
342
  // create promises for loading images
339
343
  const promises = imageSources.map((src, textureIndex)=>
340
- new Promise(resolve =>
344
+ new Promise(resolve =>
341
345
  {
342
346
  const image = new Image;
343
- image.onerror = image.onload = ()=>
347
+ image.onerror = image.onload = ()=>
344
348
  {
345
349
  const textureInfo = new TextureInfo(image);
346
350
  textureInfo.createWebGLTexture();
@@ -355,7 +359,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
355
359
  if (!imageSources.length)
356
360
  {
357
361
  // no images to load
358
- promises.push(new Promise(resolve =>
362
+ promises.push(new Promise(resolve =>
359
363
  {
360
364
  const textureInfo = new TextureInfo(new Image);
361
365
  textureInfos[0] = textureInfo;
@@ -367,7 +371,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
367
371
  if (showSplashScreen)
368
372
  {
369
373
  // draw splash screen
370
- promises.push(new Promise(resolve =>
374
+ promises.push(new Promise(resolve =>
371
375
  {
372
376
  let t = 0;
373
377
  console.log(`${engineName} Engine v${engineVersion}`);
@@ -599,7 +603,7 @@ function drawEngineSplashScreen(t)
599
603
  line(36,20,60,20);
600
604
 
601
605
  // engine front light
602
- circle(60,30,4,PI,3*PI,color(3,2));
606
+ circle(60,30,4,PI,3*PI,color(3,2));
603
607
  circle(60,30,4,PI,2*PI,color(3,3));
604
608
  circle(60,30,4,PI,3*PI);
605
609
 
@@ -648,10 +652,10 @@ function drawEngineSplashScreen(t)
648
652
  x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
649
653
  X += w;
650
654
  }
651
-
655
+
652
656
  x.restore();
653
657
  }
654
- /**
658
+ /**
655
659
  * LittleJS - Release Mode
656
660
  * - This file is used for release builds in place of engineDebug.js
657
661
  * - Debug functionality is disabled to reduce size and increase performance
@@ -764,7 +768,19 @@ function lerp(valueA, valueB, percent)
764
768
  if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
765
769
  console.warn('lerp() parameter order changed! use lerp(start, end, p)');
766
770
  return valueA + clamp(percent) * (valueB-valueA);
767
- }
771
+ }
772
+
773
+ /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
774
+ * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
775
+ * @param {number} value
776
+ * @param {number} percentA
777
+ * @param {number} percentB
778
+ * @param {number} lerpA
779
+ * @param {number} lerpB
780
+ * @return {number}
781
+ * @memberof Utilities */
782
+ function percentLerp(value, percentA, percentB, lerpA, lerpB)
783
+ { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
768
784
 
769
785
  /** Returns signed wrapped distance between the two values passed in
770
786
  * @param {number} valueA
@@ -816,7 +832,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
816
832
  * @memberof Utilities */
817
833
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
818
834
 
819
- /** Returns the nearest power of two not less then the value
835
+ /** Returns the nearest power of two not less than the value
820
836
  * @param {number} value
821
837
  * @return {number}
822
838
  * @memberof Utilities */
@@ -831,8 +847,8 @@ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
831
847
  * @return {boolean} - True if overlapping
832
848
  * @memberof Utilities */
833
849
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
834
- {
835
- return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
850
+ {
851
+ return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
836
852
  && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
837
853
  }
838
854
 
@@ -887,7 +903,7 @@ function isIntersecting(start, end, pos, size)
887
903
  function wave(frequency=1, amplitude=1, t=time, offset=0)
888
904
  { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
889
905
 
890
- /** Formats seconds to mm:ss style for display purposes
906
+ /** Formats seconds to mm:ss style for display purposes
891
907
  * @param {number} t - time in seconds
892
908
  * @return {string}
893
909
  * @memberof Utilities */
@@ -903,13 +919,12 @@ async function fetchJSON(url)
903
919
  return response.json();
904
920
  }
905
921
 
906
- /**
922
+ /**
907
923
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
908
924
  * @param {any} n
909
925
  * @return {boolean}
910
- * @memberof Utilities
911
- */
912
- function isNumber(n) { return typeof n == 'number' && !isNaN(n); }
926
+ * @memberof Utilities */
927
+ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
913
928
 
914
929
  ///////////////////////////////////////////////////////////////////////////////
915
930
 
@@ -964,13 +979,13 @@ function randInCircle(radius=1, minRadius=0)
964
979
  * @memberof Random */
965
980
  function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
966
981
  {
967
- return linear ? colorA.lerp(colorB, rand()) :
982
+ return linear ? colorA.lerp(colorB, rand()) :
968
983
  new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
969
984
  }
970
985
 
971
986
  ///////////////////////////////////////////////////////////////////////////////
972
987
 
973
- /**
988
+ /**
974
989
  * Seeded random number generator
975
990
  * - Can be used to create a deterministic random number sequence
976
991
  * @example
@@ -997,8 +1012,8 @@ class RandomGenerator
997
1012
  float(valueA=1, valueB=0)
998
1013
  {
999
1014
  // xorshift algorithm
1000
- this.seed ^= this.seed << 13;
1001
- this.seed ^= this.seed >>> 17;
1015
+ this.seed ^= this.seed << 13;
1016
+ this.seed ^= this.seed >>> 17;
1002
1017
  this.seed ^= this.seed << 5;
1003
1018
  return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
1004
1019
  }
@@ -1047,28 +1062,26 @@ class RandomGenerator
1047
1062
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
1048
1063
  * a = vec2(5); // set a to (5, 5)
1049
1064
  * b = vec2(); // set b to (0, 0)
1050
- * @memberof Utilities
1051
- */
1065
+ * @memberof Utilities */
1052
1066
  function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1053
1067
 
1054
- /**
1068
+ /**
1055
1069
  * Check if object is a valid Vector2
1056
1070
  * @param {any} v
1057
1071
  * @return {boolean}
1058
- * @memberof Utilities
1059
- */
1060
- function isVector2(v) { return v instanceof Vector2; }
1072
+ * @memberof Utilities */
1073
+ function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
1061
1074
 
1062
1075
  // vector2 asserts
1063
- function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v) && v.isValid(), 'Vector2 is invalid.', v); }
1076
+ function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), 'Vector2 is invalid.', v); }
1064
1077
  function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n); }
1065
1078
  function ASSERT_VECTOR2_NORMAL(v)
1066
1079
  {
1067
1080
  ASSERT_VECTOR2_VALID(v);
1068
- ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1081
+ ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1069
1082
  }
1070
1083
 
1071
- /**
1084
+ /**
1072
1085
  * 2D Vector object with vector math library
1073
1086
  * - Functions do not change this so they can be chained together
1074
1087
  * @example
@@ -1192,7 +1205,7 @@ class Vector2
1192
1205
  * @param {number} [angle]
1193
1206
  * @param {number} [length]
1194
1207
  * @return {Vector2} */
1195
- setAngle(angle=0, length=1)
1208
+ setAngle(angle=0, length=1)
1196
1209
  {
1197
1210
  ASSERT_NUMBER_VALID(angle);
1198
1211
  ASSERT_NUMBER_VALID(length);
@@ -1207,7 +1220,7 @@ class Vector2
1207
1220
  rotate(angle)
1208
1221
  {
1209
1222
  ASSERT_NUMBER_VALID(angle);
1210
- const c = Math.cos(-angle), s = Math.sin(-angle);
1223
+ const c = Math.cos(-angle), s = Math.sin(-angle);
1211
1224
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1212
1225
  }
1213
1226
 
@@ -1219,9 +1232,9 @@ class Vector2
1219
1232
  ASSERT_NUMBER_VALID(direction);
1220
1233
  ASSERT_NUMBER_VALID(length);
1221
1234
  direction = mod(direction, 4);
1222
- ASSERT(direction==0 || direction==1 || direction==2 || direction==3,
1235
+ ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1223
1236
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1224
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1237
+ return vec2(direction%2 ? direction-1 ? -length : length : 0,
1225
1238
  direction%2 ? 0 : direction ? -length : length);
1226
1239
  }
1227
1240
 
@@ -1273,7 +1286,7 @@ class Vector2
1273
1286
  /** Returns this vector expressed as a string
1274
1287
  * @param {number} digits - precision to display
1275
1288
  * @return {string} */
1276
- toString(digits=3)
1289
+ toString(digits=3)
1277
1290
  {
1278
1291
  ASSERT_NUMBER_VALID(digits);
1279
1292
  if (debug)
@@ -1292,7 +1305,7 @@ class Vector2
1292
1305
 
1293
1306
  ///////////////////////////////////////////////////////////////////////////////
1294
1307
 
1295
- /**
1308
+ /**
1296
1309
  * Create a color object with RGBA values, white by default
1297
1310
  * @param {number} [r=1] - red
1298
1311
  * @param {number} [g=1] - green
@@ -1303,29 +1316,27 @@ class Vector2
1303
1316
  */
1304
1317
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1305
1318
 
1306
- /**
1319
+ /**
1307
1320
  * Create a color object with HSLA values, white by default
1308
1321
  * @param {number} [h=0] - hue
1309
1322
  * @param {number} [s=0] - saturation
1310
1323
  * @param {number} [l=1] - lightness
1311
1324
  * @param {number} [a=1] - alpha
1312
1325
  * @return {Color}
1313
- * @memberof Utilities
1314
- */
1326
+ * @memberof Utilities */
1315
1327
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
1316
1328
 
1317
- /**
1329
+ /**
1318
1330
  * Check if object is a valid Color
1319
1331
  * @param {any} c
1320
1332
  * @return {boolean}
1321
- * @memberof Utilities
1322
- */
1323
- function isColor(c) { return c instanceof Color; }
1333
+ * @memberof Utilities */
1334
+ function isColor(c) { return c instanceof Color && c.isValid(); }
1324
1335
 
1325
1336
  // color asserts
1326
- function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c) && c.isValid(), 'Color is invalid.', c); }
1337
+ function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
1327
1338
 
1328
- /**
1339
+ /**
1329
1340
  * Color object (red, green, blue, alpha) with some helpful functions
1330
1341
  * @example
1331
1342
  * let a = new Color; // white
@@ -1397,7 +1408,7 @@ class Color
1397
1408
  * @param {number} scale
1398
1409
  * @param {number} [alphaScale=scale]
1399
1410
  * @return {Color} */
1400
- scale(scale, alphaScale=scale)
1411
+ scale(scale, alphaScale=scale)
1401
1412
  { return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
1402
1413
 
1403
1414
  /** Returns a copy of this color clamped to the valid range between 0 and 1
@@ -1414,9 +1425,9 @@ class Color
1414
1425
  ASSERT_NUMBER_VALID(percent);
1415
1426
  const p = clamp(percent);
1416
1427
  return new Color(
1417
- c.r*p + this.r*(1-p),
1418
- c.g*p + this.g*(1-p),
1419
- c.b*p + this.b*(1-p),
1428
+ c.r*p + this.r*(1-p),
1429
+ c.g*p + this.g*(1-p),
1430
+ c.b*p + this.b*(1-p),
1420
1431
  c.a*p + this.a*(1-p));
1421
1432
  }
1422
1433
 
@@ -1456,15 +1467,15 @@ class Color
1456
1467
  const minC = min(r, g, b);
1457
1468
  const l = (maxC + minC) / 2;
1458
1469
  let h = 0, s = 0;
1459
- if (maxC != minC)
1470
+ if (maxC !== minC)
1460
1471
  {
1461
1472
  let d = maxC - minC;
1462
1473
  s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
1463
- if (r == maxC)
1474
+ if (r === maxC)
1464
1475
  h = (g - b) / d + (g < b ? 6 : 0);
1465
- else if (g == maxC)
1476
+ else if (g === maxC)
1466
1477
  h = (b - r) / d + 2;
1467
- else if (b == maxC)
1478
+ else if (b === maxC)
1468
1479
  h = (r - g) / d + 4;
1469
1480
  }
1470
1481
  return [h / 6, s, l, a];
@@ -1474,7 +1485,7 @@ class Color
1474
1485
  * @param {number} [amount]
1475
1486
  * @param {number} [alphaAmount]
1476
1487
  * @return {Color} */
1477
- mutate(amount=.05, alphaAmount=0)
1488
+ mutate(amount=.05, alphaAmount=0)
1478
1489
  {
1479
1490
  ASSERT_NUMBER_VALID(amount);
1480
1491
  ASSERT_NUMBER_VALID(alphaAmount);
@@ -1490,47 +1501,47 @@ class Color
1490
1501
  /** Returns this color expressed as a hex color code
1491
1502
  * @param {boolean} [useAlpha] - if alpha should be included in result
1492
1503
  * @return {string} */
1493
- toString(useAlpha = true)
1504
+ toString(useAlpha = true)
1494
1505
  {
1495
- ASSERT(typeof useAlpha == 'boolean', 'Use alpha boolean is invalid.', useAlpha);
1506
+ ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
1496
1507
  if (debug && !this.isValid())
1497
1508
  return `#000`;
1498
1509
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
1499
1510
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1500
1511
  }
1501
-
1512
+
1502
1513
  /** Set this color from a hex code
1503
1514
  * @param {string} hex - html hex code
1504
1515
  * @return {Color} */
1505
1516
  setHex(hex)
1506
1517
  {
1507
- ASSERT(typeof hex == 'string' && hex[0] == '#', 'Color hex code must be a string starting with #');
1518
+ ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
1508
1519
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1509
1520
 
1510
1521
  if (hex.length < 6)
1511
1522
  {
1512
1523
  const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
1513
1524
  this.r = fromHex(1);
1514
- this.g = fromHex(2),
1525
+ this.g = fromHex(2);
1515
1526
  this.b = fromHex(3);
1516
- this.a = hex.length == 5 ? fromHex(4) : 1;
1527
+ this.a = hex.length === 5 ? fromHex(4) : 1;
1517
1528
  }
1518
1529
  else
1519
1530
  {
1520
1531
  const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
1521
1532
  this.r = fromHex(1);
1522
- this.g = fromHex(3),
1533
+ this.g = fromHex(3);
1523
1534
  this.b = fromHex(5);
1524
- this.a = hex.length == 9 ? fromHex(7) : 1;
1535
+ this.a = hex.length === 9 ? fromHex(7) : 1;
1525
1536
  }
1526
1537
 
1527
1538
  ASSERT_COLOR_VALID(this);
1528
1539
  return this;
1529
1540
  }
1530
-
1541
+
1531
1542
  /** Returns this color expressed as 32 bit RGBA value
1532
1543
  * @return {number} */
1533
- rgbaInt()
1544
+ rgbaInt()
1534
1545
  {
1535
1546
  const r = clamp(this.r)*255|0;
1536
1547
  const g = clamp(this.g)*255<<8;
@@ -1551,7 +1562,7 @@ class Color
1551
1562
  /** Color - White #ffffff
1552
1563
  * @type {Color}
1553
1564
  * @memberof Utilities */
1554
- const WHITE = rgb();
1565
+ const WHITE = rgb();
1555
1566
 
1556
1567
  /** Color - Clear White #ffffff with 0 alpha
1557
1568
  * @type {Color}
@@ -1666,11 +1677,11 @@ class Timer
1666
1677
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1667
1678
  * @return {number} */
1668
1679
  getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
1669
-
1680
+
1670
1681
  /** Returns this timer expressed as a string
1671
1682
  * @return {string} */
1672
1683
  toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
1673
-
1684
+
1674
1685
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1675
1686
  * @return {number} */
1676
1687
  valueOf() { return this.get(); }
@@ -1706,7 +1717,7 @@ let cameraScale = 32;
1706
1717
  // Display settings
1707
1718
 
1708
1719
  /** Enable applying color to tiles when using canvas2d
1709
- * - This is slower but should be the same as webgl rendering
1720
+ * - This is slower but should be the same as WebGL rendering
1710
1721
  * @type {boolean}
1711
1722
  * @default
1712
1723
  * @memberof Settings */
@@ -1752,14 +1763,13 @@ let tilesPixelated = true;
1752
1763
  * @memberof Settings */
1753
1764
  let fontDefault = 'arial';
1754
1765
 
1755
- /** Enable to show the LittleJS splash screen be shown on startup
1766
+ /** Enable to show the LittleJS splash screen on startup
1756
1767
  * @type {boolean}
1757
1768
  * @default
1758
1769
  * @memberof Settings */
1759
1770
  let showSplashScreen = false;
1760
1771
 
1761
1772
  /** Disables all rendering, audio, and input for servers
1762
- * - Must be set before startup to take effect
1763
1773
  * @type {boolean}
1764
1774
  * @default
1765
1775
  * @memberof Settings */
@@ -1768,13 +1778,18 @@ let headlessMode = false;
1768
1778
  ///////////////////////////////////////////////////////////////////////////////
1769
1779
  // WebGL settings
1770
1780
 
1771
- /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
1772
- * - Must be set before startup to take effect
1781
+ /** Enable WebGL accelerated rendering
1773
1782
  * @type {boolean}
1774
1783
  * @default
1775
1784
  * @memberof Settings */
1776
1785
  let glEnable = true;
1777
1786
 
1787
+ /** How many sided poly to use when drawing circles and ellipses with WebGL
1788
+ * @type {number}
1789
+ * @default
1790
+ * @memberof Settings */
1791
+ let glCircleSides = 32;
1792
+
1778
1793
  ///////////////////////////////////////////////////////////////////////////////
1779
1794
  // Tile sheet settings
1780
1795
 
@@ -1856,13 +1871,13 @@ let particleEmitRateScale = 1;
1856
1871
  * @memberof Settings */
1857
1872
  let gamepadsEnable = true;
1858
1873
 
1859
- /** If true, the dpad input is also routed to the left analog stick (for better accessability)
1874
+ /** If true, the dpad input is also routed to the left analog stick (for better accessibility)
1860
1875
  * @type {boolean}
1861
1876
  * @default
1862
1877
  * @memberof Settings */
1863
1878
  let gamepadDirectionEmulateStick = true;
1864
1879
 
1865
- /** If true the WASD keys are also routed to the direction keys (for better accessability)
1880
+ /** If true the WASD keys are also routed to the direction keys (for better accessibility)
1866
1881
  * @type {boolean}
1867
1882
  * @default
1868
1883
  * @memberof Settings */
@@ -1870,7 +1885,6 @@ let inputWASDEmulateDirection = true;
1870
1885
 
1871
1886
  /** True if touch input is enabled for mobile devices
1872
1887
  * - Touch events will be routed to mouse events
1873
- * - Must be set before startup to take effect
1874
1888
  * @type {boolean}
1875
1889
  * @default
1876
1890
  * @memberof Settings */
@@ -1878,7 +1892,6 @@ let touchInputEnable = true;
1878
1892
 
1879
1893
  /** True if touch gamepad should appear on mobile devices
1880
1894
  * - Supports left analog stick, 4 face buttons and start button (button 9)
1881
- * - Must be set before startup to take effect
1882
1895
  * @type {boolean}
1883
1896
  * @default
1884
1897
  * @memberof Settings */
@@ -1981,7 +1994,7 @@ function setCameraAngle(angle) { cameraAngle = angle; }
1981
1994
  function setCameraScale(scale) { cameraScale = scale; }
1982
1995
 
1983
1996
  /** Set if tiles should be colorized when using canvas2d
1984
- * This can be slower but results should look nearly identical to webgl rendering
1997
+ * This can be slower but results should look nearly identical to WebGL rendering
1985
1998
  * It can be enabled/disabled at any time
1986
1999
  * Optimized for performance, and will use faster method if color is white or untextured
1987
2000
  * @param {boolean} colorTiles
@@ -2017,8 +2030,8 @@ function setCanvasPixelated(pixelated)
2017
2030
  * @param {boolean} pixelated
2018
2031
  * @memberof Settings */
2019
2032
  function setOverlayCanvasPixelated(pixelated)
2020
- {
2021
- overlayCanvasPixelated = pixelated;
2033
+ {
2034
+ overlayCanvasPixelated = pixelated;
2022
2035
  if (overlayCanvas)
2023
2036
  overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2024
2037
  }
@@ -2033,7 +2046,7 @@ function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
2033
2046
  * @memberof Settings */
2034
2047
  function setFontDefault(font) { fontDefault = font; }
2035
2048
 
2036
- /** Set if the LittleJS splash screen be shown on startup
2049
+ /** Set if the LittleJS splash screen should be shown on startup
2037
2050
  * @param {boolean} show
2038
2051
  * @memberof Settings */
2039
2052
  function setShowSplashScreen(show) { showSplashScreen = show; }
@@ -2043,16 +2056,21 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
2043
2056
  * @memberof Settings */
2044
2057
  function setHeadlessMode(headless) { headlessMode = headless; }
2045
2058
 
2046
- /** Set if webgl rendering is enabled
2059
+ /** Set if WebGL rendering is enabled
2047
2060
  * @param {boolean} enable
2048
2061
  * @memberof Settings */
2049
2062
  function setGLEnable(enable)
2050
2063
  {
2051
2064
  glEnable = enable;
2052
- if (glCanvas) // hide glCanvas if webgl is disabled
2065
+ if (glCanvas) // hide glCanvas if WebGL is disabled
2053
2066
  glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2054
2067
  }
2055
2068
 
2069
+ /** Set how many sided polygons to use when drawing circles and elipses with WebGL
2070
+ * @param {number} sides
2071
+ * @memberof Settings */
2072
+ function setGLCircleSides(sides) { glCircleSides = sides; }
2073
+
2056
2074
  /** Set default size of tiles in pixels
2057
2075
  * @param {Vector2} size
2058
2076
  * @memberof Settings */
@@ -2083,7 +2101,7 @@ function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
2083
2101
  * @memberof Settings */
2084
2102
  function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
2085
2103
 
2086
- /** Set how much to bounce when a collision occur
2104
+ /** Set how much to bounce when a collision occurs
2087
2105
  * @param {number} restitution
2088
2106
  * @memberof Settings */
2089
2107
  function setObjectDefaultRestitution(restitution) { objectDefaultRestitution = restitution; }
@@ -2207,11 +2225,11 @@ function setShowWatermark(show) { showWatermark = show; }
2207
2225
  * @param {string} key
2208
2226
  * @memberof Debug */
2209
2227
  function setDebugKey(key) { debugKey = key; }
2210
- /**
2228
+ /**
2211
2229
  * LittleJS Object System
2212
2230
  */
2213
2231
 
2214
- /**
2232
+ /**
2215
2233
  * LittleJS Object Base Object Class
2216
2234
  * - Top level object class used by the engine
2217
2235
  * - Automatically adds self to object list
@@ -2234,7 +2252,7 @@ function setDebugKey(key) { debugKey = key; }
2234
2252
  * @example
2235
2253
  * // create an engine object, normally you would first extend the class with your own
2236
2254
  * const pos = vec2(2,3);
2237
- * const object = new EngineObject(pos);
2255
+ * const object = new EngineObject(pos);
2238
2256
  */
2239
2257
  class EngineObject
2240
2258
  {
@@ -2249,12 +2267,12 @@ class EngineObject
2249
2267
  constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
2250
2268
  {
2251
2269
  // check passed in params
2252
- ASSERT(isVector2(pos) && pos.isValid(), 'object pos should be a vec2');
2253
- ASSERT(isVector2(size) && size.isValid(), 'object size should be a vec2');
2270
+ ASSERT(isVector2(pos), 'object pos should be a vec2');
2271
+ ASSERT(isVector2(size), 'object size should be a vec2');
2254
2272
  ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
2255
- ASSERT(typeof angle == 'number' && isFinite(angle), 'object angle should be a number');
2256
- ASSERT(isColor(color) && color.isValid(), 'object color should be a valid rgba color');
2257
- ASSERT(typeof renderOrder == 'number', 'object renderOrder should be a number');
2273
+ ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2274
+ ASSERT(isColor(color), 'object color should be a valid rgba color');
2275
+ ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
2258
2276
 
2259
2277
  /** @property {Vector2} - World space position of the object */
2260
2278
  this.pos = pos.copy();
@@ -2322,7 +2340,7 @@ class EngineObject
2322
2340
  // add to list of objects
2323
2341
  engineObjects.push(this);
2324
2342
  }
2325
-
2343
+
2326
2344
  /** Update the object transform, called automatically by engine even when paused */
2327
2345
  updateTransforms()
2328
2346
  {
@@ -2391,7 +2409,7 @@ class EngineObject
2391
2409
  for (const o of engineObjectsCollide)
2392
2410
  {
2393
2411
  // non solid objects don't collide with each other
2394
- if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o == this)
2412
+ if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
2395
2413
  continue;
2396
2414
 
2397
2415
  // check collision
@@ -2414,7 +2432,7 @@ class EngineObject
2414
2432
  this.velocity = this.velocity.add(velocity);
2415
2433
  if (o.mass) // push away if not fixed
2416
2434
  o.velocity = o.velocity.subtract(velocity);
2417
-
2435
+
2418
2436
  debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
2419
2437
  continue;
2420
2438
  }
@@ -2425,7 +2443,7 @@ class EngineObject
2425
2443
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
2426
2444
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
2427
2445
  const restitution = max(this.restitution, o.restitution);
2428
-
2446
+
2429
2447
  if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
2430
2448
  {
2431
2449
  // push outside object collision
@@ -2513,7 +2531,7 @@ class EngineObject
2513
2531
  {
2514
2532
  // move to previous position
2515
2533
  this.pos.y = oldPos.y;
2516
- this.groundObject = undefined;
2534
+ this.groundObject = undefined;
2517
2535
  }
2518
2536
  }
2519
2537
  if (blockedLayerX)
@@ -2527,20 +2545,20 @@ class EngineObject
2527
2545
  }
2528
2546
  }
2529
2547
  }
2530
-
2548
+
2531
2549
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
2532
2550
  render()
2533
2551
  {
2534
2552
  // default object render
2535
2553
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
2536
2554
  }
2537
-
2555
+
2538
2556
  /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
2539
2557
  destroy()
2540
- {
2558
+ {
2541
2559
  if (this.destroyed)
2542
2560
  return;
2543
-
2561
+
2544
2562
  // disconnect from parent and destroy children
2545
2563
  this.destroyed = 1;
2546
2564
  this.parent && this.parent.removeChild(this);
@@ -2566,7 +2584,7 @@ class EngineObject
2566
2584
  /** Convert from world space to local space for a vector (rotation only)
2567
2585
  * @param {Vector2} vec - world space vector */
2568
2586
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
2569
-
2587
+
2570
2588
  /** Called to check if a tile collision should be resolved
2571
2589
  * @param {number} tileData - the value of the tile at the position
2572
2590
  * @param {Vector2} pos - tile where the collision occurred
@@ -2585,16 +2603,19 @@ class EngineObject
2585
2603
 
2586
2604
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
2587
2605
  * @param {Vector2} acceleration */
2588
- applyAcceleration(acceleration) { if (this.mass) this.velocity = this.velocity.add(acceleration); }
2606
+ applyAcceleration(acceleration)
2607
+ { if (this.mass) this.velocity = this.velocity.add(acceleration); }
2589
2608
 
2590
- /** Apply angular acceleration to this object
2609
+ /** Apply angular acceleration to this object
2591
2610
  * @param {number} acceleration */
2592
- applyAngularAcceleration(acceleration) { if (this.mass) this.angleVelocity += acceleration; }
2611
+ applyAngularAcceleration(acceleration)
2612
+ { if (this.mass) this.angleVelocity += acceleration; }
2593
2613
 
2594
2614
  /** Apply force to this object (adjust velocity, affected by mass)
2595
2615
  * @param {Vector2} force */
2596
- applyForce(force) { this.applyAcceleration(force.scale(1/this.mass)); }
2597
-
2616
+ applyForce(force)
2617
+ { if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
2618
+
2598
2619
  /** Get the direction of the mirror
2599
2620
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
2600
2621
  getMirrorSign() { return this.mirror ? -1 : 1; }
@@ -2616,7 +2637,7 @@ class EngineObject
2616
2637
  * @param {EngineObject} child */
2617
2638
  removeChild(child)
2618
2639
  {
2619
- ASSERT(child.parent == this && this.children.includes(child));
2640
+ ASSERT(child.parent === this && this.children.includes(child));
2620
2641
  this.children.splice(this.children.indexOf(child), 1);
2621
2642
  child.parent = 0;
2622
2643
  }
@@ -2662,7 +2683,7 @@ class EngineObject
2662
2683
  {
2663
2684
  if (!debug)
2664
2685
  return;
2665
-
2686
+
2666
2687
  // show object info for debugging
2667
2688
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
2668
2689
  const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
@@ -2672,24 +2693,24 @@ class EngineObject
2672
2693
  this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(1,1,1,.5));
2673
2694
  }
2674
2695
  }
2675
- /**
2696
+ /**
2676
2697
  * LittleJS Drawing System
2677
2698
  * - Hybrid system with both Canvas2D and WebGL available
2678
2699
  * - Super fast tile sheet rendering with WebGL
2679
2700
  * - Can apply rotation, mirror, color and additive color
2680
2701
  * - Font rendering system with built in engine font
2681
2702
  * - Many useful utility functions
2682
- *
2703
+ *
2683
2704
  * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
2684
2705
  * There are 3 canvas/contexts available to draw to...
2685
2706
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
2686
2707
  * glCanvas - Used by the accelerated WebGL batch rendering system.
2687
2708
  * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
2688
- *
2709
+ *
2689
2710
  * The WebGL rendering system is very fast with some caveats...
2690
2711
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
2691
2712
  * - Group additive rendering together using renderOrder to mitigate this issue
2692
- *
2713
+ *
2693
2714
  * The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
2694
2715
  * @namespace Draw
2695
2716
  */
@@ -2734,7 +2755,7 @@ let workCanvas;
2734
2755
  * @memberof Draw */
2735
2756
  let workContext;
2736
2757
 
2737
- /** The size of the main canvas (and other secondary canvases)
2758
+ /** The size of the main canvas (and other secondary canvases)
2738
2759
  * @type {Vector2}
2739
2760
  * @memberof Draw */
2740
2761
  let mainCanvasSize = vec2();
@@ -2744,12 +2765,14 @@ let mainCanvasSize = vec2();
2744
2765
  * @memberof Draw */
2745
2766
  let textureInfos = [];
2746
2767
 
2747
- // Keep track of how many draw calls there were each frame for debugging
2768
+ /** Keeps track of how many draw calls there were each frame for debugging
2769
+ * @type {number}
2770
+ * @memberof Draw */
2748
2771
  let drawCount;
2749
2772
 
2750
2773
  ///////////////////////////////////////////////////////////////////////////////
2751
2774
 
2752
- /**
2775
+ /**
2753
2776
  * Create a tile info object using a grid based system
2754
2777
  * - This can take vecs or floats for easier use and conversion
2755
2778
  * - If an index is passed in, the tile size and index will determine the position
@@ -2763,15 +2786,14 @@ let drawCount;
2763
2786
  * tile(5, 8) // a tile at index 5 using a tile size of 8
2764
2787
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
2765
2788
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
2766
- * @memberof Draw
2767
- */
2789
+ * @memberof Draw */
2768
2790
  function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
2769
2791
  {
2770
2792
  if (headlessMode)
2771
2793
  return new TileInfo;
2772
2794
 
2773
2795
  // if size is a number, make it a vector
2774
- if (typeof size == 'number')
2796
+ if (typeof size === 'number')
2775
2797
  {
2776
2798
  ASSERT(size > 0);
2777
2799
  size = new Vector2(size, size);
@@ -2780,24 +2802,24 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
2780
2802
  // create tile info object
2781
2803
  const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
2782
2804
 
2783
- // use get the pos of the tile
2805
+ // get the position of the tile
2784
2806
  const textureInfo = textureInfos[textureIndex];
2785
2807
  ASSERT(!!textureInfo, 'Texture not loaded');
2786
2808
  const sizePaddedX = size.x + padding*2;
2787
2809
  const sizePaddedY = size.y + padding*2;
2788
- if (typeof pos == 'number')
2810
+ if (typeof pos === 'number')
2789
2811
  {
2790
2812
  const cols = textureInfo.size.x / sizePaddedX |0;
2791
- ASSERT(cols>0, 'Tile size is too big for texture');
2813
+ ASSERT(cols > 0, 'Tile size is too big for texture');
2792
2814
  const posX = pos % cols, posY = (pos / cols) |0;
2793
2815
  tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
2794
2816
  }
2795
2817
  else
2796
2818
  tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
2797
- return tileInfo;
2819
+ return tileInfo;
2798
2820
  }
2799
2821
 
2800
- /**
2822
+ /**
2801
2823
  * Tile Info - Stores info about how to draw a tile
2802
2824
  */
2803
2825
  class TileInfo
@@ -2835,14 +2857,14 @@ class TileInfo
2835
2857
  */
2836
2858
  frame(frame)
2837
2859
  {
2838
- ASSERT(typeof frame == 'number');
2860
+ ASSERT(typeof frame === 'number');
2839
2861
  return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
2840
2862
  }
2841
2863
 
2842
2864
  /**
2843
2865
  * Set this tile to use a full image
2844
2866
  * @param {HTMLImageElement|OffscreenCanvas} image
2845
- * @param {WebGLTexture} [glTexture] - webgl texture
2867
+ * @param {WebGLTexture} [glTexture] - WebGL texture
2846
2868
  * @return {TileInfo}
2847
2869
  */
2848
2870
  setFullImage(image, glTexture)
@@ -2860,7 +2882,7 @@ class TextureInfo
2860
2882
  /**
2861
2883
  * Create a TextureInfo, called automatically by the engine
2862
2884
  * @param {HTMLImageElement|OffscreenCanvas} image
2863
- * @param {WebGLTexture} [glTexture] - webgl texture
2885
+ * @param {WebGLTexture} [glTexture] - WebGL texture
2864
2886
  */
2865
2887
  constructor(image, glTexture)
2866
2888
  {
@@ -2870,7 +2892,7 @@ class TextureInfo
2870
2892
  this.size = vec2(image.width, image.height);
2871
2893
  /** @property {Vector2} - inverse of the size, cached for rendering */
2872
2894
  this.sizeInverse = vec2(1/image.width, 1/image.height);
2873
- /** @property {WebGLTexture} - webgl texture */
2895
+ /** @property {WebGLTexture} - WebGL texture */
2874
2896
  this.glTexture = glTexture;
2875
2897
  }
2876
2898
 
@@ -2886,28 +2908,25 @@ class TextureInfo
2886
2908
  // Drawing functions
2887
2909
 
2888
2910
  /** Draw textured tile centered in world space, with color applied if using WebGL
2889
- * @param {Vector2} pos - Center of the tile in world space
2890
- * @param {Vector2} [size=(1,1)] - Size of the tile in world space
2891
- * @param {TileInfo}[tileInfo] - Tile info to use, untextured if undefined
2892
- * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2893
- * @param {number} [angle] - Angle to rotate by
2894
- * @param {boolean} [mirror] - If true image is flipped along the Y axis
2895
- * @param {Color} [additiveColor] - Additive color to be applied if any
2896
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2897
- * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
2911
+ * @param {Vector2} pos - Center of the tile in world space
2912
+ * @param {Vector2} [size=(1,1)] - Size of the tile in world space
2913
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
2914
+ * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2915
+ * @param {number} [angle] - Angle to rotate by
2916
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
2917
+ * @param {Color} [additiveColor] - Additive color to be applied if any
2918
+ * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
2919
+ * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
2898
2920
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2899
2921
  * @memberof Draw */
2900
- function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2922
+ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
2901
2923
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2902
2924
  {
2903
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2904
- ASSERT(isVector2(pos) && pos.isValid(), 'drawTile pos should be a vec2');
2905
- ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
2925
+ ASSERT(isVector2(pos), 'drawTile pos should be a vec2');
2926
+ ASSERT(isVector2(size), 'drawTile size should be a vec2');
2906
2927
  ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
2907
2928
  ASSERT(isNumber(angle), 'drawTile angle should be a number');
2908
-
2909
- if (color.a <= 0 && (!additiveColor || additiveColor.a <= 0) || !size.x || !size.y)
2910
- return; // completely invisible, skip render
2929
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2911
2930
 
2912
2931
  const textureInfo = tileInfo && tileInfo.textureInfo;
2913
2932
  if (useWebGL)
@@ -2931,28 +2950,28 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2931
2950
  {
2932
2951
  const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
2933
2952
  const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
2934
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2935
- x + tileImageFixBleedX, y + tileImageFixBleedY,
2936
- x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2937
- color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2953
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2954
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
2955
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2956
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2938
2957
  }
2939
2958
  else
2940
2959
  {
2941
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2942
- x, y, x + w, y + h,
2943
- color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2960
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2961
+ x, y, x + w, y + h,
2962
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2944
2963
  }
2945
2964
  }
2946
2965
  else
2947
2966
  {
2948
2967
  // if no tile info, force untextured
2949
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
2968
+ glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
2950
2969
  }
2951
2970
  }
2952
2971
  else
2953
2972
  {
2954
2973
  // normal canvas 2D rendering method (slower)
2955
- showWatermark && ++drawCount;
2974
+ ++drawCount;
2956
2975
  size = new Vector2(size.x, -size.y); // fix upside down sprites
2957
2976
  drawCanvas2D(pos, size, angle, mirror, (context)=>
2958
2977
  {
@@ -2984,14 +3003,128 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2984
3003
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2985
3004
  * @memberof Draw */
2986
3005
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2987
- {
2988
- drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
3006
+ {
3007
+ drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
3008
+ }
3009
+
3010
+ /** Draw a rect centered on pos with a gradient from top to bottom
3011
+ * @param {Vector2} pos
3012
+ * @param {Vector2} [size=(1,1)]
3013
+ * @param {Color} [colorTop=(1,1,1,1)]
3014
+ * @param {Color} [colorBottom=(0,0,0,1)]
3015
+ * @param {number} [angle]
3016
+ * @param {boolean} [useWebGL=glEnable]
3017
+ * @param {boolean} [screenSpace]
3018
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3019
+ * @memberof Draw */
3020
+ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3021
+ {
3022
+ ASSERT(isVector2(pos), 'drawRectGradient pos should be a vec2');
3023
+ ASSERT(isVector2(size), 'drawRectGradient size should be a vec2');
3024
+ ASSERT(isColor(colorTop) && isColor(colorBottom), 'drawRectGradient color is invalid');
3025
+ ASSERT(isNumber(angle), 'drawRectGradient angle should be a number');
3026
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3027
+ if (useWebGL)
3028
+ {
3029
+ if (screenSpace)
3030
+ {
3031
+ // convert to world space
3032
+ pos = screenToWorld(pos);
3033
+ size = size.scale(1/cameraScale);
3034
+ }
3035
+ // build 4 corner points for the rectangle
3036
+ const points = [], colors = [];
3037
+ const halfSizeX = size.x/2, halfSizeY = size.y/2;
3038
+ const colorTopInt = colorTop.rgbaInt();
3039
+ const colorBottomInt = colorBottom.rgbaInt();
3040
+ const c = Math.cos(-angle), s = Math.sin(-angle);
3041
+ for (let i=4; i--;)
3042
+ {
3043
+ const x = i & 1 ? halfSizeX : -halfSizeX;
3044
+ const y = i & 2 ? halfSizeY : -halfSizeY;
3045
+ const rx = x * c - y * s;
3046
+ const ry = x * s + y * c;
3047
+ const color = i & 2 ? colorTopInt : colorBottomInt;
3048
+ points.push(vec2(pos.x + rx, pos.y + ry));
3049
+ colors.push(color);
3050
+ }
3051
+ glDrawColoredPoints(points, colors);
3052
+ }
3053
+ else
3054
+ {
3055
+ // normal canvas 2D rendering method (slower)
3056
+ ++drawCount;
3057
+ size = new Vector2(size.x, -size.y); // fix upside down sprites
3058
+ drawCanvas2D(pos, size, angle, false, (context)=>
3059
+ {
3060
+ // if no tile info, use untextured rect
3061
+ const gradient = context.createLinearGradient(0, -.5, 0, .5);
3062
+ gradient.addColorStop(0, colorTop.toString());
3063
+ gradient.addColorStop(1, colorBottom.toString());
3064
+ context.fillStyle = gradient;
3065
+ context.fillRect(-.5, -.5, 1, 1);
3066
+ }, screenSpace, context);
3067
+ }
3068
+ }
3069
+
3070
+ /** Draw connected lines between a series of points
3071
+ * @param {Array<Vector2>} points
3072
+ * @param {number} [width]
3073
+ * @param {Color} [color=(1,1,1,1)]
3074
+ * @param {boolean} [wrap] - Should the last point connect to the first?
3075
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
3076
+ * @param {number} [angle] - Angle to rotate by
3077
+ * @param {boolean} [useWebGL=glEnable]
3078
+ * @param {boolean} [screenSpace]
3079
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3080
+ * @memberof Draw */
3081
+ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3082
+ {
3083
+ ASSERT(Array.isArray(points), 'drawLineList points should be an array');
3084
+ ASSERT(isNumber(width), 'drawLineList width should be a number');
3085
+ ASSERT(isColor(color), 'drawLineList color is invalid');
3086
+ ASSERT(isVector2(pos), 'drawLineList pos should be a vec2');
3087
+ ASSERT(isNumber(angle), 'drawLineList angle should be a number');
3088
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3089
+ if (useWebGL)
3090
+ {
3091
+ let scale = 1;
3092
+ if (screenSpace)
3093
+ {
3094
+ // convert to world space
3095
+ pos = screenToWorld(pos);
3096
+ scale = 1/cameraScale;
3097
+ }
3098
+ glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, scale, scale, angle, wrap);
3099
+ }
3100
+ else
3101
+ {
3102
+ // normal canvas 2D rendering method (slower)
3103
+ ++drawCount;
3104
+ drawCanvas2D(pos, vec2(1), angle, false, (context)=>
3105
+ {
3106
+ context.strokeStyle = color.toString();
3107
+ context.lineWidth = width;
3108
+ context.beginPath();
3109
+ for (let i=0; i<points.length; ++i)
3110
+ {
3111
+ const point = points[i];
3112
+ if (i)
3113
+ context.lineTo(point.x, point.y);
3114
+ else
3115
+ context.moveTo(point.x, point.y);
3116
+ }
3117
+ if (wrap)
3118
+ context.closePath();
3119
+ context.stroke();
3120
+ }, screenSpace, context);
3121
+ }
2989
3122
  }
2990
3123
 
2991
3124
  /** Draw colored line between two points
2992
3125
  * @param {Vector2} posA
2993
3126
  * @param {Vector2} posB
2994
- * @param {number} [thickness]
3127
+ * @param {number} [width]
2995
3128
  * @param {Color} [color=(1,1,1,1)]
2996
3129
  * @param {Vector2} [pos=(0,0)] - Offset to apply
2997
3130
  * @param {number} [angle] - Angle to rotate by
@@ -2999,15 +3132,43 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2999
3132
  * @param {boolean} [screenSpace]
3000
3133
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3001
3134
  * @memberof Draw */
3002
- function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
3135
+ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
3003
3136
  {
3004
3137
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
3005
- const size = vec2(thickness, halfDelta.length()*2);
3138
+ const size = vec2(width, halfDelta.length()*2);
3006
3139
  pos = pos.add(posA.add(halfDelta));
3007
3140
  angle += halfDelta.angle();
3008
3141
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3009
3142
  }
3010
3143
 
3144
+ /** Draw colored regular polygon using passed in number of sides
3145
+ * @param {Vector2} pos
3146
+ * @param {Vector2} [size=(1,1)]
3147
+ * @param {number} [sides]
3148
+ * @param {Color} [color=(1,1,1,1)]
3149
+ * @param {number} [angle]
3150
+ * @param {number} [lineWidth]
3151
+ * @param {Color} [lineColor=(0,0,0,1)]
3152
+ * @param {boolean} [useWebGL=glEnable]
3153
+ * @param {boolean} [screenSpace]
3154
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3155
+ * @memberof Draw */
3156
+ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3157
+ {
3158
+ ASSERT(isVector2(size), 'drawRegularPoly size should be a vec2');
3159
+ ASSERT(isNumber(sides), 'drawRegularPoly sides should be a number');
3160
+
3161
+ // build regular polygon points
3162
+ const points = [];
3163
+ const sizeX = size.x/2, sizeY = size.y/2;
3164
+ for (let i=sides; i--;)
3165
+ {
3166
+ const a = (i/sides)*PI*2;
3167
+ points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
3168
+ }
3169
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3170
+ }
3171
+
3011
3172
  /** Draw colored polygon using passed in points
3012
3173
  * @param {Array<Vector2>} points - Array of Vector2 points
3013
3174
  * @param {Color} [color=(1,1,1,1)]
@@ -3015,82 +3176,110 @@ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL
3015
3176
  * @param {Color} [lineColor=(0,0,0,1)]
3016
3177
  * @param {Vector2} [pos=(0,0)] - Offset to apply
3017
3178
  * @param {number} [angle] - Angle to rotate by
3018
- * @param {boolean} [useWebGL] - Webgl not supported
3179
+ * @param {boolean} [useWebGL=glEnable]
3019
3180
  * @param {boolean} [screenSpace]
3020
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3181
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3021
3182
  * @memberof Draw */
3022
- function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=false, screenSpace=false, context=drawContext)
3183
+ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3023
3184
  {
3024
- ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
3185
+ ASSERT(isVector2(pos), 'drawPoly pos should be a vec2');
3025
3186
  ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3026
3187
  ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3027
3188
  ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3028
3189
  ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3029
- ASSERT(!useWebGL, 'drawPoly webgl not supported');
3030
- drawCanvas2D(pos, vec2(1), angle, false, context=>
3190
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3191
+ if (useWebGL)
3031
3192
  {
3032
- context.beginPath();
3033
- for (const point of points)
3034
- context.lineTo(point.x, point.y);
3035
- context.closePath();
3036
- context.fillStyle = color.toString();
3037
- context.fill();
3038
- if (lineWidth)
3193
+ let scale = 1;
3194
+ if (screenSpace)
3039
3195
  {
3040
- context.strokeStyle = lineColor.toString();
3041
- context.lineWidth = lineWidth;
3042
- context.stroke();
3196
+ // convert to world space
3197
+ pos = screenToWorld(pos);
3198
+ scale = 1/cameraScale;
3043
3199
  }
3044
- }, screenSpace, context);
3200
+ glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
3201
+ if (lineWidth > 0)
3202
+ glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
3203
+ }
3204
+ else
3205
+ {
3206
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3207
+ {
3208
+ context.fillStyle = color.toString();
3209
+ context.beginPath();
3210
+ for (const point of points)
3211
+ context.lineTo(point.x, point.y);
3212
+ context.closePath();
3213
+ context.fill();
3214
+ if (lineWidth)
3215
+ {
3216
+ context.strokeStyle = lineColor.toString();
3217
+ context.lineWidth = lineWidth;
3218
+ context.stroke();
3219
+ }
3220
+ }, screenSpace, context);
3221
+ }
3045
3222
  }
3046
3223
 
3047
3224
  /** Draw colored ellipse using passed in point
3048
3225
  * @param {Vector2} pos
3049
- * @param {Vector2} [size=(1,1)]
3226
+ * @param {Vector2} [size=(1,1)] - Width and height diameter
3050
3227
  * @param {Color} [color=(1,1,1,1)]
3051
3228
  * @param {number} [angle]
3052
3229
  * @param {number} [lineWidth]
3053
3230
  * @param {Color} [lineColor=(0,0,0,1)]
3054
- * @param {boolean} [useWebGL] - Webgl not supported
3231
+ * @param {boolean} [useWebGL=glEnable]
3055
3232
  * @param {boolean} [screenSpace]
3056
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3233
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3057
3234
  * @memberof Draw */
3058
- function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace=false, context=drawContext)
3235
+ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3059
3236
  {
3060
- ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
3061
- ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
3237
+ ASSERT(isVector2(pos), 'drawEllipse pos should be a vec2');
3238
+ ASSERT(isVector2(size), 'drawEllipse size should be a vec2');
3062
3239
  ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3063
3240
  ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3064
3241
  ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
3065
- ASSERT(lineWidth>=0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3066
- ASSERT(!useWebGL, 'drawEllipse webgl not supported');
3067
- drawCanvas2D(pos, vec2(1), angle, false, context=>
3242
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3243
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3244
+ if (useWebGL)
3068
3245
  {
3069
- context.beginPath();
3070
- context.ellipse(0, 0, size.y, size.x, 0, 0, 9);
3071
- context.fillStyle = color.toString();
3072
- context.fill();
3073
- if (lineWidth)
3246
+ // draw as a regular polygon
3247
+ const sides = glCircleSides;
3248
+ drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
3249
+ }
3250
+ else
3251
+ {
3252
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3074
3253
  {
3075
- context.strokeStyle = lineColor.toString();
3076
- context.lineWidth = lineWidth;
3077
- context.stroke();
3078
- }
3079
- }, screenSpace, context);
3254
+ context.fillStyle = color.toString();
3255
+ context.beginPath();
3256
+ context.ellipse(0, 0, size.x/2, size.y/2, 0, 0, 9);
3257
+ context.fill();
3258
+ if (lineWidth)
3259
+ {
3260
+ context.strokeStyle = lineColor.toString();
3261
+ context.lineWidth = lineWidth;
3262
+ context.stroke();
3263
+ }
3264
+ }, screenSpace, context);
3265
+ }
3080
3266
  }
3081
3267
 
3082
3268
  /** Draw colored circle using passed in point
3083
3269
  * @param {Vector2} pos
3084
- * @param {number} [radius=1]
3270
+ * @param {number} [size=1] - Diameter
3085
3271
  * @param {Color} [color=(1,1,1,1)]
3086
3272
  * @param {number} [lineWidth=0]
3087
3273
  * @param {Color} [lineColor=(0,0,0,1)]
3088
- * @param {boolean} [useWebGL] - Webgl not supported
3089
- * @param {boolean} [screenSpace=false]
3090
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3274
+ * @param {boolean} [useWebGL=glEnable]
3275
+ * @param {boolean} [screenSpace]
3276
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3091
3277
  * @memberof Draw */
3092
- function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace, context=drawContext)
3093
- { drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
3278
+ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3279
+ {
3280
+ ASSERT(isNumber(size), 'drawCircle size should be a number');
3281
+ drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
3282
+ }
3094
3283
 
3095
3284
  /** Draw directly to a 2d canvas context in world space
3096
3285
  * @param {Vector2} pos
@@ -3098,7 +3287,7 @@ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK
3098
3287
  * @param {number} angle
3099
3288
  * @param {boolean} [mirror]
3100
3289
  * @param {Function} [drawFunction]
3101
- * @param {boolean} [screenSpace=false]
3290
+ * @param {boolean} [screenSpace=false]
3102
3291
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3103
3292
  * @memberof Draw */
3104
3293
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
@@ -3168,7 +3357,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3168
3357
  * @param {number} [maxWidth]
3169
3358
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3170
3359
  * @memberof Draw */
3171
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
3360
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3172
3361
  {
3173
3362
  context.fillStyle = color.toString();
3174
3363
  context.strokeStyle = lineColor.toString();
@@ -3176,7 +3365,6 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
3176
3365
  context.textAlign = textAlign;
3177
3366
  context.font = size + 'px '+ font;
3178
3367
  context.textBaseline = 'middle';
3179
- context.lineJoin = 'round';
3180
3368
 
3181
3369
  const lines = (text+'').split('\n');
3182
3370
  let posY = pos.y;
@@ -3200,7 +3388,7 @@ function screenToWorld(screenPos)
3200
3388
  {
3201
3389
  let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3202
3390
  let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3203
- if (cameraAngle)
3391
+ if (cameraAngle)
3204
3392
  {
3205
3393
  // apply camera rotation
3206
3394
  const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
@@ -3293,7 +3481,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3293
3481
  {
3294
3482
  // white texture with no additive alpha, no need to tint
3295
3483
  context.globalAlpha = color.a;
3296
- context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3484
+ context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3297
3485
  context.globalAlpha = 1;
3298
3486
  }
3299
3487
  else
@@ -3314,7 +3502,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3314
3502
  for (let i = 0; i < data.length; ++i)
3315
3503
  data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
3316
3504
  workContext.putImageData(imageData, 0, 0);
3317
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3505
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3318
3506
  }
3319
3507
  else
3320
3508
  {
@@ -3327,7 +3515,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3327
3515
  }
3328
3516
  workContext.putImageData(imageData, 0, 0);
3329
3517
  context.globalAlpha = color.a;
3330
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3518
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3331
3519
  context.globalAlpha = 1;
3332
3520
  }
3333
3521
  }
@@ -3354,7 +3542,7 @@ function toggleFullscreen()
3354
3542
  }
3355
3543
 
3356
3544
  /** Set the cursor style
3357
- * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
3545
+ * @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
3358
3546
  * @memberof Draw */
3359
3547
  function setCursor(cursorStyle = 'auto')
3360
3548
  {
@@ -3366,7 +3554,7 @@ function setCursor(cursorStyle = 'auto')
3366
3554
 
3367
3555
  let engineFontImage;
3368
3556
 
3369
- /**
3557
+ /**
3370
3558
  * Font Image Object - Draw text on a 2D canvas by using characters in an image
3371
3559
  * - 96 characters (from space to tilde) are stored in an image
3372
3560
  * - Uses a default 8x8 font if none is supplied
@@ -3374,9 +3562,9 @@ let engineFontImage;
3374
3562
  * @example
3375
3563
  * // use built in font
3376
3564
  * const font = new FontImage;
3377
- *
3565
+ *
3378
3566
  * // draw text
3379
- * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
3567
+ * font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
3380
3568
  */
3381
3569
  class FontImage
3382
3570
  {
@@ -3384,7 +3572,6 @@ class FontImage
3384
3572
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
3385
3573
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3386
3574
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
3387
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
3388
3575
  */
3389
3576
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
3390
3577
  {
@@ -3398,7 +3585,6 @@ class FontImage
3398
3585
  this.image = image || engineFontImage;
3399
3586
  this.tileSize = tileSize;
3400
3587
  this.paddingSize = paddingSize;
3401
- this.context = context;
3402
3588
  }
3403
3589
 
3404
3590
  /** Draw text in world space using the image font
@@ -3406,23 +3592,32 @@ class FontImage
3406
3592
  * @param {Vector2} pos
3407
3593
  * @param {number} [scale=.25]
3408
3594
  * @param {boolean} [center]
3595
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
3409
3596
  */
3410
- drawText(text, pos, scale=1, center)
3597
+ drawText(text, pos, scale=1, center, context=drawContext)
3411
3598
  {
3412
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
3599
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
3413
3600
  }
3414
3601
 
3415
- /** Draw text in screen space using the image font
3602
+ /** Draw text on overlay canvas in world space using the image font
3603
+ * @param {string} text
3604
+ * @param {Vector2} pos
3605
+ * @param {number} [scale]
3606
+ * @param {boolean} [center]
3607
+ */
3608
+ drawTextOverlay(text, pos, scale=4, center)
3609
+ { this.drawText(text, pos, scale, center, overlayContext); }
3610
+
3611
+ /** Draw text on overlay canvas in screen space using the image font
3416
3612
  * @param {string} text
3417
3613
  * @param {Vector2} pos
3418
3614
  * @param {number} [scale]
3419
3615
  * @param {boolean} [center]
3616
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3420
3617
  */
3421
- drawTextScreen(text, pos, scale=4, center)
3618
+ drawTextScreen(text, pos, scale=4, center, context=overlayContext)
3422
3619
  {
3423
- const context = this.context;
3424
3620
  context.save();
3425
-
3426
3621
  const size = this.tileSize;
3427
3622
  const drawSize = size.add(this.paddingSize).scale(scale);
3428
3623
  const cols = this.image.width / this.tileSize.x |0;
@@ -3441,15 +3636,14 @@ class FontImage
3441
3636
  const x = tile % cols;
3442
3637
  const y = tile / cols |0;
3443
3638
  const drawPos = pos.add(vec2(j,i).multiply(drawSize));
3444
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
3639
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
3445
3640
  drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
3446
3641
  }
3447
3642
  });
3448
-
3449
3643
  context.restore();
3450
3644
  }
3451
3645
  }
3452
- /**
3646
+ /**
3453
3647
  * LittleJS Input System
3454
3648
  * - Tracks keyboard down, pressed, and released
3455
3649
  * - Tracks mouse buttons, position, and wheel
@@ -3465,10 +3659,10 @@ class FontImage
3465
3659
  * @return {boolean}
3466
3660
  * @memberof Input */
3467
3661
  function keyIsDown(key, device=0)
3468
- {
3662
+ {
3469
3663
  ASSERT(key !== undefined, 'key is undefined');
3470
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
3471
- return inputData[device] && !!(inputData[device][key] & 1);
3664
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
3665
+ return inputData[device] && !!(inputData[device][key] & 1);
3472
3666
  }
3473
3667
 
3474
3668
  /** Returns true if device key was pressed this frame
@@ -3477,10 +3671,10 @@ function keyIsDown(key, device=0)
3477
3671
  * @return {boolean}
3478
3672
  * @memberof Input */
3479
3673
  function keyWasPressed(key, device=0)
3480
- {
3674
+ {
3481
3675
  ASSERT(key !== undefined, 'key is undefined');
3482
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
3483
- return inputData[device] && !!(inputData[device][key] & 2);
3676
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
3677
+ return inputData[device] && !!(inputData[device][key] & 2);
3484
3678
  }
3485
3679
 
3486
3680
  /** Returns true if device key was released this frame
@@ -3489,9 +3683,9 @@ function keyWasPressed(key, device=0)
3489
3683
  * @return {boolean}
3490
3684
  * @memberof Input */
3491
3685
  function keyWasReleased(key, device=0)
3492
- {
3686
+ {
3493
3687
  ASSERT(key !== undefined, 'key is undefined');
3494
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
3688
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
3495
3689
  return inputData[device] && !!(inputData[device][key] & 4);
3496
3690
  }
3497
3691
 
@@ -3613,7 +3807,7 @@ function gamepadWasReleased(button, gamepad=0)
3613
3807
  * @param {number} [gamepad]
3614
3808
  * @return {Vector2}
3615
3809
  * @memberof Input */
3616
- function gamepadStick(stick, gamepad=0)
3810
+ function gamepadStick(stick, gamepad=0)
3617
3811
  { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
3618
3812
 
3619
3813
  ///////////////////////////////////////////////////////////////////////////////
@@ -3690,10 +3884,10 @@ function inputInit()
3690
3884
  {
3691
3885
  // handle remapping wasd keys to directions
3692
3886
  return inputWASDEmulateDirection ?
3693
- c == 'KeyW' ? 'ArrowUp' :
3694
- c == 'KeyS' ? 'ArrowDown' :
3695
- c == 'KeyA' ? 'ArrowLeft' :
3696
- c == 'KeyD' ? 'ArrowRight' : c : c;
3887
+ c === 'KeyW' ? 'ArrowUp' :
3888
+ c === 'KeyS' ? 'ArrowDown' :
3889
+ c === 'KeyA' ? 'ArrowLeft' :
3890
+ c === 'KeyD' ? 'ArrowRight' : c : c;
3697
3891
  }
3698
3892
  function onMouseDown(e)
3699
3893
  {
@@ -3701,9 +3895,9 @@ function inputInit()
3701
3895
  return;
3702
3896
 
3703
3897
  // fix stalled audio requiring user interaction
3704
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
3898
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
3705
3899
  audioContext.resume();
3706
-
3900
+
3707
3901
  isUsingGamepad = false;
3708
3902
  inputData[0][e.button] = 3;
3709
3903
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
@@ -3746,8 +3940,8 @@ function gamepadsUpdate()
3746
3940
  const applyDeadZones = (v)=>
3747
3941
  {
3748
3942
  const min=.3, max=.8;
3749
- const deadZone = (v)=>
3750
- v > min ? percent( v, min, max) :
3943
+ const deadZone = (v)=>
3944
+ v > min ? percent(v, min, max) :
3751
3945
  v < -min ? -percent(-v, min, max) : 0;
3752
3946
  return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
3753
3947
  }
@@ -3755,30 +3949,29 @@ function gamepadsUpdate()
3755
3949
  // update touch gamepad if enabled
3756
3950
  if (touchGamepadEnable && isTouchDevice)
3757
3951
  {
3758
- ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
3759
- if (touchGamepadTimer.isSet())
3952
+ if (!touchGamepadTimer.isSet())
3953
+ return;
3954
+
3955
+ // read virtual analog stick
3956
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
3957
+ sticks[0] = vec2();
3958
+ if (touchGamepadAnalog)
3959
+ sticks[0] = applyDeadZones(touchGamepadStick);
3960
+ else if (touchGamepadStick.lengthSquared() > .3)
3760
3961
  {
3761
- // read virtual analog stick
3762
- const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
3763
- sticks[0] = vec2();
3764
- if (touchGamepadAnalog)
3765
- sticks[0] = applyDeadZones(touchGamepadStick);
3766
- else if (touchGamepadStick.lengthSquared() > .3)
3767
- {
3768
- // convert to 8 way dpad
3769
- sticks[0].x = Math.round(touchGamepadStick.x);
3770
- sticks[0].y = -Math.round(touchGamepadStick.y);
3771
- sticks[0] = sticks[0].clampLength();
3772
- }
3962
+ // convert to 8 way dpad
3963
+ sticks[0].x = Math.round(touchGamepadStick.x);
3964
+ sticks[0].y = -Math.round(touchGamepadStick.y);
3965
+ sticks[0] = sticks[0].clampLength();
3966
+ }
3773
3967
 
3774
- // read virtual gamepad buttons
3775
- const data = inputData[1] || (inputData[1] = []);
3776
- for (let i=10; i--;)
3777
- {
3778
- const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
3779
- const wasDown = gamepadIsDown(j,0);
3780
- data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
3781
- }
3968
+ // read virtual gamepad buttons
3969
+ const data = inputData[1] || (inputData[1] = []);
3970
+ for (let i=10; i--;)
3971
+ {
3972
+ const j = i === 3 ? 2 : i === 2 ? 3 : i; // fix button locations
3973
+ const wasDown = gamepadIsDown(j,0);
3974
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
3782
3975
  }
3783
3976
  }
3784
3977
 
@@ -3804,7 +3997,7 @@ function gamepadsUpdate()
3804
3997
  // read analog sticks
3805
3998
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
3806
3999
  sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
3807
-
4000
+
3808
4001
  // read buttons
3809
4002
  for (let j = gamepad.buttons.length; j--;)
3810
4003
  {
@@ -3820,14 +4013,14 @@ function gamepadsUpdate()
3820
4013
  {
3821
4014
  // copy dpad to left analog stick when pressed
3822
4015
  const dpad = vec2(
3823
- (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
4016
+ (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
3824
4017
  (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
3825
4018
  if (dpad.lengthSquared())
3826
4019
  sticks[0] = dpad.clampLength();
3827
4020
  }
3828
4021
 
3829
4022
  // disable touch gamepad if using real gamepad
3830
- touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
4023
+ touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
3831
4024
  }
3832
4025
  }
3833
4026
  }
@@ -3852,20 +4045,13 @@ function vibrateStop() { vibrate(0); }
3852
4045
  const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
3853
4046
 
3854
4047
  // touch gamepad internal variables
3855
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
4048
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
3856
4049
 
3857
4050
  // enable touch input mouse passthrough
3858
4051
  function touchInputInit()
3859
4052
  {
3860
4053
  // add non passive touch event listeners
3861
4054
  let handleTouch = handleTouchDefault;
3862
- if (touchGamepadEnable)
3863
- {
3864
- // touch input internal variables
3865
- handleTouch = handleTouchGamepad;
3866
- touchGamepadButtons = [];
3867
- touchGamepadStick = vec2();
3868
- }
3869
4055
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
3870
4056
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
3871
4057
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
@@ -3874,8 +4060,15 @@ function touchInputInit()
3874
4060
  let wasTouching;
3875
4061
  function handleTouchDefault(e)
3876
4062
  {
4063
+ if (!touchInputEnable)
4064
+ return;
4065
+
4066
+ // route touch to gamepad
4067
+ if (touchGamepadEnable)
4068
+ handleTouchGamepad(e);
4069
+
3877
4070
  // fix stalled audio requiring user interaction
3878
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
4071
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
3879
4072
  audioContext.resume();
3880
4073
 
3881
4074
  // check if touching and pass to mouse events
@@ -3904,7 +4097,7 @@ function touchInputInit()
3904
4097
  // prevent default handling like copy and magnifier lens
3905
4098
  if (inputPreventDefault && document.hasFocus()) // allow document to get focus
3906
4099
  e.preventDefault();
3907
-
4100
+
3908
4101
  // must return true so the document will get focus
3909
4102
  return true;
3910
4103
  }
@@ -3916,7 +4109,7 @@ function touchInputInit()
3916
4109
  touchGamepadStick = vec2();
3917
4110
  touchGamepadButtons = [];
3918
4111
  isUsingGamepad = true;
3919
-
4112
+
3920
4113
  const touching = e.touches.length;
3921
4114
  if (touching)
3922
4115
  {
@@ -3925,9 +4118,6 @@ function touchInputInit()
3925
4118
  {
3926
4119
  // touch anywhere to press start when paused
3927
4120
  touchGamepadButtons[9] = 1;
3928
-
3929
- // call default touch handler so normal touch events still work
3930
- handleTouchDefault(e);
3931
4121
  return;
3932
4122
  }
3933
4123
  }
@@ -3958,12 +4148,6 @@ function touchInputInit()
3958
4148
  touchGamepadButtons[9] = 1;
3959
4149
  }
3960
4150
  }
3961
-
3962
- // call default touch handler so normal touch events still work
3963
- handleTouchDefault(e);
3964
-
3965
- // must return true so the document will get focus
3966
- return true;
3967
4151
  }
3968
4152
  }
3969
4153
 
@@ -3973,7 +4157,7 @@ function touchGamepadRender()
3973
4157
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
3974
4158
  if (!touchGamepadEnable || !touchGamepadTimer.isSet())
3975
4159
  return;
3976
-
4160
+
3977
4161
  // fade off when not touching or paused
3978
4162
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
3979
4163
  if (!alpha || paused)
@@ -4004,11 +4188,11 @@ function touchGamepadRender()
4004
4188
  const angle = i*PI/4;
4005
4189
  context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
4006
4190
  i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
4007
- i==1 && context.fill();
4191
+ i===1 && context.fill();
4008
4192
  }
4009
4193
  context.stroke();
4010
4194
  }
4011
-
4195
+
4012
4196
  // draw right face buttons
4013
4197
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4014
4198
  for (let i=4; i--;)
@@ -4043,8 +4227,8 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
4043
4227
  /** Check if pointer is locked (true if locked)
4044
4228
  * @return {boolean}
4045
4229
  * @memberof Input */
4046
- function pointerLockIsActive() { return document.pointerLockElement == mainCanvas; }
4047
- /**
4230
+ function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
4231
+ /**
4048
4232
  * LittleJS Audio System
4049
4233
  * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
4050
4234
  * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
@@ -4065,10 +4249,21 @@ let audioContext = new AudioContext;
4065
4249
  * @memberof Audio */
4066
4250
  let audioMasterGain;
4067
4251
 
4252
+ /** Default sample rate used for sounds
4253
+ * @default 44100
4254
+ * @memberof Audio */
4255
+ const audioDefaultSampleRate = 44100;
4256
+
4257
+ /** Check if the audio context is running and available for playback
4258
+ * @return {boolean} - True if the audio context is running
4259
+ * @memberof Audio */
4260
+ function audioIsRunning()
4261
+ { return audioContext.state === 'running'; }
4262
+
4068
4263
  function audioInit()
4069
4264
  {
4070
4265
  if (!soundEnable || headlessMode) return;
4071
-
4266
+
4072
4267
  audioMasterGain = audioContext.createGain();
4073
4268
  audioMasterGain.connect(audioContext.destination);
4074
4269
  audioMasterGain.gain.value = soundVolume; // set starting value
@@ -4076,14 +4271,14 @@ function audioInit()
4076
4271
 
4077
4272
  ///////////////////////////////////////////////////////////////////////////////
4078
4273
 
4079
- /**
4274
+ /**
4080
4275
  * Sound Object - Stores a sound for later use and can be played positionally
4081
- *
4276
+ *
4082
4277
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4083
4278
  * @example
4084
4279
  * // create a sound
4085
4280
  * const sound_example = new Sound([.5,.5]);
4086
- *
4281
+ *
4087
4282
  * // play the sound
4088
4283
  * sound_example.play();
4089
4284
  */
@@ -4100,33 +4295,41 @@ class Sound
4100
4295
 
4101
4296
  /** @property {number} - World space max range of sound */
4102
4297
  this.range = range;
4103
-
4104
4298
  /** @property {number} - At what percentage of range should it start tapering */
4105
4299
  this.taper = taper;
4106
-
4107
4300
  /** @property {number} - How much to randomize frequency each time sound plays */
4108
4301
  this.randomness = 0;
4302
+ /** @property {number} - Sample rate for this sound */
4303
+ this.sampleRate = audioDefaultSampleRate;
4304
+ /** @property {number} - Percentage of this sound currently loaded */
4305
+ this.loadedPercent = 0;
4109
4306
 
4307
+ // generate zzfx sound now for fast playback
4110
4308
  if (zzfxSound)
4111
4309
  {
4112
- // generate zzfx sound now for fast playback
4113
- const defaultRandomness = .05;
4114
- this.randomness = zzfxSound[1] != undefined ? zzfxSound[1] : defaultRandomness;
4115
- zzfxSound[1] = 0; // generate without randomness
4310
+ // remove randomness so it can be applied on playback
4311
+ const randomnessIndex = 1, defaultRandomness = .05;
4312
+ this.randomness = zzfxSound[randomnessIndex] !== undefined ?
4313
+ zzfxSound[randomnessIndex] : defaultRandomness;
4314
+ zzfxSound[randomnessIndex] = 0;
4315
+
4316
+ // generate the zzfx samples
4116
4317
  this.sampleChannels = [zzfxG(...zzfxSound)];
4117
- this.sampleRate = zzfxR;
4318
+ this.loadedPercent = 1;
4118
4319
  }
4119
4320
  }
4120
4321
 
4121
4322
  /** Play the sound
4122
- * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
4123
- * @param {number} [volume] - How much to scale volume by (in addition to range fade)
4124
- * @param {number} [pitch] - How much to scale pitch by (also adjusted by this.randomness)
4125
- * @param {number} [randomnessScale] - How much to scale randomness
4126
- * @param {boolean} [loop] - Should the sound loop
4127
- * @return {AudioBufferSourceNode} - The audio source node
4323
+ * Sounds may not play until a user interaction occurs
4324
+ * @param {Vector2} [pos] - World space position to play the sound if any
4325
+ * @param {number} [volume] - How much to scale volume by
4326
+ * @param {number} [pitch] - How much to scale pitch by
4327
+ * @param {number} [randomnessScale] - How much to scale pitch randomness
4328
+ * @param {boolean} [loop] - Should the sound loop?
4329
+ * @param {boolean} [paused] - Should the sound start paused
4330
+ * @return {SoundInstance} - The audio source node
4128
4331
  */
4129
- play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
4332
+ play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
4130
4333
  {
4131
4334
  if (!soundEnable || headlessMode) return;
4132
4335
  if (!this.sampleChannels) return;
@@ -4149,75 +4352,55 @@ class Sound
4149
4352
  // get pan from screen space coords
4150
4353
  pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
4151
4354
  }
4152
-
4153
- // play the sound
4154
- const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
4155
- this.gainNode = audioContext.createGain();
4156
- this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
4157
- return this.source;
4158
- }
4159
-
4160
- /** Set the sound volume of the most recently played instance of this sound
4161
- * @param {number} [volume] - How much to scale volume by
4162
- */
4163
- setVolume(volume=1)
4164
- {
4165
- if (this.gainNode)
4166
- this.gainNode.gain.value = volume;
4167
- }
4168
-
4169
- /** Stop the last instance of this sound that was played
4170
- * @param {number} [fadeTime] - How long to fade out (seconds)
4171
- */
4172
- stop(fadeTime=0)
4173
- {
4174
- if (!this.source)
4175
- return;
4176
4355
 
4177
- // ramp off gain
4178
- const startFade = audioContext.currentTime;
4179
- const endFade = startFade + fadeTime;
4180
- this.gainNode.gain.linearRampToValueAtTime(1, startFade);
4181
- this.gainNode.gain.linearRampToValueAtTime(0, endFade);
4182
- this.source.stop(endFade);
4183
- this.source = undefined;
4356
+ // Create and return sound instance
4357
+ const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
4358
+ return new SoundInstance(this, volume, rate, pan, loop, paused);
4184
4359
  }
4185
4360
 
4186
- /** Get source of most recent instance of this sound that was played
4187
- * @return {AudioBufferSourceNode}
4361
+ /** Play a music track that loops by default
4362
+ * @param {number} [volume] - Volume to play the music at
4363
+ * @param {boolean} [loop] - Should the music loop?
4364
+ * @param {boolean} [paused] - Should the music start paused
4365
+ * @return {SoundInstance} - The audio source node
4188
4366
  */
4189
- getSource() { return this.source; }
4367
+ playMusic(volume=1, loop=true, paused=false)
4368
+ { return this.play(undefined, volume, 1, 0, loop, paused); }
4190
4369
 
4191
- /** Play the sound as a note with a semitone offset
4370
+ /** Play the sound as a musical note with a semitone offset
4371
+ * This can be used to play music with chromatic scales
4192
4372
  * @param {number} semitoneOffset - How many semitones to offset pitch
4193
- * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
4194
- * @param {number} [volume=1] - How much to scale volume by (in addition to range fade)
4195
- * @return {AudioBufferSourceNode} - The audio source node
4373
+ * @param {Vector2} [pos] - World space position to play the sound if any
4374
+ * @param {number} [volume=1] - How much to scale volume by
4375
+ * @return {SoundInstance} - The audio source node
4196
4376
  */
4197
4377
  playNote(semitoneOffset, pos, volume)
4198
- { return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
4378
+ {
4379
+ const pitch = getNoteFrequency(semitoneOffset, 1);
4380
+ return this.play(pos, volume, pitch, 0);
4381
+ }
4199
4382
 
4200
4383
  /** Get how long this sound is in seconds
4201
4384
  * @return {number} - How long the sound is in seconds (undefined if loading)
4202
4385
  */
4203
- getDuration()
4386
+ getDuration()
4204
4387
  { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
4205
-
4206
- /** Check if sound is loading, for sounds fetched from a url
4207
- * @return {boolean} - True if sound is loading and not ready to play
4388
+
4389
+ /** Check if sound is loaded, for sounds fetched from a url
4390
+ * @return {boolean} - True if sound is loaded and ready to play
4208
4391
  */
4209
- isLoading() { return !this.sampleChannels; }
4392
+ isLoaded() { return this.loadedPercent === 1; }
4210
4393
  }
4211
4394
 
4212
4395
  ///////////////////////////////////////////////////////////////////////////////
4213
4396
 
4214
- /**
4397
+ /**
4215
4398
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
4216
4399
  * - this can be used to play wave, mp3, and ogg files
4217
4400
  * @example
4218
4401
  * // create a sound
4219
4402
  * const sound_example = new SoundWave('sound.mp3');
4220
- *
4403
+ *
4221
4404
  * // play the sound
4222
4405
  * sound_example.play();
4223
4406
  */
@@ -4241,34 +4424,210 @@ class SoundWave extends Sound
4241
4424
  this.loadSound(filename);
4242
4425
  }
4243
4426
 
4244
- /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
4245
- * @param {string} filename
4246
- * @return {Promise<void>} */
4247
- async loadSound(filename)
4427
+ /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
4428
+ * @param {string} filename
4429
+ * @return {Promise<void>} */
4430
+ async loadSound(filename)
4431
+ {
4432
+ const response = await fetch(filename);
4433
+ const arrayBuffer = await response.arrayBuffer();
4434
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
4435
+
4436
+ // convert audio buffer to sample channels across multiple frames
4437
+ const channelCount = audioBuffer.numberOfChannels;
4438
+ const samplesPerFrame = 1e5;
4439
+ const sampleChannels = [];
4440
+ for (let channel = 0; channel < channelCount; channel++)
4441
+ {
4442
+ const channelData = audioBuffer.getChannelData(channel);
4443
+ const channelLength = channelData.length;
4444
+ sampleChannels[channel] = new Array(channelLength);
4445
+ let sampleIndex = 0;
4446
+ while (sampleIndex < channelLength)
4447
+ {
4448
+ // yield to next frame
4449
+ await new Promise(resolve => setTimeout(resolve, 0));
4450
+
4451
+ // copy chunk of samples
4452
+ const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
4453
+ for (; sampleIndex < endIndex; sampleIndex++)
4454
+ sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
4455
+
4456
+ // update loaded percent
4457
+ const samplesTotal = channelCount * channelLength;
4458
+ const samplesProcessed = channel * channelLength + sampleIndex;
4459
+ this.loadedPercent = samplesProcessed / samplesTotal;
4460
+ }
4461
+ }
4462
+
4463
+ // setup the sound to be played
4464
+ this.sampleRate = audioBuffer.sampleRate;
4465
+ this.sampleChannels = sampleChannels;
4466
+ this.loadedPercent = 1;
4467
+ if (this.onloadCallback)
4468
+ this.onloadCallback();
4469
+ }
4470
+ }
4471
+
4472
+ ///////////////////////////////////////////////////////////////////////////////
4473
+
4474
+ /**
4475
+ * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
4476
+ * Represents a single playing instance of a sound with pause/resume capabilities
4477
+ * @example
4478
+ * // Play a sound and get an instance for control
4479
+ * const jumpSound = new Sound([.5,.5,220]);
4480
+ * const instance = jumpSound.play();
4481
+ *
4482
+ * // Control the individual instance
4483
+ * instance.setVolume(.5);
4484
+ * instance.pause();
4485
+ * instance.unpause();
4486
+ * instance.stop();
4487
+ */
4488
+ class SoundInstance
4489
+ {
4490
+ /** Create a sound instance
4491
+ * @param {Sound} sound - The sound object
4492
+ * @param {number} [volume] - How much to scale volume by
4493
+ * @param {number} [rate] - The playback rate to use
4494
+ * @param {number} [pan] - How much to apply stereo panning
4495
+ * @param {boolean} [loop] - Should the sound loop?
4496
+ * @param {boolean} [paused] - Should the sound start paused? */
4497
+ constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
4498
+ {
4499
+ ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
4500
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
4501
+ ASSERT(rate >= 0, 'Sound rate must be positive or zero');
4502
+ ASSERT(isNumber(pan), 'Sound pan must be a number');
4503
+
4504
+ /** @property {Sound} - The sound object */
4505
+ this.sound = sound;
4506
+ /** @property {number} - How much to scale volume by */
4507
+ this.volume = volume;
4508
+ /** @property {number} - The playback rate to use */
4509
+ this.rate = rate;
4510
+ /** @property {number} - How much to apply stereo panning */
4511
+ this.pan = pan;
4512
+ /** @property {boolean} - Should the sound loop */
4513
+ this.loop = loop;
4514
+ /** @property {number} - Timestamp for audio context when paused */
4515
+ this.pausedTime = 0;
4516
+ /** @property {number} - Timestamp for audio context when started */
4517
+ this.startTime = undefined;
4518
+ /** @property {GainNode} - Gain node for the sound */
4519
+ this.gainNode = undefined;
4520
+ /** @property {AudioBufferSourceNode} - Source node of the audio */
4521
+ this.source = undefined;
4522
+ // setup end callback and start sound
4523
+ this.onendedCallback = (source)=>
4524
+ {
4525
+ if (source === this.source)
4526
+ this.source = undefined;
4527
+ };
4528
+ if (!paused)
4529
+ this.start();
4530
+ }
4531
+
4532
+ /** Start playing the sound instance from the offset time
4533
+ * @param {number} [offset] - Offset in seconds to start playback from
4534
+ */
4535
+ start(offset=0)
4536
+ {
4537
+ ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
4538
+ if (this.isPlaying())
4539
+ this.stop();
4540
+ this.gainNode = audioContext.createGain();
4541
+ this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
4542
+ this.startTime = audioContext.currentTime - offset;
4543
+ this.pausedTime = undefined;
4544
+ }
4545
+
4546
+ /** Set the volume of this sound instance
4547
+ * @param {number} volume */
4548
+ setVolume(volume)
4549
+ {
4550
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
4551
+ this.volume = volume;
4552
+ if (this.gainNode)
4553
+ this.gainNode.gain.value = volume;
4554
+ }
4555
+
4556
+ /** Stop this sound instance and reset position to the start */
4557
+ stop(fadeTime=0)
4558
+ {
4559
+ ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
4560
+ if (this.isPlaying())
4561
+ {
4562
+ if (fadeTime)
4563
+ {
4564
+ // ramp off gain
4565
+ const startFade = audioContext.currentTime;
4566
+ const endFade = startFade + fadeTime;
4567
+ this.gainNode.gain.linearRampToValueAtTime(1, startFade);
4568
+ this.gainNode.gain.linearRampToValueAtTime(0, endFade);
4569
+ this.source.stop(endFade);
4570
+ }
4571
+ else
4572
+ this.source.stop();
4573
+ }
4574
+ this.pausedTime = 0;
4575
+ this.source = undefined;
4576
+ this.startTime = undefined;
4577
+ }
4578
+
4579
+ /** Pause this sound instance */
4580
+ pause()
4581
+ {
4582
+ if (this.isPaused())
4583
+ return;
4584
+
4585
+ // save current time and stop sound
4586
+ this.pausedTime = this.getCurrentTime();
4587
+ this.source.stop();
4588
+ this.source = undefined;
4589
+ this.startTime = undefined;
4590
+ }
4591
+
4592
+ /** Unpauses this sound instance */
4593
+ resume()
4594
+ {
4595
+ if (!this.isPaused())
4596
+ return;
4597
+
4598
+ // restart sound from paused time
4599
+ this.start(this.pausedTime);
4600
+ }
4601
+
4602
+ /** Check if this instance is currently playing
4603
+ * @return {boolean} - True if playing
4604
+ */
4605
+ isPlaying() { return !!this.source; }
4606
+
4607
+ /** Check if this instance is paused and was not stopped
4608
+ * @return {boolean} - True if paused
4609
+ */
4610
+ isPaused() { return !this.isPlaying(); }
4611
+
4612
+ /** Get the current playback time in seconds
4613
+ * @return {number} - Current playback time
4614
+ */
4615
+ getCurrentTime()
4248
4616
  {
4249
- const response = await fetch(filename);
4250
- const arrayBuffer = await response.arrayBuffer();
4251
- const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
4252
- this.sampleChannels = [];
4253
- for (let i = audioBuffer.numberOfChannels; i--;)
4254
- this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
4255
- this.sampleRate = audioBuffer.sampleRate;
4256
- if (this.onloadCallback)
4257
- this.onloadCallback();
4617
+ const deltaTime = mod(audioContext.currentTime - this.startTime,
4618
+ this.getDuration());
4619
+ return this.isPlaying() ? deltaTime : this.pausedTime;
4258
4620
  }
4259
- }
4260
4621
 
4261
- /** Play an mp3, ogg, or wav audio from a local file or url
4262
- * @param {string} filename - Location of sound file to play
4263
- * @param {number} [volume] - How much to scale volume by
4264
- * @param {boolean} [loop] - True if the music should loop
4265
- * @return {SoundWave} - The sound object for this file
4266
- * @memberof Audio */
4267
- function playAudioFile(filename, volume=1, loop=false)
4268
- {
4269
- if (!soundEnable || headlessMode) return;
4622
+ /** Get the total duration of this sound
4623
+ * @return {number} - Total duration in seconds
4624
+ */
4625
+ getDuration() { return this.sound.getDuration() / this.rate; }
4270
4626
 
4271
- return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
4627
+ /** Get source of this sound instance
4628
+ * @return {AudioBufferSourceNode}
4629
+ */
4630
+ getSource() { return this.source; }
4272
4631
  }
4273
4632
 
4274
4633
  ///////////////////////////////////////////////////////////////////////////////
@@ -4322,9 +4681,11 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4322
4681
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
4323
4682
  * @param {number} [sampleRate=44100] - Sample rate for the sound
4324
4683
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
4684
+ * @param {number} [offset] - Offset in seconds to start playback from
4685
+ * @param {Function} [onended] - Callback for when the sound ends
4325
4686
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4326
4687
  * @memberof Audio */
4327
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR, gainNode)
4688
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
4328
4689
  {
4329
4690
  if (!soundEnable || headlessMode) return;
4330
4691
 
@@ -4349,16 +4710,20 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4349
4710
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
4350
4711
  source.connect(pannerNode).connect(gainNode);
4351
4712
 
4352
- // play the sound
4353
- if (audioContext.state != 'running')
4713
+ // callback when the sound ends
4714
+ if (onended)
4715
+ source.addEventListener('ended', ()=> onended(source));
4716
+
4717
+ if (!audioIsRunning())
4354
4718
  {
4355
- // fix stalled audio and play
4356
- audioContext.resume().then(()=>source.start());
4719
+ // fix stalled audio, this sound won't be able to play
4720
+ audioContext.resume();
4721
+ return;
4357
4722
  }
4358
- else
4359
- source.start();
4360
4723
 
4361
- // return sound
4724
+ // play and return sound
4725
+ const startOffset = offset * rate;
4726
+ source.start(0, startOffset);
4362
4727
  return source;
4363
4728
  }
4364
4729
 
@@ -4366,18 +4731,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4366
4731
  // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
4367
4732
 
4368
4733
  /** Generate and play a ZzFX sound
4369
- *
4734
+ *
4370
4735
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4371
4736
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
4372
4737
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4373
4738
  * @memberof Audio */
4374
4739
  function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
4375
4740
 
4376
- /** Sample rate used for all ZzFX sounds
4377
- * @default 44100
4378
- * @memberof Audio */
4379
- const zzfxR = 44100;
4380
-
4381
4741
  /** Generate samples for a ZzFX sound
4382
4742
  * @param {number} [volume] - Volume scale (percent)
4383
4743
  * @param {number} [randomness] - How much to randomize frequency (percent Hz)
@@ -4401,11 +4761,10 @@ const zzfxR = 44100;
4401
4761
  * @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
4402
4762
  * @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
4403
4763
  * @return {Array} - Array of audio samples
4404
- * @memberof Audio
4405
- */
4764
+ * @memberof Audio */
4406
4765
  function zzfxG
4407
4766
  (
4408
- volume = 1,
4767
+ volume = 1,
4409
4768
  randomness = .05,
4410
4769
  frequency = 220,
4411
4770
  attack = 0,
@@ -4413,11 +4772,11 @@ function zzfxG
4413
4772
  release = .1,
4414
4773
  shape = 0,
4415
4774
  shapeCurve = 1,
4416
- slide = 0,
4417
- deltaSlide = 0,
4418
- pitchJump = 0,
4419
- pitchJumpTime = 0,
4420
- repeatTime = 0,
4775
+ slide = 0,
4776
+ deltaSlide = 0,
4777
+ pitchJump = 0,
4778
+ pitchJumpTime = 0,
4779
+ repeatTime = 0,
4421
4780
  noise = 0,
4422
4781
  modulation = 0,
4423
4782
  bitCrush = 0,
@@ -4429,19 +4788,19 @@ function zzfxG
4429
4788
  )
4430
4789
  {
4431
4790
  // init parameters
4432
- let sampleRate = zzfxR,
4433
- PI2 = PI*2,
4791
+ let sampleRate = audioDefaultSampleRate,
4792
+ PI2 = PI*2,
4434
4793
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
4435
- startFrequency = frequency *=
4794
+ startFrequency = frequency *=
4436
4795
  (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
4437
- modOffset = 0, // modulation offset
4796
+ modOffset = 0, // modulation offset
4438
4797
  repeat = 0, // repeat offset
4439
4798
  crush = 0, // bit crush offset
4440
4799
  jump = 1, // pitch jump timer
4441
4800
  length, // sample length
4442
4801
  b = [], // sample buffer
4443
4802
  t = 0, // sample time
4444
- i = 0, // sample index
4803
+ i = 0, // sample index
4445
4804
  s = 0, // sample value
4446
4805
  f, // wave frequency
4447
4806
 
@@ -4449,7 +4808,7 @@ function zzfxG
4449
4808
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
4450
4809
  cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
4451
4810
  a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
4452
- b0 = (1 + sign(filter) * cos) / 2 / a0,
4811
+ b0 = (1 + sign(filter) * cos) / 2 / a0,
4453
4812
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
4454
4813
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
4455
4814
 
@@ -4495,7 +4854,7 @@ function zzfxG
4495
4854
  0); // post release
4496
4855
 
4497
4856
  s = delay ? s/2 + (delay > i ? 0 : // delay
4498
- (i<length-delay? 1 : (length-i)/delay) * // release delay
4857
+ (i<length-delay? 1 : (length-i)/delay) * // release delay
4499
4858
  b[i-delay|0]/2/volume) : s; // sample delay
4500
4859
 
4501
4860
  if (filter) // apply filter
@@ -4507,14 +4866,14 @@ function zzfxG
4507
4866
  t += f + f*noise*Math.sin(i**5); // noise
4508
4867
 
4509
4868
  if (jump && ++jump > pitchJumpTime) // pitch jump
4510
- {
4869
+ {
4511
4870
  frequency += pitchJump; // apply pitch jump
4512
4871
  startFrequency += pitchJump; // also apply to start
4513
4872
  jump = 0; // stop pitch jump time
4514
- }
4873
+ }
4515
4874
 
4516
4875
  if (repeatTime && !(++repeat % repeatTime)) // repeat
4517
- {
4876
+ {
4518
4877
  frequency = startFrequency; // reset frequency
4519
4878
  slide = startSlide; // reset slide
4520
4879
  jump ||= 1; // reset pitch jump time
@@ -4523,12 +4882,11 @@ function zzfxG
4523
4882
 
4524
4883
  return b; // return sample buffer
4525
4884
  }
4526
- /**
4885
+ /**
4527
4886
  * LittleJS Tile Layer System
4528
4887
  * - Caches arrays of tiles to off screen canvas for fast rendering
4529
4888
  * - Unlimited numbers of layers, allocates canvases as needed
4530
4889
  * - Tile layers can be drawn to using their context with canvas2d
4531
- * - Drawn directly to the main canvas without using WebGL
4532
4890
  * - Tile layers can also have collision with EngineObjects
4533
4891
  * @namespace TileCollision
4534
4892
  */
@@ -4537,9 +4895,9 @@ function zzfxG
4537
4895
  // Tile Layer System
4538
4896
 
4539
4897
  /** Keep track of all tile layers with collision
4540
- * @type {Array<TileCollisionLayer>}
4898
+ * @type {Array<TileCollisionLayer>}
4541
4899
  * @memberof TileCollision */
4542
- let tileCollisionLayers = [];
4900
+ const tileCollisionLayers = [];
4543
4901
 
4544
4902
  /** Get tile collision data for a given cell in the grid
4545
4903
  * @param {Vector2} pos
@@ -4593,7 +4951,7 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
4593
4951
  }
4594
4952
 
4595
4953
  ///////////////////////////////////////////////////////////////////////////////
4596
- /**
4954
+ /**
4597
4955
  * Load tile layers from exported data
4598
4956
  * @param {Object} tileMapData - Level data from exported data
4599
4957
  * @param {TileInfo} [tileInfo] - Default tile info (used for size and texture)
@@ -4626,13 +4984,13 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
4626
4984
  {
4627
4985
  const dataLayer = tileMapData.layers[layerIndex];
4628
4986
  ASSERT(dataLayer.data && dataLayer.data.length);
4629
- ASSERT(levelSize.area() == dataLayer.data.length);
4987
+ ASSERT(levelSize.area() === dataLayer.data.length);
4630
4988
 
4631
4989
  const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
4632
4990
  const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
4633
4991
  tileLayers[layerIndex] = tileLayer;
4634
4992
 
4635
- for (let x=levelSize.x; x--;)
4993
+ for (let x=levelSize.x; x--;)
4636
4994
  for (let y=levelSize.y; y--;)
4637
4995
  {
4638
4996
  const pos = vec2(x, levelSize.y-1-y);
@@ -4691,7 +5049,7 @@ class TileLayerData
4691
5049
  /**
4692
5050
  * Canvas Layer - cached off screen rendering system
4693
5051
  * - Contains an offscreen canvas that can be rendered to
4694
- * - Webgl rendering is optional, call useWebGL to enable
5052
+ * - WebGL rendering is optional, call useWebGL to enable
4695
5053
  * @extends EngineObject
4696
5054
  * @example
4697
5055
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
@@ -4713,18 +5071,18 @@ class CanvasLayer extends EngineObject
4713
5071
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
4714
5072
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
4715
5073
  this.context = headlessMode ? undefined : this.canvas.getContext('2d');
4716
- /** @property {WebGLTexture} - Texture if using webgl for this layer, call useWebGL to enable */
5074
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
4717
5075
  this.glTexture = undefined;
4718
5076
  this.gravityScale = 0; // disable gravity by default for canvas layers
4719
5077
  }
4720
-
5078
+
4721
5079
  /** Destroy this canvas layer */
4722
5080
  destroy()
4723
5081
  {
4724
5082
  if (this.destroyed)
4725
5083
  return;
4726
5084
 
4727
- // free up the webgl texture
5085
+ // free up the WebGL texture
4728
5086
  if (this.glTexture)
4729
5087
  glDeleteTexture(this.glTexture);
4730
5088
  super.destroy();
@@ -4749,12 +5107,12 @@ class CanvasLayer extends EngineObject
4749
5107
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
4750
5108
  {
4751
5109
  // draw the canvas layer as a single tile that uses the whole texture
4752
- const useWebgl = glEnable && this.glTexture != undefined;
5110
+ const useWebGL = glEnable && this.glTexture !== undefined;
4753
5111
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
4754
- drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebgl, screenSpace, context);
5112
+ drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
4755
5113
  }
4756
5114
 
4757
- /** Draw onto the layer canvas in world space (bypass webgl)
5115
+ /** Draw onto the layer canvas in world space (bypass WebGL)
4758
5116
  * @param {Vector2} pos
4759
5117
  * @param {Vector2} size
4760
5118
  * @param {number} angle
@@ -4788,8 +5146,8 @@ class CanvasLayer extends EngineObject
4788
5146
  if (textureInfo)
4789
5147
  {
4790
5148
  context.globalAlpha = color.a; // only alpha is supported
4791
- context.drawImage(textureInfo.image,
4792
- tileInfo.pos.x, tileInfo.pos.y,
5149
+ context.drawImage(textureInfo.image,
5150
+ tileInfo.pos.x, tileInfo.pos.y,
4793
5151
  tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
4794
5152
  context.globalAlpha = 1;
4795
5153
  }
@@ -4807,11 +5165,11 @@ class CanvasLayer extends EngineObject
4807
5165
  * @param {Vector2} [size=(1,1)]
4808
5166
  * @param {Color} [color=(1,1,1,1)]
4809
5167
  * @param {number} [angle=0] */
4810
- drawRect(pos, size, color, angle)
5168
+ drawRect(pos, size, color, angle)
4811
5169
  { this.drawTile(pos, size, undefined, color, angle); }
4812
5170
 
4813
- /** Create or update the webgl texture for this layer
4814
- * @param {boolean} [enable] - enable webgl rendering and update the texture */
5171
+ /** Create or update the WebGL texture for this layer
5172
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture */
4815
5173
  useWebGL(enable=true)
4816
5174
  {
4817
5175
  if (glEnable && enable)
@@ -4857,7 +5215,7 @@ class TileLayer extends CanvasLayer
4857
5215
  this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
4858
5216
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
4859
5217
  this.context = this.canvas.getContext('2d');
4860
- /** @property {WebGLTexture} - Texture if using webgl for this layer */
5218
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer */
4861
5219
  this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
4862
5220
  // set no friction by default, applied friction is max of both objects
4863
5221
  this.friction = 0;
@@ -4882,7 +5240,7 @@ class TileLayer extends CanvasLayer
4882
5240
  }
4883
5241
  }
4884
5242
 
4885
- /** Set data at a given position in the array
5243
+ /** Set data at a given position in the array
4886
5244
  * @param {Vector2} layerPos - Local position in array
4887
5245
  * @param {TileLayerData} data - Data to set
4888
5246
  * @param {boolean} [redraw] - Force the tile to redraw if true */
@@ -4894,26 +5252,26 @@ class TileLayer extends CanvasLayer
4894
5252
  redraw && this.drawTileData(layerPos);
4895
5253
  }
4896
5254
  }
4897
-
4898
- /** Get data at a given position in the array
5255
+
5256
+ /** Get data at a given position in the array
4899
5257
  * @param {Vector2} layerPos - Local position in array
4900
5258
  * @return {TileLayerData} */
4901
5259
  getData(layerPos)
4902
5260
  { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
4903
-
5261
+
4904
5262
  // Render the tile layer, called automatically by the engine
4905
5263
  render()
4906
5264
  {
4907
- ASSERT(drawContext != this.context, 'must call redrawEnd() after drawing tiles!');
4908
-
5265
+ ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5266
+
4909
5267
  // draw the tile layer as a single tile
4910
5268
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
4911
5269
  const pos = this.pos.add(this.size.scale(.5));
4912
- const useWebgl = glEnable && this.glTexture != undefined;
4913
- drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebgl);
5270
+ const useWebGL = glEnable && this.glTexture !== undefined;
5271
+ drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
4914
5272
  }
4915
5273
 
4916
- /** Draw all the tile data to an offscreen canvas
5274
+ /** Draw all the tile data to an offscreen canvas
4917
5275
  * - This may be slow in some browsers but only needs to be done once */
4918
5276
  redraw()
4919
5277
  {
@@ -4923,7 +5281,7 @@ class TileLayer extends CanvasLayer
4923
5281
  this.drawTileData(vec2(x,y), false);
4924
5282
  this.redrawEnd();
4925
5283
  if (this.glTexture)
4926
- this.useWebGL(); // update webgl texture
5284
+ this.useWebGL(); // update WebGL texture
4927
5285
  }
4928
5286
 
4929
5287
  /** Call to start the redraw process
@@ -4959,7 +5317,7 @@ class TileLayer extends CanvasLayer
4959
5317
  /** Call to end the redraw process */
4960
5318
  redrawEnd()
4961
5319
  {
4962
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5320
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
4963
5321
  glCopyToContext(drawContext);
4964
5322
  //debugSaveCanvas(this.canvas);
4965
5323
 
@@ -4970,7 +5328,7 @@ class TileLayer extends CanvasLayer
4970
5328
  /** Draw the tile at a given position in the tile grid
4971
5329
  * This can be used to clear out tiles when they are destroyed
4972
5330
  * Tiles can also be redrawn if inside a redrawStart/End block
4973
- * @param {Vector2} layerPos
5331
+ * @param {Vector2} layerPos
4974
5332
  * @param {boolean} [clear] - should the old tile be cleared out
4975
5333
  */
4976
5334
  drawTileData(layerPos, clear=true)
@@ -4985,9 +5343,9 @@ class TileLayer extends CanvasLayer
4985
5343
 
4986
5344
  // draw the tile if it has layer data
4987
5345
  const d = this.getData(layerPos);
4988
- if (d.tile != undefined)
5346
+ if (d.tile !== undefined)
4989
5347
  {
4990
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5348
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
4991
5349
  const pos = layerPos.add(vec2(.5));
4992
5350
  const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
4993
5351
  drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
@@ -5000,7 +5358,7 @@ class TileLayer extends CanvasLayer
5000
5358
  * Tile Collision Layer - a tile layer with collision
5001
5359
  * - adds collision data and functions to TileLayer
5002
5360
  * - there can be multiple tile collision layers
5003
- * - tile collison layers should not overlap each other
5361
+ * - tile collision layers should not overlap each other
5004
5362
  * @extends TileLayer
5005
5363
  */
5006
5364
  class TileCollisionLayer extends TileLayer
@@ -5151,7 +5509,7 @@ class TileCollisionLayer extends TileLayer
5151
5509
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
5152
5510
  }
5153
5511
  }
5154
- /**
5512
+ /**
5155
5513
  * LittleJS Particle System
5156
5514
  */
5157
5515
 
@@ -5168,7 +5526,7 @@ class TileCollisionLayer extends TileLayer
5168
5526
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
5169
5527
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
5170
5528
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5171
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5529
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5172
5530
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
5173
5531
  * );
5174
5532
  */
@@ -5176,35 +5534,35 @@ class ParticleEmitter extends EngineObject
5176
5534
  {
5177
5535
  /** Create a particle system with the given settings
5178
5536
  * @param {Vector2} position - World space position of the emitter
5179
- * @param {Number} [angle] - Angle to emit the particles
5180
- * @param {Number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
5181
- * @param {Number} [emitTime] - How long to stay alive (0 is forever)
5182
- * @param {Number} [emitRate] - How many particles per second to spawn, does not emit if 0
5183
- * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5537
+ * @param {number} [angle] - Angle to emit the particles
5538
+ * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
5539
+ * @param {number} [emitTime] - How long to stay alive (0 is forever)
5540
+ * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
5541
+ * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5184
5542
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
5185
5543
  * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
5186
5544
  * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
5187
5545
  * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
5188
5546
  * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
5189
- * @param {Number} [particleTime] - How long particles live
5190
- * @param {Number} [sizeStart] - How big are particles at start
5191
- * @param {Number} [sizeEnd] - How big are particles at end
5192
- * @param {Number} [speed] - How fast are particles when spawned
5193
- * @param {Number} [angleSpeed] - How fast are particles rotating
5194
- * @param {Number} [damping] - How much to dampen particle speed
5195
- * @param {Number} [angleDamping] - How much to dampen particle angular speed
5196
- * @param {Number} [gravityScale] - How much gravity effect particles
5197
- * @param {Number} [particleConeAngle] - Cone for start particle angle
5198
- * @param {Number} [fadeRate] - How quick to fade particles at start/end in percent of life
5199
- * @param {Number} [randomness] - Apply extra randomness percent
5547
+ * @param {number} [particleTime] - How long particles live
5548
+ * @param {number} [sizeStart] - How big are particles at start
5549
+ * @param {number} [sizeEnd] - How big are particles at end
5550
+ * @param {number} [speed] - How fast are particles when spawned
5551
+ * @param {number} [angleSpeed] - How fast are particles rotating
5552
+ * @param {number} [damping] - How much to dampen particle speed
5553
+ * @param {number} [angleDamping] - How much to dampen particle angular speed
5554
+ * @param {number} [gravityScale] - How much gravity effect particles
5555
+ * @param {number} [particleConeAngle] - Cone for start particle angle
5556
+ * @param {number} [fadeRate] - How quick to fade particles at start/end in percent of life
5557
+ * @param {number} [randomness] - Apply extra randomness percent
5200
5558
  * @param {boolean} [collideTiles] - Do particles collide against tiles
5201
5559
  * @param {boolean} [additive] - Should particles use additive blend
5202
5560
  * @param {boolean} [randomColorLinear] - Should color be randomized linearly or across each component
5203
- * @param {Number} [renderOrder] - Render order for particles (additive is above other stuff by default)
5561
+ * @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
5204
5562
  * @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
5205
5563
  */
5206
5564
  constructor
5207
- (
5565
+ (
5208
5566
  position,
5209
5567
  angle,
5210
5568
  emitSize = 0,
@@ -5226,7 +5584,7 @@ class ParticleEmitter extends EngineObject
5226
5584
  gravityScale = 0,
5227
5585
  particleConeAngle = PI,
5228
5586
  fadeRate = .1,
5229
- randomness = .2,
5587
+ randomness = .2,
5230
5588
  collideTiles = false,
5231
5589
  additive = false,
5232
5590
  randomColorLinear = true,
@@ -5237,13 +5595,13 @@ class ParticleEmitter extends EngineObject
5237
5595
  super(position, vec2(), tileInfo, angle, undefined, renderOrder);
5238
5596
 
5239
5597
  // emitter settings
5240
- /** @property {Number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5598
+ /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5241
5599
  this.emitSize = emitSize
5242
- /** @property {Number} - How long to stay alive (0 is forever) */
5600
+ /** @property {number} - How long to stay alive (0 is forever) */
5243
5601
  this.emitTime = emitTime;
5244
- /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
5602
+ /** @property {number} - How many particles per second to spawn, does not emit if 0 */
5245
5603
  this.emitRate = emitRate;
5246
- /** @property {Number} - Local angle to apply velocity to particles from emitter */
5604
+ /** @property {number} - Local angle to apply velocity to particles from emitter */
5247
5605
  this.emitConeAngle = emitConeAngle;
5248
5606
 
5249
5607
  // color settings
@@ -5259,27 +5617,27 @@ class ParticleEmitter extends EngineObject
5259
5617
  this.randomColorLinear = randomColorLinear;
5260
5618
 
5261
5619
  // particle settings
5262
- /** @property {Number} - How long particles live */
5620
+ /** @property {number} - How long particles live */
5263
5621
  this.particleTime = particleTime;
5264
- /** @property {Number} - How big are particles at start */
5622
+ /** @property {number} - How big are particles at start */
5265
5623
  this.sizeStart = sizeStart;
5266
- /** @property {Number} - How big are particles at end */
5624
+ /** @property {number} - How big are particles at end */
5267
5625
  this.sizeEnd = sizeEnd;
5268
- /** @property {Number} - How fast are particles when spawned */
5626
+ /** @property {number} - How fast are particles when spawned */
5269
5627
  this.speed = speed;
5270
- /** @property {Number} - How fast are particles rotating */
5628
+ /** @property {number} - How fast are particles rotating */
5271
5629
  this.angleSpeed = angleSpeed;
5272
- /** @property {Number} - How much to dampen particle speed */
5630
+ /** @property {number} - How much to dampen particle speed */
5273
5631
  this.damping = damping;
5274
- /** @property {Number} - How much to dampen particle angular speed */
5632
+ /** @property {number} - How much to dampen particle angular speed */
5275
5633
  this.angleDamping = angleDamping;
5276
- /** @property {Number} - How much does gravity effect particles */
5634
+ /** @property {number} - How much gravity affects particles */
5277
5635
  this.gravityScale = gravityScale;
5278
- /** @property {Number} - Cone for start particle angle */
5636
+ /** @property {number} - Cone for start particle angle */
5279
5637
  this.particleConeAngle = particleConeAngle;
5280
- /** @property {Number} - How quick to fade in particles at start/end in percent of life */
5638
+ /** @property {number} - How quick to fade in particles at start/end in percent of life */
5281
5639
  this.fadeRate = fadeRate;
5282
- /** @property {Number} - Apply extra randomness percent */
5640
+ /** @property {number} - Apply extra randomness percent */
5283
5641
  this.randomness = randomness;
5284
5642
  /** @property {boolean} - Do particles collide against tiles */
5285
5643
  this.collideTiles = collideTiles;
@@ -5287,16 +5645,16 @@ class ParticleEmitter extends EngineObject
5287
5645
  this.additive = additive;
5288
5646
  /** @property {boolean} - Should it be in local space of emitter */
5289
5647
  this.localSpace = localSpace;
5290
- /** @property {Number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5648
+ /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5291
5649
  this.trailScale = 0;
5292
5650
  /** @property {Function} - Callback when particle is destroyed */
5293
5651
  this.particleDestroyCallback = undefined;
5294
5652
  /** @property {Function} - Callback when particle is created */
5295
5653
  this.particleCreateCallback = undefined;
5296
- /** @property {Number} - Track particle emit time */
5654
+ /** @property {number} - Track particle emit time */
5297
5655
  this.emitTimeBuffer = 0;
5298
5656
  }
5299
-
5657
+
5300
5658
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
5301
5659
  update()
5302
5660
  {
@@ -5320,7 +5678,7 @@ class ParticleEmitter extends EngineObject
5320
5678
  if (debugParticles)
5321
5679
  {
5322
5680
  // show emitter bounds
5323
- const emitSize = typeof this.emitSize == 'number' ? vec2(this.emitSize) : this.emitSize;
5681
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
5324
5682
  debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
5325
5683
  }
5326
5684
  }
@@ -5330,7 +5688,7 @@ class ParticleEmitter extends EngineObject
5330
5688
  emitParticle()
5331
5689
  {
5332
5690
  // spawn a particle
5333
- let pos = typeof this.emitSize == 'number' ? // check if number was used
5691
+ let pos = typeof this.emitSize === 'number' ? // check if number was used
5334
5692
  randInCircle(this.emitSize/2) // circle emitter
5335
5693
  : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
5336
5694
  .multiply(this.emitSize).rotate(this.angle)
@@ -5355,7 +5713,7 @@ class ParticleEmitter extends EngineObject
5355
5713
  const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
5356
5714
  const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
5357
5715
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
5358
-
5716
+
5359
5717
  // build particle
5360
5718
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
5361
5719
  particle.velocity = vec2().setAngle(velocityAngle, speed);
@@ -5400,38 +5758,38 @@ class Particle extends EngineObject
5400
5758
  * Typically this is created automatically by a ParticleEmitter
5401
5759
  * @param {Vector2} position - World space position of the particle
5402
5760
  * @param {TileInfo} tileInfo - Tile info to render particles
5403
- * @param {Number} angle - Angle to rotate the particle
5761
+ * @param {number} angle - Angle to rotate the particle
5404
5762
  * @param {Color} colorStart - Color at start of life
5405
5763
  * @param {Color} colorEnd - Color at end of life
5406
- * @param {Number} lifeTime - How long to live for
5407
- * @param {Number} sizeStart - Size at start of life
5408
- * @param {Number} sizeEnd - Size at end of life
5409
- * @param {Number} fadeRate - How quick to fade in/out
5764
+ * @param {number} lifeTime - How long to live for
5765
+ * @param {number} sizeStart - Size at start of life
5766
+ * @param {number} sizeEnd - Size at end of life
5767
+ * @param {number} fadeRate - How quick to fade in/out
5410
5768
  * @param {boolean} additive - Does it use additive blend mode
5411
- * @param {Number} trailScale - If a trail, how long to make it
5769
+ * @param {number} trailScale - If a trail, how long to make it
5412
5770
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
5413
5771
  * @param {Function} [destroyCallback] - Callback when particle dies
5414
5772
  */
5415
5773
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
5416
5774
  )
5417
- {
5418
- super(position, vec2(), tileInfo, angle);
5419
-
5775
+ {
5776
+ super(position, vec2(), tileInfo, angle);
5777
+
5420
5778
  /** @property {Color} - Color at start of life */
5421
5779
  this.colorStart = colorStart;
5422
5780
  /** @property {Color} - Calculated change in color */
5423
5781
  this.colorEndDelta = colorEnd.subtract(colorStart);
5424
- /** @property {Number} - How long to live for */
5782
+ /** @property {number} - How long to live for */
5425
5783
  this.lifeTime = lifeTime;
5426
- /** @property {Number} - Size at start of life */
5784
+ /** @property {number} - Size at start of life */
5427
5785
  this.sizeStart = sizeStart;
5428
- /** @property {Number} - Calculated change in size */
5786
+ /** @property {number} - Calculated change in size */
5429
5787
  this.sizeEndDelta = sizeEnd - sizeStart;
5430
- /** @property {Number} - How quick to fade in/out */
5788
+ /** @property {number} - How quick to fade in/out */
5431
5789
  this.fadeRate = fadeRate;
5432
5790
  /** @property {boolean} - Is it additive */
5433
5791
  this.additive = additive;
5434
- /** @property {Number} - If a trail, how long to make it */
5792
+ /** @property {number} - If a trail, how long to make it */
5435
5793
  this.trailScale = trailScale;
5436
5794
  /** @property {ParticleEmitter} - Parent emitter if local space */
5437
5795
  this.localSpaceEmitter = localSpaceEmitter;
@@ -5471,7 +5829,7 @@ class Particle extends EngineObject
5471
5829
  this.colorStart.r + p * this.colorEndDelta.r,
5472
5830
  this.colorStart.g + p * this.colorEndDelta.g,
5473
5831
  this.colorStart.b + p * this.colorEndDelta.b,
5474
- (this.colorStart.a + p * this.colorEndDelta.a) *
5832
+ (this.colorStart.a + p * this.colorEndDelta.a) *
5475
5833
  (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
5476
5834
 
5477
5835
  // draw the particle
@@ -5481,7 +5839,7 @@ class Particle extends EngineObject
5481
5839
  if (this.localSpaceEmitter)
5482
5840
  {
5483
5841
  // in local space of emitter
5484
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
5842
+ pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
5485
5843
  angle += this.localSpaceEmitter.angle;
5486
5844
  }
5487
5845
  if (this.trailScale)
@@ -5505,7 +5863,7 @@ class Particle extends EngineObject
5505
5863
  this.additive && setBlendMode();
5506
5864
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
5507
5865
 
5508
- if (p == 1)
5866
+ if (p === 1)
5509
5867
  {
5510
5868
  // destroy particle when it's time runs out
5511
5869
  this.color = color;
@@ -5515,7 +5873,7 @@ class Particle extends EngineObject
5515
5873
  }
5516
5874
  }
5517
5875
  }
5518
- /**
5876
+ /**
5519
5877
  * LittleJS Medal System
5520
5878
  * - Tracks and displays medals
5521
5879
  * - Saves medals to local storage
@@ -5536,7 +5894,7 @@ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
5536
5894
  /** Initialize medals with a save name used for storage
5537
5895
  * - Call this after creating all medals
5538
5896
  * - Checks if medals are unlocked
5539
- * @param {String} saveName
5897
+ * @param {string} saveName
5540
5898
  * @memberof Medals */
5541
5899
  function medalsInit(saveName)
5542
5900
  {
@@ -5551,7 +5909,7 @@ function medalsInit(saveName)
5551
5909
  {
5552
5910
  if (!medalsDisplayQueue.length)
5553
5911
  return;
5554
-
5912
+
5555
5913
  // update first medal in queue
5556
5914
  const medal = medalsDisplayQueue[0];
5557
5915
  const time = timeReal - medalsDisplayTimeLast;
@@ -5566,7 +5924,7 @@ function medalsInit(saveName)
5566
5924
  {
5567
5925
  // slide on/off medals
5568
5926
  const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
5569
- const hidePercent =
5927
+ const hidePercent =
5570
5928
  time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
5571
5929
  time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
5572
5930
  medal.render(hidePercent);
@@ -5582,43 +5940,43 @@ function medalsForEach(callback)
5582
5940
 
5583
5941
  ///////////////////////////////////////////////////////////////////////////////
5584
5942
 
5585
- /**
5586
- * Medal - Tracks an unlockable medal
5943
+ /**
5944
+ * Medal - Tracks an unlockable medal
5587
5945
  * @example
5588
5946
  * // create a medal
5589
5947
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
5590
- *
5948
+ *
5591
5949
  * // initialize medals
5592
5950
  * medalsInit('Example Game');
5593
- *
5951
+ *
5594
5952
  * // unlock the medal
5595
5953
  * medal_example.unlock();
5596
5954
  */
5597
5955
  class Medal
5598
5956
  {
5599
5957
  /** Create a medal object and adds it to the list of medals
5600
- * @param {Number} id - The unique identifier of the medal
5601
- * @param {String} name - Name of the medal
5602
- * @param {String} [description] - Description of the medal
5603
- * @param {String} [icon] - Icon for the medal
5604
- * @param {String} [src] - Image location for the medal
5958
+ * @param {number} id - The unique identifier of the medal
5959
+ * @param {string} name - Name of the medal
5960
+ * @param {string} [description] - Description of the medal
5961
+ * @param {string} [icon] - Icon for the medal
5962
+ * @param {string} [src] - Image location for the medal
5605
5963
  */
5606
5964
  constructor(id, name, description='', icon='🏆', src)
5607
5965
  {
5608
5966
  ASSERT(id >= 0 && !medals[id]);
5609
-
5610
- /** @property {Number} - The unique identifier of the medal */
5967
+
5968
+ /** @property {number} - The unique identifier of the medal */
5611
5969
  this.id = id;
5612
-
5613
- /** @property {String} - Name of the medal */
5970
+
5971
+ /** @property {string} - Name of the medal */
5614
5972
  this.name = name;
5615
-
5616
- /** @property {String} - Description of the medal */
5973
+
5974
+ /** @property {string} - Description of the medal */
5617
5975
  this.description = description;
5618
-
5619
- /** @property {String} - Icon for the medal */
5976
+
5977
+ /** @property {string} - Icon for the medal */
5620
5978
  this.icon = icon;
5621
-
5979
+
5622
5980
  /** @property {boolean} - Is the medal unlocked? */
5623
5981
  this.unlocked = false;
5624
5982
 
@@ -5643,7 +6001,7 @@ class Medal
5643
6001
  }
5644
6002
 
5645
6003
  /** Render a medal
5646
- * @param {Number} [hidePercent] - How much to slide the medal off screen
6004
+ * @param {number} [hidePercent] - How much to slide the medal off screen
5647
6005
  */
5648
6006
  render(hidePercent=0)
5649
6007
  {
@@ -5684,7 +6042,7 @@ class Medal
5684
6042
 
5685
6043
  /** Render the icon for a medal
5686
6044
  * @param {Vector2} pos - Screen space position
5687
- * @param {Number} size - Screen space size
6045
+ * @param {number} size - Screen space size
5688
6046
  */
5689
6047
  renderIcon(pos, size)
5690
6048
  {
@@ -5694,14 +6052,14 @@ class Medal
5694
6052
  else
5695
6053
  drawTextScreen(this.icon, pos, size*.7, BLACK);
5696
6054
  }
5697
-
6055
+
5698
6056
  // Get local storage key used by the medal
5699
6057
  storageKey() { return medalsSaveName + '_' + this.id; }
5700
6058
  }
5701
6059
  /**
5702
6060
  * LittleJS WebGL Interface
5703
- * - All webgl used by the engine is wrapped up here
5704
- * - Will fall back to 2D canvas rendering if webgl is not supported
6061
+ * - All WebGL used by the engine is wrapped up here
6062
+ * - Will fall back to 2D canvas rendering if WebGL is not supported
5705
6063
  * - For normal stuff you won't need to see or call anything in this file
5706
6064
  * - For advanced stuff there are helper functions to create shaders, textures, etc
5707
6065
  * - Can be disabled with glEnable to revert to 2D canvas rendering
@@ -5716,24 +6074,27 @@ class Medal
5716
6074
  * @memberof WebGL */
5717
6075
  let glCanvas;
5718
6076
 
5719
- /** 2d context for glCanvas
6077
+ /** WebGL2 context for `glCanvas`
5720
6078
  * @type {WebGL2RenderingContext}
5721
6079
  * @memberof WebGL */
5722
6080
  let glContext;
5723
6081
 
5724
- /** Should webgl be setup with anti-aliasing? must be set before calling engineInit
6082
+ /** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
5725
6083
  * @type {boolean}
5726
6084
  * @memberof WebGL */
5727
6085
  let glAntialias = true;
5728
6086
 
5729
6087
  // WebGL internal variables not exposed to documentation
5730
- let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive;
6088
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
5731
6089
 
5732
- // WebGL internal constants
5733
- const gl_MAX_INSTANCES = 1e4;
6090
+ // WebGL internal constants
6091
+ const gl_ARRAY_BUFFER_SIZE = 5e5;
5734
6092
  const gl_INDICES_PER_INSTANCE = 11;
5735
6093
  const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
5736
- const gl_INSTANCE_BUFFER_SIZE = gl_MAX_INSTANCES * gl_INSTANCE_BYTE_STRIDE;
6094
+ const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
6095
+ const gl_INDICES_PER_POLY_VERTEX = 3;
6096
+ const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
6097
+ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
5737
6098
 
5738
6099
  ///////////////////////////////////////////////////////////////////////////////
5739
6100
 
@@ -5754,11 +6115,11 @@ function glInit()
5754
6115
  return;
5755
6116
  }
5756
6117
 
5757
- // create the webgl canvas
6118
+ // create the WebGL canvas
5758
6119
  const rootElement = mainCanvas.parentElement;
5759
6120
  rootElement.appendChild(glCanvas);
5760
6121
 
5761
- // setup vertex and fragment shaders
6122
+ // setup instanced rendering shader program
5762
6123
  glShader = glCreateProgram(
5763
6124
  '#version 300 es\n' + // specify GLSL ES version
5764
6125
  'precision highp float;'+ // use highp for better accuracy
@@ -5786,40 +6147,59 @@ function glInit()
5786
6147
  '}' // end of shader
5787
6148
  );
5788
6149
 
6150
+ // setup poly rendering shaders
6151
+ glPolyShader = glCreateProgram(
6152
+ '#version 300 es\n' + // specify GLSL ES version
6153
+ 'precision highp float;'+ // use highp for better accuracy
6154
+ 'uniform mat4 m;'+ // transform matrix
6155
+ 'in vec2 p;'+ // in: position
6156
+ 'in vec4 c;'+ // in: color
6157
+ 'out vec4 d;'+ // out: color
6158
+ 'void main(){'+ // shader entry point
6159
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
6160
+ 'd=c;'+ // pass color to fragment shader
6161
+ '}' // end of shader
6162
+ ,
6163
+ '#version 300 es\n' + // specify GLSL ES version
6164
+ 'precision highp float;'+ // use highp for better accuracy
6165
+ 'in vec4 d;'+ // in: color
6166
+ 'out vec4 c;'+ // out: color
6167
+ 'void main(){'+ // shader entry point
6168
+ 'c=d;'+ // set color
6169
+ '}' // end of shader
6170
+ );
6171
+
5789
6172
  // init buffers
5790
- const glInstanceData = new ArrayBuffer(gl_INSTANCE_BUFFER_SIZE);
6173
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
5791
6174
  glPositionData = new Float32Array(glInstanceData);
5792
6175
  glColorData = new Uint32Array(glInstanceData);
5793
6176
  glArrayBuffer = glContext.createBuffer();
5794
6177
  glGeometryBuffer = glContext.createBuffer();
5795
6178
 
5796
6179
  // create the geometry buffer, triangle strip square
5797
- const geometry = new Float32Array([glInstanceCount=0,0,1,0,0,1,1,1]);
6180
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
5798
6181
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
5799
6182
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
5800
6183
  }
5801
6184
 
5802
- // Setup webgl render each frame, called automatically by engine
5803
- // Also used by tile layer rendering when redrawing tiles
5804
- function glPreRender()
6185
+ function glSetInstancedMode()
5805
6186
  {
5806
- if (!glEnable || !glContext) return;
5807
-
5808
- // set up the shader and canvas
5809
- glClearCanvas();
6187
+ if (!glPolyMode)
6188
+ return;
6189
+
6190
+ // setup instanced mode
6191
+ glFlush();
6192
+ glPolyMode = false;
5810
6193
  glContext.useProgram(glShader);
5811
- glContext.activeTexture(glContext.TEXTURE0);
5812
- if (textureInfos[0])
5813
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
5814
6194
 
5815
6195
  // set vertex attributes
5816
- let offset = glAdditive = glBatchAdditive = 0;
6196
+ let offset = 0;
5817
6197
  const initVertexAttribArray = (name, type, typeSize, size)=>
5818
6198
  {
5819
6199
  const location = glContext.getAttribLocation(glShader, name);
5820
6200
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
5821
6201
  const divisor = typeSize && 1; // only if not geometry
5822
- const normalize = typeSize == 1; // only if color
6202
+ const normalize = typeSize === 1; // only if color
5823
6203
  glContext.enableVertexAttribArray(location);
5824
6204
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
5825
6205
  glContext.vertexAttribDivisor(location, divisor);
@@ -5828,26 +6208,87 @@ function glPreRender()
5828
6208
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
5829
6209
  initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
5830
6210
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
5831
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_INSTANCE_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6211
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
5832
6212
  initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
5833
6213
  initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
5834
6214
  initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
5835
6215
  initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
5836
6216
  initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
6217
+ }
6218
+
6219
+ function glSetPolyMode()
6220
+ {
6221
+ if (glPolyMode)
6222
+ return;
5837
6223
 
6224
+ // setup poly mode
6225
+ glFlush();
6226
+ glPolyMode = true;
6227
+ glContext.useProgram(glPolyShader);
6228
+
6229
+ // set vertex attributes
6230
+ let offset = 0;
6231
+ const initVertexAttribArray = (name, type, typeSize, size)=>
6232
+ {
6233
+ const location = glContext.getAttribLocation(glPolyShader, name);
6234
+ const normalize = typeSize === 1; // only normalize if color
6235
+ const stride = gl_POLY_VERTEX_BYTE_STRIDE;
6236
+ glContext.enableVertexAttribArray(location);
6237
+ glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
6238
+ glContext.vertexAttribDivisor(location, 0);
6239
+ offset += size*typeSize;
6240
+ }
6241
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
6242
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6243
+ initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
6244
+ initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
6245
+ }
6246
+
6247
+ // Setup WebGL render each frame, called automatically by engine
6248
+ // Also used by tile layer rendering when redrawing tiles
6249
+ function glPreRender()
6250
+ {
6251
+ if (!glEnable || !glContext) return;
6252
+
6253
+ // clear the canvas
6254
+ glClearCanvas();
6255
+
5838
6256
  // build the transform matrix
5839
6257
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
5840
6258
  const rotatedCam = cameraPos.rotate(-cameraAngle);
5841
6259
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
5842
6260
  const ca = Math.cos(cameraAngle);
5843
6261
  const sa = Math.sin(cameraAngle);
5844
- glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
5845
- [
6262
+ const transform = [
5846
6263
  s.x * ca, s.y * sa, 0, 0,
5847
6264
  -s.x * sa, s.y * ca, 0, 0,
5848
6265
  1, 1, 1, 0,
5849
- p.x, p.y, 0, 1
5850
- ]);
6266
+ p.x, p.y, 0, 1];
6267
+
6268
+ // set the same transform matrix for both shaders
6269
+ const initUniform = (program, uniform, value) =>
6270
+ {
6271
+ glContext.useProgram(program);
6272
+ const location = glContext.getUniformLocation(program, uniform);
6273
+ glContext.uniformMatrix4fv(location, false, value);
6274
+ }
6275
+ initUniform(glPolyShader, 'm', transform);
6276
+ initUniform(glShader, 'm', transform);
6277
+
6278
+ // set the active texture
6279
+ glContext.activeTexture(glContext.TEXTURE0);
6280
+ if (textureInfos[0])
6281
+ {
6282
+ glActiveTexture = textureInfos[0].glTexture;
6283
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
6284
+ }
6285
+
6286
+ // start with additive blending off
6287
+ glAdditive = glBatchAdditive = false;
6288
+
6289
+ // force it to enter instanced mode
6290
+ glPolyMode = true;
6291
+ glSetInstancedMode();
5851
6292
  }
5852
6293
 
5853
6294
  /** Clear the canvas and setup the viewport
@@ -5855,21 +6296,23 @@ function glPreRender()
5855
6296
  function glClearCanvas()
5856
6297
  {
5857
6298
  if (!glContext) return;
5858
-
6299
+
5859
6300
  // clear and set to same size as main canvas
5860
- glContext.viewport(0, 0, glCanvas.width=drawCanvas.width, glCanvas.height=drawCanvas.height);
6301
+ glCanvas.width = drawCanvas.width;
6302
+ glCanvas.height = drawCanvas.height;
6303
+ glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
5861
6304
  glContext.clear(glContext.COLOR_BUFFER_BIT);
5862
6305
  }
5863
6306
 
5864
- /** Set the WebGl texture, called automatically if using multiple textures
6307
+ /** Set the WebGL texture, called automatically if using multiple textures
5865
6308
  * - This may also flush the gl buffer resulting in more draw calls and worse performance
5866
6309
  * @param {WebGLTexture} texture
5867
- * @param {boolean} wrap - Should the texture wrap or clamp
6310
+ * @param {boolean} [wrap] - Should the texture wrap or clamp
5868
6311
  * @memberof WebGL */
5869
6312
  function glSetTexture(texture, wrap=false)
5870
6313
  {
5871
6314
  // must flush cache with the old texture to set a new one
5872
- if (!glContext || texture == glActiveTexture)
6315
+ if (!glContext || texture === glActiveTexture)
5873
6316
  return;
5874
6317
 
5875
6318
  glFlush();
@@ -5882,8 +6325,8 @@ function glSetTexture(texture, wrap=false)
5882
6325
  }
5883
6326
 
5884
6327
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
5885
- * @param {String} source
5886
- * @param {Number} type
6328
+ * @param {string} source
6329
+ * @param {number} type
5887
6330
  * @return {WebGLShader}
5888
6331
  * @memberof WebGL */
5889
6332
  function glCompileShader(source, type)
@@ -5902,8 +6345,8 @@ function glCompileShader(source, type)
5902
6345
  }
5903
6346
 
5904
6347
  /** Create WebGL program with given shaders
5905
- * @param {String} vsSource
5906
- * @param {String} fsSource
6348
+ * @param {string} vsSource
6349
+ * @param {string} fsSource
5907
6350
  * @return {WebGLProgram}
5908
6351
  * @memberof WebGL */
5909
6352
  function glCreateProgram(vsSource, fsSource)
@@ -5951,7 +6394,7 @@ function glCreateTexture(image)
5951
6394
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
5952
6395
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
5953
6396
  }
5954
-
6397
+
5955
6398
  // set texture filtering
5956
6399
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
5957
6400
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -5959,7 +6402,6 @@ function glCreateTexture(image)
5959
6402
  return texture;
5960
6403
  }
5961
6404
 
5962
-
5963
6405
  /** Deletes a WebGL texture
5964
6406
  * @param {WebGLTexture} [texture]
5965
6407
  * @memberof WebGL */
@@ -5987,18 +6429,25 @@ function glSetTextureData(texture, image)
5987
6429
  * @memberof WebGL */
5988
6430
  function glFlush()
5989
6431
  {
5990
- if (!glEnable || !glContext || !glInstanceCount) return;
5991
-
5992
- const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
5993
- glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
5994
- glContext.enable(glContext.BLEND);
5995
-
5996
- // draw all the sprites in the batch and reset the buffer
5997
- glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData);
5998
- glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glInstanceCount);
5999
- if (debug || showWatermark)
6000
- drawCount += glInstanceCount;
6001
- glInstanceCount = 0;
6432
+ if (glEnable && glContext && glBatchCount)
6433
+ {
6434
+ // set bend mode
6435
+ const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
6436
+ glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
6437
+ glContext.enable(glContext.BLEND);
6438
+
6439
+ const byteLength = glBatchCount *
6440
+ (glPolyMode ? gl_INDICES_PER_POLY_VERTEX : gl_INDICES_PER_INSTANCE);
6441
+ glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData, 0, byteLength);
6442
+
6443
+ // draw the batch
6444
+ if (glPolyMode)
6445
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
6446
+ else
6447
+ glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
6448
+ drawCount += glBatchCount;
6449
+ glBatchCount = 0;
6450
+ }
6002
6451
  glBatchAdditive = glAdditive;
6003
6452
  }
6004
6453
 
@@ -6014,7 +6463,8 @@ function glCopyToContext(context)
6014
6463
  context.drawImage(glCanvas, 0, 0);
6015
6464
  }
6016
6465
 
6017
- /** Set anti-aliasing for webgl canvas
6466
+ /** Set anti-aliasing for WebGL canvas
6467
+ * Must be called before engineInit
6018
6468
  * @param {boolean} [antialias]
6019
6469
  * @memberof WebGL */
6020
6470
  function glSetAntialias(antialias=true)
@@ -6024,25 +6474,27 @@ function glSetAntialias(antialias=true)
6024
6474
  }
6025
6475
 
6026
6476
  /** Add a sprite to the gl draw list, used by all gl draw functions
6027
- * @param {Number} x
6028
- * @param {Number} y
6029
- * @param {Number} sizeX
6030
- * @param {Number} sizeY
6031
- * @param {Number} [angle]
6032
- * @param {Number} [uv0X]
6033
- * @param {Number} [uv0Y]
6034
- * @param {Number} [uv1X]
6035
- * @param {Number} [uv1Y]
6036
- * @param {Number} [rgba=-1] - white is -1
6037
- * @param {Number} [rgbaAdditive=0] - black is 0
6477
+ * @param {number} x
6478
+ * @param {number} y
6479
+ * @param {number} sizeX
6480
+ * @param {number} sizeY
6481
+ * @param {number} [angle]
6482
+ * @param {number} [uv0X]
6483
+ * @param {number} [uv0Y]
6484
+ * @param {number} [uv1X]
6485
+ * @param {number} [uv1Y]
6486
+ * @param {number} [rgba=-1] - white is -1
6487
+ * @param {number} [rgbaAdditive=0] - black is 0
6038
6488
  * @memberof WebGL */
6039
6489
  function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
6040
6490
  {
6041
6491
  // flush if there is not enough room or if different blend mode
6042
- if (glInstanceCount >= gl_MAX_INSTANCES || glBatchAdditive != glAdditive)
6492
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
6043
6493
  glFlush();
6494
+ glSetInstancedMode();
6044
6495
 
6045
- let offset = glInstanceCount++ * gl_INDICES_PER_INSTANCE;
6496
+ glPolyMode = false;
6497
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
6046
6498
  glPositionData[offset++] = x;
6047
6499
  glPositionData[offset++] = y;
6048
6500
  glPositionData[offset++] = sizeX;
@@ -6054,6 +6506,312 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
6054
6506
  glColorData[offset++] = rgba;
6055
6507
  glColorData[offset++] = rgbaAdditive;
6056
6508
  glPositionData[offset++] = angle;
6509
+ }
6510
+
6511
+ /** Transform and add a polygon to the gl draw list
6512
+ * @param {Array<Vector2>} points - Array of Vector2 points
6513
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6514
+ * @param {number} x
6515
+ * @param {number} y
6516
+ * @param {number} sx
6517
+ * @param {number} sy
6518
+ * @param {number} angle
6519
+ * @param {boolean} [tristrip] - should tristrip algorithm be used
6520
+ * @memberof WebGL */
6521
+ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
6522
+ {
6523
+ const pointsOut = [];
6524
+ for (const p of points)
6525
+ {
6526
+ // transform the point
6527
+ const px = p.x*sx;
6528
+ const py = p.y*sy;
6529
+ const sa = Math.sin(-angle);
6530
+ const ca = Math.cos(-angle);
6531
+ pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
6532
+ }
6533
+ const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
6534
+ glDrawPoints(drawPoints, rgba);
6535
+ }
6536
+
6537
+ /** Transform and add a polygon to the gl draw list
6538
+ * @param {Array<Vector2>} points - Array of Vector2 points
6539
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6540
+ * @param {number} lineWidth - Width of the outline
6541
+ * @param {number} x
6542
+ * @param {number} y
6543
+ * @param {number} sx
6544
+ * @param {number} sy
6545
+ * @param {number} angle
6546
+ * @param {boolean} [wrap] - Should the outline connect the first and last points
6547
+ * @memberof WebGL */
6548
+ function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle, wrap=true)
6549
+ {
6550
+ const outlinePoints = glMakeOutline(points, lineWidth, wrap);
6551
+ glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
6552
+ }
6553
+
6554
+ /** Add a list of points to the gl draw list
6555
+ * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
6556
+ * @param {number} rgba - Color as a 32-bit integer
6557
+ * @memberof WebGL */
6558
+ function glDrawPoints(points, rgba)
6559
+ {
6560
+ if (!glEnable || points.length < 3)
6561
+ return; // needs at least 3 points to have area
6562
+
6563
+ // flush if there is not enough room or if different blend mode
6564
+ const vertCount = points.length + 2;
6565
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
6566
+ glFlush();
6567
+ glSetPolyMode();
6568
+
6569
+ // setup triangle strip with degenerate verts at start and end
6570
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
6571
+ for(let i = vertCount; i--;)
6572
+ {
6573
+ const j = clamp(i-1, 0, vertCount-3);
6574
+ const point = points[j];
6575
+ glPositionData[offset++] = point.x;
6576
+ glPositionData[offset++] = point.y;
6577
+ glColorData[offset++] = rgba;
6578
+ }
6579
+ glBatchCount += vertCount;
6580
+ }
6581
+
6582
+ /** Add a list of colored points to the gl draw list
6583
+ * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
6584
+ * @param {Array<number>} pointColors - Array of 32-bit integer colors
6585
+ * @memberof WebGL */
6586
+ function glDrawColoredPoints(points, pointColors)
6587
+ {
6588
+ if (!glEnable || points.length < 3)
6589
+ return; // needs at least 3 points to have area
6590
+
6591
+ // flush if there is not enough room or if different blend mode
6592
+ const vertCount = points.length + 2;
6593
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
6594
+ glFlush();
6595
+ glSetPolyMode();
6596
+
6597
+ // setup triangle strip with degenerate verts at start and end
6598
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
6599
+ for(let i = vertCount; i--;)
6600
+ {
6601
+ const j = clamp(i-1, 0, vertCount-3);
6602
+ const point = points[j];
6603
+ const color = pointColors[j];
6604
+ glPositionData[offset++] = point.x;
6605
+ glPositionData[offset++] = point.y;
6606
+ glColorData[offset++] = color;
6607
+ }
6608
+ glBatchCount += vertCount;
6609
+ }
6610
+
6611
+ // WebGL internal function to convert polygon to outline triangle strip
6612
+ function glMakeOutline(points, width, wrap=true)
6613
+ {
6614
+ if (points.length < 2)
6615
+ return [];
6616
+
6617
+ const halfWidth = width / 2;
6618
+ const strip = [];
6619
+ const n = points.length;
6620
+ const e = 1e-6;
6621
+ const miterLimit = width*100;
6622
+ for (let i = 0; i < n; i++)
6623
+ {
6624
+ // for each vertex, calculate normal based on adjacent edges
6625
+ const prev = points[wrap ? (i - 1 + n) % n : max(i - 1, 0)];
6626
+ const curr = points[i];
6627
+ const next = points[wrap ? (i + 1) % n : min(i + 1, n - 1)];
6628
+
6629
+ // direction from previous to current
6630
+ const dx1 = curr.x - prev.x;
6631
+ const dy1 = curr.y - prev.y;
6632
+ const len1 = (dx1*dx1 + dy1*dy1)**.5;
6633
+
6634
+ // direction from current to next
6635
+ const dx2 = next.x - curr.x;
6636
+ const dy2 = next.y - curr.y;
6637
+ const len2 = (dx2*dx2 + dy2*dy2)**.5;
6638
+
6639
+ if (len1 < e && len2 < e)
6640
+ continue; // skip degenerate point
6641
+
6642
+ // calculate perpendicular normals for each edge
6643
+ const nx1 = len1 > e ? -dy1 / len1 : 0;
6644
+ const ny1 = len1 > e ? dx1 / len1 : 0;
6645
+ const nx2 = len2 > e ? -dy2 / len2 : 0;
6646
+ const ny2 = len2 > e ? dx2 / len2 : 0;
6647
+
6648
+ // average the normals for miter
6649
+ let nx = nx1 + nx2;
6650
+ let ny = ny1 + ny2;
6651
+ const nlen = (nx*nx + ny*ny)**.5;
6652
+ if (nlen < e)
6653
+ {
6654
+ // 180 degree turn - use perpendicular
6655
+ nx = nx1;
6656
+ ny = ny1;
6657
+ }
6658
+ else
6659
+ {
6660
+ // calculate miter length
6661
+ nx /= nlen;
6662
+ ny /= nlen;
6663
+ const dot = nx1 * nx + ny1 * ny;
6664
+ if (dot > e)
6665
+ {
6666
+ // scale normal by miter length, clamped to miterLimit
6667
+ const miterLength = min(1 / dot, miterLimit);
6668
+ nx *= miterLength;
6669
+ ny *= miterLength;
6670
+ }
6671
+ }
6672
+
6673
+ // create inner and outer points along the normal
6674
+ const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
6675
+ const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
6676
+ strip.push(inner);
6677
+ strip.push(outer);
6678
+ }
6679
+ if (strip.length > 1 && wrap)
6680
+ {
6681
+ // close the loop
6682
+ strip.push(strip[0]);
6683
+ strip.push(strip[1]);
6684
+ }
6685
+ return strip;
6686
+ }
6687
+
6688
+ // WebGL internal function to convert polys to tri strips
6689
+ function glPolyStrip(points)
6690
+ {
6691
+ // validate input
6692
+ if (points.length < 3)
6693
+ return [];
6694
+
6695
+ // cross product helper: (b-a) x (c-a)
6696
+ const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
6697
+
6698
+ // calculate signed area of polygon
6699
+ const signedArea = (poly)=>
6700
+ {
6701
+ let area = 0;
6702
+ for (let i = poly.length; i--;)
6703
+ {
6704
+ const j = (i+1) % poly.length;
6705
+ area += poly[i].cross(poly[j]);
6706
+ }
6707
+ return area;
6708
+ }
6709
+
6710
+ // ensure counter-clockwise winding
6711
+ if (signedArea(points) < 0)
6712
+ points = points.reverse();
6713
+
6714
+ // check if point is inside triangle
6715
+ const e = 1e-9;
6716
+ const pointInTriangle = (p, a, b, c)=>
6717
+ {
6718
+ const c1 = cross(a, b, p);
6719
+ const c2 = cross(b, c, p);
6720
+ const c3 = cross(c, a, p);
6721
+ const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
6722
+ const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
6723
+ return !(negative && positive);
6724
+ };
6725
+
6726
+ // ear clipping triangulation
6727
+ const indices = [];
6728
+ for (let i = 0; i < points.length; ++i)
6729
+ indices[i] = i;
6730
+ const triangles = [];
6731
+ let attempts = 0;
6732
+ const maxAttempts = points.length ** 2 + 100;
6733
+ while (indices.length > 3 && attempts++ < maxAttempts)
6734
+ {
6735
+ let foundEar = false;
6736
+ for (let i = indices.length; --i;)
6737
+ {
6738
+ const i0 = indices[(i + indices.length - 1) % indices.length];
6739
+ const i1 = indices[i];
6740
+ const i2 = indices[(i + 1) % indices.length];
6741
+ const a = points[i0], b = points[i1], c = points[i2];
6742
+
6743
+ // check if convex
6744
+ if (cross(a, b, c) < e)
6745
+ continue;
6746
+
6747
+ // check if any other point is inside
6748
+ let hasInside = false;
6749
+ for (let j = 0; j < indices.length; j++)
6750
+ {
6751
+ const k = indices[j];
6752
+ if (k === i0 || k === i1 || k === i2)
6753
+ continue;
6754
+ const p = points[k];
6755
+ hasInside = pointInTriangle(p, a, b, c);
6756
+ if (hasInside)
6757
+ break;
6758
+ }
6759
+ if (hasInside)
6760
+ continue;
6761
+
6762
+ // found valid ear
6763
+ triangles.push([i0, i1, i2]);
6764
+ indices.splice(i, 1);
6765
+ foundEar = true;
6766
+ break;
6767
+ }
6768
+
6769
+ // fallback for degenerate cases
6770
+ if (!foundEar)
6771
+ {
6772
+ let worstIndex = -1, worstValue = Infinity;
6773
+ for (let i = indices.length; --i;)
6774
+ {
6775
+ const i0 = indices[(i + indices.length - 1) % indices.length];
6776
+ const i1 = indices[i];
6777
+ const i2 = indices[(i + 1) % indices.length];
6778
+ const value = abs(cross(points[i0], points[i1], points[i2]));
6779
+ if (value < worstValue)
6780
+ {
6781
+ worstValue = value;
6782
+ worstIndex = i;
6783
+ }
6784
+ }
6785
+ if (worstIndex < 0)
6786
+ break;
6787
+
6788
+ const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
6789
+ const i1 = indices[worstIndex];
6790
+ const i2 = indices[(worstIndex + 1) % indices.length];
6791
+ triangles.push([i0, i1, i2]);
6792
+ indices.splice(worstIndex, 1);
6793
+ }
6794
+ }
6795
+
6796
+ // add final triangle
6797
+ if (indices.length === 3)
6798
+ triangles.push([indices[0], indices[1], indices[2]]);
6799
+ if (!triangles.length)
6800
+ return [];
6801
+
6802
+ // convert triangles to triangle strip with degenerate connectors
6803
+ const strip = [];
6804
+ let [a0, b0, c0] = triangles[0];
6805
+ strip.push(points[a0], points[b0], points[c0]);
6806
+ for (let i = 1; i < triangles.length; i++)
6807
+ {
6808
+ // add degenerate bridge from last vertex to first of new triangle
6809
+ const [a, b, c] = triangles[i];
6810
+ strip.push(points[c0], points[a]);
6811
+ strip.push(points[a], points[b], points[c]);
6812
+ c0 = c;
6813
+ }
6814
+ return strip;
6057
6815
  }
6058
6816
  /**
6059
6817
  * LittleJS Newgrounds API
@@ -6078,11 +6836,11 @@ let newgrounds;
6078
6836
  class NewgroundsMedal extends Medal
6079
6837
  {
6080
6838
  /** Create a newgrounds medal object and adds it to the list of medals
6081
- * @param {Number} id - The unique identifier of the medal
6082
- * @param {String} name - Name of the medal
6083
- * @param {String} [description] - Description of the medal
6084
- * @param {String} [icon] - Icon for the medal
6085
- * @param {String} [src] - Image location for the medal
6839
+ * @param {number} id - The unique identifier of the medal
6840
+ * @param {string} name - Name of the medal
6841
+ * @param {string} [description] - Description of the medal
6842
+ * @param {string} [icon] - Icon for the medal
6843
+ * @param {string} [src] - Image location for the medal
6086
6844
  */
6087
6845
  constructor(id, name, description, icon, src)
6088
6846
  { super(id, name, description, icon, src); }
@@ -6174,7 +6932,7 @@ class NewgroundsPlugin
6174
6932
  * @param {number} id - The scoreboard id
6175
6933
  * @param {string} [user] - A user's id or name
6176
6934
  * @param {number} [social] - If true, only social scores will be loaded
6177
- * @param {number} [skip] - Number of scores to skip before start
6935
+ * @param {number} [skip] - Number of scores to skip over
6178
6936
  * @param {number} [limit] - Number of scores to include in the list
6179
6937
  * @return {Object} - The response JSON object
6180
6938
  */
@@ -6397,16 +7155,16 @@ class ZzFXMusic extends Sound
6397
7155
  if (!soundEnable || headlessMode) return;
6398
7156
  this.randomness = 0;
6399
7157
  this.sampleChannels = zzfxM(...zzfxMusic);
6400
- this.sampleRate = zzfxR;
7158
+ this.sampleRate = audioDefaultSampleRate;
6401
7159
  }
6402
7160
 
6403
- /** Play the music
6404
- * @param {number} [volume=1] - How much to scale volume by
6405
- * @param {boolean} [loop] - True if the music should loop
7161
+ /** Play the music that loops by default
7162
+ * @param {number} [volume] - Volume to play the music at
7163
+ * @param {boolean} [loop] - Should the music loop?
6406
7164
  * @return {AudioBufferSourceNode} - The audio source node
6407
7165
  */
6408
- playMusic(volume, loop=false)
6409
- { return super.play(undefined, volume, 1, 1, loop); }
7166
+ playMusic(volume=1, loop=true)
7167
+ { return super.play(undefined, volume, 1, 0, loop); }
6410
7168
  }
6411
7169
 
6412
7170
  ///////////////////////////////////////////////////////////////////////////////
@@ -6440,7 +7198,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
6440
7198
  let panning = 0;
6441
7199
  let hasMore = 1;
6442
7200
  let sampleCache = {};
6443
- let beatLength = zzfxR / BPM * 60 >> 2;
7201
+ let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
6444
7202
 
6445
7203
  // for each channel in order until there are no more
6446
7204
  for (; hasMore; channelIndex++) {
@@ -6459,15 +7217,15 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
6459
7217
  // get next offset, use the length of first channel
6460
7218
  nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
6461
7219
  // for each beat in pattern, plus one extra if end of sequence
6462
- isSequenceEnd = sequenceIndex == sequence.length - 1;
7220
+ isSequenceEnd = sequenceIndex === sequence.length - 1;
6463
7221
  for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
6464
7222
 
6465
7223
  // <channel-note>
6466
7224
  note = patternChannel[i];
6467
7225
 
6468
7226
  // stop if end, different instrument or new note
6469
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
6470
- instrument != (patternChannel[0] || 0) || note | 0;
7227
+ stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
7228
+ instrument !== (patternChannel[0] || 0) || note | 0;
6471
7229
 
6472
7230
  // fill buffer with samples for previous beat, most cpu intensive part
6473
7231
  for (j = 0; j < beatLength && notFirstBeat;
@@ -6552,11 +7310,11 @@ class UISystemPlugin
6552
7310
  /** @property {Color} - Default text color for UI elements */
6553
7311
  this.defaultTextColor = BLACK;
6554
7312
  /** @property {Color} - Default button color for UI elements */
6555
- this.defaultButtonColor = hsl(0,0,.5);
7313
+ this.defaultButtonColor = hsl(0,0,.7);
6556
7314
  /** @property {Color} - Default hover color for UI elements */
6557
- this.defaultHoverColor = hsl(0,0,.7);
7315
+ this.defaultHoverColor = hsl(0,0,.9);
6558
7316
  /** @property {Color} - Default color for disabled UI elements */
6559
- this.defaultDisabledColor = hsl(0,0,.2);
7317
+ this.defaultDisabledColor = hsl(0,0,.3);
6560
7318
  /** @property {number} - Default line width for UI elements */
6561
7319
  this.defaultLineWidth = 4;
6562
7320
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -6576,16 +7334,16 @@ class UISystemPlugin
6576
7334
 
6577
7335
  engineAddPlugin(uiUpdate, uiRender);
6578
7336
 
6579
- function updateInvisible(o)
6580
- {
6581
- for (const c of o.children)
6582
- updateInvisible(c);
6583
- o.updateInvisible();
6584
- }
6585
-
6586
7337
  // setup recursive update and render
6587
7338
  function uiUpdate()
6588
7339
  {
7340
+ function updateInvisibleObject(o)
7341
+ {
7342
+ // update invisible objects
7343
+ for (const c of o.children)
7344
+ updateInvisibleObject(c);
7345
+ o.updateInvisible();
7346
+ }
6589
7347
  function updateObject(o)
6590
7348
  {
6591
7349
  if (o.visible)
@@ -6599,7 +7357,7 @@ class UISystemPlugin
6599
7357
  o.update();
6600
7358
  }
6601
7359
  else
6602
- updateInvisible(o);
7360
+ updateInvisibleObject(o);
6603
7361
  }
6604
7362
  uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
6605
7363
  }
@@ -6625,7 +7383,7 @@ class UISystemPlugin
6625
7383
  * @param {Color} [color=uiSystem.defaultColor]
6626
7384
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6627
7385
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
6628
- * @param {number} [lineWidth=uiSystem.defaultCornerRadius] */
7386
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
6629
7387
  drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
6630
7388
  {
6631
7389
  const context = uiSystem.uiContext;
@@ -6700,32 +7458,40 @@ class UIObject
6700
7458
  constructor(pos=vec2(), size=vec2())
6701
7459
  {
6702
7460
  /** @property {Vector2} - Local position of the object */
6703
- this.localPos = pos.copy();
7461
+ this.localPos = pos.copy();
6704
7462
  /** @property {Vector2} - Screen space position of the object */
6705
- this.pos = pos.copy();
7463
+ this.pos = pos.copy();
6706
7464
  /** @property {Vector2} - Screen space size of the object */
6707
- this.size = size.copy();
6708
- /** @property {Color} - color of the object */
6709
- this.color = uiSystem.defaultColor;
6710
- /** @property {Color} - color for text */
6711
- this.textColor = uiSystem.defaultTextColor;
6712
- /** @property {Color} - color used when hovering over the object */
7465
+ this.size = size.copy();
7466
+ /** @property {Color} - Color of the object */
7467
+ this.color = uiSystem.defaultColor;
7468
+ /** @property {string} - Text for this ui object */
7469
+ this.text = undefined;
7470
+ /** @property {Color} - Color when disabled */
7471
+ this.disabledColor = uiSystem.defaultDisabledColor;
7472
+ /** @property {boolean} - Is this object disabled? */
7473
+ this.disabled = false;
7474
+ /** @property {Color} - Color for text */
7475
+ this.textColor = uiSystem.defaultTextColor;
7476
+ /** @property {Color} - Color used when hovering over the object */
6713
7477
  this.hoverColor = uiSystem.defaultHoverColor;
6714
- /** @property {Color} - color for line drawing */
6715
- this.lineColor = uiSystem.defaultLineColor;
6716
- /** @property {number} - width for line drawing */
6717
- this.lineWidth = uiSystem.defaultLineWidth;
6718
- /** @property {string} - font for this objecct */
6719
- this.font = uiSystem.defaultFont;
6720
- /** @property {number} - override for text height */
6721
- this.textHeight = undefined;
6722
- /** @property {boolean} - should this object be drawn */
6723
- this.visible = true;
6724
- /** @property {Array<UIObject>} - a list of this object's children */
6725
- this.children = [];
6726
- /** @property {UIObject} - this object's parent, position is in parent space */
6727
- this.parent = undefined;
6728
- /** @property {number} - Extra size added when checking if element is touched */
7478
+ /** @property {Color} - Color for line drawing */
7479
+ this.lineColor = uiSystem.defaultLineColor;
7480
+ /** @property {number} - Width for line drawing */
7481
+ this.lineWidth = uiSystem.defaultLineWidth;
7482
+ /** @property {number} - Corner radius for rounded rects */
7483
+ this.cornerRadius = uiSystem.defaultCornerRadius;
7484
+ /** @property {string} - Font for this objecct */
7485
+ this.font = uiSystem.defaultFont;
7486
+ /** @property {number} - Override for text height */
7487
+ this.textHeight = undefined;
7488
+ /** @property {boolean} - Should this object be drawn */
7489
+ this.visible = true;
7490
+ /** @property {Array<UIObject>} - A list of this object's children */
7491
+ this.children = [];
7492
+ /** @property {UIObject} - This object's parent, position is in parent space */
7493
+ this.parent = undefined;
7494
+ /** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
6729
7495
  this.extraTouchSize = 0;
6730
7496
  /** @property {Sound} - Sound when interactive element is pressed */
6731
7497
  this.soundPress = uiSystem.defaultSoundPress;
@@ -6757,7 +7523,7 @@ class UIObject
6757
7523
  */
6758
7524
  removeChild(child)
6759
7525
  {
6760
- ASSERT(child.parent == this && this.children.includes(child));
7526
+ ASSERT(child.parent === this && this.children.includes(child));
6761
7527
  this.children.splice(this.children.indexOf(child), 1);
6762
7528
  child.parent = undefined;
6763
7529
  }
@@ -6810,7 +7576,7 @@ class UIObject
6810
7576
  this.mouseIsHeld = false;
6811
7577
  }
6812
7578
 
6813
- if (this.mouseIsOver != mouseWasOver)
7579
+ if (this.mouseIsOver !== mouseWasOver)
6814
7580
  this.mouseIsOver ? this.onEnter() : this.onLeave();
6815
7581
  }
6816
7582
 
@@ -6821,7 +7587,7 @@ class UIObject
6821
7587
  uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
6822
7588
  }
6823
7589
 
6824
- /** Special update for when object is invisible */
7590
+ /** Special update when object is not visible */
6825
7591
  updateInvisible()
6826
7592
  {
6827
7593
  // reset input state when not visible
@@ -6829,28 +7595,22 @@ class UIObject
6829
7595
  }
6830
7596
 
6831
7597
  /** Called when the mouse enters the object */
6832
- onEnter()
6833
- {}
7598
+ onEnter() {}
6834
7599
 
6835
7600
  /** Called when the mouse leaves the object */
6836
- onLeave()
6837
- {}
7601
+ onLeave() {}
6838
7602
 
6839
7603
  /** Called when the mouse is pressed while over the object */
6840
- onPress()
6841
- {}
7604
+ onPress() {}
6842
7605
 
6843
7606
  /** Called when the mouse is released while over the object */
6844
- onRelease()
6845
- {}
7607
+ onRelease() {}
6846
7608
 
6847
7609
  /** Called when user clicks on this object */
6848
- onClick()
6849
- {}
7610
+ onClick() {}
6850
7611
 
6851
7612
  /** Called when the state of this object changes */
6852
- onChange()
6853
- {}
7613
+ onChange() {}
6854
7614
  };
6855
7615
 
6856
7616
  ///////////////////////////////////////////////////////////////////////////////
@@ -6871,13 +7631,13 @@ class UIText extends UIObject
6871
7631
  {
6872
7632
  super(pos, size);
6873
7633
 
6874
- /** @property {string} */
7634
+ // set properties
6875
7635
  this.text = text;
6876
- /** @property {string} */
6877
7636
  this.align = align;
7637
+ this.font = font;
6878
7638
 
6879
- this.font = font; // set font
6880
- this.lineWidth = 0; // set text to not be outlined by default
7639
+ // make text not outlined by default
7640
+ this.lineWidth = 0;
6881
7641
  }
6882
7642
  render()
6883
7643
  {
@@ -6904,13 +7664,14 @@ class UITile extends UIObject
6904
7664
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
6905
7665
  {
6906
7666
  super(pos, size);
6907
-
6908
7667
  /** @property {TileInfo} - Tile image to use */
6909
7668
  this.tileInfo = tileInfo;
6910
7669
  /** @property {number} - Angle to rotate in radians */
6911
7670
  this.angle = angle;
6912
7671
  /** @property {boolean} - Should it be mirrored? */
6913
7672
  this.mirror = mirror;
7673
+
7674
+ // set properties
6914
7675
  this.color = color;
6915
7676
  }
6916
7677
  render()
@@ -6936,22 +7697,20 @@ class UIButton extends UIObject
6936
7697
  {
6937
7698
  super(pos, size);
6938
7699
 
6939
- /** @property {string} */
7700
+ // set properties
6940
7701
  this.text = text;
6941
- /** @property {Color} */
6942
- this.disabledColor = uiSystem.defaultDisabledColor;
6943
- /** @property {boolean} */
6944
- this.disabled = false;
6945
- this.interactive = true;
6946
7702
  this.color = color;
7703
+ this.interactive = true;
6947
7704
  }
6948
7705
  render()
6949
7706
  {
7707
+ // draw the button
6950
7708
  const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
6951
7709
  const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
6952
7710
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
6953
7711
 
6954
- const textScale = .8; // scale text to fit in button
7712
+ // draw the text
7713
+ const textScale = .8; // scale text to fit
6955
7714
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
6956
7715
  uiSystem.drawText(this.text, this.pos, textSize,
6957
7716
  this.textColor, 0, undefined, this.align, this.font);
@@ -6969,13 +7728,18 @@ class UICheckbox extends UIObject
6969
7728
  * @param {Vector2} [pos]
6970
7729
  * @param {Vector2} [size]
6971
7730
  * @param {boolean} [checked]
7731
+ * @param {string} [text]
7732
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6972
7733
  */
6973
- constructor(pos, size, checked=false)
7734
+ constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
6974
7735
  {
6975
7736
  super(pos, size);
6976
-
6977
- /** @property {boolean} */
7737
+ /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
6978
7738
  this.checked = checked;
7739
+
7740
+ // set properties
7741
+ this.text = text;
7742
+ this.color = color;
6979
7743
  this.interactive = true;
6980
7744
  }
6981
7745
  onClick()
@@ -6985,14 +7749,24 @@ class UICheckbox extends UIObject
6985
7749
  }
6986
7750
  render()
6987
7751
  {
6988
- const color = this.mouseIsOver? this.hoverColor : this.color;
7752
+ const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
6989
7753
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
6990
7754
  if (this.checked)
6991
7755
  {
6992
- // draw an X if checked
6993
- uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor);
6994
- uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor);
7756
+ const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
7757
+ const length = lerp(1, 2**.5/2, p) / 2;
7758
+ let s = this.size.scale(length);
7759
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
7760
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
6995
7761
  }
7762
+
7763
+ // draw the text to the right side of the checkbox
7764
+ const textScale = .8; // scale text to fit
7765
+ const gapScale = .55;
7766
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7767
+ const pos = this.pos.add(vec2(this.size.x*gapScale,0));
7768
+ uiSystem.drawText(this.text, pos, textSize,
7769
+ this.textColor, 0, undefined, 'left', this.font);
6996
7770
  }
6997
7771
  }
6998
7772
 
@@ -7015,43 +7789,51 @@ class UIScrollbar extends UIObject
7015
7789
  {
7016
7790
  super(pos, size);
7017
7791
 
7018
- /** @property {number} */
7792
+ /** @property {number} - Current percentage value of this scrollbar 0-1 */
7019
7793
  this.value = value;
7020
- /** @property {string} */
7021
- this.text = text;
7022
- /** @property {Color} */
7794
+ /** @property {Color} - Color for the handle part of the scrollbar */
7023
7795
  this.handleColor = handleColor;
7796
+
7797
+ // set properties
7798
+ this.text = text;
7024
7799
  this.color = color;
7025
7800
  this.interactive = true;
7026
7801
  }
7027
7802
  update()
7028
7803
  {
7029
7804
  super.update();
7030
- if (this.mouseIsHeld)
7805
+ if (this.mouseIsHeld && this.interactive)
7031
7806
  {
7807
+ // check if value changed
7032
7808
  const handleSize = vec2(this.size.y);
7033
7809
  const handleWidth = this.size.x - handleSize.x;
7034
7810
  const p1 = this.pos.x - handleWidth/2;
7035
7811
  const p2 = this.pos.x + handleWidth/2;
7036
7812
  const oldValue = this.value;
7037
7813
  this.value = percent(mousePosScreen.x, p1, p2);
7038
- this.value == oldValue || this.onChange();
7814
+ this.value === oldValue || this.onChange();
7039
7815
  }
7040
7816
  }
7041
7817
  render()
7042
7818
  {
7043
- const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
7044
- const color = this.mouseIsOver? this.hoverColor : this.color;
7819
+ // draw the scrollbar background
7820
+ const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
7821
+ this.color : this.lineColor;
7822
+ const color = this.disabled ? this.disabledColor :
7823
+ this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
7045
7824
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7046
7825
 
7826
+ // draw the scrollbar handle
7047
7827
  const handleSize = vec2(this.size.y);
7048
7828
  const handleWidth = this.size.x - handleSize.x;
7049
7829
  const p1 = this.pos.x - handleWidth/2;
7050
7830
  const p2 = this.pos.x + handleWidth/2;
7051
7831
  const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
7052
- const barColor = this.mouseIsHeld ? this.color : this.handleColor;
7053
- uiSystem.drawRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor, this.cornerRadius);
7832
+ const handleColor = this.disabled ? this.disabledColor :
7833
+ this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
7834
+ uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
7054
7835
 
7836
+ // draw the text on the scrollbar
7055
7837
  const textScale = .8; // scale text to fit in scrollbar
7056
7838
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7057
7839
  uiSystem.drawText(this.text, this.pos, textSize,
@@ -7146,14 +7928,14 @@ class Box2dObject extends EngineObject
7146
7928
  if (this.tileInfo)
7147
7929
  super.render();
7148
7930
  else
7149
- this.drawFixtures(this.color, this.lineColor, this.lineWidth, mainContext);
7931
+ this.drawFixtures(this.color, this.lineColor, this.lineWidth);
7150
7932
  }
7151
7933
 
7152
7934
  /** Render debug info */
7153
7935
  renderDebugInfo()
7154
7936
  {
7155
7937
  const isAsleep = !this.getIsAwake();
7156
- const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
7938
+ const isStatic = this.getBodyType() === box2d.bodyTypeStatic;
7157
7939
  const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
7158
7940
  this.drawFixtures(color);
7159
7941
  }
@@ -8621,7 +9403,7 @@ class Box2dPlugin
8621
9403
  queryCallback.ReportFixture = function(fixturePointer)
8622
9404
  {
8623
9405
  const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8624
- if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
9406
+ if (dynamicOnly && fixture.GetBody().GetType() !== box2d.instance.b2_dynamicBody)
8625
9407
  return true; // continue getting results
8626
9408
  if (!fixture.TestPoint(box2d.vec2dTo(pos)))
8627
9409
  return true; // continue getting results
@@ -8650,7 +9432,7 @@ class Box2dPlugin
8650
9432
  * @param {Color} [lineColor]
8651
9433
  * @param {number} [lineWidth]
8652
9434
  * @param {CanvasRenderingContext2D} [context] */
8653
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context=drawContext)
9435
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
8654
9436
  {
8655
9437
  const shape = box2d.castObjectType(fixture.GetShape());
8656
9438
  switch (shape.GetType())
@@ -8660,20 +9442,20 @@ class Box2dPlugin
8660
9442
  let points = [];
8661
9443
  for (let i=shape.GetVertexCount(); i--;)
8662
9444
  points.push(box2d.vec2From(shape.GetVertex(i)));
8663
- drawPoly(points, color, lineWidth, lineColor, pos, angle, false, false, context);
9445
+ drawPoly(points, color, lineWidth, lineColor, pos, angle);
8664
9446
  break;
8665
9447
  }
8666
9448
  case box2d.instance.b2Shape.e_circle:
8667
9449
  {
8668
9450
  const radius = shape.get_m_radius();
8669
- drawCircle(pos, radius, color, lineWidth, lineColor, false, false, context);
9451
+ drawCircle(pos, radius*2, color, lineWidth, lineColor);
8670
9452
  break;
8671
9453
  }
8672
9454
  case box2d.instance.b2Shape.e_edge:
8673
9455
  {
8674
9456
  const v1 = box2d.vec2From(shape.get_m_vertex1());
8675
9457
  const v2 = box2d.vec2From(shape.get_m_vertex2());
8676
- drawLine(v1, v2, lineWidth, lineColor, pos, angle, false, false, context);
9458
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle);
8677
9459
  break;
8678
9460
  }
8679
9461
  }
@@ -8752,7 +9534,9 @@ class Box2dPlugin
8752
9534
  }
8753
9535
 
8754
9536
  ///////////////////////////////////////////////////////////////////////////////
8755
- /** Box2d Init - Call with await before starting LittleJS to init box2d
9537
+ /** Box2d Init - Call with await to init box2d
9538
+ * @example
9539
+ * await box2dInit();
8756
9540
  * @return {Promise<Box2dPlugin>}
8757
9541
  * @memberof Box2D */
8758
9542
  async function box2dInit()
@@ -8815,14 +9599,14 @@ async function box2dInit()
8815
9599
  {
8816
9600
  color = getDebugColor(color);
8817
9601
  center = box2d.vec2FromPointer(center);
8818
- drawCircle(center, radius, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
9602
+ drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
8819
9603
  };
8820
9604
  debugDraw.DrawSolidCircle = function(center, radius, axis, color)
8821
9605
  {
8822
9606
  color = getDebugColor(color);
8823
9607
  center = box2d.vec2FromPointer(center);
8824
9608
  axis = box2d.vec2FromPointer(axis).scale(radius);
8825
- drawCircle(center, radius, color, debugLineWidth, color, false, false, overlayContext);
9609
+ drawCircle(center, radius*2, color, debugLineWidth, color, false, false, overlayContext);
8826
9610
  drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
8827
9611
  };
8828
9612
  debugDraw.DrawTransform = function(transform)
@@ -8868,7 +9652,7 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
8868
9652
  }
8869
9653
 
8870
9654
  /** Draw a scalable nine-slice UI element in world space
8871
- * This function can apply color and additive color if webgl is enabled
9655
+ * This function can apply color and additive color if WebGL is enabled
8872
9656
  * @param {Vector2} pos - World space position
8873
9657
  * @param {Vector2} size - World space size
8874
9658
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
@@ -8897,9 +9681,9 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
8897
9681
  {
8898
9682
  // sides
8899
9683
  const horizontal = i%2;
8900
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-1:1));
9684
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
8901
9685
  const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
8902
- const sideTile = centerTile.offset(startTile.size.multiply(vec2(i==1?1:i==3?-1:0,i==0?-flip:i==2?flip:0)))
9686
+ const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
8903
9687
  drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
8904
9688
  }
8905
9689
  for (let i=4; i--;)
@@ -8928,7 +9712,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
8928
9712
  }
8929
9713
 
8930
9714
  /** Draw a scalable three-slice UI element in world space
8931
- * This function can apply color and additive color if webgl is enabled
9715
+ * This function can apply color and additive color if WebGL is enabled
8932
9716
  * @param {Vector2} pos - World space position
8933
9717
  * @param {Vector2} size - World space size
8934
9718
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
@@ -8960,7 +9744,7 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
8960
9744
  // sides
8961
9745
  const a = angle + i*PI/2;
8962
9746
  const horizontal = i%2;
8963
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-flip:flip));
9747
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
8964
9748
  const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
8965
9749
  drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
8966
9750
  }