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 Debug System
656
656
  * - Press Esc to show debug overlay with mouse pick
657
657
  * - Number keys toggle debug functions
@@ -704,10 +704,10 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
704
704
  // Debug helper functions
705
705
 
706
706
  /** Asserts if the expression is false, does not do anything in release builds
707
- * @param {boolean} assert
707
+ * @param {boolean} assert
708
708
  * @param {...Object} [output] - error message output
709
709
  * @memberof Debug */
710
- function ASSERT(assert, ...output)
710
+ function ASSERT(assert, ...output)
711
711
  {
712
712
  if (enableAsserts)
713
713
  console.assert(assert, ...output);
@@ -723,9 +723,9 @@ function ASSERT(assert, ...output)
723
723
  * @memberof Debug */
724
724
  function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
725
725
  {
726
- if (typeof size == 'number')
726
+ if (typeof size === 'number')
727
727
  size = vec2(size); // allow passing in floats
728
- ASSERT(typeof color == 'string', 'pass in css color strings');
728
+ ASSERT(typeof color === 'string', 'pass in css color strings');
729
729
  debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
730
730
  }
731
731
 
@@ -739,7 +739,7 @@ function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
739
739
  * @memberof Debug */
740
740
  function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
741
741
  {
742
- ASSERT(typeof color == 'string', 'pass in css color strings');
742
+ ASSERT(typeof color === 'string', 'pass in css color strings');
743
743
  debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
744
744
  }
745
745
 
@@ -752,7 +752,7 @@ function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
752
752
  * @memberof Debug */
753
753
  function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
754
754
  {
755
- ASSERT(typeof color == 'string', 'pass in css color strings');
755
+ ASSERT(typeof color === 'string', 'pass in css color strings');
756
756
  debugPrimitives.push({pos, size:radius, color, time:new Timer(time), angle:0, fill});
757
757
  }
758
758
 
@@ -764,7 +764,7 @@ function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
764
764
  * @memberof Debug */
765
765
  function debugPoint(pos, color, time, angle)
766
766
  {
767
- ASSERT(typeof color == 'string', 'pass in css color strings');
767
+ ASSERT(typeof color === 'string', 'pass in css color strings');
768
768
  debugRect(pos, undefined, color, time, angle);
769
769
  }
770
770
 
@@ -792,11 +792,11 @@ function debugLine(posA, posB, color, thickness=.1, time)
792
792
  function debugOverlap(posA, sizeA, posB, sizeB, color)
793
793
  {
794
794
  const minPos = vec2(
795
- min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
795
+ min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
796
796
  min(posA.y - sizeA.y/2, posB.y - sizeB.y/2)
797
797
  );
798
798
  const maxPos = vec2(
799
- max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
799
+ max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
800
800
  max(posA.y + sizeA.y/2, posB.y + sizeB.y/2)
801
801
  );
802
802
  debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color);
@@ -813,7 +813,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
813
813
  * @memberof Debug */
814
814
  function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
815
815
  {
816
- ASSERT(typeof color == 'string', 'pass in css color strings');
816
+ ASSERT(typeof color === 'string', 'pass in css color strings');
817
817
  debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
818
818
  }
819
819
 
@@ -825,7 +825,7 @@ function debugClear() { debugPrimitives = []; }
825
825
  * @memberof Debug */
826
826
  function debugScreenshot() { debugTakeScreenshot = 1; }
827
827
 
828
- /** Save a canvas to disk
828
+ /** Save a canvas to disk
829
829
  * @param {HTMLCanvasElement|OffscreenCanvas} canvas
830
830
  * @param {string} [filename]
831
831
  * @param {string} [type]
@@ -846,7 +846,7 @@ function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
846
846
  debugSaveDataURL(canvas.toDataURL(type), filename);
847
847
  }
848
848
 
849
- /** Save a text file to disk
849
+ /** Save a text file to disk
850
850
  * @param {string} text
851
851
  * @param {string} [filename]
852
852
  * @param {string} [type]
@@ -854,7 +854,7 @@ function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
854
854
  function debugSaveText(text, filename='text', type='text/plain')
855
855
  { debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
856
856
 
857
- /** Save a data url to disk
857
+ /** Save a data url to disk
858
858
  * @param {string} dataURL
859
859
  * @param {string} filename
860
860
  * @memberof Debug */
@@ -973,7 +973,7 @@ function debugRender()
973
973
  {
974
974
  const saveContext = mainContext;
975
975
  mainContext = overlayContext;
976
-
976
+
977
977
  // draw red rectangle around screen
978
978
  const cameraSize = getCameraSize();
979
979
  debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
@@ -1023,14 +1023,14 @@ function debugRender()
1023
1023
  overlayContext.scale(1, p.text ? 1 : -1);
1024
1024
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
1025
1025
 
1026
- if (p.text != undefined)
1026
+ if (p.text !== undefined)
1027
1027
  {
1028
1028
  overlayContext.font = p.size*cameraScale + 'px '+ p.font;
1029
1029
  overlayContext.textAlign = 'center';
1030
1030
  overlayContext.textBaseline = 'middle';
1031
1031
  overlayContext.fillText(p.text, 0, 0);
1032
1032
  }
1033
- else if (p.points != undefined)
1033
+ else if (p.points !== undefined)
1034
1034
  {
1035
1035
  // poly
1036
1036
  overlayContext.beginPath();
@@ -1043,13 +1043,13 @@ function debugRender()
1043
1043
  p.fill && overlayContext.fill();
1044
1044
  overlayContext.stroke();
1045
1045
  }
1046
- else if (p.size == 0 || p.size.x === 0 && p.size.y === 0)
1046
+ else if (p.size === 0 || p.size.x === 0 && p.size.y === 0)
1047
1047
  {
1048
1048
  // point
1049
1049
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
1050
1050
  overlayContext.fillRect(-1, -pointSize/2, 3, pointSize);
1051
1051
  }
1052
- else if (p.size.x != undefined)
1052
+ else if (p.size.x !== undefined)
1053
1053
  {
1054
1054
  // rect
1055
1055
  const s = p.size.scale(cameraScale).floor();
@@ -1065,14 +1065,14 @@ function debugRender()
1065
1065
  p.fill && overlayContext.fill();
1066
1066
  overlayContext.stroke();
1067
1067
  }
1068
-
1068
+
1069
1069
  overlayContext.restore();
1070
1070
  });
1071
1071
 
1072
1072
  // remove expired primitives
1073
1073
  debugPrimitives = debugPrimitives.filter(r=>r.time<0);
1074
1074
  }
1075
-
1075
+
1076
1076
  if (debugObject)
1077
1077
  {
1078
1078
  const saveContext = mainContext;
@@ -1081,8 +1081,8 @@ function debugRender()
1081
1081
  raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
1082
1082
  drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5));
1083
1083
 
1084
- const debugText = 'mouse pos = ' + mousePos +
1085
- '\nmouse collision = ' + tileCollisionGetData(mousePos) +
1084
+ const debugText = 'mouse pos = ' + mousePos +
1085
+ '\nmouse collision = ' + tileCollisionGetData(mousePos) +
1086
1086
  '\n\n--- object info ---\n' +
1087
1087
  debugObject.toString();
1088
1088
  drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
@@ -1104,7 +1104,7 @@ function debugRender()
1104
1104
  let x = 9, y = 0, h = lineHeight;
1105
1105
  if (debugOverlay)
1106
1106
  {
1107
- overlayContext.fillText(engineName, x, y += h/2 );
1107
+ overlayContext.fillText(`${engineName} v${engineVersion}`, x, y += h/2 );
1108
1108
  overlayContext.fillText('Time: ' + formatTime(time), x, y += h);
1109
1109
  overlayContext.fillText('FPS: ' + averageFPS.toFixed(1), x, y += h);
1110
1110
  overlayContext.fillText('Objects: ' + engineObjects.length, x, y += h);
@@ -1148,7 +1148,7 @@ function debugRender()
1148
1148
  overlayContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
1149
1149
  overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
1150
1150
  }
1151
-
1151
+
1152
1152
  overlayContext.restore();
1153
1153
  }
1154
1154
  }
@@ -1236,7 +1236,7 @@ function debugVideoCaptureUpdate()
1236
1236
  {
1237
1237
  if (!debugVideoCaptureIsActive())
1238
1238
  return; // not recording
1239
-
1239
+
1240
1240
  // save the video frame
1241
1241
  combineCanvases();
1242
1242
  debugVideoCaptureTrack.requestFrame();
@@ -1317,7 +1317,19 @@ function lerp(valueA, valueB, percent)
1317
1317
  if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
1318
1318
  console.warn('lerp() parameter order changed! use lerp(start, end, p)');
1319
1319
  return valueA + clamp(percent) * (valueB-valueA);
1320
- }
1320
+ }
1321
+
1322
+ /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
1323
+ * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
1324
+ * @param {number} value
1325
+ * @param {number} percentA
1326
+ * @param {number} percentB
1327
+ * @param {number} lerpA
1328
+ * @param {number} lerpB
1329
+ * @return {number}
1330
+ * @memberof Utilities */
1331
+ function percentLerp(value, percentA, percentB, lerpA, lerpB)
1332
+ { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
1321
1333
 
1322
1334
  /** Returns signed wrapped distance between the two values passed in
1323
1335
  * @param {number} valueA
@@ -1369,7 +1381,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
1369
1381
  * @memberof Utilities */
1370
1382
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
1371
1383
 
1372
- /** Returns the nearest power of two not less then the value
1384
+ /** Returns the nearest power of two not less than the value
1373
1385
  * @param {number} value
1374
1386
  * @return {number}
1375
1387
  * @memberof Utilities */
@@ -1384,8 +1396,8 @@ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
1384
1396
  * @return {boolean} - True if overlapping
1385
1397
  * @memberof Utilities */
1386
1398
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
1387
- {
1388
- return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
1399
+ {
1400
+ return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
1389
1401
  && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
1390
1402
  }
1391
1403
 
@@ -1440,7 +1452,7 @@ function isIntersecting(start, end, pos, size)
1440
1452
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1441
1453
  { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
1442
1454
 
1443
- /** Formats seconds to mm:ss style for display purposes
1455
+ /** Formats seconds to mm:ss style for display purposes
1444
1456
  * @param {number} t - time in seconds
1445
1457
  * @return {string}
1446
1458
  * @memberof Utilities */
@@ -1456,13 +1468,12 @@ async function fetchJSON(url)
1456
1468
  return response.json();
1457
1469
  }
1458
1470
 
1459
- /**
1471
+ /**
1460
1472
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
1461
1473
  * @param {any} n
1462
1474
  * @return {boolean}
1463
- * @memberof Utilities
1464
- */
1465
- function isNumber(n) { return typeof n == 'number' && !isNaN(n); }
1475
+ * @memberof Utilities */
1476
+ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1466
1477
 
1467
1478
  ///////////////////////////////////////////////////////////////////////////////
1468
1479
 
@@ -1517,13 +1528,13 @@ function randInCircle(radius=1, minRadius=0)
1517
1528
  * @memberof Random */
1518
1529
  function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1519
1530
  {
1520
- return linear ? colorA.lerp(colorB, rand()) :
1531
+ return linear ? colorA.lerp(colorB, rand()) :
1521
1532
  new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
1522
1533
  }
1523
1534
 
1524
1535
  ///////////////////////////////////////////////////////////////////////////////
1525
1536
 
1526
- /**
1537
+ /**
1527
1538
  * Seeded random number generator
1528
1539
  * - Can be used to create a deterministic random number sequence
1529
1540
  * @example
@@ -1550,8 +1561,8 @@ class RandomGenerator
1550
1561
  float(valueA=1, valueB=0)
1551
1562
  {
1552
1563
  // xorshift algorithm
1553
- this.seed ^= this.seed << 13;
1554
- this.seed ^= this.seed >>> 17;
1564
+ this.seed ^= this.seed << 13;
1565
+ this.seed ^= this.seed >>> 17;
1555
1566
  this.seed ^= this.seed << 5;
1556
1567
  return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
1557
1568
  }
@@ -1600,16 +1611,14 @@ class RandomGenerator
1600
1611
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
1601
1612
  * a = vec2(5); // set a to (5, 5)
1602
1613
  * b = vec2(); // set b to (0, 0)
1603
- * @memberof Utilities
1604
- */
1614
+ * @memberof Utilities */
1605
1615
  function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1606
1616
 
1607
- /**
1617
+ /**
1608
1618
  * Check if object is a valid Vector2
1609
1619
  * @param {any} v
1610
1620
  * @return {boolean}
1611
- * @memberof Utilities
1612
- */
1621
+ * @memberof Utilities */
1613
1622
  function isVector2(v) { return v instanceof Vector2; }
1614
1623
 
1615
1624
  // vector2 asserts
@@ -1618,10 +1627,10 @@ function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n);
1618
1627
  function ASSERT_VECTOR2_NORMAL(v)
1619
1628
  {
1620
1629
  ASSERT_VECTOR2_VALID(v);
1621
- ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1630
+ ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1622
1631
  }
1623
1632
 
1624
- /**
1633
+ /**
1625
1634
  * 2D Vector object with vector math library
1626
1635
  * - Functions do not change this so they can be chained together
1627
1636
  * @example
@@ -1745,7 +1754,7 @@ class Vector2
1745
1754
  * @param {number} [angle]
1746
1755
  * @param {number} [length]
1747
1756
  * @return {Vector2} */
1748
- setAngle(angle=0, length=1)
1757
+ setAngle(angle=0, length=1)
1749
1758
  {
1750
1759
  ASSERT_NUMBER_VALID(angle);
1751
1760
  ASSERT_NUMBER_VALID(length);
@@ -1760,7 +1769,7 @@ class Vector2
1760
1769
  rotate(angle)
1761
1770
  {
1762
1771
  ASSERT_NUMBER_VALID(angle);
1763
- const c = Math.cos(-angle), s = Math.sin(-angle);
1772
+ const c = Math.cos(-angle), s = Math.sin(-angle);
1764
1773
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1765
1774
  }
1766
1775
 
@@ -1772,9 +1781,9 @@ class Vector2
1772
1781
  ASSERT_NUMBER_VALID(direction);
1773
1782
  ASSERT_NUMBER_VALID(length);
1774
1783
  direction = mod(direction, 4);
1775
- ASSERT(direction==0 || direction==1 || direction==2 || direction==3,
1784
+ ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1776
1785
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1777
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1786
+ return vec2(direction%2 ? direction-1 ? -length : length : 0,
1778
1787
  direction%2 ? 0 : direction ? -length : length);
1779
1788
  }
1780
1789
 
@@ -1826,7 +1835,7 @@ class Vector2
1826
1835
  /** Returns this vector expressed as a string
1827
1836
  * @param {number} digits - precision to display
1828
1837
  * @return {string} */
1829
- toString(digits=3)
1838
+ toString(digits=3)
1830
1839
  {
1831
1840
  ASSERT_NUMBER_VALID(digits);
1832
1841
  if (debug)
@@ -1845,7 +1854,7 @@ class Vector2
1845
1854
 
1846
1855
  ///////////////////////////////////////////////////////////////////////////////
1847
1856
 
1848
- /**
1857
+ /**
1849
1858
  * Create a color object with RGBA values, white by default
1850
1859
  * @param {number} [r=1] - red
1851
1860
  * @param {number} [g=1] - green
@@ -1856,29 +1865,27 @@ class Vector2
1856
1865
  */
1857
1866
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1858
1867
 
1859
- /**
1868
+ /**
1860
1869
  * Create a color object with HSLA values, white by default
1861
1870
  * @param {number} [h=0] - hue
1862
1871
  * @param {number} [s=0] - saturation
1863
1872
  * @param {number} [l=1] - lightness
1864
1873
  * @param {number} [a=1] - alpha
1865
1874
  * @return {Color}
1866
- * @memberof Utilities
1867
- */
1875
+ * @memberof Utilities */
1868
1876
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
1869
1877
 
1870
- /**
1878
+ /**
1871
1879
  * Check if object is a valid Color
1872
1880
  * @param {any} c
1873
1881
  * @return {boolean}
1874
- * @memberof Utilities
1875
- */
1882
+ * @memberof Utilities */
1876
1883
  function isColor(c) { return c instanceof Color; }
1877
1884
 
1878
1885
  // color asserts
1879
1886
  function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c) && c.isValid(), 'Color is invalid.', c); }
1880
1887
 
1881
- /**
1888
+ /**
1882
1889
  * Color object (red, green, blue, alpha) with some helpful functions
1883
1890
  * @example
1884
1891
  * let a = new Color; // white
@@ -1950,7 +1957,7 @@ class Color
1950
1957
  * @param {number} scale
1951
1958
  * @param {number} [alphaScale=scale]
1952
1959
  * @return {Color} */
1953
- scale(scale, alphaScale=scale)
1960
+ scale(scale, alphaScale=scale)
1954
1961
  { return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
1955
1962
 
1956
1963
  /** Returns a copy of this color clamped to the valid range between 0 and 1
@@ -1967,9 +1974,9 @@ class Color
1967
1974
  ASSERT_NUMBER_VALID(percent);
1968
1975
  const p = clamp(percent);
1969
1976
  return new Color(
1970
- c.r*p + this.r*(1-p),
1971
- c.g*p + this.g*(1-p),
1972
- c.b*p + this.b*(1-p),
1977
+ c.r*p + this.r*(1-p),
1978
+ c.g*p + this.g*(1-p),
1979
+ c.b*p + this.b*(1-p),
1973
1980
  c.a*p + this.a*(1-p));
1974
1981
  }
1975
1982
 
@@ -2009,15 +2016,15 @@ class Color
2009
2016
  const minC = min(r, g, b);
2010
2017
  const l = (maxC + minC) / 2;
2011
2018
  let h = 0, s = 0;
2012
- if (maxC != minC)
2019
+ if (maxC !== minC)
2013
2020
  {
2014
2021
  let d = maxC - minC;
2015
2022
  s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
2016
- if (r == maxC)
2023
+ if (r === maxC)
2017
2024
  h = (g - b) / d + (g < b ? 6 : 0);
2018
- else if (g == maxC)
2025
+ else if (g === maxC)
2019
2026
  h = (b - r) / d + 2;
2020
- else if (b == maxC)
2027
+ else if (b === maxC)
2021
2028
  h = (r - g) / d + 4;
2022
2029
  }
2023
2030
  return [h / 6, s, l, a];
@@ -2027,7 +2034,7 @@ class Color
2027
2034
  * @param {number} [amount]
2028
2035
  * @param {number} [alphaAmount]
2029
2036
  * @return {Color} */
2030
- mutate(amount=.05, alphaAmount=0)
2037
+ mutate(amount=.05, alphaAmount=0)
2031
2038
  {
2032
2039
  ASSERT_NUMBER_VALID(amount);
2033
2040
  ASSERT_NUMBER_VALID(alphaAmount);
@@ -2043,47 +2050,47 @@ class Color
2043
2050
  /** Returns this color expressed as a hex color code
2044
2051
  * @param {boolean} [useAlpha] - if alpha should be included in result
2045
2052
  * @return {string} */
2046
- toString(useAlpha = true)
2053
+ toString(useAlpha = true)
2047
2054
  {
2048
- ASSERT(typeof useAlpha == 'boolean', 'Use alpha boolean is invalid.', useAlpha);
2055
+ ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
2049
2056
  if (debug && !this.isValid())
2050
2057
  return `#000`;
2051
2058
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
2052
2059
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
2053
2060
  }
2054
-
2061
+
2055
2062
  /** Set this color from a hex code
2056
2063
  * @param {string} hex - html hex code
2057
2064
  * @return {Color} */
2058
2065
  setHex(hex)
2059
2066
  {
2060
- ASSERT(typeof hex == 'string' && hex[0] == '#', 'Color hex code must be a string starting with #');
2067
+ ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
2061
2068
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
2062
2069
 
2063
2070
  if (hex.length < 6)
2064
2071
  {
2065
2072
  const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
2066
2073
  this.r = fromHex(1);
2067
- this.g = fromHex(2),
2074
+ this.g = fromHex(2);
2068
2075
  this.b = fromHex(3);
2069
- this.a = hex.length == 5 ? fromHex(4) : 1;
2076
+ this.a = hex.length === 5 ? fromHex(4) : 1;
2070
2077
  }
2071
2078
  else
2072
2079
  {
2073
2080
  const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
2074
2081
  this.r = fromHex(1);
2075
- this.g = fromHex(3),
2082
+ this.g = fromHex(3);
2076
2083
  this.b = fromHex(5);
2077
- this.a = hex.length == 9 ? fromHex(7) : 1;
2084
+ this.a = hex.length === 9 ? fromHex(7) : 1;
2078
2085
  }
2079
2086
 
2080
2087
  ASSERT_COLOR_VALID(this);
2081
2088
  return this;
2082
2089
  }
2083
-
2090
+
2084
2091
  /** Returns this color expressed as 32 bit RGBA value
2085
2092
  * @return {number} */
2086
- rgbaInt()
2093
+ rgbaInt()
2087
2094
  {
2088
2095
  const r = clamp(this.r)*255|0;
2089
2096
  const g = clamp(this.g)*255<<8;
@@ -2104,7 +2111,7 @@ class Color
2104
2111
  /** Color - White #ffffff
2105
2112
  * @type {Color}
2106
2113
  * @memberof Utilities */
2107
- const WHITE = rgb();
2114
+ const WHITE = rgb();
2108
2115
 
2109
2116
  /** Color - Clear White #ffffff with 0 alpha
2110
2117
  * @type {Color}
@@ -2219,11 +2226,11 @@ class Timer
2219
2226
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
2220
2227
  * @return {number} */
2221
2228
  getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
2222
-
2229
+
2223
2230
  /** Returns this timer expressed as a string
2224
2231
  * @return {string} */
2225
2232
  toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
2226
-
2233
+
2227
2234
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2228
2235
  * @return {number} */
2229
2236
  valueOf() { return this.get(); }
@@ -2259,7 +2266,7 @@ let cameraScale = 32;
2259
2266
  // Display settings
2260
2267
 
2261
2268
  /** Enable applying color to tiles when using canvas2d
2262
- * - This is slower but should be the same as webgl rendering
2269
+ * - This is slower but should be the same as WebGL rendering
2263
2270
  * @type {boolean}
2264
2271
  * @default
2265
2272
  * @memberof Settings */
@@ -2305,14 +2312,13 @@ let tilesPixelated = true;
2305
2312
  * @memberof Settings */
2306
2313
  let fontDefault = 'arial';
2307
2314
 
2308
- /** Enable to show the LittleJS splash screen be shown on startup
2315
+ /** Enable to show the LittleJS splash screen on startup
2309
2316
  * @type {boolean}
2310
2317
  * @default
2311
2318
  * @memberof Settings */
2312
2319
  let showSplashScreen = false;
2313
2320
 
2314
2321
  /** Disables all rendering, audio, and input for servers
2315
- * - Must be set before startup to take effect
2316
2322
  * @type {boolean}
2317
2323
  * @default
2318
2324
  * @memberof Settings */
@@ -2321,13 +2327,18 @@ let headlessMode = false;
2321
2327
  ///////////////////////////////////////////////////////////////////////////////
2322
2328
  // WebGL settings
2323
2329
 
2324
- /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
2325
- * - Must be set before startup to take effect
2330
+ /** Enable WebGL accelerated rendering
2326
2331
  * @type {boolean}
2327
2332
  * @default
2328
2333
  * @memberof Settings */
2329
2334
  let glEnable = true;
2330
2335
 
2336
+ /** How many sided poly to use when drawing circles and ellipses with WebGL
2337
+ * @type {number}
2338
+ * @default
2339
+ * @memberof Settings */
2340
+ let glCircleSides = 32;
2341
+
2331
2342
  ///////////////////////////////////////////////////////////////////////////////
2332
2343
  // Tile sheet settings
2333
2344
 
@@ -2409,13 +2420,13 @@ let particleEmitRateScale = 1;
2409
2420
  * @memberof Settings */
2410
2421
  let gamepadsEnable = true;
2411
2422
 
2412
- /** If true, the dpad input is also routed to the left analog stick (for better accessability)
2423
+ /** If true, the dpad input is also routed to the left analog stick (for better accessibility)
2413
2424
  * @type {boolean}
2414
2425
  * @default
2415
2426
  * @memberof Settings */
2416
2427
  let gamepadDirectionEmulateStick = true;
2417
2428
 
2418
- /** If true the WASD keys are also routed to the direction keys (for better accessability)
2429
+ /** If true the WASD keys are also routed to the direction keys (for better accessibility)
2419
2430
  * @type {boolean}
2420
2431
  * @default
2421
2432
  * @memberof Settings */
@@ -2423,7 +2434,6 @@ let inputWASDEmulateDirection = true;
2423
2434
 
2424
2435
  /** True if touch input is enabled for mobile devices
2425
2436
  * - Touch events will be routed to mouse events
2426
- * - Must be set before startup to take effect
2427
2437
  * @type {boolean}
2428
2438
  * @default
2429
2439
  * @memberof Settings */
@@ -2431,7 +2441,6 @@ let touchInputEnable = true;
2431
2441
 
2432
2442
  /** True if touch gamepad should appear on mobile devices
2433
2443
  * - Supports left analog stick, 4 face buttons and start button (button 9)
2434
- * - Must be set before startup to take effect
2435
2444
  * @type {boolean}
2436
2445
  * @default
2437
2446
  * @memberof Settings */
@@ -2534,7 +2543,7 @@ function setCameraAngle(angle) { cameraAngle = angle; }
2534
2543
  function setCameraScale(scale) { cameraScale = scale; }
2535
2544
 
2536
2545
  /** Set if tiles should be colorized when using canvas2d
2537
- * This can be slower but results should look nearly identical to webgl rendering
2546
+ * This can be slower but results should look nearly identical to WebGL rendering
2538
2547
  * It can be enabled/disabled at any time
2539
2548
  * Optimized for performance, and will use faster method if color is white or untextured
2540
2549
  * @param {boolean} colorTiles
@@ -2570,8 +2579,8 @@ function setCanvasPixelated(pixelated)
2570
2579
  * @param {boolean} pixelated
2571
2580
  * @memberof Settings */
2572
2581
  function setOverlayCanvasPixelated(pixelated)
2573
- {
2574
- overlayCanvasPixelated = pixelated;
2582
+ {
2583
+ overlayCanvasPixelated = pixelated;
2575
2584
  if (overlayCanvas)
2576
2585
  overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2577
2586
  }
@@ -2586,7 +2595,7 @@ function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
2586
2595
  * @memberof Settings */
2587
2596
  function setFontDefault(font) { fontDefault = font; }
2588
2597
 
2589
- /** Set if the LittleJS splash screen be shown on startup
2598
+ /** Set if the LittleJS splash screen should be shown on startup
2590
2599
  * @param {boolean} show
2591
2600
  * @memberof Settings */
2592
2601
  function setShowSplashScreen(show) { showSplashScreen = show; }
@@ -2596,16 +2605,21 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
2596
2605
  * @memberof Settings */
2597
2606
  function setHeadlessMode(headless) { headlessMode = headless; }
2598
2607
 
2599
- /** Set if webgl rendering is enabled
2608
+ /** Set if WebGL rendering is enabled
2600
2609
  * @param {boolean} enable
2601
2610
  * @memberof Settings */
2602
2611
  function setGLEnable(enable)
2603
2612
  {
2604
2613
  glEnable = enable;
2605
- if (glCanvas) // hide glCanvas if webgl is disabled
2614
+ if (glCanvas) // hide glCanvas if WebGL is disabled
2606
2615
  glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2607
2616
  }
2608
2617
 
2618
+ /** Set how many sided polygons to use when drawing circles and elipses with WebGL
2619
+ * @param {number} sides
2620
+ * @memberof Settings */
2621
+ function setGLCircleSides(sides) { glCircleSides = sides; }
2622
+
2609
2623
  /** Set default size of tiles in pixels
2610
2624
  * @param {Vector2} size
2611
2625
  * @memberof Settings */
@@ -2636,7 +2650,7 @@ function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
2636
2650
  * @memberof Settings */
2637
2651
  function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
2638
2652
 
2639
- /** Set how much to bounce when a collision occur
2653
+ /** Set how much to bounce when a collision occurs
2640
2654
  * @param {number} restitution
2641
2655
  * @memberof Settings */
2642
2656
  function setObjectDefaultRestitution(restitution) { objectDefaultRestitution = restitution; }
@@ -2760,11 +2774,11 @@ function setShowWatermark(show) { showWatermark = show; }
2760
2774
  * @param {string} key
2761
2775
  * @memberof Debug */
2762
2776
  function setDebugKey(key) { debugKey = key; }
2763
- /**
2777
+ /**
2764
2778
  * LittleJS Object System
2765
2779
  */
2766
2780
 
2767
- /**
2781
+ /**
2768
2782
  * LittleJS Object Base Object Class
2769
2783
  * - Top level object class used by the engine
2770
2784
  * - Automatically adds self to object list
@@ -2787,7 +2801,7 @@ function setDebugKey(key) { debugKey = key; }
2787
2801
  * @example
2788
2802
  * // create an engine object, normally you would first extend the class with your own
2789
2803
  * const pos = vec2(2,3);
2790
- * const object = new EngineObject(pos);
2804
+ * const object = new EngineObject(pos);
2791
2805
  */
2792
2806
  class EngineObject
2793
2807
  {
@@ -2805,9 +2819,9 @@ class EngineObject
2805
2819
  ASSERT(isVector2(pos) && pos.isValid(), 'object pos should be a vec2');
2806
2820
  ASSERT(isVector2(size) && size.isValid(), 'object size should be a vec2');
2807
2821
  ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
2808
- ASSERT(typeof angle == 'number' && isFinite(angle), 'object angle should be a number');
2822
+ ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2809
2823
  ASSERT(isColor(color) && color.isValid(), 'object color should be a valid rgba color');
2810
- ASSERT(typeof renderOrder == 'number', 'object renderOrder should be a number');
2824
+ ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
2811
2825
 
2812
2826
  /** @property {Vector2} - World space position of the object */
2813
2827
  this.pos = pos.copy();
@@ -2875,7 +2889,7 @@ class EngineObject
2875
2889
  // add to list of objects
2876
2890
  engineObjects.push(this);
2877
2891
  }
2878
-
2892
+
2879
2893
  /** Update the object transform, called automatically by engine even when paused */
2880
2894
  updateTransforms()
2881
2895
  {
@@ -2944,7 +2958,7 @@ class EngineObject
2944
2958
  for (const o of engineObjectsCollide)
2945
2959
  {
2946
2960
  // non solid objects don't collide with each other
2947
- if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o == this)
2961
+ if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
2948
2962
  continue;
2949
2963
 
2950
2964
  // check collision
@@ -2967,7 +2981,7 @@ class EngineObject
2967
2981
  this.velocity = this.velocity.add(velocity);
2968
2982
  if (o.mass) // push away if not fixed
2969
2983
  o.velocity = o.velocity.subtract(velocity);
2970
-
2984
+
2971
2985
  debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
2972
2986
  continue;
2973
2987
  }
@@ -2978,7 +2992,7 @@ class EngineObject
2978
2992
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
2979
2993
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
2980
2994
  const restitution = max(this.restitution, o.restitution);
2981
-
2995
+
2982
2996
  if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
2983
2997
  {
2984
2998
  // push outside object collision
@@ -3066,7 +3080,7 @@ class EngineObject
3066
3080
  {
3067
3081
  // move to previous position
3068
3082
  this.pos.y = oldPos.y;
3069
- this.groundObject = undefined;
3083
+ this.groundObject = undefined;
3070
3084
  }
3071
3085
  }
3072
3086
  if (blockedLayerX)
@@ -3080,20 +3094,20 @@ class EngineObject
3080
3094
  }
3081
3095
  }
3082
3096
  }
3083
-
3097
+
3084
3098
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
3085
3099
  render()
3086
3100
  {
3087
3101
  // default object render
3088
3102
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
3089
3103
  }
3090
-
3104
+
3091
3105
  /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
3092
3106
  destroy()
3093
- {
3107
+ {
3094
3108
  if (this.destroyed)
3095
3109
  return;
3096
-
3110
+
3097
3111
  // disconnect from parent and destroy children
3098
3112
  this.destroyed = 1;
3099
3113
  this.parent && this.parent.removeChild(this);
@@ -3119,7 +3133,7 @@ class EngineObject
3119
3133
  /** Convert from world space to local space for a vector (rotation only)
3120
3134
  * @param {Vector2} vec - world space vector */
3121
3135
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
3122
-
3136
+
3123
3137
  /** Called to check if a tile collision should be resolved
3124
3138
  * @param {number} tileData - the value of the tile at the position
3125
3139
  * @param {Vector2} pos - tile where the collision occurred
@@ -3138,16 +3152,19 @@ class EngineObject
3138
3152
 
3139
3153
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
3140
3154
  * @param {Vector2} acceleration */
3141
- applyAcceleration(acceleration) { if (this.mass) this.velocity = this.velocity.add(acceleration); }
3155
+ applyAcceleration(acceleration)
3156
+ { if (this.mass) this.velocity = this.velocity.add(acceleration); }
3142
3157
 
3143
- /** Apply angular acceleration to this object
3158
+ /** Apply angular acceleration to this object
3144
3159
  * @param {number} acceleration */
3145
- applyAngularAcceleration(acceleration) { if (this.mass) this.angleVelocity += acceleration; }
3160
+ applyAngularAcceleration(acceleration)
3161
+ { if (this.mass) this.angleVelocity += acceleration; }
3146
3162
 
3147
3163
  /** Apply force to this object (adjust velocity, affected by mass)
3148
3164
  * @param {Vector2} force */
3149
- applyForce(force) { this.applyAcceleration(force.scale(1/this.mass)); }
3150
-
3165
+ applyForce(force)
3166
+ { if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
3167
+
3151
3168
  /** Get the direction of the mirror
3152
3169
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3153
3170
  getMirrorSign() { return this.mirror ? -1 : 1; }
@@ -3169,7 +3186,7 @@ class EngineObject
3169
3186
  * @param {EngineObject} child */
3170
3187
  removeChild(child)
3171
3188
  {
3172
- ASSERT(child.parent == this && this.children.includes(child));
3189
+ ASSERT(child.parent === this && this.children.includes(child));
3173
3190
  this.children.splice(this.children.indexOf(child), 1);
3174
3191
  child.parent = 0;
3175
3192
  }
@@ -3215,7 +3232,7 @@ class EngineObject
3215
3232
  {
3216
3233
  if (!debug)
3217
3234
  return;
3218
-
3235
+
3219
3236
  // show object info for debugging
3220
3237
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
3221
3238
  const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
@@ -3225,24 +3242,24 @@ class EngineObject
3225
3242
  this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(1,1,1,.5));
3226
3243
  }
3227
3244
  }
3228
- /**
3245
+ /**
3229
3246
  * LittleJS Drawing System
3230
3247
  * - Hybrid system with both Canvas2D and WebGL available
3231
3248
  * - Super fast tile sheet rendering with WebGL
3232
3249
  * - Can apply rotation, mirror, color and additive color
3233
3250
  * - Font rendering system with built in engine font
3234
3251
  * - Many useful utility functions
3235
- *
3252
+ *
3236
3253
  * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
3237
3254
  * There are 3 canvas/contexts available to draw to...
3238
3255
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
3239
3256
  * glCanvas - Used by the accelerated WebGL batch rendering system.
3240
3257
  * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
3241
- *
3258
+ *
3242
3259
  * The WebGL rendering system is very fast with some caveats...
3243
3260
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
3244
3261
  * - Group additive rendering together using renderOrder to mitigate this issue
3245
- *
3262
+ *
3246
3263
  * The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
3247
3264
  * @namespace Draw
3248
3265
  */
@@ -3287,7 +3304,7 @@ let workCanvas;
3287
3304
  * @memberof Draw */
3288
3305
  let workContext;
3289
3306
 
3290
- /** The size of the main canvas (and other secondary canvases)
3307
+ /** The size of the main canvas (and other secondary canvases)
3291
3308
  * @type {Vector2}
3292
3309
  * @memberof Draw */
3293
3310
  let mainCanvasSize = vec2();
@@ -3302,7 +3319,7 @@ let drawCount;
3302
3319
 
3303
3320
  ///////////////////////////////////////////////////////////////////////////////
3304
3321
 
3305
- /**
3322
+ /**
3306
3323
  * Create a tile info object using a grid based system
3307
3324
  * - This can take vecs or floats for easier use and conversion
3308
3325
  * - If an index is passed in, the tile size and index will determine the position
@@ -3316,15 +3333,14 @@ let drawCount;
3316
3333
  * tile(5, 8) // a tile at index 5 using a tile size of 8
3317
3334
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3318
3335
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3319
- * @memberof Draw
3320
- */
3336
+ * @memberof Draw */
3321
3337
  function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3322
3338
  {
3323
3339
  if (headlessMode)
3324
3340
  return new TileInfo;
3325
3341
 
3326
3342
  // if size is a number, make it a vector
3327
- if (typeof size == 'number')
3343
+ if (typeof size === 'number')
3328
3344
  {
3329
3345
  ASSERT(size > 0);
3330
3346
  size = new Vector2(size, size);
@@ -3333,24 +3349,24 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3333
3349
  // create tile info object
3334
3350
  const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
3335
3351
 
3336
- // use get the pos of the tile
3352
+ // get the position of the tile
3337
3353
  const textureInfo = textureInfos[textureIndex];
3338
3354
  ASSERT(!!textureInfo, 'Texture not loaded');
3339
3355
  const sizePaddedX = size.x + padding*2;
3340
3356
  const sizePaddedY = size.y + padding*2;
3341
- if (typeof pos == 'number')
3357
+ if (typeof pos === 'number')
3342
3358
  {
3343
3359
  const cols = textureInfo.size.x / sizePaddedX |0;
3344
- ASSERT(cols>0, 'Tile size is too big for texture');
3360
+ ASSERT(cols > 0, 'Tile size is too big for texture');
3345
3361
  const posX = pos % cols, posY = (pos / cols) |0;
3346
3362
  tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
3347
3363
  }
3348
3364
  else
3349
3365
  tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
3350
- return tileInfo;
3366
+ return tileInfo;
3351
3367
  }
3352
3368
 
3353
- /**
3369
+ /**
3354
3370
  * Tile Info - Stores info about how to draw a tile
3355
3371
  */
3356
3372
  class TileInfo
@@ -3388,14 +3404,14 @@ class TileInfo
3388
3404
  */
3389
3405
  frame(frame)
3390
3406
  {
3391
- ASSERT(typeof frame == 'number');
3407
+ ASSERT(typeof frame === 'number');
3392
3408
  return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
3393
3409
  }
3394
3410
 
3395
3411
  /**
3396
3412
  * Set this tile to use a full image
3397
3413
  * @param {HTMLImageElement|OffscreenCanvas} image
3398
- * @param {WebGLTexture} [glTexture] - webgl texture
3414
+ * @param {WebGLTexture} [glTexture] - WebGL texture
3399
3415
  * @return {TileInfo}
3400
3416
  */
3401
3417
  setFullImage(image, glTexture)
@@ -3413,7 +3429,7 @@ class TextureInfo
3413
3429
  /**
3414
3430
  * Create a TextureInfo, called automatically by the engine
3415
3431
  * @param {HTMLImageElement|OffscreenCanvas} image
3416
- * @param {WebGLTexture} [glTexture] - webgl texture
3432
+ * @param {WebGLTexture} [glTexture] - WebGL texture
3417
3433
  */
3418
3434
  constructor(image, glTexture)
3419
3435
  {
@@ -3423,7 +3439,7 @@ class TextureInfo
3423
3439
  this.size = vec2(image.width, image.height);
3424
3440
  /** @property {Vector2} - inverse of the size, cached for rendering */
3425
3441
  this.sizeInverse = vec2(1/image.width, 1/image.height);
3426
- /** @property {WebGLTexture} - webgl texture */
3442
+ /** @property {WebGLTexture} - WebGL texture */
3427
3443
  this.glTexture = glTexture;
3428
3444
  }
3429
3445
 
@@ -3439,28 +3455,25 @@ class TextureInfo
3439
3455
  // Drawing functions
3440
3456
 
3441
3457
  /** Draw textured tile centered in world space, with color applied if using WebGL
3442
- * @param {Vector2} pos - Center of the tile in world space
3443
- * @param {Vector2} [size=(1,1)] - Size of the tile in world space
3444
- * @param {TileInfo}[tileInfo] - Tile info to use, untextured if undefined
3445
- * @param {Color} [color=(1,1,1,1)] - Color to modulate with
3446
- * @param {number} [angle] - Angle to rotate by
3447
- * @param {boolean} [mirror] - If true image is flipped along the Y axis
3448
- * @param {Color} [additiveColor] - Additive color to be applied if any
3449
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
3450
- * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
3458
+ * @param {Vector2} pos - Center of the tile in world space
3459
+ * @param {Vector2} [size=(1,1)] - Size of the tile in world space
3460
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
3461
+ * @param {Color} [color=(1,1,1,1)] - Color to modulate with
3462
+ * @param {number} [angle] - Angle to rotate by
3463
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
3464
+ * @param {Color} [additiveColor] - Additive color to be applied if any
3465
+ * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
3466
+ * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
3451
3467
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
3452
3468
  * @memberof Draw */
3453
- function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
3469
+ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3454
3470
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
3455
3471
  {
3456
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3457
3472
  ASSERT(isVector2(pos) && pos.isValid(), 'drawTile pos should be a vec2');
3458
3473
  ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
3459
3474
  ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
3460
3475
  ASSERT(isNumber(angle), 'drawTile angle should be a number');
3461
-
3462
- if (color.a <= 0 && (!additiveColor || additiveColor.a <= 0) || !size.x || !size.y)
3463
- return; // completely invisible, skip render
3476
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3464
3477
 
3465
3478
  const textureInfo = tileInfo && tileInfo.textureInfo;
3466
3479
  if (useWebGL)
@@ -3484,22 +3497,22 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
3484
3497
  {
3485
3498
  const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
3486
3499
  const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
3487
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
3488
- x + tileImageFixBleedX, y + tileImageFixBleedY,
3489
- x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
3490
- color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
3500
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
3501
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
3502
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
3503
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
3491
3504
  }
3492
3505
  else
3493
3506
  {
3494
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
3495
- x, y, x + w, y + h,
3496
- color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
3507
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
3508
+ x, y, x + w, y + h,
3509
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
3497
3510
  }
3498
3511
  }
3499
3512
  else
3500
3513
  {
3501
3514
  // if no tile info, force untextured
3502
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
3515
+ glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
3503
3516
  }
3504
3517
  }
3505
3518
  else
@@ -3537,8 +3550,8 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
3537
3550
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3538
3551
  * @memberof Draw */
3539
3552
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3540
- {
3541
- drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
3553
+ {
3554
+ drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
3542
3555
  }
3543
3556
 
3544
3557
  /** Draw colored line between two points
@@ -3561,6 +3574,30 @@ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL
3561
3574
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3562
3575
  }
3563
3576
 
3577
+ /** Draw colored regular polygon using passed in number of sides
3578
+ * @param {Vector2} pos
3579
+ * @param {Vector2} [size=(1,1)]
3580
+ * @param {number} [sides]
3581
+ * @param {Color} [color=(1,1,1,1)]
3582
+ * @param {number} [angle]
3583
+ * @param {number} [lineWidth]
3584
+ * @param {Color} [lineColor=(0,0,0,1)]
3585
+ * @param {boolean} [useWebGL=glEnable]
3586
+ * @param {boolean} [screenSpace]
3587
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3588
+ * @memberof Draw */
3589
+ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3590
+ {
3591
+ // build regular polygon points
3592
+ const points = [];
3593
+ for (let i=sides; i--;)
3594
+ {
3595
+ const a = (i/sides)*PI*2;
3596
+ points.push(vec2(Math.sin(a)*size.x, Math.cos(a)*size.y));
3597
+ }
3598
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3599
+ }
3600
+
3564
3601
  /** Draw colored polygon using passed in points
3565
3602
  * @param {Array<Vector2>} points - Array of Vector2 points
3566
3603
  * @param {Color} [color=(1,1,1,1)]
@@ -3568,33 +3605,49 @@ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL
3568
3605
  * @param {Color} [lineColor=(0,0,0,1)]
3569
3606
  * @param {Vector2} [pos=(0,0)] - Offset to apply
3570
3607
  * @param {number} [angle] - Angle to rotate by
3571
- * @param {boolean} [useWebGL] - Webgl not supported
3608
+ * @param {boolean} [useWebGL=glEnable]
3572
3609
  * @param {boolean} [screenSpace]
3573
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3610
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3574
3611
  * @memberof Draw */
3575
- function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=false, screenSpace=false, context=drawContext)
3612
+ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3576
3613
  {
3577
3614
  ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
3578
3615
  ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3579
3616
  ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3580
3617
  ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3581
3618
  ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3582
- ASSERT(!useWebGL, 'drawPoly webgl not supported');
3583
- drawCanvas2D(pos, vec2(1), angle, false, context=>
3619
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3620
+ if (useWebGL)
3584
3621
  {
3585
- context.beginPath();
3586
- for (const point of points)
3587
- context.lineTo(point.x, point.y);
3588
- context.closePath();
3589
- context.fillStyle = color.toString();
3590
- context.fill();
3591
- if (lineWidth)
3622
+ let scale = 1;
3623
+ if (screenSpace)
3592
3624
  {
3593
- context.strokeStyle = lineColor.toString();
3594
- context.lineWidth = lineWidth;
3595
- context.stroke();
3625
+ // convert to world space
3626
+ pos = screenToWorld(pos);
3627
+ scale = 1/cameraScale;
3596
3628
  }
3597
- }, screenSpace, context);
3629
+ glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
3630
+ if (lineWidth > 0)
3631
+ glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
3632
+ }
3633
+ else
3634
+ {
3635
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3636
+ {
3637
+ context.fillStyle = color.toString();
3638
+ context.beginPath();
3639
+ for (const point of points)
3640
+ context.lineTo(point.x, point.y);
3641
+ context.closePath();
3642
+ context.fill();
3643
+ if (lineWidth)
3644
+ {
3645
+ context.strokeStyle = lineColor.toString();
3646
+ context.lineWidth = lineWidth;
3647
+ context.stroke();
3648
+ }
3649
+ }, screenSpace, context);
3650
+ }
3598
3651
  }
3599
3652
 
3600
3653
  /** Draw colored ellipse using passed in point
@@ -3604,32 +3657,41 @@ function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec
3604
3657
  * @param {number} [angle]
3605
3658
  * @param {number} [lineWidth]
3606
3659
  * @param {Color} [lineColor=(0,0,0,1)]
3607
- * @param {boolean} [useWebGL] - Webgl not supported
3660
+ * @param {boolean} [useWebGL=glEnable]
3608
3661
  * @param {boolean} [screenSpace]
3609
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3662
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3610
3663
  * @memberof Draw */
3611
- function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace=false, context=drawContext)
3664
+ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3612
3665
  {
3613
3666
  ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
3614
3667
  ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
3615
3668
  ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3616
3669
  ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3617
3670
  ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
3618
- ASSERT(lineWidth>=0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3619
- ASSERT(!useWebGL, 'drawEllipse webgl not supported');
3620
- drawCanvas2D(pos, vec2(1), angle, false, context=>
3671
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3672
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3673
+ if (useWebGL)
3621
3674
  {
3622
- context.beginPath();
3623
- context.ellipse(0, 0, size.y, size.x, 0, 0, 9);
3624
- context.fillStyle = color.toString();
3625
- context.fill();
3626
- if (lineWidth)
3675
+ // draw as a regular polygon
3676
+ const sides = glCircleSides;
3677
+ drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
3678
+ }
3679
+ else
3680
+ {
3681
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3627
3682
  {
3628
- context.strokeStyle = lineColor.toString();
3629
- context.lineWidth = lineWidth;
3630
- context.stroke();
3631
- }
3632
- }, screenSpace, context);
3683
+ context.fillStyle = color.toString();
3684
+ context.beginPath();
3685
+ context.ellipse(0, 0, size.x, size.y, 0, 0, 9);
3686
+ context.fill();
3687
+ if (lineWidth)
3688
+ {
3689
+ context.strokeStyle = lineColor.toString();
3690
+ context.lineWidth = lineWidth;
3691
+ context.stroke();
3692
+ }
3693
+ }, screenSpace, context);
3694
+ }
3633
3695
  }
3634
3696
 
3635
3697
  /** Draw colored circle using passed in point
@@ -3638,11 +3700,11 @@ function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, l
3638
3700
  * @param {Color} [color=(1,1,1,1)]
3639
3701
  * @param {number} [lineWidth=0]
3640
3702
  * @param {Color} [lineColor=(0,0,0,1)]
3641
- * @param {boolean} [useWebGL] - Webgl not supported
3642
- * @param {boolean} [screenSpace=false]
3643
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3703
+ * @param {boolean} [useWebGL=glEnable]
3704
+ * @param {boolean} [screenSpace]
3705
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3644
3706
  * @memberof Draw */
3645
- function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace, context=drawContext)
3707
+ function drawCircle(pos, radius=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3646
3708
  { drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
3647
3709
 
3648
3710
  /** Draw directly to a 2d canvas context in world space
@@ -3651,7 +3713,7 @@ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK
3651
3713
  * @param {number} angle
3652
3714
  * @param {boolean} [mirror]
3653
3715
  * @param {Function} [drawFunction]
3654
- * @param {boolean} [screenSpace=false]
3716
+ * @param {boolean} [screenSpace=false]
3655
3717
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3656
3718
  * @memberof Draw */
3657
3719
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
@@ -3721,7 +3783,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3721
3783
  * @param {number} [maxWidth]
3722
3784
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3723
3785
  * @memberof Draw */
3724
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
3786
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3725
3787
  {
3726
3788
  context.fillStyle = color.toString();
3727
3789
  context.strokeStyle = lineColor.toString();
@@ -3753,7 +3815,7 @@ function screenToWorld(screenPos)
3753
3815
  {
3754
3816
  let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3755
3817
  let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3756
- if (cameraAngle)
3818
+ if (cameraAngle)
3757
3819
  {
3758
3820
  // apply camera rotation
3759
3821
  const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
@@ -3846,7 +3908,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3846
3908
  {
3847
3909
  // white texture with no additive alpha, no need to tint
3848
3910
  context.globalAlpha = color.a;
3849
- context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3911
+ context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3850
3912
  context.globalAlpha = 1;
3851
3913
  }
3852
3914
  else
@@ -3867,7 +3929,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3867
3929
  for (let i = 0; i < data.length; ++i)
3868
3930
  data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
3869
3931
  workContext.putImageData(imageData, 0, 0);
3870
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3932
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3871
3933
  }
3872
3934
  else
3873
3935
  {
@@ -3880,7 +3942,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3880
3942
  }
3881
3943
  workContext.putImageData(imageData, 0, 0);
3882
3944
  context.globalAlpha = color.a;
3883
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3945
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3884
3946
  context.globalAlpha = 1;
3885
3947
  }
3886
3948
  }
@@ -3907,7 +3969,7 @@ function toggleFullscreen()
3907
3969
  }
3908
3970
 
3909
3971
  /** Set the cursor style
3910
- * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
3972
+ * @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
3911
3973
  * @memberof Draw */
3912
3974
  function setCursor(cursorStyle = 'auto')
3913
3975
  {
@@ -3919,7 +3981,7 @@ function setCursor(cursorStyle = 'auto')
3919
3981
 
3920
3982
  let engineFontImage;
3921
3983
 
3922
- /**
3984
+ /**
3923
3985
  * Font Image Object - Draw text on a 2D canvas by using characters in an image
3924
3986
  * - 96 characters (from space to tilde) are stored in an image
3925
3987
  * - Uses a default 8x8 font if none is supplied
@@ -3927,7 +3989,7 @@ let engineFontImage;
3927
3989
  * @example
3928
3990
  * // use built in font
3929
3991
  * const font = new FontImage;
3930
- *
3992
+ *
3931
3993
  * // draw text
3932
3994
  * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
3933
3995
  */
@@ -3937,7 +3999,6 @@ class FontImage
3937
3999
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
3938
4000
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3939
4001
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
3940
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
3941
4002
  */
3942
4003
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
3943
4004
  {
@@ -3951,7 +4012,6 @@ class FontImage
3951
4012
  this.image = image || engineFontImage;
3952
4013
  this.tileSize = tileSize;
3953
4014
  this.paddingSize = paddingSize;
3954
- this.context = context;
3955
4015
  }
3956
4016
 
3957
4017
  /** Draw text in world space using the image font
@@ -3959,23 +4019,32 @@ class FontImage
3959
4019
  * @param {Vector2} pos
3960
4020
  * @param {number} [scale=.25]
3961
4021
  * @param {boolean} [center]
4022
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
3962
4023
  */
3963
- drawText(text, pos, scale=1, center)
4024
+ drawText(text, pos, scale=1, center, context=drawContext)
3964
4025
  {
3965
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
4026
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
3966
4027
  }
3967
4028
 
3968
- /** Draw text in screen space using the image font
4029
+ /** Draw text on overlay canvas in world space using the image font
4030
+ * @param {string} text
4031
+ * @param {Vector2} pos
4032
+ * @param {number} [scale]
4033
+ * @param {boolean} [center]
4034
+ */
4035
+ drawTextOverlay(text, pos, scale=4, center)
4036
+ { this.drawText(text, pos, scale, center, overlayContext); }
4037
+
4038
+ /** Draw text on overlay canvas in screen space using the image font
3969
4039
  * @param {string} text
3970
4040
  * @param {Vector2} pos
3971
4041
  * @param {number} [scale]
3972
4042
  * @param {boolean} [center]
4043
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3973
4044
  */
3974
- drawTextScreen(text, pos, scale=4, center)
4045
+ drawTextScreen(text, pos, scale=4, center, context=overlayContext)
3975
4046
  {
3976
- const context = this.context;
3977
4047
  context.save();
3978
-
3979
4048
  const size = this.tileSize;
3980
4049
  const drawSize = size.add(this.paddingSize).scale(scale);
3981
4050
  const cols = this.image.width / this.tileSize.x |0;
@@ -3994,15 +4063,14 @@ class FontImage
3994
4063
  const x = tile % cols;
3995
4064
  const y = tile / cols |0;
3996
4065
  const drawPos = pos.add(vec2(j,i).multiply(drawSize));
3997
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
4066
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
3998
4067
  drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
3999
4068
  }
4000
4069
  });
4001
-
4002
4070
  context.restore();
4003
4071
  }
4004
4072
  }
4005
- /**
4073
+ /**
4006
4074
  * LittleJS Input System
4007
4075
  * - Tracks keyboard down, pressed, and released
4008
4076
  * - Tracks mouse buttons, position, and wheel
@@ -4018,10 +4086,10 @@ class FontImage
4018
4086
  * @return {boolean}
4019
4087
  * @memberof Input */
4020
4088
  function keyIsDown(key, device=0)
4021
- {
4089
+ {
4022
4090
  ASSERT(key !== undefined, 'key is undefined');
4023
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
4024
- return inputData[device] && !!(inputData[device][key] & 1);
4091
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
4092
+ return inputData[device] && !!(inputData[device][key] & 1);
4025
4093
  }
4026
4094
 
4027
4095
  /** Returns true if device key was pressed this frame
@@ -4030,10 +4098,10 @@ function keyIsDown(key, device=0)
4030
4098
  * @return {boolean}
4031
4099
  * @memberof Input */
4032
4100
  function keyWasPressed(key, device=0)
4033
- {
4101
+ {
4034
4102
  ASSERT(key !== undefined, 'key is undefined');
4035
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
4036
- return inputData[device] && !!(inputData[device][key] & 2);
4103
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
4104
+ return inputData[device] && !!(inputData[device][key] & 2);
4037
4105
  }
4038
4106
 
4039
4107
  /** Returns true if device key was released this frame
@@ -4042,9 +4110,9 @@ function keyWasPressed(key, device=0)
4042
4110
  * @return {boolean}
4043
4111
  * @memberof Input */
4044
4112
  function keyWasReleased(key, device=0)
4045
- {
4113
+ {
4046
4114
  ASSERT(key !== undefined, 'key is undefined');
4047
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
4115
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
4048
4116
  return inputData[device] && !!(inputData[device][key] & 4);
4049
4117
  }
4050
4118
 
@@ -4166,7 +4234,7 @@ function gamepadWasReleased(button, gamepad=0)
4166
4234
  * @param {number} [gamepad]
4167
4235
  * @return {Vector2}
4168
4236
  * @memberof Input */
4169
- function gamepadStick(stick, gamepad=0)
4237
+ function gamepadStick(stick, gamepad=0)
4170
4238
  { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
4171
4239
 
4172
4240
  ///////////////////////////////////////////////////////////////////////////////
@@ -4243,10 +4311,10 @@ function inputInit()
4243
4311
  {
4244
4312
  // handle remapping wasd keys to directions
4245
4313
  return inputWASDEmulateDirection ?
4246
- c == 'KeyW' ? 'ArrowUp' :
4247
- c == 'KeyS' ? 'ArrowDown' :
4248
- c == 'KeyA' ? 'ArrowLeft' :
4249
- c == 'KeyD' ? 'ArrowRight' : c : c;
4314
+ c === 'KeyW' ? 'ArrowUp' :
4315
+ c === 'KeyS' ? 'ArrowDown' :
4316
+ c === 'KeyA' ? 'ArrowLeft' :
4317
+ c === 'KeyD' ? 'ArrowRight' : c : c;
4250
4318
  }
4251
4319
  function onMouseDown(e)
4252
4320
  {
@@ -4254,9 +4322,9 @@ function inputInit()
4254
4322
  return;
4255
4323
 
4256
4324
  // fix stalled audio requiring user interaction
4257
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
4325
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
4258
4326
  audioContext.resume();
4259
-
4327
+
4260
4328
  isUsingGamepad = false;
4261
4329
  inputData[0][e.button] = 3;
4262
4330
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
@@ -4299,8 +4367,8 @@ function gamepadsUpdate()
4299
4367
  const applyDeadZones = (v)=>
4300
4368
  {
4301
4369
  const min=.3, max=.8;
4302
- const deadZone = (v)=>
4303
- v > min ? percent( v, min, max) :
4370
+ const deadZone = (v)=>
4371
+ v > min ? percent(v, min, max) :
4304
4372
  v < -min ? -percent(-v, min, max) : 0;
4305
4373
  return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
4306
4374
  }
@@ -4308,30 +4376,29 @@ function gamepadsUpdate()
4308
4376
  // update touch gamepad if enabled
4309
4377
  if (touchGamepadEnable && isTouchDevice)
4310
4378
  {
4311
- ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
4312
- if (touchGamepadTimer.isSet())
4379
+ if (!touchGamepadTimer.isSet())
4380
+ return;
4381
+
4382
+ // read virtual analog stick
4383
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
4384
+ sticks[0] = vec2();
4385
+ if (touchGamepadAnalog)
4386
+ sticks[0] = applyDeadZones(touchGamepadStick);
4387
+ else if (touchGamepadStick.lengthSquared() > .3)
4313
4388
  {
4314
- // read virtual analog stick
4315
- const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
4316
- sticks[0] = vec2();
4317
- if (touchGamepadAnalog)
4318
- sticks[0] = applyDeadZones(touchGamepadStick);
4319
- else if (touchGamepadStick.lengthSquared() > .3)
4320
- {
4321
- // convert to 8 way dpad
4322
- sticks[0].x = Math.round(touchGamepadStick.x);
4323
- sticks[0].y = -Math.round(touchGamepadStick.y);
4324
- sticks[0] = sticks[0].clampLength();
4325
- }
4389
+ // convert to 8 way dpad
4390
+ sticks[0].x = Math.round(touchGamepadStick.x);
4391
+ sticks[0].y = -Math.round(touchGamepadStick.y);
4392
+ sticks[0] = sticks[0].clampLength();
4393
+ }
4326
4394
 
4327
- // read virtual gamepad buttons
4328
- const data = inputData[1] || (inputData[1] = []);
4329
- for (let i=10; i--;)
4330
- {
4331
- const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
4332
- const wasDown = gamepadIsDown(j,0);
4333
- data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
4334
- }
4395
+ // read virtual gamepad buttons
4396
+ const data = inputData[1] || (inputData[1] = []);
4397
+ for (let i=10; i--;)
4398
+ {
4399
+ const j = i === 3 ? 2 : i === 2 ? 3 : i; // fix button locations
4400
+ const wasDown = gamepadIsDown(j,0);
4401
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
4335
4402
  }
4336
4403
  }
4337
4404
 
@@ -4357,7 +4424,7 @@ function gamepadsUpdate()
4357
4424
  // read analog sticks
4358
4425
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
4359
4426
  sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
4360
-
4427
+
4361
4428
  // read buttons
4362
4429
  for (let j = gamepad.buttons.length; j--;)
4363
4430
  {
@@ -4373,14 +4440,14 @@ function gamepadsUpdate()
4373
4440
  {
4374
4441
  // copy dpad to left analog stick when pressed
4375
4442
  const dpad = vec2(
4376
- (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
4443
+ (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
4377
4444
  (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
4378
4445
  if (dpad.lengthSquared())
4379
4446
  sticks[0] = dpad.clampLength();
4380
4447
  }
4381
4448
 
4382
4449
  // disable touch gamepad if using real gamepad
4383
- touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
4450
+ touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
4384
4451
  }
4385
4452
  }
4386
4453
  }
@@ -4405,20 +4472,13 @@ function vibrateStop() { vibrate(0); }
4405
4472
  const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4406
4473
 
4407
4474
  // touch gamepad internal variables
4408
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
4475
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
4409
4476
 
4410
4477
  // enable touch input mouse passthrough
4411
4478
  function touchInputInit()
4412
4479
  {
4413
4480
  // add non passive touch event listeners
4414
4481
  let handleTouch = handleTouchDefault;
4415
- if (touchGamepadEnable)
4416
- {
4417
- // touch input internal variables
4418
- handleTouch = handleTouchGamepad;
4419
- touchGamepadButtons = [];
4420
- touchGamepadStick = vec2();
4421
- }
4422
4482
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
4423
4483
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
4424
4484
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
@@ -4427,8 +4487,15 @@ function touchInputInit()
4427
4487
  let wasTouching;
4428
4488
  function handleTouchDefault(e)
4429
4489
  {
4490
+ if (!touchInputEnable)
4491
+ return;
4492
+
4493
+ // route touch to gamepad
4494
+ if (touchGamepadEnable)
4495
+ handleTouchGamepad(e);
4496
+
4430
4497
  // fix stalled audio requiring user interaction
4431
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
4498
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
4432
4499
  audioContext.resume();
4433
4500
 
4434
4501
  // check if touching and pass to mouse events
@@ -4457,7 +4524,7 @@ function touchInputInit()
4457
4524
  // prevent default handling like copy and magnifier lens
4458
4525
  if (inputPreventDefault && document.hasFocus()) // allow document to get focus
4459
4526
  e.preventDefault();
4460
-
4527
+
4461
4528
  // must return true so the document will get focus
4462
4529
  return true;
4463
4530
  }
@@ -4469,7 +4536,7 @@ function touchInputInit()
4469
4536
  touchGamepadStick = vec2();
4470
4537
  touchGamepadButtons = [];
4471
4538
  isUsingGamepad = true;
4472
-
4539
+
4473
4540
  const touching = e.touches.length;
4474
4541
  if (touching)
4475
4542
  {
@@ -4478,9 +4545,6 @@ function touchInputInit()
4478
4545
  {
4479
4546
  // touch anywhere to press start when paused
4480
4547
  touchGamepadButtons[9] = 1;
4481
-
4482
- // call default touch handler so normal touch events still work
4483
- handleTouchDefault(e);
4484
4548
  return;
4485
4549
  }
4486
4550
  }
@@ -4511,12 +4575,6 @@ function touchInputInit()
4511
4575
  touchGamepadButtons[9] = 1;
4512
4576
  }
4513
4577
  }
4514
-
4515
- // call default touch handler so normal touch events still work
4516
- handleTouchDefault(e);
4517
-
4518
- // must return true so the document will get focus
4519
- return true;
4520
4578
  }
4521
4579
  }
4522
4580
 
@@ -4526,7 +4584,7 @@ function touchGamepadRender()
4526
4584
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
4527
4585
  if (!touchGamepadEnable || !touchGamepadTimer.isSet())
4528
4586
  return;
4529
-
4587
+
4530
4588
  // fade off when not touching or paused
4531
4589
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
4532
4590
  if (!alpha || paused)
@@ -4557,11 +4615,11 @@ function touchGamepadRender()
4557
4615
  const angle = i*PI/4;
4558
4616
  context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
4559
4617
  i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
4560
- i==1 && context.fill();
4618
+ i===1 && context.fill();
4561
4619
  }
4562
4620
  context.stroke();
4563
4621
  }
4564
-
4622
+
4565
4623
  // draw right face buttons
4566
4624
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4567
4625
  for (let i=4; i--;)
@@ -4596,8 +4654,8 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
4596
4654
  /** Check if pointer is locked (true if locked)
4597
4655
  * @return {boolean}
4598
4656
  * @memberof Input */
4599
- function pointerLockIsActive() { return document.pointerLockElement == mainCanvas; }
4600
- /**
4657
+ function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
4658
+ /**
4601
4659
  * LittleJS Audio System
4602
4660
  * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
4603
4661
  * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
@@ -4618,10 +4676,21 @@ let audioContext = new AudioContext;
4618
4676
  * @memberof Audio */
4619
4677
  let audioMasterGain;
4620
4678
 
4679
+ /** Default sample rate used for sounds
4680
+ * @default 44100
4681
+ * @memberof Audio */
4682
+ const audioDefaultSampleRate = 44100;
4683
+
4684
+ /** Check if the audio context is running and available for playback
4685
+ * @return {boolean} - True if the audio context is running
4686
+ * @memberof Audio */
4687
+ function audioIsRunning()
4688
+ { return audioContext.state === 'running'; }
4689
+
4621
4690
  function audioInit()
4622
4691
  {
4623
4692
  if (!soundEnable || headlessMode) return;
4624
-
4693
+
4625
4694
  audioMasterGain = audioContext.createGain();
4626
4695
  audioMasterGain.connect(audioContext.destination);
4627
4696
  audioMasterGain.gain.value = soundVolume; // set starting value
@@ -4629,14 +4698,14 @@ function audioInit()
4629
4698
 
4630
4699
  ///////////////////////////////////////////////////////////////////////////////
4631
4700
 
4632
- /**
4701
+ /**
4633
4702
  * Sound Object - Stores a sound for later use and can be played positionally
4634
- *
4703
+ *
4635
4704
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4636
4705
  * @example
4637
4706
  * // create a sound
4638
4707
  * const sound_example = new Sound([.5,.5]);
4639
- *
4708
+ *
4640
4709
  * // play the sound
4641
4710
  * sound_example.play();
4642
4711
  */
@@ -4653,33 +4722,41 @@ class Sound
4653
4722
 
4654
4723
  /** @property {number} - World space max range of sound */
4655
4724
  this.range = range;
4656
-
4657
4725
  /** @property {number} - At what percentage of range should it start tapering */
4658
4726
  this.taper = taper;
4659
-
4660
4727
  /** @property {number} - How much to randomize frequency each time sound plays */
4661
4728
  this.randomness = 0;
4729
+ /** @property {number} - Sample rate for this sound */
4730
+ this.sampleRate = audioDefaultSampleRate;
4731
+ /** @property {number} - Percentage of this sound currently loaded */
4732
+ this.loadedPercent = 0;
4662
4733
 
4734
+ // generate zzfx sound now for fast playback
4663
4735
  if (zzfxSound)
4664
4736
  {
4665
- // generate zzfx sound now for fast playback
4666
- const defaultRandomness = .05;
4667
- this.randomness = zzfxSound[1] != undefined ? zzfxSound[1] : defaultRandomness;
4668
- zzfxSound[1] = 0; // generate without randomness
4737
+ // remove randomness so it can be applied on playback
4738
+ const randomnessIndex = 1, defaultRandomness = .05;
4739
+ this.randomness = zzfxSound[randomnessIndex] !== undefined ?
4740
+ zzfxSound[randomnessIndex] : defaultRandomness;
4741
+ zzfxSound[randomnessIndex] = 0;
4742
+
4743
+ // generate the zzfx samples
4669
4744
  this.sampleChannels = [zzfxG(...zzfxSound)];
4670
- this.sampleRate = zzfxR;
4745
+ this.loadedPercent = 1;
4671
4746
  }
4672
4747
  }
4673
4748
 
4674
4749
  /** Play the sound
4675
- * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
4676
- * @param {number} [volume] - How much to scale volume by (in addition to range fade)
4677
- * @param {number} [pitch] - How much to scale pitch by (also adjusted by this.randomness)
4678
- * @param {number} [randomnessScale] - How much to scale randomness
4679
- * @param {boolean} [loop] - Should the sound loop
4680
- * @return {AudioBufferSourceNode} - The audio source node
4750
+ * Sounds may not play until a user interaction occurs
4751
+ * @param {Vector2} [pos] - World space position to play the sound if any
4752
+ * @param {number} [volume] - How much to scale volume by
4753
+ * @param {number} [pitch] - How much to scale pitch by
4754
+ * @param {number} [randomnessScale] - How much to scale pitch randomness
4755
+ * @param {boolean} [loop] - Should the sound loop?
4756
+ * @param {boolean} [paused] - Should the sound start paused
4757
+ * @return {SoundInstance} - The audio source node
4681
4758
  */
4682
- play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
4759
+ play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
4683
4760
  {
4684
4761
  if (!soundEnable || headlessMode) return;
4685
4762
  if (!this.sampleChannels) return;
@@ -4702,75 +4779,55 @@ class Sound
4702
4779
  // get pan from screen space coords
4703
4780
  pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
4704
4781
  }
4705
-
4706
- // play the sound
4707
- const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
4708
- this.gainNode = audioContext.createGain();
4709
- this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
4710
- return this.source;
4711
- }
4712
-
4713
- /** Set the sound volume of the most recently played instance of this sound
4714
- * @param {number} [volume] - How much to scale volume by
4715
- */
4716
- setVolume(volume=1)
4717
- {
4718
- if (this.gainNode)
4719
- this.gainNode.gain.value = volume;
4720
- }
4721
-
4722
- /** Stop the last instance of this sound that was played
4723
- * @param {number} [fadeTime] - How long to fade out (seconds)
4724
- */
4725
- stop(fadeTime=0)
4726
- {
4727
- if (!this.source)
4728
- return;
4729
4782
 
4730
- // ramp off gain
4731
- const startFade = audioContext.currentTime;
4732
- const endFade = startFade + fadeTime;
4733
- this.gainNode.gain.linearRampToValueAtTime(1, startFade);
4734
- this.gainNode.gain.linearRampToValueAtTime(0, endFade);
4735
- this.source.stop(endFade);
4736
- this.source = undefined;
4783
+ // Create and return sound instance
4784
+ const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
4785
+ return new SoundInstance(this, volume, rate, pan, loop, paused);
4737
4786
  }
4738
4787
 
4739
- /** Get source of most recent instance of this sound that was played
4740
- * @return {AudioBufferSourceNode}
4788
+ /** Play a music track that loops by default
4789
+ * @param {number} [volume] - Volume to play the music at
4790
+ * @param {boolean} [loop] - Should the music loop?
4791
+ * @param {boolean} [paused] - Should the music start paused
4792
+ * @return {SoundInstance} - The audio source node
4741
4793
  */
4742
- getSource() { return this.source; }
4794
+ playMusic(volume=1, loop=true, paused=false)
4795
+ { return this.play(undefined, volume, 1, 0, loop, paused); }
4743
4796
 
4744
- /** Play the sound as a note with a semitone offset
4797
+ /** Play the sound as a musical note with a semitone offset
4798
+ * This can be used to play music with chromatic scales
4745
4799
  * @param {number} semitoneOffset - How many semitones to offset pitch
4746
- * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
4747
- * @param {number} [volume=1] - How much to scale volume by (in addition to range fade)
4748
- * @return {AudioBufferSourceNode} - The audio source node
4800
+ * @param {Vector2} [pos] - World space position to play the sound if any
4801
+ * @param {number} [volume=1] - How much to scale volume by
4802
+ * @return {SoundInstance} - The audio source node
4749
4803
  */
4750
4804
  playNote(semitoneOffset, pos, volume)
4751
- { return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
4805
+ {
4806
+ const pitch = getNoteFrequency(semitoneOffset, 1);
4807
+ return this.play(pos, volume, pitch, 0);
4808
+ }
4752
4809
 
4753
4810
  /** Get how long this sound is in seconds
4754
4811
  * @return {number} - How long the sound is in seconds (undefined if loading)
4755
4812
  */
4756
- getDuration()
4813
+ getDuration()
4757
4814
  { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
4758
-
4759
- /** Check if sound is loading, for sounds fetched from a url
4760
- * @return {boolean} - True if sound is loading and not ready to play
4815
+
4816
+ /** Check if sound is loaded, for sounds fetched from a url
4817
+ * @return {boolean} - True if sound is loaded and ready to play
4761
4818
  */
4762
- isLoading() { return !this.sampleChannels; }
4819
+ isLoaded() { return this.loadedPercent === 1; }
4763
4820
  }
4764
4821
 
4765
4822
  ///////////////////////////////////////////////////////////////////////////////
4766
4823
 
4767
- /**
4824
+ /**
4768
4825
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
4769
4826
  * - this can be used to play wave, mp3, and ogg files
4770
4827
  * @example
4771
4828
  * // create a sound
4772
4829
  * const sound_example = new SoundWave('sound.mp3');
4773
- *
4830
+ *
4774
4831
  * // play the sound
4775
4832
  * sound_example.play();
4776
4833
  */
@@ -4802,26 +4859,202 @@ class SoundWave extends Sound
4802
4859
  const response = await fetch(filename);
4803
4860
  const arrayBuffer = await response.arrayBuffer();
4804
4861
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
4805
- this.sampleChannels = [];
4806
- for (let i = audioBuffer.numberOfChannels; i--;)
4807
- this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
4862
+
4863
+ // convert audio buffer to sample channels across multiple frames
4864
+ const channelCount = audioBuffer.numberOfChannels;
4865
+ const samplesPerFrame = 1e5;
4866
+ const sampleChannels = [];
4867
+ for (let channel = 0; channel < channelCount; channel++)
4868
+ {
4869
+ const channelData = audioBuffer.getChannelData(channel);
4870
+ const channelLength = channelData.length;
4871
+ sampleChannels[channel] = new Array(channelLength);
4872
+ let sampleIndex = 0;
4873
+ while (sampleIndex < channelLength)
4874
+ {
4875
+ // yield to next frame
4876
+ await new Promise(resolve => setTimeout(resolve, 0));
4877
+
4878
+ // copy chunk of samples
4879
+ const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
4880
+ for (; sampleIndex < endIndex; sampleIndex++)
4881
+ sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
4882
+
4883
+ // update loaded percent
4884
+ const samplesTotal = channelCount * channelLength;
4885
+ const samplesProcessed = channel * channelLength + sampleIndex;
4886
+ this.loadedPercent = samplesProcessed / samplesTotal;
4887
+ }
4888
+ }
4889
+
4890
+ // setup the sound to be played
4808
4891
  this.sampleRate = audioBuffer.sampleRate;
4892
+ this.sampleChannels = sampleChannels;
4893
+ this.loadedPercent = 1;
4809
4894
  if (this.onloadCallback)
4810
4895
  this.onloadCallback();
4811
4896
  }
4812
4897
  }
4813
4898
 
4814
- /** Play an mp3, ogg, or wav audio from a local file or url
4815
- * @param {string} filename - Location of sound file to play
4816
- * @param {number} [volume] - How much to scale volume by
4817
- * @param {boolean} [loop] - True if the music should loop
4818
- * @return {SoundWave} - The sound object for this file
4819
- * @memberof Audio */
4820
- function playAudioFile(filename, volume=1, loop=false)
4821
- {
4822
- if (!soundEnable || headlessMode) return;
4899
+ ///////////////////////////////////////////////////////////////////////////////
4900
+
4901
+ /**
4902
+ * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
4903
+ * Represents a single playing instance of a sound with pause/resume capabilities
4904
+ * @example
4905
+ * // Play a sound and get an instance for control
4906
+ * const jumpSound = new Sound([.5,.5,220]);
4907
+ * const instance = jumpSound.play();
4908
+ *
4909
+ * // Control the individual instance
4910
+ * instance.setVolume(.5);
4911
+ * instance.pause();
4912
+ * instance.unpause();
4913
+ * instance.stop();
4914
+ */
4915
+ class SoundInstance
4916
+ {
4917
+ /** Create a sound instance
4918
+ * @param {Sound} sound - The sound object
4919
+ * @param {number} [volume] - How much to scale volume by
4920
+ * @param {number} [rate] - The playback rate to use
4921
+ * @param {number} [pan] - How much to apply stereo panning
4922
+ * @param {boolean} [loop] - Should the sound loop?
4923
+ * @param {boolean} [paused] - Should the sound start paused? */
4924
+ constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
4925
+ {
4926
+ ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
4927
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
4928
+ ASSERT(rate >= 0, 'Sound rate must be positive or zero');
4929
+ ASSERT(isNumber(pan), 'Sound pan must be a number');
4930
+
4931
+ /** @property {Sound} - The sound object */
4932
+ this.sound = sound;
4933
+ /** @property {number} - How much to scale volume by */
4934
+ this.volume = volume;
4935
+ /** @property {number} - The playback rate to use */
4936
+ this.rate = rate;
4937
+ /** @property {number} - How much to apply stereo panning */
4938
+ this.pan = pan;
4939
+ /** @property {boolean} - Should the sound loop */
4940
+ this.loop = loop;
4941
+ /** @property {number} - Timestamp for audio context when paused */
4942
+ this.pausedTime = 0;
4943
+ /** @property {number} - Timestamp for audio context when started */
4944
+ this.startTime = undefined;
4945
+ /** @property {GainNode} - Gain node for the sound */
4946
+ this.gainNode = undefined;
4947
+ /** @property {AudioBufferSourceNode} - Source node of the audio */
4948
+ this.source = undefined;
4949
+ // setup end callback and start sound
4950
+ this.onendedCallback = (source)=>
4951
+ {
4952
+ if (source === this.source)
4953
+ this.source = undefined;
4954
+ };
4955
+ if (!paused)
4956
+ this.start();
4957
+ }
4958
+
4959
+ /** Start playing the sound instance from the offset time
4960
+ * @param {number} [offset] - Offset in seconds to start playback from
4961
+ */
4962
+ start(offset=0)
4963
+ {
4964
+ ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
4965
+ if (this.isPlaying())
4966
+ this.stop();
4967
+ this.gainNode = audioContext.createGain();
4968
+ this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
4969
+ this.startTime = audioContext.currentTime - offset;
4970
+ this.pausedTime = undefined;
4971
+ }
4972
+
4973
+ /** Set the volume of this sound instance
4974
+ * @param {number} volume */
4975
+ setVolume(volume)
4976
+ {
4977
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
4978
+ this.volume = volume;
4979
+ if (this.gainNode)
4980
+ this.gainNode.gain.value = volume;
4981
+ }
4982
+
4983
+ /** Stop this sound instance and reset position to the start */
4984
+ stop(fadeTime=0)
4985
+ {
4986
+ ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
4987
+ if (this.isPlaying())
4988
+ {
4989
+ if (fadeTime)
4990
+ {
4991
+ // ramp off gain
4992
+ const startFade = audioContext.currentTime;
4993
+ const endFade = startFade + fadeTime;
4994
+ this.gainNode.gain.linearRampToValueAtTime(1, startFade);
4995
+ this.gainNode.gain.linearRampToValueAtTime(0, endFade);
4996
+ this.source.stop(endFade);
4997
+ }
4998
+ else
4999
+ this.source.stop();
5000
+ }
5001
+ this.pausedTime = 0;
5002
+ this.source = undefined;
5003
+ this.startTime = undefined;
5004
+ }
5005
+
5006
+ /** Pause this sound instance */
5007
+ pause()
5008
+ {
5009
+ if (this.isPaused())
5010
+ return;
5011
+
5012
+ // save current time and stop sound
5013
+ this.pausedTime = this.getCurrentTime();
5014
+ this.source.stop();
5015
+ this.source = undefined;
5016
+ this.startTime = undefined;
5017
+ }
5018
+
5019
+ /** Unpauses this sound instance */
5020
+ resume()
5021
+ {
5022
+ if (!this.isPaused())
5023
+ return;
5024
+
5025
+ // restart sound from paused time
5026
+ this.start(this.pausedTime);
5027
+ }
5028
+
5029
+ /** Check if this instance is currently playing
5030
+ * @return {boolean} - True if playing
5031
+ */
5032
+ isPlaying() { return !!this.source; }
5033
+
5034
+ /** Check if this instance is paused and was not stopped
5035
+ * @return {boolean} - True if paused
5036
+ */
5037
+ isPaused() { return !this.isPlaying(); }
5038
+
5039
+ /** Get the current playback time in seconds
5040
+ * @return {number} - Current playback time
5041
+ */
5042
+ getCurrentTime()
5043
+ {
5044
+ const deltaTime = mod(audioContext.currentTime - this.startTime,
5045
+ this.getDuration());
5046
+ return this.isPlaying() ? deltaTime : this.pausedTime;
5047
+ }
4823
5048
 
4824
- return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
5049
+ /** Get the total duration of this sound
5050
+ * @return {number} - Total duration in seconds
5051
+ */
5052
+ getDuration() { return this.sound.getDuration() / this.rate; }
5053
+
5054
+ /** Get source of this sound instance
5055
+ * @return {AudioBufferSourceNode}
5056
+ */
5057
+ getSource() { return this.source; }
4825
5058
  }
4826
5059
 
4827
5060
  ///////////////////////////////////////////////////////////////////////////////
@@ -4875,9 +5108,11 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4875
5108
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
4876
5109
  * @param {number} [sampleRate=44100] - Sample rate for the sound
4877
5110
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
5111
+ * @param {number} [offset] - Offset in seconds to start playback from
5112
+ * @param {Function} [onended] - Callback for when the sound ends
4878
5113
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4879
5114
  * @memberof Audio */
4880
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR, gainNode)
5115
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
4881
5116
  {
4882
5117
  if (!soundEnable || headlessMode) return;
4883
5118
 
@@ -4902,16 +5137,20 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4902
5137
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
4903
5138
  source.connect(pannerNode).connect(gainNode);
4904
5139
 
4905
- // play the sound
4906
- if (audioContext.state != 'running')
5140
+ // callback when the sound ends
5141
+ if (onended)
5142
+ source.addEventListener('ended', ()=> onended(source));
5143
+
5144
+ if (!audioIsRunning())
4907
5145
  {
4908
- // fix stalled audio and play
4909
- audioContext.resume().then(()=>source.start());
5146
+ // fix stalled audio, this sound won't be able to play
5147
+ audioContext.resume();
5148
+ return;
4910
5149
  }
4911
- else
4912
- source.start();
4913
5150
 
4914
- // return sound
5151
+ // play and return sound
5152
+ const startOffset = offset * rate;
5153
+ source.start(0, startOffset);
4915
5154
  return source;
4916
5155
  }
4917
5156
 
@@ -4919,18 +5158,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4919
5158
  // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
4920
5159
 
4921
5160
  /** Generate and play a ZzFX sound
4922
- *
5161
+ *
4923
5162
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4924
5163
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
4925
5164
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4926
5165
  * @memberof Audio */
4927
5166
  function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
4928
5167
 
4929
- /** Sample rate used for all ZzFX sounds
4930
- * @default 44100
4931
- * @memberof Audio */
4932
- const zzfxR = 44100;
4933
-
4934
5168
  /** Generate samples for a ZzFX sound
4935
5169
  * @param {number} [volume] - Volume scale (percent)
4936
5170
  * @param {number} [randomness] - How much to randomize frequency (percent Hz)
@@ -4954,11 +5188,10 @@ const zzfxR = 44100;
4954
5188
  * @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
4955
5189
  * @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
4956
5190
  * @return {Array} - Array of audio samples
4957
- * @memberof Audio
4958
- */
5191
+ * @memberof Audio */
4959
5192
  function zzfxG
4960
5193
  (
4961
- volume = 1,
5194
+ volume = 1,
4962
5195
  randomness = .05,
4963
5196
  frequency = 220,
4964
5197
  attack = 0,
@@ -4966,11 +5199,11 @@ function zzfxG
4966
5199
  release = .1,
4967
5200
  shape = 0,
4968
5201
  shapeCurve = 1,
4969
- slide = 0,
4970
- deltaSlide = 0,
4971
- pitchJump = 0,
4972
- pitchJumpTime = 0,
4973
- repeatTime = 0,
5202
+ slide = 0,
5203
+ deltaSlide = 0,
5204
+ pitchJump = 0,
5205
+ pitchJumpTime = 0,
5206
+ repeatTime = 0,
4974
5207
  noise = 0,
4975
5208
  modulation = 0,
4976
5209
  bitCrush = 0,
@@ -4982,19 +5215,19 @@ function zzfxG
4982
5215
  )
4983
5216
  {
4984
5217
  // init parameters
4985
- let sampleRate = zzfxR,
4986
- PI2 = PI*2,
5218
+ let sampleRate = audioDefaultSampleRate,
5219
+ PI2 = PI*2,
4987
5220
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
4988
- startFrequency = frequency *=
5221
+ startFrequency = frequency *=
4989
5222
  (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
4990
- modOffset = 0, // modulation offset
5223
+ modOffset = 0, // modulation offset
4991
5224
  repeat = 0, // repeat offset
4992
5225
  crush = 0, // bit crush offset
4993
5226
  jump = 1, // pitch jump timer
4994
5227
  length, // sample length
4995
5228
  b = [], // sample buffer
4996
5229
  t = 0, // sample time
4997
- i = 0, // sample index
5230
+ i = 0, // sample index
4998
5231
  s = 0, // sample value
4999
5232
  f, // wave frequency
5000
5233
 
@@ -5002,7 +5235,7 @@ function zzfxG
5002
5235
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
5003
5236
  cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
5004
5237
  a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
5005
- b0 = (1 + sign(filter) * cos) / 2 / a0,
5238
+ b0 = (1 + sign(filter) * cos) / 2 / a0,
5006
5239
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
5007
5240
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
5008
5241
 
@@ -5048,7 +5281,7 @@ function zzfxG
5048
5281
  0); // post release
5049
5282
 
5050
5283
  s = delay ? s/2 + (delay > i ? 0 : // delay
5051
- (i<length-delay? 1 : (length-i)/delay) * // release delay
5284
+ (i<length-delay? 1 : (length-i)/delay) * // release delay
5052
5285
  b[i-delay|0]/2/volume) : s; // sample delay
5053
5286
 
5054
5287
  if (filter) // apply filter
@@ -5060,14 +5293,14 @@ function zzfxG
5060
5293
  t += f + f*noise*Math.sin(i**5); // noise
5061
5294
 
5062
5295
  if (jump && ++jump > pitchJumpTime) // pitch jump
5063
- {
5296
+ {
5064
5297
  frequency += pitchJump; // apply pitch jump
5065
5298
  startFrequency += pitchJump; // also apply to start
5066
5299
  jump = 0; // stop pitch jump time
5067
- }
5300
+ }
5068
5301
 
5069
5302
  if (repeatTime && !(++repeat % repeatTime)) // repeat
5070
- {
5303
+ {
5071
5304
  frequency = startFrequency; // reset frequency
5072
5305
  slide = startSlide; // reset slide
5073
5306
  jump ||= 1; // reset pitch jump time
@@ -5076,7 +5309,7 @@ function zzfxG
5076
5309
 
5077
5310
  return b; // return sample buffer
5078
5311
  }
5079
- /**
5312
+ /**
5080
5313
  * LittleJS Tile Layer System
5081
5314
  * - Caches arrays of tiles to off screen canvas for fast rendering
5082
5315
  * - Unlimited numbers of layers, allocates canvases as needed
@@ -5090,9 +5323,9 @@ function zzfxG
5090
5323
  // Tile Layer System
5091
5324
 
5092
5325
  /** Keep track of all tile layers with collision
5093
- * @type {Array<TileCollisionLayer>}
5326
+ * @type {Array<TileCollisionLayer>}
5094
5327
  * @memberof TileCollision */
5095
- let tileCollisionLayers = [];
5328
+ const tileCollisionLayers = [];
5096
5329
 
5097
5330
  /** Get tile collision data for a given cell in the grid
5098
5331
  * @param {Vector2} pos
@@ -5146,7 +5379,7 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5146
5379
  }
5147
5380
 
5148
5381
  ///////////////////////////////////////////////////////////////////////////////
5149
- /**
5382
+ /**
5150
5383
  * Load tile layers from exported data
5151
5384
  * @param {Object} tileMapData - Level data from exported data
5152
5385
  * @param {TileInfo} [tileInfo] - Default tile info (used for size and texture)
@@ -5179,13 +5412,13 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5179
5412
  {
5180
5413
  const dataLayer = tileMapData.layers[layerIndex];
5181
5414
  ASSERT(dataLayer.data && dataLayer.data.length);
5182
- ASSERT(levelSize.area() == dataLayer.data.length);
5415
+ ASSERT(levelSize.area() === dataLayer.data.length);
5183
5416
 
5184
5417
  const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
5185
5418
  const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
5186
5419
  tileLayers[layerIndex] = tileLayer;
5187
5420
 
5188
- for (let x=levelSize.x; x--;)
5421
+ for (let x=levelSize.x; x--;)
5189
5422
  for (let y=levelSize.y; y--;)
5190
5423
  {
5191
5424
  const pos = vec2(x, levelSize.y-1-y);
@@ -5244,7 +5477,7 @@ class TileLayerData
5244
5477
  /**
5245
5478
  * Canvas Layer - cached off screen rendering system
5246
5479
  * - Contains an offscreen canvas that can be rendered to
5247
- * - Webgl rendering is optional, call useWebGL to enable
5480
+ * - WebGL rendering is optional, call useWebGL to enable
5248
5481
  * @extends EngineObject
5249
5482
  * @example
5250
5483
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
@@ -5266,18 +5499,18 @@ class CanvasLayer extends EngineObject
5266
5499
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
5267
5500
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5268
5501
  this.context = headlessMode ? undefined : this.canvas.getContext('2d');
5269
- /** @property {WebGLTexture} - Texture if using webgl for this layer, call useWebGL to enable */
5502
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
5270
5503
  this.glTexture = undefined;
5271
5504
  this.gravityScale = 0; // disable gravity by default for canvas layers
5272
5505
  }
5273
-
5506
+
5274
5507
  /** Destroy this canvas layer */
5275
5508
  destroy()
5276
5509
  {
5277
5510
  if (this.destroyed)
5278
5511
  return;
5279
5512
 
5280
- // free up the webgl texture
5513
+ // free up the WebGL texture
5281
5514
  if (this.glTexture)
5282
5515
  glDeleteTexture(this.glTexture);
5283
5516
  super.destroy();
@@ -5302,12 +5535,12 @@ class CanvasLayer extends EngineObject
5302
5535
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5303
5536
  {
5304
5537
  // draw the canvas layer as a single tile that uses the whole texture
5305
- const useWebgl = glEnable && this.glTexture != undefined;
5538
+ const useWebGL = glEnable && this.glTexture !== undefined;
5306
5539
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5307
- drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebgl, screenSpace, context);
5540
+ drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5308
5541
  }
5309
5542
 
5310
- /** Draw onto the layer canvas in world space (bypass webgl)
5543
+ /** Draw onto the layer canvas in world space (bypass WebGL)
5311
5544
  * @param {Vector2} pos
5312
5545
  * @param {Vector2} size
5313
5546
  * @param {number} angle
@@ -5341,8 +5574,8 @@ class CanvasLayer extends EngineObject
5341
5574
  if (textureInfo)
5342
5575
  {
5343
5576
  context.globalAlpha = color.a; // only alpha is supported
5344
- context.drawImage(textureInfo.image,
5345
- tileInfo.pos.x, tileInfo.pos.y,
5577
+ context.drawImage(textureInfo.image,
5578
+ tileInfo.pos.x, tileInfo.pos.y,
5346
5579
  tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
5347
5580
  context.globalAlpha = 1;
5348
5581
  }
@@ -5360,11 +5593,11 @@ class CanvasLayer extends EngineObject
5360
5593
  * @param {Vector2} [size=(1,1)]
5361
5594
  * @param {Color} [color=(1,1,1,1)]
5362
5595
  * @param {number} [angle=0] */
5363
- drawRect(pos, size, color, angle)
5596
+ drawRect(pos, size, color, angle)
5364
5597
  { this.drawTile(pos, size, undefined, color, angle); }
5365
5598
 
5366
- /** Create or update the webgl texture for this layer
5367
- * @param {boolean} [enable] - enable webgl rendering and update the texture */
5599
+ /** Create or update the WebGL texture for this layer
5600
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5368
5601
  useWebGL(enable=true)
5369
5602
  {
5370
5603
  if (glEnable && enable)
@@ -5410,7 +5643,7 @@ class TileLayer extends CanvasLayer
5410
5643
  this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
5411
5644
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
5412
5645
  this.context = this.canvas.getContext('2d');
5413
- /** @property {WebGLTexture} - Texture if using webgl for this layer */
5646
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer */
5414
5647
  this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
5415
5648
  // set no friction by default, applied friction is max of both objects
5416
5649
  this.friction = 0;
@@ -5435,7 +5668,7 @@ class TileLayer extends CanvasLayer
5435
5668
  }
5436
5669
  }
5437
5670
 
5438
- /** Set data at a given position in the array
5671
+ /** Set data at a given position in the array
5439
5672
  * @param {Vector2} layerPos - Local position in array
5440
5673
  * @param {TileLayerData} data - Data to set
5441
5674
  * @param {boolean} [redraw] - Force the tile to redraw if true */
@@ -5447,26 +5680,26 @@ class TileLayer extends CanvasLayer
5447
5680
  redraw && this.drawTileData(layerPos);
5448
5681
  }
5449
5682
  }
5450
-
5451
- /** Get data at a given position in the array
5683
+
5684
+ /** Get data at a given position in the array
5452
5685
  * @param {Vector2} layerPos - Local position in array
5453
5686
  * @return {TileLayerData} */
5454
5687
  getData(layerPos)
5455
5688
  { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
5456
-
5689
+
5457
5690
  // Render the tile layer, called automatically by the engine
5458
5691
  render()
5459
5692
  {
5460
- ASSERT(drawContext != this.context, 'must call redrawEnd() after drawing tiles!');
5461
-
5693
+ ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5694
+
5462
5695
  // draw the tile layer as a single tile
5463
5696
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5464
5697
  const pos = this.pos.add(this.size.scale(.5));
5465
- const useWebgl = glEnable && this.glTexture != undefined;
5466
- drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebgl);
5698
+ const useWebGL = glEnable && this.glTexture !== undefined;
5699
+ drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5467
5700
  }
5468
5701
 
5469
- /** Draw all the tile data to an offscreen canvas
5702
+ /** Draw all the tile data to an offscreen canvas
5470
5703
  * - This may be slow in some browsers but only needs to be done once */
5471
5704
  redraw()
5472
5705
  {
@@ -5476,7 +5709,7 @@ class TileLayer extends CanvasLayer
5476
5709
  this.drawTileData(vec2(x,y), false);
5477
5710
  this.redrawEnd();
5478
5711
  if (this.glTexture)
5479
- this.useWebGL(); // update webgl texture
5712
+ this.useWebGL(); // update WebGL texture
5480
5713
  }
5481
5714
 
5482
5715
  /** Call to start the redraw process
@@ -5512,7 +5745,7 @@ class TileLayer extends CanvasLayer
5512
5745
  /** Call to end the redraw process */
5513
5746
  redrawEnd()
5514
5747
  {
5515
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5748
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
5516
5749
  glCopyToContext(drawContext);
5517
5750
  //debugSaveCanvas(this.canvas);
5518
5751
 
@@ -5523,7 +5756,7 @@ class TileLayer extends CanvasLayer
5523
5756
  /** Draw the tile at a given position in the tile grid
5524
5757
  * This can be used to clear out tiles when they are destroyed
5525
5758
  * Tiles can also be redrawn if inside a redrawStart/End block
5526
- * @param {Vector2} layerPos
5759
+ * @param {Vector2} layerPos
5527
5760
  * @param {boolean} [clear] - should the old tile be cleared out
5528
5761
  */
5529
5762
  drawTileData(layerPos, clear=true)
@@ -5538,9 +5771,9 @@ class TileLayer extends CanvasLayer
5538
5771
 
5539
5772
  // draw the tile if it has layer data
5540
5773
  const d = this.getData(layerPos);
5541
- if (d.tile != undefined)
5774
+ if (d.tile !== undefined)
5542
5775
  {
5543
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5776
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
5544
5777
  const pos = layerPos.add(vec2(.5));
5545
5778
  const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
5546
5779
  drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
@@ -5553,7 +5786,7 @@ class TileLayer extends CanvasLayer
5553
5786
  * Tile Collision Layer - a tile layer with collision
5554
5787
  * - adds collision data and functions to TileLayer
5555
5788
  * - there can be multiple tile collision layers
5556
- * - tile collison layers should not overlap each other
5789
+ * - tile collision layers should not overlap each other
5557
5790
  * @extends TileLayer
5558
5791
  */
5559
5792
  class TileCollisionLayer extends TileLayer
@@ -5704,7 +5937,7 @@ class TileCollisionLayer extends TileLayer
5704
5937
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
5705
5938
  }
5706
5939
  }
5707
- /**
5940
+ /**
5708
5941
  * LittleJS Particle System
5709
5942
  */
5710
5943
 
@@ -5721,7 +5954,7 @@ class TileCollisionLayer extends TileLayer
5721
5954
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
5722
5955
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
5723
5956
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5724
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5957
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5725
5958
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
5726
5959
  * );
5727
5960
  */
@@ -5729,35 +5962,35 @@ class ParticleEmitter extends EngineObject
5729
5962
  {
5730
5963
  /** Create a particle system with the given settings
5731
5964
  * @param {Vector2} position - World space position of the emitter
5732
- * @param {Number} [angle] - Angle to emit the particles
5733
- * @param {Number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
5734
- * @param {Number} [emitTime] - How long to stay alive (0 is forever)
5735
- * @param {Number} [emitRate] - How many particles per second to spawn, does not emit if 0
5736
- * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5965
+ * @param {number} [angle] - Angle to emit the particles
5966
+ * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
5967
+ * @param {number} [emitTime] - How long to stay alive (0 is forever)
5968
+ * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
5969
+ * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5737
5970
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
5738
5971
  * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
5739
5972
  * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
5740
5973
  * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
5741
5974
  * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
5742
- * @param {Number} [particleTime] - How long particles live
5743
- * @param {Number} [sizeStart] - How big are particles at start
5744
- * @param {Number} [sizeEnd] - How big are particles at end
5745
- * @param {Number} [speed] - How fast are particles when spawned
5746
- * @param {Number} [angleSpeed] - How fast are particles rotating
5747
- * @param {Number} [damping] - How much to dampen particle speed
5748
- * @param {Number} [angleDamping] - How much to dampen particle angular speed
5749
- * @param {Number} [gravityScale] - How much gravity effect particles
5750
- * @param {Number} [particleConeAngle] - Cone for start particle angle
5751
- * @param {Number} [fadeRate] - How quick to fade particles at start/end in percent of life
5752
- * @param {Number} [randomness] - Apply extra randomness percent
5975
+ * @param {number} [particleTime] - How long particles live
5976
+ * @param {number} [sizeStart] - How big are particles at start
5977
+ * @param {number} [sizeEnd] - How big are particles at end
5978
+ * @param {number} [speed] - How fast are particles when spawned
5979
+ * @param {number} [angleSpeed] - How fast are particles rotating
5980
+ * @param {number} [damping] - How much to dampen particle speed
5981
+ * @param {number} [angleDamping] - How much to dampen particle angular speed
5982
+ * @param {number} [gravityScale] - How much gravity effect particles
5983
+ * @param {number} [particleConeAngle] - Cone for start particle angle
5984
+ * @param {number} [fadeRate] - How quick to fade particles at start/end in percent of life
5985
+ * @param {number} [randomness] - Apply extra randomness percent
5753
5986
  * @param {boolean} [collideTiles] - Do particles collide against tiles
5754
5987
  * @param {boolean} [additive] - Should particles use additive blend
5755
5988
  * @param {boolean} [randomColorLinear] - Should color be randomized linearly or across each component
5756
- * @param {Number} [renderOrder] - Render order for particles (additive is above other stuff by default)
5989
+ * @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
5757
5990
  * @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
5758
5991
  */
5759
5992
  constructor
5760
- (
5993
+ (
5761
5994
  position,
5762
5995
  angle,
5763
5996
  emitSize = 0,
@@ -5779,7 +6012,7 @@ class ParticleEmitter extends EngineObject
5779
6012
  gravityScale = 0,
5780
6013
  particleConeAngle = PI,
5781
6014
  fadeRate = .1,
5782
- randomness = .2,
6015
+ randomness = .2,
5783
6016
  collideTiles = false,
5784
6017
  additive = false,
5785
6018
  randomColorLinear = true,
@@ -5790,13 +6023,13 @@ class ParticleEmitter extends EngineObject
5790
6023
  super(position, vec2(), tileInfo, angle, undefined, renderOrder);
5791
6024
 
5792
6025
  // emitter settings
5793
- /** @property {Number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
6026
+ /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5794
6027
  this.emitSize = emitSize
5795
- /** @property {Number} - How long to stay alive (0 is forever) */
6028
+ /** @property {number} - How long to stay alive (0 is forever) */
5796
6029
  this.emitTime = emitTime;
5797
- /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
6030
+ /** @property {number} - How many particles per second to spawn, does not emit if 0 */
5798
6031
  this.emitRate = emitRate;
5799
- /** @property {Number} - Local angle to apply velocity to particles from emitter */
6032
+ /** @property {number} - Local angle to apply velocity to particles from emitter */
5800
6033
  this.emitConeAngle = emitConeAngle;
5801
6034
 
5802
6035
  // color settings
@@ -5812,27 +6045,27 @@ class ParticleEmitter extends EngineObject
5812
6045
  this.randomColorLinear = randomColorLinear;
5813
6046
 
5814
6047
  // particle settings
5815
- /** @property {Number} - How long particles live */
6048
+ /** @property {number} - How long particles live */
5816
6049
  this.particleTime = particleTime;
5817
- /** @property {Number} - How big are particles at start */
6050
+ /** @property {number} - How big are particles at start */
5818
6051
  this.sizeStart = sizeStart;
5819
- /** @property {Number} - How big are particles at end */
6052
+ /** @property {number} - How big are particles at end */
5820
6053
  this.sizeEnd = sizeEnd;
5821
- /** @property {Number} - How fast are particles when spawned */
6054
+ /** @property {number} - How fast are particles when spawned */
5822
6055
  this.speed = speed;
5823
- /** @property {Number} - How fast are particles rotating */
6056
+ /** @property {number} - How fast are particles rotating */
5824
6057
  this.angleSpeed = angleSpeed;
5825
- /** @property {Number} - How much to dampen particle speed */
6058
+ /** @property {number} - How much to dampen particle speed */
5826
6059
  this.damping = damping;
5827
- /** @property {Number} - How much to dampen particle angular speed */
6060
+ /** @property {number} - How much to dampen particle angular speed */
5828
6061
  this.angleDamping = angleDamping;
5829
- /** @property {Number} - How much does gravity effect particles */
6062
+ /** @property {number} - How much gravity affects particles */
5830
6063
  this.gravityScale = gravityScale;
5831
- /** @property {Number} - Cone for start particle angle */
6064
+ /** @property {number} - Cone for start particle angle */
5832
6065
  this.particleConeAngle = particleConeAngle;
5833
- /** @property {Number} - How quick to fade in particles at start/end in percent of life */
6066
+ /** @property {number} - How quick to fade in particles at start/end in percent of life */
5834
6067
  this.fadeRate = fadeRate;
5835
- /** @property {Number} - Apply extra randomness percent */
6068
+ /** @property {number} - Apply extra randomness percent */
5836
6069
  this.randomness = randomness;
5837
6070
  /** @property {boolean} - Do particles collide against tiles */
5838
6071
  this.collideTiles = collideTiles;
@@ -5840,16 +6073,16 @@ class ParticleEmitter extends EngineObject
5840
6073
  this.additive = additive;
5841
6074
  /** @property {boolean} - Should it be in local space of emitter */
5842
6075
  this.localSpace = localSpace;
5843
- /** @property {Number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
6076
+ /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5844
6077
  this.trailScale = 0;
5845
6078
  /** @property {Function} - Callback when particle is destroyed */
5846
6079
  this.particleDestroyCallback = undefined;
5847
6080
  /** @property {Function} - Callback when particle is created */
5848
6081
  this.particleCreateCallback = undefined;
5849
- /** @property {Number} - Track particle emit time */
6082
+ /** @property {number} - Track particle emit time */
5850
6083
  this.emitTimeBuffer = 0;
5851
6084
  }
5852
-
6085
+
5853
6086
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
5854
6087
  update()
5855
6088
  {
@@ -5873,7 +6106,7 @@ class ParticleEmitter extends EngineObject
5873
6106
  if (debugParticles)
5874
6107
  {
5875
6108
  // show emitter bounds
5876
- const emitSize = typeof this.emitSize == 'number' ? vec2(this.emitSize) : this.emitSize;
6109
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
5877
6110
  debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
5878
6111
  }
5879
6112
  }
@@ -5883,7 +6116,7 @@ class ParticleEmitter extends EngineObject
5883
6116
  emitParticle()
5884
6117
  {
5885
6118
  // spawn a particle
5886
- let pos = typeof this.emitSize == 'number' ? // check if number was used
6119
+ let pos = typeof this.emitSize === 'number' ? // check if number was used
5887
6120
  randInCircle(this.emitSize/2) // circle emitter
5888
6121
  : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
5889
6122
  .multiply(this.emitSize).rotate(this.angle)
@@ -5908,7 +6141,7 @@ class ParticleEmitter extends EngineObject
5908
6141
  const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
5909
6142
  const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
5910
6143
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
5911
-
6144
+
5912
6145
  // build particle
5913
6146
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
5914
6147
  particle.velocity = vec2().setAngle(velocityAngle, speed);
@@ -5953,38 +6186,38 @@ class Particle extends EngineObject
5953
6186
  * Typically this is created automatically by a ParticleEmitter
5954
6187
  * @param {Vector2} position - World space position of the particle
5955
6188
  * @param {TileInfo} tileInfo - Tile info to render particles
5956
- * @param {Number} angle - Angle to rotate the particle
6189
+ * @param {number} angle - Angle to rotate the particle
5957
6190
  * @param {Color} colorStart - Color at start of life
5958
6191
  * @param {Color} colorEnd - Color at end of life
5959
- * @param {Number} lifeTime - How long to live for
5960
- * @param {Number} sizeStart - Size at start of life
5961
- * @param {Number} sizeEnd - Size at end of life
5962
- * @param {Number} fadeRate - How quick to fade in/out
6192
+ * @param {number} lifeTime - How long to live for
6193
+ * @param {number} sizeStart - Size at start of life
6194
+ * @param {number} sizeEnd - Size at end of life
6195
+ * @param {number} fadeRate - How quick to fade in/out
5963
6196
  * @param {boolean} additive - Does it use additive blend mode
5964
- * @param {Number} trailScale - If a trail, how long to make it
6197
+ * @param {number} trailScale - If a trail, how long to make it
5965
6198
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
5966
6199
  * @param {Function} [destroyCallback] - Callback when particle dies
5967
6200
  */
5968
6201
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
5969
6202
  )
5970
- {
5971
- super(position, vec2(), tileInfo, angle);
5972
-
6203
+ {
6204
+ super(position, vec2(), tileInfo, angle);
6205
+
5973
6206
  /** @property {Color} - Color at start of life */
5974
6207
  this.colorStart = colorStart;
5975
6208
  /** @property {Color} - Calculated change in color */
5976
6209
  this.colorEndDelta = colorEnd.subtract(colorStart);
5977
- /** @property {Number} - How long to live for */
6210
+ /** @property {number} - How long to live for */
5978
6211
  this.lifeTime = lifeTime;
5979
- /** @property {Number} - Size at start of life */
6212
+ /** @property {number} - Size at start of life */
5980
6213
  this.sizeStart = sizeStart;
5981
- /** @property {Number} - Calculated change in size */
6214
+ /** @property {number} - Calculated change in size */
5982
6215
  this.sizeEndDelta = sizeEnd - sizeStart;
5983
- /** @property {Number} - How quick to fade in/out */
6216
+ /** @property {number} - How quick to fade in/out */
5984
6217
  this.fadeRate = fadeRate;
5985
6218
  /** @property {boolean} - Is it additive */
5986
6219
  this.additive = additive;
5987
- /** @property {Number} - If a trail, how long to make it */
6220
+ /** @property {number} - If a trail, how long to make it */
5988
6221
  this.trailScale = trailScale;
5989
6222
  /** @property {ParticleEmitter} - Parent emitter if local space */
5990
6223
  this.localSpaceEmitter = localSpaceEmitter;
@@ -6024,7 +6257,7 @@ class Particle extends EngineObject
6024
6257
  this.colorStart.r + p * this.colorEndDelta.r,
6025
6258
  this.colorStart.g + p * this.colorEndDelta.g,
6026
6259
  this.colorStart.b + p * this.colorEndDelta.b,
6027
- (this.colorStart.a + p * this.colorEndDelta.a) *
6260
+ (this.colorStart.a + p * this.colorEndDelta.a) *
6028
6261
  (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
6029
6262
 
6030
6263
  // draw the particle
@@ -6034,7 +6267,7 @@ class Particle extends EngineObject
6034
6267
  if (this.localSpaceEmitter)
6035
6268
  {
6036
6269
  // in local space of emitter
6037
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
6270
+ pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
6038
6271
  angle += this.localSpaceEmitter.angle;
6039
6272
  }
6040
6273
  if (this.trailScale)
@@ -6058,7 +6291,7 @@ class Particle extends EngineObject
6058
6291
  this.additive && setBlendMode();
6059
6292
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
6060
6293
 
6061
- if (p == 1)
6294
+ if (p === 1)
6062
6295
  {
6063
6296
  // destroy particle when it's time runs out
6064
6297
  this.color = color;
@@ -6068,7 +6301,7 @@ class Particle extends EngineObject
6068
6301
  }
6069
6302
  }
6070
6303
  }
6071
- /**
6304
+ /**
6072
6305
  * LittleJS Medal System
6073
6306
  * - Tracks and displays medals
6074
6307
  * - Saves medals to local storage
@@ -6089,7 +6322,7 @@ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
6089
6322
  /** Initialize medals with a save name used for storage
6090
6323
  * - Call this after creating all medals
6091
6324
  * - Checks if medals are unlocked
6092
- * @param {String} saveName
6325
+ * @param {string} saveName
6093
6326
  * @memberof Medals */
6094
6327
  function medalsInit(saveName)
6095
6328
  {
@@ -6104,7 +6337,7 @@ function medalsInit(saveName)
6104
6337
  {
6105
6338
  if (!medalsDisplayQueue.length)
6106
6339
  return;
6107
-
6340
+
6108
6341
  // update first medal in queue
6109
6342
  const medal = medalsDisplayQueue[0];
6110
6343
  const time = timeReal - medalsDisplayTimeLast;
@@ -6119,7 +6352,7 @@ function medalsInit(saveName)
6119
6352
  {
6120
6353
  // slide on/off medals
6121
6354
  const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
6122
- const hidePercent =
6355
+ const hidePercent =
6123
6356
  time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
6124
6357
  time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
6125
6358
  medal.render(hidePercent);
@@ -6135,43 +6368,43 @@ function medalsForEach(callback)
6135
6368
 
6136
6369
  ///////////////////////////////////////////////////////////////////////////////
6137
6370
 
6138
- /**
6139
- * Medal - Tracks an unlockable medal
6371
+ /**
6372
+ * Medal - Tracks an unlockable medal
6140
6373
  * @example
6141
6374
  * // create a medal
6142
6375
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
6143
- *
6376
+ *
6144
6377
  * // initialize medals
6145
6378
  * medalsInit('Example Game');
6146
- *
6379
+ *
6147
6380
  * // unlock the medal
6148
6381
  * medal_example.unlock();
6149
6382
  */
6150
6383
  class Medal
6151
6384
  {
6152
6385
  /** Create a medal object and adds it to the list of medals
6153
- * @param {Number} id - The unique identifier of the medal
6154
- * @param {String} name - Name of the medal
6155
- * @param {String} [description] - Description of the medal
6156
- * @param {String} [icon] - Icon for the medal
6157
- * @param {String} [src] - Image location for the medal
6386
+ * @param {number} id - The unique identifier of the medal
6387
+ * @param {string} name - Name of the medal
6388
+ * @param {string} [description] - Description of the medal
6389
+ * @param {string} [icon] - Icon for the medal
6390
+ * @param {string} [src] - Image location for the medal
6158
6391
  */
6159
6392
  constructor(id, name, description='', icon='🏆', src)
6160
6393
  {
6161
6394
  ASSERT(id >= 0 && !medals[id]);
6162
-
6163
- /** @property {Number} - The unique identifier of the medal */
6395
+
6396
+ /** @property {number} - The unique identifier of the medal */
6164
6397
  this.id = id;
6165
-
6166
- /** @property {String} - Name of the medal */
6398
+
6399
+ /** @property {string} - Name of the medal */
6167
6400
  this.name = name;
6168
-
6169
- /** @property {String} - Description of the medal */
6401
+
6402
+ /** @property {string} - Description of the medal */
6170
6403
  this.description = description;
6171
-
6172
- /** @property {String} - Icon for the medal */
6404
+
6405
+ /** @property {string} - Icon for the medal */
6173
6406
  this.icon = icon;
6174
-
6407
+
6175
6408
  /** @property {boolean} - Is the medal unlocked? */
6176
6409
  this.unlocked = false;
6177
6410
 
@@ -6196,7 +6429,7 @@ class Medal
6196
6429
  }
6197
6430
 
6198
6431
  /** Render a medal
6199
- * @param {Number} [hidePercent] - How much to slide the medal off screen
6432
+ * @param {number} [hidePercent] - How much to slide the medal off screen
6200
6433
  */
6201
6434
  render(hidePercent=0)
6202
6435
  {
@@ -6237,7 +6470,7 @@ class Medal
6237
6470
 
6238
6471
  /** Render the icon for a medal
6239
6472
  * @param {Vector2} pos - Screen space position
6240
- * @param {Number} size - Screen space size
6473
+ * @param {number} size - Screen space size
6241
6474
  */
6242
6475
  renderIcon(pos, size)
6243
6476
  {
@@ -6247,14 +6480,14 @@ class Medal
6247
6480
  else
6248
6481
  drawTextScreen(this.icon, pos, size*.7, BLACK);
6249
6482
  }
6250
-
6483
+
6251
6484
  // Get local storage key used by the medal
6252
6485
  storageKey() { return medalsSaveName + '_' + this.id; }
6253
6486
  }
6254
6487
  /**
6255
6488
  * LittleJS WebGL Interface
6256
- * - All webgl used by the engine is wrapped up here
6257
- * - Will fall back to 2D canvas rendering if webgl is not supported
6489
+ * - All WebGL used by the engine is wrapped up here
6490
+ * - Will fall back to 2D canvas rendering if WebGL is not supported
6258
6491
  * - For normal stuff you won't need to see or call anything in this file
6259
6492
  * - For advanced stuff there are helper functions to create shaders, textures, etc
6260
6493
  * - Can be disabled with glEnable to revert to 2D canvas rendering
@@ -6269,24 +6502,27 @@ class Medal
6269
6502
  * @memberof WebGL */
6270
6503
  let glCanvas;
6271
6504
 
6272
- /** 2d context for glCanvas
6505
+ /** WebGL2 context for `glCanvas`
6273
6506
  * @type {WebGL2RenderingContext}
6274
6507
  * @memberof WebGL */
6275
6508
  let glContext;
6276
6509
 
6277
- /** Should webgl be setup with anti-aliasing? must be set before calling engineInit
6510
+ /** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
6278
6511
  * @type {boolean}
6279
6512
  * @memberof WebGL */
6280
6513
  let glAntialias = true;
6281
6514
 
6282
6515
  // WebGL internal variables not exposed to documentation
6283
- let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive;
6516
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
6284
6517
 
6285
- // WebGL internal constants
6286
- const gl_MAX_INSTANCES = 1e4;
6518
+ // WebGL internal constants
6519
+ const gl_ARRAY_BUFFER_SIZE = 4e5;
6287
6520
  const gl_INDICES_PER_INSTANCE = 11;
6288
6521
  const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
6289
- const gl_INSTANCE_BUFFER_SIZE = gl_MAX_INSTANCES * gl_INSTANCE_BYTE_STRIDE;
6522
+ const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
6523
+ const gl_INDICES_PER_POLY_VERTEX = 3;
6524
+ const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
6525
+ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
6290
6526
 
6291
6527
  ///////////////////////////////////////////////////////////////////////////////
6292
6528
 
@@ -6307,11 +6543,11 @@ function glInit()
6307
6543
  return;
6308
6544
  }
6309
6545
 
6310
- // create the webgl canvas
6546
+ // create the WebGL canvas
6311
6547
  const rootElement = mainCanvas.parentElement;
6312
6548
  rootElement.appendChild(glCanvas);
6313
6549
 
6314
- // setup vertex and fragment shaders
6550
+ // setup instanced rendering shader program
6315
6551
  glShader = glCreateProgram(
6316
6552
  '#version 300 es\n' + // specify GLSL ES version
6317
6553
  'precision highp float;'+ // use highp for better accuracy
@@ -6339,40 +6575,59 @@ function glInit()
6339
6575
  '}' // end of shader
6340
6576
  );
6341
6577
 
6578
+ // setup poly rendering shaders
6579
+ glPolyShader = glCreateProgram(
6580
+ '#version 300 es\n' + // specify GLSL ES version
6581
+ 'precision highp float;'+ // use highp for better accuracy
6582
+ 'uniform mat4 m;'+ // transform matrix
6583
+ 'in vec2 p;'+ // in: position
6584
+ 'in vec4 c;'+ // in: color
6585
+ 'out vec4 d;'+ // out: color
6586
+ 'void main(){'+ // shader entry point
6587
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
6588
+ 'd=c;'+ // pass color to fragment shader
6589
+ '}' // end of shader
6590
+ ,
6591
+ '#version 300 es\n' + // specify GLSL ES version
6592
+ 'precision highp float;'+ // use highp for better accuracy
6593
+ 'in vec4 d;'+ // in: color
6594
+ 'out vec4 c;'+ // out: color
6595
+ 'void main(){'+ // shader entry point
6596
+ 'c=d;'+ // set color
6597
+ '}' // end of shader
6598
+ );
6599
+
6342
6600
  // init buffers
6343
- const glInstanceData = new ArrayBuffer(gl_INSTANCE_BUFFER_SIZE);
6601
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
6344
6602
  glPositionData = new Float32Array(glInstanceData);
6345
6603
  glColorData = new Uint32Array(glInstanceData);
6346
6604
  glArrayBuffer = glContext.createBuffer();
6347
6605
  glGeometryBuffer = glContext.createBuffer();
6348
6606
 
6349
6607
  // create the geometry buffer, triangle strip square
6350
- const geometry = new Float32Array([glInstanceCount=0,0,1,0,0,1,1,1]);
6608
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
6351
6609
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6352
6610
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
6353
6611
  }
6354
6612
 
6355
- // Setup webgl render each frame, called automatically by engine
6356
- // Also used by tile layer rendering when redrawing tiles
6357
- function glPreRender()
6613
+ function glSetInstancedMode(force=false)
6358
6614
  {
6359
- if (!glEnable || !glContext) return;
6360
-
6361
- // set up the shader and canvas
6362
- glClearCanvas();
6615
+ if (!glPolyMode && !force)
6616
+ return;
6617
+
6618
+ // setup instanced mode
6619
+ glFlush();
6620
+ glPolyMode = false;
6363
6621
  glContext.useProgram(glShader);
6364
- glContext.activeTexture(glContext.TEXTURE0);
6365
- if (textureInfos[0])
6366
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
6367
6622
 
6368
6623
  // set vertex attributes
6369
- let offset = glAdditive = glBatchAdditive = 0;
6624
+ let offset = 0;
6370
6625
  const initVertexAttribArray = (name, type, typeSize, size)=>
6371
6626
  {
6372
6627
  const location = glContext.getAttribLocation(glShader, name);
6373
6628
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
6374
6629
  const divisor = typeSize && 1; // only if not geometry
6375
- const normalize = typeSize == 1; // only if color
6630
+ const normalize = typeSize === 1; // only if color
6376
6631
  glContext.enableVertexAttribArray(location);
6377
6632
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
6378
6633
  glContext.vertexAttribDivisor(location, divisor);
@@ -6381,26 +6636,84 @@ function glPreRender()
6381
6636
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6382
6637
  initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
6383
6638
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
6384
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_INSTANCE_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6639
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6385
6640
  initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
6386
6641
  initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
6387
6642
  initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
6388
6643
  initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
6389
6644
  initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
6645
+ }
6646
+
6647
+ function glSetPolyMode()
6648
+ {
6649
+ if (glPolyMode)
6650
+ return;
6390
6651
 
6652
+ // setup poly mode
6653
+ glFlush();
6654
+ glPolyMode = true;
6655
+ glContext.useProgram(glPolyShader);
6656
+
6657
+ // set vertex attributes
6658
+ let offset = 0;
6659
+ const initVertexAttribArray = (name, type, typeSize, size)=>
6660
+ {
6661
+ const location = glContext.getAttribLocation(glPolyShader, name);
6662
+ const normalize = typeSize === 1; // only normalize if color
6663
+ const stride = gl_POLY_VERTEX_BYTE_STRIDE;
6664
+ glContext.enableVertexAttribArray(location);
6665
+ glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
6666
+ glContext.vertexAttribDivisor(location, 0);
6667
+ offset += size*typeSize;
6668
+ }
6669
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
6670
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6671
+ initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
6672
+ initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
6673
+ }
6674
+
6675
+ // Setup WebGL render each frame, called automatically by engine
6676
+ // Also used by tile layer rendering when redrawing tiles
6677
+ function glPreRender()
6678
+ {
6679
+ if (!glEnable || !glContext) return;
6680
+
6681
+ // clear the canvas
6682
+ glClearCanvas();
6683
+
6391
6684
  // build the transform matrix
6392
6685
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
6393
6686
  const rotatedCam = cameraPos.rotate(-cameraAngle);
6394
6687
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
6395
6688
  const ca = Math.cos(cameraAngle);
6396
6689
  const sa = Math.sin(cameraAngle);
6397
- glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
6398
- [
6690
+ const transform = [
6399
6691
  s.x * ca, s.y * sa, 0, 0,
6400
6692
  -s.x * sa, s.y * ca, 0, 0,
6401
6693
  1, 1, 1, 0,
6402
- p.x, p.y, 0, 1
6403
- ]);
6694
+ p.x, p.y, 0, 1];
6695
+
6696
+ // set the same matrix for both shaders
6697
+ const initUniform = (program, uniform, value) =>
6698
+ {
6699
+ glContext.useProgram(program);
6700
+ const location = glContext.getUniformLocation(program, uniform);
6701
+ glContext.uniformMatrix4fv(location, false, value);
6702
+ }
6703
+ initUniform(glPolyShader, 'm', transform);
6704
+ initUniform(glShader, 'm', transform);
6705
+
6706
+ // set the active texture
6707
+ glContext.activeTexture(glContext.TEXTURE0);
6708
+ if (textureInfos[0])
6709
+ {
6710
+ glActiveTexture = textureInfos[0].glTexture;
6711
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
6712
+ }
6713
+
6714
+ // start in instanced rendering mode with additive blending off
6715
+ glAdditive = glBatchAdditive = glPolyMode = false;
6716
+ glSetInstancedMode(true);
6404
6717
  }
6405
6718
 
6406
6719
  /** Clear the canvas and setup the viewport
@@ -6408,21 +6721,21 @@ function glPreRender()
6408
6721
  function glClearCanvas()
6409
6722
  {
6410
6723
  if (!glContext) return;
6411
-
6724
+
6412
6725
  // clear and set to same size as main canvas
6413
6726
  glContext.viewport(0, 0, glCanvas.width=drawCanvas.width, glCanvas.height=drawCanvas.height);
6414
6727
  glContext.clear(glContext.COLOR_BUFFER_BIT);
6415
6728
  }
6416
6729
 
6417
- /** Set the WebGl texture, called automatically if using multiple textures
6730
+ /** Set the WebGL texture, called automatically if using multiple textures
6418
6731
  * - This may also flush the gl buffer resulting in more draw calls and worse performance
6419
6732
  * @param {WebGLTexture} texture
6420
- * @param {boolean} wrap - Should the texture wrap or clamp
6733
+ * @param {boolean} [wrap] - Should the texture wrap or clamp
6421
6734
  * @memberof WebGL */
6422
6735
  function glSetTexture(texture, wrap=false)
6423
6736
  {
6424
6737
  // must flush cache with the old texture to set a new one
6425
- if (!glContext || texture == glActiveTexture)
6738
+ if (!glContext || texture === glActiveTexture)
6426
6739
  return;
6427
6740
 
6428
6741
  glFlush();
@@ -6435,8 +6748,8 @@ function glSetTexture(texture, wrap=false)
6435
6748
  }
6436
6749
 
6437
6750
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
6438
- * @param {String} source
6439
- * @param {Number} type
6751
+ * @param {string} source
6752
+ * @param {number} type
6440
6753
  * @return {WebGLShader}
6441
6754
  * @memberof WebGL */
6442
6755
  function glCompileShader(source, type)
@@ -6455,8 +6768,8 @@ function glCompileShader(source, type)
6455
6768
  }
6456
6769
 
6457
6770
  /** Create WebGL program with given shaders
6458
- * @param {String} vsSource
6459
- * @param {String} fsSource
6771
+ * @param {string} vsSource
6772
+ * @param {string} fsSource
6460
6773
  * @return {WebGLProgram}
6461
6774
  * @memberof WebGL */
6462
6775
  function glCreateProgram(vsSource, fsSource)
@@ -6504,7 +6817,7 @@ function glCreateTexture(image)
6504
6817
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
6505
6818
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
6506
6819
  }
6507
-
6820
+
6508
6821
  // set texture filtering
6509
6822
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
6510
6823
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -6512,7 +6825,6 @@ function glCreateTexture(image)
6512
6825
  return texture;
6513
6826
  }
6514
6827
 
6515
-
6516
6828
  /** Deletes a WebGL texture
6517
6829
  * @param {WebGLTexture} [texture]
6518
6830
  * @memberof WebGL */
@@ -6540,18 +6852,23 @@ function glSetTextureData(texture, image)
6540
6852
  * @memberof WebGL */
6541
6853
  function glFlush()
6542
6854
  {
6543
- if (!glEnable || !glContext || !glInstanceCount) return;
6544
-
6545
- const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
6546
- glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
6547
- glContext.enable(glContext.BLEND);
6548
-
6549
- // draw all the sprites in the batch and reset the buffer
6550
- glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData);
6551
- glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glInstanceCount);
6552
- if (debug || showWatermark)
6553
- drawCount += glInstanceCount;
6554
- glInstanceCount = 0;
6855
+ if (glEnable && glContext && glBatchCount)
6856
+ {
6857
+ // set bend mode
6858
+ const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
6859
+ glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
6860
+ glContext.enable(glContext.BLEND);
6861
+ glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData);
6862
+
6863
+ // draw the batch
6864
+ if (glPolyMode)
6865
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
6866
+ else
6867
+ glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
6868
+ if (debug || showWatermark)
6869
+ drawCount += glBatchCount;
6870
+ glBatchCount = 0;
6871
+ }
6555
6872
  glBatchAdditive = glAdditive;
6556
6873
  }
6557
6874
 
@@ -6567,7 +6884,8 @@ function glCopyToContext(context)
6567
6884
  context.drawImage(glCanvas, 0, 0);
6568
6885
  }
6569
6886
 
6570
- /** Set anti-aliasing for webgl canvas
6887
+ /** Set anti-aliasing for WebGL canvas
6888
+ * Must be called before engineInit
6571
6889
  * @param {boolean} [antialias]
6572
6890
  * @memberof WebGL */
6573
6891
  function glSetAntialias(antialias=true)
@@ -6577,25 +6895,27 @@ function glSetAntialias(antialias=true)
6577
6895
  }
6578
6896
 
6579
6897
  /** Add a sprite to the gl draw list, used by all gl draw functions
6580
- * @param {Number} x
6581
- * @param {Number} y
6582
- * @param {Number} sizeX
6583
- * @param {Number} sizeY
6584
- * @param {Number} [angle]
6585
- * @param {Number} [uv0X]
6586
- * @param {Number} [uv0Y]
6587
- * @param {Number} [uv1X]
6588
- * @param {Number} [uv1Y]
6589
- * @param {Number} [rgba=-1] - white is -1
6590
- * @param {Number} [rgbaAdditive=0] - black is 0
6898
+ * @param {number} x
6899
+ * @param {number} y
6900
+ * @param {number} sizeX
6901
+ * @param {number} sizeY
6902
+ * @param {number} [angle]
6903
+ * @param {number} [uv0X]
6904
+ * @param {number} [uv0Y]
6905
+ * @param {number} [uv1X]
6906
+ * @param {number} [uv1Y]
6907
+ * @param {number} [rgba=-1] - white is -1
6908
+ * @param {number} [rgbaAdditive=0] - black is 0
6591
6909
  * @memberof WebGL */
6592
6910
  function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
6593
6911
  {
6594
6912
  // flush if there is not enough room or if different blend mode
6595
- if (glInstanceCount >= gl_MAX_INSTANCES || glBatchAdditive != glAdditive)
6913
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
6596
6914
  glFlush();
6915
+ glSetInstancedMode();
6597
6916
 
6598
- let offset = glInstanceCount++ * gl_INDICES_PER_INSTANCE;
6917
+ glPolyMode = false;
6918
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
6599
6919
  glPositionData[offset++] = x;
6600
6920
  glPositionData[offset++] = y;
6601
6921
  glPositionData[offset++] = sizeX;
@@ -6607,6 +6927,301 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
6607
6927
  glColorData[offset++] = rgba;
6608
6928
  glColorData[offset++] = rgbaAdditive;
6609
6929
  glPositionData[offset++] = angle;
6930
+ }
6931
+
6932
+ /** Transform and add a polygon to the gl draw list
6933
+ * @param {Array} points - Array of Vector2 points
6934
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6935
+ * @param {number} x
6936
+ * @param {number} y
6937
+ * @param {number} sx
6938
+ * @param {number} sy
6939
+ * @param {number} angle
6940
+ * @param {boolean} [tristrip] - should tristrip algorithm be used
6941
+ * @memberof WebGL */
6942
+ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
6943
+ {
6944
+ const pointsOut = [];
6945
+ for (const p of points)
6946
+ {
6947
+ // transform the point
6948
+ const px = p.x*sx;
6949
+ const py = p.y*sy;
6950
+ const sa = Math.sin(-angle);
6951
+ const ca = Math.cos(-angle);
6952
+ pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
6953
+ }
6954
+ const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
6955
+ glDrawPoints(drawPoints, rgba);
6956
+ }
6957
+
6958
+ /** Transform and add a polygon to the gl draw list
6959
+ * @param {Array} points - Array of Vector2 points
6960
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6961
+ * @param {number} lineWidth - Width of the outline
6962
+ * @param {number} x
6963
+ * @param {number} y
6964
+ * @param {number} sx
6965
+ * @param {number} sy
6966
+ * @param {number} angle
6967
+ * @memberof WebGL */
6968
+ function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle)
6969
+ {
6970
+ const outlinePoints = glMakeOutline(points, lineWidth);
6971
+ glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
6972
+ }
6973
+
6974
+ /** Add a polygon to the gl draw list
6975
+ * @param {Array} points - Array of Vector2 points in triangle strip order
6976
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6977
+ * @memberof WebGL */
6978
+ function glDrawPoints(points, rgba)
6979
+ {
6980
+ if (!glEnable || points.length < 3)
6981
+ return; // needs at least 3 points to have area
6982
+
6983
+ // add 2 degenerate verts if batching with existing polys to separate them
6984
+ const needsBridge = glPolyMode && glBatchCount > 0;
6985
+ const bridgeVerts = needsBridge ? 2 : 0;
6986
+ const vertCount = points.length + bridgeVerts;
6987
+
6988
+ // flush if there is not enough room or if different blend mode
6989
+ if (!glPolyMode || glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
6990
+ glFlush();
6991
+ glSetPolyMode();
6992
+
6993
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
6994
+
6995
+ // add degenerate bridge if needed (repeat last vertex of previous poly, then first of new poly)
6996
+ if (needsBridge)
6997
+ {
6998
+ // repeat last vertex from previous batch (it's at offset - 3)
6999
+ const prevOffset = offset - 3;
7000
+ glPositionData[offset++] = glPositionData[prevOffset];
7001
+ glPositionData[offset++] = glPositionData[prevOffset + 1];
7002
+ glColorData[offset++] = glColorData[prevOffset + 2];
7003
+
7004
+ // repeat first vertex of new poly
7005
+ glPositionData[offset++] = points[0].x;
7006
+ glPositionData[offset++] = points[0].y;
7007
+ glColorData[offset++] = rgba;
7008
+ }
7009
+
7010
+ // write vertices - they're already in triangle strip order
7011
+ for (const point of points)
7012
+ {
7013
+ glPositionData[offset++] = point.x;
7014
+ glPositionData[offset++] = point.y;
7015
+ glColorData[offset++] = rgba;
7016
+ }
7017
+ glBatchCount += vertCount;
7018
+ }
7019
+
7020
+ // WebGL internal function to convert polygon to outline triangle strip
7021
+ function glMakeOutline(points, width)
7022
+ {
7023
+ if (points.length < 2)
7024
+ return [];
7025
+
7026
+ const halfWidth = width / 2;
7027
+ const strip = [];
7028
+ const n = points.length;
7029
+ const e = 1e-6;
7030
+ for (let i = 0; i < n; i++)
7031
+ {
7032
+ // for each vertex, calculate normal based on adjacent edges
7033
+ const prev = points[(i - 1 + n) % n];
7034
+ const curr = points[i];
7035
+ const next = points[(i + 1) % n];
7036
+
7037
+ // direction from previous to current
7038
+ const dx1 = curr.x - prev.x;
7039
+ const dy1 = curr.y - prev.y;
7040
+ const len1 = (dx1*dx1 + dy1*dy1)**.5;
7041
+
7042
+ // direction from current to next
7043
+ const dx2 = next.x - curr.x;
7044
+ const dy2 = next.y - curr.y;
7045
+ const len2 = (dx2*dx2 + dy2*dy2)**.5;
7046
+
7047
+ if (len1 < e && len2 < e)
7048
+ continue; // skip degenerate point
7049
+
7050
+ // calculate perpendicular normals for each edge
7051
+ const nx1 = len1 > e ? -dy1 / len1 : 0;
7052
+ const ny1 = len1 > e ? dx1 / len1 : 0;
7053
+ const nx2 = len2 > e ? -dy2 / len2 : 0;
7054
+ const ny2 = len2 > e ? dx2 / len2 : 0;
7055
+
7056
+ // average the normals for miter
7057
+ let nx = nx1 + nx2;
7058
+ let ny = ny1 + ny2;
7059
+ const nlen = (nx*nx + ny*ny)**.5;
7060
+ if (nlen < e)
7061
+ {
7062
+ // 180 degree turn - use perpendicular
7063
+ nx = nx1;
7064
+ ny = ny1;
7065
+ }
7066
+ else
7067
+ {
7068
+ // calculate miter length
7069
+ nx /= nlen;
7070
+ ny /= nlen;
7071
+ const dot = nx1 * nx + ny1 * ny;
7072
+ if (dot > e)
7073
+ {
7074
+ // scale normal by miter length
7075
+ const miterLength = 1 / dot;
7076
+ nx *= miterLength;
7077
+ ny *= miterLength;
7078
+ }
7079
+ }
7080
+
7081
+ // create inner and outer points along the normal
7082
+ const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
7083
+ const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
7084
+ strip.push(inner);
7085
+ strip.push(outer);
7086
+ }
7087
+ if (strip.length > 1)
7088
+ {
7089
+ // close the loop
7090
+ strip.push(strip[0]);
7091
+ strip.push(strip[1]);
7092
+ }
7093
+ return strip;
7094
+ }
7095
+
7096
+ // WebGL internal function to convert polys to tri strips
7097
+ function glPolyStrip(points)
7098
+ {
7099
+ // validate input
7100
+ if (points.length < 3)
7101
+ return [];
7102
+
7103
+ // cross product helper: (b-a) x (c-a)
7104
+ const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
7105
+
7106
+ // calculate signed area of polygon
7107
+ const signedArea = (poly)=>
7108
+ {
7109
+ let area = 0;
7110
+ for (let i = poly.length; i--;)
7111
+ {
7112
+ const j = (i+1) % poly.length;
7113
+ area += poly[i].cross(poly[j]);
7114
+ }
7115
+ return area;
7116
+ }
7117
+
7118
+ // ensure counter-clockwise winding
7119
+ if (signedArea(points) < 0)
7120
+ points = points.reverse();
7121
+
7122
+ // tolerance constants
7123
+ const e = 1e-10;
7124
+
7125
+ // check if point is inside triangle
7126
+ const pointInTriangle = (p, a, b, c)=>
7127
+ {
7128
+ const c1 = cross(a, b, p);
7129
+ const c2 = cross(b, c, p);
7130
+ const c3 = cross(c, a, p);
7131
+ const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
7132
+ const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
7133
+ return !(negative && positive);
7134
+ };
7135
+
7136
+ // ear clipping triangulation
7137
+ const indices = [];
7138
+ for (let i = 0; i < points.length; ++i)
7139
+ indices[i] = i;
7140
+ const triangles = [];
7141
+ let attempts = 0;
7142
+ const maxAttempts = points.length ** 2 + 100;
7143
+ while (indices.length > 3 && attempts++ < maxAttempts)
7144
+ {
7145
+ let foundEar = false;
7146
+ for (let i = indices.length; --i;)
7147
+ {
7148
+ const i0 = indices[(i + indices.length - 1) % indices.length];
7149
+ const i1 = indices[i];
7150
+ const i2 = indices[(i + 1) % indices.length];
7151
+ const a = points[i0], b = points[i1], c = points[i2];
7152
+
7153
+ // check if convex
7154
+ if (cross(a, b, c) < e)
7155
+ continue;
7156
+
7157
+ // check if any other point is inside
7158
+ let hasInside = false;
7159
+ for (let j = 0; j < indices.length; j++)
7160
+ {
7161
+ const k = indices[j];
7162
+ if (k === i0 || k === i1 || k === i2)
7163
+ continue;
7164
+ const p = points[k];
7165
+ hasInside = pointInTriangle(p, a, b, c);
7166
+ if (hasInside)
7167
+ break;
7168
+ }
7169
+ if (hasInside)
7170
+ continue;
7171
+
7172
+ // found valid ear
7173
+ triangles.push([i0, i1, i2]);
7174
+ indices.splice(i, 1);
7175
+ foundEar = true;
7176
+ break;
7177
+ }
7178
+
7179
+ // fallback for degenerate cases
7180
+ if (!foundEar)
7181
+ {
7182
+ let worstIndex = -1, worstValue = Infinity;
7183
+ for (let i = indices.length; --i;)
7184
+ {
7185
+ const i0 = indices[(i + indices.length - 1) % indices.length];
7186
+ const i1 = indices[i];
7187
+ const i2 = indices[(i + 1) % indices.length];
7188
+ const value = abs(cross(points[i0], points[i1], points[i2]));
7189
+ if (value < worstValue)
7190
+ {
7191
+ worstValue = value;
7192
+ worstIndex = i;
7193
+ }
7194
+ }
7195
+ if (worstIndex < 0)
7196
+ break;
7197
+
7198
+ const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
7199
+ const i1 = indices[worstIndex];
7200
+ const i2 = indices[(worstIndex + 1) % indices.length];
7201
+ triangles.push([i0, i1, i2]);
7202
+ indices.splice(worstIndex, 1);
7203
+ }
7204
+ }
7205
+
7206
+ // add final triangle
7207
+ if (indices.length === 3)
7208
+ triangles.push([indices[0], indices[1], indices[2]]);
7209
+ if (!triangles.length)
7210
+ return [];
7211
+
7212
+ // convert triangles to triangle strip with degenerate connectors
7213
+ const strip = [];
7214
+ let [a0, b0, c0] = triangles[0];
7215
+ strip.push(points[a0], points[b0], points[c0]);
7216
+ for (let i = 1; i < triangles.length; i++)
7217
+ {
7218
+ // add degenerate bridge from last vertex to first of new triangle
7219
+ const [a, b, c] = triangles[i];
7220
+ strip.push(points[c0], points[a]);
7221
+ strip.push(points[a], points[b], points[c]);
7222
+ c0 = c;
7223
+ }
7224
+ return strip;
6610
7225
  }
6611
7226
  /**
6612
7227
  * LittleJS Newgrounds API
@@ -6631,11 +7246,11 @@ let newgrounds;
6631
7246
  class NewgroundsMedal extends Medal
6632
7247
  {
6633
7248
  /** Create a newgrounds medal object and adds it to the list of medals
6634
- * @param {Number} id - The unique identifier of the medal
6635
- * @param {String} name - Name of the medal
6636
- * @param {String} [description] - Description of the medal
6637
- * @param {String} [icon] - Icon for the medal
6638
- * @param {String} [src] - Image location for the medal
7249
+ * @param {number} id - The unique identifier of the medal
7250
+ * @param {string} name - Name of the medal
7251
+ * @param {string} [description] - Description of the medal
7252
+ * @param {string} [icon] - Icon for the medal
7253
+ * @param {string} [src] - Image location for the medal
6639
7254
  */
6640
7255
  constructor(id, name, description, icon, src)
6641
7256
  { super(id, name, description, icon, src); }
@@ -6727,7 +7342,7 @@ class NewgroundsPlugin
6727
7342
  * @param {number} id - The scoreboard id
6728
7343
  * @param {string} [user] - A user's id or name
6729
7344
  * @param {number} [social] - If true, only social scores will be loaded
6730
- * @param {number} [skip] - Number of scores to skip before start
7345
+ * @param {number} [skip] - Number of scores to skip over
6731
7346
  * @param {number} [limit] - Number of scores to include in the list
6732
7347
  * @return {Object} - The response JSON object
6733
7348
  */
@@ -6950,16 +7565,16 @@ class ZzFXMusic extends Sound
6950
7565
  if (!soundEnable || headlessMode) return;
6951
7566
  this.randomness = 0;
6952
7567
  this.sampleChannels = zzfxM(...zzfxMusic);
6953
- this.sampleRate = zzfxR;
7568
+ this.sampleRate = audioDefaultSampleRate;
6954
7569
  }
6955
7570
 
6956
- /** Play the music
6957
- * @param {number} [volume=1] - How much to scale volume by
6958
- * @param {boolean} [loop] - True if the music should loop
7571
+ /** Play the music that loops by default
7572
+ * @param {number} [volume] - Volume to play the music at
7573
+ * @param {boolean} [loop] - Should the music loop?
6959
7574
  * @return {AudioBufferSourceNode} - The audio source node
6960
7575
  */
6961
- playMusic(volume, loop=false)
6962
- { return super.play(undefined, volume, 1, 1, loop); }
7576
+ playMusic(volume=1, loop=true)
7577
+ { return super.play(undefined, volume, 1, 0, loop); }
6963
7578
  }
6964
7579
 
6965
7580
  ///////////////////////////////////////////////////////////////////////////////
@@ -6993,7 +7608,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
6993
7608
  let panning = 0;
6994
7609
  let hasMore = 1;
6995
7610
  let sampleCache = {};
6996
- let beatLength = zzfxR / BPM * 60 >> 2;
7611
+ let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
6997
7612
 
6998
7613
  // for each channel in order until there are no more
6999
7614
  for (; hasMore; channelIndex++) {
@@ -7012,15 +7627,15 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
7012
7627
  // get next offset, use the length of first channel
7013
7628
  nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
7014
7629
  // for each beat in pattern, plus one extra if end of sequence
7015
- isSequenceEnd = sequenceIndex == sequence.length - 1;
7630
+ isSequenceEnd = sequenceIndex === sequence.length - 1;
7016
7631
  for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
7017
7632
 
7018
7633
  // <channel-note>
7019
7634
  note = patternChannel[i];
7020
7635
 
7021
7636
  // stop if end, different instrument or new note
7022
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
7023
- instrument != (patternChannel[0] || 0) || note | 0;
7637
+ stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
7638
+ instrument !== (patternChannel[0] || 0) || note | 0;
7024
7639
 
7025
7640
  // fill buffer with samples for previous beat, most cpu intensive part
7026
7641
  for (j = 0; j < beatLength && notFirstBeat;
@@ -7105,11 +7720,11 @@ class UISystemPlugin
7105
7720
  /** @property {Color} - Default text color for UI elements */
7106
7721
  this.defaultTextColor = BLACK;
7107
7722
  /** @property {Color} - Default button color for UI elements */
7108
- this.defaultButtonColor = hsl(0,0,.5);
7723
+ this.defaultButtonColor = hsl(0,0,.7);
7109
7724
  /** @property {Color} - Default hover color for UI elements */
7110
- this.defaultHoverColor = hsl(0,0,.7);
7725
+ this.defaultHoverColor = hsl(0,0,.9);
7111
7726
  /** @property {Color} - Default color for disabled UI elements */
7112
- this.defaultDisabledColor = hsl(0,0,.2);
7727
+ this.defaultDisabledColor = hsl(0,0,.3);
7113
7728
  /** @property {number} - Default line width for UI elements */
7114
7729
  this.defaultLineWidth = 4;
7115
7730
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -7129,16 +7744,16 @@ class UISystemPlugin
7129
7744
 
7130
7745
  engineAddPlugin(uiUpdate, uiRender);
7131
7746
 
7132
- function updateInvisible(o)
7133
- {
7134
- for (const c of o.children)
7135
- updateInvisible(c);
7136
- o.updateInvisible();
7137
- }
7138
-
7139
7747
  // setup recursive update and render
7140
7748
  function uiUpdate()
7141
7749
  {
7750
+ function updateInvisibleObject(o)
7751
+ {
7752
+ // update invisible objects
7753
+ for (const c of o.children)
7754
+ updateInvisibleObject(c);
7755
+ o.updateInvisible();
7756
+ }
7142
7757
  function updateObject(o)
7143
7758
  {
7144
7759
  if (o.visible)
@@ -7152,7 +7767,7 @@ class UISystemPlugin
7152
7767
  o.update();
7153
7768
  }
7154
7769
  else
7155
- updateInvisible(o);
7770
+ updateInvisibleObject(o);
7156
7771
  }
7157
7772
  uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
7158
7773
  }
@@ -7178,7 +7793,7 @@ class UISystemPlugin
7178
7793
  * @param {Color} [color=uiSystem.defaultColor]
7179
7794
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
7180
7795
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7181
- * @param {number} [lineWidth=uiSystem.defaultCornerRadius] */
7796
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
7182
7797
  drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
7183
7798
  {
7184
7799
  const context = uiSystem.uiContext;
@@ -7253,32 +7868,40 @@ class UIObject
7253
7868
  constructor(pos=vec2(), size=vec2())
7254
7869
  {
7255
7870
  /** @property {Vector2} - Local position of the object */
7256
- this.localPos = pos.copy();
7871
+ this.localPos = pos.copy();
7257
7872
  /** @property {Vector2} - Screen space position of the object */
7258
- this.pos = pos.copy();
7873
+ this.pos = pos.copy();
7259
7874
  /** @property {Vector2} - Screen space size of the object */
7260
- this.size = size.copy();
7261
- /** @property {Color} - color of the object */
7262
- this.color = uiSystem.defaultColor;
7263
- /** @property {Color} - color for text */
7264
- this.textColor = uiSystem.defaultTextColor;
7265
- /** @property {Color} - color used when hovering over the object */
7875
+ this.size = size.copy();
7876
+ /** @property {Color} - Color of the object */
7877
+ this.color = uiSystem.defaultColor;
7878
+ /** @property {string} - Text for this ui object */
7879
+ this.text = undefined;
7880
+ /** @property {Color} - Color when disabled */
7881
+ this.disabledColor = uiSystem.defaultDisabledColor;
7882
+ /** @property {boolean} - Is this object disabled? */
7883
+ this.disabled = false;
7884
+ /** @property {Color} - Color for text */
7885
+ this.textColor = uiSystem.defaultTextColor;
7886
+ /** @property {Color} - Color used when hovering over the object */
7266
7887
  this.hoverColor = uiSystem.defaultHoverColor;
7267
- /** @property {Color} - color for line drawing */
7268
- this.lineColor = uiSystem.defaultLineColor;
7269
- /** @property {number} - width for line drawing */
7270
- this.lineWidth = uiSystem.defaultLineWidth;
7271
- /** @property {string} - font for this objecct */
7272
- this.font = uiSystem.defaultFont;
7273
- /** @property {number} - override for text height */
7274
- this.textHeight = undefined;
7275
- /** @property {boolean} - should this object be drawn */
7276
- this.visible = true;
7277
- /** @property {Array<UIObject>} - a list of this object's children */
7278
- this.children = [];
7279
- /** @property {UIObject} - this object's parent, position is in parent space */
7280
- this.parent = undefined;
7281
- /** @property {number} - Extra size added when checking if element is touched */
7888
+ /** @property {Color} - Color for line drawing */
7889
+ this.lineColor = uiSystem.defaultLineColor;
7890
+ /** @property {number} - Width for line drawing */
7891
+ this.lineWidth = uiSystem.defaultLineWidth;
7892
+ /** @property {number} - Corner radius for rounded rects */
7893
+ this.cornerRadius = uiSystem.defaultCornerRadius;
7894
+ /** @property {string} - Font for this objecct */
7895
+ this.font = uiSystem.defaultFont;
7896
+ /** @property {number} - Override for text height */
7897
+ this.textHeight = undefined;
7898
+ /** @property {boolean} - Should this object be drawn */
7899
+ this.visible = true;
7900
+ /** @property {Array<UIObject>} - A list of this object's children */
7901
+ this.children = [];
7902
+ /** @property {UIObject} - This object's parent, position is in parent space */
7903
+ this.parent = undefined;
7904
+ /** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
7282
7905
  this.extraTouchSize = 0;
7283
7906
  /** @property {Sound} - Sound when interactive element is pressed */
7284
7907
  this.soundPress = uiSystem.defaultSoundPress;
@@ -7310,7 +7933,7 @@ class UIObject
7310
7933
  */
7311
7934
  removeChild(child)
7312
7935
  {
7313
- ASSERT(child.parent == this && this.children.includes(child));
7936
+ ASSERT(child.parent === this && this.children.includes(child));
7314
7937
  this.children.splice(this.children.indexOf(child), 1);
7315
7938
  child.parent = undefined;
7316
7939
  }
@@ -7363,7 +7986,7 @@ class UIObject
7363
7986
  this.mouseIsHeld = false;
7364
7987
  }
7365
7988
 
7366
- if (this.mouseIsOver != mouseWasOver)
7989
+ if (this.mouseIsOver !== mouseWasOver)
7367
7990
  this.mouseIsOver ? this.onEnter() : this.onLeave();
7368
7991
  }
7369
7992
 
@@ -7374,7 +7997,7 @@ class UIObject
7374
7997
  uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
7375
7998
  }
7376
7999
 
7377
- /** Special update for when object is invisible */
8000
+ /** Special update when object is not visible */
7378
8001
  updateInvisible()
7379
8002
  {
7380
8003
  // reset input state when not visible
@@ -7382,28 +8005,22 @@ class UIObject
7382
8005
  }
7383
8006
 
7384
8007
  /** Called when the mouse enters the object */
7385
- onEnter()
7386
- {}
8008
+ onEnter() {}
7387
8009
 
7388
8010
  /** Called when the mouse leaves the object */
7389
- onLeave()
7390
- {}
8011
+ onLeave() {}
7391
8012
 
7392
8013
  /** Called when the mouse is pressed while over the object */
7393
- onPress()
7394
- {}
8014
+ onPress() {}
7395
8015
 
7396
8016
  /** Called when the mouse is released while over the object */
7397
- onRelease()
7398
- {}
8017
+ onRelease() {}
7399
8018
 
7400
8019
  /** Called when user clicks on this object */
7401
- onClick()
7402
- {}
8020
+ onClick() {}
7403
8021
 
7404
8022
  /** Called when the state of this object changes */
7405
- onChange()
7406
- {}
8023
+ onChange() {}
7407
8024
  };
7408
8025
 
7409
8026
  ///////////////////////////////////////////////////////////////////////////////
@@ -7424,13 +8041,13 @@ class UIText extends UIObject
7424
8041
  {
7425
8042
  super(pos, size);
7426
8043
 
7427
- /** @property {string} */
8044
+ // set properties
7428
8045
  this.text = text;
7429
- /** @property {string} */
7430
8046
  this.align = align;
8047
+ this.font = font;
7431
8048
 
7432
- this.font = font; // set font
7433
- this.lineWidth = 0; // set text to not be outlined by default
8049
+ // make text not outlined by default
8050
+ this.lineWidth = 0;
7434
8051
  }
7435
8052
  render()
7436
8053
  {
@@ -7457,13 +8074,14 @@ class UITile extends UIObject
7457
8074
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
7458
8075
  {
7459
8076
  super(pos, size);
7460
-
7461
8077
  /** @property {TileInfo} - Tile image to use */
7462
8078
  this.tileInfo = tileInfo;
7463
8079
  /** @property {number} - Angle to rotate in radians */
7464
8080
  this.angle = angle;
7465
8081
  /** @property {boolean} - Should it be mirrored? */
7466
8082
  this.mirror = mirror;
8083
+
8084
+ // set properties
7467
8085
  this.color = color;
7468
8086
  }
7469
8087
  render()
@@ -7489,22 +8107,20 @@ class UIButton extends UIObject
7489
8107
  {
7490
8108
  super(pos, size);
7491
8109
 
7492
- /** @property {string} */
8110
+ // set properties
7493
8111
  this.text = text;
7494
- /** @property {Color} */
7495
- this.disabledColor = uiSystem.defaultDisabledColor;
7496
- /** @property {boolean} */
7497
- this.disabled = false;
7498
- this.interactive = true;
7499
8112
  this.color = color;
8113
+ this.interactive = true;
7500
8114
  }
7501
8115
  render()
7502
8116
  {
8117
+ // draw the button
7503
8118
  const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
7504
8119
  const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
7505
8120
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7506
8121
 
7507
- const textScale = .8; // scale text to fit in button
8122
+ // draw the text
8123
+ const textScale = .8; // scale text to fit
7508
8124
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7509
8125
  uiSystem.drawText(this.text, this.pos, textSize,
7510
8126
  this.textColor, 0, undefined, this.align, this.font);
@@ -7522,13 +8138,18 @@ class UICheckbox extends UIObject
7522
8138
  * @param {Vector2} [pos]
7523
8139
  * @param {Vector2} [size]
7524
8140
  * @param {boolean} [checked]
8141
+ * @param {string} [text]
8142
+ * @param {Color} [color=uiSystem.defaultButtonColor]
7525
8143
  */
7526
- constructor(pos, size, checked=false)
8144
+ constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
7527
8145
  {
7528
8146
  super(pos, size);
7529
-
7530
- /** @property {boolean} */
8147
+ /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
7531
8148
  this.checked = checked;
8149
+
8150
+ // set properties
8151
+ this.text = text;
8152
+ this.color = color;
7532
8153
  this.interactive = true;
7533
8154
  }
7534
8155
  onClick()
@@ -7538,14 +8159,24 @@ class UICheckbox extends UIObject
7538
8159
  }
7539
8160
  render()
7540
8161
  {
7541
- const color = this.mouseIsOver? this.hoverColor : this.color;
8162
+ const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
7542
8163
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
7543
8164
  if (this.checked)
7544
8165
  {
7545
- // draw an X if checked
7546
- uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor);
7547
- uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor);
8166
+ const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
8167
+ const length = lerp(1, 2**.5/2, p) / 2;
8168
+ let s = this.size.scale(length);
8169
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
8170
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
7548
8171
  }
8172
+
8173
+ // draw the text to the right side of the checkbox
8174
+ const textScale = .8; // scale text to fit
8175
+ const gapScale = .55;
8176
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
8177
+ const pos = this.pos.add(vec2(this.size.x*gapScale,0));
8178
+ uiSystem.drawText(this.text, pos, textSize,
8179
+ this.textColor, 0, undefined, 'left', this.font);
7549
8180
  }
7550
8181
  }
7551
8182
 
@@ -7568,43 +8199,51 @@ class UIScrollbar extends UIObject
7568
8199
  {
7569
8200
  super(pos, size);
7570
8201
 
7571
- /** @property {number} */
8202
+ /** @property {number} - Current percentage value of this scrollbar 0-1 */
7572
8203
  this.value = value;
7573
- /** @property {string} */
7574
- this.text = text;
7575
- /** @property {Color} */
8204
+ /** @property {Color} - Color for the handle part of the scrollbar */
7576
8205
  this.handleColor = handleColor;
8206
+
8207
+ // set properties
8208
+ this.text = text;
7577
8209
  this.color = color;
7578
8210
  this.interactive = true;
7579
8211
  }
7580
8212
  update()
7581
8213
  {
7582
8214
  super.update();
7583
- if (this.mouseIsHeld)
8215
+ if (this.mouseIsHeld && this.interactive)
7584
8216
  {
8217
+ // check if value changed
7585
8218
  const handleSize = vec2(this.size.y);
7586
8219
  const handleWidth = this.size.x - handleSize.x;
7587
8220
  const p1 = this.pos.x - handleWidth/2;
7588
8221
  const p2 = this.pos.x + handleWidth/2;
7589
8222
  const oldValue = this.value;
7590
8223
  this.value = percent(mousePosScreen.x, p1, p2);
7591
- this.value == oldValue || this.onChange();
8224
+ this.value === oldValue || this.onChange();
7592
8225
  }
7593
8226
  }
7594
8227
  render()
7595
8228
  {
7596
- const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
7597
- const color = this.mouseIsOver? this.hoverColor : this.color;
8229
+ // draw the scrollbar background
8230
+ const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
8231
+ this.color : this.lineColor;
8232
+ const color = this.disabled ? this.disabledColor :
8233
+ this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
7598
8234
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7599
8235
 
8236
+ // draw the scrollbar handle
7600
8237
  const handleSize = vec2(this.size.y);
7601
8238
  const handleWidth = this.size.x - handleSize.x;
7602
8239
  const p1 = this.pos.x - handleWidth/2;
7603
8240
  const p2 = this.pos.x + handleWidth/2;
7604
8241
  const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
7605
- const barColor = this.mouseIsHeld ? this.color : this.handleColor;
7606
- uiSystem.drawRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor, this.cornerRadius);
8242
+ const handleColor = this.disabled ? this.disabledColor :
8243
+ this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
8244
+ uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
7607
8245
 
8246
+ // draw the text on the scrollbar
7608
8247
  const textScale = .8; // scale text to fit in scrollbar
7609
8248
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7610
8249
  uiSystem.drawText(this.text, this.pos, textSize,
@@ -7699,14 +8338,14 @@ class Box2dObject extends EngineObject
7699
8338
  if (this.tileInfo)
7700
8339
  super.render();
7701
8340
  else
7702
- this.drawFixtures(this.color, this.lineColor, this.lineWidth, mainContext);
8341
+ this.drawFixtures(this.color, this.lineColor, this.lineWidth);
7703
8342
  }
7704
8343
 
7705
8344
  /** Render debug info */
7706
8345
  renderDebugInfo()
7707
8346
  {
7708
8347
  const isAsleep = !this.getIsAwake();
7709
- const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
8348
+ const isStatic = this.getBodyType() === box2d.bodyTypeStatic;
7710
8349
  const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
7711
8350
  this.drawFixtures(color);
7712
8351
  }
@@ -9174,7 +9813,7 @@ class Box2dPlugin
9174
9813
  queryCallback.ReportFixture = function(fixturePointer)
9175
9814
  {
9176
9815
  const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
9177
- if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
9816
+ if (dynamicOnly && fixture.GetBody().GetType() !== box2d.instance.b2_dynamicBody)
9178
9817
  return true; // continue getting results
9179
9818
  if (!fixture.TestPoint(box2d.vec2dTo(pos)))
9180
9819
  return true; // continue getting results
@@ -9203,7 +9842,7 @@ class Box2dPlugin
9203
9842
  * @param {Color} [lineColor]
9204
9843
  * @param {number} [lineWidth]
9205
9844
  * @param {CanvasRenderingContext2D} [context] */
9206
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context=drawContext)
9845
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
9207
9846
  {
9208
9847
  const shape = box2d.castObjectType(fixture.GetShape());
9209
9848
  switch (shape.GetType())
@@ -9213,20 +9852,20 @@ class Box2dPlugin
9213
9852
  let points = [];
9214
9853
  for (let i=shape.GetVertexCount(); i--;)
9215
9854
  points.push(box2d.vec2From(shape.GetVertex(i)));
9216
- drawPoly(points, color, lineWidth, lineColor, pos, angle, false, false, context);
9855
+ drawPoly(points, color, lineWidth, lineColor, pos, angle);
9217
9856
  break;
9218
9857
  }
9219
9858
  case box2d.instance.b2Shape.e_circle:
9220
9859
  {
9221
9860
  const radius = shape.get_m_radius();
9222
- drawCircle(pos, radius, color, lineWidth, lineColor, false, false, context);
9861
+ drawCircle(pos, radius, color, lineWidth, lineColor);
9223
9862
  break;
9224
9863
  }
9225
9864
  case box2d.instance.b2Shape.e_edge:
9226
9865
  {
9227
9866
  const v1 = box2d.vec2From(shape.get_m_vertex1());
9228
9867
  const v2 = box2d.vec2From(shape.get_m_vertex2());
9229
- drawLine(v1, v2, lineWidth, lineColor, pos, angle, false, false, context);
9868
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle);
9230
9869
  break;
9231
9870
  }
9232
9871
  }
@@ -9305,7 +9944,9 @@ class Box2dPlugin
9305
9944
  }
9306
9945
 
9307
9946
  ///////////////////////////////////////////////////////////////////////////////
9308
- /** Box2d Init - Call with await before starting LittleJS to init box2d
9947
+ /** Box2d Init - Call with await to init box2d
9948
+ * @example
9949
+ * await box2dInit();
9309
9950
  * @return {Promise<Box2dPlugin>}
9310
9951
  * @memberof Box2D */
9311
9952
  async function box2dInit()
@@ -9421,7 +10062,7 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
9421
10062
  }
9422
10063
 
9423
10064
  /** Draw a scalable nine-slice UI element in world space
9424
- * This function can apply color and additive color if webgl is enabled
10065
+ * This function can apply color and additive color if WebGL is enabled
9425
10066
  * @param {Vector2} pos - World space position
9426
10067
  * @param {Vector2} size - World space size
9427
10068
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
@@ -9450,9 +10091,9 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
9450
10091
  {
9451
10092
  // sides
9452
10093
  const horizontal = i%2;
9453
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-1:1));
10094
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
9454
10095
  const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
9455
- const sideTile = centerTile.offset(startTile.size.multiply(vec2(i==1?1:i==3?-1:0,i==0?-flip:i==2?flip:0)))
10096
+ const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
9456
10097
  drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9457
10098
  }
9458
10099
  for (let i=4; i--;)
@@ -9481,7 +10122,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
9481
10122
  }
9482
10123
 
9483
10124
  /** Draw a scalable three-slice UI element in world space
9484
- * This function can apply color and additive color if webgl is enabled
10125
+ * This function can apply color and additive color if WebGL is enabled
9485
10126
  * @param {Vector2} pos - World space position
9486
10127
  * @param {Vector2} size - World space size
9487
10128
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
@@ -9513,7 +10154,7 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
9513
10154
  // sides
9514
10155
  const a = angle + i*PI/2;
9515
10156
  const horizontal = i%2;
9516
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-flip:flip));
10157
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
9517
10158
  const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
9518
10159
  drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
9519
10160
  }
@@ -9755,27 +10396,32 @@ export
9755
10396
  // WebGL
9756
10397
  glCanvas,
9757
10398
  glContext,
10399
+ glClearCanvas,
10400
+ glSetTexture,
9758
10401
  glCompileShader,
9759
- glCopyToContext,
9760
10402
  glCreateProgram,
9761
10403
  glCreateTexture,
9762
10404
  glDeleteTexture,
9763
10405
  glSetTextureData,
9764
- glDraw,
9765
10406
  glFlush,
9766
- glSetTexture,
10407
+ glCopyToContext,
9767
10408
  glSetAntialias,
9768
- glClearCanvas,
10409
+ glDraw,
10410
+ glDrawPointsTransform,
10411
+ glDrawOutlineTransform,
10412
+ glDrawPoints,
9769
10413
  glAntialias,
9770
- glShader,
9771
- glActiveTexture,
9772
- glArrayBuffer,
9773
- glGeometryBuffer,
9774
- glPositionData,
9775
- glColorData,
9776
- glInstanceCount,
9777
- glAdditive,
10414
+ glShader,
10415
+ glPolyShader,
10416
+ glPolyMode,
10417
+ glAdditive,
9778
10418
  glBatchAdditive,
10419
+ glActiveTexture,
10420
+ glArrayBuffer,
10421
+ glGeometryBuffer,
10422
+ glPositionData,
10423
+ glColorData,
10424
+ glBatchCount,
9779
10425
 
9780
10426
  // Input
9781
10427
  keyIsDown,
@@ -9808,17 +10454,18 @@ export
9808
10454
  pointerLockIsActive,
9809
10455
 
9810
10456
  // Audio
10457
+ audioContext,
10458
+ audioMasterGain,
10459
+ audioDefaultSampleRate,
9811
10460
  Sound,
9812
10461
  SoundWave,
9813
- playAudioFile,
10462
+ SoundInstance,
9814
10463
  speak,
9815
10464
  speakStop,
9816
10465
  getNoteFrequency,
9817
10466
  playSamples,
9818
10467
  zzfx,
9819
10468
  zzfxG,
9820
- zzfxR,
9821
- audioContext,
9822
10469
 
9823
10470
  // Base Object
9824
10471
  EngineObject,
@@ -9843,7 +10490,7 @@ export
9843
10490
  medalsPreventUnlock,
9844
10491
  medalsInit,
9845
10492
  Medal,
9846
- };
10493
+ }
9847
10494
  /**
9848
10495
  * LittleJS Module Plugins Export
9849
10496
  */
@@ -9899,4 +10546,4 @@ export
9899
10546
  drawNineSliceScreen,
9900
10547
  drawThreeSlice,
9901
10548
  drawThreeSliceScreen,
9902
- };
10549
+ }