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.
@@ -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
@@ -306,7 +306,7 @@ class RandomGenerator
306
306
  this.seed ^= this.seed << 13;
307
307
  this.seed ^= this.seed >>> 17;
308
308
  this.seed ^= this.seed << 5;
309
- return valueB + (valueA - valueB) * abs(this.seed % 1e9) / 1e9;
309
+ return valueB + (valueA - valueB) * abs(this.seed % 1e8) / 1e8;
310
310
  }
311
311
 
312
312
  /** Returns a floored seeded random value the two values passed in
@@ -317,7 +317,7 @@ class RandomGenerator
317
317
 
318
318
  /** Randomly returns either -1 or 1 deterministically
319
319
  * @return {Number} */
320
- sign() { return this.int(2) * 2 - 1; }
320
+ sign() { return this.float() > .5 ? 1 : -1; }
321
321
  }
322
322
 
323
323
  ///////////////////////////////////////////////////////////////////////////////
@@ -365,6 +365,7 @@ class Vector2
365
365
  * @param {Number} [y] - Y axis location */
366
366
  constructor(x=0, y=0)
367
367
  {
368
+ ASSERT(typeof x === 'number' && typeof y === 'number');
368
369
  /** @property {Number} - X axis location */
369
370
  this.x = x;
370
371
  /** @property {Number} - Y axis location */
@@ -691,12 +692,14 @@ class Color
691
692
  * @return {Color} */
692
693
  setHSLA(h=0, s=0, l=1, a=1)
693
694
  {
695
+ h = mod(h,1);
696
+ s = clamp(s);
697
+ l = clamp(l);
694
698
  const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
695
699
  f = (p, q, t)=>
696
- (t = ((t%1)+1)%1) < 1/6 ? p+(q-p)*6*t :
697
- t < 1/2 ? q :
698
- t < 2/3 ? p+(q-p)*(2/3-t)*6 : p;
699
-
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;
700
703
  this.r = f(p, q, h + 1/3);
701
704
  this.g = f(p, q, h);
702
705
  this.b = f(p, q, h - 1/3);
@@ -747,13 +750,12 @@ class Color
747
750
  ).clamp();
748
751
  }
749
752
 
750
- /** Returns this color expressed as a hex color code
753
+ /** Returns this color expressed as a rgb color code
751
754
  * @param {Boolean} [useAlpha] - if alpha should be included in result
752
755
  * @return {String} */
753
- toString(useAlpha = true)
754
- {
755
- const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
756
- 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})`;
757
759
  }
758
760
 
759
761
  /** Set this color from a hex code
@@ -811,11 +813,11 @@ class Timer
811
813
 
812
814
  /** Returns true if set and has not elapsed
813
815
  * @return {Boolean} */
814
- active() { return time <= this.time; }
816
+ active() { return time < this.time; }
815
817
 
816
818
  /** Returns true if set and elapsed
817
819
  * @return {Boolean} */
818
- elapsed() { return time > this.time; }
820
+ elapsed() { return time >= this.time; }
819
821
 
820
822
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
821
823
  * @return {Number} */
@@ -890,6 +892,12 @@ let fontDefault = 'arial';
890
892
  * @memberof Settings */
891
893
  let showSplashScreen = false;
892
894
 
895
+ /** Disables all rendering, audio, and input for servers
896
+ * @type {Boolean}
897
+ * @default
898
+ * @memberof Settings */
899
+ let headlessMode = false;
900
+
893
901
  ///////////////////////////////////////////////////////////////////////////////
894
902
  // WebGL settings
895
903
 
@@ -918,7 +926,7 @@ let tileSizeDefault = vec2(16);
918
926
  * @type {Number}
919
927
  * @default
920
928
  * @memberof Settings */
921
- let tileFixBleedScale = .1;
929
+ let tileFixBleedScale = .5;
922
930
 
923
931
  ///////////////////////////////////////////////////////////////////////////////
924
932
  // Object settings
@@ -1043,7 +1051,7 @@ let soundEnable = true;
1043
1051
  * @type {Number}
1044
1052
  * @default
1045
1053
  * @memberof Settings */
1046
- let soundVolume = .5;
1054
+ let soundVolume = .3;
1047
1055
 
1048
1056
  /** Default range where sound no longer plays
1049
1057
  * @type {Number}
@@ -1128,6 +1136,11 @@ function setFontDefault(font) { fontDefault = font; }
1128
1136
  * @memberof Settings */
1129
1137
  function setShowSplashScreen(show) { showSplashScreen = show; }
1130
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
+
1131
1144
  /** Set if webgl rendering is enabled
1132
1145
  * @param {Boolean} enable
1133
1146
  * @memberof Settings */
@@ -1397,18 +1410,30 @@ class EngineObject
1397
1410
  engineObjects.push(this);
1398
1411
  }
1399
1412
 
1400
- /** Update the object transform and physics, called automatically by engine once each frame */
1401
- update()
1413
+ /** Update the object transform, called automatically by engine even when paused */
1414
+ updateTransforms()
1402
1415
  {
1403
1416
  const parent = this.parent;
1404
1417
  if (parent)
1405
1418
  {
1406
1419
  // copy parent pos/angle
1407
- this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
1408
- this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
1409
- 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;
1410
1423
  }
1411
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
+
1412
1437
  // limit max speed to prevent missing collisions
1413
1438
  this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1414
1439
  this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
@@ -1423,8 +1448,7 @@ class EngineObject
1423
1448
  // physics sanity checks
1424
1449
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1425
1450
  ASSERT(this.damping >= 0 && this.damping <= 1);
1426
-
1427
- if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
1451
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
1428
1452
  return;
1429
1453
 
1430
1454
  const wasMovingDown = this.velocity.y < 0;
@@ -1754,6 +1778,9 @@ let drawCount;
1754
1778
  */
1755
1779
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1756
1780
  {
1781
+ if (headlessMode)
1782
+ return new TileInfo;
1783
+
1757
1784
  // if size is a number, make it a vector
1758
1785
  if (typeof size === 'number')
1759
1786
  {
@@ -1787,9 +1814,9 @@ class TileInfo
1787
1814
  constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1788
1815
  {
1789
1816
  /** @property {Vector2} - Top left corner of tile in pixels */
1790
- this.pos = pos;
1817
+ this.pos = pos.copy();
1791
1818
  /** @property {Vector2} - Size of tile in pixels */
1792
- this.size = size;
1819
+ this.size = size.copy();
1793
1820
  /** @property {Number} - Texture index to use */
1794
1821
  this.textureIndex = textureIndex;
1795
1822
  }
@@ -1881,7 +1908,7 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
1881
1908
  * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
1882
1909
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1883
1910
  * @param {Boolean} [screenSpace=false] - If true the pos and size are in screen space
1884
- * @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
1911
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
1885
1912
  * @memberof Draw */
1886
1913
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
1887
1914
  angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
@@ -1954,7 +1981,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
1954
1981
  * @param {Number} [angle]
1955
1982
  * @param {Boolean} [useWebGL=glEnable]
1956
1983
  * @param {Boolean} [screenSpace=false]
1957
- * @param {CanvasRenderingContext2D} [context]
1984
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
1958
1985
  * @memberof Draw */
1959
1986
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
1960
1987
  {
@@ -1968,7 +1995,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
1968
1995
  * @param {Color} [color=(1,1,1,1)]
1969
1996
  * @param {Boolean} [useWebGL=glEnable]
1970
1997
  * @param {Boolean} [screenSpace=false]
1971
- * @param {CanvasRenderingContext2D} [context]
1998
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
1972
1999
  * @memberof Draw */
1973
2000
  function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
1974
2001
  {
@@ -1984,7 +2011,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
1984
2011
  * @param {Boolean} mirror
1985
2012
  * @param {Function} drawFunction
1986
2013
  * @param {Boolean} [screenSpace=false]
1987
- * @param {CanvasRenderingContext2D} [context=mainContext]
2014
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
1988
2015
  * @memberof Draw */
1989
2016
  function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
1990
2017
  {
@@ -2005,7 +2032,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
2005
2032
  /** Enable normal or additive blend mode
2006
2033
  * @param {Boolean} [additive]
2007
2034
  * @param {Boolean} [useWebGL=glEnable]
2008
- * @param {CanvasRenderingContext2D} [context=mainContext]
2035
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2009
2036
  * @memberof Draw */
2010
2037
  function setBlendMode(additive, useWebGL=glEnable, context)
2011
2038
  {
@@ -2030,7 +2057,7 @@ function setBlendMode(additive, useWebGL=glEnable, context)
2030
2057
  * @param {Color} [lineColor=(0,0,0,1)]
2031
2058
  * @param {CanvasTextAlign} [textAlign='center']
2032
2059
  * @param {String} [font=fontDefault]
2033
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2060
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2034
2061
  * @memberof Draw */
2035
2062
  function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
2036
2063
  {
@@ -2047,7 +2074,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
2047
2074
  * @param {Color} [lineColor=(0,0,0,1)]
2048
2075
  * @param {CanvasTextAlign} [textAlign]
2049
2076
  * @param {String} [font=fontDefault]
2050
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2077
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2051
2078
  * @memberof Draw */
2052
2079
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
2053
2080
  {
@@ -2090,7 +2117,7 @@ class FontImage
2090
2117
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
2091
2118
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
2092
2119
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
2093
- * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
2120
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
2094
2121
  */
2095
2122
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
2096
2123
  {
@@ -2298,7 +2325,7 @@ function gamepadWasReleased(button, gamepad=0)
2298
2325
  * @return {Vector2}
2299
2326
  * @memberof Input */
2300
2327
  function gamepadStick(stick, gamepad=0)
2301
- { return stickData[gamepad] ? stickData[gamepad][stick] || vec2() : vec2(); }
2328
+ { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
2302
2329
 
2303
2330
  ///////////////////////////////////////////////////////////////////////////////
2304
2331
  // Input update called by engine
@@ -2309,6 +2336,8 @@ let inputData = [[]];
2309
2336
 
2310
2337
  function inputUpdate()
2311
2338
  {
2339
+ if (headlessMode) return;
2340
+
2312
2341
  // clear input when lost focus (prevent stuck keys)
2313
2342
  isTouchDevice || document.hasFocus() || clearInput();
2314
2343
 
@@ -2321,6 +2350,8 @@ function inputUpdate()
2321
2350
 
2322
2351
  function inputUpdatePost()
2323
2352
  {
2353
+ if (headlessMode) return;
2354
+
2324
2355
  // clear input to prepare for next frame
2325
2356
  for (const deviceInputData of inputData)
2326
2357
  for (const i in deviceInputData)
@@ -2329,9 +2360,12 @@ function inputUpdatePost()
2329
2360
  }
2330
2361
 
2331
2362
  ///////////////////////////////////////////////////////////////////////////////
2332
- // Keyboard event handlers
2363
+ // Input event handlers
2333
2364
 
2365
+ function inputInit()
2334
2366
  {
2367
+ if (headlessMode) return;
2368
+
2335
2369
  onkeydown = (e)=>
2336
2370
  {
2337
2371
  if (debug && e.target != document.body) return;
@@ -2362,21 +2396,29 @@ function inputUpdatePost()
2362
2396
  c == 'KeyA' ? 'ArrowLeft' :
2363
2397
  c == 'KeyD' ? 'ArrowRight' : c : c;
2364
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();
2365
2416
  }
2366
2417
 
2367
- ///////////////////////////////////////////////////////////////////////////////
2368
- // Mouse event handlers
2369
-
2370
- onmousedown = (e)=> {isUsingGamepad = false; inputData[0][e.button] = 3; mousePosScreen = mouseToScreen(e); e.button && e.preventDefault();}
2371
- onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2372
- onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2373
- onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2374
- oncontextmenu = (e)=> false; // prevent right click menu
2375
-
2376
2418
  // convert a mouse or touch event position to screen space
2377
2419
  function mouseToScreen(mousePos)
2378
2420
  {
2379
- if (!mainCanvas)
2421
+ if (!mainCanvas || headlessMode)
2380
2422
  return vec2(); // fix bug that can occur if user clicks before page loads
2381
2423
 
2382
2424
  const rect = mainCanvas.getBoundingClientRect();
@@ -2388,7 +2430,7 @@ function mouseToScreen(mousePos)
2388
2430
  // Gamepad input
2389
2431
 
2390
2432
  // gamepad internal variables
2391
- const stickData = [];
2433
+ const gamepadStickData = [];
2392
2434
 
2393
2435
  // gamepads are updated by engine every frame automatically
2394
2436
  function gamepadsUpdate()
@@ -2405,14 +2447,11 @@ function gamepadsUpdate()
2405
2447
  // update touch gamepad if enabled
2406
2448
  if (touchGamepadEnable && isTouchDevice)
2407
2449
  {
2408
- // create the touch gamepad if it doesn't exist
2409
- if (!touchGamepadButtons)
2410
- createTouchGamepad();
2411
-
2450
+ ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
2412
2451
  if (touchGamepadTimer.isSet())
2413
2452
  {
2414
2453
  // read virtual analog stick
2415
- const sticks = stickData[0] || (stickData[0] = []);
2454
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
2416
2455
  sticks[0] = vec2();
2417
2456
  if (touchGamepadAnalog)
2418
2457
  sticks[0] = applyDeadZones(touchGamepadStick);
@@ -2429,7 +2468,8 @@ function gamepadsUpdate()
2429
2468
  for (let i=10; i--;)
2430
2469
  {
2431
2470
  const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
2432
- 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;
2433
2473
  }
2434
2474
  }
2435
2475
  }
@@ -2449,7 +2489,7 @@ function gamepadsUpdate()
2449
2489
  // get or create gamepad data
2450
2490
  const gamepad = gamepads[i];
2451
2491
  const data = inputData[i+1] || (inputData[i+1] = []);
2452
- const sticks = stickData[i] || (stickData[i] = []);
2492
+ const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
2453
2493
 
2454
2494
  if (gamepad)
2455
2495
  {
@@ -2488,28 +2528,44 @@ function gamepadsUpdate()
2488
2528
  * @param {Number|Array} [pattern] - single value in ms or vibration interval array
2489
2529
  * @memberof Input */
2490
2530
  function vibrate(pattern=100)
2491
- { vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2531
+ { vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2492
2532
 
2493
2533
  /** Cancel any ongoing vibration
2494
2534
  * @memberof Input */
2495
2535
  function vibrateStop() { vibrate(0); }
2496
2536
 
2497
2537
  ///////////////////////////////////////////////////////////////////////////////
2498
- // Touch input
2538
+ // Touch input & virtual on screen gamepad
2499
2539
 
2500
2540
  /** True if a touch device has been detected
2501
2541
  * @memberof Input */
2502
- 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;
2503
2546
 
2504
2547
  // try to enable touch mouse
2505
- if (isTouchDevice)
2548
+ function touchInputInit()
2506
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
+
2507
2563
  // override mouse events
2508
- let wasTouching;
2509
2564
  onmousedown = onmouseup = ()=> 0;
2510
2565
 
2511
2566
  // handle all touch events the same way
2512
- ontouchstart = ontouchmove = ontouchend = (e)=>
2567
+ let wasTouching;
2568
+ function handleTouchDefault(e)
2513
2569
  {
2514
2570
  // fix stalled audio requiring user interaction
2515
2571
  if (soundEnable && audioContext && audioContext.state != 'running')
@@ -2538,27 +2594,14 @@ if (isTouchDevice)
2538
2594
  // must return true so the document will get focus
2539
2595
  return true;
2540
2596
  }
2541
- }
2542
-
2543
- ///////////////////////////////////////////////////////////////////////////////
2544
- // touch gamepad, virtual on screen gamepad emulator for touch devices
2545
-
2546
- // touch input internal variables
2547
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2548
-
2549
- // create the touch gamepad, called automatically by the engine
2550
- function createTouchGamepad()
2551
- {
2552
- // touch input internal variables
2553
- touchGamepadButtons = [];
2554
- touchGamepadStick = vec2();
2555
2597
 
2556
- const touchHandler = ontouchstart;
2557
- ontouchstart = ontouchmove = ontouchend = (e)=>
2598
+ // special handling for virtual gamepad mode
2599
+ function handleTouchGamepad(e)
2558
2600
  {
2559
2601
  // clear touch gamepad input
2560
2602
  touchGamepadStick = vec2();
2561
2603
  touchGamepadButtons = [];
2604
+ isUsingGamepad = true;
2562
2605
 
2563
2606
  const touching = e.touches.length;
2564
2607
  if (touching)
@@ -2599,9 +2642,8 @@ function createTouchGamepad()
2599
2642
  }
2600
2643
  }
2601
2644
 
2602
- // call default touch handler and set to using gamepad
2603
- touchHandler.bind(window)(e);
2604
- isUsingGamepad = true;
2645
+ // call default touch handler so normal touch events still work
2646
+ handleTouchDefault(e);
2605
2647
 
2606
2648
  // must return true so the document will get focus
2607
2649
  return true;
@@ -2620,32 +2662,33 @@ function touchGamepadRender()
2620
2662
  return;
2621
2663
 
2622
2664
  // setup the canvas
2623
- overlayContext.save();
2624
- overlayContext.globalAlpha = alpha*touchGamepadAlpha;
2625
- overlayContext.strokeStyle = '#fff';
2626
- overlayContext.lineWidth = 3;
2665
+ const context = overlayContext;
2666
+ context.save();
2667
+ context.globalAlpha = alpha*touchGamepadAlpha;
2668
+ context.strokeStyle = '#fff';
2669
+ context.lineWidth = 3;
2627
2670
 
2628
2671
  // draw left analog stick
2629
- overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2630
- overlayContext.beginPath();
2672
+ context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2673
+ context.beginPath();
2631
2674
 
2632
2675
  const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2633
2676
  if (touchGamepadAnalog) // draw circle shaped gamepad
2634
2677
  {
2635
- overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2636
- overlayContext.fill();
2637
- overlayContext.stroke();
2678
+ context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2679
+ context.fill();
2680
+ context.stroke();
2638
2681
  }
2639
2682
  else // draw cross shaped gamepad
2640
2683
  {
2641
2684
  for(let i=10; i--;)
2642
2685
  {
2643
2686
  const angle = i*PI/4;
2644
- overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
2645
- i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
2646
- 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();
2647
2690
  }
2648
- overlayContext.stroke();
2691
+ context.stroke();
2649
2692
  }
2650
2693
 
2651
2694
  // draw right face buttons
@@ -2653,15 +2696,15 @@ function touchGamepadRender()
2653
2696
  for (let i=4; i--;)
2654
2697
  {
2655
2698
  const pos = rightCenter.add(vec2().setDirection(i, touchGamepadSize/2));
2656
- overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2657
- overlayContext.beginPath();
2658
- overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
2659
- overlayContext.fill();
2660
- 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();
2661
2704
  }
2662
2705
 
2663
2706
  // set canvas back to normal
2664
- overlayContext.restore();
2707
+ context.restore();
2665
2708
  }
2666
2709
  /**
2667
2710
  * LittleJS Audio System
@@ -2696,7 +2739,7 @@ class Sound
2696
2739
  */
2697
2740
  constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
2698
2741
  {
2699
- if (!soundEnable) return;
2742
+ if (!soundEnable || headlessMode) return;
2700
2743
 
2701
2744
  /** @property {Number} - World space max range of sound, will not play if camera is farther away */
2702
2745
  this.range = range;
@@ -2710,8 +2753,8 @@ class Sound
2710
2753
  if (zzfxSound)
2711
2754
  {
2712
2755
  // generate zzfx sound now for fast playback
2713
- this.randomness = zzfxSound[1] || 0;
2714
- zzfxSound[1] = 0; // generate without randomness
2756
+ const defaultRandomness = .05;
2757
+ this.randomness = zzfxSound[1] || defaultRandomness;
2715
2758
  this.sampleChannels = [zzfxG(...zzfxSound)];
2716
2759
  this.sampleRate = zzfxR;
2717
2760
  }
@@ -2727,7 +2770,7 @@ class Sound
2727
2770
  */
2728
2771
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
2729
2772
  {
2730
- if (!soundEnable || !this.sampleChannels) return;
2773
+ if (!soundEnable || !this.sampleChannels || headlessMode) return;
2731
2774
 
2732
2775
  let pan;
2733
2776
  if (pos)
@@ -2810,7 +2853,9 @@ class SoundWave extends Sound
2810
2853
  super(undefined, range, taper);
2811
2854
  this.randomness = randomness;
2812
2855
 
2813
- if (!soundEnable) return;
2856
+ if (!soundEnable || headlessMode) return;
2857
+ if (!audioContext)
2858
+ audioContext = new AudioContext; // create audio context
2814
2859
 
2815
2860
  fetch(filename)
2816
2861
  .then(response => response.arrayBuffer())
@@ -2864,7 +2909,7 @@ class Music extends Sound
2864
2909
  {
2865
2910
  super(undefined);
2866
2911
 
2867
- if (!soundEnable) return;
2912
+ if (!soundEnable || headlessMode) return;
2868
2913
  this.randomness = 0;
2869
2914
  this.sampleChannels = zzfxM(...zzfxMusic);
2870
2915
  this.sampleRate = zzfxR;
@@ -2887,7 +2932,7 @@ class Music extends Sound
2887
2932
  * @memberof Audio */
2888
2933
  function playAudioFile(filename, volume=1, loop=false)
2889
2934
  {
2890
- if (!soundEnable) return;
2935
+ if (!soundEnable || headlessMode) return;
2891
2936
 
2892
2937
  const audio = new Audio(filename);
2893
2938
  audio.volume = soundVolume * volume;
@@ -2906,7 +2951,7 @@ function playAudioFile(filename, volume=1, loop=false)
2906
2951
  * @memberof Audio */
2907
2952
  function speak(text, language='', volume=1, rate=1, pitch=1)
2908
2953
  {
2909
- if (!soundEnable || !speechSynthesis) return;
2954
+ if (!soundEnable || !speechSynthesis || headlessMode) return;
2910
2955
 
2911
2956
  // common languages (not supported by all browsers)
2912
2957
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -2939,7 +2984,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
2939
2984
  /** Audio context used by the engine
2940
2985
  * @type {AudioContext}
2941
2986
  * @memberof Audio */
2942
- let audioContext = new AudioContext;
2987
+ let audioContext;
2943
2988
 
2944
2989
  /** Keep track if audio was suspended when last sound was played
2945
2990
  * @type {Boolean}
@@ -2957,7 +3002,9 @@ let audioSuspended = false;
2957
3002
  * @memberof Audio */
2958
3003
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
2959
3004
  {
2960
- if (!soundEnable) return;
3005
+ if (!soundEnable || headlessMode) return;
3006
+ if (!audioContext)
3007
+ audioContext = new AudioContext; // create audio context
2961
3008
 
2962
3009
  // prevent sounds from building up if they can't be played
2963
3010
  const audioWasSuspended = audioSuspended;
@@ -3003,7 +3050,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3003
3050
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
3004
3051
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3005
3052
  * @memberof Audio */
3006
- function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
3053
+ function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
3007
3054
 
3008
3055
  /** Sample rate used for all ZzFX sounds
3009
3056
  * @default 44100
@@ -3012,7 +3059,7 @@ const zzfxR = 44100;
3012
3059
 
3013
3060
  /** Generate samples for a ZzFX sound
3014
3061
  * @param {Number} [volume] - Volume scale (percent)
3015
- * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
3062
+ * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
3016
3063
  * @param {Number} [frequency] - Frequency of sound (Hz)
3017
3064
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
3018
3065
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -3038,17 +3085,18 @@ const zzfxR = 44100;
3038
3085
  function zzfxG
3039
3086
  (
3040
3087
  // parameters
3041
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3088
+ volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
3042
3089
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3043
3090
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3044
3091
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3045
3092
  )
3046
3093
  {
3094
+ // LJS Note: ZZFX modded so randomness is handled by Sound class
3095
+
3047
3096
  // init parameters
3048
3097
  let PI2 = PI*2, sampleRate = zzfxR,
3049
3098
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3050
- startFrequency = frequency *=
3051
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3099
+ startFrequency = frequency *= PI2 / sampleRate,
3052
3100
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3053
3101
 
3054
3102
  // biquad LP/HP filter
@@ -3070,7 +3118,6 @@ function zzfxG
3070
3118
  pitchJump *= PI2 / sampleRate;
3071
3119
  pitchJumpTime *= sampleRate;
3072
3120
  repeatTime = repeatTime * sampleRate | 0;
3073
- volume *= soundVolume;
3074
3121
 
3075
3122
  // generate waveform
3076
3123
  for(length = attack + decay + sustain + release + delay | 0;
@@ -3416,7 +3463,7 @@ class TileLayer extends EngineObject
3416
3463
 
3417
3464
  /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3418
3465
  this.canvas = document.createElement('canvas');
3419
- /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3466
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3420
3467
  this.context = this.canvas.getContext('2d');
3421
3468
  /** @property {Vector2} - How much to scale this layer when rendered */
3422
3469
  this.scale = scale;
@@ -3427,6 +3474,17 @@ class TileLayer extends EngineObject
3427
3474
  this.data = [];
3428
3475
  for (let j = this.size.area(); j--;)
3429
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
+ }
3430
3488
  }
3431
3489
 
3432
3490
  /** Set data at a given position in the array
@@ -3457,7 +3515,7 @@ class TileLayer extends EngineObject
3457
3515
  ASSERT(mainContext != this.context, 'must call redrawEnd() after drawing tiles');
3458
3516
 
3459
3517
  // flush and copy gl canvas because tile canvas does not use webgl
3460
- glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3518
+ !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3461
3519
 
3462
3520
  // draw the entire cached level onto the canvas
3463
3521
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
@@ -3507,14 +3565,14 @@ class TileLayer extends EngineObject
3507
3565
  this.context.imageSmoothingEnabled = !canvasPixelated;
3508
3566
 
3509
3567
  // setup gl rendering if enabled
3510
- glEnable && glPreRender();
3568
+ glPreRender();
3511
3569
  }
3512
3570
 
3513
3571
  /** Call to end the redraw process */
3514
3572
  redrawEnd()
3515
3573
  {
3516
3574
  ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3517
- glEnable && glCopyToContext(mainContext, true);
3575
+ glCopyToContext(mainContext, true);
3518
3576
  //debugSaveCanvas(this.canvas);
3519
3577
 
3520
3578
  // set stuff back to normal
@@ -3839,20 +3897,21 @@ class ParticleEmitter extends EngineObject
3839
3897
  class Particle extends EngineObject
3840
3898
  {
3841
3899
  /**
3842
- * Create a particle with the given shis.colorStart = undefined;ettings
3843
- * @param {Vector2} position - World space position of the particle
3844
- * @param {TileInfo} [tileInfo] - Tile info to render particles
3845
- * @param {Number} [angle] - Angle to rotate the particle
3846
- * @param {Color} [colorStart] - Color at start of life
3847
- * @param {Color} [colorEnd] - Color at end of life
3848
- * @param {Number} [lifeTime] - How long to live for
3849
- * @param {Number} [sizeStart] - Angle to rotate the particle
3850
- * @param {Number} [sizeEnd] - Angle to rotate the particle
3851
- * @param {Number} [fadeRate] - Angle to rotate the particle
3852
- * @param {Boolean} [additive] - Angle to rotate the particle
3853
- * @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
3854
3913
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
3855
- * @param {Function} [destroyCallback] - Called when particle dies
3914
+ * @param {Function} [destroyCallback] - Callback when particle dies
3856
3915
  */
3857
3916
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
3858
3917
  )
@@ -4261,6 +4320,8 @@ let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData,
4261
4320
  // Initalize WebGL, called automatically by the engine
4262
4321
  function glInit()
4263
4322
  {
4323
+ if (!glEnable || headlessMode) return;
4324
+
4264
4325
  // create the canvas and textures
4265
4326
  glCanvas = document.createElement('canvas');
4266
4327
  glContext = glCanvas.getContext('webgl2');
@@ -4273,11 +4334,11 @@ function glInit()
4273
4334
  '#version 300 es\n' + // specify GLSL ES version
4274
4335
  'precision highp float;'+ // use highp for better accuracy
4275
4336
  'uniform mat4 m;'+ // transform matrix
4276
- 'in vec2 g;'+ // geometry
4277
- 'in vec4 p,u,c,a;'+ // position/size, uvs, color, additiveColor
4278
- 'in float r;'+ // rotation
4279
- 'out vec2 v;'+ // return uv, color, additiveColor
4280
- '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
4281
4342
  'void main(){'+ // shader entry point
4282
4343
  'vec2 s=(g-.5)*p.zw;'+ // get size offset
4283
4344
  'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
@@ -4287,10 +4348,10 @@ function glInit()
4287
4348
  ,
4288
4349
  '#version 300 es\n' + // specify GLSL ES version
4289
4350
  'precision highp float;'+ // use highp for better accuracy
4290
- 'in vec2 v;'+ // uv
4291
- 'in vec4 d,e;'+ // color, additiveColor
4292
4351
  'uniform sampler2D s;'+ // texture
4293
- '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
4294
4355
  'void main(){'+ // shader entry point
4295
4356
  'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
4296
4357
  '}' // end of shader
@@ -4312,9 +4373,11 @@ function glInit()
4312
4373
  // Setup render each frame, called automatically by engine
4313
4374
  function glPreRender()
4314
4375
  {
4376
+ if (!glEnable || headlessMode) return;
4377
+
4315
4378
  // clear and set to same size as main canvas
4316
4379
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4317
- glContext.clear(gl_COLOR_BUFFER_BIT);
4380
+ //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4318
4381
 
4319
4382
  // set up the shader
4320
4383
  glContext.useProgram(glShader);
@@ -4348,12 +4411,12 @@ function glPreRender()
4348
4411
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
4349
4412
  const p = vec2(-1).subtract(cameraPos.multiply(s));
4350
4413
  glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
4351
- new Float32Array([
4414
+ [
4352
4415
  s.x, 0, 0, 0,
4353
4416
  0, s.y, 0, 0,
4354
4417
  1, 1, 1, 1,
4355
4418
  p.x, p.y, 0, 0
4356
- ])
4419
+ ]
4357
4420
  );
4358
4421
  }
4359
4422
 
@@ -4364,7 +4427,7 @@ function glPreRender()
4364
4427
  function glSetTexture(texture)
4365
4428
  {
4366
4429
  // must flush cache with the old texture to set a new one
4367
- if (texture == glActiveTexture)
4430
+ if (headlessMode || texture == glActiveTexture)
4368
4431
  return;
4369
4432
 
4370
4433
  glFlush();
@@ -4424,7 +4487,6 @@ function glCreateTexture(image)
4424
4487
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
4425
4488
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
4426
4489
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
4427
-
4428
4490
  return texture;
4429
4491
  }
4430
4492
 
@@ -4448,12 +4510,12 @@ function glFlush()
4448
4510
  }
4449
4511
 
4450
4512
  /** Draw any sprites still in the buffer, copy to main canvas and clear
4451
- * @param {CanvasRenderingContext2D} context
4513
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4452
4514
  * @param {Boolean} [forceDraw]
4453
4515
  * @memberof WebGL */
4454
4516
  function glCopyToContext(context, forceDraw=false)
4455
4517
  {
4456
- if (!glInstanceCount && !forceDraw) return;
4518
+ if (!glEnable || !glInstanceCount && !forceDraw) return;
4457
4519
 
4458
4520
  glFlush();
4459
4521
 
@@ -4510,7 +4572,7 @@ let glPostShader, glPostTexture, glPostIncludeOverlay;
4510
4572
  function glInitPostProcess(shaderCode, includeOverlay=false)
4511
4573
  {
4512
4574
  ASSERT(!glPostShader, 'can only have 1 post effects shader');
4513
-
4575
+ if (headlessMode) return;
4514
4576
  if (!shaderCode) // default shader pass through
4515
4577
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
4516
4578
 
@@ -4549,8 +4611,7 @@ function glInitPostProcess(shaderCode, includeOverlay=false)
4549
4611
  // Render the post processing shader, called automatically by the engine
4550
4612
  function glRenderPostProcess()
4551
4613
  {
4552
- if (!glPostShader)
4553
- return;
4614
+ if (!glPostShader || headlessMode) return;
4554
4615
 
4555
4616
  // prepare to render post process shader
4556
4617
  if (glEnable)
@@ -4656,7 +4717,7 @@ const engineName = 'LittleJS';
4656
4717
  * @type {String}
4657
4718
  * @default
4658
4719
  * @memberof Engine */
4659
- const engineVersion = '1.9.5';
4720
+ const engineVersion = '1.9.6';
4660
4721
 
4661
4722
  /** Frames per second to update
4662
4723
  * @type {Number}
@@ -4733,7 +4794,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4733
4794
  mainContext.imageSmoothingEnabled = !canvasPixelated;
4734
4795
 
4735
4796
  // setup gl rendering if enabled
4736
- glEnable && glPreRender();
4797
+ glPreRender();
4737
4798
  }
4738
4799
 
4739
4800
  // internal update loop for engine
@@ -4752,11 +4813,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4752
4813
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
4753
4814
  if (!debugSpeedUp)
4754
4815
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
4816
+
4755
4817
  updateCanvas();
4756
4818
 
4757
4819
  if (paused)
4758
4820
  {
4759
- // do post update even when paused
4821
+ // update object transforms even when paused
4822
+ for (const o of engineObjects)
4823
+ o.parent || o.updateTransforms();
4760
4824
  inputUpdate();
4761
4825
  debugUpdate();
4762
4826
  gameUpdatePost();
@@ -4768,7 +4832,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4768
4832
  let deltaSmooth = 0;
4769
4833
  if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
4770
4834
  {
4771
- // 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
4772
4836
  deltaSmooth = frameTimeBufferMS;
4773
4837
  frameTimeBufferMS = 0;
4774
4838
  }
@@ -4793,34 +4857,37 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4793
4857
  // add the time smoothing back in
4794
4858
  frameTimeBufferMS += deltaSmooth;
4795
4859
  }
4796
-
4797
- // render sort then render while removing destroyed objects
4798
- enginePreRender();
4799
- gameRender();
4800
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
4801
- for (const o of engineObjects)
4802
- o.destroyed || o.render();
4803
- gameRenderPost();
4804
- glRenderPostProcess();
4805
- medalsRender();
4806
- touchGamepadRender();
4807
- debugRender();
4808
- glEnable && glCopyToContext(mainContext);
4809
-
4810
- if (showWatermark)
4860
+
4861
+ if (!headlessMode)
4811
4862
  {
4812
- // update fps
4813
- overlayContext.textAlign = 'right';
4814
- overlayContext.textBaseline = 'top';
4815
- overlayContext.font = '1em monospace';
4816
- overlayContext.fillStyle = '#000';
4817
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
4818
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
4819
- + (glEnable ? ' GL' : ' 2D') ;
4820
- overlayContext.fillText(text, mainCanvas.width-3, 3);
4821
- overlayContext.fillStyle = '#fff';
4822
- overlayContext.fillText(text, mainCanvas.width-2, 2);
4823
- 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
+ }
4824
4891
  }
4825
4892
 
4826
4893
  requestAnimationFrame(engineUpdate);
@@ -4828,6 +4895,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4828
4895
 
4829
4896
  function updateCanvas()
4830
4897
  {
4898
+ if (headlessMode) return;
4899
+
4831
4900
  if (canvasFixedSize.x)
4832
4901
  {
4833
4902
  // clear canvas and set fixed size
@@ -4855,8 +4924,20 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4855
4924
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
4856
4925
  }
4857
4926
 
4927
+ function startEngine()
4928
+ {
4929
+ gameInit();
4930
+ engineUpdate();
4931
+ }
4932
+
4933
+ if (headlessMode)
4934
+ {
4935
+ startEngine();
4936
+ return;
4937
+ }
4938
+
4858
4939
  // setup html
4859
- const styleBody =
4940
+ const styleBody =
4860
4941
  'margin:0;overflow:hidden;' + // fill the window
4861
4942
  'background:#000;' + // set background color
4862
4943
  'touch-action:none;' + // prevent mobile pinch to resize
@@ -4868,8 +4949,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4868
4949
  mainContext = mainCanvas.getContext('2d');
4869
4950
 
4870
4951
  // init stuff and start engine
4952
+ inputInit();
4871
4953
  debugInit();
4872
- glEnable && glInit();
4954
+ glInit();
4873
4955
 
4874
4956
  // create overlay canvas for hud to appear above gl canvas
4875
4957
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
@@ -4910,12 +4992,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4910
4992
  }));
4911
4993
 
4912
4994
  // load all of the images
4913
- Promise.all(promises).then(()=>
4914
- {
4915
- // start the engine
4916
- gameInit();
4917
- engineUpdate();
4918
- });
4995
+ Promise.all(promises).then(startEngine);
4919
4996
  }
4920
4997
 
4921
4998
  /** Update each engine object, remove destroyed objects, and update time
@@ -4936,7 +5013,14 @@ function engineObjectsUpdate()
4936
5013
  }
4937
5014
  }
4938
5015
  for (const o of engineObjects)
4939
- o.parent || updateObject(o);
5016
+ {
5017
+ // update top level objects
5018
+ if (!o.parent)
5019
+ {
5020
+ updateObject(o);
5021
+ o.updateTransforms();
5022
+ }
5023
+ }
4940
5024
 
4941
5025
  // remove destroyed objects
4942
5026
  engineObjects = engineObjects.filter(o=>!o.destroyed);