import * as THREE from "three"; import { blobFragment, blobVertex, particleFragment, particleVertex } from "./shaders"; // 三状态目标值 —— 每帧向目标插值,无硬切 const STATES = { idle: { freq: 1.15, amp: 0.24, speed: 0.22, heat: 0.0, scale: 1.0, attract: 0.12 }, thinking: { freq: 2.6, amp: 0.27, speed: 0.95, heat: 0.7, scale: 0.86, attract: 1.0 }, answering: { freq: 1.9, amp: 0.46, speed: 0.5, heat: 0.22, scale: 1.09, attract: 0.0 }, }; const lerp = (a, b, t) => a + (b - a) * t; export function createMind(canvas, { reduced = false } = {}) { const isMobile = window.innerWidth < 880; const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2)); renderer.setSize(window.innerWidth, window.innerHeight); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.1, 30); camera.position.z = 6; const group = new THREE.Group(); scene.add(group); /* ---- 流体球 ---- */ const blobUniforms = { uTime: { value: 0 }, uFreq: { value: STATES.idle.freq }, uAmp: { value: STATES.idle.amp }, uSpeed: { value: STATES.idle.speed }, uHeat: { value: 0 }, uOpacity: { value: 1 }, uColorA: { value: new THREE.Color("#22d3ee") }, uColorB: { value: new THREE.Color("#a78bfa") }, }; const blob = new THREE.Mesh( new THREE.IcosahedronGeometry(1.35, isMobile ? 48 : 96), new THREE.ShaderMaterial({ vertexShader: blobVertex, fragmentShader: blobFragment, uniforms: blobUniforms, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, }) ); group.add(blob); /* ---- 伴生粒子 ---- */ const COUNT = isMobile ? 900 : 2000; const seeds = new Float32Array(COUNT * 3); const radii = new Float32Array(COUNT); const speeds = new Float32Array(COUNT); for (let i = 0; i < COUNT; i++) { seeds[i * 3] = Math.random(); seeds[i * 3 + 1] = Math.random(); seeds[i * 3 + 2] = Math.random(); radii[i] = 1.7 + Math.random() * 1.5; speeds[i] = 0.25 + Math.random() * 0.6; } const pGeo = new THREE.BufferGeometry(); pGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(COUNT * 3), 3)); pGeo.setAttribute("aSeed", new THREE.BufferAttribute(seeds, 3)); pGeo.setAttribute("aRadius", new THREE.BufferAttribute(radii, 1)); pGeo.setAttribute("aSpeed", new THREE.BufferAttribute(speeds, 1)); const particleUniforms = { uTime: { value: 0 }, uAttract: { value: STATES.idle.attract }, uBurst: { value: 0 }, uDir: { value: -1 }, uSize: { value: isMobile ? 9 : 12 }, uMouse: { value: new THREE.Vector3(999, 999, 999) }, uMouseForce: { value: 0 }, uGrab: { value: 0 }, uHeat: { value: 0 }, uOpacity: { value: 1 }, uColorA: blobUniforms.uColorA, uColorB: blobUniforms.uColorB, }; const particles = new THREE.Points( pGeo, new THREE.ShaderMaterial({ vertexShader: particleVertex, fragmentShader: particleFragment, uniforms: particleUniforms, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, }) ); group.add(particles); /* ---- 状态机 ---- */ let current = { ...STATES.idle }; let target = STATES.idle; let stateName = "idle"; let burst = 0; const colorA = blobUniforms.uColorA.value; const colorB = blobUniforms.uColorB.value; const targetColorA = colorA.clone(); const targetColorB = colorB.clone(); // 停靠位置:side -1 内容在右 → 流体去左;1 → 右;0 → 居中偏后 const dock = new THREE.Vector3(); const dockTarget = new THREE.Vector3(); const pointer = { x: 0, y: 0 }; function dockFor(side) { if (window.innerWidth < 880) { // 移动端:居中上移、退后、缩小,让内容可读 return new THREE.Vector3(side * 0.3, 1.25, -1.6); } if (side === 0) return new THREE.Vector3(0, 0.95, -2.8); return new THREE.Vector3(side * 2.05, 0.1, -0.7); } let side = 1; let opacityTarget = 1; let hasPointer = false; let mouseSnapped = false; // 按压牵引弹簧(欠阻尼 → 过冲回弹的阻尼感) let grab = 0; let grabVel = 0; let grabTarget = 0; const mouseRay = new THREE.Vector3(); const mouseLocal = new THREE.Vector3(); const api = { setState(name) { if (name === stateName || reduced) return; stateName = name; target = STATES[name]; if (name === "answering") burst = 1; // 喷发脉冲,随帧衰减 }, setSide(s) { side = s; dockTarget.copy(dockFor(s)); particleUniforms.uDir.value = s === 0 ? 0 : -s; // 粒子喷向内容区 opacityTarget = s === 0 ? 0.42 : 1; // 居中停靠时减淡,保证内容可读 }, setPalette([a, b]) { targetColorA.set(a); targetColorB.set(b); }, get state() { return stateName; }, debug() { return { stateName, side, dock: dock.toArray(), dockTarget: dockTarget.toArray(), group: group.position.toArray() }; }, }; api.setSide(1); dock.copy(dockTarget); group.position.copy(dock); window.addEventListener("pointermove", (e) => { pointer.x = (e.clientX / window.innerWidth) * 2 - 1; pointer.y = (e.clientY / window.innerHeight) * 2 - 1; hasPointer = true; }); window.addEventListener("pointerdown", (e) => { pointer.x = (e.clientX / window.innerWidth) * 2 - 1; pointer.y = (e.clientY / window.innerHeight) * 2 - 1; hasPointer = true; grabTarget = 1; }); const release = () => { grabTarget = 0; }; window.addEventListener("pointerup", release); window.addEventListener("pointercancel", release); window.addEventListener("blur", release); window.addEventListener("resize", () => { renderer.setSize(window.innerWidth, window.innerHeight); camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); api.setSide(side); }); /* ---- 帧循环(由外部 ticker 驱动) ---- */ const clock = new THREE.Clock(); function tick() { const dt = Math.min(clock.getDelta(), 0.05); const t = clock.elapsedTime; const k = 1 - Math.pow(0.0014, dt); // 帧率无关的平滑系数 current.freq = lerp(current.freq, target.freq, k); current.amp = lerp(current.amp, target.amp, k); current.speed = lerp(current.speed, target.speed, k); current.heat = lerp(current.heat, target.heat, k); current.scale = lerp(current.scale, target.scale, k); current.attract = lerp(current.attract, target.attract, k); burst = lerp(burst, 0, 1 - Math.pow(0.25, dt)); // 脉冲衰减 blobUniforms.uTime.value = reduced ? t * 0.25 : t; blobUniforms.uFreq.value = current.freq; blobUniforms.uAmp.value = current.amp; blobUniforms.uSpeed.value = current.speed; blobUniforms.uHeat.value = current.heat; particleUniforms.uTime.value = reduced ? t * 0.25 : t; particleUniforms.uAttract.value = current.attract; particleUniforms.uBurst.value = burst; particleUniforms.uHeat.value = current.heat; colorA.lerp(targetColorA, k); colorB.lerp(targetColorB, k); const op = lerp(blobUniforms.uOpacity.value, opacityTarget, k); blobUniforms.uOpacity.value = op; particleUniforms.uOpacity.value = op; const breathe = 1 + Math.sin(t * 1.4) * 0.018; group.scale.setScalar(current.scale * breathe); dock.lerp(dockTarget, 1 - Math.pow(0.02, dt)); group.position.set( dock.x + pointer.x * 0.14, dock.y - pointer.y * 0.1, dock.z ); group.rotation.y += dt * 0.12; group.rotation.x = pointer.y * 0.08; // 鼠标牵引:把指针投影到流体所在深度平面,转到 group 局部空间后平滑跟随 if (hasPointer && !reduced) { mouseRay.set(pointer.x, -pointer.y, 0.5).unproject(camera).sub(camera.position).normalize(); const planeDist = (group.position.z - camera.position.z) / mouseRay.z; mouseLocal.copy(camera.position).addScaledVector(mouseRay, planeDist); group.updateMatrixWorld(); group.worldToLocal(mouseLocal); if (!mouseSnapped) { particleUniforms.uMouse.value.copy(mouseLocal); mouseSnapped = true; } else { particleUniforms.uMouse.value.lerp(mouseLocal, 1 - Math.pow(0.01, dt)); } particleUniforms.uMouseForce.value = lerp(particleUniforms.uMouseForce.value, 0.85, k); // 按压牵引:欠阻尼弹簧积分 —— 按下缓冲吸入,松开过冲散开再归位 grabVel += (grabTarget - grab) * 26 * dt; grabVel *= Math.exp(-3.2 * dt); grab += grabVel * dt; particleUniforms.uGrab.value = grab; } renderer.render(scene, camera); } return { ...api, tick }; }