AI真好玩系列-圣诞树手势交响曲 | Christmas Tree Gesture Symphony
@
- 💖 提示词
- 📚 项目代码(项目分析都在下面哦)
- 🌟 项目简介 | Project Introduction
- 📌 前提条件 | Prerequisites
- 🚀 核心技术栈 | Core Technologies
- 👐 手势控制原理 | Hand Control Principle
- 🧩 核心代码片段 | Core Code Snippets
- 🎮 交互机制 | Interaction Mechanics
- 🛠️ 使用指南 | Run Guide
- 🔧 定制项 | Customization Options
- 🐛 常见问题 | Troubleshooting
- 📚 扩展学习资源 | Extended Resources
- Conclusion | 结语
周末的深夜,睡不着觉,闲来无事,打开Gemini哈基米玩一把,废话不多说,先上图看最终效果
碎碎念:我很想上传视频,但是好像有的博客不支持,这里先放个图片
视频地址:https://www.bilibili.com/video/BV1tz29BEEh6?t=9.5

💖 提示词
请帮我使用 Three.js 和 MediaPipe Hands 编写一个单文件 HTML 的 3D 互动网页应用。
1. 场景视觉要求:
- 背景:必须是纯黑色 (
0x000000)。请在 Three.js 初始化时强制设置scene.background为黑色,并且WebGLRenderer的alpha设为false,不要依赖 CSS 背景。- 雾效:添加黑色的雾 (
FogExp2),让远处物体自然消隐。- 粒子系统:
- 数量:8000个。
- 颜色:暖金色 (
0xFFD700)。- 材质:使用贴图材质,形状为柔和的发光圆点。
- 渲染:必须开启
sizeAttenuation,实现远近大小不一的透视感。- 主体 - 圣诞树:
- 不用粒子堆积,而是用 3D 礼物盒 (BoxGeometry) 堆叠成圆锥体树形。
- 礼物盒外观:随机使用红色、深绿、白色、金色。
- 纹理细节:请使用 HTML5 Canvas API 动态生成“金色十字丝带”贴图赋予每个盒子,不要加载外部图片。
- 树顶:放置一颗 3D 星星,并带有一个巨大的发光光晕 (Sprite)。
- UI:屏幕中央有一个巨大的金色 "MERRY CHRISTMAS" 文字,默认隐藏。
2. 交互功能要求 (MediaPipe):
- 使用 CDN 引入 MediaPipe Hands 库。
- 关键逻辑:检测拇指与食指指尖的距离,计算
openness(张开度)。- 手势反馈:
- 静止/闭合时:树缓慢自转,背景粒子缓慢下落(像雪花)。
- 张开手时:
- 树的旋转速度加快,并整体轻微放大(呼吸感)。
- 背景粒子受到“反重力”作用,改为向上飞升。
- "MERRY CHRISTMAS" 文字显现并放大。
- 调试功能:在屏幕左上角显示状态文字(如“系统正常”),在右下角显示一个小圆点,当检测到手时圆点变亮,且随手势张开度变大。
3. 技术与兼容性修正(非常重要):
- 摄像头隐藏:Video 元素必须存在于 DOM 中才能被识别,但不要显示出来。请设置
opacity: 0并z-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
- 现代浏览器: Chrome/Firefox/Safari 最新版
- 摄像头设备: 内置或外接摄像头
- 基础 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
- 手势控制: 通过拇指和食指的张合控制场景元素
- 礼物树旋转: 手指张开时,礼物树会加速旋转和放大
- 粒子飞舞: 手指张开时,粒子会向上飞升,形成不同的视觉效果
- 节日祝福: 当手势张开到一定程度时,会出现"MERRY CHRISTMAS"文字
🛠️ 使用指南 | Run Guide
本地运行 | Local Run
- 将代码保存为 html 文件
- 用现代浏览器直接打开即可(需允许摄像头权限)
- 对着摄像头做出张合手势,观察效果
🔧 定制项 | 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
-
摄像头无法启动?
- 检查浏览器权限设置
- 确保没有其他程序占用摄像头
-
手势识别不准确?
- 保持手部在摄像头清晰可见范围内
- 调整环境光线避免过暗或过曝
-
动画运行卡顿?
- 降低粒子数量
- 关闭其他占用资源的程序
📚 扩展学习资源 | 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! ~~~ | 欢迎路过的小哥哥小姐姐们提出更好的意见哇~~


浙公网安备 33010602011771号