littlejsengine 1.15.3 → 1.15.9

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.
@@ -10,6 +10,85 @@
10
10
 
11
11
  'use strict';
12
12
 
13
+ /** Mouse pos in world space
14
+ * @type {Vector2}
15
+ * @memberof Input */
16
+ let mousePos = vec2();
17
+
18
+ /** Mouse pos in screen space
19
+ * @type {Vector2}
20
+ * @memberof Input */
21
+ let mousePosScreen = vec2();
22
+
23
+ /** Mouse movement delta in world space
24
+ * @type {Vector2}
25
+ * @memberof Input */
26
+ let mouseDelta = vec2();
27
+
28
+ /** Mouse movement delta in screen space
29
+ * @type {Vector2}
30
+ * @memberof Input */
31
+ let mouseDeltaScreen = vec2();
32
+
33
+ /** Mouse wheel delta this frame
34
+ * @type {number}
35
+ * @memberof Input */
36
+ let mouseWheel = 0;
37
+
38
+ /** True if mouse was inside the document window, set to false when mouse leaves
39
+ * @type {boolean}
40
+ * @memberof Input */
41
+ let mouseInWindow = true;
42
+
43
+ /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
44
+ * @type {boolean}
45
+ * @memberof Input */
46
+ let isUsingGamepad = false;
47
+
48
+ /** Prevents input continuing to the default browser handling (true by default)
49
+ * @type {boolean}
50
+ * @memberof Input */
51
+ let inputPreventDefault = true;
52
+
53
+ /** Primary gamepad index, automatically set to first gamepad with input
54
+ * @type {number}
55
+ * @memberof Input */
56
+ let gamepadPrimary = 0;
57
+
58
+ /** Prevents input continuing to the default browser handling
59
+ * This is useful to disable for html menus so the browser can handle input normally
60
+ * @param {boolean} preventDefault
61
+ * @memberof Input */
62
+ function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
63
+
64
+ /** Clears an input key state
65
+ * @param {string|number} key
66
+ * @param {number} [device]
67
+ * @param {boolean} [clearDown=true]
68
+ * @param {boolean} [clearPressed=true]
69
+ * @param {boolean} [clearReleased=true]
70
+ * @memberof Input */
71
+ function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
72
+ {
73
+ if (!inputData[device])
74
+ return;
75
+ inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
76
+ }
77
+
78
+ /** Clears all input
79
+ * @memberof Input */
80
+ function inputClear()
81
+ {
82
+ inputData.length = 0;
83
+ inputData[0] = [];
84
+ touchGamepadButtons.length = 0;
85
+ touchGamepadSticks.length = 0;
86
+ gamepadStickData.length = 0;
87
+ gamepadDpadData.length = 0;
88
+ }
89
+
90
+ ///////////////////////////////////////////////////////////////////////////////
91
+
13
92
  /** Returns true if device key is down
14
93
  * @param {string|number} key
15
94
  * @param {number} [device]
@@ -17,9 +96,9 @@
17
96
  * @memberof Input */
18
97
  function keyIsDown(key, device=0)
19
98
  {
20
- ASSERT(key !== undefined, 'key is undefined');
99
+ ASSERT(isString(key), 'key must be a number or string');
21
100
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
22
- return inputData[device] && !!(inputData[device][key] & 1);
101
+ return !!(inputData[device]?.[key] & 1);
23
102
  }
24
103
 
25
104
  /** Returns true if device key was pressed this frame
@@ -29,9 +108,9 @@ function keyIsDown(key, device=0)
29
108
  * @memberof Input */
30
109
  function keyWasPressed(key, device=0)
31
110
  {
32
- ASSERT(key !== undefined, 'key is undefined');
111
+ ASSERT(isString(key), 'key must be a number or string');
33
112
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
34
- return inputData[device] && !!(inputData[device][key] & 2);
113
+ return !!(inputData[device]?.[key] & 2);
35
114
  }
36
115
 
37
116
  /** Returns true if device key was released this frame
@@ -41,172 +120,192 @@ function keyWasPressed(key, device=0)
41
120
  * @memberof Input */
42
121
  function keyWasReleased(key, device=0)
43
122
  {
44
- ASSERT(key !== undefined, 'key is undefined');
123
+ ASSERT(isString(key), 'key must be a number or string');
45
124
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
46
- return inputData[device] && !!(inputData[device][key] & 4);
125
+ return !!(inputData[device]?.[key] & 4);
47
126
  }
48
127
 
49
128
  /** Returns input vector from arrow keys or WASD if enabled
129
+ * @param {string} [up]
130
+ * @param {string} [down]
131
+ * @param {string} [left]
132
+ * @param {string} [right]
50
133
  * @return {Vector2}
51
134
  * @memberof Input */
52
135
  function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
53
136
  {
137
+ ASSERT(isString(up), 'up key must be a string');
138
+ ASSERT(isString(down), 'down key must be a string');
139
+ ASSERT(isString(left), 'left key must be a string');
140
+ ASSERT(isString(right), 'right key must be a string');
54
141
  const k = (key)=> keyIsDown(key) ? 1 : 0;
55
142
  return vec2(k(right) - k(left), k(up) - k(down));
56
143
  }
57
144
 
58
- /** Clears all input
59
- * @memberof Input */
60
- function inputClear() { inputData = [[]]; touchGamepadButtons = []; }
61
-
62
- /** Clears an input key state
63
- * @param {string|number} key
64
- * @param {number} [device]
65
- * @param {boolean} [clearDown=true]
66
- * @param {boolean} [clearPressed=true]
67
- * @param {boolean} [clearReleased=true]
68
- * @memberof Input */
69
- function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearReleased=true)
70
- {
71
- if (!inputData[device])
72
- return;
73
- inputData[device][key] &= ~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0));
74
- }
75
-
76
145
  /** Returns true if mouse button is down
77
146
  * @function
78
147
  * @param {number} button
79
148
  * @return {boolean}
80
149
  * @memberof Input */
81
- function mouseIsDown(button) { return keyIsDown(button); }
150
+ function mouseIsDown(button)
151
+ {
152
+ ASSERT(isNumber(button), 'mouse button must be a number');
153
+ return keyIsDown(button);
154
+ }
82
155
 
83
156
  /** Returns true if mouse button was pressed
84
157
  * @function
85
158
  * @param {number} button
86
159
  * @return {boolean}
87
160
  * @memberof Input */
88
- function mouseWasPressed(button) { return keyWasPressed(button); }
161
+ function mouseWasPressed(button)
162
+ {
163
+ ASSERT(isNumber(button), 'mouse button must be a number');
164
+ return keyWasPressed(button);
165
+ }
89
166
 
90
167
  /** Returns true if mouse button was released
91
168
  * @function
92
169
  * @param {number} button
93
170
  * @return {boolean}
94
171
  * @memberof Input */
95
- function mouseWasReleased(button) { return keyWasReleased(button); }
96
-
97
- /** Mouse pos in world space
98
- * @type {Vector2}
99
- * @memberof Input */
100
- let mousePos = vec2();
101
-
102
- /** Mouse pos in screen space
103
- * @type {Vector2}
104
- * @memberof Input */
105
- let mousePosScreen = vec2();
106
-
107
- /** Mouse movement delta in world space
108
- * @type {Vector2}
109
- * @memberof Input */
110
- let mouseDelta = vec2();
111
-
112
- /** Mouse movement delta in screen space
113
- * @type {Vector2}
114
- * @memberof Input */
115
- let mouseDeltaScreen = vec2();
116
-
117
- /** Mouse wheel delta this frame
118
- * @type {number}
119
- * @memberof Input */
120
- let mouseWheel = 0;
121
-
122
- /** True if mouse was inside the document window, set to false when mouse leaves
123
- * @type {boolean}
124
- * @memberof Input */
125
- let mouseInWindow = true;
126
-
127
- /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
128
- * @type {boolean}
129
- * @memberof Input */
130
- let isUsingGamepad = false;
131
-
132
- /** Prevents input continuing to the default browser handling (true by default)
133
- * @type {boolean}
134
- * @memberof Input */
135
- let inputPreventDefault = true;
136
-
137
- /** Prevents input continuing to the default browser handling
138
- * This is useful to disable for html menus so the browser can handle input normally
139
- * @param {boolean} preventDefault
140
- * @memberof Input */
141
- function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
172
+ function mouseWasReleased(button)
173
+ {
174
+ ASSERT(isNumber(button), 'mouse button must be a number');
175
+ return keyWasReleased(button);
176
+ }
142
177
 
143
178
  /** Returns true if gamepad button is down
144
179
  * @param {number} button
145
180
  * @param {number} [gamepad]
146
181
  * @return {boolean}
147
182
  * @memberof Input */
148
- function gamepadIsDown(button, gamepad=0)
149
- { return keyIsDown(button, gamepad+1); }
183
+ function gamepadIsDown(button, gamepad=gamepadPrimary)
184
+ {
185
+ ASSERT(isNumber(button), 'button must be a number');
186
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
187
+ return keyIsDown(button, gamepad+1);
188
+ }
150
189
 
151
190
  /** Returns true if gamepad button was pressed
152
191
  * @param {number} button
153
192
  * @param {number} [gamepad]
154
193
  * @return {boolean}
155
194
  * @memberof Input */
156
- function gamepadWasPressed(button, gamepad=0)
157
- { return keyWasPressed(button, gamepad+1); }
195
+ function gamepadWasPressed(button, gamepad=gamepadPrimary)
196
+ {
197
+ ASSERT(isNumber(button), 'button must be a number');
198
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
199
+ return keyWasPressed(button, gamepad+1);
200
+ }
158
201
 
159
202
  /** Returns true if gamepad button was released
160
203
  * @param {number} button
161
204
  * @param {number} [gamepad]
162
205
  * @return {boolean}
163
206
  * @memberof Input */
164
- function gamepadWasReleased(button, gamepad=0)
165
- { return keyWasReleased(button, gamepad+1); }
207
+ function gamepadWasReleased(button, gamepad=gamepadPrimary)
208
+ {
209
+ ASSERT(isNumber(button), 'button must be a number');
210
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
211
+ return keyWasReleased(button, gamepad+1);
212
+ }
166
213
 
167
214
  /** Returns gamepad stick value
168
215
  * @param {number} stick
169
216
  * @param {number} [gamepad]
170
217
  * @return {Vector2}
171
218
  * @memberof Input */
172
- function gamepadStick(stick, gamepad=0)
173
- { return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
219
+ function gamepadStick(stick, gamepad=gamepadPrimary)
220
+ {
221
+ ASSERT(isNumber(stick), 'stick must be a number');
222
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
223
+ return gamepadStickData[gamepad]?.[stick] ?? vec2();
224
+ }
174
225
 
175
- ///////////////////////////////////////////////////////////////////////////////
176
- // Input system functions called automatically by engine
226
+ /** Returns gamepad dpad value
227
+ * @param {number} [gamepad]
228
+ * @return {Vector2}
229
+ * @memberof Input */
230
+ function gamepadDpad(gamepad=gamepadPrimary)
231
+ {
232
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
233
+ return gamepadDpadData[gamepad] ?? vec2();
234
+ }
177
235
 
178
- // input is stored as a bit field for each key: 1 = isDown, 2 = wasPressed, 4 = wasReleased
179
- // mouse and keyboard are stored together in device 0, gamepads are in devices > 0
180
- let inputData = [[]];
236
+ /** Returns true if passed in gamepad is connected
237
+ * @param {number} [gamepad]
238
+ * @return {boolean}
239
+ * @memberof Input */
240
+ function gamepadConnected(gamepad=gamepadPrimary)
241
+ {
242
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
243
+ return !!inputData[gamepad+1];
244
+ }
181
245
 
182
- function inputUpdate()
246
+ /** Returns how many control sticks the passed in gamepad has
247
+ * @param {number} [gamepad]
248
+ * @return {number}
249
+ * @memberof Input */
250
+ function gamepadStickCount(gamepad=gamepadPrimary)
183
251
  {
184
- if (headlessMode) return;
252
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
253
+ return gamepadStickData[gamepad]?.length ?? 0;
254
+ }
185
255
 
186
- // clear input when lost focus (prevent stuck keys)
187
- if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
188
- inputClear();
256
+ /** True if a touch device has been detected
257
+ * @memberof Input */
258
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
189
259
 
190
- // update mouse world space position and delta
191
- mousePos = screenToWorld(mousePosScreen);
192
- mouseDelta = screenToWorldDelta(mouseDeltaScreen);
260
+ ///////////////////////////////////////////////////////////////////////////////
193
261
 
194
- // update gamepads if enabled
195
- gamepadsUpdate();
262
+ /** Pulse the vibration hardware if it exists
263
+ * @param {number|Array} [pattern] - single value in ms or vibration interval array
264
+ * @memberof Input */
265
+ function vibrate(pattern=100)
266
+ {
267
+ ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
268
+ vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
196
269
  }
197
270
 
198
- function inputUpdatePost()
199
- {
200
- if (headlessMode) return;
271
+ /** Cancel any ongoing vibration
272
+ * @memberof Input */
273
+ function vibrateStop() { vibrate(0); }
201
274
 
202
- // clear input to prepare for next frame
203
- for (const deviceInputData of inputData)
204
- for (const i in deviceInputData)
205
- deviceInputData[i] &= 1;
206
- mouseWheel = 0;
207
- mouseDelta = vec2();
208
- mouseDeltaScreen = vec2();
209
- }
275
+ ///////////////////////////////////////////////////////////////////////////////
276
+ // Pointer Lock
277
+
278
+ /** Request to lock the pointer, does not work on touch devices
279
+ * @memberof Input */
280
+ function pointerLockRequest()
281
+ { !isTouchDevice && mainCanvas.requestPointerLock?.(); }
282
+
283
+ /** Request to unlock the pointer
284
+ * @memberof Input */
285
+ function pointerLockExit()
286
+ { document.exitPointerLock?.(); }
287
+
288
+ /** Check if pointer is locked (true if locked)
289
+ * @return {boolean}
290
+ * @memberof Input */
291
+ function pointerLockIsActive()
292
+ { return document.pointerLockElement === mainCanvas; }
293
+
294
+ ///////////////////////////////////////////////////////////////////////////////
295
+ // Input variables used by engine
296
+
297
+ // input uses bit field for each key: 1=isDown, 2=wasPressed, 4=wasReleased
298
+ // mouse and keyboard stored in device 0, gamepads stored in devices > 0
299
+ const inputData = [[]];
300
+
301
+ // gamepad internal variables
302
+ const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
303
+
304
+ // touch gamepad internal variables
305
+ const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
306
+
307
+ ///////////////////////////////////////////////////////////////////////////////
308
+ // Input system functions used by engine
210
309
 
211
310
  function inputInit()
212
311
  {
@@ -236,6 +335,18 @@ function inputInit()
236
335
  if (inputWASDEmulateDirection)
237
336
  inputData[0][remapKey(e.code)] = 3;
238
337
  }
338
+
339
+ // prevent arrow key from moving the page
340
+ const preventDefaultKeys =
341
+ [
342
+ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', // scrolling
343
+ 'Space', // page down scroll
344
+ 'Tab', // focus navigation
345
+ 'Backspace', // browser back
346
+ ];
347
+ if (preventDefaultKeys.includes(e.code))
348
+ if (inputPreventDefault && document.hasFocus() && e.cancelable)
349
+ e.preventDefault();
239
350
  }
240
351
  function onKeyUp(e)
241
352
  {
@@ -267,7 +378,9 @@ function inputInit()
267
378
  const mousePosScreenLast = mousePosScreen;
268
379
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
269
380
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
270
- inputPreventDefault && e.button && e.preventDefault();
381
+
382
+ if (inputPreventDefault && document.hasFocus() && e.cancelable)
383
+ e.preventDefault();
271
384
  }
272
385
  function onMouseUp(e)
273
386
  {
@@ -291,341 +404,385 @@ function inputInit()
291
404
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
292
405
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
293
406
  function onBlur() { inputClear(); } // reset input when focus is lost
294
- }
295
407
 
296
- // convert a mouse or touch event position to screen space
297
- function mouseEventToScreen(mousePos)
298
- {
299
- const rect = mainCanvas.getBoundingClientRect();
300
- const px = percent(mousePos.x, rect.left, rect.right);
301
- const py = percent(mousePos.y, rect.top, rect.bottom);
302
- return vec2(px*mainCanvas.width, py*mainCanvas.height);
303
- }
408
+ // enable touch input mouse passthrough
409
+ function touchInputInit()
410
+ {
411
+ // add non passive touch event listeners
412
+ document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
413
+ document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
414
+ document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
415
+
416
+ // handle all touch events the same way
417
+ let wasTouching;
418
+ function handleTouch(e)
419
+ {
420
+ if (!touchInputEnable)
421
+ return;
304
422
 
305
- ///////////////////////////////////////////////////////////////////////////////
306
- // Gamepad input
423
+ // route touch to gamepad
424
+ if (touchGamepadEnable)
425
+ handleTouchGamepad(e);
307
426
 
308
- // gamepad internal variables
309
- const gamepadStickData = [];
427
+ // fix stalled audio requiring user interaction
428
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
429
+ audioContext.resume();
310
430
 
311
- // gamepads are updated by engine every frame automatically
312
- function gamepadsUpdate()
313
- {
314
- const applyDeadZones = (v)=>
315
- {
316
- const min=.3, max=.8;
317
- const deadZone = (v)=>
318
- v > min ? percent(v, min, max) :
319
- v < -min ? -percent(-v, min, max) : 0;
320
- return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
321
- }
431
+ // check if touching and pass to mouse events
432
+ const touching = e.touches.length;
433
+ const button = 0; // all touches are left mouse button
434
+ if (touching)
435
+ {
436
+ // set event pos and pass it along
437
+ const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
438
+ const mousePosScreenLast = mousePosScreen;
439
+ mousePosScreen = mouseEventToScreen(pos);
440
+ if (wasTouching)
441
+ {
442
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
443
+ isUsingGamepad = touchGamepadEnable;
444
+ }
445
+ else
446
+ inputData[0][button] = 3;
447
+ }
448
+ else if (wasTouching)
449
+ inputData[0][button] = inputData[0][button] & 2 | 4;
322
450
 
323
- // update touch gamepad if enabled
324
- if (touchGamepadEnable && isTouchDevice)
325
- {
326
- if (!touchGamepadTimer.isSet())
327
- return;
451
+ // set was touching
452
+ wasTouching = touching;
328
453
 
329
- // read virtual analog stick
330
- const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
331
- sticks[0] = vec2();
332
- if (touchGamepadAnalog)
333
- sticks[0] = applyDeadZones(touchGamepadStick);
334
- else if (touchGamepadStick.lengthSquared() > .3)
335
- {
336
- // convert to 8 way dpad
337
- sticks[0].x = round(touchGamepadStick.x);
338
- sticks[0].y = -round(touchGamepadStick.y);
339
- sticks[0] = sticks[0].clampLength();
454
+ // prevent default handling like copy, magnifier lens, and scrolling
455
+ if (inputPreventDefault && document.hasFocus() && e.cancelable)
456
+ e.preventDefault();
457
+
458
+ // must return true so the document will get focus
459
+ return true;
340
460
  }
341
461
 
342
- // read virtual gamepad buttons
343
- const data = inputData[1] || (inputData[1] = []);
344
- for (let i=10; i--;)
462
+ // special handling for virtual gamepad mode
463
+ function handleTouchGamepad(e)
345
464
  {
346
- const wasDown = gamepadIsDown(i,0);
347
- data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
465
+ // clear touch gamepad input
466
+ touchGamepadSticks.length = 0;
467
+ touchGamepadSticks[0] = vec2();
468
+ touchGamepadSticks[1] = vec2();
469
+ touchGamepadButtons.length = 0;
470
+ isUsingGamepad = true;
471
+
472
+ const touching = e.touches.length;
473
+ if (touching)
474
+ {
475
+ touchGamepadTimer.set();
476
+ if (touchGamepadCenterButton && !wasTouching && paused)
477
+ {
478
+ // touch anywhere to press start when paused
479
+ touchGamepadButtons[9] = 1;
480
+ return;
481
+ }
482
+ }
483
+
484
+ // don't process touch gamepad if paused
485
+ if (paused)
486
+ return;
487
+
488
+ // get center of left and right sides
489
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
490
+ const buttonCenter = touchGamepadButtonCenter();
491
+ const startCenter = mainCanvasSize.scale(.5);
492
+
493
+ // check each touch point
494
+ for (const touch of e.touches)
495
+ {
496
+ const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
497
+ if (stickCenter.distance(touchPos) < touchGamepadSize)
498
+ {
499
+ // virtual analog stick
500
+ const delta = touchPos.subtract(stickCenter);
501
+ touchGamepadSticks[0] = delta.scale(2/touchGamepadSize).clampLength();
502
+ }
503
+ else if (buttonCenter.distance(touchPos) < touchGamepadSize)
504
+ {
505
+ if (touchGamepadButtonCount === 1)
506
+ {
507
+ // virtual right analog stick
508
+ const delta = touchPos.subtract(buttonCenter);
509
+ touchGamepadSticks[1] = delta.scale(2/touchGamepadSize).clampLength();
510
+ }
511
+ // virtual face buttons
512
+ let button = buttonCenter.subtract(touchPos).direction();
513
+ button = mod(button+2, 4);
514
+ if (touchGamepadButtonCount === 1)
515
+ button = 0;
516
+ else if (touchGamepadButtonCount === 2)
517
+ {
518
+ const delta = buttonCenter.subtract(touchPos);
519
+ button = -delta.x < delta.y ? 1 : 0;
520
+ }
521
+ // fix button locations (swap 2 and 3 to match gamepad layout)
522
+ button = button === 3 ? 2 : button === 2 ? 3 : button;
523
+ if (button < touchGamepadButtonCount)
524
+ touchGamepadButtons[button] = 1;
525
+ }
526
+ else if (touchGamepadCenterButton &&
527
+ startCenter.distance(touchPos) < touchGamepadSize)
528
+ {
529
+ // virtual start button in center
530
+ touchGamepadButtons[9] = 1;
531
+ }
532
+ }
348
533
  }
534
+ }
349
535
 
350
- // disable normal gamepads when touch gamepad is active
351
- return;
536
+ // convert a mouse or touch event position to screen space
537
+ function mouseEventToScreen(mousePos)
538
+ {
539
+ const rect = mainCanvas.getBoundingClientRect();
540
+ const px = percent(mousePos.x, rect.left, rect.right);
541
+ const py = percent(mousePos.y, rect.top, rect.bottom);
542
+ return vec2(px*mainCanvas.width, py*mainCanvas.height);
352
543
  }
544
+ }
353
545
 
354
- // return if gamepads are disabled or not supported
355
- if (!gamepadsEnable || !navigator || !navigator.getGamepads)
356
- return;
546
+ function inputUpdate()
547
+ {
548
+ if (headlessMode) return;
357
549
 
358
- // only poll gamepads when focused or in debug mode
359
- if (!debug && !document.hasFocus())
360
- return;
550
+ // clear input when lost focus (prevent stuck keys)
551
+ if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
552
+ inputClear();
361
553
 
362
- // poll gamepads
363
- const gamepads = navigator.getGamepads();
364
- for (let i = gamepads.length; i--;)
554
+ // update mouse world space position and delta
555
+ mousePos = screenToWorld(mousePosScreen);
556
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
557
+
558
+ // update gamepads if enabled
559
+ gamepadsUpdate();
560
+
561
+ // gamepads are updated by engine every frame automatically
562
+ function gamepadsUpdate()
365
563
  {
366
- // get or create gamepad data
367
- const gamepad = gamepads[i];
368
- const data = inputData[i+1] || (inputData[i+1] = []);
369
- const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
564
+ const applyDeadZones = (v)=>
565
+ {
566
+ const min=.3, max=.8;
567
+ const deadZone = (v)=>
568
+ v > min ? percent(v, min, max) :
569
+ v < -min ? -percent(-v, min, max) : 0;
570
+ return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
571
+ }
370
572
 
371
- if (gamepad)
573
+ // update touch gamepad if enabled
574
+ if (touchGamepadEnable && isTouchDevice)
372
575
  {
576
+ if (!touchGamepadTimer.isSet())
577
+ return;
578
+
579
+ // read virtual analog stick
580
+ gamepadPrimary = 0; // touch gamepad uses index 0
581
+ const sticks = gamepadStickData[0] ?? (gamepadStickData[0] = []);
582
+ const dpad = gamepadDpadData[0] ?? (gamepadDpadData[0] = vec2());
583
+ sticks[0] = vec2();
584
+ dpad.set();
585
+ const leftTouchStick = touchGamepadSticks[0] ?? vec2();
586
+ if (touchGamepadAnalog)
587
+ sticks[0] = applyDeadZones(leftTouchStick);
588
+ else if (leftTouchStick.lengthSquared() > .3)
589
+ {
590
+ // convert to 8 way dpad
591
+ const x = clamp(round(leftTouchStick.x), -1, 1);
592
+ const y = clamp(round(leftTouchStick.y), -1, 1);
593
+ dpad.set(x, -y);
594
+ sticks[0] = dpad.clampLength(); // clamp to circle
595
+ }
596
+ if (touchGamepadButtonCount === 1)
597
+ {
598
+ const rightTouchStick = touchGamepadSticks[1] ?? vec2();
599
+ sticks[1] = applyDeadZones(rightTouchStick);
600
+ }
601
+
602
+ // read virtual gamepad buttons
603
+ const data = inputData[1] ?? (inputData[1] = []);
604
+ for (let i=10; i--;)
605
+ {
606
+ const wasDown = gamepadIsDown(i,0);
607
+ data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
608
+ }
609
+
610
+ // disable normal gamepads when touch gamepad is active
611
+ return;
612
+ }
613
+
614
+ // return if gamepads are disabled or not supported
615
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads)
616
+ return;
617
+
618
+ // only poll gamepads when focused or in debug mode
619
+ if (!debug && !document.hasFocus())
620
+ return;
621
+
622
+ // poll gamepads
623
+ const maxGamepads = 8;
624
+ const gamepads = navigator.getGamepads();
625
+ const gamepadCount = min(maxGamepads, gamepads.length)
626
+ for (let i=0; i<gamepadCount; ++i)
627
+ {
628
+ // get or create gamepad data
629
+ const gamepad = gamepads[i];
630
+ if (!gamepad)
631
+ {
632
+ // clear gamepad data if not connected
633
+ inputData[i+1] = undefined;
634
+ gamepadStickData[i] = undefined;
635
+ gamepadDpadData[i] = undefined;
636
+ gamepadHadInput[i] = undefined;
637
+ continue;
638
+ }
639
+
640
+ const data = inputData[i+1] ?? (inputData[i+1] = []);
641
+ const sticks = gamepadStickData[i] ?? (gamepadStickData[i] = []);
642
+ const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
643
+
373
644
  // read analog sticks
374
645
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
375
646
  sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
376
647
 
377
648
  // read buttons
649
+ let hadInput = false;
378
650
  for (let j = gamepad.buttons.length; j--;)
379
651
  {
380
652
  const button = gamepad.buttons[j];
381
653
  const wasDown = gamepadIsDown(j,i);
382
654
  data[j] = button.pressed ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
383
- if (!button.value || button.value > .9) // must be a full press
384
- if (!i && button.pressed)
385
- isUsingGamepad = true;
655
+
656
+ // check for any input on this gamepad, analog must be full press
657
+ if (button.pressed)
658
+ if (!button.value || button.value > .9)
659
+ hadInput = true;
660
+ }
661
+
662
+ // set new primary gamepad if current is not connected
663
+ if (hadInput)
664
+ {
665
+ gamepadHadInput[i] = true;
666
+ if (!gamepadHadInput[gamepadPrimary])
667
+ gamepadPrimary = i;
668
+ isUsingGamepad ||= (gamepadPrimary === i);
386
669
  }
387
670
 
388
- if (gamepadDirectionEmulateStick)
671
+ if (gamepad.mapping === 'standard')
389
672
  {
390
- // copy dpad to left analog stick when pressed
391
- const dpad = vec2(
673
+ // get dpad buttons (standard mapping)
674
+ dpad.set(
392
675
  (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
393
676
  (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
394
- if (dpad.lengthSquared())
395
- sticks[0] = dpad.clampLength();
677
+ }
678
+ else if (gamepad.axes && gamepad.axes.length >= 2)
679
+ {
680
+ // digital style dpad from axes
681
+ const x = clamp(round(gamepad.axes[0]), -1, 1);
682
+ const y = clamp(round(gamepad.axes[1]), -1, 1);
683
+ dpad.set(x, -y);
396
684
  }
397
685
 
398
- // disable touch gamepad if using real gamepad
399
- touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
686
+ // copy dpad to left analog stick when pressed
687
+ if (gamepadDirectionEmulateStick && !dpad.isZero())
688
+ sticks[0] = dpad.clampLength();
400
689
  }
690
+
691
+ // disable touch gamepad if using real gamepad
692
+ touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
401
693
  }
402
694
  }
403
695
 
404
- ///////////////////////////////////////////////////////////////////////////////
405
-
406
- /** Pulse the vibration hardware if it exists
407
- * @param {number|Array} [pattern] - single value in ms or vibration interval array
408
- * @memberof Input */
409
- function vibrate(pattern=100)
410
- { vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
411
-
412
- /** Cancel any ongoing vibration
413
- * @memberof Input */
414
- function vibrateStop() { vibrate(0); }
415
-
416
- ///////////////////////////////////////////////////////////////////////////////
417
- // Touch input & virtual on screen gamepad
418
-
419
- /** True if a touch device has been detected
420
- * @memberof Input */
421
- const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
422
-
423
- // touch gamepad internal variables
424
- let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
425
-
426
- function touchGamepadButtonCenter()
696
+ function inputUpdatePost()
427
697
  {
428
- // draw right face buttons
429
- const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
430
- if (touchGamepadButtonCount <= 2)
431
- center.x += touchGamepadSize/2;
432
- return center;
698
+ if (headlessMode) return;
699
+
700
+ // clear input to prepare for next frame
701
+ for (const deviceInputData of inputData)
702
+ for (const i in deviceInputData)
703
+ deviceInputData[i] &= 1;
704
+ mouseWheel = 0;
705
+ mouseDelta = vec2();
706
+ mouseDeltaScreen = vec2();
433
707
  }
434
708
 
435
- // enable touch input mouse passthrough
436
- function touchInputInit()
709
+ function inputRender()
437
710
  {
438
- // add non passive touch event listeners
439
- document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
440
- document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
441
- document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
442
-
443
- // handle all touch events the same way
444
- let wasTouching;
445
- function handleTouch(e)
711
+ touchGamepadRender();
712
+
713
+ function touchGamepadRender()
446
714
  {
447
- if (!touchInputEnable)
715
+ if (!touchInputEnable || !isTouchDevice || headlessMode) return;
716
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet())
448
717
  return;
449
718
 
450
- // route touch to gamepad
451
- if (touchGamepadEnable)
452
- handleTouchGamepad(e);
719
+ // fade off when not touching or paused
720
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
721
+ if (!alpha || paused)
722
+ return;
453
723
 
454
- // fix stalled audio requiring user interaction
455
- if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
456
- audioContext.resume();
724
+ // setup the canvas
725
+ const context = overlayContext;
726
+ context.save();
727
+ context.globalAlpha = alpha*touchGamepadAlpha;
728
+ context.strokeStyle = '#fff';
729
+ context.lineWidth = 3;
457
730
 
458
- // check if touching and pass to mouse events
459
- const touching = e.touches.length;
460
- const button = 0; // all touches are left mouse button
461
- if (touching)
731
+ // draw left analog stick
732
+ const leftTouchStick = touchGamepadSticks[0] ?? vec2();
733
+ context.fillStyle = leftTouchStick.lengthSquared() > 0 ? '#fff' : '#000';
734
+ context.beginPath();
735
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
736
+ if (touchGamepadAnalog)
462
737
  {
463
- // set event pos and pass it along
464
- const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
465
- const mousePosScreenLast = mousePosScreen;
466
- mousePosScreen = mouseEventToScreen(pos);
467
- if (wasTouching)
468
- {
469
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
470
- isUsingGamepad = touchGamepadEnable;
471
- }
472
- else
473
- inputData[0][button] = 3;
738
+ // draw circle shaped gamepad
739
+ context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
474
740
  }
475
- else if (wasTouching)
476
- inputData[0][button] = inputData[0][button] & 2 | 4;
477
-
478
- // set was touching
479
- wasTouching = touching;
480
-
481
- // prevent default handling like copy, magnifier lens, and scrolling
482
- if (inputPreventDefault && document.hasFocus() && e.cancelable)
483
- e.preventDefault();
484
-
485
- // must return true so the document will get focus
486
- return true;
487
- }
488
-
489
- // special handling for virtual gamepad mode
490
- function handleTouchGamepad(e)
491
- {
492
- // clear touch gamepad input
493
- touchGamepadStick = vec2();
494
- touchGamepadButtons = [];
495
- isUsingGamepad = true;
496
-
497
- const touching = e.touches.length;
498
- if (touching)
741
+ else
499
742
  {
500
- touchGamepadTimer.set();
501
- if (touchGamepadCenterButton && !wasTouching && paused)
743
+ // draw cross shaped gamepad
744
+ for (let i=10; --i;)
502
745
  {
503
- // touch anywhere to press start when paused
504
- touchGamepadButtons[9] = 1;
505
- return;
746
+ const angle = i*PI/4;
747
+ context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
748
+ i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
506
749
  }
507
750
  }
751
+ context.fill();
752
+ context.stroke();
508
753
 
509
- // get center of left and right sides
510
- const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
511
- const buttonCenter = touchGamepadButtonCenter();
512
- const startCenter = mainCanvasSize.scale(.5);
513
-
514
- // check each touch point
515
- for (const touch of e.touches)
754
+ // draw right face buttons
516
755
  {
517
- const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
518
- if (stickCenter.distance(touchPos) < touchGamepadSize)
519
- {
520
- // virtual analog stick
521
- touchGamepadStick = touchPos.subtract(stickCenter).scale(2/touchGamepadSize).clampLength();
522
- }
523
- else if (buttonCenter.distance(touchPos) < touchGamepadSize)
756
+ const buttonCenter = touchGamepadButtonCenter();
757
+ const buttonSize = touchGamepadButtonCount > 1 ?
758
+ touchGamepadSize/4 : touchGamepadSize/2;
759
+ for (let i=0; i<touchGamepadButtonCount; i++)
524
760
  {
525
- // virtual face buttons
526
- let button = buttonCenter.subtract(touchPos).direction();
527
- button = mod(button+2, 4);
528
- if (touchGamepadButtonCount === 1)
529
- button = 0;
530
- else if (touchGamepadButtonCount === 2)
531
- {
532
- const delta = buttonCenter.subtract(touchPos);
533
- button = -delta.x < delta.y ? 1 : 0;
534
- }
761
+ const j = mod(i-1, 4);
762
+ let button = touchGamepadButtonCount > 2 ?
763
+ j : min(j, touchGamepadButtonCount-1);
535
764
  // fix button locations (swap 2 and 3 to match gamepad layout)
536
765
  button = button === 3 ? 2 : button === 2 ? 3 : button;
537
- if (button < touchGamepadButtonCount)
538
- touchGamepadButtons[button] = 1;
539
- }
540
- else if (touchGamepadCenterButton && !wasTouching &&
541
- startCenter.distance(touchPos) < touchGamepadSize)
542
- {
543
- // virtual start button in center
544
- touchGamepadButtons[9] = 1;
766
+ const pos = touchGamepadButtonCount < 2 ? buttonCenter :
767
+ buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
768
+ context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
769
+ context.beginPath();
770
+ context.arc(pos.x, pos.y, buttonSize, 0,9);
771
+ context.fill();
772
+ context.stroke();
545
773
  }
546
774
  }
547
- }
548
- }
549
-
550
- // render the touch gamepad, called automatically by the engine
551
- function touchGamepadRender()
552
- {
553
- if (!touchInputEnable || !isTouchDevice || headlessMode) return;
554
- if (!touchGamepadEnable || !touchGamepadTimer.isSet())
555
- return;
556
-
557
- // fade off when not touching or paused
558
- const alpha = percent(touchGamepadTimer.get(), 4, 3);
559
- if (!alpha || paused)
560
- return;
561
-
562
- // setup the canvas
563
- const context = overlayContext;
564
- context.save();
565
- context.globalAlpha = alpha*touchGamepadAlpha;
566
- context.strokeStyle = '#fff';
567
- context.lineWidth = 3;
568
-
569
- // draw left analog stick
570
- context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
571
- context.beginPath();
572
- const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
573
- if (touchGamepadAnalog) // draw circle shaped gamepad
574
- {
575
- context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
576
- context.fill();
577
- context.stroke();
578
- }
579
- else // draw cross shaped gamepad
580
- {
581
- for (let i=10; i--;)
582
- {
583
- const angle = i*PI/4;
584
- context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
585
- i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
586
- i===1 && context.fill();
587
- }
588
- context.stroke();
589
- }
590
775
 
591
- // draw right face buttons
592
- const buttonCenter = touchGamepadButtonCenter();
593
- const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
594
- for (let i=0; i<touchGamepadButtonCount; i++)
595
- {
596
- const j = mod(i-1, 4);
597
- let button = touchGamepadButtonCount > 2 ?
598
- j : min(j, touchGamepadButtonCount-1);
599
- // fix button locations (swap 2 and 3 to match gamepad layout)
600
- button = button === 3 ? 2 : button === 2 ? 3 : button;
601
- const pos = buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
602
- context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
603
- context.beginPath();
604
- context.arc(pos.x, pos.y, buttonSize, 0,9);
605
- context.fill();
606
- context.stroke();
776
+ // set canvas back to normal
777
+ context.restore();
607
778
  }
608
-
609
- // set canvas back to normal
610
- context.restore();
611
779
  }
612
780
 
613
- ///////////////////////////////////////////////////////////////////////////////
614
- // Pointer Lock
615
-
616
- /** Request to lock the pointer, does not work on touch devices
617
- * @memberof Input */
618
- function pointerLockRequest()
781
+ // center position for right tocuh pad face buttons
782
+ function touchGamepadButtonCenter()
619
783
  {
620
- if (!isTouchDevice)
621
- mainCanvas.requestPointerLock && mainCanvas.requestPointerLock();
622
- }
623
-
624
- /** Request to unlock the pointer
625
- * @memberof Input */
626
- function pointerLockExit() { document.exitPointerLock && document.exitPointerLock(); }
627
-
628
- /** Check if pointer is locked (true if locked)
629
- * @return {boolean}
630
- * @memberof Input */
631
- function pointerLockIsActive() { return document.pointerLockElement === mainCanvas; }
784
+ const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
785
+ if (touchGamepadButtonCount === 2)
786
+ center.x += touchGamepadSize/2;
787
+ return center;
788
+ }