前端技术手势识别

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <title>Three.js 粒子手掌 - WebGL + 摄像头识别</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <style>
        html, body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background: #02040a;
            font-family: Arial, sans-serif;
        }

        #container {
            width: 100vw;
            height: 100vh;
            position: relative;
            overflow: hidden;
        }

        canvas {
            display: block;
        }

        #video {
            position: fixed;
            right: 16px;
            bottom: 16px;
            width: 220px;
            height: 165px;
            object-fit: cover;
            border-radius: 12px;
            opacity: 0.35;
            transform: scaleX(-1);
            z-index: 10;
            border: 1px solid rgba(255,255,255,0.18);
        }

        #tips {
            position: fixed;
            left: 18px;
            top: 16px;
            z-index: 20;
            color: rgba(255,255,255,0.85);
            font-size: 14px;
            line-height: 1.7;
            background: rgba(0,0,0,0.28);
            backdrop-filter: blur(10px);
            padding: 12px 16px;
            border-radius: 12px;
            border: 1px solid rgba(255,255,255,0.12);
        }

        #status {
            color: #8efcff;
        }
    </style>
</head>

<body>
<div id="container"></div>

<video id="video" autoplay playsinline muted></video>

<div id="tips">
    <div>3D 粒子手掌 / Three.js + MediaPipe Hands</div>
    <div>状态:<span id="status">初始化中...</span></div>
    <div>把手掌放到摄像头前,会生成粒子手掌</div>
</div>

<!-- Three.js -->
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>

<!-- MediaPipe Hands -->
<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 container = document.getElementById('container');
    const video = document.getElementById('video');
    const statusEl = document.getElementById('status');

    let scene, camera, renderer;
    let handParticles, glowParticles;
    let particleGeometry, glowGeometry;
    let particleMaterial, glowMaterial;

    const PARTICLES_PER_BONE = 24;
    const RANDOM_FLOATING_PARTICLES = 500;
    const HAND_SCALE = 7.2;

    let targetPositions = [];
    let currentPositions = [];
    let hasHand = false;

    const HAND_CONNECTIONS = [
        [0,1], [1,2], [2,3], [3,4],
        [0,5], [5,6], [6,7], [7,8],
        [0,9], [9,10], [10,11], [11,12],
        [0,13], [13,14], [14,15], [15,16],
        [0,17], [17,18], [18,19], [19,20],
        [5,9], [9,13], [13,17], [5,17]
    ];

    initThree();
    initParticles();
    initMediaPipe();
    animate();

    function initThree() {
        scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x02040a, 0.08);

        camera = new THREE.PerspectiveCamera(
            55,
            window.innerWidth / window.innerHeight,
            0.1,
            100
        );
        camera.position.z = 12;

        renderer = new THREE.WebGLRenderer({
            antialias: true,
            alpha: true
        });

        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        container.appendChild(renderer.domElement);

        const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
        scene.add(ambientLight);

        const pointLight = new THREE.PointLight(0x66ffff, 2, 30);
        pointLight.position.set(3, 4, 8);
        scene.add(pointLight);

        window.addEventListener('resize', onResize);
    }

    function initParticles() {
        const boneParticleCount = HAND_CONNECTIONS.length * PARTICLES_PER_BONE;
        const keyPointParticleCount = 21 * 18;
        const totalHandParticles = boneParticleCount + keyPointParticleCount;

        particleGeometry = new THREE.BufferGeometry();

        const positions = new Float32Array(totalHandParticles * 3);
        const colors = new Float32Array(totalHandParticles * 3);
        const sizes = new Float32Array(totalHandParticles);

        for (let i = 0; i < totalHandParticles; i++) {
            const x = (Math.random() - 0.5) * 2;
            const y = (Math.random() - 0.5) * 2;
            const z = (Math.random() - 0.5) * 2;

            positions[i * 3] = x;
            positions[i * 3 + 1] = y;
            positions[i * 3 + 2] = z;

            currentPositions.push(new THREE.Vector3(x, y, z));
            targetPositions.push(new THREE.Vector3(x, y, z));

            const t = i / totalHandParticles;

            colors[i * 3] = 0.2 + t * 0.4;
            colors[i * 3 + 1] = 0.75 + Math.random() * 0.25;
            colors[i * 3 + 2] = 1.0;

            sizes[i] = 0.045 + Math.random() * 0.055;
        }

        particleGeometry.setAttribute(
            'position',
            new THREE.BufferAttribute(positions, 3)
        );

        particleGeometry.setAttribute(
            'color',
            new THREE.BufferAttribute(colors, 3)
        );

        particleGeometry.setAttribute(
            'size',
            new THREE.BufferAttribute(sizes, 1)
        );

        particleMaterial = new THREE.ShaderMaterial({
            transparent: true,
            depthWrite: false,
            blending: THREE.AdditiveBlending,
            vertexColors: true,
            uniforms: {
                time: { value: 0 },
                opacity: { value: 1.0 }
            },
            vertexShader: `
          attribute float size;
          varying vec3 vColor;
          uniform float time;

          void main() {
            vColor = color;

            vec3 p = position;
            p.z += sin(time * 2.0 + position.x * 2.0 + position.y) * 0.025;

            vec4 mvPosition = modelViewMatrix * vec4(p, 1.0);
            gl_PointSize = size * 420.0 / -mvPosition.z;
            gl_Position = projectionMatrix * mvPosition;
          }
        `,
            fragmentShader: `
          varying vec3 vColor;
          uniform float opacity;

          void main() {
            float d = distance(gl_PointCoord, vec2(0.5));
            float alpha = smoothstep(0.5, 0.0, d);

            vec3 finalColor = vColor * (1.5 - d);
            gl_FragColor = vec4(finalColor, alpha * opacity);
          }
        `
        });

        handParticles = new THREE.Points(particleGeometry, particleMaterial);
        scene.add(handParticles);

        createBackgroundParticles();
    }

    function createBackgroundParticles() {
        glowGeometry = new THREE.BufferGeometry();

        const positions = new Float32Array(RANDOM_FLOATING_PARTICLES * 3);
        const colors = new Float32Array(RANDOM_FLOATING_PARTICLES * 3);
        const sizes = new Float32Array(RANDOM_FLOATING_PARTICLES);

        for (let i = 0; i < RANDOM_FLOATING_PARTICLES; i++) {
            positions[i * 3] = (Math.random() - 0.5) * 26;
            positions[i * 3 + 1] = (Math.random() - 0.5) * 16;
            positions[i * 3 + 2] = (Math.random() - 0.5) * 18;

            colors[i * 3] = 0.1;
            colors[i * 3 + 1] = 0.65 + Math.random() * 0.25;
            colors[i * 3 + 2] = 1.0;

            sizes[i] = 0.015 + Math.random() * 0.04;
        }

        glowGeometry.setAttribute(
            'position',
            new THREE.BufferAttribute(positions, 3)
        );

        glowGeometry.setAttribute(
            'color',
            new THREE.BufferAttribute(colors, 3)
        );

        glowGeometry.setAttribute(
            'size',
            new THREE.BufferAttribute(sizes, 1)
        );

        glowMaterial = particleMaterial.clone();
        glowMaterial.uniforms = {
            time: { value: 0 },
            opacity: { value: 0.35 }
        };

        glowParticles = new THREE.Points(glowGeometry, glowMaterial);
        scene.add(glowParticles);
    }

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

        hands.setOptions({
            maxNumHands: 1,
            modelComplexity: 1,
            minDetectionConfidence: 0.72,
            minTrackingConfidence: 0.72
        });

        hands.onResults(onHandResults);

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

        cameraUtils.start()
            .then(() => {
                statusEl.textContent = '摄像头已开启,等待手掌';
            })
            .catch(err => {
                console.error(err);
                statusEl.textContent = '摄像头启动失败,请检查权限';
            });
    }

    function onHandResults(results) {
        if (
            results.multiHandLandmarks &&
            results.multiHandLandmarks.length > 0
        ) {
            hasHand = true;
            statusEl.textContent = '已识别手掌';

            const landmarks = results.multiHandLandmarks[0];
            buildParticleHandFromLandmarks(landmarks);
        } else {
            hasHand = false;
            statusEl.textContent = '未识别到手掌';
            scatterParticles();
        }
    }

    function landmarkToVector(lm) {
        /**
         * MediaPipe 坐标:
         * x: 0 - 1
         * y: 0 - 1
         * z: 相对深度
         *
         * 转成 Three.js 空间:
         * x 左右
         * y 上下
         * z 前后
         */
        const x = (0.5 - lm.x) * HAND_SCALE;
        const y = (0.5 - lm.y) * HAND_SCALE;
        const z = -lm.z * 8.5;

        return new THREE.Vector3(x, y, z);
    }

    function buildParticleHandFromLandmarks(landmarks) {
        const points = landmarks.map(landmarkToVector);

        let index = 0;

        // 手指骨骼上的粒子
        for (const [a, b] of HAND_CONNECTIONS) {
            const p1 = points[a];
            const p2 = points[b];

            for (let i = 0; i < PARTICLES_PER_BONE; i++) {
                const t = i / (PARTICLES_PER_BONE - 1);

                const p = new THREE.Vector3().lerpVectors(p1, p2, t);

                const spread = 0.04;
                p.x += (Math.random() - 0.5) * spread;
                p.y += (Math.random() - 0.5) * spread;
                p.z += (Math.random() - 0.5) * spread;

                if (targetPositions[index]) {
                    targetPositions[index].copy(p);
                }

                index++;
            }
        }

        // 每个关键点周围额外聚集一些粒子,让指尖和掌心更有光感
        for (let k = 0; k < points.length; k++) {
            const base = points[k];

            for (let j = 0; j < 18; j++) {
                const radius = k === 0 ? 0.18 : 0.11;

                const p = base.clone();
                p.x += (Math.random() - 0.5) * radius;
                p.y += (Math.random() - 0.5) * radius;
                p.z += (Math.random() - 0.5) * radius;

                if (targetPositions[index]) {
                    targetPositions[index].copy(p);
                }

                index++;
            }
        }
    }

    function scatterParticles() {
        for (let i = 0; i < targetPositions.length; i++) {
            const angle = i * 0.08;
            const radius = 2.5 + Math.sin(i * 0.1) * 1.2;

            targetPositions[i].x = Math.cos(angle) * radius + Math.sin(Date.now() * 0.0004 + i) * 0.8;
            targetPositions[i].y = Math.sin(angle * 0.7) * radius * 0.55;
            targetPositions[i].z = Math.sin(angle) * 1.2;
        }
    }

    function animate() {
        requestAnimationFrame(animate);

        const time = performance.now() * 0.001;

        particleMaterial.uniforms.time.value = time;
        glowMaterial.uniforms.time.value = time;

        const positions = particleGeometry.attributes.position.array;

        for (let i = 0; i < currentPositions.length; i++) {
            currentPositions[i].lerp(targetPositions[i], hasHand ? 0.32 : 0.035);

            positions[i * 3] = currentPositions[i].x;
            positions[i * 3 + 1] = currentPositions[i].y;
            positions[i * 3 + 2] = currentPositions[i].z;
        }

        particleGeometry.attributes.position.needsUpdate = true;

        handParticles.rotation.y = Math.sin(time * 0.35) * 0.08;
        handParticles.rotation.x = Math.sin(time * 0.25) * 0.04;

        glowParticles.rotation.y += 0.0008;
        glowParticles.rotation.x += 0.0004;

        renderer.render(scene, camera);
    }

    function onResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    }
</script>
</body>
</html>
相关推荐
da-peng-song3 天前
ArcGIS Desktop使用入门(四)——生成经纬度坐标
arcgis·经纬度坐标
da-peng-song3 天前
ArcGIS Desktop使用入门(三)图层右键工具——定义查询
数据库·arcgis·拆分数据·定义查询
星座5283 天前
破解水环境空间分析难题,迈向智慧水环境管理:ArcGIS水质评价、污染预测与洪水监测核心技术揭秘
arcgis·水环境·水文
非科班Java出身GISer4 天前
ArcGIS JS 基础教程(10):Camera 相机控制
arcgis·arcgis js 相机·arcgis js 相机控制·arcgis js 视角控制·arcgis js 飞行定位·arcgis js 定位·arcgis js 各种定位
码语智行5 天前
Shapefile获取空间数据和中心点坐标
java·arcgis
码语智行5 天前
地图上图、空间拓扑查询示例
java·arcgis
DXM05215 天前
第10期| 卷积神经网络CNN通俗详解:AI遥感的底层核心
人工智能·python·神经网络·机器学习·arcgis·cnn·文心一言
智航GIS6 天前
ArcGIS大师之路500技---078补零
arcgis
DXM05217 天前
第8期| 传统机器学习遥感解译:SVM & 随机森林分类全流程实操
人工智能·python·随机森林·机器学习·支持向量机·arcgis·自然语言处理