Last active
February 19, 2023 08:40
-
-
Save neftaly/6f240d953d9334cf925a4518632ac74a to your computer and use it in GitHub Desktop.
react-three-fiber declarative map controls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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