littlejsengine 1.14.19 → 1.14.23

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 (51) hide show
  1. package/dist/littlejs.d.ts +118 -18
  2. package/dist/littlejs.esm.js +506 -199
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +493 -199
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +468 -200
  7. package/examples/electron/game.js +1 -41
  8. package/examples/electron/index.html +2 -2
  9. package/examples/htmlMenu/game.js +1 -1
  10. package/examples/index.html +12 -6
  11. package/examples/module/game.js +5 -7
  12. package/examples/particles/index.html +6 -9
  13. package/examples/platformer/gameCharacter.js +1 -1
  14. package/examples/platformer/gameEffects.js +7 -2
  15. package/examples/platformer/gameObjects.js +3 -3
  16. package/examples/shorts/animation.js +2 -2
  17. package/examples/shorts/base.html +4 -1
  18. package/examples/shorts/cameraDrag.js +22 -0
  19. package/examples/shorts/debugDraw.js +38 -0
  20. package/examples/shorts/fps.js +90 -0
  21. package/examples/shorts/helloWorld.js +1 -1
  22. package/examples/shorts/hillGlideGame.js +1 -1
  23. package/examples/shorts/landerGame.js +1 -1
  24. package/examples/shorts/postProcess.js +1 -1
  25. package/examples/shorts/sequencer.js +3 -3
  26. package/examples/shorts/spaceGame.js +1 -1
  27. package/examples/shorts/spriteAtlas.js +6 -6
  28. package/examples/shorts/starfield.js +1 -1
  29. package/examples/shorts/tileRaycast.js +39 -0
  30. package/examples/shorts/tiles.png +0 -0
  31. package/examples/shorts/tiltedView.js +1 -1
  32. package/examples/starter/game.js +5 -7
  33. package/examples/starter/index.html +2 -2
  34. package/examples/typescript/game.js +3 -2
  35. package/examples/typescript/game.ts +5 -7
  36. package/examples/uiSystem/game.js +2 -2
  37. package/package.json +1 -1
  38. package/src/engine.js +3 -3
  39. package/src/engineAudio.js +13 -13
  40. package/src/engineDebug.js +27 -0
  41. package/src/engineDraw.js +68 -20
  42. package/src/engineExport.js +13 -0
  43. package/src/engineInput.js +14 -16
  44. package/src/engineObject.js +31 -6
  45. package/src/engineParticles.js +36 -15
  46. package/src/engineRelease.js +2 -1
  47. package/src/engineSettings.js +20 -1
  48. package/src/engineTileLayer.js +62 -59
  49. package/src/engineUtilities.js +206 -58
  50. package/src/engineWebGL.js +13 -8
  51. package/examples/shorts/raycasting.js +0 -79
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.19';
36
+ const engineVersion = '1.14.24';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -516,7 +516,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
516
516
 
517
517
  /**
518
518
  * @callback ObjectCallbackFunction - Function that processes an object
519
- * @param {EngineObject} uiObjects
519
+ * @param {EngineObject} object
520
520
  * @memberof Engine
521
521
  */
522
522
 
@@ -564,7 +564,7 @@ function drawEngineSplashScreen(t)
564
564
  // background
565
565
  const p3 = percent(t, 1, .8);
566
566
  const p4 = percent(t, 0, .5);
567
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,Math.hypot(w,h)*.7);
567
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
568
568
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
569
569
  g.addColorStop(1,hsl(0,0,0,p3).toString());
570
570
  x.save();
@@ -755,7 +755,8 @@ function debugShowErrors(){}
755
755
  function debugVideoCaptureIsActive(){ return false; }
756
756
  function debugVideoCaptureStart (){}
757
757
  function debugVideoCaptureStop (){}
758
- function debugVideoCaptureUpdate(){}
758
+ function debugVideoCaptureUpdate(){}
759
+ function debugProtectConstant(o){ return o; }
759
760
  /**
760
761
  * LittleJS Utility Classes and Functions
761
762
  * - General purpose math library
@@ -766,7 +767,7 @@ function debugVideoCaptureUpdate(){}
766
767
  * @namespace Utilities
767
768
  */
768
769
 
769
- /** A shortcut to get Math.PI
770
+ /** The value of PI
770
771
  * @type {number}
771
772
  * @default Math.PI
772
773
  * @memberof Utilities */
@@ -776,25 +777,80 @@ const PI = Math.PI;
776
777
  * @param {number} value
777
778
  * @return {number}
778
779
  * @memberof Utilities */
779
- function abs(value) { return Math.abs(value); }
780
+ const abs = Math.abs;
781
+
782
+ /** Returns floored value of value passed in
783
+ * @param {number} value
784
+ * @return {number}
785
+ * @memberof Utilities */
786
+ const floor = Math.floor;
787
+
788
+ /** Returns ceiled value of value passed in
789
+ * @param {number} value
790
+ * @return {number}
791
+ * @memberof Utilities */
792
+ const ceil = Math.ceil;
793
+
794
+ /** Returns rounded value passed in
795
+ * @param {number} value
796
+ * @return {number}
797
+ * @memberof Utilities */
798
+ const round = Math.round;
780
799
 
781
800
  /** Returns lowest value passed in
782
801
  * @param {...number} values
783
802
  * @return {number}
784
803
  * @memberof Utilities */
785
- function min(...values) { return Math.min(...values); }
804
+ const min = Math.min;
786
805
 
787
806
  /** Returns highest value passed in
788
807
  * @param {...number} values
789
808
  * @return {number}
790
809
  * @memberof Utilities */
791
- function max(...values) { return Math.max(...values); }
810
+ const max = Math.max;
792
811
 
793
812
  /** Returns the sign of value passed in
794
813
  * @param {number} value
795
814
  * @return {number}
796
815
  * @memberof Utilities */
797
- function sign(value) { return Math.sign(value); }
816
+ const sign = Math.sign;
817
+
818
+ /** Returns hypotenuse of values passed in
819
+ * @param {...number} values
820
+ * @return {number}
821
+ * @memberof Utilities */
822
+ const hypot = Math.hypot;
823
+
824
+ /** Returns log2 of value passed in
825
+ * @param {number} value
826
+ * @return {number}
827
+ * @memberof Utilities */
828
+ const log2 = Math.log2;
829
+
830
+ /** Returns sin of value passed in
831
+ * @param {number} value
832
+ * @return {number}
833
+ * @memberof Utilities */
834
+ const sin = Math.sin;
835
+
836
+ /** Returns cos of value passed in
837
+ * @param {number} value
838
+ * @return {number}
839
+ * @memberof Utilities */
840
+ const cos = Math.cos;
841
+
842
+ /** Returns tan of value passed in
843
+ * @param {number} value
844
+ * @return {number}
845
+ * @memberof Utilities */
846
+ const tan = Math.tan;
847
+
848
+ /** Returns atan2 of values passed in
849
+ * @param {number} y
850
+ * @param {number} x
851
+ * @return {number}
852
+ * @memberof Utilities */
853
+ const atan2 = Math.atan2;
798
854
 
799
855
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
800
856
  * @param {number} dividend
@@ -899,7 +955,7 @@ function isPowerOfTwo(value) { return !(value & (value - 1)); }
899
955
  * @param {number} value
900
956
  * @return {number}
901
957
  * @memberof Utilities */
902
- function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
958
+ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
903
959
 
904
960
  /** Returns true if two axis aligned bounding boxes are overlapping
905
961
  * this can be used for simple collision detection between objects
@@ -967,7 +1023,7 @@ function isIntersecting(start, end, pos, size)
967
1023
  * @return {number} - Value waving between 0 and amplitude
968
1024
  * @memberof Utilities */
969
1025
  function wave(frequency=1, amplitude=1, t=time, offset=0)
970
- { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
1026
+ { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
971
1027
 
972
1028
  /** Formats seconds to mm:ss style for display purposes
973
1029
  * @param {number} t - time in seconds
@@ -1006,6 +1062,96 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1006
1062
  * @memberof Utilities */
1007
1063
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1008
1064
 
1065
+ /**
1066
+ * @callback LineTestFunction - Checks if a position is colliding
1067
+ * @param {Vector2} pos
1068
+ * @memberof Draw
1069
+ */
1070
+
1071
+ /**
1072
+ * Casts a ray and returns position of the first collision found, or undefined if none are found
1073
+ * @param {Vector2} posStart
1074
+ * @param {Vector2} posEnd
1075
+ * @param {LineTestFunction} testFunction - Check if colliding
1076
+ * @param {Vector2} [normal] - Optional vector to store the normal
1077
+ * @return {Vector2|undefined} - Position of the collision or undefined if none found
1078
+ * @memberof Utilities */
1079
+ function lineTest(posStart, posEnd, testFunction, normal)
1080
+ {
1081
+ ASSERT(isVector2(posStart), 'posStart must be a vec2');
1082
+ ASSERT(isVector2(posEnd), 'posEnd must be a vec2');
1083
+ ASSERT(typeof testFunction === 'function', 'testFunction must be a function');
1084
+ ASSERT(!normal || isVector2(normal), 'normal must be a vec2');
1085
+
1086
+ // get ray direction and length
1087
+ const dx = posEnd.x - posStart.x;
1088
+ const dy = posEnd.y - posStart.y;
1089
+ const totalLength = hypot(dx, dy);
1090
+ if (!totalLength)
1091
+ return;
1092
+
1093
+ // current integer cell we are in
1094
+ const pos = posStart.floor();
1095
+
1096
+ // normalize ray direction
1097
+ const dirX = dx / totalLength;
1098
+ const dirY = dy / totalLength;
1099
+
1100
+ // step direction in grid
1101
+ const stepX = sign(dirX);
1102
+ const stepY = sign(dirY);
1103
+
1104
+ // distance along the ray to cross one full cell in X or Y
1105
+ const tDeltaX = dirX ? abs(1 / dirX) : Infinity;
1106
+ const tDeltaY = dirY ? abs(1 / dirY) : Infinity;
1107
+
1108
+ // distance along the ray from start to the first grid boundary
1109
+ const nextGridX = stepX > 0 ? pos.x + 1 : pos.x;
1110
+ const nextGridY = stepY > 0 ? pos.y + 1 : pos.y;
1111
+ const tMaxX = dirX ? (nextGridX - posStart.x) / dirX : Infinity;
1112
+ const tMaxY = dirY ? (nextGridY - posStart.y) / dirY : Infinity;
1113
+
1114
+ // use line drawing algorithm to test for collisions
1115
+ let t = 0, tX = tMaxX, tY = tMaxY, wasX = tDeltaX < tDeltaY;
1116
+ while (t < totalLength)
1117
+ {
1118
+ if (testFunction(pos))
1119
+ {
1120
+ // set hit point
1121
+ const hitPos = vec2(posStart.x + dirX*t, posStart.y + dirY*t);
1122
+
1123
+ // move inside of tile if on positive edge
1124
+ const e = 1e-9;
1125
+ if (wasX)
1126
+ {
1127
+ if (stepX < 0)
1128
+ hitPos.x -= e;
1129
+ }
1130
+ if (stepY < 0)
1131
+ hitPos.y -= e;
1132
+
1133
+ // set normal
1134
+ if (normal)
1135
+ wasX ? normal.set(-stepX,0) : normal.set(0,-stepY);
1136
+ return hitPos;
1137
+ }
1138
+
1139
+ // advance to the next grid boundary
1140
+ if (wasX = tX < tY)
1141
+ {
1142
+ pos.x += stepX;
1143
+ t = tX;
1144
+ tX += tDeltaX;
1145
+ }
1146
+ else
1147
+ {
1148
+ pos.y += stepY;
1149
+ t = tY;
1150
+ tY += tDeltaY;
1151
+ }
1152
+ }
1153
+ }
1154
+
1009
1155
  ///////////////////////////////////////////////////////////////////////////////
1010
1156
 
1011
1157
  /** Random global functions
@@ -1024,7 +1170,7 @@ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valu
1024
1170
  * @param {number} [valueB]
1025
1171
  * @return {number}
1026
1172
  * @memberof Random */
1027
- function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
1173
+ function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
1028
1174
 
1029
1175
  /** Randomly returns true or false given the chance of true passed in
1030
1176
  * @param {number} [chance]
@@ -1103,7 +1249,7 @@ class RandomGenerator
1103
1249
  * @param {number} valueA
1104
1250
  * @param {number} [valueB]
1105
1251
  * @return {number} */
1106
- int(valueA, valueB=0) { return Math.floor(this.float(valueA, valueB)); }
1252
+ int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
1107
1253
 
1108
1254
  /** Randomly returns true or false given the chance of true passed in
1109
1255
  * @param {number} [chance]
@@ -1130,6 +1276,39 @@ class RandomGenerator
1130
1276
  * @return {Vector2} */
1131
1277
  vec2(valueA=1, valueB=0)
1132
1278
  { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
1279
+
1280
+ /** Returns a random color between the two passed in colors, combine components if linear
1281
+ * @param {Color} [colorA=(1,1,1,1)]
1282
+ * @param {Color} [colorB=(0,0,0,1)]
1283
+ * @param {boolean} [linear]
1284
+ * @return {Color} */
1285
+ randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1286
+ {
1287
+ return linear ? colorA.lerp(colorB, this.float()) :
1288
+ new Color(
1289
+ this.float(colorA.r,colorB.r),
1290
+ this.float(colorA.g,colorB.g),
1291
+ this.float(colorA.b,colorB.b),
1292
+ this.float(colorA.a,colorB.a));
1293
+ }
1294
+
1295
+ /** Returns a new color that has each component randomly adjusted
1296
+ * @param {Color} color
1297
+ * @param {number} [amount]
1298
+ * @param {number} [alphaAmount]
1299
+ * @return {Color} */
1300
+ mutateColor(color, amount=.05, alphaAmount=0)
1301
+ {
1302
+ ASSERT_NUMBER_VALID(amount);
1303
+ ASSERT_NUMBER_VALID(alphaAmount);
1304
+ return new Color
1305
+ (
1306
+ color.r + this.float(amount, -amount),
1307
+ color.g + this.float(amount, -amount),
1308
+ color.b + this.float(amount, -amount),
1309
+ color.a + this.float(alphaAmount, -alphaAmount)
1310
+ ).clamp();
1311
+ }
1133
1312
  }
1134
1313
 
1135
1314
  ///////////////////////////////////////////////////////////////////////////////
@@ -1281,7 +1460,7 @@ class Vector2
1281
1460
 
1282
1461
  /** Returns the clockwise angle of this vector, up is angle 0
1283
1462
  * @return {number} */
1284
- angle() { return Math.atan2(this.x, this.y); }
1463
+ angle() { return atan2(this.x, this.y); }
1285
1464
 
1286
1465
  /** Sets this vector with clockwise angle and length passed in
1287
1466
  * @param {number} [angle]
@@ -1291,8 +1470,8 @@ class Vector2
1291
1470
  {
1292
1471
  ASSERT_NUMBER_VALID(angle);
1293
1472
  ASSERT_NUMBER_VALID(length);
1294
- this.x = length*Math.sin(angle);
1295
- this.y = length*Math.cos(angle);
1473
+ this.x = length*sin(angle);
1474
+ this.y = length*cos(angle);
1296
1475
  return this;
1297
1476
  }
1298
1477
 
@@ -1302,7 +1481,7 @@ class Vector2
1302
1481
  rotate(angle)
1303
1482
  {
1304
1483
  ASSERT_NUMBER_VALID(angle);
1305
- const c = Math.cos(-angle), s = Math.sin(-angle);
1484
+ const c = cos(-angle), s = sin(-angle);
1306
1485
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1307
1486
  }
1308
1487
 
@@ -1334,7 +1513,7 @@ class Vector2
1334
1513
 
1335
1514
  /** Returns a copy of this vector with each axis floored
1336
1515
  * @return {Vector2} */
1337
- floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
1516
+ floor() { return new Vector2(floor(this.x), floor(this.y)); }
1338
1517
 
1339
1518
  /** Returns new vec2 with modded values
1340
1519
  * @param {number} [divisor]
@@ -1642,67 +1821,67 @@ class Color
1642
1821
  /** Color - White #ffffff
1643
1822
  * @type {Color}
1644
1823
  * @memberof Utilities */
1645
- const WHITE = protectEngineConstant(rgb());
1824
+ const WHITE = debugProtectConstant(rgb());
1646
1825
 
1647
1826
  /** Color - Clear White #757474ff with 0 alpha
1648
1827
  * @type {Color}
1649
1828
  * @memberof Utilities */
1650
- const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
1829
+ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
1651
1830
 
1652
1831
  /** Color - Black #000000
1653
1832
  * @type {Color}
1654
1833
  * @memberof Utilities */
1655
- const BLACK = protectEngineConstant(rgb(0,0,0));
1834
+ const BLACK = debugProtectConstant(rgb(0,0,0));
1656
1835
 
1657
1836
  /** Color - Clear Black #000000 with 0 alpha
1658
1837
  * @type {Color}
1659
1838
  * @memberof Utilities */
1660
- const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
1839
+ const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
1661
1840
 
1662
1841
  /** Color - Gray #808080
1663
1842
  * @type {Color}
1664
1843
  * @memberof Utilities */
1665
- const GRAY = protectEngineConstant(rgb(.5,.5,.5));
1844
+ const GRAY = debugProtectConstant(rgb(.5,.5,.5));
1666
1845
 
1667
1846
  /** Color - Red #ff0000
1668
1847
  * @type {Color}
1669
1848
  * @memberof Utilities */
1670
- const RED = protectEngineConstant(rgb(1,0,0));
1849
+ const RED = debugProtectConstant(rgb(1,0,0));
1671
1850
 
1672
1851
  /** Color - Orange #ff8000
1673
1852
  * @type {Color}
1674
1853
  * @memberof Utilities */
1675
- const ORANGE = protectEngineConstant(rgb(1,.5,0));
1854
+ const ORANGE = debugProtectConstant(rgb(1,.5,0));
1676
1855
 
1677
1856
  /** Color - Yellow #ffff00
1678
1857
  * @type {Color}
1679
1858
  * @memberof Utilities */
1680
- const YELLOW = protectEngineConstant(rgb(1,1,0));
1859
+ const YELLOW = debugProtectConstant(rgb(1,1,0));
1681
1860
 
1682
1861
  /** Color - Green #00ff00
1683
1862
  * @type {Color}
1684
1863
  * @memberof Utilities */
1685
- const GREEN = protectEngineConstant(rgb(0,1,0));
1864
+ const GREEN = debugProtectConstant(rgb(0,1,0));
1686
1865
 
1687
1866
  /** Color - Cyan #00ffff
1688
1867
  * @type {Color}
1689
1868
  * @memberof Utilities */
1690
- const CYAN = protectEngineConstant(rgb(0,1,1));
1869
+ const CYAN = debugProtectConstant(rgb(0,1,1));
1691
1870
 
1692
1871
  /** Color - Blue #0000ff
1693
1872
  * @type {Color}
1694
1873
  * @memberof Utilities */
1695
- const BLUE = protectEngineConstant(rgb(0,0,1));
1874
+ const BLUE = debugProtectConstant(rgb(0,0,1));
1696
1875
 
1697
1876
  /** Color - Purple #8000ff
1698
1877
  * @type {Color}
1699
1878
  * @memberof Utilities */
1700
- const PURPLE = protectEngineConstant(rgb(.5,0,1));
1879
+ const PURPLE = debugProtectConstant(rgb(.5,0,1));
1701
1880
 
1702
1881
  /** Color - Magenta #ff00ff
1703
1882
  * @type {Color}
1704
1883
  * @memberof Utilities */
1705
- const MAGENTA = protectEngineConstant(rgb(1,0,1));
1884
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1706
1885
 
1707
1886
  ///////////////////////////////////////////////////////////////////////////////
1708
1887
 
@@ -1765,41 +1944,11 @@ class Timer
1765
1944
 
1766
1945
  /** Returns this timer expressed as a string
1767
1946
  * @return {string} */
1768
- toString() { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
1947
+ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
1769
1948
 
1770
1949
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1771
1950
  * @return {number} */
1772
1951
  valueOf() { return this.get(); }
1773
- }
1774
-
1775
- ///////////////////////////////////////////////////////////////////////////////
1776
- // Helper functions used by the engine
1777
-
1778
- // make color constants immutable with debug assertions
1779
- function protectEngineConstant(obj)
1780
- {
1781
- if (debug)
1782
- {
1783
- // get properties and store original values
1784
- const props = Object.keys(obj), values = {};
1785
- props.forEach(prop => values[prop] = obj[prop]);
1786
-
1787
- // replace with getters/setters that assert
1788
- props.forEach(prop =>
1789
- {
1790
- Object.defineProperty(obj, prop, {
1791
- get: () => values[prop],
1792
- set: (value) =>
1793
- {
1794
- ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1795
- },
1796
- enumerable: true
1797
- });
1798
- });
1799
- }
1800
-
1801
- // freeze the object to prevent adding new properties
1802
- return Object.freeze(obj);
1803
1952
  }
1804
1953
  /**
1805
1954
  * LittleJS Engine Settings
@@ -2017,6 +2166,14 @@ let touchInputEnable = true;
2017
2166
  * @memberof Settings */
2018
2167
  let touchGamepadEnable = false;
2019
2168
 
2169
+ /** True if touch gamepad should have start button in the center
2170
+ * - When the game is paused, any touch will press the button
2171
+ * - This can function as a way to pause/unpause the game
2172
+ * @type {boolean}
2173
+ * @default
2174
+ * @memberof Settings */
2175
+ let touchGamepadCenterButton = true;
2176
+
2020
2177
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
2021
2178
  * @type {boolean}
2022
2179
  * @default
@@ -2192,9 +2349,14 @@ function setHeadlessMode(headless) { headlessMode = headless; }
2192
2349
  * @memberof Settings */
2193
2350
  function setGLEnable(enable)
2194
2351
  {
2352
+ if (enable && !glCanBeEnabled)
2353
+ {
2354
+ console.warn('Can not enable WebGL if it was disabled on start.');
2355
+ return;
2356
+ }
2195
2357
  glEnable = enable;
2196
2358
  if (glCanvas) // hide glCanvas if WebGL is disabled
2197
- glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2359
+ glCanvas.style.display = enable ? '' : 'none';
2198
2360
  }
2199
2361
 
2200
2362
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
@@ -2282,6 +2444,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
2282
2444
  * @memberof Settings */
2283
2445
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
2284
2446
 
2447
+ /** True if touch gamepad should have start button in the center
2448
+ * - This can function as a way to pause/unpause the game
2449
+ * @param {boolean} enable
2450
+ * @memberof Settings */
2451
+ function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
2452
+
2285
2453
  /** Set if touch gamepad should be analog stick or 8 way dpad
2286
2454
  * @param {boolean} analog
2287
2455
  * @memberof Settings */
@@ -2643,6 +2811,27 @@ class EngineObject
2643
2811
  // test which side we bounced off (or both if a corner)
2644
2812
  const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2645
2813
  const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2814
+
2815
+ if (blockedLayerX)
2816
+ {
2817
+ // try to move up a tiny bit
2818
+ const epsilon = 1e-3;
2819
+ const maxMoveUp = .1;
2820
+ const y = floor(oldPos.y-this.size.y/2+1) +
2821
+ this.size.y/2 + epsilon;
2822
+ const delta = y - this.pos.y;
2823
+ if (delta < maxMoveUp)
2824
+ if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
2825
+ {
2826
+ this.pos.y = y;
2827
+ debugPhysics && debugRect(this.pos, this.size, '#ff0');
2828
+ return;
2829
+ }
2830
+
2831
+ // move to previous position and bounce
2832
+ this.pos.x = oldPos.x;
2833
+ this.velocity.x *= -this.restitution;
2834
+ }
2646
2835
  if (blockedLayerY || !blockedLayerX)
2647
2836
  {
2648
2837
  // bounce velocity
@@ -2666,12 +2855,6 @@ class EngineObject
2666
2855
  this.groundObject = undefined;
2667
2856
  }
2668
2857
  }
2669
- if (blockedLayerX)
2670
- {
2671
- // move to previous position and bounce
2672
- this.pos.x = oldPos.x;
2673
- this.velocity.x *= -this.restitution;
2674
- }
2675
2858
  debugPhysics && debugRect(this.pos, this.size, '#f00');
2676
2859
  }
2677
2860
  }
@@ -2729,6 +2912,16 @@ class EngineObject
2729
2912
  */
2730
2913
  collideWithObject(object) { return true; }
2731
2914
 
2915
+ /** Get this object's up vector
2916
+ * @param {number} [scale] - length of the vector
2917
+ * @return {Vector2} */
2918
+ getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
2919
+
2920
+ /** Get this object's right vector
2921
+ * @param {number} [scale] - length of the vector
2922
+ * @return {Vector2} */
2923
+ getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
2924
+
2732
2925
  /** How long since the object was created
2733
2926
  * @return {number} */
2734
2927
  getAliveTime() { return time - this.spawnTime; }
@@ -3080,6 +3273,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3080
3273
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3081
3274
  if (useWebGL)
3082
3275
  {
3276
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3083
3277
  if (screenSpace)
3084
3278
  {
3085
3279
  // convert to world space
@@ -3175,6 +3369,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3175
3369
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3176
3370
  if (useWebGL)
3177
3371
  {
3372
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3178
3373
  if (screenSpace)
3179
3374
  {
3180
3375
  // convert to world space
@@ -3186,7 +3381,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3186
3381
  const halfSizeX = size.x/2, halfSizeY = size.y/2;
3187
3382
  const colorTopInt = colorTop.rgbaInt();
3188
3383
  const colorBottomInt = colorBottom.rgbaInt();
3189
- const c = Math.cos(-angle), s = Math.sin(-angle);
3384
+ const c = cos(-angle), s = sin(-angle);
3190
3385
  for (let i=4; i--;)
3191
3386
  {
3192
3387
  const x = i & 1 ? halfSizeX : -halfSizeX;
@@ -3237,6 +3432,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3237
3432
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3238
3433
  if (useWebGL)
3239
3434
  {
3435
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3240
3436
  let scale = 1;
3241
3437
  if (screenSpace)
3242
3438
  {
@@ -3286,6 +3482,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3286
3482
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
3287
3483
  const size = vec2(width, halfDelta.length()*2);
3288
3484
  pos = pos.add(posA.add(halfDelta));
3485
+ if (screenSpace)
3486
+ halfDelta.y *= -1; // flip angle Y if screen space
3289
3487
  angle += halfDelta.angle();
3290
3488
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3291
3489
  }
@@ -3313,7 +3511,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3313
3511
  for (let i=sides; i--;)
3314
3512
  {
3315
3513
  const a = (i/sides)*PI*2;
3316
- points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
3514
+ points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
3317
3515
  }
3318
3516
  drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3319
3517
  }
@@ -3340,6 +3538,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3340
3538
 
3341
3539
  if (useWebGL)
3342
3540
  {
3541
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3343
3542
  let scale = 1;
3344
3543
  if (screenSpace)
3345
3544
  {
@@ -3561,18 +3760,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3561
3760
  * @memberof Draw */
3562
3761
  function screenToWorld(screenPos)
3563
3762
  {
3564
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3565
- let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3763
+ let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3764
+ let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3566
3765
  if (cameraAngle)
3567
3766
  {
3568
3767
  // apply camera rotation
3569
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
3570
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3571
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3572
- cameraPosRelativeX = rotatedX;
3573
- cameraPosRelativeY = rotatedY;
3768
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
3769
+ const rotatedX = x * c - y * s;
3770
+ const rotatedY = x * s + y * c;
3771
+ x = rotatedX;
3772
+ y = rotatedY;
3574
3773
  }
3575
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
3774
+ return new Vector2(x + cameraPos.x, y + cameraPos.y);
3576
3775
  }
3577
3776
 
3578
3777
  /** Convert from world to screen space coordinates
@@ -3581,24 +3780,64 @@ function screenToWorld(screenPos)
3581
3780
  * @memberof Draw */
3582
3781
  function worldToScreen(worldPos)
3583
3782
  {
3584
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
3585
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
3783
+ let x = worldPos.x - cameraPos.x;
3784
+ let y = worldPos.y - cameraPos.y;
3586
3785
  if (cameraAngle)
3587
3786
  {
3588
3787
  // apply inverse camera rotation
3589
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
3590
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3591
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3592
- cameraPosRelativeX = rotatedX;
3593
- cameraPosRelativeY = rotatedY;
3788
+ const c = cos(cameraAngle), s = sin(cameraAngle);
3789
+ const rotatedX = x * c - y * s;
3790
+ const rotatedY = x * s + y * c;
3791
+ x = rotatedX;
3792
+ y = rotatedY;
3594
3793
  }
3595
3794
  return new Vector2
3596
3795
  (
3597
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
3598
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
3796
+ x * cameraScale + mainCanvasSize.x/2 - .5,
3797
+ y * -cameraScale + mainCanvasSize.y/2 - .5
3599
3798
  );
3600
3799
  }
3601
3800
 
3801
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
3802
+ * @param {Vector2} screenDelta
3803
+ * @return {Vector2}
3804
+ * @memberof Draw */
3805
+ function screenToWorldDelta(screenDelta)
3806
+ {
3807
+ let x = screenDelta.x / cameraScale;
3808
+ let y = screenDelta.y / -cameraScale;
3809
+ if (cameraAngle)
3810
+ {
3811
+ // apply camera rotation
3812
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
3813
+ const rotatedX = x * c - y * s;
3814
+ const rotatedY = x * s + y * c;
3815
+ x = rotatedX;
3816
+ y = rotatedY;
3817
+ }
3818
+ return new Vector2(x, y);
3819
+ }
3820
+
3821
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
3822
+ * @param {Vector2} worldDelta
3823
+ * @return {Vector2}
3824
+ * @memberof Draw */
3825
+ function worldToScreenDelta(worldDelta)
3826
+ {
3827
+ let x = worldDelta.x;
3828
+ let y = worldDelta.y;
3829
+ if (cameraAngle)
3830
+ {
3831
+ // apply inverse camera rotation
3832
+ const c = cos(cameraAngle), s = sin(cameraAngle);
3833
+ const rotatedX = x * c - y * s;
3834
+ const rotatedY = x * s + y * c;
3835
+ x = rotatedX;
3836
+ y = rotatedY;
3837
+ }
3838
+ return new Vector2(x * cameraScale, y * -cameraScale);
3839
+ }
3840
+
3602
3841
  /** Get the camera's visible area in world space
3603
3842
  * @return {Vector2}
3604
3843
  * @memberof Draw */
@@ -3651,6 +3890,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3651
3890
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
3652
3891
  const sx2 = bleedScale;
3653
3892
  const sy2 = bleedScale;
3893
+ sWidth = max(1,sWidth|0);
3894
+ sHeight = max(1,sHeight|0);
3654
3895
  const sWidth2 = sWidth - 2*bleedScale;
3655
3896
  const sHeight2 = sHeight - 2*bleedScale;
3656
3897
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
@@ -3665,7 +3906,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3665
3906
  // copy to offscreen canvas
3666
3907
  workCanvas.width = sWidth;
3667
3908
  workCanvas.height = sHeight;
3668
- workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
3909
+ workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
3669
3910
 
3670
3911
  // tint image using offscreen work context
3671
3912
  const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
@@ -4002,9 +4243,9 @@ function inputUpdate()
4002
4243
  if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
4003
4244
  inputClear();
4004
4245
 
4005
- // update mouse world space position
4246
+ // update mouse world space position and delta
4006
4247
  mousePos = screenToWorld(mousePosScreen);
4007
- mouseDelta = mouseDeltaScreen.multiply(vec2(1,-1)).rotate(-cameraAngle);
4248
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
4008
4249
 
4009
4250
  // update gamepads if enabled
4010
4251
  gamepadsUpdate();
@@ -4036,7 +4277,6 @@ function inputInit()
4036
4277
  document.addEventListener('wheel', onMouseWheel);
4037
4278
  document.addEventListener('contextmenu', onContextMenu);
4038
4279
  document.addEventListener('blur', onBlur);
4039
- document.addEventListener('mouseleave', onMouseLeave);
4040
4280
 
4041
4281
  // init touch input
4042
4282
  if (isTouchDevice && touchInputEnable)
@@ -4078,7 +4318,10 @@ function inputInit()
4078
4318
 
4079
4319
  isUsingGamepad = false;
4080
4320
  inputData[0][e.button] = 3;
4321
+
4322
+ let mousePosScreenLast = mousePosScreen;
4081
4323
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4324
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4082
4325
  inputPreventDefault && e.button && e.preventDefault();
4083
4326
  }
4084
4327
  function onMouseUp(e)
@@ -4089,18 +4332,13 @@ function inputInit()
4089
4332
  }
4090
4333
  function onMouseMove(e)
4091
4334
  {
4335
+ let mousePosScreenLast = mousePosScreen;
4092
4336
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4093
- mouseDeltaScreen = mouseDeltaScreen.add(vec2(e.movementX, e.movementY));
4337
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4094
4338
  }
4095
4339
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4096
4340
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4097
4341
  function onBlur() { inputClear(); } // reset input when focus is lost
4098
- function onMouseLeave()
4099
- {
4100
- // set mouse position and delta when leaving canvas
4101
- mousePosScreen = vec2(-1);
4102
- mouseDeltaScreen = vec2(0);
4103
- }
4104
4342
  }
4105
4343
 
4106
4344
  // convert a mouse or touch event position to screen space
@@ -4144,8 +4382,8 @@ function gamepadsUpdate()
4144
4382
  else if (touchGamepadStick.lengthSquared() > .3)
4145
4383
  {
4146
4384
  // convert to 8 way dpad
4147
- sticks[0].x = Math.round(touchGamepadStick.x);
4148
- sticks[0].y = -Math.round(touchGamepadStick.y);
4385
+ sticks[0].x = round(touchGamepadStick.x);
4386
+ sticks[0].y = -round(touchGamepadStick.y);
4149
4387
  sticks[0] = sticks[0].clampLength();
4150
4388
  }
4151
4389
 
@@ -4273,11 +4511,11 @@ function touchInputInit()
4273
4511
  {
4274
4512
  // set event pos and pass it along
4275
4513
  const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
4276
- const lastMousePosScreen = mousePosScreen;
4514
+ const mousePosScreenLast = mousePosScreen;
4277
4515
  mousePosScreen = mouseEventToScreen(pos);
4278
4516
  if (wasTouching)
4279
4517
  {
4280
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(lastMousePosScreen));
4518
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4281
4519
  isUsingGamepad = touchGamepadEnable;
4282
4520
  }
4283
4521
  else
@@ -4309,7 +4547,7 @@ function touchInputInit()
4309
4547
  if (touching)
4310
4548
  {
4311
4549
  touchGamepadTimer.set();
4312
- if (paused && !wasTouching)
4550
+ if (touchGamepadCenterButton && !wasTouching && paused)
4313
4551
  {
4314
4552
  // touch anywhere to press start when paused
4315
4553
  touchGamepadButtons[9] = 1;
@@ -4348,7 +4586,8 @@ function touchInputInit()
4348
4586
  if (button < touchGamepadButtonCount)
4349
4587
  touchGamepadButtons[button] = 1;
4350
4588
  }
4351
- else if (startCenter.distance(touchPos) < touchGamepadSize && !wasTouching)
4589
+ else if (touchGamepadCenterButton && !wasTouching &&
4590
+ startCenter.distance(touchPos) < touchGamepadSize)
4352
4591
  {
4353
4592
  // virtual start button in center
4354
4593
  touchGamepadButtons[9] = 1;
@@ -5044,10 +5283,10 @@ function zzfxG
5044
5283
 
5045
5284
  // biquad LP/HP filter
5046
5285
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
5047
- cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
5048
- a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
5049
- b0 = (1 + sign(filter) * cos) / 2 / a0,
5050
- b1 = -(sign(filter) + cos) / a0, b2 = b0,
5286
+ cosw = cos(w), alpha = sin(w) / 2 / quality,
5287
+ a0 = 1 + alpha, a1 = -2*cosw / a0, a2 = (1 - alpha) / a0,
5288
+ b0 = (1 + sign(filter) * cosw) / 2 / a0,
5289
+ b1 = -(sign(filter) + cosw) / a0, b2 = b0,
5051
5290
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
5052
5291
 
5053
5292
  // scale by sample rate
@@ -5070,15 +5309,15 @@ function zzfxG
5070
5309
  if (!(++crush%(bitCrush*100|0))) // bit crush
5071
5310
  {
5072
5311
  s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
5073
- (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
5074
- Math.sin(t**3) : // 4 noise
5075
- Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
5076
- 1-(2*t/PI2%2+2)%2: // 2 saw
5077
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
5078
- Math.sin(t); // 0 sin
5312
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
5313
+ sin(t**3) : // 4 noise
5314
+ max(min(tan(t),1),-1): // 3 tan
5315
+ 1-(2*t/PI2%2+2)%2: // 2 saw
5316
+ 1-4*abs(round(t/PI2)-t/PI2): // 1 triangle
5317
+ sin(t); // 0 sin
5079
5318
 
5080
5319
  s = (repeatTime ?
5081
- 1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
5320
+ 1 - tremolo + tremolo*sin(PI2*i/repeatTime) // tremolo
5082
5321
  : 1) *
5083
5322
  (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
5084
5323
  (i < attack ? i/attack : // attack
@@ -5100,8 +5339,8 @@ function zzfxG
5100
5339
  }
5101
5340
 
5102
5341
  f = (frequency += slide += deltaSlide) *// frequency
5103
- Math.cos(modulation*modOffset++); // modulation
5104
- t += f + f*noise*Math.sin(i**5); // noise
5342
+ cos(modulation*modOffset++); // modulation
5343
+ t += f + f*noise*sin(i**5); // noise
5105
5344
 
5106
5345
  if (jump && ++jump > pitchJumpTime) // pitch jump
5107
5346
  {
@@ -5167,23 +5406,22 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5167
5406
  }
5168
5407
  }
5169
5408
 
5170
- /** Return the center of first tile hit, undefined if nothing was hit.
5171
- * This does not return the exact intersection, but the center of the tile hit.
5409
+ /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
5172
5410
  * @param {Vector2} posStart
5173
5411
  * @param {Vector2} posEnd
5174
5412
  * @param {EngineObject} [object] - An object or undefined for generic test
5413
+ * @param {Vector2} [normal] - Optional normal of the surface hit
5175
5414
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5176
- * @return {Vector2}
5415
+ * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
5177
5416
  * @memberof TileLayers */
5178
- function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5417
+ function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
5179
5418
  {
5180
5419
  for (const layer of tileCollisionLayers)
5181
5420
  {
5182
5421
  if (!solidOnly || layer.isSolid)
5183
5422
  {
5184
- const hitPos = layer.collisionRaycast(posStart, posEnd, object)
5185
- if (hitPos)
5186
- return hitPos;
5423
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
5424
+ if (hitPos) return hitPos;
5187
5425
  }
5188
5426
  }
5189
5427
  }
@@ -5321,6 +5559,8 @@ class CanvasLayer extends EngineObject
5321
5559
  /** @property {TextureInfo} - Texture info to use for this object rendering */
5322
5560
  const useWebGL = false; // do not use webgl by default
5323
5561
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
5562
+ /** @property {boolean} - True if WebGL texture needs to be refreshed */
5563
+ this.refreshWebGL = false;
5324
5564
 
5325
5565
  // disable physics by default
5326
5566
  this.mass = this.gravityScale = this.friction = this.restitution = 0;
@@ -5354,8 +5594,15 @@ class CanvasLayer extends EngineObject
5354
5594
  * @memberof Draw */
5355
5595
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5356
5596
  {
5357
- // draw the canvas layer as a single tile that uses the whole texture
5358
5597
  const useWebGL = glEnable && this.textureInfo.hasWebGL();
5598
+ if (useWebGL && this.refreshWebGL)
5599
+ {
5600
+ // update the WebGL texture
5601
+ this.textureInfo.createWebGLTexture();
5602
+ this.refreshWebGL = false;
5603
+ }
5604
+
5605
+ // draw the canvas layer as a single tile that uses the whole texture
5359
5606
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5360
5607
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5361
5608
  }
@@ -5425,9 +5672,18 @@ class CanvasLayer extends EngineObject
5425
5672
  { this.drawTile(pos, size, undefined, color, angle); }
5426
5673
 
5427
5674
  /** Create or update the WebGL texture for this layer
5428
- * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5429
- useWebGL(enable=true)
5675
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture
5676
+ * @param {boolean} [immediate] - shoulkd the texture be updated immediately
5677
+ */
5678
+ useWebGL(enable=true, immediate=false)
5430
5679
  {
5680
+ if (!immediate && enable && this.textureInfo.hasWebGL())
5681
+ {
5682
+ // refresh the texture when needed
5683
+ this.refreshWebGL = true;
5684
+ return;
5685
+ }
5686
+
5431
5687
  if (enable)
5432
5688
  this.textureInfo.createWebGLTexture();
5433
5689
  else
@@ -5458,7 +5714,7 @@ class TileLayer extends CanvasLayer
5458
5714
  */
5459
5715
  constructor(position, size, tileInfo=tile(), renderOrder=0)
5460
5716
  {
5461
- const canvasSize = size.multiply(tileInfo.size);
5717
+ const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
5462
5718
  super(position, size, 0, renderOrder, canvasSize);
5463
5719
 
5464
5720
  // set tile info
@@ -5511,6 +5767,13 @@ class TileLayer extends CanvasLayer
5511
5767
  {
5512
5768
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5513
5769
 
5770
+ if (this.refreshWebGL)
5771
+ {
5772
+ // update the WebGL texture
5773
+ this.textureInfo.createWebGLTexture();
5774
+ this.refreshWebGL = false;
5775
+ }
5776
+
5514
5777
  // draw the tile layer as a single tile
5515
5778
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5516
5779
  const size = this.drawSize || this.size;
@@ -5547,8 +5810,9 @@ class TileLayer extends CanvasLayer
5547
5810
  drawCanvas = this.canvas;
5548
5811
  drawContext = this.context;
5549
5812
  cameraPos = this.size.scale(.5);
5550
- cameraScale = this.tileInfo.size.x;
5551
- mainCanvasSize = this.size.multiply(this.tileInfo.size);
5813
+ const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
5814
+ cameraScale = tileSize.x;
5815
+ mainCanvasSize = this.size.multiply(tileSize);
5552
5816
  if (clear)
5553
5817
  {
5554
5818
  // clear and set size
@@ -5708,66 +5972,44 @@ class TileCollisionLayer extends TileLayer
5708
5972
  // check if the object should collide with this tile
5709
5973
  const tileData = this.collisionData[y*this.size.x+x];
5710
5974
  if (tileData)
5711
- if (!object || object.collideWithTile(tileData, hitPos.set(x, y)))
5975
+ if (!object || object.collideWithTile(tileData,
5976
+ hitPos.set(x + this.pos.x, y + this.pos.y)))
5712
5977
  return true;
5713
5978
  }
5714
5979
  return false;
5715
5980
  }
5716
5981
 
5717
- /** Return the center of first tile hit, undefined if nothing was hit.
5718
- * This does not return the exact intersection, but the center of the tile hit.
5982
+ /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
5719
5983
  * @param {Vector2} posStart
5720
5984
  * @param {Vector2} posEnd
5721
- * @param {EngineObject} [object]
5722
- * @return {Vector2} */
5723
- collisionRaycast(posStart, posEnd, object)
5985
+ * @param {EngineObject} [object] - An object or undefined for generic test
5986
+ * @param {Vector2} [normal] - Optional normal of the surface hit
5987
+ * @return {Vector2|undefined} */
5988
+ collisionRaycast(posStart, posEnd, object, normal)
5724
5989
  {
5725
5990
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
5726
5991
  ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
5727
-
5728
- // transform to local layer space
5729
- const posStartX = posStart.x - this.pos.x;
5730
- const posStartY = posStart.y - this.pos.y;
5731
- const posEndX = posEnd.x - this.pos.x;
5732
- const posEndY = posEnd.y - this.pos.y;
5733
-
5734
- // test if a ray collides with tiles from start to end
5735
- const deltaX = posEndX - posStartX;
5736
- const deltaY = posEndY - posStartY;
5737
- const totalLength = (deltaX**2 + deltaY**2)**.5;
5738
- const unitX = abs(totalLength/deltaX);
5739
- const unitY = abs(totalLength/deltaY);
5740
-
5741
- // setup iteration variables
5742
- const pos = posStart.floor(), signDeltaX = sign(deltaX), signDeltaY = sign(deltaY);
5743
- let xi = unitX * (deltaX < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1) || 0;
5744
- let yi = unitY * (deltaY < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1) || 0;
5745
-
5746
- // use line drawing algorithm to test for collisions
5747
- while (true)
5992
+
5993
+ const localPos = new Vector2;
5994
+ const collisionTest = (pos)=>
5748
5995
  {
5749
5996
  // check for tile collision
5750
- const tileData = this.getCollisionData(pos);
5751
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
5752
- {
5753
- pos.x += .5; pos.y += .5;
5754
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
5755
- debugRaycast && debugPoint(pos, '#ff0');
5756
- return pos;
5757
- }
5758
-
5759
- // check if past the end
5760
- if (xi >= totalLength && yi >= totalLength)
5761
- break;
5762
-
5763
- // get coordinates of next tile to check
5764
- if (xi > yi)
5765
- pos.y += signDeltaY, yi += unitY;
5766
- else
5767
- pos.x += signDeltaX, xi += unitX;
5997
+ localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
5998
+ const tileData = this.getCollisionData(localPos);
5999
+ return tileData && (!object || object.collideWithTile(tileData, pos));
5768
6000
  }
5769
-
5770
6001
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
6002
+ const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
6003
+ if (hitPos)
6004
+ {
6005
+ const tilePos = hitPos.floor().add(vec2(.5));
6006
+ debugRaycast && debugRect(tilePos, vec2(1), '#f008');
6007
+ debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
6008
+ debugRaycast && debugPoint(hitPos, '#0f0');
6009
+ debugRaycast && normal &&
6010
+ debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
6011
+ return hitPos;
6012
+ }
5771
6013
  }
5772
6014
  }
5773
6015
  /**
@@ -5922,6 +6164,12 @@ class ParticleEmitter extends EngineObject
5922
6164
  this.particleCreateCallback = undefined;
5923
6165
  /** @property {number} - Track particle emit time */
5924
6166
  this.emitTimeBuffer = 0;
6167
+ /** @property {number} - Percentage of velocity to pass to particles (0-1) */
6168
+ this.velocityInheritance = 0;
6169
+
6170
+ // track previous position and angle
6171
+ this.previousAngle = this.angle;
6172
+ this.previousPos = this.pos.copy();
5925
6173
  }
5926
6174
 
5927
6175
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
@@ -5930,6 +6178,18 @@ class ParticleEmitter extends EngineObject
5930
6178
  // only do default update to apply parent transforms
5931
6179
  this.parent && super.update();
5932
6180
 
6181
+ if (this.velocityInheritance)
6182
+ {
6183
+ // pass emitter velocity to particles
6184
+ const p = this.velocityInheritance;
6185
+ this.velocity.x = p * (this.pos.x - this.previousPos.x);
6186
+ this.velocity.y = p * (this.pos.y - this.previousPos.y);
6187
+ this.angleVelocity = p * (this.angle - this.previousAngle);
6188
+ this.previousAngle = this.angle;
6189
+ this.previousPos.x = this.pos.x;
6190
+ this.previousPos.y = this.pos.y;
6191
+ }
6192
+
5933
6193
  // update emitter
5934
6194
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
5935
6195
  {
@@ -5987,7 +6247,7 @@ class ParticleEmitter extends EngineObject
5987
6247
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
5988
6248
  particle.velocity = vec2().setAngle(velocityAngle, speed);
5989
6249
  particle.angleVelocity = angleSpeed;
5990
- if (!this.localSpace)
6250
+ if (!this.localSpace && this.velocityInheritance > 0)
5991
6251
  {
5992
6252
  // apply emitter velocity to particle
5993
6253
  particle.velocity.x += this.velocity.x;
@@ -6085,6 +6345,16 @@ class Particle extends EngineObject
6085
6345
  this.velocity.y *= s;
6086
6346
  }
6087
6347
  }
6348
+
6349
+ if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
6350
+ {
6351
+ // destroy particle when its time runs out
6352
+ const c = this.colorEnd;
6353
+ this.color.set(c.r, c.g, c.b, c.a);
6354
+ this.size.set(this.sizeEnd, this.sizeEnd);
6355
+ this.destroyCallback && this.destroyCallback(this);
6356
+ this.destroyed = 1;
6357
+ }
6088
6358
  }
6089
6359
 
6090
6360
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -6093,7 +6363,7 @@ class Particle extends EngineObject
6093
6363
  // lerp color and size
6094
6364
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6095
6365
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6096
- this.size.x = this.size.y = radius;
6366
+ const size = vec2(radius);
6097
6367
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6098
6368
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6099
6369
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
@@ -6113,7 +6383,7 @@ class Particle extends EngineObject
6113
6383
  {
6114
6384
  // in local space of emitter
6115
6385
  const a = this.localSpaceEmitter.angle;
6116
- const c = Math.cos(a), s = Math.sin(a);
6386
+ const c = cos(a), s = sin(a);
6117
6387
  pos = this.localSpaceEmitter.pos.add(
6118
6388
  new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6119
6389
  angle += this.localSpaceEmitter.angle;
@@ -6129,22 +6399,15 @@ class Particle extends EngineObject
6129
6399
  {
6130
6400
  // stretch in direction of motion
6131
6401
  const trailLength = speed * this.trailScale;
6132
- this.size.y = max(this.size.x, trailLength);
6133
- angle = Math.atan2(direction.x, direction.y);
6134
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6402
+ size.y = max(size.x, trailLength);
6403
+ angle = atan2(direction.x, direction.y);
6404
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6135
6405
  }
6136
6406
  }
6137
6407
  else
6138
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6408
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6139
6409
  this.additive && setBlendMode();
6140
- debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
6141
-
6142
- if (p1 === 1)
6143
- {
6144
- // destroy particle when its time runs out
6145
- this.destroyCallback && this.destroyCallback(this);
6146
- this.destroyed = 1;
6147
- }
6410
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
6148
6411
  }
6149
6412
  }
6150
6413
  /**
@@ -6367,7 +6630,7 @@ let glContext;
6367
6630
  let glAntialias = true;
6368
6631
 
6369
6632
  // WebGL internal variables not exposed to documentation
6370
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos;
6633
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
6371
6634
 
6372
6635
  // WebGL internal constants
6373
6636
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6386,7 +6649,11 @@ function glInit()
6386
6649
  // keep set of texture infos so they can be restored if context is lost
6387
6650
  glTextureInfos = new Set;
6388
6651
 
6389
- if (!glEnable || headlessMode) return;
6652
+ if (!glEnable || headlessMode)
6653
+ {
6654
+ glCanBeEnabled = false;
6655
+ return;
6656
+ }
6390
6657
 
6391
6658
  // create the canvas and textures
6392
6659
  glCanvas = document.createElement('canvas');
@@ -6397,10 +6664,11 @@ function glInit()
6397
6664
  console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
6398
6665
  glCanvas = glContext = undefined;
6399
6666
  glEnable = false;
6667
+ glCanBeEnabled = false;
6400
6668
  return;
6401
6669
  }
6402
6670
 
6403
- // create the WebGL canvas
6671
+ // attach the WebGL canvas
6404
6672
  const rootElement = mainCanvas.parentElement;
6405
6673
  rootElement.appendChild(glCanvas);
6406
6674
 
@@ -6423,7 +6691,7 @@ function glInit()
6423
6691
  });
6424
6692
  glCanvas.addEventListener('webglcontextrestored', ()=>
6425
6693
  {
6426
- glEnable = true; // disable WebGL rendering
6694
+ glEnable = true; // re-enable WebGL rendering
6427
6695
  glCanvas.style.display = ''; // show the gl canvas
6428
6696
  LOG('WebGL context restored, reinitializing...');
6429
6697
 
@@ -6575,8 +6843,8 @@ function glPreRender()
6575
6843
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
6576
6844
  const rotatedCam = cameraPos.rotate(-cameraAngle);
6577
6845
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
6578
- const ca = Math.cos(cameraAngle);
6579
- const sa = Math.sin(cameraAngle);
6846
+ const ca = cos(cameraAngle);
6847
+ const sa = sin(cameraAngle);
6580
6848
  const transform = [
6581
6849
  s.x * ca, s.y * sa, 0, 0,
6582
6850
  -s.x * sa, s.y * ca, 0, 0,
@@ -6881,8 +7149,8 @@ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
6881
7149
  // transform the point
6882
7150
  const px = p.x*sx;
6883
7151
  const py = p.y*sy;
6884
- const sa = Math.sin(-angle);
6885
- const ca = Math.cos(-angle);
7152
+ const sa = sin(-angle);
7153
+ const ca = cos(-angle);
6886
7154
  pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
6887
7155
  }
6888
7156
  const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;