-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathGamepadDisplay.ts
261 lines (233 loc) · 16.1 KB
/
GamepadDisplay.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import { type AxisChangeCallback, type ButtonChangeCallback, GamepadApiWrapper, type buttonChangeDetails } from "./GamepadApiWrapper.js";
import { gamepadButtonType, gamepadDirection, type standardGpadButtonMap, type standardGpadAxesMap } from "./enums.js";
export type GamepadDisplayButton = GamepadDisplayOnOffButton | GamepadDisplayVariableButton;
export type ButtonDisplayFunction = (buttonConfig: GamepadDisplayButton, value: number, touched: boolean, pressed: boolean, changes: buttonChangeDetails, btnIndex: number) => void;
export type JoystickDisplayFunction = (stickConfig: GamepadDisplayJoystick, xAxisValue: number, yAxisValue: number) => void;
export interface GamepadDisplayOnOffButton {
type: gamepadButtonType.onOff,
/** The element to add the touch and press classses, to represent touching or pressing on this button */
highlight: HTMLElement | SVGElement;
/** optional, for your own use */
extraData?: any;
}
export interface GamepadDisplayVariableButton {
type: gamepadButtonType.variable,
/** The element to add the touch and press classses to - to represent touching or pressing on this button,
* defaults to the same element as buttonElement */
highlight?: HTMLElement | SVGElement;
/** The element to move to represent pressing on this button */
buttonElement: HTMLElement | SVGElement;
/** How far the {@link GamepadDisplayVariableButton.buttonElement} should move to represent being pressed fully in HTML or SVG pixels */
movementRange: number;
/** Direction the {@link GamepadDisplayVariableButton.buttonElement} should move to represent being pressed */
direction: gamepadDirection;
/** Drag direction indicator / highlight element for this variable button */
directionHighlight?: HTMLElement | SVGElement;
/** optional, for your own use */
extraData?: any;
}
export interface GamepadDisplayJoystick {
joystickElement: HTMLElement | SVGElement;
movementRange: number;
/** Axis index (as returned by the browser gamepad api) to track for the horizontal movement of the display joystick
* see {@link standardGpadAxesMap} */
xAxisIndex?: number;
/** Axis index (as returned by the browser gamepad api) to track for the vertical movement of the display joystick
* see {@link standardGpadAxesMap} */
yAxisIndex?: number;
/** @deprecated
* use {@link GamepadDisplayJoystick.extraData} with a custom {@link DisplayGamepadConfig.joystickDisplayFunction} instead, see the custom gamepads example/demo */
highlights?: {
[gamepadDirection.up]?: HTMLElement | SVGElement | null;
[gamepadDirection.down]?: HTMLElement | SVGElement | null;
[gamepadDirection.left]?: HTMLElement | SVGElement | null;
[gamepadDirection.right]?: HTMLElement | SVGElement | null;
}
/** optional, for your own use */
extraData?: any;
}
export interface DisplayGamepadConfig {
/** The index of the gamepad this Gamepad Display should track as returned from `navigator.GetGamepads()` */
gamepadIndex: number;
/** Configuration for Buttons and Variable Pressure Buttons (eg: shoulder triggers) to represent in the gamepad display
* - The index of the button in the array corresponds to the index of the button as returned by the browser gamepad api.
* - Add null in the array for any button you don't want to track.
* - @see {@link GamepadDisplayButton} and {@link GamepadDisplayVariableButton} {@link standardGpadButtonMap} for more information */
buttons?: (GamepadDisplayButton | null | undefined)[];
/** Configuration for Joysticks to represent in the gamepad display (based on gamepad axes indecies as as returned by the browser gamepad api) */
sticks?: GamepadDisplayJoystick[];
/** The class to add to the corresponding highlight element when a gamepad button is touched (whether or not its pressed) */
touchedHighlightClass?: string;
/** The class to add to the corresponding highlight element when a gamepad button is pressed */
pressedHighlightClass?: string;
/** The class to add to the corresponding direction indicator element when a gamepad joystick is moved in a direction or a variable button is pressed */
moveDirectionHighlightClass?: string;
/** If provided, this function will be called for each button with a state change (pressed, touched, released, etc...)
* @see {@link GamepadDisplay.DefaultButtonDisplayFunction} for an example */
buttonDisplayFunction?: ButtonDisplayFunction;
/** If provided, this function will be called when the gamepad axies change for a joystick instead of the default display function.
* @see {@link GamepadDisplay.DefaultJoystickDisplayFunction} for an example */
joystickDisplayFunction?: JoystickDisplayFunction;
}
/**
* Class to handle displaying the state of a gamepad on the screen.
* This class will not draw anything to the screen. Instead it will update the classes / transforms of the elements
* you provide to represent the buttons and axes of the gamepad. See the examples for more information.
*/
export class GamepadDisplay {
protected config: DisplayGamepadConfig;
protected apiWrapper: GamepadApiWrapper;
/** Create a new GamepadDisplay instance
* @param config The config to use for the gamepad display
* @param apiWrapper (OPTIONAL) The gamepad api will use this GamepadApiWrapper instance to listen for gamepad events, otherwise it will create a new gamepad wrapper under the hood.
*/
constructor(config: DisplayGamepadConfig, apiWrapper?: GamepadApiWrapper) {
this.config = Object.assign({
// default config
gamepadIndex: 0,
buttons: [],
sticks: [],
touchedHighlightClass: "touched",
pressedHighlightClass: "pressed",
moveDirectionHighlightClass: "active",
}, config);
this.apiWrapper = apiWrapper || new GamepadApiWrapper({ buttonConfigs: [], updateDelay: 0 });
if (this.config.buttons?.length !== 0) this.apiWrapper.onGamepadButtonChange(this.displayButtonChanges);
if (this.config.sticks?.length !== 0) this.apiWrapper.onGamepadAxisChange(this.displayJoystickChanges);
};
/**
* Function called by default when the gamepad axies change for a joystick (as configured in this GamepadDisplay)
* If you specify your own {@link DisplayGamepadConfig.joystickDisplayFunction} in the config, this function won't get called.
* Instead, you can call this function with the same parameters as passed to the {@link DisplayGamepadConfig.joystickDisplayFunction}
* if you want to keep the default behaviour (and then you can add your own custom behaviour on top)
* @param stickConfig The config for the joystick that has changed (as configured in {@link DisplayGamepadConfig.sticks})
* @param xValue The new x axis value
* @param yValue The new y axis value
*/
public readonly DefaultJoystickDisplayFunction = (stickConfig: GamepadDisplayJoystick, xValue: number, yValue: number) => {
const stickRange = stickConfig.movementRange;
// stickConfig.joystickElement.style.transform = `rotateY(${-xValue * 30}deg) rotateX(${yValue * 30}deg) translate(${xValue * stickRange}px,${yValue * stickRange}px)`;
stickConfig.joystickElement.style.transform = `translate(${xValue * stickRange}px,${yValue * stickRange}px)`;
if (stickConfig.highlights && this.config.moveDirectionHighlightClass) {
const upHighlight = stickConfig.highlights[gamepadDirection.up];
const downHighlight = stickConfig.highlights[gamepadDirection.down];
const leftHighlight = stickConfig.highlights[gamepadDirection.left];
const rightHighlight = stickConfig.highlights[gamepadDirection.right];
if (upHighlight && yValue < -0.1) upHighlight.classList.add(this.config.moveDirectionHighlightClass || ""); else if (upHighlight) upHighlight.classList.remove(this.config.moveDirectionHighlightClass || "");
if (downHighlight && yValue > 0.1) downHighlight.classList.add(this.config.moveDirectionHighlightClass || ""); else if (downHighlight) downHighlight.classList.remove(this.config.moveDirectionHighlightClass || "");
if (leftHighlight && xValue < -0.1) leftHighlight.classList.add(this.config.moveDirectionHighlightClass || ""); else if (leftHighlight) leftHighlight.classList.remove(this.config.moveDirectionHighlightClass || "");
if (rightHighlight && xValue > 0.1) rightHighlight.classList.add(this.config.moveDirectionHighlightClass || ""); else if (rightHighlight) rightHighlight.classList.remove(this.config.moveDirectionHighlightClass || "");
}
}
/**
* Function called by default when any gamepad buttons change (called separately for each button (as configured in this GamepadDisplay))
* If you specify your own {@link DisplayGamepadConfig.buttonDisplayFunction} in the config, this function won't get called.
* Instead, you can call this function with the same parameters as passed to the {@link DisplayGamepadConfig.buttonDisplayFunction}
* if you want to keep the default behaviour (and then you can add your own custom behaviour on top)
* @param buttonConfig The config for the button that has changed as configured in {@link DisplayGamepadConfig.buttons}
* @param value The new value of the button
* @param touched Whether the button is currently being touched (unused, but included for consistency with the {@link ButtonDisplayFunction} signature)
* @param pressed Whether the button is currently being pressed (unused, but included for consistency with the {@link ButtonDisplayFunction} signature)
* @param changes The changes that have occurred since the last update
* @param btnIndex The index of the button that has changed (unused, but included for consistency with the {@link ButtonDisplayFunction} signature)
*/
public readonly DefaultButtonDisplayFunction = (buttonConfig: GamepadDisplayButton, value: number, touched: boolean, pressed: boolean, changes: buttonChangeDetails, btnIndex: number) => {
const btnHiglightElem = buttonConfig.highlight;
if (this.config.touchedHighlightClass && btnHiglightElem) {
if (changes.touchDown) {
btnHiglightElem.classList.add(this.config.touchedHighlightClass);
} else if (changes.touchUp) {
btnHiglightElem.classList.remove(this.config.touchedHighlightClass);
}
}
if (this.config.pressedHighlightClass && btnHiglightElem) {
if (changes.pressed) {
btnHiglightElem.classList.add(this.config.pressedHighlightClass);
} else if (changes.released) {
btnHiglightElem.classList.remove(this.config.pressedHighlightClass);
}
}
if (buttonConfig.type == gamepadButtonType.variable) {
const dirHighlightElem = buttonConfig.directionHighlight;
if (this.config.moveDirectionHighlightClass && dirHighlightElem) {
if (changes.pressed) {
dirHighlightElem.classList.add(this.config.moveDirectionHighlightClass);
} else if (changes.released) {
dirHighlightElem.classList.remove(this.config.moveDirectionHighlightClass);
}
}
if (buttonConfig.buttonElement) {
const isX = buttonConfig.direction == gamepadDirection.left || buttonConfig.direction == gamepadDirection.right;
const isPositive = buttonConfig.direction == gamepadDirection.right || buttonConfig.direction == gamepadDirection.down;
buttonConfig.buttonElement.style.transform = `translate${isX ? "X" : "Y"}(${isPositive ? "" : "-"}${value * buttonConfig.movementRange}px)`;
}
}
}
/**
* This function is registered as the callback for {@link GamepadApiWrapper.onGamepadAxisChange()}
* it calls the {@link DisplayGamepadConfig.joystickDisplayFunction} (if specified) or the {@link GamepadDisplay.DefaultJoystickDisplayFunction} otherwise
* for each configured joystick with axies that have changed
* @param gpadIndex The index of the gamepad that has changed
* @param gpadState The new state of the gamepad as reported by the browser
* @param axisChangesMask An array of booleans, where each true indicates that the corresponding axis has changed since the last update
*/
protected displayJoystickChanges: AxisChangeCallback = (gpadIndex, gpadState, axisChangesMask) => {
if (gpadIndex != this.config.gamepadIndex) return;
const joystickConfigs = this.config.sticks!; // we know this.config.sticks exists because we only register the callback in the constructor if it does
for (let i = 0; i < joystickConfigs.length; i++) {
const stickConfig = joystickConfigs[i];
if (!stickConfig) continue;
// only update if the joystick has changed
if ((stickConfig.xAxisIndex !== undefined && axisChangesMask[stickConfig.xAxisIndex]) || (stickConfig.yAxisIndex !== undefined && axisChangesMask[stickConfig.yAxisIndex])) {
// extract useful values from the config
const gpadAxisValues = gpadState.axes;
const xValue = stickConfig.xAxisIndex !== undefined ? (gpadAxisValues[stickConfig.xAxisIndex] || 0) : 0;
const yValue = stickConfig.yAxisIndex !== undefined ? (gpadAxisValues[stickConfig.yAxisIndex] || 0) : 0;
// display the new joystick state
if (this.config.joystickDisplayFunction) {
this.config.joystickDisplayFunction(stickConfig, xValue, yValue);
} else {
this.DefaultJoystickDisplayFunction(stickConfig, xValue, yValue);
}
}
}
}
/**
* This function is registered as the callback for {@link GamepadApiWrapper.onGamepadButtonChange()}
* it calls the {@link DisplayGamepadConfig.buttonDisplayFunction} (if specified) or the {@link GamepadDisplay.DefaultButtonDisplayFunction} otherwise
* for every button that has changed since the last update
* @param gpadIndex The index of the gamepad that has changed
* @param gpadState The new state of the gamepad as reported by the browser / {@link GamepadApiWrapper.onGamepadButtonChange}
* @param buttonChangesMask An array of buttonChangeDetails or false, where each false in the array indicates that the corresponding button index has not changed since the last update.
* @returns
*/
protected displayButtonChanges: ButtonChangeCallback = (gpadIndex, gpadState, buttonChangesMask) => {
if (gpadIndex != this.config.gamepadIndex) return;
const buttonConfigs = this.config.buttons!; // we know this.config.buttons exists because we only registered the callback in the constructor if it does
for (let i = 0; i < buttonConfigs.length; i++) {
const buttonConfig = buttonConfigs[i];
const changes = buttonChangesMask[i];
// only update if the button config and changes exist
if (!buttonConfig || Object.keys(buttonConfig).length == 0 || !changes || Object.keys(changes).length == 0) continue;
// extract useful values from the changes
const value = gpadState.buttons[i].value;
const touched = gpadState.buttons[i].touched;
const pressed = gpadState.buttons[i].pressed;
// display the new button state
if (this.config.buttonDisplayFunction) {
this.config.buttonDisplayFunction(buttonConfig, value, touched, pressed, changes, i);
} else {
this.DefaultButtonDisplayFunction(buttonConfig, value, touched, pressed, changes, i);
}
}
}
/**
* Cleanup function to remove all event listeners created by the {@link GamepadDisplay}
* Call this function before removing the gamepad display from the DOM or deleting
* the {@link GamepadDisplay} instance to prevent memory leaks
*/
Cleanup() {
this.apiWrapper.offGamepadButtonChange(this.displayButtonChanges)
this.apiWrapper.offGamepadAxisChange(this.displayJoystickChanges)
}
}