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.release.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
|
|
|
@@ -1383,9 +1383,15 @@ class Vector2
|
|
|
1383
1383
|
{
|
|
1384
1384
|
this.x = x;
|
|
1385
1385
|
this.y = y;
|
|
1386
|
+
ASSERT_VECTOR2_VALID(this);
|
|
1386
1387
|
return this;
|
|
1387
1388
|
}
|
|
1388
1389
|
|
|
1390
|
+
/** Sets this vector from another vector and returns self
|
|
1391
|
+
* @param {Vector2} v - other vector
|
|
1392
|
+
* @return {Vector2} */
|
|
1393
|
+
setFrom(v) { return this.set(v.x, v.y); }
|
|
1394
|
+
|
|
1389
1395
|
/** Returns a new vector that is a copy of this
|
|
1390
1396
|
* @return {Vector2} */
|
|
1391
1397
|
copy() { return new Vector2(this.x, this.y); }
|
|
@@ -1448,7 +1454,7 @@ class Vector2
|
|
|
1448
1454
|
clampLength(length=1)
|
|
1449
1455
|
{
|
|
1450
1456
|
const l = this.length();
|
|
1451
|
-
return l > length ? this.scale(length/l) : this;
|
|
1457
|
+
return l > length ? this.scale(length/l) : this.copy();
|
|
1452
1458
|
}
|
|
1453
1459
|
|
|
1454
1460
|
/** Returns the dot product of this and the vector passed in
|
|
@@ -1535,6 +1541,10 @@ class Vector2
|
|
|
1535
1541
|
* @return {number} */
|
|
1536
1542
|
area() { return abs(this.x * this.y); }
|
|
1537
1543
|
|
|
1544
|
+
/** Returns true if this vector is (0,0)
|
|
1545
|
+
* @return {boolean} */
|
|
1546
|
+
isZero() { return !this.x && !this.y; }
|
|
1547
|
+
|
|
1538
1548
|
/** Returns a new vector that is p percent between this and the vector passed in
|
|
1539
1549
|
* @param {Vector2} v - other vector
|
|
1540
1550
|
* @param {number} percent
|
|
@@ -1645,9 +1655,15 @@ class Color
|
|
|
1645
1655
|
this.g = g;
|
|
1646
1656
|
this.b = b;
|
|
1647
1657
|
this.a = a;
|
|
1658
|
+
ASSERT_COLOR_VALID(this);
|
|
1648
1659
|
return this;
|
|
1649
1660
|
}
|
|
1650
1661
|
|
|
1662
|
+
/** Sets this color from another color and returns self
|
|
1663
|
+
* @param {Color} c - other color
|
|
1664
|
+
* @return {Color} */
|
|
1665
|
+
setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
|
|
1666
|
+
|
|
1651
1667
|
/** Returns a new color that is a copy of this
|
|
1652
1668
|
* @return {Color} */
|
|
1653
1669
|
copy() { return new Color(this.r, this.g, this.b, this.a); }
|
|
@@ -2199,17 +2215,17 @@ let touchGamepadEnable = false;
|
|
|
2199
2215
|
* @memberof Settings */
|
|
2200
2216
|
let touchGamepadCenterButton = true;
|
|
2201
2217
|
|
|
2202
|
-
/**
|
|
2203
|
-
* @type {
|
|
2218
|
+
/** Number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
|
|
2219
|
+
* @type {number}
|
|
2204
2220
|
* @default
|
|
2205
2221
|
* @memberof Settings */
|
|
2206
|
-
let
|
|
2222
|
+
let touchGamepadButtonCount = 4;
|
|
2207
2223
|
|
|
2208
|
-
/**
|
|
2209
|
-
* @type {
|
|
2224
|
+
/** True if touch gamepad should be analog stick or false to use if 8 way dpad
|
|
2225
|
+
* @type {boolean}
|
|
2210
2226
|
* @default
|
|
2211
2227
|
* @memberof Settings */
|
|
2212
|
-
let
|
|
2228
|
+
let touchGamepadAnalog = true;
|
|
2213
2229
|
|
|
2214
2230
|
/** Size of virtual gamepad for touch devices in pixels
|
|
2215
2231
|
* @type {number}
|
|
@@ -2475,6 +2491,11 @@ function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
|
2475
2491
|
* @memberof Settings */
|
|
2476
2492
|
function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
|
|
2477
2493
|
|
|
2494
|
+
/** Set number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
|
|
2495
|
+
* @param {number} count
|
|
2496
|
+
* @memberof Settings */
|
|
2497
|
+
function setTouchGamepadButtonCount(count) { touchGamepadButtonCount = count; }
|
|
2498
|
+
|
|
2478
2499
|
/** Set if touch gamepad should be analog stick or 8 way dpad
|
|
2479
2500
|
* @param {boolean} analog
|
|
2480
2501
|
* @memberof Settings */
|
|
@@ -2683,7 +2704,7 @@ class EngineObject
|
|
|
2683
2704
|
child.updateTransforms();
|
|
2684
2705
|
}
|
|
2685
2706
|
|
|
2686
|
-
/** Update the object physics, called automatically by engine once each frame */
|
|
2707
|
+
/** Update the object physics, called automatically by engine once each frame. Can be overridden to stop or change how physics works for an object. */
|
|
2687
2708
|
updatePhysics()
|
|
2688
2709
|
{
|
|
2689
2710
|
// child objects do not have physics
|
|
@@ -2716,7 +2737,7 @@ class EngineObject
|
|
|
2716
2737
|
if (!enablePhysicsSolver || !this.mass) // don't do collision for static objects
|
|
2717
2738
|
return;
|
|
2718
2739
|
|
|
2719
|
-
const
|
|
2740
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
2720
2741
|
if (this.groundObject)
|
|
2721
2742
|
{
|
|
2722
2743
|
// apply friction in local space of ground object
|
|
@@ -2772,10 +2793,10 @@ class EngineObject
|
|
|
2772
2793
|
{
|
|
2773
2794
|
// push outside object collision
|
|
2774
2795
|
this.pos.y = o.pos.y + (sizeBoth.y/2 + epsilon) * sign(oldPos.y - o.pos.y);
|
|
2775
|
-
if ((o.groundObject &&
|
|
2796
|
+
if ((o.groundObject && wasFalling) || !o.mass)
|
|
2776
2797
|
{
|
|
2777
2798
|
// set ground object if landed on something
|
|
2778
|
-
if (
|
|
2799
|
+
if (wasFalling)
|
|
2779
2800
|
this.groundObject = o;
|
|
2780
2801
|
|
|
2781
2802
|
// bounce if other object is fixed or grounded
|
|
@@ -2862,12 +2883,15 @@ class EngineObject
|
|
|
2862
2883
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
2863
2884
|
this.velocity.y *= -restitution;
|
|
2864
2885
|
|
|
2865
|
-
if (
|
|
2886
|
+
if (wasFalling)
|
|
2866
2887
|
{
|
|
2867
|
-
// adjust position to slightly
|
|
2888
|
+
// adjust position to slightly away from nearest tile
|
|
2868
2889
|
// this prevents gap between object and ground
|
|
2869
2890
|
const epsilon = .0001;
|
|
2870
|
-
|
|
2891
|
+
const offset = this.size.y/2 + epsilon;
|
|
2892
|
+
this.pos.y = gravity.y < 0 ?
|
|
2893
|
+
floor(oldPos.y-this.size.y/2) + offset :
|
|
2894
|
+
ceil( oldPos.y+this.size.y/2) - offset;
|
|
2871
2895
|
|
|
2872
2896
|
// set ground object for tile collision
|
|
2873
2897
|
this.groundObject = hitLayer;
|
|
@@ -3036,21 +3060,20 @@ class EngineObject
|
|
|
3036
3060
|
* @return {string} */
|
|
3037
3061
|
toString()
|
|
3038
3062
|
{
|
|
3039
|
-
if (debug)
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
}
|
|
3063
|
+
if (!debug) return;
|
|
3064
|
+
|
|
3065
|
+
let text = 'type = ' + this.constructor.name;
|
|
3066
|
+
if (this.pos.x || this.pos.y)
|
|
3067
|
+
text += '\npos = ' + this.pos;
|
|
3068
|
+
if (this.velocity.x || this.velocity.y)
|
|
3069
|
+
text += '\nvelocity = ' + this.velocity;
|
|
3070
|
+
if (this.size.x || this.size.y)
|
|
3071
|
+
text += '\nsize = ' + this.size;
|
|
3072
|
+
if (this.angle)
|
|
3073
|
+
text += '\nangle = ' + this.angle.toFixed(3);
|
|
3074
|
+
if (this.color)
|
|
3075
|
+
text += '\ncolor = ' + this.color;
|
|
3076
|
+
return text;
|
|
3054
3077
|
}
|
|
3055
3078
|
|
|
3056
3079
|
/** Render debug info for this object */
|
|
@@ -3219,7 +3242,7 @@ class TileInfo
|
|
|
3219
3242
|
this.padding = padding;
|
|
3220
3243
|
/** @property {TextureInfo} - The texture info for this tile */
|
|
3221
3244
|
this.textureInfo = textureInfos[this.textureIndex];
|
|
3222
|
-
/** @property {
|
|
3245
|
+
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
3223
3246
|
this.bleedScale = bleedScale;
|
|
3224
3247
|
}
|
|
3225
3248
|
|
|
@@ -3706,7 +3729,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
3706
3729
|
|
|
3707
3730
|
/** Draw text on main canvas in world space
|
|
3708
3731
|
* Automatically splits new lines into rows
|
|
3709
|
-
* @param {string} text
|
|
3732
|
+
* @param {string|number} text
|
|
3710
3733
|
* @param {Vector2} pos
|
|
3711
3734
|
* @param {number} [size]
|
|
3712
3735
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3733,7 +3756,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
3733
3756
|
|
|
3734
3757
|
/** Draw text on overlay canvas in world space
|
|
3735
3758
|
* Automatically splits new lines into rows
|
|
3736
|
-
* @param {string} text
|
|
3759
|
+
* @param {string|number} text
|
|
3737
3760
|
* @param {Vector2} pos
|
|
3738
3761
|
* @param {number} [size]
|
|
3739
3762
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3752,7 +3775,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
3752
3775
|
|
|
3753
3776
|
/** Draw text on overlay canvas in screen space
|
|
3754
3777
|
* Automatically splits new lines into rows
|
|
3755
|
-
* @param {string} text
|
|
3778
|
+
* @param {string|number} text
|
|
3756
3779
|
* @param {Vector2} pos
|
|
3757
3780
|
* @param {number} [size]
|
|
3758
3781
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3917,11 +3940,28 @@ function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
|
|
|
3917
3940
|
];
|
|
3918
3941
|
}
|
|
3919
3942
|
|
|
3920
|
-
/** Get the camera
|
|
3943
|
+
/** Get the size of the camera window in world space
|
|
3921
3944
|
* @return {Vector2}
|
|
3922
3945
|
* @memberof Draw */
|
|
3923
3946
|
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
3924
3947
|
|
|
3948
|
+
/** Check if a point or circle is on screen
|
|
3949
|
+
* If size is a Vector2, uses the largest dimension as diameter
|
|
3950
|
+
* This can be used to cull offscreen objects from render or update
|
|
3951
|
+
* @param {Vector2} pos - world space position
|
|
3952
|
+
* @param {Vector2|number} size - world space size or diameter
|
|
3953
|
+
* @return {boolean}
|
|
3954
|
+
* @memberof Draw */
|
|
3955
|
+
function isOnScreen(pos, size=0)
|
|
3956
|
+
{
|
|
3957
|
+
pos = worldToScreen(pos);
|
|
3958
|
+
if (size instanceof Vector2)
|
|
3959
|
+
size = max(size.x, size.y); // use largest dimension
|
|
3960
|
+
size *= cameraScale/2;
|
|
3961
|
+
return pos.x + size > 0 && pos.x - size < mainCanvasSize.x &&
|
|
3962
|
+
pos.y + size > 0 && pos.y - size < mainCanvasSize.y;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3925
3965
|
/** Enable normal or additive blend mode
|
|
3926
3966
|
* @param {boolean} [additive]
|
|
3927
3967
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -4085,7 +4125,7 @@ class FontImage
|
|
|
4085
4125
|
}
|
|
4086
4126
|
|
|
4087
4127
|
/** Draw text in world space using the image font
|
|
4088
|
-
* @param {string} text
|
|
4128
|
+
* @param {string|number} text
|
|
4089
4129
|
* @param {Vector2} pos
|
|
4090
4130
|
* @param {number} [scale=.25]
|
|
4091
4131
|
* @param {boolean} [center]
|
|
@@ -4097,7 +4137,7 @@ class FontImage
|
|
|
4097
4137
|
}
|
|
4098
4138
|
|
|
4099
4139
|
/** Draw text on overlay canvas in world space using the image font
|
|
4100
|
-
* @param {string} text
|
|
4140
|
+
* @param {string|number} text
|
|
4101
4141
|
* @param {Vector2} pos
|
|
4102
4142
|
* @param {number} [scale]
|
|
4103
4143
|
* @param {boolean} [center]
|
|
@@ -4106,7 +4146,7 @@ class FontImage
|
|
|
4106
4146
|
{ this.drawText(text, pos, scale, center, overlayContext); }
|
|
4107
4147
|
|
|
4108
4148
|
/** Draw text on overlay canvas in screen space using the image font
|
|
4109
|
-
* @param {string} text
|
|
4149
|
+
* @param {string|number} text
|
|
4110
4150
|
* @param {Vector2} pos
|
|
4111
4151
|
* @param {number} [scale]
|
|
4112
4152
|
* @param {boolean} [center]
|
|
@@ -4150,6 +4190,85 @@ class FontImage
|
|
|
4150
4190
|
* @namespace Input
|
|
4151
4191
|
*/
|
|
4152
4192
|
|
|
4193
|
+
/** Mouse pos in world space
|
|
4194
|
+
* @type {Vector2}
|
|
4195
|
+
* @memberof Input */
|
|
4196
|
+
let mousePos = vec2();
|
|
4197
|
+
|
|
4198
|
+
/** Mouse pos in screen space
|
|
4199
|
+
* @type {Vector2}
|
|
4200
|
+
* @memberof Input */
|
|
4201
|
+
let mousePosScreen = vec2();
|
|
4202
|
+
|
|
4203
|
+
/** Mouse movement delta in world space
|
|
4204
|
+
* @type {Vector2}
|
|
4205
|
+
* @memberof Input */
|
|
4206
|
+
let mouseDelta = vec2();
|
|
4207
|
+
|
|
4208
|
+
/** Mouse movement delta in screen space
|
|
4209
|
+
* @type {Vector2}
|
|
4210
|
+
* @memberof Input */
|
|
4211
|
+
let mouseDeltaScreen = vec2();
|
|
4212
|
+
|
|
4213
|
+
/** Mouse wheel delta this frame
|
|
4214
|
+
* @type {number}
|
|
4215
|
+
* @memberof Input */
|
|
4216
|
+
let mouseWheel = 0;
|
|
4217
|
+
|
|
4218
|
+
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4219
|
+
* @type {boolean}
|
|
4220
|
+
* @memberof Input */
|
|
4221
|
+
let mouseInWindow = true;
|
|
4222
|
+
|
|
4223
|
+
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4224
|
+
* @type {boolean}
|
|
4225
|
+
* @memberof Input */
|
|
4226
|
+
let isUsingGamepad = false;
|
|
4227
|
+
|
|
4228
|
+
/** Prevents input continuing to the default browser handling (true by default)
|
|
4229
|
+
* @type {boolean}
|
|
4230
|
+
* @memberof Input */
|
|
4231
|
+
let inputPreventDefault = true;
|
|
4232
|
+
|
|
4233
|
+
/** Primary gamepad index, automatically set to first gamepad with input
|
|
4234
|
+
* @type {number}
|
|
4235
|
+
* @memberof Input */
|
|
4236
|
+
let gamepadPrimary = 0;
|
|
4237
|
+
|
|
4238
|
+
/** Prevents input continuing to the default browser handling
|
|
4239
|
+
* This is useful to disable for html menus so the browser can handle input normally
|
|
4240
|
+
* @param {boolean} preventDefault
|
|
4241
|
+
* @memberof Input */
|
|
4242
|
+
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
4243
|
+
|
|
4244
|
+
/** Clears an input key state
|
|
4245
|
+
* @param {string|number} key
|
|
4246
|
+
* @param {number} [device]
|
|
4247
|
+
* @param {boolean} [clearDown=true]
|
|
4248
|
+
* @param {boolean} [clearPressed=true]
|
|
4249
|
+
* @param {boolean} [clearReleased=true]
|
|
4250
|
+
* @memberof Input */
|
|
4251
|
+
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4252
|
+
{
|
|
4253
|
+
if (!inputData[device])
|
|
4254
|
+
return;
|
|
4255
|
+
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
/** Clears all input
|
|
4259
|
+
* @memberof Input */
|
|
4260
|
+
function inputClear()
|
|
4261
|
+
{
|
|
4262
|
+
inputData.length = 0;
|
|
4263
|
+
inputData[0] = [];
|
|
4264
|
+
touchGamepadButtons.length = 0;
|
|
4265
|
+
touchGamepadSticks.length = 0;
|
|
4266
|
+
gamepadStickData.length = 0;
|
|
4267
|
+
gamepadDpadData.length = 0;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4271
|
+
|
|
4153
4272
|
/** Returns true if device key is down
|
|
4154
4273
|
* @param {string|number} key
|
|
4155
4274
|
* @param {number} [device]
|
|
@@ -4157,9 +4276,9 @@ class FontImage
|
|
|
4157
4276
|
* @memberof Input */
|
|
4158
4277
|
function keyIsDown(key, device=0)
|
|
4159
4278
|
{
|
|
4160
|
-
ASSERT(key
|
|
4279
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4161
4280
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4162
|
-
return
|
|
4281
|
+
return !!(inputData[device]?.[key] & 1);
|
|
4163
4282
|
}
|
|
4164
4283
|
|
|
4165
4284
|
/** Returns true if device key was pressed this frame
|
|
@@ -4169,9 +4288,9 @@ function keyIsDown(key, device=0)
|
|
|
4169
4288
|
* @memberof Input */
|
|
4170
4289
|
function keyWasPressed(key, device=0)
|
|
4171
4290
|
{
|
|
4172
|
-
ASSERT(key
|
|
4291
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4173
4292
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4174
|
-
return
|
|
4293
|
+
return !!(inputData[device]?.[key] & 2);
|
|
4175
4294
|
}
|
|
4176
4295
|
|
|
4177
4296
|
/** Returns true if device key was released this frame
|
|
@@ -4181,172 +4300,182 @@ function keyWasPressed(key, device=0)
|
|
|
4181
4300
|
* @memberof Input */
|
|
4182
4301
|
function keyWasReleased(key, device=0)
|
|
4183
4302
|
{
|
|
4184
|
-
ASSERT(key
|
|
4303
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4185
4304
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4186
|
-
return
|
|
4305
|
+
return !!(inputData[device]?.[key] & 4);
|
|
4187
4306
|
}
|
|
4188
4307
|
|
|
4189
4308
|
/** Returns input vector from arrow keys or WASD if enabled
|
|
4309
|
+
* @param {string} [up]
|
|
4310
|
+
* @param {string} [down]
|
|
4311
|
+
* @param {string} [left]
|
|
4312
|
+
* @param {string} [right]
|
|
4190
4313
|
* @return {Vector2}
|
|
4191
4314
|
* @memberof Input */
|
|
4192
4315
|
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
4193
4316
|
{
|
|
4317
|
+
ASSERT(isString(up), 'up key must be a string');
|
|
4318
|
+
ASSERT(isString(down), 'down key must be a string');
|
|
4319
|
+
ASSERT(isString(left), 'left key must be a string');
|
|
4320
|
+
ASSERT(isString(right), 'right key must be a string');
|
|
4194
4321
|
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
4195
4322
|
return vec2(k(right) - k(left), k(up) - k(down));
|
|
4196
4323
|
}
|
|
4197
4324
|
|
|
4198
|
-
/** Clears all input
|
|
4199
|
-
* @memberof Input */
|
|
4200
|
-
function inputClear() { inputData = [[]]; touchGamepadButtons = []; }
|
|
4201
|
-
|
|
4202
|
-
/** Clears an input key state
|
|
4203
|
-
* @param {string|number} key
|
|
4204
|
-
* @param {number} [device]
|
|
4205
|
-
* @param {boolean} [clearDown=true]
|
|
4206
|
-
* @param {boolean} [clearPressed=true]
|
|
4207
|
-
* @param {boolean} [clearReleased=true]
|
|
4208
|
-
* @memberof Input */
|
|
4209
|
-
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4210
|
-
{
|
|
4211
|
-
if (!inputData[device])
|
|
4212
|
-
return;
|
|
4213
|
-
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4214
|
-
}
|
|
4215
|
-
|
|
4216
4325
|
/** Returns true if mouse button is down
|
|
4217
4326
|
* @function
|
|
4218
4327
|
* @param {number} button
|
|
4219
4328
|
* @return {boolean}
|
|
4220
4329
|
* @memberof Input */
|
|
4221
|
-
function mouseIsDown(button)
|
|
4330
|
+
function mouseIsDown(button)
|
|
4331
|
+
{
|
|
4332
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
4333
|
+
return keyIsDown(button);
|
|
4334
|
+
}
|
|
4222
4335
|
|
|
4223
4336
|
/** Returns true if mouse button was pressed
|
|
4224
4337
|
* @function
|
|
4225
4338
|
* @param {number} button
|
|
4226
4339
|
* @return {boolean}
|
|
4227
4340
|
* @memberof Input */
|
|
4228
|
-
function mouseWasPressed(button)
|
|
4341
|
+
function mouseWasPressed(button)
|
|
4342
|
+
{
|
|
4343
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
4344
|
+
return keyWasPressed(button);
|
|
4345
|
+
}
|
|
4229
4346
|
|
|
4230
4347
|
/** Returns true if mouse button was released
|
|
4231
4348
|
* @function
|
|
4232
4349
|
* @param {number} button
|
|
4233
4350
|
* @return {boolean}
|
|
4234
4351
|
* @memberof Input */
|
|
4235
|
-
function mouseWasReleased(button)
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
let mousePos = vec2();
|
|
4241
|
-
|
|
4242
|
-
/** Mouse pos in screen space
|
|
4243
|
-
* @type {Vector2}
|
|
4244
|
-
* @memberof Input */
|
|
4245
|
-
let mousePosScreen = vec2();
|
|
4246
|
-
|
|
4247
|
-
/** Mouse movement delta in world space
|
|
4248
|
-
* @type {Vector2}
|
|
4249
|
-
* @memberof Input */
|
|
4250
|
-
let mouseDelta = vec2();
|
|
4251
|
-
|
|
4252
|
-
/** Mouse movement delta in screen space
|
|
4253
|
-
* @type {Vector2}
|
|
4254
|
-
* @memberof Input */
|
|
4255
|
-
let mouseDeltaScreen = vec2();
|
|
4256
|
-
|
|
4257
|
-
/** Mouse wheel delta this frame
|
|
4258
|
-
* @type {number}
|
|
4259
|
-
* @memberof Input */
|
|
4260
|
-
let mouseWheel = 0;
|
|
4261
|
-
|
|
4262
|
-
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4263
|
-
* @type {boolean}
|
|
4264
|
-
* @memberof Input */
|
|
4265
|
-
let mouseInWindow = true;
|
|
4266
|
-
|
|
4267
|
-
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4268
|
-
* @type {boolean}
|
|
4269
|
-
* @memberof Input */
|
|
4270
|
-
let isUsingGamepad = false;
|
|
4271
|
-
|
|
4272
|
-
/** Prevents input continuing to the default browser handling (true by default)
|
|
4273
|
-
* @type {boolean}
|
|
4274
|
-
* @memberof Input */
|
|
4275
|
-
let inputPreventDefault = true;
|
|
4276
|
-
|
|
4277
|
-
/** Prevents input continuing to the default browser handling
|
|
4278
|
-
* This is useful to disable for html menus so the browser can handle input normally
|
|
4279
|
-
* @param {boolean} preventDefault
|
|
4280
|
-
* @memberof Input */
|
|
4281
|
-
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
4352
|
+
function mouseWasReleased(button)
|
|
4353
|
+
{
|
|
4354
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
4355
|
+
return keyWasReleased(button);
|
|
4356
|
+
}
|
|
4282
4357
|
|
|
4283
4358
|
/** Returns true if gamepad button is down
|
|
4284
4359
|
* @param {number} button
|
|
4285
4360
|
* @param {number} [gamepad]
|
|
4286
4361
|
* @return {boolean}
|
|
4287
4362
|
* @memberof Input */
|
|
4288
|
-
function gamepadIsDown(button, gamepad=
|
|
4289
|
-
{
|
|
4363
|
+
function gamepadIsDown(button, gamepad=gamepadPrimary)
|
|
4364
|
+
{
|
|
4365
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
4366
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4367
|
+
return keyIsDown(button, gamepad+1);
|
|
4368
|
+
}
|
|
4290
4369
|
|
|
4291
4370
|
/** Returns true if gamepad button was pressed
|
|
4292
4371
|
* @param {number} button
|
|
4293
4372
|
* @param {number} [gamepad]
|
|
4294
4373
|
* @return {boolean}
|
|
4295
4374
|
* @memberof Input */
|
|
4296
|
-
function gamepadWasPressed(button, gamepad=
|
|
4297
|
-
{
|
|
4375
|
+
function gamepadWasPressed(button, gamepad=gamepadPrimary)
|
|
4376
|
+
{
|
|
4377
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
4378
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4379
|
+
return keyWasPressed(button, gamepad+1);
|
|
4380
|
+
}
|
|
4298
4381
|
|
|
4299
4382
|
/** Returns true if gamepad button was released
|
|
4300
4383
|
* @param {number} button
|
|
4301
4384
|
* @param {number} [gamepad]
|
|
4302
4385
|
* @return {boolean}
|
|
4303
4386
|
* @memberof Input */
|
|
4304
|
-
function gamepadWasReleased(button, gamepad=
|
|
4305
|
-
{
|
|
4387
|
+
function gamepadWasReleased(button, gamepad=gamepadPrimary)
|
|
4388
|
+
{
|
|
4389
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
4390
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4391
|
+
return keyWasReleased(button, gamepad+1);
|
|
4392
|
+
}
|
|
4306
4393
|
|
|
4307
4394
|
/** Returns gamepad stick value
|
|
4308
4395
|
* @param {number} stick
|
|
4309
4396
|
* @param {number} [gamepad]
|
|
4310
4397
|
* @return {Vector2}
|
|
4311
4398
|
* @memberof Input */
|
|
4312
|
-
function gamepadStick(stick, gamepad=
|
|
4313
|
-
{
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4399
|
+
function gamepadStick(stick, gamepad=gamepadPrimary)
|
|
4400
|
+
{
|
|
4401
|
+
ASSERT(isNumber(stick), 'stick must be a number');
|
|
4402
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4403
|
+
return gamepadStickData[gamepad]?.[stick] ?? vec2();
|
|
4404
|
+
}
|
|
4317
4405
|
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4406
|
+
/** Returns gamepad dpad value
|
|
4407
|
+
* @param {number} [gamepad]
|
|
4408
|
+
* @return {Vector2}
|
|
4409
|
+
* @memberof Input */
|
|
4410
|
+
function gamepadDpad(gamepad=gamepadPrimary)
|
|
4411
|
+
{
|
|
4412
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4413
|
+
return gamepadDpadData[gamepad] ?? vec2();
|
|
4414
|
+
}
|
|
4321
4415
|
|
|
4322
|
-
|
|
4416
|
+
/** Returns true if passed in gamepad is connected
|
|
4417
|
+
* @param {number} [gamepad]
|
|
4418
|
+
* @return {boolean}
|
|
4419
|
+
* @memberof Input */
|
|
4420
|
+
function gamepadConnected(gamepad=gamepadPrimary)
|
|
4323
4421
|
{
|
|
4324
|
-
|
|
4422
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4423
|
+
return !!inputData[gamepad+1];
|
|
4424
|
+
}
|
|
4325
4425
|
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4426
|
+
/** True if a touch device has been detected
|
|
4427
|
+
* @memberof Input */
|
|
4428
|
+
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
4329
4429
|
|
|
4330
|
-
|
|
4331
|
-
mousePos = screenToWorld(mousePosScreen);
|
|
4332
|
-
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
4430
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4333
4431
|
|
|
4334
|
-
|
|
4335
|
-
|
|
4432
|
+
/** Pulse the vibration hardware if it exists
|
|
4433
|
+
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
4434
|
+
* @memberof Input */
|
|
4435
|
+
function vibrate(pattern=100)
|
|
4436
|
+
{
|
|
4437
|
+
ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
|
|
4438
|
+
vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
|
|
4336
4439
|
}
|
|
4337
4440
|
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4441
|
+
/** Cancel any ongoing vibration
|
|
4442
|
+
* @memberof Input */
|
|
4443
|
+
function vibrateStop() { vibrate(0); }
|
|
4341
4444
|
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4445
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4446
|
+
// Pointer Lock
|
|
4447
|
+
|
|
4448
|
+
/** Request to lock the pointer, does not work on touch devices
|
|
4449
|
+
* @memberof Input */
|
|
4450
|
+
function pointerLockRequest()
|
|
4451
|
+
{ !isTouchDevice && mainCanvas.requestPointerLock?.(); }
|
|
4452
|
+
|
|
4453
|
+
/** Request to unlock the pointer
|
|
4454
|
+
* @memberof Input */
|
|
4455
|
+
function pointerLockExit()
|
|
4456
|
+
{ document.exitPointerLock?.(); }
|
|
4457
|
+
|
|
4458
|
+
/** Check if pointer is locked (true if locked)
|
|
4459
|
+
* @return {boolean}
|
|
4460
|
+
* @memberof Input */
|
|
4461
|
+
function pointerLockIsActive()
|
|
4462
|
+
{ return document.pointerLockElement === mainCanvas; }
|
|
4463
|
+
|
|
4464
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4465
|
+
// Input variables used by engine
|
|
4466
|
+
|
|
4467
|
+
// input uses bit field for each key: 1=isDown, 2=wasPressed, 4=wasReleased
|
|
4468
|
+
// mouse and keyboard stored in device 0, gamepads stored in devices > 0
|
|
4469
|
+
const inputData = [[]];
|
|
4470
|
+
|
|
4471
|
+
// gamepad internal variables
|
|
4472
|
+
const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
|
|
4473
|
+
|
|
4474
|
+
// touch gamepad internal variables
|
|
4475
|
+
const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
|
|
4476
|
+
|
|
4477
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4478
|
+
// Input system functions used by engine
|
|
4350
4479
|
|
|
4351
4480
|
function inputInit()
|
|
4352
4481
|
{
|
|
@@ -4376,6 +4505,18 @@ function inputInit()
|
|
|
4376
4505
|
if (inputWASDEmulateDirection)
|
|
4377
4506
|
inputData[0][remapKey(e.code)] = 3;
|
|
4378
4507
|
}
|
|
4508
|
+
|
|
4509
|
+
// prevent arrow key from moving the page
|
|
4510
|
+
const preventDefaultKeys =
|
|
4511
|
+
[
|
|
4512
|
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', // scrolling
|
|
4513
|
+
'Space', // page down scroll
|
|
4514
|
+
'Tab', // focus navigation
|
|
4515
|
+
'Backspace', // browser back
|
|
4516
|
+
];
|
|
4517
|
+
if (preventDefaultKeys.includes(e.code))
|
|
4518
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
4519
|
+
e.preventDefault();
|
|
4379
4520
|
}
|
|
4380
4521
|
function onKeyUp(e)
|
|
4381
4522
|
{
|
|
@@ -4407,7 +4548,9 @@ function inputInit()
|
|
|
4407
4548
|
const mousePosScreenLast = mousePosScreen;
|
|
4408
4549
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
4409
4550
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4410
|
-
|
|
4551
|
+
|
|
4552
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
4553
|
+
e.preventDefault();
|
|
4411
4554
|
}
|
|
4412
4555
|
function onMouseUp(e)
|
|
4413
4556
|
{
|
|
@@ -4431,344 +4574,386 @@ function inputInit()
|
|
|
4431
4574
|
function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
|
|
4432
4575
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
4433
4576
|
function onBlur() { inputClear(); } // reset input when focus is lost
|
|
4434
|
-
}
|
|
4435
4577
|
|
|
4436
|
-
//
|
|
4437
|
-
function
|
|
4438
|
-
{
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
}
|
|
4578
|
+
// enable touch input mouse passthrough
|
|
4579
|
+
function touchInputInit()
|
|
4580
|
+
{
|
|
4581
|
+
// add non passive touch event listeners
|
|
4582
|
+
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
4583
|
+
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
4584
|
+
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
4444
4585
|
|
|
4445
|
-
|
|
4446
|
-
|
|
4586
|
+
// handle all touch events the same way
|
|
4587
|
+
let wasTouching;
|
|
4588
|
+
function handleTouch(e)
|
|
4589
|
+
{
|
|
4590
|
+
if (!touchInputEnable)
|
|
4591
|
+
return;
|
|
4447
4592
|
|
|
4448
|
-
//
|
|
4449
|
-
|
|
4593
|
+
// route touch to gamepad
|
|
4594
|
+
if (touchGamepadEnable)
|
|
4595
|
+
handleTouchGamepad(e);
|
|
4450
4596
|
|
|
4451
|
-
//
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
const applyDeadZones = (v)=>
|
|
4455
|
-
{
|
|
4456
|
-
const min=.3, max=.8;
|
|
4457
|
-
const deadZone = (v)=>
|
|
4458
|
-
v > min ? percent(v, min, max) :
|
|
4459
|
-
v < -min ? -percent(-v, min, max) : 0;
|
|
4460
|
-
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
4461
|
-
}
|
|
4597
|
+
// fix stalled audio requiring user interaction
|
|
4598
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
4599
|
+
audioContext.resume();
|
|
4462
4600
|
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4601
|
+
// check if touching and pass to mouse events
|
|
4602
|
+
const touching = e.touches.length;
|
|
4603
|
+
const button = 0; // all touches are left mouse button
|
|
4604
|
+
if (touching)
|
|
4605
|
+
{
|
|
4606
|
+
// set event pos and pass it along
|
|
4607
|
+
const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
|
|
4608
|
+
const mousePosScreenLast = mousePosScreen;
|
|
4609
|
+
mousePosScreen = mouseEventToScreen(pos);
|
|
4610
|
+
if (wasTouching)
|
|
4611
|
+
{
|
|
4612
|
+
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4613
|
+
isUsingGamepad = touchGamepadEnable;
|
|
4614
|
+
}
|
|
4615
|
+
else
|
|
4616
|
+
inputData[0][button] = 3;
|
|
4617
|
+
}
|
|
4618
|
+
else if (wasTouching)
|
|
4619
|
+
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
4468
4620
|
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
//
|
|
4477
|
-
|
|
4478
|
-
sticks[0].y = -round(touchGamepadStick.y);
|
|
4479
|
-
sticks[0] = sticks[0].clampLength();
|
|
4621
|
+
// set was touching
|
|
4622
|
+
wasTouching = touching;
|
|
4623
|
+
|
|
4624
|
+
// prevent default handling like copy, magnifier lens, and scrolling
|
|
4625
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
4626
|
+
e.preventDefault();
|
|
4627
|
+
|
|
4628
|
+
// must return true so the document will get focus
|
|
4629
|
+
return true;
|
|
4480
4630
|
}
|
|
4481
4631
|
|
|
4482
|
-
//
|
|
4483
|
-
|
|
4484
|
-
for (let i=10; i--;)
|
|
4632
|
+
// special handling for virtual gamepad mode
|
|
4633
|
+
function handleTouchGamepad(e)
|
|
4485
4634
|
{
|
|
4486
|
-
|
|
4487
|
-
|
|
4635
|
+
// clear touch gamepad input
|
|
4636
|
+
touchGamepadSticks.length = 0;
|
|
4637
|
+
touchGamepadSticks[0] = vec2();
|
|
4638
|
+
touchGamepadSticks[1] = vec2();
|
|
4639
|
+
touchGamepadButtons.length = 0;
|
|
4640
|
+
isUsingGamepad = true;
|
|
4641
|
+
|
|
4642
|
+
const touching = e.touches.length;
|
|
4643
|
+
if (touching)
|
|
4644
|
+
{
|
|
4645
|
+
touchGamepadTimer.set();
|
|
4646
|
+
if (touchGamepadCenterButton && !wasTouching && paused)
|
|
4647
|
+
{
|
|
4648
|
+
// touch anywhere to press start when paused
|
|
4649
|
+
touchGamepadButtons[9] = 1;
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
// don't process touch gamepad if paused
|
|
4655
|
+
if (paused)
|
|
4656
|
+
return;
|
|
4657
|
+
|
|
4658
|
+
// get center of left and right sides
|
|
4659
|
+
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4660
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
4661
|
+
const startCenter = mainCanvasSize.scale(.5);
|
|
4662
|
+
|
|
4663
|
+
// check each touch point
|
|
4664
|
+
for (const touch of e.touches)
|
|
4665
|
+
{
|
|
4666
|
+
const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
|
|
4667
|
+
if (stickCenter.distance(touchPos) < touchGamepadSize)
|
|
4668
|
+
{
|
|
4669
|
+
// virtual analog stick
|
|
4670
|
+
const delta = touchPos.subtract(stickCenter);
|
|
4671
|
+
touchGamepadSticks[0] = delta.scale(2/touchGamepadSize).clampLength();
|
|
4672
|
+
}
|
|
4673
|
+
else if (buttonCenter.distance(touchPos) < touchGamepadSize)
|
|
4674
|
+
{
|
|
4675
|
+
if (touchGamepadButtonCount === 1)
|
|
4676
|
+
{
|
|
4677
|
+
// virtual right analog stick
|
|
4678
|
+
const delta = touchPos.subtract(buttonCenter);
|
|
4679
|
+
touchGamepadSticks[1] = delta.scale(2/touchGamepadSize).clampLength();
|
|
4680
|
+
}
|
|
4681
|
+
// virtual face buttons
|
|
4682
|
+
let button = buttonCenter.subtract(touchPos).direction();
|
|
4683
|
+
button = mod(button+2, 4);
|
|
4684
|
+
if (touchGamepadButtonCount === 1)
|
|
4685
|
+
button = 0;
|
|
4686
|
+
else if (touchGamepadButtonCount === 2)
|
|
4687
|
+
{
|
|
4688
|
+
const delta = buttonCenter.subtract(touchPos);
|
|
4689
|
+
button = -delta.x < delta.y ? 1 : 0;
|
|
4690
|
+
}
|
|
4691
|
+
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4692
|
+
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
4693
|
+
if (button < touchGamepadButtonCount)
|
|
4694
|
+
touchGamepadButtons[button] = 1;
|
|
4695
|
+
}
|
|
4696
|
+
else if (touchGamepadCenterButton &&
|
|
4697
|
+
startCenter.distance(touchPos) < touchGamepadSize)
|
|
4698
|
+
{
|
|
4699
|
+
// virtual start button in center
|
|
4700
|
+
touchGamepadButtons[9] = 1;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4488
4703
|
}
|
|
4704
|
+
}
|
|
4489
4705
|
|
|
4490
|
-
|
|
4491
|
-
|
|
4706
|
+
// convert a mouse or touch event position to screen space
|
|
4707
|
+
function mouseEventToScreen(mousePos)
|
|
4708
|
+
{
|
|
4709
|
+
const rect = mainCanvas.getBoundingClientRect();
|
|
4710
|
+
const px = percent(mousePos.x, rect.left, rect.right);
|
|
4711
|
+
const py = percent(mousePos.y, rect.top, rect.bottom);
|
|
4712
|
+
return vec2(px*mainCanvas.width, py*mainCanvas.height);
|
|
4492
4713
|
}
|
|
4714
|
+
}
|
|
4493
4715
|
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4716
|
+
function inputUpdate()
|
|
4717
|
+
{
|
|
4718
|
+
if (headlessMode) return;
|
|
4497
4719
|
|
|
4498
|
-
//
|
|
4499
|
-
if (!
|
|
4500
|
-
|
|
4720
|
+
// clear input when lost focus (prevent stuck keys)
|
|
4721
|
+
if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
4722
|
+
inputClear();
|
|
4723
|
+
|
|
4724
|
+
// update mouse world space position and delta
|
|
4725
|
+
mousePos = screenToWorld(mousePosScreen);
|
|
4726
|
+
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
4501
4727
|
|
|
4502
|
-
//
|
|
4503
|
-
|
|
4504
|
-
|
|
4728
|
+
// update gamepads if enabled
|
|
4729
|
+
gamepadsUpdate();
|
|
4730
|
+
|
|
4731
|
+
// gamepads are updated by engine every frame automatically
|
|
4732
|
+
function gamepadsUpdate()
|
|
4505
4733
|
{
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4734
|
+
const applyDeadZones = (v)=>
|
|
4735
|
+
{
|
|
4736
|
+
const min=.3, max=.8;
|
|
4737
|
+
const deadZone = (v)=>
|
|
4738
|
+
v > min ? percent(v, min, max) :
|
|
4739
|
+
v < -min ? -percent(-v, min, max) : 0;
|
|
4740
|
+
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
// update touch gamepad if enabled
|
|
4744
|
+
if (touchGamepadEnable && isTouchDevice)
|
|
4745
|
+
{
|
|
4746
|
+
if (!touchGamepadTimer.isSet())
|
|
4747
|
+
return;
|
|
4748
|
+
|
|
4749
|
+
// read virtual analog stick
|
|
4750
|
+
gamepadPrimary = 0; // touch gamepad uses index 0
|
|
4751
|
+
const sticks = gamepadStickData[0] ?? (gamepadStickData[0] = []);
|
|
4752
|
+
const dpad = gamepadDpadData[0] ?? (gamepadDpadData[0] = vec2());
|
|
4753
|
+
sticks[0] = vec2();
|
|
4754
|
+
dpad.set();
|
|
4755
|
+
const leftTouchStick = touchGamepadSticks[0] ?? vec2();
|
|
4756
|
+
if (touchGamepadAnalog)
|
|
4757
|
+
sticks[0] = applyDeadZones(leftTouchStick);
|
|
4758
|
+
else if (leftTouchStick.lengthSquared() > .3)
|
|
4759
|
+
{
|
|
4760
|
+
// convert to 8 way dpad
|
|
4761
|
+
const x = clamp(round(leftTouchStick.x), -1, 1);
|
|
4762
|
+
const y = clamp(round(leftTouchStick.y), -1, 1);
|
|
4763
|
+
dpad.set(x, -y);
|
|
4764
|
+
sticks[0] = dpad.clampLength(); // clamp to circle
|
|
4765
|
+
}
|
|
4766
|
+
const rightTouchStick = touchGamepadSticks[1] ?? vec2();
|
|
4767
|
+
if (touchGamepadButtonCount === 1)
|
|
4768
|
+
sticks[1] = applyDeadZones(rightTouchStick);
|
|
4769
|
+
|
|
4770
|
+
// read virtual gamepad buttons
|
|
4771
|
+
const data = inputData[1] ?? (inputData[1] = []);
|
|
4772
|
+
for (let i=10; i--;)
|
|
4773
|
+
{
|
|
4774
|
+
const wasDown = gamepadIsDown(i,0);
|
|
4775
|
+
data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
// disable normal gamepads when touch gamepad is active
|
|
4779
|
+
return;
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
// return if gamepads are disabled or not supported
|
|
4783
|
+
if (!gamepadsEnable || !navigator || !navigator.getGamepads)
|
|
4784
|
+
return;
|
|
4785
|
+
|
|
4786
|
+
// only poll gamepads when focused or in debug mode
|
|
4787
|
+
if (!debug && !document.hasFocus())
|
|
4788
|
+
return;
|
|
4510
4789
|
|
|
4511
|
-
|
|
4790
|
+
// poll gamepads
|
|
4791
|
+
const maxGamepads = 8;
|
|
4792
|
+
const gamepads = navigator.getGamepads();
|
|
4793
|
+
const gamepadCount = min(maxGamepads, gamepads.length)
|
|
4794
|
+
for (let i=0; i<gamepadCount; ++i)
|
|
4512
4795
|
{
|
|
4796
|
+
// get or create gamepad data
|
|
4797
|
+
const gamepad = gamepads[i];
|
|
4798
|
+
if (!gamepad)
|
|
4799
|
+
{
|
|
4800
|
+
// clear gamepad data if not connected
|
|
4801
|
+
inputData[i+1] = undefined;
|
|
4802
|
+
gamepadStickData[i] = undefined;
|
|
4803
|
+
gamepadDpadData[i] = undefined;
|
|
4804
|
+
gamepadHadInput[i] = undefined;
|
|
4805
|
+
continue;
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
const data = inputData[i+1] ?? (inputData[i+1] = []);
|
|
4809
|
+
const sticks = gamepadStickData[i] ?? (gamepadStickData[i] = []);
|
|
4810
|
+
const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
|
|
4811
|
+
|
|
4513
4812
|
// read analog sticks
|
|
4514
4813
|
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
4515
4814
|
sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
|
|
4516
4815
|
|
|
4517
4816
|
// read buttons
|
|
4817
|
+
let hadInput = false;
|
|
4518
4818
|
for (let j = gamepad.buttons.length; j--;)
|
|
4519
4819
|
{
|
|
4520
4820
|
const button = gamepad.buttons[j];
|
|
4521
4821
|
const wasDown = gamepadIsDown(j,i);
|
|
4522
4822
|
data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4823
|
+
|
|
4824
|
+
// check for any input on this gamepad, analog must be full press
|
|
4825
|
+
if (button.pressed)
|
|
4826
|
+
if (!button.value || button.value > .9)
|
|
4827
|
+
hadInput = true;
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
// set new primary gamepad if current is not connected
|
|
4831
|
+
if (hadInput)
|
|
4832
|
+
{
|
|
4833
|
+
gamepadHadInput[i] = true;
|
|
4834
|
+
if (!gamepadHadInput[gamepadPrimary])
|
|
4835
|
+
gamepadPrimary = i;
|
|
4836
|
+
isUsingGamepad ||= (gamepadPrimary === i);
|
|
4526
4837
|
}
|
|
4527
4838
|
|
|
4528
|
-
if (
|
|
4839
|
+
if (gamepad.mapping === 'standard')
|
|
4529
4840
|
{
|
|
4530
|
-
//
|
|
4531
|
-
|
|
4841
|
+
// get dpad buttons (standard mapping)
|
|
4842
|
+
dpad.set(
|
|
4532
4843
|
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
4533
4844
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
4534
|
-
|
|
4535
|
-
|
|
4845
|
+
}
|
|
4846
|
+
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
4847
|
+
{
|
|
4848
|
+
// digital style dpad from axes
|
|
4849
|
+
const x = clamp(round(gamepad.axes[0]), -1, 1);
|
|
4850
|
+
const y = clamp(round(gamepad.axes[1]), -1, 1);
|
|
4851
|
+
dpad.set(x, -y);
|
|
4536
4852
|
}
|
|
4537
4853
|
|
|
4538
|
-
//
|
|
4539
|
-
|
|
4854
|
+
// copy dpad to left analog stick when pressed
|
|
4855
|
+
if (gamepadDirectionEmulateStick && !dpad.isZero())
|
|
4856
|
+
sticks[0] = dpad.clampLength();
|
|
4540
4857
|
}
|
|
4858
|
+
|
|
4859
|
+
// disable touch gamepad if using real gamepad
|
|
4860
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
4541
4861
|
}
|
|
4542
4862
|
}
|
|
4543
4863
|
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
/** Pulse the vibration hardware if it exists
|
|
4547
|
-
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
4548
|
-
* @memberof Input */
|
|
4549
|
-
function vibrate(pattern=100)
|
|
4550
|
-
{ vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
|
|
4551
|
-
|
|
4552
|
-
/** Cancel any ongoing vibration
|
|
4553
|
-
* @memberof Input */
|
|
4554
|
-
function vibrateStop() { vibrate(0); }
|
|
4555
|
-
|
|
4556
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
4557
|
-
// Touch input & virtual on screen gamepad
|
|
4558
|
-
|
|
4559
|
-
/** True if a touch device has been detected
|
|
4560
|
-
* @memberof Input */
|
|
4561
|
-
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
4562
|
-
|
|
4563
|
-
// touch gamepad internal variables
|
|
4564
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
|
|
4565
|
-
|
|
4566
|
-
function touchGamepadButtonCenter()
|
|
4864
|
+
function inputUpdatePost()
|
|
4567
4865
|
{
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4866
|
+
if (headlessMode) return;
|
|
4867
|
+
|
|
4868
|
+
// clear input to prepare for next frame
|
|
4869
|
+
for (const deviceInputData of inputData)
|
|
4870
|
+
for (const i in deviceInputData)
|
|
4871
|
+
deviceInputData[i] &= 1;
|
|
4872
|
+
mouseWheel = 0;
|
|
4873
|
+
mouseDelta = vec2();
|
|
4874
|
+
mouseDeltaScreen = vec2();
|
|
4573
4875
|
}
|
|
4574
4876
|
|
|
4575
|
-
|
|
4576
|
-
function touchInputInit()
|
|
4877
|
+
function inputRender()
|
|
4577
4878
|
{
|
|
4578
|
-
|
|
4579
|
-
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
4580
|
-
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
4581
|
-
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
4879
|
+
touchGamepadRender();
|
|
4582
4880
|
|
|
4583
|
-
|
|
4584
|
-
let wasTouching;
|
|
4585
|
-
function handleTouch(e)
|
|
4881
|
+
function touchGamepadRender()
|
|
4586
4882
|
{
|
|
4587
|
-
if (!touchInputEnable)
|
|
4883
|
+
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
4884
|
+
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
4588
4885
|
return;
|
|
4589
4886
|
|
|
4590
|
-
//
|
|
4591
|
-
|
|
4592
|
-
|
|
4887
|
+
// fade off when not touching or paused
|
|
4888
|
+
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
4889
|
+
if (!alpha || paused)
|
|
4890
|
+
return;
|
|
4593
4891
|
|
|
4594
|
-
//
|
|
4595
|
-
|
|
4596
|
-
|
|
4892
|
+
// setup the canvas
|
|
4893
|
+
const context = overlayContext;
|
|
4894
|
+
context.save();
|
|
4895
|
+
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
4896
|
+
context.strokeStyle = '#fff';
|
|
4897
|
+
context.lineWidth = 3;
|
|
4597
4898
|
|
|
4598
|
-
//
|
|
4599
|
-
const
|
|
4600
|
-
|
|
4601
|
-
|
|
4899
|
+
// draw left analog stick
|
|
4900
|
+
const leftTouchStick = touchGamepadSticks[0] ?? vec2();
|
|
4901
|
+
context.fillStyle = leftTouchStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
4902
|
+
context.beginPath();
|
|
4903
|
+
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4904
|
+
if (touchGamepadAnalog)
|
|
4602
4905
|
{
|
|
4603
|
-
//
|
|
4604
|
-
|
|
4605
|
-
const mousePosScreenLast = mousePosScreen;
|
|
4606
|
-
mousePosScreen = mouseEventToScreen(pos);
|
|
4607
|
-
if (wasTouching)
|
|
4608
|
-
{
|
|
4609
|
-
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4610
|
-
isUsingGamepad = touchGamepadEnable;
|
|
4611
|
-
}
|
|
4612
|
-
else
|
|
4613
|
-
inputData[0][button] = 3;
|
|
4906
|
+
// draw circle shaped gamepad
|
|
4907
|
+
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
4614
4908
|
}
|
|
4615
|
-
else
|
|
4616
|
-
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
4617
|
-
|
|
4618
|
-
// set was touching
|
|
4619
|
-
wasTouching = touching;
|
|
4620
|
-
|
|
4621
|
-
// prevent default handling like copy, magnifier lens, and scrolling
|
|
4622
|
-
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
4623
|
-
e.preventDefault();
|
|
4624
|
-
|
|
4625
|
-
// must return true so the document will get focus
|
|
4626
|
-
return true;
|
|
4627
|
-
}
|
|
4628
|
-
|
|
4629
|
-
// special handling for virtual gamepad mode
|
|
4630
|
-
function handleTouchGamepad(e)
|
|
4631
|
-
{
|
|
4632
|
-
// clear touch gamepad input
|
|
4633
|
-
touchGamepadStick = vec2();
|
|
4634
|
-
touchGamepadButtons = [];
|
|
4635
|
-
isUsingGamepad = true;
|
|
4636
|
-
|
|
4637
|
-
const touching = e.touches.length;
|
|
4638
|
-
if (touching)
|
|
4909
|
+
else
|
|
4639
4910
|
{
|
|
4640
|
-
|
|
4641
|
-
|
|
4911
|
+
// draw cross shaped gamepad
|
|
4912
|
+
for (let i=10; --i;)
|
|
4642
4913
|
{
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4914
|
+
const angle = i*PI/4;
|
|
4915
|
+
context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
4916
|
+
i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
|
|
4646
4917
|
}
|
|
4647
4918
|
}
|
|
4919
|
+
context.fill();
|
|
4920
|
+
context.stroke();
|
|
4648
4921
|
|
|
4649
|
-
//
|
|
4650
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4651
|
-
const buttonCenter = touchGamepadButtonCenter();
|
|
4652
|
-
const startCenter = mainCanvasSize.scale(.5);
|
|
4653
|
-
|
|
4654
|
-
// check each touch point
|
|
4655
|
-
for (const touch of e.touches)
|
|
4922
|
+
// draw right face buttons
|
|
4656
4923
|
{
|
|
4657
|
-
const
|
|
4658
|
-
|
|
4924
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
4925
|
+
const buttonSize = touchGamepadButtonCount > 1 ?
|
|
4926
|
+
touchGamepadSize/4 : touchGamepadSize/2;
|
|
4927
|
+
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
4659
4928
|
{
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
else if (buttonCenter.distance(touchPos) < touchGamepadSize)
|
|
4664
|
-
{
|
|
4665
|
-
// virtual face buttons
|
|
4666
|
-
let button = buttonCenter.subtract(touchPos).direction();
|
|
4667
|
-
button = mod(button+2, 4);
|
|
4668
|
-
if (touchGamepadButtonCount === 1)
|
|
4669
|
-
button = 0;
|
|
4670
|
-
else if (touchGamepadButtonCount === 2)
|
|
4671
|
-
{
|
|
4672
|
-
const delta = buttonCenter.subtract(touchPos);
|
|
4673
|
-
button = -delta.x < delta.y ? 1 : 0;
|
|
4674
|
-
}
|
|
4929
|
+
const j = mod(i-1, 4);
|
|
4930
|
+
let button = touchGamepadButtonCount > 2 ?
|
|
4931
|
+
j : min(j, touchGamepadButtonCount-1);
|
|
4675
4932
|
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4676
4933
|
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
touchGamepadButtons[9] = 1;
|
|
4934
|
+
const pos = touchGamepadButtonCount < 2 ? buttonCenter :
|
|
4935
|
+
buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
4936
|
+
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
4937
|
+
context.beginPath();
|
|
4938
|
+
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
4939
|
+
context.fill();
|
|
4940
|
+
context.stroke();
|
|
4685
4941
|
}
|
|
4686
4942
|
}
|
|
4687
|
-
}
|
|
4688
|
-
}
|
|
4689
|
-
|
|
4690
|
-
// render the touch gamepad, called automatically by the engine
|
|
4691
|
-
function touchGamepadRender()
|
|
4692
|
-
{
|
|
4693
|
-
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
4694
|
-
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
4695
|
-
return;
|
|
4696
|
-
|
|
4697
|
-
// fade off when not touching or paused
|
|
4698
|
-
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
4699
|
-
if (!alpha || paused)
|
|
4700
|
-
return;
|
|
4701
|
-
|
|
4702
|
-
// setup the canvas
|
|
4703
|
-
const context = overlayContext;
|
|
4704
|
-
context.save();
|
|
4705
|
-
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
4706
|
-
context.strokeStyle = '#fff';
|
|
4707
|
-
context.lineWidth = 3;
|
|
4708
|
-
|
|
4709
|
-
// draw left analog stick
|
|
4710
|
-
context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
4711
|
-
context.beginPath();
|
|
4712
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4713
|
-
if (touchGamepadAnalog) // draw circle shaped gamepad
|
|
4714
|
-
{
|
|
4715
|
-
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
4716
|
-
context.fill();
|
|
4717
|
-
context.stroke();
|
|
4718
|
-
}
|
|
4719
|
-
else // draw cross shaped gamepad
|
|
4720
|
-
{
|
|
4721
|
-
for (let i=10; i--;)
|
|
4722
|
-
{
|
|
4723
|
-
const angle = i*PI/4;
|
|
4724
|
-
context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
4725
|
-
i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
|
|
4726
|
-
i===1 && context.fill();
|
|
4727
|
-
}
|
|
4728
|
-
context.stroke();
|
|
4729
|
-
}
|
|
4730
4943
|
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
|
|
4734
|
-
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
4735
|
-
{
|
|
4736
|
-
const j = mod(i-1, 4);
|
|
4737
|
-
let button = touchGamepadButtonCount > 2 ?
|
|
4738
|
-
j : min(j, touchGamepadButtonCount-1);
|
|
4739
|
-
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4740
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
4741
|
-
const pos = buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
4742
|
-
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
4743
|
-
context.beginPath();
|
|
4744
|
-
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
4745
|
-
context.fill();
|
|
4746
|
-
context.stroke();
|
|
4944
|
+
// set canvas back to normal
|
|
4945
|
+
context.restore();
|
|
4747
4946
|
}
|
|
4748
|
-
|
|
4749
|
-
// set canvas back to normal
|
|
4750
|
-
context.restore();
|
|
4751
4947
|
}
|
|
4752
4948
|
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
/** Request to lock the pointer, does not work on touch devices
|
|
4757
|
-
* @memberof Input */
|
|
4758
|
-
function pointerLockRequest()
|
|
4949
|
+
// center position for right tocuh pad face buttons
|
|
4950
|
+
function touchGamepadButtonCenter()
|
|
4759
4951
|
{
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
* @memberof Input */
|
|
4766
|
-
function pointerLockExit() { document.exitPointerLock && document.exitPointerLock(); }
|
|
4767
|
-
|
|
4768
|
-
/** Check if pointer is locked (true if locked)
|
|
4769
|
-
* @return {boolean}
|
|
4770
|
-
* @memberof Input */
|
|
4771
|
-
function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
|
|
4952
|
+
const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
|
|
4953
|
+
if (touchGamepadButtonCount === 2)
|
|
4954
|
+
center.x += touchGamepadSize/2;
|
|
4955
|
+
return center;
|
|
4956
|
+
}
|
|
4772
4957
|
/**
|
|
4773
4958
|
* LittleJS Audio System
|
|
4774
4959
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
@@ -5508,7 +5693,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
|
|
|
5508
5693
|
}
|
|
5509
5694
|
}
|
|
5510
5695
|
|
|
5511
|
-
/** Return the exact position of the
|
|
5696
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
5512
5697
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
5513
5698
|
* @param {Vector2} posStart
|
|
5514
5699
|
* @param {Vector2} posEnd
|
|
@@ -6082,7 +6267,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6082
6267
|
return false;
|
|
6083
6268
|
}
|
|
6084
6269
|
|
|
6085
|
-
/** Return the exact position of the
|
|
6270
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6086
6271
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6087
6272
|
* @param {Vector2} posStart
|
|
6088
6273
|
* @param {Vector2} posEnd
|
|
@@ -8027,11 +8212,14 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8027
8212
|
/**
|
|
8028
8213
|
* LittleJS User Interface Plugin
|
|
8029
8214
|
* - call new UISystemPlugin() to setup the UI system
|
|
8215
|
+
* - Gamepad and keyboard navigation support
|
|
8030
8216
|
* - Nested Menus
|
|
8031
8217
|
* - Text
|
|
8032
8218
|
* - Buttons
|
|
8033
8219
|
* - Checkboxes
|
|
8034
8220
|
* - Images
|
|
8221
|
+
* - Scrollbars
|
|
8222
|
+
* - Video
|
|
8035
8223
|
* @namespace UISystem
|
|
8036
8224
|
*/
|
|
8037
8225
|
|
|
@@ -8042,6 +8230,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8042
8230
|
* @memberof UISystem */
|
|
8043
8231
|
let uiSystem;
|
|
8044
8232
|
|
|
8233
|
+
/** Enable UI system debug drawing
|
|
8234
|
+
* 0=off, 1=normal, 2=show invisible
|
|
8235
|
+
* @type {number}
|
|
8236
|
+
* @default
|
|
8237
|
+
* @memberof UISystem */
|
|
8238
|
+
let uiDebug = 0;
|
|
8239
|
+
|
|
8240
|
+
/** Enable UI system debug drawing
|
|
8241
|
+
* 0=off, 1=normal, 2=show invisible
|
|
8242
|
+
* @param {number|boolean} enable
|
|
8243
|
+
* @memberof UISystem */
|
|
8244
|
+
function uiSetDebug(debugMode)
|
|
8245
|
+
{ uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
|
|
8246
|
+
|
|
8045
8247
|
///////////////////////////////////////////////////////////////////////////////
|
|
8046
8248
|
/**
|
|
8047
8249
|
* UI System Global Object
|
|
@@ -8080,7 +8282,7 @@ class UISystemPlugin
|
|
|
8080
8282
|
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
8081
8283
|
this.defaultCornerRadius = 0;
|
|
8082
8284
|
/** @property {number} - Default scale to use for fitting text to object */
|
|
8083
|
-
this.
|
|
8285
|
+
this.defaultTextFitScale = .8;
|
|
8084
8286
|
/** @property {string} - Default font for UI elements */
|
|
8085
8287
|
this.defaultFont = fontDefault;
|
|
8086
8288
|
/** @property {Sound} - Default sound when interactive UI element is pressed */
|
|
@@ -8095,6 +8297,23 @@ class UISystemPlugin
|
|
|
8095
8297
|
this.defaultShadowBlur = 5;
|
|
8096
8298
|
/** @property {Vector2} - Offset of shadow blur */
|
|
8097
8299
|
this.defaultShadowOffset = vec2(5);
|
|
8300
|
+
/** @property {number} - If set ui coords will be renormalized to this canvas height */
|
|
8301
|
+
this.nativeHeight = 0;
|
|
8302
|
+
|
|
8303
|
+
// navigation properties
|
|
8304
|
+
/** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
|
|
8305
|
+
this.navigationObject = undefined;
|
|
8306
|
+
/** @property {number} - Gamepad index to use for UI navigation */
|
|
8307
|
+
this.navigationGamepadIndex = 0;
|
|
8308
|
+
/** @property {Timer} - Cooldown timer for navigation inputs */
|
|
8309
|
+
this.navigationTimer = new Timer(undefined, true);
|
|
8310
|
+
/** @property {number} - Time between navigation inputs in seconds */
|
|
8311
|
+
this.navigationDelay = .2;
|
|
8312
|
+
/** @property {boolean} - should the navigation be horizontal, vertical, or both? */
|
|
8313
|
+
this.navigationDirection = 1;
|
|
8314
|
+
/** @property {boolean} - True if user last used navigation instead of mouse */
|
|
8315
|
+
this.navigationMode = false;
|
|
8316
|
+
|
|
8098
8317
|
// system state
|
|
8099
8318
|
/** @property {Array<UIObject>} - List of all UI elements */
|
|
8100
8319
|
this.uiObjects = [];
|
|
@@ -8106,50 +8325,118 @@ class UISystemPlugin
|
|
|
8106
8325
|
this.hoverObject = undefined;
|
|
8107
8326
|
/** @property {UIObject} - Hover object at start of update */
|
|
8108
8327
|
this.lastHoverObject = undefined;
|
|
8109
|
-
/** @property {
|
|
8110
|
-
this.
|
|
8328
|
+
/** @property {UIObject} - Current confirm menu being shown */
|
|
8329
|
+
this.confirmDialog = undefined;
|
|
8111
8330
|
|
|
8112
8331
|
engineAddPlugin(uiUpdate, uiRender);
|
|
8113
8332
|
|
|
8333
|
+
// set object position in parent space
|
|
8334
|
+
function updateTransforms(o)
|
|
8335
|
+
{
|
|
8336
|
+
if (!o.parent) return;
|
|
8337
|
+
o.pos.x = o.localPos.x + o.parent.pos.x;
|
|
8338
|
+
o.pos.y = o.localPos.y + o.parent.pos.y;
|
|
8339
|
+
}
|
|
8340
|
+
|
|
8114
8341
|
// setup recursive update and render
|
|
8115
8342
|
// update in reverse order to detect mouse enter/leave
|
|
8116
8343
|
function uiUpdate()
|
|
8117
8344
|
{
|
|
8118
|
-
|
|
8345
|
+
if (uiSystem.activeObject && !uiSystem.activeObject.visible)
|
|
8346
|
+
uiSystem.activeObject = undefined;
|
|
8347
|
+
|
|
8348
|
+
// reset hover object at start of update
|
|
8349
|
+
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8350
|
+
uiSystem.hoverObject = undefined;
|
|
8351
|
+
|
|
8352
|
+
if (mouseWasPressed(0))
|
|
8119
8353
|
{
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
updateInvisibleObject(c);
|
|
8123
|
-
o.updateInvisible();
|
|
8354
|
+
uiSystem.navigationMode = false;
|
|
8355
|
+
uiSystem.navigationObject = undefined;
|
|
8124
8356
|
}
|
|
8125
|
-
|
|
8357
|
+
|
|
8358
|
+
// navigation with gamepad/keyboard
|
|
8359
|
+
const navigableObjects = uiSystem.getNavigableObjects();
|
|
8360
|
+
if (!navigableObjects.length)
|
|
8361
|
+
uiSystem.navigationObject = undefined;
|
|
8362
|
+
else
|
|
8126
8363
|
{
|
|
8127
|
-
if
|
|
8364
|
+
// unselect object if it is no longer navigable
|
|
8365
|
+
if (!navigableObjects.includes(uiSystem.navigationObject))
|
|
8366
|
+
uiSystem.navigationObject = undefined;
|
|
8367
|
+
|
|
8368
|
+
if (!isTouchDevice)
|
|
8369
|
+
if (uiSystem.navigationMode && !uiSystem.navigationObject)
|
|
8128
8370
|
{
|
|
8129
|
-
//
|
|
8130
|
-
|
|
8131
|
-
o.pos = o.localPos.add(o.parent.pos);
|
|
8132
|
-
// update in reverse order to detect mouse enter/leave
|
|
8133
|
-
for (let i=o.children.length; i--;)
|
|
8134
|
-
updateObject(o.children[i]);
|
|
8135
|
-
o.update();
|
|
8371
|
+
// select first auto focus object
|
|
8372
|
+
uiSystem.navigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
8136
8373
|
}
|
|
8137
|
-
|
|
8138
|
-
|
|
8374
|
+
|
|
8375
|
+
// navigate with dpad or left stick
|
|
8376
|
+
if (!uiSystem.navigationTimer.active())
|
|
8377
|
+
{
|
|
8378
|
+
// navigate through list with gamepad or keyboard
|
|
8379
|
+
const direction = sign(uiSystem.getNavigationDirection());
|
|
8380
|
+
if (direction)
|
|
8381
|
+
{
|
|
8382
|
+
let newNavigationObject;
|
|
8383
|
+
if (!uiSystem.navigationObject)
|
|
8384
|
+
{
|
|
8385
|
+
// use auto select object
|
|
8386
|
+
newNavigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
8387
|
+
|
|
8388
|
+
if (!newNavigationObject)
|
|
8389
|
+
{
|
|
8390
|
+
// try first or last object
|
|
8391
|
+
const newIndex = direction > 0 ? 0 : navigableObjects.length-1;
|
|
8392
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
else
|
|
8396
|
+
{
|
|
8397
|
+
const currentIndex = navigableObjects.indexOf(uiSystem.navigationObject);
|
|
8398
|
+
const newIndex = mod(currentIndex + direction, navigableObjects.length);
|
|
8399
|
+
newNavigationObject = navigableObjects[newIndex];
|
|
8400
|
+
}
|
|
8401
|
+
|
|
8402
|
+
if (uiSystem.navigationObject !== newNavigationObject)
|
|
8403
|
+
{
|
|
8404
|
+
uiSystem.navigationMode = true;
|
|
8405
|
+
uiSystem.hoverObject = undefined;
|
|
8406
|
+
uiSystem.navigationObject = newNavigationObject;
|
|
8407
|
+
uiSystem.navigationTimer.set(uiSystem.navigationDelay);
|
|
8408
|
+
newNavigationObject.soundPress &&
|
|
8409
|
+
newNavigationObject.soundPress.play();
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
|
|
8414
|
+
// activate the navigation object when pressed
|
|
8415
|
+
if (uiSystem.navigationObject)
|
|
8416
|
+
if (uiSystem.getNavigationWasPressed())
|
|
8417
|
+
uiSystem.navigationObject.navigatePressed();
|
|
8139
8418
|
}
|
|
8140
|
-
// reset hover object at start of update
|
|
8141
|
-
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8142
|
-
uiSystem.hoverObject = undefined;
|
|
8143
8419
|
|
|
8144
8420
|
// update in reverse order so topmost objects get priority
|
|
8145
8421
|
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8146
8422
|
{
|
|
8147
8423
|
const o = uiSystem.uiObjects[i];
|
|
8148
|
-
o.parent || updateObject(o)
|
|
8424
|
+
o.parent || updateObject(o);
|
|
8149
8425
|
}
|
|
8150
8426
|
|
|
8151
8427
|
// remove destroyed objects
|
|
8152
8428
|
uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
|
|
8429
|
+
|
|
8430
|
+
function updateObject(o)
|
|
8431
|
+
{
|
|
8432
|
+
if (!o.visible) return;
|
|
8433
|
+
|
|
8434
|
+
// update in reverse order to detect mouse enter/leave
|
|
8435
|
+
updateTransforms(o);
|
|
8436
|
+
for (let i=o.children.length; i--;)
|
|
8437
|
+
updateObject(o.children[i]);
|
|
8438
|
+
o.update();
|
|
8439
|
+
}
|
|
8153
8440
|
}
|
|
8154
8441
|
function uiRender()
|
|
8155
8442
|
{
|
|
@@ -8166,15 +8453,29 @@ class UISystemPlugin
|
|
|
8166
8453
|
|
|
8167
8454
|
function renderObject(o)
|
|
8168
8455
|
{
|
|
8169
|
-
if (!o.visible)
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8456
|
+
if (!o.visible) return;
|
|
8457
|
+
|
|
8458
|
+
// render object and children
|
|
8459
|
+
updateTransforms(o);
|
|
8173
8460
|
o.render();
|
|
8174
8461
|
for (const c of o.children)
|
|
8175
8462
|
renderObject(c);
|
|
8176
8463
|
}
|
|
8177
8464
|
uiSystem.uiObjects.forEach(o=> o.parent || renderObject(o));
|
|
8465
|
+
|
|
8466
|
+
if (uiDebug > 0)
|
|
8467
|
+
{
|
|
8468
|
+
// debug render all objects
|
|
8469
|
+
function renderDebug(o, visible=true)
|
|
8470
|
+
{
|
|
8471
|
+
visible &&= !!o.visible;
|
|
8472
|
+
updateTransforms(o);
|
|
8473
|
+
o.renderDebug(visible);
|
|
8474
|
+
for (const c of o.children)
|
|
8475
|
+
renderDebug(c, visible);
|
|
8476
|
+
}
|
|
8477
|
+
uiSystem.uiObjects.forEach(o=> o.parent || renderDebug(o));
|
|
8478
|
+
}
|
|
8178
8479
|
context.restore();
|
|
8179
8480
|
}
|
|
8180
8481
|
}
|
|
@@ -8227,8 +8528,8 @@ class UISystemPlugin
|
|
|
8227
8528
|
else
|
|
8228
8529
|
context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
|
|
8229
8530
|
context.fill();
|
|
8230
|
-
context.shadowColor = '#0000'
|
|
8231
|
-
if (lineWidth)
|
|
8531
|
+
context.shadowColor = '#0000';
|
|
8532
|
+
if (lineWidth && lineColor.a > 0)
|
|
8232
8533
|
{
|
|
8233
8534
|
context.strokeStyle = lineColor.toString();
|
|
8234
8535
|
context.lineWidth = lineWidth;
|
|
@@ -8263,10 +8564,24 @@ class UISystemPlugin
|
|
|
8263
8564
|
* @param {TileInfo} tileInfo
|
|
8264
8565
|
* @param {Color} [color=uiSystem.defaultColor]
|
|
8265
8566
|
* @param {number} [angle]
|
|
8266
|
-
* @param {boolean} [mirror]
|
|
8267
|
-
|
|
8567
|
+
* @param {boolean} [mirror]
|
|
8568
|
+
* @param {Color} [shadowColor]
|
|
8569
|
+
* @param {number} [shadowBlur]
|
|
8570
|
+
* @param {Color} [shadowOffset] */
|
|
8571
|
+
drawTile(pos, size, tileInfo, color=uiSystem.defaultColor, angle=0, mirror=false, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
8268
8572
|
{
|
|
8269
|
-
|
|
8573
|
+
const context = uiSystem.uiContext;
|
|
8574
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
8575
|
+
if (shadowColor.a > 0)
|
|
8576
|
+
{
|
|
8577
|
+
// setup shadow
|
|
8578
|
+
context.shadowColor = shadowColor.toString();
|
|
8579
|
+
context.shadowBlur = shadowBlur;
|
|
8580
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
8581
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
8582
|
+
}
|
|
8583
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, CLEAR_BLACK, false, true, context);
|
|
8584
|
+
context.shadowColor = '#0000';
|
|
8270
8585
|
}
|
|
8271
8586
|
|
|
8272
8587
|
/** Draw text to the UI context
|
|
@@ -8281,12 +8596,27 @@ class UISystemPlugin
|
|
|
8281
8596
|
* @param {string} [fontStyle]
|
|
8282
8597
|
* @param {boolean} [applyMaxWidth=true]
|
|
8283
8598
|
* @param {Vector2} [textShadow]
|
|
8284
|
-
|
|
8285
|
-
|
|
8599
|
+
* @param {Color} [shadowColor]
|
|
8600
|
+
* @param {number} [shadowBlur]
|
|
8601
|
+
* @param {Color} [shadowOffset] */
|
|
8602
|
+
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())
|
|
8286
8603
|
{
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8604
|
+
const context = uiSystem.uiContext;
|
|
8605
|
+
if (shadowColor.a > 0)
|
|
8606
|
+
{
|
|
8607
|
+
if (textShadow)
|
|
8608
|
+
drawTextScreen(text, pos.add(textShadow), size.y, shadowColor, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
8609
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
8610
|
+
{
|
|
8611
|
+
// setup shadow
|
|
8612
|
+
context.shadowColor = shadowColor.toString();
|
|
8613
|
+
context.shadowBlur = shadowBlur;
|
|
8614
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
8615
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
8616
|
+
}
|
|
8617
|
+
}
|
|
8618
|
+
drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, context);
|
|
8619
|
+
context.shadowColor = '#0000';
|
|
8290
8620
|
}
|
|
8291
8621
|
|
|
8292
8622
|
/**
|
|
@@ -8300,7 +8630,7 @@ class UISystemPlugin
|
|
|
8300
8630
|
* @param {DragAndDropCallback} [onDrop] - when a file is dropped
|
|
8301
8631
|
* @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
|
|
8302
8632
|
* @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
|
|
8303
|
-
* @param {DragAndDropCallback} [onDragOver] -
|
|
8633
|
+
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
8304
8634
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
8305
8635
|
{
|
|
8306
8636
|
function setCallback(callback, listenerType)
|
|
@@ -8316,8 +8646,7 @@ class UISystemPlugin
|
|
|
8316
8646
|
|
|
8317
8647
|
/** Convert a screen space position to native UI position
|
|
8318
8648
|
* @param {Vector2} pos
|
|
8319
|
-
* @return {Vector2}
|
|
8320
|
-
*/
|
|
8649
|
+
* @return {Vector2} */
|
|
8321
8650
|
screenToNative(pos)
|
|
8322
8651
|
{
|
|
8323
8652
|
if (!uiSystem.nativeHeight)
|
|
@@ -8340,6 +8669,160 @@ class UISystemPlugin
|
|
|
8340
8669
|
for (const o of this.uiObjects)
|
|
8341
8670
|
o.parent || o.destroy();
|
|
8342
8671
|
this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
|
|
8672
|
+
this.activeObject = undefined;
|
|
8673
|
+
this.hoverObject = undefined;
|
|
8674
|
+
this.lastHoverObject = undefined;
|
|
8675
|
+
}
|
|
8676
|
+
|
|
8677
|
+
/** Get all navigable UI objects sorted by navigationIndex
|
|
8678
|
+
* @return {Array<UIObject>} */
|
|
8679
|
+
getNavigableObjects()
|
|
8680
|
+
{
|
|
8681
|
+
function getNavigableRecursive(o)
|
|
8682
|
+
{
|
|
8683
|
+
if (!o.visible || o.disabled)
|
|
8684
|
+
return; // skip children if parent is invisible or disabled
|
|
8685
|
+
|
|
8686
|
+
if (o.isInteractive() && o.navigationIndex !== undefined)
|
|
8687
|
+
objects.push(o);
|
|
8688
|
+
for (let i=o.children.length; i--;)
|
|
8689
|
+
getNavigableRecursive(o.children[i]);
|
|
8690
|
+
}
|
|
8691
|
+
|
|
8692
|
+
// get all the valid navigable objects recursively
|
|
8693
|
+
let objects = [];
|
|
8694
|
+
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8695
|
+
{
|
|
8696
|
+
const o = uiSystem.uiObjects[i];
|
|
8697
|
+
if (uiSystem.confirmDialog && o !== uiSystem.confirmDialog)
|
|
8698
|
+
continue;
|
|
8699
|
+
o.parent || getNavigableRecursive(o);
|
|
8700
|
+
}
|
|
8701
|
+
|
|
8702
|
+
// sort by navigationIndex (lower numbers first)
|
|
8703
|
+
objects.sort((a, b)=> a.navigationIndex - b.navigationIndex);
|
|
8704
|
+
return objects;
|
|
8705
|
+
}
|
|
8706
|
+
|
|
8707
|
+
/** Get navigation direction from gamepad or keyboard
|
|
8708
|
+
* @return {number} */
|
|
8709
|
+
getNavigationDirection()
|
|
8710
|
+
{
|
|
8711
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
8712
|
+
const both = uiSystem.navigationDirection === 2;
|
|
8713
|
+
if (isUsingGamepad)
|
|
8714
|
+
{
|
|
8715
|
+
const gamepad = this.navigationGamepadIndex;
|
|
8716
|
+
const stick = gamepadStick(0, gamepad);
|
|
8717
|
+
const dpad = gamepadDpad(gamepad);
|
|
8718
|
+
if (both)
|
|
8719
|
+
return -(stick.y || dpad.y) || (stick.x || dpad.x);
|
|
8720
|
+
return vertical ? -(stick.y || dpad.y) : (stick.x || dpad.x);
|
|
8721
|
+
}
|
|
8722
|
+
const up = 'ArrowUp', down = 'ArrowDown', left = 'ArrowLeft', right = 'ArrowRight';
|
|
8723
|
+
if (both)
|
|
8724
|
+
{
|
|
8725
|
+
return keyIsDown(up) || keyIsDown(left) ? -1 :
|
|
8726
|
+
keyIsDown(down) || keyIsDown(right) ? 1 : 0;
|
|
8727
|
+
}
|
|
8728
|
+
const back = vertical ? up : left;
|
|
8729
|
+
const forward = vertical ? down : right;
|
|
8730
|
+
return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
|
|
8731
|
+
}
|
|
8732
|
+
|
|
8733
|
+
/** Get other axis navigation direction from gamepad or keyboard
|
|
8734
|
+
* @return {Vector2} */
|
|
8735
|
+
getNavigationOtherDirection()
|
|
8736
|
+
{
|
|
8737
|
+
if (uiSystem.navigationDirection === 2)
|
|
8738
|
+
return 0; // other direction disabled
|
|
8739
|
+
|
|
8740
|
+
const vertical = uiSystem.navigationDirection === 1;
|
|
8741
|
+
if (isUsingGamepad)
|
|
8742
|
+
{
|
|
8743
|
+
const gamepad = this.navigationGamepadIndex;
|
|
8744
|
+
const stick = gamepadStick(0, gamepad);
|
|
8745
|
+
const dpad = gamepadDpad(gamepad);
|
|
8746
|
+
return !vertical ? (stick.y || dpad.y) : (stick.x || dpad.x);
|
|
8747
|
+
}
|
|
8748
|
+
const back = !vertical ? 'ArrowUp' : 'ArrowLeft';
|
|
8749
|
+
const forward = !vertical ? 'ArrowDown' : 'ArrowRight';
|
|
8750
|
+
return keyIsDown(back) ? -1 : keyIsDown(forward) ? 1 : 0;
|
|
8751
|
+
}
|
|
8752
|
+
|
|
8753
|
+
/** Get if navigation button was pressed from gamepad or keyboard
|
|
8754
|
+
* @return {boolean} */
|
|
8755
|
+
getNavigationWasPressed()
|
|
8756
|
+
{
|
|
8757
|
+
const gamepad = this.navigationGamepadIndex;
|
|
8758
|
+
return isUsingGamepad ? gamepadWasPressed(0, gamepad) :
|
|
8759
|
+
keyWasPressed('Space') || keyWasPressed('Enter');
|
|
8760
|
+
}
|
|
8761
|
+
|
|
8762
|
+
/** Show a confirmation dialog with Yes/No buttons
|
|
8763
|
+
* Centers the dialog on the screen with darkened background
|
|
8764
|
+
* @param {string} [text] - The message to display
|
|
8765
|
+
* @param {Function} [yesCallback] - Called when Yes is clicked
|
|
8766
|
+
* @param {Function} [noCallback] - Called when No is clicked
|
|
8767
|
+
* @param {Vector2} [size] - Size of the confirmation dialog
|
|
8768
|
+
* @param {string} [exitKey] - Key that can exit the menu
|
|
8769
|
+
* @return {UIObject} The confirmation menu object
|
|
8770
|
+
*/
|
|
8771
|
+
showConfirmDialog(text='Are you sure?', yesCallback, noCallback, size=vec2(500,250), exitKey='Escape')
|
|
8772
|
+
{
|
|
8773
|
+
ASSERT(!uiSystem.confirmDialog);
|
|
8774
|
+
|
|
8775
|
+
const savedNavigationDirection = uiSystem.navigationDirection;
|
|
8776
|
+
|
|
8777
|
+
// allow both axies for navigation
|
|
8778
|
+
uiSystem.navigationDirection = 2;
|
|
8779
|
+
|
|
8780
|
+
// confirm menu
|
|
8781
|
+
const confirmMenu = new UIObject(vec2(), size);
|
|
8782
|
+
uiSystem.confirmDialog = confirmMenu;
|
|
8783
|
+
confirmMenu.onRender = ()=>
|
|
8784
|
+
{
|
|
8785
|
+
confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
8786
|
+
const backgroundColor = hsl(0,0,0,.7);
|
|
8787
|
+
uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
|
|
8788
|
+
}
|
|
8789
|
+
confirmMenu.onUpdate = ()=>
|
|
8790
|
+
{
|
|
8791
|
+
if (keyWasPressed(exitKey))
|
|
8792
|
+
closeMenu();
|
|
8793
|
+
}
|
|
8794
|
+
confirmMenu.isMouseOverlapping = ()=> true; // always hover
|
|
8795
|
+
|
|
8796
|
+
// title text
|
|
8797
|
+
const gap = 50;
|
|
8798
|
+
const textTitle = new UIText(vec2(0,-50), vec2(size.x-gap,70), text);
|
|
8799
|
+
confirmMenu.addChild(textTitle);
|
|
8800
|
+
|
|
8801
|
+
// yes button
|
|
8802
|
+
const buttonYes = new UIButton(vec2(-80,50), vec2(120,70), 'Yes');
|
|
8803
|
+
buttonYes.textHeight = 40;
|
|
8804
|
+
buttonYes.navigationIndex = 1;
|
|
8805
|
+
buttonYes.hoverColor = hsl(0,1,.5);
|
|
8806
|
+
buttonYes.onClick = ()=> { closeMenu(); yesCallback && yesCallback(); };
|
|
8807
|
+
confirmMenu.addChild(buttonYes);
|
|
8808
|
+
|
|
8809
|
+
// no button
|
|
8810
|
+
const buttonNo = new UIButton(vec2(80,50), vec2(120,70), 'No');
|
|
8811
|
+
buttonNo.textHeight = 40;
|
|
8812
|
+
buttonNo.navigationIndex = 2;
|
|
8813
|
+
buttonNo.navigationAutoSelect = true;
|
|
8814
|
+
buttonNo.onClick = ()=> { closeMenu(); noCallback && noCallback(); };
|
|
8815
|
+
confirmMenu.addChild(buttonNo);
|
|
8816
|
+
|
|
8817
|
+
// close menu and return to normal navigation
|
|
8818
|
+
function closeMenu()
|
|
8819
|
+
{
|
|
8820
|
+
ASSERT(uiSystem.confirmDialog === confirmMenu);
|
|
8821
|
+
confirmMenu.destroy();
|
|
8822
|
+
uiSystem.confirmDialog = undefined;
|
|
8823
|
+
uiSystem.navigationDirection = savedNavigationDirection;
|
|
8824
|
+
inputClear();
|
|
8825
|
+
}
|
|
8343
8826
|
}
|
|
8344
8827
|
}
|
|
8345
8828
|
|
|
@@ -8395,7 +8878,13 @@ class UIObject
|
|
|
8395
8878
|
/** @property {number} - Override for text height */
|
|
8396
8879
|
this.textHeight = undefined;
|
|
8397
8880
|
/** @property {number} - Scale text to fit in the object */
|
|
8398
|
-
this.
|
|
8881
|
+
this.textFitScale = uiSystem.defaultTextFitScale;
|
|
8882
|
+
/** @property {Vector2} - How much to offset the text shadow or undefined */
|
|
8883
|
+
this.textShadow = undefined;
|
|
8884
|
+
/** @property {number} - Color for text line drawing */
|
|
8885
|
+
this.textLineColor = uiSystem.defaultLineColor.copy();
|
|
8886
|
+
/** @property {number} - Width for text line drawing */
|
|
8887
|
+
this.textLineWidth = 0;
|
|
8399
8888
|
/** @property {boolean} - Should this object be drawn */
|
|
8400
8889
|
this.visible = true;
|
|
8401
8890
|
/** @property {Array<UIObject>} - A list of this object's children */
|
|
@@ -8421,16 +8910,17 @@ class UIObject
|
|
|
8421
8910
|
/** @property {number} - Size of shadow blur */
|
|
8422
8911
|
this.shadowBlur = uiSystem.defaultShadowBlur;
|
|
8423
8912
|
/** @property {Vector2} - Offset of shadow blur */
|
|
8424
|
-
this.shadowOffset = uiSystem.defaultShadowOffset
|
|
8425
|
-
|
|
8913
|
+
this.shadowOffset = uiSystem.defaultShadowOffset?.copy();
|
|
8914
|
+
/** @property {number} - Optional navigation order index, lower values are selected first */
|
|
8915
|
+
this.navigationIndex = undefined;
|
|
8916
|
+
/** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
|
|
8917
|
+
this.navigationAutoSelect = false;
|
|
8426
8918
|
|
|
8427
|
-
|
|
8428
|
-
this.textShadow = undefined;
|
|
8919
|
+
uiSystem.uiObjects.push(this);
|
|
8429
8920
|
}
|
|
8430
8921
|
|
|
8431
8922
|
/** Add a child UIObject to this object
|
|
8432
|
-
* @param {UIObject} child
|
|
8433
|
-
*/
|
|
8923
|
+
* @param {UIObject} child */
|
|
8434
8924
|
addChild(child)
|
|
8435
8925
|
{
|
|
8436
8926
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
@@ -8439,8 +8929,7 @@ class UIObject
|
|
|
8439
8929
|
}
|
|
8440
8930
|
|
|
8441
8931
|
/** Remove a child UIObject from this object
|
|
8442
|
-
* @param {UIObject} child
|
|
8443
|
-
*/
|
|
8932
|
+
* @param {UIObject} child */
|
|
8444
8933
|
removeChild(child)
|
|
8445
8934
|
{
|
|
8446
8935
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
@@ -8448,7 +8937,6 @@ class UIObject
|
|
|
8448
8937
|
child.parent = undefined;
|
|
8449
8938
|
}
|
|
8450
8939
|
|
|
8451
|
-
|
|
8452
8940
|
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
8453
8941
|
destroy()
|
|
8454
8942
|
{
|
|
@@ -8464,9 +8952,9 @@ class UIObject
|
|
|
8464
8952
|
child.destroy();
|
|
8465
8953
|
}
|
|
8466
8954
|
}
|
|
8955
|
+
|
|
8467
8956
|
/** Check if the mouse is overlapping a box in screen space
|
|
8468
|
-
* @return {boolean} - True if overlapping
|
|
8469
|
-
*/
|
|
8957
|
+
* @return {boolean} - True if overlapping */
|
|
8470
8958
|
isMouseOverlapping()
|
|
8471
8959
|
{
|
|
8472
8960
|
if (!mouseInWindow) return false;
|
|
@@ -8483,11 +8971,16 @@ class UIObject
|
|
|
8483
8971
|
// call the custom update callback
|
|
8484
8972
|
this.onUpdate();
|
|
8485
8973
|
|
|
8974
|
+
// unset active if disabled
|
|
8975
|
+
if (this.disabled && this == uiSystem.activeObject)
|
|
8976
|
+
uiSystem.activeObject = undefined;
|
|
8977
|
+
|
|
8486
8978
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
8487
8979
|
const isActive = this.isActiveObject();
|
|
8488
8980
|
const mouseDown = mouseIsDown(0);
|
|
8489
8981
|
const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
|
|
8490
8982
|
if (this.canBeHover)
|
|
8983
|
+
if (!uiSystem.navigationMode) // no mouse hover in navigation mode
|
|
8491
8984
|
if (mousePress || isActive || (!mouseDown && !isTouchDevice))
|
|
8492
8985
|
if (!uiSystem.hoverObject && this.isMouseOverlapping())
|
|
8493
8986
|
uiSystem.hoverObject = this;
|
|
@@ -8501,8 +8994,7 @@ class UIObject
|
|
|
8501
8994
|
{
|
|
8502
8995
|
if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
|
|
8503
8996
|
this.onPress();
|
|
8504
|
-
|
|
8505
|
-
this.soundPress.play();
|
|
8997
|
+
this.soundPress && this.soundPress.play();
|
|
8506
8998
|
if (uiSystem.activeObject && !isActive)
|
|
8507
8999
|
uiSystem.activeObject.onRelease();
|
|
8508
9000
|
uiSystem.activeObject = this;
|
|
@@ -8511,10 +9003,10 @@ class UIObject
|
|
|
8511
9003
|
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
8512
9004
|
{
|
|
8513
9005
|
this.onClick();
|
|
8514
|
-
|
|
8515
|
-
this.soundClick.play();
|
|
9006
|
+
this.soundClick && this.soundClick.play();
|
|
8516
9007
|
}
|
|
8517
9008
|
}
|
|
9009
|
+
|
|
8518
9010
|
// clear mouse was pressed state even when disabled
|
|
8519
9011
|
mousePress && inputClearKey(0,0,0,1,0);
|
|
8520
9012
|
}
|
|
@@ -8522,8 +9014,7 @@ class UIObject
|
|
|
8522
9014
|
if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
|
|
8523
9015
|
{
|
|
8524
9016
|
this.onRelease();
|
|
8525
|
-
|
|
8526
|
-
this.soundRelease.play();
|
|
9017
|
+
this.soundRelease && this.soundRelease.play();
|
|
8527
9018
|
uiSystem.activeObject = undefined;
|
|
8528
9019
|
}
|
|
8529
9020
|
|
|
@@ -8535,29 +9026,40 @@ class UIObject
|
|
|
8535
9026
|
/** Render the object, called automatically by plugin once each frame */
|
|
8536
9027
|
render()
|
|
8537
9028
|
{
|
|
8538
|
-
|
|
9029
|
+
// call the custom render callback
|
|
9030
|
+
this.onRender();
|
|
8539
9031
|
|
|
8540
|
-
|
|
8541
|
-
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
8542
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
8543
|
-
}
|
|
9032
|
+
if (!this.size.x || !this.size.y) return;
|
|
8544
9033
|
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
9034
|
+
const isNavigationObject = this.isNavigationObject();
|
|
9035
|
+
const lineColor = isNavigationObject ? this.color :
|
|
9036
|
+
this.interactive && this.isActiveObject() && !this.disabled ?
|
|
9037
|
+
this.color : this.lineColor;
|
|
9038
|
+
const color = isNavigationObject ? this.hoverColor :
|
|
9039
|
+
this.disabled ? this.disabledColor :
|
|
9040
|
+
this.interactive ?
|
|
9041
|
+
this.isHoverObject() ? this.hoverColor :
|
|
9042
|
+
this.isActiveObject() ? this.activeColor || this.color :
|
|
9043
|
+
this.color : this.color;
|
|
9044
|
+
const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
|
|
9045
|
+
|
|
9046
|
+
uiSystem.drawRect(this.pos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
8551
9047
|
}
|
|
8552
9048
|
|
|
8553
9049
|
/** Get the size for text with overrides and scale
|
|
8554
|
-
* @return {Vector2}
|
|
8555
|
-
*/
|
|
9050
|
+
* @return {Vector2} */
|
|
8556
9051
|
getTextSize()
|
|
8557
9052
|
{
|
|
8558
9053
|
return vec2(
|
|
8559
|
-
this.textWidth || this.
|
|
8560
|
-
this.textHeight || this.
|
|
9054
|
+
this.textWidth || this.textFitScale * this.size.x,
|
|
9055
|
+
this.textHeight || this.textFitScale * this.size.y);
|
|
9056
|
+
}
|
|
9057
|
+
|
|
9058
|
+
/** Called when the navigation button is pressed on this object */
|
|
9059
|
+
navigatePressed()
|
|
9060
|
+
{
|
|
9061
|
+
this.onClick();
|
|
9062
|
+
this.soundClick && this.soundClick.play();
|
|
8561
9063
|
}
|
|
8562
9064
|
|
|
8563
9065
|
/** @return {boolean} - Is the mouse hovering over this element */
|
|
@@ -8566,9 +9068,51 @@ class UIObject
|
|
|
8566
9068
|
/** @return {boolean} - Is the mouse held onto this element */
|
|
8567
9069
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
8568
9070
|
|
|
8569
|
-
/**
|
|
9071
|
+
/** @return {boolean} - Is the gamepad or keyboard navigation object */
|
|
9072
|
+
isNavigationObject() { return uiSystem.navigationObject === this; }
|
|
9073
|
+
|
|
9074
|
+
/** @return {boolean} - Can it be interacted with */
|
|
9075
|
+
isInteractive() { return this.interactive && this.visible && !this.disabled;}
|
|
9076
|
+
|
|
9077
|
+
/** Returns string containing info about this object for debugging
|
|
9078
|
+
* @return {string} */
|
|
9079
|
+
toString()
|
|
9080
|
+
{
|
|
9081
|
+
if (!debug) return;
|
|
9082
|
+
|
|
9083
|
+
let text = 'type = ' + this.constructor.name;
|
|
9084
|
+
if (this.text)
|
|
9085
|
+
text += '\ntext = ' + this.text;
|
|
9086
|
+
if (this.pos.x || this.pos.y)
|
|
9087
|
+
text += '\npos = ' + this.pos;
|
|
9088
|
+
if (this.localPos.x || this.localPos.y)
|
|
9089
|
+
text += '\localPos = ' + this.localPos;
|
|
9090
|
+
if (this.size.x || this.size.y)
|
|
9091
|
+
text += '\nsize = ' + this.size;
|
|
9092
|
+
if (this.color)
|
|
9093
|
+
text += '\ncolor = ' + this.color;
|
|
9094
|
+
return text;
|
|
9095
|
+
}
|
|
9096
|
+
|
|
9097
|
+
/** Called if uiDebug is enabled
|
|
9098
|
+
* @param {boolean} visible */
|
|
9099
|
+
renderDebug(visible=true)
|
|
9100
|
+
{
|
|
9101
|
+
// apply color based on state
|
|
9102
|
+
const color =
|
|
9103
|
+
!visible ? GREEN :
|
|
9104
|
+
this.isHoverObject() ? YELLOW :
|
|
9105
|
+
this.disabled ? PURPLE :
|
|
9106
|
+
this.interactive ? RED : BLUE;
|
|
9107
|
+
uiSystem.drawRect(this.pos, this.size, CLEAR_BLACK, 4, color);
|
|
9108
|
+
}
|
|
9109
|
+
|
|
9110
|
+
/** Called each frame before object updates */
|
|
8570
9111
|
onUpdate() {}
|
|
8571
9112
|
|
|
9113
|
+
/** Called each frame before object renders */
|
|
9114
|
+
onRender() {}
|
|
9115
|
+
|
|
8572
9116
|
/** Called when the mouse enters the object */
|
|
8573
9117
|
onEnter() {}
|
|
8574
9118
|
|
|
@@ -8616,16 +9160,25 @@ class UIText extends UIObject
|
|
|
8616
9160
|
this.align = align;
|
|
8617
9161
|
this.font = font;
|
|
8618
9162
|
|
|
8619
|
-
// make text not outlined by default
|
|
8620
|
-
this.lineWidth = 0;
|
|
8621
9163
|
// text can not be a hover object by default
|
|
8622
9164
|
this.canBeHover = false;
|
|
9165
|
+
|
|
9166
|
+
// no background by default
|
|
9167
|
+
this.color = CLEAR_BLACK;
|
|
9168
|
+
this.shadowColor = CLEAR_BLACK;
|
|
9169
|
+
this.gradientColor = undefined;
|
|
9170
|
+
this.lineWidth = 0;
|
|
9171
|
+
|
|
9172
|
+
// use max fit scale by default
|
|
9173
|
+
this.textFitScale = 1;
|
|
8623
9174
|
}
|
|
8624
9175
|
render()
|
|
8625
9176
|
{
|
|
8626
|
-
|
|
9177
|
+
super.render();
|
|
9178
|
+
|
|
9179
|
+
// render the text
|
|
8627
9180
|
const textSize = this.getTextSize();
|
|
8628
|
-
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.
|
|
9181
|
+
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);
|
|
8629
9182
|
}
|
|
8630
9183
|
}
|
|
8631
9184
|
|
|
@@ -8661,10 +9214,13 @@ class UITile extends UIObject
|
|
|
8661
9214
|
this.mirror = mirror;
|
|
8662
9215
|
// set properties
|
|
8663
9216
|
this.color = color.copy();
|
|
9217
|
+
|
|
9218
|
+
// no shadow by default
|
|
9219
|
+
this.shadowColor = CLEAR_BLACK;
|
|
8664
9220
|
}
|
|
8665
9221
|
render()
|
|
8666
9222
|
{
|
|
8667
|
-
uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror);
|
|
9223
|
+
uiSystem.drawTile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
8668
9224
|
}
|
|
8669
9225
|
}
|
|
8670
9226
|
|
|
@@ -8689,6 +9245,9 @@ class UIButton extends UIObject
|
|
|
8689
9245
|
ASSERT(isString(text), 'ui button must be a string');
|
|
8690
9246
|
ASSERT(isColor(color), 'ui button color must be a color');
|
|
8691
9247
|
|
|
9248
|
+
/** @property {Vector2} - Text offset for the button */
|
|
9249
|
+
this.textOffset = vec2();
|
|
9250
|
+
|
|
8692
9251
|
// set properties
|
|
8693
9252
|
this.text = text;
|
|
8694
9253
|
this.color = color.copy();
|
|
@@ -8700,8 +9259,8 @@ class UIButton extends UIObject
|
|
|
8700
9259
|
|
|
8701
9260
|
// draw the text scaled to fit
|
|
8702
9261
|
const textSize = this.getTextSize();
|
|
8703
|
-
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8704
|
-
this.textColor,
|
|
9262
|
+
uiSystem.drawText(this.text, this.pos.add(this.textOffset), textSize,
|
|
9263
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
8705
9264
|
}
|
|
8706
9265
|
}
|
|
8707
9266
|
|
|
@@ -8755,7 +9314,7 @@ class UICheckbox extends UIObject
|
|
|
8755
9314
|
const textSize = this.getTextSize();
|
|
8756
9315
|
const pos = this.pos.add(vec2(this.size.x,0));
|
|
8757
9316
|
uiSystem.drawText(this.text, pos, textSize,
|
|
8758
|
-
this.textColor,
|
|
9317
|
+
this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
8759
9318
|
}
|
|
8760
9319
|
}
|
|
8761
9320
|
|
|
@@ -8797,7 +9356,11 @@ class UIScrollbar extends UIObject
|
|
|
8797
9356
|
update()
|
|
8798
9357
|
{
|
|
8799
9358
|
super.update();
|
|
8800
|
-
if (this.
|
|
9359
|
+
if (!this.interactive)
|
|
9360
|
+
return;
|
|
9361
|
+
|
|
9362
|
+
const oldValue = this.value;
|
|
9363
|
+
if (this.isActiveObject())
|
|
8801
9364
|
{
|
|
8802
9365
|
// handle horizontal or vertical scrollbar
|
|
8803
9366
|
const isHorizontal = this.size.x > this.size.y;
|
|
@@ -8809,14 +9372,19 @@ class UIScrollbar extends UIObject
|
|
|
8809
9372
|
const handleWidth = barSize - handleSize;
|
|
8810
9373
|
const p1 = centerPos - handleWidth/2;
|
|
8811
9374
|
const p2 = centerPos + handleWidth/2;
|
|
8812
|
-
const oldValue = this.value;
|
|
8813
|
-
|
|
8814
9375
|
const p = uiSystem.screenToNative(mousePosScreen);
|
|
8815
9376
|
this.value = isHorizontal ?
|
|
8816
9377
|
percent(p.x, p1, p2) :
|
|
8817
9378
|
percent(p.y, p2, p1);
|
|
8818
|
-
this.value === oldValue || this.onChange();
|
|
8819
9379
|
}
|
|
9380
|
+
else if (this.isNavigationObject())
|
|
9381
|
+
{
|
|
9382
|
+
// gamepad/keyboard navigation adjustment
|
|
9383
|
+
const direction = uiSystem.getNavigationOtherDirection();
|
|
9384
|
+
if (!uiSystem.navigationTimer.active())
|
|
9385
|
+
this.value = clamp(this.value + direction*.01);
|
|
9386
|
+
}
|
|
9387
|
+
this.value === oldValue || this.onChange();
|
|
8820
9388
|
}
|
|
8821
9389
|
render()
|
|
8822
9390
|
{
|
|
@@ -8841,7 +9409,14 @@ class UIScrollbar extends UIObject
|
|
|
8841
9409
|
// draw the text scaled to fit on the scrollbar
|
|
8842
9410
|
const textSize = this.getTextSize();
|
|
8843
9411
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8844
|
-
this.textColor,
|
|
9412
|
+
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
9413
|
+
}
|
|
9414
|
+
navigatePressed()
|
|
9415
|
+
{
|
|
9416
|
+
// toggle value between 0 and 1
|
|
9417
|
+
this.value = this.value ? 0 : 1;
|
|
9418
|
+
this.onRelease();
|
|
9419
|
+
super.navigatePressed();
|
|
8845
9420
|
}
|
|
8846
9421
|
}
|
|
8847
9422
|
|
|
@@ -8875,7 +9450,7 @@ class UIVideo extends UIObject
|
|
|
8875
9450
|
this.color = BLACK; // default to black background
|
|
8876
9451
|
this.cornerRadius = 0; // default to no corner radius
|
|
8877
9452
|
|
|
8878
|
-
/** @property {
|
|
9453
|
+
/** @property {number} - The video volume */
|
|
8879
9454
|
this.volume = volume;
|
|
8880
9455
|
|
|
8881
9456
|
// create video element
|
|
@@ -8908,7 +9483,7 @@ class UIVideo extends UIObject
|
|
|
8908
9483
|
|
|
8909
9484
|
/** Check if video is currently loading
|
|
8910
9485
|
* @return {boolean} */
|
|
8911
|
-
|
|
9486
|
+
isLoading()
|
|
8912
9487
|
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
8913
9488
|
|
|
8914
9489
|
/** Check if video is currently paused
|
|
@@ -8918,7 +9493,7 @@ class UIVideo extends UIObject
|
|
|
8918
9493
|
/** Check if video is currently playing
|
|
8919
9494
|
* @return {boolean} */
|
|
8920
9495
|
isPlaying()
|
|
8921
|
-
{ return !this.isPaused() && !this.hasEnded() && !this.
|
|
9496
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoading(); }
|
|
8922
9497
|
|
|
8923
9498
|
/** Check if video has ended playing
|
|
8924
9499
|
* @return {boolean} */
|
|
@@ -8967,7 +9542,7 @@ class UIVideo extends UIObject
|
|
|
8967
9542
|
{
|
|
8968
9543
|
super.render();
|
|
8969
9544
|
|
|
8970
|
-
if (this.
|
|
9545
|
+
if (this.isLoading())
|
|
8971
9546
|
return;
|
|
8972
9547
|
const context = uiSystem.uiContext;
|
|
8973
9548
|
const s = this.size;
|
|
@@ -10751,7 +11326,7 @@ class Box2dPlugin
|
|
|
10751
11326
|
* @param {Vector2} v */
|
|
10752
11327
|
vec2dTo(v)
|
|
10753
11328
|
{
|
|
10754
|
-
ASSERT(v
|
|
11329
|
+
ASSERT(isVector2(v));
|
|
10755
11330
|
return new box2d.instance.b2Vec2(v.x, v.y);
|
|
10756
11331
|
}
|
|
10757
11332
|
|