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();
@@ -1325,6 +1339,33 @@ function debugVideoCaptureUpdate()
1325
1339
  combineCanvases();
1326
1340
  debugVideoCaptureTrack.requestFrame();
1327
1341
  debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
1342
+ }
1343
+
1344
+ // make color constants immutable with debug assertions
1345
+ function debugProtectConstant(obj)
1346
+ {
1347
+ if (debug)
1348
+ {
1349
+ // get properties and store original values
1350
+ const props = Object.keys(obj), values = {};
1351
+ props.forEach(prop => values[prop] = obj[prop]);
1352
+
1353
+ // replace with getters/setters that assert
1354
+ props.forEach(prop =>
1355
+ {
1356
+ Object.defineProperty(obj, prop, {
1357
+ get: () => values[prop],
1358
+ set: (value) =>
1359
+ {
1360
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1361
+ },
1362
+ enumerable: true
1363
+ });
1364
+ });
1365
+ }
1366
+
1367
+ // freeze the object to prevent adding new properties
1368
+ return Object.freeze(obj);
1328
1369
  }
1329
1370
  /**
1330
1371
  * LittleJS Utility Classes and Functions
@@ -1336,7 +1377,7 @@ function debugVideoCaptureUpdate()
1336
1377
  * @namespace Utilities
1337
1378
  */
1338
1379
 
1339
- /** A shortcut to get Math.PI
1380
+ /** The value of PI
1340
1381
  * @type {number}
1341
1382
  * @default Math.PI
1342
1383
  * @memberof Utilities */
@@ -1346,25 +1387,80 @@ const PI = Math.PI;
1346
1387
  * @param {number} value
1347
1388
  * @return {number}
1348
1389
  * @memberof Utilities */
1349
- function abs(value) { return Math.abs(value); }
1390
+ const abs = Math.abs;
1391
+
1392
+ /** Returns floored value of value passed in
1393
+ * @param {number} value
1394
+ * @return {number}
1395
+ * @memberof Utilities */
1396
+ const floor = Math.floor;
1397
+
1398
+ /** Returns ceiled value of value passed in
1399
+ * @param {number} value
1400
+ * @return {number}
1401
+ * @memberof Utilities */
1402
+ const ceil = Math.ceil;
1403
+
1404
+ /** Returns rounded value passed in
1405
+ * @param {number} value
1406
+ * @return {number}
1407
+ * @memberof Utilities */
1408
+ const round = Math.round;
1350
1409
 
1351
1410
  /** Returns lowest value passed in
1352
1411
  * @param {...number} values
1353
1412
  * @return {number}
1354
1413
  * @memberof Utilities */
1355
- function min(...values) { return Math.min(...values); }
1414
+ const min = Math.min;
1356
1415
 
1357
1416
  /** Returns highest value passed in
1358
1417
  * @param {...number} values
1359
1418
  * @return {number}
1360
1419
  * @memberof Utilities */
1361
- function max(...values) { return Math.max(...values); }
1420
+ const max = Math.max;
1362
1421
 
1363
1422
  /** Returns the sign of value passed in
1364
1423
  * @param {number} value
1365
1424
  * @return {number}
1366
1425
  * @memberof Utilities */
1367
- function sign(value) { return Math.sign(value); }
1426
+ const sign = Math.sign;
1427
+
1428
+ /** Returns hypotenuse of values passed in
1429
+ * @param {...number} values
1430
+ * @return {number}
1431
+ * @memberof Utilities */
1432
+ const hypot = Math.hypot;
1433
+
1434
+ /** Returns log2 of value passed in
1435
+ * @param {number} value
1436
+ * @return {number}
1437
+ * @memberof Utilities */
1438
+ const log2 = Math.log2;
1439
+
1440
+ /** Returns sin of value passed in
1441
+ * @param {number} value
1442
+ * @return {number}
1443
+ * @memberof Utilities */
1444
+ const sin = Math.sin;
1445
+
1446
+ /** Returns cos of value passed in
1447
+ * @param {number} value
1448
+ * @return {number}
1449
+ * @memberof Utilities */
1450
+ const cos = Math.cos;
1451
+
1452
+ /** Returns tan of value passed in
1453
+ * @param {number} value
1454
+ * @return {number}
1455
+ * @memberof Utilities */
1456
+ const tan = Math.tan;
1457
+
1458
+ /** Returns atan2 of values passed in
1459
+ * @param {number} y
1460
+ * @param {number} x
1461
+ * @return {number}
1462
+ * @memberof Utilities */
1463
+ const atan2 = Math.atan2;
1368
1464
 
1369
1465
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
1370
1466
  * @param {number} dividend
@@ -1469,7 +1565,7 @@ function isPowerOfTwo(value) { return !(value & (value - 1)); }
1469
1565
  * @param {number} value
1470
1566
  * @return {number}
1471
1567
  * @memberof Utilities */
1472
- function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
1568
+ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
1473
1569
 
1474
1570
  /** Returns true if two axis aligned bounding boxes are overlapping
1475
1571
  * this can be used for simple collision detection between objects
@@ -1537,7 +1633,7 @@ function isIntersecting(start, end, pos, size)
1537
1633
  * @return {number} - Value waving between 0 and amplitude
1538
1634
  * @memberof Utilities */
1539
1635
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1540
- { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
1636
+ { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
1541
1637
 
1542
1638
  /** Formats seconds to mm:ss style for display purposes
1543
1639
  * @param {number} t - time in seconds
@@ -1576,6 +1672,96 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1576
1672
  * @memberof Utilities */
1577
1673
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1578
1674
 
1675
+ /**
1676
+ * @callback LineTestFunction - Checks if a position is colliding
1677
+ * @param {Vector2} pos
1678
+ * @memberof Draw
1679
+ */
1680
+
1681
+ /**
1682
+ * Casts a ray and returns position of the first collision found, or undefined if none are found
1683
+ * @param {Vector2} posStart
1684
+ * @param {Vector2} posEnd
1685
+ * @param {LineTestFunction} testFunction - Check if colliding
1686
+ * @param {Vector2} [normal] - Optional vector to store the normal
1687
+ * @return {Vector2|undefined} - Position of the collision or undefined if none found
1688
+ * @memberof Utilities */
1689
+ function lineTest(posStart, posEnd, testFunction, normal)
1690
+ {
1691
+ ASSERT(isVector2(posStart), 'posStart must be a vec2');
1692
+ ASSERT(isVector2(posEnd), 'posEnd must be a vec2');
1693
+ ASSERT(typeof testFunction === 'function', 'testFunction must be a function');
1694
+ ASSERT(!normal || isVector2(normal), 'normal must be a vec2');
1695
+
1696
+ // get ray direction and length
1697
+ const dx = posEnd.x - posStart.x;
1698
+ const dy = posEnd.y - posStart.y;
1699
+ const totalLength = hypot(dx, dy);
1700
+ if (!totalLength)
1701
+ return;
1702
+
1703
+ // current integer cell we are in
1704
+ const pos = posStart.floor();
1705
+
1706
+ // normalize ray direction
1707
+ const dirX = dx / totalLength;
1708
+ const dirY = dy / totalLength;
1709
+
1710
+ // step direction in grid
1711
+ const stepX = sign(dirX);
1712
+ const stepY = sign(dirY);
1713
+
1714
+ // distance along the ray to cross one full cell in X or Y
1715
+ const tDeltaX = dirX ? abs(1 / dirX) : Infinity;
1716
+ const tDeltaY = dirY ? abs(1 / dirY) : Infinity;
1717
+
1718
+ // distance along the ray from start to the first grid boundary
1719
+ const nextGridX = stepX > 0 ? pos.x + 1 : pos.x;
1720
+ const nextGridY = stepY > 0 ? pos.y + 1 : pos.y;
1721
+ const tMaxX = dirX ? (nextGridX - posStart.x) / dirX : Infinity;
1722
+ const tMaxY = dirY ? (nextGridY - posStart.y) / dirY : Infinity;
1723
+
1724
+ // use line drawing algorithm to test for collisions
1725
+ let t = 0, tX = tMaxX, tY = tMaxY, wasX = tDeltaX < tDeltaY;
1726
+ while (t < totalLength)
1727
+ {
1728
+ if (testFunction(pos))
1729
+ {
1730
+ // set hit point
1731
+ const hitPos = vec2(posStart.x + dirX*t, posStart.y + dirY*t);
1732
+
1733
+ // move inside of tile if on positive edge
1734
+ const e = 1e-9;
1735
+ if (wasX)
1736
+ {
1737
+ if (stepX < 0)
1738
+ hitPos.x -= e;
1739
+ }
1740
+ if (stepY < 0)
1741
+ hitPos.y -= e;
1742
+
1743
+ // set normal
1744
+ if (normal)
1745
+ wasX ? normal.set(-stepX,0) : normal.set(0,-stepY);
1746
+ return hitPos;
1747
+ }
1748
+
1749
+ // advance to the next grid boundary
1750
+ if (wasX = tX < tY)
1751
+ {
1752
+ pos.x += stepX;
1753
+ t = tX;
1754
+ tX += tDeltaX;
1755
+ }
1756
+ else
1757
+ {
1758
+ pos.y += stepY;
1759
+ t = tY;
1760
+ tY += tDeltaY;
1761
+ }
1762
+ }
1763
+ }
1764
+
1579
1765
  ///////////////////////////////////////////////////////////////////////////////
1580
1766
 
1581
1767
  /** Random global functions
@@ -1594,7 +1780,7 @@ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valu
1594
1780
  * @param {number} [valueB]
1595
1781
  * @return {number}
1596
1782
  * @memberof Random */
1597
- function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
1783
+ function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
1598
1784
 
1599
1785
  /** Randomly returns true or false given the chance of true passed in
1600
1786
  * @param {number} [chance]
@@ -1673,7 +1859,7 @@ class RandomGenerator
1673
1859
  * @param {number} valueA
1674
1860
  * @param {number} [valueB]
1675
1861
  * @return {number} */
1676
- int(valueA, valueB=0) { return Math.floor(this.float(valueA, valueB)); }
1862
+ int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
1677
1863
 
1678
1864
  /** Randomly returns true or false given the chance of true passed in
1679
1865
  * @param {number} [chance]
@@ -1700,6 +1886,39 @@ class RandomGenerator
1700
1886
  * @return {Vector2} */
1701
1887
  vec2(valueA=1, valueB=0)
1702
1888
  { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
1889
+
1890
+ /** Returns a random color between the two passed in colors, combine components if linear
1891
+ * @param {Color} [colorA=(1,1,1,1)]
1892
+ * @param {Color} [colorB=(0,0,0,1)]
1893
+ * @param {boolean} [linear]
1894
+ * @return {Color} */
1895
+ randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1896
+ {
1897
+ return linear ? colorA.lerp(colorB, this.float()) :
1898
+ new Color(
1899
+ this.float(colorA.r,colorB.r),
1900
+ this.float(colorA.g,colorB.g),
1901
+ this.float(colorA.b,colorB.b),
1902
+ this.float(colorA.a,colorB.a));
1903
+ }
1904
+
1905
+ /** Returns a new color that has each component randomly adjusted
1906
+ * @param {Color} color
1907
+ * @param {number} [amount]
1908
+ * @param {number} [alphaAmount]
1909
+ * @return {Color} */
1910
+ mutateColor(color, amount=.05, alphaAmount=0)
1911
+ {
1912
+ ASSERT_NUMBER_VALID(amount);
1913
+ ASSERT_NUMBER_VALID(alphaAmount);
1914
+ return new Color
1915
+ (
1916
+ color.r + this.float(amount, -amount),
1917
+ color.g + this.float(amount, -amount),
1918
+ color.b + this.float(amount, -amount),
1919
+ color.a + this.float(alphaAmount, -alphaAmount)
1920
+ ).clamp();
1921
+ }
1703
1922
  }
1704
1923
 
1705
1924
  ///////////////////////////////////////////////////////////////////////////////
@@ -1851,7 +2070,7 @@ class Vector2
1851
2070
 
1852
2071
  /** Returns the clockwise angle of this vector, up is angle 0
1853
2072
  * @return {number} */
1854
- angle() { return Math.atan2(this.x, this.y); }
2073
+ angle() { return atan2(this.x, this.y); }
1855
2074
 
1856
2075
  /** Sets this vector with clockwise angle and length passed in
1857
2076
  * @param {number} [angle]
@@ -1861,8 +2080,8 @@ class Vector2
1861
2080
  {
1862
2081
  ASSERT_NUMBER_VALID(angle);
1863
2082
  ASSERT_NUMBER_VALID(length);
1864
- this.x = length*Math.sin(angle);
1865
- this.y = length*Math.cos(angle);
2083
+ this.x = length*sin(angle);
2084
+ this.y = length*cos(angle);
1866
2085
  return this;
1867
2086
  }
1868
2087
 
@@ -1872,7 +2091,7 @@ class Vector2
1872
2091
  rotate(angle)
1873
2092
  {
1874
2093
  ASSERT_NUMBER_VALID(angle);
1875
- const c = Math.cos(-angle), s = Math.sin(-angle);
2094
+ const c = cos(-angle), s = sin(-angle);
1876
2095
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1877
2096
  }
1878
2097
 
@@ -1904,7 +2123,7 @@ class Vector2
1904
2123
 
1905
2124
  /** Returns a copy of this vector with each axis floored
1906
2125
  * @return {Vector2} */
1907
- floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
2126
+ floor() { return new Vector2(floor(this.x), floor(this.y)); }
1908
2127
 
1909
2128
  /** Returns new vec2 with modded values
1910
2129
  * @param {number} [divisor]
@@ -2212,67 +2431,67 @@ class Color
2212
2431
  /** Color - White #ffffff
2213
2432
  * @type {Color}
2214
2433
  * @memberof Utilities */
2215
- const WHITE = protectEngineConstant(rgb());
2434
+ const WHITE = debugProtectConstant(rgb());
2216
2435
 
2217
2436
  /** Color - Clear White #757474ff with 0 alpha
2218
2437
  * @type {Color}
2219
2438
  * @memberof Utilities */
2220
- const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
2439
+ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
2221
2440
 
2222
2441
  /** Color - Black #000000
2223
2442
  * @type {Color}
2224
2443
  * @memberof Utilities */
2225
- const BLACK = protectEngineConstant(rgb(0,0,0));
2444
+ const BLACK = debugProtectConstant(rgb(0,0,0));
2226
2445
 
2227
2446
  /** Color - Clear Black #000000 with 0 alpha
2228
2447
  * @type {Color}
2229
2448
  * @memberof Utilities */
2230
- const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
2449
+ const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
2231
2450
 
2232
2451
  /** Color - Gray #808080
2233
2452
  * @type {Color}
2234
2453
  * @memberof Utilities */
2235
- const GRAY = protectEngineConstant(rgb(.5,.5,.5));
2454
+ const GRAY = debugProtectConstant(rgb(.5,.5,.5));
2236
2455
 
2237
2456
  /** Color - Red #ff0000
2238
2457
  * @type {Color}
2239
2458
  * @memberof Utilities */
2240
- const RED = protectEngineConstant(rgb(1,0,0));
2459
+ const RED = debugProtectConstant(rgb(1,0,0));
2241
2460
 
2242
2461
  /** Color - Orange #ff8000
2243
2462
  * @type {Color}
2244
2463
  * @memberof Utilities */
2245
- const ORANGE = protectEngineConstant(rgb(1,.5,0));
2464
+ const ORANGE = debugProtectConstant(rgb(1,.5,0));
2246
2465
 
2247
2466
  /** Color - Yellow #ffff00
2248
2467
  * @type {Color}
2249
2468
  * @memberof Utilities */
2250
- const YELLOW = protectEngineConstant(rgb(1,1,0));
2469
+ const YELLOW = debugProtectConstant(rgb(1,1,0));
2251
2470
 
2252
2471
  /** Color - Green #00ff00
2253
2472
  * @type {Color}
2254
2473
  * @memberof Utilities */
2255
- const GREEN = protectEngineConstant(rgb(0,1,0));
2474
+ const GREEN = debugProtectConstant(rgb(0,1,0));
2256
2475
 
2257
2476
  /** Color - Cyan #00ffff
2258
2477
  * @type {Color}
2259
2478
  * @memberof Utilities */
2260
- const CYAN = protectEngineConstant(rgb(0,1,1));
2479
+ const CYAN = debugProtectConstant(rgb(0,1,1));
2261
2480
 
2262
2481
  /** Color - Blue #0000ff
2263
2482
  * @type {Color}
2264
2483
  * @memberof Utilities */
2265
- const BLUE = protectEngineConstant(rgb(0,0,1));
2484
+ const BLUE = debugProtectConstant(rgb(0,0,1));
2266
2485
 
2267
2486
  /** Color - Purple #8000ff
2268
2487
  * @type {Color}
2269
2488
  * @memberof Utilities */
2270
- const PURPLE = protectEngineConstant(rgb(.5,0,1));
2489
+ const PURPLE = debugProtectConstant(rgb(.5,0,1));
2271
2490
 
2272
2491
  /** Color - Magenta #ff00ff
2273
2492
  * @type {Color}
2274
2493
  * @memberof Utilities */
2275
- const MAGENTA = protectEngineConstant(rgb(1,0,1));
2494
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2276
2495
 
2277
2496
  ///////////////////////////////////////////////////////////////////////////////
2278
2497
 
@@ -2335,41 +2554,11 @@ class Timer
2335
2554
 
2336
2555
  /** Returns this timer expressed as a string
2337
2556
  * @return {string} */
2338
- toString() { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2557
+ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2339
2558
 
2340
2559
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2341
2560
  * @return {number} */
2342
2561
  valueOf() { return this.get(); }
2343
- }
2344
-
2345
- ///////////////////////////////////////////////////////////////////////////////
2346
- // Helper functions used by the engine
2347
-
2348
- // make color constants immutable with debug assertions
2349
- function protectEngineConstant(obj)
2350
- {
2351
- if (debug)
2352
- {
2353
- // get properties and store original values
2354
- const props = Object.keys(obj), values = {};
2355
- props.forEach(prop => values[prop] = obj[prop]);
2356
-
2357
- // replace with getters/setters that assert
2358
- props.forEach(prop =>
2359
- {
2360
- Object.defineProperty(obj, prop, {
2361
- get: () => values[prop],
2362
- set: (value) =>
2363
- {
2364
- ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
2365
- },
2366
- enumerable: true
2367
- });
2368
- });
2369
- }
2370
-
2371
- // freeze the object to prevent adding new properties
2372
- return Object.freeze(obj);
2373
2562
  }
2374
2563
  /**
2375
2564
  * LittleJS Engine Settings
@@ -2587,6 +2776,14 @@ let touchInputEnable = true;
2587
2776
  * @memberof Settings */
2588
2777
  let touchGamepadEnable = false;
2589
2778
 
2779
+ /** True if touch gamepad should have start button in the center
2780
+ * - When the game is paused, any touch will press the button
2781
+ * - This can function as a way to pause/unpause the game
2782
+ * @type {boolean}
2783
+ * @default
2784
+ * @memberof Settings */
2785
+ let touchGamepadCenterButton = true;
2786
+
2590
2787
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
2591
2788
  * @type {boolean}
2592
2789
  * @default
@@ -2762,9 +2959,14 @@ function setHeadlessMode(headless) { headlessMode = headless; }
2762
2959
  * @memberof Settings */
2763
2960
  function setGLEnable(enable)
2764
2961
  {
2962
+ if (enable && !glCanBeEnabled)
2963
+ {
2964
+ console.warn('Can not enable WebGL if it was disabled on start.');
2965
+ return;
2966
+ }
2765
2967
  glEnable = enable;
2766
2968
  if (glCanvas) // hide glCanvas if WebGL is disabled
2767
- glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2969
+ glCanvas.style.display = enable ? '' : 'none';
2768
2970
  }
2769
2971
 
2770
2972
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
@@ -2852,6 +3054,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
2852
3054
  * @memberof Settings */
2853
3055
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
2854
3056
 
3057
+ /** True if touch gamepad should have start button in the center
3058
+ * - This can function as a way to pause/unpause the game
3059
+ * @param {boolean} enable
3060
+ * @memberof Settings */
3061
+ function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
3062
+
2855
3063
  /** Set if touch gamepad should be analog stick or 8 way dpad
2856
3064
  * @param {boolean} analog
2857
3065
  * @memberof Settings */
@@ -3213,6 +3421,27 @@ class EngineObject
3213
3421
  // test which side we bounced off (or both if a corner)
3214
3422
  const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
3215
3423
  const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
3424
+
3425
+ if (blockedLayerX)
3426
+ {
3427
+ // try to move up a tiny bit
3428
+ const epsilon = 1e-3;
3429
+ const maxMoveUp = .1;
3430
+ const y = floor(oldPos.y-this.size.y/2+1) +
3431
+ this.size.y/2 + epsilon;
3432
+ const delta = y - this.pos.y;
3433
+ if (delta < maxMoveUp)
3434
+ if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
3435
+ {
3436
+ this.pos.y = y;
3437
+ debugPhysics && debugRect(this.pos, this.size, '#ff0');
3438
+ return;
3439
+ }
3440
+
3441
+ // move to previous position and bounce
3442
+ this.pos.x = oldPos.x;
3443
+ this.velocity.x *= -this.restitution;
3444
+ }
3216
3445
  if (blockedLayerY || !blockedLayerX)
3217
3446
  {
3218
3447
  // bounce velocity
@@ -3236,12 +3465,6 @@ class EngineObject
3236
3465
  this.groundObject = undefined;
3237
3466
  }
3238
3467
  }
3239
- if (blockedLayerX)
3240
- {
3241
- // move to previous position and bounce
3242
- this.pos.x = oldPos.x;
3243
- this.velocity.x *= -this.restitution;
3244
- }
3245
3468
  debugPhysics && debugRect(this.pos, this.size, '#f00');
3246
3469
  }
3247
3470
  }
@@ -3299,6 +3522,16 @@ class EngineObject
3299
3522
  */
3300
3523
  collideWithObject(object) { return true; }
3301
3524
 
3525
+ /** Get this object's up vector
3526
+ * @param {number} [scale] - length of the vector
3527
+ * @return {Vector2} */
3528
+ getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
3529
+
3530
+ /** Get this object's right vector
3531
+ * @param {number} [scale] - length of the vector
3532
+ * @return {Vector2} */
3533
+ getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
3534
+
3302
3535
  /** How long since the object was created
3303
3536
  * @return {number} */
3304
3537
  getAliveTime() { return time - this.spawnTime; }
@@ -3571,16 +3804,15 @@ class TileInfo
3571
3804
  }
3572
3805
 
3573
3806
  /**
3574
- * Set this tile to use a full image
3575
- * @param {HTMLImageElement|OffscreenCanvas} image
3576
- * @param {WebGLTexture} [glTexture] - WebGL texture
3807
+ * Set this tile to use a full image in a texture info
3808
+ * @param {TextureInfo} textureInfo
3577
3809
  * @return {TileInfo}
3578
3810
  */
3579
- setFullImage(image, glTexture)
3811
+ setFullImage(textureInfo)
3580
3812
  {
3581
3813
  this.pos = new Vector2;
3582
- this.size = new Vector2(image.width, image.height);
3583
- this.textureInfo = new TextureInfo(image, glTexture);
3814
+ this.size = textureInfo.size.copy();
3815
+ this.textureInfo = textureInfo;
3584
3816
  // do not use padding or bleed
3585
3817
  this.bleedScale = this.padding = 0;
3586
3818
  return this;
@@ -3596,26 +3828,30 @@ class TextureInfo
3596
3828
  /**
3597
3829
  * Create a TextureInfo, called automatically by the engine
3598
3830
  * @param {HTMLImageElement|OffscreenCanvas} image
3599
- * @param {WebGLTexture} [glTexture] - WebGL texture
3831
+ * @param {boolean} [useWebGL] - Should use WebGL if available?
3600
3832
  */
3601
- constructor(image, glTexture)
3833
+ constructor(image, useWebGL=true)
3602
3834
  {
3603
- /** @property {HTMLImageElement} - image source */
3835
+ /** @property {HTMLImageElement|OffscreenCanvas} - image source */
3604
3836
  this.image = image;
3605
3837
  /** @property {Vector2} - size of the image */
3606
- this.size = vec2(image.width, image.height);
3838
+ this.size = image ? vec2(image.width, image.height) : vec2();
3607
3839
  /** @property {Vector2} - inverse of the size, cached for rendering */
3608
- this.sizeInverse = vec2(1/image.width, 1/image.height);
3840
+ this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
3609
3841
  /** @property {WebGLTexture} - WebGL texture */
3610
- this.glTexture = glTexture;
3842
+ this.glTexture = undefined;
3843
+ useWebGL && this.createWebGLTexture();
3611
3844
  }
3612
3845
 
3613
- createWebGLTexture()
3614
- {
3615
- ASSERT(!this.glTexture);
3616
- if (glEnable)
3617
- this.glTexture = glCreateTexture(this.image);
3618
- }
3846
+ /** Creates the WebGL texture, updates if already created */
3847
+ createWebGLTexture() { glRegisterTextureInfo(this); }
3848
+
3849
+ /** Destroys the WebGL texture */
3850
+ destroyWebGLTexture() { glUnregisterTextureInfo(this); }
3851
+
3852
+ /** Check if the texture is webgl enabled
3853
+ * @return {boolean} */
3854
+ hasWebGL() { return !!this.glTexture; }
3619
3855
  }
3620
3856
 
3621
3857
  ///////////////////////////////////////////////////////////////////////////////
@@ -3647,6 +3883,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3647
3883
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3648
3884
  if (useWebGL)
3649
3885
  {
3886
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3650
3887
  if (screenSpace)
3651
3888
  {
3652
3889
  // convert to world space
@@ -3742,6 +3979,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3742
3979
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3743
3980
  if (useWebGL)
3744
3981
  {
3982
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3745
3983
  if (screenSpace)
3746
3984
  {
3747
3985
  // convert to world space
@@ -3753,7 +3991,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3753
3991
  const halfSizeX = size.x/2, halfSizeY = size.y/2;
3754
3992
  const colorTopInt = colorTop.rgbaInt();
3755
3993
  const colorBottomInt = colorBottom.rgbaInt();
3756
- const c = Math.cos(-angle), s = Math.sin(-angle);
3994
+ const c = cos(-angle), s = sin(-angle);
3757
3995
  for (let i=4; i--;)
3758
3996
  {
3759
3997
  const x = i & 1 ? halfSizeX : -halfSizeX;
@@ -3804,6 +4042,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3804
4042
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3805
4043
  if (useWebGL)
3806
4044
  {
4045
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3807
4046
  let scale = 1;
3808
4047
  if (screenSpace)
3809
4048
  {
@@ -3853,6 +4092,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3853
4092
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
3854
4093
  const size = vec2(width, halfDelta.length()*2);
3855
4094
  pos = pos.add(posA.add(halfDelta));
4095
+ if (screenSpace)
4096
+ halfDelta.y *= -1; // flip angle Y if screen space
3856
4097
  angle += halfDelta.angle();
3857
4098
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3858
4099
  }
@@ -3880,7 +4121,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3880
4121
  for (let i=sides; i--;)
3881
4122
  {
3882
4123
  const a = (i/sides)*PI*2;
3883
- points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
4124
+ points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
3884
4125
  }
3885
4126
  drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3886
4127
  }
@@ -3907,6 +4148,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3907
4148
 
3908
4149
  if (useWebGL)
3909
4150
  {
4151
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3910
4152
  let scale = 1;
3911
4153
  if (screenSpace)
3912
4154
  {
@@ -4048,12 +4290,13 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4048
4290
  * @param {Color} [lineColor=(0,0,0,1)]
4049
4291
  * @param {CanvasTextAlign} [textAlign='center']
4050
4292
  * @param {string} [font=fontDefault]
4293
+ * @param {string} [fontStyle]
4051
4294
  * @param {number} [maxWidth]
4052
4295
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4053
4296
  * @memberof Draw */
4054
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth, context=drawContext)
4297
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
4055
4298
  {
4056
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, maxWidth, context);
4299
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
4057
4300
  }
4058
4301
 
4059
4302
  /** Draw text on overlay canvas in world space
@@ -4066,11 +4309,12 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4066
4309
  * @param {Color} [lineColor=(0,0,0,1)]
4067
4310
  * @param {CanvasTextAlign} [textAlign='center']
4068
4311
  * @param {string} [font=fontDefault]
4312
+ * @param {string} [fontStyle]
4069
4313
  * @param {number} [maxWidth]
4070
4314
  * @memberof Draw */
4071
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth)
4315
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
4072
4316
  {
4073
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, maxWidth, overlayContext);
4317
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
4074
4318
  }
4075
4319
 
4076
4320
  /** Draw text on overlay canvas in screen space
@@ -4083,10 +4327,11 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
4083
4327
  * @param {Color} [lineColor=(0,0,0,1)]
4084
4328
  * @param {CanvasTextAlign} [textAlign]
4085
4329
  * @param {string} [font=fontDefault]
4330
+ * @param {string} [fontStyle]
4086
4331
  * @param {number} [maxWidth]
4087
4332
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
4088
4333
  * @memberof Draw */
4089
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
4334
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
4090
4335
  {
4091
4336
  ASSERT(isString(text), 'text must be a string');
4092
4337
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4094,15 +4339,15 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4094
4339
  ASSERT(isColor(color), 'color must be a color');
4095
4340
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4096
4341
  ASSERT(isColor(lineColor), 'lineColor must be a color');
4097
- ASSERT(isColor(lineColor), 'lineColor must be a color');
4098
4342
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
4099
4343
  ASSERT(isString(font), 'font must be a string');
4344
+ ASSERT(isString(fontStyle), 'fontStyle must be a string');
4100
4345
 
4101
4346
  context.fillStyle = color.toString();
4102
4347
  context.strokeStyle = lineColor.toString();
4103
4348
  context.lineWidth = lineWidth;
4104
4349
  context.textAlign = textAlign;
4105
- context.font = size + 'px '+ font;
4350
+ context.font = fontStyle + ' ' + size + 'px '+ font;
4106
4351
  context.textBaseline = 'middle';
4107
4352
 
4108
4353
  const lines = (text+'').split('\n');
@@ -4125,18 +4370,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4125
4370
  * @memberof Draw */
4126
4371
  function screenToWorld(screenPos)
4127
4372
  {
4128
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
4129
- let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
4373
+ let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
4374
+ let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
4130
4375
  if (cameraAngle)
4131
4376
  {
4132
4377
  // apply camera rotation
4133
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
4134
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
4135
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
4136
- cameraPosRelativeX = rotatedX;
4137
- cameraPosRelativeY = rotatedY;
4378
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
4379
+ const rotatedX = x * c - y * s;
4380
+ const rotatedY = x * s + y * c;
4381
+ x = rotatedX;
4382
+ y = rotatedY;
4138
4383
  }
4139
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
4384
+ return new Vector2(x + cameraPos.x, y + cameraPos.y);
4140
4385
  }
4141
4386
 
4142
4387
  /** Convert from world to screen space coordinates
@@ -4145,24 +4390,64 @@ function screenToWorld(screenPos)
4145
4390
  * @memberof Draw */
4146
4391
  function worldToScreen(worldPos)
4147
4392
  {
4148
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
4149
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
4393
+ let x = worldPos.x - cameraPos.x;
4394
+ let y = worldPos.y - cameraPos.y;
4150
4395
  if (cameraAngle)
4151
4396
  {
4152
4397
  // apply inverse camera rotation
4153
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
4154
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
4155
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
4156
- cameraPosRelativeX = rotatedX;
4157
- cameraPosRelativeY = rotatedY;
4398
+ const c = cos(cameraAngle), s = sin(cameraAngle);
4399
+ const rotatedX = x * c - y * s;
4400
+ const rotatedY = x * s + y * c;
4401
+ x = rotatedX;
4402
+ y = rotatedY;
4158
4403
  }
4159
4404
  return new Vector2
4160
4405
  (
4161
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
4162
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
4406
+ x * cameraScale + mainCanvasSize.x/2 - .5,
4407
+ y * -cameraScale + mainCanvasSize.y/2 - .5
4163
4408
  );
4164
4409
  }
4165
4410
 
4411
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
4412
+ * @param {Vector2} screenDelta
4413
+ * @return {Vector2}
4414
+ * @memberof Draw */
4415
+ function screenToWorldDelta(screenDelta)
4416
+ {
4417
+ let x = screenDelta.x / cameraScale;
4418
+ let y = screenDelta.y / -cameraScale;
4419
+ if (cameraAngle)
4420
+ {
4421
+ // apply camera rotation
4422
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
4423
+ const rotatedX = x * c - y * s;
4424
+ const rotatedY = x * s + y * c;
4425
+ x = rotatedX;
4426
+ y = rotatedY;
4427
+ }
4428
+ return new Vector2(x, y);
4429
+ }
4430
+
4431
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
4432
+ * @param {Vector2} worldDelta
4433
+ * @return {Vector2}
4434
+ * @memberof Draw */
4435
+ function worldToScreenDelta(worldDelta)
4436
+ {
4437
+ let x = worldDelta.x;
4438
+ let y = worldDelta.y;
4439
+ if (cameraAngle)
4440
+ {
4441
+ // apply inverse camera rotation
4442
+ const c = cos(cameraAngle), s = sin(cameraAngle);
4443
+ const rotatedX = x * c - y * s;
4444
+ const rotatedY = x * s + y * c;
4445
+ x = rotatedX;
4446
+ y = rotatedY;
4447
+ }
4448
+ return new Vector2(x * cameraScale, y * -cameraScale);
4449
+ }
4450
+
4166
4451
  /** Get the camera's visible area in world space
4167
4452
  * @return {Vector2}
4168
4453
  * @memberof Draw */
@@ -4215,6 +4500,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4215
4500
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4216
4501
  const sx2 = bleedScale;
4217
4502
  const sy2 = bleedScale;
4503
+ sWidth = max(1,sWidth|0);
4504
+ sHeight = max(1,sHeight|0);
4218
4505
  const sWidth2 = sWidth - 2*bleedScale;
4219
4506
  const sHeight2 = sHeight - 2*bleedScale;
4220
4507
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
@@ -4229,7 +4516,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4229
4516
  // copy to offscreen canvas
4230
4517
  workCanvas.width = sWidth;
4231
4518
  workCanvas.height = sHeight;
4232
- workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
4519
+ workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4233
4520
 
4234
4521
  // tint image using offscreen work context
4235
4522
  const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
@@ -4310,11 +4597,11 @@ let engineFontImage;
4310
4597
  class FontImage
4311
4598
  {
4312
4599
  /** Create an image font
4313
- * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
4314
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
4315
- * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
4600
+ * @param {HTMLImageElement} [image] - Image for the font, default if undefined
4601
+ * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
4602
+ * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
4316
4603
  */
4317
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
4604
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4318
4605
  {
4319
4606
  // load default font image
4320
4607
  if (!engineFontImage)
@@ -4566,9 +4853,9 @@ function inputUpdate()
4566
4853
  if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
4567
4854
  inputClear();
4568
4855
 
4569
- // update mouse world space position
4856
+ // update mouse world space position and delta
4570
4857
  mousePos = screenToWorld(mousePosScreen);
4571
- mouseDelta = mouseDeltaScreen.multiply(vec2(1,-1)).rotate(-cameraAngle);
4858
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
4572
4859
 
4573
4860
  // update gamepads if enabled
4574
4861
  gamepadsUpdate();
@@ -4600,7 +4887,6 @@ function inputInit()
4600
4887
  document.addEventListener('wheel', onMouseWheel);
4601
4888
  document.addEventListener('contextmenu', onContextMenu);
4602
4889
  document.addEventListener('blur', onBlur);
4603
- document.addEventListener('mouseleave', onMouseLeave);
4604
4890
 
4605
4891
  // init touch input
4606
4892
  if (isTouchDevice && touchInputEnable)
@@ -4642,7 +4928,10 @@ function inputInit()
4642
4928
 
4643
4929
  isUsingGamepad = false;
4644
4930
  inputData[0][e.button] = 3;
4931
+
4932
+ let mousePosScreenLast = mousePosScreen;
4645
4933
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4934
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4646
4935
  inputPreventDefault && e.button && e.preventDefault();
4647
4936
  }
4648
4937
  function onMouseUp(e)
@@ -4653,18 +4942,13 @@ function inputInit()
4653
4942
  }
4654
4943
  function onMouseMove(e)
4655
4944
  {
4945
+ let mousePosScreenLast = mousePosScreen;
4656
4946
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4657
- mouseDeltaScreen = mouseDeltaScreen.add(vec2(e.movementX, e.movementY));
4947
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4658
4948
  }
4659
4949
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4660
4950
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4661
4951
  function onBlur() { inputClear(); } // reset input when focus is lost
4662
- function onMouseLeave()
4663
- {
4664
- // set mouse position and delta when leaving canvas
4665
- mousePosScreen = vec2(-1);
4666
- mouseDeltaScreen = vec2(0);
4667
- }
4668
4952
  }
4669
4953
 
4670
4954
  // convert a mouse or touch event position to screen space
@@ -4708,8 +4992,8 @@ function gamepadsUpdate()
4708
4992
  else if (touchGamepadStick.lengthSquared() > .3)
4709
4993
  {
4710
4994
  // convert to 8 way dpad
4711
- sticks[0].x = Math.round(touchGamepadStick.x);
4712
- sticks[0].y = -Math.round(touchGamepadStick.y);
4995
+ sticks[0].x = round(touchGamepadStick.x);
4996
+ sticks[0].y = -round(touchGamepadStick.y);
4713
4997
  sticks[0] = sticks[0].clampLength();
4714
4998
  }
4715
4999
 
@@ -4837,11 +5121,11 @@ function touchInputInit()
4837
5121
  {
4838
5122
  // set event pos and pass it along
4839
5123
  const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
4840
- const lastMousePosScreen = mousePosScreen;
5124
+ const mousePosScreenLast = mousePosScreen;
4841
5125
  mousePosScreen = mouseEventToScreen(pos);
4842
5126
  if (wasTouching)
4843
5127
  {
4844
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(lastMousePosScreen));
5128
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4845
5129
  isUsingGamepad = touchGamepadEnable;
4846
5130
  }
4847
5131
  else
@@ -4873,7 +5157,7 @@ function touchInputInit()
4873
5157
  if (touching)
4874
5158
  {
4875
5159
  touchGamepadTimer.set();
4876
- if (paused && !wasTouching)
5160
+ if (touchGamepadCenterButton && !wasTouching && paused)
4877
5161
  {
4878
5162
  // touch anywhere to press start when paused
4879
5163
  touchGamepadButtons[9] = 1;
@@ -4912,7 +5196,8 @@ function touchInputInit()
4912
5196
  if (button < touchGamepadButtonCount)
4913
5197
  touchGamepadButtons[button] = 1;
4914
5198
  }
4915
- else if (startCenter.distance(touchPos) < touchGamepadSize && !wasTouching)
5199
+ else if (touchGamepadCenterButton && !wasTouching &&
5200
+ startCenter.distance(touchPos) < touchGamepadSize)
4916
5201
  {
4917
5202
  // virtual start button in center
4918
5203
  touchGamepadButtons[9] = 1;
@@ -5327,8 +5612,16 @@ class SoundInstance
5327
5612
  this.stop();
5328
5613
  this.gainNode = audioContext.createGain();
5329
5614
  this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
5330
- this.startTime = audioContext.currentTime - offset;
5331
- this.pausedTime = undefined;
5615
+ if (this.source)
5616
+ {
5617
+ this.startTime = audioContext.currentTime - offset;
5618
+ this.pausedTime = undefined;
5619
+ }
5620
+ else
5621
+ {
5622
+ this.startTime = undefined;
5623
+ this.pausedTime = 0;
5624
+ }
5332
5625
  }
5333
5626
 
5334
5627
  /** Set the volume of this sound instance
@@ -5477,12 +5770,19 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
5477
5770
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
5478
5771
  * @param {number} [offset] - Offset in seconds to start playback from
5479
5772
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
5480
- * @return {AudioBufferSourceNode} - The audio node of the sound played
5773
+ * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
5481
5774
  * @memberof Audio */
5482
5775
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
5483
5776
  {
5484
5777
  if (!soundEnable || headlessMode) return;
5485
5778
 
5779
+ if (!audioIsRunning())
5780
+ {
5781
+ // fix stalled audio, this sound won't be able to play
5782
+ audioContext.resume();
5783
+ return;
5784
+ }
5785
+
5486
5786
  // create buffer and source
5487
5787
  const channelCount = sampleChannels.length;
5488
5788
  const sampleLength = sampleChannels[0].length;
@@ -5508,14 +5808,9 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
5508
5808
  if (onended)
5509
5809
  source.addEventListener('ended', ()=> onended(source));
5510
5810
 
5811
+ // play and return sound
5511
5812
  const startOffset = offset * rate;
5512
- if (!audioIsRunning())
5513
- {
5514
- // fix stalled audio and start
5515
- audioContext.resume().then(()=>source.start(0, startOffset));
5516
- }
5517
- else
5518
- source.start(0, startOffset);
5813
+ source.start(0, startOffset);
5519
5814
  return source;
5520
5815
  }
5521
5816
 
@@ -5598,10 +5893,10 @@ function zzfxG
5598
5893
 
5599
5894
  // biquad LP/HP filter
5600
5895
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
5601
- cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
5602
- a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
5603
- b0 = (1 + sign(filter) * cos) / 2 / a0,
5604
- b1 = -(sign(filter) + cos) / a0, b2 = b0,
5896
+ cosw = cos(w), alpha = sin(w) / 2 / quality,
5897
+ a0 = 1 + alpha, a1 = -2*cosw / a0, a2 = (1 - alpha) / a0,
5898
+ b0 = (1 + sign(filter) * cosw) / 2 / a0,
5899
+ b1 = -(sign(filter) + cosw) / a0, b2 = b0,
5605
5900
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
5606
5901
 
5607
5902
  // scale by sample rate
@@ -5624,15 +5919,15 @@ function zzfxG
5624
5919
  if (!(++crush%(bitCrush*100|0))) // bit crush
5625
5920
  {
5626
5921
  s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
5627
- (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
5628
- Math.sin(t**3) : // 4 noise
5629
- Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
5630
- 1-(2*t/PI2%2+2)%2: // 2 saw
5631
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
5632
- Math.sin(t); // 0 sin
5922
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
5923
+ sin(t**3) : // 4 noise
5924
+ max(min(tan(t),1),-1): // 3 tan
5925
+ 1-(2*t/PI2%2+2)%2: // 2 saw
5926
+ 1-4*abs(round(t/PI2)-t/PI2): // 1 triangle
5927
+ sin(t); // 0 sin
5633
5928
 
5634
5929
  s = (repeatTime ?
5635
- 1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
5930
+ 1 - tremolo + tremolo*sin(PI2*i/repeatTime) // tremolo
5636
5931
  : 1) *
5637
5932
  (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
5638
5933
  (i < attack ? i/attack : // attack
@@ -5654,8 +5949,8 @@ function zzfxG
5654
5949
  }
5655
5950
 
5656
5951
  f = (frequency += slide += deltaSlide) *// frequency
5657
- Math.cos(modulation*modOffset++); // modulation
5658
- t += f + f*noise*Math.sin(i**5); // noise
5952
+ cos(modulation*modOffset++); // modulation
5953
+ t += f + f*noise*sin(i**5); // noise
5659
5954
 
5660
5955
  if (jump && ++jump > pitchJumpTime) // pitch jump
5661
5956
  {
@@ -5721,23 +6016,22 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5721
6016
  }
5722
6017
  }
5723
6018
 
5724
- /** Return the center of first tile hit, undefined if nothing was hit.
5725
- * This does not return the exact intersection, but the center of the tile hit.
6019
+ /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
5726
6020
  * @param {Vector2} posStart
5727
6021
  * @param {Vector2} posEnd
5728
6022
  * @param {EngineObject} [object] - An object or undefined for generic test
6023
+ * @param {Vector2} [normal] - Optional normal of the surface hit
5729
6024
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5730
- * @return {Vector2}
6025
+ * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
5731
6026
  * @memberof TileLayers */
5732
- function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
6027
+ function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
5733
6028
  {
5734
6029
  for (const layer of tileCollisionLayers)
5735
6030
  {
5736
6031
  if (!solidOnly || layer.isSolid)
5737
6032
  {
5738
- const hitPos = layer.collisionRaycast(posStart, posEnd, object)
5739
- if (hitPos)
5740
- return hitPos;
6033
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
6034
+ if (hitPos) return hitPos;
5741
6035
  }
5742
6036
  }
5743
6037
  }
@@ -5871,10 +6165,15 @@ class CanvasLayer extends EngineObject
5871
6165
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
5872
6166
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
5873
6167
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5874
- this.context = headlessMode ? undefined : this.canvas.getContext('2d');
5875
- /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
5876
- this.glTexture = undefined;
5877
- this.gravityScale = 0; // disable gravity by default for canvas layers
6168
+ this.context = this.canvas?.getContext('2d');
6169
+ /** @property {TextureInfo} - Texture info to use for this object rendering */
6170
+ const useWebGL = false; // do not use webgl by default
6171
+ this.textureInfo = new TextureInfo(this.canvas, useWebGL);
6172
+ /** @property {boolean} - True if WebGL texture needs to be refreshed */
6173
+ this.refreshWebGL = false;
6174
+
6175
+ // disable physics by default
6176
+ this.mass = this.gravityScale = this.friction = this.restitution = 0;
5878
6177
  }
5879
6178
 
5880
6179
  /** Destroy this canvas layer */
@@ -5883,9 +6182,7 @@ class CanvasLayer extends EngineObject
5883
6182
  if (this.destroyed)
5884
6183
  return;
5885
6184
 
5886
- // free up the WebGL texture
5887
- if (this.glTexture)
5888
- glDeleteTexture(this.glTexture);
6185
+ this.textureInfo.destroyWebGLTexture();
5889
6186
  super.destroy();
5890
6187
  }
5891
6188
 
@@ -5907,9 +6204,16 @@ class CanvasLayer extends EngineObject
5907
6204
  * @memberof Draw */
5908
6205
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5909
6206
  {
6207
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
6208
+ if (useWebGL && this.refreshWebGL)
6209
+ {
6210
+ // update the WebGL texture
6211
+ this.textureInfo.createWebGLTexture();
6212
+ this.refreshWebGL = false;
6213
+ }
6214
+
5910
6215
  // draw the canvas layer as a single tile that uses the whole texture
5911
- const useWebGL = glEnable && this.glTexture !== undefined;
5912
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
6216
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5913
6217
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5914
6218
  }
5915
6219
 
@@ -5927,6 +6231,8 @@ class CanvasLayer extends EngineObject
5927
6231
  * @param {Canvas2DDrawCallback} drawFunction */
5928
6232
  drawCanvas2D(pos, size, angle, mirror, drawFunction)
5929
6233
  {
6234
+ if (!this.context) return;
6235
+
5930
6236
  const context = this.context;
5931
6237
  context.save();
5932
6238
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
@@ -5976,18 +6282,22 @@ class CanvasLayer extends EngineObject
5976
6282
  { this.drawTile(pos, size, undefined, color, angle); }
5977
6283
 
5978
6284
  /** Create or update the WebGL texture for this layer
5979
- * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5980
- useWebGL(enable=true)
6285
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture
6286
+ * @param {boolean} [immediate] - shoulkd the texture be updated immediately
6287
+ */
6288
+ useWebGL(enable=true, immediate=false)
5981
6289
  {
5982
- if (glEnable && enable)
6290
+ if (!immediate && enable && this.textureInfo.hasWebGL())
5983
6291
  {
5984
- if (this.glTexture)
5985
- glSetTextureData(this.glTexture, this.canvas);
5986
- else
5987
- this.glTexture = glCreateTexture(this.canvas);
6292
+ // refresh the texture when needed
6293
+ this.refreshWebGL = true;
6294
+ return;
5988
6295
  }
6296
+
6297
+ if (enable)
6298
+ this.textureInfo.createWebGLTexture();
5989
6299
  else
5990
- this.glTexture = undefined;
6300
+ this.textureInfo.destroyWebGLTexture();
5991
6301
  }
5992
6302
  }
5993
6303
 
@@ -6011,24 +6321,14 @@ class TileLayer extends CanvasLayer
6011
6321
  * @param {Vector2} size - World space size
6012
6322
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6013
6323
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6014
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
6015
6324
  */
6016
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
6325
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
6017
6326
  {
6018
- super(position, size, 0, renderOrder, size);
6019
-
6327
+ const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6328
+ super(position, size, 0, renderOrder, canvasSize);
6329
+
6330
+ // set tile info
6020
6331
  this.tileInfo = tileInfo;
6021
- const canvasSize = size.multiply(tileInfo.size);
6022
- /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
6023
- this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
6024
- /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
6025
- this.context = this.canvas.getContext('2d');
6026
- /** @property {WebGLTexture} - Texture if using WebGL for this layer */
6027
- this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
6028
- // set no friction by default, applied friction is max of both objects
6029
- this.friction = 0;
6030
- // set no restitution by default, applied restitution is max of both objects
6031
- this.restitution = 0;
6032
6332
 
6033
6333
  // init tile data
6034
6334
  this.data = [];
@@ -6077,11 +6377,18 @@ class TileLayer extends CanvasLayer
6077
6377
  {
6078
6378
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
6079
6379
 
6380
+ if (this.refreshWebGL)
6381
+ {
6382
+ // update the WebGL texture
6383
+ this.textureInfo.createWebGLTexture();
6384
+ this.refreshWebGL = false;
6385
+ }
6386
+
6080
6387
  // draw the tile layer as a single tile
6081
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
6388
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6082
6389
  const size = this.drawSize || this.size;
6083
6390
  const pos = this.pos.add(size.scale(.5));
6084
- const useWebGL = glEnable && this.glTexture !== undefined;
6391
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
6085
6392
  drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
6086
6393
  }
6087
6394
 
@@ -6094,8 +6401,7 @@ class TileLayer extends CanvasLayer
6094
6401
  for (let y = this.size.y; y--;)
6095
6402
  this.drawTileData(vec2(x,y), false);
6096
6403
  this.redrawEnd();
6097
- if (this.glTexture)
6098
- this.useWebGL(); // update WebGL texture
6404
+ this.useWebGL();
6099
6405
  }
6100
6406
 
6101
6407
  /** Call to start the redraw process
@@ -6103,6 +6409,8 @@ class TileLayer extends CanvasLayer
6103
6409
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
6104
6410
  redrawStart(clear=false)
6105
6411
  {
6412
+ if (!this.context) return;
6413
+
6106
6414
  // save current render settings
6107
6415
  /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
6108
6416
  this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
@@ -6112,8 +6420,9 @@ class TileLayer extends CanvasLayer
6112
6420
  drawCanvas = this.canvas;
6113
6421
  drawContext = this.context;
6114
6422
  cameraPos = this.size.scale(.5);
6115
- cameraScale = this.tileInfo.size.x;
6116
- mainCanvasSize = this.size.multiply(this.tileInfo.size);
6423
+ const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6424
+ cameraScale = tileSize.x;
6425
+ mainCanvasSize = this.size.multiply(tileSize);
6117
6426
  if (clear)
6118
6427
  {
6119
6428
  // clear and set size
@@ -6131,6 +6440,8 @@ class TileLayer extends CanvasLayer
6131
6440
  /** Call to end the redraw process */
6132
6441
  redrawEnd()
6133
6442
  {
6443
+ if (!this.context) return;
6444
+
6134
6445
  ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6135
6446
  glCopyToContext(drawContext);
6136
6447
  //debugSaveCanvas(this.canvas);
@@ -6147,6 +6458,8 @@ class TileLayer extends CanvasLayer
6147
6458
  */
6148
6459
  drawTileData(layerPos, clear=true)
6149
6460
  {
6461
+ if (!this.context) return;
6462
+
6150
6463
  // clear out where the tile was, for full opaque tiles this can be skipped
6151
6464
  const s = this.tileInfo.size;
6152
6465
  if (clear)
@@ -6183,11 +6496,10 @@ class TileCollisionLayer extends TileLayer
6183
6496
  * @param {Vector2} size - World space size
6184
6497
  * @param {TileInfo} [tileInfo] - Tile info for layer
6185
6498
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6186
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
6187
6499
  */
6188
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
6500
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
6189
6501
  {
6190
- super(position, size.floor(), tileInfo, renderOrder, useWebGL);
6502
+ super(position, size.floor(), tileInfo, renderOrder);
6191
6503
 
6192
6504
  /** @property {Array<number>} - The tile collision grid */
6193
6505
  this.collisionData = [];
@@ -6270,66 +6582,44 @@ class TileCollisionLayer extends TileLayer
6270
6582
  // check if the object should collide with this tile
6271
6583
  const tileData = this.collisionData[y*this.size.x+x];
6272
6584
  if (tileData)
6273
- if (!object || object.collideWithTile(tileData, hitPos.set(x, y)))
6585
+ if (!object || object.collideWithTile(tileData,
6586
+ hitPos.set(x + this.pos.x, y + this.pos.y)))
6274
6587
  return true;
6275
6588
  }
6276
6589
  return false;
6277
6590
  }
6278
6591
 
6279
- /** Return the center of first tile hit, undefined if nothing was hit.
6280
- * This does not return the exact intersection, but the center of the tile hit.
6592
+ /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
6281
6593
  * @param {Vector2} posStart
6282
6594
  * @param {Vector2} posEnd
6283
- * @param {EngineObject} [object]
6284
- * @return {Vector2} */
6285
- collisionRaycast(posStart, posEnd, object)
6595
+ * @param {EngineObject} [object] - An object or undefined for generic test
6596
+ * @param {Vector2} [normal] - Optional normal of the surface hit
6597
+ * @return {Vector2|undefined} */
6598
+ collisionRaycast(posStart, posEnd, object, normal)
6286
6599
  {
6287
6600
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
6288
6601
  ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6289
-
6290
- // transform to local layer space
6291
- const posStartX = posStart.x - this.pos.x;
6292
- const posStartY = posStart.y - this.pos.y;
6293
- const posEndX = posEnd.x - this.pos.x;
6294
- const posEndY = posEnd.y - this.pos.y;
6295
-
6296
- // test if a ray collides with tiles from start to end
6297
- const deltaX = posEndX - posStartX;
6298
- const deltaY = posEndY - posStartY;
6299
- const totalLength = (deltaX**2 + deltaY**2)**.5;
6300
- const unitX = abs(totalLength/deltaX);
6301
- const unitY = abs(totalLength/deltaY);
6302
-
6303
- // setup iteration variables
6304
- const pos = posStart.floor(), signDeltaX = sign(deltaX), signDeltaY = sign(deltaY);
6305
- let xi = unitX * (deltaX < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1) || 0;
6306
- let yi = unitY * (deltaY < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1) || 0;
6307
-
6308
- // use line drawing algorithm to test for collisions
6309
- while (true)
6602
+
6603
+ const localPos = new Vector2;
6604
+ const collisionTest = (pos)=>
6310
6605
  {
6311
6606
  // check for tile collision
6312
- const tileData = this.getCollisionData(pos);
6313
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
6314
- {
6315
- pos.x += .5; pos.y += .5;
6316
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
6317
- debugRaycast && debugPoint(pos, '#ff0');
6318
- return pos;
6319
- }
6320
-
6321
- // check if past the end
6322
- if (xi >= totalLength && yi >= totalLength)
6323
- break;
6324
-
6325
- // get coordinates of next tile to check
6326
- if (xi > yi)
6327
- pos.y += signDeltaY, yi += unitY;
6328
- else
6329
- pos.x += signDeltaX, xi += unitX;
6607
+ localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
6608
+ const tileData = this.getCollisionData(localPos);
6609
+ return tileData && (!object || object.collideWithTile(tileData, pos));
6330
6610
  }
6331
-
6332
6611
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
6612
+ const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
6613
+ if (hitPos)
6614
+ {
6615
+ const tilePos = hitPos.floor().add(vec2(.5));
6616
+ debugRaycast && debugRect(tilePos, vec2(1), '#f008');
6617
+ debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
6618
+ debugRaycast && debugPoint(hitPos, '#0f0');
6619
+ debugRaycast && normal &&
6620
+ debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
6621
+ return hitPos;
6622
+ }
6333
6623
  }
6334
6624
  }
6335
6625
  /**
@@ -6355,7 +6645,7 @@ class TileCollisionLayer extends TileLayer
6355
6645
  * tile(0, 16), // tileInfo
6356
6646
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
6357
6647
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
6358
- * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
6648
+ * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
6359
6649
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
6360
6650
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
6361
6651
  * );
@@ -6484,6 +6774,12 @@ class ParticleEmitter extends EngineObject
6484
6774
  this.particleCreateCallback = undefined;
6485
6775
  /** @property {number} - Track particle emit time */
6486
6776
  this.emitTimeBuffer = 0;
6777
+ /** @property {number} - Percentage of velocity to pass to particles (0-1) */
6778
+ this.velocityInheritance = 0;
6779
+
6780
+ // track previous position and angle
6781
+ this.previousAngle = this.angle;
6782
+ this.previousPos = this.pos.copy();
6487
6783
  }
6488
6784
 
6489
6785
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
@@ -6492,6 +6788,18 @@ class ParticleEmitter extends EngineObject
6492
6788
  // only do default update to apply parent transforms
6493
6789
  this.parent && super.update();
6494
6790
 
6791
+ if (this.velocityInheritance)
6792
+ {
6793
+ // pass emitter velocity to particles
6794
+ const p = this.velocityInheritance;
6795
+ this.velocity.x = p * (this.pos.x - this.previousPos.x);
6796
+ this.velocity.y = p * (this.pos.y - this.previousPos.y);
6797
+ this.angleVelocity = p * (this.angle - this.previousAngle);
6798
+ this.previousAngle = this.angle;
6799
+ this.previousPos.x = this.pos.x;
6800
+ this.previousPos.y = this.pos.y;
6801
+ }
6802
+
6495
6803
  // update emitter
6496
6804
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
6497
6805
  {
@@ -6549,7 +6857,7 @@ class ParticleEmitter extends EngineObject
6549
6857
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
6550
6858
  particle.velocity = vec2().setAngle(velocityAngle, speed);
6551
6859
  particle.angleVelocity = angleSpeed;
6552
- if (!this.localSpace)
6860
+ if (!this.localSpace && this.velocityInheritance > 0)
6553
6861
  {
6554
6862
  // apply emitter velocity to particle
6555
6863
  particle.velocity.x += this.velocity.x;
@@ -6647,6 +6955,16 @@ class Particle extends EngineObject
6647
6955
  this.velocity.y *= s;
6648
6956
  }
6649
6957
  }
6958
+
6959
+ if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
6960
+ {
6961
+ // destroy particle when its time runs out
6962
+ const c = this.colorEnd;
6963
+ this.color.set(c.r, c.g, c.b, c.a);
6964
+ this.size.set(this.sizeEnd, this.sizeEnd);
6965
+ this.destroyCallback && this.destroyCallback(this);
6966
+ this.destroyed = 1;
6967
+ }
6650
6968
  }
6651
6969
 
6652
6970
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -6655,7 +6973,7 @@ class Particle extends EngineObject
6655
6973
  // lerp color and size
6656
6974
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6657
6975
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6658
- this.size.x = this.size.y = radius;
6976
+ const size = vec2(radius);
6659
6977
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6660
6978
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6661
6979
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
@@ -6675,7 +6993,7 @@ class Particle extends EngineObject
6675
6993
  {
6676
6994
  // in local space of emitter
6677
6995
  const a = this.localSpaceEmitter.angle;
6678
- const c = Math.cos(a), s = Math.sin(a);
6996
+ const c = cos(a), s = sin(a);
6679
6997
  pos = this.localSpaceEmitter.pos.add(
6680
6998
  new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6681
6999
  angle += this.localSpaceEmitter.angle;
@@ -6691,22 +7009,15 @@ class Particle extends EngineObject
6691
7009
  {
6692
7010
  // stretch in direction of motion
6693
7011
  const trailLength = speed * this.trailScale;
6694
- this.size.y = max(this.size.x, trailLength);
6695
- angle = Math.atan2(direction.x, direction.y);
6696
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
7012
+ size.y = max(size.x, trailLength);
7013
+ angle = atan2(direction.x, direction.y);
7014
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6697
7015
  }
6698
7016
  }
6699
7017
  else
6700
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
7018
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6701
7019
  this.additive && setBlendMode();
6702
- debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
6703
-
6704
- if (p1 === 1)
6705
- {
6706
- // destroy particle when its time runs out
6707
- this.destroyCallback && this.destroyCallback(this);
6708
- this.destroyed = 1;
6709
- }
7020
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
6710
7021
  }
6711
7022
  }
6712
7023
  /**
@@ -6929,7 +7240,7 @@ let glContext;
6929
7240
  let glAntialias = true;
6930
7241
 
6931
7242
  // WebGL internal variables not exposed to documentation
6932
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
7243
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
6933
7244
 
6934
7245
  // WebGL internal constants
6935
7246
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6945,7 +7256,14 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
6945
7256
  // Initialize WebGL, called automatically by the engine
6946
7257
  function glInit()
6947
7258
  {
6948
- if (!glEnable || headlessMode) return;
7259
+ // keep set of texture infos so they can be restored if context is lost
7260
+ glTextureInfos = new Set;
7261
+
7262
+ if (!glEnable || headlessMode)
7263
+ {
7264
+ glCanBeEnabled = false;
7265
+ return;
7266
+ }
6949
7267
 
6950
7268
  // create the canvas and textures
6951
7269
  glCanvas = document.createElement('canvas');
@@ -6956,74 +7274,108 @@ function glInit()
6956
7274
  console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
6957
7275
  glCanvas = glContext = undefined;
6958
7276
  glEnable = false;
7277
+ glCanBeEnabled = false;
6959
7278
  return;
6960
7279
  }
6961
7280
 
6962
- // create the WebGL canvas
7281
+ // attach the WebGL canvas
6963
7282
  const rootElement = mainCanvas.parentElement;
6964
7283
  rootElement.appendChild(glCanvas);
7284
+
7285
+ // startup webgl
7286
+ initWebGL();
7287
+
7288
+ // setup context lost and restore handlers
7289
+ glCanvas.addEventListener('webglcontextlost', (e)=>
7290
+ {
7291
+ glEnable = false; // disable WebGL rendering
7292
+ glCanvas.style.display = 'none'; // hide the gl canvas
7293
+ e.preventDefault(); // prevent default to allow restoration
7294
+ LOG('WebGL context lost! Switching to Canvas2d rendering.');
7295
+
7296
+ // remove WebGL textures
7297
+ for (const info of glTextureInfos)
7298
+ info.glTexture = undefined;
7299
+ glActiveTexture = undefined;
7300
+ pluginList.forEach(plugin=>plugin.glContextLost?.());
7301
+ });
7302
+ glCanvas.addEventListener('webglcontextrestored', ()=>
7303
+ {
7304
+ glEnable = true; // re-enable WebGL rendering
7305
+ glCanvas.style.display = ''; // show the gl canvas
7306
+ LOG('WebGL context restored, reinitializing...');
6965
7307
 
6966
- // setup instanced rendering shader program
6967
- glShader = glCreateProgram(
6968
- '#version 300 es\n' + // specify GLSL ES version
6969
- 'precision highp float;'+ // use highp for better accuracy
6970
- 'uniform mat4 m;'+ // transform matrix
6971
- 'in vec2 g;'+ // in: geometry
6972
- 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
6973
- 'in float r;'+ // in: rotation
6974
- 'out vec2 v;'+ // out: uv
6975
- 'out vec4 d,e;'+ // out: color, additiveColor
6976
- 'void main(){'+ // shader entry point
6977
- 'vec2 s=(g-.5)*p.zw;'+ // get size offset
6978
- 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
6979
- 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
6980
- 'd=c;e=a;'+ // pass colors to fragment shader
6981
- '}' // end of shader
6982
- ,
6983
- '#version 300 es\n' + // specify GLSL ES version
6984
- 'precision highp float;'+ // use highp for better accuracy
6985
- 'uniform sampler2D s;'+ // texture
6986
- 'in vec2 v;'+ // in: uv
6987
- 'in vec4 d,e;'+ // in: color, additiveColor
6988
- 'out vec4 c;'+ // out: color
6989
- 'void main(){'+ // shader entry point
6990
- 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
6991
- '}' // end of shader
6992
- );
7308
+ // reinit WebGL and restore textures
7309
+ initWebGL();
7310
+ for (const info of glTextureInfos)
7311
+ info.glTexture = glCreateTexture(info.image);
7312
+ pluginList.forEach(plugin=>plugin.glContextRestored?.());
7313
+ });
6993
7314
 
6994
- // setup poly rendering shaders
6995
- glPolyShader = glCreateProgram(
6996
- '#version 300 es\n' + // specify GLSL ES version
6997
- 'precision highp float;'+ // use highp for better accuracy
6998
- 'uniform mat4 m;'+ // transform matrix
6999
- 'in vec2 p;'+ // in: position
7000
- 'in vec4 c;'+ // in: color
7001
- 'out vec4 d;'+ // out: color
7002
- 'void main(){'+ // shader entry point
7003
- 'gl_Position=m*vec4(p,1,1);'+ // transform position
7004
- 'd=c;'+ // pass color to fragment shader
7005
- '}' // end of shader
7006
- ,
7007
- '#version 300 es\n' + // specify GLSL ES version
7008
- 'precision highp float;'+ // use highp for better accuracy
7009
- 'in vec4 d;'+ // in: color
7010
- 'out vec4 c;'+ // out: color
7011
- 'void main(){'+ // shader entry point
7012
- 'c=d;'+ // set color
7013
- '}' // end of shader
7014
- );
7315
+ function initWebGL()
7316
+ {
7317
+ // setup instanced rendering shader program
7318
+ glShader = glCreateProgram(
7319
+ '#version 300 es\n' + // specify GLSL ES version
7320
+ 'precision highp float;'+ // use highp for better accuracy
7321
+ 'uniform mat4 m;'+ // transform matrix
7322
+ 'in vec2 g;'+ // in: geometry
7323
+ 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
7324
+ 'in float r;'+ // in: rotation
7325
+ 'out vec2 v;'+ // out: uv
7326
+ 'out vec4 d,e;'+ // out: color, additiveColor
7327
+ 'void main(){'+ // shader entry point
7328
+ 'vec2 s=(g-.5)*p.zw;'+ // get size offset
7329
+ 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
7330
+ 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
7331
+ 'd=c;e=a;'+ // pass colors to fragment shader
7332
+ '}' // end of shader
7333
+ ,
7334
+ '#version 300 es\n' + // specify GLSL ES version
7335
+ 'precision highp float;'+ // use highp for better accuracy
7336
+ 'uniform sampler2D s;'+ // texture
7337
+ 'in vec2 v;'+ // in: uv
7338
+ 'in vec4 d,e;'+ // in: color, additiveColor
7339
+ 'out vec4 c;'+ // out: color
7340
+ 'void main(){'+ // shader entry point
7341
+ 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
7342
+ '}' // end of shader
7343
+ );
7015
7344
 
7016
- // init buffers
7017
- const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
7018
- glPositionData = new Float32Array(glInstanceData);
7019
- glColorData = new Uint32Array(glInstanceData);
7020
- glArrayBuffer = glContext.createBuffer();
7021
- glGeometryBuffer = glContext.createBuffer();
7345
+ // setup poly rendering shaders
7346
+ glPolyShader = glCreateProgram(
7347
+ '#version 300 es\n' + // specify GLSL ES version
7348
+ 'precision highp float;'+ // use highp for better accuracy
7349
+ 'uniform mat4 m;'+ // transform matrix
7350
+ 'in vec2 p;'+ // in: position
7351
+ 'in vec4 c;'+ // in: color
7352
+ 'out vec4 d;'+ // out: color
7353
+ 'void main(){'+ // shader entry point
7354
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
7355
+ 'd=c;'+ // pass color to fragment shader
7356
+ '}' // end of shader
7357
+ ,
7358
+ '#version 300 es\n' + // specify GLSL ES version
7359
+ 'precision highp float;'+ // use highp for better accuracy
7360
+ 'in vec4 d;'+ // in: color
7361
+ 'out vec4 c;'+ // out: color
7362
+ 'void main(){'+ // shader entry point
7363
+ 'c=d;'+ // set color
7364
+ '}' // end of shader
7365
+ );
7022
7366
 
7023
- // create the geometry buffer, triangle strip square
7024
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
7025
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7026
- glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
7367
+ // init buffers
7368
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
7369
+ glPositionData = new Float32Array(glInstanceData);
7370
+ glColorData = new Uint32Array(glInstanceData);
7371
+ glArrayBuffer = glContext.createBuffer();
7372
+ glGeometryBuffer = glContext.createBuffer();
7373
+
7374
+ // create the geometry buffer, triangle strip square
7375
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
7376
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7377
+ glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
7378
+ }
7027
7379
  }
7028
7380
 
7029
7381
  function glSetInstancedMode()
@@ -7101,8 +7453,8 @@ function glPreRender()
7101
7453
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
7102
7454
  const rotatedCam = cameraPos.rotate(-cameraAngle);
7103
7455
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
7104
- const ca = Math.cos(cameraAngle);
7105
- const sa = Math.sin(cameraAngle);
7456
+ const ca = cos(cameraAngle);
7457
+ const sa = sin(cameraAngle);
7106
7458
  const transform = [
7107
7459
  s.x * ca, s.y * sa, 0, 0,
7108
7460
  -s.x * sa, s.y * ca, 0, 0,
@@ -7130,7 +7482,7 @@ function glPreRender()
7130
7482
  // start with additive blending off
7131
7483
  glAdditive = glBatchAdditive = false;
7132
7484
 
7133
- // force it to enter instanced mode
7485
+ // force it to set instanced mode by first setting poly mode true
7134
7486
  glPolyMode = true;
7135
7487
  glSetInstancedMode();
7136
7488
  }
@@ -7271,6 +7623,41 @@ function glSetTextureData(texture, image)
7271
7623
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
7272
7624
  }
7273
7625
 
7626
+ /** Tells WebGL to create or update the glTexture and start tracking it
7627
+ * @param {TextureInfo} textureInfo
7628
+ * @memberof WebGL */
7629
+ function glRegisterTextureInfo(textureInfo)
7630
+ {
7631
+ if (headlessMode) return;
7632
+
7633
+ // add texture info to tracking list even if gl is not enabled
7634
+ glTextureInfos.add(textureInfo);
7635
+
7636
+ if (!glContext) return;
7637
+
7638
+ // create or set the texture data
7639
+ if (textureInfo.glTexture)
7640
+ glSetTextureData(textureInfo.glTexture, textureInfo.image);
7641
+ else
7642
+ textureInfo.glTexture = glCreateTexture(textureInfo.image);
7643
+ }
7644
+
7645
+ /** Tells WebGL to destroy the glTexture and stop tracking it
7646
+ * @param {TextureInfo} textureInfo
7647
+ * @memberof WebGL */
7648
+ function glUnregisterTextureInfo(textureInfo)
7649
+ {
7650
+ if (headlessMode) return;
7651
+
7652
+ // delete texture info from tracking list even if gl is not enabled
7653
+ glTextureInfos.delete(textureInfo);
7654
+
7655
+ // unset and destroy the texture
7656
+ const glTexture = textureInfo.glTexture;
7657
+ textureInfo.glTexture = undefined;
7658
+ glDeleteTexture(glTexture);
7659
+ }
7660
+
7274
7661
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
7275
7662
  * @memberof WebGL */
7276
7663
  function glFlush()
@@ -7372,8 +7759,8 @@ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
7372
7759
  // transform the point
7373
7760
  const px = p.x*sx;
7374
7761
  const py = p.y*sy;
7375
- const sa = Math.sin(-angle);
7376
- const ca = Math.cos(-angle);
7762
+ const sa = sin(-angle);
7763
+ const ca = cos(-angle);
7377
7764
  pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
7378
7765
  }
7379
7766
  const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
@@ -7861,80 +8248,97 @@ class PostProcessPlugin
7861
8248
  /** Create global post processing shader
7862
8249
  * @param {string} shaderCode
7863
8250
  * @param {boolean} [includeOverlay]
8251
+ * @param {boolean} [includeMainCanvas]
7864
8252
  * @example
7865
8253
  * // create the post process plugin object
7866
8254
  * new PostProcessPlugin(shaderCode);
7867
8255
  */
7868
- constructor(shaderCode, includeOverlay=false)
8256
+ constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
7869
8257
  {
7870
8258
  ASSERT(!postProcess, 'Post process already initialized');
7871
8259
  postProcess = this;
7872
8260
 
7873
- if (headlessMode) return;
7874
-
7875
- if (!glEnable)
7876
- {
7877
- console.warn('PostProcessPlugin: WebGL not enabled!');
7878
- return;
7879
- }
7880
-
7881
8261
  if (!shaderCode) // default shader pass through
7882
8262
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
7883
8263
 
7884
8264
  /** @property {WebGLProgram} - Shader for post processing */
7885
- this.shader = glCreateProgram(
7886
- '#version 300 es\n' + // specify GLSL ES version
7887
- 'precision highp float;'+ // use highp for better accuracy
7888
- 'in vec2 p;'+ // position
7889
- 'void main(){'+ // shader entry point
7890
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7891
- '}' // end of shader
7892
- ,
7893
- '#version 300 es\n' + // specify GLSL ES version
7894
- 'precision highp float;'+ // use highp for better accuracy
7895
- 'uniform sampler2D iChannel0;'+ // input texture
7896
- 'uniform vec3 iResolution;'+ // size of output texture
7897
- 'uniform float iTime;'+ // time
7898
- 'out vec4 c;'+ // out color
7899
- '\n' + shaderCode + '\n'+ // insert custom shader code
7900
- 'void main(){'+ // shader entry point
7901
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
7902
- 'c.a=1.;'+ // always use full alpha
7903
- '}' // end of shader
7904
- );
8265
+ this.shader = undefined;
7905
8266
 
7906
8267
  /** @property {WebGLTexture} - Texture for post processing */
7907
- this.texture = glCreateTexture();
8268
+ this.texture = undefined;
7908
8269
 
7909
- /** @property {boolean} - Should overlay canvas be included in post processing */
7910
- this.includeOverlay = includeOverlay;
8270
+ // setup the post processing plugin
8271
+ initPostProcess();
8272
+ engineAddPlugin(undefined, postProcessRender, postProcessContextLost, postProcessContextRestored);
7911
8273
 
7912
- // Render the post processing shader, called automatically by the engine
7913
- engineAddPlugin(undefined, postProcessRender);
7914
- function postProcessRender()
8274
+ function initPostProcess()
7915
8275
  {
7916
8276
  if (headlessMode) return;
7917
-
7918
- // prepare to render post process shader
7919
- if (glEnable)
7920
- {
7921
- glFlush(); // clear out the buffer
7922
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
7923
- }
7924
- else
8277
+
8278
+ if (!glEnable)
7925
8279
  {
7926
- // set the viewport
7927
- glContext.viewport(0, 0, glCanvas.width = drawCanvas.width, glCanvas.height = drawCanvas.height);
8280
+ console.warn('PostProcessPlugin: WebGL not enabled!');
8281
+ return;
7928
8282
  }
7929
8283
 
7930
- if (postProcess.includeOverlay)
8284
+ // create resources
8285
+ postProcess.texture = glCreateTexture();
8286
+ postProcess.shader = glCreateProgram(
8287
+ '#version 300 es\n' + // specify GLSL ES version
8288
+ 'precision highp float;'+ // use highp for better accuracy
8289
+ 'in vec2 p;'+ // position
8290
+ 'void main(){'+ // shader entry point
8291
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
8292
+ '}' // end of shader
8293
+ ,
8294
+ '#version 300 es\n' + // specify GLSL ES version
8295
+ 'precision highp float;'+ // use highp for better accuracy
8296
+ 'uniform sampler2D iChannel0;'+ // input texture
8297
+ 'uniform vec3 iResolution;'+ // size of output texture
8298
+ 'uniform float iTime;'+ // time
8299
+ 'out vec4 c;'+ // out color
8300
+ '\n' + shaderCode + '\n'+ // insert custom shader code
8301
+ 'void main(){'+ // shader entry point
8302
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
8303
+ 'c.a=1.;'+ // always use full alpha
8304
+ '}' // end of shader
8305
+ );
8306
+ }
8307
+ function postProcessContextLost()
8308
+ {
8309
+ postProcess.shader = undefined;
8310
+ postProcess.texture = undefined;
8311
+ LOG('PostProcessPlugin: WebGL context lost');
8312
+ }
8313
+ function postProcessContextRestored()
8314
+ {
8315
+ initPostProcess();
8316
+ LOG('PostProcessPlugin: WebGL context restored');
8317
+ }
8318
+ function postProcessRender()
8319
+ {
8320
+ if (headlessMode) return;
8321
+
8322
+ if (!glEnable)
8323
+ return;
8324
+
8325
+ // clear out the buffer
8326
+ glFlush();
8327
+
8328
+ if (includeMainCanvas || includeOverlay)
7931
8329
  {
7932
- // copy overlay canvas so it will be included in post processing
7933
- mainContext.drawImage(overlayCanvas, 0, 0);
7934
- overlayCanvas.width |= 0;
8330
+ // copy WebGL to the main canvas
8331
+ mainContext.drawImage(glCanvas, 0, 0);
8332
+
8333
+ if (includeOverlay)
8334
+ {
8335
+ // copy overlay canvas so it will be included in post processing
8336
+ mainContext.drawImage(overlayCanvas, 0, 0);
8337
+ overlayCanvas.width |= 0; // clear overlay canvas
8338
+ }
7935
8339
  }
7936
8340
 
7937
- // setup shader program to draw one triangle
8341
+ // setup shader program to draw a quad
7938
8342
  glContext.useProgram(postProcess.shader);
7939
8343
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7940
8344
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
@@ -7943,7 +8347,10 @@ class PostProcessPlugin
7943
8347
  // set textures, pass in the 2d canvas and gl canvas in separate texture channels
7944
8348
  glContext.activeTexture(glContext.TEXTURE0);
7945
8349
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
7946
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
8350
+ if (includeMainCanvas || includeOverlay)
8351
+ {
8352
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
8353
+ }
7947
8354
 
7948
8355
  // set vertex position attribute
7949
8356
  const vertexByteStride = 8;
@@ -8173,6 +8580,8 @@ class UISystemPlugin
8173
8580
  this.defaultHoverColor = hsl(0,0,.9);
8174
8581
  /** @property {Color} - Default color for disabled UI elements */
8175
8582
  this.defaultDisabledColor = hsl(0,0,.3);
8583
+ /** @property {Color} - Uses a gradient fill combined with color */
8584
+ this.defaultGradientColor = undefined;
8176
8585
  /** @property {number} - Default line width for UI elements */
8177
8586
  this.defaultLineWidth = 4;
8178
8587
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -8180,7 +8589,7 @@ class UISystemPlugin
8180
8589
  /** @property {number} - Default scale to use for fitting text to object */
8181
8590
  this.defaultTextScale = .8;
8182
8591
  /** @property {string} - Default font for UI elements */
8183
- this.defaultFont = 'arial';
8592
+ this.defaultFont = fontDefault;
8184
8593
  /** @property {Sound} - Default sound when interactive UI element is pressed */
8185
8594
  this.defaultSoundPress = undefined;
8186
8595
  /** @property {Sound} - Default sound when interactive UI element is released */
@@ -8197,7 +8606,9 @@ class UISystemPlugin
8197
8606
  this.hoverObject = undefined;
8198
8607
  /** @property {UIObject} - Hover object at start of update */
8199
8608
  this.lastHoverObject = undefined;
8200
-
8609
+ /** @property {number} - If set ui coords will be renormalized to this canvas height */
8610
+ this.nativeHeight = 0;
8611
+
8201
8612
  engineAddPlugin(uiUpdate, uiRender);
8202
8613
 
8203
8614
  // setup recursive update and render
@@ -8237,6 +8648,17 @@ class UISystemPlugin
8237
8648
  }
8238
8649
  function uiRender()
8239
8650
  {
8651
+ const context = uiSystem.uiContext;
8652
+ context.save();
8653
+ if (uiSystem.nativeHeight)
8654
+ {
8655
+ // convert to native height
8656
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8657
+ context.translate(-s*mainCanvasSize.x/2,0);
8658
+ context.scale(s,s);
8659
+ context.translate(mainCanvasSize.x/2/s,0);
8660
+ }
8661
+
8240
8662
  function renderObject(o)
8241
8663
  {
8242
8664
  if (!o.visible)
@@ -8248,6 +8670,7 @@ class UISystemPlugin
8248
8670
  renderObject(c);
8249
8671
  }
8250
8672
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
8673
+ context.restore();
8251
8674
  }
8252
8675
  }
8253
8676
 
@@ -8257,8 +8680,9 @@ class UISystemPlugin
8257
8680
  * @param {Color} [color=uiSystem.defaultColor]
8258
8681
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
8259
8682
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
8260
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
8261
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
8683
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
8684
+ * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
8685
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
8262
8686
  {
8263
8687
  ASSERT(isVector2(pos), 'pos must be a vec2');
8264
8688
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -8268,7 +8692,18 @@ class UISystemPlugin
8268
8692
  ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
8269
8693
 
8270
8694
  const context = uiSystem.uiContext;
8271
- context.fillStyle = color.toString();
8695
+ if (gradientColor)
8696
+ {
8697
+ const g = context.createLinearGradient(
8698
+ pos.x, pos.y-size.y/2, pos.x, pos.y+size.y/2);
8699
+ const c = color.toString();
8700
+ g.addColorStop(0, c);
8701
+ g.addColorStop(.5, gradientColor.toString());
8702
+ g.addColorStop(1, c);
8703
+ context.fillStyle = g;
8704
+ }
8705
+ else
8706
+ context.fillStyle = color.toString();
8272
8707
  context.beginPath();
8273
8708
  if (cornerRadius && context['roundRect'])
8274
8709
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
@@ -8325,10 +8760,11 @@ class UISystemPlugin
8325
8760
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
8326
8761
  * @param {string} [align]
8327
8762
  * @param {string} [font=uiSystem.defaultFont]
8763
+ * @param {string} [fontStyle]
8328
8764
  * @param {boolean} [applyMaxWidth=true] */
8329
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, applyMaxWidth=true)
8765
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
8330
8766
  {
8331
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8767
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8332
8768
  }
8333
8769
 
8334
8770
  /**
@@ -8389,17 +8825,21 @@ class UIObject
8389
8825
  /** @property {boolean} - Is this object disabled? */
8390
8826
  this.disabled = false;
8391
8827
  /** @property {Color} - Color for text */
8392
- this.textColor = uiSystem.defaultTextColor.copy()
8828
+ this.textColor = uiSystem.defaultTextColor.copy();
8393
8829
  /** @property {Color} - Color used when hovering over the object */
8394
- this.hoverColor = uiSystem.defaultHoverColor.copy()
8830
+ this.hoverColor = uiSystem.defaultHoverColor.copy();
8395
8831
  /** @property {Color} - Color for line drawing */
8396
- this.lineColor = uiSystem.defaultLineColor.copy()
8832
+ this.lineColor = uiSystem.defaultLineColor.copy();
8833
+ /** @property {Color} - Uses a gradient fill combined with color */
8834
+ this.gradientColor = uiSystem.defaultGradientColor ? uiSystem.defaultGradientColor.copy() : undefined;
8397
8835
  /** @property {number} - Width for line drawing */
8398
8836
  this.lineWidth = uiSystem.defaultLineWidth;
8399
8837
  /** @property {number} - Corner radius for rounded rects */
8400
8838
  this.cornerRadius = uiSystem.defaultCornerRadius;
8401
8839
  /** @property {string} - Font for this objecct */
8402
8840
  this.font = uiSystem.defaultFont;
8841
+ /** @property {string} - Font style for this object or undefined */
8842
+ this.fontStyle = undefined;
8403
8843
  /** @property {number} - Override for text width */
8404
8844
  this.textWidth = undefined;
8405
8845
  /** @property {number} - Override for text height */
@@ -8424,6 +8864,8 @@ class UIObject
8424
8864
  this.interactive = false;
8425
8865
  /** @property {boolean} - Activate when dragged over with mouse held down */
8426
8866
  this.dragActivate = false;
8867
+ /** @property {boolean} - True if this can be a hover object */
8868
+ this.canBeHover = true;
8427
8869
  uiSystem.uiObjects.push(this);
8428
8870
  }
8429
8871
 
@@ -8447,6 +8889,26 @@ class UIObject
8447
8889
  child.parent = undefined;
8448
8890
  }
8449
8891
 
8892
+ /** Check if the mouse is overlapping a box in screen space
8893
+ * @return {boolean} - True if overlapping
8894
+ */
8895
+ isMouseOverlapping()
8896
+ {
8897
+ const size = !isTouchDevice ? this.size :
8898
+ this.size.add(vec2(this.extraTouchSize || 0));
8899
+ if (!uiSystem.nativeHeight)
8900
+ return isOverlapping(this.pos, size, mousePosScreen);
8901
+
8902
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8903
+ const sInv = 1/s;
8904
+ let pos = mousePosScreen.copy();
8905
+ pos.x += s*mainCanvasSize.x/2;
8906
+ pos.x *= sInv;
8907
+ pos.y *= sInv;
8908
+ pos.x -= sInv*mainCanvasSize.x/2;
8909
+ return isOverlapping(this.pos, size, pos);
8910
+ }
8911
+
8450
8912
  /** Update the object, called automatically by plugin once each frame */
8451
8913
  update()
8452
8914
  {
@@ -8454,13 +8916,10 @@ class UIObject
8454
8916
  const isActive = this.isActiveObject();
8455
8917
  const mouseDown = mouseIsDown(0);
8456
8918
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
8457
- if (!uiSystem.hoverObject)
8919
+ if (this.canBeHover)
8458
8920
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
8459
- {
8460
- const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
8461
- if (isOverlapping(this.pos, size, mousePosScreen))
8462
- uiSystem.hoverObject = this;
8463
- }
8921
+ if (!uiSystem.hoverObject && this.isMouseOverlapping())
8922
+ uiSystem.hoverObject = this;
8464
8923
  if (this.isHoverObject())
8465
8924
  {
8466
8925
  if (!this.disabled)
@@ -8585,11 +9044,13 @@ class UIText extends UIObject
8585
9044
 
8586
9045
  // make text not outlined by default
8587
9046
  this.lineWidth = 0;
9047
+ // text can not be a hover object by default
9048
+ this.canBeHover = false;
8588
9049
  }
8589
9050
  render()
8590
9051
  {
8591
9052
  const textSize = this.getTextSize();
8592
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
9053
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
8593
9054
  }
8594
9055
  }
8595
9056
 
@@ -8655,7 +9116,7 @@ class UIButton extends UIObject
8655
9116
 
8656
9117
  // set properties
8657
9118
  this.text = text;
8658
- this.color = color.copy()
9119
+ this.color = color.copy();
8659
9120
  this.interactive = true;
8660
9121
  }
8661
9122
  render()
@@ -8665,7 +9126,7 @@ class UIButton extends UIObject
8665
9126
  // draw the text scaled to fit
8666
9127
  const textSize = this.getTextSize();
8667
9128
  uiSystem.drawText(this.text, this.pos, textSize,
8668
- this.textColor, 0, undefined, this.align, this.font);
9129
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8669
9130
  }
8670
9131
  }
8671
9132
 
@@ -8719,7 +9180,7 @@ class UICheckbox extends UIObject
8719
9180
  const textSize = this.getTextSize();
8720
9181
  const pos = this.pos.add(vec2(this.size.x,0));
8721
9182
  uiSystem.drawText(this.text, pos, textSize,
8722
- this.textColor, 0, undefined, 'left', this.font, false);
9183
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
8723
9184
  }
8724
9185
  }
8725
9186
 
@@ -8797,14 +9258,13 @@ class UIScrollbar extends UIObject
8797
9258
  const handlePos = isHorizontal ?
8798
9259
  vec2(lerp(p1, p2, this.value), this.pos.y) :
8799
9260
  vec2(this.pos.x, lerp(p2, p1, this.value))
8800
- const handleColor = this.disabled ? this.disabledColor :
8801
- this.interactive && this.isActiveObject() ? this.color : this.handleColor;
9261
+ const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8802
9262
  uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8803
9263
 
8804
9264
  // draw the text scaled to fit on the scrollbar
8805
9265
  const textSize = this.getTextSize();
8806
9266
  uiSystem.drawText(this.text, this.pos, textSize,
8807
- this.textColor, 0, undefined, this.align, this.font);
9267
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8808
9268
  }
8809
9269
  }
8810
9270
  /**
@@ -10822,6 +11282,7 @@ export
10822
11282
  gamepadDirectionEmulateStick,
10823
11283
  inputWASDEmulateDirection,
10824
11284
  touchGamepadEnable,
11285
+ touchGamepadCenterButton,
10825
11286
  touchGamepadAnalog,
10826
11287
  touchGamepadSize,
10827
11288
  touchGamepadAlpha,
@@ -10865,6 +11326,7 @@ export
10865
11326
  setGamepadDirectionEmulateStick,
10866
11327
  setInputWASDEmulateDirection,
10867
11328
  setTouchGamepadEnable,
11329
+ setTouchGamepadCenterButton,
10868
11330
  setTouchGamepadAnalog,
10869
11331
  setTouchGamepadSize,
10870
11332
  setTouchGamepadAlpha,
@@ -10883,9 +11345,18 @@ export
10883
11345
  // Utilities
10884
11346
  PI,
10885
11347
  abs,
11348
+ floor,
11349
+ ceil,
11350
+ round,
10886
11351
  min,
10887
11352
  max,
10888
11353
  sign,
11354
+ hypot,
11355
+ log2,
11356
+ sin,
11357
+ cos,
11358
+ tan,
11359
+ atan2,
10889
11360
  mod,
10890
11361
  clamp,
10891
11362
  percent,
@@ -10953,6 +11424,8 @@ export
10953
11424
  drawCount,
10954
11425
  screenToWorld,
10955
11426
  worldToScreen,
11427
+ screenToWorldDelta,
11428
+ worldToScreenDelta,
10956
11429
  drawTile,
10957
11430
  drawRect,
10958
11431
  drawRectGradient,