diff --git a/src/components/StatsSection/StatsSection.jsx b/src/components/StatsSection/StatsSection.jsx new file mode 100644 index 0000000..5d90099 --- /dev/null +++ b/src/components/StatsSection/StatsSection.jsx @@ -0,0 +1,134 @@ +import { useRef, useMemo } from 'react' +import { Canvas, useFrame } from '@react-three/fiber' +import { motion, useInView } from 'framer-motion' +import * as THREE from 'three' + +const stats = [ + { value: '40M+', label: 'Tracks', sub: 'and growing daily' }, + { value: '2.1M', label: 'Artists', sub: 'worldwide' }, + { value: '180+', label: 'Countries', sub: 'streaming now' }, + { value: '99.9%', label: 'Uptime', sub: 'always on' }, +] + +const SPIKE_COUNT = 48 + +function GlobeSpikes() { + const groupRef = useRef() + const spikesRef = useRef([]) + const dummy = useMemo(() => new THREE.Object3D(), []) + + const spikeData = useMemo(() => { + return Array.from({ length: SPIKE_COUNT }, (_, i) => { + const phi = Math.acos(-1 + (2 * i) / SPIKE_COUNT) + const theta = Math.sqrt(SPIKE_COUNT * Math.PI) * phi + const r = 1.7 + return { + x: r * Math.sin(phi) * Math.cos(theta), + y: r * Math.cos(phi), + z: r * Math.sin(phi) * Math.sin(theta), + phi, theta, + phase: Math.random() * Math.PI * 2, + speed: 0.5 + Math.random() * 1.0, + } + }) + }, []) + + useFrame(({ clock }) => { + const t = clock.getElapsedTime() + if (groupRef.current) groupRef.current.rotation.y = t * 0.12 + + spikeData.forEach((s, i) => { + if (!spikesRef.current[i]) return + const len = 0.05 + Math.abs(Math.sin(t * s.speed + s.phase)) * 0.55 + const dir = new THREE.Vector3(s.x, s.y, s.z).normalize() + dummy.position.set(s.x, s.y, s.z) + dummy.lookAt(s.x + dir.x, s.y + dir.y, s.z + dir.z) + dummy.scale.set(0.02, len, 0.02) + dummy.updateMatrix() + spikesRef.current[i].setMatrixAt(0, dummy.matrix) + spikesRef.current[i].instanceMatrix.needsUpdate = true + }) + }) + + return ( + + {/* Globe wireframe */} + + + + + + + + + + {/* Spikes as instanced meshes */} + {spikeData.map((s, i) => ( + spikesRef.current[i] = el} args={[null, null, 1]}> + + + + ))} + + ) +} + +export default function StatsSection() { + const ref = useRef() + const inView = useInView(ref, { once: true, margin: '-100px' }) + + return ( + + + + + + {/* Stats */} + + + By The Numbers + + The Scale of Sound + + + + + {stats.map((stat, i) => ( + + + {stat.value} + {stat.label} + {stat.sub} + + ))} + + + + {/* Globe */} + + + + + + + + + + + + + ) +}
By The Numbers
{stat.value}
{stat.label}
{stat.sub}