"use client"; import createGlobe, { type COBEOptions } from "cobe"; import { useMotionValue, useSpring } from "motion/react"; import { useEffect, useRef } 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 widthRef = useRef(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 intersectsRef = useRef(false); 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 canvas = canvasRef.current; if (!canvas) return undefined; const dpr = config.devicePixelRatio; const updateWidth = (entry?: ResizeObserverEntry) => { const nextWidth = entry?.contentRect.width || canvas.offsetWidth || config.width / dpr; if (nextWidth > 0) widthRef.current = nextWidth; }; updateWidth(); const resizeObserver = new ResizeObserver(([entry]) => updateWidth(entry)); resizeObserver.observe(canvas); const intersectionObserver = new IntersectionObserver( ([entry]) => { intersectsRef.current = entry.isIntersecting; }, { rootMargin: "100px" }, ); intersectionObserver.observe(canvas); // Falha suave: sem WebGL (ou canvas já vinculado a outro tipo de contexto) o phenomenon // lança "reading 'enable' of null" e derrubaria a árvore React inteira — card ficaria preto. let globe: { destroy(): void } | null = null; try { const supportsWebGL = canvas.getContext("webgl2") ?? canvas.getContext("webgl"); if (supportsWebGL) { const initialRenderWidth = widthRef.current * dpr; globe = createGlobe(canvas, { ...config, width: initialRenderWidth, height: initialRenderWidth, onRender: (state) => { if (!autoRotate || !intersectsRef.current || document.hidden) return; if (!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; const renderWidth = widthRef.current * dpr; state.width = renderWidth; state.height = renderWidth; }, }); } } catch { globe = null; } if (!globe) { // Sem globo: fallback visível (gradiente do card permanece), página viva. canvas.style.opacity = "1"; return () => { resizeObserver.disconnect(); intersectionObserver.disconnect(); }; } const revealTimer = window.setTimeout(() => { if (canvasRef.current) { canvasRef.current.style.opacity = "1"; } }, 0); return () => { window.clearTimeout(revealTimer); globe.destroy(); resizeObserver.disconnect(); intersectionObserver.disconnect(); }; }, [rs, config, autoRotate, rotationSpeed]); return ( ); }