littlejsengine 1.9.5 → 1.9.7

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);
@@ -755,7 +758,7 @@ class Color
755
758
  const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
756
759
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
757
760
  }
758
-
761
+
759
762
  /** Set this color from a hex code
760
763
  * @param {String} hex - html hex code
761
764
  * @return {Color} */
@@ -811,11 +814,11 @@ class Timer
811
814
 
812
815
  /** Returns true if set and has not elapsed
813
816
  * @return {Boolean} */
814
- active() { return time <= this.time; }
817
+ active() { return time < this.time; }
815
818
 
816
819
  /** Returns true if set and elapsed
817
820
  * @return {Boolean} */
818
- elapsed() { return time > this.time; }
821
+ elapsed() { return time >= this.time; }
819
822
 
820
823
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
821
824
  * @return {Number} */
@@ -890,6 +893,12 @@ let fontDefault = 'arial';
890
893
  * @memberof Settings */
891
894
  let showSplashScreen = false;
892
895
 
896
+ /** Disables all rendering, audio, and input for servers
897
+ * @type {Boolean}
898
+ * @default
899
+ * @memberof Settings */
900
+ let headlessMode = false;
901
+
893
902
  ///////////////////////////////////////////////////////////////////////////////
894
903
  // WebGL settings
895
904
 
@@ -918,7 +927,7 @@ let tileSizeDefault = vec2(16);
918
927
  * @type {Number}
919
928
  * @default
920
929
  * @memberof Settings */
921
- let tileFixBleedScale = .1;
930
+ let tileFixBleedScale = .5;
922
931
 
923
932
  ///////////////////////////////////////////////////////////////////////////////
924
933
  // Object settings
@@ -1043,7 +1052,7 @@ let soundEnable = true;
1043
1052
  * @type {Number}
1044
1053
  * @default
1045
1054
  * @memberof Settings */
1046
- let soundVolume = .5;
1055
+ let soundVolume = .3;
1047
1056
 
1048
1057
  /** Default range where sound no longer plays
1049
1058
  * @type {Number}
@@ -1128,6 +1137,11 @@ function setFontDefault(font) { fontDefault = font; }
1128
1137
  * @memberof Settings */
1129
1138
  function setShowSplashScreen(show) { showSplashScreen = show; }
1130
1139
 
1140
+ /** Set to disalbe rendering, audio, and input for servers
1141
+ * @param {Boolean} headless
1142
+ * @memberof Settings */
1143
+ function setHeadlessMode(headless) { headlessMode = headless; }
1144
+
1131
1145
  /** Set if webgl rendering is enabled
1132
1146
  * @param {Boolean} enable
1133
1147
  * @memberof Settings */
@@ -1241,7 +1255,12 @@ function setSoundEnable(enable) { soundEnable = enable; }
1241
1255
  /** Set volume scale to apply to all sound, music and speech
1242
1256
  * @param {Number} volume
1243
1257
  * @memberof Settings */
1244
- function setSoundVolume(volume) { soundVolume = volume; }
1258
+ function setSoundVolume(volume)
1259
+ {
1260
+ soundVolume = volume;
1261
+ if (soundEnable && !headlessMode && audioGainNode)
1262
+ audioGainNode.gain.value = volume; // update gain immediatly
1263
+ }
1245
1264
 
1246
1265
  /** Set default range where sound no longer plays
1247
1266
  * @param {Number} range
@@ -1374,6 +1393,8 @@ class EngineObject
1374
1393
  this.spawnTime = time;
1375
1394
  /** @property {Array} - List of children of this object */
1376
1395
  this.children = [];
1396
+ /** @property {Boolean} - Limit object speed using linear or circular math */
1397
+ this.clampSpeedLinear = true;
1377
1398
 
1378
1399
  // parent child system
1379
1400
  /** @property {EngineObject} - Parent of object if in local space */
@@ -1397,21 +1418,46 @@ class EngineObject
1397
1418
  engineObjects.push(this);
1398
1419
  }
1399
1420
 
1400
- /** Update the object transform and physics, called automatically by engine once each frame */
1401
- update()
1421
+ /** Update the object transform, called automatically by engine even when paused */
1422
+ updateTransforms()
1402
1423
  {
1403
1424
  const parent = this.parent;
1404
1425
  if (parent)
1405
1426
  {
1406
1427
  // 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;
1428
+ const mirror = parent.getMirrorSign();
1429
+ this.pos = this.localPos.multiply(vec2(mirror,1)).rotate(-parent.angle).add(parent.pos);
1430
+ this.angle = mirror*this.localAngle + parent.angle;
1410
1431
  }
1411
1432
 
1433
+ // update children
1434
+ for (const child of this.children)
1435
+ child.updateTransforms();
1436
+ }
1437
+
1438
+ /** Update the object physics, called automatically by engine once each frame */
1439
+ update()
1440
+ {
1441
+ // child objects do not have physics
1442
+ if (this.parent)
1443
+ return;
1444
+
1412
1445
  // limit max speed to prevent missing collisions
1413
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1414
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1446
+ if (this.clampSpeedLinear)
1447
+ {
1448
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1449
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1450
+ }
1451
+ else
1452
+ {
1453
+ const length2 = this.velocity.lengthSquared();
1454
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
1455
+ {
1456
+ const s = objectMaxSpeed / length2**.5;
1457
+ this.velocity.x *= s;
1458
+ this.velocity.y *= s;
1459
+ }
1460
+ }
1415
1461
 
1416
1462
  // apply physics
1417
1463
  const oldPos = this.pos.copy();
@@ -1423,8 +1469,7 @@ class EngineObject
1423
1469
  // physics sanity checks
1424
1470
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1425
1471
  ASSERT(this.damping >= 0 && this.damping <= 1);
1426
-
1427
- if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
1472
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
1428
1473
  return;
1429
1474
 
1430
1475
  const wasMovingDown = this.velocity.y < 0;
@@ -1754,6 +1799,9 @@ let drawCount;
1754
1799
  */
1755
1800
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1756
1801
  {
1802
+ if (headlessMode)
1803
+ return new TileInfo;
1804
+
1757
1805
  // if size is a number, make it a vector
1758
1806
  if (typeof size === 'number')
1759
1807
  {
@@ -1787,9 +1835,9 @@ class TileInfo
1787
1835
  constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1788
1836
  {
1789
1837
  /** @property {Vector2} - Top left corner of tile in pixels */
1790
- this.pos = pos;
1838
+ this.pos = pos.copy();
1791
1839
  /** @property {Vector2} - Size of tile in pixels */
1792
- this.size = size;
1840
+ this.size = size.copy();
1793
1841
  /** @property {Number} - Texture index to use */
1794
1842
  this.textureIndex = textureIndex;
1795
1843
  }
@@ -1881,7 +1929,7 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
1881
1929
  * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
1882
1930
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
1883
1931
  * @param {Boolean} [screenSpace=false] - If true the pos and size are in screen space
1884
- * @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
1932
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
1885
1933
  * @memberof Draw */
1886
1934
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
1887
1935
  angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
@@ -1954,7 +2002,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
1954
2002
  * @param {Number} [angle]
1955
2003
  * @param {Boolean} [useWebGL=glEnable]
1956
2004
  * @param {Boolean} [screenSpace=false]
1957
- * @param {CanvasRenderingContext2D} [context]
2005
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
1958
2006
  * @memberof Draw */
1959
2007
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
1960
2008
  {
@@ -1968,7 +2016,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
1968
2016
  * @param {Color} [color=(1,1,1,1)]
1969
2017
  * @param {Boolean} [useWebGL=glEnable]
1970
2018
  * @param {Boolean} [screenSpace=false]
1971
- * @param {CanvasRenderingContext2D} [context]
2019
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
1972
2020
  * @memberof Draw */
1973
2021
  function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
1974
2022
  {
@@ -1984,7 +2032,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
1984
2032
  * @param {Boolean} mirror
1985
2033
  * @param {Function} drawFunction
1986
2034
  * @param {Boolean} [screenSpace=false]
1987
- * @param {CanvasRenderingContext2D} [context=mainContext]
2035
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
1988
2036
  * @memberof Draw */
1989
2037
  function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
1990
2038
  {
@@ -2005,7 +2053,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
2005
2053
  /** Enable normal or additive blend mode
2006
2054
  * @param {Boolean} [additive]
2007
2055
  * @param {Boolean} [useWebGL=glEnable]
2008
- * @param {CanvasRenderingContext2D} [context=mainContext]
2056
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2009
2057
  * @memberof Draw */
2010
2058
  function setBlendMode(additive, useWebGL=glEnable, context)
2011
2059
  {
@@ -2030,7 +2078,7 @@ function setBlendMode(additive, useWebGL=glEnable, context)
2030
2078
  * @param {Color} [lineColor=(0,0,0,1)]
2031
2079
  * @param {CanvasTextAlign} [textAlign='center']
2032
2080
  * @param {String} [font=fontDefault]
2033
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2081
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2034
2082
  * @memberof Draw */
2035
2083
  function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
2036
2084
  {
@@ -2047,7 +2095,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
2047
2095
  * @param {Color} [lineColor=(0,0,0,1)]
2048
2096
  * @param {CanvasTextAlign} [textAlign]
2049
2097
  * @param {String} [font=fontDefault]
2050
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2098
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2051
2099
  * @memberof Draw */
2052
2100
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
2053
2101
  {
@@ -2090,7 +2138,7 @@ class FontImage
2090
2138
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
2091
2139
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
2092
2140
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
2093
- * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
2141
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
2094
2142
  */
2095
2143
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
2096
2144
  {
@@ -2298,7 +2346,7 @@ function gamepadWasReleased(button, gamepad=0)
2298
2346
  * @return {Vector2}
2299
2347
  * @memberof Input */
2300
2348
  function gamepadStick(stick, gamepad=0)
2301
- { return stickData[gamepad] ? stickData[gamepad][stick] || vec2() : vec2(); }
2349
+ { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
2302
2350
 
2303
2351
  ///////////////////////////////////////////////////////////////////////////////
2304
2352
  // Input update called by engine
@@ -2309,6 +2357,8 @@ let inputData = [[]];
2309
2357
 
2310
2358
  function inputUpdate()
2311
2359
  {
2360
+ if (headlessMode) return;
2361
+
2312
2362
  // clear input when lost focus (prevent stuck keys)
2313
2363
  isTouchDevice || document.hasFocus() || clearInput();
2314
2364
 
@@ -2321,6 +2371,8 @@ function inputUpdate()
2321
2371
 
2322
2372
  function inputUpdatePost()
2323
2373
  {
2374
+ if (headlessMode) return;
2375
+
2324
2376
  // clear input to prepare for next frame
2325
2377
  for (const deviceInputData of inputData)
2326
2378
  for (const i in deviceInputData)
@@ -2329,9 +2381,12 @@ function inputUpdatePost()
2329
2381
  }
2330
2382
 
2331
2383
  ///////////////////////////////////////////////////////////////////////////////
2332
- // Keyboard event handlers
2384
+ // Input event handlers
2333
2385
 
2386
+ function inputInit()
2334
2387
  {
2388
+ if (headlessMode) return;
2389
+
2335
2390
  onkeydown = (e)=>
2336
2391
  {
2337
2392
  if (debug && e.target != document.body) return;
@@ -2362,21 +2417,29 @@ function inputUpdatePost()
2362
2417
  c == 'KeyA' ? 'ArrowLeft' :
2363
2418
  c == 'KeyD' ? 'ArrowRight' : c : c;
2364
2419
  }
2420
+
2421
+ // mouse event handlers
2422
+ onmousedown = (e)=>
2423
+ {
2424
+ isUsingGamepad = false;
2425
+ inputData[0][e.button] = 3;
2426
+ mousePosScreen = mouseToScreen(e);
2427
+ e.button && e.preventDefault();
2428
+ }
2429
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2430
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2431
+ onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2432
+ oncontextmenu = (e)=> false; // prevent right click menu
2433
+
2434
+ // init touch input
2435
+ if (isTouchDevice)
2436
+ touchInputInit();
2365
2437
  }
2366
2438
 
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
2439
  // convert a mouse or touch event position to screen space
2377
2440
  function mouseToScreen(mousePos)
2378
2441
  {
2379
- if (!mainCanvas)
2442
+ if (!mainCanvas || headlessMode)
2380
2443
  return vec2(); // fix bug that can occur if user clicks before page loads
2381
2444
 
2382
2445
  const rect = mainCanvas.getBoundingClientRect();
@@ -2388,7 +2451,7 @@ function mouseToScreen(mousePos)
2388
2451
  // Gamepad input
2389
2452
 
2390
2453
  // gamepad internal variables
2391
- const stickData = [];
2454
+ const gamepadStickData = [];
2392
2455
 
2393
2456
  // gamepads are updated by engine every frame automatically
2394
2457
  function gamepadsUpdate()
@@ -2405,14 +2468,11 @@ function gamepadsUpdate()
2405
2468
  // update touch gamepad if enabled
2406
2469
  if (touchGamepadEnable && isTouchDevice)
2407
2470
  {
2408
- // create the touch gamepad if it doesn't exist
2409
- if (!touchGamepadButtons)
2410
- createTouchGamepad();
2411
-
2471
+ ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
2412
2472
  if (touchGamepadTimer.isSet())
2413
2473
  {
2414
2474
  // read virtual analog stick
2415
- const sticks = stickData[0] || (stickData[0] = []);
2475
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
2416
2476
  sticks[0] = vec2();
2417
2477
  if (touchGamepadAnalog)
2418
2478
  sticks[0] = applyDeadZones(touchGamepadStick);
@@ -2429,7 +2489,8 @@ function gamepadsUpdate()
2429
2489
  for (let i=10; i--;)
2430
2490
  {
2431
2491
  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;
2492
+ const wasDown = gamepadIsDown(j,0);
2493
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
2433
2494
  }
2434
2495
  }
2435
2496
  }
@@ -2449,7 +2510,7 @@ function gamepadsUpdate()
2449
2510
  // get or create gamepad data
2450
2511
  const gamepad = gamepads[i];
2451
2512
  const data = inputData[i+1] || (inputData[i+1] = []);
2452
- const sticks = stickData[i] || (stickData[i] = []);
2513
+ const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
2453
2514
 
2454
2515
  if (gamepad)
2455
2516
  {
@@ -2488,28 +2549,44 @@ function gamepadsUpdate()
2488
2549
  * @param {Number|Array} [pattern] - single value in ms or vibration interval array
2489
2550
  * @memberof Input */
2490
2551
  function vibrate(pattern=100)
2491
- { vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2552
+ { vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2492
2553
 
2493
2554
  /** Cancel any ongoing vibration
2494
2555
  * @memberof Input */
2495
2556
  function vibrateStop() { vibrate(0); }
2496
2557
 
2497
2558
  ///////////////////////////////////////////////////////////////////////////////
2498
- // Touch input
2559
+ // Touch input & virtual on screen gamepad
2499
2560
 
2500
2561
  /** True if a touch device has been detected
2501
2562
  * @memberof Input */
2502
- const isTouchDevice = window.ontouchstart !== undefined;
2563
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
2564
+
2565
+ // touch gamepad internal variables
2566
+ let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2503
2567
 
2504
2568
  // try to enable touch mouse
2505
- if (isTouchDevice)
2569
+ function touchInputInit()
2506
2570
  {
2571
+ // add non passive touch event listeners
2572
+ let handleTouch = handleTouchDefault;
2573
+ if (touchGamepadEnable)
2574
+ {
2575
+ // touch input internal variables
2576
+ handleTouch = handleTouchGamepad;
2577
+ touchGamepadButtons = [];
2578
+ touchGamepadStick = vec2();
2579
+ }
2580
+ document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
2581
+ document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
2582
+ document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
2583
+
2507
2584
  // override mouse events
2508
- let wasTouching;
2509
2585
  onmousedown = onmouseup = ()=> 0;
2510
2586
 
2511
2587
  // handle all touch events the same way
2512
- ontouchstart = ontouchmove = ontouchend = (e)=>
2588
+ let wasTouching;
2589
+ function handleTouchDefault(e)
2513
2590
  {
2514
2591
  // fix stalled audio requiring user interaction
2515
2592
  if (soundEnable && audioContext && audioContext.state != 'running')
@@ -2538,27 +2615,14 @@ if (isTouchDevice)
2538
2615
  // must return true so the document will get focus
2539
2616
  return true;
2540
2617
  }
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
2618
 
2556
- const touchHandler = ontouchstart;
2557
- ontouchstart = ontouchmove = ontouchend = (e)=>
2619
+ // special handling for virtual gamepad mode
2620
+ function handleTouchGamepad(e)
2558
2621
  {
2559
2622
  // clear touch gamepad input
2560
2623
  touchGamepadStick = vec2();
2561
2624
  touchGamepadButtons = [];
2625
+ isUsingGamepad = true;
2562
2626
 
2563
2627
  const touching = e.touches.length;
2564
2628
  if (touching)
@@ -2599,9 +2663,8 @@ function createTouchGamepad()
2599
2663
  }
2600
2664
  }
2601
2665
 
2602
- // call default touch handler and set to using gamepad
2603
- touchHandler.bind(window)(e);
2604
- isUsingGamepad = true;
2666
+ // call default touch handler so normal touch events still work
2667
+ handleTouchDefault(e);
2605
2668
 
2606
2669
  // must return true so the document will get focus
2607
2670
  return true;
@@ -2620,32 +2683,33 @@ function touchGamepadRender()
2620
2683
  return;
2621
2684
 
2622
2685
  // setup the canvas
2623
- overlayContext.save();
2624
- overlayContext.globalAlpha = alpha*touchGamepadAlpha;
2625
- overlayContext.strokeStyle = '#fff';
2626
- overlayContext.lineWidth = 3;
2686
+ const context = overlayContext;
2687
+ context.save();
2688
+ context.globalAlpha = alpha*touchGamepadAlpha;
2689
+ context.strokeStyle = '#fff';
2690
+ context.lineWidth = 3;
2627
2691
 
2628
2692
  // draw left analog stick
2629
- overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2630
- overlayContext.beginPath();
2693
+ context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
2694
+ context.beginPath();
2631
2695
 
2632
2696
  const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
2633
2697
  if (touchGamepadAnalog) // draw circle shaped gamepad
2634
2698
  {
2635
- overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2636
- overlayContext.fill();
2637
- overlayContext.stroke();
2699
+ context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
2700
+ context.fill();
2701
+ context.stroke();
2638
2702
  }
2639
2703
  else // draw cross shaped gamepad
2640
2704
  {
2641
2705
  for(let i=10; i--;)
2642
2706
  {
2643
2707
  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();
2708
+ context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
2709
+ i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
2710
+ i==1 && context.fill();
2647
2711
  }
2648
- overlayContext.stroke();
2712
+ context.stroke();
2649
2713
  }
2650
2714
 
2651
2715
  // draw right face buttons
@@ -2653,15 +2717,15 @@ function touchGamepadRender()
2653
2717
  for (let i=4; i--;)
2654
2718
  {
2655
2719
  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();
2720
+ context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
2721
+ context.beginPath();
2722
+ context.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
2723
+ context.fill();
2724
+ context.stroke();
2661
2725
  }
2662
2726
 
2663
2727
  // set canvas back to normal
2664
- overlayContext.restore();
2728
+ context.restore();
2665
2729
  }
2666
2730
  /**
2667
2731
  * LittleJS Audio System
@@ -2676,6 +2740,32 @@ function touchGamepadRender()
2676
2740
 
2677
2741
 
2678
2742
 
2743
+ /** Audio context used by the engine
2744
+ * @type {AudioContext}
2745
+ * @memberof Audio */
2746
+ let audioContext;
2747
+
2748
+ /** Master gain node for all audio to pass through
2749
+ * @type {GainNode}
2750
+ * @memberof Audio */
2751
+ let audioGainNode;
2752
+
2753
+ function audioInit()
2754
+ {
2755
+ if (!soundEnable || headlessMode) return;
2756
+
2757
+ // create audio context
2758
+ audioContext = new AudioContext;
2759
+
2760
+ // create and connect gain node
2761
+ // (createGain is more widely spported then GainNode construtor)
2762
+ audioGainNode = audioContext.createGain();
2763
+ audioGainNode.connect(audioContext.destination);
2764
+ setSoundVolume(soundVolume); // update gain volume
2765
+ }
2766
+
2767
+ ///////////////////////////////////////////////////////////////////////////////
2768
+
2679
2769
  /**
2680
2770
  * Sound Object - Stores a sound for later use and can be played positionally
2681
2771
  *
@@ -2696,7 +2786,7 @@ class Sound
2696
2786
  */
2697
2787
  constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
2698
2788
  {
2699
- if (!soundEnable) return;
2789
+ if (!soundEnable || headlessMode) return;
2700
2790
 
2701
2791
  /** @property {Number} - World space max range of sound, will not play if camera is farther away */
2702
2792
  this.range = range;
@@ -2710,8 +2800,8 @@ class Sound
2710
2800
  if (zzfxSound)
2711
2801
  {
2712
2802
  // generate zzfx sound now for fast playback
2713
- this.randomness = zzfxSound[1] || 0;
2714
- zzfxSound[1] = 0; // generate without randomness
2803
+ const defaultRandomness = .05;
2804
+ this.randomness = zzfxSound[1] || defaultRandomness;
2715
2805
  this.sampleChannels = [zzfxG(...zzfxSound)];
2716
2806
  this.sampleRate = zzfxR;
2717
2807
  }
@@ -2727,7 +2817,8 @@ class Sound
2727
2817
  */
2728
2818
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
2729
2819
  {
2730
- if (!soundEnable || !this.sampleChannels) return;
2820
+ if (!soundEnable || headlessMode) return;
2821
+ if (!this.sampleChannels) return;
2731
2822
 
2732
2823
  let pan;
2733
2824
  if (pos)
@@ -2804,14 +2895,14 @@ class SoundWave extends Sound
2804
2895
  * @param {Number} [randomness] - How much to randomize frequency each time sound plays
2805
2896
  * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
2806
2897
  * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
2898
+ * @param {Function} [onloadCallback] - callback function to call when sound is loaded
2807
2899
  */
2808
- constructor(filename, randomness=0, range, taper)
2900
+ constructor(filename, randomness=0, range, taper, onloadCallback)
2809
2901
  {
2810
2902
  super(undefined, range, taper);
2811
- this.randomness = randomness;
2812
-
2813
- if (!soundEnable) return;
2903
+ if (!soundEnable || headlessMode) return;
2814
2904
 
2905
+ this.randomness = randomness;
2815
2906
  fetch(filename)
2816
2907
  .then(response => response.arrayBuffer())
2817
2908
  .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
@@ -2821,10 +2912,23 @@ class SoundWave extends Sound
2821
2912
  for (let i = audioBuffer.numberOfChannels; i--;)
2822
2913
  this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
2823
2914
  this.sampleRate = audioBuffer.sampleRate;
2824
- });
2915
+ }).then(() => onloadCallback && onloadCallback(this));
2825
2916
  }
2826
2917
  }
2827
2918
 
2919
+ /** Play an mp3, ogg, or wav audio from a local file or url
2920
+ * @param {String} filename - Location of sound file to play
2921
+ * @param {Number} [volume] - How much to scale volume by
2922
+ * @param {Boolean} [loop] - True if the music should loop
2923
+ * @return {SoundWave} - The sound object for this file
2924
+ * @memberof Audio */
2925
+ function playAudioFile(filename, volume=1, loop=false)
2926
+ {
2927
+ if (!soundEnable || headlessMode) return;
2928
+
2929
+ return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
2930
+ }
2931
+
2828
2932
  /**
2829
2933
  * Music Object - Stores a zzfx music track for later use
2830
2934
  *
@@ -2864,7 +2968,7 @@ class Music extends Sound
2864
2968
  {
2865
2969
  super(undefined);
2866
2970
 
2867
- if (!soundEnable) return;
2971
+ if (!soundEnable || headlessMode) return;
2868
2972
  this.randomness = 0;
2869
2973
  this.sampleChannels = zzfxM(...zzfxMusic);
2870
2974
  this.sampleRate = zzfxR;
@@ -2879,23 +2983,6 @@ class Music extends Sound
2879
2983
  { return super.play(undefined, volume, 1, 1, loop); }
2880
2984
  }
2881
2985
 
2882
- /** Play an mp3, ogg, or wav audio from a local file or url
2883
- * @param {String} filename - Location of sound file to play
2884
- * @param {Number} [volume] - How much to scale volume by
2885
- * @param {Boolean} [loop] - True if the music should loop
2886
- * @return {HTMLAudioElement} - The audio element for this sound
2887
- * @memberof Audio */
2888
- function playAudioFile(filename, volume=1, loop=false)
2889
- {
2890
- if (!soundEnable) return;
2891
-
2892
- const audio = new Audio(filename);
2893
- audio.volume = soundVolume * volume;
2894
- audio.loop = loop;
2895
- audio.play();
2896
- return audio;
2897
- }
2898
-
2899
2986
  /** Speak text with passed in settings
2900
2987
  * @param {String} text - The text to speak
2901
2988
  * @param {String} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
@@ -2906,7 +2993,8 @@ function playAudioFile(filename, volume=1, loop=false)
2906
2993
  * @memberof Audio */
2907
2994
  function speak(text, language='', volume=1, rate=1, pitch=1)
2908
2995
  {
2909
- if (!soundEnable || !speechSynthesis) return;
2996
+ if (!soundEnable || headlessMode) return;
2997
+ if (!speechSynthesis) return;
2910
2998
 
2911
2999
  // common languages (not supported by all browsers)
2912
3000
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -2936,14 +3024,8 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
2936
3024
 
2937
3025
  ///////////////////////////////////////////////////////////////////////////////
2938
3026
 
2939
- /** Audio context used by the engine
2940
- * @type {AudioContext}
2941
- * @memberof Audio */
2942
- let audioContext = new AudioContext;
2943
-
2944
- /** Keep track if audio was suspended when last sound was played
2945
- * @type {Boolean}
2946
- * @memberof Audio */
3027
+ // internal tracking if audio was suspended when last sound was played
3028
+ // allows first suspended sound to play when audio is resumed
2947
3029
  let audioSuspended = false;
2948
3030
 
2949
3031
  /** Play cached audio samples with given settings
@@ -2957,7 +3039,7 @@ let audioSuspended = false;
2957
3039
  * @memberof Audio */
2958
3040
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
2959
3041
  {
2960
- if (!soundEnable) return;
3042
+ if (!soundEnable || headlessMode) return;
2961
3043
 
2962
3044
  // prevent sounds from building up if they can't be played
2963
3045
  const audioWasSuspended = audioSuspended;
@@ -2981,10 +3063,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
2981
3063
  source.playbackRate.value = rate;
2982
3064
  source.loop = loop;
2983
3065
 
2984
- // create and connect gain node (createGain is more widely spported then GainNode construtor)
3066
+ // set master gain volume
3067
+ setSoundVolume(soundVolume);
3068
+
3069
+ // create and connect gain node
2985
3070
  const gainNode = audioContext.createGain();
2986
- gainNode.gain.value = soundVolume*volume;
2987
- gainNode.connect(audioContext.destination);
3071
+ gainNode.gain.value = volume;
3072
+ gainNode.connect(audioGainNode);
2988
3073
 
2989
3074
  // connect source to stereo panner and gain
2990
3075
  source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
@@ -3003,7 +3088,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3003
3088
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
3004
3089
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3005
3090
  * @memberof Audio */
3006
- function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
3091
+ function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
3007
3092
 
3008
3093
  /** Sample rate used for all ZzFX sounds
3009
3094
  * @default 44100
@@ -3012,7 +3097,7 @@ const zzfxR = 44100;
3012
3097
 
3013
3098
  /** Generate samples for a ZzFX sound
3014
3099
  * @param {Number} [volume] - Volume scale (percent)
3015
- * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
3100
+ * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
3016
3101
  * @param {Number} [frequency] - Frequency of sound (Hz)
3017
3102
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
3018
3103
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -3038,17 +3123,18 @@ const zzfxR = 44100;
3038
3123
  function zzfxG
3039
3124
  (
3040
3125
  // parameters
3041
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3126
+ volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
3042
3127
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3043
3128
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3044
3129
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3045
3130
  )
3046
3131
  {
3132
+ // LJS Note: ZZFX modded so randomness is handled by Sound class
3133
+
3047
3134
  // init parameters
3048
3135
  let PI2 = PI*2, sampleRate = zzfxR,
3049
3136
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3050
- startFrequency = frequency *=
3051
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3137
+ startFrequency = frequency *= PI2 / sampleRate,
3052
3138
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3053
3139
 
3054
3140
  // biquad LP/HP filter
@@ -3070,7 +3156,6 @@ function zzfxG
3070
3156
  pitchJump *= PI2 / sampleRate;
3071
3157
  pitchJumpTime *= sampleRate;
3072
3158
  repeatTime = repeatTime * sampleRate | 0;
3073
- volume *= soundVolume;
3074
3159
 
3075
3160
  // generate waveform
3076
3161
  for(length = attack + decay + sustain + release + delay | 0;
@@ -3416,7 +3501,7 @@ class TileLayer extends EngineObject
3416
3501
 
3417
3502
  /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3418
3503
  this.canvas = document.createElement('canvas');
3419
- /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3504
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3420
3505
  this.context = this.canvas.getContext('2d');
3421
3506
  /** @property {Vector2} - How much to scale this layer when rendered */
3422
3507
  this.scale = scale;
@@ -3427,6 +3512,17 @@ class TileLayer extends EngineObject
3427
3512
  this.data = [];
3428
3513
  for (let j = this.size.area(); j--;)
3429
3514
  this.data.push(new TileLayerData);
3515
+
3516
+ if (headlessMode)
3517
+ {
3518
+ // disable rendering
3519
+ this.redraw = () => {};
3520
+ this.render = () => {};
3521
+ this.redrawStart = () => {};
3522
+ this.redrawEnd = () => {};
3523
+ this.drawTileData = () => {};
3524
+ this.drawCanvas2D = () => {};
3525
+ }
3430
3526
  }
3431
3527
 
3432
3528
  /** Set data at a given position in the array
@@ -3457,7 +3553,7 @@ class TileLayer extends EngineObject
3457
3553
  ASSERT(mainContext != this.context, 'must call redrawEnd() after drawing tiles');
3458
3554
 
3459
3555
  // flush and copy gl canvas because tile canvas does not use webgl
3460
- glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3556
+ !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3461
3557
 
3462
3558
  // draw the entire cached level onto the canvas
3463
3559
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
@@ -3507,14 +3603,14 @@ class TileLayer extends EngineObject
3507
3603
  this.context.imageSmoothingEnabled = !canvasPixelated;
3508
3604
 
3509
3605
  // setup gl rendering if enabled
3510
- glEnable && glPreRender();
3606
+ glPreRender();
3511
3607
  }
3512
3608
 
3513
3609
  /** Call to end the redraw process */
3514
3610
  redrawEnd()
3515
3611
  {
3516
3612
  ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3517
- glEnable && glCopyToContext(mainContext, true);
3613
+ glCopyToContext(mainContext, true);
3518
3614
  //debugSaveCanvas(this.canvas);
3519
3615
 
3520
3616
  // set stuff back to normal
@@ -3839,20 +3935,21 @@ class ParticleEmitter extends EngineObject
3839
3935
  class Particle extends EngineObject
3840
3936
  {
3841
3937
  /**
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
3938
+ * Create a particle with the passed in settings
3939
+ * Typically this is created automatically by a ParticleEmitter
3940
+ * @param {Vector2} position - World space position of the particle
3941
+ * @param {TileInfo} tileInfo - Tile info to render particles
3942
+ * @param {Number} angle - Angle to rotate the particle
3943
+ * @param {Color} colorStart - Color at start of life
3944
+ * @param {Color} colorEnd - Color at end of life
3945
+ * @param {Number} lifeTime - How long to live for
3946
+ * @param {Number} sizeStart - Size at start of life
3947
+ * @param {Number} sizeEnd - Size at end of life
3948
+ * @param {Number} fadeRate - How quick to fade in/out
3949
+ * @param {Boolean} additive - Does it use additive blend mode
3950
+ * @param {Number} trailScale - If a trail, how long to make it
3854
3951
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
3855
- * @param {Function} [destroyCallback] - Called when particle dies
3952
+ * @param {Function} [destroyCallback] - Callback when particle dies
3856
3953
  */
3857
3954
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
3858
3955
  )
@@ -3879,6 +3976,9 @@ class Particle extends EngineObject
3879
3976
  this.localSpaceEmitter = localSpaceEmitter;
3880
3977
  /** @property {Function} - Called when particle dies */
3881
3978
  this.destroyCallback = destroyCallback;
3979
+
3980
+ // particles use circular clamped speed
3981
+ this.clampSpeedLinear = false;
3882
3982
  }
3883
3983
 
3884
3984
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -4261,6 +4361,8 @@ let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData,
4261
4361
  // Initalize WebGL, called automatically by the engine
4262
4362
  function glInit()
4263
4363
  {
4364
+ if (!glEnable || headlessMode) return;
4365
+
4264
4366
  // create the canvas and textures
4265
4367
  glCanvas = document.createElement('canvas');
4266
4368
  glContext = glCanvas.getContext('webgl2');
@@ -4273,11 +4375,11 @@ function glInit()
4273
4375
  '#version 300 es\n' + // specify GLSL ES version
4274
4376
  'precision highp float;'+ // use highp for better accuracy
4275
4377
  '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
4378
+ 'in vec2 g;'+ // in: geometry
4379
+ 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
4380
+ 'in float r;'+ // in: rotation
4381
+ 'out vec2 v;'+ // out: uv
4382
+ 'out vec4 d,e;'+ // out: color, additiveColor
4281
4383
  'void main(){'+ // shader entry point
4282
4384
  'vec2 s=(g-.5)*p.zw;'+ // get size offset
4283
4385
  'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
@@ -4287,10 +4389,10 @@ function glInit()
4287
4389
  ,
4288
4390
  '#version 300 es\n' + // specify GLSL ES version
4289
4391
  'precision highp float;'+ // use highp for better accuracy
4290
- 'in vec2 v;'+ // uv
4291
- 'in vec4 d,e;'+ // color, additiveColor
4292
4392
  'uniform sampler2D s;'+ // texture
4293
- 'out vec4 c;'+ // out color
4393
+ 'in vec2 v;'+ // in: uv
4394
+ 'in vec4 d,e;'+ // in: color, additiveColor
4395
+ 'out vec4 c;'+ // out: color
4294
4396
  'void main(){'+ // shader entry point
4295
4397
  'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
4296
4398
  '}' // end of shader
@@ -4312,9 +4414,11 @@ function glInit()
4312
4414
  // Setup render each frame, called automatically by engine
4313
4415
  function glPreRender()
4314
4416
  {
4417
+ if (!glEnable || headlessMode) return;
4418
+
4315
4419
  // clear and set to same size as main canvas
4316
4420
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4317
- glContext.clear(gl_COLOR_BUFFER_BIT);
4421
+ //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4318
4422
 
4319
4423
  // set up the shader
4320
4424
  glContext.useProgram(glShader);
@@ -4348,12 +4452,12 @@ function glPreRender()
4348
4452
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
4349
4453
  const p = vec2(-1).subtract(cameraPos.multiply(s));
4350
4454
  glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
4351
- new Float32Array([
4455
+ [
4352
4456
  s.x, 0, 0, 0,
4353
4457
  0, s.y, 0, 0,
4354
4458
  1, 1, 1, 1,
4355
4459
  p.x, p.y, 0, 0
4356
- ])
4460
+ ]
4357
4461
  );
4358
4462
  }
4359
4463
 
@@ -4364,7 +4468,7 @@ function glPreRender()
4364
4468
  function glSetTexture(texture)
4365
4469
  {
4366
4470
  // must flush cache with the old texture to set a new one
4367
- if (texture == glActiveTexture)
4471
+ if (headlessMode || texture == glActiveTexture)
4368
4472
  return;
4369
4473
 
4370
4474
  glFlush();
@@ -4424,7 +4528,6 @@ function glCreateTexture(image)
4424
4528
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
4425
4529
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
4426
4530
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
4427
-
4428
4531
  return texture;
4429
4532
  }
4430
4533
 
@@ -4448,12 +4551,12 @@ function glFlush()
4448
4551
  }
4449
4552
 
4450
4553
  /** Draw any sprites still in the buffer, copy to main canvas and clear
4451
- * @param {CanvasRenderingContext2D} context
4554
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4452
4555
  * @param {Boolean} [forceDraw]
4453
4556
  * @memberof WebGL */
4454
4557
  function glCopyToContext(context, forceDraw=false)
4455
4558
  {
4456
- if (!glInstanceCount && !forceDraw) return;
4559
+ if (!glEnable || !glInstanceCount && !forceDraw) return;
4457
4560
 
4458
4561
  glFlush();
4459
4562
 
@@ -4510,7 +4613,7 @@ let glPostShader, glPostTexture, glPostIncludeOverlay;
4510
4613
  function glInitPostProcess(shaderCode, includeOverlay=false)
4511
4614
  {
4512
4615
  ASSERT(!glPostShader, 'can only have 1 post effects shader');
4513
-
4616
+ if (headlessMode) return;
4514
4617
  if (!shaderCode) // default shader pass through
4515
4618
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
4516
4619
 
@@ -4549,8 +4652,7 @@ function glInitPostProcess(shaderCode, includeOverlay=false)
4549
4652
  // Render the post processing shader, called automatically by the engine
4550
4653
  function glRenderPostProcess()
4551
4654
  {
4552
- if (!glPostShader)
4553
- return;
4655
+ if (!glPostShader || headlessMode) return;
4554
4656
 
4555
4657
  // prepare to render post process shader
4556
4658
  if (glEnable)
@@ -4656,7 +4758,7 @@ const engineName = 'LittleJS';
4656
4758
  * @type {String}
4657
4759
  * @default
4658
4760
  * @memberof Engine */
4659
- const engineVersion = '1.9.5';
4761
+ const engineVersion = '1.9.7';
4660
4762
 
4661
4763
  /** Frames per second to update
4662
4764
  * @type {Number}
@@ -4733,7 +4835,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4733
4835
  mainContext.imageSmoothingEnabled = !canvasPixelated;
4734
4836
 
4735
4837
  // setup gl rendering if enabled
4736
- glEnable && glPreRender();
4838
+ glPreRender();
4737
4839
  }
4738
4840
 
4739
4841
  // internal update loop for engine
@@ -4752,11 +4854,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4752
4854
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
4753
4855
  if (!debugSpeedUp)
4754
4856
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
4857
+
4755
4858
  updateCanvas();
4756
4859
 
4757
4860
  if (paused)
4758
4861
  {
4759
- // do post update even when paused
4862
+ // update object transforms even when paused
4863
+ for (const o of engineObjects)
4864
+ o.parent || o.updateTransforms();
4760
4865
  inputUpdate();
4761
4866
  debugUpdate();
4762
4867
  gameUpdatePost();
@@ -4768,7 +4873,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4768
4873
  let deltaSmooth = 0;
4769
4874
  if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
4770
4875
  {
4771
- // force an update each frame if time is close enough (not just a fast refresh rate)
4876
+ // force at least one update each frame since it is waiting for refresh
4772
4877
  deltaSmooth = frameTimeBufferMS;
4773
4878
  frameTimeBufferMS = 0;
4774
4879
  }
@@ -4793,34 +4898,37 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4793
4898
  // add the time smoothing back in
4794
4899
  frameTimeBufferMS += deltaSmooth;
4795
4900
  }
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)
4901
+
4902
+ if (!headlessMode)
4811
4903
  {
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;
4904
+ // render sort then render while removing destroyed objects
4905
+ enginePreRender();
4906
+ gameRender();
4907
+ engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
4908
+ for (const o of engineObjects)
4909
+ o.destroyed || o.render();
4910
+ gameRenderPost();
4911
+ glRenderPostProcess();
4912
+ medalsRender();
4913
+ touchGamepadRender();
4914
+ debugRender();
4915
+ glCopyToContext(mainContext);
4916
+
4917
+ if (showWatermark)
4918
+ {
4919
+ // update fps
4920
+ overlayContext.textAlign = 'right';
4921
+ overlayContext.textBaseline = 'top';
4922
+ overlayContext.font = '1em monospace';
4923
+ overlayContext.fillStyle = '#000';
4924
+ const text = engineName + ' ' + 'v' + engineVersion + ' / '
4925
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
4926
+ + (glEnable ? ' GL' : ' 2D') ;
4927
+ overlayContext.fillText(text, mainCanvas.width-3, 3);
4928
+ overlayContext.fillStyle = '#fff';
4929
+ overlayContext.fillText(text, mainCanvas.width-2, 2);
4930
+ drawCount = 0;
4931
+ }
4824
4932
  }
4825
4933
 
4826
4934
  requestAnimationFrame(engineUpdate);
@@ -4828,6 +4936,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4828
4936
 
4829
4937
  function updateCanvas()
4830
4938
  {
4939
+ if (headlessMode) return;
4940
+
4831
4941
  if (canvasFixedSize.x)
4832
4942
  {
4833
4943
  // clear canvas and set fixed size
@@ -4855,8 +4965,20 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4855
4965
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
4856
4966
  }
4857
4967
 
4968
+ function startEngine()
4969
+ {
4970
+ gameInit();
4971
+ engineUpdate();
4972
+ }
4973
+
4974
+ if (headlessMode)
4975
+ {
4976
+ startEngine();
4977
+ return;
4978
+ }
4979
+
4858
4980
  // setup html
4859
- const styleBody =
4981
+ const styleBody =
4860
4982
  'margin:0;overflow:hidden;' + // fill the window
4861
4983
  'background:#000;' + // set background color
4862
4984
  'touch-action:none;' + // prevent mobile pinch to resize
@@ -4868,8 +4990,10 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4868
4990
  mainContext = mainCanvas.getContext('2d');
4869
4991
 
4870
4992
  // init stuff and start engine
4993
+ inputInit();
4994
+ audioInit();
4871
4995
  debugInit();
4872
- glEnable && glInit();
4996
+ glInit();
4873
4997
 
4874
4998
  // create overlay canvas for hud to appear above gl canvas
4875
4999
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
@@ -4910,12 +5034,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4910
5034
  }));
4911
5035
 
4912
5036
  // load all of the images
4913
- Promise.all(promises).then(()=>
4914
- {
4915
- // start the engine
4916
- gameInit();
4917
- engineUpdate();
4918
- });
5037
+ Promise.all(promises).then(startEngine);
4919
5038
  }
4920
5039
 
4921
5040
  /** Update each engine object, remove destroyed objects, and update time
@@ -4936,7 +5055,14 @@ function engineObjectsUpdate()
4936
5055
  }
4937
5056
  }
4938
5057
  for (const o of engineObjects)
4939
- o.parent || updateObject(o);
5058
+ {
5059
+ // update top level objects
5060
+ if (!o.parent)
5061
+ {
5062
+ updateObject(o);
5063
+ o.updateTransforms();
5064
+ }
5065
+ }
4940
5066
 
4941
5067
  // remove destroyed objects
4942
5068
  engineObjects = engineObjects.filter(o=>!o.destroyed);