♻️ refactor(ui): Dependencies
This commit is contained in:
@@ -1,113 +1,296 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { Canvas, extend, Object3DNode, useThree } from '@react-three/fiber';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Color, Fog, PerspectiveCamera, Scene, Vector3 } from 'three';
|
||||
import ThreeGlobe from 'three-globe';
|
||||
import countries from '../../data/globe.json';
|
||||
declare module '@react-three/fiber' {
|
||||
interface ThreeElements {
|
||||
threeGlobe: Object3DNode<ThreeGlobe, typeof ThreeGlobe>;
|
||||
}
|
||||
}
|
||||
|
||||
import createGlobe, { COBEOptions } from 'cobe';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
extend({ ThreeGlobe });
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
const RING_PROPAGATION_SPEED = 3;
|
||||
const aspect = 1.2;
|
||||
const cameraZ = 300;
|
||||
|
||||
const GLOBE_CONFIG: COBEOptions = {
|
||||
width: 800,
|
||||
height: 800,
|
||||
onRender: () => {},
|
||||
devicePixelRatio: 2,
|
||||
phi: 0,
|
||||
theta: 0.3,
|
||||
dark: 0,
|
||||
diffuse: 0.4,
|
||||
mapSamples: 16000,
|
||||
mapBrightness: 1.2,
|
||||
baseColor: [1, 1, 1],
|
||||
markerColor: [251 / 255, 100 / 255, 21 / 255],
|
||||
glowColor: [1, 1, 1],
|
||||
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 },
|
||||
],
|
||||
type Position = {
|
||||
order: number;
|
||||
startLat: number;
|
||||
startLng: number;
|
||||
endLat: number;
|
||||
endLng: number;
|
||||
arcAlt: number;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export default function Globe({
|
||||
className,
|
||||
config = GLOBE_CONFIG,
|
||||
}: {
|
||||
className?: string;
|
||||
config?: COBEOptions;
|
||||
}) {
|
||||
let phi = 0;
|
||||
let width = 0;
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const pointerInteracting = useRef(null);
|
||||
const pointerInteractionMovement = useRef(0);
|
||||
const [r, setR] = useState(0);
|
||||
|
||||
const updatePointerInteraction = (value: any) => {
|
||||
pointerInteracting.current = value;
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.style.cursor = value ? 'grabbing' : 'grab';
|
||||
}
|
||||
export type GlobeConfig = {
|
||||
pointSize?: number;
|
||||
globeColor?: string;
|
||||
showAtmosphere?: boolean;
|
||||
atmosphereColor?: string;
|
||||
atmosphereAltitude?: number;
|
||||
emissive?: string;
|
||||
emissiveIntensity?: number;
|
||||
shininess?: number;
|
||||
polygonColor?: string;
|
||||
ambientLight?: string;
|
||||
directionalLeftLight?: string;
|
||||
directionalTopLight?: string;
|
||||
pointLight?: string;
|
||||
arcTime?: number;
|
||||
arcLength?: number;
|
||||
rings?: number;
|
||||
maxRings?: number;
|
||||
initialPosition?: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
autoRotate?: boolean;
|
||||
autoRotateSpeed?: number;
|
||||
};
|
||||
|
||||
const updateMovement = (clientX: any) => {
|
||||
if (pointerInteracting.current !== null) {
|
||||
const delta = clientX - pointerInteracting.current;
|
||||
pointerInteractionMovement.current = delta;
|
||||
setR(delta / 200);
|
||||
}
|
||||
};
|
||||
interface WorldProps {
|
||||
globeConfig: GlobeConfig;
|
||||
data: Position[];
|
||||
}
|
||||
|
||||
const onRender = useCallback(
|
||||
(state: Record<string, any>) => {
|
||||
if (!pointerInteracting.current) phi += 0.005;
|
||||
state.phi = phi + r;
|
||||
state.width = width * 2;
|
||||
state.height = width * 2;
|
||||
},
|
||||
[r],
|
||||
);
|
||||
let numbersOfRings = [0];
|
||||
|
||||
const onResize = () => {
|
||||
if (canvasRef.current) {
|
||||
width = canvasRef.current.offsetWidth;
|
||||
}
|
||||
export function Globe({ globeConfig, data }: WorldProps) {
|
||||
const [globeData, setGlobeData] = useState<
|
||||
| {
|
||||
size: number;
|
||||
order: number;
|
||||
color: (t: number) => string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
}[]
|
||||
| null
|
||||
>(null);
|
||||
|
||||
const globeRef = useRef<ThreeGlobe | null>(null);
|
||||
|
||||
const defaultProps = {
|
||||
pointSize: 1,
|
||||
atmosphereColor: '#ffffff',
|
||||
showAtmosphere: true,
|
||||
atmosphereAltitude: 0.1,
|
||||
polygonColor: 'rgba(255,255,255,0.7)',
|
||||
globeColor: '#1d072e',
|
||||
emissive: '#000000',
|
||||
emissiveIntensity: 0.1,
|
||||
shininess: 0.9,
|
||||
arcTime: 2000,
|
||||
arcLength: 0.9,
|
||||
rings: 1,
|
||||
maxRings: 3,
|
||||
...globeConfig,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', onResize);
|
||||
onResize();
|
||||
if (globeRef.current) {
|
||||
_buildData();
|
||||
_buildMaterial();
|
||||
}
|
||||
}, [globeRef.current]);
|
||||
|
||||
const globe = createGlobe(canvasRef.current!, {
|
||||
...config,
|
||||
width: width * 2,
|
||||
height: width * 2,
|
||||
onRender,
|
||||
});
|
||||
const _buildMaterial = () => {
|
||||
if (!globeRef.current) return;
|
||||
|
||||
setTimeout(() => (canvasRef.current!.style.opacity = '1'));
|
||||
return () => globe.destroy();
|
||||
}, []);
|
||||
const globeMaterial = globeRef.current.globeMaterial() as unknown as {
|
||||
color: Color;
|
||||
emissive: Color;
|
||||
emissiveIntensity: number;
|
||||
shininess: number;
|
||||
};
|
||||
globeMaterial.color = new Color(globeConfig.globeColor);
|
||||
globeMaterial.emissive = new Color(globeConfig.emissive);
|
||||
globeMaterial.emissiveIntensity = globeConfig.emissiveIntensity || 0.1;
|
||||
globeMaterial.shininess = globeConfig.shininess || 0.9;
|
||||
};
|
||||
|
||||
const _buildData = () => {
|
||||
const arcs = data;
|
||||
let points = [];
|
||||
for (let i = 0; i < arcs.length; i++) {
|
||||
const arc = arcs[i];
|
||||
const rgb = hexToRgb(arc.color) as { r: number; g: number; b: number };
|
||||
points.push({
|
||||
size: defaultProps.pointSize,
|
||||
order: arc.order,
|
||||
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
|
||||
lat: arc.startLat,
|
||||
lng: arc.startLng,
|
||||
});
|
||||
points.push({
|
||||
size: defaultProps.pointSize,
|
||||
order: arc.order,
|
||||
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
|
||||
lat: arc.endLat,
|
||||
lng: arc.endLng,
|
||||
});
|
||||
}
|
||||
|
||||
// remove duplicates for same lat and lng
|
||||
const filteredPoints = points.filter(
|
||||
(v, i, a) =>
|
||||
a.findIndex((v2) =>
|
||||
['lat', 'lng'].every((k) => v2[k as 'lat' | 'lng'] === v[k as 'lat' | 'lng']),
|
||||
) === i,
|
||||
);
|
||||
|
||||
setGlobeData(filteredPoints);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (globeRef.current && globeData) {
|
||||
globeRef.current
|
||||
.hexPolygonsData(countries.features)
|
||||
.hexPolygonResolution(3)
|
||||
.hexPolygonMargin(0.7)
|
||||
.showAtmosphere(defaultProps.showAtmosphere)
|
||||
.atmosphereColor(defaultProps.atmosphereColor)
|
||||
.atmosphereAltitude(defaultProps.atmosphereAltitude)
|
||||
.hexPolygonColor((e) => {
|
||||
return defaultProps.polygonColor;
|
||||
});
|
||||
startAnimation();
|
||||
}
|
||||
}, [globeData]);
|
||||
|
||||
const startAnimation = () => {
|
||||
if (!globeRef.current || !globeData) return;
|
||||
|
||||
globeRef.current
|
||||
.arcsData(data)
|
||||
.arcStartLat((d) => (d as { startLat: number }).startLat * 1)
|
||||
.arcStartLng((d) => (d as { startLng: number }).startLng * 1)
|
||||
.arcEndLat((d) => (d as { endLat: number }).endLat * 1)
|
||||
.arcEndLng((d) => (d as { endLng: number }).endLng * 1)
|
||||
.arcColor((e: any) => (e as { color: string }).color)
|
||||
.arcAltitude((e) => {
|
||||
return (e as { arcAlt: number }).arcAlt * 1;
|
||||
})
|
||||
.arcStroke((e) => {
|
||||
return [0.32, 0.28, 0.3][Math.round(Math.random() * 2)];
|
||||
})
|
||||
.arcDashLength(defaultProps.arcLength)
|
||||
.arcDashInitialGap((e) => (e as { order: number }).order * 1)
|
||||
.arcDashGap(15)
|
||||
.arcDashAnimateTime((e) => defaultProps.arcTime);
|
||||
|
||||
globeRef.current
|
||||
.pointsData(data)
|
||||
.pointColor((e) => (e as { color: string }).color)
|
||||
.pointsMerge(true)
|
||||
.pointAltitude(0.0)
|
||||
.pointRadius(2);
|
||||
|
||||
globeRef.current
|
||||
.ringsData([])
|
||||
.ringColor((e: any) => (t: any) => e.color(t))
|
||||
.ringMaxRadius(defaultProps.maxRings)
|
||||
.ringPropagationSpeed(RING_PROPAGATION_SPEED)
|
||||
.ringRepeatPeriod((defaultProps.arcTime * defaultProps.arcLength) / defaultProps.rings);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!globeRef.current || !globeData) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (!globeRef.current || !globeData) return;
|
||||
numbersOfRings = genRandomNumbers(0, data.length, Math.floor((data.length * 4) / 5));
|
||||
|
||||
globeRef.current.ringsData(globeData.filter((d, i) => numbersOfRings.includes(i)));
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [globeRef.current, globeData]);
|
||||
|
||||
return (
|
||||
<div className={cn('absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px]', className)}>
|
||||
<canvas
|
||||
className={cn(
|
||||
'size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]',
|
||||
)}
|
||||
ref={canvasRef}
|
||||
onPointerDown={(e) =>
|
||||
updatePointerInteraction(e.clientX - pointerInteractionMovement.current)
|
||||
}
|
||||
onPointerUp={() => updatePointerInteraction(null)}
|
||||
onPointerOut={() => updatePointerInteraction(null)}
|
||||
onMouseMove={(e) => updateMovement(e.clientX)}
|
||||
onTouchMove={(e) => e.touches[0] && updateMovement(e.touches[0].clientX)}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<threeGlobe ref={globeRef} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function WebGLRendererConfig() {
|
||||
const { gl, size } = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
gl.setPixelRatio(window.devicePixelRatio);
|
||||
gl.setSize(size.width, size.height);
|
||||
gl.setClearColor(0xffaaff, 0);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function World(props: WorldProps) {
|
||||
const { globeConfig } = props;
|
||||
const scene = new Scene();
|
||||
scene.fog = new Fog(0xffffff, 400, 2000);
|
||||
return (
|
||||
<Canvas scene={scene} camera={new PerspectiveCamera(50, aspect, 180, 1800)}>
|
||||
<WebGLRendererConfig />
|
||||
<ambientLight color={globeConfig.ambientLight} intensity={0.6} />
|
||||
<directionalLight
|
||||
color={globeConfig.directionalLeftLight}
|
||||
position={new Vector3(-400, 100, 400)}
|
||||
/>
|
||||
<directionalLight
|
||||
color={globeConfig.directionalTopLight}
|
||||
position={new Vector3(-200, 500, 200)}
|
||||
/>
|
||||
<pointLight
|
||||
color={globeConfig.pointLight}
|
||||
position={new Vector3(-200, 500, 200)}
|
||||
intensity={0.8}
|
||||
/>
|
||||
<Globe {...props} />
|
||||
<OrbitControls
|
||||
enablePan={false}
|
||||
enableZoom={false}
|
||||
minDistance={cameraZ}
|
||||
maxDistance={cameraZ}
|
||||
autoRotateSpeed={1}
|
||||
autoRotate={true}
|
||||
minPolarAngle={Math.PI / 3.5}
|
||||
maxPolarAngle={Math.PI - Math.PI / 3}
|
||||
/>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
export function hexToRgb(hex: string) {
|
||||
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
|
||||
return r + r + g + g + b + b;
|
||||
});
|
||||
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
export function genRandomNumbers(min: number, max: number, count: number) {
|
||||
const arr = [];
|
||||
while (arr.length < count) {
|
||||
const r = Math.floor(Math.random() * (max - min)) + min;
|
||||
if (arr.indexOf(r) === -1) arr.push(r);
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user