littlejsengine 1.16.1 → 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.
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.16.1';
36
+ const engineVersion = '1.16.2';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -272,33 +272,20 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
272
272
  if (!wasUpdated)
273
273
  updateCanvas();
274
274
 
275
- // render sort then render while removing destroyed objects
275
+ // render the game and objects
276
276
  enginePreRender();
277
277
  gameRender();
278
278
  engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
279
279
  for (const o of engineObjects)
280
280
  o.destroyed || o.render();
281
+
282
+ // post rendering
281
283
  gameRenderPost();
282
284
  pluginList.forEach(plugin=>plugin.render?.());
283
285
  inputRender();
284
286
  debugRender();
285
287
  glFlush();
286
- debugVideoCaptureUpdate();
287
-
288
- if (debugWatermark && !debugVideoCaptureIsActive())
289
- {
290
- // update fps display
291
- mainContext.textAlign = 'right';
292
- mainContext.textBaseline = 'top';
293
- mainContext.font = '1em monospace';
294
- mainContext.fillStyle = '#000';
295
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
296
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
297
- + (glEnable ? ' GL' : ' 2D') ;
298
- mainContext.fillText(text, mainCanvas.width-3, 3);
299
- mainContext.fillStyle = '#fff';
300
- mainContext.fillText(text, mainCanvas.width-2, 2);
301
- }
288
+ debugRenderPost();
302
289
  drawCount = 0;
303
290
  }
304
291
  }
@@ -364,15 +351,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
364
351
  mainContext.lineJoin = 'round';
365
352
  mainContext.lineCap = 'round';
366
353
  }
367
-
368
- // wait for gameInit to load
369
- async function startEngine()
370
- {
371
- await gameInit();
372
- engineUpdate();
373
- }
374
- if (headlessMode)
375
- return startEngine();
354
+
355
+ // skip setup if headless
356
+ if (headlessMode) return startEngine();
376
357
 
377
358
  // setup webgl
378
359
  glInit(rootElement);
@@ -387,7 +368,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
387
368
  'touch-action:none;' + // prevent mobile pinch to resize
388
369
  '-webkit-touch-callout:none'; // compatibility for ios
389
370
  rootElement.style.cssText = styleRoot;
390
- drawCanvas = mainCanvas = rootElement.appendChild(document.createElement('canvas'));
371
+ mainCanvas = rootElement.appendChild(document.createElement('canvas'));
391
372
  drawContext = mainContext = mainCanvas.getContext('2d');
392
373
 
393
374
  // init stuff and start engine
@@ -462,6 +443,13 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
462
443
  await Promise.all(promises);
463
444
  return startEngine();
464
445
 
446
+ async function startEngine()
447
+ {
448
+ // wait for gameInit to load
449
+ await gameInit();
450
+ engineUpdate();
451
+ }
452
+
465
453
  ///////////////////////////////////////////////////////////////////////////
466
454
  // LittleJS Splash Screen
467
455
  function drawEngineSplashScreen(t)
@@ -762,6 +750,7 @@ function LOG (){}
762
750
  function debugInit (){}
763
751
  function debugUpdate (){}
764
752
  function debugRender (){}
753
+ function debugRenderPost (){}
765
754
  function debugRect (){}
766
755
  function debugPoly (){}
767
756
  function debugCircle (){}
@@ -771,9 +760,6 @@ function debugOverlap (){}
771
760
  function debugText (){}
772
761
  function debugClear (){}
773
762
  function debugScreenshot (){}
774
- function debugSaveCanvas (){}
775
- function debugSaveText (){}
776
- function debugSaveDataURL(){}
777
763
  function debugShowErrors(){}
778
764
  function debugVideoCaptureIsActive(){ return false; }
779
765
  function debugVideoCaptureStart (){}
@@ -787,99 +773,99 @@ function debugProtectConstant(o){ return o; }
787
773
  * - Color - holds a rgba color with some math functions
788
774
  * - Timer - tracks time automatically
789
775
  * - RandomGenerator - seeded random number generator
790
- * @namespace Utilities
776
+ * @namespace Math
791
777
  */
792
778
 
793
779
  /** The value of PI
794
780
  * @type {number}
795
781
  * @default Math.PI
796
- * @memberof Utilities */
782
+ * @memberof Math */
797
783
  const PI = Math.PI;
798
784
 
799
785
  /** Returns absolute value of value passed in
800
786
  * @param {number} value
801
787
  * @return {number}
802
- * @memberof Utilities */
788
+ * @memberof Math */
803
789
  const abs = Math.abs;
804
790
 
805
791
  /** Returns floored value of value passed in
806
792
  * @param {number} value
807
793
  * @return {number}
808
- * @memberof Utilities */
794
+ * @memberof Math */
809
795
  const floor = Math.floor;
810
796
 
811
797
  /** Returns ceiled value of value passed in
812
798
  * @param {number} value
813
799
  * @return {number}
814
- * @memberof Utilities */
800
+ * @memberof Math */
815
801
  const ceil = Math.ceil;
816
802
 
817
803
  /** Returns rounded value passed in
818
804
  * @param {number} value
819
805
  * @return {number}
820
- * @memberof Utilities */
806
+ * @memberof Math */
821
807
  const round = Math.round;
822
808
 
823
809
  /** Returns lowest value passed in
824
810
  * @param {...number} values
825
811
  * @return {number}
826
- * @memberof Utilities */
812
+ * @memberof Math */
827
813
  const min = Math.min;
828
814
 
829
815
  /** Returns highest value passed in
830
816
  * @param {...number} values
831
817
  * @return {number}
832
- * @memberof Utilities */
818
+ * @memberof Math */
833
819
  const max = Math.max;
834
820
 
835
821
  /** Returns the sign of value passed in
836
822
  * @param {number} value
837
823
  * @return {number}
838
- * @memberof Utilities */
824
+ * @memberof Math */
839
825
  const sign = Math.sign;
840
826
 
841
827
  /** Returns hypotenuse of values passed in
842
828
  * @param {...number} values
843
829
  * @return {number}
844
- * @memberof Utilities */
830
+ * @memberof Math */
845
831
  const hypot = Math.hypot;
846
832
 
847
833
  /** Returns log2 of value passed in
848
834
  * @param {number} value
849
835
  * @return {number}
850
- * @memberof Utilities */
836
+ * @memberof Math */
851
837
  const log2 = Math.log2;
852
838
 
853
839
  /** Returns sin of value passed in
854
840
  * @param {number} value
855
841
  * @return {number}
856
- * @memberof Utilities */
842
+ * @memberof Math */
857
843
  const sin = Math.sin;
858
844
 
859
845
  /** Returns cos of value passed in
860
846
  * @param {number} value
861
847
  * @return {number}
862
- * @memberof Utilities */
848
+ * @memberof Math */
863
849
  const cos = Math.cos;
864
850
 
865
851
  /** Returns tan of value passed in
866
852
  * @param {number} value
867
853
  * @return {number}
868
- * @memberof Utilities */
854
+ * @memberof Math */
869
855
  const tan = Math.tan;
870
856
 
871
857
  /** Returns atan2 of values passed in
872
858
  * @param {number} y
873
859
  * @param {number} x
874
860
  * @return {number}
875
- * @memberof Utilities */
861
+ * @memberof Math */
876
862
  const atan2 = Math.atan2;
877
863
 
878
864
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
879
865
  * @param {number} dividend
880
866
  * @param {number} [divisor]
881
867
  * @return {number}
882
- * @memberof Utilities */
868
+ * @memberof Math */
883
869
  function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
884
870
 
885
871
  /** Clamps the value between max and min
@@ -887,7 +873,7 @@ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % di
887
873
  * @param {number} [min]
888
874
  * @param {number} [max]
889
875
  * @return {number}
890
- * @memberof Utilities */
876
+ * @memberof Math */
891
877
  function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
892
878
 
893
879
  /** Returns what percentage the value is between valueA and valueB
@@ -895,7 +881,7 @@ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? m
895
881
  * @param {number} valueA
896
882
  * @param {number} valueB
897
883
  * @return {number}
898
- * @memberof Utilities */
884
+ * @memberof Math */
899
885
  function percent(value, valueA, valueB)
900
886
  { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
901
887
 
@@ -904,7 +890,7 @@ function percent(value, valueA, valueB)
904
890
  * @param {number} valueB
905
891
  * @param {number} percent
906
892
  * @return {number}
907
- * @memberof Utilities */
893
+ * @memberof Math */
908
894
  function lerp(valueA, valueB, percent)
909
895
  { return valueA + clamp(percent) * (valueB-valueA); }
910
896
 
@@ -916,7 +902,7 @@ function lerp(valueA, valueB, percent)
916
902
  * @param {number} lerpA
917
903
  * @param {number} lerpB
918
904
  * @return {number}
919
- * @memberof Utilities */
905
+ * @memberof Math */
920
906
  function percentLerp(value, percentA, percentB, lerpA, lerpB)
921
907
  { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
922
908
 
@@ -925,7 +911,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
925
911
  * @param {number} valueB
926
912
  * @param {number} [wrapSize]
927
913
  * @return {number}
928
- * @memberof Utilities */
914
+ * @memberof Math */
929
915
  function distanceWrap(valueA, valueB, wrapSize=1)
930
916
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
931
917
 
@@ -935,7 +921,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
935
921
  * @param {number} percent
936
922
  * @param {number} [wrapSize]
937
923
  * @return {number}
938
- * @memberof Utilities */
924
+ * @memberof Math */
939
925
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
940
926
  { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
941
927
 
@@ -943,7 +929,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
943
929
  * @param {number} angleA
944
930
  * @param {number} angleB
945
931
  * @return {number}
946
- * @memberof Utilities */
932
+ * @memberof Math */
947
933
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
948
934
 
949
935
  /** Linearly interpolates between the angles passed in with wrapping
@@ -951,25 +937,25 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
951
937
  * @param {number} angleB
952
938
  * @param {number} percent
953
939
  * @return {number}
954
- * @memberof Utilities */
940
+ * @memberof Math */
955
941
  function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
956
942
 
957
943
  /** Applies smoothstep function to the percentage value
958
944
  * @param {number} percent
959
945
  * @return {number}
960
- * @memberof Utilities */
946
+ * @memberof Math */
961
947
  function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
962
948
 
963
949
  /** Checks if the value passed in is a power of two
964
950
  * @param {number} value
965
951
  * @return {boolean}
966
- * @memberof Utilities */
952
+ * @memberof Math */
967
953
  function isPowerOfTwo(value) { return !(value & (value - 1)); }
968
954
 
969
955
  /** Returns the nearest power of two not less than the value
970
956
  * @param {number} value
971
957
  * @return {number}
972
- * @memberof Utilities */
958
+ * @memberof Math */
973
959
  function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
974
960
 
975
961
  /** Returns true if two axis aligned bounding boxes are overlapping
@@ -979,7 +965,7 @@ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
979
965
  * @param {Vector2} posB - Center of box B
980
966
  * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
981
967
  * @return {boolean} - True if overlapping
982
- * @memberof Utilities */
968
+ * @memberof Math */
983
969
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
984
970
  {
985
971
  const dx = (posA.x - posB.x)*2;
@@ -995,7 +981,7 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
995
981
  * @param {Vector2} pos - Center of box
996
982
  * @param {Vector2} size - Size of box
997
983
  * @return {boolean} - True if intersecting
998
- * @memberof Utilities */
984
+ * @memberof Math */
999
985
  function isIntersecting(start, end, pos, size)
1000
986
  {
1001
987
  // Liang-Barsky algorithm
@@ -1036,52 +1022,29 @@ function isIntersecting(start, end, pos, size)
1036
1022
  * @param {number} [t=time] - Value to use for time of the wave
1037
1023
  * @param {number} [offset] - Value to use for time offset of the wave
1038
1024
  * @return {number} - Value waving between 0 and amplitude
1039
- * @memberof Utilities */
1025
+ * @memberof Math */
1040
1026
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1041
1027
  { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
1042
1028
 
1043
- /** Formats seconds to mm:ss style for display purposes
1044
- * @param {number} t - time in seconds
1045
- * @return {string}
1046
- * @memberof Utilities */
1047
- function formatTime(t)
1048
- {
1049
- const sign = t < 0 ? '-' : '';
1050
- t = abs(t)|0;
1051
- return sign + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
1052
- }
1053
-
1054
- /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
1055
- * @param {string} url - URL of JSON file
1056
- * @return {Promise<object>}
1057
- * @memberof Utilities */
1058
- async function fetchJSON(url)
1059
- {
1060
- const response = await fetch(url);
1061
- if (!response.ok)
1062
- throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1063
- return response.json();
1064
- }
1065
-
1066
1029
  /**
1067
1030
  * Check if object is a valid number, not NaN or undefined, but it may be infinite
1068
1031
  * @param {any} n
1069
1032
  * @return {boolean}
1070
- * @memberof Utilities */
1033
+ * @memberof Math */
1071
1034
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1072
1035
 
1073
1036
  /**
1074
1037
  * Check if object is a valid string or can be converted to one
1075
1038
  * @param {any} s
1076
1039
  * @return {boolean}
1077
- * @memberof Utilities */
1040
+ * @memberof Math */
1078
1041
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1079
1042
 
1080
1043
  /**
1081
1044
  * Check if object is an array
1082
1045
  * @param {any} a
1083
1046
  * @return {boolean}
1084
- * @memberof Utilities */
1047
+ * @memberof Math */
1085
1048
  function isArray(a) { return Array.isArray(a); }
1086
1049
 
1087
1050
  /**
@@ -1097,7 +1060,7 @@ function isArray(a) { return Array.isArray(a); }
1097
1060
  * @param {LineTestFunction} testFunction - Check if colliding
1098
1061
  * @param {Vector2} [normal] - Optional vector to store the normal
1099
1062
  * @return {Vector2|undefined} - Position of the collision or undefined if none found
1100
- * @memberof Utilities */
1063
+ * @memberof Math */
1101
1064
  function lineTest(posStart, posEnd, testFunction, normal)
1102
1065
  {
1103
1066
  ASSERT(isVector2(posStart), 'posStart must be a vec2');
@@ -1109,8 +1072,7 @@ function lineTest(posStart, posEnd, testFunction, normal)
1109
1072
  const dx = posEnd.x - posStart.x;
1110
1073
  const dy = posEnd.y - posStart.y;
1111
1074
  const totalLength = hypot(dx, dy);
1112
- if (!totalLength)
1113
- return;
1075
+ if (!totalLength) return;
1114
1076
 
1115
1077
  // current integer cell we are in
1116
1078
  const pos = posStart.floor();
@@ -1344,14 +1306,14 @@ class RandomGenerator
1344
1306
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
1345
1307
  * a = vec2(5); // set a to (5, 5)
1346
1308
  * b = vec2(); // set b to (0, 0)
1347
- * @memberof Utilities */
1309
+ * @memberof Math */
1348
1310
  function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
1349
1311
 
1350
1312
  /**
1351
1313
  * Check if object is a valid Vector2
1352
1314
  * @param {any} v
1353
1315
  * @return {boolean}
1354
- * @memberof Utilities */
1316
+ * @memberof Math */
1355
1317
  function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
1356
1318
 
1357
1319
  // vector2 asserts
@@ -1601,7 +1563,7 @@ class Vector2
1601
1563
  * @param {number} [b=1] - blue
1602
1564
  * @param {number} [a=1] - alpha
1603
1565
  * @return {Color}
1604
- * @memberof Utilities
1566
+ * @memberof Math
1605
1567
  */
1606
1568
  function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1607
1569
 
@@ -1612,14 +1574,14 @@ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
1612
1574
  * @param {number} [l=1] - lightness
1613
1575
  * @param {number} [a=1] - alpha
1614
1576
  * @return {Color}
1615
- * @memberof Utilities */
1577
+ * @memberof Math */
1616
1578
  function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
1617
1579
 
1618
1580
  /**
1619
1581
  * Check if object is a valid Color
1620
1582
  * @param {any} c
1621
1583
  * @return {boolean}
1622
- * @memberof Utilities */
1584
+ * @memberof Math */
1623
1585
  function isColor(c) { return c instanceof Color && c.isValid(); }
1624
1586
 
1625
1587
  // color asserts
@@ -1800,7 +1762,7 @@ class Color
1800
1762
  toString(useAlpha = true)
1801
1763
  {
1802
1764
  if (debug && !this.isValid())
1803
- return `#000`;
1765
+ return '#000';
1804
1766
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
1805
1767
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1806
1768
  }
@@ -1857,67 +1819,67 @@ class Color
1857
1819
 
1858
1820
  /** Color - White #ffffff
1859
1821
  * @type {Color}
1860
- * @memberof Utilities */
1822
+ * @memberof Math */
1861
1823
  const WHITE = debugProtectConstant(rgb());
1862
1824
 
1863
1825
  /** Color - Clear White #757474ff with 0 alpha
1864
1826
  * @type {Color}
1865
- * @memberof Utilities */
1827
+ * @memberof Math */
1866
1828
  const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
1867
1829
 
1868
1830
  /** Color - Black #000000
1869
1831
  * @type {Color}
1870
- * @memberof Utilities */
1832
+ * @memberof Math */
1871
1833
  const BLACK = debugProtectConstant(rgb(0,0,0));
1872
1834
 
1873
1835
  /** Color - Clear Black #000000 with 0 alpha
1874
1836
  * @type {Color}
1875
- * @memberof Utilities */
1837
+ * @memberof Math */
1876
1838
  const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
1877
1839
 
1878
1840
  /** Color - Gray #808080
1879
1841
  * @type {Color}
1880
- * @memberof Utilities */
1842
+ * @memberof Math */
1881
1843
  const GRAY = debugProtectConstant(rgb(.5,.5,.5));
1882
1844
 
1883
1845
  /** Color - Red #ff0000
1884
1846
  * @type {Color}
1885
- * @memberof Utilities */
1847
+ * @memberof Math */
1886
1848
  const RED = debugProtectConstant(rgb(1,0,0));
1887
1849
 
1888
1850
  /** Color - Orange #ff8000
1889
1851
  * @type {Color}
1890
- * @memberof Utilities */
1852
+ * @memberof Math */
1891
1853
  const ORANGE = debugProtectConstant(rgb(1,.5,0));
1892
1854
 
1893
1855
  /** Color - Yellow #ffff00
1894
1856
  * @type {Color}
1895
- * @memberof Utilities */
1857
+ * @memberof Math */
1896
1858
  const YELLOW = debugProtectConstant(rgb(1,1,0));
1897
1859
 
1898
1860
  /** Color - Green #00ff00
1899
1861
  * @type {Color}
1900
- * @memberof Utilities */
1862
+ * @memberof Math */
1901
1863
  const GREEN = debugProtectConstant(rgb(0,1,0));
1902
1864
 
1903
1865
  /** Color - Cyan #00ffff
1904
1866
  * @type {Color}
1905
- * @memberof Utilities */
1867
+ * @memberof Math */
1906
1868
  const CYAN = debugProtectConstant(rgb(0,1,1));
1907
1869
 
1908
1870
  /** Color - Blue #0000ff
1909
1871
  * @type {Color}
1910
- * @memberof Utilities */
1872
+ * @memberof Math */
1911
1873
  const BLUE = debugProtectConstant(rgb(0,0,1));
1912
1874
 
1913
1875
  /** Color - Purple #8000ff
1914
1876
  * @type {Color}
1915
- * @memberof Utilities */
1877
+ * @memberof Math */
1916
1878
  const PURPLE = debugProtectConstant(rgb(.5,0,1));
1917
1879
 
1918
1880
  /** Color - Magenta #ff00ff
1919
1881
  * @type {Color}
1920
- * @memberof Utilities */
1882
+ * @memberof Math */
1921
1883
  const MAGENTA = debugProtectConstant(rgb(1,0,1));
1922
1884
 
1923
1885
  ///////////////////////////////////////////////////////////////////////////////
@@ -2003,6 +1965,84 @@ class Timer
2003
1965
  * @return {number} */
2004
1966
  valueOf() { return this.get(); }
2005
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
+ }
2006
2046
  /**
2007
2047
  * LittleJS Engine Settings
2008
2048
  * - All settings for the engine are here
@@ -2040,7 +2080,7 @@ let cameraScale = 32;
2040
2080
  * @memberof Settings */
2041
2081
  let canvasColorTiles = true;
2042
2082
 
2043
- /** Color to clear the canvas to before render
2083
+ /** Color to clear the canvas to before render, does not clear if alpha is 0
2044
2084
  * @type {Color}
2045
2085
  * @memberof Draw */
2046
2086
  let canvasClearColor = CLEAR_BLACK;
@@ -2349,7 +2389,7 @@ function setCameraScale(scale) { cameraScale = scale; }
2349
2389
  * @memberof Settings */
2350
2390
  function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2351
2391
 
2352
- /** 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
2353
2393
  * @param {Color} color
2354
2394
  * @memberof Settings */
2355
2395
  function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
@@ -3107,8 +3147,7 @@ class EngineObject
3107
3147
  /** Render debug info for this object */
3108
3148
  renderDebugInfo()
3109
3149
  {
3110
- if (!debug)
3111
- return;
3150
+ if (!debug) return;
3112
3151
 
3113
3152
  // show object info for debugging
3114
3153
  const size = vec2(max(this.size.x, .2), max(this.size.y, .2));
@@ -3150,11 +3189,6 @@ let mainCanvas;
3150
3189
  * @memberof Draw */
3151
3190
  let mainContext;
3152
3191
 
3153
- /** The default canvas to use for drawing, usually mainCanvas
3154
- * @type {HTMLCanvasElement|OffscreenCanvas}
3155
- * @memberof Draw */
3156
- let drawCanvas;
3157
-
3158
3192
  /** The default 2d context to use for drawing, usually mainContext
3159
3193
  * @type {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}
3160
3194
  * @memberof Draw */
@@ -3742,7 +3776,11 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3742
3776
  ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
3743
3777
 
3744
3778
  if (!screenSpace)
3745
- [pos, size, angle] = worldToScreenTransform(pos, size, angle);
3779
+ {
3780
+ pos = worldToScreen(pos);
3781
+ size = size.scale(cameraScale);
3782
+ angle -= cameraAngle;
3783
+ }
3746
3784
  context.save();
3747
3785
  context.translate(pos.x+.5, pos.y+.5);
3748
3786
  context.rotate(angle);
@@ -3794,9 +3832,9 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3794
3832
  * @param {string} [fontStyle]
3795
3833
  * @param {number} [maxWidth]
3796
3834
  * @param {number} [angle]
3797
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
3835
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3798
3836
  * @memberof Draw */
3799
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=mainContext)
3837
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
3800
3838
  {
3801
3839
  ASSERT(isString(text), 'text must be a string');
3802
3840
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3937,25 +3975,6 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
3937
3975
  ];
3938
3976
  }
3939
3977
 
3940
- /** Convert world space transform to screen space
3941
- * @param {Vector2} worldPos
3942
- * @param {Vector2} worldSize
3943
- * @param {number} [worldAngle]
3944
- * @return {[Vector2, Vector2, number]} - [pos, size, angle]
3945
- * @memberof Draw */
3946
- function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
3947
- {
3948
- ASSERT(isVector2(worldPos), 'worldPos must be a vec2');
3949
- ASSERT(isVector2(worldSize), 'worldSize must be a vec2');
3950
- ASSERT(isNumber(worldAngle), 'worldAngle must be a number');
3951
-
3952
- return [
3953
- worldToScreen(worldPos),
3954
- worldSize.scale(cameraScale),
3955
- worldAngle - cameraAngle
3956
- ];
3957
- }
3958
-
3959
3978
  /** Get the size of the camera window in world space
3960
3979
  * @return {Vector2}
3961
3980
  * @memberof Draw */
@@ -4178,7 +4197,7 @@ class FontImage
4178
4197
  * @param {boolean} [center]
4179
4198
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4180
4199
  */
4181
- drawTextScreen(text, pos, scale=4, center=true, context=mainContext)
4200
+ drawTextScreen(text, pos, scale=4, center=true, context=drawContext)
4182
4201
  {
4183
4202
  context.save();
4184
4203
  const size = this.tileSize;
@@ -4571,8 +4590,7 @@ function inputInit()
4571
4590
  }
4572
4591
  function onMouseDown(e)
4573
4592
  {
4574
- if (isTouchDevice && touchInputEnable)
4575
- return;
4593
+ if (isTouchDevice && touchInputEnable) return;
4576
4594
 
4577
4595
  // fix stalled audio requiring user interaction
4578
4596
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
@@ -4623,8 +4641,7 @@ function inputInit()
4623
4641
  let wasTouching;
4624
4642
  function handleTouch(e)
4625
4643
  {
4626
- if (!touchInputEnable)
4627
- return;
4644
+ if (!touchInputEnable) return;
4628
4645
 
4629
4646
  // route touch to gamepad
4630
4647
  if (touchGamepadEnable)
@@ -4688,8 +4705,7 @@ function inputInit()
4688
4705
  }
4689
4706
 
4690
4707
  // don't process touch gamepad if paused
4691
- if (paused)
4692
- return;
4708
+ if (paused) return;
4693
4709
 
4694
4710
  // get center of left and right sides
4695
4711
  const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
@@ -5375,8 +5391,7 @@ class SoundInstance
5375
5391
  /** Pause this sound instance */
5376
5392
  pause()
5377
5393
  {
5378
- if (this.isPaused())
5379
- return;
5394
+ if (this.isPaused()) return;
5380
5395
 
5381
5396
  // save current time and stop sound
5382
5397
  this.pausedTime = this.getCurrentTime();
@@ -5388,8 +5403,7 @@ class SoundInstance
5388
5403
  /** Unpauses this sound instance */
5389
5404
  resume()
5390
5405
  {
5391
- if (!this.isPaused())
5392
- return;
5406
+ if (!this.isPaused()) return;
5393
5407
 
5394
5408
  // restart sound from paused time
5395
5409
  this.start(this.pausedTime);
@@ -6127,12 +6141,11 @@ class TileLayer extends CanvasLayer
6127
6141
  if (!this.context) return;
6128
6142
 
6129
6143
  // save current render settings
6130
- /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6131
- this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6144
+ /** @type {[CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, Color]} */
6145
+ this.savedRenderSettings = [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor];
6132
6146
 
6133
6147
  // set the draw canvas and context to this layer
6134
6148
  // use camera settings to match this layer's canvas
6135
- drawCanvas = this.canvas;
6136
6149
  drawContext = this.context;
6137
6150
  cameraPos = this.size.scale(.5);
6138
6151
  const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
@@ -6142,8 +6155,8 @@ class TileLayer extends CanvasLayer
6142
6155
  if (clear)
6143
6156
  {
6144
6157
  // clear and set size
6145
- drawCanvas.width = mainCanvasSize.x;
6146
- drawCanvas.height = mainCanvasSize.y;
6158
+ this.canvas.width = mainCanvasSize.x;
6159
+ this.canvas.height = mainCanvasSize.y;
6147
6160
  }
6148
6161
 
6149
6162
  // disable smoothing for pixel art
@@ -6160,10 +6173,10 @@ class TileLayer extends CanvasLayer
6160
6173
 
6161
6174
  ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6162
6175
  glCopyToContext(drawContext);
6163
- //debugSaveCanvas(this.canvas);
6176
+ //saveCanvas(this.canvas);
6164
6177
 
6165
6178
  // set stuff back to normal
6166
- [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6179
+ [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6167
6180
  }
6168
6181
 
6169
6182
  /** Draw the tile at a given position in the tile grid
@@ -6231,8 +6244,7 @@ class TileCollisionLayer extends TileLayer
6231
6244
  /** Destroy this tile layer */
6232
6245
  destroy()
6233
6246
  {
6234
- if (this.destroyed)
6235
- return;
6247
+ if (this.destroyed) return;
6236
6248
 
6237
6249
  // remove from collision layers array and destroy
6238
6250
  const index = tileCollisionLayers.indexOf(this);
@@ -6769,10 +6781,11 @@ function medalsInit(saveName)
6769
6781
 
6770
6782
  // engine automatically renders medals
6771
6783
  engineAddPlugin(undefined, medalsRender);
6784
+
6785
+ // plugin functions
6772
6786
  function medalsRender()
6773
6787
  {
6774
- if (!medalsDisplayQueue.length)
6775
- return;
6788
+ if (!medalsDisplayQueue.length) return;
6776
6789
 
6777
6790
  // update first medal in queue
6778
6791
  const medal = medalsDisplayQueue[0];
@@ -6862,8 +6875,7 @@ class Medal
6862
6875
  /** Unlocks a medal if not already unlocked */
6863
6876
  unlock()
6864
6877
  {
6865
- if (medalsPreventUnlock || this.unlocked)
6866
- return;
6878
+ if (medalsPreventUnlock || this.unlocked) return;
6867
6879
 
6868
6880
  // save the medal
6869
6881
  ASSERT(medalsSaveName, 'save name must be set');
@@ -7096,8 +7108,7 @@ function glInit(rootElement)
7096
7108
 
7097
7109
  function glSetInstancedMode()
7098
7110
  {
7099
- if (!glPolyMode)
7100
- return;
7111
+ if (!glPolyMode) return;
7101
7112
 
7102
7113
  // setup instanced mode
7103
7114
  glFlush();
@@ -7130,8 +7141,7 @@ function glSetInstancedMode()
7130
7141
 
7131
7142
  function glSetPolyMode()
7132
7143
  {
7133
- if (glPolyMode)
7134
- return;
7144
+ if (glPolyMode) return;
7135
7145
 
7136
7146
  // setup poly mode
7137
7147
  glFlush();
@@ -7210,8 +7220,8 @@ function glClearCanvas()
7210
7220
  if (!glContext) return;
7211
7221
 
7212
7222
  // clear and set to same size as main canvas
7213
- glCanvas.width = drawCanvas.width;
7214
- glCanvas.height = drawCanvas.height;
7223
+ glCanvas.width = mainCanvasSize.x;
7224
+ glCanvas.height = mainCanvasSize.y;
7215
7225
  glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
7216
7226
  const color = canvasClearColor;
7217
7227
  if (color.a > 0)
@@ -7227,8 +7237,7 @@ function glClearCanvas()
7227
7237
  function glSetTexture(texture, wrap=false)
7228
7238
  {
7229
7239
  // must flush cache with the old texture to set a new one
7230
- if (!glContext || texture === glActiveTexture)
7231
- return;
7240
+ if (!glContext || texture === glActiveTexture) return;
7232
7241
 
7233
7242
  glFlush();
7234
7243
  glActiveTexture = texture;
@@ -7324,6 +7333,7 @@ function glCreateTexture(image)
7324
7333
  function glDeleteTexture(texture)
7325
7334
  {
7326
7335
  if (!glContext) return;
7336
+
7327
7337
  glContext.deleteTexture(texture);
7328
7338
  }
7329
7339
 
@@ -7408,8 +7418,7 @@ function glFlush()
7408
7418
  * @memberof WebGL */
7409
7419
  function glCopyToContext(context)
7410
7420
  {
7411
- if (!glEnable || !glContext)
7412
- return;
7421
+ if (!glEnable || !glContext) return;
7413
7422
 
7414
7423
  glFlush();
7415
7424
  context.drawImage(glCanvas, 0, 0);
@@ -9005,7 +9014,7 @@ class UIObject
9005
9014
  this.onUpdate();
9006
9015
 
9007
9016
  // unset active if disabled
9008
- if (this.disabled && this == uiSystem.activeObject)
9017
+ if (this.disabled && this === uiSystem.activeObject)
9009
9018
  uiSystem.activeObject = undefined;
9010
9019
 
9011
9020
  const wasHover = uiSystem.lastHoverObject === this;