Skip to content

Instantly share code, notes, and snippets.

@Pp3ng
Last active May 30, 2025 18:23
Show Gist options
  • Select an option

  • Save Pp3ng/5b9db0e13da1956647cbf241e36815d4 to your computer and use it in GitHub Desktop.

Select an option

Save Pp3ng/5b9db0e13da1956647cbf241e36815d4 to your computer and use it in GitHub Desktop.
a React component showing a draggable 3D globe
import createGlobe from "cobe";
import { MapPinIcon } from "lucide-react";
import { useEffect, useRef } from "react";
import { useSpring } from "react-spring";
const LocationCard = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const pointerInteracting = useRef<number | null>(null);
const pointerInteractionMovement = useRef(0);
const fadeMask =
"radial-gradient(circle at 50% 50%, rgb(0, 0, 0) 60%, rgb(0, 0, 0, 0) 70%)";
const [{ r }, api] = useSpring(() => ({
r: 0,
config: {
mass: 1,
tension: 280,
friction: 40,
precision: 0.001,
},
}));
useEffect(() => {
let width = 0;
const onResize = () => {
if (canvasRef.current && (width = canvasRef.current.offsetWidth)) {
window.addEventListener("resize", onResize);
}
};
onResize();
if (!canvasRef.current) return;
const globe = createGlobe(canvasRef.current, {
devicePixelRatio: 2,
width: width * 2,
height: width * 2,
phi: 0,
theta: 0,
dark: 1,
diffuse: 2,
mapSamples: 12_000,
mapBrightness: 2,
baseColor: [0.8, 0.8, 0.8],
markerColor: [1, 1, 1],
glowColor: [0.5, 0.5, 0.5],
markers: [{ location: [32.8678, 120.3148], size: 0.1 }], // 32°52'04.1"N 120°18'53.4"E
scale: 1.05,
onRender: (state) => {
state.phi = 2.75 + r.get();
state.width = width * 2;
state.height = width * 2;
},
});
return () => {
globe.destroy();
window.removeEventListener("resize", onResize);
};
}, [r]);
return (
<div
className="relative flex h-60 flex-col gap-6 overflow-hidden rounded-xl p-4 lg:p-6 transition-all duration-300 ease-out hover:shadow-[0_7.2px_28.8px_rgba(31,38,135,0.45)]"
style={{
background: "rgba(255, 255, 255, 0.45)",
backdropFilter: "blur(8px) saturate(180%)",
WebkitBackdropFilter: "blur(8px) saturate(180%)",
boxShadow: "0 7.2px 28.8px rgba(31, 38, 135, 0.25)",
}}
>
<div className="flex items-center gap-2">
<MapPinIcon className="size-[18px] text-gray-800" />
<h2 className="text-sm font-medium text-gray-800">Dongtai, China</h2>
</div>
<div className="absolute inset-x-0 bottom-[-190px] mx-auto aspect-square h-[388px] [@media(max-width:420px)]:bottom-[-140px] [@media(max-width:420px)]:h-[320px] [@media(min-width:768px)_and_(max-width:858px)]:h-[350px]">
<div
style={{
width: "100%",
height: "100%",
display: "flex",
placeItems: "center",
placeContent: "center",
overflow: "visible",
}}
>
<div
style={{
width: "100%",
aspectRatio: "1/1",
maxWidth: 800,
WebkitMaskImage: fadeMask,
maskImage: fadeMask,
}}
>
<canvas
ref={canvasRef}
onPointerDown={(e) => {
pointerInteracting.current =
e.clientX - pointerInteractionMovement.current;
if (canvasRef.current)
canvasRef.current.style.cursor = "grabbing";
}}
onPointerUp={() => {
pointerInteracting.current = null;
if (canvasRef.current) canvasRef.current.style.cursor = "grab";
}}
onPointerOut={() => {
pointerInteracting.current = null;
if (canvasRef.current) canvasRef.current.style.cursor = "grab";
}}
onMouseMove={(e) => {
if (pointerInteracting.current !== null) {
const delta = e.clientX - pointerInteracting.current;
pointerInteractionMovement.current = delta;
api.start({
r: delta / 200,
});
}
}}
onTouchMove={(e) => {
if (pointerInteracting.current !== null && e.touches[0]) {
const delta =
e.touches[0].clientX - pointerInteracting.current;
pointerInteractionMovement.current = delta;
api.start({
r: delta / 100,
});
}
}}
style={{
width: "100%",
height: "100%",
contain: "layout paint size",
cursor: "auto",
userSelect: "none",
}}
/>
</div>
</div>
</div>
</div>
);
};
export default LocationCard;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment