feat(me): 重构粒子系统与动画加载逻辑

- 引入独立的ParticleSystem类管理粒子系统
- 添加土星动画作为默认加载动画
- 实现动态动画切换机制,避免资源冲突
- 优化粒子爆炸与散射效果调用方式
- 移除旧版粒子初始化与物理计算代码
- 更新手势交互与UI状态同步逻辑
- 修复动画模式下按钮显示时机问题
This commit is contained in:
hehh
2025-12-10 00:04:26 +08:00
parent cff4db87af
commit 1ed730a3d2

257
me.html
View File

@@ -348,6 +348,8 @@
opacity: 1; opacity: 1;
} }
/* 已移除动画切换按钮样式,避免同时加载多个动画资源 */
@keyframes breathe { @keyframes breathe {
0%, 100% { 0%, 100% {
opacity: 0.6 opacity: 0.6
@@ -437,6 +439,9 @@
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>
<!-- 添加动画类库 -->
<script src="js/ParticleSystem.js"></script>
</head> </head>
<body> <body>
@@ -687,99 +692,46 @@
composer.addPass(bloomPass); composer.addPass(bloomPass);
} }
// --- 粒子系统 --- // 初始化粒子系统
const geometry = new THREE.BufferGeometry(); const particleSystem = new ParticleSystem({
const positions = new Float32Array(CONFIG.particleCount * 3); particleCount: CONFIG.particleCount,
const targets = new Float32Array(CONFIG.particleCount * 3); theme: ENV.theme,
const origin = new Float32Array(CONFIG.particleCount * 3); callbacks: {
const velocities = new Float32Array(CONFIG.particleCount * 3); onInit: (system) => {
const sizes = new Float32Array(CONFIG.particleCount); scene.add(system);
const seeds = new Float32Array(CONFIG.particleCount);
const simplex = new SimplexNoise();
for (let i = 0; i < CONFIG.particleCount; i++) {
const i3 = i * 3;
// 黄金螺旋球
const y = 1 - (i / (CONFIG.particleCount - 1)) * 2;
const radius = Math.sqrt(1 - y * y);
const theta = i * 2.39996;
const r = 280;
const x = Math.cos(theta) * radius * r;
const z = Math.sin(theta) * radius * r;
const py = y * r;
positions[i3] = x;
origin[i3] = x;
targets[i3] = x;
positions[i3 + 1] = py;
origin[i3 + 1] = py;
targets[i3 + 1] = py;
positions[i3 + 2] = z;
origin[i3 + 2] = z;
targets[i3 + 2] = z;
// 白天粒子略大,增强可见度
sizes[i] = (Math.random() * 2.5 + 0.5) * (ENV.theme === 'day' ? 1.3 : 1.0);
seeds[i] = Math.random();
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
geometry.setAttribute('seed', new THREE.BufferAttribute(seeds, 1));
const paletteA = ENV.theme === 'day' ? new THREE.Color(0x6ec3ff) : new THREE.Color(0x00ffff);
const paletteB = ENV.theme === 'day' ? new THREE.Color(0xffb4c8) : new THREE.Color(0xff7af3);
const material = new THREE.ShaderMaterial({
uniforms: {
scale: {value: window.innerHeight / 2},
baseColor: {value: CONFIG.colors.base},
activeColor: {value: CONFIG.colors.active},
mixVal: {value: 0.0},
time: {value: 0.0},
paletteA: {value: paletteA},
paletteB: {value: paletteB},
nebulaIntensity: {value: 0.0}
}, },
vertexShader: ` onUpdate: (system) => {
attribute float size; // 粒子系统更新后的回调
attribute float seed; },
varying float vSeed; onAddObject: (object) => {
void main() { scene.add(object);
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); },
gl_PointSize = size * ( 500.0 / -mvPosition.z ); onRemoveObject: (object) => {
gl_Position = projectionMatrix * mvPosition; scene.remove(object);
vSeed = seed;
} }
`,
fragmentShader: `
uniform vec3 baseColor;
uniform vec3 activeColor;
uniform vec3 paletteA;
uniform vec3 paletteB;
uniform float mixVal;
uniform float time;
uniform float nebulaIntensity;
varying float vSeed;
void main() {
float r = length(gl_PointCoord - vec2(0.5));
if (r > 0.5) discard;
vec3 baseMix = mix(baseColor, activeColor, mixVal);
float drift = 0.5 + 0.5 * sin(time * 0.15 + vSeed * 6.28318);
vec3 nebula = mix(paletteA, paletteB, drift);
vec3 finalColor = mix(baseMix, nebula, nebulaIntensity);
gl_FragColor = vec4(finalColor, 1.0);
} }
`,
blending: CONFIG.blending,
depthTest: false,
transparent: true
}); });
const particleSystem = new THREE.Points(geometry, material); // 根据条件动态加载动画
scene.add(particleSystem); function loadAnimation(animationType) {
// 移除当前所有动画
while (particleSystem.animations.length > 0) {
particleSystem.removeAnimation(particleSystem.animations[0]);
}
// 根据类型动态加载动画
switch (animationType) {
case 'saturn':
// 土星动画作为默认动画
const saturnAnimation = new SaturnAnimation(particleSystem);
particleSystem.addAnimation(saturnAnimation);
break;
case 'particles':
default:
// 基本粒子动画不需要额外对象
break;
}
}
// 手势光标 // 手势光标
const cursorMat = new THREE.MeshBasicMaterial({ const cursorMat = new THREE.MeshBasicMaterial({
@@ -801,98 +753,10 @@
function animate() { function animate() {
const time = clock.getElapsedTime(); const time = clock.getElapsedTime();
const delta = Math.min(clock.getDelta(), 0.1);
// 颜色插值 // 更新粒子系统
material.uniforms.mixVal.value += (targetMix - material.uniforms.mixVal.value) * 0.1; const targetMix = particleSystem.update(time, APP_STATE.mode, APP_STATE.handCount, APP_STATE.handL, APP_STATE.handR);
material.uniforms.time.value = time;
if (ENV.theme === 'night') {
material.uniforms.nebulaIntensity.value = APP_STATE.mode === 'UNLOCKED' ? 0.55 : 0.35;
} else {
material.uniforms.nebulaIntensity.value = APP_STATE.mode === 'UNLOCKED' ? 0.22 : 0.12;
}
// 1. 形状更新 (锁定模式:呼吸球)
if (APP_STATE.mode === 'LOCKED') {
targetMix = APP_STATE.handCount > 0 ? 0.6 : 0.0;
const ns = 0.002;
const ts = time * 0.15;
for (let i = 0; i < CONFIG.particleCount; i++) {
const i3 = i * 3;
const ox = origin[i3];
const oy = origin[i3 + 1];
const oz = origin[i3 + 2];
const noise = simplex.noise3D(ox * ns + ts, oy * ns, oz * ns + ts);
let offX = 0, offY = 0;
if (APP_STATE.handCount === 1) {
offX = APP_STATE.handL.x * 0.15;
offY = APP_STATE.handL.y * 0.15;
}
const scale = 1 + noise * 0.3;
targets[i3] = ox * scale + offX;
targets[i3 + 1] = oy * scale + offY;
targets[i3 + 2] = oz * scale;
}
} else {
targetMix = 1.0;
}
// 2. 物理迭代
for (let i = 0; i < CONFIG.particleCount; i++) {
const i3 = i * 3;
const px = positions[i3];
const py = positions[i3 + 1];
const pz = positions[i3 + 2];
// 归位力
const stiff = APP_STATE.mode === 'LOCKED' ? 0.03 : 0.05;
velocities[i3] += (targets[i3] - px) * stiff;
velocities[i3 + 1] += (targets[i3 + 1] - py) * stiff;
velocities[i3 + 2] += (targets[i3 + 2] - pz) * stiff;
// --- 交互物理 ---
if (APP_STATE.handCount === 1) {
// 单手黑洞 (增强吸附感)
const hx = APP_STATE.handL.x;
const hy = APP_STATE.handL.y;
const dx = hx - px;
const dy = hy - py;
const distSq = dx * dx + dy * dy;
if (distSq < 150000) {
const f = (150000 - distSq) / 150000;
velocities[i3] += dx * f * 0.05;
velocities[i3 + 1] += dy * f * 0.05;
velocities[i3 + 2] += Math.sin(time * 10 + distSq * 0.0001) * 8 * f; // 波纹效果
}
} else if (APP_STATE.handCount === 2) {
// 双手斥力
[APP_STATE.handL, APP_STATE.handR].forEach(h => {
const dx = px - h.x;
const dy = py - h.y;
const distSq = dx * dx + dy * dy;
if (distSq < 80000) {
const f = (80000 - distSq) / 80000;
velocities[i3] -= dx * f * 0.3;
velocities[i3 + 1] -= dy * f * 0.3;
velocities[i3 + 2] += 15 * f;
}
});
}
velocities[i3] *= 0.90;
velocities[i3 + 1] *= 0.90;
velocities[i3 + 2] *= 0.90;
positions[i3] += velocities[i3];
positions[i3 + 1] += velocities[i3 + 1];
positions[i3 + 2] += velocities[i3 + 2];
}
geometry.attributes.position.needsUpdate = true;
if (APP_STATE.handCount === 0) particleSystem.rotation.y += 0.002;
cursorL.position.set(APP_STATE.handL.x, APP_STATE.handL.y, 0); cursorL.position.set(APP_STATE.handL.x, APP_STATE.handL.y, 0);
cursorR.position.set(APP_STATE.handR.x, APP_STATE.handR.y, 0); cursorR.position.set(APP_STATE.handR.x, APP_STATE.handR.y, 0);
@@ -912,7 +776,6 @@
// 进入动画模式 // 进入动画模式
function enterAnimationMode() { function enterAnimationMode() {
// 不再检查APP_STATE.isLoaded状态确保函数总是执行
APP_STATE.isLoaded = true; APP_STATE.isLoaded = true;
// 隐藏加载屏幕 // 隐藏加载屏幕
@@ -925,6 +788,13 @@
} }
}, 1000); }, 1000);
// 默认使用土星动画
setTimeout(() => {
if (!CAMERA_STATE.enabled && !DOM_CACHE.interactionModeBtn) {
loadAnimation('saturn');
}
}, 1500);
// 如果摄像头已启用更新UI以反映交互模式 // 如果摄像头已启用更新UI以反映交互模式
if (CAMERA_STATE.enabled) { if (CAMERA_STATE.enabled) {
safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main); safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main);
@@ -935,7 +805,7 @@
if (!CAMERA_STATE.enabled && !DOM_CACHE.interactionModeBtn) { if (!CAMERA_STATE.enabled && !DOM_CACHE.interactionModeBtn) {
showInteractionModeButton(); showInteractionModeButton();
} }
}, 1500); // 延迟显示,确保页面已完成过渡 }, 1500);
} }
} }
@@ -1007,7 +877,7 @@
safeClass(DOM_CACHE.exitBtn, 'add', 'visible'); safeClass(DOM_CACHE.exitBtn, 'add', 'visible');
// 粒子爆炸效果 // 粒子爆炸效果
explode(300); particleSystem.explode(300);
startNarrative(); startNarrative();
} }
@@ -1032,7 +902,7 @@
APP_STATE.unlockProgress = 0; APP_STATE.unlockProgress = 0;
clearTimeout(narrativeTimer); clearTimeout(narrativeTimer);
explode(50); particleSystem.explode(50);
// 退出后重新显示交互模式按钮(如果之前已启用摄像头) // 退出后重新显示交互模式按钮(如果之前已启用摄像头)
if (CAMERA_STATE.enabled) { if (CAMERA_STATE.enabled) {
@@ -1040,14 +910,6 @@
} }
} }
function explode(f) {
for (let i = 0; i < CONFIG.particleCount; i++) {
velocities[i * 3] += (Math.random() - 0.5) * f;
velocities[i * 3 + 1] += (Math.random() - 0.5) * f;
velocities[i * 3 + 2] += (Math.random() - 0.5) * f;
}
}
function startNarrative() { function startNarrative() {
let idx = 0; let idx = 0;
const nTitle = DOM_CACHE.nTitle; const nTitle = DOM_CACHE.nTitle;
@@ -1067,17 +929,11 @@
layer.classList.add('show-text'); layer.classList.add('show-text');
// 粒子散开做背景 // 粒子散开做背景
for (let i = 0; i < CONFIG.particleCount; i++) { particleSystem.scatter();
const a = i * 0.1;
const r = 900 + Math.random() * 200;
targets[i * 3] = Math.cos(a) * r;
targets[i * 3 + 1] = (Math.random() - 0.5) * 300;
targets[i * 3 + 2] = Math.sin(a) * r;
}
narrativeTimer = setTimeout(() => { narrativeTimer = setTimeout(() => {
layer.classList.remove('show-text'); layer.classList.remove('show-text');
explode(10); particleSystem.explode(10);
narrativeTimer = setTimeout(next, 1000); narrativeTimer = setTimeout(next, 1000);
}, 3000); }, 3000);
@@ -1222,7 +1078,6 @@
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight);
material.uniforms.scale.value = window.innerHeight / 2;
}); });
// 循环播放加载文案 // 循环播放加载文案
@@ -1251,7 +1106,7 @@
safeUpdateText(DOM_CACHE.countdownHint, CONTENT.hints.countdownHint.replace('{second}', countdown + '')); safeUpdateText(DOM_CACHE.countdownHint, CONTENT.hints.countdownHint.replace('{second}', countdown + ''));
} else { } else {
clearInterval(CAMERA_STATE.countdownInterval); clearInterval(CAMERA_STATE.countdownInterval);
enterAnimationMode(); // 确保调用进入动画模式 enterAnimationMode();
} }
}, 1000); }, 1000);
}, 3000); }, 3000);