feat(me): 重构粒子系统与动画加载逻辑
- 引入独立的ParticleSystem类管理粒子系统 - 添加土星动画作为默认加载动画 - 实现动态动画切换机制,避免资源冲突 - 优化粒子爆炸与散射效果调用方式 - 移除旧版粒子初始化与物理计算代码 - 更新手势交互与UI状态同步逻辑 - 修复动画模式下按钮显示时机问题
This commit is contained in:
257
me.html
257
me.html
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user