function palmCenter(lms) {
const idx = [0, 5, 9, 13, 17];
let x = 0,
y = 0;
for (let i = 0; i < idx.length; i++) {
x += lms[idx[i]].x;
y += lms[idx[i]].y;
}
return { x: x / idx.length, y: y / idx.length };
}
function angleAt(a, b, c) {
// 각 b (°)
const v1 = { x: a.x - b.x, y: a.y - b.y };
const v2 = { x: c.x - b.x, y: c.y - b.y };
const n1 = Math.hypot(v1.x, v1.y) || 1e-6;
const n2 = Math.hypot(v2.x, v2.y) || 1e-6;
const cos = (v1.x * v2.x + v1.y * v2.y) / (n1 * n2);
return (Math.acos(Math.max(-1, Math.min(1, cos))) * 180) / Math.PI;
}
function map01(v, lo, hi) {
if (hi === lo) return 0.5;
const t = (v - lo) / (hi - lo);
return Math.max(0, Math.min(1, t));
}
function computeFingerOpenScores(sm) {
// Mediapipe 인덱스 체인: [MCP, PIP, TIP] 사용(엄지는 [2,3,4])
const defs = [
{ m: 2, p: 3, t: 4 }, //Thumb
{ m: 5, p: 6, t: 8 }, // Index
{ m: 9, p: 10, t: 12 }, // Middle
{ m: 13, p: 14, t: 16 }, // Ring
{ m: 17, p: 18, t: 20 }, // Pinky
];
const center = palmCenter(sm);
const s = handScale(sm) || 1e-6;
if (openCount === 0) {
// 모두 굽힘
nextLabel = 'FIST';
} else if (openCount >= 4) {
// 대부분 펴짐
nextLabel = 'OPEN';
}
if (nextLabel !== S.label) {
S.label = nextLabel;
if (S.label === 'FIST') {
// 메인 창으로 정보 전송
try {
if (window.opener && !window.opener.closed) {
window.opener.postMessage({ type: 'fistOpen', state: 'FIST' }, '*');
}
} catch (e) {
console.warn('postMessage failed', e);
}
console.log('%cPause', 'color:#FF0000;font-weight:bold;');
} else if (S.label === 'OPEN') {
S.lastOpenAt = performance.now();
try {
if (window.opener && !window.opener.closed) {
window.opener.postMessage({ type: 'fistOpen', state: 'OPEN' }, '*');
}
} catch (e) {
console.warn('postMessage failed', e);
}
console.log('%cPlay', 'color:#FF0000;font-weight:bold;');
}
}
return S.label;