#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>

#导入代码即可使用

预览页面:https://blog.xiaoyouyou18.top/web/gemini.html

此作者没有提供个人介绍。
最后更新于 2025-12-18