// Scene setup const scene = new THREE.Scene(); scene.background = new THREE.Color(0x050510); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild(renderer.domElement); // Particle system const particleCount = 12000; const positions = new Float32Array(particleCount * 3); const colors = new Float32Array(particleCount * 3); const velocities = new Float32Array(particleCount * 3); const sizes = new Float32Array(particleCount); const phases = new Float32Array(particleCount); const palette = [ new THREE.Color(0x6366f1), // indigo new THREE.Color(0x8b5cf6), // violet new THREE.Color(0xec4899), // pink new THREE.Color(0x06b6d4), // cyan new THREE.Color(0x10b981), // emerald ]; for (let i = 0; i < particleCount; i++) { const i3 = i * 3; // Distribute in a sphere const radius = Math.random() * 20; const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); positions[i3] = radius * Math.sin(phi) * Math.cos(theta); positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta); positions[i3 + 2] = radius * Math.cos(phi); // Velocities for orbital motion velocities[i3] = (Math.random() - 0.5) * 0.02; velocities[i3 + 1] = (Math.random() - 0.5) * 0.02; velocities[i3 + 2] = (Math.random() - 0.5) * 0.02; // Color from palette const color = palette[Math.floor(Math.random() * palette.length)]; colors[i3] = color.r; colors[i3 + 1] = color.g; colors[i3 + 2] = color.b; sizes[i] = Math.random() * 3 + 0.5; 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)); // Custom shader material const material = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 }, uPixelRatio: { value: renderer.getPixelRatio() }, }, vertexShader: ` attribute float size; varying vec3 vColor; uniform float uTime; uniform float uPixelRatio; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_PointSize = size * uPixelRatio * (80.0 / -mvPosition.z); gl_PointSize = max(gl_PointSize, 1.0); gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` varying vec3 vColor; uniform float uTime; void main() { float dist = length(gl_PointCoord - vec2(0.5)); if (dist > 0.5) discard; float alpha = 1.0 - smoothstep(0.0, 0.5, dist); alpha *= 0.85; // Soft glow vec3 glow = vColor * (1.0 + 0.5 * (1.0 - dist * 2.0)); gl_FragColor = vec4(glow, alpha); } `, vertexColors: true, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }); const particles = new THREE.Points(geometry, material); scene.add(particles); // Connection lines between close particles const lineCount = 800; const linePositions = new Float32Array(lineCount * 6); const lineColors = new Float32Array(lineCount * 6); const lineGeometry = new THREE.BufferGeometry(); lineGeometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3)); lineGeometry.setAttribute('color', new THREE.BufferAttribute(lineColors, 3)); const lineMaterial = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.15, blending: THREE.AdditiveBlending, depthWrite: false, }); const lines = new THREE.LineSegments(lineGeometry, lineMaterial); scene.add(lines); // Central glow sphere const glowGeo = new THREE.SphereGeometry(0.5, 32, 32); const glowMat = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 }, uColor1: { value: new THREE.Color(0x6366f1) }, uColor2: { value: new THREE.Color(0xec4899) }, }, vertexShader: ` varying vec3 vNormal; varying vec3 vPosition; void main() { vNormal = normalize(normalMatrix * normal); vPosition = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform float uTime; uniform vec3 uColor1; uniform vec3 uColor2; varying vec3 vNormal; varying vec3 vPosition; void main() { float fresnel = pow(1.0 - abs(dot(vNormal, vec3(0.0, 0.0, 1.0))), 2.0); vec3 color = mix(uColor1, uColor2, sin(uTime * 0.5) * 0.5 + 0.5); float pulse = 0.7 + 0.3 * sin(uTime * 2.0); gl_FragColor = vec4(color * pulse, fresnel * 0.6); } `, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, }); const glowSphere = new THREE.Mesh(glowGeo, glowMat); scene.add(glowSphere); // Outer glow const outerGlowGeo = new THREE.SphereGeometry(1.5, 32, 32); const outerGlowMat = glowMat.clone(); outerGlowMat.uniforms = { uTime: { value: 0 }, uColor1: { value: new THREE.Color(0x8b5cf6) }, uColor2: { value: new THREE.Color(0x06b6d4) }, }; const outerGlow = new THREE.Mesh(outerGlowGeo, outerGlowMat); scene.add(outerGlow); camera.position.set(0, 0, 25); const controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.autoRotate = true; controls.autoRotateSpeed = 0.3; controls.enablePan = false; controls.minDistance = 8; controls.maxDistance = 50; const clock = new THREE.Clock(); const mouse = new THREE.Vector2(0, 0); const mouse3D = new THREE.Vector3(0, 0, 0); const mouseSpeed = { value: 0 }; let prevMouse = new THREE.Vector2(0, 0); const raycaster = new THREE.Raycaster(); // Invisible sphere to raycast mouse position into 3D space const rayTargetGeo = new THREE.SphereGeometry(25, 16, 16); const rayTargetMat = new THREE.MeshBasicMaterial({ visible: false }); const rayTarget = new THREE.Mesh(rayTargetGeo, rayTargetMat); scene.add(rayTarget); // Smoothly interpolated mouse3D target for fluid field tracking const mouse3DTarget = new THREE.Vector3(0, 0, 0); // Simple 3D noise function (permutation-based) const noiseP = []; for (let i = 0; i < 256; i++) noiseP[i] = i; for (let i = 255; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [noiseP[i], noiseP[j]] = [noiseP[j], noiseP[i]]; } for (let i = 0; i < 256; i++) noiseP[i + 256] = noiseP[i]; function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } function lerp(a, b, t) { return a + t * (b - a); } function grad(hash, x, y, z) { const h = hash & 15; const u = h < 8 ? x : y; const v = h < 4 ? y : (h === 12 || h === 14 ? x : z); return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v); } function noise3D(x, y, z) { const X = Math.floor(x) & 255, Y = Math.floor(y) & 255, Z = Math.floor(z) & 255; x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z); const u = fade(x), v = fade(y), w = fade(z); const A = noiseP[X] + Y, AA = noiseP[A] + Z, AB = noiseP[A + 1] + Z; const B = noiseP[X + 1] + Y, BA = noiseP[B] + Z, BB = noiseP[B + 1] + Z; return lerp( lerp(lerp(grad(noiseP[AA], x, y, z), grad(noiseP[BA], x - 1, y, z), u), lerp(grad(noiseP[AB], x, y - 1, z), grad(noiseP[BB], x - 1, y - 1, z), u), v), lerp(lerp(grad(noiseP[AA + 1], x, y, z - 1), grad(noiseP[BA + 1], x - 1, y, z - 1), u), lerp(grad(noiseP[AB + 1], x, y - 1, z - 1), grad(noiseP[BB + 1], x - 1, y - 1, z - 1), u), v), w); } // FBM (fractal Brownian motion) for richer noise function fbm(x, y, z, octaves = 3) { let value = 0, amplitude = 1, frequency = 1, total = 0; for (let i = 0; i < octaves; i++) { value += noise3D(x * frequency, y * frequency, z * frequency) * amplitude; total += amplitude; amplitude *= 0.5; frequency *= 2.0; } return value / total; } // Explosion state let isExploded = false; let explodeTime = 0; const explodeVelocities = new Float32Array(particleCount * 3); const homePositions = new Float32Array(particleCount * 3); // Store initial home positions for (let i = 0; i < particleCount * 3; i++) { homePositions[i] = positions[i]; } window.addEventListener('mousemove', (e) => { mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; // Track mouse speed for noise intensity const dx = mouse.x - prevMouse.x; const dy = mouse.y - prevMouse.y; mouseSpeed.value = Math.min(Math.sqrt(dx * dx + dy * dy) * 15, 1.0); prevMouse.x = mouse.x; prevMouse.y = mouse.y; // Raycast to get mouse position in 3D space raycaster.setFromCamera(mouse, camera); const hits = raycaster.intersectObject(rayTarget); if (hits.length > 0) { mouse3DTarget.copy(hits[0].point); } }); window.addEventListener('click', () => { if (!isExploded) { // Explode outward isExploded = true; explodeTime = clock.getElapsedTime(); const posArray = geometry.attributes.position.array; for (let i = 0; i < particleCount; i++) { const i3 = i * 3; const x = posArray[i3]; const y = posArray[i3 + 1]; const z = posArray[i3 + 2]; const dist = Math.sqrt(x * x + y * y + z * z) || 1; // Store current positions as home targets for reform homePositions[i3] = x; homePositions[i3 + 1] = y; homePositions[i3 + 2] = z; // Radial explosion velocity + random scatter const force = (1.5 + Math.random() * 2.0); explodeVelocities[i3] = (x / dist) * force + (Math.random() - 0.5) * 1.5; explodeVelocities[i3 + 1] = (y / dist) * force + (Math.random() - 0.5) * 1.5; explodeVelocities[i3 + 2] = (z / dist) * force + (Math.random() - 0.5) * 1.5; } } }); function animate() { requestAnimationFrame(animate); const elapsed = clock.getElapsedTime(); const delta = clock.getDelta(); // Smoothly decay mouse speed mouseSpeed.value *= 0.92; // Smoothly interpolate mouse3D toward the target so the field follows fluidly mouse3D.lerp(mouse3DTarget, 0.12); material.uniforms.uTime.value = elapsed; glowMat.uniforms.uTime.value = elapsed; outerGlowMat.uniforms.uTime.value = elapsed; const posAttr = geometry.attributes.position; const posArray = posAttr.array; if (isExploded) { const timeSinceExplode = elapsed - explodeTime; const reformDelay = 1.5; const reformDuration = 3.0; if (timeSinceExplode < reformDelay) { // Phase 1: Explosion — particles fly outward with damping const damping = Math.max(0, 1.0 - timeSinceExplode * 0.6); for (let i = 0; i < particleCount; i++) { const i3 = i * 3; posArray[i3] += explodeVelocities[i3] * damping * 0.3; posArray[i3 + 1] += explodeVelocities[i3 + 1] * damping * 0.3; posArray[i3 + 2] += explodeVelocities[i3 + 2] * damping * 0.3; } } else { // Phase 2: Reform — lerp back to home positions const reformProgress = Math.min((timeSinceExplode - reformDelay) / reformDuration, 1.0); // Ease-in-out cubic const ease = reformProgress < 0.5 ? 4 * reformProgress * reformProgress * reformProgress : 1 - Math.pow(-2 * reformProgress + 2, 3) / 2; for (let i = 0; i < particleCount; i++) { const i3 = i * 3; const phase = phases[i]; // Add gentle swirl during reform const swirlAngle = elapsed * 0.3 + phase; const swirlStrength = (1.0 - ease) * 0.02; posArray[i3] += (homePositions[i3] - posArray[i3]) * ease * 0.05 + Math.cos(swirlAngle) * swirlStrength; posArray[i3 + 1] += (homePositions[i3 + 1] - posArray[i3 + 1]) * ease * 0.05 + Math.sin(swirlAngle * 0.7) * swirlStrength; posArray[i3 + 2] += (homePositions[i3 + 2] - posArray[i3 + 2]) * ease * 0.05 + Math.sin(swirlAngle * 1.3) * swirlStrength; } if (reformProgress >= 1.0) { isExploded = false; } } // Pulse the glow sphere bigger during explosion const explodePulse = timeSinceExplode < reformDelay ? 1 + Math.sin(timeSinceExplode * 8) * 0.5 * Math.max(0, 1 - timeSinceExplode / reformDelay) : 1; glowSphere.scale.setScalar(explodePulse * (1 + Math.sin(elapsed * 1.5) * 0.2)); outerGlow.scale.setScalar(explodePulse * 1.2 * (1 + Math.sin(elapsed * 1.5) * 0.2)); } else { for (let i = 0; i < particleCount; i++) { const i3 = i * 3; const x = posArray[i3]; const y = posArray[i3 + 1]; const z = posArray[i3 + 2]; const dist = Math.sqrt(x * x + y * y + z * z); const phase = phases[i]; // Orbital motion const angle = elapsed * (0.1 + (i % 50) * 0.002) + phase; const orbitRadius = dist + Math.sin(elapsed * 0.5 + phase) * 0.5; // Swirl effect const swirlX = Math.cos(angle) * velocities[i3] - Math.sin(angle) * velocities[i3 + 2]; const swirlZ = Math.sin(angle) * velocities[i3] + Math.cos(angle) * velocities[i3 + 2]; posArray[i3] += swirlX + Math.sin(elapsed * 0.3 + phase) * 0.005; posArray[i3 + 1] += velocities[i3 + 1] + Math.cos(elapsed * 0.4 + phase) * 0.005; posArray[i3 + 2] += swirlZ + Math.sin(elapsed * 0.5 + phase) * 0.005; // Pull back toward sphere if too far const newDist = Math.sqrt( posArray[i3] ** 2 + posArray[i3 + 1] ** 2 + posArray[i3 + 2] ** 2 ); if (newDist > 22) { const scale = 22 / newDist; posArray[i3] *= scale * 0.999; posArray[i3 + 1] *= scale * 0.999; posArray[i3 + 2] *= scale * 0.999; } // Store the base orbital position before displacement const baseX = posArray[i3]; const baseY = posArray[i3 + 1]; const baseZ = posArray[i3 + 2]; // Continuous noise field around the mouse — always active, moves with mouse const toMouseX = baseX - mouse3D.x; const toMouseY = baseY - mouse3D.y; const toMouseZ = baseZ - mouse3D.z; const distToMouse = Math.sqrt(toMouseX * toMouseX + toMouseY * toMouseY + toMouseZ * toMouseZ); const influenceRadius = 10.0; const fieldStrengthBase = 2.5; if (distToMouse < influenceRadius) { // Smooth hermite falloff for a natural field boundary const t = distToMouse / influenceRadius; const falloff = 1.0 - t * t * (3.0 - 2.0 * t); // Combine persistent field strength with mouse-speed boost const strength = falloff * (fieldStrengthBase + mouseSpeed.value * 5.0); const noiseScale = 0.15; const timeOffset = elapsed * 0.5; const nx = baseX * noiseScale + timeOffset; const ny = baseY * noiseScale + timeOffset * 0.7; const nz = baseZ * noiseScale + timeOffset * 0.5; // Directional FBM noise displacement — swirling field effect const displaceX = fbm(nx, ny, nz, 4) * strength * 0.15; const displaceY = fbm(nx + 31.7, ny + 47.3, nz + 13.1, 4) * strength * 0.15; const displaceZ = fbm(nx + 73.1, ny + 11.9, nz + 59.7, 4) * strength * 0.15; // Radial push away from mouse center const radialPush = falloff * falloff * 0.6; const invDist = distToMouse > 0.01 ? 1.0 / distToMouse : 0; // Apply displacement as an offset, scaled by delta-like factor so it doesn't accumulate const targetX = baseX + displaceX + toMouseX * invDist * radialPush; const targetY = baseY + displaceY + toMouseY * invDist * radialPush; const targetZ = baseZ + displaceZ + toMouseZ * invDist * radialPush; posArray[i3] = targetX; posArray[i3 + 1] = targetY; posArray[i3 + 2] = targetZ; } } } posAttr.needsUpdate = true; // Update connection lines let lineIdx = 0; const lPos = lines.geometry.attributes.position.array; const lCol = lines.geometry.attributes.color.array; const threshold = 2.5; for (let i = 0; i < particleCount && lineIdx < lineCount; i += 12) { for (let j = i + 12; j < particleCount && lineIdx < lineCount; j += 12) { const i3 = i * 3; const j3 = j * 3; const dx = posArray[i3] - posArray[j3]; const dy = posArray[i3 + 1] - posArray[j3 + 1]; const dz = posArray[i3 + 2] - posArray[j3 + 2]; const d = dx * dx + dy * dy + dz * dz; if (d < threshold * threshold) { const li = lineIdx * 6; lPos[li] = posArray[i3]; lPos[li + 1] = posArray[i3 + 1]; lPos[li + 2] = posArray[i3 + 2]; lPos[li + 3] = posArray[j3]; lPos[li + 4] = posArray[j3 + 1]; lPos[li + 5] = posArray[j3 + 2]; const alpha = 1 - Math.sqrt(d) / threshold; lCol[li] = colors[i3] * alpha; lCol[li + 1] = colors[i3 + 1] * alpha; lCol[li + 2] = colors[i3 + 2] * alpha; lCol[li + 3] = colors[j3] * alpha; lCol[li + 4] = colors[j3 + 1] * alpha; lCol[li + 5] = colors[j3 + 2] * alpha; lineIdx++; } } } // Clear unused lines for (let i = lineIdx; i < lineCount; i++) { const li = i * 6; lPos[li] = lPos[li + 1] = lPos[li + 2] = 0; lPos[li + 3] = lPos[li + 4] = lPos[li + 5] = 0; } lines.geometry.attributes.position.needsUpdate = true; lines.geometry.attributes.color.needsUpdate = true; // Pulse glow (only when not exploded — explosion handles its own pulse) if (!isExploded) { const pulse = 1 + Math.sin(elapsed * 1.5) * 0.2; glowSphere.scale.setScalar(pulse); outerGlow.scale.setScalar(pulse * 1.2); } controls.update(); renderer.render(scene, camera); } animate(); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); material.uniforms.uPixelRatio.value = renderer.getPixelRatio(); });