littlejsengine 1.17.1 → 1.17.5

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.
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.17.1';
36
+ const engineVersion = '1.17.5';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -88,8 +88,9 @@ function getPaused() { return paused; }
88
88
  * @memberof Engine */
89
89
  function setPaused(isPaused=true) { paused = isPaused; }
90
90
 
91
- // Frame time tracking
91
+ // Engine internal variables
92
92
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
93
+ let showEngineVersion = true;
93
94
 
94
95
  ///////////////////////////////////////////////////////////////////////////////
95
96
  // plugin hooks
@@ -153,16 +154,17 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
153
154
  * @example
154
155
  * // Basic engine startup
155
156
  * engineInit(
156
- * () => { LOG('Game initialized!'); }, // gameInit
157
- * () => { updateGameLogic(); }, // gameUpdate
158
- * () => { updateUI(); }, // gameUpdatePost
159
- * () => { drawBackground(); }, // gameRender
160
- * () => { drawHUD(); }, // gameRenderPost
157
+ * ()=> { LOG('Game initialized!'); }, // gameInit
158
+ * ()=> { updateGameLogic(); }, // gameUpdate
159
+ * ()=> { updateUI(); }, // gameUpdatePost
160
+ * ()=> { drawBackground(); }, // gameRender
161
+ * ()=> { drawHUD(); }, // gameRenderPost
161
162
  * ['tiles.png', 'tilesLevel.png'] // images to load
162
163
  * );
163
164
  * @memberof Engine */
164
165
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
165
166
  {
167
+ showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
166
168
  ASSERT(!mainContext, 'engine already initialized');
167
169
  ASSERT(isArray(imageSources), 'pass in images as array');
168
170
 
@@ -431,7 +433,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
431
433
  promises.push(new Promise(resolve =>
432
434
  {
433
435
  let t = 0;
434
- console.log(`${engineName} Engine v${engineVersion}`);
435
436
  updateSplash();
436
437
  function updateSplash()
437
438
  {
@@ -608,7 +609,7 @@ function drawEngineLogo(t)
608
609
  x.fillStyle = C;
609
610
  C ? x.fill() : x.stroke();
610
611
  };
611
- const color = (c=0, l=0) =>
612
+ const color = (c=0, l=0)=>
612
613
  hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
613
614
  const alpha = wave(1,1,t);
614
615
  const p = percent(alpha, .1, .5);
@@ -782,25 +783,25 @@ function debugProtectConstant(o){ return o; }
782
783
  const PI = Math.PI;
783
784
 
784
785
  /** Returns absolute value of value passed in
785
- * @param {number} value
786
+ * @param {number} x
786
787
  * @return {number}
787
788
  * @memberof Math */
788
789
  const abs = Math.abs;
789
790
 
790
791
  /** Returns floored value of value passed in
791
- * @param {number} value
792
+ * @param {number} x
792
793
  * @return {number}
793
794
  * @memberof Math */
794
795
  const floor = Math.floor;
795
796
 
796
797
  /** Returns ceiled value of value passed in
797
- * @param {number} value
798
+ * @param {number} x
798
799
  * @return {number}
799
800
  * @memberof Math */
800
801
  const ceil = Math.ceil;
801
802
 
802
803
  /** Returns rounded value passed in
803
- * @param {number} value
804
+ * @param {number} x
804
805
  * @return {number}
805
806
  * @memberof Math */
806
807
  const round = Math.round;
@@ -818,7 +819,7 @@ const min = Math.min;
818
819
  const max = Math.max;
819
820
 
820
821
  /** Returns the sign of value passed in
821
- * @param {number} value
822
+ * @param {number} x
822
823
  * @return {number}
823
824
  * @memberof Math */
824
825
  const sign = Math.sign;
@@ -830,25 +831,25 @@ const sign = Math.sign;
830
831
  const hypot = Math.hypot;
831
832
 
832
833
  /** Returns log2 of value passed in
833
- * @param {number} value
834
+ * @param {number} x
834
835
  * @return {number}
835
836
  * @memberof Math */
836
837
  const log2 = Math.log2;
837
838
 
838
839
  /** Returns sin of value passed in
839
- * @param {number} value
840
+ * @param {number} x
840
841
  * @return {number}
841
842
  * @memberof Math */
842
843
  const sin = Math.sin;
843
844
 
844
845
  /** Returns cos of value passed in
845
- * @param {number} value
846
+ * @param {number} x
846
847
  * @return {number}
847
848
  * @memberof Math */
848
849
  const cos = Math.cos;
849
850
 
850
851
  /** Returns tan of value passed in
851
- * @param {number} value
852
+ * @param {number} x
852
853
  * @return {number}
853
854
  * @memberof Math */
854
855
  const tan = Math.tan;
@@ -2920,7 +2921,7 @@ class EngineObject
2920
2921
  if (this.collideTiles)
2921
2922
  {
2922
2923
  // check collision against tiles
2923
- const hitLayer = tileCollisionTest(this.pos, this.size, this)
2924
+ const hitLayer = tileCollisionTest(this.pos, this.size, this);
2924
2925
  if (hitLayer)
2925
2926
  {
2926
2927
  // if already was stuck in collision, don't do anything
@@ -3424,7 +3425,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
3424
3425
  ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
3425
3426
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3426
3427
 
3427
- const textureInfo = tileInfo && tileInfo.textureInfo;
3428
+ const textureInfo = tileInfo?.textureInfo;
3428
3429
  const bleed = tileInfo?.bleed ?? 0;
3429
3430
  if (useWebGL && glEnable)
3430
3431
  {
@@ -3730,9 +3731,12 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3730
3731
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3731
3732
  ASSERT(isNumber(angle), 'angle must be a number');
3732
3733
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3733
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
3734
+ ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
3734
3735
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3735
3736
 
3737
+ // clamp line width to prevent artifacts
3738
+ lineWidth = clamp(lineWidth, 0, Math.min(size.x, size.y));
3739
+
3736
3740
  if (useWebGL && glEnable)
3737
3741
  {
3738
3742
  // draw as a regular polygon
@@ -4698,9 +4702,9 @@ function inputInit()
4698
4702
  function touchInputInit()
4699
4703
  {
4700
4704
  // add non passive touch event listeners
4701
- document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
4702
- document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
4703
- document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
4705
+ document.addEventListener('touchstart', (e)=> handleTouch(e), { passive: false });
4706
+ document.addEventListener('touchmove', (e)=> handleTouch(e), { passive: false });
4707
+ document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
4704
4708
 
4705
4709
  // handle all touch events the same way
4706
4710
  let wasTouching;
@@ -4906,7 +4910,7 @@ function inputUpdate()
4906
4910
  // poll gamepads
4907
4911
  const maxGamepads = 8;
4908
4912
  const gamepads = navigator.getGamepads();
4909
- const gamepadCount = min(maxGamepads, gamepads.length)
4913
+ const gamepadCount = min(maxGamepads, gamepads.length);
4910
4914
  for (let i=0; i<gamepadCount; ++i)
4911
4915
  {
4912
4916
  // get or create gamepad data
@@ -5111,29 +5115,46 @@ function audioInit()
5111
5115
  ///////////////////////////////////////////////////////////////////////////////
5112
5116
 
5113
5117
  /**
5114
- * Sound Object - Stores a sound for later use and can be played positionally
5118
+ * Sound Object - Stores a sound for later
5119
+ * - this can be used to load and play wave, mp3, and ogg files
5120
+ * - it can also create sounds using the ZzFX sound generator
5121
+ * - can attenuate and apply stereo panning to sounds
5122
+ * - sound instance control with pause/resume capability
5115
5123
  *
5116
5124
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
5117
5125
  * @memberof Audio
5118
5126
  * @example
5119
- * // create a sound
5127
+ * // load an audio asset file
5128
+ * const sound_example = new Sound('sound.mp3');
5129
+ *
5130
+ * // create a zzfx sound
5120
5131
  * const sound_example = new Sound([.5,.5]);
5121
5132
  *
5122
- * // play the sound
5133
+ * // play a sound
5123
5134
  * sound_example.play();
5124
5135
  */
5125
5136
  class Sound
5126
5137
  {
5127
- /** Create a sound object and cache the zzfx samples for later use
5128
- * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
5138
+ /**
5139
+ * @callback SoundLoadCallback - Function called when sound is loaded
5140
+ * @param {Sound} sound
5141
+ * @memberof Audio
5142
+ */
5143
+
5144
+ /** Create a sound object and cache the audio for later use
5145
+ * @param {string|Array} [asset] - Filename of audio file or zzfx array
5146
+ * @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds the zzfx default is used if undefined
5129
5147
  * @param {number} [range=soundDefaultRange] - World space max range of sound
5130
5148
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5149
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5131
5150
  */
5132
- constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
5151
+ constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
5133
5152
  {
5134
5153
  if (!soundEnable || headlessMode) return;
5135
5154
 
5136
- ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
5155
+ ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
5156
+ ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
5157
+ ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
5137
5158
  ASSERT(isNumber(range), 'range must be a number');
5138
5159
  ASSERT(isNumber(taper), 'taper must be a number');
5139
5160
 
@@ -5142,23 +5163,35 @@ class Sound
5142
5163
  /** @property {number} - At what percentage of range should it start tapering */
5143
5164
  this.taper = taper;
5144
5165
  /** @property {number} - How much to randomize frequency each time sound plays */
5145
- this.randomness = 0;
5166
+ this.randomness = randomness ?? 0;
5146
5167
  /** @property {number} - Sample rate for this sound */
5147
5168
  this.sampleRate = audioDefaultSampleRate;
5148
5169
  /** @property {number} - Percentage of this sound currently loaded */
5149
5170
  this.loadedPercent = 0;
5171
+ /** @property {SoundLoadCallback} - function to call when sound is loaded */
5172
+ this.onloadCallback = onloadCallback;
5150
5173
 
5151
- // generate zzfx sound now for fast playback
5152
- if (zzfxSound)
5174
+ if (Array.isArray(asset))
5153
5175
  {
5176
+ // generate zzfx sound
5177
+ const zzfxSound = asset;
5178
+
5154
5179
  // remove randomness so it can be applied on playback
5155
- const randomnessIndex = 1, defaultRandomness = .05;
5180
+ const defaultRandomness = randomness ?? .05;
5181
+ const randomnessIndex = 1;
5156
5182
  this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
5157
5183
  zzfxSound[randomnessIndex] = 0;
5158
5184
 
5159
5185
  // generate the zzfx samples
5160
5186
  this.sampleChannels = [zzfxG(...zzfxSound)];
5161
5187
  this.loadedPercent = 1;
5188
+ onloadCallback?.(this);
5189
+ }
5190
+ else if (typeof asset === 'string')
5191
+ {
5192
+ // load the audio file
5193
+ const filename = asset;
5194
+ this.loadSound(filename);
5162
5195
  }
5163
5196
  }
5164
5197
 
@@ -5210,7 +5243,7 @@ class Sound
5210
5243
  * @param {number} [volume] - Volume to play the music at
5211
5244
  * @param {boolean} [loop] - Should the music loop?
5212
5245
  * @param {boolean} [paused] - Should the music start paused
5213
- * @return {SoundInstance} - The audio source node
5246
+ * @return {SoundInstance} - The sound instance
5214
5247
  */
5215
5248
  playMusic(volume=1, loop=true, paused=false)
5216
5249
  { return this.play(undefined, volume, 1, 0, loop, paused); }
@@ -5220,7 +5253,7 @@ class Sound
5220
5253
  * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
5221
5254
  * @param {Vector2} [pos] - World space position to play the sound if any
5222
5255
  * @param {number} [volume=1] - How much to scale volume by
5223
- * @return {SoundInstance} - The audio source node
5256
+ * @return {SoundInstance} - The sound instance
5224
5257
  */
5225
5258
  playNote(semitoneOffset=0, pos, volume)
5226
5259
  {
@@ -5233,57 +5266,14 @@ class Sound
5233
5266
  * @return {number} - How long the sound is in seconds (undefined if loading)
5234
5267
  */
5235
5268
  getDuration()
5236
- { return this.sampleChannels?.[0].length / this.sampleRate || 0; }
5269
+ { return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
5237
5270
 
5238
5271
  /** Check if sound is loaded, for sounds fetched from a url
5239
5272
  * @return {boolean} - True if sound is loaded and ready to play
5240
5273
  */
5241
5274
  isLoaded() { return this.loadedPercent === 1; }
5242
- }
5243
-
5244
- ///////////////////////////////////////////////////////////////////////////////
5245
-
5246
- /**
5247
- * Sound Wave Object - Loads and stores an audio file for later use
5248
- * - this can be used to load and play wave, mp3, and ogg files
5249
- * @extends Sound
5250
- * @memberof Audio
5251
- * @example
5252
- * // load an audio asset file
5253
- * const sound_example = new SoundWave('sound.mp3');
5254
- *
5255
- * // play the sound
5256
- * sound_example.play();
5257
- */
5258
- class SoundWave extends Sound
5259
- {
5260
- /**
5261
- * @callback SoundLoadCallback - Function called when sound is loaded
5262
- * @param {SoundWave} sound
5263
- * @memberof Audio
5264
- */
5265
5275
 
5266
- /** Create a sound object and cache the wave file for later use
5267
- * @param {string} filename - Filename of audio file to load
5268
- * @param {number} [randomness] - How much to randomize frequency each time sound plays
5269
- * @param {number} [range=soundDefaultRange] - World space max range of sound
5270
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5271
- * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5272
- */
5273
- constructor(filename, randomness=0, range, taper, onloadCallback)
5274
- {
5275
- super(undefined, range, taper);
5276
- if (!soundEnable || headlessMode) return;
5277
- ASSERT(!filename || isString(filename), 'filename must be a string');
5278
- ASSERT(isNumber(randomness), 'randomness must be a number');
5279
-
5280
- /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5281
- this.onloadCallback = onloadCallback;
5282
- this.randomness = randomness;
5283
- filename && this.loadSound(filename);
5284
- }
5285
-
5286
- /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
5276
+ /** Loads a sound from a URL and decodes it into sample data.
5287
5277
  * @param {string} filename
5288
5278
  * @return {Promise<void>} */
5289
5279
  async loadSound(filename)
@@ -5938,17 +5928,17 @@ class TileLayerData
5938
5928
  class CanvasLayer extends EngineObject
5939
5929
  {
5940
5930
  /** Create a canvas layer object
5941
- * @param {Vector2} [position] - World space position of the layer
5931
+ * @param {Vector2} [pos] - World space position of the layer
5942
5932
  * @param {Vector2} [size] - World space size of the layer
5943
5933
  * @param {number} [angle] - Angle the layer is rotated by
5944
5934
  * @param {number} [renderOrder] - Objects sorted by renderOrder
5945
5935
  * @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
5946
5936
  * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
5947
5937
  */
5948
- constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=glEnable)
5938
+ constructor(pos, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=true)
5949
5939
  {
5950
5940
  ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5951
- super(position, size, undefined, angle, WHITE, renderOrder);
5941
+ super(pos, size, undefined, angle, WHITE, renderOrder);
5952
5942
 
5953
5943
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
5954
5944
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
@@ -6052,16 +6042,16 @@ class CanvasLayer extends EngineObject
6052
6042
  class TileLayer extends CanvasLayer
6053
6043
  {
6054
6044
  /** Create a tile layer object
6055
- * @param {Vector2} position - World space position
6045
+ * @param {Vector2} pos - World space position
6056
6046
  * @param {Vector2} size - World space size
6057
6047
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6058
6048
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6049
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6059
6050
  */
6060
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6051
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6061
6052
  {
6062
6053
  const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6063
- const useWebGL = true;
6064
- super(position, size, 0, renderOrder, canvasSize, useWebGL);
6054
+ super(pos, size, 0, renderOrder, canvasSize, useWebGL);
6065
6055
 
6066
6056
  /** @property {TileInfo} - Default tile info for layer */
6067
6057
  this.tileInfo = undefined;
@@ -6073,15 +6063,15 @@ class TileLayer extends CanvasLayer
6073
6063
  if (headlessMode)
6074
6064
  {
6075
6065
  // disable rendering in headless mode
6076
- this.render = () => {};
6077
- this.redraw = () => {};
6078
- this.redrawStart = () => {};
6079
- this.redrawEnd = () => {};
6080
- this.drawTileData = () => {};
6081
- this.redrawTileData = () => {};
6082
- this.drawLayerTile = () => {};
6083
- this.drawLayerRect = () => {};
6084
- this.clearLayerRect = () => {};
6066
+ this.render = ()=> {};
6067
+ this.redraw = ()=> {};
6068
+ this.redrawStart = ()=> {};
6069
+ this.redrawEnd = ()=> {};
6070
+ this.drawTileData = ()=> {};
6071
+ this.redrawTileData = ()=> {};
6072
+ this.drawLayerTile = ()=> {};
6073
+ this.drawLayerRect = ()=> {};
6074
+ this.clearLayerRect = ()=> {};
6085
6075
  return;
6086
6076
  }
6087
6077
 
@@ -6115,6 +6105,12 @@ class TileLayer extends CanvasLayer
6115
6105
  isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
6116
6106
  }
6117
6107
 
6108
+ /** Clear data at a given position in the array
6109
+ * @param {Vector2} layerPos - Local position in array
6110
+ * @param {boolean} [redraw] - Force the tile to redraw if true */
6111
+ clearData(layerPos, redraw=false)
6112
+ { this.setData(layerPos, new TileLayerData, redraw) }
6113
+
6118
6114
  /** Get data at a given position in the array
6119
6115
  * @param {Vector2} layerPos - Local position in array
6120
6116
  * @return {TileLayerData} */
@@ -6156,7 +6152,7 @@ class TileLayer extends CanvasLayer
6156
6152
  for (let x = this.size.x; x--;)
6157
6153
  for (let y = this.size.y; y--;)
6158
6154
  this.drawTileData(vec2(x,y), false);
6159
- this.hasWebGL() && glFlush();
6155
+ this.isUsingWebGL && glFlush();
6160
6156
  this.onRedraw();
6161
6157
  this.redrawEnd();
6162
6158
  }
@@ -6205,14 +6201,13 @@ class TileLayer extends CanvasLayer
6205
6201
  if (!this.context) return;
6206
6202
  ASSERT(drawContext === this.context);
6207
6203
 
6208
- if (glEnable && this.textureInfo.glTexture)
6209
- glSetRenderTarget();
6210
-
6211
6204
  // set stuff back to normal
6205
+ if (this.isUsingWebGL)
6206
+ glSetRenderTarget();
6212
6207
  [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6213
6208
  }
6214
6209
 
6215
- /** Draw the tile at a given position in the tile grid
6210
+ /** Draw the tile at a given position in the tile layer
6216
6211
  * This can be used to clear out tiles when they are destroyed
6217
6212
  * Tiles can also be redrawn if inside a redrawStart/End block
6218
6213
  * @param {Vector2} layerPos
@@ -6236,7 +6231,7 @@ class TileLayer extends CanvasLayer
6236
6231
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
6237
6232
  }
6238
6233
 
6239
- /** Draw the tile at a given position in the tile grid
6234
+ /** Draw the tile at a given position in the tile layer
6240
6235
  * This can be used to clear tiles when they are destroyed
6241
6236
  * For better performance use drawTileData inside a redrawStart/End block
6242
6237
  * @param {Vector2} layerPos
@@ -6264,7 +6259,7 @@ class TileLayer extends CanvasLayer
6264
6259
  angle=0, mirror, additiveColor)
6265
6260
  {
6266
6261
  const drawPos = pos.add(size.scale(.5));
6267
- drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor);
6262
+ drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor, this.isUsingWebGL);
6268
6263
  }
6269
6264
 
6270
6265
  /** Clear a rectangle in layer space
@@ -6304,14 +6299,15 @@ class TileLayer extends CanvasLayer
6304
6299
  class TileCollisionLayer extends TileLayer
6305
6300
  {
6306
6301
  /** Create a tile layer object
6307
- * @param {Vector2} position - World space position
6308
- * @param {Vector2} size - World space size
6309
- * @param {TileInfo} [tileInfo] - Tile info for layer
6302
+ * @param {Vector2} pos - World space position
6303
+ * @param {Vector2} size - World space size
6304
+ * @param {TileInfo} [tileInfo] - Tile info for layer
6310
6305
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6306
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6311
6307
  */
6312
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6308
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6313
6309
  {
6314
- super(position, size.floor(), tileInfo, renderOrder);
6310
+ super(pos, size.floor(), tileInfo, renderOrder, useWebGL);
6315
6311
 
6316
6312
  /** @property {Array<number>} - The tile collision grid */
6317
6313
  this.collisionData = [];
@@ -6347,24 +6343,29 @@ class TileCollisionLayer extends TileLayer
6347
6343
  this.collisionData.fill(0);
6348
6344
  }
6349
6345
 
6350
- /** Set tile collision data for a given cell in the grid
6351
- * @param {Vector2} gridPos
6346
+ /** Set tile collision data for a given cell in the layer
6347
+ * @param {Vector2} layerPos
6352
6348
  * @param {number} [data] */
6353
- setCollisionData(gridPos, data=1)
6349
+ setCollisionData(layerPos, data=1)
6354
6350
  {
6355
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6356
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6357
- gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6351
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6352
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
6353
+ layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6358
6354
  }
6359
6355
 
6360
- /** Get tile collision data for a given cell in the grid
6361
- * @param {Vector2} gridPos
6356
+ /** Clear tile collision data for a given cell in the layer
6357
+ * @param {Vector2} layerPos */
6358
+ clearCollisionData(layerPos)
6359
+ { this.setCollisionData(layerPos, 0); }
6360
+
6361
+ /** Get tile collision data for a given cell in the layer
6362
+ * @param {Vector2} layerPos
6362
6363
  * @return {number} */
6363
- getCollisionData(gridPos)
6364
+ getCollisionData(layerPos)
6364
6365
  {
6365
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6366
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6367
- return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6366
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6367
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
6368
+ return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6368
6369
  }
6369
6370
 
6370
6371
  /** Check if collision with another object should occur
@@ -7282,7 +7283,7 @@ function glPreRender(clear=true)
7282
7283
  p.x, p.y, 0, 1];
7283
7284
 
7284
7285
  // set the same transform matrix for both shaders
7285
- const initUniform = (program, uniform, value) =>
7286
+ const initUniform = (program, uniform, value)=>
7286
7287
  {
7287
7288
  glContext.useProgram(program);
7288
7289
  const location = glContext.getUniformLocation(program, uniform);
@@ -7676,7 +7677,7 @@ function glSetRenderTarget(texture, clear=false)
7676
7677
  glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
7677
7678
  glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
7678
7679
  glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
7679
- glPreRender(clear);
7680
+ glPreRender(clear);
7680
7681
  }
7681
7682
  else
7682
7683
  {
@@ -8295,7 +8296,7 @@ class ZzFXMusic extends Sound
8295
8296
  /** Play the music that loops by default
8296
8297
  * @param {number} [volume] - Volume to play the music at
8297
8298
  * @param {boolean} [loop] - Should the music loop?
8298
- * @return {AudioBufferSourceNode} - The audio source node
8299
+ * @return {SoundInstance} - The sound instance
8299
8300
  */
8300
8301
  playMusic(volume=1, loop=true)
8301
8302
  { return super.play(undefined, volume, 1, 0, loop); }
@@ -8342,7 +8343,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
8342
8343
  sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
8343
8344
 
8344
8345
  // for each pattern in sequence
8345
- sequence.forEach((patternIndex, sequenceIndex) => {
8346
+ sequence.forEach((patternIndex, sequenceIndex)=> {
8346
8347
  // get pattern for current channel, use empty 1 note pattern if none found
8347
8348
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
8348
8349
 
@@ -8435,7 +8436,7 @@ let uiDebug = 0;
8435
8436
 
8436
8437
  /** Enable UI system debug drawing
8437
8438
  * 0=off, 1=normal, 2=show invisible
8438
- * @param {number|boolean} enable
8439
+ * @param {number|boolean} debugMode
8439
8440
  * @memberof UISystem */
8440
8441
  function uiSetDebug(debugMode)
8441
8442
  { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
@@ -9636,8 +9637,8 @@ class UIScrollbar extends UIObject
9636
9637
  class UIVideo extends UIObject
9637
9638
  {
9638
9639
  /** Create a video player UI object
9639
- * @param {Vector2} [pos]
9640
- * @param {Vector2} [size]
9640
+ * @param {Vector2} pos
9641
+ * @param {Vector2} size
9641
9642
  * @param {string} src - Video file path or URL
9642
9643
  * @param {boolean} [autoplay=false] - Start playing immediately?
9643
9644
  * @param {boolean} [loop=false] - Loop the video?
@@ -9777,6 +9778,7 @@ class UIVideo extends UIObject
9777
9778
  * - Contact begin and end callbacks
9778
9779
  * - Wraps b2Vec2 type to/from Vector2
9779
9780
  * - Raycasting and querying
9781
+ * - Box2dTileLayer for grid based collision
9780
9782
  * - Every type of joint
9781
9783
  * - Debug physics drawing
9782
9784
  * @namespace Box2D
@@ -9826,21 +9828,24 @@ class Box2dObject extends EngineObject
9826
9828
  bodyDef.set_type(bodyType);
9827
9829
  bodyDef.set_position(box2d.vec2dTo(pos));
9828
9830
  bodyDef.set_angle(-angle);
9831
+
9832
+ /** @property {Object} - The Box2d body */
9829
9833
  this.body = box2d.world.CreateBody(bodyDef);
9830
- this.body.object = this;
9834
+ /** @property {Color} - Line color used for default box2d drawing */
9831
9835
  this.lineColor = BLACK;
9832
- box2d.objects.push(this);
9833
-
9834
- // edge lists and loops for drawing
9836
+ /** @property {Array<Object>} - List of all edges for default box2d drawing */
9835
9837
  this.edgeLists = [];
9838
+ /** @property {Array<Object>} - List of all edge loops for default box2d drawing */
9836
9839
  this.edgeLoops = [];
9840
+
9841
+ this.body.object = this; // link body to this object
9842
+ box2d.objects.push(this); // keep track of all box2d objects
9837
9843
  }
9838
9844
 
9839
9845
  /** Destroy this object and its physics body */
9840
9846
  destroy()
9841
9847
  {
9842
- if (this.destroyed)
9843
- return;
9848
+ if (this.destroyed) return;
9844
9849
 
9845
9850
  // destroy physics body, fixtures, and joints
9846
9851
  ASSERT(this.body, 'Box2dObject has no body to destroy');
@@ -9871,11 +9876,12 @@ class Box2dObject extends EngineObject
9871
9876
  }
9872
9877
 
9873
9878
  /** Draws all this object's fixtures
9874
- * @param {Color} [color]
9875
- * @param {Color} [lineColor]
9876
- * @param {number} [lineWidth]
9879
+ * @param {Color} [color]
9880
+ * @param {Color} [lineColor]
9881
+ * @param {number} [lineWidth]
9882
+ * @param {boolean} [useWebGL=glEnable]
9877
9883
  * @param {CanvasRenderingContext2D} [context] */
9878
- drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
9884
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
9879
9885
  {
9880
9886
  // draw non-edge fixtures
9881
9887
  this.getFixtureList().forEach((fixture)=>
@@ -9883,7 +9889,7 @@ class Box2dObject extends EngineObject
9883
9889
  const shape = box2d.castObjectType(fixture.GetShape());
9884
9890
  if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
9885
9891
  {
9886
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
9892
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, useWebGL, context);
9887
9893
  }
9888
9894
  });
9889
9895
 
@@ -10109,6 +10115,14 @@ class Box2dObject extends EngineObject
10109
10115
  return fixtures;
10110
10116
  }
10111
10117
 
10118
+ /** Destroy a fixture from the body
10119
+ * @param {Object} [fixture] */
10120
+ destroyFixture(fixture) { this.body.DestroyFixture(fixture); }
10121
+
10122
+ /** Destroy all fixture from the body */
10123
+ destroyAllFixtures()
10124
+ { this.getFixtureList().forEach(fixture=>this.destroyFixture(fixture)); }
10125
+
10112
10126
  ///////////////////////////////////////////////////////////////////////////////
10113
10127
  // physics get functions
10114
10128
 
@@ -10341,6 +10355,136 @@ class Box2dObject extends EngineObject
10341
10355
  }
10342
10356
  }
10343
10357
 
10358
+ ///////////////////////////////////////////////////////////////////////////////
10359
+ /**
10360
+ * Box2D Static Object - Box2d with a static physics body
10361
+ * @extends Box2dObject
10362
+ * @memberof Box2D
10363
+ */
10364
+ class Box2dStaticObject extends Box2dObject
10365
+ {
10366
+ /** Create a LittleJS object with Box2d physics
10367
+ * @param {Vector2} [pos]
10368
+ * @param {Vector2} [size]
10369
+ * @param {TileInfo} [tileInfo]
10370
+ * @param {number} [angle]
10371
+ * @param {Color} [color]
10372
+ * @param {number} [renderOrder] */
10373
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10374
+ {
10375
+ const bodyType = box2d.bodyTypeStatic;
10376
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10377
+ }
10378
+ }
10379
+
10380
+ ///////////////////////////////////////////////////////////////////////////////
10381
+ /**
10382
+ * Box2D Kiematic Object - Box2d with a kinematic physics body
10383
+ * @extends Box2dObject
10384
+ * @memberof Box2D
10385
+ */
10386
+ class Box2dKiematicObject extends Box2dObject
10387
+ {
10388
+ /** Create a LittleJS object with Box2d physics
10389
+ * @param {Vector2} [pos]
10390
+ * @param {Vector2} [size]
10391
+ * @param {TileInfo} [tileInfo]
10392
+ * @param {number} [angle]
10393
+ * @param {Color} [color]
10394
+ * @param {number} [renderOrder] */
10395
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10396
+ {
10397
+ const bodyType = box2d.bodyTypeKinematic;
10398
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10399
+ }
10400
+ }
10401
+
10402
+ ///////////////////////////////////////////////////////////////////////////////
10403
+ /**
10404
+ * Box2d Tile Layer
10405
+ * - adds Box2d support to tile layers
10406
+ * - creates static box2d fixtures for solid tiles
10407
+ * @extends Box2dObject
10408
+ * @memberof Box2D
10409
+ */
10410
+ class Box2dTileLayer extends Box2dStaticObject
10411
+ {
10412
+ /** Create a Box2d tile layer object
10413
+ * @param {TileCollisionLayer} tileLayer - Tile layer for this object */
10414
+ constructor(tileLayer)
10415
+ {
10416
+ ASSERT(tileLayer instanceof TileCollisionLayer, 'tileLayer must be a TileCollisionLayer');
10417
+ super(tileLayer.pos, tileLayer.size);
10418
+
10419
+ /** @property {TileLayer} - The tile layer */
10420
+ this.tileLayer = tileLayer;
10421
+ this.addChild(tileLayer);
10422
+ }
10423
+
10424
+ render()
10425
+ {
10426
+ // do not render fixtures, tile layer handles rendering
10427
+ }
10428
+
10429
+ /** Create box2d collision fixtures for solid tiles
10430
+ * @param {number} [friction]
10431
+ * @param {number} [restitution] */
10432
+ buildCollision(friction=.2, restitution=0)
10433
+ {
10434
+ // destroy all fixtures and create new ones
10435
+ this.destroyAllFixtures();
10436
+
10437
+ // create box2d object for this layer
10438
+ const size = this.tileLayer.size;
10439
+ this.pos = this.tileLayer.pos.copy();
10440
+ this.size = size.copy();
10441
+
10442
+ // track which tiles have been processed
10443
+ const processed = [];
10444
+ const getIndex = (x, y)=> x + y * size.x;
10445
+ const isSolidUnprocessed = (x, y)=>
10446
+ !processed[getIndex(x, y)] &&
10447
+ this.tileLayer.getCollisionData(vec2(x, y)) > 0;
10448
+
10449
+ // combine tiles into larger boxes
10450
+ for (let x = 0; x < size.x; ++x)
10451
+ for (let y = 0; y < size.y; ++y)
10452
+ {
10453
+ if (!isSolidUnprocessed(x, y)) continue;
10454
+
10455
+ // find max width by scanning right
10456
+ let width = 1, height = 1, canExpand = true;
10457
+ while (isSolidUnprocessed(x + width, y))
10458
+ ++width;
10459
+
10460
+ // find max height by scanning up, ensuring all rows have the same width
10461
+ while (canExpand)
10462
+ {
10463
+ for (let checkX = 0; checkX < width; ++checkX)
10464
+ {
10465
+ if (!isSolidUnprocessed(x + checkX, y + height))
10466
+ {
10467
+ canExpand = false;
10468
+ break;
10469
+ }
10470
+ }
10471
+ if (canExpand)
10472
+ ++height;
10473
+ }
10474
+
10475
+ // mark all tiles in this rectangle as processed
10476
+ for (let rectX = width; rectX--;)
10477
+ for (let rectY = height; rectY--;)
10478
+ processed[getIndex(x + rectX, y + rectY)] = true;
10479
+
10480
+ // create a single fixture for the entire rectangle
10481
+ const shapeSize = vec2(width, height);
10482
+ const offset = vec2(x + width/2, y + height/2);
10483
+ this.addBox(shapeSize, offset, 0, 0, friction, restitution);
10484
+ }
10485
+ }
10486
+ }
10487
+
10344
10488
  ///////////////////////////////////////////////////////////////////////////////
10345
10489
  /**
10346
10490
  * Box2D Raycast Result
@@ -10382,6 +10526,7 @@ class Box2dJoint
10382
10526
  * @param {Object} jointDef */
10383
10527
  constructor(jointDef)
10384
10528
  {
10529
+ /** @property {Object} - The Box2d joint */
10385
10530
  this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
10386
10531
  }
10387
10532
 
@@ -10728,7 +10873,7 @@ class Box2dGearJoint extends Box2dJoint
10728
10873
  * @param {Box2dObject} objectB
10729
10874
  * @param {Box2dJoint} joint1
10730
10875
  * @param {Box2dJoint} joint2
10731
- * @param {ratio} [ratio] */
10876
+ * @param {number} [ratio] */
10732
10877
  constructor(objectA, objectB, joint1, joint2, ratio=1)
10733
10878
  {
10734
10879
  const jointDef = new box2d.instance.b2GearJointDef();
@@ -10870,50 +11015,6 @@ class Box2dPrismaticJoint extends Box2dJoint
10870
11015
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
10871
11016
  }
10872
11017
 
10873
- ///////////////////////////////////////////////////////////////////////////////
10874
- /**
10875
- * Box2D Static Object - Box2d with a static physics body
10876
- * @extends Box2dObject
10877
- * @memberof Box2D
10878
- */
10879
- class Box2dStaticObject extends Box2dObject
10880
- {
10881
- /** Create a LittleJS object with Box2d physics
10882
- * @param {Vector2} [pos]
10883
- * @param {Vector2} [size]
10884
- * @param {TileInfo} [tileInfo]
10885
- * @param {number} [angle]
10886
- * @param {Color} [color]
10887
- * @param {number} [renderOrder] */
10888
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10889
- {
10890
- const bodyType = box2d.bodyTypeStatic;
10891
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10892
- }
10893
- }
10894
-
10895
- ///////////////////////////////////////////////////////////////////////////////
10896
- /**
10897
- * Box2D Kiematic Object - Box2d with a kinematic physics body
10898
- * @extends Box2dObject
10899
- * @memberof Box2D
10900
- */
10901
- class Box2dKiematicObject extends Box2dObject
10902
- {
10903
- /** Create a LittleJS object with Box2d physics
10904
- * @param {Vector2} [pos]
10905
- * @param {Vector2} [size]
10906
- * @param {TileInfo} [tileInfo]
10907
- * @param {number} [angle]
10908
- * @param {Color} [color]
10909
- * @param {number} [renderOrder] */
10910
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10911
- {
10912
- const bodyType = box2d.bodyTypeKinematic;
10913
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10914
- }
10915
- }
10916
-
10917
11018
  ///////////////////////////////////////////////////////////////////////////////
10918
11019
  /**
10919
11020
  * Box2D Wheel Joint
@@ -11274,10 +11375,13 @@ class Box2dPlugin
11274
11375
  {
11275
11376
  ASSERT(!box2d, 'Box2D already initialized');
11276
11377
  box2d = this;
11378
+
11379
+ /** @property {Object} - The Box2d instance */
11277
11380
  this.instance = instance;
11381
+ /** @property {Object} - The Box2d world */
11278
11382
  this.world = new box2d.instance.b2World();
11383
+ /** @property {Array<Box2dObject>} - List of all Box2d objects */
11279
11384
  this.objects = [];
11280
-
11281
11385
  /** @property {number} - Velocity iterations per update*/
11282
11386
  this.velocityIterations = 8;
11283
11387
  /** @property {number} - Position iterations per update*/
@@ -11476,8 +11580,9 @@ class Box2dPlugin
11476
11580
  * @param {Color} [color]
11477
11581
  * @param {Color} [lineColor]
11478
11582
  * @param {number} [lineWidth]
11583
+ * @param {boolean} [useWebGL=glEnable]
11479
11584
  * @param {CanvasRenderingContext2D} [context] */
11480
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
11585
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebgl, context)
11481
11586
  {
11482
11587
  const shape = box2d.castObjectType(fixture.GetShape());
11483
11588
  switch (shape.GetType())
@@ -11487,20 +11592,20 @@ class Box2dPlugin
11487
11592
  let points = [];
11488
11593
  for (let i=shape.GetVertexCount(); i--;)
11489
11594
  points.push(box2d.vec2From(shape.GetVertex(i)));
11490
- drawPoly(points, color, lineWidth, lineColor, pos, angle);
11595
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
11491
11596
  break;
11492
11597
  }
11493
11598
  case box2d.instance.b2Shape.e_circle:
11494
11599
  {
11495
11600
  const radius = shape.get_m_radius();
11496
- drawCircle(pos, radius*2, color, lineWidth, lineColor);
11601
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
11497
11602
  break;
11498
11603
  }
11499
11604
  case box2d.instance.b2Shape.e_edge:
11500
11605
  {
11501
11606
  const v1 = box2d.vec2From(shape.get_m_vertex1());
11502
11607
  const v2 = box2d.vec2From(shape.get_m_vertex2());
11503
- drawLine(v1, v2, lineWidth, lineColor, pos, angle);
11608
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
11504
11609
  break;
11505
11610
  }
11506
11611
  }
@@ -11518,7 +11623,7 @@ class Box2dPlugin
11518
11623
  }
11519
11624
 
11520
11625
  /** converts a box2d vec2 pointer to a Vector2
11521
- * @param {Object} v */
11626
+ * @param {Object} vp */
11522
11627
  vec2FromPointer(vp)
11523
11628
  {
11524
11629
  const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
@@ -11630,7 +11735,7 @@ async function box2dInit()
11630
11735
  const box2dColorPointer = (c)=>
11631
11736
  box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
11632
11737
  const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
11633
- const getPointsList = (vertices, vertexCount) =>
11738
+ const getPointsList = (vertices, vertexCount)=>
11634
11739
  {
11635
11740
  const points = [];
11636
11741
  for (let i=vertexCount; i--;)