littlejsengine 1.15.2 → 1.15.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/littlejs.d.ts +190 -63
  2. package/dist/littlejs.esm.js +1267 -605
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +1261 -604
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +1179 -552
  7. package/examples/electron/index.html +2 -2
  8. package/examples/index.html +4 -3
  9. package/examples/particles/index.html +22 -6
  10. package/examples/shorts/base.html +2 -1
  11. package/examples/shorts/box2dCar.js +1 -1
  12. package/examples/shorts/box2dPool.js +9 -10
  13. package/examples/shorts/empty.js +1 -1
  14. package/examples/shorts/input.js +30 -25
  15. package/examples/shorts/music.js +1 -0
  16. package/examples/shorts/musicPlayer.js +1 -0
  17. package/examples/shorts/sequencer.js +2 -0
  18. package/examples/shorts/sound.js +1 -0
  19. package/examples/shorts/tileLayer.js +2 -2
  20. package/examples/shorts/tiles.png +0 -0
  21. package/examples/shorts/timers.js +1 -0
  22. package/examples/shorts/uiSystem.js +26 -14
  23. package/examples/shorts/videoPlayer.js +2 -1
  24. package/examples/starter/index.html +3 -2
  25. package/examples/uiSystem/game.js +28 -15
  26. package/package.json +1 -1
  27. package/plugins/box2d.js +1 -1
  28. package/plugins/desktop.ini +2 -0
  29. package/plugins/pluginExport.js +2 -0
  30. package/plugins/uiSystem.js +503 -91
  31. package/src/engine.js +2 -169
  32. package/src/engineAudio.js +0 -1
  33. package/src/engineBuild.js +1 -0
  34. package/src/engineDebug.js +84 -54
  35. package/src/engineDraw.js +81 -40
  36. package/src/engineExport.js +4 -1
  37. package/src/engineInput.js +526 -381
  38. package/src/engineObject.js +27 -23
  39. package/src/engineSettings.js +11 -6
  40. package/src/engineSplash.js +173 -0
  41. package/src/engineTileLayer.js +3 -3
  42. package/src/engineUtilities.js +17 -1
@@ -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,182 @@ 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(); }
174
-
175
- ///////////////////////////////////////////////////////////////////////////////
176
- // Input system functions called automatically by engine
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
+ }
177
225
 
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 = [[]];
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
+ }
181
235
 
182
- function inputUpdate()
236
+ /** Returns true if passed in gamepad is connected
237
+ * @param {number} [gamepad]
238
+ * @return {boolean}
239
+ * @memberof Input */
240
+ function gamepadConnected(gamepad=gamepadPrimary)
183
241
  {
184
- if (headlessMode) return;
242
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
243
+ return !!inputData[gamepad+1];
244
+ }
185
245
 
186
- // clear input when lost focus (prevent stuck keys)
187
- if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
188
- inputClear();
246
+ /** True if a touch device has been detected
247
+ * @memberof Input */
248
+ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
189
249
 
190
- // update mouse world space position and delta
191
- mousePos = screenToWorld(mousePosScreen);
192
- mouseDelta = screenToWorldDelta(mouseDeltaScreen);
250
+ ///////////////////////////////////////////////////////////////////////////////
193
251
 
194
- // update gamepads if enabled
195
- gamepadsUpdate();
252
+ /** Pulse the vibration hardware if it exists
253
+ * @param {number|Array} [pattern] - single value in ms or vibration interval array
254
+ * @memberof Input */
255
+ function vibrate(pattern=100)
256
+ {
257
+ ASSERT(isNumber(pattern) || isArray(pattern), 'pattern must be a number or array');
258
+ vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern);
196
259
  }
197
260
 
198
- function inputUpdatePost()
199
- {
200
- if (headlessMode) return;
261
+ /** Cancel any ongoing vibration
262
+ * @memberof Input */
263
+ function vibrateStop() { vibrate(0); }
201
264
 
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
- }
265
+ ///////////////////////////////////////////////////////////////////////////////
266
+ // Pointer Lock
267
+
268
+ /** Request to lock the pointer, does not work on touch devices
269
+ * @memberof Input */
270
+ function pointerLockRequest()
271
+ { !isTouchDevice && mainCanvas.requestPointerLock?.(); }
272
+
273
+ /** Request to unlock the pointer
274
+ * @memberof Input */
275
+ function pointerLockExit()
276
+ { document.exitPointerLock?.(); }
277
+
278
+ /** Check if pointer is locked (true if locked)
279
+ * @return {boolean}
280
+ * @memberof Input */
281
+ function pointerLockIsActive()
282
+ { return document.pointerLockElement === mainCanvas; }
283
+
284
+ ///////////////////////////////////////////////////////////////////////////////
285
+ // Input variables used by engine
286
+
287
+ // input uses bit field for each key: 1=isDown, 2=wasPressed, 4=wasReleased
288
+ // mouse and keyboard stored in device 0, gamepads stored in devices > 0
289
+ const inputData = [[]];
290
+
291
+ // gamepad internal variables
292
+ const gamepadStickData = [], gamepadDpadData = [], gamepadHadInput = [];
293
+
294
+ // touch gamepad internal variables
295
+ const touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadSticks = [];
296
+
297
+ ///////////////////////////////////////////////////////////////////////////////
298
+ // Input system functions used by engine
210
299
 
211
300
  function inputInit()
212
301
  {
@@ -236,6 +325,18 @@ function inputInit()
236
325
  if (inputWASDEmulateDirection)
237
326
  inputData[0][remapKey(e.code)] = 3;
238
327
  }
328
+
329
+ // prevent arrow key from moving the page
330
+ const preventDefaultKeys =
331
+ [
332
+ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', // scrolling
333
+ 'Space', // page down scroll
334
+ 'Tab', // focus navigation
335
+ 'Backspace', // browser back
336
+ ];
337
+ if (preventDefaultKeys.includes(e.code))
338
+ if (inputPreventDefault && document.hasFocus() && e.cancelable)
339
+ e.preventDefault();
239
340
  }
240
341
  function onKeyUp(e)
241
342
  {
@@ -267,7 +368,9 @@ function inputInit()
267
368
  const mousePosScreenLast = mousePosScreen;
268
369
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
269
370
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
270
- inputPreventDefault && e.button && e.preventDefault();
371
+
372
+ if (inputPreventDefault && document.hasFocus() && e.cancelable)
373
+ e.preventDefault();
271
374
  }
272
375
  function onMouseUp(e)
273
376
  {
@@ -291,341 +394,383 @@ function inputInit()
291
394
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
292
395
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
293
396
  function onBlur() { inputClear(); } // reset input when focus is lost
294
- }
295
397
 
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
- }
398
+ // enable touch input mouse passthrough
399
+ function touchInputInit()
400
+ {
401
+ // add non passive touch event listeners
402
+ document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
403
+ document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
404
+ document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
405
+
406
+ // handle all touch events the same way
407
+ let wasTouching;
408
+ function handleTouch(e)
409
+ {
410
+ if (!touchInputEnable)
411
+ return;
304
412
 
305
- ///////////////////////////////////////////////////////////////////////////////
306
- // Gamepad input
413
+ // route touch to gamepad
414
+ if (touchGamepadEnable)
415
+ handleTouchGamepad(e);
307
416
 
308
- // gamepad internal variables
309
- const gamepadStickData = [];
417
+ // fix stalled audio requiring user interaction
418
+ if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
419
+ audioContext.resume();
310
420
 
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
- }
421
+ // check if touching and pass to mouse events
422
+ const touching = e.touches.length;
423
+ const button = 0; // all touches are left mouse button
424
+ if (touching)
425
+ {
426
+ // set event pos and pass it along
427
+ const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
428
+ const mousePosScreenLast = mousePosScreen;
429
+ mousePosScreen = mouseEventToScreen(pos);
430
+ if (wasTouching)
431
+ {
432
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
433
+ isUsingGamepad = touchGamepadEnable;
434
+ }
435
+ else
436
+ inputData[0][button] = 3;
437
+ }
438
+ else if (wasTouching)
439
+ inputData[0][button] = inputData[0][button] & 2 | 4;
322
440
 
323
- // update touch gamepad if enabled
324
- if (touchGamepadEnable && isTouchDevice)
325
- {
326
- if (!touchGamepadTimer.isSet())
327
- return;
441
+ // set was touching
442
+ wasTouching = touching;
328
443
 
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();
444
+ // prevent default handling like copy, magnifier lens, and scrolling
445
+ if (inputPreventDefault && document.hasFocus() && e.cancelable)
446
+ e.preventDefault();
447
+
448
+ // must return true so the document will get focus
449
+ return true;
340
450
  }
341
451
 
342
- // read virtual gamepad buttons
343
- const data = inputData[1] || (inputData[1] = []);
344
- for (let i=10; i--;)
452
+ // special handling for virtual gamepad mode
453
+ function handleTouchGamepad(e)
345
454
  {
346
- const wasDown = gamepadIsDown(i,0);
347
- data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
455
+ // clear touch gamepad input
456
+ touchGamepadSticks.length = 0;
457
+ touchGamepadSticks[0] = vec2();
458
+ touchGamepadSticks[1] = vec2();
459
+ touchGamepadButtons.length = 0;
460
+ isUsingGamepad = true;
461
+
462
+ const touching = e.touches.length;
463
+ if (touching)
464
+ {
465
+ touchGamepadTimer.set();
466
+ if (touchGamepadCenterButton && !wasTouching && paused)
467
+ {
468
+ // touch anywhere to press start when paused
469
+ touchGamepadButtons[9] = 1;
470
+ return;
471
+ }
472
+ }
473
+
474
+ // don't process touch gamepad if paused
475
+ if (paused)
476
+ return;
477
+
478
+ // get center of left and right sides
479
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
480
+ const buttonCenter = touchGamepadButtonCenter();
481
+ const startCenter = mainCanvasSize.scale(.5);
482
+
483
+ // check each touch point
484
+ for (const touch of e.touches)
485
+ {
486
+ const touchPos = mouseEventToScreen(vec2(touch.clientX, touch.clientY));
487
+ if (stickCenter.distance(touchPos) < touchGamepadSize)
488
+ {
489
+ // virtual analog stick
490
+ const delta = touchPos.subtract(stickCenter);
491
+ touchGamepadSticks[0] = delta.scale(2/touchGamepadSize).clampLength();
492
+ }
493
+ else if (buttonCenter.distance(touchPos) < touchGamepadSize)
494
+ {
495
+ if (touchGamepadButtonCount === 1)
496
+ {
497
+ // virtual right analog stick
498
+ const delta = touchPos.subtract(buttonCenter);
499
+ touchGamepadSticks[1] = delta.scale(2/touchGamepadSize).clampLength();
500
+ }
501
+ // virtual face buttons
502
+ let button = buttonCenter.subtract(touchPos).direction();
503
+ button = mod(button+2, 4);
504
+ if (touchGamepadButtonCount === 1)
505
+ button = 0;
506
+ else if (touchGamepadButtonCount === 2)
507
+ {
508
+ const delta = buttonCenter.subtract(touchPos);
509
+ button = -delta.x < delta.y ? 1 : 0;
510
+ }
511
+ // fix button locations (swap 2 and 3 to match gamepad layout)
512
+ button = button === 3 ? 2 : button === 2 ? 3 : button;
513
+ if (button < touchGamepadButtonCount)
514
+ touchGamepadButtons[button] = 1;
515
+ }
516
+ else if (touchGamepadCenterButton &&
517
+ startCenter.distance(touchPos) < touchGamepadSize)
518
+ {
519
+ // virtual start button in center
520
+ touchGamepadButtons[9] = 1;
521
+ }
522
+ }
348
523
  }
524
+ }
349
525
 
350
- // disable normal gamepads when touch gamepad is active
351
- return;
526
+ // convert a mouse or touch event position to screen space
527
+ function mouseEventToScreen(mousePos)
528
+ {
529
+ const rect = mainCanvas.getBoundingClientRect();
530
+ const px = percent(mousePos.x, rect.left, rect.right);
531
+ const py = percent(mousePos.y, rect.top, rect.bottom);
532
+ return vec2(px*mainCanvas.width, py*mainCanvas.height);
352
533
  }
534
+ }
353
535
 
354
- // return if gamepads are disabled or not supported
355
- if (!gamepadsEnable || !navigator || !navigator.getGamepads)
356
- return;
536
+ function inputUpdate()
537
+ {
538
+ if (headlessMode) return;
357
539
 
358
- // only poll gamepads when focused or in debug mode
359
- if (!debug && !document.hasFocus())
360
- return;
540
+ // clear input when lost focus (prevent stuck keys)
541
+ if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
542
+ inputClear();
543
+
544
+ // update mouse world space position and delta
545
+ mousePos = screenToWorld(mousePosScreen);
546
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
361
547
 
362
- // poll gamepads
363
- const gamepads = navigator.getGamepads();
364
- for (let i = gamepads.length; i--;)
548
+ // update gamepads if enabled
549
+ gamepadsUpdate();
550
+
551
+ // gamepads are updated by engine every frame automatically
552
+ function gamepadsUpdate()
365
553
  {
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] = []);
554
+ const applyDeadZones = (v)=>
555
+ {
556
+ const min=.3, max=.8;
557
+ const deadZone = (v)=>
558
+ v > min ? percent(v, min, max) :
559
+ v < -min ? -percent(-v, min, max) : 0;
560
+ return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
561
+ }
562
+
563
+ // update touch gamepad if enabled
564
+ if (touchGamepadEnable && isTouchDevice)
565
+ {
566
+ if (!touchGamepadTimer.isSet())
567
+ return;
568
+
569
+ // read virtual analog stick
570
+ gamepadPrimary = 0; // touch gamepad uses index 0
571
+ const sticks = gamepadStickData[0] ?? (gamepadStickData[0] = []);
572
+ const dpad = gamepadDpadData[0] ?? (gamepadDpadData[0] = vec2());
573
+ sticks[0] = vec2();
574
+ dpad.set();
575
+ const leftTouchStick = touchGamepadSticks[0] ?? vec2();
576
+ if (touchGamepadAnalog)
577
+ sticks[0] = applyDeadZones(leftTouchStick);
578
+ else if (leftTouchStick.lengthSquared() > .3)
579
+ {
580
+ // convert to 8 way dpad
581
+ const x = clamp(round(leftTouchStick.x), -1, 1);
582
+ const y = clamp(round(leftTouchStick.y), -1, 1);
583
+ dpad.set(x, -y);
584
+ sticks[0] = dpad.clampLength(); // clamp to circle
585
+ }
586
+ const rightTouchStick = touchGamepadSticks[1] ?? vec2();
587
+ if (touchGamepadButtonCount === 1)
588
+ sticks[1] = applyDeadZones(rightTouchStick);
589
+
590
+ // read virtual gamepad buttons
591
+ const data = inputData[1] ?? (inputData[1] = []);
592
+ for (let i=10; i--;)
593
+ {
594
+ const wasDown = gamepadIsDown(i,0);
595
+ data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
596
+ }
370
597
 
371
- if (gamepad)
598
+ // disable normal gamepads when touch gamepad is active
599
+ return;
600
+ }
601
+
602
+ // return if gamepads are disabled or not supported
603
+ if (!gamepadsEnable || !navigator || !navigator.getGamepads)
604
+ return;
605
+
606
+ // only poll gamepads when focused or in debug mode
607
+ if (!debug && !document.hasFocus())
608
+ return;
609
+
610
+ // poll gamepads
611
+ const maxGamepads = 8;
612
+ const gamepads = navigator.getGamepads();
613
+ const gamepadCount = min(maxGamepads, gamepads.length)
614
+ for (let i=0; i<gamepadCount; ++i)
372
615
  {
616
+ // get or create gamepad data
617
+ const gamepad = gamepads[i];
618
+ if (!gamepad)
619
+ {
620
+ // clear gamepad data if not connected
621
+ inputData[i+1] = undefined;
622
+ gamepadStickData[i] = undefined;
623
+ gamepadDpadData[i] = undefined;
624
+ gamepadHadInput[i] = undefined;
625
+ continue;
626
+ }
627
+
628
+ const data = inputData[i+1] ?? (inputData[i+1] = []);
629
+ const sticks = gamepadStickData[i] ?? (gamepadStickData[i] = []);
630
+ const dpad = gamepadDpadData[i] ?? (gamepadDpadData[i] = vec2());
631
+
373
632
  // read analog sticks
374
633
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
375
634
  sticks[j>>1] = applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));
376
635
 
377
636
  // read buttons
637
+ let hadInput = false;
378
638
  for (let j = gamepad.buttons.length; j--;)
379
639
  {
380
640
  const button = gamepad.buttons[j];
381
641
  const wasDown = gamepadIsDown(j,i);
382
642
  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;
643
+
644
+ // check for any input on this gamepad, analog must be full press
645
+ if (button.pressed)
646
+ if (!button.value || button.value > .9)
647
+ hadInput = true;
648
+ }
649
+
650
+ // set new primary gamepad if current is not connected
651
+ if (hadInput)
652
+ {
653
+ gamepadHadInput[i] = true;
654
+ if (!gamepadHadInput[gamepadPrimary])
655
+ gamepadPrimary = i;
656
+ isUsingGamepad ||= (gamepadPrimary === i);
386
657
  }
387
658
 
388
- if (gamepadDirectionEmulateStick)
659
+ if (gamepad.mapping === 'standard')
389
660
  {
390
- // copy dpad to left analog stick when pressed
391
- const dpad = vec2(
661
+ // get dpad buttons (standard mapping)
662
+ dpad.set(
392
663
  (gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
393
664
  (gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
394
- if (dpad.lengthSquared())
395
- sticks[0] = dpad.clampLength();
665
+ }
666
+ else if (gamepad.axes && gamepad.axes.length >= 2)
667
+ {
668
+ // digital style dpad from axes
669
+ const x = clamp(round(gamepad.axes[0]), -1, 1);
670
+ const y = clamp(round(gamepad.axes[1]), -1, 1);
671
+ dpad.set(x, -y);
396
672
  }
397
673
 
398
- // disable touch gamepad if using real gamepad
399
- touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
674
+ // copy dpad to left analog stick when pressed
675
+ if (gamepadDirectionEmulateStick && !dpad.isZero())
676
+ sticks[0] = dpad.clampLength();
400
677
  }
678
+
679
+ // disable touch gamepad if using real gamepad
680
+ touchGamepadEnable && isUsingGamepad && touchGamepadTimer.unset();
401
681
  }
402
682
  }
403
683
 
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()
684
+ function inputUpdatePost()
427
685
  {
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;
686
+ if (headlessMode) return;
687
+
688
+ // clear input to prepare for next frame
689
+ for (const deviceInputData of inputData)
690
+ for (const i in deviceInputData)
691
+ deviceInputData[i] &= 1;
692
+ mouseWheel = 0;
693
+ mouseDelta = vec2();
694
+ mouseDeltaScreen = vec2();
433
695
  }
434
696
 
435
- // enable touch input mouse passthrough
436
- function touchInputInit()
697
+ function inputRender()
437
698
  {
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)
699
+ touchGamepadRender();
700
+
701
+ function touchGamepadRender()
446
702
  {
447
- if (!touchInputEnable)
703
+ if (!touchInputEnable || !isTouchDevice || headlessMode) return;
704
+ if (!touchGamepadEnable || !touchGamepadTimer.isSet())
448
705
  return;
449
706
 
450
- // route touch to gamepad
451
- if (touchGamepadEnable)
452
- handleTouchGamepad(e);
707
+ // fade off when not touching or paused
708
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
709
+ if (!alpha || paused)
710
+ return;
453
711
 
454
- // fix stalled audio requiring user interaction
455
- if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
456
- audioContext.resume();
712
+ // setup the canvas
713
+ const context = overlayContext;
714
+ context.save();
715
+ context.globalAlpha = alpha*touchGamepadAlpha;
716
+ context.strokeStyle = '#fff';
717
+ context.lineWidth = 3;
457
718
 
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)
719
+ // draw left analog stick
720
+ const leftTouchStick = touchGamepadSticks[0] ?? vec2();
721
+ context.fillStyle = leftTouchStick.lengthSquared() > 0 ? '#fff' : '#000';
722
+ context.beginPath();
723
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
724
+ if (touchGamepadAnalog)
462
725
  {
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;
726
+ // draw circle shaped gamepad
727
+ context.arc(stickCenter.x, stickCenter.y, touchGamepadSize/2, 0, 9);
474
728
  }
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)
729
+ else
499
730
  {
500
- touchGamepadTimer.set();
501
- if (touchGamepadCenterButton && !wasTouching && paused)
731
+ // draw cross shaped gamepad
732
+ for (let i=10; --i;)
502
733
  {
503
- // touch anywhere to press start when paused
504
- touchGamepadButtons[9] = 1;
505
- return;
734
+ const angle = i*PI/4;
735
+ context.arc(stickCenter.x, stickCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
736
+ i%2 && context.arc(stickCenter.x, stickCenter.y, touchGamepadSize*.33, angle, angle);
506
737
  }
507
738
  }
739
+ context.fill();
740
+ context.stroke();
508
741
 
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)
742
+ // draw right face buttons
516
743
  {
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)
744
+ const buttonCenter = touchGamepadButtonCenter();
745
+ const buttonSize = touchGamepadButtonCount > 1 ?
746
+ touchGamepadSize/4 : touchGamepadSize/2;
747
+ for (let i=0; i<touchGamepadButtonCount; i++)
524
748
  {
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
- }
749
+ const j = mod(i-1, 4);
750
+ let button = touchGamepadButtonCount > 2 ?
751
+ j : min(j, touchGamepadButtonCount-1);
535
752
  // fix button locations (swap 2 and 3 to match gamepad layout)
536
753
  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;
754
+ const pos = touchGamepadButtonCount < 2 ? buttonCenter :
755
+ buttonCenter.add(vec2().setDirection(j, touchGamepadSize/2));
756
+ context.fillStyle = touchGamepadButtons[button] ? '#fff' : '#000';
757
+ context.beginPath();
758
+ context.arc(pos.x, pos.y, buttonSize, 0,9);
759
+ context.fill();
760
+ context.stroke();
545
761
  }
546
762
  }
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
763
 
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();
764
+ // set canvas back to normal
765
+ context.restore();
607
766
  }
608
-
609
- // set canvas back to normal
610
- context.restore();
611
767
  }
612
768
 
613
- ///////////////////////////////////////////////////////////////////////////////
614
- // Pointer Lock
615
-
616
- /** Request to lock the pointer, does not work on touch devices
617
- * @memberof Input */
618
- function pointerLockRequest()
769
+ // center position for right tocuh pad face buttons
770
+ function touchGamepadButtonCenter()
619
771
  {
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; }
772
+ const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
773
+ if (touchGamepadButtonCount === 2)
774
+ center.x += touchGamepadSize/2;
775
+ return center;
776
+ }