littlejsengine 1.15.2 → 1.15.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/littlejs.d.ts +190 -63
- package/dist/littlejs.esm.js +1267 -605
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1261 -604
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1179 -552
- package/examples/electron/index.html +2 -2
- package/examples/index.html +4 -3
- package/examples/particles/index.html +22 -6
- package/examples/shorts/base.html +2 -1
- package/examples/shorts/box2dCar.js +1 -1
- package/examples/shorts/box2dPool.js +9 -10
- package/examples/shorts/empty.js +1 -1
- package/examples/shorts/input.js +30 -25
- package/examples/shorts/music.js +1 -0
- package/examples/shorts/musicPlayer.js +1 -0
- package/examples/shorts/sequencer.js +2 -0
- package/examples/shorts/sound.js +1 -0
- package/examples/shorts/tileLayer.js +2 -2
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/timers.js +1 -0
- package/examples/shorts/uiSystem.js +26 -14
- package/examples/shorts/videoPlayer.js +2 -1
- package/examples/starter/index.html +3 -2
- package/examples/uiSystem/game.js +28 -15
- package/package.json +1 -1
- package/plugins/box2d.js +1 -1
- package/plugins/desktop.ini +2 -0
- package/plugins/pluginExport.js +2 -0
- package/plugins/uiSystem.js +503 -91
- package/src/engine.js +2 -169
- package/src/engineAudio.js +0 -1
- package/src/engineBuild.js +1 -0
- package/src/engineDebug.js +84 -54
- package/src/engineDraw.js +81 -40
- package/src/engineExport.js +4 -1
- package/src/engineInput.js +526 -381
- package/src/engineObject.js +27 -23
- package/src/engineSettings.js +11 -6
- package/src/engineSplash.js +173 -0
- package/src/engineTileLayer.js +3 -3
- package/src/engineUtilities.js +17 -1
package/dist/littlejs.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;
|
|
@@ -2997,7 +3021,9 @@ class EngineObject
|
|
|
2997
3021
|
{
|
|
2998
3022
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
2999
3023
|
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
3000
|
-
this.children.
|
|
3024
|
+
const index = this.children.indexOf(child);
|
|
3025
|
+
ASSERT(index >= 0, 'child not found in children array');
|
|
3026
|
+
index >= 0 && this.children.splice(index, 1);
|
|
3001
3027
|
child.parent = 0;
|
|
3002
3028
|
}
|
|
3003
3029
|
|
|
@@ -3034,21 +3060,20 @@ class EngineObject
|
|
|
3034
3060
|
* @return {string} */
|
|
3035
3061
|
toString()
|
|
3036
3062
|
{
|
|
3037
|
-
if (debug)
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
}
|
|
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;
|
|
3052
3077
|
}
|
|
3053
3078
|
|
|
3054
3079
|
/** Render debug info for this object */
|
|
@@ -3217,7 +3242,7 @@ class TileInfo
|
|
|
3217
3242
|
this.padding = padding;
|
|
3218
3243
|
/** @property {TextureInfo} - The texture info for this tile */
|
|
3219
3244
|
this.textureInfo = textureInfos[this.textureIndex];
|
|
3220
|
-
/** @property {
|
|
3245
|
+
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
3221
3246
|
this.bleedScale = bleedScale;
|
|
3222
3247
|
}
|
|
3223
3248
|
|
|
@@ -3316,15 +3341,11 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
|
3316
3341
|
|
|
3317
3342
|
const textureInfo = tileInfo && tileInfo.textureInfo;
|
|
3318
3343
|
const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
|
|
3319
|
-
if (useWebGL)
|
|
3344
|
+
if (useWebGL && glEnable)
|
|
3320
3345
|
{
|
|
3321
3346
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3322
3347
|
if (screenSpace)
|
|
3323
|
-
|
|
3324
|
-
// convert to world space
|
|
3325
|
-
pos = screenToWorld(pos);
|
|
3326
|
-
size = size.scale(1/cameraScale);
|
|
3327
|
-
}
|
|
3348
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
3328
3349
|
if (textureInfo)
|
|
3329
3350
|
{
|
|
3330
3351
|
// calculate uvs and render
|
|
@@ -3412,7 +3433,8 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
3412
3433
|
ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
|
|
3413
3434
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
3414
3435
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3415
|
-
|
|
3436
|
+
|
|
3437
|
+
if (useWebGL && glEnable)
|
|
3416
3438
|
{
|
|
3417
3439
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3418
3440
|
if (screenSpace)
|
|
@@ -3420,6 +3442,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
3420
3442
|
// convert to world space
|
|
3421
3443
|
pos = screenToWorld(pos);
|
|
3422
3444
|
size = size.scale(1/cameraScale);
|
|
3445
|
+
angle += cameraAngle;
|
|
3423
3446
|
}
|
|
3424
3447
|
// build 4 corner points for the rectangle
|
|
3425
3448
|
const points = [], colors = [];
|
|
@@ -3475,17 +3498,14 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
3475
3498
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3476
3499
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
3477
3500
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3478
|
-
|
|
3501
|
+
|
|
3502
|
+
if (useWebGL && glEnable)
|
|
3479
3503
|
{
|
|
3480
3504
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3481
|
-
let
|
|
3505
|
+
let size = vec2(1);
|
|
3482
3506
|
if (screenSpace)
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
pos = screenToWorld(pos);
|
|
3486
|
-
scale = 1/cameraScale;
|
|
3487
|
-
}
|
|
3488
|
-
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, scale, scale, angle, wrap);
|
|
3507
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
3508
|
+
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, size.x, size.y, angle, wrap);
|
|
3489
3509
|
}
|
|
3490
3510
|
else
|
|
3491
3511
|
{
|
|
@@ -3581,19 +3601,15 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
3581
3601
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
3582
3602
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3583
3603
|
|
|
3584
|
-
if (useWebGL)
|
|
3604
|
+
if (useWebGL && glEnable)
|
|
3585
3605
|
{
|
|
3586
3606
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3587
|
-
let
|
|
3607
|
+
let size = vec2(1);
|
|
3588
3608
|
if (screenSpace)
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
pos = screenToWorld(pos);
|
|
3592
|
-
scale = 1/cameraScale;
|
|
3593
|
-
}
|
|
3594
|
-
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
|
|
3609
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
3610
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, size.x, size.y, angle);
|
|
3595
3611
|
if (lineWidth > 0)
|
|
3596
|
-
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y,
|
|
3612
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, size.x, size.y, angle);
|
|
3597
3613
|
}
|
|
3598
3614
|
else
|
|
3599
3615
|
{
|
|
@@ -3636,7 +3652,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
3636
3652
|
ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
|
|
3637
3653
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3638
3654
|
|
|
3639
|
-
if (useWebGL)
|
|
3655
|
+
if (useWebGL && glEnable)
|
|
3640
3656
|
{
|
|
3641
3657
|
// draw as a regular polygon
|
|
3642
3658
|
const sides = glCircleSides;
|
|
@@ -3699,14 +3715,10 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
3699
3715
|
ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
|
|
3700
3716
|
|
|
3701
3717
|
if (!screenSpace)
|
|
3702
|
-
|
|
3703
|
-
// transform from world space to screen space
|
|
3704
|
-
pos = worldToScreen(pos);
|
|
3705
|
-
size = size.scale(cameraScale);
|
|
3706
|
-
}
|
|
3718
|
+
[pos, size, angle] = worldToScreenTransform(pos, size, angle);
|
|
3707
3719
|
context.save();
|
|
3708
3720
|
context.translate(pos.x+.5, pos.y+.5);
|
|
3709
|
-
context.rotate(angle
|
|
3721
|
+
context.rotate(angle);
|
|
3710
3722
|
context.scale(mirror ? -size.x : size.x, -size.y);
|
|
3711
3723
|
drawFunction(context);
|
|
3712
3724
|
context.restore();
|
|
@@ -3717,7 +3729,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
3717
3729
|
|
|
3718
3730
|
/** Draw text on main canvas in world space
|
|
3719
3731
|
* Automatically splits new lines into rows
|
|
3720
|
-
* @param {string} text
|
|
3732
|
+
* @param {string|number} text
|
|
3721
3733
|
* @param {Vector2} pos
|
|
3722
3734
|
* @param {number} [size]
|
|
3723
3735
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3732,12 +3744,19 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
3732
3744
|
* @memberof Draw */
|
|
3733
3745
|
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
3734
3746
|
{
|
|
3735
|
-
|
|
3747
|
+
// convert to screen space
|
|
3748
|
+
pos = worldToScreen(pos);
|
|
3749
|
+
size *= cameraScale;
|
|
3750
|
+
lineWidth *= cameraScale;
|
|
3751
|
+
angle -= cameraAngle;
|
|
3752
|
+
angle *= -1;
|
|
3753
|
+
|
|
3754
|
+
drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
|
|
3736
3755
|
}
|
|
3737
3756
|
|
|
3738
3757
|
/** Draw text on overlay canvas in world space
|
|
3739
3758
|
* Automatically splits new lines into rows
|
|
3740
|
-
* @param {string} text
|
|
3759
|
+
* @param {string|number} text
|
|
3741
3760
|
* @param {Vector2} pos
|
|
3742
3761
|
* @param {number} [size]
|
|
3743
3762
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3756,7 +3775,7 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
3756
3775
|
|
|
3757
3776
|
/** Draw text on overlay canvas in screen space
|
|
3758
3777
|
* Automatically splits new lines into rows
|
|
3759
|
-
* @param {string} text
|
|
3778
|
+
* @param {string|number} text
|
|
3760
3779
|
* @param {Vector2} pos
|
|
3761
3780
|
* @param {number} [size]
|
|
3762
3781
|
* @param {Color} [color=(1,1,1,1)]
|
|
@@ -3891,11 +3910,58 @@ function worldToScreenDelta(worldDelta)
|
|
|
3891
3910
|
return new Vector2(x * cameraScale, y * -cameraScale);
|
|
3892
3911
|
}
|
|
3893
3912
|
|
|
3894
|
-
/**
|
|
3913
|
+
/** Convert screen space transform to world space
|
|
3914
|
+
* @param {Vector2} screenPos
|
|
3915
|
+
* @param {Vector2} screenSize
|
|
3916
|
+
* @param {number} [screenAngle]
|
|
3917
|
+
* @return {[Vector2, Vector2, number]} - [pos, size, angle]
|
|
3918
|
+
* @memberof Draw */
|
|
3919
|
+
function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
|
|
3920
|
+
{
|
|
3921
|
+
return [
|
|
3922
|
+
screenToWorld(screenPos),
|
|
3923
|
+
screenSize.scale(1/cameraScale),
|
|
3924
|
+
screenAngle + cameraAngle
|
|
3925
|
+
];
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
/** Convert world space transform to screen space
|
|
3929
|
+
* @param {Vector2} worldPos
|
|
3930
|
+
* @param {Vector2} worldSize
|
|
3931
|
+
* @param {number} [worldAngle]
|
|
3932
|
+
* @return {[Vector2, Vector2, number]} - [pos, size, angle]
|
|
3933
|
+
* @memberof Draw */
|
|
3934
|
+
function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
|
|
3935
|
+
{
|
|
3936
|
+
return [
|
|
3937
|
+
worldToScreen(worldPos),
|
|
3938
|
+
worldSize.scale(cameraScale),
|
|
3939
|
+
worldAngle - cameraAngle
|
|
3940
|
+
];
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
/** Get the size of the camera window in world space
|
|
3895
3944
|
* @return {Vector2}
|
|
3896
3945
|
* @memberof Draw */
|
|
3897
3946
|
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
3898
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
|
+
|
|
3899
3965
|
/** Enable normal or additive blend mode
|
|
3900
3966
|
* @param {boolean} [additive]
|
|
3901
3967
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -4059,7 +4125,7 @@ class FontImage
|
|
|
4059
4125
|
}
|
|
4060
4126
|
|
|
4061
4127
|
/** Draw text in world space using the image font
|
|
4062
|
-
* @param {string} text
|
|
4128
|
+
* @param {string|number} text
|
|
4063
4129
|
* @param {Vector2} pos
|
|
4064
4130
|
* @param {number} [scale=.25]
|
|
4065
4131
|
* @param {boolean} [center]
|
|
@@ -4071,7 +4137,7 @@ class FontImage
|
|
|
4071
4137
|
}
|
|
4072
4138
|
|
|
4073
4139
|
/** Draw text on overlay canvas in world space using the image font
|
|
4074
|
-
* @param {string} text
|
|
4140
|
+
* @param {string|number} text
|
|
4075
4141
|
* @param {Vector2} pos
|
|
4076
4142
|
* @param {number} [scale]
|
|
4077
4143
|
* @param {boolean} [center]
|
|
@@ -4080,7 +4146,7 @@ class FontImage
|
|
|
4080
4146
|
{ this.drawText(text, pos, scale, center, overlayContext); }
|
|
4081
4147
|
|
|
4082
4148
|
/** Draw text on overlay canvas in screen space using the image font
|
|
4083
|
-
* @param {string} text
|
|
4149
|
+
* @param {string|number} text
|
|
4084
4150
|
* @param {Vector2} pos
|
|
4085
4151
|
* @param {number} [scale]
|
|
4086
4152
|
* @param {boolean} [center]
|
|
@@ -4124,6 +4190,85 @@ class FontImage
|
|
|
4124
4190
|
* @namespace Input
|
|
4125
4191
|
*/
|
|
4126
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
|
+
|
|
4127
4272
|
/** Returns true if device key is down
|
|
4128
4273
|
* @param {string|number} key
|
|
4129
4274
|
* @param {number} [device]
|
|
@@ -4131,9 +4276,9 @@ class FontImage
|
|
|
4131
4276
|
* @memberof Input */
|
|
4132
4277
|
function keyIsDown(key, device=0)
|
|
4133
4278
|
{
|
|
4134
|
-
ASSERT(key
|
|
4279
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4135
4280
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4136
|
-
return
|
|
4281
|
+
return !!(inputData[device]?.[key] & 1);
|
|
4137
4282
|
}
|
|
4138
4283
|
|
|
4139
4284
|
/** Returns true if device key was pressed this frame
|
|
@@ -4143,9 +4288,9 @@ function keyIsDown(key, device=0)
|
|
|
4143
4288
|
* @memberof Input */
|
|
4144
4289
|
function keyWasPressed(key, device=0)
|
|
4145
4290
|
{
|
|
4146
|
-
ASSERT(key
|
|
4291
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4147
4292
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4148
|
-
return
|
|
4293
|
+
return !!(inputData[device]?.[key] & 2);
|
|
4149
4294
|
}
|
|
4150
4295
|
|
|
4151
4296
|
/** Returns true if device key was released this frame
|
|
@@ -4155,172 +4300,182 @@ function keyWasPressed(key, device=0)
|
|
|
4155
4300
|
* @memberof Input */
|
|
4156
4301
|
function keyWasReleased(key, device=0)
|
|
4157
4302
|
{
|
|
4158
|
-
ASSERT(key
|
|
4303
|
+
ASSERT(isString(key), 'key must be a number or string');
|
|
4159
4304
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4160
|
-
return
|
|
4305
|
+
return !!(inputData[device]?.[key] & 4);
|
|
4161
4306
|
}
|
|
4162
4307
|
|
|
4163
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]
|
|
4164
4313
|
* @return {Vector2}
|
|
4165
4314
|
* @memberof Input */
|
|
4166
4315
|
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
4167
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');
|
|
4168
4321
|
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
4169
4322
|
return vec2(k(right) - k(left), k(up) - k(down));
|
|
4170
4323
|
}
|
|
4171
4324
|
|
|
4172
|
-
/** Clears all input
|
|
4173
|
-
* @memberof Input */
|
|
4174
|
-
function inputClear() { inputData = [[]]; touchGamepadButtons = []; }
|
|
4175
|
-
|
|
4176
|
-
/** Clears an input key state
|
|
4177
|
-
* @param {string|number} key
|
|
4178
|
-
* @param {number} [device]
|
|
4179
|
-
* @param {boolean} [clearDown=true]
|
|
4180
|
-
* @param {boolean} [clearPressed=true]
|
|
4181
|
-
* @param {boolean} [clearReleased=true]
|
|
4182
|
-
* @memberof Input */
|
|
4183
|
-
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
4184
|
-
{
|
|
4185
|
-
if (!inputData[device])
|
|
4186
|
-
return;
|
|
4187
|
-
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
4188
|
-
}
|
|
4189
|
-
|
|
4190
4325
|
/** Returns true if mouse button is down
|
|
4191
4326
|
* @function
|
|
4192
4327
|
* @param {number} button
|
|
4193
4328
|
* @return {boolean}
|
|
4194
4329
|
* @memberof Input */
|
|
4195
|
-
function mouseIsDown(button)
|
|
4330
|
+
function mouseIsDown(button)
|
|
4331
|
+
{
|
|
4332
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
4333
|
+
return keyIsDown(button);
|
|
4334
|
+
}
|
|
4196
4335
|
|
|
4197
4336
|
/** Returns true if mouse button was pressed
|
|
4198
4337
|
* @function
|
|
4199
4338
|
* @param {number} button
|
|
4200
4339
|
* @return {boolean}
|
|
4201
4340
|
* @memberof Input */
|
|
4202
|
-
function mouseWasPressed(button)
|
|
4341
|
+
function mouseWasPressed(button)
|
|
4342
|
+
{
|
|
4343
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
4344
|
+
return keyWasPressed(button);
|
|
4345
|
+
}
|
|
4203
4346
|
|
|
4204
4347
|
/** Returns true if mouse button was released
|
|
4205
4348
|
* @function
|
|
4206
4349
|
* @param {number} button
|
|
4207
4350
|
* @return {boolean}
|
|
4208
4351
|
* @memberof Input */
|
|
4209
|
-
function mouseWasReleased(button)
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
let mousePos = vec2();
|
|
4215
|
-
|
|
4216
|
-
/** Mouse pos in screen space
|
|
4217
|
-
* @type {Vector2}
|
|
4218
|
-
* @memberof Input */
|
|
4219
|
-
let mousePosScreen = vec2();
|
|
4220
|
-
|
|
4221
|
-
/** Mouse movement delta in world space
|
|
4222
|
-
* @type {Vector2}
|
|
4223
|
-
* @memberof Input */
|
|
4224
|
-
let mouseDelta = vec2();
|
|
4225
|
-
|
|
4226
|
-
/** Mouse movement delta in screen space
|
|
4227
|
-
* @type {Vector2}
|
|
4228
|
-
* @memberof Input */
|
|
4229
|
-
let mouseDeltaScreen = vec2();
|
|
4230
|
-
|
|
4231
|
-
/** Mouse wheel delta this frame
|
|
4232
|
-
* @type {number}
|
|
4233
|
-
* @memberof Input */
|
|
4234
|
-
let mouseWheel = 0;
|
|
4235
|
-
|
|
4236
|
-
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4237
|
-
* @type {boolean}
|
|
4238
|
-
* @memberof Input */
|
|
4239
|
-
let mouseInWindow = true;
|
|
4240
|
-
|
|
4241
|
-
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4242
|
-
* @type {boolean}
|
|
4243
|
-
* @memberof Input */
|
|
4244
|
-
let isUsingGamepad = false;
|
|
4245
|
-
|
|
4246
|
-
/** Prevents input continuing to the default browser handling (true by default)
|
|
4247
|
-
* @type {boolean}
|
|
4248
|
-
* @memberof Input */
|
|
4249
|
-
let inputPreventDefault = true;
|
|
4250
|
-
|
|
4251
|
-
/** Prevents input continuing to the default browser handling
|
|
4252
|
-
* This is useful to disable for html menus so the browser can handle input normally
|
|
4253
|
-
* @param {boolean} preventDefault
|
|
4254
|
-
* @memberof Input */
|
|
4255
|
-
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
|
+
}
|
|
4256
4357
|
|
|
4257
4358
|
/** Returns true if gamepad button is down
|
|
4258
4359
|
* @param {number} button
|
|
4259
4360
|
* @param {number} [gamepad]
|
|
4260
4361
|
* @return {boolean}
|
|
4261
4362
|
* @memberof Input */
|
|
4262
|
-
function gamepadIsDown(button, gamepad=
|
|
4263
|
-
{
|
|
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
|
+
}
|
|
4264
4369
|
|
|
4265
4370
|
/** Returns true if gamepad button was pressed
|
|
4266
4371
|
* @param {number} button
|
|
4267
4372
|
* @param {number} [gamepad]
|
|
4268
4373
|
* @return {boolean}
|
|
4269
4374
|
* @memberof Input */
|
|
4270
|
-
function gamepadWasPressed(button, gamepad=
|
|
4271
|
-
{
|
|
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
|
+
}
|
|
4272
4381
|
|
|
4273
4382
|
/** Returns true if gamepad button was released
|
|
4274
4383
|
* @param {number} button
|
|
4275
4384
|
* @param {number} [gamepad]
|
|
4276
4385
|
* @return {boolean}
|
|
4277
4386
|
* @memberof Input */
|
|
4278
|
-
function gamepadWasReleased(button, gamepad=
|
|
4279
|
-
{
|
|
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
|
+
}
|
|
4280
4393
|
|
|
4281
4394
|
/** Returns gamepad stick value
|
|
4282
4395
|
* @param {number} stick
|
|
4283
4396
|
* @param {number} [gamepad]
|
|
4284
4397
|
* @return {Vector2}
|
|
4285
4398
|
* @memberof Input */
|
|
4286
|
-
function gamepadStick(stick, gamepad=
|
|
4287
|
-
{
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
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
|
+
}
|
|
4291
4405
|
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
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
|
+
}
|
|
4295
4415
|
|
|
4296
|
-
|
|
4416
|
+
/** Returns true if passed in gamepad is connected
|
|
4417
|
+
* @param {number} [gamepad]
|
|
4418
|
+
* @return {boolean}
|
|
4419
|
+
* @memberof Input */
|
|
4420
|
+
function gamepadConnected(gamepad=gamepadPrimary)
|
|
4297
4421
|
{
|
|
4298
|
-
|
|
4422
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
4423
|
+
return !!inputData[gamepad+1];
|
|
4424
|
+
}
|
|
4299
4425
|
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4426
|
+
/** True if a touch device has been detected
|
|
4427
|
+
* @memberof Input */
|
|
4428
|
+
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
4303
4429
|
|
|
4304
|
-
|
|
4305
|
-
mousePos = screenToWorld(mousePosScreen);
|
|
4306
|
-
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
4430
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4307
4431
|
|
|
4308
|
-
|
|
4309
|
-
|
|
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);
|
|
4310
4439
|
}
|
|
4311
4440
|
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4441
|
+
/** Cancel any ongoing vibration
|
|
4442
|
+
* @memberof Input */
|
|
4443
|
+
function vibrateStop() { vibrate(0); }
|
|
4315
4444
|
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
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
|
|
4324
4479
|
|
|
4325
4480
|
function inputInit()
|
|
4326
4481
|
{
|
|
@@ -4350,6 +4505,18 @@ function inputInit()
|
|
|
4350
4505
|
if (inputWASDEmulateDirection)
|
|
4351
4506
|
inputData[0][remapKey(e.code)] = 3;
|
|
4352
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();
|
|
4353
4520
|
}
|
|
4354
4521
|
function onKeyUp(e)
|
|
4355
4522
|
{
|
|
@@ -4381,7 +4548,9 @@ function inputInit()
|
|
|
4381
4548
|
const mousePosScreenLast = mousePosScreen;
|
|
4382
4549
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
4383
4550
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4384
|
-
|
|
4551
|
+
|
|
4552
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
4553
|
+
e.preventDefault();
|
|
4385
4554
|
}
|
|
4386
4555
|
function onMouseUp(e)
|
|
4387
4556
|
{
|
|
@@ -4405,344 +4574,386 @@ function inputInit()
|
|
|
4405
4574
|
function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
|
|
4406
4575
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
4407
4576
|
function onBlur() { inputClear(); } // reset input when focus is lost
|
|
4408
|
-
}
|
|
4409
|
-
|
|
4410
|
-
// convert a mouse or touch event position to screen space
|
|
4411
|
-
function mouseEventToScreen(mousePos)
|
|
4412
|
-
{
|
|
4413
|
-
const rect = mainCanvas.getBoundingClientRect();
|
|
4414
|
-
const px = percent(mousePos.x, rect.left, rect.right);
|
|
4415
|
-
const py = percent(mousePos.y, rect.top, rect.bottom);
|
|
4416
|
-
return vec2(px*mainCanvas.width, py*mainCanvas.height);
|
|
4417
|
-
}
|
|
4418
|
-
|
|
4419
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
4420
|
-
// Gamepad input
|
|
4421
4577
|
|
|
4422
|
-
//
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
// gamepads are updated by engine every frame automatically
|
|
4426
|
-
function gamepadsUpdate()
|
|
4427
|
-
{
|
|
4428
|
-
const applyDeadZones = (v)=>
|
|
4429
|
-
{
|
|
4430
|
-
const min=.3, max=.8;
|
|
4431
|
-
const deadZone = (v)=>
|
|
4432
|
-
v > min ? percent(v, min, max) :
|
|
4433
|
-
v < -min ? -percent(-v, min, max) : 0;
|
|
4434
|
-
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
4435
|
-
}
|
|
4436
|
-
|
|
4437
|
-
// update touch gamepad if enabled
|
|
4438
|
-
if (touchGamepadEnable && isTouchDevice)
|
|
4578
|
+
// enable touch input mouse passthrough
|
|
4579
|
+
function touchInputInit()
|
|
4439
4580
|
{
|
|
4440
|
-
|
|
4441
|
-
|
|
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 });
|
|
4442
4585
|
|
|
4443
|
-
//
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
if (touchGamepadAnalog)
|
|
4447
|
-
sticks[0] = applyDeadZones(touchGamepadStick);
|
|
4448
|
-
else if (touchGamepadStick.lengthSquared() > .3)
|
|
4586
|
+
// handle all touch events the same way
|
|
4587
|
+
let wasTouching;
|
|
4588
|
+
function handleTouch(e)
|
|
4449
4589
|
{
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
sticks[0].y = -round(touchGamepadStick.y);
|
|
4453
|
-
sticks[0] = sticks[0].clampLength();
|
|
4454
|
-
}
|
|
4590
|
+
if (!touchInputEnable)
|
|
4591
|
+
return;
|
|
4455
4592
|
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
{
|
|
4460
|
-
const wasDown = gamepadIsDown(i,0);
|
|
4461
|
-
data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
4462
|
-
}
|
|
4593
|
+
// route touch to gamepad
|
|
4594
|
+
if (touchGamepadEnable)
|
|
4595
|
+
handleTouchGamepad(e);
|
|
4463
4596
|
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4597
|
+
// fix stalled audio requiring user interaction
|
|
4598
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
4599
|
+
audioContext.resume();
|
|
4467
4600
|
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
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;
|
|
4471
4620
|
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
return;
|
|
4621
|
+
// set was touching
|
|
4622
|
+
wasTouching = touching;
|
|
4475
4623
|
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
{
|
|
4480
|
-
// get or create gamepad data
|
|
4481
|
-
const gamepad = gamepads[i];
|
|
4482
|
-
const data = inputData[i+1] || (inputData[i+1] = []);
|
|
4483
|
-
const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
|
|
4624
|
+
// prevent default handling like copy, magnifier lens, and scrolling
|
|
4625
|
+
if (inputPreventDefault && document.hasFocus() && e.cancelable)
|
|
4626
|
+
e.preventDefault();
|
|
4484
4627
|
|
|
4485
|
-
|
|
4628
|
+
// must return true so the document will get focus
|
|
4629
|
+
return true;
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
// special handling for virtual gamepad mode
|
|
4633
|
+
function handleTouchGamepad(e)
|
|
4486
4634
|
{
|
|
4487
|
-
//
|
|
4488
|
-
|
|
4489
|
-
|
|
4635
|
+
// clear touch gamepad input
|
|
4636
|
+
touchGamepadSticks.length = 0;
|
|
4637
|
+
touchGamepadSticks[0] = vec2();
|
|
4638
|
+
touchGamepadSticks[1] = vec2();
|
|
4639
|
+
touchGamepadButtons.length = 0;
|
|
4640
|
+
isUsingGamepad = true;
|
|
4490
4641
|
|
|
4491
|
-
|
|
4492
|
-
|
|
4642
|
+
const touching = e.touches.length;
|
|
4643
|
+
if (touching)
|
|
4493
4644
|
{
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4645
|
+
touchGamepadTimer.set();
|
|
4646
|
+
if (touchGamepadCenterButton && !wasTouching && paused)
|
|
4647
|
+
{
|
|
4648
|
+
// touch anywhere to press start when paused
|
|
4649
|
+
touchGamepadButtons[9] = 1;
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4500
4652
|
}
|
|
4501
4653
|
|
|
4502
|
-
if
|
|
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)
|
|
4503
4665
|
{
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
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
|
+
}
|
|
4510
4702
|
}
|
|
4511
|
-
|
|
4512
|
-
// disable touch gamepad if using real gamepad
|
|
4513
|
-
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
4514
4703
|
}
|
|
4515
4704
|
}
|
|
4516
|
-
}
|
|
4517
|
-
|
|
4518
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
4519
|
-
|
|
4520
|
-
/** Pulse the vibration hardware if it exists
|
|
4521
|
-
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
4522
|
-
* @memberof Input */
|
|
4523
|
-
function vibrate(pattern=100)
|
|
4524
|
-
{ vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
|
|
4525
4705
|
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
* @memberof Input */
|
|
4535
|
-
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
4536
|
-
|
|
4537
|
-
// touch gamepad internal variables
|
|
4538
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
|
|
4539
|
-
|
|
4540
|
-
function touchGamepadButtonCenter()
|
|
4541
|
-
{
|
|
4542
|
-
// draw right face buttons
|
|
4543
|
-
const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4544
|
-
if (touchGamepadButtonCount <= 2)
|
|
4545
|
-
center.x += touchGamepadSize/2;
|
|
4546
|
-
return center;
|
|
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);
|
|
4713
|
+
}
|
|
4547
4714
|
}
|
|
4548
4715
|
|
|
4549
|
-
|
|
4550
|
-
function touchInputInit()
|
|
4716
|
+
function inputUpdate()
|
|
4551
4717
|
{
|
|
4552
|
-
|
|
4553
|
-
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
4554
|
-
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
4555
|
-
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
4718
|
+
if (headlessMode) return;
|
|
4556
4719
|
|
|
4557
|
-
//
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
{
|
|
4561
|
-
if (!touchInputEnable)
|
|
4562
|
-
return;
|
|
4720
|
+
// clear input when lost focus (prevent stuck keys)
|
|
4721
|
+
if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
4722
|
+
inputClear();
|
|
4563
4723
|
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4724
|
+
// update mouse world space position and delta
|
|
4725
|
+
mousePos = screenToWorld(mousePosScreen);
|
|
4726
|
+
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
4567
4727
|
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4728
|
+
// update gamepads if enabled
|
|
4729
|
+
gamepadsUpdate();
|
|
4730
|
+
|
|
4731
|
+
// gamepads are updated by engine every frame automatically
|
|
4732
|
+
function gamepadsUpdate()
|
|
4733
|
+
{
|
|
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
|
+
}
|
|
4571
4742
|
|
|
4572
|
-
//
|
|
4573
|
-
|
|
4574
|
-
const button = 0; // all touches are left mouse button
|
|
4575
|
-
if (touching)
|
|
4743
|
+
// update touch gamepad if enabled
|
|
4744
|
+
if (touchGamepadEnable && isTouchDevice)
|
|
4576
4745
|
{
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
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)
|
|
4582
4759
|
{
|
|
4583
|
-
|
|
4584
|
-
|
|
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
|
|
4585
4765
|
}
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
else if (wasTouching)
|
|
4590
|
-
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
4766
|
+
const rightTouchStick = touchGamepadSticks[1] ?? vec2();
|
|
4767
|
+
if (touchGamepadButtonCount === 1)
|
|
4768
|
+
sticks[1] = applyDeadZones(rightTouchStick);
|
|
4591
4769
|
|
|
4592
|
-
|
|
4593
|
-
|
|
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
|
+
}
|
|
4594
4777
|
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4778
|
+
// disable normal gamepads when touch gamepad is active
|
|
4779
|
+
return;
|
|
4780
|
+
}
|
|
4598
4781
|
|
|
4599
|
-
//
|
|
4600
|
-
|
|
4601
|
-
|
|
4782
|
+
// return if gamepads are disabled or not supported
|
|
4783
|
+
if (!gamepadsEnable || !navigator || !navigator.getGamepads)
|
|
4784
|
+
return;
|
|
4602
4785
|
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
// clear touch gamepad input
|
|
4607
|
-
touchGamepadStick = vec2();
|
|
4608
|
-
touchGamepadButtons = [];
|
|
4609
|
-
isUsingGamepad = true;
|
|
4786
|
+
// only poll gamepads when focused or in debug mode
|
|
4787
|
+
if (!debug && !document.hasFocus())
|
|
4788
|
+
return;
|
|
4610
4789
|
|
|
4611
|
-
|
|
4612
|
-
|
|
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)
|
|
4613
4795
|
{
|
|
4614
|
-
|
|
4615
|
-
|
|
4796
|
+
// get or create gamepad data
|
|
4797
|
+
const gamepad = gamepads[i];
|
|
4798
|
+
if (!gamepad)
|
|
4616
4799
|
{
|
|
4617
|
-
//
|
|
4618
|
-
|
|
4619
|
-
|
|
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;
|
|
4620
4806
|
}
|
|
4621
|
-
}
|
|
4622
4807
|
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
const startCenter = mainCanvasSize.scale(.5);
|
|
4808
|
+
const data = inputData[i+1] ?? (inputData[i+1] = []);
|
|
4809
|
+
const sticks = gamepadStickData[i] ?? (gamepadStickData[i] = []);
|
|
4810
|
+
const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
|
|
4627
4811
|
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4812
|
+
// read analog sticks
|
|
4813
|
+
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
4814
|
+
sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
|
|
4815
|
+
|
|
4816
|
+
// read buttons
|
|
4817
|
+
let hadInput = false;
|
|
4818
|
+
for (let j = gamepad.buttons.length; j--;)
|
|
4819
|
+
{
|
|
4820
|
+
const button = gamepad.buttons[j];
|
|
4821
|
+
const wasDown = gamepadIsDown(j,i);
|
|
4822
|
+
data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
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)
|
|
4633
4832
|
{
|
|
4634
|
-
|
|
4635
|
-
|
|
4833
|
+
gamepadHadInput[i] = true;
|
|
4834
|
+
if (!gamepadHadInput[gamepadPrimary])
|
|
4835
|
+
gamepadPrimary = i;
|
|
4836
|
+
isUsingGamepad ||= (gamepadPrimary === i);
|
|
4636
4837
|
}
|
|
4637
|
-
|
|
4838
|
+
|
|
4839
|
+
if (gamepad.mapping === 'standard')
|
|
4638
4840
|
{
|
|
4639
|
-
//
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
button = 0;
|
|
4644
|
-
else if (touchGamepadButtonCount === 2)
|
|
4645
|
-
{
|
|
4646
|
-
const delta = buttonCenter.subtract(touchPos);
|
|
4647
|
-
button = -delta.x < delta.y ? 1 : 0;
|
|
4648
|
-
}
|
|
4649
|
-
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4650
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
4651
|
-
if (button < touchGamepadButtonCount)
|
|
4652
|
-
touchGamepadButtons[button] = 1;
|
|
4841
|
+
// get dpad buttons (standard mapping)
|
|
4842
|
+
dpad.set(
|
|
4843
|
+
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
4844
|
+
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
4653
4845
|
}
|
|
4654
|
-
else if (
|
|
4655
|
-
startCenter.distance(touchPos) < touchGamepadSize)
|
|
4846
|
+
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
4656
4847
|
{
|
|
4657
|
-
//
|
|
4658
|
-
|
|
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);
|
|
4659
4852
|
}
|
|
4853
|
+
|
|
4854
|
+
// copy dpad to left analog stick when pressed
|
|
4855
|
+
if (gamepadDirectionEmulateStick && !dpad.isZero())
|
|
4856
|
+
sticks[0] = dpad.clampLength();
|
|
4660
4857
|
}
|
|
4858
|
+
|
|
4859
|
+
// disable touch gamepad if using real gamepad
|
|
4860
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
4661
4861
|
}
|
|
4662
4862
|
}
|
|
4663
4863
|
|
|
4664
|
-
|
|
4665
|
-
function touchGamepadRender()
|
|
4864
|
+
function inputUpdatePost()
|
|
4666
4865
|
{
|
|
4667
|
-
if (
|
|
4668
|
-
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
4669
|
-
return;
|
|
4866
|
+
if (headlessMode) return;
|
|
4670
4867
|
|
|
4671
|
-
//
|
|
4672
|
-
const
|
|
4673
|
-
|
|
4674
|
-
|
|
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();
|
|
4875
|
+
}
|
|
4675
4876
|
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
4680
|
-
context.strokeStyle = '#fff';
|
|
4681
|
-
context.lineWidth = 3;
|
|
4877
|
+
function inputRender()
|
|
4878
|
+
{
|
|
4879
|
+
touchGamepadRender();
|
|
4682
4880
|
|
|
4683
|
-
|
|
4684
|
-
context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
4685
|
-
context.beginPath();
|
|
4686
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4687
|
-
if (touchGamepadAnalog) // draw circle shaped gamepad
|
|
4881
|
+
function touchGamepadRender()
|
|
4688
4882
|
{
|
|
4689
|
-
|
|
4883
|
+
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
4884
|
+
if (!touchGamepadEnable || !touchGamepadTimer.isSet())
|
|
4885
|
+
return;
|
|
4886
|
+
|
|
4887
|
+
// fade off when not touching or paused
|
|
4888
|
+
const alpha = percent(touchGamepadTimer.get(), 4, 3);
|
|
4889
|
+
if (!alpha || paused)
|
|
4890
|
+
return;
|
|
4891
|
+
|
|
4892
|
+
// setup the canvas
|
|
4893
|
+
const context = overlayContext;
|
|
4894
|
+
context.save();
|
|
4895
|
+
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
4896
|
+
context.strokeStyle = '#fff';
|
|
4897
|
+
context.lineWidth = 3;
|
|
4898
|
+
|
|
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)
|
|
4905
|
+
{
|
|
4906
|
+
// draw circle shaped gamepad
|
|
4907
|
+
context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
|
|
4908
|
+
}
|
|
4909
|
+
else
|
|
4910
|
+
{
|
|
4911
|
+
// draw cross shaped gamepad
|
|
4912
|
+
for (let i=10; --i;)
|
|
4913
|
+
{
|
|
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);
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4690
4919
|
context.fill();
|
|
4691
4920
|
context.stroke();
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
{
|
|
4695
|
-
for (let i=10; i--;)
|
|
4921
|
+
|
|
4922
|
+
// draw right face buttons
|
|
4696
4923
|
{
|
|
4697
|
-
const
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
i
|
|
4924
|
+
const buttonCenter = touchGamepadButtonCenter();
|
|
4925
|
+
const buttonSize = touchGamepadButtonCount > 1 ?
|
|
4926
|
+
touchGamepadSize/4 : touchGamepadSize/2;
|
|
4927
|
+
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
4928
|
+
{
|
|
4929
|
+
const j = mod(i-1, 4);
|
|
4930
|
+
let button = touchGamepadButtonCount > 2 ?
|
|
4931
|
+
j : min(j, touchGamepadButtonCount-1);
|
|
4932
|
+
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4933
|
+
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
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();
|
|
4941
|
+
}
|
|
4701
4942
|
}
|
|
4702
|
-
context.stroke();
|
|
4703
|
-
}
|
|
4704
4943
|
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
|
|
4708
|
-
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
4709
|
-
{
|
|
4710
|
-
const j = mod(i-1, 4);
|
|
4711
|
-
let button = touchGamepadButtonCount > 2 ?
|
|
4712
|
-
j : min(j, touchGamepadButtonCount-1);
|
|
4713
|
-
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
4714
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
4715
|
-
const pos = buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
4716
|
-
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
4717
|
-
context.beginPath();
|
|
4718
|
-
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
4719
|
-
context.fill();
|
|
4720
|
-
context.stroke();
|
|
4944
|
+
// set canvas back to normal
|
|
4945
|
+
context.restore();
|
|
4721
4946
|
}
|
|
4722
|
-
|
|
4723
|
-
// set canvas back to normal
|
|
4724
|
-
context.restore();
|
|
4725
4947
|
}
|
|
4726
4948
|
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
/** Request to lock the pointer, does not work on touch devices
|
|
4731
|
-
* @memberof Input */
|
|
4732
|
-
function pointerLockRequest()
|
|
4949
|
+
// center position for right tocuh pad face buttons
|
|
4950
|
+
function touchGamepadButtonCenter()
|
|
4733
4951
|
{
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
* @memberof Input */
|
|
4740
|
-
function pointerLockExit() { document.exitPointerLock && document.exitPointerLock(); }
|
|
4741
|
-
|
|
4742
|
-
/** Check if pointer is locked (true if locked)
|
|
4743
|
-
* @return {boolean}
|
|
4744
|
-
* @memberof Input */
|
|
4745
|
-
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
|
+
}
|
|
4746
4957
|
/**
|
|
4747
4958
|
* LittleJS Audio System
|
|
4748
4959
|
* - <a href=https://killedbyapixel.github.io/ZzFX/>ZzFX Sound Effects</a> - ZzFX Sound Effect Generator
|
|
@@ -4856,7 +5067,6 @@ class Sound
|
|
|
4856
5067
|
ASSERT(isNumber(pitch), 'pitch must be a number');
|
|
4857
5068
|
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
4858
5069
|
|
|
4859
|
-
|
|
4860
5070
|
if (!soundEnable || headlessMode) return;
|
|
4861
5071
|
if (!this.sampleChannels) return;
|
|
4862
5072
|
|
|
@@ -5483,7 +5693,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
|
|
|
5483
5693
|
}
|
|
5484
5694
|
}
|
|
5485
5695
|
|
|
5486
|
-
/** Return the exact position of the
|
|
5696
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
5487
5697
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
5488
5698
|
* @param {Vector2} posStart
|
|
5489
5699
|
* @param {Vector2} posEnd
|
|
@@ -5989,7 +6199,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
5989
6199
|
// remove from collision layers array and destroy
|
|
5990
6200
|
const index = tileCollisionLayers.indexOf(this);
|
|
5991
6201
|
ASSERT(index >= 0, 'tile collision layer not found in array');
|
|
5992
|
-
tileCollisionLayers.splice(index, 1);
|
|
6202
|
+
index >= 0 && tileCollisionLayers.splice(index, 1);
|
|
5993
6203
|
super.destroy();
|
|
5994
6204
|
}
|
|
5995
6205
|
|
|
@@ -6057,7 +6267,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6057
6267
|
return false;
|
|
6058
6268
|
}
|
|
6059
6269
|
|
|
6060
|
-
/** Return the exact position of the
|
|
6270
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6061
6271
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6062
6272
|
* @param {Vector2} posStart
|
|
6063
6273
|
* @param {Vector2} posEnd
|
|
@@ -8002,11 +8212,14 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8002
8212
|
/**
|
|
8003
8213
|
* LittleJS User Interface Plugin
|
|
8004
8214
|
* - call new UISystemPlugin() to setup the UI system
|
|
8215
|
+
* - Gamepad and keyboard navigation support
|
|
8005
8216
|
* - Nested Menus
|
|
8006
8217
|
* - Text
|
|
8007
8218
|
* - Buttons
|
|
8008
8219
|
* - Checkboxes
|
|
8009
8220
|
* - Images
|
|
8221
|
+
* - Scrollbars
|
|
8222
|
+
* - Video
|
|
8010
8223
|
* @namespace UISystem
|
|
8011
8224
|
*/
|
|
8012
8225
|
|
|
@@ -8017,6 +8230,20 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
|
8017
8230
|
* @memberof UISystem */
|
|
8018
8231
|
let uiSystem;
|
|
8019
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
|
+
|
|
8020
8247
|
///////////////////////////////////////////////////////////////////////////////
|
|
8021
8248
|
/**
|
|
8022
8249
|
* UI System Global Object
|
|
@@ -8035,6 +8262,7 @@ class UISystemPlugin
|
|
|
8035
8262
|
ASSERT(!uiSystem, 'UI system already initialized');
|
|
8036
8263
|
uiSystem = this;
|
|
8037
8264
|
|
|
8265
|
+
// default settings
|
|
8038
8266
|
/** @property {Color} - Default fill color for UI elements */
|
|
8039
8267
|
this.defaultColor = WHITE;
|
|
8040
8268
|
/** @property {Color} - Default outline color for UI elements */
|
|
@@ -8054,7 +8282,7 @@ class UISystemPlugin
|
|
|
8054
8282
|
/** @property {number} - Default rounded rect corner radius for UI elements */
|
|
8055
8283
|
this.defaultCornerRadius = 0;
|
|
8056
8284
|
/** @property {number} - Default scale to use for fitting text to object */
|
|
8057
|
-
this.
|
|
8285
|
+
this.defaultTextFitScale = .8;
|
|
8058
8286
|
/** @property {string} - Default font for UI elements */
|
|
8059
8287
|
this.defaultFont = fontDefault;
|
|
8060
8288
|
/** @property {Sound} - Default sound when interactive UI element is pressed */
|
|
@@ -8063,6 +8291,30 @@ class UISystemPlugin
|
|
|
8063
8291
|
this.defaultSoundRelease = undefined;
|
|
8064
8292
|
/** @property {Sound} - Default sound when interactive UI element is clicked */
|
|
8065
8293
|
this.defaultSoundClick = undefined;
|
|
8294
|
+
/** @property {Color} - Color for shadow */
|
|
8295
|
+
this.defaultShadowColor = CLEAR_BLACK;
|
|
8296
|
+
/** @property {number} - Size of shadow blur */
|
|
8297
|
+
this.defaultShadowBlur = 5;
|
|
8298
|
+
/** @property {Vector2} - Offset of shadow blur */
|
|
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
|
+
|
|
8317
|
+
// system state
|
|
8066
8318
|
/** @property {Array<UIObject>} - List of all UI elements */
|
|
8067
8319
|
this.uiObjects = [];
|
|
8068
8320
|
/** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
|
|
@@ -8073,50 +8325,118 @@ class UISystemPlugin
|
|
|
8073
8325
|
this.hoverObject = undefined;
|
|
8074
8326
|
/** @property {UIObject} - Hover object at start of update */
|
|
8075
8327
|
this.lastHoverObject = undefined;
|
|
8076
|
-
/** @property {
|
|
8077
|
-
this.
|
|
8328
|
+
/** @property {UIObject} - Current confirm menu being shown */
|
|
8329
|
+
this.confirmDialog = undefined;
|
|
8078
8330
|
|
|
8079
8331
|
engineAddPlugin(uiUpdate, uiRender);
|
|
8080
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
|
+
|
|
8081
8341
|
// setup recursive update and render
|
|
8082
8342
|
// update in reverse order to detect mouse enter/leave
|
|
8083
8343
|
function uiUpdate()
|
|
8084
8344
|
{
|
|
8085
|
-
|
|
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))
|
|
8086
8353
|
{
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
updateInvisibleObject(c);
|
|
8090
|
-
o.updateInvisible();
|
|
8354
|
+
uiSystem.navigationMode = false;
|
|
8355
|
+
uiSystem.navigationObject = undefined;
|
|
8091
8356
|
}
|
|
8092
|
-
|
|
8357
|
+
|
|
8358
|
+
// navigation with gamepad/keyboard
|
|
8359
|
+
const navigableObjects = uiSystem.getNavigableObjects();
|
|
8360
|
+
if (!navigableObjects.length)
|
|
8361
|
+
uiSystem.navigationObject = undefined;
|
|
8362
|
+
else
|
|
8093
8363
|
{
|
|
8094
|
-
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)
|
|
8095
8370
|
{
|
|
8096
|
-
//
|
|
8097
|
-
|
|
8098
|
-
o.pos = o.localPos.add(o.parent.pos);
|
|
8099
|
-
// update in reverse order to detect mouse enter/leave
|
|
8100
|
-
for (let i=o.children.length; i--;)
|
|
8101
|
-
updateObject(o.children[i]);
|
|
8102
|
-
o.update();
|
|
8371
|
+
// select first auto focus object
|
|
8372
|
+
uiSystem.navigationObject = navigableObjects.find(o=>o.navigationAutoSelect);
|
|
8103
8373
|
}
|
|
8104
|
-
|
|
8105
|
-
|
|
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();
|
|
8106
8418
|
}
|
|
8107
|
-
// reset hover object at start of update
|
|
8108
|
-
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8109
|
-
uiSystem.hoverObject = undefined;
|
|
8110
8419
|
|
|
8111
8420
|
// update in reverse order so topmost objects get priority
|
|
8112
8421
|
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8113
8422
|
{
|
|
8114
8423
|
const o = uiSystem.uiObjects[i];
|
|
8115
|
-
o.parent || updateObject(o)
|
|
8424
|
+
o.parent || updateObject(o);
|
|
8116
8425
|
}
|
|
8117
8426
|
|
|
8118
8427
|
// remove destroyed objects
|
|
8119
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
|
+
}
|
|
8120
8440
|
}
|
|
8121
8441
|
function uiRender()
|
|
8122
8442
|
{
|
|
@@ -8133,15 +8453,29 @@ class UISystemPlugin
|
|
|
8133
8453
|
|
|
8134
8454
|
function renderObject(o)
|
|
8135
8455
|
{
|
|
8136
|
-
if (!o.visible)
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8456
|
+
if (!o.visible) return;
|
|
8457
|
+
|
|
8458
|
+
// render object and children
|
|
8459
|
+
updateTransforms(o);
|
|
8140
8460
|
o.render();
|
|
8141
8461
|
for (const c of o.children)
|
|
8142
8462
|
renderObject(c);
|
|
8143
8463
|
}
|
|
8144
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
|
+
}
|
|
8145
8479
|
context.restore();
|
|
8146
8480
|
}
|
|
8147
8481
|
}
|
|
@@ -8153,8 +8487,11 @@ class UISystemPlugin
|
|
|
8153
8487
|
* @param {number} [lineWidth]
|
|
8154
8488
|
* @param {Color} [lineColor]
|
|
8155
8489
|
* @param {number} [cornerRadius]
|
|
8156
|
-
* @param {Color} [gradientColor]
|
|
8157
|
-
|
|
8490
|
+
* @param {Color} [gradientColor]
|
|
8491
|
+
* @param {Color} [shadowColor]
|
|
8492
|
+
* @param {number} [shadowBlur]
|
|
8493
|
+
* @param {Color} [shadowOffset] */
|
|
8494
|
+
drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
|
|
8158
8495
|
{
|
|
8159
8496
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
8160
8497
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -8176,13 +8513,23 @@ class UISystemPlugin
|
|
|
8176
8513
|
}
|
|
8177
8514
|
else
|
|
8178
8515
|
context.fillStyle = color.toString();
|
|
8516
|
+
if (shadowBlur || shadowOffset.x || shadowOffset.y)
|
|
8517
|
+
if (shadowColor.a > 0)
|
|
8518
|
+
{
|
|
8519
|
+
// setup shadow
|
|
8520
|
+
context.shadowColor = shadowColor.toString();
|
|
8521
|
+
context.shadowBlur = shadowBlur;
|
|
8522
|
+
context.shadowOffsetX = shadowOffset.x;
|
|
8523
|
+
context.shadowOffsetY = shadowOffset.y;
|
|
8524
|
+
}
|
|
8179
8525
|
context.beginPath();
|
|
8180
8526
|
if (cornerRadius && context['roundRect'])
|
|
8181
8527
|
context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
|
|
8182
8528
|
else
|
|
8183
8529
|
context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
|
|
8184
8530
|
context.fill();
|
|
8185
|
-
|
|
8531
|
+
context.shadowColor = '#0000';
|
|
8532
|
+
if (lineWidth && lineColor.a > 0)
|
|
8186
8533
|
{
|
|
8187
8534
|
context.strokeStyle = lineColor.toString();
|
|
8188
8535
|
context.lineWidth = lineWidth;
|
|
@@ -8217,10 +8564,24 @@ class UISystemPlugin
|
|
|
8217
8564
|
* @param {TileInfo} tileInfo
|
|
8218
8565
|
* @param {Color} [color=uiSystem.defaultColor]
|
|
8219
8566
|
* @param {number} [angle]
|
|
8220
|
-
* @param {boolean} [mirror]
|
|
8221
|
-
|
|
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())
|
|
8222
8572
|
{
|
|
8223
|
-
|
|
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';
|
|
8224
8585
|
}
|
|
8225
8586
|
|
|
8226
8587
|
/** Draw text to the UI context
|
|
@@ -8235,12 +8596,27 @@ class UISystemPlugin
|
|
|
8235
8596
|
* @param {string} [fontStyle]
|
|
8236
8597
|
* @param {boolean} [applyMaxWidth=true]
|
|
8237
8598
|
* @param {Vector2} [textShadow]
|
|
8238
|
-
|
|
8239
|
-
|
|
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())
|
|
8240
8603
|
{
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
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';
|
|
8244
8620
|
}
|
|
8245
8621
|
|
|
8246
8622
|
/**
|
|
@@ -8254,7 +8630,7 @@ class UISystemPlugin
|
|
|
8254
8630
|
* @param {DragAndDropCallback} [onDrop] - when a file is dropped
|
|
8255
8631
|
* @param {DragAndDropCallback} [onDragEnter] - when a file is dragged onto the window
|
|
8256
8632
|
* @param {DragAndDropCallback} [onDragLeave] - when a file is dragged off the window
|
|
8257
|
-
* @param {DragAndDropCallback} [onDragOver] -
|
|
8633
|
+
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
8258
8634
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
8259
8635
|
{
|
|
8260
8636
|
function setCallback(callback, listenerType)
|
|
@@ -8270,8 +8646,7 @@ class UISystemPlugin
|
|
|
8270
8646
|
|
|
8271
8647
|
/** Convert a screen space position to native UI position
|
|
8272
8648
|
* @param {Vector2} pos
|
|
8273
|
-
* @return {Vector2}
|
|
8274
|
-
*/
|
|
8649
|
+
* @return {Vector2} */
|
|
8275
8650
|
screenToNative(pos)
|
|
8276
8651
|
{
|
|
8277
8652
|
if (!uiSystem.nativeHeight)
|
|
@@ -8294,6 +8669,160 @@ class UISystemPlugin
|
|
|
8294
8669
|
for (const o of this.uiObjects)
|
|
8295
8670
|
o.parent || o.destroy();
|
|
8296
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
|
+
}
|
|
8297
8826
|
}
|
|
8298
8827
|
}
|
|
8299
8828
|
|
|
@@ -8349,7 +8878,13 @@ class UIObject
|
|
|
8349
8878
|
/** @property {number} - Override for text height */
|
|
8350
8879
|
this.textHeight = undefined;
|
|
8351
8880
|
/** @property {number} - Scale text to fit in the object */
|
|
8352
|
-
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;
|
|
8353
8888
|
/** @property {boolean} - Should this object be drawn */
|
|
8354
8889
|
this.visible = true;
|
|
8355
8890
|
/** @property {Array<UIObject>} - A list of this object's children */
|
|
@@ -8370,15 +8905,22 @@ class UIObject
|
|
|
8370
8905
|
this.dragActivate = false;
|
|
8371
8906
|
/** @property {boolean} - True if this can be a hover object */
|
|
8372
8907
|
this.canBeHover = true;
|
|
8373
|
-
|
|
8908
|
+
/** @property {Color} - Color for shadow, undefined if no shadow */
|
|
8909
|
+
this.shadowColor = uiSystem.defaultShadowColor?.copy();
|
|
8910
|
+
/** @property {number} - Size of shadow blur */
|
|
8911
|
+
this.shadowBlur = uiSystem.defaultShadowBlur;
|
|
8912
|
+
/** @property {Vector2} - Offset of shadow blur */
|
|
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;
|
|
8374
8918
|
|
|
8375
|
-
|
|
8376
|
-
this.textShadow = undefined;
|
|
8919
|
+
uiSystem.uiObjects.push(this);
|
|
8377
8920
|
}
|
|
8378
8921
|
|
|
8379
8922
|
/** Add a child UIObject to this object
|
|
8380
|
-
* @param {UIObject} child
|
|
8381
|
-
*/
|
|
8923
|
+
* @param {UIObject} child */
|
|
8382
8924
|
addChild(child)
|
|
8383
8925
|
{
|
|
8384
8926
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
@@ -8387,8 +8929,7 @@ class UIObject
|
|
|
8387
8929
|
}
|
|
8388
8930
|
|
|
8389
8931
|
/** Remove a child UIObject from this object
|
|
8390
|
-
* @param {UIObject} child
|
|
8391
|
-
*/
|
|
8932
|
+
* @param {UIObject} child */
|
|
8392
8933
|
removeChild(child)
|
|
8393
8934
|
{
|
|
8394
8935
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
@@ -8396,7 +8937,6 @@ class UIObject
|
|
|
8396
8937
|
child.parent = undefined;
|
|
8397
8938
|
}
|
|
8398
8939
|
|
|
8399
|
-
|
|
8400
8940
|
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
8401
8941
|
destroy()
|
|
8402
8942
|
{
|
|
@@ -8412,9 +8952,9 @@ class UIObject
|
|
|
8412
8952
|
child.destroy();
|
|
8413
8953
|
}
|
|
8414
8954
|
}
|
|
8955
|
+
|
|
8415
8956
|
/** Check if the mouse is overlapping a box in screen space
|
|
8416
|
-
* @return {boolean} - True if overlapping
|
|
8417
|
-
*/
|
|
8957
|
+
* @return {boolean} - True if overlapping */
|
|
8418
8958
|
isMouseOverlapping()
|
|
8419
8959
|
{
|
|
8420
8960
|
if (!mouseInWindow) return false;
|
|
@@ -8431,11 +8971,16 @@ class UIObject
|
|
|
8431
8971
|
// call the custom update callback
|
|
8432
8972
|
this.onUpdate();
|
|
8433
8973
|
|
|
8974
|
+
// unset active if disabled
|
|
8975
|
+
if (this.disabled && this == uiSystem.activeObject)
|
|
8976
|
+
uiSystem.activeObject = undefined;
|
|
8977
|
+
|
|
8434
8978
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
8435
8979
|
const isActive = this.isActiveObject();
|
|
8436
8980
|
const mouseDown = mouseIsDown(0);
|
|
8437
8981
|
const mousePress = this.dragActivate ? mouseDown : mouseWasPressed(0);
|
|
8438
8982
|
if (this.canBeHover)
|
|
8983
|
+
if (!uiSystem.navigationMode) // no mouse hover in navigation mode
|
|
8439
8984
|
if (mousePress || isActive || (!mouseDown && !isTouchDevice))
|
|
8440
8985
|
if (!uiSystem.hoverObject && this.isMouseOverlapping())
|
|
8441
8986
|
uiSystem.hoverObject = this;
|
|
@@ -8449,8 +8994,7 @@ class UIObject
|
|
|
8449
8994
|
{
|
|
8450
8995
|
if (!this.dragActivate || (!wasHover || mouseWasPressed(0)))
|
|
8451
8996
|
this.onPress();
|
|
8452
|
-
|
|
8453
|
-
this.soundPress.play();
|
|
8997
|
+
this.soundPress && this.soundPress.play();
|
|
8454
8998
|
if (uiSystem.activeObject && !isActive)
|
|
8455
8999
|
uiSystem.activeObject.onRelease();
|
|
8456
9000
|
uiSystem.activeObject = this;
|
|
@@ -8459,10 +9003,10 @@ class UIObject
|
|
|
8459
9003
|
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
8460
9004
|
{
|
|
8461
9005
|
this.onClick();
|
|
8462
|
-
|
|
8463
|
-
this.soundClick.play();
|
|
9006
|
+
this.soundClick && this.soundClick.play();
|
|
8464
9007
|
}
|
|
8465
9008
|
}
|
|
9009
|
+
|
|
8466
9010
|
// clear mouse was pressed state even when disabled
|
|
8467
9011
|
mousePress && inputClearKey(0,0,0,1,0);
|
|
8468
9012
|
}
|
|
@@ -8470,8 +9014,7 @@ class UIObject
|
|
|
8470
9014
|
if (!mouseDown || (this.dragActivate && !this.isHoverObject()))
|
|
8471
9015
|
{
|
|
8472
9016
|
this.onRelease();
|
|
8473
|
-
|
|
8474
|
-
this.soundRelease.play();
|
|
9017
|
+
this.soundRelease && this.soundRelease.play();
|
|
8475
9018
|
uiSystem.activeObject = undefined;
|
|
8476
9019
|
}
|
|
8477
9020
|
|
|
@@ -8483,29 +9026,40 @@ class UIObject
|
|
|
8483
9026
|
/** Render the object, called automatically by plugin once each frame */
|
|
8484
9027
|
render()
|
|
8485
9028
|
{
|
|
8486
|
-
|
|
9029
|
+
// call the custom render callback
|
|
9030
|
+
this.onRender();
|
|
8487
9031
|
|
|
8488
|
-
|
|
8489
|
-
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
8490
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
|
|
8491
|
-
}
|
|
9032
|
+
if (!this.size.x || !this.size.y) return;
|
|
8492
9033
|
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
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);
|
|
8499
9047
|
}
|
|
8500
9048
|
|
|
8501
9049
|
/** Get the size for text with overrides and scale
|
|
8502
|
-
* @return {Vector2}
|
|
8503
|
-
*/
|
|
9050
|
+
* @return {Vector2} */
|
|
8504
9051
|
getTextSize()
|
|
8505
9052
|
{
|
|
8506
9053
|
return vec2(
|
|
8507
|
-
this.textWidth || this.
|
|
8508
|
-
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();
|
|
8509
9063
|
}
|
|
8510
9064
|
|
|
8511
9065
|
/** @return {boolean} - Is the mouse hovering over this element */
|
|
@@ -8514,9 +9068,51 @@ class UIObject
|
|
|
8514
9068
|
/** @return {boolean} - Is the mouse held onto this element */
|
|
8515
9069
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
8516
9070
|
|
|
8517
|
-
/**
|
|
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 */
|
|
8518
9111
|
onUpdate() {}
|
|
8519
9112
|
|
|
9113
|
+
/** Called each frame before object renders */
|
|
9114
|
+
onRender() {}
|
|
9115
|
+
|
|
8520
9116
|
/** Called when the mouse enters the object */
|
|
8521
9117
|
onEnter() {}
|
|
8522
9118
|
|
|
@@ -8564,16 +9160,25 @@ class UIText extends UIObject
|
|
|
8564
9160
|
this.align = align;
|
|
8565
9161
|
this.font = font;
|
|
8566
9162
|
|
|
8567
|
-
// make text not outlined by default
|
|
8568
|
-
this.lineWidth = 0;
|
|
8569
9163
|
// text can not be a hover object by default
|
|
8570
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;
|
|
8571
9174
|
}
|
|
8572
9175
|
render()
|
|
8573
9176
|
{
|
|
8574
|
-
|
|
9177
|
+
super.render();
|
|
9178
|
+
|
|
9179
|
+
// render the text
|
|
8575
9180
|
const textSize = this.getTextSize();
|
|
8576
|
-
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);
|
|
8577
9182
|
}
|
|
8578
9183
|
}
|
|
8579
9184
|
|
|
@@ -8609,10 +9214,13 @@ class UITile extends UIObject
|
|
|
8609
9214
|
this.mirror = mirror;
|
|
8610
9215
|
// set properties
|
|
8611
9216
|
this.color = color.copy();
|
|
9217
|
+
|
|
9218
|
+
// no shadow by default
|
|
9219
|
+
this.shadowColor = CLEAR_BLACK;
|
|
8612
9220
|
}
|
|
8613
9221
|
render()
|
|
8614
9222
|
{
|
|
8615
|
-
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);
|
|
8616
9224
|
}
|
|
8617
9225
|
}
|
|
8618
9226
|
|
|
@@ -8637,6 +9245,9 @@ class UIButton extends UIObject
|
|
|
8637
9245
|
ASSERT(isString(text), 'ui button must be a string');
|
|
8638
9246
|
ASSERT(isColor(color), 'ui button color must be a color');
|
|
8639
9247
|
|
|
9248
|
+
/** @property {Vector2} - Text offset for the button */
|
|
9249
|
+
this.textOffset = vec2();
|
|
9250
|
+
|
|
8640
9251
|
// set properties
|
|
8641
9252
|
this.text = text;
|
|
8642
9253
|
this.color = color.copy();
|
|
@@ -8648,8 +9259,8 @@ class UIButton extends UIObject
|
|
|
8648
9259
|
|
|
8649
9260
|
// draw the text scaled to fit
|
|
8650
9261
|
const textSize = this.getTextSize();
|
|
8651
|
-
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8652
|
-
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);
|
|
8653
9264
|
}
|
|
8654
9265
|
}
|
|
8655
9266
|
|
|
@@ -8703,7 +9314,7 @@ class UICheckbox extends UIObject
|
|
|
8703
9314
|
const textSize = this.getTextSize();
|
|
8704
9315
|
const pos = this.pos.add(vec2(this.size.x,0));
|
|
8705
9316
|
uiSystem.drawText(this.text, pos, textSize,
|
|
8706
|
-
this.textColor,
|
|
9317
|
+
this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
8707
9318
|
}
|
|
8708
9319
|
}
|
|
8709
9320
|
|
|
@@ -8745,7 +9356,11 @@ class UIScrollbar extends UIObject
|
|
|
8745
9356
|
update()
|
|
8746
9357
|
{
|
|
8747
9358
|
super.update();
|
|
8748
|
-
if (this.
|
|
9359
|
+
if (!this.interactive)
|
|
9360
|
+
return;
|
|
9361
|
+
|
|
9362
|
+
const oldValue = this.value;
|
|
9363
|
+
if (this.isActiveObject())
|
|
8749
9364
|
{
|
|
8750
9365
|
// handle horizontal or vertical scrollbar
|
|
8751
9366
|
const isHorizontal = this.size.x > this.size.y;
|
|
@@ -8757,14 +9372,19 @@ class UIScrollbar extends UIObject
|
|
|
8757
9372
|
const handleWidth = barSize - handleSize;
|
|
8758
9373
|
const p1 = centerPos - handleWidth/2;
|
|
8759
9374
|
const p2 = centerPos + handleWidth/2;
|
|
8760
|
-
const oldValue = this.value;
|
|
8761
|
-
|
|
8762
9375
|
const p = uiSystem.screenToNative(mousePosScreen);
|
|
8763
9376
|
this.value = isHorizontal ?
|
|
8764
9377
|
percent(p.x, p1, p2) :
|
|
8765
9378
|
percent(p.y, p2, p1);
|
|
8766
|
-
this.value === oldValue || this.onChange();
|
|
8767
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();
|
|
8768
9388
|
}
|
|
8769
9389
|
render()
|
|
8770
9390
|
{
|
|
@@ -8789,7 +9409,14 @@ class UIScrollbar extends UIObject
|
|
|
8789
9409
|
// draw the text scaled to fit on the scrollbar
|
|
8790
9410
|
const textSize = this.getTextSize();
|
|
8791
9411
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8792
|
-
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();
|
|
8793
9420
|
}
|
|
8794
9421
|
}
|
|
8795
9422
|
|
|
@@ -8823,7 +9450,7 @@ class UIVideo extends UIObject
|
|
|
8823
9450
|
this.color = BLACK; // default to black background
|
|
8824
9451
|
this.cornerRadius = 0; // default to no corner radius
|
|
8825
9452
|
|
|
8826
|
-
/** @property {
|
|
9453
|
+
/** @property {number} - The video volume */
|
|
8827
9454
|
this.volume = volume;
|
|
8828
9455
|
|
|
8829
9456
|
// create video element
|
|
@@ -8856,7 +9483,7 @@ class UIVideo extends UIObject
|
|
|
8856
9483
|
|
|
8857
9484
|
/** Check if video is currently loading
|
|
8858
9485
|
* @return {boolean} */
|
|
8859
|
-
|
|
9486
|
+
isLoading()
|
|
8860
9487
|
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
8861
9488
|
|
|
8862
9489
|
/** Check if video is currently paused
|
|
@@ -8866,7 +9493,7 @@ class UIVideo extends UIObject
|
|
|
8866
9493
|
/** Check if video is currently playing
|
|
8867
9494
|
* @return {boolean} */
|
|
8868
9495
|
isPlaying()
|
|
8869
|
-
{ return !this.isPaused() && !this.hasEnded() && !this.
|
|
9496
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoading(); }
|
|
8870
9497
|
|
|
8871
9498
|
/** Check if video has ended playing
|
|
8872
9499
|
* @return {boolean} */
|
|
@@ -8915,7 +9542,7 @@ class UIVideo extends UIObject
|
|
|
8915
9542
|
{
|
|
8916
9543
|
super.render();
|
|
8917
9544
|
|
|
8918
|
-
if (this.
|
|
9545
|
+
if (this.isLoading())
|
|
8919
9546
|
return;
|
|
8920
9547
|
const context = uiSystem.uiContext;
|
|
8921
9548
|
const s = this.size;
|
|
@@ -10699,7 +11326,7 @@ class Box2dPlugin
|
|
|
10699
11326
|
* @param {Vector2} v */
|
|
10700
11327
|
vec2dTo(v)
|
|
10701
11328
|
{
|
|
10702
|
-
ASSERT(v
|
|
11329
|
+
ASSERT(isVector2(v));
|
|
10703
11330
|
return new box2d.instance.b2Vec2(v.x, v.y);
|
|
10704
11331
|
}
|
|
10705
11332
|
|