Spaces:
Paused
Paused
| 'use client' | |
| import { useEffect, useRef } from 'react' | |
| import * as THREE from 'three' | |
| export function ThreeBackground() { | |
| const canvasRef = useRef<HTMLDivElement>(null) | |
| useEffect(() => { | |
| if (!canvasRef.current) return | |
| let animationId: number | |
| let cleanup: (() => void) | null = null | |
| // Dynamically import Three.js | |
| import('three').then((THREE) => { | |
| const container = canvasRef.current! | |
| const scene = new THREE.Scene() | |
| // Use THREE.Timer instead of deprecated THREE.Clock | |
| const timer = new (THREE as any).Timer() | |
| const camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 1000 | |
| ) | |
| const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }) | |
| renderer.setSize(window.innerWidth, window.innerHeight) | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) | |
| container.appendChild(renderer.domElement) | |
| // Create particles (matching template: 1500 count, spread 50, size 0.05) | |
| const particlesGeometry = new THREE.BufferGeometry() | |
| const particlesCount = 1500 | |
| const posArray = new Float32Array(particlesCount * 3) | |
| const colorsArray = new Float32Array(particlesCount * 3) | |
| for (let i = 0; i < particlesCount * 3; i += 3) { | |
| posArray[i] = (Math.random() - 0.5) * 50 | |
| posArray[i + 1] = (Math.random() - 0.5) * 50 | |
| posArray[i + 2] = (Math.random() - 0.5) * 50 | |
| // Template particle colors: R: 0.4-0.8, G: 0.2-0.5, B: 0.8-1.0 | |
| colorsArray[i] = 0.4 + Math.random() * 0.4 | |
| colorsArray[i + 1] = 0.2 + Math.random() * 0.3 | |
| colorsArray[i + 2] = 0.8 + Math.random() * 0.2 | |
| } | |
| particlesGeometry.setAttribute('position', new THREE.BufferAttribute(posArray, 3)) | |
| particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colorsArray, 3)) | |
| const particlesMaterial = new THREE.PointsMaterial({ | |
| size: 0.05, | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.8, | |
| blending: THREE.AdditiveBlending, | |
| }) | |
| const particlesMesh = new THREE.Points(particlesGeometry, particlesMaterial) | |
| scene.add(particlesMesh) | |
| // Create floating geometric shapes (matching template: 15 shapes, spread 40) | |
| const geometries = [ | |
| new THREE.IcosahedronGeometry(1, 0), | |
| new THREE.OctahedronGeometry(1, 0), | |
| new THREE.TetrahedronGeometry(1, 0), | |
| ] | |
| const shapes: THREE.Mesh[] = [] | |
| const shapeCount = 15 | |
| for (let i = 0; i < shapeCount; i++) { | |
| const geometry = geometries[Math.floor(Math.random() * geometries.length)] | |
| // Template: HSL hue 0.6-0.8, saturation 0.7, lightness 0.5 | |
| const material = new THREE.MeshBasicMaterial({ | |
| color: new THREE.Color().setHSL(0.6 + Math.random() * 0.2, 0.7, 0.5), | |
| wireframe: true, | |
| transparent: true, | |
| opacity: 0.3, | |
| }) | |
| const mesh = new THREE.Mesh(geometry, material) | |
| mesh.position.set( | |
| (Math.random() - 0.5) * 40, | |
| (Math.random() - 0.5) * 40, | |
| (Math.random() - 0.5) * 40 | |
| ) | |
| mesh.rotation.set( | |
| Math.random() * Math.PI, | |
| Math.random() * Math.PI, | |
| 0 | |
| ) | |
| mesh.userData = { | |
| rotationSpeed: { | |
| x: (Math.random() - 0.5) * 0.01, | |
| y: (Math.random() - 0.5) * 0.01, | |
| z: (Math.random() - 0.5) * 0.01, | |
| }, | |
| floatSpeed: Math.random() * 0.02 + 0.01, | |
| floatOffset: Math.random() * Math.PI * 2, | |
| } | |
| shapes.push(mesh) | |
| scene.add(mesh) | |
| } | |
| // Connection lines between nearby shapes (matching template) | |
| const lineMaterial = new THREE.LineBasicMaterial({ | |
| color: 0x667eea, | |
| transparent: true, | |
| opacity: 0.08, | |
| }) | |
| const lineGeometry = new THREE.BufferGeometry() | |
| const linePositions = new Float32Array(shapeCount * shapeCount * 6) | |
| lineGeometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3)) | |
| const connectionLines = new THREE.LineSegments(lineGeometry, lineMaterial) | |
| scene.add(connectionLines) | |
| camera.position.z = 30 | |
| // Mouse interaction | |
| let mouseX = 0 | |
| let mouseY = 0 | |
| let touchX: number | null = null | |
| let touchY: number | null = null | |
| const windowHalfX = window.innerWidth / 2 | |
| const windowHalfY = window.innerHeight / 2 | |
| const handleMouseMove = (event: MouseEvent) => { | |
| mouseX = (event.clientX - windowHalfX) / 100 | |
| mouseY = (event.clientY - windowHalfY) / 100 | |
| } | |
| // Touch interaction for mobile (matching template design) | |
| const handleTouchMove = (event: TouchEvent) => { | |
| if (event.touches.length > 0) { | |
| const touch = event.touches[0] | |
| touchX = (touch.clientX - windowHalfX) / 100 | |
| touchY = (touch.clientY - windowHalfY) / 100 | |
| } | |
| } | |
| const handleTouchEnd = () => { | |
| touchX = null | |
| touchY = null | |
| } | |
| document.addEventListener('mousemove', handleMouseMove) | |
| document.addEventListener('touchmove', handleTouchMove, { passive: true }) | |
| document.addEventListener('touchend', handleTouchEnd) | |
| // Animation loop using THREE.Timer (not deprecated Clock) | |
| function animate() { | |
| animationId = requestAnimationFrame(animate) | |
| // Get delta from Timer | |
| const delta = timer.getDelta() | |
| const time = timer.elapsedTime | |
| // Use touch if available, else mouse | |
| const targetX = touchX !== null ? touchX : mouseX | |
| const targetY = touchY !== null ? touchY : mouseY | |
| // Template: camera lerp factor 0.05, sensitivity 2 | |
| camera.position.x += (targetX * 2 - camera.position.x) * 0.05 | |
| camera.position.y += (-targetY * 2 - camera.position.y) * 0.05 | |
| camera.lookAt(scene.position) | |
| // Template: rotation Y speed 0.05, X speed 0.02 | |
| particlesMesh.rotation.y += delta * 0.05 | |
| particlesMesh.rotation.x += delta * 0.02 | |
| shapes.forEach((shape) => { | |
| shape.rotation.x += shape.userData.rotationSpeed.x | |
| shape.rotation.y += shape.userData.rotationSpeed.y | |
| shape.rotation.z += shape.userData.rotationSpeed.z | |
| // Template: float amplitude 0.02 | |
| shape.position.y += Math.sin( | |
| time * shape.userData.floatSpeed + shape.userData.floatOffset | |
| ) * 0.02 | |
| }) | |
| // Update connection lines between nearby shapes | |
| const positions = connectionLines.geometry.attributes.position.array as Float32Array | |
| let lineIndex = 0 | |
| const connectionDistance = 15 | |
| for (let i = 0; i < shapes.length; i++) { | |
| for (let j = i + 1; j < shapes.length; j++) { | |
| const dist = shapes[i].position.distanceTo(shapes[j].position) | |
| if (dist < connectionDistance) { | |
| positions[lineIndex++] = shapes[i].position.x | |
| positions[lineIndex++] = shapes[i].position.y | |
| positions[lineIndex++] = shapes[i].position.z | |
| positions[lineIndex++] = shapes[j].position.x | |
| positions[lineIndex++] = shapes[j].position.y | |
| positions[lineIndex++] = shapes[j].position.z | |
| } | |
| } | |
| } | |
| // Zero out unused positions | |
| for (let i = lineIndex; i < positions.length; i++) { | |
| positions[i] = 0 | |
| } | |
| connectionLines.geometry.attributes.position.needsUpdate = true | |
| // Particle wave animation (matching template: wave speed 0.5, amplitude 0.02, frequency 0.1) | |
| const particlePositions = particlesGeometry.attributes.position.array as Float32Array | |
| for (let i = 0; i < particlesCount * 3; i += 3) { | |
| particlePositions[i + 1] += Math.sin(time * 0.5 + particlePositions[i] * 0.1) * 0.02 | |
| } | |
| particlesGeometry.attributes.position.needsUpdate = true | |
| renderer.render(scene, camera) | |
| } | |
| animate() | |
| // Handle resize | |
| const handleResize = () => { | |
| camera.aspect = window.innerWidth / window.innerHeight | |
| camera.updateProjectionMatrix() | |
| renderer.setSize(window.innerWidth, window.innerHeight) | |
| } | |
| window.addEventListener('resize', handleResize) | |
| // Cleanup function | |
| cleanup = () => { | |
| document.removeEventListener('mousemove', handleMouseMove) | |
| document.removeEventListener('touchmove', handleTouchMove) | |
| document.removeEventListener('touchend', handleTouchEnd) | |
| window.removeEventListener('resize', handleResize) | |
| cancelAnimationFrame(animationId) | |
| if (container.contains(renderer.domElement)) { | |
| container.removeChild(renderer.domElement) | |
| } | |
| renderer.dispose() | |
| particlesGeometry.dispose() | |
| particlesMaterial.dispose() | |
| shapes.forEach((shape) => { | |
| shape.geometry.dispose() | |
| ;(shape.material as THREE.Material).dispose() | |
| }) | |
| lineGeometry.dispose() | |
| lineMaterial.dispose() | |
| } | |
| }) | |
| return () => { | |
| cleanup?.() | |
| } | |
| }, []) | |
| return ( | |
| <div | |
| ref={canvasRef} | |
| className="fixed inset-0 z-[-1]" | |
| style={{ | |
| background: 'radial-gradient(circle at center, #1a1a2e 0%, #0a0a0a 100%)', | |
| }} | |
| /> | |
| ) | |
| } | |