普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月20日首页

【Gemini简直无敌了】掌间星河:通过MediaPipe实现手势控制粒子

2025年12月20日 17:15

受掘金大佬“不如摸鱼去”的启发,我也试试 Gemini 3做一下手势+粒子交互, 确实学到了不少东西,在这里简单的分享下。 github地址:掌间星河:github.com/huijieya/Ge…

效果展示

在这里插入图片描述

基于原生h5、浏览器、PC摄像头实现手势控制粒子特效交互的逻辑,粒子默认离散,类似银河系分布缓慢移动,同时有5种手势:

手势1: 握拳,握拳后粒子聚拢显示爱心的形状

手势2: 展开手掌并挥手,展开手掌挥手后粒子从当前状态恢复到离散状态

手势3: 👆 比 1 :只有食指伸直,其他 3 根弯曲,此时粒子聚拢显示第一句话:春来夏往

手势4: ✌ 比 2 :只有食指和中指伸直,其他 2 根弯曲手此时粒子聚拢显示第二句话: 秋收冬藏

手势5: 👌 比 3 :只有中指、无名指、小指伸直,食指弯曲,此时粒子聚拢显示第三句话:我们来日方长

源码地址

掌间星河:github.com/huijieya/Ge…

源码分析

手势识别流程

1. 手部检测初始化

// HandTracker.tsx 中初始化 MediaPipe Hands
const hands = new (window as any).Hands({
  locateFile: (file: string) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
});

hands.setOptions({
  maxNumHands: 1,           // 最多检测一只手
  modelComplexity: 1,       // 模型复杂度
  minDetectionConfidence: 0.7,  // 最小检测置信度
  minTrackingConfidence: 0.7    // 最小跟踪置信度
});

2. 手部关键点获取

MediaPipe 会检测手部的21个关键点(landmarks),每个关键点包含 x, y, z 坐标:

  • 指尖: 4(拇指), 8(食指), 12(中指), 16(无名指), 20(小指)
  • 指关节: 用于判断手指是否伸直
扩展:MediaPipe

MediaPipe 是由 Google 开发并开源的实时机器学习框架,主要用于构建跨平台的多媒体处理应用,尤其擅长计算机视觉任务。它采用数据流图(Graph)的方式组织模块,支持在设备端(如手机、嵌入式设备)高效运行机器学习模型。

该框架的核心功能包括:

  • 人脸检测‌:提供轻量级模型(如 BlazeFace),可在短距离(前置摄像头)或全范围(后置摄像头)图像中检测人脸,并返回边界框和关键点信息。‌

  • 手部识别‌:检测手部关键点(如21个坐标),支持手势控制、虚拟键盘等交互应用。‌

  • 人体姿态估计‌:识别人体33个关键点,用于动作分析(如健身、舞蹈)或AR滤镜。‌

  • 背景分割‌:实现人物与背景分离,支持虚化或替换背景功能。‌

  • 其他功能‌:还包括虹膜检测、目标追踪、3D物体检测等解决方案,覆盖从基础检测到高级分析的多种需求。‌

3. 手势分类逻辑

// gestureLogic.ts 中的 classifyGesture 函数
const isExtended = (tipIdx: number, mcpIdx: number) => landmarks[tipIdx].y < landmarks[mcpIdx].y;

// 判断各手指是否伸直
const indexExt = isExtended(8, 5);   // 食指
const middleExt = isExtended(12, 9); // 中指
const ringExt = isExtended(16, 13);  // 无名指
const pinkyExt = isExtended(20, 17); // 小指

// 根据手指状态识别不同手势
if (!indexExt && !middleExt && !ringExt && !pinkyExt) {
  return GestureType.HEART; // 握拳 - 显示爱心
}
if (indexExt && middleExt && ringExt && pinkyExt) {
  return GestureType.GALAXY; // 手掌展开 - 银河状态
}
// ... 其他手势判断

粒子绘制机制

1. 粒子系统初始化

// ParticleCanvas.tsx 中初始化粒子
useEffect(() => {
  const particles: Particle[] = [];
  for (let i = 0; i < PARTICLE_COUNT; i++) {
    particles.push({
      x: Math.random() * window.innerWidth,     // 随机初始位置
      y: Math.random() * window.innerHeight,
      targetX: Math.random() * window.innerWidth, // 目标位置
      targetY: Math.random() * window.innerHeight,
      vx: 0,                                    // 速度
      vy: 0,
      size: Math.random() * 1.5 + 0.5,          // 大小
      color: COLORS[Math.floor(Math.random() * COLORS.length)], // 颜色
      alpha: Math.random() * 0.4 + 0.4,         // 透明度
    });
  }
  particlesRef.current = particles;
}, []);

2. 形状生成算法

const getShapePoints = (type: GestureType, width: number, height: number): Point[] => {
  const centerX = width / 2;
  const centerY = height / 2;
  
  switch (type) {
    case GestureType.HEART: 
      // 心形方程参数化生成点
      // x = 16sin³(t)
      // y = 13cos(t) - 5cos(2t) - 2cos(3t) - cos(4t)
      
    case GestureType.TEXT_1/2/3:
      // 使用 Canvas 绘制文字并提取像素点
      
    case GestureType.GALAXY:
    default:
      // 螺旋银河形状
      const angle = Math.random() * Math.PI * 2;
      const r = Math.pow(Math.random(), 0.7) * maxRadius;
      const spiralFactor = 2.0;
      const offset = r * (spiralFactor / maxRadius) * 5;
      points.push({ 
        x: centerX + Math.cos(angle + offset) * r, 
        y: centerY + Math.sin(angle + offset) * r 
      });
  }
}

3. 粒子动画更新

// 粒子运动和渲染循环
const render = () => {
  // 半透明背景覆盖产生拖尾效果
  ctx.fillStyle = 'rgba(0, 0, 0, 0.18)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  particlesRef.current.forEach((p) => {
    // 平滑插值移动到目标位置
    p.x += (tx - p.x) * LERP_FACTOR;
    p.y += (ty - p.y) * LERP_FACTOR;
    
    // 手势交互影响粒子位置
    if (hPos && canInteract) {
      const dx = p.x - hPos.x;
      const dy = p.y - hPos.y;
      const distSq = dx * dx + dy * dy;
      if (distSq < INTERACTION_RADIUS * INTERACTION_RADIUS) {
        // 排斥力计算
        const dist = Math.sqrt(distSq);
        const force = (1 - dist / INTERACTION_RADIUS) * INTERACTION_STRENGTH;
        p.x += dx * force;
        p.y += dy * force;
      }
    }
    
    // 添加随机扰动使粒子更生动
    p.x += (Math.random() - 0.5) * 0.6;
    p.y += (Math.random() - 0.5) * 0.6;
    
    // 绘制粒子
    ctx.fillStyle = p.color;
    ctx.globalAlpha = p.alpha;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
    ctx.fill();
  });
  
  animationRef.current = requestAnimationFrame(render);
};

4. 银河旋转效果

// 银河状态下的旋转动画
galaxyAngleRef.current += GALAXY_ROTATION_SPEED;
const cosA = Math.cos(galaxyAngleRef.current);
const sinA = Math.sin(galaxyAngleRef.current);

// 对每个粒子应用旋转变换
const dx = p.targetX - cx;
const dy = p.targetY - cy;
tx = cx + dx * cosA - dy * sinA;
ty = cy + dx * sinA + dy * cosA;

在这里插入图片描述

❌
❌