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
  }
@@ -2987,16 +3001,15 @@ class TileInfo
2987
3001
  }
2988
3002
 
2989
3003
  /**
2990
- * Set this tile to use a full image
2991
- * @param {HTMLImageElement|OffscreenCanvas} image
2992
- * @param {WebGLTexture} [glTexture] - WebGL texture
3004
+ * Set this tile to use a full image in a texture info
3005
+ * @param {TextureInfo} textureInfo
2993
3006
  * @return {TileInfo}
2994
3007
  */
2995
- setFullImage(image, glTexture)
3008
+ setFullImage(textureInfo)
2996
3009
  {
2997
3010
  this.pos = new Vector2;
2998
- this.size = new Vector2(image.width, image.height);
2999
- this.textureInfo = new TextureInfo(image, glTexture);
3011
+ this.size = textureInfo.size.copy();
3012
+ this.textureInfo = textureInfo;
3000
3013
  // do not use padding or bleed
3001
3014
  this.bleedScale = this.padding = 0;
3002
3015
  return this;
@@ -3012,26 +3025,30 @@ class TextureInfo
3012
3025
  /**
3013
3026
  * Create a TextureInfo, called automatically by the engine
3014
3027
  * @param {HTMLImageElement|OffscreenCanvas} image
3015
- * @param {WebGLTexture} [glTexture] - WebGL texture
3028
+ * @param {boolean} [useWebGL] - Should use WebGL if available?
3016
3029
  */
3017
- constructor(image, glTexture)
3030
+ constructor(image, useWebGL=true)
3018
3031
  {
3019
- /** @property {HTMLImageElement} - image source */
3032
+ /** @property {HTMLImageElement|OffscreenCanvas} - image source */
3020
3033
  this.image = image;
3021
3034
  /** @property {Vector2} - size of the image */
3022
- this.size = vec2(image.width, image.height);
3035
+ this.size = image ? vec2(image.width, image.height) : vec2();
3023
3036
  /** @property {Vector2} - inverse of the size, cached for rendering */
3024
- this.sizeInverse = vec2(1/image.width, 1/image.height);
3037
+ this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
3025
3038
  /** @property {WebGLTexture} - WebGL texture */
3026
- this.glTexture = glTexture;
3039
+ this.glTexture = undefined;
3040
+ useWebGL && this.createWebGLTexture();
3027
3041
  }
3028
3042
 
3029
- createWebGLTexture()
3030
- {
3031
- ASSERT(!this.glTexture);
3032
- if (glEnable)
3033
- this.glTexture = glCreateTexture(this.image);
3034
- }
3043
+ /** Creates the WebGL texture, updates if already created */
3044
+ createWebGLTexture() { glRegisterTextureInfo(this); }
3045
+
3046
+ /** Destroys the WebGL texture */
3047
+ destroyWebGLTexture() { glUnregisterTextureInfo(this); }
3048
+
3049
+ /** Check if the texture is webgl enabled
3050
+ * @return {boolean} */
3051
+ hasWebGL() { return !!this.glTexture; }
3035
3052
  }
3036
3053
 
3037
3054
  ///////////////////////////////////////////////////////////////////////////////
@@ -3464,12 +3481,13 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3464
3481
  * @param {Color} [lineColor=(0,0,0,1)]
3465
3482
  * @param {CanvasTextAlign} [textAlign='center']
3466
3483
  * @param {string} [font=fontDefault]
3484
+ * @param {string} [fontStyle]
3467
3485
  * @param {number} [maxWidth]
3468
3486
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3469
3487
  * @memberof Draw */
3470
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth, context=drawContext)
3488
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
3471
3489
  {
3472
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, maxWidth, context);
3490
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
3473
3491
  }
3474
3492
 
3475
3493
  /** Draw text on overlay canvas in world space
@@ -3482,11 +3500,12 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3482
3500
  * @param {Color} [lineColor=(0,0,0,1)]
3483
3501
  * @param {CanvasTextAlign} [textAlign='center']
3484
3502
  * @param {string} [font=fontDefault]
3503
+ * @param {string} [fontStyle]
3485
3504
  * @param {number} [maxWidth]
3486
3505
  * @memberof Draw */
3487
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth)
3506
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
3488
3507
  {
3489
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, maxWidth, overlayContext);
3508
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
3490
3509
  }
3491
3510
 
3492
3511
  /** Draw text on overlay canvas in screen space
@@ -3499,10 +3518,11 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3499
3518
  * @param {Color} [lineColor=(0,0,0,1)]
3500
3519
  * @param {CanvasTextAlign} [textAlign]
3501
3520
  * @param {string} [font=fontDefault]
3521
+ * @param {string} [fontStyle]
3502
3522
  * @param {number} [maxWidth]
3503
3523
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3504
3524
  * @memberof Draw */
3505
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, maxWidth, context=overlayContext)
3525
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
3506
3526
  {
3507
3527
  ASSERT(isString(text), 'text must be a string');
3508
3528
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3510,15 +3530,15 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3510
3530
  ASSERT(isColor(color), 'color must be a color');
3511
3531
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3512
3532
  ASSERT(isColor(lineColor), 'lineColor must be a color');
3513
- ASSERT(isColor(lineColor), 'lineColor must be a color');
3514
3533
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
3515
3534
  ASSERT(isString(font), 'font must be a string');
3535
+ ASSERT(isString(fontStyle), 'fontStyle must be a string');
3516
3536
 
3517
3537
  context.fillStyle = color.toString();
3518
3538
  context.strokeStyle = lineColor.toString();
3519
3539
  context.lineWidth = lineWidth;
3520
3540
  context.textAlign = textAlign;
3521
- context.font = size + 'px '+ font;
3541
+ context.font = fontStyle + ' ' + size + 'px '+ font;
3522
3542
  context.textBaseline = 'middle';
3523
3543
 
3524
3544
  const lines = (text+'').split('\n');
@@ -3726,11 +3746,11 @@ let engineFontImage;
3726
3746
  class FontImage
3727
3747
  {
3728
3748
  /** Create an image font
3729
- * @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
3730
- * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3731
- * @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
3749
+ * @param {HTMLImageElement} [image] - Image for the font, default if undefined
3750
+ * @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
3751
+ * @param {Vector2} [paddingSize=(0,1)] - How much space between characters
3732
3752
  */
3733
- constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
3753
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1))
3734
3754
  {
3735
3755
  // load default font image
3736
3756
  if (!engineFontImage)
@@ -4743,8 +4763,16 @@ class SoundInstance
4743
4763
  this.stop();
4744
4764
  this.gainNode = audioContext.createGain();
4745
4765
  this.source = playSamples(this.sound.sampleChannels, this.volume, this.rate, this.pan, this.loop, this.sound.sampleRate, this.gainNode, offset, this.onendedCallback);
4746
- this.startTime = audioContext.currentTime - offset;
4747
- this.pausedTime = undefined;
4766
+ if (this.source)
4767
+ {
4768
+ this.startTime = audioContext.currentTime - offset;
4769
+ this.pausedTime = undefined;
4770
+ }
4771
+ else
4772
+ {
4773
+ this.startTime = undefined;
4774
+ this.pausedTime = 0;
4775
+ }
4748
4776
  }
4749
4777
 
4750
4778
  /** Set the volume of this sound instance
@@ -4893,12 +4921,19 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
4893
4921
  * @param {GainNode} [gainNode] - Optional gain node for volume control while playing
4894
4922
  * @param {number} [offset] - Offset in seconds to start playback from
4895
4923
  * @param {AudioEndedCallback} [onended] - Callback for when the sound ends
4896
- * @return {AudioBufferSourceNode} - The audio node of the sound played
4924
+ * @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
4897
4925
  * @memberof Audio */
4898
4926
  function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=audioDefaultSampleRate, gainNode, offset=0, onended)
4899
4927
  {
4900
4928
  if (!soundEnable || headlessMode) return;
4901
4929
 
4930
+ if (!audioIsRunning())
4931
+ {
4932
+ // fix stalled audio, this sound won't be able to play
4933
+ audioContext.resume();
4934
+ return;
4935
+ }
4936
+
4902
4937
  // create buffer and source
4903
4938
  const channelCount = sampleChannels.length;
4904
4939
  const sampleLength = sampleChannels[0].length;
@@ -4924,14 +4959,9 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
4924
4959
  if (onended)
4925
4960
  source.addEventListener('ended', ()=> onended(source));
4926
4961
 
4962
+ // play and return sound
4927
4963
  const startOffset = offset * rate;
4928
- if (!audioIsRunning())
4929
- {
4930
- // fix stalled audio and start
4931
- audioContext.resume().then(()=>source.start(0, startOffset));
4932
- }
4933
- else
4934
- source.start(0, startOffset);
4964
+ source.start(0, startOffset);
4935
4965
  return source;
4936
4966
  }
4937
4967
 
@@ -5287,10 +5317,13 @@ class CanvasLayer extends EngineObject
5287
5317
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
5288
5318
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
5289
5319
  /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
5290
- this.context = headlessMode ? undefined : this.canvas.getContext('2d');
5291
- /** @property {WebGLTexture} - Texture if using WebGL for this layer, call useWebGL to enable */
5292
- this.glTexture = undefined;
5293
- this.gravityScale = 0; // disable gravity by default for canvas layers
5320
+ this.context = this.canvas?.getContext('2d');
5321
+ /** @property {TextureInfo} - Texture info to use for this object rendering */
5322
+ const useWebGL = false; // do not use webgl by default
5323
+ this.textureInfo = new TextureInfo(this.canvas, useWebGL);
5324
+
5325
+ // disable physics by default
5326
+ this.mass = this.gravityScale = this.friction = this.restitution = 0;
5294
5327
  }
5295
5328
 
5296
5329
  /** Destroy this canvas layer */
@@ -5299,9 +5332,7 @@ class CanvasLayer extends EngineObject
5299
5332
  if (this.destroyed)
5300
5333
  return;
5301
5334
 
5302
- // free up the WebGL texture
5303
- if (this.glTexture)
5304
- glDeleteTexture(this.glTexture);
5335
+ this.textureInfo.destroyWebGLTexture();
5305
5336
  super.destroy();
5306
5337
  }
5307
5338
 
@@ -5324,8 +5355,8 @@ class CanvasLayer extends EngineObject
5324
5355
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5325
5356
  {
5326
5357
  // draw the canvas layer as a single tile that uses the whole texture
5327
- const useWebGL = glEnable && this.glTexture !== undefined;
5328
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5358
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
5359
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5329
5360
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5330
5361
  }
5331
5362
 
@@ -5343,6 +5374,8 @@ class CanvasLayer extends EngineObject
5343
5374
  * @param {Canvas2DDrawCallback} drawFunction */
5344
5375
  drawCanvas2D(pos, size, angle, mirror, drawFunction)
5345
5376
  {
5377
+ if (!this.context) return;
5378
+
5346
5379
  const context = this.context;
5347
5380
  context.save();
5348
5381
  pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
@@ -5395,15 +5428,10 @@ class CanvasLayer extends EngineObject
5395
5428
  * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5396
5429
  useWebGL(enable=true)
5397
5430
  {
5398
- if (glEnable && enable)
5399
- {
5400
- if (this.glTexture)
5401
- glSetTextureData(this.glTexture, this.canvas);
5402
- else
5403
- this.glTexture = glCreateTexture(this.canvas);
5404
- }
5431
+ if (enable)
5432
+ this.textureInfo.createWebGLTexture();
5405
5433
  else
5406
- this.glTexture = undefined;
5434
+ this.textureInfo.destroyWebGLTexture();
5407
5435
  }
5408
5436
  }
5409
5437
 
@@ -5427,24 +5455,14 @@ class TileLayer extends CanvasLayer
5427
5455
  * @param {Vector2} size - World space size
5428
5456
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
5429
5457
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5430
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5431
5458
  */
5432
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
5459
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
5433
5460
  {
5434
- super(position, size, 0, renderOrder, size);
5435
-
5436
- this.tileInfo = tileInfo;
5437
5461
  const canvasSize = size.multiply(tileInfo.size);
5438
- /** @property {HTMLCanvasElement} - The canvas used by this tile layer */
5439
- this.canvas = new OffscreenCanvas(canvasSize.x, canvasSize.y);
5440
- /** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
5441
- this.context = this.canvas.getContext('2d');
5442
- /** @property {WebGLTexture} - Texture if using WebGL for this layer */
5443
- this.glTexture = useWebGL ? glCreateTexture(this.canvas) : undefined;
5444
- // set no friction by default, applied friction is max of both objects
5445
- this.friction = 0;
5446
- // set no restitution by default, applied restitution is max of both objects
5447
- this.restitution = 0;
5462
+ super(position, size, 0, renderOrder, canvasSize);
5463
+
5464
+ // set tile info
5465
+ this.tileInfo = tileInfo;
5448
5466
 
5449
5467
  // init tile data
5450
5468
  this.data = [];
@@ -5494,10 +5512,10 @@ class TileLayer extends CanvasLayer
5494
5512
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5495
5513
 
5496
5514
  // draw the tile layer as a single tile
5497
- const tileInfo = new TileInfo().setFullImage(this.canvas, this.glTexture);
5515
+ const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5498
5516
  const size = this.drawSize || this.size;
5499
5517
  const pos = this.pos.add(size.scale(.5));
5500
- const useWebGL = glEnable && this.glTexture !== undefined;
5518
+ const useWebGL = glEnable && this.textureInfo.hasWebGL();
5501
5519
  drawTile(pos, size, tileInfo, WHITE, 0, false, CLEAR_BLACK, useWebGL);
5502
5520
  }
5503
5521
 
@@ -5510,8 +5528,7 @@ class TileLayer extends CanvasLayer
5510
5528
  for (let y = this.size.y; y--;)
5511
5529
  this.drawTileData(vec2(x,y), false);
5512
5530
  this.redrawEnd();
5513
- if (this.glTexture)
5514
- this.useWebGL(); // update WebGL texture
5531
+ this.useWebGL();
5515
5532
  }
5516
5533
 
5517
5534
  /** Call to start the redraw process
@@ -5519,6 +5536,8 @@ class TileLayer extends CanvasLayer
5519
5536
  * @param {boolean} [clear] - Should it clear the canvas before drawing */
5520
5537
  redrawStart(clear=false)
5521
5538
  {
5539
+ if (!this.context) return;
5540
+
5522
5541
  // save current render settings
5523
5542
  /** @type {[HTMLCanvasElement|OffscreenCanvas, CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number]} */
5524
5543
  this.savedRenderSettings = [drawCanvas, drawContext, mainCanvasSize, cameraPos, cameraScale];
@@ -5547,6 +5566,8 @@ class TileLayer extends CanvasLayer
5547
5566
  /** Call to end the redraw process */
5548
5567
  redrawEnd()
5549
5568
  {
5569
+ if (!this.context) return;
5570
+
5550
5571
  ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
5551
5572
  glCopyToContext(drawContext);
5552
5573
  //debugSaveCanvas(this.canvas);
@@ -5563,6 +5584,8 @@ class TileLayer extends CanvasLayer
5563
5584
  */
5564
5585
  drawTileData(layerPos, clear=true)
5565
5586
  {
5587
+ if (!this.context) return;
5588
+
5566
5589
  // clear out where the tile was, for full opaque tiles this can be skipped
5567
5590
  const s = this.tileInfo.size;
5568
5591
  if (clear)
@@ -5599,11 +5622,10 @@ class TileCollisionLayer extends TileLayer
5599
5622
  * @param {Vector2} size - World space size
5600
5623
  * @param {TileInfo} [tileInfo] - Tile info for layer
5601
5624
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
5602
- * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
5603
5625
  */
5604
- constructor(position, size, tileInfo=tile(), renderOrder=0, useWebGL=glEnable)
5626
+ constructor(position, size, tileInfo=tile(), renderOrder=0)
5605
5627
  {
5606
- super(position, size.floor(), tileInfo, renderOrder, useWebGL);
5628
+ super(position, size.floor(), tileInfo, renderOrder);
5607
5629
 
5608
5630
  /** @property {Array<number>} - The tile collision grid */
5609
5631
  this.collisionData = [];
@@ -5771,7 +5793,7 @@ class TileCollisionLayer extends TileLayer
5771
5793
  * tile(0, 16), // tileInfo
5772
5794
  * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
5773
5795
  * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
5774
- * 2, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5796
+ * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
5775
5797
  * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate,
5776
5798
  * .5, 1 // randomness, collide, additive, randomColorLinear, renderOrder
5777
5799
  * );
@@ -6345,7 +6367,7 @@ let glContext;
6345
6367
  let glAntialias = true;
6346
6368
 
6347
6369
  // WebGL internal variables not exposed to documentation
6348
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount;
6370
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos;
6349
6371
 
6350
6372
  // WebGL internal constants
6351
6373
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6361,6 +6383,9 @@ const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE |
6361
6383
  // Initialize WebGL, called automatically by the engine
6362
6384
  function glInit()
6363
6385
  {
6386
+ // keep set of texture infos so they can be restored if context is lost
6387
+ glTextureInfos = new Set;
6388
+
6364
6389
  if (!glEnable || headlessMode) return;
6365
6390
 
6366
6391
  // create the canvas and textures
@@ -6378,68 +6403,101 @@ function glInit()
6378
6403
  // create the WebGL canvas
6379
6404
  const rootElement = mainCanvas.parentElement;
6380
6405
  rootElement.appendChild(glCanvas);
6406
+
6407
+ // startup webgl
6408
+ initWebGL();
6409
+
6410
+ // setup context lost and restore handlers
6411
+ glCanvas.addEventListener('webglcontextlost', (e)=>
6412
+ {
6413
+ glEnable = false; // disable WebGL rendering
6414
+ glCanvas.style.display = 'none'; // hide the gl canvas
6415
+ e.preventDefault(); // prevent default to allow restoration
6416
+ LOG('WebGL context lost! Switching to Canvas2d rendering.');
6417
+
6418
+ // remove WebGL textures
6419
+ for (const info of glTextureInfos)
6420
+ info.glTexture = undefined;
6421
+ glActiveTexture = undefined;
6422
+ pluginList.forEach(plugin=>plugin.glContextLost?.());
6423
+ });
6424
+ glCanvas.addEventListener('webglcontextrestored', ()=>
6425
+ {
6426
+ glEnable = true; // disable WebGL rendering
6427
+ glCanvas.style.display = ''; // show the gl canvas
6428
+ LOG('WebGL context restored, reinitializing...');
6381
6429
 
6382
- // setup instanced rendering shader program
6383
- glShader = glCreateProgram(
6384
- '#version 300 es\n' + // specify GLSL ES version
6385
- 'precision highp float;'+ // use highp for better accuracy
6386
- 'uniform mat4 m;'+ // transform matrix
6387
- 'in vec2 g;'+ // in: geometry
6388
- 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
6389
- 'in float r;'+ // in: rotation
6390
- 'out vec2 v;'+ // out: uv
6391
- 'out vec4 d,e;'+ // out: color, additiveColor
6392
- 'void main(){'+ // shader entry point
6393
- 'vec2 s=(g-.5)*p.zw;'+ // get size offset
6394
- 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
6395
- 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
6396
- 'd=c;e=a;'+ // pass colors to fragment shader
6397
- '}' // end of shader
6398
- ,
6399
- '#version 300 es\n' + // specify GLSL ES version
6400
- 'precision highp float;'+ // use highp for better accuracy
6401
- 'uniform sampler2D s;'+ // texture
6402
- 'in vec2 v;'+ // in: uv
6403
- 'in vec4 d,e;'+ // in: color, additiveColor
6404
- 'out vec4 c;'+ // out: color
6405
- 'void main(){'+ // shader entry point
6406
- 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
6407
- '}' // end of shader
6408
- );
6430
+ // reinit WebGL and restore textures
6431
+ initWebGL();
6432
+ for (const info of glTextureInfos)
6433
+ info.glTexture = glCreateTexture(info.image);
6434
+ pluginList.forEach(plugin=>plugin.glContextRestored?.());
6435
+ });
6409
6436
 
6410
- // setup poly rendering shaders
6411
- glPolyShader = glCreateProgram(
6412
- '#version 300 es\n' + // specify GLSL ES version
6413
- 'precision highp float;'+ // use highp for better accuracy
6414
- 'uniform mat4 m;'+ // transform matrix
6415
- 'in vec2 p;'+ // in: position
6416
- 'in vec4 c;'+ // in: color
6417
- 'out vec4 d;'+ // out: color
6418
- 'void main(){'+ // shader entry point
6419
- 'gl_Position=m*vec4(p,1,1);'+ // transform position
6420
- 'd=c;'+ // pass color to fragment shader
6421
- '}' // end of shader
6422
- ,
6423
- '#version 300 es\n' + // specify GLSL ES version
6424
- 'precision highp float;'+ // use highp for better accuracy
6425
- 'in vec4 d;'+ // in: color
6426
- 'out vec4 c;'+ // out: color
6427
- 'void main(){'+ // shader entry point
6428
- 'c=d;'+ // set color
6429
- '}' // end of shader
6430
- );
6437
+ function initWebGL()
6438
+ {
6439
+ // setup instanced rendering shader program
6440
+ glShader = glCreateProgram(
6441
+ '#version 300 es\n' + // specify GLSL ES version
6442
+ 'precision highp float;'+ // use highp for better accuracy
6443
+ 'uniform mat4 m;'+ // transform matrix
6444
+ 'in vec2 g;'+ // in: geometry
6445
+ 'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
6446
+ 'in float r;'+ // in: rotation
6447
+ 'out vec2 v;'+ // out: uv
6448
+ 'out vec4 d,e;'+ // out: color, additiveColor
6449
+ 'void main(){'+ // shader entry point
6450
+ 'vec2 s=(g-.5)*p.zw;'+ // get size offset
6451
+ 'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
6452
+ 'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
6453
+ 'd=c;e=a;'+ // pass colors to fragment shader
6454
+ '}' // end of shader
6455
+ ,
6456
+ '#version 300 es\n' + // specify GLSL ES version
6457
+ 'precision highp float;'+ // use highp for better accuracy
6458
+ 'uniform sampler2D s;'+ // texture
6459
+ 'in vec2 v;'+ // in: uv
6460
+ 'in vec4 d,e;'+ // in: color, additiveColor
6461
+ 'out vec4 c;'+ // out: color
6462
+ 'void main(){'+ // shader entry point
6463
+ 'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
6464
+ '}' // end of shader
6465
+ );
6466
+
6467
+ // setup poly rendering shaders
6468
+ glPolyShader = glCreateProgram(
6469
+ '#version 300 es\n' + // specify GLSL ES version
6470
+ 'precision highp float;'+ // use highp for better accuracy
6471
+ 'uniform mat4 m;'+ // transform matrix
6472
+ 'in vec2 p;'+ // in: position
6473
+ 'in vec4 c;'+ // in: color
6474
+ 'out vec4 d;'+ // out: color
6475
+ 'void main(){'+ // shader entry point
6476
+ 'gl_Position=m*vec4(p,1,1);'+ // transform position
6477
+ 'd=c;'+ // pass color to fragment shader
6478
+ '}' // end of shader
6479
+ ,
6480
+ '#version 300 es\n' + // specify GLSL ES version
6481
+ 'precision highp float;'+ // use highp for better accuracy
6482
+ 'in vec4 d;'+ // in: color
6483
+ 'out vec4 c;'+ // out: color
6484
+ 'void main(){'+ // shader entry point
6485
+ 'c=d;'+ // set color
6486
+ '}' // end of shader
6487
+ );
6431
6488
 
6432
- // init buffers
6433
- const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
6434
- glPositionData = new Float32Array(glInstanceData);
6435
- glColorData = new Uint32Array(glInstanceData);
6436
- glArrayBuffer = glContext.createBuffer();
6437
- glGeometryBuffer = glContext.createBuffer();
6489
+ // init buffers
6490
+ const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
6491
+ glPositionData = new Float32Array(glInstanceData);
6492
+ glColorData = new Uint32Array(glInstanceData);
6493
+ glArrayBuffer = glContext.createBuffer();
6494
+ glGeometryBuffer = glContext.createBuffer();
6438
6495
 
6439
- // create the geometry buffer, triangle strip square
6440
- const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
6441
- glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6442
- glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
6496
+ // create the geometry buffer, triangle strip square
6497
+ const geometry = new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);
6498
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
6499
+ glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
6500
+ }
6443
6501
  }
6444
6502
 
6445
6503
  function glSetInstancedMode()
@@ -6546,7 +6604,7 @@ function glPreRender()
6546
6604
  // start with additive blending off
6547
6605
  glAdditive = glBatchAdditive = false;
6548
6606
 
6549
- // force it to enter instanced mode
6607
+ // force it to set instanced mode by first setting poly mode true
6550
6608
  glPolyMode = true;
6551
6609
  glSetInstancedMode();
6552
6610
  }
@@ -6687,6 +6745,41 @@ function glSetTextureData(texture, image)
6687
6745
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
6688
6746
  }
6689
6747
 
6748
+ /** Tells WebGL to create or update the glTexture and start tracking it
6749
+ * @param {TextureInfo} textureInfo
6750
+ * @memberof WebGL */
6751
+ function glRegisterTextureInfo(textureInfo)
6752
+ {
6753
+ if (headlessMode) return;
6754
+
6755
+ // add texture info to tracking list even if gl is not enabled
6756
+ glTextureInfos.add(textureInfo);
6757
+
6758
+ if (!glContext) return;
6759
+
6760
+ // create or set the texture data
6761
+ if (textureInfo.glTexture)
6762
+ glSetTextureData(textureInfo.glTexture, textureInfo.image);
6763
+ else
6764
+ textureInfo.glTexture = glCreateTexture(textureInfo.image);
6765
+ }
6766
+
6767
+ /** Tells WebGL to destroy the glTexture and stop tracking it
6768
+ * @param {TextureInfo} textureInfo
6769
+ * @memberof WebGL */
6770
+ function glUnregisterTextureInfo(textureInfo)
6771
+ {
6772
+ if (headlessMode) return;
6773
+
6774
+ // delete texture info from tracking list even if gl is not enabled
6775
+ glTextureInfos.delete(textureInfo);
6776
+
6777
+ // unset and destroy the texture
6778
+ const glTexture = textureInfo.glTexture;
6779
+ textureInfo.glTexture = undefined;
6780
+ glDeleteTexture(glTexture);
6781
+ }
6782
+
6690
6783
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
6691
6784
  * @memberof WebGL */
6692
6785
  function glFlush()
@@ -7277,80 +7370,97 @@ class PostProcessPlugin
7277
7370
  /** Create global post processing shader
7278
7371
  * @param {string} shaderCode
7279
7372
  * @param {boolean} [includeOverlay]
7373
+ * @param {boolean} [includeMainCanvas]
7280
7374
  * @example
7281
7375
  * // create the post process plugin object
7282
7376
  * new PostProcessPlugin(shaderCode);
7283
7377
  */
7284
- constructor(shaderCode, includeOverlay=false)
7378
+ constructor(shaderCode, includeOverlay=false, includeMainCanvas=true)
7285
7379
  {
7286
7380
  ASSERT(!postProcess, 'Post process already initialized');
7287
7381
  postProcess = this;
7288
7382
 
7289
- if (headlessMode) return;
7290
-
7291
- if (!glEnable)
7292
- {
7293
- console.warn('PostProcessPlugin: WebGL not enabled!');
7294
- return;
7295
- }
7296
-
7297
7383
  if (!shaderCode) // default shader pass through
7298
7384
  shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
7299
7385
 
7300
7386
  /** @property {WebGLProgram} - Shader for post processing */
7301
- this.shader = glCreateProgram(
7302
- '#version 300 es\n' + // specify GLSL ES version
7303
- 'precision highp float;'+ // use highp for better accuracy
7304
- 'in vec2 p;'+ // position
7305
- 'void main(){'+ // shader entry point
7306
- 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7307
- '}' // end of shader
7308
- ,
7309
- '#version 300 es\n' + // specify GLSL ES version
7310
- 'precision highp float;'+ // use highp for better accuracy
7311
- 'uniform sampler2D iChannel0;'+ // input texture
7312
- 'uniform vec3 iResolution;'+ // size of output texture
7313
- 'uniform float iTime;'+ // time
7314
- 'out vec4 c;'+ // out color
7315
- '\n' + shaderCode + '\n'+ // insert custom shader code
7316
- 'void main(){'+ // shader entry point
7317
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
7318
- 'c.a=1.;'+ // always use full alpha
7319
- '}' // end of shader
7320
- );
7387
+ this.shader = undefined;
7321
7388
 
7322
7389
  /** @property {WebGLTexture} - Texture for post processing */
7323
- this.texture = glCreateTexture();
7390
+ this.texture = undefined;
7324
7391
 
7325
- /** @property {boolean} - Should overlay canvas be included in post processing */
7326
- this.includeOverlay = includeOverlay;
7392
+ // setup the post processing plugin
7393
+ initPostProcess();
7394
+ engineAddPlugin(undefined, postProcessRender, postProcessContextLost, postProcessContextRestored);
7327
7395
 
7328
- // Render the post processing shader, called automatically by the engine
7329
- engineAddPlugin(undefined, postProcessRender);
7330
- function postProcessRender()
7396
+ function initPostProcess()
7331
7397
  {
7332
7398
  if (headlessMode) return;
7333
-
7334
- // prepare to render post process shader
7335
- if (glEnable)
7336
- {
7337
- glFlush(); // clear out the buffer
7338
- mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
7339
- }
7340
- else
7399
+
7400
+ if (!glEnable)
7341
7401
  {
7342
- // set the viewport
7343
- glContext.viewport(0, 0, glCanvas.width = drawCanvas.width, glCanvas.height = drawCanvas.height);
7402
+ console.warn('PostProcessPlugin: WebGL not enabled!');
7403
+ return;
7344
7404
  }
7345
7405
 
7346
- if (postProcess.includeOverlay)
7406
+ // create resources
7407
+ postProcess.texture = glCreateTexture();
7408
+ postProcess.shader = glCreateProgram(
7409
+ '#version 300 es\n' + // specify GLSL ES version
7410
+ 'precision highp float;'+ // use highp for better accuracy
7411
+ 'in vec2 p;'+ // position
7412
+ 'void main(){'+ // shader entry point
7413
+ 'gl_Position=vec4(p+p-1.,1,1);'+ // set position
7414
+ '}' // end of shader
7415
+ ,
7416
+ '#version 300 es\n' + // specify GLSL ES version
7417
+ 'precision highp float;'+ // use highp for better accuracy
7418
+ 'uniform sampler2D iChannel0;'+ // input texture
7419
+ 'uniform vec3 iResolution;'+ // size of output texture
7420
+ 'uniform float iTime;'+ // time
7421
+ 'out vec4 c;'+ // out color
7422
+ '\n' + shaderCode + '\n'+ // insert custom shader code
7423
+ 'void main(){'+ // shader entry point
7424
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
7425
+ 'c.a=1.;'+ // always use full alpha
7426
+ '}' // end of shader
7427
+ );
7428
+ }
7429
+ function postProcessContextLost()
7430
+ {
7431
+ postProcess.shader = undefined;
7432
+ postProcess.texture = undefined;
7433
+ LOG('PostProcessPlugin: WebGL context lost');
7434
+ }
7435
+ function postProcessContextRestored()
7436
+ {
7437
+ initPostProcess();
7438
+ LOG('PostProcessPlugin: WebGL context restored');
7439
+ }
7440
+ function postProcessRender()
7441
+ {
7442
+ if (headlessMode) return;
7443
+
7444
+ if (!glEnable)
7445
+ return;
7446
+
7447
+ // clear out the buffer
7448
+ glFlush();
7449
+
7450
+ if (includeMainCanvas || includeOverlay)
7347
7451
  {
7348
- // copy overlay canvas so it will be included in post processing
7349
- mainContext.drawImage(overlayCanvas, 0, 0);
7350
- overlayCanvas.width |= 0;
7452
+ // copy WebGL to the main canvas
7453
+ mainContext.drawImage(glCanvas, 0, 0);
7454
+
7455
+ if (includeOverlay)
7456
+ {
7457
+ // copy overlay canvas so it will be included in post processing
7458
+ mainContext.drawImage(overlayCanvas, 0, 0);
7459
+ overlayCanvas.width |= 0; // clear overlay canvas
7460
+ }
7351
7461
  }
7352
7462
 
7353
- // setup shader program to draw one triangle
7463
+ // setup shader program to draw a quad
7354
7464
  glContext.useProgram(postProcess.shader);
7355
7465
  glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
7356
7466
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1);
@@ -7359,7 +7469,10 @@ class PostProcessPlugin
7359
7469
  // set textures, pass in the 2d canvas and gl canvas in separate texture channels
7360
7470
  glContext.activeTexture(glContext.TEXTURE0);
7361
7471
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.texture);
7362
- glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
7472
+ if (includeMainCanvas || includeOverlay)
7473
+ {
7474
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas);
7475
+ }
7363
7476
 
7364
7477
  // set vertex position attribute
7365
7478
  const vertexByteStride = 8;
@@ -7589,6 +7702,8 @@ class UISystemPlugin
7589
7702
  this.defaultHoverColor = hsl(0,0,.9);
7590
7703
  /** @property {Color} - Default color for disabled UI elements */
7591
7704
  this.defaultDisabledColor = hsl(0,0,.3);
7705
+ /** @property {Color} - Uses a gradient fill combined with color */
7706
+ this.defaultGradientColor = undefined;
7592
7707
  /** @property {number} - Default line width for UI elements */
7593
7708
  this.defaultLineWidth = 4;
7594
7709
  /** @property {number} - Default rounded rect corner radius for UI elements */
@@ -7596,7 +7711,7 @@ class UISystemPlugin
7596
7711
  /** @property {number} - Default scale to use for fitting text to object */
7597
7712
  this.defaultTextScale = .8;
7598
7713
  /** @property {string} - Default font for UI elements */
7599
- this.defaultFont = 'arial';
7714
+ this.defaultFont = fontDefault;
7600
7715
  /** @property {Sound} - Default sound when interactive UI element is pressed */
7601
7716
  this.defaultSoundPress = undefined;
7602
7717
  /** @property {Sound} - Default sound when interactive UI element is released */
@@ -7613,7 +7728,9 @@ class UISystemPlugin
7613
7728
  this.hoverObject = undefined;
7614
7729
  /** @property {UIObject} - Hover object at start of update */
7615
7730
  this.lastHoverObject = undefined;
7616
-
7731
+ /** @property {number} - If set ui coords will be renormalized to this canvas height */
7732
+ this.nativeHeight = 0;
7733
+
7617
7734
  engineAddPlugin(uiUpdate, uiRender);
7618
7735
 
7619
7736
  // setup recursive update and render
@@ -7653,6 +7770,17 @@ class UISystemPlugin
7653
7770
  }
7654
7771
  function uiRender()
7655
7772
  {
7773
+ const context = uiSystem.uiContext;
7774
+ context.save();
7775
+ if (uiSystem.nativeHeight)
7776
+ {
7777
+ // convert to native height
7778
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
7779
+ context.translate(-s*mainCanvasSize.x/2,0);
7780
+ context.scale(s,s);
7781
+ context.translate(mainCanvasSize.x/2/s,0);
7782
+ }
7783
+
7656
7784
  function renderObject(o)
7657
7785
  {
7658
7786
  if (!o.visible)
@@ -7664,6 +7792,7 @@ class UISystemPlugin
7664
7792
  renderObject(c);
7665
7793
  }
7666
7794
  uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
7795
+ context.restore();
7667
7796
  }
7668
7797
  }
7669
7798
 
@@ -7673,8 +7802,9 @@ class UISystemPlugin
7673
7802
  * @param {Color} [color=uiSystem.defaultColor]
7674
7803
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
7675
7804
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7676
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius] */
7677
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius)
7805
+ * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
7806
+ * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
7807
+ drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
7678
7808
  {
7679
7809
  ASSERT(isVector2(pos), 'pos must be a vec2');
7680
7810
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -7684,7 +7814,18 @@ class UISystemPlugin
7684
7814
  ASSERT(isNumber(cornerRadius), 'cornerRadius must be a number');
7685
7815
 
7686
7816
  const context = uiSystem.uiContext;
7687
- context.fillStyle = color.toString();
7817
+ if (gradientColor)
7818
+ {
7819
+ const g = context.createLinearGradient(
7820
+ pos.x, pos.y-size.y/2, pos.x, pos.y+size.y/2);
7821
+ const c = color.toString();
7822
+ g.addColorStop(0, c);
7823
+ g.addColorStop(.5, gradientColor.toString());
7824
+ g.addColorStop(1, c);
7825
+ context.fillStyle = g;
7826
+ }
7827
+ else
7828
+ context.fillStyle = color.toString();
7688
7829
  context.beginPath();
7689
7830
  if (cornerRadius && context['roundRect'])
7690
7831
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
@@ -7741,10 +7882,11 @@ class UISystemPlugin
7741
7882
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7742
7883
  * @param {string} [align]
7743
7884
  * @param {string} [font=uiSystem.defaultFont]
7885
+ * @param {string} [fontStyle]
7744
7886
  * @param {boolean} [applyMaxWidth=true] */
7745
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, applyMaxWidth=true)
7887
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
7746
7888
  {
7747
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
7889
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
7748
7890
  }
7749
7891
 
7750
7892
  /**
@@ -7805,17 +7947,21 @@ class UIObject
7805
7947
  /** @property {boolean} - Is this object disabled? */
7806
7948
  this.disabled = false;
7807
7949
  /** @property {Color} - Color for text */
7808
- this.textColor = uiSystem.defaultTextColor.copy()
7950
+ this.textColor = uiSystem.defaultTextColor.copy();
7809
7951
  /** @property {Color} - Color used when hovering over the object */
7810
- this.hoverColor = uiSystem.defaultHoverColor.copy()
7952
+ this.hoverColor = uiSystem.defaultHoverColor.copy();
7811
7953
  /** @property {Color} - Color for line drawing */
7812
- this.lineColor = uiSystem.defaultLineColor.copy()
7954
+ this.lineColor = uiSystem.defaultLineColor.copy();
7955
+ /** @property {Color} - Uses a gradient fill combined with color */
7956
+ this.gradientColor = uiSystem.defaultGradientColor ? uiSystem.defaultGradientColor.copy() : undefined;
7813
7957
  /** @property {number} - Width for line drawing */
7814
7958
  this.lineWidth = uiSystem.defaultLineWidth;
7815
7959
  /** @property {number} - Corner radius for rounded rects */
7816
7960
  this.cornerRadius = uiSystem.defaultCornerRadius;
7817
7961
  /** @property {string} - Font for this objecct */
7818
7962
  this.font = uiSystem.defaultFont;
7963
+ /** @property {string} - Font style for this object or undefined */
7964
+ this.fontStyle = undefined;
7819
7965
  /** @property {number} - Override for text width */
7820
7966
  this.textWidth = undefined;
7821
7967
  /** @property {number} - Override for text height */
@@ -7840,6 +7986,8 @@ class UIObject
7840
7986
  this.interactive = false;
7841
7987
  /** @property {boolean} - Activate when dragged over with mouse held down */
7842
7988
  this.dragActivate = false;
7989
+ /** @property {boolean} - True if this can be a hover object */
7990
+ this.canBeHover = true;
7843
7991
  uiSystem.uiObjects.push(this);
7844
7992
  }
7845
7993
 
@@ -7863,6 +8011,26 @@ class UIObject
7863
8011
  child.parent = undefined;
7864
8012
  }
7865
8013
 
8014
+ /** Check if the mouse is overlapping a box in screen space
8015
+ * @return {boolean} - True if overlapping
8016
+ */
8017
+ isMouseOverlapping()
8018
+ {
8019
+ const size = !isTouchDevice ? this.size :
8020
+ this.size.add(vec2(this.extraTouchSize || 0));
8021
+ if (!uiSystem.nativeHeight)
8022
+ return isOverlapping(this.pos, size, mousePosScreen);
8023
+
8024
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8025
+ const sInv = 1/s;
8026
+ let pos = mousePosScreen.copy();
8027
+ pos.x += s*mainCanvasSize.x/2;
8028
+ pos.x *= sInv;
8029
+ pos.y *= sInv;
8030
+ pos.x -= sInv*mainCanvasSize.x/2;
8031
+ return isOverlapping(this.pos, size, pos);
8032
+ }
8033
+
7866
8034
  /** Update the object, called automatically by plugin once each frame */
7867
8035
  update()
7868
8036
  {
@@ -7870,13 +8038,10 @@ class UIObject
7870
8038
  const isActive = this.isActiveObject();
7871
8039
  const mouseDown = mouseIsDown(0);
7872
8040
  const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
7873
- if (!uiSystem.hoverObject)
8041
+ if (this.canBeHover)
7874
8042
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
7875
- {
7876
- const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
7877
- if (isOverlapping(this.pos, size, mousePosScreen))
7878
- uiSystem.hoverObject = this;
7879
- }
8043
+ if (!uiSystem.hoverObject && this.isMouseOverlapping())
8044
+ uiSystem.hoverObject = this;
7880
8045
  if (this.isHoverObject())
7881
8046
  {
7882
8047
  if (!this.disabled)
@@ -8001,11 +8166,13 @@ class UIText extends UIObject
8001
8166
 
8002
8167
  // make text not outlined by default
8003
8168
  this.lineWidth = 0;
8169
+ // text can not be a hover object by default
8170
+ this.canBeHover = false;
8004
8171
  }
8005
8172
  render()
8006
8173
  {
8007
8174
  const textSize = this.getTextSize();
8008
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
8175
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
8009
8176
  }
8010
8177
  }
8011
8178
 
@@ -8071,7 +8238,7 @@ class UIButton extends UIObject
8071
8238
 
8072
8239
  // set properties
8073
8240
  this.text = text;
8074
- this.color = color.copy()
8241
+ this.color = color.copy();
8075
8242
  this.interactive = true;
8076
8243
  }
8077
8244
  render()
@@ -8081,7 +8248,7 @@ class UIButton extends UIObject
8081
8248
  // draw the text scaled to fit
8082
8249
  const textSize = this.getTextSize();
8083
8250
  uiSystem.drawText(this.text, this.pos, textSize,
8084
- this.textColor, 0, undefined, this.align, this.font);
8251
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8085
8252
  }
8086
8253
  }
8087
8254
 
@@ -8135,7 +8302,7 @@ class UICheckbox extends UIObject
8135
8302
  const textSize = this.getTextSize();
8136
8303
  const pos = this.pos.add(vec2(this.size.x,0));
8137
8304
  uiSystem.drawText(this.text, pos, textSize,
8138
- this.textColor, 0, undefined, 'left', this.font, false);
8305
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
8139
8306
  }
8140
8307
  }
8141
8308
 
@@ -8213,14 +8380,13 @@ class UIScrollbar extends UIObject
8213
8380
  const handlePos = isHorizontal ?
8214
8381
  vec2(lerp(p1, p2, this.value), this.pos.y) :
8215
8382
  vec2(this.pos.x, lerp(p2, p1, this.value))
8216
- const handleColor = this.disabled ? this.disabledColor :
8217
- this.interactive && this.isActiveObject() ? this.color : this.handleColor;
8383
+ const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8218
8384
  uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8219
8385
 
8220
8386
  // draw the text scaled to fit on the scrollbar
8221
8387
  const textSize = this.getTextSize();
8222
8388
  uiSystem.drawText(this.text, this.pos, textSize,
8223
- this.textColor, 0, undefined, this.align, this.font);
8389
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8224
8390
  }
8225
8391
  }
8226
8392
  /**