react-native-earl-gamepad 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # react-native-earl-gamepad
2
+
3
+ Lightweight, WebView-based Gamepad bridge for React Native that surfaces all common buttons, sticks, d-pad, and connect/disconnect events.
4
+
5
+ ## Features
6
+
7
+ - Hidden WebView polling `navigator.getGamepads()` (index 0) and emitting button/axis/status events.
8
+ - `GamepadBridge` component, `useGamepad` hook, and `GamepadDebug` visual tester.
9
+ - Deadzone handling for sticks (default 0.15) and auto-clear on disconnect.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install react-native-earl-gamepad react-native-webview
15
+ # or
16
+ yarn add react-native-earl-gamepad react-native-webview
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```tsx
22
+ import { GamepadBridge } from 'react-native-earl-gamepad';
23
+
24
+ export function Controls() {
25
+ return (
26
+ <GamepadBridge
27
+ enabled
28
+ onButton={(e) => console.log('button', e.button, e.pressed, e.value)}
29
+ onAxis={(e) => console.log('axis', e.axis, e.value)}
30
+ onDpad={(e) => console.log('dpad', e.key, e.pressed)}
31
+ onStatus={(s) => console.log('status', s.state)}
32
+ />
33
+ );
34
+ }
35
+ ```
36
+
37
+ ### Using the hook
38
+
39
+ ```tsx
40
+ import { useGamepad } from 'react-native-earl-gamepad';
41
+
42
+ export function HUD() {
43
+ const { pressedButtons, axes, isPressed, bridge } = useGamepad({ enabled: true });
44
+
45
+ return (
46
+ <>
47
+ {bridge}
48
+ <Text>Pressed: {Array.from(pressedButtons).join(', ') || 'none'}</Text>
49
+ <Text>
50
+ Left stick: x {axes.leftX?.toFixed(2)} / y {axes.leftY?.toFixed(2)}
51
+ </Text>
52
+ <Text>A held? {isPressed('a') ? 'yes' : 'no'}</Text>
53
+ </>
54
+ );
55
+ }
56
+ ```
57
+
58
+ ### Visual debugger
59
+
60
+ ```tsx
61
+ import { GamepadDebug } from 'react-native-earl-gamepad';
62
+
63
+ export function DebugScreen() {
64
+ return <GamepadDebug />;
65
+ }
66
+ ```
67
+
68
+ `GamepadDebug` renders a controller diagram that lights up buttons, shows stick offsets, and lists pressed/axes values.
69
+
70
+ ## API
71
+
72
+ ### `GamepadBridge` props
73
+
74
+ - `enabled?: boolean` — mount/unmount the hidden WebView. Default `true`.
75
+ - `axisThreshold?: number` — deadzone for axes. Default `0.15`.
76
+ - `onButton?: (event)` — `{ type:'button', button, index, pressed, value }`.
77
+ - `onAxis?: (event)` — `{ type:'axis', axis, index, value }`.
78
+ - `onDpad?: (event)` — `{ type:'dpad', key, pressed }` convenience mapped from buttons 12–15.
79
+ - `onStatus?: (event)` — `{ type:'status', state:'connected'|'disconnected' }`.
80
+ - `style?: StyleProp<ViewStyle>` — optional override; default is a 1×1 transparent view.
81
+
82
+ ### `useGamepad` return
83
+
84
+ - `pressedButtons: Set<GamepadButtonName>` — current pressed buttons (named or `button-N`).
85
+ - `axes: Partial<Record<StickAxisName, number>>` — stick/axis values with deadzone applied.
86
+ - `isPressed(key)` — helper to query a button.
87
+ - `bridge: JSX.Element | null` — render once in your tree to enable polling.
88
+
89
+ ### `GamepadDebug`
90
+
91
+ Drop-in component to visualize inputs. Accepts the same `enabled` and `axisThreshold` props as the hook/bridge.
92
+
93
+ ### Types
94
+
95
+ - `GamepadButtonName`: `a | b | x | y | lb | rb | lt | rt | back | start | ls | rs | dpadUp | dpadDown | dpadLeft | dpadRight | home | button-N`
96
+ - `StickAxisName`: `leftX | leftY | rightX | rightY | axis-N`
97
+
98
+ ## Notes
99
+
100
+ - Reads only the first connected pad (`navigator.getGamepads()[0]`).
101
+ - D-pad events are emitted from buttons 12–15; sticks pass through as axes with deadzone applied.
102
+ - On disconnect, all pressed states are cleared and release events are emitted.
103
+ - The WebView must stay mounted; avoid remounting each render to prevent losing state.
104
+
105
+ ## Development
106
+
107
+ ```sh
108
+ npm install
109
+ npm run build
110
+ ```
111
+
112
+ Outputs to `dist/` with type declarations.
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,13 @@
1
+ import { StyleProp, ViewStyle } from 'react-native';
2
+ import type { AxisEvent, ButtonEvent, DpadEvent, StatusEvent } from './types';
3
+ type Props = {
4
+ enabled?: boolean;
5
+ axisThreshold?: number;
6
+ onDpad?: (event: DpadEvent) => void;
7
+ onButton?: (event: ButtonEvent) => void;
8
+ onAxis?: (event: AxisEvent) => void;
9
+ onStatus?: (event: StatusEvent) => void;
10
+ style?: StyleProp<ViewStyle>;
11
+ };
12
+ export default function GamepadBridge({ enabled, axisThreshold, onDpad, onButton, onAxis, onStatus, style, }: Props): import("react/jsx-runtime").JSX.Element | null;
13
+ export {};
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = GamepadBridge;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const react_native_webview_1 = __importDefault(require("react-native-webview"));
10
+ // Minimal HTML bridge that polls navigator.getGamepads and posts button/axis changes.
11
+ const buildBridgeHtml = (axisThreshold) => `
12
+ <!doctype html>
13
+ <html><head><meta name="viewport" content="width=device-width,initial-scale=1" /></head>
14
+ <body style="margin:0;padding:0;background:#000">
15
+ <script>
16
+ (function(){
17
+ const deadzone = ${axisThreshold.toFixed(2)};
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'];
20
+ const axisNames = ['leftX','leftY','rightX','rightY'];
21
+ let prevButtons = [];
22
+ let prevAxes = [];
23
+
24
+ function poll(){
25
+ const pads = (navigator.getGamepads && navigator.getGamepads()) || [];
26
+ const gp = pads[0];
27
+ if (gp) {
28
+ // Buttons
29
+ gp.buttons?.forEach((btn, index) => {
30
+ const name = buttonNames[index] || ('button-' + index);
31
+ const prevBtn = prevButtons[index] || { pressed:false, value:0 };
32
+ const changed = prevBtn.pressed !== btn.pressed || Math.abs(prevBtn.value - btn.value) > 0.01;
33
+ if (changed) {
34
+ send({ type:'button', button:name, index, pressed: !!btn.pressed, value: btn.value ?? 0 });
35
+ // Also surface d-pad convenience events for the standard indices
36
+ if (index === 12) send({ type:'dpad', key:'up', pressed: !!btn.pressed });
37
+ if (index === 13) send({ type:'dpad', key:'down', pressed: !!btn.pressed });
38
+ if (index === 14) send({ type:'dpad', key:'left', pressed: !!btn.pressed });
39
+ if (index === 15) send({ type:'dpad', key:'right', pressed: !!btn.pressed });
40
+ }
41
+ prevButtons[index] = { pressed: !!btn.pressed, value: btn.value ?? 0 };
42
+ });
43
+
44
+ // Axes
45
+ gp.axes?.forEach((raw, index) => {
46
+ const name = axisNames[index] || ('axis-' + index);
47
+ const value = Math.abs(raw) < deadzone ? 0 : raw;
48
+ const prevVal = prevAxes[index] ?? 0;
49
+ if (Math.abs(prevVal - value) > 0.01) {
50
+ send({ type:'axis', axis:name, index, value });
51
+ }
52
+ prevAxes[index] = value;
53
+ });
54
+ } else {
55
+ if (prevButtons.length) {
56
+ prevButtons.forEach((btn, index) => {
57
+ if (btn?.pressed) {
58
+ const name = buttonNames[index] || ('button-' + index);
59
+ send({ type:'button', button:name, index, pressed:false, value:0 });
60
+ if (index === 12) send({ type:'dpad', key:'up', pressed:false });
61
+ if (index === 13) send({ type:'dpad', key:'down', pressed:false });
62
+ if (index === 14) send({ type:'dpad', key:'left', pressed:false });
63
+ if (index === 15) send({ type:'dpad', key:'right', pressed:false });
64
+ }
65
+ });
66
+ prevButtons = [];
67
+ }
68
+ if (prevAxes.length) {
69
+ prevAxes.forEach((prevVal, index) => {
70
+ if (prevVal !== 0) {
71
+ const name = axisNames[index] || ('axis-' + index);
72
+ send({ type:'axis', axis:name, index, value:0 });
73
+ }
74
+ });
75
+ prevAxes = [];
76
+ }
77
+ }
78
+ requestAnimationFrame(poll);
79
+ }
80
+
81
+ window.addEventListener('gamepadconnected', () => send({ type:'status', state:'connected' }));
82
+ window.addEventListener('gamepaddisconnected', () => {
83
+ // Clear previously pressed states so consumers do not get stuck buttons.
84
+ prevButtons.forEach((btn, index) => {
85
+ if (btn?.pressed) {
86
+ const name = buttonNames[index] || ('button-' + index);
87
+ send({ type:'button', button:name, index, pressed:false, value:0 });
88
+ if (index === 12) send({ type:'dpad', key:'up', pressed:false });
89
+ if (index === 13) send({ type:'dpad', key:'down', pressed:false });
90
+ if (index === 14) send({ type:'dpad', key:'left', pressed:false });
91
+ if (index === 15) send({ type:'dpad', key:'right', pressed:false });
92
+ }
93
+ });
94
+ prevButtons = [];
95
+ prevAxes = [];
96
+ send({ type:'status', state:'disconnected' });
97
+ });
98
+ poll();
99
+ })();
100
+ </script>
101
+ </body></html>`;
102
+ function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton, onAxis, onStatus, style, }) {
103
+ const html = (0, react_1.useMemo)(() => buildBridgeHtml(axisThreshold), [axisThreshold]);
104
+ if (!enabled)
105
+ return null;
106
+ const handleMessage = (event) => {
107
+ try {
108
+ const data = JSON.parse(event.nativeEvent.data);
109
+ if (data.type === 'dpad') {
110
+ onDpad === null || onDpad === void 0 ? void 0 : onDpad(data);
111
+ }
112
+ else if (data.type === 'button') {
113
+ onButton === null || onButton === void 0 ? void 0 : onButton(data);
114
+ }
115
+ else if (data.type === 'axis') {
116
+ onAxis === null || onAxis === void 0 ? void 0 : onAxis(data);
117
+ }
118
+ else if (data.type === 'status') {
119
+ onStatus === null || onStatus === void 0 ? void 0 : onStatus(data);
120
+ }
121
+ }
122
+ catch {
123
+ // ignore malformed messages
124
+ }
125
+ };
126
+ return ((0, jsx_runtime_1.jsx)(react_native_webview_1.default, { source: { html }, originWhitelist: ['*'], onMessage: handleMessage, javaScriptEnabled: true, style: style !== null && style !== void 0 ? style : { width: 1, height: 1, position: 'absolute', opacity: 0 } }));
127
+ }
@@ -0,0 +1,6 @@
1
+ type Props = {
2
+ enabled?: boolean;
3
+ axisThreshold?: number;
4
+ };
5
+ export default function GamepadDebug({ enabled, axisThreshold }: Props): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = GamepadDebug;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const react_native_1 = require("react-native");
10
+ const useGamepad_1 = __importDefault(require("./useGamepad"));
11
+ const trackedAxes = ['leftX', 'leftY', 'rightX', 'rightY'];
12
+ const format = (value) => (value !== null && value !== void 0 ? value : 0).toFixed(2);
13
+ function GamepadDebug({ enabled = true, axisThreshold = 0.15 }) {
14
+ const { bridge, pressedButtons, axes } = (0, useGamepad_1.default)({ enabled, axisThreshold });
15
+ const pressedList = (0, react_1.useMemo)(() => Array.from(pressedButtons).sort(), [pressedButtons]);
16
+ const pressed = (key) => pressedButtons.has(key);
17
+ const axis = (key) => { var _a; return (_a = axes[key]) !== null && _a !== void 0 ? _a : 0; };
18
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.container, children: [bridge, (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.title, children: "Gamepad Debug" }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.controller, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.shoulders, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.bumper, pressed('lb') && styles.active], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: "LB" }) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.bumper, pressed('rb') && styles.active], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: "RB" }) })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.triggers, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.trigger, pressed('lt') && styles.active], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: "LT" }) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.trigger, pressed('rt') && styles.active], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: "RT" }) })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.midRow, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.stickZone, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.stickRing, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
19
+ styles.stick,
20
+ {
21
+ transform: [
22
+ { translateX: axis('leftX') * 20 },
23
+ { translateY: axis('leftY') * -20 },
24
+ ],
25
+ },
26
+ pressed('ls') && styles.active,
27
+ ] }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.smallLabel, children: "LS" })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.centerCluster, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.smallKey, pressed('back') && styles.active], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: "Back" }) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.smallKey, pressed('start') && styles.active], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: "Start" }) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.smallKey, pressed('home') && styles.active], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: "Home" }) })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.faceCluster, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.faceRow, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.faceButton, pressed('y') && styles.faceActive], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.faceText, children: "Y" }) }) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.faceRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.faceButton, pressed('x') && styles.faceActive], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.faceText, children: "X" }) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.faceButton, pressed('b') && styles.faceActive], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.faceText, children: "B" }) })] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.faceRow, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.faceButton, pressed('a') && styles.faceActive], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.faceText, children: "A" }) }) })] })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.bottomRow, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.dpad, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
28
+ styles.dpadKey,
29
+ styles.dpadVertical,
30
+ styles.dpadUp,
31
+ pressed('dpadUp') && styles.active,
32
+ ] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
33
+ styles.dpadKey,
34
+ styles.dpadVertical,
35
+ styles.dpadDown,
36
+ pressed('dpadDown') && styles.active,
37
+ ] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
38
+ styles.dpadKey,
39
+ styles.dpadHorizontal,
40
+ styles.dpadLeft,
41
+ pressed('dpadLeft') && styles.active,
42
+ ] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
43
+ styles.dpadKey,
44
+ styles.dpadHorizontal,
45
+ styles.dpadRight,
46
+ pressed('dpadRight') && styles.active,
47
+ ] })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.stickZone, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.stickRing, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
48
+ styles.stick,
49
+ {
50
+ transform: [
51
+ { translateX: axis('rightX') * 20 },
52
+ { translateY: axis('rightY') * -20 },
53
+ ],
54
+ },
55
+ pressed('rs') && styles.active,
56
+ ] }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.smallLabel, children: "RS" })] })] })] }), (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.subtitle, children: ["Pressed (", pressedList.length, ")"] }), (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.subtitle, children: "Axes" }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.axesGrid, children: trackedAxes.map((axis) => ((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: axis }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.axisValue, children: format(axes[axis]) })] }, axis))) })] }));
57
+ }
58
+ const styles = react_native_1.StyleSheet.create({
59
+ container: {
60
+ padding: 12,
61
+ gap: 12,
62
+ backgroundColor: '#0b1220',
63
+ borderRadius: 12,
64
+ borderWidth: 1,
65
+ borderColor: '#1f2a3d',
66
+ },
67
+ title: {
68
+ fontSize: 18,
69
+ fontWeight: '700',
70
+ color: '#e5e7eb',
71
+ },
72
+ subtitle: {
73
+ marginTop: 4,
74
+ fontSize: 14,
75
+ fontWeight: '600',
76
+ color: '#cbd5e1',
77
+ },
78
+ muted: {
79
+ color: '#9ca3af',
80
+ fontSize: 13,
81
+ },
82
+ badgeRow: {
83
+ flexDirection: 'row',
84
+ flexWrap: 'wrap',
85
+ gap: 6,
86
+ },
87
+ badge: {
88
+ paddingVertical: 4,
89
+ paddingHorizontal: 8,
90
+ borderRadius: 12,
91
+ backgroundColor: '#1f2937',
92
+ borderWidth: 1,
93
+ borderColor: '#374151',
94
+ },
95
+ badgeText: {
96
+ color: '#e5e7eb',
97
+ fontSize: 12,
98
+ fontWeight: '600',
99
+ },
100
+ axisItem: {
101
+ padding: 8,
102
+ borderRadius: 8,
103
+ backgroundColor: '#111827',
104
+ borderWidth: 1,
105
+ borderColor: '#1f2937',
106
+ minWidth: 90,
107
+ },
108
+ axisLabel: {
109
+ color: '#cbd5e1',
110
+ fontSize: 12,
111
+ fontWeight: '600',
112
+ },
113
+ axisValue: {
114
+ color: '#f8fafc',
115
+ fontSize: 16,
116
+ fontVariant: ['tabular-nums'],
117
+ },
118
+ controller: {
119
+ backgroundColor: '#0f172a',
120
+ borderRadius: 14,
121
+ borderWidth: 1,
122
+ borderColor: '#1f2a3d',
123
+ padding: 12,
124
+ gap: 12,
125
+ },
126
+ shoulders: {
127
+ flexDirection: 'row',
128
+ justifyContent: 'space-between',
129
+ },
130
+ triggers: {
131
+ flexDirection: 'row',
132
+ justifyContent: 'space-between',
133
+ gap: 8,
134
+ },
135
+ bumper: {
136
+ flex: 1,
137
+ paddingVertical: 8,
138
+ borderRadius: 10,
139
+ backgroundColor: '#111827',
140
+ borderWidth: 1,
141
+ borderColor: '#1f2937',
142
+ alignItems: 'center',
143
+ },
144
+ trigger: {
145
+ flex: 1,
146
+ paddingVertical: 8,
147
+ borderRadius: 10,
148
+ backgroundColor: '#111827',
149
+ borderWidth: 1,
150
+ borderColor: '#1f2937',
151
+ alignItems: 'center',
152
+ },
153
+ label: {
154
+ color: '#d1d5db',
155
+ fontSize: 12,
156
+ fontWeight: '700',
157
+ },
158
+ smallLabel: {
159
+ color: '#9ca3af',
160
+ fontSize: 11,
161
+ marginTop: 4,
162
+ textAlign: 'center',
163
+ },
164
+ midRow: {
165
+ flexDirection: 'row',
166
+ alignItems: 'center',
167
+ justifyContent: 'space-between',
168
+ gap: 12,
169
+ },
170
+ centerCluster: {
171
+ alignItems: 'center',
172
+ gap: 6,
173
+ },
174
+ smallKey: {
175
+ paddingVertical: 6,
176
+ paddingHorizontal: 10,
177
+ borderRadius: 8,
178
+ backgroundColor: '#111827',
179
+ borderWidth: 1,
180
+ borderColor: '#1f2937',
181
+ },
182
+ faceCluster: {
183
+ alignItems: 'center',
184
+ gap: 4,
185
+ },
186
+ faceRow: {
187
+ flexDirection: 'row',
188
+ gap: 8,
189
+ justifyContent: 'center',
190
+ },
191
+ faceButton: {
192
+ width: 42,
193
+ height: 42,
194
+ borderRadius: 21,
195
+ backgroundColor: '#162032',
196
+ borderWidth: 1,
197
+ borderColor: '#1f2a3d',
198
+ alignItems: 'center',
199
+ justifyContent: 'center',
200
+ },
201
+ faceText: {
202
+ color: '#e5e7eb',
203
+ fontWeight: '800',
204
+ },
205
+ faceActive: {
206
+ backgroundColor: '#0ea5e9',
207
+ borderColor: '#38bdf8',
208
+ shadowColor: '#38bdf8',
209
+ shadowOpacity: 0.6,
210
+ shadowRadius: 6,
211
+ },
212
+ stickZone: {
213
+ width: 96,
214
+ alignItems: 'center',
215
+ gap: 6,
216
+ },
217
+ stickRing: {
218
+ width: 84,
219
+ height: 84,
220
+ borderRadius: 42,
221
+ borderWidth: 1,
222
+ borderColor: '#1f2937',
223
+ backgroundColor: '#0c1426',
224
+ alignItems: 'center',
225
+ justifyContent: 'center',
226
+ },
227
+ stick: {
228
+ width: 38,
229
+ height: 38,
230
+ borderRadius: 19,
231
+ backgroundColor: '#1f2937',
232
+ borderWidth: 2,
233
+ borderColor: '#111827',
234
+ },
235
+ bottomRow: {
236
+ flexDirection: 'row',
237
+ justifyContent: 'space-between',
238
+ alignItems: 'center',
239
+ gap: 12,
240
+ },
241
+ dpad: {
242
+ width: 120,
243
+ height: 120,
244
+ alignItems: 'center',
245
+ justifyContent: 'center',
246
+ position: 'relative',
247
+ },
248
+ dpadKey: {
249
+ position: 'absolute',
250
+ backgroundColor: '#111827',
251
+ borderColor: '#1f2937',
252
+ borderWidth: 1,
253
+ borderRadius: 6,
254
+ },
255
+ dpadVertical: {
256
+ width: 28,
257
+ height: 46,
258
+ },
259
+ dpadHorizontal: {
260
+ width: 46,
261
+ height: 28,
262
+ },
263
+ active: {
264
+ borderColor: '#22d3ee',
265
+ backgroundColor: '#0ea5e9',
266
+ shadowColor: '#22d3ee',
267
+ shadowOpacity: 0.5,
268
+ shadowRadius: 4,
269
+ },
270
+ dpadUp: { top: 6, left: 46 },
271
+ dpadDown: { bottom: 6, left: 46 },
272
+ dpadLeft: { left: 6, top: 46 },
273
+ dpadRight: { right: 6, top: 46 },
274
+ axesGrid: {
275
+ flexDirection: 'row',
276
+ flexWrap: 'wrap',
277
+ gap: 8,
278
+ },
279
+ });
@@ -0,0 +1,4 @@
1
+ export { default as GamepadBridge } from './GamepadBridge';
2
+ export { default as GamepadDebug } from './GamepadDebug';
3
+ export { default as useGamepad } from './useGamepad';
4
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.useGamepad = exports.GamepadDebug = exports.GamepadBridge = void 0;
21
+ var GamepadBridge_1 = require("./GamepadBridge");
22
+ Object.defineProperty(exports, "GamepadBridge", { enumerable: true, get: function () { return __importDefault(GamepadBridge_1).default; } });
23
+ var GamepadDebug_1 = require("./GamepadDebug");
24
+ Object.defineProperty(exports, "GamepadDebug", { enumerable: true, get: function () { return __importDefault(GamepadDebug_1).default; } });
25
+ var useGamepad_1 = require("./useGamepad");
26
+ Object.defineProperty(exports, "useGamepad", { enumerable: true, get: function () { return __importDefault(useGamepad_1).default; } });
27
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,26 @@
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}`;
3
+ export type StickAxisName = 'leftX' | 'leftY' | 'rightX' | 'rightY' | `axis-${number}`;
4
+ export type DpadEvent = {
5
+ type: 'dpad';
6
+ key: DpadKey;
7
+ pressed: boolean;
8
+ };
9
+ export type ButtonEvent = {
10
+ type: 'button';
11
+ button: GamepadButtonName;
12
+ index: number;
13
+ pressed: boolean;
14
+ value: number;
15
+ };
16
+ export type AxisEvent = {
17
+ type: 'axis';
18
+ axis: StickAxisName;
19
+ index: number;
20
+ value: number;
21
+ };
22
+ export type StatusEvent = {
23
+ type: 'status';
24
+ state: 'connected' | 'disconnected';
25
+ };
26
+ export type GamepadMessage = DpadEvent | ButtonEvent | AxisEvent | StatusEvent;
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,18 @@
1
+ import { type JSX } from 'react';
2
+ import type { AxisEvent, ButtonEvent, DpadEvent, GamepadButtonName, StickAxisName, StatusEvent } from './types';
3
+ type Options = {
4
+ enabled?: boolean;
5
+ axisThreshold?: number;
6
+ onButton?: (event: ButtonEvent) => void;
7
+ onAxis?: (event: AxisEvent) => void;
8
+ onDpad?: (event: DpadEvent) => void;
9
+ onStatus?: (event: StatusEvent) => void;
10
+ };
11
+ type Return = {
12
+ pressedButtons: Set<GamepadButtonName>;
13
+ axes: Partial<Record<StickAxisName, number>>;
14
+ isPressed: (key: GamepadButtonName) => boolean;
15
+ bridge: JSX.Element | null;
16
+ };
17
+ export default function useGamepad({ enabled, axisThreshold, onButton, onAxis, onDpad, onStatus, }?: Options): Return;
18
+ export {};
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = useGamepad;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const GamepadBridge_1 = __importDefault(require("./GamepadBridge"));
10
+ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, onDpad, onStatus, } = {}) {
11
+ const [pressedButtons, setPressedButtons] = (0, react_1.useState)(new Set());
12
+ const pressedRef = (0, react_1.useRef)(new Set());
13
+ const [axes, setAxes] = (0, react_1.useState)({
14
+ leftX: 0,
15
+ leftY: 0,
16
+ rightX: 0,
17
+ rightY: 0,
18
+ });
19
+ (0, react_1.useEffect)(() => {
20
+ pressedRef.current = pressedButtons;
21
+ }, [pressedButtons]);
22
+ const handleButton = (0, react_1.useCallback)((event) => {
23
+ const next = new Set(pressedRef.current);
24
+ if (event.pressed) {
25
+ next.add(event.button);
26
+ }
27
+ else {
28
+ next.delete(event.button);
29
+ }
30
+ pressedRef.current = next;
31
+ setPressedButtons(next);
32
+ onButton === null || onButton === void 0 ? void 0 : onButton(event);
33
+ }, [onButton]);
34
+ const handleAxis = (0, react_1.useCallback)((event) => {
35
+ setAxes((prev) => ({ ...prev, [event.axis]: event.value }));
36
+ onAxis === null || onAxis === void 0 ? void 0 : onAxis(event);
37
+ }, [onAxis]);
38
+ const handleDpad = (0, react_1.useCallback)((event) => {
39
+ onDpad === null || onDpad === void 0 ? void 0 : onDpad(event);
40
+ }, [onDpad]);
41
+ (0, react_1.useEffect)(() => {
42
+ if (!enabled && pressedRef.current.size) {
43
+ pressedRef.current = new Set();
44
+ setPressedButtons(new Set());
45
+ }
46
+ if (!enabled) {
47
+ setAxes({ leftX: 0, leftY: 0, rightX: 0, rightY: 0 });
48
+ }
49
+ }, [enabled]);
50
+ 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: onStatus })), [enabled, axisThreshold, handleAxis, handleButton, handleDpad, onStatus]);
51
+ const isPressed = (0, react_1.useCallback)((key) => pressedRef.current.has(key), []);
52
+ return { pressedButtons, axes, isPressed, bridge };
53
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "react-native-earl-gamepad",
3
+ "version": "0.1.0",
4
+ "description": "React Native gamepad bridge via WebView (buttons, sticks, d-pad, status).",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc -p .",
13
+ "clean": "rimraf dist"
14
+ },
15
+ "peerDependencies": {
16
+ "react": ">=18",
17
+ "react-native": ">=0.72",
18
+ "react-native-webview": ">=13"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^19.0.0",
22
+ "react": "^19.0.0",
23
+ "react-native": "^0.81.5",
24
+ "react-native-webview": "^13.12.2",
25
+ "typescript": "^5.3.3"
26
+ },
27
+ "keywords": [
28
+ "react-native",
29
+ "gamepad",
30
+ "webview",
31
+ "controller"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/earlo/react-native-earl-gamepad.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/earlo/react-native-earl-gamepad/issues"
39
+ },
40
+ "homepage": "https://github.com/earlo/react-native-earl-gamepad#readme",
41
+ "author": "Ordovez, Earl Romeo",
42
+ "license": "MIT"
43
+ }