AI真好玩系列-圣诞树手势交响曲 | Christmas Tree Gesture Symphony

@

周末的深夜,睡不着觉,闲来无事,打开Gemini哈基米玩一把,废话不多说,先上图看最终效果

碎碎念:我很想上传视频,但是好像有的博客不支持,这里先放个图片

视频地址:https://www.bilibili.com/video/BV1tz29BEEh6?t=9.5

image

💖 提示词

请帮我使用 Three.jsMediaPipe Hands 编写一个单文件 HTML 的 3D 互动网页应用。

1. 场景视觉要求:

  • 背景:必须是纯黑色 (0x000000)。请在 Three.js 初始化时强制设置 scene.background 为黑色,并且 WebGLRendereralpha 设为 false,不要依赖 CSS 背景。
  • 雾效:添加黑色的雾 (FogExp2),让远处物体自然消隐。
  • 粒子系统
    • 数量:8000个
    • 颜色:暖金色 (0xFFD700)。
    • 材质:使用贴图材质,形状为柔和的发光圆点。
    • 渲染:必须开启 sizeAttenuation,实现远近大小不一的透视感。
  • 主体 - 圣诞树
    • 不用粒子堆积,而是用 3D 礼物盒 (BoxGeometry) 堆叠成圆锥体树形。
    • 礼物盒外观:随机使用红色、深绿、白色、金色。
    • 纹理细节:请使用 HTML5 Canvas API 动态生成“金色十字丝带”贴图赋予每个盒子,不要加载外部图片。
    • 树顶:放置一颗 3D 星星,并带有一个巨大的发光光晕 (Sprite)。
    • UI:屏幕中央有一个巨大的金色 "MERRY CHRISTMAS" 文字,默认隐藏。

2. 交互功能要求 (MediaPipe):

  • 使用 CDN 引入 MediaPipe Hands 库。
  • 关键逻辑:检测拇指与食指指尖的距离,计算 openness (张开度)。
  • 手势反馈
    • 静止/闭合时:树缓慢自转,背景粒子缓慢下落(像雪花)。
    • 张开手时
      1. 树的旋转速度加快,并整体轻微放大(呼吸感)。
      2. 背景粒子受到“反重力”作用,改为向上飞升。
      3. "MERRY CHRISTMAS" 文字显现并放大。
  • 调试功能:在屏幕左上角显示状态文字(如“系统正常”),在右下角显示一个小圆点,当检测到手时圆点变亮,且随手势张开度变大。

3. 技术与兼容性修正(非常重要):

  • 摄像头隐藏:Video 元素必须存在于 DOM 中才能被识别,但不要显示出来。请设置 opacity: 0z-index: -10,千万不要设置 display: none 或将其移出屏幕坐标范围(如 left: -9999px),否则浏览器会停止渲染视频帧导致识别失效。
  • 单文件:所有 CSS、JS 代码直接写在 HTML 中,方便运行。

📚 项目代码(项目分析都在下面哦)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Christmas Magic Tree</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #000000; /* 纯黑背景 */
            font-family: 'Arial', sans-serif;
        }
        #canvas-container {
            width: 100vw;
            height: 100vh;
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
        }
        
        /* 
           【关键修复】 
           不能使用 display: none 或将位置移出屏幕 (-9999px),
           否则浏览器会暂停视频渲染,导致 MediaPipe 接收不到画面。
           解决方法:放在屏幕内,但设为透明且层级最低。
        */
        #video-input {
            position: absolute;
            top: 0;
            left: 0;
            width: 640px;
            height: 480px;
            opacity: 0;      /* 透明不可见 */
            z-index: -10;    /* 放在最底层 */
            pointer-events: none;
        }

        /* 状态提示 */
        #status-pill {
            position: absolute;
            top: 20px;
            left: 20px;
            padding: 8px 16px;
            background: rgba(20, 20, 20, 0.8);
            border: 1px solid rgba(255, 215, 0, 0.3);
            border-radius: 20px;
            color: #FFD700;
            font-size: 13px;
            z-index: 10;
            backdrop-filter: blur(4px);
            transition: all 0.3s;
        }

        /* 圣诞文字 */
        #christmas-text {
            position: absolute;
            top: 15%;
            left: 50%;
            transform: translate(-50%, -50%) scale(0.5);
            font-size: 4rem;
            font-weight: 900;
            color: #FFD700;
            text-shadow: 0 0 20px #FFD700, 0 0 40px #ff4500;
            z-index: 5;
            opacity: 0;
            transition: all 0.4s ease-out;
            pointer-events: none;
            white-space: nowrap;
            font-family: serif;
        }

        /* 调试点:右下角,检测到手时变亮 */
        #debug-dot {
            position: absolute;
            bottom: 20px;
            right: 20px;
            width: 12px;
            height: 12px;
            background-color: #FFD700;
            border-radius: 50%;
            box-shadow: 0 0 15px #FFD700;
            z-index: 10;
            opacity: 0.1;
            transition: opacity 0.2s, transform 0.1s;
        }
    </style>
    <!-- 引入库 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <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="status-pill">系统初始化... (请允许摄像头权限)</div>
    <div id="debug-dot"></div>
    <h1 id="christmas-text">MERRY CHRISTMAS</h1>
    
    <!-- 视频源 -->
    <video id="video-input" playsinline></video>
    <!-- 3D 画布 -->
    <div id="canvas-container"></div>

<script>
    // ================= 配置参数 =================
    const CONFIG = {
        particleCount: 8000,      // 粒子数量
        particleColor: 0xFFD700,  // 暖金色
        bgColor: 0x000000,        // 纯黑背景
        treeLevels: 10,
        boxSize: 0.65
    };

    // ================= 全局变量 =================
    let scene, camera, renderer;
    let giftTreeGroup, starMesh, starGlow;
    let particleSystem;
    
    let openness = 0;
    let currentOpenness = 0;
    let isHandDetected = false;

    // 入口
    window.onload = function() {
        initThreeJS();
        startCameraApp(); // 启动摄像头逻辑
        animate();
    };

    // ================= 1. Three.js 场景构建 =================
    function initThreeJS() {
        scene = new THREE.Scene();
        scene.background = new THREE.Color(CONFIG.bgColor); // 强制纯黑
        scene.fog = new THREE.FogExp2(CONFIG.bgColor, 0.02); // 黑色雾气

        camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 5, 25);
        camera.lookAt(0, 2, 0);

        // 关闭 alpha 通道,强制不透明,解决背景变白问题
        renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        document.getElementById('canvas-container').appendChild(renderer.domElement);

        // 灯光
        const ambient = new THREE.AmbientLight(0xffffff, 0.4);
        scene.add(ambient);
        const dirLight = new THREE.DirectionalLight(0xffd700, 1.2);
        dirLight.position.set(10, 20, 10);
        dirLight.castShadow = true;
        scene.add(dirLight);

        createGiftTree();
        createParticles();

        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });
    }

    // --- 礼物树构建 ---
    function createRibbonTexture(colorHex) {
        const canvas = document.createElement('canvas');
        canvas.width = 64; canvas.height = 64;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = '#' + new THREE.Color(colorHex).getHexString();
        ctx.fillRect(0,0,64,64);
        ctx.fillStyle = '#FFD700'; // 金色十字丝带
        ctx.fillRect(28,0,8,64);
        ctx.fillRect(0,28,64,8);
        return new THREE.MeshStandardMaterial({ map: new THREE.CanvasTexture(canvas), roughness: 0.4 });
    }

    function createGiftTree() {
        giftTreeGroup = new THREE.Group();
        const colors = [0xB22222, 0x006400, 0xFFFFFF, 0xDAA520]; // 红、绿、白、金
        const materials = colors.map(c => createRibbonTexture(c));
        const geom = new THREE.BoxGeometry(1, 1, 1);

        let y = 0;
        for(let i=0; i<CONFIG.treeLevels; i++) {
            const p = i/CONFIG.treeLevels;
            const r = 4.5 * (1-p) + 0.1;
            const s = CONFIG.boxSize * (1 - p*0.3);
            const count = Math.max(1, Math.floor( (2*Math.PI*r) / (s*1.2) ));
            
            if(i === CONFIG.treeLevels-1) { 
                // 树顶
                const box = new THREE.Mesh(geom, materials[3]); 
                box.position.set(0, y, 0);
                box.scale.setScalar(s);
                box.castShadow = true;
                giftTreeGroup.add(box);
            } else {
                for(let j=0; j<count; j++) {
                    const ang = (j/count)*Math.PI*2 + i*0.5;
                    const box = new THREE.Mesh(geom, materials[Math.floor(Math.random()*materials.length)]);
                    // 随机扰动位置
                    box.position.set(Math.cos(ang)*r + (Math.random()-0.5)*0.1, y, Math.sin(ang)*r + (Math.random()-0.5)*0.1);
                    box.rotation.set(Math.random()*0.2, ang + Math.random(), Math.random()*0.2);
                    box.scale.setScalar(s);
                    box.castShadow = true;
                    box.receiveShadow = true;
                    giftTreeGroup.add(box);
                }
            }
            y += s * 0.95;
        }

        // 星星
        const starGeo = new THREE.OctahedronGeometry(0.8, 0);
        const starMat = new THREE.MeshBasicMaterial({ color: 0xFFFF00 });
        starMesh = new THREE.Mesh(starGeo, starMat);
        starMesh.position.y = y + 0.5;
        giftTreeGroup.add(starMesh);

        // 星星光晕
        const canvas = document.createElement('canvas');
        canvas.width = 64; canvas.height = 64;
        const ctx = canvas.getContext('2d');
        const grad = ctx.createRadialGradient(32,32,0,32,32,32);
        grad.addColorStop(0, 'rgba(255,215,0,1)');
        grad.addColorStop(1, 'rgba(0,0,0,0)');
        ctx.fillStyle = grad; ctx.fillRect(0,0,64,64);
        
        starGlow = new THREE.Sprite(new THREE.SpriteMaterial({ 
            map: new THREE.CanvasTexture(canvas), 
            color: 0xffaa00, blending: THREE.AdditiveBlending 
        }));
        starGlow.scale.set(6,6,1);
        starMesh.add(starGlow);

        giftTreeGroup.position.y = -y/2;
        scene.add(giftTreeGroup);
    }

    // --- 暖色粒子系统 ---
    function createParticles() {
        const geom = new THREE.BufferGeometry();
        const pos = [];
        const sizes = [];
        const vels = [];

        for(let i=0; i<CONFIG.particleCount; i++) {
            pos.push((Math.random()-0.5)*120);
            pos.push((Math.random()-0.5)*80);
            pos.push((Math.random()-0.5)*80);
            sizes.push(Math.random()*0.6 + 0.1);
            vels.push(Math.random());
        }

        geom.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
        geom.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1));
        geom.setAttribute('velocity', new THREE.Float32BufferAttribute(vels, 1));

        const canvas = document.createElement('canvas');
        canvas.width=32; canvas.height=32;
        const ctx=canvas.getContext('2d');
        const g=ctx.createRadialGradient(16,16,0,16,16,16);
        g.addColorStop(0,'rgba(255,255,255,1)');
        g.addColorStop(1,'rgba(0,0,0,0)');
        ctx.fillStyle=g; ctx.fillRect(0,0,32,32);

        const mat = new THREE.PointsMaterial({
            color: CONFIG.particleColor,
            size: 0.5,
            map: new THREE.CanvasTexture(canvas),
            transparent: true, opacity: 0.9,
            blending: THREE.AdditiveBlending,
            depthWrite: false,
            sizeAttenuation: true
        });

        particleSystem = new THREE.Points(geom, mat);
        scene.add(particleSystem);
    }

    // ================= 2. 手势识别 (修复版) =================
    function startCameraApp() {
        const videoElement = document.getElementById('video-input');
        const statusPill = document.getElementById('status-pill');
        const debugDot = document.getElementById('debug-dot');

        const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`});
        
        hands.setOptions({
            maxNumHands: 1,
            modelComplexity: 1,
            minDetectionConfidence: 0.5,
            minTrackingConfidence: 0.5
        });

        hands.onResults((results) => {
            if(results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
                // 检测到手
                if(!isHandDetected) {
                    statusPill.innerText = "系统正常:手势已连接";
                    statusPill.style.borderColor = "#00FF00";
                    statusPill.style.color = "#00FF00";
                    debugDot.style.opacity = 1; // 亮起小黄点
                    isHandDetected = true;
                }
                
                const lm = results.multiHandLandmarks[0];
                // 计算食指(8)与拇指(4)距离
                const dist = Math.hypot(lm[4].x - lm[8].x, lm[4].y - lm[8].y);
                
                // 视觉反馈:小黄点随张开程度变大
                debugDot.style.transform = `scale(${1 + dist * 8})`;

                // 映射距离到 0-1 之间
                const val = (dist - 0.03) * 6.0;
                openness = Math.max(0, Math.min(1, val));
            } else {
                // 未检测到手
                if(isHandDetected) {
                    statusPill.innerText = "待机:请在镜头前张开手";
                    statusPill.style.borderColor = "rgba(255, 215, 0, 0.3)";
                    statusPill.style.color = "#FFD700";
                    debugDot.style.opacity = 0.1;
                    isHandDetected = false;
                }
                openness = 0; // 归零
            }
        });

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

        camera.start()
            .then(() => {
                statusPill.innerText = "摄像已启动,请展示手势";
            })
            .catch(err => {
                console.error(err);
                statusPill.innerText = "错误: 无法访问摄像头";
                statusPill.style.color = "red";
            });
    }

    // ================= 3. 动画循环 =================
    function animate() {
        requestAnimationFrame(animate);
        const time = Date.now() * 0.001;

        // 平滑过渡
        currentOpenness += (openness - currentOpenness) * 0.1;

        // 树
        if(giftTreeGroup) {
            giftTreeGroup.rotation.y = time * 0.2;
            // 张开手:加速旋转 + 放大
            if(currentOpenness > 0.1) {
                giftTreeGroup.rotation.y += currentOpenness * 0.3; 
                const s = 1 + currentOpenness * 0.15;
                giftTreeGroup.scale.set(s, s, s);
            } else {
                giftTreeGroup.scale.lerp(new THREE.Vector3(1,1,1), 0.1);
            }
        }

        // 星星
        if(starMesh) {
            starMesh.rotation.y -= 0.05;
            // 随张开闪烁
            const gs = 6 + Math.sin(time*3) + currentOpenness * 5;
            starGlow.scale.set(gs, gs, 1);
        }

        // 粒子
        if(particleSystem) {
            const pos = particleSystem.geometry.attributes.position.array;
            const vels = particleSystem.geometry.attributes.velocity.array;

            for(let i=0; i<pos.length; i+=3) {
                const idx = i/3;
                // 默认下落
                let vy = -vels[idx] * 0.03; 
                
                // 手势互动:向上飞升
                if(currentOpenness > 0.1) {
                    vy = vels[idx] * 0.5 * currentOpenness;
                }

                pos[i+1] += vy;
                
                // 循环边界
                if(pos[i+1] < -40) pos[i+1] = 40;
                if(pos[i+1] > 40) pos[i+1] = -40;
                
                // 左右飘动
                pos[i] += Math.sin(time + idx) * 0.015;
            }
            particleSystem.geometry.attributes.position.needsUpdate = true;
        }

        // 文字
        const text = document.getElementById('christmas-text');
        if(currentOpenness > 0.6) {
            text.style.opacity = 1;
            text.style.transform = `translate(-50%, -50%) scale(${1 + currentOpenness * 0.2})`;
        } else {
            text.style.opacity = 0;
            text.style.transform = `translate(-50%, -50%) scale(0.5)`;
        }

        renderer.render(scene, camera);
    }
</script>
</body>
</html>

🌟 项目简介 | Project Introduction

这是一个基于 Three.js 和 MediaPipe Hands 构建的前端交互式 3D 圣诞树项目。通过摄像头捕捉你的手势,你可以通过手指张合控制圣诞树的旋转速度、缩放比例以及周围粒子的运动方向。当手势张开到一定程度时,屏幕上还会出现"MERRY CHRISTMAS"的节日祝福文字。整个交互过程流畅自然,营造出浓厚的节日氛围!🎄✨

📌 前提条件 | Prerequisites

  1. 现代浏览器: Chrome/Firefox/Safari 最新版
  2. 摄像头设备: 内置或外接摄像头
  3. 基础 HTML/CSS/JS 知识: 有助于理解代码结构

🚀 核心技术栈 | Core Technologies

技术 用途 链接
Three.js WebGL 3D 渲染引擎 threejs.org
MediaPipe 手部关键点识别 mediapipe.dev
HTML5 Canvas 3D场景渲染容器 -

👐 手势控制原理 | Hand Control Principle

通过 MediaPipe 获取手部 21 个关键点坐标,我们重点关注拇指(索引4)和食指(索引8)之间的距离,计算两指间的欧几里得距离,并将其映射到 0-1 范围内,用于控制场景中的各种元素。

  • 拇指(Thumb tip): 索引 4
  • 食指指尖(Index Finger Tip): 索引 8
  • 距离计算: Math.hypot(lm[4].x - lm[8].x, lm[4].y - lm[8].y)
  • 数值映射: 将原始距离映射到 0-1 范围,便于统一控制

🧩 核心代码片段 | Core Code Snippets

1. 场景初始化 | Scene Initialization

scene = new THREE.Scene();
scene.background = new THREE.Color(CONFIG.bgColor); // 强制纯黑
scene.fog = new THREE.FogExp2(CONFIG.bgColor, 0.02); // 黑色雾气

camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 25);
camera.lookAt(0, 2, 0);

// 关闭 alpha 通道,强制不透明,解决背景变白问题
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

2. 手势识别配置 | Hand Detection Configuration

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

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

hands.onResults((results) => {
    if(results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
        // 检测到手
        const lm = results.multiHandLandmarks[0];
        // 计算食指(8)与拇指(4)距离
        const dist = Math.hypot(lm[4].x - lm[8].x, lm[4].y - lm[8].y);
        
        // 映射距离到 0-1 之间
        const val = (dist - 0.03) * 6.0;
        openness = Math.max(0, Math.min(1, val));
    } else {
        openness = 0; // 归零
    }
});

3. 手势控制逻辑 | Hand Control Logic

// 平滑过渡
currentOpenness += (openness - currentOpenness) * 0.1;

// 树
if(giftTreeGroup) {
    giftTreeGroup.rotation.y = time * 0.2;
    // 张开手:加速旋转 + 放大
    if(currentOpenness > 0.1) {
        giftTreeGroup.rotation.y += currentOpenness * 0.3; 
        const s = 1 + currentOpenness * 0.15;
        giftTreeGroup.scale.set(s, s, s);
    } else {
        giftTreeGroup.scale.lerp(new THREE.Vector3(1,1,1), 0.1);
    }
}

4. 粒子系统更新 | Particle System Update

// 粒子
if(particleSystem) {
    const pos = particleSystem.geometry.attributes.position.array;
    const vels = particleSystem.geometry.attributes.velocity.array;

    for(let i=0; i<pos.length; i+=3) {
        const idx = i/3;
        // 默认下落
        let vy = -vels[idx] * 0.03; 
        
        // 手势互动:向上飞升
        if(currentOpenness > 0.1) {
            vy = vels[idx] * 0.5 * currentOpenness;
        }

        pos[i+1] += vy;
        
        // 循环边界
        if(pos[i+1] < -40) pos[i+1] = 40;
        if(pos[i+1] > 40) pos[i+1] = -40;
        
        // 左右飘动
        pos[i] += Math.sin(time + idx) * 0.015;
    }
    particleSystem.geometry.attributes.position.needsUpdate = true;
}

🎮 交互机制 | Interaction Mechanics

  1. 手势控制: 通过拇指和食指的张合控制场景元素
  2. 礼物树旋转: 手指张开时,礼物树会加速旋转和放大
  3. 粒子飞舞: 手指张开时,粒子会向上飞升,形成不同的视觉效果
  4. 节日祝福: 当手势张开到一定程度时,会出现"MERRY CHRISTMAS"文字

🛠️ 使用指南 | Run Guide

本地运行 | Local Run

  1. 将代码保存为 html 文件
  2. 用现代浏览器直接打开即可(需允许摄像头权限)
  3. 对着摄像头做出张合手势,观察效果

🔧 定制项 | Customization Options

项目 修改方法 效果预览
粒子颜色 更改 particleColor: 0xFFD700 🟡 金色粒子 → 🔴 红色粒子
礼物树样式 修改 [createGiftTree](file:///Users/jindijsun/Desktop/jin_files/code/test/weekly-report/AI-Test/demo09.html#L129-L189) 函数 🎁 礼物盒 → 🌲 圣诞树
背景颜色 更改 bgColor: 0x000000 ⚫ 黑色 → 🔵 蓝色
粒子数量 调整 particleCount: 8000 🌫️ 稀疏 → 😘 浓密

🐛 常见问题 | Troubleshooting

  1. 摄像头无法启动?

    • 检查浏览器权限设置
    • 确保没有其他程序占用摄像头
  2. 手势识别不准确?

    • 保持手部在摄像头清晰可见范围内
    • 调整环境光线避免过暗或过曝
  3. 动画运行卡顿?

    • 降低粒子数量
    • 关闭其他占用资源的程序

📚 扩展学习资源 | Extended Resources

Conclusion | 结语

  • That's all for today~ - | 今天就写到这里啦~

  • Guys, ( ̄ω ̄( ̄ω ̄〃 ( ̄ω ̄〃)ゝ See you tomorrow~~ | 小伙伴们,( ̄ω ̄( ̄ω ̄〃 ( ̄ω ̄〃)ゝ我们明天再见啦~~

  • Everyone, be happy every day! 大家要天天开心哦

  • Welcome everyone to point out any mistakes in the article~ | 欢迎大家指出文章需要改正之处~

  • Learning has no end; win-win cooperation | 学无止境,合作共赢

  • Welcome all the passers-by, boys and girls, to offer better suggestions! ~~~ | 欢迎路过的小哥哥小姐姐们提出更好的意见哇~~

image

posted @ 2025-12-07 02:23  糖~豆豆  阅读(514)  评论(0)    收藏  举报
Live2D