- 重写粒子物理系统,支持速度与目标点分离的Verlet积分 - 新增辉光后期处理(UnrealBloomPass)增强视觉表现 - 优化手势识别引擎,提高捏合与滑动手势精度 - 修复摄像头画面镜像问题,改善交互准确性 - 增加叙事模式自动播放与手动切换功能 - 改进UI布局与加载动画,提升用户体验 - 调整粒子形态生成算法,新增黑洞、螺旋等预设 - 优化移动端渲染性能,限制高密度屏幕采样率
625 lines
24 KiB
HTML
625 lines
24 KiB
HTML
<!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>
|