Andrew-dev1.1 / components /ThreeBackground.tsx
truegleai
Add full project files
2376451
raw
history blame
9.37 kB
'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%)',
}}
/>
)
}