pixijs-input-devices 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,223 +2,218 @@
2
2
 
3
3
  🚧 WIP - This API is a work in progress, and is subject to change.
4
4
 
5
- - Adds support for ⌨️ **Keyboard**, 🎮 **Gamepads**, and other human-interface devices
6
- - A simple `Navigation` API which hooks devices into existing pointer/mouse events
7
- - A powerful event-based API for event-driven interactions
8
- - and of course, a high-performance API for real-time applications
5
+ - Adds comprehensive support for ⌨️ **Keyboard**, 🎮 **Gamepads**, and other human-interface devices
6
+ - High-performance, easy-to-use, sensible defaults
7
+ - Supports either real-time or event driven APIs
8
+ - Built-in `Navigation` API to navigate pointer/mouse based menus _(optional)_
9
9
 
10
10
  <hr/>
11
11
 
12
- ### 💿 Install
12
+ ## 💿 Install
13
13
 
14
14
  ```sh
15
15
  npm i pixijs-input-devices
16
16
  ```
17
17
 
18
- #### Setup
18
+ ### Setup
19
19
 
20
20
  ```ts
21
- import { InputDevice, Navigation } from "pixijs-input-devices"
21
+ import { InputDevice } from "pixijs-input-devices"
22
22
 
23
- // register mixin
24
- registerPixiJSInputDeviceMixin( Container )
25
-
26
- // add update loop
27
23
  Ticker.shared.add( () => InputDevice.update() )
24
+ ```
28
25
 
29
- // enable navigation
26
+ _(Optional)_ Enable the Navigation API:
27
+
28
+ ```ts
29
+ import { Navigation } from "pixijs-input-devices"
30
+
31
+ // set root node
30
32
  Navigation.stage = app.stage
33
+
34
+ // register mixin
35
+ registerPixiJSInputDeviceMixin( Container )
31
36
  ```
32
37
 
33
- ### ✨ Binding Groups
38
+ ## Overview
34
39
 
35
- Use named "groups" to create referenceable groups of inputs.
40
+ There are a few very simple themes:
36
41
 
37
- ```ts
38
- InputDevice.keyboard.options.namedGroups = {
39
- jump: [ "ArrowUp", "Space", "KeyW" ],
40
- crouch: [ "ArrowDown", "KeyS" ],
41
- slower: [ "ShiftLeft", "ShiftRight" ],
42
- left: [ "ArrowLeft", "KeyA" ],
43
- right: [ "ArrowRight", "KeyD" ],
42
+ - All devices are accessed through the `InputDevice` manager
43
+ - There are three supported device types: ⌨️ `"keyboard"`, 🎮 `"gamepad"` and 👻 `"custom"`
44
+ - Inputs can be accessed directly, or configured by [Named Groups](#named-input-groups)
44
45
 
45
- toggleGraphics: [ "KeyB" ],
46
- // other...
47
- };
46
+ ### InputDevice Manager
48
47
 
49
- GamepadDevice.defaultOptions.namedGroups = {
50
- jump: [ "A" ],
51
- crouch: [ "B", "X", "RightTrigger" ],
48
+ The `InputDevice` singleton controls all device discovery.
52
49
 
53
- toggleGraphics: [ "RightStick" ],
54
- // other...
55
- };
50
+ ```ts
51
+ InputDevice.keyboard // KeyboardDevice
52
+ InputDevice.gamepads // Array<GamepadDevice>
53
+ InputDevice.custom // Array<CustomDevice>
56
54
  ```
57
55
 
58
- These can then be used with the real-time and event-based APIs.
56
+ You can access all **active/connected** devices using `.devices`:
59
57
 
60
58
  ```ts
61
- // real-time:
62
- if ( gamepad.groupPressed("jump") ) doJump();
59
+ for ( const device of InputDevice.devices ) { // ...
60
+ ```
63
61
 
64
- // events:
65
- InputDevice.gamepads[0].onGroup( "jump", ( event ) => doJump() );
62
+ #### InputDevice - properties
66
63
 
67
- // ...or listen to ANY device:
68
- InputDevice.onGroup( "toggleGraphics", ( event ) => toggleGraphics() );
69
- ```
64
+ | Property | Type | Description |
65
+ |---|---|---|
66
+ | `InputDevice.isMobile` | `boolean` | Whether the context is mobile (including tablets). |
67
+ | `InputDevice.isTouchCapable` | `boolean` | Whether the context has touchscreen capability. |
68
+ | `InputDevice.lastInteractedDevice` | `Device?` | The most recently interacted device (or first if multiple). |
69
+ | `InputDevice.devices` | `Device[]` | All active, connected devices. |
70
+ | `InputDevice.keyboard` | `KeyboardDevice` | The global keyboard. |
71
+ | `InputDevice.gamepads` | `GamepadDevice[]` | Connected gamepads. |
72
+ | `InputDevice.custom` | `CustomDevice[]` | Custom devices. |
73
+
74
+ #### InputDevice - on() Events
70
75
 
71
- You definitely do not have to use named inputs:
76
+ Access global events directly through the manager:
72
77
 
73
78
  ```ts
74
- // real-time:
75
- if ( keyboard.key.Space || keyboard.key.KeyW ) jump = true;
76
- if ( gamepad.button.A || gamepad.button.LeftTrigger ) jump = true;
79
+ InputDevice.on( "deviceconnected", ({ device }) => {
80
+ // a device was connected
81
+ // do additional setup here, show a dialog, etc.
82
+ })
77
83
 
78
- // events:
79
- InputDevice.gamepads[0].on( "A", ( event ) => doJump() );
84
+ InputDevice.off( "deviceconnected" ) // stop listening
80
85
  ```
81
86
 
82
- ### Realtime API
87
+ | Event | Description | Payload |
88
+ |---|---|---|
89
+ | `"deviceconnected"` | `{device}` | A device has become available. |
90
+ | `"devicedisconnected"` | `{device}` | A device has been removed. |
83
91
 
84
- Iterate through `InputDevice.devices`, or access devices directly:
85
92
 
86
- ```ts
87
- let jump = false, crouch = false, moveX = 0
93
+ ### KeyboardDevice
88
94
 
89
- const keyboard = InputDevice.keyboard
90
- if ( keyboard.groupPressed( "jump" ) ) jump = true
91
- if ( keyboard.groupPressed( "crouch" ) ) crouch = true
92
- if ( keyboard.groupPressed( "left" ) ) moveX -= 1
93
- if ( keyboard.groupPressed( "right" ) ) moveX += 1
94
- if ( keyboard.groupPressed( "slower" ) ) moveX *= 0.5
95
+ Unlike gamepads & custom devices, there is a single global keyboard device.
95
96
 
96
- for ( const gamepad of InputDevice.gamepads ) {
97
- if ( gamepad.groupPressed( "jump" ) ) jump = true
98
- if ( gamepad.groupPressed( "crouch" ) ) crouch = true
97
+ ```ts
98
+ let keyboard = InputDevice.keyboard
99
99
 
100
- // gamepads have additional analog inputs
101
- // we're going to apply these only if touched
102
- if ( gamepad.leftJoystick.x != 0 ) moveX = gamepad.leftJoystick.x
103
- if ( gamepad.leftTrigger > 0 ) moveX *= ( 1 - gamepad.leftTrigger )
104
- }
100
+ if ( keyboard.key.ControlLeft ) { // ...
105
101
  ```
106
102
 
107
- ### Event API
103
+ > [!NOTE]
104
+ > **Detection:** On mobiles/tablets the keyboard will not appear in `InputDevice.devices` until
105
+ > a keyboard is detected. See `keyboard.detected`.
108
106
 
109
- Use `on( ... )` to subscribe to built-in events. Use `onGroup( ... )` to subscribe to custom named input group events.
107
+ #### Keyboard Layout - detection
110
108
 
111
109
  ```ts
112
- // global events
113
- InputDevice.on( "deviceconnected", ({ device }) =>
114
- console.debug( "A new " + device.type + " device connected!" )
115
- )
110
+ keyboard.layout // "AZERTY" | "JCUKEN" | "QWERTY" | "QWERTZ"
111
+ ```
116
112
 
117
- // device events
118
- InputDevice.keyboard.on( "layoutdetected", ({ layout }) =>
119
- console.debug( "layout detected as " + layout );
120
- )
113
+ > [!NOTE]
114
+ > **Layout support:** Detects the **"big four"** (AZERTY, JCUKEN, QWERTY and QWERTZ).
115
+ > Almost every keyboard is one of these four (or a regional derivative &ndash; e.g. Hangeul,
116
+ > Kana). There is no built-in detection for specialist or esoteric layouts (e.g. Dvorak, Colemak, BÉPO).
121
117
 
122
- // bind keys/buttons
123
- InputDevice.keyboard.on( "Escape", () => showMenu() )
124
- InputDevice.gamepads[0].on( "Back", () => showMenu() )
118
+ The keyboard layout is automatically detected from (in order):
125
119
 
126
- // use "onGroup()" to add custom events too:
127
- InputDevice.onGroup( "pause_menu", ( event ) => {
128
- // menu was triggered!
129
- })
130
- ```
120
+ 1. Browser API <sup>[(browser support)](https://caniuse.com/mdn-api_keyboardlayoutmap)</sup>
121
+ 2. Keypresses
122
+ 3. Browser Language
131
123
 
132
- #### Global Events
124
+ You can also manually force the layout:
133
125
 
134
- | Event | Description | Payload |
135
- |---|---|---|
136
- | `"deviceconnected"` | `{device}` | A device has become available. |
137
- | `"devicedisconnected"` | `{device}` | A device has been removed. |
126
+ ```ts
127
+ // force layout
128
+ InputDevice.keyboard.layout = "JCUKEN"
129
+
130
+ InputDevice.keyboard.keyLabel( "KeyW" ) // "Ц"
131
+ InputDevice.keyboard.layoutSource // "manual"
132
+ ```
138
133
 
139
- #### Keyboard Device Events
134
+ #### KeyboardDevice Events
140
135
 
141
136
  | Event | Description | Payload |
142
137
  |---|---|---|
143
138
  | `"layoutdetected"` | `{layout,layoutSource,device}` | The keyboard layout (`"QWERTY"`, `"QWERTZ"`, `"AZERTY"`, or `"JCUKEN"`) has been detected, either from the native API or from keypresses. |
144
- | `"group"` | `{groupName,event,keyCode,keyLabel,device}` | A named input group key was pressed. |
139
+ | `"group"` | `{groupName,event,keyCode,keyLabel,device}` | A **named input group** key was pressed. |
145
140
  | **Key presses:** | | |
146
- | `"KeyA"` \| `"KeyB"` \| ... 103 more ... | `{event,keyCode,keyLabel,device}` | `"KeyA"` was pressed. |
147
-
148
- #### Gamepad Device Events
149
-
150
- | Event | Description | Payload |
151
- |---|---|---|
152
- | `"group"` | `{groupName,button,buttonCode,device}` | A named input group button was pressed. |
153
- | **Button presses:** | | |
154
- | `"A"` \| `"B"` \| `"X"` \| ... 13 more ... | `{button,buttonCode,device}` | Button `"A"` was pressed. Equivalent to `0`. |
155
- | ... | ... | ... |
156
- | **Button presses (no label):** | | |
157
- | `0` \| `1` \| `2` \| ... 13 more ... | `{button,buttonCode,device}` | Button `0` was pressed. Equivalent to `"A"`. |
141
+ | `"KeyA"` | `{event,keyCode,keyLabel,device}` | The `"KeyA"` was pressed. |
142
+ | `"KeyB"` | `{event,keyCode,keyLabel,device}` | The `"KeyB"` was pressed. |
143
+ | `"KeyC"` | `{event,keyCode,keyLabel,device}` | The `"KeyC"` was pressed. |
158
144
  | ... | ... | ... |
159
145
 
160
- > [!TIP]
161
- > **Multiplayer:** For multiple players, consider assigning devices
162
- > using `device.meta` (e.g. `device.meta.player = 1`) and use
163
- > `InputDevice.devices` to iterate through devices.
164
146
 
165
- ### Navigation API
147
+ ### GamepadDevice
166
148
 
167
- By default, any element with `"mousedown"` or `"pointerdown"` handlers is navigatable.
149
+ Gamepads are automatically detected via the browser API when first interacted with <sup>[(read more)](https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API)</sup>.
168
150
 
169
- Container properties | type | default | description
170
- ---------------------|------|---------|--------------
171
- `isNavigatable` | `boolean` | `false` | returns `true` if `navigationMode` is set to `"target"`, or is `"auto"` and a `"pointerdown"` or `"mousedown"` event handler is registered.
172
- `navigationMode` | `"auto"` \| `"disabled"` \| `"target"` | `"auto"` | When set to `"auto"`, a `Container` can be navigated to if it has a `"pointerdown"` or `"mousedown"` event handler registered.
173
- `navigationPriority` | `number` | `0` | The priority relative to other navigation items in this group.
151
+ Gamepad accessors are modelled around the "Standard Controller Layout":
174
152
 
175
- Navigation intent | Keyboard | Gamepads
176
- ------------------|------------------------|-----------------------------------
177
- `"navigateLeft"` | `ArrowLeft`, `KeyA` | Left Joystick (Left), `DPadLeft`
178
- `"navigateRight"` | `ArrowRight`, `KeyD` | Left Joystick (Right), `DPadRight`
179
- `"navigateUp"` | `ArrowUp`, `KeyW` | Left Joystick (Up), `DPadDown`
180
- `"navigateDown"` | `ArrowDown`, `KeyS` | Left Joystick (Down), `DPadUp`
181
- `"navigateBack"` | `Escape`, `Backspace` | `B`, `Back`
182
- `"trigger"` | `Enter,` `Space` | `A`
153
+ ```ts
154
+ let gamepad = InputDevice.gamepads[0]
183
155
 
184
- Container events | description
185
- ------------------|--------------------------------------------------------
186
- `focus` | Target became focused.
187
- `blur` | Target lost focus.
156
+ if ( gamepad.button.Start ) { // ...
157
+ if ( gamepad.leftTrigger > 0.25 ) { // ...
158
+ if ( gamepad.leftJoystick.x > 0.5 ) { // ...
159
+ ```
188
160
 
189
161
  > [!TIP]
190
- > Modify `device.options.navigation.binds` to override which keys/buttons are used for navigation.
191
- >
192
- > Or set `device.options.navigation.enabled = false` to disable navigation.
162
+ > **Special requirements?** You can always access `gamepad.source` and reference the
163
+ > underlying API directly as needed.
193
164
 
165
+ #### Vibration & Haptics
194
166
 
195
- ### Devices
167
+ Use the `playVibration()` method to play a haptic vibration, in supported browsers.
196
168
 
197
- #### Gamepads
169
+ ```ts
170
+ gamepad.playVibration()
198
171
 
199
- ##### Gamepad Layouts
172
+ gamepad.playVibration({
173
+ duration: 150,
174
+ weakMagnitude: 0.25,
175
+ strongMagnitude: 0.65,
176
+ })
177
+ ```
200
178
 
201
- > [!NOTE]
202
- > **Gamepad Labels:** The gamepad buttons are aliased with generic standard controller buttons in a Logitech/Xbox/Steam controller layout.
203
-
204
- | Button | ButtonCode | Name | Generic | Nintendo<br/>(*physical) | Playstation | Xbox |
205
- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
206
- | `0` | `"A"` | **A** | A | B | Cross | A |
207
- | `1` | `"B"` | **B** | B | A | Circle | B |
208
- | `2` | `"X"` | **X** | X | Y | Square | X |
209
- | `3` | `"Y"` | **Y** | Y | X | Triangle | Y |
210
- | `4` | `"LeftShoulder"` | **Left Shoulder** | LeftShoulder | L | L1 | LB |
211
- | `5` | `"RightShoulder"` | **Right Shoulder** | RightShoulder | R | R1 | RB |
212
- | `6` | `"LeftTrigger"` | **Left Trigger** | LeftTrigger | L2 | ZL | LT |
213
- | `7` | `"RightTrigger"` | **Right Trigger** | RightTrigger | R2 | ZR | RT |
214
- | `8` | `"Back"` | **Back** | Back | Minus | Options | Back |
215
- | `9` | `"Start"` | **Start** | Start | Plus | Select | Start |
216
- | `10` | `"LeftStick"` | **Left Stick** | LeftStick | L3 | LeftStick | LSB |
217
- | `11` | `"RightStick"` | **Right Stick** | RightStick | R3 | RightStick | RSB |
218
- | `12` | `"DPadUp"` | **D-Pad Up** | DPadUp | DPadUp | DPadUp | DPadUp |
219
- | `13` | `"DPadDown"` | **D-Pad Down** | DPadDown | DPadDown | DPadDown | DPadDown |
220
- | `14` | `"DPadLeft"` | **D-Pad Left** | DPadLeft | DPadLeft | DPadLeft | DPadLeft |
221
- | `15` | `"DPadRight"` | **D-Pad Right** | DPadRight | DPadRight | DPadRight | DPadRight |
179
+ #### Gamepad Button Codes
180
+
181
+ The gamepad buttons reference **Standard Controller Layout**:
182
+
183
+ | Button # | ButtonCode | Standard | Nintendo* | Playstation | Xbox |
184
+ |:---:|:---:|:---:|:---:|:---:|:---:|
185
+ | `0` | `"A"` | **A** | A | Cross | A |
186
+ | `1` | `"B"` | **B** | X | Circle | B |
187
+ | `2` | `"X"` | **X** | B | Square | X |
188
+ | `3` | `"Y"` | **Y** | Y | Triangle | Y |
189
+ | `4` | `"LeftShoulder"` | **Left Shoulder** | L | L1 | LB |
190
+ | `5` | `"RightShoulder"` | **Right Shoulder** | R | R1 | RB |
191
+ | `6` | `"LeftTrigger"` | **Left Trigger** | L2 | ZL | LT |
192
+ | `7` | `"RightTrigger"` | **Right Trigger** | R2 | ZR | RT |
193
+ | `8` | `"Back"` | **Back** | Minus | Options | Back |
194
+ | `9` | `"Start"` | **Start** | Plus | Select | Start |
195
+ | `10` | `"LeftStick"` | **Left Stick (click)** | L3 | L3 | LSB |
196
+ | `11` | `"RightStick"` | **Right Stick (click)** | R3 | R3 | RSB |
197
+ | `12` | `"DPadUp"` | **D-Pad Up** | ⬆️ | ⬆️ | ⬆️ |
198
+ | `13` | `"DPadDown"` | **D-Pad Down** | ⬇️ | ⬇️ | ⬇️ |
199
+ | `14` | `"DPadLeft"` | **D-Pad Left** | ⬅️ | ⬅️ | ⬅️ |
200
+ | `15` | `"DPadRight"` | **D-Pad Right** | ➡️ | ➡️ | ➡️ |
201
+
202
+ *See [Nintendo Layout Remapping](#gamepad---nintendo-layout-remapping) for more context
203
+
204
+ #### Gamepad Layouts
205
+
206
+ ```ts
207
+ gamepad.layout // "nintendo" | "xbox" | "playstation" | "logitech" | "steam" | "generic"
208
+ ```
209
+
210
+ Layout detection is **highly non-standard** across major browsers, it should generally be used for aesthetic
211
+ improvements (e.g. showing [device-specific icons](https://thoseawesomeguys.com/prompts/)).
212
+
213
+ There is some limited layout remapping support built-in for Nintendo controllers, which appear to be the
214
+ only major brand controller that deviates from the standard.
215
+
216
+ ##### Gamepad - Nintendo Layout Remapping
222
217
 
223
218
  > [!CAUTION]
224
219
  > ***Nintendo:** Both the labels and physical positions of the A,B,X,Y buttons are different
@@ -226,9 +221,9 @@ Container events | description
226
221
  >
227
222
  > Set `GamepadDevice.defaultOptions.remapNintendoMode` to apply the remapping as required.
228
223
  >
229
- > - `"physical"` _(default)_ &ndash; A,B,X,Y refer the physical layout of a standard controller (Left=X,Top=Y,Bottom=A,Right=B).
230
- > - `"accurate"` &ndash; A,B,X,Y refer to the exact Nintendo labels (Left=Y,Top=X,Bottom=B,Right=A).
231
- > - `"none"` &ndash; A,B,X,Y refer to the button indices 0,1,2,3 (Left=Y,Top=B,Bottom=X,Right=A).
224
+ > - `"physical"` _**(default)**_ &ndash; The A,B,X,Y button codes will refer the physical layout of a standard controller (Left=X, Top=Y, Bottom=A, Right=B).
225
+ > - `"accurate"` &ndash; The A,B,X,Y button codes will correspond to the exact Nintendo labels (Left=Y, Top=X, Bottom=B, Right=A).
226
+ > - `"none"` &ndash; The A,B,X,Y button codes mapping stay at the default indices (Left=Y, Top=B, Bottom=X, Right=A).
232
227
  >
233
228
  > ```
234
229
  > standard nintendo nintendo nintendo
@@ -244,9 +239,222 @@ Container events | description
244
239
  > 0 0 1 2
245
240
  > ```
246
241
 
247
- #### Device assignment
242
+ You can manually override this per-gamepad, or for all gamepads:
243
+
244
+ ```ts
245
+ gamepad.options.remapNintendoMode = "none"
246
+ GamepadDevice.defaultOptions.remapNintendoMode = "none"
247
+ ```
248
+
249
+ #### GamepadDevice Events
250
+
251
+ | Event | Description | Payload |
252
+ |---|---|---|
253
+ | `"group"` | `{groupName,button,buttonCode,device}` | A **named input group** button was pressed. |
254
+ | **Button presses:** | | |
255
+ | `"A"` | `{button,buttonCode,device}` | Standard layout button `"A"` was pressed. Equivalent to `0`. |
256
+ | `"B"` | `{button,buttonCode,device}` | Standard layout button `"B"` was pressed. Equivalent to `1`. |
257
+ | `"X"` | `{button,buttonCode,device}` | Standard layout button `"X"` was pressed. Equivalent to `2`. |
258
+ | ... | ... | ... |
259
+ | **Button presses (no label):** | | |
260
+ | `0` or `Button.A` | `{button,buttonCode,device}` | Button at offset `0` was pressed. |
261
+ | `1` or `Button.B` | `{button,buttonCode,device}` | Button at offset `1` was pressed. |
262
+ | `2` or `Button.X` | `{button,buttonCode,device}` | Button at offset `2` was pressed. |
263
+ | ... | ... | ... |
248
264
 
249
- You can assign IDs and other meta data using the `device.meta` dictionary.
265
+ ### Custom Devices
266
+
267
+ You can add custom devices to the device manager so it will be polled togehter and included in `InputDevice.devices`.
268
+
269
+ ```ts
270
+ import { type CustomDevice, InputDevice } from "pixijs-input-devices"
271
+
272
+ export const myDevice: CustomDevice = {
273
+ id: "on-screen-buttons",
274
+ type: "custom",
275
+ meta: {},
276
+
277
+ update: ( now: number ) => {
278
+ // polling update
279
+ }
280
+ }
281
+
282
+ InputDevice.add( myDevice )
283
+ ```
284
+
285
+ ## Named Input Groups
286
+
287
+ Use named "groups" to create named inputs that can be referenced.
288
+
289
+ This allows you to change the keys/buttons later (e.g. allow users to override inputs).
290
+
291
+ ```ts
292
+ // keyboard:
293
+ InputDevice.keyboard.options.namedGroups = {
294
+ jump: [ "ArrowUp", "Space", "KeyW" ],
295
+ crouch: [ "ArrowDown", "KeyS" ],
296
+ toggleGraphics: [ "KeyB" ],
297
+ }
298
+
299
+ // all gamepads:
300
+ GamepadDevice.defaultOptions.namedGroups = {
301
+ jump: [ "A" ],
302
+ crouch: [ "B", "X", "RightTrigger" ],
303
+ toggleGraphics: [ "RightStick" ],
304
+ }
305
+ ```
306
+
307
+ These can then be used with either the real-time and event-based APIs.
308
+
309
+ #### Event-based:
310
+
311
+ ```ts
312
+ // listen to all devices:
313
+ InputDevice.onGroup( "toggleGraphics", ( e ) => toggleGraphics() )
314
+
315
+ // listen to specific devices:
316
+ InputDevice.keyboard.onGroup( "jump", ( e ) => doJump() )
317
+ InputDevice.gamepads[0].onGroup( "jump", ( e ) => doJump() )
318
+ ```
319
+
320
+ #### Real-time:
321
+
322
+ ```ts
323
+ let jump = false, crouch = false, moveX = 0
324
+
325
+ const keyboard = InputDevice.keyboard
326
+ if ( keyboard.groupPressed( "jump" ) ) jump = true
327
+ if ( keyboard.groupPressed( "crouch" ) ) crouch = true
328
+ if ( keyboard.key.ArrowLeft ) moveX = -1
329
+ else if ( keyboard.key.ArrowRight ) moveX = 1
330
+
331
+ for ( const gamepad of InputDevice.gamepads ) {
332
+ if ( gamepad.groupPressed( "jump" ) ) jump = true
333
+ if ( gamepad.groupPressed( "crouch" ) ) crouch = true
334
+
335
+ // gamepads have additional analog inputs
336
+ // we're going to apply these only if touched
337
+ if ( gamepad.leftJoystick.x != 0 ) moveX = gamepad.leftJoystick.x
338
+ if ( gamepad.leftTrigger > 0 ) moveX *= ( 1 - gamepad.leftTrigger )
339
+ }
340
+ ```
341
+
342
+ ## Navigation API
343
+
344
+ Automatically traverse existing pointer/mouse based menus using the `Navigation` API.
345
+
346
+ ```ts
347
+ Navigation.stage = app.stage
348
+
349
+ const button = new ButtonSprite()
350
+ button.on( "mousedown", () => button.run( clickAnimation ) )
351
+ button.on( "mouseout", () => button.run( resetAnimation ) )
352
+ button.on( "mouseover", () => button.run( hoverAnimation ) )
353
+
354
+ app.stage.addChild( button )
355
+
356
+ button.isNavigatable // true
357
+ ```
358
+
359
+ > [!NOTE]
360
+ > **isNavigatable:** By default, any element with `"mousedown"` or `"pointerdown"` handlers is navigatable.
361
+
362
+ > [!WARNING]
363
+ > **Fallback Hover Effect:** If there is no `"pointerover"` or `"mouseover"` handler detected on a container, `Navigation`
364
+ > will apply abasic alpha effect to the selected item to indicate which container is currently the navigation target. This
365
+ > can be disabled by setting `Navigation.options.useFallbackHoverEffect` to `false`.
366
+
367
+ ### Disable Navigation
368
+
369
+ You can **disable** the navigation API - either permanently or temporarily - like so:
370
+
371
+ ```ts
372
+ Navigation.options.enabled = false
373
+ ```
374
+
375
+ ### Navigation Hierarchy
376
+
377
+ UIs can be complex! The Navigation API allows you to take over some - or all - of the navigation elements.
378
+
379
+ ```ts
380
+ class MyVerticalMenu implements NavigationResponder
381
+ {
382
+ becameFirstResponder() {
383
+ console.log( "I'm in charge now!" )
384
+ }
385
+
386
+ resignedAsFirstResponder() {
387
+ console.log( "Nooo! My power is gone!" )
388
+ }
389
+
390
+ handledNavigationIntent( intent, device ): boolean {
391
+ if ( intent === "navigateUp" ) this.moveCursorUp()
392
+ else if ( intent === "navigateDown" ) this.moveCursorDown()
393
+ else if ( intent === "navigateBack" ) this.loseFocus()
394
+ else if ( intent === "trigger" ) this.clickCursorItem()
395
+
396
+ // we are going to return false here, which will propagates unhandled
397
+ // intents ("navigateLeft", "navigateRight") up to the next responder
398
+ // in the stack - which could be a parent view, etc.
399
+ return false
400
+ }
401
+ }
402
+
403
+ const myMenu = new MyVerticalMenu()
404
+ Navigation.pushResponder( myMenu )
405
+ ```
406
+
407
+ In a game, you might use this to disable navigation outside of menus:
408
+
409
+ ```ts
410
+ class GameScene implements NavigationResponder
411
+ {
412
+ handledNavigationIntent( intent, device ) {
413
+ // ignore navigation intents, but allow other navigatable
414
+ // views to be pushed on top of me - e.g. a dialog window:
415
+ return true
416
+ }
417
+ }
418
+ ```
419
+
420
+ ### Default Navigation Binds
421
+
422
+ Keyboard and gamepad devices are configured with a few default binds for navigation.
423
+
424
+ The default binds are below:
425
+
426
+ Navigation Intent | Keyboard | Gamepad
427
+ ------------------|------------------------|-----------------------------------
428
+ `"navigateLeft"` | `ArrowLeft`, `KeyA` | Left Joystick (Left), `DPadLeft`
429
+ `"navigateRight"` | `ArrowRight`, `KeyD` | Left Joystick (Right), `DPadRight`
430
+ `"navigateUp"` | `ArrowUp`, `KeyW` | Left Joystick (Up), `DPadDown`
431
+ `"navigateDown"` | `ArrowDown`, `KeyS` | Left Joystick (Down), `DPadUp`
432
+ `"navigateBack"` | `Escape`, `Backspace` | `B`, `Back`
433
+ `"trigger"` | `Enter,` `Space` | `A`
434
+
435
+ These can be manually configured in `<device>.options.navigation.binds`.
436
+
437
+ #### Container Mixin
438
+
439
+ Container properties | type | default | description
440
+ ---------------------|------|---------|--------------
441
+ `isNavigatable` | `boolean` | `false` | returns `true` if `navigationMode` is set to `"target"`, or is `"auto"` and a `"pointerdown"` or `"mousedown"` event handler is registered.
442
+ `navigationMode` | `"auto"` \| `"disabled"` \| `"target"` | `"auto"` | When set to `"auto"`, a `Container` can be navigated to if it has a `"pointerdown"` or `"mousedown"` event handler registered.
443
+ `navigationPriority` | `number` | `0` | The priority relative to other navigation items in this group.
444
+
445
+ Container events | description
446
+ ------------------|--------------------------------------------------------
447
+ `focus` | Target became focused.
448
+ `blur` | Target lost focus.
449
+
450
+
451
+ ## Advanced usage
452
+
453
+ ### Local Player Assignment
454
+
455
+ Use the `<device>.meta` property to set assorted meta data on devices as needed.
456
+
457
+ You lose TypeScript's nice strong types, but its very handy for things like user assignment in multiplayer games.
250
458
 
251
459
  ```ts
252
460
  InputDevice.on("deviceconnected", ({ device }) =>
@@ -263,20 +471,79 @@ for ( const device of InputDevice.devices )
263
471
  }
264
472
  ```
265
473
 
266
- #### Custom devices
474
+ ### On-Screen Inputs
267
475
 
268
- You can add custom devices to the device manager:
476
+ You can easily map an on-screen input device using the `CustomDevice` interface.
269
477
 
270
478
  ```ts
271
- // 1. subclass CustomDevice
272
- class MySpecialDevice extends CustomDevice
273
- {
274
- constructor() {
275
- super( "special-device" )
479
+ export class OnScreenInputContainer extends Container implements CustomDevice {
480
+ id = "onscreen";
481
+ type = "custom" as const;
482
+ meta: Record<string, any> = {};
483
+
484
+ inputs = {
485
+ moveX: 0.0
486
+ jump: false,
276
487
  }
488
+
489
+ update( now )
490
+ {
491
+ this.moveX = this._virtualJoystick.x
492
+ this.jump = this._jumpButton.isTouching()
493
+ }
494
+ }
495
+
496
+ const onscreen = new OnScreenInputContainer();
497
+
498
+ InputDevice.add( onscreen )
499
+ InputDevice.remove( onscreen )
500
+ ```
501
+
502
+ ### Two Users; One Keyboard
503
+
504
+ You could set up multiple named inputs:
505
+
506
+ ```ts
507
+ InputDevice.keyboard.options.namedGroups = {
508
+ jump: [ "ArrowUp", "KeyW" ],
509
+ defend: [ "ArrowDown", "KeyS" ],
510
+ left: [ "ArrowLeft", "KeyA" ],
511
+ right: [ "ArrowRight", "KeyD" ],
512
+
513
+ p1_jump: [ "KeyW" ],
514
+ p1_defend: [ "KeyS" ],
515
+ p1_left: [ "KeyA" ],
516
+ p1_right: [ "KeyD" ],
517
+
518
+ p2_jump: [ "ArrowUp" ],
519
+ p2_defend: [ "ArrowDown" ],
520
+ p2_left: [ "ArrowLeft" ],
521
+ p2_right: [ "ArrowRight" ],
277
522
  }
523
+ ```
524
+
525
+ and then switch groups depending on the mode:
278
526
 
279
- // 2. add the device
280
- const device = new MySpecialDevice();
281
- InputDevice.add( device )
527
+ ```ts
528
+ if ( gameMode === "2p" )
529
+ {
530
+ // multiplayer
531
+ player1.jump = device.pressedGroup( "p1_jump" )
532
+ player1.defend = device.pressedGroup( "p1_defend" )
533
+ player1.moveX += device.pressedGroup( "p1_left" ) ? -1 : 0
534
+ player1.moveX += device.pressedGroup( "p1_right" ) ? 1 : 0
535
+ player2.jump = device.pressedGroup( "p2_jump" )
536
+ player2.defend = device.pressedGroup( "p2_defend" )
537
+ player2.moveX += device.pressedGroup( "p2_left" ) ? -1 : 0
538
+ player2.moveX += device.pressedGroup( "p2_right" ) ? 1 : 0
539
+ }
540
+ else
541
+ {
542
+ // single player
543
+ player1.jump = device.pressedGroup( "jump" )
544
+ player1.defend = device.pressedGroup( "defend" )
545
+ player1.moveX += device.pressedGroup( "left" ) ? -1 : 0
546
+ player1.moveX += device.pressedGroup( "right" ) ? 1 : 0
547
+ player2.updateComputerPlayer()
548
+ }
282
549
  ```