|
|
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], |
|
|
secondary: [0.8, 0.2, 1.0], |
|
|
accent: [0.3, 0.5, 1.0] |
|
|
} |
|
|
}; |
|
|
|
|
|
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); |
|
|
} |