littlejsengine 1.15.3 → 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 +159 -55
- package/dist/littlejs.esm.js +1143 -547
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1137 -546
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1089 -514
- 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/box2dPool.js +9 -10
- package/examples/shorts/empty.js +1 -1
- package/examples/shorts/input.js +30 -25
- package/examples/shorts/tileLayer.js +2 -2
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/uiSystem.js +25 -14
- package/examples/starter/index.html +3 -2
- package/examples/uiSystem/game.js +27 -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 +476 -91
- package/src/engine.js +2 -169
- package/src/engineBuild.js +1 -0
- package/src/engineDebug.js +48 -32
- package/src/engineDraw.js +25 -8
- package/src/engineExport.js +4 -1
- package/src/engineInput.js +526 -381
- package/src/engineObject.js +24 -22
- package/src/engineSettings.js +11 -6
- package/src/engineSplash.js +173 -0
- package/src/engineTileLayer.js +2 -2
- 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
|
|
|
@@ -914,7 +914,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
|
914
914
|
}
|
|
915
915
|
|
|
916
916
|
/** Draw a debug axis aligned bounding box in world space
|
|
917
|
-
* @param {string} text
|
|
917
|
+
* @param {string|number} text
|
|
918
918
|
* @param {Vector2} pos
|
|
919
919
|
* @param {number} [size]
|
|
920
920
|
* @param {Color|string} [color]
|
|
@@ -1072,34 +1072,46 @@ function debugRender()
|
|
|
1072
1072
|
debugTakeScreenshot = 0;
|
|
1073
1073
|
}
|
|
1074
1074
|
|
|
1075
|
-
if (debugGamepads && gamepadsEnable
|
|
1075
|
+
if (debugGamepads && gamepadsEnable)
|
|
1076
1076
|
{
|
|
1077
|
-
//
|
|
1078
|
-
const
|
|
1079
|
-
|
|
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++)
|
|
1080
1084
|
{
|
|
1081
|
-
|
|
1082
|
-
|
|
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++)
|
|
1083
1098
|
{
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
debugCircle(drawPos, buttonScale*2, pressed ? '#f00' : '#fff7', 0, true);
|
|
1101
|
-
debugText(''+j, drawPos, .2);
|
|
1102
|
-
}
|
|
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);
|
|
1103
1115
|
}
|
|
1104
1116
|
}
|
|
1105
1117
|
}
|
|
@@ -1281,14 +1293,18 @@ function debugRender()
|
|
|
1281
1293
|
mousePressed && overlayContext.fillText('Mouse: ' + mousePressed, x, y += h);
|
|
1282
1294
|
keysPressed && overlayContext.fillText('Keys: ' + keysPressed, x, y += h);
|
|
1283
1295
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
for (const i in inputData[1])
|
|
1296
|
+
// show gamepad buttons
|
|
1297
|
+
for (let i = 1; i < inputData.length; i++)
|
|
1287
1298
|
{
|
|
1288
|
-
|
|
1289
|
-
|
|
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);
|
|
1290
1307
|
}
|
|
1291
|
-
buttonsPressed && overlayContext.fillText('Gamepad: ' + buttonsPressed, x, y += h);
|
|
1292
1308
|
}
|
|
1293
1309
|
else
|
|
1294
1310
|
{
|
|
@@ -2044,9 +2060,15 @@ class Vector2
|
|
|
2044
2060
|
{
|
|
2045
2061
|
this.x = x;
|
|
2046
2062
|
this.y = y;
|
|
2063
|
+
ASSERT_VECTOR2_VALID(this);
|
|
2047
2064
|
return this;
|
|
2048
2065
|
}
|
|
2049
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
|
+
|
|
2050
2072
|
/** Returns a new vector that is a copy of this
|
|
2051
2073
|
* @return {Vector2} */
|
|
2052
2074
|
copy() { return new Vector2(this.x, this.y); }
|
|
@@ -2109,7 +2131,7 @@ class Vector2
|
|
|
2109
2131
|
clampLength(length=1)
|
|
2110
2132
|
{
|
|
2111
2133
|
const l = this.length();
|
|
2112
|
-
return l > length ? this.scale(length/l) : this;
|
|
2134
|
+
return l > length ? this.scale(length/l) : this.copy();
|
|
2113
2135
|
}
|
|
2114
2136
|
|
|
2115
2137
|
/** Returns the dot product of this and the vector passed in
|
|
@@ -2196,6 +2218,10 @@ class Vector2
|
|
|
2196
2218
|
* @return {number} */
|
|
2197
2219
|
area() { return abs(this.x * this.y); }
|
|
2198
2220
|
|
|
2221
|
+
/** Returns true if this vector is (0,0)
|
|
2222
|
+
* @return {boolean} */
|
|
2223
|
+
isZero() { return !this.x && !this.y; }
|
|
2224
|
+
|
|
2199
2225
|
/** Returns a new vector that is p percent between this and the vector passed in
|
|
2200
2226
|
* @param {Vector2} v - other vector
|
|
2201
2227
|
* @param {number} percent
|
|
@@ -2306,9 +2332,15 @@ class Color
|
|
|
2306
2332
|
this.g = g;
|
|
2307
2333
|
this.b = b;
|
|
2308
2334
|
this.a = a;
|
|
2335
|
+
ASSERT_COLOR_VALID(this);
|
|
2309
2336
|
return this;
|
|
2310
2337
|
}
|
|
2311
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
|
+
|
|
2312
2344
|
/** Returns a new color that is a copy of this
|
|
2313
2345
|
* @return {Color} */
|
|
2314
2346
|
copy() { return new Color(this.r, this.g, this.b, this.a); }
|
|
@@ -2860,17 +2892,17 @@ let touchGamepadEnable = false;
|
|
|
2860
2892
|
* @memberof Settings */
|
|
2861
2893
|
let touchGamepadCenterButton = true;
|
|
2862
2894
|
|
|
2863
|
-
/**
|
|
2864
|
-
* @type {
|
|
2895
|
+
/** Number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
|
|
2896
|
+
* @type {number}
|
|
2865
2897
|
* @default
|
|
2866
2898
|
* @memberof Settings */
|
|
2867
|
-
let
|
|
2899
|
+
let touchGamepadButtonCount = 4;
|
|
2868
2900
|
|
|
2869
|
-
/**
|
|
2870
|
-
* @type {
|
|
2901
|
+
/** True if touch gamepad should be analog stick or false to use if 8 way dpad
|
|
2902
|
+
* @type {boolean}
|
|
2871
2903
|
* @default
|
|
2872
2904
|
* @memberof Settings */
|
|
2873
|
-
let
|
|
2905
|
+
let touchGamepadAnalog = true;
|
|
2874
2906
|
|
|
2875
2907
|
/** Size of virtual gamepad for touch devices in pixels
|
|
2876
2908
|
* @type {number}
|
|
@@ -3136,6 +3168,11 @@ function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
|
3136
3168
|
* @memberof Settings */
|
|
3137
3169
|
function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
|
|
3138
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
|
+
|
|
3139
3176
|
/** Set if touch gamepad should be analog stick or 8 way dpad
|
|
3140
3177
|
* @param {boolean} analog
|
|
3141
3178
|
* @memberof Settings */
|
|
@@ -3344,7 +3381,7 @@ class EngineObject
|
|
|
3344
3381
|
child.updateTransforms();
|
|
3345
3382
|
}
|
|
3346
3383
|
|
|
3347
|
-
/** 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. */
|
|
3348
3385
|
updatePhysics()
|
|
3349
3386
|
{
|
|
3350
3387
|
// child objects do not have physics
|
|
@@ -3377,7 +3414,7 @@ class EngineObject
|
|
|
3377
3414
|
if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
|
|
3378
3415
|
return;
|
|
3379
3416
|
|
|
3380
|
-
const
|
|
3417
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
3381
3418
|
if (this.groundObject)
|
|
3382
3419
|
{
|
|
3383
3420
|
// apply friction in local space of ground object
|
|
@@ -3433,10 +3470,10 @@ class EngineObject
|
|
|
3433
3470
|
{
|
|
3434
3471
|
// push outside object collision
|
|
3435
3472
|
this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
|
|
3436
|
-
if ((o.groundObject &&
|
|
3473
|
+
if ((o.groundObject && wasFalling) || !o.mass)
|
|
3437
3474
|
{
|
|
3438
3475
|
// set ground object if landed on something
|
|
3439
|
-
if (
|
|
3476
|
+
if (wasFalling)
|
|
3440
3477
|
this.groundObject = o;
|
|
3441
3478
|
|
|
3442
3479
|
// bounce if other object is fixed or grounded
|
|
@@ -3523,12 +3560,15 @@ class EngineObject
|
|
|
3523
3560
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
3524
3561
|
this.velocity.y *= -restitution;
|
|
3525
3562
|
|
|
3526
|
-
if (
|
|
3563
|
+
if (wasFalling)
|
|
3527
3564
|
{
|
|
3528
|
-
// adjust position to slightly
|
|
3565
|
+
// adjust position to slightly away from nearest tile
|
|
3529
3566
|
// this prevents gap between object and ground
|
|
3530
3567
|
const epsilon = .0001;
|
|
3531
|
-
|
|
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;
|
|
3532
3572
|
|
|
3533
3573
|
// set ground object for tile collision
|
|
3534
3574
|
this.groundObject = hitLayer;
|
|
@@ -3697,21 +3737,20 @@ class EngineObject
|
|
|
3697
3737
|
* @return {string} */
|
|
3698
3738
|
toString()
|
|
3699
3739
|
{
|
|
3700
|
-
if (debug)
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
}
|
|
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;
|
|
3715
3754
|
}
|
|
3716
3755
|
|
|
3717
3756
|
/** Render debug info for this object */
|
|
@@ -3880,7 +3919,7 @@ class TileInfo
|
|
|
3880
3919
|
this.padding = padding;
|
|
3881
3920
|
/** @property {TextureInfo} - The texture info for this tile */
|
|
3882
3921
|
this.textureInfo = textureInfos[this.textureIndex];
|
|
3883
|
-
/** @property {
|
|
3922
|
+
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
3884
3923
|
this.bleedScale = bleedScale;
|
|
3885
3924
|
}
|
|
3886
3925
|
|
|
@@ -4367,7 +4406,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4367
4406
|
|
|
4368
4407
|
/** Draw text on main canvas in world space
|
|
4369
4408
|
* Automatically splits new lines into rows
|
|
4370
|
-
* @param {string} text
|
|
4409
|
+
* @param {string|number} text
|
|
4371
4410
|
* @param {Vector2} pos
|
|
4372
4411
|
* @param {number} [size]
|
|
4373
4412
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -4394,7 +4433,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
4394
4433
|
|
|
4395
4434
|
/** Draw text on overlay canvas in world space
|
|
4396
4435
|
* Automatically splits new lines into rows
|
|
4397
|
-
* @param {string} text
|
|
4436
|
+
* @param {string|number} text
|
|
4398
4437
|
* @param {Vector2} pos
|
|
4399
4438
|
* @param {number} [size]
|
|
4400
4439
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -4413,7 +4452,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
4413
4452
|
|
|
4414
4453
|
/** Draw text on overlay canvas in screen space
|
|
4415
4454
|
* Automatically splits new lines into rows
|
|
4416
|
-
* @param {string} text
|
|
4455
|
+
* @param {string|number} text
|
|
4417
4456
|
* @param {Vector2} pos
|
|
4418
4457
|
* @param {number} [size]
|
|
4419
4458
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -4578,11 +4617,28 @@ function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
|
|
|
4578
4617
|
];
|
|
4579
4618
|
}
|
|
4580
4619
|
|
|
4581
|
-
/** Get the camera
|
|
4620
|
+
/** Get the size of the camera window in world space
|
|
4582
4621
|
* @return {Vector2}
|
|
4583
4622
|
* @memberof Draw */
|
|
4584
4623
|
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
4585
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
|
+
|
|
4586
4642
|
/** Enable normal or additive blend mode
|
|
4587
4643
|
* @param {boolean} [additive]
|
|
4588
4644
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -4746,7 +4802,7 @@ class FontImage
|
|
|
4746
4802
|
}
|
|
4747
4803
|
|
|
4748
4804
|
/** Draw text in world space using the image font
|
|
4749
|
-
* @param {string} text
|
|
4805
|
+
* @param {string|number} text
|
|
4750
4806
|
* @param {Vector2} pos
|
|
4751
4807
|
* @param {number} [scale=.25]
|
|
4752
4808
|
* @param {boolean} [center]
|
|
@@ -4758,7 +4814,7 @@ class FontImage
|
|
|
4758
4814
|
}
|
|
4759
4815
|
|
|
4760
4816
|
/** Draw text on overlay canvas in world space using the image font
|
|
4761
|
-
* @param {string} text
|
|
4817
|
+
* @param {string|number} text
|
|
4762
4818
|
* @param {Vector2} pos
|
|
4763
4819
|
* @param {number} [scale]
|
|
4764
4820
|
* @param {boolean} [center]
|
|
@@ -4767,7 +4823,7 @@ class FontImage
|
|
|
4767
4823
|
{ this.drawText(text, pos, scale, center, overlayContext); }
|
|
4768
4824
|
|
|
4769
4825
|
/** Draw text on overlay canvas in screen space using the image font
|
|
4770
|
-
* @param {string} text
|
|
4826
|
+
* @param {string|number} text
|
|
4771
4827
|
* @param {Vector2} pos
|
|
4772
4828
|
* @param {number} [scale]
|
|
4773
4829
|
* @param {boolean} [center]
|
|
@@ -4811,6 +4867,85 @@ class FontImage
|
|
|
4811
4867
|
* @namespace Input
|
|
4812
4868
|
*/
|
|
4813
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
|
+
|
|
4814
4949
|
/** Returns true if device key is down
|
|
4815
4950
|
* @param {string|number} key
|
|
4816
4951
|
* @param {number} [device]
|
|
@@ -4818,9 +4953,9 @@ class FontImage
|
|
|
4818
4953
|
* @memberof Input */
|
|
4819
4954
|
function keyIsDown(key, device=0)
|
|
4820
4955
|
{
|
|
4821
|
-
ASSERT(key
|
|
4956
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4822
4957
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4823
|
-
return
|
|
4958
|
+
return !!(inputData[device]?.[key] & 1);
|
|
4824
4959
|
}
|
|
4825
4960
|
|
|
4826
4961
|
/** Returns true if device key was pressed this frame
|
|
@@ -4830,9 +4965,9 @@ function keyIsDown(key, device=0)
|
|
|
4830
4965
|
* @memberof Input */
|
|
4831
4966
|
function keyWasPressed(key, device=0)
|
|
4832
4967
|
{
|
|
4833
|
-
ASSERT(key
|
|
4968
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4834
4969
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4835
|
-
return
|
|
4970
|
+
return !!(inputData[device]?.[key] & 2);
|
|
4836
4971
|
}
|
|
4837
4972
|
|
|
4838
4973
|
/** Returns true if device key was released this frame
|
|
@@ -4842,172 +4977,182 @@ function keyWasPressed(key, device=0)
|
|
|
4842
4977
|
* @memberof Input */
|
|
4843
4978
|
function keyWasReleased(key, device=0)
|
|
4844
4979
|
{
|
|
4845
|
-
ASSERT(key
|
|
4980
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4846
4981
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4847
|
-
return
|
|
4982
|
+
return !!(inputData[device]?.[key] & 4);
|
|
4848
4983
|
}
|
|
4849
4984
|
|
|
4850
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]
|
|
4851
4990
|
* @return {Vector2}
|
|
4852
4991
|
* @memberof Input */
|
|
4853
4992
|
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
4854
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');
|
|
4855
4998
|
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
4856
4999
|
return vec2(k(right) - k(left), k(up) - k(down));
|
|
4857
5000
|
}
|
|
4858
5001
|
|
|
4859
|
-
/** Clears all input
|
|
4860
|
-
* @memberof Input */
|
|
4861
|
-
function inputClear() { inputData = [[]]; touchGamepadButtons = []; }
|
|
4862
|
-
|
|
4863
|
-
/** Clears an input key state
|
|
4864
|
-
* @param {string|number} key
|
|
4865
|
-
* @param {number} [device]
|
|
4866
|
-
* @param {boolean} [clearDown=true]
|
|
4867
|
-
* @param {boolean} [clearPressed=true]
|
|
4868
|
-
* @param {boolean} [clearReleased=true]
|
|
4869
|
-
* @memberof Input */
|
|
4870
|
-
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4871
|
-
{
|
|
4872
|
-
if (!inputData[device])
|
|
4873
|
-
return;
|
|
4874
|
-
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4875
|
-
}
|
|
4876
|
-
|
|
4877
5002
|
/** Returns true if mouse button is down
|
|
4878
5003
|
* @function
|
|
4879
5004
|
* @param {number} button
|
|
4880
5005
|
* @return {boolean}
|
|
4881
5006
|
* @memberof Input */
|
|
4882
|
-
function mouseIsDown(button)
|
|
5007
|
+
function mouseIsDown(button)
|
|
5008
|
+
{
|
|
5009
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
5010
|
+
return keyIsDown(button);
|
|
5011
|
+
}
|
|
4883
5012
|
|
|
4884
5013
|
/** Returns true if mouse button was pressed
|
|
4885
5014
|
* @function
|
|
4886
5015
|
* @param {number} button
|
|
4887
5016
|
* @return {boolean}
|
|
4888
5017
|
* @memberof Input */
|
|
4889
|
-
function mouseWasPressed(button)
|
|
5018
|
+
function mouseWasPressed(button)
|
|
5019
|
+
{
|
|
5020
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
5021
|
+
return keyWasPressed(button);
|
|
5022
|
+
}
|
|
4890
5023
|
|
|
4891
5024
|
/** Returns true if mouse button was released
|
|
4892
5025
|
* @function
|
|
4893
5026
|
* @param {number} button
|
|
4894
5027
|
* @return {boolean}
|
|
4895
5028
|
* @memberof Input */
|
|
4896
|
-
function mouseWasReleased(button)
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
let mousePos = vec2();
|
|
4902
|
-
|
|
4903
|
-
/** Mouse pos in screen space
|
|
4904
|
-
* @type {Vector2}
|
|
4905
|
-
* @memberof Input */
|
|
4906
|
-
let mousePosScreen = vec2();
|
|
4907
|
-
|
|
4908
|
-
/** Mouse movement delta in world space
|
|
4909
|
-
* @type {Vector2}
|
|
4910
|
-
* @memberof Input */
|
|
4911
|
-
let mouseDelta = vec2();
|
|
4912
|
-
|
|
4913
|
-
/** Mouse movement delta in screen space
|
|
4914
|
-
* @type {Vector2}
|
|
4915
|
-
* @memberof Input */
|
|
4916
|
-
let mouseDeltaScreen = vec2();
|
|
4917
|
-
|
|
4918
|
-
/** Mouse wheel delta this frame
|
|
4919
|
-
* @type {number}
|
|
4920
|
-
* @memberof Input */
|
|
4921
|
-
let mouseWheel = 0;
|
|
4922
|
-
|
|
4923
|
-
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4924
|
-
* @type {boolean}
|
|
4925
|
-
* @memberof Input */
|
|
4926
|
-
let mouseInWindow = true;
|
|
4927
|
-
|
|
4928
|
-
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4929
|
-
* @type {boolean}
|
|
4930
|
-
* @memberof Input */
|
|
4931
|
-
let isUsingGamepad = false;
|
|
4932
|
-
|
|
4933
|
-
/** Prevents input continuing to the default browser handling (true by default)
|
|
4934
|
-
* @type {boolean}
|
|
4935
|
-
* @memberof Input */
|
|
4936
|
-
let inputPreventDefault = true;
|
|
4937
|
-
|
|
4938
|
-
/** Prevents input continuing to the default browser handling
|
|
4939
|
-
* This is useful to disable for html menus so the browser can handle input normally
|
|
4940
|
-
* @param {boolean} preventDefault
|
|
4941
|
-
* @memberof Input */
|
|
4942
|
-
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
|
+
}
|
|
4943
5034
|
|
|
4944
5035
|
/** Returns true if gamepad button is down
|
|
4945
5036
|
* @param {number} button
|
|
4946
5037
|
* @param {number} [gamepad]
|
|
4947
5038
|
* @return {boolean}
|
|
4948
5039
|
* @memberof Input */
|
|
4949
|
-
function gamepadIsDown(button, gamepad=
|
|
4950
|
-
{
|
|
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
|
+
}
|
|
4951
5046
|
|
|
4952
5047
|
/** Returns true if gamepad button was pressed
|
|
4953
5048
|
* @param {number} button
|
|
4954
5049
|
* @param {number} [gamepad]
|
|
4955
5050
|
* @return {boolean}
|
|
4956
5051
|
* @memberof Input */
|
|
4957
|
-
function gamepadWasPressed(button, gamepad=
|
|
4958
|
-
{
|
|
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
|
+
}
|
|
4959
5058
|
|
|
4960
5059
|
/** Returns true if gamepad button was released
|
|
4961
5060
|
* @param {number} button
|
|
4962
5061
|
* @param {number} [gamepad]
|
|
4963
5062
|
* @return {boolean}
|
|
4964
5063
|
* @memberof Input */
|
|
4965
|
-
function gamepadWasReleased(button, gamepad=
|
|
4966
|
-
{
|
|
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
|
+
}
|
|
4967
5070
|
|
|
4968
5071
|
/** Returns gamepad stick value
|
|
4969
5072
|
* @param {number} stick
|
|
4970
5073
|
* @param {number} [gamepad]
|
|
4971
5074
|
* @return {Vector2}
|
|
4972
5075
|
* @memberof Input */
|
|
4973
|
-
function gamepadStick(stick, gamepad=
|
|
4974
|
-
{
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
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
|
+
}
|
|
4978
5082
|
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
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
|
+
}
|
|
4982
5092
|
|
|
4983
|
-
|
|
5093
|
+
/** Returns true if passed in gamepad is connected
|
|
5094
|
+
* @param {number} [gamepad]
|
|
5095
|
+
* @return {boolean}
|
|
5096
|
+
* @memberof Input */
|
|
5097
|
+
function gamepadConnected(gamepad=gamepadPrimary)
|
|
4984
5098
|
{
|
|
4985
|
-
|
|
5099
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5100
|
+
return !!inputData[gamepad+1];
|
|
5101
|
+
}
|
|
4986
5102
|
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
5103
|
+
/** True if a touch device has been detected
|
|
5104
|
+
* @memberof Input */
|
|
5105
|
+
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
4990
5106
|
|
|
4991
|
-
|
|
4992
|
-
mousePos = screenToWorld(mousePosScreen);
|
|
4993
|
-
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
5107
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4994
5108
|
|
|
4995
|
-
|
|
4996
|
-
|
|
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);
|
|
4997
5116
|
}
|
|
4998
5117
|
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5118
|
+
/** Cancel any ongoing vibration
|
|
5119
|
+
* @memberof Input */
|
|
5120
|
+
function vibrateStop() { vibrate(0); }
|
|
5002
5121
|
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
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
|
|
5011
5156
|
|
|
5012
5157
|
function inputInit()
|
|
5013
5158
|
{
|
|
@@ -5037,6 +5182,18 @@ function inputInit()
|
|
|
5037
5182
|
if (inputWASDEmulateDirection)
|
|
5038
5183
|
inputData[0][remapKey(e.code)] = 3;
|
|
5039
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();
|
|
5040
5197
|
}
|
|
5041
5198
|
function onKeyUp(e)
|
|
5042
5199
|
{
|
|
@@ -5068,7 +5225,9 @@ function inputInit()
|
|
|
5068
5225
|
const mousePosScreenLast = mousePosScreen;
|
|
5069
5226
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
5070
5227
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
5071
|
-
|
|
5228
|
+
|
|
5229
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5230
|
+
e.preventDefault();
|
|
5072
5231
|
}
|
|
5073
5232
|
function onMouseUp(e)
|
|
5074
5233
|
{
|
|
@@ -5092,344 +5251,386 @@ function inputInit()
|
|
|
5092
5251
|
function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
|
|
5093
5252
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
5094
5253
|
function onBlur() { inputClear(); } // reset input when focus is lost
|
|
5095
|
-
}
|
|
5096
5254
|
|
|
5097
|
-
//
|
|
5098
|
-
function
|
|
5099
|
-
{
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
}
|
|
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 });
|
|
5105
5262
|
|
|
5106
|
-
|
|
5107
|
-
|
|
5263
|
+
// handle all touch events the same way
|
|
5264
|
+
let wasTouching;
|
|
5265
|
+
function handleTouch(e)
|
|
5266
|
+
{
|
|
5267
|
+
if (!touchInputEnable)
|
|
5268
|
+
return;
|
|
5108
5269
|
|
|
5109
|
-
//
|
|
5110
|
-
|
|
5270
|
+
// route touch to gamepad
|
|
5271
|
+
if (touchGamepadEnable)
|
|
5272
|
+
handleTouchGamepad(e);
|
|
5111
5273
|
|
|
5112
|
-
//
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
const applyDeadZones = (v)=>
|
|
5116
|
-
{
|
|
5117
|
-
const min=.3, max=.8;
|
|
5118
|
-
const deadZone = (v)=>
|
|
5119
|
-
v > min ? percent(v, min, max) :
|
|
5120
|
-
v < -min ? -percent(-v, min, max) : 0;
|
|
5121
|
-
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5122
|
-
}
|
|
5274
|
+
// fix stalled audio requiring user interaction
|
|
5275
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
5276
|
+
audioContext.resume();
|
|
5123
5277
|
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
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;
|
|
5129
5297
|
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
//
|
|
5138
|
-
|
|
5139
|
-
sticks[0].y = -round(touchGamepadStick.y);
|
|
5140
|
-
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;
|
|
5141
5307
|
}
|
|
5142
5308
|
|
|
5143
|
-
//
|
|
5144
|
-
|
|
5145
|
-
for (let i=10; i--;)
|
|
5309
|
+
// special handling for virtual gamepad mode
|
|
5310
|
+
function handleTouchGamepad(e)
|
|
5146
5311
|
{
|
|
5147
|
-
|
|
5148
|
-
|
|
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
|
+
}
|
|
5149
5380
|
}
|
|
5381
|
+
}
|
|
5150
5382
|
|
|
5151
|
-
|
|
5152
|
-
|
|
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);
|
|
5153
5390
|
}
|
|
5391
|
+
}
|
|
5154
5392
|
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5393
|
+
function inputUpdate()
|
|
5394
|
+
{
|
|
5395
|
+
if (headlessMode) return;
|
|
5158
5396
|
|
|
5159
|
-
//
|
|
5160
|
-
if (!
|
|
5161
|
-
|
|
5397
|
+
// clear input when lost focus (prevent stuck keys)
|
|
5398
|
+
if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
5399
|
+
inputClear();
|
|
5400
|
+
|
|
5401
|
+
// update mouse world space position and delta
|
|
5402
|
+
mousePos = screenToWorld(mousePosScreen);
|
|
5403
|
+
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
5162
5404
|
|
|
5163
|
-
//
|
|
5164
|
-
|
|
5165
|
-
|
|
5405
|
+
// update gamepads if enabled
|
|
5406
|
+
gamepadsUpdate();
|
|
5407
|
+
|
|
5408
|
+
// gamepads are updated by engine every frame automatically
|
|
5409
|
+
function gamepadsUpdate()
|
|
5166
5410
|
{
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
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
|
+
}
|
|
5171
5419
|
|
|
5172
|
-
if
|
|
5420
|
+
// update touch gamepad if enabled
|
|
5421
|
+
if (touchGamepadEnable && isTouchDevice)
|
|
5173
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;
|
|
5462
|
+
|
|
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)
|
|
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
|
+
|
|
5174
5489
|
// read analog sticks
|
|
5175
5490
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
5176
5491
|
sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
|
|
5177
5492
|
|
|
5178
5493
|
// read buttons
|
|
5494
|
+
let hadInput = false;
|
|
5179
5495
|
for (let j = gamepad.buttons.length; j--;)
|
|
5180
5496
|
{
|
|
5181
5497
|
const button = gamepad.buttons[j];
|
|
5182
5498
|
const wasDown = gamepadIsDown(j,i);
|
|
5183
5499
|
data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
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);
|
|
5187
5514
|
}
|
|
5188
5515
|
|
|
5189
|
-
if (
|
|
5516
|
+
if (gamepad.mapping === 'standard')
|
|
5190
5517
|
{
|
|
5191
|
-
//
|
|
5192
|
-
|
|
5518
|
+
// get dpad buttons (standard mapping)
|
|
5519
|
+
dpad.set(
|
|
5193
5520
|
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
5194
5521
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
5195
|
-
|
|
5196
|
-
|
|
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);
|
|
5197
5529
|
}
|
|
5198
5530
|
|
|
5199
|
-
//
|
|
5200
|
-
|
|
5531
|
+
// copy dpad to left analog stick when pressed
|
|
5532
|
+
if (gamepadDirectionEmulateStick && !dpad.isZero())
|
|
5533
|
+
sticks[0] = dpad.clampLength();
|
|
5201
5534
|
}
|
|
5535
|
+
|
|
5536
|
+
// disable touch gamepad if using real gamepad
|
|
5537
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
5202
5538
|
}
|
|
5203
5539
|
}
|
|
5204
5540
|
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
/** Pulse the vibration hardware if it exists
|
|
5208
|
-
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
5209
|
-
* @memberof Input */
|
|
5210
|
-
function vibrate(pattern=100)
|
|
5211
|
-
{ vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
|
|
5212
|
-
|
|
5213
|
-
/** Cancel any ongoing vibration
|
|
5214
|
-
* @memberof Input */
|
|
5215
|
-
function vibrateStop() { vibrate(0); }
|
|
5216
|
-
|
|
5217
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5218
|
-
// Touch input & virtual on screen gamepad
|
|
5219
|
-
|
|
5220
|
-
/** True if a touch device has been detected
|
|
5221
|
-
* @memberof Input */
|
|
5222
|
-
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
5223
|
-
|
|
5224
|
-
// touch gamepad internal variables
|
|
5225
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
|
|
5226
|
-
|
|
5227
|
-
function touchGamepadButtonCenter()
|
|
5541
|
+
function inputUpdatePost()
|
|
5228
5542
|
{
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
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();
|
|
5234
5552
|
}
|
|
5235
5553
|
|
|
5236
|
-
|
|
5237
|
-
function touchInputInit()
|
|
5554
|
+
function inputRender()
|
|
5238
5555
|
{
|
|
5239
|
-
|
|
5240
|
-
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
5241
|
-
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
5242
|
-
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
5556
|
+
touchGamepadRender();
|
|
5243
5557
|
|
|
5244
|
-
|
|
5245
|
-
let wasTouching;
|
|
5246
|
-
function handleTouch(e)
|
|
5558
|
+
function touchGamepadRender()
|
|
5247
5559
|
{
|
|
5248
|
-
if (!touchInputEnable)
|
|
5560
|
+
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
5561
|
+
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
5249
5562
|
return;
|
|
5250
5563
|
|
|
5251
|
-
//
|
|
5252
|
-
|
|
5253
|
-
|
|
5564
|
+
// fade off when not touching or paused
|
|
5565
|
+
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
5566
|
+
if (!alpha || paused)
|
|
5567
|
+
return;
|
|
5254
5568
|
|
|
5255
|
-
//
|
|
5256
|
-
|
|
5257
|
-
|
|
5569
|
+
// setup the canvas
|
|
5570
|
+
const context = overlayContext;
|
|
5571
|
+
context.save();
|
|
5572
|
+
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
5573
|
+
context.strokeStyle = '#fff';
|
|
5574
|
+
context.lineWidth = 3;
|
|
5258
5575
|
|
|
5259
|
-
//
|
|
5260
|
-
const
|
|
5261
|
-
|
|
5262
|
-
|
|
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)
|
|
5263
5582
|
{
|
|
5264
|
-
//
|
|
5265
|
-
|
|
5266
|
-
const mousePosScreenLast = mousePosScreen;
|
|
5267
|
-
mousePosScreen = mouseEventToScreen(pos);
|
|
5268
|
-
if (wasTouching)
|
|
5269
|
-
{
|
|
5270
|
-
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
5271
|
-
isUsingGamepad = touchGamepadEnable;
|
|
5272
|
-
}
|
|
5273
|
-
else
|
|
5274
|
-
inputData[0][button] = 3;
|
|
5583
|
+
// draw circle shaped gamepad
|
|
5584
|
+
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
5275
5585
|
}
|
|
5276
|
-
else
|
|
5277
|
-
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
5278
|
-
|
|
5279
|
-
// set was touching
|
|
5280
|
-
wasTouching = touching;
|
|
5281
|
-
|
|
5282
|
-
// prevent default handling like copy, magnifier lens, and scrolling
|
|
5283
|
-
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
5284
|
-
e.preventDefault();
|
|
5285
|
-
|
|
5286
|
-
// must return true so the document will get focus
|
|
5287
|
-
return true;
|
|
5288
|
-
}
|
|
5289
|
-
|
|
5290
|
-
// special handling for virtual gamepad mode
|
|
5291
|
-
function handleTouchGamepad(e)
|
|
5292
|
-
{
|
|
5293
|
-
// clear touch gamepad input
|
|
5294
|
-
touchGamepadStick = vec2();
|
|
5295
|
-
touchGamepadButtons = [];
|
|
5296
|
-
isUsingGamepad = true;
|
|
5297
|
-
|
|
5298
|
-
const touching = e.touches.length;
|
|
5299
|
-
if (touching)
|
|
5586
|
+
else
|
|
5300
5587
|
{
|
|
5301
|
-
|
|
5302
|
-
|
|
5588
|
+
// draw cross shaped gamepad
|
|
5589
|
+
for (let i=10; --i;)
|
|
5303
5590
|
{
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
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);
|
|
5307
5594
|
}
|
|
5308
5595
|
}
|
|
5596
|
+
context.fill();
|
|
5597
|
+
context.stroke();
|
|
5309
5598
|
|
|
5310
|
-
//
|
|
5311
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5312
|
-
const buttonCenter = touchGamepadButtonCenter();
|
|
5313
|
-
const startCenter = mainCanvasSize.scale(.5);
|
|
5314
|
-
|
|
5315
|
-
// check each touch point
|
|
5316
|
-
for (const touch of e.touches)
|
|
5599
|
+
// draw right face buttons
|
|
5317
5600
|
{
|
|
5318
|
-
const
|
|
5319
|
-
|
|
5601
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
5602
|
+
const buttonSize = touchGamepadButtonCount > 1 ?
|
|
5603
|
+
touchGamepadSize/4 : touchGamepadSize/2;
|
|
5604
|
+
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
5320
5605
|
{
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
else if (buttonCenter.distance(touchPos) < touchGamepadSize)
|
|
5325
|
-
{
|
|
5326
|
-
// virtual face buttons
|
|
5327
|
-
let button = buttonCenter.subtract(touchPos).direction();
|
|
5328
|
-
button = mod(button+2, 4);
|
|
5329
|
-
if (touchGamepadButtonCount === 1)
|
|
5330
|
-
button = 0;
|
|
5331
|
-
else if (touchGamepadButtonCount === 2)
|
|
5332
|
-
{
|
|
5333
|
-
const delta = buttonCenter.subtract(touchPos);
|
|
5334
|
-
button = -delta.x < delta.y ? 1 : 0;
|
|
5335
|
-
}
|
|
5606
|
+
const j = mod(i-1, 4);
|
|
5607
|
+
let button = touchGamepadButtonCount > 2 ?
|
|
5608
|
+
j : min(j, touchGamepadButtonCount-1);
|
|
5336
5609
|
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
5337
5610
|
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
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();
|
|
5346
5618
|
}
|
|
5347
5619
|
}
|
|
5348
|
-
}
|
|
5349
|
-
}
|
|
5350
|
-
|
|
5351
|
-
// render the touch gamepad, called automatically by the engine
|
|
5352
|
-
function touchGamepadRender()
|
|
5353
|
-
{
|
|
5354
|
-
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
5355
|
-
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
5356
|
-
return;
|
|
5357
|
-
|
|
5358
|
-
// fade off when not touching or paused
|
|
5359
|
-
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
5360
|
-
if (!alpha || paused)
|
|
5361
|
-
return;
|
|
5362
|
-
|
|
5363
|
-
// setup the canvas
|
|
5364
|
-
const context = overlayContext;
|
|
5365
|
-
context.save();
|
|
5366
|
-
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
5367
|
-
context.strokeStyle = '#fff';
|
|
5368
|
-
context.lineWidth = 3;
|
|
5369
|
-
|
|
5370
|
-
// draw left analog stick
|
|
5371
|
-
context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
5372
|
-
context.beginPath();
|
|
5373
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5374
|
-
if (touchGamepadAnalog) // draw circle shaped gamepad
|
|
5375
|
-
{
|
|
5376
|
-
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
5377
|
-
context.fill();
|
|
5378
|
-
context.stroke();
|
|
5379
|
-
}
|
|
5380
|
-
else // draw cross shaped gamepad
|
|
5381
|
-
{
|
|
5382
|
-
for (let i=10; i--;)
|
|
5383
|
-
{
|
|
5384
|
-
const angle = i*PI/4;
|
|
5385
|
-
context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
5386
|
-
i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
|
|
5387
|
-
i===1 && context.fill();
|
|
5388
|
-
}
|
|
5389
|
-
context.stroke();
|
|
5390
|
-
}
|
|
5391
5620
|
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
|
|
5395
|
-
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
5396
|
-
{
|
|
5397
|
-
const j = mod(i-1, 4);
|
|
5398
|
-
let button = touchGamepadButtonCount > 2 ?
|
|
5399
|
-
j : min(j, touchGamepadButtonCount-1);
|
|
5400
|
-
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
5401
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
5402
|
-
const pos = buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
5403
|
-
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
5404
|
-
context.beginPath();
|
|
5405
|
-
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
5406
|
-
context.fill();
|
|
5407
|
-
context.stroke();
|
|
5621
|
+
// set canvas back to normal
|
|
5622
|
+
context.restore();
|
|
5408
5623
|
}
|
|
5409
|
-
|
|
5410
|
-
// set canvas back to normal
|
|
5411
|
-
context.restore();
|
|
5412
5624
|
}
|
|
5413
5625
|
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
/** Request to lock the pointer, does not work on touch devices
|
|
5418
|
-
* @memberof Input */
|
|
5419
|
-
function pointerLockRequest()
|
|
5626
|
+
// center position for right tocuh pad face buttons
|
|
5627
|
+
function touchGamepadButtonCenter()
|
|
5420
5628
|
{
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
* @memberof Input */
|
|
5427
|
-
function pointerLockExit() { document.exitPointerLock && document.exitPointerLock(); }
|
|
5428
|
-
|
|
5429
|
-
/** Check if pointer is locked (true if locked)
|
|
5430
|
-
* @return {boolean}
|
|
5431
|
-
* @memberof Input */
|
|
5432
|
-
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
|
+
}
|
|
5433
5634
|
/**
|
|
5434
5635
|
* LittleJS Audio System
|
|
5435
5636
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
@@ -6169,7 +6370,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
|
|
|
6169
6370
|
}
|
|
6170
6371
|
}
|
|
6171
6372
|
|
|
6172
|
-
/** Return the exact position of the
|
|
6373
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6173
6374
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6174
6375
|
* @param {Vector2} posStart
|
|
6175
6376
|
* @param {Vector2} posEnd
|
|
@@ -6743,7 +6944,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6743
6944
|
return false;
|
|
6744
6945
|
}
|
|
6745
6946
|
|
|
6746
|
-
/** Return the exact position of the
|
|
6947
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6747
6948
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6748
6949
|
* @param {Vector2} posStart
|
|
6749
6950
|
* @param {Vector2} posEnd
|
|
@@ -8688,11 +8889,14 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8688
8889
|
/**
|
|
8689
8890
|
* LittleJS User Interface Plugin
|
|
8690
8891
|
* - call new UISystemPlugin() to setup the UI system
|
|
8892
|
+
* - Gamepad and keyboard navigation support
|
|
8691
8893
|
* - Nested Menus
|
|
8692
8894
|
* - Text
|
|
8693
8895
|
* - Buttons
|
|
8694
8896
|
* - Checkboxes
|
|
8695
8897
|
* - Images
|
|
8898
|
+
* - Scrollbars
|
|
8899
|
+
* - Video
|
|
8696
8900
|
* @namespace UISystem
|
|
8697
8901
|
*/
|
|
8698
8902
|
|
|
@@ -8703,6 +8907,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8703
8907
|
* @memberof UISystem */
|
|
8704
8908
|
let uiSystem;
|
|
8705
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
|
+
|
|
8706
8924
|
///////////////////////////////////////////////////////////////////////////////
|
|
8707
8925
|
/**
|
|
8708
8926
|
* UI System Global Object
|
|
@@ -8741,7 +8959,7 @@ class UISystemPlugin
|
|
|
8741
8959
|
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
8742
8960
|
this.defaultCornerRadius = 0;
|
|
8743
8961
|
/** @property {number} - Default scale to use for fitting text to object */
|
|
8744
|
-
this.
|
|
8962
|
+
this.defaultTextFitScale = .8;
|
|
8745
8963
|
/** @property {string} - Default font for UI elements */
|
|
8746
8964
|
this.defaultFont = fontDefault;
|
|
8747
8965
|
/** @property {Sound} - Default sound when interactive UI element is pressed */
|
|
@@ -8756,6 +8974,23 @@ class UISystemPlugin
|
|
|
8756
8974
|
this.defaultShadowBlur = 5;
|
|
8757
8975
|
/** @property {Vector2} - Offset of shadow blur */
|
|
8758
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
|
+
|
|
8759
8994
|
// system state
|
|
8760
8995
|
/** @property {Array<UIObject>} - List of all UI elements */
|
|
8761
8996
|
this.uiObjects = [];
|
|
@@ -8767,50 +9002,118 @@ class UISystemPlugin
|
|
|
8767
9002
|
this.hoverObject = undefined;
|
|
8768
9003
|
/** @property {UIObject} - Hover object at start of update */
|
|
8769
9004
|
this.lastHoverObject = undefined;
|
|
8770
|
-
/** @property {
|
|
8771
|
-
this.
|
|
9005
|
+
/** @property {UIObject} - Current confirm menu being shown */
|
|
9006
|
+
this.confirmDialog = undefined;
|
|
8772
9007
|
|
|
8773
9008
|
engineAddPlugin(uiUpdate, uiRender);
|
|
8774
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
|
+
|
|
8775
9018
|
// setup recursive update and render
|
|
8776
9019
|
// update in reverse order to detect mouse enter/leave
|
|
8777
9020
|
function uiUpdate()
|
|
8778
9021
|
{
|
|
8779
|
-
|
|
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))
|
|
8780
9030
|
{
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
updateInvisibleObject(c);
|
|
8784
|
-
o.updateInvisible();
|
|
9031
|
+
uiSystem.navigationMode = false;
|
|
9032
|
+
uiSystem.navigationObject = undefined;
|
|
8785
9033
|
}
|
|
8786
|
-
|
|
9034
|
+
|
|
9035
|
+
// navigation with gamepad/keyboard
|
|
9036
|
+
const navigableObjects = uiSystem.getNavigableObjects();
|
|
9037
|
+
if (!navigableObjects.length)
|
|
9038
|
+
uiSystem.navigationObject = undefined;
|
|
9039
|
+
else
|
|
8787
9040
|
{
|
|
8788
|
-
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)
|
|
8789
9047
|
{
|
|
8790
|
-
//
|
|
8791
|
-
|
|
8792
|
-
o.pos = o.localPos.add(o.parent.pos);
|
|
8793
|
-
// update in reverse order to detect mouse enter/leave
|
|
8794
|
-
for (let i=o.children.length; i--;)
|
|
8795
|
-
updateObject(o.children[i]);
|
|
8796
|
-
o.update();
|
|
9048
|
+
// select first auto focus object
|
|
9049
|
+
uiSystem.navigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
8797
9050
|
}
|
|
8798
|
-
|
|
8799
|
-
|
|
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();
|
|
8800
9095
|
}
|
|
8801
|
-
// reset hover object at start of update
|
|
8802
|
-
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8803
|
-
uiSystem.hoverObject = undefined;
|
|
8804
9096
|
|
|
8805
9097
|
// update in reverse order so topmost objects get priority
|
|
8806
9098
|
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8807
9099
|
{
|
|
8808
9100
|
const o = uiSystem.uiObjects[i];
|
|
8809
|
-
o.parent || updateObject(o)
|
|
9101
|
+
o.parent || updateObject(o);
|
|
8810
9102
|
}
|
|
8811
9103
|
|
|
8812
9104
|
// remove destroyed objects
|
|
8813
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
|
+
}
|
|
8814
9117
|
}
|
|
8815
9118
|
function uiRender()
|
|
8816
9119
|
{
|
|
@@ -8827,15 +9130,29 @@ class UISystemPlugin
|
|
|
8827
9130
|
|
|
8828
9131
|
function renderObject(o)
|
|
8829
9132
|
{
|
|
8830
|
-
if (!o.visible)
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
9133
|
+
if (!o.visible) return;
|
|
9134
|
+
|
|
9135
|
+
// render object and children
|
|
9136
|
+
updateTransforms(o);
|
|
8834
9137
|
o.render();
|
|
8835
9138
|
for (const c of o.children)
|
|
8836
9139
|
renderObject(c);
|
|
8837
9140
|
}
|
|
8838
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
|
+
}
|
|
8839
9156
|
context.restore();
|
|
8840
9157
|
}
|
|
8841
9158
|
}
|
|
@@ -8888,8 +9205,8 @@ class UISystemPlugin
|
|
|
8888
9205
|
else
|
|
8889
9206
|
context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
|
|
8890
9207
|
context.fill();
|
|
8891
|
-
context.shadowColor = '#0000'
|
|
8892
|
-
if (lineWidth)
|
|
9208
|
+
context.shadowColor = '#0000';
|
|
9209
|
+
if (lineWidth && lineColor.a > 0)
|
|
8893
9210
|
{
|
|
8894
9211
|
context.strokeStyle = lineColor.toString();
|
|
8895
9212
|
context.lineWidth = lineWidth;
|
|
@@ -8924,10 +9241,24 @@ class UISystemPlugin
|
|
|
8924
9241
|
* @param {TileInfo} tileInfo
|
|
8925
9242
|
* @param {Color} [color=uiSystem.defaultColor]
|
|
8926
9243
|
* @param {number} [angle]
|
|
8927
|
-
* @param {boolean} [mirror]
|
|
8928
|
-
|
|
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())
|
|
8929
9249
|
{
|
|
8930
|
-
|
|
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';
|
|
8931
9262
|
}
|
|
8932
9263
|
|
|
8933
9264
|
/** Draw text to the UI context
|
|
@@ -8942,12 +9273,27 @@ class UISystemPlugin
|
|
|
8942
9273
|
* @param {string} [fontStyle]
|
|
8943
9274
|
* @param {boolean} [applyMaxWidth=true]
|
|
8944
9275
|
* @param {Vector2} [textShadow]
|
|
8945
|
-
|
|
8946
|
-
|
|
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())
|
|
8947
9280
|
{
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
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';
|
|
8951
9297
|
}
|
|
8952
9298
|
|
|
8953
9299
|
/**
|
|
@@ -8961,7 +9307,7 @@ class UISystemPlugin
|
|
|
8961
9307
|
* @param {DragAndDropCallback} [onDrop] - when a file is dropped
|
|
8962
9308
|
* @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
|
|
8963
9309
|
* @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
|
|
8964
|
-
* @param {DragAndDropCallback} [onDragOver] -
|
|
9310
|
+
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
8965
9311
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
8966
9312
|
{
|
|
8967
9313
|
function setCallback(callback, listenerType)
|
|
@@ -8977,8 +9323,7 @@ class UISystemPlugin
|
|
|
8977
9323
|
|
|
8978
9324
|
/** Convert a screen space position to native UI position
|
|
8979
9325
|
* @param {Vector2} pos
|
|
8980
|
-
* @return {Vector2}
|
|
8981
|
-
*/
|
|
9326
|
+
* @return {Vector2} */
|
|
8982
9327
|
screenToNative(pos)
|
|
8983
9328
|
{
|
|
8984
9329
|
if (!uiSystem.nativeHeight)
|
|
@@ -9001,6 +9346,160 @@ class UISystemPlugin
|
|
|
9001
9346
|
for (const o of this.uiObjects)
|
|
9002
9347
|
o.parent || o.destroy();
|
|
9003
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
|
+
}
|
|
9004
9503
|
}
|
|
9005
9504
|
}
|
|
9006
9505
|
|
|
@@ -9056,7 +9555,13 @@ class UIObject
|
|
|
9056
9555
|
/** @property {number} - Override for text height */
|
|
9057
9556
|
this.textHeight = undefined;
|
|
9058
9557
|
/** @property {number} - Scale text to fit in the object */
|
|
9059
|
-
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;
|
|
9060
9565
|
/** @property {boolean} - Should this object be drawn */
|
|
9061
9566
|
this.visible = true;
|
|
9062
9567
|
/** @property {Array<UIObject>} - A list of this object's children */
|
|
@@ -9082,16 +9587,17 @@ class UIObject
|
|
|
9082
9587
|
/** @property {number} - Size of shadow blur */
|
|
9083
9588
|
this.shadowBlur = uiSystem.defaultShadowBlur;
|
|
9084
9589
|
/** @property {Vector2} - Offset of shadow blur */
|
|
9085
|
-
this.shadowOffset = uiSystem.defaultShadowOffset
|
|
9086
|
-
|
|
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;
|
|
9087
9595
|
|
|
9088
|
-
|
|
9089
|
-
this.textShadow = undefined;
|
|
9596
|
+
uiSystem.uiObjects.push(this);
|
|
9090
9597
|
}
|
|
9091
9598
|
|
|
9092
9599
|
/** Add a child UIObject to this object
|
|
9093
|
-
* @param {UIObject} child
|
|
9094
|
-
*/
|
|
9600
|
+
* @param {UIObject} child */
|
|
9095
9601
|
addChild(child)
|
|
9096
9602
|
{
|
|
9097
9603
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
@@ -9100,8 +9606,7 @@ class UIObject
|
|
|
9100
9606
|
}
|
|
9101
9607
|
|
|
9102
9608
|
/** Remove a child UIObject from this object
|
|
9103
|
-
* @param {UIObject} child
|
|
9104
|
-
*/
|
|
9609
|
+
* @param {UIObject} child */
|
|
9105
9610
|
removeChild(child)
|
|
9106
9611
|
{
|
|
9107
9612
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
@@ -9109,7 +9614,6 @@ class UIObject
|
|
|
9109
9614
|
child.parent = undefined;
|
|
9110
9615
|
}
|
|
9111
9616
|
|
|
9112
|
-
|
|
9113
9617
|
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
9114
9618
|
destroy()
|
|
9115
9619
|
{
|
|
@@ -9125,9 +9629,9 @@ class UIObject
|
|
|
9125
9629
|
child.destroy();
|
|
9126
9630
|
}
|
|
9127
9631
|
}
|
|
9632
|
+
|
|
9128
9633
|
/** Check if the mouse is overlapping a box in screen space
|
|
9129
|
-
* @return {boolean} - True if overlapping
|
|
9130
|
-
*/
|
|
9634
|
+
* @return {boolean} - True if overlapping */
|
|
9131
9635
|
isMouseOverlapping()
|
|
9132
9636
|
{
|
|
9133
9637
|
if (!mouseInWindow) return false;
|
|
@@ -9144,11 +9648,16 @@ class UIObject
|
|
|
9144
9648
|
// call the custom update callback
|
|
9145
9649
|
this.onUpdate();
|
|
9146
9650
|
|
|
9651
|
+
// unset active if disabled
|
|
9652
|
+
if (this.disabled && this == uiSystem.activeObject)
|
|
9653
|
+
uiSystem.activeObject = undefined;
|
|
9654
|
+
|
|
9147
9655
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
9148
9656
|
const isActive = this.isActiveObject();
|
|
9149
9657
|
const mouseDown = mouseIsDown(0);
|
|
9150
9658
|
const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
|
|
9151
9659
|
if (this.canBeHover)
|
|
9660
|
+
if (!uiSystem.navigationMode) // no mouse hover in navigation mode
|
|
9152
9661
|
if (mousePress || isActive || (!mouseDown && !isTouchDevice))
|
|
9153
9662
|
if (!uiSystem.hoverObject && this.isMouseOverlapping())
|
|
9154
9663
|
uiSystem.hoverObject = this;
|
|
@@ -9162,8 +9671,7 @@ class UIObject
|
|
|
9162
9671
|
{
|
|
9163
9672
|
if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
|
|
9164
9673
|
this.onPress();
|
|
9165
|
-
|
|
9166
|
-
this.soundPress.play();
|
|
9674
|
+
this.soundPress && this.soundPress.play();
|
|
9167
9675
|
if (uiSystem.activeObject && !isActive)
|
|
9168
9676
|
uiSystem.activeObject.onRelease();
|
|
9169
9677
|
uiSystem.activeObject = this;
|
|
@@ -9172,10 +9680,10 @@ class UIObject
|
|
|
9172
9680
|
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
9173
9681
|
{
|
|
9174
9682
|
this.onClick();
|
|
9175
|
-
|
|
9176
|
-
this.soundClick.play();
|
|
9683
|
+
this.soundClick && this.soundClick.play();
|
|
9177
9684
|
}
|
|
9178
9685
|
}
|
|
9686
|
+
|
|
9179
9687
|
// clear mouse was pressed state even when disabled
|
|
9180
9688
|
mousePress && inputClearKey(0,0,0,1,0);
|
|
9181
9689
|
}
|
|
@@ -9183,8 +9691,7 @@ class UIObject
|
|
|
9183
9691
|
if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
|
|
9184
9692
|
{
|
|
9185
9693
|
this.onRelease();
|
|
9186
|
-
|
|
9187
|
-
this.soundRelease.play();
|
|
9694
|
+
this.soundRelease && this.soundRelease.play();
|
|
9188
9695
|
uiSystem.activeObject = undefined;
|
|
9189
9696
|
}
|
|
9190
9697
|
|
|
@@ -9196,29 +9703,40 @@ class UIObject
|
|
|
9196
9703
|
/** Render the object, called automatically by plugin once each frame */
|
|
9197
9704
|
render()
|
|
9198
9705
|
{
|
|
9199
|
-
|
|
9706
|
+
// call the custom render callback
|
|
9707
|
+
this.onRender();
|
|
9200
9708
|
|
|
9201
|
-
|
|
9202
|
-
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
9203
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
9204
|
-
}
|
|
9709
|
+
if (!this.size.x || !this.size.y) return;
|
|
9205
9710
|
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
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);
|
|
9212
9724
|
}
|
|
9213
9725
|
|
|
9214
9726
|
/** Get the size for text with overrides and scale
|
|
9215
|
-
* @return {Vector2}
|
|
9216
|
-
*/
|
|
9727
|
+
* @return {Vector2} */
|
|
9217
9728
|
getTextSize()
|
|
9218
9729
|
{
|
|
9219
9730
|
return vec2(
|
|
9220
|
-
this.textWidth || this.
|
|
9221
|
-
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();
|
|
9222
9740
|
}
|
|
9223
9741
|
|
|
9224
9742
|
/** @return {boolean} - Is the mouse hovering over this element */
|
|
@@ -9227,9 +9745,51 @@ class UIObject
|
|
|
9227
9745
|
/** @return {boolean} - Is the mouse held onto this element */
|
|
9228
9746
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
9229
9747
|
|
|
9230
|
-
/**
|
|
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 */
|
|
9231
9788
|
onUpdate() {}
|
|
9232
9789
|
|
|
9790
|
+
/** Called each frame before object renders */
|
|
9791
|
+
onRender() {}
|
|
9792
|
+
|
|
9233
9793
|
/** Called when the mouse enters the object */
|
|
9234
9794
|
onEnter() {}
|
|
9235
9795
|
|
|
@@ -9277,16 +9837,25 @@ class UIText extends UIObject
|
|
|
9277
9837
|
this.align = align;
|
|
9278
9838
|
this.font = font;
|
|
9279
9839
|
|
|
9280
|
-
// make text not outlined by default
|
|
9281
|
-
this.lineWidth = 0;
|
|
9282
9840
|
// text can not be a hover object by default
|
|
9283
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;
|
|
9284
9851
|
}
|
|
9285
9852
|
render()
|
|
9286
9853
|
{
|
|
9287
|
-
|
|
9854
|
+
super.render();
|
|
9855
|
+
|
|
9856
|
+
// render the text
|
|
9288
9857
|
const textSize = this.getTextSize();
|
|
9289
|
-
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);
|
|
9290
9859
|
}
|
|
9291
9860
|
}
|
|
9292
9861
|
|
|
@@ -9322,10 +9891,13 @@ class UITile extends UIObject
|
|
|
9322
9891
|
this.mirror = mirror;
|
|
9323
9892
|
// set properties
|
|
9324
9893
|
this.color = color.copy();
|
|
9894
|
+
|
|
9895
|
+
// no shadow by default
|
|
9896
|
+
this.shadowColor = CLEAR_BLACK;
|
|
9325
9897
|
}
|
|
9326
9898
|
render()
|
|
9327
9899
|
{
|
|
9328
|
-
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);
|
|
9329
9901
|
}
|
|
9330
9902
|
}
|
|
9331
9903
|
|
|
@@ -9350,6 +9922,9 @@ class UIButton extends UIObject
|
|
|
9350
9922
|
ASSERT(isString(text), 'ui button must be a string');
|
|
9351
9923
|
ASSERT(isColor(color), 'ui button color must be a color');
|
|
9352
9924
|
|
|
9925
|
+
/** @property {Vector2} - Text offset for the button */
|
|
9926
|
+
this.textOffset = vec2();
|
|
9927
|
+
|
|
9353
9928
|
// set properties
|
|
9354
9929
|
this.text = text;
|
|
9355
9930
|
this.color = color.copy();
|
|
@@ -9361,8 +9936,8 @@ class UIButton extends UIObject
|
|
|
9361
9936
|
|
|
9362
9937
|
// draw the text scaled to fit
|
|
9363
9938
|
const textSize = this.getTextSize();
|
|
9364
|
-
uiSystem.drawText(this.text, this.pos, textSize,
|
|
9365
|
-
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);
|
|
9366
9941
|
}
|
|
9367
9942
|
}
|
|
9368
9943
|
|
|
@@ -9416,7 +9991,7 @@ class UICheckbox extends UIObject
|
|
|
9416
9991
|
const textSize = this.getTextSize();
|
|
9417
9992
|
const pos = this.pos.add(vec2(this.size.x,0));
|
|
9418
9993
|
uiSystem.drawText(this.text, pos, textSize,
|
|
9419
|
-
this.textColor,
|
|
9994
|
+
this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
9420
9995
|
}
|
|
9421
9996
|
}
|
|
9422
9997
|
|
|
@@ -9458,7 +10033,11 @@ class UIScrollbar extends UIObject
|
|
|
9458
10033
|
update()
|
|
9459
10034
|
{
|
|
9460
10035
|
super.update();
|
|
9461
|
-
if (this.
|
|
10036
|
+
if (!this.interactive)
|
|
10037
|
+
return;
|
|
10038
|
+
|
|
10039
|
+
const oldValue = this.value;
|
|
10040
|
+
if (this.isActiveObject())
|
|
9462
10041
|
{
|
|
9463
10042
|
// handle horizontal or vertical scrollbar
|
|
9464
10043
|
const isHorizontal = this.size.x > this.size.y;
|
|
@@ -9470,14 +10049,19 @@ class UIScrollbar extends UIObject
|
|
|
9470
10049
|
const handleWidth = barSize - handleSize;
|
|
9471
10050
|
const p1 = centerPos - handleWidth/2;
|
|
9472
10051
|
const p2 = centerPos + handleWidth/2;
|
|
9473
|
-
const oldValue = this.value;
|
|
9474
|
-
|
|
9475
10052
|
const p = uiSystem.screenToNative(mousePosScreen);
|
|
9476
10053
|
this.value = isHorizontal ?
|
|
9477
10054
|
percent(p.x, p1, p2) :
|
|
9478
10055
|
percent(p.y, p2, p1);
|
|
9479
|
-
this.value === oldValue || this.onChange();
|
|
9480
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();
|
|
9481
10065
|
}
|
|
9482
10066
|
render()
|
|
9483
10067
|
{
|
|
@@ -9502,7 +10086,14 @@ class UIScrollbar extends UIObject
|
|
|
9502
10086
|
// draw the text scaled to fit on the scrollbar
|
|
9503
10087
|
const textSize = this.getTextSize();
|
|
9504
10088
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
9505
|
-
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();
|
|
9506
10097
|
}
|
|
9507
10098
|
}
|
|
9508
10099
|
|
|
@@ -9536,7 +10127,7 @@ class UIVideo extends UIObject
|
|
|
9536
10127
|
this.color = BLACK; // default to black background
|
|
9537
10128
|
this.cornerRadius = 0; // default to no corner radius
|
|
9538
10129
|
|
|
9539
|
-
/** @property {
|
|
10130
|
+
/** @property {number} - The video volume */
|
|
9540
10131
|
this.volume = volume;
|
|
9541
10132
|
|
|
9542
10133
|
// create video element
|
|
@@ -9569,7 +10160,7 @@ class UIVideo extends UIObject
|
|
|
9569
10160
|
|
|
9570
10161
|
/** Check if video is currently loading
|
|
9571
10162
|
* @return {boolean} */
|
|
9572
|
-
|
|
10163
|
+
isLoading()
|
|
9573
10164
|
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
9574
10165
|
|
|
9575
10166
|
/** Check if video is currently paused
|
|
@@ -9579,7 +10170,7 @@ class UIVideo extends UIObject
|
|
|
9579
10170
|
/** Check if video is currently playing
|
|
9580
10171
|
* @return {boolean} */
|
|
9581
10172
|
isPlaying()
|
|
9582
|
-
{ return !this.isPaused() && !this.hasEnded() && !this.
|
|
10173
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoading(); }
|
|
9583
10174
|
|
|
9584
10175
|
/** Check if video has ended playing
|
|
9585
10176
|
* @return {boolean} */
|
|
@@ -9628,7 +10219,7 @@ class UIVideo extends UIObject
|
|
|
9628
10219
|
{
|
|
9629
10220
|
super.render();
|
|
9630
10221
|
|
|
9631
|
-
if (this.
|
|
10222
|
+
if (this.isLoading())
|
|
9632
10223
|
return;
|
|
9633
10224
|
const context = uiSystem.uiContext;
|
|
9634
10225
|
const s = this.size;
|
|
@@ -11412,7 +12003,7 @@ class Box2dPlugin
|
|
|
11412
12003
|
* @param {Vector2} v */
|
|
11413
12004
|
vec2dTo(v)
|
|
11414
12005
|
{
|
|
11415
|
-
ASSERT(v
|
|
12006
|
+
ASSERT(isVector2(v));
|
|
11416
12007
|
return new box2d.instance.b2Vec2(v.x, v.y);
|
|
11417
12008
|
}
|
|
11418
12009
|
|
|
@@ -11829,6 +12420,7 @@ export
|
|
|
11829
12420
|
setInputWASDEmulateDirection,
|
|
11830
12421
|
setTouchGamepadEnable,
|
|
11831
12422
|
setTouchGamepadCenterButton,
|
|
12423
|
+
setTouchGamepadButtonCount,
|
|
11832
12424
|
setTouchGamepadAnalog,
|
|
11833
12425
|
setTouchGamepadSize,
|
|
11834
12426
|
setTouchGamepadAlpha,
|
|
@@ -12000,12 +12592,14 @@ export
|
|
|
12000
12592
|
mouseInWindow,
|
|
12001
12593
|
isUsingGamepad,
|
|
12002
12594
|
inputPreventDefault,
|
|
12595
|
+
gamepadPrimary,
|
|
12003
12596
|
setInputPreventDefault,
|
|
12004
12597
|
gamepadIsDown,
|
|
12005
12598
|
gamepadWasPressed,
|
|
12006
12599
|
gamepadWasReleased,
|
|
12007
12600
|
gamepadStick,
|
|
12008
|
-
|
|
12601
|
+
gamepadDpad,
|
|
12602
|
+
gamepadConnected,
|
|
12009
12603
|
vibrate,
|
|
12010
12604
|
vibrateStop,
|
|
12011
12605
|
isTouchDevice,
|
|
@@ -12071,6 +12665,8 @@ export
|
|
|
12071
12665
|
|
|
12072
12666
|
// UI System
|
|
12073
12667
|
uiSystem,
|
|
12668
|
+
uiDebug,
|
|
12669
|
+
uiSetDebug,
|
|
12074
12670
|
UISystemPlugin,
|
|
12075
12671
|
UIObject,
|
|
12076
12672
|
UIText,
|