pixijs-input-devices 0.2.5 → 0.2.7

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,223 @@
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"
116
111
 
117
- // device events
118
- InputDevice.keyboard.on( "layoutdetected", ({ layout }) =>
119
- console.debug( "layout detected as " + layout );
120
- )
112
+ keyboard.getKeyLabel( "KeyZ" ) // Я
113
+ ```
121
114
 
122
- // bind keys/buttons
123
- InputDevice.keyboard.on( "Escape", () => showMenu() )
124
- InputDevice.gamepads[0].on( "Back", () => showMenu() )
115
+ > [!NOTE]
116
+ > **Layout support:** Detects the **"big four"** (AZERTY, JCUKEN, QWERTY and QWERTZ).
117
+ > Almost every keyboard is one of these four (or a regional derivative &ndash; e.g. Hangeul,
118
+ > Kana). There is no built-in detection for specialist or esoteric layouts (e.g. Dvorak, Colemak, BÉPO).
119
+ >
120
+ > The `keyboard.getKeyLabel( key )` uses the [KeyboardLayoutMap API](https://caniuse.com/mdn-api_keyboardlayoutmap)
121
+ > when available, before falling back to default AZERTY, JCUKEN, QWERTY or QWERTZ key values.
125
122
 
126
- // use "onGroup()" to add custom events too:
127
- InputDevice.onGroup( "pause_menu", ( event ) => {
128
- // menu was triggered!
129
- })
130
- ```
123
+ The keyboard layout is automatically detected from (in order):
131
124
 
132
- #### Global Events
125
+ 1. Browser API <sup>[(browser support)](https://caniuse.com/mdn-api_keyboardlayoutmap)</sup>
126
+ 2. Keypresses
127
+ 3. Browser Language
133
128
 
134
- | Event | Description | Payload |
135
- |---|---|---|
136
- | `"deviceconnected"` | `{device}` | A device has become available. |
137
- | `"devicedisconnected"` | `{device}` | A device has been removed. |
129
+ You can also manually force the layout:
138
130
 
139
- #### Keyboard Device Events
131
+ ```ts
132
+ // force layout
133
+ InputDevice.keyboard.layout = "JCUKEN"
140
134
 
141
- | Event | Description | Payload |
142
- |---|---|---|
143
- | `"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. |
145
- | **Key presses:** | | |
146
- | `"KeyA"` \| `"KeyB"` \| ... 103 more ... | `{event,keyCode,keyLabel,device}` | `"KeyA"` was pressed. |
135
+ InputDevice.keyboard.getKeyLabel( "KeyW" ) // "Ц"
136
+ InputDevice.keyboard.layoutSource // "manual"
137
+ ```
147
138
 
148
- #### Gamepad Device Events
139
+ #### KeyboardDevice Events
149
140
 
150
141
  | Event | Description | Payload |
151
142
  |---|---|---|
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"`. |
143
+ | `"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. |
145
+ | **Key presses:** | | |
146
+ | `"KeyA"` | `{event,keyCode,keyLabel,device}` | The `"KeyA"` was pressed. |
147
+ | `"KeyB"` | `{event,keyCode,keyLabel,device}` | The `"KeyB"` was pressed. |
148
+ | `"KeyC"` | `{event,keyCode,keyLabel,device}` | The `"KeyC"` was pressed. |
158
149
  | ... | ... | ... |
159
150
 
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
151
 
165
- ### Navigation API
152
+ ### GamepadDevice
166
153
 
167
- By default, any element with `"mousedown"` or `"pointerdown"` handlers is navigatable.
154
+ 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
155
 
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.
156
+ Gamepad accessors are modelled around the "Standard Controller Layout":
174
157
 
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`
158
+ ```ts
159
+ let gamepad = InputDevice.gamepads[0]
183
160
 
184
- Container events | description
185
- ------------------|--------------------------------------------------------
186
- `focus` | Target became focused.
187
- `blur` | Target lost focus.
161
+ if ( gamepad.button.Start ) { // ...
162
+ if ( gamepad.leftTrigger > 0.25 ) { // ...
163
+ if ( gamepad.leftJoystick.x > 0.5 ) { // ...
164
+ ```
188
165
 
189
166
  > [!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.
167
+ > **Special requirements?** You can always access `gamepad.source` and reference the
168
+ > underlying API directly as needed.
193
169
 
170
+ #### Vibration & Haptics
194
171
 
195
- ### Devices
172
+ Use the `playVibration()` method to play a haptic vibration, in supported browsers.
196
173
 
197
- #### Gamepads
174
+ ```ts
175
+ gamepad.playVibration()
198
176
 
199
- ##### Gamepad Layouts
177
+ gamepad.playVibration({
178
+ duration: 150,
179
+ weakMagnitude: 0.25,
180
+ strongMagnitude: 0.65,
181
+ })
182
+ ```
200
183
 
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 |
184
+ #### Gamepad Button Codes
185
+
186
+ The gamepad buttons reference **Standard Controller Layout**:
187
+
188
+ | Button # | ButtonCode | Standard | Nintendo* | Playstation | Xbox |
189
+ |:---:|:---:|:---:|:---:|:---:|:---:|
190
+ | `0` | `"A"` | **A** | A | Cross | A |
191
+ | `1` | `"B"` | **B** | X | Circle | B |
192
+ | `2` | `"X"` | **X** | B | Square | X |
193
+ | `3` | `"Y"` | **Y** | Y | Triangle | Y |
194
+ | `4` | `"LeftShoulder"` | **Left Shoulder** | L | L1 | LB |
195
+ | `5` | `"RightShoulder"` | **Right Shoulder** | R | R1 | RB |
196
+ | `6` | `"LeftTrigger"` | **Left Trigger** | L2 | ZL | LT |
197
+ | `7` | `"RightTrigger"` | **Right Trigger** | R2 | ZR | RT |
198
+ | `8` | `"Back"` | **Back** | Minus | Options | Back |
199
+ | `9` | `"Start"` | **Start** | Plus | Select | Start |
200
+ | `10` | `"LeftStick"` | **Left Stick (click)** | L3 | L3 | LSB |
201
+ | `11` | `"RightStick"` | **Right Stick (click)** | R3 | R3 | RSB |
202
+ | `12` | `"DPadUp"` | **D-Pad Up** | ⬆️ | ⬆️ | ⬆️ |
203
+ | `13` | `"DPadDown"` | **D-Pad Down** | ⬇️ | ⬇️ | ⬇️ |
204
+ | `14` | `"DPadLeft"` | **D-Pad Left** | ⬅️ | ⬅️ | ⬅️ |
205
+ | `15` | `"DPadRight"` | **D-Pad Right** | ➡️ | ➡️ | ➡️ |
206
+
207
+ *See [Nintendo Layout Remapping](#gamepad---nintendo-layout-remapping) for more context
208
+
209
+ #### Gamepad Layouts
210
+
211
+ ```ts
212
+ gamepad.layout // "nintendo" | "xbox" | "playstation" | "logitech" | "steam" | "generic"
213
+ ```
214
+
215
+ Layout detection is **highly non-standard** across major browsers, it should generally be used for aesthetic
216
+ improvements (e.g. showing [device-specific icons](https://thoseawesomeguys.com/prompts/)).
217
+
218
+ There is some limited layout remapping support built-in for Nintendo controllers, which appear to be the
219
+ only major brand controller that deviates from the standard.
220
+
221
+ ##### Gamepad - Nintendo Layout Remapping
222
222
 
223
223
  > [!CAUTION]
224
224
  > ***Nintendo:** Both the labels and physical positions of the A,B,X,Y buttons are different
@@ -226,9 +226,9 @@ Container events | description
226
226
  >
227
227
  > Set `GamepadDevice.defaultOptions.remapNintendoMode` to apply the remapping as required.
228
228
  >
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).
229
+ > - `"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).
230
+ > - `"accurate"` &ndash; The A,B,X,Y button codes will correspond to the exact Nintendo labels (Left=Y, Top=X, Bottom=B, Right=A).
231
+ > - `"none"` &ndash; The A,B,X,Y button codes mapping stay at the default indices (Left=Y, Top=B, Bottom=X, Right=A).
232
232
  >
233
233
  > ```
234
234
  > standard nintendo nintendo nintendo
@@ -244,9 +244,222 @@ Container events | description
244
244
  > 0 0 1 2
245
245
  > ```
246
246
 
247
- #### Device assignment
247
+ You can manually override this per-gamepad, or for all gamepads:
248
+
249
+ ```ts
250
+ gamepad.options.remapNintendoMode = "none"
251
+ GamepadDevice.defaultOptions.remapNintendoMode = "none"
252
+ ```
253
+
254
+ #### GamepadDevice Events
255
+
256
+ | Event | Description | Payload |
257
+ |---|---|---|
258
+ | `"group"` | `{groupName,button,buttonCode,device}` | A **named input group** button was pressed. |
259
+ | **Button presses:** | | |
260
+ | `"A"` | `{button,buttonCode,device}` | Standard layout button `"A"` was pressed. Equivalent to `0`. |
261
+ | `"B"` | `{button,buttonCode,device}` | Standard layout button `"B"` was pressed. Equivalent to `1`. |
262
+ | `"X"` | `{button,buttonCode,device}` | Standard layout button `"X"` was pressed. Equivalent to `2`. |
263
+ | ... | ... | ... |
264
+ | **Button presses (no label):** | | |
265
+ | `0` or `Button.A` | `{button,buttonCode,device}` | Button at offset `0` was pressed. |
266
+ | `1` or `Button.B` | `{button,buttonCode,device}` | Button at offset `1` was pressed. |
267
+ | `2` or `Button.X` | `{button,buttonCode,device}` | Button at offset `2` was pressed. |
268
+ | ... | ... | ... |
269
+
270
+ ### Custom Devices
248
271
 
249
- You can assign IDs and other meta data using the `device.meta` dictionary.
272
+ You can add custom devices to the device manager so it will be polled togehter and included in `InputDevice.devices`.
273
+
274
+ ```ts
275
+ import { type CustomDevice, InputDevice } from "pixijs-input-devices"
276
+
277
+ export const myDevice: CustomDevice = {
278
+ id: "on-screen-buttons",
279
+ type: "custom",
280
+ meta: {},
281
+
282
+ update: ( now: number ) => {
283
+ // polling update
284
+ }
285
+ }
286
+
287
+ InputDevice.add( myDevice )
288
+ ```
289
+
290
+ ## Named Input Groups
291
+
292
+ Use named "groups" to create named inputs that can be referenced.
293
+
294
+ This allows you to change the keys/buttons later (e.g. allow users to override inputs).
295
+
296
+ ```ts
297
+ // keyboard:
298
+ InputDevice.keyboard.options.namedGroups = {
299
+ jump: [ "ArrowUp", "Space", "KeyW" ],
300
+ crouch: [ "ArrowDown", "KeyS" ],
301
+ toggleGraphics: [ "KeyB" ],
302
+ }
303
+
304
+ // all gamepads:
305
+ GamepadDevice.defaultOptions.namedGroups = {
306
+ jump: [ "A" ],
307
+ crouch: [ "B", "X", "RightTrigger" ],
308
+ toggleGraphics: [ "RightStick" ],
309
+ }
310
+ ```
311
+
312
+ These can then be used with either the real-time and event-based APIs.
313
+
314
+ #### Event-based:
315
+
316
+ ```ts
317
+ // listen to all devices:
318
+ InputDevice.onGroup( "toggleGraphics", ( e ) => toggleGraphics() )
319
+
320
+ // listen to specific devices:
321
+ InputDevice.keyboard.onGroup( "jump", ( e ) => doJump() )
322
+ InputDevice.gamepads[0].onGroup( "jump", ( e ) => doJump() )
323
+ ```
324
+
325
+ #### Real-time:
326
+
327
+ ```ts
328
+ let jump = false, crouch = false, moveX = 0
329
+
330
+ const keyboard = InputDevice.keyboard
331
+ if ( keyboard.groupPressed( "jump" ) ) jump = true
332
+ if ( keyboard.groupPressed( "crouch" ) ) crouch = true
333
+ if ( keyboard.key.ArrowLeft ) moveX = -1
334
+ else if ( keyboard.key.ArrowRight ) moveX = 1
335
+
336
+ for ( const gamepad of InputDevice.gamepads ) {
337
+ if ( gamepad.groupPressed( "jump" ) ) jump = true
338
+ if ( gamepad.groupPressed( "crouch" ) ) crouch = true
339
+
340
+ // gamepads have additional analog inputs
341
+ // we're going to apply these only if touched
342
+ if ( gamepad.leftJoystick.x != 0 ) moveX = gamepad.leftJoystick.x
343
+ if ( gamepad.leftTrigger > 0 ) moveX *= ( 1 - gamepad.leftTrigger )
344
+ }
345
+ ```
346
+
347
+ ## Navigation API
348
+
349
+ Automatically traverse existing pointer/mouse based menus using the `Navigation` API.
350
+
351
+ ```ts
352
+ Navigation.stage = app.stage
353
+
354
+ const button = new ButtonSprite()
355
+ button.on( "mousedown", () => button.run( clickAnimation ) )
356
+ button.on( "mouseout", () => button.run( resetAnimation ) )
357
+ button.on( "mouseover", () => button.run( hoverAnimation ) )
358
+
359
+ app.stage.addChild( button )
360
+
361
+ button.isNavigatable // true
362
+ ```
363
+
364
+ > [!NOTE]
365
+ > **isNavigatable:** By default, any element with `"mousedown"` or `"pointerdown"` handlers is navigatable.
366
+
367
+ > [!WARNING]
368
+ > **Fallback Hover Effect:** If there is no `"pointerover"` or `"mouseover"` handler detected on a container, `Navigation`
369
+ > will apply abasic alpha effect to the selected item to indicate which container is currently the navigation target. This
370
+ > can be disabled by setting `Navigation.options.useFallbackHoverEffect` to `false`.
371
+
372
+ ### Disable Navigation
373
+
374
+ You can **disable** the navigation API - either permanently or temporarily - like so:
375
+
376
+ ```ts
377
+ Navigation.options.enabled = false
378
+ ```
379
+
380
+ ### Navigation Hierarchy
381
+
382
+ UIs can be complex! The Navigation API allows you to take over some - or all - of the navigation elements.
383
+
384
+ ```ts
385
+ class MyVerticalMenu implements NavigationResponder
386
+ {
387
+ becameFirstResponder() {
388
+ console.log( "I'm in charge now!" )
389
+ }
390
+
391
+ resignedAsFirstResponder() {
392
+ console.log( "Nooo! My power is gone!" )
393
+ }
394
+
395
+ handledNavigationIntent( intent, device ): boolean {
396
+ if ( intent === "navigateUp" ) this.moveCursorUp()
397
+ else if ( intent === "navigateDown" ) this.moveCursorDown()
398
+ else if ( intent === "navigateBack" ) this.loseFocus()
399
+ else if ( intent === "trigger" ) this.clickCursorItem()
400
+
401
+ // we are going to return false here, which will propagates unhandled
402
+ // intents ("navigateLeft", "navigateRight") up to the next responder
403
+ // in the stack - which could be a parent view, etc.
404
+ return false
405
+ }
406
+ }
407
+
408
+ const myMenu = new MyVerticalMenu()
409
+ Navigation.pushResponder( myMenu )
410
+ ```
411
+
412
+ In a game, you might use this to disable navigation outside of menus:
413
+
414
+ ```ts
415
+ class GameScene implements NavigationResponder
416
+ {
417
+ handledNavigationIntent( intent, device ) {
418
+ // ignore navigation intents, but allow other navigatable
419
+ // views to be pushed on top of me - e.g. a dialog window:
420
+ return true
421
+ }
422
+ }
423
+ ```
424
+
425
+ ### Default Navigation Binds
426
+
427
+ Keyboard and gamepad devices are configured with a few default binds for navigation.
428
+
429
+ The default binds are below:
430
+
431
+ Navigation Intent | Keyboard | Gamepad
432
+ ------------------|------------------------|-----------------------------------
433
+ `"navigateLeft"` | `ArrowLeft`, `KeyA` | Left Joystick (Left), `DPadLeft`
434
+ `"navigateRight"` | `ArrowRight`, `KeyD` | Left Joystick (Right), `DPadRight`
435
+ `"navigateUp"` | `ArrowUp`, `KeyW` | Left Joystick (Up), `DPadDown`
436
+ `"navigateDown"` | `ArrowDown`, `KeyS` | Left Joystick (Down), `DPadUp`
437
+ `"navigateBack"` | `Escape`, `Backspace` | `B`, `Back`
438
+ `"trigger"` | `Enter,` `Space` | `A`
439
+
440
+ These can be manually configured in `<device>.options.navigation.binds`.
441
+
442
+ #### Container Mixin
443
+
444
+ Container properties | type | default | description
445
+ ---------------------|------|---------|--------------
446
+ `isNavigatable` | `boolean` | `false` | returns `true` if `navigationMode` is set to `"target"`, or is `"auto"` and a `"pointerdown"` or `"mousedown"` event handler is registered.
447
+ `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.
448
+ `navigationPriority` | `number` | `0` | The priority relative to other navigation items in this group.
449
+
450
+ Container events | description
451
+ ------------------|--------------------------------------------------------
452
+ `focus` | Target became focused.
453
+ `blur` | Target lost focus.
454
+
455
+
456
+ ## Advanced usage
457
+
458
+ ### Local Player Assignment
459
+
460
+ Use the `<device>.meta` property to set assorted meta data on devices as needed.
461
+
462
+ You lose TypeScript's nice strong types, but its very handy for things like user assignment in multiplayer games.
250
463
 
251
464
  ```ts
252
465
  InputDevice.on("deviceconnected", ({ device }) =>
@@ -263,20 +476,79 @@ for ( const device of InputDevice.devices )
263
476
  }
264
477
  ```
265
478
 
266
- #### Custom devices
479
+ ### On-Screen Inputs
267
480
 
268
- You can add custom devices to the device manager:
481
+ You can easily map an on-screen input device using the `CustomDevice` interface.
269
482
 
270
483
  ```ts
271
- // 1. subclass CustomDevice
272
- class MySpecialDevice extends CustomDevice
273
- {
274
- constructor() {
275
- super( "special-device" )
484
+ export class OnScreenInputContainer extends Container implements CustomDevice {
485
+ id = "onscreen";
486
+ type = "custom" as const;
487
+ meta: Record<string, any> = {};
488
+
489
+ inputs = {
490
+ moveX: 0.0
491
+ jump: false,
492
+ }
493
+
494
+ update( now )
495
+ {
496
+ this.moveX = this._virtualJoystick.x
497
+ this.jump = this._jumpButton.isTouching()
276
498
  }
277
499
  }
278
500
 
279
- // 2. add the device
280
- const device = new MySpecialDevice();
281
- InputDevice.add( device )
501
+ const onscreen = new OnScreenInputContainer();
502
+
503
+ InputDevice.add( onscreen )
504
+ InputDevice.remove( onscreen )
505
+ ```
506
+
507
+ ### Two Users; One Keyboard
508
+
509
+ You could set up multiple named inputs:
510
+
511
+ ```ts
512
+ InputDevice.keyboard.options.namedGroups = {
513
+ jump: [ "ArrowUp", "KeyW" ],
514
+ defend: [ "ArrowDown", "KeyS" ],
515
+ left: [ "ArrowLeft", "KeyA" ],
516
+ right: [ "ArrowRight", "KeyD" ],
517
+
518
+ p1_jump: [ "KeyW" ],
519
+ p1_defend: [ "KeyS" ],
520
+ p1_left: [ "KeyA" ],
521
+ p1_right: [ "KeyD" ],
522
+
523
+ p2_jump: [ "ArrowUp" ],
524
+ p2_defend: [ "ArrowDown" ],
525
+ p2_left: [ "ArrowLeft" ],
526
+ p2_right: [ "ArrowRight" ],
527
+ }
528
+ ```
529
+
530
+ and then switch groups depending on the mode:
531
+
532
+ ```ts
533
+ if ( gameMode === "2p" )
534
+ {
535
+ // multiplayer
536
+ player1.jump = device.pressedGroup( "p1_jump" )
537
+ player1.defend = device.pressedGroup( "p1_defend" )
538
+ player1.moveX += device.pressedGroup( "p1_left" ) ? -1 : 0
539
+ player1.moveX += device.pressedGroup( "p1_right" ) ? 1 : 0
540
+ player2.jump = device.pressedGroup( "p2_jump" )
541
+ player2.defend = device.pressedGroup( "p2_defend" )
542
+ player2.moveX += device.pressedGroup( "p2_left" ) ? -1 : 0
543
+ player2.moveX += device.pressedGroup( "p2_right" ) ? 1 : 0
544
+ }
545
+ else
546
+ {
547
+ // single player
548
+ player1.jump = device.pressedGroup( "jump" )
549
+ player1.defend = device.pressedGroup( "defend" )
550
+ player1.moveX += device.pressedGroup( "left" ) ? -1 : 0
551
+ player1.moveX += device.pressedGroup( "right" ) ? 1 : 0
552
+ player2.updateComputerPlayer()
553
+ }
282
554
  ```