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.
@@ -99,7 +99,7 @@ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? m
99
99
  * @return {Number}
100
100
  * @memberof Utilities */
101
101
  function percent(value, valueA, valueB)
102
- { return valueB-valueA ? clamp((value-valueA) / (valueB-valueA)) : 0; }
102
+ { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
103
103
 
104
104
  /** Linearly interpolates between values passed in using percent
105
105
  * @param {Number} percent
@@ -156,16 +156,57 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
156
156
  function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
157
157
 
158
158
  /** Returns true if two axis aligned bounding boxes are overlapping
159
- * @param {Vector2} pointA - Center of box A
160
- * @param {Vector2} sizeA - Size of box A
161
- * @param {Vector2} pointB - Center of box B
162
- * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
163
- * @return {Boolean} - True if overlapping
159
+ * @param {Vector2} posA - Center of box A
160
+ * @param {Vector2} sizeA - Size of box A
161
+ * @param {Vector2} posB - Center of box B
162
+ * @param {Vector2} [sizeB=(0,0)] - Size of box B, a point if undefined
163
+ * @return {Boolean} - True if overlapping
164
164
  * @memberof Utilities */
165
- function isOverlapping(pointA, sizeA, pointB, sizeB=vec2())
165
+ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
166
166
  {
167
- return abs(pointA.x - pointB.x)*2 < sizeA.x + sizeB.x
168
- && abs(pointA.y - pointB.y)*2 < sizeA.y + sizeB.y;
167
+ return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
168
+ && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
169
+ }
170
+
171
+ /** Returns true if a line segment is intersecting an axis aligned box
172
+ * @param {Vector2} start - Start of raycast
173
+ * @param {Vector2} end - End of raycast
174
+ * @param {Vector2} pos - Center of box
175
+ * @param {Vector2} size - Size of box
176
+ * @return {Boolean} - True if intersecting
177
+ * @memberof Utilities */
178
+ function isIntersecting(start, end, pos, size)
179
+ {
180
+ // Liang-Barsky algorithm
181
+ const boxMin = pos.subtract(size.scale(.5));
182
+ const boxMax = boxMin.add(size);
183
+ const delta = end.subtract(start);
184
+ const a = start.subtract(boxMin);
185
+ const b = start.subtract(boxMax);
186
+ const p = [-delta.x, delta.x, -delta.y, delta.y];
187
+ const q = [a.x, -b.x, a.y, -b.y];
188
+ let tMin = 0, tMax = 1;
189
+ for (let i = 4; i--;)
190
+ {
191
+ if (p[i])
192
+ {
193
+ const t = q[i] / p[i];
194
+ if (p[i] < 0)
195
+ {
196
+ if (t > tMax) return false;
197
+ tMin = max(t, tMin);
198
+ }
199
+ else
200
+ {
201
+ if (t < tMin) return false;
202
+ tMax = min(t, tMax);
203
+ }
204
+ }
205
+ else if (q[i] < 0)
206
+ return false;
207
+ }
208
+
209
+ return true;
169
210
  }
170
211
 
171
212
  /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
@@ -265,7 +306,7 @@ class RandomGenerator
265
306
  this.seed ^= this.seed << 13;
266
307
  this.seed ^= this.seed >>> 17;
267
308
  this.seed ^= this.seed << 5;
268
- return valueB + (valueA - valueB) * abs(this.seed % 1e9) / 1e9;
309
+ return valueB + (valueA - valueB) * abs(this.seed % 1e8) / 1e8;
269
310
  }
270
311
 
271
312
  /** Returns a floored seeded random value the two values passed in
@@ -276,7 +317,7 @@ class RandomGenerator
276
317
 
277
318
  /** Randomly returns either -1 or 1 deterministically
278
319
  * @return {Number} */
279
- sign() { return this.int(2) * 2 - 1; }
320
+ sign() { return this.float() > .5 ? 1 : -1; }
280
321
  }
281
322
 
282
323
  ///////////////////////////////////////////////////////////////////////////////
@@ -324,6 +365,7 @@ class Vector2
324
365
  * @param {Number} [y] - Y axis location */
325
366
  constructor(x=0, y=0)
326
367
  {
368
+ ASSERT(typeof x === 'number' && typeof y === 'number');
327
369
  /** @property {Number} - X axis location */
328
370
  this.x = x;
329
371
  /** @property {Number} - Y axis location */
@@ -650,12 +692,14 @@ class Color
650
692
  * @return {Color} */
651
693
  setHSLA(h=0, s=0, l=1, a=1)
652
694
  {
695
+ h = mod(h,1);
696
+ s = clamp(s);
697
+ l = clamp(l);
653
698
  const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
654
699
  f = (p, q, t)=>
655
- (t = ((t%1)+1)%1) < 1/6 ? p+(q-p)*6*t :
656
- t < 1/2 ? q :
657
- t < 2/3 ? p+(q-p)*(2/3-t)*6 : p;
658
-
700
+ (t = mod(t,1))*6 < 1 ? p+(q-p)*6*t :
701
+ t*2 < 1 ? q :
702
+ t*3 < 2 ? p+(q-p)*(4-t*6) : p;
659
703
  this.r = f(p, q, h + 1/3);
660
704
  this.g = f(p, q, h);
661
705
  this.b = f(p, q, h - 1/3);
@@ -706,13 +750,12 @@ class Color
706
750
  ).clamp();
707
751
  }
708
752
 
709
- /** Returns this color expressed as a hex color code
753
+ /** Returns this color expressed as a rgb color code
710
754
  * @param {Boolean} [useAlpha] - if alpha should be included in result
711
755
  * @return {String} */
712
- toString(useAlpha = true)
713
- {
714
- const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
715
- return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
756
+ toString(useAlpha = true)
757
+ {
758
+ return `rgb(${this.r*255},${this.g*255},${this.b*255},${useAlpha ? this.a : 0})`;
716
759
  }
717
760
 
718
761
  /** Set this color from a hex code
@@ -770,11 +813,11 @@ class Timer
770
813
 
771
814
  /** Returns true if set and has not elapsed
772
815
  * @return {Boolean} */
773
- active() { return time <= this.time; }
816
+ active() { return time < this.time; }
774
817
 
775
818
  /** Returns true if set and elapsed
776
819
  * @return {Boolean} */
777
- elapsed() { return time > this.time; }
820
+ elapsed() { return time >= this.time; }
778
821
 
779
822
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
780
823
  * @return {Number} */
@@ -849,6 +892,12 @@ let fontDefault = 'arial';
849
892
  * @memberof Settings */
850
893
  let showSplashScreen = false;
851
894
 
895
+ /** Disables all rendering, audio, and input for servers
896
+ * @type {Boolean}
897
+ * @default
898
+ * @memberof Settings */
899
+ let headlessMode = false;
900
+
852
901
  ///////////////////////////////////////////////////////////////////////////////
853
902
  // WebGL settings
854
903
 
@@ -877,7 +926,7 @@ let tileSizeDefault = vec2(16);
877
926
  * @type {Number}
878
927
  * @default
879
928
  * @memberof Settings */
880
- let tileFixBleedScale = .1;
929
+ let tileFixBleedScale = .5;
881
930
 
882
931
  ///////////////////////////////////////////////////////////////////////////////
883
932
  // Object settings
@@ -1002,7 +1051,7 @@ let soundEnable = true;
1002
1051
  * @type {Number}
1003
1052
  * @default
1004
1053
  * @memberof Settings */
1005
- let soundVolume = .5;
1054
+ let soundVolume = .3;
1006
1055
 
1007
1056
  /** Default range where sound no longer plays
1008
1057
  * @type {Number}
@@ -1087,6 +1136,11 @@ function setFontDefault(font) { fontDefault = font; }
1087
1136
  * @memberof Settings */
1088
1137
  function setShowSplashScreen(show) { showSplashScreen = show; }
1089
1138
 
1139
+ /** Set to disalbe rendering, audio, and input for servers
1140
+ * @param {Boolean} headless
1141
+ * @memberof Settings */
1142
+ function setHeadlessMode(headless) { headlessMode = headless; }
1143
+
1090
1144
  /** Set if webgl rendering is enabled
1091
1145
  * @param {Boolean} enable
1092
1146
  * @memberof Settings */
@@ -1349,23 +1403,37 @@ class EngineObject
1349
1403
  this.collideSolidObjects = false;
1350
1404
  /** @property {Boolean} - Object collides with and blocks other objects */
1351
1405
  this.isSolid = false;
1406
+ /** @property {Boolean} - Object collides with raycasts */
1407
+ this.collideRaycast = false;
1352
1408
 
1353
1409
  // add to list of objects
1354
1410
  engineObjects.push(this);
1355
1411
  }
1356
1412
 
1357
- /** Update the object transform and physics, called automatically by engine once each frame */
1358
- update()
1413
+ /** Update the object transform, called automatically by engine even when paused */
1414
+ updateTransforms()
1359
1415
  {
1360
1416
  const parent = this.parent;
1361
1417
  if (parent)
1362
1418
  {
1363
1419
  // copy parent pos/angle
1364
- this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
1365
- this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
1366
- return;
1420
+ const mirror = parent.getMirrorSign();
1421
+ this.pos = this.localPos.multiply(vec2(mirror,1)).rotate(-parent.angle).add(parent.pos);
1422
+ this.angle = mirror*this.localAngle + parent.angle;
1367
1423
  }
1368
1424
 
1425
+ // update children
1426
+ for (const child of this.children)
1427
+ child.updateTransforms();
1428
+ }
1429
+
1430
+ /** Update the object physics, called automatically by engine once each frame */
1431
+ update()
1432
+ {
1433
+ // child objects do not have physics
1434
+ if (this.parent)
1435
+ return;
1436
+
1369
1437
  // limit max speed to prevent missing collisions
1370
1438
  this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1371
1439
  this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
@@ -1380,8 +1448,7 @@ class EngineObject
1380
1448
  // physics sanity checks
1381
1449
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1382
1450
  ASSERT(this.damping >= 0 && this.damping <= 1);
1383
-
1384
- if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
1451
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
1385
1452
  return;
1386
1453
 
1387
1454
  const wasMovingDown = this.velocity.y < 0;
@@ -1553,13 +1620,7 @@ class EngineObject
1553
1620
  * @param {Number} tileData - the value of the tile at the position
1554
1621
  * @param {Vector2} pos - tile where the collision occured
1555
1622
  * @return {Boolean} - true if the collision should be resolved */
1556
- collideWithTile(tileData, pos) { return tileData > 0; }
1557
-
1558
- /** Called to check if a tile raycast hit
1559
- * @param {Number} tileData - the value of the tile at the position
1560
- * @param {Vector2} pos - tile where the raycast is
1561
- * @return {Boolean} - true if the raycast should hit */
1562
- collideWithTileRaycast(tileData, pos) { return tileData > 0; }
1623
+ collideWithTile(tileData, pos) { return tileData > 0; }
1563
1624
 
1564
1625
  /** Called to check if a object collision should be resolved
1565
1626
  * @param {EngineObject} object - the object to test against
@@ -1606,16 +1667,18 @@ class EngineObject
1606
1667
  }
1607
1668
 
1608
1669
  /** Set how this object collides
1609
- * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects
1610
- * @param {Boolean} [isSolid] - Does it collide with and block other objects (expensive in large numbers)
1611
- * @param {Boolean} [collideTiles] - Does it collide with the tile collision */
1612
- setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true)
1670
+ * @param {Boolean} [collideSolidObjects] - Does it collide with solid objects?
1671
+ * @param {Boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
1672
+ * @param {Boolean} [collideTiles] - Does it collide with the tile collision?
1673
+ * @param {Boolean} [collideRaycast] - Does it collide with raycasts? */
1674
+ setCollision(collideSolidObjects=true, isSolid=true, collideTiles=true, collideRaycast=true)
1613
1675
  {
1614
1676
  ASSERT(collideSolidObjects || !isSolid, 'solid objects must be set to collide');
1615
1677
 
1616
1678
  this.collideSolidObjects = collideSolidObjects;
1617
1679
  this.isSolid = isSolid;
1618
1680
  this.collideTiles = collideTiles;
1681
+ this.collideRaycast = collideRaycast;
1619
1682
  }
1620
1683
 
1621
1684
  /** Returns string containg info about this object for debugging
@@ -1715,6 +1778,9 @@ let drawCount;
1715
1778
  */
1716
1779
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1717
1780
  {
1781
+ if (headlessMode)
1782
+ return new TileInfo;
1783
+
1718
1784
  // if size is a number, make it a vector
1719
1785
  if (typeof size === 'number')
1720
1786
  {
@@ -1748,9 +1814,9 @@ class TileInfo
1748
1814
  constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1749
1815
  {
1750
1816
  /** @property {Vector2} - Top left corner of tile in pixels */
1751
- this.pos = pos;
1817
+ this.pos = pos.copy();
1752
1818
  /** @property {Vector2} - Size of tile in pixels */
1753
- this.size = size;
1819
+ this.size = size.copy();
1754
1820
  /** @property {Number} - Texture index to use */
1755
1821
  this.textureIndex = textureIndex;
1756
1822
  }
@@ -1842,7 +1908,7 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
1842
1908
  * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
1843
1909
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1844
1910
  * @param {Boolean} [screenSpace=false] - If true the pos and size are in screen space
1845
- * @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
1911
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
1846
1912
  * @memberof Draw */
1847
1913
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
1848
1914
  angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
@@ -1915,7 +1981,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
1915
1981
  * @param {Number} [angle]
1916
1982
  * @param {Boolean} [useWebGL=glEnable]
1917
1983
  * @param {Boolean} [screenSpace=false]
1918
- * @param {CanvasRenderingContext2D} [context]
1984
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
1919
1985
  * @memberof Draw */
1920
1986
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
1921
1987
  {
@@ -1929,7 +1995,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
1929
1995
  * @param {Color} [color=(1,1,1,1)]
1930
1996
  * @param {Boolean} [useWebGL=glEnable]
1931
1997
  * @param {Boolean} [screenSpace=false]
1932
- * @param {CanvasRenderingContext2D} [context]
1998
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
1933
1999
  * @memberof Draw */
1934
2000
  function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
1935
2001
  {
@@ -1945,7 +2011,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
1945
2011
  * @param {Boolean} mirror
1946
2012
  * @param {Function} drawFunction
1947
2013
  * @param {Boolean} [screenSpace=false]
1948
- * @param {CanvasRenderingContext2D} [context=mainContext]
2014
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
1949
2015
  * @memberof Draw */
1950
2016
  function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
1951
2017
  {
@@ -1966,7 +2032,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
1966
2032
  /** Enable normal or additive blend mode
1967
2033
  * @param {Boolean} [additive]
1968
2034
  * @param {Boolean} [useWebGL=glEnable]
1969
- * @param {CanvasRenderingContext2D} [context=mainContext]
2035
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
1970
2036
  * @memberof Draw */
1971
2037
  function setBlendMode(additive, useWebGL=glEnable, context)
1972
2038
  {
@@ -1991,7 +2057,7 @@ function setBlendMode(additive, useWebGL=glEnable, context)
1991
2057
  * @param {Color} [lineColor=(0,0,0,1)]
1992
2058
  * @param {CanvasTextAlign} [textAlign='center']
1993
2059
  * @param {String} [font=fontDefault]
1994
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2060
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
1995
2061
  * @memberof Draw */
1996
2062
  function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
1997
2063
  {
@@ -2008,7 +2074,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
2008
2074
  * @param {Color} [lineColor=(0,0,0,1)]
2009
2075
  * @param {CanvasTextAlign} [textAlign]
2010
2076
  * @param {String} [font=fontDefault]
2011
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2077
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2012
2078
  * @memberof Draw */
2013
2079
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
2014
2080
  {
@@ -2051,7 +2117,7 @@ class FontImage
2051
2117
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
2052
2118
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
2053
2119
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
2054
- * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
2120
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
2055
2121
  */
2056
2122
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
2057
2123
  {
@@ -2259,7 +2325,7 @@ function gamepadWasReleased(button, gamepad=0)
2259
2325
  * @return {Vector2}
2260
2326
  * @memberof Input */
2261
2327
  function gamepadStick(stick, gamepad=0)
2262
- { return stickData[gamepad] ? stickData[gamepad][stick] || vec2() : vec2(); }
2328
+ { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
2263
2329
 
2264
2330
  ///////////////////////////////////////////////////////////////////////////////
2265
2331
  // Input update called by engine
@@ -2270,6 +2336,8 @@ let inputData = [[]];
2270
2336
 
2271
2337
  function inputUpdate()
2272
2338
  {
2339
+ if (headlessMode) return;
2340
+
2273
2341
  // clear input when lost focus (prevent stuck keys)
2274
2342
  isTouchDevice || document.hasFocus() || clearInput();
2275
2343
 
@@ -2282,6 +2350,8 @@ function inputUpdate()
2282
2350
 
2283
2351
  function inputUpdatePost()
2284
2352
  {
2353
+ if (headlessMode) return;
2354
+
2285
2355
  // clear input to prepare for next frame
2286
2356
  for (const deviceInputData of inputData)
2287
2357
  for (const i in deviceInputData)
@@ -2290,9 +2360,12 @@ function inputUpdatePost()
2290
2360
  }
2291
2361
 
2292
2362
  ///////////////////////////////////////////////////////////////////////////////
2293
- // Keyboard event handlers
2363
+ // Input event handlers
2294
2364
 
2365
+ function inputInit()
2295
2366
  {
2367
+ if (headlessMode) return;
2368
+
2296
2369
  onkeydown = (e)=>
2297
2370
  {
2298
2371
  if (debug && e.target != document.body) return;
@@ -2323,21 +2396,29 @@ function inputUpdatePost()
2323
2396
  c == 'KeyA' ? 'ArrowLeft' :
2324
2397
  c == 'KeyD' ? 'ArrowRight' : c : c;
2325
2398
  }
2399
+
2400
+ // mouse event handlers
2401
+ onmousedown = (e)=>
2402
+ {
2403
+ isUsingGamepad = false;
2404
+ inputData[0][e.button] = 3;
2405
+ mousePosScreen = mouseToScreen(e);
2406
+ e.button && e.preventDefault();
2407
+ }
2408
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2409
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2410
+ onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2411
+ oncontextmenu = (e)=> false; // prevent right click menu
2412
+
2413
+ // init touch input
2414
+ if (isTouchDevice)
2415
+ touchInputInit();
2326
2416
  }
2327
2417
 
2328
- ///////////////////////////////////////////////////////////////////////////////
2329
- // Mouse event handlers
2330
-
2331
- onmousedown = (e)=> {isUsingGamepad = false; inputData[0][e.button] = 3; mousePosScreen = mouseToScreen(e); e.button && e.preventDefault();}
2332
- onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2333
- onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2334
- onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2335
- oncontextmenu = (e)=> false; // prevent right click menu
2336
-
2337
2418
  // convert a mouse or touch event position to screen space
2338
2419
  function mouseToScreen(mousePos)
2339
2420
  {
2340
- if (!mainCanvas)
2421
+ if (!mainCanvas || headlessMode)
2341
2422
  return vec2(); // fix bug that can occur if user clicks before page loads
2342
2423
 
2343
2424
  const rect = mainCanvas.getBoundingClientRect();
@@ -2349,7 +2430,7 @@ function mouseToScreen(mousePos)
2349
2430
  // Gamepad input
2350
2431
 
2351
2432
  // gamepad internal variables
2352
- const stickData = [];
2433
+ const gamepadStickData = [];
2353
2434
 
2354
2435
  // gamepads are updated by engine every frame automatically
2355
2436
  function gamepadsUpdate()
@@ -2366,14 +2447,11 @@ function gamepadsUpdate()
2366
2447
  // update touch gamepad if enabled
2367
2448
  if (touchGamepadEnable && isTouchDevice)
2368
2449
  {
2369
- // create the touch gamepad if it doesn't exist
2370
- if (!touchGamepadButtons)
2371
- createTouchGamepad();
2372
-
2450
+ ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
2373
2451
  if (touchGamepadTimer.isSet())
2374
2452
  {
2375
2453
  // read virtual analog stick
2376
- const sticks = stickData[0] || (stickData[0] = []);
2454
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
2377
2455
  sticks[0] = vec2();
2378
2456
  if (touchGamepadAnalog)
2379
2457
  sticks[0] = applyDeadZones(touchGamepadStick);
@@ -2390,7 +2468,8 @@ function gamepadsUpdate()
2390
2468
  for (let i=10; i--;)
2391
2469
  {
2392
2470
  const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
2393
- data[j] = touchGamepadButtons[i] ? gamepadIsDown(j,0) ? 1 : 3 : gamepadIsDown(j,0) ? 4 : 0;
2471
+ const wasDown = gamepadIsDown(j,0);
2472
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
2394
2473
  }
2395
2474
  }
2396
2475
  }
@@ -2410,7 +2489,7 @@ function gamepadsUpdate()
2410
2489
  // get or create gamepad data
2411
2490
  const gamepad = gamepads[i];
2412
2491
  const data = inputData[i+1] || (inputData[i+1] = []);
2413
- const sticks = stickData[i] || (stickData[i] = []);
2492
+ const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
2414
2493
 
2415
2494
  if (gamepad)
2416
2495
  {
@@ -2449,28 +2528,44 @@ function gamepadsUpdate()
2449
2528
  * @param {Number|Array} [pattern] - single value in ms or vibration interval array
2450
2529
  * @memberof Input */
2451
2530
  function vibrate(pattern=100)
2452
- { vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2531
+ { vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2453
2532
 
2454
2533
  /** Cancel any ongoing vibration
2455
2534
  * @memberof Input */
2456
2535
  function vibrateStop() { vibrate(0); }
2457
2536
 
2458
2537
  ///////////////////////////////////////////////////////////////////////////////
2459
- // Touch input
2538
+ // Touch input & virtual on screen gamepad
2460
2539
 
2461
2540
  /** True if a touch device has been detected
2462
2541
  * @memberof Input */
2463
- const isTouchDevice = window.ontouchstart !== undefined;
2542
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
2543
+
2544
+ // touch gamepad internal variables
2545
+ let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2464
2546
 
2465
2547
  // try to enable touch mouse
2466
- if (isTouchDevice)
2548
+ function touchInputInit()
2467
2549
  {
2550
+ // add non passive touch event listeners
2551
+ let handleTouch = handleTouchDefault;
2552
+ if (touchGamepadEnable)
2553
+ {
2554
+ // touch input internal variables
2555
+ handleTouch = handleTouchGamepad;
2556
+ touchGamepadButtons = [];
2557
+ touchGamepadStick = vec2();
2558
+ }
2559
+ document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
2560
+ document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
2561
+ document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
2562
+
2468
2563
  // override mouse events
2469
- let wasTouching;
2470
2564
  onmousedown = onmouseup = ()=> 0;
2471
2565
 
2472
2566
  // handle all touch events the same way
2473
- ontouchstart = ontouchmove = ontouchend = (e)=>
2567
+ let wasTouching;
2568
+ function handleTouchDefault(e)
2474
2569
  {
2475
2570
  // fix stalled audio requiring user interaction
2476
2571
  if (soundEnable && audioContext && audioContext.state != 'running')
@@ -2499,27 +2594,14 @@ if (isTouchDevice)
2499
2594
  // must return true so the document will get focus
2500
2595
  return true;
2501
2596
  }
2502
- }
2503
2597
 
2504
- ///////////////////////////////////////////////////////////////////////////////
2505
- // touch gamepad, virtual on screen gamepad emulator for touch devices
2506
-
2507
- // touch input internal variables
2508
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2509
-
2510
- // create the touch gamepad, called automatically by the engine
2511
- function createTouchGamepad()
2512
- {
2513
- // touch input internal variables
2514
- touchGamepadButtons = [];
2515
- touchGamepadStick = vec2();
2516
-
2517
- const touchHandler = ontouchstart;
2518
- ontouchstart = ontouchmove = ontouchend = (e)=>
2598
+ // special handling for virtual gamepad mode
2599
+ function handleTouchGamepad(e)
2519
2600
  {
2520
2601
  // clear touch gamepad input
2521
2602
  touchGamepadStick = vec2();
2522
2603
  touchGamepadButtons = [];
2604
+ isUsingGamepad = true;
2523
2605
 
2524
2606
  const touching = e.touches.length;
2525
2607
  if (touching)
@@ -2560,9 +2642,8 @@ function createTouchGamepad()
2560
2642
  }
2561
2643
  }
2562
2644
 
2563
- // call default touch handler and set to using gamepad
2564
- touchHandler.bind(window)(e);
2565
- isUsingGamepad = true;
2645
+ // call default touch handler so normal touch events still work
2646
+ handleTouchDefault(e);
2566
2647
 
2567
2648
  // must return true so the document will get focus
2568
2649
  return true;
@@ -2581,32 +2662,33 @@ function touchGamepadRender()
2581
2662
  return;
2582
2663
 
2583
2664
  // setup the canvas
2584
- overlayContext.save();
2585
- overlayContext.globalAlpha = alpha*touchGamepadAlpha;
2586
- overlayContext.strokeStyle = '#fff';
2587
- overlayContext.lineWidth = 3;
2665
+ const context = overlayContext;
2666
+ context.save();
2667
+ context.globalAlpha = alpha*touchGamepadAlpha;
2668
+ context.strokeStyle = '#fff';
2669
+ context.lineWidth = 3;
2588
2670
 
2589
2671
  // draw left analog stick
2590
- overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2591
- overlayContext.beginPath();
2672
+ context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2673
+ context.beginPath();
2592
2674
 
2593
2675
  const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2594
2676
  if (touchGamepadAnalog) // draw circle shaped gamepad
2595
2677
  {
2596
- overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2597
- overlayContext.fill();
2598
- overlayContext.stroke();
2678
+ context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2679
+ context.fill();
2680
+ context.stroke();
2599
2681
  }
2600
2682
  else // draw cross shaped gamepad
2601
2683
  {
2602
2684
  for(let i=10; i--;)
2603
2685
  {
2604
2686
  const angle = i*PI/4;
2605
- overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
2606
- i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
2607
- i==1 && overlayContext.fill();
2687
+ context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
2688
+ i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
2689
+ i==1 && context.fill();
2608
2690
  }
2609
- overlayContext.stroke();
2691
+ context.stroke();
2610
2692
  }
2611
2693
 
2612
2694
  // draw right face buttons
@@ -2614,15 +2696,15 @@ function touchGamepadRender()
2614
2696
  for (let i=4; i--;)
2615
2697
  {
2616
2698
  const pos = rightCenter.add(vec2().setDirection(i, touchGamepadSize/2));
2617
- overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2618
- overlayContext.beginPath();
2619
- overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
2620
- overlayContext.fill();
2621
- overlayContext.stroke();
2699
+ context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2700
+ context.beginPath();
2701
+ context.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
2702
+ context.fill();
2703
+ context.stroke();
2622
2704
  }
2623
2705
 
2624
2706
  // set canvas back to normal
2625
- overlayContext.restore();
2707
+ context.restore();
2626
2708
  }
2627
2709
  /**
2628
2710
  * LittleJS Audio System
@@ -2657,7 +2739,7 @@ class Sound
2657
2739
  */
2658
2740
  constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
2659
2741
  {
2660
- if (!soundEnable) return;
2742
+ if (!soundEnable || headlessMode) return;
2661
2743
 
2662
2744
  /** @property {Number} - World space max range of sound, will not play if camera is farther away */
2663
2745
  this.range = range;
@@ -2671,8 +2753,8 @@ class Sound
2671
2753
  if (zzfxSound)
2672
2754
  {
2673
2755
  // generate zzfx sound now for fast playback
2674
- this.randomness = zzfxSound[1] || 0;
2675
- zzfxSound[1] = 0; // generate without randomness
2756
+ const defaultRandomness = .05;
2757
+ this.randomness = zzfxSound[1] || defaultRandomness;
2676
2758
  this.sampleChannels = [zzfxG(...zzfxSound)];
2677
2759
  this.sampleRate = zzfxR;
2678
2760
  }
@@ -2688,7 +2770,7 @@ class Sound
2688
2770
  */
2689
2771
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
2690
2772
  {
2691
- if (!soundEnable || !this.sampleChannels) return;
2773
+ if (!soundEnable || !this.sampleChannels || headlessMode) return;
2692
2774
 
2693
2775
  let pan;
2694
2776
  if (pos)
@@ -2771,7 +2853,9 @@ class SoundWave extends Sound
2771
2853
  super(undefined, range, taper);
2772
2854
  this.randomness = randomness;
2773
2855
 
2774
- if (!soundEnable) return;
2856
+ if (!soundEnable || headlessMode) return;
2857
+ if (!audioContext)
2858
+ audioContext = new AudioContext; // create audio context
2775
2859
 
2776
2860
  fetch(filename)
2777
2861
  .then(response => response.arrayBuffer())
@@ -2825,7 +2909,7 @@ class Music extends Sound
2825
2909
  {
2826
2910
  super(undefined);
2827
2911
 
2828
- if (!soundEnable) return;
2912
+ if (!soundEnable || headlessMode) return;
2829
2913
  this.randomness = 0;
2830
2914
  this.sampleChannels = zzfxM(...zzfxMusic);
2831
2915
  this.sampleRate = zzfxR;
@@ -2848,7 +2932,7 @@ class Music extends Sound
2848
2932
  * @memberof Audio */
2849
2933
  function playAudioFile(filename, volume=1, loop=false)
2850
2934
  {
2851
- if (!soundEnable) return;
2935
+ if (!soundEnable || headlessMode) return;
2852
2936
 
2853
2937
  const audio = new Audio(filename);
2854
2938
  audio.volume = soundVolume * volume;
@@ -2867,7 +2951,7 @@ function playAudioFile(filename, volume=1, loop=false)
2867
2951
  * @memberof Audio */
2868
2952
  function speak(text, language='', volume=1, rate=1, pitch=1)
2869
2953
  {
2870
- if (!soundEnable || !speechSynthesis) return;
2954
+ if (!soundEnable || !speechSynthesis || headlessMode) return;
2871
2955
 
2872
2956
  // common languages (not supported by all browsers)
2873
2957
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -2900,7 +2984,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
2900
2984
  /** Audio context used by the engine
2901
2985
  * @type {AudioContext}
2902
2986
  * @memberof Audio */
2903
- let audioContext = new AudioContext;
2987
+ let audioContext;
2904
2988
 
2905
2989
  /** Keep track if audio was suspended when last sound was played
2906
2990
  * @type {Boolean}
@@ -2918,7 +3002,9 @@ let audioSuspended = false;
2918
3002
  * @memberof Audio */
2919
3003
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
2920
3004
  {
2921
- if (!soundEnable) return;
3005
+ if (!soundEnable || headlessMode) return;
3006
+ if (!audioContext)
3007
+ audioContext = new AudioContext; // create audio context
2922
3008
 
2923
3009
  // prevent sounds from building up if they can't be played
2924
3010
  const audioWasSuspended = audioSuspended;
@@ -2964,7 +3050,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
2964
3050
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
2965
3051
  * @return {AudioBufferSourceNode} - The audio node of the sound played
2966
3052
  * @memberof Audio */
2967
- function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
3053
+ function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
2968
3054
 
2969
3055
  /** Sample rate used for all ZzFX sounds
2970
3056
  * @default 44100
@@ -2973,7 +3059,7 @@ const zzfxR = 44100;
2973
3059
 
2974
3060
  /** Generate samples for a ZzFX sound
2975
3061
  * @param {Number} [volume] - Volume scale (percent)
2976
- * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
3062
+ * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
2977
3063
  * @param {Number} [frequency] - Frequency of sound (Hz)
2978
3064
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
2979
3065
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -2999,17 +3085,18 @@ const zzfxR = 44100;
2999
3085
  function zzfxG
3000
3086
  (
3001
3087
  // parameters
3002
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3088
+ volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
3003
3089
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3004
3090
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3005
3091
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3006
3092
  )
3007
3093
  {
3094
+ // LJS Note: ZZFX modded so randomness is handled by Sound class
3095
+
3008
3096
  // init parameters
3009
3097
  let PI2 = PI*2, sampleRate = zzfxR,
3010
3098
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3011
- startFrequency = frequency *=
3012
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3099
+ startFrequency = frequency *= PI2 / sampleRate,
3013
3100
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3014
3101
 
3015
3102
  // biquad LP/HP filter
@@ -3031,7 +3118,6 @@ function zzfxG
3031
3118
  pitchJump *= PI2 / sampleRate;
3032
3119
  pitchJumpTime *= sampleRate;
3033
3120
  repeatTime = repeatTime * sampleRate | 0;
3034
- volume *= soundVolume;
3035
3121
 
3036
3122
  // generate waveform
3037
3123
  for(length = attack + decay + sustain + release + delay | 0;
@@ -3268,7 +3354,7 @@ function tileCollisionTest(pos, size=vec2(), object)
3268
3354
  }
3269
3355
  }
3270
3356
 
3271
- /** Return the center of tile if any that is hit (does not return the exact intersection)
3357
+ /** Return the center of first tile hit (does not return the exact intersection)
3272
3358
  * @param {Vector2} posStart
3273
3359
  * @param {Vector2} posEnd
3274
3360
  * @param {EngineObject} [object]
@@ -3377,7 +3463,7 @@ class TileLayer extends EngineObject
3377
3463
 
3378
3464
  /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3379
3465
  this.canvas = document.createElement('canvas');
3380
- /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3466
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3381
3467
  this.context = this.canvas.getContext('2d');
3382
3468
  /** @property {Vector2} - How much to scale this layer when rendered */
3383
3469
  this.scale = scale;
@@ -3388,6 +3474,17 @@ class TileLayer extends EngineObject
3388
3474
  this.data = [];
3389
3475
  for (let j = this.size.area(); j--;)
3390
3476
  this.data.push(new TileLayerData);
3477
+
3478
+ if (headlessMode)
3479
+ {
3480
+ // disable rendering
3481
+ this.redraw = () => {};
3482
+ this.render = () => {};
3483
+ this.redrawStart = () => {};
3484
+ this.redrawEnd = () => {};
3485
+ this.drawTileData = () => {};
3486
+ this.drawCanvas2D = () => {};
3487
+ }
3391
3488
  }
3392
3489
 
3393
3490
  /** Set data at a given position in the array
@@ -3418,7 +3515,7 @@ class TileLayer extends EngineObject
3418
3515
  ASSERT(mainContext != this.context, 'must call redrawEnd() after drawing tiles');
3419
3516
 
3420
3517
  // flush and copy gl canvas because tile canvas does not use webgl
3421
- glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3518
+ !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3422
3519
 
3423
3520
  // draw the entire cached level onto the canvas
3424
3521
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
@@ -3464,15 +3561,18 @@ class TileLayer extends EngineObject
3464
3561
  mainCanvas.height = mainCanvasSize.y;
3465
3562
  }
3466
3563
 
3467
- // begin a new render for the tile canvas
3468
- enginePreRender();
3564
+ // disable smoothing for pixel art
3565
+ this.context.imageSmoothingEnabled = !canvasPixelated;
3566
+
3567
+ // setup gl rendering if enabled
3568
+ glPreRender();
3469
3569
  }
3470
3570
 
3471
3571
  /** Call to end the redraw process */
3472
3572
  redrawEnd()
3473
3573
  {
3474
3574
  ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3475
- glEnable && glCopyToContext(mainContext, true);
3575
+ glCopyToContext(mainContext, true);
3476
3576
  //debugSaveCanvas(this.canvas);
3477
3577
 
3478
3578
  // set stuff back to normal
@@ -3797,20 +3897,21 @@ class ParticleEmitter extends EngineObject
3797
3897
  class Particle extends EngineObject
3798
3898
  {
3799
3899
  /**
3800
- * Create a particle with the given shis.colorStart = undefined;ettings
3801
- * @param {Vector2} position - World space position of the particle
3802
- * @param {TileInfo} [tileInfo] - Tile info to render particles
3803
- * @param {Number} [angle] - Angle to rotate the particle
3804
- * @param {Color} [colorStart] - Color at start of life
3805
- * @param {Color} [colorEnd] - Color at end of life
3806
- * @param {Number} [lifeTime] - How long to live for
3807
- * @param {Number} [sizeStart] - Angle to rotate the particle
3808
- * @param {Number} [sizeEnd] - Angle to rotate the particle
3809
- * @param {Number} [fadeRate] - Angle to rotate the particle
3810
- * @param {Boolean} [additive] - Angle to rotate the particle
3811
- * @param {Number} [trailScale] - If a trail, how long to make it
3900
+ * Create a particle with the passed in settings
3901
+ * Typically this is created automatically by a ParticleEmitter
3902
+ * @param {Vector2} position - World space position of the particle
3903
+ * @param {TileInfo} tileInfo - Tile info to render particles
3904
+ * @param {Number} angle - Angle to rotate the particle
3905
+ * @param {Color} colorStart - Color at start of life
3906
+ * @param {Color} colorEnd - Color at end of life
3907
+ * @param {Number} lifeTime - How long to live for
3908
+ * @param {Number} sizeStart - Size at start of life
3909
+ * @param {Number} sizeEnd - Size at end of life
3910
+ * @param {Number} fadeRate - How quick to fade in/out
3911
+ * @param {Boolean} additive - Does it use additive blend mode
3912
+ * @param {Number} trailScale - If a trail, how long to make it
3812
3913
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
3813
- * @param {Function} [destroyCallback] - Called when particle dies
3914
+ * @param {Function} [destroyCallback] - Callback when particle dies
3814
3915
  */
3815
3916
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
3816
3917
  )
@@ -4219,6 +4320,8 @@ let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData,
4219
4320
  // Initalize WebGL, called automatically by the engine
4220
4321
  function glInit()
4221
4322
  {
4323
+ if (!glEnable || headlessMode) return;
4324
+
4222
4325
  // create the canvas and textures
4223
4326
  glCanvas = document.createElement('canvas');
4224
4327
  glContext = glCanvas.getContext('webgl2');
@@ -4231,11 +4334,11 @@ function glInit()
4231
4334
  '#version 300 es\n' + // specify GLSL ES version
4232
4335
  'precision highp float;'+ // use highp for better accuracy
4233
4336
  'uniform mat4 m;'+ // transform matrix
4234
- 'in vec2 g;'+ // geometry
4235
- 'in vec4 p,u,c,a;'+ // position/size, uvs, color, additiveColor
4236
- 'in float r;'+ // rotation
4237
- 'out vec2 v;'+ // return uv, color, additiveColor
4238
- 'out vec4 d,e;'+ // return uv, color, additiveColor
4337
+ 'in vec2 g;'+ // in: geometry
4338
+ 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
4339
+ 'in float r;'+ // in: rotation
4340
+ 'out vec2 v;'+ // out: uv
4341
+ 'out vec4 d,e;'+ // out: color, additiveColor
4239
4342
  'void main(){'+ // shader entry point
4240
4343
  'vec2 s=(g-.5)*p.zw;'+ // get size offset
4241
4344
  'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
@@ -4245,10 +4348,10 @@ function glInit()
4245
4348
  ,
4246
4349
  '#version 300 es\n' + // specify GLSL ES version
4247
4350
  'precision highp float;'+ // use highp for better accuracy
4248
- 'in vec2 v;'+ // uv
4249
- 'in vec4 d,e;'+ // color, additiveColor
4250
4351
  'uniform sampler2D s;'+ // texture
4251
- 'out vec4 c;'+ // out color
4352
+ 'in vec2 v;'+ // in: uv
4353
+ 'in vec4 d,e;'+ // in: color, additiveColor
4354
+ 'out vec4 c;'+ // out: color
4252
4355
  'void main(){'+ // shader entry point
4253
4356
  'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
4254
4357
  '}' // end of shader
@@ -4270,9 +4373,11 @@ function glInit()
4270
4373
  // Setup render each frame, called automatically by engine
4271
4374
  function glPreRender()
4272
4375
  {
4376
+ if (!glEnable || headlessMode) return;
4377
+
4273
4378
  // clear and set to same size as main canvas
4274
4379
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4275
- glContext.clear(gl_COLOR_BUFFER_BIT);
4380
+ //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4276
4381
 
4277
4382
  // set up the shader
4278
4383
  glContext.useProgram(glShader);
@@ -4306,12 +4411,12 @@ function glPreRender()
4306
4411
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
4307
4412
  const p = vec2(-1).subtract(cameraPos.multiply(s));
4308
4413
  glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
4309
- new Float32Array([
4414
+ [
4310
4415
  s.x, 0, 0, 0,
4311
4416
  0, s.y, 0, 0,
4312
4417
  1, 1, 1, 1,
4313
4418
  p.x, p.y, 0, 0
4314
- ])
4419
+ ]
4315
4420
  );
4316
4421
  }
4317
4422
 
@@ -4322,7 +4427,7 @@ function glPreRender()
4322
4427
  function glSetTexture(texture)
4323
4428
  {
4324
4429
  // must flush cache with the old texture to set a new one
4325
- if (texture == glActiveTexture)
4430
+ if (headlessMode || texture == glActiveTexture)
4326
4431
  return;
4327
4432
 
4328
4433
  glFlush();
@@ -4382,7 +4487,6 @@ function glCreateTexture(image)
4382
4487
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
4383
4488
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
4384
4489
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
4385
-
4386
4490
  return texture;
4387
4491
  }
4388
4492
 
@@ -4406,12 +4510,12 @@ function glFlush()
4406
4510
  }
4407
4511
 
4408
4512
  /** Draw any sprites still in the buffer, copy to main canvas and clear
4409
- * @param {CanvasRenderingContext2D} context
4513
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4410
4514
  * @param {Boolean} [forceDraw]
4411
4515
  * @memberof WebGL */
4412
4516
  function glCopyToContext(context, forceDraw=false)
4413
4517
  {
4414
- if (!glInstanceCount && !forceDraw) return;
4518
+ if (!glEnable || !glInstanceCount && !forceDraw) return;
4415
4519
 
4416
4520
  glFlush();
4417
4521
 
@@ -4468,7 +4572,7 @@ let glPostShader, glPostTexture, glPostIncludeOverlay;
4468
4572
  function glInitPostProcess(shaderCode, includeOverlay=false)
4469
4573
  {
4470
4574
  ASSERT(!glPostShader, 'can only have 1 post effects shader');
4471
-
4575
+ if (headlessMode) return;
4472
4576
  if (!shaderCode) // default shader pass through
4473
4577
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
4474
4578
 
@@ -4507,8 +4611,7 @@ function glInitPostProcess(shaderCode, includeOverlay=false)
4507
4611
  // Render the post processing shader, called automatically by the engine
4508
4612
  function glRenderPostProcess()
4509
4613
  {
4510
- if (!glPostShader)
4511
- return;
4614
+ if (!glPostShader || headlessMode) return;
4512
4615
 
4513
4616
  // prepare to render post process shader
4514
4617
  if (glEnable)
@@ -4614,7 +4717,7 @@ const engineName = 'LittleJS';
4614
4717
  * @type {String}
4615
4718
  * @default
4616
4719
  * @memberof Engine */
4617
- const engineVersion = '1.9.4';
4720
+ const engineVersion = '1.9.6';
4618
4721
 
4619
4722
  /** Frames per second to update
4620
4723
  * @type {Number}
@@ -4681,6 +4784,19 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4681
4784
  {
4682
4785
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
4683
4786
 
4787
+ // Called automatically by engine to setup render system
4788
+ function enginePreRender()
4789
+ {
4790
+ // save canvas size
4791
+ mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
4792
+
4793
+ // disable smoothing for pixel art
4794
+ mainContext.imageSmoothingEnabled = !canvasPixelated;
4795
+
4796
+ // setup gl rendering if enabled
4797
+ glPreRender();
4798
+ }
4799
+
4684
4800
  // internal update loop for engine
4685
4801
  function engineUpdate(frameTimeMS=0)
4686
4802
  {
@@ -4697,11 +4813,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4697
4813
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
4698
4814
  if (!debugSpeedUp)
4699
4815
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
4816
+
4700
4817
  updateCanvas();
4701
4818
 
4702
4819
  if (paused)
4703
4820
  {
4704
- // do post update even when paused
4821
+ // update object transforms even when paused
4822
+ for (const o of engineObjects)
4823
+ o.parent || o.updateTransforms();
4705
4824
  inputUpdate();
4706
4825
  debugUpdate();
4707
4826
  gameUpdatePost();
@@ -4713,7 +4832,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4713
4832
  let deltaSmooth = 0;
4714
4833
  if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
4715
4834
  {
4716
- // force an update each frame if time is close enough (not just a fast refresh rate)
4835
+ // force at least one update each frame since it is waiting for refresh
4717
4836
  deltaSmooth = frameTimeBufferMS;
4718
4837
  frameTimeBufferMS = 0;
4719
4838
  }
@@ -4738,34 +4857,37 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4738
4857
  // add the time smoothing back in
4739
4858
  frameTimeBufferMS += deltaSmooth;
4740
4859
  }
4741
-
4742
- // render sort then render while removing destroyed objects
4743
- enginePreRender();
4744
- gameRender();
4745
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
4746
- for (const o of engineObjects)
4747
- o.destroyed || o.render();
4748
- gameRenderPost();
4749
- glRenderPostProcess();
4750
- medalsRender();
4751
- touchGamepadRender();
4752
- debugRender();
4753
- glEnable && glCopyToContext(mainContext);
4754
-
4755
- if (showWatermark)
4860
+
4861
+ if (!headlessMode)
4756
4862
  {
4757
- // update fps
4758
- overlayContext.textAlign = 'right';
4759
- overlayContext.textBaseline = 'top';
4760
- overlayContext.font = '1em monospace';
4761
- overlayContext.fillStyle = '#000';
4762
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
4763
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
4764
- + (glEnable ? ' GL' : ' 2D') ;
4765
- overlayContext.fillText(text, mainCanvas.width-3, 3);
4766
- overlayContext.fillStyle = '#fff';
4767
- overlayContext.fillText(text, mainCanvas.width-2, 2);
4768
- drawCount = 0;
4863
+ // render sort then render while removing destroyed objects
4864
+ enginePreRender();
4865
+ gameRender();
4866
+ engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
4867
+ for (const o of engineObjects)
4868
+ o.destroyed || o.render();
4869
+ gameRenderPost();
4870
+ glRenderPostProcess();
4871
+ medalsRender();
4872
+ touchGamepadRender();
4873
+ debugRender();
4874
+ glCopyToContext(mainContext);
4875
+
4876
+ if (showWatermark)
4877
+ {
4878
+ // update fps
4879
+ overlayContext.textAlign = 'right';
4880
+ overlayContext.textBaseline = 'top';
4881
+ overlayContext.font = '1em monospace';
4882
+ overlayContext.fillStyle = '#000';
4883
+ const text = engineName + ' ' + 'v' + engineVersion + ' / '
4884
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
4885
+ + (glEnable ? ' GL' : ' 2D') ;
4886
+ overlayContext.fillText(text, mainCanvas.width-3, 3);
4887
+ overlayContext.fillStyle = '#fff';
4888
+ overlayContext.fillText(text, mainCanvas.width-2, 2);
4889
+ drawCount = 0;
4890
+ }
4769
4891
  }
4770
4892
 
4771
4893
  requestAnimationFrame(engineUpdate);
@@ -4773,6 +4895,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4773
4895
 
4774
4896
  function updateCanvas()
4775
4897
  {
4898
+ if (headlessMode) return;
4899
+
4776
4900
  if (canvasFixedSize.x)
4777
4901
  {
4778
4902
  // clear canvas and set fixed size
@@ -4800,8 +4924,20 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4800
4924
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
4801
4925
  }
4802
4926
 
4927
+ function startEngine()
4928
+ {
4929
+ gameInit();
4930
+ engineUpdate();
4931
+ }
4932
+
4933
+ if (headlessMode)
4934
+ {
4935
+ startEngine();
4936
+ return;
4937
+ }
4938
+
4803
4939
  // setup html
4804
- const styleBody =
4940
+ const styleBody =
4805
4941
  'margin:0;overflow:hidden;' + // fill the window
4806
4942
  'background:#000;' + // set background color
4807
4943
  'touch-action:none;' + // prevent mobile pinch to resize
@@ -4813,8 +4949,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4813
4949
  mainContext = mainCanvas.getContext('2d');
4814
4950
 
4815
4951
  // init stuff and start engine
4952
+ inputInit();
4816
4953
  debugInit();
4817
- glEnable && glInit();
4954
+ glInit();
4818
4955
 
4819
4956
  // create overlay canvas for hud to appear above gl canvas
4820
4957
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
@@ -4838,7 +4975,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4838
4975
  }
4839
4976
  image.src = src;
4840
4977
  })
4841
- )
4978
+ );
4842
4979
 
4843
4980
  // draw splash screen
4844
4981
  showSplashScreen && promises.push(new Promise(resolve =>
@@ -4855,25 +4992,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4855
4992
  }));
4856
4993
 
4857
4994
  // load all of the images
4858
- Promise.all(promises).then(()=>
4859
- {
4860
- // start the engine
4861
- gameInit();
4862
- engineUpdate();
4863
- });
4864
- }
4865
-
4866
- // Called automatically by engine to setup render system
4867
- function enginePreRender()
4868
- {
4869
- // save canvas size
4870
- mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
4871
-
4872
- // disable smoothing for pixel art
4873
- mainContext.imageSmoothingEnabled = !canvasPixelated;
4874
-
4875
- // setup gl rendering if enabled
4876
- glEnable && glPreRender();
4995
+ Promise.all(promises).then(startEngine);
4877
4996
  }
4878
4997
 
4879
4998
  /** Update each engine object, remove destroyed objects, and update time
@@ -4894,7 +5013,14 @@ function engineObjectsUpdate()
4894
5013
  }
4895
5014
  }
4896
5015
  for (const o of engineObjects)
4897
- o.parent || updateObject(o);
5016
+ {
5017
+ // update top level objects
5018
+ if (!o.parent)
5019
+ {
5020
+ updateObject(o);
5021
+ o.updateTransforms();
5022
+ }
5023
+ }
4898
5024
 
4899
5025
  // remove destroyed objects
4900
5026
  engineObjects = engineObjects.filter(o=>!o.destroyed);
@@ -4909,30 +5035,63 @@ function engineObjectsDestroy()
4909
5035
  engineObjects = engineObjects.filter(o=>!o.destroyed);
4910
5036
  }
4911
5037
 
4912
- /** Triggers a callback for each object within a given area
4913
- * @param {Vector2} [pos] - Center of test area
5038
+ /** Collects all object within a given area
5039
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
4914
5040
  * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
4915
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
4916
5041
  * @param {Array} [objects=engineObjects] - List of objects to check
5042
+ * @return {Array} - List of collected objects
4917
5043
  * @memberof Engine */
4918
- function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
5044
+ function engineObjectsCollect(pos, size, objects=engineObjects)
4919
5045
  {
5046
+ const collectedObjects = [];
4920
5047
  if (!pos) // all objects
4921
5048
  {
4922
5049
  for (const o of objects)
4923
- callbackFunction(o);
5050
+ collectedObjects.push(o);
4924
5051
  }
4925
- else if (typeof size === 'object') // bounding box test
5052
+ else if (size instanceof Vector2) // bounding box test
4926
5053
  {
4927
5054
  for (const o of objects)
4928
- isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
5055
+ isOverlapping(pos, size, o.pos, o.size) && collectedObjects.push(o);
4929
5056
  }
4930
5057
  else // circle test
4931
5058
  {
4932
5059
  const sizeSquared = size*size;
4933
5060
  for (const o of objects)
4934
- pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
5061
+ pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
4935
5062
  }
5063
+ return collectedObjects;
5064
+ }
5065
+
5066
+ /** Triggers a callback for each object within a given area
5067
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
5068
+ * @param {Number|Vector2} [size] - Radius of circle if float, rectangle size if Vector2
5069
+ * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
5070
+ * @param {Array} [objects=engineObjects] - List of objects to check
5071
+ * @memberof Engine */
5072
+ function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
5073
+ { engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
5074
+
5075
+ /** Return a list of objects intersecting a ray
5076
+ * @param {Vector2} start
5077
+ * @param {Vector2} end
5078
+ * @param {Array} [objects=engineObjects] - List of objects to check
5079
+ * @return {Array} - List of objects hit
5080
+ * @memberof Engine */
5081
+ function engineObjectsRaycast(start, end, objects=engineObjects)
5082
+ {
5083
+ const hitObjects = [];
5084
+ for (const o of objects)
5085
+ {
5086
+ if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
5087
+ {
5088
+ debugRaycast && debugRect(o.pos, o.size, '#f00');
5089
+ hitObjects.push(o);
5090
+ }
5091
+ }
5092
+
5093
+ debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
5094
+ return hitObjects;
4936
5095
  }
4937
5096
 
4938
5097
  ///////////////////////////////////////////////////////////////////////////////
@@ -5020,16 +5179,16 @@ function drawEngineSplashScreen(t)
5020
5179
  rect(37,14,9,6);
5021
5180
 
5022
5181
  // big stack
5023
- rect(50,20,10,-10,color(0,1));
5024
- rect(50,20,6.5,-10,color(0,2));
5025
- rect(50,20,3.5,-10,color(0,3));
5026
- rect(50,20,10,-10);
5027
- circle(55,2,11.4,.5,PI-.5,color(3,3));
5028
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
5029
- circle(55,2,11.4,.5,PI-.5);
5030
- rect(45,7,20,-7,color(0,2));
5031
- rect(45,0,20,3,color(0,3));
5032
- rect(45,0,20,7);
5182
+ rect(50,20,10,-8,color(0,1))
5183
+ rect(50,20,6.5,-8,color(0,2))
5184
+ rect(50,20,3.5,-8,color(0,3))
5185
+ rect(50,20,10,-8)
5186
+ circle(55,2,11.4,.5,PI-.5,color(3,3))
5187
+ circle(55,2,11.4,.5,PI/2,color(3,2),1)
5188
+ circle(55,2,11.4,.5,PI-.5)
5189
+ rect(45,7,20,-7,color(0,2))
5190
+ rect(45,-1,20,4,color(0,3))
5191
+ rect(45,-1,20,8)
5033
5192
 
5034
5193
  // engine
5035
5194
  for (let i=5; i--;)