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)
@@ -727,10 +764,16 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
727
764
  // Debug helper functions
728
765
 
729
766
  /** Asserts if the expression is false, does nothing in release builds
767
+ * Halts execution if the assert fails and throws an error
730
768
  * @param {boolean} assert
731
769
  * @param {...Object} [output] - error message output
732
770
  * @memberof Debug */
733
- function ASSERT(assert, ...output) { console.assert(assert, ...output); }
771
+ function ASSERT(assert, ...output)
772
+ {
773
+ if (assert) return;
774
+ console.assert(assert, ...output)
775
+ throw new Error('Assert failed!'); // halt execution
776
+ }
734
777
 
735
778
  /** Log to console if debug is enabled, does nothing in release builds
736
779
  * @param {...Object} [output] - message output
@@ -740,64 +783,72 @@ function LOG(...output) { console.log(...output); }
740
783
  /** Draw a debug rectangle in world space
741
784
  * @param {Vector2} pos
742
785
  * @param {Vector2} [size=Vector2()]
743
- * @param {string} [color]
744
- * @param {number} [time]
745
- * @param {number} [angle]
786
+ * @param {Color|string} [color]
787
+ * @param {number} [time]
788
+ * @param {number} [angle]
746
789
  * @param {boolean} [fill]
747
790
  * @memberof Debug */
748
- function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
791
+ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
749
792
  {
750
793
  if (typeof size === 'number')
751
794
  size = vec2(size); // allow passing in floats
752
- ASSERT(typeof color === 'string', 'pass in css color strings');
753
- debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
795
+ if (isColor(color))
796
+ color = color.toString();
797
+ pos = pos.copy();
798
+ size = size.copy();
799
+ const timer = new Timer(time);
800
+ debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill});
754
801
  }
755
802
 
756
803
  /** Draw a debug poly in world space
757
804
  * @param {Vector2} pos
758
805
  * @param {Array<Vector2>} points
759
- * @param {string} [color]
760
- * @param {number} [time]
761
- * @param {number} [angle]
806
+ * @param {Color|string} [color]
807
+ * @param {number} [time]
808
+ * @param {number} [angle]
762
809
  * @param {boolean} [fill]
763
810
  * @memberof Debug */
764
- function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
811
+ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
765
812
  {
766
- ASSERT(typeof color === 'string', 'pass in css color strings');
767
- debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
813
+ if (isColor(color))
814
+ color = color.toString();
815
+ pos = pos.copy();
816
+ points = points.map(p=>p.copy());
817
+ const timer = new Timer(time);
818
+ debugPrimitives.push({pos, points, color, timer, angle, fill});
768
819
  }
769
820
 
770
821
  /** Draw a debug circle in world space
771
822
  * @param {Vector2} pos
772
- * @param {number} [size] - diameter
773
- * @param {string} [color]
774
- * @param {number} [time]
823
+ * @param {number} [size] - diameter
824
+ * @param {Color|string} [color]
825
+ * @param {number} [time]
775
826
  * @param {boolean} [fill]
776
827
  * @memberof Debug */
777
- function debugCircle(pos, size=0, color='#fff', time=0, fill=false)
828
+ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
778
829
  {
779
- ASSERT(typeof color === 'string', 'pass in css color strings');
780
- debugPrimitives.push({pos, size, color, time:new Timer(time), angle:0, fill});
830
+ if (isColor(color))
831
+ color = color.toString();
832
+ pos = pos.copy();
833
+ const timer = new Timer(time);
834
+ debugPrimitives.push({pos, size, color, timer, angle:0, fill});
781
835
  }
782
836
 
783
837
  /** Draw a debug point in world space
784
838
  * @param {Vector2} pos
785
- * @param {string} [color]
786
- * @param {number} [time]
787
- * @param {number} [angle]
839
+ * @param {Color|string} [color]
840
+ * @param {number} [time]
841
+ * @param {number} [angle]
788
842
  * @memberof Debug */
789
843
  function debugPoint(pos, color, time, angle)
790
- {
791
- ASSERT(typeof color === 'string', 'pass in css color strings');
792
- debugRect(pos, undefined, color, time, angle);
793
- }
844
+ { debugRect(pos, undefined, color, time, angle); }
794
845
 
795
846
  /** Draw a debug line in world space
796
847
  * @param {Vector2} posA
797
848
  * @param {Vector2} posB
798
- * @param {string} [color]
799
- * @param {number} [width]
800
- * @param {number} [time]
849
+ * @param {Color|string} [color]
850
+ * @param {number} [width]
851
+ * @param {number} [time]
801
852
  * @memberof Debug */
802
853
  function debugLine(posA, posB, color, width=.1, time)
803
854
  {
@@ -811,7 +862,7 @@ function debugLine(posA, posB, color, width=.1, time)
811
862
  * @param {Vector2} sizeA
812
863
  * @param {Vector2} posB
813
864
  * @param {Vector2} sizeB
814
- * @param {string} [color]
865
+ * @param {Color|string} [color]
815
866
  * @memberof Debug */
816
867
  function debugOverlap(posA, sizeA, posB, sizeB, color)
817
868
  {
@@ -827,18 +878,21 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
827
878
  }
828
879
 
829
880
  /** Draw a debug axis aligned bounding box in world space
830
- * @param {string} text
881
+ * @param {string} text
831
882
  * @param {Vector2} pos
832
- * @param {number} [size]
833
- * @param {string} [color]
834
- * @param {number} [time]
835
- * @param {number} [angle]
836
- * @param {string} [font]
883
+ * @param {number} [size]
884
+ * @param {Color|string} [color]
885
+ * @param {number} [time]
886
+ * @param {number} [angle]
887
+ * @param {string} [font]
837
888
  * @memberof Debug */
838
- function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
889
+ function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
839
890
  {
840
- ASSERT(typeof color === 'string', 'pass in css color strings');
841
- debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
891
+ if (isColor(color))
892
+ color = color.toString();
893
+ pos = pos.copy();
894
+ const timer = new Timer(time);
895
+ debugPrimitives.push({text, pos, size, color, timer, angle, font});
842
896
  }
843
897
 
844
898
  /** Clear all debug primitives in the list
@@ -889,17 +943,30 @@ function debugSaveDataURL(dataURL, filename)
889
943
  downloadLink.click();
890
944
  }
891
945
 
892
- /** Show error as full page of red text
946
+ /** Breaks on all asserts/errors, hides the canvas, and shows message in plain text
947
+ * This is a good function to call at the start of your game to catch all errors
948
+ * In release builds this function has no effect
893
949
  * @memberof Debug */
894
950
  function debugShowErrors()
895
951
  {
896
952
  const showError = (message)=>
897
953
  {
898
954
  // replace entire page with error message
899
- document.body.style.display = '';
900
- document.body.style.backgroundColor = '#111';
901
- document.body.innerHTML = `<pre style=color:#f00;font-size:50px;white-space:pre-wrap>` + message;
955
+ document.body.style = 'background-color:#111;margin:8px';
956
+ document.body.innerHTML = `<pre style=color:#f00;font-size:28px;white-space:pre-wrap>` + message;
902
957
  }
958
+
959
+ const originalAssert = console.assert;
960
+ console.assert = (assertion, ...output)=>
961
+ {
962
+ originalAssert(assertion, ...output);
963
+ if (!assertion)
964
+ {
965
+ const message = output.join(' ');
966
+ const stack = new Error().stack;
967
+ throw 'Assertion failed!\n' + message + '\n' + stack;
968
+ }
969
+ };
903
970
  onunhandledrejection = (event)=>
904
971
  showError(event.reason.stack || event.reason);
905
972
  onerror = (message, source, lineno, colno)=>
@@ -1067,7 +1134,7 @@ function debugRender()
1067
1134
  p.fill && overlayContext.fill();
1068
1135
  overlayContext.stroke();
1069
1136
  }
1070
- else if (p.size === 0 || p.size.x === 0 && p.size.y === 0)
1137
+ else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
1071
1138
  {
1072
1139
  // point
1073
1140
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
@@ -1094,7 +1161,7 @@ function debugRender()
1094
1161
  });
1095
1162
 
1096
1163
  // remove expired primitives
1097
- debugPrimitives = debugPrimitives.filter(r=>r.time<0);
1164
+ debugPrimitives = debugPrimitives.filter(r=>r.timer<0);
1098
1165
  }
1099
1166
 
1100
1167
  if (debugObject)
@@ -1504,6 +1571,8 @@ function formatTime(t)
1504
1571
  async function fetchJSON(url)
1505
1572
  {
1506
1573
  const response = await fetch(url);
1574
+ if (!response.ok)
1575
+ throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
1507
1576
  return response.json();
1508
1577
  }
1509
1578
 
@@ -1514,6 +1583,13 @@ async function fetchJSON(url)
1514
1583
  * @memberof Utilities */
1515
1584
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1516
1585
 
1586
+ /**
1587
+ * Check if object is a valid string or can be converted to one
1588
+ * @param {any} s
1589
+ * @return {boolean}
1590
+ * @memberof Utilities */
1591
+ function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1592
+
1517
1593
  ///////////////////////////////////////////////////////////////////////////////
1518
1594
 
1519
1595
  /** Random global functions
@@ -1576,6 +1652,7 @@ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1576
1652
  /**
1577
1653
  * Seeded random number generator
1578
1654
  * - Can be used to create a deterministic random number sequence
1655
+ * @memberof Engine
1579
1656
  * @example
1580
1657
  * let r = new RandomGenerator(123); // random number generator with seed 123
1581
1658
  * let a = r.float(); // random value between 0 and 1
@@ -1672,6 +1749,7 @@ function ASSERT_VECTOR2_NORMAL(v)
1672
1749
  /**
1673
1750
  * 2D Vector object with vector math library
1674
1751
  * - Functions do not change this so they can be chained together
1752
+ * @memberof Engine
1675
1753
  * @example
1676
1754
  * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
1677
1755
  * let b = new Vector2; // vector with coordinates (0, 0)
@@ -1812,9 +1890,10 @@ class Vector2
1812
1890
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1813
1891
  }
1814
1892
 
1815
- /** Set the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
1893
+ /** Sets this this vector to point in the specified integer direction (0-3), corresponding to multiples of 90 degree rotation
1816
1894
  * @param {number} [direction]
1817
- * @param {number} [length] */
1895
+ * @param {number} [length]
1896
+ * @return {Vector2} */
1818
1897
  setDirection(direction, length=1)
1819
1898
  {
1820
1899
  ASSERT_NUMBER_VALID(direction);
@@ -1822,8 +1901,10 @@ class Vector2
1822
1901
  direction = mod(direction, 4);
1823
1902
  ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
1824
1903
  'Vector2.setDirection() direction must be an integer between 0 and 3.');
1825
- return vec2(direction%2 ? direction-1 ? -length : length : 0,
1826
- direction%2 ? 0 : direction ? -length : length);
1904
+
1905
+ this.x = direction%2 ? direction-1 ? -length : length : 0;
1906
+ this.y = direction%2 ? 0 : direction ? -length : length;
1907
+ return this;
1827
1908
  }
1828
1909
 
1829
1910
  /** Returns the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
@@ -1919,6 +2000,7 @@ function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
1919
2000
 
1920
2001
  /**
1921
2002
  * Color object (red, green, blue, alpha) with some helpful functions
2003
+ * @memberof Engine
1922
2004
  * @example
1923
2005
  * let a = new Color; // white
1924
2006
  * let b = new Color(1, 0, 0); // red
@@ -2096,7 +2178,8 @@ class Color
2096
2178
  * @return {Color} */
2097
2179
  setHex(hex)
2098
2180
  {
2099
- ASSERT(typeof hex === 'string' && hex[0] === '#', 'Color hex code must be a string starting with #');
2181
+ ASSERT(isString(hex), 'Color hex code must be a string');
2182
+ ASSERT(hex[0] === '#', 'Color hex code must start with #');
2100
2183
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
2101
2184
 
2102
2185
  if (hex.length < 6)
@@ -2138,77 +2221,78 @@ class Color
2138
2221
  }
2139
2222
 
2140
2223
  ///////////////////////////////////////////////////////////////////////////////
2141
- // default colors
2224
+ // Default Colors
2142
2225
 
2143
2226
  /** Color - White #ffffff
2144
2227
  * @type {Color}
2145
2228
  * @memberof Utilities */
2146
- const WHITE = rgb();
2229
+ const WHITE = protectEngineConstant(rgb());
2147
2230
 
2148
- /** Color - Clear White #ffffff with 0 alpha
2231
+ /** Color - Clear White #757474ff with 0 alpha
2149
2232
  * @type {Color}
2150
2233
  * @memberof Utilities */
2151
- const CLEAR_WHITE = rgb(1,1,1,0);
2234
+ const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
2152
2235
 
2153
2236
  /** Color - Black #000000
2154
2237
  * @type {Color}
2155
2238
  * @memberof Utilities */
2156
- const BLACK = rgb(0,0,0);
2239
+ const BLACK = protectEngineConstant(rgb(0,0,0));
2157
2240
 
2158
2241
  /** Color - Clear Black #000000 with 0 alpha
2159
2242
  * @type {Color}
2160
2243
  * @memberof Utilities */
2161
- const CLEAR_BLACK = rgb(0,0,0,0);
2244
+ const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
2162
2245
 
2163
2246
  /** Color - Gray #808080
2164
2247
  * @type {Color}
2165
2248
  * @memberof Utilities */
2166
- const GRAY = rgb(.5,.5,.5);
2249
+ const GRAY = protectEngineConstant(rgb(.5,.5,.5));
2167
2250
 
2168
2251
  /** Color - Red #ff0000
2169
2252
  * @type {Color}
2170
2253
  * @memberof Utilities */
2171
- const RED = rgb(1,0,0);
2254
+ const RED = protectEngineConstant(rgb(1,0,0));
2172
2255
 
2173
2256
  /** Color - Orange #ff8000
2174
2257
  * @type {Color}
2175
2258
  * @memberof Utilities */
2176
- const ORANGE = rgb(1,.5,0);
2259
+ const ORANGE = protectEngineConstant(rgb(1,.5,0));
2177
2260
 
2178
2261
  /** Color - Yellow #ffff00
2179
2262
  * @type {Color}
2180
2263
  * @memberof Utilities */
2181
- const YELLOW = rgb(1,1,0);
2264
+ const YELLOW = protectEngineConstant(rgb(1,1,0));
2182
2265
 
2183
2266
  /** Color - Green #00ff00
2184
2267
  * @type {Color}
2185
2268
  * @memberof Utilities */
2186
- const GREEN = rgb(0,1,0);
2269
+ const GREEN = protectEngineConstant(rgb(0,1,0));
2187
2270
 
2188
2271
  /** Color - Cyan #00ffff
2189
2272
  * @type {Color}
2190
2273
  * @memberof Utilities */
2191
- const CYAN = rgb(0,1,1);
2274
+ const CYAN = protectEngineConstant(rgb(0,1,1));
2192
2275
 
2193
2276
  /** Color - Blue #0000ff
2194
2277
  * @type {Color}
2195
2278
  * @memberof Utilities */
2196
- const BLUE = rgb(0,0,1);
2279
+ const BLUE = protectEngineConstant(rgb(0,0,1));
2197
2280
 
2198
2281
  /** Color - Purple #8000ff
2199
2282
  * @type {Color}
2200
2283
  * @memberof Utilities */
2201
- const PURPLE = rgb(.5,0,1);
2284
+ const PURPLE = protectEngineConstant(rgb(.5,0,1));
2202
2285
 
2203
2286
  /** Color - Magenta #ff00ff
2204
2287
  * @type {Color}
2205
2288
  * @memberof Utilities */
2206
- const MAGENTA = rgb(1,0,1);
2289
+ const MAGENTA = protectEngineConstant(rgb(1,0,1));
2207
2290
 
2208
2291
  ///////////////////////////////////////////////////////////////////////////////
2209
2292
 
2210
2293
  /**
2211
2294
  * Timer object tracks how long has passed since it was set
2295
+ * @memberof Engine
2212
2296
  * @example
2213
2297
  * let a = new Timer; // creates a timer that is not set
2214
2298
  * a.set(3); // sets the timer to 3 seconds
@@ -2270,6 +2354,36 @@ class Timer
2270
2354
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2271
2355
  * @return {number} */
2272
2356
  valueOf() { return this.get(); }
2357
+ }
2358
+
2359
+ ///////////////////////////////////////////////////////////////////////////////
2360
+ // Helper functions used by the engine
2361
+
2362
+ // make color constants immutable with debug assertions
2363
+ function protectEngineConstant(obj)
2364
+ {
2365
+ if (debug)
2366
+ {
2367
+ // get properties and store original values
2368
+ const props = Object.keys(obj), values = {};
2369
+ props.forEach(prop => values[prop] = obj[prop]);
2370
+
2371
+ // replace with getters/setters that assert
2372
+ props.forEach(prop =>
2373
+ {
2374
+ Object.defineProperty(obj, prop, {
2375
+ get: () => values[prop],
2376
+ set: (value) =>
2377
+ {
2378
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
2379
+ },
2380
+ enumerable: true
2381
+ });
2382
+ });
2383
+ }
2384
+
2385
+ // freeze the object to prevent adding new properties
2386
+ return Object.freeze(obj);
2273
2387
  }
2274
2388
  /**
2275
2389
  * LittleJS Engine Settings
@@ -2577,7 +2691,7 @@ let medalsPreventUnlock = false;
2577
2691
  /** Set position of camera in world space
2578
2692
  * @param {Vector2} pos
2579
2693
  * @memberof Settings */
2580
- function setCameraPos(pos) { cameraPos = pos; }
2694
+ function setCameraPos(pos) { cameraPos = pos.copy(); }
2581
2695
 
2582
2696
  /** Set angle of camera in world space
2583
2697
  * @param {number} angle
@@ -2600,17 +2714,17 @@ function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2600
2714
  /** Set color to clear the canvas to before render
2601
2715
  * @param {Color} color
2602
2716
  * @memberof Settings */
2603
- function setCanvasClearColor(color) { canvasClearColor = color; }
2717
+ function setCanvasClearColor(color) { canvasClearColor = color.copy(); }
2604
2718
 
2605
2719
  /** Set max size of the canvas
2606
2720
  * @param {Vector2} size
2607
2721
  * @memberof Settings */
2608
- function setCanvasMaxSize(size) { canvasMaxSize = size; }
2722
+ function setCanvasMaxSize(size) { canvasMaxSize = size.copy(); }
2609
2723
 
2610
2724
  /** Set fixed size of the canvas
2611
2725
  * @param {Vector2} size
2612
2726
  * @memberof Settings */
2613
- function setCanvasFixedSize(size) { canvasFixedSize = size; }
2727
+ function setCanvasFixedSize(size) { canvasFixedSize = size.copy(); }
2614
2728
 
2615
2729
  /** Use nearest scaling algorithm for canvas for more pixelated look
2616
2730
  * - If enabled sets css image-rendering:pixelated
@@ -2675,7 +2789,7 @@ function setGLCircleSides(sides) { glCircleSides = sides; }
2675
2789
  /** Set default size of tiles in pixels
2676
2790
  * @param {Vector2} size
2677
2791
  * @memberof Settings */
2678
- function setTileSizeDefault(size) { tileSizeDefault = size; }
2792
+ function setTileSizeDefault(size) { tileSizeDefault = size.copy(); }
2679
2793
 
2680
2794
  /** Set to prevent tile bleeding from neighbors in pixels
2681
2795
  * @param {number} scale
@@ -2720,7 +2834,7 @@ function setObjectMaxSpeed(speed) { objectMaxSpeed = speed; }
2720
2834
  /** Set how much gravity to apply to objects
2721
2835
  * @param {Vector2} newGravity
2722
2836
  * @memberof Settings */
2723
- function setGravity(newGravity) { gravity = newGravity; }
2837
+ function setGravity(newGravity) { gravity = newGravity.copy(); }
2724
2838
 
2725
2839
  /** Set to scales emit rate of particles
2726
2840
  * @param {number} scale
@@ -2810,7 +2924,7 @@ function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
2810
2924
  /** Set size of medal display
2811
2925
  * @param {Vector2} size
2812
2926
  * @memberof Settings */
2813
- function setMedalDisplaySize(size) { medalDisplaySize = size; }
2927
+ function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
2814
2928
 
2815
2929
  /** Set to stop medals from being unlockable
2816
2930
  * @param {boolean} preventUnlock
@@ -2850,6 +2964,7 @@ function setDebugKey(key) { debugKey = key; }
2850
2964
  * - Collision for objects can be set to be solid to block other objects
2851
2965
  * - Objects may get pushed into overlapping other solid objects, if so they will push away
2852
2966
  * - Solid objects are more performance intensive and should be used sparingly
2967
+ * @memberof Engine
2853
2968
  * @example
2854
2969
  * // create an engine object, normally you would first extend the class with your own
2855
2970
  * const pos = vec2(2,3);
@@ -2858,18 +2973,18 @@ function setDebugKey(key) { debugKey = key; }
2858
2973
  class EngineObject
2859
2974
  {
2860
2975
  /** Create an engine object and adds it to the list of objects
2861
- * @param {Vector2} [pos=(0,0)] - World space position of the object
2862
- * @param {Vector2} [size=(1,1)] - World space size of the object
2863
- * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2864
- * @param {number} [angle] - Angle the object is rotated by
2865
- * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
2866
- * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2976
+ * @param {Vector2} [pos=(0,0)] - World space position of the object
2977
+ * @param {Vector2} [size=(1,1)] - World space size of the object
2978
+ * @param {TileInfo} [tileInfo] - Tile info to render object (undefined is untextured)
2979
+ * @param {number} [angle] - Angle the object is rotated by
2980
+ * @param {Color} [color=WHITE] - Color to apply to tile when rendered
2981
+ * @param {number} [renderOrder] - Objects sorted by renderOrder before being rendered
2867
2982
  */
2868
- constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
2983
+ constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=WHITE, renderOrder=0)
2869
2984
  {
2870
2985
  // check passed in params
2871
- ASSERT(isVector2(pos), 'object pos should be a vec2');
2872
- ASSERT(isVector2(size), 'object size should be a vec2');
2986
+ ASSERT(isVector2(pos), 'object pos must be a vec2');
2987
+ ASSERT(isVector2(size), 'object size must be a vec2');
2873
2988
  ASSERT(!tileInfo || tileInfo instanceof TileInfo, 'object tileInfo should be a TileInfo or undefined');
2874
2989
  ASSERT(typeof angle === 'number' && isFinite(angle), 'object angle should be a number');
2875
2990
  ASSERT(isColor(color), 'object color should be a valid rgba color');
@@ -3154,7 +3269,7 @@ class EngineObject
3154
3269
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
3155
3270
  }
3156
3271
 
3157
- /** Destroy this object, destroy its children, detach it's parent, and mark it for removal */
3272
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
3158
3273
  destroy()
3159
3274
  {
3160
3275
  if (this.destroyed)
@@ -3228,6 +3343,8 @@ class EngineObject
3228
3343
  addChild(child, localPos=vec2(), localAngle=0)
3229
3344
  {
3230
3345
  ASSERT(!child.parent && !this.children.includes(child));
3346
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
3347
+ ASSERT(child !== this, 'cannot add self as child');
3231
3348
  this.children.push(child);
3232
3349
  child.parent = this;
3233
3350
  child.localPos = localPos.copy();
@@ -3239,6 +3356,7 @@ class EngineObject
3239
3356
  removeChild(child)
3240
3357
  {
3241
3358
  ASSERT(child.parent === this && this.children.includes(child));
3359
+ ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
3242
3360
  this.children.splice(this.children.indexOf(child), 1);
3243
3361
  child.parent = 0;
3244
3362
  }
@@ -3377,7 +3495,7 @@ let drawCount;
3377
3495
  * Create a tile info object using a grid based system
3378
3496
  * - This can take vecs or floats for easier use and conversion
3379
3497
  * - If an index is passed in, the tile size and index will determine the position
3380
- * @param {Vector2|number} [pos=0] - Index of tile in sheet
3498
+ * @param {Vector2|number} [pos=0] - Position of the tile in pixels, or tile index
3381
3499
  * @param {Vector2|number} [size=tileSizeDefault] - Size of tile in pixels
3382
3500
  * @param {number} [textureIndex] - Texture index to use
3383
3501
  * @param {number} [padding] - How many pixels padding around tiles
@@ -3422,6 +3540,7 @@ function tile(pos=new Vector2, size=tileSizeDefault, textureIndex=0, padding=0)
3422
3540
 
3423
3541
  /**
3424
3542
  * Tile Info - Stores info about how to draw a tile
3543
+ * @memberof Draw
3425
3544
  */
3426
3545
  class TileInfo
3427
3546
  {
@@ -3466,48 +3585,54 @@ class TileInfo
3466
3585
  }
3467
3586
 
3468
3587
  /**
3469
- * Set this tile to use a full image
3470
- * @param {HTMLImageElement|OffscreenCanvas} image
3471
- * @param {WebGLTexture} [glTexture] - WebGL texture
3588
+ * Set this tile to use a full image in a texture info
3589
+ * @param {TextureInfo} textureInfo
3472
3590
  * @return {TileInfo}
3473
3591
  */
3474
- setFullImage(image, glTexture)
3592
+ setFullImage(textureInfo)
3475
3593
  {
3476
3594
  this.pos = new Vector2;
3477
- this.size = new Vector2(image.width, image.height);
3478
- this.textureInfo = new TextureInfo(image, glTexture);
3595
+ this.size = textureInfo.size.copy();
3596
+ this.textureInfo = textureInfo;
3479
3597
  // do not use padding or bleed
3480
3598
  this.bleedScale = this.padding = 0;
3481
3599
  return this;
3482
3600
  }
3483
3601
  }
3484
3602
 
3485
- /** Texture Info - Stores info about each texture */
3603
+ /**
3604
+ * Tile Info - Stores info about each texture
3605
+ * @memberof Draw
3606
+ */
3486
3607
  class TextureInfo
3487
3608
  {
3488
3609
  /**
3489
3610
  * Create a TextureInfo, called automatically by the engine
3490
3611
  * @param {HTMLImageElement|OffscreenCanvas} image
3491
- * @param {WebGLTexture} [glTexture] - WebGL texture
3612
+ * @param {boolean} [useWebGL] - Should use WebGL if available?
3492
3613
  */
3493
- constructor(image, glTexture)
3614
+ constructor(image, useWebGL=true)
3494
3615
  {
3495
- /** @property {HTMLImageElement} - image source */
3616
+ /** @property {HTMLImageElement|OffscreenCanvas} - image source */
3496
3617
  this.image = image;
3497
3618
  /** @property {Vector2} - size of the image */
3498
- this.size = vec2(image.width, image.height);
3619
+ this.size = image ? vec2(image.width, image.height) : vec2();
3499
3620
  /** @property {Vector2} - inverse of the size, cached for rendering */
3500
- this.sizeInverse = vec2(1/image.width, 1/image.height);
3621
+ this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
3501
3622
  /** @property {WebGLTexture} - WebGL texture */
3502
- this.glTexture = glTexture;
3623
+ this.glTexture = undefined;
3624
+ useWebGL && this.createWebGLTexture();
3503
3625
  }
3504
3626
 
3505
- createWebGLTexture()
3506
- {
3507
- ASSERT(!this.glTexture);
3508
- if (glEnable)
3509
- this.glTexture = glCreateTexture(this.image);
3510
- }
3627
+ /** Creates the WebGL texture, updates if already created */
3628
+ createWebGLTexture() { glRegisterTextureInfo(this); }
3629
+
3630
+ /** Destroys the WebGL texture */
3631
+ destroyWebGLTexture() { glUnregisterTextureInfo(this); }
3632
+
3633
+ /** Check if the texture is webgl enabled
3634
+ * @return {boolean} */
3635
+ hasWebGL() { return !!this.glTexture; }
3511
3636
  }
3512
3637
 
3513
3638
  ///////////////////////////////////////////////////////////////////////////////
@@ -3528,10 +3653,11 @@ class TextureInfo
3528
3653
  function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3529
3654
  angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
3530
3655
  {
3531
- ASSERT(isVector2(pos), 'drawTile pos should be a vec2');
3532
- ASSERT(isVector2(size), 'drawTile size should be a vec2');
3533
- ASSERT(isColor(color) && (!additiveColor || isColor(additiveColor)), 'drawTile color is invalid');
3534
- ASSERT(isNumber(angle), 'drawTile angle should be a number');
3656
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3657
+ ASSERT(isVector2(size), 'size must be a vec2');
3658
+ ASSERT(isColor(color), 'color is invalid');
3659
+ ASSERT(isNumber(angle), 'angle must be a number');
3660
+ ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
3535
3661
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3536
3662
 
3537
3663
  const textureInfo = tileInfo && tileInfo.textureInfo;
@@ -3626,10 +3752,10 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
3626
3752
  * @memberof Draw */
3627
3753
  function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3628
3754
  {
3629
- ASSERT(isVector2(pos), 'drawRectGradient pos should be a vec2');
3630
- ASSERT(isVector2(size), 'drawRectGradient size should be a vec2');
3631
- ASSERT(isColor(colorTop) && isColor(colorBottom), 'drawRectGradient color is invalid');
3632
- ASSERT(isNumber(angle), 'drawRectGradient angle should be a number');
3755
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3756
+ ASSERT(isVector2(size), 'size must be a vec2');
3757
+ ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
3758
+ ASSERT(isNumber(angle), 'angle must be a number');
3633
3759
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3634
3760
  if (useWebGL)
3635
3761
  {
@@ -3687,11 +3813,11 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3687
3813
  * @memberof Draw */
3688
3814
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3689
3815
  {
3690
- ASSERT(Array.isArray(points), 'drawLineList points should be an array');
3691
- ASSERT(isNumber(width), 'drawLineList width should be a number');
3692
- ASSERT(isColor(color), 'drawLineList color is invalid');
3693
- ASSERT(isVector2(pos), 'drawLineList pos should be a vec2');
3694
- ASSERT(isNumber(angle), 'drawLineList angle should be a number');
3816
+ ASSERT(Array.isArray(points), 'points must be an array');
3817
+ ASSERT(isNumber(width), 'width must be a number');
3818
+ ASSERT(isColor(color), 'color is invalid');
3819
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3820
+ ASSERT(isNumber(angle), 'angle must be a number');
3695
3821
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3696
3822
  if (useWebGL)
3697
3823
  {
@@ -3762,8 +3888,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3762
3888
  * @memberof Draw */
3763
3889
  function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, lineColor=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
3764
3890
  {
3765
- ASSERT(isVector2(size), 'drawRegularPoly size should be a vec2');
3766
- ASSERT(isNumber(sides), 'drawRegularPoly sides should be a number');
3891
+ ASSERT(isVector2(size), 'size must be a vec2');
3892
+ ASSERT(isNumber(sides), 'sides must be a number');
3767
3893
 
3768
3894
  // build regular polygon points
3769
3895
  const points = [];
@@ -3789,12 +3915,13 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3789
3915
  * @memberof Draw */
3790
3916
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3791
3917
  {
3792
- ASSERT(isVector2(pos), 'drawPoly pos should be a vec2');
3793
- ASSERT(Array.isArray(points), 'drawPoly points should be an array');
3794
- ASSERT(isColor(color) && isColor(lineColor), 'drawPoly color is invalid');
3795
- ASSERT(isNumber(lineWidth), 'drawPoly lineWidth should be a number');
3796
- ASSERT(isNumber(angle), 'drawPoly angle should be a number');
3918
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3919
+ ASSERT(Array.isArray(points), 'points must be an array');
3920
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3921
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3922
+ ASSERT(isNumber(angle), 'angle must be a number');
3797
3923
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3924
+
3798
3925
  if (useWebGL)
3799
3926
  {
3800
3927
  let scale = 1;
@@ -3841,13 +3968,14 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3841
3968
  * @memberof Draw */
3842
3969
  function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3843
3970
  {
3844
- ASSERT(isVector2(pos), 'drawEllipse pos should be a vec2');
3845
- ASSERT(isVector2(size), 'drawEllipse size should be a vec2');
3846
- ASSERT(isColor(color) && isColor(lineColor), 'drawEllipse color is invalid');
3847
- ASSERT(isNumber(angle), 'drawEllipse angle should be a number');
3848
- ASSERT(isNumber(lineWidth), 'drawEllipse lineWidth should be a number');
3849
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'drawEllipse invalid lineWidth');
3971
+ ASSERT(isVector2(pos), 'pos must be a vec2');
3972
+ ASSERT(isVector2(size), 'size must be a vec2');
3973
+ ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3974
+ ASSERT(isNumber(angle), 'angle must be a number');
3975
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3976
+ ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
3850
3977
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3978
+
3851
3979
  if (useWebGL)
3852
3980
  {
3853
3981
  // draw as a regular polygon
@@ -3884,21 +4012,32 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3884
4012
  * @memberof Draw */
3885
4013
  function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
3886
4014
  {
3887
- ASSERT(isNumber(size), 'drawCircle size should be a number');
4015
+ ASSERT(isNumber(size), 'size must be a number');
3888
4016
  drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
3889
4017
  }
3890
4018
 
4019
+ /**
4020
+ * @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
4021
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
4022
+ * @memberof Draw
4023
+ */
4024
+
3891
4025
  /** Draw directly to a 2d canvas context in world space
3892
4026
  * @param {Vector2} pos
3893
4027
  * @param {Vector2} size
3894
4028
  * @param {number} angle
3895
4029
  * @param {boolean} [mirror]
3896
- * @param {Function} [drawFunction]
4030
+ * @param {Canvas2DDrawFunction} [drawFunction]
3897
4031
  * @param {boolean} [screenSpace=false]
3898
4032
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3899
4033
  * @memberof Draw */
3900
4034
  function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpace=false, context=drawContext)
3901
4035
  {
4036
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4037
+ ASSERT(isVector2(size), 'size must be a vec2');
4038
+ ASSERT(isNumber(angle), 'angle must be a number');
4039
+ ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
4040
+
3902
4041
  if (!screenSpace)
3903
4042
  {
3904
4043
  // transform from world space to screen space
@@ -3926,12 +4065,13 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3926
4065
  * @param {Color} [lineColor=(0,0,0,1)]
3927
4066
  * @param {CanvasTextAlign} [textAlign='center']
3928
4067
  * @param {string} [font=fontDefault]
4068
+ * @param {string} [fontStyle]
3929
4069
  * @param {number} [maxWidth]
3930
4070
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3931
4071
  * @memberof Draw */
3932
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth, context=drawContext)
4072
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
3933
4073
  {
3934
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, maxWidth, context);
4074
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
3935
4075
  }
3936
4076
 
3937
4077
  /** Draw text on overlay canvas in world space
@@ -3944,11 +4084,12 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3944
4084
  * @param {Color} [lineColor=(0,0,0,1)]
3945
4085
  * @param {CanvasTextAlign} [textAlign='center']
3946
4086
  * @param {string} [font=fontDefault]
4087
+ * @param {string} [fontStyle]
3947
4088
  * @param {number} [maxWidth]
3948
4089
  * @memberof Draw */
3949
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth)
4090
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
3950
4091
  {
3951
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, maxWidth, overlayContext);
4092
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
3952
4093
  }
3953
4094
 
3954
4095
  /** Draw text on overlay canvas in screen space
@@ -3961,16 +4102,27 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3961
4102
  * @param {Color} [lineColor=(0,0,0,1)]
3962
4103
  * @param {CanvasTextAlign} [textAlign]
3963
4104
  * @param {string} [font=fontDefault]
4105
+ * @param {string} [fontStyle]
3964
4106
  * @param {number} [maxWidth]
3965
4107
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3966
4108
  * @memberof Draw */
3967
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3968
- {
4109
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
4110
+ {
4111
+ ASSERT(isString(text), 'text must be a string');
4112
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4113
+ ASSERT(isNumber(size), 'size must be a number');
4114
+ ASSERT(isColor(color), 'color must be a color');
4115
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4116
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
4117
+ ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
4118
+ ASSERT(isString(font), 'font must be a string');
4119
+ ASSERT(isString(fontStyle), 'fontStyle must be a string');
4120
+
3969
4121
  context.fillStyle = color.toString();
3970
4122
  context.strokeStyle = lineColor.toString();
3971
4123
  context.lineWidth = lineWidth;
3972
4124
  context.textAlign = textAlign;
3973
- context.font = size + 'px '+ font;
4125
+ context.font = fontStyle + ' ' + size + 'px '+ font;
3974
4126
  context.textBaseline = 'middle';
3975
4127
 
3976
4128
  const lines = (text+'').split('\n');
@@ -4167,6 +4319,7 @@ let engineFontImage;
4167
4319
  * - 96 characters (from space to tilde) are stored in an image
4168
4320
  * - Uses a default 8x8 font if none is supplied
4169
4321
  * - You can also use fonts from the main tile sheet
4322
+ * @memberof Draw
4170
4323
  * @example
4171
4324
  * // use built in font
4172
4325
  * const font = new FontImage;
@@ -4177,11 +4330,11 @@ let engineFontImage;
4177
4330
  class FontImage
4178
4331
  {
4179
4332
  /** Create an image font
4180
- * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
4181
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
4182
- * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
4333
+ * @param {HTMLImageElement} [image] - Image for the font, default if undefined
4334
+ * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
4335
+ * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
4183
4336
  */
4184
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
4337
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4185
4338
  {
4186
4339
  // load default font image
4187
4340
  if (!engineFontImage)
@@ -4223,7 +4376,7 @@ class FontImage
4223
4376
  * @param {boolean} [center]
4224
4377
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4225
4378
  */
4226
- drawTextScreen(text, pos, scale=4, center, context=overlayContext)
4379
+ drawTextScreen(text, pos, scale=4, center=true, context=overlayContext)
4227
4380
  {
4228
4381
  context.save();
4229
4382
  const size = this.tileSize;
@@ -4917,6 +5070,7 @@ function audioInit()
4917
5070
  * Sound Object - Stores a sound for later use and can be played positionally
4918
5071
  *
4919
5072
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
5073
+ * @memberof Audio
4920
5074
  * @example
4921
5075
  * // create a sound
4922
5076
  * const sound_example = new Sound([.5,.5]);
@@ -5011,12 +5165,12 @@ class Sound
5011
5165
 
5012
5166
  /** Play the sound as a musical note with a semitone offset
5013
5167
  * This can be used to play music with chromatic scales
5014
- * @param {number} semitoneOffset - How many semitones to offset pitch
5168
+ * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
5015
5169
  * @param {Vector2} [pos] - World space position to play the sound if any
5016
5170
  * @param {number} [volume=1] - How much to scale volume by
5017
5171
  * @return {SoundInstance} - The audio source node
5018
5172
  */
5019
- playNote(semitoneOffset, pos, volume)
5173
+ playNote(semitoneOffset=0, pos, volume)
5020
5174
  {
5021
5175
  const pitch = getNoteFrequency(semitoneOffset, 1);
5022
5176
  return this.play(pos, volume, pitch, 0);
@@ -5039,6 +5193,8 @@ class Sound
5039
5193
  /**
5040
5194
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
5041
5195
  * - this can be used to play wave, mp3, and ogg files
5196
+ * @extends Sound
5197
+ * @memberof Audio
5042
5198
  * @example
5043
5199
  * // create a sound
5044
5200
  * const sound_example = new SoundWave('sound.mp3');
@@ -5048,22 +5204,29 @@ class Sound
5048
5204
  */
5049
5205
  class SoundWave extends Sound
5050
5206
  {
5207
+ /**
5208
+ * @callback SoundLoadCallback - Function called when sound is loaded
5209
+ * @param {SoundWave} sound
5210
+ * @memberof Audio
5211
+ */
5212
+
5051
5213
  /** Create a sound object and cache the wave file for later use
5052
5214
  * @param {string} filename - Filename of audio file to load
5053
5215
  * @param {number} [randomness] - How much to randomize frequency each time sound plays
5054
5216
  * @param {number} [range=soundDefaultRange] - World space max range of sound
5055
5217
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5056
- * @param {Function} [onloadCallback] - callback function to call when sound is loaded
5218
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5057
5219
  */
5058
5220
  constructor(filename, randomness=0, range, taper, onloadCallback)
5059
5221
  {
5060
5222
  super(undefined, range, taper);
5061
5223
  if (!soundEnable || headlessMode) return;
5224
+ ASSERT(!filename || isString(filename), 'filename must be a string');
5062
5225
 
5063
- /** @property {Function} - callback function to call when sound is loaded */
5226
+ /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5064
5227
  this.onloadCallback = onloadCallback;
5065
5228
  this.randomness = randomness;
5066
- this.loadSound(filename);
5229
+ filename && this.loadSound(filename);
5067
5230
  }
5068
5231
 
5069
5232
  /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
@@ -5072,6 +5235,8 @@ class SoundWave extends Sound
5072
5235
  async loadSound(filename)
5073
5236
  {
5074
5237
  const response = await fetch(filename);
5238
+ if (!response.ok)
5239
+ throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);
5075
5240
  const arrayBuffer = await response.arrayBuffer();
5076
5241
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
5077
5242
 
@@ -5107,7 +5272,7 @@ class SoundWave extends Sound
5107
5272
  this.sampleChannels = sampleChannels;
5108
5273
  this.loadedPercent = 1;
5109
5274
  if (this.onloadCallback)
5110
- this.onloadCallback();
5275
+ this.onloadCallback(this);
5111
5276
  }
5112
5277
  }
5113
5278
 
@@ -5116,6 +5281,7 @@ class SoundWave extends Sound
5116
5281
  /**
5117
5282
  * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
5118
5283
  * Represents a single playing instance of a sound with pause/resume capabilities
5284
+ * @memberof Audio
5119
5285
  * @example
5120
5286
  * // Play a sound and get an instance for control
5121
5287
  * const jumpSound = new Sound([.5,.5,220]);
@@ -5181,8 +5347,16 @@ class SoundInstance
5181
5347
  this.stop();
5182
5348
  this.gainNode = audioContext.createGain();
5183
5349
  this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
5184
- this.startTime = audioContext.currentTime - offset;
5185
- this.pausedTime = undefined;
5350
+ if (this.source)
5351
+ {
5352
+ this.startTime = audioContext.currentTime - offset;
5353
+ this.pausedTime = undefined;
5354
+ }
5355
+ else
5356
+ {
5357
+ this.startTime = undefined;
5358
+ this.pausedTime = 0;
5359
+ }
5186
5360
  }
5187
5361
 
5188
5362
  /** Set the volume of this sound instance
@@ -5315,6 +5489,12 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
5315
5489
 
5316
5490
  ///////////////////////////////////////////////////////////////////////////////
5317
5491
 
5492
+ /**
5493
+ * @callback AudioEndedCallback - Function called when a sound ends
5494
+ * @param {AudioBufferSourceNode} source
5495
+ * @memberof Audio
5496
+ */
5497
+
5318
5498
  /** Play cached audio samples with given settings
5319
5499
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
5320
5500
  * @param {number} [volume] - How much to scale volume by
@@ -5324,13 +5504,20 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
5324
5504
  * @param {number} [sampleRate=44100] - Sample rate for the sound
5325
5505
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
5326
5506
  * @param {number} [offset] - Offset in seconds to start playback from
5327
- * @param {Function} [onended] - Callback for when the sound ends
5328
- * @return {AudioBufferSourceNode} - The audio node of the sound played
5507
+ * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
5508
+ * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
5329
5509
  * @memberof Audio */
5330
5510
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
5331
5511
  {
5332
5512
  if (!soundEnable || headlessMode) return;
5333
5513
 
5514
+ if (!audioIsRunning())
5515
+ {
5516
+ // fix stalled audio, this sound won't be able to play
5517
+ audioContext.resume();
5518
+ return;
5519
+ }
5520
+
5334
5521
  // create buffer and source
5335
5522
  const channelCount = sampleChannels.length;
5336
5523
  const sampleLength = sampleChannels[0].length;
@@ -5356,13 +5543,6 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
5356
5543
  if (onended)
5357
5544
  source.addEventListener('ended', ()=> onended(source));
5358
5545
 
5359
- if (!audioIsRunning())
5360
- {
5361
- // fix stalled audio, this sound won't be able to play
5362
- audioContext.resume();
5363
- return;
5364
- }
5365
-
5366
5546
  // play and return sound
5367
5547
  const startOffset = offset * rate;
5368
5548
  source.start(0, startOffset);
@@ -5530,7 +5710,7 @@ function zzfxG
5530
5710
  * - Unlimited numbers of layers, allocates canvases as needed
5531
5711
  * - Tile layers can be drawn to using their context with canvas2d
5532
5712
  * - Tile layers can also have collision with EngineObjects
5533
- * @namespace TileCollision
5713
+ * @namespace TileLayers
5534
5714
  */
5535
5715
 
5536
5716
  ///////////////////////////////////////////////////////////////////////////////
@@ -5538,13 +5718,13 @@ function zzfxG
5538
5718
 
5539
5719
  /** Keep track of all tile layers with collision
5540
5720
  * @type {Array<TileCollisionLayer>}
5541
- * @memberof TileCollision */
5721
+ * @memberof TileLayers */
5542
5722
  const tileCollisionLayers = [];
5543
5723
 
5544
5724
  /** Get tile collision data for a given cell in the grid
5545
5725
  * @param {Vector2} pos
5546
5726
  * @return {number}
5547
- * @memberof TileCollision */
5727
+ * @memberof TileLayers */
5548
5728
  function tileCollisionGetData(pos)
5549
5729
  {
5550
5730
  // check all tile collision layers
@@ -5560,7 +5740,7 @@ function tileCollisionGetData(pos)
5560
5740
  * @param {EngineObject} [object] - An object or undefined for generic test
5561
5741
  * @param {boolean} [solidOnly] - Only check solid layers if true
5562
5742
  * @return {TileCollisionLayer}
5563
- * @memberof TileCollision */
5743
+ * @memberof TileLayers */
5564
5744
  function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5565
5745
  {
5566
5746
  for (const layer of tileCollisionLayers)
@@ -5578,7 +5758,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5578
5758
  * @param {EngineObject} [object] - An object or undefined for generic test
5579
5759
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5580
5760
  * @return {Vector2}
5581
- * @memberof TileCollision */
5761
+ * @memberof TileLayers */
5582
5762
  function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5583
5763
  {
5584
5764
  for (const layer of tileCollisionLayers)
@@ -5601,8 +5781,8 @@ function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5601
5781
  * @param {number} [collisionLayer] - Layer to use for collision if any
5602
5782
  * @param {boolean} [draw] - Should the layer be drawn automatically
5603
5783
  * @return {Array<TileCollisionLayer>}
5604
- * @memberof TileCollision */
5605
- function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5784
+ * @memberof TileLayers */
5785
+ function tileLayersLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisionLayer, draw=true)
5606
5786
  {
5607
5787
  if (!tileMapData)
5608
5788
  {
@@ -5633,9 +5813,9 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5633
5813
  tileLayers[layerIndex] = tileLayer;
5634
5814
 
5635
5815
  // apply layer color
5636
- const layerColor = dataLayer.color || WHITE;
5637
- if (dataLayer.tintcolor)
5638
- layerColor.setHex(dataLayer.tintcolor);
5816
+ const layerColor = dataLayer.tintcolor ?
5817
+ new Color().setHex(dataLayer.tintcolor) :
5818
+ dataLayer.color || WHITE;
5639
5819
  ASSERT(isColor(layerColor), 'layer color is not a color');
5640
5820
 
5641
5821
  for (let x=levelSize.x; x--;)
@@ -5662,6 +5842,7 @@ function tileCollisionLoad(tileMapData, tileInfo=tile(), renderOrder=0, collisio
5662
5842
  ///////////////////////////////////////////////////////////////////////////////
5663
5843
  /**
5664
5844
  * Tile layer data object stores info about how to draw a tile
5845
+ * @memberof TileLayers
5665
5846
  * @example
5666
5847
  * // create tile layer data with tile index 0 and random orientation and color
5667
5848
  * const tileIndex = 0;
@@ -5699,6 +5880,7 @@ class TileLayerData
5699
5880
  * - Contains an offscreen canvas that can be rendered to
5700
5881
  * - WebGL rendering is optional, call useWebGL to enable
5701
5882
  * @extends EngineObject
5883
+ * @memberof TileLayers
5702
5884
  * @example
5703
5885
  * const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
5704
5886
  */
@@ -5713,15 +5895,19 @@ class CanvasLayer extends EngineObject
5713
5895
  */
5714
5896
  constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512))
5715
5897
  {
5898
+ ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5716
5899
  super(position, size, undefined, angle, WHITE, renderOrder);
5717
5900
 
5718
5901
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
5719
5902
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
5720
5903
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5721
- this.context = headlessMode ? undefined : this.canvas.getContext('2d');
5722
- /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
5723
- this.glTexture = undefined;
5724
- this.gravityScale = 0; // disable gravity by default for canvas layers
5904
+ this.context = this.canvas?.getContext('2d');
5905
+ /** @property {TextureInfo} - Texture info to use for this object rendering */
5906
+ const useWebGL = false; // do not use webgl by default
5907
+ this.textureInfo = new TextureInfo(this.canvas, useWebGL);
5908
+
5909
+ // disable physics by default
5910
+ this.mass = this.gravityScale = this.friction = this.restitution = 0;
5725
5911
  }
5726
5912
 
5727
5913
  /** Destroy this canvas layer */
@@ -5730,9 +5916,7 @@ class CanvasLayer extends EngineObject
5730
5916
  if (this.destroyed)
5731
5917
  return;
5732
5918
 
5733
- // free up the WebGL texture
5734
- if (this.glTexture)
5735
- glDeleteTexture(this.glTexture);
5919
+ this.textureInfo.destroyWebGLTexture();
5736
5920
  super.destroy();
5737
5921
  }
5738
5922
 
@@ -5755,19 +5939,27 @@ class CanvasLayer extends EngineObject
5755
5939
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5756
5940
  {
5757
5941
  // draw the canvas layer as a single tile that uses the whole texture
5758
- const useWebGL = glEnable && this.glTexture !== undefined;
5759
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5942
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
5943
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5760
5944
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5761
5945
  }
5762
5946
 
5947
+ /**
5948
+ * @callback Canvas2DDrawCallback - Function that draws to a canvas 2D context
5949
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
5950
+ * @memberof TileLayers
5951
+ */
5952
+
5763
5953
  /** Draw onto the layer canvas in world space (bypass WebGL)
5764
5954
  * @param {Vector2} pos
5765
5955
  * @param {Vector2} size
5766
5956
  * @param {number} angle
5767
5957
  * @param {boolean} mirror
5768
- * @param {Function} drawFunction */
5958
+ * @param {Canvas2DDrawCallback} drawFunction */
5769
5959
  drawCanvas2D(pos, size, angle, mirror, drawFunction)
5770
5960
  {
5961
+ if (!this.context) return;
5962
+
5771
5963
  const context = this.context;
5772
5964
  context.save();
5773
5965
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
@@ -5802,7 +5994,7 @@ class CanvasLayer extends EngineObject
5802
5994
  else
5803
5995
  {
5804
5996
  // untextured
5805
- context.fillStyle = color;
5997
+ context.fillStyle = color.toString();
5806
5998
  context.fillRect(-.5, -.5, 1, 1);
5807
5999
  }
5808
6000
  });
@@ -5820,15 +6012,10 @@ class CanvasLayer extends EngineObject
5820
6012
  * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5821
6013
  useWebGL(enable=true)
5822
6014
  {
5823
- if (glEnable && enable)
5824
- {
5825
- if (this.glTexture)
5826
- glSetTextureData(this.glTexture, this.canvas);
5827
- else
5828
- this.glTexture = glCreateTexture(this.canvas);
5829
- }
6015
+ if (enable)
6016
+ this.textureInfo.createWebGLTexture();
5830
6017
  else
5831
- this.glTexture = undefined;
6018
+ this.textureInfo.destroyWebGLTexture();
5832
6019
  }
5833
6020
  }
5834
6021
 
@@ -5839,7 +6026,9 @@ class CanvasLayer extends EngineObject
5839
6026
  * - To allow dynamic modifications, layers are rendered using canvas 2d
5840
6027
  * - Some devices like mobile phones are limited to 4k texture resolution
5841
6028
  * - For with 16x16 tiles this limits layers to 256x256 on mobile devices
6029
+ * - Tile layers are centered on their corner, so normal levels are at (0,0)
5842
6030
  * @extends CanvasLayer
6031
+ * @memberof TileLayers
5843
6032
  * @example
5844
6033
  * const tileLayer = new TileLayer(vec2(), vec2(200,100));
5845
6034
  */
@@ -5849,26 +6038,15 @@ class TileLayer extends CanvasLayer
5849
6038
  * @param {Vector2} position - World space position
5850
6039
  * @param {Vector2} size - World space size
5851
6040
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
5852
- * @param {Vector2} [scale=(1,1)] - How much to scale this layer when rendered
5853
6041
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5854
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5855
6042
  */
5856
- constructor(position, size, tileInfo=tile(), scale=vec2(1), renderOrder=0, useWebGL=glEnable)
6043
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
5857
6044
  {
5858
- super(position, size, 0, renderOrder, size);
5859
- this.tileInfo = tileInfo;
5860
-
5861
6045
  const canvasSize = size.multiply(tileInfo.size);
5862
- /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
5863
- this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
5864
- /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
5865
- this.context = this.canvas.getContext('2d');
5866
- /** @property {WebGLTexture} - Texture if using WebGL for this layer */
5867
- this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
5868
- // set no friction by default, applied friction is max of both objects
5869
- this.friction = 0;
5870
- // set no restitution by default, applied restitution is max of both objects
5871
- this.restitution = 0;
6046
+ super(position, size, 0, renderOrder, canvasSize);
6047
+
6048
+ // set tile info
6049
+ this.tileInfo = tileInfo;
5872
6050
 
5873
6051
  // init tile data
5874
6052
  this.data = [];
@@ -5894,6 +6072,8 @@ class TileLayer extends CanvasLayer
5894
6072
  * @param {boolean} [redraw] - Force the tile to redraw if true */
5895
6073
  setData(layerPos, data, redraw=false)
5896
6074
  {
6075
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6076
+ ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
5897
6077
  if (layerPos.arrayCheck(this.size))
5898
6078
  {
5899
6079
  this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
@@ -5905,7 +6085,10 @@ class TileLayer extends CanvasLayer
5905
6085
  * @param {Vector2} layerPos - Local position in array
5906
6086
  * @return {TileLayerData} */
5907
6087
  getData(layerPos)
5908
- { return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]; }
6088
+ {
6089
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6090
+ return layerPos.arrayCheck(this.size) && this.data[(layerPos.y|0)*this.size.x+layerPos.x|0];
6091
+ }
5909
6092
 
5910
6093
  // Render the tile layer, called automatically by the engine
5911
6094
  render()
@@ -5913,10 +6096,11 @@ class TileLayer extends CanvasLayer
5913
6096
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5914
6097
 
5915
6098
  // draw the tile layer as a single tile
5916
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5917
- const pos = this.pos.add(this.size.scale(.5));
5918
- const useWebGL = glEnable && this.glTexture !== undefined;
5919
- drawTile(pos, this.size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
6099
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6100
+ const size = this.drawSize || this.size;
6101
+ const pos = this.pos.add(size.scale(.5));
6102
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
6103
+ drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5920
6104
  }
5921
6105
 
5922
6106
  /** Draw all the tile data to an offscreen canvas
@@ -5928,8 +6112,7 @@ class TileLayer extends CanvasLayer
5928
6112
  for (let y = this.size.y; y--;)
5929
6113
  this.drawTileData(vec2(x,y), false);
5930
6114
  this.redrawEnd();
5931
- if (this.glTexture)
5932
- this.useWebGL(); // update WebGL texture
6115
+ this.useWebGL();
5933
6116
  }
5934
6117
 
5935
6118
  /** Call to start the redraw process
@@ -5937,6 +6120,8 @@ class TileLayer extends CanvasLayer
5937
6120
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
5938
6121
  redrawStart(clear=false)
5939
6122
  {
6123
+ if (!this.context) return;
6124
+
5940
6125
  // save current render settings
5941
6126
  /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
5942
6127
  this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
@@ -5965,6 +6150,8 @@ class TileLayer extends CanvasLayer
5965
6150
  /** Call to end the redraw process */
5966
6151
  redrawEnd()
5967
6152
  {
6153
+ if (!this.context) return;
6154
+
5968
6155
  ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
5969
6156
  glCopyToContext(drawContext);
5970
6157
  //debugSaveCanvas(this.canvas);
@@ -5981,6 +6168,8 @@ class TileLayer extends CanvasLayer
5981
6168
  */
5982
6169
  drawTileData(layerPos, clear=true)
5983
6170
  {
6171
+ if (!this.context) return;
6172
+
5984
6173
  // clear out where the tile was, for full opaque tiles this can be skipped
5985
6174
  const s = this.tileInfo.size;
5986
6175
  if (clear)
@@ -6008,6 +6197,7 @@ class TileLayer extends CanvasLayer
6008
6197
  * - there can be multiple tile collision layers
6009
6198
  * - tile collision layers should not overlap each other
6010
6199
  * @extends TileLayer
6200
+ * @memberof TileLayers
6011
6201
  */
6012
6202
  class TileCollisionLayer extends TileLayer
6013
6203
  {
@@ -6016,12 +6206,10 @@ class TileCollisionLayer extends TileLayer
6016
6206
  * @param {Vector2} size - World space size
6017
6207
  * @param {TileInfo} [tileInfo] - Tile info for layer
6018
6208
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6019
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
6020
6209
  */
6021
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
6210
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
6022
6211
  {
6023
- const scale = vec2(1); // collision layers are not scaled
6024
- super(position, size.floor(), tileInfo, scale, renderOrder, useWebGL);
6212
+ super(position, size.floor(), tileInfo, renderOrder);
6025
6213
 
6026
6214
  /** @property {Array<number>} - The tile collision grid */
6027
6215
  this.collisionData = [];
@@ -6051,6 +6239,7 @@ class TileCollisionLayer extends TileLayer
6051
6239
  * @param {Vector2} size - width and height of tile collision 2d grid */
6052
6240
  initCollision(size)
6053
6241
  {
6242
+ ASSERT(isVector2(size), 'size must be a Vector2');
6054
6243
  this.size = size.floor();
6055
6244
  this.collisionData = [];
6056
6245
  this.collisionData.length = size.area();
@@ -6062,6 +6251,7 @@ class TileCollisionLayer extends TileLayer
6062
6251
  * @param {number} [data] */
6063
6252
  setCollisionData(gridPos, data=1)
6064
6253
  {
6254
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6065
6255
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6066
6256
  gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6067
6257
  }
@@ -6071,6 +6261,7 @@ class TileCollisionLayer extends TileLayer
6071
6261
  * @return {number} */
6072
6262
  getCollisionData(gridPos)
6073
6263
  {
6264
+ ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6074
6265
  const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6075
6266
  return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6076
6267
  }
@@ -6082,6 +6273,9 @@ class TileCollisionLayer extends TileLayer
6082
6273
  * @return {boolean} */
6083
6274
  collisionTest(pos, size=new Vector2, object)
6084
6275
  {
6276
+ ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
6277
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6278
+
6085
6279
  // transform to local layer space
6086
6280
  const posX = pos.x - this.pos.x;
6087
6281
  const posY = pos.y - this.pos.y;
@@ -6112,6 +6306,9 @@ class TileCollisionLayer extends TileLayer
6112
6306
  * @return {Vector2} */
6113
6307
  collisionRaycast(posStart, posEnd, object)
6114
6308
  {
6309
+ ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
6310
+ ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6311
+
6115
6312
  // transform to local layer space
6116
6313
  const posStartX = posStart.x - this.pos.x;
6117
6314
  const posStartY = posStart.y - this.pos.y;
@@ -6161,9 +6358,16 @@ class TileCollisionLayer extends TileLayer
6161
6358
  * LittleJS Particle System
6162
6359
  */
6163
6360
 
6361
+ /**
6362
+ * @callback ParticleCallbackFunction - Function that processes a particle
6363
+ * @param {Particle} particle
6364
+ * @memberof Engine
6365
+ */
6366
+
6164
6367
  /**
6165
6368
  * Particle Emitter - Spawns particles with the given settings
6166
6369
  * @extends EngineObject
6370
+ * @memberof Engine
6167
6371
  * @example
6168
6372
  * // create a particle emitter
6169
6373
  * let pos = vec2(2,3);
@@ -6173,7 +6377,7 @@ class TileCollisionLayer extends TileLayer
6173
6377
  * tile(0, 16), // tileInfo
6174
6378
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
6175
6379
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
6176
- * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
6380
+ * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
6177
6381
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
6178
6382
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
6179
6383
  * );
@@ -6188,10 +6392,10 @@ class ParticleEmitter extends EngineObject
6188
6392
  * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
6189
6393
  * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
6190
6394
  * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
6191
- * @param {Color} [colorStartA=(1,1,1,1)] - Color at start of life 1, randomized between start colors
6192
- * @param {Color} [colorStartB=(1,1,1,1)] - Color at start of life 2, randomized between start colors
6193
- * @param {Color} [colorEndA=(1,1,1,0)] - Color at end of life 1, randomized between end colors
6194
- * @param {Color} [colorEndB=(1,1,1,0)] - Color at end of life 2, randomized between end colors
6395
+ * @param {Color} [colorStartA=WHITE] - Color at start of life 1, randomized between start colors
6396
+ * @param {Color} [colorStartB=WHITE] - Color at start of life 2, randomized between start colors
6397
+ * @param {Color} [colorEndA=CLEAR_WHITE] - Color at end of life 1, randomized between end colors
6398
+ * @param {Color} [colorEndB=CLEAR_WHITE] - Color at end of life 2, randomized between end colors
6195
6399
  * @param {number} [particleTime] - How long particles live
6196
6400
  * @param {number} [sizeStart] - How big are particles at start
6197
6401
  * @param {number} [sizeEnd] - How big are particles at end
@@ -6218,10 +6422,10 @@ class ParticleEmitter extends EngineObject
6218
6422
  emitRate = 100,
6219
6423
  emitConeAngle = PI,
6220
6424
  tileInfo,
6221
- colorStartA = new Color,
6222
- colorStartB = new Color,
6223
- colorEndA = new Color(1,1,1,0),
6224
- colorEndB = new Color(1,1,1,0),
6425
+ colorStartA = WHITE,
6426
+ colorStartB = WHITE,
6427
+ colorEndA = CLEAR_WHITE,
6428
+ colorEndB = CLEAR_WHITE,
6225
6429
  particleTime = .5,
6226
6430
  sizeStart = .1,
6227
6431
  sizeEnd = 1,
@@ -6244,7 +6448,8 @@ class ParticleEmitter extends EngineObject
6244
6448
 
6245
6449
  // emitter settings
6246
6450
  /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
6247
- this.emitSize = emitSize
6451
+ this.emitSize = emitSize instanceof Vector2 ?
6452
+ emitSize.copy() : emitSize;
6248
6453
  /** @property {number} - How long to stay alive (0 is forever) */
6249
6454
  this.emitTime = emitTime;
6250
6455
  /** @property {number} - How many particles per second to spawn, does not emit if 0 */
@@ -6254,13 +6459,13 @@ class ParticleEmitter extends EngineObject
6254
6459
 
6255
6460
  // color settings
6256
6461
  /** @property {Color} - Color at start of life 1, randomized between start colors */
6257
- this.colorStartA = colorStartA;
6462
+ this.colorStartA = colorStartA.copy();
6258
6463
  /** @property {Color} - Color at start of life 2, randomized between start colors */
6259
- this.colorStartB = colorStartB;
6464
+ this.colorStartB = colorStartB.copy();
6260
6465
  /** @property {Color} - Color at end of life 1, randomized between end colors */
6261
- this.colorEndA = colorEndA;
6466
+ this.colorEndA = colorEndA.copy();
6262
6467
  /** @property {Color} - Color at end of life 2, randomized between end colors */
6263
- this.colorEndB = colorEndB;
6468
+ this.colorEndB = colorEndB.copy();
6264
6469
  /** @property {boolean} - Should color be randomized linearly or across each component */
6265
6470
  this.randomColorLinear = randomColorLinear;
6266
6471
 
@@ -6295,9 +6500,9 @@ class ParticleEmitter extends EngineObject
6295
6500
  this.localSpace = localSpace;
6296
6501
  /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
6297
6502
  this.trailScale = 0;
6298
- /** @property {Function} - Callback when particle is destroyed */
6503
+ /** @property {ParticleCallbackFunction} - Callback when particle is destroyed */
6299
6504
  this.particleDestroyCallback = undefined;
6300
- /** @property {Function} - Callback when particle is created */
6505
+ /** @property {ParticleCallbackFunction} - Callback when particle is created */
6301
6506
  this.particleCreateCallback = undefined;
6302
6507
  /** @property {number} - Track particle emit time */
6303
6508
  this.emitTimeBuffer = 0;
@@ -6398,6 +6603,7 @@ class ParticleEmitter extends EngineObject
6398
6603
  /**
6399
6604
  * Particle Object - Created automatically by Particle Emitters
6400
6605
  * @extends EngineObject
6606
+ * @memberof Engine
6401
6607
  */
6402
6608
  class Particle extends EngineObject
6403
6609
  {
@@ -6416,7 +6622,7 @@ class Particle extends EngineObject
6416
6622
  * @param {boolean} additive - Does it use additive blend mode
6417
6623
  * @param {number} trailScale - If a trail, how long to make it
6418
6624
  * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
6419
- * @param {Function} [destroyCallback] - Callback when particle dies
6625
+ * @param {ParticleCallbackFunction} [destroyCallback] - Callback when particle dies
6420
6626
  */
6421
6627
  constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
6422
6628
  )
@@ -6425,14 +6631,14 @@ class Particle extends EngineObject
6425
6631
 
6426
6632
  /** @property {Color} - Color at start of life */
6427
6633
  this.colorStart = colorStart;
6428
- /** @property {Color} - Calculated change in color */
6429
- this.colorEndDelta = colorEnd.subtract(colorStart);
6634
+ /** @property {Color} - Color at end of life */
6635
+ this.colorEnd = colorEnd;
6430
6636
  /** @property {number} - How long to live for */
6431
6637
  this.lifeTime = lifeTime;
6432
6638
  /** @property {number} - Size at start of life */
6433
6639
  this.sizeStart = sizeStart;
6434
- /** @property {number} - Calculated change in size */
6435
- this.sizeEndDelta = sizeEnd - sizeStart;
6640
+ /** @property {number} - Size at end of life */
6641
+ this.sizeEnd = sizeEnd;
6436
6642
  /** @property {number} - How quick to fade in/out */
6437
6643
  this.fadeRate = fadeRate;
6438
6644
  /** @property {boolean} - Is it additive */
@@ -6441,7 +6647,7 @@ class Particle extends EngineObject
6441
6647
  this.trailScale = trailScale;
6442
6648
  /** @property {ParticleEmitter} - Parent emitter if local space */
6443
6649
  this.localSpaceEmitter = localSpaceEmitter;
6444
- /** @property {Function} - Called when particle dies */
6650
+ /** @property {ParticleCallbackFunction} - Called when particle dies */
6445
6651
  this.destroyCallback = destroyCallback;
6446
6652
  // particles do not clamp speed by default
6447
6653
  this.clampSpeed = false;
@@ -6468,54 +6674,58 @@ class Particle extends EngineObject
6468
6674
  /** Render the particle, automatically called each frame, sorted by renderOrder */
6469
6675
  render()
6470
6676
  {
6471
- // modulate size and color
6472
- const p = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1;
6473
- const radius = this.sizeStart + p * this.sizeEndDelta;
6474
- const size = vec2(radius);
6677
+ // lerp color and size
6678
+ const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6679
+ const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6680
+ this.size.x = this.size.y = radius;
6681
+ this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6682
+ this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6683
+ this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
6684
+ this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
6685
+
6686
+ // fade alpha
6475
6687
  const fadeRate = this.fadeRate/2;
6476
- const color = new Color(
6477
- this.colorStart.r + p * this.colorEndDelta.r,
6478
- this.colorStart.g + p * this.colorEndDelta.g,
6479
- this.colorStart.b + p * this.colorEndDelta.b,
6480
- (this.colorStart.a + p * this.colorEndDelta.a) *
6481
- (p < fadeRate ? p/fadeRate : p > 1-fadeRate ? (1-p)/fadeRate : 1)); // fade alpha
6688
+ this.color.a *= p1 < fadeRate ? p1/fadeRate :
6689
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
6482
6690
 
6483
6691
  // draw the particle
6484
6692
  this.additive && setBlendMode(true);
6485
6693
 
6694
+ // update the position and angle for drawing
6486
6695
  let pos = this.pos, angle = this.angle;
6487
6696
  if (this.localSpaceEmitter)
6488
6697
  {
6489
6698
  // in local space of emitter
6490
- pos = this.localSpaceEmitter.pos.add(pos.rotate(-this.localSpaceEmitter.angle));
6699
+ const a = this.localSpaceEmitter.angle;
6700
+ const c = Math.cos(a), s = Math.sin(a);
6701
+ pos = this.localSpaceEmitter.pos.add(
6702
+ new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6491
6703
  angle += this.localSpaceEmitter.angle;
6492
6704
  }
6493
6705
  if (this.trailScale)
6494
6706
  {
6495
6707
  // trail style particles
6496
- let velocity = this.velocity;
6497
- if (this.localSpaceEmitter)
6498
- velocity = velocity.rotate(-this.localSpaceEmitter.angle);
6499
- const speed = velocity.length();
6708
+ const direction = this.localSpaceEmitter ?
6709
+ this.velocity.rotate(-this.localSpaceEmitter.angle) :
6710
+ this.velocity;
6711
+ const speed = direction.length();
6500
6712
  if (speed)
6501
6713
  {
6502
- const direction = velocity.scale(1/speed);
6714
+ // stretch in direction of motion
6503
6715
  const trailLength = speed * this.trailScale;
6504
- size.y = max(size.x, trailLength);
6505
- angle = direction.angle();
6506
- drawTile(pos.add(direction.multiply(vec2(0,-trailLength/2))), size, this.tileInfo, color, angle, this.mirror);
6716
+ this.size.y = max(this.size.x, trailLength);
6717
+ angle = Math.atan2(direction.x, direction.y);
6718
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6507
6719
  }
6508
6720
  }
6509
6721
  else
6510
- drawTile(pos, size, this.tileInfo, color, angle, this.mirror);
6722
+ drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6511
6723
  this.additive && setBlendMode();
6512
- debugParticles && debugRect(pos, size, '#f005', 0, angle);
6724
+ debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
6513
6725
 
6514
- if (p === 1)
6726
+ if (p1 === 1)
6515
6727
  {
6516
- // destroy particle when it's time runs out
6517
- this.color = color;
6518
- this.size = size;
6728
+ // destroy particle when its time runs out
6519
6729
  this.destroyCallback && this.destroyCallback(this);
6520
6730
  this.destroyed = 1;
6521
6731
  }
@@ -6580,8 +6790,14 @@ function medalsInit(saveName)
6580
6790
  }
6581
6791
  }
6582
6792
 
6793
+ /**
6794
+ * @callback MedalCallbackFunction - Function that processes a medal
6795
+ * @param {Medal} medal
6796
+ * @memberof Medals
6797
+ */
6798
+
6583
6799
  /** Calls a function for each medal
6584
- * @param {Function} callback
6800
+ * @param {MedalCallbackFunction} callback
6585
6801
  * @memberof Medals */
6586
6802
  function medalsForEach(callback)
6587
6803
  { Object.values(medals).forEach(medal=>callback(medal)); }
@@ -6590,6 +6806,7 @@ function medalsForEach(callback)
6590
6806
 
6591
6807
  /**
6592
6808
  * Medal - Tracks an unlockable medal
6809
+ * @memberof Medals
6593
6810
  * @example
6594
6811
  * // create a medal
6595
6812
  * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
@@ -6658,11 +6875,12 @@ class Medal
6658
6875
  const height = medalDisplaySize.y;
6659
6876
  const x = overlayCanvas.width - width;
6660
6877
  const y = -height*hidePercent;
6878
+ const backgroundColor = hsl(0,0,.9);
6661
6879
 
6662
6880
  // draw containing rect and clip to that region
6663
6881
  context.save();
6664
6882
  context.beginPath();
6665
- context.fillStyle = new Color(.9,.9,.9).toString();
6883
+ context.fillStyle = backgroundColor.toString();
6666
6884
  context.strokeStyle = BLACK.toString();
6667
6885
  context.lineWidth = 3;
6668
6886
  context.rect(x, y, width, height);
@@ -6733,7 +6951,7 @@ let glContext;
6733
6951
  let glAntialias = true;
6734
6952
 
6735
6953
  // WebGL internal variables not exposed to documentation
6736
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
6954
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos;
6737
6955
 
6738
6956
  // WebGL internal constants
6739
6957
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6749,6 +6967,9 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
6749
6967
  // Initialize WebGL, called automatically by the engine
6750
6968
  function glInit()
6751
6969
  {
6970
+ // keep set of texture infos so they can be restored if context is lost
6971
+ glTextureInfos = new Set;
6972
+
6752
6973
  if (!glEnable || headlessMode) return;
6753
6974
 
6754
6975
  // create the canvas and textures
@@ -6766,68 +6987,101 @@ function glInit()
6766
6987
  // create the WebGL canvas
6767
6988
  const rootElement = mainCanvas.parentElement;
6768
6989
  rootElement.appendChild(glCanvas);
6990
+
6991
+ // startup webgl
6992
+ initWebGL();
6993
+
6994
+ // setup context lost and restore handlers
6995
+ glCanvas.addEventListener('webglcontextlost', (e)=>
6996
+ {
6997
+ glEnable = false; // disable WebGL rendering
6998
+ glCanvas.style.display = 'none'; // hide the gl canvas
6999
+ e.preventDefault(); // prevent default to allow restoration
7000
+ LOG('WebGL context lost! Switching to Canvas2d rendering.');
7001
+
7002
+ // remove WebGL textures
7003
+ for (const info of glTextureInfos)
7004
+ info.glTexture = undefined;
7005
+ glActiveTexture = undefined;
7006
+ pluginList.forEach(plugin=>plugin.glContextLost?.());
7007
+ });
7008
+ glCanvas.addEventListener('webglcontextrestored', ()=>
7009
+ {
7010
+ glEnable = true; // disable WebGL rendering
7011
+ glCanvas.style.display = ''; // show the gl canvas
7012
+ LOG('WebGL context restored, reinitializing...');
6769
7013
 
6770
- // setup instanced rendering shader program
6771
- glShader = glCreateProgram(
6772
- '#version 300 es\n' + // specify GLSL ES version
6773
- 'precision highp float;'+ // use highp for better accuracy
6774
- 'uniform mat4 m;'+ // transform matrix
6775
- 'in vec2 g;'+ // in: geometry
6776
- 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
6777
- 'in float r;'+ // in: rotation
6778
- 'out vec2 v;'+ // out: uv
6779
- 'out vec4 d,e;'+ // out: color, additiveColor
6780
- 'void main(){'+ // shader entry point
6781
- 'vec2 s=(g-.5)*p.zw;'+ // get size offset
6782
- 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
6783
- 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
6784
- 'd=c;e=a;'+ // pass colors to fragment shader
6785
- '}' // end of shader
6786
- ,
6787
- '#version 300 es\n' + // specify GLSL ES version
6788
- 'precision highp float;'+ // use highp for better accuracy
6789
- 'uniform sampler2D s;'+ // texture
6790
- 'in vec2 v;'+ // in: uv
6791
- 'in vec4 d,e;'+ // in: color, additiveColor
6792
- 'out vec4 c;'+ // out: color
6793
- 'void main(){'+ // shader entry point
6794
- 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
6795
- '}' // end of shader
6796
- );
7014
+ // reinit WebGL and restore textures
7015
+ initWebGL();
7016
+ for (const info of glTextureInfos)
7017
+ info.glTexture = glCreateTexture(info.image);
7018
+ pluginList.forEach(plugin=>plugin.glContextRestored?.());
7019
+ });
6797
7020
 
6798
- // setup poly rendering shaders
6799
- glPolyShader = glCreateProgram(
6800
- '#version 300 es\n' + // specify GLSL ES version
6801
- 'precision highp float;'+ // use highp for better accuracy
6802
- 'uniform mat4 m;'+ // transform matrix
6803
- 'in vec2 p;'+ // in: position
6804
- 'in vec4 c;'+ // in: color
6805
- 'out vec4 d;'+ // out: color
6806
- 'void main(){'+ // shader entry point
6807
- 'gl_Position=m*vec4(p,1,1);'+ // transform position
6808
- 'd=c;'+ // pass color to fragment shader
6809
- '}' // end of shader
6810
- ,
6811
- '#version 300 es\n' + // specify GLSL ES version
6812
- 'precision highp float;'+ // use highp for better accuracy
6813
- 'in vec4 d;'+ // in: color
6814
- 'out vec4 c;'+ // out: color
6815
- 'void main(){'+ // shader entry point
6816
- 'c=d;'+ // set color
6817
- '}' // end of shader
6818
- );
7021
+ function initWebGL()
7022
+ {
7023
+ // setup instanced rendering shader program
7024
+ glShader = glCreateProgram(
7025
+ '#version 300 es\n' + // specify GLSL ES version
7026
+ 'precision highp float;'+ // use highp for better accuracy
7027
+ 'uniform mat4 m;'+ // transform matrix
7028
+ 'in vec2 g;'+ // in: geometry
7029
+ 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
7030
+ 'in float r;'+ // in: rotation
7031
+ 'out vec2 v;'+ // out: uv
7032
+ 'out vec4 d,e;'+ // out: color, additiveColor
7033
+ 'void main(){'+ // shader entry point
7034
+ 'vec2 s=(g-.5)*p.zw;'+ // get size offset
7035
+ 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
7036
+ 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
7037
+ 'd=c;e=a;'+ // pass colors to fragment shader
7038
+ '}' // end of shader
7039
+ ,
7040
+ '#version 300 es\n' + // specify GLSL ES version
7041
+ 'precision highp float;'+ // use highp for better accuracy
7042
+ 'uniform sampler2D s;'+ // texture
7043
+ 'in vec2 v;'+ // in: uv
7044
+ 'in vec4 d,e;'+ // in: color, additiveColor
7045
+ 'out vec4 c;'+ // out: color
7046
+ 'void main(){'+ // shader entry point
7047
+ 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
7048
+ '}' // end of shader
7049
+ );
6819
7050
 
6820
- // init buffers
6821
- const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
6822
- glPositionData = new Float32Array(glInstanceData);
6823
- glColorData = new Uint32Array(glInstanceData);
6824
- glArrayBuffer = glContext.createBuffer();
6825
- glGeometryBuffer = glContext.createBuffer();
7051
+ // setup poly rendering shaders
7052
+ glPolyShader = glCreateProgram(
7053
+ '#version 300 es\n' + // specify GLSL ES version
7054
+ 'precision highp float;'+ // use highp for better accuracy
7055
+ 'uniform mat4 m;'+ // transform matrix
7056
+ 'in vec2 p;'+ // in: position
7057
+ 'in vec4 c;'+ // in: color
7058
+ 'out vec4 d;'+ // out: color
7059
+ 'void main(){'+ // shader entry point
7060
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
7061
+ 'd=c;'+ // pass color to fragment shader
7062
+ '}' // end of shader
7063
+ ,
7064
+ '#version 300 es\n' + // specify GLSL ES version
7065
+ 'precision highp float;'+ // use highp for better accuracy
7066
+ 'in vec4 d;'+ // in: color
7067
+ 'out vec4 c;'+ // out: color
7068
+ 'void main(){'+ // shader entry point
7069
+ 'c=d;'+ // set color
7070
+ '}' // end of shader
7071
+ );
6826
7072
 
6827
- // create the geometry buffer, triangle strip square
6828
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
6829
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6830
- glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
7073
+ // init buffers
7074
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
7075
+ glPositionData = new Float32Array(glInstanceData);
7076
+ glColorData = new Uint32Array(glInstanceData);
7077
+ glArrayBuffer = glContext.createBuffer();
7078
+ glGeometryBuffer = glContext.createBuffer();
7079
+
7080
+ // create the geometry buffer, triangle strip square
7081
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
7082
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7083
+ glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
7084
+ }
6831
7085
  }
6832
7086
 
6833
7087
  function glSetInstancedMode()
@@ -6934,7 +7188,7 @@ function glPreRender()
6934
7188
  // start with additive blending off
6935
7189
  glAdditive = glBatchAdditive = false;
6936
7190
 
6937
- // force it to enter instanced mode
7191
+ // force it to set instanced mode by first setting poly mode true
6938
7192
  glPolyMode = true;
6939
7193
  glSetInstancedMode();
6940
7194
  }
@@ -7075,6 +7329,41 @@ function glSetTextureData(texture, image)
7075
7329
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
7076
7330
  }
7077
7331
 
7332
+ /** Tells WebGL to create or update the glTexture and start tracking it
7333
+ * @param {TextureInfo} textureInfo
7334
+ * @memberof WebGL */
7335
+ function glRegisterTextureInfo(textureInfo)
7336
+ {
7337
+ if (headlessMode) return;
7338
+
7339
+ // add texture info to tracking list even if gl is not enabled
7340
+ glTextureInfos.add(textureInfo);
7341
+
7342
+ if (!glContext) return;
7343
+
7344
+ // create or set the texture data
7345
+ if (textureInfo.glTexture)
7346
+ glSetTextureData(textureInfo.glTexture, textureInfo.image);
7347
+ else
7348
+ textureInfo.glTexture = glCreateTexture(textureInfo.image);
7349
+ }
7350
+
7351
+ /** Tells WebGL to destroy the glTexture and stop tracking it
7352
+ * @param {TextureInfo} textureInfo
7353
+ * @memberof WebGL */
7354
+ function glUnregisterTextureInfo(textureInfo)
7355
+ {
7356
+ if (headlessMode) return;
7357
+
7358
+ // delete texture info from tracking list even if gl is not enabled
7359
+ glTextureInfos.delete(textureInfo);
7360
+
7361
+ // unset and destroy the texture
7362
+ const glTexture = textureInfo.glTexture;
7363
+ textureInfo.glTexture = undefined;
7364
+ glDeleteTexture(glTexture);
7365
+ }
7366
+
7078
7367
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
7079
7368
  * @memberof WebGL */
7080
7369
  function glFlush()
@@ -7464,24 +7753,25 @@ function glPolyStrip(points)
7464
7753
  return strip;
7465
7754
  }
7466
7755
  /**
7467
- * LittleJS Newgrounds API
7756
+ * LittleJS Newgrounds Plugin
7468
7757
  * - NewgroundsMedal extends Medal with Newgrounds API functionality
7469
- * - Call new NewgroundsPlugin() to setup Newgrounds
7758
+ * - Call new NewgroundsPlugin(app_id) to setup Newgrounds
7470
7759
  * - Uses CryptoJS for encryption if optional cipher is provided
7760
+ * - provides functions to interact with medals scoreboards
7471
7761
  * - Keeps connection alive and logs views
7472
- * - Functions to interact with scoreboards
7473
- * - Functions to unlock medals
7762
+ * @namespace Newgrounds
7474
7763
  */
7475
7764
 
7476
7765
  /** Global Newgrounds object
7477
7766
  * @type {NewgroundsPlugin}
7478
- * @memberof Medal */
7767
+ * @memberof Newgrounds */
7479
7768
  let newgrounds;
7480
7769
 
7481
7770
  ///////////////////////////////////////////////////////////////////////////////
7482
7771
  /**
7483
7772
  * Newgrounds medal auto unlocks in newgrounds API
7484
7773
  * @extends Medal
7774
+ * @memberof Newgrounds
7485
7775
  */
7486
7776
  class NewgroundsMedal extends Medal
7487
7777
  {
@@ -7506,6 +7796,7 @@ class NewgroundsMedal extends Medal
7506
7796
  ///////////////////////////////////////////////////////////////////////////////
7507
7797
  /**
7508
7798
  * Newgrounds API object
7799
+ * @memberof Newgrounds
7509
7800
  */
7510
7801
  class NewgroundsPlugin
7511
7802
  {
@@ -7641,99 +7932,119 @@ class NewgroundsPlugin
7641
7932
  /**
7642
7933
  * LittleJS Post Processing Plugin
7643
7934
  * - Supports shadertoy style post processing shaders
7644
- * - call new new PostProcessPlugin() to setup post processing
7935
+ * - call new PostProcessPlugin() to setup post processing
7645
7936
  * - can be enabled to pass other canvases through a final shader
7937
+ * @namespace PostProcess
7646
7938
  */
7647
7939
 
7648
7940
  ///////////////////////////////////////////////////////////////////////////////
7649
7941
 
7650
7942
  /** Global Post Process plugin object
7651
- * @type {PostProcessPlugin} */
7943
+ * @type {PostProcessPlugin}
7944
+ * @memberof PostProcess */
7652
7945
  let postProcess;
7653
7946
 
7654
7947
  /////////////////////////////////////////////////////////////////////////
7655
7948
  /**
7656
7949
  * UI System Global Object
7950
+ * @memberof PostProcess
7657
7951
  */
7658
7952
  class PostProcessPlugin
7659
7953
  {
7660
7954
  /** Create global post processing shader
7661
7955
  * @param {string} shaderCode
7662
7956
  * @param {boolean} [includeOverlay]
7957
+ * @param {boolean} [includeMainCanvas]
7663
7958
  * @example
7664
7959
  * // create the post process plugin object
7665
7960
  * new PostProcessPlugin(shaderCode);
7666
7961
  */
7667
- constructor(shaderCode, includeOverlay=false)
7962
+ constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
7668
7963
  {
7669
7964
  ASSERT(!postProcess, 'Post process already initialized');
7670
7965
  postProcess = this;
7671
7966
 
7672
- if (headlessMode) return;
7673
-
7674
- if (!glEnable)
7675
- {
7676
- console.warn('PostProcessPlugin: WebGL not enabled!');
7677
- return;
7678
- }
7679
-
7680
7967
  if (!shaderCode) // default shader pass through
7681
7968
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
7682
7969
 
7683
7970
  /** @property {WebGLProgram} - Shader for post processing */
7684
- this.shader = glCreateProgram(
7685
- '#version 300 es\n' + // specify GLSL ES version
7686
- 'precision highp float;'+ // use highp for better accuracy
7687
- 'in vec2 p;'+ // position
7688
- 'void main(){'+ // shader entry point
7689
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7690
- '}' // end of shader
7691
- ,
7692
- '#version 300 es\n' + // specify GLSL ES version
7693
- 'precision highp float;'+ // use highp for better accuracy
7694
- 'uniform sampler2D iChannel0;'+ // input texture
7695
- 'uniform vec3 iResolution;'+ // size of output texture
7696
- 'uniform float iTime;'+ // time
7697
- 'out vec4 c;'+ // out color
7698
- '\n' + shaderCode + '\n'+ // insert custom shader code
7699
- 'void main(){'+ // shader entry point
7700
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
7701
- 'c.a=1.;'+ // always use full alpha
7702
- '}' // end of shader
7703
- );
7971
+ this.shader = undefined;
7704
7972
 
7705
7973
  /** @property {WebGLTexture} - Texture for post processing */
7706
- this.texture = glCreateTexture();
7974
+ this.texture = undefined;
7707
7975
 
7708
- /** @property {boolean} - Should overlay canvas be included in post processing */
7709
- this.includeOverlay = includeOverlay;
7976
+ // setup the post processing plugin
7977
+ initPostProcess();
7978
+ engineAddPlugin(undefined, postProcessRender, postProcessContextLost, postProcessContextRestored);
7710
7979
 
7711
- // Render the post processing shader, called automatically by the engine
7712
- engineAddPlugin(undefined, postProcessRender);
7713
- function postProcessRender()
7980
+ function initPostProcess()
7714
7981
  {
7715
7982
  if (headlessMode) return;
7716
-
7717
- // prepare to render post process shader
7718
- if (glEnable)
7719
- {
7720
- glFlush(); // clear out the buffer
7721
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
7722
- }
7723
- else
7983
+
7984
+ if (!glEnable)
7724
7985
  {
7725
- // set the viewport
7726
- glContext.viewport(0, 0, glCanvas.width = drawCanvas.width, glCanvas.height = drawCanvas.height);
7986
+ console.warn('PostProcessPlugin: WebGL not enabled!');
7987
+ return;
7727
7988
  }
7728
7989
 
7729
- if (postProcess.includeOverlay)
7990
+ // create resources
7991
+ postProcess.texture = glCreateTexture();
7992
+ postProcess.shader = glCreateProgram(
7993
+ '#version 300 es\n' + // specify GLSL ES version
7994
+ 'precision highp float;'+ // use highp for better accuracy
7995
+ 'in vec2 p;'+ // position
7996
+ 'void main(){'+ // shader entry point
7997
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7998
+ '}' // end of shader
7999
+ ,
8000
+ '#version 300 es\n' + // specify GLSL ES version
8001
+ 'precision highp float;'+ // use highp for better accuracy
8002
+ 'uniform sampler2D iChannel0;'+ // input texture
8003
+ 'uniform vec3 iResolution;'+ // size of output texture
8004
+ 'uniform float iTime;'+ // time
8005
+ 'out vec4 c;'+ // out color
8006
+ '\n' + shaderCode + '\n'+ // insert custom shader code
8007
+ 'void main(){'+ // shader entry point
8008
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
8009
+ 'c.a=1.;'+ // always use full alpha
8010
+ '}' // end of shader
8011
+ );
8012
+ }
8013
+ function postProcessContextLost()
8014
+ {
8015
+ postProcess.shader = undefined;
8016
+ postProcess.texture = undefined;
8017
+ LOG('PostProcessPlugin: WebGL context lost');
8018
+ }
8019
+ function postProcessContextRestored()
8020
+ {
8021
+ initPostProcess();
8022
+ LOG('PostProcessPlugin: WebGL context restored');
8023
+ }
8024
+ function postProcessRender()
8025
+ {
8026
+ if (headlessMode) return;
8027
+
8028
+ if (!glEnable)
8029
+ return;
8030
+
8031
+ // clear out the buffer
8032
+ glFlush();
8033
+
8034
+ if (includeMainCanvas || includeOverlay)
7730
8035
  {
7731
- // copy overlay canvas so it will be included in post processing
7732
- mainContext.drawImage(overlayCanvas, 0, 0);
7733
- overlayCanvas.width |= 0;
8036
+ // copy WebGL to the main canvas
8037
+ mainContext.drawImage(glCanvas, 0, 0);
8038
+
8039
+ if (includeOverlay)
8040
+ {
8041
+ // copy overlay canvas so it will be included in post processing
8042
+ mainContext.drawImage(overlayCanvas, 0, 0);
8043
+ overlayCanvas.width |= 0; // clear overlay canvas
8044
+ }
7734
8045
  }
7735
8046
 
7736
- // setup shader program to draw one triangle
8047
+ // setup shader program to draw a quad
7737
8048
  glContext.useProgram(postProcess.shader);
7738
8049
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7739
8050
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
@@ -7742,7 +8053,10 @@ class PostProcessPlugin
7742
8053
  // set textures, pass in the 2d canvas and gl canvas in separate texture channels
7743
8054
  glContext.activeTexture(glContext.TEXTURE0);
7744
8055
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
7745
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
8056
+ if (includeMainCanvas || includeOverlay)
8057
+ {
8058
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
8059
+ }
7746
8060
 
7747
8061
  // set vertex position attribute
7748
8062
  const vertexByteStride = 8;
@@ -7761,12 +8075,15 @@ class PostProcessPlugin
7761
8075
  }
7762
8076
  /**
7763
8077
  * LittleJS ZzFXM Plugin
8078
+ * @namespace ZzFXM
7764
8079
  */
7765
8080
 
7766
8081
  /**
7767
8082
  * Music Object - Stores a zzfx music track for later use
7768
8083
  *
7769
8084
  * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
8085
+ * @extends Sound
8086
+ * @memberof ZzFXM
7770
8087
  * @example
7771
8088
  * // create some music
7772
8089
  * const music_example = new Music(
@@ -7825,7 +8142,8 @@ class ZzFXMusic extends Sound
7825
8142
  * @param {Array} patterns - Array of pattern data
7826
8143
  * @param {Array} sequence - Array of pattern indexes
7827
8144
  * @param {number} [BPM] - Playback speed of the song in BPM
7828
- * @return {Array} - Left and right channel sample data */
8145
+ * @return {Array} - Left and right channel sample data
8146
+ * @memberof ZzFXM */
7829
8147
  function zzfxM(instruments, patterns, sequence, BPM = 125)
7830
8148
  {
7831
8149
  let i, j, k;
@@ -7928,17 +8246,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
7928
8246
  * - Buttons
7929
8247
  * - Checkboxes
7930
8248
  * - Images
8249
+ * @namespace UISystem
7931
8250
  */
7932
8251
 
7933
8252
  ///////////////////////////////////////////////////////////////////////////////
7934
8253
 
7935
8254
  /** Global UI system plugin object
7936
- * @type {UISystemPlugin} */
8255
+ * @type {UISystemPlugin}
8256
+ * @memberof UISystem */
7937
8257
  let uiSystem;
7938
8258
 
7939
8259
  ///////////////////////////////////////////////////////////////////////////////
7940
8260
  /**
7941
8261
  * UI System Global Object
8262
+ * @memberof UISystem
7942
8263
  */
7943
8264
  class UISystemPlugin
7944
8265
  {
@@ -7965,6 +8286,8 @@ class UISystemPlugin
7965
8286
  this.defaultHoverColor = hsl(0,0,.9);
7966
8287
  /** @property {Color} - Default color for disabled UI elements */
7967
8288
  this.defaultDisabledColor = hsl(0,0,.3);
8289
+ /** @property {Color} - Uses a gradient fill combined with color */
8290
+ this.defaultGradientColor = undefined;
7968
8291
  /** @property {number} - Default line width for UI elements */
7969
8292
  this.defaultLineWidth = 4;
7970
8293
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -7972,7 +8295,7 @@ class UISystemPlugin
7972
8295
  /** @property {number} - Default scale to use for fitting text to object */
7973
8296
  this.defaultTextScale = .8;
7974
8297
  /** @property {string} - Default font for UI elements */
7975
- this.defaultFont = 'arial';
8298
+ this.defaultFont = fontDefault;
7976
8299
  /** @property {Sound} - Default sound when interactive UI element is pressed */
7977
8300
  this.defaultSoundPress = undefined;
7978
8301
  /** @property {Sound} - Default sound when interactive UI element is released */
@@ -7983,11 +8306,15 @@ class UISystemPlugin
7983
8306
  this.uiObjects = [];
7984
8307
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
7985
8308
  this.uiContext = context;
7986
- /** @property {UIObject} - Top most object user is over */
7987
- this.hoverObject = undefined;
7988
8309
  /** @property {UIObject} - Object user is currently interacting with */
7989
8310
  this.activeObject = undefined;
7990
-
8311
+ /** @property {UIObject} - Top most object user is over */
8312
+ this.hoverObject = undefined;
8313
+ /** @property {UIObject} - Hover object at start of update */
8314
+ this.lastHoverObject = undefined;
8315
+ /** @property {number} - If set ui coords will be renormalized to this canvas height */
8316
+ this.nativeHeight = 0;
8317
+
7991
8318
  engineAddPlugin(uiUpdate, uiRender);
7992
8319
 
7993
8320
  // setup recursive update and render
@@ -8017,6 +8344,7 @@ class UISystemPlugin
8017
8344
  updateInvisibleObject(o);
8018
8345
  }
8019
8346
  // reset hover object at start of update
8347
+ uiSystem.lastHoverObject = uiSystem.hoverObject;
8020
8348
  uiSystem.hoverObject = undefined;
8021
8349
  for (let i = uiSystem.uiObjects.length; i--;)
8022
8350
  {
@@ -8026,6 +8354,17 @@ class UISystemPlugin
8026
8354
  }
8027
8355
  function uiRender()
8028
8356
  {
8357
+ const context = uiSystem.uiContext;
8358
+ context.save();
8359
+ if (uiSystem.nativeHeight)
8360
+ {
8361
+ // convert to native height
8362
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8363
+ context.translate(-s*mainCanvasSize.x/2,0);
8364
+ context.scale(s,s);
8365
+ context.translate(mainCanvasSize.x/2/s,0);
8366
+ }
8367
+
8029
8368
  function renderObject(o)
8030
8369
  {
8031
8370
  if (!o.visible)
@@ -8037,6 +8376,7 @@ class UISystemPlugin
8037
8376
  renderObject(c);
8038
8377
  }
8039
8378
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
8379
+ context.restore();
8040
8380
  }
8041
8381
  }
8042
8382
 
@@ -8046,11 +8386,30 @@ class UISystemPlugin
8046
8386
  * @param {Color} [color=uiSystem.defaultColor]
8047
8387
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
8048
8388
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
8049
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
8050
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
8051
- {
8389
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
8390
+ * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
8391
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
8392
+ {
8393
+ ASSERT(isVector2(pos), 'pos must be a vec2');
8394
+ ASSERT(isVector2(size), 'size must be a vec2');
8395
+ ASSERT(isColor(color), 'color must be a color');
8396
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
8397
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
8398
+ ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
8399
+
8052
8400
  const context = uiSystem.uiContext;
8053
- context.fillStyle = color.toString();
8401
+ if (gradientColor)
8402
+ {
8403
+ const g = context.createLinearGradient(
8404
+ pos.x, pos.y-size.y/2, pos.x, pos.y+size.y/2);
8405
+ const c = color.toString();
8406
+ g.addColorStop(0, c);
8407
+ g.addColorStop(.5, gradientColor.toString());
8408
+ g.addColorStop(1, c);
8409
+ context.fillStyle = g;
8410
+ }
8411
+ else
8412
+ context.fillStyle = color.toString();
8054
8413
  context.beginPath();
8055
8414
  if (cornerRadius && context['roundRect'])
8056
8415
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
@@ -8072,6 +8431,11 @@ class UISystemPlugin
8072
8431
  * @param {Color} [lineColor=uiSystem.defaultLineColor] */
8073
8432
  drawLine(posA, posB, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor)
8074
8433
  {
8434
+ ASSERT(isVector2(posA), 'posA must be a vec2');
8435
+ ASSERT(isVector2(posB), 'posB must be a vec2');
8436
+ ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
8437
+ ASSERT(isColor(lineColor), 'lineColor must be a color');
8438
+
8075
8439
  const context = uiSystem.uiContext;
8076
8440
  context.strokeStyle = lineColor.toString();
8077
8441
  context.lineWidth = lineWidth;
@@ -8102,17 +8466,43 @@ class UISystemPlugin
8102
8466
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
8103
8467
  * @param {string} [align]
8104
8468
  * @param {string} [font=uiSystem.defaultFont]
8469
+ * @param {string} [fontStyle]
8105
8470
  * @param {boolean} [applyMaxWidth=true] */
8106
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, applyMaxWidth=true)
8471
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
8107
8472
  {
8108
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8473
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8474
+ }
8475
+
8476
+ /**
8477
+ * @callback DragAndDropCallback - Callback for drag and drop events
8478
+ * @param {DragEvent} event - The drag event
8479
+ * @memberof UISystem
8480
+ */
8481
+
8482
+ /** Setup drag and drop event handlers
8483
+ * Automatically prevents defaults and calls the given functions
8484
+ * @param {DragAndDropCallback} [onDrop] - when a file is dropped
8485
+ * @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
8486
+ * @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
8487
+ * @param {DragAndDropCallback} [onDragOver] - continously when dragging over */
8488
+ setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
8489
+ {
8490
+ function setCallback(callback, listenerType)
8491
+ {
8492
+ function listener(e) { e.preventDefault(); callback && callback(e); }
8493
+ document.addEventListener(listenerType, listener);
8494
+ }
8495
+ setCallback(onDrop, 'drop');
8496
+ setCallback(onDragEnter, 'dragenter');
8497
+ setCallback(onDragLeave, 'dragleave');
8498
+ setCallback(onDragOver, 'dragover');
8109
8499
  }
8110
8500
  }
8111
8501
 
8112
8502
  ///////////////////////////////////////////////////////////////////////////////
8113
8503
  /**
8114
8504
  * UI Object - Base level object for all UI elements
8115
- */
8505
+ * @memberof UISystem */
8116
8506
  class UIObject
8117
8507
  {
8118
8508
  /** Create a UIObject
@@ -8121,6 +8511,9 @@ class UIObject
8121
8511
  */
8122
8512
  constructor(pos=vec2(), size=vec2())
8123
8513
  {
8514
+ ASSERT(isVector2(pos), 'ui object pos must be a vec2');
8515
+ ASSERT(isVector2(size), 'ui object size must be a vec2');
8516
+
8124
8517
  /** @property {Vector2} - Local position of the object */
8125
8518
  this.localPos = pos.copy();
8126
8519
  /** @property {Vector2} - Screen space position of the object */
@@ -8128,27 +8521,31 @@ class UIObject
8128
8521
  /** @property {Vector2} - Screen space size of the object */
8129
8522
  this.size = size.copy();
8130
8523
  /** @property {Color} - Color of the object */
8131
- this.color = uiSystem.defaultColor;
8524
+ this.color = uiSystem.defaultColor.copy();
8132
8525
  /** @property {Color} - Color of the object when active, uses color if undefined */
8133
8526
  this.activeColor = undefined;
8134
8527
  /** @property {string} - Text for this ui object */
8135
8528
  this.text = undefined;
8136
8529
  /** @property {Color} - Color when disabled */
8137
- this.disabledColor = uiSystem.defaultDisabledColor;
8530
+ this.disabledColor = uiSystem.defaultDisabledColor.copy();
8138
8531
  /** @property {boolean} - Is this object disabled? */
8139
8532
  this.disabled = false;
8140
8533
  /** @property {Color} - Color for text */
8141
- this.textColor = uiSystem.defaultTextColor;
8534
+ this.textColor = uiSystem.defaultTextColor.copy();
8142
8535
  /** @property {Color} - Color used when hovering over the object */
8143
- this.hoverColor = uiSystem.defaultHoverColor;
8536
+ this.hoverColor = uiSystem.defaultHoverColor.copy();
8144
8537
  /** @property {Color} - Color for line drawing */
8145
- this.lineColor = uiSystem.defaultLineColor;
8538
+ this.lineColor = uiSystem.defaultLineColor.copy();
8539
+ /** @property {Color} - Uses a gradient fill combined with color */
8540
+ this.gradientColor = uiSystem.defaultGradientColor ? uiSystem.defaultGradientColor.copy() : undefined;
8146
8541
  /** @property {number} - Width for line drawing */
8147
8542
  this.lineWidth = uiSystem.defaultLineWidth;
8148
8543
  /** @property {number} - Corner radius for rounded rects */
8149
8544
  this.cornerRadius = uiSystem.defaultCornerRadius;
8150
8545
  /** @property {string} - Font for this objecct */
8151
8546
  this.font = uiSystem.defaultFont;
8547
+ /** @property {string} - Font style for this object or undefined */
8548
+ this.fontStyle = undefined;
8152
8549
  /** @property {number} - Override for text width */
8153
8550
  this.textWidth = undefined;
8154
8551
  /** @property {number} - Override for text height */
@@ -8173,6 +8570,8 @@ class UIObject
8173
8570
  this.interactive = false;
8174
8571
  /** @property {boolean} - Activate when dragged over with mouse held down */
8175
8572
  this.dragActivate = false;
8573
+ /** @property {boolean} - True if this can be a hover object */
8574
+ this.canBeHover = true;
8176
8575
  uiSystem.uiObjects.push(this);
8177
8576
  }
8178
8577
 
@@ -8196,31 +8595,47 @@ class UIObject
8196
8595
  child.parent = undefined;
8197
8596
  }
8198
8597
 
8598
+ /** Check if the mouse is overlapping a box in screen space
8599
+ * @return {boolean} - True if overlapping
8600
+ */
8601
+ isMouseOverlapping()
8602
+ {
8603
+ const size = !isTouchDevice ? this.size :
8604
+ this.size.add(vec2(this.extraTouchSize || 0));
8605
+ if (!uiSystem.nativeHeight)
8606
+ return isOverlapping(this.pos, size, mousePosScreen);
8607
+
8608
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8609
+ const sInv = 1/s;
8610
+ let pos = mousePosScreen.copy();
8611
+ pos.x += s*mainCanvasSize.x/2;
8612
+ pos.x *= sInv;
8613
+ pos.y *= sInv;
8614
+ pos.x -= sInv*mainCanvasSize.x/2;
8615
+ return isOverlapping(this.pos, size, pos);
8616
+ }
8617
+
8199
8618
  /** Update the object, called automatically by plugin once each frame */
8200
8619
  update()
8201
8620
  {
8202
- const wasHover = this.isHoverObject();
8621
+ const wasHover = uiSystem.lastHoverObject === this;
8203
8622
  const isActive = this.isActiveObject();
8204
8623
  const mouseDown = mouseIsDown(0);
8205
8624
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
8206
- if (!uiSystem.hoverObject)
8625
+ if (this.canBeHover)
8207
8626
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
8208
- {
8209
- const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
8210
- if (isOverlapping(this.pos, size, mousePosScreen))
8211
- uiSystem.hoverObject = this;
8212
- }
8627
+ if (!uiSystem.hoverObject && this.isMouseOverlapping())
8628
+ uiSystem.hoverObject = this;
8213
8629
  if (this.isHoverObject())
8214
8630
  {
8215
- if (mousePress)
8216
- inputClearKey(0,0,0,1,0); // clear mouse was pressed state
8217
8631
  if (!this.disabled)
8218
8632
  {
8219
8633
  if (mousePress)
8220
8634
  {
8221
8635
  if (this.interactive)
8222
8636
  {
8223
- this.onPress();
8637
+ if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
8638
+ this.onPress();
8224
8639
  if (this.soundPress)
8225
8640
  this.soundPress.play();
8226
8641
  if (uiSystem.activeObject && !isActive)
@@ -8228,13 +8643,15 @@ class UIObject
8228
8643
  uiSystem.activeObject = this;
8229
8644
  }
8230
8645
  }
8231
- if (!mouseDown && uiSystem.activeObject === this && this.interactive)
8646
+ if (!mouseDown && this.isActiveObject() && this.interactive)
8232
8647
  {
8233
8648
  this.onClick();
8234
8649
  if (this.soundClick)
8235
8650
  this.soundClick.play();
8236
8651
  }
8237
8652
  }
8653
+ // clear mouse was pressed state even when disabled
8654
+ mousePress && inputClearKey(0,0,0,1,0);
8238
8655
  }
8239
8656
  if (isActive)
8240
8657
  if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
@@ -8245,6 +8662,7 @@ class UIObject
8245
8662
  uiSystem.activeObject = undefined;
8246
8663
  }
8247
8664
 
8665
+ // call enter/leave events
8248
8666
  if (this.isHoverObject() !== wasHover)
8249
8667
  this.isHoverObject() ? this.onEnter() : this.onLeave();
8250
8668
  }
@@ -8255,7 +8673,7 @@ class UIObject
8255
8673
  if (!this.size.x || !this.size.y) return;
8256
8674
 
8257
8675
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8258
- const color = this.interactive ? this.disabled ? this.disabledColor : this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8676
+ const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8259
8677
  uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
8260
8678
  }
8261
8679
 
@@ -8306,6 +8724,7 @@ class UIObject
8306
8724
  /**
8307
8725
  * UIText - A UI object that displays text
8308
8726
  * @extends UIObject
8727
+ * @memberof UISystem
8309
8728
  */
8310
8729
  class UIText extends UIObject
8311
8730
  {
@@ -8320,6 +8739,10 @@ class UIText extends UIObject
8320
8739
  {
8321
8740
  super(pos, size);
8322
8741
 
8742
+ ASSERT(isString(text), 'ui text must be a string');
8743
+ ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
8744
+ ASSERT(isString(font), 'ui text font must be a string');
8745
+
8323
8746
  // set properties
8324
8747
  this.text = text;
8325
8748
  this.align = align;
@@ -8327,11 +8750,13 @@ class UIText extends UIObject
8327
8750
 
8328
8751
  // make text not outlined by default
8329
8752
  this.lineWidth = 0;
8753
+ // text can not be a hover object by default
8754
+ this.canBeHover = false;
8330
8755
  }
8331
8756
  render()
8332
8757
  {
8333
8758
  const textSize = this.getTextSize();
8334
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
8759
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
8335
8760
  }
8336
8761
  }
8337
8762
 
@@ -8339,6 +8764,7 @@ class UIText extends UIObject
8339
8764
  /**
8340
8765
  * UITile - A UI object that displays a tile image
8341
8766
  * @extends UIObject
8767
+ * @memberof UISystem
8342
8768
  */
8343
8769
  class UITile extends UIObject
8344
8770
  {
@@ -8353,15 +8779,19 @@ class UITile extends UIObject
8353
8779
  constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false)
8354
8780
  {
8355
8781
  super(pos, size);
8782
+
8783
+ ASSERT(tileInfo instanceof TileInfo, 'ui tile tileInfo must be a TileInfo');
8784
+ ASSERT(isColor(color), 'ui tile color must be a color');
8785
+ ASSERT(isNumber(angle), 'ui tile angle must be a number');
8786
+
8356
8787
  /** @property {TileInfo} - Tile image to use */
8357
8788
  this.tileInfo = tileInfo;
8358
8789
  /** @property {number} - Angle to rotate in radians */
8359
8790
  this.angle = angle;
8360
8791
  /** @property {boolean} - Should it be mirrored? */
8361
8792
  this.mirror = mirror;
8362
-
8363
8793
  // set properties
8364
- this.color = color;
8794
+ this.color = color.copy();
8365
8795
  }
8366
8796
  render()
8367
8797
  {
@@ -8373,6 +8803,7 @@ class UITile extends UIObject
8373
8803
  /**
8374
8804
  * UIButton - A UI object that acts as a button
8375
8805
  * @extends UIObject
8806
+ * @memberof UISystem
8376
8807
  */
8377
8808
  class UIButton extends UIObject
8378
8809
  {
@@ -8386,9 +8817,12 @@ class UIButton extends UIObject
8386
8817
  {
8387
8818
  super(pos, size);
8388
8819
 
8820
+ ASSERT(isString(text), 'ui button must be a string');
8821
+ ASSERT(isColor(color), 'ui button color must be a color');
8822
+
8389
8823
  // set properties
8390
8824
  this.text = text;
8391
- this.color = color;
8825
+ this.color = color.copy();
8392
8826
  this.interactive = true;
8393
8827
  }
8394
8828
  render()
@@ -8398,7 +8832,7 @@ class UIButton extends UIObject
8398
8832
  // draw the text scaled to fit
8399
8833
  const textSize = this.getTextSize();
8400
8834
  uiSystem.drawText(this.text, this.pos, textSize,
8401
- this.textColor, 0, undefined, this.align, this.font);
8835
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8402
8836
  }
8403
8837
  }
8404
8838
 
@@ -8406,6 +8840,7 @@ class UIButton extends UIObject
8406
8840
  /**
8407
8841
  * UICheckbox - A UI object that acts as a checkbox
8408
8842
  * @extends UIObject
8843
+ * @memberof UISystem
8409
8844
  */
8410
8845
  class UICheckbox extends UIObject
8411
8846
  {
@@ -8419,12 +8854,15 @@ class UICheckbox extends UIObject
8419
8854
  constructor(pos, size, checked=false, text='', color=uiSystem.defaultButtonColor)
8420
8855
  {
8421
8856
  super(pos, size);
8857
+
8858
+ ASSERT(isString(text), 'ui checkbox must be a string');
8859
+ ASSERT(isColor(color), 'ui checkbox color must be a color');
8860
+
8422
8861
  /** @property {boolean} - Current percentage value of this scrollbar 0-1 */
8423
8862
  this.checked = checked;
8424
-
8425
8863
  // set properties
8426
8864
  this.text = text;
8427
- this.color = color;
8865
+ this.color = color.copy();
8428
8866
  this.interactive = true;
8429
8867
  }
8430
8868
  onClick()
@@ -8448,7 +8886,7 @@ class UICheckbox extends UIObject
8448
8886
  const textSize = this.getTextSize();
8449
8887
  const pos = this.pos.add(vec2(this.size.x,0));
8450
8888
  uiSystem.drawText(this.text, pos, textSize,
8451
- this.textColor, 0, undefined, 'left', this.font, false);
8889
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
8452
8890
  }
8453
8891
  }
8454
8892
 
@@ -8456,6 +8894,7 @@ class UICheckbox extends UIObject
8456
8894
  /**
8457
8895
  * UIScrollbar - A UI object that acts as a scrollbar
8458
8896
  * @extends UIObject
8897
+ * @memberof UISystem
8459
8898
  */
8460
8899
  class UIScrollbar extends UIObject
8461
8900
  {
@@ -8471,14 +8910,19 @@ class UIScrollbar extends UIObject
8471
8910
  {
8472
8911
  super(pos, size);
8473
8912
 
8913
+ ASSERT(isNumber(value), 'ui scrollbar value must be a number');
8914
+ ASSERT(isString(text), 'ui scrollbar must be a string');
8915
+ ASSERT(isColor(color), 'ui scrollbar color must be a color');
8916
+ ASSERT(isColor(handleColor), 'ui scrollbar handleColor must be a color');
8917
+
8474
8918
  /** @property {number} - Current percentage value of this scrollbar 0-1 */
8475
8919
  this.value = value;
8476
8920
  /** @property {Color} - Color for the handle part of the scrollbar */
8477
- this.handleColor = handleColor;
8921
+ this.handleColor = handleColor.copy();
8478
8922
 
8479
8923
  // set properties
8480
8924
  this.text = text;
8481
- this.color = color;
8925
+ this.color = color.copy();
8482
8926
  this.interactive = true;
8483
8927
  }
8484
8928
  update()
@@ -8486,34 +8930,47 @@ class UIScrollbar extends UIObject
8486
8930
  super.update();
8487
8931
  if (this.isActiveObject() && this.interactive)
8488
8932
  {
8933
+ // handle horizontal or vertical scrollbar
8934
+ const isHorizontal = this.size.x > this.size.y;
8935
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8936
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8937
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8938
+
8489
8939
  // check if value changed
8490
- const handleSize = vec2(this.size.y);
8491
- const handleWidth = this.size.x - handleSize.x;
8492
- const p1 = this.pos.x - handleWidth/2;
8493
- const p2 = this.pos.x + handleWidth/2;
8940
+ const handleWidth = barSize - handleSize;
8941
+ const p1 = centerPos - handleWidth/2;
8942
+ const p2 = centerPos + handleWidth/2;
8494
8943
  const oldValue = this.value;
8495
- this.value = percent(mousePosScreen.x, p1, p2);
8944
+ this.value = isHorizontal ?
8945
+ percent(mousePosScreen.x, p1, p2) :
8946
+ percent(mousePosScreen.y, p2, p1);
8496
8947
  this.value === oldValue || this.onChange();
8497
8948
  }
8498
8949
  }
8499
8950
  render()
8500
8951
  {
8501
8952
  super.render();
8502
-
8953
+
8954
+ // handle horizontal or vertical scrollbar
8955
+ const isHorizontal = this.size.x > this.size.y;
8956
+ const handleSize = isHorizontal ? this.size.y : this.size.x;
8957
+ const barSize = isHorizontal ? this.size.x : this.size.y;
8958
+ const centerPos = isHorizontal ? this.pos.x : this.pos.y;
8959
+
8503
8960
  // draw the scrollbar handle
8504
- const handleSize = vec2(this.size.y);
8505
- const handleWidth = this.size.x - handleSize.x;
8506
- const p1 = this.pos.x - handleWidth/2;
8507
- const p2 = this.pos.x + handleWidth/2;
8508
- const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
8509
- const handleColor = this.disabled ? this.disabledColor :
8510
- this.interactive && this.isActiveObject() ? this.color : this.handleColor;
8511
- uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8961
+ const handleWidth = barSize - handleSize;
8962
+ const p1 = centerPos - handleWidth/2;
8963
+ const p2 = centerPos + handleWidth/2;
8964
+ const handlePos = isHorizontal ?
8965
+ vec2(lerp(p1, p2, this.value), this.pos.y) :
8966
+ vec2(this.pos.x, lerp(p2, p1, this.value))
8967
+ const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8968
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8512
8969
 
8513
8970
  // draw the text scaled to fit on the scrollbar
8514
8971
  const textSize = this.getTextSize();
8515
8972
  uiSystem.drawText(this.text, this.pos, textSize,
8516
- this.textColor, 0, undefined, this.align, this.font);
8973
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8517
8974
  }
8518
8975
  }
8519
8976
  /**
@@ -8555,6 +9012,7 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
8555
9012
  * - Each object has a Box2D body which can have multiple fixtures and joints
8556
9013
  * - Provides interface for Box2D body and fixture functions
8557
9014
  * @extends EngineObject
9015
+ * @memberof Box2D
8558
9016
  */
8559
9017
  class Box2dObject extends EngineObject
8560
9018
  {
@@ -8566,7 +9024,7 @@ class Box2dObject extends EngineObject
8566
9024
  * @param {Color} [color]
8567
9025
  * @param {number} [bodyType]
8568
9026
  * @param {number} [renderOrder] */
8569
- constructor(pos=vec2(), size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
9027
+ constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8570
9028
  {
8571
9029
  super(pos, size, tileInfo, angle, color, renderOrder);
8572
9030
 
@@ -8580,7 +9038,7 @@ class Box2dObject extends EngineObject
8580
9038
  this.lineColor = BLACK;
8581
9039
  }
8582
9040
 
8583
- /** Destroy this object and it's physics body */
9041
+ /** Destroy this object and its physics body */
8584
9042
  destroy()
8585
9043
  {
8586
9044
  // destroy physics body, fixtures, and joints
@@ -9064,6 +9522,7 @@ class Box2dRaycastResult
9064
9522
  * Box2D Joint
9065
9523
  * - Base class for Box2D joints
9066
9524
  * - A joint is used to connect objects together
9525
+ * @memberof Box2D
9067
9526
  */
9068
9527
  class Box2dJoint
9069
9528
  {
@@ -9119,6 +9578,7 @@ class Box2dJoint
9119
9578
  * - This a soft constraint with a max force
9120
9579
  * - This allows the constraint to stretch and without applying huge forces
9121
9580
  * @extends Box2dJoint
9581
+ * @memberof Box2D
9122
9582
  */
9123
9583
  class Box2dTargetJoint extends Box2dJoint
9124
9584
  {
@@ -9168,6 +9628,7 @@ class Box2dTargetJoint extends Box2dJoint
9168
9628
  * - Constrains two points on two objects to remain at a fixed distance
9169
9629
  * - You can view this as a massless, rigid rod
9170
9630
  * @extends Box2dJoint
9631
+ * @memberof Box2D
9171
9632
  */
9172
9633
  class Box2dDistanceJoint extends Box2dJoint
9173
9634
  {
@@ -9231,6 +9692,7 @@ class Box2dDistanceJoint extends Box2dJoint
9231
9692
  * Box2D Pin Joint
9232
9693
  * - Pins two objects together at a point
9233
9694
  * @extends Box2dDistanceJoint
9695
+ * @memberof Box2D
9234
9696
  */
9235
9697
  class Box2dPinJoint extends Box2dDistanceJoint
9236
9698
  {
@@ -9250,6 +9712,7 @@ class Box2dPinJoint extends Box2dDistanceJoint
9250
9712
  * Box2D Rope Joint
9251
9713
  * - Enforces a maximum distance between two points on two objects
9252
9714
  * @extends Box2dJoint
9715
+ * @memberof Box2D
9253
9716
  */
9254
9717
  class Box2dRopeJoint extends Box2dJoint
9255
9718
  {
@@ -9302,6 +9765,7 @@ class Box2dRopeJoint extends Box2dJoint
9302
9765
  * - You can use a motor to drive the relative rotation about the shared point
9303
9766
  * - A maximum motor torque is provided so that infinite forces are not generated
9304
9767
  * @extends Box2dJoint
9768
+ * @memberof Box2D
9305
9769
  */
9306
9770
  class Box2dRevoluteJoint extends Box2dJoint
9307
9771
  {
@@ -9403,6 +9867,7 @@ class Box2dRevoluteJoint extends Box2dJoint
9403
9867
  * - Either joint can be a revolute or prismatic joint
9404
9868
  * - You specify a gear ratio to bind the motions together
9405
9869
  * @extends Box2dJoint
9870
+ * @memberof Box2D
9406
9871
  */
9407
9872
  class Box2dGearJoint extends Box2dJoint
9408
9873
  {
@@ -9451,6 +9916,7 @@ class Box2dGearJoint extends Box2dJoint
9451
9916
  * - You can use a joint limit to restrict the range of motion
9452
9917
  * - You can use a joint motor to drive the motion or to model joint friction
9453
9918
  * @extends Box2dJoint
9919
+ * @memberof Box2D
9454
9920
  */
9455
9921
  class Box2dPrismaticJoint extends Box2dJoint
9456
9922
  {
@@ -9560,6 +10026,7 @@ class Box2dPrismaticJoint extends Box2dJoint
9560
10026
  * - You can use a joint motor to drive the motion or to model joint friction
9561
10027
  * - This joint is designed for vehicle suspensions
9562
10028
  * @extends Box2dJoint
10029
+ * @memberof Box2D
9563
10030
  */
9564
10031
  class Box2dWheelJoint extends Box2dJoint
9565
10032
  {
@@ -9655,6 +10122,7 @@ class Box2dWheelJoint extends Box2dJoint
9655
10122
  * Box2D Weld Joint
9656
10123
  * - Glues two objects together
9657
10124
  * @extends Box2dJoint
10125
+ * @memberof Box2D
9658
10126
  */
9659
10127
  class Box2dWeldJoint extends Box2dJoint
9660
10128
  {
@@ -9713,6 +10181,7 @@ class Box2dWeldJoint extends Box2dJoint
9713
10181
  * - Used to apply top-down friction
9714
10182
  * - Provides 2D translational friction and angular friction
9715
10183
  * @extends Box2dJoint
10184
+ * @memberof Box2D
9716
10185
  */
9717
10186
  class Box2dFrictionJoint extends Box2dJoint
9718
10187
  {
@@ -9767,6 +10236,7 @@ class Box2dFrictionJoint extends Box2dJoint
9767
10236
  * - The pulley supports a ratio such that: length1 + ratio * length2 <= constant
9768
10237
  * - The force transmitted is scaled by the ratio
9769
10238
  * @extends Box2dJoint
10239
+ * @memberof Box2D
9770
10240
  */
9771
10241
  class Box2dPulleyJoint extends Box2dJoint
9772
10242
  {
@@ -9834,6 +10304,7 @@ class Box2dPulleyJoint extends Box2dJoint
9834
10304
  * - Controls the relative motion between two objects
9835
10305
  * - Typical usage is to control the movement of a object with respect to the ground
9836
10306
  * @extends Box2dJoint
10307
+ * @memberof Box2D
9837
10308
  */
9838
10309
  class Box2dMotorJoint extends Box2dJoint
9839
10310
  {
@@ -9897,6 +10368,7 @@ class Box2dMotorJoint extends Box2dJoint
9897
10368
  /**
9898
10369
  * Box2D Global Object
9899
10370
  * - Wraps Box2d world and provides global functions
10371
+ * @memberof Box2D
9900
10372
  */
9901
10373
  class Box2dPlugin
9902
10374
  {
@@ -10615,6 +11087,7 @@ export
10615
11087
  isColor,
10616
11088
  isVector2,
10617
11089
  isNumber,
11090
+ isString,
10618
11091
 
10619
11092
  // Default Colors
10620
11093
  WHITE,
@@ -10750,7 +11223,7 @@ export
10750
11223
  tileCollisionGetData,
10751
11224
  tileCollisionTest,
10752
11225
  tileCollisionRaycast,
10753
- tileCollisionLoad,
11226
+ tileLayersLoad,
10754
11227
  TileLayerData,
10755
11228
  CanvasLayer,
10756
11229
  TileLayer,