littlejsengine 1.18.29 → 1.20.0
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/FAQ.md +841 -815
- package/README.md +211 -199
- package/dist/littlejs.d.ts +4383 -812
- package/dist/littlejs.esm.js +31848 -16680
- package/dist/littlejs.esm.min.js +14 -1
- package/dist/littlejs.js +31230 -16166
- package/dist/littlejs.min.js +13 -1
- package/dist/littlejs.release.js +27419 -15457
- package/package.json +10 -5
- package/plugins/audioEffects.js +375 -0
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -330
- package/plugins/math3d.js +911 -0
- package/plugins/medalSystem.js +118 -72
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +196 -115
- package/plugins/postProcess.js +284 -166
- package/plugins/render3d.js +4072 -0
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +86 -48
- package/plugins/threejs.js +183 -158
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +646 -526
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +8 -15
- package/src/engine.js +345 -194
- package/src/engineAudio.js +1022 -777
- package/src/engineBuild.mjs +289 -253
- package/src/engineDebug.js +912 -735
- package/src/engineDraw.js +1774 -1510
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +419 -396
- package/src/engineInput.js +1462 -1293
- package/src/engineLogo.js +4 -3
- package/src/engineMath.js +1274 -1183
- package/src/engineObject.js +674 -551
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +808 -726
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +32 -13
- package/src/engineWebGL.js +1162 -941
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/src/engineInput.js
CHANGED
|
@@ -1,1294 +1,1463 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS Input System
|
|
3
|
-
* - Keyboard input with key down, pressed, and released states
|
|
4
|
-
* - Mouse input with position (world and screen space), buttons, and wheel
|
|
5
|
-
* - Gamepad support for multiple controllers with analog sticks and buttons
|
|
6
|
-
* - Touch input mapped to mouse position and buttons
|
|
7
|
-
* - Virtual on-screen gamepad for mobile devices
|
|
8
|
-
* - Automatic gamepad vs keyboard/mouse detection
|
|
9
|
-
* - Input event prevention for canvas focus
|
|
10
|
-
* - Clipboard copy/paste support
|
|
11
|
-
* @namespace Input
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
'use strict';
|
|
15
|
-
|
|
16
|
-
/** Mouse pos in world space
|
|
17
|
-
* @type {Vector2}
|
|
18
|
-
* @memberof Input */
|
|
19
|
-
let mousePos = vec2();
|
|
20
|
-
|
|
21
|
-
/** Mouse pos in screen space
|
|
22
|
-
* @type {Vector2}
|
|
23
|
-
* @memberof Input */
|
|
24
|
-
let mousePosScreen = vec2();
|
|
25
|
-
|
|
26
|
-
/** Mouse movement delta in world space
|
|
27
|
-
* @type {Vector2}
|
|
28
|
-
* @memberof Input */
|
|
29
|
-
let mouseDelta = vec2();
|
|
30
|
-
|
|
31
|
-
/** Mouse movement delta in screen space
|
|
32
|
-
* @type {Vector2}
|
|
33
|
-
* @memberof Input */
|
|
34
|
-
let mouseDeltaScreen = vec2();
|
|
35
|
-
|
|
36
|
-
/** Mouse wheel delta this frame
|
|
37
|
-
* @type {number}
|
|
38
|
-
* @memberof Input */
|
|
39
|
-
let mouseWheel = 0;
|
|
40
|
-
|
|
41
|
-
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
42
|
-
* @type {boolean}
|
|
43
|
-
* @memberof Input */
|
|
44
|
-
let mouseInWindow = true;
|
|
45
|
-
|
|
46
|
-
/** True if a gamepad is the most recently used input device.
|
|
47
|
-
* Equivalent to usingGamepadInput(); derived from lastInputDevice each frame.
|
|
48
|
-
* @type {boolean}
|
|
49
|
-
* @memberof Input */
|
|
50
|
-
let isUsingGamepad = false;
|
|
51
|
-
|
|
52
|
-
/** The most recently used input device: 'mouse' | 'keyboard' | 'gamepad'.
|
|
53
|
-
* Sticky: it holds its value while every device is idle, so a mouse-follow
|
|
54
|
-
* control (e.g. paddle = mousePos) won't snap back the instant the stick/keys
|
|
55
|
-
* are released. With several devices in play at once (e.g. keyboard to move +
|
|
56
|
-
* mouse to aim) it tracks whichever was touched last each frame, so it may
|
|
57
|
-
* alternate — that's intended; use it to pick which control drives a shared
|
|
58
|
-
* action. Updated every frame by inputUpdate().
|
|
59
|
-
* @type {
|
|
60
|
-
* @memberof Input */
|
|
61
|
-
let lastInputDevice = 'mouse';
|
|
62
|
-
|
|
63
|
-
/** Screen-pixel mouse movement per frame that counts as "using the mouse"
|
|
64
|
-
* (so sub-pixel hand jitter doesn't steal focus from the keyboard/gamepad).
|
|
65
|
-
* @type {number}
|
|
66
|
-
* @default
|
|
67
|
-
* @memberof Input */
|
|
68
|
-
let inputMouseMoveThreshold = 6;
|
|
69
|
-
|
|
70
|
-
/** Prevents input continuing to the default browser handling (true by default)
|
|
71
|
-
* @type {boolean}
|
|
72
|
-
* @memberof Input */
|
|
73
|
-
let inputPreventDefault = true;
|
|
74
|
-
|
|
75
|
-
/** Primary gamepad index, automatically set to first gamepad with input
|
|
76
|
-
* @type {number}
|
|
77
|
-
* @memberof Input */
|
|
78
|
-
let gamepadPrimary = 0;
|
|
79
|
-
|
|
80
|
-
/** True if a touch device has been detected
|
|
81
|
-
* @memberof Input */
|
|
82
|
-
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
83
|
-
|
|
84
|
-
/** Prevents input continuing to the default browser handling
|
|
85
|
-
* This is useful to disable for html menus so the browser can handle input normally
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* @
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
* @
|
|
107
|
-
* @
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
* @memberof Input */
|
|
118
|
-
function
|
|
119
|
-
{
|
|
120
|
-
inputData
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
/** Returns true if
|
|
186
|
-
* @
|
|
187
|
-
* @param {number}
|
|
188
|
-
* @return {boolean}
|
|
189
|
-
* @memberof Input */
|
|
190
|
-
function
|
|
191
|
-
{
|
|
192
|
-
ASSERT(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
* @
|
|
200
|
-
* @
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
* @param {number} button
|
|
244
|
-
* @
|
|
245
|
-
* @
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
* @
|
|
256
|
-
* @
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
* @
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
* @
|
|
278
|
-
* @
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Returns
|
|
287
|
-
* @param {number}
|
|
288
|
-
* @
|
|
289
|
-
* @
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
* @param {number}
|
|
300
|
-
* @param {number} [
|
|
301
|
-
* @
|
|
302
|
-
* @
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
* @
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
* @
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
* @
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
{
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
//
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
{
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
{
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
//
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
{
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
//
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
if (!
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Input System
|
|
3
|
+
* - Keyboard input with key down, pressed, and released states
|
|
4
|
+
* - Mouse input with position (world and screen space), buttons, and wheel
|
|
5
|
+
* - Gamepad support for multiple controllers with analog sticks and buttons
|
|
6
|
+
* - Touch input mapped to mouse position and buttons
|
|
7
|
+
* - Virtual on-screen gamepad for mobile devices
|
|
8
|
+
* - Automatic gamepad vs keyboard/mouse detection
|
|
9
|
+
* - Input event prevention for canvas focus
|
|
10
|
+
* - Clipboard copy/paste support
|
|
11
|
+
* @namespace Input
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
/** Mouse pos in world space
|
|
17
|
+
* @type {Vector2}
|
|
18
|
+
* @memberof Input */
|
|
19
|
+
let mousePos = vec2();
|
|
20
|
+
|
|
21
|
+
/** Mouse pos in screen space
|
|
22
|
+
* @type {Vector2}
|
|
23
|
+
* @memberof Input */
|
|
24
|
+
let mousePosScreen = vec2();
|
|
25
|
+
|
|
26
|
+
/** Mouse movement delta in world space
|
|
27
|
+
* @type {Vector2}
|
|
28
|
+
* @memberof Input */
|
|
29
|
+
let mouseDelta = vec2();
|
|
30
|
+
|
|
31
|
+
/** Mouse movement delta in screen space
|
|
32
|
+
* @type {Vector2}
|
|
33
|
+
* @memberof Input */
|
|
34
|
+
let mouseDeltaScreen = vec2();
|
|
35
|
+
|
|
36
|
+
/** Mouse wheel delta this frame
|
|
37
|
+
* @type {number}
|
|
38
|
+
* @memberof Input */
|
|
39
|
+
let mouseWheel = 0;
|
|
40
|
+
|
|
41
|
+
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
42
|
+
* @type {boolean}
|
|
43
|
+
* @memberof Input */
|
|
44
|
+
let mouseInWindow = true;
|
|
45
|
+
|
|
46
|
+
/** True if a gamepad is the most recently used input device.
|
|
47
|
+
* Equivalent to usingGamepadInput(); derived from lastInputDevice each frame.
|
|
48
|
+
* @type {boolean}
|
|
49
|
+
* @memberof Input */
|
|
50
|
+
let isUsingGamepad = false;
|
|
51
|
+
|
|
52
|
+
/** The most recently used input device: 'mouse' | 'keyboard' | 'gamepad'.
|
|
53
|
+
* Sticky: it holds its value while every device is idle, so a mouse-follow
|
|
54
|
+
* control (e.g. paddle = mousePos) won't snap back the instant the stick/keys
|
|
55
|
+
* are released. With several devices in play at once (e.g. keyboard to move +
|
|
56
|
+
* mouse to aim) it tracks whichever was touched last each frame, so it may
|
|
57
|
+
* alternate — that's intended; use it to pick which control drives a shared
|
|
58
|
+
* action. Updated every frame by inputUpdate().
|
|
59
|
+
* @type {'mouse'|'keyboard'|'gamepad'}
|
|
60
|
+
* @memberof Input */
|
|
61
|
+
let lastInputDevice = 'mouse';
|
|
62
|
+
|
|
63
|
+
/** Screen-pixel mouse movement per frame that counts as "using the mouse"
|
|
64
|
+
* (so sub-pixel hand jitter doesn't steal focus from the keyboard/gamepad).
|
|
65
|
+
* @type {number}
|
|
66
|
+
* @default
|
|
67
|
+
* @memberof Input */
|
|
68
|
+
let inputMouseMoveThreshold = 6;
|
|
69
|
+
|
|
70
|
+
/** Prevents input continuing to the default browser handling (true by default)
|
|
71
|
+
* @type {boolean}
|
|
72
|
+
* @memberof Input */
|
|
73
|
+
let inputPreventDefault = true;
|
|
74
|
+
|
|
75
|
+
/** Primary gamepad index, automatically set to first gamepad with input
|
|
76
|
+
* @type {number}
|
|
77
|
+
* @memberof Input */
|
|
78
|
+
let gamepadPrimary = 0;
|
|
79
|
+
|
|
80
|
+
/** True if a touch device has been detected
|
|
81
|
+
* @memberof Input */
|
|
82
|
+
const isTouchDevice = !headlessMode && typeof window != 'undefined' && window.ontouchstart !== undefined;
|
|
83
|
+
|
|
84
|
+
/** Prevents input continuing to the default browser handling
|
|
85
|
+
* This is useful to disable for html menus so the browser can handle input normally,
|
|
86
|
+
* the right click menu included; over an html text field that menu always shows
|
|
87
|
+
* - While on, the mouse's back and forward buttons don't leave the page, a game can read them as mouse 3 and 4
|
|
88
|
+
* @param {boolean} [preventDefault]
|
|
89
|
+
* @memberof Input */
|
|
90
|
+
function setInputPreventDefault(preventDefault=true) { inputPreventDefault = preventDefault; }
|
|
91
|
+
|
|
92
|
+
/** Set the screen-pixel mouse movement per frame that counts as using the mouse
|
|
93
|
+
* @param {number} threshold
|
|
94
|
+
* @memberof Input */
|
|
95
|
+
function setInputMouseMoveThreshold(threshold) { inputMouseMoveThreshold = threshold; }
|
|
96
|
+
|
|
97
|
+
/** Is the mouse the most recently used input device?
|
|
98
|
+
* @return {boolean}
|
|
99
|
+
* @memberof Input */
|
|
100
|
+
function usingMouseInput() { return lastInputDevice === 'mouse'; }
|
|
101
|
+
/** Is the keyboard the most recently used input device?
|
|
102
|
+
* @return {boolean}
|
|
103
|
+
* @memberof Input */
|
|
104
|
+
function usingKeyboardInput() { return lastInputDevice === 'keyboard'; }
|
|
105
|
+
/** Is a gamepad the most recently used input device?
|
|
106
|
+
* @return {boolean}
|
|
107
|
+
* @memberof Input */
|
|
108
|
+
function usingGamepadInput() { return lastInputDevice === 'gamepad'; }
|
|
109
|
+
|
|
110
|
+
/** Clear a key's state, so it stops reading as down, pressed or released this frame; a key still held is not
|
|
111
|
+
* pressed again until it is let go
|
|
112
|
+
* @param {string|number} key - A key code like 'Space', or a mouse or gamepad button number
|
|
113
|
+
* @param {number} [device] - 0 for the keyboard and mouse, gamepad n is device n + 1
|
|
114
|
+
* @param {boolean} [clearDown] - Clear the held state
|
|
115
|
+
* @param {boolean} [clearPressed] - Clear the pressed this frame state
|
|
116
|
+
* @param {boolean} [clearReleased] - Clear the released this frame state
|
|
117
|
+
* @memberof Input */
|
|
118
|
+
function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
|
|
119
|
+
{
|
|
120
|
+
if (!inputData[device])
|
|
121
|
+
return;
|
|
122
|
+
inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// the WASD keys and the arrows they stand in for when inputWASDEmulateDirection is on, and the arrows' aliases
|
|
126
|
+
const inputWASDToArrow = {KeyW:'ArrowUp', KeyS:'ArrowDown', KeyA:'ArrowLeft', KeyD:'ArrowRight'};
|
|
127
|
+
const inputArrowToWASD = {ArrowUp:'KeyW', ArrowDown:'KeyS', ArrowLeft:'KeyA', ArrowRight:'KeyD'};
|
|
128
|
+
const inputKeysHeld = new Set; // the keys physically down, since an arrow's slot is shared with its alias
|
|
129
|
+
let inputWasTouching = 0, inputTouchIdentifier; // the touch driving the mouse, cleared with the input so only a new touch presses
|
|
130
|
+
let inputLastTouchTime = -1e9; // when a touch event last came, the mouse events a browser makes from a tap follow it
|
|
131
|
+
|
|
132
|
+
// is this mouse event one the browser made from a touch, which the touch input already handled
|
|
133
|
+
function inputIsTouchMouseEvent(e)
|
|
134
|
+
{ return e.sourceCapabilities?.firesTouchEvents || performance.now() - inputLastTouchTime < 500; }
|
|
135
|
+
|
|
136
|
+
// is a real gamepad being used, a button down or a standard stick pushed, only read to hand over from the touch
|
|
137
|
+
// gamepad, a non standard pad's axes may rest at full deflection so they are not trusted here
|
|
138
|
+
function inputRealGamepadUsed()
|
|
139
|
+
{
|
|
140
|
+
for (const gamepad of inputGetGamepads())
|
|
141
|
+
{
|
|
142
|
+
if (!gamepad) continue;
|
|
143
|
+
for (const button of gamepad.buttons)
|
|
144
|
+
if (button.pressed) return true;
|
|
145
|
+
if (gamepad.mapping === 'standard')
|
|
146
|
+
for (let j = 0; j < 4 && j < gamepad.axes.length; ++j)
|
|
147
|
+
if (abs(gamepad.axes[j]) > .5) return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// let go of every keyboard key, for when something else takes the keyboard, like a text field
|
|
153
|
+
function inputClearKeyboard()
|
|
154
|
+
{
|
|
155
|
+
const keys = inputData[0];
|
|
156
|
+
for (const key in keys)
|
|
157
|
+
isNaN(+key) && (keys[key] = 0); // mouse buttons are numbers, keys are codes
|
|
158
|
+
inputKeysHeld.clear();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Clears all input
|
|
162
|
+
* @memberof Input */
|
|
163
|
+
function inputClear()
|
|
164
|
+
{
|
|
165
|
+
// empty every device but keep which gamepad slots exist, so gamepadConnected still reads true through a
|
|
166
|
+
// clear, like the one every frame while the window is unfocused; a disconnect clears its slot in the poll
|
|
167
|
+
for (let i = inputData.length; i--;)
|
|
168
|
+
if (!i || inputData[i]) inputData[i] = [];
|
|
169
|
+
inputKeysHeld.clear();
|
|
170
|
+
inputWasTouching = 0;
|
|
171
|
+
// release the touch gamepad, a finger still on it has to lift before it can take a control again
|
|
172
|
+
touchGamepadPointerRole.clear();
|
|
173
|
+
touchGamepadButtons.length = 0;
|
|
174
|
+
touchGamepadButtonsPressed.length = 0;
|
|
175
|
+
touchGamepadSticks.length = 0;
|
|
176
|
+
touchGamepadStickPointerId.length = 0; // release floating sticks so they re-anchor
|
|
177
|
+
gamepadStickData.length = 0;
|
|
178
|
+
gamepadDpadData.length = 0;
|
|
179
|
+
// gamepadButtonsLast and gamepadAxisCentered describe the pads, not input, so they are kept:
|
|
180
|
+
// a button held through the clear reads as down, not newly pressed, and axes stay trusted
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
184
|
+
|
|
185
|
+
/** Returns true if device key is down
|
|
186
|
+
* @param {string|number} key
|
|
187
|
+
* @param {number} [device] - 0 for the keyboard and mouse, gamepad n is device n + 1
|
|
188
|
+
* @return {boolean}
|
|
189
|
+
* @memberof Input */
|
|
190
|
+
function keyIsDown(key, device=0)
|
|
191
|
+
{
|
|
192
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
193
|
+
ASSERT(typeof key !== 'string' || key.length > 1, "keys are codes like 'KeyW' or 'Space', not characters");
|
|
194
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 5, 'use code string for keyboard');
|
|
195
|
+
return !!(inputData[device]?.[key] & 1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Returns true if device key was pressed this frame
|
|
199
|
+
* @param {string|number} key
|
|
200
|
+
* @param {number} [device] - 0 for the keyboard and mouse, gamepad n is device n + 1
|
|
201
|
+
* @return {boolean}
|
|
202
|
+
* @memberof Input */
|
|
203
|
+
function keyWasPressed(key, device=0)
|
|
204
|
+
{
|
|
205
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
206
|
+
ASSERT(typeof key !== 'string' || key.length > 1, "keys are codes like 'KeyW' or 'Space', not characters");
|
|
207
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 5, 'use code string for keyboard');
|
|
208
|
+
return !!(inputData[device]?.[key] & 2);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Returns true if device key was released this frame
|
|
212
|
+
* @param {string|number} key
|
|
213
|
+
* @param {number} [device] - 0 for the keyboard and mouse, gamepad n is device n + 1
|
|
214
|
+
* @return {boolean}
|
|
215
|
+
* @memberof Input */
|
|
216
|
+
function keyWasReleased(key, device=0)
|
|
217
|
+
{
|
|
218
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
219
|
+
ASSERT(typeof key !== 'string' || key.length > 1, "keys are codes like 'KeyW' or 'Space', not characters");
|
|
220
|
+
ASSERT(device > 0 || typeof key !== 'number' || key < 5, 'use code string for keyboard');
|
|
221
|
+
return !!(inputData[device]?.[key] & 4);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Returns input vector from arrow keys or WASD if enabled
|
|
225
|
+
* @param {string} [up]
|
|
226
|
+
* @param {string} [down]
|
|
227
|
+
* @param {string} [left]
|
|
228
|
+
* @param {string} [right]
|
|
229
|
+
* @return {Vector2}
|
|
230
|
+
* @memberof Input */
|
|
231
|
+
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
232
|
+
{
|
|
233
|
+
ASSERT(isStringLike(up), 'up key must be a string');
|
|
234
|
+
ASSERT(isStringLike(down), 'down key must be a string');
|
|
235
|
+
ASSERT(isStringLike(left), 'left key must be a string');
|
|
236
|
+
ASSERT(isStringLike(right), 'right key must be a string');
|
|
237
|
+
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
238
|
+
return vec2(k(right) - k(left), k(up) - k(down));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Returns true if mouse button is down
|
|
242
|
+
* @function
|
|
243
|
+
* @param {number} button
|
|
244
|
+
* @return {boolean}
|
|
245
|
+
* @memberof Input */
|
|
246
|
+
function mouseIsDown(button)
|
|
247
|
+
{
|
|
248
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
249
|
+
return keyIsDown(button);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Returns true if mouse button was pressed
|
|
253
|
+
* @function
|
|
254
|
+
* @param {number} button
|
|
255
|
+
* @return {boolean}
|
|
256
|
+
* @memberof Input */
|
|
257
|
+
function mouseWasPressed(button)
|
|
258
|
+
{
|
|
259
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
260
|
+
return keyWasPressed(button);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Returns true if mouse button was released
|
|
264
|
+
* @function
|
|
265
|
+
* @param {number} button
|
|
266
|
+
* @return {boolean}
|
|
267
|
+
* @memberof Input */
|
|
268
|
+
function mouseWasReleased(button)
|
|
269
|
+
{
|
|
270
|
+
ASSERT(isNumber(button), 'mouse button must be a number');
|
|
271
|
+
return keyWasReleased(button);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Returns true if gamepad button is down
|
|
275
|
+
* @param {number} button
|
|
276
|
+
* @param {number} [gamepad]
|
|
277
|
+
* @return {boolean}
|
|
278
|
+
* @memberof Input */
|
|
279
|
+
function gamepadIsDown(button, gamepad=gamepadPrimary)
|
|
280
|
+
{
|
|
281
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
282
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
283
|
+
return keyIsDown(button, gamepad+1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Returns true if gamepad button was pressed
|
|
287
|
+
* @param {number} button
|
|
288
|
+
* @param {number} [gamepad]
|
|
289
|
+
* @return {boolean}
|
|
290
|
+
* @memberof Input */
|
|
291
|
+
function gamepadWasPressed(button, gamepad=gamepadPrimary)
|
|
292
|
+
{
|
|
293
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
294
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
295
|
+
return keyWasPressed(button, gamepad+1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Returns true if gamepad button was released
|
|
299
|
+
* @param {number} button
|
|
300
|
+
* @param {number} [gamepad]
|
|
301
|
+
* @return {boolean}
|
|
302
|
+
* @memberof Input */
|
|
303
|
+
function gamepadWasReleased(button, gamepad=gamepadPrimary)
|
|
304
|
+
{
|
|
305
|
+
ASSERT(isNumber(button), 'button must be a number');
|
|
306
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
307
|
+
return keyWasReleased(button, gamepad+1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Returns gamepad stick value
|
|
311
|
+
* @param {number} stick
|
|
312
|
+
* @param {number} [gamepad]
|
|
313
|
+
* @return {Vector2} - Where the stick points, y up, length 0 to 1 past the dead zone, zero when there is no such stick
|
|
314
|
+
* @memberof Input */
|
|
315
|
+
function gamepadStick(stick, gamepad=gamepadPrimary)
|
|
316
|
+
{
|
|
317
|
+
ASSERT(isNumber(stick), 'stick must be a number');
|
|
318
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
319
|
+
return gamepadStickData[gamepad]?.[stick] ?? vec2();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Returns gamepad dpad value
|
|
323
|
+
* @param {number} [gamepad]
|
|
324
|
+
* @return {Vector2}
|
|
325
|
+
* @memberof Input */
|
|
326
|
+
function gamepadDpad(gamepad=gamepadPrimary)
|
|
327
|
+
{
|
|
328
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
329
|
+
return gamepadDpadData[gamepad]?.copy() ?? vec2(); // a copy, the poller keeps its own
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Returns true if passed in gamepad is connected
|
|
333
|
+
* @param {number} [gamepad]
|
|
334
|
+
* @return {boolean}
|
|
335
|
+
* @memberof Input */
|
|
336
|
+
function gamepadConnected(gamepad=gamepadPrimary)
|
|
337
|
+
{
|
|
338
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
339
|
+
return !!inputData[gamepad+1];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Returns how many control sticks the passed in gamepad has
|
|
343
|
+
* @param {number} [gamepad]
|
|
344
|
+
* @return {number}
|
|
345
|
+
* @memberof Input */
|
|
346
|
+
function gamepadStickCount(gamepad=gamepadPrimary)
|
|
347
|
+
{
|
|
348
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
349
|
+
return gamepadStickData[gamepad]?.length ?? 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Pulse a gamepad's vibration hardware using the dual-rumble effect if it exists
|
|
353
|
+
* Strong magnitude is usually the left side motor, weak magnitude is usually the right side motor
|
|
354
|
+
* @param {number} [gamepad] - gamepad index
|
|
355
|
+
* @param {number} [duration] - effect duration in ms, browsers limit it and the delay to 5 seconds together
|
|
356
|
+
* @param {number} [strongMagnitude] - strong (left) motor intensity, 0 to 1
|
|
357
|
+
* @param {number} [weakMagnitude] - weak (right) motor intensity, 0 to 1
|
|
358
|
+
* @param {number} [startDelay] - delay in ms before the effect starts
|
|
359
|
+
* @memberof Input */
|
|
360
|
+
function gamepadVibrate(gamepad=gamepadPrimary, duration=200, strongMagnitude=1, weakMagnitude=1, startDelay=0)
|
|
361
|
+
{
|
|
362
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
363
|
+
ASSERT(strongMagnitude >= 0 && strongMagnitude <= 1 && weakMagnitude >= 0 && weakMagnitude <= 1,
|
|
364
|
+
'rumble magnitudes must be 0 to 1');
|
|
365
|
+
ASSERT(duration >= 0 && startDelay >= 0 && duration + startDelay <= 5e3,
|
|
366
|
+
'browsers limit a rumble and its delay to 5 seconds');
|
|
367
|
+
if (!vibrateEnable || headlessMode) return;
|
|
368
|
+
const pad = inputGetGamepads()[gamepad];
|
|
369
|
+
// a browser refuses a rumble it can not play by rejecting, which is nothing to report
|
|
370
|
+
pad?.vibrationActuator?.playEffect?.('dual-rumble', {duration, strongMagnitude, weakMagnitude, startDelay})
|
|
371
|
+
?.catch?.(()=> {});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Stop vibration on a gamepad
|
|
375
|
+
* @param {number} [gamepad] - gamepad index
|
|
376
|
+
* @memberof Input */
|
|
377
|
+
function gamepadVibrateStop(gamepad=gamepadPrimary)
|
|
378
|
+
{
|
|
379
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
380
|
+
if (!vibrateEnable || headlessMode) return;
|
|
381
|
+
const pad = inputGetGamepads()[gamepad];
|
|
382
|
+
pad?.vibrationActuator?.reset?.();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// the gamepads the browser reports, none when it has no gamepad support or refuses the page them, which it
|
|
386
|
+
// does by throwing a SecurityError from the call, as the spec says, for a page not allowed gamepads
|
|
387
|
+
function inputGetGamepads()
|
|
388
|
+
{
|
|
389
|
+
try { return navigator?.getGamepads?.() || []; }
|
|
390
|
+
catch (e) { return []; }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
394
|
+
|
|
395
|
+
/** Pulse the vibration hardware if it exists
|
|
396
|
+
* @param {number|Array} [pattern] - single value in ms or vibration interval array
|
|
397
|
+
* @memberof Input */
|
|
398
|
+
function vibrate(pattern=100)
|
|
399
|
+
{
|
|
400
|
+
ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
|
|
401
|
+
vibrateEnable && !headlessMode && navigator?.vibrate?.(pattern);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Cancel any ongoing vibration
|
|
405
|
+
* @memberof Input */
|
|
406
|
+
function vibrateStop() { vibrate(0); }
|
|
407
|
+
|
|
408
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
409
|
+
// Pointer Lock
|
|
410
|
+
|
|
411
|
+
/** Request to lock the pointer, for a mouse; a device with only touch refuses it
|
|
412
|
+
* @memberof Input */
|
|
413
|
+
function pointerLockRequest()
|
|
414
|
+
{
|
|
415
|
+
// newer browsers return a promise that rejects when the lock is refused, like just after Esc left it,
|
|
416
|
+
// or on a phone; a touchscreen laptop's mouse can still lock
|
|
417
|
+
try { /** @type {any} */ (mainCanvas.requestPointerLock?.())?.catch?.(()=>{}); }
|
|
418
|
+
catch { }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Request to unlock the pointer
|
|
422
|
+
* @memberof Input */
|
|
423
|
+
function pointerLockExit()
|
|
424
|
+
{ document.exitPointerLock?.(); }
|
|
425
|
+
|
|
426
|
+
/** Check if pointer is locked (true if locked)
|
|
427
|
+
* @return {boolean}
|
|
428
|
+
* @memberof Input */
|
|
429
|
+
function pointerLockIsActive()
|
|
430
|
+
{ return document.pointerLockElement === mainCanvas; }
|
|
431
|
+
|
|
432
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
433
|
+
// Input variables used by engine
|
|
434
|
+
|
|
435
|
+
// input uses bit field for each key: 1=isDown, 2=wasPressed, 4=wasReleased
|
|
436
|
+
// mouse and keyboard stored in device 0, gamepads stored in devices > 0
|
|
437
|
+
const inputData = [[]];
|
|
438
|
+
|
|
439
|
+
// gamepad internal variables
|
|
440
|
+
const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
|
|
441
|
+
// per gamepad, each button's raw pressed state from the last poll, kept through inputClear and cleared on
|
|
442
|
+
// disconnect, so a button held through a clear reads as down and not newly pressed
|
|
443
|
+
const gamepadButtonsLast = [];
|
|
444
|
+
// per gamepad, how many consecutive frames each axis has rested inside the
|
|
445
|
+
// dead zone, used to tell stick axes from axes that rest at full deflection
|
|
446
|
+
const gamepadAxisCentered = [];
|
|
447
|
+
// how long an axis must rest inside the dead zone before it counts as a stick
|
|
448
|
+
const gamepadAxisCenteredFrames = 15;
|
|
449
|
+
|
|
450
|
+
// touch gamepad internal variables
|
|
451
|
+
const touchGamepadTimer = new Timer(undefined, true), touchGamepadButtons = [], touchGamepadSticks = [];
|
|
452
|
+
// buttons pressed since the last poll, so a tap that lifts before the poll still counts
|
|
453
|
+
const touchGamepadButtonsPressed = [];
|
|
454
|
+
// floating stick anchors (stage-local CSS pixels) and owning pointer ids, indexed by stick (0=left, 1=right)
|
|
455
|
+
const touchGamepadStickAnchors = [], touchGamepadStickPointerId = [];
|
|
456
|
+
// pointerId -> control role ('stick0', 'stick1', 'face<n>', or 'start')
|
|
457
|
+
const touchGamepadPointerRole = new Map();
|
|
458
|
+
// overlay DOM elements (created lazily on touch devices) and cached SVG shapes
|
|
459
|
+
let touchGamepadOverlay, touchGamepadStage, touchGamepadSvg, touchGamepadSvgEls;
|
|
460
|
+
let touchGamepadSideZones = [], touchGamepadZoneC;
|
|
461
|
+
let touchGamepadNeedRelayout = true, touchGamepadLastLayout;
|
|
462
|
+
|
|
463
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
464
|
+
// Input system functions used by engine
|
|
465
|
+
|
|
466
|
+
function inputInit()
|
|
467
|
+
{
|
|
468
|
+
if (headlessMode) return;
|
|
469
|
+
|
|
470
|
+
// add event listeners
|
|
471
|
+
document.addEventListener('keydown', onKeyDown);
|
|
472
|
+
document.addEventListener('keyup', onKeyUp);
|
|
473
|
+
document.addEventListener('mousedown', onMouseDown);
|
|
474
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
475
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
476
|
+
document.addEventListener('mouseleave', onMouseLeave);
|
|
477
|
+
document.addEventListener('wheel', onMouseWheel, { passive: false });
|
|
478
|
+
document.addEventListener('contextmenu', onContextMenu);
|
|
479
|
+
addEventListener('blur', onBlur); // the window's, the browser fires blur there and it does not bubble to the document
|
|
480
|
+
|
|
481
|
+
// init touch input, its handler checks touchInputEnable itself, so turning it on later works too
|
|
482
|
+
if (isTouchDevice)
|
|
483
|
+
touchInputInit();
|
|
484
|
+
|
|
485
|
+
function onKeyDown(e)
|
|
486
|
+
{
|
|
487
|
+
// fix stalled audio requiring user interaction, a keyboard only game has no other gesture
|
|
488
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
489
|
+
audioContext.resume();
|
|
490
|
+
|
|
491
|
+
// keys typed into an html text field are the player's typing, not game input;
|
|
492
|
+
// a key already down still releases on keyup, which only lets go of keys that are down
|
|
493
|
+
const typing = isTextInput(e.target) || isTextInput(document.activeElement);
|
|
494
|
+
if (!e.repeat && !typing)
|
|
495
|
+
{
|
|
496
|
+
inputKeysHeld.add(e.code);
|
|
497
|
+
// an arrow its alias already holds down is not pressed again, like its release waits for both
|
|
498
|
+
if (!(inputWASDEmulateDirection && inputKeysHeld.has(inputArrowToWASD[e.code]) && inputData[0][e.code] & 1))
|
|
499
|
+
inputData[0][e.code] = 3;
|
|
500
|
+
// an alias presses its arrow's slot too, unless the arrow itself already holds it down
|
|
501
|
+
const remap = remapKey(e.code);
|
|
502
|
+
if (remap !== e.code && !(inputData[0][remap] & 1))
|
|
503
|
+
inputData[0][remap] = 3;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// try to prevent default browser handling of input
|
|
507
|
+
if (!inputPreventDefault || !e.cancelable || !document.hasFocus()) return;
|
|
508
|
+
|
|
509
|
+
// don't break browser shortcuts
|
|
510
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
511
|
+
|
|
512
|
+
// don't interfere with user typing into UI fields
|
|
513
|
+
if (typing) return;
|
|
514
|
+
|
|
515
|
+
// fix browser setting "Search for text when you start typing"
|
|
516
|
+
const printable = typeof e.key === 'string' && e.key.length === 1;
|
|
517
|
+
|
|
518
|
+
// prevent arrow key and other default keys from messing with stuff
|
|
519
|
+
const preventDefaultKeys =
|
|
520
|
+
[
|
|
521
|
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', // scrolling
|
|
522
|
+
'Space', // page down scroll
|
|
523
|
+
'Tab', // focus navigation
|
|
524
|
+
'Backspace', // browser back
|
|
525
|
+
];
|
|
526
|
+
if (preventDefaultKeys.includes(e.code) || printable)
|
|
527
|
+
e.preventDefault();
|
|
528
|
+
}
|
|
529
|
+
// true if an event is on an HTML control on the page, which handles it itself
|
|
530
|
+
function isOnControl(e)
|
|
531
|
+
{
|
|
532
|
+
const target = /** @type {HTMLElement} */ (e.target);
|
|
533
|
+
return !!target?.closest?.('input,textarea,select,button,a,label,[contenteditable]');
|
|
534
|
+
}
|
|
535
|
+
function isTextInput(element)
|
|
536
|
+
{
|
|
537
|
+
// a field that takes typing or arrow keys, not an input that is really a button, checkbox or slider,
|
|
538
|
+
// which would otherwise keep every key from the game after it was clicked
|
|
539
|
+
const tag = element?.tagName;
|
|
540
|
+
if (tag === 'INPUT')
|
|
541
|
+
return !['button','checkbox','color','file','image','radio','range','reset','submit'].includes(element.type);
|
|
542
|
+
return !!element?.isContentEditable || tag === 'TEXTAREA' || tag === 'SELECT';
|
|
543
|
+
}
|
|
544
|
+
function onKeyUp(e)
|
|
545
|
+
{
|
|
546
|
+
inputKeysHeld.delete(e.code);
|
|
547
|
+
// the key's own slot and the arrow slot an alias shares, each released only once nothing holds it:
|
|
548
|
+
// an arrow held with its alias stays down until both are let go; a slot not down was never pressed as far
|
|
549
|
+
// as the game knows (held since before focus or through a clear), so it is not released either
|
|
550
|
+
// the alias is released whatever the setting is now, emulation turned off while W is held still lets go of
|
|
551
|
+
// the ArrowUp it pressed
|
|
552
|
+
const remap = inputWASDToArrow[e.code] || e.code;
|
|
553
|
+
for (const key of remap === e.code ? [e.code] : [e.code, remap])
|
|
554
|
+
if (!inputKeysHeld.has(key) && !(inputWASDEmulateDirection && inputKeysHeld.has(inputArrowToWASD[key])))
|
|
555
|
+
if (inputData[0][key] & 1)
|
|
556
|
+
inputData[0][key] = (inputData[0][key]&2) | 4;
|
|
557
|
+
|
|
558
|
+
// a Mac sends no keyup for a key let go while Cmd is held, so letting go of Cmd lets go of every key
|
|
559
|
+
if (e.key === 'Meta')
|
|
560
|
+
for (const code of [...inputKeysHeld])
|
|
561
|
+
onKeyUp({code});
|
|
562
|
+
}
|
|
563
|
+
function remapKey(k)
|
|
564
|
+
{
|
|
565
|
+
// handle remapping wasd keys to directions
|
|
566
|
+
return inputWASDEmulateDirection && inputWASDToArrow[k] || k;
|
|
567
|
+
}
|
|
568
|
+
function onMouseDown(e)
|
|
569
|
+
{
|
|
570
|
+
// a mouse on a touchscreen laptop works, only the mouse events a tap makes are left to the touch input
|
|
571
|
+
if (touchInputEnable && inputIsTouchMouseEvent(e)) return;
|
|
572
|
+
|
|
573
|
+
// fix stalled audio requiring user interaction
|
|
574
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
575
|
+
audioContext.resume();
|
|
576
|
+
|
|
577
|
+
inputData[0][e.button] = 3;
|
|
578
|
+
|
|
579
|
+
const mousePosScreenLast = mousePosScreen;
|
|
580
|
+
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
581
|
+
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
582
|
+
|
|
583
|
+
// a click on an HTML form control on the page is left to it, so it can take focus, place the caret or drag
|
|
584
|
+
if (inputPreventDefault && e.cancelable && document.hasFocus() && !isOnControl(e))
|
|
585
|
+
{
|
|
586
|
+
// this keeps focus where it is, so a click outside a text field lets it go, or it keeps the keys
|
|
587
|
+
const active = /** @type {HTMLElement} */ (document.activeElement);
|
|
588
|
+
isTextInput(active) && !active.contains(/** @type {Node} */ (e.target)) && active.blur();
|
|
589
|
+
e.preventDefault();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function onMouseUp(e)
|
|
593
|
+
{
|
|
594
|
+
if (touchInputEnable && inputIsTouchMouseEvent(e)) return;
|
|
595
|
+
|
|
596
|
+
// released only if it was pressed, like a key or a touch
|
|
597
|
+
if (inputData[0][e.button] & 1)
|
|
598
|
+
inputData[0][e.button] = (inputData[0][e.button]&2) | 4;
|
|
599
|
+
|
|
600
|
+
// the mouse's back and forward buttons would leave the page, like Backspace would
|
|
601
|
+
if (inputPreventDefault && e.cancelable && (e.button === 3 || e.button === 4))
|
|
602
|
+
e.preventDefault();
|
|
603
|
+
}
|
|
604
|
+
function onMouseMove(e)
|
|
605
|
+
{
|
|
606
|
+
mouseInWindow = true;
|
|
607
|
+
const mousePosScreenLast = mousePosScreen;
|
|
608
|
+
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
609
|
+
|
|
610
|
+
// when pointer is locked use movementX/Y for delta, css pixels scaled to canvas units the way
|
|
611
|
+
// mouseEventToScreen maps positions, so the delta matches the unlocked one with canvasFixedSize
|
|
612
|
+
let movement;
|
|
613
|
+
if (pointerLockIsActive())
|
|
614
|
+
{
|
|
615
|
+
const rect = mainCanvas.getBoundingClientRect();
|
|
616
|
+
movement = vec2(e.movementX * mainCanvasSize.x / rect.width, e.movementY * mainCanvasSize.y / rect.height);
|
|
617
|
+
}
|
|
618
|
+
else
|
|
619
|
+
movement = mousePosScreen.subtract(mousePosScreenLast);
|
|
620
|
+
mouseDeltaScreen = mouseDeltaScreen.add(movement);
|
|
621
|
+
}
|
|
622
|
+
function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
|
|
623
|
+
function onMouseWheel(e)
|
|
624
|
+
{
|
|
625
|
+
// accumulate so multiple wheel events in one frame are not lost
|
|
626
|
+
// a text area, list or field on the page scrolls itself, and the game does not take that wheel
|
|
627
|
+
const target = /** @type {HTMLElement} */ (e.target);
|
|
628
|
+
if (target?.closest?.('input,textarea,select,[contenteditable]'))
|
|
629
|
+
return;
|
|
630
|
+
if (!e.ctrlKey)
|
|
631
|
+
mouseWheel += sign(e.deltaY);
|
|
632
|
+
if (inputPreventDefault && e.cancelable && document.hasFocus())
|
|
633
|
+
e.preventDefault(); // prevent page scrolling
|
|
634
|
+
}
|
|
635
|
+
function onContextMenu(e)
|
|
636
|
+
{
|
|
637
|
+
// prevent right click menu, but a text field keeps its copy and paste menu
|
|
638
|
+
if (inputPreventDefault && !isTextInput(e.target))
|
|
639
|
+
e.preventDefault();
|
|
640
|
+
}
|
|
641
|
+
function onBlur()
|
|
642
|
+
{
|
|
643
|
+
// inputClear also releases any held virtual gamepad controls so they don't stick
|
|
644
|
+
inputClear();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// enable touch input mouse passthrough
|
|
648
|
+
function touchInputInit()
|
|
649
|
+
{
|
|
650
|
+
// add non passive touch event listeners
|
|
651
|
+
document.addEventListener('touchstart', (e)=> handleTouch(e), { passive: false });
|
|
652
|
+
document.addEventListener('touchmove', (e)=> handleTouch(e), { passive: false });
|
|
653
|
+
document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
|
|
654
|
+
document.addEventListener('touchcancel', (e)=> handleTouch(e), { passive: false }); // the browser ended it
|
|
655
|
+
|
|
656
|
+
// handle all touch events the same way
|
|
657
|
+
function handleTouch(e)
|
|
658
|
+
{
|
|
659
|
+
if (!touchInputEnable)
|
|
660
|
+
{
|
|
661
|
+
// turned off mid touch, the finger that drove the mouse lets go of it
|
|
662
|
+
if (inputWasTouching && (inputData[0][0] & 1))
|
|
663
|
+
inputData[0][0] = inputData[0][0] & 2 | 4;
|
|
664
|
+
inputWasTouching = 0;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
inputLastTouchTime = performance.now();
|
|
668
|
+
|
|
669
|
+
// fix stalled audio requiring user interaction
|
|
670
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
671
|
+
audioContext.resume();
|
|
672
|
+
|
|
673
|
+
// when the touch gamepad is enabled it owns touch input: suppress the
|
|
674
|
+
// touch->mouse passthrough entirely unless touchGamepadPassthrough is set
|
|
675
|
+
// (its own zones drive gameplay via pointer events)
|
|
676
|
+
if (!touchGamepadEnable || touchGamepadPassthrough)
|
|
677
|
+
{
|
|
678
|
+
// touches that landed on a virtual gamepad zone are owned by the gamepad
|
|
679
|
+
// (handled by its own pointer listeners) and must not drive the game mouse
|
|
680
|
+
const isGamepadTouch = (t)=>
|
|
681
|
+
touchGamepadSideZones.includes(t.target) || t.target === touchGamepadZoneC;
|
|
682
|
+
const gameTouches = [];
|
|
683
|
+
for (const t of e.touches)
|
|
684
|
+
if (!isGamepadTouch(t)) gameTouches.push(t);
|
|
685
|
+
|
|
686
|
+
// check if touching and pass to mouse events
|
|
687
|
+
const touching = gameTouches.length;
|
|
688
|
+
const button = 0; // all touches are left mouse button
|
|
689
|
+
if (touching)
|
|
690
|
+
{
|
|
691
|
+
// only a game finger coming down presses, not one held through a clear, and it drives the mouse
|
|
692
|
+
const pressTouch = !inputWasTouching && e.type == 'touchstart' &&
|
|
693
|
+
[...e.changedTouches].find(t=> !isGamepadTouch(t));
|
|
694
|
+
const touch = pressTouch || gameTouches[0];
|
|
695
|
+
|
|
696
|
+
// set event pos and pass it along
|
|
697
|
+
const pos = vec2(touch.clientX, touch.clientY);
|
|
698
|
+
const mousePosScreenLast = mousePosScreen;
|
|
699
|
+
mousePosScreen = mouseEventToScreen(pos);
|
|
700
|
+
if (inputWasTouching && touch.identifier === inputTouchIdentifier)
|
|
701
|
+
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
702
|
+
else if (pressTouch)
|
|
703
|
+
inputData[0][button] = 3;
|
|
704
|
+
inputTouchIdentifier = touch.identifier;
|
|
705
|
+
}
|
|
706
|
+
else if (inputWasTouching && (inputData[0][button] & 1))
|
|
707
|
+
inputData[0][button] = inputData[0][button] & 2 | 4; // released only if it was pressed
|
|
708
|
+
|
|
709
|
+
// set was touching
|
|
710
|
+
inputWasTouching = touching;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// prevent default handling like copy, magnifier lens, and scrolling, but a tap on an HTML control is
|
|
714
|
+
// left to it, cancelling a touch would also take away a button's click or a slider's drag
|
|
715
|
+
if (inputPreventDefault && e.cancelable && document.hasFocus() && !isOnControl(e))
|
|
716
|
+
{
|
|
717
|
+
// like a mouse click, a tap outside a focused text field lets it go, the cancel keeps focus where it is
|
|
718
|
+
const active = /** @type {HTMLElement} */ (document.activeElement);
|
|
719
|
+
e.type == 'touchstart' && isTextInput(active) && !active.contains(/** @type {Node} */ (e.target)) && active.blur();
|
|
720
|
+
e.preventDefault();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// must return true so the document will get focus
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// convert a mouse or touch event position to screen space
|
|
730
|
+
function mouseEventToScreen(mousePos)
|
|
731
|
+
{
|
|
732
|
+
const rect = mainCanvas.getBoundingClientRect();
|
|
733
|
+
const px = percent(mousePos.x, rect.left, rect.right);
|
|
734
|
+
const py = percent(mousePos.y, rect.top, rect.bottom);
|
|
735
|
+
return vec2(px*mainCanvasSize.x, py*mainCanvasSize.y);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function inputUpdate()
|
|
740
|
+
{
|
|
741
|
+
if (headlessMode) return;
|
|
742
|
+
|
|
743
|
+
// clear input when lost focus (prevent stuck keys)
|
|
744
|
+
if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
745
|
+
inputClear();
|
|
746
|
+
|
|
747
|
+
// update mouse world space position and delta
|
|
748
|
+
mousePos = screenToWorld(mousePosScreen);
|
|
749
|
+
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
750
|
+
|
|
751
|
+
// build the touch gamepad overlay lazily once enabled on a touch device
|
|
752
|
+
touchGamepadInit();
|
|
753
|
+
|
|
754
|
+
// update gamepads if enabled
|
|
755
|
+
gamepadsUpdate();
|
|
756
|
+
|
|
757
|
+
// update most recently used input device
|
|
758
|
+
updateLastInputDevice();
|
|
759
|
+
|
|
760
|
+
function updateLastInputDevice()
|
|
761
|
+
{
|
|
762
|
+
// mouse: any button held or moved
|
|
763
|
+
const mouseActive = mouseIsDown(0) || mouseIsDown(1) || mouseIsDown(2) || mouseDeltaScreen.length() > inputMouseMoveThreshold;
|
|
764
|
+
|
|
765
|
+
// gamepad: any button held or stick moved
|
|
766
|
+
let gamepadActive = false;
|
|
767
|
+
for (let s = gamepadStickCount(); s-- && !gamepadActive;)
|
|
768
|
+
gamepadActive = gamepadStick(s).lengthSquared() > .2;
|
|
769
|
+
for (let b = 17; b-- && !gamepadActive;)
|
|
770
|
+
gamepadActive = gamepadIsDown(b);
|
|
771
|
+
|
|
772
|
+
// keyboard: any non-mouse key down
|
|
773
|
+
let keyboardActive = false;
|
|
774
|
+
for (const k in inputData[0])
|
|
775
|
+
if (isNaN(+k) && (inputData[0][k] & 1))
|
|
776
|
+
{
|
|
777
|
+
keyboardActive = true;
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// update the last input
|
|
782
|
+
if (gamepadActive)
|
|
783
|
+
lastInputDevice = 'gamepad';
|
|
784
|
+
else if (mouseActive)
|
|
785
|
+
lastInputDevice = 'mouse';
|
|
786
|
+
else if (keyboardActive)
|
|
787
|
+
lastInputDevice = 'keyboard';
|
|
788
|
+
|
|
789
|
+
// set flag if gamepad is last device
|
|
790
|
+
isUsingGamepad = lastInputDevice === 'gamepad';
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// gamepads are updated by engine every frame automatically
|
|
794
|
+
function gamepadsUpdate()
|
|
795
|
+
{
|
|
796
|
+
const deadZoneMin=.3, deadZoneMax=.8;
|
|
797
|
+
const applyDeadZones = (v)=>
|
|
798
|
+
{
|
|
799
|
+
const deadZone = (v)=>
|
|
800
|
+
v > deadZoneMin ? percent(v, deadZoneMin, deadZoneMax) :
|
|
801
|
+
v < -deadZoneMin ? -percent(-v, deadZoneMin, deadZoneMax) : 0;
|
|
802
|
+
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// the touch gamepad owns gamepad 0 once touched, until a real gamepad is used: that one takes over and the
|
|
806
|
+
// touch gamepad hides until the screen is touched again
|
|
807
|
+
if (touchGamepadEnable && isTouchDevice && touchGamepadTimer.isSet() && gamepadsEnable && inputRealGamepadUsed())
|
|
808
|
+
touchGamepadTimer.unset();
|
|
809
|
+
if (touchGamepadEnable && isTouchDevice && touchGamepadTimer.isSet())
|
|
810
|
+
{
|
|
811
|
+
// read virtual analog stick
|
|
812
|
+
gamepadPrimary = 0; // touch gamepad uses index 0
|
|
813
|
+
const sticks = gamepadStickData[0] ?? (gamepadStickData[0] = []);
|
|
814
|
+
const dpad = gamepadDpadData[0] ?? (gamepadDpadData[0] = vec2());
|
|
815
|
+
sticks.length = 0; // only report sticks that are enabled
|
|
816
|
+
dpad.set();
|
|
817
|
+
// read each side's directional stick (analog, or quantized to an 8 way dpad)
|
|
818
|
+
for (let side = 0; side < 2; side++)
|
|
819
|
+
{
|
|
820
|
+
if (!touchGamepadSideStick(side)) continue;
|
|
821
|
+
const out = touchGamepadStickOut(side);
|
|
822
|
+
sticks[out] = vec2();
|
|
823
|
+
const touchStick = touchGamepadSticks[side] ?? vec2();
|
|
824
|
+
if (touchGamepadAnalog)
|
|
825
|
+
sticks[out] = applyDeadZones(touchStick);
|
|
826
|
+
else if (touchStick.lengthSquared() > .3)
|
|
827
|
+
{
|
|
828
|
+
const x = clamp(round(touchStick.x), -1, 1);
|
|
829
|
+
const y = clamp(round(touchStick.y), -1, 1);
|
|
830
|
+
sticks[out] = vec2(x, -y).clampLength(); // clamp to circle
|
|
831
|
+
if (!out) dpad.set(x, -y); // the primary (stick 0) also drives the dpad vector
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// read virtual gamepad buttons
|
|
836
|
+
// a press latched since the last poll counts even when the finger already lifted,
|
|
837
|
+
// then it reads pressed and released at once, like a quick key tap
|
|
838
|
+
const data = inputData[1] ?? (inputData[1] = []);
|
|
839
|
+
for (let i=12; i--;)
|
|
840
|
+
{
|
|
841
|
+
const wasDown = gamepadIsDown(i,0), isDown = touchGamepadButtons[i];
|
|
842
|
+
const pressed = touchGamepadButtonsPressed[i] || isDown && !wasDown;
|
|
843
|
+
data[i] = isDown ? pressed ? 3 : 1 : pressed ? 6 : wasDown ? 4 : 0;
|
|
844
|
+
|
|
845
|
+
// haptic tap when a face button or start button is first pressed
|
|
846
|
+
// skip stick touches (10, 11) so movement doesn't buzz
|
|
847
|
+
if (touchGamepadVibration && (data[i] & 2) &&
|
|
848
|
+
(i === 9 || touchGamepadIsFaceButton(i)))
|
|
849
|
+
vibrate(touchGamepadVibration);
|
|
850
|
+
}
|
|
851
|
+
touchGamepadButtonsPressed.length = 0;
|
|
852
|
+
gamepadButtonsLast[0] = undefined; // a real gamepad 0 starts fresh when it takes over again
|
|
853
|
+
|
|
854
|
+
// real gamepads are not read while the touch gamepad is in use
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// only poll gamepads when focused or in debug mode;
|
|
859
|
+
// what the last poll saw is forgotten meanwhile, a button let go while away is not released on return
|
|
860
|
+
if (gamepadsEnable && !debug && !document.hasFocus())
|
|
861
|
+
return void (gamepadButtonsLast.length = 0);
|
|
862
|
+
|
|
863
|
+
// poll gamepads; every slot is visited and a slot with no gamepad is cleared, so a refused or
|
|
864
|
+
// vanished gamepad does not leave its buttons held, even past the end of a shorter array
|
|
865
|
+
// like the one Firefox returns after the highest numbered gamepad disconnects
|
|
866
|
+
const maxGamepads = 8;
|
|
867
|
+
const gamepads = gamepadsEnable ? inputGetGamepads() : []; // disabled lets go of them like unplugged
|
|
868
|
+
for (let i=0; i<maxGamepads; ++i)
|
|
869
|
+
{
|
|
870
|
+
// get or create gamepad data
|
|
871
|
+
const gamepad = gamepads[i];
|
|
872
|
+
if (!gamepad)
|
|
873
|
+
{
|
|
874
|
+
// clear gamepad data if not connected
|
|
875
|
+
inputData[i+1] = undefined;
|
|
876
|
+
gamepadStickData[i] = undefined;
|
|
877
|
+
gamepadDpadData[i] = undefined;
|
|
878
|
+
gamepadHadInput[i] = undefined;
|
|
879
|
+
gamepadAxisCentered[i] = undefined;
|
|
880
|
+
gamepadButtonsLast[i] = undefined;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const data = inputData[i+1] ?? (inputData[i+1] = []);
|
|
885
|
+
const buttonsLast = gamepadButtonsLast[i] ?? (gamepadButtonsLast[i] = []);
|
|
886
|
+
const sticks = gamepadStickData[i] ?? (gamepadStickData[i] = []);
|
|
887
|
+
const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
|
|
888
|
+
|
|
889
|
+
// read analog sticks
|
|
890
|
+
// gamepads without standard mapping (steering wheels, flight sticks)
|
|
891
|
+
// can report axes that rest at full deflection instead of center,
|
|
892
|
+
// which would otherwise read as a stick held down forever, so only
|
|
893
|
+
// trust an axis once it has rested inside the dead zone for a moment
|
|
894
|
+
const isStandard = gamepad.mapping === 'standard';
|
|
895
|
+
const centered = gamepadAxisCentered[i] ?? (gamepadAxisCentered[i] = []);
|
|
896
|
+
const readAxis = (j)=>
|
|
897
|
+
{
|
|
898
|
+
const v = gamepad.axes[j];
|
|
899
|
+
if (isStandard && j < 4)
|
|
900
|
+
return v; // spec guarantees axes 0-3 are the two sticks
|
|
901
|
+
if (!gamepadAxisFilterEnable)
|
|
902
|
+
return v;
|
|
903
|
+
|
|
904
|
+
// once an axis has proven it rests at center it stays trusted,
|
|
905
|
+
// otherwise moving it would immediately disqualify it again
|
|
906
|
+
const frames = centered[j] | 0;
|
|
907
|
+
if (frames > gamepadAxisCenteredFrames)
|
|
908
|
+
return v;
|
|
909
|
+
centered[j] = abs(v) < deadZoneMin ? frames + 1 : 0;
|
|
910
|
+
return 0;
|
|
911
|
+
};
|
|
912
|
+
for (let j = 0; j < gamepad.axes.length-1; j+=2)
|
|
913
|
+
sticks[j>>1] = applyDeadZones(vec2(readAxis(j), readAxis(j+1)));
|
|
914
|
+
|
|
915
|
+
// read buttons; a stick pushed past halfway counts as input too, after the dead zone and the
|
|
916
|
+
// axis filter, so a controller can become the primary one by moving and a noisy axis cannot
|
|
917
|
+
let hadInput = sticks.some(stick=> stick.lengthSquared() > .25);
|
|
918
|
+
for (let j = gamepad.buttons.length; j--;)
|
|
919
|
+
{
|
|
920
|
+
// compare with the raw state of the last poll, not the input data an inputClear may have emptied
|
|
921
|
+
const button = gamepad.buttons[j];
|
|
922
|
+
const wasDown = buttonsLast[j];
|
|
923
|
+
buttonsLast[j] = button.pressed;
|
|
924
|
+
data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
925
|
+
|
|
926
|
+
// check for any input on this gamepad, analog must be full press
|
|
927
|
+
if (button.pressed && (!button.value || button.value > .9))
|
|
928
|
+
hadInput = true;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// set new primary gamepad if current is not connected
|
|
932
|
+
if (hadInput)
|
|
933
|
+
{
|
|
934
|
+
gamepadHadInput[i] = true;
|
|
935
|
+
if (!gamepadHadInput[gamepadPrimary])
|
|
936
|
+
gamepadPrimary = i;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (gamepad.mapping === 'standard')
|
|
940
|
+
{
|
|
941
|
+
// get dpad buttons (standard mapping)
|
|
942
|
+
dpad.set(
|
|
943
|
+
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
944
|
+
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// copy dpad to left analog stick when pressed
|
|
948
|
+
if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
|
|
949
|
+
sticks[0] = dpad.clampLength();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// disable touch gamepad if using real gamepad
|
|
953
|
+
touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function inputUpdatePost()
|
|
958
|
+
{
|
|
959
|
+
if (headlessMode) return;
|
|
960
|
+
|
|
961
|
+
// clear input to prepare for next frame
|
|
962
|
+
for (const deviceInputData of inputData)
|
|
963
|
+
for (const i in deviceInputData)
|
|
964
|
+
deviceInputData[i] &= 1;
|
|
965
|
+
mouseWheel = 0;
|
|
966
|
+
mouseDelta = vec2();
|
|
967
|
+
mouseDeltaScreen = vec2();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function inputRender()
|
|
971
|
+
{
|
|
972
|
+
touchGamepadRender();
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
976
|
+
// Touch gamepad - full-viewport HTML/SVG overlay driven by Pointer Events
|
|
977
|
+
|
|
978
|
+
const touchGamepadSvgNS = 'http://www.w3.org/2000/svg';
|
|
979
|
+
|
|
980
|
+
// build the overlay DOM once; no-op if already built, disabled, headless, or non-touch
|
|
981
|
+
function touchGamepadInit()
|
|
982
|
+
{
|
|
983
|
+
if (touchGamepadOverlay || !touchGamepadEnable || !isTouchDevice || headlessMode ||
|
|
984
|
+
!document.body) // body may not exist yet; retry on a later frame
|
|
985
|
+
return;
|
|
986
|
+
|
|
987
|
+
// full-viewport overlay; only the input zones receive pointer events. The
|
|
988
|
+
// env() padding insets the stage out of notches / home indicators natively.
|
|
989
|
+
const overlay = touchGamepadOverlay = document.createElement('div');
|
|
990
|
+
overlay.style.cssText =
|
|
991
|
+
'position:fixed;inset:0;z-index:50;pointer-events:none;opacity:0;' +
|
|
992
|
+
'touch-action:none;user-select:none;-webkit-user-select:none;' +
|
|
993
|
+
'-webkit-touch-callout:none;transition:opacity .2s;box-sizing:border-box;' +
|
|
994
|
+
'padding:env(safe-area-inset-top) env(safe-area-inset-right) ' +
|
|
995
|
+
'env(safe-area-inset-bottom) env(safe-area-inset-left)';
|
|
996
|
+
|
|
997
|
+
// stage fills the padded (safe-area) content box; all controls live inside it
|
|
998
|
+
const stage = touchGamepadStage = document.createElement('div');
|
|
999
|
+
stage.style.cssText = 'position:relative;width:100%;height:100%;pointer-events:none';
|
|
1000
|
+
overlay.appendChild(stage);
|
|
1001
|
+
|
|
1002
|
+
// svg draws every visual and never blocks input
|
|
1003
|
+
const svg = touchGamepadSvg = document.createElementNS(touchGamepadSvgNS, 'svg');
|
|
1004
|
+
svg.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;' +
|
|
1005
|
+
'pointer-events:none;overflow:visible;fill:none;stroke:#fff;stroke-width:3';
|
|
1006
|
+
stage.appendChild(svg);
|
|
1007
|
+
|
|
1008
|
+
// invisible input zones (left stick, right buttons/stick, center start)
|
|
1009
|
+
const makeZone = ()=>
|
|
1010
|
+
{
|
|
1011
|
+
const z = document.createElement('div');
|
|
1012
|
+
z.style.cssText = 'position:absolute;pointer-events:auto;touch-action:none';
|
|
1013
|
+
z.addEventListener('pointerdown', e=> touchGamepadPointerDown(e, z));
|
|
1014
|
+
z.addEventListener('pointermove', e=> touchGamepadPointerMove(e));
|
|
1015
|
+
z.addEventListener('pointerup', e=> touchGamepadPointerUp(e));
|
|
1016
|
+
z.addEventListener('pointercancel', e=> touchGamepadPointerUp(e));
|
|
1017
|
+
stage.appendChild(z);
|
|
1018
|
+
return z;
|
|
1019
|
+
};
|
|
1020
|
+
touchGamepadSideZones[0] = makeZone(); // left
|
|
1021
|
+
touchGamepadSideZones[1] = makeZone(); // right
|
|
1022
|
+
touchGamepadZoneC = makeZone(); // center/start, appended last so it sits above the sides
|
|
1023
|
+
|
|
1024
|
+
addEventListener('resize', ()=> touchGamepadNeedRelayout = true);
|
|
1025
|
+
document.body.appendChild(overlay);
|
|
1026
|
+
touchGamepadNeedRelayout = true;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// stage-local size in CSS pixels (excludes safe-area insets)
|
|
1030
|
+
function touchGamepadStageRect() { return touchGamepadStage.getBoundingClientRect(); }
|
|
1031
|
+
|
|
1032
|
+
// per-side touch gamepad config (side 0 = left, 1 = right) - the left and right
|
|
1033
|
+
// sides behave identically, differing only in position and gamepad button indices
|
|
1034
|
+
function touchGamepadSideStick(side)
|
|
1035
|
+
{ return side ? touchGamepadRightStick : touchGamepadLeftStick; }
|
|
1036
|
+
function touchGamepadSideButtonCount(side)
|
|
1037
|
+
{ return side ? touchGamepadButtonCount : touchGamepadLeftButtonCount; }
|
|
1038
|
+
// gamepad button index a side's buttons start at (right 0-3, left 4-7)
|
|
1039
|
+
function touchGamepadSideButtonBase(side)
|
|
1040
|
+
{ return side ? 0 : 4; }
|
|
1041
|
+
// output stick index for a side: the right stick uses stick 0 when there is no left stick
|
|
1042
|
+
function touchGamepadStickOut(side)
|
|
1043
|
+
{ return side && touchGamepadLeftStick ? 1 : 0; }
|
|
1044
|
+
// the button a stick's touch presses, 10 for stick 0 and 11 for stick 1, following the output index
|
|
1045
|
+
function touchGamepadStickButton(side) { return touchGamepadStickOut(side) ? 11 : 10; }
|
|
1046
|
+
// a face button's place around the cluster to its gamepad index, 2 and 3 swap to match the gamepad layout
|
|
1047
|
+
function touchGamepadFaceIndex(b) { return b === 3 ? 2 : b === 2 ? 3 : b; }
|
|
1048
|
+
// true if the side has any control (a stick or at least one button)
|
|
1049
|
+
function touchGamepadSideHasControl(side)
|
|
1050
|
+
{ return touchGamepadSideStick(side) || touchGamepadSideButtonCount(side) > 0; }
|
|
1051
|
+
|
|
1052
|
+
// true if gamepad button index i is an active touch gamepad face/single button
|
|
1053
|
+
function touchGamepadIsFaceButton(i)
|
|
1054
|
+
{
|
|
1055
|
+
for (let side = 0; side < 2; side++)
|
|
1056
|
+
{
|
|
1057
|
+
const base = touchGamepadSideButtonBase(side);
|
|
1058
|
+
if (!touchGamepadSideStick(side) &&
|
|
1059
|
+
i >= base && i < base + touchGamepadSideButtonCount(side))
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// center of a side's controls in stage-local CSS pixels (stick rest / button cluster)
|
|
1066
|
+
// returns the floating stick anchor when that side is an active floating stick
|
|
1067
|
+
function touchGamepadSideCenter(side, W, H)
|
|
1068
|
+
{
|
|
1069
|
+
if (touchGamepadFloating && touchGamepadSideStick(side) && touchGamepadStickAnchors[side])
|
|
1070
|
+
return touchGamepadStickAnchors[side];
|
|
1071
|
+
let y = H - touchGamepadSize;
|
|
1072
|
+
const count = touchGamepadSideButtonCount(side);
|
|
1073
|
+
if (!touchGamepadSideStick(side) && (count === 2 || count === 3))
|
|
1074
|
+
y -= touchGamepadSize/4; // nudge a 2/3 button cluster up a bit
|
|
1075
|
+
return vec2(side ? W - touchGamepadSize : touchGamepadSize, y);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// position the input zones for the current mode and rebuild the SVG visuals
|
|
1079
|
+
function touchGamepadRelayout()
|
|
1080
|
+
{
|
|
1081
|
+
if (!touchGamepadOverlay) return;
|
|
1082
|
+
const r = touchGamepadStageRect();
|
|
1083
|
+
const W = r.width, H = r.height, S = touchGamepadSize;
|
|
1084
|
+
const setZone = (z, css)=> z.style.cssText =
|
|
1085
|
+
'position:absolute;pointer-events:auto;touch-action:none;' + css;
|
|
1086
|
+
|
|
1087
|
+
if (paused)
|
|
1088
|
+
{
|
|
1089
|
+
// the gamepad is hidden while paused, so its side zones must not capture
|
|
1090
|
+
// touches - otherwise they silently steal taps from menus and dialogs
|
|
1091
|
+
for (const zone of touchGamepadSideZones) zone.style.display = 'none';
|
|
1092
|
+
if (touchGamepadCenterButtonSize)
|
|
1093
|
+
{
|
|
1094
|
+
// any touch presses start
|
|
1095
|
+
setZone(touchGamepadZoneC, 'inset:0');
|
|
1096
|
+
touchGamepadZoneC.style.display = '';
|
|
1097
|
+
}
|
|
1098
|
+
else
|
|
1099
|
+
touchGamepadZoneC.style.display = 'none';
|
|
1100
|
+
}
|
|
1101
|
+
else
|
|
1102
|
+
{
|
|
1103
|
+
// position each side zone (left/right differ only by which edge they hug)
|
|
1104
|
+
for (let side = 0; side < 2; side++)
|
|
1105
|
+
{
|
|
1106
|
+
const zone = touchGamepadSideZones[side], edge = side ? 'right' : 'left';
|
|
1107
|
+
zone.style.display = touchGamepadSideHasControl(side) ? '' : 'none';
|
|
1108
|
+
if (touchGamepadFloating && touchGamepadSideStick(side))
|
|
1109
|
+
{
|
|
1110
|
+
// bottom 60% grabs the stick; the top 40% passes through. A side with no
|
|
1111
|
+
// control on the other side uses the full width (matching the hit-test)
|
|
1112
|
+
const width = touchGamepadSideHasControl(side ? 0 : 1) ? '50%' : '100%';
|
|
1113
|
+
setZone(zone, `${edge}:0;bottom:0;width:${width};height:60%`);
|
|
1114
|
+
}
|
|
1115
|
+
else // fixed, and face buttons which never float: a compact box hugging the corner control
|
|
1116
|
+
setZone(zone, `${edge}:0;bottom:0;width:${3*S}px;height:${3*S}px`);
|
|
1117
|
+
}
|
|
1118
|
+
touchGamepadZoneC.style.display = touchGamepadCenterButtonSize ? '' : 'none';
|
|
1119
|
+
const c = touchGamepadCenterButtonSize;
|
|
1120
|
+
setZone(touchGamepadZoneC,
|
|
1121
|
+
`left:50%;top:50%;width:${2*c}px;height:${2*c}px;transform:translate(-50%,-50%)`);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
touchGamepadBuildSvg(W, H);
|
|
1125
|
+
touchGamepadNeedRelayout = false;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// (re)build the SVG shapes for the current layout; dynamic bits update per-frame
|
|
1129
|
+
function touchGamepadBuildSvg(W, H)
|
|
1130
|
+
{
|
|
1131
|
+
const svg = touchGamepadSvg;
|
|
1132
|
+
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
|
1133
|
+
const els = touchGamepadSvgEls = { face: [], thumb: [] };
|
|
1134
|
+
const S = touchGamepadSize;
|
|
1135
|
+
const circle = (cx, cy, rr, fill)=>
|
|
1136
|
+
{
|
|
1137
|
+
const c = document.createElementNS(touchGamepadSvgNS, 'circle');
|
|
1138
|
+
c.setAttribute('cx', cx); c.setAttribute('cy', cy); c.setAttribute('r', rr);
|
|
1139
|
+
if (fill) c.setAttribute('fill', fill);
|
|
1140
|
+
svg.appendChild(c);
|
|
1141
|
+
return c;
|
|
1142
|
+
};
|
|
1143
|
+
const cross = (ctr)=>
|
|
1144
|
+
{
|
|
1145
|
+
// plus-shaped dpad outline centered at ctr
|
|
1146
|
+
const a = S*.18, b = S*.5, x = ctr.x, y = ctr.y;
|
|
1147
|
+
const p = document.createElementNS(touchGamepadSvgNS, 'path');
|
|
1148
|
+
p.setAttribute('d',
|
|
1149
|
+
`M ${x-a} ${y-b} H ${x+a} V ${y-a} H ${x+b} V ${y+a} H ${x+a} ` +
|
|
1150
|
+
`V ${y+b} H ${x-a} V ${y+a} H ${x-b} V ${y-a} H ${x-a} Z`);
|
|
1151
|
+
svg.appendChild(p);
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
// draw each side: a directional stick, a single large button, or face buttons
|
|
1155
|
+
for (let side = 0; side < 2; side++)
|
|
1156
|
+
{
|
|
1157
|
+
const count = touchGamepadSideButtonCount(side);
|
|
1158
|
+
const base = touchGamepadSideButtonBase(side);
|
|
1159
|
+
const ctr = touchGamepadSideCenter(side, W, H);
|
|
1160
|
+
if (touchGamepadSideStick(side))
|
|
1161
|
+
{
|
|
1162
|
+
// directional stick (circle or cross) with a thumb dot that moves per-frame
|
|
1163
|
+
if (touchGamepadAnalog) circle(ctr.x, ctr.y, S/2); else cross(ctr);
|
|
1164
|
+
els.thumb[side] = circle(ctr.x, ctr.y, S/4, '#fff');
|
|
1165
|
+
}
|
|
1166
|
+
else if (count === 1)
|
|
1167
|
+
els.face[base] = circle(ctr.x, ctr.y, S/2, '#000'); // single large button
|
|
1168
|
+
else for (let i = 0; i < count; i++)
|
|
1169
|
+
{
|
|
1170
|
+
const j = mod(i-1, 4);
|
|
1171
|
+
let button = count > 2 ? j : min(j, count-1);
|
|
1172
|
+
button = touchGamepadFaceIndex(button);
|
|
1173
|
+
const offset = vec2().setDirection(j, S/2);
|
|
1174
|
+
if (count === 2) offset.x *= -1;
|
|
1175
|
+
// left side mirrors the right layout's positions, keeping indices in order
|
|
1176
|
+
// (e.g. 2 buttons -> button 4 at bottom, button 5 at left)
|
|
1177
|
+
if (!side) offset.x *= -1;
|
|
1178
|
+
const pos = ctr.add(offset);
|
|
1179
|
+
els.face[base + button] = circle(pos.x, pos.y, S/4, '#000');
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// debug: draw the proximity hit regions the hit-test actually uses
|
|
1184
|
+
if (debug && debugGamepads) touchGamepadBuildDebug(W, H);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// draw debug outlines of the touch control hit regions into the overlay svg
|
|
1188
|
+
function touchGamepadBuildDebug(W, H)
|
|
1189
|
+
{
|
|
1190
|
+
const S = touchGamepadSize, svg = touchGamepadSvg;
|
|
1191
|
+
const shape = (tag, attrs, stroke)=>
|
|
1192
|
+
{
|
|
1193
|
+
const el = document.createElementNS(touchGamepadSvgNS, tag);
|
|
1194
|
+
for (const k in attrs) el.setAttribute(k, attrs[k]);
|
|
1195
|
+
el.setAttribute('stroke', stroke);
|
|
1196
|
+
el.setAttribute('stroke-width', 2);
|
|
1197
|
+
el.setAttribute('fill', 'none');
|
|
1198
|
+
svg.appendChild(el);
|
|
1199
|
+
};
|
|
1200
|
+
const ring = (c, rr, stroke)=> shape('circle', {cx:c.x, cy:c.y, r:rr}, stroke);
|
|
1201
|
+
|
|
1202
|
+
// green line: the left/right split that assigns a stick press to a side
|
|
1203
|
+
shape('line', {x1:W/2, y1:0, x2:W/2, y2:H}, '#0f0');
|
|
1204
|
+
|
|
1205
|
+
// cyan: where each side's control can be grabbed
|
|
1206
|
+
for (let side = 0; side < 2; side++)
|
|
1207
|
+
{
|
|
1208
|
+
if (touchGamepadSideStick(side))
|
|
1209
|
+
{
|
|
1210
|
+
if (touchGamepadFloating)
|
|
1211
|
+
{
|
|
1212
|
+
// grab region: this side's half (or the full width if the other side is empty)
|
|
1213
|
+
const top = H*.4, full = !touchGamepadSideHasControl(side ? 0 : 1);
|
|
1214
|
+
const x = full ? 0 : (side ? W/2 : 0);
|
|
1215
|
+
shape('rect', {x, y:top, width:full ? W : W/2, height:H-top}, '#0ff');
|
|
1216
|
+
}
|
|
1217
|
+
else
|
|
1218
|
+
ring(touchGamepadSideCenter(side, W, H), 2*S, '#0ff');
|
|
1219
|
+
}
|
|
1220
|
+
else if (touchGamepadSideButtonCount(side) >= 1)
|
|
1221
|
+
ring(touchGamepadSideCenter(side, W, H), S, '#0ff'); // face / single-button radius
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// yellow: start button radius; magenta: where start is blocked (near a control)
|
|
1225
|
+
if (touchGamepadCenterButtonSize)
|
|
1226
|
+
{
|
|
1227
|
+
ring(vec2(W/2, H/2), touchGamepadCenterButtonSize, '#ff0');
|
|
1228
|
+
for (let side = 0; side < 2; side++)
|
|
1229
|
+
if (touchGamepadStartBlockSide(side))
|
|
1230
|
+
ring(touchGamepadSideCenter(side, W, H), 2*S, '#f0f');
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// per-frame: fade the overlay and move the thumbs / set pressed states
|
|
1235
|
+
function touchGamepadRender()
|
|
1236
|
+
{
|
|
1237
|
+
if (!touchGamepadOverlay || headlessMode) return;
|
|
1238
|
+
|
|
1239
|
+
// hide and bail if disabled at runtime (overlay stays in the DOM for reuse)
|
|
1240
|
+
// display:none also takes the input zones out of hit-testing so touches are
|
|
1241
|
+
// not silently captured away from the game while disabled
|
|
1242
|
+
if (!touchGamepadEnable || !isTouchDevice)
|
|
1243
|
+
{
|
|
1244
|
+
if (touchGamepadOverlay.style.display !== 'none')
|
|
1245
|
+
{
|
|
1246
|
+
// just disabled: hide the overlay and release any held controls, and the gamepad slot they
|
|
1247
|
+
// fed, which nothing else rewrites while real gamepads are off
|
|
1248
|
+
touchGamepadOverlay.style.display = 'none';
|
|
1249
|
+
touchGamepadPointerRole.clear();
|
|
1250
|
+
touchGamepadButtons.length = 0;
|
|
1251
|
+
touchGamepadButtonsPressed.length = 0;
|
|
1252
|
+
touchGamepadSticks.length = 0;
|
|
1253
|
+
touchGamepadStickPointerId.length = 0;
|
|
1254
|
+
inputData[1] = undefined;
|
|
1255
|
+
gamepadStickData[0] = gamepadDpadData[0] = undefined;
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
touchGamepadOverlay.style.display = '';
|
|
1260
|
+
|
|
1261
|
+
// relayout when the paused state, a layout setting, or the debug view changes
|
|
1262
|
+
const dbg = debug && debugGamepads;
|
|
1263
|
+
const layout = [touchGamepadButtonCount, touchGamepadLeftButtonCount, touchGamepadLeftStick,
|
|
1264
|
+
touchGamepadRightStick, touchGamepadAnalog, touchGamepadSize, touchGamepadFloating,
|
|
1265
|
+
touchGamepadCenterButtonSize, paused, dbg].join();
|
|
1266
|
+
if (layout !== touchGamepadLastLayout)
|
|
1267
|
+
{
|
|
1268
|
+
touchGamepadLastLayout = layout;
|
|
1269
|
+
touchGamepadNeedRelayout = true;
|
|
1270
|
+
}
|
|
1271
|
+
// relayout before the visibility bail-out so the paused full-screen start zone applies
|
|
1272
|
+
if (touchGamepadNeedRelayout) touchGamepadRelayout();
|
|
1273
|
+
|
|
1274
|
+
// fade out when idle (always show when displayTime is 0, or while debugging), a control held is in use,
|
|
1275
|
+
// its release sets the timer the fade counts from
|
|
1276
|
+
const fade = touchGamepadDisplayTime && !touchGamepadPointerRole.size ?
|
|
1277
|
+
percent(touchGamepadTimer.get(), touchGamepadDisplayTime+1, touchGamepadDisplayTime) : 1;
|
|
1278
|
+
const visible = dbg || (touchGamepadTimer.isSet() && fade > 0 && !paused);
|
|
1279
|
+
touchGamepadOverlay.style.opacity = !visible ? 0 : dbg ? 1 : fade*touchGamepadAlpha;
|
|
1280
|
+
if (!visible) return;
|
|
1281
|
+
|
|
1282
|
+
const r = touchGamepadStageRect();
|
|
1283
|
+
const W = r.width, H = r.height, S = touchGamepadSize;
|
|
1284
|
+
const els = touchGamepadSvgEls;
|
|
1285
|
+
if (!els) return;
|
|
1286
|
+
|
|
1287
|
+
for (let side = 0; side < 2; side++)
|
|
1288
|
+
if (touchGamepadSideStick(side) && els.thumb[side])
|
|
1289
|
+
{
|
|
1290
|
+
const ctr = touchGamepadSideCenter(side, W, H);
|
|
1291
|
+
const t = ctr.add((touchGamepadSticks[side] ?? vec2()).scale(S/2));
|
|
1292
|
+
els.thumb[side].setAttribute('cx', t.x);
|
|
1293
|
+
els.thumb[side].setAttribute('cy', t.y);
|
|
1294
|
+
}
|
|
1295
|
+
for (let i = 0; i < els.face.length; i++)
|
|
1296
|
+
if (els.face[i])
|
|
1297
|
+
els.face[i].setAttribute('fill', touchGamepadButtons[i] ? '#fff' : '#000');
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// convert a pointer event to stage-local CSS pixels
|
|
1301
|
+
function touchGamepadEventPos(e)
|
|
1302
|
+
{
|
|
1303
|
+
const r = touchGamepadStageRect();
|
|
1304
|
+
return vec2(e.clientX - r.left, e.clientY - r.top);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// set a directional stick from a stage-local point and flag its stick-touch button
|
|
1308
|
+
// (stick 0 press = button 10, stick 1 press = button 11, following the output index)
|
|
1309
|
+
function touchGamepadApplyStick(side, p)
|
|
1310
|
+
{
|
|
1311
|
+
const delta = p.subtract(touchGamepadStickAnchors[side]);
|
|
1312
|
+
touchGamepadSticks[side] = delta.scale(2/touchGamepadSize).clampLength();
|
|
1313
|
+
touchGamepadButtons[touchGamepadStickButton(side)] = 1;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// pick a side's gamepad button index from a stage-local point, or -1 if outside the cluster
|
|
1317
|
+
function touchGamepadFaceButtonAt(side, p, W, H)
|
|
1318
|
+
{
|
|
1319
|
+
const count = touchGamepadSideButtonCount(side);
|
|
1320
|
+
const base = touchGamepadSideButtonBase(side);
|
|
1321
|
+
const bc = touchGamepadSideCenter(side, W, H);
|
|
1322
|
+
if (bc.distance(p) >= touchGamepadSize) return -1;
|
|
1323
|
+
if (count === 1) return base; // single large button
|
|
1324
|
+
const d = bc.subtract(p);
|
|
1325
|
+
if (!side) d.x *= -1; // left side mirrors the right layout's positions horizontally
|
|
1326
|
+
let button = count === 2 ? (d.x < d.y ? 1 : 0) : mod(d.direction()+2, 4);
|
|
1327
|
+
button = touchGamepadFaceIndex(button);
|
|
1328
|
+
return button < count ? base + button : -1;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// pick which control a stage-local press activates, by priority then proximity,
|
|
1332
|
+
// independent of which zone element captured it - so overlapping zones on small
|
|
1333
|
+
// screens resolve to the nearest control instead of whichever zone is topmost
|
|
1334
|
+
// returns {role:'stick', side} or {role:'face', btn} or {role:'start'} or undefined
|
|
1335
|
+
function touchGamepadControlAt(p, W, H)
|
|
1336
|
+
{
|
|
1337
|
+
const S = touchGamepadSize;
|
|
1338
|
+
const leftHalf = p.x < W/2;
|
|
1339
|
+
const floatTop = H*.4; // floating grab region is the bottom 60% of the screen
|
|
1340
|
+
|
|
1341
|
+
// center start button first, a floating stick's region always covers it; blocked within 2*size of a fixed
|
|
1342
|
+
// control so drift off a control can't accidentally fire start (matches the original exclusion logic)
|
|
1343
|
+
if (touchGamepadCenterButtonSize && vec2(W/2, H/2).distance(p) < touchGamepadCenterButtonSize &&
|
|
1344
|
+
!touchGamepadStartBlockedAt(p, W, H))
|
|
1345
|
+
return {role:'start'};
|
|
1346
|
+
|
|
1347
|
+
// check each side (left first for priority); a side is a stick or buttons
|
|
1348
|
+
for (let side = 0; side < 2; side++)
|
|
1349
|
+
{
|
|
1350
|
+
const onHalf = side ? !leftHalf : leftHalf;
|
|
1351
|
+
if (touchGamepadSideStick(side))
|
|
1352
|
+
{
|
|
1353
|
+
// a side with no control on the other side uses the full width
|
|
1354
|
+
const otherControl = touchGamepadSideHasControl(side ? 0 : 1);
|
|
1355
|
+
const grab = touchGamepadFloating ?
|
|
1356
|
+
(!otherControl || onHalf) && p.y > floatTop :
|
|
1357
|
+
onHalf && touchGamepadSideCenter(side, W, H).distance(p) < 2*S;
|
|
1358
|
+
if (grab) return {role:'stick', side};
|
|
1359
|
+
}
|
|
1360
|
+
else if (touchGamepadSideButtonCount(side) >= 1)
|
|
1361
|
+
{
|
|
1362
|
+
const btn = touchGamepadFaceButtonAt(side, p, W, H);
|
|
1363
|
+
if (btn >= 0) return {role:'face', btn};
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// true if a press is too near a fixed control for the start button, a floating stick has no fixed place
|
|
1369
|
+
function touchGamepadStartBlockedAt(p, W, H)
|
|
1370
|
+
{
|
|
1371
|
+
for (let side = 0; side < 2; side++)
|
|
1372
|
+
if (touchGamepadStartBlockSide(side) && touchGamepadSideCenter(side, W, H).distance(p) < 2*touchGamepadSize)
|
|
1373
|
+
return true;
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
function touchGamepadStartBlockSide(side)
|
|
1377
|
+
{ return touchGamepadSideHasControl(side) && !(touchGamepadFloating && touchGamepadSideStick(side)); }
|
|
1378
|
+
|
|
1379
|
+
function touchGamepadPointerDown(e, zone)
|
|
1380
|
+
{
|
|
1381
|
+
// a mouse on a touchscreen laptop clicks the game, the zones are there even while the gamepad is hidden
|
|
1382
|
+
if (!touchGamepadEnable || e.pointerType === 'mouse') return;
|
|
1383
|
+
e.preventDefault();
|
|
1384
|
+
zone.setPointerCapture(e.pointerId);
|
|
1385
|
+
touchGamepadTimer.set();
|
|
1386
|
+
|
|
1387
|
+
// resume audio on first interaction
|
|
1388
|
+
if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
|
|
1389
|
+
audioContext.resume();
|
|
1390
|
+
|
|
1391
|
+
// while paused, any touch is the start button; a control belongs to the first finger on it until that
|
|
1392
|
+
// finger lifts, so a second finger landing on the same one neither takes it over nor lets it go
|
|
1393
|
+
if (paused)
|
|
1394
|
+
{
|
|
1395
|
+
if (touchGamepadCenterButtonSize && !touchGamepadButtons[9])
|
|
1396
|
+
{
|
|
1397
|
+
touchGamepadButtons[9] = touchGamepadButtonsPressed[9] = 1;
|
|
1398
|
+
touchGamepadPointerRole.set(e.pointerId, 'start');
|
|
1399
|
+
}
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const r = touchGamepadStageRect();
|
|
1404
|
+
const W = r.width, H = r.height;
|
|
1405
|
+
const p = vec2(e.clientX - r.left, e.clientY - r.top);
|
|
1406
|
+
|
|
1407
|
+
// choose the control by proximity/priority, not by which zone captured the touch
|
|
1408
|
+
const hit = touchGamepadControlAt(p, W, H);
|
|
1409
|
+
if (!hit) return;
|
|
1410
|
+
if (hit.role === 'stick')
|
|
1411
|
+
{
|
|
1412
|
+
const side = hit.side;
|
|
1413
|
+
if (touchGamepadStickPointerId[side] !== undefined) return; // another finger has the stick
|
|
1414
|
+
touchGamepadStickAnchors[side] = touchGamepadFloating ? p : touchGamepadSideCenter(side, W, H);
|
|
1415
|
+
touchGamepadStickPointerId[side] = e.pointerId;
|
|
1416
|
+
touchGamepadPointerRole.set(e.pointerId, 'stick'+side);
|
|
1417
|
+
touchGamepadNeedRelayout = true; // base may have re-anchored
|
|
1418
|
+
touchGamepadApplyStick(side, p);
|
|
1419
|
+
touchGamepadButtonsPressed[touchGamepadStickButton(side)] = 1; // the stick's own press, latched
|
|
1420
|
+
}
|
|
1421
|
+
else if (hit.role === 'face')
|
|
1422
|
+
{
|
|
1423
|
+
if (touchGamepadButtons[hit.btn]) return; // another finger holds the button
|
|
1424
|
+
touchGamepadButtons[hit.btn] = touchGamepadButtonsPressed[hit.btn] = 1;
|
|
1425
|
+
touchGamepadPointerRole.set(e.pointerId, 'face'+hit.btn);
|
|
1426
|
+
}
|
|
1427
|
+
else // 'start'
|
|
1428
|
+
{
|
|
1429
|
+
if (touchGamepadButtons[9]) return;
|
|
1430
|
+
touchGamepadButtons[9] = touchGamepadButtonsPressed[9] = 1;
|
|
1431
|
+
touchGamepadPointerRole.set(e.pointerId, 'start');
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function touchGamepadPointerMove(e)
|
|
1436
|
+
{
|
|
1437
|
+
const role = touchGamepadPointerRole.get(e.pointerId);
|
|
1438
|
+
if (!role) return;
|
|
1439
|
+
e.preventDefault();
|
|
1440
|
+
const p = touchGamepadEventPos(e);
|
|
1441
|
+
if (role === 'stick0' || role === 'stick1')
|
|
1442
|
+
touchGamepadApplyStick(role === 'stick1' ? 1 : 0, p);
|
|
1443
|
+
// face buttons & start are held until release (no slide-between this pass)
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function touchGamepadPointerUp(e)
|
|
1447
|
+
{
|
|
1448
|
+
const role = touchGamepadPointerRole.get(e.pointerId);
|
|
1449
|
+
if (!role) return;
|
|
1450
|
+
touchGamepadPointerRole.delete(e.pointerId);
|
|
1451
|
+
if (role === 'stick0' || role === 'stick1')
|
|
1452
|
+
{
|
|
1453
|
+
const side = role === 'stick1' ? 1 : 0;
|
|
1454
|
+
touchGamepadStickPointerId[side] = undefined;
|
|
1455
|
+
touchGamepadSticks[side] = vec2();
|
|
1456
|
+
delete touchGamepadButtons[touchGamepadStickButton(side)];
|
|
1457
|
+
}
|
|
1458
|
+
else if (role === 'start')
|
|
1459
|
+
delete touchGamepadButtons[9];
|
|
1460
|
+
else // 'face<n>'
|
|
1461
|
+
delete touchGamepadButtons[+role.slice(4)];
|
|
1462
|
+
touchGamepadTimer.set();
|
|
1294
1463
|
}
|