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]
}