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
package/dist/littlejs.js CHANGED
@@ -128,7 +128,6 @@ function debugPoint(pos, color, time, angle)
128
128
  * @memberof Debug */
129
129
  function debugLine(posA, posB, color, thickness=.1, time)
130
130
  {
131
- ASSERT(typeof color == 'string', 'pass in css color strings');
132
131
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
133
132
  const size = vec2(thickness, halfDelta.length()*2);
134
133
  debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
@@ -194,8 +193,24 @@ function debugSaveDataURL(dataURL, filename)
194
193
  downloadLink.click();
195
194
  }
196
195
 
196
+ /** Show error as full page of red text
197
+ * @memberof Debug */
198
+ function debugShowErrors()
199
+ {
200
+ onunhandledrejection = (event)=>showError(event.reason);
201
+ onerror = (event, source, lineno, colno)=>
202
+ showError(`${event}\n${source}\nLn ${lineno}, Col ${colno}`);
203
+
204
+ const showError = (message)=>
205
+ {
206
+ // replace entire page with error message
207
+ document.body.style.backgroundColor = '#111';
208
+ document.body.innerHTML = `<pre style=color:#f00;font-size:50px>` + message;
209
+ }
210
+ }
211
+
197
212
  ///////////////////////////////////////////////////////////////////////////////
198
- // Engine debug function (called automatically)
213
+ // Engine debug functions (called automatically)
199
214
 
200
215
  function debugInit()
201
216
  {
@@ -326,7 +341,7 @@ function debugRender()
326
341
  const pos = worldToScreen(p.pos);
327
342
  overlayContext.translate(pos.x|0, pos.y|0);
328
343
  overlayContext.rotate(p.angle);
329
- overlayContext.scale(1, -1);
344
+ overlayContext.scale(1, p.text ? 1 : -1);
330
345
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
331
346
 
332
347
  if (p.text != undefined)
@@ -1190,7 +1205,7 @@ class Color
1190
1205
  * @return {String} */
1191
1206
  toString(useAlpha = true)
1192
1207
  {
1193
- const toHex = (c)=> ((c=c*255|0)<16 ? '0' : '') + c.toString(16);
1208
+ const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
1194
1209
  return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1195
1210
  }
1196
1211
 
@@ -1420,7 +1435,7 @@ let tileSizeDefault = vec2(16);
1420
1435
  * @type {Number}
1421
1436
  * @default
1422
1437
  * @memberof Settings */
1423
- let tileFixBleedScale = .5;
1438
+ let tileFixBleedScale = 0;
1424
1439
 
1425
1440
  ///////////////////////////////////////////////////////////////////////////////
1426
1441
  // Object settings
@@ -1966,9 +1981,9 @@ class EngineObject
1966
1981
 
1967
1982
  // apply physics
1968
1983
  const oldPos = this.pos.copy();
1969
- this.velocity.y += gravity * this.gravityScale;
1970
1984
  this.pos.x += this.velocity.x *= this.damping;
1971
- this.pos.y += this.velocity.y *= this.damping;
1985
+ this.pos.y += this.velocity.y = this.damping * this.velocity.y
1986
+ + gravity * this.gravityScale;
1972
1987
  this.angle += this.angleVelocity *= this.angleDamping;
1973
1988
 
1974
1989
  // physics sanity checks
@@ -2097,19 +2112,22 @@ class EngineObject
2097
2112
  const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2098
2113
  if (isBlockedY || !isBlockedX)
2099
2114
  {
2100
- // set if landed on ground
2101
- this.groundObject = wasMovingDown;
2102
-
2103
2115
  // bounce velocity
2104
2116
  this.velocity.y *= -this.elasticity;
2105
2117
 
2106
- // adjust next velocity to settle on ground
2107
- const o = (oldPos.y - this.size.y/2|0) - (oldPos.y - this.size.y/2);
2108
- if (o < 0 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
2109
- this.velocity.y = this.damping ? (o - gravity * this.gravityScale) / this.damping : 0;
2110
-
2111
- // move to previous position
2112
- this.pos.y = oldPos.y;
2118
+ // set if landed on ground
2119
+ if (this.groundObject = wasMovingDown)
2120
+ {
2121
+ // adjust position to slightly above nearest tile boundary
2122
+ // this prevents gap between object and ground
2123
+ const epsilon = .0001;
2124
+ this.pos.y = (oldPos.y-this.size.y/2|0)+this.size.y/2+epsilon;
2125
+ }
2126
+ else
2127
+ {
2128
+ // move to previous position
2129
+ this.pos.y = oldPos.y;
2130
+ }
2113
2131
  }
2114
2132
  if (isBlockedX)
2115
2133
  {
@@ -2319,12 +2337,13 @@ let drawCount;
2319
2337
  ///////////////////////////////////////////////////////////////////////////////
2320
2338
 
2321
2339
  /**
2322
- * Create a tile info object
2340
+ * Create a tile info object using a grid based system
2323
2341
  * - This can take vecs or floats for easier use and conversion
2324
2342
  * - If an index is passed in, the tile size and index will determine the position
2325
- * @param {(Number|Vector2)} [pos=(0,0)] - Top left corner of tile in pixels or index
2343
+ * @param {(Number|Vector2)} [pos=0] - Index of tile in sheet
2326
2344
  * @param {(Number|Vector2)} [size=tileSizeDefault] - Size of tile in pixels
2327
2345
  * @param {Number} [textureIndex] - Texture index to use
2346
+ * @param {Number} [padding] - How many pixels padding around tiles
2328
2347
  * @return {TileInfo}
2329
2348
  * @example
2330
2349
  * tile(2) // a tile at index 2 using the default tile size of 16
@@ -2333,7 +2352,7 @@ let drawCount;
2333
2352
  * tile(vec2(4,8), vec2(30,10)) // a tile at pixel location (4,8) with a size of (30,10)
2334
2353
  * @memberof Draw
2335
2354
  */
2336
- function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
2355
+ function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
2337
2356
  {
2338
2357
  if (headlessMode)
2339
2358
  return new TileInfo;
@@ -2345,17 +2364,17 @@ function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
2345
2364
  size = vec2(size);
2346
2365
  }
2347
2366
 
2348
- // if pos is a number, use it as a tile index
2367
+ // use pos as a tile index
2368
+ const textureInfo = textureInfos[textureIndex];
2369
+ ASSERT(textureInfo, 'Texture not loaded');
2370
+ const sizePadded = size.add(vec2(padding*2));
2371
+ const cols = textureInfo.size.x / sizePadded.x |0;
2349
2372
  if (typeof pos === 'number')
2350
- {
2351
- const textureInfo = textureInfos[textureIndex];
2352
- ASSERT(textureInfo, 'Texture not loaded');
2353
- const cols = textureInfo.size.x / size.x |0;
2354
- pos = vec2((pos%cols)*size.x, (pos/cols|0)*size.y);
2355
- }
2373
+ pos = vec2(pos%cols, pos/cols|0);
2374
+ pos = vec2(pos.x*sizePadded.x+padding, pos.y*sizePadded.y+padding);
2356
2375
 
2357
2376
  // return a tile info object
2358
- return new TileInfo(pos, size, textureIndex);
2377
+ return new TileInfo(pos, size, textureIndex, padding);
2359
2378
  }
2360
2379
 
2361
2380
  /**
@@ -2367,8 +2386,9 @@ class TileInfo
2367
2386
  * @param {Vector2} [pos=(0,0)] - Top left corner of tile in pixels
2368
2387
  * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
2369
2388
  * @param {Number} [textureIndex] - Texture index to use
2389
+ * @param {Number} [padding] - How many pixels padding around tiles
2370
2390
  */
2371
- constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
2391
+ constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
2372
2392
  {
2373
2393
  /** @property {Vector2} - Top left corner of tile in pixels */
2374
2394
  this.pos = pos.copy();
@@ -2376,6 +2396,8 @@ class TileInfo
2376
2396
  this.size = size.copy();
2377
2397
  /** @property {Number} - Texture index to use */
2378
2398
  this.textureIndex = textureIndex;
2399
+ /** @property {Number} - How many pixels padding around tiles */
2400
+ this.padding = padding;
2379
2401
  }
2380
2402
 
2381
2403
  /** Returns a copy of this tile offset by a vector
@@ -2392,7 +2414,7 @@ class TileInfo
2392
2414
  frame(frame)
2393
2415
  {
2394
2416
  ASSERT(typeof frame == 'number');
2395
- return this.offset(vec2(frame*this.size.x, 0));
2417
+ return this.offset(vec2(frame*(this.size.x+this.padding*2), 0));
2396
2418
  }
2397
2419
 
2398
2420
  /** Returns the texture info for this tile
@@ -2417,8 +2439,6 @@ class TextureInfo
2417
2439
  this.size = vec2(image.width, image.height);
2418
2440
  /** @property {WebGLTexture} - webgl texture */
2419
2441
  this.glTexture = glEnable && glCreateTexture(image);
2420
- /** @property {Vector2} - size to adjust tile to fix bleeding */
2421
- this.fixBleedSize = vec2(tileFixBleedScale).divide(this.size);
2422
2442
  }
2423
2443
  }
2424
2444
 
@@ -2487,11 +2507,12 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2487
2507
  if (textureInfo)
2488
2508
  {
2489
2509
  // calculate uvs and render
2490
- const x = tileInfo.pos.x / textureInfo.size.x;
2491
- const y = tileInfo.pos.y / textureInfo.size.y;
2492
- const w = tileInfo.size.x / textureInfo.size.x;
2493
- const h = tileInfo.size.y / textureInfo.size.y;
2494
- const tileImageFixBleed = textureInfo.fixBleedSize;
2510
+ const sizeInverse = vec2(1).divide(textureInfo.size);
2511
+ const x = tileInfo.pos.x * sizeInverse.x;
2512
+ const y = tileInfo.pos.y * sizeInverse.y;
2513
+ const w = tileInfo.size.x * sizeInverse.x;
2514
+ const h = tileInfo.size.y * sizeInverse.y;
2515
+ const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
2495
2516
  glSetTexture(textureInfo.glTexture);
2496
2517
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2497
2518
  x + tileImageFixBleed.x, y + tileImageFixBleed.y,
@@ -2508,6 +2529,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2508
2529
  {
2509
2530
  // normal canvas 2D rendering method (slower)
2510
2531
  showWatermark && ++drawCount;
2532
+ size = vec2(size.x, -size.y); // fix upside down sprites
2511
2533
  drawCanvas2D(pos, size, angle, mirror, (context)=>
2512
2534
  {
2513
2535
  if (textureInfo)
@@ -2545,6 +2567,74 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2545
2567
  drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
2546
2568
  }
2547
2569
 
2570
+ /** Draw colored polygon using passed in points
2571
+ * @param {Array} points - Array of Vector2 points
2572
+ * @param {Color} [color=(1,1,1,1)]
2573
+ * @param {Number} [lineWidth=0]
2574
+ * @param {Color} [lineColor=(0,0,0,1)]
2575
+ * @param {Boolean} [screenSpace=false]
2576
+ * @param {CanvasRenderingContext2D} [context=mainContext]
2577
+ * @memberof Draw */
2578
+ function drawPoly(points, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
2579
+ {
2580
+ context.fillStyle = color.toString();
2581
+ context.beginPath();
2582
+ for (const point of screenSpace ? points : points.map(worldToScreen))
2583
+ context.lineTo(point.x, point.y);
2584
+ context.closePath();
2585
+ context.fill();
2586
+ if (lineWidth)
2587
+ {
2588
+ context.strokeStyle = lineColor.toString();
2589
+ context.lineWidth = screenSpace ? lineWidth : lineWidth*cameraScale;
2590
+ context.stroke();
2591
+ }
2592
+ }
2593
+
2594
+ /** Draw colored ellipse using passed in point
2595
+ * @param {Vector2} pos
2596
+ * @param {Number} [width=1]
2597
+ * @param {Number} [height=1]
2598
+ * @param {Number} [angle=0]
2599
+ * @param {Color} [color=(1,1,1,1)]
2600
+ * @param {Number} [lineWidth=0]
2601
+ * @param {Color} [lineColor=(0,0,0,1)]
2602
+ * @param {Boolean} [screenSpace=false]
2603
+ * @param {CanvasRenderingContext2D} [context=mainContext]
2604
+ * @memberof Draw */
2605
+ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
2606
+ {
2607
+ if (!screenSpace)
2608
+ {
2609
+ pos = worldToScreen(pos);
2610
+ width *= cameraScale;
2611
+ height *= cameraScale;
2612
+ lineWidth *= cameraScale;
2613
+ }
2614
+ context.fillStyle = color.toString();
2615
+ context.beginPath();
2616
+ context.ellipse(pos.x, pos.y, width, height, angle, 0, 9);
2617
+ context.fill();
2618
+ if (lineWidth)
2619
+ {
2620
+ context.strokeStyle = lineColor.toString();
2621
+ context.lineWidth = lineWidth;
2622
+ context.stroke();
2623
+ }
2624
+ }
2625
+
2626
+ /** Draw colored circle using passed in point
2627
+ * @param {Vector2} pos
2628
+ * @param {Number} [radius=1]
2629
+ * @param {Color} [color=(1,1,1,1)]
2630
+ * @param {Number} [lineWidth=0]
2631
+ * @param {Color} [lineColor=(0,0,0,1)]
2632
+ * @param {Boolean} [screenSpace=false]
2633
+ * @param {CanvasRenderingContext2D} [context=mainContext]
2634
+ * @memberof Draw */
2635
+ function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
2636
+ { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
2637
+
2548
2638
  /** Draw colored line between two points
2549
2639
  * @param {Vector2} posA
2550
2640
  * @param {Vector2} posB
@@ -2615,10 +2705,11 @@ function setBlendMode(additive, useWebGL=glEnable, context)
2615
2705
  * @param {CanvasTextAlign} [textAlign='center']
2616
2706
  * @param {String} [font=fontDefault]
2617
2707
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2708
+ * @param {Number} [maxWidth]
2618
2709
  * @memberof Draw */
2619
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
2710
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context, maxWidth)
2620
2711
  {
2621
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, context);
2712
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, context, maxWidth);
2622
2713
  }
2623
2714
 
2624
2715
  /** Draw text on overlay canvas in screen space
@@ -2632,8 +2723,9 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
2632
2723
  * @param {CanvasTextAlign} [textAlign]
2633
2724
  * @param {String} [font=fontDefault]
2634
2725
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2726
+ * @param {Number} [maxWidth]
2635
2727
  * @memberof Draw */
2636
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
2728
+ 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)
2637
2729
  {
2638
2730
  context.fillStyle = color.toString();
2639
2731
  context.lineWidth = lineWidth;
@@ -2646,8 +2738,8 @@ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineCol
2646
2738
  pos = pos.copy();
2647
2739
  (text+'').split('\n').forEach(line=>
2648
2740
  {
2649
- lineWidth && context.strokeText(line, pos.x, pos.y);
2650
- context.fillText(line, pos.x, pos.y);
2741
+ lineWidth && context.strokeText(line, pos.x, pos.y, maxWidth);
2742
+ context.fillText(line, pos.x, pos.y, maxWidth);
2651
2743
  pos.y += size;
2652
2744
  });
2653
2745
  }
@@ -2755,8 +2847,8 @@ function toggleFullscreen()
2755
2847
  if (document.exitFullscreen)
2756
2848
  document.exitFullscreen();
2757
2849
  }
2758
- else if (document.body.requestFullscreen)
2759
- document.body.requestFullscreen();
2850
+ else if (engineRoot.requestFullscreen)
2851
+ engineRoot.requestFullscreen();
2760
2852
  }
2761
2853
  /**
2762
2854
  * LittleJS Input System
@@ -2927,7 +3019,7 @@ function inputInit()
2927
3019
 
2928
3020
  onkeydown = (e)=>
2929
3021
  {
2930
- if (debug && e.target != document.body) return;
3022
+ if (debug && e.target != engineRoot) return;
2931
3023
  if (!e.repeat)
2932
3024
  {
2933
3025
  isUsingGamepad = false;
@@ -2940,7 +3032,7 @@ function inputInit()
2940
3032
 
2941
3033
  onkeyup = (e)=>
2942
3034
  {
2943
- if (debug && e.target != document.body) return;
3035
+ if (debug && e.target != engineRoot) return;
2944
3036
  inputData[0][e.code] = 4;
2945
3037
  if (inputWASDEmulateDirection)
2946
3038
  inputData[0][remapKey(e.code)] = 4;
@@ -2959,6 +3051,10 @@ function inputInit()
2959
3051
  // mouse event handlers
2960
3052
  onmousedown = (e)=>
2961
3053
  {
3054
+ // fix stalled audio requiring user interaction
3055
+ if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
3056
+ audioContext.resume();
3057
+
2962
3058
  isUsingGamepad = false;
2963
3059
  inputData[0][e.button] = 3;
2964
3060
  mousePosScreen = mouseToScreen(e);
@@ -2970,7 +3066,7 @@ function inputInit()
2970
3066
  oncontextmenu = (e)=> false; // prevent right click menu
2971
3067
 
2972
3068
  // init touch input
2973
- if (isTouchDevice && touchInputEnable && !headlessMode)
3069
+ if (isTouchDevice && touchInputEnable)
2974
3070
  touchInputInit();
2975
3071
  }
2976
3072
 
@@ -3127,8 +3223,8 @@ function touchInputInit()
3127
3223
  function handleTouchDefault(e)
3128
3224
  {
3129
3225
  // fix stalled audio requiring user interaction
3130
- if (soundEnable && audioContext && audioContext.state != 'running')
3131
- zzfx(0);
3226
+ if (soundEnable && !headlessMode && audioContext && audioContext.state != 'running')
3227
+ audioContext.resume();
3132
3228
 
3133
3229
  // check if touching and pass to mouse events
3134
3230
  const touching = e.touches.length;
@@ -3341,6 +3437,7 @@ class Sound
3341
3437
  // generate zzfx sound now for fast playback
3342
3438
  const defaultRandomness = .05;
3343
3439
  this.randomness = zzfxSound[1] || defaultRandomness;
3440
+ zzfxSound[1] = 0; // generate without randomness
3344
3441
  this.sampleChannels = [zzfxG(...zzfxSound)];
3345
3442
  this.sampleRate = zzfxR;
3346
3443
  }
@@ -3573,10 +3670,6 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
3573
3670
 
3574
3671
  ///////////////////////////////////////////////////////////////////////////////
3575
3672
 
3576
- // internal tracking if audio was suspended when last sound was played
3577
- // allows first suspended sound to play when audio is resumed
3578
- let audioSuspended = false;
3579
-
3580
3673
  /** Play cached audio samples with given settings
3581
3674
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
3582
3675
  * @param {Number} [volume] - How much to scale volume by
@@ -3591,21 +3684,11 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3591
3684
  {
3592
3685
  if (!soundEnable || headlessMode) return;
3593
3686
 
3594
- // prevent sounds from building up if they can't be played
3595
- const audioWasSuspended = audioSuspended;
3596
- if (audioSuspended = audioContext.state != 'running')
3597
- {
3598
- // fix stalled audio
3599
- audioContext.resume();
3600
-
3601
- // prevent suspended sounds from building up
3602
- if (audioWasSuspended)
3603
- return;
3604
- }
3605
-
3606
3687
  // create buffer and source
3607
- const buffer = audioContext.createBuffer(sampleChannels.length, sampleChannels[0].length, sampleRate),
3608
- source = audioContext.createBufferSource();
3688
+ const channelCount = sampleChannels.length;
3689
+ const sampleLength = sampleChannels[0].length;
3690
+ const buffer = audioContext.createBuffer(channelCount, sampleLength, sampleRate);
3691
+ const source = audioContext.createBufferSource();
3609
3692
 
3610
3693
  // copy samples to buffer and setup source
3611
3694
  sampleChannels.forEach((c,i)=> buffer.getChannelData(i).set(c));
@@ -3619,10 +3702,19 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3619
3702
  gainNode.connect(audioGainNode);
3620
3703
 
3621
3704
  // connect source to stereo panner and gain
3622
- source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)})).connect(gainNode);
3705
+ const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
3706
+ source.connect(pannerNode).connect(gainNode);
3707
+
3708
+ // play the sound
3709
+ if (audioContext.state != 'running')
3710
+ {
3711
+ // fix stalled audio and play
3712
+ audioContext.resume().then(()=>source.start());
3713
+ }
3714
+ else
3715
+ source.start();
3623
3716
 
3624
- // play and return sound
3625
- source.start();
3717
+ // return sound
3626
3718
  return source;
3627
3719
  }
3628
3720
 
@@ -3635,7 +3727,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3635
3727
  * @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
3636
3728
  * @return {AudioBufferSourceNode} - The audio node of the sound played
3637
3729
  * @memberof Audio */
3638
- function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
3730
+ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
3639
3731
 
3640
3732
  /** Sample rate used for all ZzFX sounds
3641
3733
  * @default 44100
@@ -3644,7 +3736,7 @@ const zzfxR = 44100;
3644
3736
 
3645
3737
  /** Generate samples for a ZzFX sound
3646
3738
  * @param {Number} [volume] - Volume scale (percent)
3647
- * @param {Number} [randomness] - Unused in this fuction, handled by Sound class
3739
+ * @param {Number} [randomness] - How much to randomize frequency (percent Hz)
3648
3740
  * @param {Number} [frequency] - Frequency of sound (Hz)
3649
3741
  * @param {Number} [attack] - Attack time, how fast sound starts (seconds)
3650
3742
  * @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
@@ -3670,7 +3762,7 @@ const zzfxR = 44100;
3670
3762
  function zzfxG
3671
3763
  (
3672
3764
  // parameters
3673
- volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
3765
+ volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3674
3766
  release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3675
3767
  pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3676
3768
  bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
@@ -3681,7 +3773,8 @@ function zzfxG
3681
3773
  // init parameters
3682
3774
  let PI2 = PI*2, sampleRate = zzfxR,
3683
3775
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3684
- startFrequency = frequency *= PI2 / sampleRate,
3776
+ startFrequency = frequency *=
3777
+ rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3685
3778
  b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3686
3779
 
3687
3780
  // biquad LP/HP filter
@@ -4781,10 +4874,10 @@ function glInit()
4781
4874
 
4782
4875
  // create the canvas and textures
4783
4876
  glCanvas = document.createElement('canvas');
4784
- glContext = glCanvas.getContext('webgl2');
4877
+ glContext = glCanvas.getContext('webgl2', {antialias:!canvasPixelated});
4785
4878
 
4786
4879
  // some browsers are much faster without copying the gl buffer so we just overlay it instead
4787
- glOverlay && document.body.appendChild(glCanvas);
4880
+ glOverlay && engineRoot.appendChild(glCanvas);
4788
4881
 
4789
4882
  // setup vertex and fragment shaders
4790
4883
  glShader = glCreateProgram(
@@ -4839,7 +4932,8 @@ function glPreRender()
4839
4932
  // set up the shader
4840
4933
  glContext.useProgram(glShader);
4841
4934
  glContext.activeTexture(gl_TEXTURE0);
4842
- glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
4935
+ if (textureInfos[0])
4936
+ glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
4843
4937
 
4844
4938
  // set vertex attributes
4845
4939
  let offset = glAdditive = glBatchAdditive = 0;
@@ -4937,8 +5031,14 @@ function glCreateTexture(image)
4937
5031
  // build the texture
4938
5032
  const texture = glContext.createTexture();
4939
5033
  glContext.bindTexture(gl_TEXTURE_2D, texture);
4940
- if (image)
5034
+ if (image && image.width)
4941
5035
  glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
5036
+ else
5037
+ {
5038
+ // create a white texture
5039
+ const whitePixel = new Uint8Array([255, 255, 255, 255]);
5040
+ glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, 1, 1, 0, gl_RGBA, gl_UNSIGNED_BYTE, whitePixel);
5041
+ }
4942
5042
 
4943
5043
  // use point filtering for pixelated rendering
4944
5044
  const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
@@ -5081,7 +5181,7 @@ const engineName = 'LittleJS';
5081
5181
  * @type {String}
5082
5182
  * @default
5083
5183
  * @memberof Engine */
5084
- const engineVersion = '1.9.9';
5184
+ const engineVersion = '1.10.2';
5085
5185
 
5086
5186
  /** Frames per second to update
5087
5187
  * @type {Number}
@@ -5126,6 +5226,12 @@ let timeReal = 0;
5126
5226
  * @memberof Engine */
5127
5227
  let paused = false;
5128
5228
 
5229
+ /** The root element that engine is attached to
5230
+ * @type {HTMLElement}
5231
+ * @default document.body
5232
+ * @memberof Engine */
5233
+ let engineRoot;
5234
+
5129
5235
  /** Set if game is paused
5130
5236
  * @param {Boolean} isPaused
5131
5237
  * @memberof Engine */
@@ -5159,8 +5265,9 @@ function engineAddPlugin(updateFunction, renderFunction)
5159
5265
  * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
5160
5266
  * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
5161
5267
  * @param {Array} [imageSources=['tiles.png']] - Image to load
5268
+ * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
5162
5269
  * @memberof Engine */
5163
- function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=['tiles.png'])
5270
+ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
5164
5271
  {
5165
5272
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
5166
5273
 
@@ -5202,6 +5309,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5202
5309
  for (const o of engineObjects)
5203
5310
  o.parent || o.updateTransforms();
5204
5311
  inputUpdate();
5312
+ pluginUpdateList.forEach(f=>f());
5205
5313
  debugUpdate();
5206
5314
  gameUpdatePost();
5207
5315
  inputUpdatePost();
@@ -5317,16 +5425,22 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5317
5425
  }
5318
5426
 
5319
5427
  // setup html
5320
- const styleBody =
5428
+ const styleRoot =
5321
5429
  'margin:0;overflow:hidden;' + // fill the window
5430
+ 'width:100vw;height:100vh;' + // fill the window
5431
+ 'display:flex;' + // use flexbox
5432
+ 'align-items:center;' + // horizontal center
5433
+ (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5434
+ 'justify-content:center;' + // vertical center
5322
5435
  'background:#000;' + // set background color
5323
5436
  'user-select:none;' + // prevent hold to select
5324
5437
  '-webkit-user-select:none;' + // compatibility for ios
5325
5438
  (!touchInputEnable ? '' : // no touch css setttings
5326
5439
  'touch-action:none;' + // prevent mobile pinch to resize
5327
5440
  '-webkit-touch-callout:none');// compatibility for ios
5328
- document.body.style.cssText = styleBody;
5329
- document.body.appendChild(mainCanvas = document.createElement('canvas'));
5441
+ engineRoot = rootElement;
5442
+ engineRoot.style.cssText = styleRoot;
5443
+ engineRoot.appendChild(mainCanvas = document.createElement('canvas'));
5330
5444
  mainContext = mainCanvas.getContext('2d');
5331
5445
 
5332
5446
  // init stuff and start engine
@@ -5336,13 +5450,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5336
5450
  glInit();
5337
5451
 
5338
5452
  // create overlay canvas for hud to appear above gl canvas
5339
- document.body.appendChild(overlayCanvas = document.createElement('canvas'));
5453
+ engineRoot.appendChild(overlayCanvas = document.createElement('canvas'));
5340
5454
  overlayContext = overlayCanvas.getContext('2d');
5341
5455
 
5342
5456
  // set canvas style
5343
- const styleCanvas = 'position:absolute;' + // position
5344
- 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center
5345
- (glCanvas||mainCanvas).style.cssText = mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
5457
+ const styleCanvas = 'position:absolute'; // allow canvases to overlap
5458
+ mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
5459
+ if (glCanvas)
5460
+ glCanvas.style.cssText = styleCanvas;
5346
5461
  updateCanvas();
5347
5462
 
5348
5463
  // create promises for loading images
@@ -5359,19 +5474,32 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5359
5474
  })
5360
5475
  );
5361
5476
 
5362
- // draw splash screen
5363
- showSplashScreen && promises.push(new Promise(resolve =>
5477
+ if (!imageSources.length)
5364
5478
  {
5365
- let t = 0;
5366
- console.log(`${engineName} Engine v${engineVersion}`);
5367
- updateSplash();
5368
- function updateSplash()
5479
+ // no images to load
5480
+ promises.push(new Promise(resolve =>
5369
5481
  {
5370
- clearInput();
5371
- drawEngineSplashScreen(t+=.01);
5372
- t>1 ? resolve() : setTimeout(updateSplash, 16);
5373
- }
5374
- }));
5482
+ textureInfos[0] = new TextureInfo(new Image);
5483
+ resolve();
5484
+ }));
5485
+ }
5486
+
5487
+ if (showSplashScreen)
5488
+ {
5489
+ // draw splash screen
5490
+ promises.push(new Promise(resolve =>
5491
+ {
5492
+ let t = 0;
5493
+ console.log(`${engineName} Engine v${engineVersion}`);
5494
+ updateSplash();
5495
+ function updateSplash()
5496
+ {
5497
+ clearInput();
5498
+ drawEngineSplashScreen(t+=.01);
5499
+ t>1 ? resolve() : setTimeout(updateSplash, 16);
5500
+ }
5501
+ }));
5502
+ }
5375
5503
 
5376
5504
  // load all of the images
5377
5505
  Promise.all(promises).then(startEngine);