@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/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  ## What it does
17
17
 
18
18
  - Detects hands and classifies gestures at 30+ fps using MediaPipe Hand Landmarker
19
- - Recognizes **fist** (left = pan, right = zoom, both = rotate) and **idle** states
19
+ - Recognizes **fist** and **pinch** as interchangeable triggers (left = pan, right = zoom, both = rotate)
20
20
  - Manages gesture transitions with dwell timers and grace periods to avoid flickering
21
21
  - Provides a configurable webcam overlay with corner/full/hidden display modes
22
22
  - Ships fully typed TypeScript declarations
@@ -59,7 +59,8 @@ controller.start();
59
59
  | `GestureController` | Class | Opens the webcam, runs MediaPipe detection, and emits gesture frames |
60
60
  | `GestureStateMachine` | Class | Manages gesture state transitions with dwell and grace timers |
61
61
  | `WebcamOverlay` | Class | Renders a configurable camera preview overlay |
62
- | `classifyGesture` | Function | Classifies a set of hand landmarks into `fist` or `none` |
62
+ | `classifyGesture` | Function | Stateless classifier: returns `fist`, `pinch`, `openPalm`, or `none` for a set of landmarks |
63
+ | `createHandClassifier` | Function | Returns a stateful per-hand classifier with pinch hysteresis (use this instead of `classifyGesture` in custom pipelines) |
63
64
  | `getHandSize` | Function | Computes the bounding size of a hand from its landmarks |
64
65
  | `getTwoHandDistance` | Function | Measures the distance between two detected hands |
65
66
  | `DEFAULT_WEBCAM_CONFIG` | Constant | Default webcam overlay settings |
@@ -69,13 +70,21 @@ Full TypeScript types are exported for `GestureMode`, `GestureFrame`, `DetectedH
69
70
 
70
71
  ## Gesture recognition
71
72
 
72
- | Gesture | Detection rule | Use case |
73
+ Both **fist** and **pinch** trigger the same map actions, users can use whichever is more comfortable.
74
+
75
+ | Gesture | Detection rule | Map action |
73
76
  | --- | --- | --- |
74
77
  | **Left fist** | Left hand, 3+ fingers curled | Pan / drag |
75
- | **Right fist** | Right hand, 3+ fingers curled | Zoom in/out (vertical movement) |
76
- | **Both fists** | Both hands, 3+ fingers curled each | Rotate map |
78
+ | **Left pinch** | Left hand, thumb and index tip within 25% of hand size (exits at 35%) | Pan / drag |
79
+ | **Right fist** | Right hand, 3+ fingers curled | Zoom (move up = in, down = out) |
80
+ | **Right pinch** | Right hand, thumb and index tip within 25% of hand size (exits at 35%) | Zoom (move up = in, down = out) |
81
+ | **Both hands active** | Both hands fist or pinch (mixed is fine) | Rotate map |
77
82
  | **Idle** | Anything else | No action |
78
83
 
84
+ > **Reset (OpenLayers only):** `@map-gesture-controls/ol` adds a reset gesture on top of the above. Bring both hands together in a prayer/namaste pose (wrists close, neither hand making a fist or pinch) and hold for 1 second. A progress bar fills in the webcam overlay; when it completes, pan, zoom, and rotation all snap back to their initial values.
85
+
86
+ Pinch detection uses hysteresis: the gesture is entered at 25% of hand size and held until fingers open beyond 35%. This prevents flickering when fingers hover near the threshold during a held pinch.
87
+
79
88
  Gestures are confirmed after a configurable dwell period (default 80 ms) and held through a grace period (default 150 ms) to prevent flickering when tracking briefly drops.
80
89
 
81
90
  ## Use cases
@@ -15,6 +15,8 @@ export declare class GestureController {
15
15
  private onFrame;
16
16
  private tuning;
17
17
  private lastVideoTime;
18
+ private leftClassifier;
19
+ private rightClassifier;
18
20
  constructor(tuning: TuningConfig, onFrame: FrameCallback);
19
21
  /**
20
22
  * Initialise MediaPipe and request webcam access.
@@ -1,5 +1,5 @@
1
1
  import { FilesetResolver } from '@mediapipe/tasks-vision';
2
- import { classifyGesture } from './gestureClassifier.js';
2
+ import { createHandClassifier } from './gestureClassifier.js';
3
3
  import { MEDIAPIPE_WASM_URL } from './constants.js';
4
4
  /**
5
5
  * GestureController
@@ -57,6 +57,19 @@ export class GestureController {
57
57
  writable: true,
58
58
  value: -1
59
59
  });
60
+ // One stateful classifier per hand label; persists pinch hysteresis across frames.
61
+ Object.defineProperty(this, "leftClassifier", {
62
+ enumerable: true,
63
+ configurable: true,
64
+ writable: true,
65
+ value: createHandClassifier()
66
+ });
67
+ Object.defineProperty(this, "rightClassifier", {
68
+ enumerable: true,
69
+ configurable: true,
70
+ writable: true,
71
+ value: createHandClassifier()
72
+ });
60
73
  this.tuning = tuning;
61
74
  this.onFrame = onFrame;
62
75
  }
@@ -146,7 +159,8 @@ export class GestureController {
146
159
  const rawLabel = handednessArr?.[0]?.categoryName;
147
160
  const label = rawLabel === 'Left' ? 'Left' : 'Right';
148
161
  const score = handednessArr?.[0]?.score ?? 0;
149
- const gesture = classifyGesture(landmarks);
162
+ const classify = label === 'Left' ? this.leftClassifier : this.rightClassifier;
163
+ const gesture = classify(landmarks);
150
164
  return { handedness: label, score, landmarks, gesture };
151
165
  });
152
166
  const leftHand = hands.find((h) => h.handedness === 'Left') ?? null;
@@ -33,6 +33,9 @@ export declare class GestureStateMachine {
33
33
  private prevZoomDist;
34
34
  private rotateSmoother;
35
35
  private prevRotateAngle;
36
+ private leftActiveFrames;
37
+ private rightActiveFrames;
38
+ private static readonly ESCALATION_FRAMES;
36
39
  constructor(tuning: TuningConfig);
37
40
  getMode(): GestureMode;
38
41
  update(frame: GestureFrame): StateMachineOutput;
@@ -142,6 +142,22 @@ export class GestureStateMachine {
142
142
  writable: true,
143
143
  value: null
144
144
  });
145
+ // Tracks how many consecutive frames each hand has been active.
146
+ // Used to require the secondary hand to be stable before escalating
147
+ // the mode (e.g. pan → rotate), preventing a single noisy frame from
148
+ // interrupting an ongoing single-hand gesture.
149
+ Object.defineProperty(this, "leftActiveFrames", {
150
+ enumerable: true,
151
+ configurable: true,
152
+ writable: true,
153
+ value: 0
154
+ });
155
+ Object.defineProperty(this, "rightActiveFrames", {
156
+ enumerable: true,
157
+ configurable: true,
158
+ writable: true,
159
+ value: 0
160
+ });
145
161
  this.panSmoother = new EMAPoint(tuning.smoothingAlpha);
146
162
  this.zoomSmoother = new EMAScalar(tuning.smoothingAlpha);
147
163
  this.rotateSmoother = new EMAScalar(tuning.smoothingAlpha);
@@ -154,17 +170,35 @@ export class GestureStateMachine {
154
170
  const { actionDwellMs, releaseGraceMs } = this.tuning;
155
171
  const { leftHand, rightHand } = frame;
156
172
  // ── Determine desired mode for this frame ─────────────────────────────────
157
- const rightFist = rightHand !== null && rightHand.gesture === 'fist';
158
- const leftFist = leftHand !== null && leftHand.gesture === 'fist';
159
- const bothFists = rightFist && leftFist;
160
- // Both fists → rotate; right only → zoom; left only → pan
161
- const desired = bothFists
173
+ // Both fist and pinch trigger the same modes, users can choose either.
174
+ const isActive = (hand) => hand !== null && (hand.gesture === 'fist' || hand.gesture === 'pinch');
175
+ const rightActive = isActive(rightHand);
176
+ const leftActive = isActive(leftHand);
177
+ // Track consecutive active frames per hand. Used to guard against a single
178
+ // noisy frame from the secondary hand escalating (or interrupting) an
179
+ // ongoing single-hand gesture. Counts reset to 0 the moment a hand drops.
180
+ this.leftActiveFrames = leftActive ? this.leftActiveFrames + 1 : 0;
181
+ this.rightActiveFrames = rightActive ? this.rightActiveFrames + 1 : 0;
182
+ // Escalation to 'rotating' requires both hands to have been active for at
183
+ // least ESCALATION_FRAMES consecutive frames. This prevents one brief noisy
184
+ // frame on the secondary hand from interrupting an ongoing pan or zoom.
185
+ const bothStable = this.leftActiveFrames >= GestureStateMachine.ESCALATION_FRAMES &&
186
+ this.rightActiveFrames >= GestureStateMachine.ESCALATION_FRAMES;
187
+ // Both stable → rotate; right only → zoom; left only → pan.
188
+ // When both hands are active but not yet stable, we preserve the current
189
+ // single-hand mode (panning/zooming) so the secondary hand must be held
190
+ // for ESCALATION_FRAMES before rotating kicks in. This prevents a single
191
+ // noisy frame from the secondary hand from interrupting an ongoing gesture.
192
+ const bothActiveButUnstable = leftActive && rightActive && !bothStable;
193
+ const desired = bothStable
162
194
  ? 'rotating'
163
- : rightFist
164
- ? 'zooming'
165
- : leftFist
166
- ? 'panning'
167
- : 'idle';
195
+ : bothActiveButUnstable && (this.mode === 'panning' || this.mode === 'zooming')
196
+ ? this.mode // hold current mode until secondary hand is confirmed stable
197
+ : rightActive
198
+ ? 'zooming'
199
+ : leftActive
200
+ ? 'panning'
201
+ : 'idle';
168
202
  // ── idle ──────────────────────────────────────────────────────────────────
169
203
  if (this.mode === 'idle') {
170
204
  if (desired !== 'idle') {
@@ -182,6 +216,11 @@ export class GestureStateMachine {
182
216
  }
183
217
  // ── panning ───────────────────────────────────────────────────────────────
184
218
  if (this.mode === 'panning') {
219
+ // Escalate to rotating once the right hand becomes stably active too.
220
+ if (bothStable) {
221
+ this.transitionTo('rotating');
222
+ return this.buildOutput(null, null, null);
223
+ }
185
224
  if (desired !== 'panning') {
186
225
  if (this.releaseTimer === null) {
187
226
  this.releaseTimer = now;
@@ -192,7 +231,7 @@ export class GestureStateMachine {
192
231
  return this.buildOutput(null, null, null);
193
232
  }
194
233
  this.releaseTimer = null;
195
- const fistHand = leftHand?.gesture === 'fist' ? leftHand : null;
234
+ const fistHand = isActive(leftHand) ? leftHand : null;
196
235
  if (!fistHand) {
197
236
  this.transitionTo('idle');
198
237
  return this.buildOutput(null, null, null);
@@ -213,6 +252,11 @@ export class GestureStateMachine {
213
252
  }
214
253
  // ── zooming ───────────────────────────────────────────────────────────────
215
254
  if (this.mode === 'zooming') {
255
+ // Escalate to rotating once the left hand becomes stably active too.
256
+ if (bothStable) {
257
+ this.transitionTo('rotating');
258
+ return this.buildOutput(null, null, null);
259
+ }
216
260
  if (desired !== 'zooming') {
217
261
  if (this.releaseTimer === null) {
218
262
  this.releaseTimer = now;
@@ -283,6 +327,13 @@ export class GestureStateMachine {
283
327
  this.mode = next;
284
328
  this.releaseTimer = null;
285
329
  this.actionDwell = null;
330
+ // Reset both counters so escalation requires a fresh stable run in the new mode.
331
+ // Exception: keep the dominant hand's counter when entering a single-hand mode so
332
+ // it does not need to re-accumulate from zero if the hand was already stable.
333
+ if (next !== 'panning' && next !== 'rotating')
334
+ this.leftActiveFrames = 0;
335
+ if (next !== 'zooming' && next !== 'rotating')
336
+ this.rightActiveFrames = 0;
286
337
  if (next !== 'panning') {
287
338
  this.panSmoother.reset();
288
339
  this.prevPanPos = null;
@@ -303,3 +354,9 @@ export class GestureStateMachine {
303
354
  this.transitionTo('idle');
304
355
  }
305
356
  }
357
+ Object.defineProperty(GestureStateMachine, "ESCALATION_FRAMES", {
358
+ enumerable: true,
359
+ configurable: true,
360
+ writable: true,
361
+ value: 3
362
+ });
@@ -14,15 +14,19 @@ export declare class WebcamOverlay {
14
14
  private canvas;
15
15
  private ctx;
16
16
  private badge;
17
+ private resetBar;
18
+ private resetFill;
17
19
  private config;
20
+ private lastMode;
18
21
  constructor(config: WebcamConfig);
19
22
  /** Attach video element produced by GestureController */
20
23
  attachVideo(video: HTMLVideoElement): void;
21
24
  /** Mount the overlay into the given parent (usually document.body or map container) */
22
25
  mount(parent: HTMLElement): void;
23
26
  unmount(): void;
24
- /** Called each frame with the latest gesture frame and mode. */
25
- render(frame: GestureFrame | null, mode: GestureMode): void;
27
+ /** Called each frame with the latest gesture frame, mode, and optional reset progress (0 to 1). */
28
+ render(frame: GestureFrame | null, mode: GestureMode, resetProgress?: number): void;
29
+ private updateResetBar;
26
30
  private drawSkeleton;
27
31
  private updateBadge;
28
32
  private applyContainerStyles;
@@ -51,12 +51,30 @@ export class WebcamOverlay {
51
51
  writable: true,
52
52
  value: void 0
53
53
  });
54
+ Object.defineProperty(this, "resetBar", {
55
+ enumerable: true,
56
+ configurable: true,
57
+ writable: true,
58
+ value: void 0
59
+ });
60
+ Object.defineProperty(this, "resetFill", {
61
+ enumerable: true,
62
+ configurable: true,
63
+ writable: true,
64
+ value: void 0
65
+ });
54
66
  Object.defineProperty(this, "config", {
55
67
  enumerable: true,
56
68
  configurable: true,
57
69
  writable: true,
58
70
  value: void 0
59
71
  });
72
+ Object.defineProperty(this, "lastMode", {
73
+ enumerable: true,
74
+ configurable: true,
75
+ writable: true,
76
+ value: null
77
+ });
60
78
  this.config = config;
61
79
  this.container = document.createElement('div');
62
80
  this.container.className = 'ol-gesture-overlay';
@@ -67,8 +85,16 @@ export class WebcamOverlay {
67
85
  this.badge = document.createElement('div');
68
86
  this.badge.className = 'ol-gesture-badge ol-gesture-badge--idle';
69
87
  this.badge.textContent = 'Idle';
88
+ this.resetBar = document.createElement('div');
89
+ this.resetBar.className = 'ol-gesture-reset';
90
+ this.resetBar.innerHTML =
91
+ '<span class="ol-gesture-reset-label">Reset</span>' +
92
+ '<div class="ol-gesture-reset-track"><div class="ol-gesture-reset-fill"></div></div>';
93
+ this.resetBar.style.opacity = '0';
94
+ this.resetFill = this.resetBar.querySelector('.ol-gesture-reset-fill');
70
95
  this.container.appendChild(this.canvas);
71
96
  this.container.appendChild(this.badge);
97
+ this.container.appendChild(this.resetBar);
72
98
  const ctx = this.canvas.getContext('2d');
73
99
  if (!ctx)
74
100
  throw new Error('Cannot get 2D canvas context');
@@ -88,13 +114,16 @@ export class WebcamOverlay {
88
114
  unmount() {
89
115
  this.container.parentElement?.removeChild(this.container);
90
116
  }
91
- /** Called each frame with the latest gesture frame and mode. */
92
- render(frame, mode) {
117
+ /** Called each frame with the latest gesture frame, mode, and optional reset progress (0 to 1). */
118
+ render(frame, mode, resetProgress = 0) {
93
119
  this.updateBadge(mode);
120
+ this.updateResetBar(resetProgress);
94
121
  const w = this.config.width;
95
122
  const h = this.config.height;
96
- this.canvas.width = w;
97
- this.canvas.height = h;
123
+ if (this.canvas.width !== w || this.canvas.height !== h) {
124
+ this.canvas.width = w;
125
+ this.canvas.height = h;
126
+ }
98
127
  this.ctx.clearRect(0, 0, w, h);
99
128
  if (frame === null)
100
129
  return;
@@ -102,6 +131,10 @@ export class WebcamOverlay {
102
131
  this.drawSkeleton(hand.landmarks, mode, hand.gesture === 'fist');
103
132
  }
104
133
  }
134
+ updateResetBar(resetProgress) {
135
+ this.resetFill.style.width = `${resetProgress * 100}%`;
136
+ this.resetBar.style.opacity = resetProgress > 0 ? '1' : '0';
137
+ }
105
138
  drawSkeleton(landmarks, mode, isActionHand) {
106
139
  const { ctx } = this;
107
140
  const w = this.config.width;
@@ -141,6 +174,9 @@ export class WebcamOverlay {
141
174
  }
142
175
  }
143
176
  updateBadge(mode) {
177
+ if (mode === this.lastMode)
178
+ return;
179
+ this.lastMode = mode;
144
180
  const labels = {
145
181
  idle: 'Idle',
146
182
  panning: 'Pan',
@@ -13,6 +13,8 @@ export declare const LANDMARKS: {
13
13
  readonly PINKY_TIP: 20;
14
14
  readonly PINKY_MCP: 17;
15
15
  };
16
+ export declare const PINCH_THRESHOLD = 0.25;
17
+ export declare const PINCH_RELEASE_THRESHOLD = 0.35;
16
18
  export declare const FINGERTIP_INDICES: readonly [8, 12, 16, 20];
17
19
  export declare const FINGER_BASE_INDICES: readonly [5, 9, 13, 17];
18
20
  export declare const COLORS: {
package/dist/constants.js CHANGED
@@ -29,6 +29,12 @@ export const LANDMARKS = {
29
29
  PINKY_TIP: 20,
30
30
  PINKY_MCP: 17,
31
31
  };
32
+ // Pinch detection thresholds as a fraction of hand size.
33
+ // Hysteresis: enter pinch at PINCH_THRESHOLD, exit (release) at PINCH_RELEASE_THRESHOLD.
34
+ // The wider release threshold prevents the classifier from flickering between
35
+ // 'pinch' and 'none' when the fingers hover at the edge of the enter threshold.
36
+ export const PINCH_THRESHOLD = 0.25;
37
+ export const PINCH_RELEASE_THRESHOLD = 0.35;
32
38
  // Fingertip indices for open-palm detection
33
39
  export const FINGERTIP_INDICES = [
34
40
  LANDMARKS.INDEX_TIP,
@@ -14,9 +14,24 @@ export declare function getTwoHandDistance(landmarksA: HandLandmark[], landmarks
14
14
  /**
15
15
  * Classify the gesture for a single hand from its landmarks.
16
16
  *
17
- * Priority: fist > openPalm > none
17
+ * Priority: fist > pinch > openPalm > none
18
18
  *
19
- * Fist: most fingers curled
20
- * Open palm: all fingers extended
19
+ * Fist: most fingers curled
20
+ * Pinch: thumb tip and index tip close together
21
+ * Open palm: all fingers extended and spread
22
+ *
23
+ * NOTE: This stateless version has no pinch hysteresis. Use `createHandClassifier`
24
+ * for a stateful per-hand classifier that prevents pinch flickering.
21
25
  */
22
26
  export declare function classifyGesture(landmarks: HandLandmark[]): GestureType;
27
+ /**
28
+ * Returns a stateful classify function for a single hand.
29
+ *
30
+ * Pinch uses hysteresis: the hand enters 'pinch' when tips are within
31
+ * PINCH_THRESHOLD of hand size, and stays in 'pinch' until tips separate
32
+ * beyond PINCH_RELEASE_THRESHOLD. This prevents rapid toggling when fingers
33
+ * hover at the entry threshold during a held pinch gesture.
34
+ *
35
+ * Use one instance per hand (left / right) and reset between hand sessions.
36
+ */
37
+ export declare function createHandClassifier(): (landmarks: HandLandmark[]) => GestureType;
@@ -1,4 +1,4 @@
1
- import { LANDMARKS, FINGERTIP_INDICES, FINGER_BASE_INDICES } from './constants.js';
1
+ import { LANDMARKS, FINGERTIP_INDICES, FINGER_BASE_INDICES, PINCH_THRESHOLD, PINCH_RELEASE_THRESHOLD } from './constants.js';
2
2
  /**
3
3
  * Euclidean distance between two landmarks (ignoring Z).
4
4
  */
@@ -89,13 +89,42 @@ export function getTwoHandDistance(landmarksA, landmarksB) {
89
89
  return 0;
90
90
  return dist(tipA, tipB);
91
91
  }
92
+ /**
93
+ * Stateless pinch check used only as the entry condition.
94
+ * Returns true when thumb tip and index tip are within PINCH_THRESHOLD of hand size.
95
+ */
96
+ function isPinchEnter(landmarks) {
97
+ const handSize = getHandSize(landmarks);
98
+ if (handSize === 0)
99
+ return false;
100
+ const thumbTip = landmarks[LANDMARKS.THUMB_TIP];
101
+ const indexTip = landmarks[LANDMARKS.INDEX_TIP];
102
+ return dist(thumbTip, indexTip) < handSize * PINCH_THRESHOLD;
103
+ }
104
+ /**
105
+ * Returns true when thumb tip and index tip are still within PINCH_RELEASE_THRESHOLD.
106
+ * Used as the exit condition to implement hysteresis: once pinching, we stay in
107
+ * 'pinch' until the fingers open wider than the release threshold.
108
+ */
109
+ function isPinchHeld(landmarks) {
110
+ const handSize = getHandSize(landmarks);
111
+ if (handSize === 0)
112
+ return false;
113
+ const thumbTip = landmarks[LANDMARKS.THUMB_TIP];
114
+ const indexTip = landmarks[LANDMARKS.INDEX_TIP];
115
+ return dist(thumbTip, indexTip) < handSize * PINCH_RELEASE_THRESHOLD;
116
+ }
92
117
  /**
93
118
  * Classify the gesture for a single hand from its landmarks.
94
119
  *
95
- * Priority: fist > openPalm > none
120
+ * Priority: fist > pinch > openPalm > none
121
+ *
122
+ * Fist: most fingers curled
123
+ * Pinch: thumb tip and index tip close together
124
+ * Open palm: all fingers extended and spread
96
125
  *
97
- * Fist: most fingers curled
98
- * Open palm: all fingers extended
126
+ * NOTE: This stateless version has no pinch hysteresis. Use `createHandClassifier`
127
+ * for a stateful per-hand classifier that prevents pinch flickering.
99
128
  */
100
129
  export function classifyGesture(landmarks) {
101
130
  if (landmarks.length < 21)
@@ -103,8 +132,48 @@ export function classifyGesture(landmarks) {
103
132
  if (areAllFingersCurled(landmarks)) {
104
133
  return 'fist';
105
134
  }
135
+ if (isPinchEnter(landmarks)) {
136
+ return 'pinch';
137
+ }
106
138
  if (areAllFingersExtended(landmarks)) {
107
139
  return 'openPalm';
108
140
  }
109
141
  return 'none';
110
142
  }
143
+ /**
144
+ * Returns a stateful classify function for a single hand.
145
+ *
146
+ * Pinch uses hysteresis: the hand enters 'pinch' when tips are within
147
+ * PINCH_THRESHOLD of hand size, and stays in 'pinch' until tips separate
148
+ * beyond PINCH_RELEASE_THRESHOLD. This prevents rapid toggling when fingers
149
+ * hover at the entry threshold during a held pinch gesture.
150
+ *
151
+ * Use one instance per hand (left / right) and reset between hand sessions.
152
+ */
153
+ export function createHandClassifier() {
154
+ let pinching = false;
155
+ return function classify(landmarks) {
156
+ if (landmarks.length < 21) {
157
+ pinching = false;
158
+ return 'none';
159
+ }
160
+ if (areAllFingersCurled(landmarks)) {
161
+ pinching = false;
162
+ return 'fist';
163
+ }
164
+ // Hysteresis: enter pinch on PINCH_THRESHOLD, exit on PINCH_RELEASE_THRESHOLD.
165
+ if (pinching) {
166
+ pinching = isPinchHeld(landmarks);
167
+ }
168
+ else {
169
+ pinching = isPinchEnter(landmarks);
170
+ }
171
+ if (pinching) {
172
+ return 'pinch';
173
+ }
174
+ if (areAllFingersExtended(landmarks)) {
175
+ return 'openPalm';
176
+ }
177
+ return 'none';
178
+ };
179
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { GestureController } from './GestureController.js';
2
2
  export { GestureStateMachine } from './GestureStateMachine.js';
3
3
  export type { StateMachineOutput } from './GestureStateMachine.js';
4
4
  export { WebcamOverlay } from './WebcamOverlay.js';
5
- export { classifyGesture, getHandSize, getTwoHandDistance } from './gestureClassifier.js';
5
+ export { classifyGesture, createHandClassifier, getHandSize, getTwoHandDistance } from './gestureClassifier.js';
6
6
  export type { GestureMode, GestureType, HandednessLabel, GestureFrame, DetectedHand, WebcamConfig, TuningConfig, SmoothedPoint, Point2D, HandLandmark, } from './types.js';
7
7
  export { DEFAULT_WEBCAM_CONFIG, DEFAULT_TUNING_CONFIG, LANDMARKS, COLORS, } from './constants.js';