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.
@@ -13,8 +13,8 @@
13
13
  * This function can not apply color because it draws using the 2d context
14
14
  * @param {Vector2} pos - Screen space position
15
15
  * @param {Vector2} size - Screen space size
16
- * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
17
- * @param {number} [borderSize] - Width of the border sections
16
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample (see drawNineSlice)
17
+ * @param {number} [borderSize] - Rendered thickness of the border sections
18
18
  * @param {number} [extraSpace] - Extra spacing adjustment
19
19
  * @param {number} [angle] - Angle to rotate by
20
20
  * @memberof DrawUtilities */
@@ -25,11 +25,16 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
25
25
 
26
26
  /** Draw a scalable nine-slice UI element in world space
27
27
  * This function can apply color and additive color if WebGL is enabled
28
+ * The nine-slice samples a 3x3 block of tiles from the tilesheet, it does not
29
+ * subdivide a single tile. Pass the top-left tile of that block as startTile;
30
+ * the other 8 tiles (edges, corners, and center) are taken automatically from
31
+ * the 3x3 grid of tiles extending right and down from it. borderSize only sets
32
+ * the rendered thickness of the edges and corners, not how the texture is cut.
28
33
  * @param {Vector2} pos - World space position
29
34
  * @param {Vector2} size - World space size
30
- * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
35
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample the nine-slice from
31
36
  * @param {Color} [color] - Color to modulate with
32
- * @param {number} [borderSize] - Width of the border sections
37
+ * @param {number} [borderSize] - Rendered thickness of the border sections
33
38
  * @param {Color} [additiveColor] - Additive color
34
39
  * @param {number} [extraSpace] - Extra spacing adjustment
35
40
  * @param {number} [angle] - Angle to rotate by
@@ -39,7 +44,8 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
39
44
  * @memberof DrawUtilities */
40
45
  function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
41
46
  {
42
- // setup nine slice tiles
47
+ // setup nine slice tiles - startTile is the top-left of a 3x3 tile block,
48
+ // so the center tile is one tile down and right from it
43
49
  const centerTile = startTile.offset(startTile.size);
44
50
  const centerSize = size.add(vec2(extraSpace-borderSize*2));
45
51
  const cornerSize = vec2(borderSize);
@@ -73,8 +79,8 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
73
79
  * This function can not apply color because it draws using the 2d context
74
80
  * @param {Vector2} pos - Screen space position
75
81
  * @param {Vector2} size - Screen space size
76
- * @param {TileInfo} startTile - Starting tile for the three-slice pattern
77
- * @param {number} [borderSize] - Width of the border sections
82
+ * @param {TileInfo} startTile - First of 3 consecutive tiles: corner, side, center (see drawThreeSlice)
83
+ * @param {number} [borderSize] - Rendered thickness of the border sections
78
84
  * @param {number} [extraSpace] - Extra spacing adjustment
79
85
  * @param {number} [angle] - Angle to rotate by
80
86
  * @memberof DrawUtilities */
@@ -85,11 +91,15 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
85
91
 
86
92
  /** Draw a scalable three-slice UI element in world space
87
93
  * This function can apply color and additive color if WebGL is enabled
94
+ * The three-slice samples 3 consecutive tiles from the tilesheet, it does not
95
+ * subdivide a single tile. Pass the first tile as startTile; the three tiles
96
+ * are used in order as corner, side, and center, then rotated and mirrored to
97
+ * build all four edges and corners. borderSize only sets the rendered thickness.
88
98
  * @param {Vector2} pos - World space position
89
99
  * @param {Vector2} size - World space size
90
- * @param {TileInfo} startTile - Starting tile for the three-slice pattern
100
+ * @param {TileInfo} startTile - First of 3 consecutive tiles (corner, side, center) for the three-slice
91
101
  * @param {Color} [color] - Color to modulate with
92
- * @param {number} [borderSize] - Width of the border sections
102
+ * @param {number} [borderSize] - Rendered thickness of the border sections
93
103
  * @param {Color} [additiveColor] - Additive color
94
104
  * @param {number} [extraSpace] - Extra spacing adjustment
95
105
  * @param {number} [angle] - Angle to rotate by
@@ -99,7 +109,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
99
109
  * @memberof DrawUtilities */
100
110
  function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
101
111
  {
102
- // setup three slice tiles
112
+ // setup three slice tiles - 3 tiles in a row starting at startTile
103
113
  const cornerTile = startTile.frame(0);
104
114
  const sideTile = startTile.frame(1);
105
115
  const centerTile = startTile.frame(2);
@@ -147,8 +157,9 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
147
157
  * @memberof DrawUtilities */
148
158
  function drawCrescent(pos, size=1, percent=0, color=WHITE, angle=0, invert=false, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
149
159
  {
150
- const points = getCrescentPoints(pos, size, percent, angle, invert);
151
- drawPoly(points, color, lineWidth, lineColor, vec2(), 0, useWebGL, screenSpace, context);
160
+ // build local-space points and let drawPoly apply pos/angle so screen space works
161
+ const points = getCrescentPoints(vec2(), size, percent, 0, invert);
162
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
152
163
  }
153
164
 
154
165
  /** Get the list of points that make up a crescent / moon-phase shape
@@ -183,7 +183,7 @@ class LightSystemPlugin
183
183
 
184
184
  // 3. walk engineObjects calling renderLight() — additive blend
185
185
  // (lightmap accumulates raw additive color contributions)
186
- setBlendMode(true);
186
+ setAdditiveBlendMode();
187
187
  glContext.enable(glContext.BLEND);
188
188
  glContext.blendFunc(glContext.ONE, glContext.ONE);
189
189
 
@@ -217,7 +217,7 @@ class LightSystemPlugin
217
217
  // is, and any debug text / future draw could sample the lightmap)
218
218
  if (glActiveTexture)
219
219
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
220
- setBlendMode(prevAdditive);
220
+ setAdditiveBlendMode(prevAdditive);
221
221
  glSetInstancedMode(true);
222
222
  }
223
223
  function lightSystemContextLost()
@@ -132,7 +132,7 @@ class UISystemPlugin
132
132
  let targetPos, targetSize;
133
133
  if (o.parent)
134
134
  {
135
- targetPos = o.parent.pos;
135
+ targetPos = o.parent.nativePos;
136
136
  targetSize = o.parent.size;
137
137
  }
138
138
  else
@@ -146,7 +146,7 @@ class UISystemPlugin
146
146
  }
147
147
 
148
148
  const a = o.anchor;
149
- o.pos = targetPos
149
+ o.nativePos = targetPos
150
150
  .add(targetSize.multiply(a).scale(.5)) // anchor point on target
151
151
  .subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
152
152
  .add(o.localPos); // user offset
@@ -252,13 +252,18 @@ class UISystemPlugin
252
252
 
253
253
  function updateObject(o)
254
254
  {
255
- if (!o.visible) return;
255
+ if (o.destroyed || !o.visible) return;
256
256
 
257
257
  // update in reverse order to detect mouse enter/leave
258
258
  updateTransforms(o);
259
259
  for (let i=o.children.length; i--;)
260
- updateObject(o.children[i]);
261
- o.update();
260
+ {
261
+ // a child may destroy siblings mid-update (e.g. dialog close)
262
+ const child = o.children[i];
263
+ child && updateObject(child);
264
+ }
265
+ if (!o.destroyed)
266
+ o.update();
262
267
  }
263
268
  }
264
269
  function uiRender()
@@ -682,10 +687,15 @@ class UIObject
682
687
  ASSERT(isVector2(pos), 'ui object pos must be a vec2');
683
688
  ASSERT(isVector2(size), 'ui object size must be a vec2');
684
689
 
685
- /** @property {Vector2} - Local position of the object */
690
+ /** @property {Vector2} - Position you set: an offset from this object's
691
+ * anchor point (the parent box, or the canvas for roots). This is the
692
+ * input that controls placement — set this, not nativePos. */
686
693
  this.localPos = pos.copy();
687
- /** @property {Vector2} - Screen space position of the object */
688
- this.pos = pos.copy();
694
+ /** @property {Vector2} - Resolved position in native UI space, recomputed
695
+ * every frame from localPos + anchor (and nativeHeight, if set). This is a
696
+ * derived output used for drawing and hit-testing; assigning to it has no
697
+ * effect since it is overwritten each frame. Set localPos instead. */
698
+ this.nativePos = pos.copy();
689
699
  /** @property {Vector2} - Screen space size of the object */
690
700
  this.size = size.copy();
691
701
  /** @property {Color} - Color of the object */
@@ -820,7 +830,7 @@ class UIObject
820
830
  const size = !isTouchDevice ? this.size :
821
831
  this.size.add(vec2(this.extraTouchSize || 0));
822
832
  const pos = uiSystem.screenToNative(mousePosScreen);
823
- return isOverlapping(this.pos, size, pos);
833
+ return isOverlapping(this.nativePos, size, pos);
824
834
  }
825
835
 
826
836
  /** Update the object, called automatically by plugin once each frame */
@@ -910,7 +920,7 @@ class UIObject
910
920
  this.color : this.color;
911
921
  const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
912
922
 
913
- uiSystem.drawRect(this.pos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
923
+ uiSystem.drawRect(this.nativePos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
914
924
  }
915
925
 
916
926
  /** Get the size for text with overrides and scale
@@ -947,8 +957,8 @@ class UIObject
947
957
  let text = 'type = ' + this.constructor.name;
948
958
  if (this.text)
949
959
  text += '\ntext = ' + this.text;
950
- if (this.pos.x || this.pos.y)
951
- text += '\npos = ' + this.pos;
960
+ if (this.nativePos.x || this.nativePos.y)
961
+ text += '\nnativePos = ' + this.nativePos;
952
962
  if (this.localPos.x || this.localPos.y)
953
963
  text += '\nlocalPos = ' + this.localPos;
954
964
  if (this.size.x || this.size.y)
@@ -968,7 +978,7 @@ class UIObject
968
978
  this.isHoverObject() ? YELLOW :
969
979
  this.disabled ? PURPLE :
970
980
  this.interactive ? RED : BLUE;
971
- uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
981
+ uiSystem.drawRect(this.nativePos, this.size, CLEAR_BLACK, 4, color);
972
982
  }
973
983
 
974
984
  /** Internal function called when object is clicked
@@ -1051,7 +1061,7 @@ class UIText extends UIObject
1051
1061
 
1052
1062
  // render the text
1053
1063
  const textSize = this.getTextSize();
1054
- 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);
1064
+ 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);
1055
1065
  }
1056
1066
  }
1057
1067
 
@@ -1146,7 +1156,7 @@ class UITextInput extends UIObject
1146
1156
  let text = this.text;
1147
1157
  if (this.isKeyInputObject()) // add a cursor to end of text
1148
1158
  text += timeReal%1 < .5 ? '█' : '░';
1149
- uiSystem.drawText(text, this.pos, textSize,
1159
+ uiSystem.drawText(text, this.nativePos, textSize,
1150
1160
  this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
1151
1161
  }
1152
1162
  }
@@ -1189,7 +1199,7 @@ class UITile extends UIObject
1189
1199
  }
1190
1200
  render()
1191
1201
  {
1192
- uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
1202
+ uiSystem.drawTile(this.nativePos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
1193
1203
  }
1194
1204
  }
1195
1205
 
@@ -1228,7 +1238,7 @@ class UIButton extends UIObject
1228
1238
 
1229
1239
  // draw the text scaled to fit
1230
1240
  const textSize = this.getTextSize();
1231
- uiSystem.drawText(this.text, this.pos.add(this.textOffset), textSize,
1241
+ uiSystem.drawText(this.text, this.nativePos.add(this.textOffset), textSize,
1232
1242
  this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
1233
1243
  }
1234
1244
  }
@@ -1276,13 +1286,13 @@ class UICheckbox extends UIObject
1276
1286
  const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
1277
1287
  const length = lerp(1, 2**.5/2, p) / 2;
1278
1288
  let s = this.size.scale(length);
1279
- uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))), this.pos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
1280
- uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))), this.pos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
1289
+ uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1))), this.nativePos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
1290
+ uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1,1))), this.nativePos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
1281
1291
  }
1282
1292
 
1283
1293
  // draw the text next to the checkbox
1284
1294
  const textSize = this.getTextSize();
1285
- const pos = this.pos.add(vec2(this.size.x,0));
1295
+ const pos = this.nativePos.add(vec2(this.size.x,0));
1286
1296
  uiSystem.drawText(this.text, pos, textSize,
1287
1297
  this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
1288
1298
  }
@@ -1338,7 +1348,7 @@ class UISlider extends UIObject
1338
1348
  const isHorizontal = this.size.x > this.size.y;
1339
1349
  const handleSize = isHorizontal ? this.size.y : this.size.x;
1340
1350
  const barSize = isHorizontal ? this.size.x : this.size.y;
1341
- const centerPos = isHorizontal ? this.pos.x : this.pos.y;
1351
+ const centerPos = isHorizontal ? this.nativePos.x : this.nativePos.y;
1342
1352
 
1343
1353
  // check if value changed
1344
1354
  const handleWidth = barSize - handleSize;
@@ -1372,7 +1382,7 @@ class UISlider extends UIObject
1372
1382
  const minWidth = min(handleWidth, this.cornerRadius * 2);
1373
1383
  const progressWidth = lerp(minWidth, barWidth, this.value);
1374
1384
  const p = (progressWidth - barWidth) * (isHorizontal ? .5 : -.5);
1375
- const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
1385
+ const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
1376
1386
  const color = this.disabled ? this.disabledColor : this.handleColor;
1377
1387
  const drawSize = isHorizontal ?
1378
1388
  vec2(progressWidth, this.size.y) : vec2(this.size.x, progressWidth);
@@ -1383,7 +1393,7 @@ class UISlider extends UIObject
1383
1393
  // draw the slider handle
1384
1394
  const value = clamp(isHorizontal ? this.value : 1 - this.value);
1385
1395
  const p = (barWidth - handleWidth) * (value - .5);
1386
- const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
1396
+ const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
1387
1397
  const color = this.disabled ? this.disabledColor : this.handleColor;
1388
1398
  const drawSize = vec2(handleWidth);
1389
1399
  uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
@@ -1391,7 +1401,7 @@ class UISlider extends UIObject
1391
1401
 
1392
1402
  // draw the text scaled to fit on the slider
1393
1403
  const textSize = this.getTextSize();
1394
- uiSystem.drawText(this.text, this.pos, textSize,
1404
+ uiSystem.drawText(this.text, this.nativePos, textSize,
1395
1405
  this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
1396
1406
  }
1397
1407
  navigatePressed()
@@ -1530,7 +1540,7 @@ class UIVideo extends UIObject
1530
1540
  const context = uiSystem.uiContext;
1531
1541
  const s = this.size;
1532
1542
  context.save();
1533
- context.translate(this.pos.x, this.pos.y);
1543
+ context.translate(this.nativePos.x, this.nativePos.y);
1534
1544
  context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
1535
1545
  context.restore();
1536
1546
  }
package/src/engine.js CHANGED
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
32
32
  * @type {string}
33
33
  * @default
34
34
  * @memberof Engine */
35
- const engineVersion = '1.18.17';
35
+ const engineVersion = '1.18.18';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
package/src/engineDraw.js CHANGED
@@ -1087,6 +1087,62 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
1087
1087
  * @memberof Draw */
1088
1088
  function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
1089
1089
 
1090
+ /** Fit the camera to a rectangle in world space by setting cameraPos and cameraScale
1091
+ * - worldMargin pads the content rectangle in world units, so the gap scales with the content on resize
1092
+ * - screenInset reserves space in screen pixels on each viewport edge (for example a HUD band) and
1093
+ * re-centers the content away from that edge, so the reserved band stays a fixed pixel size on resize
1094
+ * - worldMargin and screenInset may each be a number for all sides, a Vector2 (x=left/right, y=top/bottom),
1095
+ * or an object with any of {top, right, bottom, left}
1096
+ * @param {Vector2} center - Center of the rectangle in world space
1097
+ * @param {Vector2} size - Size of the rectangle in world space
1098
+ * @param {number|Vector2|Object} [worldMargin] - World space padding added around the content rectangle
1099
+ * @param {number|Vector2|Object} [screenInset] - Screen space padding in pixels reserved on each viewport edge
1100
+ * @return {number} - The new camera scale
1101
+ * @memberof Draw */
1102
+ function cameraFit(center, size, worldMargin, screenInset)
1103
+ {
1104
+ ASSERT(isVector2(center), 'center must be a vec2');
1105
+ ASSERT(isVector2(size), 'size must be a vec2');
1106
+
1107
+ // pad the content
1108
+ const margin = padSides(worldMargin);
1109
+ const inset = padSides(screenInset);
1110
+ const worldW = size.x + margin.left + margin.right;
1111
+ const worldH = size.y + margin.top + margin.bottom;
1112
+ const viewW = mainCanvasSize.x - inset.left - inset.right;
1113
+ const viewH = mainCanvasSize.y - inset.top - inset.bottom;
1114
+
1115
+ // bail on a degenerate rect or viewport rather than NaN the camera
1116
+ if (!(worldW > 0 && worldH > 0 && viewW > 0 && viewH > 0))
1117
+ return cameraScale;
1118
+
1119
+ // scale to fit the padded content
1120
+ cameraScale = min(viewW / worldW, viewH / worldH);
1121
+
1122
+ // calculate offset vectors
1123
+ const marginVector = vec2(margin.right - margin.left, margin.top - margin.bottom).scale(.5);
1124
+ const insetVector = vec2(inset.right - inset.left, inset.top - inset.bottom).scale(.5 / cameraScale);
1125
+
1126
+ // apply the offsets and return camera scale
1127
+ cameraPos = center.add(marginVector).add(insetVector);
1128
+ return cameraScale;
1129
+
1130
+ function padSides(p)
1131
+ {
1132
+ // normalize a padding option to {top, right, bottom, left}
1133
+ if (p === undefined || isNumber(p))
1134
+ p = vec2(p);
1135
+ if (isVector2(p))
1136
+ return { top: p.y, right: p.x, bottom: p.y, left: p.x };
1137
+ return {
1138
+ top: p.top || 0,
1139
+ right: p.right || 0,
1140
+ bottom: p.bottom || 0,
1141
+ left: p.left || 0,
1142
+ };
1143
+ }
1144
+ }
1145
+
1090
1146
  /** Check if a box, point, or circle is on screen with a circle test
1091
1147
  * If size is a Vector2, uses the length as diameter
1092
1148
  * This can be used to cull offscreen objects from render or update
@@ -1125,14 +1181,13 @@ function isOnScreen(pos, size=0)
1125
1181
  y + size > -h && y - size < h;
1126
1182
  }
1127
1183
 
1128
- /** Enable normal or additive blend mode
1184
+ /** Enable additive blending
1129
1185
  * @param {boolean} [additive]
1130
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
1131
1186
  * @memberof Draw */
1132
- function setBlendMode(additive=false, context=drawContext)
1187
+ function setAdditiveBlendMode(additive=true)
1133
1188
  {
1134
1189
  glAdditive = additive;
1135
- context.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1190
+ drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1136
1191
  }
1137
1192
 
1138
1193
  /** Combines LittleJS canvases onto the main canvas
@@ -276,7 +276,7 @@ export
276
276
  drawCanvas2D,
277
277
  drawText,
278
278
  drawTextScreen,
279
- setBlendMode,
279
+ setAdditiveBlendMode,
280
280
  combineCanvases,
281
281
  engineImageFont,
282
282
  ImageFont,
@@ -284,6 +284,7 @@ export
284
284
  toggleFullscreen,
285
285
  setCursor,
286
286
  getCameraSize,
287
+ cameraFit,
287
288
  isOnScreen,
288
289
 
289
290
  // WebGL
@@ -327,10 +328,16 @@ export
327
328
  mouseWheel,
328
329
  mouseInWindow,
329
330
  isUsingGamepad,
331
+ lastInputDevice,
332
+ inputMouseMoveThreshold,
330
333
  inputPreventDefault,
331
334
  gamepadPrimary,
332
335
  isTouchDevice,
333
336
  setInputPreventDefault,
337
+ setInputMouseMoveThreshold,
338
+ usingMouseInput,
339
+ usingKeyboardInput,
340
+ usingGamepadInput,
334
341
  gamepadIsDown,
335
342
  gamepadWasPressed,
336
343
  gamepadWasReleased,
@@ -43,11 +43,30 @@ let mouseWheel = 0;
43
43
  * @memberof Input */
44
44
  let mouseInWindow = true;
45
45
 
46
- /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
46
+ /** True if a gamepad is the most recently used input device.
47
+ * Equivalent to usingGamepadInput(); derived from lastInputDevice each frame.
47
48
  * @type {boolean}
48
49
  * @memberof Input */
49
50
  let isUsingGamepad = false;
50
51
 
52
+ /** The most recently used input device: 'mouse' | 'keyboard' | 'gamepad'.
53
+ * Sticky: it holds its value while every device is idle, so a mouse-follow
54
+ * control (e.g. paddle = mousePos) won't snap back the instant the stick/keys
55
+ * are released. With several devices in play at once (e.g. keyboard to move +
56
+ * mouse to aim) it tracks whichever was touched last each frame, so it may
57
+ * alternate — that's intended; use it to pick which control drives a shared
58
+ * action. Updated every frame by inputUpdate().
59
+ * @type {string}
60
+ * @memberof Input */
61
+ let lastInputDevice = 'mouse';
62
+
63
+ /** Screen-pixel mouse movement per frame that counts as "using the mouse"
64
+ * (so sub-pixel hand jitter doesn't steal focus from the keyboard/gamepad).
65
+ * @type {number}
66
+ * @default
67
+ * @memberof Input */
68
+ let inputMouseMoveThreshold = 6;
69
+
51
70
  /** Prevents input continuing to the default browser handling (true by default)
52
71
  * @type {boolean}
53
72
  * @memberof Input */
@@ -68,6 +87,18 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
68
87
  * @memberof Input */
69
88
  function setInputPreventDefault(preventDefault=true) { inputPreventDefault = preventDefault; }
70
89
 
90
+ /** Set the screen-pixel mouse movement per frame that counts as using the mouse
91
+ * @param {number} threshold
92
+ * @memberof Input */
93
+ function setInputMouseMoveThreshold(threshold) { inputMouseMoveThreshold = threshold; }
94
+
95
+ /** @return {boolean} - Is the mouse the most recently used input device? @memberof Input */
96
+ function usingMouseInput() { return lastInputDevice === 'mouse'; }
97
+ /** @return {boolean} - Is the keyboard the most recently used input device? @memberof Input */
98
+ function usingKeyboardInput() { return lastInputDevice === 'keyboard'; }
99
+ /** @return {boolean} - Is a gamepad the most recently used input device? @memberof Input */
100
+ function usingGamepadInput() { return lastInputDevice === 'gamepad'; }
101
+
71
102
  /** Clears an input key state
72
103
  * @param {string|number} key
73
104
  * @param {number} [device]
@@ -368,7 +399,6 @@ function inputInit()
368
399
  {
369
400
  if (!e.repeat)
370
401
  {
371
- isUsingGamepad = false;
372
402
  inputData[0][e.code] = 3;
373
403
  if (inputWASDEmulateDirection)
374
404
  inputData[0][remapKey(e.code)] = 3;
@@ -427,7 +457,6 @@ function inputInit()
427
457
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
428
458
  audioContext.resume();
429
459
 
430
- isUsingGamepad = false;
431
460
  inputData[0][e.button] = 3;
432
461
 
433
462
  const mousePosScreenLast = mousePosScreen;
@@ -518,10 +547,7 @@ function inputInit()
518
547
  if (wasTouching)
519
548
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
520
549
  else
521
- {
522
550
  inputData[0][button] = 3;
523
- isUsingGamepad = false; // a passthrough tap is mouse-style input
524
- }
525
551
  }
526
552
  else if (wasTouching)
527
553
  inputData[0][button] = inputData[0][button] & 2 | 4;
@@ -567,7 +593,43 @@ function inputUpdate()
567
593
 
568
594
  // update gamepads if enabled
569
595
  gamepadsUpdate();
570
-
596
+
597
+ // update most recently used input device
598
+ updateLastInputDevice();
599
+
600
+ function updateLastInputDevice()
601
+ {
602
+ // mouse: any button held or moved
603
+ const mouseActive = mouseIsDown(0) || mouseIsDown(1) || mouseIsDown(2) || mouseDeltaScreen.length() > inputMouseMoveThreshold;
604
+
605
+ // gamepad: any button held or stick moved
606
+ let gamepadActive = false;
607
+ for (let s = gamepadStickCount(); s-- && !gamepadActive;)
608
+ gamepadActive = gamepadStick(s).lengthSquared() > .04;
609
+ for (let b = 17; b-- && !gamepadActive;)
610
+ gamepadActive = gamepadIsDown(b);
611
+
612
+ // keyboard: any non-mouse key down
613
+ let keyboardActive = false;
614
+ for (const k in inputData[0])
615
+ if (isNaN(+k) && (inputData[0][k] & 1))
616
+ {
617
+ keyboardActive = true;
618
+ break;
619
+ }
620
+
621
+ // update the last input
622
+ if (gamepadActive)
623
+ lastInputDevice = 'gamepad';
624
+ else if (mouseActive)
625
+ lastInputDevice = 'mouse';
626
+ else if (keyboardActive)
627
+ lastInputDevice = 'keyboard';
628
+
629
+ // set flag if gamepad is last device
630
+ isUsingGamepad = lastInputDevice === 'gamepad';
631
+ }
632
+
571
633
  // gamepads are updated by engine every frame automatically
572
634
  function gamepadsUpdate()
573
635
  {
@@ -690,7 +752,6 @@ function inputUpdate()
690
752
  gamepadHadInput[i] = true;
691
753
  if (!gamepadHadInput[gamepadPrimary])
692
754
  gamepadPrimary = i;
693
- isUsingGamepad ||= (gamepadPrimary === i);
694
755
  }
695
756
 
696
757
  if (gamepad.mapping === 'standard')
@@ -837,12 +898,19 @@ function touchGamepadRelayout()
837
898
  const setZone = (z, css)=> z.style.cssText =
838
899
  'position:absolute;pointer-events:auto;touch-action:none;' + css;
839
900
 
840
- if (paused && touchGamepadCenterButtonSize)
901
+ if (paused)
841
902
  {
842
- // while paused, any touch presses start
843
- setZone(touchGamepadZoneC, 'inset:0');
903
+ // the gamepad is hidden while paused, so its side zones must not capture
904
+ // touches - otherwise they silently steal taps from menus and dialogs
844
905
  for (const zone of touchGamepadSideZones) zone.style.display = 'none';
845
- touchGamepadZoneC.style.display = '';
906
+ if (touchGamepadCenterButtonSize)
907
+ {
908
+ // any touch presses start
909
+ setZone(touchGamepadZoneC, 'inset:0');
910
+ touchGamepadZoneC.style.display = '';
911
+ }
912
+ else
913
+ touchGamepadZoneC.style.display = 'none';
846
914
  }
847
915
  else
848
916
  {
@@ -1118,7 +1186,6 @@ function touchGamepadPointerDown(e, zone)
1118
1186
  e.preventDefault();
1119
1187
  zone.setPointerCapture(e.pointerId);
1120
1188
  touchGamepadTimer.set();
1121
- isUsingGamepad = true;
1122
1189
 
1123
1190
  // resume audio on first interaction
1124
1191
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
package/src/engineMath.js CHANGED
@@ -924,10 +924,25 @@ class Color
924
924
  * @return {Color} */
925
925
  setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
926
926
 
927
+ /** Sets the alpha of this color and returns self
928
+ * @param {number} [a] - alpha
929
+ * @return {Color} */
930
+ setAlpha(a=1)
931
+ {
932
+ this.a = a;
933
+ ASSERT_COLOR_VALID(this);
934
+ return this;
935
+ }
936
+
927
937
  /** Returns a new color that is a copy of this
928
938
  * @return {Color} */
929
939
  copy() { return new Color(this.r, this.g, this.b, this.a); }
930
940
 
941
+ /** Returns a copy of this color with the alpha set
942
+ * @param {number} [a] - alpha
943
+ * @return {Color} */
944
+ withAlpha(a=1) { return new Color(this.r, this.g, this.b, a); }
945
+
931
946
  /** Returns a copy of this color plus the color passed in
932
947
  * @param {Color} c - other color
933
948
  * @return {Color} */
@@ -496,9 +496,6 @@ class Particle
496
496
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
497
497
  this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
498
498
 
499
- // draw the particle
500
- additive && setBlendMode(true);
501
-
502
499
  // update the position and angle for drawing
503
500
  const pos = particleDrawPos.set(this.pos.x, this.pos.y);
504
501
  let angle = this.angle;
@@ -511,6 +508,9 @@ class Particle
511
508
  emitter.pos.y + pos.x*s + pos.y*c);
512
509
  angle += a;
513
510
  }
511
+
512
+ // draw the particle
513
+ additive && setAdditiveBlendMode();
514
514
  if (trailScale)
515
515
  {
516
516
  // trail style particles
@@ -528,7 +528,7 @@ class Particle
528
528
  }
529
529
  else
530
530
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
531
- additive && setBlendMode();
531
+ additive && setAdditiveBlendMode(false);
532
532
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
533
533
  }
534
534
  }
@@ -89,6 +89,8 @@ let canvasFixedSize = vec2();
89
89
  let canvasPixelated = false;
90
90
 
91
91
  /** Disables texture filtering for crisper pixel art
92
+ * - Leave true for pixel art so sprites stay sharp when scaled (uses NEAREST filtering)
93
+ * - Set false for smooth/high-resolution art to enable bilinear filtering and mipmaps
92
94
  * @type {boolean}
93
95
  * @default
94
96
  * @memberof Settings */
@@ -442,6 +444,7 @@ function setCanvasPixelated(pixelated)
442
444
  }
443
445
 
444
446
  /** Disables texture filtering for crisper pixel art
447
+ * - Leave true for pixel art; set false for smooth/high-resolution art
445
448
  * @param {boolean} pixelated
446
449
  * @memberof Settings */
447
450
  function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }