littlejsengine 1.18.17 → 1.18.19

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