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
@@ -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
  );
@@ -5935,9 +5923,10 @@ function drawEngineSplashScreen(t)
5935
5923
 
5936
5924
  /**
5937
5925
  * LittleJS Module Export
5938
- * - Export engine as a module
5939
5926
  */
5940
5927
 
5928
+ 'use strict';
5929
+
5941
5930
  export
5942
5931
  {
5943
5932
  // Engine
@@ -5978,6 +5967,10 @@ export
5978
5967
  debugSaveCanvas,
5979
5968
  debugSaveText,
5980
5969
  debugSaveDataURL,
5970
+ debugShowErrors,
5971
+ debugVideoCaptureIsActive,
5972
+ debugVideoCaptureStart,
5973
+ debugVideoCaptureStop,
5981
5974
 
5982
5975
  // Settings
5983
5976
  cameraPos,
@@ -6151,6 +6144,7 @@ export
6151
6144
  glCopyToContext,
6152
6145
  glCreateProgram,
6153
6146
  glCreateTexture,
6147
+ glSetTextureData,
6154
6148
  glDraw,
6155
6149
  glFlush,
6156
6150
  glSetTexture,
@@ -6180,7 +6174,8 @@ export
6180
6174
  mousePosScreen,
6181
6175
  mouseWheel,
6182
6176
  isUsingGamepad,
6183
- preventDefaultInput,
6177
+ inputPreventDefault,
6178
+ setInputPreventDefault,
6184
6179
  gamepadIsDown,
6185
6180
  gamepadWasPressed,
6186
6181
  gamepadWasReleased,
@@ -6193,14 +6188,15 @@ export
6193
6188
  // Audio
6194
6189
  Sound,
6195
6190
  SoundWave,
6196
- Music,
6197
6191
  playAudioFile,
6198
6192
  speak,
6199
6193
  speakStop,
6200
6194
  getNoteFrequency,
6201
- audioContext,
6202
6195
  playSamples,
6203
6196
  zzfx,
6197
+ zzfxG,
6198
+ zzfxR,
6199
+ audioContext,
6204
6200
 
6205
6201
  // Base Object
6206
6202
  EngineObject,
@@ -6227,3 +6223,2835 @@ export
6227
6223
  Medal,
6228
6224
  };
6229
6225
 
6226
+ /**
6227
+ * LittleJS Newgrounds API
6228
+ * - NewgroundsMedal extends Medal with Newgrounds API functionality
6229
+ * - Call new NewgroundsPlugin() to setup Newgrounds
6230
+ * - Uses CryptoJS for encryption if optional cipher is provided
6231
+ * - Keeps connection alive and logs views
6232
+ * - Functions to interact with scoreboards
6233
+ * - Functions to unlock medals
6234
+ */
6235
+
6236
+ 'use strict';
6237
+
6238
+ /** Global Newgrounds object
6239
+ * @type {NewgroundsPlugin}
6240
+ * @memberof Medal */
6241
+ let newgrounds;
6242
+
6243
+ ///////////////////////////////////////////////////////////////////////////////
6244
+ /**
6245
+ * Newgrounds medal auto unlocks in newgrounds API
6246
+ * @extends Medal
6247
+ */
6248
+ class NewgroundsMedal extends Medal
6249
+ {
6250
+ /** Create a newgrounds medal object and adds it to the list of medals
6251
+ * @param {Number} id - The unique identifier of the medal
6252
+ * @param {String} name - Name of the medal
6253
+ * @param {String} [description] - Description of the medal
6254
+ * @param {String} [icon] - Icon for the medal
6255
+ * @param {String} [src] - Image location for the medal
6256
+ */
6257
+ constructor(id, name, description, icon, src)
6258
+ { super(id, name, description, icon, src); }
6259
+
6260
+ /** Unlocks a medal if not already unlocked */
6261
+ unlock()
6262
+ {
6263
+ super.unlock();
6264
+ newgrounds && newgrounds.unlockMedal(this.id);
6265
+ }
6266
+ }
6267
+
6268
+ ///////////////////////////////////////////////////////////////////////////////
6269
+ /**
6270
+ * Newgrounds API object
6271
+ */
6272
+ class NewgroundsPlugin
6273
+ {
6274
+ /** Create the global newgrounds object
6275
+ * @param {string} app_id - The newgrounds App ID
6276
+ * @param {string} [cipher] - The encryption Key (AES-128/Base64)
6277
+ * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
6278
+ * @example
6279
+ * // create the newgrounds object, replace the app id with your own
6280
+ * const app_id = 'your_app_id_here';
6281
+ * new NewgroundsPlugin(app_id);
6282
+ */
6283
+ constructor(app_id, cipher, cryptoJS)
6284
+ {
6285
+ ASSERT(!newgrounds, 'there can only be one newgrounds object');
6286
+ ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
6287
+
6288
+ newgrounds = this; // set global newgrounds object
6289
+ this.app_id = app_id;
6290
+ this.cipher = cipher;
6291
+ this.cryptoJS = cryptoJS;
6292
+ this.host = location ? location.hostname : '';
6293
+
6294
+ // get session id from url search params
6295
+ const url = new URL(location.href);
6296
+ this.session_id = url.searchParams.get('ngio_session_id');
6297
+
6298
+ if (!this.session_id)
6299
+ return; // only use newgrounds when logged in
6300
+
6301
+ // get medals
6302
+ const medalsResult = this.call('Medal.getList');
6303
+ this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
6304
+ debugMedals && console.log(this.medals);
6305
+ for (const newgroundsMedal of this.medals)
6306
+ {
6307
+ const medal = medals[newgroundsMedal['id']];
6308
+ if (medal)
6309
+ {
6310
+ // copy newgrounds medal data
6311
+ medal.image = new Image;
6312
+ medal.image.src = newgroundsMedal['icon'];
6313
+ medal.name = newgroundsMedal['name'];
6314
+ medal.description = newgroundsMedal['description'];
6315
+ medal.unlocked = newgroundsMedal['unlocked'];
6316
+ medal.difficulty = newgroundsMedal['difficulty'];
6317
+ medal.value = newgroundsMedal['value'];
6318
+
6319
+ if (medal.value) // add value to description
6320
+ medal.description = medal.description + ` (${ medal.value })`;
6321
+ }
6322
+ }
6323
+
6324
+ // get scoreboards
6325
+ const scoreboardResult = this.call('ScoreBoard.getBoards');
6326
+ this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
6327
+ debugMedals && console.log(this.scoreboards);
6328
+
6329
+ // keep the session alive with a ping every minute
6330
+ const keepAliveMS = 60 * 1e3;
6331
+ setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
6332
+ }
6333
+
6334
+ /** Send message to unlock a medal by id
6335
+ * @param {number} id - The medal id */
6336
+ unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
6337
+
6338
+ /** Send message to post score
6339
+ * @param {number} id - The scoreboard id
6340
+ * @param {number} value - The score value */
6341
+ postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
6342
+
6343
+ /** Get scores from a scoreboard
6344
+ * @param {number} id - The scoreboard id
6345
+ * @param {string} [user] - A user's id or name
6346
+ * @param {number} [social] - If true, only social scores will be loaded
6347
+ * @param {number} [skip] - Number of scores to skip before start
6348
+ * @param {number} [limit] - Number of scores to include in the list
6349
+ * @return {Object} - The response JSON object
6350
+ */
6351
+ getScores(id, user, social=0, skip=0, limit=10)
6352
+ { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
6353
+
6354
+ /** Send message to log a view */
6355
+ logView() { return this.call('App.logView', {'host':this.host}, true); }
6356
+
6357
+ /** Send a message to call a component of the Newgrounds API
6358
+ * @param {string} component - Name of the component
6359
+ * @param {Object} [parameters] - Parameters to use for call
6360
+ * @param {boolean} [async] - If true, don't wait for response before continuing
6361
+ * @return {Object} - The response JSON object
6362
+ */
6363
+ call(component, parameters, async=false)
6364
+ {
6365
+ const call = {'component':component, 'parameters':parameters};
6366
+ if (this.cipher)
6367
+ {
6368
+ // encrypt using AES-128 Base64 with cryptoJS
6369
+ const cryptoJS = this.cryptoJS;
6370
+ const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
6371
+ const iv = cryptoJS['lib']['WordArray']['random'](16);
6372
+ const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
6373
+ call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
6374
+ call['parameters'] = 0;
6375
+ }
6376
+
6377
+ // build the input object
6378
+ const input =
6379
+ {
6380
+ 'app_id': this.app_id,
6381
+ 'session_id': this.session_id,
6382
+ 'call': call
6383
+ };
6384
+
6385
+ // build post data
6386
+ const formData = new FormData();
6387
+ formData.append('input', JSON.stringify(input));
6388
+
6389
+ // send post data
6390
+ const xmlHttp = new XMLHttpRequest();
6391
+ const url = 'https://newgrounds.io/gateway_v3.php';
6392
+ xmlHttp.open('POST', url, !debugMedals && async);
6393
+ try { xmlHttp.send(formData); }
6394
+ catch(e)
6395
+ {
6396
+ debugMedals && console.log('newgrounds call failed', e);
6397
+ return;
6398
+ }
6399
+ debugMedals && console.log(xmlHttp.responseText);
6400
+ return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
6401
+ }
6402
+ }
6403
+
6404
+ /**
6405
+ * LittleJS Post Processing Plugin
6406
+ * - Supports shadertoy style post processing shaders
6407
+ * - call new new PostProcessPlugin() to setup post processing
6408
+ * - can be enabled to pass other canvases through a final shader
6409
+ */
6410
+
6411
+ 'use strict';
6412
+
6413
+ ///////////////////////////////////////////////////////////////////////////////
6414
+
6415
+ /** Global Post Process plugin object
6416
+ * @type {PostProcessPlugin} */
6417
+ let postProcess;
6418
+
6419
+ /////////////////////////////////////////////////////////////////////////
6420
+ /**
6421
+ * UI System Global Object
6422
+ */
6423
+ class PostProcessPlugin
6424
+ {
6425
+ /** Create global post processing shader
6426
+ * @param {string} shaderCode
6427
+ * @param {boolean} [includeOverlay]
6428
+ * @example
6429
+ * // create the post process plugin object
6430
+ * new PostProcessPlugin(shaderCode);
6431
+ */
6432
+ constructor(shaderCode, includeOverlay=false)
6433
+ {
6434
+ ASSERT(!postProcess, 'Post process already initialized');
6435
+ postProcess = this;
6436
+
6437
+ if (headlessMode) return;
6438
+ if (!shaderCode) // default shader pass through
6439
+ shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
6440
+
6441
+ /** @property {WebGLProgram} - Shader for post processing */
6442
+ this.shader = glCreateProgram(
6443
+ '#version 300 es\n' + // specify GLSL ES version
6444
+ 'precision highp float;'+ // use highp for better accuracy
6445
+ 'in vec2 p;'+ // position
6446
+ 'void main(){'+ // shader entry point
6447
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
6448
+ '}' // end of shader
6449
+ ,
6450
+ '#version 300 es\n' + // specify GLSL ES version
6451
+ 'precision highp float;'+ // use highp for better accuracy
6452
+ 'uniform sampler2D iChannel0;'+ // input texture
6453
+ 'uniform vec3 iResolution;'+ // size of output texture
6454
+ 'uniform float iTime;'+ // time
6455
+ 'out vec4 c;'+ // out color
6456
+ '\n' + shaderCode + '\n'+ // insert custom shader code
6457
+ 'void main(){'+ // shader entry point
6458
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
6459
+ 'c.a=1.;'+ // always use full alpha
6460
+ '}' // end of shader
6461
+ );
6462
+
6463
+ /** @property {WebGLTexture} - Texture for post processing */
6464
+ this.texture = glCreateTexture();
6465
+
6466
+ /** @property {boolean} - Should overlay canvas be included in post processing */
6467
+ this.includeOverlay = includeOverlay;
6468
+
6469
+ // Render the post processing shader, called automatically by the engine
6470
+ engineAddPlugin(undefined, postProcessRender);
6471
+ function postProcessRender()
6472
+ {
6473
+ if (headlessMode) return;
6474
+
6475
+ // prepare to render post process shader
6476
+ if (glEnable)
6477
+ {
6478
+ glFlush(); // clear out the buffer
6479
+ mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
6480
+ }
6481
+ else
6482
+ {
6483
+ // set the viewport
6484
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
6485
+ }
6486
+
6487
+ if (postProcess.includeOverlay)
6488
+ {
6489
+ // copy overlay canvas so it will be included in post processing
6490
+ mainContext.drawImage(overlayCanvas, 0, 0);
6491
+ overlayCanvas.width |= 0;
6492
+ }
6493
+
6494
+ // setup shader program to draw one triangle
6495
+ glContext.useProgram(postProcess.shader);
6496
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6497
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
6498
+ glContext.disable(glContext.BLEND);
6499
+
6500
+ // set textures, pass in the 2d canvas and gl canvas in separate texture channels
6501
+ glContext.activeTexture(glContext.TEXTURE0);
6502
+ glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
6503
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
6504
+
6505
+ // set vertex position attribute
6506
+ const vertexByteStride = 8;
6507
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
6508
+ glContext.enableVertexAttribArray(pLocation);
6509
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
6510
+
6511
+ // set uniforms and draw
6512
+ const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
6513
+ glContext.uniform1i(uniformLocation('iChannel0'), 0);
6514
+ glContext.uniform1f(uniformLocation('iTime'), time);
6515
+ glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
6516
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
6517
+ }
6518
+ }
6519
+ }
6520
+
6521
+ /**
6522
+ * LittleJS ZzFXM Plugin
6523
+ */
6524
+
6525
+ 'use strict';
6526
+
6527
+ /**
6528
+ * Music Object - Stores a zzfx music track for later use
6529
+ *
6530
+ * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
6531
+ * @example
6532
+ * // create some music
6533
+ * const music_example = new Music(
6534
+ * [
6535
+ * [ // instruments
6536
+ * [,0,400] // simple note
6537
+ * ],
6538
+ * [ // patterns
6539
+ * [ // pattern 1
6540
+ * [ // channel 0
6541
+ * 0, -1, // instrument 0, left speaker
6542
+ * 1, 0, 9, 1 // channel notes
6543
+ * ],
6544
+ * [ // channel 1
6545
+ * 0, 1, // instrument 0, right speaker
6546
+ * 0, 12, 17, -1 // channel notes
6547
+ * ]
6548
+ * ],
6549
+ * ],
6550
+ * [0, 0, 0, 0], // sequence, play pattern 0 four times
6551
+ * 90 // BPM
6552
+ * ]);
6553
+ *
6554
+ * // play the music
6555
+ * music_example.play();
6556
+ */
6557
+ class ZzFXMusic extends Sound
6558
+ {
6559
+ /** Create a music object and cache the zzfx music samples for later use
6560
+ * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
6561
+ */
6562
+ constructor(zzfxMusic)
6563
+ {
6564
+ super(undefined);
6565
+
6566
+ if (!soundEnable || headlessMode) return;
6567
+ this.randomness = 0;
6568
+ this.sampleChannels = zzfxM(...zzfxMusic);
6569
+ this.sampleRate = zzfxR;
6570
+ }
6571
+
6572
+ /** Play the music
6573
+ * @param {number} [volume=1] - How much to scale volume by
6574
+ * @param {boolean} [loop] - True if the music should loop
6575
+ * @return {AudioBufferSourceNode} - The audio source node
6576
+ */
6577
+ playMusic(volume, loop=false)
6578
+ { return super.play(undefined, volume, 1, 1, loop); }
6579
+ }
6580
+
6581
+ ///////////////////////////////////////////////////////////////////////////////
6582
+ // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
6583
+
6584
+ /** Generate samples for a ZzFM song with given parameters
6585
+ * @param {Array} instruments - Array of ZzFX sound parameters
6586
+ * @param {Array} patterns - Array of pattern data
6587
+ * @param {Array} sequence - Array of pattern indexes
6588
+ * @param {number} [BPM] - Playback speed of the song in BPM
6589
+ * @return {Array} - Left and right channel sample data */
6590
+ function zzfxM(instruments, patterns, sequence, BPM = 125)
6591
+ {
6592
+ let i, j, k;
6593
+ let instrumentParameters;
6594
+ let note;
6595
+ let sample;
6596
+ let patternChannel;
6597
+ let notFirstBeat;
6598
+ let stop;
6599
+ let instrument;
6600
+ let attenuation;
6601
+ let outSampleOffset;
6602
+ let isSequenceEnd;
6603
+ let sampleOffset = 0;
6604
+ let nextSampleOffset;
6605
+ let sampleBuffer = [];
6606
+ let leftChannelBuffer = [];
6607
+ let rightChannelBuffer = [];
6608
+ let channelIndex = 0;
6609
+ let panning = 0;
6610
+ let hasMore = 1;
6611
+ let sampleCache = {};
6612
+ let beatLength = zzfxR / BPM * 60 >> 2;
6613
+
6614
+ // for each channel in order until there are no more
6615
+ for (; hasMore; channelIndex++) {
6616
+
6617
+ // reset current values
6618
+ sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
6619
+
6620
+ // for each pattern in sequence
6621
+ sequence.forEach((patternIndex, sequenceIndex) => {
6622
+ // get pattern for current channel, use empty 1 note pattern if none found
6623
+ patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
6624
+
6625
+ // check if there are more channels
6626
+ hasMore |= patterns[patternIndex][channelIndex]&&1;
6627
+
6628
+ // get next offset, use the length of first channel
6629
+ nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
6630
+ // for each beat in pattern, plus one extra if end of sequence
6631
+ isSequenceEnd = sequenceIndex == sequence.length - 1;
6632
+ for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
6633
+
6634
+ // <channel-note>
6635
+ note = patternChannel[i];
6636
+
6637
+ // stop if end, different instrument or new note
6638
+ stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
6639
+ instrument != (patternChannel[0] || 0) || note | 0;
6640
+
6641
+ // fill buffer with samples for previous beat, most cpu intensive part
6642
+ for (j = 0; j < beatLength && notFirstBeat;
6643
+
6644
+ // fade off attenuation at end of beat if stopping note, prevents clicking
6645
+ j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
6646
+ ) {
6647
+ // copy sample to stereo buffers with panning
6648
+ sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
6649
+ leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
6650
+ rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
6651
+ }
6652
+
6653
+ // set up for next note
6654
+ if (note) {
6655
+ // set attenuation
6656
+ attenuation = note % 1;
6657
+ panning = patternChannel[1] || 0;
6658
+ if (note |= 0) {
6659
+ // get cached sample
6660
+ sampleBuffer = sampleCache[
6661
+ [
6662
+ instrument = patternChannel[sampleOffset = 0] || 0,
6663
+ note
6664
+ ]
6665
+ ] = sampleCache[[instrument, note]] || (
6666
+ // add sample to cache
6667
+ instrumentParameters = [...instruments[instrument]],
6668
+ instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
6669
+
6670
+ // allow negative values to stop notes
6671
+ note > 0 ? zzfxG(...instrumentParameters) : []
6672
+ );
6673
+ }
6674
+ }
6675
+ }
6676
+
6677
+ // update the sample offset
6678
+ outSampleOffset = nextSampleOffset;
6679
+ });
6680
+ }
6681
+
6682
+ return [leftChannelBuffer, rightChannelBuffer];
6683
+ }
6684
+
6685
+ /**
6686
+ * LittleJS User Interface Plugin
6687
+ * - call new UISystemPlugin() to setup the UI system
6688
+ * - Nested Menus
6689
+ * - Text
6690
+ * - Buttons
6691
+ * - Checkboxes
6692
+ * - Images
6693
+ */
6694
+
6695
+ 'use strict';
6696
+
6697
+ ///////////////////////////////////////////////////////////////////////////////
6698
+
6699
+ /** Global UI system plugin object
6700
+ * @type {UISystemPlugin} */
6701
+ let uiSystem;
6702
+
6703
+ ///////////////////////////////////////////////////////////////////////////////
6704
+ /**
6705
+ * UI System Global Object
6706
+ */
6707
+ class UISystemPlugin
6708
+ {
6709
+ /** Create the global UI system object
6710
+ * @param {CanvasRenderingContext2D} [context]
6711
+ * @example
6712
+ * // create the ui plugin object
6713
+ * new UISystemPlugin;
6714
+ */
6715
+ constructor(context=overlayContext)
6716
+ {
6717
+ ASSERT(!uiSystem, 'UI system already initialized');
6718
+ uiSystem = this;
6719
+
6720
+ /** @property {Color} - Default fill color for UI elements */
6721
+ this.defaultColor = WHITE;
6722
+ /** @property {Color} - Default outline color for UI elements */
6723
+ this.defaultLineColor = BLACK;
6724
+ /** @property {Color} - Default text color for UI elements */
6725
+ this.defaultTextColor = BLACK;
6726
+ /** @property {Color} - Default button color for UI elements */
6727
+ this.defaultButtonColor = hsl(0,0,.5);
6728
+ /** @property {Color} - Default hover color for UI elements */
6729
+ this.defaultHoverColor = hsl(0,0,.7);
6730
+ /** @property {number} - Default line width for UI elements */
6731
+ this.defaultLineWidth = 4;
6732
+ /** @property {string} - Default font for UI elements */
6733
+ this.defaultFont = 'arial';
6734
+ /** @property {Array<UIObject>} - List of all UI elements */
6735
+ this.uiObjects = [];
6736
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
6737
+ this.uiContext = context;
6738
+
6739
+ engineAddPlugin(uiUpdate, uiRender);
6740
+
6741
+ // setup recursive update and render
6742
+ function uiUpdate()
6743
+ {
6744
+ function updateObject(o)
6745
+ {
6746
+ if (!o.visible)
6747
+ return;
6748
+ if (o.parent)
6749
+ o.pos = o.localPos.add(o.parent.pos);
6750
+ o.update();
6751
+ for(const c of o.children)
6752
+ updateObject(c);
6753
+ }
6754
+ uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
6755
+ }
6756
+ function uiRender()
6757
+ {
6758
+ function renderObject(o)
6759
+ {
6760
+ if (!o.visible)
6761
+ return;
6762
+ if (o.parent)
6763
+ o.pos = o.localPos.add(o.parent.pos);
6764
+ o.render();
6765
+ for(const c of o.children)
6766
+ renderObject(c);
6767
+ }
6768
+ uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
6769
+ }
6770
+ }
6771
+
6772
+ /** Draw a rectangle to the UI context
6773
+ * @param {Vector2} pos
6774
+ * @param {Vector2} size
6775
+ * @param {Color} [color=uiSystem.defaultColor]
6776
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6777
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6778
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6779
+ {
6780
+ uiSystem.uiContext.fillStyle = color.toString();
6781
+ uiSystem.uiContext.beginPath();
6782
+ uiSystem.uiContext.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
6783
+ uiSystem.uiContext.fill();
6784
+ if (lineWidth)
6785
+ {
6786
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6787
+ uiSystem.uiContext.lineWidth = lineWidth;
6788
+ uiSystem.uiContext.stroke();
6789
+ }
6790
+ }
6791
+
6792
+ /** Draw a line to the UI context
6793
+ * @param {Vector2} posA
6794
+ * @param {Vector2} posB
6795
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6796
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6797
+ drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6798
+ {
6799
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6800
+ uiSystem.uiContext.lineWidth = lineWidth;
6801
+ uiSystem.uiContext.beginPath();
6802
+ uiSystem.uiContext.lineTo(posA.x, posA.y);
6803
+ uiSystem.uiContext.lineTo(posB.x, posB.y);
6804
+ uiSystem.uiContext.stroke();
6805
+ }
6806
+
6807
+ /** Draw a tile to the UI context
6808
+ * @param {Vector2} pos
6809
+ * @param {Vector2} size
6810
+ * @param {TileInfo} tileInfo
6811
+ * @param {Color} [color=uiSystem.defaultColor]
6812
+ * @param {number} [angle]
6813
+ * @param {boolean} [mirror] */
6814
+ drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false)
6815
+ {
6816
+ drawTile(pos, size, tileInfo, color, angle, mirror, BLACK, false, true, uiSystem.uiContext);
6817
+ }
6818
+
6819
+ /** Draw text to the UI context
6820
+ * @param {string} text
6821
+ * @param {Vector2} pos
6822
+ * @param {Vector2} size
6823
+ * @param {Color} [color=uiSystem.defaultColor]
6824
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6825
+ * @param {Color} [lineColor=uiSystem.defaultLineColor]
6826
+ * @param {string} [align]
6827
+ * @param {string} [font=uiSystem.defaultFont] */
6828
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont)
6829
+ {
6830
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, size.x, uiSystem.uiContext);
6831
+ }
6832
+ }
6833
+
6834
+ ///////////////////////////////////////////////////////////////////////////////
6835
+ /**
6836
+ * UI Object - Base level object for all UI elements
6837
+ */
6838
+ class UIObject
6839
+ {
6840
+ /** Create a UIObject
6841
+ * @param {Vector2} [pos=(0,0)]
6842
+ * @param {Vector2} [size=(1,1)]
6843
+ */
6844
+ constructor(pos=vec2(), size=vec2())
6845
+ {
6846
+ /** @property {Vector2} - Local position of the object */
6847
+ this.localPos = pos.copy();
6848
+ /** @property {Vector2} - Screen space position of the object */
6849
+ this.pos = pos.copy();
6850
+ /** @property {Vector2} - Screen space size of the object */
6851
+ this.size = size.copy();
6852
+ /** @property {Color} */
6853
+ this.color = uiSystem.defaultColor;
6854
+ /** @property {Color} */
6855
+ this.lineColor = uiSystem.defaultLineColor;
6856
+ /** @property {Color} */
6857
+ this.textColor = uiSystem.defaultTextColor;
6858
+ /** @property {Color} */
6859
+ this.hoverColor = uiSystem.defaultHoverColor;
6860
+ /** @property {number} */
6861
+ this.lineWidth = uiSystem.defaultLineWidth;
6862
+ /** @property {string} */
6863
+ this.font = uiSystem.defaultFont;
6864
+ /** @property {boolean} */
6865
+ this.visible = true;
6866
+ /** @property {Array<UIObject>} */
6867
+ this.children = [];
6868
+ /** @property {UIObject} */
6869
+ this.parent = undefined;
6870
+ uiSystem.uiObjects.push(this);
6871
+ }
6872
+
6873
+ /** Add a child UIObject to this object
6874
+ * @param {UIObject} child
6875
+ */
6876
+ addChild(child)
6877
+ {
6878
+ ASSERT(!child.parent && !this.children.includes(child));
6879
+ this.children.push(child);
6880
+ child.parent = this;
6881
+ }
6882
+
6883
+ /** Remove a child UIObject from this object
6884
+ * @param {UIObject} child
6885
+ */
6886
+ removeChild(child)
6887
+ {
6888
+ ASSERT(child.parent == this && this.children.includes(child));
6889
+ this.children.splice(this.children.indexOf(child), 1);
6890
+ child.parent = undefined;
6891
+ }
6892
+
6893
+ /** Update the object, called automatically by plugin once each frame */
6894
+ update()
6895
+ {
6896
+ // track mouse input
6897
+ const mouseWasOver = this.mouseIsOver;
6898
+ const mouseDown = mouseIsDown(0);
6899
+ if (!mouseDown || isTouchDevice)
6900
+ {
6901
+ this.mouseIsOver = isOverlapping(this.pos, this.size, mousePosScreen);
6902
+ if (!mouseDown && isTouchDevice)
6903
+ this.mouseIsOver = false;
6904
+ if (this.mouseIsOver && !mouseWasOver)
6905
+ this.onEnter();
6906
+ if (!this.mouseIsOver && mouseWasOver)
6907
+ this.onLeave();
6908
+ }
6909
+ if (mouseWasPressed(0) && this.mouseIsOver)
6910
+ {
6911
+ this.mouseIsHeld = true;
6912
+ this.onPress();
6913
+ if (isTouchDevice)
6914
+ this.mouseIsOver = false;
6915
+ }
6916
+ else if (this.mouseIsHeld && !mouseDown)
6917
+ {
6918
+ this.mouseIsHeld = false;
6919
+ this.onRelease();
6920
+ }
6921
+ }
6922
+
6923
+ /** Render the object, called automatically by plugin once each frame */
6924
+ render()
6925
+ {
6926
+ if (this.size.x && this.size.y)
6927
+ uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor);
6928
+ }
6929
+
6930
+ /** Called when the mouse enters the object */
6931
+ onEnter() {}
6932
+
6933
+ /** Called when the mouse leaves the object */
6934
+ onLeave() {}
6935
+
6936
+ /** Called when the mouse is pressed while over the object */
6937
+ onPress() {}
6938
+
6939
+ /** Called when the mouse is released while over the object */
6940
+ onRelease() {}
6941
+
6942
+ /** Called when the state of this object changes */
6943
+ onChange() {}
6944
+ }
6945
+
6946
+ ///////////////////////////////////////////////////////////////////////////////
6947
+ /**
6948
+ * UIText - A UI object that displays text
6949
+ * @extends UIObject
6950
+ */
6951
+ class UIText extends UIObject
6952
+ {
6953
+ /** Create a UIText object
6954
+ * @param {Vector2} [pos]
6955
+ * @param {Vector2} [size]
6956
+ * @param {string} [text]
6957
+ * @param {string} [align]
6958
+ * @param {string} [font=uiSystem.defaultFont]
6959
+ */
6960
+ constructor(pos, size, text='', align='center', font=uiSystem.defaultFont)
6961
+ {
6962
+ super(pos, size);
6963
+
6964
+ /** @property {string} */
6965
+ this.text = text;
6966
+ /** @property {string} */
6967
+ this.align = align;
6968
+
6969
+ this.font = font; // set font
6970
+ this.lineWidth = 0; // set text to not be outlined by default
6971
+ }
6972
+ render()
6973
+ {
6974
+ uiSystem.drawText(this.text, this.pos, this.size, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
6975
+ }
6976
+ }
6977
+
6978
+ ///////////////////////////////////////////////////////////////////////////////
6979
+ /**
6980
+ * UITile - A UI object that displays a tile image
6981
+ * @extends UIObject
6982
+ */
6983
+ class UITile extends UIObject
6984
+ {
6985
+ /** Create a UITile object
6986
+ * @param {Vector2} [pos]
6987
+ * @param {Vector2} [size]
6988
+ * @param {TileInfo} [tileInfo]
6989
+ * @param {Color} [color=WHITE]
6990
+ * @param {number} [angle]
6991
+ * @param {boolean} [mirror]
6992
+ */
6993
+ constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
6994
+ {
6995
+ super(pos, size);
6996
+
6997
+ /** @property {TileInfo} - Tile image to use */
6998
+ this.tileInfo = tileInfo;
6999
+ /** @property {number} - Angle to rotate in radians */
7000
+ this.angle = angle;
7001
+ /** @property {boolean} - Should it be mirrored? */
7002
+ this.mirror = mirror;
7003
+ this.color = color;
7004
+ }
7005
+ render()
7006
+ {
7007
+ uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
7008
+ }
7009
+ }
7010
+
7011
+ ///////////////////////////////////////////////////////////////////////////////
7012
+ /**
7013
+ * UIButton - A UI object that acts as a button
7014
+ * @extends UIObject
7015
+ */
7016
+ class UIButton extends UIObject
7017
+ {
7018
+ /** Create a UIButton object
7019
+ * @param {Vector2} [pos]
7020
+ * @param {Vector2} [size]
7021
+ * @param {string} [text]
7022
+ * @param {Color} [color=uiSystem.defaultButtonColor]
7023
+ */
7024
+ constructor(pos, size, text='', color=uiSystem.defaultButtonColor)
7025
+ {
7026
+ super(pos, size);
7027
+
7028
+ /** @property {string} */
7029
+ this.text = text;
7030
+ this.color = color;
7031
+ }
7032
+ render()
7033
+ {
7034
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
7035
+ const color = this.mouseIsOver? this.hoverColor : this.color;
7036
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
7037
+ const textSize = vec2(this.size.x, this.size.y*.8);
7038
+ uiSystem.drawText(this.text, this.pos, textSize,
7039
+ this.textColor, 0, undefined, this.align, this.font);
7040
+ }
7041
+ }
7042
+
7043
+ ///////////////////////////////////////////////////////////////////////////////
7044
+ /**
7045
+ * UICheckbox - A UI object that acts as a checkbox
7046
+ * @extends UIObject
7047
+ */
7048
+ class UICheckbox extends UIObject
7049
+ {
7050
+ /** Create a UICheckbox object
7051
+ * @param {Vector2} [pos]
7052
+ * @param {Vector2} [size]
7053
+ * @param {boolean} [checked]
7054
+ */
7055
+ constructor(pos, size, checked=false)
7056
+ {
7057
+ super(pos, size);
7058
+
7059
+ /** @property {boolean} */
7060
+ this.checked = checked;
7061
+ }
7062
+ onPress()
7063
+ {
7064
+ this.checked = !this.checked;
7065
+ this.onChange();
7066
+ }
7067
+ render()
7068
+ {
7069
+ const color = this.mouseIsOver? this.hoverColor : this.color;
7070
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor);
7071
+ if (this.checked)
7072
+ {
7073
+ // draw an X if checked
7074
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor);
7075
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor);
7076
+ }
7077
+ }
7078
+ }
7079
+
7080
+ ///////////////////////////////////////////////////////////////////////////////
7081
+ /**
7082
+ * UIScrollbar - A UI object that acts as a scrollbar
7083
+ * @extends UIObject
7084
+ */
7085
+ class UIScrollbar extends UIObject
7086
+ {
7087
+ /** Create a UIScrollbar object
7088
+ * @param {Vector2} [pos]
7089
+ * @param {Vector2} [size]
7090
+ * @param {number} [value]
7091
+ * @param {string} [text]
7092
+ * @param {Color} [color=uiSystem.defaultButtonColor]
7093
+ * @param {Color} [handleColor=WHITE]
7094
+ */
7095
+ constructor(pos, size, value=.5, text='', color=uiSystem.defaultButtonColor, handleColor=WHITE)
7096
+ {
7097
+ super(pos, size);
7098
+
7099
+ /** @property {number} */
7100
+ this.value = value;
7101
+ /** @property {string} */
7102
+ this.text = text;
7103
+ this.color = color;
7104
+ this.handleColor = handleColor;
7105
+ }
7106
+ update()
7107
+ {
7108
+ super.update();
7109
+ if (this.mouseIsHeld)
7110
+ {
7111
+ const handleSize = vec2(this.size.y);
7112
+ const handleWidth = this.size.x - handleSize.x;
7113
+ const p1 = this.pos.x - handleWidth/2;
7114
+ const p2 = this.pos.x + handleWidth/2;
7115
+ const oldValue = this.value;
7116
+ this.value = percent(mousePosScreen.x, p1, p2);
7117
+ this.value == oldValue || this.onChange();
7118
+ }
7119
+ }
7120
+ render()
7121
+ {
7122
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
7123
+ const color = this.mouseIsOver? this.hoverColor : this.color;
7124
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
7125
+
7126
+ const handleSize = vec2(this.size.y);
7127
+ const handleWidth = this.size.x - handleSize.x;
7128
+ const p1 = this.pos.x - handleWidth/2;
7129
+ const p2 = this.pos.x + handleWidth/2;
7130
+ const handlePos = vec2(lerp(this.value, p1, p2), this.pos.y);
7131
+ const barColor = this.mouseIsHeld ? this.color : this.handleColor;
7132
+ uiSystem.drawRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor);
7133
+
7134
+ const textSize = vec2(this.size.x, this.size.y*.8);
7135
+ uiSystem.drawText(this.text, this.pos, textSize,
7136
+ this.textColor, 0, undefined, this.align, this.font);
7137
+ }
7138
+ }
7139
+
7140
+ /**
7141
+ * LittleJS Box2D Physics Plugin
7142
+ * - Box2dObject extends EngineObject with Box2D physics
7143
+ * - Call box2dEngineInit() to start instead of normal engineInit()
7144
+ * - You will also need to include box2d.wasm.js
7145
+ * - Uses box2d.js super fast web assembly port of Box2D
7146
+ * - More info: https://github.com/kripken/box2d.js
7147
+ * - Fully wraps everything in Box2d
7148
+ * - Functions to create polygon, circle, and edge shapes
7149
+ * - Raycasting and querying
7150
+ * - Joint creation
7151
+ * - Contact begin and end callbacks
7152
+ * - Debug physics drawing
7153
+ */
7154
+
7155
+ 'use strict';
7156
+
7157
+ /** Global Box2d Plugin object
7158
+ * @type {Box2dPlugin} */
7159
+ let box2d;
7160
+
7161
+ /** Enable Box2D debug drawing
7162
+ * @type {boolean}
7163
+ * @default */
7164
+ let box2dDebug = false;
7165
+
7166
+ ///////////////////////////////////////////////////////////////////////////////
7167
+ /**
7168
+ * Box2D Object - extend with your own custom physics objects
7169
+ * - A LittleJS object with Box2D physics
7170
+ * - Each object has a Box2D body which can have multiple fixtures and joints
7171
+ * - Provides interface for Box2D body and fixture functions
7172
+ * @extends EngineObject
7173
+ */
7174
+ class Box2dObject extends EngineObject
7175
+ {
7176
+ /** Create a LittleJS object with Box2d physics
7177
+ * @param {Vector2} [pos]
7178
+ * @param {Vector2} [size]
7179
+ * @param {TileInfo} [tileInfo]
7180
+ * @param {number} [angle]
7181
+ * @param {Color} [color]
7182
+ * @param {number} [bodyType]
7183
+ * @param {number} [renderOrder] */
7184
+ constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
7185
+ {
7186
+ super(pos, size, tileInfo, angle, color, renderOrder);
7187
+
7188
+ // create physics body
7189
+ const bodyDef = new box2d.instance.b2BodyDef();
7190
+ bodyDef.set_type(bodyType);
7191
+ bodyDef.set_position(box2d.vec2dTo(pos));
7192
+ bodyDef.set_angle(-angle);
7193
+ this.body = box2d.world.CreateBody(bodyDef);
7194
+ this.body.object = this;
7195
+ this.outlineColor = BLACK;
7196
+ }
7197
+
7198
+ /** Destroy this object and it's physics body */
7199
+ destroy()
7200
+ {
7201
+ // destroy physics body, fixtures, and joints
7202
+ this.body && box2d.world.DestroyBody(this.body);
7203
+ this.body = 0;
7204
+ super.destroy();
7205
+ }
7206
+
7207
+ /** Copy box2d update sim data */
7208
+ update()
7209
+ {
7210
+ // use box2d physics update
7211
+ this.pos = box2d.vec2From(this.body.GetPosition());
7212
+ this.angle = -this.body.GetAngle();
7213
+ }
7214
+
7215
+ /** Render the object, uses box2d drawing if no tile info exists */
7216
+ render()
7217
+ {
7218
+ // use default render or draw fixtures
7219
+ if (this.tileInfo)
7220
+ super.render();
7221
+ else
7222
+ this.drawFixtures(this.color, this.outlineColor, this.lineWidth, mainContext);
7223
+ }
7224
+
7225
+ /** Render debug info */
7226
+ renderDebugInfo()
7227
+ {
7228
+ const isAsleep = !this.getIsAwake();
7229
+ const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
7230
+ const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
7231
+ this.drawFixtures(color);
7232
+ }
7233
+
7234
+ /** Draws all this object's fixtures
7235
+ * @param {Color} [color]
7236
+ * @param {Color} [outlineColor]
7237
+ * @param {number} [lineWidth]
7238
+ * @param {CanvasRenderingContext2D} [context] */
7239
+ drawFixtures(color=WHITE, outlineColor, lineWidth=.1, context)
7240
+ {
7241
+ this.getFixtureList().forEach(fixture=>
7242
+ box2d.drawFixture(fixture, this.pos, this.angle, color, outlineColor, lineWidth, context));
7243
+ }
7244
+
7245
+ ///////////////////////////////////////////////////////////////////////////////
7246
+ // physics contact callbacks
7247
+
7248
+ /** Called when a contact begins
7249
+ * @param {Box2dObject} otherObject */
7250
+ beginContact(otherObject) {}
7251
+
7252
+ /** Called when a contact ends
7253
+ * @param {Box2dObject} otherObject */
7254
+ endContact(otherObject) {}
7255
+
7256
+ ///////////////////////////////////////////////////////////////////////////////
7257
+ // physics fixtures and shapes
7258
+
7259
+ /** Add a shape fixture to the body
7260
+ * @param {Object} shape
7261
+ * @param {number} [density]
7262
+ * @param {number} [friction]
7263
+ * @param {number} [restitution]
7264
+ * @param {boolean} [isSensor] */
7265
+ addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
7266
+ {
7267
+ const fd = new box2d.instance.b2FixtureDef();
7268
+ fd.set_shape(shape);
7269
+ fd.set_density(density);
7270
+ fd.set_friction(friction);
7271
+ fd.set_restitution(restitution);
7272
+ fd.set_isSensor(isSensor);
7273
+ return this.body.CreateFixture(fd);
7274
+ }
7275
+
7276
+ /** Add a box shape to the body
7277
+ * @param {Vector2} [size]
7278
+ * @param {Vector2} [offset]
7279
+ * @param {number} [angle]
7280
+ * @param {number} [density]
7281
+ * @param {number} [friction]
7282
+ * @param {number} [restitution]
7283
+ * @param {boolean} [isSensor] */
7284
+ addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
7285
+ {
7286
+ const shape = new box2d.instance.b2PolygonShape();
7287
+ shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
7288
+ return this.addShape(shape, density, friction, restitution, isSensor);
7289
+ }
7290
+
7291
+ /** Add a polygon shape to the body
7292
+ * @param {Array<Vector2>} points
7293
+ * @param {number} [density]
7294
+ * @param {number} [friction]
7295
+ * @param {number} [restitution]
7296
+ * @param {boolean} [isSensor] */
7297
+ addPoly(points, density, friction, restitution, isSensor)
7298
+ {
7299
+ function box2dCreatePolygonShape(points)
7300
+ {
7301
+ function box2dCreatePointList(points)
7302
+ {
7303
+ const buffer = box2d.instance._malloc(points.length * 8);
7304
+ for (let i=0, offset=0; i<points.length; ++i)
7305
+ {
7306
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x;
7307
+ offset += 4;
7308
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
7309
+ offset += 4;
7310
+ }
7311
+ return box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
7312
+ }
7313
+
7314
+ ASSERT(3 <= points.length && points.length <= 8);
7315
+ const shape = new box2d.instance.b2PolygonShape();
7316
+ const box2dPoints = box2dCreatePointList(points);
7317
+ shape.Set(box2dPoints, points.length);
7318
+ return shape;
7319
+ }
7320
+
7321
+ const shape = box2dCreatePolygonShape(points);
7322
+ return this.addShape(shape, density, friction, restitution, isSensor);
7323
+ }
7324
+
7325
+ /** Add a regular polygon shape to the body
7326
+ * @param {number} [diameter]
7327
+ * @param {number} [sides]
7328
+ * @param {number} [density]
7329
+ * @param {number} [friction]
7330
+ * @param {number} [restitution]
7331
+ * @param {boolean} [isSensor] */
7332
+ addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
7333
+ {
7334
+ const points = [];
7335
+ const radius = diameter/2;
7336
+ for (let i=sides; i--;)
7337
+ points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
7338
+ return this.addPoly(points, density, friction, restitution, isSensor);
7339
+ }
7340
+
7341
+ /** Add a random polygon shape to the body
7342
+ * @param {number} [diameter]
7343
+ * @param {number} [density]
7344
+ * @param {number} [friction]
7345
+ * @param {number} [restitution]
7346
+ * @param {boolean} [isSensor] */
7347
+ addRandomPoly(diameter=1, density, friction, restitution, isSensor)
7348
+ {
7349
+ const sides = randInt(3, 9);
7350
+ const points = [];
7351
+ const radius = diameter/2;
7352
+ for (let i=sides; i--;)
7353
+ points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2));
7354
+ return this.addPoly(points, density, friction, restitution, isSensor);
7355
+ }
7356
+
7357
+ /** Add a circle shape to the body
7358
+ * @param {number} [diameter]
7359
+ * @param {Vector2} [offset]
7360
+ * @param {number} [density]
7361
+ * @param {number} [friction]
7362
+ * @param {number} [restitution]
7363
+ * @param {boolean} [isSensor] */
7364
+ addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
7365
+ {
7366
+ const shape = new box2d.instance.b2CircleShape();
7367
+ shape.set_m_p(box2d.vec2dTo(offset));
7368
+ shape.set_m_radius(diameter/2);
7369
+ return this.addShape(shape, density, friction, restitution, isSensor);
7370
+ }
7371
+
7372
+ /** Add an edge shape to the body
7373
+ * @param {Vector2} point1
7374
+ * @param {Vector2} point2
7375
+ * @param {number} [density]
7376
+ * @param {number} [friction]
7377
+ * @param {number} [restitution]
7378
+ * @param {boolean} [isSensor] */
7379
+ addEdge(point1, point2, density, friction, restitution, isSensor)
7380
+ {
7381
+ const shape = new box2d.instance.b2EdgeShape();
7382
+ shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
7383
+ return this.addShape(shape, density, friction, restitution, isSensor);
7384
+ }
7385
+
7386
+ /** Add an edge loop to the body, an edge loop connects the end points
7387
+ * @param {Array<Vector2>} points
7388
+ * @param {number} [density]
7389
+ * @param {number} [friction]
7390
+ * @param {number} [restitution]
7391
+ * @param {boolean} [isSensor] */
7392
+ addEdgeLoop(points, density, friction, restitution, isSensor)
7393
+ {
7394
+ const fixtures = [];
7395
+ const getPoint = i=> points[mod(i,points.length)];
7396
+ for (let i=0; i<points.length; ++i)
7397
+ {
7398
+ const shape = new box2d.instance.b2EdgeShape();
7399
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
7400
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
7401
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
7402
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
7403
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
7404
+ fixtures.push(f);
7405
+ }
7406
+ return fixtures;
7407
+ }
7408
+
7409
+ /** Add an edge list to the body
7410
+ * @param {Array<Vector2>} points
7411
+ * @param {number} [density]
7412
+ * @param {number} [friction]
7413
+ * @param {number} [restitution]
7414
+ * @param {boolean} [isSensor] */
7415
+ addEdgeList(points, density, friction, restitution, isSensor)
7416
+ {
7417
+ const fixtures = [];
7418
+ for (let i=0; i<points.length-1; ++i)
7419
+ {
7420
+ const shape = new box2d.instance.b2EdgeShape();
7421
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
7422
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
7423
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
7424
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
7425
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
7426
+ fixtures.push(f);
7427
+ }
7428
+ return fixtures;
7429
+ }
7430
+
7431
+ ///////////////////////////////////////////////////////////////////////////////
7432
+ // physics get functions
7433
+
7434
+ /** Gets the center of mass
7435
+ * @return {Vector2} */
7436
+ getCenterOfMass() { return box2d.vec2From(this.body.GetWorldCenter()); }
7437
+
7438
+ /** Gets the linear velocity
7439
+ * @return {Vector2} */
7440
+ getLinearVelocity() { return box2d.vec2From(this.body.GetLinearVelocity()); }
7441
+
7442
+ /** Gets the angular velocity
7443
+ * @return {Vector2} */
7444
+ getAngularVelocity() { return this.body.GetAngularVelocity(); }
7445
+
7446
+ /** Gets the mass
7447
+ * @return {number} */
7448
+ getMass() { return this.body.GetMass(); }
7449
+
7450
+ /** Gets the rotational inertia
7451
+ * @return {number} */
7452
+ getInertia() { return this.body.GetInertia(); }
7453
+
7454
+ /** Check if this object is awake
7455
+ * @return {boolean} */
7456
+ getIsAwake() { return this.body.IsAwake(); }
7457
+
7458
+ /** Gets the physics body type
7459
+ * @return {number} */
7460
+ getBodyType() { return this.body.GetType(); }
7461
+
7462
+ ///////////////////////////////////////////////////////////////////////////////
7463
+ // physics set functions
7464
+
7465
+ /** Sets the position and angle
7466
+ * @param {Vector2} pos
7467
+ * @param {number} angle */
7468
+ setTransform(pos, angle)
7469
+ {
7470
+ this.pos = pos;
7471
+ this.angle = angle;
7472
+ this.body.SetTransform(box2d.vec2dTo(pos), angle);
7473
+ }
7474
+
7475
+ /** Sets the position
7476
+ * @param {Vector2} pos */
7477
+ setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
7478
+
7479
+ /** Sets the angle
7480
+ * @param {number} angle */
7481
+ setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
7482
+
7483
+ /** Sets the linear velocity
7484
+ * @param {Vector2} velocity */
7485
+ setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
7486
+
7487
+ /** Sets the angular velocity
7488
+ * @param {number} angularVelocity */
7489
+ setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
7490
+
7491
+ /** Sets the linear damping
7492
+ * @param {number} damping */
7493
+ setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
7494
+
7495
+ /** Sets the angular damping
7496
+ * @param {number} damping */
7497
+ setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
7498
+
7499
+ /** Sets the gravity scale
7500
+ * @param {number} [scale] */
7501
+ setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
7502
+
7503
+ /** Should this body be treated like a bullet for continuous collision detection?
7504
+ * @param {boolean} [isBullet] */
7505
+ setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
7506
+
7507
+ /** Set the sleep state of the body
7508
+ * @param {boolean} [isAwake] */
7509
+ setAwake(isAwake=true) { this.body.SetAwake(isAwake); }
7510
+
7511
+ /** Set the physics body type
7512
+ * @param {number} type */
7513
+ setBodyType(type) { this.body.SetType(type); }
7514
+
7515
+ /** Set whether the body is allowed to sleep
7516
+ * @param {boolean} [isAllowed] */
7517
+ setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
7518
+
7519
+ /** Set whether the body can rotate
7520
+ * @param {boolean} [isFixed] */
7521
+ setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
7522
+
7523
+ /** Set the center of mass of the body
7524
+ * @param {Vector2} center */
7525
+ setCenterOfMass(center) { this.setMassData(center) }
7526
+
7527
+ /** Set the mass of the body
7528
+ * @param {number} mass */
7529
+ setMass(mass) { this.setMassData(undefined, mass) }
7530
+
7531
+ /** Set the moment of inertia of the body
7532
+ * @param {number} momentOfInertia */
7533
+ setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
7534
+
7535
+ /** Reset the mass, center of mass, and moment */
7536
+ resetMassData() { this.body.ResetMassData(); }
7537
+
7538
+ /** Set the mass data of the body
7539
+ * @param {Vector2} [localCenter]
7540
+ * @param {number} [mass]
7541
+ * @param {number} [momentOfInertia] */
7542
+ setMassData(localCenter, mass, momentOfInertia)
7543
+ {
7544
+ const data = new box2d.instance.b2MassData();
7545
+ this.body.GetMassData(data);
7546
+ localCenter && data.set_center(box2d.vec2dTo(localCenter));
7547
+ mass && data.set_mass(mass);
7548
+ momentOfInertia && data.set_I(momentOfInertia);
7549
+ this.body.SetMassData(data);
7550
+ }
7551
+
7552
+ /** Set the collision filter data for this body
7553
+ * @param {number} [categoryBits]
7554
+ * @param {number} [ignoreCategoryBits]
7555
+ * @param {number} [groupIndex] */
7556
+ setFilterData(categoryBits=0, ignoreCategoryBits=0, groupIndex=0)
7557
+ {
7558
+ this.getFixtureList().forEach(fixture=>
7559
+ {
7560
+ const filter = fixture.GetFilterData();
7561
+ filter.set_categoryBits(categoryBits);
7562
+ filter.set_maskBits(0xffff & ~ignoreCategoryBits);
7563
+ filter.set_groupIndex(groupIndex);
7564
+ });
7565
+ }
7566
+
7567
+ /** Set if this body is a sensor
7568
+ * @param {boolean} [isSensor] */
7569
+ setSensor(isSensor=true)
7570
+ { this.getFixtureList().forEach(f=>f.SetSensor(isSensor)); }
7571
+
7572
+ ///////////////////////////////////////////////////////////////////////////////
7573
+ // physics force and torque functions
7574
+
7575
+ /** Apply force to this object
7576
+ * @param {Vector2} force
7577
+ * @param {Vector2} [pos] */
7578
+ applyForce(force, pos)
7579
+ {
7580
+ pos ||= this.getCenterOfMass();
7581
+ this.setAwake();
7582
+ this.body.ApplyForce(box2d.vec2dTo(force), box2d.vec2dTo(pos));
7583
+ }
7584
+
7585
+ /** Apply acceleration to this object
7586
+ * @param {Vector2} acceleration
7587
+ * @param {Vector2} [pos] */
7588
+ applyAcceleration(acceleration, pos)
7589
+ {
7590
+ pos ||= this.getCenterOfMass();
7591
+ this.setAwake();
7592
+ this.body.ApplyLinearImpulse(box2d.vec2dTo(acceleration), box2d.vec2dTo(pos));
7593
+ }
7594
+
7595
+ /** Apply torque to this object
7596
+ * @param {number} torque */
7597
+ applyTorque(torque)
7598
+ {
7599
+ this.setAwake();
7600
+ this.body.ApplyTorque(torque);
7601
+ }
7602
+
7603
+ /** Apply angular acceleration to this object
7604
+ * @param {number} acceleration */
7605
+ applyAngularAcceleration(acceleration)
7606
+ {
7607
+ this.setAwake();
7608
+ this.body.ApplyAngularImpulse(acceleration);
7609
+ }
7610
+
7611
+ ///////////////////////////////////////////////////////////////////////////////
7612
+ // lists of fixtures and joints
7613
+
7614
+ /** Check if this object has any fixtures
7615
+ * @return {boolean} */
7616
+ hasFixtures() { return !box2d.isNull(this.body.GetFixtureList()); }
7617
+
7618
+ /** Get list of fixtures for this object
7619
+ * @return {Array<Object>} */
7620
+ getFixtureList()
7621
+ {
7622
+ const fixtures = [];
7623
+ for (let fixture=this.body.GetFixtureList(); !box2d.isNull(fixture); )
7624
+ {
7625
+ fixtures.push(fixture);
7626
+ fixture = fixture.GetNext();
7627
+ }
7628
+ return fixtures;
7629
+ }
7630
+
7631
+ /** Check if this object has any joints
7632
+ * @return {boolean} */
7633
+ hasJoints() { return !box2d.isNull(this.body.GetJointList()); }
7634
+
7635
+ /** Get list of joints for this object
7636
+ * @return {Array<Object>} */
7637
+ getJointList()
7638
+ {
7639
+ const joints = [];
7640
+ for (let joint=this.body.GetJointList(); !box2d.isNull(joint); )
7641
+ {
7642
+ joints.push(joint);
7643
+ joint = joint.get_next();
7644
+ }
7645
+ return joints;
7646
+ }
7647
+ }
7648
+
7649
+ ///////////////////////////////////////////////////////////////////////////////
7650
+ /**
7651
+ * Box2D Raycast Result
7652
+ * - Holds results from a box2d raycast queries
7653
+ * - Automatically created by box2d raycast functions
7654
+ */
7655
+ class Box2dRaycastResult
7656
+ {
7657
+ /** Create a raycast result
7658
+ * @param {Object} fixture
7659
+ * @param {Vector2} point
7660
+ * @param {Vector2} normal
7661
+ * @param {number} fraction */
7662
+ constructor(fixture, point, normal, fraction)
7663
+ {
7664
+ /** @property {Box2dObject} - The box2d object */
7665
+ this.object = fixture.GetBody().object;
7666
+ /** @property {Object} - The fixture that was hit */
7667
+ this.fixture = fixture;
7668
+ /** @property {Vector2} - The hit point */
7669
+ this.point = point;
7670
+ /** @property {Vector2} - The hit normal */
7671
+ this.normal = normal;
7672
+ /** @property {number} - Distance fraction at the point of intersection */
7673
+ this.fraction = fraction;
7674
+ }
7675
+ }
7676
+
7677
+ ///////////////////////////////////////////////////////////////////////////////
7678
+ /**
7679
+ * Box2D Joint
7680
+ * - Base class for Box2D joints
7681
+ * - A joint is used to connect objects together
7682
+ */
7683
+ class Box2dJoint
7684
+ {
7685
+ /** Create a box2d joint, the base class is not intended to be used directly
7686
+ * @param {Object} jointDef */
7687
+ constructor(jointDef)
7688
+ {
7689
+ this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
7690
+ }
7691
+
7692
+ /** Destroy this joint */
7693
+ destroy() { box2d.world.DestroyJoint(this.box2dJoint); this.box2dJoint = 0; }
7694
+
7695
+ /** Get the first object attached to this joint
7696
+ * @return {Box2dObject} */
7697
+ getObjectA() { return this.box2dJoint.GetBodyA().object; }
7698
+
7699
+ /** Get the second object attached to this joint
7700
+ * @return {Box2dObject} */
7701
+ getObjectB() { return this.box2dJoint.GetBodyB().object; }
7702
+
7703
+ /** Get the first anchor for this joint in world coordinates
7704
+ * @return {Vector2} */
7705
+ getAnchorA() { return box2d.vec2From(this.box2dJoint.GetAnchorA());}
7706
+
7707
+ /** Get the second anchor for this joint in world coordinates
7708
+ * @return {Vector2} */
7709
+ getAnchorB() { return box2d.vec2From(this.box2dJoint.GetAnchorB());}
7710
+
7711
+ /** Get the reaction force on bodyB at the joint anchor given a time step
7712
+ * @param {number} time
7713
+ * @return {Vector2} */
7714
+ getReactionForce(time) { return box2d.vec2From(this.box2dJoint.GetReactionForce(1/time));}
7715
+
7716
+ /** Get the reaction torque on bodyB in N*m given a time step
7717
+ * @param {number} time
7718
+ * @return {number} */
7719
+ getReactionTorque(time) { return this.box2dJoint.GetReactionTorque(1/time);}
7720
+
7721
+ /** Check if the connected bodies should collide
7722
+ * @return {boolean} */
7723
+ getCollideConnected() { return this.box2dJoint.getCollideConnected();}
7724
+
7725
+ /** Check if either connected body is active
7726
+ * @return {boolean} */
7727
+ isActive() { return this.box2dJoint.IsActive();}
7728
+ }
7729
+
7730
+ ///////////////////////////////////////////////////////////////////////////////
7731
+ /**
7732
+ * Box2D Target Joint, also known as a mouse joint
7733
+ * - Used to make a point on a object track a specific world point target
7734
+ * - This a soft constraint with a max force
7735
+ * - This allows the constraint to stretch and without applying huge forces
7736
+ * @extends Box2dJoint
7737
+ */
7738
+ class Box2dTargetJoint extends Box2dJoint
7739
+ {
7740
+ /** Create a target joint
7741
+ * @param {Box2dObject} object
7742
+ * @param {Box2dObject} fixedObject
7743
+ * @param {Vector2} worldPos */
7744
+ constructor(object, fixedObject, worldPos)
7745
+ {
7746
+ object.setAwake();
7747
+ const jointDef = new box2d.instance.b2MouseJointDef();
7748
+ jointDef.set_bodyA(fixedObject.body);
7749
+ jointDef.set_bodyB(object.body);
7750
+ jointDef.set_target(box2d.vec2dTo(worldPos));
7751
+ jointDef.set_maxForce(2e3 * object.getMass());
7752
+ super(jointDef);
7753
+ }
7754
+
7755
+ /** Set the target point in world coordinates
7756
+ * @param {Vector2} pos */
7757
+ setTarget(pos) { this.box2dJoint.SetTarget(box2d.vec2dTo(pos)); }
7758
+
7759
+ /** Get the target point in world coordinates
7760
+ * @return {Vector2} */
7761
+ getTarget(){ return box2d.vec2From(this.box2dJoint.GetTarget()); }
7762
+
7763
+ /** Sets the maximum force in Newtons
7764
+ * @param {number} force */
7765
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
7766
+
7767
+ /** Gets the maximum force in Newtons
7768
+ * @return {number} */
7769
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
7770
+
7771
+ /** Sets the joint frequency in Hertz
7772
+ * @param {number} hz */
7773
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7774
+
7775
+ /** Gets the joint frequency in Hertz
7776
+ * @return {number} */
7777
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7778
+ }
7779
+
7780
+ ///////////////////////////////////////////////////////////////////////////////
7781
+ /**
7782
+ * Box2D Distance Joint
7783
+ * - Constrains two points on two objects to remain at a fixed distance
7784
+ * - You can view this as a massless, rigid rod
7785
+ * @extends Box2dJoint
7786
+ */
7787
+ class Box2dDistanceJoint extends Box2dJoint
7788
+ {
7789
+ /** Create a distance joint
7790
+ * @param {Box2dObject} objectA
7791
+ * @param {Box2dObject} objectB
7792
+ * @param {Vector2} anchorA
7793
+ * @param {Vector2} anchorB
7794
+ * @param {boolean} [collide] */
7795
+ constructor(objectA, objectB, anchorA, anchorB, collide=false)
7796
+ {
7797
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7798
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7799
+ const localAnchorA = objectA.worldToLocal(anchorA);
7800
+ const localAnchorB = objectB.worldToLocal(anchorB);
7801
+ const jointDef = new box2d.instance.b2DistanceJointDef();
7802
+ jointDef.set_bodyA(objectA.body);
7803
+ jointDef.set_bodyB(objectB.body);
7804
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7805
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7806
+ jointDef.set_length(anchorA.distance(anchorB));
7807
+ jointDef.set_collideConnected(collide);
7808
+ super(jointDef);
7809
+ }
7810
+
7811
+ /** Get the local anchor point relative to objectA's origin
7812
+ * @return {Vector2} */
7813
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7814
+
7815
+ /** Get the local anchor point relative to objectB's origin
7816
+ * @return {Vector2} */
7817
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7818
+
7819
+ /** Set the length of the joint
7820
+ * @param {number} length */
7821
+ setLength(length) { this.box2dJoint.SetLength(length); }
7822
+
7823
+ /** Get the length of the joint
7824
+ * @return {number} */
7825
+ getLength() { return this.box2dJoint.GetLength(); }
7826
+
7827
+ /** Set the frequency in Hertz
7828
+ * @param {number} hz */
7829
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7830
+
7831
+ /** Get the frequency in Hertz
7832
+ * @return {number} */
7833
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7834
+
7835
+ /** Set the damping ratio
7836
+ * @param {number} ratio */
7837
+ setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); }
7838
+
7839
+ /** Get the damping ratio
7840
+ * @return {number} */
7841
+ getDampingRatio() { return this.box2dJoint.GetDampingRatio(); }
7842
+ }
7843
+
7844
+ ///////////////////////////////////////////////////////////////////////////////
7845
+ /**
7846
+ * Box2D Pin Joint
7847
+ * - Pins two objects together at a point
7848
+ * @extends Box2dDistanceJoint
7849
+ */
7850
+ class Box2dPinJoint extends Box2dDistanceJoint
7851
+ {
7852
+ /** Create a pin joint
7853
+ * @param {Box2dObject} objectA
7854
+ * @param {Box2dObject} objectB
7855
+ * @param {Vector2} [pos]
7856
+ * @param {boolean} [collide] */
7857
+ constructor(objectA, objectB, pos=objectA.pos, collide=false)
7858
+ {
7859
+ super(objectA, objectB, undefined, pos, collide);
7860
+ }
7861
+ }
7862
+
7863
+ ///////////////////////////////////////////////////////////////////////////////
7864
+ /**
7865
+ * Box2D Rope Joint
7866
+ * - Enforces a maximum distance between two points on two objects
7867
+ * @extends Box2dJoint
7868
+ */
7869
+ class Box2dRopeJoint extends Box2dJoint
7870
+ {
7871
+ /** Create a rope joint
7872
+ * @param {Box2dObject} objectA
7873
+ * @param {Box2dObject} objectB
7874
+ * @param {Vector2} anchorA
7875
+ * @param {Vector2} anchorB
7876
+ * @param {number} extraLength
7877
+ * @param {boolean} [collide] */
7878
+ constructor(objectA, objectB, anchorA, anchorB, extraLength=0, collide=false)
7879
+ {
7880
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7881
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7882
+ const localAnchorA = objectA.worldToLocal(anchorA);
7883
+ const localAnchorB = objectB.worldToLocal(anchorB);
7884
+ const jointDef = new box2d.instance.b2RopeJointDef();
7885
+ jointDef.set_bodyA(objectA.body);
7886
+ jointDef.set_bodyB(objectB.body);
7887
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7888
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7889
+ jointDef.set_maxLength(anchorA.distance(anchorB)+extraLength);
7890
+ jointDef.set_collideConnected(collide);
7891
+ super(jointDef);
7892
+ }
7893
+
7894
+ /** Get the local anchor point relative to objectA's origin
7895
+ * @return {Vector2} */
7896
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7897
+
7898
+ /** Get the local anchor point relative to objectB's origin
7899
+ * @return {Vector2} */
7900
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7901
+
7902
+ /** Set the max length of the joint
7903
+ * @param {number} length */
7904
+ setMaxLength(length) { this.box2dJoint.SetMaxLength(length); }
7905
+
7906
+ /** Get the max length of the joint
7907
+ * @return {number} */
7908
+ getMaxLength() { return this.box2dJoint.GetMaxLength(); }
7909
+ }
7910
+
7911
+ ///////////////////////////////////////////////////////////////////////////////
7912
+ /**
7913
+ * Box2D Revolute Joint
7914
+ * - Constrains two objects to share a point while they are free to rotate around the point
7915
+ * - The relative rotation about the shared point is the joint angle
7916
+ * - You can limit the relative rotation with a joint limit
7917
+ * - You can use a motor to drive the relative rotation about the shared point
7918
+ * - A maximum motor torque is provided so that infinite forces are not generated
7919
+ * @extends Box2dJoint
7920
+ */
7921
+ class Box2dRevoluteJoint extends Box2dJoint
7922
+ {
7923
+ /** Create a revolute joint
7924
+ * @param {Box2dObject} objectA
7925
+ * @param {Box2dObject} objectB
7926
+ * @param {Vector2} anchor
7927
+ * @param {boolean} [collide] */
7928
+ constructor(objectA, objectB, anchor, collide=false)
7929
+ {
7930
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7931
+ const localAnchorA = objectA.worldToLocal(anchor);
7932
+ const localAnchorB = objectB.worldToLocal(anchor);
7933
+ const jointDef = new box2d.instance.b2RevoluteJointDef();
7934
+ jointDef.set_bodyA(objectA.body);
7935
+ jointDef.set_bodyB(objectB.body);
7936
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7937
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7938
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7939
+ jointDef.set_collideConnected(collide);
7940
+ super(jointDef);
7941
+ }
7942
+
7943
+ /** Get the local anchor point relative to objectA's origin
7944
+ * @return {Vector2} */
7945
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7946
+
7947
+ /** Get the local anchor point relative to objectB's origin
7948
+ * @return {Vector2} */
7949
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7950
+
7951
+ /** Get the reference angle, objectB angle minus objectA angle in the reference state
7952
+ * @return {number} */
7953
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7954
+
7955
+ /** Get the current joint angle
7956
+ * @return {number} */
7957
+ getJointAngle() { return this.box2dJoint.GetJointAngle(); }
7958
+
7959
+ /** Get the current joint angle speed in radians per second
7960
+ * @return {number} */
7961
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7962
+
7963
+ /** Is the joint limit enabled?
7964
+ * @return {boolean} */
7965
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
7966
+
7967
+ /** Enable/disable the joint limit
7968
+ * @param {boolean} [enable] */
7969
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
7970
+
7971
+ /** Get the lower joint limit
7972
+ * @return {number} */
7973
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
7974
+
7975
+ /** Get the upper joint limit
7976
+ * @return {number} */
7977
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
7978
+
7979
+ /** Set the joint limits
7980
+ * @param {number} min
7981
+ * @param {number} max */
7982
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
7983
+
7984
+ /** Is the joint motor enabled?
7985
+ * @return {boolean} */
7986
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7987
+
7988
+ /** Enable/disable the joint motor
7989
+ * @param {boolean} [enable] */
7990
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7991
+
7992
+ /** Set the motor speed
7993
+ * @param {number} speed */
7994
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7995
+
7996
+ /** Get the motor speed
7997
+ * @return {number} */
7998
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7999
+
8000
+ /** Set the motor torque
8001
+ * @param {number} torque */
8002
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
8003
+
8004
+ /** Get the max motor torque
8005
+ * @return {number} */
8006
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
8007
+
8008
+ /** Get the motor torque given a time step
8009
+ * @param {number} time
8010
+ * @return {number} */
8011
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
8012
+ }
8013
+
8014
+ ///////////////////////////////////////////////////////////////////////////////
8015
+ /**
8016
+ * Box2D Gear Joint
8017
+ * - A gear joint is used to connect two joints together
8018
+ * - Either joint can be a revolute or prismatic joint
8019
+ * - You specify a gear ratio to bind the motions together
8020
+ * @extends Box2dJoint
8021
+ */
8022
+ class Box2dGearJoint extends Box2dJoint
8023
+ {
8024
+ /** Create a gear joint
8025
+ * @param {Box2dObject} objectA
8026
+ * @param {Box2dObject} objectB
8027
+ * @param {Box2dJoint} joint1
8028
+ * @param {Box2dJoint} joint2
8029
+ * @param {ratio} [ratio] */
8030
+ constructor(objectA, objectB, joint1, joint2, ratio=1)
8031
+ {
8032
+ const jointDef = new box2d.instance.b2GearJointDef();
8033
+ jointDef.set_bodyA(objectA.body);
8034
+ jointDef.set_bodyB(objectB.body);
8035
+ jointDef.set_joint1(joint1.box2dJoint);
8036
+ jointDef.set_joint2(joint2.box2dJoint);
8037
+ jointDef.set_ratio(ratio);
8038
+ super(jointDef);
8039
+
8040
+ this.joint1 = joint1;
8041
+ this.joint2 = joint2;
8042
+ }
8043
+
8044
+ /** Get the first joint
8045
+ * @return {Box2dJoint} */
8046
+ getJoint1() { return this.joint1; }
8047
+
8048
+ /** Get the second joint
8049
+ * @return {Box2dJoint} */
8050
+ getJoint2() { return this.joint2; }
8051
+
8052
+ /** Set the gear ratio
8053
+ * @param {number} ratio */
8054
+ setRatio(ratio) { return this.box2dJoint.SetRatio(ratio); }
8055
+
8056
+ /** Get the gear ratio
8057
+ * @return {number} */
8058
+ getRatio() { return this.box2dJoint.GetRatio(); }
8059
+ }
8060
+
8061
+ ///////////////////////////////////////////////////////////////////////////////
8062
+ /**
8063
+ * Box2D Prismatic Joint
8064
+ * - Provides one degree of freedom: translation along an axis fixed in objectA
8065
+ * - Relative rotation is prevented
8066
+ * - You can use a joint limit to restrict the range of motion
8067
+ * - You can use a joint motor to drive the motion or to model joint friction
8068
+ * @extends Box2dJoint
8069
+ */
8070
+ class Box2dPrismaticJoint extends Box2dJoint
8071
+ {
8072
+ /** Create a prismatic joint
8073
+ * @param {Box2dObject} objectA
8074
+ * @param {Box2dObject} objectB
8075
+ * @param {Vector2} anchor
8076
+ * @param {Vector2} worldAxis
8077
+ * @param {boolean} [collide] */
8078
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
8079
+ {
8080
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
8081
+ const localAnchorA = objectA.worldToLocal(anchor);
8082
+ const localAnchorB = objectB.worldToLocal(anchor);
8083
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
8084
+ const jointDef = new box2d.instance.b2PrismaticJointDef();
8085
+ jointDef.set_bodyA(objectA.body);
8086
+ jointDef.set_bodyB(objectB.body);
8087
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8088
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8089
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
8090
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
8091
+ jointDef.set_collideConnected(collide);
8092
+ super(jointDef);
8093
+ }
8094
+
8095
+ /** Get the local anchor point relative to objectA's origin
8096
+ * @return {Vector2} */
8097
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
8098
+
8099
+ /** Get the local anchor point relative to objectB's origin
8100
+ * @return {Vector2} */
8101
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
8102
+
8103
+ /** Get the local joint axis relative to bodyA
8104
+ * @return {Vector2} */
8105
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
8106
+
8107
+ /** Get the reference angle
8108
+ * @return {number} */
8109
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
8110
+
8111
+ /** Get the current joint translation
8112
+ * @return {number} */
8113
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
8114
+
8115
+ /** Get the current joint translation speed
8116
+ * @return {number} */
8117
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
8118
+
8119
+ /** Is the joint limit enabled?
8120
+ * @return {boolean} */
8121
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
8122
+
8123
+ /** Enable/disable the joint limit
8124
+ * @param {boolean} [enable] */
8125
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
8126
+
8127
+ /** Get the lower joint limit
8128
+ * @return {number} */
8129
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
8130
+
8131
+ /** Get the upper joint limit
8132
+ * @return {number} */
8133
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
8134
+
8135
+ /** Set the joint limits
8136
+ * @param {number} min
8137
+ * @param {number} max */
8138
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
8139
+
8140
+ /** Is the motor enabled?
8141
+ * @return {boolean} */
8142
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
8143
+
8144
+ /** Enable/disable the joint motor
8145
+ * @param {boolean} [enable] */
8146
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
8147
+
8148
+ /** Set the motor speed
8149
+ * @param {number} speed */
8150
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
8151
+
8152
+ /** Get the motor speed
8153
+ * @return {number} */
8154
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
8155
+
8156
+ /** Set the maximum motor force
8157
+ * @param {number} force */
8158
+ setMaxMotorForce(force) { return this.box2dJoint.SetMaxMotorForce(force); }
8159
+
8160
+ /** Get the maximum motor force
8161
+ * @return {number} */
8162
+ getMaxMotorForce() { return this.box2dJoint.GetMaxMotorForce(); }
8163
+
8164
+ /** Get the motor force given a time step
8165
+ * @param {number} time
8166
+ * @return {number} */
8167
+ getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
8168
+ }
8169
+
8170
+ ///////////////////////////////////////////////////////////////////////////////
8171
+ /**
8172
+ * Box2D Wheel Joint
8173
+ * - Provides two degrees of freedom: translation along an axis fixed in objectA and rotation
8174
+ * - You can use a joint limit to restrict the range of motion
8175
+ * - You can use a joint motor to drive the motion or to model joint friction
8176
+ * - This joint is designed for vehicle suspensions
8177
+ * @extends Box2dJoint
8178
+ */
8179
+ class Box2dWheelJoint extends Box2dJoint
8180
+ {
8181
+ /** Create a wheel joint
8182
+ * @param {Box2dObject} objectA
8183
+ * @param {Box2dObject} objectB
8184
+ * @param {Vector2} anchor
8185
+ * @param {Vector2} worldAxis
8186
+ * @param {boolean} [collide] */
8187
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
8188
+ {
8189
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
8190
+ const localAnchorA = objectA.worldToLocal(anchor);
8191
+ const localAnchorB = objectB.worldToLocal(anchor);
8192
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
8193
+ const jointDef = new box2d.instance.b2WheelJointDef();
8194
+ jointDef.set_bodyA(objectA.body);
8195
+ jointDef.set_bodyB(objectB.body);
8196
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8197
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8198
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
8199
+ jointDef.set_collideConnected(collide);
8200
+ super(jointDef);
8201
+ }
8202
+
8203
+ /** Get the local anchor point relative to objectA's origin
8204
+ * @return {Vector2} */
8205
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
8206
+
8207
+ /** Get the local anchor point relative to objectB's origin
8208
+ * @return {Vector2} */
8209
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
8210
+
8211
+ /** Get the local joint axis relative to bodyA
8212
+ * @return {Vector2} */
8213
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
8214
+
8215
+ /** Get the current joint translation
8216
+ * @return {number} */
8217
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
8218
+
8219
+ /** Get the current joint translation speed
8220
+ * @return {number} */
8221
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
8222
+
8223
+ /** Is the joint motor enabled?
8224
+ * @return {boolean} */
8225
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
8226
+
8227
+ /** Enable/disable the joint motor
8228
+ * @param {boolean} [enable] */
8229
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
8230
+
8231
+ /** Set the motor speed
8232
+ * @param {number} speed */
8233
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
8234
+
8235
+ /** Get the motor speed
8236
+ * @return {number} */
8237
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
8238
+
8239
+ /** Set the maximum motor torque
8240
+ * @param {number} torque */
8241
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
8242
+
8243
+ /** Get the max motor torque
8244
+ * @return {number} */
8245
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
8246
+
8247
+ /** Get the motor torque for a time step
8248
+ * @return {number} */
8249
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
8250
+
8251
+ /** Set the spring frequency in Hertz
8252
+ * @param {number} hz */
8253
+ setSpringFrequencyHz(hz) { return this.box2dJoint.SetSpringFrequencyHz(hz); }
8254
+
8255
+ /** Get the spring frequency in Hertz
8256
+ * @return {number} */
8257
+ getSpringFrequencyHz() { return this.box2dJoint.GetSpringFrequencyHz(); }
8258
+
8259
+ /** Set the spring damping ratio
8260
+ * @param {number} ratio */
8261
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
8262
+
8263
+ /** Get the spring damping ratio
8264
+ * @return {number} */
8265
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
8266
+ }
8267
+
8268
+ ///////////////////////////////////////////////////////////////////////////////
8269
+ /**
8270
+ * Box2D Weld Joint
8271
+ * - Glues two objects together
8272
+ * @extends Box2dJoint
8273
+ */
8274
+ class Box2dWeldJoint extends Box2dJoint
8275
+ {
8276
+ /** Create a weld joint
8277
+ * @param {Box2dObject} objectA
8278
+ * @param {Box2dObject} objectB
8279
+ * @param {Vector2} anchor
8280
+ * @param {boolean} [collide] */
8281
+ constructor(objectA, objectB, anchor, collide=false)
8282
+ {
8283
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
8284
+ const localAnchorA = objectA.worldToLocal(anchor);
8285
+ const localAnchorB = objectB.worldToLocal(anchor);
8286
+ const jointDef = new box2d.instance.b2WeldJointDef();
8287
+ jointDef.set_bodyA(objectA.body);
8288
+ jointDef.set_bodyB(objectB.body);
8289
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8290
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8291
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
8292
+ jointDef.set_collideConnected(collide);
8293
+ super(jointDef);
8294
+ }
8295
+
8296
+ /** Get the local anchor point relative to objectA's origin
8297
+ * @return {Vector2} */
8298
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
8299
+
8300
+ /** Get the local anchor point relative to objectB's origin
8301
+ * @return {Vector2} */
8302
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
8303
+
8304
+ /** Get the reference angle
8305
+ * @return {number} */
8306
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
8307
+
8308
+ /** Set the frequency in Hertz
8309
+ * @param {number} hz */
8310
+ setFrequency(hz) { return this.box2dJoint.SetFrequency(hz); }
8311
+
8312
+ /** Get the frequency in Hertz
8313
+ * @return {number} */
8314
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
8315
+
8316
+ /** Set the damping ratio
8317
+ * @param {number} ratio */
8318
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
8319
+
8320
+ /** Get the damping ratio
8321
+ * @return {number} */
8322
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
8323
+ }
8324
+
8325
+ ///////////////////////////////////////////////////////////////////////////////
8326
+ /**
8327
+ * Box2D Friction Joint
8328
+ * - Used to apply top-down friction
8329
+ * - Provides 2D translational friction and angular friction
8330
+ * @extends Box2dJoint
8331
+ */
8332
+ class Box2dFrictionJoint extends Box2dJoint
8333
+ {
8334
+ /** Create a friction joint
8335
+ * @param {Box2dObject} objectA
8336
+ * @param {Box2dObject} objectB
8337
+ * @param {Vector2} anchor
8338
+ * @param {boolean} [collide] */
8339
+ constructor(objectA, objectB, anchor, collide=false)
8340
+ {
8341
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
8342
+ const localAnchorA = objectA.worldToLocal(anchor);
8343
+ const localAnchorB = objectB.worldToLocal(anchor);
8344
+ const jointDef = new box2d.instance.b2FrictionJointDef();
8345
+ jointDef.set_bodyA(objectA.body);
8346
+ jointDef.set_bodyB(objectB.body);
8347
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8348
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8349
+ jointDef.set_collideConnected(collide);
8350
+ super(jointDef);
8351
+ }
8352
+
8353
+ /** Get the local anchor point relative to objectA's origin
8354
+ * @return {Vector2} */
8355
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
8356
+
8357
+ /** Get the local anchor point relative to objectB's origin
8358
+ * @return {Vector2} */
8359
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
8360
+
8361
+ /** Set the maximum friction force
8362
+ * @param {number} force */
8363
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
8364
+
8365
+ /** Get the maximum friction force
8366
+ * @return {number} */
8367
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
8368
+
8369
+ /** Set the maximum friction torque
8370
+ * @param {number} torque */
8371
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
8372
+
8373
+ /** Get the maximum friction torque
8374
+ * @return {number} */
8375
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
8376
+ }
8377
+
8378
+ ///////////////////////////////////////////////////////////////////////////////
8379
+ /**
8380
+ * Box2D Pulley Joint
8381
+ * - Connects to two objects and two fixed ground points
8382
+ * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
8383
+ * - The force transmitted is scaled by the ratio
8384
+ * @extends Box2dJoint
8385
+ */
8386
+ class Box2dPulleyJoint extends Box2dJoint
8387
+ {
8388
+ /** Create a pulley joint
8389
+ * @param {Box2dObject} objectA
8390
+ * @param {Box2dObject} objectB
8391
+ * @param {Vector2} groundAnchorA
8392
+ * @param {Vector2} groundAnchorB
8393
+ * @param {Vector2} anchorA
8394
+ * @param {Vector2} anchorB
8395
+ * @param {number} [ratio]
8396
+ * @param {boolean} [collide] */
8397
+ constructor(objectA, objectB, groundAnchorA, groundAnchorB, anchorA, anchorB, ratio=1, collide=false)
8398
+ {
8399
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
8400
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
8401
+ const localAnchorA = objectA.worldToLocal(anchorA);
8402
+ const localAnchorB = objectB.worldToLocal(anchorB);
8403
+ const jointDef = new box2d.instance.b2PulleyJointDef();
8404
+ jointDef.set_bodyA(objectA.body);
8405
+ jointDef.set_bodyB(objectB.body);
8406
+ jointDef.set_groundAnchorA(box2d.vec2dTo(groundAnchorA));
8407
+ jointDef.set_groundAnchorB(box2d.vec2dTo(groundAnchorB));
8408
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8409
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8410
+ jointDef.set_ratio(ratio);
8411
+ jointDef.set_lengthA(groundAnchorA.distance(anchorA));
8412
+ jointDef.set_lengthB(groundAnchorB.distance(anchorB));
8413
+ jointDef.set_collideConnected(collide);
8414
+ super(jointDef);
8415
+ }
8416
+
8417
+ /** Get the first ground anchor
8418
+ * @return {Vector2} */
8419
+ getGroundAnchorA() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorA()); }
8420
+
8421
+ /** Get the second ground anchor
8422
+ * @return {Vector2} */
8423
+ getGroundAnchorB() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorB()); }
8424
+
8425
+ /** Get the current length of the segment attached to objectA
8426
+ * @return {number} */
8427
+ getLengthA() { return this.box2dJoint.GetLengthA(); }
8428
+
8429
+ /** Get the current length of the segment attached to objectB
8430
+ * @return {number} */
8431
+ getLengthB(){ return this.box2dJoint.GetLengthB(); }
8432
+
8433
+ /** Get the pulley ratio
8434
+ * @return {number} */
8435
+ getRatio() { return this.box2dJoint.GetRatio(); }
8436
+
8437
+ /** Get the current length of the segment attached to objectA
8438
+ * @return {number} */
8439
+ getCurrentLengthA() { return this.box2dJoint.GetCurrentLengthA(); }
8440
+
8441
+ /** Get the current length of the segment attached to objectB
8442
+ * @return {number} */
8443
+ getCurrentLengthB() { return this.box2dJoint.GetCurrentLengthB(); }
8444
+ }
8445
+
8446
+ ///////////////////////////////////////////////////////////////////////////////
8447
+ /**
8448
+ * Box2D Motor Joint
8449
+ * - Controls the relative motion between two objects
8450
+ * - Typical usage is to control the movement of a object with respect to the ground
8451
+ * @extends Box2dJoint
8452
+ */
8453
+ class Box2dMotorJoint extends Box2dJoint
8454
+ {
8455
+ /** Create a motor joint
8456
+ * @param {Box2dObject} objectA
8457
+ * @param {Box2dObject} objectB */
8458
+ constructor(objectA, objectB)
8459
+ {
8460
+ const linearOffset = objectA.worldToLocal(box2d.vec2From(objectB.body.GetPosition()));
8461
+ const angularOffset = objectB.body.GetAngle() - objectA.body.GetAngle();
8462
+ const jointDef = new box2d.instance.b2MotorJointDef();
8463
+ jointDef.set_bodyA(objectA.body);
8464
+ jointDef.set_bodyB(objectB.body);
8465
+ jointDef.set_linearOffset(box2d.vec2dTo(linearOffset));
8466
+ jointDef.set_angularOffset(angularOffset);
8467
+ super(jointDef);
8468
+ }
8469
+
8470
+ /** Set the target linear offset, in frame A, in meters.
8471
+ * @param {Vector2} offset */
8472
+ setLinearOffset(offset) { this.box2dJoint.SetLinearOffset(box2d.vec2dTo(offset)); }
8473
+
8474
+ /** Get the target linear offset, in frame A, in meters.
8475
+ * @return {Vector2} */
8476
+ getLinearOffset() { return box2d.vec2From(this.box2dJoint.GetLinearOffset()); }
8477
+
8478
+ /** Set the target angular offset
8479
+ * @param {number} offset */
8480
+ setAngularOffset(offset) { this.box2dJoint.SetAngularOffset(offset); }
8481
+
8482
+ /** Get the target angular offset
8483
+ * @return {number} */
8484
+ getAngularOffset() { return this.box2dJoint.GetAngularOffset(); }
8485
+
8486
+ /** Set the maximum friction force
8487
+ * @param {number} force */
8488
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
8489
+
8490
+ /** Get the maximum friction force
8491
+ * @return {number} */
8492
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
8493
+
8494
+ /** Set the maximum torque
8495
+ * @param {number} torque */
8496
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
8497
+
8498
+ /** Get the maximum torque
8499
+ * @return {number} */
8500
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
8501
+
8502
+ /** Set the position correction factor in the range [0,1]
8503
+ * @param {number} factor */
8504
+ setCorrectionFactor(factor) { this.box2dJoint.SetCorrectionFactor(factor); }
8505
+
8506
+ /** Get the position correction factor in the range [0,1]
8507
+ * @return {number} */
8508
+ getCorrectionFactor() { return this.box2dJoint.GetCorrectionFactor(); }
8509
+ }
8510
+
8511
+ ///////////////////////////////////////////////////////////////////////////////
8512
+ /**
8513
+ * Box2D Global Object
8514
+ * - Wraps Box2d world and provides global functions
8515
+ */
8516
+ class Box2dPlugin
8517
+ {
8518
+ /** Create the global UI system object
8519
+ * @param {Object} instance */
8520
+ constructor(instance)
8521
+ {
8522
+ ASSERT(!box2d, 'Box2D already initialized');
8523
+ box2d = this;
8524
+ this.instance = instance;
8525
+ this.world = new box2d.instance.b2World();
8526
+
8527
+ /** @property {number} - Velocity iterations per update*/
8528
+ this.velocityIterations = 8;
8529
+ /** @property {number} - Position iterations per update*/
8530
+ this.positionIterations = 3;
8531
+ /** @property {number} - Static, zero mass, zero velocity, may be manually moved */
8532
+ this.bodyTypeStatic = instance.b2_staticBody;
8533
+ /** @property {number} - Kinematic, zero mass, non-zero velocity set by user, moved by solver */
8534
+ this.bodyTypeKinematic = instance.b2_kinematicBody;
8535
+ /** @property {number} - Dynamic, positive mass, non-zero velocity determined by forces, moved by solver */
8536
+ this.bodyTypeDynamic = instance.b2_dynamicBody;
8537
+
8538
+ // setup contact listener
8539
+ const listener = new box2d.instance.JSContactListener();
8540
+ listener.BeginContact = function(contactPtr)
8541
+ {
8542
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
8543
+ const fixtureA = contact.GetFixtureA();
8544
+ const fixtureB = contact.GetFixtureB();
8545
+ const objectA = fixtureA.GetBody().object;
8546
+ const objectB = fixtureB.GetBody().object;
8547
+ objectA.beginContact(objectB);
8548
+ objectB.beginContact(objectA);
8549
+ }
8550
+ listener.EndContact = function(contactPtr)
8551
+ {
8552
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
8553
+ const fixtureA = contact.GetFixtureA();
8554
+ const fixtureB = contact.GetFixtureB();
8555
+ const objectA = fixtureA.GetBody().object;
8556
+ const objectB = fixtureB.GetBody().object;
8557
+ objectA.endContact(objectB);
8558
+ objectB.endContact(objectA);
8559
+ };
8560
+ listener.PreSolve = function() {};
8561
+ listener.PostSolve = function() {};
8562
+ box2d.world.SetContactListener(listener);
8563
+ }
8564
+
8565
+ /** Step the physics world simulation
8566
+ * @param {number} [frames] */
8567
+ step(frames=1)
8568
+ {
8569
+ box2d.world.SetGravity(box2d.vec2dTo(vec2(0,gravity)));
8570
+ for (let i=frames; i--;)
8571
+ box2d.world.Step(timeDelta, this.velocityIterations, this.positionIterations);
8572
+ }
8573
+
8574
+ ///////////////////////////////////////////////////////////////////////////////
8575
+ // raycasting and querying
8576
+
8577
+ /** raycast and return a list of all the results
8578
+ * @param {Vector2} start
8579
+ * @param {Vector2} end */
8580
+ raycastAll(start, end)
8581
+ {
8582
+ const raycastCallback = new box2d.instance.JSRayCastCallback();
8583
+ raycastCallback.ReportFixture = function(fixturePointer, point, normal, fraction)
8584
+ {
8585
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8586
+ point = box2d.vec2FromPointer(point);
8587
+ normal = box2d.vec2FromPointer(normal);
8588
+ raycastResults.push(new Box2dRaycastResult(fixture, point, normal, fraction));
8589
+ return 1; // continue getting results
8590
+ };
8591
+
8592
+ const raycastResults = [];
8593
+ box2d.world.RayCast(raycastCallback, box2d.vec2dTo(start), box2d.vec2dTo(end));
8594
+ debugRaycast && debugLine(start, end, raycastResults.length ? '#f00' : '#00f', .02);
8595
+ return raycastResults;
8596
+ }
8597
+
8598
+ /** raycast and return the first result
8599
+ * @param {Vector2} start
8600
+ * @param {Vector2} end */
8601
+ raycast(start, end)
8602
+ {
8603
+ const raycastResults = box2d.raycastAll(start, end);
8604
+ if (!raycastResults.length)
8605
+ return undefined;
8606
+ return raycastResults.reduce((a,b)=>a.fraction < b.fraction ? a : b);
8607
+ }
8608
+
8609
+ /** box aabb cast and return all the objects
8610
+ * @param {Vector2} pos
8611
+ * @param {Vector2} size */
8612
+ boxCastAll(pos, size)
8613
+ {
8614
+ const queryCallback = new box2d.instance.JSQueryCallback();
8615
+ queryCallback.ReportFixture = function(fixturePointer)
8616
+ {
8617
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8618
+ const o = fixture.GetBody().object;
8619
+ if (!queryObjects.includes(o))
8620
+ queryObjects.push(o); // add if not already in list
8621
+ return true; // continue getting results
8622
+ };
8623
+
8624
+ const aabb = new box2d.instance.b2AABB();
8625
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
8626
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
8627
+
8628
+ let queryObjects = [];
8629
+ box2d.world.QueryAABB(queryCallback, aabb);
8630
+ debugRaycast && debugRect(pos, size, queryObjects.length ? '#f00' : '#00f', .02);
8631
+ return queryObjects;
8632
+ }
8633
+
8634
+ /** box aabb cast and return the first object
8635
+ * @param {Vector2} pos
8636
+ * @param {Vector2} size */
8637
+ boxCast(pos, size)
8638
+ {
8639
+ const queryCallback = new box2d.instance.JSQueryCallback();
8640
+ queryCallback.ReportFixture = function(fixturePointer)
8641
+ {
8642
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8643
+ queryObject = fixture.GetBody().object;
8644
+ return false; // stop getting results
8645
+ };
8646
+
8647
+ const aabb = new box2d.instance.b2AABB();
8648
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
8649
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
8650
+
8651
+ let queryObject;
8652
+ box2d.world.QueryAABB(queryCallback, aabb);
8653
+ debugRaycast && debugRect(pos, size, queryObject ? '#f00' : '#00f', .02);
8654
+ return queryObject;
8655
+ }
8656
+
8657
+ /** circle cast and return all the objects
8658
+ * @param {Vector2} pos
8659
+ * @param {number} diameter */
8660
+ circleCastAll(pos, diameter)
8661
+ {
8662
+ const radius2 = (diameter/2)**2;
8663
+ const results = box2d.boxCastAll(pos, vec2(diameter));
8664
+ return results.filter(o=>o.pos.distanceSquared(pos) < radius2);
8665
+ }
8666
+
8667
+ /** circle cast and return the first object
8668
+ * @param {Vector2} pos
8669
+ * @param {number} diameter */
8670
+ circleCast(pos, diameter)
8671
+ {
8672
+ const radius2 = (diameter/2)**2;
8673
+ let results = box2d.boxCastAll(pos, vec2(diameter));
8674
+
8675
+ let bestResult, bestDistance2;
8676
+ for (const result of results)
8677
+ {
8678
+ const distance2 = result.pos.distanceSquared(pos);
8679
+ if (distance2 < radius2 && (!bestResult || distance2 < bestDistance2))
8680
+ {
8681
+ bestResult = result;
8682
+ bestDistance2 = distance2;
8683
+ }
8684
+ }
8685
+ return bestResult;
8686
+ }
8687
+
8688
+ /** point cast and return the first object
8689
+ * @param {Vector2} pos
8690
+ * @param {boolean} dynamicOnly */
8691
+ pointCast(pos, dynamicOnly=true)
8692
+ {
8693
+ const queryCallback = new box2d.instance.JSQueryCallback();
8694
+ queryCallback.ReportFixture = function(fixturePointer)
8695
+ {
8696
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8697
+ if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
8698
+ return true; // continue getting results
8699
+ if (!fixture.TestPoint(box2d.vec2dTo(pos)))
8700
+ return true; // continue getting results
8701
+ queryObject = fixture.GetBody().object;
8702
+ return false; // stop getting results
8703
+ };
8704
+
8705
+ const aabb = new box2d.instance.b2AABB();
8706
+ aabb.set_lowerBound(box2d.vec2dTo(pos));
8707
+ aabb.set_upperBound(box2d.vec2dTo(pos));
8708
+
8709
+ let queryObject;
8710
+ debugRaycast && debugRect(pos, vec2(), queryObject ? '#f00' : '#00f', .02);
8711
+ box2d.world.QueryAABB(queryCallback, aabb);
8712
+ return queryObject;
8713
+ }
8714
+
8715
+ ///////////////////////////////////////////////////////////////////////////////
8716
+ // drawing
8717
+
8718
+ /** draws a fixture
8719
+ * @param {Object} fixture
8720
+ * @param {Vector2} pos
8721
+ * @param {number} angle
8722
+ * @param {Color} [color]
8723
+ * @param {Color} [outlineColor]
8724
+ * @param {number} [lineWidth]
8725
+ * @param {CanvasRenderingContext2D} [context] */
8726
+ drawFixture(fixture, pos, angle, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8727
+ {
8728
+ const shape = box2d.castObjectType(fixture.GetShape());
8729
+ switch (shape.GetType())
8730
+ {
8731
+ case box2d.instance.b2Shape.e_polygon:
8732
+ {
8733
+ let points = [];
8734
+ for (let i=shape.GetVertexCount(); i--;)
8735
+ points.push(box2d.vec2From(shape.GetVertex(i)));
8736
+ box2d.drawPoly(pos, angle, points, color, outlineColor, lineWidth, context);
8737
+ break;
8738
+ }
8739
+ case box2d.instance.b2Shape.e_circle:
8740
+ {
8741
+ const radius = shape.get_m_radius();
8742
+ box2d.drawCircle(pos, radius, color, outlineColor, lineWidth, context);
8743
+ break;
8744
+ }
8745
+ case box2d.instance.b2Shape.e_edge:
8746
+ {
8747
+ const v1 = box2d.vec2From(shape.get_m_vertex1());
8748
+ const v2 = box2d.vec2From(shape.get_m_vertex2());
8749
+ box2d.drawLine(pos, angle, v1, v2, color, lineWidth, context);
8750
+ break;
8751
+ }
8752
+ }
8753
+ }
8754
+
8755
+ /** draws a circle
8756
+ * @param {Vector2} pos
8757
+ * @param {number} radius
8758
+ * @param {Color} [color]
8759
+ * @param {Color} [outlineColor]
8760
+ * @param {number} [lineWidth]
8761
+ * @param {CanvasRenderingContext2D} [context] */
8762
+ drawCircle(pos, radius, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8763
+ {
8764
+ drawCanvas2D(pos, vec2(1), 0, 0, context=>
8765
+ {
8766
+ context.beginPath();
8767
+ context.arc(0, 0, radius, 0, 9);
8768
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8769
+ }, 0, context);
8770
+ }
8771
+
8772
+ /** draws a polygon
8773
+ * @param {Vector2} pos
8774
+ * @param {number} angle
8775
+ * @param {Array<Vector2>} points
8776
+ * @param {Color} [color]
8777
+ * @param {Color} [outlineColor]
8778
+ * @param {number} [lineWidth]
8779
+ * @param {CanvasRenderingContext2D} [context] */
8780
+ drawPoly(pos, angle, points, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8781
+ {
8782
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8783
+ {
8784
+ context.beginPath();
8785
+ points.forEach(p=>context.lineTo(p.x, p.y));
8786
+ context.closePath();
8787
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8788
+ }, 0, context);
8789
+ }
8790
+
8791
+ /** draws a line
8792
+ * @param {Vector2} pos
8793
+ * @param {number} angle
8794
+ * @param {Vector2} posA
8795
+ * @param {Vector2} posB
8796
+ * @param {Color} [color]
8797
+ * @param {number} [lineWidth]
8798
+ * @param {CanvasRenderingContext2D} [context] */
8799
+ drawLine(pos, angle, posA, posB, color=WHITE, lineWidth=.1, context=mainContext)
8800
+ {
8801
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8802
+ {
8803
+ context.beginPath();
8804
+ context.lineTo(posA.x, posA.y);
8805
+ context.lineTo(posB.x, posB.y);
8806
+ box2d.drawFillStroke(0, color, lineWidth, context);
8807
+ }, 0, context);
8808
+ }
8809
+
8810
+ /** performs a fill or stroke as a helper to the other draw functions
8811
+ * @param {Color} [color]
8812
+ * @param {Color} [outlineColor]
8813
+ * @param {number} [lineWidth]
8814
+ * @param {CanvasRenderingContext2D} [context] */
8815
+ drawFillStroke(color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8816
+ {
8817
+ if (color)
8818
+ {
8819
+ context.fillStyle = color.toString();
8820
+ context.fill();
8821
+ }
8822
+ if (outlineColor && lineWidth)
8823
+ {
8824
+ context.lineWidth = lineWidth;
8825
+ context.lineJoin = context.lineCap = 'round';
8826
+ context.strokeStyle = outlineColor.toString();
8827
+ context.stroke();
8828
+ }
8829
+ }
8830
+
8831
+ ///////////////////////////////////////////////////////////////////////////////
8832
+ // helper functions
8833
+
8834
+ /** converts a box2d vec2 to a Vector2
8835
+ * @param {Object} v */
8836
+ vec2From(v)
8837
+ {
8838
+ ASSERT(v instanceof box2d.instance.b2Vec2);
8839
+ return new Vector2(v.get_x(), v.get_y());
8840
+ }
8841
+
8842
+ /** converts a box2d vec2 pointer to a Vector2
8843
+ * @param {Object} v */
8844
+ vec2FromPointer(v)
8845
+ {
8846
+ return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
8847
+ }
8848
+
8849
+ /** converts a Vector2 to a box2 vec2
8850
+ * @param {Vector2} v */
8851
+ vec2dTo(v)
8852
+ {
8853
+ ASSERT(v instanceof Vector2);
8854
+ return new box2d.instance.b2Vec2(v.x, v.y);
8855
+ }
8856
+
8857
+ /** checks if a box2d object is null
8858
+ * @param {Object} o */
8859
+ isNull(o) { return !box2d.instance.getPointer(o); }
8860
+
8861
+ /** casts a box2d object to its correct type
8862
+ * @param {Object} o */
8863
+ castObjectType(o)
8864
+ {
8865
+ switch (o.GetType())
8866
+ {
8867
+ case box2d.instance.b2Shape.e_circle:
8868
+ return box2d.instance.castObject(o, box2d.instance.b2CircleShape);
8869
+ case box2d.instance.b2Shape.e_edge:
8870
+ return box2d.instance.castObject(o, box2d.instance.b2EdgeShape);
8871
+ case box2d.instance.b2Shape.e_polygon:
8872
+ return box2d.instance.castObject(o, box2d.instance.b2PolygonShape);
8873
+ case box2d.instance.b2Shape.e_chain:
8874
+ return box2d.instance.castObject(o, box2d.instance.b2ChainShape);
8875
+ case box2d.instance.e_revoluteJoint:
8876
+ return box2d.instance.castObject(o, box2d.instance.b2RevoluteJoint);
8877
+ case box2d.instance.e_prismaticJoint:
8878
+ return box2d.instance.castObject(o, box2d.instance.b2PrismaticJoint);
8879
+ case box2d.instance.e_distanceJoint:
8880
+ return box2d.instance.castObject(o, box2d.instance.b2DistanceJoint);
8881
+ case box2d.instance.e_pulleyJoint:
8882
+ return box2d.instance.castObject(o, box2d.instance.b2PulleyJoint);
8883
+ case box2d.instance.e_mouseJoint:
8884
+ return box2d.instance.castObject(o, box2d.instance.b2MouseJoint);
8885
+ case box2d.instance.e_gearJoint:
8886
+ return box2d.instance.castObject(o, box2d.instance.b2GearJoint);
8887
+ case box2d.instance.e_wheelJoint:
8888
+ return box2d.instance.castObject(o, box2d.instance.b2WheelJoint);
8889
+ case box2d.instance.e_weldJoint:
8890
+ return box2d.instance.castObject(o, box2d.instance.b2WeldJoint);
8891
+ case box2d.instance.e_frictionJoint:
8892
+ return box2d.instance.castObject(o, box2d.instance.b2FrictionJoint);
8893
+ case box2d.instance.e_ropeJoint:
8894
+ return box2d.instance.castObject(o, box2d.instance.b2RopeJoint);
8895
+ case box2d.instance.e_motorJoint:
8896
+ return box2d.instance.castObject(o, box2d.instance.b2MotorJoint);
8897
+ }
8898
+
8899
+ ASSERT(false, 'Unknown box2d object type');
8900
+ }
8901
+ }
8902
+
8903
+ ///////////////////////////////////////////////////////////////////////////////
8904
+ /** Box2d Init - Startup LittleJS engine with your callback functions
8905
+ * @param {Function|function():Promise} gameInit - Called once after the engine starts up
8906
+ * @param {Function} gameUpdate - Called every frame before objects are updated
8907
+ * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused
8908
+ * @param {Function} gameRender - Called before objects are rendered, for drawing the background
8909
+ * @param {Function} gameRenderPost - Called after objects are rendered, useful for drawing UI
8910
+ * @param {Array<string>} [imageSources=[]] - List of images to load
8911
+ * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default */
8912
+ function box2dEngineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources, rootElement)
8913
+ {
8914
+ Box2D().then(box2dInstance=>
8915
+ {
8916
+ // create box2d object
8917
+ new Box2dPlugin(box2dInstance);
8918
+ setupDebugDraw();
8919
+
8920
+ // start littlejs
8921
+ engineAddPlugin(box2dUpdate, box2dRender);
8922
+ engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources, rootElement);
8923
+ });
8924
+
8925
+ // hook up box2d plugin to update and render
8926
+ function box2dUpdate()
8927
+ {
8928
+ if (!paused)
8929
+ box2d.step();
8930
+ }
8931
+ function box2dRender()
8932
+ {
8933
+ if (box2dDebug || debugPhysics && debugOverlay)
8934
+ box2d.world.DrawDebugData();
8935
+ }
8936
+
8937
+ // box2d debug drawing
8938
+ function setupDebugDraw()
8939
+ {
8940
+ // setup debug draw
8941
+ const debugDraw = new box2d.instance.JSDraw();
8942
+ const box2dColor = (c)=> new Color(c.get_r(), c.get_g(), c.get_b());
8943
+ const box2dColorPointer = (c)=>
8944
+ box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
8945
+ const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
8946
+ const getPointsList = (vertices, vertexCount) =>
8947
+ {
8948
+ const points = [];
8949
+ for (let i=vertexCount; i--;)
8950
+ points.push(box2d.vec2FromPointer(vertices+i*8));
8951
+ return points;
8952
+ }
8953
+ debugDraw.DrawSegment = function(point1, point2, color)
8954
+ {
8955
+ color = getDebugColor(color);
8956
+ point1 = box2d.vec2FromPointer(point1);
8957
+ point2 = box2d.vec2FromPointer(point2);
8958
+ box2d.drawLine(vec2(), 0, point1, point2, color, undefined, overlayContext);
8959
+ };
8960
+ debugDraw.DrawPolygon = function(vertices, vertexCount, color)
8961
+ {
8962
+ color = getDebugColor(color);
8963
+ const points = getPointsList(vertices, vertexCount);
8964
+ box2d.drawPoly(vec2(), 0, points, undefined, color, undefined, overlayContext);
8965
+ };
8966
+ debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
8967
+ {
8968
+ color = getDebugColor(color);
8969
+ const points = getPointsList(vertices, vertexCount);
8970
+ box2d.drawPoly(vec2(), 0, points, color, color, undefined, overlayContext);
8971
+ };
8972
+ debugDraw.DrawCircle = function(center, radius, color)
8973
+ {
8974
+ color = getDebugColor(color);
8975
+ center = box2d.vec2FromPointer(center);
8976
+ box2d.drawCircle(center, radius, undefined, color, undefined, overlayContext);
8977
+ };
8978
+ debugDraw.DrawSolidCircle = function(center, radius, axis, color)
8979
+ {
8980
+ color = getDebugColor(color);
8981
+ center = box2d.vec2FromPointer(center);
8982
+ axis = box2d.vec2FromPointer(axis).scale(radius);
8983
+ box2d.drawCircle(center, radius, color, color, undefined, overlayContext);
8984
+ box2d.drawLine(center, 0, vec2(), axis, color, undefined, overlayContext);
8985
+ };
8986
+ debugDraw.DrawTransform = function(transform)
8987
+ {
8988
+ transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
8989
+ const pos = vec2(transform.get_p());
8990
+ const angle = -transform.get_q().GetAngle();
8991
+ const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
8992
+ const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
8993
+ box2d.drawLine(pos, angle, vec2(), p1, c1, undefined, overlayContext);
8994
+ box2d.drawLine(pos, angle, vec2(), p2, c2, undefined, overlayContext);
8995
+ }
8996
+
8997
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
8998
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_jointBit);
8999
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_aabbBit);
9000
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_pairBit);
9001
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_centerOfMassBit);
9002
+ box2d.world.SetDebugDraw(debugDraw);
9003
+ }
9004
+ }
9005
+
9006
+ /**
9007
+ * LittleJS Module Plugins Export
9008
+ */
9009
+
9010
+ 'use strict';
9011
+
9012
+ export
9013
+ {
9014
+ // Newgrounds
9015
+ newgrounds,
9016
+ NewgroundsPlugin,
9017
+ NewgroundsMedal,
9018
+
9019
+ // Post Process
9020
+ postProcess,
9021
+ PostProcessPlugin,
9022
+
9023
+ // ZzFXMusic
9024
+ ZzFXMusic,
9025
+
9026
+ // UI System
9027
+ uiSystem,
9028
+ UISystemPlugin,
9029
+ UIObject,
9030
+ UIText,
9031
+ UITile,
9032
+ UIButton,
9033
+ UICheckbox,
9034
+ UIScrollbar,
9035
+
9036
+ // Box2D Physics
9037
+ box2d,
9038
+ box2dDebug,
9039
+ box2dEngineInit,
9040
+ Box2dPlugin,
9041
+ Box2dObject,
9042
+ Box2dRaycastResult,
9043
+ Box2dJoint,
9044
+ Box2dTargetJoint,
9045
+ Box2dDistanceJoint,
9046
+ Box2dPinJoint,
9047
+ Box2dRopeJoint,
9048
+ Box2dRevoluteJoint,
9049
+ Box2dGearJoint,
9050
+ Box2dPrismaticJoint,
9051
+ Box2dWheelJoint,
9052
+ Box2dWeldJoint,
9053
+ Box2dFrictionJoint,
9054
+ Box2dPulleyJoint,
9055
+ Box2dMotorJoint,
9056
+ };
9057
+