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
@@ -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;
@@ -848,8 +865,11 @@ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
848
865
  * @memberof Utilities */
849
866
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
850
867
  {
851
- return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
852
- && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
868
+ const dx = (posA.x - posB.x)*2;
869
+ const dy = (posA.y - posB.y)*2;
870
+ const sx = sizeA.x + sizeB.x;
871
+ const sy = sizeA.y + sizeB.y;
872
+ return dx >= -sx && dx < sx && dy >= -sy && dy < sy;
853
873
  }
854
874
 
855
875
  /** Returns true if a line segment is intersecting an axis aligned box
@@ -1678,6 +1698,10 @@ class Timer
1678
1698
  * @return {number} */
1679
1699
  getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
1680
1700
 
1701
+ /** Get the time this timer was set to, returns 0 if not set
1702
+ * @return {number} */
1703
+ getSetTime() { return this.isSet() ? this.setTime : 0; }
1704
+
1681
1705
  /** Returns this timer expressed as a string
1682
1706
  * @return {string} */
1683
1707
  toString() { if (debug) { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }}
@@ -1723,6 +1747,11 @@ let cameraScale = 32;
1723
1747
  * @memberof Settings */
1724
1748
  let canvasColorTiles = true;
1725
1749
 
1750
+ /** Color to clear the canvas to before render
1751
+ * @type {Color}
1752
+ * @memberof Draw */
1753
+ let canvasClearColor = CLEAR_BLACK;
1754
+
1726
1755
  /** The max size of the canvas, centered if window is larger
1727
1756
  * @type {Vector2}
1728
1757
  * @default Vector2(1920,1080)
@@ -2001,6 +2030,11 @@ function setCameraScale(scale) { cameraScale = scale; }
2001
2030
  * @memberof Settings */
2002
2031
  function setCanvasColorTiles(colorTiles) { canvasColorTiles = colorTiles; }
2003
2032
 
2033
+ /** Set color to clear the canvas to before render
2034
+ * @param {Color} color
2035
+ * @memberof Settings */
2036
+ function setCanvasClearColor(color) { canvasClearColor = color; }
2037
+
2004
2038
  /** Set max size of the canvas
2005
2039
  * @param {Vector2} size
2006
2040
  * @memberof Settings */
@@ -2066,7 +2100,7 @@ function setGLEnable(enable)
2066
2100
  glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2067
2101
  }
2068
2102
 
2069
- /** Set how many sided polygons to use when drawing circles and elipses with WebGL
2103
+ /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
2070
2104
  * @param {number} sides
2071
2105
  * @memberof Settings */
2072
2106
  function setGLCircleSides(sides) { glCircleSides = sides; }
@@ -2277,7 +2311,7 @@ class EngineObject
2277
2311
  /** @property {Vector2} - World space position of the object */
2278
2312
  this.pos = pos.copy();
2279
2313
  /** @property {Vector2} - World space width and height of the object */
2280
- this.size = size;
2314
+ this.size = size.copy();
2281
2315
  /** @property {Vector2} - Size of object used for drawing, uses size if not set */
2282
2316
  this.drawSize = undefined;
2283
2317
  /** @property {TileInfo} - Tile info to render object (undefined is untextured) */
@@ -2285,7 +2319,7 @@ class EngineObject
2285
2319
  /** @property {number} - Angle to rotate the object */
2286
2320
  this.angle = angle;
2287
2321
  /** @property {Color} - Color to apply when rendered */
2288
- this.color = color;
2322
+ this.color = color.copy();
2289
2323
  /** @property {Color} - Additive color to apply when rendered */
2290
2324
  this.additiveColor = undefined;
2291
2325
  /** @property {boolean} - Should it flip along y axis when rendered */
@@ -2409,7 +2443,7 @@ class EngineObject
2409
2443
  for (const o of engineObjectsCollide)
2410
2444
  {
2411
2445
  // non solid objects don't collide with each other
2412
- if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
2446
+ if ((!this.isSolid && !o.isSolid) || o.destroyed || o.parent || o === this)
2413
2447
  continue;
2414
2448
 
2415
2449
  // check collision
@@ -2448,7 +2482,7 @@ class EngineObject
2448
2482
  {
2449
2483
  // push outside object collision
2450
2484
  this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
2451
- if (o.groundObject && wasMovingDown || !o.mass)
2485
+ if ((o.groundObject && wasMovingDown) || !o.mass)
2452
2486
  {
2453
2487
  // set ground object if landed on something
2454
2488
  if (wasMovingDown)
@@ -2829,8 +2863,9 @@ class TileInfo
2829
2863
  * @param {Vector2} [size=tileSizeDefault] - Size of tile in pixels
2830
2864
  * @param {number} [textureIndex] - Texture index to use
2831
2865
  * @param {number} [padding] - How many pixels padding around tiles
2866
+ * @param {number} [bleedScale] - How many pixels smaller to draw tiles
2832
2867
  */
2833
- constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
2868
+ constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0, bleedScale=tileFixBleedScale)
2834
2869
  {
2835
2870
  /** @property {Vector2} - Top left corner of tile in pixels */
2836
2871
  this.pos = pos.copy();
@@ -2842,6 +2877,8 @@ class TileInfo
2842
2877
  this.padding = padding;
2843
2878
  /** @property {TextureInfo} - The texture info for this tile */
2844
2879
  this.textureInfo = textureInfos[this.textureIndex];
2880
+ /** @property {float} - Shrinks tile by this many pixels to prevent neighbors bleeding */
2881
+ this.bleedScale = bleedScale;
2845
2882
  }
2846
2883
 
2847
2884
  /** Returns a copy of this tile offset by a vector
@@ -2849,7 +2886,7 @@ class TileInfo
2849
2886
  * @return {TileInfo}
2850
2887
  */
2851
2888
  offset(offset)
2852
- { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex); }
2889
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureIndex, this.padding, this.bleedScale); }
2853
2890
 
2854
2891
  /** Returns a copy of this tile offset by a number of animation frames
2855
2892
  * @param {number} frame - Offset to apply in animation frames
@@ -2872,6 +2909,8 @@ class TileInfo
2872
2909
  this.pos = new Vector2;
2873
2910
  this.size = new Vector2(image.width, image.height);
2874
2911
  this.textureInfo = new TextureInfo(image, glTexture);
2912
+ // do not use padding or bleed
2913
+ this.bleedScale = this.padding = 0;
2875
2914
  return this;
2876
2915
  }
2877
2916
  }
@@ -2929,6 +2968,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
2929
2968
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
2930
2969
 
2931
2970
  const textureInfo = tileInfo && tileInfo.textureInfo;
2971
+ const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
2932
2972
  if (useWebGL)
2933
2973
  {
2934
2974
  if (screenSpace)
@@ -2946,10 +2986,10 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
2946
2986
  const w = tileInfo.size.x * sizeInverse.x;
2947
2987
  const h = tileInfo.size.y * sizeInverse.y;
2948
2988
  glSetTexture(textureInfo.glTexture);
2949
- if (tileFixBleedScale)
2989
+ if (bleedScale)
2950
2990
  {
2951
- const tileImageFixBleedX = sizeInverse.x*tileFixBleedScale;
2952
- const tileImageFixBleedY = sizeInverse.y*tileFixBleedScale;
2991
+ const tileImageFixBleedX = sizeInverse.x*bleedScale;
2992
+ const tileImageFixBleedY = sizeInverse.y*bleedScale;
2953
2993
  glDraw(pos.x, pos.y, mirror ? -size.x : size.x, size.y, angle,
2954
2994
  x + tileImageFixBleedX, y + tileImageFixBleedY,
2955
2995
  x - tileImageFixBleedX + w, y - tileImageFixBleedY + h,
@@ -2980,7 +3020,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
2980
3020
  // calculate uvs and render
2981
3021
  const x = tileInfo.pos.x, y = tileInfo.pos.y;
2982
3022
  const w = tileInfo.size.x, h = tileInfo.size.y;
2983
- drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor);
3023
+ drawImageColor(context, textureInfo.image, x, y, w, h, -.5, -.5, 1, 1, color, additiveColor, bleedScale);
2984
3024
  }
2985
3025
  else
2986
3026
  {
@@ -3468,15 +3508,16 @@ function combineCanvases()
3468
3508
  * @param {number} dHeight
3469
3509
  * @param {Color} color
3470
3510
  * @param {Color} [additiveColor]
3511
+ * @param {number} [bleedScale] - How much to shrink the source, used to fix bleeding
3471
3512
  * @memberof Draw */
3472
- function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor)
3513
+ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleedScale=0)
3473
3514
  {
3474
3515
  function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
3475
3516
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
3476
- const sx2 = tileFixBleedScale;
3477
- const sy2 = tileFixBleedScale;
3478
- const sWidth2 = sWidth - 2*tileFixBleedScale;
3479
- const sHeight2 = sHeight - 2*tileFixBleedScale;
3517
+ const sx2 = bleedScale;
3518
+ const sy2 = bleedScale;
3519
+ const sWidth2 = sWidth - 2*bleedScale;
3520
+ const sHeight2 = sHeight - 2*bleedScale;
3480
3521
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
3481
3522
  {
3482
3523
  // white texture with no additive alpha, no need to tint
@@ -3592,7 +3633,7 @@ class FontImage
3592
3633
  * @param {Vector2} pos
3593
3634
  * @param {number} [scale=.25]
3594
3635
  * @param {boolean} [center]
3595
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
3636
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3596
3637
  */
3597
3638
  drawText(text, pos, scale=1, center, context=drawContext)
3598
3639
  {
@@ -3859,6 +3900,7 @@ function inputInit()
3859
3900
  document.addEventListener('wheel', onMouseWheel);
3860
3901
  document.addEventListener('contextmenu', onContextMenu);
3861
3902
  document.addEventListener('blur', onBlur);
3903
+ document.addEventListener('mouseleave', onMouseLeave);
3862
3904
 
3863
3905
  // init touch input
3864
3906
  if (isTouchDevice && touchInputEnable)
@@ -3917,6 +3959,12 @@ function inputInit()
3917
3959
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
3918
3960
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
3919
3961
  function onBlur() { inputClear(); } // reset input when focus is lost
3962
+ function onMouseLeave()
3963
+ {
3964
+ // set mouse position and delta when leaving canvas
3965
+ mousePosScreen = vec2(Infinity);
3966
+ mouseDeltaScreen = vec2(0);
3967
+ }
3920
3968
  }
3921
3969
 
3922
3970
  // convert a mouse or touch event position to screen space
@@ -5038,7 +5086,7 @@ class TileLayerData
5038
5086
  /** @property {boolean} - If the tile should be mirrored along the x axis */
5039
5087
  this.mirror = mirror;
5040
5088
  /** @property {Color} - Color of the tile */
5041
- this.color = color;
5089
+ this.color = color.copy();
5042
5090
  }
5043
5091
 
5044
5092
  /** Set this tile to clear, it will not be rendered */
@@ -5137,7 +5185,7 @@ class CanvasLayer extends EngineObject
5137
5185
  * @param {TileInfo} [tileInfo]
5138
5186
  * @param {Color} [color=(1,1,1,1)]
5139
5187
  * @param {number} [angle=0]
5140
- * @param {boolean} [mirror=0] */
5188
+ * @param {boolean} [mirror=false] */
5141
5189
  drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle, mirror)
5142
5190
  {
5143
5191
  this.drawCanvas2D(pos, size, angle, mirror, (context)=>
@@ -6316,7 +6364,8 @@ function glSetTexture(texture, wrap=false)
6316
6364
  return;
6317
6365
 
6318
6366
  glFlush();
6319
- glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture = texture);
6367
+ glActiveTexture = texture;
6368
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
6320
6369
 
6321
6370
  // set wrap mode
6322
6371
  const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
@@ -6366,6 +6415,7 @@ function glCreateProgram(vsSource, fsSource)
6366
6415
  }
6367
6416
 
6368
6417
  /** Create WebGL texture from an image and init the texture settings
6418
+ * Restores the active texture when done
6369
6419
  * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} [image]
6370
6420
  * @return {WebGLTexture}
6371
6421
  * @memberof WebGL */
@@ -6375,30 +6425,29 @@ function glCreateTexture(image)
6375
6425
 
6376
6426
  // build the texture
6377
6427
  const texture = glContext.createTexture();
6378
- glContext.bindTexture(glContext.TEXTURE_2D, texture);
6428
+ let mipMap = false;
6379
6429
  if (image && image.width)
6380
6430
  {
6381
6431
  glSetTextureData(texture, image);
6382
- if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
6383
- {
6384
- // use mipmap filtering
6385
- glContext.generateMipmap(glContext.TEXTURE_2D);
6386
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR_MIPMAP_LINEAR);
6387
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, glContext.LINEAR);
6388
- return texture;
6389
- }
6432
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
6433
+ mipMap = !tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height);
6390
6434
  }
6391
6435
  else
6392
6436
  {
6393
6437
  // create a white texture
6394
6438
  const whitePixel = new Uint8Array([255, 255, 255, 255]);
6439
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
6395
6440
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
6396
6441
  }
6397
6442
 
6398
6443
  // set texture filtering
6399
- const filter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
6400
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, filter);
6401
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, filter);
6444
+ const magFilter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
6445
+ const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
6446
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
6447
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
6448
+ if (mipMap)
6449
+ glContext.generateMipmap(glContext.TEXTURE_2D);
6450
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
6402
6451
  return texture;
6403
6452
  }
6404
6453
 
@@ -6411,7 +6460,7 @@ function glDeleteTexture(texture)
6411
6460
  glContext.deleteTexture(texture);
6412
6461
  }
6413
6462
 
6414
- /** Set WebGL texture data from an image
6463
+ /** Set WebGL texture data from an image, restores the active texture when done
6415
6464
  * @param {WebGLTexture} texture
6416
6465
  * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} image
6417
6466
  * @memberof WebGL */
@@ -6423,6 +6472,7 @@ function glSetTextureData(texture, image)
6423
6472
  ASSERT(!!image && image.width > 0, 'Invalid image data.');
6424
6473
  glContext.bindTexture(glContext.TEXTURE_2D, texture);
6425
6474
  glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
6475
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture); // rebind active texture
6426
6476
  }
6427
6477
 
6428
6478
  /** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
@@ -6568,7 +6618,7 @@ function glDrawPoints(points, rgba)
6568
6618
 
6569
6619
  // setup triangle strip with degenerate verts at start and end
6570
6620
  let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
6571
- for(let i = vertCount; i--;)
6621
+ for (let i = vertCount; i--;)
6572
6622
  {
6573
6623
  const j = clamp(i-1, 0, vertCount-3);
6574
6624
  const point = points[j];
@@ -6596,7 +6646,7 @@ function glDrawColoredPoints(points, pointColors)
6596
6646
 
6597
6647
  // setup triangle strip with degenerate verts at start and end
6598
6648
  let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
6599
- for(let i = vertCount; i--;)
6649
+ for (let i = vertCount; i--;)
6600
6650
  {
6601
6651
  const j = clamp(i-1, 0, vertCount-3);
6602
6652
  const point = points[j];
@@ -6733,7 +6783,7 @@ function glPolyStrip(points)
6733
6783
  while (indices.length > 3 && attempts++ < maxAttempts)
6734
6784
  {
6735
6785
  let foundEar = false;
6736
- for (let i = indices.length; --i;)
6786
+ for (let i = 0; i < indices.length; i++)
6737
6787
  {
6738
6788
  const i0 = indices[(i + indices.length - 1) % indices.length];
6739
6789
  const i1 = indices[i];
@@ -6770,7 +6820,7 @@ function glPolyStrip(points)
6770
6820
  if (!foundEar)
6771
6821
  {
6772
6822
  let worstIndex = -1, worstValue = Infinity;
6773
- for (let i = indices.length; --i;)
6823
+ for (let i = 0; i < indices.length; i++)
6774
6824
  {
6775
6825
  const i0 = indices[(i + indices.length - 1) % indices.length];
6776
6826
  const i1 = indices[i];
@@ -7319,6 +7369,8 @@ class UISystemPlugin
7319
7369
  this.defaultLineWidth = 4;
7320
7370
  /** @property {number} - Default rounded rect corner radius for UI elements */
7321
7371
  this.defaultCornerRadius = 0;
7372
+ /** @property {number} - Default scale to use for fitting text to object */
7373
+ this.defaultTextScale = .8;
7322
7374
  /** @property {string} - Default font for UI elements */
7323
7375
  this.defaultFont = 'arial';
7324
7376
  /** @property {Sound} - Default sound when interactive UI element is pressed */
@@ -7331,10 +7383,15 @@ class UISystemPlugin
7331
7383
  this.uiObjects = [];
7332
7384
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
7333
7385
  this.uiContext = context;
7386
+ /** @property {UIObject} - Top most object user is over */
7387
+ this.hoverObject = undefined;
7388
+ /** @property {UIObject} - Object user is currently interacting with */
7389
+ this.activeObject = undefined;
7334
7390
 
7335
7391
  engineAddPlugin(uiUpdate, uiRender);
7336
7392
 
7337
7393
  // setup recursive update and render
7394
+ // update in reverse order to detect mouse enter/leave
7338
7395
  function uiUpdate()
7339
7396
  {
7340
7397
  function updateInvisibleObject(o)
@@ -7359,7 +7416,13 @@ class UISystemPlugin
7359
7416
  else
7360
7417
  updateInvisibleObject(o);
7361
7418
  }
7362
- uiSystem.uiObjects.forEach(o=> o.parent || updateObject(o));
7419
+ // reset hover object at start of update
7420
+ uiSystem.hoverObject = undefined;
7421
+ for (let i = uiSystem.uiObjects.length; i--;)
7422
+ {
7423
+ const o = uiSystem.uiObjects[i];
7424
+ o.parent || updateObject(o)
7425
+ }
7363
7426
  }
7364
7427
  function uiRender()
7365
7428
  {
@@ -7438,10 +7501,11 @@ class UISystemPlugin
7438
7501
  * @param {number} [lineWidth=uiSystem.defaultLineWidth]
7439
7502
  * @param {Color} [lineColor=uiSystem.defaultLineColor]
7440
7503
  * @param {string} [align]
7441
- * @param {string} [font=uiSystem.defaultFont] */
7442
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont)
7504
+ * @param {string} [font=uiSystem.defaultFont]
7505
+ * @param {boolean} [applyMaxWidth=true] */
7506
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, applyMaxWidth=true)
7443
7507
  {
7444
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, size.x, uiSystem.uiContext);
7508
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
7445
7509
  }
7446
7510
  }
7447
7511
 
@@ -7465,6 +7529,8 @@ class UIObject
7465
7529
  this.size = size.copy();
7466
7530
  /** @property {Color} - Color of the object */
7467
7531
  this.color = uiSystem.defaultColor;
7532
+ /** @property {Color} - Color of the object when active, uses color if undefined */
7533
+ this.activeColor = undefined;
7468
7534
  /** @property {string} - Text for this ui object */
7469
7535
  this.text = undefined;
7470
7536
  /** @property {Color} - Color when disabled */
@@ -7483,15 +7549,19 @@ class UIObject
7483
7549
  this.cornerRadius = uiSystem.defaultCornerRadius;
7484
7550
  /** @property {string} - Font for this objecct */
7485
7551
  this.font = uiSystem.defaultFont;
7552
+ /** @property {number} - Override for text width */
7553
+ this.textWidth = undefined;
7486
7554
  /** @property {number} - Override for text height */
7487
7555
  this.textHeight = undefined;
7556
+ /** @property {number} - Scale text to fit in the object */
7557
+ this.textScale = uiSystem.defaultTextScale;
7488
7558
  /** @property {boolean} - Should this object be drawn */
7489
7559
  this.visible = true;
7490
7560
  /** @property {Array<UIObject>} - A list of this object's children */
7491
7561
  this.children = [];
7492
7562
  /** @property {UIObject} - This object's parent, position is in parent space */
7493
7563
  this.parent = undefined;
7494
- /** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
7564
+ /** @property {number} - Added size to make small buttons easier to touch on mobile devices */
7495
7565
  this.extraTouchSize = 0;
7496
7566
  /** @property {Sound} - Sound when interactive element is pressed */
7497
7567
  this.soundPress = uiSystem.defaultSoundPress;
@@ -7499,12 +7569,10 @@ class UIObject
7499
7569
  this.soundRelease = uiSystem.defaultSoundRelease;
7500
7570
  /** @property {Sound} - Sound when interactive element is clicked */
7501
7571
  this.soundClick = uiSystem.defaultSoundClick;
7502
- /** @property {boolean} - Is the mouse over this element */
7503
- this.mouseIsOver = false;
7504
7572
  /** @property {boolean} - Is this element interactive */
7505
7573
  this.interactive = false;
7506
- /** @property {boolean} - Is the mouse held on this element (was pressed while over and hasn't been released) */
7507
- this.mouseIsHeld = false;
7574
+ /** @property {boolean} - Activate when dragged over with mouse held down */
7575
+ this.dragActivate = false;
7508
7576
  uiSystem.uiObjects.push(this);
7509
7577
  }
7510
7578
 
@@ -7531,15 +7599,18 @@ class UIObject
7531
7599
  /** Update the object, called automatically by plugin once each frame */
7532
7600
  update()
7533
7601
  {
7534
- const mouseWasOver = this.mouseIsOver;
7535
- const mousePress = mouseWasPressed(0);
7602
+ const wasHover = this.isHoverObject();
7603
+ const isActive = this.isActiveObject();
7536
7604
  const mouseDown = mouseIsDown(0);
7537
- if (mousePress || !mouseDown || this.mouseIsHeld)
7605
+ const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
7606
+ if (!uiSystem.hoverObject)
7607
+ if (mousePress || !mouseDown || isActive)
7538
7608
  {
7539
7609
  const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
7540
- this.mouseIsOver = isOverlapping(this.pos, size, mousePosScreen);
7610
+ if (isOverlapping(this.pos, size, mousePosScreen))
7611
+ uiSystem.hoverObject = this;
7541
7612
  }
7542
- if (this.mouseIsOver)
7613
+ if (this.isHoverObject())
7543
7614
  {
7544
7615
  if (mousePress)
7545
7616
  inputClearKey(0,0,0,1,0); // clear mouse was pressed state
@@ -7552,10 +7623,12 @@ class UIObject
7552
7623
  this.onPress();
7553
7624
  if (this.soundPress)
7554
7625
  this.soundPress.play();
7626
+ if (uiSystem.activeObject && !isActive)
7627
+ uiSystem.activeObject.onRelease();
7628
+ uiSystem.activeObject = this;
7555
7629
  }
7556
- this.mouseIsHeld = true;
7557
7630
  }
7558
- if (!mouseDown && this.mouseIsHeld && this.interactive)
7631
+ if (!mouseDown && uiSystem.activeObject === this && this.interactive)
7559
7632
  {
7560
7633
  this.onClick();
7561
7634
  if (this.soundClick)
@@ -7563,37 +7636,52 @@ class UIObject
7563
7636
  }
7564
7637
  }
7565
7638
  }
7566
- if (!mouseDown)
7639
+ if (isActive && (!mouseDown || !this.isHoverObject()))
7567
7640
  {
7568
- if (this.mouseIsHeld && this.interactive)
7569
- {
7570
- this.onRelease();
7571
- if (this.soundRelease)
7572
- this.soundRelease.play();
7573
- }
7574
- if (isTouchDevice)
7575
- this.mouseIsOver = false;
7576
- this.mouseIsHeld = false;
7641
+ this.onRelease();
7642
+ if (this.soundRelease)
7643
+ this.soundRelease.play();
7644
+ uiSystem.activeObject = undefined;
7577
7645
  }
7578
7646
 
7579
- if (this.mouseIsOver !== mouseWasOver)
7580
- this.mouseIsOver ? this.onEnter() : this.onLeave();
7647
+ if (this.isHoverObject() !== wasHover)
7648
+ this.isHoverObject() ? this.onEnter() : this.onLeave();
7581
7649
  }
7582
7650
 
7583
7651
  /** Render the object, called automatically by plugin once each frame */
7584
7652
  render()
7585
7653
  {
7586
- if (this.size.x && this.size.y)
7587
- uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
7654
+ if (!this.size.x || !this.size.y) return;
7655
+
7656
+ const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
7657
+ const color = this.interactive ? this.disabled ? this.disabledColor : this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
7658
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7588
7659
  }
7589
7660
 
7590
7661
  /** Special update when object is not visible */
7591
7662
  updateInvisible()
7592
7663
  {
7593
7664
  // reset input state when not visible
7594
- this.mouseIsOver = this.mouseIsHeld = false;
7665
+ if (this.isActiveObject())
7666
+ uiSystem.activeObject = undefined;
7667
+ }
7668
+
7669
+ /** Get the size for text with overrides and scale
7670
+ * @return {Vector2}
7671
+ */
7672
+ getTextSize()
7673
+ {
7674
+ return vec2(
7675
+ this.textWidth || this.textScale * this.size.x,
7676
+ this.textHeight || this.textScale * this.size.y);
7595
7677
  }
7596
7678
 
7679
+ /** @return {boolean} - Is the mouse hovering over this element */
7680
+ isHoverObject() { return uiSystem.hoverObject === this; }
7681
+
7682
+ /** @return {boolean} - Is the mouse held onto this element */
7683
+ isActiveObject() { return uiSystem.activeObject === this; }
7684
+
7597
7685
  /** Called when the mouse enters the object */
7598
7686
  onEnter() {}
7599
7687
 
@@ -7641,7 +7729,7 @@ class UIText extends UIObject
7641
7729
  }
7642
7730
  render()
7643
7731
  {
7644
- const textSize = vec2(this.size.x, this.textHeight || this.size.y);
7732
+ const textSize = this.getTextSize();
7645
7733
  uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font);
7646
7734
  }
7647
7735
  }
@@ -7704,14 +7792,10 @@ class UIButton extends UIObject
7704
7792
  }
7705
7793
  render()
7706
7794
  {
7707
- // draw the button
7708
- const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
7709
- const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
7710
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7795
+ super.render();
7711
7796
 
7712
- // draw the text
7713
- const textScale = .8; // scale text to fit
7714
- const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7797
+ // draw the text scaled to fit
7798
+ const textSize = this.getTextSize();
7715
7799
  uiSystem.drawText(this.text, this.pos, textSize,
7716
7800
  this.textColor, 0, undefined, this.align, this.font);
7717
7801
  }
@@ -7749,8 +7833,7 @@ class UICheckbox extends UIObject
7749
7833
  }
7750
7834
  render()
7751
7835
  {
7752
- const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
7753
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
7836
+ super.render();
7754
7837
  if (this.checked)
7755
7838
  {
7756
7839
  const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
@@ -7760,13 +7843,11 @@ class UICheckbox extends UIObject
7760
7843
  uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
7761
7844
  }
7762
7845
 
7763
- // draw the text to the right side of the checkbox
7764
- const textScale = .8; // scale text to fit
7765
- const gapScale = .55;
7766
- const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7767
- const pos = this.pos.add(vec2(this.size.x*gapScale,0));
7846
+ // draw the text next to the checkbox
7847
+ const textSize = this.getTextSize();
7848
+ const pos = this.pos.add(vec2(this.size.x,0));
7768
7849
  uiSystem.drawText(this.text, pos, textSize,
7769
- this.textColor, 0, undefined, 'left', this.font);
7850
+ this.textColor, 0, undefined, 'left', this.font, false);
7770
7851
  }
7771
7852
  }
7772
7853
 
@@ -7802,7 +7883,7 @@ class UIScrollbar extends UIObject
7802
7883
  update()
7803
7884
  {
7804
7885
  super.update();
7805
- if (this.mouseIsHeld && this.interactive)
7886
+ if (this.isActiveObject() && this.interactive)
7806
7887
  {
7807
7888
  // check if value changed
7808
7889
  const handleSize = vec2(this.size.y);
@@ -7816,12 +7897,7 @@ class UIScrollbar extends UIObject
7816
7897
  }
7817
7898
  render()
7818
7899
  {
7819
- // draw the scrollbar background
7820
- const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
7821
- this.color : this.lineColor;
7822
- const color = this.disabled ? this.disabledColor :
7823
- this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
7824
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
7900
+ super.render();
7825
7901
 
7826
7902
  // draw the scrollbar handle
7827
7903
  const handleSize = vec2(this.size.y);
@@ -7830,12 +7906,11 @@ class UIScrollbar extends UIObject
7830
7906
  const p2 = this.pos.x + handleWidth/2;
7831
7907
  const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
7832
7908
  const handleColor = this.disabled ? this.disabledColor :
7833
- this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
7909
+ this.interactive && this.isActiveObject() ? this.color : this.handleColor;
7834
7910
  uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
7835
7911
 
7836
- // draw the text on the scrollbar
7837
- const textScale = .8; // scale text to fit in scrollbar
7838
- const textSize = vec2(this.size.x, this.textHeight || this.size.y*textScale);
7912
+ // draw the text scaled to fit on the scrollbar
7913
+ const textSize = this.getTextSize();
7839
7914
  uiSystem.drawText(this.text, this.pos, textSize,
7840
7915
  this.textColor, 0, undefined, this.align, this.font);
7841
7916
  }