littlejsengine 1.13.1 → 1.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +15 -11
  2. package/copyToGithub.bat +28 -0
  3. package/dist/littlejs.d.ts +430 -304
  4. package/dist/littlejs.esm.js +1745 -982
  5. package/dist/littlejs.esm.min.js +1 -1
  6. package/dist/littlejs.js +1716 -965
  7. package/dist/littlejs.min.js +1 -1
  8. package/dist/littlejs.release.js +1673 -922
  9. package/examples/box2d/game.js +2 -2
  10. package/examples/box2d/gameObjects.js +2 -2
  11. package/examples/box2d/scenes.js +1 -1
  12. package/examples/breakout/game.js +30 -25
  13. package/examples/breakoutTutorial/README.md +44 -41
  14. package/examples/breakoutTutorial/game.js +2 -2
  15. package/examples/electron/build.bat +7 -0
  16. package/examples/electron/build.js +124 -0
  17. package/examples/electron/electron.js +35 -0
  18. package/examples/electron/game.js +140 -0
  19. package/examples/electron/index.html +13 -0
  20. package/examples/electron/package.json +12 -0
  21. package/examples/electron/tiles.png +0 -0
  22. package/examples/empty/game.js +1 -0
  23. package/examples/htmlMenu/game.js +1 -1
  24. package/examples/index.html +332 -160
  25. package/examples/module/game.js +5 -2
  26. package/examples/particles/index.html +12 -3
  27. package/examples/platformer/gameEffects.js +20 -19
  28. package/examples/platformer/gameLevel.js +6 -1
  29. package/examples/shorts/base.html +1 -1
  30. package/examples/shorts/blending.js +9 -5
  31. package/examples/shorts/box2d.js +1 -1
  32. package/examples/shorts/box2dCar.js +1 -1
  33. package/examples/shorts/clock.js +1 -1
  34. package/examples/shorts/colors.js +2 -2
  35. package/examples/shorts/flappyGame.js +2 -1
  36. package/examples/shorts/helloWorld.js +1 -1
  37. package/examples/shorts/maze.js +1 -1
  38. package/examples/shorts/music.js +106 -0
  39. package/examples/shorts/nineSlice.js +9 -9
  40. package/examples/shorts/parallax.js +71 -0
  41. package/examples/shorts/piano.js +9 -10
  42. package/examples/shorts/postProcess.js +25 -8
  43. package/examples/shorts/raycasting.js +2 -2
  44. package/examples/shorts/shapes.js +7 -15
  45. package/examples/shorts/slidingPuzzle.js +2 -2
  46. package/examples/shorts/song.mp3 +0 -0
  47. package/examples/shorts/starfield.js +5 -7
  48. package/examples/shorts/systemFont.js +1 -1
  49. package/examples/shorts/uiSystem.js +16 -8
  50. package/examples/starter/build.js +9 -8
  51. package/examples/starter/game.js +5 -2
  52. package/examples/starter/index.html +2 -2
  53. package/examples/stress/index.html +13 -8
  54. package/examples/style.css +19 -6
  55. package/examples/typescript/build.js +1 -1
  56. package/examples/typescript/game.js +2 -2
  57. package/examples/typescript/game.ts +5 -2
  58. package/examples/uiSystem/game.js +13 -12
  59. package/package.json +2 -2
  60. package/plugins/box2d.js +24 -97
  61. package/plugins/drawUtilities.js +29 -23
  62. package/plugins/newgrounds.js +6 -6
  63. package/plugins/pluginExport.js +1 -1
  64. package/plugins/postProcess.js +7 -0
  65. package/plugins/uiSystem.js +106 -82
  66. package/plugins/zzfxm.js +10 -10
  67. package/reference.md +90 -54
  68. package/src/engine.js +28 -23
  69. package/src/engineAudio.js +285 -110
  70. package/src/engineBuild.js +9 -9
  71. package/src/engineDebug.js +28 -28
  72. package/src/engineDraw.js +346 -202
  73. package/src/engineExport.js +28 -16
  74. package/src/engineInput.js +58 -68
  75. package/src/engineMedals.js +29 -29
  76. package/src/engineObject.js +28 -25
  77. package/src/engineParticles.js +67 -60
  78. package/src/engineRelease.js +1 -1
  79. package/src/engineSettings.js +70 -16
  80. package/src/engineTileLayer.js +34 -34
  81. package/src/engineUtilities.js +69 -62
  82. package/src/engineWebGL.js +467 -65
  83. /package/examples/shorts/{playSound.js → sound.js} +0 -0
@@ -3,10 +3,10 @@
3
3
 
4
4
  'use strict';
5
5
 
6
- /**
6
+ /**
7
7
  * LittleJS - The Tiny Fast JavaScript Game Engine
8
8
  * MIT License - Copyright 2021 Frank Force
9
- *
9
+ *
10
10
  * Engine Features
11
11
  * - Object oriented system with base class engine object
12
12
  * - Base class object handles update, physics, collision, rendering, etc
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.13.1';
36
+ const engineVersion = '1.14.2';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -123,7 +123,7 @@ function engineAddPlugin(updateFunction, renderFunction)
123
123
  * // Basic engine startup
124
124
  * engineInit(
125
125
  * () => { console.log('Game initialized!'); }, // gameInit
126
- * () => { updatePlayer(); }, // gameUpdate
126
+ * () => { updateGameLogic(); }, // gameUpdate
127
127
  * () => { updateUI(); }, // gameUpdatePost
128
128
  * () => { drawBackground(); }, // gameRender
129
129
  * () => { drawHUD(); }, // gameRenderPost
@@ -149,7 +149,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
149
149
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
150
150
 
151
151
  // disable smoothing for pixel art
152
- overlayContext.imageSmoothingEnabled =
152
+ overlayContext.imageSmoothingEnabled =
153
153
  mainContext.imageSmoothingEnabled = !tilesPixelated;
154
154
 
155
155
  // setup gl rendering if enabled
@@ -197,7 +197,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
197
197
  deltaSmooth = frameTimeBufferMS;
198
198
  frameTimeBufferMS = 0;
199
199
  }
200
-
200
+
201
201
  // update multiple frames if necessary in case of slow framerate
202
202
  for (;frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
203
203
  {
@@ -242,7 +242,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
242
242
  overlayContext.textBaseline = 'top';
243
243
  overlayContext.font = '1em monospace';
244
244
  overlayContext.fillStyle = '#000';
245
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
245
+ const text = engineName + ' ' + 'v' + engineVersion + ' / '
246
246
  + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
247
247
  + (glEnable ? ' GL' : ' 2D') ;
248
248
  overlayContext.fillText(text, mainCanvas.width-3, 3);
@@ -258,13 +258,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
258
258
  function updateCanvas()
259
259
  {
260
260
  if (headlessMode) return;
261
-
261
+
262
262
  if (canvasFixedSize.x)
263
263
  {
264
264
  // clear canvas and set fixed size
265
265
  mainCanvas.width = canvasFixedSize.x;
266
266
  mainCanvas.height = canvasFixedSize.y;
267
-
267
+
268
268
  // fit to window by adding space on top or bottom if necessary
269
269
  const aspect = innerWidth / innerHeight;
270
270
  const fixedAspect = mainCanvas.width / mainCanvas.height;
@@ -274,10 +274,10 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
274
274
  else
275
275
  {
276
276
  // clear canvas and set size to same as window
277
- mainCanvas.width = min(innerWidth, canvasMaxSize.x);
277
+ mainCanvas.width = min(innerWidth, canvasMaxSize.x);
278
278
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
279
279
  }
280
-
280
+
281
281
  // clear overlay canvas and set size
282
282
  overlayCanvas.width = mainCanvas.width;
283
283
  overlayCanvas.height = mainCanvas.height;
@@ -296,16 +296,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
296
296
  return startEngine();
297
297
 
298
298
  // setup html
299
- const styleRoot =
299
+ const styleRoot =
300
300
  'margin:0;' + // fill the window
301
301
  'overflow:hidden;' + // no scroll bars
302
302
  'background:#000;' + // set background color
303
- (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
304
303
  'user-select:none;' + // prevent hold to select
305
304
  '-webkit-user-select:none;' + // compatibility for ios
306
- (!touchInputEnable ? '' : // no touch css settings
307
305
  'touch-action:none;' + // prevent mobile pinch to resize
308
- '-webkit-touch-callout:none');// compatibility for ios
306
+ '-webkit-touch-callout:none';// compatibility for ios
309
307
  rootElement.style.cssText = styleRoot;
310
308
  drawCanvas = mainCanvas = document.createElement('canvas');
311
309
  rootElement.appendChild(mainCanvas);
@@ -322,20 +320,27 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
322
320
  rootElement.appendChild(overlayCanvas);
323
321
  overlayContext = overlayCanvas.getContext('2d');
324
322
 
325
- // set canvas style
323
+ // set canvases
326
324
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
327
325
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
328
326
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
329
327
  if (glCanvas)
330
328
  glCanvas.style.cssText = styleCanvas;
329
+ setCanvasPixelated(canvasPixelated);
330
+ setOverlayCanvasPixelated(overlayCanvasPixelated);
331
331
  updateCanvas();
332
-
332
+ glPreRender();
333
+
334
+ // create offscreen canvas for image processing
335
+ workCanvas = new OffscreenCanvas(256, 256);
336
+ workContext = workCanvas.getContext('2d', { willReadFrequently: true });
337
+
333
338
  // create promises for loading images
334
339
  const promises = imageSources.map((src, textureIndex)=>
335
- new Promise(resolve =>
340
+ new Promise(resolve =>
336
341
  {
337
342
  const image = new Image;
338
- image.onerror = image.onload = ()=>
343
+ image.onerror = image.onload = ()=>
339
344
  {
340
345
  const textureInfo = new TextureInfo(image);
341
346
  textureInfo.createWebGLTexture();
@@ -350,7 +355,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
350
355
  if (!imageSources.length)
351
356
  {
352
357
  // no images to load
353
- promises.push(new Promise(resolve =>
358
+ promises.push(new Promise(resolve =>
354
359
  {
355
360
  const textureInfo = new TextureInfo(new Image);
356
361
  textureInfos[0] = textureInfo;
@@ -362,7 +367,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
362
367
  if (showSplashScreen)
363
368
  {
364
369
  // draw splash screen
365
- promises.push(new Promise(resolve =>
370
+ promises.push(new Promise(resolve =>
366
371
  {
367
372
  let t = 0;
368
373
  console.log(`${engineName} Engine v${engineVersion}`);
@@ -594,7 +599,7 @@ function drawEngineSplashScreen(t)
594
599
  line(36,20,60,20);
595
600
 
596
601
  // engine front light
597
- circle(60,30,4,PI,3*PI,color(3,2));
602
+ circle(60,30,4,PI,3*PI,color(3,2));
598
603
  circle(60,30,4,PI,2*PI,color(3,3));
599
604
  circle(60,30,4,PI,3*PI);
600
605
 
@@ -643,10 +648,10 @@ function drawEngineSplashScreen(t)
643
648
  x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
644
649
  X += w;
645
650
  }
646
-
651
+
647
652
  x.restore();
648
653
  }
649
- /**
654
+ /**
650
655
  * LittleJS - Release Mode
651
656
  * - This file is used for release builds in place of engineDebug.js
652
657
  * - Debug functionality is disabled to reduce size and increase performance
@@ -759,7 +764,19 @@ function lerp(valueA, valueB, percent)
759
764
  if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
760
765
  console.warn('lerp() parameter order changed! use lerp(start, end, p)');
761
766
  return valueA + clamp(percent) * (valueB-valueA);
762
- }
767
+ }
768
+
769
+ /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
770
+ * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
771
+ * @param {number} value
772
+ * @param {number} percentA
773
+ * @param {number} percentB
774
+ * @param {number} lerpA
775
+ * @param {number} lerpB
776
+ * @return {number}
777
+ * @memberof Utilities */
778
+ function percentLerp(value, percentA, percentB, lerpA, lerpB)
779
+ { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
763
780
 
764
781
  /** Returns signed wrapped distance between the two values passed in
765
782
  * @param {number} valueA
@@ -811,22 +828,23 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
811
828
  * @memberof Utilities */
812
829
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
813
830
 
814
- /** Returns the nearest power of two not less then the value
831
+ /** Returns the nearest power of two not less than the value
815
832
  * @param {number} value
816
833
  * @return {number}
817
834
  * @memberof Utilities */
818
835
  function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
819
836
 
820
- /** Returns true if two axis aligned bounding boxes are overlapping
837
+ /** Returns true if two axis aligned bounding boxes are overlapping
838
+ * this can be used for simple collision detection between objects
821
839
  * @param {Vector2} posA - Center of box A
822
840
  * @param {Vector2} sizeA - Size of box A
823
841
  * @param {Vector2} posB - Center of box B
824
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
842
+ * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
825
843
  * @return {boolean} - True if overlapping
826
844
  * @memberof Utilities */
827
845
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
828
- {
829
- return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
846
+ {
847
+ return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
830
848
  && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
831
849
  }
832
850
 
@@ -875,12 +893,13 @@ function isIntersecting(start, end, pos, size)
875
893
  * @param {number} [frequency] - Frequency of the wave in Hz
876
894
  * @param {number} [amplitude] - Amplitude (max height) of the wave
877
895
  * @param {number} [t=time] - Value to use for time of the wave
896
+ * @param {number} [offset] - Value to use for time offset of the wave
878
897
  * @return {number} - Value waving between 0 and amplitude
879
898
  * @memberof Utilities */
880
- function wave(frequency=1, amplitude=1, t=time)
881
- { return amplitude/2 * (1 - Math.cos(t*frequency*2*PI)); }
899
+ function wave(frequency=1, amplitude=1, t=time, offset=0)
900
+ { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
882
901
 
883
- /** Formats seconds to mm:ss style for display purposes
902
+ /** Formats seconds to mm:ss style for display purposes
884
903
  * @param {number} t - time in seconds
885
904
  * @return {string}
886
905
  * @memberof Utilities */
@@ -896,13 +915,12 @@ async function fetchJSON(url)
896
915
  return response.json();
897
916
  }
898
917
 
899
- /**
918
+ /**
900
919
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
901
920
  * @param {any} n
902
921
  * @return {boolean}
903
- * @memberof Utilities
904
- */
905
- function isNumber(n) { return typeof n == 'number' && !isNaN(n); }
922
+ * @memberof Utilities */
923
+ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
906
924
 
907
925
  ///////////////////////////////////////////////////////////////////////////////
908
926
 
@@ -957,13 +975,13 @@ function randInCircle(radius=1, minRadius=0)
957
975
  * @memberof Random */
958
976
  function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
959
977
  {
960
- return linear ? colorA.lerp(colorB, rand()) :
978
+ return linear ? colorA.lerp(colorB, rand()) :
961
979
  new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
962
980
  }
963
981
 
964
982
  ///////////////////////////////////////////////////////////////////////////////
965
983
 
966
- /**
984
+ /**
967
985
  * Seeded random number generator
968
986
  * - Can be used to create a deterministic random number sequence
969
987
  * @example
@@ -990,8 +1008,8 @@ class RandomGenerator
990
1008
  float(valueA=1, valueB=0)
991
1009
  {
992
1010
  // xorshift algorithm
993
- this.seed ^= this.seed << 13;
994
- this.seed ^= this.seed >>> 17;
1011
+ this.seed ^= this.seed << 13;
1012
+ this.seed ^= this.seed >>> 17;
995
1013
  this.seed ^= this.seed << 5;
996
1014
  return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
997
1015
  }
@@ -1040,16 +1058,14 @@ class RandomGenerator
1040
1058
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
1041
1059
  * a = vec2(5); // set a to (5, 5)
1042
1060
  * b = vec2(); // set b to (0, 0)
1043
- * @memberof Utilities
1044
- */
1061
+ * @memberof Utilities */
1045
1062
  function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1046
1063
 
1047
- /**
1064
+ /**
1048
1065
  * Check if object is a valid Vector2
1049
1066
  * @param {any} v
1050
1067
  * @return {boolean}
1051
- * @memberof Utilities
1052
- */
1068
+ * @memberof Utilities */
1053
1069
  function isVector2(v) { return v instanceof Vector2; }
1054
1070
 
1055
1071
  // vector2 asserts
@@ -1058,10 +1074,10 @@ function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n);
1058
1074
  function ASSERT_VECTOR2_NORMAL(v)
1059
1075
  {
1060
1076
  ASSERT_VECTOR2_VALID(v);
1061
- ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1077
+ ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
1062
1078
  }
1063
1079
 
1064
- /**
1080
+ /**
1065
1081
  * 2D Vector object with vector math library
1066
1082
  * - Functions do not change this so they can be chained together
1067
1083
  * @example
@@ -1185,7 +1201,7 @@ class Vector2
1185
1201
  * @param {number} [angle]
1186
1202
  * @param {number} [length]
1187
1203
  * @return {Vector2} */
1188
- setAngle(angle=0, length=1)
1204
+ setAngle(angle=0, length=1)
1189
1205
  {
1190
1206
  ASSERT_NUMBER_VALID(angle);
1191
1207
  ASSERT_NUMBER_VALID(length);
@@ -1200,7 +1216,7 @@ class Vector2
1200
1216
  rotate(angle)
1201
1217
  {
1202
1218
  ASSERT_NUMBER_VALID(angle);
1203
- const c = Math.cos(-angle), s = Math.sin(-angle);
1219
+ const c = Math.cos(-angle), s = Math.sin(-angle);
1204
1220
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1205
1221
  }
1206
1222
 
@@ -1212,9 +1228,9 @@ class Vector2
1212
1228
  ASSERT_NUMBER_VALID(direction);
1213
1229
  ASSERT_NUMBER_VALID(length);
1214
1230
  direction = mod(direction, 4);
1215
- ASSERT(direction==0 || direction==1 || direction==2 || direction==3,
1231
+ ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1216
1232
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1217
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1233
+ return vec2(direction%2 ? direction-1 ? -length : length : 0,
1218
1234
  direction%2 ? 0 : direction ? -length : length);
1219
1235
  }
1220
1236
 
@@ -1266,7 +1282,7 @@ class Vector2
1266
1282
  /** Returns this vector expressed as a string
1267
1283
  * @param {number} digits - precision to display
1268
1284
  * @return {string} */
1269
- toString(digits=3)
1285
+ toString(digits=3)
1270
1286
  {
1271
1287
  ASSERT_NUMBER_VALID(digits);
1272
1288
  if (debug)
@@ -1285,7 +1301,7 @@ class Vector2
1285
1301
 
1286
1302
  ///////////////////////////////////////////////////////////////////////////////
1287
1303
 
1288
- /**
1304
+ /**
1289
1305
  * Create a color object with RGBA values, white by default
1290
1306
  * @param {number} [r=1] - red
1291
1307
  * @param {number} [g=1] - green
@@ -1296,29 +1312,27 @@ class Vector2
1296
1312
  */
1297
1313
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1298
1314
 
1299
- /**
1315
+ /**
1300
1316
  * Create a color object with HSLA values, white by default
1301
1317
  * @param {number} [h=0] - hue
1302
1318
  * @param {number} [s=0] - saturation
1303
1319
  * @param {number} [l=1] - lightness
1304
1320
  * @param {number} [a=1] - alpha
1305
1321
  * @return {Color}
1306
- * @memberof Utilities
1307
- */
1322
+ * @memberof Utilities */
1308
1323
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
1309
1324
 
1310
- /**
1325
+ /**
1311
1326
  * Check if object is a valid Color
1312
1327
  * @param {any} c
1313
1328
  * @return {boolean}
1314
- * @memberof Utilities
1315
- */
1329
+ * @memberof Utilities */
1316
1330
  function isColor(c) { return c instanceof Color; }
1317
1331
 
1318
1332
  // color asserts
1319
1333
  function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c) && c.isValid(), 'Color is invalid.', c); }
1320
1334
 
1321
- /**
1335
+ /**
1322
1336
  * Color object (red, green, blue, alpha) with some helpful functions
1323
1337
  * @example
1324
1338
  * let a = new Color; // white
@@ -1390,7 +1404,7 @@ class Color
1390
1404
  * @param {number} scale
1391
1405
  * @param {number} [alphaScale=scale]
1392
1406
  * @return {Color} */
1393
- scale(scale, alphaScale=scale)
1407
+ scale(scale, alphaScale=scale)
1394
1408
  { return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
1395
1409
 
1396
1410
  /** Returns a copy of this color clamped to the valid range between 0 and 1
@@ -1407,9 +1421,9 @@ class Color
1407
1421
  ASSERT_NUMBER_VALID(percent);
1408
1422
  const p = clamp(percent);
1409
1423
  return new Color(
1410
- c.r*p + this.r*(1-p),
1411
- c.g*p + this.g*(1-p),
1412
- c.b*p + this.b*(1-p),
1424
+ c.r*p + this.r*(1-p),
1425
+ c.g*p + this.g*(1-p),
1426
+ c.b*p + this.b*(1-p),
1413
1427
  c.a*p + this.a*(1-p));
1414
1428
  }
1415
1429
 
@@ -1449,15 +1463,15 @@ class Color
1449
1463
  const minC = min(r, g, b);
1450
1464
  const l = (maxC + minC) / 2;
1451
1465
  let h = 0, s = 0;
1452
- if (maxC != minC)
1466
+ if (maxC !== minC)
1453
1467
  {
1454
1468
  let d = maxC - minC;
1455
1469
  s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
1456
- if (r == maxC)
1470
+ if (r === maxC)
1457
1471
  h = (g - b) / d + (g < b ? 6 : 0);
1458
- else if (g == maxC)
1472
+ else if (g === maxC)
1459
1473
  h = (b - r) / d + 2;
1460
- else if (b == maxC)
1474
+ else if (b === maxC)
1461
1475
  h = (r - g) / d + 4;
1462
1476
  }
1463
1477
  return [h / 6, s, l, a];
@@ -1467,7 +1481,7 @@ class Color
1467
1481
  * @param {number} [amount]
1468
1482
  * @param {number} [alphaAmount]
1469
1483
  * @return {Color} */
1470
- mutate(amount=.05, alphaAmount=0)
1484
+ mutate(amount=.05, alphaAmount=0)
1471
1485
  {
1472
1486
  ASSERT_NUMBER_VALID(amount);
1473
1487
  ASSERT_NUMBER_VALID(alphaAmount);
@@ -1483,47 +1497,47 @@ class Color
1483
1497
  /** Returns this color expressed as a hex color code
1484
1498
  * @param {boolean} [useAlpha] - if alpha should be included in result
1485
1499
  * @return {string} */
1486
- toString(useAlpha = true)
1500
+ toString(useAlpha = true)
1487
1501
  {
1488
- ASSERT(typeof useAlpha == 'boolean', 'Use alpha boolean is invalid.', useAlpha);
1502
+ ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
1489
1503
  if (debug && !this.isValid())
1490
1504
  return `#000`;
1491
1505
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
1492
1506
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1493
1507
  }
1494
-
1508
+
1495
1509
  /** Set this color from a hex code
1496
1510
  * @param {string} hex - html hex code
1497
1511
  * @return {Color} */
1498
1512
  setHex(hex)
1499
1513
  {
1500
- ASSERT(typeof hex == 'string' && hex[0] == '#', 'Color hex code must be a string starting with #');
1514
+ ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
1501
1515
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1502
1516
 
1503
1517
  if (hex.length < 6)
1504
1518
  {
1505
1519
  const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
1506
1520
  this.r = fromHex(1);
1507
- this.g = fromHex(2),
1521
+ this.g = fromHex(2);
1508
1522
  this.b = fromHex(3);
1509
- this.a = hex.length == 5 ? fromHex(4) : 1;
1523
+ this.a = hex.length === 5 ? fromHex(4) : 1;
1510
1524
  }
1511
1525
  else
1512
1526
  {
1513
1527
  const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
1514
1528
  this.r = fromHex(1);
1515
- this.g = fromHex(3),
1529
+ this.g = fromHex(3);
1516
1530
  this.b = fromHex(5);
1517
- this.a = hex.length == 9 ? fromHex(7) : 1;
1531
+ this.a = hex.length === 9 ? fromHex(7) : 1;
1518
1532
  }
1519
1533
 
1520
1534
  ASSERT_COLOR_VALID(this);
1521
1535
  return this;
1522
1536
  }
1523
-
1537
+
1524
1538
  /** Returns this color expressed as 32 bit RGBA value
1525
1539
  * @return {number} */
1526
- rgbaInt()
1540
+ rgbaInt()
1527
1541
  {
1528
1542
  const r = clamp(this.r)*255|0;
1529
1543
  const g = clamp(this.g)*255<<8;
@@ -1535,9 +1549,7 @@ class Color
1535
1549
  /** Checks if this is a valid color
1536
1550
  * @return {boolean} */
1537
1551
  isValid()
1538
- {
1539
- return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a);
1540
- }
1552
+ { return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a); }
1541
1553
  }
1542
1554
 
1543
1555
  ///////////////////////////////////////////////////////////////////////////////
@@ -1546,7 +1558,7 @@ class Color
1546
1558
  /** Color - White #ffffff
1547
1559
  * @type {Color}
1548
1560
  * @memberof Utilities */
1549
- const WHITE = rgb();
1561
+ const WHITE = rgb();
1550
1562
 
1551
1563
  /** Color - Clear White #ffffff with 0 alpha
1552
1564
  * @type {Color}
@@ -1661,11 +1673,11 @@ class Timer
1661
1673
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1662
1674
  * @return {number} */
1663
1675
  getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
1664
-
1676
+
1665
1677
  /** Returns this timer expressed as a string
1666
1678
  * @return {string} */
1667
1679
  toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
1668
-
1680
+
1669
1681
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1670
1682
  * @return {number} */
1671
1683
  valueOf() { return this.get(); }
@@ -1700,6 +1712,13 @@ let cameraScale = 32;
1700
1712
  ///////////////////////////////////////////////////////////////////////////////
1701
1713
  // Display settings
1702
1714
 
1715
+ /** Enable applying color to tiles when using canvas2d
1716
+ * - This is slower but should be the same as WebGL rendering
1717
+ * @type {boolean}
1718
+ * @default
1719
+ * @memberof Settings */
1720
+ let canvasColorTiles = true;
1721
+
1703
1722
  /** The max size of the canvas, centered if window is larger
1704
1723
  * @type {Vector2}
1705
1724
  * @default Vector2(1920,1080)
@@ -1713,14 +1732,21 @@ let canvasMaxSize = vec2(1920, 1080);
1713
1732
  * @memberof Settings */
1714
1733
  let canvasFixedSize = vec2();
1715
1734
 
1716
- /** Use nearest neighbor canvas scaling for more pixelated look
1717
- * - Must be set before startup to take effect
1735
+ /** Use nearest canvas scaling for more pixelated look
1718
1736
  * - If enabled sets css image-rendering:pixelated
1719
1737
  * @type {boolean}
1720
1738
  * @default
1721
1739
  * @memberof Settings */
1722
1740
  let canvasPixelated = true;
1723
1741
 
1742
+ /** Use nearest canvas scaling for more pixelated look
1743
+ * - If enabled sets css image-rendering:pixelated
1744
+ * - This defaults to false because text looks better with smoothing
1745
+ * @type {boolean}
1746
+ * @default
1747
+ * @memberof Settings */
1748
+ let overlayCanvasPixelated = false;
1749
+
1724
1750
  /** Disables texture filtering for crisper pixel art
1725
1751
  * @type {boolean}
1726
1752
  * @default
@@ -1733,14 +1759,13 @@ let tilesPixelated = true;
1733
1759
  * @memberof Settings */
1734
1760
  let fontDefault = 'arial';
1735
1761
 
1736
- /** Enable to show the LittleJS splash screen be shown on startup
1762
+ /** Enable to show the LittleJS splash screen on startup
1737
1763
  * @type {boolean}
1738
1764
  * @default
1739
1765
  * @memberof Settings */
1740
1766
  let showSplashScreen = false;
1741
1767
 
1742
1768
  /** Disables all rendering, audio, and input for servers
1743
- * - Must be set before startup to take effect
1744
1769
  * @type {boolean}
1745
1770
  * @default
1746
1771
  * @memberof Settings */
@@ -1749,13 +1774,18 @@ let headlessMode = false;
1749
1774
  ///////////////////////////////////////////////////////////////////////////////
1750
1775
  // WebGL settings
1751
1776
 
1752
- /** Enable webgl rendering, webgl can be disabled and removed from build (with some features disabled)
1753
- * - Must be set before startup to take effect
1777
+ /** Enable WebGL accelerated rendering
1754
1778
  * @type {boolean}
1755
1779
  * @default
1756
1780
  * @memberof Settings */
1757
1781
  let glEnable = true;
1758
1782
 
1783
+ /** How many sided poly to use when drawing circles and ellipses with WebGL
1784
+ * @type {number}
1785
+ * @default
1786
+ * @memberof Settings */
1787
+ let glCircleSides = 32;
1788
+
1759
1789
  ///////////////////////////////////////////////////////////////////////////////
1760
1790
  // Tile sheet settings
1761
1791
 
@@ -1837,13 +1867,13 @@ let particleEmitRateScale = 1;
1837
1867
  * @memberof Settings */
1838
1868
  let gamepadsEnable = true;
1839
1869
 
1840
- /** If true, the dpad input is also routed to the left analog stick (for better accessability)
1870
+ /** If true, the dpad input is also routed to the left analog stick (for better accessibility)
1841
1871
  * @type {boolean}
1842
1872
  * @default
1843
1873
  * @memberof Settings */
1844
1874
  let gamepadDirectionEmulateStick = true;
1845
1875
 
1846
- /** If true the WASD keys are also routed to the direction keys (for better accessability)
1876
+ /** If true the WASD keys are also routed to the direction keys (for better accessibility)
1847
1877
  * @type {boolean}
1848
1878
  * @default
1849
1879
  * @memberof Settings */
@@ -1851,7 +1881,6 @@ let inputWASDEmulateDirection = true;
1851
1881
 
1852
1882
  /** True if touch input is enabled for mobile devices
1853
1883
  * - Touch events will be routed to mouse events
1854
- * - Must be set before startup to take effect
1855
1884
  * @type {boolean}
1856
1885
  * @default
1857
1886
  * @memberof Settings */
@@ -1859,7 +1888,6 @@ let touchInputEnable = true;
1859
1888
 
1860
1889
  /** True if touch gamepad should appear on mobile devices
1861
1890
  * - Supports left analog stick, 4 face buttons and start button (button 9)
1862
- * - Must be set before startup to take effect
1863
1891
  * @type {boolean}
1864
1892
  * @default
1865
1893
  * @memberof Settings */
@@ -1961,6 +1989,14 @@ function setCameraAngle(angle) { cameraAngle = angle; }
1961
1989
  * @memberof Settings */
1962
1990
  function setCameraScale(scale) { cameraScale = scale; }
1963
1991
 
1992
+ /** Set if tiles should be colorized when using canvas2d
1993
+ * This can be slower but results should look nearly identical to WebGL rendering
1994
+ * It can be enabled/disabled at any time
1995
+ * Optimized for performance, and will use faster method if color is white or untextured
1996
+ * @param {boolean} colorTiles
1997
+ * @memberof Settings */
1998
+ function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
1999
+
1964
2000
  /** Set max size of the canvas
1965
2001
  * @param {Vector2} size
1966
2002
  * @memberof Settings */
@@ -1971,10 +2007,30 @@ function setCanvasMaxSize(size) { canvasMaxSize = size; }
1971
2007
  * @memberof Settings */
1972
2008
  function setCanvasFixedSize(size) { canvasFixedSize = size; }
1973
2009
 
1974
- /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
2010
+ /** Use nearest scaling algorithm for canvas for more pixelated look
2011
+ * - If enabled sets css image-rendering:pixelated
2012
+ * @param {boolean} pixelated
2013
+ * @memberof Settings */
2014
+ function setCanvasPixelated(pixelated)
2015
+ {
2016
+ canvasPixelated = pixelated;
2017
+ if (mainCanvas)
2018
+ mainCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2019
+ if (glCanvas)
2020
+ glCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2021
+ }
2022
+
2023
+ /** Use nearest scaling algorithm for canvas for more pixelated look
2024
+ * - If enabled sets css image-rendering:pixelated
2025
+ * - This defaults to false because text looks better with smoothing
1975
2026
  * @param {boolean} pixelated
1976
2027
  * @memberof Settings */
1977
- function setCanvasPixelated(pixelated) { canvasPixelated = pixelated; }
2028
+ function setOverlayCanvasPixelated(pixelated)
2029
+ {
2030
+ overlayCanvasPixelated = pixelated;
2031
+ if (overlayCanvas)
2032
+ overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2033
+ }
1978
2034
 
1979
2035
  /** Disables texture filtering for crisper pixel art
1980
2036
  * @param {boolean} pixelated
@@ -1986,7 +2042,7 @@ function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
1986
2042
  * @memberof Settings */
1987
2043
  function setFontDefault(font) { fontDefault = font; }
1988
2044
 
1989
- /** Set if the LittleJS splash screen be shown on startup
2045
+ /** Set if the LittleJS splash screen should be shown on startup
1990
2046
  * @param {boolean} show
1991
2047
  * @memberof Settings */
1992
2048
  function setShowSplashScreen(show) { showSplashScreen = show; }
@@ -1996,10 +2052,20 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
1996
2052
  * @memberof Settings */
1997
2053
  function setHeadlessMode(headless) { headlessMode = headless; }
1998
2054
 
1999
- /** Set if webgl rendering is enabled
2055
+ /** Set if WebGL rendering is enabled
2000
2056
  * @param {boolean} enable
2001
2057
  * @memberof Settings */
2002
- function setGLEnable(enable) { glEnable = enable; }
2058
+ function setGLEnable(enable)
2059
+ {
2060
+ glEnable = enable;
2061
+ if (glCanvas) // hide glCanvas if WebGL is disabled
2062
+ glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2063
+ }
2064
+
2065
+ /** Set how many sided polygons to use when drawing circles and elipses with WebGL
2066
+ * @param {number} sides
2067
+ * @memberof Settings */
2068
+ function setGLCircleSides(sides) { glCircleSides = sides; }
2003
2069
 
2004
2070
  /** Set default size of tiles in pixels
2005
2071
  * @param {Vector2} size
@@ -2031,7 +2097,7 @@ function setObjectDefaultDamping(damp) { objectDefaultDamping = damp; }
2031
2097
  * @memberof Settings */
2032
2098
  function setObjectDefaultAngleDamping(damp) { objectDefaultAngleDamping = damp; }
2033
2099
 
2034
- /** Set how much to bounce when a collision occur
2100
+ /** Set how much to bounce when a collision occurs
2035
2101
  * @param {number} restitution
2036
2102
  * @memberof Settings */
2037
2103
  function setObjectDefaultRestitution(restitution) { objectDefaultRestitution = restitution; }
@@ -2155,11 +2221,11 @@ function setShowWatermark(show) { showWatermark = show; }
2155
2221
  * @param {string} key
2156
2222
  * @memberof Debug */
2157
2223
  function setDebugKey(key) { debugKey = key; }
2158
- /**
2224
+ /**
2159
2225
  * LittleJS Object System
2160
2226
  */
2161
2227
 
2162
- /**
2228
+ /**
2163
2229
  * LittleJS Object Base Object Class
2164
2230
  * - Top level object class used by the engine
2165
2231
  * - Automatically adds self to object list
@@ -2182,7 +2248,7 @@ function setDebugKey(key) { debugKey = key; }
2182
2248
  * @example
2183
2249
  * // create an engine object, normally you would first extend the class with your own
2184
2250
  * const pos = vec2(2,3);
2185
- * const object = new EngineObject(pos);
2251
+ * const object = new EngineObject(pos);
2186
2252
  */
2187
2253
  class EngineObject
2188
2254
  {
@@ -2200,9 +2266,9 @@ class EngineObject
2200
2266
  ASSERT(isVector2(pos) && pos.isValid(), 'object pos should be a vec2');
2201
2267
  ASSERT(isVector2(size) && size.isValid(), 'object size should be a vec2');
2202
2268
  ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
2203
- ASSERT(typeof angle == 'number' && isFinite(angle), 'object angle should be a number');
2269
+ ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2204
2270
  ASSERT(isColor(color) && color.isValid(), 'object color should be a valid rgba color');
2205
- ASSERT(typeof renderOrder == 'number', 'object renderOrder should be a number');
2271
+ ASSERT(typeof renderOrder === 'number', 'object renderOrder should be a number');
2206
2272
 
2207
2273
  /** @property {Vector2} - World space position of the object */
2208
2274
  this.pos = pos.copy();
@@ -2270,7 +2336,7 @@ class EngineObject
2270
2336
  // add to list of objects
2271
2337
  engineObjects.push(this);
2272
2338
  }
2273
-
2339
+
2274
2340
  /** Update the object transform, called automatically by engine even when paused */
2275
2341
  updateTransforms()
2276
2342
  {
@@ -2339,7 +2405,7 @@ class EngineObject
2339
2405
  for (const o of engineObjectsCollide)
2340
2406
  {
2341
2407
  // non solid objects don't collide with each other
2342
- if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o == this)
2408
+ if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
2343
2409
  continue;
2344
2410
 
2345
2411
  // check collision
@@ -2362,8 +2428,8 @@ class EngineObject
2362
2428
  this.velocity = this.velocity.add(velocity);
2363
2429
  if (o.mass) // push away if not fixed
2364
2430
  o.velocity = o.velocity.subtract(velocity);
2365
-
2366
- debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
2431
+
2432
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
2367
2433
  continue;
2368
2434
  }
2369
2435
 
@@ -2373,7 +2439,7 @@ class EngineObject
2373
2439
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
2374
2440
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
2375
2441
  const restitution = max(this.restitution, o.restitution);
2376
-
2442
+
2377
2443
  if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
2378
2444
  {
2379
2445
  // push outside object collision
@@ -2425,7 +2491,7 @@ class EngineObject
2425
2491
  else // bounce if other object is fixed
2426
2492
  this.velocity.x *= -restitution;
2427
2493
  }
2428
- debugOverlay && debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
2494
+ debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f0f');
2429
2495
  }
2430
2496
  }
2431
2497
  if (this.collideTiles)
@@ -2461,7 +2527,7 @@ class EngineObject
2461
2527
  {
2462
2528
  // move to previous position
2463
2529
  this.pos.y = oldPos.y;
2464
- this.groundObject = undefined;
2530
+ this.groundObject = undefined;
2465
2531
  }
2466
2532
  }
2467
2533
  if (blockedLayerX)
@@ -2470,25 +2536,25 @@ class EngineObject
2470
2536
  this.pos.x = oldPos.x;
2471
2537
  this.velocity.x *= -this.restitution;
2472
2538
  }
2473
- debugOverlay && debugPhysics && debugRect(this.pos, this.size, '#f00');
2539
+ debugPhysics && debugRect(this.pos, this.size, '#f00');
2474
2540
  }
2475
2541
  }
2476
2542
  }
2477
2543
  }
2478
-
2544
+
2479
2545
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
2480
2546
  render()
2481
2547
  {
2482
2548
  // default object render
2483
2549
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
2484
2550
  }
2485
-
2551
+
2486
2552
  /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
2487
2553
  destroy()
2488
- {
2554
+ {
2489
2555
  if (this.destroyed)
2490
2556
  return;
2491
-
2557
+
2492
2558
  // disconnect from parent and destroy children
2493
2559
  this.destroyed = 1;
2494
2560
  this.parent && this.parent.removeChild(this);
@@ -2514,7 +2580,7 @@ class EngineObject
2514
2580
  /** Convert from world space to local space for a vector (rotation only)
2515
2581
  * @param {Vector2} vec - world space vector */
2516
2582
  worldToLocalVector(vec) { return vec.rotate(-this.angle); }
2517
-
2583
+
2518
2584
  /** Called to check if a tile collision should be resolved
2519
2585
  * @param {number} tileData - the value of the tile at the position
2520
2586
  * @param {Vector2} pos - tile where the collision occurred
@@ -2533,16 +2599,19 @@ class EngineObject
2533
2599
 
2534
2600
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
2535
2601
  * @param {Vector2} acceleration */
2536
- applyAcceleration(acceleration) { if (this.mass) this.velocity = this.velocity.add(acceleration); }
2602
+ applyAcceleration(acceleration)
2603
+ { if (this.mass) this.velocity = this.velocity.add(acceleration); }
2537
2604
 
2538
- /** Apply angular acceleration to this object
2605
+ /** Apply angular acceleration to this object
2539
2606
  * @param {number} acceleration */
2540
- applyAngularAcceleration(acceleration) { if (this.mass) this.angleVelocity += acceleration; }
2607
+ applyAngularAcceleration(acceleration)
2608
+ { if (this.mass) this.angleVelocity += acceleration; }
2541
2609
 
2542
2610
  /** Apply force to this object (adjust velocity, affected by mass)
2543
2611
  * @param {Vector2} force */
2544
- applyForce(force) { this.applyAcceleration(force.scale(1/this.mass)); }
2545
-
2612
+ applyForce(force)
2613
+ { if (this.mass) this.applyAcceleration(force.scale(1/this.mass)); }
2614
+
2546
2615
  /** Get the direction of the mirror
2547
2616
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
2548
2617
  getMirrorSign() { return this.mirror ? -1 : 1; }
@@ -2564,7 +2633,7 @@ class EngineObject
2564
2633
  * @param {EngineObject} child */
2565
2634
  removeChild(child)
2566
2635
  {
2567
- ASSERT(child.parent == this && this.children.includes(child));
2636
+ ASSERT(child.parent === this && this.children.includes(child));
2568
2637
  this.children.splice(this.children.indexOf(child), 1);
2569
2638
  child.parent = 0;
2570
2639
  }
@@ -2610,7 +2679,7 @@ class EngineObject
2610
2679
  {
2611
2680
  if (!debug)
2612
2681
  return;
2613
-
2682
+
2614
2683
  // show object info for debugging
2615
2684
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
2616
2685
  const color = rgb(this.collideTiles?1:0, this.collideSolidObjects?1:0, this.isSolid?1:0, .5);
@@ -2620,24 +2689,24 @@ class EngineObject
2620
2689
  this.parent && drawLine(this.pos, this.parent.pos, .1, rgb(1,1,1,.5));
2621
2690
  }
2622
2691
  }
2623
- /**
2692
+ /**
2624
2693
  * LittleJS Drawing System
2625
2694
  * - Hybrid system with both Canvas2D and WebGL available
2626
2695
  * - Super fast tile sheet rendering with WebGL
2627
2696
  * - Can apply rotation, mirror, color and additive color
2628
2697
  * - Font rendering system with built in engine font
2629
2698
  * - Many useful utility functions
2630
- *
2699
+ *
2631
2700
  * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
2632
2701
  * There are 3 canvas/contexts available to draw to...
2633
2702
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
2634
2703
  * glCanvas - Used by the accelerated WebGL batch rendering system.
2635
2704
  * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
2636
- *
2705
+ *
2637
2706
  * The WebGL rendering system is very fast with some caveats...
2638
2707
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
2639
2708
  * - Group additive rendering together using renderOrder to mitigate this issue
2640
- *
2709
+ *
2641
2710
  * The LittleJS rendering solution is intentionally simple, feel free to adjust it for your needs!
2642
2711
  * @namespace Draw
2643
2712
  */
@@ -2672,7 +2741,17 @@ let drawCanvas;
2672
2741
  * @memberof Draw */
2673
2742
  let drawContext;
2674
2743
 
2675
- /** The size of the main canvas (and other secondary canvases)
2744
+ /** Offscreen canvas that can be used for image processing
2745
+ * @type {OffscreenCanvas}
2746
+ * @memberof Draw */
2747
+ let workCanvas;
2748
+
2749
+ /** Offscreen canvas that can be used for image processing
2750
+ * @type {OffscreenCanvasRenderingContext2D}
2751
+ * @memberof Draw */
2752
+ let workContext;
2753
+
2754
+ /** The size of the main canvas (and other secondary canvases)
2676
2755
  * @type {Vector2}
2677
2756
  * @memberof Draw */
2678
2757
  let mainCanvasSize = vec2();
@@ -2687,29 +2766,28 @@ let drawCount;
2687
2766
 
2688
2767
  ///////////////////////////////////////////////////////////////////////////////
2689
2768
 
2690
- /**
2769
+ /**
2691
2770
  * Create a tile info object using a grid based system
2692
2771
  * - This can take vecs or floats for easier use and conversion
2693
2772
  * - If an index is passed in, the tile size and index will determine the position
2694
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
2773
+ * @param {Vector2|number} [pos=0] - Index of tile in sheet
2695
2774
  * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
2696
- * @param {number} [textureIndex] - Texture index to use
2697
- * @param {number} [padding] - How many pixels padding around tiles
2775
+ * @param {number} [textureIndex] - Texture index to use
2776
+ * @param {number} [padding] - How many pixels padding around tiles
2698
2777
  * @return {TileInfo}
2699
2778
  * @example
2700
2779
  * tile(2) // a tile at index 2 using the default tile size of 16
2701
2780
  * tile(5, 8) // a tile at index 5 using a tile size of 8
2702
2781
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
2703
2782
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
2704
- * @memberof Draw
2705
- */
2783
+ * @memberof Draw */
2706
2784
  function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
2707
2785
  {
2708
2786
  if (headlessMode)
2709
2787
  return new TileInfo;
2710
2788
 
2711
2789
  // if size is a number, make it a vector
2712
- if (typeof size == 'number')
2790
+ if (typeof size === 'number')
2713
2791
  {
2714
2792
  ASSERT(size > 0);
2715
2793
  size = new Vector2(size, size);
@@ -2718,24 +2796,24 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
2718
2796
  // create tile info object
2719
2797
  const tileInfo = new TileInfo(new Vector2, size, textureIndex, padding);
2720
2798
 
2721
- // use get the pos of the tile
2799
+ // get the position of the tile
2722
2800
  const textureInfo = textureInfos[textureIndex];
2723
2801
  ASSERT(!!textureInfo, 'Texture not loaded');
2724
2802
  const sizePaddedX = size.x + padding*2;
2725
2803
  const sizePaddedY = size.y + padding*2;
2726
- if (typeof pos == 'number')
2804
+ if (typeof pos === 'number')
2727
2805
  {
2728
2806
  const cols = textureInfo.size.x / sizePaddedX |0;
2729
- ASSERT(cols>0, 'Tile size is too big for texture');
2807
+ ASSERT(cols > 0, 'Tile size is too big for texture');
2730
2808
  const posX = pos % cols, posY = (pos / cols) |0;
2731
2809
  tileInfo.pos.set(posX*sizePaddedX+padding, posY*sizePaddedY+padding);
2732
2810
  }
2733
2811
  else
2734
2812
  tileInfo.pos.set(pos.x*sizePaddedX+padding, pos.y*sizePaddedY+padding);
2735
- return tileInfo;
2813
+ return tileInfo;
2736
2814
  }
2737
2815
 
2738
- /**
2816
+ /**
2739
2817
  * Tile Info - Stores info about how to draw a tile
2740
2818
  */
2741
2819
  class TileInfo
@@ -2773,14 +2851,14 @@ class TileInfo
2773
2851
  */
2774
2852
  frame(frame)
2775
2853
  {
2776
- ASSERT(typeof frame == 'number');
2854
+ ASSERT(typeof frame === 'number');
2777
2855
  return this.offset(new Vector2(frame*(this.size.x+this.padding*2), 0));
2778
2856
  }
2779
2857
 
2780
2858
  /**
2781
2859
  * Set this tile to use a full image
2782
2860
  * @param {HTMLImageElement|OffscreenCanvas} image
2783
- * @param {WebGLTexture} [glTexture] - webgl texture
2861
+ * @param {WebGLTexture} [glTexture] - WebGL texture
2784
2862
  * @return {TileInfo}
2785
2863
  */
2786
2864
  setFullImage(image, glTexture)
@@ -2798,7 +2876,7 @@ class TextureInfo
2798
2876
  /**
2799
2877
  * Create a TextureInfo, called automatically by the engine
2800
2878
  * @param {HTMLImageElement|OffscreenCanvas} image
2801
- * @param {WebGLTexture} [glTexture] - webgl texture
2879
+ * @param {WebGLTexture} [glTexture] - WebGL texture
2802
2880
  */
2803
2881
  constructor(image, glTexture)
2804
2882
  {
@@ -2808,7 +2886,7 @@ class TextureInfo
2808
2886
  this.size = vec2(image.width, image.height);
2809
2887
  /** @property {Vector2} - inverse of the size, cached for rendering */
2810
2888
  this.sizeInverse = vec2(1/image.width, 1/image.height);
2811
- /** @property {WebGLTexture} - webgl texture */
2889
+ /** @property {WebGLTexture} - WebGL texture */
2812
2890
  this.glTexture = glTexture;
2813
2891
  }
2814
2892
 
@@ -2821,76 +2899,28 @@ class TextureInfo
2821
2899
  }
2822
2900
 
2823
2901
  ///////////////////////////////////////////////////////////////////////////////
2824
-
2825
- /** Convert from screen to world space coordinates
2826
- * @param {Vector2} screenPos
2827
- * @return {Vector2}
2828
- * @memberof Draw */
2829
- function screenToWorld(screenPos)
2830
- {
2831
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
2832
- let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
2833
- if (cameraAngle)
2834
- {
2835
- // apply camera rotation
2836
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
2837
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
2838
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
2839
- cameraPosRelativeX = rotatedX;
2840
- cameraPosRelativeY = rotatedY;
2841
- }
2842
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
2843
- }
2844
-
2845
- /** Convert from world to screen space coordinates
2846
- * @param {Vector2} worldPos
2847
- * @return {Vector2}
2848
- * @memberof Draw */
2849
- function worldToScreen(worldPos)
2850
- {
2851
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
2852
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
2853
- if (cameraAngle)
2854
- {
2855
- // apply inverse camera rotation
2856
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
2857
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
2858
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
2859
- cameraPosRelativeX = rotatedX;
2860
- cameraPosRelativeY = rotatedY;
2861
- }
2862
- return new Vector2
2863
- (
2864
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
2865
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
2866
- );
2867
- }
2868
-
2869
- /** Get the camera's visible area in world space
2870
- * @return {Vector2}
2871
- * @memberof Draw */
2872
- function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
2902
+ // Drawing functions
2873
2903
 
2874
2904
  /** Draw textured tile centered in world space, with color applied if using WebGL
2875
- * @param {Vector2} pos - Center of the tile in world space
2876
- * @param {Vector2} [size=(1,1)] - Size of the tile in world space
2877
- * @param {TileInfo}[tileInfo] - Tile info to use, untextured if undefined
2878
- * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2879
- * @param {number} [angle] - Angle to rotate by
2880
- * @param {boolean} [mirror] - If true image is flipped along the Y axis
2881
- * @param {Color} [additiveColor] - Additive color to be applied if any
2882
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2883
- * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
2905
+ * @param {Vector2} pos - Center of the tile in world space
2906
+ * @param {Vector2} [size=(1,1)] - Size of the tile in world space
2907
+ * @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
2908
+ * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2909
+ * @param {number} [angle] - Angle to rotate by
2910
+ * @param {boolean} [mirror] - Is image flipped along the Y axis?
2911
+ * @param {Color} [additiveColor] - Additive color to be applied if any
2912
+ * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
2913
+ * @param {boolean} [screenSpace=false] - Are the pos and size are in screen space?
2884
2914
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2885
2915
  * @memberof Draw */
2886
- function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2916
+ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
2887
2917
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2888
2918
  {
2889
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2890
2919
  ASSERT(isVector2(pos) && pos.isValid(), 'drawTile pos should be a vec2');
2891
2920
  ASSERT(isVector2(size) && size.isValid(), 'drawTile size should be a vec2');
2892
2921
  ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
2893
2922
  ASSERT(isNumber(angle), 'drawTile angle should be a number');
2923
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2894
2924
 
2895
2925
  const textureInfo = tileInfo && tileInfo.textureInfo;
2896
2926
  if (useWebGL)
@@ -2914,22 +2944,22 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2914
2944
  {
2915
2945
  const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
2916
2946
  const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
2917
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2918
- x + tileImageFixBleedX, y + tileImageFixBleedY,
2919
- x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2920
- color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2947
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2948
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
2949
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2950
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2921
2951
  }
2922
2952
  else
2923
2953
  {
2924
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2925
- x, y, x + w, y + h,
2926
- color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2954
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2955
+ x, y, x + w, y + h,
2956
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2927
2957
  }
2928
2958
  }
2929
2959
  else
2930
2960
  {
2931
2961
  // if no tile info, force untextured
2932
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
2962
+ glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
2933
2963
  }
2934
2964
  }
2935
2965
  else
@@ -2942,18 +2972,15 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2942
2972
  if (textureInfo)
2943
2973
  {
2944
2974
  // calculate uvs and render
2945
- const x = tileInfo.pos.x + tileFixBleedScale;
2946
- const y = tileInfo.pos.y + tileFixBleedScale;
2947
- const w = tileInfo.size.x - 2*tileFixBleedScale;
2948
- const h = tileInfo.size.y - 2*tileFixBleedScale;
2949
- context.globalAlpha = color.a; // only alpha is supported
2950
- context.drawImage(textureInfo.image, x, y, w, h, -.5, -.5, 1, 1);
2951
- context.globalAlpha = 1; // set back to full alpha
2975
+ const x = tileInfo.pos.x, y = tileInfo.pos.y;
2976
+ const w = tileInfo.size.x, h = tileInfo.size.y;
2977
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor);
2952
2978
  }
2953
2979
  else
2954
2980
  {
2955
- // if no tile info, force untextured
2956
- context.fillStyle = color.toString();
2981
+ // if no tile info, use untextured rect
2982
+ const c = additiveColor ? color.add(additiveColor) : color;
2983
+ context.fillStyle = c.toString();
2957
2984
  context.fillRect(-.5, -.5, 1, 1);
2958
2985
  }
2959
2986
  }, screenSpace, context);
@@ -2966,12 +2993,12 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=new Color,
2966
2993
  * @param {Color} [color=(1,1,1,1)]
2967
2994
  * @param {number} [angle]
2968
2995
  * @param {boolean} [useWebGL=glEnable]
2969
- * @param {boolean} [screenSpace=false]
2996
+ * @param {boolean} [screenSpace]
2970
2997
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2971
2998
  * @memberof Draw */
2972
2999
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2973
- {
2974
- drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
3000
+ {
3001
+ drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
2975
3002
  }
2976
3003
 
2977
3004
  /** Draw colored line between two points
@@ -2979,72 +3006,138 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2979
3006
  * @param {Vector2} posB
2980
3007
  * @param {number} [thickness]
2981
3008
  * @param {Color} [color=(1,1,1,1)]
3009
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
3010
+ * @param {number} [angle] - Angle to rotate by
2982
3011
  * @param {boolean} [useWebGL=glEnable]
2983
- * @param {boolean} [screenSpace=false]
3012
+ * @param {boolean} [screenSpace]
2984
3013
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2985
3014
  * @memberof Draw */
2986
- function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
3015
+ function drawLine(posA, posB, thickness=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
2987
3016
  {
2988
3017
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
2989
3018
  const size = vec2(thickness, halfDelta.length()*2);
2990
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
3019
+ pos = pos.add(posA.add(halfDelta));
3020
+ angle += halfDelta.angle();
3021
+ drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3022
+ }
3023
+
3024
+ /** Draw colored regular polygon using passed in number of sides
3025
+ * @param {Vector2} pos
3026
+ * @param {Vector2} [size=(1,1)]
3027
+ * @param {number} [sides]
3028
+ * @param {Color} [color=(1,1,1,1)]
3029
+ * @param {number} [angle]
3030
+ * @param {number} [lineWidth]
3031
+ * @param {Color} [lineColor=(0,0,0,1)]
3032
+ * @param {boolean} [useWebGL=glEnable]
3033
+ * @param {boolean} [screenSpace]
3034
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3035
+ * @memberof Draw */
3036
+ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3037
+ {
3038
+ // build regular polygon points
3039
+ const points = [];
3040
+ for (let i=sides; i--;)
3041
+ {
3042
+ const a = (i/sides)*PI*2;
3043
+ points.push(vec2(Math.sin(a)*size.x, Math.cos(a)*size.y));
3044
+ }
3045
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
2991
3046
  }
2992
3047
 
2993
3048
  /** Draw colored polygon using passed in points
2994
- * @param {Array<Vector2>} points - Array of Vector2 points
3049
+ * @param {Array<Vector2>} points - Array of Vector2 points
2995
3050
  * @param {Color} [color=(1,1,1,1)]
2996
- * @param {number} [lineWidth=0]
3051
+ * @param {number} [lineWidth]
2997
3052
  * @param {Color} [lineColor=(0,0,0,1)]
2998
- * @param {boolean} [screenSpace=false]
2999
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3053
+ * @param {Vector2} [pos=(0,0)] - Offset to apply
3054
+ * @param {number} [angle] - Angle to rotate by
3055
+ * @param {boolean} [useWebGL=glEnable]
3056
+ * @param {boolean} [screenSpace]
3057
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3000
3058
  * @memberof Draw */
3001
- function drawPoly(points, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
3059
+ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3002
3060
  {
3003
- ASSERT(isColor(color) && isColor(lineColor));
3004
- context.fillStyle = color.toString();
3005
- context.beginPath();
3006
- for (const point of screenSpace ? points : points.map(worldToScreen))
3007
- context.lineTo(point.x, point.y);
3008
- context.closePath();
3009
- context.fill();
3010
- if (lineWidth)
3061
+ ASSERT(isVector2(pos) && pos.isValid(), 'drawPoly pos should be a vec2');
3062
+ ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3063
+ ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3064
+ ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3065
+ ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3066
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3067
+ if (useWebGL)
3011
3068
  {
3012
- context.strokeStyle = lineColor.toString();
3013
- context.lineWidth = screenSpace ? lineWidth : lineWidth*cameraScale;
3014
- context.stroke();
3069
+ let scale = 1;
3070
+ if (screenSpace)
3071
+ {
3072
+ // convert to world space
3073
+ pos = screenToWorld(pos);
3074
+ scale = 1/cameraScale;
3075
+ }
3076
+ glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
3077
+ if (lineWidth > 0)
3078
+ glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
3079
+ }
3080
+ else
3081
+ {
3082
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3083
+ {
3084
+ context.fillStyle = color.toString();
3085
+ context.beginPath();
3086
+ for (const point of points)
3087
+ context.lineTo(point.x, point.y);
3088
+ context.closePath();
3089
+ context.fill();
3090
+ if (lineWidth)
3091
+ {
3092
+ context.strokeStyle = lineColor.toString();
3093
+ context.lineWidth = lineWidth;
3094
+ context.stroke();
3095
+ }
3096
+ }, screenSpace, context);
3015
3097
  }
3016
3098
  }
3017
3099
 
3018
3100
  /** Draw colored ellipse using passed in point
3019
3101
  * @param {Vector2} pos
3020
- * @param {number} [width=1]
3021
- * @param {number} [height=1]
3022
- * @param {number} [angle=0]
3102
+ * @param {Vector2} [size=(1,1)]
3023
3103
  * @param {Color} [color=(1,1,1,1)]
3024
- * @param {number} [lineWidth=0]
3104
+ * @param {number} [angle]
3105
+ * @param {number} [lineWidth]
3025
3106
  * @param {Color} [lineColor=(0,0,0,1)]
3026
- * @param {boolean} [screenSpace=false]
3027
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3107
+ * @param {boolean} [useWebGL=glEnable]
3108
+ * @param {boolean} [screenSpace]
3109
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3028
3110
  * @memberof Draw */
3029
- function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
3111
+ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3030
3112
  {
3031
- ASSERT(isColor(color) && isColor(lineColor));
3032
- if (!screenSpace)
3113
+ ASSERT(isVector2(pos) && pos.isValid(), 'drawEllipse pos should be a vec2');
3114
+ ASSERT(isVector2(size) && size.isValid(), 'drawEllipse size should be a vec2');
3115
+ ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3116
+ ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3117
+ ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
3118
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3119
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3120
+ if (useWebGL)
3033
3121
  {
3034
- pos = worldToScreen(pos);
3035
- width *= cameraScale;
3036
- height *= cameraScale;
3037
- lineWidth *= cameraScale;
3122
+ // draw as a regular polygon
3123
+ const sides = glCircleSides;
3124
+ drawRegularPoly(pos, size, sides, color, lineWidth, lineColor, angle, useWebGL, screenSpace, context);
3038
3125
  }
3039
- context.fillStyle = color.toString();
3040
- context.beginPath();
3041
- context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
3042
- context.fill();
3043
- if (lineWidth)
3126
+ else
3044
3127
  {
3045
- context.strokeStyle = lineColor.toString();
3046
- context.lineWidth = lineWidth;
3047
- context.stroke();
3128
+ drawCanvas2D(pos, vec2(1), angle, false, context=>
3129
+ {
3130
+ context.fillStyle = color.toString();
3131
+ context.beginPath();
3132
+ context.ellipse(0, 0, size.x, size.y, 0, 0, 9);
3133
+ context.fill();
3134
+ if (lineWidth)
3135
+ {
3136
+ context.strokeStyle = lineColor.toString();
3137
+ context.lineWidth = lineWidth;
3138
+ context.stroke();
3139
+ }
3140
+ }, screenSpace, context);
3048
3141
  }
3049
3142
  }
3050
3143
 
@@ -3054,11 +3147,12 @@ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth
3054
3147
  * @param {Color} [color=(1,1,1,1)]
3055
3148
  * @param {number} [lineWidth=0]
3056
3149
  * @param {Color} [lineColor=(0,0,0,1)]
3057
- * @param {boolean} [screenSpace=false]
3058
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3150
+ * @param {boolean} [useWebGL=glEnable]
3151
+ * @param {boolean} [screenSpace]
3152
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3059
3153
  * @memberof Draw */
3060
- function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=drawContext)
3061
- { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
3154
+ function drawCircle(pos, radius=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3155
+ { drawEllipse(pos, vec2(radius), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context); }
3062
3156
 
3063
3157
  /** Draw directly to a 2d canvas context in world space
3064
3158
  * @param {Vector2} pos
@@ -3066,7 +3160,7 @@ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new C
3066
3160
  * @param {number} angle
3067
3161
  * @param {boolean} [mirror]
3068
3162
  * @param {Function} [drawFunction]
3069
- * @param {boolean} [screenSpace=false]
3163
+ * @param {boolean} [screenSpace=false]
3070
3164
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3071
3165
  * @memberof Draw */
3072
3166
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
@@ -3085,6 +3179,9 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3085
3179
  context.restore();
3086
3180
  }
3087
3181
 
3182
+ ///////////////////////////////////////////////////////////////////////////////
3183
+ // Text Drawing Functions
3184
+
3088
3185
  /** Draw text on main canvas in world space
3089
3186
  * Automatically splits new lines into rows
3090
3187
  * @param {string} text
@@ -3133,7 +3230,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3133
3230
  * @param {number} [maxWidth]
3134
3231
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3135
3232
  * @memberof Draw */
3136
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
3233
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3137
3234
  {
3138
3235
  context.fillStyle = color.toString();
3139
3236
  context.strokeStyle = lineColor.toString();
@@ -3154,22 +3251,67 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
3154
3251
  });
3155
3252
  }
3156
3253
 
3157
- /** Enable normal or additive blend mode
3158
- * @param {boolean} [additive]
3159
- * @param {boolean} [useWebGL=glEnable]
3160
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3254
+ ///////////////////////////////////////////////////////////////////////////////
3255
+ // Drawing utilities
3256
+
3257
+ /** Convert from screen to world space coordinates
3258
+ * @param {Vector2} screenPos
3259
+ * @return {Vector2}
3161
3260
  * @memberof Draw */
3162
- function setBlendMode(additive=false, useWebGL=glEnable, context)
3261
+ function screenToWorld(screenPos)
3163
3262
  {
3164
- ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3165
- if (useWebGL)
3166
- glAdditive = additive;
3167
- else
3263
+ let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3264
+ let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3265
+ if (cameraAngle)
3168
3266
  {
3169
- if (!context)
3170
- context = drawContext;
3171
- context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
3267
+ // apply camera rotation
3268
+ const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
3269
+ const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3270
+ const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3271
+ cameraPosRelativeX = rotatedX;
3272
+ cameraPosRelativeY = rotatedY;
3172
3273
  }
3274
+ return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
3275
+ }
3276
+
3277
+ /** Convert from world to screen space coordinates
3278
+ * @param {Vector2} worldPos
3279
+ * @return {Vector2}
3280
+ * @memberof Draw */
3281
+ function worldToScreen(worldPos)
3282
+ {
3283
+ let cameraPosRelativeX = worldPos.x - cameraPos.x;
3284
+ let cameraPosRelativeY = worldPos.y - cameraPos.y;
3285
+ if (cameraAngle)
3286
+ {
3287
+ // apply inverse camera rotation
3288
+ const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
3289
+ const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3290
+ const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3291
+ cameraPosRelativeX = rotatedX;
3292
+ cameraPosRelativeY = rotatedY;
3293
+ }
3294
+ return new Vector2
3295
+ (
3296
+ cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
3297
+ cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
3298
+ );
3299
+ }
3300
+
3301
+ /** Get the camera's visible area in world space
3302
+ * @return {Vector2}
3303
+ * @memberof Draw */
3304
+ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
3305
+
3306
+ /** Enable normal or additive blend mode
3307
+ * @param {boolean} [additive]
3308
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
3309
+ * @memberof Draw */
3310
+ function setBlendMode(additive=false, context)
3311
+ {
3312
+ glAdditive = additive;
3313
+ context ||= drawContext;
3314
+ context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
3173
3315
  }
3174
3316
 
3175
3317
  /** Combines all LittleJS canvases onto the main canvas and clears them
@@ -3186,11 +3328,107 @@ function combineCanvases()
3186
3328
  overlayCanvas.width |= 0;
3187
3329
  }
3188
3330
 
3331
+ /** Helper function to draw an image with color and additive color applied
3332
+ * This is slower then normal drawImage when color is applied
3333
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
3334
+ * @param {HTMLImageElement|OffscreenCanvas} image
3335
+ * @param {number} sx
3336
+ * @param {number} sy
3337
+ * @param {number} sWidth
3338
+ * @param {number} sHeight
3339
+ * @param {number} dx
3340
+ * @param {number} dy
3341
+ * @param {number} dWidth
3342
+ * @param {number} dHeight
3343
+ * @param {Color} color
3344
+ * @param {Color} [additiveColor]
3345
+ * @memberof Draw */
3346
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor)
3347
+ {
3348
+ function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
3349
+ function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
3350
+ const sx2 = tileFixBleedScale;
3351
+ const sy2 = tileFixBleedScale;
3352
+ const sWidth2 = sWidth - 2*tileFixBleedScale;
3353
+ const sHeight2 = sHeight - 2*tileFixBleedScale;
3354
+ if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
3355
+ {
3356
+ // white texture with no additive alpha, no need to tint
3357
+ context.globalAlpha = color.a;
3358
+ context.drawImage(image, sx+sx2, sy+sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3359
+ context.globalAlpha = 1;
3360
+ }
3361
+ else
3362
+ {
3363
+ // copy to offscreen canvas
3364
+ workCanvas.width = sWidth;
3365
+ workCanvas.height = sHeight;
3366
+ workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
3367
+
3368
+ // tint image using offscreen work context
3369
+ const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
3370
+ const data = imageData.data;
3371
+ if (additiveColor && !isBlack(additiveColor))
3372
+ {
3373
+ // slower path with additive color
3374
+ const colorMultiply = [color.r, color.g, color.b, color.a];
3375
+ const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
3376
+ for (let i = 0; i < data.length; ++i)
3377
+ data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
3378
+ workContext.putImageData(imageData, 0, 0);
3379
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3380
+ }
3381
+ else
3382
+ {
3383
+ // faster path with no additive color
3384
+ for (let i = 0; i < data.length; i+=4)
3385
+ {
3386
+ data[i ] *= color.r;
3387
+ data[i+1] *= color.g;
3388
+ data[i+2] *= color.b;
3389
+ }
3390
+ workContext.putImageData(imageData, 0, 0);
3391
+ context.globalAlpha = color.a;
3392
+ context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
3393
+ context.globalAlpha = 1;
3394
+ }
3395
+ }
3396
+ }
3397
+
3398
+
3399
+ /** Returns true if fullscreen mode is active
3400
+ * @return {boolean}
3401
+ * @memberof Draw */
3402
+ function isFullscreen() { return !!document.fullscreenElement; }
3403
+
3404
+ /** Toggle fullscreen mode
3405
+ * @memberof Draw */
3406
+ function toggleFullscreen()
3407
+ {
3408
+ const rootElement = mainCanvas.parentElement;
3409
+ if (isFullscreen())
3410
+ {
3411
+ if (document.exitFullscreen)
3412
+ document.exitFullscreen();
3413
+ }
3414
+ else if (rootElement.requestFullscreen)
3415
+ rootElement.requestFullscreen();
3416
+ }
3417
+
3418
+ /** Set the cursor style
3419
+ * @param {string} [cursorStyle] - CSS cursor style (auto, none, crosshair, etc)
3420
+ * @memberof Draw */
3421
+ function setCursor(cursorStyle = 'auto')
3422
+ {
3423
+ const rootElement = mainCanvas.parentElement;
3424
+ rootElement.style.cursor = cursorStyle;
3425
+ }
3426
+
3189
3427
  ///////////////////////////////////////////////////////////////////////////////
3190
3428
 
3191
3429
  let engineFontImage;
3192
3430
 
3193
- /**
3431
+ /**
3194
3432
  * Font Image Object - Draw text on a 2D canvas by using characters in an image
3195
3433
  * - 96 characters (from space to tilde) are stored in an image
3196
3434
  * - Uses a default 8x8 font if none is supplied
@@ -3198,7 +3436,7 @@ let engineFontImage;
3198
3436
  * @example
3199
3437
  * // use built in font
3200
3438
  * const font = new FontImage;
3201
- *
3439
+ *
3202
3440
  * // draw text
3203
3441
  * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
3204
3442
  */
@@ -3208,7 +3446,6 @@ class FontImage
3208
3446
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
3209
3447
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3210
3448
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
3211
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
3212
3449
  */
3213
3450
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
3214
3451
  {
@@ -3222,7 +3459,6 @@ class FontImage
3222
3459
  this.image = image || engineFontImage;
3223
3460
  this.tileSize = tileSize;
3224
3461
  this.paddingSize = paddingSize;
3225
- this.context = context;
3226
3462
  }
3227
3463
 
3228
3464
  /** Draw text in world space using the image font
@@ -3230,30 +3466,39 @@ class FontImage
3230
3466
  * @param {Vector2} pos
3231
3467
  * @param {number} [scale=.25]
3232
3468
  * @param {boolean} [center]
3469
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
3233
3470
  */
3234
- drawText(text, pos, scale=1, center)
3471
+ drawText(text, pos, scale=1, center, context=drawContext)
3235
3472
  {
3236
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
3473
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
3237
3474
  }
3238
3475
 
3239
- /** Draw text in screen space using the image font
3476
+ /** Draw text on overlay canvas in world space using the image font
3240
3477
  * @param {string} text
3241
3478
  * @param {Vector2} pos
3242
3479
  * @param {number} [scale]
3243
3480
  * @param {boolean} [center]
3244
3481
  */
3245
- drawTextScreen(text, pos, scale=4, center)
3482
+ drawTextOverlay(text, pos, scale=4, center)
3483
+ { this.drawText(text, pos, scale, center, overlayContext); }
3484
+
3485
+ /** Draw text on overlay canvas in screen space using the image font
3486
+ * @param {string} text
3487
+ * @param {Vector2} pos
3488
+ * @param {number} [scale]
3489
+ * @param {boolean} [center]
3490
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3491
+ */
3492
+ drawTextScreen(text, pos, scale=4, center, context=overlayContext)
3246
3493
  {
3247
- const context = this.context;
3248
3494
  context.save();
3249
-
3250
3495
  const size = this.tileSize;
3251
3496
  const drawSize = size.add(this.paddingSize).scale(scale);
3252
3497
  const cols = this.image.width / this.tileSize.x |0;
3253
3498
  (text+'').split('\n').forEach((line, i)=>
3254
3499
  {
3255
3500
  const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
3256
- for(let j=line.length; j--;)
3501
+ for (let j=line.length; j--;)
3257
3502
  {
3258
3503
  // draw each character
3259
3504
  let charCode = line[j].charCodeAt(0);
@@ -3265,46 +3510,14 @@ class FontImage
3265
3510
  const x = tile % cols;
3266
3511
  const y = tile / cols |0;
3267
3512
  const drawPos = pos.add(vec2(j,i).multiply(drawSize));
3268
- context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
3513
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
3269
3514
  drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
3270
3515
  }
3271
3516
  });
3272
-
3273
3517
  context.restore();
3274
3518
  }
3275
- }
3276
-
3277
- ///////////////////////////////////////////////////////////////////////////////
3278
- // Display functions
3279
-
3280
- /** Returns true if fullscreen mode is active
3281
- * @return {boolean}
3282
- * @memberof Draw */
3283
- function isFullscreen() { return !!document.fullscreenElement; }
3284
-
3285
- /** Toggle fullscreen mode
3286
- * @memberof Draw */
3287
- function toggleFullscreen()
3288
- {
3289
- const rootElement = mainCanvas.parentElement;
3290
- if (isFullscreen())
3291
- {
3292
- if (document.exitFullscreen)
3293
- document.exitFullscreen();
3294
- }
3295
- else if (rootElement.requestFullscreen)
3296
- rootElement.requestFullscreen();
3297
- }
3298
-
3299
- /** Set the cursor style
3300
- * @param {string} cursorStyle - CSS cursor style (auto, none, crosshair, etc)
3301
- * @memberof Draw */
3302
- function setCursor(cursorStyle = 'auto')
3303
- {
3304
- const rootElement = mainCanvas.parentElement;
3305
- rootElement.style.cursor = cursorStyle;
3306
3519
  }
3307
- /**
3520
+ /**
3308
3521
  * LittleJS Input System
3309
3522
  * - Tracks keyboard down, pressed, and released
3310
3523
  * - Tracks mouse buttons, position, and wheel
@@ -3320,10 +3533,10 @@ function setCursor(cursorStyle = 'auto')
3320
3533
  * @return {boolean}
3321
3534
  * @memberof Input */
3322
3535
  function keyIsDown(key, device=0)
3323
- {
3536
+ {
3324
3537
  ASSERT(key !== undefined, 'key is undefined');
3325
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
3326
- return inputData[device] && !!(inputData[device][key] & 1);
3538
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
3539
+ return inputData[device] && !!(inputData[device][key] & 1);
3327
3540
  }
3328
3541
 
3329
3542
  /** Returns true if device key was pressed this frame
@@ -3332,10 +3545,10 @@ function keyIsDown(key, device=0)
3332
3545
  * @return {boolean}
3333
3546
  * @memberof Input */
3334
3547
  function keyWasPressed(key, device=0)
3335
- {
3548
+ {
3336
3549
  ASSERT(key !== undefined, 'key is undefined');
3337
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
3338
- return inputData[device] && !!(inputData[device][key] & 2);
3550
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
3551
+ return inputData[device] && !!(inputData[device][key] & 2);
3339
3552
  }
3340
3553
 
3341
3554
  /** Returns true if device key was released this frame
@@ -3344,9 +3557,9 @@ function keyWasPressed(key, device=0)
3344
3557
  * @return {boolean}
3345
3558
  * @memberof Input */
3346
3559
  function keyWasReleased(key, device=0)
3347
- {
3560
+ {
3348
3561
  ASSERT(key !== undefined, 'key is undefined');
3349
- ASSERT(device > 0 || typeof key != 'number' || key < 3, 'use code string for keyboard');
3562
+ ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
3350
3563
  return inputData[device] && !!(inputData[device][key] & 4);
3351
3564
  }
3352
3565
 
@@ -3468,7 +3681,7 @@ function gamepadWasReleased(button, gamepad=0)
3468
3681
  * @param {number} [gamepad]
3469
3682
  * @return {Vector2}
3470
3683
  * @memberof Input */
3471
- function gamepadStick(stick, gamepad=0)
3684
+ function gamepadStick(stick, gamepad=0)
3472
3685
  { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
3473
3686
 
3474
3687
  ///////////////////////////////////////////////////////////////////////////////
@@ -3545,10 +3758,10 @@ function inputInit()
3545
3758
  {
3546
3759
  // handle remapping wasd keys to directions
3547
3760
  return inputWASDEmulateDirection ?
3548
- c == 'KeyW' ? 'ArrowUp' :
3549
- c == 'KeyS' ? 'ArrowDown' :
3550
- c == 'KeyA' ? 'ArrowLeft' :
3551
- c == 'KeyD' ? 'ArrowRight' : c : c;
3761
+ c === 'KeyW' ? 'ArrowUp' :
3762
+ c === 'KeyS' ? 'ArrowDown' :
3763
+ c === 'KeyA' ? 'ArrowLeft' :
3764
+ c === 'KeyD' ? 'ArrowRight' : c : c;
3552
3765
  }
3553
3766
  function onMouseDown(e)
3554
3767
  {
@@ -3556,9 +3769,9 @@ function inputInit()
3556
3769
  return;
3557
3770
 
3558
3771
  // fix stalled audio requiring user interaction
3559
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
3772
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
3560
3773
  audioContext.resume();
3561
-
3774
+
3562
3775
  isUsingGamepad = false;
3563
3776
  inputData[0][e.button] = 3;
3564
3777
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
@@ -3601,8 +3814,8 @@ function gamepadsUpdate()
3601
3814
  const applyDeadZones = (v)=>
3602
3815
  {
3603
3816
  const min=.3, max=.8;
3604
- const deadZone = (v)=>
3605
- v > min ? percent( v, min, max) :
3817
+ const deadZone = (v)=>
3818
+ v > min ? percent(v, min, max) :
3606
3819
  v < -min ? -percent(-v, min, max) : 0;
3607
3820
  return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
3608
3821
  }
@@ -3610,30 +3823,29 @@ function gamepadsUpdate()
3610
3823
  // update touch gamepad if enabled
3611
3824
  if (touchGamepadEnable && isTouchDevice)
3612
3825
  {
3613
- ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
3614
- if (touchGamepadTimer.isSet())
3826
+ if (!touchGamepadTimer.isSet())
3827
+ return;
3828
+
3829
+ // read virtual analog stick
3830
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
3831
+ sticks[0] = vec2();
3832
+ if (touchGamepadAnalog)
3833
+ sticks[0] = applyDeadZones(touchGamepadStick);
3834
+ else if (touchGamepadStick.lengthSquared() > .3)
3615
3835
  {
3616
- // read virtual analog stick
3617
- const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
3618
- sticks[0] = vec2();
3619
- if (touchGamepadAnalog)
3620
- sticks[0] = applyDeadZones(touchGamepadStick);
3621
- else if (touchGamepadStick.lengthSquared() > .3)
3622
- {
3623
- // convert to 8 way dpad
3624
- sticks[0].x = Math.round(touchGamepadStick.x);
3625
- sticks[0].y = -Math.round(touchGamepadStick.y);
3626
- sticks[0] = sticks[0].clampLength();
3627
- }
3836
+ // convert to 8 way dpad
3837
+ sticks[0].x = Math.round(touchGamepadStick.x);
3838
+ sticks[0].y = -Math.round(touchGamepadStick.y);
3839
+ sticks[0] = sticks[0].clampLength();
3840
+ }
3628
3841
 
3629
- // read virtual gamepad buttons
3630
- const data = inputData[1] || (inputData[1] = []);
3631
- for (let i=10; i--;)
3632
- {
3633
- const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
3634
- const wasDown = gamepadIsDown(j,0);
3635
- data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
3636
- }
3842
+ // read virtual gamepad buttons
3843
+ const data = inputData[1] || (inputData[1] = []);
3844
+ for (let i=10; i--;)
3845
+ {
3846
+ const j = i === 3 ? 2 : i === 2 ? 3 : i; // fix button locations
3847
+ const wasDown = gamepadIsDown(j,0);
3848
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
3637
3849
  }
3638
3850
  }
3639
3851
 
@@ -3659,7 +3871,7 @@ function gamepadsUpdate()
3659
3871
  // read analog sticks
3660
3872
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
3661
3873
  sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
3662
-
3874
+
3663
3875
  // read buttons
3664
3876
  for (let j = gamepad.buttons.length; j--;)
3665
3877
  {
@@ -3675,14 +3887,14 @@ function gamepadsUpdate()
3675
3887
  {
3676
3888
  // copy dpad to left analog stick when pressed
3677
3889
  const dpad = vec2(
3678
- (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
3890
+ (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
3679
3891
  (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
3680
3892
  if (dpad.lengthSquared())
3681
3893
  sticks[0] = dpad.clampLength();
3682
3894
  }
3683
3895
 
3684
3896
  // disable touch gamepad if using real gamepad
3685
- touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
3897
+ touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
3686
3898
  }
3687
3899
  }
3688
3900
  }
@@ -3707,20 +3919,13 @@ function vibrateStop() { vibrate(0); }
3707
3919
  const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
3708
3920
 
3709
3921
  // touch gamepad internal variables
3710
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
3922
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
3711
3923
 
3712
3924
  // enable touch input mouse passthrough
3713
3925
  function touchInputInit()
3714
3926
  {
3715
3927
  // add non passive touch event listeners
3716
3928
  let handleTouch = handleTouchDefault;
3717
- if (touchGamepadEnable)
3718
- {
3719
- // touch input internal variables
3720
- handleTouch = handleTouchGamepad;
3721
- touchGamepadButtons = [];
3722
- touchGamepadStick = vec2();
3723
- }
3724
3929
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
3725
3930
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
3726
3931
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
@@ -3729,8 +3934,15 @@ function touchInputInit()
3729
3934
  let wasTouching;
3730
3935
  function handleTouchDefault(e)
3731
3936
  {
3937
+ if (!touchInputEnable)
3938
+ return;
3939
+
3940
+ // route touch to gamepad
3941
+ if (touchGamepadEnable)
3942
+ handleTouchGamepad(e);
3943
+
3732
3944
  // fix stalled audio requiring user interaction
3733
- if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
3945
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
3734
3946
  audioContext.resume();
3735
3947
 
3736
3948
  // check if touching and pass to mouse events
@@ -3759,7 +3971,7 @@ function touchInputInit()
3759
3971
  // prevent default handling like copy and magnifier lens
3760
3972
  if (inputPreventDefault && document.hasFocus()) // allow document to get focus
3761
3973
  e.preventDefault();
3762
-
3974
+
3763
3975
  // must return true so the document will get focus
3764
3976
  return true;
3765
3977
  }
@@ -3771,7 +3983,7 @@ function touchInputInit()
3771
3983
  touchGamepadStick = vec2();
3772
3984
  touchGamepadButtons = [];
3773
3985
  isUsingGamepad = true;
3774
-
3986
+
3775
3987
  const touching = e.touches.length;
3776
3988
  if (touching)
3777
3989
  {
@@ -3780,9 +3992,6 @@ function touchInputInit()
3780
3992
  {
3781
3993
  // touch anywhere to press start when paused
3782
3994
  touchGamepadButtons[9] = 1;
3783
-
3784
- // call default touch handler so normal touch events still work
3785
- handleTouchDefault(e);
3786
3995
  return;
3787
3996
  }
3788
3997
  }
@@ -3813,12 +4022,6 @@ function touchInputInit()
3813
4022
  touchGamepadButtons[9] = 1;
3814
4023
  }
3815
4024
  }
3816
-
3817
- // call default touch handler so normal touch events still work
3818
- handleTouchDefault(e);
3819
-
3820
- // must return true so the document will get focus
3821
- return true;
3822
4025
  }
3823
4026
  }
3824
4027
 
@@ -3828,7 +4031,7 @@ function touchGamepadRender()
3828
4031
  if (!touchInputEnable || !isTouchDevice || headlessMode) return;
3829
4032
  if (!touchGamepadEnable || !touchGamepadTimer.isSet())
3830
4033
  return;
3831
-
4034
+
3832
4035
  // fade off when not touching or paused
3833
4036
  const alpha = percent(touchGamepadTimer.get(), 4, 3);
3834
4037
  if (!alpha || paused)
@@ -3854,16 +4057,16 @@ function touchGamepadRender()
3854
4057
  }
3855
4058
  else // draw cross shaped gamepad
3856
4059
  {
3857
- for(let i=10; i--;)
4060
+ for (let i=10; i--;)
3858
4061
  {
3859
4062
  const angle = i*PI/4;
3860
4063
  context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
3861
4064
  i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
3862
- i==1 && context.fill();
4065
+ i===1 && context.fill();
3863
4066
  }
3864
4067
  context.stroke();
3865
4068
  }
3866
-
4069
+
3867
4070
  // draw right face buttons
3868
4071
  const rightCenter = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
3869
4072
  for (let i=4; i--;)
@@ -3898,8 +4101,8 @@ function pointerLockExit() { document.exitPointerLock && document.exitPointerLoc
3898
4101
  /** Check if pointer is locked (true if locked)
3899
4102
  * @return {boolean}
3900
4103
  * @memberof Input */
3901
- function pointerLockIsActive() { return document.pointerLockElement == mainCanvas }
3902
- /**
4104
+ function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
4105
+ /**
3903
4106
  * LittleJS Audio System
3904
4107
  * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
3905
4108
  * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
@@ -3920,10 +4123,21 @@ let audioContext = new AudioContext;
3920
4123
  * @memberof Audio */
3921
4124
  let audioMasterGain;
3922
4125
 
4126
+ /** Default sample rate used for sounds
4127
+ * @default 44100
4128
+ * @memberof Audio */
4129
+ const audioDefaultSampleRate = 44100;
4130
+
4131
+ /** Check if the audio context is running and available for playback
4132
+ * @return {boolean} - True if the audio context is running
4133
+ * @memberof Audio */
4134
+ function audioIsRunning()
4135
+ { return audioContext.state === 'running'; }
4136
+
3923
4137
  function audioInit()
3924
4138
  {
3925
4139
  if (!soundEnable || headlessMode) return;
3926
-
4140
+
3927
4141
  audioMasterGain = audioContext.createGain();
3928
4142
  audioMasterGain.connect(audioContext.destination);
3929
4143
  audioMasterGain.gain.value = soundVolume; // set starting value
@@ -3931,14 +4145,14 @@ function audioInit()
3931
4145
 
3932
4146
  ///////////////////////////////////////////////////////////////////////////////
3933
4147
 
3934
- /**
4148
+ /**
3935
4149
  * Sound Object - Stores a sound for later use and can be played positionally
3936
- *
4150
+ *
3937
4151
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
3938
4152
  * @example
3939
4153
  * // create a sound
3940
4154
  * const sound_example = new Sound([.5,.5]);
3941
- *
4155
+ *
3942
4156
  * // play the sound
3943
4157
  * sound_example.play();
3944
4158
  */
@@ -3955,33 +4169,41 @@ class Sound
3955
4169
 
3956
4170
  /** @property {number} - World space max range of sound */
3957
4171
  this.range = range;
3958
-
3959
4172
  /** @property {number} - At what percentage of range should it start tapering */
3960
4173
  this.taper = taper;
3961
-
3962
4174
  /** @property {number} - How much to randomize frequency each time sound plays */
3963
4175
  this.randomness = 0;
4176
+ /** @property {number} - Sample rate for this sound */
4177
+ this.sampleRate = audioDefaultSampleRate;
4178
+ /** @property {number} - Percentage of this sound currently loaded */
4179
+ this.loadedPercent = 0;
3964
4180
 
4181
+ // generate zzfx sound now for fast playback
3965
4182
  if (zzfxSound)
3966
4183
  {
3967
- // generate zzfx sound now for fast playback
3968
- const defaultRandomness = .05;
3969
- this.randomness = zzfxSound[1] != undefined ? zzfxSound[1] : defaultRandomness;
3970
- zzfxSound[1] = 0; // generate without randomness
4184
+ // remove randomness so it can be applied on playback
4185
+ const randomnessIndex = 1, defaultRandomness = .05;
4186
+ this.randomness = zzfxSound[randomnessIndex] !== undefined ?
4187
+ zzfxSound[randomnessIndex] : defaultRandomness;
4188
+ zzfxSound[randomnessIndex] = 0;
4189
+
4190
+ // generate the zzfx samples
3971
4191
  this.sampleChannels = [zzfxG(...zzfxSound)];
3972
- this.sampleRate = zzfxR;
4192
+ this.loadedPercent = 1;
3973
4193
  }
3974
4194
  }
3975
4195
 
3976
4196
  /** Play the sound
3977
- * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
3978
- * @param {number} [volume] - How much to scale volume by (in addition to range fade)
3979
- * @param {number} [pitch] - How much to scale pitch by (also adjusted by this.randomness)
3980
- * @param {number} [randomnessScale] - How much to scale randomness
3981
- * @param {boolean} [loop] - Should the sound loop
3982
- * @return {AudioBufferSourceNode} - The audio source node
4197
+ * Sounds may not play until a user interaction occurs
4198
+ * @param {Vector2} [pos] - World space position to play the sound if any
4199
+ * @param {number} [volume] - How much to scale volume by
4200
+ * @param {number} [pitch] - How much to scale pitch by
4201
+ * @param {number} [randomnessScale] - How much to scale pitch randomness
4202
+ * @param {boolean} [loop] - Should the sound loop?
4203
+ * @param {boolean} [paused] - Should the sound start paused
4204
+ * @return {SoundInstance} - The audio source node
3983
4205
  */
3984
- play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
4206
+ play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
3985
4207
  {
3986
4208
  if (!soundEnable || headlessMode) return;
3987
4209
  if (!this.sampleChannels) return;
@@ -4004,75 +4226,55 @@ class Sound
4004
4226
  // get pan from screen space coords
4005
4227
  pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
4006
4228
  }
4007
-
4008
- // play the sound
4009
- const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
4010
- this.gainNode = audioContext.createGain();
4011
- this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
4012
- return this.source;
4013
- }
4014
-
4015
- /** Set the sound volume of the most recently played instance of this sound
4016
- * @param {number} [volume] - How much to scale volume by
4017
- */
4018
- setVolume(volume=1)
4019
- {
4020
- if (this.gainNode)
4021
- this.gainNode.gain.value = volume;
4022
- }
4023
-
4024
- /** Stop the last instance of this sound that was played
4025
- * @param {number} [fadeTime] - How long to fade out (seconds)
4026
- */
4027
- stop(fadeTime=0)
4028
- {
4029
- if (!this.source)
4030
- return;
4031
4229
 
4032
- // ramp off gain
4033
- const startFade = audioContext.currentTime;
4034
- const endFade = startFade + fadeTime;
4035
- this.gainNode.gain.linearRampToValueAtTime(1, startFade);
4036
- this.gainNode.gain.linearRampToValueAtTime(0, endFade);
4037
- this.source.stop(endFade);
4038
- this.source = undefined;
4230
+ // Create and return sound instance
4231
+ const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
4232
+ return new SoundInstance(this, volume, rate, pan, loop, paused);
4039
4233
  }
4040
4234
 
4041
- /** Get source of most recent instance of this sound that was played
4042
- * @return {AudioBufferSourceNode}
4235
+ /** Play a music track that loops by default
4236
+ * @param {number} [volume] - Volume to play the music at
4237
+ * @param {boolean} [loop] - Should the music loop?
4238
+ * @param {boolean} [paused] - Should the music start paused
4239
+ * @return {SoundInstance} - The audio source node
4043
4240
  */
4044
- getSource() { return this.source; }
4241
+ playMusic(volume=1, loop=true, paused=false)
4242
+ { return this.play(undefined, volume, 1, 0, loop, paused); }
4045
4243
 
4046
- /** Play the sound as a note with a semitone offset
4244
+ /** Play the sound as a musical note with a semitone offset
4245
+ * This can be used to play music with chromatic scales
4047
4246
  * @param {number} semitoneOffset - How many semitones to offset pitch
4048
- * @param {Vector2} [pos] - World space position to play the sound, sound is not attenuated if null
4049
- * @param {number} [volume=1] - How much to scale volume by (in addition to range fade)
4050
- * @return {AudioBufferSourceNode} - The audio source node
4247
+ * @param {Vector2} [pos] - World space position to play the sound if any
4248
+ * @param {number} [volume=1] - How much to scale volume by
4249
+ * @return {SoundInstance} - The audio source node
4051
4250
  */
4052
4251
  playNote(semitoneOffset, pos, volume)
4053
- { return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
4252
+ {
4253
+ const pitch = getNoteFrequency(semitoneOffset, 1);
4254
+ return this.play(pos, volume, pitch, 0);
4255
+ }
4054
4256
 
4055
4257
  /** Get how long this sound is in seconds
4056
4258
  * @return {number} - How long the sound is in seconds (undefined if loading)
4057
4259
  */
4058
- getDuration()
4260
+ getDuration()
4059
4261
  { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
4060
-
4061
- /** Check if sound is loading, for sounds fetched from a url
4062
- * @return {boolean} - True if sound is loading and not ready to play
4262
+
4263
+ /** Check if sound is loaded, for sounds fetched from a url
4264
+ * @return {boolean} - True if sound is loaded and ready to play
4063
4265
  */
4064
- isLoading() { return !this.sampleChannels; }
4266
+ isLoaded() { return this.loadedPercent === 1; }
4065
4267
  }
4066
4268
 
4067
4269
  ///////////////////////////////////////////////////////////////////////////////
4068
4270
 
4069
- /**
4271
+ /**
4070
4272
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
4071
4273
  * - this can be used to play wave, mp3, and ogg files
4072
4274
  * @example
4073
4275
  * // create a sound
4074
4276
  * const sound_example = new SoundWave('sound.mp3');
4075
- *
4277
+ *
4076
4278
  * // play the sound
4077
4279
  * sound_example.play();
4078
4280
  */
@@ -4104,26 +4306,202 @@ class SoundWave extends Sound
4104
4306
  const response = await fetch(filename);
4105
4307
  const arrayBuffer = await response.arrayBuffer();
4106
4308
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
4107
- this.sampleChannels = [];
4108
- for (let i = audioBuffer.numberOfChannels; i--;)
4109
- this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
4309
+
4310
+ // convert audio buffer to sample channels across multiple frames
4311
+ const channelCount = audioBuffer.numberOfChannels;
4312
+ const samplesPerFrame = 1e5;
4313
+ const sampleChannels = [];
4314
+ for (let channel = 0; channel < channelCount; channel++)
4315
+ {
4316
+ const channelData = audioBuffer.getChannelData(channel);
4317
+ const channelLength = channelData.length;
4318
+ sampleChannels[channel] = new Array(channelLength);
4319
+ let sampleIndex = 0;
4320
+ while (sampleIndex < channelLength)
4321
+ {
4322
+ // yield to next frame
4323
+ await new Promise(resolve => setTimeout(resolve, 0));
4324
+
4325
+ // copy chunk of samples
4326
+ const endIndex = min(sampleIndex + samplesPerFrame, channelLength);
4327
+ for (; sampleIndex < endIndex; sampleIndex++)
4328
+ sampleChannels[channel][sampleIndex] = channelData[sampleIndex];
4329
+
4330
+ // update loaded percent
4331
+ const samplesTotal = channelCount * channelLength;
4332
+ const samplesProcessed = channel * channelLength + sampleIndex;
4333
+ this.loadedPercent = samplesProcessed / samplesTotal;
4334
+ }
4335
+ }
4336
+
4337
+ // setup the sound to be played
4110
4338
  this.sampleRate = audioBuffer.sampleRate;
4339
+ this.sampleChannels = sampleChannels;
4340
+ this.loadedPercent = 1;
4111
4341
  if (this.onloadCallback)
4112
4342
  this.onloadCallback();
4113
4343
  }
4114
4344
  }
4115
4345
 
4116
- /** Play an mp3, ogg, or wav audio from a local file or url
4117
- * @param {string} filename - Location of sound file to play
4118
- * @param {number} [volume] - How much to scale volume by
4119
- * @param {boolean} [loop] - True if the music should loop
4120
- * @return {SoundWave} - The sound object for this file
4121
- * @memberof Audio */
4122
- function playAudioFile(filename, volume=1, loop=false)
4346
+ ///////////////////////////////////////////////////////////////////////////////
4347
+
4348
+ /**
4349
+ * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
4350
+ * Represents a single playing instance of a sound with pause/resume capabilities
4351
+ * @example
4352
+ * // Play a sound and get an instance for control
4353
+ * const jumpSound = new Sound([.5,.5,220]);
4354
+ * const instance = jumpSound.play();
4355
+ *
4356
+ * // Control the individual instance
4357
+ * instance.setVolume(.5);
4358
+ * instance.pause();
4359
+ * instance.unpause();
4360
+ * instance.stop();
4361
+ */
4362
+ class SoundInstance
4123
4363
  {
4124
- if (!soundEnable || headlessMode) return;
4364
+ /** Create a sound instance
4365
+ * @param {Sound} sound - The sound object
4366
+ * @param {number} [volume] - How much to scale volume by
4367
+ * @param {number} [rate] - The playback rate to use
4368
+ * @param {number} [pan] - How much to apply stereo panning
4369
+ * @param {boolean} [loop] - Should the sound loop?
4370
+ * @param {boolean} [paused] - Should the sound start paused? */
4371
+ constructor(sound, volume=1, rate=1, pan=0, loop=false, paused=false)
4372
+ {
4373
+ ASSERT(sound instanceof Sound, 'SoundInstance requires a valid Sound object');
4374
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
4375
+ ASSERT(rate >= 0, 'Sound rate must be positive or zero');
4376
+ ASSERT(isNumber(pan), 'Sound pan must be a number');
4377
+
4378
+ /** @property {Sound} - The sound object */
4379
+ this.sound = sound;
4380
+ /** @property {number} - How much to scale volume by */
4381
+ this.volume = volume;
4382
+ /** @property {number} - The playback rate to use */
4383
+ this.rate = rate;
4384
+ /** @property {number} - How much to apply stereo panning */
4385
+ this.pan = pan;
4386
+ /** @property {boolean} - Should the sound loop */
4387
+ this.loop = loop;
4388
+ /** @property {number} - Timestamp for audio context when paused */
4389
+ this.pausedTime = 0;
4390
+ /** @property {number} - Timestamp for audio context when started */
4391
+ this.startTime = undefined;
4392
+ /** @property {GainNode} - Gain node for the sound */
4393
+ this.gainNode = undefined;
4394
+ /** @property {AudioBufferSourceNode} - Source node of the audio */
4395
+ this.source = undefined;
4396
+ // setup end callback and start sound
4397
+ this.onendedCallback = (source)=>
4398
+ {
4399
+ if (source === this.source)
4400
+ this.source = undefined;
4401
+ };
4402
+ if (!paused)
4403
+ this.start();
4404
+ }
4125
4405
 
4126
- return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
4406
+ /** Start playing the sound instance from the offset time
4407
+ * @param {number} [offset] - Offset in seconds to start playback from
4408
+ */
4409
+ start(offset=0)
4410
+ {
4411
+ ASSERT(offset >= 0, 'Sound start offset must be positive or zero');
4412
+ if (this.isPlaying())
4413
+ this.stop();
4414
+ this.gainNode = audioContext.createGain();
4415
+ this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
4416
+ this.startTime = audioContext.currentTime - offset;
4417
+ this.pausedTime = undefined;
4418
+ }
4419
+
4420
+ /** Set the volume of this sound instance
4421
+ * @param {number} volume */
4422
+ setVolume(volume)
4423
+ {
4424
+ ASSERT(volume >= 0, 'Sound volume must be positive or zero');
4425
+ this.volume = volume;
4426
+ if (this.gainNode)
4427
+ this.gainNode.gain.value = volume;
4428
+ }
4429
+
4430
+ /** Stop this sound instance and reset position to the start */
4431
+ stop(fadeTime=0)
4432
+ {
4433
+ ASSERT(fadeTime >= 0, 'Sound fade time must be positive or zero');
4434
+ if (this.isPlaying())
4435
+ {
4436
+ if (fadeTime)
4437
+ {
4438
+ // ramp off gain
4439
+ const startFade = audioContext.currentTime;
4440
+ const endFade = startFade + fadeTime;
4441
+ this.gainNode.gain.linearRampToValueAtTime(1, startFade);
4442
+ this.gainNode.gain.linearRampToValueAtTime(0, endFade);
4443
+ this.source.stop(endFade);
4444
+ }
4445
+ else
4446
+ this.source.stop();
4447
+ }
4448
+ this.pausedTime = 0;
4449
+ this.source = undefined;
4450
+ this.startTime = undefined;
4451
+ }
4452
+
4453
+ /** Pause this sound instance */
4454
+ pause()
4455
+ {
4456
+ if (this.isPaused())
4457
+ return;
4458
+
4459
+ // save current time and stop sound
4460
+ this.pausedTime = this.getCurrentTime();
4461
+ this.source.stop();
4462
+ this.source = undefined;
4463
+ this.startTime = undefined;
4464
+ }
4465
+
4466
+ /** Unpauses this sound instance */
4467
+ resume()
4468
+ {
4469
+ if (!this.isPaused())
4470
+ return;
4471
+
4472
+ // restart sound from paused time
4473
+ this.start(this.pausedTime);
4474
+ }
4475
+
4476
+ /** Check if this instance is currently playing
4477
+ * @return {boolean} - True if playing
4478
+ */
4479
+ isPlaying() { return !!this.source; }
4480
+
4481
+ /** Check if this instance is paused and was not stopped
4482
+ * @return {boolean} - True if paused
4483
+ */
4484
+ isPaused() { return !this.isPlaying(); }
4485
+
4486
+ /** Get the current playback time in seconds
4487
+ * @return {number} - Current playback time
4488
+ */
4489
+ getCurrentTime()
4490
+ {
4491
+ const deltaTime = mod(audioContext.currentTime - this.startTime,
4492
+ this.getDuration());
4493
+ return this.isPlaying() ? deltaTime : this.pausedTime;
4494
+ }
4495
+
4496
+ /** Get the total duration of this sound
4497
+ * @return {number} - Total duration in seconds
4498
+ */
4499
+ getDuration() { return this.sound.getDuration() / this.rate; }
4500
+
4501
+ /** Get source of this sound instance
4502
+ * @return {AudioBufferSourceNode}
4503
+ */
4504
+ getSource() { return this.source; }
4127
4505
  }
4128
4506
 
4129
4507
  ///////////////////////////////////////////////////////////////////////////////
@@ -4177,9 +4555,11 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4177
4555
  * @param {boolean} [loop] - True if the sound should loop when it reaches the end
4178
4556
  * @param {number} [sampleRate=44100] - Sample rate for the sound
4179
4557
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
4558
+ * @param {number} [offset] - Offset in seconds to start playback from
4559
+ * @param {Function} [onended] - Callback for when the sound ends
4180
4560
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4181
4561
  * @memberof Audio */
4182
- function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR, gainNode)
4562
+ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
4183
4563
  {
4184
4564
  if (!soundEnable || headlessMode) return;
4185
4565
 
@@ -4204,16 +4584,20 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4204
4584
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
4205
4585
  source.connect(pannerNode).connect(gainNode);
4206
4586
 
4207
- // play the sound
4208
- if (audioContext.state != 'running')
4587
+ // callback when the sound ends
4588
+ if (onended)
4589
+ source.addEventListener('ended', ()=> onended(source));
4590
+
4591
+ if (!audioIsRunning())
4209
4592
  {
4210
- // fix stalled audio and play
4211
- audioContext.resume().then(()=>source.start());
4593
+ // fix stalled audio, this sound won't be able to play
4594
+ audioContext.resume();
4595
+ return;
4212
4596
  }
4213
- else
4214
- source.start();
4215
4597
 
4216
- // return sound
4598
+ // play and return sound
4599
+ const startOffset = offset * rate;
4600
+ source.start(0, startOffset);
4217
4601
  return source;
4218
4602
  }
4219
4603
 
@@ -4221,18 +4605,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4221
4605
  // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
4222
4606
 
4223
4607
  /** Generate and play a ZzFX sound
4224
- *
4608
+ *
4225
4609
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4226
4610
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
4227
4611
  * @return {AudioBufferSourceNode} - The audio node of the sound played
4228
4612
  * @memberof Audio */
4229
4613
  function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
4230
4614
 
4231
- /** Sample rate used for all ZzFX sounds
4232
- * @default 44100
4233
- * @memberof Audio */
4234
- const zzfxR = 44100;
4235
-
4236
4615
  /** Generate samples for a ZzFX sound
4237
4616
  * @param {number} [volume] - Volume scale (percent)
4238
4617
  * @param {number} [randomness] - How much to randomize frequency (percent Hz)
@@ -4256,11 +4635,10 @@ const zzfxR = 44100;
4256
4635
  * @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
4257
4636
  * @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
4258
4637
  * @return {Array} - Array of audio samples
4259
- * @memberof Audio
4260
- */
4638
+ * @memberof Audio */
4261
4639
  function zzfxG
4262
4640
  (
4263
- volume = 1,
4641
+ volume = 1,
4264
4642
  randomness = .05,
4265
4643
  frequency = 220,
4266
4644
  attack = 0,
@@ -4268,11 +4646,11 @@ function zzfxG
4268
4646
  release = .1,
4269
4647
  shape = 0,
4270
4648
  shapeCurve = 1,
4271
- slide = 0,
4272
- deltaSlide = 0,
4273
- pitchJump = 0,
4274
- pitchJumpTime = 0,
4275
- repeatTime = 0,
4649
+ slide = 0,
4650
+ deltaSlide = 0,
4651
+ pitchJump = 0,
4652
+ pitchJumpTime = 0,
4653
+ repeatTime = 0,
4276
4654
  noise = 0,
4277
4655
  modulation = 0,
4278
4656
  bitCrush = 0,
@@ -4284,19 +4662,19 @@ function zzfxG
4284
4662
  )
4285
4663
  {
4286
4664
  // init parameters
4287
- let sampleRate = zzfxR,
4288
- PI2 = PI*2,
4665
+ let sampleRate = audioDefaultSampleRate,
4666
+ PI2 = PI*2,
4289
4667
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
4290
- startFrequency = frequency *=
4668
+ startFrequency = frequency *=
4291
4669
  (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
4292
- modOffset = 0, // modulation offset
4670
+ modOffset = 0, // modulation offset
4293
4671
  repeat = 0, // repeat offset
4294
4672
  crush = 0, // bit crush offset
4295
4673
  jump = 1, // pitch jump timer
4296
4674
  length, // sample length
4297
4675
  b = [], // sample buffer
4298
4676
  t = 0, // sample time
4299
- i = 0, // sample index
4677
+ i = 0, // sample index
4300
4678
  s = 0, // sample value
4301
4679
  f, // wave frequency
4302
4680
 
@@ -4304,7 +4682,7 @@ function zzfxG
4304
4682
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
4305
4683
  cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
4306
4684
  a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
4307
- b0 = (1 + sign(filter) * cos) / 2 / a0,
4685
+ b0 = (1 + sign(filter) * cos) / 2 / a0,
4308
4686
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
4309
4687
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
4310
4688
 
@@ -4322,7 +4700,7 @@ function zzfxG
4322
4700
  repeatTime = repeatTime * sampleRate | 0;
4323
4701
 
4324
4702
  // generate waveform
4325
- for(length = attack + decay + sustain + release + delay | 0;
4703
+ for (length = attack + decay + sustain + release + delay | 0;
4326
4704
  i < length; b[i++] = s * volume) // sample
4327
4705
  {
4328
4706
  if (!(++crush%(bitCrush*100|0))) // bit crush
@@ -4350,7 +4728,7 @@ function zzfxG
4350
4728
  0); // post release
4351
4729
 
4352
4730
  s = delay ? s/2 + (delay > i ? 0 : // delay
4353
- (i<length-delay? 1 : (length-i)/delay) * // release delay
4731
+ (i<length-delay? 1 : (length-i)/delay) * // release delay
4354
4732
  b[i-delay|0]/2/volume) : s; // sample delay
4355
4733
 
4356
4734
  if (filter) // apply filter
@@ -4362,14 +4740,14 @@ function zzfxG
4362
4740
  t += f + f*noise*Math.sin(i**5); // noise
4363
4741
 
4364
4742
  if (jump && ++jump > pitchJumpTime) // pitch jump
4365
- {
4743
+ {
4366
4744
  frequency += pitchJump; // apply pitch jump
4367
4745
  startFrequency += pitchJump; // also apply to start
4368
4746
  jump = 0; // stop pitch jump time
4369
- }
4747
+ }
4370
4748
 
4371
4749
  if (repeatTime && !(++repeat % repeatTime)) // repeat
4372
- {
4750
+ {
4373
4751
  frequency = startFrequency; // reset frequency
4374
4752
  slide = startSlide; // reset slide
4375
4753
  jump ||= 1; // reset pitch jump time
@@ -4378,7 +4756,7 @@ function zzfxG
4378
4756
 
4379
4757
  return b; // return sample buffer
4380
4758
  }
4381
- /**
4759
+ /**
4382
4760
  * LittleJS Tile Layer System
4383
4761
  * - Caches arrays of tiles to off screen canvas for fast rendering
4384
4762
  * - Unlimited numbers of layers, allocates canvases as needed
@@ -4392,9 +4770,9 @@ function zzfxG
4392
4770
  // Tile Layer System
4393
4771
 
4394
4772
  /** Keep track of all tile layers with collision
4395
- * @type {Array<TileCollisionLayer>}
4773
+ * @type {Array<TileCollisionLayer>}
4396
4774
  * @memberof TileCollision */
4397
- let tileCollisionLayers = [];
4775
+ const tileCollisionLayers = [];
4398
4776
 
4399
4777
  /** Get tile collision data for a given cell in the grid
4400
4778
  * @param {Vector2} pos
@@ -4448,7 +4826,7 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
4448
4826
  }
4449
4827
 
4450
4828
  ///////////////////////////////////////////////////////////////////////////////
4451
- /**
4829
+ /**
4452
4830
  * Load tile layers from exported data
4453
4831
  * @param {Object} tileMapData - Level data from exported data
4454
4832
  * @param {TileInfo} [tileInfo] - Default tile info (used for size and texture)
@@ -4481,13 +4859,13 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
4481
4859
  {
4482
4860
  const dataLayer = tileMapData.layers[layerIndex];
4483
4861
  ASSERT(dataLayer.data && dataLayer.data.length);
4484
- ASSERT(levelSize.area() == dataLayer.data.length);
4862
+ ASSERT(levelSize.area() === dataLayer.data.length);
4485
4863
 
4486
4864
  const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
4487
4865
  const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
4488
4866
  tileLayers[layerIndex] = tileLayer;
4489
4867
 
4490
- for (let x=levelSize.x; x--;)
4868
+ for (let x=levelSize.x; x--;)
4491
4869
  for (let y=levelSize.y; y--;)
4492
4870
  {
4493
4871
  const pos = vec2(x, levelSize.y-1-y);
@@ -4546,7 +4924,7 @@ class TileLayerData
4546
4924
  /**
4547
4925
  * Canvas Layer - cached off screen rendering system
4548
4926
  * - Contains an offscreen canvas that can be rendered to
4549
- * - Webgl rendering is optional, call useWebGL to enable
4927
+ * - WebGL rendering is optional, call useWebGL to enable
4550
4928
  * @extends EngineObject
4551
4929
  * @example
4552
4930
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
@@ -4568,18 +4946,18 @@ class CanvasLayer extends EngineObject
4568
4946
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
4569
4947
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
4570
4948
  this.context = headlessMode ? undefined : this.canvas.getContext('2d');
4571
- /** @property {WebGLTexture} - Texture if using webgl for this layer, call useWebGL to enable */
4949
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
4572
4950
  this.glTexture = undefined;
4573
4951
  this.gravityScale = 0; // disable gravity by default for canvas layers
4574
4952
  }
4575
-
4953
+
4576
4954
  /** Destroy this canvas layer */
4577
4955
  destroy()
4578
4956
  {
4579
4957
  if (this.destroyed)
4580
4958
  return;
4581
4959
 
4582
- // free up the webgl texture
4960
+ // free up the WebGL texture
4583
4961
  if (this.glTexture)
4584
4962
  glDeleteTexture(this.glTexture);
4585
4963
  super.destroy();
@@ -4604,12 +4982,12 @@ class CanvasLayer extends EngineObject
4604
4982
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
4605
4983
  {
4606
4984
  // draw the canvas layer as a single tile that uses the whole texture
4607
- const useWebgl = glEnable && this.glTexture != undefined;
4985
+ const useWebGL = glEnable && this.glTexture !== undefined;
4608
4986
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
4609
- drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebgl, screenSpace, context);
4987
+ drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
4610
4988
  }
4611
4989
 
4612
- /** Draw onto the layer canvas in world space (bypass webgl)
4990
+ /** Draw onto the layer canvas in world space (bypass WebGL)
4613
4991
  * @param {Vector2} pos
4614
4992
  * @param {Vector2} size
4615
4993
  * @param {number} angle
@@ -4643,8 +5021,8 @@ class CanvasLayer extends EngineObject
4643
5021
  if (textureInfo)
4644
5022
  {
4645
5023
  context.globalAlpha = color.a; // only alpha is supported
4646
- context.drawImage(textureInfo.image,
4647
- tileInfo.pos.x, tileInfo.pos.y,
5024
+ context.drawImage(textureInfo.image,
5025
+ tileInfo.pos.x, tileInfo.pos.y,
4648
5026
  tileInfo.size.x, tileInfo.size.y, -.5, -.5, 1, 1);
4649
5027
  context.globalAlpha = 1;
4650
5028
  }
@@ -4662,11 +5040,11 @@ class CanvasLayer extends EngineObject
4662
5040
  * @param {Vector2} [size=(1,1)]
4663
5041
  * @param {Color} [color=(1,1,1,1)]
4664
5042
  * @param {number} [angle=0] */
4665
- drawRect(pos, size, color, angle)
5043
+ drawRect(pos, size, color, angle)
4666
5044
  { this.drawTile(pos, size, undefined, color, angle); }
4667
5045
 
4668
- /** Create or update the webgl texture for this layer
4669
- * @param {boolean} [enable] - enable webgl rendering and update the texture */
5046
+ /** Create or update the WebGL texture for this layer
5047
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture */
4670
5048
  useWebGL(enable=true)
4671
5049
  {
4672
5050
  if (glEnable && enable)
@@ -4712,7 +5090,7 @@ class TileLayer extends CanvasLayer
4712
5090
  this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
4713
5091
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
4714
5092
  this.context = this.canvas.getContext('2d');
4715
- /** @property {WebGLTexture} - Texture if using webgl for this layer */
5093
+ /** @property {WebGLTexture} - Texture if using WebGL for this layer */
4716
5094
  this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
4717
5095
  // set no friction by default, applied friction is max of both objects
4718
5096
  this.friction = 0;
@@ -4737,7 +5115,7 @@ class TileLayer extends CanvasLayer
4737
5115
  }
4738
5116
  }
4739
5117
 
4740
- /** Set data at a given position in the array
5118
+ /** Set data at a given position in the array
4741
5119
  * @param {Vector2} layerPos - Local position in array
4742
5120
  * @param {TileLayerData} data - Data to set
4743
5121
  * @param {boolean} [redraw] - Force the tile to redraw if true */
@@ -4749,26 +5127,26 @@ class TileLayer extends CanvasLayer
4749
5127
  redraw && this.drawTileData(layerPos);
4750
5128
  }
4751
5129
  }
4752
-
4753
- /** Get data at a given position in the array
5130
+
5131
+ /** Get data at a given position in the array
4754
5132
  * @param {Vector2} layerPos - Local position in array
4755
5133
  * @return {TileLayerData} */
4756
5134
  getData(layerPos)
4757
5135
  { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
4758
-
5136
+
4759
5137
  // Render the tile layer, called automatically by the engine
4760
5138
  render()
4761
5139
  {
4762
- ASSERT(drawContext != this.context, 'must call redrawEnd() after drawing tiles!');
4763
-
5140
+ ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5141
+
4764
5142
  // draw the tile layer as a single tile
4765
5143
  const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
4766
5144
  const pos = this.pos.add(this.size.scale(.5));
4767
- const useWebgl = glEnable && this.glTexture != undefined;
4768
- drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebgl);
5145
+ const useWebGL = glEnable && this.glTexture !== undefined;
5146
+ drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
4769
5147
  }
4770
5148
 
4771
- /** Draw all the tile data to an offscreen canvas
5149
+ /** Draw all the tile data to an offscreen canvas
4772
5150
  * - This may be slow in some browsers but only needs to be done once */
4773
5151
  redraw()
4774
5152
  {
@@ -4778,7 +5156,7 @@ class TileLayer extends CanvasLayer
4778
5156
  this.drawTileData(vec2(x,y), false);
4779
5157
  this.redrawEnd();
4780
5158
  if (this.glTexture)
4781
- this.useWebGL(); // update webgl texture
5159
+ this.useWebGL(); // update WebGL texture
4782
5160
  }
4783
5161
 
4784
5162
  /** Call to start the redraw process
@@ -4814,7 +5192,7 @@ class TileLayer extends CanvasLayer
4814
5192
  /** Call to end the redraw process */
4815
5193
  redrawEnd()
4816
5194
  {
4817
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5195
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
4818
5196
  glCopyToContext(drawContext);
4819
5197
  //debugSaveCanvas(this.canvas);
4820
5198
 
@@ -4825,7 +5203,7 @@ class TileLayer extends CanvasLayer
4825
5203
  /** Draw the tile at a given position in the tile grid
4826
5204
  * This can be used to clear out tiles when they are destroyed
4827
5205
  * Tiles can also be redrawn if inside a redrawStart/End block
4828
- * @param {Vector2} layerPos
5206
+ * @param {Vector2} layerPos
4829
5207
  * @param {boolean} [clear] - should the old tile be cleared out
4830
5208
  */
4831
5209
  drawTileData(layerPos, clear=true)
@@ -4840,9 +5218,9 @@ class TileLayer extends CanvasLayer
4840
5218
 
4841
5219
  // draw the tile if it has layer data
4842
5220
  const d = this.getData(layerPos);
4843
- if (d.tile != undefined)
5221
+ if (d.tile !== undefined)
4844
5222
  {
4845
- ASSERT(drawContext == this.context, 'must call redrawStart() before drawing tiles');
5223
+ ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
4846
5224
  const pos = layerPos.add(vec2(.5));
4847
5225
  const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex, this.tileInfo.padding);
4848
5226
  drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
@@ -4855,7 +5233,7 @@ class TileLayer extends CanvasLayer
4855
5233
  * Tile Collision Layer - a tile layer with collision
4856
5234
  * - adds collision data and functions to TileLayer
4857
5235
  * - there can be multiple tile collision layers
4858
- * - tile collison layers should not overlap each other
5236
+ * - tile collision layers should not overlap each other
4859
5237
  * @extends TileLayer
4860
5238
  */
4861
5239
  class TileCollisionLayer extends TileLayer
@@ -5006,7 +5384,7 @@ class TileCollisionLayer extends TileLayer
5006
5384
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
5007
5385
  }
5008
5386
  }
5009
- /**
5387
+ /**
5010
5388
  * LittleJS Particle System
5011
5389
  */
5012
5390
 
@@ -5023,7 +5401,7 @@ class TileCollisionLayer extends TileLayer
5023
5401
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
5024
5402
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
5025
5403
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5026
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5404
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5027
5405
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
5028
5406
  * );
5029
5407
  */
@@ -5031,35 +5409,35 @@ class ParticleEmitter extends EngineObject
5031
5409
  {
5032
5410
  /** Create a particle system with the given settings
5033
5411
  * @param {Vector2} position - World space position of the emitter
5034
- * @param {Number} [angle] - Angle to emit the particles
5035
- * @param {Number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
5036
- * @param {Number} [emitTime] - How long to stay alive (0 is forever)
5037
- * @param {Number} [emitRate] - How many particles per second to spawn, does not emit if 0
5038
- * @param {Number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5412
+ * @param {number} [angle] - Angle to emit the particles
5413
+ * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
5414
+ * @param {number} [emitTime] - How long to stay alive (0 is forever)
5415
+ * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
5416
+ * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5039
5417
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
5040
5418
  * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
5041
5419
  * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
5042
5420
  * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
5043
5421
  * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
5044
- * @param {Number} [particleTime] - How long particles live
5045
- * @param {Number} [sizeStart] - How big are particles at start
5046
- * @param {Number} [sizeEnd] - How big are particles at end
5047
- * @param {Number} [speed] - How fast are particles when spawned
5048
- * @param {Number} [angleSpeed] - How fast are particles rotating
5049
- * @param {Number} [damping] - How much to dampen particle speed
5050
- * @param {Number} [angleDamping] - How much to dampen particle angular speed
5051
- * @param {Number} [gravityScale] - How much gravity effect particles
5052
- * @param {Number} [particleConeAngle] - Cone for start particle angle
5053
- * @param {Number} [fadeRate] - How quick to fade particles at start/end in percent of life
5054
- * @param {Number} [randomness] - Apply extra randomness percent
5422
+ * @param {number} [particleTime] - How long particles live
5423
+ * @param {number} [sizeStart] - How big are particles at start
5424
+ * @param {number} [sizeEnd] - How big are particles at end
5425
+ * @param {number} [speed] - How fast are particles when spawned
5426
+ * @param {number} [angleSpeed] - How fast are particles rotating
5427
+ * @param {number} [damping] - How much to dampen particle speed
5428
+ * @param {number} [angleDamping] - How much to dampen particle angular speed
5429
+ * @param {number} [gravityScale] - How much gravity effect particles
5430
+ * @param {number} [particleConeAngle] - Cone for start particle angle
5431
+ * @param {number} [fadeRate] - How quick to fade particles at start/end in percent of life
5432
+ * @param {number} [randomness] - Apply extra randomness percent
5055
5433
  * @param {boolean} [collideTiles] - Do particles collide against tiles
5056
5434
  * @param {boolean} [additive] - Should particles use additive blend
5057
5435
  * @param {boolean} [randomColorLinear] - Should color be randomized linearly or across each component
5058
- * @param {Number} [renderOrder] - Render order for particles (additive is above other stuff by default)
5436
+ * @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
5059
5437
  * @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
5060
5438
  */
5061
5439
  constructor
5062
- (
5440
+ (
5063
5441
  position,
5064
5442
  angle,
5065
5443
  emitSize = 0,
@@ -5081,7 +5459,7 @@ class ParticleEmitter extends EngineObject
5081
5459
  gravityScale = 0,
5082
5460
  particleConeAngle = PI,
5083
5461
  fadeRate = .1,
5084
- randomness = .2,
5462
+ randomness = .2,
5085
5463
  collideTiles = false,
5086
5464
  additive = false,
5087
5465
  randomColorLinear = true,
@@ -5092,13 +5470,13 @@ class ParticleEmitter extends EngineObject
5092
5470
  super(position, vec2(), tileInfo, angle, undefined, renderOrder);
5093
5471
 
5094
5472
  // emitter settings
5095
- /** @property {Number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5473
+ /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5096
5474
  this.emitSize = emitSize
5097
- /** @property {Number} - How long to stay alive (0 is forever) */
5475
+ /** @property {number} - How long to stay alive (0 is forever) */
5098
5476
  this.emitTime = emitTime;
5099
- /** @property {Number} - How many particles per second to spawn, does not emit if 0 */
5477
+ /** @property {number} - How many particles per second to spawn, does not emit if 0 */
5100
5478
  this.emitRate = emitRate;
5101
- /** @property {Number} - Local angle to apply velocity to particles from emitter */
5479
+ /** @property {number} - Local angle to apply velocity to particles from emitter */
5102
5480
  this.emitConeAngle = emitConeAngle;
5103
5481
 
5104
5482
  // color settings
@@ -5114,27 +5492,27 @@ class ParticleEmitter extends EngineObject
5114
5492
  this.randomColorLinear = randomColorLinear;
5115
5493
 
5116
5494
  // particle settings
5117
- /** @property {Number} - How long particles live */
5495
+ /** @property {number} - How long particles live */
5118
5496
  this.particleTime = particleTime;
5119
- /** @property {Number} - How big are particles at start */
5497
+ /** @property {number} - How big are particles at start */
5120
5498
  this.sizeStart = sizeStart;
5121
- /** @property {Number} - How big are particles at end */
5499
+ /** @property {number} - How big are particles at end */
5122
5500
  this.sizeEnd = sizeEnd;
5123
- /** @property {Number} - How fast are particles when spawned */
5501
+ /** @property {number} - How fast are particles when spawned */
5124
5502
  this.speed = speed;
5125
- /** @property {Number} - How fast are particles rotating */
5503
+ /** @property {number} - How fast are particles rotating */
5126
5504
  this.angleSpeed = angleSpeed;
5127
- /** @property {Number} - How much to dampen particle speed */
5505
+ /** @property {number} - How much to dampen particle speed */
5128
5506
  this.damping = damping;
5129
- /** @property {Number} - How much to dampen particle angular speed */
5507
+ /** @property {number} - How much to dampen particle angular speed */
5130
5508
  this.angleDamping = angleDamping;
5131
- /** @property {Number} - How much does gravity effect particles */
5509
+ /** @property {number} - How much gravity affects particles */
5132
5510
  this.gravityScale = gravityScale;
5133
- /** @property {Number} - Cone for start particle angle */
5511
+ /** @property {number} - Cone for start particle angle */
5134
5512
  this.particleConeAngle = particleConeAngle;
5135
- /** @property {Number} - How quick to fade in particles at start/end in percent of life */
5513
+ /** @property {number} - How quick to fade in particles at start/end in percent of life */
5136
5514
  this.fadeRate = fadeRate;
5137
- /** @property {Number} - Apply extra randomness percent */
5515
+ /** @property {number} - Apply extra randomness percent */
5138
5516
  this.randomness = randomness;
5139
5517
  /** @property {boolean} - Do particles collide against tiles */
5140
5518
  this.collideTiles = collideTiles;
@@ -5142,16 +5520,16 @@ class ParticleEmitter extends EngineObject
5142
5520
  this.additive = additive;
5143
5521
  /** @property {boolean} - Should it be in local space of emitter */
5144
5522
  this.localSpace = localSpace;
5145
- /** @property {Number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5523
+ /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5146
5524
  this.trailScale = 0;
5147
5525
  /** @property {Function} - Callback when particle is destroyed */
5148
5526
  this.particleDestroyCallback = undefined;
5149
5527
  /** @property {Function} - Callback when particle is created */
5150
5528
  this.particleCreateCallback = undefined;
5151
- /** @property {Number} - Track particle emit time */
5529
+ /** @property {number} - Track particle emit time */
5152
5530
  this.emitTimeBuffer = 0;
5153
5531
  }
5154
-
5532
+
5155
5533
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
5156
5534
  update()
5157
5535
  {
@@ -5175,7 +5553,7 @@ class ParticleEmitter extends EngineObject
5175
5553
  if (debugParticles)
5176
5554
  {
5177
5555
  // show emitter bounds
5178
- const emitSize = typeof this.emitSize == 'number' ? vec2(this.emitSize) : this.emitSize;
5556
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
5179
5557
  debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
5180
5558
  }
5181
5559
  }
@@ -5185,7 +5563,7 @@ class ParticleEmitter extends EngineObject
5185
5563
  emitParticle()
5186
5564
  {
5187
5565
  // spawn a particle
5188
- let pos = typeof this.emitSize == 'number' ? // check if number was used
5566
+ let pos = typeof this.emitSize === 'number' ? // check if number was used
5189
5567
  randInCircle(this.emitSize/2) // circle emitter
5190
5568
  : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
5191
5569
  .multiply(this.emitSize).rotate(this.angle)
@@ -5210,15 +5588,22 @@ class ParticleEmitter extends EngineObject
5210
5588
  const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
5211
5589
  const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
5212
5590
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
5213
-
5591
+
5214
5592
  // build particle
5215
5593
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
5216
5594
  particle.velocity = vec2().setAngle(velocityAngle, speed);
5217
5595
  particle.angleVelocity = angleSpeed;
5596
+ if (!this.localSpace)
5597
+ {
5598
+ // apply emitter velocity to particle
5599
+ particle.velocity.x += this.velocity.x;
5600
+ particle.velocity.y += this.velocity.y;
5601
+ particle.angleVelocity += this.angleVelocity;
5602
+ }
5218
5603
  particle.fadeRate = this.fadeRate;
5219
5604
  particle.damping = this.damping;
5220
5605
  particle.angleDamping = this.angleDamping;
5221
- particle.restitution = this.restitution;
5606
+ particle.restitution = this.restitution;
5222
5607
  particle.friction = this.friction;
5223
5608
  particle.gravityScale = this.gravityScale;
5224
5609
  particle.collideTiles = this.collideTiles;
@@ -5248,38 +5633,38 @@ class Particle extends EngineObject
5248
5633
  * Typically this is created automatically by a ParticleEmitter
5249
5634
  * @param {Vector2} position - World space position of the particle
5250
5635
  * @param {TileInfo} tileInfo - Tile info to render particles
5251
- * @param {Number} angle - Angle to rotate the particle
5636
+ * @param {number} angle - Angle to rotate the particle
5252
5637
  * @param {Color} colorStart - Color at start of life
5253
5638
  * @param {Color} colorEnd - Color at end of life
5254
- * @param {Number} lifeTime - How long to live for
5255
- * @param {Number} sizeStart - Size at start of life
5256
- * @param {Number} sizeEnd - Size at end of life
5257
- * @param {Number} fadeRate - How quick to fade in/out
5639
+ * @param {number} lifeTime - How long to live for
5640
+ * @param {number} sizeStart - Size at start of life
5641
+ * @param {number} sizeEnd - Size at end of life
5642
+ * @param {number} fadeRate - How quick to fade in/out
5258
5643
  * @param {boolean} additive - Does it use additive blend mode
5259
- * @param {Number} trailScale - If a trail, how long to make it
5644
+ * @param {number} trailScale - If a trail, how long to make it
5260
5645
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
5261
5646
  * @param {Function} [destroyCallback] - Callback when particle dies
5262
5647
  */
5263
5648
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
5264
5649
  )
5265
- {
5266
- super(position, vec2(), tileInfo, angle);
5267
-
5650
+ {
5651
+ super(position, vec2(), tileInfo, angle);
5652
+
5268
5653
  /** @property {Color} - Color at start of life */
5269
5654
  this.colorStart = colorStart;
5270
5655
  /** @property {Color} - Calculated change in color */
5271
5656
  this.colorEndDelta = colorEnd.subtract(colorStart);
5272
- /** @property {Number} - How long to live for */
5657
+ /** @property {number} - How long to live for */
5273
5658
  this.lifeTime = lifeTime;
5274
- /** @property {Number} - Size at start of life */
5659
+ /** @property {number} - Size at start of life */
5275
5660
  this.sizeStart = sizeStart;
5276
- /** @property {Number} - Calculated change in size */
5661
+ /** @property {number} - Calculated change in size */
5277
5662
  this.sizeEndDelta = sizeEnd - sizeStart;
5278
- /** @property {Number} - How quick to fade in/out */
5663
+ /** @property {number} - How quick to fade in/out */
5279
5664
  this.fadeRate = fadeRate;
5280
5665
  /** @property {boolean} - Is it additive */
5281
5666
  this.additive = additive;
5282
- /** @property {Number} - If a trail, how long to make it */
5667
+ /** @property {number} - If a trail, how long to make it */
5283
5668
  this.trailScale = trailScale;
5284
5669
  /** @property {ParticleEmitter} - Parent emitter if local space */
5285
5670
  this.localSpaceEmitter = localSpaceEmitter;
@@ -5319,7 +5704,7 @@ class Particle extends EngineObject
5319
5704
  this.colorStart.r + p * this.colorEndDelta.r,
5320
5705
  this.colorStart.g + p * this.colorEndDelta.g,
5321
5706
  this.colorStart.b + p * this.colorEndDelta.b,
5322
- (this.colorStart.a + p * this.colorEndDelta.a) *
5707
+ (this.colorStart.a + p * this.colorEndDelta.a) *
5323
5708
  (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
5324
5709
 
5325
5710
  // draw the particle
@@ -5329,7 +5714,7 @@ class Particle extends EngineObject
5329
5714
  if (this.localSpaceEmitter)
5330
5715
  {
5331
5716
  // in local space of emitter
5332
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
5717
+ pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
5333
5718
  angle += this.localSpaceEmitter.angle;
5334
5719
  }
5335
5720
  if (this.trailScale)
@@ -5353,7 +5738,7 @@ class Particle extends EngineObject
5353
5738
  this.additive && setBlendMode();
5354
5739
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
5355
5740
 
5356
- if (p == 1)
5741
+ if (p === 1)
5357
5742
  {
5358
5743
  // destroy particle when it's time runs out
5359
5744
  this.color = color;
@@ -5363,7 +5748,7 @@ class Particle extends EngineObject
5363
5748
  }
5364
5749
  }
5365
5750
  }
5366
- /**
5751
+ /**
5367
5752
  * LittleJS Medal System
5368
5753
  * - Tracks and displays medals
5369
5754
  * - Saves medals to local storage
@@ -5384,7 +5769,7 @@ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
5384
5769
  /** Initialize medals with a save name used for storage
5385
5770
  * - Call this after creating all medals
5386
5771
  * - Checks if medals are unlocked
5387
- * @param {String} saveName
5772
+ * @param {string} saveName
5388
5773
  * @memberof Medals */
5389
5774
  function medalsInit(saveName)
5390
5775
  {
@@ -5399,7 +5784,7 @@ function medalsInit(saveName)
5399
5784
  {
5400
5785
  if (!medalsDisplayQueue.length)
5401
5786
  return;
5402
-
5787
+
5403
5788
  // update first medal in queue
5404
5789
  const medal = medalsDisplayQueue[0];
5405
5790
  const time = timeReal - medalsDisplayTimeLast;
@@ -5414,7 +5799,7 @@ function medalsInit(saveName)
5414
5799
  {
5415
5800
  // slide on/off medals
5416
5801
  const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
5417
- const hidePercent =
5802
+ const hidePercent =
5418
5803
  time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
5419
5804
  time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
5420
5805
  medal.render(hidePercent);
@@ -5430,43 +5815,43 @@ function medalsForEach(callback)
5430
5815
 
5431
5816
  ///////////////////////////////////////////////////////////////////////////////
5432
5817
 
5433
- /**
5434
- * Medal - Tracks an unlockable medal
5818
+ /**
5819
+ * Medal - Tracks an unlockable medal
5435
5820
  * @example
5436
5821
  * // create a medal
5437
5822
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
5438
- *
5823
+ *
5439
5824
  * // initialize medals
5440
5825
  * medalsInit('Example Game');
5441
- *
5826
+ *
5442
5827
  * // unlock the medal
5443
5828
  * medal_example.unlock();
5444
5829
  */
5445
5830
  class Medal
5446
5831
  {
5447
5832
  /** Create a medal object and adds it to the list of medals
5448
- * @param {Number} id - The unique identifier of the medal
5449
- * @param {String} name - Name of the medal
5450
- * @param {String} [description] - Description of the medal
5451
- * @param {String} [icon] - Icon for the medal
5452
- * @param {String} [src] - Image location for the medal
5833
+ * @param {number} id - The unique identifier of the medal
5834
+ * @param {string} name - Name of the medal
5835
+ * @param {string} [description] - Description of the medal
5836
+ * @param {string} [icon] - Icon for the medal
5837
+ * @param {string} [src] - Image location for the medal
5453
5838
  */
5454
5839
  constructor(id, name, description='', icon='🏆', src)
5455
5840
  {
5456
5841
  ASSERT(id >= 0 && !medals[id]);
5457
-
5458
- /** @property {Number} - The unique identifier of the medal */
5842
+
5843
+ /** @property {number} - The unique identifier of the medal */
5459
5844
  this.id = id;
5460
-
5461
- /** @property {String} - Name of the medal */
5845
+
5846
+ /** @property {string} - Name of the medal */
5462
5847
  this.name = name;
5463
-
5464
- /** @property {String} - Description of the medal */
5848
+
5849
+ /** @property {string} - Description of the medal */
5465
5850
  this.description = description;
5466
-
5467
- /** @property {String} - Icon for the medal */
5851
+
5852
+ /** @property {string} - Icon for the medal */
5468
5853
  this.icon = icon;
5469
-
5854
+
5470
5855
  /** @property {boolean} - Is the medal unlocked? */
5471
5856
  this.unlocked = false;
5472
5857
 
@@ -5491,7 +5876,7 @@ class Medal
5491
5876
  }
5492
5877
 
5493
5878
  /** Render a medal
5494
- * @param {Number} [hidePercent] - How much to slide the medal off screen
5879
+ * @param {number} [hidePercent] - How much to slide the medal off screen
5495
5880
  */
5496
5881
  render(hidePercent=0)
5497
5882
  {
@@ -5505,7 +5890,7 @@ class Medal
5505
5890
  context.save();
5506
5891
  context.beginPath();
5507
5892
  context.fillStyle = new Color(.9,.9,.9).toString();
5508
- context.strokeStyle = new Color(0,0,0).toString();
5893
+ context.strokeStyle = BLACK.toString();
5509
5894
  context.lineWidth = 3;
5510
5895
  context.rect(x, y, width, height);
5511
5896
  context.fill();
@@ -5522,17 +5907,17 @@ class Medal
5522
5907
  const descriptionSize = height*.3;
5523
5908
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
5524
5909
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
5525
- drawTextScreen(this.name, pos, nameSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
5910
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
5526
5911
 
5527
5912
  // draw the description
5528
5913
  pos.y = y + height - gap.y*2 - descriptionSize/2;
5529
- drawTextScreen(this.description, pos, descriptionSize, new Color(0,0,0), 0, undefined, 'left', undefined, textWidth);
5914
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
5530
5915
  context.restore();
5531
5916
  }
5532
5917
 
5533
5918
  /** Render the icon for a medal
5534
5919
  * @param {Vector2} pos - Screen space position
5535
- * @param {Number} size - Screen space size
5920
+ * @param {number} size - Screen space size
5536
5921
  */
5537
5922
  renderIcon(pos, size)
5538
5923
  {
@@ -5540,21 +5925,22 @@ class Medal
5540
5925
  if (this.image)
5541
5926
  overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
5542
5927
  else
5543
- drawTextScreen(this.icon, pos, size*.7, new Color(0,0,0));
5928
+ drawTextScreen(this.icon, pos, size*.7, BLACK);
5544
5929
  }
5545
-
5930
+
5546
5931
  // Get local storage key used by the medal
5547
5932
  storageKey() { return medalsSaveName + '_' + this.id; }
5548
5933
  }
5549
5934
  /**
5550
5935
  * LittleJS WebGL Interface
5551
- * - All webgl used by the engine is wrapped up here
5936
+ * - All WebGL used by the engine is wrapped up here
5937
+ * - Will fall back to 2D canvas rendering if WebGL is not supported
5552
5938
  * - For normal stuff you won't need to see or call anything in this file
5553
5939
  * - For advanced stuff there are helper functions to create shaders, textures, etc
5554
5940
  * - Can be disabled with glEnable to revert to 2D canvas rendering
5555
5941
  * - Batches sprite rendering on GPU for incredibly fast performance
5556
5942
  * - Sprite transform math is done in the shader where possible
5557
- * - Supports shadertoy style post processing shaders
5943
+ * - Supports shadertoy style post processing shaders via plugin
5558
5944
  * @namespace WebGL
5559
5945
  */
5560
5946
 
@@ -5563,24 +5949,27 @@ class Medal
5563
5949
  * @memberof WebGL */
5564
5950
  let glCanvas;
5565
5951
 
5566
- /** 2d context for glCanvas
5952
+ /** WebGL2 context for `glCanvas`
5567
5953
  * @type {WebGL2RenderingContext}
5568
5954
  * @memberof WebGL */
5569
5955
  let glContext;
5570
5956
 
5571
- /** Should webgl be setup with anti-aliasing? must be set before calling engineInit
5957
+ /** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
5572
5958
  * @type {boolean}
5573
5959
  * @memberof WebGL */
5574
5960
  let glAntialias = true;
5575
5961
 
5576
5962
  // WebGL internal variables not exposed to documentation
5577
- let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive;
5963
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
5578
5964
 
5579
- // WebGL internal constants
5580
- const gl_MAX_INSTANCES = 1e4;
5965
+ // WebGL internal constants
5966
+ const gl_ARRAY_BUFFER_SIZE = 4e5;
5581
5967
  const gl_INDICES_PER_INSTANCE = 11;
5582
5968
  const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
5583
- const gl_INSTANCE_BUFFER_SIZE = gl_MAX_INSTANCES * gl_INSTANCE_BYTE_STRIDE;
5969
+ const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
5970
+ const gl_INDICES_PER_POLY_VERTEX = 3;
5971
+ const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
5972
+ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
5584
5973
 
5585
5974
  ///////////////////////////////////////////////////////////////////////////////
5586
5975
 
@@ -5593,11 +5982,19 @@ function glInit()
5593
5982
  glCanvas = document.createElement('canvas');
5594
5983
  glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
5595
5984
 
5596
- // create the webgl canvas
5985
+ if (!glContext)
5986
+ {
5987
+ console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
5988
+ glCanvas = glContext = undefined;
5989
+ glEnable = false;
5990
+ return;
5991
+ }
5992
+
5993
+ // create the WebGL canvas
5597
5994
  const rootElement = mainCanvas.parentElement;
5598
5995
  rootElement.appendChild(glCanvas);
5599
5996
 
5600
- // setup vertex and fragment shaders
5997
+ // setup instanced rendering shader program
5601
5998
  glShader = glCreateProgram(
5602
5999
  '#version 300 es\n' + // specify GLSL ES version
5603
6000
  'precision highp float;'+ // use highp for better accuracy
@@ -5625,40 +6022,59 @@ function glInit()
5625
6022
  '}' // end of shader
5626
6023
  );
5627
6024
 
6025
+ // setup poly rendering shaders
6026
+ glPolyShader = glCreateProgram(
6027
+ '#version 300 es\n' + // specify GLSL ES version
6028
+ 'precision highp float;'+ // use highp for better accuracy
6029
+ 'uniform mat4 m;'+ // transform matrix
6030
+ 'in vec2 p;'+ // in: position
6031
+ 'in vec4 c;'+ // in: color
6032
+ 'out vec4 d;'+ // out: color
6033
+ 'void main(){'+ // shader entry point
6034
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
6035
+ 'd=c;'+ // pass color to fragment shader
6036
+ '}' // end of shader
6037
+ ,
6038
+ '#version 300 es\n' + // specify GLSL ES version
6039
+ 'precision highp float;'+ // use highp for better accuracy
6040
+ 'in vec4 d;'+ // in: color
6041
+ 'out vec4 c;'+ // out: color
6042
+ 'void main(){'+ // shader entry point
6043
+ 'c=d;'+ // set color
6044
+ '}' // end of shader
6045
+ );
6046
+
5628
6047
  // init buffers
5629
- const glInstanceData = new ArrayBuffer(gl_INSTANCE_BUFFER_SIZE);
6048
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
5630
6049
  glPositionData = new Float32Array(glInstanceData);
5631
6050
  glColorData = new Uint32Array(glInstanceData);
5632
6051
  glArrayBuffer = glContext.createBuffer();
5633
6052
  glGeometryBuffer = glContext.createBuffer();
5634
6053
 
5635
6054
  // create the geometry buffer, triangle strip square
5636
- const geometry = new Float32Array([glInstanceCount=0,0,1,0,0,1,1,1]);
6055
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
5637
6056
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
5638
6057
  glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
5639
6058
  }
5640
6059
 
5641
- // Setup webgl render each frame, called automatically by engine
5642
- // Also used by tile layer rendering when redrawing tiles
5643
- function glPreRender()
6060
+ function glSetInstancedMode(force=false)
5644
6061
  {
5645
- if (!glEnable || headlessMode) return;
5646
-
5647
- // set up the shader and canvas
5648
- glClearCanvas();
6062
+ if (!glPolyMode && !force)
6063
+ return;
6064
+
6065
+ // setup instanced mode
6066
+ glFlush();
6067
+ glPolyMode = false;
5649
6068
  glContext.useProgram(glShader);
5650
- glContext.activeTexture(glContext.TEXTURE0);
5651
- if (textureInfos[0])
5652
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
5653
6069
 
5654
6070
  // set vertex attributes
5655
- let offset = glAdditive = glBatchAdditive = 0;
6071
+ let offset = 0;
5656
6072
  const initVertexAttribArray = (name, type, typeSize, size)=>
5657
6073
  {
5658
6074
  const location = glContext.getAttribLocation(glShader, name);
5659
6075
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
5660
6076
  const divisor = typeSize && 1; // only if not geometry
5661
- const normalize = typeSize == 1; // only if color
6077
+ const normalize = typeSize === 1; // only if color
5662
6078
  glContext.enableVertexAttribArray(location);
5663
6079
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
5664
6080
  glContext.vertexAttribDivisor(location, divisor);
@@ -5667,46 +6083,106 @@ function glPreRender()
5667
6083
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
5668
6084
  initVertexAttribArray('g', glContext.FLOAT, 0, 2); // geometry
5669
6085
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
5670
- glContext.bufferData(glContext.ARRAY_BUFFER, gl_INSTANCE_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6086
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
5671
6087
  initVertexAttribArray('p', glContext.FLOAT, 4, 4); // position & size
5672
6088
  initVertexAttribArray('u', glContext.FLOAT, 4, 4); // texture coords
5673
6089
  initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
5674
6090
  initVertexAttribArray('a', glContext.UNSIGNED_BYTE, 1, 4); // additiveColor
5675
6091
  initVertexAttribArray('r', glContext.FLOAT, 4, 1); // rotation
6092
+ }
6093
+
6094
+ function glSetPolyMode()
6095
+ {
6096
+ if (glPolyMode)
6097
+ return;
5676
6098
 
6099
+ // setup poly mode
6100
+ glFlush();
6101
+ glPolyMode = true;
6102
+ glContext.useProgram(glPolyShader);
6103
+
6104
+ // set vertex attributes
6105
+ let offset = 0;
6106
+ const initVertexAttribArray = (name, type, typeSize, size)=>
6107
+ {
6108
+ const location = glContext.getAttribLocation(glPolyShader, name);
6109
+ const normalize = typeSize === 1; // only normalize if color
6110
+ const stride = gl_POLY_VERTEX_BYTE_STRIDE;
6111
+ glContext.enableVertexAttribArray(location);
6112
+ glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
6113
+ glContext.vertexAttribDivisor(location, 0);
6114
+ offset += size*typeSize;
6115
+ }
6116
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
6117
+ glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
6118
+ initVertexAttribArray('p', glContext.FLOAT, 4, 2); // position
6119
+ initVertexAttribArray('c', glContext.UNSIGNED_BYTE, 1, 4); // color
6120
+ }
6121
+
6122
+ // Setup WebGL render each frame, called automatically by engine
6123
+ // Also used by tile layer rendering when redrawing tiles
6124
+ function glPreRender()
6125
+ {
6126
+ if (!glEnable || !glContext) return;
6127
+
6128
+ // clear the canvas
6129
+ glClearCanvas();
6130
+
5677
6131
  // build the transform matrix
5678
6132
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
5679
6133
  const rotatedCam = cameraPos.rotate(-cameraAngle);
5680
6134
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
5681
6135
  const ca = Math.cos(cameraAngle);
5682
6136
  const sa = Math.sin(cameraAngle);
5683
- glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
5684
- [
6137
+ const transform = [
5685
6138
  s.x * ca, s.y * sa, 0, 0,
5686
6139
  -s.x * sa, s.y * ca, 0, 0,
5687
6140
  1, 1, 1, 0,
5688
- p.x, p.y, 0, 1
5689
- ]);
6141
+ p.x, p.y, 0, 1];
6142
+
6143
+ // set the same matrix for both shaders
6144
+ const initUniform = (program, uniform, value) =>
6145
+ {
6146
+ glContext.useProgram(program);
6147
+ const location = glContext.getUniformLocation(program, uniform);
6148
+ glContext.uniformMatrix4fv(location, false, value);
6149
+ }
6150
+ initUniform(glPolyShader, 'm', transform);
6151
+ initUniform(glShader, 'm', transform);
6152
+
6153
+ // set the active texture
6154
+ glContext.activeTexture(glContext.TEXTURE0);
6155
+ if (textureInfos[0])
6156
+ {
6157
+ glActiveTexture = textureInfos[0].glTexture;
6158
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
6159
+ }
6160
+
6161
+ // start in instanced rendering mode with additive blending off
6162
+ glAdditive = glBatchAdditive = glPolyMode = false;
6163
+ glSetInstancedMode(true);
5690
6164
  }
5691
6165
 
5692
6166
  /** Clear the canvas and setup the viewport
5693
6167
  * @memberof WebGL */
5694
6168
  function glClearCanvas()
5695
6169
  {
6170
+ if (!glContext) return;
6171
+
5696
6172
  // clear and set to same size as main canvas
5697
6173
  glContext.viewport(0, 0, glCanvas.width=drawCanvas.width, glCanvas.height=drawCanvas.height);
5698
6174
  glContext.clear(glContext.COLOR_BUFFER_BIT);
5699
6175
  }
5700
6176
 
5701
- /** Set the WebGl texture, called automatically if using multiple textures
6177
+ /** Set the WebGL texture, called automatically if using multiple textures
5702
6178
  * - This may also flush the gl buffer resulting in more draw calls and worse performance
5703
6179
  * @param {WebGLTexture} texture
5704
- * @param {boolean} wrap - Should the texture wrap or clamp
6180
+ * @param {boolean} [wrap] - Should the texture wrap or clamp
5705
6181
  * @memberof WebGL */
5706
6182
  function glSetTexture(texture, wrap=false)
5707
6183
  {
5708
6184
  // must flush cache with the old texture to set a new one
5709
- if (headlessMode || texture == glActiveTexture)
6185
+ if (!glContext || texture === glActiveTexture)
5710
6186
  return;
5711
6187
 
5712
6188
  glFlush();
@@ -5719,12 +6195,14 @@ function glSetTexture(texture, wrap=false)
5719
6195
  }
5720
6196
 
5721
6197
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
5722
- * @param {String} source
5723
- * @param {Number} type
6198
+ * @param {string} source
6199
+ * @param {number} type
5724
6200
  * @return {WebGLShader}
5725
6201
  * @memberof WebGL */
5726
6202
  function glCompileShader(source, type)
5727
6203
  {
6204
+ if (!glContext) return;
6205
+
5728
6206
  // build the shader
5729
6207
  const shader = glContext.createShader(type);
5730
6208
  glContext.shaderSource(shader, source);
@@ -5737,12 +6215,14 @@ function glCompileShader(source, type)
5737
6215
  }
5738
6216
 
5739
6217
  /** Create WebGL program with given shaders
5740
- * @param {String} vsSource
5741
- * @param {String} fsSource
6218
+ * @param {string} vsSource
6219
+ * @param {string} fsSource
5742
6220
  * @return {WebGLProgram}
5743
6221
  * @memberof WebGL */
5744
6222
  function glCreateProgram(vsSource, fsSource)
5745
6223
  {
6224
+ if (!glContext) return;
6225
+
5746
6226
  // build the program
5747
6227
  const program = glContext.createProgram();
5748
6228
  glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
@@ -5761,6 +6241,8 @@ function glCreateProgram(vsSource, fsSource)
5761
6241
  * @memberof WebGL */
5762
6242
  function glCreateTexture(image)
5763
6243
  {
6244
+ if (!glContext) return;
6245
+
5764
6246
  // build the texture
5765
6247
  const texture = glContext.createTexture();
5766
6248
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
@@ -5782,7 +6264,7 @@ function glCreateTexture(image)
5782
6264
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
5783
6265
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
5784
6266
  }
5785
-
6267
+
5786
6268
  // set texture filtering
5787
6269
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
5788
6270
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -5790,12 +6272,12 @@ function glCreateTexture(image)
5790
6272
  return texture;
5791
6273
  }
5792
6274
 
5793
-
5794
6275
  /** Deletes a WebGL texture
5795
6276
  * @param {WebGLTexture} [texture]
5796
6277
  * @memberof WebGL */
5797
6278
  function glDeleteTexture(texture)
5798
6279
  {
6280
+ if (!glContext) return;
5799
6281
  glContext.deleteTexture(texture);
5800
6282
  }
5801
6283
 
@@ -5805,6 +6287,8 @@ function glDeleteTexture(texture)
5805
6287
  * @memberof WebGL */
5806
6288
  function glSetTextureData(texture, image)
5807
6289
  {
6290
+ if (!glContext) return;
6291
+
5808
6292
  // build the texture
5809
6293
  ASSERT(!!image && image.width > 0, 'Invalid image data.');
5810
6294
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
@@ -5815,18 +6299,23 @@ function glSetTextureData(texture, image)
5815
6299
  * @memberof WebGL */
5816
6300
  function glFlush()
5817
6301
  {
5818
- if (!glEnable || !glInstanceCount) return;
5819
-
5820
- const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
5821
- glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
5822
- glContext.enable(glContext.BLEND);
5823
-
5824
- // draw all the sprites in the batch and reset the buffer
5825
- glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData);
5826
- glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glInstanceCount);
5827
- if (debug || showWatermark)
5828
- drawCount += glInstanceCount;
5829
- glInstanceCount = 0;
6302
+ if (glEnable && glContext && glBatchCount)
6303
+ {
6304
+ // set bend mode
6305
+ const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
6306
+ glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
6307
+ glContext.enable(glContext.BLEND);
6308
+ glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData);
6309
+
6310
+ // draw the batch
6311
+ if (glPolyMode)
6312
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
6313
+ else
6314
+ glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
6315
+ if (debug || showWatermark)
6316
+ drawCount += glBatchCount;
6317
+ glBatchCount = 0;
6318
+ }
5830
6319
  glBatchAdditive = glAdditive;
5831
6320
  }
5832
6321
 
@@ -5835,14 +6324,15 @@ function glFlush()
5835
6324
  * @memberof WebGL */
5836
6325
  function glCopyToContext(context)
5837
6326
  {
5838
- if (!glEnable)
6327
+ if (!glEnable || !glContext)
5839
6328
  return;
5840
6329
 
5841
6330
  glFlush();
5842
6331
  context.drawImage(glCanvas, 0, 0);
5843
6332
  }
5844
6333
 
5845
- /** Set anti-aliasing for webgl canvas
6334
+ /** Set anti-aliasing for WebGL canvas
6335
+ * Must be called before engineInit
5846
6336
  * @param {boolean} [antialias]
5847
6337
  * @memberof WebGL */
5848
6338
  function glSetAntialias(antialias=true)
@@ -5852,25 +6342,27 @@ function glSetAntialias(antialias=true)
5852
6342
  }
5853
6343
 
5854
6344
  /** Add a sprite to the gl draw list, used by all gl draw functions
5855
- * @param {Number} x
5856
- * @param {Number} y
5857
- * @param {Number} sizeX
5858
- * @param {Number} sizeY
5859
- * @param {Number} [angle]
5860
- * @param {Number} [uv0X]
5861
- * @param {Number} [uv0Y]
5862
- * @param {Number} [uv1X]
5863
- * @param {Number} [uv1Y]
5864
- * @param {Number} [rgba=-1] - white is -1
5865
- * @param {Number} [rgbaAdditive=0] - black is 0
6345
+ * @param {number} x
6346
+ * @param {number} y
6347
+ * @param {number} sizeX
6348
+ * @param {number} sizeY
6349
+ * @param {number} [angle]
6350
+ * @param {number} [uv0X]
6351
+ * @param {number} [uv0Y]
6352
+ * @param {number} [uv1X]
6353
+ * @param {number} [uv1Y]
6354
+ * @param {number} [rgba=-1] - white is -1
6355
+ * @param {number} [rgbaAdditive=0] - black is 0
5866
6356
  * @memberof WebGL */
5867
6357
  function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
5868
6358
  {
5869
6359
  // flush if there is not enough room or if different blend mode
5870
- if (glInstanceCount >= gl_MAX_INSTANCES || glBatchAdditive != glAdditive)
6360
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
5871
6361
  glFlush();
6362
+ glSetInstancedMode();
5872
6363
 
5873
- let offset = glInstanceCount++ * gl_INDICES_PER_INSTANCE;
6364
+ glPolyMode = false;
6365
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
5874
6366
  glPositionData[offset++] = x;
5875
6367
  glPositionData[offset++] = y;
5876
6368
  glPositionData[offset++] = sizeX;
@@ -5882,6 +6374,301 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
5882
6374
  glColorData[offset++] = rgba;
5883
6375
  glColorData[offset++] = rgbaAdditive;
5884
6376
  glPositionData[offset++] = angle;
6377
+ }
6378
+
6379
+ /** Transform and add a polygon to the gl draw list
6380
+ * @param {Array} points - Array of Vector2 points
6381
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6382
+ * @param {number} x
6383
+ * @param {number} y
6384
+ * @param {number} sx
6385
+ * @param {number} sy
6386
+ * @param {number} angle
6387
+ * @param {boolean} [tristrip] - should tristrip algorithm be used
6388
+ * @memberof WebGL */
6389
+ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
6390
+ {
6391
+ const pointsOut = [];
6392
+ for (const p of points)
6393
+ {
6394
+ // transform the point
6395
+ const px = p.x*sx;
6396
+ const py = p.y*sy;
6397
+ const sa = Math.sin(-angle);
6398
+ const ca = Math.cos(-angle);
6399
+ pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
6400
+ }
6401
+ const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
6402
+ glDrawPoints(drawPoints, rgba);
6403
+ }
6404
+
6405
+ /** Transform and add a polygon to the gl draw list
6406
+ * @param {Array} points - Array of Vector2 points
6407
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6408
+ * @param {number} lineWidth - Width of the outline
6409
+ * @param {number} x
6410
+ * @param {number} y
6411
+ * @param {number} sx
6412
+ * @param {number} sy
6413
+ * @param {number} angle
6414
+ * @memberof WebGL */
6415
+ function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle)
6416
+ {
6417
+ const outlinePoints = glMakeOutline(points, lineWidth);
6418
+ glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
6419
+ }
6420
+
6421
+ /** Add a polygon to the gl draw list
6422
+ * @param {Array} points - Array of Vector2 points in triangle strip order
6423
+ * @param {number} rgba - Color of the polygon as a 32-bit integer
6424
+ * @memberof WebGL */
6425
+ function glDrawPoints(points, rgba)
6426
+ {
6427
+ if (!glEnable || points.length < 3)
6428
+ return; // needs at least 3 points to have area
6429
+
6430
+ // add 2 degenerate verts if batching with existing polys to separate them
6431
+ const needsBridge = glPolyMode && glBatchCount > 0;
6432
+ const bridgeVerts = needsBridge ? 2 : 0;
6433
+ const vertCount = points.length + bridgeVerts;
6434
+
6435
+ // flush if there is not enough room or if different blend mode
6436
+ if (!glPolyMode || glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
6437
+ glFlush();
6438
+ glSetPolyMode();
6439
+
6440
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
6441
+
6442
+ // add degenerate bridge if needed (repeat last vertex of previous poly, then first of new poly)
6443
+ if (needsBridge)
6444
+ {
6445
+ // repeat last vertex from previous batch (it's at offset - 3)
6446
+ const prevOffset = offset - 3;
6447
+ glPositionData[offset++] = glPositionData[prevOffset];
6448
+ glPositionData[offset++] = glPositionData[prevOffset + 1];
6449
+ glColorData[offset++] = glColorData[prevOffset + 2];
6450
+
6451
+ // repeat first vertex of new poly
6452
+ glPositionData[offset++] = points[0].x;
6453
+ glPositionData[offset++] = points[0].y;
6454
+ glColorData[offset++] = rgba;
6455
+ }
6456
+
6457
+ // write vertices - they're already in triangle strip order
6458
+ for (const point of points)
6459
+ {
6460
+ glPositionData[offset++] = point.x;
6461
+ glPositionData[offset++] = point.y;
6462
+ glColorData[offset++] = rgba;
6463
+ }
6464
+ glBatchCount += vertCount;
6465
+ }
6466
+
6467
+ // WebGL internal function to convert polygon to outline triangle strip
6468
+ function glMakeOutline(points, width)
6469
+ {
6470
+ if (points.length < 2)
6471
+ return [];
6472
+
6473
+ const halfWidth = width / 2;
6474
+ const strip = [];
6475
+ const n = points.length;
6476
+ const e = 1e-6;
6477
+ for (let i = 0; i < n; i++)
6478
+ {
6479
+ // for each vertex, calculate normal based on adjacent edges
6480
+ const prev = points[(i - 1 + n) % n];
6481
+ const curr = points[i];
6482
+ const next = points[(i + 1) % n];
6483
+
6484
+ // direction from previous to current
6485
+ const dx1 = curr.x - prev.x;
6486
+ const dy1 = curr.y - prev.y;
6487
+ const len1 = (dx1*dx1 + dy1*dy1)**.5;
6488
+
6489
+ // direction from current to next
6490
+ const dx2 = next.x - curr.x;
6491
+ const dy2 = next.y - curr.y;
6492
+ const len2 = (dx2*dx2 + dy2*dy2)**.5;
6493
+
6494
+ if (len1 < e && len2 < e)
6495
+ continue; // skip degenerate point
6496
+
6497
+ // calculate perpendicular normals for each edge
6498
+ const nx1 = len1 > e ? -dy1 / len1 : 0;
6499
+ const ny1 = len1 > e ? dx1 / len1 : 0;
6500
+ const nx2 = len2 > e ? -dy2 / len2 : 0;
6501
+ const ny2 = len2 > e ? dx2 / len2 : 0;
6502
+
6503
+ // average the normals for miter
6504
+ let nx = nx1 + nx2;
6505
+ let ny = ny1 + ny2;
6506
+ const nlen = (nx*nx + ny*ny)**.5;
6507
+ if (nlen < e)
6508
+ {
6509
+ // 180 degree turn - use perpendicular
6510
+ nx = nx1;
6511
+ ny = ny1;
6512
+ }
6513
+ else
6514
+ {
6515
+ // calculate miter length
6516
+ nx /= nlen;
6517
+ ny /= nlen;
6518
+ const dot = nx1 * nx + ny1 * ny;
6519
+ if (dot > e)
6520
+ {
6521
+ // scale normal by miter length
6522
+ const miterLength = 1 / dot;
6523
+ nx *= miterLength;
6524
+ ny *= miterLength;
6525
+ }
6526
+ }
6527
+
6528
+ // create inner and outer points along the normal
6529
+ const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
6530
+ const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
6531
+ strip.push(inner);
6532
+ strip.push(outer);
6533
+ }
6534
+ if (strip.length > 1)
6535
+ {
6536
+ // close the loop
6537
+ strip.push(strip[0]);
6538
+ strip.push(strip[1]);
6539
+ }
6540
+ return strip;
6541
+ }
6542
+
6543
+ // WebGL internal function to convert polys to tri strips
6544
+ function glPolyStrip(points)
6545
+ {
6546
+ // validate input
6547
+ if (points.length < 3)
6548
+ return [];
6549
+
6550
+ // cross product helper: (b-a) x (c-a)
6551
+ const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
6552
+
6553
+ // calculate signed area of polygon
6554
+ const signedArea = (poly)=>
6555
+ {
6556
+ let area = 0;
6557
+ for (let i = poly.length; i--;)
6558
+ {
6559
+ const j = (i+1) % poly.length;
6560
+ area += poly[i].cross(poly[j]);
6561
+ }
6562
+ return area;
6563
+ }
6564
+
6565
+ // ensure counter-clockwise winding
6566
+ if (signedArea(points) < 0)
6567
+ points = points.reverse();
6568
+
6569
+ // tolerance constants
6570
+ const e = 1e-10;
6571
+
6572
+ // check if point is inside triangle
6573
+ const pointInTriangle = (p, a, b, c)=>
6574
+ {
6575
+ const c1 = cross(a, b, p);
6576
+ const c2 = cross(b, c, p);
6577
+ const c3 = cross(c, a, p);
6578
+ const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
6579
+ const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
6580
+ return !(negative && positive);
6581
+ };
6582
+
6583
+ // ear clipping triangulation
6584
+ const indices = [];
6585
+ for (let i = 0; i < points.length; ++i)
6586
+ indices[i] = i;
6587
+ const triangles = [];
6588
+ let attempts = 0;
6589
+ const maxAttempts = points.length ** 2 + 100;
6590
+ while (indices.length > 3 && attempts++ < maxAttempts)
6591
+ {
6592
+ let foundEar = false;
6593
+ for (let i = indices.length; --i;)
6594
+ {
6595
+ const i0 = indices[(i + indices.length - 1) % indices.length];
6596
+ const i1 = indices[i];
6597
+ const i2 = indices[(i + 1) % indices.length];
6598
+ const a = points[i0], b = points[i1], c = points[i2];
6599
+
6600
+ // check if convex
6601
+ if (cross(a, b, c) < e)
6602
+ continue;
6603
+
6604
+ // check if any other point is inside
6605
+ let hasInside = false;
6606
+ for (let j = 0; j < indices.length; j++)
6607
+ {
6608
+ const k = indices[j];
6609
+ if (k === i0 || k === i1 || k === i2)
6610
+ continue;
6611
+ const p = points[k];
6612
+ hasInside = pointInTriangle(p, a, b, c);
6613
+ if (hasInside)
6614
+ break;
6615
+ }
6616
+ if (hasInside)
6617
+ continue;
6618
+
6619
+ // found valid ear
6620
+ triangles.push([i0, i1, i2]);
6621
+ indices.splice(i, 1);
6622
+ foundEar = true;
6623
+ break;
6624
+ }
6625
+
6626
+ // fallback for degenerate cases
6627
+ if (!foundEar)
6628
+ {
6629
+ let worstIndex = -1, worstValue = Infinity;
6630
+ for (let i = indices.length; --i;)
6631
+ {
6632
+ const i0 = indices[(i + indices.length - 1) % indices.length];
6633
+ const i1 = indices[i];
6634
+ const i2 = indices[(i + 1) % indices.length];
6635
+ const value = abs(cross(points[i0], points[i1], points[i2]));
6636
+ if (value < worstValue)
6637
+ {
6638
+ worstValue = value;
6639
+ worstIndex = i;
6640
+ }
6641
+ }
6642
+ if (worstIndex < 0)
6643
+ break;
6644
+
6645
+ const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
6646
+ const i1 = indices[worstIndex];
6647
+ const i2 = indices[(worstIndex + 1) % indices.length];
6648
+ triangles.push([i0, i1, i2]);
6649
+ indices.splice(worstIndex, 1);
6650
+ }
6651
+ }
6652
+
6653
+ // add final triangle
6654
+ if (indices.length === 3)
6655
+ triangles.push([indices[0], indices[1], indices[2]]);
6656
+ if (!triangles.length)
6657
+ return [];
6658
+
6659
+ // convert triangles to triangle strip with degenerate connectors
6660
+ const strip = [];
6661
+ let [a0, b0, c0] = triangles[0];
6662
+ strip.push(points[a0], points[b0], points[c0]);
6663
+ for (let i = 1; i < triangles.length; i++)
6664
+ {
6665
+ // add degenerate bridge from last vertex to first of new triangle
6666
+ const [a, b, c] = triangles[i];
6667
+ strip.push(points[c0], points[a]);
6668
+ strip.push(points[a], points[b], points[c]);
6669
+ c0 = c;
6670
+ }
6671
+ return strip;
5885
6672
  }
5886
6673
  /**
5887
6674
  * LittleJS Newgrounds API
@@ -5906,11 +6693,11 @@ let newgrounds;
5906
6693
  class NewgroundsMedal extends Medal
5907
6694
  {
5908
6695
  /** Create a newgrounds medal object and adds it to the list of medals
5909
- * @param {Number} id - The unique identifier of the medal
5910
- * @param {String} name - Name of the medal
5911
- * @param {String} [description] - Description of the medal
5912
- * @param {String} [icon] - Icon for the medal
5913
- * @param {String} [src] - Image location for the medal
6696
+ * @param {number} id - The unique identifier of the medal
6697
+ * @param {string} name - Name of the medal
6698
+ * @param {string} [description] - Description of the medal
6699
+ * @param {string} [icon] - Icon for the medal
6700
+ * @param {string} [src] - Image location for the medal
5914
6701
  */
5915
6702
  constructor(id, name, description, icon, src)
5916
6703
  { super(id, name, description, icon, src); }
@@ -6002,7 +6789,7 @@ class NewgroundsPlugin
6002
6789
  * @param {number} id - The scoreboard id
6003
6790
  * @param {string} [user] - A user's id or name
6004
6791
  * @param {number} [social] - If true, only social scores will be loaded
6005
- * @param {number} [skip] - Number of scores to skip before start
6792
+ * @param {number} [skip] - Number of scores to skip over
6006
6793
  * @param {number} [limit] - Number of scores to include in the list
6007
6794
  * @return {Object} - The response JSON object
6008
6795
  */
@@ -6090,6 +6877,13 @@ class PostProcessPlugin
6090
6877
  postProcess = this;
6091
6878
 
6092
6879
  if (headlessMode) return;
6880
+
6881
+ if (!glEnable)
6882
+ {
6883
+ console.warn('PostProcessPlugin: WebGL not enabled!');
6884
+ return;
6885
+ }
6886
+
6093
6887
  if (!shaderCode) // default shader pass through
6094
6888
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
6095
6889
 
@@ -6218,16 +7012,16 @@ class ZzFXMusic extends Sound
6218
7012
  if (!soundEnable || headlessMode) return;
6219
7013
  this.randomness = 0;
6220
7014
  this.sampleChannels = zzfxM(...zzfxMusic);
6221
- this.sampleRate = zzfxR;
7015
+ this.sampleRate = audioDefaultSampleRate;
6222
7016
  }
6223
7017
 
6224
- /** Play the music
6225
- * @param {number} [volume=1] - How much to scale volume by
6226
- * @param {boolean} [loop] - True if the music should loop
7018
+ /** Play the music that loops by default
7019
+ * @param {number} [volume] - Volume to play the music at
7020
+ * @param {boolean} [loop] - Should the music loop?
6227
7021
  * @return {AudioBufferSourceNode} - The audio source node
6228
7022
  */
6229
- playMusic(volume, loop=false)
6230
- { return super.play(undefined, volume, 1, 1, loop); }
7023
+ playMusic(volume=1, loop=true)
7024
+ { return super.play(undefined, volume, 1, 0, loop); }
6231
7025
  }
6232
7026
 
6233
7027
  ///////////////////////////////////////////////////////////////////////////////
@@ -6261,7 +7055,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
6261
7055
  let panning = 0;
6262
7056
  let hasMore = 1;
6263
7057
  let sampleCache = {};
6264
- let beatLength = zzfxR / BPM * 60 >> 2;
7058
+ let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
6265
7059
 
6266
7060
  // for each channel in order until there are no more
6267
7061
  for (; hasMore; channelIndex++) {
@@ -6280,15 +7074,15 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
6280
7074
  // get next offset, use the length of first channel
6281
7075
  nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
6282
7076
  // for each beat in pattern, plus one extra if end of sequence
6283
- isSequenceEnd = sequenceIndex == sequence.length - 1;
7077
+ isSequenceEnd = sequenceIndex === sequence.length - 1;
6284
7078
  for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
6285
7079
 
6286
7080
  // <channel-note>
6287
7081
  note = patternChannel[i];
6288
7082
 
6289
7083
  // stop if end, different instrument or new note
6290
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
6291
- instrument != (patternChannel[0] || 0) || note | 0;
7084
+ stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
7085
+ instrument !== (patternChannel[0] || 0) || note | 0;
6292
7086
 
6293
7087
  // fill buffer with samples for previous beat, most cpu intensive part
6294
7088
  for (j = 0; j < beatLength && notFirstBeat;
@@ -6373,11 +7167,11 @@ class UISystemPlugin
6373
7167
  /** @property {Color} - Default text color for UI elements */
6374
7168
  this.defaultTextColor = BLACK;
6375
7169
  /** @property {Color} - Default button color for UI elements */
6376
- this.defaultButtonColor = hsl(0,0,.5);
7170
+ this.defaultButtonColor = hsl(0,0,.7);
6377
7171
  /** @property {Color} - Default hover color for UI elements */
6378
- this.defaultHoverColor = hsl(0,0,.7);
7172
+ this.defaultHoverColor = hsl(0,0,.9);
6379
7173
  /** @property {Color} - Default color for disabled UI elements */
6380
- this.defaultDisabledColor = hsl(0,0,.2);
7174
+ this.defaultDisabledColor = hsl(0,0,.3);
6381
7175
  /** @property {number} - Default line width for UI elements */
6382
7176
  this.defaultLineWidth = 4;
6383
7177
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -6397,16 +7191,16 @@ class UISystemPlugin
6397
7191
 
6398
7192
  engineAddPlugin(uiUpdate, uiRender);
6399
7193
 
6400
- function updateInvisible(o)
6401
- {
6402
- for(const c of o.children)
6403
- updateInvisible(c);
6404
- o.updateInvisible();
6405
- }
6406
-
6407
7194
  // setup recursive update and render
6408
7195
  function uiUpdate()
6409
7196
  {
7197
+ function updateInvisibleObject(o)
7198
+ {
7199
+ // update invisible objects
7200
+ for (const c of o.children)
7201
+ updateInvisibleObject(c);
7202
+ o.updateInvisible();
7203
+ }
6410
7204
  function updateObject(o)
6411
7205
  {
6412
7206
  if (o.visible)
@@ -6415,12 +7209,12 @@ class UISystemPlugin
6415
7209
  if (o.parent)
6416
7210
  o.pos = o.localPos.add(o.parent.pos);
6417
7211
  // update in reverse order to detect mouse enter/leave
6418
- for(let i=o.children.length; i--;)
7212
+ for (let i=o.children.length; i--;)
6419
7213
  updateObject(o.children[i]);
6420
7214
  o.update();
6421
7215
  }
6422
7216
  else
6423
- updateInvisible(o);
7217
+ updateInvisibleObject(o);
6424
7218
  }
6425
7219
  uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
6426
7220
  }
@@ -6433,7 +7227,7 @@ class UISystemPlugin
6433
7227
  if (o.parent)
6434
7228
  o.pos = o.localPos.add(o.parent.pos);
6435
7229
  o.render();
6436
- for(const c of o.children)
7230
+ for (const c of o.children)
6437
7231
  renderObject(c);
6438
7232
  }
6439
7233
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
@@ -6446,7 +7240,7 @@ class UISystemPlugin
6446
7240
  * @param {Color} [color=uiSystem.defaultColor]
6447
7241
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6448
7242
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
6449
- * @param {number} [lineWidth=uiSystem.defaultCornerRadius] */
7243
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
6450
7244
  drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
6451
7245
  {
6452
7246
  const context = uiSystem.uiContext;
@@ -6490,7 +7284,7 @@ class UISystemPlugin
6490
7284
  * @param {boolean} [mirror] */
6491
7285
  drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false)
6492
7286
  {
6493
- drawTile(pos, size, tileInfo, color, angle, mirror, BLACK, false, true, uiSystem.uiContext);
7287
+ drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, uiSystem.uiContext);
6494
7288
  }
6495
7289
 
6496
7290
  /** Draw text to the UI context
@@ -6521,32 +7315,40 @@ class UIObject
6521
7315
  constructor(pos=vec2(), size=vec2())
6522
7316
  {
6523
7317
  /** @property {Vector2} - Local position of the object */
6524
- this.localPos = pos.copy();
7318
+ this.localPos = pos.copy();
6525
7319
  /** @property {Vector2} - Screen space position of the object */
6526
- this.pos = pos.copy();
7320
+ this.pos = pos.copy();
6527
7321
  /** @property {Vector2} - Screen space size of the object */
6528
- this.size = size.copy();
6529
- /** @property {Color} - color of the object */
6530
- this.color = uiSystem.defaultColor;
6531
- /** @property {Color} - color for text */
6532
- this.textColor = uiSystem.defaultTextColor;
6533
- /** @property {Color} - color used when hovering over the object */
7322
+ this.size = size.copy();
7323
+ /** @property {Color} - Color of the object */
7324
+ this.color = uiSystem.defaultColor;
7325
+ /** @property {string} - Text for this ui object */
7326
+ this.text = undefined;
7327
+ /** @property {Color} - Color when disabled */
7328
+ this.disabledColor = uiSystem.defaultDisabledColor;
7329
+ /** @property {boolean} - Is this object disabled? */
7330
+ this.disabled = false;
7331
+ /** @property {Color} - Color for text */
7332
+ this.textColor = uiSystem.defaultTextColor;
7333
+ /** @property {Color} - Color used when hovering over the object */
6534
7334
  this.hoverColor = uiSystem.defaultHoverColor;
6535
- /** @property {Color} - color for line drawing */
6536
- this.lineColor = uiSystem.defaultLineColor;
6537
- /** @property {number} - width for line drawing */
6538
- this.lineWidth = uiSystem.defaultLineWidth;
6539
- /** @property {string} - font for this objecct */
6540
- this.font = uiSystem.defaultFont;
6541
- /** @property {number} - override for text height */
6542
- this.textHeight = undefined;
6543
- /** @property {boolean} - should this object be drawn */
6544
- this.visible = true;
6545
- /** @property {Array<UIObject>} - a list of this object's children */
6546
- this.children = [];
6547
- /** @property {UIObject} - this object's parent, position is in parent space */
6548
- this.parent = undefined;
6549
- /** @property {number} - Extra size added when checking if element is touched */
7335
+ /** @property {Color} - Color for line drawing */
7336
+ this.lineColor = uiSystem.defaultLineColor;
7337
+ /** @property {number} - Width for line drawing */
7338
+ this.lineWidth = uiSystem.defaultLineWidth;
7339
+ /** @property {number} - Corner radius for rounded rects */
7340
+ this.cornerRadius = uiSystem.defaultCornerRadius;
7341
+ /** @property {string} - Font for this objecct */
7342
+ this.font = uiSystem.defaultFont;
7343
+ /** @property {number} - Override for text height */
7344
+ this.textHeight = undefined;
7345
+ /** @property {boolean} - Should this object be drawn */
7346
+ this.visible = true;
7347
+ /** @property {Array<UIObject>} - A list of this object's children */
7348
+ this.children = [];
7349
+ /** @property {UIObject} - This object's parent, position is in parent space */
7350
+ this.parent = undefined;
7351
+ /** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
6550
7352
  this.extraTouchSize = 0;
6551
7353
  /** @property {Sound} - Sound when interactive element is pressed */
6552
7354
  this.soundPress = uiSystem.defaultSoundPress;
@@ -6578,7 +7380,7 @@ class UIObject
6578
7380
  */
6579
7381
  removeChild(child)
6580
7382
  {
6581
- ASSERT(child.parent == this && this.children.includes(child));
7383
+ ASSERT(child.parent === this && this.children.includes(child));
6582
7384
  this.children.splice(this.children.indexOf(child), 1);
6583
7385
  child.parent = undefined;
6584
7386
  }
@@ -6631,7 +7433,7 @@ class UIObject
6631
7433
  this.mouseIsHeld = false;
6632
7434
  }
6633
7435
 
6634
- if (this.mouseIsOver != mouseWasOver)
7436
+ if (this.mouseIsOver !== mouseWasOver)
6635
7437
  this.mouseIsOver ? this.onEnter() : this.onLeave();
6636
7438
  }
6637
7439
 
@@ -6642,7 +7444,7 @@ class UIObject
6642
7444
  uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
6643
7445
  }
6644
7446
 
6645
- /** Special update for when object is invisible */
7447
+ /** Special update when object is not visible */
6646
7448
  updateInvisible()
6647
7449
  {
6648
7450
  // reset input state when not visible
@@ -6650,28 +7452,22 @@ class UIObject
6650
7452
  }
6651
7453
 
6652
7454
  /** Called when the mouse enters the object */
6653
- onEnter()
6654
- {}
7455
+ onEnter() {}
6655
7456
 
6656
7457
  /** Called when the mouse leaves the object */
6657
- onLeave()
6658
- {}
7458
+ onLeave() {}
6659
7459
 
6660
7460
  /** Called when the mouse is pressed while over the object */
6661
- onPress()
6662
- {}
7461
+ onPress() {}
6663
7462
 
6664
7463
  /** Called when the mouse is released while over the object */
6665
- onRelease()
6666
- {}
7464
+ onRelease() {}
6667
7465
 
6668
7466
  /** Called when user clicks on this object */
6669
- onClick()
6670
- {}
7467
+ onClick() {}
6671
7468
 
6672
7469
  /** Called when the state of this object changes */
6673
- onChange()
6674
- {}
7470
+ onChange() {}
6675
7471
  };
6676
7472
 
6677
7473
  ///////////////////////////////////////////////////////////////////////////////
@@ -6692,13 +7488,13 @@ class UIText extends UIObject
6692
7488
  {
6693
7489
  super(pos, size);
6694
7490
 
6695
- /** @property {string} */
7491
+ // set properties
6696
7492
  this.text = text;
6697
- /** @property {string} */
6698
7493
  this.align = align;
7494
+ this.font = font;
6699
7495
 
6700
- this.font = font; // set font
6701
- this.lineWidth = 0; // set text to not be outlined by default
7496
+ // make text not outlined by default
7497
+ this.lineWidth = 0;
6702
7498
  }
6703
7499
  render()
6704
7500
  {
@@ -6725,13 +7521,14 @@ class UITile extends UIObject
6725
7521
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
6726
7522
  {
6727
7523
  super(pos, size);
6728
-
6729
7524
  /** @property {TileInfo} - Tile image to use */
6730
7525
  this.tileInfo = tileInfo;
6731
7526
  /** @property {number} - Angle to rotate in radians */
6732
7527
  this.angle = angle;
6733
7528
  /** @property {boolean} - Should it be mirrored? */
6734
7529
  this.mirror = mirror;
7530
+
7531
+ // set properties
6735
7532
  this.color = color;
6736
7533
  }
6737
7534
  render()
@@ -6757,22 +7554,20 @@ class UIButton extends UIObject
6757
7554
  {
6758
7555
  super(pos, size);
6759
7556
 
6760
- /** @property {string} */
7557
+ // set properties
6761
7558
  this.text = text;
6762
- /** @property {Color} */
6763
- this.disabledColor = uiSystem.defaultDisabledColor;
6764
- /** @property {boolean} */
6765
- this.disabled = false;
6766
- this.interactive = true;
6767
7559
  this.color = color;
7560
+ this.interactive = true;
6768
7561
  }
6769
7562
  render()
6770
7563
  {
7564
+ // draw the button
6771
7565
  const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
6772
7566
  const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
6773
7567
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
6774
7568
 
6775
- const textScale = .8; // scale text to fit in button
7569
+ // draw the text
7570
+ const textScale = .8; // scale text to fit
6776
7571
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
6777
7572
  uiSystem.drawText(this.text, this.pos, textSize,
6778
7573
  this.textColor, 0, undefined, this.align, this.font);
@@ -6790,13 +7585,18 @@ class UICheckbox extends UIObject
6790
7585
  * @param {Vector2} [pos]
6791
7586
  * @param {Vector2} [size]
6792
7587
  * @param {boolean} [checked]
7588
+ * @param {string} [text]
7589
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6793
7590
  */
6794
- constructor(pos, size, checked=false)
7591
+ constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
6795
7592
  {
6796
7593
  super(pos, size);
6797
-
6798
- /** @property {boolean} */
7594
+ /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
6799
7595
  this.checked = checked;
7596
+
7597
+ // set properties
7598
+ this.text = text;
7599
+ this.color = color;
6800
7600
  this.interactive = true;
6801
7601
  }
6802
7602
  onClick()
@@ -6806,14 +7606,24 @@ class UICheckbox extends UIObject
6806
7606
  }
6807
7607
  render()
6808
7608
  {
6809
- const color = this.mouseIsOver? this.hoverColor : this.color;
7609
+ const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
6810
7610
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
6811
7611
  if (this.checked)
6812
7612
  {
6813
- // draw an X if checked
6814
- uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor);
6815
- uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor);
7613
+ const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
7614
+ const length = lerp(1, 2**.5/2, p) / 2;
7615
+ let s = this.size.scale(length);
7616
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
7617
+ uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
6816
7618
  }
7619
+
7620
+ // draw the text to the right side of the checkbox
7621
+ const textScale = .8; // scale text to fit
7622
+ const gapScale = .55;
7623
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7624
+ const pos = this.pos.add(vec2(this.size.x*gapScale,0));
7625
+ uiSystem.drawText(this.text, pos, textSize,
7626
+ this.textColor, 0, undefined, 'left', this.font);
6817
7627
  }
6818
7628
  }
6819
7629
 
@@ -6836,43 +7646,51 @@ class UIScrollbar extends UIObject
6836
7646
  {
6837
7647
  super(pos, size);
6838
7648
 
6839
- /** @property {number} */
7649
+ /** @property {number} - Current percentage value of this scrollbar 0-1 */
6840
7650
  this.value = value;
6841
- /** @property {string} */
6842
- this.text = text;
6843
- /** @property {Color} */
7651
+ /** @property {Color} - Color for the handle part of the scrollbar */
6844
7652
  this.handleColor = handleColor;
7653
+
7654
+ // set properties
7655
+ this.text = text;
6845
7656
  this.color = color;
6846
7657
  this.interactive = true;
6847
7658
  }
6848
7659
  update()
6849
7660
  {
6850
7661
  super.update();
6851
- if (this.mouseIsHeld)
7662
+ if (this.mouseIsHeld && this.interactive)
6852
7663
  {
7664
+ // check if value changed
6853
7665
  const handleSize = vec2(this.size.y);
6854
7666
  const handleWidth = this.size.x - handleSize.x;
6855
7667
  const p1 = this.pos.x - handleWidth/2;
6856
7668
  const p2 = this.pos.x + handleWidth/2;
6857
7669
  const oldValue = this.value;
6858
7670
  this.value = percent(mousePosScreen.x, p1, p2);
6859
- this.value == oldValue || this.onChange();
7671
+ this.value === oldValue || this.onChange();
6860
7672
  }
6861
7673
  }
6862
7674
  render()
6863
7675
  {
6864
- const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
6865
- const color = this.mouseIsOver? this.hoverColor : this.color;
7676
+ // draw the scrollbar background
7677
+ const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
7678
+ this.color : this.lineColor;
7679
+ const color = this.disabled ? this.disabledColor :
7680
+ this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
6866
7681
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
6867
7682
 
7683
+ // draw the scrollbar handle
6868
7684
  const handleSize = vec2(this.size.y);
6869
7685
  const handleWidth = this.size.x - handleSize.x;
6870
7686
  const p1 = this.pos.x - handleWidth/2;
6871
7687
  const p2 = this.pos.x + handleWidth/2;
6872
7688
  const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
6873
- const barColor = this.mouseIsHeld ? this.color : this.handleColor;
6874
- uiSystem.drawRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor, this.cornerRadius);
7689
+ const handleColor = this.disabled ? this.disabledColor :
7690
+ this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
7691
+ uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
6875
7692
 
7693
+ // draw the text on the scrollbar
6876
7694
  const textScale = .8; // scale text to fit in scrollbar
6877
7695
  const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
6878
7696
  uiSystem.drawText(this.text, this.pos, textSize,
@@ -6940,7 +7758,7 @@ class Box2dObject extends EngineObject
6940
7758
  bodyDef.set_angle(-angle);
6941
7759
  this.body = box2d.world.CreateBody(bodyDef);
6942
7760
  this.body.object = this;
6943
- this.outlineColor = BLACK;
7761
+ this.lineColor = BLACK;
6944
7762
  }
6945
7763
 
6946
7764
  /** Destroy this object and it's physics body */
@@ -6967,27 +7785,27 @@ class Box2dObject extends EngineObject
6967
7785
  if (this.tileInfo)
6968
7786
  super.render();
6969
7787
  else
6970
- this.drawFixtures(this.color, this.outlineColor, this.lineWidth, mainContext);
7788
+ this.drawFixtures(this.color, this.lineColor, this.lineWidth);
6971
7789
  }
6972
7790
 
6973
7791
  /** Render debug info */
6974
7792
  renderDebugInfo()
6975
7793
  {
6976
7794
  const isAsleep = !this.getIsAwake();
6977
- const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
7795
+ const isStatic = this.getBodyType() === box2d.bodyTypeStatic;
6978
7796
  const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
6979
7797
  this.drawFixtures(color);
6980
7798
  }
6981
7799
 
6982
7800
  /** Draws all this object's fixtures
6983
7801
  * @param {Color} [color]
6984
- * @param {Color} [outlineColor]
7802
+ * @param {Color} [lineColor]
6985
7803
  * @param {number} [lineWidth]
6986
7804
  * @param {CanvasRenderingContext2D} [context] */
6987
- drawFixtures(color=WHITE, outlineColor, lineWidth=.1, context)
7805
+ drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
6988
7806
  {
6989
7807
  this.getFixtureList().forEach(fixture=>
6990
- box2d.drawFixture(fixture, this.pos, this.angle, color, outlineColor, lineWidth, context));
7808
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context));
6991
7809
  }
6992
7810
 
6993
7811
  ///////////////////////////////////////////////////////////////////////////////
@@ -8442,7 +9260,7 @@ class Box2dPlugin
8442
9260
  queryCallback.ReportFixture = function(fixturePointer)
8443
9261
  {
8444
9262
  const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8445
- if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
9263
+ if (dynamicOnly && fixture.GetBody().GetType() !== box2d.instance.b2_dynamicBody)
8446
9264
  return true; // continue getting results
8447
9265
  if (!fixture.TestPoint(box2d.vec2dTo(pos)))
8448
9266
  return true; // continue getting results
@@ -8468,10 +9286,10 @@ class Box2dPlugin
8468
9286
  * @param {Vector2} pos
8469
9287
  * @param {number} angle
8470
9288
  * @param {Color} [color]
8471
- * @param {Color} [outlineColor]
9289
+ * @param {Color} [lineColor]
8472
9290
  * @param {number} [lineWidth]
8473
9291
  * @param {CanvasRenderingContext2D} [context] */
8474
- drawFixture(fixture, pos, angle, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
9292
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
8475
9293
  {
8476
9294
  const shape = box2d.castObjectType(fixture.GetShape());
8477
9295
  switch (shape.GetType())
@@ -8481,101 +9299,25 @@ class Box2dPlugin
8481
9299
  let points = [];
8482
9300
  for (let i=shape.GetVertexCount(); i--;)
8483
9301
  points.push(box2d.vec2From(shape.GetVertex(i)));
8484
- box2d.drawPoly(pos, angle, points, color, outlineColor, lineWidth, context);
9302
+ drawPoly(points, color, lineWidth, lineColor, pos, angle);
8485
9303
  break;
8486
9304
  }
8487
9305
  case box2d.instance.b2Shape.e_circle:
8488
9306
  {
8489
9307
  const radius = shape.get_m_radius();
8490
- box2d.drawCircle(pos, radius, color, outlineColor, lineWidth, context);
9308
+ drawCircle(pos, radius, color, lineWidth, lineColor);
8491
9309
  break;
8492
9310
  }
8493
9311
  case box2d.instance.b2Shape.e_edge:
8494
9312
  {
8495
9313
  const v1 = box2d.vec2From(shape.get_m_vertex1());
8496
9314
  const v2 = box2d.vec2From(shape.get_m_vertex2());
8497
- box2d.drawLine(pos, angle, v1, v2, color, lineWidth, context);
9315
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle);
8498
9316
  break;
8499
9317
  }
8500
9318
  }
8501
9319
  }
8502
9320
 
8503
- /** draws a circle
8504
- * @param {Vector2} pos
8505
- * @param {number} radius
8506
- * @param {Color} [color]
8507
- * @param {Color} [outlineColor]
8508
- * @param {number} [lineWidth]
8509
- * @param {CanvasRenderingContext2D} [context] */
8510
- drawCircle(pos, radius, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
8511
- {
8512
- drawCanvas2D(pos, vec2(1), 0, 0, context=>
8513
- {
8514
- context.beginPath();
8515
- context.arc(0, 0, radius, 0, 9);
8516
- box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8517
- }, 0, context);
8518
- }
8519
-
8520
- /** draws a polygon
8521
- * @param {Vector2} pos
8522
- * @param {number} angle
8523
- * @param {Array<Vector2>} points
8524
- * @param {Color} [color]
8525
- * @param {Color} [outlineColor]
8526
- * @param {number} [lineWidth]
8527
- * @param {CanvasRenderingContext2D} [context] */
8528
- drawPoly(pos, angle, points, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
8529
- {
8530
- drawCanvas2D(pos, vec2(1), angle, 0, context=>
8531
- {
8532
- context.beginPath();
8533
- points.forEach(p=>context.lineTo(p.x, p.y));
8534
- context.closePath();
8535
- box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8536
- }, 0, context);
8537
- }
8538
-
8539
- /** draws a line
8540
- * @param {Vector2} pos
8541
- * @param {number} angle
8542
- * @param {Vector2} posA
8543
- * @param {Vector2} posB
8544
- * @param {Color} [color]
8545
- * @param {number} [lineWidth]
8546
- * @param {CanvasRenderingContext2D} [context] */
8547
- drawLine(pos, angle, posA, posB, color=WHITE, lineWidth=.1, context=drawContext)
8548
- {
8549
- drawCanvas2D(pos, vec2(1), angle, 0, context=>
8550
- {
8551
- context.beginPath();
8552
- context.lineTo(posA.x, posA.y);
8553
- context.lineTo(posB.x, posB.y);
8554
- box2d.drawFillStroke(0, color, lineWidth, context);
8555
- }, 0, context);
8556
- }
8557
-
8558
- /** performs a fill or stroke as a helper to the other draw functions
8559
- * @param {Color} [color]
8560
- * @param {Color} [outlineColor]
8561
- * @param {number} [lineWidth]
8562
- * @param {CanvasRenderingContext2D} [context] */
8563
- drawFillStroke(color=WHITE, outlineColor=BLACK, lineWidth=.1, context=drawContext)
8564
- {
8565
- if (color)
8566
- {
8567
- context.fillStyle = color.toString();
8568
- context.fill();
8569
- }
8570
- if (outlineColor && lineWidth)
8571
- {
8572
- context.lineWidth = lineWidth;
8573
- context.lineJoin = context.lineCap = 'round';
8574
- context.strokeStyle = outlineColor.toString();
8575
- context.stroke();
8576
- }
8577
- }
8578
-
8579
9321
  ///////////////////////////////////////////////////////////////////////////////
8580
9322
  // helper functions
8581
9323
 
@@ -8649,7 +9391,9 @@ class Box2dPlugin
8649
9391
  }
8650
9392
 
8651
9393
  ///////////////////////////////////////////////////////////////////////////////
8652
- /** Box2d Init - Call with await before starting LittleJS to init box2d
9394
+ /** Box2d Init - Call with await to init box2d
9395
+ * @example
9396
+ * await box2dInit();
8653
9397
  * @return {Promise<Box2dPlugin>}
8654
9398
  * @memberof Box2D */
8655
9399
  async function box2dInit()
@@ -8676,6 +9420,7 @@ async function box2dInit()
8676
9420
  function setupDebugDraw()
8677
9421
  {
8678
9422
  // setup debug draw
9423
+ const debugLineWidth = .1;
8679
9424
  const debugDraw = new box2d.instance.JSDraw();
8680
9425
  const box2dColor = (c)=> new Color(c.get_r(), c.get_g(), c.get_b());
8681
9426
  const box2dColorPointer = (c)=>
@@ -8693,33 +9438,33 @@ async function box2dInit()
8693
9438
  color = getDebugColor(color);
8694
9439
  point1 = box2d.vec2FromPointer(point1);
8695
9440
  point2 = box2d.vec2FromPointer(point2);
8696
- box2d.drawLine(vec2(), 0, point1, point2, color, undefined, overlayContext);
9441
+ drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
8697
9442
  };
8698
9443
  debugDraw.DrawPolygon = function(vertices, vertexCount, color)
8699
9444
  {
8700
9445
  color = getDebugColor(color);
8701
9446
  const points = getPointsList(vertices, vertexCount);
8702
- box2d.drawPoly(vec2(), 0, points, undefined, color, undefined, overlayContext);
9447
+ drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
8703
9448
  };
8704
9449
  debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
8705
9450
  {
8706
9451
  color = getDebugColor(color);
8707
9452
  const points = getPointsList(vertices, vertexCount);
8708
- box2d.drawPoly(vec2(), 0, points, color, color, undefined, overlayContext);
9453
+ drawPoly(points, color, 0, color, vec2(), 0, false, false, overlayContext);
8709
9454
  };
8710
9455
  debugDraw.DrawCircle = function(center, radius, color)
8711
9456
  {
8712
9457
  color = getDebugColor(color);
8713
9458
  center = box2d.vec2FromPointer(center);
8714
- box2d.drawCircle(center, radius, undefined, color, undefined, overlayContext);
9459
+ drawCircle(center, radius, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
8715
9460
  };
8716
9461
  debugDraw.DrawSolidCircle = function(center, radius, axis, color)
8717
9462
  {
8718
9463
  color = getDebugColor(color);
8719
9464
  center = box2d.vec2FromPointer(center);
8720
9465
  axis = box2d.vec2FromPointer(axis).scale(radius);
8721
- box2d.drawCircle(center, radius, color, color, undefined, overlayContext);
8722
- box2d.drawLine(center, 0, vec2(), axis, color, undefined, overlayContext);
9466
+ drawCircle(center, radius, color, debugLineWidth, color, false, false, overlayContext);
9467
+ drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
8723
9468
  };
8724
9469
  debugDraw.DrawTransform = function(transform)
8725
9470
  {
@@ -8728,8 +9473,8 @@ async function box2dInit()
8728
9473
  const angle = -transform.get_q().GetAngle();
8729
9474
  const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
8730
9475
  const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
8731
- box2d.drawLine(pos, angle, vec2(), p1, c1, undefined, overlayContext);
8732
- box2d.drawLine(pos, angle, vec2(), p2, c2, undefined, overlayContext);
9476
+ drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false, false, overlayContext);
9477
+ drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false, false, overlayContext);
8733
9478
  }
8734
9479
 
8735
9480
  debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
@@ -8754,28 +9499,30 @@ async function box2dInit()
8754
9499
  * @param {Vector2} pos - Screen space position
8755
9500
  * @param {Vector2} size - Screen space size
8756
9501
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
8757
- * @param {number} [borderSize=1] - Width of the border sections
8758
- * @param {number} [extraSpace=.01] - Extra spacing adjustment
9502
+ * @param {number} [borderSize] - Width of the border sections
9503
+ * @param {number} [extraSpace] - Extra spacing adjustment
9504
+ * @param {number} [angle] - Angle to rotate by
8759
9505
  * @memberof DrawUtilities */
8760
- function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=1)
9506
+ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
8761
9507
  {
8762
- drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, false, true, overlayContext);
9508
+ drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
8763
9509
  }
8764
9510
 
8765
9511
  /** Draw a scalable nine-slice UI element in world space
8766
- * This function can apply color and additive color if webgl is enabled
9512
+ * This function can apply color and additive color if WebGL is enabled
8767
9513
  * @param {Vector2} pos - World space position
8768
9514
  * @param {Vector2} size - World space size
8769
9515
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
8770
9516
  * @param {Color} [color] - Color to modulate with
8771
- * @param {number} [borderSize=1] - Width of the border sections
9517
+ * @param {number} [borderSize] - Width of the border sections
8772
9518
  * @param {Color} [additiveColor] - Additive color
8773
- * @param {number} [extraSpace=.01] - Extra spacing adjustment
9519
+ * @param {number} [extraSpace] - Extra spacing adjustment
9520
+ * @param {number} [angle] - Angle to rotate by
8774
9521
  * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
8775
9522
  * @param {boolean} [screenSpace] - Use screen space coordinates
8776
9523
  * @param {CanvasRenderingContext2D} [context] - Canvas context to use
8777
9524
  * @memberof DrawUtilities */
8778
- function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.01, useWebGL=glEnable, screenSpace, context)
9525
+ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
8779
9526
  {
8780
9527
  // setup nine slice tiles
8781
9528
  const centerTile = startTile.offset(startTile.size);
@@ -8783,26 +9530,27 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
8783
9530
  const cornerSize = vec2(borderSize);
8784
9531
  const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
8785
9532
  const flip = screenSpace ? -1 : 1;
9533
+ const rotateAngle = screenSpace ? -angle : angle;
8786
9534
 
8787
9535
  // center
8788
- drawTile(pos, centerSize, centerTile, color, 0, false, additiveColor, useWebGL, screenSpace, context);
8789
- for(let i=4; i--;)
9536
+ drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9537
+ for (let i=4; i--;)
8790
9538
  {
8791
9539
  // sides
8792
9540
  const horizontal = i%2;
8793
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-1:1));
9541
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
8794
9542
  const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
8795
- const sideTile = centerTile.offset(startTile.size.multiply(vec2(i==1?1:i==3?-1:0,i==0?-flip:i==2?flip:0)))
8796
- drawTile(pos.add(sidePos), sideSize, sideTile, color, 0, false, additiveColor, useWebGL, screenSpace, context);
9543
+ const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
9544
+ drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
8797
9545
  }
8798
- for(let i=4; i--;)
9546
+ for (let i=4; i--;)
8799
9547
  {
8800
9548
  // corners
8801
9549
  const flipX = i>1;
8802
9550
  const flipY = i && i<3;
8803
9551
  const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-1:1));
8804
9552
  const cornerTile = centerTile.offset(startTile.size.multiply(vec2(flipX?-1:1,flipY?flip:-flip)));
8805
- drawTile(pos.add(cornerPos), cornerSize, cornerTile, color, 0, false, additiveColor, useWebGL, screenSpace, context);
9553
+ drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
8806
9554
  }
8807
9555
  }
8808
9556
 
@@ -8811,28 +9559,30 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
8811
9559
  * @param {Vector2} pos - Screen space position
8812
9560
  * @param {Vector2} size - Screen space size
8813
9561
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
8814
- * @param {number} [borderSize=1] - Width of the border sections
8815
- * @param {number} [extraSpace=.01] - Extra spacing adjustment
9562
+ * @param {number} [borderSize] - Width of the border sections
9563
+ * @param {number} [extraSpace] - Extra spacing adjustment
9564
+ * @param {number} [angle] - Angle to rotate by
8816
9565
  * @memberof DrawUtilities */
8817
- function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=1)
9566
+ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
8818
9567
  {
8819
- drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, false, true, overlayContext);
9568
+ drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
8820
9569
  }
8821
9570
 
8822
9571
  /** Draw a scalable three-slice UI element in world space
8823
- * This function can apply color and additive color if webgl is enabled
9572
+ * This function can apply color and additive color if WebGL is enabled
8824
9573
  * @param {Vector2} pos - World space position
8825
9574
  * @param {Vector2} size - World space size
8826
9575
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
8827
9576
  * @param {Color} [color] - Color to modulate with
8828
- * @param {number} [borderSize=1] - Width of the border sections
9577
+ * @param {number} [borderSize] - Width of the border sections
8829
9578
  * @param {Color} [additiveColor] - Additive color
8830
- * @param {number} [extraSpace=.01] - Extra spacing adjustment
9579
+ * @param {number} [extraSpace] - Extra spacing adjustment
9580
+ * @param {number} [angle] - Angle to rotate by
8831
9581
  * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
8832
9582
  * @param {boolean} [screenSpace] - Use screen space coordinates
8833
9583
  * @param {CanvasRenderingContext2D} [context] - Canvas context to use
8834
9584
  * @memberof DrawUtilities */
8835
- function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.01, useWebGL=glEnable, screenSpace, context)
9585
+ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
8836
9586
  {
8837
9587
  // setup three slice tiles
8838
9588
  const cornerTile = startTile.frame(0);
@@ -8842,25 +9592,26 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
8842
9592
  const cornerSize = vec2(borderSize);
8843
9593
  const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
8844
9594
  const flip = screenSpace ? -1 : 1;
9595
+ const rotateAngle = screenSpace ? -angle : angle;
8845
9596
 
8846
9597
  // center
8847
- drawTile(pos, centerSize, centerTile, color, 0, false, additiveColor, useWebGL, screenSpace, context);
8848
- for(let i=4; i--;)
9598
+ drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9599
+ for (let i=4; i--;)
8849
9600
  {
8850
9601
  // sides
8851
- const angle = i*PI/2;
9602
+ const a = angle + i*PI/2;
8852
9603
  const horizontal = i%2;
8853
- const sidePos = cornerOffset.multiply(vec2(horizontal?i==1?1:-1:0, horizontal?0:i?-flip:flip));
9604
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
8854
9605
  const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
8855
- drawTile(pos.add(sidePos), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9606
+ drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
8856
9607
  }
8857
- for(let i=4; i--;)
9608
+ for (let i=4; i--;)
8858
9609
  {
8859
9610
  // corners
8860
- const angle = i*PI/2;
9611
+ const a = angle + i*PI/2;
8861
9612
  const flipX = !i || i>2;
8862
9613
  const flipY = i>1;
8863
9614
  const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip));
8864
- drawTile(pos.add(cornerPos), cornerSize, cornerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
9615
+ drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
8865
9616
  }
8866
9617
  }