#HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D粒子手势系统 - 交互优化版</title>
<style>
body { margin: 0; overflow: hidden; background: #000; font-family: 'Segoe UI', sans-serif; }
#container { width: 100vw; height: 100vh; }
#ui-layer {
position: absolute; top: 20px; left: 20px; z-index: 100;
color: white; background: rgba(0, 20, 40, 0.6);
backdrop-filter: blur(15px); padding: 25px;
border-radius: 20px; border: 1px solid rgba(162, 210, 255, 0.3);
pointer-events: none;
}
.control-panel { pointer-events: auto; margin-top: 15px; display: flex; align-items: center; gap: 15px; }
#preview-container {
position: absolute; bottom: 20px; right: 20px;
width: 200px; height: 150px; border-radius: 12px; overflow: hidden;
border: 1px solid #a2d2ff; transform: scaleX(-1);
}
#video-preview, #canvas-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
button { background: #a2d2ff; border: none; padding: 8px 18px; border-radius: 6px; cursor: pointer; font-weight: bold; }
.hint { font-size: 0.85em; color: #a2d2ff; margin-top: 15px; line-height: 1.6; }
#status { font-weight: bold; color: #fff; margin-top: 10px; }
</style>
</head>
<body>
<div id="ui-layer">
<h2 style="margin:0; font-weight: 300;">NEURAL INTERACTION</h2>
<div class="control-panel">
<input type="color" id="colorPicker" value="#a2d2ff">
<button id="fsBtn">全屏模式</button>
</div>
<div class="hint">
● <b>握拳</b>:粒子坍缩 (黑洞状态)<br>
● <b>1/2/3指</b>:几何变换 (球/环/带)<br>
● <b>张开全掌</b>:粒子喷发 (混沌状态)
</div>
<p id="status">感知器加载中...</p>
</div>
<div id="preview-container">
<video id="video-preview" autoplay playsinline></video>
<canvas id="canvas-overlay"></canvas>
</div>
<div id="container"></div>
<script src="https://cdn.jsdelivr.net/npm/three@0.150.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
<script>
// --- 1. 场景初始化 ---
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 1, 3000);
camera.position.z = 600;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
// --- 2. 几何体数据 (增加 fist 状态) ---
const PARTICLE_COUNT = 8500;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(PARTICLE_COUNT * 3);
const shapes = { sphere: [], torus: [], mobius: [], chaos: [], fist: [] };
for (let i = 0; i < PARTICLE_COUNT; i++) {
// 球体
const phi = Math.acos(-1 + (2 * i) / PARTICLE_COUNT);
const theta = Math.sqrt(PARTICLE_COUNT * Math.PI) * phi;
shapes.sphere.push(230 * Math.cos(theta) * Math.sin(phi), 230 * Math.sin(theta) * Math.sin(phi), 230 * Math.cos(phi));
// 环面
const u = Math.random() * Math.PI * 2;
const v = Math.random() * Math.PI * 2;
shapes.torus.push((190 + 50 * Math.cos(v)) * Math.cos(u), (190 + 50 * Math.cos(v)) * Math.sin(u), 50 * Math.sin(v));
// 莫比乌斯
const mu = Math.random() * Math.PI * 2;
const mv = Math.random() * 80 - 40;
shapes.mobius.push((170 + mv * Math.cos(mu/2)) * Math.cos(mu), (170 + mv * Math.cos(mu/2)) * Math.sin(mu), mv * Math.sin(mu/2));
// 混沌 (张开手掌时的剧烈散射)
shapes.chaos.push((Math.random()-0.5)*1200, (Math.random()-0.5)*1200, (Math.random()-0.5)*1200);
// 握拳 (高度压缩的核心)
const r = Math.random() * 30;
const fp = Math.acos(-1 + (2 * i) / PARTICLE_COUNT);
const ft = Math.sqrt(PARTICLE_COUNT * Math.PI) * fp;
shapes.fist.push(r * Math.cos(ft) * Math.sin(fp), r * Math.sin(ft) * Math.sin(fp), r * Math.cos(fp));
positions[i*3] = shapes.chaos[i*3];
positions[i*3+1] = shapes.chaos[i*3+1];
positions[i*3+2] = shapes.chaos[i*3+2];
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const sprite = new THREE.TextureLoader().load('https://threejs.org/examples/textures/sprites/disc.png');
const material = new THREE.PointsMaterial({
size: 4, sizeAttenuation: true, map: sprite,
transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending, color: 0xa2d2ff
});
const points = new THREE.Points(geometry, material);
scene.add(points);
// --- 3. 核心算法 ---
function morphTo(shapeKey) {
const targetData = shapes[shapeKey];
const posAttr = geometry.attributes.position;
TWEEN.removeAll(); // 清除之前的动画防止冲突
for (let i = 0; i < PARTICLE_COUNT; i++) {
new TWEEN.Tween(posAttr.array)
.to({
[i*3]: targetData[i*3],
[i*3+1]: targetData[i*3+1],
[i*3+2]: targetData[i*3+2]
}, 1200)
.easing(TWEEN.Easing.Quadratic.Out)
.start();
}
}
// --- 4. 优化后的手势识别 ---
const videoElement = document.getElementById('video-preview');
const canvasElement = document.getElementById('canvas-overlay');
const canvasCtx = canvasElement.getContext('2d');
const statusText = document.getElementById('status');
const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
hands.setOptions({ maxNumHands: 1, modelComplexity: 1, minDetectionConfidence: 0.8, minTrackingConfidence: 0.8 });
let lastGesture = "chaos";
hands.onResults(results => {
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
const landmarks = results.multiHandLandmarks[0];
drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, {color: '#a2d2ff', lineWidth: 2});
// 1. 握拳判定逻辑 (计算指尖到掌心的平均距离)
const palmCenter = landmarks[9];
const tips = [4, 8, 12, 16, 20];
let totalDist = 0;
tips.forEach(idx => {
totalDist += Math.hypot(landmarks[idx].x - palmCenter.x, landmarks[idx].y - palmCenter.y);
});
const avgDist = totalDist / 5;
// 2. 伸出手指计数
let extendedFingers = 0;
if (landmarks[8].y < landmarks[6].y) extendedFingers++;
if (landmarks[12].y < landmarks[10].y) extendedFingers++;
if (landmarks[16].y < landmarks[14].y) extendedFingers++;
if (landmarks[20].y < landmarks[18].y) extendedFingers++;
// 大拇指检测 (根据水平位移判断)
const thumbExtended = Math.abs(landmarks[4].x - landmarks[2].x) > 0.1;
if(thumbExtended) extendedFingers++;
// 3. 状态机切换
let currentGesture = "";
if (avgDist < 0.12) {
currentGesture = "fist";
} else if (extendedFingers >= 4) {
currentGesture = "chaos";
} else if (extendedFingers === 1) {
currentGesture = "sphere";
} else if (extendedFingers === 2) {
currentGesture = "torus";
} else if (extendedFingers === 3) {
currentGesture = "mobius";
}
if (currentGesture && currentGesture !== lastGesture) {
morphTo(currentGesture);
lastGesture = currentGesture;
statusText.innerText = "当前手势: " + currentGesture.toUpperCase();
}
// 缩放逻辑保持
const zoomDist = Math.hypot(landmarks[4].x - landmarks[8].x, landmarks[4].y - landmarks[8].y);
const s = THREE.MathUtils.mapLinear(zoomDist, 0.05, 0.4, 0.6, 2.5);
points.scale.lerp(new THREE.Vector3(s, s, s), 0.1);
}
});
const cameraFeed = new Camera(videoElement, {
onFrame: async () => {
canvasElement.width = videoElement.videoWidth;
canvasElement.height = videoElement.videoHeight;
await hands.send({image: videoElement});
}
});
cameraFeed.start();
// --- 5. 动画循环 ---
function animate(time) {
requestAnimationFrame(animate);
TWEEN.update(time);
geometry.attributes.position.needsUpdate = true;
points.rotation.y += 0.002;
renderer.render(scene, camera);
}
animate();
// 颜色与全屏
document.getElementById('colorPicker').oninput = (e) => material.color.set(e.target.value);
document.getElementById('fsBtn').onclick = () => {
if (!document.fullscreenElement) document.documentElement.requestFullscreen();
else document.exitFullscreen();
};
window.onresize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
</script>
</body>
</html>
#导入代码即可使用

Comments NOTHING