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,6 +1,7 @@
1
+ // LittleJS - MIT License - Copyright 2021 Frank Force
2
+
1
3
  /**
2
- * LittleJS - Release Build
3
- * MIT License - Copyright 2021 Frank Force
4
+ * LittleJS - Release Mode
4
5
  * - This file is used for release builds in place of engineDebug.js
5
6
  * - Debug functionality is disabled to reduce size and increase performance
6
7
  */
@@ -36,6 +37,7 @@ function debugSaveCanvas (){}
36
37
  * - Vector2 - fast, simple, easy 2D vector class
37
38
  * - Color - holds a rgba color with some math functions
38
39
  * - Timer - tracks time automatically
40
+ * - RandomGenerator - seeded random number generator
39
41
  * @namespace Utilities
40
42
  */
41
43
 
@@ -86,25 +88,58 @@ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % di
86
88
  * @param {Number} [max=1]
87
89
  * @return {Number}
88
90
  * @memberof Utilities */
89
- function clamp(value, min=0, max=1)
90
- { return value < min ? min : value > max ? max : value; }
91
+ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
91
92
 
92
- /** Returns what percentage the value is between max and min
93
+ /** Returns what percentage the value is between valueA and valueB
93
94
  * @param {Number} value
94
- * @param {Number} [min=0]
95
- * @param {Number} [max=1]
95
+ * @param {Number} valueA
96
+ * @param {Number} valueB
96
97
  * @return {Number}
97
98
  * @memberof Utilities */
98
- function percent(value, min=0, max=1)
99
- { return max-min ? clamp((value-min) / (max-min)) : 0; }
99
+ function percent(value, valueA, valueB)
100
+ { return valueB-valueA ? clamp((value-valueA) / (valueB-valueA)) : 0; }
100
101
 
101
- /** Linearly interpolates the percent value between max and min
102
+ /** Linearly interpolates between values passed in using percent
102
103
  * @param {Number} percent
103
- * @param {Number} [min=0]
104
- * @param {Number} [max=1]
104
+ * @param {Number} valueA
105
+ * @param {Number} valueB
105
106
  * @return {Number}
106
107
  * @memberof Utilities */
107
- function lerp(percent, min=0, max=1){ return min + clamp(percent) * (max-min); }
108
+ function lerp(percent, valueA, valueB) { return valueA + clamp(percent) * (valueB-valueA); }
109
+
110
+ /** Returns signed wrapped distance between the two values passed in
111
+ * @param {Number} valueA
112
+ * @param {Number} valueB
113
+ * @param {Number} [wrapSize=1]
114
+ * @returns {Number}
115
+ * @memberof Utilities */
116
+ function distanceWrap(valueA, valueB, wrapSize=1)
117
+ { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
118
+
119
+ /** Linearly interpolates between values passed in with wrappping
120
+ * @param {Number} percent
121
+ * @param {Number} valueA
122
+ * @param {Number} valueB
123
+ * @param {Number} [wrapSize=1]
124
+ * @returns {Number}
125
+ * @memberof Utilities */
126
+ function lerpWrap(percent, valueA, valueB, wrapSize=1)
127
+ { return valueB + clamp(percent) * distanceWrap(valueA, valueB, wrapSize); }
128
+
129
+ /** Returns signed wrapped distance between the two angles passed in
130
+ * @param {Number} angleA
131
+ * @param {Number} angleB
132
+ * @returns {Number}
133
+ * @memberof Utilities */
134
+ function distanceAngle(angleA, angleB) { distanceWrap(angleA, angleB, 2*PI); }
135
+
136
+ /** Linearly interpolates between the angles passed in with wrappping
137
+ * @param {Number} percent
138
+ * @param {Number} angleA
139
+ * @param {Number} angleB
140
+ * @returns {Number}
141
+ * @memberof Utilities */
142
+ function lerpAngle(percent, angleA, angleB) { return lerpWrap(percent, angleA, angleB, 2*PI); }
108
143
 
109
144
  /** Applies smoothstep function to the percentage value
110
145
  * @param {Number} percent
@@ -159,17 +194,23 @@ function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
159
194
  function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
160
195
 
161
196
  /** Returns a floored random value the two values passed in
162
- * @param {Number} [valueA=1]
197
+ * @param {Number} valueA
163
198
  * @param {Number} [valueB=0]
164
199
  * @return {Number}
165
200
  * @memberof Random */
166
- function randInt(valueA=1, valueB=0) { return Math.floor(rand(valueA,valueB)); }
201
+ function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
167
202
 
168
203
  /** Randomly returns either -1 or 1
169
204
  * @return {Number}
170
205
  * @memberof Random */
171
206
  function randSign() { return randInt(2) * 2 - 1; }
172
207
 
208
+ /** Returns a random Vector2 with the passed in length
209
+ * @param {Number} [length=1]
210
+ * @return {Vector2}
211
+ * @memberof Random */
212
+ function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
213
+
173
214
  /** Returns a random Vector2 within a circular shape
174
215
  * @param {Number} [radius=1]
175
216
  * @param {Number} [minRadius=0]
@@ -178,12 +219,6 @@ function randSign() { return randInt(2) * 2 - 1; }
178
219
  function randInCircle(radius=1, minRadius=0)
179
220
  { return radius > 0 ? randVector(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
180
221
 
181
- /** Returns a random Vector2 with the passed in length
182
- * @param {Number} [length=1]
183
- * @return {Vector2}
184
- * @memberof Random */
185
- function randVector(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
186
-
187
222
  /** Returns a random color between the two passed in colors, combine components if linear
188
223
  * @param {Color} [colorA=Color()]
189
224
  * @param {Color} [colorB=Color(0,0,0,1)]
@@ -196,29 +231,50 @@ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear)
196
231
  new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
197
232
  }
198
233
 
199
- /** Seed used by the randSeeded function
200
- * @type {Number}
201
- * @default
202
- * @memberof Random */
203
- let randSeed = 1;
204
-
205
- /** Set seed used by the randSeeded function, should not be 0
206
- * @param {Number} seed
207
- * @memberof Random */
208
- function setRandSeed(seed) { randSeed = seed; }
234
+ ///////////////////////////////////////////////////////////////////////////////
209
235
 
210
- /** Returns a seeded random value between the two values passed in using randSeed
211
- * @param {Number} [valueA=1]
212
- * @param {Number} [valueB=0]
213
- * @return {Number}
214
- * @memberof Random */
215
- function randSeeded(valueA=1, valueB=0)
236
+ /**
237
+ * Seeded random number generator
238
+ * - Can be used to create a deterministic random number sequence
239
+ * @example
240
+ * let r = new RandomGenerator(123); // random number generator with seed 123
241
+ * let a = r.rand(); // random value between 0 and 1
242
+ * let b = r.randInt(10); // random integer between 0 and 9
243
+ * r.seed = 123; // reset the seed
244
+ * let c = r.rand(); // the same value as a
245
+ */
246
+ class RandomGenerator
216
247
  {
217
- // xorshift algorithm
218
- randSeed ^= randSeed << 13;
219
- randSeed ^= randSeed >>> 17;
220
- randSeed ^= randSeed << 5;
221
- return valueB + (valueA-valueB) * abs(randSeed % 1e9) / 1e9;
248
+ /** Create a random number generator with the seed passed in
249
+ * @param {Number} seed - Starting seed */
250
+ constructor(seed)
251
+ {
252
+ /** @property {Number} - random seed */
253
+ this.seed = seed;
254
+ }
255
+
256
+ /** Returns a seeded random value between the two values passed in
257
+ * @param {Number} [valueA=1]
258
+ * @param {Number} [valueB=0]
259
+ * @return {Number} */
260
+ float(valueA=1, valueB=0)
261
+ {
262
+ // xorshift algorithm
263
+ this.seed ^= this.seed << 13;
264
+ this.seed ^= this.seed >>> 17;
265
+ this.seed ^= this.seed << 5;
266
+ return valueB + (valueA - valueB) * abs(this.seed % 1e9) / 1e9;
267
+ }
268
+
269
+ /** Returns a floored seeded random value the two values passed in
270
+ * @param {Number} valueA
271
+ * @param {Number} [valueB=0]
272
+ * @return {Number} */
273
+ int(valueA, valueB=0) { return Math.floor(this.rand(valueA, valueB)); }
274
+
275
+ /** Randomly returns either -1 or 1 deterministically
276
+ * @return {Number} */
277
+ sign() { return this.randInt(2) * 2 - 1; }
222
278
  }
223
279
 
224
280
  ///////////////////////////////////////////////////////////////////////////////
@@ -636,6 +692,7 @@ class Timer
636
692
  }
637
693
  /**
638
694
  * LittleJS Engine Settings
695
+ * - All settings for the engine are here
639
696
  * @namespace Settings
640
697
  */
641
698
 
@@ -891,12 +948,12 @@ let medalsPreventUnlock;
891
948
 
892
949
  /**
893
950
  * LittleJS Object Base Object Class
894
- * - Base object class used by the engine
951
+ * - Top level object class used by the engine
895
952
  * - Automatically adds self to object list
896
953
  * - Will be updated and rendered each frame
897
954
  * - Renders as a sprite from a tilesheet by default
898
955
  * - Can have color and addtive color applied
899
- * - 2d Physics and collision system
956
+ * - 2D Physics and collision system
900
957
  * - Sorted by renderOrder
901
958
  * - Objects can have children attached
902
959
  * - Parents are updated before children, and set child transform
@@ -1263,9 +1320,10 @@ class EngineObject
1263
1320
  }
1264
1321
  /**
1265
1322
  * LittleJS Drawing System
1266
- * - Hybrid with both Canvas2D and WebGL available
1323
+ * - Hybrid system with both Canvas2D and WebGL available
1267
1324
  * - Super fast tile sheet rendering with WebGL
1268
1325
  * - Can apply rotation, mirror, color and additive color
1326
+ * - Font rendering system with built in engine font
1269
1327
  * - Many useful utility functions
1270
1328
  *
1271
1329
  * LittleJS uses a hybrid rendering solution with the best of both Canvas2D and WebGL.
@@ -1275,7 +1333,6 @@ class EngineObject
1275
1333
  * overlayCanvas - Another 2D canvas that appears on top of the other 2 canvases.
1276
1334
  *
1277
1335
  * The WebGL rendering system is very fast with some caveats...
1278
- * - The default setup supports only 1 tile sheet, to support more call glCreateTexture and glSetTexture
1279
1336
  * - Switching blend modes (additive) or textures causes another draw call which is expensive in excess
1280
1337
  * - Group additive rendering together using renderOrder to mitigate this issue
1281
1338
  *
@@ -1319,33 +1376,29 @@ const tileImage = new Image;
1319
1376
  let tileImageSize, tileImageFixBleed, drawCount;
1320
1377
 
1321
1378
  /** Convert from screen to world space coordinates
1322
- * - if calling outside of render, you may need to manually set mainCanvasSize
1323
1379
  * @param {Vector2} screenPos
1324
1380
  * @return {Vector2}
1325
1381
  * @memberof Draw */
1326
1382
  function screenToWorld(screenPos)
1327
1383
  {
1328
- ASSERT(mainCanvasSize.x && mainCanvasSize.y, 'mainCanvasSize is invalid');
1329
- return screenPos
1330
- .add(vec2(.5))
1331
- .subtract(mainCanvasSize.scale(.5))
1332
- .multiply(vec2(1/cameraScale,-1/cameraScale))
1333
- .add(cameraPos);
1384
+ return new Vector2
1385
+ (
1386
+ (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale + cameraPos.x,
1387
+ (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale + cameraPos.y
1388
+ );
1334
1389
  }
1335
1390
 
1336
1391
  /** Convert from world to screen space coordinates
1337
- * - if calling outside of render, you may need to manually set mainCanvasSize
1338
1392
  * @param {Vector2} worldPos
1339
1393
  * @return {Vector2}
1340
1394
  * @memberof Draw */
1341
1395
  function worldToScreen(worldPos)
1342
1396
  {
1343
- ASSERT(mainCanvasSize.x && mainCanvasSize.y, 'mainCanvasSize is invalid');
1344
- return worldPos
1345
- .subtract(cameraPos)
1346
- .multiply(vec2(cameraScale,-cameraScale))
1347
- .add(mainCanvasSize.scale(.5))
1348
- .subtract(vec2(.5));
1397
+ return new Vector2
1398
+ (
1399
+ (worldPos.x - cameraPos.x) * cameraScale + mainCanvasSize.x/2 - .5,
1400
+ (worldPos.y - cameraPos.y) * -cameraScale + mainCanvasSize.y/2 - .5
1401
+ );
1349
1402
  }
1350
1403
 
1351
1404
  /** Draw textured tile centered in world space, with color applied if using WebGL
@@ -1358,13 +1411,21 @@ function worldToScreen(worldPos)
1358
1411
  * @param {Boolean} [mirror=0] - If true image is flipped along the Y axis
1359
1412
  * @param {Color} [additiveColor=Color(0,0,0,0)] - Additive color to be applied
1360
1413
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1414
+ * @param {Boolean} [screenSpace=0] - If true the pos and size are in screen space
1361
1415
  * @memberof Draw */
1362
1416
  function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, color=new Color,
1363
- angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable)
1417
+ angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace)
1364
1418
  {
1365
1419
  showWatermark && ++drawCount;
1420
+
1366
1421
  if (glEnable && useWebGL)
1367
1422
  {
1423
+ if (screenSpace)
1424
+ {
1425
+ // convert to world space
1426
+ pos = screenToWorld(pos);
1427
+ size = size.scale(1/cameraScale);
1428
+ }
1368
1429
  if (tileIndex < 0 || !tileImage.width)
1369
1430
  {
1370
1431
  // if negative tile index or image not found, force untextured
@@ -1406,7 +1467,7 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1406
1467
  context.globalAlpha = color.a; // only alpha is supported
1407
1468
  context.drawImage(tileImage, sX, sY, sWidth, sHeight, -.5, -.5, 1, 1);
1408
1469
  }
1409
- });
1470
+ }, undefined, screenSpace);
1410
1471
  }
1411
1472
  }
1412
1473
 
@@ -1416,38 +1477,30 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
1416
1477
  * @param {Color} [color=Color()]
1417
1478
  * @param {Number} [angle=0]
1418
1479
  * @param {Boolean} [useWebGL=glEnable]
1480
+ * @param {Boolean} [screenSpace=0]
1419
1481
  * @memberof Draw */
1420
- function drawRect(pos, size, color, angle, useWebGL)
1421
- {
1422
- drawTile(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1423
- }
1424
-
1425
- /** Draw textured tile centered on pos in screen space
1426
- * @param {Vector2} pos - Center of the tile
1427
- * @param {Vector2} [size=Vector2(1,1)] - Size of the tile
1428
- * @param {Number} [tileIndex=-1] - Tile index to use, negative is untextured
1429
- * @param {Vector2} [tileSize=tileSizeDefault] - Tile size in source pixels
1430
- * @param {Color} [color=Color()]
1431
- * @param {Number} [angle=0]
1432
- * @param {Boolean} [mirror=0]
1433
- * @param {Color} [additiveColor=Color(0,0,0,0)]
1434
- * @param {Boolean} [useWebGL=glEnable]
1435
- * @memberof Draw */
1436
- function drawTileScreenSpace(pos, size=vec2(1), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL)
1437
- {
1438
- drawTile(screenToWorld(pos), size.scale(1/cameraScale), tileIndex, tileSize, color, angle, mirror, additiveColor, useWebGL);
1439
- }
1482
+ function drawRect(pos, size, color, angle, useWebGL, screenSpace)
1483
+ { drawTile(pos, size, -1, tileSizeDefault, color, angle, 0, undefined, useWebGL, screenSpace); }
1440
1484
 
1441
- /** Draw colored rectangle in screen space
1442
- * @param {Vector2} pos
1443
- * @param {Vector2} [size=Vector2(1,1)]
1485
+ /** Draw colored polygon using passed in points
1486
+ * @param {Array} points - Array of Vector2 points
1444
1487
  * @param {Color} [color=Color()]
1445
- * @param {Number} [angle=0]
1446
1488
  * @param {Boolean} [useWebGL=glEnable]
1489
+ * @param {Boolean} [screenSpace=0]
1447
1490
  * @memberof Draw */
1448
- function drawRectScreenSpace(pos, size, color, angle, useWebGL)
1491
+ function drawPoly(points, color=new Color, useWebGL=glEnable, screenSpace)
1449
1492
  {
1450
- drawTileScreenSpace(pos, size, -1, tileSizeDefault, color, angle, 0, 0, useWebGL);
1493
+ if (useWebGL)
1494
+ glDrawPoints(screenSpace ? points.map(screenToWorld) : points, color.rgbaInt());
1495
+ else
1496
+ {
1497
+ // draw using canvas
1498
+ mainContext.fillStyle = color;
1499
+ mainContext.beginPath();
1500
+ for (const point of screenSpace ? points : points.map(worldToScreen))
1501
+ mainContext.lineTo(point.x, point.y);
1502
+ mainContext.fill();
1503
+ }
1451
1504
  }
1452
1505
 
1453
1506
  /** Draw colored line between two points
@@ -1456,12 +1509,13 @@ function drawRectScreenSpace(pos, size, color, angle, useWebGL)
1456
1509
  * @param {Number} [thickness=.1]
1457
1510
  * @param {Color} [color=Color()]
1458
1511
  * @param {Boolean} [useWebGL=glEnable]
1512
+ * @param {Boolean} [screenSpace=0]
1459
1513
  * @memberof Draw */
1460
1514
  function drawLine(posA, posB, thickness=.1, color, useWebGL)
1461
1515
  {
1462
1516
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
1463
1517
  const size = vec2(thickness, halfDelta.length()*2);
1464
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL);
1518
+ drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace);
1465
1519
  }
1466
1520
 
1467
1521
  /** Draw directly to a 2d canvas context in world space
@@ -1471,12 +1525,16 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL)
1471
1525
  * @param {Boolean} mirror
1472
1526
  * @param {Function} drawFunction
1473
1527
  * @param {CanvasRenderingContext2D} [context=mainContext]
1528
+ * @param {Boolean} [screenSpace=0]
1474
1529
  * @memberof Draw */
1475
- function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainContext)
1530
+ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainContext, screenSpace)
1476
1531
  {
1477
- // create canvas transform from world space to screen space
1478
- pos = worldToScreen(pos);
1479
- size = size.scale(cameraScale);
1532
+ if (!screenSpace)
1533
+ {
1534
+ // create canvas transform from world space to screen space
1535
+ pos = worldToScreen(pos);
1536
+ size = size.scale(cameraScale);
1537
+ }
1480
1538
  context.save();
1481
1539
  context.translate(pos.x+.5|0, pos.y+.5|0);
1482
1540
  context.rotate(angle);
@@ -1583,6 +1641,17 @@ class FontImage
1583
1641
  this.context = context;
1584
1642
  }
1585
1643
 
1644
+ /** Draw text in world space using the image font
1645
+ * @param {String} text
1646
+ * @param {Vector2} pos
1647
+ * @param {Number} [scale=.25]
1648
+ * @param {Boolean} [center]
1649
+ */
1650
+ drawText(text, pos, scale=1, center)
1651
+ {
1652
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
1653
+ }
1654
+
1586
1655
  /** Draw text in screen space using the image font
1587
1656
  * @param {String} text
1588
1657
  * @param {Vector2} pos
@@ -1620,17 +1689,6 @@ class FontImage
1620
1689
 
1621
1690
  context.restore();
1622
1691
  }
1623
-
1624
- /** Draw text in world space using the image font
1625
- * @param {String} text
1626
- * @param {Vector2} pos
1627
- * @param {Number} [scale=.25]
1628
- * @param {Boolean} [center]
1629
- */
1630
- drawText(text, pos, scale=1, center)
1631
- {
1632
- this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
1633
- }
1634
1692
  }
1635
1693
 
1636
1694
  ///////////////////////////////////////////////////////////////////////////////
@@ -1656,10 +1714,10 @@ function toggleFullscreen()
1656
1714
 
1657
1715
  /**
1658
1716
  * LittleJS Input System
1659
- * - Tracks key down, pressed, and released
1660
- * - Also tracks mouse buttons, position, and wheel
1661
- * - Supports multiple gamepads
1662
- * - Virtual gamepad for touch devices with touchGamepadSize
1717
+ * - Tracks keyboard down, pressed, and released
1718
+ * - Tracks mouse buttons, position, and wheel
1719
+ * - Tracks multiple analog gamepads
1720
+ * - Virtual gamepad for touch devices
1663
1721
  * @namespace Input
1664
1722
  */
1665
1723
 
@@ -1931,40 +1989,32 @@ if (isTouchDevice)
1931
1989
  let wasTouching, mouseDown = onmousedown, mouseUp = onmouseup;
1932
1990
  onmousedown = onmouseup = ()=> 0;
1933
1991
 
1934
- // setup touch input
1935
- ontouchstart = (e)=>
1992
+ // handle all touch events the same way
1993
+ ontouchstart = ontouchmove = ontouchend = (e)=>
1936
1994
  {
1937
- // fix mobile audio, force it to play a sound on first touch
1938
- zzfx(0);
1939
-
1940
- // handle all touch events the same way
1941
- ontouchstart = ontouchmove = ontouchend = (e)=>
1942
- {
1943
- e.button = 0; // all touches are left click
1995
+ e.button = 0; // all touches are left click
1944
1996
 
1945
- // check if touching and pass to mouse events
1946
- const touching = e.touches.length;
1947
- if (touching)
1948
- {
1949
- // set event pos and pass it along
1950
- e.x = e.touches[0].clientX;
1951
- e.y = e.touches[0].clientY;
1952
- wasTouching ? onmousemove(e) : mouseDown(e);
1953
- }
1954
- else if (wasTouching)
1955
- mouseUp(e);
1997
+ // fix stalled audio on mobile
1998
+ if (soundEnable)
1999
+ audioContext ? audioContext.resume() : zzfx(0);
1956
2000
 
1957
- // set was touching
1958
- wasTouching = touching;
1959
-
1960
- // must return true so the document will get focus
1961
- return true;
2001
+ // check if touching and pass to mouse events
2002
+ const touching = e.touches.length;
2003
+ if (touching)
2004
+ {
2005
+ // set event pos and pass it along
2006
+ e.x = e.touches[0].clientX;
2007
+ e.y = e.touches[0].clientY;
2008
+ wasTouching ? onmousemove(e) : mouseDown(e);
1962
2009
  }
2010
+ else if (wasTouching)
2011
+ mouseUp(e);
1963
2012
 
1964
- // try to create touch game pad
1965
- touchGamepadEnable && touchGamepadCreate();
2013
+ // set was touching
2014
+ wasTouching = touching;
1966
2015
 
1967
- return ontouchstart(e);
2016
+ // must return true so the document will get focus
2017
+ return true;
1968
2018
  }
1969
2019
  }
1970
2020
 
@@ -1975,13 +2025,13 @@ if (isTouchDevice)
1975
2025
  let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
1976
2026
 
1977
2027
  // create the touch gamepad, called automatically by the engine
1978
- function touchGamepadCreate()
2028
+ if (touchGamepadEnable)
1979
2029
  {
1980
2030
  // touch input internal variables
1981
2031
  touchGamepadButtons = [];
1982
2032
  touchGamepadStick = vec2();
1983
2033
 
1984
- let touchHandler = ontouchstart;
2034
+ const touchHandler = ontouchstart;
1985
2035
  ontouchstart = ontouchmove = ontouchend = (e)=>
1986
2036
  {
1987
2037
  // clear touch gamepad input
@@ -2100,12 +2150,12 @@ function touchGamepadRender()
2100
2150
  }
2101
2151
  /**
2102
2152
  * LittleJS Audio System
2103
- * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - Sound Effect Generator
2104
- * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - Music System
2153
+ * - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
2154
+ * - <a href=https://keithclark.github.io/ZzFXM/>ZzFXM Music</a> - ZzFXM Music System
2105
2155
  * - Caches sounds and music for fast playback
2106
2156
  * - Can attenuate and apply stereo panning to sounds
2107
2157
  * - Ability to play mp3, ogg, and wave files
2108
- * - Speech synthesis wrapper functions
2158
+ * - Speech synthesis functions
2109
2159
  * @namespace Audio
2110
2160
  */
2111
2161
 
@@ -2139,12 +2189,15 @@ class Sound
2139
2189
  /** @property {Number} - At what percentage of range should it start tapering off */
2140
2190
  this.taper = taper;
2141
2191
 
2142
- // get randomness from sound parameters
2143
- this.randomness = zzfxSound[1] || 0;
2144
- zzfxSound[1] = 0;
2192
+ /** @property {Number} - How much to randomize frequency each time sound plays */
2193
+ this.randomness = 0;
2145
2194
 
2146
- // generate sound now for fast playback
2147
- this.cachedSamples = zzfxG(...zzfxSound);
2195
+ if (zzfxSound)
2196
+ {
2197
+ // generate zzfx sound now for fast playback
2198
+ this.randomness = zzfxSound[1] || (zzfxSound[1] = 0);
2199
+ this.cachedSamples = zzfxSound && zzfxG(...zzfxSound);
2200
+ }
2148
2201
  }
2149
2202
 
2150
2203
  /** Play the sound
@@ -2156,7 +2209,7 @@ class Sound
2156
2209
  */
2157
2210
  play(pos, volume=1, pitch=1, randomnessScale=1)
2158
2211
  {
2159
- if (!soundEnable) return;
2212
+ if (!soundEnable || !this.cachedSamples) return;
2160
2213
 
2161
2214
  let pan;
2162
2215
  if (pos)
@@ -2189,12 +2242,36 @@ class Sound
2189
2242
  * @return {AudioBufferSourceNode} - The audio, can be used to stop sound later
2190
2243
  */
2191
2244
  playNote(semitoneOffset, pos, volume)
2245
+ { return this.play(pos, volume, 2**(semitoneOffset/12), 0); }
2246
+ }
2247
+
2248
+ /**
2249
+ * Sound Wave Object - Stores a wave sound for later use and can be played positionally
2250
+ */
2251
+ class SoundWave extends Sound
2252
+ {
2253
+ /** Create a sound object and cache the wave file for later use
2254
+ * @param {String} waveFilename - Filename of wave file to load
2255
+ * @param {Number} [randomness=.05] - How much to randomize frequency each time sound plays
2256
+ * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2257
+ * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
2258
+ */
2259
+ constructor(waveFilename, randomness=.05, range, taper)
2192
2260
  {
2261
+ super(0, range, taper);
2262
+ this.randomness = randomness;
2263
+
2193
2264
  if (!soundEnable) return;
2265
+ if (!soundWaveDecoderContext)
2266
+ soundDecoderContext = new AudioContext;
2194
2267
 
2195
- return this.play(pos, volume, 2**(semitoneOffset/12), 0);
2268
+ fetch(waveFilename)
2269
+ .then(response => response.arrayBuffer())
2270
+ .then(arrayBuffer => soundWaveDecoderContext.decodeAudioData(arrayBuffer))
2271
+ .then(audioBuffer => this.cachedSamples = audioBuffer.getChannelData(0));
2196
2272
  }
2197
2273
  }
2274
+ let soundDecoderContext; // audio context used only to decode audio files
2198
2275
 
2199
2276
  /**
2200
2277
  * Music Object - Stores a zzfx music track for later use
@@ -2337,16 +2414,17 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2337
2414
  {
2338
2415
  if (!soundEnable) return;
2339
2416
 
2340
- // create audio context
2417
+ // create audio context if needed
2341
2418
  if (!audioContext)
2342
2419
  audioContext = new AudioContext;
2343
2420
 
2344
- // fix stalled audio
2345
- audioContext.resume();
2346
-
2347
2421
  // prevent sounds from building up if they can't be played
2348
2422
  if (audioContext.state != 'running')
2423
+ {
2424
+ // fix stalled audio
2425
+ audioContext.resume();
2349
2426
  return;
2427
+ }
2350
2428
 
2351
2429
  // create buffer and source
2352
2430
  const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, zzfxR),
@@ -2551,7 +2629,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
2551
2629
 
2552
2630
  // stop if end, different instrument or new note
2553
2631
  stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
2554
- instrument != (patternChannel[0] || 0) || note;
2632
+ instrument != (patternChannel[0] || 0) || note | 0;
2555
2633
 
2556
2634
  // fill buffer with samples for previous beat, most cpu intensive part
2557
2635
  for (j = 0; j < beatLength && notFirstBeat;
@@ -3789,8 +3867,8 @@ function glFlush()
3789
3867
 
3790
3868
  // draw all the sprites in the batch and reset the buffer
3791
3869
  glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
3792
- glPositionData.subarray(0, glBatchCount * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT));
3793
- glContext.drawArrays(gl_TRIANGLES, 0, glBatchCount * gl_VERTICES_PER_QUAD);
3870
+ glPositionData.subarray(0, glBatchCount * gl_INDICIES_PER_VERT));
3871
+ glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount);
3794
3872
  glBatchCount = 0;
3795
3873
  glBatchAdditive = glAdditive;
3796
3874
  }
@@ -3811,39 +3889,75 @@ function glCopyToContext(context, forceDraw)
3811
3889
  }
3812
3890
 
3813
3891
  /** Add a sprite to the gl draw list, used by all gl draw functions
3814
- * @param x
3815
- * @param y
3816
- * @param sizeX
3817
- * @param sizeY
3818
- * @param angle
3819
- * @param uv0X
3820
- * @param uv0Y
3821
- * @param uv1X
3822
- * @param uv1Y
3823
- * @param rgba
3824
- * @param [rgbaAdditive=0]
3892
+ * @param {Number} x
3893
+ * @param {Number} y
3894
+ * @param {Number} sizeX
3895
+ * @param {Number} sizeY
3896
+ * @param {Number} angle
3897
+ * @param {Number} uv0X
3898
+ * @param {Number} uv0Y
3899
+ * @param {Number} uv1X
3900
+ * @param {Number} uv1Y
3901
+ * @param {Number} rgba
3902
+ * @param {Number} [rgbaAdditive=0]
3825
3903
  * @memberof WebGL */
3826
3904
  function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
3827
3905
  {
3828
- // flush if there is no room for more verts or if different blend mode
3829
- if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3906
+ // flush if there is not enough room or if different blend mode
3907
+ const vertCount = 6;
3908
+ if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
3830
3909
  glFlush();
3831
3910
 
3832
3911
  // prepare to create the verts from size and angle
3833
3912
  const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
3834
3913
  const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
3835
-
3836
- // setup 2 triangles to form a quad
3837
- for(let i=6, offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT; i--;)
3914
+ const positionData =
3915
+ [
3916
+ x-cx+sy, y+cy+sx, uv0X, uv0Y,
3917
+ x-cx-sy, y-cy+sx, uv0X, uv1Y,
3918
+ x+cx+sy, y+cy-sx, uv1X, uv0Y,
3919
+ x+cx-sy, y-cy-sx, uv1X, uv1Y,
3920
+ ];
3921
+
3922
+ // setup 2 triangle strip quad
3923
+ for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
3838
3924
  {
3839
- const a = i-4&&i>1, b = i-5&&i-2&&i-1;
3840
- glPositionData[offset++] = x + (a?-cx:cx) + (b?sy:-sy);
3841
- glPositionData[offset++] = y + (b?cy:-cy) + (a?sx:-sx);
3842
- glPositionData[offset++] = a ? uv0X : uv1X;
3843
- glPositionData[offset++] = b ? uv0Y : uv1Y;
3925
+ let j = clamp(i-1, 0, 3)*4; // degenerate tri at ends
3926
+ glPositionData[offset++] = positionData[j++];
3927
+ glPositionData[offset++] = positionData[j++];
3928
+ glPositionData[offset++] = positionData[j++];
3929
+ glPositionData[offset++] = positionData[j++];
3844
3930
  glColorData[offset++] = rgba;
3845
3931
  glColorData[offset++] = rgbaAdditive;
3846
3932
  }
3933
+ glBatchCount += vertCount;
3934
+ }
3935
+
3936
+ /** Add a convex polygon to the gl draw list
3937
+ * @param {Array} points - Array of Vector2 points
3938
+ * @param {Number} rgba - Color of the polygon
3939
+ * @memberof WebGL */
3940
+ function glDrawPoints(points, rgba)
3941
+ {
3942
+ // flush if there is not enough room or if different blend mode
3943
+ const vertCount = points.length + 2;
3944
+ if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
3945
+ glFlush();
3946
+
3947
+ // setup triangle strip from list of points
3948
+ for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
3949
+ {
3950
+ const j = clamp(i-1, 0, vertCount-3); // degenerate tri at ends
3951
+ const h = j>>1;
3952
+ const point = points[j%2? h : vertCount-3-h];
3953
+ glPositionData[offset++] = point.x;
3954
+ glPositionData[offset++] = point.y;
3955
+ glPositionData[offset++] = 0; // uvx
3956
+ glPositionData[offset++] = 0; // uvy
3957
+ glColorData[offset++] = 0; // nothing to tint
3958
+ glColorData[offset++] = rgba; // apply rgba via additive
3959
+ }
3960
+ glBatchCount += vertCount;
3847
3961
  }
3848
3962
 
3849
3963
  ///////////////////////////////////////////////////////////////////////////////
@@ -3937,14 +4051,14 @@ function glRenderPostProcess()
3937
4051
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
3938
4052
  glContext.uniform1f(uniformLocation('iTime'), time);
3939
4053
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
3940
- glContext.drawArrays(gl_TRIANGLES, 0, 3);
4054
+ glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 3);
3941
4055
  }
3942
4056
 
3943
4057
  ///////////////////////////////////////////////////////////////////////////////
3944
4058
  // store gl constants as integers so their name doesn't use space in minifed
3945
4059
  const
3946
4060
  gl_ONE = 1,
3947
- gl_TRIANGLES = 4,
4061
+ gl_TRIANGLE_STRIP = 5,
3948
4062
  gl_SRC_ALPHA = 770,
3949
4063
  gl_ONE_MINUS_SRC_ALPHA = 771,
3950
4064
  gl_BLEND = 3042,
@@ -3975,9 +4089,9 @@ gl_UNPACK_FLIP_Y_WEBGL = 37440,
3975
4089
  // constants for batch rendering
3976
4090
  gl_VERTICES_PER_QUAD = 6,
3977
4091
  gl_INDICIES_PER_VERT = 6,
3978
- gl_MAX_BATCH = 1<<16,
4092
+ gl_MAX_BATCH = 1e5,
3979
4093
  gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2, // vec2 * 2 + (char * 4) * 2
3980
- gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTICES_PER_QUAD * gl_VERTEX_BYTE_STRIDE;
4094
+ gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTEX_BYTE_STRIDE;
3981
4095
  /**
3982
4096
  * LittleJS - The Tiny JavaScript Game Engine That Can!
3983
4097
  * MIT License - Copyright 2021 Frank Force
@@ -4010,7 +4124,7 @@ const engineName = 'LittleJS';
4010
4124
  * @type {String}
4011
4125
  * @default
4012
4126
  * @memberof Engine */
4013
- const engineVersion = '1.6.92';
4127
+ const engineVersion = '1.7.01';
4014
4128
 
4015
4129
  /** Frames per second to update objects
4016
4130
  * @type {Number}
@@ -4108,7 +4222,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4108
4222
  };
4109
4223
 
4110
4224
  // frame time tracking
4111
- let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS;
4225
+ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
4112
4226
 
4113
4227
  // main update loop
4114
4228
  function engineUpdate(frameTimeMS=0)