🎮 Maps controller buttons to modes

This commit is contained in:
Rune Harlyk
2025-10-14 19:41:40 +02:00
parent 76d965ff43
commit 0aab42f0e9
2 changed files with 90 additions and 30 deletions
+65 -25
View File
@@ -5,43 +5,83 @@ export type GamepadState = {
gamepads: Gamepad[]
}
const DEADZONE = 0.15
const dz = (x: number) => {
const a = Math.abs(x)
if (a < DEADZONE) return 0
return ((a - DEADZONE) / (1 - DEADZONE)) * Math.sign(x)
}
let raf = 0
let running = false
export const gamepads = readable<GamepadState>({ available: false, gamepads: [] }, set => {
const update = () => {
const hasGamepadAPI = 'getGamepads' in navigator
if (!hasGamepadAPI) {
set({ available: false, gamepads: [] })
return
}
const gps = navigator.getGamepads?.() ?? []
const validGamepads = gps.filter(Boolean) as Gamepad[]
set({
available: true,
gamepads: validGamepads
})
const pads = navigator.getGamepads?.() ?? []
const list = Array.from(pads)
.map(p => p || null)
.filter(Boolean) as Gamepad[]
set({ available: 'getGamepads' in navigator, gamepads: list })
raf = requestAnimationFrame(update)
}
window.addEventListener('gamepadconnected', update)
window.addEventListener('gamepaddisconnected', update)
let raf = requestAnimationFrame(update)
const onConnect = () => update()
const onDisconnect = () => update()
const onVis = () => {
if (document.hidden) {
running = false
cancelAnimationFrame(raf)
} else if (!running) {
running = true
raf = requestAnimationFrame(update)
}
}
window.addEventListener('gamepadconnected', onConnect)
window.addEventListener('gamepaddisconnected', onDisconnect)
document.addEventListener('visibilitychange', onVis)
running = true
raf = requestAnimationFrame(update)
return () => {
running = false
cancelAnimationFrame(raf)
window.removeEventListener('gamepadconnected', update)
window.removeEventListener('gamepaddisconnected', update)
window.removeEventListener('gamepadconnected', onConnect)
window.removeEventListener('gamepaddisconnected', onDisconnect)
document.removeEventListener('visibilitychange', onVis)
}
})
export const gamepad = derived(gamepads, $gamepads =>
$gamepads.available && $gamepads.gamepads.length > 0 ? $gamepads.gamepads[0] : null
export const gamepad = derived(gamepads, s =>
s.available && s.gamepads.length ? s.gamepads[0] : null
)
export const gamepadAxes = derived(gamepad, $gamepad => $gamepad?.axes ?? [0, 0, 0, 0])
export const hasGamepad = derived(gamepads, s => s.available && s.gamepads.length > 0)
export const gamepadButtons = derived(gamepad, $gamepad => $gamepad?.buttons ?? [])
export const gamepadAxes = derived(gamepad, g => (g ? g.axes.map(dz) : [0, 0, 0, 0]))
export const hasGamepad = derived(
gamepads,
$gamepads => $gamepads.available && $gamepads.gamepads.length > 0
)
type ButtonEdge = { pressed: boolean; value: number; justPressed: boolean; justReleased: boolean }
const prev = new Map<number, { pressed: boolean; value: number }[]>()
export const gamepadButtons = derived(gamepad, g => g?.buttons ?? [])
export const gamepadButtonsEdges = derived(gamepad, g => {
if (!g) return [] as ButtonEdge[]
const p = prev.get(g.index) || []
const out = g.buttons.map((b, i): ButtonEdge => {
const pr = p[i] || { pressed: false, value: 0 }
const pressed = !!b.pressed || b.value > 0.5
return {
pressed,
value: b.value,
justPressed: pressed && !pr.pressed,
justReleased: !pressed && pr.pressed
}
})
prev.set(
g.index,
out.map(x => ({ pressed: x.pressed, value: x.value }))
)
return out
})
+25 -5
View File
@@ -15,7 +15,12 @@
} from '$lib/stores'
import type { vector } from '$lib/types/models'
import { VerticalSlider } from '$lib/components/input'
import { gamepadAxes, hasGamepad } from '$lib/stores/gamepad'
import {
gamepadAxes,
gamepadButtons,
gamepadButtonsEdges,
hasGamepad
} from '$lib/stores/gamepad'
import { notifications } from '$lib/components/toasts/notifications'
let throttle = new throttler()
@@ -37,10 +42,25 @@
})
// TODO React to button press
// $effect(() => {
// if ($gamepadButtons.length === 0) return
//
// })
$effect(() => {
if (!$hasGamepad) return
const b = $gamepadButtonsEdges
if (!b.length) return
if (b[0]?.justPressed) mode.set(5)
if (b[1]?.justPressed) mode.set(4)
if (b[2]?.justPressed) mode.set(3)
if (b[3]?.justPressed) mode.set(0)
if (b[12]?.justPressed)
input.update(inputData => {
inputData['height'] = Math.min(inputData.height + 0.1, 1)
return inputData
})
if (b[13]?.justPressed)
input.update(inputData => {
inputData['height'] = Math.min(inputData.height - 0.1, 1)
return inputData
})
})
onMount(() => {
left = nipplejs.create({