página web “Partículas Interactivas con Manos”
El HTML usa MediaPipe Hands para detectar la mano en la webcam y, a partir de sus “landmarks”, calcula apertura (mano abierta/cerrada) y posición (x,y) de la mano para controlar el sistema de partículas hecho con Three.js. [pueden verse ejemplos parecidos en fritz y youtube]
Librerías que intervienen
three(import map): se usa para toda la parte 3D (escena, cámara, partículas). ver fuente realacionada en developer.mozilla@mediapipe/camera_utils: da la claseCameraque gestiona la captura de vídeo de la webcam y llama aonFrame. ver fuente realacionada en fritz@mediapipe/hands: da la claseHands, que ejecuta el modelo de IA de detección de manos y entregamultiHandLandmarks. ver fuente realacionada en youtube y fritz@mediapipe/control_utilsydrawing_utilspodrían usarse para UI y dibujo, pero en este código solo se cargan; la lógica de gestos la implementas tú enonResults.ver fuente realacionada en fritz
Variables que representan el gesto
En la sección // --- 5. MEDIAPIPE HAND TRACKING --- se declaran las variables de estado que resumen el gesto:
handOpenness: valor en [0,1] que representa qué tan abierta está la mano (0 cerrada, 1 abierta).handX,handY: posición de la mano normalizada al sistema de coordenadas de Three.js, de −1 a 1.isHandDetected: indica si hay una mano detectada (para cambiar estado de la UI).
Estas variables son las que luego se usan en el bucle de animación para escalar, agitar y rotar las partículas.
Función onResults(results): interpretar landmarks
Esta función es el “callback” que MediaPipe Hands llama cada vez que procesa un frame:
- Comprobación de mano detectada
- Si
results.multiHandLandmarkstiene al menos una mano, se marcaisHandDetected = true, se pone el punto de estado en verde y el texto “Mano detectada”. - Si no hay manos, se vuelve a “Buscando mano…” y se relaja la apertura hacia 1 (mano abierta por defecto).
- Si
- Lectura de landmarks
landmarks = results.multiHandLandmarks[0]toma la primera mano.- Usa índices estándar de MediaPipe:
landmarks[4]: punta del pulgar (thumbTip).landmarks[8]: punta del índice (indexTip).landmarks[0]: muñeca (wrist).fritz
- Cálculo de apertura de mano (
handOpenness)- Calcula la distancia euclidiana en 2D entre pulgar e índice: js
const distance = Math.sqrt( Math.pow(thumbTip.x - indexTip.x, 2) + Math.pow(thumbTip.y - indexTip.y, 2) ); - Normaliza esa distancia a un rango 0–1: js
let openness = (distance - 0.05) * 5; openness = Math.max(0, Math.min(1, openness)); - Suaviza la transición con un lerp para evitar jitter: js
handOpenness += (openness - handOpenness) * 0.1;
distancees pequeña → mano “cerrada” (handOpenness bajo); si están separados, mano “abierta”.fritz - Calcula la distancia euclidiana en 2D entre pulgar e índice: js
- Cálculo de posición de mano (
handX,handY)- Toma la muñeca como punto de referencia de la mano:
wrist. - Convierte coordenadas MediaPipe [0,1] a sistema Three.js [−1,1] e invierte Y: js
const targetX = (wrist.x - 0.5) * 2; // 0→-1, 1→1 const targetY = -(wrist.y - 0.5) * 2; // invierte eje vertical - Aplica también suavizado: js
handX += (targetX - handX) * 0.1; handY += (targetY - handY) * 0.1;
- Toma la muñeca como punto de referencia de la mano:
Objeto Hands y cámara MediaPipe
Debajo se instancia y configura el detector de manos:
const hands = new Hands({ locateFile: (file) => ... }): indica a MediaPipe dónde cargar los archivos del modelo desde el CDN.fritzhands.setOptions({ maxNumHands: 1, modelComplexity: 1, ... }): limita a 1 mano y fija umbrales de detección y seguimiento.hands.onResults(onResults): registra la función anterior como callback de resultados.
La cámara se configura con Camera de camera_utils:
jsconst cameraUtils = new Camera(videoElement, {
onFrame: async () => {
await hands.send({ image: videoElement });
},
width: 640,
height: 480
});
cameraUtils.start();
Cameracaptura de la webcam al<video>oculto (#input-video).- En cada frame llama a
onFrame, que a su vez envía la imagen actual ahands.send, disparando el pipeline de MediaPipe y finalmenteonResults.fritz
Uso de handOpenness, handX, handY en las partículas
En el bucle animate() se conectan las variables de gesto con la animación:
baseScale = 0.2 + (handOpenness * 0.8):- Mano abierta (
handOpenness≈1) → escala grande, la figura se expande. - Mano cerrada (
handOpenness≈0) → escala pequeña, la figura se contrae.
- Mano abierta (
noiseAmplitude = (1.0 - handOpenness) * 0.3:- Mano cerrada → ruido alto → partículas vibran, efecto “energía”.
- Mano abierta → ruido bajo → partículas más estables.
- Rotación según posición: js
particles.rotation.y = handX * 1.5 + time * 0.1; particles.rotation.x = handY * 1.5;Mover la mano horizontal/verticalmente rota el conjunto de partículas, y se suma una pequeña rotación automática con el tiempo. - En el bucle por partícula, se hace “morphing” suave hacia
targetPositionsescaladas porbaseScale, y se suma ruido aleatorio proporcional anoiseAmplitudecuando la mano está cerrada.
Con todo esto, el flujo queda así: webcam → Camera → Hands (MediaPipe) → onResults (landmarks → handOpenness, handX, handY) → animate() (aplica esos valores a escala, ruido y rotación de las partículas).youtubefritz
El siguiente paso podría ser añadir otra dimensión al gesto (por ejemplo, usar la altura de la mano o el número de dedos extendidos) para cambiar entre formas (corazón/flor/Saturno/fuegos) sin tocar el ratón.
- https://fritz.ai/introduction-to-hand-detection-in-the-browser-with-handtrack-js-and-tensorflow/
- https://www.youtube.com/watch?v=eI-d5yuPeJw
- https://developer.mozilla.org/es/docs/Learn_web_development/Core/Structuring_content/Basic_HTML_syntax
- VirtualWorlds.html
Aunque creo que la verdadera continuación de este capítulo podría estar en GitHub – Viral-Doshi/Gesture-Controlled-Virtual-Mouse: Virtually controlling computer using hand-gestures and voice commands. Using MediaPipe, OpenCV Python.