littlejsengine 1.11.17 → 1.12.6

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 (73) hide show
  1. package/dist/box2d.wasm.js +630 -0
  2. package/dist/box2d.wasm.wasm +0 -0
  3. package/dist/littlejs.d.ts +158 -81
  4. package/dist/littlejs.esm.js +882 -793
  5. package/dist/littlejs.esm.min.js +1 -1
  6. package/dist/littlejs.js +3048 -175
  7. package/dist/littlejs.min.js +1 -1
  8. package/dist/littlejs.release.js +3047 -172
  9. package/examples/box2d/game.js +82 -56
  10. package/examples/box2d/gameObjects.js +114 -107
  11. package/examples/box2d/index.html +2 -7
  12. package/examples/box2d/scenes.js +72 -78
  13. package/examples/box2d/tiles.png +0 -0
  14. package/examples/breakout/game.js +63 -28
  15. package/examples/breakout/gameObjects.js +45 -47
  16. package/examples/breakout/index.html +1 -5
  17. package/examples/breakoutTutorial/game.js +36 -29
  18. package/examples/breakoutTutorial/index.html +1 -3
  19. package/examples/empty/game.js +5 -2
  20. package/examples/empty/index.html +1 -3
  21. package/examples/htmlMenu/game.js +24 -15
  22. package/examples/htmlMenu/index.html +5 -7
  23. package/examples/index.html +24 -17
  24. package/examples/module/game.js +32 -34
  25. package/examples/module/index.html +1 -1
  26. package/examples/particles/index.html +27 -22
  27. package/examples/platformer/game.js +62 -45
  28. package/examples/platformer/gameCharacter.js +42 -34
  29. package/examples/platformer/gameEffects.js +82 -75
  30. package/examples/platformer/gameLevel.js +67 -38
  31. package/examples/platformer/{gameLevelData.js → gameLevelData.json} +1 -11
  32. package/examples/platformer/gameObjects.js +70 -64
  33. package/examples/platformer/gamePlayer.js +12 -7
  34. package/examples/platformer/index.html +1 -9
  35. package/examples/puzzle/game.js +59 -40
  36. package/examples/puzzle/index.html +1 -3
  37. package/examples/shorts/base.html +3 -2
  38. package/examples/shorts/box2d.js +46 -0
  39. package/examples/shorts/box2dCar.js +49 -0
  40. package/examples/shorts/particles.js +1 -1
  41. package/examples/shorts/platformer.js +1 -1
  42. package/examples/shorts/postProcess.js +45 -0
  43. package/examples/shorts/tileLayer.js +5 -6
  44. package/examples/shorts/tiles.png +0 -0
  45. package/examples/shorts/uiSystem.js +39 -0
  46. package/examples/starter/game.js +9 -10
  47. package/examples/starter/index.html +13 -13
  48. package/examples/stress/index.html +44 -44
  49. package/examples/typescript/build.js +17 -18
  50. package/examples/typescript/game.js +32 -34
  51. package/examples/typescript/game.ts +32 -34
  52. package/examples/typescript/index.html +1 -1
  53. package/examples/uiSystem/game.js +34 -31
  54. package/examples/uiSystem/index.html +1 -5
  55. package/package.json +1 -1
  56. package/plugins/box2d.js +28 -30
  57. package/plugins/pluginExport.js +2 -3
  58. package/plugins/uiSystem.js +9 -3
  59. package/reference.md +29 -27
  60. package/src/engine.js +10 -11
  61. package/src/engineAudio.js +24 -15
  62. package/src/engineBuild.js +25 -8
  63. package/src/engineDebug.js +1 -3
  64. package/src/engineExport.js +3 -6
  65. package/src/engineInput.js +6 -4
  66. package/src/engineObject.js +18 -14
  67. package/src/engineSettings.js +6 -6
  68. package/src/engineTileLayer.js +179 -96
  69. package/src/engineUtilities.js +16 -0
  70. package/src/engineWebGL.js +13 -1
  71. package/examples/typescript/build/dist/littlejs.esm.js +0 -4834
  72. package/examples/typescript/build/examples/typescript/build.js +0 -24
  73. package/examples/typescript/build/examples/typescript/game.js +0 -102
@@ -9,8 +9,6 @@
9
9
  * - Debug functionality is disabled to reduce size and increase performance
10
10
  */
11
11
 
12
-
13
-
14
12
  let showWatermark = 0;
15
13
  let debugKey = '';
16
14
  const debug = 0;
@@ -53,8 +51,6 @@ function debugVideoCaptureUpdate(){}
53
51
  * @namespace Utilities
54
52
  */
55
53
 
56
-
57
-
58
54
  /** A shortcut to get Math.PI
59
55
  * @type {number}
60
56
  * @default Math.PI
@@ -234,6 +230,16 @@ function wave(frequency=1, amplitude=1, t=time)
234
230
  * @memberof Utilities */
235
231
  function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
236
232
 
233
+ /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
234
+ * @param {string} url - URL of JSON file
235
+ * @return {Promise<object>}
236
+ * @memberof Utilities */
237
+ async function fetchJSON(url)
238
+ {
239
+ const response = await fetch(url);
240
+ return response.json();
241
+ }
242
+
237
243
  ///////////////////////////////////////////////////////////////////////////////
238
244
 
239
245
  /** Random global functions
@@ -329,6 +335,12 @@ class RandomGenerator
329
335
  /** Randomly returns either -1 or 1 deterministically
330
336
  * @return {number} */
331
337
  sign() { return this.float() > .5 ? 1 : -1; }
338
+
339
+ /** Returns a seeded random value between the two values passed in with a random sign
340
+ * @param {number} [valueA]
341
+ * @param {number} [valueB]
342
+ * @return {number} */
343
+ floatSign(valueA=1, valueB=0) { return this.float(valueA, valueB) * this.sign(); }
332
344
  }
333
345
 
334
346
  ///////////////////////////////////////////////////////////////////////////////
@@ -969,8 +981,6 @@ class Timer
969
981
  * @namespace Settings
970
982
  */
971
983
 
972
-
973
-
974
984
  ///////////////////////////////////////////////////////////////////////////////
975
985
  // Camera settings
976
986
 
@@ -1002,7 +1012,7 @@ let canvasMaxSize = vec2(1920, 1080);
1002
1012
  * @memberof Settings */
1003
1013
  let canvasFixedSize = vec2();
1004
1014
 
1005
- /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
1015
+ /** Use nearest neighbor canvas scaling for more pixelated look
1006
1016
  * - Must be set before startup to take effect
1007
1017
  * - If enabled sets css image-rendering:pixelated
1008
1018
  * @type {boolean}
@@ -1112,11 +1122,11 @@ let objectDefaultFriction = .8;
1112
1122
  * @memberof Settings */
1113
1123
  let objectMaxSpeed = 1;
1114
1124
 
1115
- /** How much gravity to apply to objects along the Y axis, negative is down
1116
- * @type {number}
1125
+ /** How much gravity to apply to objects, negative Y is down
1126
+ * @type {Vector2}
1117
1127
  * @default
1118
1128
  * @memberof Settings */
1119
- let gravity = 0;
1129
+ let gravity = vec2();
1120
1130
 
1121
1131
  /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
1122
1132
  * @type {number}
@@ -1342,8 +1352,8 @@ function setObjectDefaultFriction(friction) { objectDefaultFriction = friction;
1342
1352
  * @memberof Settings */
1343
1353
  function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
1344
1354
 
1345
- /** Set how much gravity to apply to objects along the Y axis
1346
- * @param {number} newGravity
1355
+ /** Set how much gravity to apply to objects
1356
+ * @param {Vector2} newGravity
1347
1357
  * @memberof Settings */
1348
1358
  function setGravity(newGravity) { gravity = newGravity; }
1349
1359
 
@@ -1455,8 +1465,6 @@ function setDebugKey(key) { debugKey = key; }
1455
1465
  * LittleJS Object System
1456
1466
  */
1457
1467
 
1458
-
1459
-
1460
1468
  /**
1461
1469
  * LittleJS Object Base Object Class
1462
1470
  * - Top level object class used by the engine
@@ -1611,7 +1619,10 @@ class EngineObject
1611
1619
  this.velocity.x *= this.damping;
1612
1620
  this.velocity.y *= this.damping;
1613
1621
  if (this.mass) // don't apply gravity to static objects
1614
- this.velocity.y += gravity * this.gravityScale;
1622
+ {
1623
+ this.velocity.x += gravity.x * this.gravityScale;
1624
+ this.velocity.y += gravity.y * this.gravityScale;
1625
+ }
1615
1626
  this.pos.x += this.velocity.x;
1616
1627
  this.pos.y += this.velocity.y;
1617
1628
  this.angle += this.angleVelocity *= this.angleDamping;
@@ -1626,9 +1637,9 @@ class EngineObject
1626
1637
  if (this.groundObject)
1627
1638
  {
1628
1639
  // apply friction in local space of ground object
1629
- const groundSpeed = this.groundObject != this && this.groundObject.velocity ?
1630
- this.groundObject.velocity.x : 0;
1631
- this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
1640
+ const friction = max(this.friction, this.groundObject.friction);
1641
+ const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
1642
+ this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
1632
1643
  this.groundObject = undefined;
1633
1644
  //debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
1634
1645
  }
@@ -1670,7 +1681,7 @@ class EngineObject
1670
1681
 
1671
1682
  // check for collision
1672
1683
  const sizeBoth = this.size.add(o.size);
1673
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity; // prefer to push up if small delta
1684
+ const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity.y; // prefer to push up if small delta
1674
1685
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
1675
1686
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
1676
1687
  const elasticity = max(this.elasticity, o.elasticity);
@@ -1732,19 +1743,21 @@ class EngineObject
1732
1743
  if (this.collideTiles)
1733
1744
  {
1734
1745
  // check collision against tiles
1735
- if (tileCollisionTest(this.pos, this.size, this))
1746
+ const hitLayer = tileCollisionTest(this.pos, this.size, this)
1747
+ if (hitLayer)
1736
1748
  {
1737
1749
  // if already was stuck in collision, don't do anything
1738
1750
  // this should not happen unless something starts in collision
1739
1751
  if (!tileCollisionTest(oldPos, this.size, this))
1740
1752
  {
1741
1753
  // test which side we bounced off (or both if a corner)
1742
- const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
1743
- const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
1744
- if (isBlockedY || !isBlockedX)
1754
+ const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
1755
+ const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
1756
+ if (blockedLayerY || !blockedLayerX)
1745
1757
  {
1746
1758
  // bounce velocity
1747
- this.velocity.y *= -this.elasticity;
1759
+ const elasticity = max(this.elasticity, hitLayer.elasticity);
1760
+ this.velocity.y *= -elasticity;
1748
1761
 
1749
1762
  if (wasMovingDown)
1750
1763
  {
@@ -1753,9 +1766,8 @@ class EngineObject
1753
1766
  const epsilon = .0001;
1754
1767
  this.pos.y = (oldPos.y-this.size.y/2|0)+this.size.y/2+epsilon;
1755
1768
 
1756
- // set ground object to self for tile collision
1757
- // TODO: rework system so tile collision is its own object
1758
- this.groundObject = this;
1769
+ // set ground object for tile collision
1770
+ this.groundObject = hitLayer;
1759
1771
  }
1760
1772
  else
1761
1773
  {
@@ -1764,7 +1776,7 @@ class EngineObject
1764
1776
  this.groundObject = undefined;
1765
1777
  }
1766
1778
  }
1767
- if (isBlockedX)
1779
+ if (blockedLayerX)
1768
1780
  {
1769
1781
  // move to previous position and bounce
1770
1782
  this.pos.x = oldPos.x;
@@ -1935,8 +1947,6 @@ class EngineObject
1935
1947
  * @namespace Draw
1936
1948
  */
1937
1949
 
1938
-
1939
-
1940
1950
  /** The primary 2D canvas visible to the user
1941
1951
  * @type {HTMLCanvasElement}
1942
1952
  * @memberof Draw */
@@ -2558,8 +2568,6 @@ function setCursor(cursorStyle = 'auto')
2558
2568
  * @namespace Input
2559
2569
  */
2560
2570
 
2561
-
2562
-
2563
2571
  /** Returns true if device key is down
2564
2572
  * @param {string|number} key
2565
2573
  * @param {number} [device]
@@ -2611,21 +2619,21 @@ function clearInput() { inputData = [[]]; touchGamepadButtons = []; }
2611
2619
  * @param {number} button
2612
2620
  * @return {boolean}
2613
2621
  * @memberof Input */
2614
- const mouseIsDown = keyIsDown;
2622
+ function mouseIsDown(button) { return keyIsDown(button); }
2615
2623
 
2616
2624
  /** Returns true if mouse button was pressed
2617
2625
  * @function
2618
2626
  * @param {number} button
2619
2627
  * @return {boolean}
2620
2628
  * @memberof Input */
2621
- const mouseWasPressed = keyWasPressed;
2629
+ function mouseWasPressed(button) { return keyWasPressed(button); }
2622
2630
 
2623
2631
  /** Returns true if mouse button was released
2624
2632
  * @function
2625
2633
  * @param {number} button
2626
2634
  * @return {boolean}
2627
2635
  * @memberof Input */
2628
- const mouseWasReleased = keyWasReleased;
2636
+ function mouseWasReleased(button) { return keyWasReleased(button); }
2629
2637
 
2630
2638
  /** Mouse pos in world space
2631
2639
  * @type {Vector2}
@@ -2864,7 +2872,9 @@ function gamepadsUpdate()
2864
2872
  const button = gamepad.buttons[j];
2865
2873
  const wasDown = gamepadIsDown(j,i);
2866
2874
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
2867
- isUsingGamepad ||= !i && button.pressed;
2875
+ if (!button.value || button.value > .9) // must be a full press
2876
+ if (!i && button.pressed)
2877
+ isUsingGamepad = true;
2868
2878
  }
2869
2879
 
2870
2880
  if (gamepadDirectionEmulateStick)
@@ -3082,8 +3092,6 @@ function touchGamepadRender()
3082
3092
  * @namespace Audio
3083
3093
  */
3084
3094
 
3085
-
3086
-
3087
3095
  /** Audio context used by the engine
3088
3096
  * @type {AudioContext}
3089
3097
  * @memberof Audio */
@@ -3120,17 +3128,17 @@ class Sound
3120
3128
  {
3121
3129
  /** Create a sound object and cache the zzfx samples for later use
3122
3130
  * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
3123
- * @param {number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
3131
+ * @param {number} [range=soundDefaultRange] - World space max range of sound
3124
3132
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
3125
3133
  */
3126
3134
  constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
3127
3135
  {
3128
3136
  if (!soundEnable || headlessMode) return;
3129
3137
 
3130
- /** @property {number} - World space max range of sound, will not play if camera is farther away */
3138
+ /** @property {number} - World space max range of sound */
3131
3139
  this.range = range;
3132
3140
 
3133
- /** @property {number} - At what percentage of range should it start tapering off */
3141
+ /** @property {number} - At what percentage of range should it start tapering */
3134
3142
  this.taper = taper;
3135
3143
 
3136
3144
  /** @property {number} - How much to randomize frequency each time sound plays */
@@ -3246,8 +3254,8 @@ class SoundWave extends Sound
3246
3254
  /** Create a sound object and cache the wave file for later use
3247
3255
  * @param {string} filename - Filename of audio file to load
3248
3256
  * @param {number} [randomness] - How much to randomize frequency each time sound plays
3249
- * @param {number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
3250
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
3257
+ * @param {number} [range=soundDefaultRange] - World space max range of sound
3258
+ * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
3251
3259
  * @param {Function} [onloadCallback] - callback function to call when sound is loaded
3252
3260
  */
3253
3261
  constructor(filename, randomness=0, range, taper, onloadCallback)
@@ -3255,17 +3263,26 @@ class SoundWave extends Sound
3255
3263
  super(undefined, range, taper);
3256
3264
  if (!soundEnable || headlessMode) return;
3257
3265
 
3266
+ /** @property {Function} - callback function to call when sound is loaded */
3267
+ this.onloadCallback = onloadCallback;
3258
3268
  this.randomness = randomness;
3259
- fetch(filename)
3260
- .then(response => response.arrayBuffer())
3261
- .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
3262
- .then(audioBuffer =>
3263
- {
3264
- this.sampleChannels = [];
3265
- for (let i = audioBuffer.numberOfChannels; i--;)
3266
- this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
3267
- this.sampleRate = audioBuffer.sampleRate;
3268
- }).then(() => onloadCallback && onloadCallback(this));
3269
+ this.loadSound(filename);
3270
+ }
3271
+
3272
+ /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
3273
+ * @param {string} filename
3274
+ * @return {Promise<void>} */
3275
+ async loadSound(filename)
3276
+ {
3277
+ const response = await fetch(filename);
3278
+ const arrayBuffer = await response.arrayBuffer();
3279
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
3280
+ this.sampleChannels = [];
3281
+ for (let i = audioBuffer.numberOfChannels; i--;)
3282
+ this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
3283
+ this.sampleRate = audioBuffer.sampleRate;
3284
+ if (this.onloadCallback)
3285
+ this.onloadCallback();
3269
3286
  }
3270
3287
  }
3271
3288
 
@@ -3539,75 +3556,45 @@ function zzfxG
3539
3556
  * LittleJS Tile Layer System
3540
3557
  * - Caches arrays of tiles to off screen canvas for fast rendering
3541
3558
  * - Unlimited numbers of layers, allocates canvases as needed
3542
- * - Interfaces with EngineObject for collision
3543
- * - Collision layer is separate from visible layers
3544
- * - It is recommended to have a visible layer that matches the collision
3545
3559
  * - Tile layers can be drawn to using their context with canvas2d
3546
3560
  * - Drawn directly to the main canvas without using WebGL
3561
+ * - Tile layers can also have collision with EngineObjects
3547
3562
  * @namespace TileCollision
3548
3563
  */
3549
3564
 
3565
+ ///////////////////////////////////////////////////////////////////////////////
3566
+ // Tile Layer System
3550
3567
 
3551
-
3552
- /** The tile collision layer grid, use setTileCollisionData and getTileCollisionData to access
3553
- * @type {Array<number>}
3554
- * @memberof TileCollision */
3555
- let tileCollision = [];
3556
-
3557
- /** Size of the tile collision layer 2d grid
3558
- * @type {Vector2}
3559
- * @memberof TileCollision */
3560
- let tileCollisionSize = vec2();
3561
-
3562
- /** Clear and initialize tile collision
3563
- * @param {Vector2} size - width and height of tile collision 2d grid
3564
- * @memberof TileCollision */
3565
- function initTileCollision(size)
3566
- {
3567
- tileCollisionSize = size;
3568
- tileCollision = [];
3569
- for (let i=tileCollision.length = tileCollisionSize.area(); i--;)
3570
- tileCollision[i] = 0;
3571
- }
3572
-
3573
- /** Set tile collision data for a given cell in the grid
3574
- * @param {Vector2} pos
3575
- * @param {number} [data]
3568
+ /** Keep track of all tile layers with collision
3569
+ * @type {Array<TileCollisionLayer>}
3576
3570
  * @memberof TileCollision */
3577
- function setTileCollisionData(pos, data=0)
3578
- {
3579
- pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
3580
- }
3571
+ let tileCollisionLayers = [];
3581
3572
 
3582
3573
  /** Get tile collision data for a given cell in the grid
3583
- * @param {Vector2} pos
3584
- * @return {number}
3585
- * @memberof TileCollision */
3574
+ * @param {Vector2} pos
3575
+ * @return {number}
3576
+ * @memberof TileCollision */
3586
3577
  function getTileCollisionData(pos)
3587
3578
  {
3588
- return pos.arrayCheck(tileCollisionSize) ? tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] : 0;
3579
+ // check all tile collision layers
3580
+ for (const layer of tileCollisionLayers)
3581
+ if (pos.arrayCheck(layer.size))
3582
+ return layer.getCollisionData(pos);
3583
+ return 0;
3589
3584
  }
3590
3585
 
3591
- /** Check if collision with another object should occur
3586
+ /** Check if a tile layer collides with another object
3592
3587
  * @param {Vector2} pos
3593
3588
  * @param {Vector2} [size=(0,0)]
3594
3589
  * @param {EngineObject} [object]
3595
- * @return {boolean}
3590
+ * @return {TileCollisionLayer}
3596
3591
  * @memberof TileCollision */
3597
3592
  function tileCollisionTest(pos, size=vec2(), object)
3598
3593
  {
3599
- const minX = max(pos.x - size.x/2|0, 0);
3600
- const minY = max(pos.y - size.y/2|0, 0);
3601
- const maxX = min(pos.x + size.x/2, tileCollisionSize.x);
3602
- const maxY = min(pos.y + size.y/2, tileCollisionSize.y);
3603
- for (let y = minY; y < maxY; ++y)
3604
- for (let x = minX; x < maxX; ++x)
3605
- {
3606
- const tileData = tileCollision[y*tileCollisionSize.x+x];
3607
- if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
3608
- return true;
3609
- }
3610
- return false;
3594
+ // check all tile collision layers
3595
+ for (const layer of tileCollisionLayers)
3596
+ if (layer.collisionTest(pos, size, object))
3597
+ return layer;
3611
3598
  }
3612
3599
 
3613
3600
  /** Return the center of first tile hit, undefined if nothing was hit.
@@ -3619,49 +3606,17 @@ function tileCollisionTest(pos, size=vec2(), object)
3619
3606
  * @memberof TileCollision */
3620
3607
  function tileCollisionRaycast(posStart, posEnd, object)
3621
3608
  {
3622
- // test if a ray collides with tiles from start to end
3623
- // todo: a way to get the exact hit point, it must still be inside the hit tile
3624
- const delta = posEnd.subtract(posStart);
3625
- const totalLength = delta.length();
3626
- const normalizedDelta = delta.normalize();
3627
- const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
3628
- const flooredPosStart = posStart.floor();
3629
-
3630
- // setup iteration variables
3631
- let pos = flooredPosStart;
3632
- let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
3633
- let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
3634
-
3635
- while (true)
3636
- {
3637
- // check for tile collision
3638
- const tileData = getTileCollisionData(pos);
3639
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
3640
- {
3641
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
3642
- debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
3643
- return pos.add(vec2(.5));
3644
- }
3645
-
3646
- // check if past the end
3647
- if (xi > totalLength && yi > totalLength)
3648
- break;
3649
-
3650
- // get coordinates of the next tile to check
3651
- if (xi > yi)
3652
- pos.y += sign(delta.y), yi += unit.y;
3653
- else
3654
- pos.x += sign(delta.x), xi += unit.x;
3609
+ // check all tile collision layers
3610
+ for (const layer of tileCollisionLayers)
3611
+ {
3612
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object)
3613
+ if (hitPos)
3614
+ return hitPos;
3655
3615
  }
3656
-
3657
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
3658
3616
  }
3659
3617
 
3660
- ///////////////////////////////////////////////////////////////////////////////
3661
- // Tile Layer Rendering System
3662
-
3663
3618
  /**
3664
- * Tile layer data object stores info about how to render a tile
3619
+ * Tile layer data object stores info about how to draw a tile
3665
3620
  * @example
3666
3621
  * // create tile layer data with tile index 0 and random orientation and color
3667
3622
  * const tileIndex = 0;
@@ -3693,28 +3648,27 @@ class TileLayerData
3693
3648
  clear() { this.tile = this.direction = 0; this.mirror = false; this.color = new Color; }
3694
3649
  }
3695
3650
 
3651
+ ///////////////////////////////////////////////////////////////////////////////
3696
3652
  /**
3697
3653
  * Tile Layer - cached rendering system for tile layers
3698
3654
  * - Each Tile layer is rendered to an off screen canvas
3699
3655
  * - To allow dynamic modifications, layers are rendered using canvas 2d
3700
3656
  * - Some devices like mobile phones are limited to 4k texture resolution
3701
- * - So with 16x16 tiles this limits layers to 256x256 on mobile devices
3657
+ * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
3702
3658
  * @extends EngineObject
3703
3659
  * @example
3704
- * // create tile collision and visible tile layer
3705
- * initTileCollision(vec2(200,100));
3706
- * const tileLayer = new TileLayer();
3660
+ * const tileLayer = new TileLayer(vec2(), vec2(200,100));
3707
3661
  */
3708
3662
  class TileLayer extends EngineObject
3709
3663
  {
3710
3664
  /** Create a tile layer object
3711
- * @param {Vector2} [position=(0,0)] - World space position
3712
- * @param {Vector2} [size=tileCollisionSize] - World space size
3713
- * @param {TileInfo} [tileInfo] - Tile info for layer
3714
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
3715
- * @param {number} [renderOrder] - Objects are sorted by renderOrder
3665
+ * @param {Vector2} [position=(0,0)] - World space position
3666
+ * @param {Vector2} [size=(1,1)] - World space size
3667
+ * @param {TileInfo} [tileInfo] - Tile info for layer
3668
+ * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
3669
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
3716
3670
  */
3717
- constructor(position, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderOrder=0)
3671
+ constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0)
3718
3672
  {
3719
3673
  super(position, size, tileInfo, 0, undefined, renderOrder);
3720
3674
 
@@ -3726,6 +3680,10 @@ class TileLayer extends EngineObject
3726
3680
  this.scale = scale;
3727
3681
  /** @property {boolean} - If true this layer will render to overlay canvas and appear above all objects */
3728
3682
  this.isOverlay = false;
3683
+ // set no friction by default, applied friction is max of both objects
3684
+ this.friction = 0;
3685
+ // set no elasticity by default, applied elasticity is max of both objects
3686
+ this.elasticity = 0;
3729
3687
 
3730
3688
  // init tile data
3731
3689
  this.data = [];
@@ -3922,13 +3880,151 @@ class TileLayer extends EngineObject
3922
3880
  * @param {number} [angle=0] */
3923
3881
  drawRect(pos, size, color, angle)
3924
3882
  { this.drawTile(pos, size, undefined, color, angle); }
3883
+ }
3884
+
3885
+ ///////////////////////////////////////////////////////////////////////////////
3886
+ /**
3887
+ * Tile Collision Layer - a tile layer with collision
3888
+ * - adds collision data and functions to TileLayer
3889
+ * - there can be multiple tile collision layers
3890
+ * - tile collison layers should not overlap each other
3891
+ * @extends TileLayer
3892
+ */
3893
+ class TileCollisionLayer extends TileLayer
3894
+ {
3895
+ /** Create a tile layer object
3896
+ * @param {Vector2} [position=(0,0)] - World space position
3897
+ * @param {Vector2} [size=(0,0)] - World space size
3898
+ * @param {TileInfo} [tileInfo] - Tile info for layer
3899
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
3900
+ */
3901
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
3902
+ {
3903
+ const scale = vec2(1); // collision layers are not scaled
3904
+ super(position, size.floor(), tileInfo, scale, renderOrder);
3905
+
3906
+ /** @property {Array<number>} - The tile collision grid */
3907
+ this.collisionData = [];
3908
+ this.initCollision(this.size);
3909
+
3910
+ // keep track of all collision layers
3911
+ tileCollisionLayers.push(this);
3912
+ }
3913
+
3914
+ /** Destroy this collision layer */
3915
+ destroy()
3916
+ {
3917
+ if (this.destroyed)
3918
+ return;
3919
+
3920
+ // remove from collision layers array and destroy
3921
+ const index = tileCollisionLayers.indexOf(this);
3922
+ ASSERT(index >= 0, 'tile collision layer not found in array');
3923
+ tileCollisionLayers.splice(index, 1);
3924
+ super.destroy();
3925
+ }
3926
+
3927
+ /** Clear and initialize tile collision to new size
3928
+ * @param {Vector2} size - width and height of tile collision 2d grid */
3929
+ initCollision(size)
3930
+ {
3931
+ this.size = size.floor();
3932
+ this.collisionData = [];
3933
+ this.collisionData.length = size.area();
3934
+ this.collisionData.fill(0);
3935
+ }
3936
+
3937
+ /** Set tile collision data for a given cell in the grid
3938
+ * @param {Vector2} pos
3939
+ * @param {number} [data] */
3940
+ setCollisionData(pos, data=1)
3941
+ {
3942
+ const i = (pos.y|0)*this.size.x + pos.x|0;
3943
+ pos.arrayCheck(this.size) && (this.collisionData[i] = data);
3944
+ }
3945
+
3946
+ /** Get tile collision data for a given cell in the grid
3947
+ * @param {Vector2} pos
3948
+ * @return {number} */
3949
+ getCollisionData(pos)
3950
+ {
3951
+ const i = (pos.y|0)*this.size.x + pos.x|0;
3952
+ return pos.arrayCheck(this.size) ? this.collisionData[i] : 0;
3953
+ }
3954
+
3955
+ /** Check if collision with another object should occur
3956
+ * @param {Vector2} pos
3957
+ * @param {Vector2} [size=(0,0)]
3958
+ * @param {EngineObject} [object]
3959
+ * @return {boolean} */
3960
+ collisionTest(pos, size=vec2(), object)
3961
+ {
3962
+ const minX = max(pos.x - size.x/2|0, 0);
3963
+ const minY = max(pos.y - size.y/2|0, 0);
3964
+ const maxX = min(pos.x + size.x/2, this.size.x);
3965
+ const maxY = min(pos.y + size.y/2, this.size.y);
3966
+ for (let y = minY; y < maxY; ++y)
3967
+ for (let x = minX; x < maxX; ++x)
3968
+ {
3969
+ // check if the object should collide with this tile
3970
+ const tileData = this.collisionData[y*this.size.x+x];
3971
+ if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
3972
+ return true;
3973
+ }
3974
+ return false;
3975
+ }
3976
+
3977
+ /** Return the center of first tile hit, undefined if nothing was hit.
3978
+ * This does not return the exact intersection, but the center of the tile hit.
3979
+ * @param {Vector2} posStart
3980
+ * @param {Vector2} posEnd
3981
+ * @param {EngineObject} [object]
3982
+ * @return {Vector2} */
3983
+ collisionRaycast(posStart, posEnd, object)
3984
+ {
3985
+ // test if a ray collides with tiles from start to end
3986
+ // todo: a way to get the exact hit point, it must still be inside the hit tile
3987
+ const delta = posEnd.subtract(posStart);
3988
+ const totalLength = delta.length();
3989
+ const normalizedDelta = delta.normalize();
3990
+ const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
3991
+ const flooredPosStart = posStart.floor();
3992
+
3993
+ // setup iteration variables
3994
+ let pos = flooredPosStart;
3995
+ let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
3996
+ let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
3997
+
3998
+ // use line drawing algorithm to test for collisions
3999
+ while (true)
4000
+ {
4001
+ // check for tile collision
4002
+ const tileData = this.getCollisionData(pos);
4003
+ if (tileData && (!object || object.collideWithTile(tileData, pos)))
4004
+ {
4005
+ debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
4006
+ debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
4007
+ return pos.add(vec2(.5));
4008
+ }
4009
+
4010
+ // check if past the end
4011
+ if (xi > totalLength && yi > totalLength)
4012
+ break;
4013
+
4014
+ // get coordinates of next tile to check
4015
+ if (xi > yi)
4016
+ pos.y += sign(delta.y), yi += unit.y;
4017
+ else
4018
+ pos.x += sign(delta.x), xi += unit.x;
4019
+ }
4020
+
4021
+ debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
4022
+ }
3925
4023
  }
3926
4024
  /**
3927
4025
  * LittleJS Particle System
3928
4026
  */
3929
4027
 
3930
-
3931
-
3932
4028
  /**
3933
4029
  * Particle Emitter - Spawns particles with the given settings
3934
4030
  * @extends EngineObject
@@ -4273,8 +4369,6 @@ class Particle extends EngineObject
4273
4369
  * @namespace Medals
4274
4370
  */
4275
4371
 
4276
-
4277
-
4278
4372
  /** List of all medals
4279
4373
  * @type {Object}
4280
4374
  * @memberof Medals */
@@ -4462,8 +4556,6 @@ class Medal
4462
4556
  * @namespace WebGL
4463
4557
  */
4464
4558
 
4465
-
4466
-
4467
4559
  /** The WebGL canvas which appears above the main canvas and below the overlay canvas
4468
4560
  * @type {HTMLCanvasElement}
4469
4561
  * @memberof WebGL */
@@ -4662,7 +4754,19 @@ function glCreateTexture(image)
4662
4754
  const texture = glContext.createTexture();
4663
4755
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
4664
4756
  if (image && image.width)
4757
+ {
4665
4758
  glSetTextureData(texture, image);
4759
+
4760
+ const isPowerOfTwo = (value)=> !(value & (value - 1));
4761
+ if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
4762
+ {
4763
+ // use mipmap filtering
4764
+ glContext.generateMipmap(glContext.TEXTURE_2D);
4765
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR_MIPMAP_LINEAR);
4766
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, glContext.LINEAR);
4767
+ return texture;
4768
+ }
4769
+ }
4666
4770
  else
4667
4771
  {
4668
4772
  // create a white texture
@@ -4670,7 +4774,7 @@ function glCreateTexture(image)
4670
4774
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
4671
4775
  }
4672
4776
 
4673
- // use point filtering for pixelated rendering
4777
+ // set texture filtering
4674
4778
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
4675
4779
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
4676
4780
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, filter);
@@ -4786,8 +4890,6 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=-1, rgba
4786
4890
  * @namespace Engine
4787
4891
  */
4788
4892
 
4789
-
4790
-
4791
4893
  /** Name of engine
4792
4894
  * @type {string}
4793
4895
  * @default
@@ -4798,7 +4900,7 @@ const engineName = 'LittleJS';
4798
4900
  * @type {string}
4799
4901
  * @default
4800
4902
  * @memberof Engine */
4801
- const engineVersion = '1.11.17';
4903
+ const engineVersion = '1.12.6';
4802
4904
 
4803
4905
  /** Frames per second to update
4804
4906
  * @type {number}
@@ -4880,7 +4982,7 @@ function engineAddPlugin(updateFunction, renderFunction)
4880
4982
  * @param {Array<string>} [imageSources=[]] - List of images to load
4881
4983
  * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
4882
4984
  * @memberof Engine */
4883
- function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
4985
+ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
4884
4986
  {
4885
4987
  ASSERT(!mainContext, 'engine already initialized');
4886
4988
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
@@ -5037,16 +5139,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5037
5139
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5038
5140
  }
5039
5141
 
5040
- function startEngine()
5142
+ // wait for gameInit to load
5143
+ async function startEngine()
5041
5144
  {
5042
- new Promise((resolve) => resolve(gameInit())).then(engineUpdate);
5145
+ await gameInit();
5146
+ engineUpdate();
5043
5147
  }
5044
-
5045
5148
  if (headlessMode)
5046
- {
5047
- startEngine();
5048
- return;
5049
- }
5149
+ return startEngine();
5050
5150
 
5051
5151
  // setup html
5052
5152
  const styleRoot =
@@ -5122,8 +5222,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5122
5222
  }));
5123
5223
  }
5124
5224
 
5125
- // load all of the images
5126
- Promise.all(promises).then(startEngine);
5225
+ // wait for all the promises to finish
5226
+ await Promise.all(promises);
5227
+ return startEngine();
5127
5228
  }
5128
5229
 
5129
5230
  /** Update each engine object, remove destroyed objects, and update time
@@ -5392,3 +5493,2777 @@ function drawEngineSplashScreen(t)
5392
5493
  x.restore();
5393
5494
  }
5394
5495
 
5496
+ /**
5497
+ * LittleJS Newgrounds API
5498
+ * - NewgroundsMedal extends Medal with Newgrounds API functionality
5499
+ * - Call new NewgroundsPlugin() to setup Newgrounds
5500
+ * - Uses CryptoJS for encryption if optional cipher is provided
5501
+ * - Keeps connection alive and logs views
5502
+ * - Functions to interact with scoreboards
5503
+ * - Functions to unlock medals
5504
+ */
5505
+
5506
+ /** Global Newgrounds object
5507
+ * @type {NewgroundsPlugin}
5508
+ * @memberof Medal */
5509
+ let newgrounds;
5510
+
5511
+ ///////////////////////////////////////////////////////////////////////////////
5512
+ /**
5513
+ * Newgrounds medal auto unlocks in newgrounds API
5514
+ * @extends Medal
5515
+ */
5516
+ class NewgroundsMedal extends Medal
5517
+ {
5518
+ /** Create a newgrounds medal object and adds it to the list of medals
5519
+ * @param {Number} id - The unique identifier of the medal
5520
+ * @param {String} name - Name of the medal
5521
+ * @param {String} [description] - Description of the medal
5522
+ * @param {String} [icon] - Icon for the medal
5523
+ * @param {String} [src] - Image location for the medal
5524
+ */
5525
+ constructor(id, name, description, icon, src)
5526
+ { super(id, name, description, icon, src); }
5527
+
5528
+ /** Unlocks a medal if not already unlocked */
5529
+ unlock()
5530
+ {
5531
+ super.unlock();
5532
+ newgrounds && newgrounds.unlockMedal(this.id);
5533
+ }
5534
+ }
5535
+
5536
+ ///////////////////////////////////////////////////////////////////////////////
5537
+ /**
5538
+ * Newgrounds API object
5539
+ */
5540
+ class NewgroundsPlugin
5541
+ {
5542
+ /** Create the global newgrounds object
5543
+ * @param {string} app_id - The newgrounds App ID
5544
+ * @param {string} [cipher] - The encryption Key (AES-128/Base64)
5545
+ * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
5546
+ * @example
5547
+ * // create the newgrounds object, replace the app id with your own
5548
+ * const app_id = 'your_app_id_here';
5549
+ * new NewgroundsPlugin(app_id);
5550
+ */
5551
+ constructor(app_id, cipher, cryptoJS)
5552
+ {
5553
+ ASSERT(!newgrounds, 'there can only be one newgrounds object');
5554
+ ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
5555
+
5556
+ newgrounds = this; // set global newgrounds object
5557
+ this.app_id = app_id;
5558
+ this.cipher = cipher;
5559
+ this.cryptoJS = cryptoJS;
5560
+ this.host = location ? location.hostname : '';
5561
+
5562
+ // get session id from url search params
5563
+ const url = new URL(location.href);
5564
+ this.session_id = url.searchParams.get('ngio_session_id');
5565
+
5566
+ if (!this.session_id)
5567
+ return; // only use newgrounds when logged in
5568
+
5569
+ // get medals
5570
+ const medalsResult = this.call('Medal.getList');
5571
+ this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
5572
+ debugMedals && console.log(this.medals);
5573
+ for (const newgroundsMedal of this.medals)
5574
+ {
5575
+ const medal = medals[newgroundsMedal['id']];
5576
+ if (medal)
5577
+ {
5578
+ // copy newgrounds medal data
5579
+ medal.image = new Image;
5580
+ medal.image.src = newgroundsMedal['icon'];
5581
+ medal.name = newgroundsMedal['name'];
5582
+ medal.description = newgroundsMedal['description'];
5583
+ medal.unlocked = newgroundsMedal['unlocked'];
5584
+ medal.difficulty = newgroundsMedal['difficulty'];
5585
+ medal.value = newgroundsMedal['value'];
5586
+
5587
+ if (medal.value) // add value to description
5588
+ medal.description = medal.description + ` (${ medal.value })`;
5589
+ }
5590
+ }
5591
+
5592
+ // get scoreboards
5593
+ const scoreboardResult = this.call('ScoreBoard.getBoards');
5594
+ this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
5595
+ debugMedals && console.log(this.scoreboards);
5596
+
5597
+ // keep the session alive with a ping every minute
5598
+ const keepAliveMS = 60 * 1e3;
5599
+ setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
5600
+ }
5601
+
5602
+ /** Send message to unlock a medal by id
5603
+ * @param {number} id - The medal id */
5604
+ unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
5605
+
5606
+ /** Send message to post score
5607
+ * @param {number} id - The scoreboard id
5608
+ * @param {number} value - The score value */
5609
+ postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
5610
+
5611
+ /** Get scores from a scoreboard
5612
+ * @param {number} id - The scoreboard id
5613
+ * @param {string} [user] - A user's id or name
5614
+ * @param {number} [social] - If true, only social scores will be loaded
5615
+ * @param {number} [skip] - Number of scores to skip before start
5616
+ * @param {number} [limit] - Number of scores to include in the list
5617
+ * @return {Object} - The response JSON object
5618
+ */
5619
+ getScores(id, user, social=0, skip=0, limit=10)
5620
+ { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
5621
+
5622
+ /** Send message to log a view */
5623
+ logView() { return this.call('App.logView', {'host':this.host}, true); }
5624
+
5625
+ /** Send a message to call a component of the Newgrounds API
5626
+ * @param {string} component - Name of the component
5627
+ * @param {Object} [parameters] - Parameters to use for call
5628
+ * @param {boolean} [async] - If true, don't wait for response before continuing
5629
+ * @return {Object} - The response JSON object
5630
+ */
5631
+ call(component, parameters, async=false)
5632
+ {
5633
+ const call = {'component':component, 'parameters':parameters};
5634
+ if (this.cipher)
5635
+ {
5636
+ // encrypt using AES-128 Base64 with cryptoJS
5637
+ const cryptoJS = this.cryptoJS;
5638
+ const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
5639
+ const iv = cryptoJS['lib']['WordArray']['random'](16);
5640
+ const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
5641
+ call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
5642
+ call['parameters'] = 0;
5643
+ }
5644
+
5645
+ // build the input object
5646
+ const input =
5647
+ {
5648
+ 'app_id': this.app_id,
5649
+ 'session_id': this.session_id,
5650
+ 'call': call
5651
+ };
5652
+
5653
+ // build post data
5654
+ const formData = new FormData();
5655
+ formData.append('input', JSON.stringify(input));
5656
+
5657
+ // send post data
5658
+ const xmlHttp = new XMLHttpRequest();
5659
+ const url = 'https://newgrounds.io/gateway_v3.php';
5660
+ xmlHttp.open('POST', url, !debugMedals && async);
5661
+ try { xmlHttp.send(formData); }
5662
+ catch(e)
5663
+ {
5664
+ debugMedals && console.log('newgrounds call failed', e);
5665
+ return;
5666
+ }
5667
+ debugMedals && console.log(xmlHttp.responseText);
5668
+ return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
5669
+ }
5670
+ }
5671
+
5672
+ /**
5673
+ * LittleJS Post Processing Plugin
5674
+ * - Supports shadertoy style post processing shaders
5675
+ * - call new new PostProcessPlugin() to setup post processing
5676
+ * - can be enabled to pass other canvases through a final shader
5677
+ */
5678
+
5679
+ ///////////////////////////////////////////////////////////////////////////////
5680
+
5681
+ /** Global Post Process plugin object
5682
+ * @type {PostProcessPlugin} */
5683
+ let postProcess;
5684
+
5685
+ /////////////////////////////////////////////////////////////////////////
5686
+ /**
5687
+ * UI System Global Object
5688
+ */
5689
+ class PostProcessPlugin
5690
+ {
5691
+ /** Create global post processing shader
5692
+ * @param {string} shaderCode
5693
+ * @param {boolean} [includeOverlay]
5694
+ * @example
5695
+ * // create the post process plugin object
5696
+ * new PostProcessPlugin(shaderCode);
5697
+ */
5698
+ constructor(shaderCode, includeOverlay=false)
5699
+ {
5700
+ ASSERT(!postProcess, 'Post process already initialized');
5701
+ postProcess = this;
5702
+
5703
+ if (headlessMode) return;
5704
+ if (!shaderCode) // default shader pass through
5705
+ shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
5706
+
5707
+ /** @property {WebGLProgram} - Shader for post processing */
5708
+ this.shader = glCreateProgram(
5709
+ '#version 300 es\n' + // specify GLSL ES version
5710
+ 'precision highp float;'+ // use highp for better accuracy
5711
+ 'in vec2 p;'+ // position
5712
+ 'void main(){'+ // shader entry point
5713
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
5714
+ '}' // end of shader
5715
+ ,
5716
+ '#version 300 es\n' + // specify GLSL ES version
5717
+ 'precision highp float;'+ // use highp for better accuracy
5718
+ 'uniform sampler2D iChannel0;'+ // input texture
5719
+ 'uniform vec3 iResolution;'+ // size of output texture
5720
+ 'uniform float iTime;'+ // time
5721
+ 'out vec4 c;'+ // out color
5722
+ '\n' + shaderCode + '\n'+ // insert custom shader code
5723
+ 'void main(){'+ // shader entry point
5724
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
5725
+ 'c.a=1.;'+ // always use full alpha
5726
+ '}' // end of shader
5727
+ );
5728
+
5729
+ /** @property {WebGLTexture} - Texture for post processing */
5730
+ this.texture = glCreateTexture();
5731
+
5732
+ /** @property {boolean} - Should overlay canvas be included in post processing */
5733
+ this.includeOverlay = includeOverlay;
5734
+
5735
+ // Render the post processing shader, called automatically by the engine
5736
+ engineAddPlugin(undefined, postProcessRender);
5737
+ function postProcessRender()
5738
+ {
5739
+ if (headlessMode) return;
5740
+
5741
+ // prepare to render post process shader
5742
+ if (glEnable)
5743
+ {
5744
+ glFlush(); // clear out the buffer
5745
+ mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
5746
+ }
5747
+ else
5748
+ {
5749
+ // set the viewport
5750
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
5751
+ }
5752
+
5753
+ if (postProcess.includeOverlay)
5754
+ {
5755
+ // copy overlay canvas so it will be included in post processing
5756
+ mainContext.drawImage(overlayCanvas, 0, 0);
5757
+ overlayCanvas.width |= 0;
5758
+ }
5759
+
5760
+ // setup shader program to draw one triangle
5761
+ glContext.useProgram(postProcess.shader);
5762
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
5763
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
5764
+ glContext.disable(glContext.BLEND);
5765
+
5766
+ // set textures, pass in the 2d canvas and gl canvas in separate texture channels
5767
+ glContext.activeTexture(glContext.TEXTURE0);
5768
+ glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
5769
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
5770
+
5771
+ // set vertex position attribute
5772
+ const vertexByteStride = 8;
5773
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
5774
+ glContext.enableVertexAttribArray(pLocation);
5775
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
5776
+
5777
+ // set uniforms and draw
5778
+ const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
5779
+ glContext.uniform1i(uniformLocation('iChannel0'), 0);
5780
+ glContext.uniform1f(uniformLocation('iTime'), time);
5781
+ glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
5782
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
5783
+ }
5784
+ }
5785
+ }
5786
+
5787
+ /**
5788
+ * LittleJS ZzFXM Plugin
5789
+ */
5790
+
5791
+ /**
5792
+ * Music Object - Stores a zzfx music track for later use
5793
+ *
5794
+ * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
5795
+ * @example
5796
+ * // create some music
5797
+ * const music_example = new Music(
5798
+ * [
5799
+ * [ // instruments
5800
+ * [,0,400] // simple note
5801
+ * ],
5802
+ * [ // patterns
5803
+ * [ // pattern 1
5804
+ * [ // channel 0
5805
+ * 0, -1, // instrument 0, left speaker
5806
+ * 1, 0, 9, 1 // channel notes
5807
+ * ],
5808
+ * [ // channel 1
5809
+ * 0, 1, // instrument 0, right speaker
5810
+ * 0, 12, 17, -1 // channel notes
5811
+ * ]
5812
+ * ],
5813
+ * ],
5814
+ * [0, 0, 0, 0], // sequence, play pattern 0 four times
5815
+ * 90 // BPM
5816
+ * ]);
5817
+ *
5818
+ * // play the music
5819
+ * music_example.play();
5820
+ */
5821
+ class ZzFXMusic extends Sound
5822
+ {
5823
+ /** Create a music object and cache the zzfx music samples for later use
5824
+ * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
5825
+ */
5826
+ constructor(zzfxMusic)
5827
+ {
5828
+ super(undefined);
5829
+
5830
+ if (!soundEnable || headlessMode) return;
5831
+ this.randomness = 0;
5832
+ this.sampleChannels = zzfxM(...zzfxMusic);
5833
+ this.sampleRate = zzfxR;
5834
+ }
5835
+
5836
+ /** Play the music
5837
+ * @param {number} [volume=1] - How much to scale volume by
5838
+ * @param {boolean} [loop] - True if the music should loop
5839
+ * @return {AudioBufferSourceNode} - The audio source node
5840
+ */
5841
+ playMusic(volume, loop=false)
5842
+ { return super.play(undefined, volume, 1, 1, loop); }
5843
+ }
5844
+
5845
+ ///////////////////////////////////////////////////////////////////////////////
5846
+ // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
5847
+
5848
+ /** Generate samples for a ZzFM song with given parameters
5849
+ * @param {Array} instruments - Array of ZzFX sound parameters
5850
+ * @param {Array} patterns - Array of pattern data
5851
+ * @param {Array} sequence - Array of pattern indexes
5852
+ * @param {number} [BPM] - Playback speed of the song in BPM
5853
+ * @return {Array} - Left and right channel sample data */
5854
+ function zzfxM(instruments, patterns, sequence, BPM = 125)
5855
+ {
5856
+ let i, j, k;
5857
+ let instrumentParameters;
5858
+ let note;
5859
+ let sample;
5860
+ let patternChannel;
5861
+ let notFirstBeat;
5862
+ let stop;
5863
+ let instrument;
5864
+ let attenuation;
5865
+ let outSampleOffset;
5866
+ let isSequenceEnd;
5867
+ let sampleOffset = 0;
5868
+ let nextSampleOffset;
5869
+ let sampleBuffer = [];
5870
+ let leftChannelBuffer = [];
5871
+ let rightChannelBuffer = [];
5872
+ let channelIndex = 0;
5873
+ let panning = 0;
5874
+ let hasMore = 1;
5875
+ let sampleCache = {};
5876
+ let beatLength = zzfxR / BPM * 60 >> 2;
5877
+
5878
+ // for each channel in order until there are no more
5879
+ for (; hasMore; channelIndex++) {
5880
+
5881
+ // reset current values
5882
+ sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
5883
+
5884
+ // for each pattern in sequence
5885
+ sequence.forEach((patternIndex, sequenceIndex) => {
5886
+ // get pattern for current channel, use empty 1 note pattern if none found
5887
+ patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
5888
+
5889
+ // check if there are more channels
5890
+ hasMore |= patterns[patternIndex][channelIndex]&&1;
5891
+
5892
+ // get next offset, use the length of first channel
5893
+ nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
5894
+ // for each beat in pattern, plus one extra if end of sequence
5895
+ isSequenceEnd = sequenceIndex == sequence.length - 1;
5896
+ for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
5897
+
5898
+ // <channel-note>
5899
+ note = patternChannel[i];
5900
+
5901
+ // stop if end, different instrument or new note
5902
+ stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
5903
+ instrument != (patternChannel[0] || 0) || note | 0;
5904
+
5905
+ // fill buffer with samples for previous beat, most cpu intensive part
5906
+ for (j = 0; j < beatLength && notFirstBeat;
5907
+
5908
+ // fade off attenuation at end of beat if stopping note, prevents clicking
5909
+ j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
5910
+ ) {
5911
+ // copy sample to stereo buffers with panning
5912
+ sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
5913
+ leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
5914
+ rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
5915
+ }
5916
+
5917
+ // set up for next note
5918
+ if (note) {
5919
+ // set attenuation
5920
+ attenuation = note % 1;
5921
+ panning = patternChannel[1] || 0;
5922
+ if (note |= 0) {
5923
+ // get cached sample
5924
+ sampleBuffer = sampleCache[
5925
+ [
5926
+ instrument = patternChannel[sampleOffset = 0] || 0,
5927
+ note
5928
+ ]
5929
+ ] = sampleCache[[instrument, note]] || (
5930
+ // add sample to cache
5931
+ instrumentParameters = [...instruments[instrument]],
5932
+ instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
5933
+
5934
+ // allow negative values to stop notes
5935
+ note > 0 ? zzfxG(...instrumentParameters) : []
5936
+ );
5937
+ }
5938
+ }
5939
+ }
5940
+
5941
+ // update the sample offset
5942
+ outSampleOffset = nextSampleOffset;
5943
+ });
5944
+ }
5945
+
5946
+ return [leftChannelBuffer, rightChannelBuffer];
5947
+ }
5948
+
5949
+ /**
5950
+ * LittleJS User Interface Plugin
5951
+ * - call new UISystemPlugin() to setup the UI system
5952
+ * - Nested Menus
5953
+ * - Text
5954
+ * - Buttons
5955
+ * - Checkboxes
5956
+ * - Images
5957
+ */
5958
+
5959
+ ///////////////////////////////////////////////////////////////////////////////
5960
+
5961
+ /** Global UI system plugin object
5962
+ * @type {UISystemPlugin} */
5963
+ let uiSystem;
5964
+
5965
+ ///////////////////////////////////////////////////////////////////////////////
5966
+ /**
5967
+ * UI System Global Object
5968
+ */
5969
+ class UISystemPlugin
5970
+ {
5971
+ /** Create the global UI system object
5972
+ * @param {CanvasRenderingContext2D} [context]
5973
+ * @example
5974
+ * // create the ui plugin object
5975
+ * new UISystemPlugin;
5976
+ */
5977
+ constructor(context=overlayContext)
5978
+ {
5979
+ ASSERT(!uiSystem, 'UI system already initialized');
5980
+ uiSystem = this;
5981
+
5982
+ /** @property {Color} - Default fill color for UI elements */
5983
+ this.defaultColor = WHITE;
5984
+ /** @property {Color} - Default outline color for UI elements */
5985
+ this.defaultLineColor = BLACK;
5986
+ /** @property {Color} - Default text color for UI elements */
5987
+ this.defaultTextColor = BLACK;
5988
+ /** @property {Color} - Default button color for UI elements */
5989
+ this.defaultButtonColor = hsl(0,0,.5);
5990
+ /** @property {Color} - Default hover color for UI elements */
5991
+ this.defaultHoverColor = hsl(0,0,.7);
5992
+ /** @property {number} - Default line width for UI elements */
5993
+ this.defaultLineWidth = 4;
5994
+ /** @property {string} - Default font for UI elements */
5995
+ this.defaultFont = 'arial';
5996
+ /** @property {Array<UIObject>} - List of all UI elements */
5997
+ this.uiObjects = [];
5998
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
5999
+ this.uiContext = context;
6000
+
6001
+ engineAddPlugin(uiUpdate, uiRender);
6002
+
6003
+ // setup recursive update and render
6004
+ function uiUpdate()
6005
+ {
6006
+ function updateObject(o)
6007
+ {
6008
+ if (!o.visible)
6009
+ return;
6010
+ if (o.parent)
6011
+ o.pos = o.localPos.add(o.parent.pos);
6012
+ o.update();
6013
+ for(const c of o.children)
6014
+ updateObject(c);
6015
+ }
6016
+ uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
6017
+ }
6018
+ function uiRender()
6019
+ {
6020
+ function renderObject(o)
6021
+ {
6022
+ if (!o.visible)
6023
+ return;
6024
+ if (o.parent)
6025
+ o.pos = o.localPos.add(o.parent.pos);
6026
+ o.render();
6027
+ for(const c of o.children)
6028
+ renderObject(c);
6029
+ }
6030
+ uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
6031
+ }
6032
+ }
6033
+
6034
+ /** Draw a rectangle to the UI context
6035
+ * @param {Vector2} pos
6036
+ * @param {Vector2} size
6037
+ * @param {Color} [color=uiSystem.defaultColor]
6038
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6039
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6040
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6041
+ {
6042
+ uiSystem.uiContext.fillStyle = color.toString();
6043
+ uiSystem.uiContext.beginPath();
6044
+ uiSystem.uiContext.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
6045
+ uiSystem.uiContext.fill();
6046
+ if (lineWidth)
6047
+ {
6048
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6049
+ uiSystem.uiContext.lineWidth = lineWidth;
6050
+ uiSystem.uiContext.stroke();
6051
+ }
6052
+ }
6053
+
6054
+ /** Draw a line to the UI context
6055
+ * @param {Vector2} posA
6056
+ * @param {Vector2} posB
6057
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6058
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6059
+ drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6060
+ {
6061
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6062
+ uiSystem.uiContext.lineWidth = lineWidth;
6063
+ uiSystem.uiContext.beginPath();
6064
+ uiSystem.uiContext.lineTo(posA.x, posA.y);
6065
+ uiSystem.uiContext.lineTo(posB.x, posB.y);
6066
+ uiSystem.uiContext.stroke();
6067
+ }
6068
+
6069
+ /** Draw a tile to the UI context
6070
+ * @param {Vector2} pos
6071
+ * @param {Vector2} size
6072
+ * @param {TileInfo} tileInfo
6073
+ * @param {Color} [color=uiSystem.defaultColor]
6074
+ * @param {number} [angle]
6075
+ * @param {boolean} [mirror] */
6076
+ drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false)
6077
+ {
6078
+ drawTile(pos, size, tileInfo, color, angle, mirror, BLACK, false, true, uiSystem.uiContext);
6079
+ }
6080
+
6081
+ /** Draw text to the UI context
6082
+ * @param {string} text
6083
+ * @param {Vector2} pos
6084
+ * @param {Vector2} size
6085
+ * @param {Color} [color=uiSystem.defaultColor]
6086
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6087
+ * @param {Color} [lineColor=uiSystem.defaultLineColor]
6088
+ * @param {string} [align]
6089
+ * @param {string} [font=uiSystem.defaultFont] */
6090
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont)
6091
+ {
6092
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, size.x, uiSystem.uiContext);
6093
+ }
6094
+ }
6095
+
6096
+ ///////////////////////////////////////////////////////////////////////////////
6097
+ /**
6098
+ * UI Object - Base level object for all UI elements
6099
+ */
6100
+ class UIObject
6101
+ {
6102
+ /** Create a UIObject
6103
+ * @param {Vector2} [pos=(0,0)]
6104
+ * @param {Vector2} [size=(1,1)]
6105
+ */
6106
+ constructor(pos=vec2(), size=vec2())
6107
+ {
6108
+ /** @property {Vector2} - Local position of the object */
6109
+ this.localPos = pos.copy();
6110
+ /** @property {Vector2} - Screen space position of the object */
6111
+ this.pos = pos.copy();
6112
+ /** @property {Vector2} - Screen space size of the object */
6113
+ this.size = size.copy();
6114
+ /** @property {Color} */
6115
+ this.color = uiSystem.defaultColor;
6116
+ /** @property {Color} */
6117
+ this.lineColor = uiSystem.defaultLineColor;
6118
+ /** @property {Color} */
6119
+ this.textColor = uiSystem.defaultTextColor;
6120
+ /** @property {Color} */
6121
+ this.hoverColor = uiSystem.defaultHoverColor;
6122
+ /** @property {number} */
6123
+ this.lineWidth = uiSystem.defaultLineWidth;
6124
+ /** @property {string} */
6125
+ this.font = uiSystem.defaultFont;
6126
+ /** @property {number} - override for text height */
6127
+ this.textHeight = undefined;
6128
+ /** @property {boolean} */
6129
+ this.visible = true;
6130
+ /** @property {Array<UIObject>} */
6131
+ this.children = [];
6132
+ /** @property {UIObject} */
6133
+ this.parent = undefined;
6134
+ uiSystem.uiObjects.push(this);
6135
+ }
6136
+
6137
+ /** Add a child UIObject to this object
6138
+ * @param {UIObject} child
6139
+ */
6140
+ addChild(child)
6141
+ {
6142
+ ASSERT(!child.parent && !this.children.includes(child));
6143
+ this.children.push(child);
6144
+ child.parent = this;
6145
+ }
6146
+
6147
+ /** Remove a child UIObject from this object
6148
+ * @param {UIObject} child
6149
+ */
6150
+ removeChild(child)
6151
+ {
6152
+ ASSERT(child.parent == this && this.children.includes(child));
6153
+ this.children.splice(this.children.indexOf(child), 1);
6154
+ child.parent = undefined;
6155
+ }
6156
+
6157
+ /** Update the object, called automatically by plugin once each frame */
6158
+ update()
6159
+ {
6160
+ // track mouse input
6161
+ const mouseWasOver = this.mouseIsOver;
6162
+ const mouseDown = mouseIsDown(0);
6163
+ if (!mouseDown || isTouchDevice)
6164
+ {
6165
+ this.mouseIsOver = isOverlapping(this.pos, this.size, mousePosScreen);
6166
+ if (!mouseDown && isTouchDevice)
6167
+ this.mouseIsOver = false;
6168
+ if (this.mouseIsOver && !mouseWasOver)
6169
+ this.onEnter();
6170
+ if (!this.mouseIsOver && mouseWasOver)
6171
+ this.onLeave();
6172
+ }
6173
+ if (mouseWasPressed(0) && this.mouseIsOver)
6174
+ {
6175
+ this.mouseIsHeld = true;
6176
+ this.onPress();
6177
+ if (isTouchDevice)
6178
+ this.mouseIsOver = false;
6179
+ }
6180
+ else if (this.mouseIsHeld && !mouseDown)
6181
+ {
6182
+ this.mouseIsHeld = false;
6183
+ this.onRelease();
6184
+ }
6185
+ }
6186
+
6187
+ /** Render the object, called automatically by plugin once each frame */
6188
+ render()
6189
+ {
6190
+ if (this.size.x && this.size.y)
6191
+ uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor);
6192
+ }
6193
+
6194
+ /** Called when the mouse enters the object */
6195
+ onEnter() {}
6196
+
6197
+ /** Called when the mouse leaves the object */
6198
+ onLeave() {}
6199
+
6200
+ /** Called when the mouse is pressed while over the object */
6201
+ onPress() {}
6202
+
6203
+ /** Called when the mouse is released while over the object */
6204
+ onRelease() {}
6205
+
6206
+ /** Called when the state of this object changes */
6207
+ onChange() {}
6208
+ }
6209
+
6210
+ ///////////////////////////////////////////////////////////////////////////////
6211
+ /**
6212
+ * UIText - A UI object that displays text
6213
+ * @extends UIObject
6214
+ */
6215
+ class UIText extends UIObject
6216
+ {
6217
+ /** Create a UIText object
6218
+ * @param {Vector2} [pos]
6219
+ * @param {Vector2} [size]
6220
+ * @param {string} [text]
6221
+ * @param {string} [align]
6222
+ * @param {string} [font=uiSystem.defaultFont]
6223
+ */
6224
+ constructor(pos, size, text='', align='center', font=uiSystem.defaultFont)
6225
+ {
6226
+ super(pos, size);
6227
+
6228
+ /** @property {string} */
6229
+ this.text = text;
6230
+ /** @property {string} */
6231
+ this.align = align;
6232
+
6233
+ this.font = font; // set font
6234
+ this.lineWidth = 0; // set text to not be outlined by default
6235
+ }
6236
+ render()
6237
+ {
6238
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y);
6239
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
6240
+ }
6241
+ }
6242
+
6243
+ ///////////////////////////////////////////////////////////////////////////////
6244
+ /**
6245
+ * UITile - A UI object that displays a tile image
6246
+ * @extends UIObject
6247
+ */
6248
+ class UITile extends UIObject
6249
+ {
6250
+ /** Create a UITile object
6251
+ * @param {Vector2} [pos]
6252
+ * @param {Vector2} [size]
6253
+ * @param {TileInfo} [tileInfo]
6254
+ * @param {Color} [color=WHITE]
6255
+ * @param {number} [angle]
6256
+ * @param {boolean} [mirror]
6257
+ */
6258
+ constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
6259
+ {
6260
+ super(pos, size);
6261
+
6262
+ /** @property {TileInfo} - Tile image to use */
6263
+ this.tileInfo = tileInfo;
6264
+ /** @property {number} - Angle to rotate in radians */
6265
+ this.angle = angle;
6266
+ /** @property {boolean} - Should it be mirrored? */
6267
+ this.mirror = mirror;
6268
+ this.color = color;
6269
+ }
6270
+ render()
6271
+ {
6272
+ uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
6273
+ }
6274
+ }
6275
+
6276
+ ///////////////////////////////////////////////////////////////////////////////
6277
+ /**
6278
+ * UIButton - A UI object that acts as a button
6279
+ * @extends UIObject
6280
+ */
6281
+ class UIButton extends UIObject
6282
+ {
6283
+ /** Create a UIButton object
6284
+ * @param {Vector2} [pos]
6285
+ * @param {Vector2} [size]
6286
+ * @param {string} [text]
6287
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6288
+ */
6289
+ constructor(pos, size, text='', color=uiSystem.defaultButtonColor)
6290
+ {
6291
+ super(pos, size);
6292
+
6293
+ /** @property {string} */
6294
+ this.text = text;
6295
+ this.color = color;
6296
+ }
6297
+ render()
6298
+ {
6299
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
6300
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6301
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
6302
+
6303
+ const textScale = .8; // scale text to fit in button
6304
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
6305
+ uiSystem.drawText(this.text, this.pos, textSize,
6306
+ this.textColor, 0, undefined, this.align, this.font);
6307
+ }
6308
+ }
6309
+
6310
+ ///////////////////////////////////////////////////////////////////////////////
6311
+ /**
6312
+ * UICheckbox - A UI object that acts as a checkbox
6313
+ * @extends UIObject
6314
+ */
6315
+ class UICheckbox extends UIObject
6316
+ {
6317
+ /** Create a UICheckbox object
6318
+ * @param {Vector2} [pos]
6319
+ * @param {Vector2} [size]
6320
+ * @param {boolean} [checked]
6321
+ */
6322
+ constructor(pos, size, checked=false)
6323
+ {
6324
+ super(pos, size);
6325
+
6326
+ /** @property {boolean} */
6327
+ this.checked = checked;
6328
+ }
6329
+ onPress()
6330
+ {
6331
+ this.checked = !this.checked;
6332
+ this.onChange();
6333
+ }
6334
+ render()
6335
+ {
6336
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6337
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor);
6338
+ if (this.checked)
6339
+ {
6340
+ // draw an X if checked
6341
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor);
6342
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor);
6343
+ }
6344
+ }
6345
+ }
6346
+
6347
+ ///////////////////////////////////////////////////////////////////////////////
6348
+ /**
6349
+ * UIScrollbar - A UI object that acts as a scrollbar
6350
+ * @extends UIObject
6351
+ */
6352
+ class UIScrollbar extends UIObject
6353
+ {
6354
+ /** Create a UIScrollbar object
6355
+ * @param {Vector2} [pos]
6356
+ * @param {Vector2} [size]
6357
+ * @param {number} [value]
6358
+ * @param {string} [text]
6359
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6360
+ * @param {Color} [handleColor=WHITE]
6361
+ */
6362
+ constructor(pos, size, value=.5, text='', color=uiSystem.defaultButtonColor, handleColor=WHITE)
6363
+ {
6364
+ super(pos, size);
6365
+
6366
+ /** @property {number} */
6367
+ this.value = value;
6368
+ /** @property {string} */
6369
+ this.text = text;
6370
+ this.color = color;
6371
+ this.handleColor = handleColor;
6372
+ }
6373
+ update()
6374
+ {
6375
+ super.update();
6376
+ if (this.mouseIsHeld)
6377
+ {
6378
+ const handleSize = vec2(this.size.y);
6379
+ const handleWidth = this.size.x - handleSize.x;
6380
+ const p1 = this.pos.x - handleWidth/2;
6381
+ const p2 = this.pos.x + handleWidth/2;
6382
+ const oldValue = this.value;
6383
+ this.value = percent(mousePosScreen.x, p1, p2);
6384
+ this.value == oldValue || this.onChange();
6385
+ }
6386
+ }
6387
+ render()
6388
+ {
6389
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
6390
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6391
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
6392
+
6393
+ const handleSize = vec2(this.size.y);
6394
+ const handleWidth = this.size.x - handleSize.x;
6395
+ const p1 = this.pos.x - handleWidth/2;
6396
+ const p2 = this.pos.x + handleWidth/2;
6397
+ const handlePos = vec2(lerp(this.value, p1, p2), this.pos.y);
6398
+ const barColor = this.mouseIsHeld ? this.color : this.handleColor;
6399
+ uiSystem.drawRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor);
6400
+
6401
+ const textScale = .8; // scale text to fit in scrollbar
6402
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
6403
+ uiSystem.drawText(this.text, this.pos, textSize,
6404
+ this.textColor, 0, undefined, this.align, this.font);
6405
+ }
6406
+ }
6407
+
6408
+ /**
6409
+ * LittleJS Box2D Physics Plugin
6410
+ * - Box2dObject extends EngineObject with Box2D physics
6411
+ * - Call box2dInit() before engineInit() to enable
6412
+ * - You will also need to include box2d.wasm.js
6413
+ * - Uses a super fast web assembly port of Box2D
6414
+ * - More info: https://github.com/kripken/box2d.js
6415
+ * - Functions to create polygon, circle, and edge shapes
6416
+ * - Contact begin and end callbacks
6417
+ * - Wraps b2Vec2 type to/from Vector2
6418
+ * - Raycasting and querying
6419
+ * - Every type of joint
6420
+ * - Debug physics drawing
6421
+ * @namespace Box2D
6422
+ */
6423
+
6424
+ /** Global Box2d Plugin object
6425
+ * @type {Box2dPlugin}
6426
+ * @memberof Box2D */
6427
+ let box2d;
6428
+
6429
+ /** Enable Box2D debug drawing
6430
+ * @type {boolean}
6431
+ * @default
6432
+ * @memberof Box2D */
6433
+ let box2dDebug = false;
6434
+
6435
+ /** Enable Box2D debug drawing
6436
+ * @param {boolean} enable
6437
+ * @memberof Box2D */
6438
+ function box2dSetDebug(enable) { box2dDebug = enable; }
6439
+
6440
+ ///////////////////////////////////////////////////////////////////////////////
6441
+ /**
6442
+ * Box2D Object - extend with your own custom physics objects
6443
+ * - A LittleJS object with Box2D physics
6444
+ * - Each object has a Box2D body which can have multiple fixtures and joints
6445
+ * - Provides interface for Box2D body and fixture functions
6446
+ * @extends EngineObject
6447
+ */
6448
+ class Box2dObject extends EngineObject
6449
+ {
6450
+ /** Create a LittleJS object with Box2d physics
6451
+ * @param {Vector2} [pos]
6452
+ * @param {Vector2} [size]
6453
+ * @param {TileInfo} [tileInfo]
6454
+ * @param {number} [angle]
6455
+ * @param {Color} [color]
6456
+ * @param {number} [bodyType]
6457
+ * @param {number} [renderOrder] */
6458
+ constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
6459
+ {
6460
+ super(pos, size, tileInfo, angle, color, renderOrder);
6461
+
6462
+ // create physics body
6463
+ const bodyDef = new box2d.instance.b2BodyDef();
6464
+ bodyDef.set_type(bodyType);
6465
+ bodyDef.set_position(box2d.vec2dTo(pos));
6466
+ bodyDef.set_angle(-angle);
6467
+ this.body = box2d.world.CreateBody(bodyDef);
6468
+ this.body.object = this;
6469
+ this.outlineColor = BLACK;
6470
+ }
6471
+
6472
+ /** Destroy this object and it's physics body */
6473
+ destroy()
6474
+ {
6475
+ // destroy physics body, fixtures, and joints
6476
+ this.body && box2d.world.DestroyBody(this.body);
6477
+ this.body = 0;
6478
+ super.destroy();
6479
+ }
6480
+
6481
+ /** Copy box2d update sim data */
6482
+ update()
6483
+ {
6484
+ // use box2d physics update
6485
+ this.pos = box2d.vec2From(this.body.GetPosition());
6486
+ this.angle = -this.body.GetAngle();
6487
+ }
6488
+
6489
+ /** Render the object, uses box2d drawing if no tile info exists */
6490
+ render()
6491
+ {
6492
+ // use default render or draw fixtures
6493
+ if (this.tileInfo)
6494
+ super.render();
6495
+ else
6496
+ this.drawFixtures(this.color, this.outlineColor, this.lineWidth, mainContext);
6497
+ }
6498
+
6499
+ /** Render debug info */
6500
+ renderDebugInfo()
6501
+ {
6502
+ const isAsleep = !this.getIsAwake();
6503
+ const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
6504
+ const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
6505
+ this.drawFixtures(color);
6506
+ }
6507
+
6508
+ /** Draws all this object's fixtures
6509
+ * @param {Color} [color]
6510
+ * @param {Color} [outlineColor]
6511
+ * @param {number} [lineWidth]
6512
+ * @param {CanvasRenderingContext2D} [context] */
6513
+ drawFixtures(color=WHITE, outlineColor, lineWidth=.1, context)
6514
+ {
6515
+ this.getFixtureList().forEach(fixture=>
6516
+ box2d.drawFixture(fixture, this.pos, this.angle, color, outlineColor, lineWidth, context));
6517
+ }
6518
+
6519
+ ///////////////////////////////////////////////////////////////////////////////
6520
+ // physics contact callbacks
6521
+
6522
+ /** Called when a contact begins
6523
+ * @param {Box2dObject} otherObject */
6524
+ beginContact(otherObject) {}
6525
+
6526
+ /** Called when a contact ends
6527
+ * @param {Box2dObject} otherObject */
6528
+ endContact(otherObject) {}
6529
+
6530
+ ///////////////////////////////////////////////////////////////////////////////
6531
+ // physics fixtures and shapes
6532
+
6533
+ /** Add a shape fixture to the body
6534
+ * @param {Object} shape
6535
+ * @param {number} [density]
6536
+ * @param {number} [friction]
6537
+ * @param {number} [restitution]
6538
+ * @param {boolean} [isSensor] */
6539
+ addShape(shape, density=1, friction=1, restitution=0, isSensor=false)
6540
+ {
6541
+ const fd = new box2d.instance.b2FixtureDef();
6542
+ fd.set_shape(shape);
6543
+ fd.set_density(density);
6544
+ fd.set_friction(friction);
6545
+ fd.set_restitution(restitution);
6546
+ fd.set_isSensor(isSensor);
6547
+ return this.body.CreateFixture(fd);
6548
+ }
6549
+
6550
+ /** Add a box shape to the body
6551
+ * @param {Vector2} [size]
6552
+ * @param {Vector2} [offset]
6553
+ * @param {number} [angle]
6554
+ * @param {number} [density]
6555
+ * @param {number} [friction]
6556
+ * @param {number} [restitution]
6557
+ * @param {boolean} [isSensor] */
6558
+ addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
6559
+ {
6560
+ const shape = new box2d.instance.b2PolygonShape();
6561
+ shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
6562
+ return this.addShape(shape, density, friction, restitution, isSensor);
6563
+ }
6564
+
6565
+ /** Add a polygon shape to the body
6566
+ * @param {Array<Vector2>} points
6567
+ * @param {number} [density]
6568
+ * @param {number} [friction]
6569
+ * @param {number} [restitution]
6570
+ * @param {boolean} [isSensor] */
6571
+ addPoly(points, density, friction, restitution, isSensor)
6572
+ {
6573
+ function box2dCreatePolygonShape(points)
6574
+ {
6575
+ function box2dCreatePointList(points)
6576
+ {
6577
+ const buffer = box2d.instance._malloc(points.length * 8);
6578
+ for (let i=0, offset=0; i<points.length; ++i)
6579
+ {
6580
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x;
6581
+ offset += 4;
6582
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
6583
+ offset += 4;
6584
+ }
6585
+ return box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
6586
+ }
6587
+
6588
+ ASSERT(3 <= points.length && points.length <= 8);
6589
+ const shape = new box2d.instance.b2PolygonShape();
6590
+ const box2dPoints = box2dCreatePointList(points);
6591
+ shape.Set(box2dPoints, points.length);
6592
+ return shape;
6593
+ }
6594
+
6595
+ const shape = box2dCreatePolygonShape(points);
6596
+ return this.addShape(shape, density, friction, restitution, isSensor);
6597
+ }
6598
+
6599
+ /** Add a regular polygon shape to the body
6600
+ * @param {number} [diameter]
6601
+ * @param {number} [sides]
6602
+ * @param {number} [density]
6603
+ * @param {number} [friction]
6604
+ * @param {number} [restitution]
6605
+ * @param {boolean} [isSensor] */
6606
+ addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
6607
+ {
6608
+ const points = [];
6609
+ const radius = diameter/2;
6610
+ for (let i=sides; i--;)
6611
+ points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
6612
+ return this.addPoly(points, density, friction, restitution, isSensor);
6613
+ }
6614
+
6615
+ /** Add a random polygon shape to the body
6616
+ * @param {number} [diameter]
6617
+ * @param {number} [density]
6618
+ * @param {number} [friction]
6619
+ * @param {number} [restitution]
6620
+ * @param {boolean} [isSensor] */
6621
+ addRandomPoly(diameter=1, density, friction, restitution, isSensor)
6622
+ {
6623
+ const sides = randInt(3, 9);
6624
+ const points = [];
6625
+ const radius = diameter/2;
6626
+ for (let i=sides; i--;)
6627
+ points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2));
6628
+ return this.addPoly(points, density, friction, restitution, isSensor);
6629
+ }
6630
+
6631
+ /** Add a circle shape to the body
6632
+ * @param {number} [diameter]
6633
+ * @param {Vector2} [offset]
6634
+ * @param {number} [density]
6635
+ * @param {number} [friction]
6636
+ * @param {number} [restitution]
6637
+ * @param {boolean} [isSensor] */
6638
+ addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
6639
+ {
6640
+ const shape = new box2d.instance.b2CircleShape();
6641
+ shape.set_m_p(box2d.vec2dTo(offset));
6642
+ shape.set_m_radius(diameter/2);
6643
+ return this.addShape(shape, density, friction, restitution, isSensor);
6644
+ }
6645
+
6646
+ /** Add an edge shape to the body
6647
+ * @param {Vector2} point1
6648
+ * @param {Vector2} point2
6649
+ * @param {number} [density]
6650
+ * @param {number} [friction]
6651
+ * @param {number} [restitution]
6652
+ * @param {boolean} [isSensor] */
6653
+ addEdge(point1, point2, density, friction, restitution, isSensor)
6654
+ {
6655
+ const shape = new box2d.instance.b2EdgeShape();
6656
+ shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
6657
+ return this.addShape(shape, density, friction, restitution, isSensor);
6658
+ }
6659
+
6660
+ /** Add an edge loop to the body, an edge loop connects the end points
6661
+ * @param {Array<Vector2>} points
6662
+ * @param {number} [density]
6663
+ * @param {number} [friction]
6664
+ * @param {number} [restitution]
6665
+ * @param {boolean} [isSensor] */
6666
+ addEdgeLoop(points, density, friction, restitution, isSensor)
6667
+ {
6668
+ const fixtures = [];
6669
+ const getPoint = i=> points[mod(i,points.length)];
6670
+ for (let i=0; i<points.length; ++i)
6671
+ {
6672
+ const shape = new box2d.instance.b2EdgeShape();
6673
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
6674
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
6675
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
6676
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
6677
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
6678
+ fixtures.push(f);
6679
+ }
6680
+ return fixtures;
6681
+ }
6682
+
6683
+ /** Add an edge list to the body
6684
+ * @param {Array<Vector2>} points
6685
+ * @param {number} [density]
6686
+ * @param {number} [friction]
6687
+ * @param {number} [restitution]
6688
+ * @param {boolean} [isSensor] */
6689
+ addEdgeList(points, density, friction, restitution, isSensor)
6690
+ {
6691
+ const fixtures = [];
6692
+ for (let i=0; i<points.length-1; ++i)
6693
+ {
6694
+ const shape = new box2d.instance.b2EdgeShape();
6695
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
6696
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
6697
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
6698
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
6699
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
6700
+ fixtures.push(f);
6701
+ }
6702
+ return fixtures;
6703
+ }
6704
+
6705
+ ///////////////////////////////////////////////////////////////////////////////
6706
+ // physics get functions
6707
+
6708
+ /** Gets the center of mass
6709
+ * @return {Vector2} */
6710
+ getCenterOfMass() { return box2d.vec2From(this.body.GetWorldCenter()); }
6711
+
6712
+ /** Gets the linear velocity
6713
+ * @return {Vector2} */
6714
+ getLinearVelocity() { return box2d.vec2From(this.body.GetLinearVelocity()); }
6715
+
6716
+ /** Gets the angular velocity
6717
+ * @return {Vector2} */
6718
+ getAngularVelocity() { return this.body.GetAngularVelocity(); }
6719
+
6720
+ /** Gets the mass
6721
+ * @return {number} */
6722
+ getMass() { return this.body.GetMass(); }
6723
+
6724
+ /** Gets the rotational inertia
6725
+ * @return {number} */
6726
+ getInertia() { return this.body.GetInertia(); }
6727
+
6728
+ /** Check if this object is awake
6729
+ * @return {boolean} */
6730
+ getIsAwake() { return this.body.IsAwake(); }
6731
+
6732
+ /** Gets the physics body type
6733
+ * @return {number} */
6734
+ getBodyType() { return this.body.GetType(); }
6735
+
6736
+ ///////////////////////////////////////////////////////////////////////////////
6737
+ // physics set functions
6738
+
6739
+ /** Sets the position and angle
6740
+ * @param {Vector2} pos
6741
+ * @param {number} angle */
6742
+ setTransform(pos, angle)
6743
+ {
6744
+ this.pos = pos;
6745
+ this.angle = angle;
6746
+ this.body.SetTransform(box2d.vec2dTo(pos), angle);
6747
+ }
6748
+
6749
+ /** Sets the position
6750
+ * @param {Vector2} pos */
6751
+ setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
6752
+
6753
+ /** Sets the angle
6754
+ * @param {number} angle */
6755
+ setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
6756
+
6757
+ /** Sets the linear velocity
6758
+ * @param {Vector2} velocity */
6759
+ setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
6760
+
6761
+ /** Sets the angular velocity
6762
+ * @param {number} angularVelocity */
6763
+ setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
6764
+
6765
+ /** Sets the linear damping
6766
+ * @param {number} damping */
6767
+ setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
6768
+
6769
+ /** Sets the angular damping
6770
+ * @param {number} damping */
6771
+ setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
6772
+
6773
+ /** Sets the gravity scale
6774
+ * @param {number} [scale] */
6775
+ setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
6776
+
6777
+ /** Should this body be treated like a bullet for continuous collision detection?
6778
+ * @param {boolean} [isBullet] */
6779
+ setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
6780
+
6781
+ /** Set the sleep state of the body
6782
+ * @param {boolean} [isAwake] */
6783
+ setAwake(isAwake=true) { this.body.SetAwake(isAwake); }
6784
+
6785
+ /** Set the physics body type
6786
+ * @param {number} type */
6787
+ setBodyType(type) { this.body.SetType(type); }
6788
+
6789
+ /** Set whether the body is allowed to sleep
6790
+ * @param {boolean} [isAllowed] */
6791
+ setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
6792
+
6793
+ /** Set whether the body can rotate
6794
+ * @param {boolean} [isFixed] */
6795
+ setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
6796
+
6797
+ /** Set the center of mass of the body
6798
+ * @param {Vector2} center */
6799
+ setCenterOfMass(center) { this.setMassData(center) }
6800
+
6801
+ /** Set the mass of the body
6802
+ * @param {number} mass */
6803
+ setMass(mass) { this.setMassData(undefined, mass) }
6804
+
6805
+ /** Set the moment of inertia of the body
6806
+ * @param {number} momentOfInertia */
6807
+ setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
6808
+
6809
+ /** Reset the mass, center of mass, and moment */
6810
+ resetMassData() { this.body.ResetMassData(); }
6811
+
6812
+ /** Set the mass data of the body
6813
+ * @param {Vector2} [localCenter]
6814
+ * @param {number} [mass]
6815
+ * @param {number} [momentOfInertia] */
6816
+ setMassData(localCenter, mass, momentOfInertia)
6817
+ {
6818
+ const data = new box2d.instance.b2MassData();
6819
+ this.body.GetMassData(data);
6820
+ localCenter && data.set_center(box2d.vec2dTo(localCenter));
6821
+ mass && data.set_mass(mass);
6822
+ momentOfInertia && data.set_I(momentOfInertia);
6823
+ this.body.SetMassData(data);
6824
+ }
6825
+
6826
+ /** Set the collision filter data for this body
6827
+ * @param {number} [categoryBits]
6828
+ * @param {number} [ignoreCategoryBits]
6829
+ * @param {number} [groupIndex] */
6830
+ setFilterData(categoryBits=0, ignoreCategoryBits=0, groupIndex=0)
6831
+ {
6832
+ this.getFixtureList().forEach(fixture=>
6833
+ {
6834
+ const filter = fixture.GetFilterData();
6835
+ filter.set_categoryBits(categoryBits);
6836
+ filter.set_maskBits(0xffff & ~ignoreCategoryBits);
6837
+ filter.set_groupIndex(groupIndex);
6838
+ });
6839
+ }
6840
+
6841
+ /** Set if this body is a sensor
6842
+ * @param {boolean} [isSensor] */
6843
+ setSensor(isSensor=true)
6844
+ { this.getFixtureList().forEach(f=>f.SetSensor(isSensor)); }
6845
+
6846
+ ///////////////////////////////////////////////////////////////////////////////
6847
+ // physics force and torque functions
6848
+
6849
+ /** Apply force to this object
6850
+ * @param {Vector2} force
6851
+ * @param {Vector2} [pos] */
6852
+ applyForce(force, pos)
6853
+ {
6854
+ pos ||= this.getCenterOfMass();
6855
+ this.setAwake();
6856
+ this.body.ApplyForce(box2d.vec2dTo(force), box2d.vec2dTo(pos));
6857
+ }
6858
+
6859
+ /** Apply acceleration to this object
6860
+ * @param {Vector2} acceleration
6861
+ * @param {Vector2} [pos] */
6862
+ applyAcceleration(acceleration, pos)
6863
+ {
6864
+ pos ||= this.getCenterOfMass();
6865
+ this.setAwake();
6866
+ this.body.ApplyLinearImpulse(box2d.vec2dTo(acceleration), box2d.vec2dTo(pos));
6867
+ }
6868
+
6869
+ /** Apply torque to this object
6870
+ * @param {number} torque */
6871
+ applyTorque(torque)
6872
+ {
6873
+ this.setAwake();
6874
+ this.body.ApplyTorque(torque);
6875
+ }
6876
+
6877
+ /** Apply angular acceleration to this object
6878
+ * @param {number} acceleration */
6879
+ applyAngularAcceleration(acceleration)
6880
+ {
6881
+ this.setAwake();
6882
+ this.body.ApplyAngularImpulse(acceleration);
6883
+ }
6884
+
6885
+ ///////////////////////////////////////////////////////////////////////////////
6886
+ // lists of fixtures and joints
6887
+
6888
+ /** Check if this object has any fixtures
6889
+ * @return {boolean} */
6890
+ hasFixtures() { return !box2d.isNull(this.body.GetFixtureList()); }
6891
+
6892
+ /** Get list of fixtures for this object
6893
+ * @return {Array<Object>} */
6894
+ getFixtureList()
6895
+ {
6896
+ const fixtures = [];
6897
+ for (let fixture=this.body.GetFixtureList(); !box2d.isNull(fixture); )
6898
+ {
6899
+ fixtures.push(fixture);
6900
+ fixture = fixture.GetNext();
6901
+ }
6902
+ return fixtures;
6903
+ }
6904
+
6905
+ /** Check if this object has any joints
6906
+ * @return {boolean} */
6907
+ hasJoints() { return !box2d.isNull(this.body.GetJointList()); }
6908
+
6909
+ /** Get list of joints for this object
6910
+ * @return {Array<Object>} */
6911
+ getJointList()
6912
+ {
6913
+ const joints = [];
6914
+ for (let joint=this.body.GetJointList(); !box2d.isNull(joint); )
6915
+ {
6916
+ joints.push(joint);
6917
+ joint = joint.get_next();
6918
+ }
6919
+ return joints;
6920
+ }
6921
+ }
6922
+
6923
+ ///////////////////////////////////////////////////////////////////////////////
6924
+ /**
6925
+ * Box2D Raycast Result
6926
+ * - Holds results from a box2d raycast queries
6927
+ * - Automatically created by box2d raycast functions
6928
+ */
6929
+ class Box2dRaycastResult
6930
+ {
6931
+ /** Create a raycast result
6932
+ * @param {Object} fixture
6933
+ * @param {Vector2} point
6934
+ * @param {Vector2} normal
6935
+ * @param {number} fraction */
6936
+ constructor(fixture, point, normal, fraction)
6937
+ {
6938
+ /** @property {Box2dObject} - The box2d object */
6939
+ this.object = fixture.GetBody().object;
6940
+ /** @property {Object} - The fixture that was hit */
6941
+ this.fixture = fixture;
6942
+ /** @property {Vector2} - The hit point */
6943
+ this.point = point;
6944
+ /** @property {Vector2} - The hit normal */
6945
+ this.normal = normal;
6946
+ /** @property {number} - Distance fraction at the point of intersection */
6947
+ this.fraction = fraction;
6948
+ }
6949
+ }
6950
+
6951
+ ///////////////////////////////////////////////////////////////////////////////
6952
+ /**
6953
+ * Box2D Joint
6954
+ * - Base class for Box2D joints
6955
+ * - A joint is used to connect objects together
6956
+ */
6957
+ class Box2dJoint
6958
+ {
6959
+ /** Create a box2d joint, the base class is not intended to be used directly
6960
+ * @param {Object} jointDef */
6961
+ constructor(jointDef)
6962
+ {
6963
+ this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
6964
+ }
6965
+
6966
+ /** Destroy this joint */
6967
+ destroy() { box2d.world.DestroyJoint(this.box2dJoint); this.box2dJoint = 0; }
6968
+
6969
+ /** Get the first object attached to this joint
6970
+ * @return {Box2dObject} */
6971
+ getObjectA() { return this.box2dJoint.GetBodyA().object; }
6972
+
6973
+ /** Get the second object attached to this joint
6974
+ * @return {Box2dObject} */
6975
+ getObjectB() { return this.box2dJoint.GetBodyB().object; }
6976
+
6977
+ /** Get the first anchor for this joint in world coordinates
6978
+ * @return {Vector2} */
6979
+ getAnchorA() { return box2d.vec2From(this.box2dJoint.GetAnchorA());}
6980
+
6981
+ /** Get the second anchor for this joint in world coordinates
6982
+ * @return {Vector2} */
6983
+ getAnchorB() { return box2d.vec2From(this.box2dJoint.GetAnchorB());}
6984
+
6985
+ /** Get the reaction force on bodyB at the joint anchor given a time step
6986
+ * @param {number} time
6987
+ * @return {Vector2} */
6988
+ getReactionForce(time) { return box2d.vec2From(this.box2dJoint.GetReactionForce(1/time));}
6989
+
6990
+ /** Get the reaction torque on bodyB in N*m given a time step
6991
+ * @param {number} time
6992
+ * @return {number} */
6993
+ getReactionTorque(time) { return this.box2dJoint.GetReactionTorque(1/time);}
6994
+
6995
+ /** Check if the connected bodies should collide
6996
+ * @return {boolean} */
6997
+ getCollideConnected() { return this.box2dJoint.getCollideConnected();}
6998
+
6999
+ /** Check if either connected body is active
7000
+ * @return {boolean} */
7001
+ isActive() { return this.box2dJoint.IsActive();}
7002
+ }
7003
+
7004
+ ///////////////////////////////////////////////////////////////////////////////
7005
+ /**
7006
+ * Box2D Target Joint, also known as a mouse joint
7007
+ * - Used to make a point on a object track a specific world point target
7008
+ * - This a soft constraint with a max force
7009
+ * - This allows the constraint to stretch and without applying huge forces
7010
+ * @extends Box2dJoint
7011
+ */
7012
+ class Box2dTargetJoint extends Box2dJoint
7013
+ {
7014
+ /** Create a target joint
7015
+ * @param {Box2dObject} object
7016
+ * @param {Box2dObject} fixedObject
7017
+ * @param {Vector2} worldPos */
7018
+ constructor(object, fixedObject, worldPos)
7019
+ {
7020
+ object.setAwake();
7021
+ const jointDef = new box2d.instance.b2MouseJointDef();
7022
+ jointDef.set_bodyA(fixedObject.body);
7023
+ jointDef.set_bodyB(object.body);
7024
+ jointDef.set_target(box2d.vec2dTo(worldPos));
7025
+ jointDef.set_maxForce(2e3 * object.getMass());
7026
+ super(jointDef);
7027
+ }
7028
+
7029
+ /** Set the target point in world coordinates
7030
+ * @param {Vector2} pos */
7031
+ setTarget(pos) { this.box2dJoint.SetTarget(box2d.vec2dTo(pos)); }
7032
+
7033
+ /** Get the target point in world coordinates
7034
+ * @return {Vector2} */
7035
+ getTarget(){ return box2d.vec2From(this.box2dJoint.GetTarget()); }
7036
+
7037
+ /** Sets the maximum force in Newtons
7038
+ * @param {number} force */
7039
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
7040
+
7041
+ /** Gets the maximum force in Newtons
7042
+ * @return {number} */
7043
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
7044
+
7045
+ /** Sets the joint frequency in Hertz
7046
+ * @param {number} hz */
7047
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7048
+
7049
+ /** Gets the joint frequency in Hertz
7050
+ * @return {number} */
7051
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7052
+ }
7053
+
7054
+ ///////////////////////////////////////////////////////////////////////////////
7055
+ /**
7056
+ * Box2D Distance Joint
7057
+ * - Constrains two points on two objects to remain at a fixed distance
7058
+ * - You can view this as a massless, rigid rod
7059
+ * @extends Box2dJoint
7060
+ */
7061
+ class Box2dDistanceJoint extends Box2dJoint
7062
+ {
7063
+ /** Create a distance joint
7064
+ * @param {Box2dObject} objectA
7065
+ * @param {Box2dObject} objectB
7066
+ * @param {Vector2} anchorA
7067
+ * @param {Vector2} anchorB
7068
+ * @param {boolean} [collide] */
7069
+ constructor(objectA, objectB, anchorA, anchorB, collide=false)
7070
+ {
7071
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7072
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7073
+ const localAnchorA = objectA.worldToLocal(anchorA);
7074
+ const localAnchorB = objectB.worldToLocal(anchorB);
7075
+ const jointDef = new box2d.instance.b2DistanceJointDef();
7076
+ jointDef.set_bodyA(objectA.body);
7077
+ jointDef.set_bodyB(objectB.body);
7078
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7079
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7080
+ jointDef.set_length(anchorA.distance(anchorB));
7081
+ jointDef.set_collideConnected(collide);
7082
+ super(jointDef);
7083
+ }
7084
+
7085
+ /** Get the local anchor point relative to objectA's origin
7086
+ * @return {Vector2} */
7087
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7088
+
7089
+ /** Get the local anchor point relative to objectB's origin
7090
+ * @return {Vector2} */
7091
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7092
+
7093
+ /** Set the length of the joint
7094
+ * @param {number} length */
7095
+ setLength(length) { this.box2dJoint.SetLength(length); }
7096
+
7097
+ /** Get the length of the joint
7098
+ * @return {number} */
7099
+ getLength() { return this.box2dJoint.GetLength(); }
7100
+
7101
+ /** Set the frequency in Hertz
7102
+ * @param {number} hz */
7103
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7104
+
7105
+ /** Get the frequency in Hertz
7106
+ * @return {number} */
7107
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7108
+
7109
+ /** Set the damping ratio
7110
+ * @param {number} ratio */
7111
+ setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); }
7112
+
7113
+ /** Get the damping ratio
7114
+ * @return {number} */
7115
+ getDampingRatio() { return this.box2dJoint.GetDampingRatio(); }
7116
+ }
7117
+
7118
+ ///////////////////////////////////////////////////////////////////////////////
7119
+ /**
7120
+ * Box2D Pin Joint
7121
+ * - Pins two objects together at a point
7122
+ * @extends Box2dDistanceJoint
7123
+ */
7124
+ class Box2dPinJoint extends Box2dDistanceJoint
7125
+ {
7126
+ /** Create a pin joint
7127
+ * @param {Box2dObject} objectA
7128
+ * @param {Box2dObject} objectB
7129
+ * @param {Vector2} [pos]
7130
+ * @param {boolean} [collide] */
7131
+ constructor(objectA, objectB, pos=objectA.pos, collide=false)
7132
+ {
7133
+ super(objectA, objectB, undefined, pos, collide);
7134
+ }
7135
+ }
7136
+
7137
+ ///////////////////////////////////////////////////////////////////////////////
7138
+ /**
7139
+ * Box2D Rope Joint
7140
+ * - Enforces a maximum distance between two points on two objects
7141
+ * @extends Box2dJoint
7142
+ */
7143
+ class Box2dRopeJoint extends Box2dJoint
7144
+ {
7145
+ /** Create a rope joint
7146
+ * @param {Box2dObject} objectA
7147
+ * @param {Box2dObject} objectB
7148
+ * @param {Vector2} anchorA
7149
+ * @param {Vector2} anchorB
7150
+ * @param {number} extraLength
7151
+ * @param {boolean} [collide] */
7152
+ constructor(objectA, objectB, anchorA, anchorB, extraLength=0, collide=false)
7153
+ {
7154
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7155
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7156
+ const localAnchorA = objectA.worldToLocal(anchorA);
7157
+ const localAnchorB = objectB.worldToLocal(anchorB);
7158
+ const jointDef = new box2d.instance.b2RopeJointDef();
7159
+ jointDef.set_bodyA(objectA.body);
7160
+ jointDef.set_bodyB(objectB.body);
7161
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7162
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7163
+ jointDef.set_maxLength(anchorA.distance(anchorB)+extraLength);
7164
+ jointDef.set_collideConnected(collide);
7165
+ super(jointDef);
7166
+ }
7167
+
7168
+ /** Get the local anchor point relative to objectA's origin
7169
+ * @return {Vector2} */
7170
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7171
+
7172
+ /** Get the local anchor point relative to objectB's origin
7173
+ * @return {Vector2} */
7174
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7175
+
7176
+ /** Set the max length of the joint
7177
+ * @param {number} length */
7178
+ setMaxLength(length) { this.box2dJoint.SetMaxLength(length); }
7179
+
7180
+ /** Get the max length of the joint
7181
+ * @return {number} */
7182
+ getMaxLength() { return this.box2dJoint.GetMaxLength(); }
7183
+ }
7184
+
7185
+ ///////////////////////////////////////////////////////////////////////////////
7186
+ /**
7187
+ * Box2D Revolute Joint
7188
+ * - Constrains two objects to share a point while they are free to rotate around the point
7189
+ * - The relative rotation about the shared point is the joint angle
7190
+ * - You can limit the relative rotation with a joint limit
7191
+ * - You can use a motor to drive the relative rotation about the shared point
7192
+ * - A maximum motor torque is provided so that infinite forces are not generated
7193
+ * @extends Box2dJoint
7194
+ */
7195
+ class Box2dRevoluteJoint extends Box2dJoint
7196
+ {
7197
+ /** Create a revolute joint
7198
+ * @param {Box2dObject} objectA
7199
+ * @param {Box2dObject} objectB
7200
+ * @param {Vector2} anchor
7201
+ * @param {boolean} [collide] */
7202
+ constructor(objectA, objectB, anchor, collide=false)
7203
+ {
7204
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7205
+ const localAnchorA = objectA.worldToLocal(anchor);
7206
+ const localAnchorB = objectB.worldToLocal(anchor);
7207
+ const jointDef = new box2d.instance.b2RevoluteJointDef();
7208
+ jointDef.set_bodyA(objectA.body);
7209
+ jointDef.set_bodyB(objectB.body);
7210
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7211
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7212
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7213
+ jointDef.set_collideConnected(collide);
7214
+ super(jointDef);
7215
+ }
7216
+
7217
+ /** Get the local anchor point relative to objectA's origin
7218
+ * @return {Vector2} */
7219
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7220
+
7221
+ /** Get the local anchor point relative to objectB's origin
7222
+ * @return {Vector2} */
7223
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7224
+
7225
+ /** Get the reference angle, objectB angle minus objectA angle in the reference state
7226
+ * @return {number} */
7227
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7228
+
7229
+ /** Get the current joint angle
7230
+ * @return {number} */
7231
+ getJointAngle() { return this.box2dJoint.GetJointAngle(); }
7232
+
7233
+ /** Get the current joint angle speed in radians per second
7234
+ * @return {number} */
7235
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7236
+
7237
+ /** Is the joint limit enabled?
7238
+ * @return {boolean} */
7239
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
7240
+
7241
+ /** Enable/disable the joint limit
7242
+ * @param {boolean} [enable] */
7243
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
7244
+
7245
+ /** Get the lower joint limit
7246
+ * @return {number} */
7247
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
7248
+
7249
+ /** Get the upper joint limit
7250
+ * @return {number} */
7251
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
7252
+
7253
+ /** Set the joint limits
7254
+ * @param {number} min
7255
+ * @param {number} max */
7256
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
7257
+
7258
+ /** Is the joint motor enabled?
7259
+ * @return {boolean} */
7260
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7261
+
7262
+ /** Enable/disable the joint motor
7263
+ * @param {boolean} [enable] */
7264
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7265
+
7266
+ /** Set the motor speed
7267
+ * @param {number} speed */
7268
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7269
+
7270
+ /** Get the motor speed
7271
+ * @return {number} */
7272
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7273
+
7274
+ /** Set the motor torque
7275
+ * @param {number} torque */
7276
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
7277
+
7278
+ /** Get the max motor torque
7279
+ * @return {number} */
7280
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
7281
+
7282
+ /** Get the motor torque given a time step
7283
+ * @param {number} time
7284
+ * @return {number} */
7285
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
7286
+ }
7287
+
7288
+ ///////////////////////////////////////////////////////////////////////////////
7289
+ /**
7290
+ * Box2D Gear Joint
7291
+ * - A gear joint is used to connect two joints together
7292
+ * - Either joint can be a revolute or prismatic joint
7293
+ * - You specify a gear ratio to bind the motions together
7294
+ * @extends Box2dJoint
7295
+ */
7296
+ class Box2dGearJoint extends Box2dJoint
7297
+ {
7298
+ /** Create a gear joint
7299
+ * @param {Box2dObject} objectA
7300
+ * @param {Box2dObject} objectB
7301
+ * @param {Box2dJoint} joint1
7302
+ * @param {Box2dJoint} joint2
7303
+ * @param {ratio} [ratio] */
7304
+ constructor(objectA, objectB, joint1, joint2, ratio=1)
7305
+ {
7306
+ const jointDef = new box2d.instance.b2GearJointDef();
7307
+ jointDef.set_bodyA(objectA.body);
7308
+ jointDef.set_bodyB(objectB.body);
7309
+ jointDef.set_joint1(joint1.box2dJoint);
7310
+ jointDef.set_joint2(joint2.box2dJoint);
7311
+ jointDef.set_ratio(ratio);
7312
+ super(jointDef);
7313
+
7314
+ this.joint1 = joint1;
7315
+ this.joint2 = joint2;
7316
+ }
7317
+
7318
+ /** Get the first joint
7319
+ * @return {Box2dJoint} */
7320
+ getJoint1() { return this.joint1; }
7321
+
7322
+ /** Get the second joint
7323
+ * @return {Box2dJoint} */
7324
+ getJoint2() { return this.joint2; }
7325
+
7326
+ /** Set the gear ratio
7327
+ * @param {number} ratio */
7328
+ setRatio(ratio) { return this.box2dJoint.SetRatio(ratio); }
7329
+
7330
+ /** Get the gear ratio
7331
+ * @return {number} */
7332
+ getRatio() { return this.box2dJoint.GetRatio(); }
7333
+ }
7334
+
7335
+ ///////////////////////////////////////////////////////////////////////////////
7336
+ /**
7337
+ * Box2D Prismatic Joint
7338
+ * - Provides one degree of freedom: translation along an axis fixed in objectA
7339
+ * - Relative rotation is prevented
7340
+ * - You can use a joint limit to restrict the range of motion
7341
+ * - You can use a joint motor to drive the motion or to model joint friction
7342
+ * @extends Box2dJoint
7343
+ */
7344
+ class Box2dPrismaticJoint extends Box2dJoint
7345
+ {
7346
+ /** Create a prismatic joint
7347
+ * @param {Box2dObject} objectA
7348
+ * @param {Box2dObject} objectB
7349
+ * @param {Vector2} anchor
7350
+ * @param {Vector2} worldAxis
7351
+ * @param {boolean} [collide] */
7352
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
7353
+ {
7354
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7355
+ const localAnchorA = objectA.worldToLocal(anchor);
7356
+ const localAnchorB = objectB.worldToLocal(anchor);
7357
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
7358
+ const jointDef = new box2d.instance.b2PrismaticJointDef();
7359
+ jointDef.set_bodyA(objectA.body);
7360
+ jointDef.set_bodyB(objectB.body);
7361
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7362
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7363
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
7364
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7365
+ jointDef.set_collideConnected(collide);
7366
+ super(jointDef);
7367
+ }
7368
+
7369
+ /** Get the local anchor point relative to objectA's origin
7370
+ * @return {Vector2} */
7371
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7372
+
7373
+ /** Get the local anchor point relative to objectB's origin
7374
+ * @return {Vector2} */
7375
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7376
+
7377
+ /** Get the local joint axis relative to bodyA
7378
+ * @return {Vector2} */
7379
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
7380
+
7381
+ /** Get the reference angle
7382
+ * @return {number} */
7383
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7384
+
7385
+ /** Get the current joint translation
7386
+ * @return {number} */
7387
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
7388
+
7389
+ /** Get the current joint translation speed
7390
+ * @return {number} */
7391
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7392
+
7393
+ /** Is the joint limit enabled?
7394
+ * @return {boolean} */
7395
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
7396
+
7397
+ /** Enable/disable the joint limit
7398
+ * @param {boolean} [enable] */
7399
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
7400
+
7401
+ /** Get the lower joint limit
7402
+ * @return {number} */
7403
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
7404
+
7405
+ /** Get the upper joint limit
7406
+ * @return {number} */
7407
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
7408
+
7409
+ /** Set the joint limits
7410
+ * @param {number} min
7411
+ * @param {number} max */
7412
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
7413
+
7414
+ /** Is the motor enabled?
7415
+ * @return {boolean} */
7416
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7417
+
7418
+ /** Enable/disable the joint motor
7419
+ * @param {boolean} [enable] */
7420
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7421
+
7422
+ /** Set the motor speed
7423
+ * @param {number} speed */
7424
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7425
+
7426
+ /** Get the motor speed
7427
+ * @return {number} */
7428
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7429
+
7430
+ /** Set the maximum motor force
7431
+ * @param {number} force */
7432
+ setMaxMotorForce(force) { return this.box2dJoint.SetMaxMotorForce(force); }
7433
+
7434
+ /** Get the maximum motor force
7435
+ * @return {number} */
7436
+ getMaxMotorForce() { return this.box2dJoint.GetMaxMotorForce(); }
7437
+
7438
+ /** Get the motor force given a time step
7439
+ * @param {number} time
7440
+ * @return {number} */
7441
+ getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
7442
+ }
7443
+
7444
+ ///////////////////////////////////////////////////////////////////////////////
7445
+ /**
7446
+ * Box2D Wheel Joint
7447
+ * - Provides two degrees of freedom: translation along an axis fixed in objectA and rotation
7448
+ * - You can use a joint limit to restrict the range of motion
7449
+ * - You can use a joint motor to drive the motion or to model joint friction
7450
+ * - This joint is designed for vehicle suspensions
7451
+ * @extends Box2dJoint
7452
+ */
7453
+ class Box2dWheelJoint extends Box2dJoint
7454
+ {
7455
+ /** Create a wheel joint
7456
+ * @param {Box2dObject} objectA
7457
+ * @param {Box2dObject} objectB
7458
+ * @param {Vector2} anchor
7459
+ * @param {Vector2} worldAxis
7460
+ * @param {boolean} [collide] */
7461
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
7462
+ {
7463
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7464
+ const localAnchorA = objectA.worldToLocal(anchor);
7465
+ const localAnchorB = objectB.worldToLocal(anchor);
7466
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
7467
+ const jointDef = new box2d.instance.b2WheelJointDef();
7468
+ jointDef.set_bodyA(objectA.body);
7469
+ jointDef.set_bodyB(objectB.body);
7470
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7471
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7472
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
7473
+ jointDef.set_collideConnected(collide);
7474
+ super(jointDef);
7475
+ }
7476
+
7477
+ /** Get the local anchor point relative to objectA's origin
7478
+ * @return {Vector2} */
7479
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7480
+
7481
+ /** Get the local anchor point relative to objectB's origin
7482
+ * @return {Vector2} */
7483
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7484
+
7485
+ /** Get the local joint axis relative to bodyA
7486
+ * @return {Vector2} */
7487
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
7488
+
7489
+ /** Get the current joint translation
7490
+ * @return {number} */
7491
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
7492
+
7493
+ /** Get the current joint translation speed
7494
+ * @return {number} */
7495
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7496
+
7497
+ /** Is the joint motor enabled?
7498
+ * @return {boolean} */
7499
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7500
+
7501
+ /** Enable/disable the joint motor
7502
+ * @param {boolean} [enable] */
7503
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7504
+
7505
+ /** Set the motor speed
7506
+ * @param {number} speed */
7507
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7508
+
7509
+ /** Get the motor speed
7510
+ * @return {number} */
7511
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7512
+
7513
+ /** Set the maximum motor torque
7514
+ * @param {number} torque */
7515
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
7516
+
7517
+ /** Get the max motor torque
7518
+ * @return {number} */
7519
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
7520
+
7521
+ /** Get the motor torque for a time step
7522
+ * @return {number} */
7523
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
7524
+
7525
+ /** Set the spring frequency in Hertz
7526
+ * @param {number} hz */
7527
+ setSpringFrequencyHz(hz) { return this.box2dJoint.SetSpringFrequencyHz(hz); }
7528
+
7529
+ /** Get the spring frequency in Hertz
7530
+ * @return {number} */
7531
+ getSpringFrequencyHz() { return this.box2dJoint.GetSpringFrequencyHz(); }
7532
+
7533
+ /** Set the spring damping ratio
7534
+ * @param {number} ratio */
7535
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
7536
+
7537
+ /** Get the spring damping ratio
7538
+ * @return {number} */
7539
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
7540
+ }
7541
+
7542
+ ///////////////////////////////////////////////////////////////////////////////
7543
+ /**
7544
+ * Box2D Weld Joint
7545
+ * - Glues two objects together
7546
+ * @extends Box2dJoint
7547
+ */
7548
+ class Box2dWeldJoint extends Box2dJoint
7549
+ {
7550
+ /** Create a weld joint
7551
+ * @param {Box2dObject} objectA
7552
+ * @param {Box2dObject} objectB
7553
+ * @param {Vector2} anchor
7554
+ * @param {boolean} [collide] */
7555
+ constructor(objectA, objectB, anchor, collide=false)
7556
+ {
7557
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7558
+ const localAnchorA = objectA.worldToLocal(anchor);
7559
+ const localAnchorB = objectB.worldToLocal(anchor);
7560
+ const jointDef = new box2d.instance.b2WeldJointDef();
7561
+ jointDef.set_bodyA(objectA.body);
7562
+ jointDef.set_bodyB(objectB.body);
7563
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7564
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7565
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7566
+ jointDef.set_collideConnected(collide);
7567
+ super(jointDef);
7568
+ }
7569
+
7570
+ /** Get the local anchor point relative to objectA's origin
7571
+ * @return {Vector2} */
7572
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7573
+
7574
+ /** Get the local anchor point relative to objectB's origin
7575
+ * @return {Vector2} */
7576
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7577
+
7578
+ /** Get the reference angle
7579
+ * @return {number} */
7580
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7581
+
7582
+ /** Set the frequency in Hertz
7583
+ * @param {number} hz */
7584
+ setFrequency(hz) { return this.box2dJoint.SetFrequency(hz); }
7585
+
7586
+ /** Get the frequency in Hertz
7587
+ * @return {number} */
7588
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7589
+
7590
+ /** Set the damping ratio
7591
+ * @param {number} ratio */
7592
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
7593
+
7594
+ /** Get the damping ratio
7595
+ * @return {number} */
7596
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
7597
+ }
7598
+
7599
+ ///////////////////////////////////////////////////////////////////////////////
7600
+ /**
7601
+ * Box2D Friction Joint
7602
+ * - Used to apply top-down friction
7603
+ * - Provides 2D translational friction and angular friction
7604
+ * @extends Box2dJoint
7605
+ */
7606
+ class Box2dFrictionJoint extends Box2dJoint
7607
+ {
7608
+ /** Create a friction joint
7609
+ * @param {Box2dObject} objectA
7610
+ * @param {Box2dObject} objectB
7611
+ * @param {Vector2} anchor
7612
+ * @param {boolean} [collide] */
7613
+ constructor(objectA, objectB, anchor, collide=false)
7614
+ {
7615
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7616
+ const localAnchorA = objectA.worldToLocal(anchor);
7617
+ const localAnchorB = objectB.worldToLocal(anchor);
7618
+ const jointDef = new box2d.instance.b2FrictionJointDef();
7619
+ jointDef.set_bodyA(objectA.body);
7620
+ jointDef.set_bodyB(objectB.body);
7621
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7622
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7623
+ jointDef.set_collideConnected(collide);
7624
+ super(jointDef);
7625
+ }
7626
+
7627
+ /** Get the local anchor point relative to objectA's origin
7628
+ * @return {Vector2} */
7629
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7630
+
7631
+ /** Get the local anchor point relative to objectB's origin
7632
+ * @return {Vector2} */
7633
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7634
+
7635
+ /** Set the maximum friction force
7636
+ * @param {number} force */
7637
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
7638
+
7639
+ /** Get the maximum friction force
7640
+ * @return {number} */
7641
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
7642
+
7643
+ /** Set the maximum friction torque
7644
+ * @param {number} torque */
7645
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
7646
+
7647
+ /** Get the maximum friction torque
7648
+ * @return {number} */
7649
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
7650
+ }
7651
+
7652
+ ///////////////////////////////////////////////////////////////////////////////
7653
+ /**
7654
+ * Box2D Pulley Joint
7655
+ * - Connects to two objects and two fixed ground points
7656
+ * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
7657
+ * - The force transmitted is scaled by the ratio
7658
+ * @extends Box2dJoint
7659
+ */
7660
+ class Box2dPulleyJoint extends Box2dJoint
7661
+ {
7662
+ /** Create a pulley joint
7663
+ * @param {Box2dObject} objectA
7664
+ * @param {Box2dObject} objectB
7665
+ * @param {Vector2} groundAnchorA
7666
+ * @param {Vector2} groundAnchorB
7667
+ * @param {Vector2} anchorA
7668
+ * @param {Vector2} anchorB
7669
+ * @param {number} [ratio]
7670
+ * @param {boolean} [collide] */
7671
+ constructor(objectA, objectB, groundAnchorA, groundAnchorB, anchorA, anchorB, ratio=1, collide=false)
7672
+ {
7673
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7674
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7675
+ const localAnchorA = objectA.worldToLocal(anchorA);
7676
+ const localAnchorB = objectB.worldToLocal(anchorB);
7677
+ const jointDef = new box2d.instance.b2PulleyJointDef();
7678
+ jointDef.set_bodyA(objectA.body);
7679
+ jointDef.set_bodyB(objectB.body);
7680
+ jointDef.set_groundAnchorA(box2d.vec2dTo(groundAnchorA));
7681
+ jointDef.set_groundAnchorB(box2d.vec2dTo(groundAnchorB));
7682
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7683
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7684
+ jointDef.set_ratio(ratio);
7685
+ jointDef.set_lengthA(groundAnchorA.distance(anchorA));
7686
+ jointDef.set_lengthB(groundAnchorB.distance(anchorB));
7687
+ jointDef.set_collideConnected(collide);
7688
+ super(jointDef);
7689
+ }
7690
+
7691
+ /** Get the first ground anchor
7692
+ * @return {Vector2} */
7693
+ getGroundAnchorA() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorA()); }
7694
+
7695
+ /** Get the second ground anchor
7696
+ * @return {Vector2} */
7697
+ getGroundAnchorB() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorB()); }
7698
+
7699
+ /** Get the current length of the segment attached to objectA
7700
+ * @return {number} */
7701
+ getLengthA() { return this.box2dJoint.GetLengthA(); }
7702
+
7703
+ /** Get the current length of the segment attached to objectB
7704
+ * @return {number} */
7705
+ getLengthB(){ return this.box2dJoint.GetLengthB(); }
7706
+
7707
+ /** Get the pulley ratio
7708
+ * @return {number} */
7709
+ getRatio() { return this.box2dJoint.GetRatio(); }
7710
+
7711
+ /** Get the current length of the segment attached to objectA
7712
+ * @return {number} */
7713
+ getCurrentLengthA() { return this.box2dJoint.GetCurrentLengthA(); }
7714
+
7715
+ /** Get the current length of the segment attached to objectB
7716
+ * @return {number} */
7717
+ getCurrentLengthB() { return this.box2dJoint.GetCurrentLengthB(); }
7718
+ }
7719
+
7720
+ ///////////////////////////////////////////////////////////////////////////////
7721
+ /**
7722
+ * Box2D Motor Joint
7723
+ * - Controls the relative motion between two objects
7724
+ * - Typical usage is to control the movement of a object with respect to the ground
7725
+ * @extends Box2dJoint
7726
+ */
7727
+ class Box2dMotorJoint extends Box2dJoint
7728
+ {
7729
+ /** Create a motor joint
7730
+ * @param {Box2dObject} objectA
7731
+ * @param {Box2dObject} objectB */
7732
+ constructor(objectA, objectB)
7733
+ {
7734
+ const linearOffset = objectA.worldToLocal(box2d.vec2From(objectB.body.GetPosition()));
7735
+ const angularOffset = objectB.body.GetAngle() - objectA.body.GetAngle();
7736
+ const jointDef = new box2d.instance.b2MotorJointDef();
7737
+ jointDef.set_bodyA(objectA.body);
7738
+ jointDef.set_bodyB(objectB.body);
7739
+ jointDef.set_linearOffset(box2d.vec2dTo(linearOffset));
7740
+ jointDef.set_angularOffset(angularOffset);
7741
+ super(jointDef);
7742
+ }
7743
+
7744
+ /** Set the target linear offset, in frame A, in meters.
7745
+ * @param {Vector2} offset */
7746
+ setLinearOffset(offset) { this.box2dJoint.SetLinearOffset(box2d.vec2dTo(offset)); }
7747
+
7748
+ /** Get the target linear offset, in frame A, in meters.
7749
+ * @return {Vector2} */
7750
+ getLinearOffset() { return box2d.vec2From(this.box2dJoint.GetLinearOffset()); }
7751
+
7752
+ /** Set the target angular offset
7753
+ * @param {number} offset */
7754
+ setAngularOffset(offset) { this.box2dJoint.SetAngularOffset(offset); }
7755
+
7756
+ /** Get the target angular offset
7757
+ * @return {number} */
7758
+ getAngularOffset() { return this.box2dJoint.GetAngularOffset(); }
7759
+
7760
+ /** Set the maximum friction force
7761
+ * @param {number} force */
7762
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
7763
+
7764
+ /** Get the maximum friction force
7765
+ * @return {number} */
7766
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
7767
+
7768
+ /** Set the maximum torque
7769
+ * @param {number} torque */
7770
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
7771
+
7772
+ /** Get the maximum torque
7773
+ * @return {number} */
7774
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
7775
+
7776
+ /** Set the position correction factor in the range [0,1]
7777
+ * @param {number} factor */
7778
+ setCorrectionFactor(factor) { this.box2dJoint.SetCorrectionFactor(factor); }
7779
+
7780
+ /** Get the position correction factor in the range [0,1]
7781
+ * @return {number} */
7782
+ getCorrectionFactor() { return this.box2dJoint.GetCorrectionFactor(); }
7783
+ }
7784
+
7785
+ ///////////////////////////////////////////////////////////////////////////////
7786
+ /**
7787
+ * Box2D Global Object
7788
+ * - Wraps Box2d world and provides global functions
7789
+ */
7790
+ class Box2dPlugin
7791
+ {
7792
+ /** Create the global UI system object
7793
+ * @param {Object} instance */
7794
+ constructor(instance)
7795
+ {
7796
+ ASSERT(!box2d, 'Box2D already initialized');
7797
+ box2d = this;
7798
+ this.instance = instance;
7799
+ this.world = new box2d.instance.b2World();
7800
+
7801
+ /** @property {number} - Velocity iterations per update*/
7802
+ this.velocityIterations = 8;
7803
+ /** @property {number} - Position iterations per update*/
7804
+ this.positionIterations = 3;
7805
+ /** @property {number} - Static, zero mass, zero velocity, may be manually moved */
7806
+ this.bodyTypeStatic = instance.b2_staticBody;
7807
+ /** @property {number} - Kinematic, zero mass, non-zero velocity set by user, moved by solver */
7808
+ this.bodyTypeKinematic = instance.b2_kinematicBody;
7809
+ /** @property {number} - Dynamic, positive mass, non-zero velocity determined by forces, moved by solver */
7810
+ this.bodyTypeDynamic = instance.b2_dynamicBody;
7811
+
7812
+ // setup contact listener
7813
+ const listener = new box2d.instance.JSContactListener();
7814
+ listener.BeginContact = function(contactPtr)
7815
+ {
7816
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
7817
+ const fixtureA = contact.GetFixtureA();
7818
+ const fixtureB = contact.GetFixtureB();
7819
+ const objectA = fixtureA.GetBody().object;
7820
+ const objectB = fixtureB.GetBody().object;
7821
+ objectA.beginContact(objectB);
7822
+ objectB.beginContact(objectA);
7823
+ }
7824
+ listener.EndContact = function(contactPtr)
7825
+ {
7826
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
7827
+ const fixtureA = contact.GetFixtureA();
7828
+ const fixtureB = contact.GetFixtureB();
7829
+ const objectA = fixtureA.GetBody().object;
7830
+ const objectB = fixtureB.GetBody().object;
7831
+ objectA.endContact(objectB);
7832
+ objectB.endContact(objectA);
7833
+ };
7834
+ listener.PreSolve = function() {};
7835
+ listener.PostSolve = function() {};
7836
+ box2d.world.SetContactListener(listener);
7837
+ }
7838
+
7839
+ /** Step the physics world simulation
7840
+ * @param {number} [frames] */
7841
+ step(frames=1)
7842
+ {
7843
+ box2d.world.SetGravity(box2d.vec2dTo(gravity));
7844
+ for (let i=frames; i--;)
7845
+ box2d.world.Step(timeDelta, this.velocityIterations, this.positionIterations);
7846
+ }
7847
+
7848
+ ///////////////////////////////////////////////////////////////////////////////
7849
+ // raycasting and querying
7850
+
7851
+ /** raycast and return a list of all the results
7852
+ * @param {Vector2} start
7853
+ * @param {Vector2} end */
7854
+ raycastAll(start, end)
7855
+ {
7856
+ const raycastCallback = new box2d.instance.JSRayCastCallback();
7857
+ raycastCallback.ReportFixture = function(fixturePointer, point, normal, fraction)
7858
+ {
7859
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
7860
+ point = box2d.vec2FromPointer(point);
7861
+ normal = box2d.vec2FromPointer(normal);
7862
+ raycastResults.push(new Box2dRaycastResult(fixture, point, normal, fraction));
7863
+ return 1; // continue getting results
7864
+ };
7865
+
7866
+ const raycastResults = [];
7867
+ box2d.world.RayCast(raycastCallback, box2d.vec2dTo(start), box2d.vec2dTo(end));
7868
+ debugRaycast && debugLine(start, end, raycastResults.length ? '#f00' : '#00f', .02);
7869
+ return raycastResults;
7870
+ }
7871
+
7872
+ /** raycast and return the first result
7873
+ * @param {Vector2} start
7874
+ * @param {Vector2} end */
7875
+ raycast(start, end)
7876
+ {
7877
+ const raycastResults = box2d.raycastAll(start, end);
7878
+ if (!raycastResults.length)
7879
+ return undefined;
7880
+ return raycastResults.reduce((a,b)=>a.fraction < b.fraction ? a : b);
7881
+ }
7882
+
7883
+ /** box aabb cast and return all the objects
7884
+ * @param {Vector2} pos
7885
+ * @param {Vector2} size */
7886
+ boxCastAll(pos, size)
7887
+ {
7888
+ const queryCallback = new box2d.instance.JSQueryCallback();
7889
+ queryCallback.ReportFixture = function(fixturePointer)
7890
+ {
7891
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
7892
+ const o = fixture.GetBody().object;
7893
+ if (!queryObjects.includes(o))
7894
+ queryObjects.push(o); // add if not already in list
7895
+ return true; // continue getting results
7896
+ };
7897
+
7898
+ const aabb = new box2d.instance.b2AABB();
7899
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
7900
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
7901
+
7902
+ let queryObjects = [];
7903
+ box2d.world.QueryAABB(queryCallback, aabb);
7904
+ debugRaycast && debugRect(pos, size, queryObjects.length ? '#f00' : '#00f', .02);
7905
+ return queryObjects;
7906
+ }
7907
+
7908
+ /** box aabb cast and return the first object
7909
+ * @param {Vector2} pos
7910
+ * @param {Vector2} size */
7911
+ boxCast(pos, size)
7912
+ {
7913
+ const queryCallback = new box2d.instance.JSQueryCallback();
7914
+ queryCallback.ReportFixture = function(fixturePointer)
7915
+ {
7916
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
7917
+ queryObject = fixture.GetBody().object;
7918
+ return false; // stop getting results
7919
+ };
7920
+
7921
+ const aabb = new box2d.instance.b2AABB();
7922
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
7923
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
7924
+
7925
+ let queryObject;
7926
+ box2d.world.QueryAABB(queryCallback, aabb);
7927
+ debugRaycast && debugRect(pos, size, queryObject ? '#f00' : '#00f', .02);
7928
+ return queryObject;
7929
+ }
7930
+
7931
+ /** circle cast and return all the objects
7932
+ * @param {Vector2} pos
7933
+ * @param {number} diameter */
7934
+ circleCastAll(pos, diameter)
7935
+ {
7936
+ const radius2 = (diameter/2)**2;
7937
+ const results = box2d.boxCastAll(pos, vec2(diameter));
7938
+ return results.filter(o=>o.pos.distanceSquared(pos) < radius2);
7939
+ }
7940
+
7941
+ /** circle cast and return the first object
7942
+ * @param {Vector2} pos
7943
+ * @param {number} diameter */
7944
+ circleCast(pos, diameter)
7945
+ {
7946
+ const radius2 = (diameter/2)**2;
7947
+ let results = box2d.boxCastAll(pos, vec2(diameter));
7948
+
7949
+ let bestResult, bestDistance2;
7950
+ for (const result of results)
7951
+ {
7952
+ const distance2 = result.pos.distanceSquared(pos);
7953
+ if (distance2 < radius2 && (!bestResult || distance2 < bestDistance2))
7954
+ {
7955
+ bestResult = result;
7956
+ bestDistance2 = distance2;
7957
+ }
7958
+ }
7959
+ return bestResult;
7960
+ }
7961
+
7962
+ /** point cast and return the first object
7963
+ * @param {Vector2} pos
7964
+ * @param {boolean} dynamicOnly */
7965
+ pointCast(pos, dynamicOnly=true)
7966
+ {
7967
+ const queryCallback = new box2d.instance.JSQueryCallback();
7968
+ queryCallback.ReportFixture = function(fixturePointer)
7969
+ {
7970
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
7971
+ if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
7972
+ return true; // continue getting results
7973
+ if (!fixture.TestPoint(box2d.vec2dTo(pos)))
7974
+ return true; // continue getting results
7975
+ queryObject = fixture.GetBody().object;
7976
+ return false; // stop getting results
7977
+ };
7978
+
7979
+ const aabb = new box2d.instance.b2AABB();
7980
+ aabb.set_lowerBound(box2d.vec2dTo(pos));
7981
+ aabb.set_upperBound(box2d.vec2dTo(pos));
7982
+
7983
+ let queryObject;
7984
+ box2d.world.QueryAABB(queryCallback, aabb);
7985
+ debugRaycast && debugRect(pos, vec2(), queryObject ? '#f00' : '#00f', .02);
7986
+ return queryObject;
7987
+ }
7988
+
7989
+ ///////////////////////////////////////////////////////////////////////////////
7990
+ // drawing
7991
+
7992
+ /** draws a fixture
7993
+ * @param {Object} fixture
7994
+ * @param {Vector2} pos
7995
+ * @param {number} angle
7996
+ * @param {Color} [color]
7997
+ * @param {Color} [outlineColor]
7998
+ * @param {number} [lineWidth]
7999
+ * @param {CanvasRenderingContext2D} [context] */
8000
+ drawFixture(fixture, pos, angle, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8001
+ {
8002
+ const shape = box2d.castObjectType(fixture.GetShape());
8003
+ switch (shape.GetType())
8004
+ {
8005
+ case box2d.instance.b2Shape.e_polygon:
8006
+ {
8007
+ let points = [];
8008
+ for (let i=shape.GetVertexCount(); i--;)
8009
+ points.push(box2d.vec2From(shape.GetVertex(i)));
8010
+ box2d.drawPoly(pos, angle, points, color, outlineColor, lineWidth, context);
8011
+ break;
8012
+ }
8013
+ case box2d.instance.b2Shape.e_circle:
8014
+ {
8015
+ const radius = shape.get_m_radius();
8016
+ box2d.drawCircle(pos, radius, color, outlineColor, lineWidth, context);
8017
+ break;
8018
+ }
8019
+ case box2d.instance.b2Shape.e_edge:
8020
+ {
8021
+ const v1 = box2d.vec2From(shape.get_m_vertex1());
8022
+ const v2 = box2d.vec2From(shape.get_m_vertex2());
8023
+ box2d.drawLine(pos, angle, v1, v2, color, lineWidth, context);
8024
+ break;
8025
+ }
8026
+ }
8027
+ }
8028
+
8029
+ /** draws a circle
8030
+ * @param {Vector2} pos
8031
+ * @param {number} radius
8032
+ * @param {Color} [color]
8033
+ * @param {Color} [outlineColor]
8034
+ * @param {number} [lineWidth]
8035
+ * @param {CanvasRenderingContext2D} [context] */
8036
+ drawCircle(pos, radius, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8037
+ {
8038
+ drawCanvas2D(pos, vec2(1), 0, 0, context=>
8039
+ {
8040
+ context.beginPath();
8041
+ context.arc(0, 0, radius, 0, 9);
8042
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8043
+ }, 0, context);
8044
+ }
8045
+
8046
+ /** draws a polygon
8047
+ * @param {Vector2} pos
8048
+ * @param {number} angle
8049
+ * @param {Array<Vector2>} points
8050
+ * @param {Color} [color]
8051
+ * @param {Color} [outlineColor]
8052
+ * @param {number} [lineWidth]
8053
+ * @param {CanvasRenderingContext2D} [context] */
8054
+ drawPoly(pos, angle, points, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8055
+ {
8056
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8057
+ {
8058
+ context.beginPath();
8059
+ points.forEach(p=>context.lineTo(p.x, p.y));
8060
+ context.closePath();
8061
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8062
+ }, 0, context);
8063
+ }
8064
+
8065
+ /** draws a line
8066
+ * @param {Vector2} pos
8067
+ * @param {number} angle
8068
+ * @param {Vector2} posA
8069
+ * @param {Vector2} posB
8070
+ * @param {Color} [color]
8071
+ * @param {number} [lineWidth]
8072
+ * @param {CanvasRenderingContext2D} [context] */
8073
+ drawLine(pos, angle, posA, posB, color=WHITE, lineWidth=.1, context=mainContext)
8074
+ {
8075
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8076
+ {
8077
+ context.beginPath();
8078
+ context.lineTo(posA.x, posA.y);
8079
+ context.lineTo(posB.x, posB.y);
8080
+ box2d.drawFillStroke(0, color, lineWidth, context);
8081
+ }, 0, context);
8082
+ }
8083
+
8084
+ /** performs a fill or stroke as a helper to the other draw functions
8085
+ * @param {Color} [color]
8086
+ * @param {Color} [outlineColor]
8087
+ * @param {number} [lineWidth]
8088
+ * @param {CanvasRenderingContext2D} [context] */
8089
+ drawFillStroke(color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8090
+ {
8091
+ if (color)
8092
+ {
8093
+ context.fillStyle = color.toString();
8094
+ context.fill();
8095
+ }
8096
+ if (outlineColor && lineWidth)
8097
+ {
8098
+ context.lineWidth = lineWidth;
8099
+ context.lineJoin = context.lineCap = 'round';
8100
+ context.strokeStyle = outlineColor.toString();
8101
+ context.stroke();
8102
+ }
8103
+ }
8104
+
8105
+ ///////////////////////////////////////////////////////////////////////////////
8106
+ // helper functions
8107
+
8108
+ /** converts a box2d vec2 to a Vector2
8109
+ * @param {Object} v */
8110
+ vec2From(v)
8111
+ {
8112
+ ASSERT(v instanceof box2d.instance.b2Vec2);
8113
+ return new Vector2(v.get_x(), v.get_y());
8114
+ }
8115
+
8116
+ /** converts a box2d vec2 pointer to a Vector2
8117
+ * @param {Object} v */
8118
+ vec2FromPointer(v)
8119
+ {
8120
+ return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
8121
+ }
8122
+
8123
+ /** converts a Vector2 to a box2 vec2
8124
+ * @param {Vector2} v */
8125
+ vec2dTo(v)
8126
+ {
8127
+ ASSERT(v instanceof Vector2);
8128
+ return new box2d.instance.b2Vec2(v.x, v.y);
8129
+ }
8130
+
8131
+ /** checks if a box2d object is null
8132
+ * @param {Object} o */
8133
+ isNull(o) { return !box2d.instance.getPointer(o); }
8134
+
8135
+ /** casts a box2d object to its correct type
8136
+ * @param {Object} o */
8137
+ castObjectType(o)
8138
+ {
8139
+ switch (o.GetType())
8140
+ {
8141
+ case box2d.instance.b2Shape.e_circle:
8142
+ return box2d.instance.castObject(o, box2d.instance.b2CircleShape);
8143
+ case box2d.instance.b2Shape.e_edge:
8144
+ return box2d.instance.castObject(o, box2d.instance.b2EdgeShape);
8145
+ case box2d.instance.b2Shape.e_polygon:
8146
+ return box2d.instance.castObject(o, box2d.instance.b2PolygonShape);
8147
+ case box2d.instance.b2Shape.e_chain:
8148
+ return box2d.instance.castObject(o, box2d.instance.b2ChainShape);
8149
+ case box2d.instance.e_revoluteJoint:
8150
+ return box2d.instance.castObject(o, box2d.instance.b2RevoluteJoint);
8151
+ case box2d.instance.e_prismaticJoint:
8152
+ return box2d.instance.castObject(o, box2d.instance.b2PrismaticJoint);
8153
+ case box2d.instance.e_distanceJoint:
8154
+ return box2d.instance.castObject(o, box2d.instance.b2DistanceJoint);
8155
+ case box2d.instance.e_pulleyJoint:
8156
+ return box2d.instance.castObject(o, box2d.instance.b2PulleyJoint);
8157
+ case box2d.instance.e_mouseJoint:
8158
+ return box2d.instance.castObject(o, box2d.instance.b2MouseJoint);
8159
+ case box2d.instance.e_gearJoint:
8160
+ return box2d.instance.castObject(o, box2d.instance.b2GearJoint);
8161
+ case box2d.instance.e_wheelJoint:
8162
+ return box2d.instance.castObject(o, box2d.instance.b2WheelJoint);
8163
+ case box2d.instance.e_weldJoint:
8164
+ return box2d.instance.castObject(o, box2d.instance.b2WeldJoint);
8165
+ case box2d.instance.e_frictionJoint:
8166
+ return box2d.instance.castObject(o, box2d.instance.b2FrictionJoint);
8167
+ case box2d.instance.e_ropeJoint:
8168
+ return box2d.instance.castObject(o, box2d.instance.b2RopeJoint);
8169
+ case box2d.instance.e_motorJoint:
8170
+ return box2d.instance.castObject(o, box2d.instance.b2MotorJoint);
8171
+ }
8172
+
8173
+ ASSERT(false, 'Unknown box2d object type');
8174
+ }
8175
+ }
8176
+
8177
+ ///////////////////////////////////////////////////////////////////////////////
8178
+ /** Box2d Init - Call with await before starting LittleJS to init box2d
8179
+ * @return {Promise<Box2dPlugin>}
8180
+ * @memberof Box2D */
8181
+ async function box2dInit()
8182
+ {
8183
+ // load box2d
8184
+ new Box2dPlugin(await Box2D());
8185
+ setupDebugDraw();
8186
+ engineAddPlugin(box2dUpdate, box2dRender);
8187
+ return box2d;
8188
+
8189
+ // add the box2d plugin to the engine
8190
+ function box2dUpdate()
8191
+ {
8192
+ if (!paused)
8193
+ box2d.step();
8194
+ }
8195
+ function box2dRender()
8196
+ {
8197
+ if (box2dDebug || debugPhysics && debugOverlay)
8198
+ box2d.world.DrawDebugData();
8199
+ }
8200
+
8201
+ // box2d debug drawing
8202
+ function setupDebugDraw()
8203
+ {
8204
+ // setup debug draw
8205
+ const debugDraw = new box2d.instance.JSDraw();
8206
+ const box2dColor = (c)=> new Color(c.get_r(), c.get_g(), c.get_b());
8207
+ const box2dColorPointer = (c)=>
8208
+ box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
8209
+ const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
8210
+ const getPointsList = (vertices, vertexCount) =>
8211
+ {
8212
+ const points = [];
8213
+ for (let i=vertexCount; i--;)
8214
+ points.push(box2d.vec2FromPointer(vertices+i*8));
8215
+ return points;
8216
+ }
8217
+ debugDraw.DrawSegment = function(point1, point2, color)
8218
+ {
8219
+ color = getDebugColor(color);
8220
+ point1 = box2d.vec2FromPointer(point1);
8221
+ point2 = box2d.vec2FromPointer(point2);
8222
+ box2d.drawLine(vec2(), 0, point1, point2, color, undefined, overlayContext);
8223
+ };
8224
+ debugDraw.DrawPolygon = function(vertices, vertexCount, color)
8225
+ {
8226
+ color = getDebugColor(color);
8227
+ const points = getPointsList(vertices, vertexCount);
8228
+ box2d.drawPoly(vec2(), 0, points, undefined, color, undefined, overlayContext);
8229
+ };
8230
+ debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
8231
+ {
8232
+ color = getDebugColor(color);
8233
+ const points = getPointsList(vertices, vertexCount);
8234
+ box2d.drawPoly(vec2(), 0, points, color, color, undefined, overlayContext);
8235
+ };
8236
+ debugDraw.DrawCircle = function(center, radius, color)
8237
+ {
8238
+ color = getDebugColor(color);
8239
+ center = box2d.vec2FromPointer(center);
8240
+ box2d.drawCircle(center, radius, undefined, color, undefined, overlayContext);
8241
+ };
8242
+ debugDraw.DrawSolidCircle = function(center, radius, axis, color)
8243
+ {
8244
+ color = getDebugColor(color);
8245
+ center = box2d.vec2FromPointer(center);
8246
+ axis = box2d.vec2FromPointer(axis).scale(radius);
8247
+ box2d.drawCircle(center, radius, color, color, undefined, overlayContext);
8248
+ box2d.drawLine(center, 0, vec2(), axis, color, undefined, overlayContext);
8249
+ };
8250
+ debugDraw.DrawTransform = function(transform)
8251
+ {
8252
+ transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
8253
+ const pos = vec2(transform.get_p());
8254
+ const angle = -transform.get_q().GetAngle();
8255
+ const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
8256
+ const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
8257
+ box2d.drawLine(pos, angle, vec2(), p1, c1, undefined, overlayContext);
8258
+ box2d.drawLine(pos, angle, vec2(), p2, c2, undefined, overlayContext);
8259
+ }
8260
+
8261
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
8262
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_jointBit);
8263
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_aabbBit);
8264
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_pairBit);
8265
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_centerOfMassBit);
8266
+ box2d.world.SetDebugDraw(debugDraw);
8267
+ }
8268
+ }
8269
+