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.
package/dist/littlejs.js CHANGED
@@ -153,7 +153,7 @@ function debugClear() { debugPrimitives = []; }
153
153
  * @param {String} [filename]
154
154
  * @param {String} [type]
155
155
  * @memberof Debug */
156
- function debugSaveCanvas(canvas, filename=engineName, type='image/png')
156
+ function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
157
157
  { debugSaveDataURL(canvas.toDataURL(type), filename); }
158
158
 
159
159
  /** Save a text file to disk
@@ -161,7 +161,7 @@ function debugSaveCanvas(canvas, filename=engineName, type='image/png')
161
161
  * @param {String} [filename]
162
162
  * @param {String} [type]
163
163
  * @memberof Debug */
164
- function debugSaveText(text, filename=engineName, type='text/plain')
164
+ function debugSaveText(text, filename='text', type='text/plain')
165
165
  { debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
166
166
 
167
167
  /** Save a data url to disk
@@ -181,8 +181,7 @@ function debugSaveDataURL(dataURL, filename)
181
181
  function debugInit()
182
182
  {
183
183
  // create link for saving screenshots
184
- document.body.appendChild(downloadLink = document.createElement('a'));
185
- downloadLink.style.display = 'none';
184
+ downloadLink = document.createElement('a');
186
185
  }
187
186
 
188
187
  function debugUpdate()
@@ -485,7 +484,7 @@ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? m
485
484
  * @return {Number}
486
485
  * @memberof Utilities */
487
486
  function percent(value, valueA, valueB)
488
- { return valueB-valueA ? clamp((value-valueA) / (valueB-valueA)) : 0; }
487
+ { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
489
488
 
490
489
  /** Linearly interpolates between values passed in using percent
491
490
  * @param {Number} percent
@@ -692,7 +691,7 @@ class RandomGenerator
692
691
  this.seed ^= this.seed << 13;
693
692
  this.seed ^= this.seed >>> 17;
694
693
  this.seed ^= this.seed << 5;
695
- return valueB + (valueA - valueB) * abs(this.seed % 1e9) / 1e9;
694
+ return valueB + (valueA - valueB) * abs(this.seed % 1e8) / 1e8;
696
695
  }
697
696
 
698
697
  /** Returns a floored seeded random value the two values passed in
@@ -703,7 +702,7 @@ class RandomGenerator
703
702
 
704
703
  /** Randomly returns either -1 or 1 deterministically
705
704
  * @return {Number} */
706
- sign() { return this.int(2) * 2 - 1; }
705
+ sign() { return this.float() > .5 ? 1 : -1; }
707
706
  }
708
707
 
709
708
  ///////////////////////////////////////////////////////////////////////////////
@@ -751,6 +750,7 @@ class Vector2
751
750
  * @param {Number} [y] - Y axis location */
752
751
  constructor(x=0, y=0)
753
752
  {
753
+ ASSERT(typeof x === 'number' && typeof y === 'number');
754
754
  /** @property {Number} - X axis location */
755
755
  this.x = x;
756
756
  /** @property {Number} - Y axis location */
@@ -1077,12 +1077,14 @@ class Color
1077
1077
  * @return {Color} */
1078
1078
  setHSLA(h=0, s=0, l=1, a=1)
1079
1079
  {
1080
+ h = mod(h,1);
1081
+ s = clamp(s);
1082
+ l = clamp(l);
1080
1083
  const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
1081
1084
  f = (p, q, t)=>
1082
- (t = ((t%1)+1)%1) < 1/6 ? p+(q-p)*6*t :
1083
- t < 1/2 ? q :
1084
- t < 2/3 ? p+(q-p)*(2/3-t)*6 : p;
1085
-
1085
+ (t = mod(t,1))*6 < 1 ? p+(q-p)*6*t :
1086
+ t*2 < 1 ? q :
1087
+ t*3 < 2 ? p+(q-p)*(4-t*6) : p;
1086
1088
  this.r = f(p, q, h + 1/3);
1087
1089
  this.g = f(p, q, h);
1088
1090
  this.b = f(p, q, h - 1/3);
@@ -1141,7 +1143,7 @@ class Color
1141
1143
  const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
1142
1144
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1143
1145
  }
1144
-
1146
+
1145
1147
  /** Set this color from a hex code
1146
1148
  * @param {String} hex - html hex code
1147
1149
  * @return {Color} */
@@ -1197,11 +1199,11 @@ class Timer
1197
1199
 
1198
1200
  /** Returns true if set and has not elapsed
1199
1201
  * @return {Boolean} */
1200
- active() { return time <= this.time; }
1202
+ active() { return time < this.time; }
1201
1203
 
1202
1204
  /** Returns true if set and elapsed
1203
1205
  * @return {Boolean} */
1204
- elapsed() { return time > this.time; }
1206
+ elapsed() { return time >= this.time; }
1205
1207
 
1206
1208
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1207
1209
  * @return {Number} */
@@ -1276,6 +1278,12 @@ let fontDefault = 'arial';
1276
1278
  * @memberof Settings */
1277
1279
  let showSplashScreen = false;
1278
1280
 
1281
+ /** Disables all rendering, audio, and input for servers
1282
+ * @type {Boolean}
1283
+ * @default
1284
+ * @memberof Settings */
1285
+ let headlessMode = false;
1286
+
1279
1287
  ///////////////////////////////////////////////////////////////////////////////
1280
1288
  // WebGL settings
1281
1289
 
@@ -1304,7 +1312,7 @@ let tileSizeDefault = vec2(16);
1304
1312
  * @type {Number}
1305
1313
  * @default
1306
1314
  * @memberof Settings */
1307
- let tileFixBleedScale = .1;
1315
+ let tileFixBleedScale = .5;
1308
1316
 
1309
1317
  ///////////////////////////////////////////////////////////////////////////////
1310
1318
  // Object settings
@@ -1429,7 +1437,7 @@ let soundEnable = true;
1429
1437
  * @type {Number}
1430
1438
  * @default
1431
1439
  * @memberof Settings */
1432
- let soundVolume = .5;
1440
+ let soundVolume = .3;
1433
1441
 
1434
1442
  /** Default range where sound no longer plays
1435
1443
  * @type {Number}
@@ -1514,6 +1522,11 @@ function setFontDefault(font) { fontDefault = font; }
1514
1522
  * @memberof Settings */
1515
1523
  function setShowSplashScreen(show) { showSplashScreen = show; }
1516
1524
 
1525
+ /** Set to disalbe rendering, audio, and input for servers
1526
+ * @param {Boolean} headless
1527
+ * @memberof Settings */
1528
+ function setHeadlessMode(headless) { headlessMode = headless; }
1529
+
1517
1530
  /** Set if webgl rendering is enabled
1518
1531
  * @param {Boolean} enable
1519
1532
  * @memberof Settings */
@@ -1627,7 +1640,12 @@ function setSoundEnable(enable) { soundEnable = enable; }
1627
1640
  /** Set volume scale to apply to all sound, music and speech
1628
1641
  * @param {Number} volume
1629
1642
  * @memberof Settings */
1630
- function setSoundVolume(volume) { soundVolume = volume; }
1643
+ function setSoundVolume(volume)
1644
+ {
1645
+ soundVolume = volume;
1646
+ if (soundEnable && !headlessMode && audioGainNode)
1647
+ audioGainNode.gain.value = volume; // update gain immediatly
1648
+ }
1631
1649
 
1632
1650
  /** Set default range where sound no longer plays
1633
1651
  * @param {Number} range
@@ -1760,6 +1778,8 @@ class EngineObject
1760
1778
  this.spawnTime = time;
1761
1779
  /** @property {Array} - List of children of this object */
1762
1780
  this.children = [];
1781
+ /** @property {Boolean} - Limit object speed using linear or circular math */
1782
+ this.clampSpeedLinear = true;
1763
1783
 
1764
1784
  // parent child system
1765
1785
  /** @property {EngineObject} - Parent of object if in local space */
@@ -1783,21 +1803,46 @@ class EngineObject
1783
1803
  engineObjects.push(this);
1784
1804
  }
1785
1805
 
1786
- /** Update the object transform and physics, called automatically by engine once each frame */
1787
- update()
1806
+ /** Update the object transform, called automatically by engine even when paused */
1807
+ updateTransforms()
1788
1808
  {
1789
1809
  const parent = this.parent;
1790
1810
  if (parent)
1791
1811
  {
1792
1812
  // copy parent pos/angle
1793
- this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(),1)).rotate(-parent.angle).add(parent.pos);
1794
- this.angle = parent.getMirrorSign()*this.localAngle + parent.angle;
1795
- return;
1813
+ const mirror = parent.getMirrorSign();
1814
+ this.pos = this.localPos.multiply(vec2(mirror,1)).rotate(-parent.angle).add(parent.pos);
1815
+ this.angle = mirror*this.localAngle + parent.angle;
1796
1816
  }
1797
1817
 
1818
+ // update children
1819
+ for (const child of this.children)
1820
+ child.updateTransforms();
1821
+ }
1822
+
1823
+ /** Update the object physics, called automatically by engine once each frame */
1824
+ update()
1825
+ {
1826
+ // child objects do not have physics
1827
+ if (this.parent)
1828
+ return;
1829
+
1798
1830
  // limit max speed to prevent missing collisions
1799
- this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1800
- this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1831
+ if (this.clampSpeedLinear)
1832
+ {
1833
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1834
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1835
+ }
1836
+ else
1837
+ {
1838
+ const length2 = this.velocity.lengthSquared();
1839
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
1840
+ {
1841
+ const s = objectMaxSpeed / length2**.5;
1842
+ this.velocity.x *= s;
1843
+ this.velocity.y *= s;
1844
+ }
1845
+ }
1801
1846
 
1802
1847
  // apply physics
1803
1848
  const oldPos = this.pos.copy();
@@ -1809,8 +1854,7 @@ class EngineObject
1809
1854
  // physics sanity checks
1810
1855
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1811
1856
  ASSERT(this.damping >= 0 && this.damping <= 1);
1812
-
1813
- if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
1857
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
1814
1858
  return;
1815
1859
 
1816
1860
  const wasMovingDown = this.velocity.y < 0;
@@ -2140,6 +2184,9 @@ let drawCount;
2140
2184
  */
2141
2185
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
2142
2186
  {
2187
+ if (headlessMode)
2188
+ return new TileInfo;
2189
+
2143
2190
  // if size is a number, make it a vector
2144
2191
  if (typeof size === 'number')
2145
2192
  {
@@ -2173,9 +2220,9 @@ class TileInfo
2173
2220
  constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
2174
2221
  {
2175
2222
  /** @property {Vector2} - Top left corner of tile in pixels */
2176
- this.pos = pos;
2223
+ this.pos = pos.copy();
2177
2224
  /** @property {Vector2} - Size of tile in pixels */
2178
- this.size = size;
2225
+ this.size = size.copy();
2179
2226
  /** @property {Number} - Texture index to use */
2180
2227
  this.textureIndex = textureIndex;
2181
2228
  }
@@ -2267,7 +2314,7 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
2267
2314
  * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
2268
2315
  * @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2269
2316
  * @param {Boolean} [screenSpace=false] - If true the pos and size are in screen space
2270
- * @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2317
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2271
2318
  * @memberof Draw */
2272
2319
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2273
2320
  angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
@@ -2340,7 +2387,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2340
2387
  * @param {Number} [angle]
2341
2388
  * @param {Boolean} [useWebGL=glEnable]
2342
2389
  * @param {Boolean} [screenSpace=false]
2343
- * @param {CanvasRenderingContext2D} [context]
2390
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2344
2391
  * @memberof Draw */
2345
2392
  function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2346
2393
  {
@@ -2354,7 +2401,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2354
2401
  * @param {Color} [color=(1,1,1,1)]
2355
2402
  * @param {Boolean} [useWebGL=glEnable]
2356
2403
  * @param {Boolean} [screenSpace=false]
2357
- * @param {CanvasRenderingContext2D} [context]
2404
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2358
2405
  * @memberof Draw */
2359
2406
  function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
2360
2407
  {
@@ -2370,7 +2417,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
2370
2417
  * @param {Boolean} mirror
2371
2418
  * @param {Function} drawFunction
2372
2419
  * @param {Boolean} [screenSpace=false]
2373
- * @param {CanvasRenderingContext2D} [context=mainContext]
2420
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2374
2421
  * @memberof Draw */
2375
2422
  function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
2376
2423
  {
@@ -2391,7 +2438,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
2391
2438
  /** Enable normal or additive blend mode
2392
2439
  * @param {Boolean} [additive]
2393
2440
  * @param {Boolean} [useWebGL=glEnable]
2394
- * @param {CanvasRenderingContext2D} [context=mainContext]
2441
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2395
2442
  * @memberof Draw */
2396
2443
  function setBlendMode(additive, useWebGL=glEnable, context)
2397
2444
  {
@@ -2416,7 +2463,7 @@ function setBlendMode(additive, useWebGL=glEnable, context)
2416
2463
  * @param {Color} [lineColor=(0,0,0,1)]
2417
2464
  * @param {CanvasTextAlign} [textAlign='center']
2418
2465
  * @param {String} [font=fontDefault]
2419
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2466
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2420
2467
  * @memberof Draw */
2421
2468
  function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
2422
2469
  {
@@ -2433,7 +2480,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
2433
2480
  * @param {Color} [lineColor=(0,0,0,1)]
2434
2481
  * @param {CanvasTextAlign} [textAlign]
2435
2482
  * @param {String} [font=fontDefault]
2436
- * @param {CanvasRenderingContext2D} [context=overlayContext]
2483
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2437
2484
  * @memberof Draw */
2438
2485
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
2439
2486
  {
@@ -2476,7 +2523,7 @@ class FontImage
2476
2523
  * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
2477
2524
  * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
2478
2525
  * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
2479
- * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
2526
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
2480
2527
  */
2481
2528
  constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
2482
2529
  {
@@ -2684,7 +2731,7 @@ function gamepadWasReleased(button, gamepad=0)
2684
2731
  * @return {Vector2}
2685
2732
  * @memberof Input */
2686
2733
  function gamepadStick(stick, gamepad=0)
2687
- { return stickData[gamepad] ? stickData[gamepad][stick] || vec2() : vec2(); }
2734
+ { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
2688
2735
 
2689
2736
  ///////////////////////////////////////////////////////////////////////////////
2690
2737
  // Input update called by engine
@@ -2695,6 +2742,8 @@ let inputData = [[]];
2695
2742
 
2696
2743
  function inputUpdate()
2697
2744
  {
2745
+ if (headlessMode) return;
2746
+
2698
2747
  // clear input when lost focus (prevent stuck keys)
2699
2748
  isTouchDevice || document.hasFocus() || clearInput();
2700
2749
 
@@ -2707,6 +2756,8 @@ function inputUpdate()
2707
2756
 
2708
2757
  function inputUpdatePost()
2709
2758
  {
2759
+ if (headlessMode) return;
2760
+
2710
2761
  // clear input to prepare for next frame
2711
2762
  for (const deviceInputData of inputData)
2712
2763
  for (const i in deviceInputData)
@@ -2715,9 +2766,12 @@ function inputUpdatePost()
2715
2766
  }
2716
2767
 
2717
2768
  ///////////////////////////////////////////////////////////////////////////////
2718
- // Keyboard event handlers
2769
+ // Input event handlers
2719
2770
 
2771
+ function inputInit()
2720
2772
  {
2773
+ if (headlessMode) return;
2774
+
2721
2775
  onkeydown = (e)=>
2722
2776
  {
2723
2777
  if (debug && e.target != document.body) return;
@@ -2748,21 +2802,29 @@ function inputUpdatePost()
2748
2802
  c == 'KeyA' ? 'ArrowLeft' :
2749
2803
  c == 'KeyD' ? 'ArrowRight' : c : c;
2750
2804
  }
2805
+
2806
+ // mouse event handlers
2807
+ onmousedown = (e)=>
2808
+ {
2809
+ isUsingGamepad = false;
2810
+ inputData[0][e.button] = 3;
2811
+ mousePosScreen = mouseToScreen(e);
2812
+ e.button && e.preventDefault();
2813
+ }
2814
+ onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2815
+ onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2816
+ onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2817
+ oncontextmenu = (e)=> false; // prevent right click menu
2818
+
2819
+ // init touch input
2820
+ if (isTouchDevice)
2821
+ touchInputInit();
2751
2822
  }
2752
2823
 
2753
- ///////////////////////////////////////////////////////////////////////////////
2754
- // Mouse event handlers
2755
-
2756
- onmousedown = (e)=> {isUsingGamepad = false; inputData[0][e.button] = 3; mousePosScreen = mouseToScreen(e); e.button && e.preventDefault();}
2757
- onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
2758
- onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2759
- onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2760
- oncontextmenu = (e)=> false; // prevent right click menu
2761
-
2762
2824
  // convert a mouse or touch event position to screen space
2763
2825
  function mouseToScreen(mousePos)
2764
2826
  {
2765
- if (!mainCanvas)
2827
+ if (!mainCanvas || headlessMode)
2766
2828
  return vec2(); // fix bug that can occur if user clicks before page loads
2767
2829
 
2768
2830
  const rect = mainCanvas.getBoundingClientRect();
@@ -2774,7 +2836,7 @@ function mouseToScreen(mousePos)
2774
2836
  // Gamepad input
2775
2837
 
2776
2838
  // gamepad internal variables
2777
- const stickData = [];
2839
+ const gamepadStickData = [];
2778
2840
 
2779
2841
  // gamepads are updated by engine every frame automatically
2780
2842
  function gamepadsUpdate()
@@ -2791,14 +2853,11 @@ function gamepadsUpdate()
2791
2853
  // update touch gamepad if enabled
2792
2854
  if (touchGamepadEnable && isTouchDevice)
2793
2855
  {
2794
- // create the touch gamepad if it doesn't exist
2795
- if (!touchGamepadButtons)
2796
- createTouchGamepad();
2797
-
2856
+ ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
2798
2857
  if (touchGamepadTimer.isSet())
2799
2858
  {
2800
2859
  // read virtual analog stick
2801
- const sticks = stickData[0] || (stickData[0] = []);
2860
+ const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
2802
2861
  sticks[0] = vec2();
2803
2862
  if (touchGamepadAnalog)
2804
2863
  sticks[0] = applyDeadZones(touchGamepadStick);
@@ -2815,7 +2874,8 @@ function gamepadsUpdate()
2815
2874
  for (let i=10; i--;)
2816
2875
  {
2817
2876
  const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
2818
- data[j] = touchGamepadButtons[i] ? gamepadIsDown(j,0) ? 1 : 3 : gamepadIsDown(j,0) ? 4 : 0;
2877
+ const wasDown = gamepadIsDown(j,0);
2878
+ data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
2819
2879
  }
2820
2880
  }
2821
2881
  }
@@ -2835,7 +2895,7 @@ function gamepadsUpdate()
2835
2895
  // get or create gamepad data
2836
2896
  const gamepad = gamepads[i];
2837
2897
  const data = inputData[i+1] || (inputData[i+1] = []);
2838
- const sticks = stickData[i] || (stickData[i] = []);
2898
+ const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
2839
2899
 
2840
2900
  if (gamepad)
2841
2901
  {
@@ -2874,28 +2934,44 @@ function gamepadsUpdate()
2874
2934
  * @param {Number|Array} [pattern] - single value in ms or vibration interval array
2875
2935
  * @memberof Input */
2876
2936
  function vibrate(pattern=100)
2877
- { vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2937
+ { vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
2878
2938
 
2879
2939
  /** Cancel any ongoing vibration
2880
2940
  * @memberof Input */
2881
2941
  function vibrateStop() { vibrate(0); }
2882
2942
 
2883
2943
  ///////////////////////////////////////////////////////////////////////////////
2884
- // Touch input
2944
+ // Touch input & virtual on screen gamepad
2885
2945
 
2886
2946
  /** True if a touch device has been detected
2887
2947
  * @memberof Input */
2888
- const isTouchDevice = window.ontouchstart !== undefined;
2948
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
2949
+
2950
+ // touch gamepad internal variables
2951
+ let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2889
2952
 
2890
2953
  // try to enable touch mouse
2891
- if (isTouchDevice)
2954
+ function touchInputInit()
2892
2955
  {
2956
+ // add non passive touch event listeners
2957
+ let handleTouch = handleTouchDefault;
2958
+ if (touchGamepadEnable)
2959
+ {
2960
+ // touch input internal variables
2961
+ handleTouch = handleTouchGamepad;
2962
+ touchGamepadButtons = [];
2963
+ touchGamepadStick = vec2();
2964
+ }
2965
+ document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
2966
+ document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
2967
+ document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
2968
+
2893
2969
  // override mouse events
2894
- let wasTouching;
2895
2970
  onmousedown = onmouseup = ()=> 0;
2896
2971
 
2897
2972
  // handle all touch events the same way
2898
- ontouchstart = ontouchmove = ontouchend = (e)=>
2973
+ let wasTouching;
2974
+ function handleTouchDefault(e)
2899
2975
  {
2900
2976
  // fix stalled audio requiring user interaction
2901
2977
  if (soundEnable && audioContext && audioContext.state != 'running')
@@ -2924,27 +3000,14 @@ if (isTouchDevice)
2924
3000
  // must return true so the document will get focus
2925
3001
  return true;
2926
3002
  }
2927
- }
2928
-
2929
- ///////////////////////////////////////////////////////////////////////////////
2930
- // touch gamepad, virtual on screen gamepad emulator for touch devices
2931
-
2932
- // touch input internal variables
2933
- let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
2934
3003
 
2935
- // create the touch gamepad, called automatically by the engine
2936
- function createTouchGamepad()
2937
- {
2938
- // touch input internal variables
2939
- touchGamepadButtons = [];
2940
- touchGamepadStick = vec2();
2941
-
2942
- const touchHandler = ontouchstart;
2943
- ontouchstart = ontouchmove = ontouchend = (e)=>
3004
+ // special handling for virtual gamepad mode
3005
+ function handleTouchGamepad(e)
2944
3006
  {
2945
3007
  // clear touch gamepad input
2946
3008
  touchGamepadStick = vec2();
2947
3009
  touchGamepadButtons = [];
3010
+ isUsingGamepad = true;
2948
3011
 
2949
3012
  const touching = e.touches.length;
2950
3013
  if (touching)
@@ -2985,9 +3048,8 @@ function createTouchGamepad()
2985
3048
  }
2986
3049
  }
2987
3050
 
2988
- // call default touch handler and set to using gamepad
2989
- touchHandler.bind(window)(e);
2990
- isUsingGamepad = true;
3051
+ // call default touch handler so normal touch events still work
3052
+ handleTouchDefault(e);
2991
3053
 
2992
3054
  // must return true so the document will get focus
2993
3055
  return true;
@@ -3006,32 +3068,33 @@ function touchGamepadRender()
3006
3068
  return;
3007
3069
 
3008
3070
  // setup the canvas
3009
- overlayContext.save();
3010
- overlayContext.globalAlpha = alpha*touchGamepadAlpha;
3011
- overlayContext.strokeStyle = '#fff';
3012
- overlayContext.lineWidth = 3;
3071
+ const context = overlayContext;
3072
+ context.save();
3073
+ context.globalAlpha = alpha*touchGamepadAlpha;
3074
+ context.strokeStyle = '#fff';
3075
+ context.lineWidth = 3;
3013
3076
 
3014
3077
  // draw left analog stick
3015
- overlayContext.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
3016
- overlayContext.beginPath();
3078
+ context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
3079
+ context.beginPath();
3017
3080
 
3018
3081
  const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
3019
3082
  if (touchGamepadAnalog) // draw circle shaped gamepad
3020
3083
  {
3021
- overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
3022
- overlayContext.fill();
3023
- overlayContext.stroke();
3084
+ context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
3085
+ context.fill();
3086
+ context.stroke();
3024
3087
  }
3025
3088
  else // draw cross shaped gamepad
3026
3089
  {
3027
3090
  for(let i=10; i--;)
3028
3091
  {
3029
3092
  const angle = i*PI/4;
3030
- overlayContext.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
3031
- i%2 && overlayContext.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
3032
- i==1 && overlayContext.fill();
3093
+ context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
3094
+ i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
3095
+ i==1 && context.fill();
3033
3096
  }
3034
- overlayContext.stroke();
3097
+ context.stroke();
3035
3098
  }
3036
3099
 
3037
3100
  // draw right face buttons
@@ -3039,15 +3102,15 @@ function touchGamepadRender()
3039
3102
  for (let i=4; i--;)
3040
3103
  {
3041
3104
  const pos = rightCenter.add(vec2().setDirection(i, touchGamepadSize/2));
3042
- overlayContext.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
3043
- overlayContext.beginPath();
3044
- overlayContext.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
3045
- overlayContext.fill();
3046
- overlayContext.stroke();
3105
+ context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
3106
+ context.beginPath();
3107
+ context.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
3108
+ context.fill();
3109
+ context.stroke();
3047
3110
  }
3048
3111
 
3049
3112
  // set canvas back to normal
3050
- overlayContext.restore();
3113
+ context.restore();
3051
3114
  }
3052
3115
  /**
3053
3116
  * LittleJS Audio System
@@ -3062,6 +3125,32 @@ function touchGamepadRender()
3062
3125
 
3063
3126
 
3064
3127
 
3128
+ /** Audio context used by the engine
3129
+ * @type {AudioContext}
3130
+ * @memberof Audio */
3131
+ let audioContext;
3132
+
3133
+ /** Master gain node for all audio to pass through
3134
+ * @type {GainNode}
3135
+ * @memberof Audio */
3136
+ let audioGainNode;
3137
+
3138
+ function audioInit()
3139
+ {
3140
+ if (!soundEnable || headlessMode) return;
3141
+
3142
+ // create audio context
3143
+ audioContext = new AudioContext;
3144
+
3145
+ // create and connect gain node
3146
+ // (createGain is more widely spported then GainNode construtor)
3147
+ audioGainNode = audioContext.createGain();
3148
+ audioGainNode.connect(audioContext.destination);
3149
+ setSoundVolume(soundVolume); // update gain volume
3150
+ }
3151
+
3152
+ ///////////////////////////////////////////////////////////////////////////////
3153
+
3065
3154
  /**
3066
3155
  * Sound Object - Stores a sound for later use and can be played positionally
3067
3156
  *
@@ -3082,7 +3171,7 @@ class Sound
3082
3171
  */
3083
3172
  constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
3084
3173
  {
3085
- if (!soundEnable) return;
3174
+ if (!soundEnable || headlessMode) return;
3086
3175
 
3087
3176
  /** @property {Number} - World space max range of sound, will not play if camera is farther away */
3088
3177
  this.range = range;
@@ -3096,8 +3185,8 @@ class Sound
3096
3185
  if (zzfxSound)
3097
3186
  {
3098
3187
  // generate zzfx sound now for fast playback
3099
- this.randomness = zzfxSound[1] || 0;
3100
- zzfxSound[1] = 0; // generate without randomness
3188
+ const defaultRandomness = .05;
3189
+ this.randomness = zzfxSound[1] || defaultRandomness;
3101
3190
  this.sampleChannels = [zzfxG(...zzfxSound)];
3102
3191
  this.sampleRate = zzfxR;
3103
3192
  }
@@ -3113,7 +3202,8 @@ class Sound
3113
3202
  */
3114
3203
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
3115
3204
  {
3116
- if (!soundEnable || !this.sampleChannels) return;
3205
+ if (!soundEnable || headlessMode) return;
3206
+ if (!this.sampleChannels) return;
3117
3207
 
3118
3208
  let pan;
3119
3209
  if (pos)
@@ -3190,14 +3280,14 @@ class SoundWave extends Sound
3190
3280
  * @param {Number} [randomness] - How much to randomize frequency each time sound plays
3191
3281
  * @param {Number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
3192
3282
  * @param {Number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
3283
+ * @param {Function} [onloadCallback] - callback function to call when sound is loaded
3193
3284
  */
3194
- constructor(filename, randomness=0, range, taper)
3285
+ constructor(filename, randomness=0, range, taper, onloadCallback)
3195
3286
  {
3196
3287
  super(undefined, range, taper);
3197
- this.randomness = randomness;
3198
-
3199
- if (!soundEnable) return;
3288
+ if (!soundEnable || headlessMode) return;
3200
3289
 
3290
+ this.randomness = randomness;
3201
3291
  fetch(filename)
3202
3292
  .then(response => response.arrayBuffer())
3203
3293
  .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
@@ -3207,10 +3297,23 @@ class SoundWave extends Sound
3207
3297
  for (let i = audioBuffer.numberOfChannels; i--;)
3208
3298
  this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
3209
3299
  this.sampleRate = audioBuffer.sampleRate;
3210
- });
3300
+ }).then(() => onloadCallback && onloadCallback(this));
3211
3301
  }
3212
3302
  }
3213
3303
 
3304
+ /** Play an mp3, ogg, or wav audio from a local file or url
3305
+ * @param {String} filename - Location of sound file to play
3306
+ * @param {Number} [volume] - How much to scale volume by
3307
+ * @param {Boolean} [loop] - True if the music should loop
3308
+ * @return {SoundWave} - The sound object for this file
3309
+ * @memberof Audio */
3310
+ function playAudioFile(filename, volume=1, loop=false)
3311
+ {
3312
+ if (!soundEnable || headlessMode) return;
3313
+
3314
+ return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
3315
+ }
3316
+
3214
3317
  /**
3215
3318
  * Music Object - Stores a zzfx music track for later use
3216
3319
  *
@@ -3250,7 +3353,7 @@ class Music extends Sound
3250
3353
  {
3251
3354
  super(undefined);
3252
3355
 
3253
- if (!soundEnable) return;
3356
+ if (!soundEnable || headlessMode) return;
3254
3357
  this.randomness = 0;
3255
3358
  this.sampleChannels = zzfxM(...zzfxMusic);
3256
3359
  this.sampleRate = zzfxR;
@@ -3265,23 +3368,6 @@ class Music extends Sound
3265
3368
  { return super.play(undefined, volume, 1, 1, loop); }
3266
3369
  }
3267
3370
 
3268
- /** Play an mp3, ogg, or wav audio from a local file or url
3269
- * @param {String} filename - Location of sound file to play
3270
- * @param {Number} [volume] - How much to scale volume by
3271
- * @param {Boolean} [loop] - True if the music should loop
3272
- * @return {HTMLAudioElement} - The audio element for this sound
3273
- * @memberof Audio */
3274
- function playAudioFile(filename, volume=1, loop=false)
3275
- {
3276
- if (!soundEnable) return;
3277
-
3278
- const audio = new Audio(filename);
3279
- audio.volume = soundVolume * volume;
3280
- audio.loop = loop;
3281
- audio.play();
3282
- return audio;
3283
- }
3284
-
3285
3371
  /** Speak text with passed in settings
3286
3372
  * @param {String} text - The text to speak
3287
3373
  * @param {String} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
@@ -3292,7 +3378,8 @@ function playAudioFile(filename, volume=1, loop=false)
3292
3378
  * @memberof Audio */
3293
3379
  function speak(text, language='', volume=1, rate=1, pitch=1)
3294
3380
  {
3295
- if (!soundEnable || !speechSynthesis) return;
3381
+ if (!soundEnable || headlessMode) return;
3382
+ if (!speechSynthesis) return;
3296
3383
 
3297
3384
  // common languages (not supported by all browsers)
3298
3385
  // en - english, it - italian, fr - french, de - german, es - spanish
@@ -3322,14 +3409,8 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
3322
3409
 
3323
3410
  ///////////////////////////////////////////////////////////////////////////////
3324
3411
 
3325
- /** Audio context used by the engine
3326
- * @type {AudioContext}
3327
- * @memberof Audio */
3328
- let audioContext = new AudioContext;
3329
-
3330
- /** Keep track if audio was suspended when last sound was played
3331
- * @type {Boolean}
3332
- * @memberof Audio */
3412
+ // internal tracking if audio was suspended when last sound was played
3413
+ // allows first suspended sound to play when audio is resumed
3333
3414
  let audioSuspended = false;
3334
3415
 
3335
3416
  /** Play cached audio samples with given settings
@@ -3343,7 +3424,7 @@ let audioSuspended = false;
3343
3424
  * @memberof Audio */
3344
3425
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
3345
3426
  {
3346
- if (!soundEnable) return;
3427
+ if (!soundEnable || headlessMode) return;
3347
3428
 
3348
3429
  // prevent sounds from building up if they can't be played
3349
3430
  const audioWasSuspended = audioSuspended;
@@ -3367,10 +3448,13 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3367
3448
  source.playbackRate.value = rate;
3368
3449
  source.loop = loop;
3369
3450
 
3370
- // create and connect gain node (createGain is more widely spported then GainNode construtor)
3451
+ // set master gain volume
3452
+ setSoundVolume(soundVolume);
3453
+
3454
+ // create and connect gain node
3371
3455
  const gainNode = audioContext.createGain();
3372
- gainNode.gain.value = soundVolume*volume;
3373
- gainNode.connect(audioContext.destination);
3456
+ gainNode.gain.value = volume;
3457
+ gainNode.connect(audioGainNode);
3374
3458
 
3375
3459
  // connect source to stereo panner and gain
3376
3460
  source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
@@ -3389,7 +3473,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3389
3473
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
3390
3474
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3391
3475
  * @memberof Audio */
3392
- function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
3476
+ function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
3393
3477
 
3394
3478
  /** Sample rate used for all ZzFX sounds
3395
3479
  * @default 44100
@@ -3398,7 +3482,7 @@ const zzfxR = 44100;
3398
3482
 
3399
3483
  /** Generate samples for a ZzFX sound
3400
3484
  * @param {Number} [volume] - Volume scale (percent)
3401
- * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
3485
+ * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
3402
3486
  * @param {Number} [frequency] - Frequency of sound (Hz)
3403
3487
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
3404
3488
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -3424,17 +3508,18 @@ const zzfxR = 44100;
3424
3508
  function zzfxG
3425
3509
  (
3426
3510
  // parameters
3427
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3511
+ volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
3428
3512
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3429
3513
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3430
3514
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3431
3515
  )
3432
3516
  {
3517
+ // LJS Note: ZZFX modded so randomness is handled by Sound class
3518
+
3433
3519
  // init parameters
3434
3520
  let PI2 = PI*2, sampleRate = zzfxR,
3435
3521
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3436
- startFrequency = frequency *=
3437
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3522
+ startFrequency = frequency *= PI2 / sampleRate,
3438
3523
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3439
3524
 
3440
3525
  // biquad LP/HP filter
@@ -3456,7 +3541,6 @@ function zzfxG
3456
3541
  pitchJump *= PI2 / sampleRate;
3457
3542
  pitchJumpTime *= sampleRate;
3458
3543
  repeatTime = repeatTime * sampleRate | 0;
3459
- volume *= soundVolume;
3460
3544
 
3461
3545
  // generate waveform
3462
3546
  for(length = attack + decay + sustain + release + delay | 0;
@@ -3802,7 +3886,7 @@ class TileLayer extends EngineObject
3802
3886
 
3803
3887
  /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
3804
3888
  this.canvas = document.createElement('canvas');
3805
- /** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3889
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
3806
3890
  this.context = this.canvas.getContext('2d');
3807
3891
  /** @property {Vector2} - How much to scale this layer when rendered */
3808
3892
  this.scale = scale;
@@ -3813,6 +3897,17 @@ class TileLayer extends EngineObject
3813
3897
  this.data = [];
3814
3898
  for (let j = this.size.area(); j--;)
3815
3899
  this.data.push(new TileLayerData);
3900
+
3901
+ if (headlessMode)
3902
+ {
3903
+ // disable rendering
3904
+ this.redraw = () => {};
3905
+ this.render = () => {};
3906
+ this.redrawStart = () => {};
3907
+ this.redrawEnd = () => {};
3908
+ this.drawTileData = () => {};
3909
+ this.drawCanvas2D = () => {};
3910
+ }
3816
3911
  }
3817
3912
 
3818
3913
  /** Set data at a given position in the array
@@ -3843,7 +3938,7 @@ class TileLayer extends EngineObject
3843
3938
  ASSERT(mainContext != this.context, 'must call redrawEnd() after drawing tiles');
3844
3939
 
3845
3940
  // flush and copy gl canvas because tile canvas does not use webgl
3846
- glEnable && !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3941
+ !glOverlay && !this.isOverlay && glCopyToContext(mainContext);
3847
3942
 
3848
3943
  // draw the entire cached level onto the canvas
3849
3944
  const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
@@ -3893,14 +3988,14 @@ class TileLayer extends EngineObject
3893
3988
  this.context.imageSmoothingEnabled = !canvasPixelated;
3894
3989
 
3895
3990
  // setup gl rendering if enabled
3896
- glEnable && glPreRender();
3991
+ glPreRender();
3897
3992
  }
3898
3993
 
3899
3994
  /** Call to end the redraw process */
3900
3995
  redrawEnd()
3901
3996
  {
3902
3997
  ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3903
- glEnable && glCopyToContext(mainContext, true);
3998
+ glCopyToContext(mainContext, true);
3904
3999
  //debugSaveCanvas(this.canvas);
3905
4000
 
3906
4001
  // set stuff back to normal
@@ -4225,20 +4320,21 @@ class ParticleEmitter extends EngineObject
4225
4320
  class Particle extends EngineObject
4226
4321
  {
4227
4322
  /**
4228
- * Create a particle with the given shis.colorStart = undefined;ettings
4229
- * @param {Vector2} position - World space position of the particle
4230
- * @param {TileInfo} [tileInfo] - Tile info to render particles
4231
- * @param {Number} [angle] - Angle to rotate the particle
4232
- * @param {Color} [colorStart] - Color at start of life
4233
- * @param {Color} [colorEnd] - Color at end of life
4234
- * @param {Number} [lifeTime] - How long to live for
4235
- * @param {Number} [sizeStart] - Angle to rotate the particle
4236
- * @param {Number} [sizeEnd] - Angle to rotate the particle
4237
- * @param {Number} [fadeRate] - Angle to rotate the particle
4238
- * @param {Boolean} [additive] - Angle to rotate the particle
4239
- * @param {Number} [trailScale] - If a trail, how long to make it
4323
+ * Create a particle with the passed in settings
4324
+ * Typically this is created automatically by a ParticleEmitter
4325
+ * @param {Vector2} position - World space position of the particle
4326
+ * @param {TileInfo} tileInfo - Tile info to render particles
4327
+ * @param {Number} angle - Angle to rotate the particle
4328
+ * @param {Color} colorStart - Color at start of life
4329
+ * @param {Color} colorEnd - Color at end of life
4330
+ * @param {Number} lifeTime - How long to live for
4331
+ * @param {Number} sizeStart - Size at start of life
4332
+ * @param {Number} sizeEnd - Size at end of life
4333
+ * @param {Number} fadeRate - How quick to fade in/out
4334
+ * @param {Boolean} additive - Does it use additive blend mode
4335
+ * @param {Number} trailScale - If a trail, how long to make it
4240
4336
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
4241
- * @param {Function} [destroyCallback] - Called when particle dies
4337
+ * @param {Function} [destroyCallback] - Callback when particle dies
4242
4338
  */
4243
4339
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
4244
4340
  )
@@ -4265,6 +4361,9 @@ class Particle extends EngineObject
4265
4361
  this.localSpaceEmitter = localSpaceEmitter;
4266
4362
  /** @property {Function} - Called when particle dies */
4267
4363
  this.destroyCallback = destroyCallback;
4364
+
4365
+ // particles use circular clamped speed
4366
+ this.clampSpeedLinear = false;
4268
4367
  }
4269
4368
 
4270
4369
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -4647,6 +4746,8 @@ let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData,
4647
4746
  // Initalize WebGL, called automatically by the engine
4648
4747
  function glInit()
4649
4748
  {
4749
+ if (!glEnable || headlessMode) return;
4750
+
4650
4751
  // create the canvas and textures
4651
4752
  glCanvas = document.createElement('canvas');
4652
4753
  glContext = glCanvas.getContext('webgl2');
@@ -4659,11 +4760,11 @@ function glInit()
4659
4760
  '#version 300 es\n' + // specify GLSL ES version
4660
4761
  'precision highp float;'+ // use highp for better accuracy
4661
4762
  'uniform mat4 m;'+ // transform matrix
4662
- 'in vec2 g;'+ // geometry
4663
- 'in vec4 p,u,c,a;'+ // position/size, uvs, color, additiveColor
4664
- 'in float r;'+ // rotation
4665
- 'out vec2 v;'+ // return uv, color, additiveColor
4666
- 'out vec4 d,e;'+ // return uv, color, additiveColor
4763
+ 'in vec2 g;'+ // in: geometry
4764
+ 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
4765
+ 'in float r;'+ // in: rotation
4766
+ 'out vec2 v;'+ // out: uv
4767
+ 'out vec4 d,e;'+ // out: color, additiveColor
4667
4768
  'void main(){'+ // shader entry point
4668
4769
  'vec2 s=(g-.5)*p.zw;'+ // get size offset
4669
4770
  'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
@@ -4673,10 +4774,10 @@ function glInit()
4673
4774
  ,
4674
4775
  '#version 300 es\n' + // specify GLSL ES version
4675
4776
  'precision highp float;'+ // use highp for better accuracy
4676
- 'in vec2 v;'+ // uv
4677
- 'in vec4 d,e;'+ // color, additiveColor
4678
4777
  'uniform sampler2D s;'+ // texture
4679
- 'out vec4 c;'+ // out color
4778
+ 'in vec2 v;'+ // in: uv
4779
+ 'in vec4 d,e;'+ // in: color, additiveColor
4780
+ 'out vec4 c;'+ // out: color
4680
4781
  'void main(){'+ // shader entry point
4681
4782
  'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
4682
4783
  '}' // end of shader
@@ -4698,9 +4799,11 @@ function glInit()
4698
4799
  // Setup render each frame, called automatically by engine
4699
4800
  function glPreRender()
4700
4801
  {
4802
+ if (!glEnable || headlessMode) return;
4803
+
4701
4804
  // clear and set to same size as main canvas
4702
4805
  glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
4703
- glContext.clear(gl_COLOR_BUFFER_BIT);
4806
+ //glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
4704
4807
 
4705
4808
  // set up the shader
4706
4809
  glContext.useProgram(glShader);
@@ -4734,12 +4837,12 @@ function glPreRender()
4734
4837
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
4735
4838
  const p = vec2(-1).subtract(cameraPos.multiply(s));
4736
4839
  glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
4737
- new Float32Array([
4840
+ [
4738
4841
  s.x, 0, 0, 0,
4739
4842
  0, s.y, 0, 0,
4740
4843
  1, 1, 1, 1,
4741
4844
  p.x, p.y, 0, 0
4742
- ])
4845
+ ]
4743
4846
  );
4744
4847
  }
4745
4848
 
@@ -4750,7 +4853,7 @@ function glPreRender()
4750
4853
  function glSetTexture(texture)
4751
4854
  {
4752
4855
  // must flush cache with the old texture to set a new one
4753
- if (texture == glActiveTexture)
4856
+ if (headlessMode || texture == glActiveTexture)
4754
4857
  return;
4755
4858
 
4756
4859
  glFlush();
@@ -4810,7 +4913,6 @@ function glCreateTexture(image)
4810
4913
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
4811
4914
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
4812
4915
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
4813
-
4814
4916
  return texture;
4815
4917
  }
4816
4918
 
@@ -4834,12 +4936,12 @@ function glFlush()
4834
4936
  }
4835
4937
 
4836
4938
  /** Draw any sprites still in the buffer, copy to main canvas and clear
4837
- * @param {CanvasRenderingContext2D} context
4939
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4838
4940
  * @param {Boolean} [forceDraw]
4839
4941
  * @memberof WebGL */
4840
4942
  function glCopyToContext(context, forceDraw=false)
4841
4943
  {
4842
- if (!glInstanceCount && !forceDraw) return;
4944
+ if (!glEnable || !glInstanceCount && !forceDraw) return;
4843
4945
 
4844
4946
  glFlush();
4845
4947
 
@@ -4896,7 +4998,7 @@ let glPostShader, glPostTexture, glPostIncludeOverlay;
4896
4998
  function glInitPostProcess(shaderCode, includeOverlay=false)
4897
4999
  {
4898
5000
  ASSERT(!glPostShader, 'can only have 1 post effects shader');
4899
-
5001
+ if (headlessMode) return;
4900
5002
  if (!shaderCode) // default shader pass through
4901
5003
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
4902
5004
 
@@ -4935,8 +5037,7 @@ function glInitPostProcess(shaderCode, includeOverlay=false)
4935
5037
  // Render the post processing shader, called automatically by the engine
4936
5038
  function glRenderPostProcess()
4937
5039
  {
4938
- if (!glPostShader)
4939
- return;
5040
+ if (!glPostShader || headlessMode) return;
4940
5041
 
4941
5042
  // prepare to render post process shader
4942
5043
  if (glEnable)
@@ -5042,7 +5143,7 @@ const engineName = 'LittleJS';
5042
5143
  * @type {String}
5043
5144
  * @default
5044
5145
  * @memberof Engine */
5045
- const engineVersion = '1.9.5';
5146
+ const engineVersion = '1.9.7';
5046
5147
 
5047
5148
  /** Frames per second to update
5048
5149
  * @type {Number}
@@ -5119,7 +5220,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5119
5220
  mainContext.imageSmoothingEnabled = !canvasPixelated;
5120
5221
 
5121
5222
  // setup gl rendering if enabled
5122
- glEnable && glPreRender();
5223
+ glPreRender();
5123
5224
  }
5124
5225
 
5125
5226
  // internal update loop for engine
@@ -5138,11 +5239,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5138
5239
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
5139
5240
  if (!debugSpeedUp)
5140
5241
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
5242
+
5141
5243
  updateCanvas();
5142
5244
 
5143
5245
  if (paused)
5144
5246
  {
5145
- // do post update even when paused
5247
+ // update object transforms even when paused
5248
+ for (const o of engineObjects)
5249
+ o.parent || o.updateTransforms();
5146
5250
  inputUpdate();
5147
5251
  debugUpdate();
5148
5252
  gameUpdatePost();
@@ -5154,7 +5258,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5154
5258
  let deltaSmooth = 0;
5155
5259
  if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
5156
5260
  {
5157
- // force an update each frame if time is close enough (not just a fast refresh rate)
5261
+ // force at least one update each frame since it is waiting for refresh
5158
5262
  deltaSmooth = frameTimeBufferMS;
5159
5263
  frameTimeBufferMS = 0;
5160
5264
  }
@@ -5179,34 +5283,37 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5179
5283
  // add the time smoothing back in
5180
5284
  frameTimeBufferMS += deltaSmooth;
5181
5285
  }
5182
-
5183
- // render sort then render while removing destroyed objects
5184
- enginePreRender();
5185
- gameRender();
5186
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
5187
- for (const o of engineObjects)
5188
- o.destroyed || o.render();
5189
- gameRenderPost();
5190
- glRenderPostProcess();
5191
- medalsRender();
5192
- touchGamepadRender();
5193
- debugRender();
5194
- glEnable && glCopyToContext(mainContext);
5195
-
5196
- if (showWatermark)
5286
+
5287
+ if (!headlessMode)
5197
5288
  {
5198
- // update fps
5199
- overlayContext.textAlign = 'right';
5200
- overlayContext.textBaseline = 'top';
5201
- overlayContext.font = '1em monospace';
5202
- overlayContext.fillStyle = '#000';
5203
- const text = engineName + ' ' + 'v' + engineVersion + ' / '
5204
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
5205
- + (glEnable ? ' GL' : ' 2D') ;
5206
- overlayContext.fillText(text, mainCanvas.width-3, 3);
5207
- overlayContext.fillStyle = '#fff';
5208
- overlayContext.fillText(text, mainCanvas.width-2, 2);
5209
- drawCount = 0;
5289
+ // render sort then render while removing destroyed objects
5290
+ enginePreRender();
5291
+ gameRender();
5292
+ engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
5293
+ for (const o of engineObjects)
5294
+ o.destroyed || o.render();
5295
+ gameRenderPost();
5296
+ glRenderPostProcess();
5297
+ medalsRender();
5298
+ touchGamepadRender();
5299
+ debugRender();
5300
+ glCopyToContext(mainContext);
5301
+
5302
+ if (showWatermark)
5303
+ {
5304
+ // update fps
5305
+ overlayContext.textAlign = 'right';
5306
+ overlayContext.textBaseline = 'top';
5307
+ overlayContext.font = '1em monospace';
5308
+ overlayContext.fillStyle = '#000';
5309
+ const text = engineName + ' ' + 'v' + engineVersion + ' / '
5310
+ + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
5311
+ + (glEnable ? ' GL' : ' 2D') ;
5312
+ overlayContext.fillText(text, mainCanvas.width-3, 3);
5313
+ overlayContext.fillStyle = '#fff';
5314
+ overlayContext.fillText(text, mainCanvas.width-2, 2);
5315
+ drawCount = 0;
5316
+ }
5210
5317
  }
5211
5318
 
5212
5319
  requestAnimationFrame(engineUpdate);
@@ -5214,6 +5321,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5214
5321
 
5215
5322
  function updateCanvas()
5216
5323
  {
5324
+ if (headlessMode) return;
5325
+
5217
5326
  if (canvasFixedSize.x)
5218
5327
  {
5219
5328
  // clear canvas and set fixed size
@@ -5241,8 +5350,20 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5241
5350
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5242
5351
  }
5243
5352
 
5353
+ function startEngine()
5354
+ {
5355
+ gameInit();
5356
+ engineUpdate();
5357
+ }
5358
+
5359
+ if (headlessMode)
5360
+ {
5361
+ startEngine();
5362
+ return;
5363
+ }
5364
+
5244
5365
  // setup html
5245
- const styleBody =
5366
+ const styleBody =
5246
5367
  'margin:0;overflow:hidden;' + // fill the window
5247
5368
  'background:#000;' + // set background color
5248
5369
  'touch-action:none;' + // prevent mobile pinch to resize
@@ -5254,8 +5375,10 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5254
5375
  mainContext = mainCanvas.getContext('2d');
5255
5376
 
5256
5377
  // init stuff and start engine
5378
+ inputInit();
5379
+ audioInit();
5257
5380
  debugInit();
5258
- glEnable && glInit();
5381
+ glInit();
5259
5382
 
5260
5383
  // create overlay canvas for hud to appear above gl canvas
5261
5384
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
@@ -5296,12 +5419,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5296
5419
  }));
5297
5420
 
5298
5421
  // load all of the images
5299
- Promise.all(promises).then(()=>
5300
- {
5301
- // start the engine
5302
- gameInit();
5303
- engineUpdate();
5304
- });
5422
+ Promise.all(promises).then(startEngine);
5305
5423
  }
5306
5424
 
5307
5425
  /** Update each engine object, remove destroyed objects, and update time
@@ -5322,7 +5440,14 @@ function engineObjectsUpdate()
5322
5440
  }
5323
5441
  }
5324
5442
  for (const o of engineObjects)
5325
- o.parent || updateObject(o);
5443
+ {
5444
+ // update top level objects
5445
+ if (!o.parent)
5446
+ {
5447
+ updateObject(o);
5448
+ o.updateTransforms();
5449
+ }
5450
+ }
5326
5451
 
5327
5452
  // remove destroyed objects
5328
5453
  engineObjects = engineObjects.filter(o=>!o.destroyed);