refactor: add common controller interface
parent
212bda8d5e
commit
07e5d4ee4d
15
src/App.tsx
15
src/App.tsx
|
|
@ -1,7 +1,20 @@
|
|||
import { AppRoutes } from './app/AppRoutes'
|
||||
import { useKeyboardInput } from './input/useKeyboardInput'
|
||||
import { usePoseInput } from './input/usePoseInput'
|
||||
|
||||
function InputControllers() {
|
||||
useKeyboardInput()
|
||||
usePoseInput()
|
||||
return null
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <AppRoutes />
|
||||
return (
|
||||
<>
|
||||
<InputControllers />
|
||||
<AppRoutes />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { CSSProperties } from 'react'
|
||||
import { useGameInput } from '../../input/useGameInput'
|
||||
import { ARHud } from './ARHud'
|
||||
import { SONGS } from './song-data'
|
||||
import { SongCarousel } from './SongCarousel'
|
||||
|
|
@ -14,21 +15,21 @@ function wrapIndex(index: number, length: number): number {
|
|||
export function SongSelectScreen() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const activeSong = SONGS[currentIndex]
|
||||
const handleConfirm = () => {}
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
useGameInput((action) => {
|
||||
if (action === 'LEFT') {
|
||||
setCurrentIndex((prev) => wrapIndex(prev - 1, SONGS.length))
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
setCurrentIndex((prev) => wrapIndex(prev + 1, SONGS.length))
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
}, [])
|
||||
if (action === 'RIGHT') {
|
||||
setCurrentIndex((prev) => wrapIndex(prev + 1, SONGS.length))
|
||||
}
|
||||
|
||||
if (action === 'BUTTON_A') {
|
||||
handleConfirm()
|
||||
}
|
||||
})
|
||||
|
||||
const cssVars = useMemo(
|
||||
() =>
|
||||
|
|
@ -60,7 +61,7 @@ export function SongSelectScreen() {
|
|||
|
||||
<SongInfoBar
|
||||
song={activeSong}
|
||||
onConfirm={() => {}}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export type GameAction = 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' | 'BUTTON_A' | 'BUTTON_B'
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import type { GameAction } from './actions'
|
||||
|
||||
type GameActionListener = (action: GameAction) => void
|
||||
|
||||
const listeners = new Set<GameActionListener>()
|
||||
|
||||
export function emit(action: GameAction): void {
|
||||
listeners.forEach((listener) => listener(action))
|
||||
}
|
||||
|
||||
export function subscribe(listener: GameActionListener): () => void {
|
||||
listeners.add(listener)
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { useEffect } from 'react'
|
||||
import type { GameAction } from './actions'
|
||||
import { subscribe } from './inputBus'
|
||||
|
||||
export function useGameInput(onAction: (action: GameAction) => void): void {
|
||||
useEffect(() => subscribe(onAction), [onAction])
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { useEffect } from 'react'
|
||||
import type { GameAction } from './actions'
|
||||
import { emit } from './inputBus'
|
||||
|
||||
const KEY_TO_ACTION: Partial<Record<string, GameAction>> = {
|
||||
ArrowLeft: 'LEFT',
|
||||
ArrowRight: 'RIGHT',
|
||||
ArrowUp: 'UP',
|
||||
ArrowDown: 'DOWN',
|
||||
Enter: 'BUTTON_A',
|
||||
Escape: 'BUTTON_B',
|
||||
}
|
||||
|
||||
export function useKeyboardInput(): void {
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const action = KEY_TO_ACTION[event.key]
|
||||
if (!action) return
|
||||
emit(action)
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
}, [])
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { useEffect } from 'react'
|
||||
import { emit } from './inputBus'
|
||||
|
||||
export function usePoseInput(): void {
|
||||
useEffect(() => {
|
||||
const emitAction = (action: Parameters<typeof emit>[0]) => {
|
||||
emit(action)
|
||||
}
|
||||
|
||||
// MediaPipe integration point:
|
||||
// Convert detected pose/gesture events into GameAction values and call `emitAction(action)`.
|
||||
// Example mapping idea: swipe-left -> 'LEFT', swipe-right -> 'RIGHT',
|
||||
// hand-raise -> 'BUTTON_A', cancel gesture -> 'BUTTON_B'.
|
||||
// Keep all MediaPipe-specific setup/teardown in this hook so UI components remain input-agnostic.
|
||||
void emitAction
|
||||
|
||||
return () => {
|
||||
// Cleanup MediaPipe resources/listeners here when integration is added.
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
Loading…
Reference in New Issue