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