Add gamepad navigation
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
type GamepadAction = 'up' | 'down' | 'left' | 'right' | 'select' | 'back';
|
||||
|
||||
const GAMEPAD_AXIS_THRESHOLD = 0.55;
|
||||
const GAMEPAD_REPEAT_MS = 180;
|
||||
|
||||
const KEY_BY_ACTION: Record<GamepadAction, string> = {
|
||||
up: 'ArrowUp',
|
||||
down: 'ArrowDown',
|
||||
left: 'ArrowLeft',
|
||||
right: 'ArrowRight',
|
||||
select: 'Enter',
|
||||
back: 'Escape',
|
||||
};
|
||||
|
||||
const gamepadPressedState: Record<GamepadAction, boolean> = {
|
||||
up: false,
|
||||
down: false,
|
||||
left: false,
|
||||
right: false,
|
||||
select: false,
|
||||
back: false,
|
||||
};
|
||||
|
||||
const gamepadLastTriggerAt: Record<GamepadAction, number> = {
|
||||
up: 0,
|
||||
down: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
select: 0,
|
||||
back: 0,
|
||||
};
|
||||
|
||||
let gamepadFrameId: number | null = null;
|
||||
let gamepadInstalled = false;
|
||||
|
||||
function dispatchKey(key: string) {
|
||||
const active = document.activeElement;
|
||||
const target = active instanceof HTMLElement ? active : document;
|
||||
target.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
function applyGamepadAction(action: GamepadAction, isPressed: boolean, now: number) {
|
||||
const wasPressed = gamepadPressedState[action];
|
||||
gamepadPressedState[action] = isPressed;
|
||||
|
||||
if (!isPressed) return;
|
||||
|
||||
const shouldRepeat = action === 'up' || action === 'down' || action === 'left' || action === 'right';
|
||||
const canTrigger = !wasPressed || (shouldRepeat && now - gamepadLastTriggerAt[action] >= GAMEPAD_REPEAT_MS);
|
||||
if (!canTrigger) return;
|
||||
|
||||
gamepadLastTriggerAt[action] = now;
|
||||
dispatchKey(KEY_BY_ACTION[action]);
|
||||
}
|
||||
|
||||
function resetPressedState() {
|
||||
gamepadPressedState.up = false;
|
||||
gamepadPressedState.down = false;
|
||||
gamepadPressedState.left = false;
|
||||
gamepadPressedState.right = false;
|
||||
gamepadPressedState.select = false;
|
||||
gamepadPressedState.back = false;
|
||||
}
|
||||
|
||||
function pollGamepad() {
|
||||
const gamepads = navigator.getGamepads?.() ?? [];
|
||||
const now = performance.now();
|
||||
const connectedGamepads = gamepads.filter((gp): gp is Gamepad => Boolean(gp && gp.connected));
|
||||
|
||||
if (connectedGamepads.length > 0) {
|
||||
let up = false;
|
||||
let down = false;
|
||||
let left = false;
|
||||
let right = false;
|
||||
let select = false;
|
||||
let back = false;
|
||||
|
||||
for (const gamepad of connectedGamepads) {
|
||||
const axisX = gamepad.axes[0] ?? 0;
|
||||
const axisY = gamepad.axes[1] ?? 0;
|
||||
|
||||
up = up || Boolean(gamepad.buttons[12]?.pressed) || axisY <= -GAMEPAD_AXIS_THRESHOLD;
|
||||
down = down || Boolean(gamepad.buttons[13]?.pressed) || axisY >= GAMEPAD_AXIS_THRESHOLD;
|
||||
left = left || Boolean(gamepad.buttons[14]?.pressed) || axisX <= -GAMEPAD_AXIS_THRESHOLD;
|
||||
right = right || Boolean(gamepad.buttons[15]?.pressed) || axisX >= GAMEPAD_AXIS_THRESHOLD;
|
||||
|
||||
// Xbox mapping on standard gamepads: A=0, B=1
|
||||
select = select || Boolean(gamepad.buttons[0]?.pressed);
|
||||
back = back || Boolean(gamepad.buttons[1]?.pressed);
|
||||
}
|
||||
|
||||
applyGamepadAction('up', up, now);
|
||||
applyGamepadAction('down', down, now);
|
||||
applyGamepadAction('left', left, now);
|
||||
applyGamepadAction('right', right, now);
|
||||
applyGamepadAction('select', select, now);
|
||||
applyGamepadAction('back', back, now);
|
||||
} else {
|
||||
resetPressedState();
|
||||
}
|
||||
|
||||
gamepadFrameId = window.requestAnimationFrame(pollGamepad);
|
||||
}
|
||||
|
||||
export function installGamepadNavigation() {
|
||||
if (gamepadInstalled) return;
|
||||
gamepadInstalled = true;
|
||||
gamepadFrameId = window.requestAnimationFrame(pollGamepad);
|
||||
}
|
||||
|
||||
export function uninstallGamepadNavigation() {
|
||||
if (!gamepadInstalled) return;
|
||||
gamepadInstalled = false;
|
||||
if (gamepadFrameId !== null) {
|
||||
window.cancelAnimationFrame(gamepadFrameId);
|
||||
gamepadFrameId = null;
|
||||
}
|
||||
resetPressedState();
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/main.css'
|
||||
import { installKeyboardNavigation } from './composables/useKeyboardNavigation'
|
||||
import { installGamepadNavigation } from './composables/useGamepadNavigation'
|
||||
|
||||
// Install global keyboard navigation handlers immediately
|
||||
installKeyboardNavigation()
|
||||
installGamepadNavigation()
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
|
||||
Reference in New Issue
Block a user