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.
- package/dist/littlejs.d.ts +79 -15
- package/dist/littlejs.esm.js +230 -64
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +222 -63
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +222 -63
- package/package.json +1 -1
- package/plugins/drawUtilities.js +23 -12
- package/plugins/lightSystem.js +2 -2
- package/plugins/uiSystem.js +35 -25
- package/src/engine.js +1 -1
- package/src/engineDraw.js +59 -4
- package/src/engineExport.js +8 -1
- package/src/engineInput.js +80 -13
- package/src/engineMath.js +15 -0
- package/src/engineObject.js +0 -2
- package/src/engineParticles.js +4 -4
- package/src/engineSettings.js +3 -0
package/plugins/drawUtilities.js
CHANGED
|
@@ -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 -
|
|
17
|
-
* @param {number} [borderSize] -
|
|
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 -
|
|
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] -
|
|
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 -
|
|
77
|
-
* @param {number} [borderSize] -
|
|
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 -
|
|
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] -
|
|
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
|
-
|
|
151
|
-
|
|
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
|
package/plugins/lightSystem.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
220
|
+
setAdditiveBlendMode(prevAdditive);
|
|
221
221
|
glSetInstancedMode(true);
|
|
222
222
|
}
|
|
223
223
|
function lightSystemContextLost()
|
package/plugins/uiSystem.js
CHANGED
|
@@ -132,7 +132,7 @@ class UISystemPlugin
|
|
|
132
132
|
let targetPos, targetSize;
|
|
133
133
|
if (o.parent)
|
|
134
134
|
{
|
|
135
|
-
targetPos = o.parent.
|
|
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.
|
|
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
|
-
|
|
261
|
-
|
|
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} -
|
|
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} -
|
|
688
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
951
|
-
text += '\
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1280
|
-
uiSystem.drawLine(this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
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
|
|
1184
|
+
/** Enable additive blending
|
|
1129
1185
|
* @param {boolean} [additive]
|
|
1130
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
1131
1186
|
* @memberof Draw */
|
|
1132
|
-
function
|
|
1187
|
+
function setAdditiveBlendMode(additive=true)
|
|
1133
1188
|
{
|
|
1134
1189
|
glAdditive = additive;
|
|
1135
|
-
|
|
1190
|
+
drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
|
|
1136
1191
|
}
|
|
1137
1192
|
|
|
1138
1193
|
/** Combines LittleJS canvases onto the main canvas
|
package/src/engineExport.js
CHANGED
|
@@ -276,7 +276,7 @@ export
|
|
|
276
276
|
drawCanvas2D,
|
|
277
277
|
drawText,
|
|
278
278
|
drawTextScreen,
|
|
279
|
-
|
|
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,
|
package/src/engineInput.js
CHANGED
|
@@ -43,11 +43,30 @@ let mouseWheel = 0;
|
|
|
43
43
|
* @memberof Input */
|
|
44
44
|
let mouseInWindow = true;
|
|
45
45
|
|
|
46
|
-
/**
|
|
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
|
|
901
|
+
if (paused)
|
|
841
902
|
{
|
|
842
|
-
// while paused,
|
|
843
|
-
|
|
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
|
-
|
|
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} */
|
package/src/engineObject.js
CHANGED
|
@@ -215,8 +215,6 @@ class EngineObject
|
|
|
215
215
|
|
|
216
216
|
// notify objects of collision and check if should be resolved
|
|
217
217
|
const collide1 = this.collideWithObject(o);
|
|
218
|
-
// callback may have destroyed us; stop resolving against more objects
|
|
219
|
-
if (this.destroyed) return;
|
|
220
218
|
const collide2 = o.collideWithObject(this);
|
|
221
219
|
if (!collide1 || !collide2) continue;
|
|
222
220
|
|
package/src/engineParticles.js
CHANGED
|
@@ -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 &&
|
|
531
|
+
additive && setAdditiveBlendMode(false);
|
|
532
532
|
debugParticles && debugRect(pos, size, '#f005', 0, angle);
|
|
533
533
|
}
|
|
534
534
|
}
|
package/src/engineSettings.js
CHANGED
|
@@ -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; }
|