littlejsengine 1.18.15 → 1.18.18
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/README.md +4 -4
- package/dist/littlejs.d.ts +277 -32
- package/dist/littlejs.esm.js +1395 -304
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1367 -303
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1341 -287
- package/package.json +1 -1
- package/plugins/box2d.js +9 -13
- package/plugins/drawUtilities.js +84 -10
- package/plugins/lightSystem.js +330 -0
- package/plugins/medalSystem.js +36 -5
- package/plugins/pluginExport.js +8 -0
- package/plugins/uiSystem.js +35 -25
- package/src/engine.js +1 -1
- package/src/engineAudio.js +21 -3
- package/src/engineBuild.mjs +1 -0
- package/src/engineDebug.js +27 -16
- package/src/engineDraw.js +68 -10
- package/src/engineExport.js +20 -1
- package/src/engineInput.js +597 -191
- package/src/engineMath.js +15 -0
- package/src/engineObject.js +5 -2
- package/src/engineParticles.js +23 -19
- package/src/engineRelease.js +1 -0
- package/src/engineSettings.js +114 -10
- package/src/engineTileLayer.js +5 -0
package/dist/littlejs.esm.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.18.
|
|
38
|
+
const engineVersion = '1.18.18';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -622,7 +622,7 @@ let debugKey = 'Escape';
|
|
|
622
622
|
let debugOverlay = false;
|
|
623
623
|
|
|
624
624
|
// Engine internal variables not exposed to documentation
|
|
625
|
-
let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugTakeScreenshot;
|
|
625
|
+
let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugSound = false, debugTakeScreenshot;
|
|
626
626
|
|
|
627
627
|
///////////////////////////////////////////////////////////////////////////////
|
|
628
628
|
// Debug helper functions
|
|
@@ -862,6 +862,8 @@ function debugUpdate()
|
|
|
862
862
|
debugRaycast = !debugRaycast;
|
|
863
863
|
if (keyWasPressed('Digit5'))
|
|
864
864
|
debugScreenshot();
|
|
865
|
+
if (keyWasPressed('Digit7'))
|
|
866
|
+
debugSound = !debugSound;
|
|
865
867
|
}
|
|
866
868
|
if (debugVideoCaptureIsActive())
|
|
867
869
|
{
|
|
@@ -881,6 +883,9 @@ function debugRender()
|
|
|
881
883
|
// flush any gl sprites before drawing debug info
|
|
882
884
|
glFlush();
|
|
883
885
|
|
|
886
|
+
const savedDrawCount = drawCount;
|
|
887
|
+
const savedPrimitiveCount = primitiveCount;
|
|
888
|
+
|
|
884
889
|
if (debugTakeScreenshot)
|
|
885
890
|
{
|
|
886
891
|
// combine canvases, remove alpha and save
|
|
@@ -914,6 +919,8 @@ function debugRender()
|
|
|
914
919
|
const stickCount = gamepadStickData[i].length;
|
|
915
920
|
for (let j = 0; j < stickCount; j++)
|
|
916
921
|
{
|
|
922
|
+
if (!(j in gamepadStickData[i]))
|
|
923
|
+
continue; // skip sticks that are not present (eg a disabled touch left stick)
|
|
917
924
|
const stick = gamepadStick(j, i);
|
|
918
925
|
const drawPos = cornerPos.add(vec2(j*stickScale*2, 0));
|
|
919
926
|
const stickPos = drawPos.add(stick.scale(stickScale));
|
|
@@ -1092,6 +1099,8 @@ function debugRender()
|
|
|
1092
1099
|
debugContext.fillStyle = '#fff';
|
|
1093
1100
|
debugContext.fillText('5: Save Screenshot', x, y += h);
|
|
1094
1101
|
debugContext.fillText('6: Toggle Video Capture', x, y += h);
|
|
1102
|
+
debugContext.fillStyle = debugSound ? '#f00' : '#fff';
|
|
1103
|
+
debugContext.fillText('7: Debug Sound', x, y += h);
|
|
1095
1104
|
|
|
1096
1105
|
let keysPressed = '';
|
|
1097
1106
|
let mousePressed = '';
|
|
@@ -1126,10 +1135,27 @@ function debugRender()
|
|
|
1126
1135
|
debugContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
|
|
1127
1136
|
debugContext.fillText(debugRaycast ? 'Debug Raycasts' : '', x, y += h);
|
|
1128
1137
|
debugContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
|
|
1138
|
+
debugContext.fillText(debugSound ? 'Debug Sound' : '', x, y += h);
|
|
1129
1139
|
}
|
|
1130
1140
|
|
|
1131
1141
|
debugContext.restore();
|
|
1132
1142
|
}
|
|
1143
|
+
|
|
1144
|
+
if (debugWatermark || debugOverlay)
|
|
1145
|
+
{
|
|
1146
|
+
// show fps stats display
|
|
1147
|
+
mainContext.textAlign = 'right';
|
|
1148
|
+
mainContext.textBaseline = 'top';
|
|
1149
|
+
mainContext.font = '1em monospace';
|
|
1150
|
+
mainContext.fillStyle = '#000';
|
|
1151
|
+
const text = engineName + ' v' + engineVersion + ' / '
|
|
1152
|
+
+ savedDrawCount + ' / ' + savedPrimitiveCount + ' / '
|
|
1153
|
+
+ engineObjects.length + ' / ' + averageFPS.toFixed(1)
|
|
1154
|
+
+ (glEnable ? ' GL' : ' 2D') ;
|
|
1155
|
+
mainContext.fillText(text, mainCanvas.width-3, 3);
|
|
1156
|
+
mainContext.fillStyle = '#fff';
|
|
1157
|
+
mainContext.fillText(text, mainCanvas.width-2, 2);
|
|
1158
|
+
}
|
|
1133
1159
|
}
|
|
1134
1160
|
|
|
1135
1161
|
function debugRenderPost()
|
|
@@ -1139,21 +1165,6 @@ function debugRenderPost()
|
|
|
1139
1165
|
debugVideoCaptureUpdate();
|
|
1140
1166
|
return;
|
|
1141
1167
|
}
|
|
1142
|
-
|
|
1143
|
-
if (!debugWatermark && !debugOverlay) return;
|
|
1144
|
-
|
|
1145
|
-
// update fps display
|
|
1146
|
-
mainContext.textAlign = 'right';
|
|
1147
|
-
mainContext.textBaseline = 'top';
|
|
1148
|
-
mainContext.font = '1em monospace';
|
|
1149
|
-
mainContext.fillStyle = '#000';
|
|
1150
|
-
const text = engineName + ' v' + engineVersion + ' / '
|
|
1151
|
-
+ drawCount + ' / ' + primitiveCount + ' / '
|
|
1152
|
-
+ engineObjects.length + ' / ' + averageFPS.toFixed(1)
|
|
1153
|
-
+ (glEnable ? ' GL' : ' 2D') ;
|
|
1154
|
-
mainContext.fillText(text, mainCanvas.width-3, 3);
|
|
1155
|
-
mainContext.fillStyle = '#fff';
|
|
1156
|
-
mainContext.fillText(text, mainCanvas.width-2, 2);
|
|
1157
1168
|
}
|
|
1158
1169
|
|
|
1159
1170
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -2222,10 +2233,25 @@ class Color
|
|
|
2222
2233
|
* @return {Color} */
|
|
2223
2234
|
setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
|
|
2224
2235
|
|
|
2236
|
+
/** Sets the alpha of this color and returns self
|
|
2237
|
+
* @param {number} [a] - alpha
|
|
2238
|
+
* @return {Color} */
|
|
2239
|
+
setAlpha(a=1)
|
|
2240
|
+
{
|
|
2241
|
+
this.a = a;
|
|
2242
|
+
ASSERT_COLOR_VALID(this);
|
|
2243
|
+
return this;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2225
2246
|
/** Returns a new color that is a copy of this
|
|
2226
2247
|
* @return {Color} */
|
|
2227
2248
|
copy() { return new Color(this.r, this.g, this.b, this.a); }
|
|
2228
2249
|
|
|
2250
|
+
/** Returns a copy of this color with the alpha set
|
|
2251
|
+
* @param {number} [a] - alpha
|
|
2252
|
+
* @return {Color} */
|
|
2253
|
+
withAlpha(a=1) { return new Color(this.r, this.g, this.b, a); }
|
|
2254
|
+
|
|
2229
2255
|
/** Returns a copy of this color plus the color passed in
|
|
2230
2256
|
* @param {Color} c - other color
|
|
2231
2257
|
* @return {Color} */
|
|
@@ -2820,6 +2846,8 @@ let canvasFixedSize = vec2();
|
|
|
2820
2846
|
let canvasPixelated = false;
|
|
2821
2847
|
|
|
2822
2848
|
/** Disables texture filtering for crisper pixel art
|
|
2849
|
+
* - Leave true for pixel art so sprites stay sharp when scaled (uses NEAREST filtering)
|
|
2850
|
+
* - Set false for smooth/high-resolution art to enable bilinear filtering and mipmaps
|
|
2823
2851
|
* @type {boolean}
|
|
2824
2852
|
* @default
|
|
2825
2853
|
* @memberof Settings */
|
|
@@ -2975,35 +3003,78 @@ let touchInputEnable = true;
|
|
|
2975
3003
|
* - Supports left analog stick, 4 face buttons and start button (button 9)
|
|
2976
3004
|
* - setTouchGamepadButtonCount(1) to use face buttons as right analog stick
|
|
2977
3005
|
* - Analog stick buttons 10 and 11 are also activated when virtual sticks are touched
|
|
2978
|
-
|
|
3006
|
+
* - Rendered as a full-viewport HTML/SVG overlay, so controls may sit outside the game canvas
|
|
2979
3007
|
* @type {boolean}
|
|
2980
3008
|
* @default
|
|
2981
3009
|
* @memberof Settings */
|
|
2982
3010
|
let touchGamepadEnable = false;
|
|
2983
3011
|
|
|
2984
|
-
/** True if
|
|
2985
|
-
* -
|
|
2986
|
-
*
|
|
3012
|
+
/** True if touches outside the gamepad controls should still drive mouse/touch input
|
|
3013
|
+
* - When false (the default), enabling the touch gamepad suppresses touch-to-mouse input entirely
|
|
3014
|
+
* - Set true to also pass touches outside the controls through to the game as mouse/touch input
|
|
3015
|
+
* - Touches on the gamepad controls never drive the mouse regardless of this setting
|
|
3016
|
+
* @type {boolean}
|
|
3017
|
+
* @default
|
|
3018
|
+
* @memberof Settings */
|
|
3019
|
+
let touchGamepadPassthrough = false;
|
|
3020
|
+
|
|
3021
|
+
/** Size of center button if touch gamepad should have start button in the center
|
|
3022
|
+
* - Prevents activating when pressed near virtual stick or face buttons
|
|
2987
3023
|
* - When the game is paused, any touch will press the button
|
|
2988
|
-
* -
|
|
3024
|
+
* - Measured in viewport CSS pixels
|
|
2989
3025
|
* @type {number}
|
|
2990
3026
|
* @default
|
|
2991
3027
|
* @memberof Settings */
|
|
2992
|
-
let touchGamepadCenterButtonSize =
|
|
3028
|
+
let touchGamepadCenterButtonSize = 0;
|
|
2993
3029
|
|
|
2994
|
-
/** Number of buttons on touch gamepad (0-4),
|
|
3030
|
+
/** Number of buttons on the right side of the touch gamepad (0-4), using gamepad buttons 0-3
|
|
3031
|
+
* - A count of 1 is a single large button (the size of a stick)
|
|
3032
|
+
* - Ignored when touchGamepadRightStick is set (the right side is a stick instead)
|
|
2995
3033
|
* @type {number}
|
|
2996
3034
|
* @default
|
|
2997
3035
|
* @memberof Settings */
|
|
2998
3036
|
let touchGamepadButtonCount = 4;
|
|
2999
3037
|
|
|
3038
|
+
/** True if the touch gamepad should have a left analog stick (or dpad)
|
|
3039
|
+
* - When false, the left side is face buttons (touchGamepadLeftButtonCount) or nothing
|
|
3040
|
+
* @type {boolean}
|
|
3041
|
+
* @default
|
|
3042
|
+
* @memberof Settings */
|
|
3043
|
+
let touchGamepadLeftStick = true;
|
|
3044
|
+
|
|
3045
|
+
/** Number of buttons on the left side of the touch gamepad (0-4), using gamepad buttons 4-7
|
|
3046
|
+
* - Only used when touchGamepadLeftStick is false (otherwise the left side is a stick)
|
|
3047
|
+
* - A count of 1 is a single large button (the size of a stick)
|
|
3048
|
+
* @type {number}
|
|
3049
|
+
* @default
|
|
3050
|
+
* @memberof Settings */
|
|
3051
|
+
let touchGamepadLeftButtonCount = 0;
|
|
3052
|
+
|
|
3053
|
+
/** True if the touch gamepad right side should be an analog stick (or dpad) instead of face buttons
|
|
3054
|
+
* - When set, touchGamepadButtonCount is ignored and the right side is a stick
|
|
3055
|
+
* - Uses an analog stick when touchGamepadAnalog is true, otherwise an 8 way dpad
|
|
3056
|
+
* @type {boolean}
|
|
3057
|
+
* @default
|
|
3058
|
+
* @memberof Settings */
|
|
3059
|
+
let touchGamepadRightStick = false;
|
|
3060
|
+
|
|
3000
3061
|
/** True if touch gamepad should be analog stick or false to use if 8 way dpad
|
|
3001
3062
|
* @type {boolean}
|
|
3002
3063
|
* @default
|
|
3003
3064
|
* @memberof Settings */
|
|
3004
3065
|
let touchGamepadAnalog = true;
|
|
3005
3066
|
|
|
3006
|
-
/**
|
|
3067
|
+
/** True if touch gamepad directional controls should float to where you press
|
|
3068
|
+
* - Only affects analog sticks and dpads, not face buttons
|
|
3069
|
+
* - Directional controls re-anchor to where you press within the bottom ~60% of their screen half; the top ~40% passes through to the game
|
|
3070
|
+
* - The right side floats only when it acts as the right analog stick (touchGamepadRightStick is set)
|
|
3071
|
+
* - A center button (touchGamepadCenterButtonSize) still works since it ignores touches near the sticks
|
|
3072
|
+
* @type {boolean}
|
|
3073
|
+
* @default
|
|
3074
|
+
* @memberof Settings */
|
|
3075
|
+
let touchGamepadFloating = false;
|
|
3076
|
+
|
|
3077
|
+
/** Size of virtual gamepad for touch devices in viewport CSS pixels
|
|
3007
3078
|
* @type {number}
|
|
3008
3079
|
* @default
|
|
3009
3080
|
* @memberof Settings */
|
|
@@ -3021,6 +3092,13 @@ let touchGamepadAlpha = .3;
|
|
|
3021
3092
|
* @memberof Settings */
|
|
3022
3093
|
let touchGamepadDisplayTime = 3;
|
|
3023
3094
|
|
|
3095
|
+
/** Duration in ms to vibrate when a touch gamepad face button or start button is pressed
|
|
3096
|
+
* - Set to 0 to disable, also requires vibrateEnable and hardware support (ignored on iOS)
|
|
3097
|
+
* @type {number}
|
|
3098
|
+
* @default
|
|
3099
|
+
* @memberof Settings */
|
|
3100
|
+
let touchGamepadVibration = 0;
|
|
3101
|
+
|
|
3024
3102
|
/** Allow vibration hardware if it exists
|
|
3025
3103
|
* @type {boolean}
|
|
3026
3104
|
* @default
|
|
@@ -3123,6 +3201,7 @@ function setCanvasPixelated(pixelated)
|
|
|
3123
3201
|
}
|
|
3124
3202
|
|
|
3125
3203
|
/** Disables texture filtering for crisper pixel art
|
|
3204
|
+
* - Leave true for pixel art; set false for smooth/high-resolution art
|
|
3126
3205
|
* @param {boolean} pixelated
|
|
3127
3206
|
* @memberof Settings */
|
|
3128
3207
|
function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
|
|
@@ -3253,6 +3332,11 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
|
|
|
3253
3332
|
* @memberof Settings */
|
|
3254
3333
|
function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
3255
3334
|
|
|
3335
|
+
/** Set if touches outside the gamepad controls should still drive mouse/touch input
|
|
3336
|
+
* @param {boolean} passthrough
|
|
3337
|
+
* @memberof Settings */
|
|
3338
|
+
function setTouchGamepadPassthrough(passthrough) { touchGamepadPassthrough = passthrough; }
|
|
3339
|
+
|
|
3256
3340
|
/** Set if touch gamepad should have start button in the center
|
|
3257
3341
|
* - Set size to enable the center button
|
|
3258
3342
|
* - When the game is paused, any touch will press the button
|
|
@@ -3260,16 +3344,57 @@ function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
|
3260
3344
|
* @memberof Settings */
|
|
3261
3345
|
function setTouchGamepadCenterButtonSize(size) { touchGamepadCenterButtonSize = size; }
|
|
3262
3346
|
|
|
3263
|
-
/** Set number of buttons on touch gamepad (0-4
|
|
3347
|
+
/** Set number of buttons on the right side of the touch gamepad (0-4, gamepad buttons 0-3)
|
|
3348
|
+
* @param {number} count
|
|
3349
|
+
* @memberof Settings */
|
|
3350
|
+
function setTouchGamepadButtonCount(count)
|
|
3351
|
+
{
|
|
3352
|
+
touchGamepadButtonCount = count;
|
|
3353
|
+
if (count > 0)
|
|
3354
|
+
touchGamepadRightStick = false;
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
/** Set if the touch gamepad should have a left analog stick (or dpad)
|
|
3358
|
+
* @param {boolean} enable
|
|
3359
|
+
* @memberof Settings */
|
|
3360
|
+
function setTouchGamepadLeftStick(enable)
|
|
3361
|
+
{
|
|
3362
|
+
touchGamepadLeftStick = enable;
|
|
3363
|
+
if (enable)
|
|
3364
|
+
touchGamepadLeftButtonCount = 0;
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
/** Set number of buttons on the left side of the touch gamepad (0-4, gamepad buttons 4-7)
|
|
3368
|
+
* - Only used when touchGamepadLeftStick is false
|
|
3264
3369
|
* @param {number} count
|
|
3265
3370
|
* @memberof Settings */
|
|
3266
|
-
function
|
|
3371
|
+
function setTouchGamepadLeftButtonCount(count)
|
|
3372
|
+
{
|
|
3373
|
+
touchGamepadLeftButtonCount = count;
|
|
3374
|
+
if (count > 0)
|
|
3375
|
+
touchGamepadLeftStick = false;
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
/** Set if the touch gamepad right side is an analog stick (or dpad) instead of face buttons
|
|
3379
|
+
* @param {boolean} rightStick
|
|
3380
|
+
* @memberof Settings */
|
|
3381
|
+
function setTouchGamepadRightStick(rightStick)
|
|
3382
|
+
{
|
|
3383
|
+
touchGamepadRightStick = rightStick;
|
|
3384
|
+
if (rightStick)
|
|
3385
|
+
touchGamepadButtonCount = 0;
|
|
3386
|
+
}
|
|
3267
3387
|
|
|
3268
3388
|
/** Set if touch gamepad should be analog stick or 8 way dpad
|
|
3269
3389
|
* @param {boolean} analog
|
|
3270
3390
|
* @memberof Settings */
|
|
3271
3391
|
function setTouchGamepadAnalog(analog) { touchGamepadAnalog = analog; }
|
|
3272
3392
|
|
|
3393
|
+
/** Set if touch gamepad directional controls should float to where you press
|
|
3394
|
+
* @param {boolean} floating
|
|
3395
|
+
* @memberof Settings */
|
|
3396
|
+
function setTouchGamepadFloating(floating) { touchGamepadFloating = floating; }
|
|
3397
|
+
|
|
3273
3398
|
/** Set size of virtual gamepad for touch devices in pixels
|
|
3274
3399
|
* @param {number} size
|
|
3275
3400
|
* @memberof Settings */
|
|
@@ -3285,6 +3410,11 @@ function setTouchGamepadAlpha(alpha) { touchGamepadAlpha = alpha; }
|
|
|
3285
3410
|
* @memberof Settings */
|
|
3286
3411
|
function setTouchGamepadDisplayTime(time) { touchGamepadDisplayTime = time; }
|
|
3287
3412
|
|
|
3413
|
+
/** Set duration in ms to vibrate when a touch gamepad face or start button is pressed (0 disables)
|
|
3414
|
+
* @param {number} ms
|
|
3415
|
+
* @memberof Settings */
|
|
3416
|
+
function setTouchGamepadVibration(ms) { touchGamepadVibration = ms; }
|
|
3417
|
+
|
|
3288
3418
|
/** Set to allow vibration hardware if it exists
|
|
3289
3419
|
* @param {boolean} enable
|
|
3290
3420
|
* @memberof Settings */
|
|
@@ -3463,10 +3593,10 @@ class EngineObject
|
|
|
3463
3593
|
if (pa)
|
|
3464
3594
|
{
|
|
3465
3595
|
const c = cos(-pa), s = sin(-pa);
|
|
3466
|
-
this.pos
|
|
3596
|
+
this.pos.set(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
|
|
3467
3597
|
}
|
|
3468
3598
|
else
|
|
3469
|
-
this.pos
|
|
3599
|
+
this.pos.set(lx + pp.x, ly + pp.y);
|
|
3470
3600
|
this.angle = mirror*this.localAngle + pa;
|
|
3471
3601
|
}
|
|
3472
3602
|
|
|
@@ -3698,6 +3828,9 @@ class EngineObject
|
|
|
3698
3828
|
drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
|
|
3699
3829
|
}
|
|
3700
3830
|
|
|
3831
|
+
/** Optional hook called during the light system plugin's lightmap pass to draw this object's lightmap contribution. Does nothing by default. */
|
|
3832
|
+
renderLight() {}
|
|
3833
|
+
|
|
3701
3834
|
/** Destroy this object, destroy its children, detach its parent, and mark it for removal
|
|
3702
3835
|
* @param {boolean} [immediate] - should attached effects be allowed to die off? */
|
|
3703
3836
|
destroy(immediate=false)
|
|
@@ -4202,11 +4335,12 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
4202
4335
|
// normal canvas 2D rendering method (slower)
|
|
4203
4336
|
++drawCount;
|
|
4204
4337
|
++primitiveCount;
|
|
4205
|
-
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
4206
4338
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
4207
4339
|
{
|
|
4208
4340
|
if (textureInfo)
|
|
4209
4341
|
{
|
|
4342
|
+
// un-flip Y so the image renders right-side up under drawCanvas2D's Y flip
|
|
4343
|
+
context.scale(1, -1);
|
|
4210
4344
|
// calculate uvs and render
|
|
4211
4345
|
const x = tileInfo.pos.x, y = tileInfo.pos.y;
|
|
4212
4346
|
const w = tileInfo.size.x, h = tileInfo.size.y;
|
|
@@ -4214,7 +4348,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
4214
4348
|
}
|
|
4215
4349
|
else
|
|
4216
4350
|
{
|
|
4217
|
-
// if no tile info, use untextured rect
|
|
4351
|
+
// if no tile info, use untextured rect (Y-symmetric, no compensation needed)
|
|
4218
4352
|
const c = additiveColor ? color.add(additiveColor) : color;
|
|
4219
4353
|
context.fillStyle = c.toString();
|
|
4220
4354
|
context.fillRect(-.5, -.5, 1, 1);
|
|
@@ -4288,11 +4422,10 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, an
|
|
|
4288
4422
|
// normal canvas 2D rendering method (slower)
|
|
4289
4423
|
++drawCount;
|
|
4290
4424
|
++primitiveCount;
|
|
4291
|
-
size = new Vector2(size.x, -size.y); // fix upside down sprites
|
|
4292
4425
|
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
4293
4426
|
{
|
|
4294
|
-
//
|
|
4295
|
-
const gradient = context.createLinearGradient(0,
|
|
4427
|
+
// gradient endpoints are flipped to match the Y flip inside drawCanvas2D
|
|
4428
|
+
const gradient = context.createLinearGradient(0, .5, 0, -.5);
|
|
4296
4429
|
gradient.addColorStop(0, colorTop.toString());
|
|
4297
4430
|
gradient.addColorStop(1, colorBottom.toString());
|
|
4298
4431
|
context.fillStyle = gradient;
|
|
@@ -4711,7 +4844,10 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
4711
4844
|
* @memberof Draw
|
|
4712
4845
|
*/
|
|
4713
4846
|
|
|
4714
|
-
/** Draw directly to a 2d canvas context in world space
|
|
4847
|
+
/** Draw directly to a 2d canvas context in world space.
|
|
4848
|
+
* The Y axis is flipped so world-Y-up coordinates render right-side up
|
|
4849
|
+
* (matches the WebGL path). Callers whose drawing depends on Y direction
|
|
4850
|
+
* (e.g. linear gradients) should flip their own Y endpoints accordingly.
|
|
4715
4851
|
* @param {Vector2} pos
|
|
4716
4852
|
* @param {Vector2} size
|
|
4717
4853
|
* @param {number} angle
|
|
@@ -4957,6 +5093,62 @@ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
|
|
|
4957
5093
|
* @memberof Draw */
|
|
4958
5094
|
function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
4959
5095
|
|
|
5096
|
+
/** Fit the camera to a rectangle in world space by setting cameraPos and cameraScale
|
|
5097
|
+
* - worldMargin pads the content rectangle in world units, so the gap scales with the content on resize
|
|
5098
|
+
* - screenInset reserves space in screen pixels on each viewport edge (for example a HUD band) and
|
|
5099
|
+
* re-centers the content away from that edge, so the reserved band stays a fixed pixel size on resize
|
|
5100
|
+
* - worldMargin and screenInset may each be a number for all sides, a Vector2 (x=left/right, y=top/bottom),
|
|
5101
|
+
* or an object with any of {top, right, bottom, left}
|
|
5102
|
+
* @param {Vector2} center - Center of the rectangle in world space
|
|
5103
|
+
* @param {Vector2} size - Size of the rectangle in world space
|
|
5104
|
+
* @param {number|Vector2|Object} [worldMargin] - World space padding added around the content rectangle
|
|
5105
|
+
* @param {number|Vector2|Object} [screenInset] - Screen space padding in pixels reserved on each viewport edge
|
|
5106
|
+
* @return {number} - The new camera scale
|
|
5107
|
+
* @memberof Draw */
|
|
5108
|
+
function cameraFit(center, size, worldMargin, screenInset)
|
|
5109
|
+
{
|
|
5110
|
+
ASSERT(isVector2(center), 'center must be a vec2');
|
|
5111
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
5112
|
+
|
|
5113
|
+
// pad the content
|
|
5114
|
+
const margin = padSides(worldMargin);
|
|
5115
|
+
const inset = padSides(screenInset);
|
|
5116
|
+
const worldW = size.x + margin.left + margin.right;
|
|
5117
|
+
const worldH = size.y + margin.top + margin.bottom;
|
|
5118
|
+
const viewW = mainCanvasSize.x - inset.left - inset.right;
|
|
5119
|
+
const viewH = mainCanvasSize.y - inset.top - inset.bottom;
|
|
5120
|
+
|
|
5121
|
+
// bail on a degenerate rect or viewport rather than NaN the camera
|
|
5122
|
+
if (!(worldW > 0 && worldH > 0 && viewW > 0 && viewH > 0))
|
|
5123
|
+
return cameraScale;
|
|
5124
|
+
|
|
5125
|
+
// scale to fit the padded content
|
|
5126
|
+
cameraScale = min(viewW / worldW, viewH / worldH);
|
|
5127
|
+
|
|
5128
|
+
// calculate offset vectors
|
|
5129
|
+
const marginVector = vec2(margin.right - margin.left, margin.top - margin.bottom).scale(.5);
|
|
5130
|
+
const insetVector = vec2(inset.right - inset.left, inset.top - inset.bottom).scale(.5 / cameraScale);
|
|
5131
|
+
|
|
5132
|
+
// apply the offsets and return camera scale
|
|
5133
|
+
cameraPos = center.add(marginVector).add(insetVector);
|
|
5134
|
+
return cameraScale;
|
|
5135
|
+
|
|
5136
|
+
function padSides(p)
|
|
5137
|
+
{
|
|
5138
|
+
// normalize a padding option to {top, right, bottom, left}
|
|
5139
|
+
if (p === undefined || isNumber(p))
|
|
5140
|
+
p = vec2(p);
|
|
5141
|
+
if (isVector2(p))
|
|
5142
|
+
return { top: p.y, right: p.x, bottom: p.y, left: p.x };
|
|
5143
|
+
return {
|
|
5144
|
+
top: p.top || 0,
|
|
5145
|
+
right: p.right || 0,
|
|
5146
|
+
bottom: p.bottom || 0,
|
|
5147
|
+
left: p.left || 0,
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
|
|
4960
5152
|
/** Check if a box, point, or circle is on screen with a circle test
|
|
4961
5153
|
* If size is a Vector2, uses the length as diameter
|
|
4962
5154
|
* This can be used to cull offscreen objects from render or update
|
|
@@ -4995,14 +5187,13 @@ function isOnScreen(pos, size=0)
|
|
|
4995
5187
|
y + size > -h && y - size < h;
|
|
4996
5188
|
}
|
|
4997
5189
|
|
|
4998
|
-
/** Enable
|
|
5190
|
+
/** Enable additive blending
|
|
4999
5191
|
* @param {boolean} [additive]
|
|
5000
|
-
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
5001
5192
|
* @memberof Draw */
|
|
5002
|
-
function
|
|
5193
|
+
function setAdditiveBlendMode(additive=true)
|
|
5003
5194
|
{
|
|
5004
5195
|
glAdditive = additive;
|
|
5005
|
-
|
|
5196
|
+
drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
|
|
5006
5197
|
}
|
|
5007
5198
|
|
|
5008
5199
|
/** Combines LittleJS canvases onto the main canvas
|
|
@@ -5328,11 +5519,30 @@ let mouseWheel = 0;
|
|
|
5328
5519
|
* @memberof Input */
|
|
5329
5520
|
let mouseInWindow = true;
|
|
5330
5521
|
|
|
5331
|
-
/**
|
|
5522
|
+
/** True if a gamepad is the most recently used input device.
|
|
5523
|
+
* Equivalent to usingGamepadInput(); derived from lastInputDevice each frame.
|
|
5332
5524
|
* @type {boolean}
|
|
5333
5525
|
* @memberof Input */
|
|
5334
5526
|
let isUsingGamepad = false;
|
|
5335
5527
|
|
|
5528
|
+
/** The most recently used input device: 'mouse' | 'keyboard' | 'gamepad'.
|
|
5529
|
+
* Sticky: it holds its value while every device is idle, so a mouse-follow
|
|
5530
|
+
* control (e.g. paddle = mousePos) won't snap back the instant the stick/keys
|
|
5531
|
+
* are released. With several devices in play at once (e.g. keyboard to move +
|
|
5532
|
+
* mouse to aim) it tracks whichever was touched last each frame, so it may
|
|
5533
|
+
* alternate — that's intended; use it to pick which control drives a shared
|
|
5534
|
+
* action. Updated every frame by inputUpdate().
|
|
5535
|
+
* @type {string}
|
|
5536
|
+
* @memberof Input */
|
|
5537
|
+
let lastInputDevice = 'mouse';
|
|
5538
|
+
|
|
5539
|
+
/** Screen-pixel mouse movement per frame that counts as "using the mouse"
|
|
5540
|
+
* (so sub-pixel hand jitter doesn't steal focus from the keyboard/gamepad).
|
|
5541
|
+
* @type {number}
|
|
5542
|
+
* @default
|
|
5543
|
+
* @memberof Input */
|
|
5544
|
+
let inputMouseMoveThreshold = 6;
|
|
5545
|
+
|
|
5336
5546
|
/** Prevents input continuing to the default browser handling (true by default)
|
|
5337
5547
|
* @type {boolean}
|
|
5338
5548
|
* @memberof Input */
|
|
@@ -5353,6 +5563,18 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
|
5353
5563
|
* @memberof Input */
|
|
5354
5564
|
function setInputPreventDefault(preventDefault=true) { inputPreventDefault = preventDefault; }
|
|
5355
5565
|
|
|
5566
|
+
/** Set the screen-pixel mouse movement per frame that counts as using the mouse
|
|
5567
|
+
* @param {number} threshold
|
|
5568
|
+
* @memberof Input */
|
|
5569
|
+
function setInputMouseMoveThreshold(threshold) { inputMouseMoveThreshold = threshold; }
|
|
5570
|
+
|
|
5571
|
+
/** @return {boolean} - Is the mouse the most recently used input device? @memberof Input */
|
|
5572
|
+
function usingMouseInput() { return lastInputDevice === 'mouse'; }
|
|
5573
|
+
/** @return {boolean} - Is the keyboard the most recently used input device? @memberof Input */
|
|
5574
|
+
function usingKeyboardInput() { return lastInputDevice === 'keyboard'; }
|
|
5575
|
+
/** @return {boolean} - Is a gamepad the most recently used input device? @memberof Input */
|
|
5576
|
+
function usingGamepadInput() { return lastInputDevice === 'gamepad'; }
|
|
5577
|
+
|
|
5356
5578
|
/** Clears an input key state
|
|
5357
5579
|
* @param {string|number} key
|
|
5358
5580
|
* @param {number} [device]
|
|
@@ -5375,6 +5597,7 @@ function inputClear()
|
|
|
5375
5597
|
inputData[0] = [];
|
|
5376
5598
|
touchGamepadButtons.length = 0;
|
|
5377
5599
|
touchGamepadSticks.length = 0;
|
|
5600
|
+
touchGamepadStickPointerId.length = 0; // release floating sticks so they re-anchor
|
|
5378
5601
|
gamepadStickData.length = 0;
|
|
5379
5602
|
gamepadDpadData.length = 0;
|
|
5380
5603
|
}
|
|
@@ -5617,6 +5840,14 @@ const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
|
|
|
5617
5840
|
|
|
5618
5841
|
// touch gamepad internal variables
|
|
5619
5842
|
const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
|
|
5843
|
+
// floating stick anchors (stage-local CSS pixels) and owning pointer ids, indexed by stick (0=left, 1=right)
|
|
5844
|
+
const touchGamepadStickAnchors = [], touchGamepadStickPointerId = [];
|
|
5845
|
+
// pointerId -> control role ('stick0', 'stick1', 'face<n>', or 'start')
|
|
5846
|
+
const touchGamepadPointerRole = new Map();
|
|
5847
|
+
// overlay DOM elements (created lazily on touch devices) and cached SVG shapes
|
|
5848
|
+
let touchGamepadOverlay, touchGamepadStage, touchGamepadSvg, touchGamepadSvgEls;
|
|
5849
|
+
let touchGamepadSideZones = [], touchGamepadZoneC;
|
|
5850
|
+
let touchGamepadNeedRelayout = true, touchGamepadLastLayout;
|
|
5620
5851
|
|
|
5621
5852
|
///////////////////////////////////////////////////////////////////////////////
|
|
5622
5853
|
// Input system functions used by engine
|
|
@@ -5644,7 +5875,6 @@ function inputInit()
|
|
|
5644
5875
|
{
|
|
5645
5876
|
if (!e.repeat)
|
|
5646
5877
|
{
|
|
5647
|
-
isUsingGamepad = false;
|
|
5648
5878
|
inputData[0][e.code] = 3;
|
|
5649
5879
|
if (inputWASDEmulateDirection)
|
|
5650
5880
|
inputData[0][remapKey(e.code)] = 3;
|
|
@@ -5703,7 +5933,6 @@ function inputInit()
|
|
|
5703
5933
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
5704
5934
|
audioContext.resume();
|
|
5705
5935
|
|
|
5706
|
-
isUsingGamepad = false;
|
|
5707
5936
|
inputData[0][e.button] = 3;
|
|
5708
5937
|
|
|
5709
5938
|
const mousePosScreenLast = mousePosScreen;
|
|
@@ -5741,7 +5970,15 @@ function inputInit()
|
|
|
5741
5970
|
e.preventDefault(); // prevent page scrolling
|
|
5742
5971
|
}
|
|
5743
5972
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
5744
|
-
function onBlur()
|
|
5973
|
+
function onBlur()
|
|
5974
|
+
{
|
|
5975
|
+
inputClear();
|
|
5976
|
+
// release any held virtual gamepad controls so they don't stick
|
|
5977
|
+
touchGamepadPointerRole.clear();
|
|
5978
|
+
touchGamepadButtons.length = 0;
|
|
5979
|
+
touchGamepadSticks.length = 0;
|
|
5980
|
+
touchGamepadStickPointerId.length = 0;
|
|
5981
|
+
}
|
|
5745
5982
|
|
|
5746
5983
|
// enable touch input mouse passthrough
|
|
5747
5984
|
function touchInputInit()
|
|
@@ -5757,36 +5994,43 @@ function inputInit()
|
|
|
5757
5994
|
{
|
|
5758
5995
|
if (!touchInputEnable) return;
|
|
5759
5996
|
|
|
5760
|
-
// route touch to gamepad
|
|
5761
|
-
if (touchGamepadEnable)
|
|
5762
|
-
handleTouchGamepad(e);
|
|
5763
|
-
|
|
5764
5997
|
// fix stalled audio requiring user interaction
|
|
5765
5998
|
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
5766
5999
|
audioContext.resume();
|
|
5767
6000
|
|
|
5768
|
-
//
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
if (
|
|
6001
|
+
// when the touch gamepad is enabled it owns touch input: suppress the
|
|
6002
|
+
// touch->mouse passthrough entirely unless touchGamepadPassthrough is set
|
|
6003
|
+
// (its own zones drive gameplay via pointer events)
|
|
6004
|
+
if (!touchGamepadEnable || touchGamepadPassthrough)
|
|
5772
6005
|
{
|
|
5773
|
-
//
|
|
5774
|
-
|
|
5775
|
-
const
|
|
5776
|
-
|
|
5777
|
-
|
|
6006
|
+
// touches that landed on a virtual gamepad zone are owned by the gamepad
|
|
6007
|
+
// (handled by its own pointer listeners) and must not drive the game mouse
|
|
6008
|
+
const isGamepadTouch = (t)=>
|
|
6009
|
+
touchGamepadSideZones.includes(t.target) || t.target === touchGamepadZoneC;
|
|
6010
|
+
const gameTouches = [];
|
|
6011
|
+
for (const t of e.touches)
|
|
6012
|
+
if (!isGamepadTouch(t)) gameTouches.push(t);
|
|
6013
|
+
|
|
6014
|
+
// check if touching and pass to mouse events
|
|
6015
|
+
const touching = gameTouches.length;
|
|
6016
|
+
const button = 0; // all touches are left mouse button
|
|
6017
|
+
if (touching)
|
|
5778
6018
|
{
|
|
5779
|
-
|
|
5780
|
-
|
|
6019
|
+
// set event pos and pass it along
|
|
6020
|
+
const pos = vec2(gameTouches[0].clientX, gameTouches[0].clientY);
|
|
6021
|
+
const mousePosScreenLast = mousePosScreen;
|
|
6022
|
+
mousePosScreen = mouseEventToScreen(pos);
|
|
6023
|
+
if (wasTouching)
|
|
6024
|
+
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
6025
|
+
else
|
|
6026
|
+
inputData[0][button] = 3;
|
|
5781
6027
|
}
|
|
5782
|
-
else
|
|
5783
|
-
inputData[0][button] =
|
|
5784
|
-
}
|
|
5785
|
-
else if (wasTouching)
|
|
5786
|
-
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
6028
|
+
else if (wasTouching)
|
|
6029
|
+
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
5787
6030
|
|
|
5788
|
-
|
|
5789
|
-
|
|
6031
|
+
// set was touching
|
|
6032
|
+
wasTouching = touching;
|
|
6033
|
+
}
|
|
5790
6034
|
|
|
5791
6035
|
// prevent default handling like copy, magnifier lens, and scrolling
|
|
5792
6036
|
if (inputPreventDefault && e.cancelable && document.hasFocus())
|
|
@@ -5796,83 +6040,6 @@ function inputInit()
|
|
|
5796
6040
|
return true;
|
|
5797
6041
|
}
|
|
5798
6042
|
|
|
5799
|
-
// special handling for virtual gamepad mode
|
|
5800
|
-
function handleTouchGamepad(e)
|
|
5801
|
-
{
|
|
5802
|
-
// clear touch gamepad input
|
|
5803
|
-
touchGamepadSticks.length = 0;
|
|
5804
|
-
touchGamepadSticks[0] = vec2();
|
|
5805
|
-
touchGamepadSticks[1] = vec2();
|
|
5806
|
-
touchGamepadButtons.length = 0;
|
|
5807
|
-
isUsingGamepad = true;
|
|
5808
|
-
|
|
5809
|
-
const touching = e.touches.length;
|
|
5810
|
-
if (touching)
|
|
5811
|
-
{
|
|
5812
|
-
touchGamepadTimer.set();
|
|
5813
|
-
if (touchGamepadCenterButtonSize && !wasTouching && paused)
|
|
5814
|
-
{
|
|
5815
|
-
// touch anywhere to press start when paused
|
|
5816
|
-
touchGamepadButtons[9] = 1;
|
|
5817
|
-
return;
|
|
5818
|
-
}
|
|
5819
|
-
}
|
|
5820
|
-
|
|
5821
|
-
// don't process touch gamepad if paused
|
|
5822
|
-
if (paused) return;
|
|
5823
|
-
|
|
5824
|
-
// get center of left and right sides
|
|
5825
|
-
const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5826
|
-
const buttonCenter = touchGamepadButtonCenter();
|
|
5827
|
-
const startCenter = mainCanvasSize.scale(.5);
|
|
5828
|
-
|
|
5829
|
-
// check each touch point
|
|
5830
|
-
for (const touch of e.touches)
|
|
5831
|
-
{
|
|
5832
|
-
const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
|
|
5833
|
-
if (stickCenter.distance(touchPos) < touchGamepadSize)
|
|
5834
|
-
{
|
|
5835
|
-
// virtual analog stick
|
|
5836
|
-
const delta = touchPos.subtract(stickCenter);
|
|
5837
|
-
touchGamepadSticks[0] = delta.scale(2/touchGamepadSize).clampLength();
|
|
5838
|
-
touchGamepadButtons[10] = 1; // also press a button when touching stick
|
|
5839
|
-
}
|
|
5840
|
-
else if (buttonCenter.distance(touchPos) < touchGamepadSize)
|
|
5841
|
-
{
|
|
5842
|
-
if (touchGamepadButtonCount === 1)
|
|
5843
|
-
{
|
|
5844
|
-
// virtual right analog stick
|
|
5845
|
-
const delta = touchPos.subtract(buttonCenter);
|
|
5846
|
-
touchGamepadSticks[1] = delta.scale(2/touchGamepadSize).clampLength();
|
|
5847
|
-
touchGamepadButtons[11] = 1; // also press a button when touching right stick
|
|
5848
|
-
}
|
|
5849
|
-
// virtual face buttons
|
|
5850
|
-
let button = buttonCenter.subtract(touchPos).direction();
|
|
5851
|
-
button = mod(button+2, 4);
|
|
5852
|
-
if (touchGamepadButtonCount === 1)
|
|
5853
|
-
button = 0;
|
|
5854
|
-
else if (touchGamepadButtonCount === 2)
|
|
5855
|
-
{
|
|
5856
|
-
const delta = buttonCenter.subtract(touchPos);
|
|
5857
|
-
button = -delta.x < delta.y ? 1 : 0;
|
|
5858
|
-
}
|
|
5859
|
-
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
5860
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
5861
|
-
if (button < touchGamepadButtonCount)
|
|
5862
|
-
touchGamepadButtons[button] = 1;
|
|
5863
|
-
}
|
|
5864
|
-
else if (startCenter.distance(touchPos) < touchGamepadCenterButtonSize &&
|
|
5865
|
-
stickCenter.distance(touchPos) >= 2 * touchGamepadSize &&
|
|
5866
|
-
buttonCenter.distance(touchPos) >= 2 * touchGamepadSize)
|
|
5867
|
-
{
|
|
5868
|
-
// virtual start button in center
|
|
5869
|
-
// require a fat-finger buffer of touchGamepadSize beyond the
|
|
5870
|
-
// edge of the stick/buttons so drift off those controls can't
|
|
5871
|
-
// accidentally fire start
|
|
5872
|
-
touchGamepadButtons[9] = 1;
|
|
5873
|
-
}
|
|
5874
|
-
}
|
|
5875
|
-
}
|
|
5876
6043
|
}
|
|
5877
6044
|
|
|
5878
6045
|
// convert a mouse or touch event position to screen space
|
|
@@ -5897,9 +6064,48 @@ function inputUpdate()
|
|
|
5897
6064
|
mousePos = screenToWorld(mousePosScreen);
|
|
5898
6065
|
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
5899
6066
|
|
|
6067
|
+
// build the touch gamepad overlay lazily once enabled on a touch device
|
|
6068
|
+
touchGamepadInit();
|
|
6069
|
+
|
|
5900
6070
|
// update gamepads if enabled
|
|
5901
6071
|
gamepadsUpdate();
|
|
5902
|
-
|
|
6072
|
+
|
|
6073
|
+
// update most recently used input device
|
|
6074
|
+
updateLastInputDevice();
|
|
6075
|
+
|
|
6076
|
+
function updateLastInputDevice()
|
|
6077
|
+
{
|
|
6078
|
+
// mouse: any button held or moved
|
|
6079
|
+
const mouseActive = mouseIsDown(0) || mouseIsDown(1) || mouseIsDown(2) || mouseDeltaScreen.length() > inputMouseMoveThreshold;
|
|
6080
|
+
|
|
6081
|
+
// gamepad: any button held or stick moved
|
|
6082
|
+
let gamepadActive = false;
|
|
6083
|
+
for (let s = gamepadStickCount(); s-- && !gamepadActive;)
|
|
6084
|
+
gamepadActive = gamepadStick(s).lengthSquared() > .04;
|
|
6085
|
+
for (let b = 17; b-- && !gamepadActive;)
|
|
6086
|
+
gamepadActive = gamepadIsDown(b);
|
|
6087
|
+
|
|
6088
|
+
// keyboard: any non-mouse key down
|
|
6089
|
+
let keyboardActive = false;
|
|
6090
|
+
for (const k in inputData[0])
|
|
6091
|
+
if (isNaN(+k) && (inputData[0][k] & 1))
|
|
6092
|
+
{
|
|
6093
|
+
keyboardActive = true;
|
|
6094
|
+
break;
|
|
6095
|
+
}
|
|
6096
|
+
|
|
6097
|
+
// update the last input
|
|
6098
|
+
if (gamepadActive)
|
|
6099
|
+
lastInputDevice = 'gamepad';
|
|
6100
|
+
else if (mouseActive)
|
|
6101
|
+
lastInputDevice = 'mouse';
|
|
6102
|
+
else if (keyboardActive)
|
|
6103
|
+
lastInputDevice = 'keyboard';
|
|
6104
|
+
|
|
6105
|
+
// set flag if gamepad is last device
|
|
6106
|
+
isUsingGamepad = lastInputDevice === 'gamepad';
|
|
6107
|
+
}
|
|
6108
|
+
|
|
5903
6109
|
// gamepads are updated by engine every frame automatically
|
|
5904
6110
|
function gamepadsUpdate()
|
|
5905
6111
|
{
|
|
@@ -5915,22 +6121,11 @@ function inputUpdate()
|
|
|
5915
6121
|
// update touch gamepad if enabled
|
|
5916
6122
|
if (touchGamepadEnable && isTouchDevice)
|
|
5917
6123
|
{
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
debugCircle(stickCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5925
|
-
debugCircle(buttonCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5926
|
-
if (touchGamepadCenterButtonSize)
|
|
5927
|
-
{
|
|
5928
|
-
debugCircle(startCenter, 2*touchGamepadCenterButtonSize, 'cyan', 0, false, true);
|
|
5929
|
-
// exclusion bubbles around controls (where start is blocked)
|
|
5930
|
-
debugCircle(stickCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
5931
|
-
debugCircle(buttonCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
5932
|
-
}
|
|
5933
|
-
}
|
|
6124
|
+
// a side is either a stick or buttons - setting both is ambiguous
|
|
6125
|
+
ASSERT(!touchGamepadLeftStick || !touchGamepadLeftButtonCount,
|
|
6126
|
+
'set touchGamepadLeftStick or touchGamepadLeftButtonCount, not both');
|
|
6127
|
+
ASSERT(!touchGamepadRightStick || !touchGamepadButtonCount,
|
|
6128
|
+
'set touchGamepadRightStick or touchGamepadButtonCount, not both');
|
|
5934
6129
|
|
|
5935
6130
|
if (!touchGamepadTimer.isSet()) return;
|
|
5936
6131
|
|
|
@@ -5938,23 +6133,24 @@ function inputUpdate()
|
|
|
5938
6133
|
gamepadPrimary = 0; // touch gamepad uses index 0
|
|
5939
6134
|
const sticks = gamepadStickData[0] ?? (gamepadStickData[0] = []);
|
|
5940
6135
|
const dpad = gamepadDpadData[0] ?? (gamepadDpadData[0] = vec2());
|
|
5941
|
-
sticks
|
|
6136
|
+
sticks.length = 0; // only report sticks that are enabled
|
|
5942
6137
|
dpad.set();
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
sticks[0] = applyDeadZones(leftTouchStick);
|
|
5946
|
-
else if (leftTouchStick.lengthSquared() > .3)
|
|
5947
|
-
{
|
|
5948
|
-
// convert to 8 way dpad
|
|
5949
|
-
const x = clamp(round(leftTouchStick.x), -1, 1);
|
|
5950
|
-
const y = clamp(round(leftTouchStick.y), -1, 1);
|
|
5951
|
-
dpad.set(x, -y);
|
|
5952
|
-
sticks[0] = dpad.clampLength(); // clamp to circle
|
|
5953
|
-
}
|
|
5954
|
-
if (touchGamepadButtonCount === 1)
|
|
6138
|
+
// read each side's directional stick (analog, or quantized to an 8 way dpad)
|
|
6139
|
+
for (let side = 0; side < 2; side++)
|
|
5955
6140
|
{
|
|
5956
|
-
|
|
5957
|
-
|
|
6141
|
+
if (!touchGamepadSideStick(side)) continue;
|
|
6142
|
+
const out = touchGamepadStickOut(side);
|
|
6143
|
+
sticks[out] = vec2();
|
|
6144
|
+
const touchStick = touchGamepadSticks[side] ?? vec2();
|
|
6145
|
+
if (touchGamepadAnalog)
|
|
6146
|
+
sticks[out] = applyDeadZones(touchStick);
|
|
6147
|
+
else if (touchStick.lengthSquared() > .3)
|
|
6148
|
+
{
|
|
6149
|
+
const x = clamp(round(touchStick.x), -1, 1);
|
|
6150
|
+
const y = clamp(round(touchStick.y), -1, 1);
|
|
6151
|
+
sticks[out] = vec2(x, -y).clampLength(); // clamp to circle
|
|
6152
|
+
if (!out) dpad.set(x, -y); // the primary (stick 0) also drives the dpad vector
|
|
6153
|
+
}
|
|
5958
6154
|
}
|
|
5959
6155
|
|
|
5960
6156
|
// read virtual gamepad buttons
|
|
@@ -5963,6 +6159,12 @@ function inputUpdate()
|
|
|
5963
6159
|
{
|
|
5964
6160
|
const wasDown = gamepadIsDown(i,0);
|
|
5965
6161
|
data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
6162
|
+
|
|
6163
|
+
// haptic tap when a face button or start button is first pressed (3 = newly down)
|
|
6164
|
+
// skip stick touches (10, 11) so movement doesn't buzz
|
|
6165
|
+
if (touchGamepadVibration && data[i] === 3 &&
|
|
6166
|
+
(i === 9 || touchGamepadIsFaceButton(i)))
|
|
6167
|
+
vibrate(touchGamepadVibration);
|
|
5966
6168
|
}
|
|
5967
6169
|
|
|
5968
6170
|
// disable normal gamepads when touch gamepad is active
|
|
@@ -6026,7 +6228,6 @@ function inputUpdate()
|
|
|
6026
6228
|
gamepadHadInput[i] = true;
|
|
6027
6229
|
if (!gamepadHadInput[gamepadPrimary])
|
|
6028
6230
|
gamepadPrimary = i;
|
|
6029
|
-
isUsingGamepad ||= (gamepadPrimary === i);
|
|
6030
6231
|
}
|
|
6031
6232
|
|
|
6032
6233
|
if (gamepad.mapping === 'standard')
|
|
@@ -6063,80 +6264,476 @@ function inputUpdatePost()
|
|
|
6063
6264
|
function inputRender()
|
|
6064
6265
|
{
|
|
6065
6266
|
touchGamepadRender();
|
|
6267
|
+
}
|
|
6066
6268
|
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
if (!touchInputEnable || !isTouchDevice || headlessMode) return;
|
|
6070
|
-
if (!touchGamepadEnable || !touchGamepadTimer.isSet() && touchGamepadDisplayTime) return;
|
|
6269
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6270
|
+
// Touch gamepad - full-viewport HTML/SVG overlay driven by Pointer Events
|
|
6071
6271
|
|
|
6072
|
-
|
|
6073
|
-
const alpha = touchGamepadDisplayTime ? percent(touchGamepadTimer.get(), touchGamepadDisplayTime+1, touchGamepadDisplayTime) : 1;
|
|
6074
|
-
if (!alpha || paused) return;
|
|
6272
|
+
const touchGamepadSvgNS = 'http://www.w3.org/2000/svg';
|
|
6075
6273
|
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6274
|
+
// build the overlay DOM once; no-op if already built, disabled, headless, or non-touch
|
|
6275
|
+
function touchGamepadInit()
|
|
6276
|
+
{
|
|
6277
|
+
if (touchGamepadOverlay || !touchGamepadEnable || !isTouchDevice || headlessMode ||
|
|
6278
|
+
!document.body) // body may not exist yet; retry on a later frame
|
|
6279
|
+
return;
|
|
6082
6280
|
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6281
|
+
// full-viewport overlay; only the input zones receive pointer events. The
|
|
6282
|
+
// env() padding insets the stage out of notches / home indicators natively.
|
|
6283
|
+
const overlay = touchGamepadOverlay = document.createElement('div');
|
|
6284
|
+
overlay.style.cssText =
|
|
6285
|
+
'position:fixed;inset:0;z-index:50;pointer-events:none;opacity:0;' +
|
|
6286
|
+
'touch-action:none;user-select:none;-webkit-user-select:none;' +
|
|
6287
|
+
'-webkit-touch-callout:none;transition:opacity .2s;box-sizing:border-box;' +
|
|
6288
|
+
'padding:env(safe-area-inset-top) env(safe-area-inset-right) ' +
|
|
6289
|
+
'env(safe-area-inset-bottom) env(safe-area-inset-left)';
|
|
6290
|
+
|
|
6291
|
+
// stage fills the padded (safe-area) content box; all controls live inside it
|
|
6292
|
+
const stage = touchGamepadStage = document.createElement('div');
|
|
6293
|
+
stage.style.cssText = 'position:relative;width:100%;height:100%;pointer-events:none';
|
|
6294
|
+
overlay.appendChild(stage);
|
|
6295
|
+
|
|
6296
|
+
// svg draws every visual and never blocks input
|
|
6297
|
+
const svg = touchGamepadSvg = document.createElementNS(touchGamepadSvgNS, 'svg');
|
|
6298
|
+
svg.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;' +
|
|
6299
|
+
'pointer-events:none;overflow:visible;fill:none;stroke:#fff;stroke-width:3';
|
|
6300
|
+
stage.appendChild(svg);
|
|
6301
|
+
|
|
6302
|
+
// invisible input zones (left stick, right buttons/stick, center start)
|
|
6303
|
+
const makeZone = ()=>
|
|
6304
|
+
{
|
|
6305
|
+
const z = document.createElement('div');
|
|
6306
|
+
z.style.cssText = 'position:absolute;pointer-events:auto;touch-action:none';
|
|
6307
|
+
z.addEventListener('pointerdown', e=> touchGamepadPointerDown(e, z));
|
|
6308
|
+
z.addEventListener('pointermove', e=> touchGamepadPointerMove(e));
|
|
6309
|
+
z.addEventListener('pointerup', e=> touchGamepadPointerUp(e));
|
|
6310
|
+
z.addEventListener('pointercancel', e=> touchGamepadPointerUp(e));
|
|
6311
|
+
stage.appendChild(z);
|
|
6312
|
+
return z;
|
|
6313
|
+
};
|
|
6314
|
+
touchGamepadSideZones[0] = makeZone(); // left
|
|
6315
|
+
touchGamepadSideZones[1] = makeZone(); // right
|
|
6316
|
+
touchGamepadZoneC = makeZone(); // center/start, appended last so it sits above the sides
|
|
6317
|
+
|
|
6318
|
+
addEventListener('resize', ()=> touchGamepadNeedRelayout = true);
|
|
6319
|
+
document.body.appendChild(overlay);
|
|
6320
|
+
touchGamepadNeedRelayout = true;
|
|
6321
|
+
}
|
|
6322
|
+
|
|
6323
|
+
// stage-local size in CSS pixels (excludes safe-area insets)
|
|
6324
|
+
function touchGamepadStageRect() { return touchGamepadStage.getBoundingClientRect(); }
|
|
6325
|
+
|
|
6326
|
+
// per-side touch gamepad config (side 0 = left, 1 = right) - the left and right
|
|
6327
|
+
// sides behave identically, differing only in position and gamepad button indices
|
|
6328
|
+
function touchGamepadSideStick(side)
|
|
6329
|
+
{ return side ? touchGamepadRightStick : touchGamepadLeftStick; }
|
|
6330
|
+
function touchGamepadSideButtonCount(side)
|
|
6331
|
+
{ return side ? touchGamepadButtonCount : touchGamepadLeftButtonCount; }
|
|
6332
|
+
// gamepad button index a side's buttons start at (right 0-3, left 4-7)
|
|
6333
|
+
function touchGamepadSideButtonBase(side)
|
|
6334
|
+
{ return side ? 0 : 4; }
|
|
6335
|
+
// output stick index for a side: the right stick uses stick 0 when there is no left stick
|
|
6336
|
+
function touchGamepadStickOut(side)
|
|
6337
|
+
{ return side && touchGamepadLeftStick ? 1 : 0; }
|
|
6338
|
+
// true if the side has any control (a stick or at least one button)
|
|
6339
|
+
function touchGamepadSideHasControl(side)
|
|
6340
|
+
{ return touchGamepadSideStick(side) || touchGamepadSideButtonCount(side) > 0; }
|
|
6341
|
+
|
|
6342
|
+
// true if gamepad button index i is an active touch gamepad face/single button
|
|
6343
|
+
function touchGamepadIsFaceButton(i)
|
|
6344
|
+
{
|
|
6345
|
+
for (let side = 0; side < 2; side++)
|
|
6346
|
+
{
|
|
6347
|
+
const base = touchGamepadSideButtonBase(side);
|
|
6348
|
+
if (!touchGamepadSideStick(side) &&
|
|
6349
|
+
i >= base && i < base + touchGamepadSideButtonCount(side))
|
|
6350
|
+
return true;
|
|
6351
|
+
}
|
|
6352
|
+
return false;
|
|
6353
|
+
}
|
|
6354
|
+
|
|
6355
|
+
// center of a side's controls in stage-local CSS pixels (stick rest / button cluster)
|
|
6356
|
+
// returns the floating stick anchor when that side is an active floating stick
|
|
6357
|
+
function touchGamepadSideCenter(side, W, H)
|
|
6358
|
+
{
|
|
6359
|
+
if (touchGamepadFloating && touchGamepadSideStick(side) && touchGamepadStickAnchors[side])
|
|
6360
|
+
return touchGamepadStickAnchors[side];
|
|
6361
|
+
let y = H - touchGamepadSize;
|
|
6362
|
+
const count = touchGamepadSideButtonCount(side);
|
|
6363
|
+
if (!touchGamepadSideStick(side) && (count === 2 || count === 3))
|
|
6364
|
+
y -= touchGamepadSize/4; // nudge a 2/3 button cluster up a bit
|
|
6365
|
+
return vec2(side ? W - touchGamepadSize : touchGamepadSize, y);
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
// position the input zones for the current mode and rebuild the SVG visuals
|
|
6369
|
+
function touchGamepadRelayout()
|
|
6370
|
+
{
|
|
6371
|
+
if (!touchGamepadOverlay) return;
|
|
6372
|
+
const r = touchGamepadStageRect();
|
|
6373
|
+
const W = r.width, H = r.height, S = touchGamepadSize;
|
|
6374
|
+
const setZone = (z, css)=> z.style.cssText =
|
|
6375
|
+
'position:absolute;pointer-events:auto;touch-action:none;' + css;
|
|
6376
|
+
|
|
6377
|
+
if (paused)
|
|
6378
|
+
{
|
|
6379
|
+
// the gamepad is hidden while paused, so its side zones must not capture
|
|
6380
|
+
// touches - otherwise they silently steal taps from menus and dialogs
|
|
6381
|
+
for (const zone of touchGamepadSideZones) zone.style.display = 'none';
|
|
6382
|
+
if (touchGamepadCenterButtonSize)
|
|
6089
6383
|
{
|
|
6090
|
-
//
|
|
6091
|
-
|
|
6384
|
+
// any touch presses start
|
|
6385
|
+
setZone(touchGamepadZoneC, 'inset:0');
|
|
6386
|
+
touchGamepadZoneC.style.display = '';
|
|
6092
6387
|
}
|
|
6093
6388
|
else
|
|
6389
|
+
touchGamepadZoneC.style.display = 'none';
|
|
6390
|
+
}
|
|
6391
|
+
else
|
|
6392
|
+
{
|
|
6393
|
+
// position each side zone (left/right differ only by which edge they hug)
|
|
6394
|
+
for (let side = 0; side < 2; side++)
|
|
6094
6395
|
{
|
|
6095
|
-
|
|
6096
|
-
|
|
6396
|
+
const zone = touchGamepadSideZones[side], edge = side ? 'right' : 'left';
|
|
6397
|
+
zone.style.display = touchGamepadSideHasControl(side) ? '' : 'none';
|
|
6398
|
+
if (touchGamepadFloating)
|
|
6097
6399
|
{
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6400
|
+
// bottom 60% grabs the control; the top 40% passes through. A side with no
|
|
6401
|
+
// control on the other side uses the full width (matching the hit-test)
|
|
6402
|
+
const width = touchGamepadSideHasControl(side ? 0 : 1) ? '50%' : '100%';
|
|
6403
|
+
setZone(zone, `${edge}:0;bottom:0;width:${width};height:60%`);
|
|
6101
6404
|
}
|
|
6405
|
+
else // fixed: a compact box hugging the corner control
|
|
6406
|
+
setZone(zone, `${edge}:0;bottom:0;width:${3*S}px;height:${3*S}px`);
|
|
6102
6407
|
}
|
|
6103
|
-
|
|
6104
|
-
|
|
6408
|
+
touchGamepadZoneC.style.display = touchGamepadCenterButtonSize ? '' : 'none';
|
|
6409
|
+
const c = touchGamepadCenterButtonSize;
|
|
6410
|
+
setZone(touchGamepadZoneC,
|
|
6411
|
+
`left:50%;top:50%;width:${2*c}px;height:${2*c}px;transform:translate(-50%,-50%)`);
|
|
6412
|
+
}
|
|
6413
|
+
|
|
6414
|
+
touchGamepadBuildSvg(W, H);
|
|
6415
|
+
touchGamepadNeedRelayout = false;
|
|
6416
|
+
}
|
|
6417
|
+
|
|
6418
|
+
// (re)build the SVG shapes for the current layout; dynamic bits update per-frame
|
|
6419
|
+
function touchGamepadBuildSvg(W, H)
|
|
6420
|
+
{
|
|
6421
|
+
const svg = touchGamepadSvg;
|
|
6422
|
+
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
|
6423
|
+
const els = touchGamepadSvgEls = { face: [], thumb: [] };
|
|
6424
|
+
const S = touchGamepadSize;
|
|
6425
|
+
const circle = (cx, cy, rr, fill)=>
|
|
6426
|
+
{
|
|
6427
|
+
const c = document.createElementNS(touchGamepadSvgNS, 'circle');
|
|
6428
|
+
c.setAttribute('cx', cx); c.setAttribute('cy', cy); c.setAttribute('r', rr);
|
|
6429
|
+
if (fill) c.setAttribute('fill', fill);
|
|
6430
|
+
svg.appendChild(c);
|
|
6431
|
+
return c;
|
|
6432
|
+
};
|
|
6433
|
+
const cross = (ctr)=>
|
|
6434
|
+
{
|
|
6435
|
+
// plus-shaped dpad outline centered at ctr
|
|
6436
|
+
const a = S*.18, b = S*.5, x = ctr.x, y = ctr.y;
|
|
6437
|
+
const p = document.createElementNS(touchGamepadSvgNS, 'path');
|
|
6438
|
+
p.setAttribute('d',
|
|
6439
|
+
`M ${x-a} ${y-b} H ${x+a} V ${y-a} H ${x+b} V ${y+a} H ${x+a} ` +
|
|
6440
|
+
`V ${y+b} H ${x-a} V ${y+a} H ${x-b} V ${y-a} H ${x-a} Z`);
|
|
6441
|
+
svg.appendChild(p);
|
|
6442
|
+
};
|
|
6105
6443
|
|
|
6106
|
-
|
|
6444
|
+
// draw each side: a directional stick, a single large button, or face buttons
|
|
6445
|
+
for (let side = 0; side < 2; side++)
|
|
6446
|
+
{
|
|
6447
|
+
const count = touchGamepadSideButtonCount(side);
|
|
6448
|
+
const base = touchGamepadSideButtonBase(side);
|
|
6449
|
+
const ctr = touchGamepadSideCenter(side, W, H);
|
|
6450
|
+
if (touchGamepadSideStick(side))
|
|
6451
|
+
{
|
|
6452
|
+
// directional stick (circle or cross) with a thumb dot that moves per-frame
|
|
6453
|
+
if (touchGamepadAnalog) circle(ctr.x, ctr.y, S/2); else cross(ctr);
|
|
6454
|
+
els.thumb[side] = circle(ctr.x, ctr.y, S/4, '#fff');
|
|
6455
|
+
}
|
|
6456
|
+
else if (count === 1)
|
|
6457
|
+
els.face[base] = circle(ctr.x, ctr.y, S/2, '#000'); // single large button
|
|
6458
|
+
else for (let i = 0; i < count; i++)
|
|
6459
|
+
{
|
|
6460
|
+
const j = mod(i-1, 4);
|
|
6461
|
+
let button = count > 2 ? j : min(j, count-1);
|
|
6462
|
+
button = button === 3 ? 2 : button === 2 ? 3 : button; // match gamepad layout
|
|
6463
|
+
const offset = vec2().setDirection(j, S/2);
|
|
6464
|
+
if (count === 2) offset.x *= -1;
|
|
6465
|
+
// left side mirrors the right layout's positions, keeping indices in order
|
|
6466
|
+
// (e.g. 2 buttons -> button 4 at bottom, button 5 at left)
|
|
6467
|
+
if (!side) offset.x *= -1;
|
|
6468
|
+
const pos = ctr.add(offset);
|
|
6469
|
+
els.face[base + button] = circle(pos.x, pos.y, S/4, '#000');
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
|
|
6473
|
+
// debug: draw the proximity hit regions the hit-test actually uses
|
|
6474
|
+
if (debug && debugGamepads) touchGamepadBuildDebug(W, H);
|
|
6475
|
+
}
|
|
6476
|
+
|
|
6477
|
+
// draw debug outlines of the touch control hit regions into the overlay svg
|
|
6478
|
+
function touchGamepadBuildDebug(W, H)
|
|
6479
|
+
{
|
|
6480
|
+
const S = touchGamepadSize, svg = touchGamepadSvg;
|
|
6481
|
+
const shape = (tag, attrs, stroke)=>
|
|
6482
|
+
{
|
|
6483
|
+
const el = document.createElementNS(touchGamepadSvgNS, tag);
|
|
6484
|
+
for (const k in attrs) el.setAttribute(k, attrs[k]);
|
|
6485
|
+
el.setAttribute('stroke', stroke);
|
|
6486
|
+
el.setAttribute('stroke-width', 2);
|
|
6487
|
+
el.setAttribute('fill', 'none');
|
|
6488
|
+
svg.appendChild(el);
|
|
6489
|
+
};
|
|
6490
|
+
const ring = (c, rr, stroke)=> shape('circle', {cx:c.x, cy:c.y, r:rr}, stroke);
|
|
6491
|
+
|
|
6492
|
+
// green line: the left/right split that assigns a stick press to a side
|
|
6493
|
+
shape('line', {x1:W/2, y1:0, x2:W/2, y2:H}, '#0f0');
|
|
6494
|
+
|
|
6495
|
+
// cyan: where each side's control can be grabbed
|
|
6496
|
+
for (let side = 0; side < 2; side++)
|
|
6497
|
+
{
|
|
6498
|
+
if (touchGamepadSideStick(side))
|
|
6107
6499
|
{
|
|
6108
|
-
|
|
6109
|
-
const buttonSize = touchGamepadButtonCount > 1 ?
|
|
6110
|
-
touchGamepadSize/4 : touchGamepadSize/2;
|
|
6111
|
-
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
6500
|
+
if (touchGamepadFloating)
|
|
6112
6501
|
{
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
button = button === 3 ? 2 : button === 2 ? 3 : button;
|
|
6118
|
-
const pos = touchGamepadButtonCount < 2 ? buttonCenter :
|
|
6119
|
-
buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
|
|
6120
|
-
context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
|
|
6121
|
-
context.beginPath();
|
|
6122
|
-
context.arc(pos.x, pos.y, buttonSize, 0,9);
|
|
6123
|
-
context.fill();
|
|
6124
|
-
context.stroke();
|
|
6502
|
+
// grab region: this side's half (or the full width if the other side is empty)
|
|
6503
|
+
const top = H*.4, full = !touchGamepadSideHasControl(side ? 0 : 1);
|
|
6504
|
+
const x = full ? 0 : (side ? W/2 : 0);
|
|
6505
|
+
shape('rect', {x, y:top, width:full ? W : W/2, height:H-top}, '#0ff');
|
|
6125
6506
|
}
|
|
6507
|
+
else
|
|
6508
|
+
ring(touchGamepadSideCenter(side, W, H), 2*S, '#0ff');
|
|
6126
6509
|
}
|
|
6510
|
+
else if (touchGamepadSideButtonCount(side) >= 1)
|
|
6511
|
+
ring(touchGamepadSideCenter(side, W, H), S, '#0ff'); // face / single-button radius
|
|
6512
|
+
}
|
|
6127
6513
|
|
|
6128
|
-
|
|
6129
|
-
|
|
6514
|
+
// yellow: start button radius; magenta: where start is blocked (near a control)
|
|
6515
|
+
if (touchGamepadCenterButtonSize)
|
|
6516
|
+
{
|
|
6517
|
+
ring(vec2(W/2, H/2), touchGamepadCenterButtonSize, '#ff0');
|
|
6518
|
+
for (let side = 0; side < 2; side++)
|
|
6519
|
+
if (touchGamepadSideHasControl(side))
|
|
6520
|
+
ring(touchGamepadSideCenter(side, W, H), 2*S, '#f0f');
|
|
6521
|
+
}
|
|
6522
|
+
}
|
|
6523
|
+
|
|
6524
|
+
// per-frame: fade the overlay and move the thumbs / set pressed states
|
|
6525
|
+
function touchGamepadRender()
|
|
6526
|
+
{
|
|
6527
|
+
if (!touchGamepadOverlay || headlessMode) return;
|
|
6528
|
+
|
|
6529
|
+
// hide and bail if disabled at runtime (overlay stays in the DOM for reuse)
|
|
6530
|
+
// display:none also takes the input zones out of hit-testing so touches are
|
|
6531
|
+
// not silently captured away from the game while disabled
|
|
6532
|
+
if (!touchGamepadEnable || !isTouchDevice)
|
|
6533
|
+
{
|
|
6534
|
+
if (touchGamepadOverlay.style.display !== 'none')
|
|
6535
|
+
{
|
|
6536
|
+
// just disabled: hide the overlay and release any held controls
|
|
6537
|
+
touchGamepadOverlay.style.display = 'none';
|
|
6538
|
+
touchGamepadPointerRole.clear();
|
|
6539
|
+
touchGamepadButtons.length = 0;
|
|
6540
|
+
touchGamepadSticks.length = 0;
|
|
6541
|
+
touchGamepadStickPointerId.length = 0;
|
|
6542
|
+
}
|
|
6543
|
+
return;
|
|
6544
|
+
}
|
|
6545
|
+
touchGamepadOverlay.style.display = '';
|
|
6546
|
+
|
|
6547
|
+
// relayout when the paused state, a layout setting, or the debug view changes
|
|
6548
|
+
const dbg = debug && debugGamepads;
|
|
6549
|
+
const layout = [touchGamepadButtonCount, touchGamepadLeftButtonCount, touchGamepadLeftStick,
|
|
6550
|
+
touchGamepadRightStick, touchGamepadAnalog, touchGamepadSize, touchGamepadFloating,
|
|
6551
|
+
touchGamepadCenterButtonSize, paused, dbg].join();
|
|
6552
|
+
if (layout !== touchGamepadLastLayout)
|
|
6553
|
+
{
|
|
6554
|
+
touchGamepadLastLayout = layout;
|
|
6555
|
+
touchGamepadNeedRelayout = true;
|
|
6556
|
+
}
|
|
6557
|
+
// relayout before the visibility bail-out so the paused full-screen start zone applies
|
|
6558
|
+
if (touchGamepadNeedRelayout) touchGamepadRelayout();
|
|
6559
|
+
|
|
6560
|
+
// fade out when idle (always show when displayTime is 0, or while debugging)
|
|
6561
|
+
const fade = touchGamepadDisplayTime ?
|
|
6562
|
+
percent(touchGamepadTimer.get(), touchGamepadDisplayTime+1, touchGamepadDisplayTime) : 1;
|
|
6563
|
+
const visible = dbg || (touchGamepadTimer.isSet() && fade > 0 && !paused);
|
|
6564
|
+
touchGamepadOverlay.style.opacity = !visible ? 0 : dbg ? 1 : fade*touchGamepadAlpha;
|
|
6565
|
+
if (!visible) return;
|
|
6566
|
+
|
|
6567
|
+
const r = touchGamepadStageRect();
|
|
6568
|
+
const W = r.width, H = r.height, S = touchGamepadSize;
|
|
6569
|
+
const els = touchGamepadSvgEls;
|
|
6570
|
+
if (!els) return;
|
|
6571
|
+
|
|
6572
|
+
for (let side = 0; side < 2; side++)
|
|
6573
|
+
if (touchGamepadSideStick(side) && els.thumb[side])
|
|
6574
|
+
{
|
|
6575
|
+
const ctr = touchGamepadSideCenter(side, W, H);
|
|
6576
|
+
const t = ctr.add((touchGamepadSticks[side] ?? vec2()).scale(S/2));
|
|
6577
|
+
els.thumb[side].setAttribute('cx', t.x);
|
|
6578
|
+
els.thumb[side].setAttribute('cy', t.y);
|
|
6579
|
+
}
|
|
6580
|
+
for (let i = 0; i < els.face.length; i++)
|
|
6581
|
+
if (els.face[i])
|
|
6582
|
+
els.face[i].setAttribute('fill', touchGamepadButtons[i] ? '#fff' : '#000');
|
|
6583
|
+
}
|
|
6584
|
+
|
|
6585
|
+
// convert a pointer event to stage-local CSS pixels
|
|
6586
|
+
function touchGamepadEventPos(e)
|
|
6587
|
+
{
|
|
6588
|
+
const r = touchGamepadStageRect();
|
|
6589
|
+
return vec2(e.clientX - r.left, e.clientY - r.top);
|
|
6590
|
+
}
|
|
6591
|
+
|
|
6592
|
+
// set a directional stick from a stage-local point and flag its stick-touch button
|
|
6593
|
+
// (stick 0 press = button 10, stick 1 press = button 11, following the output index)
|
|
6594
|
+
function touchGamepadApplyStick(side, p)
|
|
6595
|
+
{
|
|
6596
|
+
const delta = p.subtract(touchGamepadStickAnchors[side]);
|
|
6597
|
+
touchGamepadSticks[side] = delta.scale(2/touchGamepadSize).clampLength();
|
|
6598
|
+
touchGamepadButtons[touchGamepadStickOut(side) ? 11 : 10] = 1;
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6601
|
+
// pick a side's gamepad button index from a stage-local point, or -1 if outside the cluster
|
|
6602
|
+
function touchGamepadFaceButtonAt(side, p, W, H)
|
|
6603
|
+
{
|
|
6604
|
+
const count = touchGamepadSideButtonCount(side);
|
|
6605
|
+
const base = touchGamepadSideButtonBase(side);
|
|
6606
|
+
const bc = touchGamepadSideCenter(side, W, H);
|
|
6607
|
+
if (bc.distance(p) >= touchGamepadSize) return -1;
|
|
6608
|
+
if (count === 1) return base; // single large button
|
|
6609
|
+
const d = bc.subtract(p);
|
|
6610
|
+
if (!side) d.x *= -1; // left side mirrors the right layout's positions horizontally
|
|
6611
|
+
let button = count === 2 ? (d.x < d.y ? 1 : 0) : mod(d.direction()+2, 4);
|
|
6612
|
+
button = button === 3 ? 2 : button === 2 ? 3 : button; // match gamepad layout
|
|
6613
|
+
return button < count ? base + button : -1;
|
|
6614
|
+
}
|
|
6615
|
+
|
|
6616
|
+
// pick which control a stage-local press activates, by priority then proximity,
|
|
6617
|
+
// independent of which zone element captured it - so overlapping zones on small
|
|
6618
|
+
// screens resolve to the nearest control instead of whichever zone is topmost
|
|
6619
|
+
// returns {role:'stick', side} or {role:'face', btn} or {role:'start'} or undefined
|
|
6620
|
+
function touchGamepadControlAt(p, W, H)
|
|
6621
|
+
{
|
|
6622
|
+
const S = touchGamepadSize;
|
|
6623
|
+
const leftHalf = p.x < W/2;
|
|
6624
|
+
const floatTop = H*.4; // floating grab region is the bottom 60% of the screen
|
|
6625
|
+
|
|
6626
|
+
// check each side (left first for priority); a side is a stick or buttons
|
|
6627
|
+
for (let side = 0; side < 2; side++)
|
|
6628
|
+
{
|
|
6629
|
+
const onHalf = side ? !leftHalf : leftHalf;
|
|
6630
|
+
if (touchGamepadSideStick(side))
|
|
6631
|
+
{
|
|
6632
|
+
// a side with no control on the other side uses the full width
|
|
6633
|
+
const otherControl = touchGamepadSideHasControl(side ? 0 : 1);
|
|
6634
|
+
const grab = touchGamepadFloating ?
|
|
6635
|
+
(!otherControl || onHalf) && p.y > floatTop :
|
|
6636
|
+
onHalf && touchGamepadSideCenter(side, W, H).distance(p) < 2*S;
|
|
6637
|
+
if (grab) return {role:'stick', side};
|
|
6638
|
+
}
|
|
6639
|
+
else if (touchGamepadSideButtonCount(side) >= 1)
|
|
6640
|
+
{
|
|
6641
|
+
const btn = touchGamepadFaceButtonAt(side, p, W, H);
|
|
6642
|
+
if (btn >= 0) return {role:'face', btn};
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6645
|
+
|
|
6646
|
+
// center start button, blocked within 2*size of a control so drift off a
|
|
6647
|
+
// control can't accidentally fire start (matches the original exclusion logic)
|
|
6648
|
+
if (touchGamepadCenterButtonSize)
|
|
6649
|
+
{
|
|
6650
|
+
for (let side = 0; side < 2; side++)
|
|
6651
|
+
if (touchGamepadSideHasControl(side) &&
|
|
6652
|
+
touchGamepadSideCenter(side, W, H).distance(p) < 2*S)
|
|
6653
|
+
return;
|
|
6654
|
+
if (vec2(W/2, H/2).distance(p) < touchGamepadCenterButtonSize)
|
|
6655
|
+
return {role:'start'};
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
|
|
6659
|
+
function touchGamepadPointerDown(e, zone)
|
|
6660
|
+
{
|
|
6661
|
+
if (!touchGamepadEnable) return;
|
|
6662
|
+
e.preventDefault();
|
|
6663
|
+
zone.setPointerCapture(e.pointerId);
|
|
6664
|
+
touchGamepadTimer.set();
|
|
6665
|
+
|
|
6666
|
+
// resume audio on first interaction
|
|
6667
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
6668
|
+
audioContext.resume();
|
|
6669
|
+
|
|
6670
|
+
// while paused, any touch is the start button
|
|
6671
|
+
if (paused)
|
|
6672
|
+
{
|
|
6673
|
+
if (touchGamepadCenterButtonSize)
|
|
6674
|
+
{
|
|
6675
|
+
touchGamepadButtons[9] = 1;
|
|
6676
|
+
touchGamepadPointerRole.set(e.pointerId, 'start');
|
|
6677
|
+
}
|
|
6678
|
+
return;
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
const r = touchGamepadStageRect();
|
|
6682
|
+
const W = r.width, H = r.height;
|
|
6683
|
+
const p = vec2(e.clientX - r.left, e.clientY - r.top);
|
|
6684
|
+
|
|
6685
|
+
// choose the control by proximity/priority, not by which zone captured the touch
|
|
6686
|
+
const hit = touchGamepadControlAt(p, W, H);
|
|
6687
|
+
if (!hit) return;
|
|
6688
|
+
if (hit.role === 'stick')
|
|
6689
|
+
{
|
|
6690
|
+
const side = hit.side;
|
|
6691
|
+
touchGamepadStickAnchors[side] = touchGamepadFloating ? p : touchGamepadSideCenter(side, W, H);
|
|
6692
|
+
touchGamepadStickPointerId[side] = e.pointerId;
|
|
6693
|
+
touchGamepadPointerRole.set(e.pointerId, 'stick'+side);
|
|
6694
|
+
touchGamepadNeedRelayout = true; // base may have re-anchored
|
|
6695
|
+
touchGamepadApplyStick(side, p);
|
|
6696
|
+
}
|
|
6697
|
+
else if (hit.role === 'face')
|
|
6698
|
+
{
|
|
6699
|
+
touchGamepadButtons[hit.btn] = 1;
|
|
6700
|
+
touchGamepadPointerRole.set(e.pointerId, 'face'+hit.btn);
|
|
6701
|
+
}
|
|
6702
|
+
else // 'start'
|
|
6703
|
+
{
|
|
6704
|
+
touchGamepadButtons[9] = 1;
|
|
6705
|
+
touchGamepadPointerRole.set(e.pointerId, 'start');
|
|
6130
6706
|
}
|
|
6131
6707
|
}
|
|
6132
6708
|
|
|
6133
|
-
|
|
6134
|
-
function touchGamepadButtonCenter()
|
|
6709
|
+
function touchGamepadPointerMove(e)
|
|
6135
6710
|
{
|
|
6136
|
-
const
|
|
6137
|
-
if (
|
|
6138
|
-
|
|
6139
|
-
|
|
6711
|
+
const role = touchGamepadPointerRole.get(e.pointerId);
|
|
6712
|
+
if (!role) return;
|
|
6713
|
+
e.preventDefault();
|
|
6714
|
+
const p = touchGamepadEventPos(e);
|
|
6715
|
+
if (role === 'stick0' || role === 'stick1')
|
|
6716
|
+
touchGamepadApplyStick(role === 'stick1' ? 1 : 0, p);
|
|
6717
|
+
// face buttons & start are held until release (no slide-between this pass)
|
|
6718
|
+
}
|
|
6719
|
+
|
|
6720
|
+
function touchGamepadPointerUp(e)
|
|
6721
|
+
{
|
|
6722
|
+
const role = touchGamepadPointerRole.get(e.pointerId);
|
|
6723
|
+
if (!role) return;
|
|
6724
|
+
touchGamepadPointerRole.delete(e.pointerId);
|
|
6725
|
+
if (role === 'stick0' || role === 'stick1')
|
|
6726
|
+
{
|
|
6727
|
+
const side = role === 'stick1' ? 1 : 0;
|
|
6728
|
+
touchGamepadStickPointerId[side] = undefined;
|
|
6729
|
+
touchGamepadSticks[side] = vec2();
|
|
6730
|
+
delete touchGamepadButtons[touchGamepadStickOut(side) ? 11 : 10];
|
|
6731
|
+
}
|
|
6732
|
+
else if (role === 'start')
|
|
6733
|
+
delete touchGamepadButtons[9];
|
|
6734
|
+
else // 'face<n>'
|
|
6735
|
+
delete touchGamepadButtons[+role.slice(4)];
|
|
6736
|
+
touchGamepadTimer.set();
|
|
6140
6737
|
}
|
|
6141
6738
|
/**
|
|
6142
6739
|
* LittleJS Audio System
|
|
@@ -6273,7 +6870,7 @@ class Sound
|
|
|
6273
6870
|
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
6274
6871
|
* @param {boolean} [loop] - Should the sound loop?
|
|
6275
6872
|
* @param {boolean} [paused] - Should the sound start paused
|
|
6276
|
-
* @return {SoundInstance} - The
|
|
6873
|
+
* @return {SoundInstance} - The sound instance, or undefined if sound is disabled, not loaded, or running in headless mode
|
|
6277
6874
|
*/
|
|
6278
6875
|
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
6279
6876
|
{
|
|
@@ -6304,9 +6901,23 @@ class Sound
|
|
|
6304
6901
|
pan = worldToScreen(pos).x * 2/mainCanvas.width - 1;
|
|
6305
6902
|
}
|
|
6306
6903
|
|
|
6307
|
-
// Create
|
|
6904
|
+
// Create sound instance
|
|
6308
6905
|
const rate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
|
|
6309
|
-
|
|
6906
|
+
const instance = new SoundInstance(this, volume, rate, pan, loop, paused);
|
|
6907
|
+
|
|
6908
|
+
if (debug && debugSound && pos)
|
|
6909
|
+
{
|
|
6910
|
+
// visualize where positioned sounds play and their falloff range
|
|
6911
|
+
debugCircle(pos, .5, '#0ff', .5, true);
|
|
6912
|
+
if (this.range)
|
|
6913
|
+
{
|
|
6914
|
+
debugCircle(pos, 2*this.range, '#0ff', .5); // silent radius
|
|
6915
|
+
debugCircle(pos, 2*this.range*this.taper, '#0ff', .5); // full volume radius
|
|
6916
|
+
}
|
|
6917
|
+
debugText('vol '+volume.toFixed(2)+' pitch '+rate.toFixed(2), pos, .5, '#0ff', .5);
|
|
6918
|
+
}
|
|
6919
|
+
|
|
6920
|
+
return instance;
|
|
6310
6921
|
}
|
|
6311
6922
|
|
|
6312
6923
|
/** Play a music track that loops by default
|
|
@@ -6672,6 +7283,10 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
6672
7283
|
// play and return sound
|
|
6673
7284
|
const startOffset = offset * rate;
|
|
6674
7285
|
source.start(0, startOffset);
|
|
7286
|
+
|
|
7287
|
+
if (debug && debugSound)
|
|
7288
|
+
LOG('sound', 'vol', volume.toFixed(2), 'rate', rate.toFixed(2), 'pan', pan.toFixed(2), loop ? 'loop' : '');
|
|
7289
|
+
|
|
6675
7290
|
return source;
|
|
6676
7291
|
}
|
|
6677
7292
|
|
|
@@ -7505,6 +8120,11 @@ class TileCollisionLayer extends TileLayer
|
|
|
7505
8120
|
// check any tiles in the area for collision
|
|
7506
8121
|
const posX = pos.x - this.pos.x;
|
|
7507
8122
|
const posY = pos.y - this.pos.y;
|
|
8123
|
+
// reject AABBs entirely past either edge; without this, the negative
|
|
8124
|
+
// side leaks into row/col 0 because minX/minY clamp to 0 and the
|
|
8125
|
+
// point-test floor below forces maxX/maxY up to 1
|
|
8126
|
+
if (posX + size.x/2 < 0 || posX - size.x/2 > this.size.x) return false;
|
|
8127
|
+
if (posY + size.y/2 < 0 || posY - size.y/2 > this.size.y) return false;
|
|
7508
8128
|
const minX = max(posX - size.x/2|0, 0);
|
|
7509
8129
|
const minY = max(posY - size.y/2|0, 0);
|
|
7510
8130
|
// ensure at least one cell is visited even when size is 0 and pos
|
|
@@ -7623,10 +8243,10 @@ class ParticleEmitter extends EngineObject
|
|
|
7623
8243
|
* @param {number} [particleTime] - How long particles live
|
|
7624
8244
|
* @param {number} [sizeStart] - How big are particles at start
|
|
7625
8245
|
* @param {number} [sizeEnd] - How big are particles at end
|
|
7626
|
-
* @param {number} [speed] - How fast are particles when spawned
|
|
7627
|
-
* @param {number} [angleSpeed] - How fast are particles rotating
|
|
7628
|
-
* @param {number} [damping] - How much to dampen particle speed
|
|
7629
|
-
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
8246
|
+
* @param {number} [speed] - How fast are particles when spawned, in world units per frame (at 60fps, so multiply units/sec by 1/60)
|
|
8247
|
+
* @param {number} [angleSpeed] - How fast are particles rotating, in radians per frame (at 60fps)
|
|
8248
|
+
* @param {number} [damping] - How much to dampen particle speed, per-frame velocity multiplier (1 = no damping, .9 = lose 10% speed each frame)
|
|
8249
|
+
* @param {number} [angleDamping] - How much to dampen particle angular speed, per-frame multiplier (1 = no damping)
|
|
7630
8250
|
* @param {number} [gravityScale] - How much gravity effect particles
|
|
7631
8251
|
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
7632
8252
|
* @param {number} [fadeRate] - Fraction of life spent fading: half at fade-in (start), half at fade-out (end). e.g. .2 = 10% fade-in, 80% full opacity, 10% fade-out
|
|
@@ -7701,13 +8321,13 @@ class ParticleEmitter extends EngineObject
|
|
|
7701
8321
|
this.sizeStart = sizeStart;
|
|
7702
8322
|
/** @property {number} - How big are particles at end */
|
|
7703
8323
|
this.sizeEnd = sizeEnd;
|
|
7704
|
-
/** @property {number} -
|
|
8324
|
+
/** @property {number} - Particle speed when spawned, in world units per frame (at 60fps) */
|
|
7705
8325
|
this.speed = speed;
|
|
7706
|
-
/** @property {number} -
|
|
8326
|
+
/** @property {number} - Particle angular speed when spawned, in radians per frame (at 60fps) */
|
|
7707
8327
|
this.angleSpeed = angleSpeed;
|
|
7708
|
-
/** @property {number} -
|
|
8328
|
+
/** @property {number} - Per-frame velocity multiplier (1 = no damping, .9 = lose 10% speed each frame) */
|
|
7709
8329
|
this.damping = damping;
|
|
7710
|
-
/** @property {number} -
|
|
8330
|
+
/** @property {number} - Per-frame angular velocity multiplier (1 = no damping) */
|
|
7711
8331
|
this.angleDamping = angleDamping;
|
|
7712
8332
|
/** @property {number} - How much gravity affects particles */
|
|
7713
8333
|
this.gravityScale = gravityScale;
|
|
@@ -7776,12 +8396,16 @@ class ParticleEmitter extends EngineObject
|
|
|
7776
8396
|
else if (this.particles.length === 0)
|
|
7777
8397
|
this.destroy(true);
|
|
7778
8398
|
|
|
7779
|
-
// update and remove destroyed particles
|
|
7780
|
-
|
|
8399
|
+
// update and remove destroyed particles in place to avoid per-frame array allocation
|
|
8400
|
+
const particles = this.particles;
|
|
8401
|
+
let alive = 0;
|
|
8402
|
+
for (let i = 0; i < particles.length; ++i)
|
|
7781
8403
|
{
|
|
8404
|
+
const p = particles[i];
|
|
7782
8405
|
p.update();
|
|
7783
|
-
|
|
7784
|
-
}
|
|
8406
|
+
if (!p.destroyed) particles[alive++] = p;
|
|
8407
|
+
}
|
|
8408
|
+
particles.length = alive;
|
|
7785
8409
|
|
|
7786
8410
|
if (debugParticles)
|
|
7787
8411
|
{
|
|
@@ -8053,9 +8677,6 @@ class Particle
|
|
|
8053
8677
|
this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
|
|
8054
8678
|
this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
|
|
8055
8679
|
|
|
8056
|
-
// draw the particle
|
|
8057
|
-
additive && setBlendMode(true);
|
|
8058
|
-
|
|
8059
8680
|
// update the position and angle for drawing
|
|
8060
8681
|
const pos = particleDrawPos.set(this.pos.x, this.pos.y);
|
|
8061
8682
|
let angle = this.angle;
|
|
@@ -8063,16 +8684,19 @@ class Particle
|
|
|
8063
8684
|
{
|
|
8064
8685
|
// in local space of emitter
|
|
8065
8686
|
const a = emitter.angle;
|
|
8066
|
-
const c = cos(a), s = sin(a);
|
|
8687
|
+
const c = cos(-a), s = sin(-a);
|
|
8067
8688
|
pos.set(emitter.pos.x + pos.x*c - pos.y*s,
|
|
8068
8689
|
emitter.pos.y + pos.x*s + pos.y*c);
|
|
8069
8690
|
angle += a;
|
|
8070
8691
|
}
|
|
8692
|
+
|
|
8693
|
+
// draw the particle
|
|
8694
|
+
additive && setAdditiveBlendMode();
|
|
8071
8695
|
if (trailScale)
|
|
8072
8696
|
{
|
|
8073
8697
|
// trail style particles
|
|
8074
|
-
const velocity = localSpace ?
|
|
8075
|
-
this.velocity.rotate(
|
|
8698
|
+
const velocity = localSpace ?
|
|
8699
|
+
this.velocity.rotate(emitter.angle) : this.velocity;
|
|
8076
8700
|
const speed = velocity.length();
|
|
8077
8701
|
if (speed)
|
|
8078
8702
|
{
|
|
@@ -8085,7 +8709,7 @@ class Particle
|
|
|
8085
8709
|
}
|
|
8086
8710
|
else
|
|
8087
8711
|
drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
|
|
8088
|
-
additive &&
|
|
8712
|
+
additive && setAdditiveBlendMode(false);
|
|
8089
8713
|
debugParticles && debugRect(pos, size, '#f005', 0, angle);
|
|
8090
8714
|
}
|
|
8091
8715
|
}
|
|
@@ -9234,7 +9858,15 @@ function medalsInit(saveName)
|
|
|
9234
9858
|
// check if medals are unlocked
|
|
9235
9859
|
medalsSaveName = saveName;
|
|
9236
9860
|
if (!debugMedals)
|
|
9237
|
-
|
|
9861
|
+
{
|
|
9862
|
+
let saved = {};
|
|
9863
|
+
try { saved = JSON.parse(localStorage[saveName] || '{}'); }
|
|
9864
|
+
catch (e) { saved = {}; }
|
|
9865
|
+
medalsForEach(medal => {
|
|
9866
|
+
medal.unlocked = !!(saved[medal.id] && saved[medal.id].unlocked);
|
|
9867
|
+
});
|
|
9868
|
+
medalsSave();
|
|
9869
|
+
}
|
|
9238
9870
|
|
|
9239
9871
|
// engine automatically renders medals
|
|
9240
9872
|
engineAddPlugin(undefined, medalsRender);
|
|
@@ -9278,6 +9910,31 @@ function medalsInit(saveName)
|
|
|
9278
9910
|
function medalsForEach(callback)
|
|
9279
9911
|
{ Object.values(medals).forEach(medal=>callback(medal)); }
|
|
9280
9912
|
|
|
9913
|
+
/** Reset all medals to locked and persist the cleared catalog
|
|
9914
|
+
* @memberof Medals */
|
|
9915
|
+
function medalsReset()
|
|
9916
|
+
{
|
|
9917
|
+
medalsForEach(medal => medal.unlocked = false);
|
|
9918
|
+
medalsSave();
|
|
9919
|
+
}
|
|
9920
|
+
|
|
9921
|
+
function medalsSave()
|
|
9922
|
+
{
|
|
9923
|
+
if (!medalsSaveName) return;
|
|
9924
|
+
const data = {};
|
|
9925
|
+
medalsForEach(medal => {
|
|
9926
|
+
const entry = {
|
|
9927
|
+
name: medal.name,
|
|
9928
|
+
description: medal.description,
|
|
9929
|
+
icon: medal.icon,
|
|
9930
|
+
unlocked: medal.unlocked,
|
|
9931
|
+
};
|
|
9932
|
+
if (medal.image) entry.src = medal.image.src;
|
|
9933
|
+
data[medal.id] = entry;
|
|
9934
|
+
});
|
|
9935
|
+
localStorage[medalsSaveName] = JSON.stringify(data);
|
|
9936
|
+
}
|
|
9937
|
+
|
|
9281
9938
|
///////////////////////////////////////////////////////////////////////////////
|
|
9282
9939
|
|
|
9283
9940
|
/**
|
|
@@ -9335,9 +9992,9 @@ class Medal
|
|
|
9335
9992
|
{
|
|
9336
9993
|
if (medalsPreventUnlock || this.unlocked) return;
|
|
9337
9994
|
|
|
9338
|
-
// save the medal
|
|
9339
9995
|
ASSERT(medalsSaveName, 'save name must be set');
|
|
9340
|
-
|
|
9996
|
+
this.unlocked = true;
|
|
9997
|
+
medalsSave();
|
|
9341
9998
|
medalsDisplayQueue.push(this);
|
|
9342
9999
|
}
|
|
9343
10000
|
|
|
@@ -9395,8 +10052,6 @@ class Medal
|
|
|
9395
10052
|
drawTextScreen(this.icon, pos, size*.7, BLACK);
|
|
9396
10053
|
}
|
|
9397
10054
|
|
|
9398
|
-
// Get local storage key used by the medal
|
|
9399
|
-
storageKey() { return medalsSaveName + '_' + this.id; }
|
|
9400
10055
|
}
|
|
9401
10056
|
|
|
9402
10057
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -9781,6 +10436,335 @@ class PostProcessPlugin
|
|
|
9781
10436
|
}
|
|
9782
10437
|
}
|
|
9783
10438
|
}
|
|
10439
|
+
/**
|
|
10440
|
+
* LittleJS Light System Plugin
|
|
10441
|
+
* - Adds 2D dynamic lighting to the scene
|
|
10442
|
+
* - Lights are first-class EngineObjects (the Light class)
|
|
10443
|
+
* - Each Light draws a soft falloff blob of its color into a shared lightmap
|
|
10444
|
+
* - Lights accumulate ADDITIVELY in the lightmap (red + blue = magenta)
|
|
10445
|
+
* - The lightmap is then MULTIPLIED with the scene during composite, so unlit
|
|
10446
|
+
* areas go to the ambient color and lit areas show the scene tinted by the
|
|
10447
|
+
* accumulated light color
|
|
10448
|
+
* - Draw the world at full brightness — the lightmap does the darkening
|
|
10449
|
+
* - Any EngineObject may override renderLight() to additively contribute to the
|
|
10450
|
+
* lightmap (e.g. emissive lava tiles, weapon flashes, glowing crystals)
|
|
10451
|
+
* - Must be constructed BEFORE PostProcessPlugin so post-process sees lit pixels
|
|
10452
|
+
* @namespace LightSystem
|
|
10453
|
+
*/
|
|
10454
|
+
|
|
10455
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
10456
|
+
|
|
10457
|
+
/** Global Light System plugin object
|
|
10458
|
+
* @type {LightSystemPlugin}
|
|
10459
|
+
* @memberof LightSystem */
|
|
10460
|
+
let lightSystem;
|
|
10461
|
+
|
|
10462
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
10463
|
+
|
|
10464
|
+
/**
|
|
10465
|
+
* LightSystemPlugin
|
|
10466
|
+
* - Owns the offscreen lightmap texture, falloff/composite shaders, and the
|
|
10467
|
+
* per-frame render pass that multiplies the lightmap onto the WebGL scene
|
|
10468
|
+
* - The composite is MULTIPLICATIVE: unlit areas get the ambient color, lit
|
|
10469
|
+
* areas show the scene tinted by the accumulated light color. So you should
|
|
10470
|
+
* draw your world at full brightness — the lightmap handles the darkening.
|
|
10471
|
+
* @memberof LightSystem
|
|
10472
|
+
*/
|
|
10473
|
+
class LightSystemPlugin
|
|
10474
|
+
{
|
|
10475
|
+
/** Create the global light system plugin.
|
|
10476
|
+
* @param {Vector2} [textureSize] - Size of the lightmap texture (defaults to mainCanvasSize)
|
|
10477
|
+
* @param {Color} [ambientColor] - Color applied to unlit areas of the scene (defaults to BLACK = pitch dark). Set a small RGB like rgb(0.1,0.1,0.15) for a faint "moonlight" baseline so unlit areas aren't fully black.
|
|
10478
|
+
* @example
|
|
10479
|
+
* // simplest usage
|
|
10480
|
+
* new LightSystemPlugin();
|
|
10481
|
+
*/
|
|
10482
|
+
constructor(textureSize, ambientColor)
|
|
10483
|
+
{
|
|
10484
|
+
ASSERT(!lightSystem, 'LightSystemPlugin already initialized');
|
|
10485
|
+
ASSERT(!postProcess, 'LightSystemPlugin must be created before PostProcessPlugin');
|
|
10486
|
+
lightSystem = this;
|
|
10487
|
+
|
|
10488
|
+
/** @property {boolean} - When false, the render pass is skipped entirely */
|
|
10489
|
+
this.enabled = true;
|
|
10490
|
+
/** @property {Color} - Baseline color applied to unlit areas of the scene. Defaults to BLACK (pitch dark). Set to a small RGB for a faint ambient. The lightmap is cleared to this color each frame, then lights add on top, then the result multiplies the scene. */
|
|
10491
|
+
this.ambientColor = (ambientColor || BLACK).copy();
|
|
10492
|
+
/** @property {Vector2} - Size of the lightmap texture (set at construction; falls back to mainCanvasSize at init time) */
|
|
10493
|
+
this.textureSize = textureSize ? textureSize.copy() : undefined;
|
|
10494
|
+
|
|
10495
|
+
/** @property {WebGLTexture} - The lightmap texture */
|
|
10496
|
+
this.texture = undefined;
|
|
10497
|
+
/** @property {WebGLProgram} - Shader for drawing per-Light falloff blobs into the lightmap */
|
|
10498
|
+
this.lightShader = undefined;
|
|
10499
|
+
/** @property {WebGLProgram} - Shader for compositing the lightmap over the main scene */
|
|
10500
|
+
this.compositeShader = undefined;
|
|
10501
|
+
/** @property {WebGLVertexArrayObject} - Vertex array object for the light shader */
|
|
10502
|
+
this.lightVAO = undefined;
|
|
10503
|
+
/** @property {WebGLVertexArrayObject} - Vertex array object for the composite shader */
|
|
10504
|
+
this.compositeVAO = undefined;
|
|
10505
|
+
|
|
10506
|
+
initLightSystem();
|
|
10507
|
+
engineAddPlugin(undefined, lightSystemRender,
|
|
10508
|
+
lightSystemContextLost, lightSystemContextRestored);
|
|
10509
|
+
|
|
10510
|
+
function initLightSystem()
|
|
10511
|
+
{
|
|
10512
|
+
if (headlessMode) return;
|
|
10513
|
+
if (!glEnable)
|
|
10514
|
+
{
|
|
10515
|
+
console.warn('LightSystemPlugin: WebGL not enabled!');
|
|
10516
|
+
return;
|
|
10517
|
+
}
|
|
10518
|
+
|
|
10519
|
+
// resolve texture size default at init time (mainCanvasSize may
|
|
10520
|
+
// not be set yet at the moment the constructor first ran)
|
|
10521
|
+
if (!lightSystem.textureSize)
|
|
10522
|
+
lightSystem.textureSize = mainCanvasSize.copy();
|
|
10523
|
+
|
|
10524
|
+
// allocate the lightmap texture with null data at textureSize
|
|
10525
|
+
lightSystem.texture = glContext.createTexture();
|
|
10526
|
+
glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
|
|
10527
|
+
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
|
|
10528
|
+
lightSystem.textureSize.x, lightSystem.textureSize.y, 0,
|
|
10529
|
+
glContext.RGBA, glContext.UNSIGNED_BYTE, null);
|
|
10530
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, glContext.LINEAR);
|
|
10531
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR);
|
|
10532
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, glContext.CLAMP_TO_EDGE);
|
|
10533
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, glContext.CLAMP_TO_EDGE);
|
|
10534
|
+
|
|
10535
|
+
// light falloff shader: one quad per Light, fragment computes radial falloff
|
|
10536
|
+
lightSystem.lightShader = glCreateProgram(
|
|
10537
|
+
'#version 300 es\n' +
|
|
10538
|
+
'precision highp float;'+
|
|
10539
|
+
'uniform mat4 m;'+
|
|
10540
|
+
'uniform vec2 lightPos;'+
|
|
10541
|
+
'uniform float radius;'+
|
|
10542
|
+
'in vec2 g;'+ // unit quad geometry [0..1]
|
|
10543
|
+
'out vec2 vWorldPos;'+
|
|
10544
|
+
'void main(){'+
|
|
10545
|
+
'vec2 worldP=lightPos+(g-.5)*2.*radius;'+
|
|
10546
|
+
'gl_Position=m*vec4(worldP,1,1);'+
|
|
10547
|
+
'vWorldPos=worldP;'+
|
|
10548
|
+
'}'
|
|
10549
|
+
,
|
|
10550
|
+
'#version 300 es\n' +
|
|
10551
|
+
'precision highp float;'+
|
|
10552
|
+
'uniform vec2 lightPos;'+
|
|
10553
|
+
'uniform float radius;'+
|
|
10554
|
+
'uniform float fadeRange;'+
|
|
10555
|
+
'uniform vec4 color;'+
|
|
10556
|
+
'in vec2 vWorldPos;'+
|
|
10557
|
+
'out vec4 c;'+
|
|
10558
|
+
'void main(){'+
|
|
10559
|
+
'float dist=distance(vWorldPos,lightPos);'+
|
|
10560
|
+
'float t=clamp((radius-dist)/max(fadeRange,1e-6),0.,1.);'+
|
|
10561
|
+
'c=vec4(color.rgb*t*color.a,1.);'+
|
|
10562
|
+
'}'
|
|
10563
|
+
);
|
|
10564
|
+
|
|
10565
|
+
// composite shader: fullscreen quad, samples the lightmap
|
|
10566
|
+
lightSystem.compositeShader = glCreateProgram(
|
|
10567
|
+
'#version 300 es\n' +
|
|
10568
|
+
'precision highp float;'+
|
|
10569
|
+
'in vec2 p;'+
|
|
10570
|
+
'void main(){'+
|
|
10571
|
+
'gl_Position=vec4(p+p-1.,1,1);'+
|
|
10572
|
+
'}'
|
|
10573
|
+
,
|
|
10574
|
+
'#version 300 es\n' +
|
|
10575
|
+
'precision highp float;'+
|
|
10576
|
+
'uniform sampler2D s;'+
|
|
10577
|
+
'uniform vec3 iResolution;'+
|
|
10578
|
+
'out vec4 c;'+
|
|
10579
|
+
'void main(){'+
|
|
10580
|
+
'vec2 uv=gl_FragCoord.xy/iResolution.xy;'+
|
|
10581
|
+
'c=vec4(texture(s,uv).rgb,1.);'+
|
|
10582
|
+
'}'
|
|
10583
|
+
);
|
|
10584
|
+
|
|
10585
|
+
// VAO for the per-Light quad — reuses the engine unit triangle-strip
|
|
10586
|
+
lightSystem.lightVAO = glContext.createVertexArray();
|
|
10587
|
+
glContext.bindVertexArray(lightSystem.lightVAO);
|
|
10588
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
10589
|
+
const gLight = glContext.getAttribLocation(lightSystem.lightShader, 'g');
|
|
10590
|
+
glContext.enableVertexAttribArray(gLight);
|
|
10591
|
+
glContext.vertexAttribPointer(gLight, 2, glContext.FLOAT, false, 8, 0);
|
|
10592
|
+
|
|
10593
|
+
// VAO for the composite fullscreen quad — same buffer, attribute named 'p'
|
|
10594
|
+
lightSystem.compositeVAO = glContext.createVertexArray();
|
|
10595
|
+
glContext.bindVertexArray(lightSystem.compositeVAO);
|
|
10596
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
10597
|
+
const pComp = glContext.getAttribLocation(lightSystem.compositeShader, 'p');
|
|
10598
|
+
glContext.enableVertexAttribArray(pComp);
|
|
10599
|
+
glContext.vertexAttribPointer(pComp, 2, glContext.FLOAT, false, 8, 0);
|
|
10600
|
+
}
|
|
10601
|
+
function lightSystemRender()
|
|
10602
|
+
{
|
|
10603
|
+
if (headlessMode || !glEnable) return;
|
|
10604
|
+
if (!lightSystem.enabled) return;
|
|
10605
|
+
if (!lightSystem.texture) return; // init failed or context lost
|
|
10606
|
+
|
|
10607
|
+
// 1. flush any in-flight sprite batch from earlier render passes
|
|
10608
|
+
glFlush();
|
|
10609
|
+
const prevAdditive = glAdditive;
|
|
10610
|
+
|
|
10611
|
+
// 2. bind lightmap as render target, clear to ambientColor
|
|
10612
|
+
const ac = lightSystem.ambientColor;
|
|
10613
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
|
|
10614
|
+
glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
|
|
10615
|
+
glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, lightSystem.texture, 0);
|
|
10616
|
+
glContext.viewport(0, 0, lightSystem.textureSize.x, lightSystem.textureSize.y);
|
|
10617
|
+
glContext.clearColor(ac.r, ac.g, ac.b, ac.a);
|
|
10618
|
+
glContext.clear(glContext.COLOR_BUFFER_BIT);
|
|
10619
|
+
|
|
10620
|
+
// 3. walk engineObjects calling renderLight() — additive blend
|
|
10621
|
+
// (lightmap accumulates raw additive color contributions)
|
|
10622
|
+
setAdditiveBlendMode();
|
|
10623
|
+
glContext.enable(glContext.BLEND);
|
|
10624
|
+
glContext.blendFunc(glContext.ONE, glContext.ONE);
|
|
10625
|
+
|
|
10626
|
+
for (const o of engineObjects)
|
|
10627
|
+
o.destroyed || o.renderLight();
|
|
10628
|
+
|
|
10629
|
+
// 4. drain any sprite-batched draws (e.g. drawTile inside a
|
|
10630
|
+
// custom renderLight override) so they hit the FBO, not the
|
|
10631
|
+
// canvas after we unbind
|
|
10632
|
+
glFlush();
|
|
10633
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
|
|
10634
|
+
glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
|
|
10635
|
+
|
|
10636
|
+
// 5. composite: fullscreen quad, multiplicative blend onto glCanvas
|
|
10637
|
+
// (scene * lightmap — unlit areas go to black, lit areas are
|
|
10638
|
+
// the scene tinted by the accumulated light color)
|
|
10639
|
+
glContext.useProgram(lightSystem.compositeShader);
|
|
10640
|
+
glContext.bindVertexArray(lightSystem.compositeVAO);
|
|
10641
|
+
glContext.activeTexture(glContext.TEXTURE0);
|
|
10642
|
+
glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
|
|
10643
|
+
const cs = lightSystem.compositeShader;
|
|
10644
|
+
glContext.uniform1i(glContext.getUniformLocation(cs, 's'), 0);
|
|
10645
|
+
glContext.uniform3f(glContext.getUniformLocation(cs, 'iResolution'),
|
|
10646
|
+
mainCanvas.width, mainCanvas.height, 1);
|
|
10647
|
+
glContext.blendFunc(glContext.DST_COLOR, glContext.ZERO);
|
|
10648
|
+
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
|
|
10649
|
+
|
|
10650
|
+
// 6. restore engine state so subsequent draws use the engine's
|
|
10651
|
+
// tracked texture binding (otherwise glSetTexture would think
|
|
10652
|
+
// the prior texture was still bound when actually the lightmap
|
|
10653
|
+
// is, and any debug text / future draw could sample the lightmap)
|
|
10654
|
+
if (glActiveTexture)
|
|
10655
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
10656
|
+
setAdditiveBlendMode(prevAdditive);
|
|
10657
|
+
glSetInstancedMode(true);
|
|
10658
|
+
}
|
|
10659
|
+
function lightSystemContextLost()
|
|
10660
|
+
{
|
|
10661
|
+
lightSystem.texture = undefined;
|
|
10662
|
+
lightSystem.lightShader = undefined;
|
|
10663
|
+
lightSystem.compositeShader = undefined;
|
|
10664
|
+
lightSystem.lightVAO = undefined;
|
|
10665
|
+
lightSystem.compositeVAO = undefined;
|
|
10666
|
+
LOG('LightSystemPlugin: WebGL context lost');
|
|
10667
|
+
}
|
|
10668
|
+
function lightSystemContextRestored()
|
|
10669
|
+
{
|
|
10670
|
+
initLightSystem();
|
|
10671
|
+
LOG('LightSystemPlugin: WebGL context restored');
|
|
10672
|
+
}
|
|
10673
|
+
}
|
|
10674
|
+
|
|
10675
|
+
/** Draw a single Light's falloff blob into the currently bound lightmap.
|
|
10676
|
+
* Called by Light.renderLight() during the plugin's render pass.
|
|
10677
|
+
* @param {Light} light */
|
|
10678
|
+
drawLight(light)
|
|
10679
|
+
{
|
|
10680
|
+
if (headlessMode || !glEnable || !this.lightShader) return;
|
|
10681
|
+
|
|
10682
|
+
// drain any sprite-batched draws queued by a previous custom
|
|
10683
|
+
// renderLight() override (e.g. drawRect inside a LavaTile). They were
|
|
10684
|
+
// queued in the engine's instanced-vertex format and must flush with
|
|
10685
|
+
// the engine's shader+VAO bound — NOT this plugin's light shader.
|
|
10686
|
+
glFlush();
|
|
10687
|
+
|
|
10688
|
+
glContext.useProgram(this.lightShader);
|
|
10689
|
+
glContext.bindVertexArray(this.lightVAO);
|
|
10690
|
+
|
|
10691
|
+
// re-apply the engine camera transform onto this shader. Divide by
|
|
10692
|
+
// mainCanvasSize (not textureSize) so world→NDC matches the main
|
|
10693
|
+
// pass; the viewport handles the lightmap's actual resolution.
|
|
10694
|
+
// No y-flip here: the composite samples this FBO with
|
|
10695
|
+
// gl_FragCoord/iResolution (origin bottom-left), so storing world
|
|
10696
|
+
// +Y at the top of the texture lines up with the canvas convention.
|
|
10697
|
+
const s = vec2(2*cameraScale).divide(mainCanvasSize);
|
|
10698
|
+
const rotatedCam = cameraPos.rotate(-cameraAngle);
|
|
10699
|
+
const p = vec2(-1).subtract(rotatedCam.multiply(s));
|
|
10700
|
+
const ca = cos(cameraAngle);
|
|
10701
|
+
const sa = sin(cameraAngle);
|
|
10702
|
+
const transform = [
|
|
10703
|
+
s.x * ca, s.y * sa, 0, 0,
|
|
10704
|
+
-s.x * sa, s.y * ca, 0, 0,
|
|
10705
|
+
1, 1, 1, 0,
|
|
10706
|
+
p.x, p.y, 0, 1];
|
|
10707
|
+
|
|
10708
|
+
const ls = this.lightShader;
|
|
10709
|
+
glContext.uniformMatrix4fv(glContext.getUniformLocation(ls, 'm'), false, transform);
|
|
10710
|
+
glContext.uniform2f(glContext.getUniformLocation(ls, 'lightPos'), light.pos.x, light.pos.y);
|
|
10711
|
+
glContext.uniform1f(glContext.getUniformLocation(ls, 'radius'), light.radius);
|
|
10712
|
+
glContext.uniform1f(glContext.getUniformLocation(ls, 'fadeRange'), light.fadeRange);
|
|
10713
|
+
const c = light.color;
|
|
10714
|
+
glContext.uniform4f(glContext.getUniformLocation(ls, 'color'), c.r, c.g, c.b, c.a);
|
|
10715
|
+
|
|
10716
|
+
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
|
|
10717
|
+
|
|
10718
|
+
// restore engine's instanced shader+VAO so subsequent renderLight()
|
|
10719
|
+
// overrides that batch through drawRect/drawTile work correctly
|
|
10720
|
+
glSetInstancedMode(true);
|
|
10721
|
+
}
|
|
10722
|
+
}
|
|
10723
|
+
|
|
10724
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
10725
|
+
|
|
10726
|
+
/**
|
|
10727
|
+
* A Light is an EngineObject that contributes a soft additive blob of color
|
|
10728
|
+
* to the LightSystem plugin's lightmap.
|
|
10729
|
+
* @extends EngineObject
|
|
10730
|
+
* @memberof LightSystem
|
|
10731
|
+
* @example
|
|
10732
|
+
* new Light(vec2(5, 5), 4, rgb(1, 0.5, 0)); // orange light, full soft blob
|
|
10733
|
+
* new Light(vec2(0, 0), 8, rgb(1, 1, 1), 2); // white core with 2-unit soft halo
|
|
10734
|
+
*/
|
|
10735
|
+
class Light extends EngineObject
|
|
10736
|
+
{
|
|
10737
|
+
/** Create a light object and add it to the engine object list
|
|
10738
|
+
* @param {Vector2} pos - World space position
|
|
10739
|
+
* @param {number} radius - Total extent of the light in world units
|
|
10740
|
+
* @param {Color} [color] - Color of the light; alpha modulates intensity
|
|
10741
|
+
* @param {number} [fadeRange] - Width of the soft edge in world units (defaults to radius) */
|
|
10742
|
+
constructor(pos, radius, color, fadeRange)
|
|
10743
|
+
{
|
|
10744
|
+
super(pos, vec2(1), undefined, 0, color);
|
|
10745
|
+
ASSERT(isNumber(radius) && radius >= 0, 'Light radius must be a non-negative number');
|
|
10746
|
+
ASSERT(fadeRange === undefined || (isNumber(fadeRange) && fadeRange >= 0),
|
|
10747
|
+
'Light fadeRange must be a non-negative number when provided');
|
|
10748
|
+
|
|
10749
|
+
/** @property {number} - Total extent of the light in world units */
|
|
10750
|
+
this.radius = radius;
|
|
10751
|
+
/** @property {number} - Width of the soft edge in world units */
|
|
10752
|
+
this.fadeRange = fadeRange === undefined ? radius : fadeRange;
|
|
10753
|
+
}
|
|
10754
|
+
|
|
10755
|
+
/** Lights are invisible in the main render pass — they only contribute
|
|
10756
|
+
* to the lightmap via renderLight(). */
|
|
10757
|
+
render() {}
|
|
10758
|
+
|
|
10759
|
+
/** Draw this light's falloff blob into the lightmap.
|
|
10760
|
+
* Called by LightSystemPlugin during its render pass. No-op when the
|
|
10761
|
+
* plugin or WebGL is unavailable. */
|
|
10762
|
+
renderLight()
|
|
10763
|
+
{
|
|
10764
|
+
lightSystem && lightSystem.drawLight(this);
|
|
10765
|
+
}
|
|
10766
|
+
}
|
|
10767
|
+
|
|
9784
10768
|
/**
|
|
9785
10769
|
* LittleJS ZzFXM Plugin
|
|
9786
10770
|
* @namespace ZzFXM
|
|
@@ -10078,7 +11062,7 @@ class UISystemPlugin
|
|
|
10078
11062
|
let targetPos, targetSize;
|
|
10079
11063
|
if (o.parent)
|
|
10080
11064
|
{
|
|
10081
|
-
targetPos = o.parent.
|
|
11065
|
+
targetPos = o.parent.nativePos;
|
|
10082
11066
|
targetSize = o.parent.size;
|
|
10083
11067
|
}
|
|
10084
11068
|
else
|
|
@@ -10092,7 +11076,7 @@ class UISystemPlugin
|
|
|
10092
11076
|
}
|
|
10093
11077
|
|
|
10094
11078
|
const a = o.anchor;
|
|
10095
|
-
o.
|
|
11079
|
+
o.nativePos = targetPos
|
|
10096
11080
|
.add(targetSize.multiply(a).scale(.5)) // anchor point on target
|
|
10097
11081
|
.subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
|
|
10098
11082
|
.add(o.localPos); // user offset
|
|
@@ -10198,13 +11182,18 @@ class UISystemPlugin
|
|
|
10198
11182
|
|
|
10199
11183
|
function updateObject(o)
|
|
10200
11184
|
{
|
|
10201
|
-
if (!o.visible) return;
|
|
11185
|
+
if (o.destroyed || !o.visible) return;
|
|
10202
11186
|
|
|
10203
11187
|
// update in reverse order to detect mouse enter/leave
|
|
10204
11188
|
updateTransforms(o);
|
|
10205
11189
|
for (let i=o.children.length; i--;)
|
|
10206
|
-
|
|
10207
|
-
|
|
11190
|
+
{
|
|
11191
|
+
// a child may destroy siblings mid-update (e.g. dialog close)
|
|
11192
|
+
const child = o.children[i];
|
|
11193
|
+
child && updateObject(child);
|
|
11194
|
+
}
|
|
11195
|
+
if (!o.destroyed)
|
|
11196
|
+
o.update();
|
|
10208
11197
|
}
|
|
10209
11198
|
}
|
|
10210
11199
|
function uiRender()
|
|
@@ -10628,10 +11617,15 @@ class UIObject
|
|
|
10628
11617
|
ASSERT(isVector2(pos), 'ui object pos must be a vec2');
|
|
10629
11618
|
ASSERT(isVector2(size), 'ui object size must be a vec2');
|
|
10630
11619
|
|
|
10631
|
-
/** @property {Vector2} -
|
|
11620
|
+
/** @property {Vector2} - Position you set: an offset from this object's
|
|
11621
|
+
* anchor point (the parent box, or the canvas for roots). This is the
|
|
11622
|
+
* input that controls placement — set this, not nativePos. */
|
|
10632
11623
|
this.localPos = pos.copy();
|
|
10633
|
-
/** @property {Vector2} -
|
|
10634
|
-
|
|
11624
|
+
/** @property {Vector2} - Resolved position in native UI space, recomputed
|
|
11625
|
+
* every frame from localPos + anchor (and nativeHeight, if set). This is a
|
|
11626
|
+
* derived output used for drawing and hit-testing; assigning to it has no
|
|
11627
|
+
* effect since it is overwritten each frame. Set localPos instead. */
|
|
11628
|
+
this.nativePos = pos.copy();
|
|
10635
11629
|
/** @property {Vector2} - Screen space size of the object */
|
|
10636
11630
|
this.size = size.copy();
|
|
10637
11631
|
/** @property {Color} - Color of the object */
|
|
@@ -10766,7 +11760,7 @@ class UIObject
|
|
|
10766
11760
|
const size = !isTouchDevice ? this.size :
|
|
10767
11761
|
this.size.add(vec2(this.extraTouchSize || 0));
|
|
10768
11762
|
const pos = uiSystem.screenToNative(mousePosScreen);
|
|
10769
|
-
return isOverlapping(this.
|
|
11763
|
+
return isOverlapping(this.nativePos, size, pos);
|
|
10770
11764
|
}
|
|
10771
11765
|
|
|
10772
11766
|
/** Update the object, called automatically by plugin once each frame */
|
|
@@ -10856,7 +11850,7 @@ class UIObject
|
|
|
10856
11850
|
this.color : this.color;
|
|
10857
11851
|
const lineWidth = this.lineWidth * (isNavigationObject ? 1.5 : 1);
|
|
10858
11852
|
|
|
10859
|
-
uiSystem.drawRect(this.
|
|
11853
|
+
uiSystem.drawRect(this.nativePos, this.size, color, lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
10860
11854
|
}
|
|
10861
11855
|
|
|
10862
11856
|
/** Get the size for text with overrides and scale
|
|
@@ -10893,8 +11887,8 @@ class UIObject
|
|
|
10893
11887
|
let text = 'type = ' + this.constructor.name;
|
|
10894
11888
|
if (this.text)
|
|
10895
11889
|
text += '\ntext = ' + this.text;
|
|
10896
|
-
if (this.
|
|
10897
|
-
text += '\
|
|
11890
|
+
if (this.nativePos.x || this.nativePos.y)
|
|
11891
|
+
text += '\nnativePos = ' + this.nativePos;
|
|
10898
11892
|
if (this.localPos.x || this.localPos.y)
|
|
10899
11893
|
text += '\nlocalPos = ' + this.localPos;
|
|
10900
11894
|
if (this.size.x || this.size.y)
|
|
@@ -10914,7 +11908,7 @@ class UIObject
|
|
|
10914
11908
|
this.isHoverObject() ? YELLOW :
|
|
10915
11909
|
this.disabled ? PURPLE :
|
|
10916
11910
|
this.interactive ? RED : BLUE;
|
|
10917
|
-
uiSystem.drawRect(this.
|
|
11911
|
+
uiSystem.drawRect(this.nativePos, this.size, CLEAR_BLACK, 4, color);
|
|
10918
11912
|
}
|
|
10919
11913
|
|
|
10920
11914
|
/** Internal function called when object is clicked
|
|
@@ -10997,7 +11991,7 @@ class UIText extends UIObject
|
|
|
10997
11991
|
|
|
10998
11992
|
// render the text
|
|
10999
11993
|
const textSize = this.getTextSize();
|
|
11000
|
-
uiSystem.drawText(this.text, this.
|
|
11994
|
+
uiSystem.drawText(this.text, this.nativePos, textSize, this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
11001
11995
|
}
|
|
11002
11996
|
}
|
|
11003
11997
|
|
|
@@ -11092,7 +12086,7 @@ class UITextInput extends UIObject
|
|
|
11092
12086
|
let text = this.text;
|
|
11093
12087
|
if (this.isKeyInputObject()) // add a cursor to end of text
|
|
11094
12088
|
text += timeReal%1 < .5 ? '█' : '░';
|
|
11095
|
-
uiSystem.drawText(text, this.
|
|
12089
|
+
uiSystem.drawText(text, this.nativePos, textSize,
|
|
11096
12090
|
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
11097
12091
|
}
|
|
11098
12092
|
}
|
|
@@ -11135,7 +12129,7 @@ class UITile extends UIObject
|
|
|
11135
12129
|
}
|
|
11136
12130
|
render()
|
|
11137
12131
|
{
|
|
11138
|
-
uiSystem.drawTile(this.
|
|
12132
|
+
uiSystem.drawTile(this.nativePos, this.size, this.tileInfo, this.color, this.angle, this.mirror, this.shadowColor, this.shadowBlur, this.shadowOffset);
|
|
11139
12133
|
}
|
|
11140
12134
|
}
|
|
11141
12135
|
|
|
@@ -11174,7 +12168,7 @@ class UIButton extends UIObject
|
|
|
11174
12168
|
|
|
11175
12169
|
// draw the text scaled to fit
|
|
11176
12170
|
const textSize = this.getTextSize();
|
|
11177
|
-
uiSystem.drawText(this.text, this.
|
|
12171
|
+
uiSystem.drawText(this.text, this.nativePos.add(this.textOffset), textSize,
|
|
11178
12172
|
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
11179
12173
|
}
|
|
11180
12174
|
}
|
|
@@ -11222,13 +12216,13 @@ class UICheckbox extends UIObject
|
|
|
11222
12216
|
const p = this.cornerRadius / min(this.size.x, this.size.y) * 2;
|
|
11223
12217
|
const length = lerp(1, 2**.5/2, p) / 2;
|
|
11224
12218
|
let s = this.size.scale(length);
|
|
11225
|
-
uiSystem.drawLine(this.
|
|
11226
|
-
uiSystem.drawLine(this.
|
|
12219
|
+
uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1))), this.nativePos.add(s.multiply(vec2(1))), this.lineWidth, this.lineColor);
|
|
12220
|
+
uiSystem.drawLine(this.nativePos.add(s.multiply(vec2(-1,1))), this.nativePos.add(s.multiply(vec2(1,-1))), this.lineWidth, this.lineColor);
|
|
11227
12221
|
}
|
|
11228
12222
|
|
|
11229
12223
|
// draw the text next to the checkbox
|
|
11230
12224
|
const textSize = this.getTextSize();
|
|
11231
|
-
const pos = this.
|
|
12225
|
+
const pos = this.nativePos.add(vec2(this.size.x,0));
|
|
11232
12226
|
uiSystem.drawText(this.text, pos, textSize,
|
|
11233
12227
|
this.textColor, this.textLineWidth, this.textLineColor, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
11234
12228
|
}
|
|
@@ -11284,7 +12278,7 @@ class UISlider extends UIObject
|
|
|
11284
12278
|
const isHorizontal = this.size.x > this.size.y;
|
|
11285
12279
|
const handleSize = isHorizontal ? this.size.y : this.size.x;
|
|
11286
12280
|
const barSize = isHorizontal ? this.size.x : this.size.y;
|
|
11287
|
-
const centerPos = isHorizontal ? this.
|
|
12281
|
+
const centerPos = isHorizontal ? this.nativePos.x : this.nativePos.y;
|
|
11288
12282
|
|
|
11289
12283
|
// check if value changed
|
|
11290
12284
|
const handleWidth = barSize - handleSize;
|
|
@@ -11318,7 +12312,7 @@ class UISlider extends UIObject
|
|
|
11318
12312
|
const minWidth = min(handleWidth, this.cornerRadius * 2);
|
|
11319
12313
|
const progressWidth = lerp(minWidth, barWidth, this.value);
|
|
11320
12314
|
const p = (progressWidth - barWidth) * (isHorizontal ? .5 : -.5);
|
|
11321
|
-
const pos = this.
|
|
12315
|
+
const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
|
|
11322
12316
|
const color = this.disabled ? this.disabledColor : this.handleColor;
|
|
11323
12317
|
const drawSize = isHorizontal ?
|
|
11324
12318
|
vec2(progressWidth, this.size.y) : vec2(this.size.x, progressWidth);
|
|
@@ -11329,7 +12323,7 @@ class UISlider extends UIObject
|
|
|
11329
12323
|
// draw the slider handle
|
|
11330
12324
|
const value = clamp(isHorizontal ? this.value : 1 - this.value);
|
|
11331
12325
|
const p = (barWidth - handleWidth) * (value - .5);
|
|
11332
|
-
const pos = this.
|
|
12326
|
+
const pos = this.nativePos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
|
|
11333
12327
|
const color = this.disabled ? this.disabledColor : this.handleColor;
|
|
11334
12328
|
const drawSize = vec2(handleWidth);
|
|
11335
12329
|
uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
|
|
@@ -11337,7 +12331,7 @@ class UISlider extends UIObject
|
|
|
11337
12331
|
|
|
11338
12332
|
// draw the text scaled to fit on the slider
|
|
11339
12333
|
const textSize = this.getTextSize();
|
|
11340
|
-
uiSystem.drawText(this.text, this.
|
|
12334
|
+
uiSystem.drawText(this.text, this.nativePos, textSize,
|
|
11341
12335
|
this.textColor, this.textLineWidth, this.textLineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
11342
12336
|
}
|
|
11343
12337
|
navigatePressed()
|
|
@@ -11476,7 +12470,7 @@ class UIVideo extends UIObject
|
|
|
11476
12470
|
const context = uiSystem.uiContext;
|
|
11477
12471
|
const s = this.size;
|
|
11478
12472
|
context.save();
|
|
11479
|
-
context.translate(this.
|
|
12473
|
+
context.translate(this.nativePos.x, this.nativePos.y);
|
|
11480
12474
|
context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
|
|
11481
12475
|
context.restore();
|
|
11482
12476
|
}
|
|
@@ -11828,23 +12822,19 @@ class Box2dObject extends EngineObject
|
|
|
11828
12822
|
|
|
11829
12823
|
function box2dCreatePolygonShape(points)
|
|
11830
12824
|
{
|
|
11831
|
-
|
|
12825
|
+
ASSERT(3 <= points.length && points.length <= 8);
|
|
12826
|
+
const buffer = box2d.instance._malloc(points.length * 8);
|
|
12827
|
+
for (let i=0, offset=0; i<points.length; ++i)
|
|
11832
12828
|
{
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
offset += 4;
|
|
11838
|
-
box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
|
|
11839
|
-
offset += 4;
|
|
11840
|
-
}
|
|
11841
|
-
return box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
|
|
12829
|
+
box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x;
|
|
12830
|
+
offset += 4;
|
|
12831
|
+
box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
|
|
12832
|
+
offset += 4;
|
|
11842
12833
|
}
|
|
11843
|
-
|
|
11844
|
-
ASSERT(3 <= points.length && points.length <= 8);
|
|
12834
|
+
const box2dPoints = box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
|
|
11845
12835
|
const shape = new box2d.instance.b2PolygonShape();
|
|
11846
|
-
const box2dPoints = box2dCreatePointList(points);
|
|
11847
12836
|
shape.Set(box2dPoints, points.length);
|
|
12837
|
+
box2d.instance._free(buffer);
|
|
11848
12838
|
return shape;
|
|
11849
12839
|
}
|
|
11850
12840
|
|
|
@@ -13709,8 +14699,8 @@ async function box2dInit()
|
|
|
13709
14699
|
* This function can not apply color because it draws using the 2d context
|
|
13710
14700
|
* @param {Vector2} pos - Screen space position
|
|
13711
14701
|
* @param {Vector2} size - Screen space size
|
|
13712
|
-
* @param {TileInfo} startTile -
|
|
13713
|
-
* @param {number} [borderSize] -
|
|
14702
|
+
* @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample (see drawNineSlice)
|
|
14703
|
+
* @param {number} [borderSize] - Rendered thickness of the border sections
|
|
13714
14704
|
* @param {number} [extraSpace] - Extra spacing adjustment
|
|
13715
14705
|
* @param {number} [angle] - Angle to rotate by
|
|
13716
14706
|
* @memberof DrawUtilities */
|
|
@@ -13721,11 +14711,16 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
|
|
|
13721
14711
|
|
|
13722
14712
|
/** Draw a scalable nine-slice UI element in world space
|
|
13723
14713
|
* This function can apply color and additive color if WebGL is enabled
|
|
14714
|
+
* The nine-slice samples a 3x3 block of tiles from the tilesheet, it does not
|
|
14715
|
+
* subdivide a single tile. Pass the top-left tile of that block as startTile;
|
|
14716
|
+
* the other 8 tiles (edges, corners, and center) are taken automatically from
|
|
14717
|
+
* the 3x3 grid of tiles extending right and down from it. borderSize only sets
|
|
14718
|
+
* the rendered thickness of the edges and corners, not how the texture is cut.
|
|
13724
14719
|
* @param {Vector2} pos - World space position
|
|
13725
14720
|
* @param {Vector2} size - World space size
|
|
13726
|
-
* @param {TileInfo} startTile -
|
|
14721
|
+
* @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample the nine-slice from
|
|
13727
14722
|
* @param {Color} [color] - Color to modulate with
|
|
13728
|
-
* @param {number} [borderSize] -
|
|
14723
|
+
* @param {number} [borderSize] - Rendered thickness of the border sections
|
|
13729
14724
|
* @param {Color} [additiveColor] - Additive color
|
|
13730
14725
|
* @param {number} [extraSpace] - Extra spacing adjustment
|
|
13731
14726
|
* @param {number} [angle] - Angle to rotate by
|
|
@@ -13735,7 +14730,8 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
|
|
|
13735
14730
|
* @memberof DrawUtilities */
|
|
13736
14731
|
function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
|
|
13737
14732
|
{
|
|
13738
|
-
// setup nine slice tiles
|
|
14733
|
+
// setup nine slice tiles - startTile is the top-left of a 3x3 tile block,
|
|
14734
|
+
// so the center tile is one tile down and right from it
|
|
13739
14735
|
const centerTile = startTile.offset(startTile.size);
|
|
13740
14736
|
const centerSize = size.add(vec2(extraSpace-borderSize*2));
|
|
13741
14737
|
const cornerSize = vec2(borderSize);
|
|
@@ -13769,8 +14765,8 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
|
|
|
13769
14765
|
* This function can not apply color because it draws using the 2d context
|
|
13770
14766
|
* @param {Vector2} pos - Screen space position
|
|
13771
14767
|
* @param {Vector2} size - Screen space size
|
|
13772
|
-
* @param {TileInfo} startTile -
|
|
13773
|
-
* @param {number} [borderSize] -
|
|
14768
|
+
* @param {TileInfo} startTile - First of 3 consecutive tiles: corner, side, center (see drawThreeSlice)
|
|
14769
|
+
* @param {number} [borderSize] - Rendered thickness of the border sections
|
|
13774
14770
|
* @param {number} [extraSpace] - Extra spacing adjustment
|
|
13775
14771
|
* @param {number} [angle] - Angle to rotate by
|
|
13776
14772
|
* @memberof DrawUtilities */
|
|
@@ -13781,11 +14777,15 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
|
|
|
13781
14777
|
|
|
13782
14778
|
/** Draw a scalable three-slice UI element in world space
|
|
13783
14779
|
* This function can apply color and additive color if WebGL is enabled
|
|
14780
|
+
* The three-slice samples 3 consecutive tiles from the tilesheet, it does not
|
|
14781
|
+
* subdivide a single tile. Pass the first tile as startTile; the three tiles
|
|
14782
|
+
* are used in order as corner, side, and center, then rotated and mirrored to
|
|
14783
|
+
* build all four edges and corners. borderSize only sets the rendered thickness.
|
|
13784
14784
|
* @param {Vector2} pos - World space position
|
|
13785
14785
|
* @param {Vector2} size - World space size
|
|
13786
|
-
* @param {TileInfo} startTile -
|
|
14786
|
+
* @param {TileInfo} startTile - First of 3 consecutive tiles (corner, side, center) for the three-slice
|
|
13787
14787
|
* @param {Color} [color] - Color to modulate with
|
|
13788
|
-
* @param {number} [borderSize] -
|
|
14788
|
+
* @param {number} [borderSize] - Rendered thickness of the border sections
|
|
13789
14789
|
* @param {Color} [additiveColor] - Additive color
|
|
13790
14790
|
* @param {number} [extraSpace] - Extra spacing adjustment
|
|
13791
14791
|
* @param {number} [angle] - Angle to rotate by
|
|
@@ -13795,7 +14795,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
|
|
|
13795
14795
|
* @memberof DrawUtilities */
|
|
13796
14796
|
function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
|
|
13797
14797
|
{
|
|
13798
|
-
// setup three slice tiles
|
|
14798
|
+
// setup three slice tiles - 3 tiles in a row starting at startTile
|
|
13799
14799
|
const cornerTile = startTile.frame(0);
|
|
13800
14800
|
const sideTile = startTile.frame(1);
|
|
13801
14801
|
const centerTile = startTile.frame(2);
|
|
@@ -13825,6 +14825,70 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
|
|
|
13825
14825
|
const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip));
|
|
13826
14826
|
drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
|
|
13827
14827
|
}
|
|
14828
|
+
}
|
|
14829
|
+
|
|
14830
|
+
/** Draw a crescent / moon-phase shape built from a polygon
|
|
14831
|
+
* Routes through drawPoly, so it supports WebGL, screen space, color, and outlines
|
|
14832
|
+
* @param {Vector2} pos - Center position
|
|
14833
|
+
* @param {number} [size] - Diameter
|
|
14834
|
+
* @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
|
|
14835
|
+
* @param {Color} [color] - Fill color
|
|
14836
|
+
* @param {number} [angle] - Angle to rotate by
|
|
14837
|
+
* @param {boolean} [invert] - Flip which side is illuminated
|
|
14838
|
+
* @param {number} [lineWidth] - Outline width, 0 for no outline
|
|
14839
|
+
* @param {Color} [lineColor] - Outline color
|
|
14840
|
+
* @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
|
|
14841
|
+
* @param {boolean} [screenSpace] - Use screen space coordinates
|
|
14842
|
+
* @param {CanvasRenderingContext2D} [context] - Canvas context to use
|
|
14843
|
+
* @memberof DrawUtilities */
|
|
14844
|
+
function drawCrescent(pos, size=1, percent=0, color=WHITE, angle=0, invert=false, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
|
|
14845
|
+
{
|
|
14846
|
+
// build local-space points and let drawPoly apply pos/angle so screen space works
|
|
14847
|
+
const points = getCrescentPoints(vec2(), size, percent, 0, invert);
|
|
14848
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
|
|
14849
|
+
}
|
|
14850
|
+
|
|
14851
|
+
/** Get the list of points that make up a crescent / moon-phase shape
|
|
14852
|
+
* Returns world-space points with pos and angle baked in, ready for drawPoly or other use
|
|
14853
|
+
* @param {Vector2} pos - Center position
|
|
14854
|
+
* @param {number} [size] - Diameter
|
|
14855
|
+
* @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
|
|
14856
|
+
* @param {number} [angle] - Angle to rotate by
|
|
14857
|
+
* @param {boolean} [invert] - Flip which side is illuminated
|
|
14858
|
+
* @param {number} [sides=glCircleSides] - Number of sides for a full circle (halved per arc)
|
|
14859
|
+
* @return {Array<Vector2>} - List of points making up the crescent
|
|
14860
|
+
* @memberof DrawUtilities */
|
|
14861
|
+
function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=glCircleSides)
|
|
14862
|
+
{
|
|
14863
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
14864
|
+
ASSERT(isNumber(size) && isNumber(percent), 'size and percent must be numbers');
|
|
14865
|
+
|
|
14866
|
+
// map phase to a signed terminator curve: -1 new, 0 half, 1 full
|
|
14867
|
+
let p = mod(percent*4, 4); // quarter phase 0..4
|
|
14868
|
+
if (p >= 2) // second half of cycle flips orientation
|
|
14869
|
+
angle += PI;
|
|
14870
|
+
p = p <= 2 ? p-1 : 3-p;
|
|
14871
|
+
if (invert) // flip the illuminated side
|
|
14872
|
+
{
|
|
14873
|
+
p = -p;
|
|
14874
|
+
angle += PI;
|
|
14875
|
+
}
|
|
14876
|
+
|
|
14877
|
+
// build the crescent: outer semicircle, then inner half-ellipse traced back
|
|
14878
|
+
const points = [];
|
|
14879
|
+
const segs = max(3, sides>>1);
|
|
14880
|
+
const radius = size/2;
|
|
14881
|
+
for (let i=0; i<=segs; i++)
|
|
14882
|
+
{
|
|
14883
|
+
const t = i/segs*PI;
|
|
14884
|
+
points.push(vec2(radius*cos(t), radius*sin(t)).rotate(angle).add(pos));
|
|
14885
|
+
}
|
|
14886
|
+
for (let i=segs; i>=0; i--)
|
|
14887
|
+
{
|
|
14888
|
+
const t = i/segs*PI;
|
|
14889
|
+
points.push(vec2(radius*cos(t), -radius*p*sin(t)).rotate(angle).add(pos));
|
|
14890
|
+
}
|
|
14891
|
+
return points;
|
|
13828
14892
|
}
|
|
13829
14893
|
/**
|
|
13830
14894
|
* LittleJS Tween System Plugin
|
|
@@ -15223,12 +16287,18 @@ export
|
|
|
15223
16287
|
inputWASDEmulateDirection,
|
|
15224
16288
|
touchInputEnable,
|
|
15225
16289
|
touchGamepadEnable,
|
|
16290
|
+
touchGamepadPassthrough,
|
|
15226
16291
|
touchGamepadCenterButtonSize,
|
|
15227
16292
|
touchGamepadButtonCount,
|
|
16293
|
+
touchGamepadLeftStick,
|
|
16294
|
+
touchGamepadLeftButtonCount,
|
|
16295
|
+
touchGamepadRightStick,
|
|
15228
16296
|
touchGamepadAnalog,
|
|
16297
|
+
touchGamepadFloating,
|
|
15229
16298
|
touchGamepadSize,
|
|
15230
16299
|
touchGamepadAlpha,
|
|
15231
16300
|
touchGamepadDisplayTime,
|
|
16301
|
+
touchGamepadVibration,
|
|
15232
16302
|
vibrateEnable,
|
|
15233
16303
|
soundEnable,
|
|
15234
16304
|
soundVolume,
|
|
@@ -15271,12 +16341,18 @@ export
|
|
|
15271
16341
|
setGamepadDirectionEmulateStick,
|
|
15272
16342
|
setInputWASDEmulateDirection,
|
|
15273
16343
|
setTouchGamepadEnable,
|
|
16344
|
+
setTouchGamepadPassthrough,
|
|
15274
16345
|
setTouchGamepadCenterButtonSize,
|
|
15275
16346
|
setTouchGamepadButtonCount,
|
|
16347
|
+
setTouchGamepadLeftStick,
|
|
16348
|
+
setTouchGamepadLeftButtonCount,
|
|
16349
|
+
setTouchGamepadRightStick,
|
|
15276
16350
|
setTouchGamepadAnalog,
|
|
16351
|
+
setTouchGamepadFloating,
|
|
15277
16352
|
setTouchGamepadSize,
|
|
15278
16353
|
setTouchGamepadAlpha,
|
|
15279
16354
|
setTouchGamepadDisplayTime,
|
|
16355
|
+
setTouchGamepadVibration,
|
|
15280
16356
|
setVibrateEnable,
|
|
15281
16357
|
setSoundEnable,
|
|
15282
16358
|
setSoundVolume,
|
|
@@ -15402,7 +16478,7 @@ export
|
|
|
15402
16478
|
drawCanvas2D,
|
|
15403
16479
|
drawText,
|
|
15404
16480
|
drawTextScreen,
|
|
15405
|
-
|
|
16481
|
+
setAdditiveBlendMode,
|
|
15406
16482
|
combineCanvases,
|
|
15407
16483
|
engineImageFont,
|
|
15408
16484
|
ImageFont,
|
|
@@ -15410,6 +16486,7 @@ export
|
|
|
15410
16486
|
toggleFullscreen,
|
|
15411
16487
|
setCursor,
|
|
15412
16488
|
getCameraSize,
|
|
16489
|
+
cameraFit,
|
|
15413
16490
|
isOnScreen,
|
|
15414
16491
|
|
|
15415
16492
|
// WebGL
|
|
@@ -15453,10 +16530,16 @@ export
|
|
|
15453
16530
|
mouseWheel,
|
|
15454
16531
|
mouseInWindow,
|
|
15455
16532
|
isUsingGamepad,
|
|
16533
|
+
lastInputDevice,
|
|
16534
|
+
inputMouseMoveThreshold,
|
|
15456
16535
|
inputPreventDefault,
|
|
15457
16536
|
gamepadPrimary,
|
|
15458
16537
|
isTouchDevice,
|
|
15459
16538
|
setInputPreventDefault,
|
|
16539
|
+
setInputMouseMoveThreshold,
|
|
16540
|
+
usingMouseInput,
|
|
16541
|
+
usingKeyboardInput,
|
|
16542
|
+
usingGamepadInput,
|
|
15460
16543
|
gamepadIsDown,
|
|
15461
16544
|
gamepadWasPressed,
|
|
15462
16545
|
gamepadWasReleased,
|
|
@@ -15519,6 +16602,7 @@ export
|
|
|
15519
16602
|
medalDisplaySize,
|
|
15520
16603
|
medalsInit,
|
|
15521
16604
|
medalsForEach,
|
|
16605
|
+
medalsReset,
|
|
15522
16606
|
setMedalDisplayTime,
|
|
15523
16607
|
setMedalDisplaySlideTime,
|
|
15524
16608
|
setMedalDisplaySize,
|
|
@@ -15534,6 +16618,11 @@ export
|
|
|
15534
16618
|
postProcess,
|
|
15535
16619
|
PostProcessPlugin,
|
|
15536
16620
|
|
|
16621
|
+
// Light System
|
|
16622
|
+
lightSystem,
|
|
16623
|
+
LightSystemPlugin,
|
|
16624
|
+
Light,
|
|
16625
|
+
|
|
15537
16626
|
// ZzFXMusic
|
|
15538
16627
|
ZzFXMusic,
|
|
15539
16628
|
zzfxM,
|
|
@@ -15583,6 +16672,8 @@ export
|
|
|
15583
16672
|
drawNineSliceScreen,
|
|
15584
16673
|
drawThreeSlice,
|
|
15585
16674
|
drawThreeSliceScreen,
|
|
16675
|
+
drawCrescent,
|
|
16676
|
+
getCrescentPoints,
|
|
15586
16677
|
|
|
15587
16678
|
// Tween System
|
|
15588
16679
|
Tween,
|