littlejsengine 1.14.19 → 1.14.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/littlejs.d.ts +118 -18
  2. package/dist/littlejs.esm.js +506 -199
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +493 -199
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +468 -200
  7. package/examples/electron/game.js +1 -41
  8. package/examples/electron/index.html +2 -2
  9. package/examples/htmlMenu/game.js +1 -1
  10. package/examples/index.html +12 -6
  11. package/examples/module/game.js +5 -7
  12. package/examples/particles/index.html +6 -9
  13. package/examples/platformer/gameCharacter.js +1 -1
  14. package/examples/platformer/gameEffects.js +7 -2
  15. package/examples/platformer/gameObjects.js +3 -3
  16. package/examples/shorts/animation.js +2 -2
  17. package/examples/shorts/base.html +4 -1
  18. package/examples/shorts/cameraDrag.js +22 -0
  19. package/examples/shorts/debugDraw.js +38 -0
  20. package/examples/shorts/fps.js +90 -0
  21. package/examples/shorts/helloWorld.js +1 -1
  22. package/examples/shorts/hillGlideGame.js +1 -1
  23. package/examples/shorts/landerGame.js +1 -1
  24. package/examples/shorts/postProcess.js +1 -1
  25. package/examples/shorts/sequencer.js +3 -3
  26. package/examples/shorts/spaceGame.js +1 -1
  27. package/examples/shorts/spriteAtlas.js +6 -6
  28. package/examples/shorts/starfield.js +1 -1
  29. package/examples/shorts/tileRaycast.js +39 -0
  30. package/examples/shorts/tiles.png +0 -0
  31. package/examples/shorts/tiltedView.js +1 -1
  32. package/examples/starter/game.js +5 -7
  33. package/examples/starter/index.html +2 -2
  34. package/examples/typescript/game.js +3 -2
  35. package/examples/typescript/game.ts +5 -7
  36. package/examples/uiSystem/game.js +2 -2
  37. package/package.json +1 -1
  38. package/src/engine.js +3 -3
  39. package/src/engineAudio.js +13 -13
  40. package/src/engineDebug.js +27 -0
  41. package/src/engineDraw.js +68 -20
  42. package/src/engineExport.js +13 -0
  43. package/src/engineInput.js +14 -16
  44. package/src/engineObject.js +31 -6
  45. package/src/engineParticles.js +36 -15
  46. package/src/engineRelease.js +2 -1
  47. package/src/engineSettings.js +20 -1
  48. package/src/engineTileLayer.js +62 -59
  49. package/src/engineUtilities.js +206 -58
  50. package/src/engineWebGL.js +13 -8
  51. package/examples/shorts/raycasting.js +0 -79
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.19';
36
+ const engineVersion = '1.14.24';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -516,7 +516,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
516
516
 
517
517
  /**
518
518
  * @callback ObjectCallbackFunction - Function that processes an object
519
- * @param {EngineObject} uiObjects
519
+ * @param {EngineObject} object
520
520
  * @memberof Engine
521
521
  */
522
522
 
@@ -564,7 +564,7 @@ function drawEngineSplashScreen(t)
564
564
  // background
565
565
  const p3 = percent(t, 1, .8);
566
566
  const p4 = percent(t, 0, .5);
567
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,Math.hypot(w,h)*.7);
567
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
568
568
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
569
569
  g.addColorStop(1,hsl(0,0,0,p3).toString());
570
570
  x.save();
@@ -1339,6 +1339,33 @@ function debugVideoCaptureUpdate()
1339
1339
  combineCanvases();
1340
1340
  debugVideoCaptureTrack.requestFrame();
1341
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);
1342
1369
  }
1343
1370
  /**
1344
1371
  * LittleJS Utility Classes and Functions
@@ -1350,7 +1377,7 @@ function debugVideoCaptureUpdate()
1350
1377
  * @namespace Utilities
1351
1378
  */
1352
1379
 
1353
- /** A shortcut to get Math.PI
1380
+ /** The value of PI
1354
1381
  * @type {number}
1355
1382
  * @default Math.PI
1356
1383
  * @memberof Utilities */
@@ -1360,25 +1387,80 @@ const PI = Math.PI;
1360
1387
  * @param {number} value
1361
1388
  * @return {number}
1362
1389
  * @memberof Utilities */
1363
- 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;
1364
1409
 
1365
1410
  /** Returns lowest value passed in
1366
1411
  * @param {...number} values
1367
1412
  * @return {number}
1368
1413
  * @memberof Utilities */
1369
- function min(...values) { return Math.min(...values); }
1414
+ const min = Math.min;
1370
1415
 
1371
1416
  /** Returns highest value passed in
1372
1417
  * @param {...number} values
1373
1418
  * @return {number}
1374
1419
  * @memberof Utilities */
1375
- function max(...values) { return Math.max(...values); }
1420
+ const max = Math.max;
1376
1421
 
1377
1422
  /** Returns the sign of value passed in
1378
1423
  * @param {number} value
1379
1424
  * @return {number}
1380
1425
  * @memberof Utilities */
1381
- 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;
1382
1464
 
1383
1465
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
1384
1466
  * @param {number} dividend
@@ -1483,7 +1565,7 @@ function isPowerOfTwo(value) { return !(value & (value - 1)); }
1483
1565
  * @param {number} value
1484
1566
  * @return {number}
1485
1567
  * @memberof Utilities */
1486
- function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
1568
+ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
1487
1569
 
1488
1570
  /** Returns true if two axis aligned bounding boxes are overlapping
1489
1571
  * this can be used for simple collision detection between objects
@@ -1551,7 +1633,7 @@ function isIntersecting(start, end, pos, size)
1551
1633
  * @return {number} - Value waving between 0 and amplitude
1552
1634
  * @memberof Utilities */
1553
1635
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1554
- { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
1636
+ { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
1555
1637
 
1556
1638
  /** Formats seconds to mm:ss style for display purposes
1557
1639
  * @param {number} t - time in seconds
@@ -1590,6 +1672,96 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1590
1672
  * @memberof Utilities */
1591
1673
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1592
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
+
1593
1765
  ///////////////////////////////////////////////////////////////////////////////
1594
1766
 
1595
1767
  /** Random global functions
@@ -1608,7 +1780,7 @@ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valu
1608
1780
  * @param {number} [valueB]
1609
1781
  * @return {number}
1610
1782
  * @memberof Random */
1611
- function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
1783
+ function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
1612
1784
 
1613
1785
  /** Randomly returns true or false given the chance of true passed in
1614
1786
  * @param {number} [chance]
@@ -1687,7 +1859,7 @@ class RandomGenerator
1687
1859
  * @param {number} valueA
1688
1860
  * @param {number} [valueB]
1689
1861
  * @return {number} */
1690
- int(valueA, valueB=0) { return Math.floor(this.float(valueA, valueB)); }
1862
+ int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
1691
1863
 
1692
1864
  /** Randomly returns true or false given the chance of true passed in
1693
1865
  * @param {number} [chance]
@@ -1714,6 +1886,39 @@ class RandomGenerator
1714
1886
  * @return {Vector2} */
1715
1887
  vec2(valueA=1, valueB=0)
1716
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
+ }
1717
1922
  }
1718
1923
 
1719
1924
  ///////////////////////////////////////////////////////////////////////////////
@@ -1865,7 +2070,7 @@ class Vector2
1865
2070
 
1866
2071
  /** Returns the clockwise angle of this vector, up is angle 0
1867
2072
  * @return {number} */
1868
- angle() { return Math.atan2(this.x, this.y); }
2073
+ angle() { return atan2(this.x, this.y); }
1869
2074
 
1870
2075
  /** Sets this vector with clockwise angle and length passed in
1871
2076
  * @param {number} [angle]
@@ -1875,8 +2080,8 @@ class Vector2
1875
2080
  {
1876
2081
  ASSERT_NUMBER_VALID(angle);
1877
2082
  ASSERT_NUMBER_VALID(length);
1878
- this.x = length*Math.sin(angle);
1879
- this.y = length*Math.cos(angle);
2083
+ this.x = length*sin(angle);
2084
+ this.y = length*cos(angle);
1880
2085
  return this;
1881
2086
  }
1882
2087
 
@@ -1886,7 +2091,7 @@ class Vector2
1886
2091
  rotate(angle)
1887
2092
  {
1888
2093
  ASSERT_NUMBER_VALID(angle);
1889
- const c = Math.cos(-angle), s = Math.sin(-angle);
2094
+ const c = cos(-angle), s = sin(-angle);
1890
2095
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1891
2096
  }
1892
2097
 
@@ -1918,7 +2123,7 @@ class Vector2
1918
2123
 
1919
2124
  /** Returns a copy of this vector with each axis floored
1920
2125
  * @return {Vector2} */
1921
- floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
2126
+ floor() { return new Vector2(floor(this.x), floor(this.y)); }
1922
2127
 
1923
2128
  /** Returns new vec2 with modded values
1924
2129
  * @param {number} [divisor]
@@ -2226,67 +2431,67 @@ class Color
2226
2431
  /** Color - White #ffffff
2227
2432
  * @type {Color}
2228
2433
  * @memberof Utilities */
2229
- const WHITE = protectEngineConstant(rgb());
2434
+ const WHITE = debugProtectConstant(rgb());
2230
2435
 
2231
2436
  /** Color - Clear White #757474ff with 0 alpha
2232
2437
  * @type {Color}
2233
2438
  * @memberof Utilities */
2234
- const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
2439
+ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
2235
2440
 
2236
2441
  /** Color - Black #000000
2237
2442
  * @type {Color}
2238
2443
  * @memberof Utilities */
2239
- const BLACK = protectEngineConstant(rgb(0,0,0));
2444
+ const BLACK = debugProtectConstant(rgb(0,0,0));
2240
2445
 
2241
2446
  /** Color - Clear Black #000000 with 0 alpha
2242
2447
  * @type {Color}
2243
2448
  * @memberof Utilities */
2244
- const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
2449
+ const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
2245
2450
 
2246
2451
  /** Color - Gray #808080
2247
2452
  * @type {Color}
2248
2453
  * @memberof Utilities */
2249
- const GRAY = protectEngineConstant(rgb(.5,.5,.5));
2454
+ const GRAY = debugProtectConstant(rgb(.5,.5,.5));
2250
2455
 
2251
2456
  /** Color - Red #ff0000
2252
2457
  * @type {Color}
2253
2458
  * @memberof Utilities */
2254
- const RED = protectEngineConstant(rgb(1,0,0));
2459
+ const RED = debugProtectConstant(rgb(1,0,0));
2255
2460
 
2256
2461
  /** Color - Orange #ff8000
2257
2462
  * @type {Color}
2258
2463
  * @memberof Utilities */
2259
- const ORANGE = protectEngineConstant(rgb(1,.5,0));
2464
+ const ORANGE = debugProtectConstant(rgb(1,.5,0));
2260
2465
 
2261
2466
  /** Color - Yellow #ffff00
2262
2467
  * @type {Color}
2263
2468
  * @memberof Utilities */
2264
- const YELLOW = protectEngineConstant(rgb(1,1,0));
2469
+ const YELLOW = debugProtectConstant(rgb(1,1,0));
2265
2470
 
2266
2471
  /** Color - Green #00ff00
2267
2472
  * @type {Color}
2268
2473
  * @memberof Utilities */
2269
- const GREEN = protectEngineConstant(rgb(0,1,0));
2474
+ const GREEN = debugProtectConstant(rgb(0,1,0));
2270
2475
 
2271
2476
  /** Color - Cyan #00ffff
2272
2477
  * @type {Color}
2273
2478
  * @memberof Utilities */
2274
- const CYAN = protectEngineConstant(rgb(0,1,1));
2479
+ const CYAN = debugProtectConstant(rgb(0,1,1));
2275
2480
 
2276
2481
  /** Color - Blue #0000ff
2277
2482
  * @type {Color}
2278
2483
  * @memberof Utilities */
2279
- const BLUE = protectEngineConstant(rgb(0,0,1));
2484
+ const BLUE = debugProtectConstant(rgb(0,0,1));
2280
2485
 
2281
2486
  /** Color - Purple #8000ff
2282
2487
  * @type {Color}
2283
2488
  * @memberof Utilities */
2284
- const PURPLE = protectEngineConstant(rgb(.5,0,1));
2489
+ const PURPLE = debugProtectConstant(rgb(.5,0,1));
2285
2490
 
2286
2491
  /** Color - Magenta #ff00ff
2287
2492
  * @type {Color}
2288
2493
  * @memberof Utilities */
2289
- const MAGENTA = protectEngineConstant(rgb(1,0,1));
2494
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2290
2495
 
2291
2496
  ///////////////////////////////////////////////////////////////////////////////
2292
2497
 
@@ -2349,41 +2554,11 @@ class Timer
2349
2554
 
2350
2555
  /** Returns this timer expressed as a string
2351
2556
  * @return {string} */
2352
- 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'; }
2353
2558
 
2354
2559
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2355
2560
  * @return {number} */
2356
2561
  valueOf() { return this.get(); }
2357
- }
2358
-
2359
- ///////////////////////////////////////////////////////////////////////////////
2360
- // Helper functions used by the engine
2361
-
2362
- // make color constants immutable with debug assertions
2363
- function protectEngineConstant(obj)
2364
- {
2365
- if (debug)
2366
- {
2367
- // get properties and store original values
2368
- const props = Object.keys(obj), values = {};
2369
- props.forEach(prop => values[prop] = obj[prop]);
2370
-
2371
- // replace with getters/setters that assert
2372
- props.forEach(prop =>
2373
- {
2374
- Object.defineProperty(obj, prop, {
2375
- get: () => values[prop],
2376
- set: (value) =>
2377
- {
2378
- ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
2379
- },
2380
- enumerable: true
2381
- });
2382
- });
2383
- }
2384
-
2385
- // freeze the object to prevent adding new properties
2386
- return Object.freeze(obj);
2387
2562
  }
2388
2563
  /**
2389
2564
  * LittleJS Engine Settings
@@ -2601,6 +2776,14 @@ let touchInputEnable = true;
2601
2776
  * @memberof Settings */
2602
2777
  let touchGamepadEnable = false;
2603
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
+
2604
2787
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
2605
2788
  * @type {boolean}
2606
2789
  * @default
@@ -2776,9 +2959,14 @@ function setHeadlessMode(headless) { headlessMode = headless; }
2776
2959
  * @memberof Settings */
2777
2960
  function setGLEnable(enable)
2778
2961
  {
2962
+ if (enable && !glCanBeEnabled)
2963
+ {
2964
+ console.warn('Can not enable WebGL if it was disabled on start.');
2965
+ return;
2966
+ }
2779
2967
  glEnable = enable;
2780
2968
  if (glCanvas) // hide glCanvas if WebGL is disabled
2781
- glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2969
+ glCanvas.style.display = enable ? '' : 'none';
2782
2970
  }
2783
2971
 
2784
2972
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
@@ -2866,6 +3054,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
2866
3054
  * @memberof Settings */
2867
3055
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
2868
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
+
2869
3063
  /** Set if touch gamepad should be analog stick or 8 way dpad
2870
3064
  * @param {boolean} analog
2871
3065
  * @memberof Settings */
@@ -3227,6 +3421,27 @@ class EngineObject
3227
3421
  // test which side we bounced off (or both if a corner)
3228
3422
  const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
3229
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
+ }
3230
3445
  if (blockedLayerY || !blockedLayerX)
3231
3446
  {
3232
3447
  // bounce velocity
@@ -3250,12 +3465,6 @@ class EngineObject
3250
3465
  this.groundObject = undefined;
3251
3466
  }
3252
3467
  }
3253
- if (blockedLayerX)
3254
- {
3255
- // move to previous position and bounce
3256
- this.pos.x = oldPos.x;
3257
- this.velocity.x *= -this.restitution;
3258
- }
3259
3468
  debugPhysics && debugRect(this.pos, this.size, '#f00');
3260
3469
  }
3261
3470
  }
@@ -3313,6 +3522,16 @@ class EngineObject
3313
3522
  */
3314
3523
  collideWithObject(object) { return true; }
3315
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
+
3316
3535
  /** How long since the object was created
3317
3536
  * @return {number} */
3318
3537
  getAliveTime() { return time - this.spawnTime; }
@@ -3664,6 +3883,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3664
3883
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3665
3884
  if (useWebGL)
3666
3885
  {
3886
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3667
3887
  if (screenSpace)
3668
3888
  {
3669
3889
  // convert to world space
@@ -3759,6 +3979,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3759
3979
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3760
3980
  if (useWebGL)
3761
3981
  {
3982
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3762
3983
  if (screenSpace)
3763
3984
  {
3764
3985
  // convert to world space
@@ -3770,7 +3991,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3770
3991
  const halfSizeX = size.x/2, halfSizeY = size.y/2;
3771
3992
  const colorTopInt = colorTop.rgbaInt();
3772
3993
  const colorBottomInt = colorBottom.rgbaInt();
3773
- const c = Math.cos(-angle), s = Math.sin(-angle);
3994
+ const c = cos(-angle), s = sin(-angle);
3774
3995
  for (let i=4; i--;)
3775
3996
  {
3776
3997
  const x = i & 1 ? halfSizeX : -halfSizeX;
@@ -3821,6 +4042,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3821
4042
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3822
4043
  if (useWebGL)
3823
4044
  {
4045
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3824
4046
  let scale = 1;
3825
4047
  if (screenSpace)
3826
4048
  {
@@ -3870,6 +4092,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3870
4092
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
3871
4093
  const size = vec2(width, halfDelta.length()*2);
3872
4094
  pos = pos.add(posA.add(halfDelta));
4095
+ if (screenSpace)
4096
+ halfDelta.y *= -1; // flip angle Y if screen space
3873
4097
  angle += halfDelta.angle();
3874
4098
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3875
4099
  }
@@ -3897,7 +4121,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3897
4121
  for (let i=sides; i--;)
3898
4122
  {
3899
4123
  const a = (i/sides)*PI*2;
3900
- points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
4124
+ points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
3901
4125
  }
3902
4126
  drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3903
4127
  }
@@ -3924,6 +4148,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3924
4148
 
3925
4149
  if (useWebGL)
3926
4150
  {
4151
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3927
4152
  let scale = 1;
3928
4153
  if (screenSpace)
3929
4154
  {
@@ -4145,18 +4370,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4145
4370
  * @memberof Draw */
4146
4371
  function screenToWorld(screenPos)
4147
4372
  {
4148
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
4149
- 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;
4150
4375
  if (cameraAngle)
4151
4376
  {
4152
4377
  // apply 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;
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;
4158
4383
  }
4159
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
4384
+ return new Vector2(x + cameraPos.x, y + cameraPos.y);
4160
4385
  }
4161
4386
 
4162
4387
  /** Convert from world to screen space coordinates
@@ -4165,24 +4390,64 @@ function screenToWorld(screenPos)
4165
4390
  * @memberof Draw */
4166
4391
  function worldToScreen(worldPos)
4167
4392
  {
4168
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
4169
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
4393
+ let x = worldPos.x - cameraPos.x;
4394
+ let y = worldPos.y - cameraPos.y;
4170
4395
  if (cameraAngle)
4171
4396
  {
4172
4397
  // apply inverse camera rotation
4173
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
4174
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
4175
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
4176
- cameraPosRelativeX = rotatedX;
4177
- 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;
4178
4403
  }
4179
4404
  return new Vector2
4180
4405
  (
4181
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
4182
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
4406
+ x * cameraScale + mainCanvasSize.x/2 - .5,
4407
+ y * -cameraScale + mainCanvasSize.y/2 - .5
4183
4408
  );
4184
4409
  }
4185
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
+
4186
4451
  /** Get the camera's visible area in world space
4187
4452
  * @return {Vector2}
4188
4453
  * @memberof Draw */
@@ -4235,6 +4500,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4235
4500
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4236
4501
  const sx2 = bleedScale;
4237
4502
  const sy2 = bleedScale;
4503
+ sWidth = max(1,sWidth|0);
4504
+ sHeight = max(1,sHeight|0);
4238
4505
  const sWidth2 = sWidth - 2*bleedScale;
4239
4506
  const sHeight2 = sHeight - 2*bleedScale;
4240
4507
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
@@ -4249,7 +4516,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4249
4516
  // copy to offscreen canvas
4250
4517
  workCanvas.width = sWidth;
4251
4518
  workCanvas.height = sHeight;
4252
- 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);
4253
4520
 
4254
4521
  // tint image using offscreen work context
4255
4522
  const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
@@ -4586,9 +4853,9 @@ function inputUpdate()
4586
4853
  if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
4587
4854
  inputClear();
4588
4855
 
4589
- // update mouse world space position
4856
+ // update mouse world space position and delta
4590
4857
  mousePos = screenToWorld(mousePosScreen);
4591
- mouseDelta = mouseDeltaScreen.multiply(vec2(1,-1)).rotate(-cameraAngle);
4858
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
4592
4859
 
4593
4860
  // update gamepads if enabled
4594
4861
  gamepadsUpdate();
@@ -4620,7 +4887,6 @@ function inputInit()
4620
4887
  document.addEventListener('wheel', onMouseWheel);
4621
4888
  document.addEventListener('contextmenu', onContextMenu);
4622
4889
  document.addEventListener('blur', onBlur);
4623
- document.addEventListener('mouseleave', onMouseLeave);
4624
4890
 
4625
4891
  // init touch input
4626
4892
  if (isTouchDevice && touchInputEnable)
@@ -4662,7 +4928,10 @@ function inputInit()
4662
4928
 
4663
4929
  isUsingGamepad = false;
4664
4930
  inputData[0][e.button] = 3;
4931
+
4932
+ let mousePosScreenLast = mousePosScreen;
4665
4933
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4934
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4666
4935
  inputPreventDefault && e.button && e.preventDefault();
4667
4936
  }
4668
4937
  function onMouseUp(e)
@@ -4673,18 +4942,13 @@ function inputInit()
4673
4942
  }
4674
4943
  function onMouseMove(e)
4675
4944
  {
4945
+ let mousePosScreenLast = mousePosScreen;
4676
4946
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4677
- mouseDeltaScreen = mouseDeltaScreen.add(vec2(e.movementX, e.movementY));
4947
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4678
4948
  }
4679
4949
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4680
4950
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4681
4951
  function onBlur() { inputClear(); } // reset input when focus is lost
4682
- function onMouseLeave()
4683
- {
4684
- // set mouse position and delta when leaving canvas
4685
- mousePosScreen = vec2(-1);
4686
- mouseDeltaScreen = vec2(0);
4687
- }
4688
4952
  }
4689
4953
 
4690
4954
  // convert a mouse or touch event position to screen space
@@ -4728,8 +4992,8 @@ function gamepadsUpdate()
4728
4992
  else if (touchGamepadStick.lengthSquared() > .3)
4729
4993
  {
4730
4994
  // convert to 8 way dpad
4731
- sticks[0].x = Math.round(touchGamepadStick.x);
4732
- sticks[0].y = -Math.round(touchGamepadStick.y);
4995
+ sticks[0].x = round(touchGamepadStick.x);
4996
+ sticks[0].y = -round(touchGamepadStick.y);
4733
4997
  sticks[0] = sticks[0].clampLength();
4734
4998
  }
4735
4999
 
@@ -4857,11 +5121,11 @@ function touchInputInit()
4857
5121
  {
4858
5122
  // set event pos and pass it along
4859
5123
  const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
4860
- const lastMousePosScreen = mousePosScreen;
5124
+ const mousePosScreenLast = mousePosScreen;
4861
5125
  mousePosScreen = mouseEventToScreen(pos);
4862
5126
  if (wasTouching)
4863
5127
  {
4864
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(lastMousePosScreen));
5128
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4865
5129
  isUsingGamepad = touchGamepadEnable;
4866
5130
  }
4867
5131
  else
@@ -4893,7 +5157,7 @@ function touchInputInit()
4893
5157
  if (touching)
4894
5158
  {
4895
5159
  touchGamepadTimer.set();
4896
- if (paused && !wasTouching)
5160
+ if (touchGamepadCenterButton && !wasTouching && paused)
4897
5161
  {
4898
5162
  // touch anywhere to press start when paused
4899
5163
  touchGamepadButtons[9] = 1;
@@ -4932,7 +5196,8 @@ function touchInputInit()
4932
5196
  if (button < touchGamepadButtonCount)
4933
5197
  touchGamepadButtons[button] = 1;
4934
5198
  }
4935
- else if (startCenter.distance(touchPos) < touchGamepadSize && !wasTouching)
5199
+ else if (touchGamepadCenterButton && !wasTouching &&
5200
+ startCenter.distance(touchPos) < touchGamepadSize)
4936
5201
  {
4937
5202
  // virtual start button in center
4938
5203
  touchGamepadButtons[9] = 1;
@@ -5628,10 +5893,10 @@ function zzfxG
5628
5893
 
5629
5894
  // biquad LP/HP filter
5630
5895
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
5631
- cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
5632
- a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
5633
- b0 = (1 + sign(filter) * cos) / 2 / a0,
5634
- 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,
5635
5900
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
5636
5901
 
5637
5902
  // scale by sample rate
@@ -5654,15 +5919,15 @@ function zzfxG
5654
5919
  if (!(++crush%(bitCrush*100|0))) // bit crush
5655
5920
  {
5656
5921
  s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
5657
- (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
5658
- Math.sin(t**3) : // 4 noise
5659
- Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
5660
- 1-(2*t/PI2%2+2)%2: // 2 saw
5661
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
5662
- 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
5663
5928
 
5664
5929
  s = (repeatTime ?
5665
- 1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
5930
+ 1 - tremolo + tremolo*sin(PI2*i/repeatTime) // tremolo
5666
5931
  : 1) *
5667
5932
  (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
5668
5933
  (i < attack ? i/attack : // attack
@@ -5684,8 +5949,8 @@ function zzfxG
5684
5949
  }
5685
5950
 
5686
5951
  f = (frequency += slide += deltaSlide) *// frequency
5687
- Math.cos(modulation*modOffset++); // modulation
5688
- t += f + f*noise*Math.sin(i**5); // noise
5952
+ cos(modulation*modOffset++); // modulation
5953
+ t += f + f*noise*sin(i**5); // noise
5689
5954
 
5690
5955
  if (jump && ++jump > pitchJumpTime) // pitch jump
5691
5956
  {
@@ -5751,23 +6016,22 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5751
6016
  }
5752
6017
  }
5753
6018
 
5754
- /** Return the center of first tile hit, undefined if nothing was hit.
5755
- * 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.
5756
6020
  * @param {Vector2} posStart
5757
6021
  * @param {Vector2} posEnd
5758
6022
  * @param {EngineObject} [object] - An object or undefined for generic test
6023
+ * @param {Vector2} [normal] - Optional normal of the surface hit
5759
6024
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5760
- * @return {Vector2}
6025
+ * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
5761
6026
  * @memberof TileLayers */
5762
- function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
6027
+ function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
5763
6028
  {
5764
6029
  for (const layer of tileCollisionLayers)
5765
6030
  {
5766
6031
  if (!solidOnly || layer.isSolid)
5767
6032
  {
5768
- const hitPos = layer.collisionRaycast(posStart, posEnd, object)
5769
- if (hitPos)
5770
- return hitPos;
6033
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
6034
+ if (hitPos) return hitPos;
5771
6035
  }
5772
6036
  }
5773
6037
  }
@@ -5905,6 +6169,8 @@ class CanvasLayer extends EngineObject
5905
6169
  /** @property {TextureInfo} - Texture info to use for this object rendering */
5906
6170
  const useWebGL = false; // do not use webgl by default
5907
6171
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
6172
+ /** @property {boolean} - True if WebGL texture needs to be refreshed */
6173
+ this.refreshWebGL = false;
5908
6174
 
5909
6175
  // disable physics by default
5910
6176
  this.mass = this.gravityScale = this.friction = this.restitution = 0;
@@ -5938,8 +6204,15 @@ class CanvasLayer extends EngineObject
5938
6204
  * @memberof Draw */
5939
6205
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5940
6206
  {
5941
- // draw the canvas layer as a single tile that uses the whole texture
5942
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
+
6215
+ // draw the canvas layer as a single tile that uses the whole texture
5943
6216
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5944
6217
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5945
6218
  }
@@ -6009,9 +6282,18 @@ class CanvasLayer extends EngineObject
6009
6282
  { this.drawTile(pos, size, undefined, color, angle); }
6010
6283
 
6011
6284
  /** Create or update the WebGL texture for this layer
6012
- * @param {boolean} [enable] - enable WebGL rendering and update the texture */
6013
- 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)
6014
6289
  {
6290
+ if (!immediate && enable && this.textureInfo.hasWebGL())
6291
+ {
6292
+ // refresh the texture when needed
6293
+ this.refreshWebGL = true;
6294
+ return;
6295
+ }
6296
+
6015
6297
  if (enable)
6016
6298
  this.textureInfo.createWebGLTexture();
6017
6299
  else
@@ -6042,7 +6324,7 @@ class TileLayer extends CanvasLayer
6042
6324
  */
6043
6325
  constructor(position, size, tileInfo=tile(), renderOrder=0)
6044
6326
  {
6045
- const canvasSize = size.multiply(tileInfo.size);
6327
+ const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6046
6328
  super(position, size, 0, renderOrder, canvasSize);
6047
6329
 
6048
6330
  // set tile info
@@ -6095,6 +6377,13 @@ class TileLayer extends CanvasLayer
6095
6377
  {
6096
6378
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
6097
6379
 
6380
+ if (this.refreshWebGL)
6381
+ {
6382
+ // update the WebGL texture
6383
+ this.textureInfo.createWebGLTexture();
6384
+ this.refreshWebGL = false;
6385
+ }
6386
+
6098
6387
  // draw the tile layer as a single tile
6099
6388
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6100
6389
  const size = this.drawSize || this.size;
@@ -6131,8 +6420,9 @@ class TileLayer extends CanvasLayer
6131
6420
  drawCanvas = this.canvas;
6132
6421
  drawContext = this.context;
6133
6422
  cameraPos = this.size.scale(.5);
6134
- cameraScale = this.tileInfo.size.x;
6135
- 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);
6136
6426
  if (clear)
6137
6427
  {
6138
6428
  // clear and set size
@@ -6292,66 +6582,44 @@ class TileCollisionLayer extends TileLayer
6292
6582
  // check if the object should collide with this tile
6293
6583
  const tileData = this.collisionData[y*this.size.x+x];
6294
6584
  if (tileData)
6295
- 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)))
6296
6587
  return true;
6297
6588
  }
6298
6589
  return false;
6299
6590
  }
6300
6591
 
6301
- /** Return the center of first tile hit, undefined if nothing was hit.
6302
- * 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.
6303
6593
  * @param {Vector2} posStart
6304
6594
  * @param {Vector2} posEnd
6305
- * @param {EngineObject} [object]
6306
- * @return {Vector2} */
6307
- 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)
6308
6599
  {
6309
6600
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
6310
6601
  ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6311
-
6312
- // transform to local layer space
6313
- const posStartX = posStart.x - this.pos.x;
6314
- const posStartY = posStart.y - this.pos.y;
6315
- const posEndX = posEnd.x - this.pos.x;
6316
- const posEndY = posEnd.y - this.pos.y;
6317
-
6318
- // test if a ray collides with tiles from start to end
6319
- const deltaX = posEndX - posStartX;
6320
- const deltaY = posEndY - posStartY;
6321
- const totalLength = (deltaX**2 + deltaY**2)**.5;
6322
- const unitX = abs(totalLength/deltaX);
6323
- const unitY = abs(totalLength/deltaY);
6324
-
6325
- // setup iteration variables
6326
- const pos = posStart.floor(), signDeltaX = sign(deltaX), signDeltaY = sign(deltaY);
6327
- let xi = unitX * (deltaX < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1) || 0;
6328
- let yi = unitY * (deltaY < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1) || 0;
6329
-
6330
- // use line drawing algorithm to test for collisions
6331
- while (true)
6602
+
6603
+ const localPos = new Vector2;
6604
+ const collisionTest = (pos)=>
6332
6605
  {
6333
6606
  // check for tile collision
6334
- const tileData = this.getCollisionData(pos);
6335
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
6336
- {
6337
- pos.x += .5; pos.y += .5;
6338
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
6339
- debugRaycast && debugPoint(pos, '#ff0');
6340
- return pos;
6341
- }
6342
-
6343
- // check if past the end
6344
- if (xi >= totalLength && yi >= totalLength)
6345
- break;
6346
-
6347
- // get coordinates of next tile to check
6348
- if (xi > yi)
6349
- pos.y += signDeltaY, yi += unitY;
6350
- else
6351
- 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));
6352
6610
  }
6353
-
6354
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
+ }
6355
6623
  }
6356
6624
  }
6357
6625
  /**
@@ -6506,6 +6774,12 @@ class ParticleEmitter extends EngineObject
6506
6774
  this.particleCreateCallback = undefined;
6507
6775
  /** @property {number} - Track particle emit time */
6508
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();
6509
6783
  }
6510
6784
 
6511
6785
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
@@ -6514,6 +6788,18 @@ class ParticleEmitter extends EngineObject
6514
6788
  // only do default update to apply parent transforms
6515
6789
  this.parent && super.update();
6516
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
+
6517
6803
  // update emitter
6518
6804
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
6519
6805
  {
@@ -6571,7 +6857,7 @@ class ParticleEmitter extends EngineObject
6571
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);
6572
6858
  particle.velocity = vec2().setAngle(velocityAngle, speed);
6573
6859
  particle.angleVelocity = angleSpeed;
6574
- if (!this.localSpace)
6860
+ if (!this.localSpace && this.velocityInheritance > 0)
6575
6861
  {
6576
6862
  // apply emitter velocity to particle
6577
6863
  particle.velocity.x += this.velocity.x;
@@ -6669,6 +6955,16 @@ class Particle extends EngineObject
6669
6955
  this.velocity.y *= s;
6670
6956
  }
6671
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
+ }
6672
6968
  }
6673
6969
 
6674
6970
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -6677,7 +6973,7 @@ class Particle extends EngineObject
6677
6973
  // lerp color and size
6678
6974
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6679
6975
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6680
- this.size.x = this.size.y = radius;
6976
+ const size = vec2(radius);
6681
6977
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6682
6978
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6683
6979
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
@@ -6697,7 +6993,7 @@ class Particle extends EngineObject
6697
6993
  {
6698
6994
  // in local space of emitter
6699
6995
  const a = this.localSpaceEmitter.angle;
6700
- const c = Math.cos(a), s = Math.sin(a);
6996
+ const c = cos(a), s = sin(a);
6701
6997
  pos = this.localSpaceEmitter.pos.add(
6702
6998
  new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6703
6999
  angle += this.localSpaceEmitter.angle;
@@ -6713,22 +7009,15 @@ class Particle extends EngineObject
6713
7009
  {
6714
7010
  // stretch in direction of motion
6715
7011
  const trailLength = speed * this.trailScale;
6716
- this.size.y = max(this.size.x, trailLength);
6717
- angle = Math.atan2(direction.x, direction.y);
6718
- 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);
6719
7015
  }
6720
7016
  }
6721
7017
  else
6722
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
7018
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6723
7019
  this.additive && setBlendMode();
6724
- debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
6725
-
6726
- if (p1 === 1)
6727
- {
6728
- // destroy particle when its time runs out
6729
- this.destroyCallback && this.destroyCallback(this);
6730
- this.destroyed = 1;
6731
- }
7020
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
6732
7021
  }
6733
7022
  }
6734
7023
  /**
@@ -6951,7 +7240,7 @@ let glContext;
6951
7240
  let glAntialias = true;
6952
7241
 
6953
7242
  // WebGL internal variables not exposed to documentation
6954
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos;
7243
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
6955
7244
 
6956
7245
  // WebGL internal constants
6957
7246
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6970,7 +7259,11 @@ function glInit()
6970
7259
  // keep set of texture infos so they can be restored if context is lost
6971
7260
  glTextureInfos = new Set;
6972
7261
 
6973
- if (!glEnable || headlessMode) return;
7262
+ if (!glEnable || headlessMode)
7263
+ {
7264
+ glCanBeEnabled = false;
7265
+ return;
7266
+ }
6974
7267
 
6975
7268
  // create the canvas and textures
6976
7269
  glCanvas = document.createElement('canvas');
@@ -6981,10 +7274,11 @@ function glInit()
6981
7274
  console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
6982
7275
  glCanvas = glContext = undefined;
6983
7276
  glEnable = false;
7277
+ glCanBeEnabled = false;
6984
7278
  return;
6985
7279
  }
6986
7280
 
6987
- // create the WebGL canvas
7281
+ // attach the WebGL canvas
6988
7282
  const rootElement = mainCanvas.parentElement;
6989
7283
  rootElement.appendChild(glCanvas);
6990
7284
 
@@ -7007,7 +7301,7 @@ function glInit()
7007
7301
  });
7008
7302
  glCanvas.addEventListener('webglcontextrestored', ()=>
7009
7303
  {
7010
- glEnable = true; // disable WebGL rendering
7304
+ glEnable = true; // re-enable WebGL rendering
7011
7305
  glCanvas.style.display = ''; // show the gl canvas
7012
7306
  LOG('WebGL context restored, reinitializing...');
7013
7307
 
@@ -7159,8 +7453,8 @@ function glPreRender()
7159
7453
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
7160
7454
  const rotatedCam = cameraPos.rotate(-cameraAngle);
7161
7455
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
7162
- const ca = Math.cos(cameraAngle);
7163
- const sa = Math.sin(cameraAngle);
7456
+ const ca = cos(cameraAngle);
7457
+ const sa = sin(cameraAngle);
7164
7458
  const transform = [
7165
7459
  s.x * ca, s.y * sa, 0, 0,
7166
7460
  -s.x * sa, s.y * ca, 0, 0,
@@ -7465,8 +7759,8 @@ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
7465
7759
  // transform the point
7466
7760
  const px = p.x*sx;
7467
7761
  const py = p.y*sy;
7468
- const sa = Math.sin(-angle);
7469
- const ca = Math.cos(-angle);
7762
+ const sa = sin(-angle);
7763
+ const ca = cos(-angle);
7470
7764
  pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
7471
7765
  }
7472
7766
  const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
@@ -10988,6 +11282,7 @@ export
10988
11282
  gamepadDirectionEmulateStick,
10989
11283
  inputWASDEmulateDirection,
10990
11284
  touchGamepadEnable,
11285
+ touchGamepadCenterButton,
10991
11286
  touchGamepadAnalog,
10992
11287
  touchGamepadSize,
10993
11288
  touchGamepadAlpha,
@@ -11031,6 +11326,7 @@ export
11031
11326
  setGamepadDirectionEmulateStick,
11032
11327
  setInputWASDEmulateDirection,
11033
11328
  setTouchGamepadEnable,
11329
+ setTouchGamepadCenterButton,
11034
11330
  setTouchGamepadAnalog,
11035
11331
  setTouchGamepadSize,
11036
11332
  setTouchGamepadAlpha,
@@ -11049,9 +11345,18 @@ export
11049
11345
  // Utilities
11050
11346
  PI,
11051
11347
  abs,
11348
+ floor,
11349
+ ceil,
11350
+ round,
11052
11351
  min,
11053
11352
  max,
11054
11353
  sign,
11354
+ hypot,
11355
+ log2,
11356
+ sin,
11357
+ cos,
11358
+ tan,
11359
+ atan2,
11055
11360
  mod,
11056
11361
  clamp,
11057
11362
  percent,
@@ -11119,6 +11424,8 @@ export
11119
11424
  drawCount,
11120
11425
  screenToWorld,
11121
11426
  worldToScreen,
11427
+ screenToWorldDelta,
11428
+ worldToScreenDelta,
11122
11429
  drawTile,
11123
11430
  drawRect,
11124
11431
  drawRectGradient,