react-native-earl-gamepad 0.6.0 → 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 +14 -6
- package/dist/GamepadBridge.d.ts +3 -2
- package/dist/GamepadBridge.js +23 -4
- package/dist/GamepadDebug.js +144 -21
- package/dist/types.d.ts +8 -2
- package/dist/useGamepad.js +25 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
8
|
-
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
9
|
|
|
10
10
|
- Components: `GamepadBridge`, `useGamepad`, and `GamepadDebug`.
|
|
11
|
-
- Deadzone handling (default `0.15`) with auto-clear on disconnect.
|
|
12
|
-
- 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
13
|
|
|
14
14
|
### Why this?
|
|
15
15
|
|
|
@@ -134,6 +134,11 @@ export function HUD() {
|
|
|
134
134
|
|
|
135
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
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).
|
|
141
|
+
|
|
137
142
|
```tsx
|
|
138
143
|
import { GamepadDebug } from "react-native-earl-gamepad";
|
|
139
144
|
|
|
@@ -142,9 +147,9 @@ export function DebugScreen() {
|
|
|
142
147
|
}
|
|
143
148
|
```
|
|
144
149
|
|
|
145
|
-

|
|
151
|
+

|
|
152
|
+

|
|
148
153
|
|
|
149
154
|
## API
|
|
150
155
|
|
|
@@ -156,6 +161,7 @@ export function DebugScreen() {
|
|
|
156
161
|
- `onAxis?: (event: AxisEvent) => void` — fired when an axis changes beyond threshold.
|
|
157
162
|
- `onDpad?: (event: DpadEvent) => void` — convenience mapping of button indices 12–15.
|
|
158
163
|
- `onStatus?: (event: StatusEvent) => void` — `connected` / `disconnected` events.
|
|
164
|
+
- `onState?: (event: StateEvent) => void` — full snapshot of pressed buttons, values, and axes each poll.
|
|
159
165
|
- `style?: StyleProp<ViewStyle>` — override container; default is a 1×1 transparent view.
|
|
160
166
|
|
|
161
167
|
### `useGamepad` options and return
|
|
@@ -188,6 +194,8 @@ Return shape:
|
|
|
188
194
|
- `AxisEvent`: `{ type: 'axis'; axis: StickAxisName; index: number; value: number }`
|
|
189
195
|
- `DpadEvent`: `{ type: 'dpad'; key: 'up' | 'down' | 'left' | 'right'; pressed: boolean }`
|
|
190
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> }`
|
|
191
199
|
|
|
192
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`.
|
|
193
201
|
|
package/dist/GamepadBridge.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StyleProp, ViewStyle } from "react-native";
|
|
2
|
-
import type { AxisEvent, ButtonEvent, DpadEvent, InfoEvent, StatusEvent } from "./types";
|
|
2
|
+
import type { AxisEvent, ButtonEvent, DpadEvent, InfoEvent, StateEvent, StatusEvent } from "./types";
|
|
3
3
|
type VibrationRequest = {
|
|
4
4
|
type: "once";
|
|
5
5
|
duration: number;
|
|
@@ -18,8 +18,9 @@ type Props = {
|
|
|
18
18
|
onAxis?: (event: AxisEvent) => void;
|
|
19
19
|
onStatus?: (event: StatusEvent) => void;
|
|
20
20
|
onInfo?: (event: InfoEvent) => void;
|
|
21
|
+
onState?: (event: StateEvent) => void;
|
|
21
22
|
vibrationRequest?: VibrationRequest;
|
|
22
23
|
style?: StyleProp<ViewStyle>;
|
|
23
24
|
};
|
|
24
|
-
export default function GamepadBridge({ enabled, axisThreshold, onDpad, onButton, onAxis, onStatus, onInfo, vibrationRequest, 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;
|
|
25
26
|
export {};
|
package/dist/GamepadBridge.js
CHANGED
|
@@ -16,7 +16,7 @@ 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 = [];
|
|
@@ -79,14 +79,24 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
79
79
|
window.__earlVibrateOnce = vibrateOnce;
|
|
80
80
|
window.__earlStopVibration = stopVibration;
|
|
81
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
|
+
};
|
|
88
|
+
|
|
82
89
|
function poll(){
|
|
83
90
|
const pads = (navigator.getGamepads && navigator.getGamepads()) || [];
|
|
84
91
|
const gp = pads[0];
|
|
85
92
|
sendInfo(gp);
|
|
86
93
|
if (gp) {
|
|
94
|
+
const pressedNow = [];
|
|
95
|
+
const valueMap = {};
|
|
96
|
+
const axesState = {};
|
|
87
97
|
// Buttons
|
|
88
98
|
gp.buttons?.forEach((btn, index) => {
|
|
89
|
-
const name =
|
|
99
|
+
const name = nameForIndex(index);
|
|
90
100
|
const prevBtn = prevButtons[index] || { pressed:false, value:0 };
|
|
91
101
|
const changed = prevBtn.pressed !== btn.pressed || Math.abs(prevBtn.value - btn.value) > 0.01;
|
|
92
102
|
if (changed) {
|
|
@@ -97,6 +107,8 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
97
107
|
if (index === 14) send({ type:'dpad', key:'left', pressed: !!btn.pressed });
|
|
98
108
|
if (index === 15) send({ type:'dpad', key:'right', pressed: !!btn.pressed });
|
|
99
109
|
}
|
|
110
|
+
if (btn?.pressed) pressedNow.push(name);
|
|
111
|
+
valueMap[name] = btn?.value ?? 0;
|
|
100
112
|
prevButtons[index] = { pressed: !!btn.pressed, value: btn.value ?? 0 };
|
|
101
113
|
});
|
|
102
114
|
|
|
@@ -108,15 +120,18 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
108
120
|
if (Math.abs(prevVal - value) > 0.01) {
|
|
109
121
|
send({ type:'axis', axis:name, index, value });
|
|
110
122
|
}
|
|
123
|
+
axesState[name] = value;
|
|
111
124
|
prevAxes[index] = value;
|
|
112
125
|
});
|
|
126
|
+
|
|
127
|
+
send({ type:'state', pressed: pressedNow, values: valueMap, axes: axesState });
|
|
113
128
|
} else {
|
|
114
129
|
// If no pad, send a single disconnected info payload
|
|
115
130
|
sendInfo(null);
|
|
116
131
|
if (prevButtons.length) {
|
|
117
132
|
prevButtons.forEach((btn, index) => {
|
|
118
133
|
if (btn?.pressed) {
|
|
119
|
-
const name =
|
|
134
|
+
const name = nameForIndex(index);
|
|
120
135
|
send({ type:'button', button:name, index, pressed:false, value:0 });
|
|
121
136
|
if (index === 12) send({ type:'dpad', key:'up', pressed:false });
|
|
122
137
|
if (index === 13) send({ type:'dpad', key:'down', pressed:false });
|
|
@@ -135,6 +150,7 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
135
150
|
});
|
|
136
151
|
prevAxes = [];
|
|
137
152
|
}
|
|
153
|
+
send({ type:'state', pressed: [], values: {}, axes: {} });
|
|
138
154
|
}
|
|
139
155
|
requestAnimationFrame(poll);
|
|
140
156
|
}
|
|
@@ -160,7 +176,7 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
160
176
|
})();
|
|
161
177
|
</script>
|
|
162
178
|
</body></html>`;
|
|
163
|
-
function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton, onAxis, onStatus, onInfo, vibrationRequest, style, }) {
|
|
179
|
+
function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton, onAxis, onStatus, onInfo, onState, vibrationRequest, style, }) {
|
|
164
180
|
const webviewRef = (0, react_1.useRef)(null);
|
|
165
181
|
const html = (0, react_1.useMemo)(() => buildBridgeHtml(axisThreshold), [axisThreshold]);
|
|
166
182
|
const focusBridge = (0, react_1.useCallback)(() => {
|
|
@@ -207,6 +223,9 @@ function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton,
|
|
|
207
223
|
else if (data.type === "info") {
|
|
208
224
|
onInfo === null || onInfo === void 0 ? void 0 : onInfo(data);
|
|
209
225
|
}
|
|
226
|
+
else if (data.type === "state") {
|
|
227
|
+
onState === null || onState === void 0 ? void 0 : onState(data);
|
|
228
|
+
}
|
|
210
229
|
}
|
|
211
230
|
catch {
|
|
212
231
|
// ignore malformed messages
|
package/dist/GamepadDebug.js
CHANGED
|
@@ -18,7 +18,7 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
18
18
|
const { width, height } = (0, react_native_1.useWindowDimensions)();
|
|
19
19
|
const isPortrait = height >= width;
|
|
20
20
|
const containerScaleStyle = isPortrait
|
|
21
|
-
? { transform: [{ scale: 0.
|
|
21
|
+
? { transform: [{ scale: 0.7 }] }
|
|
22
22
|
: undefined;
|
|
23
23
|
const mix = (a, b, t) => Math.round(a + (b - a) * t);
|
|
24
24
|
const stickColor = (mag) => {
|
|
@@ -67,7 +67,10 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
67
67
|
], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
|
|
68
68
|
styles.psShoulderText,
|
|
69
69
|
level("rt") > 0 && styles.psShoulderTextActive,
|
|
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:
|
|
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: [
|
|
71
74
|
styles.psShare,
|
|
72
75
|
pressed("back") && styles.psActiveButton,
|
|
73
76
|
] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
@@ -126,7 +129,7 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
126
129
|
{
|
|
127
130
|
transform: [
|
|
128
131
|
{ translateX: axis("leftX") * 6 },
|
|
129
|
-
{ translateY: axis("leftY") *
|
|
132
|
+
{ translateY: axis("leftY") * 6 },
|
|
130
133
|
],
|
|
131
134
|
backgroundColor: stickColor(leftMag),
|
|
132
135
|
},
|
|
@@ -147,7 +150,7 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
147
150
|
{
|
|
148
151
|
transform: [
|
|
149
152
|
{ translateX: axis("rightX") * 6 },
|
|
150
|
-
{ translateY: axis("rightY") *
|
|
153
|
+
{ translateY: axis("rightY") * 6 },
|
|
151
154
|
],
|
|
152
155
|
backgroundColor: stickColor(rightMag),
|
|
153
156
|
},
|
|
@@ -169,6 +172,36 @@ function GamepadDebug({ enabled = true, axisThreshold = 0.15, }) {
|
|
|
169
172
|
const pressed = (key) => pressedButtons.has(key);
|
|
170
173
|
const axisValue = (key) => { var _a; return (_a = axes[key]) !== null && _a !== void 0 ? _a : 0; };
|
|
171
174
|
const buttonValue = (key) => { var _a; return (_a = buttonValues[key]) !== null && _a !== void 0 ? _a : 0; };
|
|
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
|
+
})() })] })] }));
|
|
172
205
|
const isConnected = info.connected;
|
|
173
206
|
const controllerLabel = (_a = info.id) !== null && _a !== void 0 ? _a : "No controller detected";
|
|
174
207
|
const vendorLabel = (_b = info.vendor) !== null && _b !== void 0 ? _b : "—";
|
|
@@ -191,23 +224,7 @@ function GamepadDebug({ enabled = true, axisThreshold = 0.15, }) {
|
|
|
191
224
|
isConnected
|
|
192
225
|
? styles.statusPillConnected
|
|
193
226
|
: styles.statusPillWaiting,
|
|
194
|
-
], 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 })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.testsRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: [
|
|
195
|
-
styles.button,
|
|
196
|
-
!info.canVibrate && styles.buttonDisabled,
|
|
197
|
-
], onPress: () => info.canVibrate && vibrate(900, 1), disabled: !info.canVibrate, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
|
|
198
|
-
styles.buttonText,
|
|
199
|
-
!info.canVibrate &&
|
|
200
|
-
styles.buttonTextDisabled,
|
|
201
|
-
], children: "Vibration, 1 sec" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: [
|
|
202
|
-
styles.button,
|
|
203
|
-
!info.canVibrate && styles.buttonDisabled,
|
|
204
|
-
], onPress: () => info.canVibrate && stopVibration(), disabled: !info.canVibrate, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
|
|
205
|
-
styles.buttonText,
|
|
206
|
-
!info.canVibrate &&
|
|
207
|
-
styles.buttonTextDisabled,
|
|
208
|
-
], children: "Stop vibration" }) })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.helperTextSmall, children: info.canVibrate
|
|
209
|
-
? "Uses vibrationActuator when available."
|
|
210
|
-
: "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.jsx)(react_native_1.View, { style: styles.axesGrid, children: trackedAxes.map((axisName) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.axisItem, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.axisLabel, children: axisName }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.axisValue, children: format(axes[axisName]) })] }, axisName))) })] })] })] }) }));
|
|
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"))] })] })] })] }) }));
|
|
211
228
|
}
|
|
212
229
|
const styles = react_native_1.StyleSheet.create({
|
|
213
230
|
container: {
|
|
@@ -296,6 +313,106 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
296
313
|
fontSize: 16,
|
|
297
314
|
fontVariant: ["tabular-nums"],
|
|
298
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
|
+
},
|
|
299
416
|
body: {
|
|
300
417
|
flexDirection: "row",
|
|
301
418
|
flexWrap: "wrap",
|
|
@@ -517,6 +634,12 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
517
634
|
shadowRadius: 10,
|
|
518
635
|
elevation: 5,
|
|
519
636
|
},
|
|
637
|
+
psPaveTactileActive: {
|
|
638
|
+
backgroundColor: "#2563eb",
|
|
639
|
+
shadowColor: "#2563eb",
|
|
640
|
+
shadowOpacity: 0.65,
|
|
641
|
+
shadowRadius: 12,
|
|
642
|
+
},
|
|
520
643
|
psShare: {
|
|
521
644
|
width: 12,
|
|
522
645
|
height: 25,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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" | `button-${number}`;
|
|
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
3
|
export type StickAxisName = "leftX" | "leftY" | "rightX" | "rightY" | `axis-${number}`;
|
|
4
4
|
export type DpadEvent = {
|
|
5
5
|
type: "dpad";
|
|
@@ -34,8 +34,14 @@ export type GamepadInfo = {
|
|
|
34
34
|
export type InfoEvent = GamepadInfo & {
|
|
35
35
|
type: "info";
|
|
36
36
|
};
|
|
37
|
+
export type StateEvent = {
|
|
38
|
+
type: "state";
|
|
39
|
+
pressed: GamepadButtonName[];
|
|
40
|
+
values: Partial<Record<GamepadButtonName, number>>;
|
|
41
|
+
axes: Partial<Record<StickAxisName, number>>;
|
|
42
|
+
};
|
|
37
43
|
export type StatusEvent = {
|
|
38
44
|
type: "status";
|
|
39
45
|
state: "connected" | "disconnected";
|
|
40
46
|
};
|
|
41
|
-
export type GamepadMessage = DpadEvent | ButtonEvent | AxisEvent | StatusEvent | InfoEvent;
|
|
47
|
+
export type GamepadMessage = DpadEvent | ButtonEvent | AxisEvent | StatusEvent | InfoEvent | StateEvent;
|
package/dist/useGamepad.js
CHANGED
|
@@ -31,6 +31,13 @@ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, on
|
|
|
31
31
|
});
|
|
32
32
|
const [vibrationRequest, setVibrationRequest] = (0, react_1.useState)(null);
|
|
33
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
|
+
}, []);
|
|
34
41
|
(0, react_1.useEffect)(() => {
|
|
35
42
|
pressedRef.current = pressedButtons;
|
|
36
43
|
}, [pressedButtons]);
|
|
@@ -58,18 +65,30 @@ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, on
|
|
|
58
65
|
onDpad === null || onDpad === void 0 ? void 0 : onDpad(event);
|
|
59
66
|
}, [onDpad]);
|
|
60
67
|
const handleInfo = (0, react_1.useCallback)((event) => {
|
|
68
|
+
if (!event.connected) {
|
|
69
|
+
resetState();
|
|
70
|
+
}
|
|
61
71
|
setInfo(event);
|
|
62
72
|
onInfo === null || onInfo === void 0 ? void 0 : onInfo(event);
|
|
63
|
-
}, [onInfo]);
|
|
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
|
+
}, []);
|
|
64
82
|
const handleStatus = (0, react_1.useCallback)((event) => {
|
|
65
83
|
if (event.state === "disconnected") {
|
|
84
|
+
resetState();
|
|
66
85
|
setInfo((prev) => ({
|
|
67
86
|
...prev,
|
|
68
87
|
connected: false,
|
|
69
88
|
}));
|
|
70
89
|
}
|
|
71
90
|
onStatus === null || onStatus === void 0 ? void 0 : onStatus(event);
|
|
72
|
-
}, [onStatus]);
|
|
91
|
+
}, [onStatus, resetState]);
|
|
73
92
|
const vibrate = (0, react_1.useCallback)((duration = 800, strength = 1) => {
|
|
74
93
|
vibrationNonce.current += 1;
|
|
75
94
|
const safeStrength = Math.max(0, Math.min(strength, 1));
|
|
@@ -86,20 +105,15 @@ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, on
|
|
|
86
105
|
setVibrationRequest({ type: "stop", nonce: vibrationNonce.current });
|
|
87
106
|
}, []);
|
|
88
107
|
(0, react_1.useEffect)(() => {
|
|
89
|
-
if (!enabled && pressedRef.current.size) {
|
|
90
|
-
pressedRef.current = new Set();
|
|
91
|
-
setPressedButtons(new Set());
|
|
92
|
-
}
|
|
93
108
|
if (!enabled) {
|
|
94
|
-
|
|
95
|
-
setButtonValues({});
|
|
109
|
+
resetState();
|
|
96
110
|
setInfo((prev) => ({
|
|
97
111
|
...prev,
|
|
98
112
|
connected: false,
|
|
99
113
|
}));
|
|
100
114
|
}
|
|
101
|
-
}, [enabled]);
|
|
102
|
-
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, vibrationRequest: vibrationRequest !== null && vibrationRequest !== void 0 ? vibrationRequest : undefined })), [
|
|
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 })), [
|
|
103
117
|
enabled,
|
|
104
118
|
axisThreshold,
|
|
105
119
|
handleAxis,
|
|
@@ -107,6 +121,7 @@ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, on
|
|
|
107
121
|
handleDpad,
|
|
108
122
|
handleInfo,
|
|
109
123
|
handleStatus,
|
|
124
|
+
handleState,
|
|
110
125
|
vibrationRequest,
|
|
111
126
|
]);
|
|
112
127
|
const isPressed = (0, react_1.useCallback)((key) => pressedRef.current.has(key), []);
|