粒子交互edit icon

作者:
ZGR
Fork(复制)
下载
嵌入
BUG反馈
index.html
现在支持上传本地图片了!
index.html
            
            <!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>3D Particle System - Gesture Control</title>
    <style>
        body { margin: 0; background: #000; overflow: hidden; font-family: 'Courier New', monospace; }
        #overlay { position: absolute; top: 20px; left: 20px; color: #00ff00; z-index: 10; pointer-events: none; text-shadow: 1px 1px 2px #000; }
        #controls { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; z-index: 100; background: rgba(0,0,0,0.8); padding: 30px; border: 2px solid #00ff00; border-radius: 10px; }
        button { padding: 15px 40px; font-size: 20px; cursor: pointer; background: #00ff00; border: none; font-weight: bold; color: #000; transition: 0.3s; }
        button:hover { background: #008800; color: #fff; }
        #error-msg { color: #ff4444; margin-top: 20px; display: none; max-width: 400px; line-height: 1.5; }
        video { display: none; }
    </style>
</head>
<body>
    <div id="overlay">
        STATUS: <span id="st">WAITING</span><br>
        GESTURE: <span id="gs">-</span><br>
        SPREAD: <span id="sp">0</span>
    </div>

    <div id="controls">
        <h2 style="color:#00ff00; margin-top:0;">3D 粒子交互系统</h2>
        <p style="color:#ccc;">需要摄像头权限以识别手势</p>
        <button id="start-btn">开启摄像头并启动</button>
        <div id="error-msg">
            <strong>无法访问摄像头:</strong><br>
            1. 请确保使用 <b>HTTPS</b> 或 <b>localhost</b> 访问。<br>
            2. 请在浏览器地址栏点击“锁”图标,重新允许摄像头权限。<br>
            3. 确保没有其他程序占用摄像头。
        </div>
    </div>

    <video id="v" playsinline></video>

    <!-- 库文件 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/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>
        const video = document.getElementById('v');
        const startBtn = document.getElementById('start-btn');
        const errorMsg = document.getElementById('error-msg');
        const st = document.getElementById('st');
        const gs = document.getElementById('gs');
        const sp = document.getElementById('sp');

        let scene, camera, renderer, points;
        const COUNT = 15000; // 粒子数量
        const targets = new Float32Array(COUNT * 3);
        let curGesture = -1;
        let spreadVal = 0;

        // 离屏Canvas用于文字转换
        const tCvs = document.createElement('canvas');
        const tCtx = tCvs.getContext('2d');
        tCvs.width = 400; tCvs.height = 400;

        function setTargetShape(text) {
            tCtx.clearRect(0, 0, 400, 400);
            tCtx.fillStyle = 'white';
            tCtx.font = 'bold 100px Arial';
            tCtx.textAlign = 'center';
            tCtx.textBaseline = 'middle';
            tCtx.fillText(text, 200, 200);
            
            const pixels = tCtx.getImageData(0, 0, 400, 400).data;
            const pts = [];
            for (let y = 0; y < 400; y += 2) {
                for (let x = 0; x < 400; x += 2) {
                    if (pixels[(y * 400 + x) * 4] > 128) {
                        pts.push({x: (x - 200) * 0.7, y: (200 - y) * 0.7});
                    }
                }
            }
            // 填充粒子目标
            for (let i = 0; i < COUNT; i++) {
                const p = pts[i % pts.length];
                targets[i * 3] = p.x;
                targets[i * 3 + 1] = p.y;
                targets[i * 3 + 2] = (Math.random() - 0.5) * 10;
            }
        }

        function initThree() {
            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.z = 120;

            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            document.body.appendChild(renderer.domElement);

            const geo = new THREE.BufferGeometry();
            const pos = new Float32Array(COUNT * 3);
            const col = new Float32Array(COUNT * 3);

            for (let i = 0; i < COUNT; i++) {
                pos[i*3] = (Math.random()-0.5)*500;
                pos[i*3+1] = (Math.random()-0.5)*500;
                pos[i*3+2] = (Math.random()-0.5)*500;
                // 蓝青色系
                col[i*3] = 0.1; col[i*3+1] = 0.7 + Math.random()*0.3; col[i*3+2] = 1.0;
            }

            geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
            geo.setAttribute('color', new THREE.BufferAttribute(col, 3));

            points = new THREE.Points(geo, new THREE.PointsMaterial({
                size: 1.0, vertexColors: true, blending: THREE.AdditiveBlending, transparent: true, opacity: 0.8
            }));
            scene.add(points);
            setTargetShape("h()");
        }

        const handLogic = new Hands({
            locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
        });

        handLogic.setOptions({
            maxNumHands: 1,
            modelComplexity: 1,
            minDetectionConfidence: 0.5,
            minTrackingConfidence: 0.5
        });

        handLogic.onResults(res => {
            if (!res.multiHandLandmarks || res.multiHandLandmarks.length === 0) {
                st.innerText = "LOST";
                spreadVal *= 0.95; // 失去检测时缓慢收缩
                return;
            }
            st.innerText = "TRACKING";
            const lm = res.multiHandLandmarks[0];

            // 1. 手势判断 (计数伸出的手指)
            let count = 0;
            if (lm[8].y < lm[6].y) count++;   // 食指
            if (lm[12].y < lm[10].y) count++; // 中指
            if (lm[16].y < lm[14].y) count++; // 无名指

            if (count !== curGesture && count >= 1 && count <= 3) {
                curGesture = count;
                if (count === 1) { setTargetShape("h()"); gs.innerText = "1: h()"; }
                else if (count === 2) { setTargetShape("()"); gs.innerText = "2: ()"; }
                else if (count === 3) { setTargetShape("()"); gs.innerText = "3: ()"; }
            }

            // 2. 张合控制 (计算指尖到掌心的距离)
            const wrist = lm[0];
            const fingerTip = lm[12];
            const dist = Math.hypot(fingerTip.x - wrist.x, fingerTip.y - wrist.y);
            // 映射距离到扩散系数
            const targetSpread = Math.max(0, (dist - 0.2) * 250);
            spreadVal += (targetSpread - spreadVal) * 0.1;
            sp.innerText = Math.round(spreadVal);
        });

        const camHelper = new Camera(video, {
            onFrame: async () => { await handLogic.send({ image: video }); },
            width: 640, height: 480
        });

        startBtn.addEventListener('click', () => {
            errorMsg.style.display = 'none';
            camHelper.start()
                .then(() => {
                    document.getElementById('controls').style.display = 'none';
                })
  
        
编辑器加载中
预览
控制台