littlejsengine 1.14.16 → 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.
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.16';
36
+ const engineVersion = '1.14.19';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -94,7 +94,17 @@ 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
+ }
98
108
 
99
109
  /**
100
110
  * @callback PluginCallback - Update or render function for a plugin
@@ -102,15 +112,21 @@ const pluginUpdateList = [], pluginRenderList = [];
102
112
  */
103
113
 
104
114
  /** Add a new update function for a plugin
105
- * @param {PluginCallback} [updateFunction]
106
- * @param {PluginCallback} [renderFunction]
115
+ * @param {PluginCallback} [update]
116
+ * @param {PluginCallback} [render]
117
+ * @param {PluginCallback} [glContextLost]
118
+ * @param {PluginCallback} [glContextRestored]
107
119
  * @memberof Engine */
108
- function engineAddPlugin(updateFunction, renderFunction)
120
+ function engineAddPlugin(update, render, glContextLost, glContextRestored)
109
121
  {
110
- ASSERT(!pluginUpdateList.includes(updateFunction));
111
- ASSERT(!pluginRenderList.includes(renderFunction));
112
- updateFunction && pluginUpdateList.push(updateFunction);
113
- 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);
114
130
  }
115
131
 
116
132
  ///////////////////////////////////////////////////////////////////////////////
@@ -195,7 +211,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
195
211
  wasUpdated = true;
196
212
  updateCanvas();
197
213
  inputUpdate();
198
- pluginUpdateList.forEach(f=>f());
214
+ pluginList.forEach(plugin=>plugin.update?.());
199
215
 
200
216
  // update object transforms even when paused
201
217
  for (const o of engineObjects)
@@ -228,7 +244,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
228
244
  updateCanvas();
229
245
  inputUpdate();
230
246
  gameUpdate();
231
- pluginUpdateList.forEach(f=>f());
247
+ pluginList.forEach(plugin=>plugin.update?.());
232
248
  engineObjectsUpdate();
233
249
 
234
250
  // do post update
@@ -262,7 +278,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
262
278
  for (const o of engineObjects)
263
279
  o.destroyed || o.render();
264
280
  gameRenderPost();
265
- pluginRenderList.forEach(f=>f());
281
+ pluginList.forEach(plugin=>plugin.render?.());
266
282
  touchGamepadRender();
267
283
  debugRender();
268
284
  glFlush();
@@ -389,7 +405,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
389
405
  image.onerror = image.onload = ()=>
390
406
  {
391
407
  const textureInfo = new TextureInfo(image);
392
- textureInfo.createWebGLTexture();
393
408
  textureInfos[textureIndex] = textureInfo;
394
409
  resolve();
395
410
  }
@@ -405,7 +420,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
405
420
  {
406
421
  const textureInfo = new TextureInfo(new Image);
407
422
  textureInfos[0] = textureInfo;
408
- textureInfo.createWebGLTexture();
409
423
  resolve();
410
424
  }));
411
425
  }
@@ -3571,16 +3585,15 @@ class TileInfo
3571
3585
  }
3572
3586
 
3573
3587
  /**
3574
- * Set this tile to use a full image
3575
- * @param {HTMLImageElement|OffscreenCanvas} image
3576
- * @param {WebGLTexture} [glTexture] - WebGL texture
3588
+ * Set this tile to use a full image in a texture info
3589
+ * @param {TextureInfo} textureInfo
3577
3590
  * @return {TileInfo}
3578
3591
  */
3579
- setFullImage(image, glTexture)
3592
+ setFullImage(textureInfo)
3580
3593
  {
3581
3594
  this.pos = new Vector2;
3582
- this.size = new Vector2(image.width, image.height);
3583
- this.textureInfo = new TextureInfo(image, glTexture);
3595
+ this.size = textureInfo.size.copy();
3596
+ this.textureInfo = textureInfo;
3584
3597
  // do not use padding or bleed
3585
3598
  this.bleedScale = this.padding = 0;
3586
3599
  return this;
@@ -3596,26 +3609,30 @@ class TextureInfo
3596
3609
  /**
3597
3610
  * Create a TextureInfo, called automatically by the engine
3598
3611
  * @param {HTMLImageElement|OffscreenCanvas} image
3599
- * @param {WebGLTexture} [glTexture] - WebGL texture
3612
+ * @param {boolean} [useWebGL] - Should use WebGL if available?
3600
3613
  */
3601
- constructor(image, glTexture)
3614
+ constructor(image, useWebGL=true)
3602
3615
  {
3603
- /** @property {HTMLImageElement} - image source */
3616
+ /** @property {HTMLImageElement|OffscreenCanvas} - image source */
3604
3617
  this.image = image;
3605
3618
  /** @property {Vector2} - size of the image */
3606
- this.size = vec2(image.width, image.height);
3619
+ this.size = image ? vec2(image.width, image.height) : vec2();
3607
3620
  /** @property {Vector2} - inverse of the size, cached for rendering */
3608
- this.sizeInverse = vec2(1/image.width, 1/image.height);
3621
+ this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
3609
3622
  /** @property {WebGLTexture} - WebGL texture */
3610
- this.glTexture = glTexture;
3623
+ this.glTexture = undefined;
3624
+ useWebGL && this.createWebGLTexture();
3611
3625
  }
3612
3626
 
3613
- createWebGLTexture()
3614
- {
3615
- ASSERT(!this.glTexture);
3616
- if (glEnable)
3617
- this.glTexture = glCreateTexture(this.image);
3618
- }
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; }
3619
3636
  }
3620
3637
 
3621
3638
  ///////////////////////////////////////////////////////////////////////////////
@@ -4048,12 +4065,13 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4048
4065
  * @param {Color} [lineColor=(0,0,0,1)]
4049
4066
  * @param {CanvasTextAlign} [textAlign='center']
4050
4067
  * @param {string} [font=fontDefault]
4068
+ * @param {string} [fontStyle]
4051
4069
  * @param {number} [maxWidth]
4052
4070
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4053
4071
  * @memberof Draw */
4054
- 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)
4055
4073
  {
4056
- 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);
4057
4075
  }
4058
4076
 
4059
4077
  /** Draw text on overlay canvas in world space
@@ -4066,11 +4084,12 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4066
4084
  * @param {Color} [lineColor=(0,0,0,1)]
4067
4085
  * @param {CanvasTextAlign} [textAlign='center']
4068
4086
  * @param {string} [font=fontDefault]
4087
+ * @param {string} [fontStyle]
4069
4088
  * @param {number} [maxWidth]
4070
4089
  * @memberof Draw */
4071
- 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)
4072
4091
  {
4073
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, maxWidth, overlayContext);
4092
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
4074
4093
  }
4075
4094
 
4076
4095
  /** Draw text on overlay canvas in screen space
@@ -4083,10 +4102,11 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
4083
4102
  * @param {Color} [lineColor=(0,0,0,1)]
4084
4103
  * @param {CanvasTextAlign} [textAlign]
4085
4104
  * @param {string} [font=fontDefault]
4105
+ * @param {string} [fontStyle]
4086
4106
  * @param {number} [maxWidth]
4087
4107
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
4088
4108
  * @memberof Draw */
4089
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
4109
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
4090
4110
  {
4091
4111
  ASSERT(isString(text), 'text must be a string');
4092
4112
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4094,15 +4114,15 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4094
4114
  ASSERT(isColor(color), 'color must be a color');
4095
4115
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4096
4116
  ASSERT(isColor(lineColor), 'lineColor must be a color');
4097
- ASSERT(isColor(lineColor), 'lineColor must be a color');
4098
4117
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
4099
4118
  ASSERT(isString(font), 'font must be a string');
4119
+ ASSERT(isString(fontStyle), 'fontStyle must be a string');
4100
4120
 
4101
4121
  context.fillStyle = color.toString();
4102
4122
  context.strokeStyle = lineColor.toString();
4103
4123
  context.lineWidth = lineWidth;
4104
4124
  context.textAlign = textAlign;
4105
- context.font = size + 'px '+ font;
4125
+ context.font = fontStyle + ' ' + size + 'px '+ font;
4106
4126
  context.textBaseline = 'middle';
4107
4127
 
4108
4128
  const lines = (text+'').split('\n');
@@ -4310,11 +4330,11 @@ let engineFontImage;
4310
4330
  class FontImage
4311
4331
  {
4312
4332
  /** Create an image font
4313
- * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
4314
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
4315
- * @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
4316
4336
  */
4317
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
4337
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
4318
4338
  {
4319
4339
  // load default font image
4320
4340
  if (!engineFontImage)
@@ -5327,8 +5347,16 @@ class SoundInstance
5327
5347
  this.stop();
5328
5348
  this.gainNode = audioContext.createGain();
5329
5349
  this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
5330
- this.startTime = audioContext.currentTime - offset;
5331
- 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
+ }
5332
5360
  }
5333
5361
 
5334
5362
  /** Set the volume of this sound instance
@@ -5477,12 +5505,19 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
5477
5505
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
5478
5506
  * @param {number} [offset] - Offset in seconds to start playback from
5479
5507
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
5480
- * @return {AudioBufferSourceNode} - The audio node of the sound played
5508
+ * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
5481
5509
  * @memberof Audio */
5482
5510
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
5483
5511
  {
5484
5512
  if (!soundEnable || headlessMode) return;
5485
5513
 
5514
+ if (!audioIsRunning())
5515
+ {
5516
+ // fix stalled audio, this sound won't be able to play
5517
+ audioContext.resume();
5518
+ return;
5519
+ }
5520
+
5486
5521
  // create buffer and source
5487
5522
  const channelCount = sampleChannels.length;
5488
5523
  const sampleLength = sampleChannels[0].length;
@@ -5508,14 +5543,9 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
5508
5543
  if (onended)
5509
5544
  source.addEventListener('ended', ()=> onended(source));
5510
5545
 
5546
+ // play and return sound
5511
5547
  const startOffset = offset * rate;
5512
- if (!audioIsRunning())
5513
- {
5514
- // fix stalled audio and start
5515
- audioContext.resume().then(()=>source.start(0, startOffset));
5516
- }
5517
- else
5518
- source.start(0, startOffset);
5548
+ source.start(0, startOffset);
5519
5549
  return source;
5520
5550
  }
5521
5551
 
@@ -5871,10 +5901,13 @@ class CanvasLayer extends EngineObject
5871
5901
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
5872
5902
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
5873
5903
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5874
- this.context = headlessMode ? undefined : this.canvas.getContext('2d');
5875
- /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
5876
- this.glTexture = undefined;
5877
- 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;
5878
5911
  }
5879
5912
 
5880
5913
  /** Destroy this canvas layer */
@@ -5883,9 +5916,7 @@ class CanvasLayer extends EngineObject
5883
5916
  if (this.destroyed)
5884
5917
  return;
5885
5918
 
5886
- // free up the WebGL texture
5887
- if (this.glTexture)
5888
- glDeleteTexture(this.glTexture);
5919
+ this.textureInfo.destroyWebGLTexture();
5889
5920
  super.destroy();
5890
5921
  }
5891
5922
 
@@ -5908,8 +5939,8 @@ class CanvasLayer extends EngineObject
5908
5939
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5909
5940
  {
5910
5941
  // draw the canvas layer as a single tile that uses the whole texture
5911
- const useWebGL = glEnable && this.glTexture !== undefined;
5912
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5942
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
5943
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5913
5944
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5914
5945
  }
5915
5946
 
@@ -5927,6 +5958,8 @@ class CanvasLayer extends EngineObject
5927
5958
  * @param {Canvas2DDrawCallback} drawFunction */
5928
5959
  drawCanvas2D(pos, size, angle, mirror, drawFunction)
5929
5960
  {
5961
+ if (!this.context) return;
5962
+
5930
5963
  const context = this.context;
5931
5964
  context.save();
5932
5965
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
@@ -5979,15 +6012,10 @@ class CanvasLayer extends EngineObject
5979
6012
  * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5980
6013
  useWebGL(enable=true)
5981
6014
  {
5982
- if (glEnable && enable)
5983
- {
5984
- if (this.glTexture)
5985
- glSetTextureData(this.glTexture, this.canvas);
5986
- else
5987
- this.glTexture = glCreateTexture(this.canvas);
5988
- }
6015
+ if (enable)
6016
+ this.textureInfo.createWebGLTexture();
5989
6017
  else
5990
- this.glTexture = undefined;
6018
+ this.textureInfo.destroyWebGLTexture();
5991
6019
  }
5992
6020
  }
5993
6021
 
@@ -6011,24 +6039,14 @@ class TileLayer extends CanvasLayer
6011
6039
  * @param {Vector2} size - World space size
6012
6040
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6013
6041
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6014
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
6015
6042
  */
6016
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
6043
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
6017
6044
  {
6018
- super(position, size, 0, renderOrder, size);
6019
-
6020
- this.tileInfo = tileInfo;
6021
6045
  const canvasSize = size.multiply(tileInfo.size);
6022
- /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
6023
- this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
6024
- /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
6025
- this.context = this.canvas.getContext('2d');
6026
- /** @property {WebGLTexture} - Texture if using WebGL for this layer */
6027
- this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
6028
- // set no friction by default, applied friction is max of both objects
6029
- this.friction = 0;
6030
- // set no restitution by default, applied restitution is max of both objects
6031
- this.restitution = 0;
6046
+ super(position, size, 0, renderOrder, canvasSize);
6047
+
6048
+ // set tile info
6049
+ this.tileInfo = tileInfo;
6032
6050
 
6033
6051
  // init tile data
6034
6052
  this.data = [];
@@ -6078,10 +6096,10 @@ class TileLayer extends CanvasLayer
6078
6096
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
6079
6097
 
6080
6098
  // draw the tile layer as a single tile
6081
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
6099
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6082
6100
  const size = this.drawSize || this.size;
6083
6101
  const pos = this.pos.add(size.scale(.5));
6084
- const useWebGL = glEnable && this.glTexture !== undefined;
6102
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
6085
6103
  drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
6086
6104
  }
6087
6105
 
@@ -6094,8 +6112,7 @@ class TileLayer extends CanvasLayer
6094
6112
  for (let y = this.size.y; y--;)
6095
6113
  this.drawTileData(vec2(x,y), false);
6096
6114
  this.redrawEnd();
6097
- if (this.glTexture)
6098
- this.useWebGL(); // update WebGL texture
6115
+ this.useWebGL();
6099
6116
  }
6100
6117
 
6101
6118
  /** Call to start the redraw process
@@ -6103,6 +6120,8 @@ class TileLayer extends CanvasLayer
6103
6120
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
6104
6121
  redrawStart(clear=false)
6105
6122
  {
6123
+ if (!this.context) return;
6124
+
6106
6125
  // save current render settings
6107
6126
  /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
6108
6127
  this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
@@ -6131,6 +6150,8 @@ class TileLayer extends CanvasLayer
6131
6150
  /** Call to end the redraw process */
6132
6151
  redrawEnd()
6133
6152
  {
6153
+ if (!this.context) return;
6154
+
6134
6155
  ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
6135
6156
  glCopyToContext(drawContext);
6136
6157
  //debugSaveCanvas(this.canvas);
@@ -6147,6 +6168,8 @@ class TileLayer extends CanvasLayer
6147
6168
  */
6148
6169
  drawTileData(layerPos, clear=true)
6149
6170
  {
6171
+ if (!this.context) return;
6172
+
6150
6173
  // clear out where the tile was, for full opaque tiles this can be skipped
6151
6174
  const s = this.tileInfo.size;
6152
6175
  if (clear)
@@ -6183,11 +6206,10 @@ class TileCollisionLayer extends TileLayer
6183
6206
  * @param {Vector2} size - World space size
6184
6207
  * @param {TileInfo} [tileInfo] - Tile info for layer
6185
6208
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6186
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
6187
6209
  */
6188
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
6210
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
6189
6211
  {
6190
- super(position, size.floor(), tileInfo, renderOrder, useWebGL);
6212
+ super(position, size.floor(), tileInfo, renderOrder);
6191
6213
 
6192
6214
  /** @property {Array<number>} - The tile collision grid */
6193
6215
  this.collisionData = [];
@@ -6355,7 +6377,7 @@ class TileCollisionLayer extends TileLayer
6355
6377
  * tile(0, 16), // tileInfo
6356
6378
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
6357
6379
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
6358
- * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
6380
+ * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
6359
6381
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
6360
6382
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
6361
6383
  * );
@@ -6929,7 +6951,7 @@ let glContext;
6929
6951
  let glAntialias = true;
6930
6952
 
6931
6953
  // WebGL internal variables not exposed to documentation
6932
- 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;
6933
6955
 
6934
6956
  // WebGL internal constants
6935
6957
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6945,6 +6967,9 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
6945
6967
  // Initialize WebGL, called automatically by the engine
6946
6968
  function glInit()
6947
6969
  {
6970
+ // keep set of texture infos so they can be restored if context is lost
6971
+ glTextureInfos = new Set;
6972
+
6948
6973
  if (!glEnable || headlessMode) return;
6949
6974
 
6950
6975
  // create the canvas and textures
@@ -6962,68 +6987,101 @@ function glInit()
6962
6987
  // create the WebGL canvas
6963
6988
  const rootElement = mainCanvas.parentElement;
6964
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...');
6965
7013
 
6966
- // setup instanced rendering shader program
6967
- glShader = glCreateProgram(
6968
- '#version 300 es\n' + // specify GLSL ES version
6969
- 'precision highp float;'+ // use highp for better accuracy
6970
- 'uniform mat4 m;'+ // transform matrix
6971
- 'in vec2 g;'+ // in: geometry
6972
- 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
6973
- 'in float r;'+ // in: rotation
6974
- 'out vec2 v;'+ // out: uv
6975
- 'out vec4 d,e;'+ // out: color, additiveColor
6976
- 'void main(){'+ // shader entry point
6977
- 'vec2 s=(g-.5)*p.zw;'+ // get size offset
6978
- 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
6979
- 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
6980
- 'd=c;e=a;'+ // pass colors to fragment shader
6981
- '}' // end of shader
6982
- ,
6983
- '#version 300 es\n' + // specify GLSL ES version
6984
- 'precision highp float;'+ // use highp for better accuracy
6985
- 'uniform sampler2D s;'+ // texture
6986
- 'in vec2 v;'+ // in: uv
6987
- 'in vec4 d,e;'+ // in: color, additiveColor
6988
- 'out vec4 c;'+ // out: color
6989
- 'void main(){'+ // shader entry point
6990
- 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
6991
- '}' // end of shader
6992
- );
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
+ });
6993
7020
 
6994
- // setup poly rendering shaders
6995
- glPolyShader = glCreateProgram(
6996
- '#version 300 es\n' + // specify GLSL ES version
6997
- 'precision highp float;'+ // use highp for better accuracy
6998
- 'uniform mat4 m;'+ // transform matrix
6999
- 'in vec2 p;'+ // in: position
7000
- 'in vec4 c;'+ // in: color
7001
- 'out vec4 d;'+ // out: color
7002
- 'void main(){'+ // shader entry point
7003
- 'gl_Position=m*vec4(p,1,1);'+ // transform position
7004
- 'd=c;'+ // pass color to fragment shader
7005
- '}' // end of shader
7006
- ,
7007
- '#version 300 es\n' + // specify GLSL ES version
7008
- 'precision highp float;'+ // use highp for better accuracy
7009
- 'in vec4 d;'+ // in: color
7010
- 'out vec4 c;'+ // out: color
7011
- 'void main(){'+ // shader entry point
7012
- 'c=d;'+ // set color
7013
- '}' // end of shader
7014
- );
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
+ );
7050
+
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
+ );
7015
7072
 
7016
- // init buffers
7017
- const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
7018
- glPositionData = new Float32Array(glInstanceData);
7019
- glColorData = new Uint32Array(glInstanceData);
7020
- glArrayBuffer = glContext.createBuffer();
7021
- glGeometryBuffer = glContext.createBuffer();
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();
7022
7079
 
7023
- // create the geometry buffer, triangle strip square
7024
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
7025
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7026
- glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
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
+ }
7027
7085
  }
7028
7086
 
7029
7087
  function glSetInstancedMode()
@@ -7130,7 +7188,7 @@ function glPreRender()
7130
7188
  // start with additive blending off
7131
7189
  glAdditive = glBatchAdditive = false;
7132
7190
 
7133
- // force it to enter instanced mode
7191
+ // force it to set instanced mode by first setting poly mode true
7134
7192
  glPolyMode = true;
7135
7193
  glSetInstancedMode();
7136
7194
  }
@@ -7271,6 +7329,41 @@ function glSetTextureData(texture, image)
7271
7329
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
7272
7330
  }
7273
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
+
7274
7367
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
7275
7368
  * @memberof WebGL */
7276
7369
  function glFlush()
@@ -7861,80 +7954,97 @@ class PostProcessPlugin
7861
7954
  /** Create global post processing shader
7862
7955
  * @param {string} shaderCode
7863
7956
  * @param {boolean} [includeOverlay]
7957
+ * @param {boolean} [includeMainCanvas]
7864
7958
  * @example
7865
7959
  * // create the post process plugin object
7866
7960
  * new PostProcessPlugin(shaderCode);
7867
7961
  */
7868
- constructor(shaderCode, includeOverlay=false)
7962
+ constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
7869
7963
  {
7870
7964
  ASSERT(!postProcess, 'Post process already initialized');
7871
7965
  postProcess = this;
7872
7966
 
7873
- if (headlessMode) return;
7874
-
7875
- if (!glEnable)
7876
- {
7877
- console.warn('PostProcessPlugin: WebGL not enabled!');
7878
- return;
7879
- }
7880
-
7881
7967
  if (!shaderCode) // default shader pass through
7882
7968
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
7883
7969
 
7884
7970
  /** @property {WebGLProgram} - Shader for post processing */
7885
- this.shader = glCreateProgram(
7886
- '#version 300 es\n' + // specify GLSL ES version
7887
- 'precision highp float;'+ // use highp for better accuracy
7888
- 'in vec2 p;'+ // position
7889
- 'void main(){'+ // shader entry point
7890
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7891
- '}' // end of shader
7892
- ,
7893
- '#version 300 es\n' + // specify GLSL ES version
7894
- 'precision highp float;'+ // use highp for better accuracy
7895
- 'uniform sampler2D iChannel0;'+ // input texture
7896
- 'uniform vec3 iResolution;'+ // size of output texture
7897
- 'uniform float iTime;'+ // time
7898
- 'out vec4 c;'+ // out color
7899
- '\n' + shaderCode + '\n'+ // insert custom shader code
7900
- 'void main(){'+ // shader entry point
7901
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
7902
- 'c.a=1.;'+ // always use full alpha
7903
- '}' // end of shader
7904
- );
7971
+ this.shader = undefined;
7905
7972
 
7906
7973
  /** @property {WebGLTexture} - Texture for post processing */
7907
- this.texture = glCreateTexture();
7974
+ this.texture = undefined;
7908
7975
 
7909
- /** @property {boolean} - Should overlay canvas be included in post processing */
7910
- this.includeOverlay = includeOverlay;
7976
+ // setup the post processing plugin
7977
+ initPostProcess();
7978
+ engineAddPlugin(undefined, postProcessRender, postProcessContextLost, postProcessContextRestored);
7911
7979
 
7912
- // Render the post processing shader, called automatically by the engine
7913
- engineAddPlugin(undefined, postProcessRender);
7914
- function postProcessRender()
7980
+ function initPostProcess()
7915
7981
  {
7916
7982
  if (headlessMode) return;
7917
-
7918
- // prepare to render post process shader
7919
- if (glEnable)
7920
- {
7921
- glFlush(); // clear out the buffer
7922
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
7923
- }
7924
- else
7983
+
7984
+ if (!glEnable)
7925
7985
  {
7926
- // set the viewport
7927
- glContext.viewport(0, 0, glCanvas.width = drawCanvas.width, glCanvas.height = drawCanvas.height);
7986
+ console.warn('PostProcessPlugin: WebGL not enabled!');
7987
+ return;
7928
7988
  }
7929
7989
 
7930
- 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)
7931
8035
  {
7932
- // copy overlay canvas so it will be included in post processing
7933
- mainContext.drawImage(overlayCanvas, 0, 0);
7934
- 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
+ }
7935
8045
  }
7936
8046
 
7937
- // setup shader program to draw one triangle
8047
+ // setup shader program to draw a quad
7938
8048
  glContext.useProgram(postProcess.shader);
7939
8049
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7940
8050
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
@@ -7943,7 +8053,10 @@ class PostProcessPlugin
7943
8053
  // set textures, pass in the 2d canvas and gl canvas in separate texture channels
7944
8054
  glContext.activeTexture(glContext.TEXTURE0);
7945
8055
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
7946
- 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
+ }
7947
8060
 
7948
8061
  // set vertex position attribute
7949
8062
  const vertexByteStride = 8;
@@ -8173,6 +8286,8 @@ class UISystemPlugin
8173
8286
  this.defaultHoverColor = hsl(0,0,.9);
8174
8287
  /** @property {Color} - Default color for disabled UI elements */
8175
8288
  this.defaultDisabledColor = hsl(0,0,.3);
8289
+ /** @property {Color} - Uses a gradient fill combined with color */
8290
+ this.defaultGradientColor = undefined;
8176
8291
  /** @property {number} - Default line width for UI elements */
8177
8292
  this.defaultLineWidth = 4;
8178
8293
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -8180,7 +8295,7 @@ class UISystemPlugin
8180
8295
  /** @property {number} - Default scale to use for fitting text to object */
8181
8296
  this.defaultTextScale = .8;
8182
8297
  /** @property {string} - Default font for UI elements */
8183
- this.defaultFont = 'arial';
8298
+ this.defaultFont = fontDefault;
8184
8299
  /** @property {Sound} - Default sound when interactive UI element is pressed */
8185
8300
  this.defaultSoundPress = undefined;
8186
8301
  /** @property {Sound} - Default sound when interactive UI element is released */
@@ -8197,7 +8312,9 @@ class UISystemPlugin
8197
8312
  this.hoverObject = undefined;
8198
8313
  /** @property {UIObject} - Hover object at start of update */
8199
8314
  this.lastHoverObject = undefined;
8200
-
8315
+ /** @property {number} - If set ui coords will be renormalized to this canvas height */
8316
+ this.nativeHeight = 0;
8317
+
8201
8318
  engineAddPlugin(uiUpdate, uiRender);
8202
8319
 
8203
8320
  // setup recursive update and render
@@ -8237,6 +8354,17 @@ class UISystemPlugin
8237
8354
  }
8238
8355
  function uiRender()
8239
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
+
8240
8368
  function renderObject(o)
8241
8369
  {
8242
8370
  if (!o.visible)
@@ -8248,6 +8376,7 @@ class UISystemPlugin
8248
8376
  renderObject(c);
8249
8377
  }
8250
8378
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
8379
+ context.restore();
8251
8380
  }
8252
8381
  }
8253
8382
 
@@ -8257,8 +8386,9 @@ class UISystemPlugin
8257
8386
  * @param {Color} [color=uiSystem.defaultColor]
8258
8387
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
8259
8388
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
8260
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
8261
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
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)
8262
8392
  {
8263
8393
  ASSERT(isVector2(pos), 'pos must be a vec2');
8264
8394
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -8268,7 +8398,18 @@ class UISystemPlugin
8268
8398
  ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
8269
8399
 
8270
8400
  const context = uiSystem.uiContext;
8271
- 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();
8272
8413
  context.beginPath();
8273
8414
  if (cornerRadius && context['roundRect'])
8274
8415
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
@@ -8325,10 +8466,11 @@ class UISystemPlugin
8325
8466
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
8326
8467
  * @param {string} [align]
8327
8468
  * @param {string} [font=uiSystem.defaultFont]
8469
+ * @param {string} [fontStyle]
8328
8470
  * @param {boolean} [applyMaxWidth=true] */
8329
- 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)
8330
8472
  {
8331
- 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);
8332
8474
  }
8333
8475
 
8334
8476
  /**
@@ -8389,17 +8531,21 @@ class UIObject
8389
8531
  /** @property {boolean} - Is this object disabled? */
8390
8532
  this.disabled = false;
8391
8533
  /** @property {Color} - Color for text */
8392
- this.textColor = uiSystem.defaultTextColor.copy()
8534
+ this.textColor = uiSystem.defaultTextColor.copy();
8393
8535
  /** @property {Color} - Color used when hovering over the object */
8394
- this.hoverColor = uiSystem.defaultHoverColor.copy()
8536
+ this.hoverColor = uiSystem.defaultHoverColor.copy();
8395
8537
  /** @property {Color} - Color for line drawing */
8396
- this.lineColor = uiSystem.defaultLineColor.copy()
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;
8397
8541
  /** @property {number} - Width for line drawing */
8398
8542
  this.lineWidth = uiSystem.defaultLineWidth;
8399
8543
  /** @property {number} - Corner radius for rounded rects */
8400
8544
  this.cornerRadius = uiSystem.defaultCornerRadius;
8401
8545
  /** @property {string} - Font for this objecct */
8402
8546
  this.font = uiSystem.defaultFont;
8547
+ /** @property {string} - Font style for this object or undefined */
8548
+ this.fontStyle = undefined;
8403
8549
  /** @property {number} - Override for text width */
8404
8550
  this.textWidth = undefined;
8405
8551
  /** @property {number} - Override for text height */
@@ -8424,6 +8570,8 @@ class UIObject
8424
8570
  this.interactive = false;
8425
8571
  /** @property {boolean} - Activate when dragged over with mouse held down */
8426
8572
  this.dragActivate = false;
8573
+ /** @property {boolean} - True if this can be a hover object */
8574
+ this.canBeHover = true;
8427
8575
  uiSystem.uiObjects.push(this);
8428
8576
  }
8429
8577
 
@@ -8447,6 +8595,26 @@ class UIObject
8447
8595
  child.parent = undefined;
8448
8596
  }
8449
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
+
8450
8618
  /** Update the object, called automatically by plugin once each frame */
8451
8619
  update()
8452
8620
  {
@@ -8454,13 +8622,10 @@ class UIObject
8454
8622
  const isActive = this.isActiveObject();
8455
8623
  const mouseDown = mouseIsDown(0);
8456
8624
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
8457
- if (!uiSystem.hoverObject)
8625
+ if (this.canBeHover)
8458
8626
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
8459
- {
8460
- const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
8461
- if (isOverlapping(this.pos, size, mousePosScreen))
8462
- uiSystem.hoverObject = this;
8463
- }
8627
+ if (!uiSystem.hoverObject && this.isMouseOverlapping())
8628
+ uiSystem.hoverObject = this;
8464
8629
  if (this.isHoverObject())
8465
8630
  {
8466
8631
  if (!this.disabled)
@@ -8585,11 +8750,13 @@ class UIText extends UIObject
8585
8750
 
8586
8751
  // make text not outlined by default
8587
8752
  this.lineWidth = 0;
8753
+ // text can not be a hover object by default
8754
+ this.canBeHover = false;
8588
8755
  }
8589
8756
  render()
8590
8757
  {
8591
8758
  const textSize = this.getTextSize();
8592
- 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);
8593
8760
  }
8594
8761
  }
8595
8762
 
@@ -8655,7 +8822,7 @@ class UIButton extends UIObject
8655
8822
 
8656
8823
  // set properties
8657
8824
  this.text = text;
8658
- this.color = color.copy()
8825
+ this.color = color.copy();
8659
8826
  this.interactive = true;
8660
8827
  }
8661
8828
  render()
@@ -8665,7 +8832,7 @@ class UIButton extends UIObject
8665
8832
  // draw the text scaled to fit
8666
8833
  const textSize = this.getTextSize();
8667
8834
  uiSystem.drawText(this.text, this.pos, textSize,
8668
- this.textColor, 0, undefined, this.align, this.font);
8835
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8669
8836
  }
8670
8837
  }
8671
8838
 
@@ -8719,7 +8886,7 @@ class UICheckbox extends UIObject
8719
8886
  const textSize = this.getTextSize();
8720
8887
  const pos = this.pos.add(vec2(this.size.x,0));
8721
8888
  uiSystem.drawText(this.text, pos, textSize,
8722
- this.textColor, 0, undefined, 'left', this.font, false);
8889
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
8723
8890
  }
8724
8891
  }
8725
8892
 
@@ -8797,14 +8964,13 @@ class UIScrollbar extends UIObject
8797
8964
  const handlePos = isHorizontal ?
8798
8965
  vec2(lerp(p1, p2, this.value), this.pos.y) :
8799
8966
  vec2(this.pos.x, lerp(p2, p1, this.value))
8800
- const handleColor = this.disabled ? this.disabledColor :
8801
- this.interactive && this.isActiveObject() ? this.color : this.handleColor;
8967
+ const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8802
8968
  uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8803
8969
 
8804
8970
  // draw the text scaled to fit on the scrollbar
8805
8971
  const textSize = this.getTextSize();
8806
8972
  uiSystem.drawText(this.text, this.pos, textSize,
8807
- this.textColor, 0, undefined, this.align, this.font);
8973
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8808
8974
  }
8809
8975
  }
8810
8976
  /**