Complete guide for developers to extend and customize the Hand Gesture Control System.
The system follows a modular, event-driven architecture with clear separation of concerns:
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ UI Controller│ │Config Manager│ │Action Handler│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Gesture Processing Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │Gesture Engine│ │Event Emitter │ │Smoothing │ │
│ │ │ │ │ │Filter │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Detection Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │MediaPipe │ │Hand Tracker │ │Landmark │ │
│ │Hands │ │ │ │Processor │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Input Layer │
│ ┌──────────────┐ ┌──────────────┐ │
│ │WebRTC Stream │ │Video Canvas │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
Central event system for communication between modules.
Key Methods:
on(eventName, callback): Subscribe to eventsemit(eventName, data): Emit eventsoff(eventName, callback): Unsubscribeonce(eventName, callback): Subscribe once
Usage:
const eventBus = new EventBus();
// Subscribe
eventBus.on('gesture:swipe:left', (data) => {
console.log('Swipe left detected:', data);
});
// Emit
eventBus.emit('gesture:swipe:left', { velocity: 0.8 });Processes raw MediaPipe landmarks into meaningful data.
Key Methods:
calculateDistance(point1, point2): Euclidean distancecalculateVelocity(current, previous, deltaTime): Movement velocitygetBoundingBox(landmarks): Hand bounding boxgetFingerStates(landmarks): Which fingers are extended
Coordinates all gesture detectors and manages detection flow.
Key Methods:
registerDetector(name, detector): Add new detectorprocessFrame(handData): Process hand dataenableDetector(name)/disableDetector(name): Toggle detectors
Create a new file in src/js/recognition/detectors/:
// src/js/recognition/detectors/ThumbsUpDetector.js
import { BaseGestureDetector } from './BaseGestureDetector.js';
export class ThumbsUpDetector extends BaseGestureDetector {
constructor(config, landmarkProcessor) {
super(config);
this.landmarkProcessor = landmarkProcessor;
}
detect(landmarks, previousLandmarks, deltaTime, handedness) {
if (!this.isEnabled() || !landmarks || landmarks.length < 21) {
return null;
}
// Get finger states
const fingerStates = this.landmarkProcessor.getFingerStates(landmarks);
// Thumb should be extended and pointing up
const thumbUp = landmarks[4].y < landmarks[3].y;
// Other fingers should be closed
const fingersClosed =
!fingerStates.index &&
!fingerStates.middle &&
!fingerStates.ring &&
!fingerStates.pinky;
if (thumbUp && fingersClosed) {
// Check debouncing
if (!this.canDetect()) {
return null;
}
this.markDetection();
return {
detected: true,
type: 'static',
gesture: 'thumbsup',
confidence: 0.9,
handedness,
timestamp: performance.now()
};
}
return null;
}
}Edit src/js/config/gestureConfig.js:
export const gestureConfig = {
// ... existing config
thumbsup: {
enabled: true,
holdDuration: 500,
confidenceThreshold: 0.7,
debounceMs: 1000
}
};Edit src/js/main.js in the initializeGestureDetectors() method:
import { ThumbsUpDetector } from './recognition/detectors/ThumbsUpDetector.js';
initializeGestureDetectors() {
// ... existing detectors
// Thumbs up detector
const thumbsUpDetector = new ThumbsUpDetector(
this.gestureConfig.thumbsup,
this.landmarkProcessor
);
this.gestureEngine.registerDetector('thumbsup', thumbsUpDetector);
}Edit src/js/actions/actionRegistry.js:
export function onThumbsUpGesture(data) {
console.log('Thumbs up detected:', data);
logToUI(`Thumbs Up → Like! (${data.handedness} hand)`, 'gesture');
// Your custom action
// Example: Send "like" to server
}
export const actionRegistry = {
// ... existing actions
'static:thumbsup': onThumbsUpGesture
};Edit src/js/actions/ActionHandler.js in subscribeToGestureEvents():
subscribeToGestureEvents() {
// ... existing subscriptions
this.eventBus.on('gesture:static:thumbsup', (data) =>
this.executeAction('static:thumbsup', data)
);
}export class FistDetector extends BaseGestureDetector {
detect(landmarks, previousLandmarks, deltaTime, handedness) {
if (!this.isEnabled() || !landmarks) return null;
const fingerStates = this.landmarkProcessor.getFingerStates(landmarks);
// All fingers should be closed
const allClosed = !fingerStates.thumb &&
!fingerStates.index &&
!fingerStates.middle &&
!fingerStates.ring &&
!fingerStates.pinky;
if (allClosed && this.canDetect()) {
this.markDetection();
return {
detected: true,
type: 'static',
gesture: 'fist',
confidence: 0.85,
handedness,
timestamp: performance.now()
};
}
return null;
}
}export class PointingDetector extends BaseGestureDetector {
detect(landmarks, previousLandmarks, deltaTime, handedness) {
if (!this.isEnabled() || !landmarks) return null;
const fingerStates = this.landmarkProcessor.getFingerStates(landmarks);
// Only index finger extended
const pointing = fingerStates.index &&
!fingerStates.middle &&
!fingerStates.ring &&
!fingerStates.pinky;
if (pointing && this.canDetect()) {
// Get pointing direction
const indexTip = landmarks[8];
const wrist = landmarks[0];
const direction = {
x: indexTip.x - wrist.x,
y: indexTip.y - wrist.y
};
this.markDetection();
return {
detected: true,
type: 'static',
gesture: 'pointing',
direction,
confidence: 0.9,
handedness,
timestamp: performance.now()
};
}
return null;
}
}export class RotationDetector extends BaseGestureDetector {
constructor(config, landmarkProcessor) {
super(config);
this.landmarkProcessor = landmarkProcessor;
this.previousAngle = null;
this.rotationAccumulator = 0;
}
detect(landmarks, previousLandmarks, deltaTime, handedness) {
if (!this.isEnabled() || !landmarks || !previousLandmarks) return null;
// Calculate angle between index and pinky
const index = landmarks[8];
const pinky = landmarks[20];
const wrist = landmarks[0];
const angle = this.landmarkProcessor.calculateAngle(index, wrist, pinky);
if (this.previousAngle !== null) {
const angleDelta = angle - this.previousAngle;
this.rotationAccumulator += angleDelta;
// Detect full rotation (360 degrees)
if (Math.abs(this.rotationAccumulator) > 180 && this.canDetect()) {
const direction = this.rotationAccumulator > 0 ? 'clockwise' : 'counterclockwise';
this.markDetection();
this.rotationAccumulator = 0;
return {
detected: true,
type: 'rotation',
direction,
angle: Math.abs(this.rotationAccumulator),
handedness,
timestamp: performance.now()
};
}
}
this.previousAngle = angle;
return null;
}
reset() {
super.reset();
this.previousAngle = null;
this.rotationAccumulator = 0;
}
}{
enabled: true, // Enable/disable detector
velocityThreshold: 0.5, // Minimum velocity (units/sec)
distanceThreshold: 0.05, // Minimum distance (normalized)
holdDuration: 500, // Hold time (milliseconds)
confidenceThreshold: 0.7, // Minimum confidence (0-1)
debounceMs: 300, // Debounce period (milliseconds)
smoothingWindow: 5, // Smoothing window size (frames)
smoothingAlpha: 0.3 // EMA smoothing factor (0-1)
}{
camera: {
resolution: { width: 1280, height: 720 },
fps: 30,
facingMode: 'user'
},
mediapipe: {
useOffline: false,
maxNumHands: 2,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
},
performance: {
targetFPS: 30,
maxCPUUsage: 80,
enableThrottling: true
}
}Base class for all gesture detectors.
Methods:
detect(landmarks, previousLandmarks, deltaTime, handedness): Main detection methodreset(): Reset detector statecanDetect(): Check if debounce period passedmarkDetection(): Mark detection timeisEnabled(): Check if detector is enabled
Methods:
calculateDistance(point1, point2, use3D): Calculate distance between pointscalculateVelocity(current, previous, deltaTime): Calculate velocitygetBoundingBox(landmarks): Get bounding boxgetFingerStates(landmarks): Get finger extension statesgetCenterPoint(landmarks): Get hand centercalculateAngle(point1, point2, point3): Calculate angleareFingersSpread(landmarks): Check if fingers are spread
Classes:
MovingAverageFilter(windowSize): Simple moving averageExponentialMovingAverageFilter(alpha): Exponential moving averageOneEuroFilter(minCutoff, beta, dCutoff): Advanced adaptive filter
Methods:
addSample(value): Add new samplegetSmoothed(): Get smoothed valuereset(): Reset filter
// Skip frames if processing is slow
if (processingTime > framebudget) {
skipFrame = true;
}
// Reduce resolution
systemConfig.camera.resolution = { width: 640, height: 480 };
// Limit hand detection
systemConfig.mediapipe.maxNumHands = 1;// Early returns
if (!this.isEnabled()) return null;
if (!landmarks || landmarks.length < 21) return null;
// Cache calculations
const fingerStates = this.landmarkProcessor.getFingerStates(landmarks);
// Use debouncing
if (!this.canDetect()) return null;// EMA is faster than moving average
const filter = new ExponentialMovingAverageFilter(0.3);
// Reduce smoothing window
smoothingWindow: 3 // Instead of 5// Example test for ThumbsUpDetector
import { ThumbsUpDetector } from './ThumbsUpDetector.js';
describe('ThumbsUpDetector', () => {
let detector;
let mockLandmarkProcessor;
beforeEach(() => {
mockLandmarkProcessor = {
getFingerStates: jest.fn()
};
detector = new ThumbsUpDetector({}, mockLandmarkProcessor);
});
test('detects thumbs up gesture', () => {
const landmarks = createMockLandmarks();
mockLandmarkProcessor.getFingerStates.mockReturnValue({
thumb: true,
index: false,
middle: false,
ring: false,
pinky: false
});
const result = detector.detect(landmarks, null, 0.016, 'Right');
expect(result).not.toBeNull();
expect(result.gesture).toBe('thumbsup');
});
});// In main.js
console.log('Hand data:', handData);
console.log('Gesture detected:', result);
console.log('Processing time:', processingTime);// Enable all visualization options
systemConfig.visualization = {
enabled: true,
showLandmarks: true,
showConnections: true,
showLabels: true,
showFPS: true,
showBoundingBox: true
};// Track processing times
const startTime = performance.now();
// ... processing code
const endTime = performance.now();
console.log(`Processing took ${endTime - startTime}ms`);- Always extend BaseGestureDetector for consistency
- Use debouncing to prevent multiple triggers
- Implement reset() to clean up state
- Add configuration options for flexibility
- Test with different lighting conditions
- Document your gestures in GESTURE_GUIDE.md
- Use meaningful event names following the pattern
gesture:type:name - Handle edge cases (null landmarks, missing data)
- Optimize for performance (early returns, caching)
- Provide user feedback through visualization
When contributing new gestures:
- Follow the existing code style
- Add comprehensive documentation
- Include configuration options
- Write unit tests
- Update GESTURE_GUIDE.md
- Test on multiple devices
- Consider accessibility
For development questions:
- Check existing detector implementations
- Review the architecture diagram
- Open an issue on GitHub
- Join our developer community