feat(me): 重构粒子系统与动画加载逻辑
- 引入独立的ParticleSystem类管理粒子系统 - 添加土星动画作为默认加载动画 - 实现动态动画切换机制,避免资源冲突 - 优化粒子爆炸与散射效果调用方式 - 移除旧版粒子初始化与物理计算代码 - 更新手势交互与UI状态同步逻辑 - 修复动画模式下按钮显示时机问题
This commit is contained in:
257
me.html
257
me.html
@@ -348,6 +348,8 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 已移除动画切换按钮样式,避免同时加载多个动画资源 */
|
||||
|
||||
@keyframes breathe {
|
||||
0%, 100% {
|
||||
opacity: 0.6
|
||||
@@ -437,6 +439,9 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.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>
|
||||
<body>
|
||||
|
||||
@@ -687,99 +692,46 @@
|
||||
composer.addPass(bloomPass);
|
||||
}
|
||||
|
||||
// --- 粒子系统 ---
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(CONFIG.particleCount * 3);
|
||||
const targets = new Float32Array(CONFIG.particleCount * 3);
|
||||
const origin = new Float32Array(CONFIG.particleCount * 3);
|
||||
const velocities = new Float32Array(CONFIG.particleCount * 3);
|
||||
const sizes = new Float32Array(CONFIG.particleCount);
|
||||
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}
|
||||
// 初始化粒子系统
|
||||
const particleSystem = new ParticleSystem({
|
||||
particleCount: CONFIG.particleCount,
|
||||
theme: ENV.theme,
|
||||
callbacks: {
|
||||
onInit: (system) => {
|
||||
scene.add(system);
|
||||
},
|
||||
vertexShader: `
|
||||
attribute float size;
|
||||
attribute float seed;
|
||||
varying float vSeed;
|
||||
void main() {
|
||||
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
|
||||
gl_PointSize = size * ( 500.0 / -mvPosition.z );
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
vSeed = seed;
|
||||
onUpdate: (system) => {
|
||||
// 粒子系统更新后的回调
|
||||
},
|
||||
onAddObject: (object) => {
|
||||
scene.add(object);
|
||||
},
|
||||
onRemoveObject: (object) => {
|
||||
scene.remove(object);
|
||||
}
|
||||
`,
|
||||
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({
|
||||
@@ -801,98 +753,10 @@
|
||||
|
||||
function animate() {
|
||||
const time = clock.getElapsedTime();
|
||||
const delta = Math.min(clock.getDelta(), 0.1);
|
||||
|
||||
// 颜色插值
|
||||
material.uniforms.mixVal.value += (targetMix - material.uniforms.mixVal.value) * 0.1;
|
||||
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;
|
||||
// 更新粒子系统
|
||||
const targetMix = particleSystem.update(time, APP_STATE.mode, APP_STATE.handCount, APP_STATE.handL, APP_STATE.handR);
|
||||
|
||||
cursorL.position.set(APP_STATE.handL.x, APP_STATE.handL.y, 0);
|
||||
cursorR.position.set(APP_STATE.handR.x, APP_STATE.handR.y, 0);
|
||||
@@ -912,7 +776,6 @@
|
||||
|
||||
// 进入动画模式
|
||||
function enterAnimationMode() {
|
||||
// 不再检查APP_STATE.isLoaded状态,确保函数总是执行
|
||||
APP_STATE.isLoaded = true;
|
||||
|
||||
// 隐藏加载屏幕
|
||||
@@ -925,6 +788,13 @@
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 默认使用土星动画
|
||||
setTimeout(() => {
|
||||
if (!CAMERA_STATE.enabled && !DOM_CACHE.interactionModeBtn) {
|
||||
loadAnimation('saturn');
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
// 如果摄像头已启用,更新UI以反映交互模式
|
||||
if (CAMERA_STATE.enabled) {
|
||||
safeUpdateText(DOM_CACHE.mainHint, CONTENT.hints.main);
|
||||
@@ -935,7 +805,7 @@
|
||||
if (!CAMERA_STATE.enabled && !DOM_CACHE.interactionModeBtn) {
|
||||
showInteractionModeButton();
|
||||
}
|
||||
}, 1500); // 延迟显示,确保页面已完成过渡
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,7 +877,7 @@
|
||||
safeClass(DOM_CACHE.exitBtn, 'add', 'visible');
|
||||
|
||||
// 粒子爆炸效果
|
||||
explode(300);
|
||||
particleSystem.explode(300);
|
||||
|
||||
startNarrative();
|
||||
}
|
||||
@@ -1032,7 +902,7 @@
|
||||
|
||||
APP_STATE.unlockProgress = 0;
|
||||
clearTimeout(narrativeTimer);
|
||||
explode(50);
|
||||
particleSystem.explode(50);
|
||||
|
||||
// 退出后重新显示交互模式按钮(如果之前已启用摄像头)
|
||||
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() {
|
||||
let idx = 0;
|
||||
const nTitle = DOM_CACHE.nTitle;
|
||||
@@ -1067,17 +929,11 @@
|
||||
layer.classList.add('show-text');
|
||||
|
||||
// 粒子散开做背景
|
||||
for (let i = 0; i < CONFIG.particleCount; i++) {
|
||||
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;
|
||||
}
|
||||
particleSystem.scatter();
|
||||
|
||||
narrativeTimer = setTimeout(() => {
|
||||
layer.classList.remove('show-text');
|
||||
explode(10);
|
||||
particleSystem.explode(10);
|
||||
narrativeTimer = setTimeout(next, 1000);
|
||||
}, 3000);
|
||||
|
||||
@@ -1222,7 +1078,6 @@
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.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 + ''));
|
||||
} else {
|
||||
clearInterval(CAMERA_STATE.countdownInterval);
|
||||
enterAnimationMode(); // 确保调用进入动画模式
|
||||
enterAnimationMode();
|
||||
}
|
||||
}, 1000);
|
||||
}, 3000);
|
||||
|
||||
Reference in New Issue
Block a user