|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GestureController { |
|
|
constructor(videoElement) { |
|
|
this.videoElement = videoElement; |
|
|
this.canvas = document.createElement('canvas'); |
|
|
this.ctx = this.canvas.getContext('2d'); |
|
|
this.isInitialized = false; |
|
|
this.isActive = false; |
|
|
this.hands = new Map(); |
|
|
this.gestures = new Map(); |
|
|
this.gestureHistory = []; |
|
|
|
|
|
|
|
|
this.config = { |
|
|
maxHands: 2, |
|
|
confidenceThreshold: 0.7, |
|
|
enable3DTracking: true, |
|
|
gestureSmoothing: 0.3, |
|
|
enableCustomGestures: true, |
|
|
sensitivity: 1.0, |
|
|
enableMultiUser: false |
|
|
}; |
|
|
|
|
|
|
|
|
this.defineGestures(); |
|
|
|
|
|
|
|
|
this.trackingData = { |
|
|
lastHandPositions: new Map(), |
|
|
gestureStartTime: null, |
|
|
currentGesture: null, |
|
|
gestureConfidence: 0 |
|
|
}; |
|
|
|
|
|
|
|
|
this.mockHandTracking = this.createMockHandTracking(); |
|
|
|
|
|
console.log('👋 تم تهيئة Gesture Controller'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async initialize() { |
|
|
try { |
|
|
console.log('🔧 بدء تهيئة التحكم بالإيماءات...'); |
|
|
|
|
|
|
|
|
this.canvas.width = this.videoElement.videoWidth || 640; |
|
|
this.canvas.height = this.videoElement.videoHeight || 480; |
|
|
|
|
|
|
|
|
if (!this.videoElement.srcObject) { |
|
|
throw new Error('لا يوجد مصدر فيديو للكاميرا'); |
|
|
} |
|
|
|
|
|
|
|
|
this.startTracking(); |
|
|
|
|
|
this.isInitialized = true; |
|
|
this.isActive = true; |
|
|
|
|
|
console.log('✅ تم تهيئة التحكم بالإيماءات'); |
|
|
|
|
|
return true; |
|
|
|
|
|
} catch (error) { |
|
|
console.error('❌ خطأ في تهيئة التحكم بالإيماءات:', error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defineGestures() { |
|
|
|
|
|
this.gestures.set('hand_open', { |
|
|
name: 'اليد مفتوحة', |
|
|
description: 'افتح يدك للتحديد', |
|
|
confidence: 0.8, |
|
|
triggers: ['select', 'confirm'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('hand_closed', { |
|
|
name: 'اليد مغلقة', |
|
|
description: 'أغلق يدك للإلغاء', |
|
|
confidence: 0.8, |
|
|
triggers: ['cancel', 'back'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('pointing', { |
|
|
name: 'الإشارة', |
|
|
description: 'أشر للإشارة إلى عنصر', |
|
|
confidence: 0.9, |
|
|
triggers: ['select', 'point'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('thumbs_up', { |
|
|
name: 'إبهام للأعلى', |
|
|
description: 'ارفع إبهامك للموافقة', |
|
|
confidence: 0.85, |
|
|
triggers: ['confirm', 'approve'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('thumbs_down', { |
|
|
name: 'إبهام للأسفل', |
|
|
description: 'أشر بإبهامك للأسفل للرفض', |
|
|
confidence: 0.85, |
|
|
triggers: ['reject', 'deny'] |
|
|
}); |
|
|
|
|
|
|
|
|
this.gestures.set('swipe_left', { |
|
|
name: 'السحب لليسار', |
|
|
description: 'حرك يدك من اليمين إلى اليسار', |
|
|
confidence: 0.75, |
|
|
triggers: ['navigate', 'previous'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('swipe_right', { |
|
|
name: 'السحب لليمين', |
|
|
description: 'حرك يدك من اليسار إلى اليمين', |
|
|
confidence: 0.75, |
|
|
triggers: ['navigate', 'next'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('swipe_up', { |
|
|
name: 'السحب للأعلى', |
|
|
description: 'حرك يدك من أسفل إلى أعلى', |
|
|
confidence: 0.75, |
|
|
triggers: ['scroll_up', 'zoom_in'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('swipe_down', { |
|
|
name: 'السحب للأسفل', |
|
|
description: 'حرك يدك من أعلى إلى أسفل', |
|
|
confidence: 0.75, |
|
|
triggers: ['scroll_down', 'zoom_out'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('pinch', { |
|
|
name: 'الإمساك', |
|
|
description: 'قرّب إصبعين لبعض للإمساك', |
|
|
confidence: 0.8, |
|
|
triggers: ['grab', 'hold'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('pinch_out', { |
|
|
name: 'فتح الإمساك', |
|
|
description: 'افتح إصبعيك للإفلات', |
|
|
confidence: 0.8, |
|
|
triggers: ['release', 'drop'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('rotate_clockwise', { |
|
|
name: 'الدوران عقارب الساعة', |
|
|
description: 'حرك يدك في اتجاه عقارب الساعة', |
|
|
confidence: 0.7, |
|
|
triggers: ['rotate', 'clockwise'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('rotate_counter_clockwise', { |
|
|
name: 'الدوران عكس عقارب الساعة', |
|
|
description: 'حرك يدك عكس اتجاه عقارب الساعة', |
|
|
confidence: 0.7, |
|
|
triggers: ['rotate', 'counter_clockwise'] |
|
|
}); |
|
|
|
|
|
|
|
|
this.gestures.set('call_me', { |
|
|
name: 'إشارة النداء', |
|
|
description: 'حرك إصبعك نحوك لنداء النظام', |
|
|
confidence: 0.75, |
|
|
triggers: ['voice_command', 'call'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('stop', { |
|
|
name: 'إشارة التوقف', |
|
|
description: 'ارفع راحة يدك للتوقف', |
|
|
confidence: 0.9, |
|
|
triggers: ['stop', 'pause'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('victory', { |
|
|
name: 'إشارة النصر', |
|
|
description: 'اعرض إصبعين للأعلى للإيجاب', |
|
|
confidence: 0.8, |
|
|
triggers: ['positive', 'success'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('number_one', { |
|
|
name: 'الرقم واحد', |
|
|
description: 'ارفع إصبع واحد للتركيز', |
|
|
confidence: 0.8, |
|
|
triggers: ['focus', 'primary'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('number_two', { |
|
|
name: 'الرقم اثنان', |
|
|
description: 'ارفع إصبعين للتحديد المتعدد', |
|
|
confidence: 0.8, |
|
|
triggers: ['multi_select', 'secondary'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('number_three', { |
|
|
name: 'الرقم ثلاثة', |
|
|
description: 'ارفع ثلاثة أصابع للإعدادات', |
|
|
confidence: 0.8, |
|
|
triggers: ['settings', 'options'] |
|
|
}); |
|
|
|
|
|
|
|
|
this.gestures.set('shield', { |
|
|
name: 'درع الحماية', |
|
|
description: 'ارفع يديك كدرع للحماية', |
|
|
confidence: 0.7, |
|
|
triggers: ['protect', 'defense'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('warning', { |
|
|
name: 'تحذير', |
|
|
description: 'حرك يدك بتحذير للخطر', |
|
|
confidence: 0.75, |
|
|
triggers: ['warning', 'alert'] |
|
|
}); |
|
|
|
|
|
this.gestures.set('scan', { |
|
|
name: 'فحص', |
|
|
description: 'حرك يدك في حركة فحص', |
|
|
confidence: 0.7, |
|
|
triggers: ['scan', 'analyze'] |
|
|
}); |
|
|
|
|
|
console.log('📝 تم تعريف', this.gestures.size, 'إيماءة'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
createMockHandTracking() { |
|
|
return { |
|
|
isTracking: false, |
|
|
hands: [], |
|
|
|
|
|
start: () => { |
|
|
this.isTracking = true; |
|
|
console.log('🎥 بدء تتبع اليدين الوهمي'); |
|
|
}, |
|
|
|
|
|
stop: () => { |
|
|
this.isTracking = false; |
|
|
this.hands.clear(); |
|
|
console.log('🛑 توقف تتبع اليدين الوهمي'); |
|
|
}, |
|
|
|
|
|
update: () => { |
|
|
if (!this.isTracking) return; |
|
|
|
|
|
|
|
|
this.simulateHandDetection(); |
|
|
} |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
simulateHandDetection() { |
|
|
const time = Date.now() * 0.001; |
|
|
|
|
|
|
|
|
const handCount = Math.random() > 0.3 ? 1 : 0; |
|
|
this.hands.clear(); |
|
|
|
|
|
for (let i = 0; i < handCount; i++) { |
|
|
const hand = { |
|
|
id: `hand_${i}`, |
|
|
handedness: i === 0 ? 'right' : 'left', |
|
|
landmarks: this.generateHandLandmarks(time, i), |
|
|
confidence: 0.8 + Math.random() * 0.2, |
|
|
timestamp: Date.now() |
|
|
}; |
|
|
|
|
|
this.hands.set(hand.id, hand); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.hands.size > 0) { |
|
|
this.processGestures(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generateHandLandmarks(time, handIndex) { |
|
|
const landmarks = []; |
|
|
|
|
|
|
|
|
for (let i = 0; i < 21; i++) { |
|
|
|
|
|
const baseX = 0.3 + handIndex * 0.4; |
|
|
const baseY = 0.5; |
|
|
const baseZ = 0; |
|
|
|
|
|
let x, y, z; |
|
|
|
|
|
if (i < 5) { |
|
|
|
|
|
x = baseX + Math.sin(time * 0.5 + i * 0.3) * 0.1; |
|
|
y = baseY + Math.cos(time * 0.7 + i * 0.2) * 0.05; |
|
|
} else if (i < 9) { |
|
|
|
|
|
x = baseX + Math.sin(time * 0.6 + (i-5) * 0.2) * 0.08; |
|
|
y = baseY - 0.1 - (i-5) * 0.03 + Math.sin(time * 0.8) * 0.02; |
|
|
} else if (i < 13) { |
|
|
|
|
|
x = baseX + Math.sin(time * 0.4 + (i-9) * 0.25) * 0.08; |
|
|
y = baseY - 0.12 - (i-9) * 0.03 + Math.cos(time * 0.9) * 0.02; |
|
|
} else if (i < 17) { |
|
|
|
|
|
x = baseX + Math.sin(time * 0.5 + (i-13) * 0.2) * 0.07; |
|
|
y = baseY - 0.11 - (i-13) * 0.03 + Math.sin(time * 1.0) * 0.02; |
|
|
} else { |
|
|
|
|
|
x = baseX + Math.sin(time * 0.7 + (i-17) * 0.15) * 0.06; |
|
|
y = baseY - 0.1 - (i-17) * 0.03 + Math.cos(time * 1.2) * 0.02; |
|
|
} |
|
|
|
|
|
z = baseZ + Math.sin(time * 0.3 + i * 0.1) * 0.02; |
|
|
|
|
|
landmarks.push({ x, y, z }); |
|
|
} |
|
|
|
|
|
return landmarks; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
startTracking() { |
|
|
if (!this.isInitialized) { |
|
|
throw new Error('Gesture Controller غير مهيأ'); |
|
|
} |
|
|
|
|
|
this.isActive = true; |
|
|
this.mockHandTracking.start(); |
|
|
this.startTrackingLoop(); |
|
|
|
|
|
console.log('🎯 بدء تتبع الإيماءات'); |
|
|
this.emit('tracking_started'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stopTracking() { |
|
|
this.isActive = false; |
|
|
this.mockHandTracking.stop(); |
|
|
|
|
|
console.log('🛑 توقف تتبع الإيماءات'); |
|
|
this.emit('tracking_stopped'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
startTrackingLoop() { |
|
|
const track = () => { |
|
|
if (!this.isActive) return; |
|
|
|
|
|
try { |
|
|
|
|
|
this.mockHandTracking.update(); |
|
|
|
|
|
|
|
|
this.updateHandLandmarks(); |
|
|
|
|
|
|
|
|
requestAnimationFrame(track); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('خطأ في حلقة التتبع:', error); |
|
|
setTimeout(track, 100); |
|
|
} |
|
|
}; |
|
|
|
|
|
track(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateHandLandmarks() { |
|
|
this.hands.forEach((hand, handId) => { |
|
|
|
|
|
hand.landmarks = this.smoothLandmarks(hand.landmarks, handId); |
|
|
|
|
|
|
|
|
hand.screenPosition = this.calculateScreenPosition(hand.landmarks); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
smoothLandmarks(landmarks, handId) { |
|
|
const lastPositions = this.trackingData.lastHandPositions.get(handId) || []; |
|
|
const smoothed = []; |
|
|
|
|
|
for (let i = 0; i < landmarks.length; i++) { |
|
|
const current = landmarks[i]; |
|
|
const last = lastPositions[i] || current; |
|
|
|
|
|
|
|
|
const alpha = this.config.gestureSmoothing; |
|
|
const smoothedPoint = { |
|
|
x: last.x * (1 - alpha) + current.x * alpha, |
|
|
y: last.y * (1 - alpha) + current.y * alpha, |
|
|
z: last.z * (1 - alpha) + current.z * alpha |
|
|
}; |
|
|
|
|
|
smoothed.push(smoothedPoint); |
|
|
} |
|
|
|
|
|
|
|
|
this.trackingData.lastHandPositions.set(handId, smoothed); |
|
|
|
|
|
return smoothed; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateScreenPosition(landmarks) { |
|
|
if (landmarks.length === 0) return null; |
|
|
|
|
|
|
|
|
const palm = landmarks[9]; |
|
|
|
|
|
|
|
|
const x = palm.x * this.canvas.width; |
|
|
const y = palm.y * this.canvas.height; |
|
|
|
|
|
return { x, y }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processGestures() { |
|
|
this.hands.forEach(hand => { |
|
|
const gesture = this.recognizeGesture(hand); |
|
|
|
|
|
if (gesture) { |
|
|
this.handleRecognizedGesture(hand, gesture); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
recognizeGesture(hand) { |
|
|
const landmarks = hand.landmarks; |
|
|
|
|
|
if (landmarks.length < 21) return null; |
|
|
|
|
|
|
|
|
for (const [gestureId, gestureData] of this.gestures) { |
|
|
const confidence = this.calculateGestureConfidence(gestureId, landmarks); |
|
|
|
|
|
if (confidence >= gestureData.confidence * this.config.sensitivity) { |
|
|
return { |
|
|
id: gestureId, |
|
|
data: gestureData, |
|
|
confidence: confidence, |
|
|
hand: hand |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
return null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateGestureConfidence(gestureId, landmarks) { |
|
|
switch (gestureId) { |
|
|
case 'hand_open': |
|
|
return this.checkHandOpen(landmarks); |
|
|
|
|
|
case 'hand_closed': |
|
|
return this.checkHandClosed(landmarks); |
|
|
|
|
|
case 'pointing': |
|
|
return this.checkPointing(landmarks); |
|
|
|
|
|
case 'swipe_left': |
|
|
return this.checkSwipeGesture(landmarks, 'left'); |
|
|
|
|
|
case 'swipe_right': |
|
|
return this.checkSwipeGesture(landmarks, 'right'); |
|
|
|
|
|
case 'swipe_up': |
|
|
return this.checkSwipeGesture(landmarks, 'up'); |
|
|
|
|
|
case 'swipe_down': |
|
|
return this.checkSwipeGesture(landmarks, 'down'); |
|
|
|
|
|
case 'pinch': |
|
|
return this.checkPinch(landmarks); |
|
|
|
|
|
case 'pinch_out': |
|
|
return this.checkPinchOut(landmarks); |
|
|
|
|
|
case 'thumbs_up': |
|
|
return this.checkThumbsUp(landmarks); |
|
|
|
|
|
case 'thumbs_down': |
|
|
return this.checkThumbsDown(landmarks); |
|
|
|
|
|
case 'victory': |
|
|
return this.checkVictory(landmarks); |
|
|
|
|
|
case 'number_one': |
|
|
return this.checkNumberOne(landmarks); |
|
|
|
|
|
case 'number_two': |
|
|
return this.checkNumberTwo(landmarks); |
|
|
|
|
|
case 'number_three': |
|
|
return this.checkNumberThree(landmarks); |
|
|
|
|
|
default: |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkHandOpen(landmarks) { |
|
|
|
|
|
const wrist = landmarks[0]; |
|
|
let fingerCount = 0; |
|
|
|
|
|
const fingerTips = [4, 8, 12, 16, 20]; |
|
|
|
|
|
for (const tip of fingerTips) { |
|
|
const distance = this.calculateDistance(landmarks[tip], wrist); |
|
|
if (distance > 0.1) fingerCount++; |
|
|
} |
|
|
|
|
|
return fingerCount >= 4 ? 0.9 : 0.3; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkHandClosed(landmarks) { |
|
|
|
|
|
const palm = landmarks[9]; |
|
|
let closeCount = 0; |
|
|
|
|
|
const fingerTips = [4, 8, 12, 16, 20]; |
|
|
|
|
|
for (const tip of fingerTips) { |
|
|
const distance = this.calculateDistance(landmarks[tip], palm); |
|
|
if (distance < 0.05) closeCount++; |
|
|
} |
|
|
|
|
|
return closeCount >= 4 ? 0.9 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkPointing(landmarks) { |
|
|
|
|
|
const indexTip = landmarks[8]; |
|
|
const indexPip = landmarks[6]; |
|
|
const middleTip = landmarks[12]; |
|
|
const ringTip = landmarks[16]; |
|
|
const pinkyTip = landmarks[20]; |
|
|
|
|
|
const indexExtended = this.calculateDistance(indexTip, indexPip) > 0.08; |
|
|
const othersClosed = this.calculateDistance(middleTip, landmarks[9]) < 0.06 && |
|
|
this.calculateDistance(ringTip, landmarks[9]) < 0.06 && |
|
|
this.calculateDistance(pinkyTip, landmarks[9]) < 0.06; |
|
|
|
|
|
return (indexExtended && othersClosed) ? 0.85 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkSwipeGesture(landmarks, direction) { |
|
|
const time = Date.now() * 0.001; |
|
|
const currentPos = landmarks[9]; |
|
|
|
|
|
|
|
|
const lastPos = this.trackingData.lastHandPositions.get('swipe_history') || []; |
|
|
if (lastPos.length < 2) return 0; |
|
|
|
|
|
const deltaX = currentPos.x - lastPos[lastPos.length - 2].x; |
|
|
const deltaY = currentPos.y - lastPos[lastPos.length - 2].y; |
|
|
|
|
|
|
|
|
lastPos.push(currentPos); |
|
|
if (lastPos.length > 10) lastPos.shift(); |
|
|
this.trackingData.lastHandPositions.set('swipe_history', lastPos); |
|
|
|
|
|
const threshold = 0.05; |
|
|
|
|
|
switch (direction) { |
|
|
case 'left': |
|
|
return deltaX < -threshold ? 0.8 : 0.1; |
|
|
case 'right': |
|
|
return deltaX > threshold ? 0.8 : 0.1; |
|
|
case 'up': |
|
|
return deltaY < -threshold ? 0.8 : 0.1; |
|
|
case 'down': |
|
|
return deltaY > threshold ? 0.8 : 0.1; |
|
|
default: |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkPinch(landmarks) { |
|
|
const thumbTip = landmarks[4]; |
|
|
const indexTip = landmarks[8]; |
|
|
const distance = this.calculateDistance(thumbTip, indexTip); |
|
|
|
|
|
return distance < 0.03 ? 0.9 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkPinchOut(landmarks) { |
|
|
const thumbTip = landmarks[4]; |
|
|
const indexTip = landmarks[8]; |
|
|
const distance = this.calculateDistance(thumbTip, indexTip); |
|
|
|
|
|
return distance > 0.08 ? 0.9 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkThumbsUp(landmarks) { |
|
|
const thumbTip = landmarks[4]; |
|
|
const thumbIp = landmarks[3]; |
|
|
const indexTip = landmarks[8]; |
|
|
const middleTip = landmarks[12]; |
|
|
|
|
|
const thumbUp = thumbTip.y < thumbIp.y - 0.02; |
|
|
const othersDown = this.calculateDistance(indexTip, landmarks[9]) < 0.06 && |
|
|
this.calculateDistance(middleTip, landmarks[9]) < 0.06; |
|
|
|
|
|
return (thumbUp && othersDown) ? 0.85 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkThumbsDown(landmarks) { |
|
|
const thumbTip = landmarks[4]; |
|
|
const thumbIp = landmarks[3]; |
|
|
const indexTip = landmarks[8]; |
|
|
const middleTip = landmarks[12]; |
|
|
|
|
|
const thumbDown = thumbTip.y > thumbIp.y + 0.02; |
|
|
const othersDown = this.calculateDistance(indexTip, landmarks[9]) < 0.06 && |
|
|
this.calculateDistance(middleTip, landmarks[9]) < 0.06; |
|
|
|
|
|
return (thumbDown && othersDown) ? 0.85 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkVictory(landmarks) { |
|
|
const indexTip = landmarks[8]; |
|
|
const middleTip = landmarks[12]; |
|
|
const indexPip = landmarks[6]; |
|
|
const middlePip = landmarks[10]; |
|
|
|
|
|
const bothUp = indexTip.y < indexPip.y && middleTip.y < middlePip.y; |
|
|
const distance = this.calculateDistance(indexTip, middleTip); |
|
|
const closeTogether = distance < 0.04; |
|
|
|
|
|
return (bothUp && closeTogether) ? 0.8 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkNumberOne(landmarks) { |
|
|
const indexTip = landmarks[8]; |
|
|
const indexPip = landmarks[6]; |
|
|
const middleTip = landmarks[12]; |
|
|
const ringTip = landmarks[16]; |
|
|
const pinkyTip = landmarks[20]; |
|
|
|
|
|
const indexExtended = indexTip.y < indexPip.y - 0.02; |
|
|
const othersDown = this.calculateDistance(middleTip, landmarks[9]) < 0.06 && |
|
|
this.calculateDistance(ringTip, landmarks[9]) < 0.06 && |
|
|
this.calculateDistance(pinkyTip, landmarks[9]) < 0.06; |
|
|
|
|
|
return (indexExtended && othersDown) ? 0.8 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkNumberTwo(landmarks) { |
|
|
const indexTip = landmarks[8]; |
|
|
const middleTip = landmarks[12]; |
|
|
const indexPip = landmarks[6]; |
|
|
const middlePip = landmarks[10]; |
|
|
|
|
|
const indexUp = indexTip.y < indexPip.y - 0.02; |
|
|
const middleUp = middleTip.y < middlePip.y - 0.02; |
|
|
const ringDown = this.calculateDistance(landmarks[16], landmarks[9]) < 0.06; |
|
|
const pinkyDown = this.calculateDistance(landmarks[20], landmarks[9]) < 0.06; |
|
|
|
|
|
return (indexUp && middleUp && ringDown && pinkyDown) ? 0.8 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
checkNumberThree(landmarks) { |
|
|
const indexTip = landmarks[8]; |
|
|
const middleTip = landmarks[12]; |
|
|
const ringTip = landmarks[16]; |
|
|
const indexPip = landmarks[6]; |
|
|
const middlePip = landmarks[10]; |
|
|
const ringPip = landmarks[14]; |
|
|
|
|
|
const indexUp = indexTip.y < indexPip.y - 0.02; |
|
|
const middleUp = middleTip.y < middlePip.y - 0.02; |
|
|
const ringUp = ringTip.y < ringPip.y - 0.02; |
|
|
const pinkyDown = this.calculateDistance(landmarks[20], landmarks[9]) < 0.06; |
|
|
|
|
|
return (indexUp && middleUp && ringUp && pinkyDown) ? 0.8 : 0.2; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculateDistance(point1, point2) { |
|
|
const dx = point1.x - point2.x; |
|
|
const dy = point1.y - point2.y; |
|
|
const dz = point1.z - point2.z; |
|
|
return Math.sqrt(dx * dx + dy * dy + dz * dz); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleRecognizedGesture(hand, gesture) { |
|
|
const now = Date.now(); |
|
|
|
|
|
|
|
|
const lastGesture = this.gestureHistory[this.gestureHistory.length - 1]; |
|
|
|
|
|
if (!lastGesture || lastGesture.id !== gesture.id || |
|
|
(now - lastGesture.timestamp) > 1000) { |
|
|
|
|
|
console.log('👋 إيماءة معترف بها:', gesture.data.name, `(الثقة: ${Math.round(gesture.confidence * 100)}%)`); |
|
|
|
|
|
|
|
|
this.gestureHistory.push({ |
|
|
id: gesture.id, |
|
|
data: gesture.data, |
|
|
confidence: gesture.confidence, |
|
|
hand: hand, |
|
|
timestamp: now, |
|
|
screenPosition: hand.screenPosition |
|
|
}); |
|
|
|
|
|
|
|
|
if (this.gestureHistory.length > 50) { |
|
|
this.gestureHistory.shift(); |
|
|
} |
|
|
|
|
|
|
|
|
this.emit('gesture_recognized', gesture); |
|
|
|
|
|
|
|
|
this.executeGestureActions(gesture); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
executeGestureActions(gesture) { |
|
|
const actions = gesture.data.triggers; |
|
|
|
|
|
actions.forEach(action => { |
|
|
console.log('⚡ تنفيذ إجراء:', action); |
|
|
this.emit(`gesture_${action}`, gesture); |
|
|
}); |
|
|
|
|
|
|
|
|
switch (gesture.id) { |
|
|
case 'pointing': |
|
|
this.emit('hand_detected', { |
|
|
position: gesture.hand.screenPosition, |
|
|
confidence: gesture.confidence |
|
|
}); |
|
|
break; |
|
|
|
|
|
case 'swipe_left': |
|
|
this.emit('swipe_left', gesture); |
|
|
break; |
|
|
|
|
|
case 'swipe_right': |
|
|
this.emit('swipe_right', gesture); |
|
|
break; |
|
|
|
|
|
case 'pinch': |
|
|
this.emit('pinch', { |
|
|
position: gesture.hand.screenPosition, |
|
|
confidence: gesture.confidence |
|
|
}); |
|
|
break; |
|
|
|
|
|
case 'pinch_out': |
|
|
this.emit('pinch_out', gesture); |
|
|
break; |
|
|
|
|
|
case 'call_me': |
|
|
this.emit('voice_command', gesture); |
|
|
break; |
|
|
|
|
|
case 'stop': |
|
|
this.emit('emergency_stop', gesture); |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getSupportedGestures() { |
|
|
const gestures = []; |
|
|
this.gestures.forEach((data, id) => { |
|
|
gestures.push({ |
|
|
id: id, |
|
|
name: data.name, |
|
|
description: data.description, |
|
|
triggers: data.triggers |
|
|
}); |
|
|
}); |
|
|
return gestures; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getGestureHistory(limit = 10) { |
|
|
return this.gestureHistory.slice(-limit); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getTrackingState() { |
|
|
return { |
|
|
isActive: this.isActive, |
|
|
isInitialized: this.isInitialized, |
|
|
handsDetected: this.hands.size, |
|
|
lastGesture: this.gestureHistory[this.gestureHistory.length - 1] || null, |
|
|
supportedGestures: this.gestures.size, |
|
|
config: this.config |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateConfig(newConfig) { |
|
|
this.config = { ...this.config, ...newConfig }; |
|
|
console.log('🔧 تم تحديث إعدادات التحكم بالإيماءات:', this.config); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addCustomGesture(id, name, description, callback) { |
|
|
this.gestures.set(id, { |
|
|
name: name, |
|
|
description: description, |
|
|
confidence: 0.7, |
|
|
triggers: ['custom'], |
|
|
callback: callback |
|
|
}); |
|
|
|
|
|
console.log('✨ تم إضافة إيماءة مخصصة:', id); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
simulateGesture(gestureId) { |
|
|
const gesture = this.gestures.get(gestureId); |
|
|
if (!gesture) { |
|
|
console.warn('إيماءة غير موجودة:', gestureId); |
|
|
return; |
|
|
} |
|
|
|
|
|
const mockHand = { |
|
|
id: 'mock_hand', |
|
|
handedness: 'right', |
|
|
landmarks: this.generateHandLandmarks(Date.now() * 0.001, 0), |
|
|
confidence: 0.9, |
|
|
screenPosition: { x: 320, y: 240 }, |
|
|
timestamp: Date.now() |
|
|
}; |
|
|
|
|
|
const recognizedGesture = { |
|
|
id: gestureId, |
|
|
data: gesture, |
|
|
confidence: 0.9, |
|
|
hand: mockHand |
|
|
}; |
|
|
|
|
|
this.handleRecognizedGesture(mockHand, recognizedGesture); |
|
|
console.log('🎭 تم محاكاة إيماءة:', gestureId); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
emit(eventName, data) { |
|
|
if (this.eventListeners[eventName]) { |
|
|
this.eventListeners[eventName].forEach(callback => { |
|
|
try { |
|
|
callback(data); |
|
|
} catch (error) { |
|
|
console.error('خطأ في مستمع الحدث:', error); |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
on(eventName, callback) { |
|
|
if (!this.eventListeners[eventName]) { |
|
|
this.eventListeners[eventName] = []; |
|
|
} |
|
|
this.eventListeners[eventName].push(callback); |
|
|
} |
|
|
|
|
|
off(eventName, callback) { |
|
|
if (this.eventListeners[eventName]) { |
|
|
this.eventListeners[eventName] = this.eventListeners[eventName].filter(cb => cb !== callback); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
destroy() { |
|
|
console.log('🗑️ تدمير Gesture Controller...'); |
|
|
|
|
|
this.stopTracking(); |
|
|
this.hands.clear(); |
|
|
this.gestureHistory = []; |
|
|
this.trackingData.lastHandPositions.clear(); |
|
|
|
|
|
console.log('✅ تم تدمير Gesture Controller'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof module !== 'undefined' && module.exports) { |
|
|
module.exports = GestureController; |
|
|
} |