littlejsengine 1.6.92 → 1.7.1

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.
@@ -1,10 +1,12 @@
1
+ // LittleJS - MIT License - Copyright 2021 Frank Force
2
+
1
3
  /**
2
4
  * LittleJS Debug System
3
- * - Press ~ to show debug overlay with mouse pick
5
+ * - Press Esc to show debug overlay with mouse pick
4
6
  * - Number keys toggle debug functions
5
7
  * - +/- apply time scale
6
8
  * - Debug primitive rendering
7
- * - Save a 2d canvas as an image
9
+ * - Save a 2d canvas as a png image
8
10
  * @namespace Debug
9
11
  */
10
12
 
@@ -138,11 +140,27 @@ function debugClear() { debugPrimitives = []; }
138
140
  /** Save a canvas to disk
139
141
  * @param {HTMLCanvasElement} canvas
140
142
  * @param {String} [filename]
143
+ * @param {String} [type='image/png']
144
+ * @memberof Debug */
145
+ function debugSaveCanvas(canvas, filename=engineName, type='image/png')
146
+ { debugSaveDataURL(canvas.toDataURL(type), filename); }
147
+
148
+ /** Save a text file to disk
149
+ * @param {String} text
150
+ * @param {String} [filename]
151
+ * @param {String} [type='text/plain']
152
+ * @memberof Debug */
153
+ function debugSaveText(text, filename=engineName, type='text/plain')
154
+ { debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
155
+
156
+ /** Save a data url to disk
157
+ * @param {String} dataURL
158
+ * @param {String} filename
141
159
  * @memberof Debug */
142
- function debugSaveCanvas(canvas, filename = engineName + '.png')
160
+ function debugSaveDataURL(dataURL, filename)
143
161
  {
144
- downloadLink.download = 'screenshot.png';
145
- downloadLink.href = canvas.toDataURL('image/png').replace('image/png','image/octet-stream');
162
+ downloadLink.download = filename;
163
+ downloadLink.href = dataURL;
146
164
  downloadLink.click();
147
165
  }
148
166
 
@@ -392,6 +410,7 @@ function debugRender()
392
410
  * - Vector2 - fast, simple, easy 2D vector class
393
411
  * - Color - holds a rgba color with some math functions
394
412
  * - Timer - tracks time automatically
413
+ * - RandomGenerator - seeded random number generator
395
414
  * @namespace Utilities
396
415
  */
397
416
 
@@ -442,25 +461,58 @@ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % di
442
461
  * @param {Number} [max=1]
443
462
  * @return {Number}
444
463
  * @memberof Utilities */
445
- function clamp(value, min=0, max=1)
446
- { return value < min ? min : value > max ? max : value; }
464
+ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
447
465
 
448
- /** Returns what percentage the value is between max and min
466
+ /** Returns what percentage the value is between valueA and valueB
449
467
  * @param {Number} value
450
- * @param {Number} [min=0]
451
- * @param {Number} [max=1]
468
+ * @param {Number} valueA
469
+ * @param {Number} valueB
452
470
  * @return {Number}
453
471
  * @memberof Utilities */
454
- function percent(value, min=0, max=1)
455
- { return max-min ? clamp((value-min) / (max-min)) : 0; }
472
+ function percent(value, valueA, valueB)
473
+ { return valueB-valueA ? clamp((value-valueA) / (valueB-valueA)) : 0; }
456
474
 
457
- /** Linearly interpolates the percent value between max and min
475
+ /** Linearly interpolates between values passed in using percent
458
476
  * @param {Number} percent
459
- * @param {Number} [min=0]
460
- * @param {Number} [max=1]
477
+ * @param {Number} valueA
478
+ * @param {Number} valueB
461
479
  * @return {Number}
462
480
  * @memberof Utilities */
463
- function lerp(percent, min=0, max=1){ return min + clamp(percent) * (max-min); }
481
+ function lerp(percent, valueA, valueB) { return valueA + clamp(percent) * (valueB-valueA); }
482
+
483
+ /** Returns signed wrapped distance between the two values passed in
484
+ * @param {Number} valueA
485
+ * @param {Number} valueB
486
+ * @param {Number} [wrapSize=1]
487
+ * @returns {Number}
488
+ * @memberof Utilities */
489
+ function distanceWrap(valueA, valueB, wrapSize=1)
490
+ { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
491
+
492
+ /** Linearly interpolates between values passed in with wrappping
493
+ * @param {Number} percent
494
+ * @param {Number} valueA
495
+ * @param {Number} valueB
496
+ * @param {Number} [wrapSize=1]
497
+ * @returns {Number}
498
+ * @memberof Utilities */
499
+ function lerpWrap(percent, valueA, valueB, wrapSize=1)
500
+ { return valueB + clamp(percent) * distanceWrap(valueA, valueB, wrapSize); }
501
+
502
+ /** Returns signed wrapped distance between the two angles passed in
503
+ * @param {Number} angleA
504
+ * @param {Number} angleB
505
+ * @returns {Number}
506
+ * @memberof Utilities */
507
+ function distanceAngle(angleA, angleB) { distanceWrap(angleA, angleB, 2*PI); }
508
+
509
+ /** Linearly interpolates between the angles passed in with wrappping
510
+ * @param {Number} percent
511
+ * @param {Number} angleA
512
+ * @param {Number} angleB
513
+ * @returns {Number}
514
+ * @memberof Utilities */
515
+ function lerpAngle(percent, angleA, angleB) { return lerpWrap(percent, angleA, angleB, 2*PI); }
464
516
 
465
517
  /** Applies smoothstep function to the percentage value
466
518
  * @param {Number} percent
@@ -515,17 +567,23 @@ function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
515
567
  function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
516
568
 
517
569
  /** Returns a floored random value the two values passed in
518
- * @param {Number} [valueA=1]
570
+ * @param {Number} valueA
519
571
  * @param {Number} [valueB=0]
520
572
  * @return {Number}
521
573
  * @memberof Random */
522
- function randInt(valueA=1, valueB=0) { return Math.floor(rand(valueA,valueB)); }
574
+ function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
523
575
 
524
576
  /** Randomly returns either -1 or 1
525
577
  * @return {Number}
526
578
  * @memberof Random */
527
579
  function randSign() { return randInt(2) * 2 - 1; }
528
580
 
581
+ /** Returns a random Vector2 with the passed in length
582
+ * @param {Number} [length=1]
583
+ * @return {Vector2}
584
+ * @memberof Random */
585
+ function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
586
+
529
587
  /** Returns a random Vector2 within a circular shape
530
588
  * @param {Number} [radius=1]
531
589
  * @param {Number} [minRadius=0]
@@ -534,12 +592,6 @@ function randSign() { return randInt(2) * 2 - 1; }
534
592
  function randInCircle(radius=1, minRadius=0)
535
593
  { return radius > 0 ? randVector(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
536
594
 
537
- /** Returns a random Vector2 with the passed in length
538
- * @param {Number} [length=1]
539
- * @return {Vector2}
540
- * @memberof Random */
541
- function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
542
-
543
595
  /** Returns a random color between the two passed in colors, combine components if linear
544
596
  * @param {Color} [colorA=Color()]
545
597
  * @param {Color} [colorB=Color(0,0,0,1)]
@@ -552,29 +604,50 @@ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear)
552
604
  new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
553
605
  }
554
606
 
555
- /** Seed used by the randSeeded function
556
- * @type {Number}
557
- * @default
558
- * @memberof Random */
559
- let randSeed = 1;
560
-
561
- /** Set seed used by the randSeeded function, should not be 0
562
- * @param {Number} seed
563
- * @memberof Random */
564
- function setRandSeed(seed) { randSeed = seed; }
607
+ ///////////////////////////////////////////////////////////////////////////////
565
608
 
566
- /** Returns a seeded random value between the two values passed in using randSeed
567
- * @param {Number} [valueA=1]
568
- * @param {Number} [valueB=0]
569
- * @return {Number}
570
- * @memberof Random */
571
- function randSeeded(valueA=1, valueB=0)
609
+ /**
610
+ * Seeded random number generator
611
+ * - Can be used to create a deterministic random number sequence
612
+ * @example
613
+ * let r = new RandomGenerator(123); // random number generator with seed 123
614
+ * let a = r.rand(); // random value between 0 and 1
615
+ * let b = r.randInt(10); // random integer between 0 and 9
616
+ * r.seed = 123; // reset the seed
617
+ * let c = r.rand(); // the same value as a
618
+ */
619
+ class RandomGenerator
572
620
  {
573
- // xorshift algorithm
574
- randSeed ^= randSeed << 13;
575
- randSeed ^= randSeed >>> 17;
576
- randSeed ^= randSeed << 5;
577
- return valueB + (valueA-valueB) * abs(randSeed % 1e9) / 1e9;
621
+ /** Create a random number generator with the seed passed in
622
+ * @param {Number} seed - Starting seed */
623
+ constructor(seed)
624
+ {
625
+ /** @property {Number} - random seed */
626
+ this.seed = seed;
627
+ }
628
+
629
+ /** Returns a seeded random value between the two values passed in
630
+ * @param {Number} [valueA=1]
631
+ * @param {Number} [valueB=0]
632
+ * @return {Number} */
633
+ float(valueA=1, valueB=0)
634
+ {
635
+ // xorshift algorithm
636
+ this.seed ^= this.seed << 13;
637
+ this.seed ^= this.seed >>> 17;
638
+ this.seed ^= this.seed << 5;
639
+ return valueB + (valueA - valueB) * abs(this.seed % 1e9) / 1e9;
640
+ }
641
+
642
+ /** Returns a floored seeded random value the two values passed in
643
+ * @param {Number} valueA
644
+ * @param {Number} [valueB=0]
645
+ * @return {Number} */
646
+ int(valueA, valueB=0) { return Math.floor(this.rand(valueA, valueB)); }
647
+
648
+ /** Randomly returns either -1 or 1 deterministically
649
+ * @return {Number} */
650
+ sign() { return this.randInt(2) * 2 - 1; }
578
651
  }
579
652
 
580
653
  ///////////////////////////////////////////////////////////////////////////////
@@ -992,6 +1065,7 @@ class Timer
992
1065
  }
993
1066
  /**
994
1067
  * LittleJS Engine Settings
1068
+ * - All settings for the engine are here
995
1069
  * @namespace Settings
996
1070
  */
997
1071
 
@@ -1247,12 +1321,12 @@ let medalsPreventUnlock;
1247
1321
 
1248
1322
  /**
1249
1323
  * LittleJS Object Base Object Class
1250
- * - Base object class used by the engine
1324
+ * - Top level object class used by the engine
1251
1325
  * - Automatically adds self to object list
1252
1326
  * - Will be updated and rendered each frame
1253
1327
  * - Renders as a sprite from a tilesheet by default
1254
1328
  * - Can have color and addtive color applied
1255
- * - 2d Physics and collision system
1329
+ * - 2D Physics and collision system
1256
1330
  * - Sorted by renderOrder
1257
1331
  * - Objects can have children attached
1258
1332
  * - Parents are updated before children, and set child transform
@@ -1619,9 +1693,10 @@ class EngineObject
1619
1693
  }
1620
1694
  /**
1621
1695
  * LittleJS Drawing System
1622
- * - Hybrid with both Canvas2D and WebGL available
1696
+ * - Hybrid system with both Canvas2D and WebGL available
1623
1697
  * - Super fast tile sheet rendering with WebGL
1624
1698
  * - Can apply rotation, mirror, color and additive color
1699
+ * - Font rendering system with built in engine font
1625
1700
  * - Many useful utility functions
1626
1701
  *
1627
1702
  * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
@@ -1631,7 +1706,6 @@ class EngineObject
1631
1706
  * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
1632
1707
  *
1633
1708
  * The WebGL rendering system is very fast with some caveats...
1634
- * - The default setup supports only 1 tile sheet, to support more call glCreateTexture and glSetTexture
1635
1709
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
1636
1710
  * - Group additive rendering together using renderOrder to mitigate this issue
1637
1711
  *
@@ -1675,33 +1749,29 @@ const tileImage = new Image;
1675
1749
  let tileImageSize, tileImageFixBleed, drawCount;
1676
1750
 
1677
1751
  /** Convert from screen to world space coordinates
1678
- * - if calling outside of render, you may need to manually set mainCanvasSize
1679
1752
  * @param {Vector2} screenPos
1680
1753
  * @return {Vector2}
1681
1754
  * @memberof Draw */
1682
1755
  function screenToWorld(screenPos)
1683
1756
  {
1684
- ASSERT(mainCanvasSize.x && mainCanvasSize.y, 'mainCanvasSize is invalid');
1685
- return screenPos
1686
- .add(vec2(.5))
1687
- .subtract(mainCanvasSize.scale(.5))
1688
- .multiply(vec2(1/cameraScale,-1/cameraScale))
1689
- .add(cameraPos);
1757
+ return new Vector2
1758
+ (
1759
+ (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale + cameraPos.x,
1760
+ (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale + cameraPos.y
1761
+ );
1690
1762
  }
1691
1763
 
1692
1764
  /** Convert from world to screen space coordinates
1693
- * - if calling outside of render, you may need to manually set mainCanvasSize
1694
1765
  * @param {Vector2} worldPos
1695
1766
  * @return {Vector2}
1696
1767
  * @memberof Draw */
1697
1768
  function worldToScreen(worldPos)
1698
1769
  {
1699
- ASSERT(mainCanvasSize.x && mainCanvasSize.y, 'mainCanvasSize is invalid');
1700
- return worldPos
1701
- .subtract(cameraPos)
1702
- .multiply(vec2(cameraScale,-cameraScale))
1703
- .add(mainCanvasSize.scale(.5))
1704
- .subtract(vec2(.5));
1770
+ return new Vector2
1771
+ (
1772
+ (worldPos.x - cameraPos.x) * cameraScale + mainCanvasSize.x/2 - .5,
1773
+ (worldPos.y - cameraPos.y) * -cameraScale + mainCanvasSize.y/2 - .5
1774
+ );
1705
1775
  }
1706
1776
 
1707
1777
  /** Draw textured tile centered in world space, with color applied if using WebGL
@@ -1714,13 +1784,21 @@ function worldToScreen(worldPos)
1714
1784
  * @param {Boolean} [mirror=0] - If true image is flipped along the Y axis
1715
1785
  * @param {Color} [additiveColor=Color(0,0,0,0)] - Additive color to be applied
1716
1786
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1787
+ * @param {Boolean} [screenSpace=0] - If true the pos and size are in screen space
1717
1788
  * @memberof Draw */
1718
1789
  function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color,
1719
- angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable)
1790
+ angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace)
1720
1791
  {
1721
1792
  showWatermark && ++drawCount;
1793
+
1722
1794
  if (glEnable && useWebGL)
1723
1795
  {
1796
+ if (screenSpace)
1797
+ {
1798
+ // convert to world space
1799
+ pos = screenToWorld(pos);
1800
+ size = size.scale(1/cameraScale);
1801
+ }
1724
1802
  if (tileIndex < 0 || !tileImage.width)
1725
1803
  {
1726
1804
  // if negative tile index or image not found, force untextured
@@ -1762,7 +1840,7 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1762
1840
  context.globalAlpha = color.a; // only alpha is supported
1763
1841
  context.drawImage(tileImage, sX, sY, sWidth, sHeight, -.5, -.5, 1, 1);
1764
1842
  }
1765
- });
1843
+ }, undefined, screenSpace);
1766
1844
  }
1767
1845
  }
1768
1846
 
@@ -1772,38 +1850,30 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1772
1850
  * @param {Color} [color=Color()]
1773
1851
  * @param {Number} [angle=0]
1774
1852
  * @param {Boolean} [useWebGL=glEnable]
1853
+ * @param {Boolean} [screenSpace=0]
1775
1854
  * @memberof Draw */
1776
- function drawRect(pos, size, color, angle, useWebGL)
1777
- {
1778
- drawTile(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1779
- }
1855
+ function drawRect(pos, size, color, angle, useWebGL, screenSpace)
1856
+ { drawTile(pos, size, -1, tileSizeDefault, color, angle, 0, undefined, useWebGL, screenSpace); }
1780
1857
 
1781
- /** Draw textured tile centered on pos in screen space
1782
- * @param {Vector2} pos - Center of the tile
1783
- * @param {Vector2} [size=Vector2(1,1)] - Size of the tile
1784
- * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1785
- * @param {Vector2} [tileSize=tileSizeDefault] - Tile size in source pixels
1858
+ /** Draw colored polygon using passed in points
1859
+ * @param {Array} points - Array of Vector2 points
1786
1860
  * @param {Color} [color=Color()]
1787
- * @param {Number} [angle=0]
1788
- * @param {Boolean} [mirror=0]
1789
- * @param {Color} [additiveColor=Color(0,0,0,0)]
1790
1861
  * @param {Boolean} [useWebGL=glEnable]
1862
+ * @param {Boolean} [screenSpace=0]
1791
1863
  * @memberof Draw */
1792
- function drawTileScreenSpace(pos, size=vec2(1), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL)
1864
+ function drawPoly(points, color=new Color, useWebGL=glEnable, screenSpace)
1793
1865
  {
1794
- drawTile(screenToWorld(pos), size.scale(1/cameraScale), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL);
1795
- }
1796
-
1797
- /** Draw colored rectangle in screen space
1798
- * @param {Vector2} pos
1799
- * @param {Vector2} [size=Vector2(1,1)]
1800
- * @param {Color} [color=Color()]
1801
- * @param {Number} [angle=0]
1802
- * @param {Boolean} [useWebGL=glEnable]
1803
- * @memberof Draw */
1804
- function drawRectScreenSpace(pos, size, color, angle, useWebGL)
1805
- {
1806
- drawTileScreenSpace(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1866
+ if (useWebGL)
1867
+ glDrawPoints(screenSpace ? points.map(screenToWorld) : points, color.rgbaInt());
1868
+ else
1869
+ {
1870
+ // draw using canvas
1871
+ mainContext.fillStyle = color;
1872
+ mainContext.beginPath();
1873
+ for (const point of screenSpace ? points : points.map(worldToScreen))
1874
+ mainContext.lineTo(point.x, point.y);
1875
+ mainContext.fill();
1876
+ }
1807
1877
  }
1808
1878
 
1809
1879
  /** Draw colored line between two points
@@ -1812,12 +1882,13 @@ function drawRectScreenSpace(pos, size, color, angle, useWebGL)
1812
1882
  * @param {Number} [thickness=.1]
1813
1883
  * @param {Color} [color=Color()]
1814
1884
  * @param {Boolean} [useWebGL=glEnable]
1885
+ * @param {Boolean} [screenSpace=0]
1815
1886
  * @memberof Draw */
1816
1887
  function drawLine(posA, posB, thickness=.1, color, useWebGL)
1817
1888
  {
1818
1889
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
1819
1890
  const size = vec2(thickness, halfDelta.length()*2);
1820
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL);
1891
+ drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace);
1821
1892
  }
1822
1893
 
1823
1894
  /** Draw directly to a 2d canvas context in world space
@@ -1827,12 +1898,16 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL)
1827
1898
  * @param {Boolean} mirror
1828
1899
  * @param {Function} drawFunction
1829
1900
  * @param {CanvasRenderingContext2D} [context=mainContext]
1901
+ * @param {Boolean} [screenSpace=0]
1830
1902
  * @memberof Draw */
1831
- function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainContext)
1903
+ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainContext, screenSpace)
1832
1904
  {
1833
- // create canvas transform from world space to screen space
1834
- pos = worldToScreen(pos);
1835
- size = size.scale(cameraScale);
1905
+ if (!screenSpace)
1906
+ {
1907
+ // create canvas transform from world space to screen space
1908
+ pos = worldToScreen(pos);
1909
+ size = size.scale(cameraScale);
1910
+ }
1836
1911
  context.save();
1837
1912
  context.translate(pos.x+.5|0, pos.y+.5|0);
1838
1913
  context.rotate(angle);
@@ -1939,6 +2014,17 @@ class FontImage
1939
2014
  this.context = context;
1940
2015
  }
1941
2016
 
2017
+ /** Draw text in world space using the image font
2018
+ * @param {String} text
2019
+ * @param {Vector2} pos
2020
+ * @param {Number} [scale=.25]
2021
+ * @param {Boolean} [center]
2022
+ */
2023
+ drawText(text, pos, scale=1, center)
2024
+ {
2025
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
2026
+ }
2027
+
1942
2028
  /** Draw text in screen space using the image font
1943
2029
  * @param {String} text
1944
2030
  * @param {Vector2} pos
@@ -1976,17 +2062,6 @@ class FontImage
1976
2062
 
1977
2063
  context.restore();
1978
2064
  }
1979
-
1980
- /** Draw text in world space using the image font
1981
- * @param {String} text
1982
- * @param {Vector2} pos
1983
- * @param {Number} [scale=.25]
1984
- * @param {Boolean} [center]
1985
- */
1986
- drawText(text, pos, scale=1, center)
1987
- {
1988
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
1989
- }
1990
2065
  }
1991
2066
 
1992
2067
  ///////////////////////////////////////////////////////////////////////////////
@@ -2012,10 +2087,10 @@ function toggleFullscreen()
2012
2087
 
2013
2088
  /**
2014
2089
  * LittleJS Input System
2015
- * - Tracks key down, pressed, and released
2016
- * - Also tracks mouse buttons, position, and wheel
2017
- * - Supports multiple gamepads
2018
- * - Virtual gamepad for touch devices with touchGamepadSize
2090
+ * - Tracks keyboard down, pressed, and released
2091
+ * - Tracks mouse buttons, position, and wheel
2092
+ * - Tracks multiple analog gamepads
2093
+ * - Virtual gamepad for touch devices
2019
2094
  * @namespace Input
2020
2095
  */
2021
2096
 
@@ -2287,40 +2362,32 @@ if (isTouchDevice)
2287
2362
  let wasTouching, mouseDown = onmousedown, mouseUp = onmouseup;
2288
2363
  onmousedown = onmouseup = ()=> 0;
2289
2364
 
2290
- // setup touch input
2291
- ontouchstart = (e)=>
2365
+ // handle all touch events the same way
2366
+ ontouchstart = ontouchmove = ontouchend = (e)=>
2292
2367
  {
2293
- // fix mobile audio, force it to play a sound on first touch
2294
- zzfx(0);
2295
-
2296
- // handle all touch events the same way
2297
- ontouchstart = ontouchmove = ontouchend = (e)=>
2298
- {
2299
- e.button = 0; // all touches are left click
2368
+ e.button = 0; // all touches are left click
2300
2369
 
2301
- // check if touching and pass to mouse events
2302
- const touching = e.touches.length;
2303
- if (touching)
2304
- {
2305
- // set event pos and pass it along
2306
- e.x = e.touches[0].clientX;
2307
- e.y = e.touches[0].clientY;
2308
- wasTouching ? onmousemove(e) : mouseDown(e);
2309
- }
2310
- else if (wasTouching)
2311
- mouseUp(e);
2370
+ // fix stalled audio on mobile
2371
+ if (soundEnable)
2372
+ audioContext ? audioContext.resume() : zzfx(0);
2312
2373
 
2313
- // set was touching
2314
- wasTouching = touching;
2315
-
2316
- // must return true so the document will get focus
2317
- return true;
2374
+ // check if touching and pass to mouse events
2375
+ const touching = e.touches.length;
2376
+ if (touching)
2377
+ {
2378
+ // set event pos and pass it along
2379
+ e.x = e.touches[0].clientX;
2380
+ e.y = e.touches[0].clientY;
2381
+ wasTouching ? onmousemove(e) : mouseDown(e);
2318
2382
  }
2383
+ else if (wasTouching)
2384
+ mouseUp(e);
2319
2385
 
2320
- // try to create touch game pad
2321
- touchGamepadEnable && touchGamepadCreate();
2386
+ // set was touching
2387
+ wasTouching = touching;
2322
2388
 
2323
- return ontouchstart(e);
2389
+ // must return true so the document will get focus
2390
+ return true;
2324
2391
  }
2325
2392
  }
2326
2393
 
@@ -2331,13 +2398,13 @@ if (isTouchDevice)
2331
2398
  let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2332
2399
 
2333
2400
  // create the touch gamepad, called automatically by the engine
2334
- function touchGamepadCreate()
2401
+ if (touchGamepadEnable)
2335
2402
  {
2336
2403
  // touch input internal variables
2337
2404
  touchGamepadButtons = [];
2338
2405
  touchGamepadStick = vec2();
2339
2406
 
2340
- let touchHandler = ontouchstart;
2407
+ const touchHandler = ontouchstart;
2341
2408
  ontouchstart = ontouchmove = ontouchend = (e)=>
2342
2409
  {
2343
2410
  // clear touch gamepad input
@@ -2456,12 +2523,12 @@ function touchGamepadRender()
2456
2523
  }
2457
2524
  /**
2458
2525
  * LittleJS Audio System
2459
- * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - Sound Effect Generator
2460
- * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - Music System
2526
+ * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
2527
+ * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
2461
2528
  * - Caches sounds and music for fast playback
2462
2529
  * - Can attenuate and apply stereo panning to sounds
2463
2530
  * - Ability to play mp3, ogg, and wave files
2464
- * - Speech synthesis wrapper functions
2531
+ * - Speech synthesis functions
2465
2532
  * @namespace Audio
2466
2533
  */
2467
2534
 
@@ -2495,12 +2562,15 @@ class Sound
2495
2562
  /** @property {Number} - At what percentage of range should it start tapering off */
2496
2563
  this.taper = taper;
2497
2564
 
2498
- // get randomness from sound parameters
2499
- this.randomness = zzfxSound[1] || 0;
2500
- zzfxSound[1] = 0;
2565
+ /** @property {Number} - How much to randomize frequency each time sound plays */
2566
+ this.randomness = 0;
2501
2567
 
2502
- // generate sound now for fast playback
2503
- this.cachedSamples = zzfxG(...zzfxSound);
2568
+ if (zzfxSound)
2569
+ {
2570
+ // generate zzfx sound now for fast playback
2571
+ this.randomness = zzfxSound[1] || (zzfxSound[1] = 0);
2572
+ this.cachedSamples = zzfxSound && zzfxG(...zzfxSound);
2573
+ }
2504
2574
  }
2505
2575
 
2506
2576
  /** Play the sound
@@ -2512,7 +2582,7 @@ class Sound
2512
2582
  */
2513
2583
  play(pos, volume=1, pitch=1, randomnessScale=1)
2514
2584
  {
2515
- if (!soundEnable) return;
2585
+ if (!soundEnable || !this.cachedSamples) return;
2516
2586
 
2517
2587
  let pan;
2518
2588
  if (pos)
@@ -2545,12 +2615,36 @@ class Sound
2545
2615
  * @return {AudioBufferSourceNode} - The audio, can be used to stop sound later
2546
2616
  */
2547
2617
  playNote(semitoneOffset, pos, volume)
2618
+ { return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
2619
+ }
2620
+
2621
+ /**
2622
+ * Sound Wave Object - Stores a wave sound for later use and can be played positionally
2623
+ */
2624
+ class SoundWave extends Sound
2625
+ {
2626
+ /** Create a sound object and cache the wave file for later use
2627
+ * @param {String} waveFilename - Filename of wave file to load
2628
+ * @param {Number} [randomness=.05] - How much to randomize frequency each time sound plays
2629
+ * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2630
+ * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
2631
+ */
2632
+ constructor(waveFilename, randomness=.05, range, taper)
2548
2633
  {
2634
+ super(0, range, taper);
2635
+ this.randomness = randomness;
2636
+
2549
2637
  if (!soundEnable) return;
2638
+ if (!soundWaveDecoderContext)
2639
+ soundDecoderContext = new AudioContext;
2550
2640
 
2551
- return this.play(pos, volume, 2**(semitoneOffset/12), 0);
2641
+ fetch(waveFilename)
2642
+ .then(response => response.arrayBuffer())
2643
+ .then(arrayBuffer => soundWaveDecoderContext.decodeAudioData(arrayBuffer))
2644
+ .then(audioBuffer => this.cachedSamples = audioBuffer.getChannelData(0));
2552
2645
  }
2553
2646
  }
2647
+ let soundDecoderContext; // audio context used only to decode audio files
2554
2648
 
2555
2649
  /**
2556
2650
  * Music Object - Stores a zzfx music track for later use
@@ -2693,16 +2787,17 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2693
2787
  {
2694
2788
  if (!soundEnable) return;
2695
2789
 
2696
- // create audio context
2790
+ // create audio context if needed
2697
2791
  if (!audioContext)
2698
2792
  audioContext = new AudioContext;
2699
2793
 
2700
- // fix stalled audio
2701
- audioContext.resume();
2702
-
2703
2794
  // prevent sounds from building up if they can't be played
2704
2795
  if (audioContext.state != 'running')
2796
+ {
2797
+ // fix stalled audio
2798
+ audioContext.resume();
2705
2799
  return;
2800
+ }
2706
2801
 
2707
2802
  // create buffer and source
2708
2803
  const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, zzfxR),
@@ -2907,7 +3002,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2907
3002
 
2908
3003
  // stop if end, different instrument or new note
2909
3004
  stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
2910
- instrument != (patternChannel[0] || 0) || note;
3005
+ instrument != (patternChannel[0] || 0) || note | 0;
2911
3006
 
2912
3007
  // fill buffer with samples for previous beat, most cpu intensive part
2913
3008
  for (j = 0; j < beatLength && notFirstBeat;
@@ -4145,8 +4240,8 @@ function glFlush()
4145
4240
 
4146
4241
  // draw all the sprites in the batch and reset the buffer
4147
4242
  glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
4148
- glPositionData.subarray(0, glBatchCount * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT));
4149
- glContext.drawArrays(gl_TRIANGLES, 0, glBatchCount * gl_VERTICES_PER_QUAD);
4243
+ glPositionData.subarray(0, glBatchCount * gl_INDICIES_PER_VERT));
4244
+ glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount);
4150
4245
  glBatchCount = 0;
4151
4246
  glBatchAdditive = glAdditive;
4152
4247
  }
@@ -4167,39 +4262,75 @@ function glCopyToContext(context, forceDraw)
4167
4262
  }
4168
4263
 
4169
4264
  /** Add a sprite to the gl draw list, used by all gl draw functions
4170
- * @param x
4171
- * @param y
4172
- * @param sizeX
4173
- * @param sizeY
4174
- * @param angle
4175
- * @param uv0X
4176
- * @param uv0Y
4177
- * @param uv1X
4178
- * @param uv1Y
4179
- * @param rgba
4180
- * @param [rgbaAdditive=0]
4265
+ * @param {Number} x
4266
+ * @param {Number} y
4267
+ * @param {Number} sizeX
4268
+ * @param {Number} sizeY
4269
+ * @param {Number} angle
4270
+ * @param {Number} uv0X
4271
+ * @param {Number} uv0Y
4272
+ * @param {Number} uv1X
4273
+ * @param {Number} uv1Y
4274
+ * @param {Number} rgba
4275
+ * @param {Number} [rgbaAdditive=0]
4181
4276
  * @memberof WebGL */
4182
4277
  function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
4183
4278
  {
4184
- // flush if there is no room for more verts or if different blend mode
4185
- if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
4279
+ // flush if there is not enough room or if different blend mode
4280
+ const vertCount = 6;
4281
+ if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
4186
4282
  glFlush();
4187
4283
 
4188
4284
  // prepare to create the verts from size and angle
4189
4285
  const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
4190
4286
  const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
4191
-
4192
- // setup 2 triangles to form a quad
4193
- for(let i=6, offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT; i--;)
4287
+ const positionData =
4288
+ [
4289
+ x-cx+sy, y+cy+sx, uv0X, uv0Y,
4290
+ x-cx-sy, y-cy+sx, uv0X, uv1Y,
4291
+ x+cx+sy, y+cy-sx, uv1X, uv0Y,
4292
+ x+cx-sy, y-cy-sx, uv1X, uv1Y,
4293
+ ];
4294
+
4295
+ // setup 2 triangle strip quad
4296
+ for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
4194
4297
  {
4195
- const a = i-4&&i>1, b = i-5&&i-2&&i-1;
4196
- glPositionData[offset++] = x + (a?-cx:cx) + (b?sy:-sy);
4197
- glPositionData[offset++] = y + (b?cy:-cy) + (a?sx:-sx);
4198
- glPositionData[offset++] = a ? uv0X : uv1X;
4199
- glPositionData[offset++] = b ? uv0Y : uv1Y;
4298
+ let j = clamp(i-1, 0, 3)*4; // degenerate tri at ends
4299
+ glPositionData[offset++] = positionData[j++];
4300
+ glPositionData[offset++] = positionData[j++];
4301
+ glPositionData[offset++] = positionData[j++];
4302
+ glPositionData[offset++] = positionData[j++];
4200
4303
  glColorData[offset++] = rgba;
4201
4304
  glColorData[offset++] = rgbaAdditive;
4202
4305
  }
4306
+ glBatchCount += vertCount;
4307
+ }
4308
+
4309
+ /** Add a convex polygon to the gl draw list
4310
+ * @param {Array} points - Array of Vector2 points
4311
+ * @param {Number} rgba - Color of the polygon
4312
+ * @memberof WebGL */
4313
+ function glDrawPoints(points, rgba)
4314
+ {
4315
+ // flush if there is not enough room or if different blend mode
4316
+ const vertCount = points.length + 2;
4317
+ if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
4318
+ glFlush();
4319
+
4320
+ // setup triangle strip from list of points
4321
+ for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
4322
+ {
4323
+ const j = clamp(i-1, 0, vertCount-3); // degenerate tri at ends
4324
+ const h = j>>1;
4325
+ const point = points[j%2? h : vertCount-3-h];
4326
+ glPositionData[offset++] = point.x;
4327
+ glPositionData[offset++] = point.y;
4328
+ glPositionData[offset++] = 0; // uvx
4329
+ glPositionData[offset++] = 0; // uvy
4330
+ glColorData[offset++] = 0; // nothing to tint
4331
+ glColorData[offset++] = rgba; // apply rgba via additive
4332
+ }
4333
+ glBatchCount += vertCount;
4203
4334
  }
4204
4335
 
4205
4336
  ///////////////////////////////////////////////////////////////////////////////
@@ -4293,14 +4424,14 @@ function glRenderPostProcess()
4293
4424
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
4294
4425
  glContext.uniform1f(uniformLocation('iTime'), time);
4295
4426
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
4296
- glContext.drawArrays(gl_TRIANGLES, 0, 3);
4427
+ glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 3);
4297
4428
  }
4298
4429
 
4299
4430
  ///////////////////////////////////////////////////////////////////////////////
4300
4431
  // store gl constants as integers so their name doesn't use space in minifed
4301
4432
  const
4302
4433
  gl_ONE = 1,
4303
- gl_TRIANGLES = 4,
4434
+ gl_TRIANGLE_STRIP = 5,
4304
4435
  gl_SRC_ALPHA = 770,
4305
4436
  gl_ONE_MINUS_SRC_ALPHA = 771,
4306
4437
  gl_BLEND = 3042,
@@ -4331,9 +4462,9 @@ gl_UNPACK_FLIP_Y_WEBGL = 37440,
4331
4462
  // constants for batch rendering
4332
4463
  gl_VERTICES_PER_QUAD = 6,
4333
4464
  gl_INDICIES_PER_VERT = 6,
4334
- gl_MAX_BATCH = 1<<16,
4465
+ gl_MAX_BATCH = 1e5,
4335
4466
  gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2, // vec2 * 2 + (char * 4) * 2
4336
- gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE;
4467
+ gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTEX_BYTE_STRIDE;
4337
4468
  /**
4338
4469
  * LittleJS - The Tiny JavaScript Game Engine That Can!
4339
4470
  * MIT License - Copyright 2021 Frank Force
@@ -4366,7 +4497,7 @@ const engineName = 'LittleJS';
4366
4497
  * @type {String}
4367
4498
  * @default
4368
4499
  * @memberof Engine */
4369
- const engineVersion = '1.6.92';
4500
+ const engineVersion = '1.7.01';
4370
4501
 
4371
4502
  /** Frames per second to update objects
4372
4503
  * @type {Number}
@@ -4464,7 +4595,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4464
4595
  };
4465
4596
 
4466
4597
  // frame time tracking
4467
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS;
4598
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
4468
4599
 
4469
4600
  // main update loop
4470
4601
  function engineUpdate(frameTimeMS=0)
@@ -4659,7 +4790,7 @@ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObject
4659
4790
 
4660
4791
  /**
4661
4792
  * LittleJS Module Export
4662
- * - Export engine as a module with extra functions where necessary
4793
+ * - Export engine as a module with functions where necessary
4663
4794
  */
4664
4795
 
4665
4796
  /** Set position of camera in world space
@@ -4954,6 +5085,10 @@ export {
4954
5085
  mod,
4955
5086
  clamp,
4956
5087
  percent,
5088
+ distanceWrap,
5089
+ lerpWrap,
5090
+ distanceAngle,
5091
+ lerpAngle,
4957
5092
  lerp,
4958
5093
  smoothStep,
4959
5094
  nearestPowerOfTwo,
@@ -4968,11 +5103,9 @@ export {
4968
5103
  randInCircle,
4969
5104
  randVector,
4970
5105
  randColor,
4971
- randSeed,
4972
- setRandSeed,
4973
- randSeeded,
4974
5106
 
4975
5107
  // Utility Classes
5108
+ RandomGenerator,
4976
5109
  Vector2,
4977
5110
  Color,
4978
5111
  Timer,