Import source: src/components/StatsSection/StatsSection
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -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 (
|
||||||
|
<group ref={groupRef}>
|
||||||
|
{/* Globe wireframe */}
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[1.7, 24, 24]} />
|
||||||
|
<meshBasicMaterial color="#c9a84c" transparent opacity={0.04} wireframe />
|
||||||
|
</mesh>
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[1.65, 32, 32]} />
|
||||||
|
<meshBasicMaterial color="#c9a84c" transparent opacity={0.02} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* Spikes as instanced meshes */}
|
||||||
|
{spikeData.map((s, i) => (
|
||||||
|
<instancedMesh key={i} ref={el => spikesRef.current[i] = el} args={[null, null, 1]}>
|
||||||
|
<cylinderGeometry args={[1, 0.1, 1, 4]} />
|
||||||
|
<meshStandardMaterial color="#c9a84c" emissive="#c9a84c" emissiveIntensity={0.8} toneMapped={false} />
|
||||||
|
</instancedMesh>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatsSection() {
|
||||||
|
const ref = useRef()
|
||||||
|
const inView = useInView(ref, { once: true, margin: '-100px' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="py-28 px-6 relative overflow-hidden" style={{ background: '#050505' }}>
|
||||||
|
<div className="section-divider" />
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center mt-14">
|
||||||
|
{/* Stats */}
|
||||||
|
<div>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={inView ? { opacity: 1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="mb-12"
|
||||||
|
>
|
||||||
|
<p className="text-xs tracking-[0.25em] uppercase gold-text mb-3">By The Numbers</p>
|
||||||
|
<h2 className="font-display font-bold text-4xl md:text-5xl text-white">
|
||||||
|
The Scale of <span className="gold-text">Sound</span>
|
||||||
|
</h2>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{stats.map((stat, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={stat.label}
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={inView ? { opacity: 1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.5, delay: i * 0.1 }}
|
||||||
|
className="relative p-6 rounded-2xl group"
|
||||||
|
style={{ background: '#101010', border: '1px solid #1e1e1e' }}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
|
style={{ boxShadow: 'inset 0 0 0 1px rgba(201,168,76,0.2)' }} />
|
||||||
|
<p className="font-display font-bold text-3xl md:text-4xl gold-text mb-1">{stat.value}</p>
|
||||||
|
<p className="text-white font-medium text-sm">{stat.label}</p>
|
||||||
|
<p className="text-gray-700 text-xs mt-0.5">{stat.sub}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Globe */}
|
||||||
|
<div style={{ height: '420px' }}>
|
||||||
|
<Canvas camera={{ position: [0, 0, 6], fov: 50 }} gl={{ alpha: true, antialias: true }} style={{ background: 'transparent' }}>
|
||||||
|
<ambientLight intensity={0.05} />
|
||||||
|
<pointLight position={[0, 5, 3]} intensity={5} color="#c9a84c" />
|
||||||
|
<pointLight position={[-4, -3, 2]} intensity={2} color="#e8c547" />
|
||||||
|
<pointLight position={[4, 3, -2]} intensity={1.5} color="#8a6f2e" />
|
||||||
|
<GlobeSpikes />
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user