littlejsengine 1.14.11 → 1.14.19

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 (80) hide show
  1. package/dist/littlejs.d.ts +303 -137
  2. package/dist/littlejs.esm.js +936 -463
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +934 -462
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +859 -417
  7. package/examples/box2d/game.js +3 -2
  8. package/examples/box2d/gameObjects.js +2 -2
  9. package/examples/box2d/tiles.png +0 -0
  10. package/examples/breakout/game.js +5 -5
  11. package/examples/breakout/gameObjects.js +2 -2
  12. package/examples/breakoutTutorial/README.md +32 -32
  13. package/examples/breakoutTutorial/game.js +1 -1
  14. package/examples/electron/game.js +4 -4
  15. package/examples/electron/index.html +2 -2
  16. package/examples/electron/package.json +1 -8
  17. package/examples/index.html +67 -56
  18. package/examples/module/game.js +4 -4
  19. package/examples/platformer/gameEffects.js +5 -5
  20. package/examples/platformer/gameLevel.js +1 -1
  21. package/examples/platformer/gameObjects.js +4 -4
  22. package/examples/puzzle/game.js +1 -1
  23. package/examples/shorts/animation.js +1 -1
  24. package/examples/shorts/base.html +1 -1
  25. package/examples/shorts/blending.js +8 -14
  26. package/examples/shorts/box2d.js +7 -3
  27. package/examples/shorts/box2dCar.js +2 -1
  28. package/examples/shorts/empty.js +30 -0
  29. package/examples/shorts/helloWorld.js +1 -1
  30. package/examples/shorts/hillGlideGame.js +10 -4
  31. package/examples/shorts/landerGame.js +11 -8
  32. package/examples/shorts/medals.js +4 -4
  33. package/examples/shorts/music.js +29 -56
  34. package/examples/shorts/musicPlayer.js +135 -0
  35. package/examples/shorts/nineSlice.js +34 -15
  36. package/examples/shorts/parallax.js +5 -4
  37. package/examples/shorts/particles.js +15 -15
  38. package/examples/shorts/piano.js +15 -21
  39. package/examples/shorts/pongGame.js +6 -4
  40. package/examples/shorts/raycasting.js +9 -4
  41. package/examples/shorts/sequencer.js +122 -0
  42. package/examples/shorts/shader.js +29 -0
  43. package/examples/shorts/shapes.js +7 -4
  44. package/examples/shorts/slidingPuzzle.js +4 -2
  45. package/examples/shorts/sound.js +18 -9
  46. package/examples/shorts/spaceGame.js +12 -8
  47. package/examples/shorts/spriteAtlas.js +7 -7
  48. package/examples/shorts/starfield.js +1 -1
  49. package/examples/shorts/systemFont.js +2 -2
  50. package/examples/shorts/texture.js +2 -2
  51. package/examples/shorts/tileLayer.js +10 -10
  52. package/examples/shorts/tiltedView.js +24 -5
  53. package/examples/shorts/timers.js +10 -0
  54. package/examples/shorts/topDown.js +4 -1
  55. package/examples/shorts/uiSystem.js +8 -5
  56. package/examples/starter/game.js +4 -4
  57. package/examples/starter/index.html +2 -2
  58. package/examples/style.css +1 -0
  59. package/examples/typescript/game.js +4 -4
  60. package/examples/typescript/game.ts +4 -4
  61. package/examples/uiSystem/game.js +4 -3
  62. package/package.json +4 -2
  63. package/plugins/box2d.js +17 -2
  64. package/plugins/newgrounds.js +7 -5
  65. package/plugins/postProcess.js +76 -53
  66. package/plugins/uiSystem.js +192 -53
  67. package/plugins/zzfxm.js +5 -1
  68. package/reference.md +1 -1
  69. package/src/engine.js +60 -23
  70. package/src/engineAudio.js +44 -17
  71. package/src/engineDebug.js +75 -45
  72. package/src/engineDraw.js +94 -59
  73. package/src/engineExport.js +2 -1
  74. package/src/engineMedals.js +10 -2
  75. package/src/engineObject.js +14 -10
  76. package/src/engineParticles.js +60 -47
  77. package/src/engineSettings.js +7 -7
  78. package/src/engineTileLayer.js +73 -56
  79. package/src/engineUtilities.js +67 -20
  80. package/src/engineWebGL.js +134 -63
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.11';
36
+ const engineVersion = '1.14.19';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -94,29 +94,60 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
94
94
  ///////////////////////////////////////////////////////////////////////////////
95
95
  // plugin hooks
96
96
 
97
- const pluginUpdateList = [], pluginRenderList = [];
97
+ const pluginList = [];
98
+ class EnginePlugin
99
+ {
100
+ constructor(update, render, glContextLost, glContextRestored)
101
+ {
102
+ this.update = update;
103
+ this.render = render;
104
+ this.glContextLost = glContextLost;
105
+ this.glContextRestored = glContextRestored;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * @callback PluginCallback - Update or render function for a plugin
111
+ * @memberof Engine
112
+ */
98
113
 
99
114
  /** Add a new update function for a plugin
100
- * @param {Function} [updateFunction]
101
- * @param {Function} [renderFunction]
115
+ * @param {PluginCallback} [update]
116
+ * @param {PluginCallback} [render]
117
+ * @param {PluginCallback} [glContextLost]
118
+ * @param {PluginCallback} [glContextRestored]
102
119
  * @memberof Engine */
103
- function engineAddPlugin(updateFunction, renderFunction)
120
+ function engineAddPlugin(update, render, glContextLost, glContextRestored)
104
121
  {
105
- ASSERT(!pluginUpdateList.includes(updateFunction));
106
- ASSERT(!pluginRenderList.includes(renderFunction));
107
- updateFunction && pluginUpdateList.push(updateFunction);
108
- renderFunction && pluginRenderList.push(renderFunction);
122
+ // make sure plugin functions are unique
123
+ ASSERT(!pluginList.find(p=>
124
+ p.update === update && p.render === render &&
125
+ p.glContextLost === glContextLost &&
126
+ p.glContextRestored === glContextRestored));
127
+
128
+ const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
129
+ pluginList.push(plugin);
109
130
  }
110
131
 
111
132
  ///////////////////////////////////////////////////////////////////////////////
112
133
  // Main Engine Functions
113
134
 
135
+ /**
136
+ * @callback GameInitCallback - Called after the engine starts, can be async
137
+ * @returns {void|Promise<void>}
138
+ * @memberof Engine
139
+ */
140
+ /**
141
+ * @callback GameCallback - Update or render function for the game
142
+ * @memberof Engine
143
+ */
144
+
114
145
  /** Startup LittleJS engine with your callback functions
115
- * @param {Function|function():Promise} gameInit - Called once after the engine starts up, can be async for loading
116
- * @param {Function} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
117
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
118
- * @param {Function} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
119
- * @param {Function} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
146
+ * @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
147
+ * @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
148
+ * @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
149
+ * @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
150
+ * @param {GameCallback} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
120
151
  * @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
121
152
  * @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
122
153
  * @example
@@ -180,7 +211,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
180
211
  wasUpdated = true;
181
212
  updateCanvas();
182
213
  inputUpdate();
183
- pluginUpdateList.forEach(f=>f());
214
+ pluginList.forEach(plugin=>plugin.update?.());
184
215
 
185
216
  // update object transforms even when paused
186
217
  for (const o of engineObjects)
@@ -213,7 +244,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
213
244
  updateCanvas();
214
245
  inputUpdate();
215
246
  gameUpdate();
216
- pluginUpdateList.forEach(f=>f());
247
+ pluginList.forEach(plugin=>plugin.update?.());
217
248
  engineObjectsUpdate();
218
249
 
219
250
  // do post update
@@ -247,7 +278,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
247
278
  for (const o of engineObjects)
248
279
  o.destroyed || o.render();
249
280
  gameRenderPost();
250
- pluginRenderList.forEach(f=>f());
281
+ pluginList.forEach(plugin=>plugin.render?.());
251
282
  touchGamepadRender();
252
283
  debugRender();
253
284
  glFlush();
@@ -368,11 +399,12 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
368
399
  const promises = imageSources.map((src, textureIndex)=>
369
400
  new Promise(resolve =>
370
401
  {
402
+ ASSERT(isString(src), 'imageSources must be an array of strings');
403
+
371
404
  const image = new Image;
372
405
  image.onerror = image.onload = ()=>
373
406
  {
374
407
  const textureInfo = new TextureInfo(image);
375
- textureInfo.createWebGLTexture();
376
408
  textureInfos[textureIndex] = textureInfo;
377
409
  resolve();
378
410
  }
@@ -388,7 +420,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
388
420
  {
389
421
  const textureInfo = new TextureInfo(new Image);
390
422
  textureInfos[0] = textureInfo;
391
- textureInfo.createWebGLTexture();
392
423
  resolve();
393
424
  }));
394
425
  }
@@ -399,7 +430,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
399
430
  promises.push(new Promise(resolve =>
400
431
  {
401
432
  let t = 0;
402
- LOG(`${engineName} Engine v${engineVersion}`);
433
+ console.log(`${engineName} Engine v${engineVersion}`);
403
434
  updateSplash();
404
435
  function updateSplash()
405
436
  {
@@ -483,10 +514,16 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
483
514
  return collectedObjects;
484
515
  }
485
516
 
517
+ /**
518
+ * @callback ObjectCallbackFunction - Function that processes an object
519
+ * @param {EngineObject} uiObjects
520
+ * @memberof Engine
521
+ */
522
+
486
523
  /** Triggers a callback for each object within a given area
487
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
488
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
489
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
524
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
525
+ * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
526
+ * @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test
490
527
  * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
491
528
  * @memberof Engine */
492
529
  function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
@@ -950,6 +987,8 @@ function formatTime(t)
950
987
  async function fetchJSON(url)
951
988
  {
952
989
  const response = await fetch(url);
990
+ if (!response.ok)
991
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
953
992
  return response.json();
954
993
  }
955
994
 
@@ -960,6 +999,13 @@ async function fetchJSON(url)
960
999
  * @memberof Utilities */
961
1000
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
962
1001
 
1002
+ /**
1003
+ * Check if object is a valid string or can be converted to one
1004
+ * @param {any} s
1005
+ * @return {boolean}
1006
+ * @memberof Utilities */
1007
+ function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1008
+
963
1009
  ///////////////////////////////////////////////////////////////////////////////
964
1010
 
965
1011
  /** Random global functions
@@ -1022,6 +1068,7 @@ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1022
1068
  /**
1023
1069
  * Seeded random number generator
1024
1070
  * - Can be used to create a deterministic random number sequence
1071
+ * @memberof Engine
1025
1072
  * @example
1026
1073
  * let r = new RandomGenerator(123); // random number generator with seed 123
1027
1074
  * let a = r.float(); // random value between 0 and 1
@@ -1118,6 +1165,7 @@ function ASSERT_VECTOR2_NORMAL(v)
1118
1165
  /**
1119
1166
  * 2D Vector object with vector math library
1120
1167
  * - Functions do not change this so they can be chained together
1168
+ * @memberof Engine
1121
1169
  * @example
1122
1170
  * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
1123
1171
  * let b = new Vector2; // vector with coordinates (0, 0)
@@ -1258,9 +1306,10 @@ class Vector2
1258
1306
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1259
1307
  }
1260
1308
 
1261
- /** Set the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
1309
+ /** Sets this this vector to point in the specified integer direction (0-3), corresponding to multiples of 90 degree rotation
1262
1310
  * @param {number} [direction]
1263
- * @param {number} [length] */
1311
+ * @param {number} [length]
1312
+ * @return {Vector2} */
1264
1313
  setDirection(direction, length=1)
1265
1314
  {
1266
1315
  ASSERT_NUMBER_VALID(direction);
@@ -1268,8 +1317,10 @@ class Vector2
1268
1317
  direction = mod(direction, 4);
1269
1318
  ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1270
1319
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1271
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1272
- direction%2 ? 0 : direction ? -length : length);
1320
+
1321
+ this.x = direction%2 ? direction-1 ? -length : length : 0;
1322
+ this.y = direction%2 ? 0 : direction ? -length : length;
1323
+ return this;
1273
1324
  }
1274
1325
 
1275
1326
  /** Returns the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
@@ -1365,6 +1416,7 @@ function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
1365
1416
 
1366
1417
  /**
1367
1418
  * Color object (red, green, blue, alpha) with some helpful functions
1419
+ * @memberof Engine
1368
1420
  * @example
1369
1421
  * let a = new Color; // white
1370
1422
  * let b = new Color(1, 0, 0); // red
@@ -1542,7 +1594,8 @@ class Color
1542
1594
  * @return {Color} */
1543
1595
  setHex(hex)
1544
1596
  {
1545
- ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
1597
+ ASSERT(isString(hex), 'Color hex code must be a string');
1598
+ ASSERT(hex[0] === '#', 'Color hex code must start with #');
1546
1599
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1547
1600
 
1548
1601
  if (hex.length < 6)
@@ -1584,77 +1637,78 @@ class Color
1584
1637
  }
1585
1638
 
1586
1639
  ///////////////////////////////////////////////////////////////////////////////
1587
- // default colors
1640
+ // Default Colors
1588
1641
 
1589
1642
  /** Color - White #ffffff
1590
1643
  * @type {Color}
1591
1644
  * @memberof Utilities */
1592
- const WHITE = rgb();
1645
+ const WHITE = protectEngineConstant(rgb());
1593
1646
 
1594
- /** Color - Clear White #ffffff with 0 alpha
1647
+ /** Color - Clear White #757474ff with 0 alpha
1595
1648
  * @type {Color}
1596
1649
  * @memberof Utilities */
1597
- const CLEAR_WHITE = rgb(1,1,1,0);
1650
+ const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
1598
1651
 
1599
1652
  /** Color - Black #000000
1600
1653
  * @type {Color}
1601
1654
  * @memberof Utilities */
1602
- const BLACK = rgb(0,0,0);
1655
+ const BLACK = protectEngineConstant(rgb(0,0,0));
1603
1656
 
1604
1657
  /** Color - Clear Black #000000 with 0 alpha
1605
1658
  * @type {Color}
1606
1659
  * @memberof Utilities */
1607
- const CLEAR_BLACK = rgb(0,0,0,0);
1660
+ const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
1608
1661
 
1609
1662
  /** Color - Gray #808080
1610
1663
  * @type {Color}
1611
1664
  * @memberof Utilities */
1612
- const GRAY = rgb(.5,.5,.5);
1665
+ const GRAY = protectEngineConstant(rgb(.5,.5,.5));
1613
1666
 
1614
1667
  /** Color - Red #ff0000
1615
1668
  * @type {Color}
1616
1669
  * @memberof Utilities */
1617
- const RED = rgb(1,0,0);
1670
+ const RED = protectEngineConstant(rgb(1,0,0));
1618
1671
 
1619
1672
  /** Color - Orange #ff8000
1620
1673
  * @type {Color}
1621
1674
  * @memberof Utilities */
1622
- const ORANGE = rgb(1,.5,0);
1675
+ const ORANGE = protectEngineConstant(rgb(1,.5,0));
1623
1676
 
1624
1677
  /** Color - Yellow #ffff00
1625
1678
  * @type {Color}
1626
1679
  * @memberof Utilities */
1627
- const YELLOW = rgb(1,1,0);
1680
+ const YELLOW = protectEngineConstant(rgb(1,1,0));
1628
1681
 
1629
1682
  /** Color - Green #00ff00
1630
1683
  * @type {Color}
1631
1684
  * @memberof Utilities */
1632
- const GREEN = rgb(0,1,0);
1685
+ const GREEN = protectEngineConstant(rgb(0,1,0));
1633
1686
 
1634
1687
  /** Color - Cyan #00ffff
1635
1688
  * @type {Color}
1636
1689
  * @memberof Utilities */
1637
- const CYAN = rgb(0,1,1);
1690
+ const CYAN = protectEngineConstant(rgb(0,1,1));
1638
1691
 
1639
1692
  /** Color - Blue #0000ff
1640
1693
  * @type {Color}
1641
1694
  * @memberof Utilities */
1642
- const BLUE = rgb(0,0,1);
1695
+ const BLUE = protectEngineConstant(rgb(0,0,1));
1643
1696
 
1644
1697
  /** Color - Purple #8000ff
1645
1698
  * @type {Color}
1646
1699
  * @memberof Utilities */
1647
- const PURPLE = rgb(.5,0,1);
1700
+ const PURPLE = protectEngineConstant(rgb(.5,0,1));
1648
1701
 
1649
1702
  /** Color - Magenta #ff00ff
1650
1703
  * @type {Color}
1651
1704
  * @memberof Utilities */
1652
- const MAGENTA = rgb(1,0,1);
1705
+ const MAGENTA = protectEngineConstant(rgb(1,0,1));
1653
1706
 
1654
1707
  ///////////////////////////////////////////////////////////////////////////////
1655
1708
 
1656
1709
  /**
1657
1710
  * Timer object tracks how long has passed since it was set
1711
+ * @memberof Engine
1658
1712
  * @example
1659
1713
  * let a = new Timer; // creates a timer that is not set
1660
1714
  * a.set(3); // sets the timer to 3 seconds
@@ -1716,6 +1770,36 @@ class Timer
1716
1770
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1717
1771
  * @return {number} */
1718
1772
  valueOf() { return this.get(); }
1773
+ }
1774
+
1775
+ ///////////////////////////////////////////////////////////////////////////////
1776
+ // Helper functions used by the engine
1777
+
1778
+ // make color constants immutable with debug assertions
1779
+ function protectEngineConstant(obj)
1780
+ {
1781
+ if (debug)
1782
+ {
1783
+ // get properties and store original values
1784
+ const props = Object.keys(obj), values = {};
1785
+ props.forEach(prop => values[prop] = obj[prop]);
1786
+
1787
+ // replace with getters/setters that assert
1788
+ props.forEach(prop =>
1789
+ {
1790
+ Object.defineProperty(obj, prop, {
1791
+ get: () => values[prop],
1792
+ set: (value) =>
1793
+ {
1794
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1795
+ },
1796
+ enumerable: true
1797
+ });
1798
+ });
1799
+ }
1800
+
1801
+ // freeze the object to prevent adding new properties
1802
+ return Object.freeze(obj);
1719
1803
  }
1720
1804
  /**
1721
1805
  * LittleJS Engine Settings
@@ -2023,7 +2107,7 @@ let medalsPreventUnlock = false;
2023
2107
  /** Set position of camera in world space
2024
2108
  * @param {Vector2} pos
2025
2109
  * @memberof Settings */
2026
- function setCameraPos(pos) { cameraPos = pos; }
2110
+ function setCameraPos(pos) { cameraPos = pos.copy(); }
2027
2111
 
2028
2112
  /** Set angle of camera in world space
2029
2113
  * @param {number} angle
@@ -2046,17 +2130,17 @@ function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2046
2130
  /** Set color to clear the canvas to before render
2047
2131
  * @param {Color} color
2048
2132
  * @memberof Settings */
2049
- function setCanvasClearColor(color) { canvasClearColor = color; }
2133
+ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
2050
2134
 
2051
2135
  /** Set max size of the canvas
2052
2136
  * @param {Vector2} size
2053
2137
  * @memberof Settings */
2054
- function setCanvasMaxSize(size) { canvasMaxSize = size; }
2138
+ function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
2055
2139
 
2056
2140
  /** Set fixed size of the canvas
2057
2141
  * @param {Vector2} size
2058
2142
  * @memberof Settings */
2059
- function setCanvasFixedSize(size) { canvasFixedSize = size; }
2143
+ function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
2060
2144
 
2061
2145
  /** Use nearest scaling algorithm for canvas for more pixelated look
2062
2146
  * - If enabled sets css image-rendering:pixelated
@@ -2121,7 +2205,7 @@ function setGLCircleSides(sides) { glCircleSides = sides; }
2121
2205
  /** Set default size of tiles in pixels
2122
2206
  * @param {Vector2} size
2123
2207
  * @memberof Settings */
2124
- function setTileSizeDefault(size) { tileSizeDefault = size; }
2208
+ function setTileSizeDefault(size) { tileSizeDefault = size.copy(); }
2125
2209
 
2126
2210
  /** Set to prevent tile bleeding from neighbors in pixels
2127
2211
  * @param {number} scale
@@ -2166,7 +2250,7 @@ function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
2166
2250
  /** Set how much gravity to apply to objects
2167
2251
  * @param {Vector2} newGravity
2168
2252
  * @memberof Settings */
2169
- function setGravity(newGravity) { gravity = newGravity; }
2253
+ function setGravity(newGravity) { gravity = newGravity.copy(); }
2170
2254
 
2171
2255
  /** Set to scales emit rate of particles
2172
2256
  * @param {number} scale
@@ -2256,7 +2340,7 @@ function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
2256
2340
  /** Set size of medal display
2257
2341
  * @param {Vector2} size
2258
2342
  * @memberof Settings */
2259
- function setMedalDisplaySize(size) { medalDisplaySize = size; }
2343
+ function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
2260
2344
 
2261
2345
  /** Set to stop medals from being unlockable
2262
2346
  * @param {boolean} preventUnlock
@@ -2296,6 +2380,7 @@ function setDebugKey(key) { debugKey = key; }
2296
2380
  * - Collision for objects can be set to be solid to block other objects
2297
2381
  * - Objects may get pushed into overlapping other solid objects, if so they will push away
2298
2382
  * - Solid objects are more performance intensive and should be used sparingly
2383
+ * @memberof Engine
2299
2384
  * @example
2300
2385
  * // create an engine object, normally you would first extend the class with your own
2301
2386
  * const pos = vec2(2,3);
@@ -2304,18 +2389,18 @@ function setDebugKey(key) { debugKey = key; }
2304
2389
  class EngineObject
2305
2390
  {
2306
2391
  /** Create an engine object and adds it to the list of objects
2307
- * @param {Vector2} [pos=(0,0)] - World space position of the object
2308
- * @param {Vector2} [size=(1,1)] - World space size of the object
2309
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2310
- * @param {number} [angle] - Angle the object is rotated by
2311
- * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
2312
- * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2392
+ * @param {Vector2} [pos=(0,0)] - World space position of the object
2393
+ * @param {Vector2} [size=(1,1)] - World space size of the object
2394
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2395
+ * @param {number} [angle] - Angle the object is rotated by
2396
+ * @param {Color} [color=WHITE] - Color to apply to tile when rendered
2397
+ * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2313
2398
  */
2314
- constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
2399
+ constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
2315
2400
  {
2316
2401
  // check passed in params
2317
- ASSERT(isVector2(pos), 'object pos should be a vec2');
2318
- ASSERT(isVector2(size), 'object size should be a vec2');
2402
+ ASSERT(isVector2(pos), 'object pos must be a vec2');
2403
+ ASSERT(isVector2(size), 'object size must be a vec2');
2319
2404
  ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
2320
2405
  ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2321
2406
  ASSERT(isColor(color), 'object color should be a valid rgba color');
@@ -2600,7 +2685,7 @@ class EngineObject
2600
2685
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
2601
2686
  }
2602
2687
 
2603
- /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
2688
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
2604
2689
  destroy()
2605
2690
  {
2606
2691
  if (this.destroyed)
@@ -2674,6 +2759,8 @@ class EngineObject
2674
2759
  addChild(child, localPos=vec2(), localAngle=0)
2675
2760
  {
2676
2761
  ASSERT(!child.parent && !this.children.includes(child));
2762
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
2763
+ ASSERT(child !== this, 'cannot add self as child');
2677
2764
  this.children.push(child);
2678
2765
  child.parent = this;
2679
2766
  child.localPos = localPos.copy();
@@ -2685,6 +2772,7 @@ class EngineObject
2685
2772
  removeChild(child)
2686
2773
  {
2687
2774
  ASSERT(child.parent === this && this.children.includes(child));
2775
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
2688
2776
  this.children.splice(this.children.indexOf(child), 1);
2689
2777
  child.parent = 0;
2690
2778
  }
@@ -2823,7 +2911,7 @@ let drawCount;
2823
2911
  * Create a tile info object using a grid based system
2824
2912
  * - This can take vecs or floats for easier use and conversion
2825
2913
  * - If an index is passed in, the tile size and index will determine the position
2826
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
2914
+ * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
2827
2915
  * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
2828
2916
  * @param {number} [textureIndex] - Texture index to use
2829
2917
  * @param {number} [padding] - How many pixels padding around tiles
@@ -2868,6 +2956,7 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
2868
2956
 
2869
2957
  /**
2870
2958
  * Tile Info - Stores info about how to draw a tile
2959
+ * @memberof Draw
2871
2960
  */
2872
2961
  class TileInfo
2873
2962
  {
@@ -2912,48 +3001,54 @@ class TileInfo
2912
3001
  }
2913
3002
 
2914
3003
  /**
2915
- * Set this tile to use a full image
2916
- * @param {HTMLImageElement|OffscreenCanvas} image
2917
- * @param {WebGLTexture} [glTexture] - WebGL texture
3004
+ * Set this tile to use a full image in a texture info
3005
+ * @param {TextureInfo} textureInfo
2918
3006
  * @return {TileInfo}
2919
3007
  */
2920
- setFullImage(image, glTexture)
3008
+ setFullImage(textureInfo)
2921
3009
  {
2922
3010
  this.pos = new Vector2;
2923
- this.size = new Vector2(image.width, image.height);
2924
- this.textureInfo = new TextureInfo(image, glTexture);
3011
+ this.size = textureInfo.size.copy();
3012
+ this.textureInfo = textureInfo;
2925
3013
  // do not use padding or bleed
2926
3014
  this.bleedScale = this.padding = 0;
2927
3015
  return this;
2928
3016
  }
2929
3017
  }
2930
3018
 
2931
- /** Texture Info - Stores info about each texture */
3019
+ /**
3020
+ * Tile Info - Stores info about each texture
3021
+ * @memberof Draw
3022
+ */
2932
3023
  class TextureInfo
2933
3024
  {
2934
3025
  /**
2935
3026
  * Create a TextureInfo, called automatically by the engine
2936
3027
  * @param {HTMLImageElement|OffscreenCanvas} image
2937
- * @param {WebGLTexture} [glTexture] - WebGL texture
3028
+ * @param {boolean} [useWebGL] - Should use WebGL if available?
2938
3029
  */
2939
- constructor(image, glTexture)
3030
+ constructor(image, useWebGL=true)
2940
3031
  {
2941
- /** @property {HTMLImageElement} - image source */
3032
+ /** @property {HTMLImageElement|OffscreenCanvas} - image source */
2942
3033
  this.image = image;
2943
3034
  /** @property {Vector2} - size of the image */
2944
- this.size = vec2(image.width, image.height);
3035
+ this.size = image ? vec2(image.width, image.height) : vec2();
2945
3036
  /** @property {Vector2} - inverse of the size, cached for rendering */
2946
- this.sizeInverse = vec2(1/image.width, 1/image.height);
3037
+ this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
2947
3038
  /** @property {WebGLTexture} - WebGL texture */
2948
- this.glTexture = glTexture;
3039
+ this.glTexture = undefined;
3040
+ useWebGL && this.createWebGLTexture();
2949
3041
  }
2950
3042
 
2951
- createWebGLTexture()
2952
- {
2953
- ASSERT(!this.glTexture);
2954
- if (glEnable)
2955
- this.glTexture = glCreateTexture(this.image);
2956
- }
3043
+ /** Creates the WebGL texture, updates if already created */
3044
+ createWebGLTexture() { glRegisterTextureInfo(this); }
3045
+
3046
+ /** Destroys the WebGL texture */
3047
+ destroyWebGLTexture() { glUnregisterTextureInfo(this); }
3048
+
3049
+ /** Check if the texture is webgl enabled
3050
+ * @return {boolean} */
3051
+ hasWebGL() { return !!this.glTexture; }
2957
3052
  }
2958
3053
 
2959
3054
  ///////////////////////////////////////////////////////////////////////////////
@@ -2974,10 +3069,11 @@ class TextureInfo
2974
3069
  function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
2975
3070
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
2976
3071
  {
2977
- ASSERT(isVector2(pos), 'drawTile pos should be a vec2');
2978
- ASSERT(isVector2(size), 'drawTile size should be a vec2');
2979
- ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
2980
- ASSERT(isNumber(angle), 'drawTile angle should be a number');
3072
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3073
+ ASSERT(isVector2(size), 'size must be a vec2');
3074
+ ASSERT(isColor(color), 'color is invalid');
3075
+ ASSERT(isNumber(angle), 'angle must be a number');
3076
+ ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
2981
3077
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2982
3078
 
2983
3079
  const textureInfo = tileInfo && tileInfo.textureInfo;
@@ -3072,10 +3168,10 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3072
3168
  * @memberof Draw */
3073
3169
  function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3074
3170
  {
3075
- ASSERT(isVector2(pos), 'drawRectGradient pos should be a vec2');
3076
- ASSERT(isVector2(size), 'drawRectGradient size should be a vec2');
3077
- ASSERT(isColor(colorTop) && isColor(colorBottom), 'drawRectGradient color is invalid');
3078
- ASSERT(isNumber(angle), 'drawRectGradient angle should be a number');
3171
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3172
+ ASSERT(isVector2(size), 'size must be a vec2');
3173
+ ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
3174
+ ASSERT(isNumber(angle), 'angle must be a number');
3079
3175
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3080
3176
  if (useWebGL)
3081
3177
  {
@@ -3133,11 +3229,11 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3133
3229
  * @memberof Draw */
3134
3230
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3135
3231
  {
3136
- ASSERT(Array.isArray(points), 'drawLineList points should be an array');
3137
- ASSERT(isNumber(width), 'drawLineList width should be a number');
3138
- ASSERT(isColor(color), 'drawLineList color is invalid');
3139
- ASSERT(isVector2(pos), 'drawLineList pos should be a vec2');
3140
- ASSERT(isNumber(angle), 'drawLineList angle should be a number');
3232
+ ASSERT(Array.isArray(points), 'points must be an array');
3233
+ ASSERT(isNumber(width), 'width must be a number');
3234
+ ASSERT(isColor(color), 'color is invalid');
3235
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3236
+ ASSERT(isNumber(angle), 'angle must be a number');
3141
3237
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3142
3238
  if (useWebGL)
3143
3239
  {
@@ -3208,8 +3304,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3208
3304
  * @memberof Draw */
3209
3305
  function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3210
3306
  {
3211
- ASSERT(isVector2(size), 'drawRegularPoly size should be a vec2');
3212
- ASSERT(isNumber(sides), 'drawRegularPoly sides should be a number');
3307
+ ASSERT(isVector2(size), 'size must be a vec2');
3308
+ ASSERT(isNumber(sides), 'sides must be a number');
3213
3309
 
3214
3310
  // build regular polygon points
3215
3311
  const points = [];
@@ -3235,12 +3331,13 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3235
3331
  * @memberof Draw */
3236
3332
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3237
3333
  {
3238
- ASSERT(isVector2(pos), 'drawPoly pos should be a vec2');
3239
- ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3240
- ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3241
- ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3242
- ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3334
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3335
+ ASSERT(Array.isArray(points), 'points must be an array');
3336
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3337
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3338
+ ASSERT(isNumber(angle), 'angle must be a number');
3243
3339
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3340
+
3244
3341
  if (useWebGL)
3245
3342
  {
3246
3343
  let scale = 1;
@@ -3287,13 +3384,14 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3287
3384
  * @memberof Draw */
3288
3385
  function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3289
3386
  {
3290
- ASSERT(isVector2(pos), 'drawEllipse pos should be a vec2');
3291
- ASSERT(isVector2(size), 'drawEllipse size should be a vec2');
3292
- ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3293
- ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3294
- ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
3295
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3387
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3388
+ ASSERT(isVector2(size), 'size must be a vec2');
3389
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3390
+ ASSERT(isNumber(angle), 'angle must be a number');
3391
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3392
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
3296
3393
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3394
+
3297
3395
  if (useWebGL)
3298
3396
  {
3299
3397
  // draw as a regular polygon
@@ -3330,21 +3428,32 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3330
3428
  * @memberof Draw */
3331
3429
  function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3332
3430
  {
3333
- ASSERT(isNumber(size), 'drawCircle size should be a number');
3431
+ ASSERT(isNumber(size), 'size must be a number');
3334
3432
  drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
3335
3433
  }
3336
3434
 
3435
+ /**
3436
+ * @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
3437
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
3438
+ * @memberof Draw
3439
+ */
3440
+
3337
3441
  /** Draw directly to a 2d canvas context in world space
3338
3442
  * @param {Vector2} pos
3339
3443
  * @param {Vector2} size
3340
3444
  * @param {number} angle
3341
3445
  * @param {boolean} [mirror]
3342
- * @param {Function} [drawFunction]
3446
+ * @param {Canvas2DDrawFunction} [drawFunction]
3343
3447
  * @param {boolean} [screenSpace=false]
3344
3448
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3345
3449
  * @memberof Draw */
3346
3450
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
3347
3451
  {
3452
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3453
+ ASSERT(isVector2(size), 'size must be a vec2');
3454
+ ASSERT(isNumber(angle), 'angle must be a number');
3455
+ ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
3456
+
3348
3457
  if (!screenSpace)
3349
3458
  {
3350
3459
  // transform from world space to screen space
@@ -3372,12 +3481,13 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3372
3481
  * @param {Color} [lineColor=(0,0,0,1)]
3373
3482
  * @param {CanvasTextAlign} [textAlign='center']
3374
3483
  * @param {string} [font=fontDefault]
3484
+ * @param {string} [fontStyle]
3375
3485
  * @param {number} [maxWidth]
3376
3486
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3377
3487
  * @memberof Draw */
3378
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth, context=drawContext)
3488
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
3379
3489
  {
3380
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, maxWidth, context);
3490
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
3381
3491
  }
3382
3492
 
3383
3493
  /** Draw text on overlay canvas in world space
@@ -3390,11 +3500,12 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3390
3500
  * @param {Color} [lineColor=(0,0,0,1)]
3391
3501
  * @param {CanvasTextAlign} [textAlign='center']
3392
3502
  * @param {string} [font=fontDefault]
3503
+ * @param {string} [fontStyle]
3393
3504
  * @param {number} [maxWidth]
3394
3505
  * @memberof Draw */
3395
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth)
3506
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
3396
3507
  {
3397
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, maxWidth, overlayContext);
3508
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
3398
3509
  }
3399
3510
 
3400
3511
  /** Draw text on overlay canvas in screen space
@@ -3407,16 +3518,27 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3407
3518
  * @param {Color} [lineColor=(0,0,0,1)]
3408
3519
  * @param {CanvasTextAlign} [textAlign]
3409
3520
  * @param {string} [font=fontDefault]
3521
+ * @param {string} [fontStyle]
3410
3522
  * @param {number} [maxWidth]
3411
3523
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3412
3524
  * @memberof Draw */
3413
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3414
- {
3525
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
3526
+ {
3527
+ ASSERT(isString(text), 'text must be a string');
3528
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3529
+ ASSERT(isNumber(size), 'size must be a number');
3530
+ ASSERT(isColor(color), 'color must be a color');
3531
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3532
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
3533
+ ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
3534
+ ASSERT(isString(font), 'font must be a string');
3535
+ ASSERT(isString(fontStyle), 'fontStyle must be a string');
3536
+
3415
3537
  context.fillStyle = color.toString();
3416
3538
  context.strokeStyle = lineColor.toString();
3417
3539
  context.lineWidth = lineWidth;
3418
3540
  context.textAlign = textAlign;
3419
- context.font = size + 'px '+ font;
3541
+ context.font = fontStyle + ' ' + size + 'px '+ font;
3420
3542
  context.textBaseline = 'middle';
3421
3543
 
3422
3544
  const lines = (text+'').split('\n');
@@ -3613,6 +3735,7 @@ let engineFontImage;
3613
3735
  * - 96 characters (from space to tilde) are stored in an image
3614
3736
  * - Uses a default 8x8 font if none is supplied
3615
3737
  * - You can also use fonts from the main tile sheet
3738
+ * @memberof Draw
3616
3739
  * @example
3617
3740
  * // use built in font
3618
3741
  * const font = new FontImage;
@@ -3623,11 +3746,11 @@ let engineFontImage;
3623
3746
  class FontImage
3624
3747
  {
3625
3748
  /** Create an image font
3626
- * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
3627
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3628
- * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
3749
+ * @param {HTMLImageElement} [image] - Image for the font, default if undefined
3750
+ * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3751
+ * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
3629
3752
  */
3630
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
3753
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
3631
3754
  {
3632
3755
  // load default font image
3633
3756
  if (!engineFontImage)
@@ -3669,7 +3792,7 @@ class FontImage
3669
3792
  * @param {boolean} [center]
3670
3793
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3671
3794
  */
3672
- drawTextScreen(text, pos, scale=4, center, context=overlayContext)
3795
+ drawTextScreen(text, pos, scale=4, center=true, context=overlayContext)
3673
3796
  {
3674
3797
  context.save();
3675
3798
  const size = this.tileSize;
@@ -4363,6 +4486,7 @@ function audioInit()
4363
4486
  * Sound Object - Stores a sound for later use and can be played positionally
4364
4487
  *
4365
4488
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
4489
+ * @memberof Audio
4366
4490
  * @example
4367
4491
  * // create a sound
4368
4492
  * const sound_example = new Sound([.5,.5]);
@@ -4457,12 +4581,12 @@ class Sound
4457
4581
 
4458
4582
  /** Play the sound as a musical note with a semitone offset
4459
4583
  * This can be used to play music with chromatic scales
4460
- * @param {number} semitoneOffset - How many semitones to offset pitch
4584
+ * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
4461
4585
  * @param {Vector2} [pos] - World space position to play the sound if any
4462
4586
  * @param {number} [volume=1] - How much to scale volume by
4463
4587
  * @return {SoundInstance} - The audio source node
4464
4588
  */
4465
- playNote(semitoneOffset, pos, volume)
4589
+ playNote(semitoneOffset=0, pos, volume)
4466
4590
  {
4467
4591
  const pitch = getNoteFrequency(semitoneOffset, 1);
4468
4592
  return this.play(pos, volume, pitch, 0);
@@ -4485,6 +4609,8 @@ class Sound
4485
4609
  /**
4486
4610
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
4487
4611
  * - this can be used to play wave, mp3, and ogg files
4612
+ * @extends Sound
4613
+ * @memberof Audio
4488
4614
  * @example
4489
4615
  * // create a sound
4490
4616
  * const sound_example = new SoundWave('sound.mp3');
@@ -4494,22 +4620,29 @@ class Sound
4494
4620
  */
4495
4621
  class SoundWave extends Sound
4496
4622
  {
4623
+ /**
4624
+ * @callback SoundLoadCallback - Function called when sound is loaded
4625
+ * @param {SoundWave} sound
4626
+ * @memberof Audio
4627
+ */
4628
+
4497
4629
  /** Create a sound object and cache the wave file for later use
4498
4630
  * @param {string} filename - Filename of audio file to load
4499
4631
  * @param {number} [randomness] - How much to randomize frequency each time sound plays
4500
4632
  * @param {number} [range=soundDefaultRange] - World space max range of sound
4501
4633
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
4502
- * @param {Function} [onloadCallback] - callback function to call when sound is loaded
4634
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
4503
4635
  */
4504
4636
  constructor(filename, randomness=0, range, taper, onloadCallback)
4505
4637
  {
4506
4638
  super(undefined, range, taper);
4507
4639
  if (!soundEnable || headlessMode) return;
4640
+ ASSERT(!filename || isString(filename), 'filename must be a string');
4508
4641
 
4509
- /** @property {Function} - callback function to call when sound is loaded */
4642
+ /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
4510
4643
  this.onloadCallback = onloadCallback;
4511
4644
  this.randomness = randomness;
4512
- this.loadSound(filename);
4645
+ filename && this.loadSound(filename);
4513
4646
  }
4514
4647
 
4515
4648
  /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
@@ -4518,6 +4651,8 @@ class SoundWave extends Sound
4518
4651
  async loadSound(filename)
4519
4652
  {
4520
4653
  const response = await fetch(filename);
4654
+ if (!response.ok)
4655
+ throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);
4521
4656
  const arrayBuffer = await response.arrayBuffer();
4522
4657
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
4523
4658
 
@@ -4553,7 +4688,7 @@ class SoundWave extends Sound
4553
4688
  this.sampleChannels = sampleChannels;
4554
4689
  this.loadedPercent = 1;
4555
4690
  if (this.onloadCallback)
4556
- this.onloadCallback();
4691
+ this.onloadCallback(this);
4557
4692
  }
4558
4693
  }
4559
4694
 
@@ -4562,6 +4697,7 @@ class SoundWave extends Sound
4562
4697
  /**
4563
4698
  * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
4564
4699
  * Represents a single playing instance of a sound with pause/resume capabilities
4700
+ * @memberof Audio
4565
4701
  * @example
4566
4702
  * // Play a sound and get an instance for control
4567
4703
  * const jumpSound = new Sound([.5,.5,220]);
@@ -4627,8 +4763,16 @@ class SoundInstance
4627
4763
  this.stop();
4628
4764
  this.gainNode = audioContext.createGain();
4629
4765
  this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
4630
- this.startTime = audioContext.currentTime - offset;
4631
- this.pausedTime = undefined;
4766
+ if (this.source)
4767
+ {
4768
+ this.startTime = audioContext.currentTime - offset;
4769
+ this.pausedTime = undefined;
4770
+ }
4771
+ else
4772
+ {
4773
+ this.startTime = undefined;
4774
+ this.pausedTime = 0;
4775
+ }
4632
4776
  }
4633
4777
 
4634
4778
  /** Set the volume of this sound instance
@@ -4761,6 +4905,12 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4761
4905
 
4762
4906
  ///////////////////////////////////////////////////////////////////////////////
4763
4907
 
4908
+ /**
4909
+ * @callback AudioEndedCallback - Function called when a sound ends
4910
+ * @param {AudioBufferSourceNode} source
4911
+ * @memberof Audio
4912
+ */
4913
+
4764
4914
  /** Play cached audio samples with given settings
4765
4915
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
4766
4916
  * @param {number} [volume] - How much to scale volume by
@@ -4770,13 +4920,20 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4770
4920
  * @param {number} [sampleRate=44100] - Sample rate for the sound
4771
4921
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
4772
4922
  * @param {number} [offset] - Offset in seconds to start playback from
4773
- * @param {Function} [onended] - Callback for when the sound ends
4774
- * @return {AudioBufferSourceNode} - The audio node of the sound played
4923
+ * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
4924
+ * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
4775
4925
  * @memberof Audio */
4776
4926
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
4777
4927
  {
4778
4928
  if (!soundEnable || headlessMode) return;
4779
4929
 
4930
+ if (!audioIsRunning())
4931
+ {
4932
+ // fix stalled audio, this sound won't be able to play
4933
+ audioContext.resume();
4934
+ return;
4935
+ }
4936
+
4780
4937
  // create buffer and source
4781
4938
  const channelCount = sampleChannels.length;
4782
4939
  const sampleLength = sampleChannels[0].length;
@@ -4802,13 +4959,6 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4802
4959
  if (onended)
4803
4960
  source.addEventListener('ended', ()=> onended(source));
4804
4961
 
4805
- if (!audioIsRunning())
4806
- {
4807
- // fix stalled audio, this sound won't be able to play
4808
- audioContext.resume();
4809
- return;
4810
- }
4811
-
4812
4962
  // play and return sound
4813
4963
  const startOffset = offset * rate;
4814
4964
  source.start(0, startOffset);
@@ -4976,7 +5126,7 @@ function zzfxG
4976
5126
  * - Unlimited numbers of layers, allocates canvases as needed
4977
5127
  * - Tile layers can be drawn to using their context with canvas2d
4978
5128
  * - Tile layers can also have collision with EngineObjects
4979
- * @namespace TileCollision
5129
+ * @namespace TileLayers
4980
5130
  */
4981
5131
 
4982
5132
  ///////////////////////////////////////////////////////////////////////////////
@@ -4984,13 +5134,13 @@ function zzfxG
4984
5134
 
4985
5135
  /** Keep track of all tile layers with collision
4986
5136
  * @type {Array<TileCollisionLayer>}
4987
- * @memberof TileCollision */
5137
+ * @memberof TileLayers */
4988
5138
  const tileCollisionLayers = [];
4989
5139
 
4990
5140
  /** Get tile collision data for a given cell in the grid
4991
5141
  * @param {Vector2} pos
4992
5142
  * @return {number}
4993
- * @memberof TileCollision */
5143
+ * @memberof TileLayers */
4994
5144
  function tileCollisionGetData(pos)
4995
5145
  {
4996
5146
  // check all tile collision layers
@@ -5006,7 +5156,7 @@ function tileCollisionGetData(pos)
5006
5156
  * @param {EngineObject} [object] - An object or undefined for generic test
5007
5157
  * @param {boolean} [solidOnly] - Only check solid layers if true
5008
5158
  * @return {TileCollisionLayer}
5009
- * @memberof TileCollision */
5159
+ * @memberof TileLayers */
5010
5160
  function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5011
5161
  {
5012
5162
  for (const layer of tileCollisionLayers)
@@ -5024,7 +5174,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5024
5174
  * @param {EngineObject} [object] - An object or undefined for generic test
5025
5175
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5026
5176
  * @return {Vector2}
5027
- * @memberof TileCollision */
5177
+ * @memberof TileLayers */
5028
5178
  function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5029
5179
  {
5030
5180
  for (const layer of tileCollisionLayers)
@@ -5047,8 +5197,8 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5047
5197
  * @param {number} [collisionLayer] - Layer to use for collision if any
5048
5198
  * @param {boolean} [draw] - Should the layer be drawn automatically
5049
5199
  * @return {Array<TileCollisionLayer>}
5050
- * @memberof TileCollision */
5051
- function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5200
+ * @memberof TileLayers */
5201
+ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5052
5202
  {
5053
5203
  if (!tileMapData)
5054
5204
  {
@@ -5079,9 +5229,9 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5079
5229
  tileLayers[layerIndex] = tileLayer;
5080
5230
 
5081
5231
  // apply layer color
5082
- const layerColor = dataLayer.color || WHITE;
5083
- if (dataLayer.tintcolor)
5084
- layerColor.setHex(dataLayer.tintcolor);
5232
+ const layerColor = dataLayer.tintcolor ?
5233
+ new Color().setHex(dataLayer.tintcolor) :
5234
+ dataLayer.color || WHITE;
5085
5235
  ASSERT(isColor(layerColor), 'layer color is not a color');
5086
5236
 
5087
5237
  for (let x=levelSize.x; x--;)
@@ -5108,6 +5258,7 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5108
5258
  ///////////////////////////////////////////////////////////////////////////////
5109
5259
  /**
5110
5260
  * Tile layer data object stores info about how to draw a tile
5261
+ * @memberof TileLayers
5111
5262
  * @example
5112
5263
  * // create tile layer data with tile index 0 and random orientation and color
5113
5264
  * const tileIndex = 0;
@@ -5145,6 +5296,7 @@ class TileLayerData
5145
5296
  * - Contains an offscreen canvas that can be rendered to
5146
5297
  * - WebGL rendering is optional, call useWebGL to enable
5147
5298
  * @extends EngineObject
5299
+ * @memberof TileLayers
5148
5300
  * @example
5149
5301
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
5150
5302
  */
@@ -5159,15 +5311,19 @@ class CanvasLayer extends EngineObject
5159
5311
  */
5160
5312
  constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
5161
5313
  {
5314
+ ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5162
5315
  super(position, size, undefined, angle, WHITE, renderOrder);
5163
5316
 
5164
5317
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
5165
5318
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
5166
5319
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5167
- this.context = headlessMode ? undefined : this.canvas.getContext('2d');
5168
- /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
5169
- this.glTexture = undefined;
5170
- this.gravityScale = 0; // disable gravity by default for canvas layers
5320
+ this.context = this.canvas?.getContext('2d');
5321
+ /** @property {TextureInfo} - Texture info to use for this object rendering */
5322
+ const useWebGL = false; // do not use webgl by default
5323
+ this.textureInfo = new TextureInfo(this.canvas, useWebGL);
5324
+
5325
+ // disable physics by default
5326
+ this.mass = this.gravityScale = this.friction = this.restitution = 0;
5171
5327
  }
5172
5328
 
5173
5329
  /** Destroy this canvas layer */
@@ -5176,9 +5332,7 @@ class CanvasLayer extends EngineObject
5176
5332
  if (this.destroyed)
5177
5333
  return;
5178
5334
 
5179
- // free up the WebGL texture
5180
- if (this.glTexture)
5181
- glDeleteTexture(this.glTexture);
5335
+ this.textureInfo.destroyWebGLTexture();
5182
5336
  super.destroy();
5183
5337
  }
5184
5338
 
@@ -5201,19 +5355,27 @@ class CanvasLayer extends EngineObject
5201
5355
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5202
5356
  {
5203
5357
  // draw the canvas layer as a single tile that uses the whole texture
5204
- const useWebGL = glEnable && this.glTexture !== undefined;
5205
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5358
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
5359
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5206
5360
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5207
5361
  }
5208
5362
 
5363
+ /**
5364
+ * @callback Canvas2DDrawCallback - Function that draws to a canvas 2D context
5365
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
5366
+ * @memberof TileLayers
5367
+ */
5368
+
5209
5369
  /** Draw onto the layer canvas in world space (bypass WebGL)
5210
5370
  * @param {Vector2} pos
5211
5371
  * @param {Vector2} size
5212
5372
  * @param {number} angle
5213
5373
  * @param {boolean} mirror
5214
- * @param {Function} drawFunction */
5374
+ * @param {Canvas2DDrawCallback} drawFunction */
5215
5375
  drawCanvas2D(pos, size, angle, mirror, drawFunction)
5216
5376
  {
5377
+ if (!this.context) return;
5378
+
5217
5379
  const context = this.context;
5218
5380
  context.save();
5219
5381
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
@@ -5248,7 +5410,7 @@ class CanvasLayer extends EngineObject
5248
5410
  else
5249
5411
  {
5250
5412
  // untextured
5251
- context.fillStyle = color;
5413
+ context.fillStyle = color.toString();
5252
5414
  context.fillRect(-.5, -.5, 1, 1);
5253
5415
  }
5254
5416
  });
@@ -5266,15 +5428,10 @@ class CanvasLayer extends EngineObject
5266
5428
  * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5267
5429
  useWebGL(enable=true)
5268
5430
  {
5269
- if (glEnable && enable)
5270
- {
5271
- if (this.glTexture)
5272
- glSetTextureData(this.glTexture, this.canvas);
5273
- else
5274
- this.glTexture = glCreateTexture(this.canvas);
5275
- }
5431
+ if (enable)
5432
+ this.textureInfo.createWebGLTexture();
5276
5433
  else
5277
- this.glTexture = undefined;
5434
+ this.textureInfo.destroyWebGLTexture();
5278
5435
  }
5279
5436
  }
5280
5437
 
@@ -5285,7 +5442,9 @@ class CanvasLayer extends EngineObject
5285
5442
  * - To allow dynamic modifications, layers are rendered using canvas 2d
5286
5443
  * - Some devices like mobile phones are limited to 4k texture resolution
5287
5444
  * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
5445
+ * - Tile layers are centered on their corner, so normal levels are at (0,0)
5288
5446
  * @extends CanvasLayer
5447
+ * @memberof TileLayers
5289
5448
  * @example
5290
5449
  * const tileLayer = new TileLayer(vec2(), vec2(200,100));
5291
5450
  */
@@ -5295,26 +5454,15 @@ class TileLayer extends CanvasLayer
5295
5454
  * @param {Vector2} position - World space position
5296
5455
  * @param {Vector2} size - World space size
5297
5456
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
5298
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
5299
5457
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5300
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5301
5458
  */
5302
- constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0, useWebGL=glEnable)
5459
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
5303
5460
  {
5304
- super(position, size, 0, renderOrder, size);
5305
- this.tileInfo = tileInfo;
5306
-
5307
5461
  const canvasSize = size.multiply(tileInfo.size);
5308
- /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
5309
- this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
5310
- /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
5311
- this.context = this.canvas.getContext('2d');
5312
- /** @property {WebGLTexture} - Texture if using WebGL for this layer */
5313
- this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
5314
- // set no friction by default, applied friction is max of both objects
5315
- this.friction = 0;
5316
- // set no restitution by default, applied restitution is max of both objects
5317
- this.restitution = 0;
5462
+ super(position, size, 0, renderOrder, canvasSize);
5463
+
5464
+ // set tile info
5465
+ this.tileInfo = tileInfo;
5318
5466
 
5319
5467
  // init tile data
5320
5468
  this.data = [];
@@ -5340,6 +5488,8 @@ class TileLayer extends CanvasLayer
5340
5488
  * @param {boolean} [redraw] - Force the tile to redraw if true */
5341
5489
  setData(layerPos, data, redraw=false)
5342
5490
  {
5491
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
5492
+ ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
5343
5493
  if (layerPos.arrayCheck(this.size))
5344
5494
  {
5345
5495
  this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
@@ -5351,7 +5501,10 @@ class TileLayer extends CanvasLayer
5351
5501
  * @param {Vector2} layerPos - Local position in array
5352
5502
  * @return {TileLayerData} */
5353
5503
  getData(layerPos)
5354
- { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
5504
+ {
5505
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
5506
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
5507
+ }
5355
5508
 
5356
5509
  // Render the tile layer, called automatically by the engine
5357
5510
  render()
@@ -5359,10 +5512,11 @@ class TileLayer extends CanvasLayer
5359
5512
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5360
5513
 
5361
5514
  // draw the tile layer as a single tile
5362
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5363
- const pos = this.pos.add(this.size.scale(.5));
5364
- const useWebGL = glEnable && this.glTexture !== undefined;
5365
- drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5515
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5516
+ const size = this.drawSize || this.size;
5517
+ const pos = this.pos.add(size.scale(.5));
5518
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
5519
+ drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5366
5520
  }
5367
5521
 
5368
5522
  /** Draw all the tile data to an offscreen canvas
@@ -5374,8 +5528,7 @@ class TileLayer extends CanvasLayer
5374
5528
  for (let y = this.size.y; y--;)
5375
5529
  this.drawTileData(vec2(x,y), false);
5376
5530
  this.redrawEnd();
5377
- if (this.glTexture)
5378
- this.useWebGL(); // update WebGL texture
5531
+ this.useWebGL();
5379
5532
  }
5380
5533
 
5381
5534
  /** Call to start the redraw process
@@ -5383,6 +5536,8 @@ class TileLayer extends CanvasLayer
5383
5536
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
5384
5537
  redrawStart(clear=false)
5385
5538
  {
5539
+ if (!this.context) return;
5540
+
5386
5541
  // save current render settings
5387
5542
  /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
5388
5543
  this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
@@ -5411,6 +5566,8 @@ class TileLayer extends CanvasLayer
5411
5566
  /** Call to end the redraw process */
5412
5567
  redrawEnd()
5413
5568
  {
5569
+ if (!this.context) return;
5570
+
5414
5571
  ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
5415
5572
  glCopyToContext(drawContext);
5416
5573
  //debugSaveCanvas(this.canvas);
@@ -5427,6 +5584,8 @@ class TileLayer extends CanvasLayer
5427
5584
  */
5428
5585
  drawTileData(layerPos, clear=true)
5429
5586
  {
5587
+ if (!this.context) return;
5588
+
5430
5589
  // clear out where the tile was, for full opaque tiles this can be skipped
5431
5590
  const s = this.tileInfo.size;
5432
5591
  if (clear)
@@ -5454,6 +5613,7 @@ class TileLayer extends CanvasLayer
5454
5613
  * - there can be multiple tile collision layers
5455
5614
  * - tile collision layers should not overlap each other
5456
5615
  * @extends TileLayer
5616
+ * @memberof TileLayers
5457
5617
  */
5458
5618
  class TileCollisionLayer extends TileLayer
5459
5619
  {
@@ -5462,12 +5622,10 @@ class TileCollisionLayer extends TileLayer
5462
5622
  * @param {Vector2} size - World space size
5463
5623
  * @param {TileInfo} [tileInfo] - Tile info for layer
5464
5624
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5465
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5466
5625
  */
5467
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
5626
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
5468
5627
  {
5469
- const scale = vec2(1); // collision layers are not scaled
5470
- super(position, size.floor(), tileInfo, scale, renderOrder, useWebGL);
5628
+ super(position, size.floor(), tileInfo, renderOrder);
5471
5629
 
5472
5630
  /** @property {Array<number>} - The tile collision grid */
5473
5631
  this.collisionData = [];
@@ -5497,6 +5655,7 @@ class TileCollisionLayer extends TileLayer
5497
5655
  * @param {Vector2} size - width and height of tile collision 2d grid */
5498
5656
  initCollision(size)
5499
5657
  {
5658
+ ASSERT(isVector2(size), 'size must be a Vector2');
5500
5659
  this.size = size.floor();
5501
5660
  this.collisionData = [];
5502
5661
  this.collisionData.length = size.area();
@@ -5508,6 +5667,7 @@ class TileCollisionLayer extends TileLayer
5508
5667
  * @param {number} [data] */
5509
5668
  setCollisionData(gridPos, data=1)
5510
5669
  {
5670
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
5511
5671
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
5512
5672
  gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
5513
5673
  }
@@ -5517,6 +5677,7 @@ class TileCollisionLayer extends TileLayer
5517
5677
  * @return {number} */
5518
5678
  getCollisionData(gridPos)
5519
5679
  {
5680
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
5520
5681
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
5521
5682
  return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
5522
5683
  }
@@ -5528,6 +5689,9 @@ class TileCollisionLayer extends TileLayer
5528
5689
  * @return {boolean} */
5529
5690
  collisionTest(pos, size=new Vector2, object)
5530
5691
  {
5692
+ ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
5693
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
5694
+
5531
5695
  // transform to local layer space
5532
5696
  const posX = pos.x - this.pos.x;
5533
5697
  const posY = pos.y - this.pos.y;
@@ -5558,6 +5722,9 @@ class TileCollisionLayer extends TileLayer
5558
5722
  * @return {Vector2} */
5559
5723
  collisionRaycast(posStart, posEnd, object)
5560
5724
  {
5725
+ ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
5726
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
5727
+
5561
5728
  // transform to local layer space
5562
5729
  const posStartX = posStart.x - this.pos.x;
5563
5730
  const posStartY = posStart.y - this.pos.y;
@@ -5607,9 +5774,16 @@ class TileCollisionLayer extends TileLayer
5607
5774
  * LittleJS Particle System
5608
5775
  */
5609
5776
 
5777
+ /**
5778
+ * @callback ParticleCallbackFunction - Function that processes a particle
5779
+ * @param {Particle} particle
5780
+ * @memberof Engine
5781
+ */
5782
+
5610
5783
  /**
5611
5784
  * Particle Emitter - Spawns particles with the given settings
5612
5785
  * @extends EngineObject
5786
+ * @memberof Engine
5613
5787
  * @example
5614
5788
  * // create a particle emitter
5615
5789
  * let pos = vec2(2,3);
@@ -5619,7 +5793,7 @@ class TileCollisionLayer extends TileLayer
5619
5793
  * tile(0, 16), // tileInfo
5620
5794
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
5621
5795
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
5622
- * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5796
+ * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5623
5797
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5624
5798
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
5625
5799
  * );
@@ -5634,10 +5808,10 @@ class ParticleEmitter extends EngineObject
5634
5808
  * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
5635
5809
  * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
5636
5810
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
5637
- * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
5638
- * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
5639
- * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
5640
- * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
5811
+ * @param {Color} [colorStartA=WHITE] - Color at start of life 1, randomized between start colors
5812
+ * @param {Color} [colorStartB=WHITE] - Color at start of life 2, randomized between start colors
5813
+ * @param {Color} [colorEndA=CLEAR_WHITE] - Color at end of life 1, randomized between end colors
5814
+ * @param {Color} [colorEndB=CLEAR_WHITE] - Color at end of life 2, randomized between end colors
5641
5815
  * @param {number} [particleTime] - How long particles live
5642
5816
  * @param {number} [sizeStart] - How big are particles at start
5643
5817
  * @param {number} [sizeEnd] - How big are particles at end
@@ -5664,10 +5838,10 @@ class ParticleEmitter extends EngineObject
5664
5838
  emitRate = 100,
5665
5839
  emitConeAngle = PI,
5666
5840
  tileInfo,
5667
- colorStartA = new Color,
5668
- colorStartB = new Color,
5669
- colorEndA = new Color(1,1,1,0),
5670
- colorEndB = new Color(1,1,1,0),
5841
+ colorStartA = WHITE,
5842
+ colorStartB = WHITE,
5843
+ colorEndA = CLEAR_WHITE,
5844
+ colorEndB = CLEAR_WHITE,
5671
5845
  particleTime = .5,
5672
5846
  sizeStart = .1,
5673
5847
  sizeEnd = 1,
@@ -5690,7 +5864,8 @@ class ParticleEmitter extends EngineObject
5690
5864
 
5691
5865
  // emitter settings
5692
5866
  /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
5693
- this.emitSize = emitSize
5867
+ this.emitSize = emitSize instanceof Vector2 ?
5868
+ emitSize.copy() : emitSize;
5694
5869
  /** @property {number} - How long to stay alive (0 is forever) */
5695
5870
  this.emitTime = emitTime;
5696
5871
  /** @property {number} - How many particles per second to spawn, does not emit if 0 */
@@ -5700,13 +5875,13 @@ class ParticleEmitter extends EngineObject
5700
5875
 
5701
5876
  // color settings
5702
5877
  /** @property {Color} - Color at start of life 1, randomized between start colors */
5703
- this.colorStartA = colorStartA;
5878
+ this.colorStartA = colorStartA.copy();
5704
5879
  /** @property {Color} - Color at start of life 2, randomized between start colors */
5705
- this.colorStartB = colorStartB;
5880
+ this.colorStartB = colorStartB.copy();
5706
5881
  /** @property {Color} - Color at end of life 1, randomized between end colors */
5707
- this.colorEndA = colorEndA;
5882
+ this.colorEndA = colorEndA.copy();
5708
5883
  /** @property {Color} - Color at end of life 2, randomized between end colors */
5709
- this.colorEndB = colorEndB;
5884
+ this.colorEndB = colorEndB.copy();
5710
5885
  /** @property {boolean} - Should color be randomized linearly or across each component */
5711
5886
  this.randomColorLinear = randomColorLinear;
5712
5887
 
@@ -5741,9 +5916,9 @@ class ParticleEmitter extends EngineObject
5741
5916
  this.localSpace = localSpace;
5742
5917
  /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
5743
5918
  this.trailScale = 0;
5744
- /** @property {Function} - Callback when particle is destroyed */
5919
+ /** @property {ParticleCallbackFunction} - Callback when particle is destroyed */
5745
5920
  this.particleDestroyCallback = undefined;
5746
- /** @property {Function} - Callback when particle is created */
5921
+ /** @property {ParticleCallbackFunction} - Callback when particle is created */
5747
5922
  this.particleCreateCallback = undefined;
5748
5923
  /** @property {number} - Track particle emit time */
5749
5924
  this.emitTimeBuffer = 0;
@@ -5844,6 +6019,7 @@ class ParticleEmitter extends EngineObject
5844
6019
  /**
5845
6020
  * Particle Object - Created automatically by Particle Emitters
5846
6021
  * @extends EngineObject
6022
+ * @memberof Engine
5847
6023
  */
5848
6024
  class Particle extends EngineObject
5849
6025
  {
@@ -5862,7 +6038,7 @@ class Particle extends EngineObject
5862
6038
  * @param {boolean} additive - Does it use additive blend mode
5863
6039
  * @param {number} trailScale - If a trail, how long to make it
5864
6040
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
5865
- * @param {Function} [destroyCallback] - Callback when particle dies
6041
+ * @param {ParticleCallbackFunction} [destroyCallback] - Callback when particle dies
5866
6042
  */
5867
6043
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
5868
6044
  )
@@ -5871,14 +6047,14 @@ class Particle extends EngineObject
5871
6047
 
5872
6048
  /** @property {Color} - Color at start of life */
5873
6049
  this.colorStart = colorStart;
5874
- /** @property {Color} - Calculated change in color */
5875
- this.colorEndDelta = colorEnd.subtract(colorStart);
6050
+ /** @property {Color} - Color at end of life */
6051
+ this.colorEnd = colorEnd;
5876
6052
  /** @property {number} - How long to live for */
5877
6053
  this.lifeTime = lifeTime;
5878
6054
  /** @property {number} - Size at start of life */
5879
6055
  this.sizeStart = sizeStart;
5880
- /** @property {number} - Calculated change in size */
5881
- this.sizeEndDelta = sizeEnd - sizeStart;
6056
+ /** @property {number} - Size at end of life */
6057
+ this.sizeEnd = sizeEnd;
5882
6058
  /** @property {number} - How quick to fade in/out */
5883
6059
  this.fadeRate = fadeRate;
5884
6060
  /** @property {boolean} - Is it additive */
@@ -5887,7 +6063,7 @@ class Particle extends EngineObject
5887
6063
  this.trailScale = trailScale;
5888
6064
  /** @property {ParticleEmitter} - Parent emitter if local space */
5889
6065
  this.localSpaceEmitter = localSpaceEmitter;
5890
- /** @property {Function} - Called when particle dies */
6066
+ /** @property {ParticleCallbackFunction} - Called when particle dies */
5891
6067
  this.destroyCallback = destroyCallback;
5892
6068
  // particles do not clamp speed by default
5893
6069
  this.clampSpeed = false;
@@ -5914,54 +6090,58 @@ class Particle extends EngineObject
5914
6090
  /** Render the particle, automatically called each frame, sorted by renderOrder */
5915
6091
  render()
5916
6092
  {
5917
- // modulate size and color
5918
- const p = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1;
5919
- const radius = this.sizeStart + p * this.sizeEndDelta;
5920
- const size = vec2(radius);
6093
+ // lerp color and size
6094
+ const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6095
+ const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6096
+ this.size.x = this.size.y = radius;
6097
+ this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6098
+ this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6099
+ this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
6100
+ this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
6101
+
6102
+ // fade alpha
5921
6103
  const fadeRate = this.fadeRate/2;
5922
- const color = new Color(
5923
- this.colorStart.r + p * this.colorEndDelta.r,
5924
- this.colorStart.g + p * this.colorEndDelta.g,
5925
- this.colorStart.b + p * this.colorEndDelta.b,
5926
- (this.colorStart.a + p * this.colorEndDelta.a) *
5927
- (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
6104
+ this.color.a *= p1 < fadeRate ? p1/fadeRate :
6105
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
5928
6106
 
5929
6107
  // draw the particle
5930
6108
  this.additive && setBlendMode(true);
5931
6109
 
6110
+ // update the position and angle for drawing
5932
6111
  let pos = this.pos, angle = this.angle;
5933
6112
  if (this.localSpaceEmitter)
5934
6113
  {
5935
6114
  // in local space of emitter
5936
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
6115
+ const a = this.localSpaceEmitter.angle;
6116
+ const c = Math.cos(a), s = Math.sin(a);
6117
+ pos = this.localSpaceEmitter.pos.add(
6118
+ new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
5937
6119
  angle += this.localSpaceEmitter.angle;
5938
6120
  }
5939
6121
  if (this.trailScale)
5940
6122
  {
5941
6123
  // trail style particles
5942
- let velocity = this.velocity;
5943
- if (this.localSpaceEmitter)
5944
- velocity = velocity.rotate(-this.localSpaceEmitter.angle);
5945
- const speed = velocity.length();
6124
+ const direction = this.localSpaceEmitter ?
6125
+ this.velocity.rotate(-this.localSpaceEmitter.angle) :
6126
+ this.velocity;
6127
+ const speed = direction.length();
5946
6128
  if (speed)
5947
6129
  {
5948
- const direction = velocity.scale(1/speed);
6130
+ // stretch in direction of motion
5949
6131
  const trailLength = speed * this.trailScale;
5950
- size.y = max(size.x, trailLength);
5951
- angle = direction.angle();
5952
- drawTile(pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileInfo, color, angle, this.mirror);
6132
+ this.size.y = max(this.size.x, trailLength);
6133
+ angle = Math.atan2(direction.x, direction.y);
6134
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
5953
6135
  }
5954
6136
  }
5955
6137
  else
5956
- drawTile(pos, size, this.tileInfo, color, angle, this.mirror);
6138
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
5957
6139
  this.additive && setBlendMode();
5958
- debugParticles && debugRect(pos, size, '#f005', 0, angle);
6140
+ debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
5959
6141
 
5960
- if (p === 1)
6142
+ if (p1 === 1)
5961
6143
  {
5962
- // destroy particle when it's time runs out
5963
- this.color = color;
5964
- this.size = size;
6144
+ // destroy particle when its time runs out
5965
6145
  this.destroyCallback && this.destroyCallback(this);
5966
6146
  this.destroyed = 1;
5967
6147
  }
@@ -6026,8 +6206,14 @@ function medalsInit(saveName)
6026
6206
  }
6027
6207
  }
6028
6208
 
6209
+ /**
6210
+ * @callback MedalCallbackFunction - Function that processes a medal
6211
+ * @param {Medal} medal
6212
+ * @memberof Medals
6213
+ */
6214
+
6029
6215
  /** Calls a function for each medal
6030
- * @param {Function} callback
6216
+ * @param {MedalCallbackFunction} callback
6031
6217
  * @memberof Medals */
6032
6218
  function medalsForEach(callback)
6033
6219
  { Object.values(medals).forEach(medal=>callback(medal)); }
@@ -6036,6 +6222,7 @@ function medalsForEach(callback)
6036
6222
 
6037
6223
  /**
6038
6224
  * Medal - Tracks an unlockable medal
6225
+ * @memberof Medals
6039
6226
  * @example
6040
6227
  * // create a medal
6041
6228
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
@@ -6104,11 +6291,12 @@ class Medal
6104
6291
  const height = medalDisplaySize.y;
6105
6292
  const x = overlayCanvas.width - width;
6106
6293
  const y = -height*hidePercent;
6294
+ const backgroundColor = hsl(0,0,.9);
6107
6295
 
6108
6296
  // draw containing rect and clip to that region
6109
6297
  context.save();
6110
6298
  context.beginPath();
6111
- context.fillStyle = new Color(.9,.9,.9).toString();
6299
+ context.fillStyle = backgroundColor.toString();
6112
6300
  context.strokeStyle = BLACK.toString();
6113
6301
  context.lineWidth = 3;
6114
6302
  context.rect(x, y, width, height);
@@ -6179,7 +6367,7 @@ let glContext;
6179
6367
  let glAntialias = true;
6180
6368
 
6181
6369
  // WebGL internal variables not exposed to documentation
6182
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
6370
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos;
6183
6371
 
6184
6372
  // WebGL internal constants
6185
6373
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6195,6 +6383,9 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
6195
6383
  // Initialize WebGL, called automatically by the engine
6196
6384
  function glInit()
6197
6385
  {
6386
+ // keep set of texture infos so they can be restored if context is lost
6387
+ glTextureInfos = new Set;
6388
+
6198
6389
  if (!glEnable || headlessMode) return;
6199
6390
 
6200
6391
  // create the canvas and textures
@@ -6212,68 +6403,101 @@ function glInit()
6212
6403
  // create the WebGL canvas
6213
6404
  const rootElement = mainCanvas.parentElement;
6214
6405
  rootElement.appendChild(glCanvas);
6406
+
6407
+ // startup webgl
6408
+ initWebGL();
6409
+
6410
+ // setup context lost and restore handlers
6411
+ glCanvas.addEventListener('webglcontextlost', (e)=>
6412
+ {
6413
+ glEnable = false; // disable WebGL rendering
6414
+ glCanvas.style.display = 'none'; // hide the gl canvas
6415
+ e.preventDefault(); // prevent default to allow restoration
6416
+ LOG('WebGL context lost! Switching to Canvas2d rendering.');
6417
+
6418
+ // remove WebGL textures
6419
+ for (const info of glTextureInfos)
6420
+ info.glTexture = undefined;
6421
+ glActiveTexture = undefined;
6422
+ pluginList.forEach(plugin=>plugin.glContextLost?.());
6423
+ });
6424
+ glCanvas.addEventListener('webglcontextrestored', ()=>
6425
+ {
6426
+ glEnable = true; // disable WebGL rendering
6427
+ glCanvas.style.display = ''; // show the gl canvas
6428
+ LOG('WebGL context restored, reinitializing...');
6215
6429
 
6216
- // setup instanced rendering shader program
6217
- glShader = glCreateProgram(
6218
- '#version 300 es\n' + // specify GLSL ES version
6219
- 'precision highp float;'+ // use highp for better accuracy
6220
- 'uniform mat4 m;'+ // transform matrix
6221
- 'in vec2 g;'+ // in: geometry
6222
- 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
6223
- 'in float r;'+ // in: rotation
6224
- 'out vec2 v;'+ // out: uv
6225
- 'out vec4 d,e;'+ // out: color, additiveColor
6226
- 'void main(){'+ // shader entry point
6227
- 'vec2 s=(g-.5)*p.zw;'+ // get size offset
6228
- 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
6229
- 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
6230
- 'd=c;e=a;'+ // pass colors to fragment shader
6231
- '}' // end of shader
6232
- ,
6233
- '#version 300 es\n' + // specify GLSL ES version
6234
- 'precision highp float;'+ // use highp for better accuracy
6235
- 'uniform sampler2D s;'+ // texture
6236
- 'in vec2 v;'+ // in: uv
6237
- 'in vec4 d,e;'+ // in: color, additiveColor
6238
- 'out vec4 c;'+ // out: color
6239
- 'void main(){'+ // shader entry point
6240
- 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
6241
- '}' // end of shader
6242
- );
6430
+ // reinit WebGL and restore textures
6431
+ initWebGL();
6432
+ for (const info of glTextureInfos)
6433
+ info.glTexture = glCreateTexture(info.image);
6434
+ pluginList.forEach(plugin=>plugin.glContextRestored?.());
6435
+ });
6243
6436
 
6244
- // setup poly rendering shaders
6245
- glPolyShader = glCreateProgram(
6246
- '#version 300 es\n' + // specify GLSL ES version
6247
- 'precision highp float;'+ // use highp for better accuracy
6248
- 'uniform mat4 m;'+ // transform matrix
6249
- 'in vec2 p;'+ // in: position
6250
- 'in vec4 c;'+ // in: color
6251
- 'out vec4 d;'+ // out: color
6252
- 'void main(){'+ // shader entry point
6253
- 'gl_Position=m*vec4(p,1,1);'+ // transform position
6254
- 'd=c;'+ // pass color to fragment shader
6255
- '}' // end of shader
6256
- ,
6257
- '#version 300 es\n' + // specify GLSL ES version
6258
- 'precision highp float;'+ // use highp for better accuracy
6259
- 'in vec4 d;'+ // in: color
6260
- 'out vec4 c;'+ // out: color
6261
- 'void main(){'+ // shader entry point
6262
- 'c=d;'+ // set color
6263
- '}' // end of shader
6264
- );
6437
+ function initWebGL()
6438
+ {
6439
+ // setup instanced rendering shader program
6440
+ glShader = glCreateProgram(
6441
+ '#version 300 es\n' + // specify GLSL ES version
6442
+ 'precision highp float;'+ // use highp for better accuracy
6443
+ 'uniform mat4 m;'+ // transform matrix
6444
+ 'in vec2 g;'+ // in: geometry
6445
+ 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
6446
+ 'in float r;'+ // in: rotation
6447
+ 'out vec2 v;'+ // out: uv
6448
+ 'out vec4 d,e;'+ // out: color, additiveColor
6449
+ 'void main(){'+ // shader entry point
6450
+ 'vec2 s=(g-.5)*p.zw;'+ // get size offset
6451
+ 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
6452
+ 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
6453
+ 'd=c;e=a;'+ // pass colors to fragment shader
6454
+ '}' // end of shader
6455
+ ,
6456
+ '#version 300 es\n' + // specify GLSL ES version
6457
+ 'precision highp float;'+ // use highp for better accuracy
6458
+ 'uniform sampler2D s;'+ // texture
6459
+ 'in vec2 v;'+ // in: uv
6460
+ 'in vec4 d,e;'+ // in: color, additiveColor
6461
+ 'out vec4 c;'+ // out: color
6462
+ 'void main(){'+ // shader entry point
6463
+ 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
6464
+ '}' // end of shader
6465
+ );
6466
+
6467
+ // setup poly rendering shaders
6468
+ glPolyShader = glCreateProgram(
6469
+ '#version 300 es\n' + // specify GLSL ES version
6470
+ 'precision highp float;'+ // use highp for better accuracy
6471
+ 'uniform mat4 m;'+ // transform matrix
6472
+ 'in vec2 p;'+ // in: position
6473
+ 'in vec4 c;'+ // in: color
6474
+ 'out vec4 d;'+ // out: color
6475
+ 'void main(){'+ // shader entry point
6476
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
6477
+ 'd=c;'+ // pass color to fragment shader
6478
+ '}' // end of shader
6479
+ ,
6480
+ '#version 300 es\n' + // specify GLSL ES version
6481
+ 'precision highp float;'+ // use highp for better accuracy
6482
+ 'in vec4 d;'+ // in: color
6483
+ 'out vec4 c;'+ // out: color
6484
+ 'void main(){'+ // shader entry point
6485
+ 'c=d;'+ // set color
6486
+ '}' // end of shader
6487
+ );
6265
6488
 
6266
- // init buffers
6267
- const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
6268
- glPositionData = new Float32Array(glInstanceData);
6269
- glColorData = new Uint32Array(glInstanceData);
6270
- glArrayBuffer = glContext.createBuffer();
6271
- glGeometryBuffer = glContext.createBuffer();
6489
+ // init buffers
6490
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
6491
+ glPositionData = new Float32Array(glInstanceData);
6492
+ glColorData = new Uint32Array(glInstanceData);
6493
+ glArrayBuffer = glContext.createBuffer();
6494
+ glGeometryBuffer = glContext.createBuffer();
6272
6495
 
6273
- // create the geometry buffer, triangle strip square
6274
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
6275
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6276
- glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
6496
+ // create the geometry buffer, triangle strip square
6497
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
6498
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6499
+ glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
6500
+ }
6277
6501
  }
6278
6502
 
6279
6503
  function glSetInstancedMode()
@@ -6380,7 +6604,7 @@ function glPreRender()
6380
6604
  // start with additive blending off
6381
6605
  glAdditive = glBatchAdditive = false;
6382
6606
 
6383
- // force it to enter instanced mode
6607
+ // force it to set instanced mode by first setting poly mode true
6384
6608
  glPolyMode = true;
6385
6609
  glSetInstancedMode();
6386
6610
  }
@@ -6521,6 +6745,41 @@ function glSetTextureData(texture, image)
6521
6745
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
6522
6746
  }
6523
6747
 
6748
+ /** Tells WebGL to create or update the glTexture and start tracking it
6749
+ * @param {TextureInfo} textureInfo
6750
+ * @memberof WebGL */
6751
+ function glRegisterTextureInfo(textureInfo)
6752
+ {
6753
+ if (headlessMode) return;
6754
+
6755
+ // add texture info to tracking list even if gl is not enabled
6756
+ glTextureInfos.add(textureInfo);
6757
+
6758
+ if (!glContext) return;
6759
+
6760
+ // create or set the texture data
6761
+ if (textureInfo.glTexture)
6762
+ glSetTextureData(textureInfo.glTexture, textureInfo.image);
6763
+ else
6764
+ textureInfo.glTexture = glCreateTexture(textureInfo.image);
6765
+ }
6766
+
6767
+ /** Tells WebGL to destroy the glTexture and stop tracking it
6768
+ * @param {TextureInfo} textureInfo
6769
+ * @memberof WebGL */
6770
+ function glUnregisterTextureInfo(textureInfo)
6771
+ {
6772
+ if (headlessMode) return;
6773
+
6774
+ // delete texture info from tracking list even if gl is not enabled
6775
+ glTextureInfos.delete(textureInfo);
6776
+
6777
+ // unset and destroy the texture
6778
+ const glTexture = textureInfo.glTexture;
6779
+ textureInfo.glTexture = undefined;
6780
+ glDeleteTexture(glTexture);
6781
+ }
6782
+
6524
6783
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
6525
6784
  * @memberof WebGL */
6526
6785
  function glFlush()
@@ -6910,24 +7169,25 @@ function glPolyStrip(points)
6910
7169
  return strip;
6911
7170
  }
6912
7171
  /**
6913
- * LittleJS Newgrounds API
7172
+ * LittleJS Newgrounds Plugin
6914
7173
  * - NewgroundsMedal extends Medal with Newgrounds API functionality
6915
- * - Call new NewgroundsPlugin() to setup Newgrounds
7174
+ * - Call new NewgroundsPlugin(app_id) to setup Newgrounds
6916
7175
  * - Uses CryptoJS for encryption if optional cipher is provided
7176
+ * - provides functions to interact with medals scoreboards
6917
7177
  * - Keeps connection alive and logs views
6918
- * - Functions to interact with scoreboards
6919
- * - Functions to unlock medals
7178
+ * @namespace Newgrounds
6920
7179
  */
6921
7180
 
6922
7181
  /** Global Newgrounds object
6923
7182
  * @type {NewgroundsPlugin}
6924
- * @memberof Medal */
7183
+ * @memberof Newgrounds */
6925
7184
  let newgrounds;
6926
7185
 
6927
7186
  ///////////////////////////////////////////////////////////////////////////////
6928
7187
  /**
6929
7188
  * Newgrounds medal auto unlocks in newgrounds API
6930
7189
  * @extends Medal
7190
+ * @memberof Newgrounds
6931
7191
  */
6932
7192
  class NewgroundsMedal extends Medal
6933
7193
  {
@@ -6952,6 +7212,7 @@ class NewgroundsMedal extends Medal
6952
7212
  ///////////////////////////////////////////////////////////////////////////////
6953
7213
  /**
6954
7214
  * Newgrounds API object
7215
+ * @memberof Newgrounds
6955
7216
  */
6956
7217
  class NewgroundsPlugin
6957
7218
  {
@@ -7087,99 +7348,119 @@ class NewgroundsPlugin
7087
7348
  /**
7088
7349
  * LittleJS Post Processing Plugin
7089
7350
  * - Supports shadertoy style post processing shaders
7090
- * - call new new PostProcessPlugin() to setup post processing
7351
+ * - call new PostProcessPlugin() to setup post processing
7091
7352
  * - can be enabled to pass other canvases through a final shader
7353
+ * @namespace PostProcess
7092
7354
  */
7093
7355
 
7094
7356
  ///////////////////////////////////////////////////////////////////////////////
7095
7357
 
7096
7358
  /** Global Post Process plugin object
7097
- * @type {PostProcessPlugin} */
7359
+ * @type {PostProcessPlugin}
7360
+ * @memberof PostProcess */
7098
7361
  let postProcess;
7099
7362
 
7100
7363
  /////////////////////////////////////////////////////////////////////////
7101
7364
  /**
7102
7365
  * UI System Global Object
7366
+ * @memberof PostProcess
7103
7367
  */
7104
7368
  class PostProcessPlugin
7105
7369
  {
7106
7370
  /** Create global post processing shader
7107
7371
  * @param {string} shaderCode
7108
7372
  * @param {boolean} [includeOverlay]
7373
+ * @param {boolean} [includeMainCanvas]
7109
7374
  * @example
7110
7375
  * // create the post process plugin object
7111
7376
  * new PostProcessPlugin(shaderCode);
7112
7377
  */
7113
- constructor(shaderCode, includeOverlay=false)
7378
+ constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
7114
7379
  {
7115
7380
  ASSERT(!postProcess, 'Post process already initialized');
7116
7381
  postProcess = this;
7117
7382
 
7118
- if (headlessMode) return;
7119
-
7120
- if (!glEnable)
7121
- {
7122
- console.warn('PostProcessPlugin: WebGL not enabled!');
7123
- return;
7124
- }
7125
-
7126
7383
  if (!shaderCode) // default shader pass through
7127
7384
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
7128
7385
 
7129
7386
  /** @property {WebGLProgram} - Shader for post processing */
7130
- this.shader = glCreateProgram(
7131
- '#version 300 es\n' + // specify GLSL ES version
7132
- 'precision highp float;'+ // use highp for better accuracy
7133
- 'in vec2 p;'+ // position
7134
- 'void main(){'+ // shader entry point
7135
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7136
- '}' // end of shader
7137
- ,
7138
- '#version 300 es\n' + // specify GLSL ES version
7139
- 'precision highp float;'+ // use highp for better accuracy
7140
- 'uniform sampler2D iChannel0;'+ // input texture
7141
- 'uniform vec3 iResolution;'+ // size of output texture
7142
- 'uniform float iTime;'+ // time
7143
- 'out vec4 c;'+ // out color
7144
- '\n' + shaderCode + '\n'+ // insert custom shader code
7145
- 'void main(){'+ // shader entry point
7146
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
7147
- 'c.a=1.;'+ // always use full alpha
7148
- '}' // end of shader
7149
- );
7387
+ this.shader = undefined;
7150
7388
 
7151
7389
  /** @property {WebGLTexture} - Texture for post processing */
7152
- this.texture = glCreateTexture();
7390
+ this.texture = undefined;
7153
7391
 
7154
- /** @property {boolean} - Should overlay canvas be included in post processing */
7155
- this.includeOverlay = includeOverlay;
7392
+ // setup the post processing plugin
7393
+ initPostProcess();
7394
+ engineAddPlugin(undefined, postProcessRender, postProcessContextLost, postProcessContextRestored);
7156
7395
 
7157
- // Render the post processing shader, called automatically by the engine
7158
- engineAddPlugin(undefined, postProcessRender);
7159
- function postProcessRender()
7396
+ function initPostProcess()
7160
7397
  {
7161
7398
  if (headlessMode) return;
7162
-
7163
- // prepare to render post process shader
7164
- if (glEnable)
7165
- {
7166
- glFlush(); // clear out the buffer
7167
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
7168
- }
7169
- else
7399
+
7400
+ if (!glEnable)
7170
7401
  {
7171
- // set the viewport
7172
- glContext.viewport(0, 0, glCanvas.width = drawCanvas.width, glCanvas.height = drawCanvas.height);
7402
+ console.warn('PostProcessPlugin: WebGL not enabled!');
7403
+ return;
7173
7404
  }
7174
7405
 
7175
- if (postProcess.includeOverlay)
7406
+ // create resources
7407
+ postProcess.texture = glCreateTexture();
7408
+ postProcess.shader = glCreateProgram(
7409
+ '#version 300 es\n' + // specify GLSL ES version
7410
+ 'precision highp float;'+ // use highp for better accuracy
7411
+ 'in vec2 p;'+ // position
7412
+ 'void main(){'+ // shader entry point
7413
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7414
+ '}' // end of shader
7415
+ ,
7416
+ '#version 300 es\n' + // specify GLSL ES version
7417
+ 'precision highp float;'+ // use highp for better accuracy
7418
+ 'uniform sampler2D iChannel0;'+ // input texture
7419
+ 'uniform vec3 iResolution;'+ // size of output texture
7420
+ 'uniform float iTime;'+ // time
7421
+ 'out vec4 c;'+ // out color
7422
+ '\n' + shaderCode + '\n'+ // insert custom shader code
7423
+ 'void main(){'+ // shader entry point
7424
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
7425
+ 'c.a=1.;'+ // always use full alpha
7426
+ '}' // end of shader
7427
+ );
7428
+ }
7429
+ function postProcessContextLost()
7430
+ {
7431
+ postProcess.shader = undefined;
7432
+ postProcess.texture = undefined;
7433
+ LOG('PostProcessPlugin: WebGL context lost');
7434
+ }
7435
+ function postProcessContextRestored()
7436
+ {
7437
+ initPostProcess();
7438
+ LOG('PostProcessPlugin: WebGL context restored');
7439
+ }
7440
+ function postProcessRender()
7441
+ {
7442
+ if (headlessMode) return;
7443
+
7444
+ if (!glEnable)
7445
+ return;
7446
+
7447
+ // clear out the buffer
7448
+ glFlush();
7449
+
7450
+ if (includeMainCanvas || includeOverlay)
7176
7451
  {
7177
- // copy overlay canvas so it will be included in post processing
7178
- mainContext.drawImage(overlayCanvas, 0, 0);
7179
- overlayCanvas.width |= 0;
7452
+ // copy WebGL to the main canvas
7453
+ mainContext.drawImage(glCanvas, 0, 0);
7454
+
7455
+ if (includeOverlay)
7456
+ {
7457
+ // copy overlay canvas so it will be included in post processing
7458
+ mainContext.drawImage(overlayCanvas, 0, 0);
7459
+ overlayCanvas.width |= 0; // clear overlay canvas
7460
+ }
7180
7461
  }
7181
7462
 
7182
- // setup shader program to draw one triangle
7463
+ // setup shader program to draw a quad
7183
7464
  glContext.useProgram(postProcess.shader);
7184
7465
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7185
7466
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
@@ -7188,7 +7469,10 @@ class PostProcessPlugin
7188
7469
  // set textures, pass in the 2d canvas and gl canvas in separate texture channels
7189
7470
  glContext.activeTexture(glContext.TEXTURE0);
7190
7471
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
7191
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
7472
+ if (includeMainCanvas || includeOverlay)
7473
+ {
7474
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
7475
+ }
7192
7476
 
7193
7477
  // set vertex position attribute
7194
7478
  const vertexByteStride = 8;
@@ -7207,12 +7491,15 @@ class PostProcessPlugin
7207
7491
  }
7208
7492
  /**
7209
7493
  * LittleJS ZzFXM Plugin
7494
+ * @namespace ZzFXM
7210
7495
  */
7211
7496
 
7212
7497
  /**
7213
7498
  * Music Object - Stores a zzfx music track for later use
7214
7499
  *
7215
7500
  * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
7501
+ * @extends Sound
7502
+ * @memberof ZzFXM
7216
7503
  * @example
7217
7504
  * // create some music
7218
7505
  * const music_example = new Music(
@@ -7271,7 +7558,8 @@ class ZzFXMusic extends Sound
7271
7558
  * @param {Array} patterns - Array of pattern data
7272
7559
  * @param {Array} sequence - Array of pattern indexes
7273
7560
  * @param {number} [BPM] - Playback speed of the song in BPM
7274
- * @return {Array} - Left and right channel sample data */
7561
+ * @return {Array} - Left and right channel sample data
7562
+ * @memberof ZzFXM */
7275
7563
  function zzfxM(instruments, patterns, sequence, BPM = 125)
7276
7564
  {
7277
7565
  let i, j, k;
@@ -7374,17 +7662,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
7374
7662
  * - Buttons
7375
7663
  * - Checkboxes
7376
7664
  * - Images
7665
+ * @namespace UISystem
7377
7666
  */
7378
7667
 
7379
7668
  ///////////////////////////////////////////////////////////////////////////////
7380
7669
 
7381
7670
  /** Global UI system plugin object
7382
- * @type {UISystemPlugin} */
7671
+ * @type {UISystemPlugin}
7672
+ * @memberof UISystem */
7383
7673
  let uiSystem;
7384
7674
 
7385
7675
  ///////////////////////////////////////////////////////////////////////////////
7386
7676
  /**
7387
7677
  * UI System Global Object
7678
+ * @memberof UISystem
7388
7679
  */
7389
7680
  class UISystemPlugin
7390
7681
  {
@@ -7411,6 +7702,8 @@ class UISystemPlugin
7411
7702
  this.defaultHoverColor = hsl(0,0,.9);
7412
7703
  /** @property {Color} - Default color for disabled UI elements */
7413
7704
  this.defaultDisabledColor = hsl(0,0,.3);
7705
+ /** @property {Color} - Uses a gradient fill combined with color */
7706
+ this.defaultGradientColor = undefined;
7414
7707
  /** @property {number} - Default line width for UI elements */
7415
7708
  this.defaultLineWidth = 4;
7416
7709
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -7418,7 +7711,7 @@ class UISystemPlugin
7418
7711
  /** @property {number} - Default scale to use for fitting text to object */
7419
7712
  this.defaultTextScale = .8;
7420
7713
  /** @property {string} - Default font for UI elements */
7421
- this.defaultFont = 'arial';
7714
+ this.defaultFont = fontDefault;
7422
7715
  /** @property {Sound} - Default sound when interactive UI element is pressed */
7423
7716
  this.defaultSoundPress = undefined;
7424
7717
  /** @property {Sound} - Default sound when interactive UI element is released */
@@ -7429,11 +7722,15 @@ class UISystemPlugin
7429
7722
  this.uiObjects = [];
7430
7723
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
7431
7724
  this.uiContext = context;
7432
- /** @property {UIObject} - Top most object user is over */
7433
- this.hoverObject = undefined;
7434
7725
  /** @property {UIObject} - Object user is currently interacting with */
7435
7726
  this.activeObject = undefined;
7436
-
7727
+ /** @property {UIObject} - Top most object user is over */
7728
+ this.hoverObject = undefined;
7729
+ /** @property {UIObject} - Hover object at start of update */
7730
+ this.lastHoverObject = undefined;
7731
+ /** @property {number} - If set ui coords will be renormalized to this canvas height */
7732
+ this.nativeHeight = 0;
7733
+
7437
7734
  engineAddPlugin(uiUpdate, uiRender);
7438
7735
 
7439
7736
  // setup recursive update and render
@@ -7463,6 +7760,7 @@ class UISystemPlugin
7463
7760
  updateInvisibleObject(o);
7464
7761
  }
7465
7762
  // reset hover object at start of update
7763
+ uiSystem.lastHoverObject = uiSystem.hoverObject;
7466
7764
  uiSystem.hoverObject = undefined;
7467
7765
  for (let i = uiSystem.uiObjects.length; i--;)
7468
7766
  {
@@ -7472,6 +7770,17 @@ class UISystemPlugin
7472
7770
  }
7473
7771
  function uiRender()
7474
7772
  {
7773
+ const context = uiSystem.uiContext;
7774
+ context.save();
7775
+ if (uiSystem.nativeHeight)
7776
+ {
7777
+ // convert to native height
7778
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
7779
+ context.translate(-s*mainCanvasSize.x/2,0);
7780
+ context.scale(s,s);
7781
+ context.translate(mainCanvasSize.x/2/s,0);
7782
+ }
7783
+
7475
7784
  function renderObject(o)
7476
7785
  {
7477
7786
  if (!o.visible)
@@ -7483,6 +7792,7 @@ class UISystemPlugin
7483
7792
  renderObject(c);
7484
7793
  }
7485
7794
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
7795
+ context.restore();
7486
7796
  }
7487
7797
  }
7488
7798
 
@@ -7492,11 +7802,30 @@ class UISystemPlugin
7492
7802
  * @param {Color} [color=uiSystem.defaultColor]
7493
7803
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
7494
7804
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7495
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
7496
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
7497
- {
7805
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
7806
+ * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
7807
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
7808
+ {
7809
+ ASSERT(isVector2(pos), 'pos must be a vec2');
7810
+ ASSERT(isVector2(size), 'size must be a vec2');
7811
+ ASSERT(isColor(color), 'color must be a color');
7812
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
7813
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
7814
+ ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
7815
+
7498
7816
  const context = uiSystem.uiContext;
7499
- context.fillStyle = color.toString();
7817
+ if (gradientColor)
7818
+ {
7819
+ const g = context.createLinearGradient(
7820
+ pos.x, pos.y-size.y/2, pos.x, pos.y+size.y/2);
7821
+ const c = color.toString();
7822
+ g.addColorStop(0, c);
7823
+ g.addColorStop(.5, gradientColor.toString());
7824
+ g.addColorStop(1, c);
7825
+ context.fillStyle = g;
7826
+ }
7827
+ else
7828
+ context.fillStyle = color.toString();
7500
7829
  context.beginPath();
7501
7830
  if (cornerRadius && context['roundRect'])
7502
7831
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
@@ -7518,6 +7847,11 @@ class UISystemPlugin
7518
7847
  * @param {Color} [lineColor=uiSystem.defaultLineColor] */
7519
7848
  drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
7520
7849
  {
7850
+ ASSERT(isVector2(posA), 'posA must be a vec2');
7851
+ ASSERT(isVector2(posB), 'posB must be a vec2');
7852
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
7853
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
7854
+
7521
7855
  const context = uiSystem.uiContext;
7522
7856
  context.strokeStyle = lineColor.toString();
7523
7857
  context.lineWidth = lineWidth;
@@ -7548,17 +7882,43 @@ class UISystemPlugin
7548
7882
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7549
7883
  * @param {string} [align]
7550
7884
  * @param {string} [font=uiSystem.defaultFont]
7885
+ * @param {string} [fontStyle]
7551
7886
  * @param {boolean} [applyMaxWidth=true] */
7552
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, applyMaxWidth=true)
7887
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
7888
+ {
7889
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
7890
+ }
7891
+
7892
+ /**
7893
+ * @callback DragAndDropCallback - Callback for drag and drop events
7894
+ * @param {DragEvent} event - The drag event
7895
+ * @memberof UISystem
7896
+ */
7897
+
7898
+ /** Setup drag and drop event handlers
7899
+ * Automatically prevents defaults and calls the given functions
7900
+ * @param {DragAndDropCallback} [onDrop] - when a file is dropped
7901
+ * @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
7902
+ * @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
7903
+ * @param {DragAndDropCallback} [onDragOver] - continously when dragging over */
7904
+ setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
7553
7905
  {
7554
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
7906
+ function setCallback(callback, listenerType)
7907
+ {
7908
+ function listener(e) { e.preventDefault(); callback && callback(e); }
7909
+ document.addEventListener(listenerType, listener);
7910
+ }
7911
+ setCallback(onDrop, 'drop');
7912
+ setCallback(onDragEnter, 'dragenter');
7913
+ setCallback(onDragLeave, 'dragleave');
7914
+ setCallback(onDragOver, 'dragover');
7555
7915
  }
7556
7916
  }
7557
7917
 
7558
7918
  ///////////////////////////////////////////////////////////////////////////////
7559
7919
  /**
7560
7920
  * UI Object - Base level object for all UI elements
7561
- */
7921
+ * @memberof UISystem */
7562
7922
  class UIObject
7563
7923
  {
7564
7924
  /** Create a UIObject
@@ -7567,6 +7927,9 @@ class UIObject
7567
7927
  */
7568
7928
  constructor(pos=vec2(), size=vec2())
7569
7929
  {
7930
+ ASSERT(isVector2(pos), 'ui object pos must be a vec2');
7931
+ ASSERT(isVector2(size), 'ui object size must be a vec2');
7932
+
7570
7933
  /** @property {Vector2} - Local position of the object */
7571
7934
  this.localPos = pos.copy();
7572
7935
  /** @property {Vector2} - Screen space position of the object */
@@ -7574,27 +7937,31 @@ class UIObject
7574
7937
  /** @property {Vector2} - Screen space size of the object */
7575
7938
  this.size = size.copy();
7576
7939
  /** @property {Color} - Color of the object */
7577
- this.color = uiSystem.defaultColor;
7940
+ this.color = uiSystem.defaultColor.copy();
7578
7941
  /** @property {Color} - Color of the object when active, uses color if undefined */
7579
7942
  this.activeColor = undefined;
7580
7943
  /** @property {string} - Text for this ui object */
7581
7944
  this.text = undefined;
7582
7945
  /** @property {Color} - Color when disabled */
7583
- this.disabledColor = uiSystem.defaultDisabledColor;
7946
+ this.disabledColor = uiSystem.defaultDisabledColor.copy();
7584
7947
  /** @property {boolean} - Is this object disabled? */
7585
7948
  this.disabled = false;
7586
7949
  /** @property {Color} - Color for text */
7587
- this.textColor = uiSystem.defaultTextColor;
7950
+ this.textColor = uiSystem.defaultTextColor.copy();
7588
7951
  /** @property {Color} - Color used when hovering over the object */
7589
- this.hoverColor = uiSystem.defaultHoverColor;
7952
+ this.hoverColor = uiSystem.defaultHoverColor.copy();
7590
7953
  /** @property {Color} - Color for line drawing */
7591
- this.lineColor = uiSystem.defaultLineColor;
7954
+ this.lineColor = uiSystem.defaultLineColor.copy();
7955
+ /** @property {Color} - Uses a gradient fill combined with color */
7956
+ this.gradientColor = uiSystem.defaultGradientColor ? uiSystem.defaultGradientColor.copy() : undefined;
7592
7957
  /** @property {number} - Width for line drawing */
7593
7958
  this.lineWidth = uiSystem.defaultLineWidth;
7594
7959
  /** @property {number} - Corner radius for rounded rects */
7595
7960
  this.cornerRadius = uiSystem.defaultCornerRadius;
7596
7961
  /** @property {string} - Font for this objecct */
7597
7962
  this.font = uiSystem.defaultFont;
7963
+ /** @property {string} - Font style for this object or undefined */
7964
+ this.fontStyle = undefined;
7598
7965
  /** @property {number} - Override for text width */
7599
7966
  this.textWidth = undefined;
7600
7967
  /** @property {number} - Override for text height */
@@ -7619,6 +7986,8 @@ class UIObject
7619
7986
  this.interactive = false;
7620
7987
  /** @property {boolean} - Activate when dragged over with mouse held down */
7621
7988
  this.dragActivate = false;
7989
+ /** @property {boolean} - True if this can be a hover object */
7990
+ this.canBeHover = true;
7622
7991
  uiSystem.uiObjects.push(this);
7623
7992
  }
7624
7993
 
@@ -7642,31 +8011,47 @@ class UIObject
7642
8011
  child.parent = undefined;
7643
8012
  }
7644
8013
 
8014
+ /** Check if the mouse is overlapping a box in screen space
8015
+ * @return {boolean} - True if overlapping
8016
+ */
8017
+ isMouseOverlapping()
8018
+ {
8019
+ const size = !isTouchDevice ? this.size :
8020
+ this.size.add(vec2(this.extraTouchSize || 0));
8021
+ if (!uiSystem.nativeHeight)
8022
+ return isOverlapping(this.pos, size, mousePosScreen);
8023
+
8024
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8025
+ const sInv = 1/s;
8026
+ let pos = mousePosScreen.copy();
8027
+ pos.x += s*mainCanvasSize.x/2;
8028
+ pos.x *= sInv;
8029
+ pos.y *= sInv;
8030
+ pos.x -= sInv*mainCanvasSize.x/2;
8031
+ return isOverlapping(this.pos, size, pos);
8032
+ }
8033
+
7645
8034
  /** Update the object, called automatically by plugin once each frame */
7646
8035
  update()
7647
8036
  {
7648
- const wasHover = this.isHoverObject();
8037
+ const wasHover = uiSystem.lastHoverObject === this;
7649
8038
  const isActive = this.isActiveObject();
7650
8039
  const mouseDown = mouseIsDown(0);
7651
8040
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
7652
- if (!uiSystem.hoverObject)
8041
+ if (this.canBeHover)
7653
8042
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
7654
- {
7655
- const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
7656
- if (isOverlapping(this.pos, size, mousePosScreen))
7657
- uiSystem.hoverObject = this;
7658
- }
8043
+ if (!uiSystem.hoverObject && this.isMouseOverlapping())
8044
+ uiSystem.hoverObject = this;
7659
8045
  if (this.isHoverObject())
7660
8046
  {
7661
- if (mousePress)
7662
- inputClearKey(0,0,0,1,0); // clear mouse was pressed state
7663
8047
  if (!this.disabled)
7664
8048
  {
7665
8049
  if (mousePress)
7666
8050
  {
7667
8051
  if (this.interactive)
7668
8052
  {
7669
- this.onPress();
8053
+ if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
8054
+ this.onPress();
7670
8055
  if (this.soundPress)
7671
8056
  this.soundPress.play();
7672
8057
  if (uiSystem.activeObject && !isActive)
@@ -7674,13 +8059,15 @@ class UIObject
7674
8059
  uiSystem.activeObject = this;
7675
8060
  }
7676
8061
  }
7677
- if (!mouseDown && uiSystem.activeObject === this && this.interactive)
8062
+ if (!mouseDown && this.isActiveObject() && this.interactive)
7678
8063
  {
7679
8064
  this.onClick();
7680
8065
  if (this.soundClick)
7681
8066
  this.soundClick.play();
7682
8067
  }
7683
8068
  }
8069
+ // clear mouse was pressed state even when disabled
8070
+ mousePress && inputClearKey(0,0,0,1,0);
7684
8071
  }
7685
8072
  if (isActive)
7686
8073
  if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
@@ -7691,6 +8078,7 @@ class UIObject
7691
8078
  uiSystem.activeObject = undefined;
7692
8079
  }
7693
8080
 
8081
+ // call enter/leave events
7694
8082
  if (this.isHoverObject() !== wasHover)
7695
8083
  this.isHoverObject() ? this.onEnter() : this.onLeave();
7696
8084
  }
@@ -7701,7 +8089,7 @@ class UIObject
7701
8089
  if (!this.size.x || !this.size.y) return;
7702
8090
 
7703
8091
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
7704
- const color = this.interactive ? this.disabled ? this.disabledColor : this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8092
+ const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
7705
8093
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7706
8094
  }
7707
8095
 
@@ -7752,6 +8140,7 @@ class UIObject
7752
8140
  /**
7753
8141
  * UIText - A UI object that displays text
7754
8142
  * @extends UIObject
8143
+ * @memberof UISystem
7755
8144
  */
7756
8145
  class UIText extends UIObject
7757
8146
  {
@@ -7766,6 +8155,10 @@ class UIText extends UIObject
7766
8155
  {
7767
8156
  super(pos, size);
7768
8157
 
8158
+ ASSERT(isString(text), 'ui text must be a string');
8159
+ ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
8160
+ ASSERT(isString(font), 'ui text font must be a string');
8161
+
7769
8162
  // set properties
7770
8163
  this.text = text;
7771
8164
  this.align = align;
@@ -7773,11 +8166,13 @@ class UIText extends UIObject
7773
8166
 
7774
8167
  // make text not outlined by default
7775
8168
  this.lineWidth = 0;
8169
+ // text can not be a hover object by default
8170
+ this.canBeHover = false;
7776
8171
  }
7777
8172
  render()
7778
8173
  {
7779
8174
  const textSize = this.getTextSize();
7780
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
8175
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
7781
8176
  }
7782
8177
  }
7783
8178
 
@@ -7785,6 +8180,7 @@ class UIText extends UIObject
7785
8180
  /**
7786
8181
  * UITile - A UI object that displays a tile image
7787
8182
  * @extends UIObject
8183
+ * @memberof UISystem
7788
8184
  */
7789
8185
  class UITile extends UIObject
7790
8186
  {
@@ -7799,15 +8195,19 @@ class UITile extends UIObject
7799
8195
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
7800
8196
  {
7801
8197
  super(pos, size);
8198
+
8199
+ ASSERT(tileInfo instanceof TileInfo, 'ui tile tileInfo must be a TileInfo');
8200
+ ASSERT(isColor(color), 'ui tile color must be a color');
8201
+ ASSERT(isNumber(angle), 'ui tile angle must be a number');
8202
+
7802
8203
  /** @property {TileInfo} - Tile image to use */
7803
8204
  this.tileInfo = tileInfo;
7804
8205
  /** @property {number} - Angle to rotate in radians */
7805
8206
  this.angle = angle;
7806
8207
  /** @property {boolean} - Should it be mirrored? */
7807
8208
  this.mirror = mirror;
7808
-
7809
8209
  // set properties
7810
- this.color = color;
8210
+ this.color = color.copy();
7811
8211
  }
7812
8212
  render()
7813
8213
  {
@@ -7819,6 +8219,7 @@ class UITile extends UIObject
7819
8219
  /**
7820
8220
  * UIButton - A UI object that acts as a button
7821
8221
  * @extends UIObject
8222
+ * @memberof UISystem
7822
8223
  */
7823
8224
  class UIButton extends UIObject
7824
8225
  {
@@ -7832,9 +8233,12 @@ class UIButton extends UIObject
7832
8233
  {
7833
8234
  super(pos, size);
7834
8235
 
8236
+ ASSERT(isString(text), 'ui button must be a string');
8237
+ ASSERT(isColor(color), 'ui button color must be a color');
8238
+
7835
8239
  // set properties
7836
8240
  this.text = text;
7837
- this.color = color;
8241
+ this.color = color.copy();
7838
8242
  this.interactive = true;
7839
8243
  }
7840
8244
  render()
@@ -7844,7 +8248,7 @@ class UIButton extends UIObject
7844
8248
  // draw the text scaled to fit
7845
8249
  const textSize = this.getTextSize();
7846
8250
  uiSystem.drawText(this.text, this.pos, textSize,
7847
- this.textColor, 0, undefined, this.align, this.font);
8251
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
7848
8252
  }
7849
8253
  }
7850
8254
 
@@ -7852,6 +8256,7 @@ class UIButton extends UIObject
7852
8256
  /**
7853
8257
  * UICheckbox - A UI object that acts as a checkbox
7854
8258
  * @extends UIObject
8259
+ * @memberof UISystem
7855
8260
  */
7856
8261
  class UICheckbox extends UIObject
7857
8262
  {
@@ -7865,12 +8270,15 @@ class UICheckbox extends UIObject
7865
8270
  constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
7866
8271
  {
7867
8272
  super(pos, size);
8273
+
8274
+ ASSERT(isString(text), 'ui checkbox must be a string');
8275
+ ASSERT(isColor(color), 'ui checkbox color must be a color');
8276
+
7868
8277
  /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
7869
8278
  this.checked = checked;
7870
-
7871
8279
  // set properties
7872
8280
  this.text = text;
7873
- this.color = color;
8281
+ this.color = color.copy();
7874
8282
  this.interactive = true;
7875
8283
  }
7876
8284
  onClick()
@@ -7894,7 +8302,7 @@ class UICheckbox extends UIObject
7894
8302
  const textSize = this.getTextSize();
7895
8303
  const pos = this.pos.add(vec2(this.size.x,0));
7896
8304
  uiSystem.drawText(this.text, pos, textSize,
7897
- this.textColor, 0, undefined, 'left', this.font, false);
8305
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
7898
8306
  }
7899
8307
  }
7900
8308
 
@@ -7902,6 +8310,7 @@ class UICheckbox extends UIObject
7902
8310
  /**
7903
8311
  * UIScrollbar - A UI object that acts as a scrollbar
7904
8312
  * @extends UIObject
8313
+ * @memberof UISystem
7905
8314
  */
7906
8315
  class UIScrollbar extends UIObject
7907
8316
  {
@@ -7917,14 +8326,19 @@ class UIScrollbar extends UIObject
7917
8326
  {
7918
8327
  super(pos, size);
7919
8328
 
8329
+ ASSERT(isNumber(value), 'ui scrollbar value must be a number');
8330
+ ASSERT(isString(text), 'ui scrollbar must be a string');
8331
+ ASSERT(isColor(color), 'ui scrollbar color must be a color');
8332
+ ASSERT(isColor(handleColor), 'ui scrollbar handleColor must be a color');
8333
+
7920
8334
  /** @property {number} - Current percentage value of this scrollbar 0-1 */
7921
8335
  this.value = value;
7922
8336
  /** @property {Color} - Color for the handle part of the scrollbar */
7923
- this.handleColor = handleColor;
8337
+ this.handleColor = handleColor.copy();
7924
8338
 
7925
8339
  // set properties
7926
8340
  this.text = text;
7927
- this.color = color;
8341
+ this.color = color.copy();
7928
8342
  this.interactive = true;
7929
8343
  }
7930
8344
  update()
@@ -7932,34 +8346,47 @@ class UIScrollbar extends UIObject
7932
8346
  super.update();
7933
8347
  if (this.isActiveObject() && this.interactive)
7934
8348
  {
8349
+ // handle horizontal or vertical scrollbar
8350
+ const isHorizontal = this.size.x > this.size.y;
8351
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8352
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8353
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8354
+
7935
8355
  // check if value changed
7936
- const handleSize = vec2(this.size.y);
7937
- const handleWidth = this.size.x - handleSize.x;
7938
- const p1 = this.pos.x - handleWidth/2;
7939
- const p2 = this.pos.x + handleWidth/2;
8356
+ const handleWidth = barSize - handleSize;
8357
+ const p1 = centerPos - handleWidth/2;
8358
+ const p2 = centerPos + handleWidth/2;
7940
8359
  const oldValue = this.value;
7941
- this.value = percent(mousePosScreen.x, p1, p2);
8360
+ this.value = isHorizontal ?
8361
+ percent(mousePosScreen.x, p1, p2) :
8362
+ percent(mousePosScreen.y, p2, p1);
7942
8363
  this.value === oldValue || this.onChange();
7943
8364
  }
7944
8365
  }
7945
8366
  render()
7946
8367
  {
7947
8368
  super.render();
7948
-
8369
+
8370
+ // handle horizontal or vertical scrollbar
8371
+ const isHorizontal = this.size.x > this.size.y;
8372
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8373
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8374
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8375
+
7949
8376
  // draw the scrollbar handle
7950
- const handleSize = vec2(this.size.y);
7951
- const handleWidth = this.size.x - handleSize.x;
7952
- const p1 = this.pos.x - handleWidth/2;
7953
- const p2 = this.pos.x + handleWidth/2;
7954
- const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
7955
- const handleColor = this.disabled ? this.disabledColor :
7956
- this.interactive && this.isActiveObject() ? this.color : this.handleColor;
7957
- uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8377
+ const handleWidth = barSize - handleSize;
8378
+ const p1 = centerPos - handleWidth/2;
8379
+ const p2 = centerPos + handleWidth/2;
8380
+ const handlePos = isHorizontal ?
8381
+ vec2(lerp(p1, p2, this.value), this.pos.y) :
8382
+ vec2(this.pos.x, lerp(p2, p1, this.value))
8383
+ const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8384
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
7958
8385
 
7959
8386
  // draw the text scaled to fit on the scrollbar
7960
8387
  const textSize = this.getTextSize();
7961
8388
  uiSystem.drawText(this.text, this.pos, textSize,
7962
- this.textColor, 0, undefined, this.align, this.font);
8389
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
7963
8390
  }
7964
8391
  }
7965
8392
  /**
@@ -8001,6 +8428,7 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
8001
8428
  * - Each object has a Box2D body which can have multiple fixtures and joints
8002
8429
  * - Provides interface for Box2D body and fixture functions
8003
8430
  * @extends EngineObject
8431
+ * @memberof Box2D
8004
8432
  */
8005
8433
  class Box2dObject extends EngineObject
8006
8434
  {
@@ -8012,7 +8440,7 @@ class Box2dObject extends EngineObject
8012
8440
  * @param {Color} [color]
8013
8441
  * @param {number} [bodyType]
8014
8442
  * @param {number} [renderOrder] */
8015
- constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8443
+ constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8016
8444
  {
8017
8445
  super(pos, size, tileInfo, angle, color, renderOrder);
8018
8446
 
@@ -8026,7 +8454,7 @@ class Box2dObject extends EngineObject
8026
8454
  this.lineColor = BLACK;
8027
8455
  }
8028
8456
 
8029
- /** Destroy this object and it's physics body */
8457
+ /** Destroy this object and its physics body */
8030
8458
  destroy()
8031
8459
  {
8032
8460
  // destroy physics body, fixtures, and joints
@@ -8510,6 +8938,7 @@ class Box2dRaycastResult
8510
8938
  * Box2D Joint
8511
8939
  * - Base class for Box2D joints
8512
8940
  * - A joint is used to connect objects together
8941
+ * @memberof Box2D
8513
8942
  */
8514
8943
  class Box2dJoint
8515
8944
  {
@@ -8565,6 +8994,7 @@ class Box2dJoint
8565
8994
  * - This a soft constraint with a max force
8566
8995
  * - This allows the constraint to stretch and without applying huge forces
8567
8996
  * @extends Box2dJoint
8997
+ * @memberof Box2D
8568
8998
  */
8569
8999
  class Box2dTargetJoint extends Box2dJoint
8570
9000
  {
@@ -8614,6 +9044,7 @@ class Box2dTargetJoint extends Box2dJoint
8614
9044
  * - Constrains two points on two objects to remain at a fixed distance
8615
9045
  * - You can view this as a massless, rigid rod
8616
9046
  * @extends Box2dJoint
9047
+ * @memberof Box2D
8617
9048
  */
8618
9049
  class Box2dDistanceJoint extends Box2dJoint
8619
9050
  {
@@ -8677,6 +9108,7 @@ class Box2dDistanceJoint extends Box2dJoint
8677
9108
  * Box2D Pin Joint
8678
9109
  * - Pins two objects together at a point
8679
9110
  * @extends Box2dDistanceJoint
9111
+ * @memberof Box2D
8680
9112
  */
8681
9113
  class Box2dPinJoint extends Box2dDistanceJoint
8682
9114
  {
@@ -8696,6 +9128,7 @@ class Box2dPinJoint extends Box2dDistanceJoint
8696
9128
  * Box2D Rope Joint
8697
9129
  * - Enforces a maximum distance between two points on two objects
8698
9130
  * @extends Box2dJoint
9131
+ * @memberof Box2D
8699
9132
  */
8700
9133
  class Box2dRopeJoint extends Box2dJoint
8701
9134
  {
@@ -8748,6 +9181,7 @@ class Box2dRopeJoint extends Box2dJoint
8748
9181
  * - You can use a motor to drive the relative rotation about the shared point
8749
9182
  * - A maximum motor torque is provided so that infinite forces are not generated
8750
9183
  * @extends Box2dJoint
9184
+ * @memberof Box2D
8751
9185
  */
8752
9186
  class Box2dRevoluteJoint extends Box2dJoint
8753
9187
  {
@@ -8849,6 +9283,7 @@ class Box2dRevoluteJoint extends Box2dJoint
8849
9283
  * - Either joint can be a revolute or prismatic joint
8850
9284
  * - You specify a gear ratio to bind the motions together
8851
9285
  * @extends Box2dJoint
9286
+ * @memberof Box2D
8852
9287
  */
8853
9288
  class Box2dGearJoint extends Box2dJoint
8854
9289
  {
@@ -8897,6 +9332,7 @@ class Box2dGearJoint extends Box2dJoint
8897
9332
  * - You can use a joint limit to restrict the range of motion
8898
9333
  * - You can use a joint motor to drive the motion or to model joint friction
8899
9334
  * @extends Box2dJoint
9335
+ * @memberof Box2D
8900
9336
  */
8901
9337
  class Box2dPrismaticJoint extends Box2dJoint
8902
9338
  {
@@ -9006,6 +9442,7 @@ class Box2dPrismaticJoint extends Box2dJoint
9006
9442
  * - You can use a joint motor to drive the motion or to model joint friction
9007
9443
  * - This joint is designed for vehicle suspensions
9008
9444
  * @extends Box2dJoint
9445
+ * @memberof Box2D
9009
9446
  */
9010
9447
  class Box2dWheelJoint extends Box2dJoint
9011
9448
  {
@@ -9101,6 +9538,7 @@ class Box2dWheelJoint extends Box2dJoint
9101
9538
  * Box2D Weld Joint
9102
9539
  * - Glues two objects together
9103
9540
  * @extends Box2dJoint
9541
+ * @memberof Box2D
9104
9542
  */
9105
9543
  class Box2dWeldJoint extends Box2dJoint
9106
9544
  {
@@ -9159,6 +9597,7 @@ class Box2dWeldJoint extends Box2dJoint
9159
9597
  * - Used to apply top-down friction
9160
9598
  * - Provides 2D translational friction and angular friction
9161
9599
  * @extends Box2dJoint
9600
+ * @memberof Box2D
9162
9601
  */
9163
9602
  class Box2dFrictionJoint extends Box2dJoint
9164
9603
  {
@@ -9213,6 +9652,7 @@ class Box2dFrictionJoint extends Box2dJoint
9213
9652
  * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
9214
9653
  * - The force transmitted is scaled by the ratio
9215
9654
  * @extends Box2dJoint
9655
+ * @memberof Box2D
9216
9656
  */
9217
9657
  class Box2dPulleyJoint extends Box2dJoint
9218
9658
  {
@@ -9280,6 +9720,7 @@ class Box2dPulleyJoint extends Box2dJoint
9280
9720
  * - Controls the relative motion between two objects
9281
9721
  * - Typical usage is to control the movement of a object with respect to the ground
9282
9722
  * @extends Box2dJoint
9723
+ * @memberof Box2D
9283
9724
  */
9284
9725
  class Box2dMotorJoint extends Box2dJoint
9285
9726
  {
@@ -9343,6 +9784,7 @@ class Box2dMotorJoint extends Box2dJoint
9343
9784
  /**
9344
9785
  * Box2D Global Object
9345
9786
  * - Wraps Box2d world and provides global functions
9787
+ * @memberof Box2D
9346
9788
  */
9347
9789
  class Box2dPlugin
9348
9790
  {