"use client"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { cls } from "@/lib/utils"; import createGlobe, { COBEOptions } from "cobe"; // Helper function to convert CSS color to RGB array const getRGBFromCSSVar = (varName: string): [number, number, number] => { if (typeof window === "undefined") return [0.5, 0.5, 0.5]; const value = getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); // Handle CSS named colors by creating a temporary element to get computed RGB if (value && !value.startsWith("#") && !value.startsWith("rgb") && !value.includes("%") && !value.match(/^\d+\s+\d+\s+\d+$/)) { const temp = document.createElement("div"); temp.style.color = value; document.body.appendChild(temp); const computed = getComputedStyle(temp).color; document.body.removeChild(temp); if (computed && computed.startsWith("rgb")) { const match = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (match) { const r = parseInt(match[1]) / 255; const g = parseInt(match[2]) / 255; const b = parseInt(match[3]) / 255; return [r, g, b]; } } } // Handle rgba/rgb format (e.g., "rgba(18, 0, 6, .9)" or "rgb(255, 255, 255)") if (value.startsWith("rgb")) { const match = value.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (match) { const r = parseInt(match[1]) / 255; const g = parseInt(match[2]) / 255; const b = parseInt(match[3]) / 255; return [r, g, b]; } } // Handle hex format (e.g., "#ffffff", "#ffffffaa", or shorthand "#fff", "#f0f") if (value.startsWith("#")) { let hex = value.replace("#", ""); // Expand shorthand hex (e.g., "93f" -> "9933ff") if (hex.length === 3 || hex.length === 4) { hex = hex.split("").map(c => c + c).join("").substring(0, 6); } // Take only first 6 characters (ignore alpha channel if present) const r = parseInt(hex.substring(0, 2), 16) / 255; const g = parseInt(hex.substring(2, 4), 16) / 255; const b = parseInt(hex.substring(4, 6), 16) / 255; return [r, g, b]; } // Handle HSL format (e.g., "0 0% 100%") if (value.includes("%")) { const [h, s, l] = value.split(/\s+/).map(v => parseFloat(v)); // Convert HSL to RGB const sNorm = s / 100; const lNorm = l / 100; const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = lNorm - c / 2; let r = 0, g = 0, b = 0; if (h < 60) { r = c; g = x; b = 0; } else if (h < 120) { r = x; g = c; b = 0; } else if (h < 180) { r = 0; g = c; b = x; } else if (h < 240) { r = 0; g = x; b = c; } else if (h < 300) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } return [(r + m), (g + m), (b + m)]; } // Handle RGB format (e.g., "255 255 255") const [r, g, b] = value.split(/\s+/).map(v => parseFloat(v) / 255); return [r || 0.5, g || 0.5, b || 0.5]; }; const getGlobeConfig = (): COBEOptions => ({ width: 800, height: 800, onRender: () => {}, devicePixelRatio: 2, phi: 0, theta: 0.3, dark: 0, diffuse: 0.4, mapSamples: 16000, mapBrightness: 1.2, baseColor: getRGBFromCSSVar("--secondary-cta"), markerColor: getRGBFromCSSVar("--primary-cta"), glowColor: getRGBFromCSSVar("--card"), markers: [ { location: [14.5995, 120.9842], size: 0.03 }, { location: [19.076, 72.8777], size: 0.1 }, { location: [23.8103, 90.4125], size: 0.05 }, { location: [30.0444, 31.2357], size: 0.07 }, { location: [39.9042, 116.4074], size: 0.08 }, { location: [-23.5505, -46.6333], size: 0.1 }, { location: [19.4326, -99.1332], size: 0.1 }, { location: [40.7128, -74.006], size: 0.1 }, { location: [34.6937, 135.5022], size: 0.05 }, { location: [41.0082, 28.9784], size: 0.06 }, ], }); interface GlobeProps { className?: string; config?: COBEOptions; } const GlobeComponent = ({ className = "", config, }: GlobeProps) => { const canvasRef = useRef(null); const globeRef = useRef<{ destroy: () => void } | null>(null); const phiRef = useRef(0); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const [globeConfig, setGlobeConfig] = useState(null); const onRender = useCallback( (state: Record) => { phiRef.current += 0.005; state.phi = phiRef.current; state.width = dimensions.width * 2; state.height = dimensions.width * 2; }, [dimensions] ); const onResize = useCallback(() => { if (canvasRef.current) { const newWidth = canvasRef.current.offsetWidth; setDimensions(prev => { if (prev.width === newWidth) return prev; return { width: newWidth, height: newWidth }; }); } }, []); useEffect(() => { window.addEventListener("resize", onResize); onResize(); return () => { window.removeEventListener("resize", onResize); }; }, [onResize]); useEffect(() => { // Initialize globe config with CSS variables const defaultConfig = getGlobeConfig(); setGlobeConfig(config ? { ...defaultConfig, ...config } : defaultConfig); }, [config]); useEffect(() => { if (!canvasRef.current || dimensions.width === 0 || !globeConfig) return; if (globeRef.current) { globeRef.current.destroy(); } globeRef.current = createGlobe(canvasRef.current, { ...globeConfig, width: dimensions.width * 2, height: dimensions.width * 2, onRender, }); setTimeout(() => { if (canvasRef.current) { canvasRef.current.style.opacity = "1"; } }); return () => { if (globeRef.current) { globeRef.current.destroy(); globeRef.current = null; } }; }, [dimensions, globeConfig, onRender]); return (
); }; GlobeComponent.displayName = "BentoGlobe"; export const BentoGlobe = React.memo(GlobeComponent);