littlejsengine 1.11.13 → 1.11.17

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 (51) hide show
  1. package/dist/littlejs.d.ts +1306 -66
  2. package/dist/littlejs.esm.js +3097 -269
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +253 -265
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +155 -260
  7. package/examples/box2d/game.js +7 -10
  8. package/examples/box2d/gameObjects.js +33 -33
  9. package/examples/box2d/index.html +6 -6
  10. package/examples/box2d/scenes.js +28 -28
  11. package/examples/breakout/game.js +1 -4
  12. package/examples/breakout/index.html +4 -4
  13. package/examples/breakoutTutorial/index.html +2 -2
  14. package/examples/htmlMenu/game.js +3 -6
  15. package/examples/htmlMenu/index.html +2 -2
  16. package/examples/module/index.html +1 -1
  17. package/examples/particles/index.html +1 -1
  18. package/examples/platformer/game.js +0 -3
  19. package/examples/platformer/index.html +8 -8
  20. package/examples/puzzle/game.js +0 -3
  21. package/examples/puzzle/index.html +2 -2
  22. package/examples/shorts/base.html +1 -1
  23. package/examples/starter/build.js +4 -4
  24. package/examples/starter/index.html +13 -13
  25. package/examples/stress/index.html +3 -2
  26. package/examples/uiSystem/game.js +1 -7
  27. package/examples/uiSystem/index.html +3 -3
  28. package/package.json +3 -3
  29. package/plugins/box2d.js +1552 -640
  30. package/plugins/{Box2D_v2.3.1_min.wasm.js → box2d.wasm.js} +1 -1
  31. package/plugins/newgrounds.js +7 -8
  32. package/plugins/pluginExport.js +51 -0
  33. package/plugins/postProcess.js +94 -93
  34. package/plugins/uiSystem.js +145 -161
  35. package/plugins/zzfxm.js +163 -0
  36. package/src/engine.js +15 -20
  37. package/src/engineAudio.js +74 -204
  38. package/src/engineBuild.js +21 -3
  39. package/src/engineDebug.js +104 -6
  40. package/src/engineDraw.js +27 -14
  41. package/src/engineExport.js +12 -4
  42. package/src/engineParticles.js +6 -1
  43. package/src/engineRelease.js +6 -1
  44. package/src/engineSettings.js +2 -2
  45. package/src/engineUtilities.js +5 -11
  46. package/src/engineWebGL.js +19 -7
  47. package/examples/starter/build/index.html +0 -2
  48. package/examples/starter/build/index.js +0 -1
  49. package/examples/starter/build/tiles.png +0 -0
  50. package/examples/starter/game.zip +0 -0
  51. /package/plugins/{Box2D_v2.3.1_min.wasm.wasm → box2d.wasm.wasm} +0 -0
package/dist/littlejs.js CHANGED
@@ -77,8 +77,10 @@ function ASSERT(assert, output)
77
77
  * @memberof Debug */
78
78
  function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
79
79
  {
80
+ if (typeof size == 'number')
81
+ size = vec2(size); // allow passing in floats
80
82
  ASSERT(typeof color == 'string', 'pass in css color strings');
81
- debugPrimitives.push({pos, size:vec2(size), color, time:new Timer(time), angle, fill});
83
+ debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
82
84
  }
83
85
 
84
86
  /** Draw a debug poly in world space
@@ -202,17 +204,17 @@ function debugSaveDataURL(dataURL, filename)
202
204
  * @memberof Debug */
203
205
  function debugShowErrors()
204
206
  {
205
- onunhandledrejection = (event)=>showError(event.reason);
206
- onerror = (event, source, lineno, colno)=>
207
- showError(`${event}\n${source}\nLn ${lineno}, Col ${colno}`);
208
-
209
207
  const showError = (message)=>
210
208
  {
211
209
  // replace entire page with error message
212
210
  document.body.style.display = '';
213
211
  document.body.style.backgroundColor = '#111';
214
- document.body.innerHTML = `<pre style=color:#f00;font-size:50px>` + message;
212
+ document.body.innerHTML = `<pre style=color:#f00;font-size:50px;white-space:pre-wrap>` + message;
215
213
  }
214
+ onunhandledrejection = (event)=>
215
+ showError(event.reason.stack || event.reason);
216
+ onerror = (message, source, lineno, colno)=>
217
+ showError(`${message}\n${source}\nLn ${lineno}, Col ${colno}`);
216
218
  }
217
219
 
218
220
  ///////////////////////////////////////////////////////////////////////////////
@@ -245,11 +247,16 @@ function debugUpdate()
245
247
  debugRaycast = !debugRaycast;
246
248
  if (keyWasPressed('Digit5'))
247
249
  debugScreenshot();
250
+ if (keyWasPressed('Digit6'))
251
+ debugVideoCaptureIsActive() ? debugVideoCaptureStop() : debugVideoCaptureStart();
248
252
  }
249
253
  }
250
254
 
251
255
  function debugRender()
252
256
  {
257
+ if (debugVideoCaptureIsActive())
258
+ return; // don't show debug info when capturing video
259
+
253
260
  glCopyToContext(mainContext);
254
261
 
255
262
  if (debugTakeScreenshot)
@@ -444,6 +451,7 @@ function debugRender()
444
451
  overlayContext.fillText('4: Debug Raycasts', x, y += h);
445
452
  overlayContext.fillStyle = '#fff';
446
453
  overlayContext.fillText('5: Save Screenshot', x, y += h);
454
+ overlayContext.fillText('6: Capture Video', x, y += h);
447
455
 
448
456
  let keysPressed = '';
449
457
  for(const i in inputData[0])
@@ -472,6 +480,96 @@ function debugRender()
472
480
 
473
481
  overlayContext.restore();
474
482
  }
483
+ }
484
+
485
+ ///////////////////////////////////////////////////////////////////////////////
486
+ // video capture - records video and audio at 60 fps using MediaRecorder API
487
+
488
+ // internal variables used to capture video
489
+ let debugVideoCapture, debugVideoCaptureTrack, debugVideoCaptureIcon, debugVideoCaptureTimer;
490
+
491
+ /** Check if video capture is active
492
+ * @memberof Debug */
493
+ function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
494
+
495
+ /** Start capturing video
496
+ * @memberof Debug */
497
+ function debugVideoCaptureStart()
498
+ {
499
+ if (debugVideoCaptureIsActive())
500
+ return; // already recording
501
+
502
+ // captureStream passing in 0 to only capture when requestFrame() is called
503
+ const stream = mainCanvas.captureStream(0);
504
+ const chunks = [];
505
+ debugVideoCaptureTrack = stream.getVideoTracks()[0];
506
+ if (debugVideoCaptureTrack.applyConstraints)
507
+ debugVideoCaptureTrack.applyConstraints({frameRate:frameRate}); // force 60 fps
508
+ debugVideoCapture = new MediaRecorder(stream, {mimeType:'video/webm;codecs=vp8'});
509
+ debugVideoCapture.ondataavailable = (e)=> chunks.push(e.data);
510
+ debugVideoCapture.onstop = ()=>
511
+ {
512
+ const blob = new Blob(chunks, {type: 'video/webm'});
513
+ const url = URL.createObjectURL(blob);
514
+ downloadLink.download = 'capture.webm';
515
+ downloadLink.href = url;
516
+ downloadLink.click();
517
+ URL.revokeObjectURL(url);
518
+ };
519
+
520
+ if (audioMasterGain)
521
+ {
522
+ // connect to audio master gain node
523
+ const audioStreamDestination = audioContext.createMediaStreamDestination();
524
+ audioMasterGain.connect(audioStreamDestination);
525
+ for (const track of audioStreamDestination.stream.getAudioTracks())
526
+ stream.addTrack(track); // add audio tracks to capture stream
527
+ }
528
+
529
+ // start recording
530
+ console.log('Video capture started.');
531
+ debugVideoCapture.start();
532
+ debugVideoCaptureTimer = new Timer(0);
533
+
534
+ if (!debugVideoCaptureIcon)
535
+ {
536
+ // create recording icon to show it is capturing video
537
+ debugVideoCaptureIcon = document.createElement('div');
538
+ debugVideoCaptureIcon.style.position = 'absolute';
539
+ debugVideoCaptureIcon.style.padding = '9px';
540
+ debugVideoCaptureIcon.style.color = '#f00';
541
+ debugVideoCaptureIcon.style.font = '50px monospace';
542
+ document.body.appendChild(debugVideoCaptureIcon);
543
+ }
544
+ // show recording icon
545
+ debugVideoCaptureIcon.textContent = '';
546
+ debugVideoCaptureIcon.style.display = '';
547
+ }
548
+
549
+ /** Stop capturing video and save to disk
550
+ * @memberof Debug */
551
+ function debugVideoCaptureStop()
552
+ {
553
+ if (!debugVideoCaptureIsActive())
554
+ return; // not recording
555
+
556
+ // stop recording
557
+ console.log(`Video capture ended. ${debugVideoCaptureTimer.get().toFixed(2)} seconds recorded.`);
558
+ debugVideoCapture.stop();
559
+ debugVideoCapture = 0;
560
+ debugVideoCaptureIcon.style.display = 'none';
561
+ }
562
+
563
+ // update video capture, called automatically by engine
564
+ function debugVideoCaptureUpdate()
565
+ {
566
+ if (!debugVideoCaptureIsActive())
567
+ return; // not recording
568
+
569
+ // save the video frame
570
+ combineCanvases();
571
+ debugVideoCaptureTrack.requestFrame();
572
+ debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
475
573
  }
476
574
  /**
477
575
  * LittleJS Utility Classes and Functions
@@ -566,7 +664,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
566
664
  * @returns {number}
567
665
  * @memberof Utilities */
568
666
  function lerpWrap(percent, valueA, valueB, wrapSize=1)
569
- { return valueB + clamp(percent) * distanceWrap(valueA, valueB, wrapSize); }
667
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
570
668
 
571
669
  /** Returns signed wrapped distance between the two angles passed in
572
670
  * @param {number} angleA
@@ -764,23 +862,17 @@ class RandomGenerator
764
862
  ///////////////////////////////////////////////////////////////////////////////
765
863
 
766
864
  /**
767
- * Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
768
- * @param {Vector2|number} [x]
769
- * @param {number} [y]
865
+ * Create a 2d vector, can take 1 or 2 scalar values
866
+ * @param {number} [x]
867
+ * @param {number} [y] - if y is undefined, x is used for both
770
868
  * @return {Vector2}
771
869
  * @example
772
870
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
773
- * let b = vec2(a); // copy a into b
774
871
  * a = vec2(5); // set a to (5, 5)
775
872
  * b = vec2(); // set b to (0, 0)
776
873
  * @memberof Utilities
777
874
  */
778
- function vec2(x=0, y)
779
- {
780
- return typeof x == 'number' ?
781
- new Vector2(x, y == undefined? x : y) :
782
- new Vector2(x.x, x.y);
783
- }
875
+ function vec2(x=0, y) { return new Vector2(x, y == undefined? x : y); }
784
876
 
785
877
  /**
786
878
  * Check if object is a valid Vector2
@@ -1844,8 +1936,8 @@ function setSoundEnable(enable) { soundEnable = enable; }
1844
1936
  function setSoundVolume(volume)
1845
1937
  {
1846
1938
  soundVolume = volume;
1847
- if (soundEnable && !headlessMode && audioGainNode)
1848
- audioGainNode.gain.value = volume; // update gain immediately
1939
+ if (soundEnable && !headlessMode && audioMasterGain)
1940
+ audioMasterGain.gain.value = volume; // update gain immediately
1849
1941
  }
1850
1942
 
1851
1943
  /** Set default range where sound no longer plays
@@ -2511,6 +2603,8 @@ class TextureInfo
2511
2603
  this.image = image;
2512
2604
  /** @property {Vector2} - size of the image */
2513
2605
  this.size = vec2(image.width, image.height);
2606
+ /** @property {Vector2} - inverse of the size, cached for rendering */
2607
+ this.sizeInverse = vec2(1/image.width, 1/image.height);
2514
2608
  /** @property {WebGLTexture} - webgl texture */
2515
2609
  this.glTexture = glEnable && glCreateTexture(image);
2516
2610
  }
@@ -2556,19 +2650,19 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
2556
2650
  * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2557
2651
  * @param {number} [angle] - Angle to rotate by
2558
2652
  * @param {boolean} [mirror] - If true image is flipped along the Y axis
2559
- * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
2653
+ * @param {Color} [additiveColor] - Additive color to be applied if any
2560
2654
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2561
2655
  * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
2562
2656
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2563
2657
  * @memberof Draw */
2564
2658
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2565
- angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
2659
+ angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2566
2660
  {
2567
2661
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2568
2662
  ASSERT(typeof tileInfo !== 'number' || !tileInfo,
2569
2663
  'this is an old style calls, to fix replace it with tile(tileIndex, tileSize)');
2570
2664
  ASSERT(isVector2(pos) && isVector2(size));
2571
- ASSERT(isColor(color) && isColor(additiveColor));
2665
+ ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)));
2572
2666
 
2573
2667
  const textureInfo = tileInfo && tileInfo.getTextureInfo();
2574
2668
  if (useWebGL)
@@ -2579,21 +2673,30 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2579
2673
  pos = screenToWorld(pos);
2580
2674
  size = size.scale(1/cameraScale);
2581
2675
  }
2582
-
2583
2676
  if (textureInfo)
2584
2677
  {
2585
2678
  // calculate uvs and render
2586
- const sizeInverse = vec2(1).divide(textureInfo.size);
2679
+ const sizeInverse = textureInfo.sizeInverse;
2587
2680
  const x = tileInfo.pos.x * sizeInverse.x;
2588
2681
  const y = tileInfo.pos.y * sizeInverse.y;
2589
2682
  const w = tileInfo.size.x * sizeInverse.x;
2590
2683
  const h = tileInfo.size.y * sizeInverse.y;
2591
- const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
2592
2684
  glSetTexture(textureInfo.glTexture);
2593
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2594
- x + tileImageFixBleed.x, y + tileImageFixBleed.y,
2595
- x - tileImageFixBleed.x + w, y - tileImageFixBleed.y + h,
2596
- color.rgbaInt(), additiveColor.rgbaInt());
2685
+ if (tileFixBleedScale)
2686
+ {
2687
+ const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
2688
+ const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
2689
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2690
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
2691
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2692
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2693
+ }
2694
+ else
2695
+ {
2696
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2697
+ x, y, x + w, y + h,
2698
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2699
+ }
2597
2700
  }
2598
2701
  else
2599
2702
  {
@@ -2805,16 +2908,15 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
2805
2908
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
2806
2909
  {
2807
2910
  context.fillStyle = color.toString();
2808
- context.lineWidth = lineWidth;
2809
2911
  context.strokeStyle = lineColor.toString();
2912
+ context.lineWidth = lineWidth;
2810
2913
  context.textAlign = textAlign;
2811
2914
  context.font = size + 'px '+ font;
2812
2915
  context.textBaseline = 'middle';
2813
2916
  context.lineJoin = 'round';
2814
2917
 
2815
- pos = pos.copy();
2816
-
2817
2918
  const lines = (text+'').split('\n');
2919
+ pos = pos.copy();
2818
2920
  pos.y -= (lines.length-1) * size/2; // center text vertically
2819
2921
  lines.forEach(line=>
2820
2922
  {
@@ -2884,7 +2986,10 @@ class FontImage
2884
2986
  {
2885
2987
  // load default font image
2886
2988
  if (!engineFontImage)
2887
- (engineFontImage = new Image).src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2989
+ {
2990
+ engineFontImage = new Image;
2991
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2992
+ }
2888
2993
 
2889
2994
  this.image = image || engineFontImage;
2890
2995
  this.tileSize = tileSize;
@@ -3515,16 +3620,15 @@ let audioContext = new AudioContext;
3515
3620
  /** Master gain node for all audio to pass through
3516
3621
  * @type {GainNode}
3517
3622
  * @memberof Audio */
3518
- let audioGainNode;
3623
+ let audioMasterGain;
3519
3624
 
3520
3625
  function audioInit()
3521
3626
  {
3522
3627
  if (!soundEnable || headlessMode) return;
3523
3628
 
3524
- // (createGain is more widely supported then GainNode constructor)
3525
- audioGainNode = audioContext.createGain();
3526
- audioGainNode.connect(audioContext.destination);
3527
- audioGainNode.gain.value = soundVolume; // set starting value
3629
+ audioMasterGain = audioContext.createGain();
3630
+ audioMasterGain.connect(audioContext.destination);
3631
+ audioMasterGain.gain.value = soundVolume; // set starting value
3528
3632
  }
3529
3633
 
3530
3634
  ///////////////////////////////////////////////////////////////////////////////
@@ -3653,6 +3757,8 @@ class Sound
3653
3757
  isLoading() { return !this.sampleChannels; }
3654
3758
  }
3655
3759
 
3760
+ ///////////////////////////////////////////////////////////////////////////////
3761
+
3656
3762
  /**
3657
3763
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
3658
3764
  * - this can be used to play wave, mp3, and ogg files
@@ -3704,59 +3810,7 @@ function playAudioFile(filename, volume=1, loop=false)
3704
3810
  return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
3705
3811
  }
3706
3812
 
3707
- /**
3708
- * Music Object - Stores a zzfx music track for later use
3709
- *
3710
- * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
3711
- * @example
3712
- * // create some music
3713
- * const music_example = new Music(
3714
- * [
3715
- * [ // instruments
3716
- * [,0,400] // simple note
3717
- * ],
3718
- * [ // patterns
3719
- * [ // pattern 1
3720
- * [ // channel 0
3721
- * 0, -1, // instrument 0, left speaker
3722
- * 1, 0, 9, 1 // channel notes
3723
- * ],
3724
- * [ // channel 1
3725
- * 0, 1, // instrument 0, right speaker
3726
- * 0, 12, 17, -1 // channel notes
3727
- * ]
3728
- * ],
3729
- * ],
3730
- * [0, 0, 0, 0], // sequence, play pattern 0 four times
3731
- * 90 // BPM
3732
- * ]);
3733
- *
3734
- * // play the music
3735
- * music_example.play();
3736
- */
3737
- class Music extends Sound
3738
- {
3739
- /** Create a music object and cache the zzfx music samples for later use
3740
- * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
3741
- */
3742
- constructor(zzfxMusic)
3743
- {
3744
- super(undefined);
3745
-
3746
- if (!soundEnable || headlessMode) return;
3747
- this.randomness = 0;
3748
- this.sampleChannels = zzfxM(...zzfxMusic);
3749
- this.sampleRate = zzfxR;
3750
- }
3751
-
3752
- /** Play the music
3753
- * @param {number} [volume=1] - How much to scale volume by
3754
- * @param {boolean} [loop] - True if the music should loop
3755
- * @return {AudioBufferSourceNode} - The audio source node
3756
- */
3757
- playMusic(volume, loop=false)
3758
- { return super.play(undefined, volume, 1, 1, loop); }
3759
- }
3813
+ ///////////////////////////////////////////////////////////////////////////////
3760
3814
 
3761
3815
  /** Speak text with passed in settings
3762
3816
  * @param {string} text - The text to speak
@@ -3828,7 +3882,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3828
3882
  // create and connect gain node
3829
3883
  gainNode = gainNode || audioContext.createGain();
3830
3884
  gainNode.gain.value = volume;
3831
- gainNode.connect(audioGainNode);
3885
+ gainNode.connect(audioMasterGain);
3832
3886
 
3833
3887
  // connect source to stereo panner and gain
3834
3888
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
@@ -3848,7 +3902,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3848
3902
  }
3849
3903
 
3850
3904
  ///////////////////////////////////////////////////////////////////////////////
3851
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
3905
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
3852
3906
 
3853
3907
  /** Generate and play a ZzFX sound
3854
3908
  *
@@ -3890,21 +3944,45 @@ const zzfxR = 44100;
3890
3944
  */
3891
3945
  function zzfxG
3892
3946
  (
3893
- // parameters
3894
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3895
- release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3896
- pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3897
- bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3947
+ volume = 1,
3948
+ randomness = .05,
3949
+ frequency = 220,
3950
+ attack = 0,
3951
+ sustain = 0,
3952
+ release = .1,
3953
+ shape = 0,
3954
+ shapeCurve = 1,
3955
+ slide = 0,
3956
+ deltaSlide = 0,
3957
+ pitchJump = 0,
3958
+ pitchJumpTime = 0,
3959
+ repeatTime = 0,
3960
+ noise = 0,
3961
+ modulation = 0,
3962
+ bitCrush = 0,
3963
+ delay = 0,
3964
+ sustainVolume = 1,
3965
+ decay = 0,
3966
+ tremolo = 0,
3967
+ filter = 0
3898
3968
  )
3899
3969
  {
3900
- // LJS Note: ZZFX modded so randomness is handled by Sound class
3901
-
3902
3970
  // init parameters
3903
- let PI2 = PI*2, sampleRate = zzfxR,
3971
+ let sampleRate = zzfxR,
3972
+ PI2 = PI*2,
3904
3973
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3905
3974
  startFrequency = frequency *=
3906
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3907
- b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3975
+ (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
3976
+ modOffset = 0, // modulation offset
3977
+ repeat = 0, // repeat offset
3978
+ crush = 0, // bit crush offset
3979
+ jump = 1, // pitch jump timer
3980
+ length, // sample length
3981
+ b = [], // sample buffer
3982
+ t = 0, // sample time
3983
+ i = 0, // sample index
3984
+ s = 0, // sample value
3985
+ f, // wave frequency
3908
3986
 
3909
3987
  // biquad LP/HP filter
3910
3988
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
@@ -3914,35 +3992,37 @@ function zzfxG
3914
3992
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
3915
3993
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
3916
3994
 
3917
- // scale by sample rate
3918
- attack = attack * sampleRate + 9; // minimum attack to prevent pop
3919
- decay *= sampleRate;
3920
- sustain *= sampleRate;
3921
- release *= sampleRate;
3922
- delay *= sampleRate;
3923
- deltaSlide *= 500 * PI2 / sampleRate**3;
3924
- modulation *= PI2 / sampleRate;
3925
- pitchJump *= PI2 / sampleRate;
3926
- pitchJumpTime *= sampleRate;
3927
- repeatTime = repeatTime * sampleRate | 0;
3995
+ // scale by sample rate
3996
+ const minAttack = 9; // prevent pop if attack is 0
3997
+ attack = attack * sampleRate || minAttack;
3998
+ decay *= sampleRate;
3999
+ sustain *= sampleRate;
4000
+ release *= sampleRate;
4001
+ delay *= sampleRate;
4002
+ deltaSlide *= 500 * PI2 / sampleRate**3;
4003
+ modulation *= PI2 / sampleRate;
4004
+ pitchJump *= PI2 / sampleRate;
4005
+ pitchJumpTime *= sampleRate;
4006
+ repeatTime = repeatTime * sampleRate | 0;
3928
4007
 
3929
4008
  // generate waveform
3930
4009
  for(length = attack + decay + sustain + release + delay | 0;
3931
- i < length; b[i++] = s * volume) // sample
4010
+ i < length; b[i++] = s * volume) // sample
3932
4011
  {
3933
- if (!(++c%(bitCrush*100|0))) // bit crush
4012
+ if (!(++crush%(bitCrush*100|0))) // bit crush
3934
4013
  {
3935
- s = shape? shape>1? shape>2? shape>3? // wave shape
3936
- Math.sin(t**3) : // 4 noise
3937
- clamp(Math.tan(t),1,-1): // 3 tan
3938
- 1-(2*t/PI2%2+2)%2: // 2 saw
3939
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
3940
- Math.sin(t); // 0 sin
4014
+ s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
4015
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
4016
+ Math.sin(t**3) : // 4 noise
4017
+ Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
4018
+ 1-(2*t/PI2%2+2)%2: // 2 saw
4019
+ 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
4020
+ Math.sin(t); // 0 sin
3941
4021
 
3942
4022
  s = (repeatTime ?
3943
4023
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
3944
4024
  : 1) *
3945
- sign(s)*(abs(s)**shapeCurve) * // curve
4025
+ (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
3946
4026
  (i < attack ? i/attack : // attack
3947
4027
  i < attack + decay ? // decay
3948
4028
  1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
@@ -3957,136 +4037,32 @@ function zzfxG
3957
4037
  (i<length-delay? 1 : (length-i)/delay) * // release delay
3958
4038
  b[i-delay|0]/2/volume) : s; // sample delay
3959
4039
 
3960
- if (filter) // apply filter
4040
+ if (filter) // apply filter
3961
4041
  s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
3962
4042
  }
3963
4043
 
3964
4044
  f = (frequency += slide += deltaSlide) *// frequency
3965
- Math.cos(modulation*tm++); // modulation
4045
+ Math.cos(modulation*modOffset++); // modulation
3966
4046
  t += f + f*noise*Math.sin(i**5); // noise
3967
4047
 
3968
- if (j && ++j > pitchJumpTime) // pitch jump
4048
+ if (jump && ++jump > pitchJumpTime) // pitch jump
3969
4049
  {
3970
4050
  frequency += pitchJump; // apply pitch jump
3971
4051
  startFrequency += pitchJump; // also apply to start
3972
- j = 0; // stop pitch jump time
4052
+ jump = 0; // stop pitch jump time
3973
4053
  }
3974
4054
 
3975
- if (repeatTime && !(++r % repeatTime)) // repeat
4055
+ if (repeatTime && !(++repeat % repeatTime)) // repeat
3976
4056
  {
3977
- frequency = startFrequency; // reset frequency
3978
- slide = startSlide; // reset slide
3979
- j = j || 1; // reset pitch jump time
4057
+ frequency = startFrequency; // reset frequency
4058
+ slide = startSlide; // reset slide
4059
+ jump ||= 1; // reset pitch jump time
3980
4060
  }
3981
4061
  }
3982
4062
 
3983
- return b;
4063
+ return b; // return sample buffer
3984
4064
  }
3985
-
3986
- ///////////////////////////////////////////////////////////////////////////////
3987
- // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
3988
-
3989
- /** Generate samples for a ZzFM song with given parameters
3990
- * @param {Array} instruments - Array of ZzFX sound parameters
3991
- * @param {Array} patterns - Array of pattern data
3992
- * @param {Array} sequence - Array of pattern indexes
3993
- * @param {number} [BPM] - Playback speed of the song in BPM
3994
- * @return {Array} - Left and right channel sample data
3995
- * @memberof Audio */
3996
- function zzfxM(instruments, patterns, sequence, BPM = 125)
3997
- {
3998
- let i, j, k;
3999
- let instrumentParameters;
4000
- let note;
4001
- let sample;
4002
- let patternChannel;
4003
- let notFirstBeat;
4004
- let stop;
4005
- let instrument;
4006
- let attenuation;
4007
- let outSampleOffset;
4008
- let isSequenceEnd;
4009
- let sampleOffset = 0;
4010
- let nextSampleOffset;
4011
- let sampleBuffer = [];
4012
- let leftChannelBuffer = [];
4013
- let rightChannelBuffer = [];
4014
- let channelIndex = 0;
4015
- let panning = 0;
4016
- let hasMore = 1;
4017
- let sampleCache = {};
4018
- let beatLength = zzfxR / BPM * 60 >> 2;
4019
-
4020
- // for each channel in order until there are no more
4021
- for (; hasMore; channelIndex++) {
4022
-
4023
- // reset current values
4024
- sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
4025
-
4026
- // for each pattern in sequence
4027
- sequence.forEach((patternIndex, sequenceIndex) => {
4028
- // get pattern for current channel, use empty 1 note pattern if none found
4029
- patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
4030
-
4031
- // check if there are more channels
4032
- hasMore |= patterns[patternIndex][channelIndex]&&1;
4033
-
4034
- // get next offset, use the length of first channel
4035
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
4036
- // for each beat in pattern, plus one extra if end of sequence
4037
- isSequenceEnd = sequenceIndex == sequence.length - 1;
4038
- for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
4039
-
4040
- // <channel-note>
4041
- note = patternChannel[i];
4042
-
4043
- // stop if end, different instrument or new note
4044
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
4045
- instrument != (patternChannel[0] || 0) || note | 0;
4046
-
4047
- // fill buffer with samples for previous beat, most cpu intensive part
4048
- for (j = 0; j < beatLength && notFirstBeat;
4049
-
4050
- // fade off attenuation at end of beat if stopping note, prevents clicking
4051
- j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
4052
- ) {
4053
- // copy sample to stereo buffers with panning
4054
- sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
4055
- leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
4056
- rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
4057
- }
4058
-
4059
- // set up for next note
4060
- if (note) {
4061
- // set attenuation
4062
- attenuation = note % 1;
4063
- panning = patternChannel[1] || 0;
4064
- if (note |= 0) {
4065
- // get cached sample
4066
- sampleBuffer = sampleCache[
4067
- [
4068
- instrument = patternChannel[sampleOffset = 0] || 0,
4069
- note
4070
- ]
4071
- ] = sampleCache[[instrument, note]] || (
4072
- // add sample to cache
4073
- instrumentParameters = [...instruments[instrument]],
4074
- instrumentParameters[2] *= 2 ** ((note - 12) / 12),
4075
-
4076
- // allow negative values to stop notes
4077
- note > 0 ? zzfxG(...instrumentParameters) : []
4078
- );
4079
- }
4080
- }
4081
- }
4082
-
4083
- // update the sample offset
4084
- outSampleOffset = nextSampleOffset;
4085
- });
4086
- }
4087
-
4088
- return [leftChannelBuffer, rightChannelBuffer];
4089
- }
4065
+
4090
4066
  /**
4091
4067
  * LittleJS Tile Layer System
4092
4068
  * - Caches arrays of tiles to off screen canvas for fast rendering
@@ -4643,7 +4619,12 @@ class ParticleEmitter extends EngineObject
4643
4619
  else
4644
4620
  this.destroy();
4645
4621
 
4646
- debugParticles && debugRect(this.pos, vec2(this.emitSize), '#0f0', 0, this.angle);
4622
+ if (debugParticles)
4623
+ {
4624
+ // show emitter bounds
4625
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
4626
+ debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
4627
+ }
4647
4628
  }
4648
4629
 
4649
4630
  /** Spawn one particle
@@ -5105,12 +5086,12 @@ function glPreRender()
5105
5086
 
5106
5087
  // set vertex attributes
5107
5088
  let offset = glAdditive = glBatchAdditive = 0;
5108
- let initVertexAttribArray = (name, type, typeSize, size)=>
5089
+ const initVertexAttribArray = (name, type, typeSize, size)=>
5109
5090
  {
5110
5091
  const location = glContext.getAttribLocation(glShader, name);
5111
5092
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
5112
5093
  const divisor = typeSize && 1; // only if not geometry
5113
- const normalize = typeSize==1; // only if color
5094
+ const normalize = typeSize == 1; // only if color
5114
5095
  glContext.enableVertexAttribArray(location);
5115
5096
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
5116
5097
  glContext.vertexAttribDivisor(location, divisor);
@@ -5209,14 +5190,14 @@ function glCreateTexture(image)
5209
5190
  const texture = glContext.createTexture();
5210
5191
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
5211
5192
  if (image && image.width)
5212
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
5193
+ glSetTextureData(texture, image);
5213
5194
  else
5214
5195
  {
5215
5196
  // create a white texture
5216
5197
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
5217
5198
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
5218
5199
  }
5219
-
5200
+
5220
5201
  // use point filtering for pixelated rendering
5221
5202
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
5222
5203
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -5224,6 +5205,18 @@ function glCreateTexture(image)
5224
5205
  return texture;
5225
5206
  }
5226
5207
 
5208
+ /** Set WebGL texture data from an image
5209
+ * @param {WebGLTexture} texture
5210
+ * @param {HTMLImageElement} image
5211
+ * @memberof WebGL */
5212
+ function glSetTextureData(texture, image)
5213
+ {
5214
+ // build the texture
5215
+ ASSERT(!!image && image.width > 0, 'Invalid image data.');
5216
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
5217
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
5218
+ }
5219
+
5227
5220
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
5228
5221
  * @memberof WebGL */
5229
5222
  function glFlush()
@@ -5277,10 +5270,10 @@ function glSetAntialias(antialias=true)
5277
5270
  * @param {Number} uv0Y
5278
5271
  * @param {Number} uv1X
5279
5272
  * @param {Number} uv1Y
5280
- * @param {Number} rgba
5281
- * @param {Number} [rgbaAdditive=0]
5273
+ * @param {Number} [rgba=-1] - white is -1
5274
+ * @param {Number} [rgbaAdditive=0] - black is 0
5282
5275
  * @memberof WebGL */
5283
- function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
5276
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=-1, rgbaAdditive=0)
5284
5277
  {
5285
5278
  ASSERT(typeof rgba == 'number' && typeof rgbaAdditive == 'number', 'invalid color');
5286
5279
 
@@ -5333,7 +5326,7 @@ const engineName = 'LittleJS';
5333
5326
  * @type {string}
5334
5327
  * @default
5335
5328
  * @memberof Engine */
5336
- const engineVersion = '1.11.13';
5329
+ const engineVersion = '1.11.17';
5337
5330
 
5338
5331
  /** Frames per second to update
5339
5332
  * @type {number}
@@ -5421,16 +5414,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5421
5414
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
5422
5415
 
5423
5416
  // allow passing in empty functions
5424
- if (!gameInit)
5425
- gameInit = ()=>{};
5426
- if (!gameUpdate)
5427
- gameUpdate = ()=>{};
5428
- if (!gameUpdatePost)
5429
- gameUpdatePost = ()=>{};
5430
- if (!gameRender)
5431
- gameRender = ()=>{};
5432
- if (!gameRenderPost)
5433
- gameRenderPost = ()=>{};
5417
+ gameInit ||= ()=>{};
5418
+ gameUpdate ||= ()=>{};
5419
+ gameUpdatePost ||= ()=>{};
5420
+ gameRender ||= ()=>{};
5421
+ gameRenderPost ||= ()=>{};
5434
5422
 
5435
5423
  // Called automatically by engine to setup render system
5436
5424
  function enginePreRender()
@@ -5457,11 +5445,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5457
5445
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
5458
5446
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
5459
5447
  if (debug) // +/- to speed/slow time
5460
- frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
5448
+ frameTimeDeltaMS *= debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
5461
5449
  timeReal += frameTimeDeltaMS / 1e3;
5462
5450
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
5463
5451
  if (!debugSpeedUp)
5464
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
5452
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
5453
+ if (debug && debugVideoCaptureIsActive())
5454
+ frameTimeBufferMS = 0; // disable time smoothing when capturing video
5465
5455
 
5466
5456
  updateCanvas();
5467
5457
 
@@ -5540,6 +5530,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5540
5530
  }
5541
5531
  }
5542
5532
 
5533
+ debugVideoCaptureUpdate();
5543
5534
  requestAnimationFrame(engineUpdate);
5544
5535
  }
5545
5536
 
@@ -5587,11 +5578,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5587
5578
 
5588
5579
  // setup html
5589
5580
  const styleRoot =
5590
- 'margin:0;overflow:hidden;' + // fill the window
5591
- 'width:100vw;height:100vh;' + // fill the window
5592
- 'display:flex;' + // use flexbox
5593
- 'align-items:center;' + // horizontal center
5594
- 'justify-content:center;' + // vertical center
5581
+ 'margin:0;' + // fill the window
5595
5582
  'background:#000;' + // set background color
5596
5583
  (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5597
5584
  'user-select:none;' + // prevent hold to select
@@ -5614,7 +5601,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5614
5601
  overlayContext = overlayCanvas.getContext('2d');
5615
5602
 
5616
5603
  // set canvas style
5617
- const styleCanvas = 'position:absolute'; // allow canvases to overlap
5604
+ const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
5605
+ 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
5618
5606
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
5619
5607
  if (glCanvas)
5620
5608
  glCanvas.style.cssText = styleCanvas;
@@ -5625,12 +5613,12 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5625
5613
  new Promise(resolve =>
5626
5614
  {
5627
5615
  const image = new Image;
5628
- image.crossOrigin = 'anonymous';
5629
5616
  image.onerror = image.onload = ()=>
5630
5617
  {
5631
5618
  textureInfos[textureIndex] = new TextureInfo(image);
5632
5619
  resolve();
5633
5620
  }
5621
+ image.crossOrigin = 'anonymous';
5634
5622
  image.src = src;
5635
5623
  })
5636
5624
  );