使用 Three.js 的 WebGL 小实验。催眠粒子漩涡与鼠标反应性运动。”

<style>* {margin: 0;padding: 0;box-sizing: border-box;}#container {position: fixed;width: 100%;height: 100%;background: linear-gradient(180deg,#0a0001 0%,#220002 50%,#430100 100%);overflow: hidden;}.glow {position: fixed;top: 0;left: 0;width: 100%;height: 100%;pointer-events: none;background: radial-gradient(circle at 50% 50%,rgba(255, 140, 0, 0.08) 0%,rgba(255, 0, 76, 0.1) 45%,transparent 70%);mix-blend-mode: screen;opacity: 0.85;}</style><script type="importmap">{"imports": {"three": "https://unpkg.com/three@0.162.0/build/three.module.js"}}</script><div id="container"></div><div class="glow"></div><script type="module">import * as THREE from 'three';let scene, camera, renderer, particles;let time = 0;const screenMouse = new THREE.Vector2(10000, 10000);const worldMouse = new THREE.Vector3();const lastWorldMouse = new THREE.Vector3();const mouseVelocity = new THREE.Vector3();const smoothedMouseVelocity = new THREE.Vector3();function generateShape(t, angle, phase) {const baseRadius = 26;const spiralTwist = 6;const pulse = Math.sin(t * Math.PI * 2 + phase);const radius = baseRadius * Math.sqrt(t) * (1 + 0.3 * pulse);let x = radius * Math.cos(angle + t * spiralTwist);let y = radius * Math.sin(angle + t * spiralTwist);let z = radius * Math.sin(t * 8 + phase) * 0.4;return { x, y, z };}const particleCount = 100000;function createParticleSystem() {const geometry = new THREE.BufferGeometry();const positions = new Float32Array(particleCount * 3);const colors = new Float32Array(particleCount * 3);const sizes = new Float32Array(particleCount);const angles = new Float32Array(particleCount);const originalPos = new Float32Array(particleCount * 3);const randomFactors = new Float32Array(particleCount);const colorPalette = [new THREE.Color('#ff9a00'),new THREE.Color('#ff004d'),new THREE.Color('#ffec00'),new THREE.Color('#ffffff'),new THREE.Color('#ff5500')];const goldenAngle = Math.PI * (3 - Math.sqrt(5));for(let i = 0; i < particleCount; i++) {const i3 = i * 3;const t = i / particleCount;const angle = i * goldenAngle;const pos = generateShape(t, angle, 0.8);positions[i3] = pos.x;positions[i3 + 1] = pos.y;positions[i3 + 2] = pos.z;originalPos[i3] = pos.x;originalPos[i3 + 1] = pos.y;originalPos[i3 + 2] = pos.z;angles[i] = angle;const colorT = t * (colorPalette.length - 1);const idx = Math.floor(colorT);const mix = colorT - idx;const c1 = colorPalette[idx];const c2 = colorPalette[Math.min(idx + 1, colorPalette.length - 1)];const color = new THREE.Color().lerpColors(c1, c2, mix);color.multiplyScalar(0.9 + Math.random() * 0.5);colors[i3] = color.r;colors[i3 + 1] = color.g;colors[i3 + 2] = color.b;sizes[i] = 0.7 * (1.1 - t * 0.3) * (0.6 + Math.random() * 0.7);randomFactors[i] = Math.random();}geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));geometry.setAttribute('angle', new THREE.BufferAttribute(angles, 1));geometry.setAttribute('originalPos', new THREE.BufferAttribute(originalPos, 3));geometry.setAttribute('randomFactor', new THREE.BufferAttribute(randomFactors, 1));const material = new THREE.ShaderMaterial({uniforms: {time: { value: 0 },mousePos: { value: new THREE.Vector3(10000, 10000, 0) },mouseVel: { value: new THREE.Vector3() }},vertexShader: `uniform float time;uniform vec3 mousePos;uniform vec3 mouseVel;attribute vec3 originalPos;attribute float size;attribute float angle;attribute float randomFactor;varying vec3 vColor;varying float vIntensity;varying float vRandomFactor;void main() {vColor = color;vRandomFactor = randomFactor;vec3 basePos = originalPos;float morphTime = time * 0.7;float pattern1 = sin(angle * 5.0 + morphTime) * cos(angle * 2.0);float pattern2 = cos(angle * 4.0 - morphTime) * sin(angle * 3.0);float blend = sin(morphTime * 0.6) * 0.5 + 0.5;float displacement = mix(pattern1, pattern2, blend);vec3 normOrig = normalize(originalPos + vec3(0.001));vec3 animatedPos = originalPos + normOrig * displacement * 3.5;vec3 pos = animatedPos;vec3 toMouse = mousePos - pos;float dist = length(toMouse);float influence = pow(smoothstep(35.0, 8.0, dist), 2.0);vIntensity = 0.0;if (influence > 0.001) {float velMag = length(mouseVel);float velInfluence = smoothstep(0.1, 2.0, velMag) * influence;vec3 forceDir = normalize(mouseVel + vec3(0.001));vec3 pushDir = normalize(pos - mousePos);vec3 dir = normalize(mix(forceDir, pushDir, 0.3));float dispMag = velInfluence * (3.5 + randomFactor * 4.0);pos += dir * dispMag;vIntensity = clamp(influence * 0.6 + velInfluence * 0.9, 0.0, 1.0);}pos += (animatedPos - pos) * 0.01;float breath = sin(time * 1.3 + angle) * 0.08;pos *= 1.0 + breath * (1.0 - influence * 0.7) * (0.6 + vRandomFactor * 0.7);vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);gl_Position = projectionMatrix * mvPos;float sizeFactor = 1.0 + vIntensity * 0.3;float perspective = 400.0 / -mvPos.z;gl_PointSize = size * sizeFactor * perspective * (0.6 + vRandomFactor * 0.7);}`,fragmentShader: `uniform float time;varying vec3 vColor;varying float vIntensity;varying float vRandomFactor;void main() {vec2 pc = gl_PointCoord * 2.0 - 1.0;float dist = length(pc);if (dist > 1.0) discard;float core = smoothstep(0.2, 0.0, dist) * 0.6;float glow = exp(-dist * 2.4) * 0.7;vec3 highlight = vec3(1.0, 0.9, 0.6);vec3 finalColor = mix(vColor, highlight, vIntensity * 0.75);float shimmer = 1.0 + sin((time * 60.0 + vRandomFactor * 150.0) * (0.7 + vIntensity * 0.3)) * 0.15 * (1.0 - dist * 0.5);float baseAlpha = (core + glow) * clamp(0.4 + vIntensity * 0.6, 0.0, 1.0);float finalAlpha = baseAlpha * shimmer;gl_FragColor = vec4(finalColor, finalAlpha);}`,transparent: true,depthWrite: false,blending: THREE.AdditiveBlending,vertexColors: true});material.uniforms.time = { value: 0 };return new THREE.Points(geometry, material);}function init() {scene = new THREE.Scene();camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);camera.position.z = 100;renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });renderer.setSize(window.innerWidth, window.innerHeight);renderer.setClearColor(0x000000, 0);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));document.getElementById('container').appendChild(renderer.domElement);particles = createParticleSystem();scene.add(particles);document.addEventListener('mousemove', e => {screenMouse.x = (e.clientX / window.innerWidth) * 2 - 1;screenMouse.y = -(e.clientY / window.innerHeight) * 2 + 1;}, { passive: true });document.addEventListener('mouseleave', () => {screenMouse.x = 10000;screenMouse.y = 10000;});window.addEventListener('resize', onWindowResize);}const raycaster = new THREE.Raycaster();const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);function updateMouseAndUniforms() {lastWorldMouse.copy(worldMouse);raycaster.setFromCamera(screenMouse, camera);const intersect = new THREE.Vector3();if (screenMouse.x < 9999 && raycaster.ray.intersectPlane(plane, intersect)) {worldMouse.copy(intersect);if (lastWorldMouse.x < 9000) {mouseVelocity.subVectors(worldMouse, lastWorldMouse);} else {mouseVelocity.set(0, 0, 0);}} else {worldMouse.set(10000, 10000, 0);mouseVelocity.set(0, 0, 0);}smoothedMouseVelocity.lerp(mouseVelocity, 0.15);mouseVelocity.multiplyScalar(0.92);if (particles) {particles.material.uniforms.mousePos.value.copy(worldMouse);particles.material.uniforms.mouseVel.value.copy(smoothedMouseVelocity);particles.material.uniforms.time.value = time;}}function animate() {requestAnimationFrame(animate);time = performance.now() * 0.0007;updateMouseAndUniforms();camera.position.x = Math.sin(time * 0.3) * 12;camera.position.y = Math.cos(time * 0.4) * 12;camera.lookAt(0, 0, 0);renderer.render(scene, camera);}function onWindowResize() {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth, window.innerHeight);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));}init();animate();</script>
源码:
https://codepen.io/VoXelo/pen/qEBQqBR
体验:
https://codepen.io/VoXelo/ben/qEBQqBR

