littlejsengine 1.13.4 → 1.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +4 -3
  2. package/dist/littlejs.d.ts +395 -249
  3. package/dist/littlejs.esm.js +1550 -754
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +1528 -744
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +1504 -720
  8. package/examples/box2d/game.js +4 -4
  9. package/examples/box2d/gameObjects.js +1 -1
  10. package/examples/breakout/game.js +30 -25
  11. package/examples/breakoutTutorial/README.md +1 -1
  12. package/examples/breakoutTutorial/game.js +1 -1
  13. package/examples/electron/build.js +1 -1
  14. package/examples/electron/index.html +2 -2
  15. package/examples/empty/game.js +1 -1
  16. package/examples/htmlMenu/index.html +7 -7
  17. package/examples/index.html +208 -97
  18. package/examples/platformer/gameEffects.js +20 -25
  19. package/examples/platformer/gameLevel.js +6 -1
  20. package/examples/shorts/base.html +1 -1
  21. package/examples/shorts/flappyGame.js +2 -1
  22. package/examples/shorts/helloWorld.js +1 -1
  23. package/examples/shorts/music.js +106 -0
  24. package/examples/shorts/parallax.js +71 -0
  25. package/examples/shorts/piano.js +7 -8
  26. package/examples/shorts/postProcess.js +25 -8
  27. package/examples/shorts/shapes.js +12 -18
  28. package/examples/shorts/song.mp3 +0 -0
  29. package/examples/shorts/uiSystem.js +15 -8
  30. package/examples/starter/index.html +2 -2
  31. package/examples/stress/index.html +14 -7
  32. package/examples/style.css +14 -4
  33. package/examples/typescript/build.js +1 -1
  34. package/examples/uiSystem/game.js +11 -11
  35. package/package.json +1 -1
  36. package/plugins/box2d.js +12 -10
  37. package/plugins/drawUtilities.js +5 -5
  38. package/plugins/newgrounds.js +6 -6
  39. package/plugins/pluginExport.js +1 -1
  40. package/plugins/uiSystem.js +103 -79
  41. package/plugins/zzfxm.js +10 -10
  42. package/reference.md +94 -56
  43. package/src/engine.js +28 -24
  44. package/src/engineAudio.js +284 -109
  45. package/src/engineBuild.js +11 -11
  46. package/src/engineDebug.js +29 -29
  47. package/src/engineDraw.js +285 -112
  48. package/src/engineExport.js +28 -16
  49. package/src/engineInput.js +57 -67
  50. package/src/engineMedals.js +25 -25
  51. package/src/engineObject.js +28 -25
  52. package/src/engineParticles.js +59 -59
  53. package/src/engineRelease.js +1 -1
  54. package/src/engineSettings.js +20 -13
  55. package/src/engineTileLayer.js +34 -35
  56. package/src/engineUtilities.js +66 -59
  57. package/src/engineWebGL.js +466 -66
  58. /package/examples/shorts/{playSound.js → sound.js} +0 -0
package/dist/littlejs.js CHANGED
@@ -3,10 +3,10 @@
3
3
 
4
4
  'use strict';
5
5
 
6
- /**
6
+ /**
7
7
  * LittleJS - The Tiny Fast JavaScript Game Engine
8
8
  * MIT License - Copyright 2021 Frank Force
9
- *
9
+ *
10
10
  * Engine Features
11
11
  * - Object oriented system with base class engine object
12
12
  * - Base class object handles update, physics, collision, rendering, etc
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.13.4';
36
+ const engineVersion = '1.14.4';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -123,7 +123,7 @@ function engineAddPlugin(updateFunction, renderFunction)
123
123
  * // Basic engine startup
124
124
  * engineInit(
125
125
  * () => { console.log('Game initialized!'); }, // gameInit
126
- * () => { updatePlayer(); }, // gameUpdate
126
+ * () => { updateGameLogic(); }, // gameUpdate
127
127
  * () => { updateUI(); }, // gameUpdatePost
128
128
  * () => { drawBackground(); }, // gameRender
129
129
  * () => { drawHUD(); }, // gameRenderPost
@@ -149,7 +149,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
149
149
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
150
150
 
151
151
  // disable smoothing for pixel art
152
- overlayContext.imageSmoothingEnabled =
152
+ overlayContext.imageSmoothingEnabled =
153
153
  mainContext.imageSmoothingEnabled = !tilesPixelated;
154
154
 
155
155
  // setup gl rendering if enabled
@@ -197,7 +197,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
197
197
  deltaSmooth = frameTimeBufferMS;
198
198
  frameTimeBufferMS = 0;
199
199
  }
200
-
200
+
201
201
  // update multiple frames if necessary in case of slow framerate
202
202
  for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
203
203
  {
@@ -242,15 +242,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
242
242
  overlayContext.textBaseline = 'top';
243
243
  overlayContext.font = '1em monospace';
244
244
  overlayContext.fillStyle = '#000';
245
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
245
+ const text = engineName + ' ' + 'v' + engineVersion + ' / '
246
246
  + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
247
247
  + (glEnable ? ' GL' : ' 2D') ;
248
248
  overlayContext.fillText(text, mainCanvas.width-3, 3);
249
249
  overlayContext.fillStyle = '#fff';
250
250
  overlayContext.fillText(text, mainCanvas.width-2, 2);
251
251
  }
252
- if (debug || showWatermark)
253
- drawCount = 0;
252
+ drawCount = 0;
254
253
  }
255
254
  requestAnimationFrame(engineUpdate);
256
255
  }
@@ -258,13 +257,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
258
257
  function updateCanvas()
259
258
  {
260
259
  if (headlessMode) return;
261
-
260
+
262
261
  if (canvasFixedSize.x)
263
262
  {
264
263
  // clear canvas and set fixed size
265
264
  mainCanvas.width = canvasFixedSize.x;
266
265
  mainCanvas.height = canvasFixedSize.y;
267
-
266
+
268
267
  // fit to window by adding space on top or bottom if necessary
269
268
  const aspect = innerWidth / innerHeight;
270
269
  const fixedAspect = mainCanvas.width / mainCanvas.height;
@@ -274,16 +273,21 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
274
273
  else
275
274
  {
276
275
  // clear canvas and set size to same as window
277
- mainCanvas.width = min(innerWidth, canvasMaxSize.x);
276
+ mainCanvas.width = min(innerWidth, canvasMaxSize.x);
278
277
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
279
278
  }
280
-
279
+
281
280
  // clear overlay canvas and set size
282
281
  overlayCanvas.width = mainCanvas.width;
283
282
  overlayCanvas.height = mainCanvas.height;
284
283
 
285
284
  // save canvas size
286
285
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
286
+
287
+ // set default line join and cap
288
+ const lineJoin = 'round', lineCap = 'round';
289
+ mainContext.lineJoin = overlayContext.lineJoin = lineJoin;
290
+ mainContext.lineCap = overlayContext.lineCap = lineCap;
287
291
  }
288
292
 
289
293
  // wait for gameInit to load
@@ -296,15 +300,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
296
300
  return startEngine();
297
301
 
298
302
  // setup html
299
- const styleRoot =
303
+ const styleRoot =
300
304
  'margin:0;' + // fill the window
301
305
  'overflow:hidden;' + // no scroll bars
302
306
  'background:#000;' + // set background color
303
307
  'user-select:none;' + // prevent hold to select
304
308
  '-webkit-user-select:none;' + // compatibility for ios
305
- (!touchInputEnable ? '' : // no touch css settings
306
309
  'touch-action:none;' + // prevent mobile pinch to resize
307
- '-webkit-touch-callout:none');// compatibility for ios
310
+ '-webkit-touch-callout:none';// compatibility for ios
308
311
  rootElement.style.cssText = styleRoot;
309
312
  drawCanvas = mainCanvas = document.createElement('canvas');
310
313
  rootElement.appendChild(mainCanvas);
@@ -321,7 +324,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
321
324
  rootElement.appendChild(overlayCanvas);
322
325
  overlayContext = overlayCanvas.getContext('2d');
323
326
 
324
- // set canvas style
327
+ // set canvases
325
328
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
326
329
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
327
330
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
@@ -330,17 +333,18 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
330
333
  setCanvasPixelated(canvasPixelated);
331
334
  setOverlayCanvasPixelated(overlayCanvasPixelated);
332
335
  updateCanvas();
336
+ glPreRender();
333
337
 
334
338
  // create offscreen canvas for image processing
335
339
  workCanvas = new OffscreenCanvas(256, 256);
336
340
  workContext = workCanvas.getContext('2d', { willReadFrequently: true });
337
-
341
+
338
342
  // create promises for loading images
339
343
  const promises = imageSources.map((src, textureIndex)=>
340
- new Promise(resolve =>
344
+ new Promise(resolve =>
341
345
  {
342
346
  const image = new Image;
343
- image.onerror = image.onload = ()=>
347
+ image.onerror = image.onload = ()=>
344
348
  {
345
349
  const textureInfo = new TextureInfo(image);
346
350
  textureInfo.createWebGLTexture();
@@ -355,7 +359,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
355
359
  if (!imageSources.length)
356
360
  {
357
361
  // no images to load
358
- promises.push(new Promise(resolve =>
362
+ promises.push(new Promise(resolve =>
359
363
  {
360
364
  const textureInfo = new TextureInfo(new Image);
361
365
  textureInfos[0] = textureInfo;
@@ -367,7 +371,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
367
371
  if (showSplashScreen)
368
372
  {
369
373
  // draw splash screen
370
- promises.push(new Promise(resolve =>
374
+ promises.push(new Promise(resolve =>
371
375
  {
372
376
  let t = 0;
373
377
  console.log(`${engineName} Engine v${engineVersion}`);
@@ -599,7 +603,7 @@ function drawEngineSplashScreen(t)
599
603
  line(36,20,60,20);
600
604
 
601
605
  // engine front light
602
- circle(60,30,4,PI,3*PI,color(3,2));
606
+ circle(60,30,4,PI,3*PI,color(3,2));
603
607
  circle(60,30,4,PI,2*PI,color(3,3));
604
608
  circle(60,30,4,PI,3*PI);
605
609
 
@@ -648,10 +652,10 @@ function drawEngineSplashScreen(t)
648
652
  x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
649
653
  X += w;
650
654
  }
651
-
655
+
652
656
  x.restore();
653
657
  }
654
- /**
658
+ /**
655
659
  * LittleJS Debug System
656
660
  * - Press Esc to show debug overlay with mouse pick
657
661
  * - Number keys toggle debug functions
@@ -704,10 +708,10 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
704
708
  // Debug helper functions
705
709
 
706
710
  /** Asserts if the expression is false, does not do anything in release builds
707
- * @param {boolean} assert
711
+ * @param {boolean} assert
708
712
  * @param {...Object} [output] - error message output
709
713
  * @memberof Debug */
710
- function ASSERT(assert, ...output)
714
+ function ASSERT(assert, ...output)
711
715
  {
712
716
  if (enableAsserts)
713
717
  console.assert(assert, ...output);
@@ -723,9 +727,9 @@ function ASSERT(assert, ...output)
723
727
  * @memberof Debug */
724
728
  function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
725
729
  {
726
- if (typeof size == 'number')
730
+ if (typeof size === 'number')
727
731
  size = vec2(size); // allow passing in floats
728
- ASSERT(typeof color == 'string', 'pass in css color strings');
732
+ ASSERT(typeof color === 'string', 'pass in css color strings');
729
733
  debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
730
734
  }
731
735
 
@@ -739,7 +743,7 @@ function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
739
743
  * @memberof Debug */
740
744
  function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
741
745
  {
742
- ASSERT(typeof color == 'string', 'pass in css color strings');
746
+ ASSERT(typeof color === 'string', 'pass in css color strings');
743
747
  debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
744
748
  }
745
749
 
@@ -752,7 +756,7 @@ function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
752
756
  * @memberof Debug */
753
757
  function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
754
758
  {
755
- ASSERT(typeof color == 'string', 'pass in css color strings');
759
+ ASSERT(typeof color === 'string', 'pass in css color strings');
756
760
  debugPrimitives.push({pos, size:radius, color, time:new Timer(time), angle:0, fill});
757
761
  }
758
762
 
@@ -764,7 +768,7 @@ function debugCircle(pos, radius=0, color='#fff', time=0, fill=false)
764
768
  * @memberof Debug */
765
769
  function debugPoint(pos, color, time, angle)
766
770
  {
767
- ASSERT(typeof color == 'string', 'pass in css color strings');
771
+ ASSERT(typeof color === 'string', 'pass in css color strings');
768
772
  debugRect(pos, undefined, color, time, angle);
769
773
  }
770
774
 
@@ -772,13 +776,13 @@ function debugPoint(pos, color, time, angle)
772
776
  * @param {Vector2} posA
773
777
  * @param {Vector2} posB
774
778
  * @param {string} [color]
775
- * @param {number} [thickness]
779
+ * @param {number} [width]
776
780
  * @param {number} [time]
777
781
  * @memberof Debug */
778
- function debugLine(posA, posB, color, thickness=.1, time)
782
+ function debugLine(posA, posB, color, width=.1, time)
779
783
  {
780
784
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
781
- const size = vec2(thickness, halfDelta.length()*2);
785
+ const size = vec2(width, halfDelta.length()*2);
782
786
  debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
783
787
  }
784
788
 
@@ -792,11 +796,11 @@ function debugLine(posA, posB, color, thickness=.1, time)
792
796
  function debugOverlap(posA, sizeA, posB, sizeB, color)
793
797
  {
794
798
  const minPos = vec2(
795
- min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
799
+ min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
796
800
  min(posA.y - sizeA.y/2, posB.y - sizeB.y/2)
797
801
  );
798
802
  const maxPos = vec2(
799
- max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
803
+ max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
800
804
  max(posA.y + sizeA.y/2, posB.y + sizeB.y/2)
801
805
  );
802
806
  debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color);
@@ -813,7 +817,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
813
817
  * @memberof Debug */
814
818
  function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
815
819
  {
816
- ASSERT(typeof color == 'string', 'pass in css color strings');
820
+ ASSERT(typeof color === 'string', 'pass in css color strings');
817
821
  debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
818
822
  }
819
823
 
@@ -825,7 +829,7 @@ function debugClear() { debugPrimitives = []; }
825
829
  * @memberof Debug */
826
830
  function debugScreenshot() { debugTakeScreenshot = 1; }
827
831
 
828
- /** Save a canvas to disk
832
+ /** Save a canvas to disk
829
833
  * @param {HTMLCanvasElement|OffscreenCanvas} canvas
830
834
  * @param {string} [filename]
831
835
  * @param {string} [type]
@@ -846,7 +850,7 @@ function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
846
850
  debugSaveDataURL(canvas.toDataURL(type), filename);
847
851
  }
848
852
 
849
- /** Save a text file to disk
853
+ /** Save a text file to disk
850
854
  * @param {string} text
851
855
  * @param {string} [filename]
852
856
  * @param {string} [type]
@@ -854,7 +858,7 @@ function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
854
858
  function debugSaveText(text, filename='text', type='text/plain')
855
859
  { debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
856
860
 
857
- /** Save a data url to disk
861
+ /** Save a data url to disk
858
862
  * @param {string} dataURL
859
863
  * @param {string} filename
860
864
  * @memberof Debug */
@@ -973,7 +977,7 @@ function debugRender()
973
977
  {
974
978
  const saveContext = mainContext;
975
979
  mainContext = overlayContext;
976
-
980
+
977
981
  // draw red rectangle around screen
978
982
  const cameraSize = getCameraSize();
979
983
  debugRect(cameraPos, cameraSize.subtract(vec2(.1)), '#f008');
@@ -1023,14 +1027,14 @@ function debugRender()
1023
1027
  overlayContext.scale(1, p.text ? 1 : -1);
1024
1028
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
1025
1029
 
1026
- if (p.text != undefined)
1030
+ if (p.text !== undefined)
1027
1031
  {
1028
1032
  overlayContext.font = p.size*cameraScale + 'px '+ p.font;
1029
1033
  overlayContext.textAlign = 'center';
1030
1034
  overlayContext.textBaseline = 'middle';
1031
1035
  overlayContext.fillText(p.text, 0, 0);
1032
1036
  }
1033
- else if (p.points != undefined)
1037
+ else if (p.points !== undefined)
1034
1038
  {
1035
1039
  // poly
1036
1040
  overlayContext.beginPath();
@@ -1043,13 +1047,13 @@ function debugRender()
1043
1047
  p.fill && overlayContext.fill();
1044
1048
  overlayContext.stroke();
1045
1049
  }
1046
- else if (p.size == 0 || p.size.x === 0 && p.size.y === 0)
1050
+ else if (p.size === 0 || p.size.x === 0 && p.size.y === 0)
1047
1051
  {
1048
1052
  // point
1049
1053
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
1050
1054
  overlayContext.fillRect(-1, -pointSize/2, 3, pointSize);
1051
1055
  }
1052
- else if (p.size.x != undefined)
1056
+ else if (p.size.x !== undefined)
1053
1057
  {
1054
1058
  // rect
1055
1059
  const s = p.size.scale(cameraScale).floor();
@@ -1065,14 +1069,14 @@ function debugRender()
1065
1069
  p.fill && overlayContext.fill();
1066
1070
  overlayContext.stroke();
1067
1071
  }
1068
-
1072
+
1069
1073
  overlayContext.restore();
1070
1074
  });
1071
1075
 
1072
1076
  // remove expired primitives
1073
1077
  debugPrimitives = debugPrimitives.filter(r=>r.time<0);
1074
1078
  }
1075
-
1079
+
1076
1080
  if (debugObject)
1077
1081
  {
1078
1082
  const saveContext = mainContext;
@@ -1081,8 +1085,8 @@ function debugRender()
1081
1085
  raycastHitPos && drawRect(raycastHitPos.floor().add(vec2(.5)), vec2(1), rgb(0,1,1,.3));
1082
1086
  drawLine(mousePos, debugObject.pos, .1, raycastHitPos ? rgb(1,0,0,.5) : rgb(0,1,0,.5));
1083
1087
 
1084
- const debugText = 'mouse pos = ' + mousePos +
1085
- '\nmouse collision = ' + tileCollisionGetData(mousePos) +
1088
+ const debugText = 'mouse pos = ' + mousePos +
1089
+ '\nmouse collision = ' + tileCollisionGetData(mousePos) +
1086
1090
  '\n\n--- object info ---\n' +
1087
1091
  debugObject.toString();
1088
1092
  drawTextScreen(debugText, mousePosScreen, 24, rgb(), .05, undefined, 'center', 'monospace');
@@ -1104,7 +1108,7 @@ function debugRender()
1104
1108
  let x = 9, y = 0, h = lineHeight;
1105
1109
  if (debugOverlay)
1106
1110
  {
1107
- overlayContext.fillText(engineName, x, y += h/2 );
1111
+ overlayContext.fillText(`${engineName} v${engineVersion}`, x, y += h/2 );
1108
1112
  overlayContext.fillText('Time: ' + formatTime(time), x, y += h);
1109
1113
  overlayContext.fillText('FPS: ' + averageFPS.toFixed(1), x, y += h);
1110
1114
  overlayContext.fillText('Objects: ' + engineObjects.length, x, y += h);
@@ -1148,7 +1152,7 @@ function debugRender()
1148
1152
  overlayContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
1149
1153
  overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
1150
1154
  }
1151
-
1155
+
1152
1156
  overlayContext.restore();
1153
1157
  }
1154
1158
  }
@@ -1236,7 +1240,7 @@ function debugVideoCaptureUpdate()
1236
1240
  {
1237
1241
  if (!debugVideoCaptureIsActive())
1238
1242
  return; // not recording
1239
-
1243
+
1240
1244
  // save the video frame
1241
1245
  combineCanvases();
1242
1246
  debugVideoCaptureTrack.requestFrame();
@@ -1317,7 +1321,19 @@ function lerp(valueA, valueB, percent)
1317
1321
  if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
1318
1322
  console.warn('lerp() parameter order changed! use lerp(start, end, p)');
1319
1323
  return valueA + clamp(percent) * (valueB-valueA);
1320
- }
1324
+ }
1325
+
1326
+ /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
1327
+ * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
1328
+ * @param {number} value
1329
+ * @param {number} percentA
1330
+ * @param {number} percentB
1331
+ * @param {number} lerpA
1332
+ * @param {number} lerpB
1333
+ * @return {number}
1334
+ * @memberof Utilities */
1335
+ function percentLerp(value, percentA, percentB, lerpA, lerpB)
1336
+ { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
1321
1337
 
1322
1338
  /** Returns signed wrapped distance between the two values passed in
1323
1339
  * @param {number} valueA
@@ -1369,7 +1385,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
1369
1385
  * @memberof Utilities */
1370
1386
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
1371
1387
 
1372
- /** Returns the nearest power of two not less then the value
1388
+ /** Returns the nearest power of two not less than the value
1373
1389
  * @param {number} value
1374
1390
  * @return {number}
1375
1391
  * @memberof Utilities */
@@ -1384,8 +1400,8 @@ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
1384
1400
  * @return {boolean} - True if overlapping
1385
1401
  * @memberof Utilities */
1386
1402
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
1387
- {
1388
- return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
1403
+ {
1404
+ return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
1389
1405
  && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
1390
1406
  }
1391
1407
 
@@ -1440,7 +1456,7 @@ function isIntersecting(start, end, pos, size)
1440
1456
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1441
1457
  { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
1442
1458
 
1443
- /** Formats seconds to mm:ss style for display purposes
1459
+ /** Formats seconds to mm:ss style for display purposes
1444
1460
  * @param {number} t - time in seconds
1445
1461
  * @return {string}
1446
1462
  * @memberof Utilities */
@@ -1456,13 +1472,12 @@ async function fetchJSON(url)
1456
1472
  return response.json();
1457
1473
  }
1458
1474
 
1459
- /**
1475
+ /**
1460
1476
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
1461
1477
  * @param {any} n
1462
1478
  * @return {boolean}
1463
- * @memberof Utilities
1464
- */
1465
- function isNumber(n) { return typeof n == 'number' && !isNaN(n); }
1479
+ * @memberof Utilities */
1480
+ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1466
1481
 
1467
1482
  ///////////////////////////////////////////////////////////////////////////////
1468
1483
 
@@ -1517,13 +1532,13 @@ function randInCircle(radius=1, minRadius=0)
1517
1532
  * @memberof Random */
1518
1533
  function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1519
1534
  {
1520
- return linear ? colorA.lerp(colorB, rand()) :
1535
+ return linear ? colorA.lerp(colorB, rand()) :
1521
1536
  new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
1522
1537
  }
1523
1538
 
1524
1539
  ///////////////////////////////////////////////////////////////////////////////
1525
1540
 
1526
- /**
1541
+ /**
1527
1542
  * Seeded random number generator
1528
1543
  * - Can be used to create a deterministic random number sequence
1529
1544
  * @example
@@ -1550,8 +1565,8 @@ class RandomGenerator
1550
1565
  float(valueA=1, valueB=0)
1551
1566
  {
1552
1567
  // xorshift algorithm
1553
- this.seed ^= this.seed << 13;
1554
- this.seed ^= this.seed >>> 17;
1568
+ this.seed ^= this.seed << 13;
1569
+ this.seed ^= this.seed >>> 17;
1555
1570
  this.seed ^= this.seed << 5;
1556
1571
  return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
1557
1572
  }
@@ -1600,28 +1615,26 @@ class RandomGenerator
1600
1615
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
1601
1616
  * a = vec2(5); // set a to (5, 5)
1602
1617
  * b = vec2(); // set b to (0, 0)
1603
- * @memberof Utilities
1604
- */
1618
+ * @memberof Utilities */
1605
1619
  function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1606
1620
 
1607
- /**
1621
+ /**
1608
1622
  * Check if object is a valid Vector2
1609
1623
  * @param {any} v
1610
1624
  * @return {boolean}
1611
- * @memberof Utilities
1612
- */
1613
- function isVector2(v) { return v instanceof Vector2; }
1625
+ * @memberof Utilities */
1626
+ function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
1614
1627
 
1615
1628
  // vector2 asserts
1616
- function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v) && v.isValid(), 'Vector2 is invalid.', v); }
1629
+ function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), 'Vector2 is invalid.', v); }
1617
1630
  function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n); }
1618
1631
  function ASSERT_VECTOR2_NORMAL(v)
1619
1632
  {
1620
1633
  ASSERT_VECTOR2_VALID(v);
1621
- ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1634
+ ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1622
1635
  }
1623
1636
 
1624
- /**
1637
+ /**
1625
1638
  * 2D Vector object with vector math library
1626
1639
  * - Functions do not change this so they can be chained together
1627
1640
  * @example
@@ -1745,7 +1758,7 @@ class Vector2
1745
1758
  * @param {number} [angle]
1746
1759
  * @param {number} [length]
1747
1760
  * @return {Vector2} */
1748
- setAngle(angle=0, length=1)
1761
+ setAngle(angle=0, length=1)
1749
1762
  {
1750
1763
  ASSERT_NUMBER_VALID(angle);
1751
1764
  ASSERT_NUMBER_VALID(length);
@@ -1760,7 +1773,7 @@ class Vector2
1760
1773
  rotate(angle)
1761
1774
  {
1762
1775
  ASSERT_NUMBER_VALID(angle);
1763
- const c = Math.cos(-angle), s = Math.sin(-angle);
1776
+ const c = Math.cos(-angle), s = Math.sin(-angle);
1764
1777
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1765
1778
  }
1766
1779
 
@@ -1772,9 +1785,9 @@ class Vector2
1772
1785
  ASSERT_NUMBER_VALID(direction);
1773
1786
  ASSERT_NUMBER_VALID(length);
1774
1787
  direction = mod(direction, 4);
1775
- ASSERT(direction==0 || direction==1 || direction==2 || direction==3,
1788
+ ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1776
1789
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1777
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1790
+ return vec2(direction%2 ? direction-1 ? -length : length : 0,
1778
1791
  direction%2 ? 0 : direction ? -length : length);
1779
1792
  }
1780
1793
 
@@ -1826,7 +1839,7 @@ class Vector2
1826
1839
  /** Returns this vector expressed as a string
1827
1840
  * @param {number} digits - precision to display
1828
1841
  * @return {string} */
1829
- toString(digits=3)
1842
+ toString(digits=3)
1830
1843
  {
1831
1844
  ASSERT_NUMBER_VALID(digits);
1832
1845
  if (debug)
@@ -1845,7 +1858,7 @@ class Vector2
1845
1858
 
1846
1859
  ///////////////////////////////////////////////////////////////////////////////
1847
1860
 
1848
- /**
1861
+ /**
1849
1862
  * Create a color object with RGBA values, white by default
1850
1863
  * @param {number} [r=1] - red
1851
1864
  * @param {number} [g=1] - green
@@ -1856,29 +1869,27 @@ class Vector2
1856
1869
  */
1857
1870
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1858
1871
 
1859
- /**
1872
+ /**
1860
1873
  * Create a color object with HSLA values, white by default
1861
1874
  * @param {number} [h=0] - hue
1862
1875
  * @param {number} [s=0] - saturation
1863
1876
  * @param {number} [l=1] - lightness
1864
1877
  * @param {number} [a=1] - alpha
1865
1878
  * @return {Color}
1866
- * @memberof Utilities
1867
- */
1879
+ * @memberof Utilities */
1868
1880
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
1869
1881
 
1870
- /**
1882
+ /**
1871
1883
  * Check if object is a valid Color
1872
1884
  * @param {any} c
1873
1885
  * @return {boolean}
1874
- * @memberof Utilities
1875
- */
1876
- function isColor(c) { return c instanceof Color; }
1886
+ * @memberof Utilities */
1887
+ function isColor(c) { return c instanceof Color && c.isValid(); }
1877
1888
 
1878
1889
  // color asserts
1879
- function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c) && c.isValid(), 'Color is invalid.', c); }
1890
+ function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
1880
1891
 
1881
- /**
1892
+ /**
1882
1893
  * Color object (red, green, blue, alpha) with some helpful functions
1883
1894
  * @example
1884
1895
  * let a = new Color; // white
@@ -1950,7 +1961,7 @@ class Color
1950
1961
  * @param {number} scale
1951
1962
  * @param {number} [alphaScale=scale]
1952
1963
  * @return {Color} */
1953
- scale(scale, alphaScale=scale)
1964
+ scale(scale, alphaScale=scale)
1954
1965
  { return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
1955
1966
 
1956
1967
  /** Returns a copy of this color clamped to the valid range between 0 and 1
@@ -1967,9 +1978,9 @@ class Color
1967
1978
  ASSERT_NUMBER_VALID(percent);
1968
1979
  const p = clamp(percent);
1969
1980
  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),
1981
+ c.r*p + this.r*(1-p),
1982
+ c.g*p + this.g*(1-p),
1983
+ c.b*p + this.b*(1-p),
1973
1984
  c.a*p + this.a*(1-p));
1974
1985
  }
1975
1986
 
@@ -2009,15 +2020,15 @@ class Color
2009
2020
  const minC = min(r, g, b);
2010
2021
  const l = (maxC + minC) / 2;
2011
2022
  let h = 0, s = 0;
2012
- if (maxC != minC)
2023
+ if (maxC !== minC)
2013
2024
  {
2014
2025
  let d = maxC - minC;
2015
2026
  s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
2016
- if (r == maxC)
2027
+ if (r === maxC)
2017
2028
  h = (g - b) / d + (g < b ? 6 : 0);
2018
- else if (g == maxC)
2029
+ else if (g === maxC)
2019
2030
  h = (b - r) / d + 2;
2020
- else if (b == maxC)
2031
+ else if (b === maxC)
2021
2032
  h = (r - g) / d + 4;
2022
2033
  }
2023
2034
  return [h / 6, s, l, a];
@@ -2027,7 +2038,7 @@ class Color
2027
2038
  * @param {number} [amount]
2028
2039
  * @param {number} [alphaAmount]
2029
2040
  * @return {Color} */
2030
- mutate(amount=.05, alphaAmount=0)
2041
+ mutate(amount=.05, alphaAmount=0)
2031
2042
  {
2032
2043
  ASSERT_NUMBER_VALID(amount);
2033
2044
  ASSERT_NUMBER_VALID(alphaAmount);
@@ -2043,47 +2054,47 @@ class Color
2043
2054
  /** Returns this color expressed as a hex color code
2044
2055
  * @param {boolean} [useAlpha] - if alpha should be included in result
2045
2056
  * @return {string} */
2046
- toString(useAlpha = true)
2057
+ toString(useAlpha = true)
2047
2058
  {
2048
- ASSERT(typeof useAlpha == 'boolean', 'Use alpha boolean is invalid.', useAlpha);
2059
+ ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
2049
2060
  if (debug && !this.isValid())
2050
2061
  return `#000`;
2051
2062
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
2052
2063
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
2053
2064
  }
2054
-
2065
+
2055
2066
  /** Set this color from a hex code
2056
2067
  * @param {string} hex - html hex code
2057
2068
  * @return {Color} */
2058
2069
  setHex(hex)
2059
2070
  {
2060
- ASSERT(typeof hex == 'string' && hex[0] == '#', 'Color hex code must be a string starting with #');
2071
+ ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
2061
2072
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
2062
2073
 
2063
2074
  if (hex.length < 6)
2064
2075
  {
2065
2076
  const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
2066
2077
  this.r = fromHex(1);
2067
- this.g = fromHex(2),
2078
+ this.g = fromHex(2);
2068
2079
  this.b = fromHex(3);
2069
- this.a = hex.length == 5 ? fromHex(4) : 1;
2080
+ this.a = hex.length === 5 ? fromHex(4) : 1;
2070
2081
  }
2071
2082
  else
2072
2083
  {
2073
2084
  const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
2074
2085
  this.r = fromHex(1);
2075
- this.g = fromHex(3),
2086
+ this.g = fromHex(3);
2076
2087
  this.b = fromHex(5);
2077
- this.a = hex.length == 9 ? fromHex(7) : 1;
2088
+ this.a = hex.length === 9 ? fromHex(7) : 1;
2078
2089
  }
2079
2090
 
2080
2091
  ASSERT_COLOR_VALID(this);
2081
2092
  return this;
2082
2093
  }
2083
-
2094
+
2084
2095
  /** Returns this color expressed as 32 bit RGBA value
2085
2096
  * @return {number} */
2086
- rgbaInt()
2097
+ rgbaInt()
2087
2098
  {
2088
2099
  const r = clamp(this.r)*255|0;
2089
2100
  const g = clamp(this.g)*255<<8;
@@ -2104,7 +2115,7 @@ class Color
2104
2115
  /** Color - White #ffffff
2105
2116
  * @type {Color}
2106
2117
  * @memberof Utilities */
2107
- const WHITE = rgb();
2118
+ const WHITE = rgb();
2108
2119
 
2109
2120
  /** Color - Clear White #ffffff with 0 alpha
2110
2121
  * @type {Color}
@@ -2219,11 +2230,11 @@ class Timer
2219
2230
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
2220
2231
  * @return {number} */
2221
2232
  getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
2222
-
2233
+
2223
2234
  /** Returns this timer expressed as a string
2224
2235
  * @return {string} */
2225
2236
  toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
2226
-
2237
+
2227
2238
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2228
2239
  * @return {number} */
2229
2240
  valueOf() { return this.get(); }
@@ -2259,7 +2270,7 @@ let cameraScale = 32;
2259
2270
  // Display settings
2260
2271
 
2261
2272
  /** Enable applying color to tiles when using canvas2d
2262
- * - This is slower but should be the same as webgl rendering
2273
+ * - This is slower but should be the same as WebGL rendering
2263
2274
  * @type {boolean}
2264
2275
  * @default
2265
2276
  * @memberof Settings */
@@ -2305,14 +2316,13 @@ let tilesPixelated = true;
2305
2316
  * @memberof Settings */
2306
2317
  let fontDefault = 'arial';
2307
2318
 
2308
- /** Enable to show the LittleJS splash screen be shown on startup
2319
+ /** Enable to show the LittleJS splash screen on startup
2309
2320
  * @type {boolean}
2310
2321
  * @default
2311
2322
  * @memberof Settings */
2312
2323
  let showSplashScreen = false;
2313
2324
 
2314
2325
  /** Disables all rendering, audio, and input for servers
2315
- * - Must be set before startup to take effect
2316
2326
  * @type {boolean}
2317
2327
  * @default
2318
2328
  * @memberof Settings */
@@ -2321,13 +2331,18 @@ let headlessMode = false;
2321
2331
  ///////////////////////////////////////////////////////////////////////////////
2322
2332
  // WebGL settings
2323
2333
 
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
2334
+ /** Enable WebGL accelerated rendering
2326
2335
  * @type {boolean}
2327
2336
  * @default
2328
2337
  * @memberof Settings */
2329
2338
  let glEnable = true;
2330
2339
 
2340
+ /** How many sided poly to use when drawing circles and ellipses with WebGL
2341
+ * @type {number}
2342
+ * @default
2343
+ * @memberof Settings */
2344
+ let glCircleSides = 32;
2345
+
2331
2346
  ///////////////////////////////////////////////////////////////////////////////
2332
2347
  // Tile sheet settings
2333
2348
 
@@ -2409,13 +2424,13 @@ let particleEmitRateScale = 1;
2409
2424
  * @memberof Settings */
2410
2425
  let gamepadsEnable = true;
2411
2426
 
2412
- /** If true, the dpad input is also routed to the left analog stick (for better accessability)
2427
+ /** If true, the dpad input is also routed to the left analog stick (for better accessibility)
2413
2428
  * @type {boolean}
2414
2429
  * @default
2415
2430
  * @memberof Settings */
2416
2431
  let gamepadDirectionEmulateStick = true;
2417
2432
 
2418
- /** If true the WASD keys are also routed to the direction keys (for better accessability)
2433
+ /** If true the WASD keys are also routed to the direction keys (for better accessibility)
2419
2434
  * @type {boolean}
2420
2435
  * @default
2421
2436
  * @memberof Settings */
@@ -2423,7 +2438,6 @@ let inputWASDEmulateDirection = true;
2423
2438
 
2424
2439
  /** True if touch input is enabled for mobile devices
2425
2440
  * - Touch events will be routed to mouse events
2426
- * - Must be set before startup to take effect
2427
2441
  * @type {boolean}
2428
2442
  * @default
2429
2443
  * @memberof Settings */
@@ -2431,7 +2445,6 @@ let touchInputEnable = true;
2431
2445
 
2432
2446
  /** True if touch gamepad should appear on mobile devices
2433
2447
  * - Supports left analog stick, 4 face buttons and start button (button 9)
2434
- * - Must be set before startup to take effect
2435
2448
  * @type {boolean}
2436
2449
  * @default
2437
2450
  * @memberof Settings */
@@ -2534,7 +2547,7 @@ function setCameraAngle(angle) { cameraAngle = angle; }
2534
2547
  function setCameraScale(scale) { cameraScale = scale; }
2535
2548
 
2536
2549
  /** Set if tiles should be colorized when using canvas2d
2537
- * This can be slower but results should look nearly identical to webgl rendering
2550
+ * This can be slower but results should look nearly identical to WebGL rendering
2538
2551
  * It can be enabled/disabled at any time
2539
2552
  * Optimized for performance, and will use faster method if color is white or untextured
2540
2553
  * @param {boolean} colorTiles
@@ -2570,8 +2583,8 @@ function setCanvasPixelated(pixelated)
2570
2583
  * @param {boolean} pixelated
2571
2584
  * @memberof Settings */
2572
2585
  function setOverlayCanvasPixelated(pixelated)
2573
- {
2574
- overlayCanvasPixelated = pixelated;
2586
+ {
2587
+ overlayCanvasPixelated = pixelated;
2575
2588
  if (overlayCanvas)
2576
2589
  overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2577
2590
  }
@@ -2586,7 +2599,7 @@ function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
2586
2599
  * @memberof Settings */
2587
2600
  function setFontDefault(font) { fontDefault = font; }
2588
2601
 
2589
- /** Set if the LittleJS splash screen be shown on startup
2602
+ /** Set if the LittleJS splash screen should be shown on startup
2590
2603
  * @param {boolean} show
2591
2604
  * @memberof Settings */
2592
2605
  function setShowSplashScreen(show) { showSplashScreen = show; }
@@ -2596,16 +2609,21 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
2596
2609
  * @memberof Settings */
2597
2610
  function setHeadlessMode(headless) { headlessMode = headless; }
2598
2611
 
2599
- /** Set if webgl rendering is enabled
2612
+ /** Set if WebGL rendering is enabled
2600
2613
  * @param {boolean} enable
2601
2614
  * @memberof Settings */
2602
2615
  function setGLEnable(enable)
2603
2616
  {
2604
2617
  glEnable = enable;
2605
- if (glCanvas) // hide glCanvas if webgl is disabled
2618
+ if (glCanvas) // hide glCanvas if WebGL is disabled
2606
2619
  glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2607
2620
  }
2608
2621
 
2622
+ /** Set how many sided polygons to use when drawing circles and elipses with WebGL
2623
+ * @param {number} sides
2624
+ * @memberof Settings */
2625
+ function setGLCircleSides(sides) { glCircleSides = sides; }
2626
+
2609
2627
  /** Set default size of tiles in pixels
2610
2628
  * @param {Vector2} size
2611
2629
  * @memberof Settings */
@@ -2636,7 +2654,7 @@ function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
2636
2654
  * @memberof Settings */
2637
2655
  function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
2638
2656
 
2639
- /** Set how much to bounce when a collision occur
2657
+ /** Set how much to bounce when a collision occurs
2640
2658
  * @param {number} restitution
2641
2659
  * @memberof Settings */
2642
2660
  function setObjectDefaultRestitution(restitution) { objectDefaultRestitution = restitution; }
@@ -2760,11 +2778,11 @@ function setShowWatermark(show) { showWatermark = show; }
2760
2778
  * @param {string} key
2761
2779
  * @memberof Debug */
2762
2780
  function setDebugKey(key) { debugKey = key; }
2763
- /**
2781
+ /**
2764
2782
  * LittleJS Object System
2765
2783
  */
2766
2784
 
2767
- /**
2785
+ /**
2768
2786
  * LittleJS Object Base Object Class
2769
2787
  * - Top level object class used by the engine
2770
2788
  * - Automatically adds self to object list
@@ -2787,7 +2805,7 @@ function setDebugKey(key) { debugKey = key; }
2787
2805
  * @example
2788
2806
  * // create an engine object, normally you would first extend the class with your own
2789
2807
  * const pos = vec2(2,3);
2790
- * const object = new EngineObject(pos);
2808
+ * const object = new EngineObject(pos);
2791
2809
  */
2792
2810
  class EngineObject
2793
2811
  {
@@ -2802,12 +2820,12 @@ class EngineObject
2802
2820
  constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
2803
2821
  {
2804
2822
  // check passed in params
2805
- ASSERT(isVector2(pos) && pos.isValid(), 'object pos should be a vec2');
2806
- ASSERT(isVector2(size) && size.isValid(), 'object size should be a vec2');
2823
+ ASSERT(isVector2(pos), 'object pos should be a vec2');
2824
+ ASSERT(isVector2(size), 'object size should be a vec2');
2807
2825
  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');
2809
- ASSERT(isColor(color) && color.isValid(), 'object color should be a valid rgba color');
2810
- ASSERT(typeof renderOrder == 'number', 'object renderOrder should be a number');
2826
+ ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2827
+ ASSERT(isColor(color), 'object color should be a valid rgba color');
2828
+ ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
2811
2829
 
2812
2830
  /** @property {Vector2} - World space position of the object */
2813
2831
  this.pos = pos.copy();
@@ -2875,7 +2893,7 @@ class EngineObject
2875
2893
  // add to list of objects
2876
2894
  engineObjects.push(this);
2877
2895
  }
2878
-
2896
+
2879
2897
  /** Update the object transform, called automatically by engine even when paused */
2880
2898
  updateTransforms()
2881
2899
  {
@@ -2944,7 +2962,7 @@ class EngineObject
2944
2962
  for (const o of engineObjectsCollide)
2945
2963
  {
2946
2964
  // non solid objects don't collide with each other
2947
- if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o == this)
2965
+ if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
2948
2966
  continue;
2949
2967
 
2950
2968
  // check collision
@@ -2967,7 +2985,7 @@ class EngineObject
2967
2985
  this.velocity = this.velocity.add(velocity);
2968
2986
  if (o.mass) // push away if not fixed
2969
2987
  o.velocity = o.velocity.subtract(velocity);
2970
-
2988
+
2971
2989
  debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
2972
2990
  continue;
2973
2991
  }
@@ -2978,7 +2996,7 @@ class EngineObject
2978
2996
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
2979
2997
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
2980
2998
  const restitution = max(this.restitution, o.restitution);
2981
-
2999
+
2982
3000
  if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
2983
3001
  {
2984
3002
  // push outside object collision
@@ -3066,7 +3084,7 @@ class EngineObject
3066
3084
  {
3067
3085
  // move to previous position
3068
3086
  this.pos.y = oldPos.y;
3069
- this.groundObject = undefined;
3087
+ this.groundObject = undefined;
3070
3088
  }
3071
3089
  }
3072
3090
  if (blockedLayerX)
@@ -3080,20 +3098,20 @@ class EngineObject
3080
3098
  }
3081
3099
  }
3082
3100
  }
3083
-
3101
+
3084
3102
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
3085
3103
  render()
3086
3104
  {
3087
3105
  // default object render
3088
3106
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
3089
3107
  }
3090
-
3108
+
3091
3109
  /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
3092
3110
  destroy()
3093
- {
3111
+ {
3094
3112
  if (this.destroyed)
3095
3113
  return;
3096
-
3114
+
3097
3115
  // disconnect from parent and destroy children
3098
3116
  this.destroyed = 1;
3099
3117
  this.parent && this.parent.removeChild(this);
@@ -3119,7 +3137,7 @@ class EngineObject
3119
3137
  /** Convert from world space to local space for a vector (rotation only)
3120
3138
  * @param {Vector2} vec - world space vector */
3121
3139
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
3122
-
3140
+
3123
3141
  /** Called to check if a tile collision should be resolved
3124
3142
  * @param {number} tileData - the value of the tile at the position
3125
3143
  * @param {Vector2} pos - tile where the collision occurred
@@ -3138,16 +3156,19 @@ class EngineObject
3138
3156
 
3139
3157
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
3140
3158
  * @param {Vector2} acceleration */
3141
- applyAcceleration(acceleration) { if (this.mass) this.velocity = this.velocity.add(acceleration); }
3159
+ applyAcceleration(acceleration)
3160
+ { if (this.mass) this.velocity = this.velocity.add(acceleration); }
3142
3161
 
3143
- /** Apply angular acceleration to this object
3162
+ /** Apply angular acceleration to this object
3144
3163
  * @param {number} acceleration */
3145
- applyAngularAcceleration(acceleration) { if (this.mass) this.angleVelocity += acceleration; }
3164
+ applyAngularAcceleration(acceleration)
3165
+ { if (this.mass) this.angleVelocity += acceleration; }
3146
3166
 
3147
3167
  /** Apply force to this object (adjust velocity, affected by mass)
3148
3168
  * @param {Vector2} force */
3149
- applyForce(force) { this.applyAcceleration(force.scale(1/this.mass)); }
3150
-
3169
+ applyForce(force)
3170
+ { if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
3171
+
3151
3172
  /** Get the direction of the mirror
3152
3173
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3153
3174
  getMirrorSign() { return this.mirror ? -1 : 1; }
@@ -3169,7 +3190,7 @@ class EngineObject
3169
3190
  * @param {EngineObject} child */
3170
3191
  removeChild(child)
3171
3192
  {
3172
- ASSERT(child.parent == this && this.children.includes(child));
3193
+ ASSERT(child.parent === this && this.children.includes(child));
3173
3194
  this.children.splice(this.children.indexOf(child), 1);
3174
3195
  child.parent = 0;
3175
3196
  }
@@ -3215,7 +3236,7 @@ class EngineObject
3215
3236
  {
3216
3237
  if (!debug)
3217
3238
  return;
3218
-
3239
+
3219
3240
  // show object info for debugging
3220
3241
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
3221
3242
  const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
@@ -3225,24 +3246,24 @@ class EngineObject
3225
3246
  this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(1,1,1,.5));
3226
3247
  }
3227
3248
  }
3228
- /**
3249
+ /**
3229
3250
  * LittleJS Drawing System
3230
3251
  * - Hybrid system with both Canvas2D and WebGL available
3231
3252
  * - Super fast tile sheet rendering with WebGL
3232
3253
  * - Can apply rotation, mirror, color and additive color
3233
3254
  * - Font rendering system with built in engine font
3234
3255
  * - Many useful utility functions
3235
- *
3256
+ *
3236
3257
  * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
3237
3258
  * There are 3 canvas/contexts available to draw to...
3238
3259
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
3239
3260
  * glCanvas - Used by the accelerated WebGL batch rendering system.
3240
3261
  * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
3241
- *
3262
+ *
3242
3263
  * The WebGL rendering system is very fast with some caveats...
3243
3264
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
3244
3265
  * - Group additive rendering together using renderOrder to mitigate this issue
3245
- *
3266
+ *
3246
3267
  * The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
3247
3268
  * @namespace Draw
3248
3269
  */
@@ -3287,7 +3308,7 @@ let workCanvas;
3287
3308
  * @memberof Draw */
3288
3309
  let workContext;
3289
3310
 
3290
- /** The size of the main canvas (and other secondary canvases)
3311
+ /** The size of the main canvas (and other secondary canvases)
3291
3312
  * @type {Vector2}
3292
3313
  * @memberof Draw */
3293
3314
  let mainCanvasSize = vec2();
@@ -3297,12 +3318,14 @@ let mainCanvasSize = vec2();
3297
3318
  * @memberof Draw */
3298
3319
  let textureInfos = [];
3299
3320
 
3300
- // Keep track of how many draw calls there were each frame for debugging
3321
+ /** Keeps track of how many draw calls there were each frame for debugging
3322
+ * @type {number}
3323
+ * @memberof Draw */
3301
3324
  let drawCount;
3302
3325
 
3303
3326
  ///////////////////////////////////////////////////////////////////////////////
3304
3327
 
3305
- /**
3328
+ /**
3306
3329
  * Create a tile info object using a grid based system
3307
3330
  * - This can take vecs or floats for easier use and conversion
3308
3331
  * - If an index is passed in, the tile size and index will determine the position
@@ -3316,15 +3339,14 @@ let drawCount;
3316
3339
  * tile(5, 8) // a tile at index 5 using a tile size of 8
3317
3340
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3318
3341
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3319
- * @memberof Draw
3320
- */
3342
+ * @memberof Draw */
3321
3343
  function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3322
3344
  {
3323
3345
  if (headlessMode)
3324
3346
  return new TileInfo;
3325
3347
 
3326
3348
  // if size is a number, make it a vector
3327
- if (typeof size == 'number')
3349
+ if (typeof size === 'number')
3328
3350
  {
3329
3351
  ASSERT(size > 0);
3330
3352
  size = new Vector2(size, size);
@@ -3333,24 +3355,24 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3333
3355
  // create tile info object
3334
3356
  const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
3335
3357
 
3336
- // use get the pos of the tile
3358
+ // get the position of the tile
3337
3359
  const textureInfo = textureInfos[textureIndex];
3338
3360
  ASSERT(!!textureInfo, 'Texture not loaded');
3339
3361
  const sizePaddedX = size.x + padding*2;
3340
3362
  const sizePaddedY = size.y + padding*2;
3341
- if (typeof pos == 'number')
3363
+ if (typeof pos === 'number')
3342
3364
  {
3343
3365
  const cols = textureInfo.size.x / sizePaddedX |0;
3344
- ASSERT(cols>0, 'Tile size is too big for texture');
3366
+ ASSERT(cols > 0, 'Tile size is too big for texture');
3345
3367
  const posX = pos % cols, posY = (pos / cols) |0;
3346
3368
  tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
3347
3369
  }
3348
3370
  else
3349
3371
  tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
3350
- return tileInfo;
3372
+ return tileInfo;
3351
3373
  }
3352
3374
 
3353
- /**
3375
+ /**
3354
3376
  * Tile Info - Stores info about how to draw a tile
3355
3377
  */
3356
3378
  class TileInfo
@@ -3388,14 +3410,14 @@ class TileInfo
3388
3410
  */
3389
3411
  frame(frame)
3390
3412
  {
3391
- ASSERT(typeof frame == 'number');
3413
+ ASSERT(typeof frame === 'number');
3392
3414
  return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
3393
3415
  }
3394
3416
 
3395
3417
  /**
3396
3418
  * Set this tile to use a full image
3397
3419
  * @param {HTMLImageElement|OffscreenCanvas} image
3398
- * @param {WebGLTexture} [glTexture] - webgl texture
3420
+ * @param {WebGLTexture} [glTexture] - WebGL texture
3399
3421
  * @return {TileInfo}
3400
3422
  */
3401
3423
  setFullImage(image, glTexture)
@@ -3413,7 +3435,7 @@ class TextureInfo
3413
3435
  /**
3414
3436
  * Create a TextureInfo, called automatically by the engine
3415
3437
  * @param {HTMLImageElement|OffscreenCanvas} image
3416
- * @param {WebGLTexture} [glTexture] - webgl texture
3438
+ * @param {WebGLTexture} [glTexture] - WebGL texture
3417
3439
  */
3418
3440
  constructor(image, glTexture)
3419
3441
  {
@@ -3423,7 +3445,7 @@ class TextureInfo
3423
3445
  this.size = vec2(image.width, image.height);
3424
3446
  /** @property {Vector2} - inverse of the size, cached for rendering */
3425
3447
  this.sizeInverse = vec2(1/image.width, 1/image.height);
3426
- /** @property {WebGLTexture} - webgl texture */
3448
+ /** @property {WebGLTexture} - WebGL texture */
3427
3449
  this.glTexture = glTexture;
3428
3450
  }
3429
3451
 
@@ -3439,28 +3461,25 @@ class TextureInfo
3439
3461
  // Drawing functions
3440
3462
 
3441
3463
  /** 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
3464
+ * @param {Vector2} pos - Center of the tile in world space
3465
+ * @param {Vector2} [size=(1,1)] - Size of the tile in world space
3466
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
3467
+ * @param {Color} [color=(1,1,1,1)] - Color to modulate with
3468
+ * @param {number} [angle] - Angle to rotate by
3469
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
3470
+ * @param {Color} [additiveColor] - Additive color to be applied if any
3471
+ * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
3472
+ * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
3451
3473
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
3452
3474
  * @memberof Draw */
3453
- function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
3475
+ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3454
3476
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
3455
3477
  {
3456
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3457
- ASSERT(isVector2(pos) && pos.isValid(), 'drawTile pos should be a vec2');
3458
- ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
3478
+ ASSERT(isVector2(pos), 'drawTile pos should be a vec2');
3479
+ ASSERT(isVector2(size), 'drawTile size should be a vec2');
3459
3480
  ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
3460
3481
  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
3482
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3464
3483
 
3465
3484
  const textureInfo = tileInfo && tileInfo.textureInfo;
3466
3485
  if (useWebGL)
@@ -3484,28 +3503,28 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
3484
3503
  {
3485
3504
  const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
3486
3505
  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());
3506
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
3507
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
3508
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
3509
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
3491
3510
  }
3492
3511
  else
3493
3512
  {
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());
3513
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
3514
+ x, y, x + w, y + h,
3515
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
3497
3516
  }
3498
3517
  }
3499
3518
  else
3500
3519
  {
3501
3520
  // if no tile info, force untextured
3502
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
3521
+ glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
3503
3522
  }
3504
3523
  }
3505
3524
  else
3506
3525
  {
3507
3526
  // normal canvas 2D rendering method (slower)
3508
- showWatermark && ++drawCount;
3527
+ ++drawCount;
3509
3528
  size = new Vector2(size.x, -size.y); // fix upside down sprites
3510
3529
  drawCanvas2D(pos, size, angle, mirror, (context)=>
3511
3530
  {
@@ -3537,14 +3556,128 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
3537
3556
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3538
3557
  * @memberof Draw */
3539
3558
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3540
- {
3541
- drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
3559
+ {
3560
+ drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
3561
+ }
3562
+
3563
+ /** Draw a rect centered on pos with a gradient from top to bottom
3564
+ * @param {Vector2} pos
3565
+ * @param {Vector2} [size=(1,1)]
3566
+ * @param {Color} [colorTop=(1,1,1,1)]
3567
+ * @param {Color} [colorBottom=(0,0,0,1)]
3568
+ * @param {number} [angle]
3569
+ * @param {boolean} [useWebGL=glEnable]
3570
+ * @param {boolean} [screenSpace]
3571
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3572
+ * @memberof Draw */
3573
+ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3574
+ {
3575
+ ASSERT(isVector2(pos), 'drawRectGradient pos should be a vec2');
3576
+ ASSERT(isVector2(size), 'drawRectGradient size should be a vec2');
3577
+ ASSERT(isColor(colorTop) && isColor(colorBottom), 'drawRectGradient color is invalid');
3578
+ ASSERT(isNumber(angle), 'drawRectGradient angle should be a number');
3579
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3580
+ if (useWebGL)
3581
+ {
3582
+ if (screenSpace)
3583
+ {
3584
+ // convert to world space
3585
+ pos = screenToWorld(pos);
3586
+ size = size.scale(1/cameraScale);
3587
+ }
3588
+ // build 4 corner points for the rectangle
3589
+ const points = [], colors = [];
3590
+ const halfSizeX = size.x/2, halfSizeY = size.y/2;
3591
+ const colorTopInt = colorTop.rgbaInt();
3592
+ const colorBottomInt = colorBottom.rgbaInt();
3593
+ const c = Math.cos(-angle), s = Math.sin(-angle);
3594
+ for (let i=4; i--;)
3595
+ {
3596
+ const x = i & 1 ? halfSizeX : -halfSizeX;
3597
+ const y = i & 2 ? halfSizeY : -halfSizeY;
3598
+ const rx = x * c - y * s;
3599
+ const ry = x * s + y * c;
3600
+ const color = i & 2 ? colorTopInt : colorBottomInt;
3601
+ points.push(vec2(pos.x + rx, pos.y + ry));
3602
+ colors.push(color);
3603
+ }
3604
+ glDrawColoredPoints(points, colors);
3605
+ }
3606
+ else
3607
+ {
3608
+ // normal canvas 2D rendering method (slower)
3609
+ ++drawCount;
3610
+ size = new Vector2(size.x, -size.y); // fix upside down sprites
3611
+ drawCanvas2D(pos, size, angle, false, (context)=>
3612
+ {
3613
+ // if no tile info, use untextured rect
3614
+ const gradient = context.createLinearGradient(0, -.5, 0, .5);
3615
+ gradient.addColorStop(0, colorTop.toString());
3616
+ gradient.addColorStop(1, colorBottom.toString());
3617
+ context.fillStyle = gradient;
3618
+ context.fillRect(-.5, -.5, 1, 1);
3619
+ }, screenSpace, context);
3620
+ }
3621
+ }
3622
+
3623
+ /** Draw connected lines between a series of points
3624
+ * @param {Array<Vector2>} points
3625
+ * @param {number} [width]
3626
+ * @param {Color} [color=(1,1,1,1)]
3627
+ * @param {boolean} [wrap] - Should the last point connect to the first?
3628
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
3629
+ * @param {number} [angle] - Angle to rotate by
3630
+ * @param {boolean} [useWebGL=glEnable]
3631
+ * @param {boolean} [screenSpace]
3632
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3633
+ * @memberof Draw */
3634
+ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3635
+ {
3636
+ ASSERT(Array.isArray(points), 'drawLineList points should be an array');
3637
+ ASSERT(isNumber(width), 'drawLineList width should be a number');
3638
+ ASSERT(isColor(color), 'drawLineList color is invalid');
3639
+ ASSERT(isVector2(pos), 'drawLineList pos should be a vec2');
3640
+ ASSERT(isNumber(angle), 'drawLineList angle should be a number');
3641
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3642
+ if (useWebGL)
3643
+ {
3644
+ let scale = 1;
3645
+ if (screenSpace)
3646
+ {
3647
+ // convert to world space
3648
+ pos = screenToWorld(pos);
3649
+ scale = 1/cameraScale;
3650
+ }
3651
+ glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, scale, scale, angle, wrap);
3652
+ }
3653
+ else
3654
+ {
3655
+ // normal canvas 2D rendering method (slower)
3656
+ ++drawCount;
3657
+ drawCanvas2D(pos, vec2(1), angle, false, (context)=>
3658
+ {
3659
+ context.strokeStyle = color.toString();
3660
+ context.lineWidth = width;
3661
+ context.beginPath();
3662
+ for (let i=0; i<points.length; ++i)
3663
+ {
3664
+ const point = points[i];
3665
+ if (i)
3666
+ context.lineTo(point.x, point.y);
3667
+ else
3668
+ context.moveTo(point.x, point.y);
3669
+ }
3670
+ if (wrap)
3671
+ context.closePath();
3672
+ context.stroke();
3673
+ }, screenSpace, context);
3674
+ }
3542
3675
  }
3543
3676
 
3544
3677
  /** Draw colored line between two points
3545
3678
  * @param {Vector2} posA
3546
3679
  * @param {Vector2} posB
3547
- * @param {number} [thickness]
3680
+ * @param {number} [width]
3548
3681
  * @param {Color} [color=(1,1,1,1)]
3549
3682
  * @param {Vector2} [pos=(0,0)] - Offset to apply
3550
3683
  * @param {number} [angle] - Angle to rotate by
@@ -3552,15 +3685,43 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3552
3685
  * @param {boolean} [screenSpace]
3553
3686
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3554
3687
  * @memberof Draw */
3555
- function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
3688
+ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
3556
3689
  {
3557
3690
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
3558
- const size = vec2(thickness, halfDelta.length()*2);
3691
+ const size = vec2(width, halfDelta.length()*2);
3559
3692
  pos = pos.add(posA.add(halfDelta));
3560
3693
  angle += halfDelta.angle();
3561
3694
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3562
3695
  }
3563
3696
 
3697
+ /** Draw colored regular polygon using passed in number of sides
3698
+ * @param {Vector2} pos
3699
+ * @param {Vector2} [size=(1,1)]
3700
+ * @param {number} [sides]
3701
+ * @param {Color} [color=(1,1,1,1)]
3702
+ * @param {number} [angle]
3703
+ * @param {number} [lineWidth]
3704
+ * @param {Color} [lineColor=(0,0,0,1)]
3705
+ * @param {boolean} [useWebGL=glEnable]
3706
+ * @param {boolean} [screenSpace]
3707
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3708
+ * @memberof Draw */
3709
+ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3710
+ {
3711
+ ASSERT(isVector2(size), 'drawRegularPoly size should be a vec2');
3712
+ ASSERT(isNumber(sides), 'drawRegularPoly sides should be a number');
3713
+
3714
+ // build regular polygon points
3715
+ const points = [];
3716
+ const sizeX = size.x/2, sizeY = size.y/2;
3717
+ for (let i=sides; i--;)
3718
+ {
3719
+ const a = (i/sides)*PI*2;
3720
+ points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
3721
+ }
3722
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3723
+ }
3724
+
3564
3725
  /** Draw colored polygon using passed in points
3565
3726
  * @param {Array<Vector2>} points - Array of Vector2 points
3566
3727
  * @param {Color} [color=(1,1,1,1)]
@@ -3568,82 +3729,110 @@ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL
3568
3729
  * @param {Color} [lineColor=(0,0,0,1)]
3569
3730
  * @param {Vector2} [pos=(0,0)] - Offset to apply
3570
3731
  * @param {number} [angle] - Angle to rotate by
3571
- * @param {boolean} [useWebGL] - Webgl not supported
3732
+ * @param {boolean} [useWebGL=glEnable]
3572
3733
  * @param {boolean} [screenSpace]
3573
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3734
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3574
3735
  * @memberof Draw */
3575
- function drawPoly(points, color=new Color, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=false, screenSpace=false, context=drawContext)
3736
+ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3576
3737
  {
3577
- ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
3738
+ ASSERT(isVector2(pos), 'drawPoly pos should be a vec2');
3578
3739
  ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3579
3740
  ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3580
3741
  ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3581
3742
  ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3582
- ASSERT(!useWebGL, 'drawPoly webgl not supported');
3583
- drawCanvas2D(pos, vec2(1), angle, false, context=>
3743
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3744
+ if (useWebGL)
3584
3745
  {
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)
3746
+ let scale = 1;
3747
+ if (screenSpace)
3592
3748
  {
3593
- context.strokeStyle = lineColor.toString();
3594
- context.lineWidth = lineWidth;
3595
- context.stroke();
3749
+ // convert to world space
3750
+ pos = screenToWorld(pos);
3751
+ scale = 1/cameraScale;
3596
3752
  }
3597
- }, screenSpace, context);
3753
+ glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
3754
+ if (lineWidth > 0)
3755
+ glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
3756
+ }
3757
+ else
3758
+ {
3759
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3760
+ {
3761
+ context.fillStyle = color.toString();
3762
+ context.beginPath();
3763
+ for (const point of points)
3764
+ context.lineTo(point.x, point.y);
3765
+ context.closePath();
3766
+ context.fill();
3767
+ if (lineWidth)
3768
+ {
3769
+ context.strokeStyle = lineColor.toString();
3770
+ context.lineWidth = lineWidth;
3771
+ context.stroke();
3772
+ }
3773
+ }, screenSpace, context);
3774
+ }
3598
3775
  }
3599
3776
 
3600
3777
  /** Draw colored ellipse using passed in point
3601
3778
  * @param {Vector2} pos
3602
- * @param {Vector2} [size=(1,1)]
3779
+ * @param {Vector2} [size=(1,1)] - Width and height diameter
3603
3780
  * @param {Color} [color=(1,1,1,1)]
3604
3781
  * @param {number} [angle]
3605
3782
  * @param {number} [lineWidth]
3606
3783
  * @param {Color} [lineColor=(0,0,0,1)]
3607
- * @param {boolean} [useWebGL] - Webgl not supported
3784
+ * @param {boolean} [useWebGL=glEnable]
3608
3785
  * @param {boolean} [screenSpace]
3609
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3786
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3610
3787
  * @memberof Draw */
3611
- function drawEllipse(pos, size=vec2(1), color=new Color, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace=false, context=drawContext)
3788
+ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3612
3789
  {
3613
- ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
3614
- ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
3790
+ ASSERT(isVector2(pos), 'drawEllipse pos should be a vec2');
3791
+ ASSERT(isVector2(size), 'drawEllipse size should be a vec2');
3615
3792
  ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3616
3793
  ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3617
3794
  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=>
3795
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3796
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3797
+ if (useWebGL)
3621
3798
  {
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)
3799
+ // draw as a regular polygon
3800
+ const sides = glCircleSides;
3801
+ drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
3802
+ }
3803
+ else
3804
+ {
3805
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3627
3806
  {
3628
- context.strokeStyle = lineColor.toString();
3629
- context.lineWidth = lineWidth;
3630
- context.stroke();
3631
- }
3632
- }, screenSpace, context);
3807
+ context.fillStyle = color.toString();
3808
+ context.beginPath();
3809
+ context.ellipse(0, 0, size.x/2, size.y/2, 0, 0, 9);
3810
+ context.fill();
3811
+ if (lineWidth)
3812
+ {
3813
+ context.strokeStyle = lineColor.toString();
3814
+ context.lineWidth = lineWidth;
3815
+ context.stroke();
3816
+ }
3817
+ }, screenSpace, context);
3818
+ }
3633
3819
  }
3634
3820
 
3635
3821
  /** Draw colored circle using passed in point
3636
3822
  * @param {Vector2} pos
3637
- * @param {number} [radius=1]
3823
+ * @param {number} [size=1] - Diameter
3638
3824
  * @param {Color} [color=(1,1,1,1)]
3639
3825
  * @param {number} [lineWidth=0]
3640
3826
  * @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]
3827
+ * @param {boolean} [useWebGL=glEnable]
3828
+ * @param {boolean} [screenSpace]
3829
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3644
3830
  * @memberof Draw */
3645
- function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK, useWebGL=false, screenSpace, context=drawContext)
3646
- { drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
3831
+ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3832
+ {
3833
+ ASSERT(isNumber(size), 'drawCircle size should be a number');
3834
+ drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
3835
+ }
3647
3836
 
3648
3837
  /** Draw directly to a 2d canvas context in world space
3649
3838
  * @param {Vector2} pos
@@ -3651,7 +3840,7 @@ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=BLACK
3651
3840
  * @param {number} angle
3652
3841
  * @param {boolean} [mirror]
3653
3842
  * @param {Function} [drawFunction]
3654
- * @param {boolean} [screenSpace=false]
3843
+ * @param {boolean} [screenSpace=false]
3655
3844
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3656
3845
  * @memberof Draw */
3657
3846
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
@@ -3721,7 +3910,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3721
3910
  * @param {number} [maxWidth]
3722
3911
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3723
3912
  * @memberof Draw */
3724
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
3913
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3725
3914
  {
3726
3915
  context.fillStyle = color.toString();
3727
3916
  context.strokeStyle = lineColor.toString();
@@ -3729,7 +3918,6 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
3729
3918
  context.textAlign = textAlign;
3730
3919
  context.font = size + 'px '+ font;
3731
3920
  context.textBaseline = 'middle';
3732
- context.lineJoin = 'round';
3733
3921
 
3734
3922
  const lines = (text+'').split('\n');
3735
3923
  let posY = pos.y;
@@ -3753,7 +3941,7 @@ function screenToWorld(screenPos)
3753
3941
  {
3754
3942
  let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3755
3943
  let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3756
- if (cameraAngle)
3944
+ if (cameraAngle)
3757
3945
  {
3758
3946
  // apply camera rotation
3759
3947
  const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
@@ -3846,7 +4034,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3846
4034
  {
3847
4035
  // white texture with no additive alpha, no need to tint
3848
4036
  context.globalAlpha = color.a;
3849
- context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4037
+ context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3850
4038
  context.globalAlpha = 1;
3851
4039
  }
3852
4040
  else
@@ -3867,7 +4055,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3867
4055
  for (let i = 0; i < data.length; ++i)
3868
4056
  data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
3869
4057
  workContext.putImageData(imageData, 0, 0);
3870
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4058
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3871
4059
  }
3872
4060
  else
3873
4061
  {
@@ -3880,7 +4068,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3880
4068
  }
3881
4069
  workContext.putImageData(imageData, 0, 0);
3882
4070
  context.globalAlpha = color.a;
3883
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4071
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3884
4072
  context.globalAlpha = 1;
3885
4073
  }
3886
4074
  }
@@ -3907,7 +4095,7 @@ function toggleFullscreen()
3907
4095
  }
3908
4096
 
3909
4097
  /** Set the cursor style
3910
- * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
4098
+ * @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
3911
4099
  * @memberof Draw */
3912
4100
  function setCursor(cursorStyle = 'auto')
3913
4101
  {
@@ -3919,7 +4107,7 @@ function setCursor(cursorStyle = 'auto')
3919
4107
 
3920
4108
  let engineFontImage;
3921
4109
 
3922
- /**
4110
+ /**
3923
4111
  * Font Image Object - Draw text on a 2D canvas by using characters in an image
3924
4112
  * - 96 characters (from space to tilde) are stored in an image
3925
4113
  * - Uses a default 8x8 font if none is supplied
@@ -3927,9 +4115,9 @@ let engineFontImage;
3927
4115
  * @example
3928
4116
  * // use built in font
3929
4117
  * const font = new FontImage;
3930
- *
4118
+ *
3931
4119
  * // draw text
3932
- * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
4120
+ * font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
3933
4121
  */
3934
4122
  class FontImage
3935
4123
  {
@@ -3937,7 +4125,6 @@ class FontImage
3937
4125
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
3938
4126
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3939
4127
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
3940
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
3941
4128
  */
3942
4129
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
3943
4130
  {
@@ -3951,7 +4138,6 @@ class FontImage
3951
4138
  this.image = image || engineFontImage;
3952
4139
  this.tileSize = tileSize;
3953
4140
  this.paddingSize = paddingSize;
3954
- this.context = context;
3955
4141
  }
3956
4142
 
3957
4143
  /** Draw text in world space using the image font
@@ -3959,23 +4145,32 @@ class FontImage
3959
4145
  * @param {Vector2} pos
3960
4146
  * @param {number} [scale=.25]
3961
4147
  * @param {boolean} [center]
4148
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
3962
4149
  */
3963
- drawText(text, pos, scale=1, center)
4150
+ drawText(text, pos, scale=1, center, context=drawContext)
3964
4151
  {
3965
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
4152
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
3966
4153
  }
3967
4154
 
3968
- /** Draw text in screen space using the image font
4155
+ /** Draw text on overlay canvas in world space using the image font
4156
+ * @param {string} text
4157
+ * @param {Vector2} pos
4158
+ * @param {number} [scale]
4159
+ * @param {boolean} [center]
4160
+ */
4161
+ drawTextOverlay(text, pos, scale=4, center)
4162
+ { this.drawText(text, pos, scale, center, overlayContext); }
4163
+
4164
+ /** Draw text on overlay canvas in screen space using the image font
3969
4165
  * @param {string} text
3970
4166
  * @param {Vector2} pos
3971
4167
  * @param {number} [scale]
3972
4168
  * @param {boolean} [center]
4169
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3973
4170
  */
3974
- drawTextScreen(text, pos, scale=4, center)
4171
+ drawTextScreen(text, pos, scale=4, center, context=overlayContext)
3975
4172
  {
3976
- const context = this.context;
3977
4173
  context.save();
3978
-
3979
4174
  const size = this.tileSize;
3980
4175
  const drawSize = size.add(this.paddingSize).scale(scale);
3981
4176
  const cols = this.image.width / this.tileSize.x |0;
@@ -3994,15 +4189,14 @@ class FontImage
3994
4189
  const x = tile % cols;
3995
4190
  const y = tile / cols |0;
3996
4191
  const drawPos = pos.add(vec2(j,i).multiply(drawSize));
3997
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
4192
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
3998
4193
  drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
3999
4194
  }
4000
4195
  });
4001
-
4002
4196
  context.restore();
4003
4197
  }
4004
4198
  }
4005
- /**
4199
+ /**
4006
4200
  * LittleJS Input System
4007
4201
  * - Tracks keyboard down, pressed, and released
4008
4202
  * - Tracks mouse buttons, position, and wheel
@@ -4018,10 +4212,10 @@ class FontImage
4018
4212
  * @return {boolean}
4019
4213
  * @memberof Input */
4020
4214
  function keyIsDown(key, device=0)
4021
- {
4215
+ {
4022
4216
  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);
4217
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
4218
+ return inputData[device] && !!(inputData[device][key] & 1);
4025
4219
  }
4026
4220
 
4027
4221
  /** Returns true if device key was pressed this frame
@@ -4030,10 +4224,10 @@ function keyIsDown(key, device=0)
4030
4224
  * @return {boolean}
4031
4225
  * @memberof Input */
4032
4226
  function keyWasPressed(key, device=0)
4033
- {
4227
+ {
4034
4228
  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);
4229
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
4230
+ return inputData[device] && !!(inputData[device][key] & 2);
4037
4231
  }
4038
4232
 
4039
4233
  /** Returns true if device key was released this frame
@@ -4042,9 +4236,9 @@ function keyWasPressed(key, device=0)
4042
4236
  * @return {boolean}
4043
4237
  * @memberof Input */
4044
4238
  function keyWasReleased(key, device=0)
4045
- {
4239
+ {
4046
4240
  ASSERT(key !== undefined, 'key is undefined');
4047
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
4241
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
4048
4242
  return inputData[device] && !!(inputData[device][key] & 4);
4049
4243
  }
4050
4244
 
@@ -4166,7 +4360,7 @@ function gamepadWasReleased(button, gamepad=0)
4166
4360
  * @param {number} [gamepad]
4167
4361
  * @return {Vector2}
4168
4362
  * @memberof Input */
4169
- function gamepadStick(stick, gamepad=0)
4363
+ function gamepadStick(stick, gamepad=0)
4170
4364
  { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
4171
4365
 
4172
4366
  ///////////////////////////////////////////////////////////////////////////////
@@ -4243,10 +4437,10 @@ function inputInit()
4243
4437
  {
4244
4438
  // handle remapping wasd keys to directions
4245
4439
  return inputWASDEmulateDirection ?
4246
- c == 'KeyW' ? 'ArrowUp' :
4247
- c == 'KeyS' ? 'ArrowDown' :
4248
- c == 'KeyA' ? 'ArrowLeft' :
4249
- c == 'KeyD' ? 'ArrowRight' : c : c;
4440
+ c === 'KeyW' ? 'ArrowUp' :
4441
+ c === 'KeyS' ? 'ArrowDown' :
4442
+ c === 'KeyA' ? 'ArrowLeft' :
4443
+ c === 'KeyD' ? 'ArrowRight' : c : c;
4250
4444
  }
4251
4445
  function onMouseDown(e)
4252
4446
  {
@@ -4254,9 +4448,9 @@ function inputInit()
4254
4448
  return;
4255
4449
 
4256
4450
  // fix stalled audio requiring user interaction
4257
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
4451
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
4258
4452
  audioContext.resume();
4259
-
4453
+
4260
4454
  isUsingGamepad = false;
4261
4455
  inputData[0][e.button] = 3;
4262
4456
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
@@ -4299,8 +4493,8 @@ function gamepadsUpdate()
4299
4493
  const applyDeadZones = (v)=>
4300
4494
  {
4301
4495
  const min=.3, max=.8;
4302
- const deadZone = (v)=>
4303
- v > min ? percent( v, min, max) :
4496
+ const deadZone = (v)=>
4497
+ v > min ? percent(v, min, max) :
4304
4498
  v < -min ? -percent(-v, min, max) : 0;
4305
4499
  return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
4306
4500
  }
@@ -4308,30 +4502,29 @@ function gamepadsUpdate()
4308
4502
  // update touch gamepad if enabled
4309
4503
  if (touchGamepadEnable && isTouchDevice)
4310
4504
  {
4311
- ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
4312
- if (touchGamepadTimer.isSet())
4505
+ if (!touchGamepadTimer.isSet())
4506
+ return;
4507
+
4508
+ // read virtual analog stick
4509
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
4510
+ sticks[0] = vec2();
4511
+ if (touchGamepadAnalog)
4512
+ sticks[0] = applyDeadZones(touchGamepadStick);
4513
+ else if (touchGamepadStick.lengthSquared() > .3)
4313
4514
  {
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
- }
4515
+ // convert to 8 way dpad
4516
+ sticks[0].x = Math.round(touchGamepadStick.x);
4517
+ sticks[0].y = -Math.round(touchGamepadStick.y);
4518
+ sticks[0] = sticks[0].clampLength();
4519
+ }
4326
4520
 
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
- }
4521
+ // read virtual gamepad buttons
4522
+ const data = inputData[1] || (inputData[1] = []);
4523
+ for (let i=10; i--;)
4524
+ {
4525
+ const j = i === 3 ? 2 : i === 2 ? 3 : i; // fix button locations
4526
+ const wasDown = gamepadIsDown(j,0);
4527
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
4335
4528
  }
4336
4529
  }
4337
4530
 
@@ -4357,7 +4550,7 @@ function gamepadsUpdate()
4357
4550
  // read analog sticks
4358
4551
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
4359
4552
  sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
4360
-
4553
+
4361
4554
  // read buttons
4362
4555
  for (let j = gamepad.buttons.length; j--;)
4363
4556
  {
@@ -4373,14 +4566,14 @@ function gamepadsUpdate()
4373
4566
  {
4374
4567
  // copy dpad to left analog stick when pressed
4375
4568
  const dpad = vec2(
4376
- (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
4569
+ (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
4377
4570
  (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
4378
4571
  if (dpad.lengthSquared())
4379
4572
  sticks[0] = dpad.clampLength();
4380
4573
  }
4381
4574
 
4382
4575
  // disable touch gamepad if using real gamepad
4383
- touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
4576
+ touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
4384
4577
  }
4385
4578
  }
4386
4579
  }
@@ -4405,20 +4598,13 @@ function vibrateStop() { vibrate(0); }
4405
4598
  const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4406
4599
 
4407
4600
  // touch gamepad internal variables
4408
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
4601
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
4409
4602
 
4410
4603
  // enable touch input mouse passthrough
4411
4604
  function touchInputInit()
4412
4605
  {
4413
4606
  // add non passive touch event listeners
4414
4607
  let handleTouch = handleTouchDefault;
4415
- if (touchGamepadEnable)
4416
- {
4417
- // touch input internal variables
4418
- handleTouch = handleTouchGamepad;
4419
- touchGamepadButtons = [];
4420
- touchGamepadStick = vec2();
4421
- }
4422
4608
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
4423
4609
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
4424
4610
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
@@ -4427,8 +4613,15 @@ function touchInputInit()
4427
4613
  let wasTouching;
4428
4614
  function handleTouchDefault(e)
4429
4615
  {
4616
+ if (!touchInputEnable)
4617
+ return;
4618
+
4619
+ // route touch to gamepad
4620
+ if (touchGamepadEnable)
4621
+ handleTouchGamepad(e);
4622
+
4430
4623
  // fix stalled audio requiring user interaction
4431
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
4624
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
4432
4625
  audioContext.resume();
4433
4626
 
4434
4627
  // check if touching and pass to mouse events
@@ -4457,7 +4650,7 @@ function touchInputInit()
4457
4650
  // prevent default handling like copy and magnifier lens
4458
4651
  if (inputPreventDefault && document.hasFocus()) // allow document to get focus
4459
4652
  e.preventDefault();
4460
-
4653
+
4461
4654
  // must return true so the document will get focus
4462
4655
  return true;
4463
4656
  }
@@ -4469,7 +4662,7 @@ function touchInputInit()
4469
4662
  touchGamepadStick = vec2();
4470
4663
  touchGamepadButtons = [];
4471
4664
  isUsingGamepad = true;
4472
-
4665
+
4473
4666
  const touching = e.touches.length;
4474
4667
  if (touching)
4475
4668
  {
@@ -4478,9 +4671,6 @@ function touchInputInit()
4478
4671
  {
4479
4672
  // touch anywhere to press start when paused
4480
4673
  touchGamepadButtons[9] = 1;
4481
-
4482
- // call default touch handler so normal touch events still work
4483
- handleTouchDefault(e);
4484
4674
  return;
4485
4675
  }
4486
4676
  }
@@ -4511,12 +4701,6 @@ function touchInputInit()
4511
4701
  touchGamepadButtons[9] = 1;
4512
4702
  }
4513
4703
  }
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
4704
  }
4521
4705
  }
4522
4706
 
@@ -4526,7 +4710,7 @@ function touchGamepadRender()
4526
4710
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
4527
4711
  if (!touchGamepadEnable || !touchGamepadTimer.isSet())
4528
4712
  return;
4529
-
4713
+
4530
4714
  // fade off when not touching or paused
4531
4715
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
4532
4716
  if (!alpha || paused)
@@ -4557,11 +4741,11 @@ function touchGamepadRender()
4557
4741
  const angle = i*PI/4;
4558
4742
  context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
4559
4743
  i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
4560
- i==1 && context.fill();
4744
+ i===1 && context.fill();
4561
4745
  }
4562
4746
  context.stroke();
4563
4747
  }
4564
-
4748
+
4565
4749
  // draw right face buttons
4566
4750
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4567
4751
  for (let i=4; i--;)
@@ -4596,8 +4780,8 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
4596
4780
  /** Check if pointer is locked (true if locked)
4597
4781
  * @return {boolean}
4598
4782
  * @memberof Input */
4599
- function pointerLockIsActive() { return document.pointerLockElement == mainCanvas; }
4600
- /**
4783
+ function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
4784
+ /**
4601
4785
  * LittleJS Audio System
4602
4786
  * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
4603
4787
  * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
@@ -4618,10 +4802,21 @@ let audioContext = new AudioContext;
4618
4802
  * @memberof Audio */
4619
4803
  let audioMasterGain;
4620
4804
 
4805
+ /** Default sample rate used for sounds
4806
+ * @default 44100
4807
+ * @memberof Audio */
4808
+ const audioDefaultSampleRate = 44100;
4809
+
4810
+ /** Check if the audio context is running and available for playback
4811
+ * @return {boolean} - True if the audio context is running
4812
+ * @memberof Audio */
4813
+ function audioIsRunning()
4814
+ { return audioContext.state === 'running'; }
4815
+
4621
4816
  function audioInit()
4622
4817
  {
4623
4818
  if (!soundEnable || headlessMode) return;
4624
-
4819
+
4625
4820
  audioMasterGain = audioContext.createGain();
4626
4821
  audioMasterGain.connect(audioContext.destination);
4627
4822
  audioMasterGain.gain.value = soundVolume; // set starting value
@@ -4629,14 +4824,14 @@ function audioInit()
4629
4824
 
4630
4825
  ///////////////////////////////////////////////////////////////////////////////
4631
4826
 
4632
- /**
4827
+ /**
4633
4828
  * Sound Object - Stores a sound for later use and can be played positionally
4634
- *
4829
+ *
4635
4830
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4636
4831
  * @example
4637
4832
  * // create a sound
4638
4833
  * const sound_example = new Sound([.5,.5]);
4639
- *
4834
+ *
4640
4835
  * // play the sound
4641
4836
  * sound_example.play();
4642
4837
  */
@@ -4653,33 +4848,41 @@ class Sound
4653
4848
 
4654
4849
  /** @property {number} - World space max range of sound */
4655
4850
  this.range = range;
4656
-
4657
4851
  /** @property {number} - At what percentage of range should it start tapering */
4658
4852
  this.taper = taper;
4659
-
4660
4853
  /** @property {number} - How much to randomize frequency each time sound plays */
4661
4854
  this.randomness = 0;
4855
+ /** @property {number} - Sample rate for this sound */
4856
+ this.sampleRate = audioDefaultSampleRate;
4857
+ /** @property {number} - Percentage of this sound currently loaded */
4858
+ this.loadedPercent = 0;
4662
4859
 
4860
+ // generate zzfx sound now for fast playback
4663
4861
  if (zzfxSound)
4664
4862
  {
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
4863
+ // remove randomness so it can be applied on playback
4864
+ const randomnessIndex = 1, defaultRandomness = .05;
4865
+ this.randomness = zzfxSound[randomnessIndex] !== undefined ?
4866
+ zzfxSound[randomnessIndex] : defaultRandomness;
4867
+ zzfxSound[randomnessIndex] = 0;
4868
+
4869
+ // generate the zzfx samples
4669
4870
  this.sampleChannels = [zzfxG(...zzfxSound)];
4670
- this.sampleRate = zzfxR;
4871
+ this.loadedPercent = 1;
4671
4872
  }
4672
4873
  }
4673
4874
 
4674
4875
  /** 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
4876
+ * Sounds may not play until a user interaction occurs
4877
+ * @param {Vector2} [pos] - World space position to play the sound if any
4878
+ * @param {number} [volume] - How much to scale volume by
4879
+ * @param {number} [pitch] - How much to scale pitch by
4880
+ * @param {number} [randomnessScale] - How much to scale pitch randomness
4881
+ * @param {boolean} [loop] - Should the sound loop?
4882
+ * @param {boolean} [paused] - Should the sound start paused
4883
+ * @return {SoundInstance} - The audio source node
4681
4884
  */
4682
- play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
4885
+ play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
4683
4886
  {
4684
4887
  if (!soundEnable || headlessMode) return;
4685
4888
  if (!this.sampleChannels) return;
@@ -4702,75 +4905,55 @@ class Sound
4702
4905
  // get pan from screen space coords
4703
4906
  pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
4704
4907
  }
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
4908
 
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;
4909
+ // Create and return sound instance
4910
+ const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
4911
+ return new SoundInstance(this, volume, rate, pan, loop, paused);
4737
4912
  }
4738
4913
 
4739
- /** Get source of most recent instance of this sound that was played
4740
- * @return {AudioBufferSourceNode}
4914
+ /** Play a music track that loops by default
4915
+ * @param {number} [volume] - Volume to play the music at
4916
+ * @param {boolean} [loop] - Should the music loop?
4917
+ * @param {boolean} [paused] - Should the music start paused
4918
+ * @return {SoundInstance} - The audio source node
4741
4919
  */
4742
- getSource() { return this.source; }
4920
+ playMusic(volume=1, loop=true, paused=false)
4921
+ { return this.play(undefined, volume, 1, 0, loop, paused); }
4743
4922
 
4744
- /** Play the sound as a note with a semitone offset
4923
+ /** Play the sound as a musical note with a semitone offset
4924
+ * This can be used to play music with chromatic scales
4745
4925
  * @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
4926
+ * @param {Vector2} [pos] - World space position to play the sound if any
4927
+ * @param {number} [volume=1] - How much to scale volume by
4928
+ * @return {SoundInstance} - The audio source node
4749
4929
  */
4750
4930
  playNote(semitoneOffset, pos, volume)
4751
- { return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
4931
+ {
4932
+ const pitch = getNoteFrequency(semitoneOffset, 1);
4933
+ return this.play(pos, volume, pitch, 0);
4934
+ }
4752
4935
 
4753
4936
  /** Get how long this sound is in seconds
4754
4937
  * @return {number} - How long the sound is in seconds (undefined if loading)
4755
4938
  */
4756
- getDuration()
4939
+ getDuration()
4757
4940
  { 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
4941
+
4942
+ /** Check if sound is loaded, for sounds fetched from a url
4943
+ * @return {boolean} - True if sound is loaded and ready to play
4761
4944
  */
4762
- isLoading() { return !this.sampleChannels; }
4945
+ isLoaded() { return this.loadedPercent === 1; }
4763
4946
  }
4764
4947
 
4765
4948
  ///////////////////////////////////////////////////////////////////////////////
4766
4949
 
4767
- /**
4950
+ /**
4768
4951
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
4769
4952
  * - this can be used to play wave, mp3, and ogg files
4770
4953
  * @example
4771
4954
  * // create a sound
4772
4955
  * const sound_example = new SoundWave('sound.mp3');
4773
- *
4956
+ *
4774
4957
  * // play the sound
4775
4958
  * sound_example.play();
4776
4959
  */
@@ -4799,29 +4982,205 @@ class SoundWave extends Sound
4799
4982
  * @return {Promise<void>} */
4800
4983
  async loadSound(filename)
4801
4984
  {
4802
- const response = await fetch(filename);
4803
- const arrayBuffer = await response.arrayBuffer();
4804
- 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));
4808
- this.sampleRate = audioBuffer.sampleRate;
4809
- if (this.onloadCallback)
4810
- this.onloadCallback();
4985
+ const response = await fetch(filename);
4986
+ const arrayBuffer = await response.arrayBuffer();
4987
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
4988
+
4989
+ // convert audio buffer to sample channels across multiple frames
4990
+ const channelCount = audioBuffer.numberOfChannels;
4991
+ const samplesPerFrame = 1e5;
4992
+ const sampleChannels = [];
4993
+ for (let channel = 0; channel < channelCount; channel++)
4994
+ {
4995
+ const channelData = audioBuffer.getChannelData(channel);
4996
+ const channelLength = channelData.length;
4997
+ sampleChannels[channel] = new Array(channelLength);
4998
+ let sampleIndex = 0;
4999
+ while (sampleIndex < channelLength)
5000
+ {
5001
+ // yield to next frame
5002
+ await new Promise(resolve => setTimeout(resolve, 0));
5003
+
5004
+ // copy chunk of samples
5005
+ const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
5006
+ for (; sampleIndex < endIndex; sampleIndex++)
5007
+ sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
5008
+
5009
+ // update loaded percent
5010
+ const samplesTotal = channelCount * channelLength;
5011
+ const samplesProcessed = channel * channelLength + sampleIndex;
5012
+ this.loadedPercent = samplesProcessed / samplesTotal;
5013
+ }
5014
+ }
5015
+
5016
+ // setup the sound to be played
5017
+ this.sampleRate = audioBuffer.sampleRate;
5018
+ this.sampleChannels = sampleChannels;
5019
+ this.loadedPercent = 1;
5020
+ if (this.onloadCallback)
5021
+ this.onloadCallback();
5022
+ }
5023
+ }
5024
+
5025
+ ///////////////////////////////////////////////////////////////////////////////
5026
+
5027
+ /**
5028
+ * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
5029
+ * Represents a single playing instance of a sound with pause/resume capabilities
5030
+ * @example
5031
+ * // Play a sound and get an instance for control
5032
+ * const jumpSound = new Sound([.5,.5,220]);
5033
+ * const instance = jumpSound.play();
5034
+ *
5035
+ * // Control the individual instance
5036
+ * instance.setVolume(.5);
5037
+ * instance.pause();
5038
+ * instance.unpause();
5039
+ * instance.stop();
5040
+ */
5041
+ class SoundInstance
5042
+ {
5043
+ /** Create a sound instance
5044
+ * @param {Sound} sound - The sound object
5045
+ * @param {number} [volume] - How much to scale volume by
5046
+ * @param {number} [rate] - The playback rate to use
5047
+ * @param {number} [pan] - How much to apply stereo panning
5048
+ * @param {boolean} [loop] - Should the sound loop?
5049
+ * @param {boolean} [paused] - Should the sound start paused? */
5050
+ constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
5051
+ {
5052
+ ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
5053
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
5054
+ ASSERT(rate >= 0, 'Sound rate must be positive or zero');
5055
+ ASSERT(isNumber(pan), 'Sound pan must be a number');
5056
+
5057
+ /** @property {Sound} - The sound object */
5058
+ this.sound = sound;
5059
+ /** @property {number} - How much to scale volume by */
5060
+ this.volume = volume;
5061
+ /** @property {number} - The playback rate to use */
5062
+ this.rate = rate;
5063
+ /** @property {number} - How much to apply stereo panning */
5064
+ this.pan = pan;
5065
+ /** @property {boolean} - Should the sound loop */
5066
+ this.loop = loop;
5067
+ /** @property {number} - Timestamp for audio context when paused */
5068
+ this.pausedTime = 0;
5069
+ /** @property {number} - Timestamp for audio context when started */
5070
+ this.startTime = undefined;
5071
+ /** @property {GainNode} - Gain node for the sound */
5072
+ this.gainNode = undefined;
5073
+ /** @property {AudioBufferSourceNode} - Source node of the audio */
5074
+ this.source = undefined;
5075
+ // setup end callback and start sound
5076
+ this.onendedCallback = (source)=>
5077
+ {
5078
+ if (source === this.source)
5079
+ this.source = undefined;
5080
+ };
5081
+ if (!paused)
5082
+ this.start();
5083
+ }
5084
+
5085
+ /** Start playing the sound instance from the offset time
5086
+ * @param {number} [offset] - Offset in seconds to start playback from
5087
+ */
5088
+ start(offset=0)
5089
+ {
5090
+ ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
5091
+ if (this.isPlaying())
5092
+ this.stop();
5093
+ this.gainNode = audioContext.createGain();
5094
+ this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
5095
+ this.startTime = audioContext.currentTime - offset;
5096
+ this.pausedTime = undefined;
5097
+ }
5098
+
5099
+ /** Set the volume of this sound instance
5100
+ * @param {number} volume */
5101
+ setVolume(volume)
5102
+ {
5103
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
5104
+ this.volume = volume;
5105
+ if (this.gainNode)
5106
+ this.gainNode.gain.value = volume;
5107
+ }
5108
+
5109
+ /** Stop this sound instance and reset position to the start */
5110
+ stop(fadeTime=0)
5111
+ {
5112
+ ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
5113
+ if (this.isPlaying())
5114
+ {
5115
+ if (fadeTime)
5116
+ {
5117
+ // ramp off gain
5118
+ const startFade = audioContext.currentTime;
5119
+ const endFade = startFade + fadeTime;
5120
+ this.gainNode.gain.linearRampToValueAtTime(1, startFade);
5121
+ this.gainNode.gain.linearRampToValueAtTime(0, endFade);
5122
+ this.source.stop(endFade);
5123
+ }
5124
+ else
5125
+ this.source.stop();
5126
+ }
5127
+ this.pausedTime = 0;
5128
+ this.source = undefined;
5129
+ this.startTime = undefined;
5130
+ }
5131
+
5132
+ /** Pause this sound instance */
5133
+ pause()
5134
+ {
5135
+ if (this.isPaused())
5136
+ return;
5137
+
5138
+ // save current time and stop sound
5139
+ this.pausedTime = this.getCurrentTime();
5140
+ this.source.stop();
5141
+ this.source = undefined;
5142
+ this.startTime = undefined;
5143
+ }
5144
+
5145
+ /** Unpauses this sound instance */
5146
+ resume()
5147
+ {
5148
+ if (!this.isPaused())
5149
+ return;
5150
+
5151
+ // restart sound from paused time
5152
+ this.start(this.pausedTime);
5153
+ }
5154
+
5155
+ /** Check if this instance is currently playing
5156
+ * @return {boolean} - True if playing
5157
+ */
5158
+ isPlaying() { return !!this.source; }
5159
+
5160
+ /** Check if this instance is paused and was not stopped
5161
+ * @return {boolean} - True if paused
5162
+ */
5163
+ isPaused() { return !this.isPlaying(); }
5164
+
5165
+ /** Get the current playback time in seconds
5166
+ * @return {number} - Current playback time
5167
+ */
5168
+ getCurrentTime()
5169
+ {
5170
+ const deltaTime = mod(audioContext.currentTime - this.startTime,
5171
+ this.getDuration());
5172
+ return this.isPlaying() ? deltaTime : this.pausedTime;
4811
5173
  }
4812
- }
4813
5174
 
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;
5175
+ /** Get the total duration of this sound
5176
+ * @return {number} - Total duration in seconds
5177
+ */
5178
+ getDuration() { return this.sound.getDuration() / this.rate; }
4823
5179
 
4824
- return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
5180
+ /** Get source of this sound instance
5181
+ * @return {AudioBufferSourceNode}
5182
+ */
5183
+ getSource() { return this.source; }
4825
5184
  }
4826
5185
 
4827
5186
  ///////////////////////////////////////////////////////////////////////////////
@@ -4875,9 +5234,11 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4875
5234
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
4876
5235
  * @param {number} [sampleRate=44100] - Sample rate for the sound
4877
5236
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
5237
+ * @param {number} [offset] - Offset in seconds to start playback from
5238
+ * @param {Function} [onended] - Callback for when the sound ends
4878
5239
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4879
5240
  * @memberof Audio */
4880
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR, gainNode)
5241
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
4881
5242
  {
4882
5243
  if (!soundEnable || headlessMode) return;
4883
5244
 
@@ -4902,16 +5263,20 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4902
5263
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
4903
5264
  source.connect(pannerNode).connect(gainNode);
4904
5265
 
4905
- // play the sound
4906
- if (audioContext.state != 'running')
5266
+ // callback when the sound ends
5267
+ if (onended)
5268
+ source.addEventListener('ended', ()=> onended(source));
5269
+
5270
+ if (!audioIsRunning())
4907
5271
  {
4908
- // fix stalled audio and play
4909
- audioContext.resume().then(()=>source.start());
5272
+ // fix stalled audio, this sound won't be able to play
5273
+ audioContext.resume();
5274
+ return;
4910
5275
  }
4911
- else
4912
- source.start();
4913
5276
 
4914
- // return sound
5277
+ // play and return sound
5278
+ const startOffset = offset * rate;
5279
+ source.start(0, startOffset);
4915
5280
  return source;
4916
5281
  }
4917
5282
 
@@ -4919,18 +5284,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4919
5284
  // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
4920
5285
 
4921
5286
  /** Generate and play a ZzFX sound
4922
- *
5287
+ *
4923
5288
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4924
5289
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
4925
5290
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4926
5291
  * @memberof Audio */
4927
5292
  function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
4928
5293
 
4929
- /** Sample rate used for all ZzFX sounds
4930
- * @default 44100
4931
- * @memberof Audio */
4932
- const zzfxR = 44100;
4933
-
4934
5294
  /** Generate samples for a ZzFX sound
4935
5295
  * @param {number} [volume] - Volume scale (percent)
4936
5296
  * @param {number} [randomness] - How much to randomize frequency (percent Hz)
@@ -4954,11 +5314,10 @@ const zzfxR = 44100;
4954
5314
  * @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
4955
5315
  * @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
4956
5316
  * @return {Array} - Array of audio samples
4957
- * @memberof Audio
4958
- */
5317
+ * @memberof Audio */
4959
5318
  function zzfxG
4960
5319
  (
4961
- volume = 1,
5320
+ volume = 1,
4962
5321
  randomness = .05,
4963
5322
  frequency = 220,
4964
5323
  attack = 0,
@@ -4966,11 +5325,11 @@ function zzfxG
4966
5325
  release = .1,
4967
5326
  shape = 0,
4968
5327
  shapeCurve = 1,
4969
- slide = 0,
4970
- deltaSlide = 0,
4971
- pitchJump = 0,
4972
- pitchJumpTime = 0,
4973
- repeatTime = 0,
5328
+ slide = 0,
5329
+ deltaSlide = 0,
5330
+ pitchJump = 0,
5331
+ pitchJumpTime = 0,
5332
+ repeatTime = 0,
4974
5333
  noise = 0,
4975
5334
  modulation = 0,
4976
5335
  bitCrush = 0,
@@ -4982,19 +5341,19 @@ function zzfxG
4982
5341
  )
4983
5342
  {
4984
5343
  // init parameters
4985
- let sampleRate = zzfxR,
4986
- PI2 = PI*2,
5344
+ let sampleRate = audioDefaultSampleRate,
5345
+ PI2 = PI*2,
4987
5346
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
4988
- startFrequency = frequency *=
5347
+ startFrequency = frequency *=
4989
5348
  (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
4990
- modOffset = 0, // modulation offset
5349
+ modOffset = 0, // modulation offset
4991
5350
  repeat = 0, // repeat offset
4992
5351
  crush = 0, // bit crush offset
4993
5352
  jump = 1, // pitch jump timer
4994
5353
  length, // sample length
4995
5354
  b = [], // sample buffer
4996
5355
  t = 0, // sample time
4997
- i = 0, // sample index
5356
+ i = 0, // sample index
4998
5357
  s = 0, // sample value
4999
5358
  f, // wave frequency
5000
5359
 
@@ -5002,7 +5361,7 @@ function zzfxG
5002
5361
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
5003
5362
  cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
5004
5363
  a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
5005
- b0 = (1 + sign(filter) * cos) / 2 / a0,
5364
+ b0 = (1 + sign(filter) * cos) / 2 / a0,
5006
5365
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
5007
5366
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
5008
5367
 
@@ -5048,7 +5407,7 @@ function zzfxG
5048
5407
  0); // post release
5049
5408
 
5050
5409
  s = delay ? s/2 + (delay > i ? 0 : // delay
5051
- (i<length-delay? 1 : (length-i)/delay) * // release delay
5410
+ (i<length-delay? 1 : (length-i)/delay) * // release delay
5052
5411
  b[i-delay|0]/2/volume) : s; // sample delay
5053
5412
 
5054
5413
  if (filter) // apply filter
@@ -5060,14 +5419,14 @@ function zzfxG
5060
5419
  t += f + f*noise*Math.sin(i**5); // noise
5061
5420
 
5062
5421
  if (jump && ++jump > pitchJumpTime) // pitch jump
5063
- {
5422
+ {
5064
5423
  frequency += pitchJump; // apply pitch jump
5065
5424
  startFrequency += pitchJump; // also apply to start
5066
5425
  jump = 0; // stop pitch jump time
5067
- }
5426
+ }
5068
5427
 
5069
5428
  if (repeatTime && !(++repeat % repeatTime)) // repeat
5070
- {
5429
+ {
5071
5430
  frequency = startFrequency; // reset frequency
5072
5431
  slide = startSlide; // reset slide
5073
5432
  jump ||= 1; // reset pitch jump time
@@ -5076,12 +5435,11 @@ function zzfxG
5076
5435
 
5077
5436
  return b; // return sample buffer
5078
5437
  }
5079
- /**
5438
+ /**
5080
5439
  * LittleJS Tile Layer System
5081
5440
  * - Caches arrays of tiles to off screen canvas for fast rendering
5082
5441
  * - Unlimited numbers of layers, allocates canvases as needed
5083
5442
  * - Tile layers can be drawn to using their context with canvas2d
5084
- * - Drawn directly to the main canvas without using WebGL
5085
5443
  * - Tile layers can also have collision with EngineObjects
5086
5444
  * @namespace TileCollision
5087
5445
  */
@@ -5090,9 +5448,9 @@ function zzfxG
5090
5448
  // Tile Layer System
5091
5449
 
5092
5450
  /** Keep track of all tile layers with collision
5093
- * @type {Array<TileCollisionLayer>}
5451
+ * @type {Array<TileCollisionLayer>}
5094
5452
  * @memberof TileCollision */
5095
- let tileCollisionLayers = [];
5453
+ const tileCollisionLayers = [];
5096
5454
 
5097
5455
  /** Get tile collision data for a given cell in the grid
5098
5456
  * @param {Vector2} pos
@@ -5146,7 +5504,7 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5146
5504
  }
5147
5505
 
5148
5506
  ///////////////////////////////////////////////////////////////////////////////
5149
- /**
5507
+ /**
5150
5508
  * Load tile layers from exported data
5151
5509
  * @param {Object} tileMapData - Level data from exported data
5152
5510
  * @param {TileInfo} [tileInfo] - Default tile info (used for size and texture)
@@ -5179,13 +5537,13 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5179
5537
  {
5180
5538
  const dataLayer = tileMapData.layers[layerIndex];
5181
5539
  ASSERT(dataLayer.data && dataLayer.data.length);
5182
- ASSERT(levelSize.area() == dataLayer.data.length);
5540
+ ASSERT(levelSize.area() === dataLayer.data.length);
5183
5541
 
5184
5542
  const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
5185
5543
  const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
5186
5544
  tileLayers[layerIndex] = tileLayer;
5187
5545
 
5188
- for (let x=levelSize.x; x--;)
5546
+ for (let x=levelSize.x; x--;)
5189
5547
  for (let y=levelSize.y; y--;)
5190
5548
  {
5191
5549
  const pos = vec2(x, levelSize.y-1-y);
@@ -5244,7 +5602,7 @@ class TileLayerData
5244
5602
  /**
5245
5603
  * Canvas Layer - cached off screen rendering system
5246
5604
  * - Contains an offscreen canvas that can be rendered to
5247
- * - Webgl rendering is optional, call useWebGL to enable
5605
+ * - WebGL rendering is optional, call useWebGL to enable
5248
5606
  * @extends EngineObject
5249
5607
  * @example
5250
5608
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
@@ -5266,18 +5624,18 @@ class CanvasLayer extends EngineObject
5266
5624
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
5267
5625
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5268
5626
  this.context = headlessMode ? undefined : this.canvas.getContext('2d');
5269
- /** @property {WebGLTexture} - Texture if using webgl for this layer, call useWebGL to enable */
5627
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
5270
5628
  this.glTexture = undefined;
5271
5629
  this.gravityScale = 0; // disable gravity by default for canvas layers
5272
5630
  }
5273
-
5631
+
5274
5632
  /** Destroy this canvas layer */
5275
5633
  destroy()
5276
5634
  {
5277
5635
  if (this.destroyed)
5278
5636
  return;
5279
5637
 
5280
- // free up the webgl texture
5638
+ // free up the WebGL texture
5281
5639
  if (this.glTexture)
5282
5640
  glDeleteTexture(this.glTexture);
5283
5641
  super.destroy();
@@ -5302,12 +5660,12 @@ class CanvasLayer extends EngineObject
5302
5660
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5303
5661
  {
5304
5662
  // draw the canvas layer as a single tile that uses the whole texture
5305
- const useWebgl = glEnable && this.glTexture != undefined;
5663
+ const useWebGL = glEnable && this.glTexture !== undefined;
5306
5664
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5307
- drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebgl, screenSpace, context);
5665
+ drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5308
5666
  }
5309
5667
 
5310
- /** Draw onto the layer canvas in world space (bypass webgl)
5668
+ /** Draw onto the layer canvas in world space (bypass WebGL)
5311
5669
  * @param {Vector2} pos
5312
5670
  * @param {Vector2} size
5313
5671
  * @param {number} angle
@@ -5341,8 +5699,8 @@ class CanvasLayer extends EngineObject
5341
5699
  if (textureInfo)
5342
5700
  {
5343
5701
  context.globalAlpha = color.a; // only alpha is supported
5344
- context.drawImage(textureInfo.image,
5345
- tileInfo.pos.x, tileInfo.pos.y,
5702
+ context.drawImage(textureInfo.image,
5703
+ tileInfo.pos.x, tileInfo.pos.y,
5346
5704
  tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
5347
5705
  context.globalAlpha = 1;
5348
5706
  }
@@ -5360,11 +5718,11 @@ class CanvasLayer extends EngineObject
5360
5718
  * @param {Vector2} [size=(1,1)]
5361
5719
  * @param {Color} [color=(1,1,1,1)]
5362
5720
  * @param {number} [angle=0] */
5363
- drawRect(pos, size, color, angle)
5721
+ drawRect(pos, size, color, angle)
5364
5722
  { this.drawTile(pos, size, undefined, color, angle); }
5365
5723
 
5366
- /** Create or update the webgl texture for this layer
5367
- * @param {boolean} [enable] - enable webgl rendering and update the texture */
5724
+ /** Create or update the WebGL texture for this layer
5725
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5368
5726
  useWebGL(enable=true)
5369
5727
  {
5370
5728
  if (glEnable && enable)
@@ -5410,7 +5768,7 @@ class TileLayer extends CanvasLayer
5410
5768
  this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
5411
5769
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
5412
5770
  this.context = this.canvas.getContext('2d');
5413
- /** @property {WebGLTexture} - Texture if using webgl for this layer */
5771
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer */
5414
5772
  this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
5415
5773
  // set no friction by default, applied friction is max of both objects
5416
5774
  this.friction = 0;
@@ -5435,7 +5793,7 @@ class TileLayer extends CanvasLayer
5435
5793
  }
5436
5794
  }
5437
5795
 
5438
- /** Set data at a given position in the array
5796
+ /** Set data at a given position in the array
5439
5797
  * @param {Vector2} layerPos - Local position in array
5440
5798
  * @param {TileLayerData} data - Data to set
5441
5799
  * @param {boolean} [redraw] - Force the tile to redraw if true */
@@ -5447,26 +5805,26 @@ class TileLayer extends CanvasLayer
5447
5805
  redraw && this.drawTileData(layerPos);
5448
5806
  }
5449
5807
  }
5450
-
5451
- /** Get data at a given position in the array
5808
+
5809
+ /** Get data at a given position in the array
5452
5810
  * @param {Vector2} layerPos - Local position in array
5453
5811
  * @return {TileLayerData} */
5454
5812
  getData(layerPos)
5455
5813
  { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
5456
-
5814
+
5457
5815
  // Render the tile layer, called automatically by the engine
5458
5816
  render()
5459
5817
  {
5460
- ASSERT(drawContext != this.context, 'must call redrawEnd() after drawing tiles!');
5461
-
5818
+ ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5819
+
5462
5820
  // draw the tile layer as a single tile
5463
5821
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5464
5822
  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);
5823
+ const useWebGL = glEnable && this.glTexture !== undefined;
5824
+ drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5467
5825
  }
5468
5826
 
5469
- /** Draw all the tile data to an offscreen canvas
5827
+ /** Draw all the tile data to an offscreen canvas
5470
5828
  * - This may be slow in some browsers but only needs to be done once */
5471
5829
  redraw()
5472
5830
  {
@@ -5476,7 +5834,7 @@ class TileLayer extends CanvasLayer
5476
5834
  this.drawTileData(vec2(x,y), false);
5477
5835
  this.redrawEnd();
5478
5836
  if (this.glTexture)
5479
- this.useWebGL(); // update webgl texture
5837
+ this.useWebGL(); // update WebGL texture
5480
5838
  }
5481
5839
 
5482
5840
  /** Call to start the redraw process
@@ -5512,7 +5870,7 @@ class TileLayer extends CanvasLayer
5512
5870
  /** Call to end the redraw process */
5513
5871
  redrawEnd()
5514
5872
  {
5515
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5873
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
5516
5874
  glCopyToContext(drawContext);
5517
5875
  //debugSaveCanvas(this.canvas);
5518
5876
 
@@ -5523,7 +5881,7 @@ class TileLayer extends CanvasLayer
5523
5881
  /** Draw the tile at a given position in the tile grid
5524
5882
  * This can be used to clear out tiles when they are destroyed
5525
5883
  * Tiles can also be redrawn if inside a redrawStart/End block
5526
- * @param {Vector2} layerPos
5884
+ * @param {Vector2} layerPos
5527
5885
  * @param {boolean} [clear] - should the old tile be cleared out
5528
5886
  */
5529
5887
  drawTileData(layerPos, clear=true)
@@ -5538,9 +5896,9 @@ class TileLayer extends CanvasLayer
5538
5896
 
5539
5897
  // draw the tile if it has layer data
5540
5898
  const d = this.getData(layerPos);
5541
- if (d.tile != undefined)
5899
+ if (d.tile !== undefined)
5542
5900
  {
5543
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5901
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
5544
5902
  const pos = layerPos.add(vec2(.5));
5545
5903
  const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
5546
5904
  drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
@@ -5553,7 +5911,7 @@ class TileLayer extends CanvasLayer
5553
5911
  * Tile Collision Layer - a tile layer with collision
5554
5912
  * - adds collision data and functions to TileLayer
5555
5913
  * - there can be multiple tile collision layers
5556
- * - tile collison layers should not overlap each other
5914
+ * - tile collision layers should not overlap each other
5557
5915
  * @extends TileLayer
5558
5916
  */
5559
5917
  class TileCollisionLayer extends TileLayer
@@ -5704,7 +6062,7 @@ class TileCollisionLayer extends TileLayer
5704
6062
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
5705
6063
  }
5706
6064
  }
5707
- /**
6065
+ /**
5708
6066
  * LittleJS Particle System
5709
6067
  */
5710
6068
 
@@ -5721,7 +6079,7 @@ class TileCollisionLayer extends TileLayer
5721
6079
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
5722
6080
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
5723
6081
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5724
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
6082
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5725
6083
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
5726
6084
  * );
5727
6085
  */
@@ -5729,35 +6087,35 @@ class ParticleEmitter extends EngineObject
5729
6087
  {
5730
6088
  /** Create a particle system with the given settings
5731
6089
  * @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
6090
+ * @param {number} [angle] - Angle to emit the particles
6091
+ * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
6092
+ * @param {number} [emitTime] - How long to stay alive (0 is forever)
6093
+ * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
6094
+ * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5737
6095
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
5738
6096
  * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
5739
6097
  * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
5740
6098
  * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
5741
6099
  * @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
6100
+ * @param {number} [particleTime] - How long particles live
6101
+ * @param {number} [sizeStart] - How big are particles at start
6102
+ * @param {number} [sizeEnd] - How big are particles at end
6103
+ * @param {number} [speed] - How fast are particles when spawned
6104
+ * @param {number} [angleSpeed] - How fast are particles rotating
6105
+ * @param {number} [damping] - How much to dampen particle speed
6106
+ * @param {number} [angleDamping] - How much to dampen particle angular speed
6107
+ * @param {number} [gravityScale] - How much gravity effect particles
6108
+ * @param {number} [particleConeAngle] - Cone for start particle angle
6109
+ * @param {number} [fadeRate] - How quick to fade particles at start/end in percent of life
6110
+ * @param {number} [randomness] - Apply extra randomness percent
5753
6111
  * @param {boolean} [collideTiles] - Do particles collide against tiles
5754
6112
  * @param {boolean} [additive] - Should particles use additive blend
5755
6113
  * @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)
6114
+ * @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
5757
6115
  * @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
5758
6116
  */
5759
6117
  constructor
5760
- (
6118
+ (
5761
6119
  position,
5762
6120
  angle,
5763
6121
  emitSize = 0,
@@ -5779,7 +6137,7 @@ class ParticleEmitter extends EngineObject
5779
6137
  gravityScale = 0,
5780
6138
  particleConeAngle = PI,
5781
6139
  fadeRate = .1,
5782
- randomness = .2,
6140
+ randomness = .2,
5783
6141
  collideTiles = false,
5784
6142
  additive = false,
5785
6143
  randomColorLinear = true,
@@ -5790,13 +6148,13 @@ class ParticleEmitter extends EngineObject
5790
6148
  super(position, vec2(), tileInfo, angle, undefined, renderOrder);
5791
6149
 
5792
6150
  // emitter settings
5793
- /** @property {Number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
6151
+ /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5794
6152
  this.emitSize = emitSize
5795
- /** @property {Number} - How long to stay alive (0 is forever) */
6153
+ /** @property {number} - How long to stay alive (0 is forever) */
5796
6154
  this.emitTime = emitTime;
5797
- /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
6155
+ /** @property {number} - How many particles per second to spawn, does not emit if 0 */
5798
6156
  this.emitRate = emitRate;
5799
- /** @property {Number} - Local angle to apply velocity to particles from emitter */
6157
+ /** @property {number} - Local angle to apply velocity to particles from emitter */
5800
6158
  this.emitConeAngle = emitConeAngle;
5801
6159
 
5802
6160
  // color settings
@@ -5812,27 +6170,27 @@ class ParticleEmitter extends EngineObject
5812
6170
  this.randomColorLinear = randomColorLinear;
5813
6171
 
5814
6172
  // particle settings
5815
- /** @property {Number} - How long particles live */
6173
+ /** @property {number} - How long particles live */
5816
6174
  this.particleTime = particleTime;
5817
- /** @property {Number} - How big are particles at start */
6175
+ /** @property {number} - How big are particles at start */
5818
6176
  this.sizeStart = sizeStart;
5819
- /** @property {Number} - How big are particles at end */
6177
+ /** @property {number} - How big are particles at end */
5820
6178
  this.sizeEnd = sizeEnd;
5821
- /** @property {Number} - How fast are particles when spawned */
6179
+ /** @property {number} - How fast are particles when spawned */
5822
6180
  this.speed = speed;
5823
- /** @property {Number} - How fast are particles rotating */
6181
+ /** @property {number} - How fast are particles rotating */
5824
6182
  this.angleSpeed = angleSpeed;
5825
- /** @property {Number} - How much to dampen particle speed */
6183
+ /** @property {number} - How much to dampen particle speed */
5826
6184
  this.damping = damping;
5827
- /** @property {Number} - How much to dampen particle angular speed */
6185
+ /** @property {number} - How much to dampen particle angular speed */
5828
6186
  this.angleDamping = angleDamping;
5829
- /** @property {Number} - How much does gravity effect particles */
6187
+ /** @property {number} - How much gravity affects particles */
5830
6188
  this.gravityScale = gravityScale;
5831
- /** @property {Number} - Cone for start particle angle */
6189
+ /** @property {number} - Cone for start particle angle */
5832
6190
  this.particleConeAngle = particleConeAngle;
5833
- /** @property {Number} - How quick to fade in particles at start/end in percent of life */
6191
+ /** @property {number} - How quick to fade in particles at start/end in percent of life */
5834
6192
  this.fadeRate = fadeRate;
5835
- /** @property {Number} - Apply extra randomness percent */
6193
+ /** @property {number} - Apply extra randomness percent */
5836
6194
  this.randomness = randomness;
5837
6195
  /** @property {boolean} - Do particles collide against tiles */
5838
6196
  this.collideTiles = collideTiles;
@@ -5840,16 +6198,16 @@ class ParticleEmitter extends EngineObject
5840
6198
  this.additive = additive;
5841
6199
  /** @property {boolean} - Should it be in local space of emitter */
5842
6200
  this.localSpace = localSpace;
5843
- /** @property {Number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
6201
+ /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5844
6202
  this.trailScale = 0;
5845
6203
  /** @property {Function} - Callback when particle is destroyed */
5846
6204
  this.particleDestroyCallback = undefined;
5847
6205
  /** @property {Function} - Callback when particle is created */
5848
6206
  this.particleCreateCallback = undefined;
5849
- /** @property {Number} - Track particle emit time */
6207
+ /** @property {number} - Track particle emit time */
5850
6208
  this.emitTimeBuffer = 0;
5851
6209
  }
5852
-
6210
+
5853
6211
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
5854
6212
  update()
5855
6213
  {
@@ -5873,7 +6231,7 @@ class ParticleEmitter extends EngineObject
5873
6231
  if (debugParticles)
5874
6232
  {
5875
6233
  // show emitter bounds
5876
- const emitSize = typeof this.emitSize == 'number' ? vec2(this.emitSize) : this.emitSize;
6234
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
5877
6235
  debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
5878
6236
  }
5879
6237
  }
@@ -5883,7 +6241,7 @@ class ParticleEmitter extends EngineObject
5883
6241
  emitParticle()
5884
6242
  {
5885
6243
  // spawn a particle
5886
- let pos = typeof this.emitSize == 'number' ? // check if number was used
6244
+ let pos = typeof this.emitSize === 'number' ? // check if number was used
5887
6245
  randInCircle(this.emitSize/2) // circle emitter
5888
6246
  : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
5889
6247
  .multiply(this.emitSize).rotate(this.angle)
@@ -5908,7 +6266,7 @@ class ParticleEmitter extends EngineObject
5908
6266
  const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
5909
6267
  const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
5910
6268
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
5911
-
6269
+
5912
6270
  // build particle
5913
6271
  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
6272
  particle.velocity = vec2().setAngle(velocityAngle, speed);
@@ -5953,38 +6311,38 @@ class Particle extends EngineObject
5953
6311
  * Typically this is created automatically by a ParticleEmitter
5954
6312
  * @param {Vector2} position - World space position of the particle
5955
6313
  * @param {TileInfo} tileInfo - Tile info to render particles
5956
- * @param {Number} angle - Angle to rotate the particle
6314
+ * @param {number} angle - Angle to rotate the particle
5957
6315
  * @param {Color} colorStart - Color at start of life
5958
6316
  * @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
6317
+ * @param {number} lifeTime - How long to live for
6318
+ * @param {number} sizeStart - Size at start of life
6319
+ * @param {number} sizeEnd - Size at end of life
6320
+ * @param {number} fadeRate - How quick to fade in/out
5963
6321
  * @param {boolean} additive - Does it use additive blend mode
5964
- * @param {Number} trailScale - If a trail, how long to make it
6322
+ * @param {number} trailScale - If a trail, how long to make it
5965
6323
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
5966
6324
  * @param {Function} [destroyCallback] - Callback when particle dies
5967
6325
  */
5968
6326
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
5969
6327
  )
5970
- {
5971
- super(position, vec2(), tileInfo, angle);
5972
-
6328
+ {
6329
+ super(position, vec2(), tileInfo, angle);
6330
+
5973
6331
  /** @property {Color} - Color at start of life */
5974
6332
  this.colorStart = colorStart;
5975
6333
  /** @property {Color} - Calculated change in color */
5976
6334
  this.colorEndDelta = colorEnd.subtract(colorStart);
5977
- /** @property {Number} - How long to live for */
6335
+ /** @property {number} - How long to live for */
5978
6336
  this.lifeTime = lifeTime;
5979
- /** @property {Number} - Size at start of life */
6337
+ /** @property {number} - Size at start of life */
5980
6338
  this.sizeStart = sizeStart;
5981
- /** @property {Number} - Calculated change in size */
6339
+ /** @property {number} - Calculated change in size */
5982
6340
  this.sizeEndDelta = sizeEnd - sizeStart;
5983
- /** @property {Number} - How quick to fade in/out */
6341
+ /** @property {number} - How quick to fade in/out */
5984
6342
  this.fadeRate = fadeRate;
5985
6343
  /** @property {boolean} - Is it additive */
5986
6344
  this.additive = additive;
5987
- /** @property {Number} - If a trail, how long to make it */
6345
+ /** @property {number} - If a trail, how long to make it */
5988
6346
  this.trailScale = trailScale;
5989
6347
  /** @property {ParticleEmitter} - Parent emitter if local space */
5990
6348
  this.localSpaceEmitter = localSpaceEmitter;
@@ -6024,7 +6382,7 @@ class Particle extends EngineObject
6024
6382
  this.colorStart.r + p * this.colorEndDelta.r,
6025
6383
  this.colorStart.g + p * this.colorEndDelta.g,
6026
6384
  this.colorStart.b + p * this.colorEndDelta.b,
6027
- (this.colorStart.a + p * this.colorEndDelta.a) *
6385
+ (this.colorStart.a + p * this.colorEndDelta.a) *
6028
6386
  (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
6029
6387
 
6030
6388
  // draw the particle
@@ -6034,7 +6392,7 @@ class Particle extends EngineObject
6034
6392
  if (this.localSpaceEmitter)
6035
6393
  {
6036
6394
  // in local space of emitter
6037
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
6395
+ pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
6038
6396
  angle += this.localSpaceEmitter.angle;
6039
6397
  }
6040
6398
  if (this.trailScale)
@@ -6058,7 +6416,7 @@ class Particle extends EngineObject
6058
6416
  this.additive && setBlendMode();
6059
6417
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
6060
6418
 
6061
- if (p == 1)
6419
+ if (p === 1)
6062
6420
  {
6063
6421
  // destroy particle when it's time runs out
6064
6422
  this.color = color;
@@ -6068,7 +6426,7 @@ class Particle extends EngineObject
6068
6426
  }
6069
6427
  }
6070
6428
  }
6071
- /**
6429
+ /**
6072
6430
  * LittleJS Medal System
6073
6431
  * - Tracks and displays medals
6074
6432
  * - Saves medals to local storage
@@ -6089,7 +6447,7 @@ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
6089
6447
  /** Initialize medals with a save name used for storage
6090
6448
  * - Call this after creating all medals
6091
6449
  * - Checks if medals are unlocked
6092
- * @param {String} saveName
6450
+ * @param {string} saveName
6093
6451
  * @memberof Medals */
6094
6452
  function medalsInit(saveName)
6095
6453
  {
@@ -6104,7 +6462,7 @@ function medalsInit(saveName)
6104
6462
  {
6105
6463
  if (!medalsDisplayQueue.length)
6106
6464
  return;
6107
-
6465
+
6108
6466
  // update first medal in queue
6109
6467
  const medal = medalsDisplayQueue[0];
6110
6468
  const time = timeReal - medalsDisplayTimeLast;
@@ -6119,7 +6477,7 @@ function medalsInit(saveName)
6119
6477
  {
6120
6478
  // slide on/off medals
6121
6479
  const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
6122
- const hidePercent =
6480
+ const hidePercent =
6123
6481
  time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
6124
6482
  time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
6125
6483
  medal.render(hidePercent);
@@ -6135,43 +6493,43 @@ function medalsForEach(callback)
6135
6493
 
6136
6494
  ///////////////////////////////////////////////////////////////////////////////
6137
6495
 
6138
- /**
6139
- * Medal - Tracks an unlockable medal
6496
+ /**
6497
+ * Medal - Tracks an unlockable medal
6140
6498
  * @example
6141
6499
  * // create a medal
6142
6500
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
6143
- *
6501
+ *
6144
6502
  * // initialize medals
6145
6503
  * medalsInit('Example Game');
6146
- *
6504
+ *
6147
6505
  * // unlock the medal
6148
6506
  * medal_example.unlock();
6149
6507
  */
6150
6508
  class Medal
6151
6509
  {
6152
6510
  /** 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
6511
+ * @param {number} id - The unique identifier of the medal
6512
+ * @param {string} name - Name of the medal
6513
+ * @param {string} [description] - Description of the medal
6514
+ * @param {string} [icon] - Icon for the medal
6515
+ * @param {string} [src] - Image location for the medal
6158
6516
  */
6159
6517
  constructor(id, name, description='', icon='🏆', src)
6160
6518
  {
6161
6519
  ASSERT(id >= 0 && !medals[id]);
6162
-
6163
- /** @property {Number} - The unique identifier of the medal */
6520
+
6521
+ /** @property {number} - The unique identifier of the medal */
6164
6522
  this.id = id;
6165
-
6166
- /** @property {String} - Name of the medal */
6523
+
6524
+ /** @property {string} - Name of the medal */
6167
6525
  this.name = name;
6168
-
6169
- /** @property {String} - Description of the medal */
6526
+
6527
+ /** @property {string} - Description of the medal */
6170
6528
  this.description = description;
6171
-
6172
- /** @property {String} - Icon for the medal */
6529
+
6530
+ /** @property {string} - Icon for the medal */
6173
6531
  this.icon = icon;
6174
-
6532
+
6175
6533
  /** @property {boolean} - Is the medal unlocked? */
6176
6534
  this.unlocked = false;
6177
6535
 
@@ -6196,7 +6554,7 @@ class Medal
6196
6554
  }
6197
6555
 
6198
6556
  /** Render a medal
6199
- * @param {Number} [hidePercent] - How much to slide the medal off screen
6557
+ * @param {number} [hidePercent] - How much to slide the medal off screen
6200
6558
  */
6201
6559
  render(hidePercent=0)
6202
6560
  {
@@ -6237,7 +6595,7 @@ class Medal
6237
6595
 
6238
6596
  /** Render the icon for a medal
6239
6597
  * @param {Vector2} pos - Screen space position
6240
- * @param {Number} size - Screen space size
6598
+ * @param {number} size - Screen space size
6241
6599
  */
6242
6600
  renderIcon(pos, size)
6243
6601
  {
@@ -6247,14 +6605,14 @@ class Medal
6247
6605
  else
6248
6606
  drawTextScreen(this.icon, pos, size*.7, BLACK);
6249
6607
  }
6250
-
6608
+
6251
6609
  // Get local storage key used by the medal
6252
6610
  storageKey() { return medalsSaveName + '_' + this.id; }
6253
6611
  }
6254
6612
  /**
6255
6613
  * 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
6614
+ * - All WebGL used by the engine is wrapped up here
6615
+ * - Will fall back to 2D canvas rendering if WebGL is not supported
6258
6616
  * - For normal stuff you won't need to see or call anything in this file
6259
6617
  * - For advanced stuff there are helper functions to create shaders, textures, etc
6260
6618
  * - Can be disabled with glEnable to revert to 2D canvas rendering
@@ -6269,24 +6627,27 @@ class Medal
6269
6627
  * @memberof WebGL */
6270
6628
  let glCanvas;
6271
6629
 
6272
- /** 2d context for glCanvas
6630
+ /** WebGL2 context for `glCanvas`
6273
6631
  * @type {WebGL2RenderingContext}
6274
6632
  * @memberof WebGL */
6275
6633
  let glContext;
6276
6634
 
6277
- /** Should webgl be setup with anti-aliasing? must be set before calling engineInit
6635
+ /** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
6278
6636
  * @type {boolean}
6279
6637
  * @memberof WebGL */
6280
6638
  let glAntialias = true;
6281
6639
 
6282
6640
  // WebGL internal variables not exposed to documentation
6283
- let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive;
6641
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
6284
6642
 
6285
- // WebGL internal constants
6286
- const gl_MAX_INSTANCES = 1e4;
6643
+ // WebGL internal constants
6644
+ const gl_ARRAY_BUFFER_SIZE = 5e5;
6287
6645
  const gl_INDICES_PER_INSTANCE = 11;
6288
6646
  const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
6289
- const gl_INSTANCE_BUFFER_SIZE = gl_MAX_INSTANCES * gl_INSTANCE_BYTE_STRIDE;
6647
+ const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
6648
+ const gl_INDICES_PER_POLY_VERTEX = 3;
6649
+ const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
6650
+ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
6290
6651
 
6291
6652
  ///////////////////////////////////////////////////////////////////////////////
6292
6653
 
@@ -6307,11 +6668,11 @@ function glInit()
6307
6668
  return;
6308
6669
  }
6309
6670
 
6310
- // create the webgl canvas
6671
+ // create the WebGL canvas
6311
6672
  const rootElement = mainCanvas.parentElement;
6312
6673
  rootElement.appendChild(glCanvas);
6313
6674
 
6314
- // setup vertex and fragment shaders
6675
+ // setup instanced rendering shader program
6315
6676
  glShader = glCreateProgram(
6316
6677
  '#version 300 es\n' + // specify GLSL ES version
6317
6678
  'precision highp float;'+ // use highp for better accuracy
@@ -6339,40 +6700,59 @@ function glInit()
6339
6700
  '}' // end of shader
6340
6701
  );
6341
6702
 
6703
+ // setup poly rendering shaders
6704
+ glPolyShader = glCreateProgram(
6705
+ '#version 300 es\n' + // specify GLSL ES version
6706
+ 'precision highp float;'+ // use highp for better accuracy
6707
+ 'uniform mat4 m;'+ // transform matrix
6708
+ 'in vec2 p;'+ // in: position
6709
+ 'in vec4 c;'+ // in: color
6710
+ 'out vec4 d;'+ // out: color
6711
+ 'void main(){'+ // shader entry point
6712
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
6713
+ 'd=c;'+ // pass color to fragment shader
6714
+ '}' // end of shader
6715
+ ,
6716
+ '#version 300 es\n' + // specify GLSL ES version
6717
+ 'precision highp float;'+ // use highp for better accuracy
6718
+ 'in vec4 d;'+ // in: color
6719
+ 'out vec4 c;'+ // out: color
6720
+ 'void main(){'+ // shader entry point
6721
+ 'c=d;'+ // set color
6722
+ '}' // end of shader
6723
+ );
6724
+
6342
6725
  // init buffers
6343
- const glInstanceData = new ArrayBuffer(gl_INSTANCE_BUFFER_SIZE);
6726
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
6344
6727
  glPositionData = new Float32Array(glInstanceData);
6345
6728
  glColorData = new Uint32Array(glInstanceData);
6346
6729
  glArrayBuffer = glContext.createBuffer();
6347
6730
  glGeometryBuffer = glContext.createBuffer();
6348
6731
 
6349
6732
  // create the geometry buffer, triangle strip square
6350
- const geometry = new Float32Array([glInstanceCount=0,0,1,0,0,1,1,1]);
6733
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
6351
6734
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6352
6735
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
6353
6736
  }
6354
6737
 
6355
- // Setup webgl render each frame, called automatically by engine
6356
- // Also used by tile layer rendering when redrawing tiles
6357
- function glPreRender()
6738
+ function glSetInstancedMode()
6358
6739
  {
6359
- if (!glEnable || !glContext) return;
6360
-
6361
- // set up the shader and canvas
6362
- glClearCanvas();
6740
+ if (!glPolyMode)
6741
+ return;
6742
+
6743
+ // setup instanced mode
6744
+ glFlush();
6745
+ glPolyMode = false;
6363
6746
  glContext.useProgram(glShader);
6364
- glContext.activeTexture(glContext.TEXTURE0);
6365
- if (textureInfos[0])
6366
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
6367
6747
 
6368
6748
  // set vertex attributes
6369
- let offset = glAdditive = glBatchAdditive = 0;
6749
+ let offset = 0;
6370
6750
  const initVertexAttribArray = (name, type, typeSize, size)=>
6371
6751
  {
6372
6752
  const location = glContext.getAttribLocation(glShader, name);
6373
6753
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
6374
6754
  const divisor = typeSize && 1; // only if not geometry
6375
- const normalize = typeSize == 1; // only if color
6755
+ const normalize = typeSize === 1; // only if color
6376
6756
  glContext.enableVertexAttribArray(location);
6377
6757
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
6378
6758
  glContext.vertexAttribDivisor(location, divisor);
@@ -6381,26 +6761,87 @@ function glPreRender()
6381
6761
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6382
6762
  initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
6383
6763
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
6384
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_INSTANCE_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6764
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6385
6765
  initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
6386
6766
  initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
6387
6767
  initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
6388
6768
  initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
6389
6769
  initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
6770
+ }
6771
+
6772
+ function glSetPolyMode()
6773
+ {
6774
+ if (glPolyMode)
6775
+ return;
6390
6776
 
6777
+ // setup poly mode
6778
+ glFlush();
6779
+ glPolyMode = true;
6780
+ glContext.useProgram(glPolyShader);
6781
+
6782
+ // set vertex attributes
6783
+ let offset = 0;
6784
+ const initVertexAttribArray = (name, type, typeSize, size)=>
6785
+ {
6786
+ const location = glContext.getAttribLocation(glPolyShader, name);
6787
+ const normalize = typeSize === 1; // only normalize if color
6788
+ const stride = gl_POLY_VERTEX_BYTE_STRIDE;
6789
+ glContext.enableVertexAttribArray(location);
6790
+ glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
6791
+ glContext.vertexAttribDivisor(location, 0);
6792
+ offset += size*typeSize;
6793
+ }
6794
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
6795
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6796
+ initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
6797
+ initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
6798
+ }
6799
+
6800
+ // Setup WebGL render each frame, called automatically by engine
6801
+ // Also used by tile layer rendering when redrawing tiles
6802
+ function glPreRender()
6803
+ {
6804
+ if (!glEnable || !glContext) return;
6805
+
6806
+ // clear the canvas
6807
+ glClearCanvas();
6808
+
6391
6809
  // build the transform matrix
6392
6810
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
6393
6811
  const rotatedCam = cameraPos.rotate(-cameraAngle);
6394
6812
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
6395
6813
  const ca = Math.cos(cameraAngle);
6396
6814
  const sa = Math.sin(cameraAngle);
6397
- glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
6398
- [
6815
+ const transform = [
6399
6816
  s.x * ca, s.y * sa, 0, 0,
6400
6817
  -s.x * sa, s.y * ca, 0, 0,
6401
6818
  1, 1, 1, 0,
6402
- p.x, p.y, 0, 1
6403
- ]);
6819
+ p.x, p.y, 0, 1];
6820
+
6821
+ // set the same transform matrix for both shaders
6822
+ const initUniform = (program, uniform, value) =>
6823
+ {
6824
+ glContext.useProgram(program);
6825
+ const location = glContext.getUniformLocation(program, uniform);
6826
+ glContext.uniformMatrix4fv(location, false, value);
6827
+ }
6828
+ initUniform(glPolyShader, 'm', transform);
6829
+ initUniform(glShader, 'm', transform);
6830
+
6831
+ // set the active texture
6832
+ glContext.activeTexture(glContext.TEXTURE0);
6833
+ if (textureInfos[0])
6834
+ {
6835
+ glActiveTexture = textureInfos[0].glTexture;
6836
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
6837
+ }
6838
+
6839
+ // start with additive blending off
6840
+ glAdditive = glBatchAdditive = false;
6841
+
6842
+ // force it to enter instanced mode
6843
+ glPolyMode = true;
6844
+ glSetInstancedMode();
6404
6845
  }
6405
6846
 
6406
6847
  /** Clear the canvas and setup the viewport
@@ -6408,21 +6849,23 @@ function glPreRender()
6408
6849
  function glClearCanvas()
6409
6850
  {
6410
6851
  if (!glContext) return;
6411
-
6852
+
6412
6853
  // clear and set to same size as main canvas
6413
- glContext.viewport(0, 0, glCanvas.width=drawCanvas.width, glCanvas.height=drawCanvas.height);
6854
+ glCanvas.width = drawCanvas.width;
6855
+ glCanvas.height = drawCanvas.height;
6856
+ glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
6414
6857
  glContext.clear(glContext.COLOR_BUFFER_BIT);
6415
6858
  }
6416
6859
 
6417
- /** Set the WebGl texture, called automatically if using multiple textures
6860
+ /** Set the WebGL texture, called automatically if using multiple textures
6418
6861
  * - This may also flush the gl buffer resulting in more draw calls and worse performance
6419
6862
  * @param {WebGLTexture} texture
6420
- * @param {boolean} wrap - Should the texture wrap or clamp
6863
+ * @param {boolean} [wrap] - Should the texture wrap or clamp
6421
6864
  * @memberof WebGL */
6422
6865
  function glSetTexture(texture, wrap=false)
6423
6866
  {
6424
6867
  // must flush cache with the old texture to set a new one
6425
- if (!glContext || texture == glActiveTexture)
6868
+ if (!glContext || texture === glActiveTexture)
6426
6869
  return;
6427
6870
 
6428
6871
  glFlush();
@@ -6435,8 +6878,8 @@ function glSetTexture(texture, wrap=false)
6435
6878
  }
6436
6879
 
6437
6880
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
6438
- * @param {String} source
6439
- * @param {Number} type
6881
+ * @param {string} source
6882
+ * @param {number} type
6440
6883
  * @return {WebGLShader}
6441
6884
  * @memberof WebGL */
6442
6885
  function glCompileShader(source, type)
@@ -6455,8 +6898,8 @@ function glCompileShader(source, type)
6455
6898
  }
6456
6899
 
6457
6900
  /** Create WebGL program with given shaders
6458
- * @param {String} vsSource
6459
- * @param {String} fsSource
6901
+ * @param {string} vsSource
6902
+ * @param {string} fsSource
6460
6903
  * @return {WebGLProgram}
6461
6904
  * @memberof WebGL */
6462
6905
  function glCreateProgram(vsSource, fsSource)
@@ -6504,7 +6947,7 @@ function glCreateTexture(image)
6504
6947
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
6505
6948
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
6506
6949
  }
6507
-
6950
+
6508
6951
  // set texture filtering
6509
6952
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
6510
6953
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -6512,7 +6955,6 @@ function glCreateTexture(image)
6512
6955
  return texture;
6513
6956
  }
6514
6957
 
6515
-
6516
6958
  /** Deletes a WebGL texture
6517
6959
  * @param {WebGLTexture} [texture]
6518
6960
  * @memberof WebGL */
@@ -6540,18 +6982,25 @@ function glSetTextureData(texture, image)
6540
6982
  * @memberof WebGL */
6541
6983
  function glFlush()
6542
6984
  {
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;
6985
+ if (glEnable && glContext && glBatchCount)
6986
+ {
6987
+ // set bend mode
6988
+ const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
6989
+ glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
6990
+ glContext.enable(glContext.BLEND);
6991
+
6992
+ const byteLength = glBatchCount *
6993
+ (glPolyMode ? gl_INDICES_PER_POLY_VERTEX : gl_INDICES_PER_INSTANCE);
6994
+ glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData, 0, byteLength);
6995
+
6996
+ // draw the batch
6997
+ if (glPolyMode)
6998
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
6999
+ else
7000
+ glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
7001
+ drawCount += glBatchCount;
7002
+ glBatchCount = 0;
7003
+ }
6555
7004
  glBatchAdditive = glAdditive;
6556
7005
  }
6557
7006
 
@@ -6567,7 +7016,8 @@ function glCopyToContext(context)
6567
7016
  context.drawImage(glCanvas, 0, 0);
6568
7017
  }
6569
7018
 
6570
- /** Set anti-aliasing for webgl canvas
7019
+ /** Set anti-aliasing for WebGL canvas
7020
+ * Must be called before engineInit
6571
7021
  * @param {boolean} [antialias]
6572
7022
  * @memberof WebGL */
6573
7023
  function glSetAntialias(antialias=true)
@@ -6577,25 +7027,27 @@ function glSetAntialias(antialias=true)
6577
7027
  }
6578
7028
 
6579
7029
  /** 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
7030
+ * @param {number} x
7031
+ * @param {number} y
7032
+ * @param {number} sizeX
7033
+ * @param {number} sizeY
7034
+ * @param {number} [angle]
7035
+ * @param {number} [uv0X]
7036
+ * @param {number} [uv0Y]
7037
+ * @param {number} [uv1X]
7038
+ * @param {number} [uv1Y]
7039
+ * @param {number} [rgba=-1] - white is -1
7040
+ * @param {number} [rgbaAdditive=0] - black is 0
6591
7041
  * @memberof WebGL */
6592
7042
  function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
6593
7043
  {
6594
7044
  // flush if there is not enough room or if different blend mode
6595
- if (glInstanceCount >= gl_MAX_INSTANCES || glBatchAdditive != glAdditive)
7045
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
6596
7046
  glFlush();
7047
+ glSetInstancedMode();
6597
7048
 
6598
- let offset = glInstanceCount++ * gl_INDICES_PER_INSTANCE;
7049
+ glPolyMode = false;
7050
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
6599
7051
  glPositionData[offset++] = x;
6600
7052
  glPositionData[offset++] = y;
6601
7053
  glPositionData[offset++] = sizeX;
@@ -6607,6 +7059,312 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
6607
7059
  glColorData[offset++] = rgba;
6608
7060
  glColorData[offset++] = rgbaAdditive;
6609
7061
  glPositionData[offset++] = angle;
7062
+ }
7063
+
7064
+ /** Transform and add a polygon to the gl draw list
7065
+ * @param {Array<Vector2>} points - Array of Vector2 points
7066
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
7067
+ * @param {number} x
7068
+ * @param {number} y
7069
+ * @param {number} sx
7070
+ * @param {number} sy
7071
+ * @param {number} angle
7072
+ * @param {boolean} [tristrip] - should tristrip algorithm be used
7073
+ * @memberof WebGL */
7074
+ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
7075
+ {
7076
+ const pointsOut = [];
7077
+ for (const p of points)
7078
+ {
7079
+ // transform the point
7080
+ const px = p.x*sx;
7081
+ const py = p.y*sy;
7082
+ const sa = Math.sin(-angle);
7083
+ const ca = Math.cos(-angle);
7084
+ pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
7085
+ }
7086
+ const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
7087
+ glDrawPoints(drawPoints, rgba);
7088
+ }
7089
+
7090
+ /** Transform and add a polygon to the gl draw list
7091
+ * @param {Array<Vector2>} points - Array of Vector2 points
7092
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
7093
+ * @param {number} lineWidth - Width of the outline
7094
+ * @param {number} x
7095
+ * @param {number} y
7096
+ * @param {number} sx
7097
+ * @param {number} sy
7098
+ * @param {number} angle
7099
+ * @param {boolean} [wrap] - Should the outline connect the first and last points
7100
+ * @memberof WebGL */
7101
+ function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle, wrap=true)
7102
+ {
7103
+ const outlinePoints = glMakeOutline(points, lineWidth, wrap);
7104
+ glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
7105
+ }
7106
+
7107
+ /** Add a list of points to the gl draw list
7108
+ * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
7109
+ * @param {number} rgba - Color as a 32-bit integer
7110
+ * @memberof WebGL */
7111
+ function glDrawPoints(points, rgba)
7112
+ {
7113
+ if (!glEnable || points.length < 3)
7114
+ return; // needs at least 3 points to have area
7115
+
7116
+ // flush if there is not enough room or if different blend mode
7117
+ const vertCount = points.length + 2;
7118
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
7119
+ glFlush();
7120
+ glSetPolyMode();
7121
+
7122
+ // setup triangle strip with degenerate verts at start and end
7123
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
7124
+ for(let i = vertCount; i--;)
7125
+ {
7126
+ const j = clamp(i-1, 0, vertCount-3);
7127
+ const point = points[j];
7128
+ glPositionData[offset++] = point.x;
7129
+ glPositionData[offset++] = point.y;
7130
+ glColorData[offset++] = rgba;
7131
+ }
7132
+ glBatchCount += vertCount;
7133
+ }
7134
+
7135
+ /** Add a list of colored points to the gl draw list
7136
+ * @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
7137
+ * @param {Array<number>} pointColors - Array of 32-bit integer colors
7138
+ * @memberof WebGL */
7139
+ function glDrawColoredPoints(points, pointColors)
7140
+ {
7141
+ if (!glEnable || points.length < 3)
7142
+ return; // needs at least 3 points to have area
7143
+
7144
+ // flush if there is not enough room or if different blend mode
7145
+ const vertCount = points.length + 2;
7146
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
7147
+ glFlush();
7148
+ glSetPolyMode();
7149
+
7150
+ // setup triangle strip with degenerate verts at start and end
7151
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
7152
+ for(let i = vertCount; i--;)
7153
+ {
7154
+ const j = clamp(i-1, 0, vertCount-3);
7155
+ const point = points[j];
7156
+ const color = pointColors[j];
7157
+ glPositionData[offset++] = point.x;
7158
+ glPositionData[offset++] = point.y;
7159
+ glColorData[offset++] = color;
7160
+ }
7161
+ glBatchCount += vertCount;
7162
+ }
7163
+
7164
+ // WebGL internal function to convert polygon to outline triangle strip
7165
+ function glMakeOutline(points, width, wrap=true)
7166
+ {
7167
+ if (points.length < 2)
7168
+ return [];
7169
+
7170
+ const halfWidth = width / 2;
7171
+ const strip = [];
7172
+ const n = points.length;
7173
+ const e = 1e-6;
7174
+ const miterLimit = width*100;
7175
+ for (let i = 0; i < n; i++)
7176
+ {
7177
+ // for each vertex, calculate normal based on adjacent edges
7178
+ const prev = points[wrap ? (i - 1 + n) % n : max(i - 1, 0)];
7179
+ const curr = points[i];
7180
+ const next = points[wrap ? (i + 1) % n : min(i + 1, n - 1)];
7181
+
7182
+ // direction from previous to current
7183
+ const dx1 = curr.x - prev.x;
7184
+ const dy1 = curr.y - prev.y;
7185
+ const len1 = (dx1*dx1 + dy1*dy1)**.5;
7186
+
7187
+ // direction from current to next
7188
+ const dx2 = next.x - curr.x;
7189
+ const dy2 = next.y - curr.y;
7190
+ const len2 = (dx2*dx2 + dy2*dy2)**.5;
7191
+
7192
+ if (len1 < e && len2 < e)
7193
+ continue; // skip degenerate point
7194
+
7195
+ // calculate perpendicular normals for each edge
7196
+ const nx1 = len1 > e ? -dy1 / len1 : 0;
7197
+ const ny1 = len1 > e ? dx1 / len1 : 0;
7198
+ const nx2 = len2 > e ? -dy2 / len2 : 0;
7199
+ const ny2 = len2 > e ? dx2 / len2 : 0;
7200
+
7201
+ // average the normals for miter
7202
+ let nx = nx1 + nx2;
7203
+ let ny = ny1 + ny2;
7204
+ const nlen = (nx*nx + ny*ny)**.5;
7205
+ if (nlen < e)
7206
+ {
7207
+ // 180 degree turn - use perpendicular
7208
+ nx = nx1;
7209
+ ny = ny1;
7210
+ }
7211
+ else
7212
+ {
7213
+ // calculate miter length
7214
+ nx /= nlen;
7215
+ ny /= nlen;
7216
+ const dot = nx1 * nx + ny1 * ny;
7217
+ if (dot > e)
7218
+ {
7219
+ // scale normal by miter length, clamped to miterLimit
7220
+ const miterLength = min(1 / dot, miterLimit);
7221
+ nx *= miterLength;
7222
+ ny *= miterLength;
7223
+ }
7224
+ }
7225
+
7226
+ // create inner and outer points along the normal
7227
+ const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
7228
+ const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
7229
+ strip.push(inner);
7230
+ strip.push(outer);
7231
+ }
7232
+ if (strip.length > 1 && wrap)
7233
+ {
7234
+ // close the loop
7235
+ strip.push(strip[0]);
7236
+ strip.push(strip[1]);
7237
+ }
7238
+ return strip;
7239
+ }
7240
+
7241
+ // WebGL internal function to convert polys to tri strips
7242
+ function glPolyStrip(points)
7243
+ {
7244
+ // validate input
7245
+ if (points.length < 3)
7246
+ return [];
7247
+
7248
+ // cross product helper: (b-a) x (c-a)
7249
+ const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
7250
+
7251
+ // calculate signed area of polygon
7252
+ const signedArea = (poly)=>
7253
+ {
7254
+ let area = 0;
7255
+ for (let i = poly.length; i--;)
7256
+ {
7257
+ const j = (i+1) % poly.length;
7258
+ area += poly[i].cross(poly[j]);
7259
+ }
7260
+ return area;
7261
+ }
7262
+
7263
+ // ensure counter-clockwise winding
7264
+ if (signedArea(points) < 0)
7265
+ points = points.reverse();
7266
+
7267
+ // check if point is inside triangle
7268
+ const e = 1e-9;
7269
+ const pointInTriangle = (p, a, b, c)=>
7270
+ {
7271
+ const c1 = cross(a, b, p);
7272
+ const c2 = cross(b, c, p);
7273
+ const c3 = cross(c, a, p);
7274
+ const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
7275
+ const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
7276
+ return !(negative && positive);
7277
+ };
7278
+
7279
+ // ear clipping triangulation
7280
+ const indices = [];
7281
+ for (let i = 0; i < points.length; ++i)
7282
+ indices[i] = i;
7283
+ const triangles = [];
7284
+ let attempts = 0;
7285
+ const maxAttempts = points.length ** 2 + 100;
7286
+ while (indices.length > 3 && attempts++ < maxAttempts)
7287
+ {
7288
+ let foundEar = false;
7289
+ for (let i = indices.length; --i;)
7290
+ {
7291
+ const i0 = indices[(i + indices.length - 1) % indices.length];
7292
+ const i1 = indices[i];
7293
+ const i2 = indices[(i + 1) % indices.length];
7294
+ const a = points[i0], b = points[i1], c = points[i2];
7295
+
7296
+ // check if convex
7297
+ if (cross(a, b, c) < e)
7298
+ continue;
7299
+
7300
+ // check if any other point is inside
7301
+ let hasInside = false;
7302
+ for (let j = 0; j < indices.length; j++)
7303
+ {
7304
+ const k = indices[j];
7305
+ if (k === i0 || k === i1 || k === i2)
7306
+ continue;
7307
+ const p = points[k];
7308
+ hasInside = pointInTriangle(p, a, b, c);
7309
+ if (hasInside)
7310
+ break;
7311
+ }
7312
+ if (hasInside)
7313
+ continue;
7314
+
7315
+ // found valid ear
7316
+ triangles.push([i0, i1, i2]);
7317
+ indices.splice(i, 1);
7318
+ foundEar = true;
7319
+ break;
7320
+ }
7321
+
7322
+ // fallback for degenerate cases
7323
+ if (!foundEar)
7324
+ {
7325
+ let worstIndex = -1, worstValue = Infinity;
7326
+ for (let i = indices.length; --i;)
7327
+ {
7328
+ const i0 = indices[(i + indices.length - 1) % indices.length];
7329
+ const i1 = indices[i];
7330
+ const i2 = indices[(i + 1) % indices.length];
7331
+ const value = abs(cross(points[i0], points[i1], points[i2]));
7332
+ if (value < worstValue)
7333
+ {
7334
+ worstValue = value;
7335
+ worstIndex = i;
7336
+ }
7337
+ }
7338
+ if (worstIndex < 0)
7339
+ break;
7340
+
7341
+ const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
7342
+ const i1 = indices[worstIndex];
7343
+ const i2 = indices[(worstIndex + 1) % indices.length];
7344
+ triangles.push([i0, i1, i2]);
7345
+ indices.splice(worstIndex, 1);
7346
+ }
7347
+ }
7348
+
7349
+ // add final triangle
7350
+ if (indices.length === 3)
7351
+ triangles.push([indices[0], indices[1], indices[2]]);
7352
+ if (!triangles.length)
7353
+ return [];
7354
+
7355
+ // convert triangles to triangle strip with degenerate connectors
7356
+ const strip = [];
7357
+ let [a0, b0, c0] = triangles[0];
7358
+ strip.push(points[a0], points[b0], points[c0]);
7359
+ for (let i = 1; i < triangles.length; i++)
7360
+ {
7361
+ // add degenerate bridge from last vertex to first of new triangle
7362
+ const [a, b, c] = triangles[i];
7363
+ strip.push(points[c0], points[a]);
7364
+ strip.push(points[a], points[b], points[c]);
7365
+ c0 = c;
7366
+ }
7367
+ return strip;
6610
7368
  }
6611
7369
  /**
6612
7370
  * LittleJS Newgrounds API
@@ -6631,11 +7389,11 @@ let newgrounds;
6631
7389
  class NewgroundsMedal extends Medal
6632
7390
  {
6633
7391
  /** 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
7392
+ * @param {number} id - The unique identifier of the medal
7393
+ * @param {string} name - Name of the medal
7394
+ * @param {string} [description] - Description of the medal
7395
+ * @param {string} [icon] - Icon for the medal
7396
+ * @param {string} [src] - Image location for the medal
6639
7397
  */
6640
7398
  constructor(id, name, description, icon, src)
6641
7399
  { super(id, name, description, icon, src); }
@@ -6727,7 +7485,7 @@ class NewgroundsPlugin
6727
7485
  * @param {number} id - The scoreboard id
6728
7486
  * @param {string} [user] - A user's id or name
6729
7487
  * @param {number} [social] - If true, only social scores will be loaded
6730
- * @param {number} [skip] - Number of scores to skip before start
7488
+ * @param {number} [skip] - Number of scores to skip over
6731
7489
  * @param {number} [limit] - Number of scores to include in the list
6732
7490
  * @return {Object} - The response JSON object
6733
7491
  */
@@ -6950,16 +7708,16 @@ class ZzFXMusic extends Sound
6950
7708
  if (!soundEnable || headlessMode) return;
6951
7709
  this.randomness = 0;
6952
7710
  this.sampleChannels = zzfxM(...zzfxMusic);
6953
- this.sampleRate = zzfxR;
7711
+ this.sampleRate = audioDefaultSampleRate;
6954
7712
  }
6955
7713
 
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
7714
+ /** Play the music that loops by default
7715
+ * @param {number} [volume] - Volume to play the music at
7716
+ * @param {boolean} [loop] - Should the music loop?
6959
7717
  * @return {AudioBufferSourceNode} - The audio source node
6960
7718
  */
6961
- playMusic(volume, loop=false)
6962
- { return super.play(undefined, volume, 1, 1, loop); }
7719
+ playMusic(volume=1, loop=true)
7720
+ { return super.play(undefined, volume, 1, 0, loop); }
6963
7721
  }
6964
7722
 
6965
7723
  ///////////////////////////////////////////////////////////////////////////////
@@ -6993,7 +7751,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
6993
7751
  let panning = 0;
6994
7752
  let hasMore = 1;
6995
7753
  let sampleCache = {};
6996
- let beatLength = zzfxR / BPM * 60 >> 2;
7754
+ let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
6997
7755
 
6998
7756
  // for each channel in order until there are no more
6999
7757
  for (; hasMore; channelIndex++) {
@@ -7012,15 +7770,15 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
7012
7770
  // get next offset, use the length of first channel
7013
7771
  nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
7014
7772
  // for each beat in pattern, plus one extra if end of sequence
7015
- isSequenceEnd = sequenceIndex == sequence.length - 1;
7773
+ isSequenceEnd = sequenceIndex === sequence.length - 1;
7016
7774
  for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
7017
7775
 
7018
7776
  // <channel-note>
7019
7777
  note = patternChannel[i];
7020
7778
 
7021
7779
  // stop if end, different instrument or new note
7022
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
7023
- instrument != (patternChannel[0] || 0) || note | 0;
7780
+ stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
7781
+ instrument !== (patternChannel[0] || 0) || note | 0;
7024
7782
 
7025
7783
  // fill buffer with samples for previous beat, most cpu intensive part
7026
7784
  for (j = 0; j < beatLength && notFirstBeat;
@@ -7105,11 +7863,11 @@ class UISystemPlugin
7105
7863
  /** @property {Color} - Default text color for UI elements */
7106
7864
  this.defaultTextColor = BLACK;
7107
7865
  /** @property {Color} - Default button color for UI elements */
7108
- this.defaultButtonColor = hsl(0,0,.5);
7866
+ this.defaultButtonColor = hsl(0,0,.7);
7109
7867
  /** @property {Color} - Default hover color for UI elements */
7110
- this.defaultHoverColor = hsl(0,0,.7);
7868
+ this.defaultHoverColor = hsl(0,0,.9);
7111
7869
  /** @property {Color} - Default color for disabled UI elements */
7112
- this.defaultDisabledColor = hsl(0,0,.2);
7870
+ this.defaultDisabledColor = hsl(0,0,.3);
7113
7871
  /** @property {number} - Default line width for UI elements */
7114
7872
  this.defaultLineWidth = 4;
7115
7873
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -7129,16 +7887,16 @@ class UISystemPlugin
7129
7887
 
7130
7888
  engineAddPlugin(uiUpdate, uiRender);
7131
7889
 
7132
- function updateInvisible(o)
7133
- {
7134
- for (const c of o.children)
7135
- updateInvisible(c);
7136
- o.updateInvisible();
7137
- }
7138
-
7139
7890
  // setup recursive update and render
7140
7891
  function uiUpdate()
7141
7892
  {
7893
+ function updateInvisibleObject(o)
7894
+ {
7895
+ // update invisible objects
7896
+ for (const c of o.children)
7897
+ updateInvisibleObject(c);
7898
+ o.updateInvisible();
7899
+ }
7142
7900
  function updateObject(o)
7143
7901
  {
7144
7902
  if (o.visible)
@@ -7152,7 +7910,7 @@ class UISystemPlugin
7152
7910
  o.update();
7153
7911
  }
7154
7912
  else
7155
- updateInvisible(o);
7913
+ updateInvisibleObject(o);
7156
7914
  }
7157
7915
  uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
7158
7916
  }
@@ -7178,7 +7936,7 @@ class UISystemPlugin
7178
7936
  * @param {Color} [color=uiSystem.defaultColor]
7179
7937
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
7180
7938
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7181
- * @param {number} [lineWidth=uiSystem.defaultCornerRadius] */
7939
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
7182
7940
  drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
7183
7941
  {
7184
7942
  const context = uiSystem.uiContext;
@@ -7253,32 +8011,40 @@ class UIObject
7253
8011
  constructor(pos=vec2(), size=vec2())
7254
8012
  {
7255
8013
  /** @property {Vector2} - Local position of the object */
7256
- this.localPos = pos.copy();
8014
+ this.localPos = pos.copy();
7257
8015
  /** @property {Vector2} - Screen space position of the object */
7258
- this.pos = pos.copy();
8016
+ this.pos = pos.copy();
7259
8017
  /** @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 */
8018
+ this.size = size.copy();
8019
+ /** @property {Color} - Color of the object */
8020
+ this.color = uiSystem.defaultColor;
8021
+ /** @property {string} - Text for this ui object */
8022
+ this.text = undefined;
8023
+ /** @property {Color} - Color when disabled */
8024
+ this.disabledColor = uiSystem.defaultDisabledColor;
8025
+ /** @property {boolean} - Is this object disabled? */
8026
+ this.disabled = false;
8027
+ /** @property {Color} - Color for text */
8028
+ this.textColor = uiSystem.defaultTextColor;
8029
+ /** @property {Color} - Color used when hovering over the object */
7266
8030
  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 */
8031
+ /** @property {Color} - Color for line drawing */
8032
+ this.lineColor = uiSystem.defaultLineColor;
8033
+ /** @property {number} - Width for line drawing */
8034
+ this.lineWidth = uiSystem.defaultLineWidth;
8035
+ /** @property {number} - Corner radius for rounded rects */
8036
+ this.cornerRadius = uiSystem.defaultCornerRadius;
8037
+ /** @property {string} - Font for this objecct */
8038
+ this.font = uiSystem.defaultFont;
8039
+ /** @property {number} - Override for text height */
8040
+ this.textHeight = undefined;
8041
+ /** @property {boolean} - Should this object be drawn */
8042
+ this.visible = true;
8043
+ /** @property {Array<UIObject>} - A list of this object's children */
8044
+ this.children = [];
8045
+ /** @property {UIObject} - This object's parent, position is in parent space */
8046
+ this.parent = undefined;
8047
+ /** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
7282
8048
  this.extraTouchSize = 0;
7283
8049
  /** @property {Sound} - Sound when interactive element is pressed */
7284
8050
  this.soundPress = uiSystem.defaultSoundPress;
@@ -7310,7 +8076,7 @@ class UIObject
7310
8076
  */
7311
8077
  removeChild(child)
7312
8078
  {
7313
- ASSERT(child.parent == this && this.children.includes(child));
8079
+ ASSERT(child.parent === this && this.children.includes(child));
7314
8080
  this.children.splice(this.children.indexOf(child), 1);
7315
8081
  child.parent = undefined;
7316
8082
  }
@@ -7363,7 +8129,7 @@ class UIObject
7363
8129
  this.mouseIsHeld = false;
7364
8130
  }
7365
8131
 
7366
- if (this.mouseIsOver != mouseWasOver)
8132
+ if (this.mouseIsOver !== mouseWasOver)
7367
8133
  this.mouseIsOver ? this.onEnter() : this.onLeave();
7368
8134
  }
7369
8135
 
@@ -7374,7 +8140,7 @@ class UIObject
7374
8140
  uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
7375
8141
  }
7376
8142
 
7377
- /** Special update for when object is invisible */
8143
+ /** Special update when object is not visible */
7378
8144
  updateInvisible()
7379
8145
  {
7380
8146
  // reset input state when not visible
@@ -7382,28 +8148,22 @@ class UIObject
7382
8148
  }
7383
8149
 
7384
8150
  /** Called when the mouse enters the object */
7385
- onEnter()
7386
- {}
8151
+ onEnter() {}
7387
8152
 
7388
8153
  /** Called when the mouse leaves the object */
7389
- onLeave()
7390
- {}
8154
+ onLeave() {}
7391
8155
 
7392
8156
  /** Called when the mouse is pressed while over the object */
7393
- onPress()
7394
- {}
8157
+ onPress() {}
7395
8158
 
7396
8159
  /** Called when the mouse is released while over the object */
7397
- onRelease()
7398
- {}
8160
+ onRelease() {}
7399
8161
 
7400
8162
  /** Called when user clicks on this object */
7401
- onClick()
7402
- {}
8163
+ onClick() {}
7403
8164
 
7404
8165
  /** Called when the state of this object changes */
7405
- onChange()
7406
- {}
8166
+ onChange() {}
7407
8167
  };
7408
8168
 
7409
8169
  ///////////////////////////////////////////////////////////////////////////////
@@ -7424,13 +8184,13 @@ class UIText extends UIObject
7424
8184
  {
7425
8185
  super(pos, size);
7426
8186
 
7427
- /** @property {string} */
8187
+ // set properties
7428
8188
  this.text = text;
7429
- /** @property {string} */
7430
8189
  this.align = align;
8190
+ this.font = font;
7431
8191
 
7432
- this.font = font; // set font
7433
- this.lineWidth = 0; // set text to not be outlined by default
8192
+ // make text not outlined by default
8193
+ this.lineWidth = 0;
7434
8194
  }
7435
8195
  render()
7436
8196
  {
@@ -7457,13 +8217,14 @@ class UITile extends UIObject
7457
8217
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
7458
8218
  {
7459
8219
  super(pos, size);
7460
-
7461
8220
  /** @property {TileInfo} - Tile image to use */
7462
8221
  this.tileInfo = tileInfo;
7463
8222
  /** @property {number} - Angle to rotate in radians */
7464
8223
  this.angle = angle;
7465
8224
  /** @property {boolean} - Should it be mirrored? */
7466
8225
  this.mirror = mirror;
8226
+
8227
+ // set properties
7467
8228
  this.color = color;
7468
8229
  }
7469
8230
  render()
@@ -7489,22 +8250,20 @@ class UIButton extends UIObject
7489
8250
  {
7490
8251
  super(pos, size);
7491
8252
 
7492
- /** @property {string} */
8253
+ // set properties
7493
8254
  this.text = text;
7494
- /** @property {Color} */
7495
- this.disabledColor = uiSystem.defaultDisabledColor;
7496
- /** @property {boolean} */
7497
- this.disabled = false;
7498
- this.interactive = true;
7499
8255
  this.color = color;
8256
+ this.interactive = true;
7500
8257
  }
7501
8258
  render()
7502
8259
  {
8260
+ // draw the button
7503
8261
  const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
7504
8262
  const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
7505
8263
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7506
8264
 
7507
- const textScale = .8; // scale text to fit in button
8265
+ // draw the text
8266
+ const textScale = .8; // scale text to fit
7508
8267
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7509
8268
  uiSystem.drawText(this.text, this.pos, textSize,
7510
8269
  this.textColor, 0, undefined, this.align, this.font);
@@ -7522,13 +8281,18 @@ class UICheckbox extends UIObject
7522
8281
  * @param {Vector2} [pos]
7523
8282
  * @param {Vector2} [size]
7524
8283
  * @param {boolean} [checked]
8284
+ * @param {string} [text]
8285
+ * @param {Color} [color=uiSystem.defaultButtonColor]
7525
8286
  */
7526
- constructor(pos, size, checked=false)
8287
+ constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
7527
8288
  {
7528
8289
  super(pos, size);
7529
-
7530
- /** @property {boolean} */
8290
+ /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
7531
8291
  this.checked = checked;
8292
+
8293
+ // set properties
8294
+ this.text = text;
8295
+ this.color = color;
7532
8296
  this.interactive = true;
7533
8297
  }
7534
8298
  onClick()
@@ -7538,14 +8302,24 @@ class UICheckbox extends UIObject
7538
8302
  }
7539
8303
  render()
7540
8304
  {
7541
- const color = this.mouseIsOver? this.hoverColor : this.color;
8305
+ const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
7542
8306
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
7543
8307
  if (this.checked)
7544
8308
  {
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);
8309
+ const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
8310
+ const length = lerp(1, 2**.5/2, p) / 2;
8311
+ let s = this.size.scale(length);
8312
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
8313
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
7548
8314
  }
8315
+
8316
+ // draw the text to the right side of the checkbox
8317
+ const textScale = .8; // scale text to fit
8318
+ const gapScale = .55;
8319
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
8320
+ const pos = this.pos.add(vec2(this.size.x*gapScale,0));
8321
+ uiSystem.drawText(this.text, pos, textSize,
8322
+ this.textColor, 0, undefined, 'left', this.font);
7549
8323
  }
7550
8324
  }
7551
8325
 
@@ -7568,43 +8342,51 @@ class UIScrollbar extends UIObject
7568
8342
  {
7569
8343
  super(pos, size);
7570
8344
 
7571
- /** @property {number} */
8345
+ /** @property {number} - Current percentage value of this scrollbar 0-1 */
7572
8346
  this.value = value;
7573
- /** @property {string} */
7574
- this.text = text;
7575
- /** @property {Color} */
8347
+ /** @property {Color} - Color for the handle part of the scrollbar */
7576
8348
  this.handleColor = handleColor;
8349
+
8350
+ // set properties
8351
+ this.text = text;
7577
8352
  this.color = color;
7578
8353
  this.interactive = true;
7579
8354
  }
7580
8355
  update()
7581
8356
  {
7582
8357
  super.update();
7583
- if (this.mouseIsHeld)
8358
+ if (this.mouseIsHeld && this.interactive)
7584
8359
  {
8360
+ // check if value changed
7585
8361
  const handleSize = vec2(this.size.y);
7586
8362
  const handleWidth = this.size.x - handleSize.x;
7587
8363
  const p1 = this.pos.x - handleWidth/2;
7588
8364
  const p2 = this.pos.x + handleWidth/2;
7589
8365
  const oldValue = this.value;
7590
8366
  this.value = percent(mousePosScreen.x, p1, p2);
7591
- this.value == oldValue || this.onChange();
8367
+ this.value === oldValue || this.onChange();
7592
8368
  }
7593
8369
  }
7594
8370
  render()
7595
8371
  {
7596
- const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
7597
- const color = this.mouseIsOver? this.hoverColor : this.color;
8372
+ // draw the scrollbar background
8373
+ const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
8374
+ this.color : this.lineColor;
8375
+ const color = this.disabled ? this.disabledColor :
8376
+ this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
7598
8377
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7599
8378
 
8379
+ // draw the scrollbar handle
7600
8380
  const handleSize = vec2(this.size.y);
7601
8381
  const handleWidth = this.size.x - handleSize.x;
7602
8382
  const p1 = this.pos.x - handleWidth/2;
7603
8383
  const p2 = this.pos.x + handleWidth/2;
7604
8384
  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);
8385
+ const handleColor = this.disabled ? this.disabledColor :
8386
+ this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
8387
+ uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
7607
8388
 
8389
+ // draw the text on the scrollbar
7608
8390
  const textScale = .8; // scale text to fit in scrollbar
7609
8391
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7610
8392
  uiSystem.drawText(this.text, this.pos, textSize,
@@ -7699,14 +8481,14 @@ class Box2dObject extends EngineObject
7699
8481
  if (this.tileInfo)
7700
8482
  super.render();
7701
8483
  else
7702
- this.drawFixtures(this.color, this.lineColor, this.lineWidth, mainContext);
8484
+ this.drawFixtures(this.color, this.lineColor, this.lineWidth);
7703
8485
  }
7704
8486
 
7705
8487
  /** Render debug info */
7706
8488
  renderDebugInfo()
7707
8489
  {
7708
8490
  const isAsleep = !this.getIsAwake();
7709
- const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
8491
+ const isStatic = this.getBodyType() === box2d.bodyTypeStatic;
7710
8492
  const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
7711
8493
  this.drawFixtures(color);
7712
8494
  }
@@ -9174,7 +9956,7 @@ class Box2dPlugin
9174
9956
  queryCallback.ReportFixture = function(fixturePointer)
9175
9957
  {
9176
9958
  const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
9177
- if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
9959
+ if (dynamicOnly && fixture.GetBody().GetType() !== box2d.instance.b2_dynamicBody)
9178
9960
  return true; // continue getting results
9179
9961
  if (!fixture.TestPoint(box2d.vec2dTo(pos)))
9180
9962
  return true; // continue getting results
@@ -9203,7 +9985,7 @@ class Box2dPlugin
9203
9985
  * @param {Color} [lineColor]
9204
9986
  * @param {number} [lineWidth]
9205
9987
  * @param {CanvasRenderingContext2D} [context] */
9206
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context=drawContext)
9988
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
9207
9989
  {
9208
9990
  const shape = box2d.castObjectType(fixture.GetShape());
9209
9991
  switch (shape.GetType())
@@ -9213,20 +9995,20 @@ class Box2dPlugin
9213
9995
  let points = [];
9214
9996
  for (let i=shape.GetVertexCount(); i--;)
9215
9997
  points.push(box2d.vec2From(shape.GetVertex(i)));
9216
- drawPoly(points, color, lineWidth, lineColor, pos, angle, false, false, context);
9998
+ drawPoly(points, color, lineWidth, lineColor, pos, angle);
9217
9999
  break;
9218
10000
  }
9219
10001
  case box2d.instance.b2Shape.e_circle:
9220
10002
  {
9221
10003
  const radius = shape.get_m_radius();
9222
- drawCircle(pos, radius, color, lineWidth, lineColor, false, false, context);
10004
+ drawCircle(pos, radius*2, color, lineWidth, lineColor);
9223
10005
  break;
9224
10006
  }
9225
10007
  case box2d.instance.b2Shape.e_edge:
9226
10008
  {
9227
10009
  const v1 = box2d.vec2From(shape.get_m_vertex1());
9228
10010
  const v2 = box2d.vec2From(shape.get_m_vertex2());
9229
- drawLine(v1, v2, lineWidth, lineColor, pos, angle, false, false, context);
10011
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle);
9230
10012
  break;
9231
10013
  }
9232
10014
  }
@@ -9305,7 +10087,9 @@ class Box2dPlugin
9305
10087
  }
9306
10088
 
9307
10089
  ///////////////////////////////////////////////////////////////////////////////
9308
- /** Box2d Init - Call with await before starting LittleJS to init box2d
10090
+ /** Box2d Init - Call with await to init box2d
10091
+ * @example
10092
+ * await box2dInit();
9309
10093
  * @return {Promise<Box2dPlugin>}
9310
10094
  * @memberof Box2D */
9311
10095
  async function box2dInit()
@@ -9368,14 +10152,14 @@ async function box2dInit()
9368
10152
  {
9369
10153
  color = getDebugColor(color);
9370
10154
  center = box2d.vec2FromPointer(center);
9371
- drawCircle(center, radius, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
10155
+ drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
9372
10156
  };
9373
10157
  debugDraw.DrawSolidCircle = function(center, radius, axis, color)
9374
10158
  {
9375
10159
  color = getDebugColor(color);
9376
10160
  center = box2d.vec2FromPointer(center);
9377
10161
  axis = box2d.vec2FromPointer(axis).scale(radius);
9378
- drawCircle(center, radius, color, debugLineWidth, color, false, false, overlayContext);
10162
+ drawCircle(center, radius*2, color, debugLineWidth, color, false, false, overlayContext);
9379
10163
  drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
9380
10164
  };
9381
10165
  debugDraw.DrawTransform = function(transform)
@@ -9421,7 +10205,7 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
9421
10205
  }
9422
10206
 
9423
10207
  /** Draw a scalable nine-slice UI element in world space
9424
- * This function can apply color and additive color if webgl is enabled
10208
+ * This function can apply color and additive color if WebGL is enabled
9425
10209
  * @param {Vector2} pos - World space position
9426
10210
  * @param {Vector2} size - World space size
9427
10211
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
@@ -9450,9 +10234,9 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
9450
10234
  {
9451
10235
  // sides
9452
10236
  const horizontal = i%2;
9453
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-1:1));
10237
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
9454
10238
  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)))
10239
+ const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
9456
10240
  drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9457
10241
  }
9458
10242
  for (let i=4; i--;)
@@ -9481,7 +10265,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
9481
10265
  }
9482
10266
 
9483
10267
  /** Draw a scalable three-slice UI element in world space
9484
- * This function can apply color and additive color if webgl is enabled
10268
+ * This function can apply color and additive color if WebGL is enabled
9485
10269
  * @param {Vector2} pos - World space position
9486
10270
  * @param {Vector2} size - World space size
9487
10271
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
@@ -9513,7 +10297,7 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
9513
10297
  // sides
9514
10298
  const a = angle + i*PI/2;
9515
10299
  const horizontal = i%2;
9516
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-flip:flip));
10300
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
9517
10301
  const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
9518
10302
  drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
9519
10303
  }