Files
home/me.html
hehh 3031355836 feat(webgl): 重构粒子系统与手势交互引擎
- 重写粒子物理系统,支持速度与目标点分离的Verlet积分
- 新增辉光后期处理(UnrealBloomPass)增强视觉表现
- 优化手势识别引擎,提高捏合与滑动手势精度
- 修复摄像头画面镜像问题,改善交互准确性
- 增加叙事模式自动播放与手动切换功能
- 改进UI布局与加载动画,提升用户体验
- 调整粒子形态生成算法,新增黑洞、螺旋等预设
- 优化移动端渲染性能,限制高密度屏幕采样率
2025-12-04 00:32:11 +08:00

625 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>DEEP SPACE | INTERSTELLAR NARRATIVE</title>
<style>
:root { --bg: #000000; --accent: #FFFFFF; }
body { margin: 0; overflow: hidden; background: var(--bg); font-family: 'Courier New', monospace; }
/* UI 层 */
#ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
/* 顶部 HUD */
.hud-top {
position: absolute; top: 20px; left: 20px;
display: flex; flex-direction: column; gap: 5px;
color: rgba(255,255,255,0.8); font-size: 12px; letter-spacing: 2px;
text-shadow: 0 0 10px rgba(255,255,255,0.5);
}
/* 状态指示器 */
.status-box {
border-left: 3px solid #FFF;
padding-left: 10px;
background: linear-gradient(90deg, rgba(255,255,255,0.1), transparent);
}
/* 核心手势提示 */
.gesture-hint {
position: absolute; bottom: 30px; width: 100%; text-align: center;
color: #FFF; font-size: 14px; opacity: 0.7; letter-spacing: 4px;
animation: breathe 4s infinite ease-in-out;
}
/* 加载页 */
#loader {
position: fixed; inset: 0; background: #000; z-index: 100;
display: flex; align-items: center; justify-content: center;
flex-direction: column; color: #FFF; transition: opacity 0.8s;
}
.loader-bar { width: 200px; height: 2px; background: #333; margin-top: 20px; position: relative; overflow: hidden; }
.loader-progress { position: absolute; left: 0; top: 0; height: 100%; width: 0%; background: #FFF; box-shadow: 0 0 15px #FFF; transition: width 0.2s; }
@keyframes breathe { 0%,100%{opacity:0.4} 50%{opacity:1} }
/* 隐藏视频流 */
video { display: none; }
</style>
<!-- 核心库 (国内CDN) -->
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/r128/three.min.js"></script>
<!-- 后处理库 (Bloom) -->
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<!-- MediaPipe Hands -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<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>
</head>
<body>
<div id="loader">
<div style="font-weight:bold; letter-spacing:5px;">NEURAL LINKING...</div>
<div class="loader-bar"><div class="loader-progress" id="load-bar"></div></div>
<div style="font-size:10px; margin-top:10px; opacity:0.6;">需授权摄像头以接入神经元网络</div>
</div>
<div id="ui-layer">
<div class="hud-top">
<div class="status-box">
<div>SYS: <span id="fps">60</span> FPS</div>
<div>NET: <span id="hand-state">SEARCHING</span></div>
<div>MODE: <span id="mode-txt">IDLE_COSMOS</span></div>
</div>
</div>
<div class="gesture-hint" id="hint-txt">双手合十 · 解锁档案</div>
</div>
<video id="input-video"></video>
<div id="canvas-container"></div>
<script>
/**
* ============================================================================
* 1. 核心配置与状态机
* ============================================================================
*/
const CONFIG = {
particleCount: 2000, // 粒子总数,越多越细腻但越卡
baseSize: 3.0, // 基础粒子大小
bloomStrength: 1.2, // 辉光强度 (AAA级对比度关键)
bloomThreshold: 0.1, // 辉光阈值
bloomRadius: 0.5, // 辉光扩散半径
colors: {
primary: new THREE.Color(0xFFFFFF),
active: new THREE.Color(0xAAAAFF) // 手势激活时的微蓝偏色
}
};
const STATE = {
isUnlocked: false, // 是否已解锁档案
handDetected: false,
gesture: 'NONE', // 当前手势
narrativeIndex: 0, // 叙事进度
targetScale: 1, // 宇宙缩放目标
rotationSpeed: { x: 0, y: 0.05 }, // 自动旋转速度
interactionForce: new THREE.Vector3(0,0,0) // 手势产生的物理力场
};
const NARRATIVE_TEXTS = [
["HONESTY", "提倡者"],
["INFJ-A", "1% 的宇宙"],
["DEPTH", "Over Breadth"],
["CODE", "As Vessel"],
["湖南", "Blooded"],
["上海", "Hearted"]
];
/**
* ============================================================================
* 2. 增强型手势识别引擎 (解决灵敏度问题)
* ============================================================================
*/
class GestureEngine {
constructor() {
this.history = []; // 历史坐标,用于计算速度
this.maxHistory = 10;
this.lastGestureTime = 0;
}
update(landmarks) {
const now = Date.now();
// 平滑处理 (EMA)
const rawCenter = landmarks[9]; // 中指根部作为手掌中心
// 存入历史以计算速度
this.history.push({ x: rawCenter.x, y: rawCenter.y, z: rawCenter.z, time: now });
if (this.history.length > this.maxHistory) this.history.shift();
if (this.history.length < 5) return 'TRACKING';
// 计算平均速度向量
const start = this.history[0];
const end = this.history[this.history.length - 1];
const dt = (end.time - start.time) / 1000; // 秒
const vx = (end.x - start.x) / dt;
const vy = (end.y - start.y) / dt;
const speed = Math.sqrt(vx*vx + vy*vy);
// 阈值设定 (灵敏度调优)
const SWIPE_SPEED = 0.8;
const PINCH_DIST = 0.04;
// 1. 捏合检测 (Pinch) - 拇指(4)与食指(8)
const thumb = landmarks[4];
const index = landmarks[8];
const pinchDist = Math.hypot(thumb.x - index.x, thumb.y - index.y);
if (pinchDist < PINCH_DIST) return 'PINCH';
// 2. 挥动检测 (Swipe) - 基于速度
if (speed > SWIPE_SPEED) {
if (Math.abs(vx) > Math.abs(vy)) {
return vx > 0 ? 'SWIPE_LEFT' : 'SWIPE_RIGHT'; // 镜像后,手向右移是 x 增大
} else {
return vy > 0 ? 'SWIPE_DOWN' : 'SWIPE_UP'; // y 增大是向下
}
}
// 3. 静态手掌 (Open Hand)
return 'OPEN_PALM';
}
}
const gestureEngine = new GestureEngine();
/**
* ============================================================================
* 3. THREE.JS 渲染与粒子物理系统
* ============================================================================
*/
const container = document.getElementById('canvas-container');
// 场景搭建
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.0015); // 深度雾
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.z = 400;
const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 性能平衡
container.appendChild(renderer.domElement);
// 后处理 (Bloom)
const renderScene = new THREE.RenderPass(scene, camera);
const bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
bloomPass.threshold = CONFIG.bloomThreshold;
bloomPass.strength = CONFIG.bloomStrength;
bloomPass.radius = CONFIG.bloomRadius;
const composer = new THREE.EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
// 粒子系统数据结构
const particlesGeometry = new THREE.BufferGeometry();
const posArray = new Float32Array(CONFIG.particleCount * 3);
const targetArray = new Float32Array(CONFIG.particleCount * 3); // 目标位置
const velocityArray = new Float32Array(CONFIG.particleCount * 3); // 物理速度
const sizeArray = new Float32Array(CONFIG.particleCount);
for(let i=0; i<CONFIG.particleCount; i++) {
// 初始随机分布
posArray[i*3] = (Math.random()-0.5) * 800;
posArray[i*3+1] = (Math.random()-0.5) * 800;
posArray[i*3+2] = (Math.random()-0.5) * 800;
// 初始目标:默认星云
targetArray[i*3] = posArray[i*3];
targetArray[i*3+1] = posArray[i*3+1];
targetArray[i*3+2] = posArray[i*3+2];
// 初始速度为0
velocityArray[i*3] = 0;
velocityArray[i*3+1] = 0;
velocityArray[i*3+2] = 0;
sizeArray[i] = Math.random() * CONFIG.baseSize;
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
particlesGeometry.setAttribute('size', new THREE.BufferAttribute(sizeArray, 1));
// 自定义着色器 (实现锐利边缘 + 深度衰减)
const particlesMaterial = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(0xffffff) },
pixelRatio: { value: window.devicePixelRatio }
},
vertexShader: `
attribute float size;
uniform float pixelRatio;
void main() {
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * pixelRatio * (300.0 / -mvPosition.z); // 距离越远越小
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform vec3 color;
void main() {
// 绘制圆形
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) discard; // 锐利边缘,无羽化
// 极简光照感
float alpha = 1.0 - smoothstep(0.45, 0.5, dist);
gl_FragColor = vec4(color, alpha);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const particleSystem = new THREE.Points(particlesGeometry, particlesMaterial);
// --- 核心修复:镜像翻转 ---
// 通过将容器 scale.x 设为 -1修正左右镜像问题且不影响文字阅读因为文字生成时我们会反向处理
const sceneContainer = new THREE.Group();
sceneContainer.scale.x = -1;
sceneContainer.add(particleSystem);
scene.add(sceneContainer);
// 光标 (手部追踪点)
const cursorGeo = new THREE.RingGeometry(5, 6, 32);
const cursorMat = new THREE.MeshBasicMaterial({ color: 0xFFFFFF, side: THREE.DoubleSide, transparent: true, opacity: 0.5 });
const cursor = new THREE.Mesh(cursorGeo, cursorMat);
scene.add(cursor); // 光标不放在翻转容器里,直接对应屏幕坐标
/**
* ============================================================================
* 4. 形状生成器 (文字与几何体)
* ============================================================================
*/
const canvasGen = document.createElement('canvas');
const ctxGen = canvasGen.getContext('2d');
canvasGen.width = 1024;
canvasGen.height = 512;
function generateTextTargets(text1, text2) {
ctxGen.clearRect(0, 0, 1024, 512);
ctxGen.fillStyle = '#FFF';
ctxGen.font = 'bold 120px "Courier New"';
ctxGen.textAlign = 'center';
ctxGen.textBaseline = 'middle';
ctxGen.fillText(text1, 512, 180);
ctxGen.font = '60px "Courier New"';
ctxGen.fillText(text2, 512, 320);
const data = ctxGen.getImageData(0,0,1024,512).data;
const points = [];
// 扫描像素
for(let y=0; y<512; y+=4) {
for(let x=0; x<1024; x+=4) {
const idx = (y*1024 + x)*4;
if(data[idx] > 128) { // 亮度阈值
points.push({
x: (x - 512) * 1.5, // 居中并缩放
y: -(y - 256) * 1.5, // 翻转Y轴
z: 0
});
}
}
}
return points;
}
// 各种形态的目标点计算
function updateTargets(mode, time) {
const t = time * 0.5;
// 1. 叙事模式 (文字)
if (mode === 'NARRATIVE') {
const txtPoints = STATE.currentTextPoints || [];
for(let i=0; i<CONFIG.particleCount; i++) {
if(i < txtPoints.length) {
targetArray[i*3] = txtPoints[i].x;
targetArray[i*3+1] = txtPoints[i].y;
targetArray[i*3+2] = txtPoints[i].z;
} else {
// 多余粒子变成背景环绕
const angle = i * 0.1 + t;
const r = 600 + Math.sin(t + i)*50;
targetArray[i*3] = Math.cos(angle) * r;
targetArray[i*3+1] = Math.sin(angle) * r;
targetArray[i*3+2] = (Math.random()-0.5)*200;
}
}
return;
}
// 2. 默认深空演化 (基于 Perlin Noise 思想的伪随机)
for(let i=0; i<CONFIG.particleCount; i++) {
const ix3 = i*3;
if (mode === 'IDLE_COSMOS') {
// 流场效果
const angle = Math.cos(posArray[ix3] * 0.002 + t) * Math.PI * 2;
const r = 300 + Math.sin(t*0.5 + i)*100;
// 不强制归位,而是给予流动方向
targetArray[ix3] = Math.cos(angle + i) * r;
targetArray[ix3+1] = Math.sin(angle * 0.5 + t) * r;
targetArray[ix3+2] = Math.sin(i) * 200;
}
else if (mode === 'DNA_SPIRAL') {
const angle = i * 0.05 + t;
const h = (i % 200) * 4 - 400;
const r = 100;
const strand = (i % 2 === 0) ? 0 : Math.PI;
targetArray[ix3] = Math.cos(angle + strand) * r;
targetArray[ix3+1] = h;
targetArray[ix3+2] = Math.sin(angle + strand) * r;
}
else if (mode === 'BLACK_HOLE') {
// 引力坍缩
const angle = i * 0.1 + t * 5;
const r = 50 * (i/CONFIG.particleCount); // 极小
targetArray[ix3] = Math.cos(angle) * r;
targetArray[ix3+1] = Math.sin(angle) * r;
targetArray[ix3+2] = 0;
}
}
}
/**
* ============================================================================
* 5. 动画与物理循环
* ============================================================================
*/
const clock = new THREE.Clock();
function animate() {
const delta = clock.getDelta();
const time = clock.getElapsedTime();
// 1. 更新目标点
updateTargets(STATE.mode, time);
// 2. 物理模拟 (Velocity Verlet 简化版)
const positions = particleSystem.geometry.attributes.position.array;
// 物理参数
const springStrength = STATE.mode === 'NARRATIVE' ? 0.08 : 0.02; // 文字模式吸附力更强
const dampening = 0.92; // 阻尼
const noiseStrength = 0.5;
// 手势交互力
const interactX = STATE.interactionForce.x;
const interactY = STATE.interactionForce.y;
for(let i=0; i<CONFIG.particleCount; i++) {
const ix3 = i*3;
// 计算引力: Target - Current
const dx = targetArray[ix3] - positions[ix3];
const dy = targetArray[ix3+1] - positions[ix3+1];
const dz = targetArray[ix3+2] - positions[ix3+2];
// 加速度 += 引力
velocityArray[ix3] += dx * springStrength;
velocityArray[ix3+1] += dy * springStrength;
velocityArray[ix3+2] += dz * springStrength;
// 加上 卷曲噪声 (Curl Noise 模拟) - 让运动不那么机械
velocityArray[ix3] += Math.sin(positions[ix3+1]*0.01 + time)*noiseStrength;
velocityArray[ix3+1] += Math.cos(positions[ix3]*0.01 + time)*noiseStrength;
// 加上 手势交互力 (爆炸/风场)
if (Math.abs(interactX) > 0.1 || Math.abs(interactY) > 0.1) {
velocityArray[ix3] += interactX * Math.random() * 5;
velocityArray[ix3+1] += interactY * Math.random() * 5;
}
// 速度阻尼
velocityArray[ix3] *= dampening;
velocityArray[ix3+1] *= dampening;
velocityArray[ix3+2] *= dampening;
// 更新位置
positions[ix3] += velocityArray[ix3];
positions[ix3+1] += velocityArray[ix3+1];
positions[ix3+2] += velocityArray[ix3+2];
}
// 衰减交互力
STATE.interactionForce.multiplyScalar(0.9);
particleSystem.geometry.attributes.position.needsUpdate = true;
// 3. 场景整体旋转 (由手势或自动控制)
sceneContainer.rotation.y += STATE.rotationSpeed.y * delta;
sceneContainer.rotation.x += STATE.rotationSpeed.x * delta;
// 阻尼回复正常转速
STATE.rotationSpeed.y = THREE.MathUtils.lerp(STATE.rotationSpeed.y, 0.05, 0.05);
STATE.rotationSpeed.x = THREE.MathUtils.lerp(STATE.rotationSpeed.x, 0, 0.05);
// 4. 渲染
composer.render();
document.getElementById('fps').innerText = Math.round(1/delta);
requestAnimationFrame(animate);
}
/**
* ============================================================================
* 6. 逻辑控制与手势响应
* ============================================================================
*/
// 切换到下一段叙事
function nextNarrative() {
if (STATE.narrativeIndex >= NARRATIVE_TEXTS.length) {
STATE.narrativeIndex = 0; // 循环或结束
}
const txt = NARRATIVE_TEXTS[STATE.narrativeIndex];
STATE.currentTextPoints = generateTextTargets(txt[0], txt[1]);
STATE.mode = 'NARRATIVE';
document.getElementById('mode-txt').innerText = `ARCHIVE_${STATE.narrativeIndex}`;
STATE.narrativeIndex++;
// 爆炸特效:给所有粒子一个随机向外的初速度
for(let i=0; i<CONFIG.particleCount*3; i++) {
velocityArray[i] += (Math.random()-0.5) * 50;
}
}
// 处理手势动作
function handleGestureAction(gesture) {
const hint = document.getElementById('hint-txt');
if (gesture === 'NAMASTE') {
if (!STATE.isUnlocked) {
STATE.isUnlocked = true;
STATE.mode = 'BLACK_HOLE'; // 先坍缩
hint.innerText = "ACCESS GRANTED // 正在载入";
setTimeout(() => nextNarrative(), 1500); // 1.5秒后展示文字
}
}
else if (STATE.isUnlocked) {
// 已解锁状态下的交互
if (gesture === 'SWIPE_LEFT') {
nextNarrative();
hint.innerText = "NEXT ENTRY >>";
STATE.interactionForce.x = 20; // 物理推力
}
else if (gesture === 'SWIPE_RIGHT') {
STATE.interactionForce.x = -20;
}
else if (gesture === 'SWIPE_UP') {
STATE.rotationSpeed.x = 0.5;
hint.innerText = "PITCH UP";
}
else if (gesture === 'PINCH') {
STATE.mode = 'DNA_SPIRAL';
hint.innerText = "DNA RECONSTRUCTION";
// 3秒后恢复
clearTimeout(STATE.resetTimer);
STATE.resetTimer = setTimeout(() => {
STATE.mode = 'NARRATIVE';
}, 3000);
}
} else {
// 未解锁时的交互
if (gesture === 'SWIPE_LEFT' || gesture === 'SWIPE_RIGHT') {
STATE.rotationSpeed.y = gesture === 'SWIPE_LEFT' ? 1.0 : -1.0;
}
}
}
/**
* ============================================================================
* 7. MEDIAPIPE 集成
* ============================================================================
*/
const videoElement = document.getElementById('input-video');
const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
hands.setOptions({
maxNumHands: 2,
modelComplexity: 1,
minDetectionConfidence: 0.6, // 降低阈值提高检出率
minTrackingConfidence: 0.6
});
hands.onResults((results) => {
const landmarks = results.multiHandLandmarks;
const handStateEl = document.getElementById('hand-state');
// 隐藏加载条
document.getElementById('loader').style.opacity = 0;
setTimeout(()=> document.getElementById('loader').style.display='none', 800);
if (landmarks && landmarks.length > 0) {
STATE.handDetected = true;
handStateEl.innerText = "LINKED";
handStateEl.style.color = "#0F0";
// 更新光标位置 (取第一只手)
const hand = landmarks[0];
const palm = hand[9];
// 映射屏幕坐标到 3D 坐标平面
const vec = new THREE.Vector3(
(palm.x * 2 - 1) * 300, // 这里的 X 不需要翻转,因为我们已经在场景容器翻转了
-(palm.y * 2 - 1) * 200,
0
);
cursor.position.copy(vec);
cursor.visible = true;
// 双手合十检测
if (landmarks.length === 2) {
const h1 = landmarks[0];
const h2 = landmarks[1];
const dist = Math.hypot(h1[9].x - h2[9].x, h1[9].y - h2[9].y);
if (dist < 0.15) { // 阈值
handleGestureAction('NAMASTE');
return;
}
}
// 单手手势检测
const gesture = gestureEngine.update(hand);
if (gesture !== STATE.gesture && gesture !== 'TRACKING') {
STATE.gesture = gesture;
console.log("Gesture Detected:", gesture);
handleGestureAction(gesture);
// 视觉反馈
bloomPass.strength = 3.0; // 瞬间高亮
setTimeout(() => bloomPass.strength = CONFIG.bloomStrength, 300);
}
} else {
STATE.handDetected = false;
handStateEl.innerText = "SEARCHING...";
handStateEl.style.color = "inherit";
cursor.visible = false;
}
});
const cameraUtils = new Camera(videoElement, {
onFrame: async () => { await hands.send({image: videoElement}); },
width: 640, height: 480
});
// 启动系统
document.getElementById('load-bar').style.width = '100%';
cameraUtils.start();
animate();
// 窗口自适应
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>