layout
Global Presence
An interactive 3D WebGL globe hero section with orbiting rings, customizable metrics, and global coverage icon.
globe3dthreejspresenceheroorbitmapearth
▶Preview
🖲Usage
example-usage.tsx
import { GlobalPresence } from "@/components/kanso/global-presence"
export default function GlobalPresenceDemo() {
return (
<div className="w-full border rounded-2xl overflow-hidden bg-black">
<GlobalPresence
badgeText="Global Network"
primaryActionText="Get Started"
secondaryActionText="Learn More"
/>
</div>
)
}↓Installation
1
Create folder & copy source
Create a folder named kanso inside your project's components directory (i.e. components/kanso/). Copy the source code shown in the next section, and paste it into a file named global-presence.tsx inside it.
2
Install dependencies
pnpm add three d3-geo3
Add global styling
Add the custom keyframe animations and styles to your global stylesheet (e.g. app/globals.css):
globals.css
/* Add to app/globals.css @theme inline section: */
--animate-globe-orbit: globe-orbit 14s linear infinite;
@keyframes globe-orbit {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}4
Required helper files
Ensure your project has the following helper files configured:
- →
lib/utils
<>Source Code
global-presence.tsx
'use client';
import * as React from 'react';
import { useEffect, useRef, useState, type CSSProperties } from 'react';
import {
Scene,
PerspectiveCamera,
WebGLRenderer,
SphereGeometry,
MeshBasicMaterial,
Color,
Mesh,
Group,
InstancedMesh,
Matrix4,
Raycaster,
Vector2,
TubeGeometry,
CatmullRomCurve3,
Vector3,
CanvasTexture,
} from 'three';
import { geoEquirectangular, geoPath } from 'd3-geo';
import { cn } from '@/lib/utils';
/* -------------------------------------------------------------------------- */
/* SATELLITE MARKERS & COLOR THEMES */
/* -------------------------------------------------------------------------- */
export interface GlobeMarker {
lat: number;
lng: number;
}
export const DEFAULT_GLOBE_MARKERS: GlobeMarker[] = [
{ lat: 40.7128, lng: -74.006 }, // New York
{ lat: 51.5074, lng: -0.1278 }, // London
{ lat: 35.6762, lng: 139.6503 }, // Tokyo
{ lat: 37.7749, lng: -122.4194 }, // San Francisco
{ lat: -33.8688, lng: 151.2093 }, // Sydney
{ lat: 52.52, lng: 13.405 }, // Berlin
{ lat: 1.3521, lng: 103.8198 }, // Singapore
];
export const THEME_CONFIGS = {
emerald: {
dotColor: '#60C94F',
oceanColor: '#030704',
markerColor: '#60C94F',
glowColor: 'rgba(96, 201, 79, 0.25)',
primaryColor: '#60C94F',
cometGradient:
'conic-gradient(from 0deg, transparent 0deg, transparent 220deg, rgba(96,201,79,0.08) 250deg, rgba(96,201,79,0.3) 290deg, rgba(96,201,79,0.85) 345deg, #60C94F 360deg)',
radarGradient:
'conic-gradient(from 0deg, rgba(96,201,79,0.35) 0deg, rgba(96,201,79,0.05) 45deg, transparent 90deg, transparent 360deg)',
},
cyan: {
dotColor: '#00f7ff',
oceanColor: '#02060d',
markerColor: '#ff0055',
glowColor: 'rgba(0, 247, 255, 0.25)',
primaryColor: '#00f7ff',
cometGradient:
'conic-gradient(from 0deg, transparent 0deg, transparent 220deg, rgba(0,247,255,0.08) 250deg, rgba(0,247,255,0.3) 290deg, rgba(0,247,255,0.85) 345deg, #00f7ff 360deg)',
radarGradient:
'conic-gradient(from 0deg, rgba(0,247,255,0.35) 0deg, rgba(0,247,255,0.05) 45deg, transparent 90deg, transparent 360deg)',
},
purple: {
dotColor: '#a855f7',
oceanColor: '#090314',
markerColor: '#f472b6',
glowColor: 'rgba(168, 85, 247, 0.25)',
primaryColor: '#e879f9',
cometGradient:
'conic-gradient(from 0deg, transparent 0deg, transparent 220deg, rgba(168,85,247,0.08) 250deg, rgba(168,85,247,0.3) 290deg, rgba(168,85,247,0.85) 345deg, #e879f9 360deg)',
radarGradient:
'conic-gradient(from 0deg, rgba(168,85,247,0.35) 0deg, rgba(168,85,247,0.05) 45deg, transparent 90deg, transparent 360deg)',
},
monochrome: {
dotColor: '#737373',
oceanColor: '#050505',
markerColor: '#ffffff',
glowColor: 'rgba(255, 255, 255, 0.15)',
primaryColor: '#ffffff',
cometGradient:
'conic-gradient(from 0deg, transparent 0deg, transparent 220deg, rgba(255,255,255,0.05) 250deg, rgba(255,255,255,0.2) 290deg, rgba(255,255,255,0.7) 345deg, #ffffff 360deg)',
radarGradient:
'conic-gradient(from 0deg, rgba(255,255,255,0.25) 0deg, rgba(255,255,255,0.03) 45deg, transparent 90deg, transparent 360deg)',
},
} as const;
export type GlobeThemeKey = keyof typeof THEME_CONFIGS;
/* -------------------------------------------------------------------------- */
/* 3D GLOBE CANVAS */
/* -------------------------------------------------------------------------- */
type Rgba = { r: number; g: number; b: number; a: number };
function parseColorToRgba(input: string): Rgba {
if (!input || input.trim() === '') return { r: 0, g: 0, b: 0, a: 0 };
const str = input.trim();
const rgbaMatch = str.match(
/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)/i
);
if (rgbaMatch) {
const r = Math.max(0, Math.min(255, parseFloat(rgbaMatch[1]))) / 255;
const g = Math.max(0, Math.min(255, parseFloat(rgbaMatch[2]))) / 255;
const b = Math.max(0, Math.min(255, parseFloat(rgbaMatch[3]))) / 255;
const a =
rgbaMatch[4] !== undefined
? Math.max(0, Math.min(1, parseFloat(rgbaMatch[4])))
: 1;
return { r, g, b, a };
}
const hex = str.replace(/^#/, '');
if (hex.length === 8) {
return {
r: parseInt(hex.slice(0, 2), 16) / 255,
g: parseInt(hex.slice(2, 4), 16) / 255,
b: parseInt(hex.slice(4, 6), 16) / 255,
a: parseInt(hex.slice(6, 8), 16) / 255,
};
}
if (hex.length === 6) {
return {
r: parseInt(hex.slice(0, 2), 16) / 255,
g: parseInt(hex.slice(2, 4), 16) / 255,
b: parseInt(hex.slice(4, 6), 16) / 255,
a: 1,
};
}
if (hex.length === 4) {
return {
r: parseInt(hex[0] + hex[0], 16) / 255,
g: parseInt(hex[1] + hex[1], 16) / 255,
b: parseInt(hex[2] + hex[2], 16) / 255,
a: parseInt(hex[3] + hex[3], 16) / 255,
};
}
if (hex.length === 3) {
return {
r: parseInt(hex[0] + hex[0], 16) / 255,
g: parseInt(hex[1] + hex[1], 16) / 255,
b: parseInt(hex[2] + hex[2], 16) / 255,
a: 1,
};
}
return { r: 0, g: 0, b: 0, a: 1 };
}
function mapLinear(
value: number,
inMin: number,
inMax: number,
outMin: number,
outMax: number
): number {
if (inMax === inMin) return outMin;
const t = (value - inMin) / (inMax - inMin);
return outMin + t * (outMax - outMin);
}
function mapSpeedUiToInternal(ui: number): number {
if (ui === 0) return 0;
const clamped = Math.max(0, Math.min(10, ui));
return mapLinear(clamped, 0, 10, 0, 0.9);
}
function mapDensityUiToSpacing(ui: number): number {
const clamped = Math.max(1, Math.min(10, ui));
return mapLinear(clamped, 1, 10, 24, 8);
}
function mapScaleUiToMultiplier(ui: number): number {
const clamped = Math.max(1, Math.min(20, ui));
return mapLinear(clamped, 1, 20, 0.2, 2);
}
function mapDotSizeUiToMultiplier(ui: number): number {
const clamped = Math.max(1, Math.min(10, ui));
return mapLinear(clamped, 1, 10, 0.1, 0.5);
}
function mapMarkerDotSizeUiToMultiplier(ui: number): number {
const clamped = Math.max(0, Math.min(100, ui));
return mapLinear(clamped, 0, 100, 0.1, 2.5);
}
function normalizeSmoothing(ui: number): number {
return Math.max(0, Math.min(1, ui / 10));
}
function mapDragSpeedUiToSensitivity(ui: number): number {
return mapLinear(Math.max(0, Math.min(10, ui)), 0, 10, 0.001, 0.02);
}
function mapDetailToStepSize(ui: number): number {
const clamped = Math.max(1, Math.min(10, ui));
return mapLinear(clamped, 1, 10, 10, 1);
}
function simplifyRing(ring: number[][], detail: number): number[][] {
if (ring.length < 2) return ring;
if (detail >= 10) return ring;
const stepSize = Math.max(1, Math.floor(mapDetailToStepSize(detail)));
const simplified: number[][] = [];
simplified.push(ring[0]);
for (let i = stepSize; i < ring.length - 1; i += stepSize) {
const idx = Math.min(i, ring.length - 1);
simplified.push(ring[idx]);
}
const lastPoint = ring[ring.length - 1];
const firstPoint = ring[0];
const isClosed =
Math.abs(lastPoint[0] - firstPoint[0]) < 1e-4 &&
Math.abs(lastPoint[1] - firstPoint[1]) < 1e-4;
if (!isClosed) {
simplified.push(lastPoint);
}
return simplified.length >= 2 ? simplified : ring;
}
function latLngToPosition(
lat: number,
lng: number
): { x: number; y: number; z: number } {
const latRad = lat * (Math.PI / 180);
const lngRad = lng * (Math.PI / 180);
const x = Math.cos(latRad) * Math.sin(lngRad);
const y = Math.sin(latRad);
const z = Math.cos(latRad) * Math.cos(lngRad);
return { x, y, z };
}
export interface GlobeMarkerConfig {
markers?: GlobeMarker[];
color?: string;
size?: number;
}
export interface GlobeDotsConfig {
color?: string;
size?: number;
density?: number;
allDots?: boolean;
}
export interface GlobeProps {
speed?: number;
smoothing?: number;
dots?: GlobeDotsConfig;
fill?: 'dots' | 'solid';
fillColor?: string;
scale?: number;
stopOnHover?: boolean;
interactive?: boolean;
markerConfig?: GlobeMarkerConfig;
direction?: 'left' | 'right';
initialLatitude?: number;
initialLongitude?: number;
oceanColor?: string;
outlineColor?: string;
showOutline?: boolean;
graticuleColor?: string;
showGrid?: boolean;
outlineWidth?: number;
dragSpeed?: number;
detail?: number;
style?: CSSProperties;
className?: string;
}
interface GeoJsonFeature {
type: string;
properties?: {
featurecla?: string;
type?: string;
name?: string;
[key: string]: unknown;
};
geometry?: {
type: 'Polygon' | 'MultiPolygon' | string;
coordinates: number[][][] | number[][][][];
};
}
export function Globe({
speed = 2,
smoothing = 8,
dots = { color: '#ffffff', size: 5, density: 8, allDots: false },
fill = 'dots',
fillColor = '#ffffff',
scale = 8,
stopOnHover = true,
markerConfig = { markers: [], color: '#00f7ff', size: 40 },
direction = 'left',
initialLatitude = 23,
initialLongitude = -23,
oceanColor = '#000000',
outlineColor = '#ffffff',
showOutline = true,
graticuleColor = '#D4D4D4',
showGrid = true,
outlineWidth = 1,
dragSpeed = 5,
detail = 5,
style,
className,
}: GlobeProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const dotColor = dots.color ?? '#ffffff';
const dotSize = dots.size ?? 5;
const density = dots.density ?? 8;
const allDots = dots.allDots ?? false;
const gridWidth = 1;
const smoothingN = normalizeSmoothing(smoothing);
const baseRotationSpeed = mapSpeedUiToInternal(speed);
const rotationSpeed =
direction === 'left' ? -baseRotationSpeed : baseRotationSpeed;
const dotSpacing = mapDensityUiToSpacing(density);
const dotSizeMultiplier = mapDotSizeUiToMultiplier(dotSize);
const markerRadiusMultiplier = mapMarkerDotSizeUiToMultiplier(
markerConfig.size ?? 40
);
const scaleMultiplier = mapScaleUiToMultiplier(scale);
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
const containerWidth =
container.clientWidth || container.offsetWidth || 800;
const containerHeight =
container.clientHeight || container.offsetHeight || 600;
const scene = new Scene();
const camera = new PerspectiveCamera(
50,
containerWidth / containerHeight,
0.1,
1e3
);
const baseRadius = 1;
const globeRadius = baseRadius * scaleMultiplier;
const cameraDistance = 2.5 / scaleMultiplier;
camera.position.set(0, 0, cameraDistance);
camera.lookAt(0, 0, 0);
const renderer = new WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(containerWidth, containerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = 'srgb';
const canvas = renderer.domElement;
canvas.style.position = 'absolute';
canvas.style.inset = '0';
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
canvas.style.opacity = '0';
canvas.style.visibility = 'hidden';
container.appendChild(canvas);
const resolvedOceanColor = oceanColor;
const resolvedOutlineColor = outlineColor;
const resolvedDotColor = dotColor;
const resolvedMarkerColor = markerConfig.color ?? '#00f7ff';
const resolvedGraticuleColor = graticuleColor;
const resolvedFillColor = fillColor;
const oceanRgba = parseColorToRgba(resolvedOceanColor);
const outlineRgba = parseColorToRgba(resolvedOutlineColor);
const dotRgba = parseColorToRgba(resolvedDotColor);
const graticuleRgba = parseColorToRgba(resolvedGraticuleColor);
const fillRgba = parseColorToRgba(resolvedFillColor);
const oceanGeometry = new SphereGeometry(globeRadius, 64, 64);
const oceanColorObj = resolvedOceanColor
? new Color(resolvedOceanColor)
: new Color(0, 0, 0);
const oceanMaterial = new MeshBasicMaterial({
color: oceanColorObj,
transparent: oceanRgba.a < 1 || oceanRgba.a === 0,
opacity: oceanRgba.a,
});
const oceanMesh = new Mesh(oceanGeometry, oceanMaterial);
scene.add(oceanMesh);
let globeOutlineMesh: Mesh | null = null;
if (showOutline && outlineColor && outlineRgba.a > 0) {
const outlinePositions: number[] = [];
const segments = 128;
for (let i = 0; i <= segments; i++) {
const angle = (i / segments) * Math.PI * 2;
const x = Math.cos(angle) * globeRadius;
const y = Math.sin(angle) * globeRadius;
const z = 0;
outlinePositions.push(x, y, z);
}
const outlinePoints: Vector3[] = [];
for (let i = 0; i < outlinePositions.length; i += 3) {
outlinePoints.push(
new Vector3(
outlinePositions[i],
outlinePositions[i + 1],
outlinePositions[i + 2]
)
);
}
if (outlinePoints.length >= 2) {
outlinePoints.push(outlinePoints[0].clone());
const outlineColorObj = new Color(resolvedOutlineColor);
const outlineMaterial = new MeshBasicMaterial({
color: outlineColorObj,
transparent: outlineRgba.a < 1,
opacity: outlineRgba.a,
});
const curve = new CatmullRomCurve3(outlinePoints);
const radius = (outlineWidth / 10) * 0.01;
const tubeGeometry = new TubeGeometry(
curve,
outlinePoints.length * 2,
radius,
8,
false
);
globeOutlineMesh = new Mesh(tubeGeometry, outlineMaterial);
}
}
void globeOutlineMesh;
const continentOutlineGroup = new Group();
const graticuleGroup = new Group();
if (showGrid && resolvedGraticuleColor && graticuleRgba.a > 0) {
const graticuleColorObj = resolvedGraticuleColor
? new Color(resolvedGraticuleColor)
: new Color(1, 1, 1);
const graticuleMaterial = new MeshBasicMaterial({
color: graticuleColorObj,
transparent: graticuleRgba.a < 1 || graticuleRgba.a === 0,
opacity: graticuleRgba.a,
});
const gridSpacing = 15;
for (let lat = -90; lat <= 90; lat += gridSpacing) {
const positions: number[] = [];
const segments = 64;
for (let i = 0; i <= segments; i++) {
const lng = (i / segments) * 360 - 180;
const pos = latLngToPosition(lat, lng);
positions.push(
pos.x * globeRadius,
pos.y * globeRadius,
pos.z * globeRadius
);
}
if (positions && positions.length >= 6) {
const points: Vector3[] = [];
for (let i = 0; i < positions.length; i += 3) {
points.push(
new Vector3(positions[i], positions[i + 1], positions[i + 2])
);
}
if (points.length >= 2) {
const curve = new CatmullRomCurve3(points);
const radius = (gridWidth / 10) * 0.01;
const tubeGeometry = new TubeGeometry(
curve,
points.length * 2,
radius,
8,
false
);
const tubeMesh = new Mesh(tubeGeometry, graticuleMaterial);
tubeMesh.renderOrder = 0;
graticuleGroup.add(tubeMesh);
}
}
}
for (let lng = -180; lng < 180; lng += gridSpacing) {
const positions: number[] = [];
const segments = 64;
for (let i = 0; i <= segments; i++) {
const lat = (i / segments) * 180 - 90;
const pos = latLngToPosition(lat, lng);
positions.push(
pos.x * globeRadius,
pos.y * globeRadius,
pos.z * globeRadius
);
}
if (positions && positions.length >= 6) {
const points: Vector3[] = [];
for (let i = 0; i < positions.length; i += 3) {
points.push(
new Vector3(positions[i], positions[i + 1], positions[i + 2])
);
}
if (points.length >= 2) {
const curve = new CatmullRomCurve3(points);
const radius = (gridWidth / 10) * 0.01;
const tubeGeometry = new TubeGeometry(
curve,
points.length * 2,
radius,
8,
false
);
const tubeMesh = new Mesh(tubeGeometry, graticuleMaterial);
tubeMesh.renderOrder = 0;
graticuleGroup.add(tubeMesh);
}
}
}
}
let dotInstances: InstancedMesh | Mesh | null = null;
let markerMeshes: Mesh[] = [];
const globeGroup = new Group();
const initialLongitudeRad = (initialLongitude * Math.PI) / 180;
const initialLatitudeRad = (initialLatitude * Math.PI) / 180;
globeGroup.rotation.y = initialLongitudeRad;
globeGroup.rotation.x = initialLatitudeRad;
scene.add(globeGroup);
globeGroup.add(oceanMesh);
if (showGrid && graticuleColor && graticuleRgba.a > 0) {
globeGroup.add(graticuleGroup);
}
globeGroup.add(continentOutlineGroup);
const updateMarkers = () => {
markerMeshes.forEach((mesh) => globeGroup.remove(mesh));
markerMeshes = [];
if (markerConfig.markers && markerConfig.markers.length > 0) {
const markerSize = 0.01 * markerRadiusMultiplier;
const markerGeometry = new SphereGeometry(markerSize, 16, 16);
const markerColorObj = resolvedMarkerColor
? new Color(resolvedMarkerColor)
: new Color(1, 1, 1);
const markerMaterial = new MeshBasicMaterial({
color: markerColorObj,
});
markerConfig.markers.forEach((marker) => {
if (
!marker ||
typeof marker.lat !== 'number' ||
typeof marker.lng !== 'number'
)
return;
const pos = latLngToPosition(marker.lat, marker.lng);
const markerMesh = new Mesh(markerGeometry, markerMaterial.clone());
markerMesh.position.set(
pos.x * globeRadius,
pos.y * globeRadius,
pos.z * globeRadius
);
globeGroup.add(markerMesh);
markerMeshes.push(markerMesh);
});
}
};
const loadWorldData = async () => {
try {
setIsLoading(true);
const response = await fetch(
'https://raw.githubusercontent.com/martynafford/natural-earth-geojson/refs/heads/master/50m/physical/ne_50m_land.json'
);
if (!response.ok) throw new Error('Failed to load land data');
const landFeatures = (await response.json()) as {
features: GeoJsonFeature[];
};
while (continentOutlineGroup.children.length > 0) {
continentOutlineGroup.remove(continentOutlineGroup.children[0]);
}
if (showOutline && outlineColor && outlineRgba.a > 0) {
const outlineColorObj = new Color(resolvedOutlineColor);
const outlineMaterial = new MeshBasicMaterial({
color: outlineColorObj,
transparent: outlineRgba.a < 1,
opacity: outlineRgba.a,
depthTest: true,
depthWrite: true,
});
const projection = geoEquirectangular();
const pathGenerator = geoPath().projection(projection);
landFeatures.features.forEach((feature) => {
const featureType =
feature.properties?.featurecla || feature.properties?.type || '';
const featureName = feature.properties?.name || '';
const typeStr = String(featureType).toLowerCase();
const nameStr = String(featureName).toLowerCase();
if (
typeStr.includes('graticule') ||
typeStr.includes('grid') ||
typeStr.includes('line') ||
nameStr.includes('graticule') ||
nameStr.includes('grid') ||
nameStr.includes('line')
) {
return;
}
const pathString = pathGenerator(feature as never);
if (!pathString) return;
const commands = pathString.match(/[ML][^MLZ]*/g) || [];
if (commands.length === 0) return;
const geometry = feature.geometry;
if (!geometry || !geometry.coordinates) return;
const processRing = (ring: number[][]) => {
if (ring.length < 2) return;
const simplifiedRing = simplifyRing(ring, detail);
const positions: number[] = [];
simplifiedRing.forEach((coord) => {
const [lng, lat] = coord;
const pos = latLngToPosition(lat, lng);
positions.push(
pos.x * globeRadius,
pos.y * globeRadius,
pos.z * globeRadius
);
});
if (positions && positions.length >= 6) {
const points: Vector3[] = [];
for (let i = 0; i < positions.length; i += 3) {
points.push(
new Vector3(
positions[i],
positions[i + 1],
positions[i + 2]
)
);
}
if (
points.length > 0 &&
points[0].distanceTo(points[points.length - 1]) > 0.001
) {
points.push(points[0].clone());
}
if (points.length >= 2) {
const curve = new CatmullRomCurve3(points);
const radius = (outlineWidth / 10) * 0.01;
const tubeGeometry = new TubeGeometry(
curve,
points.length * 2,
radius,
8,
false
);
const tubeMesh = new Mesh(tubeGeometry, outlineMaterial);
tubeMesh.renderOrder = 0;
continentOutlineGroup.add(tubeMesh);
}
}
};
if (
geometry.type === 'Polygon' &&
geometry.coordinates.length > 0
) {
processRing(geometry.coordinates[0] as number[][]);
} else if (geometry.type === 'MultiPolygon') {
(geometry.coordinates as number[][][][]).forEach((polygon) => {
if (polygon.length > 0) {
processRing(polygon[0]);
}
});
}
});
}
const bitmapWidth = 2048;
const bitmapHeight = 1024;
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = bitmapWidth;
offscreenCanvas.height = bitmapHeight;
const ctx = offscreenCanvas.getContext('2d', {
willReadFrequently: true,
});
if (!ctx) throw new Error('Canvas not supported');
const projection = geoEquirectangular().fitSize(
[bitmapWidth, bitmapHeight],
{ type: 'Sphere' } as never
);
const pathGenerator = geoPath().projection(projection).context(ctx);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, bitmapWidth, bitmapHeight);
ctx.fillStyle = '#fff';
ctx.beginPath();
landFeatures.features.forEach((feature) => {
pathGenerator(feature as never);
});
ctx.fill();
const imageData = ctx.getImageData(0, 0, bitmapWidth, bitmapHeight);
const pixels = imageData.data;
const isOnLand = (lng: number, lat: number) => {
const x = Math.round(((lng + 180) / 360) * bitmapWidth) % bitmapWidth;
const y = Math.round(((90 - lat) / 180) * bitmapHeight);
const clampedY = Math.max(0, Math.min(bitmapHeight - 1, y));
const idx = (clampedY * bitmapWidth + x) * 4;
return pixels[idx] > 128;
};
if (fill === 'solid') {
const texW = 1024;
const texH = 512;
const fillCanvas = document.createElement('canvas');
fillCanvas.width = texW;
fillCanvas.height = texH;
const fctx = fillCanvas.getContext('2d')!;
const img = fctx.createImageData(texW, texH);
const data = img.data;
const fr = Math.round(fillRgba.r * 255);
const fg = Math.round(fillRgba.g * 255);
const fb = Math.round(fillRgba.b * 255);
const fa = Math.round((fillRgba.a || 1) * 255);
for (let ty = 0; ty < texH; ty++) {
for (let tx = 0; tx < texW; tx++) {
const u = tx / texW;
const v = ty / texH;
let lng = (u - 0.25) * 360;
lng = ((((lng + 180) % 360) + 360) % 360) - 180;
const lat = (v - 0.5) * 180;
const onLand = allDots || isOnLand(lng, lat);
const idx = (ty * texW + tx) * 4;
if (onLand) {
data[idx] = fr;
data[idx + 1] = fg;
data[idx + 2] = fb;
data[idx + 3] = fa;
} else {
data[idx + 3] = 0;
}
}
}
fctx.putImageData(img, 0, 0);
const fillTexture = new CanvasTexture(fillCanvas);
fillTexture.flipY = false;
fillTexture.needsUpdate = true;
const fillGeometry = new SphereGeometry(globeRadius * 1.002, 64, 64);
const fillMaterial = new MeshBasicMaterial({
map: fillTexture,
transparent: true,
});
dotInstances = new Mesh(fillGeometry, fillMaterial);
globeGroup.add(dotInstances);
} else {
const dotCoordinates: number[][] = [];
const baseStep = dotSpacing * 0.08;
for (let lat = -90; lat <= 90; lat += baseStep) {
const latRad = (Math.abs(lat) * Math.PI) / 180;
const cosLat = Math.cos(latRad);
const lngStep =
cosLat > 0.01 ? baseStep / Math.max(0.3, cosLat) : 360;
for (let lng = -180; lng < 180; lng += lngStep) {
if (allDots || isOnLand(lng, lat)) {
dotCoordinates.push([lng, lat]);
}
}
}
if (dotCoordinates.length > 0) {
const dotGeometry = new SphereGeometry(
0.01 * dotSizeMultiplier,
4,
4
);
const dotColorObj = resolvedDotColor
? new Color(resolvedDotColor)
: new Color(0.6, 0.6, 0.6);
const dotMaterial = new MeshBasicMaterial({
color: dotColorObj,
transparent: dotRgba.a < 1 || dotRgba.a === 0,
opacity: dotRgba.a,
});
const instanced = new InstancedMesh(
dotGeometry,
dotMaterial,
dotCoordinates.length
);
const matrix = new Matrix4();
for (let i = 0; i < dotCoordinates.length; i++) {
const [lng, lat] = dotCoordinates[i];
const pos = latLngToPosition(lat, lng);
matrix.makeScale(1, 1, 1);
matrix.setPosition(
pos.x * globeRadius,
pos.y * globeRadius,
pos.z * globeRadius
);
instanced.setMatrixAt(i, matrix);
}
instanced.instanceMatrix.needsUpdate = true;
dotInstances = instanced;
globeGroup.add(dotInstances);
}
}
updateMarkers();
renderer.render(scene, camera);
canvas.style.opacity = '1';
canvas.style.visibility = 'visible';
setIsLoading(false);
} catch (err) {
setError(
err instanceof Error ? err.message : 'Failed to load land map data'
);
setIsLoading(false);
}
};
const rotation = { x: initialLongitudeRad, y: initialLatitudeRad };
const targetRotation = {
x: initialLongitudeRad,
y: initialLatitudeRad,
};
const velocity = { x: 0, y: 0 };
let isDragging = false;
let isHovering = false;
let lastMouseX = 0;
let lastMouseY = 0;
let animationFrameId: number | null = null;
const lerpFactor =
smoothingN === 0 ? 1 : mapLinear(smoothingN, 0, 1, 0.4, 0.03);
const velocityDecay = mapLinear(smoothingN, 0, 1, 0.7, 0.96);
const animate = () => {
let needsRender = false;
const threshold = 0.01;
if (!isDragging && rotationSpeed !== 0 && (!stopOnHover || !isHovering)) {
targetRotation.x += rotationSpeed * 0.01;
}
if (!isDragging && smoothingN > 0) {
if (
Math.abs(velocity.x) > threshold ||
Math.abs(velocity.y) > threshold
) {
targetRotation.x += velocity.x;
targetRotation.y += velocity.y;
targetRotation.y = Math.max(
-Math.PI / 2,
Math.min(Math.PI / 2, targetRotation.y)
);
velocity.x *= velocityDecay;
velocity.y *= velocityDecay;
} else {
velocity.x = 0;
velocity.y = 0;
}
}
const dx = targetRotation.x - rotation.x;
const dy = targetRotation.y - rotation.y;
if (
Math.abs(dx) > threshold ||
Math.abs(dy) > threshold ||
rotationSpeed !== 0 ||
isDragging
) {
rotation.x += dx * lerpFactor;
rotation.y += dy * lerpFactor;
rotation.y = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, rotation.y));
needsRender = true;
}
if (needsRender || rotationSpeed !== 0 || isDragging) {
globeGroup.rotation.y = rotation.x;
globeGroup.rotation.x = rotation.y;
renderer.render(scene, camera);
}
const hasVelocity =
Math.abs(velocity.x) > threshold || Math.abs(velocity.y) > threshold;
const hasLerpDelta = Math.abs(dx) > threshold || Math.abs(dy) > threshold;
const needsContinue =
isDragging || rotationSpeed !== 0 || hasVelocity || hasLerpDelta;
if (needsContinue) {
animationFrameId = requestAnimationFrame(animate);
} else {
animationFrameId = null;
}
};
const startAnimation = () => {
if (animationFrameId === null) {
animationFrameId = requestAnimationFrame(animate);
}
};
if (rotationSpeed !== 0) {
startAnimation();
}
const handleMouseDown = (event: MouseEvent) => {
isDragging = true;
velocity.x = 0;
velocity.y = 0;
lastMouseX = event.clientX;
lastMouseY = event.clientY;
startAnimation();
const handleMouseMoveDrag = (moveEvent: MouseEvent) => {
const sensitivity = mapDragSpeedUiToSensitivity(dragSpeed);
const dx = moveEvent.clientX - lastMouseX;
const dy = moveEvent.clientY - lastMouseY;
targetRotation.x += dx * sensitivity;
targetRotation.y += dy * sensitivity;
targetRotation.y = Math.max(
-Math.PI / 2,
Math.min(Math.PI / 2, targetRotation.y)
);
velocity.x = dx * sensitivity * 0.3;
velocity.y = dy * sensitivity * 0.3;
lastMouseX = moveEvent.clientX;
lastMouseY = moveEvent.clientY;
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMoveDrag);
document.removeEventListener('mouseup', handleMouseUp);
isDragging = false;
};
document.addEventListener('mousemove', handleMouseMoveDrag);
document.addEventListener('mouseup', handleMouseUp);
};
canvas.addEventListener('mousedown', handleMouseDown);
const raycaster = new Raycaster();
const mouse = new Vector2();
const handleMouseMove = (event: MouseEvent) => {
if (!stopOnHover) return;
const rect = canvas.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(oceanMesh);
isHovering = intersects.length > 0;
};
canvas.addEventListener('mousemove', handleMouseMove);
const resizeObserver = new ResizeObserver(() => {
const newWidth = container.clientWidth || container.offsetWidth || 800;
const newHeight = container.clientHeight || container.offsetHeight || 600;
camera.aspect = newWidth / newHeight;
camera.updateProjectionMatrix();
renderer.setSize(newWidth, newHeight);
const newCameraDistance = 2.5 / scaleMultiplier;
camera.position.set(0, 0, newCameraDistance);
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
});
resizeObserver.observe(container);
loadWorldData();
return () => {
if (animationFrameId !== null) cancelAnimationFrame(animationFrameId);
canvas.removeEventListener('mousedown', handleMouseDown);
canvas.removeEventListener('mousemove', handleMouseMove);
resizeObserver.disconnect();
renderer.dispose();
if (container.contains(canvas)) {
container.removeChild(canvas);
}
};
}, [
speed,
smoothing,
dots,
fill,
fillColor,
allDots,
density,
dotSize,
dotColor,
scale,
stopOnHover,
markerConfig,
direction,
initialLatitude,
initialLongitude,
oceanColor,
outlineColor,
showOutline,
graticuleColor,
showGrid,
outlineWidth,
dragSpeed,
detail,
rotationSpeed,
dotSpacing,
dotSizeMultiplier,
markerRadiusMultiplier,
scaleMultiplier,
smoothingN,
]);
const containerStyle: CSSProperties = {
...style,
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
if (error) {
return (
<div style={containerStyle} className={className}>
<div className="flex flex-col items-center justify-center p-4 text-center font-sans text-white">
<div className="text-base font-semibold">
Error loading Earth visualization
</div>
<div className="mt-1 text-xs opacity-70">{error}</div>
</div>
</div>
);
}
return (
<div ref={containerRef} style={containerStyle} className={className} />
);
}
/* -------------------------------------------------------------------------- */
/* INNOVATIVE ORBIT MODES (SATELLITES, RADAR, MULTIVERSE, HOLOGRAM) */
/* -------------------------------------------------------------------------- */
type OrbitRing = {
diameter: number;
positionY: number;
};
const DESKTOP_RINGS: OrbitRing[] = [
{ diameter: 318, positionY: -2 },
{ diameter: 355, positionY: -1 },
{ diameter: 393, positionY: 0 },
{ diameter: 432, positionY: 1 },
{ diameter: 434, positionY: 2 },
];
const MOBILE_RINGS: OrbitRing[] = [
{ diameter: 297, positionY: -3 },
{ diameter: 327, positionY: 0 },
{ diameter: 358, positionY: 0 },
{ diameter: 381, positionY: 0 },
{ diameter: 416, positionY: 0 },
];
const ORBIT_LINE_RINGS = 3;
const ORBIT_LINE_DURATIONS = ['12s', '18s', '24s'] as const;
const ORBIT_LINE_WIDTH = 2.5;
function OrbitLine({
duration,
cometGradient,
reverse = false,
}: {
duration: string;
cometGradient?: string;
reverse?: boolean;
}) {
const ringMask = `radial-gradient(farthest-side, transparent calc(100% - ${ORBIT_LINE_WIDTH}px), #000 calc(100% - ${ORBIT_LINE_WIDTH}px))`;
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-visible"
>
<div
className={cn(
'absolute inset-0 rounded-full will-change-transform animate-globe-orbit motion-reduce:animate-none',
reverse && 'direction-reverse'
)}
style={{
animationDuration: duration,
animationDirection: reverse ? 'reverse' : 'normal',
background:
cometGradient ??
'conic-gradient(from 0deg, transparent 0deg, transparent 220deg, rgba(255,255,255,0.05) 250deg, rgba(255,255,255,0.2) 290deg, rgba(255,255,255,0.7) 345deg, #ffffff 360deg)',
maskImage: ringMask,
WebkitMaskImage: ringMask,
}}
/>
</div>
);
}
/** Animated Satellite Node Beacon floating along an orbit ring */
function OrbitSatelliteNode({
diameter,
duration,
primaryColor,
delay = '0s',
reverse = false,
}: {
diameter: number;
duration: string;
primaryColor: string;
delay?: string;
reverse?: boolean;
}) {
return (
<div
className="pointer-events-none absolute inset-0 rounded-full animate-globe-orbit"
style={{
animationDuration: duration,
animationDelay: delay,
animationDirection: reverse ? 'reverse' : 'normal',
}}
>
{/* Satellite Node Head at 0 deg */}
<div
className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center justify-center"
style={{ marginTop: `-${diameter / 2}px` }}
>
<div
className="size-2.5 rounded-full shadow-[0_0_12px_currentColor]"
style={{ backgroundColor: primaryColor, color: primaryColor }}
/>
{/* Pulsing Sonar Ring */}
<div
className="absolute size-6 rounded-full border opacity-75 animate-ping"
style={{ borderColor: primaryColor }}
/>
</div>
</div>
);
}
function OrbitLayer({
rings,
className,
positionClass,
cometGradient,
showSatellites = false,
showRadar = false,
radarGradient,
primaryColor = '#60C94F',
counterRotate = false,
}: {
rings: OrbitRing[];
className: string;
positionClass: string;
cometGradient?: string;
showSatellites?: boolean;
showRadar?: boolean;
radarGradient?: string;
primaryColor?: string;
counterRotate?: boolean;
}) {
return (
<div
aria-hidden="true"
className={cn(
'absolute inset-0 [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)] [-webkit-mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]',
className
)}
>
{rings.map((ring, index) => {
const isReverse = counterRotate && index % 2 === 1;
return (
<div
key={ring.diameter}
style={{
width: `${ring.diameter}px`,
height: `${ring.diameter}px`,
marginTop: `${ring.positionY}px`,
}}
className={cn(
'absolute left-1/2 -translate-x-1/2 -translate-y-1/2',
positionClass
)}
>
{/* Dashed Orbit Ring Path */}
<div
className="absolute inset-0 rounded-full border border-dashed border-white/60"
style={{ opacity: 0.4 - index * 0.06 }}
/>
{/* Sweeping Comet Trail */}
{index < ORBIT_LINE_RINGS ? (
<OrbitLine
duration={ORBIT_LINE_DURATIONS[index]}
cometGradient={cometGradient}
reverse={isReverse}
/>
) : null}
{/* Animated Orbiting Satellite Nodes */}
{showSatellites && (index === 1 || index === 3) && (
<OrbitSatelliteNode
diameter={ring.diameter}
duration={index === 1 ? '14s' : '20s'}
delay={index === 1 ? '0s' : '-6s'}
primaryColor={primaryColor}
reverse={isReverse}
/>
)}
{/* Rotating Radar Scanner Beam */}
{showRadar && index === 2 && (
<div className="absolute inset-0 rounded-full overflow-hidden pointer-events-none opacity-40">
<div
className="size-full animate-globe-orbit"
style={{
animationDuration: '8s',
background: radarGradient,
}}
/>
</div>
)}
</div>
);
})}
</div>
);
}
export function OrbitControls({
className,
cometGradient,
showSatellites = false,
showRadar = false,
radarGradient,
primaryColor = '#60C94F',
counterRotate = false,
}: {
className?: string;
cometGradient?: string;
showSatellites?: boolean;
showRadar?: boolean;
radarGradient?: string;
primaryColor?: string;
counterRotate?: boolean;
}) {
return (
<div className={cn('relative size-full', className)}>
<OrbitLayer
rings={DESKTOP_RINGS}
className="hidden sm:block"
positionClass="top-[369.5px]"
cometGradient={cometGradient}
showSatellites={showSatellites}
showRadar={showRadar}
radarGradient={radarGradient}
primaryColor={primaryColor}
counterRotate={counterRotate}
/>
<OrbitLayer
rings={MOBILE_RINGS}
className="sm:hidden"
positionClass="top-[268px]"
cometGradient={cometGradient}
showSatellites={showSatellites}
showRadar={showRadar}
radarGradient={radarGradient}
primaryColor={primaryColor}
counterRotate={counterRotate}
/>
</div>
);
}
/* -------------------------------------------------------------------------- */
/* 5. MULTIVERSE TILTED 3D ORBITAL HALO COMPONENT */
/* -------------------------------------------------------------------------- */
function MultiverseTiltedOrbits({ primaryColor }: { primaryColor: string }) {
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 flex items-center justify-center overflow-visible"
>
{/* Tilted Ring Plane 1 */}
<div
className="absolute size-[420px] rounded-full border border-dashed border-white/40 animate-globe-orbit"
style={{
transform: 'rotateX(70deg) rotateY(25deg)',
animationDuration: '16s',
borderColor: primaryColor,
opacity: 0.5,
}}
/>
{/* Tilted Ring Plane 2 */}
<div
className="absolute size-[460px] rounded-full border border-dashed border-white/30 animate-globe-orbit"
style={{
transform: 'rotateX(65deg) rotateY(-40deg)',
animationDuration: '22s',
animationDirection: 'reverse',
borderColor: primaryColor,
opacity: 0.4,
}}
/>
{/* Tilted Ring Plane 3 */}
<div
className="absolute size-[500px] rounded-full border border-dashed border-white/20 animate-globe-orbit"
style={{
transform: 'rotateX(80deg) rotateY(60deg)',
animationDuration: '28s',
borderColor: primaryColor,
opacity: 0.3,
}}
/>
</div>
);
}
/* -------------------------------------------------------------------------- */
/* 6. MAIN GLOBE + ORBITS VISUAL COMPONENT */
/* -------------------------------------------------------------------------- */
export type OrbitMode =
| 'satellites'
| 'radar'
| 'multiverse'
| 'hologram'
| 'minimal';
export interface GlobalPresenceProps extends React.HTMLAttributes<HTMLDivElement> {
/** Innovative structural orbital mode ('satellites' | 'radar' | 'multiverse' | 'hologram' | 'minimal') */
mode?: OrbitMode;
/** Color theme variation ('emerald' | 'cyan' | 'purple' | 'monochrome') */
theme?: GlobeThemeKey;
/** Custom Globe properties */
globeProps?: GlobeProps;
}
export function GlobalPresence({
mode = 'satellites',
theme = 'emerald',
globeProps,
className,
...props
}: GlobalPresenceProps) {
const themeConfig = THEME_CONFIGS[theme] ?? THEME_CONFIGS.emerald;
const isSatellites = mode === 'satellites';
const isRadar = mode === 'radar';
const isMultiverse = mode === 'multiverse';
const isHologram = mode === 'hologram';
return (
<div
className={cn(
'relative min-h-[460px] w-full overflow-hidden bg-black text-white px-4 py-6 flex items-center justify-center',
className
)}
{...props}
>
<div
aria-hidden="true"
className="relative h-[440px] w-full max-w-[1182px] shrink-0 overflow-hidden sm:h-[500px]"
>
<div className="absolute inset-0 [mask-image:linear-gradient(to_bottom,black_0%,black_60%,rgba(0,0,0,0.35)_85%,transparent_100%)] [-webkit-mask-image:linear-gradient(to_bottom,black_0%,black_60%,rgba(0,0,0,0.35)_85%,transparent_100%)]">
{/* Ambient Radial Backglow */}
<div
className="pointer-events-none absolute left-1/2 top-[268px] h-80 w-80 -translate-x-1/2 -translate-y-1/2 rounded-full sm:top-[369.5px] sm:h-[350px] sm:w-[350px]"
style={{
background: `radial-gradient(circle, ${themeConfig.glowColor}, transparent 70%)`,
}}
/>
{/* 3D Tilted Multiverse Orbital Planes (if mode === 'multiverse') */}
{isMultiverse && (
<div className="absolute left-1/2 top-[268px] -translate-x-1/2 -translate-y-1/2 sm:top-[369.5px]">
<MultiverseTiltedOrbits primaryColor={themeConfig.primaryColor} />
</div>
)}
{/* Concentric Orbiting Dashed Rings, Comet Trails, Satellite Beacons & Radar Sweeper */}
<OrbitControls
cometGradient={themeConfig.cometGradient}
showSatellites={isSatellites || isRadar}
showRadar={isRadar}
radarGradient={themeConfig.radarGradient}
primaryColor={themeConfig.primaryColor}
counterRotate={isHologram}
/>
{/* 3D WebGL Globe Canvas centered cleanly inside the orbit system */}
<div className="pointer-events-auto absolute left-1/2 top-28 size-[312px] -translate-x-1/2 cursor-grab touch-none active:cursor-grabbing sm:top-[204px] sm:size-[331px]">
<div className="relative size-[312px] sm:size-[331px]">
<Globe
direction="right"
dots={{
color: themeConfig.dotColor,
size: 10,
density: 4,
allDots: false,
}}
speed={1}
smoothing={0}
stopOnHover={false}
dragSpeed={5}
showOutline={false}
showGrid={false}
oceanColor={themeConfig.oceanColor}
scale={9}
initialLatitude={23}
initialLongitude={-23}
markerConfig={{
markers: DEFAULT_GLOBE_MARKERS,
color: themeConfig.markerColor,
size: 35,
}}
{...globeProps}
/>
</div>
</div>
</div>
{/* Smooth bottom gradient vignette */}
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-28 bg-gradient-to-b from-transparent to-black" />
</div>
</div>
);
}