Skip to content

Commit

Permalink
feat: Add joystick to Vector3d
Browse files Browse the repository at this point in the history
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
  • Loading branch information
s4l4x committed Jan 31, 2023
1 parent 5e80cd1 commit d6e2607
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-toes-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'leva': minor
---

Add a joystick to Vector3d
2 changes: 2 additions & 0 deletions demo/src/sandboxes/leva-busy/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const ExtraControls = () => {

function Controls() {
const data = useControls({
vector2D: [10, 10],
vector3D: [10, 10, 10],
dimension: '4px',
string: { value: 'something', optional: true, order: -2 },
range: { value: 0, min: -10, max: 10, order: -3 },
Expand Down
2 changes: 1 addition & 1 deletion packages/leva/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Row } from '../UI'
import { StyledButton } from './StyledButton'

type ButtonProps = {
label: string
label: string | JSX.Element
} & Omit<ButtonInput, 'type'>

export function Button({ onClick, settings, label }: ButtonProps) {
Expand Down
51 changes: 51 additions & 0 deletions packages/leva/src/components/UI/JoyCube.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react'
import { StyledJoyCubeFace, StyledJoyCube } from './StyledJoystick3d'

export function JoyCube({
isTop,
isRight,
showFront = true,
showMid = true,
showRear = false,
}: {
isTop?: boolean
isRight?: boolean
showFront?: boolean
showMid?: boolean
showRear?: boolean
}) {
return (
<StyledJoyCube top={isTop} right={isRight}>
{showFront && (
<>
<StyledJoyCubeFace className="joycube-face--front" />
<StyledJoyCubeFace className="joycube-face--back" />
<StyledJoyCubeFace className="joycube-face--right" />
<StyledJoyCubeFace className="joycube-face--left" />
<StyledJoyCubeFace className="joycube-face--top" />
<StyledJoyCubeFace className="joycube-face--bottom" />
</>
)}
{showMid && (
<>
<StyledJoyCubeFace className="joycube-face--front-mid" />
<StyledJoyCubeFace className="joycube-face--back-mid" />
<StyledJoyCubeFace className="joycube-face--right-mid" />
<StyledJoyCubeFace className="joycube-face--left-mid" />
<StyledJoyCubeFace className="joycube-face--top-mid" />
<StyledJoyCubeFace className="joycube-face--bottom-mid" />
</>
)}
{showRear && (
<>
<StyledJoyCubeFace className="joycube-face--front-rear" />
<StyledJoyCubeFace className="joycube-face--back-rear" />
<StyledJoyCubeFace className="joycube-face--right-rear" />
<StyledJoyCubeFace className="joycube-face--left-rear" />
<StyledJoyCubeFace className="joycube-face--top-rear" />
<StyledJoyCubeFace className="joycube-face--bottom-rear" />
</>
)}
</StyledJoyCube>
)
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React, { useState, useRef, useCallback, useEffect, useLayoutEffect } from 'react'
import { useDrag } from '../../hooks'
import { clamp, multiplyStep } from '../../utils'
import { JoystickTrigger, JoystickPlayground } from './StyledJoystick'
import { JoystickTrigger, JoystickPlayground, JoystickGrid } from './StyledJoystick'
import { useTh } from '../../styles'
import { Portal } from '../UI'
import { Portal } from '.'
import { useTransform } from '../../hooks'
import type { Vector2d } from '../../types'
import type { Vector2dProps } from './vector2d-types'
import type { Vector2d, Vector3d } from '../../types'
import type { Vector2dProps } from '../Vector2d/vector2d-types'

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

export function Joystick({ value, settings, onUpdate }: JoystickProps) {
export function Joystick({ value, settings, onUpdate, children }: JoystickProps) {
const timeout = useRef<number | undefined>()
const outOfBoundsX = useRef(0)
const outOfBoundsY = useRef(0)
Expand Down Expand Up @@ -52,13 +52,14 @@ export function Joystick({ value, settings, onUpdate }: JoystickProps) {
if (outOfBoundsX.current) set({ x: outOfBoundsX.current * w })
if (outOfBoundsY.current) set({ y: outOfBoundsY.current * -h })
timeout.current = window.setInterval(() => {
onUpdate((v: Vector2d) => {
onUpdate((v: Vector2d | Vector3d) => {
const incX = stepV1 * outOfBoundsX.current * stepMultiplier.current
const incY = yFactor * stepV2 * outOfBoundsY.current * stepMultiplier.current

return Array.isArray(v)
? {
[v1]: v[0] + incX,
[v2]: v[1] + incY,
[v1]: v[['x', 'y', 'z'].indexOf(v1)] + incX,
[v2]: v[['x', 'y', 'z'].indexOf(v2)] + incY,
}
: {
[v1]: v[v1] + incX,
Expand Down Expand Up @@ -128,7 +129,7 @@ export function Joystick({ value, settings, onUpdate }: JoystickProps) {
{joystickShown && (
<Portal>
<JoystickPlayground ref={playgroundRef} isOutOfBounds={isOutOfBounds}>
<div />
{children ? children : <JoystickGrid />}
<span ref={spanRef} />
</JoystickPlayground>
</Portal>
Expand Down
63 changes: 63 additions & 0 deletions packages/leva/src/components/UI/Joystick3d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react'
import { Joystick } from './Joystick'
import { useKeyPress } from '../../hooks/useKeyPress'
import { JoystickButtons, KeyLabel } from './StyledJoystick3d'
import { Button } from '../Button'
import type { InternalVector2dSettings } from '../Vector2d/vector2d-types'
import type { Vector3d } from '../../types'
import type { Vector3dProps } from '../Vector3d/vector3d-types'
import { JoyCube } from './JoyCube'

type Joystick3dProps = { value: Vector3d } & Pick<Vector3dProps, 'onUpdate' | 'settings'>

const joystick3dKeyBindings = [
{ key: 'Control', keyLabel: 'ctrl', plane: 'xz', label: 'XZ' },
{ key: '', keyLabel: '', plane: 'xy', label: 'XY' },
{ key: 'Meta', keyLabel: 'meta', plane: 'zy', label: 'ZY' },
]

export function Joystick3d({ value, settings, onUpdate }: Joystick3dProps) {
const [plane, setPlane] = React.useState('xy')
const keyPress0 = useKeyPress(joystick3dKeyBindings[0].key)
// const keyPress1 = useKeyPress(joystick3dKeyBindings[1].key)
const keyPress2 = useKeyPress(joystick3dKeyBindings[2].key)

React.useEffect(() => {
if (keyPress0) setPlane(joystick3dKeyBindings[0].plane)
else if (keyPress2) setPlane(joystick3dKeyBindings[2].plane)
else setPlane(joystick3dKeyBindings[1].plane)
}, [keyPress0, keyPress2])

const settings2d = React.useMemo(() => {
const { keys, ...rest } = settings
return { keys: plane, ...rest } as unknown as InternalVector2dSettings
}, [settings, plane])

return (
<>
<Joystick value={value} settings={settings2d} onUpdate={onUpdate}>
<JoyCube isTop={keyPress0} isRight={keyPress2} />

<JoystickButtons>
{joystick3dKeyBindings.map((kb) => (
<Button
key={kb.label}
label={<ButtonLabel label={kb.label} keyLabel={kb.keyLabel || kb.key} />}
onClick={() => ''}
settings={{ disabled: plane !== kb.plane }}
/>
))}
</JoystickButtons>
</Joystick>
</>
)
}

function ButtonLabel({ label, keyLabel }: { label: string; keyLabel: string }) {
return (
<div>
<KeyLabel>{keyLabel}</KeyLabel>
<span>{label}</span>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,43 +31,17 @@ export const JoystickPlayground = styled('div', {
boxShadow: '$level2',
position: 'fixed',
zIndex: 10000,
overflow: 'hidden',
$draggable: '',
transform: 'translate(-50%, -50%)',

perspective: '100px',

variants: {
isOutOfBounds: {
true: { backgroundColor: '$elevation1' },
false: { backgroundColor: '$elevation3' },
},
},
'> div': {
position: 'absolute',
$flexCenter: '',
borderStyle: 'solid',
borderWidth: 1,
borderColor: '$highlight1',
backgroundColor: '$elevation3',
width: '80%',
height: '80%',

'&::after,&::before': {
content: '""',
position: 'absolute',
zindex: 10,
backgroundColor: '$highlight1',
},

'&::before': {
width: '100%',
height: 1,
},

'&::after': {
height: '100%',
width: 1,
},
},

'> span': {
position: 'relative',
Expand All @@ -78,3 +52,30 @@ export const JoystickPlayground = styled('div', {
borderRadius: '50%',
},
})

export const JoystickGrid = styled('div', {
position: 'absolute',
$flexCenter: '',
borderStyle: 'solid',
borderWidth: 1,
borderColor: '$highlight1',
width: '80%',
height: '80%',

'&::after,&::before': {
content: '""',
position: 'absolute',
zindex: 10,
backgroundColor: '$highlight1',
},

'&::before': {
width: '100%',
height: 1,
},

'&::after': {
height: '100%',
width: 1,
},
})
Loading

0 comments on commit d6e2607

Please sign in to comment.