littlejsengine 1.18.17 → 1.18.18

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.
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.18.17';
38
+ const engineVersion = '1.18.18';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -1545,10 +1545,25 @@ class Color
1545
1545
  * @return {Color} */
1546
1546
  setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
1547
1547
 
1548
+ /** Sets the alpha of this color and returns self
1549
+ * @param {number} [a] - alpha
1550
+ * @return {Color} */
1551
+ setAlpha(a=1)
1552
+ {
1553
+ this.a = a;
1554
+ ASSERT_COLOR_VALID(this);
1555
+ return this;
1556
+ }
1557
+
1548
1558
  /** Returns a new color that is a copy of this
1549
1559
  * @return {Color} */
1550
1560
  copy() { return new Color(this.r, this.g, this.b, this.a); }
1551
1561
 
1562
+ /** Returns a copy of this color with the alpha set
1563
+ * @param {number} [a] - alpha
1564
+ * @return {Color} */
1565
+ withAlpha(a=1) { return new Color(this.r, this.g, this.b, a); }
1566
+
1552
1567
  /** Returns a copy of this color plus the color passed in
1553
1568
  * @param {Color} c - other color
1554
1569
  * @return {Color} */
@@ -2143,6 +2158,8 @@ let canvasFixedSize = vec2();
2143
2158
  let canvasPixelated = false;
2144
2159
 
2145
2160
  /** Disables texture filtering for crisper pixel art
2161
+ * - Leave true for pixel art so sprites stay sharp when scaled (uses NEAREST filtering)
2162
+ * - Set false for smooth/high-resolution art to enable bilinear filtering and mipmaps
2146
2163
  * @type {boolean}
2147
2164
  * @default
2148
2165
  * @memberof Settings */
@@ -2496,6 +2513,7 @@ function setCanvasPixelated(pixelated)
2496
2513
  }
2497
2514
 
2498
2515
  /** Disables texture filtering for crisper pixel art
2516
+ * - Leave true for pixel art; set false for smooth/high-resolution art
2499
2517
  * @param {boolean} pixelated
2500
2518
  * @memberof Settings */
2501
2519
  function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
@@ -4387,6 +4405,62 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
4387
4405
  * @memberof Draw */
4388
4406
  function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
4389
4407
 
4408
+ /** Fit the camera to a rectangle in world space by setting cameraPos and cameraScale
4409
+ * - worldMargin pads the content rectangle in world units, so the gap scales with the content on resize
4410
+ * - screenInset reserves space in screen pixels on each viewport edge (for example a HUD band) and
4411
+ * re-centers the content away from that edge, so the reserved band stays a fixed pixel size on resize
4412
+ * - worldMargin and screenInset may each be a number for all sides, a Vector2 (x=left/right, y=top/bottom),
4413
+ * or an object with any of {top, right, bottom, left}
4414
+ * @param {Vector2} center - Center of the rectangle in world space
4415
+ * @param {Vector2} size - Size of the rectangle in world space
4416
+ * @param {number|Vector2|Object} [worldMargin] - World space padding added around the content rectangle
4417
+ * @param {number|Vector2|Object} [screenInset] - Screen space padding in pixels reserved on each viewport edge
4418
+ * @return {number} - The new camera scale
4419
+ * @memberof Draw */
4420
+ function cameraFit(center, size, worldMargin, screenInset)
4421
+ {
4422
+ ASSERT(isVector2(center), 'center must be a vec2');
4423
+ ASSERT(isVector2(size), 'size must be a vec2');
4424
+
4425
+ // pad the content
4426
+ const margin = padSides(worldMargin);
4427
+ const inset = padSides(screenInset);
4428
+ const worldW = size.x + margin.left + margin.right;
4429
+ const worldH = size.y + margin.top + margin.bottom;
4430
+ const viewW = mainCanvasSize.x - inset.left - inset.right;
4431
+ const viewH = mainCanvasSize.y - inset.top - inset.bottom;
4432
+
4433
+ // bail on a degenerate rect or viewport rather than NaN the camera
4434
+ if (!(worldW > 0 && worldH > 0 && viewW > 0 && viewH > 0))
4435
+ return cameraScale;
4436
+
4437
+ // scale to fit the padded content
4438
+ cameraScale = min(viewW / worldW, viewH / worldH);
4439
+
4440
+ // calculate offset vectors
4441
+ const marginVector = vec2(margin.right - margin.left, margin.top - margin.bottom).scale(.5);
4442
+ const insetVector = vec2(inset.right - inset.left, inset.top - inset.bottom).scale(.5 / cameraScale);
4443
+
4444
+ // apply the offsets and return camera scale
4445
+ cameraPos = center.add(marginVector).add(insetVector);
4446
+ return cameraScale;
4447
+
4448
+ function padSides(p)
4449
+ {
4450
+ // normalize a padding option to {top, right, bottom, left}
4451
+ if (p === undefined || isNumber(p))
4452
+ p = vec2(p);
4453
+ if (isVector2(p))
4454
+ return { top: p.y, right: p.x, bottom: p.y, left: p.x };
4455
+ return {
4456
+ top: p.top || 0,
4457
+ right: p.right || 0,
4458
+ bottom: p.bottom || 0,
4459
+ left: p.left || 0,
4460
+ };
4461
+ }
4462
+ }
4463
+
4390
4464
  /** Check if a box, point, or circle is on screen with a circle test
4391
4465
  * If size is a Vector2, uses the length as diameter
4392
4466
  * This can be used to cull offscreen objects from render or update
@@ -4425,14 +4499,13 @@ function isOnScreen(pos, size=0)
4425
4499
  y + size > -h && y - size < h;
4426
4500
  }
4427
4501
 
4428
- /** Enable normal or additive blend mode
4502
+ /** Enable additive blending
4429
4503
  * @param {boolean} [additive]
4430
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
4431
4504
  * @memberof Draw */
4432
- function setBlendMode(additive=false, context=drawContext)
4505
+ function setAdditiveBlendMode(additive=true)
4433
4506
  {
4434
4507
  glAdditive = additive;
4435
- context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4508
+ drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
4436
4509
  }
4437
4510
 
4438
4511
  /** Combines LittleJS canvases onto the main canvas
@@ -4758,11 +4831,30 @@ let mouseWheel = 0;
4758
4831
  * @memberof Input */
4759
4832
  let mouseInWindow = true;
4760
4833
 
4761
- /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
4834
+ /** True if a gamepad is the most recently used input device.
4835
+ * Equivalent to usingGamepadInput(); derived from lastInputDevice each frame.
4762
4836
  * @type {boolean}
4763
4837
  * @memberof Input */
4764
4838
  let isUsingGamepad = false;
4765
4839
 
4840
+ /** The most recently used input device: 'mouse' | 'keyboard' | 'gamepad'.
4841
+ * Sticky: it holds its value while every device is idle, so a mouse-follow
4842
+ * control (e.g. paddle = mousePos) won't snap back the instant the stick/keys
4843
+ * are released. With several devices in play at once (e.g. keyboard to move +
4844
+ * mouse to aim) it tracks whichever was touched last each frame, so it may
4845
+ * alternate — that's intended; use it to pick which control drives a shared
4846
+ * action. Updated every frame by inputUpdate().
4847
+ * @type {string}
4848
+ * @memberof Input */
4849
+ let lastInputDevice = 'mouse';
4850
+
4851
+ /** Screen-pixel mouse movement per frame that counts as "using the mouse"
4852
+ * (so sub-pixel hand jitter doesn't steal focus from the keyboard/gamepad).
4853
+ * @type {number}
4854
+ * @default
4855
+ * @memberof Input */
4856
+ let inputMouseMoveThreshold = 6;
4857
+
4766
4858
  /** Prevents input continuing to the default browser handling (true by default)
4767
4859
  * @type {boolean}
4768
4860
  * @memberof Input */
@@ -4783,6 +4875,18 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
4783
4875
  * @memberof Input */
4784
4876
  function setInputPreventDefault(preventDefault=true) { inputPreventDefault = preventDefault; }
4785
4877
 
4878
+ /** Set the screen-pixel mouse movement per frame that counts as using the mouse
4879
+ * @param {number} threshold
4880
+ * @memberof Input */
4881
+ function setInputMouseMoveThreshold(threshold) { inputMouseMoveThreshold = threshold; }
4882
+
4883
+ /** @return {boolean} - Is the mouse the most recently used input device? @memberof Input */
4884
+ function usingMouseInput() { return lastInputDevice === 'mouse'; }
4885
+ /** @return {boolean} - Is the keyboard the most recently used input device? @memberof Input */
4886
+ function usingKeyboardInput() { return lastInputDevice === 'keyboard'; }
4887
+ /** @return {boolean} - Is a gamepad the most recently used input device? @memberof Input */
4888
+ function usingGamepadInput() { return lastInputDevice === 'gamepad'; }
4889
+
4786
4890
  /** Clears an input key state
4787
4891
  * @param {string|number} key
4788
4892
  * @param {number} [device]
@@ -5083,7 +5187,6 @@ function inputInit()
5083
5187
  {
5084
5188
  if (!e.repeat)
5085
5189
  {
5086
- isUsingGamepad = false;
5087
5190
  inputData[0][e.code] = 3;
5088
5191
  if (inputWASDEmulateDirection)
5089
5192
  inputData[0][remapKey(e.code)] = 3;
@@ -5142,7 +5245,6 @@ function inputInit()
5142
5245
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
5143
5246
  audioContext.resume();
5144
5247
 
5145
- isUsingGamepad = false;
5146
5248
  inputData[0][e.button] = 3;
5147
5249
 
5148
5250
  const mousePosScreenLast = mousePosScreen;
@@ -5233,10 +5335,7 @@ function inputInit()
5233
5335
  if (wasTouching)
5234
5336
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
5235
5337
  else
5236
- {
5237
5338
  inputData[0][button] = 3;
5238
- isUsingGamepad = false; // a passthrough tap is mouse-style input
5239
- }
5240
5339
  }
5241
5340
  else if (wasTouching)
5242
5341
  inputData[0][button] = inputData[0][button] & 2 | 4;
@@ -5282,7 +5381,43 @@ function inputUpdate()
5282
5381
 
5283
5382
  // update gamepads if enabled
5284
5383
  gamepadsUpdate();
5285
-
5384
+
5385
+ // update most recently used input device
5386
+ updateLastInputDevice();
5387
+
5388
+ function updateLastInputDevice()
5389
+ {
5390
+ // mouse: any button held or moved
5391
+ const mouseActive = mouseIsDown(0) || mouseIsDown(1) || mouseIsDown(2) || mouseDeltaScreen.length() > inputMouseMoveThreshold;
5392
+
5393
+ // gamepad: any button held or stick moved
5394
+ let gamepadActive = false;
5395
+ for (let s = gamepadStickCount(); s-- && !gamepadActive;)
5396
+ gamepadActive = gamepadStick(s).lengthSquared() > .04;
5397
+ for (let b = 17; b-- && !gamepadActive;)
5398
+ gamepadActive = gamepadIsDown(b);
5399
+
5400
+ // keyboard: any non-mouse key down
5401
+ let keyboardActive = false;
5402
+ for (const k in inputData[0])
5403
+ if (isNaN(+k) && (inputData[0][k] & 1))
5404
+ {
5405
+ keyboardActive = true;
5406
+ break;
5407
+ }
5408
+
5409
+ // update the last input
5410
+ if (gamepadActive)
5411
+ lastInputDevice = 'gamepad';
5412
+ else if (mouseActive)
5413
+ lastInputDevice = 'mouse';
5414
+ else if (keyboardActive)
5415
+ lastInputDevice = 'keyboard';
5416
+
5417
+ // set flag if gamepad is last device
5418
+ isUsingGamepad = lastInputDevice === 'gamepad';
5419
+ }
5420
+
5286
5421
  // gamepads are updated by engine every frame automatically
5287
5422
  function gamepadsUpdate()
5288
5423
  {
@@ -5405,7 +5540,6 @@ function inputUpdate()
5405
5540
  gamepadHadInput[i] = true;
5406
5541
  if (!gamepadHadInput[gamepadPrimary])
5407
5542
  gamepadPrimary = i;
5408
- isUsingGamepad ||= (gamepadPrimary === i);
5409
5543
  }
5410
5544
 
5411
5545
  if (gamepad.mapping === 'standard')
@@ -5552,12 +5686,19 @@ function touchGamepadRelayout()
5552
5686
  const setZone = (z, css)=> z.style.cssText =
5553
5687
  'position:absolute;pointer-events:auto;touch-action:none;' + css;
5554
5688
 
5555
- if (paused && touchGamepadCenterButtonSize)
5689
+ if (paused)
5556
5690
  {
5557
- // while paused, any touch presses start
5558
- setZone(touchGamepadZoneC, 'inset:0');
5691
+ // the gamepad is hidden while paused, so its side zones must not capture
5692
+ // touches - otherwise they silently steal taps from menus and dialogs
5559
5693
  for (const zone of touchGamepadSideZones) zone.style.display = 'none';
5560
- touchGamepadZoneC.style.display = '';
5694
+ if (touchGamepadCenterButtonSize)
5695
+ {
5696
+ // any touch presses start
5697
+ setZone(touchGamepadZoneC, 'inset:0');
5698
+ touchGamepadZoneC.style.display = '';
5699
+ }
5700
+ else
5701
+ touchGamepadZoneC.style.display = 'none';
5561
5702
  }
5562
5703
  else
5563
5704
  {
@@ -5833,7 +5974,6 @@ function touchGamepadPointerDown(e, zone)
5833
5974
  e.preventDefault();
5834
5975
  zone.setPointerCapture(e.pointerId);
5835
5976
  touchGamepadTimer.set();
5836
- isUsingGamepad = true;
5837
5977
 
5838
5978
  // resume audio on first interaction
5839
5979
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
@@ -7849,9 +7989,6 @@ class Particle
7849
7989
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
7850
7990
  this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
7851
7991
 
7852
- // draw the particle
7853
- additive && setBlendMode(true);
7854
-
7855
7992
  // update the position and angle for drawing
7856
7993
  const pos = particleDrawPos.set(this.pos.x, this.pos.y);
7857
7994
  let angle = this.angle;
@@ -7864,6 +8001,9 @@ class Particle
7864
8001
  emitter.pos.y + pos.x*s + pos.y*c);
7865
8002
  angle += a;
7866
8003
  }
8004
+
8005
+ // draw the particle
8006
+ additive && setAdditiveBlendMode();
7867
8007
  if (trailScale)
7868
8008
  {
7869
8009
  // trail style particles
@@ -7881,7 +8021,7 @@ class Particle
7881
8021
  }
7882
8022
  else
7883
8023
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
7884
- additive && setBlendMode();
8024
+ additive && setAdditiveBlendMode(false);
7885
8025
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
7886
8026
  }
7887
8027
  }
@@ -9791,7 +9931,7 @@ class LightSystemPlugin
9791
9931
 
9792
9932
  // 3. walk engineObjects calling renderLight() — additive blend
9793
9933
  // (lightmap accumulates raw additive color contributions)
9794
- setBlendMode(true);
9934
+ setAdditiveBlendMode();
9795
9935
  glContext.enable(glContext.BLEND);
9796
9936
  glContext.blendFunc(glContext.ONE, glContext.ONE);
9797
9937
 
@@ -9825,7 +9965,7 @@ class LightSystemPlugin
9825
9965
  // is, and any debug text / future draw could sample the lightmap)
9826
9966
  if (glActiveTexture)
9827
9967
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
9828
- setBlendMode(prevAdditive);
9968
+ setAdditiveBlendMode(prevAdditive);
9829
9969
  glSetInstancedMode(true);
9830
9970
  }
9831
9971
  function lightSystemContextLost()
@@ -10234,7 +10374,7 @@ class UISystemPlugin
10234
10374
  let targetPos, targetSize;
10235
10375
  if (o.parent)
10236
10376
  {
10237
- targetPos = o.parent.pos;
10377
+ targetPos = o.parent.nativePos;
10238
10378
  targetSize = o.parent.size;
10239
10379
  }
10240
10380
  else
@@ -10248,7 +10388,7 @@ class UISystemPlugin
10248
10388
  }
10249
10389
 
10250
10390
  const a = o.anchor;
10251
- o.pos = targetPos
10391
+ o.nativePos = targetPos
10252
10392
  .add(targetSize.multiply(a).scale(.5)) // anchor point on target
10253
10393
  .subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
10254
10394
  .add(o.localPos); // user offset
@@ -10354,13 +10494,18 @@ class UISystemPlugin
10354
10494
 
10355
10495
  function updateObject(o)
10356
10496
  {
10357
- if (!o.visible) return;
10497
+ if (o.destroyed || !o.visible) return;
10358
10498
 
10359
10499
  // update in reverse order to detect mouse enter/leave
10360
10500
  updateTransforms(o);
10361
10501
  for (let i=o.children.length; i--;)
10362
- updateObject(o.children[i]);
10363
- o.update();
10502
+ {
10503
+ // a child may destroy siblings mid-update (e.g. dialog close)
10504
+ const child = o.children[i];
10505
+ child && updateObject(child);
10506
+ }
10507
+ if (!o.destroyed)
10508
+ o.update();
10364
10509
  }
10365
10510
  }
10366
10511
  function uiRender()
@@ -10784,10 +10929,15 @@ class UIObject
10784
10929
  ASSERT(isVector2(pos), 'ui object pos must be a vec2');
10785
10930
  ASSERT(isVector2(size), 'ui object size must be a vec2');
10786
10931
 
10787
- /** @property {Vector2} - Local position of the object */
10932
+ /** @property {Vector2} - Position you set: an offset from this object's
10933
+ * anchor point (the parent box, or the canvas for roots). This is the
10934
+ * input that controls placement — set this, not nativePos. */
10788
10935
  this.localPos = pos.copy();
10789
- /** @property {Vector2} - Screen space position of the object */
10790
- this.pos = pos.copy();
10936
+ /** @property {Vector2} - Resolved position in native UI space, recomputed
10937
+ * every frame from localPos + anchor (and nativeHeight, if set). This is a
10938
+ * derived output used for drawing and hit-testing; assigning to it has no
10939
+ * effect since it is overwritten each frame. Set localPos instead. */
10940
+ this.nativePos = pos.copy();
10791
10941
  /** @property {Vector2} - Screen space size of the object */
10792
10942
  this.size = size.copy();
10793
10943
  /** @property {Color} - Color of the object */
@@ -10922,7 +11072,7 @@ class UIObject
10922
11072
  const size = !isTouchDevice ? this.size :
10923
11073
  this.size.add(vec2(this.extraTouchSize || 0));
10924
11074
  const pos = uiSystem.screenToNative(mousePosScreen);
10925
- return isOverlapping(this.pos, size, pos);
11075
+ return isOverlapping(this.nativePos, size, pos);
10926
11076
  }
10927
11077
 
10928
11078
  /** Update the object, called automatically by plugin once each frame */
@@ -11012,7 +11162,7 @@ class UIObject
11012
11162
  this.color : this.color;
11013
11163
  const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
11014
11164
 
11015
- uiSystem.drawRect(this.pos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
11165
+ uiSystem.drawRect(this.nativePos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
11016
11166
  }
11017
11167
 
11018
11168
  /** Get the size for text with overrides and scale
@@ -11049,8 +11199,8 @@ class UIObject
11049
11199
  let text = 'type = ' + this.constructor.name;
11050
11200
  if (this.text)
11051
11201
  text += '\ntext = ' + this.text;
11052
- if (this.pos.x || this.pos.y)
11053
- text += '\npos = ' + this.pos;
11202
+ if (this.nativePos.x || this.nativePos.y)
11203
+ text += '\nnativePos = ' + this.nativePos;
11054
11204
  if (this.localPos.x || this.localPos.y)
11055
11205
  text += '\nlocalPos = ' + this.localPos;
11056
11206
  if (this.size.x || this.size.y)
@@ -11070,7 +11220,7 @@ class UIObject
11070
11220
  this.isHoverObject() ? YELLOW :
11071
11221
  this.disabled ? PURPLE :
11072
11222
  this.interactive ? RED : BLUE;
11073
- uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
11223
+ uiSystem.drawRect(this.nativePos, this.size, CLEAR_BLACK, 4, color);
11074
11224
  }
11075
11225
 
11076
11226
  /** Internal function called when object is clicked
@@ -11153,7 +11303,7 @@ class UIText extends UIObject
11153
11303
 
11154
11304
  // render the text
11155
11305
  const textSize = this.getTextSize();
11156
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow, this.shadowColor, this.shadowBlur, this.shadowOffset);
11306
+ uiSystem.drawText(this.text, this.nativePos, textSize, this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow, this.shadowColor, this.shadowBlur, this.shadowOffset);
11157
11307
  }
11158
11308
  }
11159
11309
 
@@ -11248,7 +11398,7 @@ class UITextInput extends UIObject
11248
11398
  let text = this.text;
11249
11399
  if (this.isKeyInputObject()) // add a cursor to end of text
11250
11400
  text += timeReal%1 < .5 ? '█' : '░';
11251
- uiSystem.drawText(text, this.pos, textSize,
11401
+ uiSystem.drawText(text, this.nativePos, textSize,
11252
11402
  this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
11253
11403
  }
11254
11404
  }
@@ -11291,7 +11441,7 @@ class UITile extends UIObject
11291
11441
  }
11292
11442
  render()
11293
11443
  {
11294
- uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
11444
+ uiSystem.drawTile(this.nativePos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
11295
11445
  }
11296
11446
  }
11297
11447
 
@@ -11330,7 +11480,7 @@ class UIButton extends UIObject
11330
11480
 
11331
11481
  // draw the text scaled to fit
11332
11482
  const textSize = this.getTextSize();
11333
- uiSystem.drawText(this.text, this.pos.add(this.textOffset), textSize,
11483
+ uiSystem.drawText(this.text, this.nativePos.add(this.textOffset), textSize,
11334
11484
  this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
11335
11485
  }
11336
11486
  }
@@ -11378,13 +11528,13 @@ class UICheckbox extends UIObject
11378
11528
  const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
11379
11529
  const length = lerp(1, 2**.5/2, p) / 2;
11380
11530
  let s = this.size.scale(length);
11381
- uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
11382
- uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
11531
+ uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1))), this.nativePos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
11532
+ uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1,1))), this.nativePos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
11383
11533
  }
11384
11534
 
11385
11535
  // draw the text next to the checkbox
11386
11536
  const textSize = this.getTextSize();
11387
- const pos = this.pos.add(vec2(this.size.x,0));
11537
+ const pos = this.nativePos.add(vec2(this.size.x,0));
11388
11538
  uiSystem.drawText(this.text, pos, textSize,
11389
11539
  this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
11390
11540
  }
@@ -11440,7 +11590,7 @@ class UISlider extends UIObject
11440
11590
  const isHorizontal = this.size.x > this.size.y;
11441
11591
  const handleSize = isHorizontal ? this.size.y : this.size.x;
11442
11592
  const barSize = isHorizontal ? this.size.x : this.size.y;
11443
- const centerPos = isHorizontal ? this.pos.x : this.pos.y;
11593
+ const centerPos = isHorizontal ? this.nativePos.x : this.nativePos.y;
11444
11594
 
11445
11595
  // check if value changed
11446
11596
  const handleWidth = barSize - handleSize;
@@ -11474,7 +11624,7 @@ class UISlider extends UIObject
11474
11624
  const minWidth = min(handleWidth, this.cornerRadius * 2);
11475
11625
  const progressWidth = lerp(minWidth, barWidth, this.value);
11476
11626
  const p = (progressWidth - barWidth) * (isHorizontal ? .5 : -.5);
11477
- const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
11627
+ const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
11478
11628
  const color = this.disabled ? this.disabledColor : this.handleColor;
11479
11629
  const drawSize = isHorizontal ?
11480
11630
  vec2(progressWidth, this.size.y) : vec2(this.size.x, progressWidth);
@@ -11485,7 +11635,7 @@ class UISlider extends UIObject
11485
11635
  // draw the slider handle
11486
11636
  const value = clamp(isHorizontal ? this.value : 1 - this.value);
11487
11637
  const p = (barWidth - handleWidth) * (value - .5);
11488
- const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
11638
+ const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
11489
11639
  const color = this.disabled ? this.disabledColor : this.handleColor;
11490
11640
  const drawSize = vec2(handleWidth);
11491
11641
  uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
@@ -11493,7 +11643,7 @@ class UISlider extends UIObject
11493
11643
 
11494
11644
  // draw the text scaled to fit on the slider
11495
11645
  const textSize = this.getTextSize();
11496
- uiSystem.drawText(this.text, this.pos, textSize,
11646
+ uiSystem.drawText(this.text, this.nativePos, textSize,
11497
11647
  this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
11498
11648
  }
11499
11649
  navigatePressed()
@@ -11632,7 +11782,7 @@ class UIVideo extends UIObject
11632
11782
  const context = uiSystem.uiContext;
11633
11783
  const s = this.size;
11634
11784
  context.save();
11635
- context.translate(this.pos.x, this.pos.y);
11785
+ context.translate(this.nativePos.x, this.nativePos.y);
11636
11786
  context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
11637
11787
  context.restore();
11638
11788
  }
@@ -13861,8 +14011,8 @@ async function box2dInit()
13861
14011
  * This function can not apply color because it draws using the 2d context
13862
14012
  * @param {Vector2} pos - Screen space position
13863
14013
  * @param {Vector2} size - Screen space size
13864
- * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
13865
- * @param {number} [borderSize] - Width of the border sections
14014
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample (see drawNineSlice)
14015
+ * @param {number} [borderSize] - Rendered thickness of the border sections
13866
14016
  * @param {number} [extraSpace] - Extra spacing adjustment
13867
14017
  * @param {number} [angle] - Angle to rotate by
13868
14018
  * @memberof DrawUtilities */
@@ -13873,11 +14023,16 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
13873
14023
 
13874
14024
  /** Draw a scalable nine-slice UI element in world space
13875
14025
  * This function can apply color and additive color if WebGL is enabled
14026
+ * The nine-slice samples a 3x3 block of tiles from the tilesheet, it does not
14027
+ * subdivide a single tile. Pass the top-left tile of that block as startTile;
14028
+ * the other 8 tiles (edges, corners, and center) are taken automatically from
14029
+ * the 3x3 grid of tiles extending right and down from it. borderSize only sets
14030
+ * the rendered thickness of the edges and corners, not how the texture is cut.
13876
14031
  * @param {Vector2} pos - World space position
13877
14032
  * @param {Vector2} size - World space size
13878
- * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
14033
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample the nine-slice from
13879
14034
  * @param {Color} [color] - Color to modulate with
13880
- * @param {number} [borderSize] - Width of the border sections
14035
+ * @param {number} [borderSize] - Rendered thickness of the border sections
13881
14036
  * @param {Color} [additiveColor] - Additive color
13882
14037
  * @param {number} [extraSpace] - Extra spacing adjustment
13883
14038
  * @param {number} [angle] - Angle to rotate by
@@ -13887,7 +14042,8 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
13887
14042
  * @memberof DrawUtilities */
13888
14043
  function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
13889
14044
  {
13890
- // setup nine slice tiles
14045
+ // setup nine slice tiles - startTile is the top-left of a 3x3 tile block,
14046
+ // so the center tile is one tile down and right from it
13891
14047
  const centerTile = startTile.offset(startTile.size);
13892
14048
  const centerSize = size.add(vec2(extraSpace-borderSize*2));
13893
14049
  const cornerSize = vec2(borderSize);
@@ -13921,8 +14077,8 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
13921
14077
  * This function can not apply color because it draws using the 2d context
13922
14078
  * @param {Vector2} pos - Screen space position
13923
14079
  * @param {Vector2} size - Screen space size
13924
- * @param {TileInfo} startTile - Starting tile for the three-slice pattern
13925
- * @param {number} [borderSize] - Width of the border sections
14080
+ * @param {TileInfo} startTile - First of 3 consecutive tiles: corner, side, center (see drawThreeSlice)
14081
+ * @param {number} [borderSize] - Rendered thickness of the border sections
13926
14082
  * @param {number} [extraSpace] - Extra spacing adjustment
13927
14083
  * @param {number} [angle] - Angle to rotate by
13928
14084
  * @memberof DrawUtilities */
@@ -13933,11 +14089,15 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
13933
14089
 
13934
14090
  /** Draw a scalable three-slice UI element in world space
13935
14091
  * This function can apply color and additive color if WebGL is enabled
14092
+ * The three-slice samples 3 consecutive tiles from the tilesheet, it does not
14093
+ * subdivide a single tile. Pass the first tile as startTile; the three tiles
14094
+ * are used in order as corner, side, and center, then rotated and mirrored to
14095
+ * build all four edges and corners. borderSize only sets the rendered thickness.
13936
14096
  * @param {Vector2} pos - World space position
13937
14097
  * @param {Vector2} size - World space size
13938
- * @param {TileInfo} startTile - Starting tile for the three-slice pattern
14098
+ * @param {TileInfo} startTile - First of 3 consecutive tiles (corner, side, center) for the three-slice
13939
14099
  * @param {Color} [color] - Color to modulate with
13940
- * @param {number} [borderSize] - Width of the border sections
14100
+ * @param {number} [borderSize] - Rendered thickness of the border sections
13941
14101
  * @param {Color} [additiveColor] - Additive color
13942
14102
  * @param {number} [extraSpace] - Extra spacing adjustment
13943
14103
  * @param {number} [angle] - Angle to rotate by
@@ -13947,7 +14107,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
13947
14107
  * @memberof DrawUtilities */
13948
14108
  function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
13949
14109
  {
13950
- // setup three slice tiles
14110
+ // setup three slice tiles - 3 tiles in a row starting at startTile
13951
14111
  const cornerTile = startTile.frame(0);
13952
14112
  const sideTile = startTile.frame(1);
13953
14113
  const centerTile = startTile.frame(2);
@@ -13995,8 +14155,9 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
13995
14155
  * @memberof DrawUtilities */
13996
14156
  function drawCrescent(pos, size=1, percent=0, color=WHITE, angle=0, invert=false, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
13997
14157
  {
13998
- const points = getCrescentPoints(pos, size, percent, angle, invert);
13999
- drawPoly(points, color, lineWidth, lineColor, vec2(), 0, useWebGL, screenSpace, context);
14158
+ // build local-space points and let drawPoly apply pos/angle so screen space works
14159
+ const points = getCrescentPoints(vec2(), size, percent, 0, invert);
14160
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
14000
14161
  }
14001
14162
 
14002
14163
  /** Get the list of points that make up a crescent / moon-phase shape
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.18.17",
3
+ "version": "1.18.18",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",