// Landmarks -> performance controls. Every control is normalized [0,1] and // derived from in-plane geometry normalized by palm size, so it is invariant to // how close the hand is to the lens. Four INDEPENDENT controls per hand chosen so // they don't false-trigger each other: // HEIGHT (wrist y) -> the hero filter sweep // ROLL (wrist->middle_mcp tilt) -> turntable warp (tempo/pitch) // PINCH (thumb<->index) -> reverb / delay space [ignores m/r/pinky] // FIST (middle+ring+pinky curl) -> stutter buildup [ignores thumb/index] import { H, dist, norm, clamp } from "./math.js"; const palmSize = (lm) => dist(lm[H.WRIST], lm[H.MIDDLE_MCP]) || 1e-6; export function controls(lm) { const ps = palmSize(lm); // HEIGHT: 1 - wrist.y, mapped so a comfortable raise spans the full range. const height = norm(1 - lm[H.WRIST].y, 0.18, 0.82); // PINCH: thumb tip to index tip over palm. ~0.25 pinched … ~1.1 spread. // Tight pinch => high "space" send. const pinchRaw = dist(lm[H.THUMB_TIP], lm[H.INDEX_TIP]) / ps; const pinch = 1 - norm(pinchRaw, 0.28, 0.95); // FIST: how curled the middle/ring/pinky are (tip distance to wrist over palm). // open hand ~2.0, tight fist ~1.05 -> fist amount near 1. const curl = (dist(lm[H.MIDDLE_TIP], lm[H.WRIST]) + dist(lm[H.RING_TIP], lm[H.WRIST]) + dist(lm[H.PINKY_TIP], lm[H.WRIST])) / (3 * ps); const fist = 1 - norm(curl, 1.15, 1.95); // ROLL: tilt of the wrist->middle_mcp vector. 0=straight up; +right / -left. const dx = lm[H.MIDDLE_MCP].x - lm[H.WRIST].x; const dy = lm[H.MIDDLE_MCP].y - lm[H.WRIST].y; const rollAngle = Math.atan2(dx, -dy); // radians const roll = norm(rollAngle, -0.7, 0.7); // 0.5 = upright return { height, pinch, fist, roll, cx: clamp(lm[H.WRIST].x, 0, 1) }; }