littlejsengine 1.15.2 → 1.15.8
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 +190 -63
- package/dist/littlejs.esm.js +1267 -605
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1261 -604
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1179 -552
- package/examples/electron/index.html +2 -2
- package/examples/index.html +4 -3
- package/examples/particles/index.html +22 -6
- package/examples/shorts/base.html +2 -1
- package/examples/shorts/box2dCar.js +1 -1
- package/examples/shorts/box2dPool.js +9 -10
- package/examples/shorts/empty.js +1 -1
- package/examples/shorts/input.js +30 -25
- package/examples/shorts/music.js +1 -0
- package/examples/shorts/musicPlayer.js +1 -0
- package/examples/shorts/sequencer.js +2 -0
- package/examples/shorts/sound.js +1 -0
- package/examples/shorts/tileLayer.js +2 -2
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/timers.js +1 -0
- package/examples/shorts/uiSystem.js +26 -14
- package/examples/shorts/videoPlayer.js +2 -1
- package/examples/starter/index.html +3 -2
- package/examples/uiSystem/game.js +28 -15
- package/package.json +1 -1
- package/plugins/box2d.js +1 -1
- package/plugins/desktop.ini +2 -0
- package/plugins/pluginExport.js +2 -0
- package/plugins/uiSystem.js +503 -91
- package/src/engine.js +2 -169
- package/src/engineAudio.js +0 -1
- package/src/engineBuild.js +1 -0
- package/src/engineDebug.js +84 -54
- package/src/engineDraw.js +81 -40
- package/src/engineExport.js +4 -1
- package/src/engineInput.js +526 -381
- package/src/engineObject.js +27 -23
- package/src/engineSettings.js +11 -6
- package/src/engineSplash.js +173 -0
- package/src/engineTileLayer.js +3 -3
- package/src/engineUtilities.js +17 -1
package/dist/littlejs.esm.js
CHANGED
|
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
|
|
|
33
33
|
* @type {string}
|
|
34
34
|
* @default
|
|
35
35
|
* @memberof Engine */
|
|
36
|
-
const engineVersion = '1.15.
|
|
36
|
+
const engineVersion = '1.15.8';
|
|
37
37
|
|
|
38
38
|
/** Frames per second to update
|
|
39
39
|
* @type {number}
|
|
@@ -279,7 +279,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
279
279
|
o.destroyed || o.render();
|
|
280
280
|
gameRenderPost();
|
|
281
281
|
pluginList.forEach(plugin=>plugin.render?.());
|
|
282
|
-
|
|
282
|
+
inputRender();
|
|
283
283
|
debugRender();
|
|
284
284
|
glFlush();
|
|
285
285
|
debugVideoCaptureUpdate();
|
|
@@ -600,7 +600,7 @@ function drawEngineSplashScreen(t)
|
|
|
600
600
|
C ? x.fill() : x.stroke();
|
|
601
601
|
};
|
|
602
602
|
const color = (c=0, l=0) =>
|
|
603
|
-
hsl([.98,.3,.57,.14][c%4]
|
|
603
|
+
hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();
|
|
604
604
|
const alpha = wave(1,1,t);
|
|
605
605
|
const p = percent(alpha, .1, .5);
|
|
606
606
|
|
|
@@ -790,8 +790,9 @@ function LOG(...output) { console.log(...output); }
|
|
|
790
790
|
* @param {number} [time]
|
|
791
791
|
* @param {number} [angle]
|
|
792
792
|
* @param {boolean} [fill]
|
|
793
|
+
* @param {boolean} [screenSpace]
|
|
793
794
|
* @memberof Debug */
|
|
794
|
-
function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
|
|
795
|
+
function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, screenSpace=false)
|
|
795
796
|
{
|
|
796
797
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
797
798
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -806,7 +807,7 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
|
|
|
806
807
|
pos = pos.copy();
|
|
807
808
|
size = size.copy();
|
|
808
809
|
const timer = new Timer(time);
|
|
809
|
-
debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill});
|
|
810
|
+
debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill, screenSpace});
|
|
810
811
|
}
|
|
811
812
|
|
|
812
813
|
/** Draw a debug poly in world space
|
|
@@ -816,8 +817,9 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
|
|
|
816
817
|
* @param {number} [time]
|
|
817
818
|
* @param {number} [angle]
|
|
818
819
|
* @param {boolean} [fill]
|
|
820
|
+
* @param {boolean} [screenSpace]
|
|
819
821
|
* @memberof Debug */
|
|
820
|
-
function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
|
|
822
|
+
function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screenSpace=false)
|
|
821
823
|
{
|
|
822
824
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
823
825
|
ASSERT(isArray(points), 'points must be an array');
|
|
@@ -830,7 +832,7 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
|
|
|
830
832
|
pos = pos.copy();
|
|
831
833
|
points = points.map(p=>p.copy());
|
|
832
834
|
const timer = new Timer(time);
|
|
833
|
-
debugPrimitives.push({pos, points, color, timer, angle, fill});
|
|
835
|
+
debugPrimitives.push({pos, points, color, timer, angle, fill, screenSpace});
|
|
834
836
|
}
|
|
835
837
|
|
|
836
838
|
/** Draw a debug circle in world space
|
|
@@ -839,8 +841,9 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
|
|
|
839
841
|
* @param {Color|string} [color]
|
|
840
842
|
* @param {number} [time]
|
|
841
843
|
* @param {boolean} [fill]
|
|
844
|
+
* @param {boolean} [screenSpace]
|
|
842
845
|
* @memberof Debug */
|
|
843
|
-
function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
|
|
846
|
+
function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=false)
|
|
844
847
|
{
|
|
845
848
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
846
849
|
ASSERT(isNumber(size), 'size must be a number');
|
|
@@ -851,7 +854,7 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
|
|
|
851
854
|
color = color.toString();
|
|
852
855
|
pos = pos.copy();
|
|
853
856
|
const timer = new Timer(time);
|
|
854
|
-
debugPrimitives.push({pos, size, color, timer, angle:0, fill});
|
|
857
|
+
debugPrimitives.push({pos, size, color, timer, angle:0, fill, screenSpace});
|
|
855
858
|
}
|
|
856
859
|
|
|
857
860
|
/** Draw a debug point in world space
|
|
@@ -859,9 +862,10 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
|
|
|
859
862
|
* @param {Color|string} [color]
|
|
860
863
|
* @param {number} [time]
|
|
861
864
|
* @param {number} [angle]
|
|
865
|
+
* @param {boolean} [screenSpace]
|
|
862
866
|
* @memberof Debug */
|
|
863
|
-
function debugPoint(pos, color, time, angle)
|
|
864
|
-
{ debugRect(pos, undefined, color, time, angle); }
|
|
867
|
+
function debugPoint(pos, color, time, angle, screenSpace=false)
|
|
868
|
+
{ debugRect(pos, undefined, color, time, angle, false, screenSpace); }
|
|
865
869
|
|
|
866
870
|
/** Draw a debug line in world space
|
|
867
871
|
* @param {Vector2} posA
|
|
@@ -869,8 +873,9 @@ function debugPoint(pos, color, time, angle)
|
|
|
869
873
|
* @param {Color|string} [color]
|
|
870
874
|
* @param {number} [width]
|
|
871
875
|
* @param {number} [time]
|
|
876
|
+
* @param {boolean} [screenSpace]
|
|
872
877
|
* @memberof Debug */
|
|
873
|
-
function debugLine(posA, posB, color, width=.1, time)
|
|
878
|
+
function debugLine(posA, posB, color, width=.1, time, screenSpace=false)
|
|
874
879
|
{
|
|
875
880
|
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
876
881
|
ASSERT(isVector2(posB), 'posB must be sa vec2');
|
|
@@ -878,7 +883,7 @@ function debugLine(posA, posB, color, width=.1, time)
|
|
|
878
883
|
|
|
879
884
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
880
885
|
const size = vec2(width, halfDelta.length()*2);
|
|
881
|
-
debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
|
|
886
|
+
debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true, screenSpace);
|
|
882
887
|
}
|
|
883
888
|
|
|
884
889
|
/** Draw a debug combined axis aligned bounding box in world space
|
|
@@ -887,8 +892,10 @@ function debugLine(posA, posB, color, width=.1, time)
|
|
|
887
892
|
* @param {Vector2} posB
|
|
888
893
|
* @param {Vector2} sizeB
|
|
889
894
|
* @param {Color|string} [color]
|
|
895
|
+
* @param {number} [time]
|
|
896
|
+
* @param {boolean} [screenSpace]
|
|
890
897
|
* @memberof Debug */
|
|
891
|
-
function debugOverlap(posA, sizeA, posB, sizeB, color)
|
|
898
|
+
function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
892
899
|
{
|
|
893
900
|
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
894
901
|
ASSERT(isVector2(posB), 'posB must be a vec2');
|
|
@@ -903,19 +910,20 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
|
|
|
903
910
|
max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
|
|
904
911
|
max(posA.y + sizeA.y/2, posB.y + sizeB.y/2)
|
|
905
912
|
);
|
|
906
|
-
debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color);
|
|
913
|
+
debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color, time, 0, false, screenSpace);
|
|
907
914
|
}
|
|
908
915
|
|
|
909
916
|
/** Draw a debug axis aligned bounding box in world space
|
|
910
|
-
* @param {string} text
|
|
917
|
+
* @param {string|number} text
|
|
911
918
|
* @param {Vector2} pos
|
|
912
919
|
* @param {number} [size]
|
|
913
920
|
* @param {Color|string} [color]
|
|
914
921
|
* @param {number} [time]
|
|
915
922
|
* @param {number} [angle]
|
|
916
923
|
* @param {string} [font]
|
|
924
|
+
* @param {boolean} [screenSpace]
|
|
917
925
|
* @memberof Debug */
|
|
918
|
-
function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
|
|
926
|
+
function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=false)
|
|
919
927
|
{
|
|
920
928
|
ASSERT(isString(text), 'text must be a string');
|
|
921
929
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
@@ -929,7 +937,7 @@ function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monosp
|
|
|
929
937
|
color = color.toString();
|
|
930
938
|
pos = pos.copy();
|
|
931
939
|
const timer = new Timer(time);
|
|
932
|
-
debugPrimitives.push({text, pos, size, color, timer, angle, font});
|
|
940
|
+
debugPrimitives.push({text, pos, size, color, timer, angle, font, screenSpace});
|
|
933
941
|
}
|
|
934
942
|
|
|
935
943
|
/** Clear all debug primitives in the list
|
|
@@ -1064,34 +1072,46 @@ function debugRender()
|
|
|
1064
1072
|
debugTakeScreenshot = 0;
|
|
1065
1073
|
}
|
|
1066
1074
|
|
|
1067
|
-
if (debugGamepads && gamepadsEnable
|
|
1075
|
+
if (debugGamepads && gamepadsEnable)
|
|
1068
1076
|
{
|
|
1069
|
-
//
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1077
|
+
// draw gamepads
|
|
1078
|
+
const maxGamepads = 8;
|
|
1079
|
+
let gamepadConnectedCount = 0;
|
|
1080
|
+
for (let i = 0; i < maxGamepads; i++)
|
|
1081
|
+
gamepadConnected(i) && gamepadConnectedCount++;
|
|
1082
|
+
|
|
1083
|
+
for (let i = 0; i < maxGamepads; i++)
|
|
1072
1084
|
{
|
|
1073
|
-
|
|
1074
|
-
|
|
1085
|
+
if (!gamepadConnected(i))
|
|
1086
|
+
continue;
|
|
1087
|
+
|
|
1088
|
+
const stickScale = 1;
|
|
1089
|
+
const buttonScale = .2;
|
|
1090
|
+
const cornerPos = cameraPos.add(vec2(-stickScale*2, ((gamepadConnectedCount-1)/2-i)*stickScale*3));
|
|
1091
|
+
debugText(i, cornerPos.add(vec2(-stickScale, stickScale)), 1);
|
|
1092
|
+
if (i === gamepadPrimary)
|
|
1093
|
+
debugText('Main', cornerPos.add(vec2(-stickScale*2, 0)),1, '#0f0');
|
|
1094
|
+
|
|
1095
|
+
// read analog sticks
|
|
1096
|
+
const stickCount = gamepadStickData[i].length;
|
|
1097
|
+
for (let j = 0; j < stickCount; j++)
|
|
1075
1098
|
{
|
|
1076
|
-
const
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
debugCircle(drawPos, buttonScale*2, pressed ? '#f00' : '#fff7', 0, true);
|
|
1093
|
-
debugText(''+j, drawPos, .2);
|
|
1094
|
-
}
|
|
1099
|
+
const stick = gamepadStick(j, i);
|
|
1100
|
+
const drawPos = cornerPos.add(vec2(j*stickScale*2, 0));
|
|
1101
|
+
const stickPos = drawPos.add(stick.scale(stickScale));
|
|
1102
|
+
debugCircle(drawPos, stickScale*2, '#fff7',0,true);
|
|
1103
|
+
debugLine(drawPos, stickPos, '#f00');
|
|
1104
|
+
debugText(j, drawPos, .3);
|
|
1105
|
+
debugPoint(stickPos, '#f00');
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const buttonCount = inputData[i+1].length;
|
|
1109
|
+
for (let j = 0; j < buttonCount; j++)
|
|
1110
|
+
{
|
|
1111
|
+
const drawPos = cornerPos.add(vec2(j*buttonScale*2, -stickScale-buttonScale*2));
|
|
1112
|
+
const pressed = gamepadIsDown(j, i);
|
|
1113
|
+
debugCircle(drawPos, buttonScale*2, pressed ? '#f00' : '#fff7', 0, true);
|
|
1114
|
+
debugText(j, drawPos, .3);
|
|
1095
1115
|
}
|
|
1096
1116
|
}
|
|
1097
1117
|
}
|
|
@@ -1139,21 +1159,26 @@ function debugRender()
|
|
|
1139
1159
|
{
|
|
1140
1160
|
// draw debug primitives
|
|
1141
1161
|
overlayContext.lineWidth = 2;
|
|
1142
|
-
const pointSize = debugPointSize * cameraScale;
|
|
1143
1162
|
debugPrimitives.forEach(p=>
|
|
1144
1163
|
{
|
|
1145
1164
|
overlayContext.save();
|
|
1146
1165
|
|
|
1147
1166
|
// create canvas transform from world space to screen space
|
|
1148
|
-
|
|
1167
|
+
// without scaling because we want consistent pixel sizes
|
|
1168
|
+
let pos = p.pos, scale = 1, angle = p.angle;
|
|
1169
|
+
if (!p.screenSpace)
|
|
1170
|
+
{
|
|
1171
|
+
pos = worldToScreen(p.pos);
|
|
1172
|
+
scale = cameraScale;
|
|
1173
|
+
angle -= cameraAngle;
|
|
1174
|
+
}
|
|
1149
1175
|
overlayContext.translate(pos.x|0, pos.y|0);
|
|
1150
|
-
overlayContext.rotate(
|
|
1176
|
+
overlayContext.rotate(angle);
|
|
1151
1177
|
overlayContext.scale(1, p.text ? 1 : -1);
|
|
1152
1178
|
overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
|
|
1153
|
-
|
|
1154
1179
|
if (p.text !== undefined)
|
|
1155
1180
|
{
|
|
1156
|
-
overlayContext.font = p.size*
|
|
1181
|
+
overlayContext.font = p.size*scale + 'px '+ p.font;
|
|
1157
1182
|
overlayContext.textAlign = 'center';
|
|
1158
1183
|
overlayContext.textBaseline = 'middle';
|
|
1159
1184
|
overlayContext.fillText(p.text, 0, 0);
|
|
@@ -1164,7 +1189,7 @@ function debugRender()
|
|
|
1164
1189
|
overlayContext.beginPath();
|
|
1165
1190
|
for (const point of p.points)
|
|
1166
1191
|
{
|
|
1167
|
-
const p2 = point.scale(
|
|
1192
|
+
const p2 = point.scale(scale).floor();
|
|
1168
1193
|
overlayContext.lineTo(p2.x, p2.y);
|
|
1169
1194
|
}
|
|
1170
1195
|
overlayContext.closePath();
|
|
@@ -1174,13 +1199,14 @@ function debugRender()
|
|
|
1174
1199
|
else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
|
|
1175
1200
|
{
|
|
1176
1201
|
// point
|
|
1202
|
+
const pointSize = debugPointSize * scale;
|
|
1177
1203
|
overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
|
|
1178
1204
|
overlayContext.fillRect(-1, -pointSize/2, 3, pointSize);
|
|
1179
1205
|
}
|
|
1180
1206
|
else if (p.size.x !== undefined)
|
|
1181
1207
|
{
|
|
1182
1208
|
// rect
|
|
1183
|
-
const s = p.size.scale(
|
|
1209
|
+
const s = p.size.scale(scale).floor();
|
|
1184
1210
|
const w = s.x, h = s.y;
|
|
1185
1211
|
p.fill && overlayContext.fillRect(-w/2|0, -h/2|0, w, h);
|
|
1186
1212
|
overlayContext.strokeRect(-w/2|0, -h/2|0, w, h);
|
|
@@ -1189,7 +1215,7 @@ function debugRender()
|
|
|
1189
1215
|
{
|
|
1190
1216
|
// circle
|
|
1191
1217
|
overlayContext.beginPath();
|
|
1192
|
-
overlayContext.arc(0, 0, p.size*
|
|
1218
|
+
overlayContext.arc(0, 0, p.size*scale/2, 0, 9);
|
|
1193
1219
|
p.fill && overlayContext.fill();
|
|
1194
1220
|
overlayContext.stroke();
|
|
1195
1221
|
}
|
|
@@ -1267,14 +1293,18 @@ function debugRender()
|
|
|
1267
1293
|
mousePressed && overlayContext.fillText('Mouse: ' + mousePressed, x, y += h);
|
|
1268
1294
|
keysPressed && overlayContext.fillText('Keys: ' + keysPressed, x, y += h);
|
|
1269
1295
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
for (const i in inputData[1])
|
|
1296
|
+
// show gamepad buttons
|
|
1297
|
+
for (let i = 1; i < inputData.length; i++)
|
|
1273
1298
|
{
|
|
1274
|
-
|
|
1275
|
-
|
|
1299
|
+
let buttonsPressed = '';
|
|
1300
|
+
if (inputData[i])
|
|
1301
|
+
for (const j in inputData[i])
|
|
1302
|
+
{
|
|
1303
|
+
if (keyIsDown(j, i))
|
|
1304
|
+
buttonsPressed += j + ' ' ;
|
|
1305
|
+
}
|
|
1306
|
+
buttonsPressed && overlayContext.fillText(`Gamepad ${i-1}: ` + buttonsPressed, x, y += h);
|
|
1276
1307
|
}
|
|
1277
|
-
buttonsPressed && overlayContext.fillText('Gamepad: ' + buttonsPressed, x, y += h);
|
|
1278
1308
|
}
|
|
1279
1309
|
else
|
|
1280
1310
|
{
|
|
@@ -2030,9 +2060,15 @@ class Vector2
|
|
|
2030
2060
|
{
|
|
2031
2061
|
this.x = x;
|
|
2032
2062
|
this.y = y;
|
|
2063
|
+
ASSERT_VECTOR2_VALID(this);
|
|
2033
2064
|
return this;
|
|
2034
2065
|
}
|
|
2035
2066
|
|
|
2067
|
+
/** Sets this vector from another vector and returns self
|
|
2068
|
+
* @param {Vector2} v - other vector
|
|
2069
|
+
* @return {Vector2} */
|
|
2070
|
+
setFrom(v) { return this.set(v.x, v.y); }
|
|
2071
|
+
|
|
2036
2072
|
/** Returns a new vector that is a copy of this
|
|
2037
2073
|
* @return {Vector2} */
|
|
2038
2074
|
copy() { return new Vector2(this.x, this.y); }
|
|
@@ -2095,7 +2131,7 @@ class Vector2
|
|
|
2095
2131
|
clampLength(length=1)
|
|
2096
2132
|
{
|
|
2097
2133
|
const l = this.length();
|
|
2098
|
-
return l > length ? this.scale(length/l) : this;
|
|
2134
|
+
return l > length ? this.scale(length/l) : this.copy();
|
|
2099
2135
|
}
|
|
2100
2136
|
|
|
2101
2137
|
/** Returns the dot product of this and the vector passed in
|
|
@@ -2182,6 +2218,10 @@ class Vector2
|
|
|
2182
2218
|
* @return {number} */
|
|
2183
2219
|
area() { return abs(this.x * this.y); }
|
|
2184
2220
|
|
|
2221
|
+
/** Returns true if this vector is (0,0)
|
|
2222
|
+
* @return {boolean} */
|
|
2223
|
+
isZero() { return !this.x && !this.y; }
|
|
2224
|
+
|
|
2185
2225
|
/** Returns a new vector that is p percent between this and the vector passed in
|
|
2186
2226
|
* @param {Vector2} v - other vector
|
|
2187
2227
|
* @param {number} percent
|
|
@@ -2292,9 +2332,15 @@ class Color
|
|
|
2292
2332
|
this.g = g;
|
|
2293
2333
|
this.b = b;
|
|
2294
2334
|
this.a = a;
|
|
2335
|
+
ASSERT_COLOR_VALID(this);
|
|
2295
2336
|
return this;
|
|
2296
2337
|
}
|
|
2297
2338
|
|
|
2339
|
+
/** Sets this color from another color and returns self
|
|
2340
|
+
* @param {Color} c - other color
|
|
2341
|
+
* @return {Color} */
|
|
2342
|
+
setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
|
|
2343
|
+
|
|
2298
2344
|
/** Returns a new color that is a copy of this
|
|
2299
2345
|
* @return {Color} */
|
|
2300
2346
|
copy() { return new Color(this.r, this.g, this.b, this.a); }
|
|
@@ -2846,17 +2892,17 @@ let touchGamepadEnable = false;
|
|
|
2846
2892
|
* @memberof Settings */
|
|
2847
2893
|
let touchGamepadCenterButton = true;
|
|
2848
2894
|
|
|
2849
|
-
/**
|
|
2850
|
-
* @type {
|
|
2895
|
+
/** Number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
|
|
2896
|
+
* @type {number}
|
|
2851
2897
|
* @default
|
|
2852
2898
|
* @memberof Settings */
|
|
2853
|
-
let
|
|
2899
|
+
let touchGamepadButtonCount = 4;
|
|
2854
2900
|
|
|
2855
|
-
/**
|
|
2856
|
-
* @type {
|
|
2901
|
+
/** True if touch gamepad should be analog stick or false to use if 8 way dpad
|
|
2902
|
+
* @type {boolean}
|
|
2857
2903
|
* @default
|
|
2858
2904
|
* @memberof Settings */
|
|
2859
|
-
let
|
|
2905
|
+
let touchGamepadAnalog = true;
|
|
2860
2906
|
|
|
2861
2907
|
/** Size of virtual gamepad for touch devices in pixels
|
|
2862
2908
|
* @type {number}
|
|
@@ -3122,6 +3168,11 @@ function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
|
3122
3168
|
* @memberof Settings */
|
|
3123
3169
|
function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
|
|
3124
3170
|
|
|
3171
|
+
/** Set number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
|
|
3172
|
+
* @param {number} count
|
|
3173
|
+
* @memberof Settings */
|
|
3174
|
+
function setTouchGamepadButtonCount(count) { touchGamepadButtonCount = count; }
|
|
3175
|
+
|
|
3125
3176
|
/** Set if touch gamepad should be analog stick or 8 way dpad
|
|
3126
3177
|
* @param {boolean} analog
|
|
3127
3178
|
* @memberof Settings */
|
|
@@ -3330,7 +3381,7 @@ class EngineObject
|
|
|
3330
3381
|
child.updateTransforms();
|
|
3331
3382
|
}
|
|
3332
3383
|
|
|
3333
|
-
/** Update the object physics, called automatically by engine once each frame */
|
|
3384
|
+
/** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object. */
|
|
3334
3385
|
updatePhysics()
|
|
3335
3386
|
{
|
|
3336
3387
|
// child objects do not have physics
|
|
@@ -3363,7 +3414,7 @@ class EngineObject
|
|
|
3363
3414
|
if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
|
|
3364
3415
|
return;
|
|
3365
3416
|
|
|
3366
|
-
const
|
|
3417
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
3367
3418
|
if (this.groundObject)
|
|
3368
3419
|
{
|
|
3369
3420
|
// apply friction in local space of ground object
|
|
@@ -3419,10 +3470,10 @@ class EngineObject
|
|
|
3419
3470
|
{
|
|
3420
3471
|
// push outside object collision
|
|
3421
3472
|
this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
|
|
3422
|
-
if ((o.groundObject &&
|
|
3473
|
+
if ((o.groundObject && wasFalling) || !o.mass)
|
|
3423
3474
|
{
|
|
3424
3475
|
// set ground object if landed on something
|
|
3425
|
-
if (
|
|
3476
|
+
if (wasFalling)
|
|
3426
3477
|
this.groundObject = o;
|
|
3427
3478
|
|
|
3428
3479
|
// bounce if other object is fixed or grounded
|
|
@@ -3509,12 +3560,15 @@ class EngineObject
|
|
|
3509
3560
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
3510
3561
|
this.velocity.y *= -restitution;
|
|
3511
3562
|
|
|
3512
|
-
if (
|
|
3563
|
+
if (wasFalling)
|
|
3513
3564
|
{
|
|
3514
|
-
// adjust position to slightly
|
|
3565
|
+
// adjust position to slightly away from nearest tile
|
|
3515
3566
|
// this prevents gap between object and ground
|
|
3516
3567
|
const epsilon = .0001;
|
|
3517
|
-
|
|
3568
|
+
const offset = this.size.y/2 + epsilon;
|
|
3569
|
+
this.pos.y = gravity.y < 0 ?
|
|
3570
|
+
floor(oldPos.y-this.size.y/2) + offset :
|
|
3571
|
+
ceil( oldPos.y+this.size.y/2) - offset;
|
|
3518
3572
|
|
|
3519
3573
|
// set ground object for tile collision
|
|
3520
3574
|
this.groundObject = hitLayer;
|
|
@@ -3644,7 +3698,9 @@ class EngineObject
|
|
|
3644
3698
|
{
|
|
3645
3699
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
3646
3700
|
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
3647
|
-
this.children.
|
|
3701
|
+
const index = this.children.indexOf(child);
|
|
3702
|
+
ASSERT(index >= 0, 'child not found in children array');
|
|
3703
|
+
index >= 0 && this.children.splice(index, 1);
|
|
3648
3704
|
child.parent = 0;
|
|
3649
3705
|
}
|
|
3650
3706
|
|
|
@@ -3681,21 +3737,20 @@ class EngineObject
|
|
|
3681
3737
|
* @return {string} */
|
|
3682
3738
|
toString()
|
|
3683
3739
|
{
|
|
3684
|
-
if (debug)
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
}
|
|
3740
|
+
if (!debug) return;
|
|
3741
|
+
|
|
3742
|
+
let text = 'type = ' + this.constructor.name;
|
|
3743
|
+
if (this.pos.x || this.pos.y)
|
|
3744
|
+
text += '\npos = ' + this.pos;
|
|
3745
|
+
if (this.velocity.x || this.velocity.y)
|
|
3746
|
+
text += '\nvelocity = ' + this.velocity;
|
|
3747
|
+
if (this.size.x || this.size.y)
|
|
3748
|
+
text += '\nsize = ' + this.size;
|
|
3749
|
+
if (this.angle)
|
|
3750
|
+
text += '\nangle = ' + this.angle.toFixed(3);
|
|
3751
|
+
if (this.color)
|
|
3752
|
+
text += '\ncolor = ' + this.color;
|
|
3753
|
+
return text;
|
|
3699
3754
|
}
|
|
3700
3755
|
|
|
3701
3756
|
/** Render debug info for this object */
|
|
@@ -3864,7 +3919,7 @@ class TileInfo
|
|
|
3864
3919
|
this.padding = padding;
|
|
3865
3920
|
/** @property {TextureInfo} - The texture info for this tile */
|
|
3866
3921
|
this.textureInfo = textureInfos[this.textureIndex];
|
|
3867
|
-
/** @property {
|
|
3922
|
+
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
3868
3923
|
this.bleedScale = bleedScale;
|
|
3869
3924
|
}
|
|
3870
3925
|
|
|
@@ -3963,15 +4018,11 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
|
3963
4018
|
|
|
3964
4019
|
const textureInfo = tileInfo && tileInfo.textureInfo;
|
|
3965
4020
|
const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
|
|
3966
|
-
if (useWebGL)
|
|
4021
|
+
if (useWebGL && glEnable)
|
|
3967
4022
|
{
|
|
3968
4023
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3969
4024
|
if (screenSpace)
|
|
3970
|
-
|
|
3971
|
-
// convert to world space
|
|
3972
|
-
pos = screenToWorld(pos);
|
|
3973
|
-
size = size.scale(1/cameraScale);
|
|
3974
|
-
}
|
|
4025
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
3975
4026
|
if (textureInfo)
|
|
3976
4027
|
{
|
|
3977
4028
|
// calculate uvs and render
|
|
@@ -4059,7 +4110,8 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
4059
4110
|
ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
|
|
4060
4111
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4061
4112
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4062
|
-
|
|
4113
|
+
|
|
4114
|
+
if (useWebGL && glEnable)
|
|
4063
4115
|
{
|
|
4064
4116
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
4065
4117
|
if (screenSpace)
|
|
@@ -4067,6 +4119,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
4067
4119
|
// convert to world space
|
|
4068
4120
|
pos = screenToWorld(pos);
|
|
4069
4121
|
size = size.scale(1/cameraScale);
|
|
4122
|
+
angle += cameraAngle;
|
|
4070
4123
|
}
|
|
4071
4124
|
// build 4 corner points for the rectangle
|
|
4072
4125
|
const points = [], colors = [];
|
|
@@ -4122,17 +4175,14 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
4122
4175
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4123
4176
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4124
4177
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4125
|
-
|
|
4178
|
+
|
|
4179
|
+
if (useWebGL && glEnable)
|
|
4126
4180
|
{
|
|
4127
4181
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
4128
|
-
let
|
|
4182
|
+
let size = vec2(1);
|
|
4129
4183
|
if (screenSpace)
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
pos = screenToWorld(pos);
|
|
4133
|
-
scale = 1/cameraScale;
|
|
4134
|
-
}
|
|
4135
|
-
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, scale, scale, angle, wrap);
|
|
4184
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
4185
|
+
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, size.x, size.y, angle, wrap);
|
|
4136
4186
|
}
|
|
4137
4187
|
else
|
|
4138
4188
|
{
|
|
@@ -4228,19 +4278,15 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
4228
4278
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4229
4279
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4230
4280
|
|
|
4231
|
-
if (useWebGL)
|
|
4281
|
+
if (useWebGL && glEnable)
|
|
4232
4282
|
{
|
|
4233
4283
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
4234
|
-
let
|
|
4284
|
+
let size = vec2(1);
|
|
4235
4285
|
if (screenSpace)
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
pos = screenToWorld(pos);
|
|
4239
|
-
scale = 1/cameraScale;
|
|
4240
|
-
}
|
|
4241
|
-
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
|
|
4286
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
4287
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, size.x, size.y, angle);
|
|
4242
4288
|
if (lineWidth > 0)
|
|
4243
|
-
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y,
|
|
4289
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, size.x, size.y, angle);
|
|
4244
4290
|
}
|
|
4245
4291
|
else
|
|
4246
4292
|
{
|
|
@@ -4283,7 +4329,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
4283
4329
|
ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
|
|
4284
4330
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4285
4331
|
|
|
4286
|
-
if (useWebGL)
|
|
4332
|
+
if (useWebGL && glEnable)
|
|
4287
4333
|
{
|
|
4288
4334
|
// draw as a regular polygon
|
|
4289
4335
|
const sides = glCircleSides;
|
|
@@ -4346,14 +4392,10 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4346
4392
|
ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
|
|
4347
4393
|
|
|
4348
4394
|
if (!screenSpace)
|
|
4349
|
-
|
|
4350
|
-
// transform from world space to screen space
|
|
4351
|
-
pos = worldToScreen(pos);
|
|
4352
|
-
size = size.scale(cameraScale);
|
|
4353
|
-
}
|
|
4395
|
+
[pos, size, angle] = worldToScreenTransform(pos, size, angle);
|
|
4354
4396
|
context.save();
|
|
4355
4397
|
context.translate(pos.x+.5, pos.y+.5);
|
|
4356
|
-
context.rotate(angle
|
|
4398
|
+
context.rotate(angle);
|
|
4357
4399
|
context.scale(mirror ? -size.x : size.x, -size.y);
|
|
4358
4400
|
drawFunction(context);
|
|
4359
4401
|
context.restore();
|
|
@@ -4364,7 +4406,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4364
4406
|
|
|
4365
4407
|
/** Draw text on main canvas in world space
|
|
4366
4408
|
* Automatically splits new lines into rows
|
|
4367
|
-
* @param {string} text
|
|
4409
|
+
* @param {string|number} text
|
|
4368
4410
|
* @param {Vector2} pos
|
|
4369
4411
|
* @param {number} [size]
|
|
4370
4412
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -4379,12 +4421,19 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4379
4421
|
* @memberof Draw */
|
|
4380
4422
|
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
4381
4423
|
{
|
|
4382
|
-
|
|
4424
|
+
// convert to screen space
|
|
4425
|
+
pos = worldToScreen(pos);
|
|
4426
|
+
size *= cameraScale;
|
|
4427
|
+
lineWidth *= cameraScale;
|
|
4428
|
+
angle -= cameraAngle;
|
|
4429
|
+
angle *= -1;
|
|
4430
|
+
|
|
4431
|
+
drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
|
|
4383
4432
|
}
|
|
4384
4433
|
|
|
4385
4434
|
/** Draw text on overlay canvas in world space
|
|
4386
4435
|
* Automatically splits new lines into rows
|
|
4387
|
-
* @param {string} text
|
|
4436
|
+
* @param {string|number} text
|
|
4388
4437
|
* @param {Vector2} pos
|
|
4389
4438
|
* @param {number} [size]
|
|
4390
4439
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -4403,7 +4452,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
4403
4452
|
|
|
4404
4453
|
/** Draw text on overlay canvas in screen space
|
|
4405
4454
|
* Automatically splits new lines into rows
|
|
4406
|
-
* @param {string} text
|
|
4455
|
+
* @param {string|number} text
|
|
4407
4456
|
* @param {Vector2} pos
|
|
4408
4457
|
* @param {number} [size]
|
|
4409
4458
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -4538,11 +4587,58 @@ function worldToScreenDelta(worldDelta)
|
|
|
4538
4587
|
return new Vector2(x * cameraScale, y * -cameraScale);
|
|
4539
4588
|
}
|
|
4540
4589
|
|
|
4541
|
-
/**
|
|
4590
|
+
/** Convert screen space transform to world space
|
|
4591
|
+
* @param {Vector2} screenPos
|
|
4592
|
+
* @param {Vector2} screenSize
|
|
4593
|
+
* @param {number} [screenAngle]
|
|
4594
|
+
* @return {[Vector2, Vector2, number]} - [pos, size, angle]
|
|
4595
|
+
* @memberof Draw */
|
|
4596
|
+
function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
|
|
4597
|
+
{
|
|
4598
|
+
return [
|
|
4599
|
+
screenToWorld(screenPos),
|
|
4600
|
+
screenSize.scale(1/cameraScale),
|
|
4601
|
+
screenAngle + cameraAngle
|
|
4602
|
+
];
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4605
|
+
/** Convert world space transform to screen space
|
|
4606
|
+
* @param {Vector2} worldPos
|
|
4607
|
+
* @param {Vector2} worldSize
|
|
4608
|
+
* @param {number} [worldAngle]
|
|
4609
|
+
* @return {[Vector2, Vector2, number]} - [pos, size, angle]
|
|
4610
|
+
* @memberof Draw */
|
|
4611
|
+
function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
|
|
4612
|
+
{
|
|
4613
|
+
return [
|
|
4614
|
+
worldToScreen(worldPos),
|
|
4615
|
+
worldSize.scale(cameraScale),
|
|
4616
|
+
worldAngle - cameraAngle
|
|
4617
|
+
];
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
/** Get the size of the camera window in world space
|
|
4542
4621
|
* @return {Vector2}
|
|
4543
4622
|
* @memberof Draw */
|
|
4544
4623
|
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
4545
4624
|
|
|
4625
|
+
/** Check if a point or circle is on screen
|
|
4626
|
+
* If size is a Vector2, uses the largest dimension as diameter
|
|
4627
|
+
* This can be used to cull offscreen objects from render or update
|
|
4628
|
+
* @param {Vector2} pos - world space position
|
|
4629
|
+
* @param {Vector2|number} size - world space size or diameter
|
|
4630
|
+
* @return {boolean}
|
|
4631
|
+
* @memberof Draw */
|
|
4632
|
+
function isOnScreen(pos, size=0)
|
|
4633
|
+
{
|
|
4634
|
+
pos = worldToScreen(pos);
|
|
4635
|
+
if (size instanceof Vector2)
|
|
4636
|
+
size = max(size.x, size.y); // use largest dimension
|
|
4637
|
+
size *= cameraScale/2;
|
|
4638
|
+
return pos.x + size > 0 && pos.x - size < mainCanvasSize.x &&
|
|
4639
|
+
pos.y + size > 0 && pos.y - size < mainCanvasSize.y;
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4546
4642
|
/** Enable normal or additive blend mode
|
|
4547
4643
|
* @param {boolean} [additive]
|
|
4548
4644
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -4706,7 +4802,7 @@ class FontImage
|
|
|
4706
4802
|
}
|
|
4707
4803
|
|
|
4708
4804
|
/** Draw text in world space using the image font
|
|
4709
|
-
* @param {string} text
|
|
4805
|
+
* @param {string|number} text
|
|
4710
4806
|
* @param {Vector2} pos
|
|
4711
4807
|
* @param {number} [scale=.25]
|
|
4712
4808
|
* @param {boolean} [center]
|
|
@@ -4718,7 +4814,7 @@ class FontImage
|
|
|
4718
4814
|
}
|
|
4719
4815
|
|
|
4720
4816
|
/** Draw text on overlay canvas in world space using the image font
|
|
4721
|
-
* @param {string} text
|
|
4817
|
+
* @param {string|number} text
|
|
4722
4818
|
* @param {Vector2} pos
|
|
4723
4819
|
* @param {number} [scale]
|
|
4724
4820
|
* @param {boolean} [center]
|
|
@@ -4727,7 +4823,7 @@ class FontImage
|
|
|
4727
4823
|
{ this.drawText(text, pos, scale, center, overlayContext); }
|
|
4728
4824
|
|
|
4729
4825
|
/** Draw text on overlay canvas in screen space using the image font
|
|
4730
|
-
* @param {string} text
|
|
4826
|
+
* @param {string|number} text
|
|
4731
4827
|
* @param {Vector2} pos
|
|
4732
4828
|
* @param {number} [scale]
|
|
4733
4829
|
* @param {boolean} [center]
|
|
@@ -4771,6 +4867,85 @@ class FontImage
|
|
|
4771
4867
|
* @namespace Input
|
|
4772
4868
|
*/
|
|
4773
4869
|
|
|
4870
|
+
/** Mouse pos in world space
|
|
4871
|
+
* @type {Vector2}
|
|
4872
|
+
* @memberof Input */
|
|
4873
|
+
let mousePos = vec2();
|
|
4874
|
+
|
|
4875
|
+
/** Mouse pos in screen space
|
|
4876
|
+
* @type {Vector2}
|
|
4877
|
+
* @memberof Input */
|
|
4878
|
+
let mousePosScreen = vec2();
|
|
4879
|
+
|
|
4880
|
+
/** Mouse movement delta in world space
|
|
4881
|
+
* @type {Vector2}
|
|
4882
|
+
* @memberof Input */
|
|
4883
|
+
let mouseDelta = vec2();
|
|
4884
|
+
|
|
4885
|
+
/** Mouse movement delta in screen space
|
|
4886
|
+
* @type {Vector2}
|
|
4887
|
+
* @memberof Input */
|
|
4888
|
+
let mouseDeltaScreen = vec2();
|
|
4889
|
+
|
|
4890
|
+
/** Mouse wheel delta this frame
|
|
4891
|
+
* @type {number}
|
|
4892
|
+
* @memberof Input */
|
|
4893
|
+
let mouseWheel = 0;
|
|
4894
|
+
|
|
4895
|
+
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4896
|
+
* @type {boolean}
|
|
4897
|
+
* @memberof Input */
|
|
4898
|
+
let mouseInWindow = true;
|
|
4899
|
+
|
|
4900
|
+
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4901
|
+
* @type {boolean}
|
|
4902
|
+
* @memberof Input */
|
|
4903
|
+
let isUsingGamepad = false;
|
|
4904
|
+
|
|
4905
|
+
/** Prevents input continuing to the default browser handling (true by default)
|
|
4906
|
+
* @type {boolean}
|
|
4907
|
+
* @memberof Input */
|
|
4908
|
+
let inputPreventDefault = true;
|
|
4909
|
+
|
|
4910
|
+
/** Primary gamepad index, automatically set to first gamepad with input
|
|
4911
|
+
* @type {number}
|
|
4912
|
+
* @memberof Input */
|
|
4913
|
+
let gamepadPrimary = 0;
|
|
4914
|
+
|
|
4915
|
+
/** Prevents input continuing to the default browser handling
|
|
4916
|
+
* This is useful to disable for html menus so the browser can handle input normally
|
|
4917
|
+
* @param {boolean} preventDefault
|
|
4918
|
+
* @memberof Input */
|
|
4919
|
+
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
4920
|
+
|
|
4921
|
+
/** Clears an input key state
|
|
4922
|
+
* @param {string|number} key
|
|
4923
|
+
* @param {number} [device]
|
|
4924
|
+
* @param {boolean} [clearDown=true]
|
|
4925
|
+
* @param {boolean} [clearPressed=true]
|
|
4926
|
+
* @param {boolean} [clearReleased=true]
|
|
4927
|
+
* @memberof Input */
|
|
4928
|
+
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4929
|
+
{
|
|
4930
|
+
if (!inputData[device])
|
|
4931
|
+
return;
|
|
4932
|
+
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
/** Clears all input
|
|
4936
|
+
* @memberof Input */
|
|
4937
|
+
function inputClear()
|
|
4938
|
+
{
|
|
4939
|
+
inputData.length = 0;
|
|
4940
|
+
inputData[0] = [];
|
|
4941
|
+
touchGamepadButtons.length = 0;
|
|
4942
|
+
touchGamepadSticks.length = 0;
|
|
4943
|
+
gamepadStickData.length = 0;
|
|
4944
|
+
gamepadDpadData.length = 0;
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4947
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4948
|
+
|
|
4774
4949
|
/** Returns true if device key is down
|
|
4775
4950
|
* @param {string|number} key
|
|
4776
4951
|
* @param {number} [device]
|
|
@@ -4778,9 +4953,9 @@ class FontImage
|
|
|
4778
4953
|
* @memberof Input */
|
|
4779
4954
|
function keyIsDown(key, device=0)
|
|
4780
4955
|
{
|
|
4781
|
-
ASSERT(key
|
|
4956
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4782
4957
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4783
|
-
return
|
|
4958
|
+
return !!(inputData[device]?.[key] & 1);
|
|
4784
4959
|
}
|
|
4785
4960
|
|
|
4786
4961
|
/** Returns true if device key was pressed this frame
|
|
@@ -4790,9 +4965,9 @@ function keyIsDown(key, device=0)
|
|
|
4790
4965
|
* @memberof Input */
|
|
4791
4966
|
function keyWasPressed(key, device=0)
|
|
4792
4967
|
{
|
|
4793
|
-
ASSERT(key
|
|
4968
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4794
4969
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4795
|
-
return
|
|
4970
|
+
return !!(inputData[device]?.[key] & 2);
|
|
4796
4971
|
}
|
|
4797
4972
|
|
|
4798
4973
|
/** Returns true if device key was released this frame
|
|
@@ -4802,172 +4977,182 @@ function keyWasPressed(key, device=0)
|
|
|
4802
4977
|
* @memberof Input */
|
|
4803
4978
|
function keyWasReleased(key, device=0)
|
|
4804
4979
|
{
|
|
4805
|
-
ASSERT(key
|
|
4980
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4806
4981
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4807
|
-
return
|
|
4982
|
+
return !!(inputData[device]?.[key] & 4);
|
|
4808
4983
|
}
|
|
4809
4984
|
|
|
4810
4985
|
/** Returns input vector from arrow keys or WASD if enabled
|
|
4986
|
+
* @param {string} [up]
|
|
4987
|
+
* @param {string} [down]
|
|
4988
|
+
* @param {string} [left]
|
|
4989
|
+
* @param {string} [right]
|
|
4811
4990
|
* @return {Vector2}
|
|
4812
4991
|
* @memberof Input */
|
|
4813
4992
|
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
4814
4993
|
{
|
|
4994
|
+
ASSERT(isString(up), 'up key must be a string');
|
|
4995
|
+
ASSERT(isString(down), 'down key must be a string');
|
|
4996
|
+
ASSERT(isString(left), 'left key must be a string');
|
|
4997
|
+
ASSERT(isString(right), 'right key must be a string');
|
|
4815
4998
|
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
4816
4999
|
return vec2(k(right) - k(left), k(up) - k(down));
|
|
4817
5000
|
}
|
|
4818
5001
|
|
|
4819
|
-
/** Clears all input
|
|
4820
|
-
* @memberof Input */
|
|
4821
|
-
function inputClear() { inputData = [[]]; touchGamepadButtons = []; }
|
|
4822
|
-
|
|
4823
|
-
/** Clears an input key state
|
|
4824
|
-
* @param {string|number} key
|
|
4825
|
-
* @param {number} [device]
|
|
4826
|
-
* @param {boolean} [clearDown=true]
|
|
4827
|
-
* @param {boolean} [clearPressed=true]
|
|
4828
|
-
* @param {boolean} [clearReleased=true]
|
|
4829
|
-
* @memberof Input */
|
|
4830
|
-
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4831
|
-
{
|
|
4832
|
-
if (!inputData[device])
|
|
4833
|
-
return;
|
|
4834
|
-
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4835
|
-
}
|
|
4836
|
-
|
|
4837
5002
|
/** Returns true if mouse button is down
|
|
4838
5003
|
* @function
|
|
4839
5004
|
* @param {number} button
|
|
4840
5005
|
* @return {boolean}
|
|
4841
5006
|
* @memberof Input */
|
|
4842
|
-
function mouseIsDown(button)
|
|
5007
|
+
function mouseIsDown(button)
|
|
5008
|
+
{
|
|
5009
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
5010
|
+
return keyIsDown(button);
|
|
5011
|
+
}
|
|
4843
5012
|
|
|
4844
5013
|
/** Returns true if mouse button was pressed
|
|
4845
5014
|
* @function
|
|
4846
5015
|
* @param {number} button
|
|
4847
5016
|
* @return {boolean}
|
|
4848
5017
|
* @memberof Input */
|
|
4849
|
-
function mouseWasPressed(button)
|
|
5018
|
+
function mouseWasPressed(button)
|
|
5019
|
+
{
|
|
5020
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
5021
|
+
return keyWasPressed(button);
|
|
5022
|
+
}
|
|
4850
5023
|
|
|
4851
5024
|
/** Returns true if mouse button was released
|
|
4852
5025
|
* @function
|
|
4853
5026
|
* @param {number} button
|
|
4854
5027
|
* @return {boolean}
|
|
4855
5028
|
* @memberof Input */
|
|
4856
|
-
function mouseWasReleased(button)
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
let mousePos = vec2();
|
|
4862
|
-
|
|
4863
|
-
/** Mouse pos in screen space
|
|
4864
|
-
* @type {Vector2}
|
|
4865
|
-
* @memberof Input */
|
|
4866
|
-
let mousePosScreen = vec2();
|
|
4867
|
-
|
|
4868
|
-
/** Mouse movement delta in world space
|
|
4869
|
-
* @type {Vector2}
|
|
4870
|
-
* @memberof Input */
|
|
4871
|
-
let mouseDelta = vec2();
|
|
4872
|
-
|
|
4873
|
-
/** Mouse movement delta in screen space
|
|
4874
|
-
* @type {Vector2}
|
|
4875
|
-
* @memberof Input */
|
|
4876
|
-
let mouseDeltaScreen = vec2();
|
|
4877
|
-
|
|
4878
|
-
/** Mouse wheel delta this frame
|
|
4879
|
-
* @type {number}
|
|
4880
|
-
* @memberof Input */
|
|
4881
|
-
let mouseWheel = 0;
|
|
4882
|
-
|
|
4883
|
-
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4884
|
-
* @type {boolean}
|
|
4885
|
-
* @memberof Input */
|
|
4886
|
-
let mouseInWindow = true;
|
|
4887
|
-
|
|
4888
|
-
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4889
|
-
* @type {boolean}
|
|
4890
|
-
* @memberof Input */
|
|
4891
|
-
let isUsingGamepad = false;
|
|
4892
|
-
|
|
4893
|
-
/** Prevents input continuing to the default browser handling (true by default)
|
|
4894
|
-
* @type {boolean}
|
|
4895
|
-
* @memberof Input */
|
|
4896
|
-
let inputPreventDefault = true;
|
|
4897
|
-
|
|
4898
|
-
/** Prevents input continuing to the default browser handling
|
|
4899
|
-
* This is useful to disable for html menus so the browser can handle input normally
|
|
4900
|
-
* @param {boolean} preventDefault
|
|
4901
|
-
* @memberof Input */
|
|
4902
|
-
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
5029
|
+
function mouseWasReleased(button)
|
|
5030
|
+
{
|
|
5031
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
5032
|
+
return keyWasReleased(button);
|
|
5033
|
+
}
|
|
4903
5034
|
|
|
4904
5035
|
/** Returns true if gamepad button is down
|
|
4905
5036
|
* @param {number} button
|
|
4906
5037
|
* @param {number} [gamepad]
|
|
4907
5038
|
* @return {boolean}
|
|
4908
5039
|
* @memberof Input */
|
|
4909
|
-
function gamepadIsDown(button, gamepad=
|
|
4910
|
-
{
|
|
5040
|
+
function gamepadIsDown(button, gamepad=gamepadPrimary)
|
|
5041
|
+
{
|
|
5042
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
5043
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5044
|
+
return keyIsDown(button, gamepad+1);
|
|
5045
|
+
}
|
|
4911
5046
|
|
|
4912
5047
|
/** Returns true if gamepad button was pressed
|
|
4913
5048
|
* @param {number} button
|
|
4914
5049
|
* @param {number} [gamepad]
|
|
4915
5050
|
* @return {boolean}
|
|
4916
5051
|
* @memberof Input */
|
|
4917
|
-
function gamepadWasPressed(button, gamepad=
|
|
4918
|
-
{
|
|
5052
|
+
function gamepadWasPressed(button, gamepad=gamepadPrimary)
|
|
5053
|
+
{
|
|
5054
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
5055
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5056
|
+
return keyWasPressed(button, gamepad+1);
|
|
5057
|
+
}
|
|
4919
5058
|
|
|
4920
5059
|
/** Returns true if gamepad button was released
|
|
4921
5060
|
* @param {number} button
|
|
4922
5061
|
* @param {number} [gamepad]
|
|
4923
5062
|
* @return {boolean}
|
|
4924
5063
|
* @memberof Input */
|
|
4925
|
-
function gamepadWasReleased(button, gamepad=
|
|
4926
|
-
{
|
|
5064
|
+
function gamepadWasReleased(button, gamepad=gamepadPrimary)
|
|
5065
|
+
{
|
|
5066
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
5067
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5068
|
+
return keyWasReleased(button, gamepad+1);
|
|
5069
|
+
}
|
|
4927
5070
|
|
|
4928
5071
|
/** Returns gamepad stick value
|
|
4929
5072
|
* @param {number} stick
|
|
4930
5073
|
* @param {number} [gamepad]
|
|
4931
5074
|
* @return {Vector2}
|
|
4932
5075
|
* @memberof Input */
|
|
4933
|
-
function gamepadStick(stick, gamepad=
|
|
4934
|
-
{
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
5076
|
+
function gamepadStick(stick, gamepad=gamepadPrimary)
|
|
5077
|
+
{
|
|
5078
|
+
ASSERT(isNumber(stick), 'stick must be a number');
|
|
5079
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5080
|
+
return gamepadStickData[gamepad]?.[stick] ?? vec2();
|
|
5081
|
+
}
|
|
4938
5082
|
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
5083
|
+
/** Returns gamepad dpad value
|
|
5084
|
+
* @param {number} [gamepad]
|
|
5085
|
+
* @return {Vector2}
|
|
5086
|
+
* @memberof Input */
|
|
5087
|
+
function gamepadDpad(gamepad=gamepadPrimary)
|
|
5088
|
+
{
|
|
5089
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5090
|
+
return gamepadDpadData[gamepad] ?? vec2();
|
|
5091
|
+
}
|
|
4942
5092
|
|
|
4943
|
-
|
|
5093
|
+
/** Returns true if passed in gamepad is connected
|
|
5094
|
+
* @param {number} [gamepad]
|
|
5095
|
+
* @return {boolean}
|
|
5096
|
+
* @memberof Input */
|
|
5097
|
+
function gamepadConnected(gamepad=gamepadPrimary)
|
|
4944
5098
|
{
|
|
4945
|
-
|
|
5099
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5100
|
+
return !!inputData[gamepad+1];
|
|
5101
|
+
}
|
|
4946
5102
|
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
5103
|
+
/** True if a touch device has been detected
|
|
5104
|
+
* @memberof Input */
|
|
5105
|
+
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
4950
5106
|
|
|
4951
|
-
|
|
4952
|
-
mousePos = screenToWorld(mousePosScreen);
|
|
4953
|
-
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
5107
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4954
5108
|
|
|
4955
|
-
|
|
4956
|
-
|
|
5109
|
+
/** Pulse the vibration hardware if it exists
|
|
5110
|
+
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
5111
|
+
* @memberof Input */
|
|
5112
|
+
function vibrate(pattern=100)
|
|
5113
|
+
{
|
|
5114
|
+
ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
|
|
5115
|
+
vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
|
|
4957
5116
|
}
|
|
4958
5117
|
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
5118
|
+
/** Cancel any ongoing vibration
|
|
5119
|
+
* @memberof Input */
|
|
5120
|
+
function vibrateStop() { vibrate(0); }
|
|
4962
5121
|
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
5122
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5123
|
+
// Pointer Lock
|
|
5124
|
+
|
|
5125
|
+
/** Request to lock the pointer, does not work on touch devices
|
|
5126
|
+
* @memberof Input */
|
|
5127
|
+
function pointerLockRequest()
|
|
5128
|
+
{ !isTouchDevice && mainCanvas.requestPointerLock?.(); }
|
|
5129
|
+
|
|
5130
|
+
/** Request to unlock the pointer
|
|
5131
|
+
* @memberof Input */
|
|
5132
|
+
function pointerLockExit()
|
|
5133
|
+
{ document.exitPointerLock?.(); }
|
|
5134
|
+
|
|
5135
|
+
/** Check if pointer is locked (true if locked)
|
|
5136
|
+
* @return {boolean}
|
|
5137
|
+
* @memberof Input */
|
|
5138
|
+
function pointerLockIsActive()
|
|
5139
|
+
{ return document.pointerLockElement === mainCanvas; }
|
|
5140
|
+
|
|
5141
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5142
|
+
// Input variables used by engine
|
|
5143
|
+
|
|
5144
|
+
// input uses bit field for each key: 1=isDown, 2=wasPressed, 4=wasReleased
|
|
5145
|
+
// mouse and keyboard stored in device 0, gamepads stored in devices > 0
|
|
5146
|
+
const inputData = [[]];
|
|
5147
|
+
|
|
5148
|
+
// gamepad internal variables
|
|
5149
|
+
const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
|
|
5150
|
+
|
|
5151
|
+
// touch gamepad internal variables
|
|
5152
|
+
const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
|
|
5153
|
+
|
|
5154
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5155
|
+
// Input system functions used by engine
|
|
4971
5156
|
|
|
4972
5157
|
function inputInit()
|
|
4973
5158
|
{
|
|
@@ -4997,6 +5182,18 @@ function inputInit()
|
|
|
4997
5182
|
if (inputWASDEmulateDirection)
|
|
4998
5183
|
inputData[0][remapKey(e.code)] = 3;
|
|
4999
5184
|
}
|
|
5185
|
+
|
|
5186
|
+
// prevent arrow key from moving the page
|
|
5187
|
+
const preventDefaultKeys =
|
|
5188
|
+
[
|
|
5189
|
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', // scrolling
|
|
5190
|
+
'Space', // page down scroll
|
|
5191
|
+
'Tab', // focus navigation
|
|
5192
|
+
'Backspace', // browser back
|
|
5193
|
+
];
|
|
5194
|
+
if (preventDefaultKeys.includes(e.code))
|
|
5195
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5196
|
+
e.preventDefault();
|
|
5000
5197
|
}
|
|
5001
5198
|
function onKeyUp(e)
|
|
5002
5199
|
{
|
|
@@ -5028,7 +5225,9 @@ function inputInit()
|
|
|
5028
5225
|
const mousePosScreenLast = mousePosScreen;
|
|
5029
5226
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
5030
5227
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
5031
|
-
|
|
5228
|
+
|
|
5229
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5230
|
+
e.preventDefault();
|
|
5032
5231
|
}
|
|
5033
5232
|
function onMouseUp(e)
|
|
5034
5233
|
{
|
|
@@ -5052,344 +5251,386 @@ function inputInit()
|
|
|
5052
5251
|
function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
|
|
5053
5252
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
5054
5253
|
function onBlur() { inputClear(); } // reset input when focus is lost
|
|
5055
|
-
}
|
|
5056
5254
|
|
|
5057
|
-
//
|
|
5058
|
-
function
|
|
5059
|
-
{
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
}
|
|
5255
|
+
// enable touch input mouse passthrough
|
|
5256
|
+
function touchInputInit()
|
|
5257
|
+
{
|
|
5258
|
+
// add non passive touch event listeners
|
|
5259
|
+
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
5260
|
+
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
5261
|
+
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
5065
5262
|
|
|
5066
|
-
|
|
5067
|
-
|
|
5263
|
+
// handle all touch events the same way
|
|
5264
|
+
let wasTouching;
|
|
5265
|
+
function handleTouch(e)
|
|
5266
|
+
{
|
|
5267
|
+
if (!touchInputEnable)
|
|
5268
|
+
return;
|
|
5068
5269
|
|
|
5069
|
-
//
|
|
5070
|
-
|
|
5270
|
+
// route touch to gamepad
|
|
5271
|
+
if (touchGamepadEnable)
|
|
5272
|
+
handleTouchGamepad(e);
|
|
5071
5273
|
|
|
5072
|
-
//
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
const applyDeadZones = (v)=>
|
|
5076
|
-
{
|
|
5077
|
-
const min=.3, max=.8;
|
|
5078
|
-
const deadZone = (v)=>
|
|
5079
|
-
v > min ? percent(v, min, max) :
|
|
5080
|
-
v < -min ? -percent(-v, min, max) : 0;
|
|
5081
|
-
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5082
|
-
}
|
|
5274
|
+
// fix stalled audio requiring user interaction
|
|
5275
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
5276
|
+
audioContext.resume();
|
|
5083
5277
|
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5278
|
+
// check if touching and pass to mouse events
|
|
5279
|
+
const touching = e.touches.length;
|
|
5280
|
+
const button = 0; // all touches are left mouse button
|
|
5281
|
+
if (touching)
|
|
5282
|
+
{
|
|
5283
|
+
// set event pos and pass it along
|
|
5284
|
+
const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
|
|
5285
|
+
const mousePosScreenLast = mousePosScreen;
|
|
5286
|
+
mousePosScreen = mouseEventToScreen(pos);
|
|
5287
|
+
if (wasTouching)
|
|
5288
|
+
{
|
|
5289
|
+
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
5290
|
+
isUsingGamepad = touchGamepadEnable;
|
|
5291
|
+
}
|
|
5292
|
+
else
|
|
5293
|
+
inputData[0][button] = 3;
|
|
5294
|
+
}
|
|
5295
|
+
else if (wasTouching)
|
|
5296
|
+
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
5089
5297
|
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
//
|
|
5098
|
-
|
|
5099
|
-
sticks[0].y = -round(touchGamepadStick.y);
|
|
5100
|
-
sticks[0] = sticks[0].clampLength();
|
|
5298
|
+
// set was touching
|
|
5299
|
+
wasTouching = touching;
|
|
5300
|
+
|
|
5301
|
+
// prevent default handling like copy, magnifier lens, and scrolling
|
|
5302
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5303
|
+
e.preventDefault();
|
|
5304
|
+
|
|
5305
|
+
// must return true so the document will get focus
|
|
5306
|
+
return true;
|
|
5101
5307
|
}
|
|
5102
5308
|
|
|
5103
|
-
//
|
|
5104
|
-
|
|
5105
|
-
for (let i=10; i--;)
|
|
5309
|
+
// special handling for virtual gamepad mode
|
|
5310
|
+
function handleTouchGamepad(e)
|
|
5106
5311
|
{
|
|
5107
|
-
|
|
5108
|
-
|
|
5312
|
+
// clear touch gamepad input
|
|
5313
|
+
touchGamepadSticks.length = 0;
|
|
5314
|
+
touchGamepadSticks[0] = vec2();
|
|
5315
|
+
touchGamepadSticks[1] = vec2();
|
|
5316
|
+
touchGamepadButtons.length = 0;
|
|
5317
|
+
isUsingGamepad = true;
|
|
5318
|
+
|
|
5319
|
+
const touching = e.touches.length;
|
|
5320
|
+
if (touching)
|
|
5321
|
+
{
|
|
5322
|
+
touchGamepadTimer.set();
|
|
5323
|
+
if (touchGamepadCenterButton && !wasTouching && paused)
|
|
5324
|
+
{
|
|
5325
|
+
// touch anywhere to press start when paused
|
|
5326
|
+
touchGamepadButtons[9] = 1;
|
|
5327
|
+
return;
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
|
|
5331
|
+
// don't process touch gamepad if paused
|
|
5332
|
+
if (paused)
|
|
5333
|
+
return;
|
|
5334
|
+
|
|
5335
|
+
// get center of left and right sides
|
|
5336
|
+
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5337
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
5338
|
+
const startCenter = mainCanvasSize.scale(.5);
|
|
5339
|
+
|
|
5340
|
+
// check each touch point
|
|
5341
|
+
for (const touch of e.touches)
|
|
5342
|
+
{
|
|
5343
|
+
const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
|
|
5344
|
+
if (stickCenter.distance(touchPos) < touchGamepadSize)
|
|
5345
|
+
{
|
|
5346
|
+
// virtual analog stick
|
|
5347
|
+
const delta = touchPos.subtract(stickCenter);
|
|
5348
|
+
touchGamepadSticks[0] = delta.scale(2/touchGamepadSize).clampLength();
|
|
5349
|
+
}
|
|
5350
|
+
else if (buttonCenter.distance(touchPos) < touchGamepadSize)
|
|
5351
|
+
{
|
|
5352
|
+
if (touchGamepadButtonCount === 1)
|
|
5353
|
+
{
|
|
5354
|
+
// virtual right analog stick
|
|
5355
|
+
const delta = touchPos.subtract(buttonCenter);
|
|
5356
|
+
touchGamepadSticks[1] = delta.scale(2/touchGamepadSize).clampLength();
|
|
5357
|
+
}
|
|
5358
|
+
// virtual face buttons
|
|
5359
|
+
let button = buttonCenter.subtract(touchPos).direction();
|
|
5360
|
+
button = mod(button+2, 4);
|
|
5361
|
+
if (touchGamepadButtonCount === 1)
|
|
5362
|
+
button = 0;
|
|
5363
|
+
else if (touchGamepadButtonCount === 2)
|
|
5364
|
+
{
|
|
5365
|
+
const delta = buttonCenter.subtract(touchPos);
|
|
5366
|
+
button = -delta.x < delta.y ? 1 : 0;
|
|
5367
|
+
}
|
|
5368
|
+
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
5369
|
+
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
5370
|
+
if (button < touchGamepadButtonCount)
|
|
5371
|
+
touchGamepadButtons[button] = 1;
|
|
5372
|
+
}
|
|
5373
|
+
else if (touchGamepadCenterButton &&
|
|
5374
|
+
startCenter.distance(touchPos) < touchGamepadSize)
|
|
5375
|
+
{
|
|
5376
|
+
// virtual start button in center
|
|
5377
|
+
touchGamepadButtons[9] = 1;
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5109
5380
|
}
|
|
5381
|
+
}
|
|
5110
5382
|
|
|
5111
|
-
|
|
5112
|
-
|
|
5383
|
+
// convert a mouse or touch event position to screen space
|
|
5384
|
+
function mouseEventToScreen(mousePos)
|
|
5385
|
+
{
|
|
5386
|
+
const rect = mainCanvas.getBoundingClientRect();
|
|
5387
|
+
const px = percent(mousePos.x, rect.left, rect.right);
|
|
5388
|
+
const py = percent(mousePos.y, rect.top, rect.bottom);
|
|
5389
|
+
return vec2(px*mainCanvas.width, py*mainCanvas.height);
|
|
5113
5390
|
}
|
|
5391
|
+
}
|
|
5114
5392
|
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5393
|
+
function inputUpdate()
|
|
5394
|
+
{
|
|
5395
|
+
if (headlessMode) return;
|
|
5118
5396
|
|
|
5119
|
-
//
|
|
5120
|
-
if (!
|
|
5121
|
-
|
|
5397
|
+
// clear input when lost focus (prevent stuck keys)
|
|
5398
|
+
if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
5399
|
+
inputClear();
|
|
5122
5400
|
|
|
5123
|
-
//
|
|
5124
|
-
|
|
5125
|
-
|
|
5401
|
+
// update mouse world space position and delta
|
|
5402
|
+
mousePos = screenToWorld(mousePosScreen);
|
|
5403
|
+
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
5404
|
+
|
|
5405
|
+
// update gamepads if enabled
|
|
5406
|
+
gamepadsUpdate();
|
|
5407
|
+
|
|
5408
|
+
// gamepads are updated by engine every frame automatically
|
|
5409
|
+
function gamepadsUpdate()
|
|
5126
5410
|
{
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5411
|
+
const applyDeadZones = (v)=>
|
|
5412
|
+
{
|
|
5413
|
+
const min=.3, max=.8;
|
|
5414
|
+
const deadZone = (v)=>
|
|
5415
|
+
v > min ? percent(v, min, max) :
|
|
5416
|
+
v < -min ? -percent(-v, min, max) : 0;
|
|
5417
|
+
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5418
|
+
}
|
|
5419
|
+
|
|
5420
|
+
// update touch gamepad if enabled
|
|
5421
|
+
if (touchGamepadEnable && isTouchDevice)
|
|
5422
|
+
{
|
|
5423
|
+
if (!touchGamepadTimer.isSet())
|
|
5424
|
+
return;
|
|
5425
|
+
|
|
5426
|
+
// read virtual analog stick
|
|
5427
|
+
gamepadPrimary = 0; // touch gamepad uses index 0
|
|
5428
|
+
const sticks = gamepadStickData[0] ?? (gamepadStickData[0] = []);
|
|
5429
|
+
const dpad = gamepadDpadData[0] ?? (gamepadDpadData[0] = vec2());
|
|
5430
|
+
sticks[0] = vec2();
|
|
5431
|
+
dpad.set();
|
|
5432
|
+
const leftTouchStick = touchGamepadSticks[0] ?? vec2();
|
|
5433
|
+
if (touchGamepadAnalog)
|
|
5434
|
+
sticks[0] = applyDeadZones(leftTouchStick);
|
|
5435
|
+
else if (leftTouchStick.lengthSquared() > .3)
|
|
5436
|
+
{
|
|
5437
|
+
// convert to 8 way dpad
|
|
5438
|
+
const x = clamp(round(leftTouchStick.x), -1, 1);
|
|
5439
|
+
const y = clamp(round(leftTouchStick.y), -1, 1);
|
|
5440
|
+
dpad.set(x, -y);
|
|
5441
|
+
sticks[0] = dpad.clampLength(); // clamp to circle
|
|
5442
|
+
}
|
|
5443
|
+
const rightTouchStick = touchGamepadSticks[1] ?? vec2();
|
|
5444
|
+
if (touchGamepadButtonCount === 1)
|
|
5445
|
+
sticks[1] = applyDeadZones(rightTouchStick);
|
|
5446
|
+
|
|
5447
|
+
// read virtual gamepad buttons
|
|
5448
|
+
const data = inputData[1] ?? (inputData[1] = []);
|
|
5449
|
+
for (let i=10; i--;)
|
|
5450
|
+
{
|
|
5451
|
+
const wasDown = gamepadIsDown(i,0);
|
|
5452
|
+
data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5455
|
+
// disable normal gamepads when touch gamepad is active
|
|
5456
|
+
return;
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
// return if gamepads are disabled or not supported
|
|
5460
|
+
if (!gamepadsEnable || !navigator || !navigator.getGamepads)
|
|
5461
|
+
return;
|
|
5131
5462
|
|
|
5132
|
-
|
|
5463
|
+
// only poll gamepads when focused or in debug mode
|
|
5464
|
+
if (!debug && !document.hasFocus())
|
|
5465
|
+
return;
|
|
5466
|
+
|
|
5467
|
+
// poll gamepads
|
|
5468
|
+
const maxGamepads = 8;
|
|
5469
|
+
const gamepads = navigator.getGamepads();
|
|
5470
|
+
const gamepadCount = min(maxGamepads, gamepads.length)
|
|
5471
|
+
for (let i=0; i<gamepadCount; ++i)
|
|
5133
5472
|
{
|
|
5473
|
+
// get or create gamepad data
|
|
5474
|
+
const gamepad = gamepads[i];
|
|
5475
|
+
if (!gamepad)
|
|
5476
|
+
{
|
|
5477
|
+
// clear gamepad data if not connected
|
|
5478
|
+
inputData[i+1] = undefined;
|
|
5479
|
+
gamepadStickData[i] = undefined;
|
|
5480
|
+
gamepadDpadData[i] = undefined;
|
|
5481
|
+
gamepadHadInput[i] = undefined;
|
|
5482
|
+
continue;
|
|
5483
|
+
}
|
|
5484
|
+
|
|
5485
|
+
const data = inputData[i+1] ?? (inputData[i+1] = []);
|
|
5486
|
+
const sticks = gamepadStickData[i] ?? (gamepadStickData[i] = []);
|
|
5487
|
+
const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
|
|
5488
|
+
|
|
5134
5489
|
// read analog sticks
|
|
5135
5490
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
5136
5491
|
sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
|
|
5137
5492
|
|
|
5138
5493
|
// read buttons
|
|
5494
|
+
let hadInput = false;
|
|
5139
5495
|
for (let j = gamepad.buttons.length; j--;)
|
|
5140
5496
|
{
|
|
5141
5497
|
const button = gamepad.buttons[j];
|
|
5142
5498
|
const wasDown = gamepadIsDown(j,i);
|
|
5143
5499
|
data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5500
|
+
|
|
5501
|
+
// check for any input on this gamepad, analog must be full press
|
|
5502
|
+
if (button.pressed)
|
|
5503
|
+
if (!button.value || button.value > .9)
|
|
5504
|
+
hadInput = true;
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
// set new primary gamepad if current is not connected
|
|
5508
|
+
if (hadInput)
|
|
5509
|
+
{
|
|
5510
|
+
gamepadHadInput[i] = true;
|
|
5511
|
+
if (!gamepadHadInput[gamepadPrimary])
|
|
5512
|
+
gamepadPrimary = i;
|
|
5513
|
+
isUsingGamepad ||= (gamepadPrimary === i);
|
|
5147
5514
|
}
|
|
5148
5515
|
|
|
5149
|
-
if (
|
|
5516
|
+
if (gamepad.mapping === 'standard')
|
|
5150
5517
|
{
|
|
5151
|
-
//
|
|
5152
|
-
|
|
5518
|
+
// get dpad buttons (standard mapping)
|
|
5519
|
+
dpad.set(
|
|
5153
5520
|
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
5154
5521
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
5155
|
-
|
|
5156
|
-
|
|
5522
|
+
}
|
|
5523
|
+
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
5524
|
+
{
|
|
5525
|
+
// digital style dpad from axes
|
|
5526
|
+
const x = clamp(round(gamepad.axes[0]), -1, 1);
|
|
5527
|
+
const y = clamp(round(gamepad.axes[1]), -1, 1);
|
|
5528
|
+
dpad.set(x, -y);
|
|
5157
5529
|
}
|
|
5158
5530
|
|
|
5159
|
-
//
|
|
5160
|
-
|
|
5531
|
+
// copy dpad to left analog stick when pressed
|
|
5532
|
+
if (gamepadDirectionEmulateStick && !dpad.isZero())
|
|
5533
|
+
sticks[0] = dpad.clampLength();
|
|
5161
5534
|
}
|
|
5162
|
-
}
|
|
5163
|
-
}
|
|
5164
|
-
|
|
5165
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5166
|
-
|
|
5167
|
-
/** Pulse the vibration hardware if it exists
|
|
5168
|
-
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
5169
|
-
* @memberof Input */
|
|
5170
|
-
function vibrate(pattern=100)
|
|
5171
|
-
{ vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
|
|
5172
5535
|
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5178
|
-
// Touch input & virtual on screen gamepad
|
|
5179
|
-
|
|
5180
|
-
/** True if a touch device has been detected
|
|
5181
|
-
* @memberof Input */
|
|
5182
|
-
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
5183
|
-
|
|
5184
|
-
// touch gamepad internal variables
|
|
5185
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
|
|
5536
|
+
// disable touch gamepad if using real gamepad
|
|
5537
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5186
5540
|
|
|
5187
|
-
function
|
|
5541
|
+
function inputUpdatePost()
|
|
5188
5542
|
{
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5543
|
+
if (headlessMode) return;
|
|
5544
|
+
|
|
5545
|
+
// clear input to prepare for next frame
|
|
5546
|
+
for (const deviceInputData of inputData)
|
|
5547
|
+
for (const i in deviceInputData)
|
|
5548
|
+
deviceInputData[i] &= 1;
|
|
5549
|
+
mouseWheel = 0;
|
|
5550
|
+
mouseDelta = vec2();
|
|
5551
|
+
mouseDeltaScreen = vec2();
|
|
5194
5552
|
}
|
|
5195
5553
|
|
|
5196
|
-
|
|
5197
|
-
function touchInputInit()
|
|
5554
|
+
function inputRender()
|
|
5198
5555
|
{
|
|
5199
|
-
|
|
5200
|
-
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
5201
|
-
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
5202
|
-
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
5556
|
+
touchGamepadRender();
|
|
5203
5557
|
|
|
5204
|
-
|
|
5205
|
-
let wasTouching;
|
|
5206
|
-
function handleTouch(e)
|
|
5558
|
+
function touchGamepadRender()
|
|
5207
5559
|
{
|
|
5208
|
-
if (!touchInputEnable)
|
|
5560
|
+
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
5561
|
+
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
5209
5562
|
return;
|
|
5210
5563
|
|
|
5211
|
-
//
|
|
5212
|
-
|
|
5213
|
-
|
|
5564
|
+
// fade off when not touching or paused
|
|
5565
|
+
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
5566
|
+
if (!alpha || paused)
|
|
5567
|
+
return;
|
|
5214
5568
|
|
|
5215
|
-
//
|
|
5216
|
-
|
|
5217
|
-
|
|
5569
|
+
// setup the canvas
|
|
5570
|
+
const context = overlayContext;
|
|
5571
|
+
context.save();
|
|
5572
|
+
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
5573
|
+
context.strokeStyle = '#fff';
|
|
5574
|
+
context.lineWidth = 3;
|
|
5218
5575
|
|
|
5219
|
-
//
|
|
5220
|
-
const
|
|
5221
|
-
|
|
5222
|
-
|
|
5576
|
+
// draw left analog stick
|
|
5577
|
+
const leftTouchStick = touchGamepadSticks[0] ?? vec2();
|
|
5578
|
+
context.fillStyle = leftTouchStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
5579
|
+
context.beginPath();
|
|
5580
|
+
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5581
|
+
if (touchGamepadAnalog)
|
|
5223
5582
|
{
|
|
5224
|
-
//
|
|
5225
|
-
|
|
5226
|
-
const mousePosScreenLast = mousePosScreen;
|
|
5227
|
-
mousePosScreen = mouseEventToScreen(pos);
|
|
5228
|
-
if (wasTouching)
|
|
5229
|
-
{
|
|
5230
|
-
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
5231
|
-
isUsingGamepad = touchGamepadEnable;
|
|
5232
|
-
}
|
|
5233
|
-
else
|
|
5234
|
-
inputData[0][button] = 3;
|
|
5583
|
+
// draw circle shaped gamepad
|
|
5584
|
+
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
5235
5585
|
}
|
|
5236
|
-
else
|
|
5237
|
-
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
5238
|
-
|
|
5239
|
-
// set was touching
|
|
5240
|
-
wasTouching = touching;
|
|
5241
|
-
|
|
5242
|
-
// prevent default handling like copy, magnifier lens, and scrolling
|
|
5243
|
-
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5244
|
-
e.preventDefault();
|
|
5245
|
-
|
|
5246
|
-
// must return true so the document will get focus
|
|
5247
|
-
return true;
|
|
5248
|
-
}
|
|
5249
|
-
|
|
5250
|
-
// special handling for virtual gamepad mode
|
|
5251
|
-
function handleTouchGamepad(e)
|
|
5252
|
-
{
|
|
5253
|
-
// clear touch gamepad input
|
|
5254
|
-
touchGamepadStick = vec2();
|
|
5255
|
-
touchGamepadButtons = [];
|
|
5256
|
-
isUsingGamepad = true;
|
|
5257
|
-
|
|
5258
|
-
const touching = e.touches.length;
|
|
5259
|
-
if (touching)
|
|
5586
|
+
else
|
|
5260
5587
|
{
|
|
5261
|
-
|
|
5262
|
-
|
|
5588
|
+
// draw cross shaped gamepad
|
|
5589
|
+
for (let i=10; --i;)
|
|
5263
5590
|
{
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5591
|
+
const angle = i*PI/4;
|
|
5592
|
+
context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
5593
|
+
i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
|
|
5267
5594
|
}
|
|
5268
5595
|
}
|
|
5596
|
+
context.fill();
|
|
5597
|
+
context.stroke();
|
|
5269
5598
|
|
|
5270
|
-
//
|
|
5271
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5272
|
-
const buttonCenter = touchGamepadButtonCenter();
|
|
5273
|
-
const startCenter = mainCanvasSize.scale(.5);
|
|
5274
|
-
|
|
5275
|
-
// check each touch point
|
|
5276
|
-
for (const touch of e.touches)
|
|
5599
|
+
// draw right face buttons
|
|
5277
5600
|
{
|
|
5278
|
-
const
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
|
|
5283
|
-
}
|
|
5284
|
-
else if (buttonCenter.distance(touchPos) < touchGamepadSize)
|
|
5601
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
5602
|
+
const buttonSize = touchGamepadButtonCount > 1 ?
|
|
5603
|
+
touchGamepadSize/4 : touchGamepadSize/2;
|
|
5604
|
+
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
5285
5605
|
{
|
|
5286
|
-
|
|
5287
|
-
let button =
|
|
5288
|
-
|
|
5289
|
-
if (touchGamepadButtonCount === 1)
|
|
5290
|
-
button = 0;
|
|
5291
|
-
else if (touchGamepadButtonCount === 2)
|
|
5292
|
-
{
|
|
5293
|
-
const delta = buttonCenter.subtract(touchPos);
|
|
5294
|
-
button = -delta.x < delta.y ? 1 : 0;
|
|
5295
|
-
}
|
|
5606
|
+
const j = mod(i-1, 4);
|
|
5607
|
+
let button = touchGamepadButtonCount > 2 ?
|
|
5608
|
+
j : min(j, touchGamepadButtonCount-1);
|
|
5296
5609
|
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
5297
5610
|
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
touchGamepadButtons[9] = 1;
|
|
5611
|
+
const pos = touchGamepadButtonCount < 2 ? buttonCenter :
|
|
5612
|
+
buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
5613
|
+
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
5614
|
+
context.beginPath();
|
|
5615
|
+
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
5616
|
+
context.fill();
|
|
5617
|
+
context.stroke();
|
|
5306
5618
|
}
|
|
5307
5619
|
}
|
|
5308
|
-
}
|
|
5309
|
-
}
|
|
5310
|
-
|
|
5311
|
-
// render the touch gamepad, called automatically by the engine
|
|
5312
|
-
function touchGamepadRender()
|
|
5313
|
-
{
|
|
5314
|
-
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
5315
|
-
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
5316
|
-
return;
|
|
5317
|
-
|
|
5318
|
-
// fade off when not touching or paused
|
|
5319
|
-
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
5320
|
-
if (!alpha || paused)
|
|
5321
|
-
return;
|
|
5322
|
-
|
|
5323
|
-
// setup the canvas
|
|
5324
|
-
const context = overlayContext;
|
|
5325
|
-
context.save();
|
|
5326
|
-
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
5327
|
-
context.strokeStyle = '#fff';
|
|
5328
|
-
context.lineWidth = 3;
|
|
5329
|
-
|
|
5330
|
-
// draw left analog stick
|
|
5331
|
-
context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
5332
|
-
context.beginPath();
|
|
5333
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5334
|
-
if (touchGamepadAnalog) // draw circle shaped gamepad
|
|
5335
|
-
{
|
|
5336
|
-
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
5337
|
-
context.fill();
|
|
5338
|
-
context.stroke();
|
|
5339
|
-
}
|
|
5340
|
-
else // draw cross shaped gamepad
|
|
5341
|
-
{
|
|
5342
|
-
for (let i=10; i--;)
|
|
5343
|
-
{
|
|
5344
|
-
const angle = i*PI/4;
|
|
5345
|
-
context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
5346
|
-
i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
|
|
5347
|
-
i===1 && context.fill();
|
|
5348
|
-
}
|
|
5349
|
-
context.stroke();
|
|
5350
|
-
}
|
|
5351
5620
|
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
|
|
5355
|
-
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
5356
|
-
{
|
|
5357
|
-
const j = mod(i-1, 4);
|
|
5358
|
-
let button = touchGamepadButtonCount > 2 ?
|
|
5359
|
-
j : min(j, touchGamepadButtonCount-1);
|
|
5360
|
-
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
5361
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
5362
|
-
const pos = buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
5363
|
-
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
5364
|
-
context.beginPath();
|
|
5365
|
-
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
5366
|
-
context.fill();
|
|
5367
|
-
context.stroke();
|
|
5621
|
+
// set canvas back to normal
|
|
5622
|
+
context.restore();
|
|
5368
5623
|
}
|
|
5369
|
-
|
|
5370
|
-
// set canvas back to normal
|
|
5371
|
-
context.restore();
|
|
5372
5624
|
}
|
|
5373
5625
|
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
/** Request to lock the pointer, does not work on touch devices
|
|
5378
|
-
* @memberof Input */
|
|
5379
|
-
function pointerLockRequest()
|
|
5626
|
+
// center position for right tocuh pad face buttons
|
|
5627
|
+
function touchGamepadButtonCenter()
|
|
5380
5628
|
{
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
* @memberof Input */
|
|
5387
|
-
function pointerLockExit() { document.exitPointerLock && document.exitPointerLock(); }
|
|
5388
|
-
|
|
5389
|
-
/** Check if pointer is locked (true if locked)
|
|
5390
|
-
* @return {boolean}
|
|
5391
|
-
* @memberof Input */
|
|
5392
|
-
function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
|
|
5629
|
+
const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
|
|
5630
|
+
if (touchGamepadButtonCount === 2)
|
|
5631
|
+
center.x += touchGamepadSize/2;
|
|
5632
|
+
return center;
|
|
5633
|
+
}
|
|
5393
5634
|
/**
|
|
5394
5635
|
* LittleJS Audio System
|
|
5395
5636
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
@@ -5503,7 +5744,6 @@ class Sound
|
|
|
5503
5744
|
ASSERT(isNumber(pitch), 'pitch must be a number');
|
|
5504
5745
|
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
5505
5746
|
|
|
5506
|
-
|
|
5507
5747
|
if (!soundEnable || headlessMode) return;
|
|
5508
5748
|
if (!this.sampleChannels) return;
|
|
5509
5749
|
|
|
@@ -6130,7 +6370,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
|
|
|
6130
6370
|
}
|
|
6131
6371
|
}
|
|
6132
6372
|
|
|
6133
|
-
/** Return the exact position of the
|
|
6373
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6134
6374
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6135
6375
|
* @param {Vector2} posStart
|
|
6136
6376
|
* @param {Vector2} posEnd
|
|
@@ -6636,7 +6876,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6636
6876
|
// remove from collision layers array and destroy
|
|
6637
6877
|
const index = tileCollisionLayers.indexOf(this);
|
|
6638
6878
|
ASSERT(index >= 0, 'tile collision layer not found in array');
|
|
6639
|
-
tileCollisionLayers.splice(index, 1);
|
|
6879
|
+
index >= 0 && tileCollisionLayers.splice(index, 1);
|
|
6640
6880
|
super.destroy();
|
|
6641
6881
|
}
|
|
6642
6882
|
|
|
@@ -6704,7 +6944,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6704
6944
|
return false;
|
|
6705
6945
|
}
|
|
6706
6946
|
|
|
6707
|
-
/** Return the exact position of the
|
|
6947
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6708
6948
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6709
6949
|
* @param {Vector2} posStart
|
|
6710
6950
|
* @param {Vector2} posEnd
|
|
@@ -8649,11 +8889,14 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8649
8889
|
/**
|
|
8650
8890
|
* LittleJS User Interface Plugin
|
|
8651
8891
|
* - call new UISystemPlugin() to setup the UI system
|
|
8892
|
+
* - Gamepad and keyboard navigation support
|
|
8652
8893
|
* - Nested Menus
|
|
8653
8894
|
* - Text
|
|
8654
8895
|
* - Buttons
|
|
8655
8896
|
* - Checkboxes
|
|
8656
8897
|
* - Images
|
|
8898
|
+
* - Scrollbars
|
|
8899
|
+
* - Video
|
|
8657
8900
|
* @namespace UISystem
|
|
8658
8901
|
*/
|
|
8659
8902
|
|
|
@@ -8664,6 +8907,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8664
8907
|
* @memberof UISystem */
|
|
8665
8908
|
let uiSystem;
|
|
8666
8909
|
|
|
8910
|
+
/** Enable UI system debug drawing
|
|
8911
|
+
* 0=off, 1=normal, 2=show invisible
|
|
8912
|
+
* @type {number}
|
|
8913
|
+
* @default
|
|
8914
|
+
* @memberof UISystem */
|
|
8915
|
+
let uiDebug = 0;
|
|
8916
|
+
|
|
8917
|
+
/** Enable UI system debug drawing
|
|
8918
|
+
* 0=off, 1=normal, 2=show invisible
|
|
8919
|
+
* @param {number|boolean} enable
|
|
8920
|
+
* @memberof UISystem */
|
|
8921
|
+
function uiSetDebug(debugMode)
|
|
8922
|
+
{ uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
|
|
8923
|
+
|
|
8667
8924
|
///////////////////////////////////////////////////////////////////////////////
|
|
8668
8925
|
/**
|
|
8669
8926
|
* UI System Global Object
|
|
@@ -8682,6 +8939,7 @@ class UISystemPlugin
|
|
|
8682
8939
|
ASSERT(!uiSystem, 'UI system already initialized');
|
|
8683
8940
|
uiSystem = this;
|
|
8684
8941
|
|
|
8942
|
+
// default settings
|
|
8685
8943
|
/** @property {Color} - Default fill color for UI elements */
|
|
8686
8944
|
this.defaultColor = WHITE;
|
|
8687
8945
|
/** @property {Color} - Default outline color for UI elements */
|
|
@@ -8701,7 +8959,7 @@ class UISystemPlugin
|
|
|
8701
8959
|
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
8702
8960
|
this.defaultCornerRadius = 0;
|
|
8703
8961
|
/** @property {number} - Default scale to use for fitting text to object */
|
|
8704
|
-
this.
|
|
8962
|
+
this.defaultTextFitScale = .8;
|
|
8705
8963
|
/** @property {string} - Default font for UI elements */
|
|
8706
8964
|
this.defaultFont = fontDefault;
|
|
8707
8965
|
/** @property {Sound} - Default sound when interactive UI element is pressed */
|
|
@@ -8710,6 +8968,30 @@ class UISystemPlugin
|
|
|
8710
8968
|
this.defaultSoundRelease = undefined;
|
|
8711
8969
|
/** @property {Sound} - Default sound when interactive UI element is clicked */
|
|
8712
8970
|
this.defaultSoundClick = undefined;
|
|
8971
|
+
/** @property {Color} - Color for shadow */
|
|
8972
|
+
this.defaultShadowColor = CLEAR_BLACK;
|
|
8973
|
+
/** @property {number} - Size of shadow blur */
|
|
8974
|
+
this.defaultShadowBlur = 5;
|
|
8975
|
+
/** @property {Vector2} - Offset of shadow blur */
|
|
8976
|
+
this.defaultShadowOffset = vec2(5);
|
|
8977
|
+
/** @property {number} - If set ui coords will be renormalized to this canvas height */
|
|
8978
|
+
this.nativeHeight = 0;
|
|
8979
|
+
|
|
8980
|
+
// navigation properties
|
|
8981
|
+
/** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
|
|
8982
|
+
this.navigationObject = undefined;
|
|
8983
|
+
/** @property {number} - Gamepad index to use for UI navigation */
|
|
8984
|
+
this.navigationGamepadIndex = 0;
|
|
8985
|
+
/** @property {Timer} - Cooldown timer for navigation inputs */
|
|
8986
|
+
this.navigationTimer = new Timer(undefined, true);
|
|
8987
|
+
/** @property {number} - Time between navigation inputs in seconds */
|
|
8988
|
+
this.navigationDelay = .2;
|
|
8989
|
+
/** @property {boolean} - should the navigation be horizontal, vertical, or both? */
|
|
8990
|
+
this.navigationDirection = 1;
|
|
8991
|
+
/** @property {boolean} - True if user last used navigation instead of mouse */
|
|
8992
|
+
this.navigationMode = false;
|
|
8993
|
+
|
|
8994
|
+
// system state
|
|
8713
8995
|
/** @property {Array<UIObject>} - List of all UI elements */
|
|
8714
8996
|
this.uiObjects = [];
|
|
8715
8997
|
/** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
|
|
@@ -8720,50 +9002,118 @@ class UISystemPlugin
|
|
|
8720
9002
|
this.hoverObject = undefined;
|
|
8721
9003
|
/** @property {UIObject} - Hover object at start of update */
|
|
8722
9004
|
this.lastHoverObject = undefined;
|
|
8723
|
-
/** @property {
|
|
8724
|
-
this.
|
|
9005
|
+
/** @property {UIObject} - Current confirm menu being shown */
|
|
9006
|
+
this.confirmDialog = undefined;
|
|
8725
9007
|
|
|
8726
9008
|
engineAddPlugin(uiUpdate, uiRender);
|
|
8727
9009
|
|
|
9010
|
+
// set object position in parent space
|
|
9011
|
+
function updateTransforms(o)
|
|
9012
|
+
{
|
|
9013
|
+
if (!o.parent) return;
|
|
9014
|
+
o.pos.x = o.localPos.x + o.parent.pos.x;
|
|
9015
|
+
o.pos.y = o.localPos.y + o.parent.pos.y;
|
|
9016
|
+
}
|
|
9017
|
+
|
|
8728
9018
|
// setup recursive update and render
|
|
8729
9019
|
// update in reverse order to detect mouse enter/leave
|
|
8730
9020
|
function uiUpdate()
|
|
8731
9021
|
{
|
|
8732
|
-
|
|
9022
|
+
if (uiSystem.activeObject && !uiSystem.activeObject.visible)
|
|
9023
|
+
uiSystem.activeObject = undefined;
|
|
9024
|
+
|
|
9025
|
+
// reset hover object at start of update
|
|
9026
|
+
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
9027
|
+
uiSystem.hoverObject = undefined;
|
|
9028
|
+
|
|
9029
|
+
if (mouseWasPressed(0))
|
|
8733
9030
|
{
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
updateInvisibleObject(c);
|
|
8737
|
-
o.updateInvisible();
|
|
9031
|
+
uiSystem.navigationMode = false;
|
|
9032
|
+
uiSystem.navigationObject = undefined;
|
|
8738
9033
|
}
|
|
8739
|
-
|
|
9034
|
+
|
|
9035
|
+
// navigation with gamepad/keyboard
|
|
9036
|
+
const navigableObjects = uiSystem.getNavigableObjects();
|
|
9037
|
+
if (!navigableObjects.length)
|
|
9038
|
+
uiSystem.navigationObject = undefined;
|
|
9039
|
+
else
|
|
8740
9040
|
{
|
|
8741
|
-
if
|
|
9041
|
+
// unselect object if it is no longer navigable
|
|
9042
|
+
if (!navigableObjects.includes(uiSystem.navigationObject))
|
|
9043
|
+
uiSystem.navigationObject = undefined;
|
|
9044
|
+
|
|
9045
|
+
if (!isTouchDevice)
|
|
9046
|
+
if (uiSystem.navigationMode && !uiSystem.navigationObject)
|
|
8742
9047
|
{
|
|
8743
|
-
//
|
|
8744
|
-
|
|
8745
|
-
o.pos = o.localPos.add(o.parent.pos);
|
|
8746
|
-
// update in reverse order to detect mouse enter/leave
|
|
8747
|
-
for (let i=o.children.length; i--;)
|
|
8748
|
-
updateObject(o.children[i]);
|
|
8749
|
-
o.update();
|
|
9048
|
+
// select first auto focus object
|
|
9049
|
+
uiSystem.navigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
8750
9050
|
}
|
|
8751
|
-
|
|
8752
|
-
|
|
9051
|
+
|
|
9052
|
+
// navigate with dpad or left stick
|
|
9053
|
+
if (!uiSystem.navigationTimer.active())
|
|
9054
|
+
{
|
|
9055
|
+
// navigate through list with gamepad or keyboard
|
|
9056
|
+
const direction = sign(uiSystem.getNavigationDirection());
|
|
9057
|
+
if (direction)
|
|
9058
|
+
{
|
|
9059
|
+
let newNavigationObject;
|
|
9060
|
+
if (!uiSystem.navigationObject)
|
|
9061
|
+
{
|
|
9062
|
+
// use auto select object
|
|
9063
|
+
newNavigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
9064
|
+
|
|
9065
|
+
if (!newNavigationObject)
|
|
9066
|
+
{
|
|
9067
|
+
// try first or last object
|
|
9068
|
+
const newIndex = direction > 0 ? 0 : navigableObjects.length-1;
|
|
9069
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
9070
|
+
}
|
|
9071
|
+
}
|
|
9072
|
+
else
|
|
9073
|
+
{
|
|
9074
|
+
const currentIndex = navigableObjects.indexOf(uiSystem.navigationObject);
|
|
9075
|
+
const newIndex = mod(currentIndex + direction, navigableObjects.length);
|
|
9076
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
9077
|
+
}
|
|
9078
|
+
|
|
9079
|
+
if (uiSystem.navigationObject !== newNavigationObject)
|
|
9080
|
+
{
|
|
9081
|
+
uiSystem.navigationMode = true;
|
|
9082
|
+
uiSystem.hoverObject = undefined;
|
|
9083
|
+
uiSystem.navigationObject = newNavigationObject;
|
|
9084
|
+
uiSystem.navigationTimer.set(uiSystem.navigationDelay);
|
|
9085
|
+
newNavigationObject.soundPress &&
|
|
9086
|
+
newNavigationObject.soundPress.play();
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
}
|
|
9090
|
+
|
|
9091
|
+
// activate the navigation object when pressed
|
|
9092
|
+
if (uiSystem.navigationObject)
|
|
9093
|
+
if (uiSystem.getNavigationWasPressed())
|
|
9094
|
+
uiSystem.navigationObject.navigatePressed();
|
|
8753
9095
|
}
|
|
8754
|
-
// reset hover object at start of update
|
|
8755
|
-
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8756
|
-
uiSystem.hoverObject = undefined;
|
|
8757
9096
|
|
|
8758
9097
|
// update in reverse order so topmost objects get priority
|
|
8759
9098
|
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8760
9099
|
{
|
|
8761
9100
|
const o = uiSystem.uiObjects[i];
|
|
8762
|
-
o.parent || updateObject(o)
|
|
9101
|
+
o.parent || updateObject(o);
|
|
8763
9102
|
}
|
|
8764
9103
|
|
|
8765
9104
|
// remove destroyed objects
|
|
8766
9105
|
uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
|
|
9106
|
+
|
|
9107
|
+
function updateObject(o)
|
|
9108
|
+
{
|
|
9109
|
+
if (!o.visible) return;
|
|
9110
|
+
|
|
9111
|
+
// update in reverse order to detect mouse enter/leave
|
|
9112
|
+
updateTransforms(o);
|
|
9113
|
+
for (let i=o.children.length; i--;)
|
|
9114
|
+
updateObject(o.children[i]);
|
|
9115
|
+
o.update();
|
|
9116
|
+
}
|
|
8767
9117
|
}
|
|
8768
9118
|
function uiRender()
|
|
8769
9119
|
{
|
|
@@ -8780,15 +9130,29 @@ class UISystemPlugin
|
|
|
8780
9130
|
|
|
8781
9131
|
function renderObject(o)
|
|
8782
9132
|
{
|
|
8783
|
-
if (!o.visible)
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
9133
|
+
if (!o.visible) return;
|
|
9134
|
+
|
|
9135
|
+
// render object and children
|
|
9136
|
+
updateTransforms(o);
|
|
8787
9137
|
o.render();
|
|
8788
9138
|
for (const c of o.children)
|
|
8789
9139
|
renderObject(c);
|
|
8790
9140
|
}
|
|
8791
9141
|
uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
|
|
9142
|
+
|
|
9143
|
+
if (uiDebug > 0)
|
|
9144
|
+
{
|
|
9145
|
+
// debug render all objects
|
|
9146
|
+
function renderDebug(o, visible=true)
|
|
9147
|
+
{
|
|
9148
|
+
visible &&= !!o.visible;
|
|
9149
|
+
updateTransforms(o);
|
|
9150
|
+
o.renderDebug(visible);
|
|
9151
|
+
for (const c of o.children)
|
|
9152
|
+
renderDebug(c, visible);
|
|
9153
|
+
}
|
|
9154
|
+
uiSystem.uiObjects.forEach(o=> o.parent || renderDebug(o));
|
|
9155
|
+
}
|
|
8792
9156
|
context.restore();
|
|
8793
9157
|
}
|
|
8794
9158
|
}
|
|
@@ -8800,8 +9164,11 @@ class UISystemPlugin
|
|
|
8800
9164
|
* @param {number} [lineWidth]
|
|
8801
9165
|
* @param {Color} [lineColor]
|
|
8802
9166
|
* @param {number} [cornerRadius]
|
|
8803
|
-
* @param {Color} [gradientColor]
|
|
8804
|
-
|
|
9167
|
+
* @param {Color} [gradientColor]
|
|
9168
|
+
* @param {Color} [shadowColor]
|
|
9169
|
+
* @param {number} [shadowBlur]
|
|
9170
|
+
* @param {Color} [shadowOffset] */
|
|
9171
|
+
drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
8805
9172
|
{
|
|
8806
9173
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
8807
9174
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -8823,13 +9190,23 @@ class UISystemPlugin
|
|
|
8823
9190
|
}
|
|
8824
9191
|
else
|
|
8825
9192
|
context.fillStyle = color.toString();
|
|
9193
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
9194
|
+
if (shadowColor.a > 0)
|
|
9195
|
+
{
|
|
9196
|
+
// setup shadow
|
|
9197
|
+
context.shadowColor = shadowColor.toString();
|
|
9198
|
+
context.shadowBlur = shadowBlur;
|
|
9199
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
9200
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
9201
|
+
}
|
|
8826
9202
|
context.beginPath();
|
|
8827
9203
|
if (cornerRadius && context['roundRect'])
|
|
8828
9204
|
context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
|
|
8829
9205
|
else
|
|
8830
9206
|
context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
|
|
8831
9207
|
context.fill();
|
|
8832
|
-
|
|
9208
|
+
context.shadowColor = '#0000';
|
|
9209
|
+
if (lineWidth && lineColor.a > 0)
|
|
8833
9210
|
{
|
|
8834
9211
|
context.strokeStyle = lineColor.toString();
|
|
8835
9212
|
context.lineWidth = lineWidth;
|
|
@@ -8864,10 +9241,24 @@ class UISystemPlugin
|
|
|
8864
9241
|
* @param {TileInfo} tileInfo
|
|
8865
9242
|
* @param {Color} [color=uiSystem.defaultColor]
|
|
8866
9243
|
* @param {number} [angle]
|
|
8867
|
-
* @param {boolean} [mirror]
|
|
8868
|
-
|
|
9244
|
+
* @param {boolean} [mirror]
|
|
9245
|
+
* @param {Color} [shadowColor]
|
|
9246
|
+
* @param {number} [shadowBlur]
|
|
9247
|
+
* @param {Color} [shadowOffset] */
|
|
9248
|
+
drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
8869
9249
|
{
|
|
8870
|
-
|
|
9250
|
+
const context = uiSystem.uiContext;
|
|
9251
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
9252
|
+
if (shadowColor.a > 0)
|
|
9253
|
+
{
|
|
9254
|
+
// setup shadow
|
|
9255
|
+
context.shadowColor = shadowColor.toString();
|
|
9256
|
+
context.shadowBlur = shadowBlur;
|
|
9257
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
9258
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
9259
|
+
}
|
|
9260
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, context);
|
|
9261
|
+
context.shadowColor = '#0000';
|
|
8871
9262
|
}
|
|
8872
9263
|
|
|
8873
9264
|
/** Draw text to the UI context
|
|
@@ -8882,12 +9273,27 @@ class UISystemPlugin
|
|
|
8882
9273
|
* @param {string} [fontStyle]
|
|
8883
9274
|
* @param {boolean} [applyMaxWidth=true]
|
|
8884
9275
|
* @param {Vector2} [textShadow]
|
|
8885
|
-
|
|
8886
|
-
|
|
9276
|
+
* @param {Color} [shadowColor]
|
|
9277
|
+
* @param {number} [shadowBlur]
|
|
9278
|
+
* @param {Color} [shadowOffset] */
|
|
9279
|
+
drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
8887
9280
|
{
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
9281
|
+
const context = uiSystem.uiContext;
|
|
9282
|
+
if (shadowColor.a > 0)
|
|
9283
|
+
{
|
|
9284
|
+
if (textShadow)
|
|
9285
|
+
drawTextScreen(text, pos.add(textShadow), size.y, shadowColor, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
9286
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
9287
|
+
{
|
|
9288
|
+
// setup shadow
|
|
9289
|
+
context.shadowColor = shadowColor.toString();
|
|
9290
|
+
context.shadowBlur = shadowBlur;
|
|
9291
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
9292
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
9296
|
+
context.shadowColor = '#0000';
|
|
8891
9297
|
}
|
|
8892
9298
|
|
|
8893
9299
|
/**
|
|
@@ -8901,7 +9307,7 @@ class UISystemPlugin
|
|
|
8901
9307
|
* @param {DragAndDropCallback} [onDrop] - when a file is dropped
|
|
8902
9308
|
* @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
|
|
8903
9309
|
* @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
|
|
8904
|
-
* @param {DragAndDropCallback} [onDragOver] -
|
|
9310
|
+
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
8905
9311
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
8906
9312
|
{
|
|
8907
9313
|
function setCallback(callback, listenerType)
|
|
@@ -8917,8 +9323,7 @@ class UISystemPlugin
|
|
|
8917
9323
|
|
|
8918
9324
|
/** Convert a screen space position to native UI position
|
|
8919
9325
|
* @param {Vector2} pos
|
|
8920
|
-
* @return {Vector2}
|
|
8921
|
-
*/
|
|
9326
|
+
* @return {Vector2} */
|
|
8922
9327
|
screenToNative(pos)
|
|
8923
9328
|
{
|
|
8924
9329
|
if (!uiSystem.nativeHeight)
|
|
@@ -8941,6 +9346,160 @@ class UISystemPlugin
|
|
|
8941
9346
|
for (const o of this.uiObjects)
|
|
8942
9347
|
o.parent || o.destroy();
|
|
8943
9348
|
this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
|
|
9349
|
+
this.activeObject = undefined;
|
|
9350
|
+
this.hoverObject = undefined;
|
|
9351
|
+
this.lastHoverObject = undefined;
|
|
9352
|
+
}
|
|
9353
|
+
|
|
9354
|
+
/** Get all navigable UI objects sorted by navigationIndex
|
|
9355
|
+
* @return {Array<UIObject>} */
|
|
9356
|
+
getNavigableObjects()
|
|
9357
|
+
{
|
|
9358
|
+
function getNavigableRecursive(o)
|
|
9359
|
+
{
|
|
9360
|
+
if (!o.visible || o.disabled)
|
|
9361
|
+
return; // skip children if parent is invisible or disabled
|
|
9362
|
+
|
|
9363
|
+
if (o.isInteractive() && o.navigationIndex !== undefined)
|
|
9364
|
+
objects.push(o);
|
|
9365
|
+
for (let i=o.children.length; i--;)
|
|
9366
|
+
getNavigableRecursive(o.children[i]);
|
|
9367
|
+
}
|
|
9368
|
+
|
|
9369
|
+
// get all the valid navigable objects recursively
|
|
9370
|
+
let objects = [];
|
|
9371
|
+
for (let i = uiSystem.uiObjects.length; i--;)
|
|
9372
|
+
{
|
|
9373
|
+
const o = uiSystem.uiObjects[i];
|
|
9374
|
+
if (uiSystem.confirmDialog && o !== uiSystem.confirmDialog)
|
|
9375
|
+
continue;
|
|
9376
|
+
o.parent || getNavigableRecursive(o);
|
|
9377
|
+
}
|
|
9378
|
+
|
|
9379
|
+
// sort by navigationIndex (lower numbers first)
|
|
9380
|
+
objects.sort((a, b)=> a.navigationIndex - b.navigationIndex);
|
|
9381
|
+
return objects;
|
|
9382
|
+
}
|
|
9383
|
+
|
|
9384
|
+
/** Get navigation direction from gamepad or keyboard
|
|
9385
|
+
* @return {number} */
|
|
9386
|
+
getNavigationDirection()
|
|
9387
|
+
{
|
|
9388
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
9389
|
+
const both = uiSystem.navigationDirection === 2;
|
|
9390
|
+
if (isUsingGamepad)
|
|
9391
|
+
{
|
|
9392
|
+
const gamepad = this.navigationGamepadIndex;
|
|
9393
|
+
const stick = gamepadStick(0, gamepad);
|
|
9394
|
+
const dpad = gamepadDpad(gamepad);
|
|
9395
|
+
if (both)
|
|
9396
|
+
return -(stick.y || dpad.y) || (stick.x || dpad.x);
|
|
9397
|
+
return vertical ? -(stick.y || dpad.y) : (stick.x || dpad.x);
|
|
9398
|
+
}
|
|
9399
|
+
const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
|
|
9400
|
+
if (both)
|
|
9401
|
+
{
|
|
9402
|
+
return keyIsDown(up) || keyIsDown(left) ? -1 :
|
|
9403
|
+
keyIsDown(down) || keyIsDown(right) ? 1 : 0;
|
|
9404
|
+
}
|
|
9405
|
+
const back = vertical ? up : left;
|
|
9406
|
+
const forward = vertical ? down : right;
|
|
9407
|
+
return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
|
|
9408
|
+
}
|
|
9409
|
+
|
|
9410
|
+
/** Get other axis navigation direction from gamepad or keyboard
|
|
9411
|
+
* @return {Vector2} */
|
|
9412
|
+
getNavigationOtherDirection()
|
|
9413
|
+
{
|
|
9414
|
+
if (uiSystem.navigationDirection === 2)
|
|
9415
|
+
return 0; // other direction disabled
|
|
9416
|
+
|
|
9417
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
9418
|
+
if (isUsingGamepad)
|
|
9419
|
+
{
|
|
9420
|
+
const gamepad = this.navigationGamepadIndex;
|
|
9421
|
+
const stick = gamepadStick(0, gamepad);
|
|
9422
|
+
const dpad = gamepadDpad(gamepad);
|
|
9423
|
+
return !vertical ? (stick.y || dpad.y) : (stick.x || dpad.x);
|
|
9424
|
+
}
|
|
9425
|
+
const back = !vertical ? 'ArrowUp' : 'ArrowLeft';
|
|
9426
|
+
const forward = !vertical ? 'ArrowDown' : 'ArrowRight';
|
|
9427
|
+
return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
|
|
9428
|
+
}
|
|
9429
|
+
|
|
9430
|
+
/** Get if navigation button was pressed from gamepad or keyboard
|
|
9431
|
+
* @return {boolean} */
|
|
9432
|
+
getNavigationWasPressed()
|
|
9433
|
+
{
|
|
9434
|
+
const gamepad = this.navigationGamepadIndex;
|
|
9435
|
+
return isUsingGamepad ? gamepadWasPressed(0, gamepad) :
|
|
9436
|
+
keyWasPressed('Space') || keyWasPressed('Enter');
|
|
9437
|
+
}
|
|
9438
|
+
|
|
9439
|
+
/** Show a confirmation dialog with Yes/No buttons
|
|
9440
|
+
* Centers the dialog on the screen with darkened background
|
|
9441
|
+
* @param {string} [text] - The message to display
|
|
9442
|
+
* @param {Function} [yesCallback] - Called when Yes is clicked
|
|
9443
|
+
* @param {Function} [noCallback] - Called when No is clicked
|
|
9444
|
+
* @param {Vector2} [size] - Size of the confirmation dialog
|
|
9445
|
+
* @param {string} [exitKey] - Key that can exit the menu
|
|
9446
|
+
* @return {UIObject} The confirmation menu object
|
|
9447
|
+
*/
|
|
9448
|
+
showConfirmDialog(text='Are you sure?', yesCallback, noCallback, size=vec2(500,250), exitKey='Escape')
|
|
9449
|
+
{
|
|
9450
|
+
ASSERT(!uiSystem.confirmDialog);
|
|
9451
|
+
|
|
9452
|
+
const savedNavigationDirection = uiSystem.navigationDirection;
|
|
9453
|
+
|
|
9454
|
+
// allow both axies for navigation
|
|
9455
|
+
uiSystem.navigationDirection = 2;
|
|
9456
|
+
|
|
9457
|
+
// confirm menu
|
|
9458
|
+
const confirmMenu = new UIObject(vec2(), size);
|
|
9459
|
+
uiSystem.confirmDialog = confirmMenu;
|
|
9460
|
+
confirmMenu.onRender = ()=>
|
|
9461
|
+
{
|
|
9462
|
+
confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
9463
|
+
const backgroundColor = hsl(0,0,0,.7);
|
|
9464
|
+
uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
|
|
9465
|
+
}
|
|
9466
|
+
confirmMenu.onUpdate = ()=>
|
|
9467
|
+
{
|
|
9468
|
+
if (keyWasPressed(exitKey))
|
|
9469
|
+
closeMenu();
|
|
9470
|
+
}
|
|
9471
|
+
confirmMenu.isMouseOverlapping = ()=> true; // always hover
|
|
9472
|
+
|
|
9473
|
+
// title text
|
|
9474
|
+
const gap = 50;
|
|
9475
|
+
const textTitle = new UIText(vec2(0,-50), vec2(size.x-gap,70), text);
|
|
9476
|
+
confirmMenu.addChild(textTitle);
|
|
9477
|
+
|
|
9478
|
+
// yes button
|
|
9479
|
+
const buttonYes = new UIButton(vec2(-80,50), vec2(120,70), 'Yes');
|
|
9480
|
+
buttonYes.textHeight = 40;
|
|
9481
|
+
buttonYes.navigationIndex = 1;
|
|
9482
|
+
buttonYes.hoverColor = hsl(0,1,.5);
|
|
9483
|
+
buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
|
|
9484
|
+
confirmMenu.addChild(buttonYes);
|
|
9485
|
+
|
|
9486
|
+
// no button
|
|
9487
|
+
const buttonNo = new UIButton(vec2(80,50), vec2(120,70), 'No');
|
|
9488
|
+
buttonNo.textHeight = 40;
|
|
9489
|
+
buttonNo.navigationIndex = 2;
|
|
9490
|
+
buttonNo.navigationAutoSelect = true;
|
|
9491
|
+
buttonNo.onClick = ()=> { closeMenu(); noCallback && noCallback(); };
|
|
9492
|
+
confirmMenu.addChild(buttonNo);
|
|
9493
|
+
|
|
9494
|
+
// close menu and return to normal navigation
|
|
9495
|
+
function closeMenu()
|
|
9496
|
+
{
|
|
9497
|
+
ASSERT(uiSystem.confirmDialog === confirmMenu);
|
|
9498
|
+
confirmMenu.destroy();
|
|
9499
|
+
uiSystem.confirmDialog = undefined;
|
|
9500
|
+
uiSystem.navigationDirection = savedNavigationDirection;
|
|
9501
|
+
inputClear();
|
|
9502
|
+
}
|
|
8944
9503
|
}
|
|
8945
9504
|
}
|
|
8946
9505
|
|
|
@@ -8996,7 +9555,13 @@ class UIObject
|
|
|
8996
9555
|
/** @property {number} - Override for text height */
|
|
8997
9556
|
this.textHeight = undefined;
|
|
8998
9557
|
/** @property {number} - Scale text to fit in the object */
|
|
8999
|
-
this.
|
|
9558
|
+
this.textFitScale = uiSystem.defaultTextFitScale;
|
|
9559
|
+
/** @property {Vector2} - How much to offset the text shadow or undefined */
|
|
9560
|
+
this.textShadow = undefined;
|
|
9561
|
+
/** @property {number} - Color for text line drawing */
|
|
9562
|
+
this.textLineColor = uiSystem.defaultLineColor.copy();
|
|
9563
|
+
/** @property {number} - Width for text line drawing */
|
|
9564
|
+
this.textLineWidth = 0;
|
|
9000
9565
|
/** @property {boolean} - Should this object be drawn */
|
|
9001
9566
|
this.visible = true;
|
|
9002
9567
|
/** @property {Array<UIObject>} - A list of this object's children */
|
|
@@ -9017,15 +9582,22 @@ class UIObject
|
|
|
9017
9582
|
this.dragActivate = false;
|
|
9018
9583
|
/** @property {boolean} - True if this can be a hover object */
|
|
9019
9584
|
this.canBeHover = true;
|
|
9020
|
-
|
|
9585
|
+
/** @property {Color} - Color for shadow, undefined if no shadow */
|
|
9586
|
+
this.shadowColor = uiSystem.defaultShadowColor?.copy();
|
|
9587
|
+
/** @property {number} - Size of shadow blur */
|
|
9588
|
+
this.shadowBlur = uiSystem.defaultShadowBlur;
|
|
9589
|
+
/** @property {Vector2} - Offset of shadow blur */
|
|
9590
|
+
this.shadowOffset = uiSystem.defaultShadowOffset?.copy();
|
|
9591
|
+
/** @property {number} - Optional navigation order index, lower values are selected first */
|
|
9592
|
+
this.navigationIndex = undefined;
|
|
9593
|
+
/** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
|
|
9594
|
+
this.navigationAutoSelect = false;
|
|
9021
9595
|
|
|
9022
|
-
|
|
9023
|
-
this.textShadow = undefined;
|
|
9596
|
+
uiSystem.uiObjects.push(this);
|
|
9024
9597
|
}
|
|
9025
9598
|
|
|
9026
9599
|
/** Add a child UIObject to this object
|
|
9027
|
-
* @param {UIObject} child
|
|
9028
|
-
*/
|
|
9600
|
+
* @param {UIObject} child */
|
|
9029
9601
|
addChild(child)
|
|
9030
9602
|
{
|
|
9031
9603
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
@@ -9034,8 +9606,7 @@ class UIObject
|
|
|
9034
9606
|
}
|
|
9035
9607
|
|
|
9036
9608
|
/** Remove a child UIObject from this object
|
|
9037
|
-
* @param {UIObject} child
|
|
9038
|
-
*/
|
|
9609
|
+
* @param {UIObject} child */
|
|
9039
9610
|
removeChild(child)
|
|
9040
9611
|
{
|
|
9041
9612
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
@@ -9043,7 +9614,6 @@ class UIObject
|
|
|
9043
9614
|
child.parent = undefined;
|
|
9044
9615
|
}
|
|
9045
9616
|
|
|
9046
|
-
|
|
9047
9617
|
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
9048
9618
|
destroy()
|
|
9049
9619
|
{
|
|
@@ -9059,9 +9629,9 @@ class UIObject
|
|
|
9059
9629
|
child.destroy();
|
|
9060
9630
|
}
|
|
9061
9631
|
}
|
|
9632
|
+
|
|
9062
9633
|
/** Check if the mouse is overlapping a box in screen space
|
|
9063
|
-
* @return {boolean} - True if overlapping
|
|
9064
|
-
*/
|
|
9634
|
+
* @return {boolean} - True if overlapping */
|
|
9065
9635
|
isMouseOverlapping()
|
|
9066
9636
|
{
|
|
9067
9637
|
if (!mouseInWindow) return false;
|
|
@@ -9078,11 +9648,16 @@ class UIObject
|
|
|
9078
9648
|
// call the custom update callback
|
|
9079
9649
|
this.onUpdate();
|
|
9080
9650
|
|
|
9651
|
+
// unset active if disabled
|
|
9652
|
+
if (this.disabled && this == uiSystem.activeObject)
|
|
9653
|
+
uiSystem.activeObject = undefined;
|
|
9654
|
+
|
|
9081
9655
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
9082
9656
|
const isActive = this.isActiveObject();
|
|
9083
9657
|
const mouseDown = mouseIsDown(0);
|
|
9084
9658
|
const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
|
|
9085
9659
|
if (this.canBeHover)
|
|
9660
|
+
if (!uiSystem.navigationMode) // no mouse hover in navigation mode
|
|
9086
9661
|
if (mousePress || isActive || (!mouseDown && !isTouchDevice))
|
|
9087
9662
|
if (!uiSystem.hoverObject && this.isMouseOverlapping())
|
|
9088
9663
|
uiSystem.hoverObject = this;
|
|
@@ -9096,8 +9671,7 @@ class UIObject
|
|
|
9096
9671
|
{
|
|
9097
9672
|
if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
|
|
9098
9673
|
this.onPress();
|
|
9099
|
-
|
|
9100
|
-
this.soundPress.play();
|
|
9674
|
+
this.soundPress && this.soundPress.play();
|
|
9101
9675
|
if (uiSystem.activeObject && !isActive)
|
|
9102
9676
|
uiSystem.activeObject.onRelease();
|
|
9103
9677
|
uiSystem.activeObject = this;
|
|
@@ -9106,10 +9680,10 @@ class UIObject
|
|
|
9106
9680
|
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
9107
9681
|
{
|
|
9108
9682
|
this.onClick();
|
|
9109
|
-
|
|
9110
|
-
this.soundClick.play();
|
|
9683
|
+
this.soundClick && this.soundClick.play();
|
|
9111
9684
|
}
|
|
9112
9685
|
}
|
|
9686
|
+
|
|
9113
9687
|
// clear mouse was pressed state even when disabled
|
|
9114
9688
|
mousePress && inputClearKey(0,0,0,1,0);
|
|
9115
9689
|
}
|
|
@@ -9117,8 +9691,7 @@ class UIObject
|
|
|
9117
9691
|
if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
|
|
9118
9692
|
{
|
|
9119
9693
|
this.onRelease();
|
|
9120
|
-
|
|
9121
|
-
this.soundRelease.play();
|
|
9694
|
+
this.soundRelease && this.soundRelease.play();
|
|
9122
9695
|
uiSystem.activeObject = undefined;
|
|
9123
9696
|
}
|
|
9124
9697
|
|
|
@@ -9130,29 +9703,40 @@ class UIObject
|
|
|
9130
9703
|
/** Render the object, called automatically by plugin once each frame */
|
|
9131
9704
|
render()
|
|
9132
9705
|
{
|
|
9133
|
-
|
|
9706
|
+
// call the custom render callback
|
|
9707
|
+
this.onRender();
|
|
9134
9708
|
|
|
9135
|
-
|
|
9136
|
-
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
9137
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
|
|
9138
|
-
}
|
|
9709
|
+
if (!this.size.x || !this.size.y) return;
|
|
9139
9710
|
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9711
|
+
const isNavigationObject = this.isNavigationObject();
|
|
9712
|
+
const lineColor = isNavigationObject ? this.color :
|
|
9713
|
+
this.interactive && this.isActiveObject() && !this.disabled ?
|
|
9714
|
+
this.color : this.lineColor;
|
|
9715
|
+
const color = isNavigationObject ? this.hoverColor :
|
|
9716
|
+
this.disabled ? this.disabledColor :
|
|
9717
|
+
this.interactive ?
|
|
9718
|
+
this.isHoverObject() ? this.hoverColor :
|
|
9719
|
+
this.isActiveObject() ? this.activeColor || this.color :
|
|
9720
|
+
this.color : this.color;
|
|
9721
|
+
const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
|
|
9722
|
+
|
|
9723
|
+
uiSystem.drawRect(this.pos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
9146
9724
|
}
|
|
9147
9725
|
|
|
9148
9726
|
/** Get the size for text with overrides and scale
|
|
9149
|
-
* @return {Vector2}
|
|
9150
|
-
*/
|
|
9727
|
+
* @return {Vector2} */
|
|
9151
9728
|
getTextSize()
|
|
9152
9729
|
{
|
|
9153
9730
|
return vec2(
|
|
9154
|
-
this.textWidth || this.
|
|
9155
|
-
this.textHeight || this.
|
|
9731
|
+
this.textWidth || this.textFitScale * this.size.x,
|
|
9732
|
+
this.textHeight || this.textFitScale * this.size.y);
|
|
9733
|
+
}
|
|
9734
|
+
|
|
9735
|
+
/** Called when the navigation button is pressed on this object */
|
|
9736
|
+
navigatePressed()
|
|
9737
|
+
{
|
|
9738
|
+
this.onClick();
|
|
9739
|
+
this.soundClick && this.soundClick.play();
|
|
9156
9740
|
}
|
|
9157
9741
|
|
|
9158
9742
|
/** @return {boolean} - Is the mouse hovering over this element */
|
|
@@ -9161,9 +9745,51 @@ class UIObject
|
|
|
9161
9745
|
/** @return {boolean} - Is the mouse held onto this element */
|
|
9162
9746
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
9163
9747
|
|
|
9164
|
-
/**
|
|
9748
|
+
/** @return {boolean} - Is the gamepad or keyboard navigation object */
|
|
9749
|
+
isNavigationObject() { return uiSystem.navigationObject === this; }
|
|
9750
|
+
|
|
9751
|
+
/** @return {boolean} - Can it be interacted with */
|
|
9752
|
+
isInteractive() { return this.interactive && this.visible && !this.disabled;}
|
|
9753
|
+
|
|
9754
|
+
/** Returns string containing info about this object for debugging
|
|
9755
|
+
* @return {string} */
|
|
9756
|
+
toString()
|
|
9757
|
+
{
|
|
9758
|
+
if (!debug) return;
|
|
9759
|
+
|
|
9760
|
+
let text = 'type = ' + this.constructor.name;
|
|
9761
|
+
if (this.text)
|
|
9762
|
+
text += '\ntext = ' + this.text;
|
|
9763
|
+
if (this.pos.x || this.pos.y)
|
|
9764
|
+
text += '\npos = ' + this.pos;
|
|
9765
|
+
if (this.localPos.x || this.localPos.y)
|
|
9766
|
+
text += '\localPos = ' + this.localPos;
|
|
9767
|
+
if (this.size.x || this.size.y)
|
|
9768
|
+
text += '\nsize = ' + this.size;
|
|
9769
|
+
if (this.color)
|
|
9770
|
+
text += '\ncolor = ' + this.color;
|
|
9771
|
+
return text;
|
|
9772
|
+
}
|
|
9773
|
+
|
|
9774
|
+
/** Called if uiDebug is enabled
|
|
9775
|
+
* @param {boolean} visible */
|
|
9776
|
+
renderDebug(visible=true)
|
|
9777
|
+
{
|
|
9778
|
+
// apply color based on state
|
|
9779
|
+
const color =
|
|
9780
|
+
!visible ? GREEN :
|
|
9781
|
+
this.isHoverObject() ? YELLOW :
|
|
9782
|
+
this.disabled ? PURPLE :
|
|
9783
|
+
this.interactive ? RED : BLUE;
|
|
9784
|
+
uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
|
|
9785
|
+
}
|
|
9786
|
+
|
|
9787
|
+
/** Called each frame before object updates */
|
|
9165
9788
|
onUpdate() {}
|
|
9166
9789
|
|
|
9790
|
+
/** Called each frame before object renders */
|
|
9791
|
+
onRender() {}
|
|
9792
|
+
|
|
9167
9793
|
/** Called when the mouse enters the object */
|
|
9168
9794
|
onEnter() {}
|
|
9169
9795
|
|
|
@@ -9211,16 +9837,25 @@ class UIText extends UIObject
|
|
|
9211
9837
|
this.align = align;
|
|
9212
9838
|
this.font = font;
|
|
9213
9839
|
|
|
9214
|
-
// make text not outlined by default
|
|
9215
|
-
this.lineWidth = 0;
|
|
9216
9840
|
// text can not be a hover object by default
|
|
9217
9841
|
this.canBeHover = false;
|
|
9842
|
+
|
|
9843
|
+
// no background by default
|
|
9844
|
+
this.color = CLEAR_BLACK;
|
|
9845
|
+
this.shadowColor = CLEAR_BLACK;
|
|
9846
|
+
this.gradientColor = undefined;
|
|
9847
|
+
this.lineWidth = 0;
|
|
9848
|
+
|
|
9849
|
+
// use max fit scale by default
|
|
9850
|
+
this.textFitScale = 1;
|
|
9218
9851
|
}
|
|
9219
9852
|
render()
|
|
9220
9853
|
{
|
|
9221
|
-
|
|
9854
|
+
super.render();
|
|
9855
|
+
|
|
9856
|
+
// render the text
|
|
9222
9857
|
const textSize = this.getTextSize();
|
|
9223
|
-
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.
|
|
9858
|
+
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);
|
|
9224
9859
|
}
|
|
9225
9860
|
}
|
|
9226
9861
|
|
|
@@ -9256,10 +9891,13 @@ class UITile extends UIObject
|
|
|
9256
9891
|
this.mirror = mirror;
|
|
9257
9892
|
// set properties
|
|
9258
9893
|
this.color = color.copy();
|
|
9894
|
+
|
|
9895
|
+
// no shadow by default
|
|
9896
|
+
this.shadowColor = CLEAR_BLACK;
|
|
9259
9897
|
}
|
|
9260
9898
|
render()
|
|
9261
9899
|
{
|
|
9262
|
-
uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
|
|
9900
|
+
uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
9263
9901
|
}
|
|
9264
9902
|
}
|
|
9265
9903
|
|
|
@@ -9284,6 +9922,9 @@ class UIButton extends UIObject
|
|
|
9284
9922
|
ASSERT(isString(text), 'ui button must be a string');
|
|
9285
9923
|
ASSERT(isColor(color), 'ui button color must be a color');
|
|
9286
9924
|
|
|
9925
|
+
/** @property {Vector2} - Text offset for the button */
|
|
9926
|
+
this.textOffset = vec2();
|
|
9927
|
+
|
|
9287
9928
|
// set properties
|
|
9288
9929
|
this.text = text;
|
|
9289
9930
|
this.color = color.copy();
|
|
@@ -9295,8 +9936,8 @@ class UIButton extends UIObject
|
|
|
9295
9936
|
|
|
9296
9937
|
// draw the text scaled to fit
|
|
9297
9938
|
const textSize = this.getTextSize();
|
|
9298
|
-
uiSystem.drawText(this.text, this.pos, textSize,
|
|
9299
|
-
this.textColor,
|
|
9939
|
+
uiSystem.drawText(this.text, this.pos.add(this.textOffset), textSize,
|
|
9940
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
9300
9941
|
}
|
|
9301
9942
|
}
|
|
9302
9943
|
|
|
@@ -9350,7 +9991,7 @@ class UICheckbox extends UIObject
|
|
|
9350
9991
|
const textSize = this.getTextSize();
|
|
9351
9992
|
const pos = this.pos.add(vec2(this.size.x,0));
|
|
9352
9993
|
uiSystem.drawText(this.text, pos, textSize,
|
|
9353
|
-
this.textColor,
|
|
9994
|
+
this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
9354
9995
|
}
|
|
9355
9996
|
}
|
|
9356
9997
|
|
|
@@ -9392,7 +10033,11 @@ class UIScrollbar extends UIObject
|
|
|
9392
10033
|
update()
|
|
9393
10034
|
{
|
|
9394
10035
|
super.update();
|
|
9395
|
-
if (this.
|
|
10036
|
+
if (!this.interactive)
|
|
10037
|
+
return;
|
|
10038
|
+
|
|
10039
|
+
const oldValue = this.value;
|
|
10040
|
+
if (this.isActiveObject())
|
|
9396
10041
|
{
|
|
9397
10042
|
// handle horizontal or vertical scrollbar
|
|
9398
10043
|
const isHorizontal = this.size.x > this.size.y;
|
|
@@ -9404,14 +10049,19 @@ class UIScrollbar extends UIObject
|
|
|
9404
10049
|
const handleWidth = barSize - handleSize;
|
|
9405
10050
|
const p1 = centerPos - handleWidth/2;
|
|
9406
10051
|
const p2 = centerPos + handleWidth/2;
|
|
9407
|
-
const oldValue = this.value;
|
|
9408
|
-
|
|
9409
10052
|
const p = uiSystem.screenToNative(mousePosScreen);
|
|
9410
10053
|
this.value = isHorizontal ?
|
|
9411
10054
|
percent(p.x, p1, p2) :
|
|
9412
10055
|
percent(p.y, p2, p1);
|
|
9413
|
-
this.value === oldValue || this.onChange();
|
|
9414
10056
|
}
|
|
10057
|
+
else if (this.isNavigationObject())
|
|
10058
|
+
{
|
|
10059
|
+
// gamepad/keyboard navigation adjustment
|
|
10060
|
+
const direction = uiSystem.getNavigationOtherDirection();
|
|
10061
|
+
if (!uiSystem.navigationTimer.active())
|
|
10062
|
+
this.value = clamp(this.value + direction*.01);
|
|
10063
|
+
}
|
|
10064
|
+
this.value === oldValue || this.onChange();
|
|
9415
10065
|
}
|
|
9416
10066
|
render()
|
|
9417
10067
|
{
|
|
@@ -9436,7 +10086,14 @@ class UIScrollbar extends UIObject
|
|
|
9436
10086
|
// draw the text scaled to fit on the scrollbar
|
|
9437
10087
|
const textSize = this.getTextSize();
|
|
9438
10088
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
9439
|
-
this.textColor,
|
|
10089
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
10090
|
+
}
|
|
10091
|
+
navigatePressed()
|
|
10092
|
+
{
|
|
10093
|
+
// toggle value between 0 and 1
|
|
10094
|
+
this.value = this.value ? 0 : 1;
|
|
10095
|
+
this.onRelease();
|
|
10096
|
+
super.navigatePressed();
|
|
9440
10097
|
}
|
|
9441
10098
|
}
|
|
9442
10099
|
|
|
@@ -9470,7 +10127,7 @@ class UIVideo extends UIObject
|
|
|
9470
10127
|
this.color = BLACK; // default to black background
|
|
9471
10128
|
this.cornerRadius = 0; // default to no corner radius
|
|
9472
10129
|
|
|
9473
|
-
/** @property {
|
|
10130
|
+
/** @property {number} - The video volume */
|
|
9474
10131
|
this.volume = volume;
|
|
9475
10132
|
|
|
9476
10133
|
// create video element
|
|
@@ -9503,7 +10160,7 @@ class UIVideo extends UIObject
|
|
|
9503
10160
|
|
|
9504
10161
|
/** Check if video is currently loading
|
|
9505
10162
|
* @return {boolean} */
|
|
9506
|
-
|
|
10163
|
+
isLoading()
|
|
9507
10164
|
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
9508
10165
|
|
|
9509
10166
|
/** Check if video is currently paused
|
|
@@ -9513,7 +10170,7 @@ class UIVideo extends UIObject
|
|
|
9513
10170
|
/** Check if video is currently playing
|
|
9514
10171
|
* @return {boolean} */
|
|
9515
10172
|
isPlaying()
|
|
9516
|
-
{ return !this.isPaused() && !this.hasEnded() && !this.
|
|
10173
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoading(); }
|
|
9517
10174
|
|
|
9518
10175
|
/** Check if video has ended playing
|
|
9519
10176
|
* @return {boolean} */
|
|
@@ -9562,7 +10219,7 @@ class UIVideo extends UIObject
|
|
|
9562
10219
|
{
|
|
9563
10220
|
super.render();
|
|
9564
10221
|
|
|
9565
|
-
if (this.
|
|
10222
|
+
if (this.isLoading())
|
|
9566
10223
|
return;
|
|
9567
10224
|
const context = uiSystem.uiContext;
|
|
9568
10225
|
const s = this.size;
|
|
@@ -11346,7 +12003,7 @@ class Box2dPlugin
|
|
|
11346
12003
|
* @param {Vector2} v */
|
|
11347
12004
|
vec2dTo(v)
|
|
11348
12005
|
{
|
|
11349
|
-
ASSERT(v
|
|
12006
|
+
ASSERT(isVector2(v));
|
|
11350
12007
|
return new box2d.instance.b2Vec2(v.x, v.y);
|
|
11351
12008
|
}
|
|
11352
12009
|
|
|
@@ -11763,6 +12420,7 @@ export
|
|
|
11763
12420
|
setInputWASDEmulateDirection,
|
|
11764
12421
|
setTouchGamepadEnable,
|
|
11765
12422
|
setTouchGamepadCenterButton,
|
|
12423
|
+
setTouchGamepadButtonCount,
|
|
11766
12424
|
setTouchGamepadAnalog,
|
|
11767
12425
|
setTouchGamepadSize,
|
|
11768
12426
|
setTouchGamepadAlpha,
|
|
@@ -11934,12 +12592,14 @@ export
|
|
|
11934
12592
|
mouseInWindow,
|
|
11935
12593
|
isUsingGamepad,
|
|
11936
12594
|
inputPreventDefault,
|
|
12595
|
+
gamepadPrimary,
|
|
11937
12596
|
setInputPreventDefault,
|
|
11938
12597
|
gamepadIsDown,
|
|
11939
12598
|
gamepadWasPressed,
|
|
11940
12599
|
gamepadWasReleased,
|
|
11941
12600
|
gamepadStick,
|
|
11942
|
-
|
|
12601
|
+
gamepadDpad,
|
|
12602
|
+
gamepadConnected,
|
|
11943
12603
|
vibrate,
|
|
11944
12604
|
vibrateStop,
|
|
11945
12605
|
isTouchDevice,
|
|
@@ -12005,6 +12665,8 @@ export
|
|
|
12005
12665
|
|
|
12006
12666
|
// UI System
|
|
12007
12667
|
uiSystem,
|
|
12668
|
+
uiDebug,
|
|
12669
|
+
uiSetDebug,
|
|
12008
12670
|
UISystemPlugin,
|
|
12009
12671
|
UIObject,
|
|
12010
12672
|
UIText,
|