@map-gesture-controls/core 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
- var E = Object.defineProperty;
2
- var M = (o, t, e) => t in o ? E(o, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : o[t] = e;
3
- var l = (o, t, e) => M(o, typeof t != "symbol" ? t + "" : t, e);
4
- import { FilesetResolver as C } from "@mediapipe/tasks-vision";
5
- const L = {
1
+ var O = Object.defineProperty;
2
+ var x = (i, t, e) => t in i ? O(i, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : i[t] = e;
3
+ var l = (i, t, e) => x(i, typeof t != "symbol" ? t + "" : t, e);
4
+ import { FilesetResolver as H } from "@mediapipe/tasks-vision";
5
+ const Y = {
6
6
  enabled: !0,
7
7
  mode: "corner",
8
8
  opacity: 0.85,
9
9
  position: "bottom-right",
10
10
  width: 320,
11
11
  height: 240
12
- }, H = {
12
+ }, Z = {
13
13
  actionDwellMs: 80,
14
14
  releaseGraceMs: 150,
15
15
  panDeadzonePx: 10,
@@ -29,17 +29,17 @@ const L = {
29
29
  RING_MCP: 13,
30
30
  PINKY_TIP: 20,
31
31
  PINKY_MCP: 17
32
- }, P = [
32
+ }, L = 0.25, z = 0.35, b = [
33
33
  r.INDEX_TIP,
34
34
  r.MIDDLE_TIP,
35
35
  r.RING_TIP,
36
36
  r.PINKY_TIP
37
- ], b = [
37
+ ], N = [
38
38
  r.INDEX_MCP,
39
39
  r.MIDDLE_MCP,
40
40
  r.RING_MCP,
41
41
  r.PINKY_MCP
42
- ], y = {
42
+ ], w = {
43
43
  idle: "#888888",
44
44
  panning: "#00ccff",
45
45
  zooming: "#00ffcc",
@@ -47,49 +47,67 @@ const L = {
47
47
  landmark: "rgba(255,255,255,0.6)",
48
48
  connection: "rgba(255,255,255,0.3)",
49
49
  fingertipGlow: "#4488ff"
50
- }, _ = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm";
51
- function T(o, t) {
52
- const e = o.x - t.x, i = o.y - t.y;
53
- return Math.sqrt(e * e + i * i);
50
+ }, B = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm";
51
+ function T(i, t) {
52
+ const e = i.x - t.x, n = i.y - t.y;
53
+ return Math.sqrt(e * e + n * n);
54
54
  }
55
- function N(o) {
56
- const t = o[r.WRIST];
57
- for (let n = 0; n < P.length; n++) {
58
- const d = o[P[n]], c = o[b[n]];
59
- if (T(d, t) < T(c, t) * 0.9)
55
+ function _(i) {
56
+ const t = i[r.WRIST];
57
+ for (let s = 0; s < b.length; s++) {
58
+ const u = i[b[s]], c = i[N[s]];
59
+ if (T(u, t) < T(c, t) * 0.9)
60
60
  return !1;
61
61
  }
62
- const e = x(o);
62
+ const e = D(i);
63
63
  if (e === 0) return !1;
64
- const i = P.map((n) => o[n]);
65
- let s = 1 / 0;
66
- for (let n = 0; n < i.length - 1; n++) {
67
- const d = T(i[n], i[n + 1]);
68
- d < s && (s = d);
64
+ const n = b.map((s) => i[s]);
65
+ let o = 1 / 0;
66
+ for (let s = 0; s < n.length - 1; s++) {
67
+ const u = T(n[s], n[s + 1]);
68
+ u < o && (o = u);
69
69
  }
70
- return s >= e * 0.18;
70
+ return o >= e * 0.18;
71
71
  }
72
- function S(o) {
73
- const t = o[r.WRIST];
72
+ function R(i) {
73
+ const t = i[r.WRIST];
74
74
  let e = 0;
75
- for (let i = 0; i < P.length; i++) {
76
- const s = o[P[i]], a = o[b[i]];
77
- T(s, t) < T(a, t) * 1.1 && e++;
75
+ for (let n = 0; n < b.length; n++) {
76
+ const o = i[b[n]], a = i[N[n]];
77
+ T(o, t) < T(a, t) * 1.1 && e++;
78
78
  }
79
79
  return e >= 3;
80
80
  }
81
- function x(o) {
82
- const t = o[r.WRIST], e = o[r.MIDDLE_MCP];
81
+ function D(i) {
82
+ const t = i[r.WRIST], e = i[r.MIDDLE_MCP];
83
83
  return !t || !e ? 0 : T(t, e);
84
84
  }
85
- function k(o, t) {
86
- const e = o[r.INDEX_TIP], i = t[r.INDEX_TIP];
87
- return !e || !i ? 0 : T(e, i);
85
+ function k(i, t) {
86
+ const e = i[r.INDEX_TIP], n = t[r.INDEX_TIP];
87
+ return !e || !n ? 0 : T(e, n);
88
88
  }
89
- function R(o) {
90
- return o.length < 21 ? "none" : S(o) ? "fist" : N(o) ? "openPalm" : "none";
89
+ function F(i) {
90
+ const t = D(i);
91
+ if (t === 0) return !1;
92
+ const e = i[r.THUMB_TIP], n = i[r.INDEX_TIP];
93
+ return T(e, n) < t * L;
91
94
  }
92
- class B {
95
+ function G(i) {
96
+ const t = D(i);
97
+ if (t === 0) return !1;
98
+ const e = i[r.THUMB_TIP], n = i[r.INDEX_TIP];
99
+ return T(e, n) < t * z;
100
+ }
101
+ function j(i) {
102
+ return i.length < 21 ? "none" : R(i) ? "fist" : F(i) ? "pinch" : _(i) ? "openPalm" : "none";
103
+ }
104
+ function M() {
105
+ let i = !1;
106
+ return function(e) {
107
+ return e.length < 21 ? (i = !1, "none") : R(e) ? (i = !1, "fist") : (i ? i = G(e) : i = F(e), i ? "pinch" : _(e) ? "openPalm" : "none");
108
+ };
109
+ }
110
+ class $ {
93
111
  constructor(t, e) {
94
112
  l(this, "landmarker", null);
95
113
  l(this, "videoEl", null);
@@ -99,6 +117,9 @@ class B {
99
117
  l(this, "onFrame");
100
118
  l(this, "tuning");
101
119
  l(this, "lastVideoTime", -1);
120
+ // One stateful classifier per hand label; persists pinch hysteresis across frames.
121
+ l(this, "leftClassifier", M());
122
+ l(this, "rightClassifier", M());
102
123
  this.tuning = t, this.onFrame = e;
103
124
  }
104
125
  /**
@@ -106,7 +127,7 @@ class B {
106
127
  * Returns the video element so the overlay can render it.
107
128
  */
108
129
  async init() {
109
- const t = await C.forVisionTasks(_), { HandLandmarker: e } = await import("@mediapipe/tasks-vision");
130
+ const t = await H.forVisionTasks(B), { HandLandmarker: e } = await import("@mediapipe/tasks-vision");
110
131
  return this.landmarker = await e.createFromOptions(t, {
111
132
  baseOptions: {
112
133
  modelAssetPath: "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task",
@@ -119,8 +140,8 @@ class B {
119
140
  minTrackingConfidence: this.tuning.minTrackingConfidence
120
141
  }), this.videoEl = document.createElement("video"), this.videoEl.setAttribute("playsinline", ""), this.videoEl.setAttribute("autoplay", ""), this.videoEl.muted = !0, this.videoEl.width = 640, this.videoEl.height = 480, this.stream = await navigator.mediaDevices.getUserMedia({
121
142
  video: { width: 640, height: 480, facingMode: "user" }
122
- }), this.videoEl.srcObject = this.stream, await new Promise((i) => {
123
- this.videoEl.addEventListener("loadeddata", () => i(), { once: !0 });
143
+ }), this.videoEl.srcObject = this.stream, await new Promise((n) => {
144
+ this.videoEl.addEventListener("loadeddata", () => n(), { once: !0 });
124
145
  }), this.videoEl;
125
146
  }
126
147
  start() {
@@ -131,7 +152,7 @@ class B {
131
152
  }
132
153
  destroy() {
133
154
  var t, e;
134
- this.stop(), (t = this.stream) == null || t.getTracks().forEach((i) => i.stop()), (e = this.landmarker) == null || e.close(), this.landmarker = null, this.videoEl = null, this.stream = null;
155
+ this.stop(), (t = this.stream) == null || t.getTracks().forEach((n) => n.stop()), (e = this.landmarker) == null || e.close(), this.landmarker = null, this.videoEl = null, this.stream = null;
135
156
  }
136
157
  loop() {
137
158
  this.running && (this.rafHandle = requestAnimationFrame(() => this.loop()), this.processFrame());
@@ -139,31 +160,31 @@ class B {
139
160
  processFrame() {
140
161
  const t = this.videoEl, e = this.landmarker;
141
162
  if (!t || !e || t.readyState < 2) return;
142
- const i = performance.now();
163
+ const n = performance.now();
143
164
  if (t.currentTime === this.lastVideoTime) return;
144
165
  this.lastVideoTime = t.currentTime;
145
- let s;
166
+ let o;
146
167
  try {
147
- s = e.detectForVideo(t, i);
168
+ o = e.detectForVideo(t, n);
148
169
  } catch {
149
170
  return;
150
171
  }
151
- const a = this.buildFrame(s, i);
172
+ const a = this.buildFrame(o, n);
152
173
  this.onFrame(a);
153
174
  }
154
175
  buildFrame(t, e) {
155
- const i = t.landmarks.map((n, d) => {
176
+ const n = t.landmarks.map((s, u) => {
156
177
  var p, g;
157
- const c = t.handedness[d], h = ((p = c == null ? void 0 : c[0]) == null ? void 0 : p.categoryName) === "Left" ? "Left" : "Right", m = ((g = c == null ? void 0 : c[0]) == null ? void 0 : g.score) ?? 0, f = R(n);
158
- return { handedness: h, score: m, landmarks: n, gesture: f };
159
- }), s = i.find((n) => n.handedness === "Left") ?? null, a = i.find((n) => n.handedness === "Right") ?? null;
160
- return { timestamp: e, hands: i, leftHand: s, rightHand: a };
178
+ const c = t.handedness[u], d = ((p = c == null ? void 0 : c[0]) == null ? void 0 : p.categoryName) === "Left" ? "Left" : "Right", P = ((g = c == null ? void 0 : c[0]) == null ? void 0 : g.score) ?? 0, f = (d === "Left" ? this.leftClassifier : this.rightClassifier)(s);
179
+ return { handedness: d, score: P, landmarks: s, gesture: f };
180
+ }), o = n.find((s) => s.handedness === "Left") ?? null, a = n.find((s) => s.handedness === "Right") ?? null;
181
+ return { timestamp: e, hands: n, leftHand: o, rightHand: a };
161
182
  }
162
183
  getVideoElement() {
163
184
  return this.videoEl;
164
185
  }
165
186
  }
166
- class A {
187
+ class U {
167
188
  constructor(t) {
168
189
  l(this, "value", null);
169
190
  this.alpha = t;
@@ -178,7 +199,7 @@ class A {
178
199
  this.value = null;
179
200
  }
180
201
  }
181
- class D {
202
+ class C {
182
203
  constructor(t) {
183
204
  l(this, "value", null);
184
205
  this.alpha = t;
@@ -190,7 +211,7 @@ class D {
190
211
  this.value = null;
191
212
  }
192
213
  }
193
- class U {
214
+ const E = class E {
194
215
  constructor(t) {
195
216
  l(this, "mode", "idle");
196
217
  l(this, "actionDwell", null);
@@ -201,69 +222,83 @@ class U {
201
222
  l(this, "prevZoomDist", null);
202
223
  l(this, "rotateSmoother");
203
224
  l(this, "prevRotateAngle", null);
204
- this.tuning = t, this.panSmoother = new A(t.smoothingAlpha), this.zoomSmoother = new D(t.smoothingAlpha), this.rotateSmoother = new D(t.smoothingAlpha);
225
+ // Tracks how many consecutive frames each hand has been active.
226
+ // Used to require the secondary hand to be stable before escalating
227
+ // the mode (e.g. pan → rotate), preventing a single noisy frame from
228
+ // interrupting an ongoing single-hand gesture.
229
+ l(this, "leftActiveFrames", 0);
230
+ l(this, "rightActiveFrames", 0);
231
+ this.tuning = t, this.panSmoother = new U(t.smoothingAlpha), this.zoomSmoother = new C(t.smoothingAlpha), this.rotateSmoother = new C(t.smoothingAlpha);
205
232
  }
206
233
  getMode() {
207
234
  return this.mode;
208
235
  }
209
236
  update(t) {
210
- const e = t.timestamp, { actionDwellMs: i, releaseGraceMs: s } = this.tuning, { leftHand: a, rightHand: n } = t, d = n !== null && n.gesture === "fist", c = a !== null && a.gesture === "fist", h = d && c ? "rotating" : d ? "zooming" : c ? "panning" : "idle";
237
+ const e = t.timestamp, { actionDwellMs: n, releaseGraceMs: o } = this.tuning, { leftHand: a, rightHand: s } = t, u = (f) => f !== null && (f.gesture === "fist" || f.gesture === "pinch"), c = u(s), h = u(a);
238
+ this.leftActiveFrames = h ? this.leftActiveFrames + 1 : 0, this.rightActiveFrames = c ? this.rightActiveFrames + 1 : 0;
239
+ const d = this.leftActiveFrames >= E.ESCALATION_FRAMES && this.rightActiveFrames >= E.ESCALATION_FRAMES, m = d ? "rotating" : h && c && !d && (this.mode === "panning" || this.mode === "zooming") ? this.mode : c ? "zooming" : h ? "panning" : "idle";
211
240
  if (this.mode === "idle")
212
- return h !== "idle" ? this.actionDwell === null || this.actionDwell.gesture !== h ? this.actionDwell = { gesture: h, startMs: e } : e - this.actionDwell.startMs >= i && this.transitionTo(h) : this.actionDwell = null, this.buildOutput(null, null, null);
241
+ return m !== "idle" ? this.actionDwell === null || this.actionDwell.gesture !== m ? this.actionDwell = { gesture: m, startMs: e } : e - this.actionDwell.startMs >= n && this.transitionTo(m) : this.actionDwell = null, this.buildOutput(null, null, null);
213
242
  if (this.mode === "panning") {
214
- if (h !== "panning")
215
- return this.releaseTimer === null ? this.releaseTimer = e : e - this.releaseTimer >= s && this.transitionTo("idle"), this.buildOutput(null, null, null);
243
+ if (d)
244
+ return this.transitionTo("rotating"), this.buildOutput(null, null, null);
245
+ if (m !== "panning")
246
+ return this.releaseTimer === null ? this.releaseTimer = e : e - this.releaseTimer >= o && this.transitionTo("idle"), this.buildOutput(null, null, null);
216
247
  this.releaseTimer = null;
217
- const m = (a == null ? void 0 : a.gesture) === "fist" ? a : null;
218
- if (!m)
248
+ const f = u(a) ? a : null;
249
+ if (!f)
219
250
  return this.transitionTo("idle"), this.buildOutput(null, null, null);
220
- const f = m.landmarks[0], p = this.panSmoother.update(f.x, f.y);
221
- let g = null;
251
+ const p = f.landmarks[0], g = this.panSmoother.update(p.x, p.y);
252
+ let I = null;
222
253
  if (this.prevPanPos !== null) {
223
- const I = p.x - this.prevPanPos.x, v = p.y - this.prevPanPos.y, w = this.tuning.panDeadzonePx / 640;
224
- (Math.abs(I) > w || Math.abs(v) > w) && (g = { x: I, y: v });
254
+ const y = g.x - this.prevPanPos.x, v = g.y - this.prevPanPos.y, A = this.tuning.panDeadzonePx / 640;
255
+ (Math.abs(y) > A || Math.abs(v) > A) && (I = { x: y, y: v });
225
256
  }
226
- return this.prevPanPos = p, this.buildOutput(g, null, null);
257
+ return this.prevPanPos = g, this.buildOutput(I, null, null);
227
258
  }
228
259
  if (this.mode === "zooming") {
229
- if (h !== "zooming")
230
- return this.releaseTimer === null ? this.releaseTimer = e : e - this.releaseTimer >= s && this.transitionTo("idle"), this.buildOutput(null, null, null);
231
- if (this.releaseTimer = null, !n)
260
+ if (d)
261
+ return this.transitionTo("rotating"), this.buildOutput(null, null, null);
262
+ if (m !== "zooming")
263
+ return this.releaseTimer === null ? this.releaseTimer = e : e - this.releaseTimer >= o && this.transitionTo("idle"), this.buildOutput(null, null, null);
264
+ if (this.releaseTimer = null, !s)
232
265
  return this.transitionTo("idle"), this.buildOutput(null, null, null);
233
- const m = n.landmarks[0], f = this.zoomSmoother.update(m.y);
234
- let p = null;
266
+ const f = s.landmarks[0], p = this.zoomSmoother.update(f.y);
267
+ let g = null;
235
268
  if (this.prevZoomDist !== null) {
236
- const g = f - this.prevZoomDist;
237
- Math.abs(g) > this.tuning.zoomDeadzoneRatio && (p = -g);
269
+ const I = p - this.prevZoomDist;
270
+ Math.abs(I) > this.tuning.zoomDeadzoneRatio && (g = -I);
238
271
  }
239
- return this.prevZoomDist = f, this.buildOutput(null, p, null);
272
+ return this.prevZoomDist = p, this.buildOutput(null, g, null);
240
273
  }
241
274
  if (this.mode === "rotating") {
242
- if (h !== "rotating")
243
- return this.releaseTimer === null ? this.releaseTimer = e : e - this.releaseTimer >= s && this.transitionTo("idle"), this.buildOutput(null, null, null);
244
- if (this.releaseTimer = null, !a || !n)
275
+ if (m !== "rotating")
276
+ return this.releaseTimer === null ? this.releaseTimer = e : e - this.releaseTimer >= o && this.transitionTo("idle"), this.buildOutput(null, null, null);
277
+ if (this.releaseTimer = null, !a || !s)
245
278
  return this.transitionTo("idle"), this.buildOutput(null, null, null);
246
- const m = a.landmarks[0], f = n.landmarks[0], p = Math.atan2(f.y - m.y, f.x - m.x), g = this.rotateSmoother.update(p);
247
- let I = null;
279
+ const f = a.landmarks[0], p = s.landmarks[0], g = Math.atan2(p.y - f.y, p.x - f.x), I = this.rotateSmoother.update(g);
280
+ let y = null;
248
281
  if (this.prevRotateAngle !== null) {
249
- let v = g - this.prevRotateAngle;
250
- v > Math.PI && (v -= 2 * Math.PI), v < -Math.PI && (v += 2 * Math.PI), Math.abs(v) > 5e-3 && (I = v);
282
+ let v = I - this.prevRotateAngle;
283
+ v > Math.PI && (v -= 2 * Math.PI), v < -Math.PI && (v += 2 * Math.PI), Math.abs(v) > 5e-3 && (y = v);
251
284
  }
252
- return this.prevRotateAngle = g, this.buildOutput(null, null, I);
285
+ return this.prevRotateAngle = I, this.buildOutput(null, null, y);
253
286
  }
254
287
  return this.buildOutput(null, null, null);
255
288
  }
256
289
  transitionTo(t) {
257
- this.mode = t, this.releaseTimer = null, this.actionDwell = null, t !== "panning" && (this.panSmoother.reset(), this.prevPanPos = null), t !== "zooming" && (this.zoomSmoother.reset(), this.prevZoomDist = null), t !== "rotating" && (this.rotateSmoother.reset(), this.prevRotateAngle = null);
290
+ this.mode = t, this.releaseTimer = null, this.actionDwell = null, t !== "panning" && t !== "rotating" && (this.leftActiveFrames = 0), t !== "zooming" && t !== "rotating" && (this.rightActiveFrames = 0), t !== "panning" && (this.panSmoother.reset(), this.prevPanPos = null), t !== "zooming" && (this.zoomSmoother.reset(), this.prevZoomDist = null), t !== "rotating" && (this.rotateSmoother.reset(), this.prevRotateAngle = null);
258
291
  }
259
- buildOutput(t, e, i) {
260
- return { mode: this.mode, panDelta: t, zoomDelta: e, rotateDelta: i };
292
+ buildOutput(t, e, n) {
293
+ return { mode: this.mode, panDelta: t, zoomDelta: e, rotateDelta: n };
261
294
  }
262
295
  reset() {
263
296
  this.transitionTo("idle");
264
297
  }
265
- }
266
- const F = [
298
+ };
299
+ l(E, "ESCALATION_FRAMES", 3);
300
+ let S = E;
301
+ const X = [
267
302
  [0, 1],
268
303
  [1, 2],
269
304
  [2, 3],
@@ -293,21 +328,24 @@ const F = [
293
328
  [9, 13],
294
329
  [13, 17]
295
330
  // palm cross
296
- ], O = [
331
+ ], V = [
297
332
  r.THUMB_TIP,
298
333
  r.INDEX_TIP,
299
334
  r.MIDDLE_TIP,
300
335
  r.RING_TIP,
301
336
  r.PINKY_TIP
302
337
  ];
303
- class V {
338
+ class q {
304
339
  constructor(t) {
305
340
  l(this, "container");
306
341
  l(this, "canvas");
307
342
  l(this, "ctx");
308
343
  l(this, "badge");
344
+ l(this, "resetBar");
345
+ l(this, "resetFill");
309
346
  l(this, "config");
310
- this.config = t, this.container = document.createElement("div"), this.container.className = "ol-gesture-overlay", this.applyContainerStyles(), this.canvas = document.createElement("canvas"), this.canvas.className = "ol-gesture-canvas", this.canvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;", this.badge = document.createElement("div"), this.badge.className = "ol-gesture-badge ol-gesture-badge--idle", this.badge.textContent = "Idle", this.container.appendChild(this.canvas), this.container.appendChild(this.badge);
347
+ l(this, "lastMode", null);
348
+ this.config = t, this.container = document.createElement("div"), this.container.className = "ol-gesture-overlay", this.applyContainerStyles(), this.canvas = document.createElement("canvas"), this.canvas.className = "ol-gesture-canvas", this.canvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;", this.badge = document.createElement("div"), this.badge.className = "ol-gesture-badge ol-gesture-badge--idle", this.badge.textContent = "Idle", this.resetBar = document.createElement("div"), this.resetBar.className = "ol-gesture-reset", this.resetBar.innerHTML = '<span class="ol-gesture-reset-label">Reset</span><div class="ol-gesture-reset-track"><div class="ol-gesture-reset-fill"></div></div>', this.resetBar.style.opacity = "0", this.resetFill = this.resetBar.querySelector(".ol-gesture-reset-fill"), this.container.appendChild(this.canvas), this.container.appendChild(this.badge), this.container.appendChild(this.resetBar);
311
349
  const e = this.canvas.getContext("2d");
312
350
  if (!e) throw new Error("Cannot get 2D canvas context");
313
351
  this.ctx = e;
@@ -324,25 +362,30 @@ class V {
324
362
  var t;
325
363
  (t = this.container.parentElement) == null || t.removeChild(this.container);
326
364
  }
327
- /** Called each frame with the latest gesture frame and mode. */
328
- render(t, e) {
329
- this.updateBadge(e);
330
- const i = this.config.width, s = this.config.height;
331
- if (this.canvas.width = i, this.canvas.height = s, this.ctx.clearRect(0, 0, i, s), t !== null)
332
- for (const a of t.hands)
333
- this.drawSkeleton(a.landmarks, e, a.gesture === "fist");
334
- }
335
- drawSkeleton(t, e, i) {
336
- const { ctx: s } = this, a = this.config.width, n = this.config.height, d = (u) => (1 - u.x) * a, c = (u) => u.y * n;
337
- s.strokeStyle = y.connection, s.lineWidth = 1.5;
338
- for (const [u, h] of F)
339
- !t[u] || !t[h] || (s.beginPath(), s.moveTo(d(t[u]), c(t[u])), s.lineTo(d(t[h]), c(t[h])), s.stroke());
340
- for (let u = 0; u < t.length; u++) {
341
- const h = t[u], m = O.includes(u), f = e !== "idle" && m ? y.fingertipGlow : y.landmark;
342
- s.beginPath(), s.arc(d(h), c(h), m ? 5 : 3, 0, Math.PI * 2), s.fillStyle = f, s.fill(), e !== "idle" && m && (s.shadowBlur = i ? 12 : 6, s.shadowColor = y.fingertipGlow, s.fill(), s.shadowBlur = 0);
365
+ /** Called each frame with the latest gesture frame, mode, and optional reset progress (0 to 1). */
366
+ render(t, e, n = 0) {
367
+ this.updateBadge(e), this.updateResetBar(n);
368
+ const o = this.config.width, a = this.config.height;
369
+ if ((this.canvas.width !== o || this.canvas.height !== a) && (this.canvas.width = o, this.canvas.height = a), this.ctx.clearRect(0, 0, o, a), t !== null)
370
+ for (const s of t.hands)
371
+ this.drawSkeleton(s.landmarks, e, s.gesture === "fist");
372
+ }
373
+ updateResetBar(t) {
374
+ this.resetFill.style.width = `${t * 100}%`, this.resetBar.style.opacity = t > 0 ? "1" : "0";
375
+ }
376
+ drawSkeleton(t, e, n) {
377
+ const { ctx: o } = this, a = this.config.width, s = this.config.height, u = (h) => (1 - h.x) * a, c = (h) => h.y * s;
378
+ o.strokeStyle = w.connection, o.lineWidth = 1.5;
379
+ for (const [h, d] of X)
380
+ !t[h] || !t[d] || (o.beginPath(), o.moveTo(u(t[h]), c(t[h])), o.lineTo(u(t[d]), c(t[d])), o.stroke());
381
+ for (let h = 0; h < t.length; h++) {
382
+ const d = t[h], P = V.includes(h), m = e !== "idle" && P ? w.fingertipGlow : w.landmark;
383
+ o.beginPath(), o.arc(u(d), c(d), P ? 5 : 3, 0, Math.PI * 2), o.fillStyle = m, o.fill(), e !== "idle" && P && (o.shadowBlur = n ? 12 : 6, o.shadowColor = w.fingertipGlow, o.fill(), o.shadowBlur = 0);
343
384
  }
344
385
  }
345
386
  updateBadge(t) {
387
+ if (t === this.lastMode) return;
388
+ this.lastMode = t;
346
389
  const e = {
347
390
  idle: "Idle",
348
391
  panning: "Pan",
@@ -352,11 +395,11 @@ class V {
352
395
  this.badge.textContent = e[t], this.badge.className = `ol-gesture-badge ol-gesture-badge--${t}`;
353
396
  }
354
397
  applyContainerStyles() {
355
- const { mode: t, position: e, width: i, height: s, opacity: a } = this.config;
398
+ const { mode: t, position: e, width: n, height: o, opacity: a } = this.config;
356
399
  if (this.container.style.cssText = "", this.container.style.position = "fixed", this.container.style.zIndex = "9999", this.container.style.overflow = "hidden", this.container.style.borderRadius = "8px", this.container.style.opacity = String(a), this.container.style.display = t === "hidden" ? "none" : "block", t === "corner") {
357
- this.container.style.width = `${i}px`, this.container.style.height = `${s}px`;
358
- const n = "16px";
359
- e === "bottom-right" ? (this.container.style.bottom = n, this.container.style.right = n) : e === "bottom-left" ? (this.container.style.bottom = n, this.container.style.left = n) : e === "top-right" ? (this.container.style.top = n, this.container.style.right = n) : (this.container.style.top = n, this.container.style.left = n);
400
+ this.container.style.width = `${n}px`, this.container.style.height = `${o}px`;
401
+ const s = "16px";
402
+ e === "bottom-right" ? (this.container.style.bottom = s, this.container.style.right = s) : e === "bottom-left" ? (this.container.style.bottom = s, this.container.style.left = s) : e === "top-right" ? (this.container.style.top = s, this.container.style.right = s) : (this.container.style.top = s, this.container.style.left = s);
360
403
  } else t === "full" && (this.container.style.top = "0", this.container.style.left = "0", this.container.style.width = "100vw", this.container.style.height = "100vh", this.container.style.borderRadius = "0");
361
404
  }
362
405
  updateConfig(t) {
@@ -364,14 +407,15 @@ class V {
364
407
  }
365
408
  }
366
409
  export {
367
- y as COLORS,
368
- H as DEFAULT_TUNING_CONFIG,
369
- L as DEFAULT_WEBCAM_CONFIG,
370
- B as GestureController,
371
- U as GestureStateMachine,
410
+ w as COLORS,
411
+ Z as DEFAULT_TUNING_CONFIG,
412
+ Y as DEFAULT_WEBCAM_CONFIG,
413
+ $ as GestureController,
414
+ S as GestureStateMachine,
372
415
  r as LANDMARKS,
373
- V as WebcamOverlay,
374
- R as classifyGesture,
375
- x as getHandSize,
416
+ q as WebcamOverlay,
417
+ j as classifyGesture,
418
+ M as createHandClassifier,
419
+ D as getHandSize,
376
420
  k as getTwoHandDistance
377
421
  };
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type GestureMode = 'idle' | 'panning' | 'zooming' | 'rotating';
2
2
  export type HandednessLabel = 'Left' | 'Right';
3
- export type GestureType = 'openPalm' | 'fist' | 'none';
3
+ export type GestureType = 'openPalm' | 'fist' | 'pinch' | 'none';
4
4
  export interface Point2D {
5
5
  x: number;
6
6
  y: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@map-gesture-controls/core",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Map-agnostic hand gesture detection and state machine (MediaPipe)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1 +0,0 @@
1
- export {};