AI教程

Gemini3 Pro生成圣诞主题可交互的粒子特效

2025-12-18

提示词:

请使用 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>