littlejsengine 1.9.5 → 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()
@@ -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
@@ -692,7 +691,7 @@ class RandomGenerator
692
691
  this.seed ^= this.seed << 13;
693
692
  this.seed ^= this.seed >>> 17;
694
693
  this.seed ^= this.seed << 5;
695
- return valueB + (valueA - valueB) * abs(this.seed % 1e9) / 1e9;
694
+ return valueB + (valueA - valueB) * abs(this.seed % 1e8) / 1e8;
696
695
  }
697
696
 
698
697
  /** Returns a floored seeded random value the two values passed in
@@ -703,7 +702,7 @@ class RandomGenerator
703
702
 
704
703
  /** Randomly returns either -1 or 1 deterministically
705
704
  * @return {Number} */
706
- sign() { return this.int(2) * 2 - 1; }
705
+ sign() { return this.float() > .5 ? 1 : -1; }
707
706
  }
708
707
 
709
708
  ///////////////////////////////////////////////////////////////////////////////
@@ -751,6 +750,7 @@ class Vector2
751
750
  * @param {Number} [y] - Y axis location */
752
751
  constructor(x=0, y=0)
753
752
  {
753
+ ASSERT(typeof x === 'number' && typeof y === 'number');
754
754
  /** @property {Number} - X axis location */
755
755
  this.x = x;
756
756
  /** @property {Number} - Y axis location */
@@ -1077,12 +1077,14 @@ class Color
1077
1077
  * @return {Color} */
1078
1078
  setHSLA(h=0, s=0, l=1, a=1)
1079
1079
  {
1080
+ h = mod(h,1);
1081
+ s = clamp(s);
1082
+ l = clamp(l);
1080
1083
  const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
1081
1084
  f = (p, q, t)=>
1082
- (t = ((t%1)+1)%1) < 1/6 ? p+(q-p)*6*t :
1083
- t < 1/2 ? q :
1084
- t < 2/3 ? p+(q-p)*(2/3-t)*6 : p;
1085
-
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;
1086
1088
  this.r = f(p, q, h + 1/3);
1087
1089
  this.g = f(p, q, h);
1088
1090
  this.b = f(p, q, h - 1/3);
@@ -1133,13 +1135,12 @@ class Color
1133
1135
  ).clamp();
1134
1136
  }
1135
1137
 
1136
- /** Returns this color expressed as a hex color code
1138
+ /** Returns this color expressed as a rgb color code
1137
1139
  * @param {Boolean} [useAlpha] - if alpha should be included in result
1138
1140
  * @return {String} */
1139
- toString(useAlpha = true)
1140
- {
1141
- const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
1142
- 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})`;
1143
1144
  }
1144
1145
 
1145
1146
  /** Set this color from a hex code
@@ -1197,11 +1198,11 @@ class Timer
1197
1198
 
1198
1199
  /** Returns true if set and has not elapsed
1199
1200
  * @return {Boolean} */
1200
- active() { return time <= this.time; }
1201
+ active() { return time < this.time; }
1201
1202
 
1202
1203
  /** Returns true if set and elapsed
1203
1204
  * @return {Boolean} */
1204
- elapsed() { return time > this.time; }
1205
+ elapsed() { return time >= this.time; }
1205
1206
 
1206
1207
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1207
1208
  * @return {Number} */
@@ -1276,6 +1277,12 @@ let fontDefault = 'arial';
1276
1277
  * @memberof Settings */
1277
1278
  let showSplashScreen = false;
1278
1279
 
1280
+ /** Disables all rendering, audio, and input for servers
1281
+ * @type {Boolean}
1282
+ * @default
1283
+ * @memberof Settings */
1284
+ let headlessMode = false;
1285
+
1279
1286
  ///////////////////////////////////////////////////////////////////////////////
1280
1287
  // WebGL settings
1281
1288
 
@@ -1304,7 +1311,7 @@ let tileSizeDefault = vec2(16);
1304
1311
  * @type {Number}
1305
1312
  * @default
1306
1313
  * @memberof Settings */
1307
- let tileFixBleedScale = .1;
1314
+ let tileFixBleedScale = .5;
1308
1315
 
1309
1316
  ///////////////////////////////////////////////////////////////////////////////
1310
1317
  // Object settings
@@ -1429,7 +1436,7 @@ let soundEnable = true;
1429
1436
  * @type {Number}
1430
1437
  * @default
1431
1438
  * @memberof Settings */
1432
- let soundVolume = .5;
1439
+ let soundVolume = .3;
1433
1440
 
1434
1441
  /** Default range where sound no longer plays
1435
1442
  * @type {Number}
@@ -1514,6 +1521,11 @@ function setFontDefault(font) { fontDefault = font; }
1514
1521
  * @memberof Settings */
1515
1522
  function setShowSplashScreen(show) { showSplashScreen = show; }
1516
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
+
1517
1529
  /** Set if webgl rendering is enabled
1518
1530
  * @param {Boolean} enable
1519
1531
  * @memberof Settings */
@@ -1783,18 +1795,30 @@ class EngineObject
1783
1795
  engineObjects.push(this);
1784
1796
  }
1785
1797
 
1786
- /** Update the object transform and physics, called automatically by engine once each frame */
1787
- update()
1798
+ /** Update the object transform, called automatically by engine even when paused */
1799
+ updateTransforms()
1788
1800
  {
1789
1801
  const parent = this.parent;
1790
1802
  if (parent)
1791
1803
  {
1792
1804
  // copy parent pos/angle
1793
- this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
1794
- this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
1795
- 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;
1796
1808
  }
1797
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
+
1798
1822
  // limit max speed to prevent missing collisions
1799
1823
  this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1800
1824
  this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
@@ -1809,8 +1833,7 @@ class EngineObject
1809
1833
  // physics sanity checks
1810
1834
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1811
1835
  ASSERT(this.damping >= 0 && this.damping <= 1);
1812
-
1813
- if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
1836
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
1814
1837
  return;
1815
1838
 
1816
1839
  const wasMovingDown = this.velocity.y < 0;
@@ -2140,6 +2163,9 @@ let drawCount;
2140
2163
  */
2141
2164
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
2142
2165
  {
2166
+ if (headlessMode)
2167
+ return new TileInfo;
2168
+
2143
2169
  // if size is a number, make it a vector
2144
2170
  if (typeof size === 'number')
2145
2171
  {
@@ -2173,9 +2199,9 @@ class TileInfo
2173
2199
  constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
2174
2200
  {
2175
2201
  /** @property {Vector2} - Top left corner of tile in pixels */
2176
- this.pos = pos;
2202
+ this.pos = pos.copy();
2177
2203
  /** @property {Vector2} - Size of tile in pixels */
2178
- this.size = size;
2204
+ this.size = size.copy();
2179
2205
  /** @property {Number} - Texture index to use */
2180
2206
  this.textureIndex = textureIndex;
2181
2207
  }
@@ -2267,7 +2293,7 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
2267
2293
  * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
2268
2294
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2269
2295
  * @param {Boolean} [screenSpace=false] - If true the pos and size are in screen space
2270
- * @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2296
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2271
2297
  * @memberof Draw */
2272
2298
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2273
2299
  angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
@@ -2340,7 +2366,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2340
2366
  * @param {Number} [angle]
2341
2367
  * @param {Boolean} [useWebGL=glEnable]
2342
2368
  * @param {Boolean} [screenSpace=false]
2343
- * @param {CanvasRenderingContext2D} [context]
2369
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2344
2370
  * @memberof Draw */
2345
2371
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2346
2372
  {
@@ -2354,7 +2380,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2354
2380
  * @param {Color} [color=(1,1,1,1)]
2355
2381
  * @param {Boolean} [useWebGL=glEnable]
2356
2382
  * @param {Boolean} [screenSpace=false]
2357
- * @param {CanvasRenderingContext2D} [context]
2383
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2358
2384
  * @memberof Draw */
2359
2385
  function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
2360
2386
  {
@@ -2370,7 +2396,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
2370
2396
  * @param {Boolean} mirror
2371
2397
  * @param {Function} drawFunction
2372
2398
  * @param {Boolean} [screenSpace=false]
2373
- * @param {CanvasRenderingContext2D} [context=mainContext]
2399
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2374
2400
  * @memberof Draw */
2375
2401
  function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
2376
2402
  {
@@ -2391,7 +2417,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
2391
2417
  /** Enable normal or additive blend mode
2392
2418
  * @param {Boolean} [additive]
2393
2419
  * @param {Boolean} [useWebGL=glEnable]
2394
- * @param {CanvasRenderingContext2D} [context=mainContext]
2420
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2395
2421
  * @memberof Draw */
2396
2422
  function setBlendMode(additive, useWebGL=glEnable, context)
2397
2423
  {
@@ -2416,7 +2442,7 @@ function setBlendMode(additive, useWebGL=glEnable, context)
2416
2442
  * @param {Color} [lineColor=(0,0,0,1)]
2417
2443
  * @param {CanvasTextAlign} [textAlign='center']
2418
2444
  * @param {String} [font=fontDefault]
2419
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2445
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2420
2446
  * @memberof Draw */
2421
2447
  function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
2422
2448
  {
@@ -2433,7 +2459,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
2433
2459
  * @param {Color} [lineColor=(0,0,0,1)]
2434
2460
  * @param {CanvasTextAlign} [textAlign]
2435
2461
  * @param {String} [font=fontDefault]
2436
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2462
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2437
2463
  * @memberof Draw */
2438
2464
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
2439
2465
  {
@@ -2476,7 +2502,7 @@ class FontImage
2476
2502
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
2477
2503
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
2478
2504
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
2479
- * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
2505
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
2480
2506
  */
2481
2507
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
2482
2508
  {
@@ -2684,7 +2710,7 @@ function gamepadWasReleased(button, gamepad=0)
2684
2710
  * @return {Vector2}
2685
2711
  * @memberof Input */
2686
2712
  function gamepadStick(stick, gamepad=0)
2687
- { return stickData[gamepad] ? stickData[gamepad][stick] || vec2() : vec2(); }
2713
+ { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
2688
2714
 
2689
2715
  ///////////////////////////////////////////////////////////////////////////////
2690
2716
  // Input update called by engine
@@ -2695,6 +2721,8 @@ let inputData = [[]];
2695
2721
 
2696
2722
  function inputUpdate()
2697
2723
  {
2724
+ if (headlessMode) return;
2725
+
2698
2726
  // clear input when lost focus (prevent stuck keys)
2699
2727
  isTouchDevice || document.hasFocus() || clearInput();
2700
2728
 
@@ -2707,6 +2735,8 @@ function inputUpdate()
2707
2735
 
2708
2736
  function inputUpdatePost()
2709
2737
  {
2738
+ if (headlessMode) return;
2739
+
2710
2740
  // clear input to prepare for next frame
2711
2741
  for (const deviceInputData of inputData)
2712
2742
  for (const i in deviceInputData)
@@ -2715,9 +2745,12 @@ function inputUpdatePost()
2715
2745
  }
2716
2746
 
2717
2747
  ///////////////////////////////////////////////////////////////////////////////
2718
- // Keyboard event handlers
2748
+ // Input event handlers
2719
2749
 
2750
+ function inputInit()
2720
2751
  {
2752
+ if (headlessMode) return;
2753
+
2721
2754
  onkeydown = (e)=>
2722
2755
  {
2723
2756
  if (debug && e.target != document.body) return;
@@ -2748,21 +2781,29 @@ function inputUpdatePost()
2748
2781
  c == 'KeyA' ? 'ArrowLeft' :
2749
2782
  c == 'KeyD' ? 'ArrowRight' : c : c;
2750
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();
2751
2801
  }
2752
2802
 
2753
- ///////////////////////////////////////////////////////////////////////////////
2754
- // Mouse event handlers
2755
-
2756
- onmousedown = (e)=> {isUsingGamepad = false; inputData[0][e.button] = 3; mousePosScreen = mouseToScreen(e); e.button && e.preventDefault();}
2757
- onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2758
- onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2759
- onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2760
- oncontextmenu = (e)=> false; // prevent right click menu
2761
-
2762
2803
  // convert a mouse or touch event position to screen space
2763
2804
  function mouseToScreen(mousePos)
2764
2805
  {
2765
- if (!mainCanvas)
2806
+ if (!mainCanvas || headlessMode)
2766
2807
  return vec2(); // fix bug that can occur if user clicks before page loads
2767
2808
 
2768
2809
  const rect = mainCanvas.getBoundingClientRect();
@@ -2774,7 +2815,7 @@ function mouseToScreen(mousePos)
2774
2815
  // Gamepad input
2775
2816
 
2776
2817
  // gamepad internal variables
2777
- const stickData = [];
2818
+ const gamepadStickData = [];
2778
2819
 
2779
2820
  // gamepads are updated by engine every frame automatically
2780
2821
  function gamepadsUpdate()
@@ -2791,14 +2832,11 @@ function gamepadsUpdate()
2791
2832
  // update touch gamepad if enabled
2792
2833
  if (touchGamepadEnable && isTouchDevice)
2793
2834
  {
2794
- // create the touch gamepad if it doesn't exist
2795
- if (!touchGamepadButtons)
2796
- createTouchGamepad();
2797
-
2835
+ ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
2798
2836
  if (touchGamepadTimer.isSet())
2799
2837
  {
2800
2838
  // read virtual analog stick
2801
- const sticks = stickData[0] || (stickData[0] = []);
2839
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
2802
2840
  sticks[0] = vec2();
2803
2841
  if (touchGamepadAnalog)
2804
2842
  sticks[0] = applyDeadZones(touchGamepadStick);
@@ -2815,7 +2853,8 @@ function gamepadsUpdate()
2815
2853
  for (let i=10; i--;)
2816
2854
  {
2817
2855
  const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
2818
- 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;
2819
2858
  }
2820
2859
  }
2821
2860
  }
@@ -2835,7 +2874,7 @@ function gamepadsUpdate()
2835
2874
  // get or create gamepad data
2836
2875
  const gamepad = gamepads[i];
2837
2876
  const data = inputData[i+1] || (inputData[i+1] = []);
2838
- const sticks = stickData[i] || (stickData[i] = []);
2877
+ const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
2839
2878
 
2840
2879
  if (gamepad)
2841
2880
  {
@@ -2874,28 +2913,44 @@ function gamepadsUpdate()
2874
2913
  * @param {Number|Array} [pattern] - single value in ms or vibration interval array
2875
2914
  * @memberof Input */
2876
2915
  function vibrate(pattern=100)
2877
- { vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2916
+ { vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2878
2917
 
2879
2918
  /** Cancel any ongoing vibration
2880
2919
  * @memberof Input */
2881
2920
  function vibrateStop() { vibrate(0); }
2882
2921
 
2883
2922
  ///////////////////////////////////////////////////////////////////////////////
2884
- // Touch input
2923
+ // Touch input & virtual on screen gamepad
2885
2924
 
2886
2925
  /** True if a touch device has been detected
2887
2926
  * @memberof Input */
2888
- 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;
2889
2931
 
2890
2932
  // try to enable touch mouse
2891
- if (isTouchDevice)
2933
+ function touchInputInit()
2892
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
+
2893
2948
  // override mouse events
2894
- let wasTouching;
2895
2949
  onmousedown = onmouseup = ()=> 0;
2896
2950
 
2897
2951
  // handle all touch events the same way
2898
- ontouchstart = ontouchmove = ontouchend = (e)=>
2952
+ let wasTouching;
2953
+ function handleTouchDefault(e)
2899
2954
  {
2900
2955
  // fix stalled audio requiring user interaction
2901
2956
  if (soundEnable && audioContext && audioContext.state != 'running')
@@ -2924,27 +2979,14 @@ if (isTouchDevice)
2924
2979
  // must return true so the document will get focus
2925
2980
  return true;
2926
2981
  }
2927
- }
2928
2982
 
2929
- ///////////////////////////////////////////////////////////////////////////////
2930
- // touch gamepad, virtual on screen gamepad emulator for touch devices
2931
-
2932
- // touch input internal variables
2933
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2934
-
2935
- // create the touch gamepad, called automatically by the engine
2936
- function createTouchGamepad()
2937
- {
2938
- // touch input internal variables
2939
- touchGamepadButtons = [];
2940
- touchGamepadStick = vec2();
2941
-
2942
- const touchHandler = ontouchstart;
2943
- ontouchstart = ontouchmove = ontouchend = (e)=>
2983
+ // special handling for virtual gamepad mode
2984
+ function handleTouchGamepad(e)
2944
2985
  {
2945
2986
  // clear touch gamepad input
2946
2987
  touchGamepadStick = vec2();
2947
2988
  touchGamepadButtons = [];
2989
+ isUsingGamepad = true;
2948
2990
 
2949
2991
  const touching = e.touches.length;
2950
2992
  if (touching)
@@ -2985,9 +3027,8 @@ function createTouchGamepad()
2985
3027
  }
2986
3028
  }
2987
3029
 
2988
- // call default touch handler and set to using gamepad
2989
- touchHandler.bind(window)(e);
2990
- isUsingGamepad = true;
3030
+ // call default touch handler so normal touch events still work
3031
+ handleTouchDefault(e);
2991
3032
 
2992
3033
  // must return true so the document will get focus
2993
3034
  return true;
@@ -3006,32 +3047,33 @@ function touchGamepadRender()
3006
3047
  return;
3007
3048
 
3008
3049
  // setup the canvas
3009
- overlayContext.save();
3010
- overlayContext.globalAlpha = alpha*touchGamepadAlpha;
3011
- overlayContext.strokeStyle = '#fff';
3012
- overlayContext.lineWidth = 3;
3050
+ const context = overlayContext;
3051
+ context.save();
3052
+ context.globalAlpha = alpha*touchGamepadAlpha;
3053
+ context.strokeStyle = '#fff';
3054
+ context.lineWidth = 3;
3013
3055
 
3014
3056
  // draw left analog stick
3015
- overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
3016
- overlayContext.beginPath();
3057
+ context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
3058
+ context.beginPath();
3017
3059
 
3018
3060
  const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
3019
3061
  if (touchGamepadAnalog) // draw circle shaped gamepad
3020
3062
  {
3021
- overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
3022
- overlayContext.fill();
3023
- overlayContext.stroke();
3063
+ context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
3064
+ context.fill();
3065
+ context.stroke();
3024
3066
  }
3025
3067
  else // draw cross shaped gamepad
3026
3068
  {
3027
3069
  for(let i=10; i--;)
3028
3070
  {
3029
3071
  const angle = i*PI/4;
3030
- overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
3031
- i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
3032
- 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();
3033
3075
  }
3034
- overlayContext.stroke();
3076
+ context.stroke();
3035
3077
  }
3036
3078
 
3037
3079
  // draw right face buttons
@@ -3039,15 +3081,15 @@ function touchGamepadRender()
3039
3081
  for (let i=4; i--;)
3040
3082
  {
3041
3083
  const pos = rightCenter.add(vec2().setDirection(i, touchGamepadSize/2));
3042
- overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
3043
- overlayContext.beginPath();
3044
- overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
3045
- overlayContext.fill();
3046
- 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();
3047
3089
  }
3048
3090
 
3049
3091
  // set canvas back to normal
3050
- overlayContext.restore();
3092
+ context.restore();
3051
3093
  }
3052
3094
  /**
3053
3095
  * LittleJS Audio System
@@ -3082,7 +3124,7 @@ class Sound
3082
3124
  */
3083
3125
  constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
3084
3126
  {
3085
- if (!soundEnable) return;
3127
+ if (!soundEnable || headlessMode) return;
3086
3128
 
3087
3129
  /** @property {Number} - World space max range of sound, will not play if camera is farther away */
3088
3130
  this.range = range;
@@ -3096,8 +3138,8 @@ class Sound
3096
3138
  if (zzfxSound)
3097
3139
  {
3098
3140
  // generate zzfx sound now for fast playback
3099
- this.randomness = zzfxSound[1] || 0;
3100
- zzfxSound[1] = 0; // generate without randomness
3141
+ const defaultRandomness = .05;
3142
+ this.randomness = zzfxSound[1] || defaultRandomness;
3101
3143
  this.sampleChannels = [zzfxG(...zzfxSound)];
3102
3144
  this.sampleRate = zzfxR;
3103
3145
  }
@@ -3113,7 +3155,7 @@ class Sound
3113
3155
  */
3114
3156
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
3115
3157
  {
3116
- if (!soundEnable || !this.sampleChannels) return;
3158
+ if (!soundEnable || !this.sampleChannels || headlessMode) return;
3117
3159
 
3118
3160
  let pan;
3119
3161
  if (pos)
@@ -3196,7 +3238,9 @@ class SoundWave extends Sound
3196
3238
  super(undefined, range, taper);
3197
3239
  this.randomness = randomness;
3198
3240
 
3199
- if (!soundEnable) return;
3241
+ if (!soundEnable || headlessMode) return;
3242
+ if (!audioContext)
3243
+ audioContext = new AudioContext; // create audio context
3200
3244
 
3201
3245
  fetch(filename)
3202
3246
  .then(response => response.arrayBuffer())
@@ -3250,7 +3294,7 @@ class Music extends Sound
3250
3294
  {
3251
3295
  super(undefined);
3252
3296
 
3253
- if (!soundEnable) return;
3297
+ if (!soundEnable || headlessMode) return;
3254
3298
  this.randomness = 0;
3255
3299
  this.sampleChannels = zzfxM(...zzfxMusic);
3256
3300
  this.sampleRate = zzfxR;
@@ -3273,7 +3317,7 @@ class Music extends Sound
3273
3317
  * @memberof Audio */
3274
3318
  function playAudioFile(filename, volume=1, loop=false)
3275
3319
  {
3276
- if (!soundEnable) return;
3320
+ if (!soundEnable || headlessMode) return;
3277
3321
 
3278
3322
  const audio = new Audio(filename);
3279
3323
  audio.volume = soundVolume * volume;
@@ -3292,7 +3336,7 @@ function playAudioFile(filename, volume=1, loop=false)
3292
3336
  * @memberof Audio */
3293
3337
  function speak(text, language='', volume=1, rate=1, pitch=1)
3294
3338
  {
3295
- if (!soundEnable || !speechSynthesis) return;
3339
+ if (!soundEnable || !speechSynthesis || headlessMode) return;
3296
3340
 
3297
3341
  // common languages (not supported by all browsers)
3298
3342
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -3325,7 +3369,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
3325
3369
  /** Audio context used by the engine
3326
3370
  * @type {AudioContext}
3327
3371
  * @memberof Audio */
3328
- let audioContext = new AudioContext;
3372
+ let audioContext;
3329
3373
 
3330
3374
  /** Keep track if audio was suspended when last sound was played
3331
3375
  * @type {Boolean}
@@ -3343,7 +3387,9 @@ let audioSuspended = false;
3343
3387
  * @memberof Audio */
3344
3388
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
3345
3389
  {
3346
- if (!soundEnable) return;
3390
+ if (!soundEnable || headlessMode) return;
3391
+ if (!audioContext)
3392
+ audioContext = new AudioContext; // create audio context
3347
3393
 
3348
3394
  // prevent sounds from building up if they can't be played
3349
3395
  const audioWasSuspended = audioSuspended;
@@ -3389,7 +3435,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3389
3435
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
3390
3436
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3391
3437
  * @memberof Audio */
3392
- function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
3438
+ function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
3393
3439
 
3394
3440
  /** Sample rate used for all ZzFX sounds
3395
3441
  * @default 44100
@@ -3398,7 +3444,7 @@ const zzfxR = 44100;
3398
3444
 
3399
3445
  /** Generate samples for a ZzFX sound
3400
3446
  * @param {Number} [volume] - Volume scale (percent)
3401
- * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
3447
+ * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
3402
3448
  * @param {Number} [frequency] - Frequency of sound (Hz)
3403
3449
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
3404
3450
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -3424,17 +3470,18 @@ const zzfxR = 44100;
3424
3470
  function zzfxG
3425
3471
  (
3426
3472
  // parameters
3427
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3473
+ volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
3428
3474
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3429
3475
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3430
3476
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3431
3477
  )
3432
3478
  {
3479
+ // LJS Note: ZZFX modded so randomness is handled by Sound class
3480
+
3433
3481
  // init parameters
3434
3482
  let PI2 = PI*2, sampleRate = zzfxR,
3435
3483
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3436
- startFrequency = frequency *=
3437
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3484
+ startFrequency = frequency *= PI2 / sampleRate,
3438
3485
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3439
3486
 
3440
3487
  // biquad LP/HP filter
@@ -3456,7 +3503,6 @@ function zzfxG
3456
3503
  pitchJump *= PI2 / sampleRate;
3457
3504
  pitchJumpTime *= sampleRate;
3458
3505
  repeatTime = repeatTime * sampleRate | 0;
3459
- volume *= soundVolume;
3460
3506
 
3461
3507
  // generate waveform
3462
3508
  for(length = attack + decay + sustain + release + delay | 0;
@@ -3802,7 +3848,7 @@ class TileLayer extends EngineObject
3802
3848
 
3803
3849
  /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3804
3850
  this.canvas = document.createElement('canvas');
3805
- /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3851
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3806
3852
  this.context = this.canvas.getContext('2d');
3807
3853
  /** @property {Vector2} - How much to scale this layer when rendered */
3808
3854
  this.scale = scale;
@@ -3813,6 +3859,17 @@ class TileLayer extends EngineObject
3813
3859
  this.data = [];
3814
3860
  for (let j = this.size.area(); j--;)
3815
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
+ }
3816
3873
  }
3817
3874
 
3818
3875
  /** Set data at a given position in the array
@@ -3843,7 +3900,7 @@ class TileLayer extends EngineObject
3843
3900
  ASSERT(mainContext != this.context, 'must call redrawEnd() after drawing tiles');
3844
3901
 
3845
3902
  // flush and copy gl canvas because tile canvas does not use webgl
3846
- glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3903
+ !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3847
3904
 
3848
3905
  // draw the entire cached level onto the canvas
3849
3906
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
@@ -3893,14 +3950,14 @@ class TileLayer extends EngineObject
3893
3950
  this.context.imageSmoothingEnabled = !canvasPixelated;
3894
3951
 
3895
3952
  // setup gl rendering if enabled
3896
- glEnable && glPreRender();
3953
+ glPreRender();
3897
3954
  }
3898
3955
 
3899
3956
  /** Call to end the redraw process */
3900
3957
  redrawEnd()
3901
3958
  {
3902
3959
  ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3903
- glEnable && glCopyToContext(mainContext, true);
3960
+ glCopyToContext(mainContext, true);
3904
3961
  //debugSaveCanvas(this.canvas);
3905
3962
 
3906
3963
  // set stuff back to normal
@@ -4225,20 +4282,21 @@ class ParticleEmitter extends EngineObject
4225
4282
  class Particle extends EngineObject
4226
4283
  {
4227
4284
  /**
4228
- * Create a particle with the given shis.colorStart = undefined;ettings
4229
- * @param {Vector2} position - World space position of the particle
4230
- * @param {TileInfo} [tileInfo] - Tile info to render particles
4231
- * @param {Number} [angle] - Angle to rotate the particle
4232
- * @param {Color} [colorStart] - Color at start of life
4233
- * @param {Color} [colorEnd] - Color at end of life
4234
- * @param {Number} [lifeTime] - How long to live for
4235
- * @param {Number} [sizeStart] - Angle to rotate the particle
4236
- * @param {Number} [sizeEnd] - Angle to rotate the particle
4237
- * @param {Number} [fadeRate] - Angle to rotate the particle
4238
- * @param {Boolean} [additive] - Angle to rotate the particle
4239
- * @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
4240
4298
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
4241
- * @param {Function} [destroyCallback] - Called when particle dies
4299
+ * @param {Function} [destroyCallback] - Callback when particle dies
4242
4300
  */
4243
4301
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
4244
4302
  )
@@ -4647,6 +4705,8 @@ let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData,
4647
4705
  // Initalize WebGL, called automatically by the engine
4648
4706
  function glInit()
4649
4707
  {
4708
+ if (!glEnable || headlessMode) return;
4709
+
4650
4710
  // create the canvas and textures
4651
4711
  glCanvas = document.createElement('canvas');
4652
4712
  glContext = glCanvas.getContext('webgl2');
@@ -4659,11 +4719,11 @@ function glInit()
4659
4719
  '#version 300 es\n' + // specify GLSL ES version
4660
4720
  'precision highp float;'+ // use highp for better accuracy
4661
4721
  'uniform mat4 m;'+ // transform matrix
4662
- 'in vec2 g;'+ // geometry
4663
- 'in vec4 p,u,c,a;'+ // position/size, uvs, color, additiveColor
4664
- 'in float r;'+ // rotation
4665
- 'out vec2 v;'+ // return uv, color, additiveColor
4666
- '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
4667
4727
  'void main(){'+ // shader entry point
4668
4728
  'vec2 s=(g-.5)*p.zw;'+ // get size offset
4669
4729
  'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
@@ -4673,10 +4733,10 @@ function glInit()
4673
4733
  ,
4674
4734
  '#version 300 es\n' + // specify GLSL ES version
4675
4735
  'precision highp float;'+ // use highp for better accuracy
4676
- 'in vec2 v;'+ // uv
4677
- 'in vec4 d,e;'+ // color, additiveColor
4678
4736
  'uniform sampler2D s;'+ // texture
4679
- '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
4680
4740
  'void main(){'+ // shader entry point
4681
4741
  'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
4682
4742
  '}' // end of shader
@@ -4698,9 +4758,11 @@ function glInit()
4698
4758
  // Setup render each frame, called automatically by engine
4699
4759
  function glPreRender()
4700
4760
  {
4761
+ if (!glEnable || headlessMode) return;
4762
+
4701
4763
  // clear and set to same size as main canvas
4702
4764
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4703
- glContext.clear(gl_COLOR_BUFFER_BIT);
4765
+ //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4704
4766
 
4705
4767
  // set up the shader
4706
4768
  glContext.useProgram(glShader);
@@ -4734,12 +4796,12 @@ function glPreRender()
4734
4796
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
4735
4797
  const p = vec2(-1).subtract(cameraPos.multiply(s));
4736
4798
  glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
4737
- new Float32Array([
4799
+ [
4738
4800
  s.x, 0, 0, 0,
4739
4801
  0, s.y, 0, 0,
4740
4802
  1, 1, 1, 1,
4741
4803
  p.x, p.y, 0, 0
4742
- ])
4804
+ ]
4743
4805
  );
4744
4806
  }
4745
4807
 
@@ -4750,7 +4812,7 @@ function glPreRender()
4750
4812
  function glSetTexture(texture)
4751
4813
  {
4752
4814
  // must flush cache with the old texture to set a new one
4753
- if (texture == glActiveTexture)
4815
+ if (headlessMode || texture == glActiveTexture)
4754
4816
  return;
4755
4817
 
4756
4818
  glFlush();
@@ -4810,7 +4872,6 @@ function glCreateTexture(image)
4810
4872
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
4811
4873
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
4812
4874
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
4813
-
4814
4875
  return texture;
4815
4876
  }
4816
4877
 
@@ -4834,12 +4895,12 @@ function glFlush()
4834
4895
  }
4835
4896
 
4836
4897
  /** Draw any sprites still in the buffer, copy to main canvas and clear
4837
- * @param {CanvasRenderingContext2D} context
4898
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4838
4899
  * @param {Boolean} [forceDraw]
4839
4900
  * @memberof WebGL */
4840
4901
  function glCopyToContext(context, forceDraw=false)
4841
4902
  {
4842
- if (!glInstanceCount && !forceDraw) return;
4903
+ if (!glEnable || !glInstanceCount && !forceDraw) return;
4843
4904
 
4844
4905
  glFlush();
4845
4906
 
@@ -4896,7 +4957,7 @@ let glPostShader, glPostTexture, glPostIncludeOverlay;
4896
4957
  function glInitPostProcess(shaderCode, includeOverlay=false)
4897
4958
  {
4898
4959
  ASSERT(!glPostShader, 'can only have 1 post effects shader');
4899
-
4960
+ if (headlessMode) return;
4900
4961
  if (!shaderCode) // default shader pass through
4901
4962
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
4902
4963
 
@@ -4935,8 +4996,7 @@ function glInitPostProcess(shaderCode, includeOverlay=false)
4935
4996
  // Render the post processing shader, called automatically by the engine
4936
4997
  function glRenderPostProcess()
4937
4998
  {
4938
- if (!glPostShader)
4939
- return;
4999
+ if (!glPostShader || headlessMode) return;
4940
5000
 
4941
5001
  // prepare to render post process shader
4942
5002
  if (glEnable)
@@ -5042,7 +5102,7 @@ const engineName = 'LittleJS';
5042
5102
  * @type {String}
5043
5103
  * @default
5044
5104
  * @memberof Engine */
5045
- const engineVersion = '1.9.5';
5105
+ const engineVersion = '1.9.6';
5046
5106
 
5047
5107
  /** Frames per second to update
5048
5108
  * @type {Number}
@@ -5119,7 +5179,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5119
5179
  mainContext.imageSmoothingEnabled = !canvasPixelated;
5120
5180
 
5121
5181
  // setup gl rendering if enabled
5122
- glEnable && glPreRender();
5182
+ glPreRender();
5123
5183
  }
5124
5184
 
5125
5185
  // internal update loop for engine
@@ -5138,11 +5198,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5138
5198
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
5139
5199
  if (!debugSpeedUp)
5140
5200
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
5201
+
5141
5202
  updateCanvas();
5142
5203
 
5143
5204
  if (paused)
5144
5205
  {
5145
- // do post update even when paused
5206
+ // update object transforms even when paused
5207
+ for (const o of engineObjects)
5208
+ o.parent || o.updateTransforms();
5146
5209
  inputUpdate();
5147
5210
  debugUpdate();
5148
5211
  gameUpdatePost();
@@ -5154,7 +5217,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5154
5217
  let deltaSmooth = 0;
5155
5218
  if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
5156
5219
  {
5157
- // 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
5158
5221
  deltaSmooth = frameTimeBufferMS;
5159
5222
  frameTimeBufferMS = 0;
5160
5223
  }
@@ -5179,34 +5242,37 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5179
5242
  // add the time smoothing back in
5180
5243
  frameTimeBufferMS += deltaSmooth;
5181
5244
  }
5182
-
5183
- // render sort then render while removing destroyed objects
5184
- enginePreRender();
5185
- gameRender();
5186
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
5187
- for (const o of engineObjects)
5188
- o.destroyed || o.render();
5189
- gameRenderPost();
5190
- glRenderPostProcess();
5191
- medalsRender();
5192
- touchGamepadRender();
5193
- debugRender();
5194
- glEnable && glCopyToContext(mainContext);
5195
-
5196
- if (showWatermark)
5245
+
5246
+ if (!headlessMode)
5197
5247
  {
5198
- // update fps
5199
- overlayContext.textAlign = 'right';
5200
- overlayContext.textBaseline = 'top';
5201
- overlayContext.font = '1em monospace';
5202
- overlayContext.fillStyle = '#000';
5203
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
5204
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
5205
- + (glEnable ? ' GL' : ' 2D') ;
5206
- overlayContext.fillText(text, mainCanvas.width-3, 3);
5207
- overlayContext.fillStyle = '#fff';
5208
- overlayContext.fillText(text, mainCanvas.width-2, 2);
5209
- 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
+ }
5210
5276
  }
5211
5277
 
5212
5278
  requestAnimationFrame(engineUpdate);
@@ -5214,6 +5280,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5214
5280
 
5215
5281
  function updateCanvas()
5216
5282
  {
5283
+ if (headlessMode) return;
5284
+
5217
5285
  if (canvasFixedSize.x)
5218
5286
  {
5219
5287
  // clear canvas and set fixed size
@@ -5241,8 +5309,20 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5241
5309
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5242
5310
  }
5243
5311
 
5312
+ function startEngine()
5313
+ {
5314
+ gameInit();
5315
+ engineUpdate();
5316
+ }
5317
+
5318
+ if (headlessMode)
5319
+ {
5320
+ startEngine();
5321
+ return;
5322
+ }
5323
+
5244
5324
  // setup html
5245
- const styleBody =
5325
+ const styleBody =
5246
5326
  'margin:0;overflow:hidden;' + // fill the window
5247
5327
  'background:#000;' + // set background color
5248
5328
  'touch-action:none;' + // prevent mobile pinch to resize
@@ -5254,8 +5334,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5254
5334
  mainContext = mainCanvas.getContext('2d');
5255
5335
 
5256
5336
  // init stuff and start engine
5337
+ inputInit();
5257
5338
  debugInit();
5258
- glEnable && glInit();
5339
+ glInit();
5259
5340
 
5260
5341
  // create overlay canvas for hud to appear above gl canvas
5261
5342
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
@@ -5296,12 +5377,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5296
5377
  }));
5297
5378
 
5298
5379
  // load all of the images
5299
- Promise.all(promises).then(()=>
5300
- {
5301
- // start the engine
5302
- gameInit();
5303
- engineUpdate();
5304
- });
5380
+ Promise.all(promises).then(startEngine);
5305
5381
  }
5306
5382
 
5307
5383
  /** Update each engine object, remove destroyed objects, and update time
@@ -5322,7 +5398,14 @@ function engineObjectsUpdate()
5322
5398
  }
5323
5399
  }
5324
5400
  for (const o of engineObjects)
5325
- o.parent || updateObject(o);
5401
+ {
5402
+ // update top level objects
5403
+ if (!o.parent)
5404
+ {
5405
+ updateObject(o);
5406
+ o.updateTransforms();
5407
+ }
5408
+ }
5326
5409
 
5327
5410
  // remove destroyed objects
5328
5411
  engineObjects = engineObjects.filter(o=>!o.destroyed);
@@ -5608,6 +5691,7 @@ export {
5608
5691
  canvasPixelated,
5609
5692
  fontDefault,
5610
5693
  showSplashScreen,
5694
+ headlessMode,
5611
5695
  tileSizeDefault,
5612
5696
  tileFixBleedScale,
5613
5697
  enablePhysicsSolver,
@@ -5646,6 +5730,7 @@ export {
5646
5730
  setCanvasPixelated,
5647
5731
  setFontDefault,
5648
5732
  setShowSplashScreen,
5733
+ setHeadlessMode,
5649
5734
  setGlEnable,
5650
5735
  setGlOverlay,
5651
5736
  setTileSizeDefault,