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
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {string}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.14.11';
33
+ const engineVersion = '1.14.19';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -91,29 +91,60 @@ let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
91
91
  ///////////////////////////////////////////////////////////////////////////////
92
92
  // plugin hooks
93
93
 
94
- const pluginUpdateList = [], pluginRenderList = [];
94
+ const pluginList = [];
95
+ class EnginePlugin
96
+ {
97
+ constructor(update, render, glContextLost, glContextRestored)
98
+ {
99
+ this.update = update;
100
+ this.render = render;
101
+ this.glContextLost = glContextLost;
102
+ this.glContextRestored = glContextRestored;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @callback PluginCallback - Update or render function for a plugin
108
+ * @memberof Engine
109
+ */
95
110
 
96
111
  /** Add a new update function for a plugin
97
- * @param {Function} [updateFunction]
98
- * @param {Function} [renderFunction]
112
+ * @param {PluginCallback} [update]
113
+ * @param {PluginCallback} [render]
114
+ * @param {PluginCallback} [glContextLost]
115
+ * @param {PluginCallback} [glContextRestored]
99
116
  * @memberof Engine */
100
- function engineAddPlugin(updateFunction, renderFunction)
117
+ function engineAddPlugin(update, render, glContextLost, glContextRestored)
101
118
  {
102
- ASSERT(!pluginUpdateList.includes(updateFunction));
103
- ASSERT(!pluginRenderList.includes(renderFunction));
104
- updateFunction && pluginUpdateList.push(updateFunction);
105
- renderFunction && pluginRenderList.push(renderFunction);
119
+ // make sure plugin functions are unique
120
+ ASSERT(!pluginList.find(p=>
121
+ p.update === update && p.render === render &&
122
+ p.glContextLost === glContextLost &&
123
+ p.glContextRestored === glContextRestored));
124
+
125
+ const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
126
+ pluginList.push(plugin);
106
127
  }
107
128
 
108
129
  ///////////////////////////////////////////////////////////////////////////////
109
130
  // Main Engine Functions
110
131
 
132
+ /**
133
+ * @callback GameInitCallback - Called after the engine starts, can be async
134
+ * @returns {void|Promise<void>}
135
+ * @memberof Engine
136
+ */
137
+ /**
138
+ * @callback GameCallback - Update or render function for the game
139
+ * @memberof Engine
140
+ */
141
+
111
142
  /** Startup LittleJS engine with your callback functions
112
- * @param {Function|function():Promise} gameInit - Called once after the engine starts up, can be async for loading
113
- * @param {Function} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
114
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
115
- * @param {Function} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
116
- * @param {Function} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
143
+ * @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
144
+ * @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
145
+ * @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
146
+ * @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
147
+ * @param {GameCallback} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
117
148
  * @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
118
149
  * @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
119
150
  * @example
@@ -177,7 +208,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
177
208
  wasUpdated = true;
178
209
  updateCanvas();
179
210
  inputUpdate();
180
- pluginUpdateList.forEach(f=>f());
211
+ pluginList.forEach(plugin=>plugin.update?.());
181
212
 
182
213
  // update object transforms even when paused
183
214
  for (const o of engineObjects)
@@ -210,7 +241,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
210
241
  updateCanvas();
211
242
  inputUpdate();
212
243
  gameUpdate();
213
- pluginUpdateList.forEach(f=>f());
244
+ pluginList.forEach(plugin=>plugin.update?.());
214
245
  engineObjectsUpdate();
215
246
 
216
247
  // do post update
@@ -244,7 +275,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
244
275
  for (const o of engineObjects)
245
276
  o.destroyed || o.render();
246
277
  gameRenderPost();
247
- pluginRenderList.forEach(f=>f());
278
+ pluginList.forEach(plugin=>plugin.render?.());
248
279
  touchGamepadRender();
249
280
  debugRender();
250
281
  glFlush();
@@ -365,11 +396,12 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
365
396
  const promises = imageSources.map((src, textureIndex)=>
366
397
  new Promise(resolve =>
367
398
  {
399
+ ASSERT(isString(src), 'imageSources must be an array of strings');
400
+
368
401
  const image = new Image;
369
402
  image.onerror = image.onload = ()=>
370
403
  {
371
404
  const textureInfo = new TextureInfo(image);
372
- textureInfo.createWebGLTexture();
373
405
  textureInfos[textureIndex] = textureInfo;
374
406
  resolve();
375
407
  }
@@ -385,7 +417,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
385
417
  {
386
418
  const textureInfo = new TextureInfo(new Image);
387
419
  textureInfos[0] = textureInfo;
388
- textureInfo.createWebGLTexture();
389
420
  resolve();
390
421
  }));
391
422
  }
@@ -396,7 +427,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
396
427
  promises.push(new Promise(resolve =>
397
428
  {
398
429
  let t = 0;
399
- LOG(`${engineName} Engine v${engineVersion}`);
430
+ console.log(`${engineName} Engine v${engineVersion}`);
400
431
  updateSplash();
401
432
  function updateSplash()
402
433
  {
@@ -480,10 +511,16 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
480
511
  return collectedObjects;
481
512
  }
482
513
 
514
+ /**
515
+ * @callback ObjectCallbackFunction - Function that processes an object
516
+ * @param {EngineObject} uiObjects
517
+ * @memberof Engine
518
+ */
519
+
483
520
  /** Triggers a callback for each object within a given area
484
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
485
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
486
- * @param {Function} [callbackFunction] - Calls this function on every object that passes the test
521
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
522
+ * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
523
+ * @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test
487
524
  * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
488
525
  * @memberof Engine */
489
526
  function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
@@ -47,6 +47,7 @@ function audioInit()
47
47
  * Sound Object - Stores a sound for later use and can be played positionally
48
48
  *
49
49
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
50
+ * @memberof Audio
50
51
  * @example
51
52
  * // create a sound
52
53
  * const sound_example = new Sound([.5,.5]);
@@ -141,12 +142,12 @@ class Sound
141
142
 
142
143
  /** Play the sound as a musical note with a semitone offset
143
144
  * This can be used to play music with chromatic scales
144
- * @param {number} semitoneOffset - How many semitones to offset pitch
145
+ * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
145
146
  * @param {Vector2} [pos] - World space position to play the sound if any
146
147
  * @param {number} [volume=1] - How much to scale volume by
147
148
  * @return {SoundInstance} - The audio source node
148
149
  */
149
- playNote(semitoneOffset, pos, volume)
150
+ playNote(semitoneOffset=0, pos, volume)
150
151
  {
151
152
  const pitch = getNoteFrequency(semitoneOffset, 1);
152
153
  return this.play(pos, volume, pitch, 0);
@@ -169,6 +170,8 @@ class Sound
169
170
  /**
170
171
  * Sound Wave Object - Stores a wave sound for later use and can be played positionally
171
172
  * - this can be used to play wave, mp3, and ogg files
173
+ * @extends Sound
174
+ * @memberof Audio
172
175
  * @example
173
176
  * // create a sound
174
177
  * const sound_example = new SoundWave('sound.mp3');
@@ -178,22 +181,29 @@ class Sound
178
181
  */
179
182
  class SoundWave extends Sound
180
183
  {
184
+ /**
185
+ * @callback SoundLoadCallback - Function called when sound is loaded
186
+ * @param {SoundWave} sound
187
+ * @memberof Audio
188
+ */
189
+
181
190
  /** Create a sound object and cache the wave file for later use
182
191
  * @param {string} filename - Filename of audio file to load
183
192
  * @param {number} [randomness] - How much to randomize frequency each time sound plays
184
193
  * @param {number} [range=soundDefaultRange] - World space max range of sound
185
194
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
186
- * @param {Function} [onloadCallback] - callback function to call when sound is loaded
195
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
187
196
  */
188
197
  constructor(filename, randomness=0, range, taper, onloadCallback)
189
198
  {
190
199
  super(undefined, range, taper);
191
200
  if (!soundEnable || headlessMode) return;
201
+ ASSERT(!filename || isString(filename), 'filename must be a string');
192
202
 
193
- /** @property {Function} - callback function to call when sound is loaded */
203
+ /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
194
204
  this.onloadCallback = onloadCallback;
195
205
  this.randomness = randomness;
196
- this.loadSound(filename);
206
+ filename && this.loadSound(filename);
197
207
  }
198
208
 
199
209
  /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
@@ -202,6 +212,8 @@ class SoundWave extends Sound
202
212
  async loadSound(filename)
203
213
  {
204
214
  const response = await fetch(filename);
215
+ if (!response.ok)
216
+ throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);
205
217
  const arrayBuffer = await response.arrayBuffer();
206
218
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
207
219
 
@@ -237,7 +249,7 @@ class SoundWave extends Sound
237
249
  this.sampleChannels = sampleChannels;
238
250
  this.loadedPercent = 1;
239
251
  if (this.onloadCallback)
240
- this.onloadCallback();
252
+ this.onloadCallback(this);
241
253
  }
242
254
  }
243
255
 
@@ -246,6 +258,7 @@ class SoundWave extends Sound
246
258
  /**
247
259
  * Sound Instance - Wraps an AudioBufferSourceNode for individual sound control
248
260
  * Represents a single playing instance of a sound with pause/resume capabilities
261
+ * @memberof Audio
249
262
  * @example
250
263
  * // Play a sound and get an instance for control
251
264
  * const jumpSound = new Sound([.5,.5,220]);
@@ -311,8 +324,16 @@ class SoundInstance
311
324
  this.stop();
312
325
  this.gainNode = audioContext.createGain();
313
326
  this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
314
- this.startTime = audioContext.currentTime - offset;
315
- this.pausedTime = undefined;
327
+ if (this.source)
328
+ {
329
+ this.startTime = audioContext.currentTime - offset;
330
+ this.pausedTime = undefined;
331
+ }
332
+ else
333
+ {
334
+ this.startTime = undefined;
335
+ this.pausedTime = 0;
336
+ }
316
337
  }
317
338
 
318
339
  /** Set the volume of this sound instance
@@ -445,6 +466,12 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
445
466
 
446
467
  ///////////////////////////////////////////////////////////////////////////////
447
468
 
469
+ /**
470
+ * @callback AudioEndedCallback - Function called when a sound ends
471
+ * @param {AudioBufferSourceNode} source
472
+ * @memberof Audio
473
+ */
474
+
448
475
  /** Play cached audio samples with given settings
449
476
  * @param {Array} sampleChannels - Array of arrays of samples to play (for stereo playback)
450
477
  * @param {number} [volume] - How much to scale volume by
@@ -454,13 +481,20 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
454
481
  * @param {number} [sampleRate=44100] - Sample rate for the sound
455
482
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
456
483
  * @param {number} [offset] - Offset in seconds to start playback from
457
- * @param {Function} [onended] - Callback for when the sound ends
458
- * @return {AudioBufferSourceNode} - The audio node of the sound played
484
+ * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
485
+ * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
459
486
  * @memberof Audio */
460
487
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
461
488
  {
462
489
  if (!soundEnable || headlessMode) return;
463
490
 
491
+ if (!audioIsRunning())
492
+ {
493
+ // fix stalled audio, this sound won't be able to play
494
+ audioContext.resume();
495
+ return;
496
+ }
497
+
464
498
  // create buffer and source
465
499
  const channelCount = sampleChannels.length;
466
500
  const sampleLength = sampleChannels[0].length;
@@ -486,13 +520,6 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
486
520
  if (onended)
487
521
  source.addEventListener('ended', ()=> onended(source));
488
522
 
489
- if (!audioIsRunning())
490
- {
491
- // fix stalled audio, this sound won't be able to play
492
- audioContext.resume();
493
- return;
494
- }
495
-
496
523
  // play and return sound
497
524
  const startOffset = offset * rate;
498
525
  source.start(0, startOffset);
@@ -47,10 +47,16 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
47
47
  // Debug helper functions
48
48
 
49
49
  /** Asserts if the expression is false, does nothing in release builds
50
+ * Halts execution if the assert fails and throws an error
50
51
  * @param {boolean} assert
51
52
  * @param {...Object} [output] - error message output
52
53
  * @memberof Debug */
53
- function ASSERT(assert, ...output) { console.assert(assert, ...output); }
54
+ function ASSERT(assert, ...output)
55
+ {
56
+ if (assert) return;
57
+ console.assert(assert, ...output)
58
+ throw new Error('Assert failed!'); // halt execution
59
+ }
54
60
 
55
61
  /** Log to console if debug is enabled, does nothing in release builds
56
62
  * @param {...Object} [output] - message output
@@ -60,64 +66,72 @@ function LOG(...output) { console.log(...output); }
60
66
  /** Draw a debug rectangle in world space
61
67
  * @param {Vector2} pos
62
68
  * @param {Vector2} [size=Vector2()]
63
- * @param {string} [color]
64
- * @param {number} [time]
65
- * @param {number} [angle]
69
+ * @param {Color|string} [color]
70
+ * @param {number} [time]
71
+ * @param {number} [angle]
66
72
  * @param {boolean} [fill]
67
73
  * @memberof Debug */
68
- function debugRect(pos, size=vec2(), color='#fff', time=0, angle=0, fill=false)
74
+ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
69
75
  {
70
76
  if (typeof size === 'number')
71
77
  size = vec2(size); // allow passing in floats
72
- ASSERT(typeof color === 'string', 'pass in css color strings');
73
- debugPrimitives.push({pos, size, color, time:new Timer(time), angle, fill});
78
+ if (isColor(color))
79
+ color = color.toString();
80
+ pos = pos.copy();
81
+ size = size.copy();
82
+ const timer = new Timer(time);
83
+ debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill});
74
84
  }
75
85
 
76
86
  /** Draw a debug poly in world space
77
87
  * @param {Vector2} pos
78
88
  * @param {Array<Vector2>} points
79
- * @param {string} [color]
80
- * @param {number} [time]
81
- * @param {number} [angle]
89
+ * @param {Color|string} [color]
90
+ * @param {number} [time]
91
+ * @param {number} [angle]
82
92
  * @param {boolean} [fill]
83
93
  * @memberof Debug */
84
- function debugPoly(pos, points, color='#fff', time=0, angle=0, fill=false)
94
+ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
85
95
  {
86
- ASSERT(typeof color === 'string', 'pass in css color strings');
87
- debugPrimitives.push({pos, points, color, time:new Timer(time), angle, fill});
96
+ if (isColor(color))
97
+ color = color.toString();
98
+ pos = pos.copy();
99
+ points = points.map(p=>p.copy());
100
+ const timer = new Timer(time);
101
+ debugPrimitives.push({pos, points, color, timer, angle, fill});
88
102
  }
89
103
 
90
104
  /** Draw a debug circle in world space
91
105
  * @param {Vector2} pos
92
- * @param {number} [size] - diameter
93
- * @param {string} [color]
94
- * @param {number} [time]
106
+ * @param {number} [size] - diameter
107
+ * @param {Color|string} [color]
108
+ * @param {number} [time]
95
109
  * @param {boolean} [fill]
96
110
  * @memberof Debug */
97
- function debugCircle(pos, size=0, color='#fff', time=0, fill=false)
111
+ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
98
112
  {
99
- ASSERT(typeof color === 'string', 'pass in css color strings');
100
- debugPrimitives.push({pos, size, color, time:new Timer(time), angle:0, fill});
113
+ if (isColor(color))
114
+ color = color.toString();
115
+ pos = pos.copy();
116
+ const timer = new Timer(time);
117
+ debugPrimitives.push({pos, size, color, timer, angle:0, fill});
101
118
  }
102
119
 
103
120
  /** Draw a debug point in world space
104
121
  * @param {Vector2} pos
105
- * @param {string} [color]
106
- * @param {number} [time]
107
- * @param {number} [angle]
122
+ * @param {Color|string} [color]
123
+ * @param {number} [time]
124
+ * @param {number} [angle]
108
125
  * @memberof Debug */
109
126
  function debugPoint(pos, color, time, angle)
110
- {
111
- ASSERT(typeof color === 'string', 'pass in css color strings');
112
- debugRect(pos, undefined, color, time, angle);
113
- }
127
+ { debugRect(pos, undefined, color, time, angle); }
114
128
 
115
129
  /** Draw a debug line in world space
116
130
  * @param {Vector2} posA
117
131
  * @param {Vector2} posB
118
- * @param {string} [color]
119
- * @param {number} [width]
120
- * @param {number} [time]
132
+ * @param {Color|string} [color]
133
+ * @param {number} [width]
134
+ * @param {number} [time]
121
135
  * @memberof Debug */
122
136
  function debugLine(posA, posB, color, width=.1, time)
123
137
  {
@@ -131,7 +145,7 @@ function debugLine(posA, posB, color, width=.1, time)
131
145
  * @param {Vector2} sizeA
132
146
  * @param {Vector2} posB
133
147
  * @param {Vector2} sizeB
134
- * @param {string} [color]
148
+ * @param {Color|string} [color]
135
149
  * @memberof Debug */
136
150
  function debugOverlap(posA, sizeA, posB, sizeB, color)
137
151
  {
@@ -147,18 +161,21 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
147
161
  }
148
162
 
149
163
  /** Draw a debug axis aligned bounding box in world space
150
- * @param {string} text
164
+ * @param {string} text
151
165
  * @param {Vector2} pos
152
- * @param {number} [size]
153
- * @param {string} [color]
154
- * @param {number} [time]
155
- * @param {number} [angle]
156
- * @param {string} [font]
166
+ * @param {number} [size]
167
+ * @param {Color|string} [color]
168
+ * @param {number} [time]
169
+ * @param {number} [angle]
170
+ * @param {string} [font]
157
171
  * @memberof Debug */
158
- function debugText(text, pos, size=1, color='#fff', time=0, angle=0, font='monospace')
172
+ function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
159
173
  {
160
- ASSERT(typeof color === 'string', 'pass in css color strings');
161
- debugPrimitives.push({text, pos, size, color, time:new Timer(time), angle, font});
174
+ if (isColor(color))
175
+ color = color.toString();
176
+ pos = pos.copy();
177
+ const timer = new Timer(time);
178
+ debugPrimitives.push({text, pos, size, color, timer, angle, font});
162
179
  }
163
180
 
164
181
  /** Clear all debug primitives in the list
@@ -209,17 +226,30 @@ function debugSaveDataURL(dataURL, filename)
209
226
  downloadLink.click();
210
227
  }
211
228
 
212
- /** Show error as full page of red text
229
+ /** Breaks on all asserts/errors, hides the canvas, and shows message in plain text
230
+ * This is a good function to call at the start of your game to catch all errors
231
+ * In release builds this function has no effect
213
232
  * @memberof Debug */
214
233
  function debugShowErrors()
215
234
  {
216
235
  const showError = (message)=>
217
236
  {
218
237
  // replace entire page with error message
219
- document.body.style.display = '';
220
- document.body.style.backgroundColor = '#111';
221
- document.body.innerHTML = `<pre style=color:#f00;font-size:50px;white-space:pre-wrap>` + message;
238
+ document.body.style = 'background-color:#111;margin:8px';
239
+ document.body.innerHTML = `<pre style=color:#f00;font-size:28px;white-space:pre-wrap>` + message;
222
240
  }
241
+
242
+ const originalAssert = console.assert;
243
+ console.assert = (assertion, ...output)=>
244
+ {
245
+ originalAssert(assertion, ...output);
246
+ if (!assertion)
247
+ {
248
+ const message = output.join(' ');
249
+ const stack = new Error().stack;
250
+ throw 'Assertion failed!\n' + message + '\n' + stack;
251
+ }
252
+ };
223
253
  onunhandledrejection = (event)=>
224
254
  showError(event.reason.stack || event.reason);
225
255
  onerror = (message, source, lineno, colno)=>
@@ -387,7 +417,7 @@ function debugRender()
387
417
  p.fill && overlayContext.fill();
388
418
  overlayContext.stroke();
389
419
  }
390
- else if (p.size === 0 || p.size.x === 0 && p.size.y === 0)
420
+ else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
391
421
  {
392
422
  // point
393
423
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
@@ -414,7 +444,7 @@ function debugRender()
414
444
  });
415
445
 
416
446
  // remove expired primitives
417
- debugPrimitives = debugPrimitives.filter(r=>r.time<0);
447
+ debugPrimitives = debugPrimitives.filter(r=>r.timer<0);
418
448
  }
419
449
 
420
450
  if (debugObject)