react-native-earl-gamepad 0.5.2 → 0.7.0
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 +31 -9
- package/dist/GamepadBridge.d.ts +15 -2
- package/dist/GamepadBridge.js +100 -4
- package/dist/GamepadDebug.js +284 -14
- package/dist/types.d.ts +30 -9
- package/dist/useGamepad.d.ts +6 -2
- package/dist/useGamepad.js +89 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
# react-native-earl-gamepad
|
|
2
|
+
|
|
3
|
+

|
|
2
4
|

|
|
3
5
|

|
|
4
6
|

|
|
5
7
|
|
|
6
|
-
WebView-based gamepad bridge for React Native. Polls `navigator.getGamepads()` in a hidden WebView and surfaces buttons, sticks, d-pad, and connection events to JS.
|
|
8
|
+
WebView-based gamepad bridge for React Native. Polls `navigator.getGamepads()` in a hidden WebView and surfaces buttons, sticks, d-pad, touchpad click, and connection events to JS.
|
|
9
|
+
|
|
7
10
|
- Components: `GamepadBridge`, `useGamepad`, and `GamepadDebug`.
|
|
8
|
-
- Deadzone handling (default `0.15`) with auto-clear on disconnect.
|
|
9
|
-
- Typed events for buttons, axes, d-pad, and
|
|
11
|
+
- Deadzone handling (default `0.15`) with auto-clear on disconnect and live state snapshots to avoid stuck buttons.
|
|
12
|
+
- Typed events for buttons, axes, d-pad, touchpad click, status, and a full-state snapshot.
|
|
13
|
+
|
|
14
|
+
### Why this?
|
|
15
|
+
|
|
16
|
+
Native gamepad support in React Native can be flaky or hard to maintain. Instead of relying on old native modules, it uses a hidden WebView to bridge the HTML5 Gamepad API (navigator.getGamepads()) directly to React Native. This ensures much better compatibility across iOS and Android since it relies on the web standard.
|
|
17
|
+
|
|
18
|
+
### Controller Compatibility
|
|
19
|
+
|
|
20
|
+
- Tested with: PS4, and generic Bluetooth controllers. Supports standard mapping.
|
|
10
21
|
|
|
11
22
|
## Requirements
|
|
12
23
|
|
|
@@ -121,7 +132,12 @@ export function HUD() {
|
|
|
121
132
|
|
|
122
133
|
### Visual debugger
|
|
123
134
|
|
|
124
|
-
Drop-in component to see a controller diagram that lights up buttons, shows stick offsets, and lists state.
|
|
135
|
+
Drop-in component to see a controller diagram that lights up buttons, shows stick offsets, and lists state. Shows live metadata (name/vendor/product, mapping, axes/buttons count, vibration support) and includes vibration test buttons plus a loader prompt when no pad is connected.
|
|
136
|
+
|
|
137
|
+
The State panel includes:
|
|
138
|
+
|
|
139
|
+
- Per-stick plots (left/right) with axis values, crosshairs, and a dashed trace from center to the current dot.
|
|
140
|
+
- Touchpad click indicator (PS touchpad click is mapped to `touchpad`; position is not exposed by the Gamepad API).
|
|
125
141
|
|
|
126
142
|
```tsx
|
|
127
143
|
import { GamepadDebug } from "react-native-earl-gamepad";
|
|
@@ -131,9 +147,9 @@ export function DebugScreen() {
|
|
|
131
147
|
}
|
|
132
148
|
```
|
|
133
149
|
|
|
134
|
-

|
|
151
|
+

|
|
152
|
+

|
|
137
153
|
|
|
138
154
|
## API
|
|
139
155
|
|
|
@@ -145,6 +161,7 @@ export function DebugScreen() {
|
|
|
145
161
|
- `onAxis?: (event: AxisEvent) => void` — fired when an axis changes beyond threshold.
|
|
146
162
|
- `onDpad?: (event: DpadEvent) => void` — convenience mapping of button indices 12–15.
|
|
147
163
|
- `onStatus?: (event: StatusEvent) => void` — `connected` / `disconnected` events.
|
|
164
|
+
- `onState?: (event: StateEvent) => void` — full snapshot of pressed buttons, values, and axes each poll.
|
|
148
165
|
- `style?: StyleProp<ViewStyle>` — override container; default is a 1×1 transparent view.
|
|
149
166
|
|
|
150
167
|
### `useGamepad` options and return
|
|
@@ -162,6 +179,9 @@ Return shape:
|
|
|
162
179
|
- `buttonValues: Partial<Record<GamepadButtonName, number>>` — last analog value per button (useful for LT/RT triggers).
|
|
163
180
|
- `isPressed(key: GamepadButtonName): boolean` — helper to check a single button.
|
|
164
181
|
- `bridge: JSX.Element | null` — render once to enable polling.
|
|
182
|
+
- `info: GamepadInfo` — metadata for the first controller (id, vendor/product if exposed, mapping, counts, vibration support, timestamp, index).
|
|
183
|
+
- `vibrate(duration?: number, strength?: number): void` — fire a short rumble when `vibrationActuator` is available.
|
|
184
|
+
- `stopVibration(): void` — stop an in-flight vibration when supported.
|
|
165
185
|
|
|
166
186
|
### `GamepadDebug`
|
|
167
187
|
|
|
@@ -174,6 +194,8 @@ Return shape:
|
|
|
174
194
|
- `AxisEvent`: `{ type: 'axis'; axis: StickAxisName; index: number; value: number }`
|
|
175
195
|
- `DpadEvent`: `{ type: 'dpad'; key: 'up' | 'down' | 'left' | 'right'; pressed: boolean }`
|
|
176
196
|
- `StatusEvent`: `{ type: 'status'; state: 'connected' | 'disconnected' }`
|
|
197
|
+
- `InfoEvent`: controller metadata payload (name/vendor/product, mapping, counts, vibration capability, timestamp, index, etc.)
|
|
198
|
+
- `StateEvent`: `{ type: 'state'; pressed: GamepadButtonName[]; values: Record<GamepadButtonName, number>; axes: Record<StickAxisName, number> }`
|
|
177
199
|
|
|
178
200
|
Button names map to the standard gamepad layout (`a`, `b`, `x`, `y`, `lb`, `rb`, `lt`, `rt`, `back`, `start`, `ls`, `rs`, `dpadUp`, `dpadDown`, `dpadLeft`, `dpadRight`, `home`). Unknown indices fall back to `button-N`. Axes map to `leftX`, `leftY`, `rightX`, `rightY` with fallbacks `axis-N`.
|
|
179
201
|
|
|
@@ -199,13 +221,13 @@ npm install
|
|
|
199
221
|
npm run build
|
|
200
222
|
```
|
|
201
223
|
|
|
224
|
+
Build outputs to `dist/` with type declarations.
|
|
225
|
+
|
|
202
226
|
## Troubleshooting
|
|
203
227
|
|
|
204
228
|
- **[Invariant Violation: Tried to register two views with the same name RNCWebView]**: Check your `package.json` for multiple instances of `react-native-webview` and uninstall any duplicates.
|
|
205
229
|
When you install `react-native-earl-gamepad`, `react-native-webview` is already included, so you should not install it separately. or you can check it by running `npm ls react-native-webview`.
|
|
206
230
|
|
|
207
|
-
Build outputs to `dist/` with type declarations.
|
|
208
|
-
|
|
209
231
|
## License
|
|
210
232
|
|
|
211
233
|
MIT
|
package/dist/GamepadBridge.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { StyleProp, ViewStyle } from "react-native";
|
|
2
|
-
import type { AxisEvent, ButtonEvent, DpadEvent, StatusEvent } from "./types";
|
|
2
|
+
import type { AxisEvent, ButtonEvent, DpadEvent, InfoEvent, StateEvent, StatusEvent } from "./types";
|
|
3
|
+
type VibrationRequest = {
|
|
4
|
+
type: "once";
|
|
5
|
+
duration: number;
|
|
6
|
+
strong: number;
|
|
7
|
+
weak: number;
|
|
8
|
+
nonce: number;
|
|
9
|
+
} | {
|
|
10
|
+
type: "stop";
|
|
11
|
+
nonce: number;
|
|
12
|
+
};
|
|
3
13
|
type Props = {
|
|
4
14
|
enabled?: boolean;
|
|
5
15
|
axisThreshold?: number;
|
|
@@ -7,7 +17,10 @@ type Props = {
|
|
|
7
17
|
onButton?: (event: ButtonEvent) => void;
|
|
8
18
|
onAxis?: (event: AxisEvent) => void;
|
|
9
19
|
onStatus?: (event: StatusEvent) => void;
|
|
20
|
+
onInfo?: (event: InfoEvent) => void;
|
|
21
|
+
onState?: (event: StateEvent) => void;
|
|
22
|
+
vibrationRequest?: VibrationRequest;
|
|
10
23
|
style?: StyleProp<ViewStyle>;
|
|
11
24
|
};
|
|
12
|
-
export default function GamepadBridge({ enabled, axisThreshold, onDpad, onButton, onAxis, onStatus, style, }: Props): import("react/jsx-runtime").JSX.Element | null;
|
|
25
|
+
export default function GamepadBridge({ enabled, axisThreshold, onDpad, onButton, onAxis, onStatus, onInfo, onState, vibrationRequest, style, }: Props): import("react/jsx-runtime").JSX.Element | null;
|
|
13
26
|
export {};
|
package/dist/GamepadBridge.js
CHANGED
|
@@ -16,18 +16,87 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
16
16
|
(function(){
|
|
17
17
|
const deadzone = ${axisThreshold.toFixed(2)};
|
|
18
18
|
const send = (data) => window.ReactNativeWebView.postMessage(JSON.stringify(data));
|
|
19
|
-
const buttonNames = ['a','b','x','y','lb','rb','lt','rt','back','start','ls','rs','dpadUp','dpadDown','dpadLeft','dpadRight','home'];
|
|
19
|
+
const buttonNames = ['a','b','x','y','lb','rb','lt','rt','back','start','ls','rs','dpadUp','dpadDown','dpadLeft','dpadRight','home','touchpad'];
|
|
20
20
|
const axisNames = ['leftX','leftY','rightX','rightY'];
|
|
21
21
|
let prevButtons = [];
|
|
22
22
|
let prevAxes = [];
|
|
23
|
+
let prevInfoJson = '';
|
|
24
|
+
|
|
25
|
+
function parseVendorProduct(id){
|
|
26
|
+
const vendorMatch = /Vendor:\s?([0-9a-fA-F]+)/i.exec(id || '') || /VID_([0-9a-fA-F]+)/i.exec(id || '');
|
|
27
|
+
const productMatch = /Product:\s?([0-9a-fA-F]+)/i.exec(id || '') || /PID_([0-9a-fA-F]+)/i.exec(id || '');
|
|
28
|
+
return {
|
|
29
|
+
vendor: vendorMatch ? vendorMatch[1] : null,
|
|
30
|
+
product: productMatch ? productMatch[1] : null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sendInfo(gp){
|
|
35
|
+
const connected = !!gp;
|
|
36
|
+
const { vendor, product } = parseVendorProduct(gp?.id || '');
|
|
37
|
+
const info = {
|
|
38
|
+
type: 'info',
|
|
39
|
+
connected,
|
|
40
|
+
index: gp?.index ?? null,
|
|
41
|
+
id: gp?.id ?? null,
|
|
42
|
+
mapping: gp?.mapping ?? null,
|
|
43
|
+
timestamp: gp?.timestamp ?? null,
|
|
44
|
+
canVibrate: !!gp?.vibrationActuator,
|
|
45
|
+
vendor,
|
|
46
|
+
product,
|
|
47
|
+
axes: gp?.axes?.length ?? 0,
|
|
48
|
+
buttons: gp?.buttons?.length ?? 0,
|
|
49
|
+
};
|
|
50
|
+
const nextJson = JSON.stringify(info);
|
|
51
|
+
if (nextJson !== prevInfoJson) {
|
|
52
|
+
prevInfoJson = nextJson;
|
|
53
|
+
send(info);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function vibrateOnce(duration, strong, weak){
|
|
58
|
+
const pad = (navigator.getGamepads && navigator.getGamepads()[0]) || null;
|
|
59
|
+
if (!pad || !pad.vibrationActuator || !pad.vibrationActuator.playEffect) return;
|
|
60
|
+
try {
|
|
61
|
+
pad.vibrationActuator.playEffect('dual-rumble', {
|
|
62
|
+
duration: duration || 500,
|
|
63
|
+
strongMagnitude: strong ?? 1,
|
|
64
|
+
weakMagnitude: weak ?? 1,
|
|
65
|
+
}).catch(() => {});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stopVibration(){
|
|
72
|
+
const pad = (navigator.getGamepads && navigator.getGamepads()[0]) || null;
|
|
73
|
+
if (!pad || !pad.vibrationActuator) return;
|
|
74
|
+
if (typeof pad.vibrationActuator.reset === 'function') {
|
|
75
|
+
try { pad.vibrationActuator.reset(); } catch (err) {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
window.__earlVibrateOnce = vibrateOnce;
|
|
80
|
+
window.__earlStopVibration = stopVibration;
|
|
81
|
+
|
|
82
|
+
const nameForIndex = (index) => {
|
|
83
|
+
if (buttonNames[index]) return buttonNames[index];
|
|
84
|
+
if (index === 16) return 'home';
|
|
85
|
+
if (index === 17) return 'touchpad';
|
|
86
|
+
return 'button-' + index;
|
|
87
|
+
};
|
|
23
88
|
|
|
24
89
|
function poll(){
|
|
25
90
|
const pads = (navigator.getGamepads && navigator.getGamepads()) || [];
|
|
26
91
|
const gp = pads[0];
|
|
92
|
+
sendInfo(gp);
|
|
27
93
|
if (gp) {
|
|
94
|
+
const pressedNow = [];
|
|
95
|
+
const valueMap = {};
|
|
96
|
+
const axesState = {};
|
|
28
97
|
// Buttons
|
|
29
98
|
gp.buttons?.forEach((btn, index) => {
|
|
30
|
-
const name =
|
|
99
|
+
const name = nameForIndex(index);
|
|
31
100
|
const prevBtn = prevButtons[index] || { pressed:false, value:0 };
|
|
32
101
|
const changed = prevBtn.pressed !== btn.pressed || Math.abs(prevBtn.value - btn.value) > 0.01;
|
|
33
102
|
if (changed) {
|
|
@@ -38,6 +107,8 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
38
107
|
if (index === 14) send({ type:'dpad', key:'left', pressed: !!btn.pressed });
|
|
39
108
|
if (index === 15) send({ type:'dpad', key:'right', pressed: !!btn.pressed });
|
|
40
109
|
}
|
|
110
|
+
if (btn?.pressed) pressedNow.push(name);
|
|
111
|
+
valueMap[name] = btn?.value ?? 0;
|
|
41
112
|
prevButtons[index] = { pressed: !!btn.pressed, value: btn.value ?? 0 };
|
|
42
113
|
});
|
|
43
114
|
|
|
@@ -49,13 +120,18 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
49
120
|
if (Math.abs(prevVal - value) > 0.01) {
|
|
50
121
|
send({ type:'axis', axis:name, index, value });
|
|
51
122
|
}
|
|
123
|
+
axesState[name] = value;
|
|
52
124
|
prevAxes[index] = value;
|
|
53
125
|
});
|
|
126
|
+
|
|
127
|
+
send({ type:'state', pressed: pressedNow, values: valueMap, axes: axesState });
|
|
54
128
|
} else {
|
|
129
|
+
// If no pad, send a single disconnected info payload
|
|
130
|
+
sendInfo(null);
|
|
55
131
|
if (prevButtons.length) {
|
|
56
132
|
prevButtons.forEach((btn, index) => {
|
|
57
133
|
if (btn?.pressed) {
|
|
58
|
-
const name =
|
|
134
|
+
const name = nameForIndex(index);
|
|
59
135
|
send({ type:'button', button:name, index, pressed:false, value:0 });
|
|
60
136
|
if (index === 12) send({ type:'dpad', key:'up', pressed:false });
|
|
61
137
|
if (index === 13) send({ type:'dpad', key:'down', pressed:false });
|
|
@@ -74,6 +150,7 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
74
150
|
});
|
|
75
151
|
prevAxes = [];
|
|
76
152
|
}
|
|
153
|
+
send({ type:'state', pressed: [], values: {}, axes: {} });
|
|
77
154
|
}
|
|
78
155
|
requestAnimationFrame(poll);
|
|
79
156
|
}
|
|
@@ -99,7 +176,7 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
99
176
|
})();
|
|
100
177
|
</script>
|
|
101
178
|
</body></html>`;
|
|
102
|
-
function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton, onAxis, onStatus, style, }) {
|
|
179
|
+
function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton, onAxis, onStatus, onInfo, onState, vibrationRequest, style, }) {
|
|
103
180
|
const webviewRef = (0, react_1.useRef)(null);
|
|
104
181
|
const html = (0, react_1.useMemo)(() => buildBridgeHtml(axisThreshold), [axisThreshold]);
|
|
105
182
|
const focusBridge = (0, react_1.useCallback)(() => {
|
|
@@ -113,6 +190,19 @@ function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton,
|
|
|
113
190
|
if (enabled)
|
|
114
191
|
focusBridge();
|
|
115
192
|
}, [enabled, focusBridge]);
|
|
193
|
+
(0, react_1.useEffect)(() => {
|
|
194
|
+
if (!enabled || !vibrationRequest)
|
|
195
|
+
return;
|
|
196
|
+
const node = webviewRef.current;
|
|
197
|
+
if (!node)
|
|
198
|
+
return;
|
|
199
|
+
if (vibrationRequest.type === "once") {
|
|
200
|
+
node.injectJavaScript(`window.__earlVibrateOnce(${Math.max(vibrationRequest.duration, 0)}, ${Math.max(Math.min(vibrationRequest.strong, 1), 0)}, ${Math.max(Math.min(vibrationRequest.weak, 1), 0)}); true;`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
node.injectJavaScript(`window.__earlStopVibration(); true;`);
|
|
204
|
+
}
|
|
205
|
+
}, [enabled, vibrationRequest]);
|
|
116
206
|
if (!enabled)
|
|
117
207
|
return null;
|
|
118
208
|
const handleMessage = (event) => {
|
|
@@ -130,6 +220,12 @@ function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton,
|
|
|
130
220
|
else if (data.type === "status") {
|
|
131
221
|
onStatus === null || onStatus === void 0 ? void 0 : onStatus(data);
|
|
132
222
|
}
|
|
223
|
+
else if (data.type === "info") {
|
|
224
|
+
onInfo === null || onInfo === void 0 ? void 0 : onInfo(data);
|
|
225
|
+
}
|
|
226
|
+
else if (data.type === "state") {
|
|
227
|
+
onState === null || onState === void 0 ? void 0 : onState(data);
|
|
228
|
+
}
|
|
133
229
|
}
|
|
134
230
|
catch {
|
|
135
231
|
// ignore malformed messages
|
package/dist/GamepadDebug.js
CHANGED
|
@@ -15,6 +15,11 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
15
15
|
const v = value(name);
|
|
16
16
|
return Math.max(v, pressed(name) ? 1 : 0);
|
|
17
17
|
};
|
|
18
|
+
const { width, height } = (0, react_native_1.useWindowDimensions)();
|
|
19
|
+
const isPortrait = height >= width;
|
|
20
|
+
const containerScaleStyle = isPortrait
|
|
21
|
+
? { transform: [{ scale: 0.7 }] }
|
|
22
|
+
: undefined;
|
|
18
23
|
const mix = (a, b, t) => Math.round(a + (b - a) * t);
|
|
19
24
|
const stickColor = (mag) => {
|
|
20
25
|
const clamped = Math.min(1, Math.max(0, mag));
|
|
@@ -30,7 +35,7 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
30
35
|
};
|
|
31
36
|
const leftMag = Math.min(1, Math.hypot(axis("leftX"), axis("leftY")));
|
|
32
37
|
const rightMag = Math.min(1, Math.hypot(axis("rightX"), axis("rightY")));
|
|
33
|
-
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.psWrapper, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.psContainer, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.psShoulderRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
38
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.psWrapper, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.psContainer, containerScaleStyle], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.psShoulderRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
34
39
|
styles.psShoulder,
|
|
35
40
|
styles.psShoulderLeft,
|
|
36
41
|
level("lb") > 0 && styles.psShoulderActive,
|
|
@@ -62,7 +67,10 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
62
67
|
], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
|
|
63
68
|
styles.psShoulderText,
|
|
64
69
|
level("rt") > 0 && styles.psShoulderTextActive,
|
|
65
|
-
], children: "R2" }) })] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.psMiddle }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style:
|
|
70
|
+
], children: "R2" }) })] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.psMiddle }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
71
|
+
styles.psPaveTactile,
|
|
72
|
+
pressed("touchpad") && styles.psPaveTactileActive,
|
|
73
|
+
] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
66
74
|
styles.psShare,
|
|
67
75
|
pressed("back") && styles.psActiveButton,
|
|
68
76
|
] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
@@ -121,7 +129,7 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
121
129
|
{
|
|
122
130
|
transform: [
|
|
123
131
|
{ translateX: axis("leftX") * 6 },
|
|
124
|
-
{ translateY: axis("leftY") *
|
|
132
|
+
{ translateY: axis("leftY") * 6 },
|
|
125
133
|
],
|
|
126
134
|
backgroundColor: stickColor(leftMag),
|
|
127
135
|
},
|
|
@@ -142,7 +150,7 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
142
150
|
{
|
|
143
151
|
transform: [
|
|
144
152
|
{ translateX: axis("rightX") * 6 },
|
|
145
|
-
{ translateY: axis("rightY") *
|
|
153
|
+
{ translateY: axis("rightY") * 6 },
|
|
146
154
|
],
|
|
147
155
|
backgroundColor: stickColor(rightMag),
|
|
148
156
|
},
|
|
@@ -155,7 +163,8 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
155
163
|
] }) }) })] })] }) }));
|
|
156
164
|
}
|
|
157
165
|
function GamepadDebug({ enabled = true, axisThreshold = 0.15, }) {
|
|
158
|
-
|
|
166
|
+
var _a, _b, _c, _d;
|
|
167
|
+
const { bridge, pressedButtons, axes, buttonValues, info, vibrate, stopVibration, } = (0, useGamepad_1.default)({
|
|
159
168
|
enabled,
|
|
160
169
|
axisThreshold,
|
|
161
170
|
});
|
|
@@ -163,7 +172,59 @@ function GamepadDebug({ enabled = true, axisThreshold = 0.15, }) {
|
|
|
163
172
|
const pressed = (key) => pressedButtons.has(key);
|
|
164
173
|
const axisValue = (key) => { var _a; return (_a = axes[key]) !== null && _a !== void 0 ? _a : 0; };
|
|
165
174
|
const buttonValue = (key) => { var _a; return (_a = buttonValues[key]) !== null && _a !== void 0 ? _a : 0; };
|
|
166
|
-
|
|
175
|
+
const touchpadPressed = pressed("touchpad");
|
|
176
|
+
const renderStickCard = (title, xLabel, yLabel, x, y) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.stickCard, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.stickTitle, children: title }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.stickContent, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.stickAxesList, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.stickAxisRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.stickAxisLabel, children: xLabel }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.stickAxisValue, children: format(x) })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.stickAxisRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.stickAxisLabel, children: yLabel }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.stickAxisValue, children: format(y) })] })] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.stickPlot, children: (() => {
|
|
177
|
+
const radius = 44;
|
|
178
|
+
const dotX = x * radius;
|
|
179
|
+
const dotY = y * radius;
|
|
180
|
+
const distance = Math.hypot(dotX, dotY);
|
|
181
|
+
const hasMovement = distance > 1;
|
|
182
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.stickCircle, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.stickCrosshairH }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.stickCrosshairV }), hasMovement && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
183
|
+
styles.stickGuideDiag,
|
|
184
|
+
{
|
|
185
|
+
width: distance,
|
|
186
|
+
left: 55,
|
|
187
|
+
top: 55,
|
|
188
|
+
transform: [
|
|
189
|
+
{
|
|
190
|
+
rotate: `${Math.atan2(dotY, dotX) *
|
|
191
|
+
(180 / Math.PI)}deg`,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
] })), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
196
|
+
styles.stickDot,
|
|
197
|
+
{
|
|
198
|
+
transform: [
|
|
199
|
+
{ translateX: dotX },
|
|
200
|
+
{ translateY: dotY },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
] })] }));
|
|
204
|
+
})() })] })] }));
|
|
205
|
+
const isConnected = info.connected;
|
|
206
|
+
const controllerLabel = (_a = info.id) !== null && _a !== void 0 ? _a : "No controller detected";
|
|
207
|
+
const vendorLabel = (_b = info.vendor) !== null && _b !== void 0 ? _b : "—";
|
|
208
|
+
const productLabel = (_c = info.product) !== null && _c !== void 0 ? _c : "—";
|
|
209
|
+
const mappingLabel = (_d = info.mapping) !== null && _d !== void 0 ? _d : "unknown";
|
|
210
|
+
const timestampLabel = info.timestamp != null ? Math.round(info.timestamp).toString() : "—";
|
|
211
|
+
const infoItems = [
|
|
212
|
+
{ label: "Name", value: controllerLabel },
|
|
213
|
+
{ label: "Vendor", value: vendorLabel },
|
|
214
|
+
{ label: "Product", value: productLabel },
|
|
215
|
+
{ label: "Mapping", value: mappingLabel },
|
|
216
|
+
{ label: "Index", value: info.index != null ? `${info.index}` : "—" },
|
|
217
|
+
{ label: "Axes", value: `${info.axes || 0}` },
|
|
218
|
+
{ label: "Buttons", value: `${info.buttons || 0}` },
|
|
219
|
+
{ label: "Vibration", value: info.canVibrate ? "Yes" : "No" },
|
|
220
|
+
{ label: "Timestamp", value: timestampLabel },
|
|
221
|
+
];
|
|
222
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.container, children: (0, jsx_runtime_1.jsxs)(react_native_1.ScrollView, { contentContainerStyle: styles.scrollContent, showsVerticalScrollIndicator: false, children: [bridge, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.body, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.card, styles.controllerCard], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.statusRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.cardTitle, children: "Controller" }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
223
|
+
styles.statusPill,
|
|
224
|
+
isConnected
|
|
225
|
+
? styles.statusPillConnected
|
|
226
|
+
: styles.statusPillWaiting,
|
|
227
|
+
], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.statusPillText, children: isConnected ? "Connected" : "Waiting" }) })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.helperText, children: "Connect your gamepad and press a button to test." }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.infoGrid, children: infoItems.map(({ label, value }) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.infoItem, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.infoLabel, children: label }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.infoValue, children: value })] }, label))) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.controller, children: [!isConnected && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.loaderOverlay, children: [(0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { color: "#2563eb", size: "small" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loaderText, children: "Connect your gamepad and press a button to test..." })] })), (0, jsx_runtime_1.jsx)(ControllerVisual, { pressed: pressed, axis: axisValue, value: buttonValue })] }), info.canVibrate && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.testsRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.button, onPress: () => vibrate(900, 1), children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.buttonText, children: "Vibration, 1 sec" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.button, onPress: stopVibration, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.buttonText, children: "Stop vibration" }) })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.helperTextSmall, children: "Uses vibrationActuator when available." })] })), !info.canVibrate && ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.helperTextSmall, children: "Vibration not reported by this controller." }))] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.card, styles.stateCard], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.cardTitle, children: "State" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.sectionTitle, children: "Pressed" }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.badgeRow, children: pressedList.length === 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.muted, children: "None" })) : (pressedList.map((name) => ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.badge, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.badgeText, children: name }) }, name)))) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.sectionTitle, children: "Axes" }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.stickGrid, children: [renderStickCard("L STICK", "Axis 0 (leftX)", "Axis 1 (leftY)", axisValue("leftX"), axisValue("leftY")), renderStickCard("R STICK", "Axis 2 (rightX)", "Axis 3 (rightY)", axisValue("rightX"), axisValue("rightY"))] })] })] })] }) }));
|
|
167
228
|
}
|
|
168
229
|
const styles = react_native_1.StyleSheet.create({
|
|
169
230
|
container: {
|
|
@@ -252,6 +313,106 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
252
313
|
fontSize: 16,
|
|
253
314
|
fontVariant: ["tabular-nums"],
|
|
254
315
|
},
|
|
316
|
+
stickGrid: {
|
|
317
|
+
flexDirection: "row",
|
|
318
|
+
flexWrap: "wrap",
|
|
319
|
+
gap: 10,
|
|
320
|
+
},
|
|
321
|
+
stickCard: {
|
|
322
|
+
flexGrow: 1,
|
|
323
|
+
flexBasis: "48%",
|
|
324
|
+
minWidth: 240,
|
|
325
|
+
backgroundColor: "#f8fafc",
|
|
326
|
+
borderWidth: 1,
|
|
327
|
+
borderColor: "#cbd5e1",
|
|
328
|
+
borderRadius: 10,
|
|
329
|
+
padding: 10,
|
|
330
|
+
gap: 10,
|
|
331
|
+
},
|
|
332
|
+
stickTitle: {
|
|
333
|
+
color: "#475569",
|
|
334
|
+
fontSize: 12,
|
|
335
|
+
fontWeight: "700",
|
|
336
|
+
letterSpacing: 0.3,
|
|
337
|
+
},
|
|
338
|
+
stickContent: {
|
|
339
|
+
flexDirection: "row",
|
|
340
|
+
justifyContent: "space-between",
|
|
341
|
+
alignItems: "center",
|
|
342
|
+
gap: 12,
|
|
343
|
+
},
|
|
344
|
+
stickAxesList: {
|
|
345
|
+
gap: 6,
|
|
346
|
+
},
|
|
347
|
+
stickAxisRow: {
|
|
348
|
+
flexDirection: "row",
|
|
349
|
+
justifyContent: "space-between",
|
|
350
|
+
alignItems: "center",
|
|
351
|
+
gap: 8,
|
|
352
|
+
},
|
|
353
|
+
stickAxisLabel: {
|
|
354
|
+
color: "#475569",
|
|
355
|
+
fontSize: 12,
|
|
356
|
+
fontWeight: "600",
|
|
357
|
+
},
|
|
358
|
+
stickAxisValue: {
|
|
359
|
+
color: "#0f172a",
|
|
360
|
+
fontSize: 16,
|
|
361
|
+
fontWeight: "700",
|
|
362
|
+
fontVariant: ["tabular-nums"],
|
|
363
|
+
},
|
|
364
|
+
stickPlot: {
|
|
365
|
+
width: 120,
|
|
366
|
+
height: 120,
|
|
367
|
+
alignItems: "center",
|
|
368
|
+
justifyContent: "center",
|
|
369
|
+
},
|
|
370
|
+
stickCircle: {
|
|
371
|
+
width: 110,
|
|
372
|
+
height: 110,
|
|
373
|
+
borderRadius: 55,
|
|
374
|
+
borderWidth: 1,
|
|
375
|
+
borderColor: "#cbd5e1",
|
|
376
|
+
backgroundColor: "#fff",
|
|
377
|
+
alignItems: "center",
|
|
378
|
+
justifyContent: "center",
|
|
379
|
+
position: "relative",
|
|
380
|
+
},
|
|
381
|
+
stickCrosshairH: {
|
|
382
|
+
position: "absolute",
|
|
383
|
+
left: 5,
|
|
384
|
+
right: 5,
|
|
385
|
+
top: 55,
|
|
386
|
+
height: 1,
|
|
387
|
+
backgroundColor: "#e2e8f0",
|
|
388
|
+
},
|
|
389
|
+
stickCrosshairV: {
|
|
390
|
+
position: "absolute",
|
|
391
|
+
top: 5,
|
|
392
|
+
bottom: 5,
|
|
393
|
+
left: 55,
|
|
394
|
+
width: 1,
|
|
395
|
+
backgroundColor: "#e2e8f0",
|
|
396
|
+
},
|
|
397
|
+
stickGuideDiag: {
|
|
398
|
+
position: "absolute",
|
|
399
|
+
height: 0,
|
|
400
|
+
borderTopWidth: 1,
|
|
401
|
+
borderColor: "#94a3b8",
|
|
402
|
+
borderStyle: "dashed",
|
|
403
|
+
transformOrigin: "left center",
|
|
404
|
+
},
|
|
405
|
+
stickDot: {
|
|
406
|
+
width: 10,
|
|
407
|
+
height: 10,
|
|
408
|
+
borderRadius: 5,
|
|
409
|
+
backgroundColor: "#1d4ed8",
|
|
410
|
+
position: "absolute",
|
|
411
|
+
top: 55,
|
|
412
|
+
left: 55,
|
|
413
|
+
marginLeft: -5,
|
|
414
|
+
marginTop: -5,
|
|
415
|
+
},
|
|
255
416
|
body: {
|
|
256
417
|
flexDirection: "row",
|
|
257
418
|
flexWrap: "wrap",
|
|
@@ -279,6 +440,65 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
279
440
|
fontWeight: "700",
|
|
280
441
|
fontSize: 16,
|
|
281
442
|
},
|
|
443
|
+
statusRow: {
|
|
444
|
+
flexDirection: "row",
|
|
445
|
+
alignItems: "center",
|
|
446
|
+
justifyContent: "space-between",
|
|
447
|
+
gap: 8,
|
|
448
|
+
},
|
|
449
|
+
statusPill: {
|
|
450
|
+
paddingHorizontal: 10,
|
|
451
|
+
paddingVertical: 4,
|
|
452
|
+
borderRadius: 999,
|
|
453
|
+
borderWidth: 1,
|
|
454
|
+
},
|
|
455
|
+
statusPillConnected: {
|
|
456
|
+
backgroundColor: "#dcfce7",
|
|
457
|
+
borderColor: "#16a34a",
|
|
458
|
+
},
|
|
459
|
+
statusPillWaiting: {
|
|
460
|
+
backgroundColor: "#fee2e2",
|
|
461
|
+
borderColor: "#f97316",
|
|
462
|
+
},
|
|
463
|
+
statusPillText: {
|
|
464
|
+
fontSize: 12,
|
|
465
|
+
fontWeight: "700",
|
|
466
|
+
color: "#0f172a",
|
|
467
|
+
},
|
|
468
|
+
helperText: {
|
|
469
|
+
color: "#475569",
|
|
470
|
+
fontSize: 12,
|
|
471
|
+
},
|
|
472
|
+
helperTextSmall: {
|
|
473
|
+
color: "#94a3b8",
|
|
474
|
+
fontSize: 11,
|
|
475
|
+
},
|
|
476
|
+
infoGrid: {
|
|
477
|
+
flexDirection: "row",
|
|
478
|
+
flexWrap: "wrap",
|
|
479
|
+
gap: 8,
|
|
480
|
+
},
|
|
481
|
+
infoItem: {
|
|
482
|
+
flexGrow: 1,
|
|
483
|
+
flexBasis: "45%",
|
|
484
|
+
minWidth: 140,
|
|
485
|
+
backgroundColor: "#f8fafc",
|
|
486
|
+
borderWidth: 1,
|
|
487
|
+
borderColor: "#e2e8f0",
|
|
488
|
+
borderRadius: 8,
|
|
489
|
+
padding: 8,
|
|
490
|
+
gap: 2,
|
|
491
|
+
},
|
|
492
|
+
infoLabel: {
|
|
493
|
+
color: "#475569",
|
|
494
|
+
fontSize: 11,
|
|
495
|
+
fontWeight: "700",
|
|
496
|
+
},
|
|
497
|
+
infoValue: {
|
|
498
|
+
color: "#0f172a",
|
|
499
|
+
fontSize: 13,
|
|
500
|
+
fontWeight: "600",
|
|
501
|
+
},
|
|
282
502
|
controller: {
|
|
283
503
|
backgroundColor: "#f8fafc",
|
|
284
504
|
borderWidth: 1,
|
|
@@ -286,6 +506,50 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
286
506
|
padding: 12,
|
|
287
507
|
alignItems: "center",
|
|
288
508
|
justifyContent: "center",
|
|
509
|
+
position: "relative",
|
|
510
|
+
overflow: "hidden",
|
|
511
|
+
},
|
|
512
|
+
loaderOverlay: {
|
|
513
|
+
position: "absolute",
|
|
514
|
+
top: 0,
|
|
515
|
+
left: 0,
|
|
516
|
+
right: 0,
|
|
517
|
+
bottom: 0,
|
|
518
|
+
backgroundColor: "rgba(255,255,255,0.92)",
|
|
519
|
+
alignItems: "center",
|
|
520
|
+
justifyContent: "center",
|
|
521
|
+
padding: 12,
|
|
522
|
+
gap: 6,
|
|
523
|
+
zIndex: 50,
|
|
524
|
+
},
|
|
525
|
+
loaderText: {
|
|
526
|
+
color: "#0f172a",
|
|
527
|
+
fontSize: 12,
|
|
528
|
+
textAlign: "center",
|
|
529
|
+
},
|
|
530
|
+
testsRow: {
|
|
531
|
+
flexDirection: "row",
|
|
532
|
+
flexWrap: "wrap",
|
|
533
|
+
gap: 8,
|
|
534
|
+
},
|
|
535
|
+
button: {
|
|
536
|
+
backgroundColor: "#2563eb",
|
|
537
|
+
paddingVertical: 10,
|
|
538
|
+
paddingHorizontal: 12,
|
|
539
|
+
borderRadius: 8,
|
|
540
|
+
alignItems: "center",
|
|
541
|
+
justifyContent: "center",
|
|
542
|
+
},
|
|
543
|
+
buttonDisabled: {
|
|
544
|
+
backgroundColor: "#cbd5e1",
|
|
545
|
+
},
|
|
546
|
+
buttonText: {
|
|
547
|
+
color: "#f8fafc",
|
|
548
|
+
fontWeight: "700",
|
|
549
|
+
fontSize: 13,
|
|
550
|
+
},
|
|
551
|
+
buttonTextDisabled: {
|
|
552
|
+
color: "#475569",
|
|
289
553
|
},
|
|
290
554
|
axesGrid: {
|
|
291
555
|
flexDirection: "row",
|
|
@@ -349,7 +613,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
349
613
|
width: 395,
|
|
350
614
|
height: 160,
|
|
351
615
|
backgroundColor: "#e0e0e0",
|
|
352
|
-
marginLeft: 27
|
|
616
|
+
marginLeft: 50, // 27
|
|
353
617
|
borderRadius: 25,
|
|
354
618
|
position: "absolute",
|
|
355
619
|
zIndex: 0,
|
|
@@ -359,7 +623,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
359
623
|
width: 150,
|
|
360
624
|
height: 80,
|
|
361
625
|
backgroundColor: "#333",
|
|
362
|
-
marginLeft:
|
|
626
|
+
marginLeft: 170,
|
|
363
627
|
borderRadius: 7,
|
|
364
628
|
position: "absolute",
|
|
365
629
|
zIndex: 10,
|
|
@@ -370,12 +634,18 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
370
634
|
shadowRadius: 10,
|
|
371
635
|
elevation: 5,
|
|
372
636
|
},
|
|
637
|
+
psPaveTactileActive: {
|
|
638
|
+
backgroundColor: "#2563eb",
|
|
639
|
+
shadowColor: "#2563eb",
|
|
640
|
+
shadowOpacity: 0.65,
|
|
641
|
+
shadowRadius: 12,
|
|
642
|
+
},
|
|
373
643
|
psShare: {
|
|
374
644
|
width: 12,
|
|
375
645
|
height: 25,
|
|
376
646
|
position: "absolute",
|
|
377
647
|
backgroundColor: "#95a5a6",
|
|
378
|
-
marginLeft:
|
|
648
|
+
marginLeft: 148,
|
|
379
649
|
marginTop: 85,
|
|
380
650
|
borderRadius: 5,
|
|
381
651
|
zIndex: 10,
|
|
@@ -390,7 +660,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
390
660
|
height: 25,
|
|
391
661
|
position: "absolute",
|
|
392
662
|
backgroundColor: "#95a5a6",
|
|
393
|
-
marginLeft:
|
|
663
|
+
marginLeft: 327,
|
|
394
664
|
marginTop: 85,
|
|
395
665
|
borderRadius: 5,
|
|
396
666
|
zIndex: 10,
|
|
@@ -405,7 +675,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
405
675
|
height: 260,
|
|
406
676
|
backgroundColor: "#e0e0e0",
|
|
407
677
|
position: "absolute",
|
|
408
|
-
left:
|
|
678
|
+
left: 23,
|
|
409
679
|
top: 95,
|
|
410
680
|
transform: [{ rotate: "11deg" }],
|
|
411
681
|
borderTopLeftRadius: 30,
|
|
@@ -426,7 +696,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
426
696
|
height: 260,
|
|
427
697
|
backgroundColor: "#e0e0e0",
|
|
428
698
|
position: "absolute",
|
|
429
|
-
left:
|
|
699
|
+
left: 353,
|
|
430
700
|
top: 95,
|
|
431
701
|
transform: [{ rotate: "-11deg" }],
|
|
432
702
|
borderTopLeftRadius: 30,
|
|
@@ -448,7 +718,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
448
718
|
height: 112,
|
|
449
719
|
borderRadius: 56,
|
|
450
720
|
marginTop: 5,
|
|
451
|
-
marginLeft:
|
|
721
|
+
marginLeft: 0,
|
|
452
722
|
borderWidth: 1,
|
|
453
723
|
borderColor: "#b0b0b0",
|
|
454
724
|
position: "relative",
|
|
@@ -514,7 +784,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
514
784
|
width: 112,
|
|
515
785
|
height: 112,
|
|
516
786
|
borderRadius: 56,
|
|
517
|
-
marginTop:
|
|
787
|
+
marginTop: 4,
|
|
518
788
|
borderWidth: 1,
|
|
519
789
|
borderColor: "#b0b0b0",
|
|
520
790
|
position: "relative",
|
package/dist/types.d.ts
CHANGED
|
@@ -1,26 +1,47 @@
|
|
|
1
|
-
export type DpadKey =
|
|
2
|
-
export type GamepadButtonName =
|
|
3
|
-
export type StickAxisName =
|
|
1
|
+
export type DpadKey = "up" | "down" | "left" | "right";
|
|
2
|
+
export type GamepadButtonName = "a" | "b" | "x" | "y" | "lb" | "rb" | "lt" | "rt" | "back" | "start" | "ls" | "rs" | "dpadUp" | "dpadDown" | "dpadLeft" | "dpadRight" | "home" | "touchpad" | `button-${number}`;
|
|
3
|
+
export type StickAxisName = "leftX" | "leftY" | "rightX" | "rightY" | `axis-${number}`;
|
|
4
4
|
export type DpadEvent = {
|
|
5
|
-
type:
|
|
5
|
+
type: "dpad";
|
|
6
6
|
key: DpadKey;
|
|
7
7
|
pressed: boolean;
|
|
8
8
|
};
|
|
9
9
|
export type ButtonEvent = {
|
|
10
|
-
type:
|
|
10
|
+
type: "button";
|
|
11
11
|
button: GamepadButtonName;
|
|
12
12
|
index: number;
|
|
13
13
|
pressed: boolean;
|
|
14
14
|
value: number;
|
|
15
15
|
};
|
|
16
16
|
export type AxisEvent = {
|
|
17
|
-
type:
|
|
17
|
+
type: "axis";
|
|
18
18
|
axis: StickAxisName;
|
|
19
19
|
index: number;
|
|
20
20
|
value: number;
|
|
21
21
|
};
|
|
22
|
+
export type GamepadInfo = {
|
|
23
|
+
connected: boolean;
|
|
24
|
+
index: number | null;
|
|
25
|
+
id: string | null;
|
|
26
|
+
mapping: string | null;
|
|
27
|
+
timestamp: number | null;
|
|
28
|
+
canVibrate: boolean;
|
|
29
|
+
vendor: string | null;
|
|
30
|
+
product: string | null;
|
|
31
|
+
axes: number;
|
|
32
|
+
buttons: number;
|
|
33
|
+
};
|
|
34
|
+
export type InfoEvent = GamepadInfo & {
|
|
35
|
+
type: "info";
|
|
36
|
+
};
|
|
37
|
+
export type StateEvent = {
|
|
38
|
+
type: "state";
|
|
39
|
+
pressed: GamepadButtonName[];
|
|
40
|
+
values: Partial<Record<GamepadButtonName, number>>;
|
|
41
|
+
axes: Partial<Record<StickAxisName, number>>;
|
|
42
|
+
};
|
|
22
43
|
export type StatusEvent = {
|
|
23
|
-
type:
|
|
24
|
-
state:
|
|
44
|
+
type: "status";
|
|
45
|
+
state: "connected" | "disconnected";
|
|
25
46
|
};
|
|
26
|
-
export type GamepadMessage = DpadEvent | ButtonEvent | AxisEvent | StatusEvent;
|
|
47
|
+
export type GamepadMessage = DpadEvent | ButtonEvent | AxisEvent | StatusEvent | InfoEvent | StateEvent;
|
package/dist/useGamepad.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JSX } from "react";
|
|
2
|
-
import type { AxisEvent, ButtonEvent, DpadEvent, GamepadButtonName, StickAxisName, StatusEvent } from "./types";
|
|
2
|
+
import type { AxisEvent, ButtonEvent, DpadEvent, GamepadButtonName, GamepadInfo, StickAxisName, InfoEvent, StatusEvent } from "./types";
|
|
3
3
|
type Options = {
|
|
4
4
|
enabled?: boolean;
|
|
5
5
|
axisThreshold?: number;
|
|
@@ -7,6 +7,7 @@ type Options = {
|
|
|
7
7
|
onAxis?: (event: AxisEvent) => void;
|
|
8
8
|
onDpad?: (event: DpadEvent) => void;
|
|
9
9
|
onStatus?: (event: StatusEvent) => void;
|
|
10
|
+
onInfo?: (event: InfoEvent) => void;
|
|
10
11
|
};
|
|
11
12
|
type Return = {
|
|
12
13
|
pressedButtons: Set<GamepadButtonName>;
|
|
@@ -14,6 +15,9 @@ type Return = {
|
|
|
14
15
|
buttonValues: Partial<Record<GamepadButtonName, number>>;
|
|
15
16
|
isPressed: (key: GamepadButtonName) => boolean;
|
|
16
17
|
bridge: JSX.Element | null;
|
|
18
|
+
info: GamepadInfo;
|
|
19
|
+
vibrate: (duration?: number, strength?: number) => void;
|
|
20
|
+
stopVibration: () => void;
|
|
17
21
|
};
|
|
18
|
-
export default function useGamepad({ enabled, axisThreshold, onButton, onAxis, onDpad, onStatus, }?: Options): Return;
|
|
22
|
+
export default function useGamepad({ enabled, axisThreshold, onButton, onAxis, onDpad, onStatus, onInfo, }?: Options): Return;
|
|
19
23
|
export {};
|
package/dist/useGamepad.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.default = useGamepad;
|
|
|
7
7
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
8
|
const react_1 = require("react");
|
|
9
9
|
const GamepadBridge_1 = __importDefault(require("./GamepadBridge"));
|
|
10
|
-
function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, onDpad, onStatus, } = {}) {
|
|
10
|
+
function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, onDpad, onStatus, onInfo, } = {}) {
|
|
11
11
|
const [pressedButtons, setPressedButtons] = (0, react_1.useState)(new Set());
|
|
12
12
|
const [buttonValues, setButtonValues] = (0, react_1.useState)({});
|
|
13
13
|
const pressedRef = (0, react_1.useRef)(new Set());
|
|
@@ -17,6 +17,27 @@ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, on
|
|
|
17
17
|
rightX: 0,
|
|
18
18
|
rightY: 0,
|
|
19
19
|
});
|
|
20
|
+
const [info, setInfo] = (0, react_1.useState)({
|
|
21
|
+
connected: false,
|
|
22
|
+
index: null,
|
|
23
|
+
id: null,
|
|
24
|
+
mapping: null,
|
|
25
|
+
timestamp: null,
|
|
26
|
+
canVibrate: false,
|
|
27
|
+
vendor: null,
|
|
28
|
+
product: null,
|
|
29
|
+
axes: 0,
|
|
30
|
+
buttons: 0,
|
|
31
|
+
});
|
|
32
|
+
const [vibrationRequest, setVibrationRequest] = (0, react_1.useState)(null);
|
|
33
|
+
const vibrationNonce = (0, react_1.useRef)(0);
|
|
34
|
+
const resetState = (0, react_1.useCallback)(() => {
|
|
35
|
+
const cleared = new Set();
|
|
36
|
+
pressedRef.current = cleared;
|
|
37
|
+
setPressedButtons(cleared);
|
|
38
|
+
setButtonValues({});
|
|
39
|
+
setAxes({ leftX: 0, leftY: 0, rightX: 0, rightY: 0 });
|
|
40
|
+
}, []);
|
|
20
41
|
(0, react_1.useEffect)(() => {
|
|
21
42
|
pressedRef.current = pressedButtons;
|
|
22
43
|
}, [pressedButtons]);
|
|
@@ -43,17 +64,75 @@ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, on
|
|
|
43
64
|
const handleDpad = (0, react_1.useCallback)((event) => {
|
|
44
65
|
onDpad === null || onDpad === void 0 ? void 0 : onDpad(event);
|
|
45
66
|
}, [onDpad]);
|
|
46
|
-
(0, react_1.
|
|
47
|
-
if (!
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
const handleInfo = (0, react_1.useCallback)((event) => {
|
|
68
|
+
if (!event.connected) {
|
|
69
|
+
resetState();
|
|
70
|
+
}
|
|
71
|
+
setInfo(event);
|
|
72
|
+
onInfo === null || onInfo === void 0 ? void 0 : onInfo(event);
|
|
73
|
+
}, [onInfo, resetState]);
|
|
74
|
+
const handleState = (0, react_1.useCallback)((event) => {
|
|
75
|
+
var _a, _b;
|
|
76
|
+
const next = new Set(event.pressed);
|
|
77
|
+
pressedRef.current = next;
|
|
78
|
+
setPressedButtons(next);
|
|
79
|
+
setButtonValues((_a = event.values) !== null && _a !== void 0 ? _a : {});
|
|
80
|
+
setAxes((_b = event.axes) !== null && _b !== void 0 ? _b : {});
|
|
81
|
+
}, []);
|
|
82
|
+
const handleStatus = (0, react_1.useCallback)((event) => {
|
|
83
|
+
if (event.state === "disconnected") {
|
|
84
|
+
resetState();
|
|
85
|
+
setInfo((prev) => ({
|
|
86
|
+
...prev,
|
|
87
|
+
connected: false,
|
|
88
|
+
}));
|
|
50
89
|
}
|
|
90
|
+
onStatus === null || onStatus === void 0 ? void 0 : onStatus(event);
|
|
91
|
+
}, [onStatus, resetState]);
|
|
92
|
+
const vibrate = (0, react_1.useCallback)((duration = 800, strength = 1) => {
|
|
93
|
+
vibrationNonce.current += 1;
|
|
94
|
+
const safeStrength = Math.max(0, Math.min(strength, 1));
|
|
95
|
+
setVibrationRequest({
|
|
96
|
+
type: "once",
|
|
97
|
+
duration,
|
|
98
|
+
strong: safeStrength,
|
|
99
|
+
weak: safeStrength,
|
|
100
|
+
nonce: vibrationNonce.current,
|
|
101
|
+
});
|
|
102
|
+
}, []);
|
|
103
|
+
const stopVibration = (0, react_1.useCallback)(() => {
|
|
104
|
+
vibrationNonce.current += 1;
|
|
105
|
+
setVibrationRequest({ type: "stop", nonce: vibrationNonce.current });
|
|
106
|
+
}, []);
|
|
107
|
+
(0, react_1.useEffect)(() => {
|
|
51
108
|
if (!enabled) {
|
|
52
|
-
|
|
53
|
-
|
|
109
|
+
resetState();
|
|
110
|
+
setInfo((prev) => ({
|
|
111
|
+
...prev,
|
|
112
|
+
connected: false,
|
|
113
|
+
}));
|
|
54
114
|
}
|
|
55
|
-
}, [enabled]);
|
|
56
|
-
const bridge = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsx)(GamepadBridge_1.default, { enabled: enabled, axisThreshold: axisThreshold, onDpad: handleDpad, onButton: handleButton, onAxis: handleAxis, onStatus:
|
|
115
|
+
}, [enabled, resetState]);
|
|
116
|
+
const bridge = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsx)(GamepadBridge_1.default, { enabled: enabled, axisThreshold: axisThreshold, onDpad: handleDpad, onButton: handleButton, onAxis: handleAxis, onStatus: handleStatus, onInfo: handleInfo, onState: handleState, vibrationRequest: vibrationRequest !== null && vibrationRequest !== void 0 ? vibrationRequest : undefined })), [
|
|
117
|
+
enabled,
|
|
118
|
+
axisThreshold,
|
|
119
|
+
handleAxis,
|
|
120
|
+
handleButton,
|
|
121
|
+
handleDpad,
|
|
122
|
+
handleInfo,
|
|
123
|
+
handleStatus,
|
|
124
|
+
handleState,
|
|
125
|
+
vibrationRequest,
|
|
126
|
+
]);
|
|
57
127
|
const isPressed = (0, react_1.useCallback)((key) => pressedRef.current.has(key), []);
|
|
58
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
pressedButtons,
|
|
130
|
+
axes,
|
|
131
|
+
buttonValues,
|
|
132
|
+
isPressed,
|
|
133
|
+
bridge,
|
|
134
|
+
info,
|
|
135
|
+
vibrate,
|
|
136
|
+
stopVibration,
|
|
137
|
+
};
|
|
59
138
|
}
|