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 */} +
+ + + + + + + +
+
+
+
+ ) +}