littlejsengine 1.14.7 → 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.
@@ -63,6 +63,10 @@ class UISystemPlugin
63
63
  this.uiObjects = [];
64
64
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
65
65
  this.uiContext = context;
66
+ /** @property {UIObject} - Top most object user is over */
67
+ this.hoverObject = undefined;
68
+ /** @property {UIObject} - Object user is currently interacting with */
69
+ this.activeObject = undefined;
66
70
 
67
71
  engineAddPlugin(uiUpdate, uiRender);
68
72
 
@@ -92,6 +96,8 @@ class UISystemPlugin
92
96
  else
93
97
  updateInvisibleObject(o);
94
98
  }
99
+ // reset hover object at start of update
100
+ uiSystem.hoverObject = undefined;
95
101
  for (let i = uiSystem.uiObjects.length; i--;)
96
102
  {
97
103
  const o = uiSystem.uiObjects[i];
@@ -203,6 +209,8 @@ class UIObject
203
209
  this.size = size.copy();
204
210
  /** @property {Color} - Color of the object */
205
211
  this.color = uiSystem.defaultColor;
212
+ /** @property {Color} - Color of the object when active, uses color if undefined */
213
+ this.activeColor = undefined;
206
214
  /** @property {string} - Text for this ui object */
207
215
  this.text = undefined;
208
216
  /** @property {Color} - Color when disabled */
@@ -233,7 +241,7 @@ class UIObject
233
241
  this.children = [];
234
242
  /** @property {UIObject} - This object's parent, position is in parent space */
235
243
  this.parent = undefined;
236
- /** @property {number} - Extra size added to make small buttons easier to touch on mobile devices */
244
+ /** @property {number} - Added size to make small buttons easier to touch on mobile devices */
237
245
  this.extraTouchSize = 0;
238
246
  /** @property {Sound} - Sound when interactive element is pressed */
239
247
  this.soundPress = uiSystem.defaultSoundPress;
@@ -241,12 +249,10 @@ class UIObject
241
249
  this.soundRelease = uiSystem.defaultSoundRelease;
242
250
  /** @property {Sound} - Sound when interactive element is clicked */
243
251
  this.soundClick = uiSystem.defaultSoundClick;
244
- /** @property {boolean} - Is the mouse over this element */
245
- this.mouseIsOver = false;
246
252
  /** @property {boolean} - Is this element interactive */
247
253
  this.interactive = false;
248
- /** @property {boolean} - Is the mouse held on this element (was pressed while over and hasn't been released) */
249
- this.mouseIsHeld = false;
254
+ /** @property {boolean} - Activate when dragged over with mouse held down */
255
+ this.dragActivate = false;
250
256
  uiSystem.uiObjects.push(this);
251
257
  }
252
258
 
@@ -273,15 +279,18 @@ class UIObject
273
279
  /** Update the object, called automatically by plugin once each frame */
274
280
  update()
275
281
  {
276
- const mouseWasOver = this.mouseIsOver;
277
- const mousePress = mouseWasPressed(0);
282
+ const wasHover = this.isHoverObject();
283
+ const isActive = this.isActiveObject();
278
284
  const mouseDown = mouseIsDown(0);
279
- if (mousePress || !mouseDown || this.mouseIsHeld)
285
+ const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
286
+ if (!uiSystem.hoverObject)
287
+ if (mousePress || !mouseDown || isActive)
280
288
  {
281
289
  const size = this.size.add(vec2(isTouchDevice && this.extraTouchSize || 0));
282
- this.mouseIsOver = isOverlapping(this.pos, size, mousePosScreen);
290
+ if (isOverlapping(this.pos, size, mousePosScreen))
291
+ uiSystem.hoverObject = this;
283
292
  }
284
- if (this.mouseIsOver)
293
+ if (this.isHoverObject())
285
294
  {
286
295
  if (mousePress)
287
296
  inputClearKey(0,0,0,1,0); // clear mouse was pressed state
@@ -294,10 +303,12 @@ class UIObject
294
303
  this.onPress();
295
304
  if (this.soundPress)
296
305
  this.soundPress.play();
306
+ if (uiSystem.activeObject && !isActive)
307
+ uiSystem.activeObject.onRelease();
308
+ uiSystem.activeObject = this;
297
309
  }
298
- this.mouseIsHeld = true;
299
310
  }
300
- if (!mouseDown && this.mouseIsHeld && this.interactive)
311
+ if (!mouseDown && uiSystem.activeObject === this && this.interactive)
301
312
  {
302
313
  this.onClick();
303
314
  if (this.soundClick)
@@ -305,35 +316,34 @@ class UIObject
305
316
  }
306
317
  }
307
318
  }
308
- if (!mouseDown)
319
+ if (isActive && (!mouseDown || !this.isHoverObject()))
309
320
  {
310
- if (this.mouseIsHeld && this.interactive)
311
- {
312
- this.onRelease();
313
- if (this.soundRelease)
314
- this.soundRelease.play();
315
- }
316
- if (isTouchDevice)
317
- this.mouseIsOver = false;
318
- this.mouseIsHeld = false;
321
+ this.onRelease();
322
+ if (this.soundRelease)
323
+ this.soundRelease.play();
324
+ uiSystem.activeObject = undefined;
319
325
  }
320
326
 
321
- if (this.mouseIsOver !== mouseWasOver)
322
- this.mouseIsOver ? this.onEnter() : this.onLeave();
327
+ if (this.isHoverObject() !== wasHover)
328
+ this.isHoverObject() ? this.onEnter() : this.onLeave();
323
329
  }
324
330
 
325
331
  /** Render the object, called automatically by plugin once each frame */
326
332
  render()
327
333
  {
328
- if (this.size.x && this.size.y)
329
- uiSystem.drawRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor, this.cornerRadius);
334
+ if (!this.size.x || !this.size.y) return;
335
+
336
+ const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
337
+ const color = this.interactive ? this.disabled ? this.disabledColor : this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
338
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
330
339
  }
331
340
 
332
341
  /** Special update when object is not visible */
333
342
  updateInvisible()
334
343
  {
335
344
  // reset input state when not visible
336
- this.mouseIsOver = this.mouseIsHeld = false;
345
+ if (this.isActiveObject())
346
+ uiSystem.activeObject = undefined;
337
347
  }
338
348
 
339
349
  /** Get the size for text with overrides and scale
@@ -346,6 +356,12 @@ class UIObject
346
356
  this.textHeight || this.textScale * this.size.y);
347
357
  }
348
358
 
359
+ /** @return {boolean} - Is the mouse hovering over this element */
360
+ isHoverObject() { return uiSystem.hoverObject === this; }
361
+
362
+ /** @return {boolean} - Is the mouse held onto this element */
363
+ isActiveObject() { return uiSystem.activeObject === this; }
364
+
349
365
  /** Called when the mouse enters the object */
350
366
  onEnter() {}
351
367
 
@@ -456,10 +472,7 @@ class UIButton extends UIObject
456
472
  }
457
473
  render()
458
474
  {
459
- // draw the button
460
- const lineColor = this.mouseIsHeld && !this.disabled ? this.color : this.lineColor;
461
- const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
462
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
475
+ super.render();
463
476
 
464
477
  // draw the text scaled to fit
465
478
  const textSize = this.getTextSize();
@@ -500,8 +513,7 @@ class UICheckbox extends UIObject
500
513
  }
501
514
  render()
502
515
  {
503
- const color = this.disabled ? this.disabledColor : this.mouseIsOver ? this.hoverColor : this.color;
504
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, this.lineColor, this.cornerRadius);
516
+ super.render();
505
517
  if (this.checked)
506
518
  {
507
519
  const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
@@ -551,7 +563,7 @@ class UIScrollbar extends UIObject
551
563
  update()
552
564
  {
553
565
  super.update();
554
- if (this.mouseIsHeld && this.interactive)
566
+ if (this.isActiveObject() && this.interactive)
555
567
  {
556
568
  // check if value changed
557
569
  const handleSize = vec2(this.size.y);
@@ -565,12 +577,7 @@ class UIScrollbar extends UIObject
565
577
  }
566
578
  render()
567
579
  {
568
- // draw the scrollbar background
569
- const lineColor = this.interactive && this.mouseIsHeld && !this.disabled ?
570
- this.color : this.lineColor;
571
- const color = this.disabled ? this.disabledColor :
572
- this.interactive && this.mouseIsHeld ? this.hoverColor : this.color;
573
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
580
+ super.render();
574
581
 
575
582
  // draw the scrollbar handle
576
583
  const handleSize = vec2(this.size.y);
@@ -579,7 +586,7 @@ class UIScrollbar extends UIObject
579
586
  const p2 = this.pos.x + handleWidth/2;
580
587
  const handlePos = vec2(lerp(p1, p2, this.value), this.pos.y);
581
588
  const handleColor = this.disabled ? this.disabledColor :
582
- this.interactive && this.mouseIsHeld ? this.color : this.handleColor;
589
+ this.interactive && this.isActiveObject() ? this.color : this.handleColor;
583
590
  uiSystem.drawRect(handlePos, handleSize, handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
584
591
 
585
592
  // draw the text scaled to fit on the scrollbar
package/reference.md CHANGED
@@ -186,6 +186,7 @@ getCameraSize() // Get the camera's visible area in world space
186
186
  // Display settings
187
187
  canvasMaxSize = (1920, 1200) // The max size of the canvas
188
188
  canvasFixedSize = (0, 0) // Fixed size of the canvas
189
+ canvasClearColor = BLACK // Color used to clear the canvas at start of frame
189
190
  fontDefault = 'arial' // Default font used for text rendering
190
191
  canvasPixelated = true // Use nearest neighbor canvas scaling for more pixelated look
191
192
  tilesPixelated = true // Disable filtering for crisper pixel art
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {string}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.14.7';
33
+ const engineVersion = '1.14.8';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
package/src/engineDraw.js CHANGED
@@ -906,7 +906,7 @@ class FontImage
906
906
  * @param {Vector2} pos
907
907
  * @param {number} [scale=.25]
908
908
  * @param {boolean} [center]
909
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D}[context=drawContext]
909
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
910
910
  */
911
911
  drawText(text, pos, scale=1, center, context=drawContext)
912
912
  {
@@ -216,6 +216,7 @@ function inputInit()
216
216
  document.addEventListener('wheel', onMouseWheel);
217
217
  document.addEventListener('contextmenu', onContextMenu);
218
218
  document.addEventListener('blur', onBlur);
219
+ document.addEventListener('mouseleave', onMouseLeave);
219
220
 
220
221
  // init touch input
221
222
  if (isTouchDevice && touchInputEnable)
@@ -274,6 +275,12 @@ function inputInit()
274
275
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
275
276
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
276
277
  function onBlur() { inputClear(); } // reset input when focus is lost
278
+ function onMouseLeave()
279
+ {
280
+ // set mouse position and delta when leaving canvas
281
+ mousePosScreen = vec2(Infinity);
282
+ mouseDeltaScreen = vec2(0);
283
+ }
277
284
  }
278
285
 
279
286
  // convert a mouse or touch event position to screen space
@@ -52,7 +52,7 @@ class EngineObject
52
52
  /** @property {Vector2} - World space position of the object */
53
53
  this.pos = pos.copy();
54
54
  /** @property {Vector2} - World space width and height of the object */
55
- this.size = size;
55
+ this.size = size.copy();
56
56
  /** @property {Vector2} - Size of object used for drawing, uses size if not set */
57
57
  this.drawSize = undefined;
58
58
  /** @property {TileInfo} - Tile info to render object (undefined is untextured) */
@@ -60,7 +60,7 @@ class EngineObject
60
60
  /** @property {number} - Angle to rotate the object */
61
61
  this.angle = angle;
62
62
  /** @property {Color} - Color to apply when rendered */
63
- this.color = color;
63
+ this.color = color.copy();
64
64
  /** @property {Color} - Additive color to apply when rendered */
65
65
  this.additiveColor = undefined;
66
66
  /** @property {boolean} - Should it flip along y axis when rendered */
@@ -184,7 +184,7 @@ class EngineObject
184
184
  for (const o of engineObjectsCollide)
185
185
  {
186
186
  // non solid objects don't collide with each other
187
- if (!this.isSolid && !o.isSolid || o.destroyed || o.parent || o === this)
187
+ if ((!this.isSolid && !o.isSolid) || o.destroyed || o.parent || o === this)
188
188
  continue;
189
189
 
190
190
  // check collision
@@ -223,7 +223,7 @@ class EngineObject
223
223
  {
224
224
  // push outside object collision
225
225
  this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
226
- if (o.groundObject && wasMovingDown || !o.mass)
226
+ if ((o.groundObject && wasMovingDown) || !o.mass)
227
227
  {
228
228
  // set ground object if landed on something
229
229
  if (wasMovingDown)
@@ -390,7 +390,7 @@ function setGLEnable(enable)
390
390
  glCanvas.style.visibility = enable ? 'visible' : 'hidden';
391
391
  }
392
392
 
393
- /** Set how many sided polygons to use when drawing circles and elipses with WebGL
393
+ /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
394
394
  * @param {number} sides
395
395
  * @memberof Settings */
396
396
  function setGLCircleSides(sides) { glCircleSides = sides; }
@@ -156,7 +156,7 @@ class TileLayerData
156
156
  /** @property {boolean} - If the tile should be mirrored along the x axis */
157
157
  this.mirror = mirror;
158
158
  /** @property {Color} - Color of the tile */
159
- this.color = color;
159
+ this.color = color.copy();
160
160
  }
161
161
 
162
162
  /** Set this tile to clear, it will not be rendered */
@@ -255,7 +255,7 @@ class CanvasLayer extends EngineObject
255
255
  * @param {TileInfo} [tileInfo]
256
256
  * @param {Color} [color=(1,1,1,1)]
257
257
  * @param {number} [angle=0]
258
- * @param {boolean} [mirror=0] */
258
+ * @param {boolean} [mirror=false] */
259
259
  drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle, mirror)
260
260
  {
261
261
  this.drawCanvas2D(pos, size, angle, mirror, (context)=>
@@ -155,8 +155,11 @@ function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
155
155
  * @memberof Utilities */
156
156
  function isOverlapping(posA, sizeA, posB, sizeB=vec2())
157
157
  {
158
- return abs(posA.x - posB.x)*2 < sizeA.x + sizeB.x
159
- && abs(posA.y - posB.y)*2 < sizeA.y + sizeB.y;
158
+ const dx = (posA.x - posB.x)*2;
159
+ const dy = (posA.y - posB.y)*2;
160
+ const sx = sizeA.x + sizeB.x;
161
+ const sy = sizeA.y + sizeB.y;
162
+ return dx >= -sx && dx < sx && dy >= -sy && dy < sy;
160
163
  }
161
164
 
162
165
  /** Returns true if a line segment is intersecting an axis aligned box
@@ -679,7 +679,7 @@ function glPolyStrip(points)
679
679
  while (indices.length > 3 && attempts++ < maxAttempts)
680
680
  {
681
681
  let foundEar = false;
682
- for (let i = indices.length; --i;)
682
+ for (let i = 0; i < indices.length; i++)
683
683
  {
684
684
  const i0 = indices[(i + indices.length - 1) % indices.length];
685
685
  const i1 = indices[i];
@@ -716,7 +716,7 @@ function glPolyStrip(points)
716
716
  if (!foundEar)
717
717
  {
718
718
  let worstIndex = -1, worstValue = Infinity;
719
- for (let i = indices.length; --i;)
719
+ for (let i = 0; i < indices.length; i++)
720
720
  {
721
721
  const i0 = indices[(i + indices.length - 1) % indices.length];
722
722
  const i1 = indices[i];