From d60c941c264eab4bf6e350d34a9f94854e0b9d50 Mon Sep 17 00:00:00 2001 From: oyar Date: Sat, 29 Oct 2022 16:32:39 +0100 Subject: [PATCH] feat: add types to keyboard controls (#1117) * feat: add types to keyboard controls * docs: keyboard controls update --- .../stories/KeyboardControls.stories.tsx | 72 +++++++++++++++++++ README.md | 41 ++++++----- src/web/KeyboardControls.tsx | 27 ++++--- 3 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 .storybook/stories/KeyboardControls.stories.tsx diff --git a/.storybook/stories/KeyboardControls.stories.tsx b/.storybook/stories/KeyboardControls.stories.tsx new file mode 100644 index 000000000..266588a51 --- /dev/null +++ b/.storybook/stories/KeyboardControls.stories.tsx @@ -0,0 +1,72 @@ +import { useFrame } from '@react-three/fiber' +import * as React from 'react' +import { useMemo, useRef } from 'react' +import { MathUtils, Mesh, Vector3 } from 'three' +import { Cone, KeyboardControls, KeyboardControlsEntry, useKeyboardControls } from '../../src' +import { Setup } from '../Setup' + +export default { + title: 'Controls/KeyboardControls', + decorators: [ + (storyFn) => ( + + {storyFn()} + + ), + ], +} + +enum Controls { + forward = 'forward', + left = 'left', + right = 'right', + back = 'back', +} + +export const KeyboardControlsSt = () => { + const map = useMemo( + () => [ + { name: Controls.forward, keys: ['ArrowUp', 'w', 'W'] }, + { name: Controls.back, keys: ['ArrowDown', 's', 'S'] }, + { name: Controls.left, keys: ['ArrowLeft', 'a', 'A'] }, + { name: Controls.right, keys: ['ArrowRight', 'd', 'D'] }, + ], + [] + ) + + return ( + + + + ) +} + +const _velocity = new Vector3() +const speed = 10 + +const Player = () => { + const ref = useRef(null) + const [, get] = useKeyboardControls() + + useFrame((_s, dl) => { + if (!ref.current) return + const state = get() + if (state.left && !state.right) _velocity.x = -1 + if (state.right && !state.left) _velocity.x = 1 + if (!state.left && !state.right) _velocity.x = 0 + + if (state.forward && !state.back) _velocity.z = -1 + if (state.back && !state.forward) _velocity.z = 1 + if (!state.forward && !state.back) _velocity.z = 0 + + ref.current.position.addScaledVector(_velocity, speed * dl) + + ref.current.rotateY(4 * dl * _velocity.x) + }) + + return ( + + + + ) +} diff --git a/README.md b/README.md index 594d64ea8..d69de5d8d 100644 --- a/README.md +++ b/README.md @@ -436,11 +436,11 @@ Semi-OrbitControls with spring-physics, polar zoom and snap-back, for presentati A rudimentary keyboard controller which distributes your defined data-model to the `useKeyboard` hook. It's a rather simple way to get started with keyboard input. ```tsx -type KeyboardControlsState = { [key: string]: boolean } +type KeyboardControlsState = { [K in T]: boolean } -type KeyboardControlsEntry = { +type KeyboardControlsEntry = { /** Name of the action */ - name: string + name: T /** The keys that define it, you can use either event.key, or event.code */ keys: string[] /** If the event receives the keyup event, true by default */ @@ -461,33 +461,40 @@ type KeyboardControlsProps = { You start by wrapping your app, or scene, into ``. -```jsx +```tsx +enum Controls { + forward = 'forward', + back = 'back', + left = 'left', + right = 'right', + jump = 'jump', +} function App() { + const map = useMemo>(()=>[ + { name: Controls.forward, keys: ['ArrowUp', 'w', 'W'] }, + { name: Controls.back, keys: ['ArrowDown', 's', 'S'] }, + { name: Controls.left, keys: ['ArrowLeft', 'a', 'A'] }, + { name: Controls.right, keys: ['ArrowRight', 'd', 'D'] }, + { name: Controls.jump, keys: ['Space'] }, + ], []) return ( - + ``` You can either respond to input reactively, it uses zustand (with the `subscribeWithSelector` middleware) so all the rules apply: -```jsx +```tsx function Foo() { - const pressed = useKeyboardControls(state => forward) + const forwardPressed = useKeyboardControls(state => state.forward) ``` Or transiently, either by `subscribe`, which is a function which returns a function to unsubscribe, so you can pair it with useEffect for cleanup, or `get`, which fetches fresh state non-reactively. -```jsx +```tsx function Foo() { - const [sub, get] = useKeyboardControls() + const [sub, get] = useKeyboardControls() useEffect(() => { return sub( @@ -500,7 +507,7 @@ function Foo() { useFrame(() => { // Fetch fresh data from store - const pressed = get().backward + const pressed = get().back }) } ``` diff --git a/src/web/KeyboardControls.tsx b/src/web/KeyboardControls.tsx index 87bc0a5ed..b85c67d15 100644 --- a/src/web/KeyboardControls.tsx +++ b/src/web/KeyboardControls.tsx @@ -2,11 +2,11 @@ import * as React from 'react' import create, { GetState, StateSelector, Subscribe, UseBoundStore } from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' -type KeyboardControlsState = { [key: string]: boolean } +type KeyboardControlsState = { [K in T]: boolean } -type KeyboardControlsEntry = { +export type KeyboardControlsEntry = { /** Name of the action */ - name: string + name: T /** The keys that define it, you can use either event.key, or event.code */ keys: string[] /** If the event receives the keyup event, true by default */ @@ -24,10 +24,10 @@ type KeyboardControlsProps = { domElement?: HTMLElement } -type KeyboardControlsApi = [ - Subscribe, - GetState, - UseBoundStore +type KeyboardControlsApi = [ + Subscribe>, + GetState>, + UseBoundStore> ] const context = /*@__PURE__*/ React.createContext(null!) @@ -90,8 +90,17 @@ export function KeyboardControls({ map, children, onChange, domElement }: Keyboa return } -export function useKeyboardControls(sel?: StateSelector) { - const [sub, get, store] = React.useContext(context) +export function useKeyboardControls(): [ + Subscribe>, + GetState> +] +export function useKeyboardControls( + sel: StateSelector, U> +): U +export function useKeyboardControls( + sel?: StateSelector, U> +): U | [Subscribe>, GetState>] { + const [sub, get, store] = React.useContext>(context) if (sel) return store(sel) else return [sub, get] }