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
package/dist/littlejs.js CHANGED
@@ -13,8 +13,6 @@
13
13
  * @namespace Debug
14
14
  */
15
15
 
16
-
17
-
18
16
  /** True if debug is enabled
19
17
  * @type {boolean}
20
18
  * @default
@@ -334,11 +332,9 @@ function debugRender()
334
332
  }
335
333
  }
336
334
 
337
- if (tileCollisionSize.x > 0 && tileCollisionSize.y > 0)
335
+ if (tileCollisionLayers.length) // show floored tile pick if there is tile collision
338
336
  drawRect(mousePos.floor().add(vec2(.5)), vec2(1), rgb(0,0,1,.5), 0, false);
339
337
  mainContext = saveContext;
340
-
341
- //glCopyToContext(mainContext = saveContext);
342
338
  }
343
339
 
344
340
  {
@@ -581,8 +577,6 @@ function debugVideoCaptureUpdate()
581
577
  * @namespace Utilities
582
578
  */
583
579
 
584
-
585
-
586
580
  /** A shortcut to get Math.PI
587
581
  * @type {number}
588
582
  * @default Math.PI
@@ -762,6 +756,16 @@ function wave(frequency=1, amplitude=1, t=time)
762
756
  * @memberof Utilities */
763
757
  function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
764
758
 
759
+ /** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
760
+ * @param {string} url - URL of JSON file
761
+ * @return {Promise<object>}
762
+ * @memberof Utilities */
763
+ async function fetchJSON(url)
764
+ {
765
+ const response = await fetch(url);
766
+ return response.json();
767
+ }
768
+
765
769
  ///////////////////////////////////////////////////////////////////////////////
766
770
 
767
771
  /** Random global functions
@@ -857,6 +861,12 @@ class RandomGenerator
857
861
  /** Randomly returns either -1 or 1 deterministically
858
862
  * @return {number} */
859
863
  sign() { return this.float() > .5 ? 1 : -1; }
864
+
865
+ /** Returns a seeded random value between the two values passed in with a random sign
866
+ * @param {number} [valueA]
867
+ * @param {number} [valueB]
868
+ * @return {number} */
869
+ floatSign(valueA=1, valueB=0) { return this.float(valueA, valueB) * this.sign(); }
860
870
  }
861
871
 
862
872
  ///////////////////////////////////////////////////////////////////////////////
@@ -1497,8 +1507,6 @@ class Timer
1497
1507
  * @namespace Settings
1498
1508
  */
1499
1509
 
1500
-
1501
-
1502
1510
  ///////////////////////////////////////////////////////////////////////////////
1503
1511
  // Camera settings
1504
1512
 
@@ -1530,7 +1538,7 @@ let canvasMaxSize = vec2(1920, 1080);
1530
1538
  * @memberof Settings */
1531
1539
  let canvasFixedSize = vec2();
1532
1540
 
1533
- /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
1541
+ /** Use nearest neighbor canvas scaling for more pixelated look
1534
1542
  * - Must be set before startup to take effect
1535
1543
  * - If enabled sets css image-rendering:pixelated
1536
1544
  * @type {boolean}
@@ -1640,11 +1648,11 @@ let objectDefaultFriction = .8;
1640
1648
  * @memberof Settings */
1641
1649
  let objectMaxSpeed = 1;
1642
1650
 
1643
- /** How much gravity to apply to objects along the Y axis, negative is down
1644
- * @type {number}
1651
+ /** How much gravity to apply to objects, negative Y is down
1652
+ * @type {Vector2}
1645
1653
  * @default
1646
1654
  * @memberof Settings */
1647
- let gravity = 0;
1655
+ let gravity = vec2();
1648
1656
 
1649
1657
  /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
1650
1658
  * @type {number}
@@ -1870,8 +1878,8 @@ function setObjectDefaultFriction(friction) { objectDefaultFriction = friction;
1870
1878
  * @memberof Settings */
1871
1879
  function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
1872
1880
 
1873
- /** Set how much gravity to apply to objects along the Y axis
1874
- * @param {number} newGravity
1881
+ /** Set how much gravity to apply to objects
1882
+ * @param {Vector2} newGravity
1875
1883
  * @memberof Settings */
1876
1884
  function setGravity(newGravity) { gravity = newGravity; }
1877
1885
 
@@ -1983,8 +1991,6 @@ function setDebugKey(key) { debugKey = key; }
1983
1991
  * LittleJS Object System
1984
1992
  */
1985
1993
 
1986
-
1987
-
1988
1994
  /**
1989
1995
  * LittleJS Object Base Object Class
1990
1996
  * - Top level object class used by the engine
@@ -2139,7 +2145,10 @@ class EngineObject
2139
2145
  this.velocity.x *= this.damping;
2140
2146
  this.velocity.y *= this.damping;
2141
2147
  if (this.mass) // don't apply gravity to static objects
2142
- this.velocity.y += gravity * this.gravityScale;
2148
+ {
2149
+ this.velocity.x += gravity.x * this.gravityScale;
2150
+ this.velocity.y += gravity.y * this.gravityScale;
2151
+ }
2143
2152
  this.pos.x += this.velocity.x;
2144
2153
  this.pos.y += this.velocity.y;
2145
2154
  this.angle += this.angleVelocity *= this.angleDamping;
@@ -2154,9 +2163,9 @@ class EngineObject
2154
2163
  if (this.groundObject)
2155
2164
  {
2156
2165
  // apply friction in local space of ground object
2157
- const groundSpeed = this.groundObject != this && this.groundObject.velocity ?
2158
- this.groundObject.velocity.x : 0;
2159
- this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
2166
+ const friction = max(this.friction, this.groundObject.friction);
2167
+ const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
2168
+ this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
2160
2169
  this.groundObject = undefined;
2161
2170
  //debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
2162
2171
  }
@@ -2198,7 +2207,7 @@ class EngineObject
2198
2207
 
2199
2208
  // check for collision
2200
2209
  const sizeBoth = this.size.add(o.size);
2201
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity; // prefer to push up if small delta
2210
+ const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity.y; // prefer to push up if small delta
2202
2211
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
2203
2212
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
2204
2213
  const elasticity = max(this.elasticity, o.elasticity);
@@ -2260,19 +2269,21 @@ class EngineObject
2260
2269
  if (this.collideTiles)
2261
2270
  {
2262
2271
  // check collision against tiles
2263
- if (tileCollisionTest(this.pos, this.size, this))
2272
+ const hitLayer = tileCollisionTest(this.pos, this.size, this)
2273
+ if (hitLayer)
2264
2274
  {
2265
2275
  // if already was stuck in collision, don't do anything
2266
2276
  // this should not happen unless something starts in collision
2267
2277
  if (!tileCollisionTest(oldPos, this.size, this))
2268
2278
  {
2269
2279
  // test which side we bounced off (or both if a corner)
2270
- const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2271
- const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2272
- if (isBlockedY || !isBlockedX)
2280
+ const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2281
+ const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2282
+ if (blockedLayerY || !blockedLayerX)
2273
2283
  {
2274
2284
  // bounce velocity
2275
- this.velocity.y *= -this.elasticity;
2285
+ const elasticity = max(this.elasticity, hitLayer.elasticity);
2286
+ this.velocity.y *= -elasticity;
2276
2287
 
2277
2288
  if (wasMovingDown)
2278
2289
  {
@@ -2281,9 +2292,8 @@ class EngineObject
2281
2292
  const epsilon = .0001;
2282
2293
  this.pos.y = (oldPos.y-this.size.y/2|0)+this.size.y/2+epsilon;
2283
2294
 
2284
- // set ground object to self for tile collision
2285
- // TODO: rework system so tile collision is its own object
2286
- this.groundObject = this;
2295
+ // set ground object for tile collision
2296
+ this.groundObject = hitLayer;
2287
2297
  }
2288
2298
  else
2289
2299
  {
@@ -2292,7 +2302,7 @@ class EngineObject
2292
2302
  this.groundObject = undefined;
2293
2303
  }
2294
2304
  }
2295
- if (isBlockedX)
2305
+ if (blockedLayerX)
2296
2306
  {
2297
2307
  // move to previous position and bounce
2298
2308
  this.pos.x = oldPos.x;
@@ -2463,8 +2473,6 @@ class EngineObject
2463
2473
  * @namespace Draw
2464
2474
  */
2465
2475
 
2466
-
2467
-
2468
2476
  /** The primary 2D canvas visible to the user
2469
2477
  * @type {HTMLCanvasElement}
2470
2478
  * @memberof Draw */
@@ -3086,8 +3094,6 @@ function setCursor(cursorStyle = 'auto')
3086
3094
  * @namespace Input
3087
3095
  */
3088
3096
 
3089
-
3090
-
3091
3097
  /** Returns true if device key is down
3092
3098
  * @param {string|number} key
3093
3099
  * @param {number} [device]
@@ -3139,21 +3145,21 @@ function clearInput() { inputData = [[]]; touchGamepadButtons = []; }
3139
3145
  * @param {number} button
3140
3146
  * @return {boolean}
3141
3147
  * @memberof Input */
3142
- const mouseIsDown = keyIsDown;
3148
+ function mouseIsDown(button) { return keyIsDown(button); }
3143
3149
 
3144
3150
  /** Returns true if mouse button was pressed
3145
3151
  * @function
3146
3152
  * @param {number} button
3147
3153
  * @return {boolean}
3148
3154
  * @memberof Input */
3149
- const mouseWasPressed = keyWasPressed;
3155
+ function mouseWasPressed(button) { return keyWasPressed(button); }
3150
3156
 
3151
3157
  /** Returns true if mouse button was released
3152
3158
  * @function
3153
3159
  * @param {number} button
3154
3160
  * @return {boolean}
3155
3161
  * @memberof Input */
3156
- const mouseWasReleased = keyWasReleased;
3162
+ function mouseWasReleased(button) { return keyWasReleased(button); }
3157
3163
 
3158
3164
  /** Mouse pos in world space
3159
3165
  * @type {Vector2}
@@ -3392,7 +3398,9 @@ function gamepadsUpdate()
3392
3398
  const button = gamepad.buttons[j];
3393
3399
  const wasDown = gamepadIsDown(j,i);
3394
3400
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
3395
- isUsingGamepad ||= !i && button.pressed;
3401
+ if (!button.value || button.value > .9) // must be a full press
3402
+ if (!i && button.pressed)
3403
+ isUsingGamepad = true;
3396
3404
  }
3397
3405
 
3398
3406
  if (gamepadDirectionEmulateStick)
@@ -3610,8 +3618,6 @@ function touchGamepadRender()
3610
3618
  * @namespace Audio
3611
3619
  */
3612
3620
 
3613
-
3614
-
3615
3621
  /** Audio context used by the engine
3616
3622
  * @type {AudioContext}
3617
3623
  * @memberof Audio */
@@ -3648,17 +3654,17 @@ class Sound
3648
3654
  {
3649
3655
  /** Create a sound object and cache the zzfx samples for later use
3650
3656
  * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
3651
- * @param {number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
3657
+ * @param {number} [range=soundDefaultRange] - World space max range of sound
3652
3658
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
3653
3659
  */
3654
3660
  constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
3655
3661
  {
3656
3662
  if (!soundEnable || headlessMode) return;
3657
3663
 
3658
- /** @property {number} - World space max range of sound, will not play if camera is farther away */
3664
+ /** @property {number} - World space max range of sound */
3659
3665
  this.range = range;
3660
3666
 
3661
- /** @property {number} - At what percentage of range should it start tapering off */
3667
+ /** @property {number} - At what percentage of range should it start tapering */
3662
3668
  this.taper = taper;
3663
3669
 
3664
3670
  /** @property {number} - How much to randomize frequency each time sound plays */
@@ -3774,8 +3780,8 @@ class SoundWave extends Sound
3774
3780
  /** Create a sound object and cache the wave file for later use
3775
3781
  * @param {string} filename - Filename of audio file to load
3776
3782
  * @param {number} [randomness] - How much to randomize frequency each time sound plays
3777
- * @param {number} [range=soundDefaultRange] - World space max range of sound, will not play if camera is farther away
3778
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering off
3783
+ * @param {number} [range=soundDefaultRange] - World space max range of sound
3784
+ * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
3779
3785
  * @param {Function} [onloadCallback] - callback function to call when sound is loaded
3780
3786
  */
3781
3787
  constructor(filename, randomness=0, range, taper, onloadCallback)
@@ -3783,17 +3789,26 @@ class SoundWave extends Sound
3783
3789
  super(undefined, range, taper);
3784
3790
  if (!soundEnable || headlessMode) return;
3785
3791
 
3792
+ /** @property {Function} - callback function to call when sound is loaded */
3793
+ this.onloadCallback = onloadCallback;
3786
3794
  this.randomness = randomness;
3787
- fetch(filename)
3788
- .then(response => response.arrayBuffer())
3789
- .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
3790
- .then(audioBuffer =>
3791
- {
3792
- this.sampleChannels = [];
3793
- for (let i = audioBuffer.numberOfChannels; i--;)
3794
- this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
3795
- this.sampleRate = audioBuffer.sampleRate;
3796
- }).then(() => onloadCallback && onloadCallback(this));
3795
+ this.loadSound(filename);
3796
+ }
3797
+
3798
+ /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
3799
+ * @param {string} filename
3800
+ * @return {Promise<void>} */
3801
+ async loadSound(filename)
3802
+ {
3803
+ const response = await fetch(filename);
3804
+ const arrayBuffer = await response.arrayBuffer();
3805
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
3806
+ this.sampleChannels = [];
3807
+ for (let i = audioBuffer.numberOfChannels; i--;)
3808
+ this.sampleChannels[i] = Array.from(audioBuffer.getChannelData(i));
3809
+ this.sampleRate = audioBuffer.sampleRate;
3810
+ if (this.onloadCallback)
3811
+ this.onloadCallback();
3797
3812
  }
3798
3813
  }
3799
3814
 
@@ -4067,75 +4082,45 @@ function zzfxG
4067
4082
  * LittleJS Tile Layer System
4068
4083
  * - Caches arrays of tiles to off screen canvas for fast rendering
4069
4084
  * - Unlimited numbers of layers, allocates canvases as needed
4070
- * - Interfaces with EngineObject for collision
4071
- * - Collision layer is separate from visible layers
4072
- * - It is recommended to have a visible layer that matches the collision
4073
4085
  * - Tile layers can be drawn to using their context with canvas2d
4074
4086
  * - Drawn directly to the main canvas without using WebGL
4087
+ * - Tile layers can also have collision with EngineObjects
4075
4088
  * @namespace TileCollision
4076
4089
  */
4077
4090
 
4091
+ ///////////////////////////////////////////////////////////////////////////////
4092
+ // Tile Layer System
4078
4093
 
4079
-
4080
- /** The tile collision layer grid, use setTileCollisionData and getTileCollisionData to access
4081
- * @type {Array<number>}
4082
- * @memberof TileCollision */
4083
- let tileCollision = [];
4084
-
4085
- /** Size of the tile collision layer 2d grid
4086
- * @type {Vector2}
4087
- * @memberof TileCollision */
4088
- let tileCollisionSize = vec2();
4089
-
4090
- /** Clear and initialize tile collision
4091
- * @param {Vector2} size - width and height of tile collision 2d grid
4092
- * @memberof TileCollision */
4093
- function initTileCollision(size)
4094
- {
4095
- tileCollisionSize = size;
4096
- tileCollision = [];
4097
- for (let i=tileCollision.length = tileCollisionSize.area(); i--;)
4098
- tileCollision[i] = 0;
4099
- }
4100
-
4101
- /** Set tile collision data for a given cell in the grid
4102
- * @param {Vector2} pos
4103
- * @param {number} [data]
4094
+ /** Keep track of all tile layers with collision
4095
+ * @type {Array<TileCollisionLayer>}
4104
4096
  * @memberof TileCollision */
4105
- function setTileCollisionData(pos, data=0)
4106
- {
4107
- pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
4108
- }
4097
+ let tileCollisionLayers = [];
4109
4098
 
4110
4099
  /** Get tile collision data for a given cell in the grid
4111
- * @param {Vector2} pos
4112
- * @return {number}
4113
- * @memberof TileCollision */
4100
+ * @param {Vector2} pos
4101
+ * @return {number}
4102
+ * @memberof TileCollision */
4114
4103
  function getTileCollisionData(pos)
4115
4104
  {
4116
- return pos.arrayCheck(tileCollisionSize) ? tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] : 0;
4105
+ // check all tile collision layers
4106
+ for (const layer of tileCollisionLayers)
4107
+ if (pos.arrayCheck(layer.size))
4108
+ return layer.getCollisionData(pos);
4109
+ return 0;
4117
4110
  }
4118
4111
 
4119
- /** Check if collision with another object should occur
4112
+ /** Check if a tile layer collides with another object
4120
4113
  * @param {Vector2} pos
4121
4114
  * @param {Vector2} [size=(0,0)]
4122
4115
  * @param {EngineObject} [object]
4123
- * @return {boolean}
4116
+ * @return {TileCollisionLayer}
4124
4117
  * @memberof TileCollision */
4125
4118
  function tileCollisionTest(pos, size=vec2(), object)
4126
4119
  {
4127
- const minX = max(pos.x - size.x/2|0, 0);
4128
- const minY = max(pos.y - size.y/2|0, 0);
4129
- const maxX = min(pos.x + size.x/2, tileCollisionSize.x);
4130
- const maxY = min(pos.y + size.y/2, tileCollisionSize.y);
4131
- for (let y = minY; y < maxY; ++y)
4132
- for (let x = minX; x < maxX; ++x)
4133
- {
4134
- const tileData = tileCollision[y*tileCollisionSize.x+x];
4135
- if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
4136
- return true;
4137
- }
4138
- return false;
4120
+ // check all tile collision layers
4121
+ for (const layer of tileCollisionLayers)
4122
+ if (layer.collisionTest(pos, size, object))
4123
+ return layer;
4139
4124
  }
4140
4125
 
4141
4126
  /** Return the center of first tile hit, undefined if nothing was hit.
@@ -4147,49 +4132,17 @@ function tileCollisionTest(pos, size=vec2(), object)
4147
4132
  * @memberof TileCollision */
4148
4133
  function tileCollisionRaycast(posStart, posEnd, object)
4149
4134
  {
4150
- // test if a ray collides with tiles from start to end
4151
- // todo: a way to get the exact hit point, it must still be inside the hit tile
4152
- const delta = posEnd.subtract(posStart);
4153
- const totalLength = delta.length();
4154
- const normalizedDelta = delta.normalize();
4155
- const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
4156
- const flooredPosStart = posStart.floor();
4157
-
4158
- // setup iteration variables
4159
- let pos = flooredPosStart;
4160
- let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
4161
- let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
4162
-
4163
- while (true)
4164
- {
4165
- // check for tile collision
4166
- const tileData = getTileCollisionData(pos);
4167
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
4168
- {
4169
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
4170
- debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
4171
- return pos.add(vec2(.5));
4172
- }
4173
-
4174
- // check if past the end
4175
- if (xi > totalLength && yi > totalLength)
4176
- break;
4177
-
4178
- // get coordinates of the next tile to check
4179
- if (xi > yi)
4180
- pos.y += sign(delta.y), yi += unit.y;
4181
- else
4182
- pos.x += sign(delta.x), xi += unit.x;
4135
+ // check all tile collision layers
4136
+ for (const layer of tileCollisionLayers)
4137
+ {
4138
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object)
4139
+ if (hitPos)
4140
+ return hitPos;
4183
4141
  }
4184
-
4185
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
4186
4142
  }
4187
4143
 
4188
- ///////////////////////////////////////////////////////////////////////////////
4189
- // Tile Layer Rendering System
4190
-
4191
4144
  /**
4192
- * Tile layer data object stores info about how to render a tile
4145
+ * Tile layer data object stores info about how to draw a tile
4193
4146
  * @example
4194
4147
  * // create tile layer data with tile index 0 and random orientation and color
4195
4148
  * const tileIndex = 0;
@@ -4221,28 +4174,27 @@ class TileLayerData
4221
4174
  clear() { this.tile = this.direction = 0; this.mirror = false; this.color = new Color; }
4222
4175
  }
4223
4176
 
4177
+ ///////////////////////////////////////////////////////////////////////////////
4224
4178
  /**
4225
4179
  * Tile Layer - cached rendering system for tile layers
4226
4180
  * - Each Tile layer is rendered to an off screen canvas
4227
4181
  * - To allow dynamic modifications, layers are rendered using canvas 2d
4228
4182
  * - Some devices like mobile phones are limited to 4k texture resolution
4229
- * - So with 16x16 tiles this limits layers to 256x256 on mobile devices
4183
+ * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
4230
4184
  * @extends EngineObject
4231
4185
  * @example
4232
- * // create tile collision and visible tile layer
4233
- * initTileCollision(vec2(200,100));
4234
- * const tileLayer = new TileLayer();
4186
+ * const tileLayer = new TileLayer(vec2(), vec2(200,100));
4235
4187
  */
4236
4188
  class TileLayer extends EngineObject
4237
4189
  {
4238
4190
  /** Create a tile layer object
4239
- * @param {Vector2} [position=(0,0)] - World space position
4240
- * @param {Vector2} [size=tileCollisionSize] - World space size
4241
- * @param {TileInfo} [tileInfo] - Tile info for layer
4242
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
4243
- * @param {number} [renderOrder] - Objects are sorted by renderOrder
4191
+ * @param {Vector2} [position=(0,0)] - World space position
4192
+ * @param {Vector2} [size=(1,1)] - World space size
4193
+ * @param {TileInfo} [tileInfo] - Tile info for layer
4194
+ * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
4195
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
4244
4196
  */
4245
- constructor(position, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderOrder=0)
4197
+ constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0)
4246
4198
  {
4247
4199
  super(position, size, tileInfo, 0, undefined, renderOrder);
4248
4200
 
@@ -4254,6 +4206,10 @@ class TileLayer extends EngineObject
4254
4206
  this.scale = scale;
4255
4207
  /** @property {boolean} - If true this layer will render to overlay canvas and appear above all objects */
4256
4208
  this.isOverlay = false;
4209
+ // set no friction by default, applied friction is max of both objects
4210
+ this.friction = 0;
4211
+ // set no elasticity by default, applied elasticity is max of both objects
4212
+ this.elasticity = 0;
4257
4213
 
4258
4214
  // init tile data
4259
4215
  this.data = [];
@@ -4450,13 +4406,151 @@ class TileLayer extends EngineObject
4450
4406
  * @param {number} [angle=0] */
4451
4407
  drawRect(pos, size, color, angle)
4452
4408
  { this.drawTile(pos, size, undefined, color, angle); }
4409
+ }
4410
+
4411
+ ///////////////////////////////////////////////////////////////////////////////
4412
+ /**
4413
+ * Tile Collision Layer - a tile layer with collision
4414
+ * - adds collision data and functions to TileLayer
4415
+ * - there can be multiple tile collision layers
4416
+ * - tile collison layers should not overlap each other
4417
+ * @extends TileLayer
4418
+ */
4419
+ class TileCollisionLayer extends TileLayer
4420
+ {
4421
+ /** Create a tile layer object
4422
+ * @param {Vector2} [position=(0,0)] - World space position
4423
+ * @param {Vector2} [size=(0,0)] - World space size
4424
+ * @param {TileInfo} [tileInfo] - Tile info for layer
4425
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
4426
+ */
4427
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
4428
+ {
4429
+ const scale = vec2(1); // collision layers are not scaled
4430
+ super(position, size.floor(), tileInfo, scale, renderOrder);
4431
+
4432
+ /** @property {Array<number>} - The tile collision grid */
4433
+ this.collisionData = [];
4434
+ this.initCollision(this.size);
4435
+
4436
+ // keep track of all collision layers
4437
+ tileCollisionLayers.push(this);
4438
+ }
4439
+
4440
+ /** Destroy this collision layer */
4441
+ destroy()
4442
+ {
4443
+ if (this.destroyed)
4444
+ return;
4445
+
4446
+ // remove from collision layers array and destroy
4447
+ const index = tileCollisionLayers.indexOf(this);
4448
+ ASSERT(index >= 0, 'tile collision layer not found in array');
4449
+ tileCollisionLayers.splice(index, 1);
4450
+ super.destroy();
4451
+ }
4452
+
4453
+ /** Clear and initialize tile collision to new size
4454
+ * @param {Vector2} size - width and height of tile collision 2d grid */
4455
+ initCollision(size)
4456
+ {
4457
+ this.size = size.floor();
4458
+ this.collisionData = [];
4459
+ this.collisionData.length = size.area();
4460
+ this.collisionData.fill(0);
4461
+ }
4462
+
4463
+ /** Set tile collision data for a given cell in the grid
4464
+ * @param {Vector2} pos
4465
+ * @param {number} [data] */
4466
+ setCollisionData(pos, data=1)
4467
+ {
4468
+ const i = (pos.y|0)*this.size.x + pos.x|0;
4469
+ pos.arrayCheck(this.size) && (this.collisionData[i] = data);
4470
+ }
4471
+
4472
+ /** Get tile collision data for a given cell in the grid
4473
+ * @param {Vector2} pos
4474
+ * @return {number} */
4475
+ getCollisionData(pos)
4476
+ {
4477
+ const i = (pos.y|0)*this.size.x + pos.x|0;
4478
+ return pos.arrayCheck(this.size) ? this.collisionData[i] : 0;
4479
+ }
4480
+
4481
+ /** Check if collision with another object should occur
4482
+ * @param {Vector2} pos
4483
+ * @param {Vector2} [size=(0,0)]
4484
+ * @param {EngineObject} [object]
4485
+ * @return {boolean} */
4486
+ collisionTest(pos, size=vec2(), object)
4487
+ {
4488
+ const minX = max(pos.x - size.x/2|0, 0);
4489
+ const minY = max(pos.y - size.y/2|0, 0);
4490
+ const maxX = min(pos.x + size.x/2, this.size.x);
4491
+ const maxY = min(pos.y + size.y/2, this.size.y);
4492
+ for (let y = minY; y < maxY; ++y)
4493
+ for (let x = minX; x < maxX; ++x)
4494
+ {
4495
+ // check if the object should collide with this tile
4496
+ const tileData = this.collisionData[y*this.size.x+x];
4497
+ if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
4498
+ return true;
4499
+ }
4500
+ return false;
4501
+ }
4502
+
4503
+ /** Return the center of first tile hit, undefined if nothing was hit.
4504
+ * This does not return the exact intersection, but the center of the tile hit.
4505
+ * @param {Vector2} posStart
4506
+ * @param {Vector2} posEnd
4507
+ * @param {EngineObject} [object]
4508
+ * @return {Vector2} */
4509
+ collisionRaycast(posStart, posEnd, object)
4510
+ {
4511
+ // test if a ray collides with tiles from start to end
4512
+ // todo: a way to get the exact hit point, it must still be inside the hit tile
4513
+ const delta = posEnd.subtract(posStart);
4514
+ const totalLength = delta.length();
4515
+ const normalizedDelta = delta.normalize();
4516
+ const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
4517
+ const flooredPosStart = posStart.floor();
4518
+
4519
+ // setup iteration variables
4520
+ let pos = flooredPosStart;
4521
+ let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
4522
+ let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
4523
+
4524
+ // use line drawing algorithm to test for collisions
4525
+ while (true)
4526
+ {
4527
+ // check for tile collision
4528
+ const tileData = this.getCollisionData(pos);
4529
+ if (tileData && (!object || object.collideWithTile(tileData, pos)))
4530
+ {
4531
+ debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
4532
+ debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
4533
+ return pos.add(vec2(.5));
4534
+ }
4535
+
4536
+ // check if past the end
4537
+ if (xi > totalLength && yi > totalLength)
4538
+ break;
4539
+
4540
+ // get coordinates of next tile to check
4541
+ if (xi > yi)
4542
+ pos.y += sign(delta.y), yi += unit.y;
4543
+ else
4544
+ pos.x += sign(delta.x), xi += unit.x;
4545
+ }
4546
+
4547
+ debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
4548
+ }
4453
4549
  }
4454
4550
  /**
4455
4551
  * LittleJS Particle System
4456
4552
  */
4457
4553
 
4458
-
4459
-
4460
4554
  /**
4461
4555
  * Particle Emitter - Spawns particles with the given settings
4462
4556
  * @extends EngineObject
@@ -4801,8 +4895,6 @@ class Particle extends EngineObject
4801
4895
  * @namespace Medals
4802
4896
  */
4803
4897
 
4804
-
4805
-
4806
4898
  /** List of all medals
4807
4899
  * @type {Object}
4808
4900
  * @memberof Medals */
@@ -4990,8 +5082,6 @@ class Medal
4990
5082
  * @namespace WebGL
4991
5083
  */
4992
5084
 
4993
-
4994
-
4995
5085
  /** The WebGL canvas which appears above the main canvas and below the overlay canvas
4996
5086
  * @type {HTMLCanvasElement}
4997
5087
  * @memberof WebGL */
@@ -5190,7 +5280,19 @@ function glCreateTexture(image)
5190
5280
  const texture = glContext.createTexture();
5191
5281
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
5192
5282
  if (image && image.width)
5283
+ {
5193
5284
  glSetTextureData(texture, image);
5285
+
5286
+ const isPowerOfTwo = (value)=> !(value & (value - 1));
5287
+ if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
5288
+ {
5289
+ // use mipmap filtering
5290
+ glContext.generateMipmap(glContext.TEXTURE_2D);
5291
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR_MIPMAP_LINEAR);
5292
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, glContext.LINEAR);
5293
+ return texture;
5294
+ }
5295
+ }
5194
5296
  else
5195
5297
  {
5196
5298
  // create a white texture
@@ -5198,7 +5300,7 @@ function glCreateTexture(image)
5198
5300
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
5199
5301
  }
5200
5302
 
5201
- // use point filtering for pixelated rendering
5303
+ // set texture filtering
5202
5304
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
5203
5305
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
5204
5306
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, filter);
@@ -5314,8 +5416,6 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=-1, rgba
5314
5416
  * @namespace Engine
5315
5417
  */
5316
5418
 
5317
-
5318
-
5319
5419
  /** Name of engine
5320
5420
  * @type {string}
5321
5421
  * @default
@@ -5326,7 +5426,7 @@ const engineName = 'LittleJS';
5326
5426
  * @type {string}
5327
5427
  * @default
5328
5428
  * @memberof Engine */
5329
- const engineVersion = '1.11.17';
5429
+ const engineVersion = '1.12.6';
5330
5430
 
5331
5431
  /** Frames per second to update
5332
5432
  * @type {number}
@@ -5408,7 +5508,7 @@ function engineAddPlugin(updateFunction, renderFunction)
5408
5508
  * @param {Array<string>} [imageSources=[]] - List of images to load
5409
5509
  * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
5410
5510
  * @memberof Engine */
5411
- function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
5511
+ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
5412
5512
  {
5413
5513
  ASSERT(!mainContext, 'engine already initialized');
5414
5514
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
@@ -5565,16 +5665,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5565
5665
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5566
5666
  }
5567
5667
 
5568
- function startEngine()
5668
+ // wait for gameInit to load
5669
+ async function startEngine()
5569
5670
  {
5570
- new Promise((resolve) => resolve(gameInit())).then(engineUpdate);
5671
+ await gameInit();
5672
+ engineUpdate();
5571
5673
  }
5572
-
5573
5674
  if (headlessMode)
5574
- {
5575
- startEngine();
5576
- return;
5577
- }
5675
+ return startEngine();
5578
5676
 
5579
5677
  // setup html
5580
5678
  const styleRoot =
@@ -5650,8 +5748,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5650
5748
  }));
5651
5749
  }
5652
5750
 
5653
- // load all of the images
5654
- Promise.all(promises).then(startEngine);
5751
+ // wait for all the promises to finish
5752
+ await Promise.all(promises);
5753
+ return startEngine();
5655
5754
  }
5656
5755
 
5657
5756
  /** Update each engine object, remove destroyed objects, and update time
@@ -5920,3 +6019,2777 @@ function drawEngineSplashScreen(t)
5920
6019
  x.restore();
5921
6020
  }
5922
6021
 
6022
+ /**
6023
+ * LittleJS Newgrounds API
6024
+ * - NewgroundsMedal extends Medal with Newgrounds API functionality
6025
+ * - Call new NewgroundsPlugin() to setup Newgrounds
6026
+ * - Uses CryptoJS for encryption if optional cipher is provided
6027
+ * - Keeps connection alive and logs views
6028
+ * - Functions to interact with scoreboards
6029
+ * - Functions to unlock medals
6030
+ */
6031
+
6032
+ /** Global Newgrounds object
6033
+ * @type {NewgroundsPlugin}
6034
+ * @memberof Medal */
6035
+ let newgrounds;
6036
+
6037
+ ///////////////////////////////////////////////////////////////////////////////
6038
+ /**
6039
+ * Newgrounds medal auto unlocks in newgrounds API
6040
+ * @extends Medal
6041
+ */
6042
+ class NewgroundsMedal extends Medal
6043
+ {
6044
+ /** Create a newgrounds medal object and adds it to the list of medals
6045
+ * @param {Number} id - The unique identifier of the medal
6046
+ * @param {String} name - Name of the medal
6047
+ * @param {String} [description] - Description of the medal
6048
+ * @param {String} [icon] - Icon for the medal
6049
+ * @param {String} [src] - Image location for the medal
6050
+ */
6051
+ constructor(id, name, description, icon, src)
6052
+ { super(id, name, description, icon, src); }
6053
+
6054
+ /** Unlocks a medal if not already unlocked */
6055
+ unlock()
6056
+ {
6057
+ super.unlock();
6058
+ newgrounds && newgrounds.unlockMedal(this.id);
6059
+ }
6060
+ }
6061
+
6062
+ ///////////////////////////////////////////////////////////////////////////////
6063
+ /**
6064
+ * Newgrounds API object
6065
+ */
6066
+ class NewgroundsPlugin
6067
+ {
6068
+ /** Create the global newgrounds object
6069
+ * @param {string} app_id - The newgrounds App ID
6070
+ * @param {string} [cipher] - The encryption Key (AES-128/Base64)
6071
+ * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
6072
+ * @example
6073
+ * // create the newgrounds object, replace the app id with your own
6074
+ * const app_id = 'your_app_id_here';
6075
+ * new NewgroundsPlugin(app_id);
6076
+ */
6077
+ constructor(app_id, cipher, cryptoJS)
6078
+ {
6079
+ ASSERT(!newgrounds, 'there can only be one newgrounds object');
6080
+ ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
6081
+
6082
+ newgrounds = this; // set global newgrounds object
6083
+ this.app_id = app_id;
6084
+ this.cipher = cipher;
6085
+ this.cryptoJS = cryptoJS;
6086
+ this.host = location ? location.hostname : '';
6087
+
6088
+ // get session id from url search params
6089
+ const url = new URL(location.href);
6090
+ this.session_id = url.searchParams.get('ngio_session_id');
6091
+
6092
+ if (!this.session_id)
6093
+ return; // only use newgrounds when logged in
6094
+
6095
+ // get medals
6096
+ const medalsResult = this.call('Medal.getList');
6097
+ this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
6098
+ debugMedals && console.log(this.medals);
6099
+ for (const newgroundsMedal of this.medals)
6100
+ {
6101
+ const medal = medals[newgroundsMedal['id']];
6102
+ if (medal)
6103
+ {
6104
+ // copy newgrounds medal data
6105
+ medal.image = new Image;
6106
+ medal.image.src = newgroundsMedal['icon'];
6107
+ medal.name = newgroundsMedal['name'];
6108
+ medal.description = newgroundsMedal['description'];
6109
+ medal.unlocked = newgroundsMedal['unlocked'];
6110
+ medal.difficulty = newgroundsMedal['difficulty'];
6111
+ medal.value = newgroundsMedal['value'];
6112
+
6113
+ if (medal.value) // add value to description
6114
+ medal.description = medal.description + ` (${ medal.value })`;
6115
+ }
6116
+ }
6117
+
6118
+ // get scoreboards
6119
+ const scoreboardResult = this.call('ScoreBoard.getBoards');
6120
+ this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
6121
+ debugMedals && console.log(this.scoreboards);
6122
+
6123
+ // keep the session alive with a ping every minute
6124
+ const keepAliveMS = 60 * 1e3;
6125
+ setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
6126
+ }
6127
+
6128
+ /** Send message to unlock a medal by id
6129
+ * @param {number} id - The medal id */
6130
+ unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
6131
+
6132
+ /** Send message to post score
6133
+ * @param {number} id - The scoreboard id
6134
+ * @param {number} value - The score value */
6135
+ postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
6136
+
6137
+ /** Get scores from a scoreboard
6138
+ * @param {number} id - The scoreboard id
6139
+ * @param {string} [user] - A user's id or name
6140
+ * @param {number} [social] - If true, only social scores will be loaded
6141
+ * @param {number} [skip] - Number of scores to skip before start
6142
+ * @param {number} [limit] - Number of scores to include in the list
6143
+ * @return {Object} - The response JSON object
6144
+ */
6145
+ getScores(id, user, social=0, skip=0, limit=10)
6146
+ { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
6147
+
6148
+ /** Send message to log a view */
6149
+ logView() { return this.call('App.logView', {'host':this.host}, true); }
6150
+
6151
+ /** Send a message to call a component of the Newgrounds API
6152
+ * @param {string} component - Name of the component
6153
+ * @param {Object} [parameters] - Parameters to use for call
6154
+ * @param {boolean} [async] - If true, don't wait for response before continuing
6155
+ * @return {Object} - The response JSON object
6156
+ */
6157
+ call(component, parameters, async=false)
6158
+ {
6159
+ const call = {'component':component, 'parameters':parameters};
6160
+ if (this.cipher)
6161
+ {
6162
+ // encrypt using AES-128 Base64 with cryptoJS
6163
+ const cryptoJS = this.cryptoJS;
6164
+ const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
6165
+ const iv = cryptoJS['lib']['WordArray']['random'](16);
6166
+ const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
6167
+ call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
6168
+ call['parameters'] = 0;
6169
+ }
6170
+
6171
+ // build the input object
6172
+ const input =
6173
+ {
6174
+ 'app_id': this.app_id,
6175
+ 'session_id': this.session_id,
6176
+ 'call': call
6177
+ };
6178
+
6179
+ // build post data
6180
+ const formData = new FormData();
6181
+ formData.append('input', JSON.stringify(input));
6182
+
6183
+ // send post data
6184
+ const xmlHttp = new XMLHttpRequest();
6185
+ const url = 'https://newgrounds.io/gateway_v3.php';
6186
+ xmlHttp.open('POST', url, !debugMedals && async);
6187
+ try { xmlHttp.send(formData); }
6188
+ catch(e)
6189
+ {
6190
+ debugMedals && console.log('newgrounds call failed', e);
6191
+ return;
6192
+ }
6193
+ debugMedals && console.log(xmlHttp.responseText);
6194
+ return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
6195
+ }
6196
+ }
6197
+
6198
+ /**
6199
+ * LittleJS Post Processing Plugin
6200
+ * - Supports shadertoy style post processing shaders
6201
+ * - call new new PostProcessPlugin() to setup post processing
6202
+ * - can be enabled to pass other canvases through a final shader
6203
+ */
6204
+
6205
+ ///////////////////////////////////////////////////////////////////////////////
6206
+
6207
+ /** Global Post Process plugin object
6208
+ * @type {PostProcessPlugin} */
6209
+ let postProcess;
6210
+
6211
+ /////////////////////////////////////////////////////////////////////////
6212
+ /**
6213
+ * UI System Global Object
6214
+ */
6215
+ class PostProcessPlugin
6216
+ {
6217
+ /** Create global post processing shader
6218
+ * @param {string} shaderCode
6219
+ * @param {boolean} [includeOverlay]
6220
+ * @example
6221
+ * // create the post process plugin object
6222
+ * new PostProcessPlugin(shaderCode);
6223
+ */
6224
+ constructor(shaderCode, includeOverlay=false)
6225
+ {
6226
+ ASSERT(!postProcess, 'Post process already initialized');
6227
+ postProcess = this;
6228
+
6229
+ if (headlessMode) return;
6230
+ if (!shaderCode) // default shader pass through
6231
+ shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
6232
+
6233
+ /** @property {WebGLProgram} - Shader for post processing */
6234
+ this.shader = glCreateProgram(
6235
+ '#version 300 es\n' + // specify GLSL ES version
6236
+ 'precision highp float;'+ // use highp for better accuracy
6237
+ 'in vec2 p;'+ // position
6238
+ 'void main(){'+ // shader entry point
6239
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
6240
+ '}' // end of shader
6241
+ ,
6242
+ '#version 300 es\n' + // specify GLSL ES version
6243
+ 'precision highp float;'+ // use highp for better accuracy
6244
+ 'uniform sampler2D iChannel0;'+ // input texture
6245
+ 'uniform vec3 iResolution;'+ // size of output texture
6246
+ 'uniform float iTime;'+ // time
6247
+ 'out vec4 c;'+ // out color
6248
+ '\n' + shaderCode + '\n'+ // insert custom shader code
6249
+ 'void main(){'+ // shader entry point
6250
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
6251
+ 'c.a=1.;'+ // always use full alpha
6252
+ '}' // end of shader
6253
+ );
6254
+
6255
+ /** @property {WebGLTexture} - Texture for post processing */
6256
+ this.texture = glCreateTexture();
6257
+
6258
+ /** @property {boolean} - Should overlay canvas be included in post processing */
6259
+ this.includeOverlay = includeOverlay;
6260
+
6261
+ // Render the post processing shader, called automatically by the engine
6262
+ engineAddPlugin(undefined, postProcessRender);
6263
+ function postProcessRender()
6264
+ {
6265
+ if (headlessMode) return;
6266
+
6267
+ // prepare to render post process shader
6268
+ if (glEnable)
6269
+ {
6270
+ glFlush(); // clear out the buffer
6271
+ mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
6272
+ }
6273
+ else
6274
+ {
6275
+ // set the viewport
6276
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
6277
+ }
6278
+
6279
+ if (postProcess.includeOverlay)
6280
+ {
6281
+ // copy overlay canvas so it will be included in post processing
6282
+ mainContext.drawImage(overlayCanvas, 0, 0);
6283
+ overlayCanvas.width |= 0;
6284
+ }
6285
+
6286
+ // setup shader program to draw one triangle
6287
+ glContext.useProgram(postProcess.shader);
6288
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6289
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
6290
+ glContext.disable(glContext.BLEND);
6291
+
6292
+ // set textures, pass in the 2d canvas and gl canvas in separate texture channels
6293
+ glContext.activeTexture(glContext.TEXTURE0);
6294
+ glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
6295
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
6296
+
6297
+ // set vertex position attribute
6298
+ const vertexByteStride = 8;
6299
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
6300
+ glContext.enableVertexAttribArray(pLocation);
6301
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
6302
+
6303
+ // set uniforms and draw
6304
+ const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
6305
+ glContext.uniform1i(uniformLocation('iChannel0'), 0);
6306
+ glContext.uniform1f(uniformLocation('iTime'), time);
6307
+ glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
6308
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
6309
+ }
6310
+ }
6311
+ }
6312
+
6313
+ /**
6314
+ * LittleJS ZzFXM Plugin
6315
+ */
6316
+
6317
+ /**
6318
+ * Music Object - Stores a zzfx music track for later use
6319
+ *
6320
+ * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
6321
+ * @example
6322
+ * // create some music
6323
+ * const music_example = new Music(
6324
+ * [
6325
+ * [ // instruments
6326
+ * [,0,400] // simple note
6327
+ * ],
6328
+ * [ // patterns
6329
+ * [ // pattern 1
6330
+ * [ // channel 0
6331
+ * 0, -1, // instrument 0, left speaker
6332
+ * 1, 0, 9, 1 // channel notes
6333
+ * ],
6334
+ * [ // channel 1
6335
+ * 0, 1, // instrument 0, right speaker
6336
+ * 0, 12, 17, -1 // channel notes
6337
+ * ]
6338
+ * ],
6339
+ * ],
6340
+ * [0, 0, 0, 0], // sequence, play pattern 0 four times
6341
+ * 90 // BPM
6342
+ * ]);
6343
+ *
6344
+ * // play the music
6345
+ * music_example.play();
6346
+ */
6347
+ class ZzFXMusic extends Sound
6348
+ {
6349
+ /** Create a music object and cache the zzfx music samples for later use
6350
+ * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
6351
+ */
6352
+ constructor(zzfxMusic)
6353
+ {
6354
+ super(undefined);
6355
+
6356
+ if (!soundEnable || headlessMode) return;
6357
+ this.randomness = 0;
6358
+ this.sampleChannels = zzfxM(...zzfxMusic);
6359
+ this.sampleRate = zzfxR;
6360
+ }
6361
+
6362
+ /** Play the music
6363
+ * @param {number} [volume=1] - How much to scale volume by
6364
+ * @param {boolean} [loop] - True if the music should loop
6365
+ * @return {AudioBufferSourceNode} - The audio source node
6366
+ */
6367
+ playMusic(volume, loop=false)
6368
+ { return super.play(undefined, volume, 1, 1, loop); }
6369
+ }
6370
+
6371
+ ///////////////////////////////////////////////////////////////////////////////
6372
+ // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
6373
+
6374
+ /** Generate samples for a ZzFM song with given parameters
6375
+ * @param {Array} instruments - Array of ZzFX sound parameters
6376
+ * @param {Array} patterns - Array of pattern data
6377
+ * @param {Array} sequence - Array of pattern indexes
6378
+ * @param {number} [BPM] - Playback speed of the song in BPM
6379
+ * @return {Array} - Left and right channel sample data */
6380
+ function zzfxM(instruments, patterns, sequence, BPM = 125)
6381
+ {
6382
+ let i, j, k;
6383
+ let instrumentParameters;
6384
+ let note;
6385
+ let sample;
6386
+ let patternChannel;
6387
+ let notFirstBeat;
6388
+ let stop;
6389
+ let instrument;
6390
+ let attenuation;
6391
+ let outSampleOffset;
6392
+ let isSequenceEnd;
6393
+ let sampleOffset = 0;
6394
+ let nextSampleOffset;
6395
+ let sampleBuffer = [];
6396
+ let leftChannelBuffer = [];
6397
+ let rightChannelBuffer = [];
6398
+ let channelIndex = 0;
6399
+ let panning = 0;
6400
+ let hasMore = 1;
6401
+ let sampleCache = {};
6402
+ let beatLength = zzfxR / BPM * 60 >> 2;
6403
+
6404
+ // for each channel in order until there are no more
6405
+ for (; hasMore; channelIndex++) {
6406
+
6407
+ // reset current values
6408
+ sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
6409
+
6410
+ // for each pattern in sequence
6411
+ sequence.forEach((patternIndex, sequenceIndex) => {
6412
+ // get pattern for current channel, use empty 1 note pattern if none found
6413
+ patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
6414
+
6415
+ // check if there are more channels
6416
+ hasMore |= patterns[patternIndex][channelIndex]&&1;
6417
+
6418
+ // get next offset, use the length of first channel
6419
+ nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
6420
+ // for each beat in pattern, plus one extra if end of sequence
6421
+ isSequenceEnd = sequenceIndex == sequence.length - 1;
6422
+ for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
6423
+
6424
+ // <channel-note>
6425
+ note = patternChannel[i];
6426
+
6427
+ // stop if end, different instrument or new note
6428
+ stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
6429
+ instrument != (patternChannel[0] || 0) || note | 0;
6430
+
6431
+ // fill buffer with samples for previous beat, most cpu intensive part
6432
+ for (j = 0; j < beatLength && notFirstBeat;
6433
+
6434
+ // fade off attenuation at end of beat if stopping note, prevents clicking
6435
+ j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
6436
+ ) {
6437
+ // copy sample to stereo buffers with panning
6438
+ sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
6439
+ leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
6440
+ rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
6441
+ }
6442
+
6443
+ // set up for next note
6444
+ if (note) {
6445
+ // set attenuation
6446
+ attenuation = note % 1;
6447
+ panning = patternChannel[1] || 0;
6448
+ if (note |= 0) {
6449
+ // get cached sample
6450
+ sampleBuffer = sampleCache[
6451
+ [
6452
+ instrument = patternChannel[sampleOffset = 0] || 0,
6453
+ note
6454
+ ]
6455
+ ] = sampleCache[[instrument, note]] || (
6456
+ // add sample to cache
6457
+ instrumentParameters = [...instruments[instrument]],
6458
+ instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
6459
+
6460
+ // allow negative values to stop notes
6461
+ note > 0 ? zzfxG(...instrumentParameters) : []
6462
+ );
6463
+ }
6464
+ }
6465
+ }
6466
+
6467
+ // update the sample offset
6468
+ outSampleOffset = nextSampleOffset;
6469
+ });
6470
+ }
6471
+
6472
+ return [leftChannelBuffer, rightChannelBuffer];
6473
+ }
6474
+
6475
+ /**
6476
+ * LittleJS User Interface Plugin
6477
+ * - call new UISystemPlugin() to setup the UI system
6478
+ * - Nested Menus
6479
+ * - Text
6480
+ * - Buttons
6481
+ * - Checkboxes
6482
+ * - Images
6483
+ */
6484
+
6485
+ ///////////////////////////////////////////////////////////////////////////////
6486
+
6487
+ /** Global UI system plugin object
6488
+ * @type {UISystemPlugin} */
6489
+ let uiSystem;
6490
+
6491
+ ///////////////////////////////////////////////////////////////////////////////
6492
+ /**
6493
+ * UI System Global Object
6494
+ */
6495
+ class UISystemPlugin
6496
+ {
6497
+ /** Create the global UI system object
6498
+ * @param {CanvasRenderingContext2D} [context]
6499
+ * @example
6500
+ * // create the ui plugin object
6501
+ * new UISystemPlugin;
6502
+ */
6503
+ constructor(context=overlayContext)
6504
+ {
6505
+ ASSERT(!uiSystem, 'UI system already initialized');
6506
+ uiSystem = this;
6507
+
6508
+ /** @property {Color} - Default fill color for UI elements */
6509
+ this.defaultColor = WHITE;
6510
+ /** @property {Color} - Default outline color for UI elements */
6511
+ this.defaultLineColor = BLACK;
6512
+ /** @property {Color} - Default text color for UI elements */
6513
+ this.defaultTextColor = BLACK;
6514
+ /** @property {Color} - Default button color for UI elements */
6515
+ this.defaultButtonColor = hsl(0,0,.5);
6516
+ /** @property {Color} - Default hover color for UI elements */
6517
+ this.defaultHoverColor = hsl(0,0,.7);
6518
+ /** @property {number} - Default line width for UI elements */
6519
+ this.defaultLineWidth = 4;
6520
+ /** @property {string} - Default font for UI elements */
6521
+ this.defaultFont = 'arial';
6522
+ /** @property {Array<UIObject>} - List of all UI elements */
6523
+ this.uiObjects = [];
6524
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
6525
+ this.uiContext = context;
6526
+
6527
+ engineAddPlugin(uiUpdate, uiRender);
6528
+
6529
+ // setup recursive update and render
6530
+ function uiUpdate()
6531
+ {
6532
+ function updateObject(o)
6533
+ {
6534
+ if (!o.visible)
6535
+ return;
6536
+ if (o.parent)
6537
+ o.pos = o.localPos.add(o.parent.pos);
6538
+ o.update();
6539
+ for(const c of o.children)
6540
+ updateObject(c);
6541
+ }
6542
+ uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
6543
+ }
6544
+ function uiRender()
6545
+ {
6546
+ function renderObject(o)
6547
+ {
6548
+ if (!o.visible)
6549
+ return;
6550
+ if (o.parent)
6551
+ o.pos = o.localPos.add(o.parent.pos);
6552
+ o.render();
6553
+ for(const c of o.children)
6554
+ renderObject(c);
6555
+ }
6556
+ uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
6557
+ }
6558
+ }
6559
+
6560
+ /** Draw a rectangle to the UI context
6561
+ * @param {Vector2} pos
6562
+ * @param {Vector2} size
6563
+ * @param {Color} [color=uiSystem.defaultColor]
6564
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6565
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6566
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6567
+ {
6568
+ uiSystem.uiContext.fillStyle = color.toString();
6569
+ uiSystem.uiContext.beginPath();
6570
+ uiSystem.uiContext.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
6571
+ uiSystem.uiContext.fill();
6572
+ if (lineWidth)
6573
+ {
6574
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6575
+ uiSystem.uiContext.lineWidth = lineWidth;
6576
+ uiSystem.uiContext.stroke();
6577
+ }
6578
+ }
6579
+
6580
+ /** Draw a line to the UI context
6581
+ * @param {Vector2} posA
6582
+ * @param {Vector2} posB
6583
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6584
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6585
+ drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6586
+ {
6587
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6588
+ uiSystem.uiContext.lineWidth = lineWidth;
6589
+ uiSystem.uiContext.beginPath();
6590
+ uiSystem.uiContext.lineTo(posA.x, posA.y);
6591
+ uiSystem.uiContext.lineTo(posB.x, posB.y);
6592
+ uiSystem.uiContext.stroke();
6593
+ }
6594
+
6595
+ /** Draw a tile to the UI context
6596
+ * @param {Vector2} pos
6597
+ * @param {Vector2} size
6598
+ * @param {TileInfo} tileInfo
6599
+ * @param {Color} [color=uiSystem.defaultColor]
6600
+ * @param {number} [angle]
6601
+ * @param {boolean} [mirror] */
6602
+ drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false)
6603
+ {
6604
+ drawTile(pos, size, tileInfo, color, angle, mirror, BLACK, false, true, uiSystem.uiContext);
6605
+ }
6606
+
6607
+ /** Draw text to the UI context
6608
+ * @param {string} text
6609
+ * @param {Vector2} pos
6610
+ * @param {Vector2} size
6611
+ * @param {Color} [color=uiSystem.defaultColor]
6612
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6613
+ * @param {Color} [lineColor=uiSystem.defaultLineColor]
6614
+ * @param {string} [align]
6615
+ * @param {string} [font=uiSystem.defaultFont] */
6616
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont)
6617
+ {
6618
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, size.x, uiSystem.uiContext);
6619
+ }
6620
+ }
6621
+
6622
+ ///////////////////////////////////////////////////////////////////////////////
6623
+ /**
6624
+ * UI Object - Base level object for all UI elements
6625
+ */
6626
+ class UIObject
6627
+ {
6628
+ /** Create a UIObject
6629
+ * @param {Vector2} [pos=(0,0)]
6630
+ * @param {Vector2} [size=(1,1)]
6631
+ */
6632
+ constructor(pos=vec2(), size=vec2())
6633
+ {
6634
+ /** @property {Vector2} - Local position of the object */
6635
+ this.localPos = pos.copy();
6636
+ /** @property {Vector2} - Screen space position of the object */
6637
+ this.pos = pos.copy();
6638
+ /** @property {Vector2} - Screen space size of the object */
6639
+ this.size = size.copy();
6640
+ /** @property {Color} */
6641
+ this.color = uiSystem.defaultColor;
6642
+ /** @property {Color} */
6643
+ this.lineColor = uiSystem.defaultLineColor;
6644
+ /** @property {Color} */
6645
+ this.textColor = uiSystem.defaultTextColor;
6646
+ /** @property {Color} */
6647
+ this.hoverColor = uiSystem.defaultHoverColor;
6648
+ /** @property {number} */
6649
+ this.lineWidth = uiSystem.defaultLineWidth;
6650
+ /** @property {string} */
6651
+ this.font = uiSystem.defaultFont;
6652
+ /** @property {number} - override for text height */
6653
+ this.textHeight = undefined;
6654
+ /** @property {boolean} */
6655
+ this.visible = true;
6656
+ /** @property {Array<UIObject>} */
6657
+ this.children = [];
6658
+ /** @property {UIObject} */
6659
+ this.parent = undefined;
6660
+ uiSystem.uiObjects.push(this);
6661
+ }
6662
+
6663
+ /** Add a child UIObject to this object
6664
+ * @param {UIObject} child
6665
+ */
6666
+ addChild(child)
6667
+ {
6668
+ ASSERT(!child.parent && !this.children.includes(child));
6669
+ this.children.push(child);
6670
+ child.parent = this;
6671
+ }
6672
+
6673
+ /** Remove a child UIObject from this object
6674
+ * @param {UIObject} child
6675
+ */
6676
+ removeChild(child)
6677
+ {
6678
+ ASSERT(child.parent == this && this.children.includes(child));
6679
+ this.children.splice(this.children.indexOf(child), 1);
6680
+ child.parent = undefined;
6681
+ }
6682
+
6683
+ /** Update the object, called automatically by plugin once each frame */
6684
+ update()
6685
+ {
6686
+ // track mouse input
6687
+ const mouseWasOver = this.mouseIsOver;
6688
+ const mouseDown = mouseIsDown(0);
6689
+ if (!mouseDown || isTouchDevice)
6690
+ {
6691
+ this.mouseIsOver = isOverlapping(this.pos, this.size, mousePosScreen);
6692
+ if (!mouseDown && isTouchDevice)
6693
+ this.mouseIsOver = false;
6694
+ if (this.mouseIsOver && !mouseWasOver)
6695
+ this.onEnter();
6696
+ if (!this.mouseIsOver && mouseWasOver)
6697
+ this.onLeave();
6698
+ }
6699
+ if (mouseWasPressed(0) && this.mouseIsOver)
6700
+ {
6701
+ this.mouseIsHeld = true;
6702
+ this.onPress();
6703
+ if (isTouchDevice)
6704
+ this.mouseIsOver = false;
6705
+ }
6706
+ else if (this.mouseIsHeld && !mouseDown)
6707
+ {
6708
+ this.mouseIsHeld = false;
6709
+ this.onRelease();
6710
+ }
6711
+ }
6712
+
6713
+ /** Render the object, called automatically by plugin once each frame */
6714
+ render()
6715
+ {
6716
+ if (this.size.x && this.size.y)
6717
+ uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor);
6718
+ }
6719
+
6720
+ /** Called when the mouse enters the object */
6721
+ onEnter() {}
6722
+
6723
+ /** Called when the mouse leaves the object */
6724
+ onLeave() {}
6725
+
6726
+ /** Called when the mouse is pressed while over the object */
6727
+ onPress() {}
6728
+
6729
+ /** Called when the mouse is released while over the object */
6730
+ onRelease() {}
6731
+
6732
+ /** Called when the state of this object changes */
6733
+ onChange() {}
6734
+ }
6735
+
6736
+ ///////////////////////////////////////////////////////////////////////////////
6737
+ /**
6738
+ * UIText - A UI object that displays text
6739
+ * @extends UIObject
6740
+ */
6741
+ class UIText extends UIObject
6742
+ {
6743
+ /** Create a UIText object
6744
+ * @param {Vector2} [pos]
6745
+ * @param {Vector2} [size]
6746
+ * @param {string} [text]
6747
+ * @param {string} [align]
6748
+ * @param {string} [font=uiSystem.defaultFont]
6749
+ */
6750
+ constructor(pos, size, text='', align='center', font=uiSystem.defaultFont)
6751
+ {
6752
+ super(pos, size);
6753
+
6754
+ /** @property {string} */
6755
+ this.text = text;
6756
+ /** @property {string} */
6757
+ this.align = align;
6758
+
6759
+ this.font = font; // set font
6760
+ this.lineWidth = 0; // set text to not be outlined by default
6761
+ }
6762
+ render()
6763
+ {
6764
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y);
6765
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
6766
+ }
6767
+ }
6768
+
6769
+ ///////////////////////////////////////////////////////////////////////////////
6770
+ /**
6771
+ * UITile - A UI object that displays a tile image
6772
+ * @extends UIObject
6773
+ */
6774
+ class UITile extends UIObject
6775
+ {
6776
+ /** Create a UITile object
6777
+ * @param {Vector2} [pos]
6778
+ * @param {Vector2} [size]
6779
+ * @param {TileInfo} [tileInfo]
6780
+ * @param {Color} [color=WHITE]
6781
+ * @param {number} [angle]
6782
+ * @param {boolean} [mirror]
6783
+ */
6784
+ constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
6785
+ {
6786
+ super(pos, size);
6787
+
6788
+ /** @property {TileInfo} - Tile image to use */
6789
+ this.tileInfo = tileInfo;
6790
+ /** @property {number} - Angle to rotate in radians */
6791
+ this.angle = angle;
6792
+ /** @property {boolean} - Should it be mirrored? */
6793
+ this.mirror = mirror;
6794
+ this.color = color;
6795
+ }
6796
+ render()
6797
+ {
6798
+ uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
6799
+ }
6800
+ }
6801
+
6802
+ ///////////////////////////////////////////////////////////////////////////////
6803
+ /**
6804
+ * UIButton - A UI object that acts as a button
6805
+ * @extends UIObject
6806
+ */
6807
+ class UIButton extends UIObject
6808
+ {
6809
+ /** Create a UIButton object
6810
+ * @param {Vector2} [pos]
6811
+ * @param {Vector2} [size]
6812
+ * @param {string} [text]
6813
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6814
+ */
6815
+ constructor(pos, size, text='', color=uiSystem.defaultButtonColor)
6816
+ {
6817
+ super(pos, size);
6818
+
6819
+ /** @property {string} */
6820
+ this.text = text;
6821
+ this.color = color;
6822
+ }
6823
+ render()
6824
+ {
6825
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
6826
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6827
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
6828
+
6829
+ const textScale = .8; // scale text to fit in button
6830
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
6831
+ uiSystem.drawText(this.text, this.pos, textSize,
6832
+ this.textColor, 0, undefined, this.align, this.font);
6833
+ }
6834
+ }
6835
+
6836
+ ///////////////////////////////////////////////////////////////////////////////
6837
+ /**
6838
+ * UICheckbox - A UI object that acts as a checkbox
6839
+ * @extends UIObject
6840
+ */
6841
+ class UICheckbox extends UIObject
6842
+ {
6843
+ /** Create a UICheckbox object
6844
+ * @param {Vector2} [pos]
6845
+ * @param {Vector2} [size]
6846
+ * @param {boolean} [checked]
6847
+ */
6848
+ constructor(pos, size, checked=false)
6849
+ {
6850
+ super(pos, size);
6851
+
6852
+ /** @property {boolean} */
6853
+ this.checked = checked;
6854
+ }
6855
+ onPress()
6856
+ {
6857
+ this.checked = !this.checked;
6858
+ this.onChange();
6859
+ }
6860
+ render()
6861
+ {
6862
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6863
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor);
6864
+ if (this.checked)
6865
+ {
6866
+ // draw an X if checked
6867
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor);
6868
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor);
6869
+ }
6870
+ }
6871
+ }
6872
+
6873
+ ///////////////////////////////////////////////////////////////////////////////
6874
+ /**
6875
+ * UIScrollbar - A UI object that acts as a scrollbar
6876
+ * @extends UIObject
6877
+ */
6878
+ class UIScrollbar extends UIObject
6879
+ {
6880
+ /** Create a UIScrollbar object
6881
+ * @param {Vector2} [pos]
6882
+ * @param {Vector2} [size]
6883
+ * @param {number} [value]
6884
+ * @param {string} [text]
6885
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6886
+ * @param {Color} [handleColor=WHITE]
6887
+ */
6888
+ constructor(pos, size, value=.5, text='', color=uiSystem.defaultButtonColor, handleColor=WHITE)
6889
+ {
6890
+ super(pos, size);
6891
+
6892
+ /** @property {number} */
6893
+ this.value = value;
6894
+ /** @property {string} */
6895
+ this.text = text;
6896
+ this.color = color;
6897
+ this.handleColor = handleColor;
6898
+ }
6899
+ update()
6900
+ {
6901
+ super.update();
6902
+ if (this.mouseIsHeld)
6903
+ {
6904
+ const handleSize = vec2(this.size.y);
6905
+ const handleWidth = this.size.x - handleSize.x;
6906
+ const p1 = this.pos.x - handleWidth/2;
6907
+ const p2 = this.pos.x + handleWidth/2;
6908
+ const oldValue = this.value;
6909
+ this.value = percent(mousePosScreen.x, p1, p2);
6910
+ this.value == oldValue || this.onChange();
6911
+ }
6912
+ }
6913
+ render()
6914
+ {
6915
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
6916
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6917
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
6918
+
6919
+ const handleSize = vec2(this.size.y);
6920
+ const handleWidth = this.size.x - handleSize.x;
6921
+ const p1 = this.pos.x - handleWidth/2;
6922
+ const p2 = this.pos.x + handleWidth/2;
6923
+ const handlePos = vec2(lerp(this.value, p1, p2), this.pos.y);
6924
+ const barColor = this.mouseIsHeld ? this.color : this.handleColor;
6925
+ uiSystem.drawRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor);
6926
+
6927
+ const textScale = .8; // scale text to fit in scrollbar
6928
+ const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
6929
+ uiSystem.drawText(this.text, this.pos, textSize,
6930
+ this.textColor, 0, undefined, this.align, this.font);
6931
+ }
6932
+ }
6933
+
6934
+ /**
6935
+ * LittleJS Box2D Physics Plugin
6936
+ * - Box2dObject extends EngineObject with Box2D physics
6937
+ * - Call box2dInit() before engineInit() to enable
6938
+ * - You will also need to include box2d.wasm.js
6939
+ * - Uses a super fast web assembly port of Box2D
6940
+ * - More info: https://github.com/kripken/box2d.js
6941
+ * - Functions to create polygon, circle, and edge shapes
6942
+ * - Contact begin and end callbacks
6943
+ * - Wraps b2Vec2 type to/from Vector2
6944
+ * - Raycasting and querying
6945
+ * - Every type of joint
6946
+ * - Debug physics drawing
6947
+ * @namespace Box2D
6948
+ */
6949
+
6950
+ /** Global Box2d Plugin object
6951
+ * @type {Box2dPlugin}
6952
+ * @memberof Box2D */
6953
+ let box2d;
6954
+
6955
+ /** Enable Box2D debug drawing
6956
+ * @type {boolean}
6957
+ * @default
6958
+ * @memberof Box2D */
6959
+ let box2dDebug = false;
6960
+
6961
+ /** Enable Box2D debug drawing
6962
+ * @param {boolean} enable
6963
+ * @memberof Box2D */
6964
+ function box2dSetDebug(enable) { box2dDebug = enable; }
6965
+
6966
+ ///////////////////////////////////////////////////////////////////////////////
6967
+ /**
6968
+ * Box2D Object - extend with your own custom physics objects
6969
+ * - A LittleJS object with Box2D physics
6970
+ * - Each object has a Box2D body which can have multiple fixtures and joints
6971
+ * - Provides interface for Box2D body and fixture functions
6972
+ * @extends EngineObject
6973
+ */
6974
+ class Box2dObject extends EngineObject
6975
+ {
6976
+ /** Create a LittleJS object with Box2d physics
6977
+ * @param {Vector2} [pos]
6978
+ * @param {Vector2} [size]
6979
+ * @param {TileInfo} [tileInfo]
6980
+ * @param {number} [angle]
6981
+ * @param {Color} [color]
6982
+ * @param {number} [bodyType]
6983
+ * @param {number} [renderOrder] */
6984
+ constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
6985
+ {
6986
+ super(pos, size, tileInfo, angle, color, renderOrder);
6987
+
6988
+ // create physics body
6989
+ const bodyDef = new box2d.instance.b2BodyDef();
6990
+ bodyDef.set_type(bodyType);
6991
+ bodyDef.set_position(box2d.vec2dTo(pos));
6992
+ bodyDef.set_angle(-angle);
6993
+ this.body = box2d.world.CreateBody(bodyDef);
6994
+ this.body.object = this;
6995
+ this.outlineColor = BLACK;
6996
+ }
6997
+
6998
+ /** Destroy this object and it's physics body */
6999
+ destroy()
7000
+ {
7001
+ // destroy physics body, fixtures, and joints
7002
+ this.body && box2d.world.DestroyBody(this.body);
7003
+ this.body = 0;
7004
+ super.destroy();
7005
+ }
7006
+
7007
+ /** Copy box2d update sim data */
7008
+ update()
7009
+ {
7010
+ // use box2d physics update
7011
+ this.pos = box2d.vec2From(this.body.GetPosition());
7012
+ this.angle = -this.body.GetAngle();
7013
+ }
7014
+
7015
+ /** Render the object, uses box2d drawing if no tile info exists */
7016
+ render()
7017
+ {
7018
+ // use default render or draw fixtures
7019
+ if (this.tileInfo)
7020
+ super.render();
7021
+ else
7022
+ this.drawFixtures(this.color, this.outlineColor, this.lineWidth, mainContext);
7023
+ }
7024
+
7025
+ /** Render debug info */
7026
+ renderDebugInfo()
7027
+ {
7028
+ const isAsleep = !this.getIsAwake();
7029
+ const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
7030
+ const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
7031
+ this.drawFixtures(color);
7032
+ }
7033
+
7034
+ /** Draws all this object's fixtures
7035
+ * @param {Color} [color]
7036
+ * @param {Color} [outlineColor]
7037
+ * @param {number} [lineWidth]
7038
+ * @param {CanvasRenderingContext2D} [context] */
7039
+ drawFixtures(color=WHITE, outlineColor, lineWidth=.1, context)
7040
+ {
7041
+ this.getFixtureList().forEach(fixture=>
7042
+ box2d.drawFixture(fixture, this.pos, this.angle, color, outlineColor, lineWidth, context));
7043
+ }
7044
+
7045
+ ///////////////////////////////////////////////////////////////////////////////
7046
+ // physics contact callbacks
7047
+
7048
+ /** Called when a contact begins
7049
+ * @param {Box2dObject} otherObject */
7050
+ beginContact(otherObject) {}
7051
+
7052
+ /** Called when a contact ends
7053
+ * @param {Box2dObject} otherObject */
7054
+ endContact(otherObject) {}
7055
+
7056
+ ///////////////////////////////////////////////////////////////////////////////
7057
+ // physics fixtures and shapes
7058
+
7059
+ /** Add a shape fixture to the body
7060
+ * @param {Object} shape
7061
+ * @param {number} [density]
7062
+ * @param {number} [friction]
7063
+ * @param {number} [restitution]
7064
+ * @param {boolean} [isSensor] */
7065
+ addShape(shape, density=1, friction=1, restitution=0, isSensor=false)
7066
+ {
7067
+ const fd = new box2d.instance.b2FixtureDef();
7068
+ fd.set_shape(shape);
7069
+ fd.set_density(density);
7070
+ fd.set_friction(friction);
7071
+ fd.set_restitution(restitution);
7072
+ fd.set_isSensor(isSensor);
7073
+ return this.body.CreateFixture(fd);
7074
+ }
7075
+
7076
+ /** Add a box shape to the body
7077
+ * @param {Vector2} [size]
7078
+ * @param {Vector2} [offset]
7079
+ * @param {number} [angle]
7080
+ * @param {number} [density]
7081
+ * @param {number} [friction]
7082
+ * @param {number} [restitution]
7083
+ * @param {boolean} [isSensor] */
7084
+ addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
7085
+ {
7086
+ const shape = new box2d.instance.b2PolygonShape();
7087
+ shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
7088
+ return this.addShape(shape, density, friction, restitution, isSensor);
7089
+ }
7090
+
7091
+ /** Add a polygon shape to the body
7092
+ * @param {Array<Vector2>} points
7093
+ * @param {number} [density]
7094
+ * @param {number} [friction]
7095
+ * @param {number} [restitution]
7096
+ * @param {boolean} [isSensor] */
7097
+ addPoly(points, density, friction, restitution, isSensor)
7098
+ {
7099
+ function box2dCreatePolygonShape(points)
7100
+ {
7101
+ function box2dCreatePointList(points)
7102
+ {
7103
+ const buffer = box2d.instance._malloc(points.length * 8);
7104
+ for (let i=0, offset=0; i<points.length; ++i)
7105
+ {
7106
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x;
7107
+ offset += 4;
7108
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
7109
+ offset += 4;
7110
+ }
7111
+ return box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
7112
+ }
7113
+
7114
+ ASSERT(3 <= points.length && points.length <= 8);
7115
+ const shape = new box2d.instance.b2PolygonShape();
7116
+ const box2dPoints = box2dCreatePointList(points);
7117
+ shape.Set(box2dPoints, points.length);
7118
+ return shape;
7119
+ }
7120
+
7121
+ const shape = box2dCreatePolygonShape(points);
7122
+ return this.addShape(shape, density, friction, restitution, isSensor);
7123
+ }
7124
+
7125
+ /** Add a regular polygon shape to the body
7126
+ * @param {number} [diameter]
7127
+ * @param {number} [sides]
7128
+ * @param {number} [density]
7129
+ * @param {number} [friction]
7130
+ * @param {number} [restitution]
7131
+ * @param {boolean} [isSensor] */
7132
+ addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
7133
+ {
7134
+ const points = [];
7135
+ const radius = diameter/2;
7136
+ for (let i=sides; i--;)
7137
+ points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
7138
+ return this.addPoly(points, density, friction, restitution, isSensor);
7139
+ }
7140
+
7141
+ /** Add a random polygon shape to the body
7142
+ * @param {number} [diameter]
7143
+ * @param {number} [density]
7144
+ * @param {number} [friction]
7145
+ * @param {number} [restitution]
7146
+ * @param {boolean} [isSensor] */
7147
+ addRandomPoly(diameter=1, density, friction, restitution, isSensor)
7148
+ {
7149
+ const sides = randInt(3, 9);
7150
+ const points = [];
7151
+ const radius = diameter/2;
7152
+ for (let i=sides; i--;)
7153
+ points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2));
7154
+ return this.addPoly(points, density, friction, restitution, isSensor);
7155
+ }
7156
+
7157
+ /** Add a circle shape to the body
7158
+ * @param {number} [diameter]
7159
+ * @param {Vector2} [offset]
7160
+ * @param {number} [density]
7161
+ * @param {number} [friction]
7162
+ * @param {number} [restitution]
7163
+ * @param {boolean} [isSensor] */
7164
+ addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
7165
+ {
7166
+ const shape = new box2d.instance.b2CircleShape();
7167
+ shape.set_m_p(box2d.vec2dTo(offset));
7168
+ shape.set_m_radius(diameter/2);
7169
+ return this.addShape(shape, density, friction, restitution, isSensor);
7170
+ }
7171
+
7172
+ /** Add an edge shape to the body
7173
+ * @param {Vector2} point1
7174
+ * @param {Vector2} point2
7175
+ * @param {number} [density]
7176
+ * @param {number} [friction]
7177
+ * @param {number} [restitution]
7178
+ * @param {boolean} [isSensor] */
7179
+ addEdge(point1, point2, density, friction, restitution, isSensor)
7180
+ {
7181
+ const shape = new box2d.instance.b2EdgeShape();
7182
+ shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
7183
+ return this.addShape(shape, density, friction, restitution, isSensor);
7184
+ }
7185
+
7186
+ /** Add an edge loop to the body, an edge loop connects the end points
7187
+ * @param {Array<Vector2>} points
7188
+ * @param {number} [density]
7189
+ * @param {number} [friction]
7190
+ * @param {number} [restitution]
7191
+ * @param {boolean} [isSensor] */
7192
+ addEdgeLoop(points, density, friction, restitution, isSensor)
7193
+ {
7194
+ const fixtures = [];
7195
+ const getPoint = i=> points[mod(i,points.length)];
7196
+ for (let i=0; i<points.length; ++i)
7197
+ {
7198
+ const shape = new box2d.instance.b2EdgeShape();
7199
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
7200
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
7201
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
7202
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
7203
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
7204
+ fixtures.push(f);
7205
+ }
7206
+ return fixtures;
7207
+ }
7208
+
7209
+ /** Add an edge list to the body
7210
+ * @param {Array<Vector2>} points
7211
+ * @param {number} [density]
7212
+ * @param {number} [friction]
7213
+ * @param {number} [restitution]
7214
+ * @param {boolean} [isSensor] */
7215
+ addEdgeList(points, density, friction, restitution, isSensor)
7216
+ {
7217
+ const fixtures = [];
7218
+ for (let i=0; i<points.length-1; ++i)
7219
+ {
7220
+ const shape = new box2d.instance.b2EdgeShape();
7221
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
7222
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
7223
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
7224
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
7225
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
7226
+ fixtures.push(f);
7227
+ }
7228
+ return fixtures;
7229
+ }
7230
+
7231
+ ///////////////////////////////////////////////////////////////////////////////
7232
+ // physics get functions
7233
+
7234
+ /** Gets the center of mass
7235
+ * @return {Vector2} */
7236
+ getCenterOfMass() { return box2d.vec2From(this.body.GetWorldCenter()); }
7237
+
7238
+ /** Gets the linear velocity
7239
+ * @return {Vector2} */
7240
+ getLinearVelocity() { return box2d.vec2From(this.body.GetLinearVelocity()); }
7241
+
7242
+ /** Gets the angular velocity
7243
+ * @return {Vector2} */
7244
+ getAngularVelocity() { return this.body.GetAngularVelocity(); }
7245
+
7246
+ /** Gets the mass
7247
+ * @return {number} */
7248
+ getMass() { return this.body.GetMass(); }
7249
+
7250
+ /** Gets the rotational inertia
7251
+ * @return {number} */
7252
+ getInertia() { return this.body.GetInertia(); }
7253
+
7254
+ /** Check if this object is awake
7255
+ * @return {boolean} */
7256
+ getIsAwake() { return this.body.IsAwake(); }
7257
+
7258
+ /** Gets the physics body type
7259
+ * @return {number} */
7260
+ getBodyType() { return this.body.GetType(); }
7261
+
7262
+ ///////////////////////////////////////////////////////////////////////////////
7263
+ // physics set functions
7264
+
7265
+ /** Sets the position and angle
7266
+ * @param {Vector2} pos
7267
+ * @param {number} angle */
7268
+ setTransform(pos, angle)
7269
+ {
7270
+ this.pos = pos;
7271
+ this.angle = angle;
7272
+ this.body.SetTransform(box2d.vec2dTo(pos), angle);
7273
+ }
7274
+
7275
+ /** Sets the position
7276
+ * @param {Vector2} pos */
7277
+ setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
7278
+
7279
+ /** Sets the angle
7280
+ * @param {number} angle */
7281
+ setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
7282
+
7283
+ /** Sets the linear velocity
7284
+ * @param {Vector2} velocity */
7285
+ setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
7286
+
7287
+ /** Sets the angular velocity
7288
+ * @param {number} angularVelocity */
7289
+ setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
7290
+
7291
+ /** Sets the linear damping
7292
+ * @param {number} damping */
7293
+ setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
7294
+
7295
+ /** Sets the angular damping
7296
+ * @param {number} damping */
7297
+ setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
7298
+
7299
+ /** Sets the gravity scale
7300
+ * @param {number} [scale] */
7301
+ setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
7302
+
7303
+ /** Should this body be treated like a bullet for continuous collision detection?
7304
+ * @param {boolean} [isBullet] */
7305
+ setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
7306
+
7307
+ /** Set the sleep state of the body
7308
+ * @param {boolean} [isAwake] */
7309
+ setAwake(isAwake=true) { this.body.SetAwake(isAwake); }
7310
+
7311
+ /** Set the physics body type
7312
+ * @param {number} type */
7313
+ setBodyType(type) { this.body.SetType(type); }
7314
+
7315
+ /** Set whether the body is allowed to sleep
7316
+ * @param {boolean} [isAllowed] */
7317
+ setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
7318
+
7319
+ /** Set whether the body can rotate
7320
+ * @param {boolean} [isFixed] */
7321
+ setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
7322
+
7323
+ /** Set the center of mass of the body
7324
+ * @param {Vector2} center */
7325
+ setCenterOfMass(center) { this.setMassData(center) }
7326
+
7327
+ /** Set the mass of the body
7328
+ * @param {number} mass */
7329
+ setMass(mass) { this.setMassData(undefined, mass) }
7330
+
7331
+ /** Set the moment of inertia of the body
7332
+ * @param {number} momentOfInertia */
7333
+ setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
7334
+
7335
+ /** Reset the mass, center of mass, and moment */
7336
+ resetMassData() { this.body.ResetMassData(); }
7337
+
7338
+ /** Set the mass data of the body
7339
+ * @param {Vector2} [localCenter]
7340
+ * @param {number} [mass]
7341
+ * @param {number} [momentOfInertia] */
7342
+ setMassData(localCenter, mass, momentOfInertia)
7343
+ {
7344
+ const data = new box2d.instance.b2MassData();
7345
+ this.body.GetMassData(data);
7346
+ localCenter && data.set_center(box2d.vec2dTo(localCenter));
7347
+ mass && data.set_mass(mass);
7348
+ momentOfInertia && data.set_I(momentOfInertia);
7349
+ this.body.SetMassData(data);
7350
+ }
7351
+
7352
+ /** Set the collision filter data for this body
7353
+ * @param {number} [categoryBits]
7354
+ * @param {number} [ignoreCategoryBits]
7355
+ * @param {number} [groupIndex] */
7356
+ setFilterData(categoryBits=0, ignoreCategoryBits=0, groupIndex=0)
7357
+ {
7358
+ this.getFixtureList().forEach(fixture=>
7359
+ {
7360
+ const filter = fixture.GetFilterData();
7361
+ filter.set_categoryBits(categoryBits);
7362
+ filter.set_maskBits(0xffff & ~ignoreCategoryBits);
7363
+ filter.set_groupIndex(groupIndex);
7364
+ });
7365
+ }
7366
+
7367
+ /** Set if this body is a sensor
7368
+ * @param {boolean} [isSensor] */
7369
+ setSensor(isSensor=true)
7370
+ { this.getFixtureList().forEach(f=>f.SetSensor(isSensor)); }
7371
+
7372
+ ///////////////////////////////////////////////////////////////////////////////
7373
+ // physics force and torque functions
7374
+
7375
+ /** Apply force to this object
7376
+ * @param {Vector2} force
7377
+ * @param {Vector2} [pos] */
7378
+ applyForce(force, pos)
7379
+ {
7380
+ pos ||= this.getCenterOfMass();
7381
+ this.setAwake();
7382
+ this.body.ApplyForce(box2d.vec2dTo(force), box2d.vec2dTo(pos));
7383
+ }
7384
+
7385
+ /** Apply acceleration to this object
7386
+ * @param {Vector2} acceleration
7387
+ * @param {Vector2} [pos] */
7388
+ applyAcceleration(acceleration, pos)
7389
+ {
7390
+ pos ||= this.getCenterOfMass();
7391
+ this.setAwake();
7392
+ this.body.ApplyLinearImpulse(box2d.vec2dTo(acceleration), box2d.vec2dTo(pos));
7393
+ }
7394
+
7395
+ /** Apply torque to this object
7396
+ * @param {number} torque */
7397
+ applyTorque(torque)
7398
+ {
7399
+ this.setAwake();
7400
+ this.body.ApplyTorque(torque);
7401
+ }
7402
+
7403
+ /** Apply angular acceleration to this object
7404
+ * @param {number} acceleration */
7405
+ applyAngularAcceleration(acceleration)
7406
+ {
7407
+ this.setAwake();
7408
+ this.body.ApplyAngularImpulse(acceleration);
7409
+ }
7410
+
7411
+ ///////////////////////////////////////////////////////////////////////////////
7412
+ // lists of fixtures and joints
7413
+
7414
+ /** Check if this object has any fixtures
7415
+ * @return {boolean} */
7416
+ hasFixtures() { return !box2d.isNull(this.body.GetFixtureList()); }
7417
+
7418
+ /** Get list of fixtures for this object
7419
+ * @return {Array<Object>} */
7420
+ getFixtureList()
7421
+ {
7422
+ const fixtures = [];
7423
+ for (let fixture=this.body.GetFixtureList(); !box2d.isNull(fixture); )
7424
+ {
7425
+ fixtures.push(fixture);
7426
+ fixture = fixture.GetNext();
7427
+ }
7428
+ return fixtures;
7429
+ }
7430
+
7431
+ /** Check if this object has any joints
7432
+ * @return {boolean} */
7433
+ hasJoints() { return !box2d.isNull(this.body.GetJointList()); }
7434
+
7435
+ /** Get list of joints for this object
7436
+ * @return {Array<Object>} */
7437
+ getJointList()
7438
+ {
7439
+ const joints = [];
7440
+ for (let joint=this.body.GetJointList(); !box2d.isNull(joint); )
7441
+ {
7442
+ joints.push(joint);
7443
+ joint = joint.get_next();
7444
+ }
7445
+ return joints;
7446
+ }
7447
+ }
7448
+
7449
+ ///////////////////////////////////////////////////////////////////////////////
7450
+ /**
7451
+ * Box2D Raycast Result
7452
+ * - Holds results from a box2d raycast queries
7453
+ * - Automatically created by box2d raycast functions
7454
+ */
7455
+ class Box2dRaycastResult
7456
+ {
7457
+ /** Create a raycast result
7458
+ * @param {Object} fixture
7459
+ * @param {Vector2} point
7460
+ * @param {Vector2} normal
7461
+ * @param {number} fraction */
7462
+ constructor(fixture, point, normal, fraction)
7463
+ {
7464
+ /** @property {Box2dObject} - The box2d object */
7465
+ this.object = fixture.GetBody().object;
7466
+ /** @property {Object} - The fixture that was hit */
7467
+ this.fixture = fixture;
7468
+ /** @property {Vector2} - The hit point */
7469
+ this.point = point;
7470
+ /** @property {Vector2} - The hit normal */
7471
+ this.normal = normal;
7472
+ /** @property {number} - Distance fraction at the point of intersection */
7473
+ this.fraction = fraction;
7474
+ }
7475
+ }
7476
+
7477
+ ///////////////////////////////////////////////////////////////////////////////
7478
+ /**
7479
+ * Box2D Joint
7480
+ * - Base class for Box2D joints
7481
+ * - A joint is used to connect objects together
7482
+ */
7483
+ class Box2dJoint
7484
+ {
7485
+ /** Create a box2d joint, the base class is not intended to be used directly
7486
+ * @param {Object} jointDef */
7487
+ constructor(jointDef)
7488
+ {
7489
+ this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
7490
+ }
7491
+
7492
+ /** Destroy this joint */
7493
+ destroy() { box2d.world.DestroyJoint(this.box2dJoint); this.box2dJoint = 0; }
7494
+
7495
+ /** Get the first object attached to this joint
7496
+ * @return {Box2dObject} */
7497
+ getObjectA() { return this.box2dJoint.GetBodyA().object; }
7498
+
7499
+ /** Get the second object attached to this joint
7500
+ * @return {Box2dObject} */
7501
+ getObjectB() { return this.box2dJoint.GetBodyB().object; }
7502
+
7503
+ /** Get the first anchor for this joint in world coordinates
7504
+ * @return {Vector2} */
7505
+ getAnchorA() { return box2d.vec2From(this.box2dJoint.GetAnchorA());}
7506
+
7507
+ /** Get the second anchor for this joint in world coordinates
7508
+ * @return {Vector2} */
7509
+ getAnchorB() { return box2d.vec2From(this.box2dJoint.GetAnchorB());}
7510
+
7511
+ /** Get the reaction force on bodyB at the joint anchor given a time step
7512
+ * @param {number} time
7513
+ * @return {Vector2} */
7514
+ getReactionForce(time) { return box2d.vec2From(this.box2dJoint.GetReactionForce(1/time));}
7515
+
7516
+ /** Get the reaction torque on bodyB in N*m given a time step
7517
+ * @param {number} time
7518
+ * @return {number} */
7519
+ getReactionTorque(time) { return this.box2dJoint.GetReactionTorque(1/time);}
7520
+
7521
+ /** Check if the connected bodies should collide
7522
+ * @return {boolean} */
7523
+ getCollideConnected() { return this.box2dJoint.getCollideConnected();}
7524
+
7525
+ /** Check if either connected body is active
7526
+ * @return {boolean} */
7527
+ isActive() { return this.box2dJoint.IsActive();}
7528
+ }
7529
+
7530
+ ///////////////////////////////////////////////////////////////////////////////
7531
+ /**
7532
+ * Box2D Target Joint, also known as a mouse joint
7533
+ * - Used to make a point on a object track a specific world point target
7534
+ * - This a soft constraint with a max force
7535
+ * - This allows the constraint to stretch and without applying huge forces
7536
+ * @extends Box2dJoint
7537
+ */
7538
+ class Box2dTargetJoint extends Box2dJoint
7539
+ {
7540
+ /** Create a target joint
7541
+ * @param {Box2dObject} object
7542
+ * @param {Box2dObject} fixedObject
7543
+ * @param {Vector2} worldPos */
7544
+ constructor(object, fixedObject, worldPos)
7545
+ {
7546
+ object.setAwake();
7547
+ const jointDef = new box2d.instance.b2MouseJointDef();
7548
+ jointDef.set_bodyA(fixedObject.body);
7549
+ jointDef.set_bodyB(object.body);
7550
+ jointDef.set_target(box2d.vec2dTo(worldPos));
7551
+ jointDef.set_maxForce(2e3 * object.getMass());
7552
+ super(jointDef);
7553
+ }
7554
+
7555
+ /** Set the target point in world coordinates
7556
+ * @param {Vector2} pos */
7557
+ setTarget(pos) { this.box2dJoint.SetTarget(box2d.vec2dTo(pos)); }
7558
+
7559
+ /** Get the target point in world coordinates
7560
+ * @return {Vector2} */
7561
+ getTarget(){ return box2d.vec2From(this.box2dJoint.GetTarget()); }
7562
+
7563
+ /** Sets the maximum force in Newtons
7564
+ * @param {number} force */
7565
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
7566
+
7567
+ /** Gets the maximum force in Newtons
7568
+ * @return {number} */
7569
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
7570
+
7571
+ /** Sets the joint frequency in Hertz
7572
+ * @param {number} hz */
7573
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7574
+
7575
+ /** Gets the joint frequency in Hertz
7576
+ * @return {number} */
7577
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7578
+ }
7579
+
7580
+ ///////////////////////////////////////////////////////////////////////////////
7581
+ /**
7582
+ * Box2D Distance Joint
7583
+ * - Constrains two points on two objects to remain at a fixed distance
7584
+ * - You can view this as a massless, rigid rod
7585
+ * @extends Box2dJoint
7586
+ */
7587
+ class Box2dDistanceJoint extends Box2dJoint
7588
+ {
7589
+ /** Create a distance joint
7590
+ * @param {Box2dObject} objectA
7591
+ * @param {Box2dObject} objectB
7592
+ * @param {Vector2} anchorA
7593
+ * @param {Vector2} anchorB
7594
+ * @param {boolean} [collide] */
7595
+ constructor(objectA, objectB, anchorA, anchorB, collide=false)
7596
+ {
7597
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7598
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7599
+ const localAnchorA = objectA.worldToLocal(anchorA);
7600
+ const localAnchorB = objectB.worldToLocal(anchorB);
7601
+ const jointDef = new box2d.instance.b2DistanceJointDef();
7602
+ jointDef.set_bodyA(objectA.body);
7603
+ jointDef.set_bodyB(objectB.body);
7604
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7605
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7606
+ jointDef.set_length(anchorA.distance(anchorB));
7607
+ jointDef.set_collideConnected(collide);
7608
+ super(jointDef);
7609
+ }
7610
+
7611
+ /** Get the local anchor point relative to objectA's origin
7612
+ * @return {Vector2} */
7613
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7614
+
7615
+ /** Get the local anchor point relative to objectB's origin
7616
+ * @return {Vector2} */
7617
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7618
+
7619
+ /** Set the length of the joint
7620
+ * @param {number} length */
7621
+ setLength(length) { this.box2dJoint.SetLength(length); }
7622
+
7623
+ /** Get the length of the joint
7624
+ * @return {number} */
7625
+ getLength() { return this.box2dJoint.GetLength(); }
7626
+
7627
+ /** Set the frequency in Hertz
7628
+ * @param {number} hz */
7629
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7630
+
7631
+ /** Get the frequency in Hertz
7632
+ * @return {number} */
7633
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7634
+
7635
+ /** Set the damping ratio
7636
+ * @param {number} ratio */
7637
+ setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); }
7638
+
7639
+ /** Get the damping ratio
7640
+ * @return {number} */
7641
+ getDampingRatio() { return this.box2dJoint.GetDampingRatio(); }
7642
+ }
7643
+
7644
+ ///////////////////////////////////////////////////////////////////////////////
7645
+ /**
7646
+ * Box2D Pin Joint
7647
+ * - Pins two objects together at a point
7648
+ * @extends Box2dDistanceJoint
7649
+ */
7650
+ class Box2dPinJoint extends Box2dDistanceJoint
7651
+ {
7652
+ /** Create a pin joint
7653
+ * @param {Box2dObject} objectA
7654
+ * @param {Box2dObject} objectB
7655
+ * @param {Vector2} [pos]
7656
+ * @param {boolean} [collide] */
7657
+ constructor(objectA, objectB, pos=objectA.pos, collide=false)
7658
+ {
7659
+ super(objectA, objectB, undefined, pos, collide);
7660
+ }
7661
+ }
7662
+
7663
+ ///////////////////////////////////////////////////////////////////////////////
7664
+ /**
7665
+ * Box2D Rope Joint
7666
+ * - Enforces a maximum distance between two points on two objects
7667
+ * @extends Box2dJoint
7668
+ */
7669
+ class Box2dRopeJoint extends Box2dJoint
7670
+ {
7671
+ /** Create a rope joint
7672
+ * @param {Box2dObject} objectA
7673
+ * @param {Box2dObject} objectB
7674
+ * @param {Vector2} anchorA
7675
+ * @param {Vector2} anchorB
7676
+ * @param {number} extraLength
7677
+ * @param {boolean} [collide] */
7678
+ constructor(objectA, objectB, anchorA, anchorB, extraLength=0, collide=false)
7679
+ {
7680
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7681
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7682
+ const localAnchorA = objectA.worldToLocal(anchorA);
7683
+ const localAnchorB = objectB.worldToLocal(anchorB);
7684
+ const jointDef = new box2d.instance.b2RopeJointDef();
7685
+ jointDef.set_bodyA(objectA.body);
7686
+ jointDef.set_bodyB(objectB.body);
7687
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7688
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7689
+ jointDef.set_maxLength(anchorA.distance(anchorB)+extraLength);
7690
+ jointDef.set_collideConnected(collide);
7691
+ super(jointDef);
7692
+ }
7693
+
7694
+ /** Get the local anchor point relative to objectA's origin
7695
+ * @return {Vector2} */
7696
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7697
+
7698
+ /** Get the local anchor point relative to objectB's origin
7699
+ * @return {Vector2} */
7700
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7701
+
7702
+ /** Set the max length of the joint
7703
+ * @param {number} length */
7704
+ setMaxLength(length) { this.box2dJoint.SetMaxLength(length); }
7705
+
7706
+ /** Get the max length of the joint
7707
+ * @return {number} */
7708
+ getMaxLength() { return this.box2dJoint.GetMaxLength(); }
7709
+ }
7710
+
7711
+ ///////////////////////////////////////////////////////////////////////////////
7712
+ /**
7713
+ * Box2D Revolute Joint
7714
+ * - Constrains two objects to share a point while they are free to rotate around the point
7715
+ * - The relative rotation about the shared point is the joint angle
7716
+ * - You can limit the relative rotation with a joint limit
7717
+ * - You can use a motor to drive the relative rotation about the shared point
7718
+ * - A maximum motor torque is provided so that infinite forces are not generated
7719
+ * @extends Box2dJoint
7720
+ */
7721
+ class Box2dRevoluteJoint extends Box2dJoint
7722
+ {
7723
+ /** Create a revolute joint
7724
+ * @param {Box2dObject} objectA
7725
+ * @param {Box2dObject} objectB
7726
+ * @param {Vector2} anchor
7727
+ * @param {boolean} [collide] */
7728
+ constructor(objectA, objectB, anchor, collide=false)
7729
+ {
7730
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7731
+ const localAnchorA = objectA.worldToLocal(anchor);
7732
+ const localAnchorB = objectB.worldToLocal(anchor);
7733
+ const jointDef = new box2d.instance.b2RevoluteJointDef();
7734
+ jointDef.set_bodyA(objectA.body);
7735
+ jointDef.set_bodyB(objectB.body);
7736
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7737
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7738
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7739
+ jointDef.set_collideConnected(collide);
7740
+ super(jointDef);
7741
+ }
7742
+
7743
+ /** Get the local anchor point relative to objectA's origin
7744
+ * @return {Vector2} */
7745
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7746
+
7747
+ /** Get the local anchor point relative to objectB's origin
7748
+ * @return {Vector2} */
7749
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7750
+
7751
+ /** Get the reference angle, objectB angle minus objectA angle in the reference state
7752
+ * @return {number} */
7753
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7754
+
7755
+ /** Get the current joint angle
7756
+ * @return {number} */
7757
+ getJointAngle() { return this.box2dJoint.GetJointAngle(); }
7758
+
7759
+ /** Get the current joint angle speed in radians per second
7760
+ * @return {number} */
7761
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7762
+
7763
+ /** Is the joint limit enabled?
7764
+ * @return {boolean} */
7765
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
7766
+
7767
+ /** Enable/disable the joint limit
7768
+ * @param {boolean} [enable] */
7769
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
7770
+
7771
+ /** Get the lower joint limit
7772
+ * @return {number} */
7773
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
7774
+
7775
+ /** Get the upper joint limit
7776
+ * @return {number} */
7777
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
7778
+
7779
+ /** Set the joint limits
7780
+ * @param {number} min
7781
+ * @param {number} max */
7782
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
7783
+
7784
+ /** Is the joint motor enabled?
7785
+ * @return {boolean} */
7786
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7787
+
7788
+ /** Enable/disable the joint motor
7789
+ * @param {boolean} [enable] */
7790
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7791
+
7792
+ /** Set the motor speed
7793
+ * @param {number} speed */
7794
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7795
+
7796
+ /** Get the motor speed
7797
+ * @return {number} */
7798
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7799
+
7800
+ /** Set the motor torque
7801
+ * @param {number} torque */
7802
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
7803
+
7804
+ /** Get the max motor torque
7805
+ * @return {number} */
7806
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
7807
+
7808
+ /** Get the motor torque given a time step
7809
+ * @param {number} time
7810
+ * @return {number} */
7811
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
7812
+ }
7813
+
7814
+ ///////////////////////////////////////////////////////////////////////////////
7815
+ /**
7816
+ * Box2D Gear Joint
7817
+ * - A gear joint is used to connect two joints together
7818
+ * - Either joint can be a revolute or prismatic joint
7819
+ * - You specify a gear ratio to bind the motions together
7820
+ * @extends Box2dJoint
7821
+ */
7822
+ class Box2dGearJoint extends Box2dJoint
7823
+ {
7824
+ /** Create a gear joint
7825
+ * @param {Box2dObject} objectA
7826
+ * @param {Box2dObject} objectB
7827
+ * @param {Box2dJoint} joint1
7828
+ * @param {Box2dJoint} joint2
7829
+ * @param {ratio} [ratio] */
7830
+ constructor(objectA, objectB, joint1, joint2, ratio=1)
7831
+ {
7832
+ const jointDef = new box2d.instance.b2GearJointDef();
7833
+ jointDef.set_bodyA(objectA.body);
7834
+ jointDef.set_bodyB(objectB.body);
7835
+ jointDef.set_joint1(joint1.box2dJoint);
7836
+ jointDef.set_joint2(joint2.box2dJoint);
7837
+ jointDef.set_ratio(ratio);
7838
+ super(jointDef);
7839
+
7840
+ this.joint1 = joint1;
7841
+ this.joint2 = joint2;
7842
+ }
7843
+
7844
+ /** Get the first joint
7845
+ * @return {Box2dJoint} */
7846
+ getJoint1() { return this.joint1; }
7847
+
7848
+ /** Get the second joint
7849
+ * @return {Box2dJoint} */
7850
+ getJoint2() { return this.joint2; }
7851
+
7852
+ /** Set the gear ratio
7853
+ * @param {number} ratio */
7854
+ setRatio(ratio) { return this.box2dJoint.SetRatio(ratio); }
7855
+
7856
+ /** Get the gear ratio
7857
+ * @return {number} */
7858
+ getRatio() { return this.box2dJoint.GetRatio(); }
7859
+ }
7860
+
7861
+ ///////////////////////////////////////////////////////////////////////////////
7862
+ /**
7863
+ * Box2D Prismatic Joint
7864
+ * - Provides one degree of freedom: translation along an axis fixed in objectA
7865
+ * - Relative rotation is prevented
7866
+ * - You can use a joint limit to restrict the range of motion
7867
+ * - You can use a joint motor to drive the motion or to model joint friction
7868
+ * @extends Box2dJoint
7869
+ */
7870
+ class Box2dPrismaticJoint extends Box2dJoint
7871
+ {
7872
+ /** Create a prismatic joint
7873
+ * @param {Box2dObject} objectA
7874
+ * @param {Box2dObject} objectB
7875
+ * @param {Vector2} anchor
7876
+ * @param {Vector2} worldAxis
7877
+ * @param {boolean} [collide] */
7878
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
7879
+ {
7880
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7881
+ const localAnchorA = objectA.worldToLocal(anchor);
7882
+ const localAnchorB = objectB.worldToLocal(anchor);
7883
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
7884
+ const jointDef = new box2d.instance.b2PrismaticJointDef();
7885
+ jointDef.set_bodyA(objectA.body);
7886
+ jointDef.set_bodyB(objectB.body);
7887
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7888
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7889
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
7890
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7891
+ jointDef.set_collideConnected(collide);
7892
+ super(jointDef);
7893
+ }
7894
+
7895
+ /** Get the local anchor point relative to objectA's origin
7896
+ * @return {Vector2} */
7897
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7898
+
7899
+ /** Get the local anchor point relative to objectB's origin
7900
+ * @return {Vector2} */
7901
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7902
+
7903
+ /** Get the local joint axis relative to bodyA
7904
+ * @return {Vector2} */
7905
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
7906
+
7907
+ /** Get the reference angle
7908
+ * @return {number} */
7909
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7910
+
7911
+ /** Get the current joint translation
7912
+ * @return {number} */
7913
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
7914
+
7915
+ /** Get the current joint translation speed
7916
+ * @return {number} */
7917
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7918
+
7919
+ /** Is the joint limit enabled?
7920
+ * @return {boolean} */
7921
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
7922
+
7923
+ /** Enable/disable the joint limit
7924
+ * @param {boolean} [enable] */
7925
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
7926
+
7927
+ /** Get the lower joint limit
7928
+ * @return {number} */
7929
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
7930
+
7931
+ /** Get the upper joint limit
7932
+ * @return {number} */
7933
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
7934
+
7935
+ /** Set the joint limits
7936
+ * @param {number} min
7937
+ * @param {number} max */
7938
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
7939
+
7940
+ /** Is the motor enabled?
7941
+ * @return {boolean} */
7942
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7943
+
7944
+ /** Enable/disable the joint motor
7945
+ * @param {boolean} [enable] */
7946
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7947
+
7948
+ /** Set the motor speed
7949
+ * @param {number} speed */
7950
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7951
+
7952
+ /** Get the motor speed
7953
+ * @return {number} */
7954
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7955
+
7956
+ /** Set the maximum motor force
7957
+ * @param {number} force */
7958
+ setMaxMotorForce(force) { return this.box2dJoint.SetMaxMotorForce(force); }
7959
+
7960
+ /** Get the maximum motor force
7961
+ * @return {number} */
7962
+ getMaxMotorForce() { return this.box2dJoint.GetMaxMotorForce(); }
7963
+
7964
+ /** Get the motor force given a time step
7965
+ * @param {number} time
7966
+ * @return {number} */
7967
+ getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
7968
+ }
7969
+
7970
+ ///////////////////////////////////////////////////////////////////////////////
7971
+ /**
7972
+ * Box2D Wheel Joint
7973
+ * - Provides two degrees of freedom: translation along an axis fixed in objectA and rotation
7974
+ * - You can use a joint limit to restrict the range of motion
7975
+ * - You can use a joint motor to drive the motion or to model joint friction
7976
+ * - This joint is designed for vehicle suspensions
7977
+ * @extends Box2dJoint
7978
+ */
7979
+ class Box2dWheelJoint extends Box2dJoint
7980
+ {
7981
+ /** Create a wheel joint
7982
+ * @param {Box2dObject} objectA
7983
+ * @param {Box2dObject} objectB
7984
+ * @param {Vector2} anchor
7985
+ * @param {Vector2} worldAxis
7986
+ * @param {boolean} [collide] */
7987
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
7988
+ {
7989
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7990
+ const localAnchorA = objectA.worldToLocal(anchor);
7991
+ const localAnchorB = objectB.worldToLocal(anchor);
7992
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
7993
+ const jointDef = new box2d.instance.b2WheelJointDef();
7994
+ jointDef.set_bodyA(objectA.body);
7995
+ jointDef.set_bodyB(objectB.body);
7996
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7997
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7998
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
7999
+ jointDef.set_collideConnected(collide);
8000
+ super(jointDef);
8001
+ }
8002
+
8003
+ /** Get the local anchor point relative to objectA's origin
8004
+ * @return {Vector2} */
8005
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
8006
+
8007
+ /** Get the local anchor point relative to objectB's origin
8008
+ * @return {Vector2} */
8009
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
8010
+
8011
+ /** Get the local joint axis relative to bodyA
8012
+ * @return {Vector2} */
8013
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
8014
+
8015
+ /** Get the current joint translation
8016
+ * @return {number} */
8017
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
8018
+
8019
+ /** Get the current joint translation speed
8020
+ * @return {number} */
8021
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
8022
+
8023
+ /** Is the joint motor enabled?
8024
+ * @return {boolean} */
8025
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
8026
+
8027
+ /** Enable/disable the joint motor
8028
+ * @param {boolean} [enable] */
8029
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
8030
+
8031
+ /** Set the motor speed
8032
+ * @param {number} speed */
8033
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
8034
+
8035
+ /** Get the motor speed
8036
+ * @return {number} */
8037
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
8038
+
8039
+ /** Set the maximum motor torque
8040
+ * @param {number} torque */
8041
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
8042
+
8043
+ /** Get the max motor torque
8044
+ * @return {number} */
8045
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
8046
+
8047
+ /** Get the motor torque for a time step
8048
+ * @return {number} */
8049
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
8050
+
8051
+ /** Set the spring frequency in Hertz
8052
+ * @param {number} hz */
8053
+ setSpringFrequencyHz(hz) { return this.box2dJoint.SetSpringFrequencyHz(hz); }
8054
+
8055
+ /** Get the spring frequency in Hertz
8056
+ * @return {number} */
8057
+ getSpringFrequencyHz() { return this.box2dJoint.GetSpringFrequencyHz(); }
8058
+
8059
+ /** Set the spring damping ratio
8060
+ * @param {number} ratio */
8061
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
8062
+
8063
+ /** Get the spring damping ratio
8064
+ * @return {number} */
8065
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
8066
+ }
8067
+
8068
+ ///////////////////////////////////////////////////////////////////////////////
8069
+ /**
8070
+ * Box2D Weld Joint
8071
+ * - Glues two objects together
8072
+ * @extends Box2dJoint
8073
+ */
8074
+ class Box2dWeldJoint extends Box2dJoint
8075
+ {
8076
+ /** Create a weld joint
8077
+ * @param {Box2dObject} objectA
8078
+ * @param {Box2dObject} objectB
8079
+ * @param {Vector2} anchor
8080
+ * @param {boolean} [collide] */
8081
+ constructor(objectA, objectB, anchor, collide=false)
8082
+ {
8083
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
8084
+ const localAnchorA = objectA.worldToLocal(anchor);
8085
+ const localAnchorB = objectB.worldToLocal(anchor);
8086
+ const jointDef = new box2d.instance.b2WeldJointDef();
8087
+ jointDef.set_bodyA(objectA.body);
8088
+ jointDef.set_bodyB(objectB.body);
8089
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8090
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8091
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
8092
+ jointDef.set_collideConnected(collide);
8093
+ super(jointDef);
8094
+ }
8095
+
8096
+ /** Get the local anchor point relative to objectA's origin
8097
+ * @return {Vector2} */
8098
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
8099
+
8100
+ /** Get the local anchor point relative to objectB's origin
8101
+ * @return {Vector2} */
8102
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
8103
+
8104
+ /** Get the reference angle
8105
+ * @return {number} */
8106
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
8107
+
8108
+ /** Set the frequency in Hertz
8109
+ * @param {number} hz */
8110
+ setFrequency(hz) { return this.box2dJoint.SetFrequency(hz); }
8111
+
8112
+ /** Get the frequency in Hertz
8113
+ * @return {number} */
8114
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
8115
+
8116
+ /** Set the damping ratio
8117
+ * @param {number} ratio */
8118
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
8119
+
8120
+ /** Get the damping ratio
8121
+ * @return {number} */
8122
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
8123
+ }
8124
+
8125
+ ///////////////////////////////////////////////////////////////////////////////
8126
+ /**
8127
+ * Box2D Friction Joint
8128
+ * - Used to apply top-down friction
8129
+ * - Provides 2D translational friction and angular friction
8130
+ * @extends Box2dJoint
8131
+ */
8132
+ class Box2dFrictionJoint extends Box2dJoint
8133
+ {
8134
+ /** Create a friction joint
8135
+ * @param {Box2dObject} objectA
8136
+ * @param {Box2dObject} objectB
8137
+ * @param {Vector2} anchor
8138
+ * @param {boolean} [collide] */
8139
+ constructor(objectA, objectB, anchor, collide=false)
8140
+ {
8141
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
8142
+ const localAnchorA = objectA.worldToLocal(anchor);
8143
+ const localAnchorB = objectB.worldToLocal(anchor);
8144
+ const jointDef = new box2d.instance.b2FrictionJointDef();
8145
+ jointDef.set_bodyA(objectA.body);
8146
+ jointDef.set_bodyB(objectB.body);
8147
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8148
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8149
+ jointDef.set_collideConnected(collide);
8150
+ super(jointDef);
8151
+ }
8152
+
8153
+ /** Get the local anchor point relative to objectA's origin
8154
+ * @return {Vector2} */
8155
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
8156
+
8157
+ /** Get the local anchor point relative to objectB's origin
8158
+ * @return {Vector2} */
8159
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
8160
+
8161
+ /** Set the maximum friction force
8162
+ * @param {number} force */
8163
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
8164
+
8165
+ /** Get the maximum friction force
8166
+ * @return {number} */
8167
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
8168
+
8169
+ /** Set the maximum friction torque
8170
+ * @param {number} torque */
8171
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
8172
+
8173
+ /** Get the maximum friction torque
8174
+ * @return {number} */
8175
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
8176
+ }
8177
+
8178
+ ///////////////////////////////////////////////////////////////////////////////
8179
+ /**
8180
+ * Box2D Pulley Joint
8181
+ * - Connects to two objects and two fixed ground points
8182
+ * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
8183
+ * - The force transmitted is scaled by the ratio
8184
+ * @extends Box2dJoint
8185
+ */
8186
+ class Box2dPulleyJoint extends Box2dJoint
8187
+ {
8188
+ /** Create a pulley joint
8189
+ * @param {Box2dObject} objectA
8190
+ * @param {Box2dObject} objectB
8191
+ * @param {Vector2} groundAnchorA
8192
+ * @param {Vector2} groundAnchorB
8193
+ * @param {Vector2} anchorA
8194
+ * @param {Vector2} anchorB
8195
+ * @param {number} [ratio]
8196
+ * @param {boolean} [collide] */
8197
+ constructor(objectA, objectB, groundAnchorA, groundAnchorB, anchorA, anchorB, ratio=1, collide=false)
8198
+ {
8199
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
8200
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
8201
+ const localAnchorA = objectA.worldToLocal(anchorA);
8202
+ const localAnchorB = objectB.worldToLocal(anchorB);
8203
+ const jointDef = new box2d.instance.b2PulleyJointDef();
8204
+ jointDef.set_bodyA(objectA.body);
8205
+ jointDef.set_bodyB(objectB.body);
8206
+ jointDef.set_groundAnchorA(box2d.vec2dTo(groundAnchorA));
8207
+ jointDef.set_groundAnchorB(box2d.vec2dTo(groundAnchorB));
8208
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
8209
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
8210
+ jointDef.set_ratio(ratio);
8211
+ jointDef.set_lengthA(groundAnchorA.distance(anchorA));
8212
+ jointDef.set_lengthB(groundAnchorB.distance(anchorB));
8213
+ jointDef.set_collideConnected(collide);
8214
+ super(jointDef);
8215
+ }
8216
+
8217
+ /** Get the first ground anchor
8218
+ * @return {Vector2} */
8219
+ getGroundAnchorA() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorA()); }
8220
+
8221
+ /** Get the second ground anchor
8222
+ * @return {Vector2} */
8223
+ getGroundAnchorB() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorB()); }
8224
+
8225
+ /** Get the current length of the segment attached to objectA
8226
+ * @return {number} */
8227
+ getLengthA() { return this.box2dJoint.GetLengthA(); }
8228
+
8229
+ /** Get the current length of the segment attached to objectB
8230
+ * @return {number} */
8231
+ getLengthB(){ return this.box2dJoint.GetLengthB(); }
8232
+
8233
+ /** Get the pulley ratio
8234
+ * @return {number} */
8235
+ getRatio() { return this.box2dJoint.GetRatio(); }
8236
+
8237
+ /** Get the current length of the segment attached to objectA
8238
+ * @return {number} */
8239
+ getCurrentLengthA() { return this.box2dJoint.GetCurrentLengthA(); }
8240
+
8241
+ /** Get the current length of the segment attached to objectB
8242
+ * @return {number} */
8243
+ getCurrentLengthB() { return this.box2dJoint.GetCurrentLengthB(); }
8244
+ }
8245
+
8246
+ ///////////////////////////////////////////////////////////////////////////////
8247
+ /**
8248
+ * Box2D Motor Joint
8249
+ * - Controls the relative motion between two objects
8250
+ * - Typical usage is to control the movement of a object with respect to the ground
8251
+ * @extends Box2dJoint
8252
+ */
8253
+ class Box2dMotorJoint extends Box2dJoint
8254
+ {
8255
+ /** Create a motor joint
8256
+ * @param {Box2dObject} objectA
8257
+ * @param {Box2dObject} objectB */
8258
+ constructor(objectA, objectB)
8259
+ {
8260
+ const linearOffset = objectA.worldToLocal(box2d.vec2From(objectB.body.GetPosition()));
8261
+ const angularOffset = objectB.body.GetAngle() - objectA.body.GetAngle();
8262
+ const jointDef = new box2d.instance.b2MotorJointDef();
8263
+ jointDef.set_bodyA(objectA.body);
8264
+ jointDef.set_bodyB(objectB.body);
8265
+ jointDef.set_linearOffset(box2d.vec2dTo(linearOffset));
8266
+ jointDef.set_angularOffset(angularOffset);
8267
+ super(jointDef);
8268
+ }
8269
+
8270
+ /** Set the target linear offset, in frame A, in meters.
8271
+ * @param {Vector2} offset */
8272
+ setLinearOffset(offset) { this.box2dJoint.SetLinearOffset(box2d.vec2dTo(offset)); }
8273
+
8274
+ /** Get the target linear offset, in frame A, in meters.
8275
+ * @return {Vector2} */
8276
+ getLinearOffset() { return box2d.vec2From(this.box2dJoint.GetLinearOffset()); }
8277
+
8278
+ /** Set the target angular offset
8279
+ * @param {number} offset */
8280
+ setAngularOffset(offset) { this.box2dJoint.SetAngularOffset(offset); }
8281
+
8282
+ /** Get the target angular offset
8283
+ * @return {number} */
8284
+ getAngularOffset() { return this.box2dJoint.GetAngularOffset(); }
8285
+
8286
+ /** Set the maximum friction force
8287
+ * @param {number} force */
8288
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
8289
+
8290
+ /** Get the maximum friction force
8291
+ * @return {number} */
8292
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
8293
+
8294
+ /** Set the maximum torque
8295
+ * @param {number} torque */
8296
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
8297
+
8298
+ /** Get the maximum torque
8299
+ * @return {number} */
8300
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
8301
+
8302
+ /** Set the position correction factor in the range [0,1]
8303
+ * @param {number} factor */
8304
+ setCorrectionFactor(factor) { this.box2dJoint.SetCorrectionFactor(factor); }
8305
+
8306
+ /** Get the position correction factor in the range [0,1]
8307
+ * @return {number} */
8308
+ getCorrectionFactor() { return this.box2dJoint.GetCorrectionFactor(); }
8309
+ }
8310
+
8311
+ ///////////////////////////////////////////////////////////////////////////////
8312
+ /**
8313
+ * Box2D Global Object
8314
+ * - Wraps Box2d world and provides global functions
8315
+ */
8316
+ class Box2dPlugin
8317
+ {
8318
+ /** Create the global UI system object
8319
+ * @param {Object} instance */
8320
+ constructor(instance)
8321
+ {
8322
+ ASSERT(!box2d, 'Box2D already initialized');
8323
+ box2d = this;
8324
+ this.instance = instance;
8325
+ this.world = new box2d.instance.b2World();
8326
+
8327
+ /** @property {number} - Velocity iterations per update*/
8328
+ this.velocityIterations = 8;
8329
+ /** @property {number} - Position iterations per update*/
8330
+ this.positionIterations = 3;
8331
+ /** @property {number} - Static, zero mass, zero velocity, may be manually moved */
8332
+ this.bodyTypeStatic = instance.b2_staticBody;
8333
+ /** @property {number} - Kinematic, zero mass, non-zero velocity set by user, moved by solver */
8334
+ this.bodyTypeKinematic = instance.b2_kinematicBody;
8335
+ /** @property {number} - Dynamic, positive mass, non-zero velocity determined by forces, moved by solver */
8336
+ this.bodyTypeDynamic = instance.b2_dynamicBody;
8337
+
8338
+ // setup contact listener
8339
+ const listener = new box2d.instance.JSContactListener();
8340
+ listener.BeginContact = function(contactPtr)
8341
+ {
8342
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
8343
+ const fixtureA = contact.GetFixtureA();
8344
+ const fixtureB = contact.GetFixtureB();
8345
+ const objectA = fixtureA.GetBody().object;
8346
+ const objectB = fixtureB.GetBody().object;
8347
+ objectA.beginContact(objectB);
8348
+ objectB.beginContact(objectA);
8349
+ }
8350
+ listener.EndContact = function(contactPtr)
8351
+ {
8352
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
8353
+ const fixtureA = contact.GetFixtureA();
8354
+ const fixtureB = contact.GetFixtureB();
8355
+ const objectA = fixtureA.GetBody().object;
8356
+ const objectB = fixtureB.GetBody().object;
8357
+ objectA.endContact(objectB);
8358
+ objectB.endContact(objectA);
8359
+ };
8360
+ listener.PreSolve = function() {};
8361
+ listener.PostSolve = function() {};
8362
+ box2d.world.SetContactListener(listener);
8363
+ }
8364
+
8365
+ /** Step the physics world simulation
8366
+ * @param {number} [frames] */
8367
+ step(frames=1)
8368
+ {
8369
+ box2d.world.SetGravity(box2d.vec2dTo(gravity));
8370
+ for (let i=frames; i--;)
8371
+ box2d.world.Step(timeDelta, this.velocityIterations, this.positionIterations);
8372
+ }
8373
+
8374
+ ///////////////////////////////////////////////////////////////////////////////
8375
+ // raycasting and querying
8376
+
8377
+ /** raycast and return a list of all the results
8378
+ * @param {Vector2} start
8379
+ * @param {Vector2} end */
8380
+ raycastAll(start, end)
8381
+ {
8382
+ const raycastCallback = new box2d.instance.JSRayCastCallback();
8383
+ raycastCallback.ReportFixture = function(fixturePointer, point, normal, fraction)
8384
+ {
8385
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8386
+ point = box2d.vec2FromPointer(point);
8387
+ normal = box2d.vec2FromPointer(normal);
8388
+ raycastResults.push(new Box2dRaycastResult(fixture, point, normal, fraction));
8389
+ return 1; // continue getting results
8390
+ };
8391
+
8392
+ const raycastResults = [];
8393
+ box2d.world.RayCast(raycastCallback, box2d.vec2dTo(start), box2d.vec2dTo(end));
8394
+ debugRaycast && debugLine(start, end, raycastResults.length ? '#f00' : '#00f', .02);
8395
+ return raycastResults;
8396
+ }
8397
+
8398
+ /** raycast and return the first result
8399
+ * @param {Vector2} start
8400
+ * @param {Vector2} end */
8401
+ raycast(start, end)
8402
+ {
8403
+ const raycastResults = box2d.raycastAll(start, end);
8404
+ if (!raycastResults.length)
8405
+ return undefined;
8406
+ return raycastResults.reduce((a,b)=>a.fraction < b.fraction ? a : b);
8407
+ }
8408
+
8409
+ /** box aabb cast and return all the objects
8410
+ * @param {Vector2} pos
8411
+ * @param {Vector2} size */
8412
+ boxCastAll(pos, size)
8413
+ {
8414
+ const queryCallback = new box2d.instance.JSQueryCallback();
8415
+ queryCallback.ReportFixture = function(fixturePointer)
8416
+ {
8417
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8418
+ const o = fixture.GetBody().object;
8419
+ if (!queryObjects.includes(o))
8420
+ queryObjects.push(o); // add if not already in list
8421
+ return true; // continue getting results
8422
+ };
8423
+
8424
+ const aabb = new box2d.instance.b2AABB();
8425
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
8426
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
8427
+
8428
+ let queryObjects = [];
8429
+ box2d.world.QueryAABB(queryCallback, aabb);
8430
+ debugRaycast && debugRect(pos, size, queryObjects.length ? '#f00' : '#00f', .02);
8431
+ return queryObjects;
8432
+ }
8433
+
8434
+ /** box aabb cast and return the first object
8435
+ * @param {Vector2} pos
8436
+ * @param {Vector2} size */
8437
+ boxCast(pos, size)
8438
+ {
8439
+ const queryCallback = new box2d.instance.JSQueryCallback();
8440
+ queryCallback.ReportFixture = function(fixturePointer)
8441
+ {
8442
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8443
+ queryObject = fixture.GetBody().object;
8444
+ return false; // stop getting results
8445
+ };
8446
+
8447
+ const aabb = new box2d.instance.b2AABB();
8448
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
8449
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
8450
+
8451
+ let queryObject;
8452
+ box2d.world.QueryAABB(queryCallback, aabb);
8453
+ debugRaycast && debugRect(pos, size, queryObject ? '#f00' : '#00f', .02);
8454
+ return queryObject;
8455
+ }
8456
+
8457
+ /** circle cast and return all the objects
8458
+ * @param {Vector2} pos
8459
+ * @param {number} diameter */
8460
+ circleCastAll(pos, diameter)
8461
+ {
8462
+ const radius2 = (diameter/2)**2;
8463
+ const results = box2d.boxCastAll(pos, vec2(diameter));
8464
+ return results.filter(o=>o.pos.distanceSquared(pos) < radius2);
8465
+ }
8466
+
8467
+ /** circle cast and return the first object
8468
+ * @param {Vector2} pos
8469
+ * @param {number} diameter */
8470
+ circleCast(pos, diameter)
8471
+ {
8472
+ const radius2 = (diameter/2)**2;
8473
+ let results = box2d.boxCastAll(pos, vec2(diameter));
8474
+
8475
+ let bestResult, bestDistance2;
8476
+ for (const result of results)
8477
+ {
8478
+ const distance2 = result.pos.distanceSquared(pos);
8479
+ if (distance2 < radius2 && (!bestResult || distance2 < bestDistance2))
8480
+ {
8481
+ bestResult = result;
8482
+ bestDistance2 = distance2;
8483
+ }
8484
+ }
8485
+ return bestResult;
8486
+ }
8487
+
8488
+ /** point cast and return the first object
8489
+ * @param {Vector2} pos
8490
+ * @param {boolean} dynamicOnly */
8491
+ pointCast(pos, dynamicOnly=true)
8492
+ {
8493
+ const queryCallback = new box2d.instance.JSQueryCallback();
8494
+ queryCallback.ReportFixture = function(fixturePointer)
8495
+ {
8496
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
8497
+ if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
8498
+ return true; // continue getting results
8499
+ if (!fixture.TestPoint(box2d.vec2dTo(pos)))
8500
+ return true; // continue getting results
8501
+ queryObject = fixture.GetBody().object;
8502
+ return false; // stop getting results
8503
+ };
8504
+
8505
+ const aabb = new box2d.instance.b2AABB();
8506
+ aabb.set_lowerBound(box2d.vec2dTo(pos));
8507
+ aabb.set_upperBound(box2d.vec2dTo(pos));
8508
+
8509
+ let queryObject;
8510
+ box2d.world.QueryAABB(queryCallback, aabb);
8511
+ debugRaycast && debugRect(pos, vec2(), queryObject ? '#f00' : '#00f', .02);
8512
+ return queryObject;
8513
+ }
8514
+
8515
+ ///////////////////////////////////////////////////////////////////////////////
8516
+ // drawing
8517
+
8518
+ /** draws a fixture
8519
+ * @param {Object} fixture
8520
+ * @param {Vector2} pos
8521
+ * @param {number} angle
8522
+ * @param {Color} [color]
8523
+ * @param {Color} [outlineColor]
8524
+ * @param {number} [lineWidth]
8525
+ * @param {CanvasRenderingContext2D} [context] */
8526
+ drawFixture(fixture, pos, angle, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8527
+ {
8528
+ const shape = box2d.castObjectType(fixture.GetShape());
8529
+ switch (shape.GetType())
8530
+ {
8531
+ case box2d.instance.b2Shape.e_polygon:
8532
+ {
8533
+ let points = [];
8534
+ for (let i=shape.GetVertexCount(); i--;)
8535
+ points.push(box2d.vec2From(shape.GetVertex(i)));
8536
+ box2d.drawPoly(pos, angle, points, color, outlineColor, lineWidth, context);
8537
+ break;
8538
+ }
8539
+ case box2d.instance.b2Shape.e_circle:
8540
+ {
8541
+ const radius = shape.get_m_radius();
8542
+ box2d.drawCircle(pos, radius, color, outlineColor, lineWidth, context);
8543
+ break;
8544
+ }
8545
+ case box2d.instance.b2Shape.e_edge:
8546
+ {
8547
+ const v1 = box2d.vec2From(shape.get_m_vertex1());
8548
+ const v2 = box2d.vec2From(shape.get_m_vertex2());
8549
+ box2d.drawLine(pos, angle, v1, v2, color, lineWidth, context);
8550
+ break;
8551
+ }
8552
+ }
8553
+ }
8554
+
8555
+ /** draws a circle
8556
+ * @param {Vector2} pos
8557
+ * @param {number} radius
8558
+ * @param {Color} [color]
8559
+ * @param {Color} [outlineColor]
8560
+ * @param {number} [lineWidth]
8561
+ * @param {CanvasRenderingContext2D} [context] */
8562
+ drawCircle(pos, radius, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8563
+ {
8564
+ drawCanvas2D(pos, vec2(1), 0, 0, context=>
8565
+ {
8566
+ context.beginPath();
8567
+ context.arc(0, 0, radius, 0, 9);
8568
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8569
+ }, 0, context);
8570
+ }
8571
+
8572
+ /** draws a polygon
8573
+ * @param {Vector2} pos
8574
+ * @param {number} angle
8575
+ * @param {Array<Vector2>} points
8576
+ * @param {Color} [color]
8577
+ * @param {Color} [outlineColor]
8578
+ * @param {number} [lineWidth]
8579
+ * @param {CanvasRenderingContext2D} [context] */
8580
+ drawPoly(pos, angle, points, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8581
+ {
8582
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8583
+ {
8584
+ context.beginPath();
8585
+ points.forEach(p=>context.lineTo(p.x, p.y));
8586
+ context.closePath();
8587
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8588
+ }, 0, context);
8589
+ }
8590
+
8591
+ /** draws a line
8592
+ * @param {Vector2} pos
8593
+ * @param {number} angle
8594
+ * @param {Vector2} posA
8595
+ * @param {Vector2} posB
8596
+ * @param {Color} [color]
8597
+ * @param {number} [lineWidth]
8598
+ * @param {CanvasRenderingContext2D} [context] */
8599
+ drawLine(pos, angle, posA, posB, color=WHITE, lineWidth=.1, context=mainContext)
8600
+ {
8601
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8602
+ {
8603
+ context.beginPath();
8604
+ context.lineTo(posA.x, posA.y);
8605
+ context.lineTo(posB.x, posB.y);
8606
+ box2d.drawFillStroke(0, color, lineWidth, context);
8607
+ }, 0, context);
8608
+ }
8609
+
8610
+ /** performs a fill or stroke as a helper to the other draw functions
8611
+ * @param {Color} [color]
8612
+ * @param {Color} [outlineColor]
8613
+ * @param {number} [lineWidth]
8614
+ * @param {CanvasRenderingContext2D} [context] */
8615
+ drawFillStroke(color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8616
+ {
8617
+ if (color)
8618
+ {
8619
+ context.fillStyle = color.toString();
8620
+ context.fill();
8621
+ }
8622
+ if (outlineColor && lineWidth)
8623
+ {
8624
+ context.lineWidth = lineWidth;
8625
+ context.lineJoin = context.lineCap = 'round';
8626
+ context.strokeStyle = outlineColor.toString();
8627
+ context.stroke();
8628
+ }
8629
+ }
8630
+
8631
+ ///////////////////////////////////////////////////////////////////////////////
8632
+ // helper functions
8633
+
8634
+ /** converts a box2d vec2 to a Vector2
8635
+ * @param {Object} v */
8636
+ vec2From(v)
8637
+ {
8638
+ ASSERT(v instanceof box2d.instance.b2Vec2);
8639
+ return new Vector2(v.get_x(), v.get_y());
8640
+ }
8641
+
8642
+ /** converts a box2d vec2 pointer to a Vector2
8643
+ * @param {Object} v */
8644
+ vec2FromPointer(v)
8645
+ {
8646
+ return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
8647
+ }
8648
+
8649
+ /** converts a Vector2 to a box2 vec2
8650
+ * @param {Vector2} v */
8651
+ vec2dTo(v)
8652
+ {
8653
+ ASSERT(v instanceof Vector2);
8654
+ return new box2d.instance.b2Vec2(v.x, v.y);
8655
+ }
8656
+
8657
+ /** checks if a box2d object is null
8658
+ * @param {Object} o */
8659
+ isNull(o) { return !box2d.instance.getPointer(o); }
8660
+
8661
+ /** casts a box2d object to its correct type
8662
+ * @param {Object} o */
8663
+ castObjectType(o)
8664
+ {
8665
+ switch (o.GetType())
8666
+ {
8667
+ case box2d.instance.b2Shape.e_circle:
8668
+ return box2d.instance.castObject(o, box2d.instance.b2CircleShape);
8669
+ case box2d.instance.b2Shape.e_edge:
8670
+ return box2d.instance.castObject(o, box2d.instance.b2EdgeShape);
8671
+ case box2d.instance.b2Shape.e_polygon:
8672
+ return box2d.instance.castObject(o, box2d.instance.b2PolygonShape);
8673
+ case box2d.instance.b2Shape.e_chain:
8674
+ return box2d.instance.castObject(o, box2d.instance.b2ChainShape);
8675
+ case box2d.instance.e_revoluteJoint:
8676
+ return box2d.instance.castObject(o, box2d.instance.b2RevoluteJoint);
8677
+ case box2d.instance.e_prismaticJoint:
8678
+ return box2d.instance.castObject(o, box2d.instance.b2PrismaticJoint);
8679
+ case box2d.instance.e_distanceJoint:
8680
+ return box2d.instance.castObject(o, box2d.instance.b2DistanceJoint);
8681
+ case box2d.instance.e_pulleyJoint:
8682
+ return box2d.instance.castObject(o, box2d.instance.b2PulleyJoint);
8683
+ case box2d.instance.e_mouseJoint:
8684
+ return box2d.instance.castObject(o, box2d.instance.b2MouseJoint);
8685
+ case box2d.instance.e_gearJoint:
8686
+ return box2d.instance.castObject(o, box2d.instance.b2GearJoint);
8687
+ case box2d.instance.e_wheelJoint:
8688
+ return box2d.instance.castObject(o, box2d.instance.b2WheelJoint);
8689
+ case box2d.instance.e_weldJoint:
8690
+ return box2d.instance.castObject(o, box2d.instance.b2WeldJoint);
8691
+ case box2d.instance.e_frictionJoint:
8692
+ return box2d.instance.castObject(o, box2d.instance.b2FrictionJoint);
8693
+ case box2d.instance.e_ropeJoint:
8694
+ return box2d.instance.castObject(o, box2d.instance.b2RopeJoint);
8695
+ case box2d.instance.e_motorJoint:
8696
+ return box2d.instance.castObject(o, box2d.instance.b2MotorJoint);
8697
+ }
8698
+
8699
+ ASSERT(false, 'Unknown box2d object type');
8700
+ }
8701
+ }
8702
+
8703
+ ///////////////////////////////////////////////////////////////////////////////
8704
+ /** Box2d Init - Call with await before starting LittleJS to init box2d
8705
+ * @return {Promise<Box2dPlugin>}
8706
+ * @memberof Box2D */
8707
+ async function box2dInit()
8708
+ {
8709
+ // load box2d
8710
+ new Box2dPlugin(await Box2D());
8711
+ setupDebugDraw();
8712
+ engineAddPlugin(box2dUpdate, box2dRender);
8713
+ return box2d;
8714
+
8715
+ // add the box2d plugin to the engine
8716
+ function box2dUpdate()
8717
+ {
8718
+ if (!paused)
8719
+ box2d.step();
8720
+ }
8721
+ function box2dRender()
8722
+ {
8723
+ if (box2dDebug || debugPhysics && debugOverlay)
8724
+ box2d.world.DrawDebugData();
8725
+ }
8726
+
8727
+ // box2d debug drawing
8728
+ function setupDebugDraw()
8729
+ {
8730
+ // setup debug draw
8731
+ const debugDraw = new box2d.instance.JSDraw();
8732
+ const box2dColor = (c)=> new Color(c.get_r(), c.get_g(), c.get_b());
8733
+ const box2dColorPointer = (c)=>
8734
+ box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
8735
+ const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
8736
+ const getPointsList = (vertices, vertexCount) =>
8737
+ {
8738
+ const points = [];
8739
+ for (let i=vertexCount; i--;)
8740
+ points.push(box2d.vec2FromPointer(vertices+i*8));
8741
+ return points;
8742
+ }
8743
+ debugDraw.DrawSegment = function(point1, point2, color)
8744
+ {
8745
+ color = getDebugColor(color);
8746
+ point1 = box2d.vec2FromPointer(point1);
8747
+ point2 = box2d.vec2FromPointer(point2);
8748
+ box2d.drawLine(vec2(), 0, point1, point2, color, undefined, overlayContext);
8749
+ };
8750
+ debugDraw.DrawPolygon = function(vertices, vertexCount, color)
8751
+ {
8752
+ color = getDebugColor(color);
8753
+ const points = getPointsList(vertices, vertexCount);
8754
+ box2d.drawPoly(vec2(), 0, points, undefined, color, undefined, overlayContext);
8755
+ };
8756
+ debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
8757
+ {
8758
+ color = getDebugColor(color);
8759
+ const points = getPointsList(vertices, vertexCount);
8760
+ box2d.drawPoly(vec2(), 0, points, color, color, undefined, overlayContext);
8761
+ };
8762
+ debugDraw.DrawCircle = function(center, radius, color)
8763
+ {
8764
+ color = getDebugColor(color);
8765
+ center = box2d.vec2FromPointer(center);
8766
+ box2d.drawCircle(center, radius, undefined, color, undefined, overlayContext);
8767
+ };
8768
+ debugDraw.DrawSolidCircle = function(center, radius, axis, color)
8769
+ {
8770
+ color = getDebugColor(color);
8771
+ center = box2d.vec2FromPointer(center);
8772
+ axis = box2d.vec2FromPointer(axis).scale(radius);
8773
+ box2d.drawCircle(center, radius, color, color, undefined, overlayContext);
8774
+ box2d.drawLine(center, 0, vec2(), axis, color, undefined, overlayContext);
8775
+ };
8776
+ debugDraw.DrawTransform = function(transform)
8777
+ {
8778
+ transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
8779
+ const pos = vec2(transform.get_p());
8780
+ const angle = -transform.get_q().GetAngle();
8781
+ const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
8782
+ const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
8783
+ box2d.drawLine(pos, angle, vec2(), p1, c1, undefined, overlayContext);
8784
+ box2d.drawLine(pos, angle, vec2(), p2, c2, undefined, overlayContext);
8785
+ }
8786
+
8787
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
8788
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_jointBit);
8789
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_aabbBit);
8790
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_pairBit);
8791
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_centerOfMassBit);
8792
+ box2d.world.SetDebugDraw(debugDraw);
8793
+ }
8794
+ }
8795
+