"use client"; import createGlobe, { type COBEOptions } from "cobe"; import { useMotionValue, useSpring } from "motion/react"; import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; const MOVEMENT_DAMPING = 1400; const JOINVILLE_LAT = -26.3044; const JOINVILLE_LNG = -48.8456; // cobe espera radianos: theta = latitude da banda de frente (Trópico de Capricórnio), phi = longitude de frente (Joinville) const TROPIC_OF_CAPRICORN_RAD = (-23.4364 * Math.PI) / 180; const JOINVILLE_LNG_RAD = (JOINVILLE_LNG * Math.PI) / 180; const CONTROL_ROOM_GLOBE_CONFIG: COBEOptions = { width: 800, height: 800, onRender: () => {}, devicePixelRatio: 2, phi: JOINVILLE_LNG_RAD, theta: TROPIC_OF_CAPRICORN_RAD, dark: 0, diffuse: 1.2, mapSamples: 16000, mapBrightness: 6, baseColor: [0.32, 0.32, 0.32], markerColor: [204 / 255, 255 / 255, 0 / 255], glowColor: [204 / 255, 255 / 255, 0 / 255], markers: [ { location: [JOINVILLE_LAT, JOINVILLE_LNG], size: 0.12 }, { location: [-25.4284, -49.2733], size: 0.06 }, { location: [-26.0167, -48.5], size: 0.05 }, { location: [-25.9333, -48.7833], size: 0.04 }, ], } as const satisfies COBEOptions; export function ControlRoomGlobe({ className, config = CONTROL_ROOM_GLOBE_CONFIG, autoRotate = true, rotationSpeed = 0.003, }: { className?: string; config?: COBEOptions; autoRotate?: boolean; rotationSpeed?: number; }) { const [width, setWidth] = useState(0); const phiRef = useRef(config.phi ?? JOINVILLE_LNG_RAD); // Rotação livre: oscilação de theta (inclinação multi-eixo) + arraste vertical const thetaPhaseRef = useRef(0); const thetaDragRef = useRef(0); const canvasRef = useRef(null); const pointerInteracting = useRef(null); const pointerYOrigin = useRef(null); const isVisibleRef = useRef(true); const r = useMotionValue(0); const rs = useSpring(r, { mass: 1, damping: 30, stiffness: 100, }); const updatePointerInteraction = (value: number | null) => { pointerInteracting.current = value; if (canvasRef.current) { canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab"; } }; const updateMovement = (clientX: number, clientY?: number) => { if (pointerInteracting.current !== null) { const delta = clientX - pointerInteracting.current; r.set(r.get() + delta / MOVEMENT_DAMPING); // Eixo vertical: arrasto inclina o globo (limitado p/ não virar de cabeça pra baixo) if (clientY !== undefined && pointerYOrigin.current !== null) { const deltaY = clientY - pointerYOrigin.current; thetaDragRef.current = Math.max( -0.9, Math.min(0.9, thetaDragRef.current + deltaY / MOVEMENT_DAMPING), ); pointerYOrigin.current = clientY; } } }; useEffect(() => { const onResize = () => { if (canvasRef.current) { setWidth(canvasRef.current.offsetWidth); } }; window.addEventListener("resize", onResize); onResize(); const observer = new IntersectionObserver( ([entry]) => { isVisibleRef.current = entry.isIntersecting; }, { rootMargin: "100px" }, ); if (canvasRef.current) { observer.observe(canvasRef.current); } const handleVisibilityChange = () => { isVisibleRef.current = !document.hidden; }; document.addEventListener("visibilitychange", handleVisibilityChange); const canvas = canvasRef.current; if (!canvas) { return () => { window.removeEventListener("resize", onResize); document.removeEventListener( "visibilitychange", handleVisibilityChange, ); observer.disconnect(); }; } const globe = createGlobe(canvas, { ...config, width: width * 2, height: width * 2, onRender: (state) => { if (!isVisibleRef.current || document.hidden) return; if (autoRotate && !pointerInteracting.current) { phiRef.current += rotationSpeed; thetaPhaseRef.current += rotationSpeed * 0.9; } state.phi = phiRef.current + rs.get(); // Rotação livre em todos os eixos: spin (phi) + guinada senoidal (theta) + inclinação por arraste vertical state.theta = (config.theta ?? TROPIC_OF_CAPRICORN_RAD) + Math.sin(thetaPhaseRef.current) * 0.35 + thetaDragRef.current; state.width = width * 2; state.height = width * 2; }, }); setTimeout(() => { if (canvasRef.current) { canvasRef.current.style.opacity = "1"; } }, 0); return () => { globe.destroy(); window.removeEventListener("resize", onResize); document.removeEventListener("visibilitychange", handleVisibilityChange); observer.disconnect(); }; }, [rs, config, autoRotate, rotationSpeed, width]); return ( ); }