littlejsengine 1.13.4 → 1.14.2

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 (53) hide show
  1. package/README.md +2 -1
  2. package/copyToGithub.bat +28 -0
  3. package/dist/littlejs.d.ts +346 -240
  4. package/dist/littlejs.esm.js +1370 -723
  5. package/dist/littlejs.esm.min.js +1 -1
  6. package/dist/littlejs.js +1350 -709
  7. package/dist/littlejs.min.js +1 -1
  8. package/dist/littlejs.release.js +1333 -692
  9. package/examples/box2d/game.js +1 -1
  10. package/examples/box2d/gameObjects.js +1 -1
  11. package/examples/breakout/game.js +30 -25
  12. package/examples/electron/index.html +2 -2
  13. package/examples/index.html +207 -96
  14. package/examples/platformer/gameEffects.js +19 -18
  15. package/examples/platformer/gameLevel.js +6 -1
  16. package/examples/shorts/base.html +1 -1
  17. package/examples/shorts/flappyGame.js +2 -1
  18. package/examples/shorts/helloWorld.js +1 -1
  19. package/examples/shorts/music.js +106 -0
  20. package/examples/shorts/parallax.js +71 -0
  21. package/examples/shorts/piano.js +7 -8
  22. package/examples/shorts/postProcess.js +25 -8
  23. package/examples/shorts/shapes.js +1 -9
  24. package/examples/shorts/song.mp3 +0 -0
  25. package/examples/shorts/uiSystem.js +15 -8
  26. package/examples/starter/index.html +2 -2
  27. package/examples/stress/index.html +8 -3
  28. package/examples/style.css +14 -4
  29. package/examples/uiSystem/game.js +11 -11
  30. package/package.json +1 -1
  31. package/plugins/box2d.js +10 -8
  32. package/plugins/drawUtilities.js +5 -5
  33. package/plugins/newgrounds.js +6 -6
  34. package/plugins/pluginExport.js +1 -1
  35. package/plugins/uiSystem.js +103 -79
  36. package/plugins/zzfxm.js +10 -10
  37. package/reference.md +90 -54
  38. package/src/engine.js +22 -22
  39. package/src/engineAudio.js +284 -109
  40. package/src/engineBuild.js +9 -9
  41. package/src/engineDebug.js +26 -26
  42. package/src/engineDraw.js +148 -97
  43. package/src/engineExport.js +22 -16
  44. package/src/engineInput.js +57 -67
  45. package/src/engineMedals.js +25 -25
  46. package/src/engineObject.js +25 -22
  47. package/src/engineParticles.js +59 -59
  48. package/src/engineRelease.js +1 -1
  49. package/src/engineSettings.js +20 -13
  50. package/src/engineTileLayer.js +34 -34
  51. package/src/engineUtilities.js +62 -55
  52. package/src/engineWebGL.js +447 -65
  53. /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.2';
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,7 +242,7 @@ 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);
@@ -258,13 +258,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
258
258
  function updateCanvas()
259
259
  {
260
260
  if (headlessMode) return;
261
-
261
+
262
262
  if (canvasFixedSize.x)
263
263
  {
264
264
  // clear canvas and set fixed size
265
265
  mainCanvas.width = canvasFixedSize.x;
266
266
  mainCanvas.height = canvasFixedSize.y;
267
-
267
+
268
268
  // fit to window by adding space on top or bottom if necessary
269
269
  const aspect = innerWidth / innerHeight;
270
270
  const fixedAspect = mainCanvas.width / mainCanvas.height;
@@ -274,10 +274,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
274
274
  else
275
275
  {
276
276
  // clear canvas and set size to same as window
277
- mainCanvas.width = min(innerWidth, canvasMaxSize.x);
277
+ mainCanvas.width = min(innerWidth, canvasMaxSize.x);
278
278
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
279
279
  }
280
-
280
+
281
281
  // clear overlay canvas and set size
282
282
  overlayCanvas.width = mainCanvas.width;
283
283
  overlayCanvas.height = mainCanvas.height;
@@ -296,15 +296,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
296
296
  return startEngine();
297
297
 
298
298
  // setup html
299
- const styleRoot =
299
+ const styleRoot =
300
300
  'margin:0;' + // fill the window
301
301
  'overflow:hidden;' + // no scroll bars
302
302
  'background:#000;' + // set background color
303
303
  'user-select:none;' + // prevent hold to select
304
304
  '-webkit-user-select:none;' + // compatibility for ios
305
- (!touchInputEnable ? '' : // no touch css settings
306
305
  'touch-action:none;' + // prevent mobile pinch to resize
307
- '-webkit-touch-callout:none');// compatibility for ios
306
+ '-webkit-touch-callout:none';// compatibility for ios
308
307
  rootElement.style.cssText = styleRoot;
309
308
  drawCanvas = mainCanvas = document.createElement('canvas');
310
309
  rootElement.appendChild(mainCanvas);
@@ -321,7 +320,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
321
320
  rootElement.appendChild(overlayCanvas);
322
321
  overlayContext = overlayCanvas.getContext('2d');
323
322
 
324
- // set canvas style
323
+ // set canvases
325
324
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
326
325
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
327
326
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
@@ -330,17 +329,18 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
330
329
  setCanvasPixelated(canvasPixelated);
331
330
  setOverlayCanvasPixelated(overlayCanvasPixelated);
332
331
  updateCanvas();
332
+ glPreRender();
333
333
 
334
334
  // create offscreen canvas for image processing
335
335
  workCanvas = new OffscreenCanvas(256, 256);
336
336
  workContext = workCanvas.getContext('2d', { willReadFrequently: true });
337
-
337
+
338
338
  // create promises for loading images
339
339
  const promises = imageSources.map((src, textureIndex)=>
340
- new Promise(resolve =>
340
+ new Promise(resolve =>
341
341
  {
342
342
  const image = new Image;
343
- image.onerror = image.onload = ()=>
343
+ image.onerror = image.onload = ()=>
344
344
  {
345
345
  const textureInfo = new TextureInfo(image);
346
346
  textureInfo.createWebGLTexture();
@@ -355,7 +355,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
355
355
  if (!imageSources.length)
356
356
  {
357
357
  // no images to load
358
- promises.push(new Promise(resolve =>
358
+ promises.push(new Promise(resolve =>
359
359
  {
360
360
  const textureInfo = new TextureInfo(new Image);
361
361
  textureInfos[0] = textureInfo;
@@ -367,7 +367,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
367
367
  if (showSplashScreen)
368
368
  {
369
369
  // draw splash screen
370
- promises.push(new Promise(resolve =>
370
+ promises.push(new Promise(resolve =>
371
371
  {
372
372
  let t = 0;
373
373
  console.log(`${engineName} Engine v${engineVersion}`);
@@ -599,7 +599,7 @@ function drawEngineSplashScreen(t)
599
599
  line(36,20,60,20);
600
600
 
601
601
  // engine front light
602
- circle(60,30,4,PI,3*PI,color(3,2));
602
+ circle(60,30,4,PI,3*PI,color(3,2));
603
603
  circle(60,30,4,PI,2*PI,color(3,3));
604
604
  circle(60,30,4,PI,3*PI);
605
605
 
@@ -648,10 +648,10 @@ function drawEngineSplashScreen(t)
648
648
  x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
649
649
  X += w;
650
650
  }
651
-
651
+
652
652
  x.restore();
653
653
  }
654
- /**
654
+ /**
655
655
  * LittleJS - Release Mode
656
656
  * - This file is used for release builds in place of engineDebug.js
657
657
  * - Debug functionality is disabled to reduce size and increase performance
@@ -764,7 +764,19 @@ function lerp(valueA, valueB, percent)
764
764
  if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
765
765
  console.warn('lerp() parameter order changed! use lerp(start, end, p)');
766
766
  return valueA + clamp(percent) * (valueB-valueA);
767
- }
767
+ }
768
+
769
+ /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
770
+ * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
771
+ * @param {number} value
772
+ * @param {number} percentA
773
+ * @param {number} percentB
774
+ * @param {number} lerpA
775
+ * @param {number} lerpB
776
+ * @return {number}
777
+ * @memberof Utilities */
778
+ function percentLerp(value, percentA, percentB, lerpA, lerpB)
779
+ { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
768
780
 
769
781
  /** Returns signed wrapped distance between the two values passed in
770
782
  * @param {number} valueA
@@ -816,7 +828,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
816
828
  * @memberof Utilities */
817
829
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
818
830
 
819
- /** Returns the nearest power of two not less then the value
831
+ /** Returns the nearest power of two not less than the value
820
832
  * @param {number} value
821
833
  * @return {number}
822
834
  * @memberof Utilities */
@@ -831,8 +843,8 @@ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
831
843
  * @return {boolean} - True if overlapping
832
844
  * @memberof Utilities */
833
845
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
834
- {
835
- return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
846
+ {
847
+ return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
836
848
  && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
837
849
  }
838
850
 
@@ -887,7 +899,7 @@ function isIntersecting(start, end, pos, size)
887
899
  function wave(frequency=1, amplitude=1, t=time, offset=0)
888
900
  { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
889
901
 
890
- /** Formats seconds to mm:ss style for display purposes
902
+ /** Formats seconds to mm:ss style for display purposes
891
903
  * @param {number} t - time in seconds
892
904
  * @return {string}
893
905
  * @memberof Utilities */
@@ -903,13 +915,12 @@ async function fetchJSON(url)
903
915
  return response.json();
904
916
  }
905
917
 
906
- /**
918
+ /**
907
919
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
908
920
  * @param {any} n
909
921
  * @return {boolean}
910
- * @memberof Utilities
911
- */
912
- function isNumber(n) { return typeof n == 'number' && !isNaN(n); }
922
+ * @memberof Utilities */
923
+ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
913
924
 
914
925
  ///////////////////////////////////////////////////////////////////////////////
915
926
 
@@ -964,13 +975,13 @@ function randInCircle(radius=1, minRadius=0)
964
975
  * @memberof Random */
965
976
  function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
966
977
  {
967
- return linear ? colorA.lerp(colorB, rand()) :
978
+ return linear ? colorA.lerp(colorB, rand()) :
968
979
  new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
969
980
  }
970
981
 
971
982
  ///////////////////////////////////////////////////////////////////////////////
972
983
 
973
- /**
984
+ /**
974
985
  * Seeded random number generator
975
986
  * - Can be used to create a deterministic random number sequence
976
987
  * @example
@@ -997,8 +1008,8 @@ class RandomGenerator
997
1008
  float(valueA=1, valueB=0)
998
1009
  {
999
1010
  // xorshift algorithm
1000
- this.seed ^= this.seed << 13;
1001
- this.seed ^= this.seed >>> 17;
1011
+ this.seed ^= this.seed << 13;
1012
+ this.seed ^= this.seed >>> 17;
1002
1013
  this.seed ^= this.seed << 5;
1003
1014
  return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
1004
1015
  }
@@ -1047,16 +1058,14 @@ class RandomGenerator
1047
1058
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
1048
1059
  * a = vec2(5); // set a to (5, 5)
1049
1060
  * b = vec2(); // set b to (0, 0)
1050
- * @memberof Utilities
1051
- */
1061
+ * @memberof Utilities */
1052
1062
  function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1053
1063
 
1054
- /**
1064
+ /**
1055
1065
  * Check if object is a valid Vector2
1056
1066
  * @param {any} v
1057
1067
  * @return {boolean}
1058
- * @memberof Utilities
1059
- */
1068
+ * @memberof Utilities */
1060
1069
  function isVector2(v) { return v instanceof Vector2; }
1061
1070
 
1062
1071
  // vector2 asserts
@@ -1065,10 +1074,10 @@ function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n);
1065
1074
  function ASSERT_VECTOR2_NORMAL(v)
1066
1075
  {
1067
1076
  ASSERT_VECTOR2_VALID(v);
1068
- ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1077
+ ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1069
1078
  }
1070
1079
 
1071
- /**
1080
+ /**
1072
1081
  * 2D Vector object with vector math library
1073
1082
  * - Functions do not change this so they can be chained together
1074
1083
  * @example
@@ -1192,7 +1201,7 @@ class Vector2
1192
1201
  * @param {number} [angle]
1193
1202
  * @param {number} [length]
1194
1203
  * @return {Vector2} */
1195
- setAngle(angle=0, length=1)
1204
+ setAngle(angle=0, length=1)
1196
1205
  {
1197
1206
  ASSERT_NUMBER_VALID(angle);
1198
1207
  ASSERT_NUMBER_VALID(length);
@@ -1207,7 +1216,7 @@ class Vector2
1207
1216
  rotate(angle)
1208
1217
  {
1209
1218
  ASSERT_NUMBER_VALID(angle);
1210
- const c = Math.cos(-angle), s = Math.sin(-angle);
1219
+ const c = Math.cos(-angle), s = Math.sin(-angle);
1211
1220
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1212
1221
  }
1213
1222
 
@@ -1219,9 +1228,9 @@ class Vector2
1219
1228
  ASSERT_NUMBER_VALID(direction);
1220
1229
  ASSERT_NUMBER_VALID(length);
1221
1230
  direction = mod(direction, 4);
1222
- ASSERT(direction==0 || direction==1 || direction==2 || direction==3,
1231
+ ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1223
1232
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1224
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1233
+ return vec2(direction%2 ? direction-1 ? -length : length : 0,
1225
1234
  direction%2 ? 0 : direction ? -length : length);
1226
1235
  }
1227
1236
 
@@ -1273,7 +1282,7 @@ class Vector2
1273
1282
  /** Returns this vector expressed as a string
1274
1283
  * @param {number} digits - precision to display
1275
1284
  * @return {string} */
1276
- toString(digits=3)
1285
+ toString(digits=3)
1277
1286
  {
1278
1287
  ASSERT_NUMBER_VALID(digits);
1279
1288
  if (debug)
@@ -1292,7 +1301,7 @@ class Vector2
1292
1301
 
1293
1302
  ///////////////////////////////////////////////////////////////////////////////
1294
1303
 
1295
- /**
1304
+ /**
1296
1305
  * Create a color object with RGBA values, white by default
1297
1306
  * @param {number} [r=1] - red
1298
1307
  * @param {number} [g=1] - green
@@ -1303,29 +1312,27 @@ class Vector2
1303
1312
  */
1304
1313
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1305
1314
 
1306
- /**
1315
+ /**
1307
1316
  * Create a color object with HSLA values, white by default
1308
1317
  * @param {number} [h=0] - hue
1309
1318
  * @param {number} [s=0] - saturation
1310
1319
  * @param {number} [l=1] - lightness
1311
1320
  * @param {number} [a=1] - alpha
1312
1321
  * @return {Color}
1313
- * @memberof Utilities
1314
- */
1322
+ * @memberof Utilities */
1315
1323
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
1316
1324
 
1317
- /**
1325
+ /**
1318
1326
  * Check if object is a valid Color
1319
1327
  * @param {any} c
1320
1328
  * @return {boolean}
1321
- * @memberof Utilities
1322
- */
1329
+ * @memberof Utilities */
1323
1330
  function isColor(c) { return c instanceof Color; }
1324
1331
 
1325
1332
  // color asserts
1326
1333
  function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c) && c.isValid(), 'Color is invalid.', c); }
1327
1334
 
1328
- /**
1335
+ /**
1329
1336
  * Color object (red, green, blue, alpha) with some helpful functions
1330
1337
  * @example
1331
1338
  * let a = new Color; // white
@@ -1397,7 +1404,7 @@ class Color
1397
1404
  * @param {number} scale
1398
1405
  * @param {number} [alphaScale=scale]
1399
1406
  * @return {Color} */
1400
- scale(scale, alphaScale=scale)
1407
+ scale(scale, alphaScale=scale)
1401
1408
  { return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
1402
1409
 
1403
1410
  /** Returns a copy of this color clamped to the valid range between 0 and 1
@@ -1414,9 +1421,9 @@ class Color
1414
1421
  ASSERT_NUMBER_VALID(percent);
1415
1422
  const p = clamp(percent);
1416
1423
  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),
1424
+ c.r*p + this.r*(1-p),
1425
+ c.g*p + this.g*(1-p),
1426
+ c.b*p + this.b*(1-p),
1420
1427
  c.a*p + this.a*(1-p));
1421
1428
  }
1422
1429
 
@@ -1456,15 +1463,15 @@ class Color
1456
1463
  const minC = min(r, g, b);
1457
1464
  const l = (maxC + minC) / 2;
1458
1465
  let h = 0, s = 0;
1459
- if (maxC != minC)
1466
+ if (maxC !== minC)
1460
1467
  {
1461
1468
  let d = maxC - minC;
1462
1469
  s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
1463
- if (r == maxC)
1470
+ if (r === maxC)
1464
1471
  h = (g - b) / d + (g < b ? 6 : 0);
1465
- else if (g == maxC)
1472
+ else if (g === maxC)
1466
1473
  h = (b - r) / d + 2;
1467
- else if (b == maxC)
1474
+ else if (b === maxC)
1468
1475
  h = (r - g) / d + 4;
1469
1476
  }
1470
1477
  return [h / 6, s, l, a];
@@ -1474,7 +1481,7 @@ class Color
1474
1481
  * @param {number} [amount]
1475
1482
  * @param {number} [alphaAmount]
1476
1483
  * @return {Color} */
1477
- mutate(amount=.05, alphaAmount=0)
1484
+ mutate(amount=.05, alphaAmount=0)
1478
1485
  {
1479
1486
  ASSERT_NUMBER_VALID(amount);
1480
1487
  ASSERT_NUMBER_VALID(alphaAmount);
@@ -1490,47 +1497,47 @@ class Color
1490
1497
  /** Returns this color expressed as a hex color code
1491
1498
  * @param {boolean} [useAlpha] - if alpha should be included in result
1492
1499
  * @return {string} */
1493
- toString(useAlpha = true)
1500
+ toString(useAlpha = true)
1494
1501
  {
1495
- ASSERT(typeof useAlpha == 'boolean', 'Use alpha boolean is invalid.', useAlpha);
1502
+ ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
1496
1503
  if (debug && !this.isValid())
1497
1504
  return `#000`;
1498
1505
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
1499
1506
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1500
1507
  }
1501
-
1508
+
1502
1509
  /** Set this color from a hex code
1503
1510
  * @param {string} hex - html hex code
1504
1511
  * @return {Color} */
1505
1512
  setHex(hex)
1506
1513
  {
1507
- ASSERT(typeof hex == 'string' && hex[0] == '#', 'Color hex code must be a string starting with #');
1514
+ ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
1508
1515
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1509
1516
 
1510
1517
  if (hex.length < 6)
1511
1518
  {
1512
1519
  const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
1513
1520
  this.r = fromHex(1);
1514
- this.g = fromHex(2),
1521
+ this.g = fromHex(2);
1515
1522
  this.b = fromHex(3);
1516
- this.a = hex.length == 5 ? fromHex(4) : 1;
1523
+ this.a = hex.length === 5 ? fromHex(4) : 1;
1517
1524
  }
1518
1525
  else
1519
1526
  {
1520
1527
  const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
1521
1528
  this.r = fromHex(1);
1522
- this.g = fromHex(3),
1529
+ this.g = fromHex(3);
1523
1530
  this.b = fromHex(5);
1524
- this.a = hex.length == 9 ? fromHex(7) : 1;
1531
+ this.a = hex.length === 9 ? fromHex(7) : 1;
1525
1532
  }
1526
1533
 
1527
1534
  ASSERT_COLOR_VALID(this);
1528
1535
  return this;
1529
1536
  }
1530
-
1537
+
1531
1538
  /** Returns this color expressed as 32 bit RGBA value
1532
1539
  * @return {number} */
1533
- rgbaInt()
1540
+ rgbaInt()
1534
1541
  {
1535
1542
  const r = clamp(this.r)*255|0;
1536
1543
  const g = clamp(this.g)*255<<8;
@@ -1551,7 +1558,7 @@ class Color
1551
1558
  /** Color - White #ffffff
1552
1559
  * @type {Color}
1553
1560
  * @memberof Utilities */
1554
- const WHITE = rgb();
1561
+ const WHITE = rgb();
1555
1562
 
1556
1563
  /** Color - Clear White #ffffff with 0 alpha
1557
1564
  * @type {Color}
@@ -1666,11 +1673,11 @@ class Timer
1666
1673
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1667
1674
  * @return {number} */
1668
1675
  getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
1669
-
1676
+
1670
1677
  /** Returns this timer expressed as a string
1671
1678
  * @return {string} */
1672
1679
  toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
1673
-
1680
+
1674
1681
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1675
1682
  * @return {number} */
1676
1683
  valueOf() { return this.get(); }
@@ -1706,7 +1713,7 @@ let cameraScale = 32;
1706
1713
  // Display settings
1707
1714
 
1708
1715
  /** Enable applying color to tiles when using canvas2d
1709
- * - This is slower but should be the same as webgl rendering
1716
+ * - This is slower but should be the same as WebGL rendering
1710
1717
  * @type {boolean}
1711
1718
  * @default
1712
1719
  * @memberof Settings */
@@ -1752,14 +1759,13 @@ let tilesPixelated = true;
1752
1759
  * @memberof Settings */
1753
1760
  let fontDefault = 'arial';
1754
1761
 
1755
- /** Enable to show the LittleJS splash screen be shown on startup
1762
+ /** Enable to show the LittleJS splash screen on startup
1756
1763
  * @type {boolean}
1757
1764
  * @default
1758
1765
  * @memberof Settings */
1759
1766
  let showSplashScreen = false;
1760
1767
 
1761
1768
  /** Disables all rendering, audio, and input for servers
1762
- * - Must be set before startup to take effect
1763
1769
  * @type {boolean}
1764
1770
  * @default
1765
1771
  * @memberof Settings */
@@ -1768,13 +1774,18 @@ let headlessMode = false;
1768
1774
  ///////////////////////////////////////////////////////////////////////////////
1769
1775
  // WebGL settings
1770
1776
 
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
1777
+ /** Enable WebGL accelerated rendering
1773
1778
  * @type {boolean}
1774
1779
  * @default
1775
1780
  * @memberof Settings */
1776
1781
  let glEnable = true;
1777
1782
 
1783
+ /** How many sided poly to use when drawing circles and ellipses with WebGL
1784
+ * @type {number}
1785
+ * @default
1786
+ * @memberof Settings */
1787
+ let glCircleSides = 32;
1788
+
1778
1789
  ///////////////////////////////////////////////////////////////////////////////
1779
1790
  // Tile sheet settings
1780
1791
 
@@ -1856,13 +1867,13 @@ let particleEmitRateScale = 1;
1856
1867
  * @memberof Settings */
1857
1868
  let gamepadsEnable = true;
1858
1869
 
1859
- /** If true, the dpad input is also routed to the left analog stick (for better accessability)
1870
+ /** If true, the dpad input is also routed to the left analog stick (for better accessibility)
1860
1871
  * @type {boolean}
1861
1872
  * @default
1862
1873
  * @memberof Settings */
1863
1874
  let gamepadDirectionEmulateStick = true;
1864
1875
 
1865
- /** If true the WASD keys are also routed to the direction keys (for better accessability)
1876
+ /** If true the WASD keys are also routed to the direction keys (for better accessibility)
1866
1877
  * @type {boolean}
1867
1878
  * @default
1868
1879
  * @memberof Settings */
@@ -1870,7 +1881,6 @@ let inputWASDEmulateDirection = true;
1870
1881
 
1871
1882
  /** True if touch input is enabled for mobile devices
1872
1883
  * - Touch events will be routed to mouse events
1873
- * - Must be set before startup to take effect
1874
1884
  * @type {boolean}
1875
1885
  * @default
1876
1886
  * @memberof Settings */
@@ -1878,7 +1888,6 @@ let touchInputEnable = true;
1878
1888
 
1879
1889
  /** True if touch gamepad should appear on mobile devices
1880
1890
  * - Supports left analog stick, 4 face buttons and start button (button 9)
1881
- * - Must be set before startup to take effect
1882
1891
  * @type {boolean}
1883
1892
  * @default
1884
1893
  * @memberof Settings */
@@ -1981,7 +1990,7 @@ function setCameraAngle(angle) { cameraAngle = angle; }
1981
1990
  function setCameraScale(scale) { cameraScale = scale; }
1982
1991
 
1983
1992
  /** Set if tiles should be colorized when using canvas2d
1984
- * This can be slower but results should look nearly identical to webgl rendering
1993
+ * This can be slower but results should look nearly identical to WebGL rendering
1985
1994
  * It can be enabled/disabled at any time
1986
1995
  * Optimized for performance, and will use faster method if color is white or untextured
1987
1996
  * @param {boolean} colorTiles
@@ -2017,8 +2026,8 @@ function setCanvasPixelated(pixelated)
2017
2026
  * @param {boolean} pixelated
2018
2027
  * @memberof Settings */
2019
2028
  function setOverlayCanvasPixelated(pixelated)
2020
- {
2021
- overlayCanvasPixelated = pixelated;
2029
+ {
2030
+ overlayCanvasPixelated = pixelated;
2022
2031
  if (overlayCanvas)
2023
2032
  overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2024
2033
  }
@@ -2033,7 +2042,7 @@ function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
2033
2042
  * @memberof Settings */
2034
2043
  function setFontDefault(font) { fontDefault = font; }
2035
2044
 
2036
- /** Set if the LittleJS splash screen be shown on startup
2045
+ /** Set if the LittleJS splash screen should be shown on startup
2037
2046
  * @param {boolean} show
2038
2047
  * @memberof Settings */
2039
2048
  function setShowSplashScreen(show) { showSplashScreen = show; }
@@ -2043,16 +2052,21 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
2043
2052
  * @memberof Settings */
2044
2053
  function setHeadlessMode(headless) { headlessMode = headless; }
2045
2054
 
2046
- /** Set if webgl rendering is enabled
2055
+ /** Set if WebGL rendering is enabled
2047
2056
  * @param {boolean} enable
2048
2057
  * @memberof Settings */
2049
2058
  function setGLEnable(enable)
2050
2059
  {
2051
2060
  glEnable = enable;
2052
- if (glCanvas) // hide glCanvas if webgl is disabled
2061
+ if (glCanvas) // hide glCanvas if WebGL is disabled
2053
2062
  glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2054
2063
  }
2055
2064
 
2065
+ /** Set how many sided polygons to use when drawing circles and elipses with WebGL
2066
+ * @param {number} sides
2067
+ * @memberof Settings */
2068
+ function setGLCircleSides(sides) { glCircleSides = sides; }
2069
+
2056
2070
  /** Set default size of tiles in pixels
2057
2071
  * @param {Vector2} size
2058
2072
  * @memberof Settings */
@@ -2083,7 +2097,7 @@ function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
2083
2097
  * @memberof Settings */
2084
2098
  function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
2085
2099
 
2086
- /** Set how much to bounce when a collision occur
2100
+ /** Set how much to bounce when a collision occurs
2087
2101
  * @param {number} restitution
2088
2102
  * @memberof Settings */
2089
2103
  function setObjectDefaultRestitution(restitution) { objectDefaultRestitution = restitution; }
@@ -2207,11 +2221,11 @@ function setShowWatermark(show) { showWatermark = show; }
2207
2221
  * @param {string} key
2208
2222
  * @memberof Debug */
2209
2223
  function setDebugKey(key) { debugKey = key; }
2210
- /**
2224
+ /**
2211
2225
  * LittleJS Object System
2212
2226
  */
2213
2227
 
2214
- /**
2228
+ /**
2215
2229
  * LittleJS Object Base Object Class
2216
2230
  * - Top level object class used by the engine
2217
2231
  * - Automatically adds self to object list
@@ -2234,7 +2248,7 @@ function setDebugKey(key) { debugKey = key; }
2234
2248
  * @example
2235
2249
  * // create an engine object, normally you would first extend the class with your own
2236
2250
  * const pos = vec2(2,3);
2237
- * const object = new EngineObject(pos);
2251
+ * const object = new EngineObject(pos);
2238
2252
  */
2239
2253
  class EngineObject
2240
2254
  {
@@ -2252,9 +2266,9 @@ class EngineObject
2252
2266
  ASSERT(isVector2(pos) && pos.isValid(), 'object pos should be a vec2');
2253
2267
  ASSERT(isVector2(size) && size.isValid(), 'object size should be a vec2');
2254
2268
  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');
2269
+ ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2256
2270
  ASSERT(isColor(color) && color.isValid(), 'object color should be a valid rgba color');
2257
- ASSERT(typeof renderOrder == 'number', 'object renderOrder should be a number');
2271
+ ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
2258
2272
 
2259
2273
  /** @property {Vector2} - World space position of the object */
2260
2274
  this.pos = pos.copy();
@@ -2322,7 +2336,7 @@ class EngineObject
2322
2336
  // add to list of objects
2323
2337
  engineObjects.push(this);
2324
2338
  }
2325
-
2339
+
2326
2340
  /** Update the object transform, called automatically by engine even when paused */
2327
2341
  updateTransforms()
2328
2342
  {
@@ -2391,7 +2405,7 @@ class EngineObject
2391
2405
  for (const o of engineObjectsCollide)
2392
2406
  {
2393
2407
  // non solid objects don't collide with each other
2394
- if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o == this)
2408
+ if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
2395
2409
  continue;
2396
2410
 
2397
2411
  // check collision
@@ -2414,7 +2428,7 @@ class EngineObject
2414
2428
  this.velocity = this.velocity.add(velocity);
2415
2429
  if (o.mass) // push away if not fixed
2416
2430
  o.velocity = o.velocity.subtract(velocity);
2417
-
2431
+
2418
2432
  debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
2419
2433
  continue;
2420
2434
  }
@@ -2425,7 +2439,7 @@ class EngineObject
2425
2439
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
2426
2440
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
2427
2441
  const restitution = max(this.restitution, o.restitution);
2428
-
2442
+
2429
2443
  if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
2430
2444
  {
2431
2445
  // push outside object collision
@@ -2513,7 +2527,7 @@ class EngineObject
2513
2527
  {
2514
2528
  // move to previous position
2515
2529
  this.pos.y = oldPos.y;
2516
- this.groundObject = undefined;
2530
+ this.groundObject = undefined;
2517
2531
  }
2518
2532
  }
2519
2533
  if (blockedLayerX)
@@ -2527,20 +2541,20 @@ class EngineObject
2527
2541
  }
2528
2542
  }
2529
2543
  }
2530
-
2544
+
2531
2545
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
2532
2546
  render()
2533
2547
  {
2534
2548
  // default object render
2535
2549
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
2536
2550
  }
2537
-
2551
+
2538
2552
  /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
2539
2553
  destroy()
2540
- {
2554
+ {
2541
2555
  if (this.destroyed)
2542
2556
  return;
2543
-
2557
+
2544
2558
  // disconnect from parent and destroy children
2545
2559
  this.destroyed = 1;
2546
2560
  this.parent && this.parent.removeChild(this);
@@ -2566,7 +2580,7 @@ class EngineObject
2566
2580
  /** Convert from world space to local space for a vector (rotation only)
2567
2581
  * @param {Vector2} vec - world space vector */
2568
2582
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
2569
-
2583
+
2570
2584
  /** Called to check if a tile collision should be resolved
2571
2585
  * @param {number} tileData - the value of the tile at the position
2572
2586
  * @param {Vector2} pos - tile where the collision occurred
@@ -2585,16 +2599,19 @@ class EngineObject
2585
2599
 
2586
2600
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
2587
2601
  * @param {Vector2} acceleration */
2588
- applyAcceleration(acceleration) { if (this.mass) this.velocity = this.velocity.add(acceleration); }
2602
+ applyAcceleration(acceleration)
2603
+ { if (this.mass) this.velocity = this.velocity.add(acceleration); }
2589
2604
 
2590
- /** Apply angular acceleration to this object
2605
+ /** Apply angular acceleration to this object
2591
2606
  * @param {number} acceleration */
2592
- applyAngularAcceleration(acceleration) { if (this.mass) this.angleVelocity += acceleration; }
2607
+ applyAngularAcceleration(acceleration)
2608
+ { if (this.mass) this.angleVelocity += acceleration; }
2593
2609
 
2594
2610
  /** Apply force to this object (adjust velocity, affected by mass)
2595
2611
  * @param {Vector2} force */
2596
- applyForce(force) { this.applyAcceleration(force.scale(1/this.mass)); }
2597
-
2612
+ applyForce(force)
2613
+ { if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
2614
+
2598
2615
  /** Get the direction of the mirror
2599
2616
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
2600
2617
  getMirrorSign() { return this.mirror ? -1 : 1; }
@@ -2616,7 +2633,7 @@ class EngineObject
2616
2633
  * @param {EngineObject} child */
2617
2634
  removeChild(child)
2618
2635
  {
2619
- ASSERT(child.parent == this && this.children.includes(child));
2636
+ ASSERT(child.parent === this && this.children.includes(child));
2620
2637
  this.children.splice(this.children.indexOf(child), 1);
2621
2638
  child.parent = 0;
2622
2639
  }
@@ -2662,7 +2679,7 @@ class EngineObject
2662
2679
  {
2663
2680
  if (!debug)
2664
2681
  return;
2665
-
2682
+
2666
2683
  // show object info for debugging
2667
2684
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
2668
2685
  const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
@@ -2672,24 +2689,24 @@ class EngineObject
2672
2689
  this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(1,1,1,.5));
2673
2690
  }
2674
2691
  }
2675
- /**
2692
+ /**
2676
2693
  * LittleJS Drawing System
2677
2694
  * - Hybrid system with both Canvas2D and WebGL available
2678
2695
  * - Super fast tile sheet rendering with WebGL
2679
2696
  * - Can apply rotation, mirror, color and additive color
2680
2697
  * - Font rendering system with built in engine font
2681
2698
  * - Many useful utility functions
2682
- *
2699
+ *
2683
2700
  * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
2684
2701
  * There are 3 canvas/contexts available to draw to...
2685
2702
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
2686
2703
  * glCanvas - Used by the accelerated WebGL batch rendering system.
2687
2704
  * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
2688
- *
2705
+ *
2689
2706
  * The WebGL rendering system is very fast with some caveats...
2690
2707
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
2691
2708
  * - Group additive rendering together using renderOrder to mitigate this issue
2692
- *
2709
+ *
2693
2710
  * The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
2694
2711
  * @namespace Draw
2695
2712
  */
@@ -2734,7 +2751,7 @@ let workCanvas;
2734
2751
  * @memberof Draw */
2735
2752
  let workContext;
2736
2753
 
2737
- /** The size of the main canvas (and other secondary canvases)
2754
+ /** The size of the main canvas (and other secondary canvases)
2738
2755
  * @type {Vector2}
2739
2756
  * @memberof Draw */
2740
2757
  let mainCanvasSize = vec2();
@@ -2749,7 +2766,7 @@ let drawCount;
2749
2766
 
2750
2767
  ///////////////////////////////////////////////////////////////////////////////
2751
2768
 
2752
- /**
2769
+ /**
2753
2770
  * Create a tile info object using a grid based system
2754
2771
  * - This can take vecs or floats for easier use and conversion
2755
2772
  * - If an index is passed in, the tile size and index will determine the position
@@ -2763,15 +2780,14 @@ let drawCount;
2763
2780
  * tile(5, 8) // a tile at index 5 using a tile size of 8
2764
2781
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
2765
2782
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
2766
- * @memberof Draw
2767
- */
2783
+ * @memberof Draw */
2768
2784
  function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
2769
2785
  {
2770
2786
  if (headlessMode)
2771
2787
  return new TileInfo;
2772
2788
 
2773
2789
  // if size is a number, make it a vector
2774
- if (typeof size == 'number')
2790
+ if (typeof size === 'number')
2775
2791
  {
2776
2792
  ASSERT(size > 0);
2777
2793
  size = new Vector2(size, size);
@@ -2780,24 +2796,24 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
2780
2796
  // create tile info object
2781
2797
  const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
2782
2798
 
2783
- // use get the pos of the tile
2799
+ // get the position of the tile
2784
2800
  const textureInfo = textureInfos[textureIndex];
2785
2801
  ASSERT(!!textureInfo, 'Texture not loaded');
2786
2802
  const sizePaddedX = size.x + padding*2;
2787
2803
  const sizePaddedY = size.y + padding*2;
2788
- if (typeof pos == 'number')
2804
+ if (typeof pos === 'number')
2789
2805
  {
2790
2806
  const cols = textureInfo.size.x / sizePaddedX |0;
2791
- ASSERT(cols>0, 'Tile size is too big for texture');
2807
+ ASSERT(cols > 0, 'Tile size is too big for texture');
2792
2808
  const posX = pos % cols, posY = (pos / cols) |0;
2793
2809
  tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
2794
2810
  }
2795
2811
  else
2796
2812
  tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
2797
- return tileInfo;
2813
+ return tileInfo;
2798
2814
  }
2799
2815
 
2800
- /**
2816
+ /**
2801
2817
  * Tile Info - Stores info about how to draw a tile
2802
2818
  */
2803
2819
  class TileInfo
@@ -2835,14 +2851,14 @@ class TileInfo
2835
2851
  */
2836
2852
  frame(frame)
2837
2853
  {
2838
- ASSERT(typeof frame == 'number');
2854
+ ASSERT(typeof frame === 'number');
2839
2855
  return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
2840
2856
  }
2841
2857
 
2842
2858
  /**
2843
2859
  * Set this tile to use a full image
2844
2860
  * @param {HTMLImageElement|OffscreenCanvas} image
2845
- * @param {WebGLTexture} [glTexture] - webgl texture
2861
+ * @param {WebGLTexture} [glTexture] - WebGL texture
2846
2862
  * @return {TileInfo}
2847
2863
  */
2848
2864
  setFullImage(image, glTexture)
@@ -2860,7 +2876,7 @@ class TextureInfo
2860
2876
  /**
2861
2877
  * Create a TextureInfo, called automatically by the engine
2862
2878
  * @param {HTMLImageElement|OffscreenCanvas} image
2863
- * @param {WebGLTexture} [glTexture] - webgl texture
2879
+ * @param {WebGLTexture} [glTexture] - WebGL texture
2864
2880
  */
2865
2881
  constructor(image, glTexture)
2866
2882
  {
@@ -2870,7 +2886,7 @@ class TextureInfo
2870
2886
  this.size = vec2(image.width, image.height);
2871
2887
  /** @property {Vector2} - inverse of the size, cached for rendering */
2872
2888
  this.sizeInverse = vec2(1/image.width, 1/image.height);
2873
- /** @property {WebGLTexture} - webgl texture */
2889
+ /** @property {WebGLTexture} - WebGL texture */
2874
2890
  this.glTexture = glTexture;
2875
2891
  }
2876
2892
 
@@ -2886,28 +2902,25 @@ class TextureInfo
2886
2902
  // Drawing functions
2887
2903
 
2888
2904
  /** 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
2905
+ * @param {Vector2} pos - Center of the tile in world space
2906
+ * @param {Vector2} [size=(1,1)] - Size of the tile in world space
2907
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
2908
+ * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2909
+ * @param {number} [angle] - Angle to rotate by
2910
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
2911
+ * @param {Color} [additiveColor] - Additive color to be applied if any
2912
+ * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
2913
+ * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
2898
2914
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2899
2915
  * @memberof Draw */
2900
- function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2916
+ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
2901
2917
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2902
2918
  {
2903
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2904
2919
  ASSERT(isVector2(pos) && pos.isValid(), 'drawTile pos should be a vec2');
2905
2920
  ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
2906
2921
  ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
2907
2922
  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
2923
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2911
2924
 
2912
2925
  const textureInfo = tileInfo && tileInfo.textureInfo;
2913
2926
  if (useWebGL)
@@ -2931,22 +2944,22 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2931
2944
  {
2932
2945
  const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
2933
2946
  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());
2947
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2948
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
2949
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2950
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2938
2951
  }
2939
2952
  else
2940
2953
  {
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());
2954
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2955
+ x, y, x + w, y + h,
2956
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2944
2957
  }
2945
2958
  }
2946
2959
  else
2947
2960
  {
2948
2961
  // if no tile info, force untextured
2949
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
2962
+ glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
2950
2963
  }
2951
2964
  }
2952
2965
  else
@@ -2984,8 +2997,8 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2984
2997
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2985
2998
  * @memberof Draw */
2986
2999
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2987
- {
2988
- drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
3000
+ {
3001
+ drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
2989
3002
  }
2990
3003
 
2991
3004
  /** Draw colored line between two points
@@ -3008,6 +3021,30 @@ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL
3008
3021
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3009
3022
  }
3010
3023
 
3024
+ /** Draw colored regular polygon using passed in number of sides
3025
+ * @param {Vector2} pos
3026
+ * @param {Vector2} [size=(1,1)]
3027
+ * @param {number} [sides]
3028
+ * @param {Color} [color=(1,1,1,1)]
3029
+ * @param {number} [angle]
3030
+ * @param {number} [lineWidth]
3031
+ * @param {Color} [lineColor=(0,0,0,1)]
3032
+ * @param {boolean} [useWebGL=glEnable]
3033
+ * @param {boolean} [screenSpace]
3034
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3035
+ * @memberof Draw */
3036
+ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3037
+ {
3038
+ // build regular polygon points
3039
+ const points = [];
3040
+ for (let i=sides; i--;)
3041
+ {
3042
+ const a = (i/sides)*PI*2;
3043
+ points.push(vec2(Math.sin(a)*size.x, Math.cos(a)*size.y));
3044
+ }
3045
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3046
+ }
3047
+
3011
3048
  /** Draw colored polygon using passed in points
3012
3049
  * @param {Array<Vector2>} points - Array of Vector2 points
3013
3050
  * @param {Color} [color=(1,1,1,1)]
@@ -3015,33 +3052,49 @@ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL
3015
3052
  * @param {Color} [lineColor=(0,0,0,1)]
3016
3053
  * @param {Vector2} [pos=(0,0)] - Offset to apply
3017
3054
  * @param {number} [angle] - Angle to rotate by
3018
- * @param {boolean} [useWebGL] - Webgl not supported
3055
+ * @param {boolean} [useWebGL=glEnable]
3019
3056
  * @param {boolean} [screenSpace]
3020
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3057
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3021
3058
  * @memberof Draw */
3022
- function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=false, screenSpace=false, context=drawContext)
3059
+ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3023
3060
  {
3024
3061
  ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
3025
3062
  ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3026
3063
  ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3027
3064
  ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3028
3065
  ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3029
- ASSERT(!useWebGL, 'drawPoly webgl not supported');
3030
- drawCanvas2D(pos, vec2(1), angle, false, context=>
3066
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3067
+ if (useWebGL)
3031
3068
  {
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)
3069
+ let scale = 1;
3070
+ if (screenSpace)
3039
3071
  {
3040
- context.strokeStyle = lineColor.toString();
3041
- context.lineWidth = lineWidth;
3042
- context.stroke();
3072
+ // convert to world space
3073
+ pos = screenToWorld(pos);
3074
+ scale = 1/cameraScale;
3043
3075
  }
3044
- }, screenSpace, context);
3076
+ glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
3077
+ if (lineWidth > 0)
3078
+ glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
3079
+ }
3080
+ else
3081
+ {
3082
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3083
+ {
3084
+ context.fillStyle = color.toString();
3085
+ context.beginPath();
3086
+ for (const point of points)
3087
+ context.lineTo(point.x, point.y);
3088
+ context.closePath();
3089
+ context.fill();
3090
+ if (lineWidth)
3091
+ {
3092
+ context.strokeStyle = lineColor.toString();
3093
+ context.lineWidth = lineWidth;
3094
+ context.stroke();
3095
+ }
3096
+ }, screenSpace, context);
3097
+ }
3045
3098
  }
3046
3099
 
3047
3100
  /** Draw colored ellipse using passed in point
@@ -3051,32 +3104,41 @@ function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec
3051
3104
  * @param {number} [angle]
3052
3105
  * @param {number} [lineWidth]
3053
3106
  * @param {Color} [lineColor=(0,0,0,1)]
3054
- * @param {boolean} [useWebGL] - Webgl not supported
3107
+ * @param {boolean} [useWebGL=glEnable]
3055
3108
  * @param {boolean} [screenSpace]
3056
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3109
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3057
3110
  * @memberof Draw */
3058
- function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace=false, context=drawContext)
3111
+ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3059
3112
  {
3060
3113
  ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
3061
3114
  ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
3062
3115
  ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3063
3116
  ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3064
3117
  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=>
3118
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3119
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3120
+ if (useWebGL)
3068
3121
  {
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)
3122
+ // draw as a regular polygon
3123
+ const sides = glCircleSides;
3124
+ drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
3125
+ }
3126
+ else
3127
+ {
3128
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3074
3129
  {
3075
- context.strokeStyle = lineColor.toString();
3076
- context.lineWidth = lineWidth;
3077
- context.stroke();
3078
- }
3079
- }, screenSpace, context);
3130
+ context.fillStyle = color.toString();
3131
+ context.beginPath();
3132
+ context.ellipse(0, 0, size.x, size.y, 0, 0, 9);
3133
+ context.fill();
3134
+ if (lineWidth)
3135
+ {
3136
+ context.strokeStyle = lineColor.toString();
3137
+ context.lineWidth = lineWidth;
3138
+ context.stroke();
3139
+ }
3140
+ }, screenSpace, context);
3141
+ }
3080
3142
  }
3081
3143
 
3082
3144
  /** Draw colored circle using passed in point
@@ -3085,11 +3147,11 @@ function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, l
3085
3147
  * @param {Color} [color=(1,1,1,1)]
3086
3148
  * @param {number} [lineWidth=0]
3087
3149
  * @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]
3150
+ * @param {boolean} [useWebGL=glEnable]
3151
+ * @param {boolean} [screenSpace]
3152
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3091
3153
  * @memberof Draw */
3092
- function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace, context=drawContext)
3154
+ function drawCircle(pos, radius=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3093
3155
  { drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
3094
3156
 
3095
3157
  /** Draw directly to a 2d canvas context in world space
@@ -3098,7 +3160,7 @@ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK
3098
3160
  * @param {number} angle
3099
3161
  * @param {boolean} [mirror]
3100
3162
  * @param {Function} [drawFunction]
3101
- * @param {boolean} [screenSpace=false]
3163
+ * @param {boolean} [screenSpace=false]
3102
3164
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3103
3165
  * @memberof Draw */
3104
3166
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
@@ -3168,7 +3230,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3168
3230
  * @param {number} [maxWidth]
3169
3231
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3170
3232
  * @memberof Draw */
3171
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
3233
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3172
3234
  {
3173
3235
  context.fillStyle = color.toString();
3174
3236
  context.strokeStyle = lineColor.toString();
@@ -3200,7 +3262,7 @@ function screenToWorld(screenPos)
3200
3262
  {
3201
3263
  let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3202
3264
  let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3203
- if (cameraAngle)
3265
+ if (cameraAngle)
3204
3266
  {
3205
3267
  // apply camera rotation
3206
3268
  const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
@@ -3293,7 +3355,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3293
3355
  {
3294
3356
  // white texture with no additive alpha, no need to tint
3295
3357
  context.globalAlpha = color.a;
3296
- context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3358
+ context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3297
3359
  context.globalAlpha = 1;
3298
3360
  }
3299
3361
  else
@@ -3314,7 +3376,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3314
3376
  for (let i = 0; i < data.length; ++i)
3315
3377
  data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
3316
3378
  workContext.putImageData(imageData, 0, 0);
3317
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3379
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3318
3380
  }
3319
3381
  else
3320
3382
  {
@@ -3327,7 +3389,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3327
3389
  }
3328
3390
  workContext.putImageData(imageData, 0, 0);
3329
3391
  context.globalAlpha = color.a;
3330
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3392
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3331
3393
  context.globalAlpha = 1;
3332
3394
  }
3333
3395
  }
@@ -3354,7 +3416,7 @@ function toggleFullscreen()
3354
3416
  }
3355
3417
 
3356
3418
  /** Set the cursor style
3357
- * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
3419
+ * @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
3358
3420
  * @memberof Draw */
3359
3421
  function setCursor(cursorStyle = 'auto')
3360
3422
  {
@@ -3366,7 +3428,7 @@ function setCursor(cursorStyle = 'auto')
3366
3428
 
3367
3429
  let engineFontImage;
3368
3430
 
3369
- /**
3431
+ /**
3370
3432
  * Font Image Object - Draw text on a 2D canvas by using characters in an image
3371
3433
  * - 96 characters (from space to tilde) are stored in an image
3372
3434
  * - Uses a default 8x8 font if none is supplied
@@ -3374,7 +3436,7 @@ let engineFontImage;
3374
3436
  * @example
3375
3437
  * // use built in font
3376
3438
  * const font = new FontImage;
3377
- *
3439
+ *
3378
3440
  * // draw text
3379
3441
  * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
3380
3442
  */
@@ -3384,7 +3446,6 @@ class FontImage
3384
3446
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
3385
3447
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3386
3448
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
3387
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
3388
3449
  */
3389
3450
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
3390
3451
  {
@@ -3398,7 +3459,6 @@ class FontImage
3398
3459
  this.image = image || engineFontImage;
3399
3460
  this.tileSize = tileSize;
3400
3461
  this.paddingSize = paddingSize;
3401
- this.context = context;
3402
3462
  }
3403
3463
 
3404
3464
  /** Draw text in world space using the image font
@@ -3406,23 +3466,32 @@ class FontImage
3406
3466
  * @param {Vector2} pos
3407
3467
  * @param {number} [scale=.25]
3408
3468
  * @param {boolean} [center]
3469
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
3409
3470
  */
3410
- drawText(text, pos, scale=1, center)
3471
+ drawText(text, pos, scale=1, center, context=drawContext)
3411
3472
  {
3412
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
3473
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
3413
3474
  }
3414
3475
 
3415
- /** Draw text in screen space using the image font
3476
+ /** Draw text on overlay canvas in world space using the image font
3477
+ * @param {string} text
3478
+ * @param {Vector2} pos
3479
+ * @param {number} [scale]
3480
+ * @param {boolean} [center]
3481
+ */
3482
+ drawTextOverlay(text, pos, scale=4, center)
3483
+ { this.drawText(text, pos, scale, center, overlayContext); }
3484
+
3485
+ /** Draw text on overlay canvas in screen space using the image font
3416
3486
  * @param {string} text
3417
3487
  * @param {Vector2} pos
3418
3488
  * @param {number} [scale]
3419
3489
  * @param {boolean} [center]
3490
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3420
3491
  */
3421
- drawTextScreen(text, pos, scale=4, center)
3492
+ drawTextScreen(text, pos, scale=4, center, context=overlayContext)
3422
3493
  {
3423
- const context = this.context;
3424
3494
  context.save();
3425
-
3426
3495
  const size = this.tileSize;
3427
3496
  const drawSize = size.add(this.paddingSize).scale(scale);
3428
3497
  const cols = this.image.width / this.tileSize.x |0;
@@ -3441,15 +3510,14 @@ class FontImage
3441
3510
  const x = tile % cols;
3442
3511
  const y = tile / cols |0;
3443
3512
  const drawPos = pos.add(vec2(j,i).multiply(drawSize));
3444
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
3513
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
3445
3514
  drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
3446
3515
  }
3447
3516
  });
3448
-
3449
3517
  context.restore();
3450
3518
  }
3451
3519
  }
3452
- /**
3520
+ /**
3453
3521
  * LittleJS Input System
3454
3522
  * - Tracks keyboard down, pressed, and released
3455
3523
  * - Tracks mouse buttons, position, and wheel
@@ -3465,10 +3533,10 @@ class FontImage
3465
3533
  * @return {boolean}
3466
3534
  * @memberof Input */
3467
3535
  function keyIsDown(key, device=0)
3468
- {
3536
+ {
3469
3537
  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);
3538
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
3539
+ return inputData[device] && !!(inputData[device][key] & 1);
3472
3540
  }
3473
3541
 
3474
3542
  /** Returns true if device key was pressed this frame
@@ -3477,10 +3545,10 @@ function keyIsDown(key, device=0)
3477
3545
  * @return {boolean}
3478
3546
  * @memberof Input */
3479
3547
  function keyWasPressed(key, device=0)
3480
- {
3548
+ {
3481
3549
  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);
3550
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
3551
+ return inputData[device] && !!(inputData[device][key] & 2);
3484
3552
  }
3485
3553
 
3486
3554
  /** Returns true if device key was released this frame
@@ -3489,9 +3557,9 @@ function keyWasPressed(key, device=0)
3489
3557
  * @return {boolean}
3490
3558
  * @memberof Input */
3491
3559
  function keyWasReleased(key, device=0)
3492
- {
3560
+ {
3493
3561
  ASSERT(key !== undefined, 'key is undefined');
3494
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
3562
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
3495
3563
  return inputData[device] && !!(inputData[device][key] & 4);
3496
3564
  }
3497
3565
 
@@ -3613,7 +3681,7 @@ function gamepadWasReleased(button, gamepad=0)
3613
3681
  * @param {number} [gamepad]
3614
3682
  * @return {Vector2}
3615
3683
  * @memberof Input */
3616
- function gamepadStick(stick, gamepad=0)
3684
+ function gamepadStick(stick, gamepad=0)
3617
3685
  { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
3618
3686
 
3619
3687
  ///////////////////////////////////////////////////////////////////////////////
@@ -3690,10 +3758,10 @@ function inputInit()
3690
3758
  {
3691
3759
  // handle remapping wasd keys to directions
3692
3760
  return inputWASDEmulateDirection ?
3693
- c == 'KeyW' ? 'ArrowUp' :
3694
- c == 'KeyS' ? 'ArrowDown' :
3695
- c == 'KeyA' ? 'ArrowLeft' :
3696
- c == 'KeyD' ? 'ArrowRight' : c : c;
3761
+ c === 'KeyW' ? 'ArrowUp' :
3762
+ c === 'KeyS' ? 'ArrowDown' :
3763
+ c === 'KeyA' ? 'ArrowLeft' :
3764
+ c === 'KeyD' ? 'ArrowRight' : c : c;
3697
3765
  }
3698
3766
  function onMouseDown(e)
3699
3767
  {
@@ -3701,9 +3769,9 @@ function inputInit()
3701
3769
  return;
3702
3770
 
3703
3771
  // fix stalled audio requiring user interaction
3704
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
3772
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
3705
3773
  audioContext.resume();
3706
-
3774
+
3707
3775
  isUsingGamepad = false;
3708
3776
  inputData[0][e.button] = 3;
3709
3777
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
@@ -3746,8 +3814,8 @@ function gamepadsUpdate()
3746
3814
  const applyDeadZones = (v)=>
3747
3815
  {
3748
3816
  const min=.3, max=.8;
3749
- const deadZone = (v)=>
3750
- v > min ? percent( v, min, max) :
3817
+ const deadZone = (v)=>
3818
+ v > min ? percent(v, min, max) :
3751
3819
  v < -min ? -percent(-v, min, max) : 0;
3752
3820
  return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
3753
3821
  }
@@ -3755,30 +3823,29 @@ function gamepadsUpdate()
3755
3823
  // update touch gamepad if enabled
3756
3824
  if (touchGamepadEnable && isTouchDevice)
3757
3825
  {
3758
- ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
3759
- if (touchGamepadTimer.isSet())
3826
+ if (!touchGamepadTimer.isSet())
3827
+ return;
3828
+
3829
+ // read virtual analog stick
3830
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
3831
+ sticks[0] = vec2();
3832
+ if (touchGamepadAnalog)
3833
+ sticks[0] = applyDeadZones(touchGamepadStick);
3834
+ else if (touchGamepadStick.lengthSquared() > .3)
3760
3835
  {
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
- }
3836
+ // convert to 8 way dpad
3837
+ sticks[0].x = Math.round(touchGamepadStick.x);
3838
+ sticks[0].y = -Math.round(touchGamepadStick.y);
3839
+ sticks[0] = sticks[0].clampLength();
3840
+ }
3773
3841
 
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
- }
3842
+ // read virtual gamepad buttons
3843
+ const data = inputData[1] || (inputData[1] = []);
3844
+ for (let i=10; i--;)
3845
+ {
3846
+ const j = i === 3 ? 2 : i === 2 ? 3 : i; // fix button locations
3847
+ const wasDown = gamepadIsDown(j,0);
3848
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
3782
3849
  }
3783
3850
  }
3784
3851
 
@@ -3804,7 +3871,7 @@ function gamepadsUpdate()
3804
3871
  // read analog sticks
3805
3872
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
3806
3873
  sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
3807
-
3874
+
3808
3875
  // read buttons
3809
3876
  for (let j = gamepad.buttons.length; j--;)
3810
3877
  {
@@ -3820,14 +3887,14 @@ function gamepadsUpdate()
3820
3887
  {
3821
3888
  // copy dpad to left analog stick when pressed
3822
3889
  const dpad = vec2(
3823
- (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
3890
+ (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
3824
3891
  (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
3825
3892
  if (dpad.lengthSquared())
3826
3893
  sticks[0] = dpad.clampLength();
3827
3894
  }
3828
3895
 
3829
3896
  // disable touch gamepad if using real gamepad
3830
- touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
3897
+ touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
3831
3898
  }
3832
3899
  }
3833
3900
  }
@@ -3852,20 +3919,13 @@ function vibrateStop() { vibrate(0); }
3852
3919
  const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
3853
3920
 
3854
3921
  // touch gamepad internal variables
3855
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
3922
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
3856
3923
 
3857
3924
  // enable touch input mouse passthrough
3858
3925
  function touchInputInit()
3859
3926
  {
3860
3927
  // add non passive touch event listeners
3861
3928
  let handleTouch = handleTouchDefault;
3862
- if (touchGamepadEnable)
3863
- {
3864
- // touch input internal variables
3865
- handleTouch = handleTouchGamepad;
3866
- touchGamepadButtons = [];
3867
- touchGamepadStick = vec2();
3868
- }
3869
3929
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
3870
3930
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
3871
3931
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
@@ -3874,8 +3934,15 @@ function touchInputInit()
3874
3934
  let wasTouching;
3875
3935
  function handleTouchDefault(e)
3876
3936
  {
3937
+ if (!touchInputEnable)
3938
+ return;
3939
+
3940
+ // route touch to gamepad
3941
+ if (touchGamepadEnable)
3942
+ handleTouchGamepad(e);
3943
+
3877
3944
  // fix stalled audio requiring user interaction
3878
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
3945
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
3879
3946
  audioContext.resume();
3880
3947
 
3881
3948
  // check if touching and pass to mouse events
@@ -3904,7 +3971,7 @@ function touchInputInit()
3904
3971
  // prevent default handling like copy and magnifier lens
3905
3972
  if (inputPreventDefault && document.hasFocus()) // allow document to get focus
3906
3973
  e.preventDefault();
3907
-
3974
+
3908
3975
  // must return true so the document will get focus
3909
3976
  return true;
3910
3977
  }
@@ -3916,7 +3983,7 @@ function touchInputInit()
3916
3983
  touchGamepadStick = vec2();
3917
3984
  touchGamepadButtons = [];
3918
3985
  isUsingGamepad = true;
3919
-
3986
+
3920
3987
  const touching = e.touches.length;
3921
3988
  if (touching)
3922
3989
  {
@@ -3925,9 +3992,6 @@ function touchInputInit()
3925
3992
  {
3926
3993
  // touch anywhere to press start when paused
3927
3994
  touchGamepadButtons[9] = 1;
3928
-
3929
- // call default touch handler so normal touch events still work
3930
- handleTouchDefault(e);
3931
3995
  return;
3932
3996
  }
3933
3997
  }
@@ -3958,12 +4022,6 @@ function touchInputInit()
3958
4022
  touchGamepadButtons[9] = 1;
3959
4023
  }
3960
4024
  }
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
4025
  }
3968
4026
  }
3969
4027
 
@@ -3973,7 +4031,7 @@ function touchGamepadRender()
3973
4031
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
3974
4032
  if (!touchGamepadEnable || !touchGamepadTimer.isSet())
3975
4033
  return;
3976
-
4034
+
3977
4035
  // fade off when not touching or paused
3978
4036
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
3979
4037
  if (!alpha || paused)
@@ -4004,11 +4062,11 @@ function touchGamepadRender()
4004
4062
  const angle = i*PI/4;
4005
4063
  context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
4006
4064
  i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
4007
- i==1 && context.fill();
4065
+ i===1 && context.fill();
4008
4066
  }
4009
4067
  context.stroke();
4010
4068
  }
4011
-
4069
+
4012
4070
  // draw right face buttons
4013
4071
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4014
4072
  for (let i=4; i--;)
@@ -4043,8 +4101,8 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
4043
4101
  /** Check if pointer is locked (true if locked)
4044
4102
  * @return {boolean}
4045
4103
  * @memberof Input */
4046
- function pointerLockIsActive() { return document.pointerLockElement == mainCanvas; }
4047
- /**
4104
+ function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
4105
+ /**
4048
4106
  * LittleJS Audio System
4049
4107
  * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
4050
4108
  * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
@@ -4065,10 +4123,21 @@ let audioContext = new AudioContext;
4065
4123
  * @memberof Audio */
4066
4124
  let audioMasterGain;
4067
4125
 
4126
+ /** Default sample rate used for sounds
4127
+ * @default 44100
4128
+ * @memberof Audio */
4129
+ const audioDefaultSampleRate = 44100;
4130
+
4131
+ /** Check if the audio context is running and available for playback
4132
+ * @return {boolean} - True if the audio context is running
4133
+ * @memberof Audio */
4134
+ function audioIsRunning()
4135
+ { return audioContext.state === 'running'; }
4136
+
4068
4137
  function audioInit()
4069
4138
  {
4070
4139
  if (!soundEnable || headlessMode) return;
4071
-
4140
+
4072
4141
  audioMasterGain = audioContext.createGain();
4073
4142
  audioMasterGain.connect(audioContext.destination);
4074
4143
  audioMasterGain.gain.value = soundVolume; // set starting value
@@ -4076,14 +4145,14 @@ function audioInit()
4076
4145
 
4077
4146
  ///////////////////////////////////////////////////////////////////////////////
4078
4147
 
4079
- /**
4148
+ /**
4080
4149
  * Sound Object - Stores a sound for later use and can be played positionally
4081
- *
4150
+ *
4082
4151
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4083
4152
  * @example
4084
4153
  * // create a sound
4085
4154
  * const sound_example = new Sound([.5,.5]);
4086
- *
4155
+ *
4087
4156
  * // play the sound
4088
4157
  * sound_example.play();
4089
4158
  */
@@ -4100,33 +4169,41 @@ class Sound
4100
4169
 
4101
4170
  /** @property {number} - World space max range of sound */
4102
4171
  this.range = range;
4103
-
4104
4172
  /** @property {number} - At what percentage of range should it start tapering */
4105
4173
  this.taper = taper;
4106
-
4107
4174
  /** @property {number} - How much to randomize frequency each time sound plays */
4108
4175
  this.randomness = 0;
4176
+ /** @property {number} - Sample rate for this sound */
4177
+ this.sampleRate = audioDefaultSampleRate;
4178
+ /** @property {number} - Percentage of this sound currently loaded */
4179
+ this.loadedPercent = 0;
4109
4180
 
4181
+ // generate zzfx sound now for fast playback
4110
4182
  if (zzfxSound)
4111
4183
  {
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
4184
+ // remove randomness so it can be applied on playback
4185
+ const randomnessIndex = 1, defaultRandomness = .05;
4186
+ this.randomness = zzfxSound[randomnessIndex] !== undefined ?
4187
+ zzfxSound[randomnessIndex] : defaultRandomness;
4188
+ zzfxSound[randomnessIndex] = 0;
4189
+
4190
+ // generate the zzfx samples
4116
4191
  this.sampleChannels = [zzfxG(...zzfxSound)];
4117
- this.sampleRate = zzfxR;
4192
+ this.loadedPercent = 1;
4118
4193
  }
4119
4194
  }
4120
4195
 
4121
4196
  /** 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
4197
+ * Sounds may not play until a user interaction occurs
4198
+ * @param {Vector2} [pos] - World space position to play the sound if any
4199
+ * @param {number} [volume] - How much to scale volume by
4200
+ * @param {number} [pitch] - How much to scale pitch by
4201
+ * @param {number} [randomnessScale] - How much to scale pitch randomness
4202
+ * @param {boolean} [loop] - Should the sound loop?
4203
+ * @param {boolean} [paused] - Should the sound start paused
4204
+ * @return {SoundInstance} - The audio source node
4128
4205
  */
4129
- play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
4206
+ play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
4130
4207
  {
4131
4208
  if (!soundEnable || headlessMode) return;
4132
4209
  if (!this.sampleChannels) return;
@@ -4149,75 +4226,55 @@ class Sound
4149
4226
  // get pan from screen space coords
4150
4227
  pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
4151
4228
  }
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
4229
 
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;
4230
+ // Create and return sound instance
4231
+ const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
4232
+ return new SoundInstance(this, volume, rate, pan, loop, paused);
4184
4233
  }
4185
4234
 
4186
- /** Get source of most recent instance of this sound that was played
4187
- * @return {AudioBufferSourceNode}
4235
+ /** Play a music track that loops by default
4236
+ * @param {number} [volume] - Volume to play the music at
4237
+ * @param {boolean} [loop] - Should the music loop?
4238
+ * @param {boolean} [paused] - Should the music start paused
4239
+ * @return {SoundInstance} - The audio source node
4188
4240
  */
4189
- getSource() { return this.source; }
4241
+ playMusic(volume=1, loop=true, paused=false)
4242
+ { return this.play(undefined, volume, 1, 0, loop, paused); }
4190
4243
 
4191
- /** Play the sound as a note with a semitone offset
4244
+ /** Play the sound as a musical note with a semitone offset
4245
+ * This can be used to play music with chromatic scales
4192
4246
  * @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
4247
+ * @param {Vector2} [pos] - World space position to play the sound if any
4248
+ * @param {number} [volume=1] - How much to scale volume by
4249
+ * @return {SoundInstance} - The audio source node
4196
4250
  */
4197
4251
  playNote(semitoneOffset, pos, volume)
4198
- { return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
4252
+ {
4253
+ const pitch = getNoteFrequency(semitoneOffset, 1);
4254
+ return this.play(pos, volume, pitch, 0);
4255
+ }
4199
4256
 
4200
4257
  /** Get how long this sound is in seconds
4201
4258
  * @return {number} - How long the sound is in seconds (undefined if loading)
4202
4259
  */
4203
- getDuration()
4260
+ getDuration()
4204
4261
  { 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
4262
+
4263
+ /** Check if sound is loaded, for sounds fetched from a url
4264
+ * @return {boolean} - True if sound is loaded and ready to play
4208
4265
  */
4209
- isLoading() { return !this.sampleChannels; }
4266
+ isLoaded() { return this.loadedPercent === 1; }
4210
4267
  }
4211
4268
 
4212
4269
  ///////////////////////////////////////////////////////////////////////////////
4213
4270
 
4214
- /**
4271
+ /**
4215
4272
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
4216
4273
  * - this can be used to play wave, mp3, and ogg files
4217
4274
  * @example
4218
4275
  * // create a sound
4219
4276
  * const sound_example = new SoundWave('sound.mp3');
4220
- *
4277
+ *
4221
4278
  * // play the sound
4222
4279
  * sound_example.play();
4223
4280
  */
@@ -4241,34 +4298,210 @@ class SoundWave extends Sound
4241
4298
  this.loadSound(filename);
4242
4299
  }
4243
4300
 
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)
4301
+ /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
4302
+ * @param {string} filename
4303
+ * @return {Promise<void>} */
4304
+ async loadSound(filename)
4305
+ {
4306
+ const response = await fetch(filename);
4307
+ const arrayBuffer = await response.arrayBuffer();
4308
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
4309
+
4310
+ // convert audio buffer to sample channels across multiple frames
4311
+ const channelCount = audioBuffer.numberOfChannels;
4312
+ const samplesPerFrame = 1e5;
4313
+ const sampleChannels = [];
4314
+ for (let channel = 0; channel < channelCount; channel++)
4315
+ {
4316
+ const channelData = audioBuffer.getChannelData(channel);
4317
+ const channelLength = channelData.length;
4318
+ sampleChannels[channel] = new Array(channelLength);
4319
+ let sampleIndex = 0;
4320
+ while (sampleIndex < channelLength)
4321
+ {
4322
+ // yield to next frame
4323
+ await new Promise(resolve => setTimeout(resolve, 0));
4324
+
4325
+ // copy chunk of samples
4326
+ const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
4327
+ for (; sampleIndex < endIndex; sampleIndex++)
4328
+ sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
4329
+
4330
+ // update loaded percent
4331
+ const samplesTotal = channelCount * channelLength;
4332
+ const samplesProcessed = channel * channelLength + sampleIndex;
4333
+ this.loadedPercent = samplesProcessed / samplesTotal;
4334
+ }
4335
+ }
4336
+
4337
+ // setup the sound to be played
4338
+ this.sampleRate = audioBuffer.sampleRate;
4339
+ this.sampleChannels = sampleChannels;
4340
+ this.loadedPercent = 1;
4341
+ if (this.onloadCallback)
4342
+ this.onloadCallback();
4343
+ }
4344
+ }
4345
+
4346
+ ///////////////////////////////////////////////////////////////////////////////
4347
+
4348
+ /**
4349
+ * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
4350
+ * Represents a single playing instance of a sound with pause/resume capabilities
4351
+ * @example
4352
+ * // Play a sound and get an instance for control
4353
+ * const jumpSound = new Sound([.5,.5,220]);
4354
+ * const instance = jumpSound.play();
4355
+ *
4356
+ * // Control the individual instance
4357
+ * instance.setVolume(.5);
4358
+ * instance.pause();
4359
+ * instance.unpause();
4360
+ * instance.stop();
4361
+ */
4362
+ class SoundInstance
4363
+ {
4364
+ /** Create a sound instance
4365
+ * @param {Sound} sound - The sound object
4366
+ * @param {number} [volume] - How much to scale volume by
4367
+ * @param {number} [rate] - The playback rate to use
4368
+ * @param {number} [pan] - How much to apply stereo panning
4369
+ * @param {boolean} [loop] - Should the sound loop?
4370
+ * @param {boolean} [paused] - Should the sound start paused? */
4371
+ constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
4372
+ {
4373
+ ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
4374
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
4375
+ ASSERT(rate >= 0, 'Sound rate must be positive or zero');
4376
+ ASSERT(isNumber(pan), 'Sound pan must be a number');
4377
+
4378
+ /** @property {Sound} - The sound object */
4379
+ this.sound = sound;
4380
+ /** @property {number} - How much to scale volume by */
4381
+ this.volume = volume;
4382
+ /** @property {number} - The playback rate to use */
4383
+ this.rate = rate;
4384
+ /** @property {number} - How much to apply stereo panning */
4385
+ this.pan = pan;
4386
+ /** @property {boolean} - Should the sound loop */
4387
+ this.loop = loop;
4388
+ /** @property {number} - Timestamp for audio context when paused */
4389
+ this.pausedTime = 0;
4390
+ /** @property {number} - Timestamp for audio context when started */
4391
+ this.startTime = undefined;
4392
+ /** @property {GainNode} - Gain node for the sound */
4393
+ this.gainNode = undefined;
4394
+ /** @property {AudioBufferSourceNode} - Source node of the audio */
4395
+ this.source = undefined;
4396
+ // setup end callback and start sound
4397
+ this.onendedCallback = (source)=>
4398
+ {
4399
+ if (source === this.source)
4400
+ this.source = undefined;
4401
+ };
4402
+ if (!paused)
4403
+ this.start();
4404
+ }
4405
+
4406
+ /** Start playing the sound instance from the offset time
4407
+ * @param {number} [offset] - Offset in seconds to start playback from
4408
+ */
4409
+ start(offset=0)
4410
+ {
4411
+ ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
4412
+ if (this.isPlaying())
4413
+ this.stop();
4414
+ this.gainNode = audioContext.createGain();
4415
+ this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
4416
+ this.startTime = audioContext.currentTime - offset;
4417
+ this.pausedTime = undefined;
4418
+ }
4419
+
4420
+ /** Set the volume of this sound instance
4421
+ * @param {number} volume */
4422
+ setVolume(volume)
4423
+ {
4424
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
4425
+ this.volume = volume;
4426
+ if (this.gainNode)
4427
+ this.gainNode.gain.value = volume;
4428
+ }
4429
+
4430
+ /** Stop this sound instance and reset position to the start */
4431
+ stop(fadeTime=0)
4432
+ {
4433
+ ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
4434
+ if (this.isPlaying())
4435
+ {
4436
+ if (fadeTime)
4437
+ {
4438
+ // ramp off gain
4439
+ const startFade = audioContext.currentTime;
4440
+ const endFade = startFade + fadeTime;
4441
+ this.gainNode.gain.linearRampToValueAtTime(1, startFade);
4442
+ this.gainNode.gain.linearRampToValueAtTime(0, endFade);
4443
+ this.source.stop(endFade);
4444
+ }
4445
+ else
4446
+ this.source.stop();
4447
+ }
4448
+ this.pausedTime = 0;
4449
+ this.source = undefined;
4450
+ this.startTime = undefined;
4451
+ }
4452
+
4453
+ /** Pause this sound instance */
4454
+ pause()
4455
+ {
4456
+ if (this.isPaused())
4457
+ return;
4458
+
4459
+ // save current time and stop sound
4460
+ this.pausedTime = this.getCurrentTime();
4461
+ this.source.stop();
4462
+ this.source = undefined;
4463
+ this.startTime = undefined;
4464
+ }
4465
+
4466
+ /** Unpauses this sound instance */
4467
+ resume()
4468
+ {
4469
+ if (!this.isPaused())
4470
+ return;
4471
+
4472
+ // restart sound from paused time
4473
+ this.start(this.pausedTime);
4474
+ }
4475
+
4476
+ /** Check if this instance is currently playing
4477
+ * @return {boolean} - True if playing
4478
+ */
4479
+ isPlaying() { return !!this.source; }
4480
+
4481
+ /** Check if this instance is paused and was not stopped
4482
+ * @return {boolean} - True if paused
4483
+ */
4484
+ isPaused() { return !this.isPlaying(); }
4485
+
4486
+ /** Get the current playback time in seconds
4487
+ * @return {number} - Current playback time
4488
+ */
4489
+ getCurrentTime()
4248
4490
  {
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();
4491
+ const deltaTime = mod(audioContext.currentTime - this.startTime,
4492
+ this.getDuration());
4493
+ return this.isPlaying() ? deltaTime : this.pausedTime;
4258
4494
  }
4259
- }
4260
4495
 
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;
4496
+ /** Get the total duration of this sound
4497
+ * @return {number} - Total duration in seconds
4498
+ */
4499
+ getDuration() { return this.sound.getDuration() / this.rate; }
4270
4500
 
4271
- return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
4501
+ /** Get source of this sound instance
4502
+ * @return {AudioBufferSourceNode}
4503
+ */
4504
+ getSource() { return this.source; }
4272
4505
  }
4273
4506
 
4274
4507
  ///////////////////////////////////////////////////////////////////////////////
@@ -4322,9 +4555,11 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4322
4555
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
4323
4556
  * @param {number} [sampleRate=44100] - Sample rate for the sound
4324
4557
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
4558
+ * @param {number} [offset] - Offset in seconds to start playback from
4559
+ * @param {Function} [onended] - Callback for when the sound ends
4325
4560
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4326
4561
  * @memberof Audio */
4327
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR, gainNode)
4562
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
4328
4563
  {
4329
4564
  if (!soundEnable || headlessMode) return;
4330
4565
 
@@ -4349,16 +4584,20 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4349
4584
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
4350
4585
  source.connect(pannerNode).connect(gainNode);
4351
4586
 
4352
- // play the sound
4353
- if (audioContext.state != 'running')
4587
+ // callback when the sound ends
4588
+ if (onended)
4589
+ source.addEventListener('ended', ()=> onended(source));
4590
+
4591
+ if (!audioIsRunning())
4354
4592
  {
4355
- // fix stalled audio and play
4356
- audioContext.resume().then(()=>source.start());
4593
+ // fix stalled audio, this sound won't be able to play
4594
+ audioContext.resume();
4595
+ return;
4357
4596
  }
4358
- else
4359
- source.start();
4360
4597
 
4361
- // return sound
4598
+ // play and return sound
4599
+ const startOffset = offset * rate;
4600
+ source.start(0, startOffset);
4362
4601
  return source;
4363
4602
  }
4364
4603
 
@@ -4366,18 +4605,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4366
4605
  // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
4367
4606
 
4368
4607
  /** Generate and play a ZzFX sound
4369
- *
4608
+ *
4370
4609
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4371
4610
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
4372
4611
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4373
4612
  * @memberof Audio */
4374
4613
  function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
4375
4614
 
4376
- /** Sample rate used for all ZzFX sounds
4377
- * @default 44100
4378
- * @memberof Audio */
4379
- const zzfxR = 44100;
4380
-
4381
4615
  /** Generate samples for a ZzFX sound
4382
4616
  * @param {number} [volume] - Volume scale (percent)
4383
4617
  * @param {number} [randomness] - How much to randomize frequency (percent Hz)
@@ -4401,11 +4635,10 @@ const zzfxR = 44100;
4401
4635
  * @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
4402
4636
  * @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
4403
4637
  * @return {Array} - Array of audio samples
4404
- * @memberof Audio
4405
- */
4638
+ * @memberof Audio */
4406
4639
  function zzfxG
4407
4640
  (
4408
- volume = 1,
4641
+ volume = 1,
4409
4642
  randomness = .05,
4410
4643
  frequency = 220,
4411
4644
  attack = 0,
@@ -4413,11 +4646,11 @@ function zzfxG
4413
4646
  release = .1,
4414
4647
  shape = 0,
4415
4648
  shapeCurve = 1,
4416
- slide = 0,
4417
- deltaSlide = 0,
4418
- pitchJump = 0,
4419
- pitchJumpTime = 0,
4420
- repeatTime = 0,
4649
+ slide = 0,
4650
+ deltaSlide = 0,
4651
+ pitchJump = 0,
4652
+ pitchJumpTime = 0,
4653
+ repeatTime = 0,
4421
4654
  noise = 0,
4422
4655
  modulation = 0,
4423
4656
  bitCrush = 0,
@@ -4429,19 +4662,19 @@ function zzfxG
4429
4662
  )
4430
4663
  {
4431
4664
  // init parameters
4432
- let sampleRate = zzfxR,
4433
- PI2 = PI*2,
4665
+ let sampleRate = audioDefaultSampleRate,
4666
+ PI2 = PI*2,
4434
4667
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
4435
- startFrequency = frequency *=
4668
+ startFrequency = frequency *=
4436
4669
  (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
4437
- modOffset = 0, // modulation offset
4670
+ modOffset = 0, // modulation offset
4438
4671
  repeat = 0, // repeat offset
4439
4672
  crush = 0, // bit crush offset
4440
4673
  jump = 1, // pitch jump timer
4441
4674
  length, // sample length
4442
4675
  b = [], // sample buffer
4443
4676
  t = 0, // sample time
4444
- i = 0, // sample index
4677
+ i = 0, // sample index
4445
4678
  s = 0, // sample value
4446
4679
  f, // wave frequency
4447
4680
 
@@ -4449,7 +4682,7 @@ function zzfxG
4449
4682
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
4450
4683
  cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
4451
4684
  a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
4452
- b0 = (1 + sign(filter) * cos) / 2 / a0,
4685
+ b0 = (1 + sign(filter) * cos) / 2 / a0,
4453
4686
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
4454
4687
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
4455
4688
 
@@ -4495,7 +4728,7 @@ function zzfxG
4495
4728
  0); // post release
4496
4729
 
4497
4730
  s = delay ? s/2 + (delay > i ? 0 : // delay
4498
- (i<length-delay? 1 : (length-i)/delay) * // release delay
4731
+ (i<length-delay? 1 : (length-i)/delay) * // release delay
4499
4732
  b[i-delay|0]/2/volume) : s; // sample delay
4500
4733
 
4501
4734
  if (filter) // apply filter
@@ -4507,14 +4740,14 @@ function zzfxG
4507
4740
  t += f + f*noise*Math.sin(i**5); // noise
4508
4741
 
4509
4742
  if (jump && ++jump > pitchJumpTime) // pitch jump
4510
- {
4743
+ {
4511
4744
  frequency += pitchJump; // apply pitch jump
4512
4745
  startFrequency += pitchJump; // also apply to start
4513
4746
  jump = 0; // stop pitch jump time
4514
- }
4747
+ }
4515
4748
 
4516
4749
  if (repeatTime && !(++repeat % repeatTime)) // repeat
4517
- {
4750
+ {
4518
4751
  frequency = startFrequency; // reset frequency
4519
4752
  slide = startSlide; // reset slide
4520
4753
  jump ||= 1; // reset pitch jump time
@@ -4523,7 +4756,7 @@ function zzfxG
4523
4756
 
4524
4757
  return b; // return sample buffer
4525
4758
  }
4526
- /**
4759
+ /**
4527
4760
  * LittleJS Tile Layer System
4528
4761
  * - Caches arrays of tiles to off screen canvas for fast rendering
4529
4762
  * - Unlimited numbers of layers, allocates canvases as needed
@@ -4537,9 +4770,9 @@ function zzfxG
4537
4770
  // Tile Layer System
4538
4771
 
4539
4772
  /** Keep track of all tile layers with collision
4540
- * @type {Array<TileCollisionLayer>}
4773
+ * @type {Array<TileCollisionLayer>}
4541
4774
  * @memberof TileCollision */
4542
- let tileCollisionLayers = [];
4775
+ const tileCollisionLayers = [];
4543
4776
 
4544
4777
  /** Get tile collision data for a given cell in the grid
4545
4778
  * @param {Vector2} pos
@@ -4593,7 +4826,7 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
4593
4826
  }
4594
4827
 
4595
4828
  ///////////////////////////////////////////////////////////////////////////////
4596
- /**
4829
+ /**
4597
4830
  * Load tile layers from exported data
4598
4831
  * @param {Object} tileMapData - Level data from exported data
4599
4832
  * @param {TileInfo} [tileInfo] - Default tile info (used for size and texture)
@@ -4626,13 +4859,13 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
4626
4859
  {
4627
4860
  const dataLayer = tileMapData.layers[layerIndex];
4628
4861
  ASSERT(dataLayer.data && dataLayer.data.length);
4629
- ASSERT(levelSize.area() == dataLayer.data.length);
4862
+ ASSERT(levelSize.area() === dataLayer.data.length);
4630
4863
 
4631
4864
  const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
4632
4865
  const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
4633
4866
  tileLayers[layerIndex] = tileLayer;
4634
4867
 
4635
- for (let x=levelSize.x; x--;)
4868
+ for (let x=levelSize.x; x--;)
4636
4869
  for (let y=levelSize.y; y--;)
4637
4870
  {
4638
4871
  const pos = vec2(x, levelSize.y-1-y);
@@ -4691,7 +4924,7 @@ class TileLayerData
4691
4924
  /**
4692
4925
  * Canvas Layer - cached off screen rendering system
4693
4926
  * - Contains an offscreen canvas that can be rendered to
4694
- * - Webgl rendering is optional, call useWebGL to enable
4927
+ * - WebGL rendering is optional, call useWebGL to enable
4695
4928
  * @extends EngineObject
4696
4929
  * @example
4697
4930
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
@@ -4713,18 +4946,18 @@ class CanvasLayer extends EngineObject
4713
4946
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
4714
4947
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
4715
4948
  this.context = headlessMode ? undefined : this.canvas.getContext('2d');
4716
- /** @property {WebGLTexture} - Texture if using webgl for this layer, call useWebGL to enable */
4949
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
4717
4950
  this.glTexture = undefined;
4718
4951
  this.gravityScale = 0; // disable gravity by default for canvas layers
4719
4952
  }
4720
-
4953
+
4721
4954
  /** Destroy this canvas layer */
4722
4955
  destroy()
4723
4956
  {
4724
4957
  if (this.destroyed)
4725
4958
  return;
4726
4959
 
4727
- // free up the webgl texture
4960
+ // free up the WebGL texture
4728
4961
  if (this.glTexture)
4729
4962
  glDeleteTexture(this.glTexture);
4730
4963
  super.destroy();
@@ -4749,12 +4982,12 @@ class CanvasLayer extends EngineObject
4749
4982
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
4750
4983
  {
4751
4984
  // draw the canvas layer as a single tile that uses the whole texture
4752
- const useWebgl = glEnable && this.glTexture != undefined;
4985
+ const useWebGL = glEnable && this.glTexture !== undefined;
4753
4986
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
4754
- drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebgl, screenSpace, context);
4987
+ drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
4755
4988
  }
4756
4989
 
4757
- /** Draw onto the layer canvas in world space (bypass webgl)
4990
+ /** Draw onto the layer canvas in world space (bypass WebGL)
4758
4991
  * @param {Vector2} pos
4759
4992
  * @param {Vector2} size
4760
4993
  * @param {number} angle
@@ -4788,8 +5021,8 @@ class CanvasLayer extends EngineObject
4788
5021
  if (textureInfo)
4789
5022
  {
4790
5023
  context.globalAlpha = color.a; // only alpha is supported
4791
- context.drawImage(textureInfo.image,
4792
- tileInfo.pos.x, tileInfo.pos.y,
5024
+ context.drawImage(textureInfo.image,
5025
+ tileInfo.pos.x, tileInfo.pos.y,
4793
5026
  tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
4794
5027
  context.globalAlpha = 1;
4795
5028
  }
@@ -4807,11 +5040,11 @@ class CanvasLayer extends EngineObject
4807
5040
  * @param {Vector2} [size=(1,1)]
4808
5041
  * @param {Color} [color=(1,1,1,1)]
4809
5042
  * @param {number} [angle=0] */
4810
- drawRect(pos, size, color, angle)
5043
+ drawRect(pos, size, color, angle)
4811
5044
  { this.drawTile(pos, size, undefined, color, angle); }
4812
5045
 
4813
- /** Create or update the webgl texture for this layer
4814
- * @param {boolean} [enable] - enable webgl rendering and update the texture */
5046
+ /** Create or update the WebGL texture for this layer
5047
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture */
4815
5048
  useWebGL(enable=true)
4816
5049
  {
4817
5050
  if (glEnable && enable)
@@ -4857,7 +5090,7 @@ class TileLayer extends CanvasLayer
4857
5090
  this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
4858
5091
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
4859
5092
  this.context = this.canvas.getContext('2d');
4860
- /** @property {WebGLTexture} - Texture if using webgl for this layer */
5093
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer */
4861
5094
  this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
4862
5095
  // set no friction by default, applied friction is max of both objects
4863
5096
  this.friction = 0;
@@ -4882,7 +5115,7 @@ class TileLayer extends CanvasLayer
4882
5115
  }
4883
5116
  }
4884
5117
 
4885
- /** Set data at a given position in the array
5118
+ /** Set data at a given position in the array
4886
5119
  * @param {Vector2} layerPos - Local position in array
4887
5120
  * @param {TileLayerData} data - Data to set
4888
5121
  * @param {boolean} [redraw] - Force the tile to redraw if true */
@@ -4894,26 +5127,26 @@ class TileLayer extends CanvasLayer
4894
5127
  redraw && this.drawTileData(layerPos);
4895
5128
  }
4896
5129
  }
4897
-
4898
- /** Get data at a given position in the array
5130
+
5131
+ /** Get data at a given position in the array
4899
5132
  * @param {Vector2} layerPos - Local position in array
4900
5133
  * @return {TileLayerData} */
4901
5134
  getData(layerPos)
4902
5135
  { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
4903
-
5136
+
4904
5137
  // Render the tile layer, called automatically by the engine
4905
5138
  render()
4906
5139
  {
4907
- ASSERT(drawContext != this.context, 'must call redrawEnd() after drawing tiles!');
4908
-
5140
+ ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5141
+
4909
5142
  // draw the tile layer as a single tile
4910
5143
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
4911
5144
  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);
5145
+ const useWebGL = glEnable && this.glTexture !== undefined;
5146
+ drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
4914
5147
  }
4915
5148
 
4916
- /** Draw all the tile data to an offscreen canvas
5149
+ /** Draw all the tile data to an offscreen canvas
4917
5150
  * - This may be slow in some browsers but only needs to be done once */
4918
5151
  redraw()
4919
5152
  {
@@ -4923,7 +5156,7 @@ class TileLayer extends CanvasLayer
4923
5156
  this.drawTileData(vec2(x,y), false);
4924
5157
  this.redrawEnd();
4925
5158
  if (this.glTexture)
4926
- this.useWebGL(); // update webgl texture
5159
+ this.useWebGL(); // update WebGL texture
4927
5160
  }
4928
5161
 
4929
5162
  /** Call to start the redraw process
@@ -4959,7 +5192,7 @@ class TileLayer extends CanvasLayer
4959
5192
  /** Call to end the redraw process */
4960
5193
  redrawEnd()
4961
5194
  {
4962
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5195
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
4963
5196
  glCopyToContext(drawContext);
4964
5197
  //debugSaveCanvas(this.canvas);
4965
5198
 
@@ -4970,7 +5203,7 @@ class TileLayer extends CanvasLayer
4970
5203
  /** Draw the tile at a given position in the tile grid
4971
5204
  * This can be used to clear out tiles when they are destroyed
4972
5205
  * Tiles can also be redrawn if inside a redrawStart/End block
4973
- * @param {Vector2} layerPos
5206
+ * @param {Vector2} layerPos
4974
5207
  * @param {boolean} [clear] - should the old tile be cleared out
4975
5208
  */
4976
5209
  drawTileData(layerPos, clear=true)
@@ -4985,9 +5218,9 @@ class TileLayer extends CanvasLayer
4985
5218
 
4986
5219
  // draw the tile if it has layer data
4987
5220
  const d = this.getData(layerPos);
4988
- if (d.tile != undefined)
5221
+ if (d.tile !== undefined)
4989
5222
  {
4990
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5223
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
4991
5224
  const pos = layerPos.add(vec2(.5));
4992
5225
  const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
4993
5226
  drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
@@ -5000,7 +5233,7 @@ class TileLayer extends CanvasLayer
5000
5233
  * Tile Collision Layer - a tile layer with collision
5001
5234
  * - adds collision data and functions to TileLayer
5002
5235
  * - there can be multiple tile collision layers
5003
- * - tile collison layers should not overlap each other
5236
+ * - tile collision layers should not overlap each other
5004
5237
  * @extends TileLayer
5005
5238
  */
5006
5239
  class TileCollisionLayer extends TileLayer
@@ -5151,7 +5384,7 @@ class TileCollisionLayer extends TileLayer
5151
5384
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
5152
5385
  }
5153
5386
  }
5154
- /**
5387
+ /**
5155
5388
  * LittleJS Particle System
5156
5389
  */
5157
5390
 
@@ -5168,7 +5401,7 @@ class TileCollisionLayer extends TileLayer
5168
5401
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
5169
5402
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
5170
5403
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5171
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5404
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5172
5405
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
5173
5406
  * );
5174
5407
  */
@@ -5176,35 +5409,35 @@ class ParticleEmitter extends EngineObject
5176
5409
  {
5177
5410
  /** Create a particle system with the given settings
5178
5411
  * @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
5412
+ * @param {number} [angle] - Angle to emit the particles
5413
+ * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
5414
+ * @param {number} [emitTime] - How long to stay alive (0 is forever)
5415
+ * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
5416
+ * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5184
5417
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
5185
5418
  * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
5186
5419
  * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
5187
5420
  * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
5188
5421
  * @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
5422
+ * @param {number} [particleTime] - How long particles live
5423
+ * @param {number} [sizeStart] - How big are particles at start
5424
+ * @param {number} [sizeEnd] - How big are particles at end
5425
+ * @param {number} [speed] - How fast are particles when spawned
5426
+ * @param {number} [angleSpeed] - How fast are particles rotating
5427
+ * @param {number} [damping] - How much to dampen particle speed
5428
+ * @param {number} [angleDamping] - How much to dampen particle angular speed
5429
+ * @param {number} [gravityScale] - How much gravity effect particles
5430
+ * @param {number} [particleConeAngle] - Cone for start particle angle
5431
+ * @param {number} [fadeRate] - How quick to fade particles at start/end in percent of life
5432
+ * @param {number} [randomness] - Apply extra randomness percent
5200
5433
  * @param {boolean} [collideTiles] - Do particles collide against tiles
5201
5434
  * @param {boolean} [additive] - Should particles use additive blend
5202
5435
  * @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)
5436
+ * @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
5204
5437
  * @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
5205
5438
  */
5206
5439
  constructor
5207
- (
5440
+ (
5208
5441
  position,
5209
5442
  angle,
5210
5443
  emitSize = 0,
@@ -5226,7 +5459,7 @@ class ParticleEmitter extends EngineObject
5226
5459
  gravityScale = 0,
5227
5460
  particleConeAngle = PI,
5228
5461
  fadeRate = .1,
5229
- randomness = .2,
5462
+ randomness = .2,
5230
5463
  collideTiles = false,
5231
5464
  additive = false,
5232
5465
  randomColorLinear = true,
@@ -5237,13 +5470,13 @@ class ParticleEmitter extends EngineObject
5237
5470
  super(position, vec2(), tileInfo, angle, undefined, renderOrder);
5238
5471
 
5239
5472
  // emitter settings
5240
- /** @property {Number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5473
+ /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5241
5474
  this.emitSize = emitSize
5242
- /** @property {Number} - How long to stay alive (0 is forever) */
5475
+ /** @property {number} - How long to stay alive (0 is forever) */
5243
5476
  this.emitTime = emitTime;
5244
- /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
5477
+ /** @property {number} - How many particles per second to spawn, does not emit if 0 */
5245
5478
  this.emitRate = emitRate;
5246
- /** @property {Number} - Local angle to apply velocity to particles from emitter */
5479
+ /** @property {number} - Local angle to apply velocity to particles from emitter */
5247
5480
  this.emitConeAngle = emitConeAngle;
5248
5481
 
5249
5482
  // color settings
@@ -5259,27 +5492,27 @@ class ParticleEmitter extends EngineObject
5259
5492
  this.randomColorLinear = randomColorLinear;
5260
5493
 
5261
5494
  // particle settings
5262
- /** @property {Number} - How long particles live */
5495
+ /** @property {number} - How long particles live */
5263
5496
  this.particleTime = particleTime;
5264
- /** @property {Number} - How big are particles at start */
5497
+ /** @property {number} - How big are particles at start */
5265
5498
  this.sizeStart = sizeStart;
5266
- /** @property {Number} - How big are particles at end */
5499
+ /** @property {number} - How big are particles at end */
5267
5500
  this.sizeEnd = sizeEnd;
5268
- /** @property {Number} - How fast are particles when spawned */
5501
+ /** @property {number} - How fast are particles when spawned */
5269
5502
  this.speed = speed;
5270
- /** @property {Number} - How fast are particles rotating */
5503
+ /** @property {number} - How fast are particles rotating */
5271
5504
  this.angleSpeed = angleSpeed;
5272
- /** @property {Number} - How much to dampen particle speed */
5505
+ /** @property {number} - How much to dampen particle speed */
5273
5506
  this.damping = damping;
5274
- /** @property {Number} - How much to dampen particle angular speed */
5507
+ /** @property {number} - How much to dampen particle angular speed */
5275
5508
  this.angleDamping = angleDamping;
5276
- /** @property {Number} - How much does gravity effect particles */
5509
+ /** @property {number} - How much gravity affects particles */
5277
5510
  this.gravityScale = gravityScale;
5278
- /** @property {Number} - Cone for start particle angle */
5511
+ /** @property {number} - Cone for start particle angle */
5279
5512
  this.particleConeAngle = particleConeAngle;
5280
- /** @property {Number} - How quick to fade in particles at start/end in percent of life */
5513
+ /** @property {number} - How quick to fade in particles at start/end in percent of life */
5281
5514
  this.fadeRate = fadeRate;
5282
- /** @property {Number} - Apply extra randomness percent */
5515
+ /** @property {number} - Apply extra randomness percent */
5283
5516
  this.randomness = randomness;
5284
5517
  /** @property {boolean} - Do particles collide against tiles */
5285
5518
  this.collideTiles = collideTiles;
@@ -5287,16 +5520,16 @@ class ParticleEmitter extends EngineObject
5287
5520
  this.additive = additive;
5288
5521
  /** @property {boolean} - Should it be in local space of emitter */
5289
5522
  this.localSpace = localSpace;
5290
- /** @property {Number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5523
+ /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5291
5524
  this.trailScale = 0;
5292
5525
  /** @property {Function} - Callback when particle is destroyed */
5293
5526
  this.particleDestroyCallback = undefined;
5294
5527
  /** @property {Function} - Callback when particle is created */
5295
5528
  this.particleCreateCallback = undefined;
5296
- /** @property {Number} - Track particle emit time */
5529
+ /** @property {number} - Track particle emit time */
5297
5530
  this.emitTimeBuffer = 0;
5298
5531
  }
5299
-
5532
+
5300
5533
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
5301
5534
  update()
5302
5535
  {
@@ -5320,7 +5553,7 @@ class ParticleEmitter extends EngineObject
5320
5553
  if (debugParticles)
5321
5554
  {
5322
5555
  // show emitter bounds
5323
- const emitSize = typeof this.emitSize == 'number' ? vec2(this.emitSize) : this.emitSize;
5556
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
5324
5557
  debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
5325
5558
  }
5326
5559
  }
@@ -5330,7 +5563,7 @@ class ParticleEmitter extends EngineObject
5330
5563
  emitParticle()
5331
5564
  {
5332
5565
  // spawn a particle
5333
- let pos = typeof this.emitSize == 'number' ? // check if number was used
5566
+ let pos = typeof this.emitSize === 'number' ? // check if number was used
5334
5567
  randInCircle(this.emitSize/2) // circle emitter
5335
5568
  : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
5336
5569
  .multiply(this.emitSize).rotate(this.angle)
@@ -5355,7 +5588,7 @@ class ParticleEmitter extends EngineObject
5355
5588
  const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
5356
5589
  const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
5357
5590
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
5358
-
5591
+
5359
5592
  // build particle
5360
5593
  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
5594
  particle.velocity = vec2().setAngle(velocityAngle, speed);
@@ -5400,38 +5633,38 @@ class Particle extends EngineObject
5400
5633
  * Typically this is created automatically by a ParticleEmitter
5401
5634
  * @param {Vector2} position - World space position of the particle
5402
5635
  * @param {TileInfo} tileInfo - Tile info to render particles
5403
- * @param {Number} angle - Angle to rotate the particle
5636
+ * @param {number} angle - Angle to rotate the particle
5404
5637
  * @param {Color} colorStart - Color at start of life
5405
5638
  * @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
5639
+ * @param {number} lifeTime - How long to live for
5640
+ * @param {number} sizeStart - Size at start of life
5641
+ * @param {number} sizeEnd - Size at end of life
5642
+ * @param {number} fadeRate - How quick to fade in/out
5410
5643
  * @param {boolean} additive - Does it use additive blend mode
5411
- * @param {Number} trailScale - If a trail, how long to make it
5644
+ * @param {number} trailScale - If a trail, how long to make it
5412
5645
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
5413
5646
  * @param {Function} [destroyCallback] - Callback when particle dies
5414
5647
  */
5415
5648
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
5416
5649
  )
5417
- {
5418
- super(position, vec2(), tileInfo, angle);
5419
-
5650
+ {
5651
+ super(position, vec2(), tileInfo, angle);
5652
+
5420
5653
  /** @property {Color} - Color at start of life */
5421
5654
  this.colorStart = colorStart;
5422
5655
  /** @property {Color} - Calculated change in color */
5423
5656
  this.colorEndDelta = colorEnd.subtract(colorStart);
5424
- /** @property {Number} - How long to live for */
5657
+ /** @property {number} - How long to live for */
5425
5658
  this.lifeTime = lifeTime;
5426
- /** @property {Number} - Size at start of life */
5659
+ /** @property {number} - Size at start of life */
5427
5660
  this.sizeStart = sizeStart;
5428
- /** @property {Number} - Calculated change in size */
5661
+ /** @property {number} - Calculated change in size */
5429
5662
  this.sizeEndDelta = sizeEnd - sizeStart;
5430
- /** @property {Number} - How quick to fade in/out */
5663
+ /** @property {number} - How quick to fade in/out */
5431
5664
  this.fadeRate = fadeRate;
5432
5665
  /** @property {boolean} - Is it additive */
5433
5666
  this.additive = additive;
5434
- /** @property {Number} - If a trail, how long to make it */
5667
+ /** @property {number} - If a trail, how long to make it */
5435
5668
  this.trailScale = trailScale;
5436
5669
  /** @property {ParticleEmitter} - Parent emitter if local space */
5437
5670
  this.localSpaceEmitter = localSpaceEmitter;
@@ -5471,7 +5704,7 @@ class Particle extends EngineObject
5471
5704
  this.colorStart.r + p * this.colorEndDelta.r,
5472
5705
  this.colorStart.g + p * this.colorEndDelta.g,
5473
5706
  this.colorStart.b + p * this.colorEndDelta.b,
5474
- (this.colorStart.a + p * this.colorEndDelta.a) *
5707
+ (this.colorStart.a + p * this.colorEndDelta.a) *
5475
5708
  (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
5476
5709
 
5477
5710
  // draw the particle
@@ -5481,7 +5714,7 @@ class Particle extends EngineObject
5481
5714
  if (this.localSpaceEmitter)
5482
5715
  {
5483
5716
  // in local space of emitter
5484
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
5717
+ pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
5485
5718
  angle += this.localSpaceEmitter.angle;
5486
5719
  }
5487
5720
  if (this.trailScale)
@@ -5505,7 +5738,7 @@ class Particle extends EngineObject
5505
5738
  this.additive && setBlendMode();
5506
5739
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
5507
5740
 
5508
- if (p == 1)
5741
+ if (p === 1)
5509
5742
  {
5510
5743
  // destroy particle when it's time runs out
5511
5744
  this.color = color;
@@ -5515,7 +5748,7 @@ class Particle extends EngineObject
5515
5748
  }
5516
5749
  }
5517
5750
  }
5518
- /**
5751
+ /**
5519
5752
  * LittleJS Medal System
5520
5753
  * - Tracks and displays medals
5521
5754
  * - Saves medals to local storage
@@ -5536,7 +5769,7 @@ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
5536
5769
  /** Initialize medals with a save name used for storage
5537
5770
  * - Call this after creating all medals
5538
5771
  * - Checks if medals are unlocked
5539
- * @param {String} saveName
5772
+ * @param {string} saveName
5540
5773
  * @memberof Medals */
5541
5774
  function medalsInit(saveName)
5542
5775
  {
@@ -5551,7 +5784,7 @@ function medalsInit(saveName)
5551
5784
  {
5552
5785
  if (!medalsDisplayQueue.length)
5553
5786
  return;
5554
-
5787
+
5555
5788
  // update first medal in queue
5556
5789
  const medal = medalsDisplayQueue[0];
5557
5790
  const time = timeReal - medalsDisplayTimeLast;
@@ -5566,7 +5799,7 @@ function medalsInit(saveName)
5566
5799
  {
5567
5800
  // slide on/off medals
5568
5801
  const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
5569
- const hidePercent =
5802
+ const hidePercent =
5570
5803
  time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
5571
5804
  time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
5572
5805
  medal.render(hidePercent);
@@ -5582,43 +5815,43 @@ function medalsForEach(callback)
5582
5815
 
5583
5816
  ///////////////////////////////////////////////////////////////////////////////
5584
5817
 
5585
- /**
5586
- * Medal - Tracks an unlockable medal
5818
+ /**
5819
+ * Medal - Tracks an unlockable medal
5587
5820
  * @example
5588
5821
  * // create a medal
5589
5822
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
5590
- *
5823
+ *
5591
5824
  * // initialize medals
5592
5825
  * medalsInit('Example Game');
5593
- *
5826
+ *
5594
5827
  * // unlock the medal
5595
5828
  * medal_example.unlock();
5596
5829
  */
5597
5830
  class Medal
5598
5831
  {
5599
5832
  /** 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
5833
+ * @param {number} id - The unique identifier of the medal
5834
+ * @param {string} name - Name of the medal
5835
+ * @param {string} [description] - Description of the medal
5836
+ * @param {string} [icon] - Icon for the medal
5837
+ * @param {string} [src] - Image location for the medal
5605
5838
  */
5606
5839
  constructor(id, name, description='', icon='🏆', src)
5607
5840
  {
5608
5841
  ASSERT(id >= 0 && !medals[id]);
5609
-
5610
- /** @property {Number} - The unique identifier of the medal */
5842
+
5843
+ /** @property {number} - The unique identifier of the medal */
5611
5844
  this.id = id;
5612
-
5613
- /** @property {String} - Name of the medal */
5845
+
5846
+ /** @property {string} - Name of the medal */
5614
5847
  this.name = name;
5615
-
5616
- /** @property {String} - Description of the medal */
5848
+
5849
+ /** @property {string} - Description of the medal */
5617
5850
  this.description = description;
5618
-
5619
- /** @property {String} - Icon for the medal */
5851
+
5852
+ /** @property {string} - Icon for the medal */
5620
5853
  this.icon = icon;
5621
-
5854
+
5622
5855
  /** @property {boolean} - Is the medal unlocked? */
5623
5856
  this.unlocked = false;
5624
5857
 
@@ -5643,7 +5876,7 @@ class Medal
5643
5876
  }
5644
5877
 
5645
5878
  /** Render a medal
5646
- * @param {Number} [hidePercent] - How much to slide the medal off screen
5879
+ * @param {number} [hidePercent] - How much to slide the medal off screen
5647
5880
  */
5648
5881
  render(hidePercent=0)
5649
5882
  {
@@ -5684,7 +5917,7 @@ class Medal
5684
5917
 
5685
5918
  /** Render the icon for a medal
5686
5919
  * @param {Vector2} pos - Screen space position
5687
- * @param {Number} size - Screen space size
5920
+ * @param {number} size - Screen space size
5688
5921
  */
5689
5922
  renderIcon(pos, size)
5690
5923
  {
@@ -5694,14 +5927,14 @@ class Medal
5694
5927
  else
5695
5928
  drawTextScreen(this.icon, pos, size*.7, BLACK);
5696
5929
  }
5697
-
5930
+
5698
5931
  // Get local storage key used by the medal
5699
5932
  storageKey() { return medalsSaveName + '_' + this.id; }
5700
5933
  }
5701
5934
  /**
5702
5935
  * 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
5936
+ * - All WebGL used by the engine is wrapped up here
5937
+ * - Will fall back to 2D canvas rendering if WebGL is not supported
5705
5938
  * - For normal stuff you won't need to see or call anything in this file
5706
5939
  * - For advanced stuff there are helper functions to create shaders, textures, etc
5707
5940
  * - Can be disabled with glEnable to revert to 2D canvas rendering
@@ -5716,24 +5949,27 @@ class Medal
5716
5949
  * @memberof WebGL */
5717
5950
  let glCanvas;
5718
5951
 
5719
- /** 2d context for glCanvas
5952
+ /** WebGL2 context for `glCanvas`
5720
5953
  * @type {WebGL2RenderingContext}
5721
5954
  * @memberof WebGL */
5722
5955
  let glContext;
5723
5956
 
5724
- /** Should webgl be setup with anti-aliasing? must be set before calling engineInit
5957
+ /** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
5725
5958
  * @type {boolean}
5726
5959
  * @memberof WebGL */
5727
5960
  let glAntialias = true;
5728
5961
 
5729
5962
  // WebGL internal variables not exposed to documentation
5730
- let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive;
5963
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
5731
5964
 
5732
- // WebGL internal constants
5733
- const gl_MAX_INSTANCES = 1e4;
5965
+ // WebGL internal constants
5966
+ const gl_ARRAY_BUFFER_SIZE = 4e5;
5734
5967
  const gl_INDICES_PER_INSTANCE = 11;
5735
5968
  const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
5736
- const gl_INSTANCE_BUFFER_SIZE = gl_MAX_INSTANCES * gl_INSTANCE_BYTE_STRIDE;
5969
+ const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
5970
+ const gl_INDICES_PER_POLY_VERTEX = 3;
5971
+ const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
5972
+ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
5737
5973
 
5738
5974
  ///////////////////////////////////////////////////////////////////////////////
5739
5975
 
@@ -5754,11 +5990,11 @@ function glInit()
5754
5990
  return;
5755
5991
  }
5756
5992
 
5757
- // create the webgl canvas
5993
+ // create the WebGL canvas
5758
5994
  const rootElement = mainCanvas.parentElement;
5759
5995
  rootElement.appendChild(glCanvas);
5760
5996
 
5761
- // setup vertex and fragment shaders
5997
+ // setup instanced rendering shader program
5762
5998
  glShader = glCreateProgram(
5763
5999
  '#version 300 es\n' + // specify GLSL ES version
5764
6000
  'precision highp float;'+ // use highp for better accuracy
@@ -5786,40 +6022,59 @@ function glInit()
5786
6022
  '}' // end of shader
5787
6023
  );
5788
6024
 
6025
+ // setup poly rendering shaders
6026
+ glPolyShader = glCreateProgram(
6027
+ '#version 300 es\n' + // specify GLSL ES version
6028
+ 'precision highp float;'+ // use highp for better accuracy
6029
+ 'uniform mat4 m;'+ // transform matrix
6030
+ 'in vec2 p;'+ // in: position
6031
+ 'in vec4 c;'+ // in: color
6032
+ 'out vec4 d;'+ // out: color
6033
+ 'void main(){'+ // shader entry point
6034
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
6035
+ 'd=c;'+ // pass color to fragment shader
6036
+ '}' // end of shader
6037
+ ,
6038
+ '#version 300 es\n' + // specify GLSL ES version
6039
+ 'precision highp float;'+ // use highp for better accuracy
6040
+ 'in vec4 d;'+ // in: color
6041
+ 'out vec4 c;'+ // out: color
6042
+ 'void main(){'+ // shader entry point
6043
+ 'c=d;'+ // set color
6044
+ '}' // end of shader
6045
+ );
6046
+
5789
6047
  // init buffers
5790
- const glInstanceData = new ArrayBuffer(gl_INSTANCE_BUFFER_SIZE);
6048
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
5791
6049
  glPositionData = new Float32Array(glInstanceData);
5792
6050
  glColorData = new Uint32Array(glInstanceData);
5793
6051
  glArrayBuffer = glContext.createBuffer();
5794
6052
  glGeometryBuffer = glContext.createBuffer();
5795
6053
 
5796
6054
  // create the geometry buffer, triangle strip square
5797
- const geometry = new Float32Array([glInstanceCount=0,0,1,0,0,1,1,1]);
6055
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
5798
6056
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
5799
6057
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
5800
6058
  }
5801
6059
 
5802
- // Setup webgl render each frame, called automatically by engine
5803
- // Also used by tile layer rendering when redrawing tiles
5804
- function glPreRender()
6060
+ function glSetInstancedMode(force=false)
5805
6061
  {
5806
- if (!glEnable || !glContext) return;
5807
-
5808
- // set up the shader and canvas
5809
- glClearCanvas();
6062
+ if (!glPolyMode && !force)
6063
+ return;
6064
+
6065
+ // setup instanced mode
6066
+ glFlush();
6067
+ glPolyMode = false;
5810
6068
  glContext.useProgram(glShader);
5811
- glContext.activeTexture(glContext.TEXTURE0);
5812
- if (textureInfos[0])
5813
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
5814
6069
 
5815
6070
  // set vertex attributes
5816
- let offset = glAdditive = glBatchAdditive = 0;
6071
+ let offset = 0;
5817
6072
  const initVertexAttribArray = (name, type, typeSize, size)=>
5818
6073
  {
5819
6074
  const location = glContext.getAttribLocation(glShader, name);
5820
6075
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
5821
6076
  const divisor = typeSize && 1; // only if not geometry
5822
- const normalize = typeSize == 1; // only if color
6077
+ const normalize = typeSize === 1; // only if color
5823
6078
  glContext.enableVertexAttribArray(location);
5824
6079
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
5825
6080
  glContext.vertexAttribDivisor(location, divisor);
@@ -5828,26 +6083,84 @@ function glPreRender()
5828
6083
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
5829
6084
  initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
5830
6085
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
5831
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_INSTANCE_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6086
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
5832
6087
  initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
5833
6088
  initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
5834
6089
  initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
5835
6090
  initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
5836
6091
  initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
6092
+ }
6093
+
6094
+ function glSetPolyMode()
6095
+ {
6096
+ if (glPolyMode)
6097
+ return;
5837
6098
 
6099
+ // setup poly mode
6100
+ glFlush();
6101
+ glPolyMode = true;
6102
+ glContext.useProgram(glPolyShader);
6103
+
6104
+ // set vertex attributes
6105
+ let offset = 0;
6106
+ const initVertexAttribArray = (name, type, typeSize, size)=>
6107
+ {
6108
+ const location = glContext.getAttribLocation(glPolyShader, name);
6109
+ const normalize = typeSize === 1; // only normalize if color
6110
+ const stride = gl_POLY_VERTEX_BYTE_STRIDE;
6111
+ glContext.enableVertexAttribArray(location);
6112
+ glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
6113
+ glContext.vertexAttribDivisor(location, 0);
6114
+ offset += size*typeSize;
6115
+ }
6116
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
6117
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6118
+ initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
6119
+ initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
6120
+ }
6121
+
6122
+ // Setup WebGL render each frame, called automatically by engine
6123
+ // Also used by tile layer rendering when redrawing tiles
6124
+ function glPreRender()
6125
+ {
6126
+ if (!glEnable || !glContext) return;
6127
+
6128
+ // clear the canvas
6129
+ glClearCanvas();
6130
+
5838
6131
  // build the transform matrix
5839
6132
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
5840
6133
  const rotatedCam = cameraPos.rotate(-cameraAngle);
5841
6134
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
5842
6135
  const ca = Math.cos(cameraAngle);
5843
6136
  const sa = Math.sin(cameraAngle);
5844
- glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
5845
- [
6137
+ const transform = [
5846
6138
  s.x * ca, s.y * sa, 0, 0,
5847
6139
  -s.x * sa, s.y * ca, 0, 0,
5848
6140
  1, 1, 1, 0,
5849
- p.x, p.y, 0, 1
5850
- ]);
6141
+ p.x, p.y, 0, 1];
6142
+
6143
+ // set the same matrix for both shaders
6144
+ const initUniform = (program, uniform, value) =>
6145
+ {
6146
+ glContext.useProgram(program);
6147
+ const location = glContext.getUniformLocation(program, uniform);
6148
+ glContext.uniformMatrix4fv(location, false, value);
6149
+ }
6150
+ initUniform(glPolyShader, 'm', transform);
6151
+ initUniform(glShader, 'm', transform);
6152
+
6153
+ // set the active texture
6154
+ glContext.activeTexture(glContext.TEXTURE0);
6155
+ if (textureInfos[0])
6156
+ {
6157
+ glActiveTexture = textureInfos[0].glTexture;
6158
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
6159
+ }
6160
+
6161
+ // start in instanced rendering mode with additive blending off
6162
+ glAdditive = glBatchAdditive = glPolyMode = false;
6163
+ glSetInstancedMode(true);
5851
6164
  }
5852
6165
 
5853
6166
  /** Clear the canvas and setup the viewport
@@ -5855,21 +6168,21 @@ function glPreRender()
5855
6168
  function glClearCanvas()
5856
6169
  {
5857
6170
  if (!glContext) return;
5858
-
6171
+
5859
6172
  // clear and set to same size as main canvas
5860
6173
  glContext.viewport(0, 0, glCanvas.width=drawCanvas.width, glCanvas.height=drawCanvas.height);
5861
6174
  glContext.clear(glContext.COLOR_BUFFER_BIT);
5862
6175
  }
5863
6176
 
5864
- /** Set the WebGl texture, called automatically if using multiple textures
6177
+ /** Set the WebGL texture, called automatically if using multiple textures
5865
6178
  * - This may also flush the gl buffer resulting in more draw calls and worse performance
5866
6179
  * @param {WebGLTexture} texture
5867
- * @param {boolean} wrap - Should the texture wrap or clamp
6180
+ * @param {boolean} [wrap] - Should the texture wrap or clamp
5868
6181
  * @memberof WebGL */
5869
6182
  function glSetTexture(texture, wrap=false)
5870
6183
  {
5871
6184
  // must flush cache with the old texture to set a new one
5872
- if (!glContext || texture == glActiveTexture)
6185
+ if (!glContext || texture === glActiveTexture)
5873
6186
  return;
5874
6187
 
5875
6188
  glFlush();
@@ -5882,8 +6195,8 @@ function glSetTexture(texture, wrap=false)
5882
6195
  }
5883
6196
 
5884
6197
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
5885
- * @param {String} source
5886
- * @param {Number} type
6198
+ * @param {string} source
6199
+ * @param {number} type
5887
6200
  * @return {WebGLShader}
5888
6201
  * @memberof WebGL */
5889
6202
  function glCompileShader(source, type)
@@ -5902,8 +6215,8 @@ function glCompileShader(source, type)
5902
6215
  }
5903
6216
 
5904
6217
  /** Create WebGL program with given shaders
5905
- * @param {String} vsSource
5906
- * @param {String} fsSource
6218
+ * @param {string} vsSource
6219
+ * @param {string} fsSource
5907
6220
  * @return {WebGLProgram}
5908
6221
  * @memberof WebGL */
5909
6222
  function glCreateProgram(vsSource, fsSource)
@@ -5951,7 +6264,7 @@ function glCreateTexture(image)
5951
6264
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
5952
6265
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
5953
6266
  }
5954
-
6267
+
5955
6268
  // set texture filtering
5956
6269
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
5957
6270
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -5959,7 +6272,6 @@ function glCreateTexture(image)
5959
6272
  return texture;
5960
6273
  }
5961
6274
 
5962
-
5963
6275
  /** Deletes a WebGL texture
5964
6276
  * @param {WebGLTexture} [texture]
5965
6277
  * @memberof WebGL */
@@ -5987,18 +6299,23 @@ function glSetTextureData(texture, image)
5987
6299
  * @memberof WebGL */
5988
6300
  function glFlush()
5989
6301
  {
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;
6302
+ if (glEnable && glContext && glBatchCount)
6303
+ {
6304
+ // set bend mode
6305
+ const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
6306
+ glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
6307
+ glContext.enable(glContext.BLEND);
6308
+ glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData);
6309
+
6310
+ // draw the batch
6311
+ if (glPolyMode)
6312
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
6313
+ else
6314
+ glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
6315
+ if (debug || showWatermark)
6316
+ drawCount += glBatchCount;
6317
+ glBatchCount = 0;
6318
+ }
6002
6319
  glBatchAdditive = glAdditive;
6003
6320
  }
6004
6321
 
@@ -6014,7 +6331,8 @@ function glCopyToContext(context)
6014
6331
  context.drawImage(glCanvas, 0, 0);
6015
6332
  }
6016
6333
 
6017
- /** Set anti-aliasing for webgl canvas
6334
+ /** Set anti-aliasing for WebGL canvas
6335
+ * Must be called before engineInit
6018
6336
  * @param {boolean} [antialias]
6019
6337
  * @memberof WebGL */
6020
6338
  function glSetAntialias(antialias=true)
@@ -6024,25 +6342,27 @@ function glSetAntialias(antialias=true)
6024
6342
  }
6025
6343
 
6026
6344
  /** 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
6345
+ * @param {number} x
6346
+ * @param {number} y
6347
+ * @param {number} sizeX
6348
+ * @param {number} sizeY
6349
+ * @param {number} [angle]
6350
+ * @param {number} [uv0X]
6351
+ * @param {number} [uv0Y]
6352
+ * @param {number} [uv1X]
6353
+ * @param {number} [uv1Y]
6354
+ * @param {number} [rgba=-1] - white is -1
6355
+ * @param {number} [rgbaAdditive=0] - black is 0
6038
6356
  * @memberof WebGL */
6039
6357
  function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
6040
6358
  {
6041
6359
  // flush if there is not enough room or if different blend mode
6042
- if (glInstanceCount >= gl_MAX_INSTANCES || glBatchAdditive != glAdditive)
6360
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
6043
6361
  glFlush();
6362
+ glSetInstancedMode();
6044
6363
 
6045
- let offset = glInstanceCount++ * gl_INDICES_PER_INSTANCE;
6364
+ glPolyMode = false;
6365
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
6046
6366
  glPositionData[offset++] = x;
6047
6367
  glPositionData[offset++] = y;
6048
6368
  glPositionData[offset++] = sizeX;
@@ -6054,6 +6374,301 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
6054
6374
  glColorData[offset++] = rgba;
6055
6375
  glColorData[offset++] = rgbaAdditive;
6056
6376
  glPositionData[offset++] = angle;
6377
+ }
6378
+
6379
+ /** Transform and add a polygon to the gl draw list
6380
+ * @param {Array} points - Array of Vector2 points
6381
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6382
+ * @param {number} x
6383
+ * @param {number} y
6384
+ * @param {number} sx
6385
+ * @param {number} sy
6386
+ * @param {number} angle
6387
+ * @param {boolean} [tristrip] - should tristrip algorithm be used
6388
+ * @memberof WebGL */
6389
+ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
6390
+ {
6391
+ const pointsOut = [];
6392
+ for (const p of points)
6393
+ {
6394
+ // transform the point
6395
+ const px = p.x*sx;
6396
+ const py = p.y*sy;
6397
+ const sa = Math.sin(-angle);
6398
+ const ca = Math.cos(-angle);
6399
+ pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
6400
+ }
6401
+ const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
6402
+ glDrawPoints(drawPoints, rgba);
6403
+ }
6404
+
6405
+ /** Transform and add a polygon to the gl draw list
6406
+ * @param {Array} points - Array of Vector2 points
6407
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6408
+ * @param {number} lineWidth - Width of the outline
6409
+ * @param {number} x
6410
+ * @param {number} y
6411
+ * @param {number} sx
6412
+ * @param {number} sy
6413
+ * @param {number} angle
6414
+ * @memberof WebGL */
6415
+ function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle)
6416
+ {
6417
+ const outlinePoints = glMakeOutline(points, lineWidth);
6418
+ glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
6419
+ }
6420
+
6421
+ /** Add a polygon to the gl draw list
6422
+ * @param {Array} points - Array of Vector2 points in triangle strip order
6423
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6424
+ * @memberof WebGL */
6425
+ function glDrawPoints(points, rgba)
6426
+ {
6427
+ if (!glEnable || points.length < 3)
6428
+ return; // needs at least 3 points to have area
6429
+
6430
+ // add 2 degenerate verts if batching with existing polys to separate them
6431
+ const needsBridge = glPolyMode && glBatchCount > 0;
6432
+ const bridgeVerts = needsBridge ? 2 : 0;
6433
+ const vertCount = points.length + bridgeVerts;
6434
+
6435
+ // flush if there is not enough room or if different blend mode
6436
+ if (!glPolyMode || glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
6437
+ glFlush();
6438
+ glSetPolyMode();
6439
+
6440
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
6441
+
6442
+ // add degenerate bridge if needed (repeat last vertex of previous poly, then first of new poly)
6443
+ if (needsBridge)
6444
+ {
6445
+ // repeat last vertex from previous batch (it's at offset - 3)
6446
+ const prevOffset = offset - 3;
6447
+ glPositionData[offset++] = glPositionData[prevOffset];
6448
+ glPositionData[offset++] = glPositionData[prevOffset + 1];
6449
+ glColorData[offset++] = glColorData[prevOffset + 2];
6450
+
6451
+ // repeat first vertex of new poly
6452
+ glPositionData[offset++] = points[0].x;
6453
+ glPositionData[offset++] = points[0].y;
6454
+ glColorData[offset++] = rgba;
6455
+ }
6456
+
6457
+ // write vertices - they're already in triangle strip order
6458
+ for (const point of points)
6459
+ {
6460
+ glPositionData[offset++] = point.x;
6461
+ glPositionData[offset++] = point.y;
6462
+ glColorData[offset++] = rgba;
6463
+ }
6464
+ glBatchCount += vertCount;
6465
+ }
6466
+
6467
+ // WebGL internal function to convert polygon to outline triangle strip
6468
+ function glMakeOutline(points, width)
6469
+ {
6470
+ if (points.length < 2)
6471
+ return [];
6472
+
6473
+ const halfWidth = width / 2;
6474
+ const strip = [];
6475
+ const n = points.length;
6476
+ const e = 1e-6;
6477
+ for (let i = 0; i < n; i++)
6478
+ {
6479
+ // for each vertex, calculate normal based on adjacent edges
6480
+ const prev = points[(i - 1 + n) % n];
6481
+ const curr = points[i];
6482
+ const next = points[(i + 1) % n];
6483
+
6484
+ // direction from previous to current
6485
+ const dx1 = curr.x - prev.x;
6486
+ const dy1 = curr.y - prev.y;
6487
+ const len1 = (dx1*dx1 + dy1*dy1)**.5;
6488
+
6489
+ // direction from current to next
6490
+ const dx2 = next.x - curr.x;
6491
+ const dy2 = next.y - curr.y;
6492
+ const len2 = (dx2*dx2 + dy2*dy2)**.5;
6493
+
6494
+ if (len1 < e && len2 < e)
6495
+ continue; // skip degenerate point
6496
+
6497
+ // calculate perpendicular normals for each edge
6498
+ const nx1 = len1 > e ? -dy1 / len1 : 0;
6499
+ const ny1 = len1 > e ? dx1 / len1 : 0;
6500
+ const nx2 = len2 > e ? -dy2 / len2 : 0;
6501
+ const ny2 = len2 > e ? dx2 / len2 : 0;
6502
+
6503
+ // average the normals for miter
6504
+ let nx = nx1 + nx2;
6505
+ let ny = ny1 + ny2;
6506
+ const nlen = (nx*nx + ny*ny)**.5;
6507
+ if (nlen < e)
6508
+ {
6509
+ // 180 degree turn - use perpendicular
6510
+ nx = nx1;
6511
+ ny = ny1;
6512
+ }
6513
+ else
6514
+ {
6515
+ // calculate miter length
6516
+ nx /= nlen;
6517
+ ny /= nlen;
6518
+ const dot = nx1 * nx + ny1 * ny;
6519
+ if (dot > e)
6520
+ {
6521
+ // scale normal by miter length
6522
+ const miterLength = 1 / dot;
6523
+ nx *= miterLength;
6524
+ ny *= miterLength;
6525
+ }
6526
+ }
6527
+
6528
+ // create inner and outer points along the normal
6529
+ const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
6530
+ const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
6531
+ strip.push(inner);
6532
+ strip.push(outer);
6533
+ }
6534
+ if (strip.length > 1)
6535
+ {
6536
+ // close the loop
6537
+ strip.push(strip[0]);
6538
+ strip.push(strip[1]);
6539
+ }
6540
+ return strip;
6541
+ }
6542
+
6543
+ // WebGL internal function to convert polys to tri strips
6544
+ function glPolyStrip(points)
6545
+ {
6546
+ // validate input
6547
+ if (points.length < 3)
6548
+ return [];
6549
+
6550
+ // cross product helper: (b-a) x (c-a)
6551
+ const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
6552
+
6553
+ // calculate signed area of polygon
6554
+ const signedArea = (poly)=>
6555
+ {
6556
+ let area = 0;
6557
+ for (let i = poly.length; i--;)
6558
+ {
6559
+ const j = (i+1) % poly.length;
6560
+ area += poly[i].cross(poly[j]);
6561
+ }
6562
+ return area;
6563
+ }
6564
+
6565
+ // ensure counter-clockwise winding
6566
+ if (signedArea(points) < 0)
6567
+ points = points.reverse();
6568
+
6569
+ // tolerance constants
6570
+ const e = 1e-10;
6571
+
6572
+ // check if point is inside triangle
6573
+ const pointInTriangle = (p, a, b, c)=>
6574
+ {
6575
+ const c1 = cross(a, b, p);
6576
+ const c2 = cross(b, c, p);
6577
+ const c3 = cross(c, a, p);
6578
+ const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
6579
+ const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
6580
+ return !(negative && positive);
6581
+ };
6582
+
6583
+ // ear clipping triangulation
6584
+ const indices = [];
6585
+ for (let i = 0; i < points.length; ++i)
6586
+ indices[i] = i;
6587
+ const triangles = [];
6588
+ let attempts = 0;
6589
+ const maxAttempts = points.length ** 2 + 100;
6590
+ while (indices.length > 3 && attempts++ < maxAttempts)
6591
+ {
6592
+ let foundEar = false;
6593
+ for (let i = indices.length; --i;)
6594
+ {
6595
+ const i0 = indices[(i + indices.length - 1) % indices.length];
6596
+ const i1 = indices[i];
6597
+ const i2 = indices[(i + 1) % indices.length];
6598
+ const a = points[i0], b = points[i1], c = points[i2];
6599
+
6600
+ // check if convex
6601
+ if (cross(a, b, c) < e)
6602
+ continue;
6603
+
6604
+ // check if any other point is inside
6605
+ let hasInside = false;
6606
+ for (let j = 0; j < indices.length; j++)
6607
+ {
6608
+ const k = indices[j];
6609
+ if (k === i0 || k === i1 || k === i2)
6610
+ continue;
6611
+ const p = points[k];
6612
+ hasInside = pointInTriangle(p, a, b, c);
6613
+ if (hasInside)
6614
+ break;
6615
+ }
6616
+ if (hasInside)
6617
+ continue;
6618
+
6619
+ // found valid ear
6620
+ triangles.push([i0, i1, i2]);
6621
+ indices.splice(i, 1);
6622
+ foundEar = true;
6623
+ break;
6624
+ }
6625
+
6626
+ // fallback for degenerate cases
6627
+ if (!foundEar)
6628
+ {
6629
+ let worstIndex = -1, worstValue = Infinity;
6630
+ for (let i = indices.length; --i;)
6631
+ {
6632
+ const i0 = indices[(i + indices.length - 1) % indices.length];
6633
+ const i1 = indices[i];
6634
+ const i2 = indices[(i + 1) % indices.length];
6635
+ const value = abs(cross(points[i0], points[i1], points[i2]));
6636
+ if (value < worstValue)
6637
+ {
6638
+ worstValue = value;
6639
+ worstIndex = i;
6640
+ }
6641
+ }
6642
+ if (worstIndex < 0)
6643
+ break;
6644
+
6645
+ const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
6646
+ const i1 = indices[worstIndex];
6647
+ const i2 = indices[(worstIndex + 1) % indices.length];
6648
+ triangles.push([i0, i1, i2]);
6649
+ indices.splice(worstIndex, 1);
6650
+ }
6651
+ }
6652
+
6653
+ // add final triangle
6654
+ if (indices.length === 3)
6655
+ triangles.push([indices[0], indices[1], indices[2]]);
6656
+ if (!triangles.length)
6657
+ return [];
6658
+
6659
+ // convert triangles to triangle strip with degenerate connectors
6660
+ const strip = [];
6661
+ let [a0, b0, c0] = triangles[0];
6662
+ strip.push(points[a0], points[b0], points[c0]);
6663
+ for (let i = 1; i < triangles.length; i++)
6664
+ {
6665
+ // add degenerate bridge from last vertex to first of new triangle
6666
+ const [a, b, c] = triangles[i];
6667
+ strip.push(points[c0], points[a]);
6668
+ strip.push(points[a], points[b], points[c]);
6669
+ c0 = c;
6670
+ }
6671
+ return strip;
6057
6672
  }
6058
6673
  /**
6059
6674
  * LittleJS Newgrounds API
@@ -6078,11 +6693,11 @@ let newgrounds;
6078
6693
  class NewgroundsMedal extends Medal
6079
6694
  {
6080
6695
  /** 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
6696
+ * @param {number} id - The unique identifier of the medal
6697
+ * @param {string} name - Name of the medal
6698
+ * @param {string} [description] - Description of the medal
6699
+ * @param {string} [icon] - Icon for the medal
6700
+ * @param {string} [src] - Image location for the medal
6086
6701
  */
6087
6702
  constructor(id, name, description, icon, src)
6088
6703
  { super(id, name, description, icon, src); }
@@ -6174,7 +6789,7 @@ class NewgroundsPlugin
6174
6789
  * @param {number} id - The scoreboard id
6175
6790
  * @param {string} [user] - A user's id or name
6176
6791
  * @param {number} [social] - If true, only social scores will be loaded
6177
- * @param {number} [skip] - Number of scores to skip before start
6792
+ * @param {number} [skip] - Number of scores to skip over
6178
6793
  * @param {number} [limit] - Number of scores to include in the list
6179
6794
  * @return {Object} - The response JSON object
6180
6795
  */
@@ -6397,16 +7012,16 @@ class ZzFXMusic extends Sound
6397
7012
  if (!soundEnable || headlessMode) return;
6398
7013
  this.randomness = 0;
6399
7014
  this.sampleChannels = zzfxM(...zzfxMusic);
6400
- this.sampleRate = zzfxR;
7015
+ this.sampleRate = audioDefaultSampleRate;
6401
7016
  }
6402
7017
 
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
7018
+ /** Play the music that loops by default
7019
+ * @param {number} [volume] - Volume to play the music at
7020
+ * @param {boolean} [loop] - Should the music loop?
6406
7021
  * @return {AudioBufferSourceNode} - The audio source node
6407
7022
  */
6408
- playMusic(volume, loop=false)
6409
- { return super.play(undefined, volume, 1, 1, loop); }
7023
+ playMusic(volume=1, loop=true)
7024
+ { return super.play(undefined, volume, 1, 0, loop); }
6410
7025
  }
6411
7026
 
6412
7027
  ///////////////////////////////////////////////////////////////////////////////
@@ -6440,7 +7055,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
6440
7055
  let panning = 0;
6441
7056
  let hasMore = 1;
6442
7057
  let sampleCache = {};
6443
- let beatLength = zzfxR / BPM * 60 >> 2;
7058
+ let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
6444
7059
 
6445
7060
  // for each channel in order until there are no more
6446
7061
  for (; hasMore; channelIndex++) {
@@ -6459,15 +7074,15 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
6459
7074
  // get next offset, use the length of first channel
6460
7075
  nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
6461
7076
  // for each beat in pattern, plus one extra if end of sequence
6462
- isSequenceEnd = sequenceIndex == sequence.length - 1;
7077
+ isSequenceEnd = sequenceIndex === sequence.length - 1;
6463
7078
  for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
6464
7079
 
6465
7080
  // <channel-note>
6466
7081
  note = patternChannel[i];
6467
7082
 
6468
7083
  // stop if end, different instrument or new note
6469
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
6470
- instrument != (patternChannel[0] || 0) || note | 0;
7084
+ stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
7085
+ instrument !== (patternChannel[0] || 0) || note | 0;
6471
7086
 
6472
7087
  // fill buffer with samples for previous beat, most cpu intensive part
6473
7088
  for (j = 0; j < beatLength && notFirstBeat;
@@ -6552,11 +7167,11 @@ class UISystemPlugin
6552
7167
  /** @property {Color} - Default text color for UI elements */
6553
7168
  this.defaultTextColor = BLACK;
6554
7169
  /** @property {Color} - Default button color for UI elements */
6555
- this.defaultButtonColor = hsl(0,0,.5);
7170
+ this.defaultButtonColor = hsl(0,0,.7);
6556
7171
  /** @property {Color} - Default hover color for UI elements */
6557
- this.defaultHoverColor = hsl(0,0,.7);
7172
+ this.defaultHoverColor = hsl(0,0,.9);
6558
7173
  /** @property {Color} - Default color for disabled UI elements */
6559
- this.defaultDisabledColor = hsl(0,0,.2);
7174
+ this.defaultDisabledColor = hsl(0,0,.3);
6560
7175
  /** @property {number} - Default line width for UI elements */
6561
7176
  this.defaultLineWidth = 4;
6562
7177
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -6576,16 +7191,16 @@ class UISystemPlugin
6576
7191
 
6577
7192
  engineAddPlugin(uiUpdate, uiRender);
6578
7193
 
6579
- function updateInvisible(o)
6580
- {
6581
- for (const c of o.children)
6582
- updateInvisible(c);
6583
- o.updateInvisible();
6584
- }
6585
-
6586
7194
  // setup recursive update and render
6587
7195
  function uiUpdate()
6588
7196
  {
7197
+ function updateInvisibleObject(o)
7198
+ {
7199
+ // update invisible objects
7200
+ for (const c of o.children)
7201
+ updateInvisibleObject(c);
7202
+ o.updateInvisible();
7203
+ }
6589
7204
  function updateObject(o)
6590
7205
  {
6591
7206
  if (o.visible)
@@ -6599,7 +7214,7 @@ class UISystemPlugin
6599
7214
  o.update();
6600
7215
  }
6601
7216
  else
6602
- updateInvisible(o);
7217
+ updateInvisibleObject(o);
6603
7218
  }
6604
7219
  uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
6605
7220
  }
@@ -6625,7 +7240,7 @@ class UISystemPlugin
6625
7240
  * @param {Color} [color=uiSystem.defaultColor]
6626
7241
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6627
7242
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
6628
- * @param {number} [lineWidth=uiSystem.defaultCornerRadius] */
7243
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
6629
7244
  drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
6630
7245
  {
6631
7246
  const context = uiSystem.uiContext;
@@ -6700,32 +7315,40 @@ class UIObject
6700
7315
  constructor(pos=vec2(), size=vec2())
6701
7316
  {
6702
7317
  /** @property {Vector2} - Local position of the object */
6703
- this.localPos = pos.copy();
7318
+ this.localPos = pos.copy();
6704
7319
  /** @property {Vector2} - Screen space position of the object */
6705
- this.pos = pos.copy();
7320
+ this.pos = pos.copy();
6706
7321
  /** @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 */
7322
+ this.size = size.copy();
7323
+ /** @property {Color} - Color of the object */
7324
+ this.color = uiSystem.defaultColor;
7325
+ /** @property {string} - Text for this ui object */
7326
+ this.text = undefined;
7327
+ /** @property {Color} - Color when disabled */
7328
+ this.disabledColor = uiSystem.defaultDisabledColor;
7329
+ /** @property {boolean} - Is this object disabled? */
7330
+ this.disabled = false;
7331
+ /** @property {Color} - Color for text */
7332
+ this.textColor = uiSystem.defaultTextColor;
7333
+ /** @property {Color} - Color used when hovering over the object */
6713
7334
  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 */
7335
+ /** @property {Color} - Color for line drawing */
7336
+ this.lineColor = uiSystem.defaultLineColor;
7337
+ /** @property {number} - Width for line drawing */
7338
+ this.lineWidth = uiSystem.defaultLineWidth;
7339
+ /** @property {number} - Corner radius for rounded rects */
7340
+ this.cornerRadius = uiSystem.defaultCornerRadius;
7341
+ /** @property {string} - Font for this objecct */
7342
+ this.font = uiSystem.defaultFont;
7343
+ /** @property {number} - Override for text height */
7344
+ this.textHeight = undefined;
7345
+ /** @property {boolean} - Should this object be drawn */
7346
+ this.visible = true;
7347
+ /** @property {Array<UIObject>} - A list of this object's children */
7348
+ this.children = [];
7349
+ /** @property {UIObject} - This object's parent, position is in parent space */
7350
+ this.parent = undefined;
7351
+ /** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
6729
7352
  this.extraTouchSize = 0;
6730
7353
  /** @property {Sound} - Sound when interactive element is pressed */
6731
7354
  this.soundPress = uiSystem.defaultSoundPress;
@@ -6757,7 +7380,7 @@ class UIObject
6757
7380
  */
6758
7381
  removeChild(child)
6759
7382
  {
6760
- ASSERT(child.parent == this && this.children.includes(child));
7383
+ ASSERT(child.parent === this && this.children.includes(child));
6761
7384
  this.children.splice(this.children.indexOf(child), 1);
6762
7385
  child.parent = undefined;
6763
7386
  }
@@ -6810,7 +7433,7 @@ class UIObject
6810
7433
  this.mouseIsHeld = false;
6811
7434
  }
6812
7435
 
6813
- if (this.mouseIsOver != mouseWasOver)
7436
+ if (this.mouseIsOver !== mouseWasOver)
6814
7437
  this.mouseIsOver ? this.onEnter() : this.onLeave();
6815
7438
  }
6816
7439
 
@@ -6821,7 +7444,7 @@ class UIObject
6821
7444
  uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
6822
7445
  }
6823
7446
 
6824
- /** Special update for when object is invisible */
7447
+ /** Special update when object is not visible */
6825
7448
  updateInvisible()
6826
7449
  {
6827
7450
  // reset input state when not visible
@@ -6829,28 +7452,22 @@ class UIObject
6829
7452
  }
6830
7453
 
6831
7454
  /** Called when the mouse enters the object */
6832
- onEnter()
6833
- {}
7455
+ onEnter() {}
6834
7456
 
6835
7457
  /** Called when the mouse leaves the object */
6836
- onLeave()
6837
- {}
7458
+ onLeave() {}
6838
7459
 
6839
7460
  /** Called when the mouse is pressed while over the object */
6840
- onPress()
6841
- {}
7461
+ onPress() {}
6842
7462
 
6843
7463
  /** Called when the mouse is released while over the object */
6844
- onRelease()
6845
- {}
7464
+ onRelease() {}
6846
7465
 
6847
7466
  /** Called when user clicks on this object */
6848
- onClick()
6849
- {}
7467
+ onClick() {}
6850
7468
 
6851
7469
  /** Called when the state of this object changes */
6852
- onChange()
6853
- {}
7470
+ onChange() {}
6854
7471
  };
6855
7472
 
6856
7473
  ///////////////////////////////////////////////////////////////////////////////
@@ -6871,13 +7488,13 @@ class UIText extends UIObject
6871
7488
  {
6872
7489
  super(pos, size);
6873
7490
 
6874
- /** @property {string} */
7491
+ // set properties
6875
7492
  this.text = text;
6876
- /** @property {string} */
6877
7493
  this.align = align;
7494
+ this.font = font;
6878
7495
 
6879
- this.font = font; // set font
6880
- this.lineWidth = 0; // set text to not be outlined by default
7496
+ // make text not outlined by default
7497
+ this.lineWidth = 0;
6881
7498
  }
6882
7499
  render()
6883
7500
  {
@@ -6904,13 +7521,14 @@ class UITile extends UIObject
6904
7521
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
6905
7522
  {
6906
7523
  super(pos, size);
6907
-
6908
7524
  /** @property {TileInfo} - Tile image to use */
6909
7525
  this.tileInfo = tileInfo;
6910
7526
  /** @property {number} - Angle to rotate in radians */
6911
7527
  this.angle = angle;
6912
7528
  /** @property {boolean} - Should it be mirrored? */
6913
7529
  this.mirror = mirror;
7530
+
7531
+ // set properties
6914
7532
  this.color = color;
6915
7533
  }
6916
7534
  render()
@@ -6936,22 +7554,20 @@ class UIButton extends UIObject
6936
7554
  {
6937
7555
  super(pos, size);
6938
7556
 
6939
- /** @property {string} */
7557
+ // set properties
6940
7558
  this.text = text;
6941
- /** @property {Color} */
6942
- this.disabledColor = uiSystem.defaultDisabledColor;
6943
- /** @property {boolean} */
6944
- this.disabled = false;
6945
- this.interactive = true;
6946
7559
  this.color = color;
7560
+ this.interactive = true;
6947
7561
  }
6948
7562
  render()
6949
7563
  {
7564
+ // draw the button
6950
7565
  const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
6951
7566
  const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
6952
7567
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
6953
7568
 
6954
- const textScale = .8; // scale text to fit in button
7569
+ // draw the text
7570
+ const textScale = .8; // scale text to fit
6955
7571
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
6956
7572
  uiSystem.drawText(this.text, this.pos, textSize,
6957
7573
  this.textColor, 0, undefined, this.align, this.font);
@@ -6969,13 +7585,18 @@ class UICheckbox extends UIObject
6969
7585
  * @param {Vector2} [pos]
6970
7586
  * @param {Vector2} [size]
6971
7587
  * @param {boolean} [checked]
7588
+ * @param {string} [text]
7589
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6972
7590
  */
6973
- constructor(pos, size, checked=false)
7591
+ constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
6974
7592
  {
6975
7593
  super(pos, size);
6976
-
6977
- /** @property {boolean} */
7594
+ /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
6978
7595
  this.checked = checked;
7596
+
7597
+ // set properties
7598
+ this.text = text;
7599
+ this.color = color;
6979
7600
  this.interactive = true;
6980
7601
  }
6981
7602
  onClick()
@@ -6985,14 +7606,24 @@ class UICheckbox extends UIObject
6985
7606
  }
6986
7607
  render()
6987
7608
  {
6988
- const color = this.mouseIsOver? this.hoverColor : this.color;
7609
+ const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
6989
7610
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
6990
7611
  if (this.checked)
6991
7612
  {
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);
7613
+ const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
7614
+ const length = lerp(1, 2**.5/2, p) / 2;
7615
+ let s = this.size.scale(length);
7616
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
7617
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
6995
7618
  }
7619
+
7620
+ // draw the text to the right side of the checkbox
7621
+ const textScale = .8; // scale text to fit
7622
+ const gapScale = .55;
7623
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7624
+ const pos = this.pos.add(vec2(this.size.x*gapScale,0));
7625
+ uiSystem.drawText(this.text, pos, textSize,
7626
+ this.textColor, 0, undefined, 'left', this.font);
6996
7627
  }
6997
7628
  }
6998
7629
 
@@ -7015,43 +7646,51 @@ class UIScrollbar extends UIObject
7015
7646
  {
7016
7647
  super(pos, size);
7017
7648
 
7018
- /** @property {number} */
7649
+ /** @property {number} - Current percentage value of this scrollbar 0-1 */
7019
7650
  this.value = value;
7020
- /** @property {string} */
7021
- this.text = text;
7022
- /** @property {Color} */
7651
+ /** @property {Color} - Color for the handle part of the scrollbar */
7023
7652
  this.handleColor = handleColor;
7653
+
7654
+ // set properties
7655
+ this.text = text;
7024
7656
  this.color = color;
7025
7657
  this.interactive = true;
7026
7658
  }
7027
7659
  update()
7028
7660
  {
7029
7661
  super.update();
7030
- if (this.mouseIsHeld)
7662
+ if (this.mouseIsHeld && this.interactive)
7031
7663
  {
7664
+ // check if value changed
7032
7665
  const handleSize = vec2(this.size.y);
7033
7666
  const handleWidth = this.size.x - handleSize.x;
7034
7667
  const p1 = this.pos.x - handleWidth/2;
7035
7668
  const p2 = this.pos.x + handleWidth/2;
7036
7669
  const oldValue = this.value;
7037
7670
  this.value = percent(mousePosScreen.x, p1, p2);
7038
- this.value == oldValue || this.onChange();
7671
+ this.value === oldValue || this.onChange();
7039
7672
  }
7040
7673
  }
7041
7674
  render()
7042
7675
  {
7043
- const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
7044
- const color = this.mouseIsOver? this.hoverColor : this.color;
7676
+ // draw the scrollbar background
7677
+ const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
7678
+ this.color : this.lineColor;
7679
+ const color = this.disabled ? this.disabledColor :
7680
+ this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
7045
7681
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7046
7682
 
7683
+ // draw the scrollbar handle
7047
7684
  const handleSize = vec2(this.size.y);
7048
7685
  const handleWidth = this.size.x - handleSize.x;
7049
7686
  const p1 = this.pos.x - handleWidth/2;
7050
7687
  const p2 = this.pos.x + handleWidth/2;
7051
7688
  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);
7689
+ const handleColor = this.disabled ? this.disabledColor :
7690
+ this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
7691
+ uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
7054
7692
 
7693
+ // draw the text on the scrollbar
7055
7694
  const textScale = .8; // scale text to fit in scrollbar
7056
7695
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7057
7696
  uiSystem.drawText(this.text, this.pos, textSize,
@@ -7146,14 +7785,14 @@ class Box2dObject extends EngineObject
7146
7785
  if (this.tileInfo)
7147
7786
  super.render();
7148
7787
  else
7149
- this.drawFixtures(this.color, this.lineColor, this.lineWidth, mainContext);
7788
+ this.drawFixtures(this.color, this.lineColor, this.lineWidth);
7150
7789
  }
7151
7790
 
7152
7791
  /** Render debug info */
7153
7792
  renderDebugInfo()
7154
7793
  {
7155
7794
  const isAsleep = !this.getIsAwake();
7156
- const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
7795
+ const isStatic = this.getBodyType() === box2d.bodyTypeStatic;
7157
7796
  const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
7158
7797
  this.drawFixtures(color);
7159
7798
  }
@@ -8621,7 +9260,7 @@ class Box2dPlugin
8621
9260
  queryCallback.ReportFixture = function(fixturePointer)
8622
9261
  {
8623
9262
  const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8624
- if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
9263
+ if (dynamicOnly && fixture.GetBody().GetType() !== box2d.instance.b2_dynamicBody)
8625
9264
  return true; // continue getting results
8626
9265
  if (!fixture.TestPoint(box2d.vec2dTo(pos)))
8627
9266
  return true; // continue getting results
@@ -8650,7 +9289,7 @@ class Box2dPlugin
8650
9289
  * @param {Color} [lineColor]
8651
9290
  * @param {number} [lineWidth]
8652
9291
  * @param {CanvasRenderingContext2D} [context] */
8653
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context=drawContext)
9292
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
8654
9293
  {
8655
9294
  const shape = box2d.castObjectType(fixture.GetShape());
8656
9295
  switch (shape.GetType())
@@ -8660,20 +9299,20 @@ class Box2dPlugin
8660
9299
  let points = [];
8661
9300
  for (let i=shape.GetVertexCount(); i--;)
8662
9301
  points.push(box2d.vec2From(shape.GetVertex(i)));
8663
- drawPoly(points, color, lineWidth, lineColor, pos, angle, false, false, context);
9302
+ drawPoly(points, color, lineWidth, lineColor, pos, angle);
8664
9303
  break;
8665
9304
  }
8666
9305
  case box2d.instance.b2Shape.e_circle:
8667
9306
  {
8668
9307
  const radius = shape.get_m_radius();
8669
- drawCircle(pos, radius, color, lineWidth, lineColor, false, false, context);
9308
+ drawCircle(pos, radius, color, lineWidth, lineColor);
8670
9309
  break;
8671
9310
  }
8672
9311
  case box2d.instance.b2Shape.e_edge:
8673
9312
  {
8674
9313
  const v1 = box2d.vec2From(shape.get_m_vertex1());
8675
9314
  const v2 = box2d.vec2From(shape.get_m_vertex2());
8676
- drawLine(v1, v2, lineWidth, lineColor, pos, angle, false, false, context);
9315
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle);
8677
9316
  break;
8678
9317
  }
8679
9318
  }
@@ -8752,7 +9391,9 @@ class Box2dPlugin
8752
9391
  }
8753
9392
 
8754
9393
  ///////////////////////////////////////////////////////////////////////////////
8755
- /** Box2d Init - Call with await before starting LittleJS to init box2d
9394
+ /** Box2d Init - Call with await to init box2d
9395
+ * @example
9396
+ * await box2dInit();
8756
9397
  * @return {Promise<Box2dPlugin>}
8757
9398
  * @memberof Box2D */
8758
9399
  async function box2dInit()
@@ -8868,7 +9509,7 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
8868
9509
  }
8869
9510
 
8870
9511
  /** Draw a scalable nine-slice UI element in world space
8871
- * This function can apply color and additive color if webgl is enabled
9512
+ * This function can apply color and additive color if WebGL is enabled
8872
9513
  * @param {Vector2} pos - World space position
8873
9514
  * @param {Vector2} size - World space size
8874
9515
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
@@ -8897,9 +9538,9 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
8897
9538
  {
8898
9539
  // sides
8899
9540
  const horizontal = i%2;
8900
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-1:1));
9541
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
8901
9542
  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)))
9543
+ const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
8903
9544
  drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
8904
9545
  }
8905
9546
  for (let i=4; i--;)
@@ -8928,7 +9569,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
8928
9569
  }
8929
9570
 
8930
9571
  /** Draw a scalable three-slice UI element in world space
8931
- * This function can apply color and additive color if webgl is enabled
9572
+ * This function can apply color and additive color if WebGL is enabled
8932
9573
  * @param {Vector2} pos - World space position
8933
9574
  * @param {Vector2} size - World space size
8934
9575
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
@@ -8960,7 +9601,7 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
8960
9601
  // sides
8961
9602
  const a = angle + i*PI/2;
8962
9603
  const horizontal = i%2;
8963
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-flip:flip));
9604
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
8964
9605
  const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
8965
9606
  drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
8966
9607
  }