littlejsengine 1.11.13 → 1.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/littlejs.d.ts +1418 -109
  2. package/dist/littlejs.esm.js +3495 -581
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +3258 -399
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +3147 -379
  7. package/examples/box2d/game.js +73 -45
  8. package/examples/box2d/gameObjects.js +96 -92
  9. package/examples/box2d/index.html +2 -7
  10. package/examples/box2d/scenes.js +82 -92
  11. package/examples/breakout/game.js +62 -30
  12. package/examples/breakout/gameObjects.js +45 -47
  13. package/examples/breakout/index.html +1 -5
  14. package/examples/breakoutTutorial/game.js +36 -29
  15. package/examples/breakoutTutorial/index.html +1 -3
  16. package/examples/empty/game.js +5 -2
  17. package/examples/empty/index.html +1 -3
  18. package/examples/htmlMenu/game.js +25 -19
  19. package/examples/htmlMenu/index.html +5 -7
  20. package/examples/index.html +2 -3
  21. package/examples/module/game.js +32 -34
  22. package/examples/module/index.html +1 -1
  23. package/examples/particles/index.html +27 -22
  24. package/examples/platformer/game.js +71 -48
  25. package/examples/platformer/gameCharacter.js +42 -34
  26. package/examples/platformer/gameEffects.js +82 -75
  27. package/examples/platformer/gameLevel.js +67 -38
  28. package/examples/platformer/{gameLevelData.js → gameLevelData.json} +1 -11
  29. package/examples/platformer/gameObjects.js +70 -64
  30. package/examples/platformer/gamePlayer.js +12 -7
  31. package/examples/platformer/index.html +1 -9
  32. package/examples/puzzle/game.js +58 -42
  33. package/examples/puzzle/index.html +1 -3
  34. package/examples/shorts/base.html +2 -2
  35. package/examples/shorts/particles.js +1 -1
  36. package/examples/shorts/platformer.js +1 -1
  37. package/examples/shorts/tileLayer.js +5 -6
  38. package/examples/shorts/tiles.png +0 -0
  39. package/examples/starter/build.js +4 -4
  40. package/examples/starter/game.js +9 -12
  41. package/examples/starter/index.html +13 -13
  42. package/examples/stress/index.html +44 -43
  43. package/examples/typescript/build.js +17 -18
  44. package/examples/typescript/game.js +32 -34
  45. package/examples/typescript/game.ts +32 -34
  46. package/examples/typescript/index.html +1 -1
  47. package/examples/uiSystem/game.js +33 -36
  48. package/examples/uiSystem/index.html +1 -5
  49. package/package.json +3 -3
  50. package/plugins/box2d.js +1556 -640
  51. package/plugins/{Box2D_v2.3.1_min.wasm.js → box2d.wasm.js} +1 -1
  52. package/plugins/newgrounds.js +7 -8
  53. package/plugins/pluginExport.js +50 -0
  54. package/plugins/postProcess.js +94 -93
  55. package/plugins/uiSystem.js +145 -161
  56. package/plugins/zzfxm.js +163 -0
  57. package/reference.md +29 -27
  58. package/src/engine.js +15 -20
  59. package/src/engineAudio.js +74 -204
  60. package/src/engineBuild.js +29 -4
  61. package/src/engineDebug.js +105 -9
  62. package/src/engineDraw.js +27 -14
  63. package/src/engineExport.js +12 -8
  64. package/src/engineInput.js +3 -1
  65. package/src/engineObject.js +18 -14
  66. package/src/engineParticles.js +6 -1
  67. package/src/engineRelease.js +6 -1
  68. package/src/engineSettings.js +8 -8
  69. package/src/engineTileLayer.js +179 -96
  70. package/src/engineUtilities.js +5 -11
  71. package/src/engineWebGL.js +19 -7
  72. package/examples/starter/build/index.html +0 -2
  73. package/examples/starter/build/index.js +0 -1
  74. package/examples/starter/build/tiles.png +0 -0
  75. package/examples/starter/game.zip +0 -0
  76. package/examples/typescript/build/dist/littlejs.esm.js +0 -4834
  77. package/examples/typescript/build/examples/typescript/build.js +0 -24
  78. package/examples/typescript/build/examples/typescript/game.js +0 -102
  79. /package/plugins/{Box2D_v2.3.1_min.wasm.wasm → box2d.wasm.wasm} +0 -0
@@ -37,7 +37,12 @@ function debugClear (){}
37
37
  function debugScreenshot (){}
38
38
  function debugSaveCanvas (){}
39
39
  function debugSaveText (){}
40
- function debugSaveDataURL(){}
40
+ function debugSaveDataURL(){}
41
+ function debugShowErrors(){}
42
+ function debugVideoCaptureIsActive(){ return false; }
43
+ function debugVideoCaptureStart (){}
44
+ function debugVideoCaptureStop (){}
45
+ function debugVideoCaptureUpdate(){}
41
46
  /**
42
47
  * LittleJS Utility Classes and Functions
43
48
  * - General purpose math library
@@ -131,7 +136,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
131
136
  * @returns {number}
132
137
  * @memberof Utilities */
133
138
  function lerpWrap(percent, valueA, valueB, wrapSize=1)
134
- { return valueB + clamp(percent) * distanceWrap(valueA, valueB, wrapSize); }
139
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
135
140
 
136
141
  /** Returns signed wrapped distance between the two angles passed in
137
142
  * @param {number} angleA
@@ -329,23 +334,17 @@ class RandomGenerator
329
334
  ///////////////////////////////////////////////////////////////////////////////
330
335
 
331
336
  /**
332
- * Create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
333
- * @param {Vector2|number} [x]
334
- * @param {number} [y]
337
+ * Create a 2d vector, can take 1 or 2 scalar values
338
+ * @param {number} [x]
339
+ * @param {number} [y] - if y is undefined, x is used for both
335
340
  * @return {Vector2}
336
341
  * @example
337
342
  * let a = vec2(0, 1); // vector with coordinates (0, 1)
338
- * let b = vec2(a); // copy a into b
339
343
  * a = vec2(5); // set a to (5, 5)
340
344
  * b = vec2(); // set b to (0, 0)
341
345
  * @memberof Utilities
342
346
  */
343
- function vec2(x=0, y)
344
- {
345
- return typeof x == 'number' ?
346
- new Vector2(x, y == undefined? x : y) :
347
- new Vector2(x.x, x.y);
348
- }
347
+ function vec2(x=0, y) { return new Vector2(x, y == undefined? x : y); }
349
348
 
350
349
  /**
351
350
  * Check if object is a valid Vector2
@@ -1003,7 +1002,7 @@ let canvasMaxSize = vec2(1920, 1080);
1003
1002
  * @memberof Settings */
1004
1003
  let canvasFixedSize = vec2();
1005
1004
 
1006
- /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
1005
+ /** Use nearest neighbor canvas scaling for more pixelated look
1007
1006
  * - Must be set before startup to take effect
1008
1007
  * - If enabled sets css image-rendering:pixelated
1009
1008
  * @type {boolean}
@@ -1113,11 +1112,11 @@ let objectDefaultFriction = .8;
1113
1112
  * @memberof Settings */
1114
1113
  let objectMaxSpeed = 1;
1115
1114
 
1116
- /** How much gravity to apply to objects along the Y axis, negative is down
1117
- * @type {number}
1115
+ /** How much gravity to apply to objects, negative Y is down
1116
+ * @type {Vector2}
1118
1117
  * @default
1119
1118
  * @memberof Settings */
1120
- let gravity = 0;
1119
+ let gravity = vec2();
1121
1120
 
1122
1121
  /** Scales emit rate of particles, useful for low graphics mode (0 disables particle emitters)
1123
1122
  * @type {number}
@@ -1343,8 +1342,8 @@ function setObjectDefaultFriction(friction) { objectDefaultFriction = friction;
1343
1342
  * @memberof Settings */
1344
1343
  function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
1345
1344
 
1346
- /** Set how much gravity to apply to objects along the Y axis
1347
- * @param {number} newGravity
1345
+ /** Set how much gravity to apply to objects
1346
+ * @param {Vector2} newGravity
1348
1347
  * @memberof Settings */
1349
1348
  function setGravity(newGravity) { gravity = newGravity; }
1350
1349
 
@@ -1409,8 +1408,8 @@ function setSoundEnable(enable) { soundEnable = enable; }
1409
1408
  function setSoundVolume(volume)
1410
1409
  {
1411
1410
  soundVolume = volume;
1412
- if (soundEnable && !headlessMode && audioGainNode)
1413
- audioGainNode.gain.value = volume; // update gain immediately
1411
+ if (soundEnable && !headlessMode && audioMasterGain)
1412
+ audioMasterGain.gain.value = volume; // update gain immediately
1414
1413
  }
1415
1414
 
1416
1415
  /** Set default range where sound no longer plays
@@ -1612,7 +1611,10 @@ class EngineObject
1612
1611
  this.velocity.x *= this.damping;
1613
1612
  this.velocity.y *= this.damping;
1614
1613
  if (this.mass) // don't apply gravity to static objects
1615
- this.velocity.y += gravity * this.gravityScale;
1614
+ {
1615
+ this.velocity.x += gravity.x * this.gravityScale;
1616
+ this.velocity.y += gravity.y * this.gravityScale;
1617
+ }
1616
1618
  this.pos.x += this.velocity.x;
1617
1619
  this.pos.y += this.velocity.y;
1618
1620
  this.angle += this.angleVelocity *= this.angleDamping;
@@ -1627,9 +1629,9 @@ class EngineObject
1627
1629
  if (this.groundObject)
1628
1630
  {
1629
1631
  // apply friction in local space of ground object
1630
- const groundSpeed = this.groundObject != this && this.groundObject.velocity ?
1631
- this.groundObject.velocity.x : 0;
1632
- this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
1632
+ const friction = max(this.friction, this.groundObject.friction);
1633
+ const groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
1634
+ this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * friction;
1633
1635
  this.groundObject = undefined;
1634
1636
  //debugOverlay && debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
1635
1637
  }
@@ -1671,7 +1673,7 @@ class EngineObject
1671
1673
 
1672
1674
  // check for collision
1673
1675
  const sizeBoth = this.size.add(o.size);
1674
- const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity; // prefer to push up if small delta
1676
+ const smallStepUp = (oldPos.y - o.pos.y)*2 > sizeBoth.y + gravity.y; // prefer to push up if small delta
1675
1677
  const isBlockedX = abs(oldPos.y - o.pos.y)*2 < sizeBoth.y;
1676
1678
  const isBlockedY = abs(oldPos.x - o.pos.x)*2 < sizeBoth.x;
1677
1679
  const elasticity = max(this.elasticity, o.elasticity);
@@ -1733,19 +1735,21 @@ class EngineObject
1733
1735
  if (this.collideTiles)
1734
1736
  {
1735
1737
  // check collision against tiles
1736
- if (tileCollisionTest(this.pos, this.size, this))
1738
+ const hitLayer = tileCollisionTest(this.pos, this.size, this)
1739
+ if (hitLayer)
1737
1740
  {
1738
1741
  // if already was stuck in collision, don't do anything
1739
1742
  // this should not happen unless something starts in collision
1740
1743
  if (!tileCollisionTest(oldPos, this.size, this))
1741
1744
  {
1742
1745
  // test which side we bounced off (or both if a corner)
1743
- const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
1744
- const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
1745
- if (isBlockedY || !isBlockedX)
1746
+ const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
1747
+ const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
1748
+ if (blockedLayerY || !blockedLayerX)
1746
1749
  {
1747
1750
  // bounce velocity
1748
- this.velocity.y *= -this.elasticity;
1751
+ const elasticity = max(this.elasticity, hitLayer.elasticity);
1752
+ this.velocity.y *= -elasticity;
1749
1753
 
1750
1754
  if (wasMovingDown)
1751
1755
  {
@@ -1754,9 +1758,8 @@ class EngineObject
1754
1758
  const epsilon = .0001;
1755
1759
  this.pos.y = (oldPos.y-this.size.y/2|0)+this.size.y/2+epsilon;
1756
1760
 
1757
- // set ground object to self for tile collision
1758
- // TODO: rework system so tile collision is its own object
1759
- this.groundObject = this;
1761
+ // set ground object for tile collision
1762
+ this.groundObject = hitLayer;
1760
1763
  }
1761
1764
  else
1762
1765
  {
@@ -1765,7 +1768,7 @@ class EngineObject
1765
1768
  this.groundObject = undefined;
1766
1769
  }
1767
1770
  }
1768
- if (isBlockedX)
1771
+ if (blockedLayerX)
1769
1772
  {
1770
1773
  // move to previous position and bounce
1771
1774
  this.pos.x = oldPos.x;
@@ -2076,6 +2079,8 @@ class TextureInfo
2076
2079
  this.image = image;
2077
2080
  /** @property {Vector2} - size of the image */
2078
2081
  this.size = vec2(image.width, image.height);
2082
+ /** @property {Vector2} - inverse of the size, cached for rendering */
2083
+ this.sizeInverse = vec2(1/image.width, 1/image.height);
2079
2084
  /** @property {WebGLTexture} - webgl texture */
2080
2085
  this.glTexture = glEnable && glCreateTexture(image);
2081
2086
  }
@@ -2121,19 +2126,19 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
2121
2126
  * @param {Color} [color=(1,1,1,1)] - Color to modulate with
2122
2127
  * @param {number} [angle] - Angle to rotate by
2123
2128
  * @param {boolean} [mirror] - If true image is flipped along the Y axis
2124
- * @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
2129
+ * @param {Color} [additiveColor] - Additive color to be applied if any
2125
2130
  * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
2126
2131
  * @param {boolean} [screenSpace=false] - If true the pos and size are in screen space
2127
2132
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
2128
2133
  * @memberof Draw */
2129
2134
  function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2130
- angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
2135
+ angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2131
2136
  {
2132
2137
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2133
2138
  ASSERT(typeof tileInfo !== 'number' || !tileInfo,
2134
2139
  'this is an old style calls, to fix replace it with tile(tileIndex, tileSize)');
2135
2140
  ASSERT(isVector2(pos) && isVector2(size));
2136
- ASSERT(isColor(color) && isColor(additiveColor));
2141
+ ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)));
2137
2142
 
2138
2143
  const textureInfo = tileInfo && tileInfo.getTextureInfo();
2139
2144
  if (useWebGL)
@@ -2144,21 +2149,30 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
2144
2149
  pos = screenToWorld(pos);
2145
2150
  size = size.scale(1/cameraScale);
2146
2151
  }
2147
-
2148
2152
  if (textureInfo)
2149
2153
  {
2150
2154
  // calculate uvs and render
2151
- const sizeInverse = vec2(1).divide(textureInfo.size);
2155
+ const sizeInverse = textureInfo.sizeInverse;
2152
2156
  const x = tileInfo.pos.x * sizeInverse.x;
2153
2157
  const y = tileInfo.pos.y * sizeInverse.y;
2154
2158
  const w = tileInfo.size.x * sizeInverse.x;
2155
2159
  const h = tileInfo.size.y * sizeInverse.y;
2156
- const tileImageFixBleed = sizeInverse.scale(tileFixBleedScale);
2157
2160
  glSetTexture(textureInfo.glTexture);
2158
- glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2159
- x + tileImageFixBleed.x, y + tileImageFixBleed.y,
2160
- x - tileImageFixBleed.x + w, y - tileImageFixBleed.y + h,
2161
- color.rgbaInt(), additiveColor.rgbaInt());
2161
+ if (tileFixBleedScale)
2162
+ {
2163
+ const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
2164
+ const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
2165
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2166
+ x + tileImageFixBleedX, y + tileImageFixBleedY,
2167
+ x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
2168
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2169
+ }
2170
+ else
2171
+ {
2172
+ glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2173
+ x, y, x + w, y + h,
2174
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
2175
+ }
2162
2176
  }
2163
2177
  else
2164
2178
  {
@@ -2370,16 +2384,15 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
2370
2384
  function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
2371
2385
  {
2372
2386
  context.fillStyle = color.toString();
2373
- context.lineWidth = lineWidth;
2374
2387
  context.strokeStyle = lineColor.toString();
2388
+ context.lineWidth = lineWidth;
2375
2389
  context.textAlign = textAlign;
2376
2390
  context.font = size + 'px '+ font;
2377
2391
  context.textBaseline = 'middle';
2378
2392
  context.lineJoin = 'round';
2379
2393
 
2380
- pos = pos.copy();
2381
-
2382
2394
  const lines = (text+'').split('\n');
2395
+ pos = pos.copy();
2383
2396
  pos.y -= (lines.length-1) * size/2; // center text vertically
2384
2397
  lines.forEach(line=>
2385
2398
  {
@@ -2449,7 +2462,10 @@ class FontImage
2449
2462
  {
2450
2463
  // load default font image
2451
2464
  if (!engineFontImage)
2452
- (engineFontImage = new Image).src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2465
+ {
2466
+ engineFontImage = new Image;
2467
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
2468
+ }
2453
2469
 
2454
2470
  this.image = image || engineFontImage;
2455
2471
  this.tileSize = tileSize;
@@ -2852,7 +2868,9 @@ function gamepadsUpdate()
2852
2868
  const button = gamepad.buttons[j];
2853
2869
  const wasDown = gamepadIsDown(j,i);
2854
2870
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
2855
- isUsingGamepad ||= !i && button.pressed;
2871
+ if (!button.value || button.value > .9) // must be a full press
2872
+ if (!i && button.pressed)
2873
+ isUsingGamepad = true;
2856
2874
  }
2857
2875
 
2858
2876
  if (gamepadDirectionEmulateStick)
@@ -3080,16 +3098,15 @@ let audioContext = new AudioContext;
3080
3098
  /** Master gain node for all audio to pass through
3081
3099
  * @type {GainNode}
3082
3100
  * @memberof Audio */
3083
- let audioGainNode;
3101
+ let audioMasterGain;
3084
3102
 
3085
3103
  function audioInit()
3086
3104
  {
3087
3105
  if (!soundEnable || headlessMode) return;
3088
3106
 
3089
- // (createGain is more widely supported then GainNode constructor)
3090
- audioGainNode = audioContext.createGain();
3091
- audioGainNode.connect(audioContext.destination);
3092
- audioGainNode.gain.value = soundVolume; // set starting value
3107
+ audioMasterGain = audioContext.createGain();
3108
+ audioMasterGain.connect(audioContext.destination);
3109
+ audioMasterGain.gain.value = soundVolume; // set starting value
3093
3110
  }
3094
3111
 
3095
3112
  ///////////////////////////////////////////////////////////////////////////////
@@ -3218,6 +3235,8 @@ class Sound
3218
3235
  isLoading() { return !this.sampleChannels; }
3219
3236
  }
3220
3237
 
3238
+ ///////////////////////////////////////////////////////////////////////////////
3239
+
3221
3240
  /**
3222
3241
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
3223
3242
  * - this can be used to play wave, mp3, and ogg files
@@ -3269,59 +3288,7 @@ function playAudioFile(filename, volume=1, loop=false)
3269
3288
  return new SoundWave(filename,0,0,0, s=>s.play(undefined, volume, 1, 1, loop));
3270
3289
  }
3271
3290
 
3272
- /**
3273
- * Music Object - Stores a zzfx music track for later use
3274
- *
3275
- * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
3276
- * @example
3277
- * // create some music
3278
- * const music_example = new Music(
3279
- * [
3280
- * [ // instruments
3281
- * [,0,400] // simple note
3282
- * ],
3283
- * [ // patterns
3284
- * [ // pattern 1
3285
- * [ // channel 0
3286
- * 0, -1, // instrument 0, left speaker
3287
- * 1, 0, 9, 1 // channel notes
3288
- * ],
3289
- * [ // channel 1
3290
- * 0, 1, // instrument 0, right speaker
3291
- * 0, 12, 17, -1 // channel notes
3292
- * ]
3293
- * ],
3294
- * ],
3295
- * [0, 0, 0, 0], // sequence, play pattern 0 four times
3296
- * 90 // BPM
3297
- * ]);
3298
- *
3299
- * // play the music
3300
- * music_example.play();
3301
- */
3302
- class Music extends Sound
3303
- {
3304
- /** Create a music object and cache the zzfx music samples for later use
3305
- * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
3306
- */
3307
- constructor(zzfxMusic)
3308
- {
3309
- super(undefined);
3310
-
3311
- if (!soundEnable || headlessMode) return;
3312
- this.randomness = 0;
3313
- this.sampleChannels = zzfxM(...zzfxMusic);
3314
- this.sampleRate = zzfxR;
3315
- }
3316
-
3317
- /** Play the music
3318
- * @param {number} [volume=1] - How much to scale volume by
3319
- * @param {boolean} [loop] - True if the music should loop
3320
- * @return {AudioBufferSourceNode} - The audio source node
3321
- */
3322
- playMusic(volume, loop=false)
3323
- { return super.play(undefined, volume, 1, 1, loop); }
3324
- }
3291
+ ///////////////////////////////////////////////////////////////////////////////
3325
3292
 
3326
3293
  /** Speak text with passed in settings
3327
3294
  * @param {string} text - The text to speak
@@ -3393,7 +3360,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3393
3360
  // create and connect gain node
3394
3361
  gainNode = gainNode || audioContext.createGain();
3395
3362
  gainNode.gain.value = volume;
3396
- gainNode.connect(audioGainNode);
3363
+ gainNode.connect(audioMasterGain);
3397
3364
 
3398
3365
  // connect source to stereo panner and gain
3399
3366
  const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
@@ -3413,7 +3380,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
3413
3380
  }
3414
3381
 
3415
3382
  ///////////////////////////////////////////////////////////////////////////////
3416
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.1 by Frank Force
3383
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
3417
3384
 
3418
3385
  /** Generate and play a ZzFX sound
3419
3386
  *
@@ -3455,21 +3422,45 @@ const zzfxR = 44100;
3455
3422
  */
3456
3423
  function zzfxG
3457
3424
  (
3458
- // parameters
3459
- volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0,
3460
- release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
3461
- pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
3462
- bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
3425
+ volume = 1,
3426
+ randomness = .05,
3427
+ frequency = 220,
3428
+ attack = 0,
3429
+ sustain = 0,
3430
+ release = .1,
3431
+ shape = 0,
3432
+ shapeCurve = 1,
3433
+ slide = 0,
3434
+ deltaSlide = 0,
3435
+ pitchJump = 0,
3436
+ pitchJumpTime = 0,
3437
+ repeatTime = 0,
3438
+ noise = 0,
3439
+ modulation = 0,
3440
+ bitCrush = 0,
3441
+ delay = 0,
3442
+ sustainVolume = 1,
3443
+ decay = 0,
3444
+ tremolo = 0,
3445
+ filter = 0
3463
3446
  )
3464
3447
  {
3465
- // LJS Note: ZZFX modded so randomness is handled by Sound class
3466
-
3467
3448
  // init parameters
3468
- let PI2 = PI*2, sampleRate = zzfxR,
3449
+ let sampleRate = zzfxR,
3450
+ PI2 = PI*2,
3469
3451
  startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
3470
3452
  startFrequency = frequency *=
3471
- rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
3472
- b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
3453
+ (1 + rand(randomness,-randomness)) * PI2 / sampleRate,
3454
+ modOffset = 0, // modulation offset
3455
+ repeat = 0, // repeat offset
3456
+ crush = 0, // bit crush offset
3457
+ jump = 1, // pitch jump timer
3458
+ length, // sample length
3459
+ b = [], // sample buffer
3460
+ t = 0, // sample time
3461
+ i = 0, // sample index
3462
+ s = 0, // sample value
3463
+ f, // wave frequency
3473
3464
 
3474
3465
  // biquad LP/HP filter
3475
3466
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
@@ -3479,35 +3470,37 @@ function zzfxG
3479
3470
  b1 = -(sign(filter) + cos) / a0, b2 = b0,
3480
3471
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
3481
3472
 
3482
- // scale by sample rate
3483
- attack = attack * sampleRate + 9; // minimum attack to prevent pop
3484
- decay *= sampleRate;
3485
- sustain *= sampleRate;
3486
- release *= sampleRate;
3487
- delay *= sampleRate;
3488
- deltaSlide *= 500 * PI2 / sampleRate**3;
3489
- modulation *= PI2 / sampleRate;
3490
- pitchJump *= PI2 / sampleRate;
3491
- pitchJumpTime *= sampleRate;
3492
- repeatTime = repeatTime * sampleRate | 0;
3473
+ // scale by sample rate
3474
+ const minAttack = 9; // prevent pop if attack is 0
3475
+ attack = attack * sampleRate || minAttack;
3476
+ decay *= sampleRate;
3477
+ sustain *= sampleRate;
3478
+ release *= sampleRate;
3479
+ delay *= sampleRate;
3480
+ deltaSlide *= 500 * PI2 / sampleRate**3;
3481
+ modulation *= PI2 / sampleRate;
3482
+ pitchJump *= PI2 / sampleRate;
3483
+ pitchJumpTime *= sampleRate;
3484
+ repeatTime = repeatTime * sampleRate | 0;
3493
3485
 
3494
3486
  // generate waveform
3495
3487
  for(length = attack + decay + sustain + release + delay | 0;
3496
- i < length; b[i++] = s * volume) // sample
3488
+ i < length; b[i++] = s * volume) // sample
3497
3489
  {
3498
- if (!(++c%(bitCrush*100|0))) // bit crush
3490
+ if (!(++crush%(bitCrush*100|0))) // bit crush
3499
3491
  {
3500
- s = shape? shape>1? shape>2? shape>3? // wave shape
3501
- Math.sin(t**3) : // 4 noise
3502
- clamp(Math.tan(t),1,-1): // 3 tan
3503
- 1-(2*t/PI2%2+2)%2: // 2 saw
3504
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
3505
- Math.sin(t); // 0 sin
3492
+ s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
3493
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
3494
+ Math.sin(t**3) : // 4 noise
3495
+ Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
3496
+ 1-(2*t/PI2%2+2)%2: // 2 saw
3497
+ 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
3498
+ Math.sin(t); // 0 sin
3506
3499
 
3507
3500
  s = (repeatTime ?
3508
3501
  1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
3509
3502
  : 1) *
3510
- sign(s)*(abs(s)**shapeCurve) * // curve
3503
+ (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
3511
3504
  (i < attack ? i/attack : // attack
3512
3505
  i < attack + decay ? // decay
3513
3506
  1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
@@ -3522,209 +3515,77 @@ function zzfxG
3522
3515
  (i<length-delay? 1 : (length-i)/delay) * // release delay
3523
3516
  b[i-delay|0]/2/volume) : s; // sample delay
3524
3517
 
3525
- if (filter) // apply filter
3518
+ if (filter) // apply filter
3526
3519
  s = y1 = b2*x2 + b1*(x2=x1) + b0*(x1=s) - a2*y2 - a1*(y2=y1);
3527
3520
  }
3528
3521
 
3529
3522
  f = (frequency += slide += deltaSlide) *// frequency
3530
- Math.cos(modulation*tm++); // modulation
3523
+ Math.cos(modulation*modOffset++); // modulation
3531
3524
  t += f + f*noise*Math.sin(i**5); // noise
3532
3525
 
3533
- if (j && ++j > pitchJumpTime) // pitch jump
3526
+ if (jump && ++jump > pitchJumpTime) // pitch jump
3534
3527
  {
3535
3528
  frequency += pitchJump; // apply pitch jump
3536
3529
  startFrequency += pitchJump; // also apply to start
3537
- j = 0; // stop pitch jump time
3530
+ jump = 0; // stop pitch jump time
3538
3531
  }
3539
3532
 
3540
- if (repeatTime && !(++r % repeatTime)) // repeat
3533
+ if (repeatTime && !(++repeat % repeatTime)) // repeat
3541
3534
  {
3542
- frequency = startFrequency; // reset frequency
3543
- slide = startSlide; // reset slide
3544
- j = j || 1; // reset pitch jump time
3535
+ frequency = startFrequency; // reset frequency
3536
+ slide = startSlide; // reset slide
3537
+ jump ||= 1; // reset pitch jump time
3545
3538
  }
3546
3539
  }
3547
3540
 
3548
- return b;
3541
+ return b; // return sample buffer
3549
3542
  }
3550
-
3551
- ///////////////////////////////////////////////////////////////////////////////
3552
- // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
3553
-
3554
- /** Generate samples for a ZzFM song with given parameters
3555
- * @param {Array} instruments - Array of ZzFX sound parameters
3556
- * @param {Array} patterns - Array of pattern data
3557
- * @param {Array} sequence - Array of pattern indexes
3558
- * @param {number} [BPM] - Playback speed of the song in BPM
3559
- * @return {Array} - Left and right channel sample data
3560
- * @memberof Audio */
3561
- function zzfxM(instruments, patterns, sequence, BPM = 125)
3562
- {
3563
- let i, j, k;
3564
- let instrumentParameters;
3565
- let note;
3566
- let sample;
3567
- let patternChannel;
3568
- let notFirstBeat;
3569
- let stop;
3570
- let instrument;
3571
- let attenuation;
3572
- let outSampleOffset;
3573
- let isSequenceEnd;
3574
- let sampleOffset = 0;
3575
- let nextSampleOffset;
3576
- let sampleBuffer = [];
3577
- let leftChannelBuffer = [];
3578
- let rightChannelBuffer = [];
3579
- let channelIndex = 0;
3580
- let panning = 0;
3581
- let hasMore = 1;
3582
- let sampleCache = {};
3583
- let beatLength = zzfxR / BPM * 60 >> 2;
3584
-
3585
- // for each channel in order until there are no more
3586
- for (; hasMore; channelIndex++) {
3587
-
3588
- // reset current values
3589
- sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
3590
-
3591
- // for each pattern in sequence
3592
- sequence.forEach((patternIndex, sequenceIndex) => {
3593
- // get pattern for current channel, use empty 1 note pattern if none found
3594
- patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
3595
-
3596
- // check if there are more channels
3597
- hasMore |= patterns[patternIndex][channelIndex]&&1;
3598
-
3599
- // get next offset, use the length of first channel
3600
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
3601
- // for each beat in pattern, plus one extra if end of sequence
3602
- isSequenceEnd = sequenceIndex == sequence.length - 1;
3603
- for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
3604
-
3605
- // <channel-note>
3606
- note = patternChannel[i];
3607
-
3608
- // stop if end, different instrument or new note
3609
- stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
3610
- instrument != (patternChannel[0] || 0) || note | 0;
3611
-
3612
- // fill buffer with samples for previous beat, most cpu intensive part
3613
- for (j = 0; j < beatLength && notFirstBeat;
3614
-
3615
- // fade off attenuation at end of beat if stopping note, prevents clicking
3616
- j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
3617
- ) {
3618
- // copy sample to stereo buffers with panning
3619
- sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
3620
- leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
3621
- rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
3622
- }
3623
-
3624
- // set up for next note
3625
- if (note) {
3626
- // set attenuation
3627
- attenuation = note % 1;
3628
- panning = patternChannel[1] || 0;
3629
- if (note |= 0) {
3630
- // get cached sample
3631
- sampleBuffer = sampleCache[
3632
- [
3633
- instrument = patternChannel[sampleOffset = 0] || 0,
3634
- note
3635
- ]
3636
- ] = sampleCache[[instrument, note]] || (
3637
- // add sample to cache
3638
- instrumentParameters = [...instruments[instrument]],
3639
- instrumentParameters[2] *= 2 ** ((note - 12) / 12),
3640
-
3641
- // allow negative values to stop notes
3642
- note > 0 ? zzfxG(...instrumentParameters) : []
3643
- );
3644
- }
3645
- }
3646
- }
3647
-
3648
- // update the sample offset
3649
- outSampleOffset = nextSampleOffset;
3650
- });
3651
- }
3652
-
3653
- return [leftChannelBuffer, rightChannelBuffer];
3654
- }
3543
+
3655
3544
  /**
3656
3545
  * LittleJS Tile Layer System
3657
3546
  * - Caches arrays of tiles to off screen canvas for fast rendering
3658
3547
  * - Unlimited numbers of layers, allocates canvases as needed
3659
- * - Interfaces with EngineObject for collision
3660
- * - Collision layer is separate from visible layers
3661
- * - It is recommended to have a visible layer that matches the collision
3662
3548
  * - Tile layers can be drawn to using their context with canvas2d
3663
3549
  * - Drawn directly to the main canvas without using WebGL
3550
+ * - Tile layers can also have collision with EngineObjects
3664
3551
  * @namespace TileCollision
3665
3552
  */
3666
3553
 
3667
3554
 
3668
3555
 
3669
- /** The tile collision layer grid, use setTileCollisionData and getTileCollisionData to access
3670
- * @type {Array<number>}
3671
- * @memberof TileCollision */
3672
- let tileCollision = [];
3673
-
3674
- /** Size of the tile collision layer 2d grid
3675
- * @type {Vector2}
3676
- * @memberof TileCollision */
3677
- let tileCollisionSize = vec2();
3678
-
3679
- /** Clear and initialize tile collision
3680
- * @param {Vector2} size - width and height of tile collision 2d grid
3681
- * @memberof TileCollision */
3682
- function initTileCollision(size)
3683
- {
3684
- tileCollisionSize = size;
3685
- tileCollision = [];
3686
- for (let i=tileCollision.length = tileCollisionSize.area(); i--;)
3687
- tileCollision[i] = 0;
3688
- }
3556
+ ///////////////////////////////////////////////////////////////////////////////
3557
+ // Tile Layer System
3689
3558
 
3690
- /** Set tile collision data for a given cell in the grid
3691
- * @param {Vector2} pos
3692
- * @param {number} [data]
3559
+ /** Keep track of all tile layers with collision
3560
+ * @type {Array<TileCollisionLayer>}
3693
3561
  * @memberof TileCollision */
3694
- function setTileCollisionData(pos, data=0)
3695
- {
3696
- pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
3697
- }
3562
+ let tileCollisionLayers = [];
3698
3563
 
3699
3564
  /** Get tile collision data for a given cell in the grid
3700
- * @param {Vector2} pos
3701
- * @return {number}
3702
- * @memberof TileCollision */
3565
+ * @param {Vector2} pos
3566
+ * @return {number}
3567
+ * @memberof TileCollision */
3703
3568
  function getTileCollisionData(pos)
3704
3569
  {
3705
- return pos.arrayCheck(tileCollisionSize) ? tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] : 0;
3570
+ // check all tile collision layers
3571
+ for (const layer of tileCollisionLayers)
3572
+ if (pos.arrayCheck(layer.size))
3573
+ return layer.getCollisionData(pos);
3574
+ return 0;
3706
3575
  }
3707
3576
 
3708
- /** Check if collision with another object should occur
3577
+ /** Check if a tile layer collides with another object
3709
3578
  * @param {Vector2} pos
3710
3579
  * @param {Vector2} [size=(0,0)]
3711
3580
  * @param {EngineObject} [object]
3712
- * @return {boolean}
3581
+ * @return {TileCollisionLayer}
3713
3582
  * @memberof TileCollision */
3714
3583
  function tileCollisionTest(pos, size=vec2(), object)
3715
3584
  {
3716
- const minX = max(pos.x - size.x/2|0, 0);
3717
- const minY = max(pos.y - size.y/2|0, 0);
3718
- const maxX = min(pos.x + size.x/2, tileCollisionSize.x);
3719
- const maxY = min(pos.y + size.y/2, tileCollisionSize.y);
3720
- for (let y = minY; y < maxY; ++y)
3721
- for (let x = minX; x < maxX; ++x)
3722
- {
3723
- const tileData = tileCollision[y*tileCollisionSize.x+x];
3724
- if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
3725
- return true;
3726
- }
3727
- return false;
3585
+ // check all tile collision layers
3586
+ for (const layer of tileCollisionLayers)
3587
+ if (layer.collisionTest(pos, size, object))
3588
+ return layer;
3728
3589
  }
3729
3590
 
3730
3591
  /** Return the center of first tile hit, undefined if nothing was hit.
@@ -3736,49 +3597,17 @@ function tileCollisionTest(pos, size=vec2(), object)
3736
3597
  * @memberof TileCollision */
3737
3598
  function tileCollisionRaycast(posStart, posEnd, object)
3738
3599
  {
3739
- // test if a ray collides with tiles from start to end
3740
- // todo: a way to get the exact hit point, it must still be inside the hit tile
3741
- const delta = posEnd.subtract(posStart);
3742
- const totalLength = delta.length();
3743
- const normalizedDelta = delta.normalize();
3744
- const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
3745
- const flooredPosStart = posStart.floor();
3746
-
3747
- // setup iteration variables
3748
- let pos = flooredPosStart;
3749
- let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
3750
- let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
3751
-
3752
- while (true)
3753
- {
3754
- // check for tile collision
3755
- const tileData = getTileCollisionData(pos);
3756
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
3757
- {
3758
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
3759
- debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
3760
- return pos.add(vec2(.5));
3761
- }
3762
-
3763
- // check if past the end
3764
- if (xi > totalLength && yi > totalLength)
3765
- break;
3766
-
3767
- // get coordinates of the next tile to check
3768
- if (xi > yi)
3769
- pos.y += sign(delta.y), yi += unit.y;
3770
- else
3771
- pos.x += sign(delta.x), xi += unit.x;
3600
+ // check all tile collision layers
3601
+ for (const layer of tileCollisionLayers)
3602
+ {
3603
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object)
3604
+ if (hitPos)
3605
+ return hitPos;
3772
3606
  }
3773
-
3774
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
3775
3607
  }
3776
3608
 
3777
- ///////////////////////////////////////////////////////////////////////////////
3778
- // Tile Layer Rendering System
3779
-
3780
3609
  /**
3781
- * Tile layer data object stores info about how to render a tile
3610
+ * Tile layer data object stores info about how to draw a tile
3782
3611
  * @example
3783
3612
  * // create tile layer data with tile index 0 and random orientation and color
3784
3613
  * const tileIndex = 0;
@@ -3810,28 +3639,27 @@ class TileLayerData
3810
3639
  clear() { this.tile = this.direction = 0; this.mirror = false; this.color = new Color; }
3811
3640
  }
3812
3641
 
3642
+ ///////////////////////////////////////////////////////////////////////////////
3813
3643
  /**
3814
3644
  * Tile Layer - cached rendering system for tile layers
3815
3645
  * - Each Tile layer is rendered to an off screen canvas
3816
3646
  * - To allow dynamic modifications, layers are rendered using canvas 2d
3817
3647
  * - Some devices like mobile phones are limited to 4k texture resolution
3818
- * - So with 16x16 tiles this limits layers to 256x256 on mobile devices
3648
+ * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
3819
3649
  * @extends EngineObject
3820
3650
  * @example
3821
- * // create tile collision and visible tile layer
3822
- * initTileCollision(vec2(200,100));
3823
- * const tileLayer = new TileLayer();
3651
+ * const tileLayer = new TileLayer(vec2(), vec2(200,100));
3824
3652
  */
3825
3653
  class TileLayer extends EngineObject
3826
3654
  {
3827
3655
  /** Create a tile layer object
3828
- * @param {Vector2} [position=(0,0)] - World space position
3829
- * @param {Vector2} [size=tileCollisionSize] - World space size
3830
- * @param {TileInfo} [tileInfo] - Tile info for layer
3831
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
3832
- * @param {number} [renderOrder] - Objects are sorted by renderOrder
3656
+ * @param {Vector2} [position=(0,0)] - World space position
3657
+ * @param {Vector2} [size=(1,1)] - World space size
3658
+ * @param {TileInfo} [tileInfo] - Tile info for layer
3659
+ * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
3660
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
3833
3661
  */
3834
- constructor(position, size=tileCollisionSize, tileInfo=tile(), scale=vec2(1), renderOrder=0)
3662
+ constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0)
3835
3663
  {
3836
3664
  super(position, size, tileInfo, 0, undefined, renderOrder);
3837
3665
 
@@ -3843,6 +3671,10 @@ class TileLayer extends EngineObject
3843
3671
  this.scale = scale;
3844
3672
  /** @property {boolean} - If true this layer will render to overlay canvas and appear above all objects */
3845
3673
  this.isOverlay = false;
3674
+ // set no friction by default, applied friction is max of both objects
3675
+ this.friction = 0;
3676
+ // set no elasticity by default, applied elasticity is max of both objects
3677
+ this.elasticity = 0;
3846
3678
 
3847
3679
  // init tile data
3848
3680
  this.data = [];
@@ -4039,6 +3871,146 @@ class TileLayer extends EngineObject
4039
3871
  * @param {number} [angle=0] */
4040
3872
  drawRect(pos, size, color, angle)
4041
3873
  { this.drawTile(pos, size, undefined, color, angle); }
3874
+ }
3875
+
3876
+ ///////////////////////////////////////////////////////////////////////////////
3877
+ /**
3878
+ * Tile Collision Layer - a tile layer with collision
3879
+ * - adds collision data and functions to TileLayer
3880
+ * - there can be multiple tile collision layers
3881
+ * - tile collison layers should not overlap each other
3882
+ * @extends TileLayer
3883
+ */
3884
+ class TileCollisionLayer extends TileLayer
3885
+ {
3886
+ /** Create a tile layer object
3887
+ * @param {Vector2} [position=(0,0)] - World space position
3888
+ * @param {Vector2} [size=(0,0)] - World space size
3889
+ * @param {TileInfo} [tileInfo] - Tile info for layer
3890
+ * @param {number} [renderOrder] - Objects are sorted by renderOrder
3891
+ */
3892
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
3893
+ {
3894
+ const scale = vec2(1); // collision layers are not scaled
3895
+ super(position, size.floor(), tileInfo, scale, renderOrder);
3896
+
3897
+ /** @property {Array<number>} - The tile collision grid */
3898
+ this.collisionData = [];
3899
+ this.initCollision(this.size);
3900
+
3901
+ // keep track of all collision layers
3902
+ tileCollisionLayers.push(this);
3903
+ }
3904
+
3905
+ /** Destroy this collision layer */
3906
+ destroy()
3907
+ {
3908
+ if (this.destroyed)
3909
+ return;
3910
+
3911
+ // remove from collision layers array and destroy
3912
+ const index = tileCollisionLayers.indexOf(this);
3913
+ ASSERT(index >= 0, 'tile collision layer not found in array');
3914
+ tileCollisionLayers.splice(index, 1);
3915
+ super.destroy();
3916
+ }
3917
+
3918
+ /** Clear and initialize tile collision to new size
3919
+ * @param {Vector2} size - width and height of tile collision 2d grid */
3920
+ initCollision(size)
3921
+ {
3922
+ this.size = size.floor();
3923
+ this.collisionData = [];
3924
+ this.collisionData.length = size.area();
3925
+ this.collisionData.fill(0);
3926
+ }
3927
+
3928
+ /** Set tile collision data for a given cell in the grid
3929
+ * @param {Vector2} pos
3930
+ * @param {number} [data] */
3931
+ setCollisionData(pos, data=1)
3932
+ {
3933
+ const i = (pos.y|0)*this.size.x + pos.x|0;
3934
+ pos.arrayCheck(this.size) && (this.collisionData[i] = data);
3935
+ }
3936
+
3937
+ /** Get tile collision data for a given cell in the grid
3938
+ * @param {Vector2} pos
3939
+ * @return {number} */
3940
+ getCollisionData(pos)
3941
+ {
3942
+ const i = (pos.y|0)*this.size.x + pos.x|0;
3943
+ return pos.arrayCheck(this.size) ? this.collisionData[i] : 0;
3944
+ }
3945
+
3946
+ /** Check if collision with another object should occur
3947
+ * @param {Vector2} pos
3948
+ * @param {Vector2} [size=(0,0)]
3949
+ * @param {EngineObject} [object]
3950
+ * @return {boolean} */
3951
+ collisionTest(pos, size=vec2(), object)
3952
+ {
3953
+ const minX = max(pos.x - size.x/2|0, 0);
3954
+ const minY = max(pos.y - size.y/2|0, 0);
3955
+ const maxX = min(pos.x + size.x/2, this.size.x);
3956
+ const maxY = min(pos.y + size.y/2, this.size.y);
3957
+ for (let y = minY; y < maxY; ++y)
3958
+ for (let x = minX; x < maxX; ++x)
3959
+ {
3960
+ // check if the object should collide with this tile
3961
+ const tileData = this.collisionData[y*this.size.x+x];
3962
+ if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
3963
+ return true;
3964
+ }
3965
+ return false;
3966
+ }
3967
+
3968
+ /** Return the center of first tile hit, undefined if nothing was hit.
3969
+ * This does not return the exact intersection, but the center of the tile hit.
3970
+ * @param {Vector2} posStart
3971
+ * @param {Vector2} posEnd
3972
+ * @param {EngineObject} [object]
3973
+ * @return {Vector2} */
3974
+ collisionRaycast(posStart, posEnd, object)
3975
+ {
3976
+ // test if a ray collides with tiles from start to end
3977
+ // todo: a way to get the exact hit point, it must still be inside the hit tile
3978
+ const delta = posEnd.subtract(posStart);
3979
+ const totalLength = delta.length();
3980
+ const normalizedDelta = delta.normalize();
3981
+ const unit = vec2(abs(1/normalizedDelta.x), abs(1/normalizedDelta.y));
3982
+ const flooredPosStart = posStart.floor();
3983
+
3984
+ // setup iteration variables
3985
+ let pos = flooredPosStart;
3986
+ let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
3987
+ let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
3988
+
3989
+ // use line drawing algorithm to test for collisions
3990
+ while (true)
3991
+ {
3992
+ // check for tile collision
3993
+ const tileData = this.getCollisionData(pos);
3994
+ if (tileData && (!object || object.collideWithTile(tileData, pos)))
3995
+ {
3996
+ debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
3997
+ debugRaycast && debugPoint(pos.add(vec2(.5)), '#ff0');
3998
+ return pos.add(vec2(.5));
3999
+ }
4000
+
4001
+ // check if past the end
4002
+ if (xi > totalLength && yi > totalLength)
4003
+ break;
4004
+
4005
+ // get coordinates of next tile to check
4006
+ if (xi > yi)
4007
+ pos.y += sign(delta.y), yi += unit.y;
4008
+ else
4009
+ pos.x += sign(delta.x), xi += unit.x;
4010
+ }
4011
+
4012
+ debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
4013
+ }
4042
4014
  }
4043
4015
  /**
4044
4016
  * LittleJS Particle System
@@ -4208,7 +4180,12 @@ class ParticleEmitter extends EngineObject
4208
4180
  else
4209
4181
  this.destroy();
4210
4182
 
4211
- debugParticles && debugRect(this.pos, vec2(this.emitSize), '#0f0', 0, this.angle);
4183
+ if (debugParticles)
4184
+ {
4185
+ // show emitter bounds
4186
+ const emitSize = typeof this.emitSize === 'number' ? vec2(this.emitSize) : this.emitSize;
4187
+ debugRect(this.pos, emitSize, '#0f0', 0, this.angle);
4188
+ }
4212
4189
  }
4213
4190
 
4214
4191
  /** Spawn one particle
@@ -4670,12 +4647,12 @@ function glPreRender()
4670
4647
 
4671
4648
  // set vertex attributes
4672
4649
  let offset = glAdditive = glBatchAdditive = 0;
4673
- let initVertexAttribArray = (name, type, typeSize, size)=>
4650
+ const initVertexAttribArray = (name, type, typeSize, size)=>
4674
4651
  {
4675
4652
  const location = glContext.getAttribLocation(glShader, name);
4676
4653
  const stride = typeSize && gl_INSTANCE_BYTE_STRIDE; // only if not geometry
4677
4654
  const divisor = typeSize && 1; // only if not geometry
4678
- const normalize = typeSize==1; // only if color
4655
+ const normalize = typeSize == 1; // only if color
4679
4656
  glContext.enableVertexAttribArray(location);
4680
4657
  glContext.vertexAttribPointer(location, size, type, normalize, stride, offset);
4681
4658
  glContext.vertexAttribDivisor(location, divisor);
@@ -4774,14 +4751,14 @@ function glCreateTexture(image)
4774
4751
  const texture = glContext.createTexture();
4775
4752
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
4776
4753
  if (image && image.width)
4777
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
4754
+ glSetTextureData(texture, image);
4778
4755
  else
4779
4756
  {
4780
4757
  // create a white texture
4781
4758
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
4782
4759
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
4783
4760
  }
4784
-
4761
+
4785
4762
  // use point filtering for pixelated rendering
4786
4763
  const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
4787
4764
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
@@ -4789,7 +4766,19 @@ function glCreateTexture(image)
4789
4766
  return texture;
4790
4767
  }
4791
4768
 
4792
- /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
4769
+ /** Set WebGL texture data from an image
4770
+ * @param {WebGLTexture} texture
4771
+ * @param {HTMLImageElement} image
4772
+ * @memberof WebGL */
4773
+ function glSetTextureData(texture, image)
4774
+ {
4775
+ // build the texture
4776
+ ASSERT(!!image && image.width > 0, 'Invalid image data.');
4777
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
4778
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
4779
+ }
4780
+
4781
+ /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
4793
4782
  * @memberof WebGL */
4794
4783
  function glFlush()
4795
4784
  {
@@ -4842,10 +4831,10 @@ function glSetAntialias(antialias=true)
4842
4831
  * @param {Number} uv0Y
4843
4832
  * @param {Number} uv1X
4844
4833
  * @param {Number} uv1Y
4845
- * @param {Number} rgba
4846
- * @param {Number} [rgbaAdditive=0]
4834
+ * @param {Number} [rgba=-1] - white is -1
4835
+ * @param {Number} [rgbaAdditive=0] - black is 0
4847
4836
  * @memberof WebGL */
4848
- function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
4837
+ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=-1, rgbaAdditive=0)
4849
4838
  {
4850
4839
  ASSERT(typeof rgba == 'number' && typeof rgbaAdditive == 'number', 'invalid color');
4851
4840
 
@@ -4898,7 +4887,7 @@ const engineName = 'LittleJS';
4898
4887
  * @type {string}
4899
4888
  * @default
4900
4889
  * @memberof Engine */
4901
- const engineVersion = '1.11.13';
4890
+ const engineVersion = '1.12.4';
4902
4891
 
4903
4892
  /** Frames per second to update
4904
4893
  * @type {number}
@@ -4986,16 +4975,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4986
4975
  ASSERT(Array.isArray(imageSources), 'pass in images as array');
4987
4976
 
4988
4977
  // allow passing in empty functions
4989
- if (!gameInit)
4990
- gameInit = ()=>{};
4991
- if (!gameUpdate)
4992
- gameUpdate = ()=>{};
4993
- if (!gameUpdatePost)
4994
- gameUpdatePost = ()=>{};
4995
- if (!gameRender)
4996
- gameRender = ()=>{};
4997
- if (!gameRenderPost)
4998
- gameRenderPost = ()=>{};
4978
+ gameInit ||= ()=>{};
4979
+ gameUpdate ||= ()=>{};
4980
+ gameUpdatePost ||= ()=>{};
4981
+ gameRender ||= ()=>{};
4982
+ gameRenderPost ||= ()=>{};
4999
4983
 
5000
4984
  // Called automatically by engine to setup render system
5001
4985
  function enginePreRender()
@@ -5022,11 +5006,13 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5022
5006
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
5023
5007
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
5024
5008
  if (debug) // +/- to speed/slow time
5025
- frameTimeDeltaMS *= debugSpeedUp ? 5 : debugSpeedDown ? .2 : 1;
5009
+ frameTimeDeltaMS *= debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
5026
5010
  timeReal += frameTimeDeltaMS / 1e3;
5027
5011
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
5028
5012
  if (!debugSpeedUp)
5029
- frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
5013
+ frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
5014
+ if (debug && debugVideoCaptureIsActive())
5015
+ frameTimeBufferMS = 0; // disable time smoothing when capturing video
5030
5016
 
5031
5017
  updateCanvas();
5032
5018
 
@@ -5105,6 +5091,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5105
5091
  }
5106
5092
  }
5107
5093
 
5094
+ debugVideoCaptureUpdate();
5108
5095
  requestAnimationFrame(engineUpdate);
5109
5096
  }
5110
5097
 
@@ -5152,11 +5139,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5152
5139
 
5153
5140
  // setup html
5154
5141
  const styleRoot =
5155
- 'margin:0;overflow:hidden;' + // fill the window
5156
- 'width:100vw;height:100vh;' + // fill the window
5157
- 'display:flex;' + // use flexbox
5158
- 'align-items:center;' + // horizontal center
5159
- 'justify-content:center;' + // vertical center
5142
+ 'margin:0;' + // fill the window
5160
5143
  'background:#000;' + // set background color
5161
5144
  (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5162
5145
  'user-select:none;' + // prevent hold to select
@@ -5179,7 +5162,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5179
5162
  overlayContext = overlayCanvas.getContext('2d');
5180
5163
 
5181
5164
  // set canvas style
5182
- const styleCanvas = 'position:absolute'; // allow canvases to overlap
5165
+ const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
5166
+ 'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
5183
5167
  mainCanvas.style.cssText = overlayCanvas.style.cssText = styleCanvas;
5184
5168
  if (glCanvas)
5185
5169
  glCanvas.style.cssText = styleCanvas;
@@ -5190,12 +5174,12 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5190
5174
  new Promise(resolve =>
5191
5175
  {
5192
5176
  const image = new Image;
5193
- image.crossOrigin = 'anonymous';
5194
5177
  image.onerror = image.onload = ()=>
5195
5178
  {
5196
5179
  textureInfos[textureIndex] = new TextureInfo(image);
5197
5180
  resolve();
5198
5181
  }
5182
+ image.crossOrigin = 'anonymous';
5199
5183
  image.src = src;
5200
5184
  })
5201
5185
  );
@@ -5497,3 +5481,2787 @@ function drawEngineSplashScreen(t)
5497
5481
  x.restore();
5498
5482
  }
5499
5483
 
5484
+ /**
5485
+ * LittleJS Newgrounds API
5486
+ * - NewgroundsMedal extends Medal with Newgrounds API functionality
5487
+ * - Call new NewgroundsPlugin() to setup Newgrounds
5488
+ * - Uses CryptoJS for encryption if optional cipher is provided
5489
+ * - Keeps connection alive and logs views
5490
+ * - Functions to interact with scoreboards
5491
+ * - Functions to unlock medals
5492
+ */
5493
+
5494
+
5495
+
5496
+ /** Global Newgrounds object
5497
+ * @type {NewgroundsPlugin}
5498
+ * @memberof Medal */
5499
+ let newgrounds;
5500
+
5501
+ ///////////////////////////////////////////////////////////////////////////////
5502
+ /**
5503
+ * Newgrounds medal auto unlocks in newgrounds API
5504
+ * @extends Medal
5505
+ */
5506
+ class NewgroundsMedal extends Medal
5507
+ {
5508
+ /** Create a newgrounds medal object and adds it to the list of medals
5509
+ * @param {Number} id - The unique identifier of the medal
5510
+ * @param {String} name - Name of the medal
5511
+ * @param {String} [description] - Description of the medal
5512
+ * @param {String} [icon] - Icon for the medal
5513
+ * @param {String} [src] - Image location for the medal
5514
+ */
5515
+ constructor(id, name, description, icon, src)
5516
+ { super(id, name, description, icon, src); }
5517
+
5518
+ /** Unlocks a medal if not already unlocked */
5519
+ unlock()
5520
+ {
5521
+ super.unlock();
5522
+ newgrounds && newgrounds.unlockMedal(this.id);
5523
+ }
5524
+ }
5525
+
5526
+ ///////////////////////////////////////////////////////////////////////////////
5527
+ /**
5528
+ * Newgrounds API object
5529
+ */
5530
+ class NewgroundsPlugin
5531
+ {
5532
+ /** Create the global newgrounds object
5533
+ * @param {string} app_id - The newgrounds App ID
5534
+ * @param {string} [cipher] - The encryption Key (AES-128/Base64)
5535
+ * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
5536
+ * @example
5537
+ * // create the newgrounds object, replace the app id with your own
5538
+ * const app_id = 'your_app_id_here';
5539
+ * new NewgroundsPlugin(app_id);
5540
+ */
5541
+ constructor(app_id, cipher, cryptoJS)
5542
+ {
5543
+ ASSERT(!newgrounds, 'there can only be one newgrounds object');
5544
+ ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
5545
+
5546
+ newgrounds = this; // set global newgrounds object
5547
+ this.app_id = app_id;
5548
+ this.cipher = cipher;
5549
+ this.cryptoJS = cryptoJS;
5550
+ this.host = location ? location.hostname : '';
5551
+
5552
+ // get session id from url search params
5553
+ const url = new URL(location.href);
5554
+ this.session_id = url.searchParams.get('ngio_session_id');
5555
+
5556
+ if (!this.session_id)
5557
+ return; // only use newgrounds when logged in
5558
+
5559
+ // get medals
5560
+ const medalsResult = this.call('Medal.getList');
5561
+ this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
5562
+ debugMedals && console.log(this.medals);
5563
+ for (const newgroundsMedal of this.medals)
5564
+ {
5565
+ const medal = medals[newgroundsMedal['id']];
5566
+ if (medal)
5567
+ {
5568
+ // copy newgrounds medal data
5569
+ medal.image = new Image;
5570
+ medal.image.src = newgroundsMedal['icon'];
5571
+ medal.name = newgroundsMedal['name'];
5572
+ medal.description = newgroundsMedal['description'];
5573
+ medal.unlocked = newgroundsMedal['unlocked'];
5574
+ medal.difficulty = newgroundsMedal['difficulty'];
5575
+ medal.value = newgroundsMedal['value'];
5576
+
5577
+ if (medal.value) // add value to description
5578
+ medal.description = medal.description + ` (${ medal.value })`;
5579
+ }
5580
+ }
5581
+
5582
+ // get scoreboards
5583
+ const scoreboardResult = this.call('ScoreBoard.getBoards');
5584
+ this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
5585
+ debugMedals && console.log(this.scoreboards);
5586
+
5587
+ // keep the session alive with a ping every minute
5588
+ const keepAliveMS = 60 * 1e3;
5589
+ setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
5590
+ }
5591
+
5592
+ /** Send message to unlock a medal by id
5593
+ * @param {number} id - The medal id */
5594
+ unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
5595
+
5596
+ /** Send message to post score
5597
+ * @param {number} id - The scoreboard id
5598
+ * @param {number} value - The score value */
5599
+ postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
5600
+
5601
+ /** Get scores from a scoreboard
5602
+ * @param {number} id - The scoreboard id
5603
+ * @param {string} [user] - A user's id or name
5604
+ * @param {number} [social] - If true, only social scores will be loaded
5605
+ * @param {number} [skip] - Number of scores to skip before start
5606
+ * @param {number} [limit] - Number of scores to include in the list
5607
+ * @return {Object} - The response JSON object
5608
+ */
5609
+ getScores(id, user, social=0, skip=0, limit=10)
5610
+ { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
5611
+
5612
+ /** Send message to log a view */
5613
+ logView() { return this.call('App.logView', {'host':this.host}, true); }
5614
+
5615
+ /** Send a message to call a component of the Newgrounds API
5616
+ * @param {string} component - Name of the component
5617
+ * @param {Object} [parameters] - Parameters to use for call
5618
+ * @param {boolean} [async] - If true, don't wait for response before continuing
5619
+ * @return {Object} - The response JSON object
5620
+ */
5621
+ call(component, parameters, async=false)
5622
+ {
5623
+ const call = {'component':component, 'parameters':parameters};
5624
+ if (this.cipher)
5625
+ {
5626
+ // encrypt using AES-128 Base64 with cryptoJS
5627
+ const cryptoJS = this.cryptoJS;
5628
+ const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
5629
+ const iv = cryptoJS['lib']['WordArray']['random'](16);
5630
+ const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
5631
+ call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
5632
+ call['parameters'] = 0;
5633
+ }
5634
+
5635
+ // build the input object
5636
+ const input =
5637
+ {
5638
+ 'app_id': this.app_id,
5639
+ 'session_id': this.session_id,
5640
+ 'call': call
5641
+ };
5642
+
5643
+ // build post data
5644
+ const formData = new FormData();
5645
+ formData.append('input', JSON.stringify(input));
5646
+
5647
+ // send post data
5648
+ const xmlHttp = new XMLHttpRequest();
5649
+ const url = 'https://newgrounds.io/gateway_v3.php';
5650
+ xmlHttp.open('POST', url, !debugMedals && async);
5651
+ try { xmlHttp.send(formData); }
5652
+ catch(e)
5653
+ {
5654
+ debugMedals && console.log('newgrounds call failed', e);
5655
+ return;
5656
+ }
5657
+ debugMedals && console.log(xmlHttp.responseText);
5658
+ return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
5659
+ }
5660
+ }
5661
+
5662
+ /**
5663
+ * LittleJS Post Processing Plugin
5664
+ * - Supports shadertoy style post processing shaders
5665
+ * - call new new PostProcessPlugin() to setup post processing
5666
+ * - can be enabled to pass other canvases through a final shader
5667
+ */
5668
+
5669
+
5670
+
5671
+ ///////////////////////////////////////////////////////////////////////////////
5672
+
5673
+ /** Global Post Process plugin object
5674
+ * @type {PostProcessPlugin} */
5675
+ let postProcess;
5676
+
5677
+ /////////////////////////////////////////////////////////////////////////
5678
+ /**
5679
+ * UI System Global Object
5680
+ */
5681
+ class PostProcessPlugin
5682
+ {
5683
+ /** Create global post processing shader
5684
+ * @param {string} shaderCode
5685
+ * @param {boolean} [includeOverlay]
5686
+ * @example
5687
+ * // create the post process plugin object
5688
+ * new PostProcessPlugin(shaderCode);
5689
+ */
5690
+ constructor(shaderCode, includeOverlay=false)
5691
+ {
5692
+ ASSERT(!postProcess, 'Post process already initialized');
5693
+ postProcess = this;
5694
+
5695
+ if (headlessMode) return;
5696
+ if (!shaderCode) // default shader pass through
5697
+ shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
5698
+
5699
+ /** @property {WebGLProgram} - Shader for post processing */
5700
+ this.shader = glCreateProgram(
5701
+ '#version 300 es\n' + // specify GLSL ES version
5702
+ 'precision highp float;'+ // use highp for better accuracy
5703
+ 'in vec2 p;'+ // position
5704
+ 'void main(){'+ // shader entry point
5705
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
5706
+ '}' // end of shader
5707
+ ,
5708
+ '#version 300 es\n' + // specify GLSL ES version
5709
+ 'precision highp float;'+ // use highp for better accuracy
5710
+ 'uniform sampler2D iChannel0;'+ // input texture
5711
+ 'uniform vec3 iResolution;'+ // size of output texture
5712
+ 'uniform float iTime;'+ // time
5713
+ 'out vec4 c;'+ // out color
5714
+ '\n' + shaderCode + '\n'+ // insert custom shader code
5715
+ 'void main(){'+ // shader entry point
5716
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
5717
+ 'c.a=1.;'+ // always use full alpha
5718
+ '}' // end of shader
5719
+ );
5720
+
5721
+ /** @property {WebGLTexture} - Texture for post processing */
5722
+ this.texture = glCreateTexture();
5723
+
5724
+ /** @property {boolean} - Should overlay canvas be included in post processing */
5725
+ this.includeOverlay = includeOverlay;
5726
+
5727
+ // Render the post processing shader, called automatically by the engine
5728
+ engineAddPlugin(undefined, postProcessRender);
5729
+ function postProcessRender()
5730
+ {
5731
+ if (headlessMode) return;
5732
+
5733
+ // prepare to render post process shader
5734
+ if (glEnable)
5735
+ {
5736
+ glFlush(); // clear out the buffer
5737
+ mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
5738
+ }
5739
+ else
5740
+ {
5741
+ // set the viewport
5742
+ glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
5743
+ }
5744
+
5745
+ if (postProcess.includeOverlay)
5746
+ {
5747
+ // copy overlay canvas so it will be included in post processing
5748
+ mainContext.drawImage(overlayCanvas, 0, 0);
5749
+ overlayCanvas.width |= 0;
5750
+ }
5751
+
5752
+ // setup shader program to draw one triangle
5753
+ glContext.useProgram(postProcess.shader);
5754
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
5755
+ glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
5756
+ glContext.disable(glContext.BLEND);
5757
+
5758
+ // set textures, pass in the 2d canvas and gl canvas in separate texture channels
5759
+ glContext.activeTexture(glContext.TEXTURE0);
5760
+ glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
5761
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
5762
+
5763
+ // set vertex position attribute
5764
+ const vertexByteStride = 8;
5765
+ const pLocation = glContext.getAttribLocation(postProcess.shader, 'p');
5766
+ glContext.enableVertexAttribArray(pLocation);
5767
+ glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0);
5768
+
5769
+ // set uniforms and draw
5770
+ const uniformLocation = (name)=>glContext.getUniformLocation(postProcess.shader, name);
5771
+ glContext.uniform1i(uniformLocation('iChannel0'), 0);
5772
+ glContext.uniform1f(uniformLocation('iTime'), time);
5773
+ glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
5774
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
5775
+ }
5776
+ }
5777
+ }
5778
+
5779
+ /**
5780
+ * LittleJS ZzFXM Plugin
5781
+ */
5782
+
5783
+
5784
+
5785
+ /**
5786
+ * Music Object - Stores a zzfx music track for later use
5787
+ *
5788
+ * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
5789
+ * @example
5790
+ * // create some music
5791
+ * const music_example = new Music(
5792
+ * [
5793
+ * [ // instruments
5794
+ * [,0,400] // simple note
5795
+ * ],
5796
+ * [ // patterns
5797
+ * [ // pattern 1
5798
+ * [ // channel 0
5799
+ * 0, -1, // instrument 0, left speaker
5800
+ * 1, 0, 9, 1 // channel notes
5801
+ * ],
5802
+ * [ // channel 1
5803
+ * 0, 1, // instrument 0, right speaker
5804
+ * 0, 12, 17, -1 // channel notes
5805
+ * ]
5806
+ * ],
5807
+ * ],
5808
+ * [0, 0, 0, 0], // sequence, play pattern 0 four times
5809
+ * 90 // BPM
5810
+ * ]);
5811
+ *
5812
+ * // play the music
5813
+ * music_example.play();
5814
+ */
5815
+ class ZzFXMusic extends Sound
5816
+ {
5817
+ /** Create a music object and cache the zzfx music samples for later use
5818
+ * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters
5819
+ */
5820
+ constructor(zzfxMusic)
5821
+ {
5822
+ super(undefined);
5823
+
5824
+ if (!soundEnable || headlessMode) return;
5825
+ this.randomness = 0;
5826
+ this.sampleChannels = zzfxM(...zzfxMusic);
5827
+ this.sampleRate = zzfxR;
5828
+ }
5829
+
5830
+ /** Play the music
5831
+ * @param {number} [volume=1] - How much to scale volume by
5832
+ * @param {boolean} [loop] - True if the music should loop
5833
+ * @return {AudioBufferSourceNode} - The audio source node
5834
+ */
5835
+ playMusic(volume, loop=false)
5836
+ { return super.play(undefined, volume, 1, 1, loop); }
5837
+ }
5838
+
5839
+ ///////////////////////////////////////////////////////////////////////////////
5840
+ // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
5841
+
5842
+ /** Generate samples for a ZzFM song with given parameters
5843
+ * @param {Array} instruments - Array of ZzFX sound parameters
5844
+ * @param {Array} patterns - Array of pattern data
5845
+ * @param {Array} sequence - Array of pattern indexes
5846
+ * @param {number} [BPM] - Playback speed of the song in BPM
5847
+ * @return {Array} - Left and right channel sample data */
5848
+ function zzfxM(instruments, patterns, sequence, BPM = 125)
5849
+ {
5850
+ let i, j, k;
5851
+ let instrumentParameters;
5852
+ let note;
5853
+ let sample;
5854
+ let patternChannel;
5855
+ let notFirstBeat;
5856
+ let stop;
5857
+ let instrument;
5858
+ let attenuation;
5859
+ let outSampleOffset;
5860
+ let isSequenceEnd;
5861
+ let sampleOffset = 0;
5862
+ let nextSampleOffset;
5863
+ let sampleBuffer = [];
5864
+ let leftChannelBuffer = [];
5865
+ let rightChannelBuffer = [];
5866
+ let channelIndex = 0;
5867
+ let panning = 0;
5868
+ let hasMore = 1;
5869
+ let sampleCache = {};
5870
+ let beatLength = zzfxR / BPM * 60 >> 2;
5871
+
5872
+ // for each channel in order until there are no more
5873
+ for (; hasMore; channelIndex++) {
5874
+
5875
+ // reset current values
5876
+ sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
5877
+
5878
+ // for each pattern in sequence
5879
+ sequence.forEach((patternIndex, sequenceIndex) => {
5880
+ // get pattern for current channel, use empty 1 note pattern if none found
5881
+ patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
5882
+
5883
+ // check if there are more channels
5884
+ hasMore |= patterns[patternIndex][channelIndex]&&1;
5885
+
5886
+ // get next offset, use the length of first channel
5887
+ nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
5888
+ // for each beat in pattern, plus one extra if end of sequence
5889
+ isSequenceEnd = sequenceIndex == sequence.length - 1;
5890
+ for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
5891
+
5892
+ // <channel-note>
5893
+ note = patternChannel[i];
5894
+
5895
+ // stop if end, different instrument or new note
5896
+ stop = i == patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
5897
+ instrument != (patternChannel[0] || 0) || note | 0;
5898
+
5899
+ // fill buffer with samples for previous beat, most cpu intensive part
5900
+ for (j = 0; j < beatLength && notFirstBeat;
5901
+
5902
+ // fade off attenuation at end of beat if stopping note, prevents clicking
5903
+ j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
5904
+ ) {
5905
+ // copy sample to stereo buffers with panning
5906
+ sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
5907
+ leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
5908
+ rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
5909
+ }
5910
+
5911
+ // set up for next note
5912
+ if (note) {
5913
+ // set attenuation
5914
+ attenuation = note % 1;
5915
+ panning = patternChannel[1] || 0;
5916
+ if (note |= 0) {
5917
+ // get cached sample
5918
+ sampleBuffer = sampleCache[
5919
+ [
5920
+ instrument = patternChannel[sampleOffset = 0] || 0,
5921
+ note
5922
+ ]
5923
+ ] = sampleCache[[instrument, note]] || (
5924
+ // add sample to cache
5925
+ instrumentParameters = [...instruments[instrument]],
5926
+ instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
5927
+
5928
+ // allow negative values to stop notes
5929
+ note > 0 ? zzfxG(...instrumentParameters) : []
5930
+ );
5931
+ }
5932
+ }
5933
+ }
5934
+
5935
+ // update the sample offset
5936
+ outSampleOffset = nextSampleOffset;
5937
+ });
5938
+ }
5939
+
5940
+ return [leftChannelBuffer, rightChannelBuffer];
5941
+ }
5942
+
5943
+ /**
5944
+ * LittleJS User Interface Plugin
5945
+ * - call new UISystemPlugin() to setup the UI system
5946
+ * - Nested Menus
5947
+ * - Text
5948
+ * - Buttons
5949
+ * - Checkboxes
5950
+ * - Images
5951
+ */
5952
+
5953
+
5954
+
5955
+ ///////////////////////////////////////////////////////////////////////////////
5956
+
5957
+ /** Global UI system plugin object
5958
+ * @type {UISystemPlugin} */
5959
+ let uiSystem;
5960
+
5961
+ ///////////////////////////////////////////////////////////////////////////////
5962
+ /**
5963
+ * UI System Global Object
5964
+ */
5965
+ class UISystemPlugin
5966
+ {
5967
+ /** Create the global UI system object
5968
+ * @param {CanvasRenderingContext2D} [context]
5969
+ * @example
5970
+ * // create the ui plugin object
5971
+ * new UISystemPlugin;
5972
+ */
5973
+ constructor(context=overlayContext)
5974
+ {
5975
+ ASSERT(!uiSystem, 'UI system already initialized');
5976
+ uiSystem = this;
5977
+
5978
+ /** @property {Color} - Default fill color for UI elements */
5979
+ this.defaultColor = WHITE;
5980
+ /** @property {Color} - Default outline color for UI elements */
5981
+ this.defaultLineColor = BLACK;
5982
+ /** @property {Color} - Default text color for UI elements */
5983
+ this.defaultTextColor = BLACK;
5984
+ /** @property {Color} - Default button color for UI elements */
5985
+ this.defaultButtonColor = hsl(0,0,.5);
5986
+ /** @property {Color} - Default hover color for UI elements */
5987
+ this.defaultHoverColor = hsl(0,0,.7);
5988
+ /** @property {number} - Default line width for UI elements */
5989
+ this.defaultLineWidth = 4;
5990
+ /** @property {string} - Default font for UI elements */
5991
+ this.defaultFont = 'arial';
5992
+ /** @property {Array<UIObject>} - List of all UI elements */
5993
+ this.uiObjects = [];
5994
+ /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
5995
+ this.uiContext = context;
5996
+
5997
+ engineAddPlugin(uiUpdate, uiRender);
5998
+
5999
+ // setup recursive update and render
6000
+ function uiUpdate()
6001
+ {
6002
+ function updateObject(o)
6003
+ {
6004
+ if (!o.visible)
6005
+ return;
6006
+ if (o.parent)
6007
+ o.pos = o.localPos.add(o.parent.pos);
6008
+ o.update();
6009
+ for(const c of o.children)
6010
+ updateObject(c);
6011
+ }
6012
+ uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
6013
+ }
6014
+ function uiRender()
6015
+ {
6016
+ function renderObject(o)
6017
+ {
6018
+ if (!o.visible)
6019
+ return;
6020
+ if (o.parent)
6021
+ o.pos = o.localPos.add(o.parent.pos);
6022
+ o.render();
6023
+ for(const c of o.children)
6024
+ renderObject(c);
6025
+ }
6026
+ uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
6027
+ }
6028
+ }
6029
+
6030
+ /** Draw a rectangle to the UI context
6031
+ * @param {Vector2} pos
6032
+ * @param {Vector2} size
6033
+ * @param {Color} [color=uiSystem.defaultColor]
6034
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6035
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6036
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6037
+ {
6038
+ uiSystem.uiContext.fillStyle = color.toString();
6039
+ uiSystem.uiContext.beginPath();
6040
+ uiSystem.uiContext.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
6041
+ uiSystem.uiContext.fill();
6042
+ if (lineWidth)
6043
+ {
6044
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6045
+ uiSystem.uiContext.lineWidth = lineWidth;
6046
+ uiSystem.uiContext.stroke();
6047
+ }
6048
+ }
6049
+
6050
+ /** Draw a line to the UI context
6051
+ * @param {Vector2} posA
6052
+ * @param {Vector2} posB
6053
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6054
+ * @param {Color} [lineColor=uiSystem.defaultLineColor] */
6055
+ drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
6056
+ {
6057
+ uiSystem.uiContext.strokeStyle = lineColor.toString();
6058
+ uiSystem.uiContext.lineWidth = lineWidth;
6059
+ uiSystem.uiContext.beginPath();
6060
+ uiSystem.uiContext.lineTo(posA.x, posA.y);
6061
+ uiSystem.uiContext.lineTo(posB.x, posB.y);
6062
+ uiSystem.uiContext.stroke();
6063
+ }
6064
+
6065
+ /** Draw a tile to the UI context
6066
+ * @param {Vector2} pos
6067
+ * @param {Vector2} size
6068
+ * @param {TileInfo} tileInfo
6069
+ * @param {Color} [color=uiSystem.defaultColor]
6070
+ * @param {number} [angle]
6071
+ * @param {boolean} [mirror] */
6072
+ drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false)
6073
+ {
6074
+ drawTile(pos, size, tileInfo, color, angle, mirror, BLACK, false, true, uiSystem.uiContext);
6075
+ }
6076
+
6077
+ /** Draw text to the UI context
6078
+ * @param {string} text
6079
+ * @param {Vector2} pos
6080
+ * @param {Vector2} size
6081
+ * @param {Color} [color=uiSystem.defaultColor]
6082
+ * @param {number} [lineWidth=uiSystem.defaultLineWidth]
6083
+ * @param {Color} [lineColor=uiSystem.defaultLineColor]
6084
+ * @param {string} [align]
6085
+ * @param {string} [font=uiSystem.defaultFont] */
6086
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont)
6087
+ {
6088
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, size.x, uiSystem.uiContext);
6089
+ }
6090
+ }
6091
+
6092
+ ///////////////////////////////////////////////////////////////////////////////
6093
+ /**
6094
+ * UI Object - Base level object for all UI elements
6095
+ */
6096
+ class UIObject
6097
+ {
6098
+ /** Create a UIObject
6099
+ * @param {Vector2} [pos=(0,0)]
6100
+ * @param {Vector2} [size=(1,1)]
6101
+ */
6102
+ constructor(pos=vec2(), size=vec2())
6103
+ {
6104
+ /** @property {Vector2} - Local position of the object */
6105
+ this.localPos = pos.copy();
6106
+ /** @property {Vector2} - Screen space position of the object */
6107
+ this.pos = pos.copy();
6108
+ /** @property {Vector2} - Screen space size of the object */
6109
+ this.size = size.copy();
6110
+ /** @property {Color} */
6111
+ this.color = uiSystem.defaultColor;
6112
+ /** @property {Color} */
6113
+ this.lineColor = uiSystem.defaultLineColor;
6114
+ /** @property {Color} */
6115
+ this.textColor = uiSystem.defaultTextColor;
6116
+ /** @property {Color} */
6117
+ this.hoverColor = uiSystem.defaultHoverColor;
6118
+ /** @property {number} */
6119
+ this.lineWidth = uiSystem.defaultLineWidth;
6120
+ /** @property {string} */
6121
+ this.font = uiSystem.defaultFont;
6122
+ /** @property {boolean} */
6123
+ this.visible = true;
6124
+ /** @property {Array<UIObject>} */
6125
+ this.children = [];
6126
+ /** @property {UIObject} */
6127
+ this.parent = undefined;
6128
+ uiSystem.uiObjects.push(this);
6129
+ }
6130
+
6131
+ /** Add a child UIObject to this object
6132
+ * @param {UIObject} child
6133
+ */
6134
+ addChild(child)
6135
+ {
6136
+ ASSERT(!child.parent && !this.children.includes(child));
6137
+ this.children.push(child);
6138
+ child.parent = this;
6139
+ }
6140
+
6141
+ /** Remove a child UIObject from this object
6142
+ * @param {UIObject} child
6143
+ */
6144
+ removeChild(child)
6145
+ {
6146
+ ASSERT(child.parent == this && this.children.includes(child));
6147
+ this.children.splice(this.children.indexOf(child), 1);
6148
+ child.parent = undefined;
6149
+ }
6150
+
6151
+ /** Update the object, called automatically by plugin once each frame */
6152
+ update()
6153
+ {
6154
+ // track mouse input
6155
+ const mouseWasOver = this.mouseIsOver;
6156
+ const mouseDown = mouseIsDown(0);
6157
+ if (!mouseDown || isTouchDevice)
6158
+ {
6159
+ this.mouseIsOver = isOverlapping(this.pos, this.size, mousePosScreen);
6160
+ if (!mouseDown && isTouchDevice)
6161
+ this.mouseIsOver = false;
6162
+ if (this.mouseIsOver && !mouseWasOver)
6163
+ this.onEnter();
6164
+ if (!this.mouseIsOver && mouseWasOver)
6165
+ this.onLeave();
6166
+ }
6167
+ if (mouseWasPressed(0) && this.mouseIsOver)
6168
+ {
6169
+ this.mouseIsHeld = true;
6170
+ this.onPress();
6171
+ if (isTouchDevice)
6172
+ this.mouseIsOver = false;
6173
+ }
6174
+ else if (this.mouseIsHeld && !mouseDown)
6175
+ {
6176
+ this.mouseIsHeld = false;
6177
+ this.onRelease();
6178
+ }
6179
+ }
6180
+
6181
+ /** Render the object, called automatically by plugin once each frame */
6182
+ render()
6183
+ {
6184
+ if (this.size.x && this.size.y)
6185
+ uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor);
6186
+ }
6187
+
6188
+ /** Called when the mouse enters the object */
6189
+ onEnter() {}
6190
+
6191
+ /** Called when the mouse leaves the object */
6192
+ onLeave() {}
6193
+
6194
+ /** Called when the mouse is pressed while over the object */
6195
+ onPress() {}
6196
+
6197
+ /** Called when the mouse is released while over the object */
6198
+ onRelease() {}
6199
+
6200
+ /** Called when the state of this object changes */
6201
+ onChange() {}
6202
+ }
6203
+
6204
+ ///////////////////////////////////////////////////////////////////////////////
6205
+ /**
6206
+ * UIText - A UI object that displays text
6207
+ * @extends UIObject
6208
+ */
6209
+ class UIText extends UIObject
6210
+ {
6211
+ /** Create a UIText object
6212
+ * @param {Vector2} [pos]
6213
+ * @param {Vector2} [size]
6214
+ * @param {string} [text]
6215
+ * @param {string} [align]
6216
+ * @param {string} [font=uiSystem.defaultFont]
6217
+ */
6218
+ constructor(pos, size, text='', align='center', font=uiSystem.defaultFont)
6219
+ {
6220
+ super(pos, size);
6221
+
6222
+ /** @property {string} */
6223
+ this.text = text;
6224
+ /** @property {string} */
6225
+ this.align = align;
6226
+
6227
+ this.font = font; // set font
6228
+ this.lineWidth = 0; // set text to not be outlined by default
6229
+ }
6230
+ render()
6231
+ {
6232
+ uiSystem.drawText(this.text, this.pos, this.size, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
6233
+ }
6234
+ }
6235
+
6236
+ ///////////////////////////////////////////////////////////////////////////////
6237
+ /**
6238
+ * UITile - A UI object that displays a tile image
6239
+ * @extends UIObject
6240
+ */
6241
+ class UITile extends UIObject
6242
+ {
6243
+ /** Create a UITile object
6244
+ * @param {Vector2} [pos]
6245
+ * @param {Vector2} [size]
6246
+ * @param {TileInfo} [tileInfo]
6247
+ * @param {Color} [color=WHITE]
6248
+ * @param {number} [angle]
6249
+ * @param {boolean} [mirror]
6250
+ */
6251
+ constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
6252
+ {
6253
+ super(pos, size);
6254
+
6255
+ /** @property {TileInfo} - Tile image to use */
6256
+ this.tileInfo = tileInfo;
6257
+ /** @property {number} - Angle to rotate in radians */
6258
+ this.angle = angle;
6259
+ /** @property {boolean} - Should it be mirrored? */
6260
+ this.mirror = mirror;
6261
+ this.color = color;
6262
+ }
6263
+ render()
6264
+ {
6265
+ uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
6266
+ }
6267
+ }
6268
+
6269
+ ///////////////////////////////////////////////////////////////////////////////
6270
+ /**
6271
+ * UIButton - A UI object that acts as a button
6272
+ * @extends UIObject
6273
+ */
6274
+ class UIButton extends UIObject
6275
+ {
6276
+ /** Create a UIButton object
6277
+ * @param {Vector2} [pos]
6278
+ * @param {Vector2} [size]
6279
+ * @param {string} [text]
6280
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6281
+ */
6282
+ constructor(pos, size, text='', color=uiSystem.defaultButtonColor)
6283
+ {
6284
+ super(pos, size);
6285
+
6286
+ /** @property {string} */
6287
+ this.text = text;
6288
+ this.color = color;
6289
+ }
6290
+ render()
6291
+ {
6292
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
6293
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6294
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
6295
+ const textSize = vec2(this.size.x, this.size.y*.8);
6296
+ uiSystem.drawText(this.text, this.pos, textSize,
6297
+ this.textColor, 0, undefined, this.align, this.font);
6298
+ }
6299
+ }
6300
+
6301
+ ///////////////////////////////////////////////////////////////////////////////
6302
+ /**
6303
+ * UICheckbox - A UI object that acts as a checkbox
6304
+ * @extends UIObject
6305
+ */
6306
+ class UICheckbox extends UIObject
6307
+ {
6308
+ /** Create a UICheckbox object
6309
+ * @param {Vector2} [pos]
6310
+ * @param {Vector2} [size]
6311
+ * @param {boolean} [checked]
6312
+ */
6313
+ constructor(pos, size, checked=false)
6314
+ {
6315
+ super(pos, size);
6316
+
6317
+ /** @property {boolean} */
6318
+ this.checked = checked;
6319
+ }
6320
+ onPress()
6321
+ {
6322
+ this.checked = !this.checked;
6323
+ this.onChange();
6324
+ }
6325
+ render()
6326
+ {
6327
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6328
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor);
6329
+ if (this.checked)
6330
+ {
6331
+ // draw an X if checked
6332
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor);
6333
+ uiSystem.drawLine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor);
6334
+ }
6335
+ }
6336
+ }
6337
+
6338
+ ///////////////////////////////////////////////////////////////////////////////
6339
+ /**
6340
+ * UIScrollbar - A UI object that acts as a scrollbar
6341
+ * @extends UIObject
6342
+ */
6343
+ class UIScrollbar extends UIObject
6344
+ {
6345
+ /** Create a UIScrollbar object
6346
+ * @param {Vector2} [pos]
6347
+ * @param {Vector2} [size]
6348
+ * @param {number} [value]
6349
+ * @param {string} [text]
6350
+ * @param {Color} [color=uiSystem.defaultButtonColor]
6351
+ * @param {Color} [handleColor=WHITE]
6352
+ */
6353
+ constructor(pos, size, value=.5, text='', color=uiSystem.defaultButtonColor, handleColor=WHITE)
6354
+ {
6355
+ super(pos, size);
6356
+
6357
+ /** @property {number} */
6358
+ this.value = value;
6359
+ /** @property {string} */
6360
+ this.text = text;
6361
+ this.color = color;
6362
+ this.handleColor = handleColor;
6363
+ }
6364
+ update()
6365
+ {
6366
+ super.update();
6367
+ if (this.mouseIsHeld)
6368
+ {
6369
+ const handleSize = vec2(this.size.y);
6370
+ const handleWidth = this.size.x - handleSize.x;
6371
+ const p1 = this.pos.x - handleWidth/2;
6372
+ const p2 = this.pos.x + handleWidth/2;
6373
+ const oldValue = this.value;
6374
+ this.value = percent(mousePosScreen.x, p1, p2);
6375
+ this.value == oldValue || this.onChange();
6376
+ }
6377
+ }
6378
+ render()
6379
+ {
6380
+ const lineColor = this.mouseIsHeld ? this.color : this.lineColor;
6381
+ const color = this.mouseIsOver? this.hoverColor : this.color;
6382
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor);
6383
+
6384
+ const handleSize = vec2(this.size.y);
6385
+ const handleWidth = this.size.x - handleSize.x;
6386
+ const p1 = this.pos.x - handleWidth/2;
6387
+ const p2 = this.pos.x + handleWidth/2;
6388
+ const handlePos = vec2(lerp(this.value, p1, p2), this.pos.y);
6389
+ const barColor = this.mouseIsHeld ? this.color : this.handleColor;
6390
+ uiSystem.drawRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor);
6391
+
6392
+ const textSize = vec2(this.size.x, this.size.y*.8);
6393
+ uiSystem.drawText(this.text, this.pos, textSize,
6394
+ this.textColor, 0, undefined, this.align, this.font);
6395
+ }
6396
+ }
6397
+
6398
+ /**
6399
+ * LittleJS Box2D Physics Plugin
6400
+ * - Box2dObject extends EngineObject with Box2D physics
6401
+ * - Call box2dEngineInit() to start instead of normal engineInit()
6402
+ * - You will also need to include box2d.wasm.js
6403
+ * - Uses box2d.js super fast web assembly port of Box2D
6404
+ * - More info: https://github.com/kripken/box2d.js
6405
+ * - Fully wraps everything in Box2d
6406
+ * - Functions to create polygon, circle, and edge shapes
6407
+ * - Raycasting and querying
6408
+ * - Joint creation
6409
+ * - Contact begin and end callbacks
6410
+ * - Debug physics drawing
6411
+ */
6412
+
6413
+
6414
+
6415
+ /** Global Box2d Plugin object
6416
+ * @type {Box2dPlugin} */
6417
+ let box2d;
6418
+
6419
+ /** Enable Box2D debug drawing
6420
+ * @type {boolean}
6421
+ * @default */
6422
+ let box2dDebug = false;
6423
+
6424
+ /** Enable Box2D debug drawing
6425
+ * @param {boolean} enable */
6426
+ function box2dSetDebug(enable) { box2dDebug = enable; }
6427
+
6428
+ ///////////////////////////////////////////////////////////////////////////////
6429
+ /**
6430
+ * Box2D Object - extend with your own custom physics objects
6431
+ * - A LittleJS object with Box2D physics
6432
+ * - Each object has a Box2D body which can have multiple fixtures and joints
6433
+ * - Provides interface for Box2D body and fixture functions
6434
+ * @extends EngineObject
6435
+ */
6436
+ class Box2dObject extends EngineObject
6437
+ {
6438
+ /** Create a LittleJS object with Box2d physics
6439
+ * @param {Vector2} [pos]
6440
+ * @param {Vector2} [size]
6441
+ * @param {TileInfo} [tileInfo]
6442
+ * @param {number} [angle]
6443
+ * @param {Color} [color]
6444
+ * @param {number} [bodyType]
6445
+ * @param {number} [renderOrder] */
6446
+ constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
6447
+ {
6448
+ super(pos, size, tileInfo, angle, color, renderOrder);
6449
+
6450
+ // create physics body
6451
+ const bodyDef = new box2d.instance.b2BodyDef();
6452
+ bodyDef.set_type(bodyType);
6453
+ bodyDef.set_position(box2d.vec2dTo(pos));
6454
+ bodyDef.set_angle(-angle);
6455
+ this.body = box2d.world.CreateBody(bodyDef);
6456
+ this.body.object = this;
6457
+ this.outlineColor = BLACK;
6458
+ }
6459
+
6460
+ /** Destroy this object and it's physics body */
6461
+ destroy()
6462
+ {
6463
+ // destroy physics body, fixtures, and joints
6464
+ this.body && box2d.world.DestroyBody(this.body);
6465
+ this.body = 0;
6466
+ super.destroy();
6467
+ }
6468
+
6469
+ /** Copy box2d update sim data */
6470
+ update()
6471
+ {
6472
+ // use box2d physics update
6473
+ this.pos = box2d.vec2From(this.body.GetPosition());
6474
+ this.angle = -this.body.GetAngle();
6475
+ }
6476
+
6477
+ /** Render the object, uses box2d drawing if no tile info exists */
6478
+ render()
6479
+ {
6480
+ // use default render or draw fixtures
6481
+ if (this.tileInfo)
6482
+ super.render();
6483
+ else
6484
+ this.drawFixtures(this.color, this.outlineColor, this.lineWidth, mainContext);
6485
+ }
6486
+
6487
+ /** Render debug info */
6488
+ renderDebugInfo()
6489
+ {
6490
+ const isAsleep = !this.getIsAwake();
6491
+ const isStatic = this.getBodyType() == box2d.bodyTypeStatic;
6492
+ const color = rgb(isAsleep?1:0, isAsleep?1:0, isStatic?1:0, .5);
6493
+ this.drawFixtures(color);
6494
+ }
6495
+
6496
+ /** Draws all this object's fixtures
6497
+ * @param {Color} [color]
6498
+ * @param {Color} [outlineColor]
6499
+ * @param {number} [lineWidth]
6500
+ * @param {CanvasRenderingContext2D} [context] */
6501
+ drawFixtures(color=WHITE, outlineColor, lineWidth=.1, context)
6502
+ {
6503
+ this.getFixtureList().forEach(fixture=>
6504
+ box2d.drawFixture(fixture, this.pos, this.angle, color, outlineColor, lineWidth, context));
6505
+ }
6506
+
6507
+ ///////////////////////////////////////////////////////////////////////////////
6508
+ // physics contact callbacks
6509
+
6510
+ /** Called when a contact begins
6511
+ * @param {Box2dObject} otherObject */
6512
+ beginContact(otherObject) {}
6513
+
6514
+ /** Called when a contact ends
6515
+ * @param {Box2dObject} otherObject */
6516
+ endContact(otherObject) {}
6517
+
6518
+ ///////////////////////////////////////////////////////////////////////////////
6519
+ // physics fixtures and shapes
6520
+
6521
+ /** Add a shape fixture to the body
6522
+ * @param {Object} shape
6523
+ * @param {number} [density]
6524
+ * @param {number} [friction]
6525
+ * @param {number} [restitution]
6526
+ * @param {boolean} [isSensor] */
6527
+ addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
6528
+ {
6529
+ const fd = new box2d.instance.b2FixtureDef();
6530
+ fd.set_shape(shape);
6531
+ fd.set_density(density);
6532
+ fd.set_friction(friction);
6533
+ fd.set_restitution(restitution);
6534
+ fd.set_isSensor(isSensor);
6535
+ return this.body.CreateFixture(fd);
6536
+ }
6537
+
6538
+ /** Add a box shape to the body
6539
+ * @param {Vector2} [size]
6540
+ * @param {Vector2} [offset]
6541
+ * @param {number} [angle]
6542
+ * @param {number} [density]
6543
+ * @param {number} [friction]
6544
+ * @param {number} [restitution]
6545
+ * @param {boolean} [isSensor] */
6546
+ addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
6547
+ {
6548
+ const shape = new box2d.instance.b2PolygonShape();
6549
+ shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
6550
+ return this.addShape(shape, density, friction, restitution, isSensor);
6551
+ }
6552
+
6553
+ /** Add a polygon shape to the body
6554
+ * @param {Array<Vector2>} points
6555
+ * @param {number} [density]
6556
+ * @param {number} [friction]
6557
+ * @param {number} [restitution]
6558
+ * @param {boolean} [isSensor] */
6559
+ addPoly(points, density, friction, restitution, isSensor)
6560
+ {
6561
+ function box2dCreatePolygonShape(points)
6562
+ {
6563
+ function box2dCreatePointList(points)
6564
+ {
6565
+ const buffer = box2d.instance._malloc(points.length * 8);
6566
+ for (let i=0, offset=0; i<points.length; ++i)
6567
+ {
6568
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x;
6569
+ offset += 4;
6570
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
6571
+ offset += 4;
6572
+ }
6573
+ return box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
6574
+ }
6575
+
6576
+ ASSERT(3 <= points.length && points.length <= 8);
6577
+ const shape = new box2d.instance.b2PolygonShape();
6578
+ const box2dPoints = box2dCreatePointList(points);
6579
+ shape.Set(box2dPoints, points.length);
6580
+ return shape;
6581
+ }
6582
+
6583
+ const shape = box2dCreatePolygonShape(points);
6584
+ return this.addShape(shape, density, friction, restitution, isSensor);
6585
+ }
6586
+
6587
+ /** Add a regular polygon shape to the body
6588
+ * @param {number} [diameter]
6589
+ * @param {number} [sides]
6590
+ * @param {number} [density]
6591
+ * @param {number} [friction]
6592
+ * @param {number} [restitution]
6593
+ * @param {boolean} [isSensor] */
6594
+ addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
6595
+ {
6596
+ const points = [];
6597
+ const radius = diameter/2;
6598
+ for (let i=sides; i--;)
6599
+ points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
6600
+ return this.addPoly(points, density, friction, restitution, isSensor);
6601
+ }
6602
+
6603
+ /** Add a random polygon shape to the body
6604
+ * @param {number} [diameter]
6605
+ * @param {number} [density]
6606
+ * @param {number} [friction]
6607
+ * @param {number} [restitution]
6608
+ * @param {boolean} [isSensor] */
6609
+ addRandomPoly(diameter=1, density, friction, restitution, isSensor)
6610
+ {
6611
+ const sides = randInt(3, 9);
6612
+ const points = [];
6613
+ const radius = diameter/2;
6614
+ for (let i=sides; i--;)
6615
+ points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2));
6616
+ return this.addPoly(points, density, friction, restitution, isSensor);
6617
+ }
6618
+
6619
+ /** Add a circle shape to the body
6620
+ * @param {number} [diameter]
6621
+ * @param {Vector2} [offset]
6622
+ * @param {number} [density]
6623
+ * @param {number} [friction]
6624
+ * @param {number} [restitution]
6625
+ * @param {boolean} [isSensor] */
6626
+ addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
6627
+ {
6628
+ const shape = new box2d.instance.b2CircleShape();
6629
+ shape.set_m_p(box2d.vec2dTo(offset));
6630
+ shape.set_m_radius(diameter/2);
6631
+ return this.addShape(shape, density, friction, restitution, isSensor);
6632
+ }
6633
+
6634
+ /** Add an edge shape to the body
6635
+ * @param {Vector2} point1
6636
+ * @param {Vector2} point2
6637
+ * @param {number} [density]
6638
+ * @param {number} [friction]
6639
+ * @param {number} [restitution]
6640
+ * @param {boolean} [isSensor] */
6641
+ addEdge(point1, point2, density, friction, restitution, isSensor)
6642
+ {
6643
+ const shape = new box2d.instance.b2EdgeShape();
6644
+ shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
6645
+ return this.addShape(shape, density, friction, restitution, isSensor);
6646
+ }
6647
+
6648
+ /** Add an edge loop to the body, an edge loop connects the end points
6649
+ * @param {Array<Vector2>} points
6650
+ * @param {number} [density]
6651
+ * @param {number} [friction]
6652
+ * @param {number} [restitution]
6653
+ * @param {boolean} [isSensor] */
6654
+ addEdgeLoop(points, density, friction, restitution, isSensor)
6655
+ {
6656
+ const fixtures = [];
6657
+ const getPoint = i=> points[mod(i,points.length)];
6658
+ for (let i=0; i<points.length; ++i)
6659
+ {
6660
+ const shape = new box2d.instance.b2EdgeShape();
6661
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
6662
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
6663
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
6664
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
6665
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
6666
+ fixtures.push(f);
6667
+ }
6668
+ return fixtures;
6669
+ }
6670
+
6671
+ /** Add an edge list to the body
6672
+ * @param {Array<Vector2>} points
6673
+ * @param {number} [density]
6674
+ * @param {number} [friction]
6675
+ * @param {number} [restitution]
6676
+ * @param {boolean} [isSensor] */
6677
+ addEdgeList(points, density, friction, restitution, isSensor)
6678
+ {
6679
+ const fixtures = [];
6680
+ for (let i=0; i<points.length-1; ++i)
6681
+ {
6682
+ const shape = new box2d.instance.b2EdgeShape();
6683
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
6684
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
6685
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
6686
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
6687
+ const f = this.addShape(shape, density, friction, restitution, isSensor);
6688
+ fixtures.push(f);
6689
+ }
6690
+ return fixtures;
6691
+ }
6692
+
6693
+ ///////////////////////////////////////////////////////////////////////////////
6694
+ // physics get functions
6695
+
6696
+ /** Gets the center of mass
6697
+ * @return {Vector2} */
6698
+ getCenterOfMass() { return box2d.vec2From(this.body.GetWorldCenter()); }
6699
+
6700
+ /** Gets the linear velocity
6701
+ * @return {Vector2} */
6702
+ getLinearVelocity() { return box2d.vec2From(this.body.GetLinearVelocity()); }
6703
+
6704
+ /** Gets the angular velocity
6705
+ * @return {Vector2} */
6706
+ getAngularVelocity() { return this.body.GetAngularVelocity(); }
6707
+
6708
+ /** Gets the mass
6709
+ * @return {number} */
6710
+ getMass() { return this.body.GetMass(); }
6711
+
6712
+ /** Gets the rotational inertia
6713
+ * @return {number} */
6714
+ getInertia() { return this.body.GetInertia(); }
6715
+
6716
+ /** Check if this object is awake
6717
+ * @return {boolean} */
6718
+ getIsAwake() { return this.body.IsAwake(); }
6719
+
6720
+ /** Gets the physics body type
6721
+ * @return {number} */
6722
+ getBodyType() { return this.body.GetType(); }
6723
+
6724
+ ///////////////////////////////////////////////////////////////////////////////
6725
+ // physics set functions
6726
+
6727
+ /** Sets the position and angle
6728
+ * @param {Vector2} pos
6729
+ * @param {number} angle */
6730
+ setTransform(pos, angle)
6731
+ {
6732
+ this.pos = pos;
6733
+ this.angle = angle;
6734
+ this.body.SetTransform(box2d.vec2dTo(pos), angle);
6735
+ }
6736
+
6737
+ /** Sets the position
6738
+ * @param {Vector2} pos */
6739
+ setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
6740
+
6741
+ /** Sets the angle
6742
+ * @param {number} angle */
6743
+ setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
6744
+
6745
+ /** Sets the linear velocity
6746
+ * @param {Vector2} velocity */
6747
+ setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
6748
+
6749
+ /** Sets the angular velocity
6750
+ * @param {number} angularVelocity */
6751
+ setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
6752
+
6753
+ /** Sets the linear damping
6754
+ * @param {number} damping */
6755
+ setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
6756
+
6757
+ /** Sets the angular damping
6758
+ * @param {number} damping */
6759
+ setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
6760
+
6761
+ /** Sets the gravity scale
6762
+ * @param {number} [scale] */
6763
+ setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
6764
+
6765
+ /** Should this body be treated like a bullet for continuous collision detection?
6766
+ * @param {boolean} [isBullet] */
6767
+ setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
6768
+
6769
+ /** Set the sleep state of the body
6770
+ * @param {boolean} [isAwake] */
6771
+ setAwake(isAwake=true) { this.body.SetAwake(isAwake); }
6772
+
6773
+ /** Set the physics body type
6774
+ * @param {number} type */
6775
+ setBodyType(type) { this.body.SetType(type); }
6776
+
6777
+ /** Set whether the body is allowed to sleep
6778
+ * @param {boolean} [isAllowed] */
6779
+ setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
6780
+
6781
+ /** Set whether the body can rotate
6782
+ * @param {boolean} [isFixed] */
6783
+ setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
6784
+
6785
+ /** Set the center of mass of the body
6786
+ * @param {Vector2} center */
6787
+ setCenterOfMass(center) { this.setMassData(center) }
6788
+
6789
+ /** Set the mass of the body
6790
+ * @param {number} mass */
6791
+ setMass(mass) { this.setMassData(undefined, mass) }
6792
+
6793
+ /** Set the moment of inertia of the body
6794
+ * @param {number} momentOfInertia */
6795
+ setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
6796
+
6797
+ /** Reset the mass, center of mass, and moment */
6798
+ resetMassData() { this.body.ResetMassData(); }
6799
+
6800
+ /** Set the mass data of the body
6801
+ * @param {Vector2} [localCenter]
6802
+ * @param {number} [mass]
6803
+ * @param {number} [momentOfInertia] */
6804
+ setMassData(localCenter, mass, momentOfInertia)
6805
+ {
6806
+ const data = new box2d.instance.b2MassData();
6807
+ this.body.GetMassData(data);
6808
+ localCenter && data.set_center(box2d.vec2dTo(localCenter));
6809
+ mass && data.set_mass(mass);
6810
+ momentOfInertia && data.set_I(momentOfInertia);
6811
+ this.body.SetMassData(data);
6812
+ }
6813
+
6814
+ /** Set the collision filter data for this body
6815
+ * @param {number} [categoryBits]
6816
+ * @param {number} [ignoreCategoryBits]
6817
+ * @param {number} [groupIndex] */
6818
+ setFilterData(categoryBits=0, ignoreCategoryBits=0, groupIndex=0)
6819
+ {
6820
+ this.getFixtureList().forEach(fixture=>
6821
+ {
6822
+ const filter = fixture.GetFilterData();
6823
+ filter.set_categoryBits(categoryBits);
6824
+ filter.set_maskBits(0xffff & ~ignoreCategoryBits);
6825
+ filter.set_groupIndex(groupIndex);
6826
+ });
6827
+ }
6828
+
6829
+ /** Set if this body is a sensor
6830
+ * @param {boolean} [isSensor] */
6831
+ setSensor(isSensor=true)
6832
+ { this.getFixtureList().forEach(f=>f.SetSensor(isSensor)); }
6833
+
6834
+ ///////////////////////////////////////////////////////////////////////////////
6835
+ // physics force and torque functions
6836
+
6837
+ /** Apply force to this object
6838
+ * @param {Vector2} force
6839
+ * @param {Vector2} [pos] */
6840
+ applyForce(force, pos)
6841
+ {
6842
+ pos ||= this.getCenterOfMass();
6843
+ this.setAwake();
6844
+ this.body.ApplyForce(box2d.vec2dTo(force), box2d.vec2dTo(pos));
6845
+ }
6846
+
6847
+ /** Apply acceleration to this object
6848
+ * @param {Vector2} acceleration
6849
+ * @param {Vector2} [pos] */
6850
+ applyAcceleration(acceleration, pos)
6851
+ {
6852
+ pos ||= this.getCenterOfMass();
6853
+ this.setAwake();
6854
+ this.body.ApplyLinearImpulse(box2d.vec2dTo(acceleration), box2d.vec2dTo(pos));
6855
+ }
6856
+
6857
+ /** Apply torque to this object
6858
+ * @param {number} torque */
6859
+ applyTorque(torque)
6860
+ {
6861
+ this.setAwake();
6862
+ this.body.ApplyTorque(torque);
6863
+ }
6864
+
6865
+ /** Apply angular acceleration to this object
6866
+ * @param {number} acceleration */
6867
+ applyAngularAcceleration(acceleration)
6868
+ {
6869
+ this.setAwake();
6870
+ this.body.ApplyAngularImpulse(acceleration);
6871
+ }
6872
+
6873
+ ///////////////////////////////////////////////////////////////////////////////
6874
+ // lists of fixtures and joints
6875
+
6876
+ /** Check if this object has any fixtures
6877
+ * @return {boolean} */
6878
+ hasFixtures() { return !box2d.isNull(this.body.GetFixtureList()); }
6879
+
6880
+ /** Get list of fixtures for this object
6881
+ * @return {Array<Object>} */
6882
+ getFixtureList()
6883
+ {
6884
+ const fixtures = [];
6885
+ for (let fixture=this.body.GetFixtureList(); !box2d.isNull(fixture); )
6886
+ {
6887
+ fixtures.push(fixture);
6888
+ fixture = fixture.GetNext();
6889
+ }
6890
+ return fixtures;
6891
+ }
6892
+
6893
+ /** Check if this object has any joints
6894
+ * @return {boolean} */
6895
+ hasJoints() { return !box2d.isNull(this.body.GetJointList()); }
6896
+
6897
+ /** Get list of joints for this object
6898
+ * @return {Array<Object>} */
6899
+ getJointList()
6900
+ {
6901
+ const joints = [];
6902
+ for (let joint=this.body.GetJointList(); !box2d.isNull(joint); )
6903
+ {
6904
+ joints.push(joint);
6905
+ joint = joint.get_next();
6906
+ }
6907
+ return joints;
6908
+ }
6909
+ }
6910
+
6911
+ ///////////////////////////////////////////////////////////////////////////////
6912
+ /**
6913
+ * Box2D Raycast Result
6914
+ * - Holds results from a box2d raycast queries
6915
+ * - Automatically created by box2d raycast functions
6916
+ */
6917
+ class Box2dRaycastResult
6918
+ {
6919
+ /** Create a raycast result
6920
+ * @param {Object} fixture
6921
+ * @param {Vector2} point
6922
+ * @param {Vector2} normal
6923
+ * @param {number} fraction */
6924
+ constructor(fixture, point, normal, fraction)
6925
+ {
6926
+ /** @property {Box2dObject} - The box2d object */
6927
+ this.object = fixture.GetBody().object;
6928
+ /** @property {Object} - The fixture that was hit */
6929
+ this.fixture = fixture;
6930
+ /** @property {Vector2} - The hit point */
6931
+ this.point = point;
6932
+ /** @property {Vector2} - The hit normal */
6933
+ this.normal = normal;
6934
+ /** @property {number} - Distance fraction at the point of intersection */
6935
+ this.fraction = fraction;
6936
+ }
6937
+ }
6938
+
6939
+ ///////////////////////////////////////////////////////////////////////////////
6940
+ /**
6941
+ * Box2D Joint
6942
+ * - Base class for Box2D joints
6943
+ * - A joint is used to connect objects together
6944
+ */
6945
+ class Box2dJoint
6946
+ {
6947
+ /** Create a box2d joint, the base class is not intended to be used directly
6948
+ * @param {Object} jointDef */
6949
+ constructor(jointDef)
6950
+ {
6951
+ this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
6952
+ }
6953
+
6954
+ /** Destroy this joint */
6955
+ destroy() { box2d.world.DestroyJoint(this.box2dJoint); this.box2dJoint = 0; }
6956
+
6957
+ /** Get the first object attached to this joint
6958
+ * @return {Box2dObject} */
6959
+ getObjectA() { return this.box2dJoint.GetBodyA().object; }
6960
+
6961
+ /** Get the second object attached to this joint
6962
+ * @return {Box2dObject} */
6963
+ getObjectB() { return this.box2dJoint.GetBodyB().object; }
6964
+
6965
+ /** Get the first anchor for this joint in world coordinates
6966
+ * @return {Vector2} */
6967
+ getAnchorA() { return box2d.vec2From(this.box2dJoint.GetAnchorA());}
6968
+
6969
+ /** Get the second anchor for this joint in world coordinates
6970
+ * @return {Vector2} */
6971
+ getAnchorB() { return box2d.vec2From(this.box2dJoint.GetAnchorB());}
6972
+
6973
+ /** Get the reaction force on bodyB at the joint anchor given a time step
6974
+ * @param {number} time
6975
+ * @return {Vector2} */
6976
+ getReactionForce(time) { return box2d.vec2From(this.box2dJoint.GetReactionForce(1/time));}
6977
+
6978
+ /** Get the reaction torque on bodyB in N*m given a time step
6979
+ * @param {number} time
6980
+ * @return {number} */
6981
+ getReactionTorque(time) { return this.box2dJoint.GetReactionTorque(1/time);}
6982
+
6983
+ /** Check if the connected bodies should collide
6984
+ * @return {boolean} */
6985
+ getCollideConnected() { return this.box2dJoint.getCollideConnected();}
6986
+
6987
+ /** Check if either connected body is active
6988
+ * @return {boolean} */
6989
+ isActive() { return this.box2dJoint.IsActive();}
6990
+ }
6991
+
6992
+ ///////////////////////////////////////////////////////////////////////////////
6993
+ /**
6994
+ * Box2D Target Joint, also known as a mouse joint
6995
+ * - Used to make a point on a object track a specific world point target
6996
+ * - This a soft constraint with a max force
6997
+ * - This allows the constraint to stretch and without applying huge forces
6998
+ * @extends Box2dJoint
6999
+ */
7000
+ class Box2dTargetJoint extends Box2dJoint
7001
+ {
7002
+ /** Create a target joint
7003
+ * @param {Box2dObject} object
7004
+ * @param {Box2dObject} fixedObject
7005
+ * @param {Vector2} worldPos */
7006
+ constructor(object, fixedObject, worldPos)
7007
+ {
7008
+ object.setAwake();
7009
+ const jointDef = new box2d.instance.b2MouseJointDef();
7010
+ jointDef.set_bodyA(fixedObject.body);
7011
+ jointDef.set_bodyB(object.body);
7012
+ jointDef.set_target(box2d.vec2dTo(worldPos));
7013
+ jointDef.set_maxForce(2e3 * object.getMass());
7014
+ super(jointDef);
7015
+ }
7016
+
7017
+ /** Set the target point in world coordinates
7018
+ * @param {Vector2} pos */
7019
+ setTarget(pos) { this.box2dJoint.SetTarget(box2d.vec2dTo(pos)); }
7020
+
7021
+ /** Get the target point in world coordinates
7022
+ * @return {Vector2} */
7023
+ getTarget(){ return box2d.vec2From(this.box2dJoint.GetTarget()); }
7024
+
7025
+ /** Sets the maximum force in Newtons
7026
+ * @param {number} force */
7027
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
7028
+
7029
+ /** Gets the maximum force in Newtons
7030
+ * @return {number} */
7031
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
7032
+
7033
+ /** Sets the joint frequency in Hertz
7034
+ * @param {number} hz */
7035
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7036
+
7037
+ /** Gets the joint frequency in Hertz
7038
+ * @return {number} */
7039
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7040
+ }
7041
+
7042
+ ///////////////////////////////////////////////////////////////////////////////
7043
+ /**
7044
+ * Box2D Distance Joint
7045
+ * - Constrains two points on two objects to remain at a fixed distance
7046
+ * - You can view this as a massless, rigid rod
7047
+ * @extends Box2dJoint
7048
+ */
7049
+ class Box2dDistanceJoint extends Box2dJoint
7050
+ {
7051
+ /** Create a distance joint
7052
+ * @param {Box2dObject} objectA
7053
+ * @param {Box2dObject} objectB
7054
+ * @param {Vector2} anchorA
7055
+ * @param {Vector2} anchorB
7056
+ * @param {boolean} [collide] */
7057
+ constructor(objectA, objectB, anchorA, anchorB, collide=false)
7058
+ {
7059
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7060
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7061
+ const localAnchorA = objectA.worldToLocal(anchorA);
7062
+ const localAnchorB = objectB.worldToLocal(anchorB);
7063
+ const jointDef = new box2d.instance.b2DistanceJointDef();
7064
+ jointDef.set_bodyA(objectA.body);
7065
+ jointDef.set_bodyB(objectB.body);
7066
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7067
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7068
+ jointDef.set_length(anchorA.distance(anchorB));
7069
+ jointDef.set_collideConnected(collide);
7070
+ super(jointDef);
7071
+ }
7072
+
7073
+ /** Get the local anchor point relative to objectA's origin
7074
+ * @return {Vector2} */
7075
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7076
+
7077
+ /** Get the local anchor point relative to objectB's origin
7078
+ * @return {Vector2} */
7079
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7080
+
7081
+ /** Set the length of the joint
7082
+ * @param {number} length */
7083
+ setLength(length) { this.box2dJoint.SetLength(length); }
7084
+
7085
+ /** Get the length of the joint
7086
+ * @return {number} */
7087
+ getLength() { return this.box2dJoint.GetLength(); }
7088
+
7089
+ /** Set the frequency in Hertz
7090
+ * @param {number} hz */
7091
+ setFrequency(hz) { this.box2dJoint.SetFrequency(hz); }
7092
+
7093
+ /** Get the frequency in Hertz
7094
+ * @return {number} */
7095
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7096
+
7097
+ /** Set the damping ratio
7098
+ * @param {number} ratio */
7099
+ setDampingRatio(ratio) { this.box2dJoint.SetDampingRatio(ratio); }
7100
+
7101
+ /** Get the damping ratio
7102
+ * @return {number} */
7103
+ getDampingRatio() { return this.box2dJoint.GetDampingRatio(); }
7104
+ }
7105
+
7106
+ ///////////////////////////////////////////////////////////////////////////////
7107
+ /**
7108
+ * Box2D Pin Joint
7109
+ * - Pins two objects together at a point
7110
+ * @extends Box2dDistanceJoint
7111
+ */
7112
+ class Box2dPinJoint extends Box2dDistanceJoint
7113
+ {
7114
+ /** Create a pin joint
7115
+ * @param {Box2dObject} objectA
7116
+ * @param {Box2dObject} objectB
7117
+ * @param {Vector2} [pos]
7118
+ * @param {boolean} [collide] */
7119
+ constructor(objectA, objectB, pos=objectA.pos, collide=false)
7120
+ {
7121
+ super(objectA, objectB, undefined, pos, collide);
7122
+ }
7123
+ }
7124
+
7125
+ ///////////////////////////////////////////////////////////////////////////////
7126
+ /**
7127
+ * Box2D Rope Joint
7128
+ * - Enforces a maximum distance between two points on two objects
7129
+ * @extends Box2dJoint
7130
+ */
7131
+ class Box2dRopeJoint extends Box2dJoint
7132
+ {
7133
+ /** Create a rope joint
7134
+ * @param {Box2dObject} objectA
7135
+ * @param {Box2dObject} objectB
7136
+ * @param {Vector2} anchorA
7137
+ * @param {Vector2} anchorB
7138
+ * @param {number} extraLength
7139
+ * @param {boolean} [collide] */
7140
+ constructor(objectA, objectB, anchorA, anchorB, extraLength=0, collide=false)
7141
+ {
7142
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7143
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7144
+ const localAnchorA = objectA.worldToLocal(anchorA);
7145
+ const localAnchorB = objectB.worldToLocal(anchorB);
7146
+ const jointDef = new box2d.instance.b2RopeJointDef();
7147
+ jointDef.set_bodyA(objectA.body);
7148
+ jointDef.set_bodyB(objectB.body);
7149
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7150
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7151
+ jointDef.set_maxLength(anchorA.distance(anchorB)+extraLength);
7152
+ jointDef.set_collideConnected(collide);
7153
+ super(jointDef);
7154
+ }
7155
+
7156
+ /** Get the local anchor point relative to objectA's origin
7157
+ * @return {Vector2} */
7158
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7159
+
7160
+ /** Get the local anchor point relative to objectB's origin
7161
+ * @return {Vector2} */
7162
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7163
+
7164
+ /** Set the max length of the joint
7165
+ * @param {number} length */
7166
+ setMaxLength(length) { this.box2dJoint.SetMaxLength(length); }
7167
+
7168
+ /** Get the max length of the joint
7169
+ * @return {number} */
7170
+ getMaxLength() { return this.box2dJoint.GetMaxLength(); }
7171
+ }
7172
+
7173
+ ///////////////////////////////////////////////////////////////////////////////
7174
+ /**
7175
+ * Box2D Revolute Joint
7176
+ * - Constrains two objects to share a point while they are free to rotate around the point
7177
+ * - The relative rotation about the shared point is the joint angle
7178
+ * - You can limit the relative rotation with a joint limit
7179
+ * - You can use a motor to drive the relative rotation about the shared point
7180
+ * - A maximum motor torque is provided so that infinite forces are not generated
7181
+ * @extends Box2dJoint
7182
+ */
7183
+ class Box2dRevoluteJoint extends Box2dJoint
7184
+ {
7185
+ /** Create a revolute joint
7186
+ * @param {Box2dObject} objectA
7187
+ * @param {Box2dObject} objectB
7188
+ * @param {Vector2} anchor
7189
+ * @param {boolean} [collide] */
7190
+ constructor(objectA, objectB, anchor, collide=false)
7191
+ {
7192
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7193
+ const localAnchorA = objectA.worldToLocal(anchor);
7194
+ const localAnchorB = objectB.worldToLocal(anchor);
7195
+ const jointDef = new box2d.instance.b2RevoluteJointDef();
7196
+ jointDef.set_bodyA(objectA.body);
7197
+ jointDef.set_bodyB(objectB.body);
7198
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7199
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7200
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7201
+ jointDef.set_collideConnected(collide);
7202
+ super(jointDef);
7203
+ }
7204
+
7205
+ /** Get the local anchor point relative to objectA's origin
7206
+ * @return {Vector2} */
7207
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7208
+
7209
+ /** Get the local anchor point relative to objectB's origin
7210
+ * @return {Vector2} */
7211
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7212
+
7213
+ /** Get the reference angle, objectB angle minus objectA angle in the reference state
7214
+ * @return {number} */
7215
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7216
+
7217
+ /** Get the current joint angle
7218
+ * @return {number} */
7219
+ getJointAngle() { return this.box2dJoint.GetJointAngle(); }
7220
+
7221
+ /** Get the current joint angle speed in radians per second
7222
+ * @return {number} */
7223
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7224
+
7225
+ /** Is the joint limit enabled?
7226
+ * @return {boolean} */
7227
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
7228
+
7229
+ /** Enable/disable the joint limit
7230
+ * @param {boolean} [enable] */
7231
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
7232
+
7233
+ /** Get the lower joint limit
7234
+ * @return {number} */
7235
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
7236
+
7237
+ /** Get the upper joint limit
7238
+ * @return {number} */
7239
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
7240
+
7241
+ /** Set the joint limits
7242
+ * @param {number} min
7243
+ * @param {number} max */
7244
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
7245
+
7246
+ /** Is the joint motor enabled?
7247
+ * @return {boolean} */
7248
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7249
+
7250
+ /** Enable/disable the joint motor
7251
+ * @param {boolean} [enable] */
7252
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7253
+
7254
+ /** Set the motor speed
7255
+ * @param {number} speed */
7256
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7257
+
7258
+ /** Get the motor speed
7259
+ * @return {number} */
7260
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7261
+
7262
+ /** Set the motor torque
7263
+ * @param {number} torque */
7264
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
7265
+
7266
+ /** Get the max motor torque
7267
+ * @return {number} */
7268
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
7269
+
7270
+ /** Get the motor torque given a time step
7271
+ * @param {number} time
7272
+ * @return {number} */
7273
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
7274
+ }
7275
+
7276
+ ///////////////////////////////////////////////////////////////////////////////
7277
+ /**
7278
+ * Box2D Gear Joint
7279
+ * - A gear joint is used to connect two joints together
7280
+ * - Either joint can be a revolute or prismatic joint
7281
+ * - You specify a gear ratio to bind the motions together
7282
+ * @extends Box2dJoint
7283
+ */
7284
+ class Box2dGearJoint extends Box2dJoint
7285
+ {
7286
+ /** Create a gear joint
7287
+ * @param {Box2dObject} objectA
7288
+ * @param {Box2dObject} objectB
7289
+ * @param {Box2dJoint} joint1
7290
+ * @param {Box2dJoint} joint2
7291
+ * @param {ratio} [ratio] */
7292
+ constructor(objectA, objectB, joint1, joint2, ratio=1)
7293
+ {
7294
+ const jointDef = new box2d.instance.b2GearJointDef();
7295
+ jointDef.set_bodyA(objectA.body);
7296
+ jointDef.set_bodyB(objectB.body);
7297
+ jointDef.set_joint1(joint1.box2dJoint);
7298
+ jointDef.set_joint2(joint2.box2dJoint);
7299
+ jointDef.set_ratio(ratio);
7300
+ super(jointDef);
7301
+
7302
+ this.joint1 = joint1;
7303
+ this.joint2 = joint2;
7304
+ }
7305
+
7306
+ /** Get the first joint
7307
+ * @return {Box2dJoint} */
7308
+ getJoint1() { return this.joint1; }
7309
+
7310
+ /** Get the second joint
7311
+ * @return {Box2dJoint} */
7312
+ getJoint2() { return this.joint2; }
7313
+
7314
+ /** Set the gear ratio
7315
+ * @param {number} ratio */
7316
+ setRatio(ratio) { return this.box2dJoint.SetRatio(ratio); }
7317
+
7318
+ /** Get the gear ratio
7319
+ * @return {number} */
7320
+ getRatio() { return this.box2dJoint.GetRatio(); }
7321
+ }
7322
+
7323
+ ///////////////////////////////////////////////////////////////////////////////
7324
+ /**
7325
+ * Box2D Prismatic Joint
7326
+ * - Provides one degree of freedom: translation along an axis fixed in objectA
7327
+ * - Relative rotation is prevented
7328
+ * - You can use a joint limit to restrict the range of motion
7329
+ * - You can use a joint motor to drive the motion or to model joint friction
7330
+ * @extends Box2dJoint
7331
+ */
7332
+ class Box2dPrismaticJoint extends Box2dJoint
7333
+ {
7334
+ /** Create a prismatic joint
7335
+ * @param {Box2dObject} objectA
7336
+ * @param {Box2dObject} objectB
7337
+ * @param {Vector2} anchor
7338
+ * @param {Vector2} worldAxis
7339
+ * @param {boolean} [collide] */
7340
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
7341
+ {
7342
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7343
+ const localAnchorA = objectA.worldToLocal(anchor);
7344
+ const localAnchorB = objectB.worldToLocal(anchor);
7345
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
7346
+ const jointDef = new box2d.instance.b2PrismaticJointDef();
7347
+ jointDef.set_bodyA(objectA.body);
7348
+ jointDef.set_bodyB(objectB.body);
7349
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7350
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7351
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
7352
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7353
+ jointDef.set_collideConnected(collide);
7354
+ super(jointDef);
7355
+ }
7356
+
7357
+ /** Get the local anchor point relative to objectA's origin
7358
+ * @return {Vector2} */
7359
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7360
+
7361
+ /** Get the local anchor point relative to objectB's origin
7362
+ * @return {Vector2} */
7363
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7364
+
7365
+ /** Get the local joint axis relative to bodyA
7366
+ * @return {Vector2} */
7367
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
7368
+
7369
+ /** Get the reference angle
7370
+ * @return {number} */
7371
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7372
+
7373
+ /** Get the current joint translation
7374
+ * @return {number} */
7375
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
7376
+
7377
+ /** Get the current joint translation speed
7378
+ * @return {number} */
7379
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7380
+
7381
+ /** Is the joint limit enabled?
7382
+ * @return {boolean} */
7383
+ isLimitEnabled() { return this.box2dJoint.IsLimitEnabled(); }
7384
+
7385
+ /** Enable/disable the joint limit
7386
+ * @param {boolean} [enable] */
7387
+ enableLimit(enable=true) { return this.box2dJoint.enableLimit(enable); }
7388
+
7389
+ /** Get the lower joint limit
7390
+ * @return {number} */
7391
+ getLowerLimit() { return this.box2dJoint.GetLowerLimit(); }
7392
+
7393
+ /** Get the upper joint limit
7394
+ * @return {number} */
7395
+ getUpperLimit() { return this.box2dJoint.GetUpperLimit(); }
7396
+
7397
+ /** Set the joint limits
7398
+ * @param {number} min
7399
+ * @param {number} max */
7400
+ setLimits(min, max) { return this.box2dJoint.SetLimits(min, max); }
7401
+
7402
+ /** Is the motor enabled?
7403
+ * @return {boolean} */
7404
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7405
+
7406
+ /** Enable/disable the joint motor
7407
+ * @param {boolean} [enable] */
7408
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7409
+
7410
+ /** Set the motor speed
7411
+ * @param {number} speed */
7412
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7413
+
7414
+ /** Get the motor speed
7415
+ * @return {number} */
7416
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7417
+
7418
+ /** Set the maximum motor force
7419
+ * @param {number} force */
7420
+ setMaxMotorForce(force) { return this.box2dJoint.SetMaxMotorForce(force); }
7421
+
7422
+ /** Get the maximum motor force
7423
+ * @return {number} */
7424
+ getMaxMotorForce() { return this.box2dJoint.GetMaxMotorForce(); }
7425
+
7426
+ /** Get the motor force given a time step
7427
+ * @param {number} time
7428
+ * @return {number} */
7429
+ getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
7430
+ }
7431
+
7432
+ ///////////////////////////////////////////////////////////////////////////////
7433
+ /**
7434
+ * Box2D Wheel Joint
7435
+ * - Provides two degrees of freedom: translation along an axis fixed in objectA and rotation
7436
+ * - You can use a joint limit to restrict the range of motion
7437
+ * - You can use a joint motor to drive the motion or to model joint friction
7438
+ * - This joint is designed for vehicle suspensions
7439
+ * @extends Box2dJoint
7440
+ */
7441
+ class Box2dWheelJoint extends Box2dJoint
7442
+ {
7443
+ /** Create a wheel joint
7444
+ * @param {Box2dObject} objectA
7445
+ * @param {Box2dObject} objectB
7446
+ * @param {Vector2} anchor
7447
+ * @param {Vector2} worldAxis
7448
+ * @param {boolean} [collide] */
7449
+ constructor(objectA, objectB, anchor, worldAxis=vec2(0,1), collide=false)
7450
+ {
7451
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7452
+ const localAnchorA = objectA.worldToLocal(anchor);
7453
+ const localAnchorB = objectB.worldToLocal(anchor);
7454
+ const localAxisA = objectB.worldToLocalVector(worldAxis);
7455
+ const jointDef = new box2d.instance.b2WheelJointDef();
7456
+ jointDef.set_bodyA(objectA.body);
7457
+ jointDef.set_bodyB(objectB.body);
7458
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7459
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7460
+ jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
7461
+ jointDef.set_collideConnected(collide);
7462
+ super(jointDef);
7463
+ }
7464
+
7465
+ /** Get the local anchor point relative to objectA's origin
7466
+ * @return {Vector2} */
7467
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7468
+
7469
+ /** Get the local anchor point relative to objectB's origin
7470
+ * @return {Vector2} */
7471
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7472
+
7473
+ /** Get the local joint axis relative to bodyA
7474
+ * @return {Vector2} */
7475
+ getLocalAxisA() { return box2d.vec2From(this.box2dJoint.GetLocalAxisA()); }
7476
+
7477
+ /** Get the current joint translation
7478
+ * @return {number} */
7479
+ getJointTranslation() { return this.box2dJoint.GetJointTranslation(); }
7480
+
7481
+ /** Get the current joint translation speed
7482
+ * @return {number} */
7483
+ getJointSpeed() { return this.box2dJoint.GetJointSpeed(); }
7484
+
7485
+ /** Is the joint motor enabled?
7486
+ * @return {boolean} */
7487
+ isMotorEnabled() { return this.box2dJoint.IsMotorEnabled(); }
7488
+
7489
+ /** Enable/disable the joint motor
7490
+ * @param {boolean} [enable] */
7491
+ enableMotor(enable=true) { return this.box2dJoint.EnableMotor(enable); }
7492
+
7493
+ /** Set the motor speed
7494
+ * @param {number} speed */
7495
+ setMotorSpeed(speed) { return this.box2dJoint.SetMotorSpeed(speed); }
7496
+
7497
+ /** Get the motor speed
7498
+ * @return {number} */
7499
+ getMotorSpeed() { return this.box2dJoint.GetMotorSpeed(); }
7500
+
7501
+ /** Set the maximum motor torque
7502
+ * @param {number} torque */
7503
+ setMaxMotorTorque(torque) { return this.box2dJoint.SetMaxMotorTorque(torque); }
7504
+
7505
+ /** Get the max motor torque
7506
+ * @return {number} */
7507
+ getMaxMotorTorque() { return this.box2dJoint.GetMaxMotorTorque(); }
7508
+
7509
+ /** Get the motor torque for a time step
7510
+ * @return {number} */
7511
+ getMotorTorque(time) { return this.box2dJoint.GetMotorTorque(1/time); }
7512
+
7513
+ /** Set the spring frequency in Hertz
7514
+ * @param {number} hz */
7515
+ setSpringFrequencyHz(hz) { return this.box2dJoint.SetSpringFrequencyHz(hz); }
7516
+
7517
+ /** Get the spring frequency in Hertz
7518
+ * @return {number} */
7519
+ getSpringFrequencyHz() { return this.box2dJoint.GetSpringFrequencyHz(); }
7520
+
7521
+ /** Set the spring damping ratio
7522
+ * @param {number} ratio */
7523
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
7524
+
7525
+ /** Get the spring damping ratio
7526
+ * @return {number} */
7527
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
7528
+ }
7529
+
7530
+ ///////////////////////////////////////////////////////////////////////////////
7531
+ /**
7532
+ * Box2D Weld Joint
7533
+ * - Glues two objects together
7534
+ * @extends Box2dJoint
7535
+ */
7536
+ class Box2dWeldJoint extends Box2dJoint
7537
+ {
7538
+ /** Create a weld joint
7539
+ * @param {Box2dObject} objectA
7540
+ * @param {Box2dObject} objectB
7541
+ * @param {Vector2} anchor
7542
+ * @param {boolean} [collide] */
7543
+ constructor(objectA, objectB, anchor, collide=false)
7544
+ {
7545
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7546
+ const localAnchorA = objectA.worldToLocal(anchor);
7547
+ const localAnchorB = objectB.worldToLocal(anchor);
7548
+ const jointDef = new box2d.instance.b2WeldJointDef();
7549
+ jointDef.set_bodyA(objectA.body);
7550
+ jointDef.set_bodyB(objectB.body);
7551
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7552
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7553
+ jointDef.set_referenceAngle(objectA.body.GetAngle() - objectB.body.GetAngle());
7554
+ jointDef.set_collideConnected(collide);
7555
+ super(jointDef);
7556
+ }
7557
+
7558
+ /** Get the local anchor point relative to objectA's origin
7559
+ * @return {Vector2} */
7560
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7561
+
7562
+ /** Get the local anchor point relative to objectB's origin
7563
+ * @return {Vector2} */
7564
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7565
+
7566
+ /** Get the reference angle
7567
+ * @return {number} */
7568
+ getReferenceAngle() { return this.box2dJoint.GetReferenceAngle(); }
7569
+
7570
+ /** Set the frequency in Hertz
7571
+ * @param {number} hz */
7572
+ setFrequency(hz) { return this.box2dJoint.SetFrequency(hz); }
7573
+
7574
+ /** Get the frequency in Hertz
7575
+ * @return {number} */
7576
+ getFrequency() { return this.box2dJoint.GetFrequency(); }
7577
+
7578
+ /** Set the damping ratio
7579
+ * @param {number} ratio */
7580
+ setSpringDampingRatio(ratio) { return this.box2dJoint.SetSpringDampingRatio(ratio); }
7581
+
7582
+ /** Get the damping ratio
7583
+ * @return {number} */
7584
+ getSpringDampingRatio() { return this.box2dJoint.GetSpringDampingRatio(); }
7585
+ }
7586
+
7587
+ ///////////////////////////////////////////////////////////////////////////////
7588
+ /**
7589
+ * Box2D Friction Joint
7590
+ * - Used to apply top-down friction
7591
+ * - Provides 2D translational friction and angular friction
7592
+ * @extends Box2dJoint
7593
+ */
7594
+ class Box2dFrictionJoint extends Box2dJoint
7595
+ {
7596
+ /** Create a friction joint
7597
+ * @param {Box2dObject} objectA
7598
+ * @param {Box2dObject} objectB
7599
+ * @param {Vector2} anchor
7600
+ * @param {boolean} [collide] */
7601
+ constructor(objectA, objectB, anchor, collide=false)
7602
+ {
7603
+ anchor ||= box2d.vec2From(objectB.body.GetPosition());
7604
+ const localAnchorA = objectA.worldToLocal(anchor);
7605
+ const localAnchorB = objectB.worldToLocal(anchor);
7606
+ const jointDef = new box2d.instance.b2FrictionJointDef();
7607
+ jointDef.set_bodyA(objectA.body);
7608
+ jointDef.set_bodyB(objectB.body);
7609
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7610
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7611
+ jointDef.set_collideConnected(collide);
7612
+ super(jointDef);
7613
+ }
7614
+
7615
+ /** Get the local anchor point relative to objectA's origin
7616
+ * @return {Vector2} */
7617
+ getLocalAnchorA() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorA()); }
7618
+
7619
+ /** Get the local anchor point relative to objectB's origin
7620
+ * @return {Vector2} */
7621
+ getLocalAnchorB() { return box2d.vec2From(this.box2dJoint.GetLocalAnchorB()); }
7622
+
7623
+ /** Set the maximum friction force
7624
+ * @param {number} force */
7625
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
7626
+
7627
+ /** Get the maximum friction force
7628
+ * @return {number} */
7629
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
7630
+
7631
+ /** Set the maximum friction torque
7632
+ * @param {number} torque */
7633
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
7634
+
7635
+ /** Get the maximum friction torque
7636
+ * @return {number} */
7637
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
7638
+ }
7639
+
7640
+ ///////////////////////////////////////////////////////////////////////////////
7641
+ /**
7642
+ * Box2D Pulley Joint
7643
+ * - Connects to two objects and two fixed ground points
7644
+ * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
7645
+ * - The force transmitted is scaled by the ratio
7646
+ * @extends Box2dJoint
7647
+ */
7648
+ class Box2dPulleyJoint extends Box2dJoint
7649
+ {
7650
+ /** Create a pulley joint
7651
+ * @param {Box2dObject} objectA
7652
+ * @param {Box2dObject} objectB
7653
+ * @param {Vector2} groundAnchorA
7654
+ * @param {Vector2} groundAnchorB
7655
+ * @param {Vector2} anchorA
7656
+ * @param {Vector2} anchorB
7657
+ * @param {number} [ratio]
7658
+ * @param {boolean} [collide] */
7659
+ constructor(objectA, objectB, groundAnchorA, groundAnchorB, anchorA, anchorB, ratio=1, collide=false)
7660
+ {
7661
+ anchorA ||= box2d.vec2From(objectA.body.GetPosition());
7662
+ anchorB ||= box2d.vec2From(objectB.body.GetPosition());
7663
+ const localAnchorA = objectA.worldToLocal(anchorA);
7664
+ const localAnchorB = objectB.worldToLocal(anchorB);
7665
+ const jointDef = new box2d.instance.b2PulleyJointDef();
7666
+ jointDef.set_bodyA(objectA.body);
7667
+ jointDef.set_bodyB(objectB.body);
7668
+ jointDef.set_groundAnchorA(box2d.vec2dTo(groundAnchorA));
7669
+ jointDef.set_groundAnchorB(box2d.vec2dTo(groundAnchorB));
7670
+ jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
7671
+ jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
7672
+ jointDef.set_ratio(ratio);
7673
+ jointDef.set_lengthA(groundAnchorA.distance(anchorA));
7674
+ jointDef.set_lengthB(groundAnchorB.distance(anchorB));
7675
+ jointDef.set_collideConnected(collide);
7676
+ super(jointDef);
7677
+ }
7678
+
7679
+ /** Get the first ground anchor
7680
+ * @return {Vector2} */
7681
+ getGroundAnchorA() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorA()); }
7682
+
7683
+ /** Get the second ground anchor
7684
+ * @return {Vector2} */
7685
+ getGroundAnchorB() { return box2d.vec2From(this.box2dJoint.GetGroundAnchorB()); }
7686
+
7687
+ /** Get the current length of the segment attached to objectA
7688
+ * @return {number} */
7689
+ getLengthA() { return this.box2dJoint.GetLengthA(); }
7690
+
7691
+ /** Get the current length of the segment attached to objectB
7692
+ * @return {number} */
7693
+ getLengthB(){ return this.box2dJoint.GetLengthB(); }
7694
+
7695
+ /** Get the pulley ratio
7696
+ * @return {number} */
7697
+ getRatio() { return this.box2dJoint.GetRatio(); }
7698
+
7699
+ /** Get the current length of the segment attached to objectA
7700
+ * @return {number} */
7701
+ getCurrentLengthA() { return this.box2dJoint.GetCurrentLengthA(); }
7702
+
7703
+ /** Get the current length of the segment attached to objectB
7704
+ * @return {number} */
7705
+ getCurrentLengthB() { return this.box2dJoint.GetCurrentLengthB(); }
7706
+ }
7707
+
7708
+ ///////////////////////////////////////////////////////////////////////////////
7709
+ /**
7710
+ * Box2D Motor Joint
7711
+ * - Controls the relative motion between two objects
7712
+ * - Typical usage is to control the movement of a object with respect to the ground
7713
+ * @extends Box2dJoint
7714
+ */
7715
+ class Box2dMotorJoint extends Box2dJoint
7716
+ {
7717
+ /** Create a motor joint
7718
+ * @param {Box2dObject} objectA
7719
+ * @param {Box2dObject} objectB */
7720
+ constructor(objectA, objectB)
7721
+ {
7722
+ const linearOffset = objectA.worldToLocal(box2d.vec2From(objectB.body.GetPosition()));
7723
+ const angularOffset = objectB.body.GetAngle() - objectA.body.GetAngle();
7724
+ const jointDef = new box2d.instance.b2MotorJointDef();
7725
+ jointDef.set_bodyA(objectA.body);
7726
+ jointDef.set_bodyB(objectB.body);
7727
+ jointDef.set_linearOffset(box2d.vec2dTo(linearOffset));
7728
+ jointDef.set_angularOffset(angularOffset);
7729
+ super(jointDef);
7730
+ }
7731
+
7732
+ /** Set the target linear offset, in frame A, in meters.
7733
+ * @param {Vector2} offset */
7734
+ setLinearOffset(offset) { this.box2dJoint.SetLinearOffset(box2d.vec2dTo(offset)); }
7735
+
7736
+ /** Get the target linear offset, in frame A, in meters.
7737
+ * @return {Vector2} */
7738
+ getLinearOffset() { return box2d.vec2From(this.box2dJoint.GetLinearOffset()); }
7739
+
7740
+ /** Set the target angular offset
7741
+ * @param {number} offset */
7742
+ setAngularOffset(offset) { this.box2dJoint.SetAngularOffset(offset); }
7743
+
7744
+ /** Get the target angular offset
7745
+ * @return {number} */
7746
+ getAngularOffset() { return this.box2dJoint.GetAngularOffset(); }
7747
+
7748
+ /** Set the maximum friction force
7749
+ * @param {number} force */
7750
+ setMaxForce(force) { this.box2dJoint.SetMaxForce(force); }
7751
+
7752
+ /** Get the maximum friction force
7753
+ * @return {number} */
7754
+ getMaxForce() { return this.box2dJoint.GetMaxForce(); }
7755
+
7756
+ /** Set the maximum torque
7757
+ * @param {number} torque */
7758
+ setMaxTorque(torque) { this.box2dJoint.SetMaxTorque(torque); }
7759
+
7760
+ /** Get the maximum torque
7761
+ * @return {number} */
7762
+ getMaxTorque() { return this.box2dJoint.GetMaxTorque(); }
7763
+
7764
+ /** Set the position correction factor in the range [0,1]
7765
+ * @param {number} factor */
7766
+ setCorrectionFactor(factor) { this.box2dJoint.SetCorrectionFactor(factor); }
7767
+
7768
+ /** Get the position correction factor in the range [0,1]
7769
+ * @return {number} */
7770
+ getCorrectionFactor() { return this.box2dJoint.GetCorrectionFactor(); }
7771
+ }
7772
+
7773
+ ///////////////////////////////////////////////////////////////////////////////
7774
+ /**
7775
+ * Box2D Global Object
7776
+ * - Wraps Box2d world and provides global functions
7777
+ */
7778
+ class Box2dPlugin
7779
+ {
7780
+ /** Create the global UI system object
7781
+ * @param {Object} instance */
7782
+ constructor(instance)
7783
+ {
7784
+ ASSERT(!box2d, 'Box2D already initialized');
7785
+ box2d = this;
7786
+ this.instance = instance;
7787
+ this.world = new box2d.instance.b2World();
7788
+
7789
+ /** @property {number} - Velocity iterations per update*/
7790
+ this.velocityIterations = 8;
7791
+ /** @property {number} - Position iterations per update*/
7792
+ this.positionIterations = 3;
7793
+ /** @property {number} - Static, zero mass, zero velocity, may be manually moved */
7794
+ this.bodyTypeStatic = instance.b2_staticBody;
7795
+ /** @property {number} - Kinematic, zero mass, non-zero velocity set by user, moved by solver */
7796
+ this.bodyTypeKinematic = instance.b2_kinematicBody;
7797
+ /** @property {number} - Dynamic, positive mass, non-zero velocity determined by forces, moved by solver */
7798
+ this.bodyTypeDynamic = instance.b2_dynamicBody;
7799
+
7800
+ // setup contact listener
7801
+ const listener = new box2d.instance.JSContactListener();
7802
+ listener.BeginContact = function(contactPtr)
7803
+ {
7804
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
7805
+ const fixtureA = contact.GetFixtureA();
7806
+ const fixtureB = contact.GetFixtureB();
7807
+ const objectA = fixtureA.GetBody().object;
7808
+ const objectB = fixtureB.GetBody().object;
7809
+ objectA.beginContact(objectB);
7810
+ objectB.beginContact(objectA);
7811
+ }
7812
+ listener.EndContact = function(contactPtr)
7813
+ {
7814
+ const contact = box2d.instance.wrapPointer(contactPtr, box2d.instance.b2Contact);
7815
+ const fixtureA = contact.GetFixtureA();
7816
+ const fixtureB = contact.GetFixtureB();
7817
+ const objectA = fixtureA.GetBody().object;
7818
+ const objectB = fixtureB.GetBody().object;
7819
+ objectA.endContact(objectB);
7820
+ objectB.endContact(objectA);
7821
+ };
7822
+ listener.PreSolve = function() {};
7823
+ listener.PostSolve = function() {};
7824
+ box2d.world.SetContactListener(listener);
7825
+ }
7826
+
7827
+ /** Step the physics world simulation
7828
+ * @param {number} [frames] */
7829
+ step(frames=1)
7830
+ {
7831
+ box2d.world.SetGravity(box2d.vec2dTo(gravity));
7832
+ for (let i=frames; i--;)
7833
+ box2d.world.Step(timeDelta, this.velocityIterations, this.positionIterations);
7834
+ }
7835
+
7836
+ ///////////////////////////////////////////////////////////////////////////////
7837
+ // raycasting and querying
7838
+
7839
+ /** raycast and return a list of all the results
7840
+ * @param {Vector2} start
7841
+ * @param {Vector2} end */
7842
+ raycastAll(start, end)
7843
+ {
7844
+ const raycastCallback = new box2d.instance.JSRayCastCallback();
7845
+ raycastCallback.ReportFixture = function(fixturePointer, point, normal, fraction)
7846
+ {
7847
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
7848
+ point = box2d.vec2FromPointer(point);
7849
+ normal = box2d.vec2FromPointer(normal);
7850
+ raycastResults.push(new Box2dRaycastResult(fixture, point, normal, fraction));
7851
+ return 1; // continue getting results
7852
+ };
7853
+
7854
+ const raycastResults = [];
7855
+ box2d.world.RayCast(raycastCallback, box2d.vec2dTo(start), box2d.vec2dTo(end));
7856
+ debugRaycast && debugLine(start, end, raycastResults.length ? '#f00' : '#00f', .02);
7857
+ return raycastResults;
7858
+ }
7859
+
7860
+ /** raycast and return the first result
7861
+ * @param {Vector2} start
7862
+ * @param {Vector2} end */
7863
+ raycast(start, end)
7864
+ {
7865
+ const raycastResults = box2d.raycastAll(start, end);
7866
+ if (!raycastResults.length)
7867
+ return undefined;
7868
+ return raycastResults.reduce((a,b)=>a.fraction < b.fraction ? a : b);
7869
+ }
7870
+
7871
+ /** box aabb cast and return all the objects
7872
+ * @param {Vector2} pos
7873
+ * @param {Vector2} size */
7874
+ boxCastAll(pos, size)
7875
+ {
7876
+ const queryCallback = new box2d.instance.JSQueryCallback();
7877
+ queryCallback.ReportFixture = function(fixturePointer)
7878
+ {
7879
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
7880
+ const o = fixture.GetBody().object;
7881
+ if (!queryObjects.includes(o))
7882
+ queryObjects.push(o); // add if not already in list
7883
+ return true; // continue getting results
7884
+ };
7885
+
7886
+ const aabb = new box2d.instance.b2AABB();
7887
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
7888
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
7889
+
7890
+ let queryObjects = [];
7891
+ box2d.world.QueryAABB(queryCallback, aabb);
7892
+ debugRaycast && debugRect(pos, size, queryObjects.length ? '#f00' : '#00f', .02);
7893
+ return queryObjects;
7894
+ }
7895
+
7896
+ /** box aabb cast and return the first object
7897
+ * @param {Vector2} pos
7898
+ * @param {Vector2} size */
7899
+ boxCast(pos, size)
7900
+ {
7901
+ const queryCallback = new box2d.instance.JSQueryCallback();
7902
+ queryCallback.ReportFixture = function(fixturePointer)
7903
+ {
7904
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
7905
+ queryObject = fixture.GetBody().object;
7906
+ return false; // stop getting results
7907
+ };
7908
+
7909
+ const aabb = new box2d.instance.b2AABB();
7910
+ aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));
7911
+ aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));
7912
+
7913
+ let queryObject;
7914
+ box2d.world.QueryAABB(queryCallback, aabb);
7915
+ debugRaycast && debugRect(pos, size, queryObject ? '#f00' : '#00f', .02);
7916
+ return queryObject;
7917
+ }
7918
+
7919
+ /** circle cast and return all the objects
7920
+ * @param {Vector2} pos
7921
+ * @param {number} diameter */
7922
+ circleCastAll(pos, diameter)
7923
+ {
7924
+ const radius2 = (diameter/2)**2;
7925
+ const results = box2d.boxCastAll(pos, vec2(diameter));
7926
+ return results.filter(o=>o.pos.distanceSquared(pos) < radius2);
7927
+ }
7928
+
7929
+ /** circle cast and return the first object
7930
+ * @param {Vector2} pos
7931
+ * @param {number} diameter */
7932
+ circleCast(pos, diameter)
7933
+ {
7934
+ const radius2 = (diameter/2)**2;
7935
+ let results = box2d.boxCastAll(pos, vec2(diameter));
7936
+
7937
+ let bestResult, bestDistance2;
7938
+ for (const result of results)
7939
+ {
7940
+ const distance2 = result.pos.distanceSquared(pos);
7941
+ if (distance2 < radius2 && (!bestResult || distance2 < bestDistance2))
7942
+ {
7943
+ bestResult = result;
7944
+ bestDistance2 = distance2;
7945
+ }
7946
+ }
7947
+ return bestResult;
7948
+ }
7949
+
7950
+ /** point cast and return the first object
7951
+ * @param {Vector2} pos
7952
+ * @param {boolean} dynamicOnly */
7953
+ pointCast(pos, dynamicOnly=true)
7954
+ {
7955
+ const queryCallback = new box2d.instance.JSQueryCallback();
7956
+ queryCallback.ReportFixture = function(fixturePointer)
7957
+ {
7958
+ const fixture = box2d.instance.wrapPointer(fixturePointer, box2d.instance.b2Fixture);
7959
+ if (dynamicOnly && fixture.GetBody().GetType() != box2d.instance.b2_dynamicBody)
7960
+ return true; // continue getting results
7961
+ if (!fixture.TestPoint(box2d.vec2dTo(pos)))
7962
+ return true; // continue getting results
7963
+ queryObject = fixture.GetBody().object;
7964
+ return false; // stop getting results
7965
+ };
7966
+
7967
+ const aabb = new box2d.instance.b2AABB();
7968
+ aabb.set_lowerBound(box2d.vec2dTo(pos));
7969
+ aabb.set_upperBound(box2d.vec2dTo(pos));
7970
+
7971
+ let queryObject;
7972
+ box2d.world.QueryAABB(queryCallback, aabb);
7973
+ debugRaycast && debugRect(pos, vec2(), queryObject ? '#f00' : '#00f', .02);
7974
+ return queryObject;
7975
+ }
7976
+
7977
+ ///////////////////////////////////////////////////////////////////////////////
7978
+ // drawing
7979
+
7980
+ /** draws a fixture
7981
+ * @param {Object} fixture
7982
+ * @param {Vector2} pos
7983
+ * @param {number} angle
7984
+ * @param {Color} [color]
7985
+ * @param {Color} [outlineColor]
7986
+ * @param {number} [lineWidth]
7987
+ * @param {CanvasRenderingContext2D} [context] */
7988
+ drawFixture(fixture, pos, angle, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
7989
+ {
7990
+ const shape = box2d.castObjectType(fixture.GetShape());
7991
+ switch (shape.GetType())
7992
+ {
7993
+ case box2d.instance.b2Shape.e_polygon:
7994
+ {
7995
+ let points = [];
7996
+ for (let i=shape.GetVertexCount(); i--;)
7997
+ points.push(box2d.vec2From(shape.GetVertex(i)));
7998
+ box2d.drawPoly(pos, angle, points, color, outlineColor, lineWidth, context);
7999
+ break;
8000
+ }
8001
+ case box2d.instance.b2Shape.e_circle:
8002
+ {
8003
+ const radius = shape.get_m_radius();
8004
+ box2d.drawCircle(pos, radius, color, outlineColor, lineWidth, context);
8005
+ break;
8006
+ }
8007
+ case box2d.instance.b2Shape.e_edge:
8008
+ {
8009
+ const v1 = box2d.vec2From(shape.get_m_vertex1());
8010
+ const v2 = box2d.vec2From(shape.get_m_vertex2());
8011
+ box2d.drawLine(pos, angle, v1, v2, color, lineWidth, context);
8012
+ break;
8013
+ }
8014
+ }
8015
+ }
8016
+
8017
+ /** draws a circle
8018
+ * @param {Vector2} pos
8019
+ * @param {number} radius
8020
+ * @param {Color} [color]
8021
+ * @param {Color} [outlineColor]
8022
+ * @param {number} [lineWidth]
8023
+ * @param {CanvasRenderingContext2D} [context] */
8024
+ drawCircle(pos, radius, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8025
+ {
8026
+ drawCanvas2D(pos, vec2(1), 0, 0, context=>
8027
+ {
8028
+ context.beginPath();
8029
+ context.arc(0, 0, radius, 0, 9);
8030
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8031
+ }, 0, context);
8032
+ }
8033
+
8034
+ /** draws a polygon
8035
+ * @param {Vector2} pos
8036
+ * @param {number} angle
8037
+ * @param {Array<Vector2>} points
8038
+ * @param {Color} [color]
8039
+ * @param {Color} [outlineColor]
8040
+ * @param {number} [lineWidth]
8041
+ * @param {CanvasRenderingContext2D} [context] */
8042
+ drawPoly(pos, angle, points, color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8043
+ {
8044
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8045
+ {
8046
+ context.beginPath();
8047
+ points.forEach(p=>context.lineTo(p.x, p.y));
8048
+ context.closePath();
8049
+ box2d.drawFillStroke(color, outlineColor, lineWidth, context);
8050
+ }, 0, context);
8051
+ }
8052
+
8053
+ /** draws a line
8054
+ * @param {Vector2} pos
8055
+ * @param {number} angle
8056
+ * @param {Vector2} posA
8057
+ * @param {Vector2} posB
8058
+ * @param {Color} [color]
8059
+ * @param {number} [lineWidth]
8060
+ * @param {CanvasRenderingContext2D} [context] */
8061
+ drawLine(pos, angle, posA, posB, color=WHITE, lineWidth=.1, context=mainContext)
8062
+ {
8063
+ drawCanvas2D(pos, vec2(1), angle, 0, context=>
8064
+ {
8065
+ context.beginPath();
8066
+ context.lineTo(posA.x, posA.y);
8067
+ context.lineTo(posB.x, posB.y);
8068
+ box2d.drawFillStroke(0, color, lineWidth, context);
8069
+ }, 0, context);
8070
+ }
8071
+
8072
+ /** performs a fill or stroke as a helper to the other draw functions
8073
+ * @param {Color} [color]
8074
+ * @param {Color} [outlineColor]
8075
+ * @param {number} [lineWidth]
8076
+ * @param {CanvasRenderingContext2D} [context] */
8077
+ drawFillStroke(color=WHITE, outlineColor=BLACK, lineWidth=.1, context=mainContext)
8078
+ {
8079
+ if (color)
8080
+ {
8081
+ context.fillStyle = color.toString();
8082
+ context.fill();
8083
+ }
8084
+ if (outlineColor && lineWidth)
8085
+ {
8086
+ context.lineWidth = lineWidth;
8087
+ context.lineJoin = context.lineCap = 'round';
8088
+ context.strokeStyle = outlineColor.toString();
8089
+ context.stroke();
8090
+ }
8091
+ }
8092
+
8093
+ ///////////////////////////////////////////////////////////////////////////////
8094
+ // helper functions
8095
+
8096
+ /** converts a box2d vec2 to a Vector2
8097
+ * @param {Object} v */
8098
+ vec2From(v)
8099
+ {
8100
+ ASSERT(v instanceof box2d.instance.b2Vec2);
8101
+ return new Vector2(v.get_x(), v.get_y());
8102
+ }
8103
+
8104
+ /** converts a box2d vec2 pointer to a Vector2
8105
+ * @param {Object} v */
8106
+ vec2FromPointer(v)
8107
+ {
8108
+ return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
8109
+ }
8110
+
8111
+ /** converts a Vector2 to a box2 vec2
8112
+ * @param {Vector2} v */
8113
+ vec2dTo(v)
8114
+ {
8115
+ ASSERT(v instanceof Vector2);
8116
+ return new box2d.instance.b2Vec2(v.x, v.y);
8117
+ }
8118
+
8119
+ /** checks if a box2d object is null
8120
+ * @param {Object} o */
8121
+ isNull(o) { return !box2d.instance.getPointer(o); }
8122
+
8123
+ /** casts a box2d object to its correct type
8124
+ * @param {Object} o */
8125
+ castObjectType(o)
8126
+ {
8127
+ switch (o.GetType())
8128
+ {
8129
+ case box2d.instance.b2Shape.e_circle:
8130
+ return box2d.instance.castObject(o, box2d.instance.b2CircleShape);
8131
+ case box2d.instance.b2Shape.e_edge:
8132
+ return box2d.instance.castObject(o, box2d.instance.b2EdgeShape);
8133
+ case box2d.instance.b2Shape.e_polygon:
8134
+ return box2d.instance.castObject(o, box2d.instance.b2PolygonShape);
8135
+ case box2d.instance.b2Shape.e_chain:
8136
+ return box2d.instance.castObject(o, box2d.instance.b2ChainShape);
8137
+ case box2d.instance.e_revoluteJoint:
8138
+ return box2d.instance.castObject(o, box2d.instance.b2RevoluteJoint);
8139
+ case box2d.instance.e_prismaticJoint:
8140
+ return box2d.instance.castObject(o, box2d.instance.b2PrismaticJoint);
8141
+ case box2d.instance.e_distanceJoint:
8142
+ return box2d.instance.castObject(o, box2d.instance.b2DistanceJoint);
8143
+ case box2d.instance.e_pulleyJoint:
8144
+ return box2d.instance.castObject(o, box2d.instance.b2PulleyJoint);
8145
+ case box2d.instance.e_mouseJoint:
8146
+ return box2d.instance.castObject(o, box2d.instance.b2MouseJoint);
8147
+ case box2d.instance.e_gearJoint:
8148
+ return box2d.instance.castObject(o, box2d.instance.b2GearJoint);
8149
+ case box2d.instance.e_wheelJoint:
8150
+ return box2d.instance.castObject(o, box2d.instance.b2WheelJoint);
8151
+ case box2d.instance.e_weldJoint:
8152
+ return box2d.instance.castObject(o, box2d.instance.b2WeldJoint);
8153
+ case box2d.instance.e_frictionJoint:
8154
+ return box2d.instance.castObject(o, box2d.instance.b2FrictionJoint);
8155
+ case box2d.instance.e_ropeJoint:
8156
+ return box2d.instance.castObject(o, box2d.instance.b2RopeJoint);
8157
+ case box2d.instance.e_motorJoint:
8158
+ return box2d.instance.castObject(o, box2d.instance.b2MotorJoint);
8159
+ }
8160
+
8161
+ ASSERT(false, 'Unknown box2d object type');
8162
+ }
8163
+ }
8164
+
8165
+ ///////////////////////////////////////////////////////////////////////////////
8166
+ /** Box2d Init - Startup LittleJS engine with your callback functions
8167
+ * @param {Function|function():Promise} gameInit - Called once after the engine starts up
8168
+ * @param {Function} gameUpdate - Called every frame before objects are updated
8169
+ * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused
8170
+ * @param {Function} gameRender - Called before objects are rendered, for drawing the background
8171
+ * @param {Function} gameRenderPost - Called after objects are rendered, useful for drawing UI
8172
+ * @param {Array<string>} [imageSources=[]] - List of images to load
8173
+ * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default */
8174
+ function box2dEngineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources, rootElement)
8175
+ {
8176
+ Box2D().then(box2dInstance=>
8177
+ {
8178
+ // create box2d object
8179
+ new Box2dPlugin(box2dInstance);
8180
+ setupDebugDraw();
8181
+
8182
+ // start littlejs
8183
+ engineAddPlugin(box2dUpdate, box2dRender);
8184
+ engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources, rootElement);
8185
+ });
8186
+
8187
+ // hook up box2d plugin to update and render
8188
+ function box2dUpdate()
8189
+ {
8190
+ if (!paused)
8191
+ box2d.step();
8192
+ }
8193
+ function box2dRender()
8194
+ {
8195
+ if (box2dDebug || debugPhysics && debugOverlay)
8196
+ box2d.world.DrawDebugData();
8197
+ }
8198
+
8199
+ // box2d debug drawing
8200
+ function setupDebugDraw()
8201
+ {
8202
+ // setup debug draw
8203
+ const debugDraw = new box2d.instance.JSDraw();
8204
+ const box2dColor = (c)=> new Color(c.get_r(), c.get_g(), c.get_b());
8205
+ const box2dColorPointer = (c)=>
8206
+ box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
8207
+ const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
8208
+ const getPointsList = (vertices, vertexCount) =>
8209
+ {
8210
+ const points = [];
8211
+ for (let i=vertexCount; i--;)
8212
+ points.push(box2d.vec2FromPointer(vertices+i*8));
8213
+ return points;
8214
+ }
8215
+ debugDraw.DrawSegment = function(point1, point2, color)
8216
+ {
8217
+ color = getDebugColor(color);
8218
+ point1 = box2d.vec2FromPointer(point1);
8219
+ point2 = box2d.vec2FromPointer(point2);
8220
+ box2d.drawLine(vec2(), 0, point1, point2, color, undefined, overlayContext);
8221
+ };
8222
+ debugDraw.DrawPolygon = function(vertices, vertexCount, color)
8223
+ {
8224
+ color = getDebugColor(color);
8225
+ const points = getPointsList(vertices, vertexCount);
8226
+ box2d.drawPoly(vec2(), 0, points, undefined, color, undefined, overlayContext);
8227
+ };
8228
+ debugDraw.DrawSolidPolygon = function(vertices, vertexCount, color)
8229
+ {
8230
+ color = getDebugColor(color);
8231
+ const points = getPointsList(vertices, vertexCount);
8232
+ box2d.drawPoly(vec2(), 0, points, color, color, undefined, overlayContext);
8233
+ };
8234
+ debugDraw.DrawCircle = function(center, radius, color)
8235
+ {
8236
+ color = getDebugColor(color);
8237
+ center = box2d.vec2FromPointer(center);
8238
+ box2d.drawCircle(center, radius, undefined, color, undefined, overlayContext);
8239
+ };
8240
+ debugDraw.DrawSolidCircle = function(center, radius, axis, color)
8241
+ {
8242
+ color = getDebugColor(color);
8243
+ center = box2d.vec2FromPointer(center);
8244
+ axis = box2d.vec2FromPointer(axis).scale(radius);
8245
+ box2d.drawCircle(center, radius, color, color, undefined, overlayContext);
8246
+ box2d.drawLine(center, 0, vec2(), axis, color, undefined, overlayContext);
8247
+ };
8248
+ debugDraw.DrawTransform = function(transform)
8249
+ {
8250
+ transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
8251
+ const pos = vec2(transform.get_p());
8252
+ const angle = -transform.get_q().GetAngle();
8253
+ const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
8254
+ const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
8255
+ box2d.drawLine(pos, angle, vec2(), p1, c1, undefined, overlayContext);
8256
+ box2d.drawLine(pos, angle, vec2(), p2, c2, undefined, overlayContext);
8257
+ }
8258
+
8259
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);
8260
+ debugDraw.AppendFlags(box2d.instance.b2Draw.e_jointBit);
8261
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_aabbBit);
8262
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_pairBit);
8263
+ //debugDraw.AppendFlags(box2d.instance.b2Draw.e_centerOfMassBit);
8264
+ box2d.world.SetDebugDraw(debugDraw);
8265
+ }
8266
+ }
8267
+