Created
March 15, 2025 20:18
-
-
Save aneeskodappana/d75f38afc4451f924f1aad6a750bd205 to your computer and use it in GitHub Desktop.
ThreeJS Google earth like navigation
This file contains hidden or 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 { useRef, useEffect } from "react"; | |
import { Canvas, useThree } from "@react-three/fiber"; | |
import { OrbitControls } from "@react-three/drei"; | |
import { OrbitControls as OrbitControlsImpl } from "three-stdlib"; | |
import * as THREE from "three"; | |
import gsap from "gsap"; | |
import "./App.css"; // Added import | |
function Scene() { | |
const { camera, gl, scene, raycaster } = useThree(); | |
const controlsRef = useRef<OrbitControlsImpl>(null); | |
const pointer = useRef(new THREE.Vector2()); | |
const isDragging = useRef(false); | |
const mouseDownPos = useRef(new THREE.Vector2()); | |
const onMouseDown = (event: MouseEvent) => { | |
mouseDownPos.current.set(event.clientX, event.clientY); | |
isDragging.current = false; | |
}; | |
const onMouseMove = (event: MouseEvent) => { | |
const dx = event.clientX - mouseDownPos.current.x; | |
const dy = event.clientY - mouseDownPos.current.y; | |
const threshold = 5; | |
if (Math.sqrt(dx * dx + dy * dy) > threshold) { | |
isDragging.current = true; | |
} | |
}; | |
const onClick = (event: MouseEvent) => { | |
if (isDragging.current) return; | |
pointer.current.set( | |
(event.clientX / window.innerWidth) * 2 - 1, | |
-(event.clientY / window.innerHeight) * 2 + 1 | |
); | |
raycaster.setFromCamera(pointer.current, camera); | |
const intersects = raycaster.intersectObjects(scene.children); | |
if (controlsRef.current === null) { | |
return; | |
} | |
if (intersects.length === 0) { | |
return; | |
} | |
const clickedPoint = intersects[0].point; | |
gsap.to(controlsRef.current.target, { | |
x: clickedPoint.x, | |
y: clickedPoint.y, | |
z: clickedPoint.z, | |
duration: 1, | |
ease: "power2.inOut", | |
onUpdate: () => controlsRef.current!.update(), | |
}); | |
const direction = clickedPoint.clone().sub(camera.position).normalize(); | |
const currentDistance = camera.position.distanceTo(clickedPoint); | |
const targetDistance = Math.max(1, currentDistance * 0.3); | |
const targetPosition = clickedPoint | |
.clone() | |
.sub(direction.multiplyScalar(targetDistance)); | |
gsap.to(camera.position, { | |
x: targetPosition.x, | |
y: targetPosition.y, | |
z: targetPosition.z, | |
duration: 1, | |
ease: "power2.inOut", | |
onUpdate: () => controlsRef.current!.update(), | |
}); | |
}; | |
useEffect(() => { | |
const canvas = gl.domElement; | |
canvas.addEventListener("mousedown", onMouseDown); | |
canvas.addEventListener("mousemove", onMouseMove); | |
canvas.addEventListener("click", onClick); | |
return () => { | |
canvas.removeEventListener("mousedown", onMouseDown); | |
canvas.removeEventListener("mousemove", onMouseMove); | |
canvas.removeEventListener("click", onClick); | |
}; | |
}, [gl]); | |
return ( | |
<> | |
<OrbitControls | |
minPolarAngle={0.1} | |
maxPolarAngle={1.5} | |
ref={controlsRef} | |
enablePan={false} | |
enableZoom={false} | |
/> | |
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}> | |
<planeGeometry args={[50, 50]} /> | |
<meshStandardMaterial color="gray" /> | |
</mesh> | |
<mesh position={[0, 1, 0]}> | |
<boxGeometry args={[2, 2, 2]} /> | |
<meshStandardMaterial color="red" /> | |
</mesh> | |
<mesh position={[5, 1, 5]}> | |
<sphereGeometry args={[1, 32, 32]} /> | |
<meshStandardMaterial color="blue" /> | |
</mesh> | |
</> | |
); | |
} | |
export default function App() { | |
return ( | |
<div className="canvas-container"> | |
<Canvas camera={{ position: [0, 5, 10], fov: 60 }}> | |
<Scene /> | |
<ambientLight intensity={0.5} /> | |
<pointLight position={[10, 10, 10]} /> | |
</Canvas> | |
</div> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment