littlejsengine 1.11.13 → 1.12.4

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 (79) hide show
  1. package/dist/littlejs.d.ts +1418 -109
  2. package/dist/littlejs.esm.js +3495 -581
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +3258 -399
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +3147 -379
  7. package/examples/box2d/game.js +73 -45
  8. package/examples/box2d/gameObjects.js +96 -92
  9. package/examples/box2d/index.html +2 -7
  10. package/examples/box2d/scenes.js +82 -92
  11. package/examples/breakout/game.js +62 -30
  12. package/examples/breakout/gameObjects.js +45 -47
  13. package/examples/breakout/index.html +1 -5
  14. package/examples/breakoutTutorial/game.js +36 -29
  15. package/examples/breakoutTutorial/index.html +1 -3
  16. package/examples/empty/game.js +5 -2
  17. package/examples/empty/index.html +1 -3
  18. package/examples/htmlMenu/game.js +25 -19
  19. package/examples/htmlMenu/index.html +5 -7
  20. package/examples/index.html +2 -3
  21. package/examples/module/game.js +32 -34
  22. package/examples/module/index.html +1 -1
  23. package/examples/particles/index.html +27 -22
  24. package/examples/platformer/game.js +71 -48
  25. package/examples/platformer/gameCharacter.js +42 -34
  26. package/examples/platformer/gameEffects.js +82 -75
  27. package/examples/platformer/gameLevel.js +67 -38
  28. package/examples/platformer/{gameLevelData.js → gameLevelData.json} +1 -11
  29. package/examples/platformer/gameObjects.js +70 -64
  30. package/examples/platformer/gamePlayer.js +12 -7
  31. package/examples/platformer/index.html +1 -9
  32. package/examples/puzzle/game.js +58 -42
  33. package/examples/puzzle/index.html +1 -3
  34. package/examples/shorts/base.html +2 -2
  35. package/examples/shorts/particles.js +1 -1
  36. package/examples/shorts/platformer.js +1 -1
  37. package/examples/shorts/tileLayer.js +5 -6
  38. package/examples/shorts/tiles.png +0 -0
  39. package/examples/starter/build.js +4 -4
  40. package/examples/starter/game.js +9 -12
  41. package/examples/starter/index.html +13 -13
  42. package/examples/stress/index.html +44 -43
  43. package/examples/typescript/build.js +17 -18
  44. package/examples/typescript/game.js +32 -34
  45. package/examples/typescript/game.ts +32 -34
  46. package/examples/typescript/index.html +1 -1
  47. package/examples/uiSystem/game.js +33 -36
  48. package/examples/uiSystem/index.html +1 -5
  49. package/package.json +3 -3
  50. package/plugins/box2d.js +1556 -640
  51. package/plugins/{Box2D_v2.3.1_min.wasm.js → box2d.wasm.js} +1 -1
  52. package/plugins/newgrounds.js +7 -8
  53. package/plugins/pluginExport.js +50 -0
  54. package/plugins/postProcess.js +94 -93
  55. package/plugins/uiSystem.js +145 -161
  56. package/plugins/zzfxm.js +163 -0
  57. package/reference.md +29 -27
  58. package/src/engine.js +15 -20
  59. package/src/engineAudio.js +74 -204
  60. package/src/engineBuild.js +29 -4
  61. package/src/engineDebug.js +105 -9
  62. package/src/engineDraw.js +27 -14
  63. package/src/engineExport.js +12 -8
  64. package/src/engineInput.js +3 -1
  65. package/src/engineObject.js +18 -14
  66. package/src/engineParticles.js +6 -1
  67. package/src/engineRelease.js +6 -1
  68. package/src/engineSettings.js +8 -8
  69. package/src/engineTileLayer.js +179 -96
  70. package/src/engineUtilities.js +5 -11
  71. package/src/engineWebGL.js +19 -7
  72. package/examples/starter/build/index.html +0 -2
  73. package/examples/starter/build/index.js +0 -1
  74. package/examples/starter/build/tiles.png +0 -0
  75. package/examples/starter/game.zip +0 -0
  76. package/examples/typescript/build/dist/littlejs.esm.js +0 -4834
  77. package/examples/typescript/build/examples/typescript/build.js +0 -24
  78. package/examples/typescript/build/examples/typescript/game.js +0 -102
  79. /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)
@@ -327,11 +334,9 @@ function debugRender()
327
334
  }
328
335
  }
329
336
 
330
- if (tileCollisionSize.x > 0 && tileCollisionSize.y > 0)
337
+ if (tileCollisionLayers.length) // show floored tile pick if there is tile collision
331
338
  drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(0,0,1,.5), 0, false);
332
339
  mainContext = saveContext;
333
-
334
- //glCopyToContext(mainContext = saveContext);
335
340
  }
336
341
 
337
342
  {
@@ -444,6 +449,7 @@ function debugRender()
444
449
  overlayContext.fillText('4: Debug Raycasts', x, y += h);
445
450
  overlayContext.fillStyle = '#fff';
446
451
  overlayContext.fillText('5: Save Screenshot', x, y += h);
452
+ overlayContext.fillText('6: Capture Video', x, y += h);
447
453
 
448
454
  let keysPressed = '';
449
455
  for(const i in inputData[0])
@@ -472,6 +478,96 @@ function debugRender()
472
478
 
473
479
  overlayContext.restore();
474
480
  }
481
+ }
482
+
483
+ ///////////////////////////////////////////////////////////////////////////////
484
+ // video capture - records video and audio at 60 fps using MediaRecorder API
485
+
486
+ // internal variables used to capture video
487
+ let debugVideoCapture, debugVideoCaptureTrack, debugVideoCaptureIcon, debugVideoCaptureTimer;
488
+
489
+ /** Check if video capture is active
490
+ * @memberof Debug */
491
+ function debugVideoCaptureIsActive() { return !!debugVideoCapture; }
492
+
493
+ /** Start capturing video
494
+ * @memberof Debug */
495
+ function debugVideoCaptureStart()
496
+ {
497
+ if (debugVideoCaptureIsActive())
498
+ return; // already recording
499
+
500
+ // captureStream passing in 0 to only capture when requestFrame() is called
501
+ const stream = mainCanvas.captureStream(0);
502
+ const chunks = [];
503
+ debugVideoCaptureTrack = stream.getVideoTracks()[0];
504
+ if (debugVideoCaptureTrack.applyConstraints)
505
+ debugVideoCaptureTrack.applyConstraints({frameRate:frameRate}); // force 60 fps
506
+ debugVideoCapture = new MediaRecorder(stream, {mimeType:'video/webm;codecs=vp8'});
507
+ debugVideoCapture.ondataavailable = (e)=> chunks.push(e.data);
508
+ debugVideoCapture.onstop = ()=>
509
+ {
510
+ const blob = new Blob(chunks, {type: 'video/webm'});
511
+ const url = URL.createObjectURL(blob);
512
+ downloadLink.download = 'capture.webm';
513
+ downloadLink.href = url;
514
+ downloadLink.click();
515
+ URL.revokeObjectURL(url);
516
+ };
517
+
518
+ if (audioMasterGain)
519
+ {
520
+ // connect to audio master gain node
521
+ const audioStreamDestination = audioContext.createMediaStreamDestination();
522
+ audioMasterGain.connect(audioStreamDestination);
523
+ for (const track of audioStreamDestination.stream.getAudioTracks())
524
+ stream.addTrack(track); // add audio tracks to capture stream
525
+ }
526
+
527
+ // start recording
528
+ console.log('Video capture started.');
529
+ debugVideoCapture.start();
530
+ debugVideoCaptureTimer = new Timer(0);
531
+
532
+ if (!debugVideoCaptureIcon)
533
+ {
534
+ // create recording icon to show it is capturing video
535
+ debugVideoCaptureIcon = document.createElement('div');
536
+ debugVideoCaptureIcon.style.position = 'absolute';
537
+ debugVideoCaptureIcon.style.padding = '9px';
538
+ debugVideoCaptureIcon.style.color = '#f00';
539
+ debugVideoCaptureIcon.style.font = '50px monospace';
540
+ document.body.appendChild(debugVideoCaptureIcon);
541
+ }
542
+ // show recording icon
543
+ debugVideoCaptureIcon.textContent = '';
544
+ debugVideoCaptureIcon.style.display = '';
545
+ }
546
+
547
+ /** Stop capturing video and save to disk
548
+ * @memberof Debug */
549
+ function debugVideoCaptureStop()
550
+ {
551
+ if (!debugVideoCaptureIsActive())
552
+ return; // not recording
553
+
554
+ // stop recording
555
+ console.log(`Video capture ended. ${debugVideoCaptureTimer.get().toFixed(2)} seconds recorded.`);
556
+ debugVideoCapture.stop();
557
+ debugVideoCapture = 0;
558
+ debugVideoCaptureIcon.style.display = 'none';
559
+ }
560
+
561
+ // update video capture, called automatically by engine
562
+ function debugVideoCaptureUpdate()
563
+ {
564
+ if (!debugVideoCaptureIsActive())
565
+ return; // not recording
566
+
567
+ // save the video frame
568
+ combineCanvases();
569
+ debugVideoCaptureTrack.requestFrame();
570
+ debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
475
571
  }
476
572
  /**
477
573
  * LittleJS Utility Classes and Functions
@@ -566,7 +662,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
566
662
  * @returns {number}
567
663
  * @memberof Utilities */
568
664
  function lerpWrap(percent, valueA, valueB, wrapSize=1)
569
- { return valueB + clamp(percent) * distanceWrap(valueA, valueB, wrapSize); }
665
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
570
666
 
571
667
  /** Returns signed wrapped distance between the two angles passed in
572
668
  * @param {number} angleA
@@ -764,23 +860,17 @@ class RandomGenerator
764
860
  ///////////////////////////////////////////////////////////////////////////////
765
861
 
766
862
  /**
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]
863
+ * Create a 2d vector, can take 1 or 2 scalar values
864
+ * @param {number} [x]
865
+ * @param {number} [y] - if y is undefined, x is used for both
770
866
  * @return {Vector2}
771
867
  * @example
772
868
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
773
- * let b = vec2(a); // copy a into b
774
869
  * a = vec2(5); // set a to (5, 5)
775
870
  * b = vec2(); // set b to (0, 0)
776
871
  * @memberof Utilities
777
872
  */
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
- }
873
+ function vec2(x=0, y) { return new Vector2(x, y == undefined? x : y); }
784
874
 
785
875
  /**
786
876
  * Check if object is a valid Vector2
@@ -1438,7 +1528,7 @@ let canvasMaxSize = vec2(1920, 1080);
1438
1528
  * @memberof Settings */
1439
1529
  let canvasFixedSize = vec2();
1440
1530
 
1441
- /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
1531
+ /** Use nearest neighbor canvas scaling for more pixelated look
1442
1532
  * - Must be set before startup to take effect
1443
1533
  * - If enabled sets css image-rendering:pixelated
1444
1534
  * @type {boolean}
@@ -1548,11 +1638,11 @@ let objectDefaultFriction = .8;
1548
1638
  * @memberof Settings */
1549
1639
  let objectMaxSpeed = 1;
1550
1640
 
1551
- /** How much gravity to apply to objects along the Y axis, negative is down
1552
- * @type {number}
1641
+ /** How much gravity to apply to objects, negative Y is down
1642
+ * @type {Vector2}
1553
1643
  * @default
1554
1644
  * @memberof Settings */
1555
- let gravity = 0;
1645
+ let gravity = vec2();
1556
1646
 
1557
1647
  /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
1558
1648
  * @type {number}
@@ -1778,8 +1868,8 @@ function setObjectDefaultFriction(friction) { objectDefaultFriction = friction;
1778
1868
  * @memberof Settings */
1779
1869
  function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
1780
1870
 
1781
- /** Set how much gravity to apply to objects along the Y axis
1782
- * @param {number} newGravity
1871
+ /** Set how much gravity to apply to objects
1872
+ * @param {Vector2} newGravity
1783
1873
  * @memberof Settings */
1784
1874
  function setGravity(newGravity) { gravity = newGravity; }
1785
1875
 
@@ -1844,8 +1934,8 @@ function setSoundEnable(enable) { soundEnable = enable; }
1844
1934
  function setSoundVolume(volume)
1845
1935
  {
1846
1936
  soundVolume = volume;
1847
- if (soundEnable && !headlessMode && audioGainNode)
1848
- audioGainNode.gain.value = volume; // update gain immediately
1937
+ if (soundEnable && !headlessMode && audioMasterGain)
1938
+ audioMasterGain.gain.value = volume; // update gain immediately
1849
1939
  }
1850
1940
 
1851
1941
  /** Set default range where sound no longer plays
@@ -2047,7 +2137,10 @@ class EngineObject
2047
2137
  this.velocity.x *= this.damping;
2048
2138
  this.velocity.y *= this.damping;
2049
2139
  if (this.mass) // don't apply gravity to static objects
2050
- this.velocity.y += gravity * this.gravityScale;
2140
+ {
2141
+ this.velocity.x += gravity.x * this.gravityScale;
2142
+ this.velocity.y += gravity.y * this.gravityScale;
2143
+ }
2051
2144
  this.pos.x += this.velocity.x;
2052
2145
  this.pos.y += this.velocity.y;
2053
2146
  this.angle += this.angleVelocity *= this.angleDamping;
@@ -2062,9 +2155,9 @@ class EngineObject
2062
2155
  if (this.groundObject)
2063
2156
  {
2064
2157
  // apply friction in local space of ground object
2065
- const groundSpeed = this.groundObject != this && this.groundObject.velocity ?
2066
- this.groundObject.velocity.x : 0;
2067
- this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
2158
+ const friction = max(this.friction, this.groundObject.friction);
2159
+ const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
2160
+ this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
2068
2161
  this.groundObject = undefined;
2069
2162
  //debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
2070
2163
  }
@@ -2106,7 +2199,7 @@ class EngineObject
2106
2199
 
2107
2200
  // check for collision
2108
2201
  const sizeBoth = this.size.add(o.size);
2109
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity; // prefer to push up if small delta
2202
+ const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity.y; // prefer to push up if small delta
2110
2203
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
2111
2204
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
2112
2205
  const elasticity = max(this.elasticity, o.elasticity);
@@ -2168,19 +2261,21 @@ class EngineObject
2168
2261
  if (this.collideTiles)
2169
2262
  {
2170
2263
  // check collision against tiles
2171
- if (tileCollisionTest(this.pos, this.size, this))
2264
+ const hitLayer = tileCollisionTest(this.pos, this.size, this)
2265
+ if (hitLayer)
2172
2266
  {
2173
2267
  // if already was stuck in collision, don't do anything
2174
2268
  // this should not happen unless something starts in collision
2175
2269
  if (!tileCollisionTest(oldPos, this.size, this))
2176
2270
  {
2177
2271
  // test which side we bounced off (or both if a corner)
2178
- const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2179
- const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2180
- if (isBlockedY || !isBlockedX)
2272
+ const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2273
+ const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2274
+ if (blockedLayerY || !blockedLayerX)
2181
2275
  {
2182
2276
  // bounce velocity
2183
- this.velocity.y *= -this.elasticity;
2277
+ const elasticity = max(this.elasticity, hitLayer.elasticity);
2278
+ this.velocity.y *= -elasticity;
2184
2279
 
2185
2280
  if (wasMovingDown)
2186
2281
  {
@@ -2189,9 +2284,8 @@ class EngineObject
2189
2284
  const epsilon = .0001;
2190
2285
  this.pos.y = (oldPos.y-this.size.y/2|0)+this.size.y/2+epsilon;
2191
2286
 
2192
- // set ground object to self for tile collision
2193
- // TODO: rework system so tile collision is its own object
2194
- this.groundObject = this;
2287
+ // set ground object for tile collision
2288
+ this.groundObject = hitLayer;
2195
2289
  }
2196
2290
  else
2197
2291
  {
@@ -2200,7 +2294,7 @@ class EngineObject
2200
2294
  this.groundObject = undefined;
2201
2295
  }
2202
2296
  }
2203
- if (isBlockedX)
2297
+ if (blockedLayerX)
2204
2298
  {
2205
2299
  // move to previous position and bounce
2206
2300
  this.pos.x = oldPos.x;
@@ -2511,6 +2605,8 @@ class TextureInfo
2511
2605
  this.image = image;
2512
2606
  /** @property {Vector2} - size of the image */
2513
2607
  this.size = vec2(image.width, image.height);
2608
+ /** @property {Vector2} - inverse of the size, cached for rendering */
2609
+ this.sizeInverse = vec2(1/image.width, 1/image.height);
2514
2610
  /** @property {WebGLTexture} - webgl texture */
2515
2611
  this.glTexture = glEnable && glCreateTexture(image);
2516
2612
  }
@@ -2556,19 +2652,19 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
2556
2652
  * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2557
2653
  * @param {number} [angle] - Angle to rotate by
2558
2654
  * @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
2655
+ * @param {Color} [additiveColor] - Additive color to be applied if any
2560
2656
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2561
2657
  * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
2562
2658
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2563
2659
  * @memberof Draw */
2564
2660
  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)
2661
+ angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2566
2662
  {
2567
2663
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2568
2664
  ASSERT(typeof tileInfo !== 'number' || !tileInfo,
2569
2665
  'this is an old style calls, to fix replace it with tile(tileIndex, tileSize)');
2570
2666
  ASSERT(isVector2(pos) && isVector2(size));
2571
- ASSERT(isColor(color) && isColor(additiveColor));
2667
+ ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)));
2572
2668
 
2573
2669
  const textureInfo = tileInfo && tileInfo.getTextureInfo();
2574
2670
  if (useWebGL)
@@ -2579,21 +2675,30 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2579
2675
  pos = screenToWorld(pos);
2580
2676
  size = size.scale(1/cameraScale);
2581
2677
  }
2582
-
2583
2678
  if (textureInfo)
2584
2679
  {
2585
2680
  // calculate uvs and render
2586
- const sizeInverse = vec2(1).divide(textureInfo.size);
2681
+ const sizeInverse = textureInfo.sizeInverse;
2587
2682
  const x = tileInfo.pos.x * sizeInverse.x;
2588
2683
  const y = tileInfo.pos.y * sizeInverse.y;
2589
2684
  const w = tileInfo.size.x * sizeInverse.x;
2590
2685
  const h = tileInfo.size.y * sizeInverse.y;
2591
- const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
2592
2686
  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());
2687
+ if (tileFixBleedScale)
2688
+ {
2689
+ const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
2690
+ const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
2691
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2692
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
2693
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2694
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2695
+ }
2696
+ else
2697
+ {
2698
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2699
+ x, y, x + w, y + h,
2700
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2701
+ }
2597
2702
  }
2598
2703
  else
2599
2704
  {
@@ -2805,16 +2910,15 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
2805
2910
  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
2911
  {
2807
2912
  context.fillStyle = color.toString();
2808
- context.lineWidth = lineWidth;
2809
2913
  context.strokeStyle = lineColor.toString();
2914
+ context.lineWidth = lineWidth;
2810
2915
  context.textAlign = textAlign;
2811
2916
  context.font = size + 'px '+ font;
2812
2917
  context.textBaseline = 'middle';
2813
2918
  context.lineJoin = 'round';
2814
2919
 
2815
- pos = pos.copy();
2816
-
2817
2920
  const lines = (text+'').split('\n');
2921
+ pos = pos.copy();
2818
2922
  pos.y -= (lines.length-1) * size/2; // center text vertically
2819
2923
  lines.forEach(line=>
2820
2924
  {
@@ -2884,7 +2988,10 @@ class FontImage
2884
2988
  {
2885
2989
  // load default font image
2886
2990
  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';
2991
+ {
2992
+ engineFontImage = new Image;
2993
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2994
+ }
2888
2995
 
2889
2996
  this.image = image || engineFontImage;
2890
2997
  this.tileSize = tileSize;
@@ -3287,7 +3394,9 @@ function gamepadsUpdate()
3287
3394
  const button = gamepad.buttons[j];
3288
3395
  const wasDown = gamepadIsDown(j,i);
3289
3396
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
3290
- isUsingGamepad ||= !i && button.pressed;
3397
+ if (!button.value || button.value > .9) // must be a full press
3398
+ if (!i && button.pressed)
3399
+ isUsingGamepad = true;
3291
3400
  }
3292
3401
 
3293
3402
  if (gamepadDirectionEmulateStick)
@@ -3515,16 +3624,15 @@ let audioContext = new AudioContext;
3515
3624
  /** Master gain node for all audio to pass through
3516
3625
  * @type {GainNode}
3517
3626
  * @memberof Audio */
3518
- let audioGainNode;
3627
+ let audioMasterGain;
3519
3628
 
3520
3629
  function audioInit()
3521
3630
  {
3522
3631
  if (!soundEnable || headlessMode) return;
3523
3632
 
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
3633
+ audioMasterGain = audioContext.createGain();
3634
+ audioMasterGain.connect(audioContext.destination);
3635
+ audioMasterGain.gain.value = soundVolume; // set starting value
3528
3636
  }
3529
3637
 
3530
3638
  ///////////////////////////////////////////////////////////////////////////////
@@ -3653,6 +3761,8 @@ class Sound
3653
3761
  isLoading() { return !this.sampleChannels; }
3654
3762
  }
3655
3763
 
3764
+ ///////////////////////////////////////////////////////////////////////////////
3765
+
3656
3766
  /**
3657
3767
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
3658
3768
  * - this can be used to play wave, mp3, and ogg files
@@ -3704,59 +3814,7 @@ function playAudioFile(filename, volume=1, loop=false)
3704
3814
  return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
3705
3815
  }
3706
3816
 
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
- }
3817
+ ///////////////////////////////////////////////////////////////////////////////
3760
3818
 
3761
3819
  /** Speak text with passed in settings
3762
3820
  * @param {string} text - The text to speak
@@ -3828,7 +3886,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3828
3886
  // create and connect gain node
3829
3887
  gainNode = gainNode || audioContext.createGain();
3830
3888
  gainNode.gain.value = volume;
3831
- gainNode.connect(audioGainNode);
3889
+ gainNode.connect(audioMasterGain);
3832
3890
 
3833
3891
  // connect source to stereo panner and gain
3834
3892
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
@@ -3848,7 +3906,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3848
3906
  }
3849
3907
 
3850
3908
  ///////////////////////////////////////////////////////////////////////////////
3851
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
3909
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
3852
3910
 
3853
3911
  /** Generate and play a ZzFX sound
3854
3912
  *
@@ -3890,21 +3948,45 @@ const zzfxR = 44100;
3890
3948
  */
3891
3949
  function zzfxG
3892
3950
  (
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
3951
+ volume = 1,
3952
+ randomness = .05,
3953
+ frequency = 220,
3954
+ attack = 0,
3955
+ sustain = 0,
3956
+ release = .1,
3957
+ shape = 0,
3958
+ shapeCurve = 1,
3959
+ slide = 0,
3960
+ deltaSlide = 0,
3961
+ pitchJump = 0,
3962
+ pitchJumpTime = 0,
3963
+ repeatTime = 0,
3964
+ noise = 0,
3965
+ modulation = 0,
3966
+ bitCrush = 0,
3967
+ delay = 0,
3968
+ sustainVolume = 1,
3969
+ decay = 0,
3970
+ tremolo = 0,
3971
+ filter = 0
3898
3972
  )
3899
3973
  {
3900
- // LJS Note: ZZFX modded so randomness is handled by Sound class
3901
-
3902
3974
  // init parameters
3903
- let PI2 = PI*2, sampleRate = zzfxR,
3975
+ let sampleRate = zzfxR,
3976
+ PI2 = PI*2,
3904
3977
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3905
3978
  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,
3979
+ (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
3980
+ modOffset = 0, // modulation offset
3981
+ repeat = 0, // repeat offset
3982
+ crush = 0, // bit crush offset
3983
+ jump = 1, // pitch jump timer
3984
+ length, // sample length
3985
+ b = [], // sample buffer
3986
+ t = 0, // sample time
3987
+ i = 0, // sample index
3988
+ s = 0, // sample value
3989
+ f, // wave frequency
3908
3990
 
3909
3991
  // biquad LP/HP filter
3910
3992
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
@@ -3914,35 +3996,37 @@ function zzfxG
3914
3996
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
3915
3997
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
3916
3998
 
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;
3999
+ // scale by sample rate
4000
+ const minAttack = 9; // prevent pop if attack is 0
4001
+ attack = attack * sampleRate || minAttack;
4002
+ decay *= sampleRate;
4003
+ sustain *= sampleRate;
4004
+ release *= sampleRate;
4005
+ delay *= sampleRate;
4006
+ deltaSlide *= 500 * PI2 / sampleRate**3;
4007
+ modulation *= PI2 / sampleRate;
4008
+ pitchJump *= PI2 / sampleRate;
4009
+ pitchJumpTime *= sampleRate;
4010
+ repeatTime = repeatTime * sampleRate | 0;
3928
4011
 
3929
4012
  // generate waveform
3930
4013
  for(length = attack + decay + sustain + release + delay | 0;
3931
- i < length; b[i++] = s * volume) // sample
4014
+ i < length; b[i++] = s * volume) // sample
3932
4015
  {
3933
- if (!(++c%(bitCrush*100|0))) // bit crush
4016
+ if (!(++crush%(bitCrush*100|0))) // bit crush
3934
4017
  {
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
4018
+ s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
4019
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
4020
+ Math.sin(t**3) : // 4 noise
4021
+ Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
4022
+ 1-(2*t/PI2%2+2)%2: // 2 saw
4023
+ 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
4024
+ Math.sin(t); // 0 sin
3941
4025
 
3942
4026
  s = (repeatTime ?
3943
4027
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
3944
4028
  : 1) *
3945
- sign(s)*(abs(s)**shapeCurve) * // curve
4029
+ (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
3946
4030
  (i < attack ? i/attack : // attack
3947
4031
  i < attack + decay ? // decay
3948
4032
  1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
@@ -3957,209 +4041,77 @@ function zzfxG
3957
4041
  (i<length-delay? 1 : (length-i)/delay) * // release delay
3958
4042
  b[i-delay|0]/2/volume) : s; // sample delay
3959
4043
 
3960
- if (filter) // apply filter
4044
+ if (filter) // apply filter
3961
4045
  s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
3962
4046
  }
3963
4047
 
3964
4048
  f = (frequency += slide += deltaSlide) *// frequency
3965
- Math.cos(modulation*tm++); // modulation
4049
+ Math.cos(modulation*modOffset++); // modulation
3966
4050
  t += f + f*noise*Math.sin(i**5); // noise
3967
4051
 
3968
- if (j && ++j > pitchJumpTime) // pitch jump
4052
+ if (jump && ++jump > pitchJumpTime) // pitch jump
3969
4053
  {
3970
4054
  frequency += pitchJump; // apply pitch jump
3971
4055
  startFrequency += pitchJump; // also apply to start
3972
- j = 0; // stop pitch jump time
4056
+ jump = 0; // stop pitch jump time
3973
4057
  }
3974
4058
 
3975
- if (repeatTime && !(++r % repeatTime)) // repeat
4059
+ if (repeatTime && !(++repeat % repeatTime)) // repeat
3976
4060
  {
3977
- frequency = startFrequency; // reset frequency
3978
- slide = startSlide; // reset slide
3979
- j = j || 1; // reset pitch jump time
4061
+ frequency = startFrequency; // reset frequency
4062
+ slide = startSlide; // reset slide
4063
+ jump ||= 1; // reset pitch jump time
3980
4064
  }
3981
4065
  }
3982
4066
 
3983
- return b;
4067
+ return b; // return sample buffer
3984
4068
  }
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
- }
4069
+
4090
4070
  /**
4091
4071
  * LittleJS Tile Layer System
4092
4072
  * - Caches arrays of tiles to off screen canvas for fast rendering
4093
4073
  * - Unlimited numbers of layers, allocates canvases as needed
4094
- * - Interfaces with EngineObject for collision
4095
- * - Collision layer is separate from visible layers
4096
- * - It is recommended to have a visible layer that matches the collision
4097
4074
  * - Tile layers can be drawn to using their context with canvas2d
4098
4075
  * - Drawn directly to the main canvas without using WebGL
4076
+ * - Tile layers can also have collision with EngineObjects
4099
4077
  * @namespace TileCollision
4100
4078
  */
4101
4079
 
4102
4080
 
4103
4081
 
4104
- /** The tile collision layer grid, use setTileCollisionData and getTileCollisionData to access
4105
- * @type {Array<number>}
4106
- * @memberof TileCollision */
4107
- let tileCollision = [];
4108
-
4109
- /** Size of the tile collision layer 2d grid
4110
- * @type {Vector2}
4111
- * @memberof TileCollision */
4112
- let tileCollisionSize = vec2();
4113
-
4114
- /** Clear and initialize tile collision
4115
- * @param {Vector2} size - width and height of tile collision 2d grid
4116
- * @memberof TileCollision */
4117
- function initTileCollision(size)
4118
- {
4119
- tileCollisionSize = size;
4120
- tileCollision = [];
4121
- for (let i=tileCollision.length = tileCollisionSize.area(); i--;)
4122
- tileCollision[i] = 0;
4123
- }
4082
+ ///////////////////////////////////////////////////////////////////////////////
4083
+ // Tile Layer System
4124
4084
 
4125
- /** Set tile collision data for a given cell in the grid
4126
- * @param {Vector2} pos
4127
- * @param {number} [data]
4085
+ /** Keep track of all tile layers with collision
4086
+ * @type {Array<TileCollisionLayer>}
4128
4087
  * @memberof TileCollision */
4129
- function setTileCollisionData(pos, data=0)
4130
- {
4131
- pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
4132
- }
4088
+ let tileCollisionLayers = [];
4133
4089
 
4134
4090
  /** Get tile collision data for a given cell in the grid
4135
- * @param {Vector2} pos
4136
- * @return {number}
4137
- * @memberof TileCollision */
4091
+ * @param {Vector2} pos
4092
+ * @return {number}
4093
+ * @memberof TileCollision */
4138
4094
  function getTileCollisionData(pos)
4139
4095
  {
4140
- return pos.arrayCheck(tileCollisionSize) ? tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] : 0;
4096
+ // check all tile collision layers
4097
+ for (const layer of tileCollisionLayers)
4098
+ if (pos.arrayCheck(layer.size))
4099
+ return layer.getCollisionData(pos);
4100
+ return 0;
4141
4101
  }
4142
4102
 
4143
- /** Check if collision with another object should occur
4103
+ /** Check if a tile layer collides with another object
4144
4104
  * @param {Vector2} pos
4145
4105
  * @param {Vector2} [size=(0,0)]
4146
4106
  * @param {EngineObject} [object]
4147
- * @return {boolean}
4107
+ * @return {TileCollisionLayer}
4148
4108
  * @memberof TileCollision */
4149
4109
  function tileCollisionTest(pos, size=vec2(), object)
4150
4110
  {
4151
- const minX = max(pos.x - size.x/2|0, 0);
4152
- const minY = max(pos.y - size.y/2|0, 0);
4153
- const maxX = min(pos.x + size.x/2, tileCollisionSize.x);
4154
- const maxY = min(pos.y + size.y/2, tileCollisionSize.y);
4155
- for (let y = minY; y < maxY; ++y)
4156
- for (let x = minX; x < maxX; ++x)
4157
- {
4158
- const tileData = tileCollision[y*tileCollisionSize.x+x];
4159
- if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
4160
- return true;
4161
- }
4162
- return false;
4111
+ // check all tile collision layers
4112
+ for (const layer of tileCollisionLayers)
4113
+ if (layer.collisionTest(pos, size, object))
4114
+ return layer;
4163
4115
  }
4164
4116
 
4165
4117
  /** Return the center of first tile hit, undefined if nothing was hit.
@@ -4171,49 +4123,17 @@ function tileCollisionTest(pos, size=vec2(), object)
4171
4123
  * @memberof TileCollision */
4172
4124
  function tileCollisionRaycast(posStart, posEnd, object)
4173
4125
  {
4174
- // test if a ray collides with tiles from start to end
4175
- // todo: a way to get the exact hit point, it must still be inside the hit tile
4176
- const delta = posEnd.subtract(posStart);
4177
- const totalLength = delta.length();
4178
- const normalizedDelta = delta.normalize();
4179
- const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
4180
- const flooredPosStart = posStart.floor();
4181
-
4182
- // setup iteration variables
4183
- let pos = flooredPosStart;
4184
- let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
4185
- let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
4186
-
4187
- while (true)
4188
- {
4189
- // check for tile collision
4190
- const tileData = getTileCollisionData(pos);
4191
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
4192
- {
4193
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
4194
- debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
4195
- return pos.add(vec2(.5));
4196
- }
4197
-
4198
- // check if past the end
4199
- if (xi > totalLength && yi > totalLength)
4200
- break;
4201
-
4202
- // get coordinates of the next tile to check
4203
- if (xi > yi)
4204
- pos.y += sign(delta.y), yi += unit.y;
4205
- else
4206
- pos.x += sign(delta.x), xi += unit.x;
4126
+ // check all tile collision layers
4127
+ for (const layer of tileCollisionLayers)
4128
+ {
4129
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object)
4130
+ if (hitPos)
4131
+ return hitPos;
4207
4132
  }
4208
-
4209
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
4210
4133
  }
4211
4134
 
4212
- ///////////////////////////////////////////////////////////////////////////////
4213
- // Tile Layer Rendering System
4214
-
4215
4135
  /**
4216
- * Tile layer data object stores info about how to render a tile
4136
+ * Tile layer data object stores info about how to draw a tile
4217
4137
  * @example
4218
4138
  * // create tile layer data with tile index 0 and random orientation and color
4219
4139
  * const tileIndex = 0;
@@ -4245,28 +4165,27 @@ class TileLayerData
4245
4165
  clear() { this.tile = this.direction = 0; this.mirror = false; this.color = new Color; }
4246
4166
  }
4247
4167
 
4168
+ ///////////////////////////////////////////////////////////////////////////////
4248
4169
  /**
4249
4170
  * Tile Layer - cached rendering system for tile layers
4250
4171
  * - Each Tile layer is rendered to an off screen canvas
4251
4172
  * - To allow dynamic modifications, layers are rendered using canvas 2d
4252
4173
  * - Some devices like mobile phones are limited to 4k texture resolution
4253
- * - So with 16x16 tiles this limits layers to 256x256 on mobile devices
4174
+ * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
4254
4175
  * @extends EngineObject
4255
4176
  * @example
4256
- * // create tile collision and visible tile layer
4257
- * initTileCollision(vec2(200,100));
4258
- * const tileLayer = new TileLayer();
4177
+ * const tileLayer = new TileLayer(vec2(), vec2(200,100));
4259
4178
  */
4260
4179
  class TileLayer extends EngineObject
4261
4180
  {
4262
4181
  /** Create a tile layer object
4263
- * @param {Vector2} [position=(0,0)] - World space position
4264
- * @param {Vector2} [size=tileCollisionSize] - World space size
4265
- * @param {TileInfo} [tileInfo] - Tile info for layer
4266
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
4267
- * @param {number} [renderOrder] - Objects are sorted by renderOrder
4182
+ * @param {Vector2} [position=(0,0)] - World space position
4183
+ * @param {Vector2} [size=(1,1)] - World space size
4184
+ * @param {TileInfo} [tileInfo] - Tile info for layer
4185
+ * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
4186
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
4268
4187
  */
4269
- constructor(position, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderOrder=0)
4188
+ constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0)
4270
4189
  {
4271
4190
  super(position, size, tileInfo, 0, undefined, renderOrder);
4272
4191
 
@@ -4278,6 +4197,10 @@ class TileLayer extends EngineObject
4278
4197
  this.scale = scale;
4279
4198
  /** @property {boolean} - If true this layer will render to overlay canvas and appear above all objects */
4280
4199
  this.isOverlay = false;
4200
+ // set no friction by default, applied friction is max of both objects
4201
+ this.friction = 0;
4202
+ // set no elasticity by default, applied elasticity is max of both objects
4203
+ this.elasticity = 0;
4281
4204
 
4282
4205
  // init tile data
4283
4206
  this.data = [];
@@ -4474,23 +4397,163 @@ class TileLayer extends EngineObject
4474
4397
  * @param {number} [angle=0] */
4475
4398
  drawRect(pos, size, color, angle)
4476
4399
  { this.drawTile(pos, size, undefined, color, angle); }
4477
- }
4478
- /**
4479
- * LittleJS Particle System
4400
+ }
4401
+
4402
+ ///////////////////////////////////////////////////////////////////////////////
4403
+ /**
4404
+ * Tile Collision Layer - a tile layer with collision
4405
+ * - adds collision data and functions to TileLayer
4406
+ * - there can be multiple tile collision layers
4407
+ * - tile collison layers should not overlap each other
4408
+ * @extends TileLayer
4480
4409
  */
4410
+ class TileCollisionLayer extends TileLayer
4411
+ {
4412
+ /** Create a tile layer object
4413
+ * @param {Vector2} [position=(0,0)] - World space position
4414
+ * @param {Vector2} [size=(0,0)] - World space size
4415
+ * @param {TileInfo} [tileInfo] - Tile info for layer
4416
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
4417
+ */
4418
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
4419
+ {
4420
+ const scale = vec2(1); // collision layers are not scaled
4421
+ super(position, size.floor(), tileInfo, scale, renderOrder);
4481
4422
 
4423
+ /** @property {Array<number>} - The tile collision grid */
4424
+ this.collisionData = [];
4425
+ this.initCollision(this.size);
4482
4426
 
4427
+ // keep track of all collision layers
4428
+ tileCollisionLayers.push(this);
4429
+ }
4483
4430
 
4484
- /**
4485
- * Particle Emitter - Spawns particles with the given settings
4486
- * @extends EngineObject
4487
- * @example
4488
- * // create a particle emitter
4489
- * let pos = vec2(2,3);
4490
- * let particleEmitter = new ParticleEmitter
4491
- * (
4492
- * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emitCone
4493
- * tile(0, 16), // tileInfo
4431
+ /** Destroy this collision layer */
4432
+ destroy()
4433
+ {
4434
+ if (this.destroyed)
4435
+ return;
4436
+
4437
+ // remove from collision layers array and destroy
4438
+ const index = tileCollisionLayers.indexOf(this);
4439
+ ASSERT(index >= 0, 'tile collision layer not found in array');
4440
+ tileCollisionLayers.splice(index, 1);
4441
+ super.destroy();
4442
+ }
4443
+
4444
+ /** Clear and initialize tile collision to new size
4445
+ * @param {Vector2} size - width and height of tile collision 2d grid */
4446
+ initCollision(size)
4447
+ {
4448
+ this.size = size.floor();
4449
+ this.collisionData = [];
4450
+ this.collisionData.length = size.area();
4451
+ this.collisionData.fill(0);
4452
+ }
4453
+
4454
+ /** Set tile collision data for a given cell in the grid
4455
+ * @param {Vector2} pos
4456
+ * @param {number} [data] */
4457
+ setCollisionData(pos, data=1)
4458
+ {
4459
+ const i = (pos.y|0)*this.size.x + pos.x|0;
4460
+ pos.arrayCheck(this.size) && (this.collisionData[i] = data);
4461
+ }
4462
+
4463
+ /** Get tile collision data for a given cell in the grid
4464
+ * @param {Vector2} pos
4465
+ * @return {number} */
4466
+ getCollisionData(pos)
4467
+ {
4468
+ const i = (pos.y|0)*this.size.x + pos.x|0;
4469
+ return pos.arrayCheck(this.size) ? this.collisionData[i] : 0;
4470
+ }
4471
+
4472
+ /** Check if collision with another object should occur
4473
+ * @param {Vector2} pos
4474
+ * @param {Vector2} [size=(0,0)]
4475
+ * @param {EngineObject} [object]
4476
+ * @return {boolean} */
4477
+ collisionTest(pos, size=vec2(), object)
4478
+ {
4479
+ const minX = max(pos.x - size.x/2|0, 0);
4480
+ const minY = max(pos.y - size.y/2|0, 0);
4481
+ const maxX = min(pos.x + size.x/2, this.size.x);
4482
+ const maxY = min(pos.y + size.y/2, this.size.y);
4483
+ for (let y = minY; y < maxY; ++y)
4484
+ for (let x = minX; x < maxX; ++x)
4485
+ {
4486
+ // check if the object should collide with this tile
4487
+ const tileData = this.collisionData[y*this.size.x+x];
4488
+ if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
4489
+ return true;
4490
+ }
4491
+ return false;
4492
+ }
4493
+
4494
+ /** Return the center of first tile hit, undefined if nothing was hit.
4495
+ * This does not return the exact intersection, but the center of the tile hit.
4496
+ * @param {Vector2} posStart
4497
+ * @param {Vector2} posEnd
4498
+ * @param {EngineObject} [object]
4499
+ * @return {Vector2} */
4500
+ collisionRaycast(posStart, posEnd, object)
4501
+ {
4502
+ // test if a ray collides with tiles from start to end
4503
+ // todo: a way to get the exact hit point, it must still be inside the hit tile
4504
+ const delta = posEnd.subtract(posStart);
4505
+ const totalLength = delta.length();
4506
+ const normalizedDelta = delta.normalize();
4507
+ const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
4508
+ const flooredPosStart = posStart.floor();
4509
+
4510
+ // setup iteration variables
4511
+ let pos = flooredPosStart;
4512
+ let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
4513
+ let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
4514
+
4515
+ // use line drawing algorithm to test for collisions
4516
+ while (true)
4517
+ {
4518
+ // check for tile collision
4519
+ const tileData = this.getCollisionData(pos);
4520
+ if (tileData && (!object || object.collideWithTile(tileData, pos)))
4521
+ {
4522
+ debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
4523
+ debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
4524
+ return pos.add(vec2(.5));
4525
+ }
4526
+
4527
+ // check if past the end
4528
+ if (xi > totalLength && yi > totalLength)
4529
+ break;
4530
+
4531
+ // get coordinates of next tile to check
4532
+ if (xi > yi)
4533
+ pos.y += sign(delta.y), yi += unit.y;
4534
+ else
4535
+ pos.x += sign(delta.x), xi += unit.x;
4536
+ }
4537
+
4538
+ debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
4539
+ }
4540
+ }
4541
+ /**
4542
+ * LittleJS Particle System
4543
+ */
4544
+
4545
+
4546
+
4547
+ /**
4548
+ * Particle Emitter - Spawns particles with the given settings
4549
+ * @extends EngineObject
4550
+ * @example
4551
+ * // create a particle emitter
4552
+ * let pos = vec2(2,3);
4553
+ * let particleEmitter = new ParticleEmitter
4554
+ * (
4555
+ * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emitCone
4556
+ * tile(0, 16), // tileInfo
4494
4557
  * rgb(1,1,1), rgb(0,0,0), // colorStartA, colorStartB
4495
4558
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
4496
4559
  * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
@@ -4643,7 +4706,12 @@ class ParticleEmitter extends EngineObject
4643
4706
  else
4644
4707
  this.destroy();
4645
4708
 
4646
- debugParticles && debugRect(this.pos, vec2(this.emitSize), '#0f0', 0, this.angle);
4709
+ if (debugParticles)
4710
+ {
4711
+ // show emitter bounds
4712
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
4713
+ debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
4714
+ }
4647
4715
  }
4648
4716
 
4649
4717
  /** Spawn one particle
@@ -5105,12 +5173,12 @@ function glPreRender()
5105
5173
 
5106
5174
  // set vertex attributes
5107
5175
  let offset = glAdditive = glBatchAdditive = 0;
5108
- let initVertexAttribArray = (name, type, typeSize, size)=>
5176
+ const initVertexAttribArray = (name, type, typeSize, size)=>
5109
5177
  {
5110
5178
  const location = glContext.getAttribLocation(glShader, name);
5111
5179
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
5112
5180
  const divisor = typeSize && 1; // only if not geometry
5113
- const normalize = typeSize==1; // only if color
5181
+ const normalize = typeSize == 1; // only if color
5114
5182
  glContext.enableVertexAttribArray(location);
5115
5183
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
5116
5184
  glContext.vertexAttribDivisor(location, divisor);
@@ -5209,14 +5277,14 @@ function glCreateTexture(image)
5209
5277
  const texture = glContext.createTexture();
5210
5278
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
5211
5279
  if (image && image.width)
5212
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
5280
+ glSetTextureData(texture, image);
5213
5281
  else
5214
5282
  {
5215
5283
  // create a white texture
5216
5284
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
5217
5285
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
5218
5286
  }
5219
-
5287
+
5220
5288
  // use point filtering for pixelated rendering
5221
5289
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
5222
5290
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -5224,6 +5292,18 @@ function glCreateTexture(image)
5224
5292
  return texture;
5225
5293
  }
5226
5294
 
5295
+ /** Set WebGL texture data from an image
5296
+ * @param {WebGLTexture} texture
5297
+ * @param {HTMLImageElement} image
5298
+ * @memberof WebGL */
5299
+ function glSetTextureData(texture, image)
5300
+ {
5301
+ // build the texture
5302
+ ASSERT(!!image && image.width > 0, 'Invalid image data.');
5303
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
5304
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
5305
+ }
5306
+
5227
5307
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
5228
5308
  * @memberof WebGL */
5229
5309
  function glFlush()
@@ -5277,10 +5357,10 @@ function glSetAntialias(antialias=true)
5277
5357
  * @param {Number} uv0Y
5278
5358
  * @param {Number} uv1X
5279
5359
  * @param {Number} uv1Y
5280
- * @param {Number} rgba
5281
- * @param {Number} [rgbaAdditive=0]
5360
+ * @param {Number} [rgba=-1] - white is -1
5361
+ * @param {Number} [rgbaAdditive=0] - black is 0
5282
5362
  * @memberof WebGL */
5283
- function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
5363
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=-1, rgbaAdditive=0)
5284
5364
  {
5285
5365
  ASSERT(typeof rgba == 'number' && typeof rgbaAdditive == 'number', 'invalid color');
5286
5366
 
@@ -5333,7 +5413,7 @@ const engineName = 'LittleJS';
5333
5413
  * @type {string}
5334
5414
  * @default
5335
5415
  * @memberof Engine */
5336
- const engineVersion = '1.11.13';
5416
+ const engineVersion = '1.12.4';
5337
5417
 
5338
5418
  /** Frames per second to update
5339
5419
  * @type {number}
@@ -5421,16 +5501,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5421
5501
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
5422
5502
 
5423
5503
  // 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 = ()=>{};
5504
+ gameInit ||= ()=>{};
5505
+ gameUpdate ||= ()=>{};
5506
+ gameUpdatePost ||= ()=>{};
5507
+ gameRender ||= ()=>{};
5508
+ gameRenderPost ||= ()=>{};
5434
5509
 
5435
5510
  // Called automatically by engine to setup render system
5436
5511
  function enginePreRender()
@@ -5457,11 +5532,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5457
5532
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
5458
5533
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
5459
5534
  if (debug) // +/- to speed/slow time
5460
- frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
5535
+ frameTimeDeltaMS *= debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
5461
5536
  timeReal += frameTimeDeltaMS / 1e3;
5462
5537
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
5463
5538
  if (!debugSpeedUp)
5464
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
5539
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
5540
+ if (debug && debugVideoCaptureIsActive())
5541
+ frameTimeBufferMS = 0; // disable time smoothing when capturing video
5465
5542
 
5466
5543
  updateCanvas();
5467
5544
 
@@ -5540,6 +5617,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5540
5617
  }
5541
5618
  }
5542
5619
 
5620
+ debugVideoCaptureUpdate();
5543
5621
  requestAnimationFrame(engineUpdate);
5544
5622
  }
5545
5623
 
@@ -5587,11 +5665,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5587
5665
 
5588
5666
  // setup html
5589
5667
  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
5668
+ 'margin:0;' + // fill the window
5595
5669
  'background:#000;' + // set background color
5596
5670
  (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5597
5671
  'user-select:none;' + // prevent hold to select
@@ -5614,7 +5688,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5614
5688
  overlayContext = overlayCanvas.getContext('2d');
5615
5689
 
5616
5690
  // set canvas style
5617
- const styleCanvas = 'position:absolute'; // allow canvases to overlap
5691
+ const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
5692
+ 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
5618
5693
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
5619
5694
  if (glCanvas)
5620
5695
  glCanvas.style.cssText = styleCanvas;
@@ -5625,12 +5700,12 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5625
5700
  new Promise(resolve =>
5626
5701
  {
5627
5702
  const image = new Image;
5628
- image.crossOrigin = 'anonymous';
5629
5703
  image.onerror = image.onload = ()=>
5630
5704
  {
5631
5705
  textureInfos[textureIndex] = new TextureInfo(image);
5632
5706
  resolve();
5633
5707
  }
5708
+ image.crossOrigin = 'anonymous';
5634
5709
  image.src = src;
5635
5710
  })
5636
5711
  );
@@ -5932,192 +6007,2979 @@ function drawEngineSplashScreen(t)
5932
6007
  x.restore();
5933
6008
  }
5934
6009
 
5935
-
5936
- /**
5937
- * LittleJS Module Export
5938
- * - Export engine as a module
6010
+ /**
6011
+ * LittleJS Newgrounds API
6012
+ * - NewgroundsMedal extends Medal with Newgrounds API functionality
6013
+ * - Call new NewgroundsPlugin() to setup Newgrounds
6014
+ * - Uses CryptoJS for encryption if optional cipher is provided
6015
+ * - Keeps connection alive and logs views
6016
+ * - Functions to interact with scoreboards
6017
+ * - Functions to unlock medals
5939
6018
  */
5940
6019
 
5941
- export
5942
- {
5943
- // Engine
5944
- engineName,
5945
- engineVersion,
5946
- frameRate,
5947
- timeDelta,
5948
- engineObjects,
5949
- frame,
5950
- time,
5951
- timeReal,
5952
- paused,
5953
- setPaused,
5954
- engineInit,
5955
- engineObjectsUpdate,
5956
- engineObjectsDestroy,
5957
- engineObjectsCollect,
5958
- engineObjectsCallback,
5959
- engineObjectsRaycast,
5960
- engineAddPlugin,
5961
6020
 
5962
- // Globals
5963
- debug,
5964
- debugOverlay,
5965
- showWatermark,
5966
6021
 
5967
- // Debug
5968
- ASSERT,
5969
- debugRect,
5970
- debugPoly,
5971
- debugCircle,
5972
- debugPoint,
5973
- debugLine,
5974
- debugOverlap,
5975
- debugText,
5976
- debugClear,
5977
- debugScreenshot,
5978
- debugSaveCanvas,
5979
- debugSaveText,
5980
- debugSaveDataURL,
6022
+ /** Global Newgrounds object
6023
+ * @type {NewgroundsPlugin}
6024
+ * @memberof Medal */
6025
+ let newgrounds;
5981
6026
 
5982
- // Settings
5983
- cameraPos,
5984
- cameraScale,
5985
- canvasMaxSize,
5986
- canvasFixedSize,
5987
- canvasPixelated,
5988
- tilesPixelated,
5989
- fontDefault,
5990
- showSplashScreen,
5991
- headlessMode,
5992
- tileSizeDefault,
5993
- tileFixBleedScale,
5994
- enablePhysicsSolver,
5995
- objectDefaultMass,
5996
- objectDefaultDamping,
5997
- objectDefaultAngleDamping,
5998
- objectDefaultElasticity,
5999
- objectDefaultFriction,
6000
- objectMaxSpeed,
6001
- gravity,
6002
- particleEmitRateScale,
6003
- glEnable,
6004
- glOverlay,
6005
- gamepadsEnable,
6006
- gamepadDirectionEmulateStick,
6007
- inputWASDEmulateDirection,
6008
- touchGamepadEnable,
6009
- touchGamepadAnalog,
6010
- touchGamepadSize,
6011
- touchGamepadAlpha,
6012
- vibrateEnable,
6013
- soundEnable,
6014
- soundVolume,
6015
- soundDefaultRange,
6016
- soundDefaultTaper,
6017
- medalDisplayTime,
6018
- medalDisplaySlideTime,
6019
- medalDisplaySize,
6027
+ ///////////////////////////////////////////////////////////////////////////////
6028
+ /**
6029
+ * Newgrounds medal auto unlocks in newgrounds API
6030
+ * @extends Medal
6031
+ */
6032
+ class NewgroundsMedal extends Medal
6033
+ {
6034
+ /** Create a newgrounds medal object and adds it to the list of medals
6035
+ * @param {Number} id - The unique identifier of the medal
6036
+ * @param {String} name - Name of the medal
6037
+ * @param {String} [description] - Description of the medal
6038
+ * @param {String} [icon] - Icon for the medal
6039
+ * @param {String} [src] - Image location for the medal
6040
+ */
6041
+ constructor(id, name, description, icon, src)
6042
+ { super(id, name, description, icon, src); }
6020
6043
 
6021
- // Setters for globals
6022
- setCameraPos,
6023
- setCameraScale,
6024
- setCanvasMaxSize,
6025
- setCanvasFixedSize,
6026
- setCanvasPixelated,
6027
- setTilesPixelated,
6028
- setFontDefault,
6029
- setShowSplashScreen,
6030
- setHeadlessMode,
6031
- setGlEnable,
6032
- setGlOverlay,
6033
- setTileSizeDefault,
6034
- setTileFixBleedScale,
6035
- setEnablePhysicsSolver,
6036
- setObjectDefaultMass,
6037
- setObjectDefaultDamping,
6038
- setObjectDefaultAngleDamping,
6039
- setObjectDefaultElasticity,
6040
- setObjectDefaultFriction,
6041
- setObjectMaxSpeed,
6042
- setGravity,
6043
- setParticleEmitRateScale,
6044
- setTouchInputEnable,
6045
- setGamepadsEnable,
6046
- setGamepadDirectionEmulateStick,
6047
- setInputWASDEmulateDirection,
6048
- setTouchGamepadEnable,
6049
- setTouchGamepadAnalog,
6050
- setTouchGamepadSize,
6051
- setTouchGamepadAlpha,
6052
- setVibrateEnable,
6053
- setSoundEnable,
6054
- setSoundVolume,
6055
- setSoundDefaultRange,
6056
- setSoundDefaultTaper,
6057
- setMedalDisplayTime,
6058
- setMedalDisplaySlideTime,
6059
- setMedalDisplaySize,
6060
- setMedalsPreventUnlock,
6061
- setShowWatermark,
6062
- setDebugKey,
6044
+ /** Unlocks a medal if not already unlocked */
6045
+ unlock()
6046
+ {
6047
+ super.unlock();
6048
+ newgrounds && newgrounds.unlockMedal(this.id);
6049
+ }
6050
+ }
6063
6051
 
6064
- // Utilities
6065
- PI,
6066
- abs,
6067
- min,
6068
- max,
6069
- sign,
6070
- mod,
6071
- clamp,
6072
- percent,
6073
- distanceWrap,
6074
- lerpWrap,
6075
- distanceAngle,
6076
- lerpAngle,
6077
- lerp,
6078
- smoothStep,
6079
- nearestPowerOfTwo,
6080
- isOverlapping,
6081
- isIntersecting,
6082
- wave,
6083
- formatTime,
6052
+ ///////////////////////////////////////////////////////////////////////////////
6053
+ /**
6054
+ * Newgrounds API object
6055
+ */
6056
+ class NewgroundsPlugin
6057
+ {
6058
+ /** Create the global newgrounds object
6059
+ * @param {string} app_id - The newgrounds App ID
6060
+ * @param {string} [cipher] - The encryption Key (AES-128/Base64)
6061
+ * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
6062
+ * @example
6063
+ * // create the newgrounds object, replace the app id with your own
6064
+ * const app_id = 'your_app_id_here';
6065
+ * new NewgroundsPlugin(app_id);
6066
+ */
6067
+ constructor(app_id, cipher, cryptoJS)
6068
+ {
6069
+ ASSERT(!newgrounds, 'there can only be one newgrounds object');
6070
+ ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
6084
6071
 
6085
- // Random
6086
- rand,
6087
- randInt,
6088
- randSign,
6089
- randInCircle,
6090
- randVector,
6091
- randColor,
6072
+ newgrounds = this; // set global newgrounds object
6073
+ this.app_id = app_id;
6074
+ this.cipher = cipher;
6075
+ this.cryptoJS = cryptoJS;
6076
+ this.host = location ? location.hostname : '';
6092
6077
 
6093
- // Utility Classes
6094
- RandomGenerator,
6095
- Vector2,
6096
- Color,
6097
- Timer,
6098
- vec2,
6099
- rgb,
6100
- hsl,
6101
- isColor,
6078
+ // get session id from url search params
6079
+ const url = new URL(location.href);
6080
+ this.session_id = url.searchParams.get('ngio_session_id');
6102
6081
 
6103
- // Default Colors
6104
- WHITE,
6105
- BLACK,
6106
- GRAY,
6107
- RED,
6108
- ORANGE,
6109
- YELLOW,
6110
- GREEN,
6111
- CYAN,
6112
- BLUE,
6113
- PURPLE,
6114
- MAGENTA,
6082
+ if (!this.session_id)
6083
+ return; // only use newgrounds when logged in
6115
6084
 
6116
- // Draw
6117
- textureInfos,
6118
- tile,
6119
- TileInfo,
6120
- TextureInfo,
6085
+ // get medals
6086
+ const medalsResult = this.call('Medal.getList');
6087
+ this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
6088
+ debugMedals && console.log(this.medals);
6089
+ for (const newgroundsMedal of this.medals)
6090
+ {
6091
+ const medal = medals[newgroundsMedal['id']];
6092
+ if (medal)
6093
+ {
6094
+ // copy newgrounds medal data
6095
+ medal.image = new Image;
6096
+ medal.image.src = newgroundsMedal['icon'];
6097
+ medal.name = newgroundsMedal['name'];
6098
+ medal.description = newgroundsMedal['description'];
6099
+ medal.unlocked = newgroundsMedal['unlocked'];
6100
+ medal.difficulty = newgroundsMedal['difficulty'];
6101
+ medal.value = newgroundsMedal['value'];
6102
+
6103
+ if (medal.value) // add value to description
6104
+ medal.description = medal.description + ` (${ medal.value })`;
6105
+ }
6106
+ }
6107
+
6108
+ // get scoreboards
6109
+ const scoreboardResult = this.call('ScoreBoard.getBoards');
6110
+ this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
6111
+ debugMedals && console.log(this.scoreboards);
6112
+
6113
+ // keep the session alive with a ping every minute
6114
+ const keepAliveMS = 60 * 1e3;
6115
+ setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
6116
+ }
6117
+
6118
+ /** Send message to unlock a medal by id
6119
+ * @param {number} id - The medal id */
6120
+ unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
6121
+
6122
+ /** Send message to post score
6123
+ * @param {number} id - The scoreboard id
6124
+ * @param {number} value - The score value */
6125
+ postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
6126
+
6127
+ /** Get scores from a scoreboard
6128
+ * @param {number} id - The scoreboard id
6129
+ * @param {string} [user] - A user's id or name
6130
+ * @param {number} [social] - If true, only social scores will be loaded
6131
+ * @param {number} [skip] - Number of scores to skip before start
6132
+ * @param {number} [limit] - Number of scores to include in the list
6133
+ * @return {Object} - The response JSON object
6134
+ */
6135
+ getScores(id, user, social=0, skip=0, limit=10)
6136
+ { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
6137
+
6138
+ /** Send message to log a view */
6139
+ logView() { return this.call('App.logView', {'host':this.host}, true); }
6140
+
6141
+ /** Send a message to call a component of the Newgrounds API
6142
+ * @param {string} component - Name of the component
6143
+ * @param {Object} [parameters] - Parameters to use for call
6144
+ * @param {boolean} [async] - If true, don't wait for response before continuing
6145
+ * @return {Object} - The response JSON object
6146
+ */
6147
+ call(component, parameters, async=false)
6148
+ {
6149
+ const call = {'component':component, 'parameters':parameters};
6150
+ if (this.cipher)
6151
+ {
6152
+ // encrypt using AES-128 Base64 with cryptoJS
6153
+ const cryptoJS = this.cryptoJS;
6154
+ const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
6155
+ const iv = cryptoJS['lib']['WordArray']['random'](16);
6156
+ const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
6157
+ call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
6158
+ call['parameters'] = 0;
6159
+ }
6160
+
6161
+ // build the input object
6162
+ const input =
6163
+ {
6164
+ 'app_id': this.app_id,
6165
+ 'session_id': this.session_id,
6166
+ 'call': call
6167
+ };
6168
+
6169
+ // build post data
6170
+ const formData = new FormData();
6171
+ formData.append('input', JSON.stringify(input));
6172
+
6173
+ // send post data
6174
+ const xmlHttp = new XMLHttpRequest();
6175
+ const url = 'https://newgrounds.io/gateway_v3.php';
6176
+ xmlHttp.open('POST', url, !debugMedals && async);
6177
+ try { xmlHttp.send(formData); }
6178
+ catch(e)
6179
+ {
6180
+ debugMedals && console.log('newgrounds call failed', e);
6181
+ return;
6182
+ }
6183
+ debugMedals && console.log(xmlHttp.responseText);
6184
+ return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
6185
+ }
6186
+ }
6187
+
6188
+ /**
6189
+ * LittleJS Post Processing Plugin
6190
+ * - Supports shadertoy style post processing shaders
6191
+ * - call new new PostProcessPlugin() to setup post processing
6192
+ * - can be enabled to pass other canvases through a final shader
6193
+ */
6194
+
6195
+
6196
+
6197
+ ///////////////////////////////////////////////////////////////////////////////
6198
+
6199
+ /** Global Post Process plugin object
6200
+ * @type {PostProcessPlugin} */
6201
+ let postProcess;
6202
+
6203
+ /////////////////////////////////////////////////////////////////////////
6204
+ /**
6205
+ * UI System Global Object
6206
+ */
6207
+ class PostProcessPlugin
6208
+ {
6209
+ /** Create global post processing shader
6210
+ * @param {string} shaderCode
6211
+ * @param {boolean} [includeOverlay]
6212
+ * @example
6213
+ * // create the post process plugin object
6214
+ * new PostProcessPlugin(shaderCode);
6215
+ */
6216
+ constructor(shaderCode, includeOverlay=false)
6217
+ {
6218
+ ASSERT(!postProcess, 'Post process already initialized');
6219
+ postProcess = this;
6220
+
6221
+ if (headlessMode) return;
6222
+ if (!shaderCode) // default shader pass through
6223
+ shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
6224
+
6225
+ /** @property {WebGLProgram} - Shader for post processing */
6226
+ this.shader = glCreateProgram(
6227
+ '#version 300 es\n' + // specify GLSL ES version
6228
+ 'precision highp float;'+ // use highp for better accuracy
6229
+ 'in vec2 p;'+ // position
6230
+ 'void main(){'+ // shader entry point
6231
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
6232
+ '}' // end of shader
6233
+ ,
6234
+ '#version 300 es\n' + // specify GLSL ES version
6235
+ 'precision highp float;'+ // use highp for better accuracy
6236
+ 'uniform sampler2D iChannel0;'+ // input texture
6237
+ 'uniform vec3 iResolution;'+ // size of output texture
6238
+ 'uniform float iTime;'+ // time
6239
+ 'out vec4 c;'+ // out color
6240
+ '\n' + shaderCode + '\n'+ // insert custom shader code
6241
+ 'void main(){'+ // shader entry point
6242
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
6243
+ 'c.a=1.;'+ // always use full alpha
6244
+ '}' // end of shader
6245
+ );
6246
+
6247
+ /** @property {WebGLTexture} - Texture for post processing */
6248
+ this.texture = glCreateTexture();
6249
+
6250
+ /** @property {boolean} - Should overlay canvas be included in post processing */
6251
+ this.includeOverlay = includeOverlay;
6252
+
6253
+ // Render the post processing shader, called automatically by the engine
6254
+ engineAddPlugin(undefined, postProcessRender);
6255
+ function postProcessRender()
6256
+ {
6257
+ if (headlessMode) return;
6258
+
6259
+ // prepare to render post process shader
6260
+ if (glEnable)
6261
+ {
6262
+ glFlush(); // clear out the buffer
6263
+ mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
6264
+ }
6265
+ else
6266
+ {
6267
+ // set the viewport
6268
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
6269
+ }
6270
+
6271
+ if (postProcess.includeOverlay)
6272
+ {
6273
+ // copy overlay canvas so it will be included in post processing
6274
+ mainContext.drawImage(overlayCanvas, 0, 0);
6275
+ overlayCanvas.width |= 0;
6276
+ }
6277
+
6278
+ // setup shader program to draw one triangle
6279
+ glContext.useProgram(postProcess.shader);
6280
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6281
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
6282
+ glContext.disable(glContext.BLEND);
6283
+
6284
+ // set textures, pass in the 2d canvas and gl canvas in separate texture channels
6285
+ glContext.activeTexture(glContext.TEXTURE0);
6286
+ glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
6287
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
6288
+
6289
+ // set vertex position attribute
6290
+ const vertexByteStride = 8;
6291
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
6292
+ glContext.enableVertexAttribArray(pLocation);
6293
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
6294
+
6295
+ // set uniforms and draw
6296
+ const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
6297
+ glContext.uniform1i(uniformLocation('iChannel0'), 0);
6298
+ glContext.uniform1f(uniformLocation('iTime'), time);
6299
+ glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
6300
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
6301
+ }
6302
+ }
6303
+ }
6304
+
6305
+ /**
6306
+ * LittleJS ZzFXM Plugin
6307
+ */
6308
+
6309
+
6310
+
6311
+ /**
6312
+ * Music Object - Stores a zzfx music track for later use
6313
+ *
6314
+ * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
6315
+ * @example
6316
+ * // create some music
6317
+ * const music_example = new Music(
6318
+ * [
6319
+ * [ // instruments
6320
+ * [,0,400] // simple note
6321
+ * ],
6322
+ * [ // patterns
6323
+ * [ // pattern 1
6324
+ * [ // channel 0
6325
+ * 0, -1, // instrument 0, left speaker
6326
+ * 1, 0, 9, 1 // channel notes
6327
+ * ],
6328
+ * [ // channel 1
6329
+ * 0, 1, // instrument 0, right speaker
6330
+ * 0, 12, 17, -1 // channel notes
6331
+ * ]
6332
+ * ],
6333
+ * ],
6334
+ * [0, 0, 0, 0], // sequence, play pattern 0 four times
6335
+ * 90 // BPM
6336
+ * ]);
6337
+ *
6338
+ * // play the music
6339
+ * music_example.play();
6340
+ */
6341
+ class ZzFXMusic extends Sound
6342
+ {
6343
+ /** Create a music object and cache the zzfx music samples for later use
6344
+ * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
6345
+ */
6346
+ constructor(zzfxMusic)
6347
+ {
6348
+ super(undefined);
6349
+
6350
+ if (!soundEnable || headlessMode) return;
6351
+ this.randomness = 0;
6352
+ this.sampleChannels = zzfxM(...zzfxMusic);
6353
+ this.sampleRate = zzfxR;
6354
+ }
6355
+
6356
+ /** Play the music
6357
+ * @param {number} [volume=1] - How much to scale volume by
6358
+ * @param {boolean} [loop] - True if the music should loop
6359
+ * @return {AudioBufferSourceNode} - The audio source node
6360
+ */
6361
+ playMusic(volume, loop=false)
6362
+ { return super.play(undefined, volume, 1, 1, loop); }
6363
+ }
6364
+
6365
+ ///////////////////////////////////////////////////////////////////////////////
6366
+ // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
6367
+
6368
+ /** Generate samples for a ZzFM song with given parameters
6369
+ * @param {Array} instruments - Array of ZzFX sound parameters
6370
+ * @param {Array} patterns - Array of pattern data
6371
+ * @param {Array} sequence - Array of pattern indexes
6372
+ * @param {number} [BPM] - Playback speed of the song in BPM
6373
+ * @return {Array} - Left and right channel sample data */
6374
+ function zzfxM(instruments, patterns, sequence, BPM = 125)
6375
+ {
6376
+ let i, j, k;
6377
+ let instrumentParameters;
6378
+ let note;
6379
+ let sample;
6380
+ let patternChannel;
6381
+ let notFirstBeat;
6382
+ let stop;
6383
+ let instrument;
6384
+ let attenuation;
6385
+ let outSampleOffset;
6386
+ let isSequenceEnd;
6387
+ let sampleOffset = 0;
6388
+ let nextSampleOffset;
6389
+ let sampleBuffer = [];
6390
+ let leftChannelBuffer = [];
6391
+ let rightChannelBuffer = [];
6392
+ let channelIndex = 0;
6393
+ let panning = 0;
6394
+ let hasMore = 1;
6395
+ let sampleCache = {};
6396
+ let beatLength = zzfxR / BPM * 60 >> 2;
6397
+
6398
+ // for each channel in order until there are no more
6399
+ for (; hasMore; channelIndex++) {
6400
+
6401
+ // reset current values
6402
+ sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
6403
+
6404
+ // for each pattern in sequence
6405
+ sequence.forEach((patternIndex, sequenceIndex) => {
6406
+ // get pattern for current channel, use empty 1 note pattern if none found
6407
+ patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
6408
+
6409
+ // check if there are more channels
6410
+ hasMore |= patterns[patternIndex][channelIndex]&&1;
6411
+
6412
+ // get next offset, use the length of first channel
6413
+ nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
6414
+ // for each beat in pattern, plus one extra if end of sequence
6415
+ isSequenceEnd = sequenceIndex == sequence.length - 1;
6416
+ for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
6417
+
6418
+ // <channel-note>
6419
+ note = patternChannel[i];
6420
+
6421
+ // stop if end, different instrument or new note
6422
+ stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
6423
+ instrument != (patternChannel[0] || 0) || note | 0;
6424
+
6425
+ // fill buffer with samples for previous beat, most cpu intensive part
6426
+ for (j = 0; j < beatLength && notFirstBeat;
6427
+
6428
+ // fade off attenuation at end of beat if stopping note, prevents clicking
6429
+ j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
6430
+ ) {
6431
+ // copy sample to stereo buffers with panning
6432
+ sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
6433
+ leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
6434
+ rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
6435
+ }
6436
+
6437
+ // set up for next note
6438
+ if (note) {
6439
+ // set attenuation
6440
+ attenuation = note % 1;
6441
+ panning = patternChannel[1] || 0;
6442
+ if (note |= 0) {
6443
+ // get cached sample
6444
+ sampleBuffer = sampleCache[
6445
+ [
6446
+ instrument = patternChannel[sampleOffset = 0] || 0,
6447
+ note
6448
+ ]
6449
+ ] = sampleCache[[instrument, note]] || (
6450
+ // add sample to cache
6451
+ instrumentParameters = [...instruments[instrument]],
6452
+ instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
6453
+
6454
+ // allow negative values to stop notes
6455
+ note > 0 ? zzfxG(...instrumentParameters) : []
6456
+ );
6457
+ }
6458
+ }
6459
+ }
6460
+
6461
+ // update the sample offset
6462
+ outSampleOffset = nextSampleOffset;
6463
+ });
6464
+ }
6465
+
6466
+ return [leftChannelBuffer, rightChannelBuffer];
6467
+ }
6468
+
6469
+ /**
6470
+ * LittleJS User Interface Plugin
6471
+ * - call new UISystemPlugin() to setup the UI system
6472
+ * - Nested Menus
6473
+ * - Text
6474
+ * - Buttons
6475
+ * - Checkboxes
6476
+ * - Images
6477
+ */
6478
+
6479
+
6480
+
6481
+ ///////////////////////////////////////////////////////////////////////////////
6482
+
6483
+ /** Global UI system plugin object
6484
+ * @type {UISystemPlugin} */
6485
+ let uiSystem;
6486
+
6487
+ ///////////////////////////////////////////////////////////////////////////////
6488
+ /**
6489
+ * UI System Global Object
6490
+ */
6491
+ class UISystemPlugin
6492
+ {
6493
+ /** Create the global UI system object
6494
+ * @param {CanvasRenderingContext2D} [context]
6495
+ * @example
6496
+ * // create the ui plugin object
6497
+ * new UISystemPlugin;
6498
+ */
6499
+ constructor(context=overlayContext)
6500
+ {
6501
+ ASSERT(!uiSystem, 'UI system already initialized');
6502
+ uiSystem = this;
6503
+
6504
+ /** @property {Color} - Default fill color for UI elements */
6505
+ this.defaultColor = WHITE;
6506
+ /** @property {Color} - Default outline color for UI elements */
6507
+ this.defaultLineColor = BLACK;
6508
+ /** @property {Color} - Default text color for UI elements */
6509
+ this.defaultTextColor = BLACK;
6510
+ /** @property {Color} - Default button color for UI elements */
6511
+ this.defaultButtonColor = hsl(0,0,.5);
6512
+ /** @property {Color} - Default hover color for UI elements */
6513
+ this.defaultHoverColor = hsl(0,0,.7);
6514
+ /** @property {number} - Default line width for UI elements */
6515
+ this.defaultLineWidth = 4;
6516
+ /** @property {string} - Default font for UI elements */
6517
+ this.defaultFont = 'arial';
6518
+ /** @property {Array<UIObject>} - List of all UI elements */
6519
+ this.uiObjects = [];
6520
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
6521
+ this.uiContext = context;
6522
+
6523
+ engineAddPlugin(uiUpdate, uiRender);
6524
+
6525
+ // setup recursive update and render
6526
+ function uiUpdate()
6527
+ {
6528
+ function updateObject(o)
6529
+ {
6530
+ if (!o.visible)
6531
+ return;
6532
+ if (o.parent)
6533
+ o.pos = o.localPos.add(o.parent.pos);
6534
+ o.update();
6535
+ for(const c of o.children)
6536
+ updateObject(c);
6537
+ }
6538
+ uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
6539
+ }
6540
+ function uiRender()
6541
+ {
6542
+ function renderObject(o)
6543
+ {
6544
+ if (!o.visible)
6545
+ return;
6546
+ if (o.parent)
6547
+ o.pos = o.localPos.add(o.parent.pos);
6548
+ o.render();
6549
+ for(const c of o.children)
6550
+ renderObject(c);
6551
+ }
6552
+ uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
6553
+ }
6554
+ }
6555
+
6556
+ /** Draw a rectangle to the UI context
6557
+ * @param {Vector2} pos
6558
+ * @param {Vector2} size
6559
+ * @param {Color} [color=uiSystem.defaultColor]
6560
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6561
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6562
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6563
+ {
6564
+ uiSystem.uiContext.fillStyle = color.toString();
6565
+ uiSystem.uiContext.beginPath();
6566
+ uiSystem.uiContext.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
6567
+ uiSystem.uiContext.fill();
6568
+ if (lineWidth)
6569
+ {
6570
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6571
+ uiSystem.uiContext.lineWidth = lineWidth;
6572
+ uiSystem.uiContext.stroke();
6573
+ }
6574
+ }
6575
+
6576
+ /** Draw a line to the UI context
6577
+ * @param {Vector2} posA
6578
+ * @param {Vector2} posB
6579
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6580
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6581
+ drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6582
+ {
6583
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6584
+ uiSystem.uiContext.lineWidth = lineWidth;
6585
+ uiSystem.uiContext.beginPath();
6586
+ uiSystem.uiContext.lineTo(posA.x, posA.y);
6587
+ uiSystem.uiContext.lineTo(posB.x, posB.y);
6588
+ uiSystem.uiContext.stroke();
6589
+ }
6590
+
6591
+ /** Draw a tile to the UI context
6592
+ * @param {Vector2} pos
6593
+ * @param {Vector2} size
6594
+ * @param {TileInfo} tileInfo
6595
+ * @param {Color} [color=uiSystem.defaultColor]
6596
+ * @param {number} [angle]
6597
+ * @param {boolean} [mirror] */
6598
+ drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false)
6599
+ {
6600
+ drawTile(pos, size, tileInfo, color, angle, mirror, BLACK, false, true, uiSystem.uiContext);
6601
+ }
6602
+
6603
+ /** Draw text to the UI context
6604
+ * @param {string} text
6605
+ * @param {Vector2} pos
6606
+ * @param {Vector2} size
6607
+ * @param {Color} [color=uiSystem.defaultColor]
6608
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6609
+ * @param {Color} [lineColor=uiSystem.defaultLineColor]
6610
+ * @param {string} [align]
6611
+ * @param {string} [font=uiSystem.defaultFont] */
6612
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont)
6613
+ {
6614
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, size.x, uiSystem.uiContext);
6615
+ }
6616
+ }
6617
+
6618
+ ///////////////////////////////////////////////////////////////////////////////
6619
+ /**
6620
+ * UI Object - Base level object for all UI elements
6621
+ */
6622
+ class UIObject
6623
+ {
6624
+ /** Create a UIObject
6625
+ * @param {Vector2} [pos=(0,0)]
6626
+ * @param {Vector2} [size=(1,1)]
6627
+ */
6628
+ constructor(pos=vec2(), size=vec2())
6629
+ {
6630
+ /** @property {Vector2} - Local position of the object */
6631
+ this.localPos = pos.copy();
6632
+ /** @property {Vector2} - Screen space position of the object */
6633
+ this.pos = pos.copy();
6634
+ /** @property {Vector2} - Screen space size of the object */
6635
+ this.size = size.copy();
6636
+ /** @property {Color} */
6637
+ this.color = uiSystem.defaultColor;
6638
+ /** @property {Color} */
6639
+ this.lineColor = uiSystem.defaultLineColor;
6640
+ /** @property {Color} */
6641
+ this.textColor = uiSystem.defaultTextColor;
6642
+ /** @property {Color} */
6643
+ this.hoverColor = uiSystem.defaultHoverColor;
6644
+ /** @property {number} */
6645
+ this.lineWidth = uiSystem.defaultLineWidth;
6646
+ /** @property {string} */
6647
+ this.font = uiSystem.defaultFont;
6648
+ /** @property {boolean} */
6649
+ this.visible = true;
6650
+ /** @property {Array<UIObject>} */
6651
+ this.children = [];
6652
+ /** @property {UIObject} */
6653
+ this.parent = undefined;
6654
+ uiSystem.uiObjects.push(this);
6655
+ }
6656
+
6657
+ /** Add a child UIObject to this object
6658
+ * @param {UIObject} child
6659
+ */
6660
+ addChild(child)
6661
+ {
6662
+ ASSERT(!child.parent && !this.children.includes(child));
6663
+ this.children.push(child);
6664
+ child.parent = this;
6665
+ }
6666
+
6667
+ /** Remove a child UIObject from this object
6668
+ * @param {UIObject} child
6669
+ */
6670
+ removeChild(child)
6671
+ {
6672
+ ASSERT(child.parent == this && this.children.includes(child));
6673
+ this.children.splice(this.children.indexOf(child), 1);
6674
+ child.parent = undefined;
6675
+ }
6676
+
6677
+ /** Update the object, called automatically by plugin once each frame */
6678
+ update()
6679
+ {
6680
+ // track mouse input
6681
+ const mouseWasOver = this.mouseIsOver;
6682
+ const mouseDown = mouseIsDown(0);
6683
+ if (!mouseDown || isTouchDevice)
6684
+ {
6685
+ this.mouseIsOver = isOverlapping(this.pos, this.size, mousePosScreen);
6686
+ if (!mouseDown && isTouchDevice)
6687
+ this.mouseIsOver = false;
6688
+ if (this.mouseIsOver && !mouseWasOver)
6689
+ this.onEnter();
6690
+ if (!this.mouseIsOver && mouseWasOver)
6691
+ this.onLeave();
6692
+ }
6693
+ if (mouseWasPressed(0) && this.mouseIsOver)
6694
+ {
6695
+ this.mouseIsHeld = true;
6696
+ this.onPress();
6697
+ if (isTouchDevice)
6698
+ this.mouseIsOver = false;
6699
+ }
6700
+ else if (this.mouseIsHeld && !mouseDown)
6701
+ {
6702
+ this.mouseIsHeld = false;
6703
+ this.onRelease();
6704
+ }
6705
+ }
6706
+
6707
+ /** Render the object, called automatically by plugin once each frame */
6708
+ render()
6709
+ {
6710
+ if (this.size.x && this.size.y)
6711
+ uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor);
6712
+ }
6713
+
6714
+ /** Called when the mouse enters the object */
6715
+ onEnter() {}
6716
+
6717
+ /** Called when the mouse leaves the object */
6718
+ onLeave() {}
6719
+
6720
+ /** Called when the mouse is pressed while over the object */
6721
+ onPress() {}
6722
+
6723
+ /** Called when the mouse is released while over the object */
6724
+ onRelease() {}
6725
+
6726
+ /** Called when the state of this object changes */
6727
+ onChange() {}
6728
+ }
6729
+
6730
+ ///////////////////////////////////////////////////////////////////////////////
6731
+ /**
6732
+ * UIText - A UI object that displays text
6733
+ * @extends UIObject
6734
+ */
6735
+ class UIText extends UIObject
6736
+ {
6737
+ /** Create a UIText object
6738
+ * @param {Vector2} [pos]
6739
+ * @param {Vector2} [size]
6740
+ * @param {string} [text]
6741
+ * @param {string} [align]
6742
+ * @param {string} [font=uiSystem.defaultFont]
6743
+ */
6744
+ constructor(pos, size, text='', align='center', font=uiSystem.defaultFont)
6745
+ {
6746
+ super(pos, size);
6747
+
6748
+ /** @property {string} */
6749
+ this.text = text;
6750
+ /** @property {string} */
6751
+ this.align = align;
6752
+
6753
+ this.font = font; // set font
6754
+ this.lineWidth = 0; // set text to not be outlined by default
6755
+ }
6756
+ render()
6757
+ {
6758
+ uiSystem.drawText(this.text, this.pos, this.size, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
6759
+ }
6760
+ }
6761
+
6762
+ ///////////////////////////////////////////////////////////////////////////////
6763
+ /**
6764
+ * UITile - A UI object that displays a tile image
6765
+ * @extends UIObject
6766
+ */
6767
+ class UITile extends UIObject
6768
+ {
6769
+ /** Create a UITile object
6770
+ * @param {Vector2} [pos]
6771
+ * @param {Vector2} [size]
6772
+ * @param {TileInfo} [tileInfo]
6773
+ * @param {Color} [color=WHITE]
6774
+ * @param {number} [angle]
6775
+ * @param {boolean} [mirror]
6776
+ */
6777
+ constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
6778
+ {
6779
+ super(pos, size);
6780
+
6781
+ /** @property {TileInfo} - Tile image to use */
6782
+ this.tileInfo = tileInfo;
6783
+ /** @property {number} - Angle to rotate in radians */
6784
+ this.angle = angle;
6785
+ /** @property {boolean} - Should it be mirrored? */
6786
+ this.mirror = mirror;
6787
+ this.color = color;
6788
+ }
6789
+ render()
6790
+ {
6791
+ uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
6792
+ }
6793
+ }
6794
+
6795
+ ///////////////////////////////////////////////////////////////////////////////
6796
+ /**
6797
+ * UIButton - A UI object that acts as a button
6798
+ * @extends UIObject
6799
+ */
6800
+ class UIButton extends UIObject
6801
+ {
6802
+ /** Create a UIButton object
6803
+ * @param {Vector2} [pos]
6804
+ * @param {Vector2} [size]
6805
+ * @param {string} [text]
6806
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6807
+ */
6808
+ constructor(pos, size, text='', color=uiSystem.defaultButtonColor)
6809
+ {
6810
+ super(pos, size);
6811
+
6812
+ /** @property {string} */
6813
+ this.text = text;
6814
+ this.color = color;
6815
+ }
6816
+ render()
6817
+ {
6818
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
6819
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6820
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
6821
+ const textSize = vec2(this.size.x, this.size.y*.8);
6822
+ uiSystem.drawText(this.text, this.pos, textSize,
6823
+ this.textColor, 0, undefined, this.align, this.font);
6824
+ }
6825
+ }
6826
+
6827
+ ///////////////////////////////////////////////////////////////////////////////
6828
+ /**
6829
+ * UICheckbox - A UI object that acts as a checkbox
6830
+ * @extends UIObject
6831
+ */
6832
+ class UICheckbox extends UIObject
6833
+ {
6834
+ /** Create a UICheckbox object
6835
+ * @param {Vector2} [pos]
6836
+ * @param {Vector2} [size]
6837
+ * @param {boolean} [checked]
6838
+ */
6839
+ constructor(pos, size, checked=false)
6840
+ {
6841
+ super(pos, size);
6842
+
6843
+ /** @property {boolean} */
6844
+ this.checked = checked;
6845
+ }
6846
+ onPress()
6847
+ {
6848
+ this.checked = !this.checked;
6849
+ this.onChange();
6850
+ }
6851
+ render()
6852
+ {
6853
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6854
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor);
6855
+ if (this.checked)
6856
+ {
6857
+ // draw an X if checked
6858
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor);
6859
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor);
6860
+ }
6861
+ }
6862
+ }
6863
+
6864
+ ///////////////////////////////////////////////////////////////////////////////
6865
+ /**
6866
+ * UIScrollbar - A UI object that acts as a scrollbar
6867
+ * @extends UIObject
6868
+ */
6869
+ class UIScrollbar extends UIObject
6870
+ {
6871
+ /** Create a UIScrollbar object
6872
+ * @param {Vector2} [pos]
6873
+ * @param {Vector2} [size]
6874
+ * @param {number} [value]
6875
+ * @param {string} [text]
6876
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6877
+ * @param {Color} [handleColor=WHITE]
6878
+ */
6879
+ constructor(pos, size, value=.5, text='', color=uiSystem.defaultButtonColor, handleColor=WHITE)
6880
+ {
6881
+ super(pos, size);
6882
+
6883
+ /** @property {number} */
6884
+ this.value = value;
6885
+ /** @property {string} */
6886
+ this.text = text;
6887
+ this.color = color;
6888
+ this.handleColor = handleColor;
6889
+ }
6890
+ update()
6891
+ {
6892
+ super.update();
6893
+ if (this.mouseIsHeld)
6894
+ {
6895
+ const handleSize = vec2(this.size.y);
6896
+ const handleWidth = this.size.x - handleSize.x;
6897
+ const p1 = this.pos.x - handleWidth/2;
6898
+ const p2 = this.pos.x + handleWidth/2;
6899
+ const oldValue = this.value;
6900
+ this.value = percent(mousePosScreen.x, p1, p2);
6901
+ this.value == oldValue || this.onChange();
6902
+ }
6903
+ }
6904
+ render()
6905
+ {
6906
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
6907
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6908
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
6909
+
6910
+ const handleSize = vec2(this.size.y);
6911
+ const handleWidth = this.size.x - handleSize.x;
6912
+ const p1 = this.pos.x - handleWidth/2;
6913
+ const p2 = this.pos.x + handleWidth/2;
6914
+ const handlePos = vec2(lerp(this.value, p1, p2), this.pos.y);
6915
+ const barColor = this.mouseIsHeld ? this.color : this.handleColor;
6916
+ uiSystem.drawRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor);
6917
+
6918
+ const textSize = vec2(this.size.x, this.size.y*.8);
6919
+ uiSystem.drawText(this.text, this.pos, textSize,
6920
+ this.textColor, 0, undefined, this.align, this.font);
6921
+ }
6922
+ }
6923
+
6924
+ /**
6925
+ * LittleJS Box2D Physics Plugin
6926
+ * - Box2dObject extends EngineObject with Box2D physics
6927
+ * - Call box2dEngineInit() to start instead of normal engineInit()
6928
+ * - You will also need to include box2d.wasm.js
6929
+ * - Uses box2d.js super fast web assembly port of Box2D
6930
+ * - More info: https://github.com/kripken/box2d.js
6931
+ * - Fully wraps everything in Box2d
6932
+ * - Functions to create polygon, circle, and edge shapes
6933
+ * - Raycasting and querying
6934
+ * - Joint creation
6935
+ * - Contact begin and end callbacks
6936
+ * - Debug physics drawing
6937
+ */
6938
+
6939
+
6940
+
6941
+ /** Global Box2d Plugin object
6942
+ * @type {Box2dPlugin} */
6943
+ let box2d;
6944
+
6945
+ /** Enable Box2D debug drawing
6946
+ * @type {boolean}
6947
+ * @default */
6948
+ let box2dDebug = false;
6949
+
6950
+ /** Enable Box2D debug drawing
6951
+ * @param {boolean} enable */
6952
+ function box2dSetDebug(enable) { box2dDebug = enable; }
6953
+
6954
+ ///////////////////////////////////////////////////////////////////////////////
6955
+ /**
6956
+ * Box2D Object - extend with your own custom physics objects
6957
+ * - A LittleJS object with Box2D physics
6958
+ * - Each object has a Box2D body which can have multiple fixtures and joints
6959
+ * - Provides interface for Box2D body and fixture functions
6960
+ * @extends EngineObject
6961
+ */
6962
+ class Box2dObject extends EngineObject
6963
+ {
6964
+ /** Create a LittleJS object with Box2d physics
6965
+ * @param {Vector2} [pos]
6966
+ * @param {Vector2} [size]
6967
+ * @param {TileInfo} [tileInfo]
6968
+ * @param {number} [angle]
6969
+ * @param {Color} [color]
6970
+ * @param {number} [bodyType]
6971
+ * @param {number} [renderOrder] */
6972
+ constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
6973
+ {
6974
+ super(pos, size, tileInfo, angle, color, renderOrder);
6975
+
6976
+ // create physics body
6977
+ const bodyDef = new box2d.instance.b2BodyDef();
6978
+ bodyDef.set_type(bodyType);
6979
+ bodyDef.set_position(box2d.vec2dTo(pos));
6980
+ bodyDef.set_angle(-angle);
6981
+ this.body = box2d.world.CreateBody(bodyDef);
6982
+ this.body.object = this;
6983
+ this.outlineColor = BLACK;
6984
+ }
6985
+
6986
+ /** Destroy this object and it's physics body */
6987
+ destroy()
6988
+ {
6989
+ // destroy physics body, fixtures, and joints
6990
+ this.body && box2d.world.DestroyBody(this.body);
6991
+ this.body = 0;
6992
+ super.destroy();
6993
+ }
6994
+
6995
+ /** Copy box2d update sim data */
6996
+ update()
6997
+ {
6998
+ // use box2d physics update
6999
+ this.pos = box2d.vec2From(this.body.GetPosition());
7000
+ this.angle = -this.body.GetAngle();
7001
+ }
7002
+
7003
+ /** Render the object, uses box2d drawing if no tile info exists */
7004
+ render()
7005
+ {
7006
+ // use default render or draw fixtures
7007
+ if (this.tileInfo)
7008
+ super.render();
7009
+ else
7010
+ this.drawFixtures(this.color, this.outlineColor, this.lineWidth, mainContext);
7011
+ }
7012
+
7013
+ /** Render debug info */
7014
+ renderDebugInfo()
7015
+ {
7016
+ const isAsleep = !this.getIsAwake();
7017
+ const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
7018
+ const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
7019
+ this.drawFixtures(color);
7020
+ }
7021
+
7022
+ /** Draws all this object's fixtures
7023
+ * @param {Color} [color]
7024
+ * @param {Color} [outlineColor]
7025
+ * @param {number} [lineWidth]
7026
+ * @param {CanvasRenderingContext2D} [context] */
7027
+ drawFixtures(color=WHITE, outlineColor, lineWidth=.1, context)
7028
+ {
7029
+ this.getFixtureList().forEach(fixture=>
7030
+ box2d.drawFixture(fixture, this.pos, this.angle, color, outlineColor, lineWidth, context));
7031
+ }
7032
+
7033
+ ///////////////////////////////////////////////////////////////////////////////
7034
+ // physics contact callbacks
7035
+
7036
+ /** Called when a contact begins
7037
+ * @param {Box2dObject} otherObject */
7038
+ beginContact(otherObject) {}
7039
+
7040
+ /** Called when a contact ends
7041
+ * @param {Box2dObject} otherObject */
7042
+ endContact(otherObject) {}
7043
+
7044
+ ///////////////////////////////////////////////////////////////////////////////
7045
+ // physics fixtures and shapes
7046
+
7047
+ /** Add a shape fixture to the body
7048
+ * @param {Object} shape
7049
+ * @param {number} [density]
7050
+ * @param {number} [friction]
7051
+ * @param {number} [restitution]
7052
+ * @param {boolean} [isSensor] */
7053
+ addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
7054
+ {
7055
+ const fd = new box2d.instance.b2FixtureDef();
7056
+ fd.set_shape(shape);
7057
+ fd.set_density(density);
7058
+ fd.set_friction(friction);
7059
+ fd.set_restitution(restitution);
7060
+ fd.set_isSensor(isSensor);
7061
+ return this.body.CreateFixture(fd);
7062
+ }
7063
+
7064
+ /** Add a box shape to the body
7065
+ * @param {Vector2} [size]
7066
+ * @param {Vector2} [offset]
7067
+ * @param {number} [angle]
7068
+ * @param {number} [density]
7069
+ * @param {number} [friction]
7070
+ * @param {number} [restitution]
7071
+ * @param {boolean} [isSensor] */
7072
+ addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
7073
+ {
7074
+ const shape = new box2d.instance.b2PolygonShape();
7075
+ shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
7076
+ return this.addShape(shape, density, friction, restitution, isSensor);
7077
+ }
7078
+
7079
+ /** Add a polygon shape to the body
7080
+ * @param {Array<Vector2>} points
7081
+ * @param {number} [density]
7082
+ * @param {number} [friction]
7083
+ * @param {number} [restitution]
7084
+ * @param {boolean} [isSensor] */
7085
+ addPoly(points, density, friction, restitution, isSensor)
7086
+ {
7087
+ function box2dCreatePolygonShape(points)
7088
+ {
7089
+ function box2dCreatePointList(points)
7090
+ {
7091
+ const buffer = box2d.instance._malloc(points.length * 8);
7092
+ for (let i=0, offset=0; i<points.length; ++i)
7093
+ {
7094
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x;
7095
+ offset += 4;
7096
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
7097
+ offset += 4;
7098
+ }
7099
+ return box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
7100
+ }
7101
+
7102
+ ASSERT(3 <= points.length && points.length <= 8);
7103
+ const shape = new box2d.instance.b2PolygonShape();
7104
+ const box2dPoints = box2dCreatePointList(points);
7105
+ shape.Set(box2dPoints, points.length);
7106
+ return shape;
7107
+ }
7108
+
7109
+ const shape = box2dCreatePolygonShape(points);
7110
+ return this.addShape(shape, density, friction, restitution, isSensor);
7111
+ }
7112
+
7113
+ /** Add a regular polygon shape to the body
7114
+ * @param {number} [diameter]
7115
+ * @param {number} [sides]
7116
+ * @param {number} [density]
7117
+ * @param {number} [friction]
7118
+ * @param {number} [restitution]
7119
+ * @param {boolean} [isSensor] */
7120
+ addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
7121
+ {
7122
+ const points = [];
7123
+ const radius = diameter/2;
7124
+ for (let i=sides; i--;)
7125
+ points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
7126
+ return this.addPoly(points, density, friction, restitution, isSensor);
7127
+ }
7128
+
7129
+ /** Add a random polygon shape to the body
7130
+ * @param {number} [diameter]
7131
+ * @param {number} [density]
7132
+ * @param {number} [friction]
7133
+ * @param {number} [restitution]
7134
+ * @param {boolean} [isSensor] */
7135
+ addRandomPoly(diameter=1, density, friction, restitution, isSensor)
7136
+ {
7137
+ const sides = randInt(3, 9);
7138
+ const points = [];
7139
+ const radius = diameter/2;
7140
+ for (let i=sides; i--;)
7141
+ points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2));
7142
+ return this.addPoly(points, density, friction, restitution, isSensor);
7143
+ }
7144
+
7145
+ /** Add a circle shape to the body
7146
+ * @param {number} [diameter]
7147
+ * @param {Vector2} [offset]
7148
+ * @param {number} [density]
7149
+ * @param {number} [friction]
7150
+ * @param {number} [restitution]
7151
+ * @param {boolean} [isSensor] */
7152
+ addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
7153
+ {
7154
+ const shape = new box2d.instance.b2CircleShape();
7155
+ shape.set_m_p(box2d.vec2dTo(offset));
7156
+ shape.set_m_radius(diameter/2);
7157
+ return this.addShape(shape, density, friction, restitution, isSensor);
7158
+ }
7159
+
7160
+ /** Add an edge shape to the body
7161
+ * @param {Vector2} point1
7162
+ * @param {Vector2} point2
7163
+ * @param {number} [density]
7164
+ * @param {number} [friction]
7165
+ * @param {number} [restitution]
7166
+ * @param {boolean} [isSensor] */
7167
+ addEdge(point1, point2, density, friction, restitution, isSensor)
7168
+ {
7169
+ const shape = new box2d.instance.b2EdgeShape();
7170
+ shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
7171
+ return this.addShape(shape, density, friction, restitution, isSensor);
7172
+ }
7173
+
7174
+ /** Add an edge loop to the body, an edge loop connects the end points
7175
+ * @param {Array<Vector2>} points
7176
+ * @param {number} [density]
7177
+ * @param {number} [friction]
7178
+ * @param {number} [restitution]
7179
+ * @param {boolean} [isSensor] */
7180
+ addEdgeLoop(points, density, friction, restitution, isSensor)
7181
+ {
7182
+ const fixtures = [];
7183
+ const getPoint = i=> points[mod(i,points.length)];
7184
+ for (let i=0; i<points.length; ++i)
7185
+ {
7186
+ const shape = new box2d.instance.b2EdgeShape();
7187
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
7188
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
7189
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
7190
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
7191
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
7192
+ fixtures.push(f);
7193
+ }
7194
+ return fixtures;
7195
+ }
7196
+
7197
+ /** Add an edge list to the body
7198
+ * @param {Array<Vector2>} points
7199
+ * @param {number} [density]
7200
+ * @param {number} [friction]
7201
+ * @param {number} [restitution]
7202
+ * @param {boolean} [isSensor] */
7203
+ addEdgeList(points, density, friction, restitution, isSensor)
7204
+ {
7205
+ const fixtures = [];
7206
+ for (let i=0; i<points.length-1; ++i)
7207
+ {
7208
+ const shape = new box2d.instance.b2EdgeShape();
7209
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
7210
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
7211
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
7212
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
7213
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
7214
+ fixtures.push(f);
7215
+ }
7216
+ return fixtures;
7217
+ }
7218
+
7219
+ ///////////////////////////////////////////////////////////////////////////////
7220
+ // physics get functions
7221
+
7222
+ /** Gets the center of mass
7223
+ * @return {Vector2} */
7224
+ getCenterOfMass() { return box2d.vec2From(this.body.GetWorldCenter()); }
7225
+
7226
+ /** Gets the linear velocity
7227
+ * @return {Vector2} */
7228
+ getLinearVelocity() { return box2d.vec2From(this.body.GetLinearVelocity()); }
7229
+
7230
+ /** Gets the angular velocity
7231
+ * @return {Vector2} */
7232
+ getAngularVelocity() { return this.body.GetAngularVelocity(); }
7233
+
7234
+ /** Gets the mass
7235
+ * @return {number} */
7236
+ getMass() { return this.body.GetMass(); }
7237
+
7238
+ /** Gets the rotational inertia
7239
+ * @return {number} */
7240
+ getInertia() { return this.body.GetInertia(); }
7241
+
7242
+ /** Check if this object is awake
7243
+ * @return {boolean} */
7244
+ getIsAwake() { return this.body.IsAwake(); }
7245
+
7246
+ /** Gets the physics body type
7247
+ * @return {number} */
7248
+ getBodyType() { return this.body.GetType(); }
7249
+
7250
+ ///////////////////////////////////////////////////////////////////////////////
7251
+ // physics set functions
7252
+
7253
+ /** Sets the position and angle
7254
+ * @param {Vector2} pos
7255
+ * @param {number} angle */
7256
+ setTransform(pos, angle)
7257
+ {
7258
+ this.pos = pos;
7259
+ this.angle = angle;
7260
+ this.body.SetTransform(box2d.vec2dTo(pos), angle);
7261
+ }
7262
+
7263
+ /** Sets the position
7264
+ * @param {Vector2} pos */
7265
+ setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
7266
+
7267
+ /** Sets the angle
7268
+ * @param {number} angle */
7269
+ setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
7270
+
7271
+ /** Sets the linear velocity
7272
+ * @param {Vector2} velocity */
7273
+ setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
7274
+
7275
+ /** Sets the angular velocity
7276
+ * @param {number} angularVelocity */
7277
+ setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
7278
+
7279
+ /** Sets the linear damping
7280
+ * @param {number} damping */
7281
+ setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
7282
+
7283
+ /** Sets the angular damping
7284
+ * @param {number} damping */
7285
+ setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
7286
+
7287
+ /** Sets the gravity scale
7288
+ * @param {number} [scale] */
7289
+ setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
7290
+
7291
+ /** Should this body be treated like a bullet for continuous collision detection?
7292
+ * @param {boolean} [isBullet] */
7293
+ setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
7294
+
7295
+ /** Set the sleep state of the body
7296
+ * @param {boolean} [isAwake] */
7297
+ setAwake(isAwake=true) { this.body.SetAwake(isAwake); }
7298
+
7299
+ /** Set the physics body type
7300
+ * @param {number} type */
7301
+ setBodyType(type) { this.body.SetType(type); }
7302
+
7303
+ /** Set whether the body is allowed to sleep
7304
+ * @param {boolean} [isAllowed] */
7305
+ setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
7306
+
7307
+ /** Set whether the body can rotate
7308
+ * @param {boolean} [isFixed] */
7309
+ setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
7310
+
7311
+ /** Set the center of mass of the body
7312
+ * @param {Vector2} center */
7313
+ setCenterOfMass(center) { this.setMassData(center) }
7314
+
7315
+ /** Set the mass of the body
7316
+ * @param {number} mass */
7317
+ setMass(mass) { this.setMassData(undefined, mass) }
7318
+
7319
+ /** Set the moment of inertia of the body
7320
+ * @param {number} momentOfInertia */
7321
+ setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
7322
+
7323
+ /** Reset the mass, center of mass, and moment */
7324
+ resetMassData() { this.body.ResetMassData(); }
7325
+
7326
+ /** Set the mass data of the body
7327
+ * @param {Vector2} [localCenter]
7328
+ * @param {number} [mass]
7329
+ * @param {number} [momentOfInertia] */
7330
+ setMassData(localCenter, mass, momentOfInertia)
7331
+ {
7332
+ const data = new box2d.instance.b2MassData();
7333
+ this.body.GetMassData(data);
7334
+ localCenter && data.set_center(box2d.vec2dTo(localCenter));
7335
+ mass && data.set_mass(mass);
7336
+ momentOfInertia && data.set_I(momentOfInertia);
7337
+ this.body.SetMassData(data);
7338
+ }
7339
+
7340
+ /** Set the collision filter data for this body
7341
+ * @param {number} [categoryBits]
7342
+ * @param {number} [ignoreCategoryBits]
7343
+ * @param {number} [groupIndex] */
7344
+ setFilterData(categoryBits=0, ignoreCategoryBits=0, groupIndex=0)
7345
+ {
7346
+ this.getFixtureList().forEach(fixture=>
7347
+ {
7348
+ const filter = fixture.GetFilterData();
7349
+ filter.set_categoryBits(categoryBits);
7350
+ filter.set_maskBits(0xffff & ~ignoreCategoryBits);
7351
+ filter.set_groupIndex(groupIndex);
7352
+ });
7353
+ }
7354
+
7355
+ /** Set if this body is a sensor
7356
+ * @param {boolean} [isSensor] */
7357
+ setSensor(isSensor=true)
7358
+ { this.getFixtureList().forEach(f=>f.SetSensor(isSensor)); }
7359
+
7360
+ ///////////////////////////////////////////////////////////////////////////////
7361
+ // physics force and torque functions
7362
+
7363
+ /** Apply force to this object
7364
+ * @param {Vector2} force
7365
+ * @param {Vector2} [pos] */
7366
+ applyForce(force, pos)
7367
+ {
7368
+ pos ||= this.getCenterOfMass();
7369
+ this.setAwake();
7370
+ this.body.ApplyForce(box2d.vec2dTo(force), box2d.vec2dTo(pos));
7371
+ }
7372
+
7373
+ /** Apply acceleration to this object
7374
+ * @param {Vector2} acceleration
7375
+ * @param {Vector2} [pos] */
7376
+ applyAcceleration(acceleration, pos)
7377
+ {
7378
+ pos ||= this.getCenterOfMass();
7379
+ this.setAwake();
7380
+ this.body.ApplyLinearImpulse(box2d.vec2dTo(acceleration), box2d.vec2dTo(pos));
7381
+ }
7382
+
7383
+ /** Apply torque to this object
7384
+ * @param {number} torque */
7385
+ applyTorque(torque)
7386
+ {
7387
+ this.setAwake();
7388
+ this.body.ApplyTorque(torque);
7389
+ }
7390
+
7391
+ /** Apply angular acceleration to this object
7392
+ * @param {number} acceleration */
7393
+ applyAngularAcceleration(acceleration)
7394
+ {
7395
+ this.setAwake();
7396
+ this.body.ApplyAngularImpulse(acceleration);
7397
+ }
7398
+
7399
+ ///////////////////////////////////////////////////////////////////////////////
7400
+ // lists of fixtures and joints
7401
+
7402
+ /** Check if this object has any fixtures
7403
+ * @return {boolean} */
7404
+ hasFixtures() { return !box2d.isNull(this.body.GetFixtureList()); }
7405
+
7406
+ /** Get list of fixtures for this object
7407
+ * @return {Array<Object>} */
7408
+ getFixtureList()
7409
+ {
7410
+ const fixtures = [];
7411
+ for (let fixture=this.body.GetFixtureList(); !box2d.isNull(fixture); )
7412
+ {
7413
+ fixtures.push(fixture);
7414
+ fixture = fixture.GetNext();
7415
+ }
7416
+ return fixtures;
7417
+ }
7418
+
7419
+ /** Check if this object has any joints
7420
+ * @return {boolean} */
7421
+ hasJoints() { return !box2d.isNull(this.body.GetJointList()); }
7422
+
7423
+ /** Get list of joints for this object
7424
+ * @return {Array<Object>} */
7425
+ getJointList()
7426
+ {
7427
+ const joints = [];
7428
+ for (let joint=this.body.GetJointList(); !box2d.isNull(joint); )
7429
+ {
7430
+ joints.push(joint);
7431
+ joint = joint.get_next();
7432
+ }
7433
+ return joints;
7434
+ }
7435
+ }
7436
+
7437
+ ///////////////////////////////////////////////////////////////////////////////
7438
+ /**
7439
+ * Box2D Raycast Result
7440
+ * - Holds results from a box2d raycast queries
7441
+ * - Automatically created by box2d raycast functions
7442
+ */
7443
+ class Box2dRaycastResult
7444
+ {
7445
+ /** Create a raycast result
7446
+ * @param {Object} fixture
7447
+ * @param {Vector2} point
7448
+ * @param {Vector2} normal
7449
+ * @param {number} fraction */
7450
+ constructor(fixture, point, normal, fraction)
7451
+ {
7452
+ /** @property {Box2dObject} - The box2d object */
7453
+ this.object = fixture.GetBody().object;
7454
+ /** @property {Object} - The fixture that was hit */
7455
+ this.fixture = fixture;
7456
+ /** @property {Vector2} - The hit point */
7457
+ this.point = point;
7458
+ /** @property {Vector2} - The hit normal */
7459
+ this.normal = normal;
7460
+ /** @property {number} - Distance fraction at the point of intersection */
7461
+ this.fraction = fraction;
7462
+ }
7463
+ }
7464
+
7465
+ ///////////////////////////////////////////////////////////////////////////////
7466
+ /**
7467
+ * Box2D Joint
7468
+ * - Base class for Box2D joints
7469
+ * - A joint is used to connect objects together
7470
+ */
7471
+ class Box2dJoint
7472
+ {
7473
+ /** Create a box2d joint, the base class is not intended to be used directly
7474
+ * @param {Object} jointDef */
7475
+ constructor(jointDef)
7476
+ {
7477
+ this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
7478
+ }
7479
+
7480
+ /** Destroy this joint */
7481
+ destroy() { box2d.world.DestroyJoint(this.box2dJoint); this.box2dJoint = 0; }
7482
+
7483
+ /** Get the first object attached to this joint
7484
+ * @return {Box2dObject} */
7485
+ getObjectA() { return this.box2dJoint.GetBodyA().object; }
7486
+
7487
+ /** Get the second object attached to this joint
7488
+ * @return {Box2dObject} */
7489
+ getObjectB() { return this.box2dJoint.GetBodyB().object; }
7490
+
7491
+ /** Get the first anchor for this joint in world coordinates
7492
+ * @return {Vector2} */
7493
+ getAnchorA() { return box2d.vec2From(this.box2dJoint.GetAnchorA());}
7494
+
7495
+ /** Get the second anchor for this joint in world coordinates
7496
+ * @return {Vector2} */
7497
+ getAnchorB() { return box2d.vec2From(this.box2dJoint.GetAnchorB());}
7498
+
7499
+ /** Get the reaction force on bodyB at the joint anchor given a time step
7500
+ * @param {number} time
7501
+ * @return {Vector2} */
7502
+ getReactionForce(time) { return box2d.vec2From(this.box2dJoint.GetReactionForce(1/time));}
7503
+
7504
+ /** Get the reaction torque on bodyB in N*m given a time step
7505
+ * @param {number} time
7506
+ * @return {number} */
7507
+ getReactionTorque(time) { return this.box2dJoint.GetReactionTorque(1/time);}
7508
+
7509
+ /** Check if the connected bodies should collide
7510
+ * @return {boolean} */
7511
+ getCollideConnected() { return this.box2dJoint.getCollideConnected();}
7512
+
7513
+ /** Check if either connected body is active
7514
+ * @return {boolean} */
7515
+ isActive() { return this.box2dJoint.IsActive();}
7516
+ }
7517
+
7518
+ ///////////////////////////////////////////////////////////////////////////////
7519
+ /**
7520
+ * Box2D Target Joint, also known as a mouse joint
7521
+ * - Used to make a point on a object track a specific world point target
7522
+ * - This a soft constraint with a max force
7523
+ * - This allows the constraint to stretch and without applying huge forces
7524
+ * @extends Box2dJoint
7525
+ */
7526
+ class Box2dTargetJoint extends Box2dJoint
7527
+ {
7528
+ /** Create a target joint
7529
+ * @param {Box2dObject} object
7530
+ * @param {Box2dObject} fixedObject
7531
+ * @param {Vector2} worldPos */
7532
+ constructor(object, fixedObject, worldPos)
7533
+ {
7534
+ object.setAwake();
7535
+ const jointDef = new box2d.instance.b2MouseJointDef();
7536
+ jointDef.set_bodyA(fixedObject.body);
7537
+ jointDef.set_bodyB(object.body);
7538
+ jointDef.set_target(box2d.vec2dTo(worldPos));
7539
+ jointDef.set_maxForce(2e3 * object.getMass());
7540
+ super(jointDef);
7541
+ }
7542
+
7543
+ /** Set the target point in world coordinates
7544
+ * @param {Vector2} pos */
7545
+ setTarget(pos) { this.box2dJoint.SetTarget(box2d.vec2dTo(pos)); }
7546
+
7547
+ /** Get the target point in world coordinates
7548
+ * @return {Vector2} */
7549
+ getTarget(){ return box2d.vec2From(this.box2dJoint.GetTarget()); }
7550
+
7551
+ /** Sets the maximum force in Newtons
7552
+ * @param {number} force */
7553
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
7554
+
7555
+ /** Gets the maximum force in Newtons
7556
+ * @return {number} */
7557
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
7558
+
7559
+ /** Sets the joint frequency in Hertz
7560
+ * @param {number} hz */
7561
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7562
+
7563
+ /** Gets the joint frequency in Hertz
7564
+ * @return {number} */
7565
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7566
+ }
7567
+
7568
+ ///////////////////////////////////////////////////////////////////////////////
7569
+ /**
7570
+ * Box2D Distance Joint
7571
+ * - Constrains two points on two objects to remain at a fixed distance
7572
+ * - You can view this as a massless, rigid rod
7573
+ * @extends Box2dJoint
7574
+ */
7575
+ class Box2dDistanceJoint extends Box2dJoint
7576
+ {
7577
+ /** Create a distance joint
7578
+ * @param {Box2dObject} objectA
7579
+ * @param {Box2dObject} objectB
7580
+ * @param {Vector2} anchorA
7581
+ * @param {Vector2} anchorB
7582
+ * @param {boolean} [collide] */
7583
+ constructor(objectA, objectB, anchorA, anchorB, collide=false)
7584
+ {
7585
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7586
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7587
+ const localAnchorA = objectA.worldToLocal(anchorA);
7588
+ const localAnchorB = objectB.worldToLocal(anchorB);
7589
+ const jointDef = new box2d.instance.b2DistanceJointDef();
7590
+ jointDef.set_bodyA(objectA.body);
7591
+ jointDef.set_bodyB(objectB.body);
7592
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7593
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7594
+ jointDef.set_length(anchorA.distance(anchorB));
7595
+ jointDef.set_collideConnected(collide);
7596
+ super(jointDef);
7597
+ }
7598
+
7599
+ /** Get the local anchor point relative to objectA's origin
7600
+ * @return {Vector2} */
7601
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7602
+
7603
+ /** Get the local anchor point relative to objectB's origin
7604
+ * @return {Vector2} */
7605
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7606
+
7607
+ /** Set the length of the joint
7608
+ * @param {number} length */
7609
+ setLength(length) { this.box2dJoint.SetLength(length); }
7610
+
7611
+ /** Get the length of the joint
7612
+ * @return {number} */
7613
+ getLength() { return this.box2dJoint.GetLength(); }
7614
+
7615
+ /** Set the frequency in Hertz
7616
+ * @param {number} hz */
7617
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7618
+
7619
+ /** Get the frequency in Hertz
7620
+ * @return {number} */
7621
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7622
+
7623
+ /** Set the damping ratio
7624
+ * @param {number} ratio */
7625
+ setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); }
7626
+
7627
+ /** Get the damping ratio
7628
+ * @return {number} */
7629
+ getDampingRatio() { return this.box2dJoint.GetDampingRatio(); }
7630
+ }
7631
+
7632
+ ///////////////////////////////////////////////////////////////////////////////
7633
+ /**
7634
+ * Box2D Pin Joint
7635
+ * - Pins two objects together at a point
7636
+ * @extends Box2dDistanceJoint
7637
+ */
7638
+ class Box2dPinJoint extends Box2dDistanceJoint
7639
+ {
7640
+ /** Create a pin joint
7641
+ * @param {Box2dObject} objectA
7642
+ * @param {Box2dObject} objectB
7643
+ * @param {Vector2} [pos]
7644
+ * @param {boolean} [collide] */
7645
+ constructor(objectA, objectB, pos=objectA.pos, collide=false)
7646
+ {
7647
+ super(objectA, objectB, undefined, pos, collide);
7648
+ }
7649
+ }
7650
+
7651
+ ///////////////////////////////////////////////////////////////////////////////
7652
+ /**
7653
+ * Box2D Rope Joint
7654
+ * - Enforces a maximum distance between two points on two objects
7655
+ * @extends Box2dJoint
7656
+ */
7657
+ class Box2dRopeJoint extends Box2dJoint
7658
+ {
7659
+ /** Create a rope joint
7660
+ * @param {Box2dObject} objectA
7661
+ * @param {Box2dObject} objectB
7662
+ * @param {Vector2} anchorA
7663
+ * @param {Vector2} anchorB
7664
+ * @param {number} extraLength
7665
+ * @param {boolean} [collide] */
7666
+ constructor(objectA, objectB, anchorA, anchorB, extraLength=0, collide=false)
7667
+ {
7668
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7669
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7670
+ const localAnchorA = objectA.worldToLocal(anchorA);
7671
+ const localAnchorB = objectB.worldToLocal(anchorB);
7672
+ const jointDef = new box2d.instance.b2RopeJointDef();
7673
+ jointDef.set_bodyA(objectA.body);
7674
+ jointDef.set_bodyB(objectB.body);
7675
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7676
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7677
+ jointDef.set_maxLength(anchorA.distance(anchorB)+extraLength);
7678
+ jointDef.set_collideConnected(collide);
7679
+ super(jointDef);
7680
+ }
7681
+
7682
+ /** Get the local anchor point relative to objectA's origin
7683
+ * @return {Vector2} */
7684
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7685
+
7686
+ /** Get the local anchor point relative to objectB's origin
7687
+ * @return {Vector2} */
7688
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7689
+
7690
+ /** Set the max length of the joint
7691
+ * @param {number} length */
7692
+ setMaxLength(length) { this.box2dJoint.SetMaxLength(length); }
7693
+
7694
+ /** Get the max length of the joint
7695
+ * @return {number} */
7696
+ getMaxLength() { return this.box2dJoint.GetMaxLength(); }
7697
+ }
7698
+
7699
+ ///////////////////////////////////////////////////////////////////////////////
7700
+ /**
7701
+ * Box2D Revolute Joint
7702
+ * - Constrains two objects to share a point while they are free to rotate around the point
7703
+ * - The relative rotation about the shared point is the joint angle
7704
+ * - You can limit the relative rotation with a joint limit
7705
+ * - You can use a motor to drive the relative rotation about the shared point
7706
+ * - A maximum motor torque is provided so that infinite forces are not generated
7707
+ * @extends Box2dJoint
7708
+ */
7709
+ class Box2dRevoluteJoint extends Box2dJoint
7710
+ {
7711
+ /** Create a revolute joint
7712
+ * @param {Box2dObject} objectA
7713
+ * @param {Box2dObject} objectB
7714
+ * @param {Vector2} anchor
7715
+ * @param {boolean} [collide] */
7716
+ constructor(objectA, objectB, anchor, collide=false)
7717
+ {
7718
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7719
+ const localAnchorA = objectA.worldToLocal(anchor);
7720
+ const localAnchorB = objectB.worldToLocal(anchor);
7721
+ const jointDef = new box2d.instance.b2RevoluteJointDef();
7722
+ jointDef.set_bodyA(objectA.body);
7723
+ jointDef.set_bodyB(objectB.body);
7724
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7725
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7726
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7727
+ jointDef.set_collideConnected(collide);
7728
+ super(jointDef);
7729
+ }
7730
+
7731
+ /** Get the local anchor point relative to objectA's origin
7732
+ * @return {Vector2} */
7733
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7734
+
7735
+ /** Get the local anchor point relative to objectB's origin
7736
+ * @return {Vector2} */
7737
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7738
+
7739
+ /** Get the reference angle, objectB angle minus objectA angle in the reference state
7740
+ * @return {number} */
7741
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7742
+
7743
+ /** Get the current joint angle
7744
+ * @return {number} */
7745
+ getJointAngle() { return this.box2dJoint.GetJointAngle(); }
7746
+
7747
+ /** Get the current joint angle speed in radians per second
7748
+ * @return {number} */
7749
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7750
+
7751
+ /** Is the joint limit enabled?
7752
+ * @return {boolean} */
7753
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
7754
+
7755
+ /** Enable/disable the joint limit
7756
+ * @param {boolean} [enable] */
7757
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
7758
+
7759
+ /** Get the lower joint limit
7760
+ * @return {number} */
7761
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
7762
+
7763
+ /** Get the upper joint limit
7764
+ * @return {number} */
7765
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
7766
+
7767
+ /** Set the joint limits
7768
+ * @param {number} min
7769
+ * @param {number} max */
7770
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
7771
+
7772
+ /** Is the joint motor enabled?
7773
+ * @return {boolean} */
7774
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7775
+
7776
+ /** Enable/disable the joint motor
7777
+ * @param {boolean} [enable] */
7778
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7779
+
7780
+ /** Set the motor speed
7781
+ * @param {number} speed */
7782
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7783
+
7784
+ /** Get the motor speed
7785
+ * @return {number} */
7786
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7787
+
7788
+ /** Set the motor torque
7789
+ * @param {number} torque */
7790
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
7791
+
7792
+ /** Get the max motor torque
7793
+ * @return {number} */
7794
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
7795
+
7796
+ /** Get the motor torque given a time step
7797
+ * @param {number} time
7798
+ * @return {number} */
7799
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
7800
+ }
7801
+
7802
+ ///////////////////////////////////////////////////////////////////////////////
7803
+ /**
7804
+ * Box2D Gear Joint
7805
+ * - A gear joint is used to connect two joints together
7806
+ * - Either joint can be a revolute or prismatic joint
7807
+ * - You specify a gear ratio to bind the motions together
7808
+ * @extends Box2dJoint
7809
+ */
7810
+ class Box2dGearJoint extends Box2dJoint
7811
+ {
7812
+ /** Create a gear joint
7813
+ * @param {Box2dObject} objectA
7814
+ * @param {Box2dObject} objectB
7815
+ * @param {Box2dJoint} joint1
7816
+ * @param {Box2dJoint} joint2
7817
+ * @param {ratio} [ratio] */
7818
+ constructor(objectA, objectB, joint1, joint2, ratio=1)
7819
+ {
7820
+ const jointDef = new box2d.instance.b2GearJointDef();
7821
+ jointDef.set_bodyA(objectA.body);
7822
+ jointDef.set_bodyB(objectB.body);
7823
+ jointDef.set_joint1(joint1.box2dJoint);
7824
+ jointDef.set_joint2(joint2.box2dJoint);
7825
+ jointDef.set_ratio(ratio);
7826
+ super(jointDef);
7827
+
7828
+ this.joint1 = joint1;
7829
+ this.joint2 = joint2;
7830
+ }
7831
+
7832
+ /** Get the first joint
7833
+ * @return {Box2dJoint} */
7834
+ getJoint1() { return this.joint1; }
7835
+
7836
+ /** Get the second joint
7837
+ * @return {Box2dJoint} */
7838
+ getJoint2() { return this.joint2; }
7839
+
7840
+ /** Set the gear ratio
7841
+ * @param {number} ratio */
7842
+ setRatio(ratio) { return this.box2dJoint.SetRatio(ratio); }
7843
+
7844
+ /** Get the gear ratio
7845
+ * @return {number} */
7846
+ getRatio() { return this.box2dJoint.GetRatio(); }
7847
+ }
7848
+
7849
+ ///////////////////////////////////////////////////////////////////////////////
7850
+ /**
7851
+ * Box2D Prismatic Joint
7852
+ * - Provides one degree of freedom: translation along an axis fixed in objectA
7853
+ * - Relative rotation is prevented
7854
+ * - You can use a joint limit to restrict the range of motion
7855
+ * - You can use a joint motor to drive the motion or to model joint friction
7856
+ * @extends Box2dJoint
7857
+ */
7858
+ class Box2dPrismaticJoint extends Box2dJoint
7859
+ {
7860
+ /** Create a prismatic joint
7861
+ * @param {Box2dObject} objectA
7862
+ * @param {Box2dObject} objectB
7863
+ * @param {Vector2} anchor
7864
+ * @param {Vector2} worldAxis
7865
+ * @param {boolean} [collide] */
7866
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
7867
+ {
7868
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7869
+ const localAnchorA = objectA.worldToLocal(anchor);
7870
+ const localAnchorB = objectB.worldToLocal(anchor);
7871
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
7872
+ const jointDef = new box2d.instance.b2PrismaticJointDef();
7873
+ jointDef.set_bodyA(objectA.body);
7874
+ jointDef.set_bodyB(objectB.body);
7875
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7876
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7877
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
7878
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7879
+ jointDef.set_collideConnected(collide);
7880
+ super(jointDef);
7881
+ }
7882
+
7883
+ /** Get the local anchor point relative to objectA's origin
7884
+ * @return {Vector2} */
7885
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7886
+
7887
+ /** Get the local anchor point relative to objectB's origin
7888
+ * @return {Vector2} */
7889
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7890
+
7891
+ /** Get the local joint axis relative to bodyA
7892
+ * @return {Vector2} */
7893
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
7894
+
7895
+ /** Get the reference angle
7896
+ * @return {number} */
7897
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7898
+
7899
+ /** Get the current joint translation
7900
+ * @return {number} */
7901
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
7902
+
7903
+ /** Get the current joint translation speed
7904
+ * @return {number} */
7905
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7906
+
7907
+ /** Is the joint limit enabled?
7908
+ * @return {boolean} */
7909
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
7910
+
7911
+ /** Enable/disable the joint limit
7912
+ * @param {boolean} [enable] */
7913
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
7914
+
7915
+ /** Get the lower joint limit
7916
+ * @return {number} */
7917
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
7918
+
7919
+ /** Get the upper joint limit
7920
+ * @return {number} */
7921
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
7922
+
7923
+ /** Set the joint limits
7924
+ * @param {number} min
7925
+ * @param {number} max */
7926
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
7927
+
7928
+ /** Is the motor enabled?
7929
+ * @return {boolean} */
7930
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7931
+
7932
+ /** Enable/disable the joint motor
7933
+ * @param {boolean} [enable] */
7934
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7935
+
7936
+ /** Set the motor speed
7937
+ * @param {number} speed */
7938
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7939
+
7940
+ /** Get the motor speed
7941
+ * @return {number} */
7942
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7943
+
7944
+ /** Set the maximum motor force
7945
+ * @param {number} force */
7946
+ setMaxMotorForce(force) { return this.box2dJoint.SetMaxMotorForce(force); }
7947
+
7948
+ /** Get the maximum motor force
7949
+ * @return {number} */
7950
+ getMaxMotorForce() { return this.box2dJoint.GetMaxMotorForce(); }
7951
+
7952
+ /** Get the motor force given a time step
7953
+ * @param {number} time
7954
+ * @return {number} */
7955
+ getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
7956
+ }
7957
+
7958
+ ///////////////////////////////////////////////////////////////////////////////
7959
+ /**
7960
+ * Box2D Wheel Joint
7961
+ * - Provides two degrees of freedom: translation along an axis fixed in objectA and rotation
7962
+ * - You can use a joint limit to restrict the range of motion
7963
+ * - You can use a joint motor to drive the motion or to model joint friction
7964
+ * - This joint is designed for vehicle suspensions
7965
+ * @extends Box2dJoint
7966
+ */
7967
+ class Box2dWheelJoint extends Box2dJoint
7968
+ {
7969
+ /** Create a wheel joint
7970
+ * @param {Box2dObject} objectA
7971
+ * @param {Box2dObject} objectB
7972
+ * @param {Vector2} anchor
7973
+ * @param {Vector2} worldAxis
7974
+ * @param {boolean} [collide] */
7975
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
7976
+ {
7977
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7978
+ const localAnchorA = objectA.worldToLocal(anchor);
7979
+ const localAnchorB = objectB.worldToLocal(anchor);
7980
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
7981
+ const jointDef = new box2d.instance.b2WheelJointDef();
7982
+ jointDef.set_bodyA(objectA.body);
7983
+ jointDef.set_bodyB(objectB.body);
7984
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7985
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7986
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
7987
+ jointDef.set_collideConnected(collide);
7988
+ super(jointDef);
7989
+ }
7990
+
7991
+ /** Get the local anchor point relative to objectA's origin
7992
+ * @return {Vector2} */
7993
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7994
+
7995
+ /** Get the local anchor point relative to objectB's origin
7996
+ * @return {Vector2} */
7997
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7998
+
7999
+ /** Get the local joint axis relative to bodyA
8000
+ * @return {Vector2} */
8001
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
8002
+
8003
+ /** Get the current joint translation
8004
+ * @return {number} */
8005
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
8006
+
8007
+ /** Get the current joint translation speed
8008
+ * @return {number} */
8009
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
8010
+
8011
+ /** Is the joint motor enabled?
8012
+ * @return {boolean} */
8013
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
8014
+
8015
+ /** Enable/disable the joint motor
8016
+ * @param {boolean} [enable] */
8017
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
8018
+
8019
+ /** Set the motor speed
8020
+ * @param {number} speed */
8021
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
8022
+
8023
+ /** Get the motor speed
8024
+ * @return {number} */
8025
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
8026
+
8027
+ /** Set the maximum motor torque
8028
+ * @param {number} torque */
8029
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
8030
+
8031
+ /** Get the max motor torque
8032
+ * @return {number} */
8033
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
8034
+
8035
+ /** Get the motor torque for a time step
8036
+ * @return {number} */
8037
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
8038
+
8039
+ /** Set the spring frequency in Hertz
8040
+ * @param {number} hz */
8041
+ setSpringFrequencyHz(hz) { return this.box2dJoint.SetSpringFrequencyHz(hz); }
8042
+
8043
+ /** Get the spring frequency in Hertz
8044
+ * @return {number} */
8045
+ getSpringFrequencyHz() { return this.box2dJoint.GetSpringFrequencyHz(); }
8046
+
8047
+ /** Set the spring damping ratio
8048
+ * @param {number} ratio */
8049
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
8050
+
8051
+ /** Get the spring damping ratio
8052
+ * @return {number} */
8053
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
8054
+ }
8055
+
8056
+ ///////////////////////////////////////////////////////////////////////////////
8057
+ /**
8058
+ * Box2D Weld Joint
8059
+ * - Glues two objects together
8060
+ * @extends Box2dJoint
8061
+ */
8062
+ class Box2dWeldJoint extends Box2dJoint
8063
+ {
8064
+ /** Create a weld joint
8065
+ * @param {Box2dObject} objectA
8066
+ * @param {Box2dObject} objectB
8067
+ * @param {Vector2} anchor
8068
+ * @param {boolean} [collide] */
8069
+ constructor(objectA, objectB, anchor, collide=false)
8070
+ {
8071
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
8072
+ const localAnchorA = objectA.worldToLocal(anchor);
8073
+ const localAnchorB = objectB.worldToLocal(anchor);
8074
+ const jointDef = new box2d.instance.b2WeldJointDef();
8075
+ jointDef.set_bodyA(objectA.body);
8076
+ jointDef.set_bodyB(objectB.body);
8077
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8078
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8079
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
8080
+ jointDef.set_collideConnected(collide);
8081
+ super(jointDef);
8082
+ }
8083
+
8084
+ /** Get the local anchor point relative to objectA's origin
8085
+ * @return {Vector2} */
8086
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
8087
+
8088
+ /** Get the local anchor point relative to objectB's origin
8089
+ * @return {Vector2} */
8090
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
8091
+
8092
+ /** Get the reference angle
8093
+ * @return {number} */
8094
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
8095
+
8096
+ /** Set the frequency in Hertz
8097
+ * @param {number} hz */
8098
+ setFrequency(hz) { return this.box2dJoint.SetFrequency(hz); }
8099
+
8100
+ /** Get the frequency in Hertz
8101
+ * @return {number} */
8102
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
8103
+
8104
+ /** Set the damping ratio
8105
+ * @param {number} ratio */
8106
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
8107
+
8108
+ /** Get the damping ratio
8109
+ * @return {number} */
8110
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
8111
+ }
8112
+
8113
+ ///////////////////////////////////////////////////////////////////////////////
8114
+ /**
8115
+ * Box2D Friction Joint
8116
+ * - Used to apply top-down friction
8117
+ * - Provides 2D translational friction and angular friction
8118
+ * @extends Box2dJoint
8119
+ */
8120
+ class Box2dFrictionJoint extends Box2dJoint
8121
+ {
8122
+ /** Create a friction joint
8123
+ * @param {Box2dObject} objectA
8124
+ * @param {Box2dObject} objectB
8125
+ * @param {Vector2} anchor
8126
+ * @param {boolean} [collide] */
8127
+ constructor(objectA, objectB, anchor, collide=false)
8128
+ {
8129
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
8130
+ const localAnchorA = objectA.worldToLocal(anchor);
8131
+ const localAnchorB = objectB.worldToLocal(anchor);
8132
+ const jointDef = new box2d.instance.b2FrictionJointDef();
8133
+ jointDef.set_bodyA(objectA.body);
8134
+ jointDef.set_bodyB(objectB.body);
8135
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8136
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8137
+ jointDef.set_collideConnected(collide);
8138
+ super(jointDef);
8139
+ }
8140
+
8141
+ /** Get the local anchor point relative to objectA's origin
8142
+ * @return {Vector2} */
8143
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
8144
+
8145
+ /** Get the local anchor point relative to objectB's origin
8146
+ * @return {Vector2} */
8147
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
8148
+
8149
+ /** Set the maximum friction force
8150
+ * @param {number} force */
8151
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
8152
+
8153
+ /** Get the maximum friction force
8154
+ * @return {number} */
8155
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
8156
+
8157
+ /** Set the maximum friction torque
8158
+ * @param {number} torque */
8159
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
8160
+
8161
+ /** Get the maximum friction torque
8162
+ * @return {number} */
8163
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
8164
+ }
8165
+
8166
+ ///////////////////////////////////////////////////////////////////////////////
8167
+ /**
8168
+ * Box2D Pulley Joint
8169
+ * - Connects to two objects and two fixed ground points
8170
+ * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
8171
+ * - The force transmitted is scaled by the ratio
8172
+ * @extends Box2dJoint
8173
+ */
8174
+ class Box2dPulleyJoint extends Box2dJoint
8175
+ {
8176
+ /** Create a pulley joint
8177
+ * @param {Box2dObject} objectA
8178
+ * @param {Box2dObject} objectB
8179
+ * @param {Vector2} groundAnchorA
8180
+ * @param {Vector2} groundAnchorB
8181
+ * @param {Vector2} anchorA
8182
+ * @param {Vector2} anchorB
8183
+ * @param {number} [ratio]
8184
+ * @param {boolean} [collide] */
8185
+ constructor(objectA, objectB, groundAnchorA, groundAnchorB, anchorA, anchorB, ratio=1, collide=false)
8186
+ {
8187
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
8188
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
8189
+ const localAnchorA = objectA.worldToLocal(anchorA);
8190
+ const localAnchorB = objectB.worldToLocal(anchorB);
8191
+ const jointDef = new box2d.instance.b2PulleyJointDef();
8192
+ jointDef.set_bodyA(objectA.body);
8193
+ jointDef.set_bodyB(objectB.body);
8194
+ jointDef.set_groundAnchorA(box2d.vec2dTo(groundAnchorA));
8195
+ jointDef.set_groundAnchorB(box2d.vec2dTo(groundAnchorB));
8196
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8197
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8198
+ jointDef.set_ratio(ratio);
8199
+ jointDef.set_lengthA(groundAnchorA.distance(anchorA));
8200
+ jointDef.set_lengthB(groundAnchorB.distance(anchorB));
8201
+ jointDef.set_collideConnected(collide);
8202
+ super(jointDef);
8203
+ }
8204
+
8205
+ /** Get the first ground anchor
8206
+ * @return {Vector2} */
8207
+ getGroundAnchorA() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorA()); }
8208
+
8209
+ /** Get the second ground anchor
8210
+ * @return {Vector2} */
8211
+ getGroundAnchorB() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorB()); }
8212
+
8213
+ /** Get the current length of the segment attached to objectA
8214
+ * @return {number} */
8215
+ getLengthA() { return this.box2dJoint.GetLengthA(); }
8216
+
8217
+ /** Get the current length of the segment attached to objectB
8218
+ * @return {number} */
8219
+ getLengthB(){ return this.box2dJoint.GetLengthB(); }
8220
+
8221
+ /** Get the pulley ratio
8222
+ * @return {number} */
8223
+ getRatio() { return this.box2dJoint.GetRatio(); }
8224
+
8225
+ /** Get the current length of the segment attached to objectA
8226
+ * @return {number} */
8227
+ getCurrentLengthA() { return this.box2dJoint.GetCurrentLengthA(); }
8228
+
8229
+ /** Get the current length of the segment attached to objectB
8230
+ * @return {number} */
8231
+ getCurrentLengthB() { return this.box2dJoint.GetCurrentLengthB(); }
8232
+ }
8233
+
8234
+ ///////////////////////////////////////////////////////////////////////////////
8235
+ /**
8236
+ * Box2D Motor Joint
8237
+ * - Controls the relative motion between two objects
8238
+ * - Typical usage is to control the movement of a object with respect to the ground
8239
+ * @extends Box2dJoint
8240
+ */
8241
+ class Box2dMotorJoint extends Box2dJoint
8242
+ {
8243
+ /** Create a motor joint
8244
+ * @param {Box2dObject} objectA
8245
+ * @param {Box2dObject} objectB */
8246
+ constructor(objectA, objectB)
8247
+ {
8248
+ const linearOffset = objectA.worldToLocal(box2d.vec2From(objectB.body.GetPosition()));
8249
+ const angularOffset = objectB.body.GetAngle() - objectA.body.GetAngle();
8250
+ const jointDef = new box2d.instance.b2MotorJointDef();
8251
+ jointDef.set_bodyA(objectA.body);
8252
+ jointDef.set_bodyB(objectB.body);
8253
+ jointDef.set_linearOffset(box2d.vec2dTo(linearOffset));
8254
+ jointDef.set_angularOffset(angularOffset);
8255
+ super(jointDef);
8256
+ }
8257
+
8258
+ /** Set the target linear offset, in frame A, in meters.
8259
+ * @param {Vector2} offset */
8260
+ setLinearOffset(offset) { this.box2dJoint.SetLinearOffset(box2d.vec2dTo(offset)); }
8261
+
8262
+ /** Get the target linear offset, in frame A, in meters.
8263
+ * @return {Vector2} */
8264
+ getLinearOffset() { return box2d.vec2From(this.box2dJoint.GetLinearOffset()); }
8265
+
8266
+ /** Set the target angular offset
8267
+ * @param {number} offset */
8268
+ setAngularOffset(offset) { this.box2dJoint.SetAngularOffset(offset); }
8269
+
8270
+ /** Get the target angular offset
8271
+ * @return {number} */
8272
+ getAngularOffset() { return this.box2dJoint.GetAngularOffset(); }
8273
+
8274
+ /** Set the maximum friction force
8275
+ * @param {number} force */
8276
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
8277
+
8278
+ /** Get the maximum friction force
8279
+ * @return {number} */
8280
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
8281
+
8282
+ /** Set the maximum torque
8283
+ * @param {number} torque */
8284
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
8285
+
8286
+ /** Get the maximum torque
8287
+ * @return {number} */
8288
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
8289
+
8290
+ /** Set the position correction factor in the range [0,1]
8291
+ * @param {number} factor */
8292
+ setCorrectionFactor(factor) { this.box2dJoint.SetCorrectionFactor(factor); }
8293
+
8294
+ /** Get the position correction factor in the range [0,1]
8295
+ * @return {number} */
8296
+ getCorrectionFactor() { return this.box2dJoint.GetCorrectionFactor(); }
8297
+ }
8298
+
8299
+ ///////////////////////////////////////////////////////////////////////////////
8300
+ /**
8301
+ * Box2D Global Object
8302
+ * - Wraps Box2d world and provides global functions
8303
+ */
8304
+ class Box2dPlugin
8305
+ {
8306
+ /** Create the global UI system object
8307
+ * @param {Object} instance */
8308
+ constructor(instance)
8309
+ {
8310
+ ASSERT(!box2d, 'Box2D already initialized');
8311
+ box2d = this;
8312
+ this.instance = instance;
8313
+ this.world = new box2d.instance.b2World();
8314
+
8315
+ /** @property {number} - Velocity iterations per update*/
8316
+ this.velocityIterations = 8;
8317
+ /** @property {number} - Position iterations per update*/
8318
+ this.positionIterations = 3;
8319
+ /** @property {number} - Static, zero mass, zero velocity, may be manually moved */
8320
+ this.bodyTypeStatic = instance.b2_staticBody;
8321
+ /** @property {number} - Kinematic, zero mass, non-zero velocity set by user, moved by solver */
8322
+ this.bodyTypeKinematic = instance.b2_kinematicBody;
8323
+ /** @property {number} - Dynamic, positive mass, non-zero velocity determined by forces, moved by solver */
8324
+ this.bodyTypeDynamic = instance.b2_dynamicBody;
8325
+
8326
+ // setup contact listener
8327
+ const listener = new box2d.instance.JSContactListener();
8328
+ listener.BeginContact = function(contactPtr)
8329
+ {
8330
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
8331
+ const fixtureA = contact.GetFixtureA();
8332
+ const fixtureB = contact.GetFixtureB();
8333
+ const objectA = fixtureA.GetBody().object;
8334
+ const objectB = fixtureB.GetBody().object;
8335
+ objectA.beginContact(objectB);
8336
+ objectB.beginContact(objectA);
8337
+ }
8338
+ listener.EndContact = function(contactPtr)
8339
+ {
8340
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
8341
+ const fixtureA = contact.GetFixtureA();
8342
+ const fixtureB = contact.GetFixtureB();
8343
+ const objectA = fixtureA.GetBody().object;
8344
+ const objectB = fixtureB.GetBody().object;
8345
+ objectA.endContact(objectB);
8346
+ objectB.endContact(objectA);
8347
+ };
8348
+ listener.PreSolve = function() {};
8349
+ listener.PostSolve = function() {};
8350
+ box2d.world.SetContactListener(listener);
8351
+ }
8352
+
8353
+ /** Step the physics world simulation
8354
+ * @param {number} [frames] */
8355
+ step(frames=1)
8356
+ {
8357
+ box2d.world.SetGravity(box2d.vec2dTo(gravity));
8358
+ for (let i=frames; i--;)
8359
+ box2d.world.Step(timeDelta, this.velocityIterations, this.positionIterations);
8360
+ }
8361
+
8362
+ ///////////////////////////////////////////////////////////////////////////////
8363
+ // raycasting and querying
8364
+
8365
+ /** raycast and return a list of all the results
8366
+ * @param {Vector2} start
8367
+ * @param {Vector2} end */
8368
+ raycastAll(start, end)
8369
+ {
8370
+ const raycastCallback = new box2d.instance.JSRayCastCallback();
8371
+ raycastCallback.ReportFixture = function(fixturePointer, point, normal, fraction)
8372
+ {
8373
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8374
+ point = box2d.vec2FromPointer(point);
8375
+ normal = box2d.vec2FromPointer(normal);
8376
+ raycastResults.push(new Box2dRaycastResult(fixture, point, normal, fraction));
8377
+ return 1; // continue getting results
8378
+ };
8379
+
8380
+ const raycastResults = [];
8381
+ box2d.world.RayCast(raycastCallback, box2d.vec2dTo(start), box2d.vec2dTo(end));
8382
+ debugRaycast && debugLine(start, end, raycastResults.length ? '#f00' : '#00f', .02);
8383
+ return raycastResults;
8384
+ }
8385
+
8386
+ /** raycast and return the first result
8387
+ * @param {Vector2} start
8388
+ * @param {Vector2} end */
8389
+ raycast(start, end)
8390
+ {
8391
+ const raycastResults = box2d.raycastAll(start, end);
8392
+ if (!raycastResults.length)
8393
+ return undefined;
8394
+ return raycastResults.reduce((a,b)=>a.fraction < b.fraction ? a : b);
8395
+ }
8396
+
8397
+ /** box aabb cast and return all the objects
8398
+ * @param {Vector2} pos
8399
+ * @param {Vector2} size */
8400
+ boxCastAll(pos, size)
8401
+ {
8402
+ const queryCallback = new box2d.instance.JSQueryCallback();
8403
+ queryCallback.ReportFixture = function(fixturePointer)
8404
+ {
8405
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8406
+ const o = fixture.GetBody().object;
8407
+ if (!queryObjects.includes(o))
8408
+ queryObjects.push(o); // add if not already in list
8409
+ return true; // continue getting results
8410
+ };
8411
+
8412
+ const aabb = new box2d.instance.b2AABB();
8413
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
8414
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
8415
+
8416
+ let queryObjects = [];
8417
+ box2d.world.QueryAABB(queryCallback, aabb);
8418
+ debugRaycast && debugRect(pos, size, queryObjects.length ? '#f00' : '#00f', .02);
8419
+ return queryObjects;
8420
+ }
8421
+
8422
+ /** box aabb cast and return the first object
8423
+ * @param {Vector2} pos
8424
+ * @param {Vector2} size */
8425
+ boxCast(pos, size)
8426
+ {
8427
+ const queryCallback = new box2d.instance.JSQueryCallback();
8428
+ queryCallback.ReportFixture = function(fixturePointer)
8429
+ {
8430
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8431
+ queryObject = fixture.GetBody().object;
8432
+ return false; // stop getting results
8433
+ };
8434
+
8435
+ const aabb = new box2d.instance.b2AABB();
8436
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
8437
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
8438
+
8439
+ let queryObject;
8440
+ box2d.world.QueryAABB(queryCallback, aabb);
8441
+ debugRaycast && debugRect(pos, size, queryObject ? '#f00' : '#00f', .02);
8442
+ return queryObject;
8443
+ }
8444
+
8445
+ /** circle cast and return all the objects
8446
+ * @param {Vector2} pos
8447
+ * @param {number} diameter */
8448
+ circleCastAll(pos, diameter)
8449
+ {
8450
+ const radius2 = (diameter/2)**2;
8451
+ const results = box2d.boxCastAll(pos, vec2(diameter));
8452
+ return results.filter(o=>o.pos.distanceSquared(pos) < radius2);
8453
+ }
8454
+
8455
+ /** circle cast and return the first object
8456
+ * @param {Vector2} pos
8457
+ * @param {number} diameter */
8458
+ circleCast(pos, diameter)
8459
+ {
8460
+ const radius2 = (diameter/2)**2;
8461
+ let results = box2d.boxCastAll(pos, vec2(diameter));
8462
+
8463
+ let bestResult, bestDistance2;
8464
+ for (const result of results)
8465
+ {
8466
+ const distance2 = result.pos.distanceSquared(pos);
8467
+ if (distance2 < radius2 && (!bestResult || distance2 < bestDistance2))
8468
+ {
8469
+ bestResult = result;
8470
+ bestDistance2 = distance2;
8471
+ }
8472
+ }
8473
+ return bestResult;
8474
+ }
8475
+
8476
+ /** point cast and return the first object
8477
+ * @param {Vector2} pos
8478
+ * @param {boolean} dynamicOnly */
8479
+ pointCast(pos, dynamicOnly=true)
8480
+ {
8481
+ const queryCallback = new box2d.instance.JSQueryCallback();
8482
+ queryCallback.ReportFixture = function(fixturePointer)
8483
+ {
8484
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8485
+ if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
8486
+ return true; // continue getting results
8487
+ if (!fixture.TestPoint(box2d.vec2dTo(pos)))
8488
+ return true; // continue getting results
8489
+ queryObject = fixture.GetBody().object;
8490
+ return false; // stop getting results
8491
+ };
8492
+
8493
+ const aabb = new box2d.instance.b2AABB();
8494
+ aabb.set_lowerBound(box2d.vec2dTo(pos));
8495
+ aabb.set_upperBound(box2d.vec2dTo(pos));
8496
+
8497
+ let queryObject;
8498
+ box2d.world.QueryAABB(queryCallback, aabb);
8499
+ debugRaycast && debugRect(pos, vec2(), queryObject ? '#f00' : '#00f', .02);
8500
+ return queryObject;
8501
+ }
8502
+
8503
+ ///////////////////////////////////////////////////////////////////////////////
8504
+ // drawing
8505
+
8506
+ /** draws a fixture
8507
+ * @param {Object} fixture
8508
+ * @param {Vector2} pos
8509
+ * @param {number} angle
8510
+ * @param {Color} [color]
8511
+ * @param {Color} [outlineColor]
8512
+ * @param {number} [lineWidth]
8513
+ * @param {CanvasRenderingContext2D} [context] */
8514
+ drawFixture(fixture, pos, angle, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8515
+ {
8516
+ const shape = box2d.castObjectType(fixture.GetShape());
8517
+ switch (shape.GetType())
8518
+ {
8519
+ case box2d.instance.b2Shape.e_polygon:
8520
+ {
8521
+ let points = [];
8522
+ for (let i=shape.GetVertexCount(); i--;)
8523
+ points.push(box2d.vec2From(shape.GetVertex(i)));
8524
+ box2d.drawPoly(pos, angle, points, color, outlineColor, lineWidth, context);
8525
+ break;
8526
+ }
8527
+ case box2d.instance.b2Shape.e_circle:
8528
+ {
8529
+ const radius = shape.get_m_radius();
8530
+ box2d.drawCircle(pos, radius, color, outlineColor, lineWidth, context);
8531
+ break;
8532
+ }
8533
+ case box2d.instance.b2Shape.e_edge:
8534
+ {
8535
+ const v1 = box2d.vec2From(shape.get_m_vertex1());
8536
+ const v2 = box2d.vec2From(shape.get_m_vertex2());
8537
+ box2d.drawLine(pos, angle, v1, v2, color, lineWidth, context);
8538
+ break;
8539
+ }
8540
+ }
8541
+ }
8542
+
8543
+ /** draws a circle
8544
+ * @param {Vector2} pos
8545
+ * @param {number} radius
8546
+ * @param {Color} [color]
8547
+ * @param {Color} [outlineColor]
8548
+ * @param {number} [lineWidth]
8549
+ * @param {CanvasRenderingContext2D} [context] */
8550
+ drawCircle(pos, radius, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8551
+ {
8552
+ drawCanvas2D(pos, vec2(1), 0, 0, context=>
8553
+ {
8554
+ context.beginPath();
8555
+ context.arc(0, 0, radius, 0, 9);
8556
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8557
+ }, 0, context);
8558
+ }
8559
+
8560
+ /** draws a polygon
8561
+ * @param {Vector2} pos
8562
+ * @param {number} angle
8563
+ * @param {Array<Vector2>} points
8564
+ * @param {Color} [color]
8565
+ * @param {Color} [outlineColor]
8566
+ * @param {number} [lineWidth]
8567
+ * @param {CanvasRenderingContext2D} [context] */
8568
+ drawPoly(pos, angle, points, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8569
+ {
8570
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8571
+ {
8572
+ context.beginPath();
8573
+ points.forEach(p=>context.lineTo(p.x, p.y));
8574
+ context.closePath();
8575
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8576
+ }, 0, context);
8577
+ }
8578
+
8579
+ /** draws a line
8580
+ * @param {Vector2} pos
8581
+ * @param {number} angle
8582
+ * @param {Vector2} posA
8583
+ * @param {Vector2} posB
8584
+ * @param {Color} [color]
8585
+ * @param {number} [lineWidth]
8586
+ * @param {CanvasRenderingContext2D} [context] */
8587
+ drawLine(pos, angle, posA, posB, color=WHITE, lineWidth=.1, context=mainContext)
8588
+ {
8589
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8590
+ {
8591
+ context.beginPath();
8592
+ context.lineTo(posA.x, posA.y);
8593
+ context.lineTo(posB.x, posB.y);
8594
+ box2d.drawFillStroke(0, color, lineWidth, context);
8595
+ }, 0, context);
8596
+ }
8597
+
8598
+ /** performs a fill or stroke as a helper to the other draw functions
8599
+ * @param {Color} [color]
8600
+ * @param {Color} [outlineColor]
8601
+ * @param {number} [lineWidth]
8602
+ * @param {CanvasRenderingContext2D} [context] */
8603
+ drawFillStroke(color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8604
+ {
8605
+ if (color)
8606
+ {
8607
+ context.fillStyle = color.toString();
8608
+ context.fill();
8609
+ }
8610
+ if (outlineColor && lineWidth)
8611
+ {
8612
+ context.lineWidth = lineWidth;
8613
+ context.lineJoin = context.lineCap = 'round';
8614
+ context.strokeStyle = outlineColor.toString();
8615
+ context.stroke();
8616
+ }
8617
+ }
8618
+
8619
+ ///////////////////////////////////////////////////////////////////////////////
8620
+ // helper functions
8621
+
8622
+ /** converts a box2d vec2 to a Vector2
8623
+ * @param {Object} v */
8624
+ vec2From(v)
8625
+ {
8626
+ ASSERT(v instanceof box2d.instance.b2Vec2);
8627
+ return new Vector2(v.get_x(), v.get_y());
8628
+ }
8629
+
8630
+ /** converts a box2d vec2 pointer to a Vector2
8631
+ * @param {Object} v */
8632
+ vec2FromPointer(v)
8633
+ {
8634
+ return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
8635
+ }
8636
+
8637
+ /** converts a Vector2 to a box2 vec2
8638
+ * @param {Vector2} v */
8639
+ vec2dTo(v)
8640
+ {
8641
+ ASSERT(v instanceof Vector2);
8642
+ return new box2d.instance.b2Vec2(v.x, v.y);
8643
+ }
8644
+
8645
+ /** checks if a box2d object is null
8646
+ * @param {Object} o */
8647
+ isNull(o) { return !box2d.instance.getPointer(o); }
8648
+
8649
+ /** casts a box2d object to its correct type
8650
+ * @param {Object} o */
8651
+ castObjectType(o)
8652
+ {
8653
+ switch (o.GetType())
8654
+ {
8655
+ case box2d.instance.b2Shape.e_circle:
8656
+ return box2d.instance.castObject(o, box2d.instance.b2CircleShape);
8657
+ case box2d.instance.b2Shape.e_edge:
8658
+ return box2d.instance.castObject(o, box2d.instance.b2EdgeShape);
8659
+ case box2d.instance.b2Shape.e_polygon:
8660
+ return box2d.instance.castObject(o, box2d.instance.b2PolygonShape);
8661
+ case box2d.instance.b2Shape.e_chain:
8662
+ return box2d.instance.castObject(o, box2d.instance.b2ChainShape);
8663
+ case box2d.instance.e_revoluteJoint:
8664
+ return box2d.instance.castObject(o, box2d.instance.b2RevoluteJoint);
8665
+ case box2d.instance.e_prismaticJoint:
8666
+ return box2d.instance.castObject(o, box2d.instance.b2PrismaticJoint);
8667
+ case box2d.instance.e_distanceJoint:
8668
+ return box2d.instance.castObject(o, box2d.instance.b2DistanceJoint);
8669
+ case box2d.instance.e_pulleyJoint:
8670
+ return box2d.instance.castObject(o, box2d.instance.b2PulleyJoint);
8671
+ case box2d.instance.e_mouseJoint:
8672
+ return box2d.instance.castObject(o, box2d.instance.b2MouseJoint);
8673
+ case box2d.instance.e_gearJoint:
8674
+ return box2d.instance.castObject(o, box2d.instance.b2GearJoint);
8675
+ case box2d.instance.e_wheelJoint:
8676
+ return box2d.instance.castObject(o, box2d.instance.b2WheelJoint);
8677
+ case box2d.instance.e_weldJoint:
8678
+ return box2d.instance.castObject(o, box2d.instance.b2WeldJoint);
8679
+ case box2d.instance.e_frictionJoint:
8680
+ return box2d.instance.castObject(o, box2d.instance.b2FrictionJoint);
8681
+ case box2d.instance.e_ropeJoint:
8682
+ return box2d.instance.castObject(o, box2d.instance.b2RopeJoint);
8683
+ case box2d.instance.e_motorJoint:
8684
+ return box2d.instance.castObject(o, box2d.instance.b2MotorJoint);
8685
+ }
8686
+
8687
+ ASSERT(false, 'Unknown box2d object type');
8688
+ }
8689
+ }
8690
+
8691
+ ///////////////////////////////////////////////////////////////////////////////
8692
+ /** Box2d Init - Startup LittleJS engine with your callback functions
8693
+ * @param {Function|function():Promise} gameInit - Called once after the engine starts up
8694
+ * @param {Function} gameUpdate - Called every frame before objects are updated
8695
+ * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused
8696
+ * @param {Function} gameRender - Called before objects are rendered, for drawing the background
8697
+ * @param {Function} gameRenderPost - Called after objects are rendered, useful for drawing UI
8698
+ * @param {Array<string>} [imageSources=[]] - List of images to load
8699
+ * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default */
8700
+ function box2dEngineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources, rootElement)
8701
+ {
8702
+ Box2D().then(box2dInstance=>
8703
+ {
8704
+ // create box2d object
8705
+ new Box2dPlugin(box2dInstance);
8706
+ setupDebugDraw();
8707
+
8708
+ // start littlejs
8709
+ engineAddPlugin(box2dUpdate, box2dRender);
8710
+ engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources, rootElement);
8711
+ });
8712
+
8713
+ // hook up box2d plugin to update and render
8714
+ function box2dUpdate()
8715
+ {
8716
+ if (!paused)
8717
+ box2d.step();
8718
+ }
8719
+ function box2dRender()
8720
+ {
8721
+ if (box2dDebug || debugPhysics && debugOverlay)
8722
+ box2d.world.DrawDebugData();
8723
+ }
8724
+
8725
+ // box2d debug drawing
8726
+ function setupDebugDraw()
8727
+ {
8728
+ // setup debug draw
8729
+ const debugDraw = new box2d.instance.JSDraw();
8730
+ const box2dColor = (c)=> new Color(c.get_r(), c.get_g(), c.get_b());
8731
+ const box2dColorPointer = (c)=>
8732
+ box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
8733
+ const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
8734
+ const getPointsList = (vertices, vertexCount) =>
8735
+ {
8736
+ const points = [];
8737
+ for (let i=vertexCount; i--;)
8738
+ points.push(box2d.vec2FromPointer(vertices+i*8));
8739
+ return points;
8740
+ }
8741
+ debugDraw.DrawSegment = function(point1, point2, color)
8742
+ {
8743
+ color = getDebugColor(color);
8744
+ point1 = box2d.vec2FromPointer(point1);
8745
+ point2 = box2d.vec2FromPointer(point2);
8746
+ box2d.drawLine(vec2(), 0, point1, point2, color, undefined, overlayContext);
8747
+ };
8748
+ debugDraw.DrawPolygon = function(vertices, vertexCount, color)
8749
+ {
8750
+ color = getDebugColor(color);
8751
+ const points = getPointsList(vertices, vertexCount);
8752
+ box2d.drawPoly(vec2(), 0, points, undefined, color, undefined, overlayContext);
8753
+ };
8754
+ debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
8755
+ {
8756
+ color = getDebugColor(color);
8757
+ const points = getPointsList(vertices, vertexCount);
8758
+ box2d.drawPoly(vec2(), 0, points, color, color, undefined, overlayContext);
8759
+ };
8760
+ debugDraw.DrawCircle = function(center, radius, color)
8761
+ {
8762
+ color = getDebugColor(color);
8763
+ center = box2d.vec2FromPointer(center);
8764
+ box2d.drawCircle(center, radius, undefined, color, undefined, overlayContext);
8765
+ };
8766
+ debugDraw.DrawSolidCircle = function(center, radius, axis, color)
8767
+ {
8768
+ color = getDebugColor(color);
8769
+ center = box2d.vec2FromPointer(center);
8770
+ axis = box2d.vec2FromPointer(axis).scale(radius);
8771
+ box2d.drawCircle(center, radius, color, color, undefined, overlayContext);
8772
+ box2d.drawLine(center, 0, vec2(), axis, color, undefined, overlayContext);
8773
+ };
8774
+ debugDraw.DrawTransform = function(transform)
8775
+ {
8776
+ transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
8777
+ const pos = vec2(transform.get_p());
8778
+ const angle = -transform.get_q().GetAngle();
8779
+ const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
8780
+ const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
8781
+ box2d.drawLine(pos, angle, vec2(), p1, c1, undefined, overlayContext);
8782
+ box2d.drawLine(pos, angle, vec2(), p2, c2, undefined, overlayContext);
8783
+ }
8784
+
8785
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
8786
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_jointBit);
8787
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_aabbBit);
8788
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_pairBit);
8789
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_centerOfMassBit);
8790
+ box2d.world.SetDebugDraw(debugDraw);
8791
+ }
8792
+ }
8793
+
8794
+
8795
+ /**
8796
+ * LittleJS Module Export
8797
+ */
8798
+
8799
+ export
8800
+ {
8801
+ // Engine
8802
+ engineName,
8803
+ engineVersion,
8804
+ frameRate,
8805
+ timeDelta,
8806
+ engineObjects,
8807
+ frame,
8808
+ time,
8809
+ timeReal,
8810
+ paused,
8811
+ setPaused,
8812
+ engineInit,
8813
+ engineObjectsUpdate,
8814
+ engineObjectsDestroy,
8815
+ engineObjectsCollect,
8816
+ engineObjectsCallback,
8817
+ engineObjectsRaycast,
8818
+ engineAddPlugin,
8819
+
8820
+ // Globals
8821
+ debug,
8822
+ debugOverlay,
8823
+ showWatermark,
8824
+
8825
+ // Debug
8826
+ ASSERT,
8827
+ debugRect,
8828
+ debugPoly,
8829
+ debugCircle,
8830
+ debugPoint,
8831
+ debugLine,
8832
+ debugOverlap,
8833
+ debugText,
8834
+ debugClear,
8835
+ debugScreenshot,
8836
+ debugSaveCanvas,
8837
+ debugSaveText,
8838
+ debugSaveDataURL,
8839
+ debugShowErrors,
8840
+ debugVideoCaptureIsActive,
8841
+ debugVideoCaptureStart,
8842
+ debugVideoCaptureStop,
8843
+
8844
+ // Settings
8845
+ cameraPos,
8846
+ cameraScale,
8847
+ canvasMaxSize,
8848
+ canvasFixedSize,
8849
+ canvasPixelated,
8850
+ tilesPixelated,
8851
+ fontDefault,
8852
+ showSplashScreen,
8853
+ headlessMode,
8854
+ tileSizeDefault,
8855
+ tileFixBleedScale,
8856
+ enablePhysicsSolver,
8857
+ objectDefaultMass,
8858
+ objectDefaultDamping,
8859
+ objectDefaultAngleDamping,
8860
+ objectDefaultElasticity,
8861
+ objectDefaultFriction,
8862
+ objectMaxSpeed,
8863
+ gravity,
8864
+ particleEmitRateScale,
8865
+ glEnable,
8866
+ glOverlay,
8867
+ gamepadsEnable,
8868
+ gamepadDirectionEmulateStick,
8869
+ inputWASDEmulateDirection,
8870
+ touchGamepadEnable,
8871
+ touchGamepadAnalog,
8872
+ touchGamepadSize,
8873
+ touchGamepadAlpha,
8874
+ vibrateEnable,
8875
+ soundEnable,
8876
+ soundVolume,
8877
+ soundDefaultRange,
8878
+ soundDefaultTaper,
8879
+ medalDisplayTime,
8880
+ medalDisplaySlideTime,
8881
+ medalDisplaySize,
8882
+
8883
+ // Setters for globals
8884
+ setCameraPos,
8885
+ setCameraScale,
8886
+ setCanvasMaxSize,
8887
+ setCanvasFixedSize,
8888
+ setCanvasPixelated,
8889
+ setTilesPixelated,
8890
+ setFontDefault,
8891
+ setShowSplashScreen,
8892
+ setHeadlessMode,
8893
+ setGlEnable,
8894
+ setGlOverlay,
8895
+ setTileSizeDefault,
8896
+ setTileFixBleedScale,
8897
+ setEnablePhysicsSolver,
8898
+ setObjectDefaultMass,
8899
+ setObjectDefaultDamping,
8900
+ setObjectDefaultAngleDamping,
8901
+ setObjectDefaultElasticity,
8902
+ setObjectDefaultFriction,
8903
+ setObjectMaxSpeed,
8904
+ setGravity,
8905
+ setParticleEmitRateScale,
8906
+ setTouchInputEnable,
8907
+ setGamepadsEnable,
8908
+ setGamepadDirectionEmulateStick,
8909
+ setInputWASDEmulateDirection,
8910
+ setTouchGamepadEnable,
8911
+ setTouchGamepadAnalog,
8912
+ setTouchGamepadSize,
8913
+ setTouchGamepadAlpha,
8914
+ setVibrateEnable,
8915
+ setSoundEnable,
8916
+ setSoundVolume,
8917
+ setSoundDefaultRange,
8918
+ setSoundDefaultTaper,
8919
+ setMedalDisplayTime,
8920
+ setMedalDisplaySlideTime,
8921
+ setMedalDisplaySize,
8922
+ setMedalsPreventUnlock,
8923
+ setShowWatermark,
8924
+ setDebugKey,
8925
+
8926
+ // Utilities
8927
+ PI,
8928
+ abs,
8929
+ min,
8930
+ max,
8931
+ sign,
8932
+ mod,
8933
+ clamp,
8934
+ percent,
8935
+ distanceWrap,
8936
+ lerpWrap,
8937
+ distanceAngle,
8938
+ lerpAngle,
8939
+ lerp,
8940
+ smoothStep,
8941
+ nearestPowerOfTwo,
8942
+ isOverlapping,
8943
+ isIntersecting,
8944
+ wave,
8945
+ formatTime,
8946
+
8947
+ // Random
8948
+ rand,
8949
+ randInt,
8950
+ randSign,
8951
+ randInCircle,
8952
+ randVector,
8953
+ randColor,
8954
+
8955
+ // Utility Classes
8956
+ RandomGenerator,
8957
+ Vector2,
8958
+ Color,
8959
+ Timer,
8960
+ vec2,
8961
+ rgb,
8962
+ hsl,
8963
+ isColor,
8964
+
8965
+ // Default Colors
8966
+ WHITE,
8967
+ BLACK,
8968
+ GRAY,
8969
+ RED,
8970
+ ORANGE,
8971
+ YELLOW,
8972
+ GREEN,
8973
+ CYAN,
8974
+ BLUE,
8975
+ PURPLE,
8976
+ MAGENTA,
8977
+
8978
+ // Draw
8979
+ textureInfos,
8980
+ tile,
8981
+ TileInfo,
8982
+ TextureInfo,
6121
8983
  mainCanvas,
6122
8984
  mainContext,
6123
8985
  overlayCanvas,
@@ -6151,6 +9013,7 @@ export
6151
9013
  glCopyToContext,
6152
9014
  glCreateProgram,
6153
9015
  glCreateTexture,
9016
+ glSetTextureData,
6154
9017
  glDraw,
6155
9018
  glFlush,
6156
9019
  glSetTexture,
@@ -6180,7 +9043,8 @@ export
6180
9043
  mousePosScreen,
6181
9044
  mouseWheel,
6182
9045
  isUsingGamepad,
6183
- preventDefaultInput,
9046
+ inputPreventDefault,
9047
+ setInputPreventDefault,
6184
9048
  gamepadIsDown,
6185
9049
  gamepadWasPressed,
6186
9050
  gamepadWasReleased,
@@ -6193,28 +9057,27 @@ export
6193
9057
  // Audio
6194
9058
  Sound,
6195
9059
  SoundWave,
6196
- Music,
6197
9060
  playAudioFile,
6198
9061
  speak,
6199
9062
  speakStop,
6200
9063
  getNoteFrequency,
6201
- audioContext,
6202
9064
  playSamples,
6203
9065
  zzfx,
9066
+ zzfxG,
9067
+ zzfxR,
9068
+ audioContext,
6204
9069
 
6205
9070
  // Base Object
6206
9071
  EngineObject,
6207
9072
 
6208
9073
  // Tiles
6209
- tileCollision,
6210
- tileCollisionSize,
6211
- initTileCollision,
6212
- setTileCollisionData,
9074
+ tileCollisionLayers,
6213
9075
  getTileCollisionData,
6214
9076
  tileCollisionTest,
6215
9077
  tileCollisionRaycast,
6216
9078
  TileLayerData,
6217
9079
  TileLayer,
9080
+ TileCollisionLayer,
6218
9081
 
6219
9082
  // Particles
6220
9083
  ParticleEmitter,
@@ -6227,3 +9090,54 @@ export
6227
9090
  Medal,
6228
9091
  };
6229
9092
 
9093
+ /**
9094
+ * LittleJS Module Plugins Export
9095
+ */
9096
+
9097
+ export
9098
+ {
9099
+ // Newgrounds
9100
+ newgrounds,
9101
+ NewgroundsPlugin,
9102
+ NewgroundsMedal,
9103
+
9104
+ // Post Process
9105
+ postProcess,
9106
+ PostProcessPlugin,
9107
+
9108
+ // ZzFXMusic
9109
+ ZzFXMusic,
9110
+
9111
+ // UI System
9112
+ uiSystem,
9113
+ UISystemPlugin,
9114
+ UIObject,
9115
+ UIText,
9116
+ UITile,
9117
+ UIButton,
9118
+ UICheckbox,
9119
+ UIScrollbar,
9120
+
9121
+ // Box2D Physics
9122
+ box2d,
9123
+ box2dDebug,
9124
+ box2dSetDebug,
9125
+ box2dEngineInit,
9126
+ Box2dPlugin,
9127
+ Box2dObject,
9128
+ Box2dRaycastResult,
9129
+ Box2dJoint,
9130
+ Box2dTargetJoint,
9131
+ Box2dDistanceJoint,
9132
+ Box2dPinJoint,
9133
+ Box2dRopeJoint,
9134
+ Box2dRevoluteJoint,
9135
+ Box2dGearJoint,
9136
+ Box2dPrismaticJoint,
9137
+ Box2dWheelJoint,
9138
+ Box2dWeldJoint,
9139
+ Box2dFrictionJoint,
9140
+ Box2dPulleyJoint,
9141
+ Box2dMotorJoint,
9142
+ };
9143
+