add
This commit is contained in:
@@ -8,15 +8,20 @@ import { SecondaryFeatures } from '@/components/SecondaryFeatures'
|
||||
import Tractions from '@/components/Tractions'
|
||||
import Benefits from '@/components/Benefits'
|
||||
import Cta from '@/components/Cta'
|
||||
import { GlobeDemo } from '@/components/GlobeDemo'
|
||||
import { SpotlightPreview } from '@/components/Spotlight'
|
||||
import { StackSectionPreview } from '@/components/StackSection'
|
||||
import GlobeDemo from '@/components/GlobeDemo'
|
||||
import { Dashboard } from '@/components/Dashboard'
|
||||
import { AppsPreview } from '@/components/Apps'
|
||||
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<SpotlightPreview />
|
||||
<StackSectionPreview />
|
||||
<Dashboard />
|
||||
<AppsPreview />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
18
src/components/Apps.tsx
Normal file
18
src/components/Apps.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "@/components/Button";
|
||||
|
||||
export function AppsPreview() {
|
||||
return (
|
||||
<div className="relative flex h-[40rem] w-full overflow-hidden rounded-md bg-transparent antialiased md:items-center md:justify-center">
|
||||
<div className="relative z-10 mx-auto w-full max-w-4xl p-4 pt-20 md:pt-0">
|
||||
<div className="flex flex-col justify-center items-center mb-6">
|
||||
<h1 className="bg-opacity-50 bg-gradient-to-b from-neutral-50 to-neutral-400 bg-clip-text tracking-tighter text-center text-4xl font-semibold text-transparent lg:text-6xl">
|
||||
Anything That Runs on Linux Can Run on ThreeFold
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
118
src/components/Dashboard.tsx
Normal file
118
src/components/Dashboard.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import CountUp from "react-countup";
|
||||
import React from "react";
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<div className="py-24 bg-transparent">
|
||||
<div className="mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
||||
{/* Column 1: Title & NODES */}
|
||||
<div className="flex flex-col space-y-10">
|
||||
{/* Title + Description */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight leading-tight text-white lg:text-4xl">
|
||||
Powered by a Global Community
|
||||
</h2>
|
||||
<p className="mt-4 sm:mt-6 text-sm font-light text-pretty text-white lg:text-base">
|
||||
ThreeFold’s groundbreaking technology enables anyone – individuals, organizations, and communities – to deploy their own Internet infrastructure.
|
||||
</p>
|
||||
<button className="mt-6" variant="primary" color="transparent" href="https://threefold.io/build" >Explore TFGrid →</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* Column 2: CORES (staggered) + SSD */}
|
||||
<div className="flex flex-col space-y-10">
|
||||
<StatCard
|
||||
label="CORES"
|
||||
description="A globally distributed mesh of CPU cores powering decentralized applications, AI workloads, and edge computing — without central bottlenecks."
|
||||
value={<CountUp end={54_958} duration={2.5} separator="," />}
|
||||
note="Total Central Processing Unit Cores available on the grid."
|
||||
className="mt-24"
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
label="SSD CAPACITY"
|
||||
description="A distributed network of storage capacity — ready to support Web3, AI, and edge computing workloads around the world."
|
||||
value={<CountUp end={7_364_506} duration={2.5} separator="," />}
|
||||
unit="GB"
|
||||
note="The total amount of storage (SSD, HDD, & RAM) on the grid."
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Column 3: nodes countries */}
|
||||
<div className="flex flex-col space-y-10 justify-start">
|
||||
<StatCard
|
||||
label="NODES"
|
||||
description="A computer server 100% dedicated to the network. It is a building block of the ThreeFold Grid, providing compute, storage, and network resources."
|
||||
value={<CountUp end={1778} duration={2.5} separator="," />}
|
||||
note="The total number of nodes on the grid."
|
||||
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
label="COUNTRIES"
|
||||
description="The number of countries where at least one node is connected and operational on the grid."
|
||||
value={<CountUp end={51} duration={2.5} separator="," />}
|
||||
note="The total number of countries with active nodes."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 🧱 Stat Card Component
|
||||
function StatCard({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
unit,
|
||||
note,
|
||||
className = "",
|
||||
}: {
|
||||
label: string;
|
||||
description: string;
|
||||
value: React.ReactNode;
|
||||
unit?: string;
|
||||
note: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`relative flex flex-col overflow-hidden rounded-2xl bg-stat-gradient p-8 shadow-sm backdrop-blur transition-all duration-300 ease-out hover:scale-105 ${className}`}
|
||||
style={{
|
||||
filter: 'brightness(1)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.filter = 'brightness(0.8) drop-shadow(0 0 20px rgba(156, 163, 175, 0.5))';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.filter = 'brightness(1)';
|
||||
}}
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-cyan-400">{label}</h3>
|
||||
<p className="mt-2 text-sm font-light text-pretty text-white lg:text-base">
|
||||
{description}
|
||||
</p>
|
||||
<div className="mt-8 flex items-center space-x-3">
|
||||
<span className="text-cyan-400 text-3xl">•</span>
|
||||
<div className="text-5xl font-semibold tracking-tight text-white tabular-nums">
|
||||
{value}
|
||||
{unit && (
|
||||
<span className="ml-2 text-lg font-normal text-gray-400">{unit}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-400 uppercase tracking-wider">
|
||||
{note}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
121
src/components/Globe.tsx
Normal file
121
src/components/Globe.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import createGlobe, { COBEOptions } from "cobe";
|
||||
import { useMotionValue, useSpring } from "framer-motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const MOVEMENT_DAMPING = 1400;
|
||||
|
||||
const GLOBE_CONFIG: COBEOptions = {
|
||||
width: 800,
|
||||
height: 800,
|
||||
onRender: () => {},
|
||||
devicePixelRatio: 2,
|
||||
phi: 0,
|
||||
theta: 0.3,
|
||||
dark: 1,
|
||||
diffuse: 1.2,
|
||||
mapSamples: 16000,
|
||||
mapBrightness: 0.5,
|
||||
baseColor: [1, 1, 1], // tailwind gray-700
|
||||
markerColor: [1, 1, 1], // white dots
|
||||
glowColor: [0.6, 0.6, 0.6],
|
||||
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 },
|
||||
],
|
||||
};
|
||||
|
||||
export function Globe({
|
||||
className,
|
||||
config = GLOBE_CONFIG,
|
||||
}: {
|
||||
className?: string;
|
||||
config?: COBEOptions;
|
||||
}) {
|
||||
let phi = 0;
|
||||
let width = 0;
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const pointerInteracting = useRef<number | null>(null);
|
||||
const pointerInteractionMovement = useRef(0);
|
||||
|
||||
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) => {
|
||||
if (pointerInteracting.current !== null) {
|
||||
const delta = clientX - pointerInteracting.current;
|
||||
pointerInteractionMovement.current = delta;
|
||||
r.set(r.get() + delta / MOVEMENT_DAMPING);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
if (canvasRef.current) {
|
||||
width = canvasRef.current.offsetWidth;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
onResize();
|
||||
|
||||
const globe = createGlobe(canvasRef.current!, {
|
||||
...config,
|
||||
width: width * 2,
|
||||
height: width * 2,
|
||||
onRender: (state) => {
|
||||
if (!pointerInteracting.current) phi += 0.005;
|
||||
state.phi = phi + rs.get();
|
||||
state.width = width * 2;
|
||||
state.height = width * 2;
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => (canvasRef.current!.style.opacity = "1"), 0);
|
||||
return () => {
|
||||
globe.destroy();
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, [rs, config]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px] ${className}`}
|
||||
>
|
||||
<canvas
|
||||
className="size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]"
|
||||
ref={canvasRef}
|
||||
onPointerDown={(e) => {
|
||||
pointerInteracting.current = e.clientX;
|
||||
updatePointerInteraction(e.clientX);
|
||||
}}
|
||||
onPointerUp={() => updatePointerInteraction(null)}
|
||||
onPointerOut={() => updatePointerInteraction(null)}
|
||||
onMouseMove={(e) => updateMovement(e.clientX)}
|
||||
onTouchMove={(e) =>
|
||||
e.touches[0] && updateMovement(e.touches[0].clientX)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,111 +1,9 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Globe } from "@/components/Globe";
|
||||
|
||||
export function GlobeDemo() {
|
||||
export default function GlobeDemo() {
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-center py-20 h-screen md:h-auto dark:bg-black bg-white relative w-full">
|
||||
<div className="max-w-7xl mx-auto w-full relative overflow-hidden h-full md:h-[40rem] px-4">
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
}}
|
||||
className="div"
|
||||
>
|
||||
<h2 className="text-center text-xl md:text-4xl font-bold text-black dark:text-white">
|
||||
We sell soap worldwide
|
||||
</h2>
|
||||
<p className="text-center text-base md:text-lg font-normal text-neutral-700 dark:text-neutral-200 max-w-md mt-2 mx-auto">
|
||||
This globe is interactive and customizable. Have fun with it, and
|
||||
don't forget to share it. :)
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Simple CSS Globe */}
|
||||
<div className="absolute w-full -bottom-20 h-72 md:h-full z-10 flex items-center justify-center">
|
||||
<div className="relative">
|
||||
{/* Globe sphere */}
|
||||
<motion.div
|
||||
className="w-64 h-64 md:w-80 md:h-80 rounded-full bg-gradient-to-br from-blue-900 via-blue-700 to-blue-500 relative overflow-hidden shadow-2xl"
|
||||
animate={{
|
||||
rotateY: 360,
|
||||
}}
|
||||
transition={{
|
||||
duration: 20,
|
||||
repeat: Infinity,
|
||||
ease: "linear"
|
||||
}}
|
||||
>
|
||||
{/* Globe grid lines */}
|
||||
<div className="absolute inset-0">
|
||||
{/* Horizontal lines */}
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div
|
||||
key={`h-${i}`}
|
||||
className="absolute w-full border-t border-blue-300/30"
|
||||
style={{ top: `${(i + 1) * 12.5}%` }}
|
||||
/>
|
||||
))}
|
||||
{/* Vertical lines */}
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div
|
||||
key={`v-${i}`}
|
||||
className="absolute h-full border-l border-blue-300/30"
|
||||
style={{ left: `${(i + 1) * 8.33}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Continents (simplified shapes) */}
|
||||
<div className="absolute top-8 left-12 w-16 h-12 bg-green-600/60 rounded-lg transform rotate-12"></div>
|
||||
<div className="absolute top-16 right-8 w-12 h-8 bg-green-600/60 rounded-full"></div>
|
||||
<div className="absolute bottom-12 left-8 w-20 h-16 bg-green-600/60 rounded-2xl transform -rotate-6"></div>
|
||||
<div className="absolute bottom-8 right-12 w-14 h-10 bg-green-600/60 rounded-lg"></div>
|
||||
|
||||
{/* Glow effect */}
|
||||
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-transparent via-white/10 to-transparent"></div>
|
||||
</motion.div>
|
||||
|
||||
{/* Orbiting dots representing connections */}
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute w-2 h-2 bg-cyan-400 rounded-full"
|
||||
style={{
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
}}
|
||||
animate={{
|
||||
rotate: 360,
|
||||
}}
|
||||
transition={{
|
||||
duration: 8 + i * 2,
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
delay: i * 0.5,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-2 h-2 bg-cyan-400 rounded-full shadow-lg shadow-cyan-400/50"
|
||||
style={{
|
||||
transform: `translate(-50%, -50%) translateX(${120 + i * 20}px)`,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute w-full bottom-0 inset-x-0 h-40 bg-gradient-to-b pointer-events-none select-none from-transparent dark:to-black to-white z-40" />
|
||||
</div>
|
||||
</div>
|
||||
<main className="relative min-h-screen bg-transparent flex items-center justify-center">
|
||||
<Globe />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
@@ -44,9 +44,6 @@ export function SpotlightPreview() {
|
||||
<Button href="#" variant="outline" color="gray">
|
||||
Start Hosting
|
||||
</Button>
|
||||
<Button href="#" variant="solid" color="white">
|
||||
How it Works →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -5,7 +5,10 @@ import { StackedCubes } from "@/components/ui/StackedCubes";
|
||||
export function StackSectionPreview() {
|
||||
|
||||
return (
|
||||
<section className="w-full bg-transparent px-4 py-8 sm:px-6 sm:pb-12 lg:px-8">
|
||||
<section className="w-full bg-transparent px-4 py-8 sm:px-6 sm:pb-12 lg:px-8 relative">
|
||||
{/* Gradient Blob Component */}
|
||||
<div className="absolute w-[400px] h-[200px] bg-gradient-to-br from-[#505050] to-[#7e7e7e] opacity-40 rounded-full blur-[150px] bottom-[200px] left-[-150px] z-0" />
|
||||
<div className="absolute w-[200px] h-[100px] bg-gradient-to-br from-[#505050] to-[#7e7e7e] opacity-50 rounded-full blur-[150px] top-[200px] right-[-150px] z-0" />
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-16 items-center lg:items-start">
|
||||
{/* Left Column - Text (1/3 width) */}
|
||||
@@ -13,9 +16,10 @@ export function StackSectionPreview() {
|
||||
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight leading-tight text-white lg:text-3xl">
|
||||
A Decentralized Infrastructure Layer
|
||||
</h2>
|
||||
<p className="mt-4 sm:mt-6 text-sm font-light text-pretty text-gray-700 lg:text-base">
|
||||
<p className="mt-4 sm:mt-6 text-sm font-light text-pretty text-white lg:text-base">
|
||||
We have built a foundational platform that runs directly on bare metal, offering a scalable solution focused on the essential building blocks of the Internet and Cloud: compute, data, and network.
|
||||
</p>
|
||||
<button className="mt-4" variant="primary" color="transparent" href="https://threefold.io/build" >Discover How It Works →</button>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Stacked Cubes (2/3 width) */}
|
||||
|
@@ -1,309 +0,0 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Color, Scene, Fog, PerspectiveCamera, Vector3 } from "three";
|
||||
import ThreeGlobe from "three-globe";
|
||||
import { useThree, Canvas, extend } from "@react-three/fiber";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
import countries from "@/data/globe.json";
|
||||
declare module "@react-three/fiber" {
|
||||
interface ThreeElements {
|
||||
threeGlobe: ThreeElements["mesh"] & {
|
||||
new (): ThreeGlobe;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extend({ ThreeGlobe: ThreeGlobe });
|
||||
|
||||
const RING_PROPAGATION_SPEED = 3;
|
||||
const aspect = 1.2;
|
||||
const cameraZ = 300;
|
||||
|
||||
type Position = {
|
||||
order: number;
|
||||
startLat: number;
|
||||
startLng: number;
|
||||
endLat: number;
|
||||
endLng: number;
|
||||
arcAlt: number;
|
||||
color: string;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
interface WorldProps {
|
||||
globeConfig: GlobeConfig;
|
||||
data: Position[];
|
||||
}
|
||||
|
||||
let numbersOfRings = [0];
|
||||
|
||||
export function Globe({ globeConfig, data }: WorldProps) {
|
||||
const globeRef = useRef<ThreeGlobe | null>(null);
|
||||
const groupRef = useRef();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
// Initialize globe only once
|
||||
useEffect(() => {
|
||||
if (!globeRef.current && groupRef.current) {
|
||||
globeRef.current = new ThreeGlobe();
|
||||
(groupRef.current as any).add(globeRef.current);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Build material when globe is initialized or when relevant props change
|
||||
useEffect(() => {
|
||||
if (!globeRef.current || !isInitialized) return;
|
||||
|
||||
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;
|
||||
}, [
|
||||
isInitialized,
|
||||
globeConfig.globeColor,
|
||||
globeConfig.emissive,
|
||||
globeConfig.emissiveIntensity,
|
||||
globeConfig.shininess,
|
||||
]);
|
||||
|
||||
// Build data when globe is initialized or when data changes
|
||||
useEffect(() => {
|
||||
if (!globeRef.current || !isInitialized || !data) return;
|
||||
|
||||
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: arc.color,
|
||||
lat: arc.startLat,
|
||||
lng: arc.startLng,
|
||||
});
|
||||
points.push({
|
||||
size: defaultProps.pointSize,
|
||||
order: arc.order,
|
||||
color: arc.color,
|
||||
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,
|
||||
);
|
||||
|
||||
globeRef.current
|
||||
.hexPolygonsData(countries.features)
|
||||
.hexPolygonResolution(3)
|
||||
.hexPolygonMargin(0.7)
|
||||
.showAtmosphere(defaultProps.showAtmosphere)
|
||||
.atmosphereColor(defaultProps.atmosphereColor)
|
||||
.atmosphereAltitude(defaultProps.atmosphereAltitude)
|
||||
.hexPolygonColor(() => defaultProps.polygonColor);
|
||||
|
||||
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) => (e as { arcAlt: number }).arcAlt * 1)
|
||||
.arcStroke(() => [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(() => defaultProps.arcTime);
|
||||
|
||||
globeRef.current
|
||||
.pointsData(filteredPoints)
|
||||
.pointColor((e) => (e as { color: string }).color)
|
||||
.pointsMerge(true)
|
||||
.pointAltitude(0.0)
|
||||
.pointRadius(2);
|
||||
|
||||
globeRef.current
|
||||
.ringsData([])
|
||||
.ringColor(() => defaultProps.polygonColor)
|
||||
.ringMaxRadius(defaultProps.maxRings)
|
||||
.ringPropagationSpeed(RING_PROPAGATION_SPEED)
|
||||
.ringRepeatPeriod(
|
||||
(defaultProps.arcTime * defaultProps.arcLength) / defaultProps.rings,
|
||||
);
|
||||
}, [
|
||||
isInitialized,
|
||||
data,
|
||||
defaultProps.pointSize,
|
||||
defaultProps.showAtmosphere,
|
||||
defaultProps.atmosphereColor,
|
||||
defaultProps.atmosphereAltitude,
|
||||
defaultProps.polygonColor,
|
||||
defaultProps.arcLength,
|
||||
defaultProps.arcTime,
|
||||
defaultProps.rings,
|
||||
defaultProps.maxRings,
|
||||
]);
|
||||
|
||||
// Handle rings animation with cleanup
|
||||
useEffect(() => {
|
||||
if (!globeRef.current || !isInitialized || !data) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (!globeRef.current) return;
|
||||
|
||||
const newNumbersOfRings = genRandomNumbers(
|
||||
0,
|
||||
data.length,
|
||||
Math.floor((data.length * 4) / 5),
|
||||
);
|
||||
|
||||
const ringsData = data
|
||||
.filter((d, i) => newNumbersOfRings.includes(i))
|
||||
.map((d) => ({
|
||||
lat: d.startLat,
|
||||
lng: d.startLng,
|
||||
color: d.color,
|
||||
}));
|
||||
|
||||
globeRef.current.ringsData(ringsData);
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [isInitialized, data]);
|
||||
|
||||
return <group ref={groupRef} />;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
@@ -223,3 +223,9 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.bg-stat-gradient {
|
||||
background: linear-gradient(to bottom, rgba(17, 17, 17, 0.5), rgba(50, 48, 49, 0.5));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user