diff --git a/app/src/lib/stores/gamepad.ts b/app/src/lib/stores/gamepad.ts new file mode 100644 index 0000000..bc311d2 --- /dev/null +++ b/app/src/lib/stores/gamepad.ts @@ -0,0 +1,47 @@ +import { readable, derived } from 'svelte/store' + +export type GamepadState = { + available: boolean + gamepads: Gamepad[] +} + +export const gamepads = readable({ 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 + }) + raf = requestAnimationFrame(update) + } + + window.addEventListener('gamepadconnected', update) + window.addEventListener('gamepaddisconnected', update) + let raf = requestAnimationFrame(update) + + return () => { + cancelAnimationFrame(raf) + window.removeEventListener('gamepadconnected', update) + window.removeEventListener('gamepaddisconnected', update) + } +}) + +export const gamepad = derived(gamepads, $gamepads => + $gamepads.available && $gamepads.gamepads.length > 0 ? $gamepads.gamepads[0] : null +) + +export const gamepadAxes = derived(gamepad, $gamepad => $gamepad?.axes ?? [0, 0, 0, 0]) + +export const gamepadButtons = derived(gamepad, $gamepad => $gamepad?.buttons ?? []) + +export const hasGamepad = derived( + gamepads, + $gamepads => $gamepads.available && $gamepads.gamepads.length > 0 +) diff --git a/app/src/routes/controller/Controls.svelte b/app/src/routes/controller/Controls.svelte index 9bd110e..f71b96a 100644 --- a/app/src/routes/controller/Controls.svelte +++ b/app/src/routes/controller/Controls.svelte @@ -5,6 +5,8 @@ import { input, outControllerData, mode, modes, type Modes, ModesEnum } from '$lib/stores' import type { vector } from '$lib/types/models' import { VerticalSlider } from '$lib/components/input' + import { gamepadAxes, gamepadButtons, hasGamepad } from '$lib/stores/gamepad' + import { notifications } from '$lib/components/toasts/notifications' let throttle = new throttler() let left: nipplejs.JoystickManager @@ -13,6 +15,23 @@ let throttle_timing = 40 let data = new Array(8) + $effect(() => { + if ($hasGamepad) { + notifications.success('🎮 Gamepad connected', 3000) + } + }) + + $effect(() => { + handleJoyMove('left', { x: $gamepadAxes[0], y: -$gamepadAxes[1] }) + handleJoyMove('right', { x: $gamepadAxes[2], y: $gamepadAxes[3] }) + }) + + // TODO React to button press + // $effect(() => { + // if ($gamepadButtons.length === 0) return + // + // }) + onMount(() => { left = nipplejs.create({ zone: document.getElementById('left') as HTMLElement, @@ -70,7 +89,7 @@ } const handleRange = (event: Event, key: 'speed' | 'height' | 's1') => { - const value: number = event.target?.value + const value: number = Number((event.target as HTMLInputElement).value) input.update(inputData => { inputData[key] = value @@ -127,7 +146,7 @@ type="range" name="s1" min="0" - max="100" + max="25" oninput={e => handleRange(e, 's1')} class="range range-sm range-primary" /> @@ -137,7 +156,7 @@ type="range" name="speed" min="0" - max="100" + max="25" oninput={e => handleRange(e, 'speed')} class="range range-sm range-primary" />