import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; const config = { pointCount: 800, radius: 2, pulseSpeed: 1.2, amplitude: 0.5, color: { primary: [0.5, 0.3, 1.0], // Purple secondary: [0.8, 0.2, 1.0], // Pink accent: [0.3, 0.5, 1.0] // Blue } }; let scene, camera, renderer, controls; let pointCloud, particles; let clock; init(); animate(); function init() { scene = new THREE.Scene(); scene.background = new THREE.Color(0x0d0d2b); const canvas = document.querySelector('#webgl'); renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 ); camera.position.set(0, 0, 5); controls = new OrbitControls(camera, canvas); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.rotateSpeed = 0.5; createParticles(); setupLighting(); setupEventListeners(); } function createParticles() { const positions = new Float32Array(config.pointCount * 3); const colors = new Float32Array(config.pointCount * 3); const sizes = new Float32Array(config.pointCount); particles = { positions: new Float32Array(config.pointCount), targetPositions: new Float32Array(config.pointCount), speeds: new Float32Array(config.pointCount), phases: new Float32Array(config.pointCount) }; for (let i = 0; i < config.pointCount; i++) { const index = i * 3; const phi = Math.acos(-1 + (2 * i) / config.pointCount); const theta = Math.sqrt(config.pointCount * Math.PI) * phi; const x = config.radius * Math.sin(phi) * Math.cos(theta); const y = config.radius * Math.sin(phi) * Math.sin(theta); const z = config.radius * Math.cos(phi); positions[index] = x; positions[index + 1] = y; positions[index + 2] = z; const dist = Math.sqrt(x * x + y * y + z * z) / config.radius; const r = THREE.MathUtils.lerp(config.color.primary[0], config.color.accent[0], dist); const g = THREE.MathUtils.lerp(config.color.primary[1], config.color.secondary[1], dist); const b = THREE.MathUtils.lerp(config.color.primary[2], 1.0, dist); colors[index] = r; colors[index + 1] = g; colors[index + 2] = b; sizes[i] = Math.random() * 0.1 + 0.05; particles.positions[i] = 0; particles.targetPositions[i] = 0; particles.speeds[i] = Math.random() * 0.5 + 0.5; particles.phases[i] = Math.random() * Math.PI * 2; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); const material = new THREE.PointsMaterial({ size: 0.1, vertexColors: true, sizeAttenuation: true, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending }); pointCloud = new THREE.Points(geometry, material); scene.add(pointCloud); clock = new THREE.Clock(); scheduleNextPulse(); } function setupLighting() { const ambientLight = new THREE.AmbientLight(0x404060, 0.5); scene.add(ambientLight); const pointLight = new THREE.PointLight(0x7f5af0, 1, 10); pointLight.position.set(2, 3, 4); scene.add(pointLight); } function setupEventListeners() { window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); } function scheduleNextPulse() { const pulseDelay = Math.random() * 3000 + 1500; setTimeout(() => { triggerPulse(); }, pulseDelay); } function triggerPulse() { const waveCenter = Math.floor(Math.random() * config.pointCount); const positions = pointCloud.geometry.attributes.position.array; for (let i = 0; i < config.pointCount; i++) { const index = i * 3; const x = positions[index]; const y = positions[index + 1]; const z = positions[index + 2]; const distToCenter = Math.sqrt( (x - positions[waveCenter * 3]) ** 2 + (y - positions[waveCenter * 3 + 1]) ** 2 + (z - positions[waveCenter * 3 + 2]) ** 2 ); const normalizedDist = distToCenter / (config.radius * 2); const waveFactor = 1 - Math.min(Math.max(normalizedDist, 0), 1); particles.targetPositions[i] = config.amplitude * waveFactor; } scheduleNextPulse(); } function updateParticles(delta) { const positions = pointCloud.geometry.attributes.position.array; const originalPositions = new Float32Array(positions); for (let i = 0; i < config.pointCount; i++) { const index = i * 3; particles.positions[i] += (particles.targetPositions[i] - particles.positions[i]) * delta * particles.speeds[i]; const pulseEffect = particles.positions[i] * Math.sin(clock.getElapsedTime() * particles.speeds[i] + particles.phases[i]); const displacement = pulseEffect; positions[index] = originalPositions[index] * (1 + displacement); positions[index + 1] = originalPositions[index + 1] * (1 + displacement); positions[index + 2] = originalPositions[index + 2] * (1 + displacement); particles.targetPositions[i] *= 0.95; } pointCloud.geometry.attributes.position.needsUpdate = true; } function animate() { const delta = Math.min(clock.getDelta(), 0.1); controls.update(); updateParticles(delta); renderer.render(scene, camera); requestAnimationFrame(animate); }