littlejsengine 1.9.4 → 1.9.6

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.
@@ -153,7 +153,7 @@ function debugClear() { debugPrimitives = []; }
153
153
  * @param {String} [filename]
154
154
  * @param {String} [type]
155
155
  * @memberof Debug */
156
- function debugSaveCanvas(canvas, filename=engineName, type='image/png')
156
+ function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
157
157
  { debugSaveDataURL(canvas.toDataURL(type), filename); }
158
158
 
159
159
  /** Save a text file to disk
@@ -161,7 +161,7 @@ function debugSaveCanvas(canvas, filename=engineName, type='image/png')
161
161
  * @param {String} [filename]
162
162
  * @param {String} [type]
163
163
  * @memberof Debug */
164
- function debugSaveText(text, filename=engineName, type='text/plain')
164
+ function debugSaveText(text, filename='text', type='text/plain')
165
165
  { debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
166
166
 
167
167
  /** Save a data url to disk
@@ -181,8 +181,7 @@ function debugSaveDataURL(dataURL, filename)
181
181
  function debugInit()
182
182
  {
183
183
  // create link for saving screenshots
184
- document.body.appendChild(downloadLink = document.createElement('a'));
185
- downloadLink.style.display = 'none';
184
+ downloadLink = document.createElement('a');
186
185
  }
187
186
 
188
187
  function debugUpdate()
@@ -357,7 +356,7 @@ function debugRender()
357
356
  overlayContext.restore();
358
357
  });
359
358
 
360
- // remove expired pritives
359
+ // remove expired primitives
361
360
  debugPrimitives = debugPrimitives.filter(r=>r.time<0);
362
361
  }
363
362
 
@@ -485,7 +484,7 @@ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? m
485
484
  * @return {Number}
486
485
  * @memberof Utilities */
487
486
  function percent(value, valueA, valueB)
488
- { return valueB-valueA ? clamp((value-valueA) / (valueB-valueA)) : 0; }
487
+ { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
489
488
 
490
489
  /** Linearly interpolates between values passed in using percent
491
490
  * @param {Number} percent
@@ -542,16 +541,57 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
542
541
  function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
543
542
 
544
543
  /** Returns true if two axis aligned bounding boxes are overlapping
545
- * @param {Vector2} pointA - Center of box A
546
- * @param {Vector2} sizeA - Size of box A
547
- * @param {Vector2} pointB - Center of box B
548
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
549
- * @return {Boolean} - True if overlapping
544
+ * @param {Vector2} posA - Center of box A
545
+ * @param {Vector2} sizeA - Size of box A
546
+ * @param {Vector2} posB - Center of box B
547
+ * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
548
+ * @return {Boolean} - True if overlapping
550
549
  * @memberof Utilities */
551
- function isOverlapping(pointA, sizeA, pointB, sizeB=vec2())
550
+ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
552
551
  {
553
- return abs(pointA.x - pointB.x)*2 < sizeA.x + sizeB.x
554
- && abs(pointA.y - pointB.y)*2 < sizeA.y + sizeB.y;
552
+ return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
553
+ && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
554
+ }
555
+
556
+ /** Returns true if a line segment is intersecting an axis aligned box
557
+ * @param {Vector2} start - Start of raycast
558
+ * @param {Vector2} end - End of raycast
559
+ * @param {Vector2} pos - Center of box
560
+ * @param {Vector2} size - Size of box
561
+ * @return {Boolean} - True if intersecting
562
+ * @memberof Utilities */
563
+ function isIntersecting(start, end, pos, size)
564
+ {
565
+ // Liang-Barsky algorithm
566
+ const boxMin = pos.subtract(size.scale(.5));
567
+ const boxMax = boxMin.add(size);
568
+ const delta = end.subtract(start);
569
+ const a = start.subtract(boxMin);
570
+ const b = start.subtract(boxMax);
571
+ const p = [-delta.x, delta.x, -delta.y, delta.y];
572
+ const q = [a.x, -b.x, a.y, -b.y];
573
+ let tMin = 0, tMax = 1;
574
+ for (let i = 4; i--;)
575
+ {
576
+ if (p[i])
577
+ {
578
+ const t = q[i] / p[i];
579
+ if (p[i] < 0)
580
+ {
581
+ if (t > tMax) return false;
582
+ tMin = max(t, tMin);
583
+ }
584
+ else
585
+ {
586
+ if (t < tMin) return false;
587
+ tMax = min(t, tMax);
588
+ }
589
+ }
590
+ else if (q[i] < 0)
591
+ return false;
592
+ }
593
+
594
+ return true;
555
595
  }
556
596
 
557
597
  /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
@@ -651,7 +691,7 @@ class RandomGenerator
651
691
  this.seed ^= this.seed << 13;
652
692
  this.seed ^= this.seed >>> 17;
653
693
  this.seed ^= this.seed << 5;
654
- return valueB + (valueA - valueB) * abs(this.seed % 1e9) / 1e9;
694
+ return valueB + (valueA - valueB) * abs(this.seed % 1e8) / 1e8;
655
695
  }
656
696
 
657
697
  /** Returns a floored seeded random value the two values passed in
@@ -662,7 +702,7 @@ class RandomGenerator
662
702
 
663
703
  /** Randomly returns either -1 or 1 deterministically
664
704
  * @return {Number} */
665
- sign() { return this.int(2) * 2 - 1; }
705
+ sign() { return this.float() > .5 ? 1 : -1; }
666
706
  }
667
707
 
668
708
  ///////////////////////////////////////////////////////////////////////////////
@@ -710,6 +750,7 @@ class Vector2
710
750
  * @param {Number} [y] - Y axis location */
711
751
  constructor(x=0, y=0)
712
752
  {
753
+ ASSERT(typeof x === 'number' && typeof y === 'number');
713
754
  /** @property {Number} - X axis location */
714
755
  this.x = x;
715
756
  /** @property {Number} - Y axis location */
@@ -1036,12 +1077,14 @@ class Color
1036
1077
  * @return {Color} */
1037
1078
  setHSLA(h=0, s=0, l=1, a=1)
1038
1079
  {
1080
+ h = mod(h,1);
1081
+ s = clamp(s);
1082
+ l = clamp(l);
1039
1083
  const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
1040
1084
  f = (p, q, t)=>
1041
- (t = ((t%1)+1)%1) < 1/6 ? p+(q-p)*6*t :
1042
- t < 1/2 ? q :
1043
- t < 2/3 ? p+(q-p)*(2/3-t)*6 : p;
1044
-
1085
+ (t = mod(t,1))*6 < 1 ? p+(q-p)*6*t :
1086
+ t*2 < 1 ? q :
1087
+ t*3 < 2 ? p+(q-p)*(4-t*6) : p;
1045
1088
  this.r = f(p, q, h + 1/3);
1046
1089
  this.g = f(p, q, h);
1047
1090
  this.b = f(p, q, h - 1/3);
@@ -1092,13 +1135,12 @@ class Color
1092
1135
  ).clamp();
1093
1136
  }
1094
1137
 
1095
- /** Returns this color expressed as a hex color code
1138
+ /** Returns this color expressed as a rgb color code
1096
1139
  * @param {Boolean} [useAlpha] - if alpha should be included in result
1097
1140
  * @return {String} */
1098
- toString(useAlpha = true)
1099
- {
1100
- const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
1101
- return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1141
+ toString(useAlpha = true)
1142
+ {
1143
+ return `rgb(${this.r*255},${this.g*255},${this.b*255},${useAlpha ? this.a : 0})`;
1102
1144
  }
1103
1145
 
1104
1146
  /** Set this color from a hex code
@@ -1156,11 +1198,11 @@ class Timer
1156
1198
 
1157
1199
  /** Returns true if set and has not elapsed
1158
1200
  * @return {Boolean} */
1159
- active() { return time <= this.time; }
1201
+ active() { return time < this.time; }
1160
1202
 
1161
1203
  /** Returns true if set and elapsed
1162
1204
  * @return {Boolean} */
1163
- elapsed() { return time > this.time; }
1205
+ elapsed() { return time >= this.time; }
1164
1206
 
1165
1207
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1166
1208
  * @return {Number} */
@@ -1235,6 +1277,12 @@ let fontDefault = 'arial';
1235
1277
  * @memberof Settings */
1236
1278
  let showSplashScreen = false;
1237
1279
 
1280
+ /** Disables all rendering, audio, and input for servers
1281
+ * @type {Boolean}
1282
+ * @default
1283
+ * @memberof Settings */
1284
+ let headlessMode = false;
1285
+
1238
1286
  ///////////////////////////////////////////////////////////////////////////////
1239
1287
  // WebGL settings
1240
1288
 
@@ -1263,7 +1311,7 @@ let tileSizeDefault = vec2(16);
1263
1311
  * @type {Number}
1264
1312
  * @default
1265
1313
  * @memberof Settings */
1266
- let tileFixBleedScale = .1;
1314
+ let tileFixBleedScale = .5;
1267
1315
 
1268
1316
  ///////////////////////////////////////////////////////////////////////////////
1269
1317
  // Object settings
@@ -1388,7 +1436,7 @@ let soundEnable = true;
1388
1436
  * @type {Number}
1389
1437
  * @default
1390
1438
  * @memberof Settings */
1391
- let soundVolume = .5;
1439
+ let soundVolume = .3;
1392
1440
 
1393
1441
  /** Default range where sound no longer plays
1394
1442
  * @type {Number}
@@ -1473,6 +1521,11 @@ function setFontDefault(font) { fontDefault = font; }
1473
1521
  * @memberof Settings */
1474
1522
  function setShowSplashScreen(show) { showSplashScreen = show; }
1475
1523
 
1524
+ /** Set to disalbe rendering, audio, and input for servers
1525
+ * @param {Boolean} headless
1526
+ * @memberof Settings */
1527
+ function setHeadlessMode(headless) { headlessMode = headless; }
1528
+
1476
1529
  /** Set if webgl rendering is enabled
1477
1530
  * @param {Boolean} enable
1478
1531
  * @memberof Settings */
@@ -1735,23 +1788,37 @@ class EngineObject
1735
1788
  this.collideSolidObjects = false;
1736
1789
  /** @property {Boolean} - Object collides with and blocks other objects */
1737
1790
  this.isSolid = false;
1791
+ /** @property {Boolean} - Object collides with raycasts */
1792
+ this.collideRaycast = false;
1738
1793
 
1739
1794
  // add to list of objects
1740
1795
  engineObjects.push(this);
1741
1796
  }
1742
1797
 
1743
- /** Update the object transform and physics, called automatically by engine once each frame */
1744
- update()
1798
+ /** Update the object transform, called automatically by engine even when paused */
1799
+ updateTransforms()
1745
1800
  {
1746
1801
  const parent = this.parent;
1747
1802
  if (parent)
1748
1803
  {
1749
1804
  // copy parent pos/angle
1750
- this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
1751
- this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
1752
- return;
1805
+ const mirror = parent.getMirrorSign();
1806
+ this.pos = this.localPos.multiply(vec2(mirror,1)).rotate(-parent.angle).add(parent.pos);
1807
+ this.angle = mirror*this.localAngle + parent.angle;
1753
1808
  }
1754
1809
 
1810
+ // update children
1811
+ for (const child of this.children)
1812
+ child.updateTransforms();
1813
+ }
1814
+
1815
+ /** Update the object physics, called automatically by engine once each frame */
1816
+ update()
1817
+ {
1818
+ // child objects do not have physics
1819
+ if (this.parent)
1820
+ return;
1821
+
1755
1822
  // limit max speed to prevent missing collisions
1756
1823
  this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1757
1824
  this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
@@ -1766,8 +1833,7 @@ class EngineObject
1766
1833
  // physics sanity checks
1767
1834
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1768
1835
  ASSERT(this.damping >= 0 && this.damping <= 1);
1769
-
1770
- if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
1836
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
1771
1837
  return;
1772
1838
 
1773
1839
  const wasMovingDown = this.velocity.y < 0;
@@ -1939,13 +2005,7 @@ class EngineObject
1939
2005
  * @param {Number} tileData - the value of the tile at the position
1940
2006
  * @param {Vector2} pos - tile where the collision occured
1941
2007
  * @return {Boolean} - true if the collision should be resolved */
1942
- collideWithTile(tileData, pos) { return tileData > 0; }
1943
-
1944
- /** Called to check if a tile raycast hit
1945
- * @param {Number} tileData - the value of the tile at the position
1946
- * @param {Vector2} pos - tile where the raycast is
1947
- * @return {Boolean} - true if the raycast should hit */
1948
- collideWithTileRaycast(tileData, pos) { return tileData > 0; }
2008
+ collideWithTile(tileData, pos) { return tileData > 0; }
1949
2009
 
1950
2010
  /** Called to check if a object collision should be resolved
1951
2011
  * @param {EngineObject} object - the object to test against
@@ -1992,16 +2052,18 @@ class EngineObject
1992
2052
  }
1993
2053
 
1994
2054
  /** Set how this object collides
1995
- * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects
1996
- * @param {Boolean} [isSolid] - Does it collide with and block other objects (expensive in large numbers)
1997
- * @param {Boolean} [collideTiles] - Does it collide with the tile collision */
1998
- setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true)
2055
+ * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects?
2056
+ * @param {Boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
2057
+ * @param {Boolean} [collideTiles] - Does it collide with the tile collision?
2058
+ * @param {Boolean} [collideRaycast] - Does it collide with raycasts? */
2059
+ setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true, collideRaycast=true)
1999
2060
  {
2000
2061
  ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
2001
2062
 
2002
2063
  this.collideSolidObjects = collideSolidObjects;
2003
2064
  this.isSolid = isSolid;
2004
2065
  this.collideTiles = collideTiles;
2066
+ this.collideRaycast = collideRaycast;
2005
2067
  }
2006
2068
 
2007
2069
  /** Returns string containg info about this object for debugging
@@ -2101,6 +2163,9 @@ let drawCount;
2101
2163
  */
2102
2164
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
2103
2165
  {
2166
+ if (headlessMode)
2167
+ return new TileInfo;
2168
+
2104
2169
  // if size is a number, make it a vector
2105
2170
  if (typeof size === 'number')
2106
2171
  {
@@ -2134,9 +2199,9 @@ class TileInfo
2134
2199
  constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
2135
2200
  {
2136
2201
  /** @property {Vector2} - Top left corner of tile in pixels */
2137
- this.pos = pos;
2202
+ this.pos = pos.copy();
2138
2203
  /** @property {Vector2} - Size of tile in pixels */
2139
- this.size = size;
2204
+ this.size = size.copy();
2140
2205
  /** @property {Number} - Texture index to use */
2141
2206
  this.textureIndex = textureIndex;
2142
2207
  }
@@ -2228,7 +2293,7 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
2228
2293
  * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
2229
2294
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2230
2295
  * @param {Boolean} [screenSpace=false] - If true the pos and size are in screen space
2231
- * @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2296
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2232
2297
  * @memberof Draw */
2233
2298
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2234
2299
  angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
@@ -2301,7 +2366,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2301
2366
  * @param {Number} [angle]
2302
2367
  * @param {Boolean} [useWebGL=glEnable]
2303
2368
  * @param {Boolean} [screenSpace=false]
2304
- * @param {CanvasRenderingContext2D} [context]
2369
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2305
2370
  * @memberof Draw */
2306
2371
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2307
2372
  {
@@ -2315,7 +2380,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2315
2380
  * @param {Color} [color=(1,1,1,1)]
2316
2381
  * @param {Boolean} [useWebGL=glEnable]
2317
2382
  * @param {Boolean} [screenSpace=false]
2318
- * @param {CanvasRenderingContext2D} [context]
2383
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2319
2384
  * @memberof Draw */
2320
2385
  function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
2321
2386
  {
@@ -2331,7 +2396,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
2331
2396
  * @param {Boolean} mirror
2332
2397
  * @param {Function} drawFunction
2333
2398
  * @param {Boolean} [screenSpace=false]
2334
- * @param {CanvasRenderingContext2D} [context=mainContext]
2399
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2335
2400
  * @memberof Draw */
2336
2401
  function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
2337
2402
  {
@@ -2352,7 +2417,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
2352
2417
  /** Enable normal or additive blend mode
2353
2418
  * @param {Boolean} [additive]
2354
2419
  * @param {Boolean} [useWebGL=glEnable]
2355
- * @param {CanvasRenderingContext2D} [context=mainContext]
2420
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2356
2421
  * @memberof Draw */
2357
2422
  function setBlendMode(additive, useWebGL=glEnable, context)
2358
2423
  {
@@ -2377,7 +2442,7 @@ function setBlendMode(additive, useWebGL=glEnable, context)
2377
2442
  * @param {Color} [lineColor=(0,0,0,1)]
2378
2443
  * @param {CanvasTextAlign} [textAlign='center']
2379
2444
  * @param {String} [font=fontDefault]
2380
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2445
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2381
2446
  * @memberof Draw */
2382
2447
  function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
2383
2448
  {
@@ -2394,7 +2459,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
2394
2459
  * @param {Color} [lineColor=(0,0,0,1)]
2395
2460
  * @param {CanvasTextAlign} [textAlign]
2396
2461
  * @param {String} [font=fontDefault]
2397
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2462
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2398
2463
  * @memberof Draw */
2399
2464
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
2400
2465
  {
@@ -2437,7 +2502,7 @@ class FontImage
2437
2502
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
2438
2503
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
2439
2504
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
2440
- * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
2505
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
2441
2506
  */
2442
2507
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
2443
2508
  {
@@ -2645,7 +2710,7 @@ function gamepadWasReleased(button, gamepad=0)
2645
2710
  * @return {Vector2}
2646
2711
  * @memberof Input */
2647
2712
  function gamepadStick(stick, gamepad=0)
2648
- { return stickData[gamepad] ? stickData[gamepad][stick] || vec2() : vec2(); }
2713
+ { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
2649
2714
 
2650
2715
  ///////////////////////////////////////////////////////////////////////////////
2651
2716
  // Input update called by engine
@@ -2656,6 +2721,8 @@ let inputData = [[]];
2656
2721
 
2657
2722
  function inputUpdate()
2658
2723
  {
2724
+ if (headlessMode) return;
2725
+
2659
2726
  // clear input when lost focus (prevent stuck keys)
2660
2727
  isTouchDevice || document.hasFocus() || clearInput();
2661
2728
 
@@ -2668,6 +2735,8 @@ function inputUpdate()
2668
2735
 
2669
2736
  function inputUpdatePost()
2670
2737
  {
2738
+ if (headlessMode) return;
2739
+
2671
2740
  // clear input to prepare for next frame
2672
2741
  for (const deviceInputData of inputData)
2673
2742
  for (const i in deviceInputData)
@@ -2676,9 +2745,12 @@ function inputUpdatePost()
2676
2745
  }
2677
2746
 
2678
2747
  ///////////////////////////////////////////////////////////////////////////////
2679
- // Keyboard event handlers
2748
+ // Input event handlers
2680
2749
 
2750
+ function inputInit()
2681
2751
  {
2752
+ if (headlessMode) return;
2753
+
2682
2754
  onkeydown = (e)=>
2683
2755
  {
2684
2756
  if (debug && e.target != document.body) return;
@@ -2709,21 +2781,29 @@ function inputUpdatePost()
2709
2781
  c == 'KeyA' ? 'ArrowLeft' :
2710
2782
  c == 'KeyD' ? 'ArrowRight' : c : c;
2711
2783
  }
2784
+
2785
+ // mouse event handlers
2786
+ onmousedown = (e)=>
2787
+ {
2788
+ isUsingGamepad = false;
2789
+ inputData[0][e.button] = 3;
2790
+ mousePosScreen = mouseToScreen(e);
2791
+ e.button && e.preventDefault();
2792
+ }
2793
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2794
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2795
+ onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2796
+ oncontextmenu = (e)=> false; // prevent right click menu
2797
+
2798
+ // init touch input
2799
+ if (isTouchDevice)
2800
+ touchInputInit();
2712
2801
  }
2713
2802
 
2714
- ///////////////////////////////////////////////////////////////////////////////
2715
- // Mouse event handlers
2716
-
2717
- onmousedown = (e)=> {isUsingGamepad = false; inputData[0][e.button] = 3; mousePosScreen = mouseToScreen(e); e.button && e.preventDefault();}
2718
- onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2719
- onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2720
- onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2721
- oncontextmenu = (e)=> false; // prevent right click menu
2722
-
2723
2803
  // convert a mouse or touch event position to screen space
2724
2804
  function mouseToScreen(mousePos)
2725
2805
  {
2726
- if (!mainCanvas)
2806
+ if (!mainCanvas || headlessMode)
2727
2807
  return vec2(); // fix bug that can occur if user clicks before page loads
2728
2808
 
2729
2809
  const rect = mainCanvas.getBoundingClientRect();
@@ -2735,7 +2815,7 @@ function mouseToScreen(mousePos)
2735
2815
  // Gamepad input
2736
2816
 
2737
2817
  // gamepad internal variables
2738
- const stickData = [];
2818
+ const gamepadStickData = [];
2739
2819
 
2740
2820
  // gamepads are updated by engine every frame automatically
2741
2821
  function gamepadsUpdate()
@@ -2752,14 +2832,11 @@ function gamepadsUpdate()
2752
2832
  // update touch gamepad if enabled
2753
2833
  if (touchGamepadEnable && isTouchDevice)
2754
2834
  {
2755
- // create the touch gamepad if it doesn't exist
2756
- if (!touchGamepadButtons)
2757
- createTouchGamepad();
2758
-
2835
+ ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
2759
2836
  if (touchGamepadTimer.isSet())
2760
2837
  {
2761
2838
  // read virtual analog stick
2762
- const sticks = stickData[0] || (stickData[0] = []);
2839
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
2763
2840
  sticks[0] = vec2();
2764
2841
  if (touchGamepadAnalog)
2765
2842
  sticks[0] = applyDeadZones(touchGamepadStick);
@@ -2776,7 +2853,8 @@ function gamepadsUpdate()
2776
2853
  for (let i=10; i--;)
2777
2854
  {
2778
2855
  const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
2779
- data[j] = touchGamepadButtons[i] ? gamepadIsDown(j,0) ? 1 : 3 : gamepadIsDown(j,0) ? 4 : 0;
2856
+ const wasDown = gamepadIsDown(j,0);
2857
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
2780
2858
  }
2781
2859
  }
2782
2860
  }
@@ -2796,7 +2874,7 @@ function gamepadsUpdate()
2796
2874
  // get or create gamepad data
2797
2875
  const gamepad = gamepads[i];
2798
2876
  const data = inputData[i+1] || (inputData[i+1] = []);
2799
- const sticks = stickData[i] || (stickData[i] = []);
2877
+ const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
2800
2878
 
2801
2879
  if (gamepad)
2802
2880
  {
@@ -2835,28 +2913,44 @@ function gamepadsUpdate()
2835
2913
  * @param {Number|Array} [pattern] - single value in ms or vibration interval array
2836
2914
  * @memberof Input */
2837
2915
  function vibrate(pattern=100)
2838
- { vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2916
+ { vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2839
2917
 
2840
2918
  /** Cancel any ongoing vibration
2841
2919
  * @memberof Input */
2842
2920
  function vibrateStop() { vibrate(0); }
2843
2921
 
2844
2922
  ///////////////////////////////////////////////////////////////////////////////
2845
- // Touch input
2923
+ // Touch input & virtual on screen gamepad
2846
2924
 
2847
2925
  /** True if a touch device has been detected
2848
2926
  * @memberof Input */
2849
- const isTouchDevice = window.ontouchstart !== undefined;
2927
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
2928
+
2929
+ // touch gamepad internal variables
2930
+ let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2850
2931
 
2851
2932
  // try to enable touch mouse
2852
- if (isTouchDevice)
2933
+ function touchInputInit()
2853
2934
  {
2935
+ // add non passive touch event listeners
2936
+ let handleTouch = handleTouchDefault;
2937
+ if (touchGamepadEnable)
2938
+ {
2939
+ // touch input internal variables
2940
+ handleTouch = handleTouchGamepad;
2941
+ touchGamepadButtons = [];
2942
+ touchGamepadStick = vec2();
2943
+ }
2944
+ document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
2945
+ document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
2946
+ document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
2947
+
2854
2948
  // override mouse events
2855
- let wasTouching;
2856
2949
  onmousedown = onmouseup = ()=> 0;
2857
2950
 
2858
2951
  // handle all touch events the same way
2859
- ontouchstart = ontouchmove = ontouchend = (e)=>
2952
+ let wasTouching;
2953
+ function handleTouchDefault(e)
2860
2954
  {
2861
2955
  // fix stalled audio requiring user interaction
2862
2956
  if (soundEnable && audioContext && audioContext.state != 'running')
@@ -2885,27 +2979,14 @@ if (isTouchDevice)
2885
2979
  // must return true so the document will get focus
2886
2980
  return true;
2887
2981
  }
2888
- }
2889
-
2890
- ///////////////////////////////////////////////////////////////////////////////
2891
- // touch gamepad, virtual on screen gamepad emulator for touch devices
2892
-
2893
- // touch input internal variables
2894
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2895
-
2896
- // create the touch gamepad, called automatically by the engine
2897
- function createTouchGamepad()
2898
- {
2899
- // touch input internal variables
2900
- touchGamepadButtons = [];
2901
- touchGamepadStick = vec2();
2902
2982
 
2903
- const touchHandler = ontouchstart;
2904
- ontouchstart = ontouchmove = ontouchend = (e)=>
2983
+ // special handling for virtual gamepad mode
2984
+ function handleTouchGamepad(e)
2905
2985
  {
2906
2986
  // clear touch gamepad input
2907
2987
  touchGamepadStick = vec2();
2908
2988
  touchGamepadButtons = [];
2989
+ isUsingGamepad = true;
2909
2990
 
2910
2991
  const touching = e.touches.length;
2911
2992
  if (touching)
@@ -2946,9 +3027,8 @@ function createTouchGamepad()
2946
3027
  }
2947
3028
  }
2948
3029
 
2949
- // call default touch handler and set to using gamepad
2950
- touchHandler.bind(window)(e);
2951
- isUsingGamepad = true;
3030
+ // call default touch handler so normal touch events still work
3031
+ handleTouchDefault(e);
2952
3032
 
2953
3033
  // must return true so the document will get focus
2954
3034
  return true;
@@ -2967,32 +3047,33 @@ function touchGamepadRender()
2967
3047
  return;
2968
3048
 
2969
3049
  // setup the canvas
2970
- overlayContext.save();
2971
- overlayContext.globalAlpha = alpha*touchGamepadAlpha;
2972
- overlayContext.strokeStyle = '#fff';
2973
- overlayContext.lineWidth = 3;
3050
+ const context = overlayContext;
3051
+ context.save();
3052
+ context.globalAlpha = alpha*touchGamepadAlpha;
3053
+ context.strokeStyle = '#fff';
3054
+ context.lineWidth = 3;
2974
3055
 
2975
3056
  // draw left analog stick
2976
- overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2977
- overlayContext.beginPath();
3057
+ context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
3058
+ context.beginPath();
2978
3059
 
2979
3060
  const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2980
3061
  if (touchGamepadAnalog) // draw circle shaped gamepad
2981
3062
  {
2982
- overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2983
- overlayContext.fill();
2984
- overlayContext.stroke();
3063
+ context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
3064
+ context.fill();
3065
+ context.stroke();
2985
3066
  }
2986
3067
  else // draw cross shaped gamepad
2987
3068
  {
2988
3069
  for(let i=10; i--;)
2989
3070
  {
2990
3071
  const angle = i*PI/4;
2991
- overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
2992
- i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
2993
- i==1 && overlayContext.fill();
3072
+ context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
3073
+ i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
3074
+ i==1 && context.fill();
2994
3075
  }
2995
- overlayContext.stroke();
3076
+ context.stroke();
2996
3077
  }
2997
3078
 
2998
3079
  // draw right face buttons
@@ -3000,15 +3081,15 @@ function touchGamepadRender()
3000
3081
  for (let i=4; i--;)
3001
3082
  {
3002
3083
  const pos = rightCenter.add(vec2().setDirection(i, touchGamepadSize/2));
3003
- overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
3004
- overlayContext.beginPath();
3005
- overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
3006
- overlayContext.fill();
3007
- overlayContext.stroke();
3084
+ context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
3085
+ context.beginPath();
3086
+ context.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
3087
+ context.fill();
3088
+ context.stroke();
3008
3089
  }
3009
3090
 
3010
3091
  // set canvas back to normal
3011
- overlayContext.restore();
3092
+ context.restore();
3012
3093
  }
3013
3094
  /**
3014
3095
  * LittleJS Audio System
@@ -3043,7 +3124,7 @@ class Sound
3043
3124
  */
3044
3125
  constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
3045
3126
  {
3046
- if (!soundEnable) return;
3127
+ if (!soundEnable || headlessMode) return;
3047
3128
 
3048
3129
  /** @property {Number} - World space max range of sound, will not play if camera is farther away */
3049
3130
  this.range = range;
@@ -3057,8 +3138,8 @@ class Sound
3057
3138
  if (zzfxSound)
3058
3139
  {
3059
3140
  // generate zzfx sound now for fast playback
3060
- this.randomness = zzfxSound[1] || 0;
3061
- zzfxSound[1] = 0; // generate without randomness
3141
+ const defaultRandomness = .05;
3142
+ this.randomness = zzfxSound[1] || defaultRandomness;
3062
3143
  this.sampleChannels = [zzfxG(...zzfxSound)];
3063
3144
  this.sampleRate = zzfxR;
3064
3145
  }
@@ -3074,7 +3155,7 @@ class Sound
3074
3155
  */
3075
3156
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
3076
3157
  {
3077
- if (!soundEnable || !this.sampleChannels) return;
3158
+ if (!soundEnable || !this.sampleChannels || headlessMode) return;
3078
3159
 
3079
3160
  let pan;
3080
3161
  if (pos)
@@ -3157,7 +3238,9 @@ class SoundWave extends Sound
3157
3238
  super(undefined, range, taper);
3158
3239
  this.randomness = randomness;
3159
3240
 
3160
- if (!soundEnable) return;
3241
+ if (!soundEnable || headlessMode) return;
3242
+ if (!audioContext)
3243
+ audioContext = new AudioContext; // create audio context
3161
3244
 
3162
3245
  fetch(filename)
3163
3246
  .then(response => response.arrayBuffer())
@@ -3211,7 +3294,7 @@ class Music extends Sound
3211
3294
  {
3212
3295
  super(undefined);
3213
3296
 
3214
- if (!soundEnable) return;
3297
+ if (!soundEnable || headlessMode) return;
3215
3298
  this.randomness = 0;
3216
3299
  this.sampleChannels = zzfxM(...zzfxMusic);
3217
3300
  this.sampleRate = zzfxR;
@@ -3234,7 +3317,7 @@ class Music extends Sound
3234
3317
  * @memberof Audio */
3235
3318
  function playAudioFile(filename, volume=1, loop=false)
3236
3319
  {
3237
- if (!soundEnable) return;
3320
+ if (!soundEnable || headlessMode) return;
3238
3321
 
3239
3322
  const audio = new Audio(filename);
3240
3323
  audio.volume = soundVolume * volume;
@@ -3253,7 +3336,7 @@ function playAudioFile(filename, volume=1, loop=false)
3253
3336
  * @memberof Audio */
3254
3337
  function speak(text, language='', volume=1, rate=1, pitch=1)
3255
3338
  {
3256
- if (!soundEnable || !speechSynthesis) return;
3339
+ if (!soundEnable || !speechSynthesis || headlessMode) return;
3257
3340
 
3258
3341
  // common languages (not supported by all browsers)
3259
3342
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -3286,7 +3369,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
3286
3369
  /** Audio context used by the engine
3287
3370
  * @type {AudioContext}
3288
3371
  * @memberof Audio */
3289
- let audioContext = new AudioContext;
3372
+ let audioContext;
3290
3373
 
3291
3374
  /** Keep track if audio was suspended when last sound was played
3292
3375
  * @type {Boolean}
@@ -3304,7 +3387,9 @@ let audioSuspended = false;
3304
3387
  * @memberof Audio */
3305
3388
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
3306
3389
  {
3307
- if (!soundEnable) return;
3390
+ if (!soundEnable || headlessMode) return;
3391
+ if (!audioContext)
3392
+ audioContext = new AudioContext; // create audio context
3308
3393
 
3309
3394
  // prevent sounds from building up if they can't be played
3310
3395
  const audioWasSuspended = audioSuspended;
@@ -3350,7 +3435,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3350
3435
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
3351
3436
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3352
3437
  * @memberof Audio */
3353
- function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
3438
+ function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
3354
3439
 
3355
3440
  /** Sample rate used for all ZzFX sounds
3356
3441
  * @default 44100
@@ -3359,7 +3444,7 @@ const zzfxR = 44100;
3359
3444
 
3360
3445
  /** Generate samples for a ZzFX sound
3361
3446
  * @param {Number} [volume] - Volume scale (percent)
3362
- * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
3447
+ * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
3363
3448
  * @param {Number} [frequency] - Frequency of sound (Hz)
3364
3449
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
3365
3450
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -3385,17 +3470,18 @@ const zzfxR = 44100;
3385
3470
  function zzfxG
3386
3471
  (
3387
3472
  // parameters
3388
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3473
+ volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
3389
3474
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3390
3475
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3391
3476
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3392
3477
  )
3393
3478
  {
3479
+ // LJS Note: ZZFX modded so randomness is handled by Sound class
3480
+
3394
3481
  // init parameters
3395
3482
  let PI2 = PI*2, sampleRate = zzfxR,
3396
3483
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3397
- startFrequency = frequency *=
3398
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3484
+ startFrequency = frequency *= PI2 / sampleRate,
3399
3485
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3400
3486
 
3401
3487
  // biquad LP/HP filter
@@ -3417,7 +3503,6 @@ function zzfxG
3417
3503
  pitchJump *= PI2 / sampleRate;
3418
3504
  pitchJumpTime *= sampleRate;
3419
3505
  repeatTime = repeatTime * sampleRate | 0;
3420
- volume *= soundVolume;
3421
3506
 
3422
3507
  // generate waveform
3423
3508
  for(length = attack + decay + sustain + release + delay | 0;
@@ -3654,7 +3739,7 @@ function tileCollisionTest(pos, size=vec2(), object)
3654
3739
  }
3655
3740
  }
3656
3741
 
3657
- /** Return the center of tile if any that is hit (does not return the exact intersection)
3742
+ /** Return the center of first tile hit (does not return the exact intersection)
3658
3743
  * @param {Vector2} posStart
3659
3744
  * @param {Vector2} posEnd
3660
3745
  * @param {EngineObject} [object]
@@ -3763,7 +3848,7 @@ class TileLayer extends EngineObject
3763
3848
 
3764
3849
  /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3765
3850
  this.canvas = document.createElement('canvas');
3766
- /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3851
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3767
3852
  this.context = this.canvas.getContext('2d');
3768
3853
  /** @property {Vector2} - How much to scale this layer when rendered */
3769
3854
  this.scale = scale;
@@ -3774,6 +3859,17 @@ class TileLayer extends EngineObject
3774
3859
  this.data = [];
3775
3860
  for (let j = this.size.area(); j--;)
3776
3861
  this.data.push(new TileLayerData);
3862
+
3863
+ if (headlessMode)
3864
+ {
3865
+ // disable rendering
3866
+ this.redraw = () => {};
3867
+ this.render = () => {};
3868
+ this.redrawStart = () => {};
3869
+ this.redrawEnd = () => {};
3870
+ this.drawTileData = () => {};
3871
+ this.drawCanvas2D = () => {};
3872
+ }
3777
3873
  }
3778
3874
 
3779
3875
  /** Set data at a given position in the array
@@ -3804,7 +3900,7 @@ class TileLayer extends EngineObject
3804
3900
  ASSERT(mainContext != this.context, 'must call redrawEnd() after drawing tiles');
3805
3901
 
3806
3902
  // flush and copy gl canvas because tile canvas does not use webgl
3807
- glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3903
+ !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3808
3904
 
3809
3905
  // draw the entire cached level onto the canvas
3810
3906
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
@@ -3850,15 +3946,18 @@ class TileLayer extends EngineObject
3850
3946
  mainCanvas.height = mainCanvasSize.y;
3851
3947
  }
3852
3948
 
3853
- // begin a new render for the tile canvas
3854
- enginePreRender();
3949
+ // disable smoothing for pixel art
3950
+ this.context.imageSmoothingEnabled = !canvasPixelated;
3951
+
3952
+ // setup gl rendering if enabled
3953
+ glPreRender();
3855
3954
  }
3856
3955
 
3857
3956
  /** Call to end the redraw process */
3858
3957
  redrawEnd()
3859
3958
  {
3860
3959
  ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3861
- glEnable && glCopyToContext(mainContext, true);
3960
+ glCopyToContext(mainContext, true);
3862
3961
  //debugSaveCanvas(this.canvas);
3863
3962
 
3864
3963
  // set stuff back to normal
@@ -4183,20 +4282,21 @@ class ParticleEmitter extends EngineObject
4183
4282
  class Particle extends EngineObject
4184
4283
  {
4185
4284
  /**
4186
- * Create a particle with the given shis.colorStart = undefined;ettings
4187
- * @param {Vector2} position - World space position of the particle
4188
- * @param {TileInfo} [tileInfo] - Tile info to render particles
4189
- * @param {Number} [angle] - Angle to rotate the particle
4190
- * @param {Color} [colorStart] - Color at start of life
4191
- * @param {Color} [colorEnd] - Color at end of life
4192
- * @param {Number} [lifeTime] - How long to live for
4193
- * @param {Number} [sizeStart] - Angle to rotate the particle
4194
- * @param {Number} [sizeEnd] - Angle to rotate the particle
4195
- * @param {Number} [fadeRate] - Angle to rotate the particle
4196
- * @param {Boolean} [additive] - Angle to rotate the particle
4197
- * @param {Number} [trailScale] - If a trail, how long to make it
4285
+ * Create a particle with the passed in settings
4286
+ * Typically this is created automatically by a ParticleEmitter
4287
+ * @param {Vector2} position - World space position of the particle
4288
+ * @param {TileInfo} tileInfo - Tile info to render particles
4289
+ * @param {Number} angle - Angle to rotate the particle
4290
+ * @param {Color} colorStart - Color at start of life
4291
+ * @param {Color} colorEnd - Color at end of life
4292
+ * @param {Number} lifeTime - How long to live for
4293
+ * @param {Number} sizeStart - Size at start of life
4294
+ * @param {Number} sizeEnd - Size at end of life
4295
+ * @param {Number} fadeRate - How quick to fade in/out
4296
+ * @param {Boolean} additive - Does it use additive blend mode
4297
+ * @param {Number} trailScale - If a trail, how long to make it
4198
4298
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
4199
- * @param {Function} [destroyCallback] - Called when particle dies
4299
+ * @param {Function} [destroyCallback] - Callback when particle dies
4200
4300
  */
4201
4301
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
4202
4302
  )
@@ -4605,6 +4705,8 @@ let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData,
4605
4705
  // Initalize WebGL, called automatically by the engine
4606
4706
  function glInit()
4607
4707
  {
4708
+ if (!glEnable || headlessMode) return;
4709
+
4608
4710
  // create the canvas and textures
4609
4711
  glCanvas = document.createElement('canvas');
4610
4712
  glContext = glCanvas.getContext('webgl2');
@@ -4617,11 +4719,11 @@ function glInit()
4617
4719
  '#version 300 es\n' + // specify GLSL ES version
4618
4720
  'precision highp float;'+ // use highp for better accuracy
4619
4721
  'uniform mat4 m;'+ // transform matrix
4620
- 'in vec2 g;'+ // geometry
4621
- 'in vec4 p,u,c,a;'+ // position/size, uvs, color, additiveColor
4622
- 'in float r;'+ // rotation
4623
- 'out vec2 v;'+ // return uv, color, additiveColor
4624
- 'out vec4 d,e;'+ // return uv, color, additiveColor
4722
+ 'in vec2 g;'+ // in: geometry
4723
+ 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
4724
+ 'in float r;'+ // in: rotation
4725
+ 'out vec2 v;'+ // out: uv
4726
+ 'out vec4 d,e;'+ // out: color, additiveColor
4625
4727
  'void main(){'+ // shader entry point
4626
4728
  'vec2 s=(g-.5)*p.zw;'+ // get size offset
4627
4729
  'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
@@ -4631,10 +4733,10 @@ function glInit()
4631
4733
  ,
4632
4734
  '#version 300 es\n' + // specify GLSL ES version
4633
4735
  'precision highp float;'+ // use highp for better accuracy
4634
- 'in vec2 v;'+ // uv
4635
- 'in vec4 d,e;'+ // color, additiveColor
4636
4736
  'uniform sampler2D s;'+ // texture
4637
- 'out vec4 c;'+ // out color
4737
+ 'in vec2 v;'+ // in: uv
4738
+ 'in vec4 d,e;'+ // in: color, additiveColor
4739
+ 'out vec4 c;'+ // out: color
4638
4740
  'void main(){'+ // shader entry point
4639
4741
  'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
4640
4742
  '}' // end of shader
@@ -4656,9 +4758,11 @@ function glInit()
4656
4758
  // Setup render each frame, called automatically by engine
4657
4759
  function glPreRender()
4658
4760
  {
4761
+ if (!glEnable || headlessMode) return;
4762
+
4659
4763
  // clear and set to same size as main canvas
4660
4764
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4661
- glContext.clear(gl_COLOR_BUFFER_BIT);
4765
+ //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4662
4766
 
4663
4767
  // set up the shader
4664
4768
  glContext.useProgram(glShader);
@@ -4692,12 +4796,12 @@ function glPreRender()
4692
4796
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
4693
4797
  const p = vec2(-1).subtract(cameraPos.multiply(s));
4694
4798
  glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
4695
- new Float32Array([
4799
+ [
4696
4800
  s.x, 0, 0, 0,
4697
4801
  0, s.y, 0, 0,
4698
4802
  1, 1, 1, 1,
4699
4803
  p.x, p.y, 0, 0
4700
- ])
4804
+ ]
4701
4805
  );
4702
4806
  }
4703
4807
 
@@ -4708,7 +4812,7 @@ function glPreRender()
4708
4812
  function glSetTexture(texture)
4709
4813
  {
4710
4814
  // must flush cache with the old texture to set a new one
4711
- if (texture == glActiveTexture)
4815
+ if (headlessMode || texture == glActiveTexture)
4712
4816
  return;
4713
4817
 
4714
4818
  glFlush();
@@ -4768,7 +4872,6 @@ function glCreateTexture(image)
4768
4872
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
4769
4873
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
4770
4874
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
4771
-
4772
4875
  return texture;
4773
4876
  }
4774
4877
 
@@ -4792,12 +4895,12 @@ function glFlush()
4792
4895
  }
4793
4896
 
4794
4897
  /** Draw any sprites still in the buffer, copy to main canvas and clear
4795
- * @param {CanvasRenderingContext2D} context
4898
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4796
4899
  * @param {Boolean} [forceDraw]
4797
4900
  * @memberof WebGL */
4798
4901
  function glCopyToContext(context, forceDraw=false)
4799
4902
  {
4800
- if (!glInstanceCount && !forceDraw) return;
4903
+ if (!glEnable || !glInstanceCount && !forceDraw) return;
4801
4904
 
4802
4905
  glFlush();
4803
4906
 
@@ -4854,7 +4957,7 @@ let glPostShader, glPostTexture, glPostIncludeOverlay;
4854
4957
  function glInitPostProcess(shaderCode, includeOverlay=false)
4855
4958
  {
4856
4959
  ASSERT(!glPostShader, 'can only have 1 post effects shader');
4857
-
4960
+ if (headlessMode) return;
4858
4961
  if (!shaderCode) // default shader pass through
4859
4962
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
4860
4963
 
@@ -4893,8 +4996,7 @@ function glInitPostProcess(shaderCode, includeOverlay=false)
4893
4996
  // Render the post processing shader, called automatically by the engine
4894
4997
  function glRenderPostProcess()
4895
4998
  {
4896
- if (!glPostShader)
4897
- return;
4999
+ if (!glPostShader || headlessMode) return;
4898
5000
 
4899
5001
  // prepare to render post process shader
4900
5002
  if (glEnable)
@@ -5000,7 +5102,7 @@ const engineName = 'LittleJS';
5000
5102
  * @type {String}
5001
5103
  * @default
5002
5104
  * @memberof Engine */
5003
- const engineVersion = '1.9.4';
5105
+ const engineVersion = '1.9.6';
5004
5106
 
5005
5107
  /** Frames per second to update
5006
5108
  * @type {Number}
@@ -5067,6 +5169,19 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5067
5169
  {
5068
5170
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
5069
5171
 
5172
+ // Called automatically by engine to setup render system
5173
+ function enginePreRender()
5174
+ {
5175
+ // save canvas size
5176
+ mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5177
+
5178
+ // disable smoothing for pixel art
5179
+ mainContext.imageSmoothingEnabled = !canvasPixelated;
5180
+
5181
+ // setup gl rendering if enabled
5182
+ glPreRender();
5183
+ }
5184
+
5070
5185
  // internal update loop for engine
5071
5186
  function engineUpdate(frameTimeMS=0)
5072
5187
  {
@@ -5083,11 +5198,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5083
5198
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
5084
5199
  if (!debugSpeedUp)
5085
5200
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
5201
+
5086
5202
  updateCanvas();
5087
5203
 
5088
5204
  if (paused)
5089
5205
  {
5090
- // do post update even when paused
5206
+ // update object transforms even when paused
5207
+ for (const o of engineObjects)
5208
+ o.parent || o.updateTransforms();
5091
5209
  inputUpdate();
5092
5210
  debugUpdate();
5093
5211
  gameUpdatePost();
@@ -5099,7 +5217,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5099
5217
  let deltaSmooth = 0;
5100
5218
  if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
5101
5219
  {
5102
- // force an update each frame if time is close enough (not just a fast refresh rate)
5220
+ // force at least one update each frame since it is waiting for refresh
5103
5221
  deltaSmooth = frameTimeBufferMS;
5104
5222
  frameTimeBufferMS = 0;
5105
5223
  }
@@ -5124,34 +5242,37 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5124
5242
  // add the time smoothing back in
5125
5243
  frameTimeBufferMS += deltaSmooth;
5126
5244
  }
5127
-
5128
- // render sort then render while removing destroyed objects
5129
- enginePreRender();
5130
- gameRender();
5131
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
5132
- for (const o of engineObjects)
5133
- o.destroyed || o.render();
5134
- gameRenderPost();
5135
- glRenderPostProcess();
5136
- medalsRender();
5137
- touchGamepadRender();
5138
- debugRender();
5139
- glEnable && glCopyToContext(mainContext);
5140
-
5141
- if (showWatermark)
5245
+
5246
+ if (!headlessMode)
5142
5247
  {
5143
- // update fps
5144
- overlayContext.textAlign = 'right';
5145
- overlayContext.textBaseline = 'top';
5146
- overlayContext.font = '1em monospace';
5147
- overlayContext.fillStyle = '#000';
5148
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
5149
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
5150
- + (glEnable ? ' GL' : ' 2D') ;
5151
- overlayContext.fillText(text, mainCanvas.width-3, 3);
5152
- overlayContext.fillStyle = '#fff';
5153
- overlayContext.fillText(text, mainCanvas.width-2, 2);
5154
- drawCount = 0;
5248
+ // render sort then render while removing destroyed objects
5249
+ enginePreRender();
5250
+ gameRender();
5251
+ engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
5252
+ for (const o of engineObjects)
5253
+ o.destroyed || o.render();
5254
+ gameRenderPost();
5255
+ glRenderPostProcess();
5256
+ medalsRender();
5257
+ touchGamepadRender();
5258
+ debugRender();
5259
+ glCopyToContext(mainContext);
5260
+
5261
+ if (showWatermark)
5262
+ {
5263
+ // update fps
5264
+ overlayContext.textAlign = 'right';
5265
+ overlayContext.textBaseline = 'top';
5266
+ overlayContext.font = '1em monospace';
5267
+ overlayContext.fillStyle = '#000';
5268
+ const text = engineName + ' ' + 'v' + engineVersion + ' / '
5269
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
5270
+ + (glEnable ? ' GL' : ' 2D') ;
5271
+ overlayContext.fillText(text, mainCanvas.width-3, 3);
5272
+ overlayContext.fillStyle = '#fff';
5273
+ overlayContext.fillText(text, mainCanvas.width-2, 2);
5274
+ drawCount = 0;
5275
+ }
5155
5276
  }
5156
5277
 
5157
5278
  requestAnimationFrame(engineUpdate);
@@ -5159,6 +5280,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5159
5280
 
5160
5281
  function updateCanvas()
5161
5282
  {
5283
+ if (headlessMode) return;
5284
+
5162
5285
  if (canvasFixedSize.x)
5163
5286
  {
5164
5287
  // clear canvas and set fixed size
@@ -5186,8 +5309,20 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5186
5309
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5187
5310
  }
5188
5311
 
5312
+ function startEngine()
5313
+ {
5314
+ gameInit();
5315
+ engineUpdate();
5316
+ }
5317
+
5318
+ if (headlessMode)
5319
+ {
5320
+ startEngine();
5321
+ return;
5322
+ }
5323
+
5189
5324
  // setup html
5190
- const styleBody =
5325
+ const styleBody =
5191
5326
  'margin:0;overflow:hidden;' + // fill the window
5192
5327
  'background:#000;' + // set background color
5193
5328
  'touch-action:none;' + // prevent mobile pinch to resize
@@ -5199,8 +5334,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5199
5334
  mainContext = mainCanvas.getContext('2d');
5200
5335
 
5201
5336
  // init stuff and start engine
5337
+ inputInit();
5202
5338
  debugInit();
5203
- glEnable && glInit();
5339
+ glInit();
5204
5340
 
5205
5341
  // create overlay canvas for hud to appear above gl canvas
5206
5342
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
@@ -5224,7 +5360,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5224
5360
  }
5225
5361
  image.src = src;
5226
5362
  })
5227
- )
5363
+ );
5228
5364
 
5229
5365
  // draw splash screen
5230
5366
  showSplashScreen && promises.push(new Promise(resolve =>
@@ -5241,25 +5377,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5241
5377
  }));
5242
5378
 
5243
5379
  // load all of the images
5244
- Promise.all(promises).then(()=>
5245
- {
5246
- // start the engine
5247
- gameInit();
5248
- engineUpdate();
5249
- });
5250
- }
5251
-
5252
- // Called automatically by engine to setup render system
5253
- function enginePreRender()
5254
- {
5255
- // save canvas size
5256
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5257
-
5258
- // disable smoothing for pixel art
5259
- mainContext.imageSmoothingEnabled = !canvasPixelated;
5260
-
5261
- // setup gl rendering if enabled
5262
- glEnable && glPreRender();
5380
+ Promise.all(promises).then(startEngine);
5263
5381
  }
5264
5382
 
5265
5383
  /** Update each engine object, remove destroyed objects, and update time
@@ -5280,7 +5398,14 @@ function engineObjectsUpdate()
5280
5398
  }
5281
5399
  }
5282
5400
  for (const o of engineObjects)
5283
- o.parent || updateObject(o);
5401
+ {
5402
+ // update top level objects
5403
+ if (!o.parent)
5404
+ {
5405
+ updateObject(o);
5406
+ o.updateTransforms();
5407
+ }
5408
+ }
5284
5409
 
5285
5410
  // remove destroyed objects
5286
5411
  engineObjects = engineObjects.filter(o=>!o.destroyed);
@@ -5295,30 +5420,63 @@ function engineObjectsDestroy()
5295
5420
  engineObjects = engineObjects.filter(o=>!o.destroyed);
5296
5421
  }
5297
5422
 
5298
- /** Triggers a callback for each object within a given area
5299
- * @param {Vector2} [pos] - Center of test area
5423
+ /** Collects all object within a given area
5424
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
5300
5425
  * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
5301
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
5302
5426
  * @param {Array} [objects=engineObjects] - List of objects to check
5427
+ * @return {Array} - List of collected objects
5303
5428
  * @memberof Engine */
5304
- function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
5429
+ function engineObjectsCollect(pos, size, objects=engineObjects)
5305
5430
  {
5431
+ const collectedObjects = [];
5306
5432
  if (!pos) // all objects
5307
5433
  {
5308
5434
  for (const o of objects)
5309
- callbackFunction(o);
5435
+ collectedObjects.push(o);
5310
5436
  }
5311
- else if (typeof size === 'object') // bounding box test
5437
+ else if (size instanceof Vector2) // bounding box test
5312
5438
  {
5313
5439
  for (const o of objects)
5314
- isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
5440
+ isOverlapping(pos, size, o.pos, o.size) && collectedObjects.push(o);
5315
5441
  }
5316
5442
  else // circle test
5317
5443
  {
5318
5444
  const sizeSquared = size*size;
5319
5445
  for (const o of objects)
5320
- pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
5446
+ pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
5447
+ }
5448
+ return collectedObjects;
5449
+ }
5450
+
5451
+ /** Triggers a callback for each object within a given area
5452
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
5453
+ * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
5454
+ * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
5455
+ * @param {Array} [objects=engineObjects] - List of objects to check
5456
+ * @memberof Engine */
5457
+ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
5458
+ { engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
5459
+
5460
+ /** Return a list of objects intersecting a ray
5461
+ * @param {Vector2} start
5462
+ * @param {Vector2} end
5463
+ * @param {Array} [objects=engineObjects] - List of objects to check
5464
+ * @return {Array} - List of objects hit
5465
+ * @memberof Engine */
5466
+ function engineObjectsRaycast(start, end, objects=engineObjects)
5467
+ {
5468
+ const hitObjects = [];
5469
+ for (const o of objects)
5470
+ {
5471
+ if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
5472
+ {
5473
+ debugRaycast && debugRect(o.pos, o.size, '#f00');
5474
+ hitObjects.push(o);
5475
+ }
5321
5476
  }
5477
+
5478
+ debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
5479
+ return hitObjects;
5322
5480
  }
5323
5481
 
5324
5482
  ///////////////////////////////////////////////////////////////////////////////
@@ -5406,16 +5564,16 @@ function drawEngineSplashScreen(t)
5406
5564
  rect(37,14,9,6);
5407
5565
 
5408
5566
  // big stack
5409
- rect(50,20,10,-10,color(0,1));
5410
- rect(50,20,6.5,-10,color(0,2));
5411
- rect(50,20,3.5,-10,color(0,3));
5412
- rect(50,20,10,-10);
5413
- circle(55,2,11.4,.5,PI-.5,color(3,3));
5414
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
5415
- circle(55,2,11.4,.5,PI-.5);
5416
- rect(45,7,20,-7,color(0,2));
5417
- rect(45,0,20,3,color(0,3));
5418
- rect(45,0,20,7);
5567
+ rect(50,20,10,-8,color(0,1))
5568
+ rect(50,20,6.5,-8,color(0,2))
5569
+ rect(50,20,3.5,-8,color(0,3))
5570
+ rect(50,20,10,-8)
5571
+ circle(55,2,11.4,.5,PI-.5,color(3,3))
5572
+ circle(55,2,11.4,.5,PI/2,color(3,2),1)
5573
+ circle(55,2,11.4,.5,PI-.5)
5574
+ rect(45,7,20,-7,color(0,2))
5575
+ rect(45,-1,20,4,color(0,3))
5576
+ rect(45,-1,20,8)
5419
5577
 
5420
5578
  // engine
5421
5579
  for (let i=5; i--;)
@@ -5533,6 +5691,7 @@ export {
5533
5691
  canvasPixelated,
5534
5692
  fontDefault,
5535
5693
  showSplashScreen,
5694
+ headlessMode,
5536
5695
  tileSizeDefault,
5537
5696
  tileFixBleedScale,
5538
5697
  enablePhysicsSolver,
@@ -5571,6 +5730,7 @@ export {
5571
5730
  setCanvasPixelated,
5572
5731
  setFontDefault,
5573
5732
  setShowSplashScreen,
5733
+ setHeadlessMode,
5574
5734
  setGlEnable,
5575
5735
  setGlOverlay,
5576
5736
  setTileSizeDefault,