提示词:
请使用 Three.js + MediaPipe 编写一个名为 “圣诞指尖魔法” 的 3D 粒子交互特效网页。 核心主题: 营造沉浸式圣诞氛围,粒子要有如圣诞彩灯般的发光质感,配色以红、绿、金、白为主。 具体功能需求:
🖐️ 手势实时控制(无延迟): 通过摄像头实时识别手指数量。 伸出 1 根手指: 凝聚出由红色发光粒子组成的数字 “1”(象征驯鹿红鼻子)。 伸出 2 根手指: 凝聚出由绿色发光粒子组成的数字 “2”(象征圣诞树)。 伸出 3 根手指: 凝聚出由金色发光粒子组成的数字 “3”(象征铃铛/星星)。 注意:数字直接显示,不需要倒计时。 🎅 终极节日特效(五指张开): 当识别到五指张开时,粒子瞬间重组爆炸,形成巨大的 “MERRY XMAS” 多彩粒子文字。 同步特效: 背景瞬间炸开烟花效果,并从底部升起大量的 3D 礼物盒(立方体)、雪花(六边形) 和 彩球,营造狂欢氛围。 ❄️ 魔法重置(握拳): 当识别到握拳时,触发“暴风雪消除”效果:所有粒子和物体被风吹散消失,屏幕清空,重置回待机状态。 移除所有调试信息和识别进度条。 🌃 视觉与 UI 环境: 背景: 深邃的午夜蓝色渐变背景,带有持续缓慢飘落的背景雪花效果(无论手势如何,背景始终有微雪)。 UI: 仅保留右上角一个半透明的白色全屏切换按钮,风格极简。 视角: 摄像机位置需适中,确保 “MERRY XMAS” 文字和上升的礼物盒能完整显示在屏幕内。 💻 代码环境兼容性: 输出为单文件 JavaScript 代码(不含 HTML/CSS 外壳)。 移除 ,
, 等标签,假设代码将直接运行在 CodePen 的 JS 面板中。 使用 CDN 链接引入 Three.js 和 MediaPipe 库。演示视频
源代码
<style>
/* 纯净黑色背景 & UI 设置 */
:root {
--bg-color: #0b0f19; /* 深蓝夜空背景,比纯黑更有氛围 */
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
/* CodePen 环境通常自带 body,这里确保全屏覆盖 */
#canvas-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: radial-gradient(circle at center, #1a2035 0%, #000000 100%);
overflow: hidden;
z-index: 1;
}
/* 隐藏摄像头原始画面,仅用于计算 */
#video-input {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
/* 右上角全屏按钮 */
#fs-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 100;
background: rgba(212, 175, 55, 0.2); /* 金色半透明 */
border: 1px solid rgba(212, 175, 55, 0.5);
color: #ffd700;
padding: 10px 15px;
border-radius: 8px;
cursor: pointer;
font-family: sans-serif;
font-size: 14px;
transition: all 0.3s;
backdrop-filter: blur(5px);
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 0 10px rgba(212, 175, 55, 0.2);
}
#fs-btn:hover {
background: rgba(212, 175, 55, 0.4);
box-shadow: 0 0 20px rgba(212, 175, 55, 0.6);
}
/* 加载提示 */
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(255,255,255,0.8);
font-family: monospace;
font-size: 1.5rem;
z-index: 50;
pointer-events: none;
text-align: center;
text-shadow: 0 0 10px rgba(255,255,255,0.5);
}
</style>
<div id="canvas-container"></div>
<video id="video-input" autoplay playsinline></video>
<div id="loading">✨ Loading Christmas Magic... ✨<br><span style="font-size:0.8em; opacity:0.6">Please allow camera access</span></div>
<button id="fs-btn">Start Magic</button>
<script type="module">
import * as THREE from 'https://esm.sh/three@0.154.0';
import { FontLoader } from 'https://esm.sh/three@0.154.0/examples/jsm/loaders/FontLoader.js';
import { TextGeometry } from 'https://esm.sh/three@0.154.0/examples/jsm/geometries/TextGeometry.js';
// 引入采样器,用于在表面均匀生成粒子
import { MeshSurfaceSampler } from 'https://esm.sh/three@0.154.0/examples/jsm/math/MeshSurfaceSampler.js';
import { FilesetResolver, HandLandmarker } from 'https://esm.sh/@mediapipe/tasks-vision@0.10.3';
// --- 圣诞特别版配置 ---
const SETTINGS = {
cameraWidth: 640,
cameraHeight: 480,
particleCount: 8000, // 大幅增加粒子数量,消除断层
particleSize: 0.28, // 增大粒子尺寸,让数字更显眼
balloonCount: 40,
colors: {
one: 0xFF0033, // 圣诞红
two: 0x00FF44, // 圣诞绿
three: 0xFFD700, // 金色
snow: 0xFFFFFF,
ornaments: [0xFF0033, 0x00FF44, 0xFFD700, 0xE0E0E0, 0x3366FF]
}
};
// --- 全局变量 ---
let scene, camera, renderer;
let handLandmarker;
let font;
let activeParticles = null;
let balloons = [];
let snowSystem = null;
let currentGesture = 'none';
let isRunning = false;
const videoElement = document.getElementById('video-input');
const container = document.getElementById('canvas-container');
const loadingEl = document.getElementById('loading');
// --- 初始化入口 ---
async function init() {
initThree();
createSnow();
await loadResources();
setupUI();
animate();
}
// 1. Three.js 场景初始化
function initThree() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0b0f19, 0.02);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 30;
camera.position.y = 0;
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.4);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffd700, 1.2);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);
const pointLight = new THREE.PointLight(0xff0033, 0.8, 50);
pointLight.position.set(-10, 0, 10);
scene.add(pointLight);
window.addEventListener('resize', onWindowResize);
}
// 创建飘雪效果
function createSnow() {
const particleCount = 2000;
const geometry = new THREE.BufferGeometry();
const positions = [];
const velocities = [];
for (let i = 0; i < particleCount; i++) {
positions.push(
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 60
);
velocities.push(
(Math.random() - 0.5) * 0.1,
-Math.random() * 0.2 - 0.05,
0
);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.4,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending,
depthWrite: false
});
snowSystem = new THREE.Points(geometry, material);
snowSystem.userData = { velocities: velocities };
scene.add(snowSystem);
}
function updateSnow() {
if (!snowSystem) return;
const positions = snowSystem.geometry.attributes.position.array;
const velocities = snowSystem.userData.velocities;
for (let i = 0; i < positions.length / 3; i++) {
positions[i * 3] += velocities[i * 3];
positions[i * 3 + 1] += velocities[i * 3 + 1];
if (positions[i * 3 + 1] < -30) {
positions[i * 3 + 1] = 30;
positions[i * 3] = (Math.random() - 0.5) * 100;
}
}
snowSystem.geometry.attributes.position.needsUpdate = true;
snowSystem.rotation.y += 0.001;
}
// 2. 资源加载
async function loadResources() {
try {
const loader = new FontLoader();
font = await new Promise((resolve) => {
loader.load('https://esm.sh/three@0.154.0/examples/fonts/helvetiker_bold.typeface.json', resolve);
});
const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm');
handLandmarker = await HandLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`,
delegate: "GPU"
},
runningMode: "VIDEO",
numHands: 1
});
loadingEl.textContent = "🎄 Holiday Magic Ready 🎄";
} catch (e) {
console.error(e);
loadingEl.textContent = "Error Loading Resources.";
}
}
// 3. 摄像头
async function startCamera() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert("Browser API not supported");
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: SETTINGS.cameraWidth, height: SETTINGS.cameraHeight }
});
videoElement.srcObject = stream;
videoElement.addEventListener('loadeddata', () => {
isRunning = true;
loadingEl.style.display = 'none';
predictWebcam();
});
} catch (e) {
console.error(e);
alert("Camera permission denied.");
}
}
// 4. 粒子文字生成核心 (大幅优化版 - 使用采样器消除断层)
function createParticleText(textString, type = 'normal') {
if (activeParticles) {
scene.remove(activeParticles);
if (activeParticles.geometry) activeParticles.geometry.dispose();
if (activeParticles.material) activeParticles.material.dispose();
activeParticles = null;
}
let positions = [];
let colors = [];
let particleColor;
let geometry;
// 颜色配置
if (textString === "1") particleColor = new THREE.Color(SETTINGS.colors.one);
else if (textString === "2") particleColor = new THREE.Color(SETTINGS.colors.two);
else if (textString === "3") particleColor = new THREE.Color(SETTINGS.colors.three);
else particleColor = new THREE.Color(0xffffff);
// 采样辅助函数
const sampleGeometry = (geo, count, colorObj) => {
const tempMesh = new THREE.Mesh(geo, new THREE.MeshBasicMaterial());
const sampler = new MeshSurfaceSampler(tempMesh).build();
const tempPos = new THREE.Vector3();
for(let i = 0; i < count; i++) {
sampler.sample(tempPos);
positions.push(tempPos.x, tempPos.y, tempPos.z);
colors.push(colorObj.r, colorObj.g, colorObj.b);
}
// 清理临时资源
geo.dispose();
// 注意:tempMesh不需要add到scene,直接会被GC
};
if (type === 'christmas') {
// --- MERRY XMAS (采样版) ---
const merryColor = new THREE.Color(0xFF0033);
const xmasColor = new THREE.Color(0x00FF44);
const goldColor = new THREE.Color(0xFFD700);
// Merry
const merryGeo = new TextGeometry("MERRY", { font: font, size: 4, height: 1.0, curveSegments: 6, bevelEnabled: true, bevelThickness: 0.5, bevelSize: 0.1, bevelSegments: 2 });
merryGeo.center();
merryGeo.translate(0, 3, 0);
sampleGeometry(merryGeo, 4000, merryColor);
// Xmas
const xmasGeo = new TextGeometry("XMAS", { font: font, size: 5, height: 1.0, curveSegments: 6, bevelEnabled: true, bevelThickness: 0.5, bevelSize: 0.1, bevelSegments: 2 });
xmasGeo.center();
xmasGeo.translate(0, -3, 0);
sampleGeometry(xmasGeo, 5000, xmasColor);
// Stars
for(let i=0; i<400; i++) {
const r = 10 + Math.random() * 8;
const theta = Math.random() * Math.PI * 2;
const x = r * Math.cos(theta);
const y = r * Math.sin(theta) * 0.7;
positions.push(x, y, (Math.random()-0.5)*4);
colors.push(goldColor.r, goldColor.g, goldColor.b);
}
} else {
// --- 普通数字 1/2/3 (采样版 - 解决断层) ---
const textGeo = new TextGeometry(textString, {
font: font,
size: 11, // 更大
height: 3, // 更厚
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 1, // 倒角更厚
bevelSize: 0.3,
bevelSegments: 5
});
textGeo.center();
// 使用采样器生成 10000 个点,确保绝对致密
sampleGeometry(textGeo, 10000, particleColor);
}
geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
if (colors.length > 0) {
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
}
const material = new THREE.PointsMaterial({
color: type === 'christmas' ? 0xffffff : particleColor, // 如果有顶点颜色则用白色底,否则用纯色
size: SETTINGS.particleSize,
vertexColors: type === 'christmas', // 圣诞模式启用顶点颜色
blending: THREE.AdditiveBlending,
transparent: true,
opacity: 0.95,
depthWrite: false
});
activeParticles = new THREE.Points(geometry, material);
scene.add(activeParticles);
}
// 5. 圣诞庆祝
function spawnCelebration() {
clearCelebration();
const geometry = new THREE.SphereGeometry(1.2, 32, 32);
for (let i = 0; i < SETTINGS.balloonCount; i++) {
const color = SETTINGS.colors.ornaments[Math.floor(Math.random() * SETTINGS.colors.ornaments.length)];
const material = new THREE.MeshPhysicalMaterial({
color: color,
metalness: 0.7,
roughness: 0.2,
clearcoat: 1.0,
clearcoatRoughness: 0.1
});
const ball = new THREE.Mesh(geometry, material);
const x = (Math.random() - 0.5) * 40;
const y = -25 - Math.random() * 20;
const z = (Math.random() - 0.5) * 15;
ball.position.set(x, y, z);
ball.userData = {
speed: 0.04 + Math.random() * 0.08,
wobble: Math.random() * Math.PI,
wobbleSpeed: 0.02
};
const hookGeo = new THREE.TorusGeometry(0.3, 0.05, 8, 16);
const hookMat = new THREE.MeshBasicMaterial({ color: 0xcccccc });
const hook = new THREE.Mesh(hookGeo, hookMat);
hook.position.y = 1.1;
hook.rotation.y = Math.PI / 2;
ball.add(hook);
scene.add(ball);
balloons.push(ball);
}
}
function clearCelebration() {
balloons.forEach(b => {
scene.remove(b);
b.geometry.dispose();
b.material.dispose();
});
balloons = [];
}
function updateCelebration() {
balloons.forEach(b => {
b.position.y += b.userData.speed;
b.userData.wobble += b.userData.wobbleSpeed;
b.position.x += Math.sin(b.userData.wobble) * 0.02;
b.rotation.x += 0.01;
b.rotation.y += 0.01;
if (b.position.y > 25) {
b.position.y = -25;
}
});
}
// 6. MediaPipe 预测
let lastVideoTime = -1;
async function predictWebcam() {
if (!isRunning) return;
let startTimeMs = performance.now();
if (videoElement.currentTime !== lastVideoTime) {
lastVideoTime = videoElement.currentTime;
const detections = handLandmarker.detectForVideo(videoElement, startTimeMs);
if (detections.landmarks.length > 0) {
const fingers = countExtendedFingers(detections.landmarks[0]);
handleGesture(fingers);
}
}
requestAnimationFrame(predictWebcam);
}
// 7. 手指计数
function countExtendedFingers(landmarks) {
let count = 0;
const distTip4_17 = Math.hypot(landmarks[4].x - landmarks[17].x, landmarks[4].y - landmarks[17].y);
const distIp3_17 = Math.hypot(landmarks[3].x - landmarks[17].x, landmarks[3].y - landmarks[17].y);
if (distTip4_17 > distIp3_17 * 1.1) count++;
if (landmarks[8].y < landmarks[6].y) count++;
if (landmarks[12].y < landmarks[10].y) count++;
if (landmarks[16].y < landmarks[14].y) count++;
if (landmarks[20].y < landmarks[18].y) count++;
return count;
}
// 8. 状态管理
function handleGesture(fingerCount) {
let newState = 'none';
if (fingerCount === 0) newState = 'reset';
else if (fingerCount === 1) newState = '1';
else if (fingerCount === 2) newState = '2';
else if (fingerCount === 3) newState = '3';
else if (fingerCount === 5) newState = 'christmas';
else newState = currentGesture;
if (newState !== currentGesture) {
currentGesture = newState;
applyEffect(newState);
}
}
function applyEffect(state) {
if (state !== 'christmas') clearCelebration();
switch (state) {
case 'reset':
if (activeParticles) {
scene.remove(activeParticles);
activeParticles = null;
}
break;
case '1':
createParticleText("1");
break;
case '2':
createParticleText("2");
break;
case '3':
createParticleText("3");
break;
case 'christmas':
createParticleText("", 'christmas');
spawnCelebration();
break;
}
}
// 9. 渲染循环
function animate() {
requestAnimationFrame(animate);
updateSnow();
if (activeParticles) {
activeParticles.rotation.y = Math.sin(Date.now() * 0.001) * 0.1;
// 粒子微闪效果
if (Math.random() > 0.8) {
activeParticles.material.opacity = 0.8 + Math.random() * 0.2;
}
}
if (balloons.length > 0) {
updateCelebration();
}
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function setupUI() {
const btn = document.getElementById('fs-btn');
btn.addEventListener('click', async () => {
if (!isRunning) {
await startCamera();
btn.textContent = "Toggle Fullscreen";
}
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {});
} else {
document.exitFullscreen();
}
});
}
init();
</script>
