-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgestures.js
More file actions
88 lines (76 loc) · 3.13 KB
/
Copy pathgestures.js
File metadata and controls
88 lines (76 loc) · 3.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// Pure gesture logic — no DOM, no MediaPipe. Imported by camera.js,
// testable in Node (test_gestures.mjs).
export const POINT_HOLD_MS = 400; // next (index pointing up)
export const POINT_REPEAT_MS = 1000; // keep pointing → next again
export const PEACE_HOLD_MS = 500; // prev
export const THUMB_HOLD_MS = 1500; // like
export const PALM_HOLD_MS = 700; // pause
export const THREE_HOLD_MS = 700; // mute
const SPREAD_MIN = 0.03; // min x-gap between adjacent extended fingertips
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
// Landmarks: wrist 0, thumb 4/3/2, index 8/6/5, middle 12/10/9,
// ring 16/14/13, pinky 20/18/17.
export function classify(lm) {
// Extended = tip above PIP (y is down in normalized coords).
const ext = (tip, pip) => lm[tip].y < lm[pip].y;
const index = ext(8, 6);
const middle = ext(12, 10);
const ring = ext(16, 14);
const pinky = ext(20, 18);
// Curled = tip folded back toward the wrist (closer than its PIP).
// Orientation-independent — works on a sideways thumbs-up fist where
// tip-vs-PIP y comparison fails.
const curled = (tip, pip) => dist(lm[tip], lm[0]) < dist(lm[pip], lm[0]);
// Thumbs up: thumb column pointing up, above all fingertips, fist curled.
const thumbUp = lm[4].y < lm[3].y && lm[3].y < lm[2].y;
const thumbAboveFingers = [8, 12, 16, 20].every((t) => lm[4].y < lm[t].y);
const fistCurled =
curled(8, 6) && curled(12, 10) && curled(16, 14) && curled(20, 18);
if (thumbUp && thumbAboveFingers && fistCurled) return "thumbsUp";
if (index && middle && !ring && !pinky) return "peace";
// Three fingers (index + middle + ring, pinky down) → mute.
if (index && middle && ring && !pinky) return "three";
if (index && middle && ring && pinky) {
// Palm requires adjacent extended fingers visibly spread.
const spread =
Math.abs(lm[8].x - lm[12].x) > SPREAD_MIN &&
Math.abs(lm[12].x - lm[16].x) > SPREAD_MIN &&
Math.abs(lm[16].x - lm[20].x) > SPREAD_MIN;
if (spread) return "palm";
}
if (index && !middle && !ring && !pinky) return "point";
return "none";
}
// Stateful engine: hold timers. Each gesture has its own start time and
// fires once per hold — except "point", which repeats while held so you
// can keep scrolling the feed.
export function createEngine(onGesture) {
const holds = {
point: { ms: POINT_HOLD_MS, action: "next", repeatMs: POINT_REPEAT_MS },
peace: { ms: PEACE_HOLD_MS, action: "prev" },
thumbsUp: { ms: THUMB_HOLD_MS, action: "like" },
palm: { ms: PALM_HOLD_MS, action: "pause" },
three: { ms: THREE_HOLD_MS, action: "mute" },
};
for (const h of Object.values(holds)) {
h.start = null;
h.nextFireAt = null;
}
return function update(pose, now) {
for (const [name, h] of Object.entries(holds)) {
if (pose === name) {
if (h.start === null) {
h.start = now;
h.nextFireAt = now + h.ms;
}
if (h.nextFireAt !== null && now >= h.nextFireAt) {
onGesture(h.action);
h.nextFireAt = h.repeatMs ? now + h.repeatMs : null;
}
} else {
h.start = null;
h.nextFireAt = null;
}
}
};
}