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}
@@ -2233,10 +2233,25 @@ class Color
2233
2233
  * @return {Color} */
2234
2234
  setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
2235
2235
 
2236
+ /** Sets the alpha of this color and returns self
2237
+ * @param {number} [a] - alpha
2238
+ * @return {Color} */
2239
+ setAlpha(a=1)
2240
+ {
2241
+ this.a = a;
2242
+ ASSERT_COLOR_VALID(this);
2243
+ return this;
2244
+ }
2245
+
2236
2246
  /** Returns a new color that is a copy of this
2237
2247
  * @return {Color} */
2238
2248
  copy() { return new Color(this.r, this.g, this.b, this.a); }
2239
2249
 
2250
+ /** Returns a copy of this color with the alpha set
2251
+ * @param {number} [a] - alpha
2252
+ * @return {Color} */
2253
+ withAlpha(a=1) { return new Color(this.r, this.g, this.b, a); }
2254
+
2240
2255
  /** Returns a copy of this color plus the color passed in
2241
2256
  * @param {Color} c - other color
2242
2257
  * @return {Color} */
@@ -2831,6 +2846,8 @@ let canvasFixedSize = vec2();
2831
2846
  let canvasPixelated = false;
2832
2847
 
2833
2848
  /** Disables texture filtering for crisper pixel art
2849
+ * - Leave true for pixel art so sprites stay sharp when scaled (uses NEAREST filtering)
2850
+ * - Set false for smooth/high-resolution art to enable bilinear filtering and mipmaps
2834
2851
  * @type {boolean}
2835
2852
  * @default
2836
2853
  * @memberof Settings */
@@ -3184,6 +3201,7 @@ function setCanvasPixelated(pixelated)
3184
3201
  }
3185
3202
 
3186
3203
  /** Disables texture filtering for crisper pixel art
3204
+ * - Leave true for pixel art; set false for smooth/high-resolution art
3187
3205
  * @param {boolean} pixelated
3188
3206
  * @memberof Settings */
3189
3207
  function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
@@ -5075,6 +5093,62 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
5075
5093
  * @memberof Draw */
5076
5094
  function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
5077
5095
 
5096
+ /** Fit the camera to a rectangle in world space by setting cameraPos and cameraScale
5097
+ * - worldMargin pads the content rectangle in world units, so the gap scales with the content on resize
5098
+ * - screenInset reserves space in screen pixels on each viewport edge (for example a HUD band) and
5099
+ * re-centers the content away from that edge, so the reserved band stays a fixed pixel size on resize
5100
+ * - worldMargin and screenInset may each be a number for all sides, a Vector2 (x=left/right, y=top/bottom),
5101
+ * or an object with any of {top, right, bottom, left}
5102
+ * @param {Vector2} center - Center of the rectangle in world space
5103
+ * @param {Vector2} size - Size of the rectangle in world space
5104
+ * @param {number|Vector2|Object} [worldMargin] - World space padding added around the content rectangle
5105
+ * @param {number|Vector2|Object} [screenInset] - Screen space padding in pixels reserved on each viewport edge
5106
+ * @return {number} - The new camera scale
5107
+ * @memberof Draw */
5108
+ function cameraFit(center, size, worldMargin, screenInset)
5109
+ {
5110
+ ASSERT(isVector2(center), 'center must be a vec2');
5111
+ ASSERT(isVector2(size), 'size must be a vec2');
5112
+
5113
+ // pad the content
5114
+ const margin = padSides(worldMargin);
5115
+ const inset = padSides(screenInset);
5116
+ const worldW = size.x + margin.left + margin.right;
5117
+ const worldH = size.y + margin.top + margin.bottom;
5118
+ const viewW = mainCanvasSize.x - inset.left - inset.right;
5119
+ const viewH = mainCanvasSize.y - inset.top - inset.bottom;
5120
+
5121
+ // bail on a degenerate rect or viewport rather than NaN the camera
5122
+ if (!(worldW > 0 && worldH > 0 && viewW > 0 && viewH > 0))
5123
+ return cameraScale;
5124
+
5125
+ // scale to fit the padded content
5126
+ cameraScale = min(viewW / worldW, viewH / worldH);
5127
+
5128
+ // calculate offset vectors
5129
+ const marginVector = vec2(margin.right - margin.left, margin.top - margin.bottom).scale(.5);
5130
+ const insetVector = vec2(inset.right - inset.left, inset.top - inset.bottom).scale(.5 / cameraScale);
5131
+
5132
+ // apply the offsets and return camera scale
5133
+ cameraPos = center.add(marginVector).add(insetVector);
5134
+ return cameraScale;
5135
+
5136
+ function padSides(p)
5137
+ {
5138
+ // normalize a padding option to {top, right, bottom, left}
5139
+ if (p === undefined || isNumber(p))
5140
+ p = vec2(p);
5141
+ if (isVector2(p))
5142
+ return { top: p.y, right: p.x, bottom: p.y, left: p.x };
5143
+ return {
5144
+ top: p.top || 0,
5145
+ right: p.right || 0,
5146
+ bottom: p.bottom || 0,
5147
+ left: p.left || 0,
5148
+ };
5149
+ }
5150
+ }
5151
+
5078
5152
  /** Check if a box, point, or circle is on screen with a circle test
5079
5153
  * If size is a Vector2, uses the length as diameter
5080
5154
  * This can be used to cull offscreen objects from render or update
@@ -5113,14 +5187,13 @@ function isOnScreen(pos, size=0)
5113
5187
  y + size > -h && y - size < h;
5114
5188
  }
5115
5189
 
5116
- /** Enable normal or additive blend mode
5190
+ /** Enable additive blending
5117
5191
  * @param {boolean} [additive]
5118
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
5119
5192
  * @memberof Draw */
5120
- function setBlendMode(additive=false, context=drawContext)
5193
+ function setAdditiveBlendMode(additive=true)
5121
5194
  {
5122
5195
  glAdditive = additive;
5123
- context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
5196
+ drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
5124
5197
  }
5125
5198
 
5126
5199
  /** Combines LittleJS canvases onto the main canvas
@@ -5446,11 +5519,30 @@ let mouseWheel = 0;
5446
5519
  * @memberof Input */
5447
5520
  let mouseInWindow = true;
5448
5521
 
5449
- /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
5522
+ /** True if a gamepad is the most recently used input device.
5523
+ * Equivalent to usingGamepadInput(); derived from lastInputDevice each frame.
5450
5524
  * @type {boolean}
5451
5525
  * @memberof Input */
5452
5526
  let isUsingGamepad = false;
5453
5527
 
5528
+ /** The most recently used input device: 'mouse' | 'keyboard' | 'gamepad'.
5529
+ * Sticky: it holds its value while every device is idle, so a mouse-follow
5530
+ * control (e.g. paddle = mousePos) won't snap back the instant the stick/keys
5531
+ * are released. With several devices in play at once (e.g. keyboard to move +
5532
+ * mouse to aim) it tracks whichever was touched last each frame, so it may
5533
+ * alternate — that's intended; use it to pick which control drives a shared
5534
+ * action. Updated every frame by inputUpdate().
5535
+ * @type {string}
5536
+ * @memberof Input */
5537
+ let lastInputDevice = 'mouse';
5538
+
5539
+ /** Screen-pixel mouse movement per frame that counts as "using the mouse"
5540
+ * (so sub-pixel hand jitter doesn't steal focus from the keyboard/gamepad).
5541
+ * @type {number}
5542
+ * @default
5543
+ * @memberof Input */
5544
+ let inputMouseMoveThreshold = 6;
5545
+
5454
5546
  /** Prevents input continuing to the default browser handling (true by default)
5455
5547
  * @type {boolean}
5456
5548
  * @memberof Input */
@@ -5471,6 +5563,18 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
5471
5563
  * @memberof Input */
5472
5564
  function setInputPreventDefault(preventDefault=true) { inputPreventDefault = preventDefault; }
5473
5565
 
5566
+ /** Set the screen-pixel mouse movement per frame that counts as using the mouse
5567
+ * @param {number} threshold
5568
+ * @memberof Input */
5569
+ function setInputMouseMoveThreshold(threshold) { inputMouseMoveThreshold = threshold; }
5570
+
5571
+ /** @return {boolean} - Is the mouse the most recently used input device? @memberof Input */
5572
+ function usingMouseInput() { return lastInputDevice === 'mouse'; }
5573
+ /** @return {boolean} - Is the keyboard the most recently used input device? @memberof Input */
5574
+ function usingKeyboardInput() { return lastInputDevice === 'keyboard'; }
5575
+ /** @return {boolean} - Is a gamepad the most recently used input device? @memberof Input */
5576
+ function usingGamepadInput() { return lastInputDevice === 'gamepad'; }
5577
+
5474
5578
  /** Clears an input key state
5475
5579
  * @param {string|number} key
5476
5580
  * @param {number} [device]
@@ -5771,7 +5875,6 @@ function inputInit()
5771
5875
  {
5772
5876
  if (!e.repeat)
5773
5877
  {
5774
- isUsingGamepad = false;
5775
5878
  inputData[0][e.code] = 3;
5776
5879
  if (inputWASDEmulateDirection)
5777
5880
  inputData[0][remapKey(e.code)] = 3;
@@ -5830,7 +5933,6 @@ function inputInit()
5830
5933
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
5831
5934
  audioContext.resume();
5832
5935
 
5833
- isUsingGamepad = false;
5834
5936
  inputData[0][e.button] = 3;
5835
5937
 
5836
5938
  const mousePosScreenLast = mousePosScreen;
@@ -5921,10 +6023,7 @@ function inputInit()
5921
6023
  if (wasTouching)
5922
6024
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
5923
6025
  else
5924
- {
5925
6026
  inputData[0][button] = 3;
5926
- isUsingGamepad = false; // a passthrough tap is mouse-style input
5927
- }
5928
6027
  }
5929
6028
  else if (wasTouching)
5930
6029
  inputData[0][button] = inputData[0][button] & 2 | 4;
@@ -5970,7 +6069,43 @@ function inputUpdate()
5970
6069
 
5971
6070
  // update gamepads if enabled
5972
6071
  gamepadsUpdate();
5973
-
6072
+
6073
+ // update most recently used input device
6074
+ updateLastInputDevice();
6075
+
6076
+ function updateLastInputDevice()
6077
+ {
6078
+ // mouse: any button held or moved
6079
+ const mouseActive = mouseIsDown(0) || mouseIsDown(1) || mouseIsDown(2) || mouseDeltaScreen.length() > inputMouseMoveThreshold;
6080
+
6081
+ // gamepad: any button held or stick moved
6082
+ let gamepadActive = false;
6083
+ for (let s = gamepadStickCount(); s-- && !gamepadActive;)
6084
+ gamepadActive = gamepadStick(s).lengthSquared() > .04;
6085
+ for (let b = 17; b-- && !gamepadActive;)
6086
+ gamepadActive = gamepadIsDown(b);
6087
+
6088
+ // keyboard: any non-mouse key down
6089
+ let keyboardActive = false;
6090
+ for (const k in inputData[0])
6091
+ if (isNaN(+k) && (inputData[0][k] & 1))
6092
+ {
6093
+ keyboardActive = true;
6094
+ break;
6095
+ }
6096
+
6097
+ // update the last input
6098
+ if (gamepadActive)
6099
+ lastInputDevice = 'gamepad';
6100
+ else if (mouseActive)
6101
+ lastInputDevice = 'mouse';
6102
+ else if (keyboardActive)
6103
+ lastInputDevice = 'keyboard';
6104
+
6105
+ // set flag if gamepad is last device
6106
+ isUsingGamepad = lastInputDevice === 'gamepad';
6107
+ }
6108
+
5974
6109
  // gamepads are updated by engine every frame automatically
5975
6110
  function gamepadsUpdate()
5976
6111
  {
@@ -6093,7 +6228,6 @@ function inputUpdate()
6093
6228
  gamepadHadInput[i] = true;
6094
6229
  if (!gamepadHadInput[gamepadPrimary])
6095
6230
  gamepadPrimary = i;
6096
- isUsingGamepad ||= (gamepadPrimary === i);
6097
6231
  }
6098
6232
 
6099
6233
  if (gamepad.mapping === 'standard')
@@ -6240,12 +6374,19 @@ function touchGamepadRelayout()
6240
6374
  const setZone = (z, css)=> z.style.cssText =
6241
6375
  'position:absolute;pointer-events:auto;touch-action:none;' + css;
6242
6376
 
6243
- if (paused && touchGamepadCenterButtonSize)
6377
+ if (paused)
6244
6378
  {
6245
- // while paused, any touch presses start
6246
- setZone(touchGamepadZoneC, 'inset:0');
6379
+ // the gamepad is hidden while paused, so its side zones must not capture
6380
+ // touches - otherwise they silently steal taps from menus and dialogs
6247
6381
  for (const zone of touchGamepadSideZones) zone.style.display = 'none';
6248
- touchGamepadZoneC.style.display = '';
6382
+ if (touchGamepadCenterButtonSize)
6383
+ {
6384
+ // any touch presses start
6385
+ setZone(touchGamepadZoneC, 'inset:0');
6386
+ touchGamepadZoneC.style.display = '';
6387
+ }
6388
+ else
6389
+ touchGamepadZoneC.style.display = 'none';
6249
6390
  }
6250
6391
  else
6251
6392
  {
@@ -6521,7 +6662,6 @@ function touchGamepadPointerDown(e, zone)
6521
6662
  e.preventDefault();
6522
6663
  zone.setPointerCapture(e.pointerId);
6523
6664
  touchGamepadTimer.set();
6524
- isUsingGamepad = true;
6525
6665
 
6526
6666
  // resume audio on first interaction
6527
6667
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
@@ -8537,9 +8677,6 @@ class Particle
8537
8677
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
8538
8678
  this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
8539
8679
 
8540
- // draw the particle
8541
- additive && setBlendMode(true);
8542
-
8543
8680
  // update the position and angle for drawing
8544
8681
  const pos = particleDrawPos.set(this.pos.x, this.pos.y);
8545
8682
  let angle = this.angle;
@@ -8552,6 +8689,9 @@ class Particle
8552
8689
  emitter.pos.y + pos.x*s + pos.y*c);
8553
8690
  angle += a;
8554
8691
  }
8692
+
8693
+ // draw the particle
8694
+ additive && setAdditiveBlendMode();
8555
8695
  if (trailScale)
8556
8696
  {
8557
8697
  // trail style particles
@@ -8569,7 +8709,7 @@ class Particle
8569
8709
  }
8570
8710
  else
8571
8711
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
8572
- additive && setBlendMode();
8712
+ additive && setAdditiveBlendMode(false);
8573
8713
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
8574
8714
  }
8575
8715
  }
@@ -10479,7 +10619,7 @@ class LightSystemPlugin
10479
10619
 
10480
10620
  // 3. walk engineObjects calling renderLight() — additive blend
10481
10621
  // (lightmap accumulates raw additive color contributions)
10482
- setBlendMode(true);
10622
+ setAdditiveBlendMode();
10483
10623
  glContext.enable(glContext.BLEND);
10484
10624
  glContext.blendFunc(glContext.ONE, glContext.ONE);
10485
10625
 
@@ -10513,7 +10653,7 @@ class LightSystemPlugin
10513
10653
  // is, and any debug text / future draw could sample the lightmap)
10514
10654
  if (glActiveTexture)
10515
10655
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
10516
- setBlendMode(prevAdditive);
10656
+ setAdditiveBlendMode(prevAdditive);
10517
10657
  glSetInstancedMode(true);
10518
10658
  }
10519
10659
  function lightSystemContextLost()
@@ -10922,7 +11062,7 @@ class UISystemPlugin
10922
11062
  let targetPos, targetSize;
10923
11063
  if (o.parent)
10924
11064
  {
10925
- targetPos = o.parent.pos;
11065
+ targetPos = o.parent.nativePos;
10926
11066
  targetSize = o.parent.size;
10927
11067
  }
10928
11068
  else
@@ -10936,7 +11076,7 @@ class UISystemPlugin
10936
11076
  }
10937
11077
 
10938
11078
  const a = o.anchor;
10939
- o.pos = targetPos
11079
+ o.nativePos = targetPos
10940
11080
  .add(targetSize.multiply(a).scale(.5)) // anchor point on target
10941
11081
  .subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
10942
11082
  .add(o.localPos); // user offset
@@ -11042,13 +11182,18 @@ class UISystemPlugin
11042
11182
 
11043
11183
  function updateObject(o)
11044
11184
  {
11045
- if (!o.visible) return;
11185
+ if (o.destroyed || !o.visible) return;
11046
11186
 
11047
11187
  // update in reverse order to detect mouse enter/leave
11048
11188
  updateTransforms(o);
11049
11189
  for (let i=o.children.length; i--;)
11050
- updateObject(o.children[i]);
11051
- o.update();
11190
+ {
11191
+ // a child may destroy siblings mid-update (e.g. dialog close)
11192
+ const child = o.children[i];
11193
+ child && updateObject(child);
11194
+ }
11195
+ if (!o.destroyed)
11196
+ o.update();
11052
11197
  }
11053
11198
  }
11054
11199
  function uiRender()
@@ -11472,10 +11617,15 @@ class UIObject
11472
11617
  ASSERT(isVector2(pos), 'ui object pos must be a vec2');
11473
11618
  ASSERT(isVector2(size), 'ui object size must be a vec2');
11474
11619
 
11475
- /** @property {Vector2} - Local position of the object */
11620
+ /** @property {Vector2} - Position you set: an offset from this object's
11621
+ * anchor point (the parent box, or the canvas for roots). This is the
11622
+ * input that controls placement — set this, not nativePos. */
11476
11623
  this.localPos = pos.copy();
11477
- /** @property {Vector2} - Screen space position of the object */
11478
- this.pos = pos.copy();
11624
+ /** @property {Vector2} - Resolved position in native UI space, recomputed
11625
+ * every frame from localPos + anchor (and nativeHeight, if set). This is a
11626
+ * derived output used for drawing and hit-testing; assigning to it has no
11627
+ * effect since it is overwritten each frame. Set localPos instead. */
11628
+ this.nativePos = pos.copy();
11479
11629
  /** @property {Vector2} - Screen space size of the object */
11480
11630
  this.size = size.copy();
11481
11631
  /** @property {Color} - Color of the object */
@@ -11610,7 +11760,7 @@ class UIObject
11610
11760
  const size = !isTouchDevice ? this.size :
11611
11761
  this.size.add(vec2(this.extraTouchSize || 0));
11612
11762
  const pos = uiSystem.screenToNative(mousePosScreen);
11613
- return isOverlapping(this.pos, size, pos);
11763
+ return isOverlapping(this.nativePos, size, pos);
11614
11764
  }
11615
11765
 
11616
11766
  /** Update the object, called automatically by plugin once each frame */
@@ -11700,7 +11850,7 @@ class UIObject
11700
11850
  this.color : this.color;
11701
11851
  const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
11702
11852
 
11703
- uiSystem.drawRect(this.pos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
11853
+ uiSystem.drawRect(this.nativePos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
11704
11854
  }
11705
11855
 
11706
11856
  /** Get the size for text with overrides and scale
@@ -11737,8 +11887,8 @@ class UIObject
11737
11887
  let text = 'type = ' + this.constructor.name;
11738
11888
  if (this.text)
11739
11889
  text += '\ntext = ' + this.text;
11740
- if (this.pos.x || this.pos.y)
11741
- text += '\npos = ' + this.pos;
11890
+ if (this.nativePos.x || this.nativePos.y)
11891
+ text += '\nnativePos = ' + this.nativePos;
11742
11892
  if (this.localPos.x || this.localPos.y)
11743
11893
  text += '\nlocalPos = ' + this.localPos;
11744
11894
  if (this.size.x || this.size.y)
@@ -11758,7 +11908,7 @@ class UIObject
11758
11908
  this.isHoverObject() ? YELLOW :
11759
11909
  this.disabled ? PURPLE :
11760
11910
  this.interactive ? RED : BLUE;
11761
- uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
11911
+ uiSystem.drawRect(this.nativePos, this.size, CLEAR_BLACK, 4, color);
11762
11912
  }
11763
11913
 
11764
11914
  /** Internal function called when object is clicked
@@ -11841,7 +11991,7 @@ class UIText extends UIObject
11841
11991
 
11842
11992
  // render the text
11843
11993
  const textSize = this.getTextSize();
11844
- 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);
11994
+ 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);
11845
11995
  }
11846
11996
  }
11847
11997
 
@@ -11936,7 +12086,7 @@ class UITextInput extends UIObject
11936
12086
  let text = this.text;
11937
12087
  if (this.isKeyInputObject()) // add a cursor to end of text
11938
12088
  text += timeReal%1 < .5 ? '█' : '░';
11939
- uiSystem.drawText(text, this.pos, textSize,
12089
+ uiSystem.drawText(text, this.nativePos, textSize,
11940
12090
  this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
11941
12091
  }
11942
12092
  }
@@ -11979,7 +12129,7 @@ class UITile extends UIObject
11979
12129
  }
11980
12130
  render()
11981
12131
  {
11982
- uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
12132
+ uiSystem.drawTile(this.nativePos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
11983
12133
  }
11984
12134
  }
11985
12135
 
@@ -12018,7 +12168,7 @@ class UIButton extends UIObject
12018
12168
 
12019
12169
  // draw the text scaled to fit
12020
12170
  const textSize = this.getTextSize();
12021
- uiSystem.drawText(this.text, this.pos.add(this.textOffset), textSize,
12171
+ uiSystem.drawText(this.text, this.nativePos.add(this.textOffset), textSize,
12022
12172
  this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
12023
12173
  }
12024
12174
  }
@@ -12066,13 +12216,13 @@ class UICheckbox extends UIObject
12066
12216
  const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
12067
12217
  const length = lerp(1, 2**.5/2, p) / 2;
12068
12218
  let s = this.size.scale(length);
12069
- uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
12070
- uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
12219
+ uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1))), this.nativePos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
12220
+ uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1,1))), this.nativePos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
12071
12221
  }
12072
12222
 
12073
12223
  // draw the text next to the checkbox
12074
12224
  const textSize = this.getTextSize();
12075
- const pos = this.pos.add(vec2(this.size.x,0));
12225
+ const pos = this.nativePos.add(vec2(this.size.x,0));
12076
12226
  uiSystem.drawText(this.text, pos, textSize,
12077
12227
  this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
12078
12228
  }
@@ -12128,7 +12278,7 @@ class UISlider extends UIObject
12128
12278
  const isHorizontal = this.size.x > this.size.y;
12129
12279
  const handleSize = isHorizontal ? this.size.y : this.size.x;
12130
12280
  const barSize = isHorizontal ? this.size.x : this.size.y;
12131
- const centerPos = isHorizontal ? this.pos.x : this.pos.y;
12281
+ const centerPos = isHorizontal ? this.nativePos.x : this.nativePos.y;
12132
12282
 
12133
12283
  // check if value changed
12134
12284
  const handleWidth = barSize - handleSize;
@@ -12162,7 +12312,7 @@ class UISlider extends UIObject
12162
12312
  const minWidth = min(handleWidth, this.cornerRadius * 2);
12163
12313
  const progressWidth = lerp(minWidth, barWidth, this.value);
12164
12314
  const p = (progressWidth - barWidth) * (isHorizontal ? .5 : -.5);
12165
- const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
12315
+ const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
12166
12316
  const color = this.disabled ? this.disabledColor : this.handleColor;
12167
12317
  const drawSize = isHorizontal ?
12168
12318
  vec2(progressWidth, this.size.y) : vec2(this.size.x, progressWidth);
@@ -12173,7 +12323,7 @@ class UISlider extends UIObject
12173
12323
  // draw the slider handle
12174
12324
  const value = clamp(isHorizontal ? this.value : 1 - this.value);
12175
12325
  const p = (barWidth - handleWidth) * (value - .5);
12176
- const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
12326
+ const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
12177
12327
  const color = this.disabled ? this.disabledColor : this.handleColor;
12178
12328
  const drawSize = vec2(handleWidth);
12179
12329
  uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
@@ -12181,7 +12331,7 @@ class UISlider extends UIObject
12181
12331
 
12182
12332
  // draw the text scaled to fit on the slider
12183
12333
  const textSize = this.getTextSize();
12184
- uiSystem.drawText(this.text, this.pos, textSize,
12334
+ uiSystem.drawText(this.text, this.nativePos, textSize,
12185
12335
  this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
12186
12336
  }
12187
12337
  navigatePressed()
@@ -12320,7 +12470,7 @@ class UIVideo extends UIObject
12320
12470
  const context = uiSystem.uiContext;
12321
12471
  const s = this.size;
12322
12472
  context.save();
12323
- context.translate(this.pos.x, this.pos.y);
12473
+ context.translate(this.nativePos.x, this.nativePos.y);
12324
12474
  context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
12325
12475
  context.restore();
12326
12476
  }
@@ -14549,8 +14699,8 @@ async function box2dInit()
14549
14699
  * This function can not apply color because it draws using the 2d context
14550
14700
  * @param {Vector2} pos - Screen space position
14551
14701
  * @param {Vector2} size - Screen space size
14552
- * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
14553
- * @param {number} [borderSize] - Width of the border sections
14702
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample (see drawNineSlice)
14703
+ * @param {number} [borderSize] - Rendered thickness of the border sections
14554
14704
  * @param {number} [extraSpace] - Extra spacing adjustment
14555
14705
  * @param {number} [angle] - Angle to rotate by
14556
14706
  * @memberof DrawUtilities */
@@ -14561,11 +14711,16 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
14561
14711
 
14562
14712
  /** Draw a scalable nine-slice UI element in world space
14563
14713
  * This function can apply color and additive color if WebGL is enabled
14714
+ * The nine-slice samples a 3x3 block of tiles from the tilesheet, it does not
14715
+ * subdivide a single tile. Pass the top-left tile of that block as startTile;
14716
+ * the other 8 tiles (edges, corners, and center) are taken automatically from
14717
+ * the 3x3 grid of tiles extending right and down from it. borderSize only sets
14718
+ * the rendered thickness of the edges and corners, not how the texture is cut.
14564
14719
  * @param {Vector2} pos - World space position
14565
14720
  * @param {Vector2} size - World space size
14566
- * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
14721
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample the nine-slice from
14567
14722
  * @param {Color} [color] - Color to modulate with
14568
- * @param {number} [borderSize] - Width of the border sections
14723
+ * @param {number} [borderSize] - Rendered thickness of the border sections
14569
14724
  * @param {Color} [additiveColor] - Additive color
14570
14725
  * @param {number} [extraSpace] - Extra spacing adjustment
14571
14726
  * @param {number} [angle] - Angle to rotate by
@@ -14575,7 +14730,8 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
14575
14730
  * @memberof DrawUtilities */
14576
14731
  function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
14577
14732
  {
14578
- // setup nine slice tiles
14733
+ // setup nine slice tiles - startTile is the top-left of a 3x3 tile block,
14734
+ // so the center tile is one tile down and right from it
14579
14735
  const centerTile = startTile.offset(startTile.size);
14580
14736
  const centerSize = size.add(vec2(extraSpace-borderSize*2));
14581
14737
  const cornerSize = vec2(borderSize);
@@ -14609,8 +14765,8 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
14609
14765
  * This function can not apply color because it draws using the 2d context
14610
14766
  * @param {Vector2} pos - Screen space position
14611
14767
  * @param {Vector2} size - Screen space size
14612
- * @param {TileInfo} startTile - Starting tile for the three-slice pattern
14613
- * @param {number} [borderSize] - Width of the border sections
14768
+ * @param {TileInfo} startTile - First of 3 consecutive tiles: corner, side, center (see drawThreeSlice)
14769
+ * @param {number} [borderSize] - Rendered thickness of the border sections
14614
14770
  * @param {number} [extraSpace] - Extra spacing adjustment
14615
14771
  * @param {number} [angle] - Angle to rotate by
14616
14772
  * @memberof DrawUtilities */
@@ -14621,11 +14777,15 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
14621
14777
 
14622
14778
  /** Draw a scalable three-slice UI element in world space
14623
14779
  * This function can apply color and additive color if WebGL is enabled
14780
+ * The three-slice samples 3 consecutive tiles from the tilesheet, it does not
14781
+ * subdivide a single tile. Pass the first tile as startTile; the three tiles
14782
+ * are used in order as corner, side, and center, then rotated and mirrored to
14783
+ * build all four edges and corners. borderSize only sets the rendered thickness.
14624
14784
  * @param {Vector2} pos - World space position
14625
14785
  * @param {Vector2} size - World space size
14626
- * @param {TileInfo} startTile - Starting tile for the three-slice pattern
14786
+ * @param {TileInfo} startTile - First of 3 consecutive tiles (corner, side, center) for the three-slice
14627
14787
  * @param {Color} [color] - Color to modulate with
14628
- * @param {number} [borderSize] - Width of the border sections
14788
+ * @param {number} [borderSize] - Rendered thickness of the border sections
14629
14789
  * @param {Color} [additiveColor] - Additive color
14630
14790
  * @param {number} [extraSpace] - Extra spacing adjustment
14631
14791
  * @param {number} [angle] - Angle to rotate by
@@ -14635,7 +14795,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
14635
14795
  * @memberof DrawUtilities */
14636
14796
  function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
14637
14797
  {
14638
- // setup three slice tiles
14798
+ // setup three slice tiles - 3 tiles in a row starting at startTile
14639
14799
  const cornerTile = startTile.frame(0);
14640
14800
  const sideTile = startTile.frame(1);
14641
14801
  const centerTile = startTile.frame(2);
@@ -14683,8 +14843,9 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
14683
14843
  * @memberof DrawUtilities */
14684
14844
  function drawCrescent(pos, size=1, percent=0, color=WHITE, angle=0, invert=false, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
14685
14845
  {
14686
- const points = getCrescentPoints(pos, size, percent, angle, invert);
14687
- drawPoly(points, color, lineWidth, lineColor, vec2(), 0, useWebGL, screenSpace, context);
14846
+ // build local-space points and let drawPoly apply pos/angle so screen space works
14847
+ const points = getCrescentPoints(vec2(), size, percent, 0, invert);
14848
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
14688
14849
  }
14689
14850
 
14690
14851
  /** Get the list of points that make up a crescent / moon-phase shape
@@ -16317,7 +16478,7 @@ export
16317
16478
  drawCanvas2D,
16318
16479
  drawText,
16319
16480
  drawTextScreen,
16320
- setBlendMode,
16481
+ setAdditiveBlendMode,
16321
16482
  combineCanvases,
16322
16483
  engineImageFont,
16323
16484
  ImageFont,
@@ -16325,6 +16486,7 @@ export
16325
16486
  toggleFullscreen,
16326
16487
  setCursor,
16327
16488
  getCameraSize,
16489
+ cameraFit,
16328
16490
  isOnScreen,
16329
16491
 
16330
16492
  // WebGL
@@ -16368,10 +16530,16 @@ export
16368
16530
  mouseWheel,
16369
16531
  mouseInWindow,
16370
16532
  isUsingGamepad,
16533
+ lastInputDevice,
16534
+ inputMouseMoveThreshold,
16371
16535
  inputPreventDefault,
16372
16536
  gamepadPrimary,
16373
16537
  isTouchDevice,
16374
16538
  setInputPreventDefault,
16539
+ setInputMouseMoveThreshold,
16540
+ usingMouseInput,
16541
+ usingKeyboardInput,
16542
+ usingGamepadInput,
16375
16543
  gamepadIsDown,
16376
16544
  gamepadWasPressed,
16377
16545
  gamepadWasReleased,