Skip to content

Commit d6e2607

Browse files
committed
feat: Add joystick to Vector3d
feat: Allow joystick to control a non XY plane feat: Add hook to capture keypress chore: Create component for Joystick3d fix: Don’t use precision hotkey to set plane feat: Add children to Joystick feat: Add buttons to JoystickPlayground style: Styled joystick plane indicator feat: Add button key labels feat: Add cube rotation feat: Update cube fix: Don’t always show all joysticks ;) chore: Refactoring out JoystickPlayground3d chore: Add changeset fix: Joystick buttons should not be a button (remove nested button warn) chore: Simplify indexing chore: Rename for clarity chore: Remove comments style: Remove hardcoded size throughout
1 parent 5e80cd1 commit d6e2607

File tree

13 files changed

+359
-43
lines changed

13 files changed

+359
-43
lines changed

.changeset/cold-toes-warn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'leva': minor
3+
---
4+
5+
Add a joystick to Vector3d

demo/src/sandboxes/leva-busy/src/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const ExtraControls = () => {
4343

4444
function Controls() {
4545
const data = useControls({
46+
vector2D: [10, 10],
47+
vector3D: [10, 10, 10],
4648
dimension: '4px',
4749
string: { value: 'something', optional: true, order: -2 },
4850
range: { value: 0, min: -10, max: 10, order: -3 },

packages/leva/src/components/Button/Button.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Row } from '../UI'
55
import { StyledButton } from './StyledButton'
66

77
type ButtonProps = {
8-
label: string
8+
label: string | JSX.Element
99
} & Omit<ButtonInput, 'type'>
1010

1111
export function Button({ onClick, settings, label }: ButtonProps) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react'
2+
import { StyledJoyCubeFace, StyledJoyCube } from './StyledJoystick3d'
3+
4+
export function JoyCube({
5+
isTop,
6+
isRight,
7+
showFront = true,
8+
showMid = true,
9+
showRear = false,
10+
}: {
11+
isTop?: boolean
12+
isRight?: boolean
13+
showFront?: boolean
14+
showMid?: boolean
15+
showRear?: boolean
16+
}) {
17+
return (
18+
<StyledJoyCube top={isTop} right={isRight}>
19+
{showFront && (
20+
<>
21+
<StyledJoyCubeFace className="joycube-face--front" />
22+
<StyledJoyCubeFace className="joycube-face--back" />
23+
<StyledJoyCubeFace className="joycube-face--right" />
24+
<StyledJoyCubeFace className="joycube-face--left" />
25+
<StyledJoyCubeFace className="joycube-face--top" />
26+
<StyledJoyCubeFace className="joycube-face--bottom" />
27+
</>
28+
)}
29+
{showMid && (
30+
<>
31+
<StyledJoyCubeFace className="joycube-face--front-mid" />
32+
<StyledJoyCubeFace className="joycube-face--back-mid" />
33+
<StyledJoyCubeFace className="joycube-face--right-mid" />
34+
<StyledJoyCubeFace className="joycube-face--left-mid" />
35+
<StyledJoyCubeFace className="joycube-face--top-mid" />
36+
<StyledJoyCubeFace className="joycube-face--bottom-mid" />
37+
</>
38+
)}
39+
{showRear && (
40+
<>
41+
<StyledJoyCubeFace className="joycube-face--front-rear" />
42+
<StyledJoyCubeFace className="joycube-face--back-rear" />
43+
<StyledJoyCubeFace className="joycube-face--right-rear" />
44+
<StyledJoyCubeFace className="joycube-face--left-rear" />
45+
<StyledJoyCubeFace className="joycube-face--top-rear" />
46+
<StyledJoyCubeFace className="joycube-face--bottom-rear" />
47+
</>
48+
)}
49+
</StyledJoyCube>
50+
)
51+
}

packages/leva/src/components/Vector2d/Joystick.tsx renamed to packages/leva/src/components/UI/Joystick.tsx

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import React, { useState, useRef, useCallback, useEffect, useLayoutEffect } from 'react'
22
import { useDrag } from '../../hooks'
33
import { clamp, multiplyStep } from '../../utils'
4-
import { JoystickTrigger, JoystickPlayground } from './StyledJoystick'
4+
import { JoystickTrigger, JoystickPlayground, JoystickGrid } from './StyledJoystick'
55
import { useTh } from '../../styles'
6-
import { Portal } from '../UI'
6+
import { Portal } from '.'
77
import { useTransform } from '../../hooks'
8-
import type { Vector2d } from '../../types'
9-
import type { Vector2dProps } from './vector2d-types'
8+
import type { Vector2d, Vector3d } from '../../types'
9+
import type { Vector2dProps } from '../Vector2d/vector2d-types'
1010

11-
type JoystickProps = { value: Vector2d } & Pick<Vector2dProps, 'onUpdate' | 'settings'>
11+
type JoystickProps = { value: Vector2d | Vector3d } & Pick<Vector2dProps, 'onUpdate' | 'settings'> & { children?: any }
1212

13-
export function Joystick({ value, settings, onUpdate }: JoystickProps) {
13+
export function Joystick({ value, settings, onUpdate, children }: JoystickProps) {
1414
const timeout = useRef<number | undefined>()
1515
const outOfBoundsX = useRef(0)
1616
const outOfBoundsY = useRef(0)
@@ -52,13 +52,14 @@ export function Joystick({ value, settings, onUpdate }: JoystickProps) {
5252
if (outOfBoundsX.current) set({ x: outOfBoundsX.current * w })
5353
if (outOfBoundsY.current) set({ y: outOfBoundsY.current * -h })
5454
timeout.current = window.setInterval(() => {
55-
onUpdate((v: Vector2d) => {
55+
onUpdate((v: Vector2d | Vector3d) => {
5656
const incX = stepV1 * outOfBoundsX.current * stepMultiplier.current
5757
const incY = yFactor * stepV2 * outOfBoundsY.current * stepMultiplier.current
58+
5859
return Array.isArray(v)
5960
? {
60-
[v1]: v[0] + incX,
61-
[v2]: v[1] + incY,
61+
[v1]: v[['x', 'y', 'z'].indexOf(v1)] + incX,
62+
[v2]: v[['x', 'y', 'z'].indexOf(v2)] + incY,
6263
}
6364
: {
6465
[v1]: v[v1] + incX,
@@ -128,7 +129,7 @@ export function Joystick({ value, settings, onUpdate }: JoystickProps) {
128129
{joystickShown && (
129130
<Portal>
130131
<JoystickPlayground ref={playgroundRef} isOutOfBounds={isOutOfBounds}>
131-
<div />
132+
{children ? children : <JoystickGrid />}
132133
<span ref={spanRef} />
133134
</JoystickPlayground>
134135
</Portal>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react'
2+
import { Joystick } from './Joystick'
3+
import { useKeyPress } from '../../hooks/useKeyPress'
4+
import { JoystickButtons, KeyLabel } from './StyledJoystick3d'
5+
import { Button } from '../Button'
6+
import type { InternalVector2dSettings } from '../Vector2d/vector2d-types'
7+
import type { Vector3d } from '../../types'
8+
import type { Vector3dProps } from '../Vector3d/vector3d-types'
9+
import { JoyCube } from './JoyCube'
10+
11+
type Joystick3dProps = { value: Vector3d } & Pick<Vector3dProps, 'onUpdate' | 'settings'>
12+
13+
const joystick3dKeyBindings = [
14+
{ key: 'Control', keyLabel: 'ctrl', plane: 'xz', label: 'XZ' },
15+
{ key: '', keyLabel: '', plane: 'xy', label: 'XY' },
16+
{ key: 'Meta', keyLabel: 'meta', plane: 'zy', label: 'ZY' },
17+
]
18+
19+
export function Joystick3d({ value, settings, onUpdate }: Joystick3dProps) {
20+
const [plane, setPlane] = React.useState('xy')
21+
const keyPress0 = useKeyPress(joystick3dKeyBindings[0].key)
22+
// const keyPress1 = useKeyPress(joystick3dKeyBindings[1].key)
23+
const keyPress2 = useKeyPress(joystick3dKeyBindings[2].key)
24+
25+
React.useEffect(() => {
26+
if (keyPress0) setPlane(joystick3dKeyBindings[0].plane)
27+
else if (keyPress2) setPlane(joystick3dKeyBindings[2].plane)
28+
else setPlane(joystick3dKeyBindings[1].plane)
29+
}, [keyPress0, keyPress2])
30+
31+
const settings2d = React.useMemo(() => {
32+
const { keys, ...rest } = settings
33+
return { keys: plane, ...rest } as unknown as InternalVector2dSettings
34+
}, [settings, plane])
35+
36+
return (
37+
<>
38+
<Joystick value={value} settings={settings2d} onUpdate={onUpdate}>
39+
<JoyCube isTop={keyPress0} isRight={keyPress2} />
40+
41+
<JoystickButtons>
42+
{joystick3dKeyBindings.map((kb) => (
43+
<Button
44+
key={kb.label}
45+
label={<ButtonLabel label={kb.label} keyLabel={kb.keyLabel || kb.key} />}
46+
onClick={() => ''}
47+
settings={{ disabled: plane !== kb.plane }}
48+
/>
49+
))}
50+
</JoystickButtons>
51+
</Joystick>
52+
</>
53+
)
54+
}
55+
56+
function ButtonLabel({ label, keyLabel }: { label: string; keyLabel: string }) {
57+
return (
58+
<div>
59+
<KeyLabel>{keyLabel}</KeyLabel>
60+
<span>{label}</span>
61+
</div>
62+
)
63+
}

packages/leva/src/components/Vector2d/StyledJoystick.ts renamed to packages/leva/src/components/UI/StyledJoystick.ts

+29-28
Original file line numberDiff line numberDiff line change
@@ -31,43 +31,17 @@ export const JoystickPlayground = styled('div', {
3131
boxShadow: '$level2',
3232
position: 'fixed',
3333
zIndex: 10000,
34-
overflow: 'hidden',
3534
$draggable: '',
3635
transform: 'translate(-50%, -50%)',
3736

37+
perspective: '100px',
38+
3839
variants: {
3940
isOutOfBounds: {
4041
true: { backgroundColor: '$elevation1' },
4142
false: { backgroundColor: '$elevation3' },
4243
},
4344
},
44-
'> div': {
45-
position: 'absolute',
46-
$flexCenter: '',
47-
borderStyle: 'solid',
48-
borderWidth: 1,
49-
borderColor: '$highlight1',
50-
backgroundColor: '$elevation3',
51-
width: '80%',
52-
height: '80%',
53-
54-
'&::after,&::before': {
55-
content: '""',
56-
position: 'absolute',
57-
zindex: 10,
58-
backgroundColor: '$highlight1',
59-
},
60-
61-
'&::before': {
62-
width: '100%',
63-
height: 1,
64-
},
65-
66-
'&::after': {
67-
height: '100%',
68-
width: 1,
69-
},
70-
},
7145

7246
'> span': {
7347
position: 'relative',
@@ -78,3 +52,30 @@ export const JoystickPlayground = styled('div', {
7852
borderRadius: '50%',
7953
},
8054
})
55+
56+
export const JoystickGrid = styled('div', {
57+
position: 'absolute',
58+
$flexCenter: '',
59+
borderStyle: 'solid',
60+
borderWidth: 1,
61+
borderColor: '$highlight1',
62+
width: '80%',
63+
height: '80%',
64+
65+
'&::after,&::before': {
66+
content: '""',
67+
position: 'absolute',
68+
zindex: 10,
69+
backgroundColor: '$highlight1',
70+
},
71+
72+
'&::before': {
73+
width: '100%',
74+
height: 1,
75+
},
76+
77+
'&::after': {
78+
height: '100%',
79+
width: 1,
80+
},
81+
})

0 commit comments

Comments
 (0)