littlejsengine 1.14.16 → 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 (62) hide show
  1. package/dist/littlejs.d.ts +173 -50
  2. package/dist/littlejs.esm.js +892 -419
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +879 -419
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +854 -420
  7. package/examples/box2d/game.js +3 -2
  8. package/examples/box2d/tiles.png +0 -0
  9. package/examples/breakout/game.js +5 -5
  10. package/examples/electron/game.js +1 -41
  11. package/examples/electron/index.html +2 -2
  12. package/examples/htmlMenu/game.js +1 -1
  13. package/examples/index.html +69 -55
  14. package/examples/module/game.js +6 -8
  15. package/examples/particles/index.html +6 -9
  16. package/examples/platformer/gameCharacter.js +1 -1
  17. package/examples/platformer/gameEffects.js +8 -3
  18. package/examples/platformer/gameObjects.js +3 -3
  19. package/examples/shorts/animation.js +2 -2
  20. package/examples/shorts/base.html +4 -1
  21. package/examples/shorts/cameraDrag.js +22 -0
  22. package/examples/shorts/debugDraw.js +38 -0
  23. package/examples/shorts/fps.js +90 -0
  24. package/examples/shorts/helloWorld.js +1 -1
  25. package/examples/shorts/hillGlideGame.js +1 -1
  26. package/examples/shorts/landerGame.js +1 -1
  27. package/examples/shorts/musicPlayer.js +1 -0
  28. package/examples/shorts/parallax.js +1 -1
  29. package/examples/shorts/postProcess.js +1 -1
  30. package/examples/shorts/sequencer.js +5 -5
  31. package/examples/shorts/shader.js +29 -0
  32. package/examples/shorts/spaceGame.js +1 -1
  33. package/examples/shorts/spriteAtlas.js +6 -6
  34. package/examples/shorts/starfield.js +1 -1
  35. package/examples/shorts/tileLayer.js +1 -1
  36. package/examples/shorts/tileRaycast.js +39 -0
  37. package/examples/shorts/tiles.png +0 -0
  38. package/examples/shorts/tiltedView.js +14 -6
  39. package/examples/shorts/topDown.js +1 -1
  40. package/examples/starter/game.js +6 -8
  41. package/examples/starter/index.html +2 -2
  42. package/examples/style.css +1 -0
  43. package/examples/typescript/game.js +5 -4
  44. package/examples/typescript/game.ts +5 -7
  45. package/examples/uiSystem/game.js +3 -2
  46. package/package.json +1 -1
  47. package/plugins/postProcess.js +71 -51
  48. package/plugins/uiSystem.js +76 -23
  49. package/src/engine.js +30 -16
  50. package/src/engineAudio.js +33 -23
  51. package/src/engineDebug.js +27 -0
  52. package/src/engineDraw.js +103 -49
  53. package/src/engineExport.js +13 -0
  54. package/src/engineInput.js +14 -16
  55. package/src/engineObject.js +31 -6
  56. package/src/engineParticles.js +37 -16
  57. package/src/engineRelease.js +2 -1
  58. package/src/engineSettings.js +20 -1
  59. package/src/engineTileLayer.js +89 -94
  60. package/src/engineUtilities.js +206 -58
  61. package/src/engineWebGL.js +145 -69
  62. 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.16';
36
+ const engineVersion = '1.14.24';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -94,7 +94,17 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
94
94
  ///////////////////////////////////////////////////////////////////////////////
95
95
  // plugin hooks
96
96
 
97
- const pluginUpdateList = [], pluginRenderList = [];
97
+ const pluginList = [];
98
+ class EnginePlugin
99
+ {
100
+ constructor(update, render, glContextLost, glContextRestored)
101
+ {
102
+ this.update = update;
103
+ this.render = render;
104
+ this.glContextLost = glContextLost;
105
+ this.glContextRestored = glContextRestored;
106
+ }
107
+ }
98
108
 
99
109
  /**
100
110
  * @callback PluginCallback - Update or render function for a plugin
@@ -102,15 +112,21 @@ const pluginUpdateList = [], pluginRenderList = [];
102
112
  */
103
113
 
104
114
  /** Add a new update function for a plugin
105
- * @param {PluginCallback} [updateFunction]
106
- * @param {PluginCallback} [renderFunction]
115
+ * @param {PluginCallback} [update]
116
+ * @param {PluginCallback} [render]
117
+ * @param {PluginCallback} [glContextLost]
118
+ * @param {PluginCallback} [glContextRestored]
107
119
  * @memberof Engine */
108
- function engineAddPlugin(updateFunction, renderFunction)
120
+ function engineAddPlugin(update, render, glContextLost, glContextRestored)
109
121
  {
110
- ASSERT(!pluginUpdateList.includes(updateFunction));
111
- ASSERT(!pluginRenderList.includes(renderFunction));
112
- updateFunction && pluginUpdateList.push(updateFunction);
113
- renderFunction && pluginRenderList.push(renderFunction);
122
+ // make sure plugin functions are unique
123
+ ASSERT(!pluginList.find(p=>
124
+ p.update === update && p.render === render &&
125
+ p.glContextLost === glContextLost &&
126
+ p.glContextRestored === glContextRestored));
127
+
128
+ const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
129
+ pluginList.push(plugin);
114
130
  }
115
131
 
116
132
  ///////////////////////////////////////////////////////////////////////////////
@@ -195,7 +211,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
195
211
  wasUpdated = true;
196
212
  updateCanvas();
197
213
  inputUpdate();
198
- pluginUpdateList.forEach(f=>f());
214
+ pluginList.forEach(plugin=>plugin.update?.());
199
215
 
200
216
  // update object transforms even when paused
201
217
  for (const o of engineObjects)
@@ -228,7 +244,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
228
244
  updateCanvas();
229
245
  inputUpdate();
230
246
  gameUpdate();
231
- pluginUpdateList.forEach(f=>f());
247
+ pluginList.forEach(plugin=>plugin.update?.());
232
248
  engineObjectsUpdate();
233
249
 
234
250
  // do post update
@@ -262,7 +278,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
262
278
  for (const o of engineObjects)
263
279
  o.destroyed || o.render();
264
280
  gameRenderPost();
265
- pluginRenderList.forEach(f=>f());
281
+ pluginList.forEach(plugin=>plugin.render?.());
266
282
  touchGamepadRender();
267
283
  debugRender();
268
284
  glFlush();
@@ -389,7 +405,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
389
405
  image.onerror = image.onload = ()=>
390
406
  {
391
407
  const textureInfo = new TextureInfo(image);
392
- textureInfo.createWebGLTexture();
393
408
  textureInfos[textureIndex] = textureInfo;
394
409
  resolve();
395
410
  }
@@ -405,7 +420,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
405
420
  {
406
421
  const textureInfo = new TextureInfo(new Image);
407
422
  textureInfos[0] = textureInfo;
408
- textureInfo.createWebGLTexture();
409
423
  resolve();
410
424
  }));
411
425
  }
@@ -502,7 +516,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
502
516
 
503
517
  /**
504
518
  * @callback ObjectCallbackFunction - Function that processes an object
505
- * @param {EngineObject} uiObjects
519
+ * @param {EngineObject} object
506
520
  * @memberof Engine
507
521
  */
508
522
 
@@ -550,7 +564,7 @@ function drawEngineSplashScreen(t)
550
564
  // background
551
565
  const p3 = percent(t, 1, .8);
552
566
  const p4 = percent(t, 0, .5);
553
- 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);
554
568
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
555
569
  g.addColorStop(1,hsl(0,0,0,p3).toString());
556
570
  x.save();
@@ -741,7 +755,8 @@ function debugShowErrors(){}
741
755
  function debugVideoCaptureIsActive(){ return false; }
742
756
  function debugVideoCaptureStart (){}
743
757
  function debugVideoCaptureStop (){}
744
- function debugVideoCaptureUpdate(){}
758
+ function debugVideoCaptureUpdate(){}
759
+ function debugProtectConstant(o){ return o; }
745
760
  /**
746
761
  * LittleJS Utility Classes and Functions
747
762
  * - General purpose math library
@@ -752,7 +767,7 @@ function debugVideoCaptureUpdate(){}
752
767
  * @namespace Utilities
753
768
  */
754
769
 
755
- /** A shortcut to get Math.PI
770
+ /** The value of PI
756
771
  * @type {number}
757
772
  * @default Math.PI
758
773
  * @memberof Utilities */
@@ -762,25 +777,80 @@ const PI = Math.PI;
762
777
  * @param {number} value
763
778
  * @return {number}
764
779
  * @memberof Utilities */
765
- 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;
766
799
 
767
800
  /** Returns lowest value passed in
768
801
  * @param {...number} values
769
802
  * @return {number}
770
803
  * @memberof Utilities */
771
- function min(...values) { return Math.min(...values); }
804
+ const min = Math.min;
772
805
 
773
806
  /** Returns highest value passed in
774
807
  * @param {...number} values
775
808
  * @return {number}
776
809
  * @memberof Utilities */
777
- function max(...values) { return Math.max(...values); }
810
+ const max = Math.max;
778
811
 
779
812
  /** Returns the sign of value passed in
780
813
  * @param {number} value
781
814
  * @return {number}
782
815
  * @memberof Utilities */
783
- 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;
784
854
 
785
855
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
786
856
  * @param {number} dividend
@@ -885,7 +955,7 @@ function isPowerOfTwo(value) { return !(value & (value - 1)); }
885
955
  * @param {number} value
886
956
  * @return {number}
887
957
  * @memberof Utilities */
888
- function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
958
+ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
889
959
 
890
960
  /** Returns true if two axis aligned bounding boxes are overlapping
891
961
  * this can be used for simple collision detection between objects
@@ -953,7 +1023,7 @@ function isIntersecting(start, end, pos, size)
953
1023
  * @return {number} - Value waving between 0 and amplitude
954
1024
  * @memberof Utilities */
955
1025
  function wave(frequency=1, amplitude=1, t=time, offset=0)
956
- { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
1026
+ { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
957
1027
 
958
1028
  /** Formats seconds to mm:ss style for display purposes
959
1029
  * @param {number} t - time in seconds
@@ -992,6 +1062,96 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
992
1062
  * @memberof Utilities */
993
1063
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
994
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
+
995
1155
  ///////////////////////////////////////////////////////////////////////////////
996
1156
 
997
1157
  /** Random global functions
@@ -1010,7 +1170,7 @@ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valu
1010
1170
  * @param {number} [valueB]
1011
1171
  * @return {number}
1012
1172
  * @memberof Random */
1013
- function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
1173
+ function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
1014
1174
 
1015
1175
  /** Randomly returns true or false given the chance of true passed in
1016
1176
  * @param {number} [chance]
@@ -1089,7 +1249,7 @@ class RandomGenerator
1089
1249
  * @param {number} valueA
1090
1250
  * @param {number} [valueB]
1091
1251
  * @return {number} */
1092
- int(valueA, valueB=0) { return Math.floor(this.float(valueA, valueB)); }
1252
+ int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
1093
1253
 
1094
1254
  /** Randomly returns true or false given the chance of true passed in
1095
1255
  * @param {number} [chance]
@@ -1116,6 +1276,39 @@ class RandomGenerator
1116
1276
  * @return {Vector2} */
1117
1277
  vec2(valueA=1, valueB=0)
1118
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
+ }
1119
1312
  }
1120
1313
 
1121
1314
  ///////////////////////////////////////////////////////////////////////////////
@@ -1267,7 +1460,7 @@ class Vector2
1267
1460
 
1268
1461
  /** Returns the clockwise angle of this vector, up is angle 0
1269
1462
  * @return {number} */
1270
- angle() { return Math.atan2(this.x, this.y); }
1463
+ angle() { return atan2(this.x, this.y); }
1271
1464
 
1272
1465
  /** Sets this vector with clockwise angle and length passed in
1273
1466
  * @param {number} [angle]
@@ -1277,8 +1470,8 @@ class Vector2
1277
1470
  {
1278
1471
  ASSERT_NUMBER_VALID(angle);
1279
1472
  ASSERT_NUMBER_VALID(length);
1280
- this.x = length*Math.sin(angle);
1281
- this.y = length*Math.cos(angle);
1473
+ this.x = length*sin(angle);
1474
+ this.y = length*cos(angle);
1282
1475
  return this;
1283
1476
  }
1284
1477
 
@@ -1288,7 +1481,7 @@ class Vector2
1288
1481
  rotate(angle)
1289
1482
  {
1290
1483
  ASSERT_NUMBER_VALID(angle);
1291
- const c = Math.cos(-angle), s = Math.sin(-angle);
1484
+ const c = cos(-angle), s = sin(-angle);
1292
1485
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1293
1486
  }
1294
1487
 
@@ -1320,7 +1513,7 @@ class Vector2
1320
1513
 
1321
1514
  /** Returns a copy of this vector with each axis floored
1322
1515
  * @return {Vector2} */
1323
- floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
1516
+ floor() { return new Vector2(floor(this.x), floor(this.y)); }
1324
1517
 
1325
1518
  /** Returns new vec2 with modded values
1326
1519
  * @param {number} [divisor]
@@ -1628,67 +1821,67 @@ class Color
1628
1821
  /** Color - White #ffffff
1629
1822
  * @type {Color}
1630
1823
  * @memberof Utilities */
1631
- const WHITE = protectEngineConstant(rgb());
1824
+ const WHITE = debugProtectConstant(rgb());
1632
1825
 
1633
1826
  /** Color - Clear White #757474ff with 0 alpha
1634
1827
  * @type {Color}
1635
1828
  * @memberof Utilities */
1636
- const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
1829
+ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
1637
1830
 
1638
1831
  /** Color - Black #000000
1639
1832
  * @type {Color}
1640
1833
  * @memberof Utilities */
1641
- const BLACK = protectEngineConstant(rgb(0,0,0));
1834
+ const BLACK = debugProtectConstant(rgb(0,0,0));
1642
1835
 
1643
1836
  /** Color - Clear Black #000000 with 0 alpha
1644
1837
  * @type {Color}
1645
1838
  * @memberof Utilities */
1646
- const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
1839
+ const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
1647
1840
 
1648
1841
  /** Color - Gray #808080
1649
1842
  * @type {Color}
1650
1843
  * @memberof Utilities */
1651
- const GRAY = protectEngineConstant(rgb(.5,.5,.5));
1844
+ const GRAY = debugProtectConstant(rgb(.5,.5,.5));
1652
1845
 
1653
1846
  /** Color - Red #ff0000
1654
1847
  * @type {Color}
1655
1848
  * @memberof Utilities */
1656
- const RED = protectEngineConstant(rgb(1,0,0));
1849
+ const RED = debugProtectConstant(rgb(1,0,0));
1657
1850
 
1658
1851
  /** Color - Orange #ff8000
1659
1852
  * @type {Color}
1660
1853
  * @memberof Utilities */
1661
- const ORANGE = protectEngineConstant(rgb(1,.5,0));
1854
+ const ORANGE = debugProtectConstant(rgb(1,.5,0));
1662
1855
 
1663
1856
  /** Color - Yellow #ffff00
1664
1857
  * @type {Color}
1665
1858
  * @memberof Utilities */
1666
- const YELLOW = protectEngineConstant(rgb(1,1,0));
1859
+ const YELLOW = debugProtectConstant(rgb(1,1,0));
1667
1860
 
1668
1861
  /** Color - Green #00ff00
1669
1862
  * @type {Color}
1670
1863
  * @memberof Utilities */
1671
- const GREEN = protectEngineConstant(rgb(0,1,0));
1864
+ const GREEN = debugProtectConstant(rgb(0,1,0));
1672
1865
 
1673
1866
  /** Color - Cyan #00ffff
1674
1867
  * @type {Color}
1675
1868
  * @memberof Utilities */
1676
- const CYAN = protectEngineConstant(rgb(0,1,1));
1869
+ const CYAN = debugProtectConstant(rgb(0,1,1));
1677
1870
 
1678
1871
  /** Color - Blue #0000ff
1679
1872
  * @type {Color}
1680
1873
  * @memberof Utilities */
1681
- const BLUE = protectEngineConstant(rgb(0,0,1));
1874
+ const BLUE = debugProtectConstant(rgb(0,0,1));
1682
1875
 
1683
1876
  /** Color - Purple #8000ff
1684
1877
  * @type {Color}
1685
1878
  * @memberof Utilities */
1686
- const PURPLE = protectEngineConstant(rgb(.5,0,1));
1879
+ const PURPLE = debugProtectConstant(rgb(.5,0,1));
1687
1880
 
1688
1881
  /** Color - Magenta #ff00ff
1689
1882
  * @type {Color}
1690
1883
  * @memberof Utilities */
1691
- const MAGENTA = protectEngineConstant(rgb(1,0,1));
1884
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1692
1885
 
1693
1886
  ///////////////////////////////////////////////////////////////////////////////
1694
1887
 
@@ -1751,41 +1944,11 @@ class Timer
1751
1944
 
1752
1945
  /** Returns this timer expressed as a string
1753
1946
  * @return {string} */
1754
- 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'; }
1755
1948
 
1756
1949
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1757
1950
  * @return {number} */
1758
1951
  valueOf() { return this.get(); }
1759
- }
1760
-
1761
- ///////////////////////////////////////////////////////////////////////////////
1762
- // Helper functions used by the engine
1763
-
1764
- // make color constants immutable with debug assertions
1765
- function protectEngineConstant(obj)
1766
- {
1767
- if (debug)
1768
- {
1769
- // get properties and store original values
1770
- const props = Object.keys(obj), values = {};
1771
- props.forEach(prop => values[prop] = obj[prop]);
1772
-
1773
- // replace with getters/setters that assert
1774
- props.forEach(prop =>
1775
- {
1776
- Object.defineProperty(obj, prop, {
1777
- get: () => values[prop],
1778
- set: (value) =>
1779
- {
1780
- ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1781
- },
1782
- enumerable: true
1783
- });
1784
- });
1785
- }
1786
-
1787
- // freeze the object to prevent adding new properties
1788
- return Object.freeze(obj);
1789
1952
  }
1790
1953
  /**
1791
1954
  * LittleJS Engine Settings
@@ -2003,6 +2166,14 @@ let touchInputEnable = true;
2003
2166
  * @memberof Settings */
2004
2167
  let touchGamepadEnable = false;
2005
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
+
2006
2177
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
2007
2178
  * @type {boolean}
2008
2179
  * @default
@@ -2178,9 +2349,14 @@ function setHeadlessMode(headless) { headlessMode = headless; }
2178
2349
  * @memberof Settings */
2179
2350
  function setGLEnable(enable)
2180
2351
  {
2352
+ if (enable && !glCanBeEnabled)
2353
+ {
2354
+ console.warn('Can not enable WebGL if it was disabled on start.');
2355
+ return;
2356
+ }
2181
2357
  glEnable = enable;
2182
2358
  if (glCanvas) // hide glCanvas if WebGL is disabled
2183
- glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2359
+ glCanvas.style.display = enable ? '' : 'none';
2184
2360
  }
2185
2361
 
2186
2362
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
@@ -2268,6 +2444,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
2268
2444
  * @memberof Settings */
2269
2445
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
2270
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
+
2271
2453
  /** Set if touch gamepad should be analog stick or 8 way dpad
2272
2454
  * @param {boolean} analog
2273
2455
  * @memberof Settings */
@@ -2629,6 +2811,27 @@ class EngineObject
2629
2811
  // test which side we bounced off (or both if a corner)
2630
2812
  const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2631
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
+ }
2632
2835
  if (blockedLayerY || !blockedLayerX)
2633
2836
  {
2634
2837
  // bounce velocity
@@ -2652,12 +2855,6 @@ class EngineObject
2652
2855
  this.groundObject = undefined;
2653
2856
  }
2654
2857
  }
2655
- if (blockedLayerX)
2656
- {
2657
- // move to previous position and bounce
2658
- this.pos.x = oldPos.x;
2659
- this.velocity.x *= -this.restitution;
2660
- }
2661
2858
  debugPhysics && debugRect(this.pos, this.size, '#f00');
2662
2859
  }
2663
2860
  }
@@ -2715,6 +2912,16 @@ class EngineObject
2715
2912
  */
2716
2913
  collideWithObject(object) { return true; }
2717
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
+
2718
2925
  /** How long since the object was created
2719
2926
  * @return {number} */
2720
2927
  getAliveTime() { return time - this.spawnTime; }
@@ -2987,16 +3194,15 @@ class TileInfo
2987
3194
  }
2988
3195
 
2989
3196
  /**
2990
- * Set this tile to use a full image
2991
- * @param {HTMLImageElement|OffscreenCanvas} image
2992
- * @param {WebGLTexture} [glTexture] - WebGL texture
3197
+ * Set this tile to use a full image in a texture info
3198
+ * @param {TextureInfo} textureInfo
2993
3199
  * @return {TileInfo}
2994
3200
  */
2995
- setFullImage(image, glTexture)
3201
+ setFullImage(textureInfo)
2996
3202
  {
2997
3203
  this.pos = new Vector2;
2998
- this.size = new Vector2(image.width, image.height);
2999
- this.textureInfo = new TextureInfo(image, glTexture);
3204
+ this.size = textureInfo.size.copy();
3205
+ this.textureInfo = textureInfo;
3000
3206
  // do not use padding or bleed
3001
3207
  this.bleedScale = this.padding = 0;
3002
3208
  return this;
@@ -3012,26 +3218,30 @@ class TextureInfo
3012
3218
  /**
3013
3219
  * Create a TextureInfo, called automatically by the engine
3014
3220
  * @param {HTMLImageElement|OffscreenCanvas} image
3015
- * @param {WebGLTexture} [glTexture] - WebGL texture
3221
+ * @param {boolean} [useWebGL] - Should use WebGL if available?
3016
3222
  */
3017
- constructor(image, glTexture)
3223
+ constructor(image, useWebGL=true)
3018
3224
  {
3019
- /** @property {HTMLImageElement} - image source */
3225
+ /** @property {HTMLImageElement|OffscreenCanvas} - image source */
3020
3226
  this.image = image;
3021
3227
  /** @property {Vector2} - size of the image */
3022
- this.size = vec2(image.width, image.height);
3228
+ this.size = image ? vec2(image.width, image.height) : vec2();
3023
3229
  /** @property {Vector2} - inverse of the size, cached for rendering */
3024
- this.sizeInverse = vec2(1/image.width, 1/image.height);
3230
+ this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
3025
3231
  /** @property {WebGLTexture} - WebGL texture */
3026
- this.glTexture = glTexture;
3232
+ this.glTexture = undefined;
3233
+ useWebGL && this.createWebGLTexture();
3027
3234
  }
3028
3235
 
3029
- createWebGLTexture()
3030
- {
3031
- ASSERT(!this.glTexture);
3032
- if (glEnable)
3033
- this.glTexture = glCreateTexture(this.image);
3034
- }
3236
+ /** Creates the WebGL texture, updates if already created */
3237
+ createWebGLTexture() { glRegisterTextureInfo(this); }
3238
+
3239
+ /** Destroys the WebGL texture */
3240
+ destroyWebGLTexture() { glUnregisterTextureInfo(this); }
3241
+
3242
+ /** Check if the texture is webgl enabled
3243
+ * @return {boolean} */
3244
+ hasWebGL() { return !!this.glTexture; }
3035
3245
  }
3036
3246
 
3037
3247
  ///////////////////////////////////////////////////////////////////////////////
@@ -3063,6 +3273,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3063
3273
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3064
3274
  if (useWebGL)
3065
3275
  {
3276
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3066
3277
  if (screenSpace)
3067
3278
  {
3068
3279
  // convert to world space
@@ -3158,6 +3369,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3158
3369
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3159
3370
  if (useWebGL)
3160
3371
  {
3372
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3161
3373
  if (screenSpace)
3162
3374
  {
3163
3375
  // convert to world space
@@ -3169,7 +3381,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3169
3381
  const halfSizeX = size.x/2, halfSizeY = size.y/2;
3170
3382
  const colorTopInt = colorTop.rgbaInt();
3171
3383
  const colorBottomInt = colorBottom.rgbaInt();
3172
- const c = Math.cos(-angle), s = Math.sin(-angle);
3384
+ const c = cos(-angle), s = sin(-angle);
3173
3385
  for (let i=4; i--;)
3174
3386
  {
3175
3387
  const x = i & 1 ? halfSizeX : -halfSizeX;
@@ -3220,6 +3432,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3220
3432
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3221
3433
  if (useWebGL)
3222
3434
  {
3435
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3223
3436
  let scale = 1;
3224
3437
  if (screenSpace)
3225
3438
  {
@@ -3269,6 +3482,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3269
3482
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
3270
3483
  const size = vec2(width, halfDelta.length()*2);
3271
3484
  pos = pos.add(posA.add(halfDelta));
3485
+ if (screenSpace)
3486
+ halfDelta.y *= -1; // flip angle Y if screen space
3272
3487
  angle += halfDelta.angle();
3273
3488
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3274
3489
  }
@@ -3296,7 +3511,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3296
3511
  for (let i=sides; i--;)
3297
3512
  {
3298
3513
  const a = (i/sides)*PI*2;
3299
- points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
3514
+ points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
3300
3515
  }
3301
3516
  drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3302
3517
  }
@@ -3323,6 +3538,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3323
3538
 
3324
3539
  if (useWebGL)
3325
3540
  {
3541
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3326
3542
  let scale = 1;
3327
3543
  if (screenSpace)
3328
3544
  {
@@ -3464,12 +3680,13 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3464
3680
  * @param {Color} [lineColor=(0,0,0,1)]
3465
3681
  * @param {CanvasTextAlign} [textAlign='center']
3466
3682
  * @param {string} [font=fontDefault]
3683
+ * @param {string} [fontStyle]
3467
3684
  * @param {number} [maxWidth]
3468
3685
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3469
3686
  * @memberof Draw */
3470
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth, context=drawContext)
3687
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
3471
3688
  {
3472
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, maxWidth, context);
3689
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
3473
3690
  }
3474
3691
 
3475
3692
  /** Draw text on overlay canvas in world space
@@ -3482,11 +3699,12 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3482
3699
  * @param {Color} [lineColor=(0,0,0,1)]
3483
3700
  * @param {CanvasTextAlign} [textAlign='center']
3484
3701
  * @param {string} [font=fontDefault]
3702
+ * @param {string} [fontStyle]
3485
3703
  * @param {number} [maxWidth]
3486
3704
  * @memberof Draw */
3487
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth)
3705
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
3488
3706
  {
3489
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, maxWidth, overlayContext);
3707
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
3490
3708
  }
3491
3709
 
3492
3710
  /** Draw text on overlay canvas in screen space
@@ -3499,10 +3717,11 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3499
3717
  * @param {Color} [lineColor=(0,0,0,1)]
3500
3718
  * @param {CanvasTextAlign} [textAlign]
3501
3719
  * @param {string} [font=fontDefault]
3720
+ * @param {string} [fontStyle]
3502
3721
  * @param {number} [maxWidth]
3503
3722
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3504
3723
  * @memberof Draw */
3505
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3724
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
3506
3725
  {
3507
3726
  ASSERT(isString(text), 'text must be a string');
3508
3727
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3510,15 +3729,15 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3510
3729
  ASSERT(isColor(color), 'color must be a color');
3511
3730
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3512
3731
  ASSERT(isColor(lineColor), 'lineColor must be a color');
3513
- ASSERT(isColor(lineColor), 'lineColor must be a color');
3514
3732
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
3515
3733
  ASSERT(isString(font), 'font must be a string');
3734
+ ASSERT(isString(fontStyle), 'fontStyle must be a string');
3516
3735
 
3517
3736
  context.fillStyle = color.toString();
3518
3737
  context.strokeStyle = lineColor.toString();
3519
3738
  context.lineWidth = lineWidth;
3520
3739
  context.textAlign = textAlign;
3521
- context.font = size + 'px '+ font;
3740
+ context.font = fontStyle + ' ' + size + 'px '+ font;
3522
3741
  context.textBaseline = 'middle';
3523
3742
 
3524
3743
  const lines = (text+'').split('\n');
@@ -3541,18 +3760,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3541
3760
  * @memberof Draw */
3542
3761
  function screenToWorld(screenPos)
3543
3762
  {
3544
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3545
- 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;
3546
3765
  if (cameraAngle)
3547
3766
  {
3548
3767
  // apply camera rotation
3549
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
3550
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3551
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3552
- cameraPosRelativeX = rotatedX;
3553
- 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;
3554
3773
  }
3555
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
3774
+ return new Vector2(x + cameraPos.x, y + cameraPos.y);
3556
3775
  }
3557
3776
 
3558
3777
  /** Convert from world to screen space coordinates
@@ -3561,24 +3780,64 @@ function screenToWorld(screenPos)
3561
3780
  * @memberof Draw */
3562
3781
  function worldToScreen(worldPos)
3563
3782
  {
3564
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
3565
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
3783
+ let x = worldPos.x - cameraPos.x;
3784
+ let y = worldPos.y - cameraPos.y;
3566
3785
  if (cameraAngle)
3567
3786
  {
3568
3787
  // apply inverse 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;
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;
3574
3793
  }
3575
3794
  return new Vector2
3576
3795
  (
3577
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
3578
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
3796
+ x * cameraScale + mainCanvasSize.x/2 - .5,
3797
+ y * -cameraScale + mainCanvasSize.y/2 - .5
3579
3798
  );
3580
3799
  }
3581
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
+
3582
3841
  /** Get the camera's visible area in world space
3583
3842
  * @return {Vector2}
3584
3843
  * @memberof Draw */
@@ -3631,6 +3890,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3631
3890
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
3632
3891
  const sx2 = bleedScale;
3633
3892
  const sy2 = bleedScale;
3893
+ sWidth = max(1,sWidth|0);
3894
+ sHeight = max(1,sHeight|0);
3634
3895
  const sWidth2 = sWidth - 2*bleedScale;
3635
3896
  const sHeight2 = sHeight - 2*bleedScale;
3636
3897
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
@@ -3645,7 +3906,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3645
3906
  // copy to offscreen canvas
3646
3907
  workCanvas.width = sWidth;
3647
3908
  workCanvas.height = sHeight;
3648
- 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);
3649
3910
 
3650
3911
  // tint image using offscreen work context
3651
3912
  const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
@@ -3726,11 +3987,11 @@ let engineFontImage;
3726
3987
  class FontImage
3727
3988
  {
3728
3989
  /** Create an image font
3729
- * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
3730
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3731
- * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
3990
+ * @param {HTMLImageElement} [image] - Image for the font, default if undefined
3991
+ * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3992
+ * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
3732
3993
  */
3733
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
3994
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
3734
3995
  {
3735
3996
  // load default font image
3736
3997
  if (!engineFontImage)
@@ -3982,9 +4243,9 @@ function inputUpdate()
3982
4243
  if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
3983
4244
  inputClear();
3984
4245
 
3985
- // update mouse world space position
4246
+ // update mouse world space position and delta
3986
4247
  mousePos = screenToWorld(mousePosScreen);
3987
- mouseDelta = mouseDeltaScreen.multiply(vec2(1,-1)).rotate(-cameraAngle);
4248
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
3988
4249
 
3989
4250
  // update gamepads if enabled
3990
4251
  gamepadsUpdate();
@@ -4016,7 +4277,6 @@ function inputInit()
4016
4277
  document.addEventListener('wheel', onMouseWheel);
4017
4278
  document.addEventListener('contextmenu', onContextMenu);
4018
4279
  document.addEventListener('blur', onBlur);
4019
- document.addEventListener('mouseleave', onMouseLeave);
4020
4280
 
4021
4281
  // init touch input
4022
4282
  if (isTouchDevice && touchInputEnable)
@@ -4058,7 +4318,10 @@ function inputInit()
4058
4318
 
4059
4319
  isUsingGamepad = false;
4060
4320
  inputData[0][e.button] = 3;
4321
+
4322
+ let mousePosScreenLast = mousePosScreen;
4061
4323
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4324
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4062
4325
  inputPreventDefault && e.button && e.preventDefault();
4063
4326
  }
4064
4327
  function onMouseUp(e)
@@ -4069,18 +4332,13 @@ function inputInit()
4069
4332
  }
4070
4333
  function onMouseMove(e)
4071
4334
  {
4335
+ let mousePosScreenLast = mousePosScreen;
4072
4336
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4073
- mouseDeltaScreen = mouseDeltaScreen.add(vec2(e.movementX, e.movementY));
4337
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4074
4338
  }
4075
4339
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4076
4340
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4077
4341
  function onBlur() { inputClear(); } // reset input when focus is lost
4078
- function onMouseLeave()
4079
- {
4080
- // set mouse position and delta when leaving canvas
4081
- mousePosScreen = vec2(-1);
4082
- mouseDeltaScreen = vec2(0);
4083
- }
4084
4342
  }
4085
4343
 
4086
4344
  // convert a mouse or touch event position to screen space
@@ -4124,8 +4382,8 @@ function gamepadsUpdate()
4124
4382
  else if (touchGamepadStick.lengthSquared() > .3)
4125
4383
  {
4126
4384
  // convert to 8 way dpad
4127
- sticks[0].x = Math.round(touchGamepadStick.x);
4128
- sticks[0].y = -Math.round(touchGamepadStick.y);
4385
+ sticks[0].x = round(touchGamepadStick.x);
4386
+ sticks[0].y = -round(touchGamepadStick.y);
4129
4387
  sticks[0] = sticks[0].clampLength();
4130
4388
  }
4131
4389
 
@@ -4253,11 +4511,11 @@ function touchInputInit()
4253
4511
  {
4254
4512
  // set event pos and pass it along
4255
4513
  const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
4256
- const lastMousePosScreen = mousePosScreen;
4514
+ const mousePosScreenLast = mousePosScreen;
4257
4515
  mousePosScreen = mouseEventToScreen(pos);
4258
4516
  if (wasTouching)
4259
4517
  {
4260
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(lastMousePosScreen));
4518
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4261
4519
  isUsingGamepad = touchGamepadEnable;
4262
4520
  }
4263
4521
  else
@@ -4289,7 +4547,7 @@ function touchInputInit()
4289
4547
  if (touching)
4290
4548
  {
4291
4549
  touchGamepadTimer.set();
4292
- if (paused && !wasTouching)
4550
+ if (touchGamepadCenterButton && !wasTouching && paused)
4293
4551
  {
4294
4552
  // touch anywhere to press start when paused
4295
4553
  touchGamepadButtons[9] = 1;
@@ -4328,7 +4586,8 @@ function touchInputInit()
4328
4586
  if (button < touchGamepadButtonCount)
4329
4587
  touchGamepadButtons[button] = 1;
4330
4588
  }
4331
- else if (startCenter.distance(touchPos) < touchGamepadSize && !wasTouching)
4589
+ else if (touchGamepadCenterButton && !wasTouching &&
4590
+ startCenter.distance(touchPos) < touchGamepadSize)
4332
4591
  {
4333
4592
  // virtual start button in center
4334
4593
  touchGamepadButtons[9] = 1;
@@ -4743,8 +5002,16 @@ class SoundInstance
4743
5002
  this.stop();
4744
5003
  this.gainNode = audioContext.createGain();
4745
5004
  this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
4746
- this.startTime = audioContext.currentTime - offset;
4747
- this.pausedTime = undefined;
5005
+ if (this.source)
5006
+ {
5007
+ this.startTime = audioContext.currentTime - offset;
5008
+ this.pausedTime = undefined;
5009
+ }
5010
+ else
5011
+ {
5012
+ this.startTime = undefined;
5013
+ this.pausedTime = 0;
5014
+ }
4748
5015
  }
4749
5016
 
4750
5017
  /** Set the volume of this sound instance
@@ -4893,12 +5160,19 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4893
5160
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
4894
5161
  * @param {number} [offset] - Offset in seconds to start playback from
4895
5162
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
4896
- * @return {AudioBufferSourceNode} - The audio node of the sound played
5163
+ * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
4897
5164
  * @memberof Audio */
4898
5165
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
4899
5166
  {
4900
5167
  if (!soundEnable || headlessMode) return;
4901
5168
 
5169
+ if (!audioIsRunning())
5170
+ {
5171
+ // fix stalled audio, this sound won't be able to play
5172
+ audioContext.resume();
5173
+ return;
5174
+ }
5175
+
4902
5176
  // create buffer and source
4903
5177
  const channelCount = sampleChannels.length;
4904
5178
  const sampleLength = sampleChannels[0].length;
@@ -4924,14 +5198,9 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4924
5198
  if (onended)
4925
5199
  source.addEventListener('ended', ()=> onended(source));
4926
5200
 
5201
+ // play and return sound
4927
5202
  const startOffset = offset * rate;
4928
- if (!audioIsRunning())
4929
- {
4930
- // fix stalled audio and start
4931
- audioContext.resume().then(()=>source.start(0, startOffset));
4932
- }
4933
- else
4934
- source.start(0, startOffset);
5203
+ source.start(0, startOffset);
4935
5204
  return source;
4936
5205
  }
4937
5206
 
@@ -5014,10 +5283,10 @@ function zzfxG
5014
5283
 
5015
5284
  // biquad LP/HP filter
5016
5285
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
5017
- cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
5018
- a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
5019
- b0 = (1 + sign(filter) * cos) / 2 / a0,
5020
- 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,
5021
5290
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
5022
5291
 
5023
5292
  // scale by sample rate
@@ -5040,15 +5309,15 @@ function zzfxG
5040
5309
  if (!(++crush%(bitCrush*100|0))) // bit crush
5041
5310
  {
5042
5311
  s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
5043
- (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
5044
- Math.sin(t**3) : // 4 noise
5045
- Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
5046
- 1-(2*t/PI2%2+2)%2: // 2 saw
5047
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
5048
- 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
5049
5318
 
5050
5319
  s = (repeatTime ?
5051
- 1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
5320
+ 1 - tremolo + tremolo*sin(PI2*i/repeatTime) // tremolo
5052
5321
  : 1) *
5053
5322
  (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
5054
5323
  (i < attack ? i/attack : // attack
@@ -5070,8 +5339,8 @@ function zzfxG
5070
5339
  }
5071
5340
 
5072
5341
  f = (frequency += slide += deltaSlide) *// frequency
5073
- Math.cos(modulation*modOffset++); // modulation
5074
- t += f + f*noise*Math.sin(i**5); // noise
5342
+ cos(modulation*modOffset++); // modulation
5343
+ t += f + f*noise*sin(i**5); // noise
5075
5344
 
5076
5345
  if (jump && ++jump > pitchJumpTime) // pitch jump
5077
5346
  {
@@ -5137,23 +5406,22 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5137
5406
  }
5138
5407
  }
5139
5408
 
5140
- /** Return the center of first tile hit, undefined if nothing was hit.
5141
- * 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.
5142
5410
  * @param {Vector2} posStart
5143
5411
  * @param {Vector2} posEnd
5144
5412
  * @param {EngineObject} [object] - An object or undefined for generic test
5413
+ * @param {Vector2} [normal] - Optional normal of the surface hit
5145
5414
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5146
- * @return {Vector2}
5415
+ * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
5147
5416
  * @memberof TileLayers */
5148
- function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5417
+ function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
5149
5418
  {
5150
5419
  for (const layer of tileCollisionLayers)
5151
5420
  {
5152
5421
  if (!solidOnly || layer.isSolid)
5153
5422
  {
5154
- const hitPos = layer.collisionRaycast(posStart, posEnd, object)
5155
- if (hitPos)
5156
- return hitPos;
5423
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
5424
+ if (hitPos) return hitPos;
5157
5425
  }
5158
5426
  }
5159
5427
  }
@@ -5287,10 +5555,15 @@ class CanvasLayer extends EngineObject
5287
5555
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
5288
5556
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
5289
5557
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5290
- this.context = headlessMode ? undefined : this.canvas.getContext('2d');
5291
- /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
5292
- this.glTexture = undefined;
5293
- this.gravityScale = 0; // disable gravity by default for canvas layers
5558
+ this.context = this.canvas?.getContext('2d');
5559
+ /** @property {TextureInfo} - Texture info to use for this object rendering */
5560
+ const useWebGL = false; // do not use webgl by default
5561
+ this.textureInfo = new TextureInfo(this.canvas, useWebGL);
5562
+ /** @property {boolean} - True if WebGL texture needs to be refreshed */
5563
+ this.refreshWebGL = false;
5564
+
5565
+ // disable physics by default
5566
+ this.mass = this.gravityScale = this.friction = this.restitution = 0;
5294
5567
  }
5295
5568
 
5296
5569
  /** Destroy this canvas layer */
@@ -5299,9 +5572,7 @@ class CanvasLayer extends EngineObject
5299
5572
  if (this.destroyed)
5300
5573
  return;
5301
5574
 
5302
- // free up the WebGL texture
5303
- if (this.glTexture)
5304
- glDeleteTexture(this.glTexture);
5575
+ this.textureInfo.destroyWebGLTexture();
5305
5576
  super.destroy();
5306
5577
  }
5307
5578
 
@@ -5323,9 +5594,16 @@ class CanvasLayer extends EngineObject
5323
5594
  * @memberof Draw */
5324
5595
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5325
5596
  {
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
+
5326
5605
  // draw the canvas layer as a single tile that uses the whole texture
5327
- const useWebGL = glEnable && this.glTexture !== undefined;
5328
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5606
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5329
5607
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5330
5608
  }
5331
5609
 
@@ -5343,6 +5621,8 @@ class CanvasLayer extends EngineObject
5343
5621
  * @param {Canvas2DDrawCallback} drawFunction */
5344
5622
  drawCanvas2D(pos, size, angle, mirror, drawFunction)
5345
5623
  {
5624
+ if (!this.context) return;
5625
+
5346
5626
  const context = this.context;
5347
5627
  context.save();
5348
5628
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
@@ -5392,18 +5672,22 @@ class CanvasLayer extends EngineObject
5392
5672
  { this.drawTile(pos, size, undefined, color, angle); }
5393
5673
 
5394
5674
  /** Create or update the WebGL texture for this layer
5395
- * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5396
- 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)
5397
5679
  {
5398
- if (glEnable && enable)
5680
+ if (!immediate && enable && this.textureInfo.hasWebGL())
5399
5681
  {
5400
- if (this.glTexture)
5401
- glSetTextureData(this.glTexture, this.canvas);
5402
- else
5403
- this.glTexture = glCreateTexture(this.canvas);
5682
+ // refresh the texture when needed
5683
+ this.refreshWebGL = true;
5684
+ return;
5404
5685
  }
5686
+
5687
+ if (enable)
5688
+ this.textureInfo.createWebGLTexture();
5405
5689
  else
5406
- this.glTexture = undefined;
5690
+ this.textureInfo.destroyWebGLTexture();
5407
5691
  }
5408
5692
  }
5409
5693
 
@@ -5427,24 +5711,14 @@ class TileLayer extends CanvasLayer
5427
5711
  * @param {Vector2} size - World space size
5428
5712
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
5429
5713
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5430
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5431
5714
  */
5432
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
5715
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
5433
5716
  {
5434
- super(position, size, 0, renderOrder, size);
5435
-
5717
+ const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
5718
+ super(position, size, 0, renderOrder, canvasSize);
5719
+
5720
+ // set tile info
5436
5721
  this.tileInfo = tileInfo;
5437
- const canvasSize = size.multiply(tileInfo.size);
5438
- /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
5439
- this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
5440
- /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
5441
- this.context = this.canvas.getContext('2d');
5442
- /** @property {WebGLTexture} - Texture if using WebGL for this layer */
5443
- this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
5444
- // set no friction by default, applied friction is max of both objects
5445
- this.friction = 0;
5446
- // set no restitution by default, applied restitution is max of both objects
5447
- this.restitution = 0;
5448
5722
 
5449
5723
  // init tile data
5450
5724
  this.data = [];
@@ -5493,11 +5767,18 @@ class TileLayer extends CanvasLayer
5493
5767
  {
5494
5768
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5495
5769
 
5770
+ if (this.refreshWebGL)
5771
+ {
5772
+ // update the WebGL texture
5773
+ this.textureInfo.createWebGLTexture();
5774
+ this.refreshWebGL = false;
5775
+ }
5776
+
5496
5777
  // draw the tile layer as a single tile
5497
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5778
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5498
5779
  const size = this.drawSize || this.size;
5499
5780
  const pos = this.pos.add(size.scale(.5));
5500
- const useWebGL = glEnable && this.glTexture !== undefined;
5781
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
5501
5782
  drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5502
5783
  }
5503
5784
 
@@ -5510,8 +5791,7 @@ class TileLayer extends CanvasLayer
5510
5791
  for (let y = this.size.y; y--;)
5511
5792
  this.drawTileData(vec2(x,y), false);
5512
5793
  this.redrawEnd();
5513
- if (this.glTexture)
5514
- this.useWebGL(); // update WebGL texture
5794
+ this.useWebGL();
5515
5795
  }
5516
5796
 
5517
5797
  /** Call to start the redraw process
@@ -5519,6 +5799,8 @@ class TileLayer extends CanvasLayer
5519
5799
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
5520
5800
  redrawStart(clear=false)
5521
5801
  {
5802
+ if (!this.context) return;
5803
+
5522
5804
  // save current render settings
5523
5805
  /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
5524
5806
  this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
@@ -5528,8 +5810,9 @@ class TileLayer extends CanvasLayer
5528
5810
  drawCanvas = this.canvas;
5529
5811
  drawContext = this.context;
5530
5812
  cameraPos = this.size.scale(.5);
5531
- cameraScale = this.tileInfo.size.x;
5532
- 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);
5533
5816
  if (clear)
5534
5817
  {
5535
5818
  // clear and set size
@@ -5547,6 +5830,8 @@ class TileLayer extends CanvasLayer
5547
5830
  /** Call to end the redraw process */
5548
5831
  redrawEnd()
5549
5832
  {
5833
+ if (!this.context) return;
5834
+
5550
5835
  ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
5551
5836
  glCopyToContext(drawContext);
5552
5837
  //debugSaveCanvas(this.canvas);
@@ -5563,6 +5848,8 @@ class TileLayer extends CanvasLayer
5563
5848
  */
5564
5849
  drawTileData(layerPos, clear=true)
5565
5850
  {
5851
+ if (!this.context) return;
5852
+
5566
5853
  // clear out where the tile was, for full opaque tiles this can be skipped
5567
5854
  const s = this.tileInfo.size;
5568
5855
  if (clear)
@@ -5599,11 +5886,10 @@ class TileCollisionLayer extends TileLayer
5599
5886
  * @param {Vector2} size - World space size
5600
5887
  * @param {TileInfo} [tileInfo] - Tile info for layer
5601
5888
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5602
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5603
5889
  */
5604
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
5890
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
5605
5891
  {
5606
- super(position, size.floor(), tileInfo, renderOrder, useWebGL);
5892
+ super(position, size.floor(), tileInfo, renderOrder);
5607
5893
 
5608
5894
  /** @property {Array<number>} - The tile collision grid */
5609
5895
  this.collisionData = [];
@@ -5686,66 +5972,44 @@ class TileCollisionLayer extends TileLayer
5686
5972
  // check if the object should collide with this tile
5687
5973
  const tileData = this.collisionData[y*this.size.x+x];
5688
5974
  if (tileData)
5689
- 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)))
5690
5977
  return true;
5691
5978
  }
5692
5979
  return false;
5693
5980
  }
5694
5981
 
5695
- /** Return the center of first tile hit, undefined if nothing was hit.
5696
- * 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.
5697
5983
  * @param {Vector2} posStart
5698
5984
  * @param {Vector2} posEnd
5699
- * @param {EngineObject} [object]
5700
- * @return {Vector2} */
5701
- 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)
5702
5989
  {
5703
5990
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
5704
5991
  ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
5705
-
5706
- // transform to local layer space
5707
- const posStartX = posStart.x - this.pos.x;
5708
- const posStartY = posStart.y - this.pos.y;
5709
- const posEndX = posEnd.x - this.pos.x;
5710
- const posEndY = posEnd.y - this.pos.y;
5711
-
5712
- // test if a ray collides with tiles from start to end
5713
- const deltaX = posEndX - posStartX;
5714
- const deltaY = posEndY - posStartY;
5715
- const totalLength = (deltaX**2 + deltaY**2)**.5;
5716
- const unitX = abs(totalLength/deltaX);
5717
- const unitY = abs(totalLength/deltaY);
5718
-
5719
- // setup iteration variables
5720
- const pos = posStart.floor(), signDeltaX = sign(deltaX), signDeltaY = sign(deltaY);
5721
- let xi = unitX * (deltaX < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1) || 0;
5722
- let yi = unitY * (deltaY < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1) || 0;
5723
-
5724
- // use line drawing algorithm to test for collisions
5725
- while (true)
5992
+
5993
+ const localPos = new Vector2;
5994
+ const collisionTest = (pos)=>
5726
5995
  {
5727
5996
  // check for tile collision
5728
- const tileData = this.getCollisionData(pos);
5729
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
5730
- {
5731
- pos.x += .5; pos.y += .5;
5732
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
5733
- debugRaycast && debugPoint(pos, '#ff0');
5734
- return pos;
5735
- }
5736
-
5737
- // check if past the end
5738
- if (xi >= totalLength && yi >= totalLength)
5739
- break;
5740
-
5741
- // get coordinates of next tile to check
5742
- if (xi > yi)
5743
- pos.y += signDeltaY, yi += unitY;
5744
- else
5745
- 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));
5746
6000
  }
5747
-
5748
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
+ }
5749
6013
  }
5750
6014
  }
5751
6015
  /**
@@ -5771,7 +6035,7 @@ class TileCollisionLayer extends TileLayer
5771
6035
  * tile(0, 16), // tileInfo
5772
6036
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
5773
6037
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
5774
- * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
6038
+ * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5775
6039
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5776
6040
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
5777
6041
  * );
@@ -5900,6 +6164,12 @@ class ParticleEmitter extends EngineObject
5900
6164
  this.particleCreateCallback = undefined;
5901
6165
  /** @property {number} - Track particle emit time */
5902
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();
5903
6173
  }
5904
6174
 
5905
6175
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
@@ -5908,6 +6178,18 @@ class ParticleEmitter extends EngineObject
5908
6178
  // only do default update to apply parent transforms
5909
6179
  this.parent && super.update();
5910
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
+
5911
6193
  // update emitter
5912
6194
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
5913
6195
  {
@@ -5965,7 +6247,7 @@ class ParticleEmitter extends EngineObject
5965
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);
5966
6248
  particle.velocity = vec2().setAngle(velocityAngle, speed);
5967
6249
  particle.angleVelocity = angleSpeed;
5968
- if (!this.localSpace)
6250
+ if (!this.localSpace && this.velocityInheritance > 0)
5969
6251
  {
5970
6252
  // apply emitter velocity to particle
5971
6253
  particle.velocity.x += this.velocity.x;
@@ -6063,6 +6345,16 @@ class Particle extends EngineObject
6063
6345
  this.velocity.y *= s;
6064
6346
  }
6065
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
+ }
6066
6358
  }
6067
6359
 
6068
6360
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -6071,7 +6363,7 @@ class Particle extends EngineObject
6071
6363
  // lerp color and size
6072
6364
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6073
6365
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6074
- this.size.x = this.size.y = radius;
6366
+ const size = vec2(radius);
6075
6367
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6076
6368
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6077
6369
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
@@ -6091,7 +6383,7 @@ class Particle extends EngineObject
6091
6383
  {
6092
6384
  // in local space of emitter
6093
6385
  const a = this.localSpaceEmitter.angle;
6094
- const c = Math.cos(a), s = Math.sin(a);
6386
+ const c = cos(a), s = sin(a);
6095
6387
  pos = this.localSpaceEmitter.pos.add(
6096
6388
  new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6097
6389
  angle += this.localSpaceEmitter.angle;
@@ -6107,22 +6399,15 @@ class Particle extends EngineObject
6107
6399
  {
6108
6400
  // stretch in direction of motion
6109
6401
  const trailLength = speed * this.trailScale;
6110
- this.size.y = max(this.size.x, trailLength);
6111
- angle = Math.atan2(direction.x, direction.y);
6112
- 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);
6113
6405
  }
6114
6406
  }
6115
6407
  else
6116
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6408
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6117
6409
  this.additive && setBlendMode();
6118
- debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
6119
-
6120
- if (p1 === 1)
6121
- {
6122
- // destroy particle when its time runs out
6123
- this.destroyCallback && this.destroyCallback(this);
6124
- this.destroyed = 1;
6125
- }
6410
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
6126
6411
  }
6127
6412
  }
6128
6413
  /**
@@ -6345,7 +6630,7 @@ let glContext;
6345
6630
  let glAntialias = true;
6346
6631
 
6347
6632
  // WebGL internal variables not exposed to documentation
6348
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
6633
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
6349
6634
 
6350
6635
  // WebGL internal constants
6351
6636
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6361,7 +6646,14 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
6361
6646
  // Initialize WebGL, called automatically by the engine
6362
6647
  function glInit()
6363
6648
  {
6364
- if (!glEnable || headlessMode) return;
6649
+ // keep set of texture infos so they can be restored if context is lost
6650
+ glTextureInfos = new Set;
6651
+
6652
+ if (!glEnable || headlessMode)
6653
+ {
6654
+ glCanBeEnabled = false;
6655
+ return;
6656
+ }
6365
6657
 
6366
6658
  // create the canvas and textures
6367
6659
  glCanvas = document.createElement('canvas');
@@ -6372,74 +6664,108 @@ function glInit()
6372
6664
  console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
6373
6665
  glCanvas = glContext = undefined;
6374
6666
  glEnable = false;
6667
+ glCanBeEnabled = false;
6375
6668
  return;
6376
6669
  }
6377
6670
 
6378
- // create the WebGL canvas
6671
+ // attach the WebGL canvas
6379
6672
  const rootElement = mainCanvas.parentElement;
6380
6673
  rootElement.appendChild(glCanvas);
6674
+
6675
+ // startup webgl
6676
+ initWebGL();
6677
+
6678
+ // setup context lost and restore handlers
6679
+ glCanvas.addEventListener('webglcontextlost', (e)=>
6680
+ {
6681
+ glEnable = false; // disable WebGL rendering
6682
+ glCanvas.style.display = 'none'; // hide the gl canvas
6683
+ e.preventDefault(); // prevent default to allow restoration
6684
+ LOG('WebGL context lost! Switching to Canvas2d rendering.');
6685
+
6686
+ // remove WebGL textures
6687
+ for (const info of glTextureInfos)
6688
+ info.glTexture = undefined;
6689
+ glActiveTexture = undefined;
6690
+ pluginList.forEach(plugin=>plugin.glContextLost?.());
6691
+ });
6692
+ glCanvas.addEventListener('webglcontextrestored', ()=>
6693
+ {
6694
+ glEnable = true; // re-enable WebGL rendering
6695
+ glCanvas.style.display = ''; // show the gl canvas
6696
+ LOG('WebGL context restored, reinitializing...');
6381
6697
 
6382
- // setup instanced rendering shader program
6383
- glShader = glCreateProgram(
6384
- '#version 300 es\n' + // specify GLSL ES version
6385
- 'precision highp float;'+ // use highp for better accuracy
6386
- 'uniform mat4 m;'+ // transform matrix
6387
- 'in vec2 g;'+ // in: geometry
6388
- 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
6389
- 'in float r;'+ // in: rotation
6390
- 'out vec2 v;'+ // out: uv
6391
- 'out vec4 d,e;'+ // out: color, additiveColor
6392
- 'void main(){'+ // shader entry point
6393
- 'vec2 s=(g-.5)*p.zw;'+ // get size offset
6394
- 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
6395
- 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
6396
- 'd=c;e=a;'+ // pass colors to fragment shader
6397
- '}' // end of shader
6398
- ,
6399
- '#version 300 es\n' + // specify GLSL ES version
6400
- 'precision highp float;'+ // use highp for better accuracy
6401
- 'uniform sampler2D s;'+ // texture
6402
- 'in vec2 v;'+ // in: uv
6403
- 'in vec4 d,e;'+ // in: color, additiveColor
6404
- 'out vec4 c;'+ // out: color
6405
- 'void main(){'+ // shader entry point
6406
- 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
6407
- '}' // end of shader
6408
- );
6698
+ // reinit WebGL and restore textures
6699
+ initWebGL();
6700
+ for (const info of glTextureInfos)
6701
+ info.glTexture = glCreateTexture(info.image);
6702
+ pluginList.forEach(plugin=>plugin.glContextRestored?.());
6703
+ });
6409
6704
 
6410
- // setup poly rendering shaders
6411
- glPolyShader = glCreateProgram(
6412
- '#version 300 es\n' + // specify GLSL ES version
6413
- 'precision highp float;'+ // use highp for better accuracy
6414
- 'uniform mat4 m;'+ // transform matrix
6415
- 'in vec2 p;'+ // in: position
6416
- 'in vec4 c;'+ // in: color
6417
- 'out vec4 d;'+ // out: color
6418
- 'void main(){'+ // shader entry point
6419
- 'gl_Position=m*vec4(p,1,1);'+ // transform position
6420
- 'd=c;'+ // pass color to fragment shader
6421
- '}' // end of shader
6422
- ,
6423
- '#version 300 es\n' + // specify GLSL ES version
6424
- 'precision highp float;'+ // use highp for better accuracy
6425
- 'in vec4 d;'+ // in: color
6426
- 'out vec4 c;'+ // out: color
6427
- 'void main(){'+ // shader entry point
6428
- 'c=d;'+ // set color
6429
- '}' // end of shader
6430
- );
6705
+ function initWebGL()
6706
+ {
6707
+ // setup instanced rendering shader program
6708
+ glShader = glCreateProgram(
6709
+ '#version 300 es\n' + // specify GLSL ES version
6710
+ 'precision highp float;'+ // use highp for better accuracy
6711
+ 'uniform mat4 m;'+ // transform matrix
6712
+ 'in vec2 g;'+ // in: geometry
6713
+ 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
6714
+ 'in float r;'+ // in: rotation
6715
+ 'out vec2 v;'+ // out: uv
6716
+ 'out vec4 d,e;'+ // out: color, additiveColor
6717
+ 'void main(){'+ // shader entry point
6718
+ 'vec2 s=(g-.5)*p.zw;'+ // get size offset
6719
+ 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
6720
+ 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
6721
+ 'd=c;e=a;'+ // pass colors to fragment shader
6722
+ '}' // end of shader
6723
+ ,
6724
+ '#version 300 es\n' + // specify GLSL ES version
6725
+ 'precision highp float;'+ // use highp for better accuracy
6726
+ 'uniform sampler2D s;'+ // texture
6727
+ 'in vec2 v;'+ // in: uv
6728
+ 'in vec4 d,e;'+ // in: color, additiveColor
6729
+ 'out vec4 c;'+ // out: color
6730
+ 'void main(){'+ // shader entry point
6731
+ 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
6732
+ '}' // end of shader
6733
+ );
6734
+
6735
+ // setup poly rendering shaders
6736
+ glPolyShader = glCreateProgram(
6737
+ '#version 300 es\n' + // specify GLSL ES version
6738
+ 'precision highp float;'+ // use highp for better accuracy
6739
+ 'uniform mat4 m;'+ // transform matrix
6740
+ 'in vec2 p;'+ // in: position
6741
+ 'in vec4 c;'+ // in: color
6742
+ 'out vec4 d;'+ // out: color
6743
+ 'void main(){'+ // shader entry point
6744
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
6745
+ 'd=c;'+ // pass color to fragment shader
6746
+ '}' // end of shader
6747
+ ,
6748
+ '#version 300 es\n' + // specify GLSL ES version
6749
+ 'precision highp float;'+ // use highp for better accuracy
6750
+ 'in vec4 d;'+ // in: color
6751
+ 'out vec4 c;'+ // out: color
6752
+ 'void main(){'+ // shader entry point
6753
+ 'c=d;'+ // set color
6754
+ '}' // end of shader
6755
+ );
6431
6756
 
6432
- // init buffers
6433
- const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
6434
- glPositionData = new Float32Array(glInstanceData);
6435
- glColorData = new Uint32Array(glInstanceData);
6436
- glArrayBuffer = glContext.createBuffer();
6437
- glGeometryBuffer = glContext.createBuffer();
6757
+ // init buffers
6758
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
6759
+ glPositionData = new Float32Array(glInstanceData);
6760
+ glColorData = new Uint32Array(glInstanceData);
6761
+ glArrayBuffer = glContext.createBuffer();
6762
+ glGeometryBuffer = glContext.createBuffer();
6438
6763
 
6439
- // create the geometry buffer, triangle strip square
6440
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
6441
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6442
- glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
6764
+ // create the geometry buffer, triangle strip square
6765
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
6766
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6767
+ glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
6768
+ }
6443
6769
  }
6444
6770
 
6445
6771
  function glSetInstancedMode()
@@ -6517,8 +6843,8 @@ function glPreRender()
6517
6843
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
6518
6844
  const rotatedCam = cameraPos.rotate(-cameraAngle);
6519
6845
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
6520
- const ca = Math.cos(cameraAngle);
6521
- const sa = Math.sin(cameraAngle);
6846
+ const ca = cos(cameraAngle);
6847
+ const sa = sin(cameraAngle);
6522
6848
  const transform = [
6523
6849
  s.x * ca, s.y * sa, 0, 0,
6524
6850
  -s.x * sa, s.y * ca, 0, 0,
@@ -6546,7 +6872,7 @@ function glPreRender()
6546
6872
  // start with additive blending off
6547
6873
  glAdditive = glBatchAdditive = false;
6548
6874
 
6549
- // force it to enter instanced mode
6875
+ // force it to set instanced mode by first setting poly mode true
6550
6876
  glPolyMode = true;
6551
6877
  glSetInstancedMode();
6552
6878
  }
@@ -6687,6 +7013,41 @@ function glSetTextureData(texture, image)
6687
7013
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
6688
7014
  }
6689
7015
 
7016
+ /** Tells WebGL to create or update the glTexture and start tracking it
7017
+ * @param {TextureInfo} textureInfo
7018
+ * @memberof WebGL */
7019
+ function glRegisterTextureInfo(textureInfo)
7020
+ {
7021
+ if (headlessMode) return;
7022
+
7023
+ // add texture info to tracking list even if gl is not enabled
7024
+ glTextureInfos.add(textureInfo);
7025
+
7026
+ if (!glContext) return;
7027
+
7028
+ // create or set the texture data
7029
+ if (textureInfo.glTexture)
7030
+ glSetTextureData(textureInfo.glTexture, textureInfo.image);
7031
+ else
7032
+ textureInfo.glTexture = glCreateTexture(textureInfo.image);
7033
+ }
7034
+
7035
+ /** Tells WebGL to destroy the glTexture and stop tracking it
7036
+ * @param {TextureInfo} textureInfo
7037
+ * @memberof WebGL */
7038
+ function glUnregisterTextureInfo(textureInfo)
7039
+ {
7040
+ if (headlessMode) return;
7041
+
7042
+ // delete texture info from tracking list even if gl is not enabled
7043
+ glTextureInfos.delete(textureInfo);
7044
+
7045
+ // unset and destroy the texture
7046
+ const glTexture = textureInfo.glTexture;
7047
+ textureInfo.glTexture = undefined;
7048
+ glDeleteTexture(glTexture);
7049
+ }
7050
+
6690
7051
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
6691
7052
  * @memberof WebGL */
6692
7053
  function glFlush()
@@ -6788,8 +7149,8 @@ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
6788
7149
  // transform the point
6789
7150
  const px = p.x*sx;
6790
7151
  const py = p.y*sy;
6791
- const sa = Math.sin(-angle);
6792
- const ca = Math.cos(-angle);
7152
+ const sa = sin(-angle);
7153
+ const ca = cos(-angle);
6793
7154
  pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
6794
7155
  }
6795
7156
  const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
@@ -7277,80 +7638,97 @@ class PostProcessPlugin
7277
7638
  /** Create global post processing shader
7278
7639
  * @param {string} shaderCode
7279
7640
  * @param {boolean} [includeOverlay]
7641
+ * @param {boolean} [includeMainCanvas]
7280
7642
  * @example
7281
7643
  * // create the post process plugin object
7282
7644
  * new PostProcessPlugin(shaderCode);
7283
7645
  */
7284
- constructor(shaderCode, includeOverlay=false)
7646
+ constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
7285
7647
  {
7286
7648
  ASSERT(!postProcess, 'Post process already initialized');
7287
7649
  postProcess = this;
7288
7650
 
7289
- if (headlessMode) return;
7290
-
7291
- if (!glEnable)
7292
- {
7293
- console.warn('PostProcessPlugin: WebGL not enabled!');
7294
- return;
7295
- }
7296
-
7297
7651
  if (!shaderCode) // default shader pass through
7298
7652
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
7299
7653
 
7300
7654
  /** @property {WebGLProgram} - Shader for post processing */
7301
- this.shader = glCreateProgram(
7302
- '#version 300 es\n' + // specify GLSL ES version
7303
- 'precision highp float;'+ // use highp for better accuracy
7304
- 'in vec2 p;'+ // position
7305
- 'void main(){'+ // shader entry point
7306
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7307
- '}' // end of shader
7308
- ,
7309
- '#version 300 es\n' + // specify GLSL ES version
7310
- 'precision highp float;'+ // use highp for better accuracy
7311
- 'uniform sampler2D iChannel0;'+ // input texture
7312
- 'uniform vec3 iResolution;'+ // size of output texture
7313
- 'uniform float iTime;'+ // time
7314
- 'out vec4 c;'+ // out color
7315
- '\n' + shaderCode + '\n'+ // insert custom shader code
7316
- 'void main(){'+ // shader entry point
7317
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
7318
- 'c.a=1.;'+ // always use full alpha
7319
- '}' // end of shader
7320
- );
7655
+ this.shader = undefined;
7321
7656
 
7322
7657
  /** @property {WebGLTexture} - Texture for post processing */
7323
- this.texture = glCreateTexture();
7658
+ this.texture = undefined;
7324
7659
 
7325
- /** @property {boolean} - Should overlay canvas be included in post processing */
7326
- this.includeOverlay = includeOverlay;
7660
+ // setup the post processing plugin
7661
+ initPostProcess();
7662
+ engineAddPlugin(undefined, postProcessRender, postProcessContextLost, postProcessContextRestored);
7327
7663
 
7328
- // Render the post processing shader, called automatically by the engine
7329
- engineAddPlugin(undefined, postProcessRender);
7330
- function postProcessRender()
7664
+ function initPostProcess()
7331
7665
  {
7332
7666
  if (headlessMode) return;
7333
-
7334
- // prepare to render post process shader
7335
- if (glEnable)
7336
- {
7337
- glFlush(); // clear out the buffer
7338
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
7339
- }
7340
- else
7667
+
7668
+ if (!glEnable)
7341
7669
  {
7342
- // set the viewport
7343
- glContext.viewport(0, 0, glCanvas.width = drawCanvas.width, glCanvas.height = drawCanvas.height);
7670
+ console.warn('PostProcessPlugin: WebGL not enabled!');
7671
+ return;
7344
7672
  }
7345
7673
 
7346
- if (postProcess.includeOverlay)
7674
+ // create resources
7675
+ postProcess.texture = glCreateTexture();
7676
+ postProcess.shader = glCreateProgram(
7677
+ '#version 300 es\n' + // specify GLSL ES version
7678
+ 'precision highp float;'+ // use highp for better accuracy
7679
+ 'in vec2 p;'+ // position
7680
+ 'void main(){'+ // shader entry point
7681
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7682
+ '}' // end of shader
7683
+ ,
7684
+ '#version 300 es\n' + // specify GLSL ES version
7685
+ 'precision highp float;'+ // use highp for better accuracy
7686
+ 'uniform sampler2D iChannel0;'+ // input texture
7687
+ 'uniform vec3 iResolution;'+ // size of output texture
7688
+ 'uniform float iTime;'+ // time
7689
+ 'out vec4 c;'+ // out color
7690
+ '\n' + shaderCode + '\n'+ // insert custom shader code
7691
+ 'void main(){'+ // shader entry point
7692
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
7693
+ 'c.a=1.;'+ // always use full alpha
7694
+ '}' // end of shader
7695
+ );
7696
+ }
7697
+ function postProcessContextLost()
7698
+ {
7699
+ postProcess.shader = undefined;
7700
+ postProcess.texture = undefined;
7701
+ LOG('PostProcessPlugin: WebGL context lost');
7702
+ }
7703
+ function postProcessContextRestored()
7704
+ {
7705
+ initPostProcess();
7706
+ LOG('PostProcessPlugin: WebGL context restored');
7707
+ }
7708
+ function postProcessRender()
7709
+ {
7710
+ if (headlessMode) return;
7711
+
7712
+ if (!glEnable)
7713
+ return;
7714
+
7715
+ // clear out the buffer
7716
+ glFlush();
7717
+
7718
+ if (includeMainCanvas || includeOverlay)
7347
7719
  {
7348
- // copy overlay canvas so it will be included in post processing
7349
- mainContext.drawImage(overlayCanvas, 0, 0);
7350
- overlayCanvas.width |= 0;
7720
+ // copy WebGL to the main canvas
7721
+ mainContext.drawImage(glCanvas, 0, 0);
7722
+
7723
+ if (includeOverlay)
7724
+ {
7725
+ // copy overlay canvas so it will be included in post processing
7726
+ mainContext.drawImage(overlayCanvas, 0, 0);
7727
+ overlayCanvas.width |= 0; // clear overlay canvas
7728
+ }
7351
7729
  }
7352
7730
 
7353
- // setup shader program to draw one triangle
7731
+ // setup shader program to draw a quad
7354
7732
  glContext.useProgram(postProcess.shader);
7355
7733
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7356
7734
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
@@ -7359,7 +7737,10 @@ class PostProcessPlugin
7359
7737
  // set textures, pass in the 2d canvas and gl canvas in separate texture channels
7360
7738
  glContext.activeTexture(glContext.TEXTURE0);
7361
7739
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
7362
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
7740
+ if (includeMainCanvas || includeOverlay)
7741
+ {
7742
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
7743
+ }
7363
7744
 
7364
7745
  // set vertex position attribute
7365
7746
  const vertexByteStride = 8;
@@ -7589,6 +7970,8 @@ class UISystemPlugin
7589
7970
  this.defaultHoverColor = hsl(0,0,.9);
7590
7971
  /** @property {Color} - Default color for disabled UI elements */
7591
7972
  this.defaultDisabledColor = hsl(0,0,.3);
7973
+ /** @property {Color} - Uses a gradient fill combined with color */
7974
+ this.defaultGradientColor = undefined;
7592
7975
  /** @property {number} - Default line width for UI elements */
7593
7976
  this.defaultLineWidth = 4;
7594
7977
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -7596,7 +7979,7 @@ class UISystemPlugin
7596
7979
  /** @property {number} - Default scale to use for fitting text to object */
7597
7980
  this.defaultTextScale = .8;
7598
7981
  /** @property {string} - Default font for UI elements */
7599
- this.defaultFont = 'arial';
7982
+ this.defaultFont = fontDefault;
7600
7983
  /** @property {Sound} - Default sound when interactive UI element is pressed */
7601
7984
  this.defaultSoundPress = undefined;
7602
7985
  /** @property {Sound} - Default sound when interactive UI element is released */
@@ -7613,7 +7996,9 @@ class UISystemPlugin
7613
7996
  this.hoverObject = undefined;
7614
7997
  /** @property {UIObject} - Hover object at start of update */
7615
7998
  this.lastHoverObject = undefined;
7616
-
7999
+ /** @property {number} - If set ui coords will be renormalized to this canvas height */
8000
+ this.nativeHeight = 0;
8001
+
7617
8002
  engineAddPlugin(uiUpdate, uiRender);
7618
8003
 
7619
8004
  // setup recursive update and render
@@ -7653,6 +8038,17 @@ class UISystemPlugin
7653
8038
  }
7654
8039
  function uiRender()
7655
8040
  {
8041
+ const context = uiSystem.uiContext;
8042
+ context.save();
8043
+ if (uiSystem.nativeHeight)
8044
+ {
8045
+ // convert to native height
8046
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8047
+ context.translate(-s*mainCanvasSize.x/2,0);
8048
+ context.scale(s,s);
8049
+ context.translate(mainCanvasSize.x/2/s,0);
8050
+ }
8051
+
7656
8052
  function renderObject(o)
7657
8053
  {
7658
8054
  if (!o.visible)
@@ -7664,6 +8060,7 @@ class UISystemPlugin
7664
8060
  renderObject(c);
7665
8061
  }
7666
8062
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
8063
+ context.restore();
7667
8064
  }
7668
8065
  }
7669
8066
 
@@ -7673,8 +8070,9 @@ class UISystemPlugin
7673
8070
  * @param {Color} [color=uiSystem.defaultColor]
7674
8071
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
7675
8072
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7676
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
7677
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
8073
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
8074
+ * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
8075
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
7678
8076
  {
7679
8077
  ASSERT(isVector2(pos), 'pos must be a vec2');
7680
8078
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -7684,7 +8082,18 @@ class UISystemPlugin
7684
8082
  ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
7685
8083
 
7686
8084
  const context = uiSystem.uiContext;
7687
- context.fillStyle = color.toString();
8085
+ if (gradientColor)
8086
+ {
8087
+ const g = context.createLinearGradient(
8088
+ pos.x, pos.y-size.y/2, pos.x, pos.y+size.y/2);
8089
+ const c = color.toString();
8090
+ g.addColorStop(0, c);
8091
+ g.addColorStop(.5, gradientColor.toString());
8092
+ g.addColorStop(1, c);
8093
+ context.fillStyle = g;
8094
+ }
8095
+ else
8096
+ context.fillStyle = color.toString();
7688
8097
  context.beginPath();
7689
8098
  if (cornerRadius && context['roundRect'])
7690
8099
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
@@ -7741,10 +8150,11 @@ class UISystemPlugin
7741
8150
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7742
8151
  * @param {string} [align]
7743
8152
  * @param {string} [font=uiSystem.defaultFont]
8153
+ * @param {string} [fontStyle]
7744
8154
  * @param {boolean} [applyMaxWidth=true] */
7745
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, applyMaxWidth=true)
8155
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
7746
8156
  {
7747
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8157
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
7748
8158
  }
7749
8159
 
7750
8160
  /**
@@ -7805,17 +8215,21 @@ class UIObject
7805
8215
  /** @property {boolean} - Is this object disabled? */
7806
8216
  this.disabled = false;
7807
8217
  /** @property {Color} - Color for text */
7808
- this.textColor = uiSystem.defaultTextColor.copy()
8218
+ this.textColor = uiSystem.defaultTextColor.copy();
7809
8219
  /** @property {Color} - Color used when hovering over the object */
7810
- this.hoverColor = uiSystem.defaultHoverColor.copy()
8220
+ this.hoverColor = uiSystem.defaultHoverColor.copy();
7811
8221
  /** @property {Color} - Color for line drawing */
7812
- this.lineColor = uiSystem.defaultLineColor.copy()
8222
+ this.lineColor = uiSystem.defaultLineColor.copy();
8223
+ /** @property {Color} - Uses a gradient fill combined with color */
8224
+ this.gradientColor = uiSystem.defaultGradientColor ? uiSystem.defaultGradientColor.copy() : undefined;
7813
8225
  /** @property {number} - Width for line drawing */
7814
8226
  this.lineWidth = uiSystem.defaultLineWidth;
7815
8227
  /** @property {number} - Corner radius for rounded rects */
7816
8228
  this.cornerRadius = uiSystem.defaultCornerRadius;
7817
8229
  /** @property {string} - Font for this objecct */
7818
8230
  this.font = uiSystem.defaultFont;
8231
+ /** @property {string} - Font style for this object or undefined */
8232
+ this.fontStyle = undefined;
7819
8233
  /** @property {number} - Override for text width */
7820
8234
  this.textWidth = undefined;
7821
8235
  /** @property {number} - Override for text height */
@@ -7840,6 +8254,8 @@ class UIObject
7840
8254
  this.interactive = false;
7841
8255
  /** @property {boolean} - Activate when dragged over with mouse held down */
7842
8256
  this.dragActivate = false;
8257
+ /** @property {boolean} - True if this can be a hover object */
8258
+ this.canBeHover = true;
7843
8259
  uiSystem.uiObjects.push(this);
7844
8260
  }
7845
8261
 
@@ -7863,6 +8279,26 @@ class UIObject
7863
8279
  child.parent = undefined;
7864
8280
  }
7865
8281
 
8282
+ /** Check if the mouse is overlapping a box in screen space
8283
+ * @return {boolean} - True if overlapping
8284
+ */
8285
+ isMouseOverlapping()
8286
+ {
8287
+ const size = !isTouchDevice ? this.size :
8288
+ this.size.add(vec2(this.extraTouchSize || 0));
8289
+ if (!uiSystem.nativeHeight)
8290
+ return isOverlapping(this.pos, size, mousePosScreen);
8291
+
8292
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8293
+ const sInv = 1/s;
8294
+ let pos = mousePosScreen.copy();
8295
+ pos.x += s*mainCanvasSize.x/2;
8296
+ pos.x *= sInv;
8297
+ pos.y *= sInv;
8298
+ pos.x -= sInv*mainCanvasSize.x/2;
8299
+ return isOverlapping(this.pos, size, pos);
8300
+ }
8301
+
7866
8302
  /** Update the object, called automatically by plugin once each frame */
7867
8303
  update()
7868
8304
  {
@@ -7870,13 +8306,10 @@ class UIObject
7870
8306
  const isActive = this.isActiveObject();
7871
8307
  const mouseDown = mouseIsDown(0);
7872
8308
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
7873
- if (!uiSystem.hoverObject)
8309
+ if (this.canBeHover)
7874
8310
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
7875
- {
7876
- const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
7877
- if (isOverlapping(this.pos, size, mousePosScreen))
7878
- uiSystem.hoverObject = this;
7879
- }
8311
+ if (!uiSystem.hoverObject && this.isMouseOverlapping())
8312
+ uiSystem.hoverObject = this;
7880
8313
  if (this.isHoverObject())
7881
8314
  {
7882
8315
  if (!this.disabled)
@@ -8001,11 +8434,13 @@ class UIText extends UIObject
8001
8434
 
8002
8435
  // make text not outlined by default
8003
8436
  this.lineWidth = 0;
8437
+ // text can not be a hover object by default
8438
+ this.canBeHover = false;
8004
8439
  }
8005
8440
  render()
8006
8441
  {
8007
8442
  const textSize = this.getTextSize();
8008
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
8443
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
8009
8444
  }
8010
8445
  }
8011
8446
 
@@ -8071,7 +8506,7 @@ class UIButton extends UIObject
8071
8506
 
8072
8507
  // set properties
8073
8508
  this.text = text;
8074
- this.color = color.copy()
8509
+ this.color = color.copy();
8075
8510
  this.interactive = true;
8076
8511
  }
8077
8512
  render()
@@ -8081,7 +8516,7 @@ class UIButton extends UIObject
8081
8516
  // draw the text scaled to fit
8082
8517
  const textSize = this.getTextSize();
8083
8518
  uiSystem.drawText(this.text, this.pos, textSize,
8084
- this.textColor, 0, undefined, this.align, this.font);
8519
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8085
8520
  }
8086
8521
  }
8087
8522
 
@@ -8135,7 +8570,7 @@ class UICheckbox extends UIObject
8135
8570
  const textSize = this.getTextSize();
8136
8571
  const pos = this.pos.add(vec2(this.size.x,0));
8137
8572
  uiSystem.drawText(this.text, pos, textSize,
8138
- this.textColor, 0, undefined, 'left', this.font, false);
8573
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
8139
8574
  }
8140
8575
  }
8141
8576
 
@@ -8213,14 +8648,13 @@ class UIScrollbar extends UIObject
8213
8648
  const handlePos = isHorizontal ?
8214
8649
  vec2(lerp(p1, p2, this.value), this.pos.y) :
8215
8650
  vec2(this.pos.x, lerp(p2, p1, this.value))
8216
- const handleColor = this.disabled ? this.disabledColor :
8217
- this.interactive && this.isActiveObject() ? this.color : this.handleColor;
8651
+ const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8218
8652
  uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8219
8653
 
8220
8654
  // draw the text scaled to fit on the scrollbar
8221
8655
  const textSize = this.getTextSize();
8222
8656
  uiSystem.drawText(this.text, this.pos, textSize,
8223
- this.textColor, 0, undefined, this.align, this.font);
8657
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8224
8658
  }
8225
8659
  }
8226
8660
  /**