Skip to content

Instantly share code, notes, and snippets.

@neftaly
Last active February 19, 2023 08:40
Show Gist options
  • Save neftaly/6f240d953d9334cf925a4518632ac74a to your computer and use it in GitHub Desktop.
Save neftaly/6f240d953d9334cf925a4518632ac74a to your computer and use it in GitHub Desktop.
react-three-fiber declarative map controls
import { useEffect, useRef } from 'react'
import { useThree } from '@react-three/fiber'
import { PerspectiveCamera } from '@react-three/drei'
import useCamera from './useCamera'
// Camera bound to origin/coords state
const MapCamera = props => {
const invalidate = useThree(s => s.invalidate)
const groupRef = useRef()
const cameraRef = useRef()
useEffect(() => {
const update = ({
origin, // Target position
coords: [
r, // Distance to origin
theta, // Polar (up-down) angle
phi // Azimuthal (left-right) angle
]
}) => {
const group = groupRef.current
const camera = cameraRef.current
if (group && camera) {
group.position.set(...origin)
camera.position.x = r
group.rotation.z = theta
group.rotation.y = phi
invalidate()
}
}
update(useCamera.getState())
return useCamera.subscribe(update)
}, [invalidate])
return (
<group ref={groupRef}>
<PerspectiveCamera
makeDefault
{...props}
ref={cameraRef}
rotation={[0, Math.PI / 2, 0]}
/>
</group>
)
}
export { useCamera }
export default MapCamera
import create from 'zustand'
import { MathUtils, Vector2 } from 'three'
const handleWheel =
event =>
({
minR,
maxR,
minTheta,
maxTheta,
rotationScale,
coords: [oldR, oldTheta, oldPhi]
}) => {
const { deltaX, ctrlKey } = event
// Swap deltaY/deltaZ if ctrl key pressed (or user is pinch-zooming on trackpad)
const deltaY = ctrlKey ? event.deltaZ : event.deltaY
const deltaZ = ctrlKey ? event.deltaY : event.deltaZ
if (ctrlKey) event.preventDefault() // Prevent zoom gesture
const r = MathUtils.clamp(oldR + deltaY / (500 / oldR), minR, maxR) // Dolly in-out (faster as we move out further)
const theta = MathUtils.clamp(oldTheta - deltaZ / 250, minTheta, maxTheta) // Tilt up-down
const phi = oldPhi + deltaX / 250 // Pan left-right
return { coords: [r, theta, phi] }
}
const handleMove =
({ deltaXY }) =>
({ origin: [oldX, y, oldZ], coords: [r, , phi] }) => {
const screenXY = new Vector2(...deltaXY)
.rotateAround(new Vector2(0, 0), -phi) // Rotate screen X/Y coords to match camera rotation
.toArray()
.map(v => (v * r) / 1000) // Move faster as we move out further
const z = oldZ + screenXY[0]
const x = oldX - screenXY[1]
return { origin: [x, y, z] }
}
const handleRotate =
({ deltaXY, targetSize }) =>
({ minTheta, maxTheta, rotationScale, coords: [r, oldTheta, oldPhi] }) => {
const theta = MathUtils.clamp(
oldTheta + (deltaXY[1] / targetSize[1]) * rotationScale[1],
minTheta,
maxTheta
)
const phi = oldPhi - (deltaXY[0] / targetSize[0]) * rotationScale[0]
return { coords: [r, theta, phi] }
}
const handlePinch =
({ deltaXY, deltaAngle, deltaDistance }) =>
state => {
const {
minR,
maxR,
coords: [oldR, theta, oldPhi]
} = state
// TODO: 2-finger vertical swipe to change theta - https://www.youtube.com/watch?v=lAacv1lNcyc
const { origin } = handleMove({ deltaXY })(state)
const r = MathUtils.clamp(oldR + deltaDistance / (500 / oldR), minR, maxR)
const phi = oldPhi + deltaAngle
return { origin, coords: [r, theta, phi] }
}
const useCamera = create((set, get) => ({
// Config
minR: 1,
maxR: Infinity,
minTheta: 0,
maxTheta: Math.PI / 2,
rotationScale: [Math.PI * 4, Math.PI * 1], // How far to rotate when gesturing across width/height of container
// Camera position
origin: [0, 0, 0],
coords: [10, Math.PI / 4, 0],
// Handlers
handleWheel: (...args) => set(handleWheel(...args)),
handleMove: (...args) => set(handleMove(...args)),
handleRotate: (...args) => set(handleRotate(...args)),
handlePinch: (...args) => set(handlePinch(...args))
}))
export default useCamera
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment