littlejsengine 1.15.9 → 1.16.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 (54) hide show
  1. package/dist/littlejs.d.ts +154 -158
  2. package/dist/littlejs.esm.js +573 -570
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +553 -553
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +392 -396
  7. package/examples/box2d/game.js +1 -1
  8. package/examples/box2d/gameObjects.js +5 -5
  9. package/examples/breakout/game.js +3 -3
  10. package/examples/electron/game.js +1 -2
  11. package/examples/electron/index.html +2 -2
  12. package/examples/htmlMenu/game.js +0 -1
  13. package/examples/index.html +1 -1
  14. package/examples/logo.png +0 -0
  15. package/examples/logo2.png +0 -0
  16. package/examples/module/game.js +1 -2
  17. package/examples/particles/index.html +1 -1
  18. package/examples/platformer/game.js +4 -4
  19. package/examples/platformer/gameEffects.js +1 -1
  20. package/examples/puzzle/game.js +2 -2
  21. package/examples/shorts/base.html +4 -3
  22. package/examples/shorts/box2dPool.js +2 -2
  23. package/examples/shorts/empty.js +1 -1
  24. package/examples/shorts/{systemFont.js → fontImage.js} +3 -3
  25. package/examples/shorts/fps.js +1 -1
  26. package/examples/shorts/helloWorld.js +2 -2
  27. package/examples/shorts/nineSlice.js +2 -2
  28. package/examples/shorts/slidingPuzzle.js +1 -1
  29. package/examples/starter/game.js +1 -2
  30. package/examples/starter/index.html +3 -2
  31. package/examples/stress/index.html +1 -1
  32. package/examples/typescript/game.js +1 -2
  33. package/examples/typescript/game.ts +1 -2
  34. package/package.json +1 -1
  35. package/plugins/box2d.js +8 -8
  36. package/plugins/drawUtilities.js +6 -6
  37. package/plugins/postProcess.js +13 -20
  38. package/plugins/uiSystem.js +16 -4
  39. package/reference.md +9 -11
  40. package/src/engine.js +40 -54
  41. package/src/engineAudio.js +5 -7
  42. package/src/engineBuild.js +1 -0
  43. package/src/engineDebug.js +163 -161
  44. package/src/engineDraw.js +108 -130
  45. package/src/engineExport.js +20 -17
  46. package/src/engineInput.js +4 -7
  47. package/src/engineMath.js +1201 -0
  48. package/src/engineMedals.js +7 -7
  49. package/src/engineObject.js +5 -4
  50. package/src/engineRelease.js +2 -4
  51. package/src/engineSettings.js +22 -32
  52. package/src/engineTileLayer.js +9 -10
  53. package/src/engineUtilities.js +34 -1187
  54. package/src/engineWebGL.js +13 -15
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.15.9';
36
+ const engineVersion = '1.16.2';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -180,8 +180,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
180
180
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
181
181
 
182
182
  // disable smoothing for pixel art
183
- overlayContext.imageSmoothingEnabled =
184
- mainContext.imageSmoothingEnabled = !tilesPixelated;
183
+ mainContext.imageSmoothingEnabled = !tilesPixelated;
185
184
 
186
185
  // setup gl rendering if enabled
187
186
  glPreRender();
@@ -193,7 +192,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
193
192
  // update time keeping
194
193
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
195
194
  frameTimeLastMS = frameTimeMS;
196
- if (debug || showWatermark)
195
+ if (debug || debugWatermark)
197
196
  averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
198
197
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
199
198
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
@@ -221,6 +220,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
221
220
  debugUpdate();
222
221
  gameUpdatePost();
223
222
  inputUpdatePost();
223
+ if (debugVideoCaptureIsActive())
224
+ renderFrame();
224
225
  }
225
226
  else
226
227
  {
@@ -271,33 +272,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
271
272
  if (!wasUpdated)
272
273
  updateCanvas();
273
274
 
274
- // render sort then render while removing destroyed objects
275
+ // render the game and objects
275
276
  enginePreRender();
276
277
  gameRender();
277
278
  engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
278
279
  for (const o of engineObjects)
279
280
  o.destroyed || o.render();
281
+
282
+ // post rendering
280
283
  gameRenderPost();
281
284
  pluginList.forEach(plugin=>plugin.render?.());
282
285
  inputRender();
283
286
  debugRender();
284
287
  glFlush();
285
- debugVideoCaptureUpdate();
286
-
287
- if (showWatermark && !debugVideoCaptureIsActive())
288
- {
289
- // update fps display
290
- overlayContext.textAlign = 'right';
291
- overlayContext.textBaseline = 'top';
292
- overlayContext.font = '1em monospace';
293
- overlayContext.fillStyle = '#000';
294
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
295
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
296
- + (glEnable ? ' GL' : ' 2D') ;
297
- overlayContext.fillText(text, mainCanvas.width-3, 3);
298
- overlayContext.fillStyle = '#fff';
299
- overlayContext.fillText(text, mainCanvas.width-2, 2);
300
- }
288
+ debugRenderPost();
301
289
  drawCount = 0;
302
290
  }
303
291
  }
@@ -316,8 +304,8 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
316
304
  const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
317
305
  const w = innerAspect < fixedAspect ? '100%' : '';
318
306
  const h = innerAspect < fixedAspect ? '' : '100%';
319
- overlayCanvas.style.width = mainCanvas.style.width = w;
320
- overlayCanvas.style.height = mainCanvas.style.height = h;
307
+ mainCanvas.style.width = w;
308
+ mainCanvas.style.height = h;
321
309
  if (glCanvas)
322
310
  {
323
311
  glCanvas.style.width = w;
@@ -332,6 +320,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
332
320
 
333
321
  // responsive aspect ratio with native resolution
334
322
  const innerAspect = innerWidth / innerHeight;
323
+ ASSERT(canvasMinAspect <= canvasMaxAspect);
335
324
  if (canvasMaxAspect && innerAspect > canvasMaxAspect)
336
325
  {
337
326
  // full height
@@ -346,12 +335,12 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
346
335
  }
347
336
  }
348
337
 
349
- // clear main and overlay canvas and set size
350
- overlayCanvas.width = mainCanvas.width = mainCanvasSize.x;
351
- overlayCanvas.height = mainCanvas.height = mainCanvasSize.y;
338
+ // clear main canvas and set size
339
+ mainCanvas.width = mainCanvasSize.x;
340
+ mainCanvas.height = mainCanvasSize.y;
352
341
 
353
342
  // apply the clear color to main canvas
354
- if (canvasClearColor.a > 0)
343
+ if (canvasClearColor.a > 0 && !glEnable)
355
344
  {
356
345
  mainContext.fillStyle = canvasClearColor.toString();
357
346
  mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
@@ -359,19 +348,15 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
359
348
  }
360
349
 
361
350
  // set default line join and cap
362
- const lineJoin = 'round', lineCap = 'round';
363
- mainContext.lineJoin = overlayContext.lineJoin = lineJoin;
364
- mainContext.lineCap = overlayContext.lineCap = lineCap;
351
+ mainContext.lineJoin = 'round';
352
+ mainContext.lineCap = 'round';
365
353
  }
354
+
355
+ // skip setup if headless
356
+ if (headlessMode) return startEngine();
366
357
 
367
- // wait for gameInit to load
368
- async function startEngine()
369
- {
370
- await gameInit();
371
- engineUpdate();
372
- }
373
- if (headlessMode)
374
- return startEngine();
358
+ // setup webgl
359
+ glInit(rootElement);
375
360
 
376
361
  // setup html
377
362
  const styleRoot =
@@ -383,36 +368,30 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
383
368
  'touch-action:none;' + // prevent mobile pinch to resize
384
369
  '-webkit-touch-callout:none'; // compatibility for ios
385
370
  rootElement.style.cssText = styleRoot;
386
- drawCanvas = mainCanvas = document.createElement('canvas');
387
- rootElement.appendChild(mainCanvas);
371
+ mainCanvas = rootElement.appendChild(document.createElement('canvas'));
388
372
  drawContext = mainContext = mainCanvas.getContext('2d');
389
373
 
390
374
  // init stuff and start engine
391
375
  inputInit();
392
376
  audioInit();
393
377
  debugInit();
394
- glInit();
395
-
396
- // create overlay canvas for hud to appear above gl canvas
397
- overlayCanvas = document.createElement('canvas')
398
- rootElement.appendChild(overlayCanvas);
399
- overlayContext = overlayCanvas.getContext('2d');
400
378
 
401
379
  // setup canvases
402
380
  // transform way is still more reliable then flexbox or grid
403
381
  const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
404
382
  'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
405
- mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
383
+ mainCanvas.style.cssText = styleCanvas;
406
384
  if (glCanvas)
407
385
  glCanvas.style.cssText = styleCanvas;
408
386
  setCanvasPixelated(canvasPixelated);
409
- setOverlayCanvasPixelated(overlayCanvasPixelated);
410
387
  updateCanvas();
411
388
  glPreRender();
412
389
 
413
- // create offscreen canvas for image processing
414
- workCanvas = new OffscreenCanvas(256, 256);
415
- workContext = workCanvas.getContext('2d', { willReadFrequently: true });
390
+ // create offscreen canvases for image processing
391
+ workCanvas = new OffscreenCanvas(64, 64);
392
+ workContext = workCanvas.getContext('2d');
393
+ workReadCanvas = new OffscreenCanvas(64, 64);
394
+ workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
416
395
 
417
396
  // create promises for loading images
418
397
  const promises = imageSources.map((src, textureIndex)=>
@@ -464,13 +443,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
464
443
  await Promise.all(promises);
465
444
  return startEngine();
466
445
 
446
+ async function startEngine()
447
+ {
448
+ // wait for gameInit to load
449
+ await gameInit();
450
+ engineUpdate();
451
+ }
452
+
467
453
  ///////////////////////////////////////////////////////////////////////////
468
454
  // LittleJS Splash Screen
469
455
  function drawEngineSplashScreen(t)
470
456
  {
471
- const x = overlayContext;
472
- const w = overlayCanvas.width = innerWidth;
473
- const h = overlayCanvas.height = innerHeight;
457
+ const x = mainContext;
458
+ const w = mainCanvas.width = innerWidth;
459
+ const h = mainCanvas.height = innerHeight;
474
460
 
475
461
  {
476
462
  // background
@@ -748,7 +734,7 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
748
734
  * - Debug functionality is disabled to reduce size and increase performance
749
735
  */
750
736
 
751
- let showWatermark = 0;
737
+ let debugWatermark = 0;
752
738
  let debugKey = '';
753
739
  const debug = 0;
754
740
  const debugOverlay = 0;
@@ -764,6 +750,7 @@ function LOG (){}
764
750
  function debugInit (){}
765
751
  function debugUpdate (){}
766
752
  function debugRender (){}
753
+ function debugRenderPost (){}
767
754
  function debugRect (){}
768
755
  function debugPoly (){}
769
756
  function debugCircle (){}
@@ -773,9 +760,6 @@ function debugOverlap (){}
773
760
  function debugText (){}
774
761
  function debugClear (){}
775
762
  function debugScreenshot (){}
776
- function debugSaveCanvas (){}
777
- function debugSaveText (){}
778
- function debugSaveDataURL(){}
779
763
  function debugShowErrors(){}
780
764
  function debugVideoCaptureIsActive(){ return false; }
781
765
  function debugVideoCaptureStart (){}
@@ -789,99 +773,99 @@ function debugProtectConstant(o){ return o; }
789
773
  * - Color - holds a rgba color with some math functions
790
774
  * - Timer - tracks time automatically
791
775
  * - RandomGenerator - seeded random number generator
792
- * @namespace Utilities
776
+ * @namespace Math
793
777
  */
794
778
 
795
779
  /** The value of PI
796
780
  * @type {number}
797
781
  * @default Math.PI
798
- * @memberof Utilities */
782
+ * @memberof Math */
799
783
  const PI = Math.PI;
800
784
 
801
785
  /** Returns absolute value of value passed in
802
786
  * @param {number} value
803
787
  * @return {number}
804
- * @memberof Utilities */
788
+ * @memberof Math */
805
789
  const abs = Math.abs;
806
790
 
807
791
  /** Returns floored value of value passed in
808
792
  * @param {number} value
809
793
  * @return {number}
810
- * @memberof Utilities */
794
+ * @memberof Math */
811
795
  const floor = Math.floor;
812
796
 
813
797
  /** Returns ceiled value of value passed in
814
798
  * @param {number} value
815
799
  * @return {number}
816
- * @memberof Utilities */
800
+ * @memberof Math */
817
801
  const ceil = Math.ceil;
818
802
 
819
803
  /** Returns rounded value passed in
820
804
  * @param {number} value
821
805
  * @return {number}
822
- * @memberof Utilities */
806
+ * @memberof Math */
823
807
  const round = Math.round;
824
808
 
825
809
  /** Returns lowest value passed in
826
810
  * @param {...number} values
827
811
  * @return {number}
828
- * @memberof Utilities */
812
+ * @memberof Math */
829
813
  const min = Math.min;
830
814
 
831
815
  /** Returns highest value passed in
832
816
  * @param {...number} values
833
817
  * @return {number}
834
- * @memberof Utilities */
818
+ * @memberof Math */
835
819
  const max = Math.max;
836
820
 
837
821
  /** Returns the sign of value passed in
838
822
  * @param {number} value
839
823
  * @return {number}
840
- * @memberof Utilities */
824
+ * @memberof Math */
841
825
  const sign = Math.sign;
842
826
 
843
827
  /** Returns hypotenuse of values passed in
844
828
  * @param {...number} values
845
829
  * @return {number}
846
- * @memberof Utilities */
830
+ * @memberof Math */
847
831
  const hypot = Math.hypot;
848
832
 
849
833
  /** Returns log2 of value passed in
850
834
  * @param {number} value
851
835
  * @return {number}
852
- * @memberof Utilities */
836
+ * @memberof Math */
853
837
  const log2 = Math.log2;
854
838
 
855
839
  /** Returns sin of value passed in
856
840
  * @param {number} value
857
841
  * @return {number}
858
- * @memberof Utilities */
842
+ * @memberof Math */
859
843
  const sin = Math.sin;
860
844
 
861
845
  /** Returns cos of value passed in
862
846
  * @param {number} value
863
847
  * @return {number}
864
- * @memberof Utilities */
848
+ * @memberof Math */
865
849
  const cos = Math.cos;
866
850
 
867
851
  /** Returns tan of value passed in
868
852
  * @param {number} value
869
853
  * @return {number}
870
- * @memberof Utilities */
854
+ * @memberof Math */
871
855
  const tan = Math.tan;
872
856
 
873
857
  /** Returns atan2 of values passed in
874
858
  * @param {number} y
875
859
  * @param {number} x
876
860
  * @return {number}
877
- * @memberof Utilities */
861
+ * @memberof Math */
878
862
  const atan2 = Math.atan2;
879
863
 
880
864
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
881
865
  * @param {number} dividend
882
866
  * @param {number} [divisor]
883
867
  * @return {number}
884
- * @memberof Utilities */
868
+ * @memberof Math */
885
869
  function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
886
870
 
887
871
  /** Clamps the value between max and min
@@ -889,7 +873,7 @@ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % di
889
873
  * @param {number} [min]
890
874
  * @param {number} [max]
891
875
  * @return {number}
892
- * @memberof Utilities */
876
+ * @memberof Math */
893
877
  function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
894
878
 
895
879
  /** Returns what percentage the value is between valueA and valueB
@@ -897,7 +881,7 @@ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? m
897
881
  * @param {number} valueA
898
882
  * @param {number} valueB
899
883
  * @return {number}
900
- * @memberof Utilities */
884
+ * @memberof Math */
901
885
  function percent(value, valueA, valueB)
902
886
  { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
903
887
 
@@ -906,13 +890,9 @@ function percent(value, valueA, valueB)
906
890
  * @param {number} valueB
907
891
  * @param {number} percent
908
892
  * @return {number}
909
- * @memberof Utilities */
893
+ * @memberof Math */
910
894
  function lerp(valueA, valueB, percent)
911
- {
912
- if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
913
- console.warn('lerp() parameter order changed! use lerp(start, end, p)');
914
- return valueA + clamp(percent) * (valueB-valueA);
915
- }
895
+ { return valueA + clamp(percent) * (valueB-valueA); }
916
896
 
917
897
  /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
918
898
  * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
@@ -922,7 +902,7 @@ function lerp(valueA, valueB, percent)
922
902
  * @param {number} lerpA
923
903
  * @param {number} lerpB
924
904
  * @return {number}
925
- * @memberof Utilities */
905
+ * @memberof Math */
926
906
  function percentLerp(value, percentA, percentB, lerpA, lerpB)
927
907
  { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
928
908
 
@@ -931,7 +911,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
931
911
  * @param {number} valueB
932
912
  * @param {number} [wrapSize]
933
913
  * @return {number}
934
- * @memberof Utilities */
914
+ * @memberof Math */
935
915
  function distanceWrap(valueA, valueB, wrapSize=1)
936
916
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
937
917
 
@@ -941,19 +921,15 @@ function distanceWrap(valueA, valueB, wrapSize=1)
941
921
  * @param {number} percent
942
922
  * @param {number} [wrapSize]
943
923
  * @return {number}
944
- * @memberof Utilities */
924
+ * @memberof Math */
945
925
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
946
- {
947
- if (valueA >= 0 && valueA <= 1 && ((valueB < 0 || valueB > 1) && (percent < 0 || percent > 1)))
948
- console.warn('lerpWrap() parameter order changed! use lerpWrap(start, end, p)');
949
- return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize);
950
- }
926
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
951
927
 
952
928
  /** Returns signed wrapped distance between the two angles passed in
953
929
  * @param {number} angleA
954
930
  * @param {number} angleB
955
931
  * @return {number}
956
- * @memberof Utilities */
932
+ * @memberof Math */
957
933
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
958
934
 
959
935
  /** Linearly interpolates between the angles passed in with wrapping
@@ -961,25 +937,25 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
961
937
  * @param {number} angleB
962
938
  * @param {number} percent
963
939
  * @return {number}
964
- * @memberof Utilities */
940
+ * @memberof Math */
965
941
  function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
966
942
 
967
943
  /** Applies smoothstep function to the percentage value
968
944
  * @param {number} percent
969
945
  * @return {number}
970
- * @memberof Utilities */
946
+ * @memberof Math */
971
947
  function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
972
948
 
973
949
  /** Checks if the value passed in is a power of two
974
950
  * @param {number} value
975
951
  * @return {boolean}
976
- * @memberof Utilities */
952
+ * @memberof Math */
977
953
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
978
954
 
979
955
  /** Returns the nearest power of two not less than the value
980
956
  * @param {number} value
981
957
  * @return {number}
982
- * @memberof Utilities */
958
+ * @memberof Math */
983
959
  function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
984
960
 
985
961
  /** Returns true if two axis aligned bounding boxes are overlapping
@@ -989,7 +965,7 @@ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
989
965
  * @param {Vector2} posB - Center of box B
990
966
  * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
991
967
  * @return {boolean} - True if overlapping
992
- * @memberof Utilities */
968
+ * @memberof Math */
993
969
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
994
970
  {
995
971
  const dx = (posA.x - posB.x)*2;
@@ -1005,7 +981,7 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
1005
981
  * @param {Vector2} pos - Center of box
1006
982
  * @param {Vector2} size - Size of box
1007
983
  * @return {boolean} - True if intersecting
1008
- * @memberof Utilities */
984
+ * @memberof Math */
1009
985
  function isIntersecting(start, end, pos, size)
1010
986
  {
1011
987
  // Liang-Barsky algorithm
@@ -1046,52 +1022,29 @@ function isIntersecting(start, end, pos, size)
1046
1022
  * @param {number} [t=time] - Value to use for time of the wave
1047
1023
  * @param {number} [offset] - Value to use for time offset of the wave
1048
1024
  * @return {number} - Value waving between 0 and amplitude
1049
- * @memberof Utilities */
1025
+ * @memberof Math */
1050
1026
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1051
1027
  { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
1052
1028
 
1053
- /** Formats seconds to mm:ss style for display purposes
1054
- * @param {number} t - time in seconds
1055
- * @return {string}
1056
- * @memberof Utilities */
1057
- function formatTime(t)
1058
- {
1059
- const sign = t < 0 ? '-' : '';
1060
- t = abs(t)|0;
1061
- return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1062
- }
1063
-
1064
- /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1065
- * @param {string} url - URL of JSON file
1066
- * @return {Promise<object>}
1067
- * @memberof Utilities */
1068
- async function fetchJSON(url)
1069
- {
1070
- const response = await fetch(url);
1071
- if (!response.ok)
1072
- throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1073
- return response.json();
1074
- }
1075
-
1076
1029
  /**
1077
1030
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
1078
1031
  * @param {any} n
1079
1032
  * @return {boolean}
1080
- * @memberof Utilities */
1033
+ * @memberof Math */
1081
1034
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1082
1035
 
1083
1036
  /**
1084
1037
  * Check if object is a valid string or can be converted to one
1085
1038
  * @param {any} s
1086
1039
  * @return {boolean}
1087
- * @memberof Utilities */
1040
+ * @memberof Math */
1088
1041
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1089
1042
 
1090
1043
  /**
1091
1044
  * Check if object is an array
1092
1045
  * @param {any} a
1093
1046
  * @return {boolean}
1094
- * @memberof Utilities */
1047
+ * @memberof Math */
1095
1048
  function isArray(a) { return Array.isArray(a); }
1096
1049
 
1097
1050
  /**
@@ -1107,7 +1060,7 @@ function isArray(a) { return Array.isArray(a); }
1107
1060
  * @param {LineTestFunction} testFunction - Check if colliding
1108
1061
  * @param {Vector2} [normal] - Optional vector to store the normal
1109
1062
  * @return {Vector2|undefined} - Position of the collision or undefined if none found
1110
- * @memberof Utilities */
1063
+ * @memberof Math */
1111
1064
  function lineTest(posStart, posEnd, testFunction, normal)
1112
1065
  {
1113
1066
  ASSERT(isVector2(posStart), 'posStart must be a vec2');
@@ -1119,8 +1072,7 @@ function lineTest(posStart, posEnd, testFunction, normal)
1119
1072
  const dx = posEnd.x - posStart.x;
1120
1073
  const dy = posEnd.y - posStart.y;
1121
1074
  const totalLength = hypot(dx, dy);
1122
- if (!totalLength)
1123
- return;
1075
+ if (!totalLength) return;
1124
1076
 
1125
1077
  // current integer cell we are in
1126
1078
  const pos = posStart.floor();
@@ -1354,14 +1306,14 @@ class RandomGenerator
1354
1306
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
1355
1307
  * a = vec2(5); // set a to (5, 5)
1356
1308
  * b = vec2(); // set b to (0, 0)
1357
- * @memberof Utilities */
1309
+ * @memberof Math */
1358
1310
  function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1359
1311
 
1360
1312
  /**
1361
1313
  * Check if object is a valid Vector2
1362
1314
  * @param {any} v
1363
1315
  * @return {boolean}
1364
- * @memberof Utilities */
1316
+ * @memberof Math */
1365
1317
  function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
1366
1318
 
1367
1319
  // vector2 asserts
@@ -1611,7 +1563,7 @@ class Vector2
1611
1563
  * @param {number} [b=1] - blue
1612
1564
  * @param {number} [a=1] - alpha
1613
1565
  * @return {Color}
1614
- * @memberof Utilities
1566
+ * @memberof Math
1615
1567
  */
1616
1568
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1617
1569
 
@@ -1622,14 +1574,14 @@ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1622
1574
  * @param {number} [l=1] - lightness
1623
1575
  * @param {number} [a=1] - alpha
1624
1576
  * @return {Color}
1625
- * @memberof Utilities */
1577
+ * @memberof Math */
1626
1578
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
1627
1579
 
1628
1580
  /**
1629
1581
  * Check if object is a valid Color
1630
1582
  * @param {any} c
1631
1583
  * @return {boolean}
1632
- * @memberof Utilities */
1584
+ * @memberof Math */
1633
1585
  function isColor(c) { return c instanceof Color && c.isValid(); }
1634
1586
 
1635
1587
  // color asserts
@@ -1810,7 +1762,7 @@ class Color
1810
1762
  toString(useAlpha = true)
1811
1763
  {
1812
1764
  if (debug && !this.isValid())
1813
- return `#000`;
1765
+ return '#000';
1814
1766
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
1815
1767
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1816
1768
  }
@@ -1867,67 +1819,67 @@ class Color
1867
1819
 
1868
1820
  /** Color - White #ffffff
1869
1821
  * @type {Color}
1870
- * @memberof Utilities */
1822
+ * @memberof Math */
1871
1823
  const WHITE = debugProtectConstant(rgb());
1872
1824
 
1873
1825
  /** Color - Clear White #757474ff with 0 alpha
1874
1826
  * @type {Color}
1875
- * @memberof Utilities */
1827
+ * @memberof Math */
1876
1828
  const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
1877
1829
 
1878
1830
  /** Color - Black #000000
1879
1831
  * @type {Color}
1880
- * @memberof Utilities */
1832
+ * @memberof Math */
1881
1833
  const BLACK = debugProtectConstant(rgb(0,0,0));
1882
1834
 
1883
1835
  /** Color - Clear Black #000000 with 0 alpha
1884
1836
  * @type {Color}
1885
- * @memberof Utilities */
1837
+ * @memberof Math */
1886
1838
  const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
1887
1839
 
1888
1840
  /** Color - Gray #808080
1889
1841
  * @type {Color}
1890
- * @memberof Utilities */
1842
+ * @memberof Math */
1891
1843
  const GRAY = debugProtectConstant(rgb(.5,.5,.5));
1892
1844
 
1893
1845
  /** Color - Red #ff0000
1894
1846
  * @type {Color}
1895
- * @memberof Utilities */
1847
+ * @memberof Math */
1896
1848
  const RED = debugProtectConstant(rgb(1,0,0));
1897
1849
 
1898
1850
  /** Color - Orange #ff8000
1899
1851
  * @type {Color}
1900
- * @memberof Utilities */
1852
+ * @memberof Math */
1901
1853
  const ORANGE = debugProtectConstant(rgb(1,.5,0));
1902
1854
 
1903
1855
  /** Color - Yellow #ffff00
1904
1856
  * @type {Color}
1905
- * @memberof Utilities */
1857
+ * @memberof Math */
1906
1858
  const YELLOW = debugProtectConstant(rgb(1,1,0));
1907
1859
 
1908
1860
  /** Color - Green #00ff00
1909
1861
  * @type {Color}
1910
- * @memberof Utilities */
1862
+ * @memberof Math */
1911
1863
  const GREEN = debugProtectConstant(rgb(0,1,0));
1912
1864
 
1913
1865
  /** Color - Cyan #00ffff
1914
1866
  * @type {Color}
1915
- * @memberof Utilities */
1867
+ * @memberof Math */
1916
1868
  const CYAN = debugProtectConstant(rgb(0,1,1));
1917
1869
 
1918
1870
  /** Color - Blue #0000ff
1919
1871
  * @type {Color}
1920
- * @memberof Utilities */
1872
+ * @memberof Math */
1921
1873
  const BLUE = debugProtectConstant(rgb(0,0,1));
1922
1874
 
1923
1875
  /** Color - Purple #8000ff
1924
1876
  * @type {Color}
1925
- * @memberof Utilities */
1877
+ * @memberof Math */
1926
1878
  const PURPLE = debugProtectConstant(rgb(.5,0,1));
1927
1879
 
1928
1880
  /** Color - Magenta #ff00ff
1929
1881
  * @type {Color}
1930
- * @memberof Utilities */
1882
+ * @memberof Math */
1931
1883
  const MAGENTA = debugProtectConstant(rgb(1,0,1));
1932
1884
 
1933
1885
  ///////////////////////////////////////////////////////////////////////////////
@@ -2013,6 +1965,84 @@ class Timer
2013
1965
  * @return {number} */
2014
1966
  valueOf() { return this.get(); }
2015
1967
  }
1968
+ /**
1969
+ * LittleJS Utility Classes and Functions
1970
+ * - General purpose math library
1971
+ * - Vector2 - fast, simple, easy 2D vector class
1972
+ * - Color - holds a rgba color with some math functions
1973
+ * - Timer - tracks time automatically
1974
+ * - RandomGenerator - seeded random number generator
1975
+ * @namespace Utilities
1976
+ */
1977
+
1978
+ /** Formats seconds to mm:ss style for display purposes
1979
+ * @param {number} t - time in seconds
1980
+ * @return {string}
1981
+ * @memberof Utilities */
1982
+ function formatTime(t)
1983
+ {
1984
+ const sign = t < 0 ? '-' : '';
1985
+ t = abs(t)|0;
1986
+ return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1987
+ }
1988
+
1989
+ /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1990
+ * @param {string} url - URL of JSON file
1991
+ * @return {Promise<object>}
1992
+ * @memberof Utilities */
1993
+ async function fetchJSON(url)
1994
+ {
1995
+ const response = await fetch(url);
1996
+ if (!response.ok)
1997
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1998
+ return response.json();
1999
+ }
2000
+
2001
+ ///////////////////////////////////////////////////////////////////////////////
2002
+
2003
+ /** Save a text file to disk
2004
+ * @param {string} text
2005
+ * @param {string} [filename]
2006
+ * @param {string} [type]
2007
+ * @memberof Utilities */
2008
+ function saveText(text, filename='text', type='text/plain')
2009
+ { saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
2010
+
2011
+ /** Save a canvas to disk
2012
+ * @param {HTMLCanvasElement|OffscreenCanvas} canvas
2013
+ * @param {string} [filename]
2014
+ * @param {string} [type]
2015
+ * @memberof Utilities */
2016
+ function saveCanvas(canvas, filename='screenshot', type='image/png')
2017
+ {
2018
+ if (canvas instanceof OffscreenCanvas)
2019
+ {
2020
+ // copy to temporary canvas and save
2021
+ const saveCanvas = document.createElement('canvas');
2022
+ saveCanvas.width = canvas.width;
2023
+ saveCanvas.height = canvas.height;
2024
+ saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
2025
+ saveDataURL(saveCanvas.toDataURL(type), filename);
2026
+ }
2027
+ else
2028
+ saveDataURL(canvas.toDataURL(type), filename);
2029
+ }
2030
+
2031
+ /** Save a data url to disk
2032
+ * @param {string} url
2033
+ * @param {string} [filename]
2034
+ * @param {number} [revokeTime] - how long before revoking the url
2035
+ * @memberof Utilities */
2036
+ function saveDataURL(url, filename='download', revokeTime)
2037
+ {
2038
+ // create link for saving screenshots
2039
+ const link = document.createElement('a');
2040
+ link.download = filename;
2041
+ link.href = url;
2042
+ link.click();
2043
+ if (revokeTime !== undefined)
2044
+ setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
2045
+ }
2016
2046
  /**
2017
2047
  * LittleJS Engine Settings
2018
2048
  * - All settings for the engine are here
@@ -2050,7 +2080,7 @@ let cameraScale = 32;
2050
2080
  * @memberof Settings */
2051
2081
  let canvasColorTiles = true;
2052
2082
 
2053
- /** Color to clear the canvas to before render
2083
+ /** Color to clear the canvas to before render, does not clear if alpha is 0
2054
2084
  * @type {Color}
2055
2085
  * @memberof Draw */
2056
2086
  let canvasClearColor = CLEAR_BLACK;
@@ -2087,15 +2117,7 @@ let canvasFixedSize = vec2();
2087
2117
  * @type {boolean}
2088
2118
  * @default
2089
2119
  * @memberof Settings */
2090
- let canvasPixelated = true;
2091
-
2092
- /** Use nearest canvas scaling for more pixelated look
2093
- * - If enabled sets css image-rendering:pixelated
2094
- * - This defaults to false because text looks better with smoothing
2095
- * @type {boolean}
2096
- * @default
2097
- * @memberof Settings */
2098
- let overlayCanvasPixelated = false;
2120
+ let canvasPixelated = false;
2099
2121
 
2100
2122
  /** Disables texture filtering for crisper pixel art
2101
2123
  * @type {boolean}
@@ -2143,13 +2165,19 @@ let glCircleSides = 32;
2143
2165
  * @type {Vector2}
2144
2166
  * @default Vector2(16,16)
2145
2167
  * @memberof Settings */
2146
- let tileSizeDefault = vec2(16);
2168
+ let tileDefaultSize = vec2(16);
2169
+
2170
+ /** Default padding pixels around tiles
2171
+ * @type {number}
2172
+ * @default
2173
+ * @memberof Settings */
2174
+ let tileDefaultPadding = 0;
2147
2175
 
2148
- /** How many pixels smaller to draw tiles to prevent bleeding from neighbors
2176
+ /** Default amount of pixels smaller to draw tiles to prevent neighbor bleeding
2149
2177
  * @type {number}
2150
2178
  * @default
2151
2179
  * @memberof Settings */
2152
- let tileFixBleedScale = 0;
2180
+ let tileDefaultBleed = 0;
2153
2181
 
2154
2182
  ///////////////////////////////////////////////////////////////////////////////
2155
2183
  // Object settings
@@ -2361,7 +2389,7 @@ function setCameraScale(scale) { cameraScale = scale; }
2361
2389
  * @memberof Settings */
2362
2390
  function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2363
2391
 
2364
- /** Set color to clear the canvas to before render
2392
+ /** Set color to clear the canvas to before render, does not clear if alpha is 0
2365
2393
  * @param {Color} color
2366
2394
  * @memberof Settings */
2367
2395
  function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
@@ -2387,7 +2415,6 @@ function setCanvasMaxAspect(aspect) { canvasMaxAspect = aspect; }
2387
2415
  function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
2388
2416
 
2389
2417
  /** Use nearest scaling algorithm for canvas for more pixelated look
2390
- * - If enabled sets css image-rendering:pixelated
2391
2418
  * @param {boolean} pixelated
2392
2419
  * @memberof Settings */
2393
2420
  function setCanvasPixelated(pixelated)
@@ -2399,18 +2426,6 @@ function setCanvasPixelated(pixelated)
2399
2426
  glCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2400
2427
  }
2401
2428
 
2402
- /** Use nearest scaling algorithm for canvas for more pixelated look
2403
- * - If enabled sets css image-rendering:pixelated
2404
- * - This defaults to false because text looks better with smoothing
2405
- * @param {boolean} pixelated
2406
- * @memberof Settings */
2407
- function setOverlayCanvasPixelated(pixelated)
2408
- {
2409
- overlayCanvasPixelated = pixelated;
2410
- if (overlayCanvas)
2411
- overlayCanvas.style.imageRendering = pixelated ? 'pixelated' : '';
2412
- }
2413
-
2414
2429
  /** Disables texture filtering for crisper pixel art
2415
2430
  * @param {boolean} pixelated
2416
2431
  * @memberof Settings */
@@ -2454,12 +2469,17 @@ function setGLCircleSides(sides) { glCircleSides = sides; }
2454
2469
  /** Set default size of tiles in pixels
2455
2470
  * @param {Vector2} size
2456
2471
  * @memberof Settings */
2457
- function setTileSizeDefault(size) { tileSizeDefault = size.copy(); }
2472
+ function setTileDefaultSize(size) { tileDefaultSize = size.copy(); }
2458
2473
 
2459
- /** Set to prevent tile bleeding from neighbors in pixels
2460
- * @param {number} scale
2474
+ /** Default padding pixels around tiles
2475
+ * @param {number} padding
2476
+ * @memberof Settings */
2477
+ function setTileDefaultPadding(padding) { tileDefaultPadding = padding; }
2478
+
2479
+ /** Default amount of pixels smaller to draw tiles to prevent neighbor bleeding
2480
+ * @param {number} bleed
2461
2481
  * @memberof Settings */
2462
- function setTileFixBleedScale(scale) { tileFixBleedScale = scale; }
2482
+ function setTileDefaultBleed(bleed) { tileDefaultBleed = bleed; }
2463
2483
 
2464
2484
  /** Set if collisions between objects are enabled
2465
2485
  * @param {boolean} enable
@@ -2610,7 +2630,7 @@ function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUn
2610
2630
  /** Set if watermark with FPS should be shown
2611
2631
  * @param {boolean} show
2612
2632
  * @memberof Debug */
2613
- function setShowWatermark(show) { showWatermark = show; }
2633
+ function setDebugWatermark(show) { debugWatermark = show; }
2614
2634
 
2615
2635
  /** Set key code used to toggle debug mode, Esc by default
2616
2636
  * @param {string} key
@@ -3046,10 +3066,11 @@ class EngineObject
3046
3066
  * @return {number} -1 if this.mirror is true, or 1 if not mirrored */
3047
3067
  getMirrorSign() { return this.mirror ? -1 : 1; }
3048
3068
 
3049
- /** Attaches a child to this with a given local transform
3069
+ /** Attaches a child to this with a local transform, returns child for chaining
3050
3070
  * @param {EngineObject} child
3051
3071
  * @param {Vector2} [localPos=(0,0)]
3052
- * @param {number} [localAngle] */
3072
+ * @param {number} [localAngle]
3073
+ * @return {EngineObject} The child object added */
3053
3074
  addChild(child, localPos=vec2(), localAngle=0)
3054
3075
  {
3055
3076
  ASSERT(!child.parent && !this.children.includes(child));
@@ -3059,6 +3080,7 @@ class EngineObject
3059
3080
  child.parent = this;
3060
3081
  child.localPos = localPos.copy();
3061
3082
  child.localAngle = localAngle;
3083
+ return child;
3062
3084
  }
3063
3085
 
3064
3086
  /** Removes a child from this one
@@ -3125,8 +3147,7 @@ class EngineObject
3125
3147
  /** Render debug info for this object */
3126
3148
  renderDebugInfo()
3127
3149
  {
3128
- if (!debug)
3129
- return;
3150
+ if (!debug) return;
3130
3151
 
3131
3152
  // show object info for debugging
3132
3153
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
@@ -3149,7 +3170,6 @@ class EngineObject
3149
3170
  * There are 3 canvas/contexts available to draw to...
3150
3171
  * mainCanvas - 2D background canvas, non WebGL stuff like tile layers are drawn here.
3151
3172
  * glCanvas - Used by the accelerated WebGL batch rendering system.
3152
- * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
3153
3173
  *
3154
3174
  * The WebGL rendering system is very fast with some caveats...
3155
3175
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
@@ -3169,21 +3189,6 @@ let mainCanvas;
3169
3189
  * @memberof Draw */
3170
3190
  let mainContext;
3171
3191
 
3172
- /** A canvas that appears on top of everything the same size as mainCanvas
3173
- * @type {HTMLCanvasElement}
3174
- * @memberof Draw */
3175
- let overlayCanvas;
3176
-
3177
- /** 2d context for overlayCanvas
3178
- * @type {CanvasRenderingContext2D}
3179
- * @memberof Draw */
3180
- let overlayContext;
3181
-
3182
- /** The default canvas to use for drawing, usually mainCanvas
3183
- * @type {HTMLCanvasElement|OffscreenCanvas}
3184
- * @memberof Draw */
3185
- let drawCanvas;
3186
-
3187
3192
  /** The default 2d context to use for drawing, usually mainContext
3188
3193
  * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
3189
3194
  * @memberof Draw */
@@ -3199,6 +3204,16 @@ let workCanvas;
3199
3204
  * @memberof Draw */
3200
3205
  let workContext;
3201
3206
 
3207
+ /** Offscreen canvas with willReadFrequently that can be used for image processing
3208
+ * @type {OffscreenCanvas}
3209
+ * @memberof Draw */
3210
+ let workReadCanvas;
3211
+
3212
+ /** Offscreen canvas with willReadFrequently that can be used for image processing
3213
+ * @type {OffscreenCanvasRenderingContext2D}
3214
+ * @memberof Draw */
3215
+ let workReadContext;
3216
+
3202
3217
  /** The size of the main canvas (and other secondary canvases)
3203
3218
  * @type {Vector2}
3204
3219
  * @memberof Draw */
@@ -3221,7 +3236,7 @@ let drawCount;
3221
3236
  * - This can take vecs or floats for easier use and conversion
3222
3237
  * - If an index is passed in, the tile size and index will determine the position
3223
3238
  * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3224
- * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
3239
+ * @param {Vector2|number} [size] - Size of tile in pixels
3225
3240
  * @param {number} [textureIndex] - Texture index to use
3226
3241
  * @param {number} [padding] - How many pixels padding around tiles
3227
3242
  * @return {TileInfo}
@@ -3231,7 +3246,7 @@ let drawCount;
3231
3246
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
3232
3247
  * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
3233
3248
  * @memberof Draw */
3234
- function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3249
+ function tile(pos=new Vector2, size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding)
3235
3250
  {
3236
3251
  if (headlessMode)
3237
3252
  return new TileInfo;
@@ -3270,13 +3285,13 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3270
3285
  class TileInfo
3271
3286
  {
3272
3287
  /** Create a tile info object
3273
- * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3274
- * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
3275
- * @param {number} [textureIndex] - Texture index to use
3276
- * @param {number} [padding] - How many pixels padding around tiles
3277
- * @param {number} [bleedScale] - How many pixels smaller to draw tiles
3288
+ * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
3289
+ * @param {Vector2} [size] - Size of tile in pixels
3290
+ * @param {number} [textureIndex] - Texture index to use
3291
+ * @param {number} [padding] - How many pixels padding around tiles
3292
+ * @param {number} [bleed] - How many pixels smaller to draw tiles
3278
3293
  */
3279
- constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0, bleedScale=tileFixBleedScale)
3294
+ constructor(pos=vec2(), size=tileDefaultSize, textureIndex=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
3280
3295
  {
3281
3296
  /** @property {Vector2} - Top left corner of tile in pixels */
3282
3297
  this.pos = pos.copy();
@@ -3289,7 +3304,7 @@ class TileInfo
3289
3304
  /** @property {TextureInfo} - The texture info for this tile */
3290
3305
  this.textureInfo = textureInfos[this.textureIndex];
3291
3306
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3292
- this.bleedScale = bleedScale;
3307
+ this.bleed = bleed;
3293
3308
  }
3294
3309
 
3295
3310
  /** Returns a copy of this tile offset by a vector
@@ -3297,7 +3312,7 @@ class TileInfo
3297
3312
  * @return {TileInfo}
3298
3313
  */
3299
3314
  offset(offset)
3300
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleedScale); }
3315
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleed); }
3301
3316
 
3302
3317
  /** Returns a copy of this tile offset by a number of animation frames
3303
3318
  * @param {number} frame - Offset to apply in animation frames
@@ -3320,7 +3335,7 @@ class TileInfo
3320
3335
  this.size = textureInfo.size.copy();
3321
3336
  this.textureInfo = textureInfo;
3322
3337
  // do not use padding or bleed
3323
- this.bleedScale = this.padding = 0;
3338
+ this.bleed = this.padding = 0;
3324
3339
  return this;
3325
3340
  }
3326
3341
  }
@@ -3386,7 +3401,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3386
3401
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3387
3402
 
3388
3403
  const textureInfo = tileInfo && tileInfo.textureInfo;
3389
- const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3404
+ const bleed = tileInfo?.bleed ?? 0;
3390
3405
  if (useWebGL && glEnable)
3391
3406
  {
3392
3407
  ASSERT(!!glContext, 'WebGL is not enabled!');
@@ -3401,13 +3416,13 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3401
3416
  const w = tileInfo.size.x * sizeInverse.x;
3402
3417
  const h = tileInfo.size.y * sizeInverse.y;
3403
3418
  glSetTexture(textureInfo.glTexture);
3404
- if (bleedScale)
3419
+ if (bleed)
3405
3420
  {
3406
- const tileImageFixBleedX = sizeInverse.x*bleedScale;
3407
- const tileImageFixBleedY = sizeInverse.y*bleedScale;
3421
+ const bleedX = sizeInverse.x*bleed;
3422
+ const bleedY = sizeInverse.y*bleed;
3408
3423
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
3409
- x + tileImageFixBleedX, y + tileImageFixBleedY,
3410
- x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
3424
+ x + bleedX, y + bleedY,
3425
+ x - bleedX + w, y - bleedY + h,
3411
3426
  color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
3412
3427
  }
3413
3428
  else
@@ -3435,7 +3450,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3435
3450
  // calculate uvs and render
3436
3451
  const x = tileInfo.pos.x, y = tileInfo.pos.y;
3437
3452
  const w = tileInfo.size.x, h = tileInfo.size.y;
3438
- drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleedScale);
3453
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleed);
3439
3454
  }
3440
3455
  else
3441
3456
  {
@@ -3761,7 +3776,11 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3761
3776
  ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
3762
3777
 
3763
3778
  if (!screenSpace)
3764
- [pos, size, angle] = worldToScreenTransform(pos, size, angle);
3779
+ {
3780
+ pos = worldToScreen(pos);
3781
+ size = size.scale(cameraScale);
3782
+ angle -= cameraAngle;
3783
+ }
3765
3784
  context.save();
3766
3785
  context.translate(pos.x+.5, pos.y+.5);
3767
3786
  context.rotate(angle);
@@ -3800,26 +3819,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3800
3819
  drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
3801
3820
  }
3802
3821
 
3803
- /** Draw text on overlay canvas in world space
3804
- * Automatically splits new lines into rows
3805
- * @param {string|number} text
3806
- * @param {Vector2} pos
3807
- * @param {number} [size]
3808
- * @param {Color} [color=(1,1,1,1)]
3809
- * @param {number} [lineWidth]
3810
- * @param {Color} [lineColor=(0,0,0,1)]
3811
- * @param {CanvasTextAlign} [textAlign='center']
3812
- * @param {string} [font=fontDefault]
3813
- * @param {string} [fontStyle]
3814
- * @param {number} [maxWidth]
3815
- * @param {number} [angle]
3816
- * @memberof Draw */
3817
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
3818
- {
3819
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
3820
- }
3821
-
3822
- /** Draw text on overlay canvas in screen space
3822
+ /** Draw text in screen space
3823
3823
  * Automatically splits new lines into rows
3824
3824
  * @param {string|number} text
3825
3825
  * @param {Vector2} pos
@@ -3832,9 +3832,9 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3832
3832
  * @param {string} [fontStyle]
3833
3833
  * @param {number} [maxWidth]
3834
3834
  * @param {number} [angle]
3835
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3835
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3836
3836
  * @memberof Draw */
3837
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
3837
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
3838
3838
  {
3839
3839
  ASSERT(isString(text), 'text must be a string');
3840
3840
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3878,16 +3878,16 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3878
3878
  * @memberof Draw */
3879
3879
  function screenToWorld(screenPos)
3880
3880
  {
3881
+ ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
3882
+
3881
3883
  let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3882
3884
  let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3883
3885
  if (cameraAngle)
3884
3886
  {
3885
3887
  // apply camera rotation
3886
3888
  const c = cos(-cameraAngle), s = sin(-cameraAngle);
3887
- const rotatedX = x * c - y * s;
3888
- const rotatedY = x * s + y * c;
3889
- x = rotatedX;
3890
- y = rotatedY;
3889
+ const xr = x * c - y * s, yr = x * s + y * c;
3890
+ x = xr; y = yr;
3891
3891
  }
3892
3892
  return new Vector2(x + cameraPos.x, y + cameraPos.y);
3893
3893
  }
@@ -3898,16 +3898,16 @@ function screenToWorld(screenPos)
3898
3898
  * @memberof Draw */
3899
3899
  function worldToScreen(worldPos)
3900
3900
  {
3901
+ ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
3902
+
3901
3903
  let x = worldPos.x - cameraPos.x;
3902
3904
  let y = worldPos.y - cameraPos.y;
3903
3905
  if (cameraAngle)
3904
3906
  {
3905
3907
  // apply inverse camera rotation
3906
3908
  const c = cos(cameraAngle), s = sin(cameraAngle);
3907
- const rotatedX = x * c - y * s;
3908
- const rotatedY = x * s + y * c;
3909
- x = rotatedX;
3910
- y = rotatedY;
3909
+ const xr = x * c - y * s, yr = x * s + y * c;
3910
+ x = xr; y = yr;
3911
3911
  }
3912
3912
  return new Vector2
3913
3913
  (
@@ -3922,16 +3922,16 @@ function worldToScreen(worldPos)
3922
3922
  * @memberof Draw */
3923
3923
  function screenToWorldDelta(screenDelta)
3924
3924
  {
3925
+ ASSERT(isVector2(screenDelta), 'screenDelta must be a vec2');
3926
+
3925
3927
  let x = screenDelta.x / cameraScale;
3926
3928
  let y = screenDelta.y / -cameraScale;
3927
3929
  if (cameraAngle)
3928
3930
  {
3929
3931
  // apply camera rotation
3930
3932
  const c = cos(-cameraAngle), s = sin(-cameraAngle);
3931
- const rotatedX = x * c - y * s;
3932
- const rotatedY = x * s + y * c;
3933
- x = rotatedX;
3934
- y = rotatedY;
3933
+ const xr = x * c - y * s, yr = x * s + y * c;
3934
+ x = xr; y = yr;
3935
3935
  }
3936
3936
  return new Vector2(x, y);
3937
3937
  }
@@ -3942,16 +3942,16 @@ function screenToWorldDelta(screenDelta)
3942
3942
  * @memberof Draw */
3943
3943
  function worldToScreenDelta(worldDelta)
3944
3944
  {
3945
+ ASSERT(isVector2(worldDelta), 'worldDelta must be a vec2');
3946
+
3945
3947
  let x = worldDelta.x;
3946
3948
  let y = worldDelta.y;
3947
3949
  if (cameraAngle)
3948
3950
  {
3949
3951
  // apply inverse camera rotation
3950
3952
  const c = cos(cameraAngle), s = sin(cameraAngle);
3951
- const rotatedX = x * c - y * s;
3952
- const rotatedY = x * s + y * c;
3953
- x = rotatedX;
3954
- y = rotatedY;
3953
+ const xr = x * c - y * s, yr = x * s + y * c;
3954
+ x = xr; y = yr;
3955
3955
  }
3956
3956
  return new Vector2(x * cameraScale, y * -cameraScale);
3957
3957
  }
@@ -3964,6 +3964,10 @@ function worldToScreenDelta(worldDelta)
3964
3964
  * @memberof Draw */
3965
3965
  function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
3966
3966
  {
3967
+ ASSERT(isVector2(screenPos), 'screenPos must be a vec2');
3968
+ ASSERT(isVector2(screenSize), 'screenSize must be a vec2');
3969
+ ASSERT(isNumber(screenAngle), 'screenAngle must be a number');
3970
+
3967
3971
  return [
3968
3972
  screenToWorld(screenPos),
3969
3973
  screenSize.scale(1/cameraScale),
@@ -3971,28 +3975,13 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
3971
3975
  ];
3972
3976
  }
3973
3977
 
3974
- /** Convert world space transform to screen space
3975
- * @param {Vector2} worldPos
3976
- * @param {Vector2} worldSize
3977
- * @param {number} [worldAngle]
3978
- * @return {[Vector2, Vector2, number]} - [pos, size, angle]
3979
- * @memberof Draw */
3980
- function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
3981
- {
3982
- return [
3983
- worldToScreen(worldPos),
3984
- worldSize.scale(cameraScale),
3985
- worldAngle - cameraAngle
3986
- ];
3987
- }
3988
-
3989
3978
  /** Get the size of the camera window in world space
3990
3979
  * @return {Vector2}
3991
3980
  * @memberof Draw */
3992
3981
  function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
3993
3982
 
3994
- /** Check if a point or circle is on screen
3995
- * If size is a Vector2, uses the largest dimension as diameter
3983
+ /** Check if a box, point, or circle is on screen with a circle test
3984
+ * If size is a Vector2, uses the length as diameter
3996
3985
  * This can be used to cull offscreen objects from render or update
3997
3986
  * @param {Vector2} pos - world space position
3998
3987
  * @param {Vector2|number} size - world space size or diameter
@@ -4000,12 +3989,30 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
4000
3989
  * @memberof Draw */
4001
3990
  function isOnScreen(pos, size=0)
4002
3991
  {
4003
- pos = worldToScreen(pos);
3992
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3993
+ ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
3994
+
3995
+ // optimized circle on screen test
3996
+ // pos = worldToScreen(pos);
3997
+ let x = pos.x - cameraPos.x;
3998
+ let y = pos.y - cameraPos.y;
3999
+ if (cameraAngle)
4000
+ {
4001
+ // apply inverse camera rotation
4002
+ const c = cos(cameraAngle), s = sin(cameraAngle);
4003
+ const xr = x * c - y * s, yr = x * s + y * c;
4004
+ x = xr; y = yr;
4005
+ }
4006
+ x *= cameraScale*2; y *= -cameraScale*2;
4007
+
4004
4008
  if (size instanceof Vector2)
4005
- size = max(size.x, size.y); // use largest dimension
4006
- size *= cameraScale/2;
4007
- return pos.x + size > 0 && pos.x - size < mainCanvasSize.x &&
4008
- pos.y + size > 0 && pos.y - size < mainCanvasSize.y;
4009
+ size = size.length(); // use length of vector as diameter
4010
+ size *= cameraScale;
4011
+
4012
+ // check against screen bounds
4013
+ const w = mainCanvasSize.x, h = mainCanvasSize.y;
4014
+ return x + size > -w && x - size < w &&
4015
+ y + size > -h && y - size < h;
4009
4016
  }
4010
4017
 
4011
4018
  /** Enable normal or additive blend mode
@@ -4019,18 +4026,19 @@ function setBlendMode(additive=false, context)
4019
4026
  context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4020
4027
  }
4021
4028
 
4022
- /** Combines all LittleJS canvases onto the main canvas and clears them
4023
- * This is necessary for things like saving a screenshot
4029
+ /** Combines LittleJS canvases onto the main canvas
4030
+ * This is necessary for things like screenshots and video
4024
4031
  * @memberof Draw */
4025
4032
  function combineCanvases()
4026
4033
  {
4027
- // combine canvases
4028
- glCopyToContext(mainContext);
4029
- mainContext.drawImage(overlayCanvas, 0, 0);
4030
-
4031
- // clear canvases
4032
- glClearCanvas();
4033
- overlayCanvas.width |= 0;
4034
+ const w = mainCanvasSize.x, h = mainCanvasSize.y;
4035
+ workCanvas.width = w;
4036
+ workCanvas.height = h;
4037
+ workContext.fillRect(0,0,w,h); // remove background alpha
4038
+ glCopyToContext(workContext);
4039
+ workContext.drawImage(mainCanvas, 0, 0);
4040
+ mainCanvas.width |= 0;
4041
+ mainContext.drawImage(workCanvas, 0, 0);
4034
4042
  }
4035
4043
 
4036
4044
  /** Helper function to draw an image with color and additive color applied
@@ -4047,18 +4055,18 @@ function combineCanvases()
4047
4055
  * @param {number} dHeight
4048
4056
  * @param {Color} color
4049
4057
  * @param {Color} [additiveColor]
4050
- * @param {number} [bleedScale] - How much to shrink the source, used to fix bleeding
4058
+ * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
4051
4059
  * @memberof Draw */
4052
- function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleedScale=0)
4060
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
4053
4061
  {
4054
4062
  function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
4055
4063
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4056
- const sx2 = bleedScale;
4057
- const sy2 = bleedScale;
4064
+ const sx2 = bleed;
4065
+ const sy2 = bleed;
4058
4066
  sWidth = max(1,sWidth|0);
4059
4067
  sHeight = max(1,sHeight|0);
4060
- const sWidth2 = sWidth - 2*bleedScale;
4061
- const sHeight2 = sHeight - 2*bleedScale;
4068
+ const sWidth2 = sWidth - 2*bleed;
4069
+ const sHeight2 = sHeight - 2*bleed;
4062
4070
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
4063
4071
  {
4064
4072
  // white texture with no additive alpha, no need to tint
@@ -4069,12 +4077,12 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4069
4077
  else
4070
4078
  {
4071
4079
  // copy to offscreen canvas
4072
- workCanvas.width = sWidth;
4073
- workCanvas.height = sHeight;
4074
- workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4080
+ workReadCanvas.width = sWidth;
4081
+ workReadCanvas.height = sHeight;
4082
+ workReadContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4075
4083
 
4076
4084
  // tint image using offscreen work context
4077
- const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
4085
+ const imageData = workReadContext.getImageData(0, 0, sWidth, sHeight);
4078
4086
  const data = imageData.data;
4079
4087
  if (additiveColor && !isBlack(additiveColor))
4080
4088
  {
@@ -4083,8 +4091,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4083
4091
  const colorAdd = [additiveColor.r * 255, additiveColor.g * 255, additiveColor.b * 255, additiveColor.a * 255];
4084
4092
  for (let i = 0; i < data.length; ++i)
4085
4093
  data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
4086
- workContext.putImageData(imageData, 0, 0);
4087
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4094
+ workReadContext.putImageData(imageData, 0, 0);
4095
+ context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4088
4096
  }
4089
4097
  else
4090
4098
  {
@@ -4095,9 +4103,9 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4095
4103
  data[i+1] *= color.g;
4096
4104
  data[i+2] *= color.b;
4097
4105
  }
4098
- workContext.putImageData(imageData, 0, 0);
4106
+ workReadContext.putImageData(imageData, 0, 0);
4099
4107
  context.globalAlpha = color.a;
4100
- context.drawImage(workCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4108
+ context.drawImage(workReadCanvas, sx2, sy2, sWidth2, sHeight2, dx, dy, dWidth, dHeight);
4101
4109
  context.globalAlpha = 1;
4102
4110
  }
4103
4111
  }
@@ -4159,7 +4167,7 @@ class FontImage
4159
4167
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4160
4168
  {
4161
4169
  // load default font image
4162
- if (!engineFontImage)
4170
+ if (!image && !engineFontImage)
4163
4171
  {
4164
4172
  engineFontImage = new Image;
4165
4173
  engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
@@ -4182,23 +4190,14 @@ class FontImage
4182
4190
  this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center, context);
4183
4191
  }
4184
4192
 
4185
- /** Draw text on overlay canvas in world space using the image font
4186
- * @param {string|number} text
4187
- * @param {Vector2} pos
4188
- * @param {number} [scale]
4189
- * @param {boolean} [center]
4190
- */
4191
- drawTextOverlay(text, pos, scale=4, center)
4192
- { this.drawText(text, pos, scale, center, overlayContext); }
4193
-
4194
- /** Draw text on overlay canvas in screen space using the image font
4193
+ /** Draw text in screen space using the image font
4195
4194
  * @param {string|number} text
4196
4195
  * @param {Vector2} pos
4197
4196
  * @param {number} [scale]
4198
4197
  * @param {boolean} [center]
4199
4198
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4200
4199
  */
4201
- drawTextScreen(text, pos, scale=4, center=true, context=overlayContext)
4200
+ drawTextScreen(text, pos, scale=4, center=true, context=drawContext)
4202
4201
  {
4203
4202
  context.save();
4204
4203
  const size = this.tileSize;
@@ -4591,8 +4590,7 @@ function inputInit()
4591
4590
  }
4592
4591
  function onMouseDown(e)
4593
4592
  {
4594
- if (isTouchDevice && touchInputEnable)
4595
- return;
4593
+ if (isTouchDevice && touchInputEnable) return;
4596
4594
 
4597
4595
  // fix stalled audio requiring user interaction
4598
4596
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
@@ -4643,8 +4641,7 @@ function inputInit()
4643
4641
  let wasTouching;
4644
4642
  function handleTouch(e)
4645
4643
  {
4646
- if (!touchInputEnable)
4647
- return;
4644
+ if (!touchInputEnable) return;
4648
4645
 
4649
4646
  // route touch to gamepad
4650
4647
  if (touchGamepadEnable)
@@ -4708,8 +4705,7 @@ function inputInit()
4708
4705
  }
4709
4706
 
4710
4707
  // don't process touch gamepad if paused
4711
- if (paused)
4712
- return;
4708
+ if (paused) return;
4713
4709
 
4714
4710
  // get center of left and right sides
4715
4711
  const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
@@ -4948,7 +4944,7 @@ function inputRender()
4948
4944
  return;
4949
4945
 
4950
4946
  // setup the canvas
4951
- const context = overlayContext;
4947
+ const context = mainContext;
4952
4948
  context.save();
4953
4949
  context.globalAlpha = alpha*touchGamepadAlpha;
4954
4950
  context.strokeStyle = '#fff';
@@ -5190,12 +5186,12 @@ class Sound
5190
5186
  ///////////////////////////////////////////////////////////////////////////////
5191
5187
 
5192
5188
  /**
5193
- * Sound Wave Object - Stores a wave sound for later use and can be played positionally
5194
- * - this can be used to play wave, mp3, and ogg files
5189
+ * Sound Wave Object - Loads and stores an audio file for later use
5190
+ * - this can be used to load and play wave, mp3, and ogg files
5195
5191
  * @extends Sound
5196
5192
  * @memberof Audio
5197
5193
  * @example
5198
- * // create a sound
5194
+ * // load an audio asset file
5199
5195
  * const sound_example = new SoundWave('sound.mp3');
5200
5196
  *
5201
5197
  * // play the sound
@@ -5395,8 +5391,7 @@ class SoundInstance
5395
5391
  /** Pause this sound instance */
5396
5392
  pause()
5397
5393
  {
5398
- if (this.isPaused())
5399
- return;
5394
+ if (this.isPaused()) return;
5400
5395
 
5401
5396
  // save current time and stop sound
5402
5397
  this.pausedTime = this.getCurrentTime();
@@ -5408,8 +5403,7 @@ class SoundInstance
5408
5403
  /** Unpauses this sound instance */
5409
5404
  resume()
5410
5405
  {
5411
- if (!this.isPaused())
5412
- return;
5406
+ if (!this.isPaused()) return;
5413
5407
 
5414
5408
  // restart sound from paused time
5415
5409
  this.start(this.pausedTime);
@@ -6147,26 +6141,26 @@ class TileLayer extends CanvasLayer
6147
6141
  if (!this.context) return;
6148
6142
 
6149
6143
  // save current render settings
6150
- /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
6151
- this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
6144
+ /** @type {[CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6145
+ this.savedRenderSettings = [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6152
6146
 
6153
6147
  // set the draw canvas and context to this layer
6154
6148
  // use camera settings to match this layer's canvas
6155
- drawCanvas = this.canvas;
6156
6149
  drawContext = this.context;
6157
6150
  cameraPos = this.size.scale(.5);
6158
6151
  const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6159
6152
  cameraScale = tileSize.x;
6153
+ canvasClearColor = CLEAR_BLACK;
6160
6154
  mainCanvasSize = this.size.multiply(tileSize);
6161
6155
  if (clear)
6162
6156
  {
6163
6157
  // clear and set size
6164
- drawCanvas.width = mainCanvasSize.x;
6165
- drawCanvas.height = mainCanvasSize.y;
6158
+ this.canvas.width = mainCanvasSize.x;
6159
+ this.canvas.height = mainCanvasSize.y;
6166
6160
  }
6167
6161
 
6168
6162
  // disable smoothing for pixel art
6169
- this.context.imageSmoothingEnabled = !tilesPixelated;
6163
+ drawContext.imageSmoothingEnabled = !tilesPixelated;
6170
6164
 
6171
6165
  // setup gl rendering if enabled
6172
6166
  glPreRender();
@@ -6179,10 +6173,10 @@ class TileLayer extends CanvasLayer
6179
6173
 
6180
6174
  ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6181
6175
  glCopyToContext(drawContext);
6182
- //debugSaveCanvas(this.canvas);
6176
+ //saveCanvas(this.canvas);
6183
6177
 
6184
6178
  // set stuff back to normal
6185
- [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale] = this.savedRenderSettings;
6179
+ [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6186
6180
  }
6187
6181
 
6188
6182
  /** Draw the tile at a given position in the tile grid
@@ -6250,8 +6244,7 @@ class TileCollisionLayer extends TileLayer
6250
6244
  /** Destroy this tile layer */
6251
6245
  destroy()
6252
6246
  {
6253
- if (this.destroyed)
6254
- return;
6247
+ if (this.destroyed) return;
6255
6248
 
6256
6249
  // remove from collision layers array and destroy
6257
6250
  const index = tileCollisionLayers.indexOf(this);
@@ -6788,10 +6781,11 @@ function medalsInit(saveName)
6788
6781
 
6789
6782
  // engine automatically renders medals
6790
6783
  engineAddPlugin(undefined, medalsRender);
6784
+
6785
+ // plugin functions
6791
6786
  function medalsRender()
6792
6787
  {
6793
- if (!medalsDisplayQueue.length)
6794
- return;
6788
+ if (!medalsDisplayQueue.length) return;
6795
6789
 
6796
6790
  // update first medal in queue
6797
6791
  const medal = medalsDisplayQueue[0];
@@ -6881,8 +6875,7 @@ class Medal
6881
6875
  /** Unlocks a medal if not already unlocked */
6882
6876
  unlock()
6883
6877
  {
6884
- if (medalsPreventUnlock || this.unlocked)
6885
- return;
6878
+ if (medalsPreventUnlock || this.unlocked) return;
6886
6879
 
6887
6880
  // save the medal
6888
6881
  ASSERT(medalsSaveName, 'save name must be set');
@@ -6895,10 +6888,10 @@ class Medal
6895
6888
  */
6896
6889
  render(hidePercent=0)
6897
6890
  {
6898
- const context = overlayContext;
6891
+ const context = mainContext;
6899
6892
  const width = min(medalDisplaySize.x, mainCanvas.width);
6900
6893
  const height = medalDisplaySize.y;
6901
- const x = overlayCanvas.width - width;
6894
+ const x = mainCanvas.width - width;
6902
6895
  const y = -height*hidePercent;
6903
6896
  const backgroundColor = hsl(0,0,.9);
6904
6897
 
@@ -6939,7 +6932,7 @@ class Medal
6939
6932
  {
6940
6933
  // draw the image or icon
6941
6934
  if (this.image)
6942
- overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
6935
+ mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
6943
6936
  else
6944
6937
  drawTextScreen(this.icon, pos, size*.7, BLACK);
6945
6938
  }
@@ -6960,7 +6953,7 @@ class Medal
6960
6953
  * @namespace WebGL
6961
6954
  */
6962
6955
 
6963
- /** The WebGL canvas which appears above the main canvas and below the overlay canvas
6956
+ /** The WebGL canvas which appears below the main canvas
6964
6957
  * @type {HTMLCanvasElement}
6965
6958
  * @memberof WebGL */
6966
6959
  let glCanvas;
@@ -6990,7 +6983,7 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
6990
6983
  ///////////////////////////////////////////////////////////////////////////////
6991
6984
 
6992
6985
  // Initialize WebGL, called automatically by the engine
6993
- function glInit()
6986
+ function glInit(rootElement)
6994
6987
  {
6995
6988
  // keep set of texture infos so they can be restored if context is lost
6996
6989
  glTextureInfos = new Set;
@@ -7014,8 +7007,7 @@ function glInit()
7014
7007
  return;
7015
7008
  }
7016
7009
 
7017
- // attach the WebGL canvas
7018
- const rootElement = mainCanvas.parentElement;
7010
+ // attach the WebGL canvas;
7019
7011
  rootElement.appendChild(glCanvas);
7020
7012
 
7021
7013
  // startup webgl
@@ -7116,8 +7108,7 @@ function glInit()
7116
7108
 
7117
7109
  function glSetInstancedMode()
7118
7110
  {
7119
- if (!glPolyMode)
7120
- return;
7111
+ if (!glPolyMode) return;
7121
7112
 
7122
7113
  // setup instanced mode
7123
7114
  glFlush();
@@ -7150,8 +7141,7 @@ function glSetInstancedMode()
7150
7141
 
7151
7142
  function glSetPolyMode()
7152
7143
  {
7153
- if (glPolyMode)
7154
- return;
7144
+ if (glPolyMode) return;
7155
7145
 
7156
7146
  // setup poly mode
7157
7147
  glFlush();
@@ -7230,9 +7220,12 @@ function glClearCanvas()
7230
7220
  if (!glContext) return;
7231
7221
 
7232
7222
  // clear and set to same size as main canvas
7233
- glCanvas.width = drawCanvas.width;
7234
- glCanvas.height = drawCanvas.height;
7223
+ glCanvas.width = mainCanvasSize.x;
7224
+ glCanvas.height = mainCanvasSize.y;
7235
7225
  glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
7226
+ const color = canvasClearColor;
7227
+ if (color.a > 0)
7228
+ glContext.clearColor(color.r, color.g, color.b, color.a);
7236
7229
  glContext.clear(glContext.COLOR_BUFFER_BIT);
7237
7230
  }
7238
7231
 
@@ -7244,8 +7237,7 @@ function glClearCanvas()
7244
7237
  function glSetTexture(texture, wrap=false)
7245
7238
  {
7246
7239
  // must flush cache with the old texture to set a new one
7247
- if (!glContext || texture === glActiveTexture)
7248
- return;
7240
+ if (!glContext || texture === glActiveTexture) return;
7249
7241
 
7250
7242
  glFlush();
7251
7243
  glActiveTexture = texture;
@@ -7341,6 +7333,7 @@ function glCreateTexture(image)
7341
7333
  function glDeleteTexture(texture)
7342
7334
  {
7343
7335
  if (!glContext) return;
7336
+
7344
7337
  glContext.deleteTexture(texture);
7345
7338
  }
7346
7339
 
@@ -7425,8 +7418,7 @@ function glFlush()
7425
7418
  * @memberof WebGL */
7426
7419
  function glCopyToContext(context)
7427
7420
  {
7428
- if (!glEnable || !glContext)
7429
- return;
7421
+ if (!glEnable || !glContext) return;
7430
7422
 
7431
7423
  glFlush();
7432
7424
  context.drawImage(glCanvas, 0, 0);
@@ -7462,7 +7454,6 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
7462
7454
  glFlush();
7463
7455
  glSetInstancedMode();
7464
7456
 
7465
- glPolyMode = false;
7466
7457
  let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
7467
7458
  glPositionData[offset++] = x;
7468
7459
  glPositionData[offset++] = y;
@@ -7983,13 +7974,12 @@ class PostProcessPlugin
7983
7974
  {
7984
7975
  /** Create global post processing shader
7985
7976
  * @param {string} shaderCode
7986
- * @param {boolean} [includeOverlay]
7987
7977
  * @param {boolean} [includeMainCanvas]
7988
7978
  * @example
7989
7979
  * // create the post process plugin object
7990
7980
  * new PostProcessPlugin(shaderCode);
7991
7981
  */
7992
- constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
7982
+ constructor(shaderCode, includeMainCanvas=true)
7993
7983
  {
7994
7984
  ASSERT(!postProcess, 'Post process already initialized');
7995
7985
  postProcess = this;
@@ -8061,17 +8051,19 @@ class PostProcessPlugin
8061
8051
  // clear out the buffer
8062
8052
  glFlush();
8063
8053
 
8064
- if (includeMainCanvas || includeOverlay)
8054
+ // setup texture
8055
+ glContext.activeTexture(glContext.TEXTURE0);
8056
+ glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
8057
+ if (includeMainCanvas)
8065
8058
  {
8066
- // copy WebGL to the main canvas
8067
- mainContext.drawImage(glCanvas, 0, 0);
8068
-
8069
- if (includeOverlay)
8070
- {
8071
- // copy overlay canvas so it will be included in post processing
8072
- mainContext.drawImage(overlayCanvas, 0, 0);
8073
- overlayCanvas.width |= 0; // clear overlay canvas
8074
- }
8059
+ // copy main canvas to work canvas
8060
+ workCanvas.width = mainCanvasSize.x;
8061
+ workCanvas.height = mainCanvasSize.y;
8062
+ glCopyToContext(workContext);
8063
+ workContext.drawImage(mainCanvas, 0, 0);
8064
+
8065
+ // copy work canvas to texture
8066
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
8075
8067
  }
8076
8068
 
8077
8069
  // setup shader program to draw a quad
@@ -8080,14 +8072,6 @@ class PostProcessPlugin
8080
8072
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
8081
8073
  glContext.disable(glContext.BLEND);
8082
8074
 
8083
- // set textures, pass in the 2d canvas and gl canvas in separate texture channels
8084
- glContext.activeTexture(glContext.TEXTURE0);
8085
- glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
8086
- if (includeMainCanvas || includeOverlay)
8087
- {
8088
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
8089
- }
8090
-
8091
8075
  // set vertex position attribute
8092
8076
  const vertexByteStride = 8;
8093
8077
  const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
@@ -8316,12 +8300,14 @@ class UISystemPlugin
8316
8300
  * // create the ui plugin object
8317
8301
  * new UISystemPlugin;
8318
8302
  */
8319
- constructor(context=overlayContext)
8303
+ constructor(context=mainContext)
8320
8304
  {
8321
8305
  ASSERT(!uiSystem, 'UI system already initialized');
8322
8306
  uiSystem = this;
8323
8307
 
8324
8308
  // default settings
8309
+ /** @property {boolean} - Activate when mouse is pressed down instead of clicked */
8310
+ this.activateOnPress = false;
8325
8311
  /** @property {Color} - Default fill color for UI elements */
8326
8312
  this.defaultColor = WHITE;
8327
8313
  /** @property {Color} - Default outline color for UI elements */
@@ -8973,13 +8959,15 @@ class UIObject
8973
8959
  uiSystem.uiObjects.push(this);
8974
8960
  }
8975
8961
 
8976
- /** Add a child UIObject to this object
8977
- * @param {UIObject} child */
8962
+ /** Add a child UIObject to this object, returns child for chaining
8963
+ * @param {UIObject} child
8964
+ * @return {UIObject} The child object added */
8978
8965
  addChild(child)
8979
8966
  {
8980
8967
  ASSERT(!child.parent && !this.children.includes(child));
8981
8968
  this.children.push(child);
8982
8969
  child.parent = this;
8970
+ return child;
8983
8971
  }
8984
8972
 
8985
8973
  /** Remove a child UIObject from this object
@@ -9026,7 +9014,7 @@ class UIObject
9026
9014
  this.onUpdate();
9027
9015
 
9028
9016
  // unset active if disabled
9029
- if (this.disabled && this == uiSystem.activeObject)
9017
+ if (this.disabled && this === uiSystem.activeObject)
9030
9018
  uiSystem.activeObject = undefined;
9031
9019
 
9032
9020
  const wasHover = uiSystem.lastHoverObject === this;
@@ -9052,8 +9040,16 @@ class UIObject
9052
9040
  if (uiSystem.activeObject && !isActive)
9053
9041
  uiSystem.activeObject.onRelease();
9054
9042
  uiSystem.activeObject = this;
9043
+
9044
+ if (uiSystem.activateOnPress)
9045
+ {
9046
+ this.onClick();
9047
+ if (!this.soundPress && this.soundClick)
9048
+ this.soundClick.play();
9049
+ }
9055
9050
  }
9056
9051
  }
9052
+ if (!uiSystem.activateOnPress)
9057
9053
  if (!mouseDown && this.isActiveObject() && this.interactive)
9058
9054
  {
9059
9055
  this.onClick();
@@ -11493,33 +11489,33 @@ async function box2dInit()
11493
11489
  color = getDebugColor(color);
11494
11490
  point1 = box2d.vec2FromPointer(point1);
11495
11491
  point2 = box2d.vec2FromPointer(point2);
11496
- drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
11492
+ drawLine(point1, point2, debugLineWidth, color, vec2(), 0, false);
11497
11493
  };
11498
11494
  debugDraw.DrawPolygon = function(vertices, vertexCount, color)
11499
11495
  {
11500
11496
  color = getDebugColor(color);
11501
11497
  const points = getPointsList(vertices, vertexCount);
11502
- drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false, false, overlayContext);
11498
+ drawPoly(points, CLEAR_WHITE, debugLineWidth, color, vec2(), 0, false);
11503
11499
  };
11504
11500
  debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
11505
11501
  {
11506
11502
  color = getDebugColor(color);
11507
11503
  const points = getPointsList(vertices, vertexCount);
11508
- drawPoly(points, color, 0, color, vec2(), 0, false, false, overlayContext);
11504
+ drawPoly(points, color, 0, color, vec2(), 0, false);
11509
11505
  };
11510
11506
  debugDraw.DrawCircle = function(center, radius, color)
11511
11507
  {
11512
11508
  color = getDebugColor(color);
11513
11509
  center = box2d.vec2FromPointer(center);
11514
- drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false, false, overlayContext);
11510
+ drawCircle(center, radius*2, CLEAR_WHITE, debugLineWidth, color, false);
11515
11511
  };
11516
11512
  debugDraw.DrawSolidCircle = function(center, radius, axis, color)
11517
11513
  {
11518
11514
  color = getDebugColor(color);
11519
11515
  center = box2d.vec2FromPointer(center);
11520
11516
  axis = box2d.vec2FromPointer(axis).scale(radius);
11521
- drawCircle(center, radius*2, color, debugLineWidth, color, false, false, overlayContext);
11522
- drawLine(vec2(), axis, debugLineWidth, color, center, 0, false, false, overlayContext);
11517
+ drawCircle(center, radius*2, color, debugLineWidth, color, false);
11518
+ drawLine(vec2(), axis, debugLineWidth, color, center, 0, false);
11523
11519
  };
11524
11520
  debugDraw.DrawTransform = function(transform)
11525
11521
  {
@@ -11528,8 +11524,8 @@ async function box2dInit()
11528
11524
  const angle = -transform.get_q().GetAngle();
11529
11525
  const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
11530
11526
  const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
11531
- drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false, false, overlayContext);
11532
- drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false, false, overlayContext);
11527
+ drawLine(vec2(), p1, debugLineWidth, c1, pos, angle, false);
11528
+ drawLine(vec2(), p2, debugLineWidth, c2, pos, angle, false);
11533
11529
  }
11534
11530
 
11535
11531
  debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
@@ -11549,8 +11545,8 @@ async function box2dInit()
11549
11545
 
11550
11546
  ///////////////////////////////////////////////////////////////////////////////
11551
11547
 
11552
- /** Draw a scalable nine-slice UI element to the overlay canvas in screen space
11553
- * This function can not apply color because it draws using the overlay 2d context
11548
+ /** Draw a scalable nine-slice UI element to the main canvas in screen space
11549
+ * This function can not apply color because it draws using the 2d context
11554
11550
  * @param {Vector2} pos - Screen space position
11555
11551
  * @param {Vector2} size - Screen space size
11556
11552
  * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
@@ -11560,7 +11556,7 @@ async function box2dInit()
11560
11556
  * @memberof DrawUtilities */
11561
11557
  function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
11562
11558
  {
11563
- drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
11559
+ drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
11564
11560
  }
11565
11561
 
11566
11562
  /** Draw a scalable nine-slice UI element in world space
@@ -11609,8 +11605,8 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
11609
11605
  }
11610
11606
  }
11611
11607
 
11612
- /** Draw a scalable three-slice UI element to the overlay canvas in screen space
11613
- * This function can not apply color because it draws using the overlay 2d context
11608
+ /** Draw a scalable three-slice UI element to the main canvas in screen space
11609
+ * This function can not apply color because it draws using the 2d context
11614
11610
  * @param {Vector2} pos - Screen space position
11615
11611
  * @param {Vector2} size - Screen space size
11616
11612
  * @param {TileInfo} startTile - Starting tile for the three-slice pattern
@@ -11620,7 +11616,7 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
11620
11616
  * @memberof DrawUtilities */
11621
11617
  function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0)
11622
11618
  {
11623
- drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true, overlayContext);
11619
+ drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true);
11624
11620
  }
11625
11621
 
11626
11622
  /** Draw a scalable three-slice UI element in world space