feat(ui): implement dynamic view transitions and ambient gradients for themes
This commit is contained in:
1 parent
c7ad4a2da9
commit
31f087b0ba
18 files changed
+1577
-199
No files matched your search
@@ -3,6 +3,7 @@
|
||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { fmtBytes } from '@/lib/utils';
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
// Dynamically import Globe to avoid SSR issues
|
||||
const Globe = dynamic(() => import('react-globe.gl'), { ssr: false });
|
||||
@@ -40,7 +41,46 @@ interface GlobeMapProps {
|
||||
export const GlobeMap = ({ data }: GlobeMapProps) => {
|
||||
const globeRef = useRef<any>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 800, height: 500 });
|
||||
const [countries, setCountries] = useState<any>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { theme } = useTheme();
|
||||
const isLight = theme === 'light';
|
||||
|
||||
// Generate a beautiful, ultra-fresh gradient blue texture for the Light Mode ocean
|
||||
const lightGlobeTexture = useMemo(() => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1024;
|
||||
canvas.height = 512;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 512);
|
||||
gradient.addColorStop(0, '#bae6fd'); // sky-200 (light bright blue at the top)
|
||||
gradient.addColorStop(0.5, '#38bdf8'); // sky-400 (vibrant blue equator)
|
||||
gradient.addColorStop(1, '#0284c7'); // sky-600 (deeper rich blue at the bottom)
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, 1024, 512);
|
||||
}
|
||||
return canvas.toDataURL('image/png');
|
||||
}, []);
|
||||
|
||||
// Generate a sleek, deep gradient dark texture for the Dark Mode ocean
|
||||
const darkGlobeTexture = useMemo(() => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1024;
|
||||
canvas.height = 512;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 512);
|
||||
gradient.addColorStop(0, '#0f172a'); // slate-900
|
||||
gradient.addColorStop(0.5, '#020617'); // slate-950 (deepest dark blue/black)
|
||||
gradient.addColorStop(1, '#000000'); // black
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, 1024, 512);
|
||||
}
|
||||
return canvas.toDataURL('image/png');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Handle resizing
|
||||
@@ -52,21 +92,47 @@ export const GlobeMap = ({ data }: GlobeMapProps) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
updateDimensions();
|
||||
window.addEventListener('resize', updateDimensions);
|
||||
|
||||
// Fetch high-res vector polygons for both themes' continents
|
||||
fetch('https://unpkg.com/globe.gl/example/datasets/ne_110m_admin_0_countries.geojson')
|
||||
.then(res => res.json())
|
||||
.then(setCountries)
|
||||
.catch(err => console.error('Failed to load countries', err));
|
||||
|
||||
return () => window.removeEventListener('resize', updateDimensions);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-rotate configuration
|
||||
if (globeRef.current) {
|
||||
globeRef.current.controls().autoRotate = true;
|
||||
globeRef.current.controls().autoRotateSpeed = 1.0;
|
||||
|
||||
// Focus on Southeast Asia initially
|
||||
globeRef.current.pointOfView({ lat: ORIGIN_LAT, lng: ORIGIN_LNG, altitude: 1.8 }, 2000);
|
||||
}
|
||||
// Auto-rotate and controls configuration
|
||||
// Since Globe is dynamically imported, we must wait until the ref is actually populated.
|
||||
let checkInterval = setInterval(() => {
|
||||
if (globeRef.current && globeRef.current.controls) {
|
||||
clearInterval(checkInterval);
|
||||
|
||||
const controls = globeRef.current.controls();
|
||||
controls.autoRotate = true;
|
||||
controls.autoRotateSpeed = 1.2; // Adjusted auto-rotate speed to 1.5
|
||||
|
||||
// Enable inertia/damping for smooth deceleration after dragging
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.035;
|
||||
|
||||
// Adjust the manual rotation sensitivity
|
||||
controls.rotateSpeed = 0.8;
|
||||
|
||||
// Remove boundaries to allow 360 degree rotation in all directions (as much as OrbitControls allows)
|
||||
controls.minPolarAngle = 0; // The top pole (North)
|
||||
controls.maxPolarAngle = Math.PI; // The bottom pole (South)
|
||||
|
||||
// Focus on Southeast Asia initially
|
||||
globeRef.current.pointOfView({ lat: ORIGIN_LAT, lng: ORIGIN_LNG, altitude: 1.8 }, 2000);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearInterval(checkInterval);
|
||||
}, []);
|
||||
|
||||
const arcsData = useMemo(() => {
|
||||
@@ -95,7 +161,7 @@ export const GlobeMap = ({ data }: GlobeMapProps) => {
|
||||
name: arc.name,
|
||||
value: arc.value
|
||||
}));
|
||||
|
||||
|
||||
// Add origin point
|
||||
points.push({
|
||||
lat: ORIGIN_LAT,
|
||||
@@ -105,30 +171,34 @@ export const GlobeMap = ({ data }: GlobeMapProps) => {
|
||||
name: 'Indonesia (Origin)',
|
||||
value: 0
|
||||
});
|
||||
|
||||
|
||||
return points;
|
||||
}, [arcsData]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full h-[500px] rounded-xl overflow-visible bg-transparent cursor-move flex items-center justify-center"
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full h-[500px] rounded-xl overflow-visible bg-transparent cursor-move flex items-center justify-center transition-all duration-500"
|
||||
>
|
||||
<div className="absolute top-4 left-4 z-10 flex flex-col gap-1 pointer-events-none bg-black/60 p-3 rounded-lg border border-white/10 backdrop-blur-md">
|
||||
<div className="text-sm font-medium text-white flex items-center gap-2">
|
||||
Global Traffic Flows
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-1">Interactive 3D visualization of network traffic</p>
|
||||
</div>
|
||||
|
||||
<Globe
|
||||
ref={globeRef}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
globeImageUrl="//unpkg.com/three-globe/example/img/earth-night.jpg"
|
||||
bumpImageUrl="//unpkg.com/three-globe/example/img/earth-topology.png"
|
||||
backgroundColor="rgba(0,0,0,0)"
|
||||
showGraticules={true}
|
||||
globeImageUrl={isLight ? lightGlobeTexture : darkGlobeTexture}
|
||||
showAtmosphere={true}
|
||||
atmosphereColor={isLight ? "#0ea5e9" : "#1e40af"}
|
||||
atmosphereAltitude={isLight ? 0.15 : 0.25}
|
||||
|
||||
// Polygons Configuration for Unified Vector Designs
|
||||
polygonsData={countries ? countries.features : []}
|
||||
polygonAltitude={0.015}
|
||||
polygonCapColor={() => isLight ? '#4ade80' : '#0f172a'} // Fresh green for light, dark slate for dark
|
||||
polygonSideColor={() => isLight ? '#22c55e' : '#020617'} // Depth colors
|
||||
polygonStrokeColor={() => isLight ? '#bbf7d0' : '#0284c7'} // Crisp bright borders for light, neon blue for dark
|
||||
|
||||
// Arcs configuration
|
||||
arcsData={arcsData}
|
||||
arcStartLat={(d: any) => d.startLat}
|
||||
@@ -141,7 +211,7 @@ export const GlobeMap = ({ data }: GlobeMapProps) => {
|
||||
arcDashAnimateTime={2000}
|
||||
arcAltitude={0.3}
|
||||
arcStroke={0.7}
|
||||
|
||||
|
||||
// Points configuration
|
||||
pointsData={pointsData}
|
||||
pointLat="lat"
|
||||
@@ -150,7 +220,7 @@ export const GlobeMap = ({ data }: GlobeMapProps) => {
|
||||
pointAltitude={0.01}
|
||||
pointRadius="size"
|
||||
pointsMerge={true}
|
||||
|
||||
|
||||
// Rings configuration (Ping effect for points)
|
||||
ringsData={pointsData}
|
||||
ringLat="lat"
|
||||
@@ -159,7 +229,7 @@ export const GlobeMap = ({ data }: GlobeMapProps) => {
|
||||
ringMaxRadius={(d: any) => d.name === 'Indonesia (Origin)' ? 3 : 1.5}
|
||||
ringPropagationSpeed={3}
|
||||
ringRepeatPeriod={1000}
|
||||
|
||||
|
||||
// Labels configuration
|
||||
labelsData={pointsData}
|
||||
labelLat="lat"
|
||||
@@ -167,7 +237,7 @@ export const GlobeMap = ({ data }: GlobeMapProps) => {
|
||||
labelText="name"
|
||||
labelSize={1.2}
|
||||
labelDotRadius={0.5}
|
||||
labelColor={() => 'rgba(255, 255, 255, 0.8)'}
|
||||
labelColor={() => isLight ? '#0f172a' : 'rgba(255, 255, 255, 0.8)'}
|
||||
labelResolution={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user