littlejsengine 1.14.5 → 1.14.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/littlejs.d.ts +53 -19
  2. package/dist/littlejs.esm.js +183 -106
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +179 -104
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +178 -103
  7. package/examples/box2d/game.js +1 -3
  8. package/examples/box2d/gameObjects.js +2 -2
  9. package/examples/htmlMenu/game.js +1 -1
  10. package/examples/index.html +92 -51
  11. package/examples/puzzle/game.js +1 -2
  12. package/examples/shorts/animation.js +2 -2
  13. package/examples/shorts/box2d.js +1 -0
  14. package/examples/shorts/box2dCar.js +1 -0
  15. package/examples/shorts/flappyGame.js +2 -1
  16. package/examples/shorts/helloWorld.js +2 -2
  17. package/examples/shorts/hillGlideGame.js +6 -3
  18. package/examples/shorts/maze.js +1 -0
  19. package/examples/shorts/medals.js +3 -0
  20. package/examples/shorts/music.js +3 -5
  21. package/examples/shorts/piano.js +24 -25
  22. package/examples/shorts/platformer.js +1 -0
  23. package/examples/shorts/shapes.js +6 -6
  24. package/examples/shorts/sound.js +21 -32
  25. package/examples/shorts/spriteAtlas.js +1 -2
  26. package/examples/shorts/tileLayer.js +1 -0
  27. package/examples/shorts/tiltedView.js +1 -1
  28. package/examples/shorts/timers.js +34 -31
  29. package/examples/shorts/topDown.js +2 -1
  30. package/examples/shorts/uiSystem.js +15 -10
  31. package/examples/stress/index.html +1 -1
  32. package/examples/uiSystem/game.js +1 -1
  33. package/package.json +1 -1
  34. package/plugins/uiSystem.js +83 -58
  35. package/reference.md +1 -0
  36. package/src/engine.js +23 -6
  37. package/src/engineDebug.js +1 -1
  38. package/src/engineDraw.js +19 -12
  39. package/src/engineExport.js +4 -2
  40. package/src/engineInput.js +7 -0
  41. package/src/engineObject.js +4 -4
  42. package/src/engineSettings.js +11 -1
  43. package/src/engineTileLayer.js +2 -2
  44. package/src/engineUtilities.js +9 -2
  45. package/src/engineWebGL.js +20 -18
package/dist/littlejs.js CHANGED
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.5';
36
+ const engineVersion = '1.14.8';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -172,15 +172,15 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
172
172
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
173
173
  if (!debugSpeedUp)
174
174
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
175
- if (debugVideoCaptureIsActive())
176
- frameTimeBufferMS = 0; // no time smoothing when capturing video
177
- updateCanvas();
178
175
 
179
176
  if (paused)
180
177
  {
178
+ updateCanvas();
179
+
181
180
  // update object transforms even when paused
182
181
  for (const o of engineObjects)
183
182
  o.parent || o.updateTransforms();
183
+
184
184
  inputUpdate();
185
185
  pluginUpdateList.forEach(f=>f());
186
186
  debugUpdate();
@@ -205,6 +205,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
205
205
  time = frame++ / frameRate;
206
206
 
207
207
  // update game and objects
208
+ updateCanvas();
208
209
  inputUpdate();
209
210
  gameUpdate();
210
211
  pluginUpdateList.forEach(f=>f());
@@ -214,14 +215,23 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
214
215
  debugUpdate();
215
216
  gameUpdatePost();
216
217
  inputUpdatePost();
218
+
219
+ if (debugVideoCaptureIsActive())
220
+ renderFrame();
217
221
  }
218
222
 
219
223
  // add the time smoothing back in
220
224
  frameTimeBufferMS += deltaSmooth;
221
225
  }
222
226
 
223
- if (!headlessMode)
227
+ if (!debugVideoCaptureIsActive())
228
+ renderFrame();
229
+ requestAnimationFrame(engineUpdate);
230
+
231
+ function renderFrame()
224
232
  {
233
+ if (headlessMode) return;
234
+
225
235
  // render sort then render while removing destroyed objects
226
236
  enginePreRender();
227
237
  gameRender();
@@ -251,7 +261,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
251
261
  }
252
262
  drawCount = 0;
253
263
  }
254
- requestAnimationFrame(engineUpdate);
255
264
  }
256
265
 
257
266
  function updateCanvas()
@@ -277,6 +286,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
277
286
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
278
287
  }
279
288
 
289
+ // apply the clear color to main canvas
290
+ if (canvasClearColor.a > 0)
291
+ {
292
+ mainContext.fillStyle = canvasClearColor.toString();
293
+ mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
294
+ mainContext.fillStyle = BLACK.toString();
295
+ }
296
+
280
297
  // clear overlay canvas and set size
281
298
  overlayCanvas.width = mainCanvas.width;
282
299
  overlayCanvas.height = mainCanvas.height;
@@ -1179,7 +1196,7 @@ function debugVideoCaptureStart()
1179
1196
  const chunks = [];
1180
1197
  debugVideoCaptureTrack = stream.getVideoTracks()[0];
1181
1198
  if (debugVideoCaptureTrack.applyConstraints)
1182
- debugVideoCaptureTrack.applyConstraints({frameRate:frameRate}); // force 60 fps
1199
+ debugVideoCaptureTrack.applyConstraints({frameRate:60}); // force 60 fps
1183
1200
  debugVideoCapture = new MediaRecorder(stream, {mimeType:'video/webm;codecs=vp8'});
1184
1201
  debugVideoCapture.ondataavailable = (e)=> chunks.push(e.data);
1185
1202
  debugVideoCapture.onstop = ()=>
@@ -1401,8 +1418,11 @@ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
1401
1418
  * @memberof Utilities */
1402
1419
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
1403
1420
  {
1404
- return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
1405
- && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
1421
+ const dx = (posA.x - posB.x)*2;
1422
+ const dy = (posA.y - posB.y)*2;
1423
+ const sx = sizeA.x + sizeB.x;
1424
+ const sy = sizeA.y + sizeB.y;
1425
+ return dx >= -sx && dx < sx && dy >= -sy && dy < sy;
1406
1426
  }
1407
1427
 
1408
1428
  /** Returns true if a line segment is intersecting an axis aligned box
@@ -2231,6 +2251,10 @@ class Timer
2231
2251
  * @return {number} */
2232
2252
  getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
2233
2253
 
2254
+ /** Get the time this timer was set to, returns 0 if not set
2255
+ * @return {number} */
2256
+ getSetTime() { return this.isSet() ? this.setTime : 0; }
2257
+
2234
2258
  /** Returns this timer expressed as a string
2235
2259
  * @return {string} */
2236
2260
  toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
@@ -2276,6 +2300,11 @@ let cameraScale = 32;
2276
2300
  * @memberof Settings */
2277
2301
  let canvasColorTiles = true;
2278
2302
 
2303
+ /** Color to clear the canvas to before render
2304
+ * @type {Color}
2305
+ * @memberof Draw */
2306
+ let canvasClearColor = CLEAR_BLACK;
2307
+
2279
2308
  /** The max size of the canvas, centered if window is larger
2280
2309
  * @type {Vector2}
2281
2310
  * @default Vector2(1920,1080)
@@ -2554,6 +2583,11 @@ function setCameraScale(scale) { cameraScale = scale; }
2554
2583
  * @memberof Settings */
2555
2584
  function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2556
2585
 
2586
+ /** Set color to clear the canvas to before render
2587
+ * @param {Color} color
2588
+ * @memberof Settings */
2589
+ function setCanvasClearColor(color) { canvasClearColor = color; }
2590
+
2557
2591
  /** Set max size of the canvas
2558
2592
  * @param {Vector2} size
2559
2593
  * @memberof Settings */
@@ -2619,7 +2653,7 @@ function setGLEnable(enable)
2619
2653
  glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2620
2654
  }
2621
2655
 
2622
- /** Set how many sided polygons to use when drawing circles and elipses with WebGL
2656
+ /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
2623
2657
  * @param {number} sides
2624
2658
  * @memberof Settings */
2625
2659
  function setGLCircleSides(sides) { glCircleSides = sides; }
@@ -2830,7 +2864,7 @@ class EngineObject
2830
2864
  /** @property {Vector2} - World space position of the object */
2831
2865
  this.pos = pos.copy();
2832
2866
  /** @property {Vector2} - World space width and height of the object */
2833
- this.size = size;
2867
+ this.size = size.copy();
2834
2868
  /** @property {Vector2} - Size of object used for drawing, uses size if not set */
2835
2869
  this.drawSize = undefined;
2836
2870
  /** @property {TileInfo} - Tile info to render object (undefined is untextured) */
@@ -2838,7 +2872,7 @@ class EngineObject
2838
2872
  /** @property {number} - Angle to rotate the object */
2839
2873
  this.angle = angle;
2840
2874
  /** @property {Color} - Color to apply when rendered */
2841
- this.color = color;
2875
+ this.color = color.copy();
2842
2876
  /** @property {Color} - Additive color to apply when rendered */
2843
2877
  this.additiveColor = undefined;
2844
2878
  /** @property {boolean} - Should it flip along y axis when rendered */
@@ -2962,7 +2996,7 @@ class EngineObject
2962
2996
  for (const o of engineObjectsCollide)
2963
2997
  {
2964
2998
  // non solid objects don't collide with each other
2965
- if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
2999
+ if ((!this.isSolid && !o.isSolid) || o.destroyed || o.parent || o === this)
2966
3000
  continue;
2967
3001
 
2968
3002
  // check collision
@@ -3001,7 +3035,7 @@ class EngineObject
3001
3035
  {
3002
3036
  // push outside object collision
3003
3037
  this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
3004
- if (o.groundObject && wasMovingDown || !o.mass)
3038
+ if ((o.groundObject && wasMovingDown) || !o.mass)
3005
3039
  {
3006
3040
  // set ground object if landed on something
3007
3041
  if (wasMovingDown)
@@ -3382,8 +3416,9 @@ class TileInfo
3382
3416
  * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
3383
3417
  * @param {number} [textureIndex] - Texture index to use
3384
3418
  * @param {number} [padding] - How many pixels padding around tiles
3419
+ * @param {number} [bleedScale] - How many pixels smaller to draw tiles
3385
3420
  */
3386
- constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
3421
+ constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0, bleedScale=tileFixBleedScale)
3387
3422
  {
3388
3423
  /** @property {Vector2} - Top left corner of tile in pixels */
3389
3424
  this.pos = pos.copy();
@@ -3395,6 +3430,8 @@ class TileInfo
3395
3430
  this.padding = padding;
3396
3431
  /** @property {TextureInfo} - The texture info for this tile */
3397
3432
  this.textureInfo = textureInfos[this.textureIndex];
3433
+ /** @property {float} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3434
+ this.bleedScale = bleedScale;
3398
3435
  }
3399
3436
 
3400
3437
  /** Returns a copy of this tile offset by a vector
@@ -3402,7 +3439,7 @@ class TileInfo
3402
3439
  * @return {TileInfo}
3403
3440
  */
3404
3441
  offset(offset)
3405
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex); }
3442
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleedScale); }
3406
3443
 
3407
3444
  /** Returns a copy of this tile offset by a number of animation frames
3408
3445
  * @param {number} frame - Offset to apply in animation frames
@@ -3425,6 +3462,8 @@ class TileInfo
3425
3462
  this.pos = new Vector2;
3426
3463
  this.size = new Vector2(image.width, image.height);
3427
3464
  this.textureInfo = new TextureInfo(image, glTexture);
3465
+ // do not use padding or bleed
3466
+ this.bleedScale = this.padding = 0;
3428
3467
  return this;
3429
3468
  }
3430
3469
  }
@@ -3482,6 +3521,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3482
3521
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3483
3522
 
3484
3523
  const textureInfo = tileInfo && tileInfo.textureInfo;
3524
+ const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3485
3525
  if (useWebGL)
3486
3526
  {
3487
3527
  if (screenSpace)
@@ -3499,10 +3539,10 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3499
3539
  const w = tileInfo.size.x * sizeInverse.x;
3500
3540
  const h = tileInfo.size.y * sizeInverse.y;
3501
3541
  glSetTexture(textureInfo.glTexture);
3502
- if (tileFixBleedScale)
3542
+ if (bleedScale)
3503
3543
  {
3504
- const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
3505
- const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
3544
+ const tileImageFixBleedX = sizeInverse.x*bleedScale;
3545
+ const tileImageFixBleedY = sizeInverse.y*bleedScale;
3506
3546
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
3507
3547
  x + tileImageFixBleedX, y + tileImageFixBleedY,
3508
3548
  x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
@@ -3533,7 +3573,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3533
3573
  // calculate uvs and render
3534
3574
  const x = tileInfo.pos.x, y = tileInfo.pos.y;
3535
3575
  const w = tileInfo.size.x, h = tileInfo.size.y;
3536
- drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor);
3576
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleedScale);
3537
3577
  }
3538
3578
  else
3539
3579
  {
@@ -4021,15 +4061,16 @@ function combineCanvases()
4021
4061
  * @param {number} dHeight
4022
4062
  * @param {Color} color
4023
4063
  * @param {Color} [additiveColor]
4064
+ * @param {number} [bleedScale] - How much to shrink the source, used to fix bleeding
4024
4065
  * @memberof Draw */
4025
- function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor)
4066
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleedScale=0)
4026
4067
  {
4027
4068
  function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
4028
4069
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4029
- const sx2 = tileFixBleedScale;
4030
- const sy2 = tileFixBleedScale;
4031
- const sWidth2 = sWidth - 2*tileFixBleedScale;
4032
- const sHeight2 = sHeight - 2*tileFixBleedScale;
4070
+ const sx2 = bleedScale;
4071
+ const sy2 = bleedScale;
4072
+ const sWidth2 = sWidth - 2*bleedScale;
4073
+ const sHeight2 = sHeight - 2*bleedScale;
4033
4074
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
4034
4075
  {
4035
4076
  // white texture with no additive alpha, no need to tint
@@ -4145,7 +4186,7 @@ class FontImage
4145
4186
  * @param {Vector2} pos
4146
4187
  * @param {number} [scale=.25]
4147
4188
  * @param {boolean} [center]
4148
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
4189
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4149
4190
  */
4150
4191
  drawText(text, pos, scale=1, center, context=drawContext)
4151
4192
  {
@@ -4412,6 +4453,7 @@ function inputInit()
4412
4453
  document.addEventListener('wheel', onMouseWheel);
4413
4454
  document.addEventListener('contextmenu', onContextMenu);
4414
4455
  document.addEventListener('blur', onBlur);
4456
+ document.addEventListener('mouseleave', onMouseLeave);
4415
4457
 
4416
4458
  // init touch input
4417
4459
  if (isTouchDevice && touchInputEnable)
@@ -4470,6 +4512,12 @@ function inputInit()
4470
4512
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4471
4513
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4472
4514
  function onBlur() { inputClear(); } // reset input when focus is lost
4515
+ function onMouseLeave()
4516
+ {
4517
+ // set mouse position and delta when leaving canvas
4518
+ mousePosScreen = vec2(Infinity);
4519
+ mouseDeltaScreen = vec2(0);
4520
+ }
4473
4521
  }
4474
4522
 
4475
4523
  // convert a mouse or touch event position to screen space
@@ -5591,7 +5639,7 @@ class TileLayerData
5591
5639
  /** @property {boolean} - If the tile should be mirrored along the x axis */
5592
5640
  this.mirror = mirror;
5593
5641
  /** @property {Color} - Color of the tile */
5594
- this.color = color;
5642
+ this.color = color.copy();
5595
5643
  }
5596
5644
 
5597
5645
  /** Set this tile to clear, it will not be rendered */
@@ -5690,7 +5738,7 @@ class CanvasLayer extends EngineObject
5690
5738
  * @param {TileInfo} [tileInfo]
5691
5739
  * @param {Color} [color=(1,1,1,1)]
5692
5740
  * @param {number} [angle=0]
5693
- * @param {boolean} [mirror=0] */
5741
+ * @param {boolean} [mirror=false] */
5694
5742
  drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle, mirror)
5695
5743
  {
5696
5744
  this.drawCanvas2D(pos, size, angle, mirror, (context)=>
@@ -6869,7 +6917,8 @@ function glSetTexture(texture, wrap=false)
6869
6917
  return;
6870
6918
 
6871
6919
  glFlush();
6872
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = texture);
6920
+ glActiveTexture = texture;
6921
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
6873
6922
 
6874
6923
  // set wrap mode
6875
6924
  const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
@@ -6919,6 +6968,7 @@ function glCreateProgram(vsSource, fsSource)
6919
6968
  }
6920
6969
 
6921
6970
  /** Create WebGL texture from an image and init the texture settings
6971
+ * Restores the active texture when done
6922
6972
  * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} [image]
6923
6973
  * @return {WebGLTexture}
6924
6974
  * @memberof WebGL */
@@ -6928,30 +6978,29 @@ function glCreateTexture(image)
6928
6978
 
6929
6979
  // build the texture
6930
6980
  const texture = glContext.createTexture();
6931
- glContext.bindTexture(glContext.TEXTURE_2D, texture);
6981
+ let mipMap = false;
6932
6982
  if (image && image.width)
6933
6983
  {
6934
6984
  glSetTextureData(texture, image);
6935
- if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
6936
- {
6937
- // use mipmap filtering
6938
- glContext.generateMipmap(glContext.TEXTURE_2D);
6939
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR_MIPMAP_LINEAR);
6940
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, glContext.LINEAR);
6941
- return texture;
6942
- }
6985
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
6986
+ mipMap = !tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height);
6943
6987
  }
6944
6988
  else
6945
6989
  {
6946
6990
  // create a white texture
6947
6991
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
6992
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
6948
6993
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
6949
6994
  }
6950
6995
 
6951
6996
  // set texture filtering
6952
- const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
6953
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
6954
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, filter);
6997
+ const magFilter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
6998
+ const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
6999
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
7000
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
7001
+ if (mipMap)
7002
+ glContext.generateMipmap(glContext.TEXTURE_2D);
7003
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
6955
7004
  return texture;
6956
7005
  }
6957
7006
 
@@ -6964,7 +7013,7 @@ function glDeleteTexture(texture)
6964
7013
  glContext.deleteTexture(texture);
6965
7014
  }
6966
7015
 
6967
- /** Set WebGL texture data from an image
7016
+ /** Set WebGL texture data from an image, restores the active texture when done
6968
7017
  * @param {WebGLTexture} texture
6969
7018
  * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} image
6970
7019
  * @memberof WebGL */
@@ -6976,6 +7025,7 @@ function glSetTextureData(texture, image)
6976
7025
  ASSERT(!!image && image.width > 0, 'Invalid image data.');
6977
7026
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
6978
7027
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
7028
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
6979
7029
  }
6980
7030
 
6981
7031
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
@@ -7121,7 +7171,7 @@ function glDrawPoints(points, rgba)
7121
7171
 
7122
7172
  // setup triangle strip with degenerate verts at start and end
7123
7173
  let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
7124
- for(let i = vertCount; i--;)
7174
+ for (let i = vertCount; i--;)
7125
7175
  {
7126
7176
  const j = clamp(i-1, 0, vertCount-3);
7127
7177
  const point = points[j];
@@ -7149,7 +7199,7 @@ function glDrawColoredPoints(points, pointColors)
7149
7199
 
7150
7200
  // setup triangle strip with degenerate verts at start and end
7151
7201
  let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
7152
- for(let i = vertCount; i--;)
7202
+ for (let i = vertCount; i--;)
7153
7203
  {
7154
7204
  const j = clamp(i-1, 0, vertCount-3);
7155
7205
  const point = points[j];
@@ -7286,7 +7336,7 @@ function glPolyStrip(points)
7286
7336
  while (indices.length > 3 && attempts++ < maxAttempts)
7287
7337
  {
7288
7338
  let foundEar = false;
7289
- for (let i = indices.length; --i;)
7339
+ for (let i = 0; i < indices.length; i++)
7290
7340
  {
7291
7341
  const i0 = indices[(i + indices.length - 1) % indices.length];
7292
7342
  const i1 = indices[i];
@@ -7323,7 +7373,7 @@ function glPolyStrip(points)
7323
7373
  if (!foundEar)
7324
7374
  {
7325
7375
  let worstIndex = -1, worstValue = Infinity;
7326
- for (let i = indices.length; --i;)
7376
+ for (let i = 0; i < indices.length; i++)
7327
7377
  {
7328
7378
  const i0 = indices[(i + indices.length - 1) % indices.length];
7329
7379
  const i1 = indices[i];
@@ -7872,6 +7922,8 @@ class UISystemPlugin
7872
7922
  this.defaultLineWidth = 4;
7873
7923
  /** @property {number} - Default rounded rect corner radius for UI elements */
7874
7924
  this.defaultCornerRadius = 0;
7925
+ /** @property {number} - Default scale to use for fitting text to object */
7926
+ this.defaultTextScale = .8;
7875
7927
  /** @property {string} - Default font for UI elements */
7876
7928
  this.defaultFont = 'arial';
7877
7929
  /** @property {Sound} - Default sound when interactive UI element is pressed */
@@ -7884,10 +7936,15 @@ class UISystemPlugin
7884
7936
  this.uiObjects = [];
7885
7937
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
7886
7938
  this.uiContext = context;
7939
+ /** @property {UIObject} - Top most object user is over */
7940
+ this.hoverObject = undefined;
7941
+ /** @property {UIObject} - Object user is currently interacting with */
7942
+ this.activeObject = undefined;
7887
7943
 
7888
7944
  engineAddPlugin(uiUpdate, uiRender);
7889
7945
 
7890
7946
  // setup recursive update and render
7947
+ // update in reverse order to detect mouse enter/leave
7891
7948
  function uiUpdate()
7892
7949
  {
7893
7950
  function updateInvisibleObject(o)
@@ -7912,7 +7969,13 @@ class UISystemPlugin
7912
7969
  else
7913
7970
  updateInvisibleObject(o);
7914
7971
  }
7915
- uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
7972
+ // reset hover object at start of update
7973
+ uiSystem.hoverObject = undefined;
7974
+ for (let i = uiSystem.uiObjects.length; i--;)
7975
+ {
7976
+ const o = uiSystem.uiObjects[i];
7977
+ o.parent || updateObject(o)
7978
+ }
7916
7979
  }
7917
7980
  function uiRender()
7918
7981
  {
@@ -7991,10 +8054,11 @@ class UISystemPlugin
7991
8054
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
7992
8055
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7993
8056
  * @param {string} [align]
7994
- * @param {string} [font=uiSystem.defaultFont] */
7995
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont)
8057
+ * @param {string} [font=uiSystem.defaultFont]
8058
+ * @param {boolean} [applyMaxWidth=true] */
8059
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, applyMaxWidth=true)
7996
8060
  {
7997
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, size.x, uiSystem.uiContext);
8061
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
7998
8062
  }
7999
8063
  }
8000
8064
 
@@ -8018,6 +8082,8 @@ class UIObject
8018
8082
  this.size = size.copy();
8019
8083
  /** @property {Color} - Color of the object */
8020
8084
  this.color = uiSystem.defaultColor;
8085
+ /** @property {Color} - Color of the object when active, uses color if undefined */
8086
+ this.activeColor = undefined;
8021
8087
  /** @property {string} - Text for this ui object */
8022
8088
  this.text = undefined;
8023
8089
  /** @property {Color} - Color when disabled */
@@ -8036,15 +8102,19 @@ class UIObject
8036
8102
  this.cornerRadius = uiSystem.defaultCornerRadius;
8037
8103
  /** @property {string} - Font for this objecct */
8038
8104
  this.font = uiSystem.defaultFont;
8105
+ /** @property {number} - Override for text width */
8106
+ this.textWidth = undefined;
8039
8107
  /** @property {number} - Override for text height */
8040
8108
  this.textHeight = undefined;
8109
+ /** @property {number} - Scale text to fit in the object */
8110
+ this.textScale = uiSystem.defaultTextScale;
8041
8111
  /** @property {boolean} - Should this object be drawn */
8042
8112
  this.visible = true;
8043
8113
  /** @property {Array<UIObject>} - A list of this object's children */
8044
8114
  this.children = [];
8045
8115
  /** @property {UIObject} - This object's parent, position is in parent space */
8046
8116
  this.parent = undefined;
8047
- /** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
8117
+ /** @property {number} - Added size to make small buttons easier to touch on mobile devices */
8048
8118
  this.extraTouchSize = 0;
8049
8119
  /** @property {Sound} - Sound when interactive element is pressed */
8050
8120
  this.soundPress = uiSystem.defaultSoundPress;
@@ -8052,12 +8122,10 @@ class UIObject
8052
8122
  this.soundRelease = uiSystem.defaultSoundRelease;
8053
8123
  /** @property {Sound} - Sound when interactive element is clicked */
8054
8124
  this.soundClick = uiSystem.defaultSoundClick;
8055
- /** @property {boolean} - Is the mouse over this element */
8056
- this.mouseIsOver = false;
8057
8125
  /** @property {boolean} - Is this element interactive */
8058
8126
  this.interactive = false;
8059
- /** @property {boolean} - Is the mouse held on this element (was pressed while over and hasn't been released) */
8060
- this.mouseIsHeld = false;
8127
+ /** @property {boolean} - Activate when dragged over with mouse held down */
8128
+ this.dragActivate = false;
8061
8129
  uiSystem.uiObjects.push(this);
8062
8130
  }
8063
8131
 
@@ -8084,15 +8152,18 @@ class UIObject
8084
8152
  /** Update the object, called automatically by plugin once each frame */
8085
8153
  update()
8086
8154
  {
8087
- const mouseWasOver = this.mouseIsOver;
8088
- const mousePress = mouseWasPressed(0);
8155
+ const wasHover = this.isHoverObject();
8156
+ const isActive = this.isActiveObject();
8089
8157
  const mouseDown = mouseIsDown(0);
8090
- if (mousePress || !mouseDown || this.mouseIsHeld)
8158
+ const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
8159
+ if (!uiSystem.hoverObject)
8160
+ if (mousePress || !mouseDown || isActive)
8091
8161
  {
8092
8162
  const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
8093
- this.mouseIsOver = isOverlapping(this.pos, size, mousePosScreen);
8163
+ if (isOverlapping(this.pos, size, mousePosScreen))
8164
+ uiSystem.hoverObject = this;
8094
8165
  }
8095
- if (this.mouseIsOver)
8166
+ if (this.isHoverObject())
8096
8167
  {
8097
8168
  if (mousePress)
8098
8169
  inputClearKey(0,0,0,1,0); // clear mouse was pressed state
@@ -8105,10 +8176,12 @@ class UIObject
8105
8176
  this.onPress();
8106
8177
  if (this.soundPress)
8107
8178
  this.soundPress.play();
8179
+ if (uiSystem.activeObject && !isActive)
8180
+ uiSystem.activeObject.onRelease();
8181
+ uiSystem.activeObject = this;
8108
8182
  }
8109
- this.mouseIsHeld = true;
8110
8183
  }
8111
- if (!mouseDown && this.mouseIsHeld && this.interactive)
8184
+ if (!mouseDown && uiSystem.activeObject === this && this.interactive)
8112
8185
  {
8113
8186
  this.onClick();
8114
8187
  if (this.soundClick)
@@ -8116,37 +8189,52 @@ class UIObject
8116
8189
  }
8117
8190
  }
8118
8191
  }
8119
- if (!mouseDown)
8192
+ if (isActive && (!mouseDown || !this.isHoverObject()))
8120
8193
  {
8121
- if (this.mouseIsHeld && this.interactive)
8122
- {
8123
- this.onRelease();
8124
- if (this.soundRelease)
8125
- this.soundRelease.play();
8126
- }
8127
- if (isTouchDevice)
8128
- this.mouseIsOver = false;
8129
- this.mouseIsHeld = false;
8194
+ this.onRelease();
8195
+ if (this.soundRelease)
8196
+ this.soundRelease.play();
8197
+ uiSystem.activeObject = undefined;
8130
8198
  }
8131
8199
 
8132
- if (this.mouseIsOver !== mouseWasOver)
8133
- this.mouseIsOver ? this.onEnter() : this.onLeave();
8200
+ if (this.isHoverObject() !== wasHover)
8201
+ this.isHoverObject() ? this.onEnter() : this.onLeave();
8134
8202
  }
8135
8203
 
8136
8204
  /** Render the object, called automatically by plugin once each frame */
8137
8205
  render()
8138
8206
  {
8139
- if (this.size.x && this.size.y)
8140
- uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
8207
+ if (!this.size.x || !this.size.y) return;
8208
+
8209
+ const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8210
+ const color = this.interactive ? this.disabled ? this.disabledColor : this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8211
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
8141
8212
  }
8142
8213
 
8143
8214
  /** Special update when object is not visible */
8144
8215
  updateInvisible()
8145
8216
  {
8146
8217
  // reset input state when not visible
8147
- this.mouseIsOver = this.mouseIsHeld = false;
8218
+ if (this.isActiveObject())
8219
+ uiSystem.activeObject = undefined;
8220
+ }
8221
+
8222
+ /** Get the size for text with overrides and scale
8223
+ * @return {Vector2}
8224
+ */
8225
+ getTextSize()
8226
+ {
8227
+ return vec2(
8228
+ this.textWidth || this.textScale * this.size.x,
8229
+ this.textHeight || this.textScale * this.size.y);
8148
8230
  }
8149
8231
 
8232
+ /** @return {boolean} - Is the mouse hovering over this element */
8233
+ isHoverObject() { return uiSystem.hoverObject === this; }
8234
+
8235
+ /** @return {boolean} - Is the mouse held onto this element */
8236
+ isActiveObject() { return uiSystem.activeObject === this; }
8237
+
8150
8238
  /** Called when the mouse enters the object */
8151
8239
  onEnter() {}
8152
8240
 
@@ -8194,7 +8282,7 @@ class UIText extends UIObject
8194
8282
  }
8195
8283
  render()
8196
8284
  {
8197
- const textSize = vec2(this.size.x, this.textHeight || this.size.y);
8285
+ const textSize = this.getTextSize();
8198
8286
  uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
8199
8287
  }
8200
8288
  }
@@ -8257,14 +8345,10 @@ class UIButton extends UIObject
8257
8345
  }
8258
8346
  render()
8259
8347
  {
8260
- // draw the button
8261
- const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
8262
- const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
8263
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
8348
+ super.render();
8264
8349
 
8265
- // draw the text
8266
- const textScale = .8; // scale text to fit
8267
- const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
8350
+ // draw the text scaled to fit
8351
+ const textSize = this.getTextSize();
8268
8352
  uiSystem.drawText(this.text, this.pos, textSize,
8269
8353
  this.textColor, 0, undefined, this.align, this.font);
8270
8354
  }
@@ -8302,8 +8386,7 @@ class UICheckbox extends UIObject
8302
8386
  }
8303
8387
  render()
8304
8388
  {
8305
- const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
8306
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
8389
+ super.render();
8307
8390
  if (this.checked)
8308
8391
  {
8309
8392
  const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
@@ -8313,13 +8396,11 @@ class UICheckbox extends UIObject
8313
8396
  uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
8314
8397
  }
8315
8398
 
8316
- // draw the text to the right side of the checkbox
8317
- const textScale = .8; // scale text to fit
8318
- const gapScale = .55;
8319
- const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
8320
- const pos = this.pos.add(vec2(this.size.x*gapScale,0));
8399
+ // draw the text next to the checkbox
8400
+ const textSize = this.getTextSize();
8401
+ const pos = this.pos.add(vec2(this.size.x,0));
8321
8402
  uiSystem.drawText(this.text, pos, textSize,
8322
- this.textColor, 0, undefined, 'left', this.font);
8403
+ this.textColor, 0, undefined, 'left', this.font, false);
8323
8404
  }
8324
8405
  }
8325
8406
 
@@ -8355,7 +8436,7 @@ class UIScrollbar extends UIObject
8355
8436
  update()
8356
8437
  {
8357
8438
  super.update();
8358
- if (this.mouseIsHeld && this.interactive)
8439
+ if (this.isActiveObject() && this.interactive)
8359
8440
  {
8360
8441
  // check if value changed
8361
8442
  const handleSize = vec2(this.size.y);
@@ -8369,12 +8450,7 @@ class UIScrollbar extends UIObject
8369
8450
  }
8370
8451
  render()
8371
8452
  {
8372
- // draw the scrollbar background
8373
- const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
8374
- this.color : this.lineColor;
8375
- const color = this.disabled ? this.disabledColor :
8376
- this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
8377
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
8453
+ super.render();
8378
8454
 
8379
8455
  // draw the scrollbar handle
8380
8456
  const handleSize = vec2(this.size.y);
@@ -8383,12 +8459,11 @@ class UIScrollbar extends UIObject
8383
8459
  const p2 = this.pos.x + handleWidth/2;
8384
8460
  const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
8385
8461
  const handleColor = this.disabled ? this.disabledColor :
8386
- this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
8462
+ this.interactive && this.isActiveObject() ? this.color : this.handleColor;
8387
8463
  uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8388
8464
 
8389
- // draw the text on the scrollbar
8390
- const textScale = .8; // scale text to fit in scrollbar
8391
- const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
8465
+ // draw the text scaled to fit on the scrollbar
8466
+ const textSize = this.getTextSize();
8392
8467
  uiSystem.drawText(this.text, this.pos, textSize,
8393
8468
  this.textColor, 0, undefined, this.align, this.font);
8394
8469
  }