littlejsengine 1.9.9 → 1.10.2

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.
Files changed (59) hide show
  1. package/README.md +6 -0
  2. package/dist/littlejs.d.ts +107 -14
  3. package/dist/littlejs.esm.js +250 -102
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +226 -98
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +208 -95
  8. package/examples/box2d/game.js +29 -10
  9. package/examples/box2d/gameObjects.js +485 -0
  10. package/examples/box2d/index.html +7 -5
  11. package/examples/box2d/scenes.js +28 -247
  12. package/examples/box2d/tiles.png +0 -0
  13. package/examples/breakout/game.js +1 -1
  14. package/examples/breakout/index.html +5 -4
  15. package/examples/breakoutTutorial/index.html +4 -3
  16. package/examples/electron/game.js +1 -1
  17. package/examples/electron/index.html +4 -3
  18. package/examples/empty/game.js +1 -1
  19. package/examples/empty/index.html +2 -1
  20. package/examples/htmlMenu/game.js +67 -0
  21. package/examples/htmlMenu/index.html +56 -0
  22. package/examples/htmlMenu/tiles.png +0 -0
  23. package/examples/index.html +33 -0
  24. package/examples/js13k/game.js +1 -1
  25. package/examples/js13k/index.html +17 -14
  26. package/examples/module/game.js +1 -1
  27. package/examples/module/index.html +3 -2
  28. package/examples/particles/index.html +4 -3
  29. package/examples/platformer/game.js +1 -1
  30. package/examples/platformer/gameCharacter.js +1 -0
  31. package/examples/platformer/gameEffects.js +0 -2
  32. package/examples/platformer/gameLevel.js +2 -2
  33. package/examples/platformer/gameObjects.js +1 -2
  34. package/examples/platformer/index.html +10 -9
  35. package/examples/puzzle/game.js +1 -1
  36. package/examples/puzzle/index.html +4 -3
  37. package/examples/starter/game.js +5 -5
  38. package/examples/starter/index.html +15 -14
  39. package/examples/stress/index.html +3 -2
  40. package/examples/typescript/game.js +1 -1
  41. package/examples/typescript/index.html +3 -2
  42. package/examples/uiSystem/game.js +134 -0
  43. package/examples/uiSystem/index.html +25 -0
  44. package/examples/uiSystem/tiles.png +0 -0
  45. package/jsconfig.json +11 -0
  46. package/package.json +1 -1
  47. package/plugins/box2d.js +116 -42
  48. package/plugins/postProcess.js +6 -7
  49. package/plugins/uiSystem.js +243 -0
  50. package/src/engine.js +48 -20
  51. package/src/engineAudio.js +22 -25
  52. package/src/engineDebug.js +18 -3
  53. package/src/engineDraw.js +101 -27
  54. package/src/engineExport.js +23 -4
  55. package/src/engineInput.js +9 -5
  56. package/src/engineObject.js +15 -12
  57. package/src/engineSettings.js +1 -1
  58. package/src/engineUtilities.js +1 -1
  59. package/src/engineWebGL.js +11 -4
@@ -773,7 +773,7 @@ class Color
773
773
  * @return {String} */
774
774
  toString(useAlpha = true)
775
775
  {
776
- const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
776
+ const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
777
777
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
778
778
  }
779
779
 
@@ -1003,7 +1003,7 @@ let tileSizeDefault = vec2(16);
1003
1003
  * @type {Number}
1004
1004
  * @default
1005
1005
  * @memberof Settings */
1006
- let tileFixBleedScale = .5;
1006
+ let tileFixBleedScale = 0;
1007
1007
 
1008
1008
  ///////////////////////////////////////////////////////////////////////////////
1009
1009
  // Object settings
@@ -1549,9 +1549,9 @@ class EngineObject
1549
1549
 
1550
1550
  // apply physics
1551
1551
  const oldPos = this.pos.copy();
1552
- this.velocity.y += gravity * this.gravityScale;
1553
1552
  this.pos.x += this.velocity.x *= this.damping;
1554
- this.pos.y += this.velocity.y *= this.damping;
1553
+ this.pos.y += this.velocity.y = this.damping * this.velocity.y
1554
+ + gravity * this.gravityScale;
1555
1555
  this.angle += this.angleVelocity *= this.angleDamping;
1556
1556
 
1557
1557
  // physics sanity checks
@@ -1680,19 +1680,22 @@ class EngineObject
1680
1680
  const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
1681
1681
  if (isBlockedY || !isBlockedX)
1682
1682
  {
1683
- // set if landed on ground
1684
- this.groundObject = wasMovingDown;
1685
-
1686
1683
  // bounce velocity
1687
1684
  this.velocity.y *= -this.elasticity;
1688
1685
 
1689
- // adjust next velocity to settle on ground
1690
- const o = (oldPos.y - this.size.y/2|0) - (oldPos.y - this.size.y/2);
1691
- if (o < 0 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1692
- this.velocity.y = this.damping ? (o - gravity * this.gravityScale) / this.damping : 0;
1693
-
1694
- // move to previous position
1695
- this.pos.y = oldPos.y;
1686
+ // set if landed on ground
1687
+ if (this.groundObject = wasMovingDown)
1688
+ {
1689
+ // adjust position to slightly above nearest tile boundary
1690
+ // this prevents gap between object and ground
1691
+ const epsilon = .0001;
1692
+ this.pos.y = (oldPos.y-this.size.y/2|0)+this.size.y/2+epsilon;
1693
+ }
1694
+ else
1695
+ {
1696
+ // move to previous position
1697
+ this.pos.y = oldPos.y;
1698
+ }
1696
1699
  }
1697
1700
  if (isBlockedX)
1698
1701
  {
@@ -1902,12 +1905,13 @@ let drawCount;
1902
1905
  ///////////////////////////////////////////////////////////////////////////////
1903
1906
 
1904
1907
  /**
1905
- * Create a tile info object
1908
+ * Create a tile info object using a grid based system
1906
1909
  * - This can take vecs or floats for easier use and conversion
1907
1910
  * - If an index is passed in, the tile size and index will determine the position
1908
- * @param {(Number|Vector2)} [pos=(0,0)] - Top left corner of tile in pixels or index
1911
+ * @param {(Number|Vector2)} [pos=0] - Index of tile in sheet
1909
1912
  * @param {(Number|Vector2)} [size=tileSizeDefault] - Size of tile in pixels
1910
1913
  * @param {Number} [textureIndex] - Texture index to use
1914
+ * @param {Number} [padding] - How many pixels padding around tiles
1911
1915
  * @return {TileInfo}
1912
1916
  * @example
1913
1917
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -1916,7 +1920,7 @@ let drawCount;
1916
1920
  * tile(vec2(4,8), vec2(30,10)) // a tile at pixel location (4,8) with a size of (30,10)
1917
1921
  * @memberof Draw
1918
1922
  */
1919
- function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1923
+ function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
1920
1924
  {
1921
1925
  if (headlessMode)
1922
1926
  return new TileInfo;
@@ -1928,17 +1932,17 @@ function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1928
1932
  size = vec2(size);
1929
1933
  }
1930
1934
 
1931
- // if pos is a number, use it as a tile index
1935
+ // use pos as a tile index
1936
+ const textureInfo = textureInfos[textureIndex];
1937
+ ASSERT(textureInfo, 'Texture not loaded');
1938
+ const sizePadded = size.add(vec2(padding*2));
1939
+ const cols = textureInfo.size.x / sizePadded.x |0;
1932
1940
  if (typeof pos === 'number')
1933
- {
1934
- const textureInfo = textureInfos[textureIndex];
1935
- ASSERT(textureInfo, 'Texture not loaded');
1936
- const cols = textureInfo.size.x / size.x |0;
1937
- pos = vec2((pos%cols)*size.x, (pos/cols|0)*size.y);
1938
- }
1941
+ pos = vec2(pos%cols, pos/cols|0);
1942
+ pos = vec2(pos.x*sizePadded.x+padding, pos.y*sizePadded.y+padding);
1939
1943
 
1940
1944
  // return a tile info object
1941
- return new TileInfo(pos, size, textureIndex);
1945
+ return new TileInfo(pos, size, textureIndex, padding);
1942
1946
  }
1943
1947
 
1944
1948
  /**
@@ -1950,8 +1954,9 @@ class TileInfo
1950
1954
  * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
1951
1955
  * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
1952
1956
  * @param {Number} [textureIndex] - Texture index to use
1957
+ * @param {Number} [padding] - How many pixels padding around tiles
1953
1958
  */
1954
- constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
1959
+ constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
1955
1960
  {
1956
1961
  /** @property {Vector2} - Top left corner of tile in pixels */
1957
1962
  this.pos = pos.copy();
@@ -1959,6 +1964,8 @@ class TileInfo
1959
1964
  this.size = size.copy();
1960
1965
  /** @property {Number} - Texture index to use */
1961
1966
  this.textureIndex = textureIndex;
1967
+ /** @property {Number} - How many pixels padding around tiles */
1968
+ this.padding = padding;
1962
1969
  }
1963
1970
 
1964
1971
  /** Returns a copy of this tile offset by a vector
@@ -1975,7 +1982,7 @@ class TileInfo
1975
1982
  frame(frame)
1976
1983
  {
1977
1984
  ASSERT(typeof frame == 'number');
1978
- return this.offset(vec2(frame*this.size.x, 0));
1985
+ return this.offset(vec2(frame*(this.size.x+this.padding*2), 0));
1979
1986
  }
1980
1987
 
1981
1988
  /** Returns the texture info for this tile
@@ -2000,8 +2007,6 @@ class TextureInfo
2000
2007
  this.size = vec2(image.width, image.height);
2001
2008
  /** @property {WebGLTexture} - webgl texture */
2002
2009
  this.glTexture = glEnable && glCreateTexture(image);
2003
- /** @property {Vector2} - size to adjust tile to fix bleeding */
2004
- this.fixBleedSize = vec2(tileFixBleedScale).divide(this.size);
2005
2010
  }
2006
2011
  }
2007
2012
 
@@ -2070,11 +2075,12 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2070
2075
  if (textureInfo)
2071
2076
  {
2072
2077
  // calculate uvs and render
2073
- const x = tileInfo.pos.x / textureInfo.size.x;
2074
- const y = tileInfo.pos.y / textureInfo.size.y;
2075
- const w = tileInfo.size.x / textureInfo.size.x;
2076
- const h = tileInfo.size.y / textureInfo.size.y;
2077
- const tileImageFixBleed = textureInfo.fixBleedSize;
2078
+ const sizeInverse = vec2(1).divide(textureInfo.size);
2079
+ const x = tileInfo.pos.x * sizeInverse.x;
2080
+ const y = tileInfo.pos.y * sizeInverse.y;
2081
+ const w = tileInfo.size.x * sizeInverse.x;
2082
+ const h = tileInfo.size.y * sizeInverse.y;
2083
+ const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
2078
2084
  glSetTexture(textureInfo.glTexture);
2079
2085
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2080
2086
  x + tileImageFixBleed.x, y + tileImageFixBleed.y,
@@ -2091,6 +2097,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2091
2097
  {
2092
2098
  // normal canvas 2D rendering method (slower)
2093
2099
  showWatermark && ++drawCount;
2100
+ size = vec2(size.x, -size.y); // fix upside down sprites
2094
2101
  drawCanvas2D(pos, size, angle, mirror, (context)=>
2095
2102
  {
2096
2103
  if (textureInfo)
@@ -2128,6 +2135,74 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2128
2135
  drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
2129
2136
  }
2130
2137
 
2138
+ /** Draw colored polygon using passed in points
2139
+ * @param {Array} points - Array of Vector2 points
2140
+ * @param {Color} [color=(1,1,1,1)]
2141
+ * @param {Number} [lineWidth=0]
2142
+ * @param {Color} [lineColor=(0,0,0,1)]
2143
+ * @param {Boolean} [screenSpace=false]
2144
+ * @param {CanvasRenderingContext2D} [context=mainContext]
2145
+ * @memberof Draw */
2146
+ function drawPoly(points, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
2147
+ {
2148
+ context.fillStyle = color.toString();
2149
+ context.beginPath();
2150
+ for (const point of screenSpace ? points : points.map(worldToScreen))
2151
+ context.lineTo(point.x, point.y);
2152
+ context.closePath();
2153
+ context.fill();
2154
+ if (lineWidth)
2155
+ {
2156
+ context.strokeStyle = lineColor.toString();
2157
+ context.lineWidth = screenSpace ? lineWidth : lineWidth*cameraScale;
2158
+ context.stroke();
2159
+ }
2160
+ }
2161
+
2162
+ /** Draw colored ellipse using passed in point
2163
+ * @param {Vector2} pos
2164
+ * @param {Number} [width=1]
2165
+ * @param {Number} [height=1]
2166
+ * @param {Number} [angle=0]
2167
+ * @param {Color} [color=(1,1,1,1)]
2168
+ * @param {Number} [lineWidth=0]
2169
+ * @param {Color} [lineColor=(0,0,0,1)]
2170
+ * @param {Boolean} [screenSpace=false]
2171
+ * @param {CanvasRenderingContext2D} [context=mainContext]
2172
+ * @memberof Draw */
2173
+ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
2174
+ {
2175
+ if (!screenSpace)
2176
+ {
2177
+ pos = worldToScreen(pos);
2178
+ width *= cameraScale;
2179
+ height *= cameraScale;
2180
+ lineWidth *= cameraScale;
2181
+ }
2182
+ context.fillStyle = color.toString();
2183
+ context.beginPath();
2184
+ context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
2185
+ context.fill();
2186
+ if (lineWidth)
2187
+ {
2188
+ context.strokeStyle = lineColor.toString();
2189
+ context.lineWidth = lineWidth;
2190
+ context.stroke();
2191
+ }
2192
+ }
2193
+
2194
+ /** Draw colored circle using passed in point
2195
+ * @param {Vector2} pos
2196
+ * @param {Number} [radius=1]
2197
+ * @param {Color} [color=(1,1,1,1)]
2198
+ * @param {Number} [lineWidth=0]
2199
+ * @param {Color} [lineColor=(0,0,0,1)]
2200
+ * @param {Boolean} [screenSpace=false]
2201
+ * @param {CanvasRenderingContext2D} [context=mainContext]
2202
+ * @memberof Draw */
2203
+ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
2204
+ { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
2205
+
2131
2206
  /** Draw colored line between two points
2132
2207
  * @param {Vector2} posA
2133
2208
  * @param {Vector2} posB
@@ -2198,10 +2273,11 @@ function setBlendMode(additive, useWebGL=glEnable, context)
2198
2273
  * @param {CanvasTextAlign} [textAlign='center']
2199
2274
  * @param {String} [font=fontDefault]
2200
2275
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2276
+ * @param {Number} [maxWidth]
2201
2277
  * @memberof Draw */
2202
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
2278
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context, maxWidth)
2203
2279
  {
2204
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, context);
2280
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, context, maxWidth);
2205
2281
  }
2206
2282
 
2207
2283
  /** Draw text on overlay canvas in screen space
@@ -2215,8 +2291,9 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
2215
2291
  * @param {CanvasTextAlign} [textAlign]
2216
2292
  * @param {String} [font=fontDefault]
2217
2293
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2294
+ * @param {Number} [maxWidth]
2218
2295
  * @memberof Draw */
2219
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
2296
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext, maxWidth=undefined)
2220
2297
  {
2221
2298
  context.fillStyle = color.toString();
2222
2299
  context.lineWidth = lineWidth;
@@ -2229,8 +2306,8 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
2229
2306
  pos = pos.copy();
2230
2307
  (text+'').split('\n').forEach(line=>
2231
2308
  {
2232
- lineWidth && context.strokeText(line, pos.x, pos.y);
2233
- context.fillText(line, pos.x, pos.y);
2309
+ lineWidth && context.strokeText(line, pos.x, pos.y, maxWidth);
2310
+ context.fillText(line, pos.x, pos.y, maxWidth);
2234
2311
  pos.y += size;
2235
2312
  });
2236
2313
  }
@@ -2338,8 +2415,8 @@ function toggleFullscreen()
2338
2415
  if (document.exitFullscreen)
2339
2416
  document.exitFullscreen();
2340
2417
  }
2341
- else if (document.body.requestFullscreen)
2342
- document.body.requestFullscreen();
2418
+ else if (engineRoot.requestFullscreen)
2419
+ engineRoot.requestFullscreen();
2343
2420
  }
2344
2421
  /**
2345
2422
  * LittleJS Input System
@@ -2510,7 +2587,7 @@ function inputInit()
2510
2587
 
2511
2588
  onkeydown = (e)=>
2512
2589
  {
2513
- if (debug && e.target != document.body) return;
2590
+ if (debug && e.target != engineRoot) return;
2514
2591
  if (!e.repeat)
2515
2592
  {
2516
2593
  isUsingGamepad = false;
@@ -2523,7 +2600,7 @@ function inputInit()
2523
2600
 
2524
2601
  onkeyup = (e)=>
2525
2602
  {
2526
- if (debug && e.target != document.body) return;
2603
+ if (debug && e.target != engineRoot) return;
2527
2604
  inputData[0][e.code] = 4;
2528
2605
  if (inputWASDEmulateDirection)
2529
2606
  inputData[0][remapKey(e.code)] = 4;
@@ -2542,6 +2619,10 @@ function inputInit()
2542
2619
  // mouse event handlers
2543
2620
  onmousedown = (e)=>
2544
2621
  {
2622
+ // fix stalled audio requiring user interaction
2623
+ if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
2624
+ audioContext.resume();
2625
+
2545
2626
  isUsingGamepad = false;
2546
2627
  inputData[0][e.button] = 3;
2547
2628
  mousePosScreen = mouseToScreen(e);
@@ -2553,7 +2634,7 @@ function inputInit()
2553
2634
  oncontextmenu = (e)=> false; // prevent right click menu
2554
2635
 
2555
2636
  // init touch input
2556
- if (isTouchDevice && touchInputEnable && !headlessMode)
2637
+ if (isTouchDevice && touchInputEnable)
2557
2638
  touchInputInit();
2558
2639
  }
2559
2640
 
@@ -2710,8 +2791,8 @@ function touchInputInit()
2710
2791
  function handleTouchDefault(e)
2711
2792
  {
2712
2793
  // fix stalled audio requiring user interaction
2713
- if (soundEnable && audioContext && audioContext.state != 'running')
2714
- zzfx(0);
2794
+ if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
2795
+ audioContext.resume();
2715
2796
 
2716
2797
  // check if touching and pass to mouse events
2717
2798
  const touching = e.touches.length;
@@ -2924,6 +3005,7 @@ class Sound
2924
3005
  // generate zzfx sound now for fast playback
2925
3006
  const defaultRandomness = .05;
2926
3007
  this.randomness = zzfxSound[1] || defaultRandomness;
3008
+ zzfxSound[1] = 0; // generate without randomness
2927
3009
  this.sampleChannels = [zzfxG(...zzfxSound)];
2928
3010
  this.sampleRate = zzfxR;
2929
3011
  }
@@ -3156,10 +3238,6 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
3156
3238
 
3157
3239
  ///////////////////////////////////////////////////////////////////////////////
3158
3240
 
3159
- // internal tracking if audio was suspended when last sound was played
3160
- // allows first suspended sound to play when audio is resumed
3161
- let audioSuspended = false;
3162
-
3163
3241
  /** Play cached audio samples with given settings
3164
3242
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
3165
3243
  * @param {Number} [volume] - How much to scale volume by
@@ -3174,21 +3252,11 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3174
3252
  {
3175
3253
  if (!soundEnable || headlessMode) return;
3176
3254
 
3177
- // prevent sounds from building up if they can't be played
3178
- const audioWasSuspended = audioSuspended;
3179
- if (audioSuspended = audioContext.state != 'running')
3180
- {
3181
- // fix stalled audio
3182
- audioContext.resume();
3183
-
3184
- // prevent suspended sounds from building up
3185
- if (audioWasSuspended)
3186
- return;
3187
- }
3188
-
3189
3255
  // create buffer and source
3190
- const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, sampleRate),
3191
- source = audioContext.createBufferSource();
3256
+ const channelCount = sampleChannels.length;
3257
+ const sampleLength = sampleChannels[0].length;
3258
+ const buffer = audioContext.createBuffer(channelCount, sampleLength, sampleRate);
3259
+ const source = audioContext.createBufferSource();
3192
3260
 
3193
3261
  // copy samples to buffer and setup source
3194
3262
  sampleChannels.forEach((c,i)=> buffer.getChannelData(i).set(c));
@@ -3202,10 +3270,19 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3202
3270
  gainNode.connect(audioGainNode);
3203
3271
 
3204
3272
  // connect source to stereo panner and gain
3205
- source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
3273
+ const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
3274
+ source.connect(pannerNode).connect(gainNode);
3206
3275
 
3207
- // play and return sound
3208
- source.start();
3276
+ // play the sound
3277
+ if (audioContext.state != 'running')
3278
+ {
3279
+ // fix stalled audio and play
3280
+ audioContext.resume().then(()=>source.start());
3281
+ }
3282
+ else
3283
+ source.start();
3284
+
3285
+ // return sound
3209
3286
  return source;
3210
3287
  }
3211
3288
 
@@ -3218,7 +3295,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3218
3295
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
3219
3296
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3220
3297
  * @memberof Audio */
3221
- function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
3298
+ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
3222
3299
 
3223
3300
  /** Sample rate used for all ZzFX sounds
3224
3301
  * @default 44100
@@ -3227,7 +3304,7 @@ const zzfxR = 44100;
3227
3304
 
3228
3305
  /** Generate samples for a ZzFX sound
3229
3306
  * @param {Number} [volume] - Volume scale (percent)
3230
- * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
3307
+ * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
3231
3308
  * @param {Number} [frequency] - Frequency of sound (Hz)
3232
3309
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
3233
3310
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -3253,7 +3330,7 @@ const zzfxR = 44100;
3253
3330
  function zzfxG
3254
3331
  (
3255
3332
  // parameters
3256
- volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
3333
+ volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3257
3334
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3258
3335
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3259
3336
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
@@ -3264,7 +3341,8 @@ function zzfxG
3264
3341
  // init parameters
3265
3342
  let PI2 = PI*2, sampleRate = zzfxR,
3266
3343
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3267
- startFrequency = frequency *= PI2 / sampleRate,
3344
+ startFrequency = frequency *=
3345
+ rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3268
3346
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3269
3347
 
3270
3348
  // biquad LP/HP filter
@@ -4364,10 +4442,10 @@ function glInit()
4364
4442
 
4365
4443
  // create the canvas and textures
4366
4444
  glCanvas = document.createElement('canvas');
4367
- glContext = glCanvas.getContext('webgl2');
4445
+ glContext = glCanvas.getContext('webgl2', {antialias:!canvasPixelated});
4368
4446
 
4369
4447
  // some browsers are much faster without copying the gl buffer so we just overlay it instead
4370
- glOverlay && document.body.appendChild(glCanvas);
4448
+ glOverlay && engineRoot.appendChild(glCanvas);
4371
4449
 
4372
4450
  // setup vertex and fragment shaders
4373
4451
  glShader = glCreateProgram(
@@ -4422,7 +4500,8 @@ function glPreRender()
4422
4500
  // set up the shader
4423
4501
  glContext.useProgram(glShader);
4424
4502
  glContext.activeTexture(gl_TEXTURE0);
4425
- glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
4503
+ if (textureInfos[0])
4504
+ glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
4426
4505
 
4427
4506
  // set vertex attributes
4428
4507
  let offset = glAdditive = glBatchAdditive = 0;
@@ -4520,8 +4599,14 @@ function glCreateTexture(image)
4520
4599
  // build the texture
4521
4600
  const texture = glContext.createTexture();
4522
4601
  glContext.bindTexture(gl_TEXTURE_2D, texture);
4523
- if (image)
4602
+ if (image && image.width)
4524
4603
  glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
4604
+ else
4605
+ {
4606
+ // create a white texture
4607
+ const whitePixel = new Uint8Array([255, 255, 255, 255]);
4608
+ glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, 1, 1, 0, gl_RGBA, gl_UNSIGNED_BYTE, whitePixel);
4609
+ }
4525
4610
 
4526
4611
  // use point filtering for pixelated rendering
4527
4612
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
@@ -4664,7 +4749,7 @@ const engineName = 'LittleJS';
4664
4749
  * @type {String}
4665
4750
  * @default
4666
4751
  * @memberof Engine */
4667
- const engineVersion = '1.9.9';
4752
+ const engineVersion = '1.10.2';
4668
4753
 
4669
4754
  /** Frames per second to update
4670
4755
  * @type {Number}
@@ -4709,6 +4794,12 @@ let timeReal = 0;
4709
4794
  * @memberof Engine */
4710
4795
  let paused = false;
4711
4796
 
4797
+ /** The root element that engine is attached to
4798
+ * @type {HTMLElement}
4799
+ * @default document.body
4800
+ * @memberof Engine */
4801
+ let engineRoot;
4802
+
4712
4803
  /** Set if game is paused
4713
4804
  * @param {Boolean} isPaused
4714
4805
  * @memberof Engine */
@@ -4742,8 +4833,9 @@ function engineAddPlugin(updateFunction, renderFunction)
4742
4833
  * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
4743
4834
  * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
4744
4835
  * @param {Array} [imageSources=['tiles.png']] - Image to load
4836
+ * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
4745
4837
  * @memberof Engine */
4746
- function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=['tiles.png'])
4838
+ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
4747
4839
  {
4748
4840
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
4749
4841
 
@@ -4785,6 +4877,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4785
4877
  for (const o of engineObjects)
4786
4878
  o.parent || o.updateTransforms();
4787
4879
  inputUpdate();
4880
+ pluginUpdateList.forEach(f=>f());
4788
4881
  debugUpdate();
4789
4882
  gameUpdatePost();
4790
4883
  inputUpdatePost();
@@ -4900,16 +4993,22 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4900
4993
  }
4901
4994
 
4902
4995
  // setup html
4903
- const styleBody =
4996
+ const styleRoot =
4904
4997
  'margin:0;overflow:hidden;' + // fill the window
4998
+ 'width:100vw;height:100vh;' + // fill the window
4999
+ 'display:flex;' + // use flexbox
5000
+ 'align-items:center;' + // horizontal center
5001
+ (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5002
+ 'justify-content:center;' + // vertical center
4905
5003
  'background:#000;' + // set background color
4906
5004
  'user-select:none;' + // prevent hold to select
4907
5005
  '-webkit-user-select:none;' + // compatibility for ios
4908
5006
  (!touchInputEnable ? '' : // no touch css setttings
4909
5007
  'touch-action:none;' + // prevent mobile pinch to resize
4910
5008
  '-webkit-touch-callout:none');// compatibility for ios
4911
- document.body.style.cssText = styleBody;
4912
- document.body.appendChild(mainCanvas = document.createElement('canvas'));
5009
+ engineRoot = rootElement;
5010
+ engineRoot.style.cssText = styleRoot;
5011
+ engineRoot.appendChild(mainCanvas = document.createElement('canvas'));
4913
5012
  mainContext = mainCanvas.getContext('2d');
4914
5013
 
4915
5014
  // init stuff and start engine
@@ -4919,13 +5018,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4919
5018
  glInit();
4920
5019
 
4921
5020
  // create overlay canvas for hud to appear above gl canvas
4922
- document.body.appendChild(overlayCanvas = document.createElement('canvas'));
5021
+ engineRoot.appendChild(overlayCanvas = document.createElement('canvas'));
4923
5022
  overlayContext = overlayCanvas.getContext('2d');
4924
5023
 
4925
5024
  // set canvas style
4926
- const styleCanvas = 'position:absolute;' + // position
4927
- 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center
4928
- (glCanvas||mainCanvas).style.cssText = mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
5025
+ const styleCanvas = 'position:absolute'; // allow canvases to overlap
5026
+ mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
5027
+ if (glCanvas)
5028
+ glCanvas.style.cssText = styleCanvas;
4929
5029
  updateCanvas();
4930
5030
 
4931
5031
  // create promises for loading images
@@ -4942,19 +5042,32 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4942
5042
  })
4943
5043
  );
4944
5044
 
4945
- // draw splash screen
4946
- showSplashScreen && promises.push(new Promise(resolve =>
5045
+ if (!imageSources.length)
4947
5046
  {
4948
- let t = 0;
4949
- console.log(`${engineName} Engine v${engineVersion}`);
4950
- updateSplash();
4951
- function updateSplash()
5047
+ // no images to load
5048
+ promises.push(new Promise(resolve =>
4952
5049
  {
4953
- clearInput();
4954
- drawEngineSplashScreen(t+=.01);
4955
- t>1 ? resolve() : setTimeout(updateSplash, 16);
4956
- }
4957
- }));
5050
+ textureInfos[0] = new TextureInfo(new Image);
5051
+ resolve();
5052
+ }));
5053
+ }
5054
+
5055
+ if (showSplashScreen)
5056
+ {
5057
+ // draw splash screen
5058
+ promises.push(new Promise(resolve =>
5059
+ {
5060
+ let t = 0;
5061
+ console.log(`${engineName} Engine v${engineVersion}`);
5062
+ updateSplash();
5063
+ function updateSplash()
5064
+ {
5065
+ clearInput();
5066
+ drawEngineSplashScreen(t+=.01);
5067
+ t>1 ? resolve() : setTimeout(updateSplash, 16);
5068
+ }
5069
+ }));
5070
+ }
4958
5071
 
4959
5072
  // load all of the images
4960
5073
  Promise.all(promises).then(startEngine);