|
|
|
|
|
|
|
|
|
|
|
const emails = [ |
|
|
{ |
|
|
id: 1, |
|
|
sender: "Interview Kicks", |
|
|
subject: "Can I Become an ML Engineer? Why Not, We Ask!", |
|
|
snippet: "It's time to banish that doubt forever. Attend our masterclass to unlock your potential.", |
|
|
timestamp: "1:31 PM" |
|
|
}, |
|
|
{ |
|
|
id: 2, |
|
|
sender: "Career Brew", |
|
|
subject: "7th Sep Jobs, 87 Hottest Jobs and Early Career Jobs - Do Not Miss", |
|
|
snippet: "Top companies are hiring right now β don't miss your shot at these exclusive opportunities!", |
|
|
timestamp: "12:35 PM" |
|
|
}, |
|
|
{ |
|
|
id: 3, |
|
|
sender: "Professor Smith", |
|
|
subject: "Research Collaboration Proposal", |
|
|
snippet: "I've reviewed your proposal draft. Let's schedule a call next week to discuss next steps.", |
|
|
timestamp: "10:22 AM" |
|
|
}, |
|
|
{ |
|
|
id: 4, |
|
|
sender: "Friend", |
|
|
subject: "Weekend Plans", |
|
|
snippet: "Are we still on for the hiking trip this weekend? Let me know so I can book the gear.", |
|
|
timestamp: "Yesterday" |
|
|
}, |
|
|
{ |
|
|
id: 5, |
|
|
sender: "Bank Support", |
|
|
subject: "Monthly Statement", |
|
|
snippet: "Your statement is ready. Minimum due: $120. Avoid penalties by paying before the 15th.", |
|
|
timestamp: "Sep 5" |
|
|
}, |
|
|
{ |
|
|
id: 6, |
|
|
sender: "Netflix", |
|
|
subject: "New Releases This Week!", |
|
|
snippet: "Check out the hottest new shows and movies added to your watchlist this week.", |
|
|
timestamp: "Sep 4" |
|
|
}, |
|
|
{ |
|
|
id: 7, |
|
|
sender: "Amazon", |
|
|
subject: "Your Order #12345 Has Shipped", |
|
|
snippet: "Your package is on the way! Track your delivery using the link below.", |
|
|
timestamp: "Sep 3" |
|
|
}, |
|
|
{ |
|
|
id: 8, |
|
|
sender: "LinkedIn", |
|
|
subject: "You have 5 new notifications", |
|
|
snippet: "See who viewed your profile and new job recommendations.", |
|
|
timestamp: "Sep 2" |
|
|
}, |
|
|
{ |
|
|
id: 9, |
|
|
sender: "Microsoft", |
|
|
subject: "Your Office 365 Subscription Renewal", |
|
|
snippet: "Your Office 365 subscription will expire in 7 days. Renew now to avoid service interruption.", |
|
|
timestamp: "Sep 1" |
|
|
}, |
|
|
{ |
|
|
id: 10, |
|
|
sender: "TripAdvisor", |
|
|
subject: "Your upcoming trip to Paris", |
|
|
snippet: "Don't forget to check-in for your flight tomorrow. Here's your itinerary and hotel information.", |
|
|
timestamp: "Aug 30" |
|
|
}, |
|
|
{ |
|
|
id: 11, |
|
|
sender: "Tech News", |
|
|
subject: "The Future of AI is Here", |
|
|
snippet: "Discover how AI is transforming industries and what it means for your career.", |
|
|
timestamp: "Aug 29" |
|
|
}, |
|
|
{ |
|
|
id: 12, |
|
|
sender: "University Alumni", |
|
|
subject: "Alumni Reunion This Weekend", |
|
|
snippet: "Join us for our annual alumni reunion and reconnect with old friends.", |
|
|
timestamp: "Aug 28" |
|
|
}, |
|
|
{ |
|
|
id: 13, |
|
|
sender: "Travel Agency", |
|
|
subject: "Exclusive Deal: 50% Off Caribbean Cruise", |
|
|
snippet: "Book now and enjoy a luxury cruise with significant savings.", |
|
|
timestamp: "Aug 27" |
|
|
}, |
|
|
{ |
|
|
id: 14, |
|
|
sender: "Fitness Center", |
|
|
subject: "New Workouts Available", |
|
|
snippet: "Check out our new workout routines designed by top trainers.", |
|
|
timestamp: "Aug 26" |
|
|
}, |
|
|
{ |
|
|
id: 15, |
|
|
sender: "Online Course", |
|
|
subject: "Your Certificate of Completion", |
|
|
snippet: "Congratulations! You've completed the course. Download your certificate now.", |
|
|
timestamp: "Aug 25" |
|
|
} |
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DebugManager { |
|
|
constructor() { |
|
|
this.statusEl = document.getElementById('debugStatus'); |
|
|
this.selectedEmailEl = document.getElementById('debugSelectedEmail'); |
|
|
this.gestureTypeEl = document.getElementById('debugGestureType'); |
|
|
this.bufferCountEl = document.getElementById('debugBufferCount'); |
|
|
this.circleCountEl = document.getElementById('debugCircleCount'); |
|
|
this.cameraStatusEl = document.getElementById('debugCameraStatus'); |
|
|
this.lastErrorEl = document.getElementById('debugLastError'); |
|
|
this.statusIndicator = document.getElementById('statusIndicator'); |
|
|
|
|
|
|
|
|
this.toggleButton = document.getElementById('debugToggle'); |
|
|
this.debugPanel = document.getElementById('debugPanel'); |
|
|
|
|
|
|
|
|
this.toggleButton.addEventListener('click', () => { |
|
|
this.toggleDebugPanel(); |
|
|
}); |
|
|
} |
|
|
|
|
|
updateStatus(status) { |
|
|
this.statusEl.textContent = status; |
|
|
} |
|
|
|
|
|
updateSelectedEmail(emailId) { |
|
|
if (emailId) { |
|
|
const email = emails.find(e => e.id === emailId); |
|
|
this.selectedEmailEl.textContent = email ? email.subject.substring(0, 20) + '...' : 'Unknown'; |
|
|
} else { |
|
|
this.selectedEmailEl.textContent = 'None'; |
|
|
} |
|
|
} |
|
|
|
|
|
updateGestureType(gestureType) { |
|
|
this.gestureTypeEl.textContent = gestureType; |
|
|
} |
|
|
|
|
|
updateBufferCount(count) { |
|
|
this.bufferCountEl.textContent = `${count} points`; |
|
|
} |
|
|
|
|
|
updateCircleCount(count) { |
|
|
this.circleCountEl.textContent = `${count} points`; |
|
|
} |
|
|
|
|
|
updateCameraStatus(status) { |
|
|
this.cameraStatusEl.textContent = status; |
|
|
} |
|
|
|
|
|
logError(error) { |
|
|
console.error("Gesture Detection Error:", error); |
|
|
this.lastErrorEl.textContent = error.message.substring(0, 50) + (error.message.length > 50 ? '...' : ''); |
|
|
|
|
|
|
|
|
this.statusIndicator.className = 'status-indicator error'; |
|
|
} |
|
|
|
|
|
setReady() { |
|
|
this.updateStatus('Ready'); |
|
|
this.statusIndicator.className = 'status-indicator ready'; |
|
|
} |
|
|
|
|
|
setProcessing() { |
|
|
this.updateStatus('Processing...'); |
|
|
this.statusIndicator.className = 'status-indicator processing'; |
|
|
} |
|
|
|
|
|
toggleDebugPanel() { |
|
|
if (this.debugPanel.classList.contains('visible')) { |
|
|
this.debugPanel.classList.remove('visible'); |
|
|
} else { |
|
|
this.debugPanel.classList.add('visible'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UIManager { |
|
|
constructor() { |
|
|
this.emailList = document.getElementById('emailList'); |
|
|
this.actionFeedback = document.getElementById('actionFeedback'); |
|
|
this.selectionHighlight = document.getElementById('selectionHighlight'); |
|
|
this.handLandmarks = document.getElementById('handLandmarks'); |
|
|
this.gesturePath = document.getElementById('gesturePath'); |
|
|
this.emailListRect = null; |
|
|
|
|
|
this.selectedEmail = null; |
|
|
this.emailElements = []; |
|
|
|
|
|
this.renderEmails(); |
|
|
this.setupEventListeners(); |
|
|
|
|
|
|
|
|
this.createConfirmationOverlay(); |
|
|
|
|
|
|
|
|
this.createScrollIndicator(); |
|
|
} |
|
|
|
|
|
createConfirmationOverlay() { |
|
|
const overlay = document.createElement('div'); |
|
|
overlay.className = 'confirmation-overlay'; |
|
|
overlay.innerHTML = ` |
|
|
<div class="confirmation-box"> |
|
|
<div class="confirmation-icon">β οΈ</div> |
|
|
<h3 class="confirmation-title">Confirm Action</h3> |
|
|
<p class="confirmation-message">Are you sure you want to perform this action?</p> |
|
|
<div class="confirmation-buttons"> |
|
|
<button class="confirmation-button cancel">Cancel</button> |
|
|
<button class="confirmation-button confirm">Confirm</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
document.body.appendChild(overlay); |
|
|
this.confirmationOverlay = overlay; |
|
|
|
|
|
|
|
|
overlay.querySelector('.cancel').addEventListener('click', () => { |
|
|
this.hideConfirmation(); |
|
|
}); |
|
|
|
|
|
overlay.querySelector('.confirm').addEventListener('click', () => { |
|
|
if (this.confirmationCallback) { |
|
|
this.confirmationCallback(); |
|
|
this.hideConfirmation(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
overlay.addEventListener('click', (e) => { |
|
|
if (e.target === overlay) { |
|
|
this.hideConfirmation(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
showConfirmation(message, callback) { |
|
|
this.confirmationOverlay.querySelector('.confirmation-message').textContent = message; |
|
|
this.confirmationCallback = callback; |
|
|
this.confirmationOverlay.classList.add('show'); |
|
|
} |
|
|
|
|
|
hideConfirmation() { |
|
|
this.confirmationOverlay.classList.remove('show'); |
|
|
this.confirmationCallback = null; |
|
|
} |
|
|
|
|
|
createScrollIndicator() { |
|
|
const indicator = document.createElement('div'); |
|
|
indicator.className = 'scroll-indicator'; |
|
|
indicator.innerHTML = 'β'; |
|
|
|
|
|
document.body.appendChild(indicator); |
|
|
this.scrollIndicator = indicator; |
|
|
} |
|
|
|
|
|
showScrollIndicator() { |
|
|
this.scrollIndicator.classList.add('show'); |
|
|
} |
|
|
|
|
|
hideScrollIndicator() { |
|
|
this.scrollIndicator.classList.remove('show'); |
|
|
} |
|
|
|
|
|
renderEmails() { |
|
|
this.emailList.innerHTML = ''; |
|
|
this.emailElements = []; |
|
|
|
|
|
emails.forEach(email => { |
|
|
const emailElement = document.createElement('div'); |
|
|
emailElement.className = 'email-item'; |
|
|
emailElement.dataset.id = email.id; |
|
|
|
|
|
emailElement.innerHTML = ` |
|
|
<div class="email-header"> |
|
|
<div class="email-sender">${email.sender}</div> |
|
|
<div class="email-time">${email.timestamp}</div> |
|
|
</div> |
|
|
<div class="email-subject">${email.subject}</div> |
|
|
<div class="email-snippet">${email.snippet}</div> |
|
|
`; |
|
|
|
|
|
this.emailList.appendChild(emailElement); |
|
|
this.emailElements.push({ |
|
|
id: email.id, |
|
|
element: emailElement, |
|
|
rect: null |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
this.updateEmailPositions(); |
|
|
} |
|
|
|
|
|
updateEmailPositions() { |
|
|
this.emailListRect = this.emailList.getBoundingClientRect(); |
|
|
|
|
|
this.emailElements.forEach(item => { |
|
|
const rect = item.element.getBoundingClientRect(); |
|
|
item.rect = { |
|
|
left: rect.left, |
|
|
top: rect.top, |
|
|
right: rect.right, |
|
|
bottom: rect.bottom, |
|
|
width: rect.width, |
|
|
height: rect.height |
|
|
}; |
|
|
}); |
|
|
} |
|
|
|
|
|
selectEmail(emailId) { |
|
|
|
|
|
if (this.selectedEmail) { |
|
|
const prevElement = this.emailElements.find(e => e.id === this.selectedEmail); |
|
|
if (prevElement) { |
|
|
prevElement.element.classList.remove('selected'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.selectedEmail = emailId; |
|
|
const newElement = this.emailElements.find(e => e.id === emailId); |
|
|
|
|
|
if (newElement) { |
|
|
newElement.element.classList.add('selected'); |
|
|
this.showSelectionHighlight(newElement.rect); |
|
|
return newElement.rect; |
|
|
} |
|
|
|
|
|
return null; |
|
|
} |
|
|
|
|
|
showSelectionHighlight(rect) { |
|
|
this.selectionHighlight.style.display = 'block'; |
|
|
this.selectionHighlight.style.left = `${rect.left}px`; |
|
|
this.selectionHighlight.style.top = `${rect.top}px`; |
|
|
this.selectionHighlight.style.width = `${rect.width}px`; |
|
|
this.selectionHighlight.style.height = `${rect.height}px`; |
|
|
|
|
|
|
|
|
this.selectionHighlight.style.transform = 'scale(1.02)'; |
|
|
setTimeout(() => { |
|
|
this.selectionHighlight.style.transform = 'scale(1)'; |
|
|
}, 150); |
|
|
} |
|
|
|
|
|
hideSelectionHighlight() { |
|
|
this.selectionHighlight.style.display = 'none'; |
|
|
} |
|
|
|
|
|
showActionFeedback(message, type) { |
|
|
this.actionFeedback.textContent = message; |
|
|
this.actionFeedback.className = 'action-feedback'; |
|
|
|
|
|
if (type === 'delete') { |
|
|
this.actionFeedback.classList.add('delete'); |
|
|
} else if (type === 'archive') { |
|
|
this.actionFeedback.classList.add('archive'); |
|
|
} else { |
|
|
this.actionFeedback.classList.add('summary'); |
|
|
} |
|
|
|
|
|
this.actionFeedback.classList.add('show'); |
|
|
|
|
|
setTimeout(() => { |
|
|
this.actionFeedback.classList.remove('show'); |
|
|
}, 2000); |
|
|
} |
|
|
|
|
|
clearSelection() { |
|
|
if (this.selectedEmail) { |
|
|
const element = this.emailElements.find(e => e.id === this.selectedEmail); |
|
|
if (element) { |
|
|
element.element.classList.remove('selected'); |
|
|
} |
|
|
this.selectedEmail = null; |
|
|
this.hideSelectionHighlight(); |
|
|
} |
|
|
} |
|
|
|
|
|
setupEventListeners() { |
|
|
window.addEventListener('resize', () => { |
|
|
this.updateEmailPositions(); |
|
|
if (this.selectedEmail) { |
|
|
const element = this.emailElements.find(e => e.id === this.selectedEmail); |
|
|
if (element) { |
|
|
this.showSelectionHighlight(element.rect); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
this.emailList.addEventListener('scroll', () => { |
|
|
if (this.emailList.scrollTop > 0) { |
|
|
this.showScrollIndicator(); |
|
|
} else { |
|
|
this.hideScrollIndicator(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
this.scrollIndicator?.addEventListener('click', () => { |
|
|
this.emailList.scrollTo({ |
|
|
top: 0, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
updateHandLandmarks(landmarks) { |
|
|
this.handLandmarks.innerHTML = ''; |
|
|
|
|
|
if (!landmarks || landmarks.length === 0) return; |
|
|
|
|
|
landmarks.forEach((landmark, i) => { |
|
|
const landmarkEl = document.createElement('div'); |
|
|
landmarkEl.className = 'landmark'; |
|
|
if (i === 8) { |
|
|
landmarkEl.classList.add('index-tip'); |
|
|
} |
|
|
|
|
|
landmarkEl.style.left = `${landmark.x * 100}%`; |
|
|
landmarkEl.style.top = `${landmark.y * 100}%`; |
|
|
|
|
|
this.handLandmarks.appendChild(landmarkEl); |
|
|
}); |
|
|
} |
|
|
|
|
|
updateGesturePath(points) { |
|
|
this.gesturePath.innerHTML = ''; |
|
|
|
|
|
if (!points || points.length === 0) return; |
|
|
|
|
|
points.forEach(point => { |
|
|
const pointEl = document.createElement('div'); |
|
|
pointEl.className = 'point'; |
|
|
pointEl.style.left = `${point.x * 100}%`; |
|
|
pointEl.style.top = `${point.y * 100}%`; |
|
|
this.gesturePath.appendChild(pointEl); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
provideHapticFeedback() { |
|
|
if (navigator.vibrate) { |
|
|
navigator.vibrate(50); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
provideAudioFeedback() { |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GestureDetector { |
|
|
constructor(uiManager, debugManager) { |
|
|
this.uiManager = uiManager; |
|
|
this.debugManager = debugManager; |
|
|
this.selectedEmailId = null; |
|
|
this.gestureBuffer = []; |
|
|
this.circlePoints = []; |
|
|
this.circleThreshold = 12; |
|
|
this.swipeThreshold = 35; |
|
|
this.scrollThreshold = 20; |
|
|
this.gestureCooldown = 1500; |
|
|
this.lastGestureTime = 0; |
|
|
this.camera = null; |
|
|
this.lastTimestamp = Date.now(); |
|
|
|
|
|
this.gestureStartPos = null; |
|
|
this.holdTimer = null; |
|
|
this.scrollActive = false; |
|
|
this.scrollDirection = null; |
|
|
|
|
|
this.debugManager.updateStatus('Setting up MediaPipe...'); |
|
|
this.setupMediaPipe(); |
|
|
} |
|
|
|
|
|
setupMediaPipe() { |
|
|
try { |
|
|
const hands = new Hands({ |
|
|
locateFile: (file) => { |
|
|
return `https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/${file}`; |
|
|
} |
|
|
}); |
|
|
|
|
|
hands.setOptions({ |
|
|
maxNumHands: 1, |
|
|
modelComplexity: 1, |
|
|
minDetectionConfidence: 0.7, |
|
|
minTrackingConfidence: 0.7 |
|
|
}); |
|
|
|
|
|
hands.onResults((results) => { |
|
|
this.processResults(results); |
|
|
}); |
|
|
|
|
|
const videoElement = document.getElementById('webcam'); |
|
|
|
|
|
|
|
|
if (typeof Camera === 'undefined') { |
|
|
this.debugManager.logError(new Error("Camera utils not loaded. Make sure camera_utils.js is included.")); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.camera = new Camera(videoElement, { |
|
|
onFrame: async () => { |
|
|
try { |
|
|
this.debugManager.setProcessing(); |
|
|
await hands.send({image: videoElement}); |
|
|
} catch (error) { |
|
|
this.debugManager.logError(error); |
|
|
} |
|
|
}, |
|
|
width: 320, |
|
|
height: 240 |
|
|
}); |
|
|
|
|
|
this.startCamera(); |
|
|
} catch (error) { |
|
|
this.debugManager.logError(error); |
|
|
console.error("MediaPipe setup error:", error); |
|
|
} |
|
|
} |
|
|
|
|
|
async startCamera() { |
|
|
try { |
|
|
this.debugManager.updateCameraStatus('Starting...'); |
|
|
await this.camera.start(); |
|
|
this.debugManager.updateCameraStatus('Active'); |
|
|
this.debugManager.setReady(); |
|
|
console.log("Camera initialized successfully"); |
|
|
} catch (error) { |
|
|
this.debugManager.updateCameraStatus('Error'); |
|
|
this.debugManager.logError(error); |
|
|
|
|
|
|
|
|
if (error.name === 'NotAllowedError') { |
|
|
alert("Camera access denied. Please allow camera access in your browser settings."); |
|
|
} else if (error.name === 'NotFoundError') { |
|
|
alert("No camera found. Please connect a camera device."); |
|
|
} else { |
|
|
alert("Failed to start camera: " + error.message); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
processResults(results) { |
|
|
try { |
|
|
|
|
|
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { |
|
|
this.uiManager.updateHandLandmarks(results.multiHandLandmarks[0]); |
|
|
|
|
|
|
|
|
if (this.gestureBuffer.length > 0) { |
|
|
this.uiManager.updateGesturePath(this.gestureBuffer); |
|
|
} |
|
|
|
|
|
this.detectGesture(results.multiHandLandmarks[0]); |
|
|
} else { |
|
|
this.uiManager.clearSelection(); |
|
|
this.gestureBuffer = []; |
|
|
this.circlePoints = []; |
|
|
this.debugManager.updateGestureType('None'); |
|
|
this.debugManager.updateBufferCount(0); |
|
|
this.debugManager.updateCircleCount(0); |
|
|
} |
|
|
} catch (error) { |
|
|
this.debugManager.logError(error); |
|
|
} |
|
|
} |
|
|
|
|
|
detectGesture(landmarks) { |
|
|
try { |
|
|
const indexTip = landmarks[8]; |
|
|
const middleTip = landmarks[12]; |
|
|
const wrist = landmarks[0]; |
|
|
|
|
|
|
|
|
const screenX = indexTip.x * window.innerWidth; |
|
|
const screenY = indexTip.y * window.innerHeight; |
|
|
|
|
|
|
|
|
const emailListRect = this.uiManager.emailList.getBoundingClientRect(); |
|
|
const relativeX = screenX - emailListRect.left; |
|
|
const relativeY = screenY - emailListRect.top; |
|
|
|
|
|
|
|
|
if (indexTip.y < middleTip.y && wrist.y > indexTip.y) { |
|
|
this.checkEmailSelection(relativeX, relativeY); |
|
|
} else { |
|
|
this.uiManager.clearSelection(); |
|
|
this.gestureBuffer = []; |
|
|
this.circlePoints = []; |
|
|
this.debugManager.updateGestureType('None'); |
|
|
this.debugManager.updateBufferCount(0); |
|
|
this.debugManager.updateCircleCount(0); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.selectedEmailId === null) { |
|
|
this.processScrolling(landmarks); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const palmCenterX = (wrist.x + landmarks[9].x) / 2; |
|
|
const palmCenterY = (wrist.y + landmarks[9].y) / 2; |
|
|
|
|
|
|
|
|
const timestamp = Date.now(); |
|
|
const dt = timestamp - this.lastTimestamp; |
|
|
this.lastTimestamp = timestamp; |
|
|
|
|
|
this.gestureBuffer.push({x: palmCenterX, y: palmCenterY, timestamp}); |
|
|
|
|
|
|
|
|
if (this.gestureBuffer.length > 3) { |
|
|
const prev = this.gestureBuffer[this.gestureBuffer.length - 2]; |
|
|
const current = this.gestureBuffer[this.gestureBuffer.length - 1]; |
|
|
|
|
|
|
|
|
const dx = (current.x - prev.x) * window.innerWidth; |
|
|
const dy = (current.y - prev.y) * window.innerHeight; |
|
|
const velocityX = dx / dt; |
|
|
const velocityY = dy / dt; |
|
|
const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY); |
|
|
|
|
|
|
|
|
if (Math.abs(dx) > this.swipeThreshold && |
|
|
Math.abs(dx) > Math.abs(dy) * 1.5 && |
|
|
speed > 0.3) { |
|
|
|
|
|
if (Date.now() - this.lastGestureTime > this.gestureCooldown) { |
|
|
this.lastGestureTime = Date.now(); |
|
|
|
|
|
if (dx > 0) { |
|
|
this.debugManager.updateGestureType('Swipe Right'); |
|
|
this.handleGesture('swipe_right'); |
|
|
} else { |
|
|
this.debugManager.updateGestureType('Swipe Left'); |
|
|
this.handleGesture('swipe_left'); |
|
|
} |
|
|
|
|
|
this.gestureBuffer = []; |
|
|
this.debugManager.updateBufferCount(0); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.circlePoints.push({x: palmCenterX, y: palmCenterY}); |
|
|
this.debugManager.updateCircleCount(this.circlePoints.length); |
|
|
|
|
|
if (this.circlePoints.length > this.circleThreshold) { |
|
|
const { center, radius, circularity } = this.calculateCircleMetrics(); |
|
|
|
|
|
|
|
|
if (circularity > 0.75 && radius > 30 && |
|
|
Date.now() - this.lastGestureTime > this.gestureCooldown) { |
|
|
|
|
|
this.lastGestureTime = Date.now(); |
|
|
this.debugManager.updateGestureType('Circle'); |
|
|
this.handleGesture('circle'); |
|
|
this.circlePoints = []; |
|
|
this.debugManager.updateCircleCount(0); |
|
|
} |
|
|
} |
|
|
|
|
|
this.debugManager.updateBufferCount(this.gestureBuffer.length); |
|
|
} catch (error) { |
|
|
this.debugManager.logError(error); |
|
|
} |
|
|
} |
|
|
|
|
|
processScrolling(landmarks) { |
|
|
const wrist = landmarks[0]; |
|
|
const indexTip = landmarks[8]; |
|
|
|
|
|
|
|
|
if (this.selectedEmailId !== null) return; |
|
|
|
|
|
|
|
|
const screenX = indexTip.x * window.innerWidth; |
|
|
const screenY = indexTip.y * window.innerHeight; |
|
|
|
|
|
|
|
|
const emailListRect = this.uiManager.emailList.getBoundingClientRect(); |
|
|
const relativeX = screenX - emailListRect.left; |
|
|
const relativeY = screenY - emailListRect.top; |
|
|
|
|
|
|
|
|
if (relativeX < 0 || relativeX > emailListRect.width || |
|
|
relativeY < 0 || relativeY > emailListRect.height) { |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (this.gestureBuffer.length > 1) { |
|
|
const lastPoint = this.gestureBuffer[this.gestureBuffer.length - 2]; |
|
|
const currentPoint = this.gestureBuffer[this.gestureBuffer.length - 1]; |
|
|
|
|
|
const dx = (currentPoint.x - lastPoint.x) * window.innerWidth; |
|
|
const dy = (currentPoint.y - lastPoint.y) * window.innerHeight; |
|
|
const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
const speed = distance / (this.lastTimestamp - lastPoint.timestamp); |
|
|
|
|
|
|
|
|
if (Math.abs(dy) > this.scrollThreshold && Math.abs(dy) > Math.abs(dx) * 1.5 && speed > 0.5) { |
|
|
this.scrollActive = true; |
|
|
this.scrollDirection = dy > 0 ? 'down' : 'up'; |
|
|
|
|
|
|
|
|
this.uiManager.showScrollIndicator(); |
|
|
|
|
|
|
|
|
const scrollAmount = Math.min(150, Math.abs(dy) * 2); |
|
|
this.uiManager.emailList.scrollBy({ |
|
|
top: this.scrollDirection === 'up' ? -scrollAmount : scrollAmount, |
|
|
behavior: 'smooth' |
|
|
}); |
|
|
|
|
|
|
|
|
this.gestureBuffer = []; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
calculateCircleMetrics() { |
|
|
if (this.circlePoints.length < 3) { |
|
|
return { center: { x: 0, y: 0 }, radius: 0, circularity: 0 }; |
|
|
} |
|
|
|
|
|
|
|
|
let centerX = 0; |
|
|
let centerY = 0; |
|
|
for (const point of this.circlePoints) { |
|
|
centerX += point.x; |
|
|
centerY += point.y; |
|
|
} |
|
|
centerX /= this.circlePoints.length; |
|
|
centerY /= this.circlePoints.length; |
|
|
|
|
|
|
|
|
let totalRadius = 0; |
|
|
let radiusVariance = 0; |
|
|
const radii = []; |
|
|
|
|
|
for (const point of this.circlePoints) { |
|
|
const dx = point.x - centerX; |
|
|
const dy = point.y - centerY; |
|
|
const radius = Math.sqrt(dx * dx + dy * dy); |
|
|
radii.push(radius); |
|
|
totalRadius += radius; |
|
|
} |
|
|
|
|
|
const avgRadius = totalRadius / this.circlePoints.length; |
|
|
|
|
|
|
|
|
for (const radius of radii) { |
|
|
radiusVariance += Math.pow(radius - avgRadius, 2); |
|
|
} |
|
|
radiusVariance /= this.circlePoints.length; |
|
|
|
|
|
|
|
|
const circularity = radiusVariance < 0.0001 ? 1 : Math.max(0, 1 - radiusVariance / (avgRadius * avgRadius)); |
|
|
|
|
|
return { |
|
|
center: { x: centerX, y: centerY }, |
|
|
radius: avgRadius, |
|
|
circularity: circularity |
|
|
}; |
|
|
} |
|
|
|
|
|
checkEmailSelection(x, y) { |
|
|
try { |
|
|
|
|
|
for (let i = this.uiManager.emailElements.length - 1; i >= 0; i--) { |
|
|
const email = this.uiManager.emailElements[i]; |
|
|
if (email.rect && |
|
|
x >= 0 && x <= email.rect.width && |
|
|
y >= 0 && y <= email.rect.height) { |
|
|
|
|
|
|
|
|
if (this.selectedEmailId !== email.id) { |
|
|
this.selectedEmailId = email.id; |
|
|
this.uiManager.selectEmail(email.id); |
|
|
this.debugManager.updateSelectedEmail(email.id); |
|
|
|
|
|
|
|
|
this.startHoldTimer(); |
|
|
} |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!this.uiManager.emailElements.some(email => |
|
|
email.rect && |
|
|
x >= 0 && x <= email.rect.width && |
|
|
y >= 0 && y <= email.rect.height)) { |
|
|
|
|
|
this.uiManager.clearSelection(); |
|
|
this.selectedEmailId = null; |
|
|
this.debugManager.updateSelectedEmail(null); |
|
|
this.clearHoldTimer(); |
|
|
} |
|
|
} catch (error) { |
|
|
this.debugManager.logError(error); |
|
|
} |
|
|
} |
|
|
|
|
|
startHoldTimer() { |
|
|
this.clearHoldTimer(); |
|
|
|
|
|
|
|
|
this.holdTimer = setTimeout(() => { |
|
|
if (this.selectedEmailId) { |
|
|
const email = emails.find(e => e.id === this.selectedEmailId); |
|
|
this.uiManager.showConfirmation( |
|
|
`Long press detected on "${email.subject}".\n\nConfirm action?`, |
|
|
() => { |
|
|
|
|
|
this.handleGesture('confirm'); |
|
|
}); |
|
|
} |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
clearHoldTimer() { |
|
|
if (this.holdTimer) { |
|
|
clearTimeout(this.holdTimer); |
|
|
this.holdTimer = null; |
|
|
} |
|
|
} |
|
|
|
|
|
handleGesture(gesture) { |
|
|
try { |
|
|
if (!this.selectedEmailId) return; |
|
|
|
|
|
const email = emails.find(e => e.id === this.selectedEmailId); |
|
|
if (!email) return; |
|
|
|
|
|
switch (gesture) { |
|
|
case 'swipe_left': |
|
|
|
|
|
this.uiManager.showConfirmation( |
|
|
`Delete "${email.subject}"?`, |
|
|
() => { |
|
|
this.uiManager.showActionFeedback(`ποΈ Deleted: ${email.subject}`, 'delete'); |
|
|
const index = emails.findIndex(e => e.id === this.selectedEmailId); |
|
|
if (index !== -1) emails.splice(index, 1); |
|
|
this.uiManager.renderEmails(); |
|
|
this.selectedEmailId = null; |
|
|
this.debugManager.updateSelectedEmail(null); |
|
|
this.uiManager.provideHapticFeedback(); |
|
|
} |
|
|
); |
|
|
break; |
|
|
|
|
|
case 'swipe_right': |
|
|
this.uiManager.showConfirmation( |
|
|
`Archive "${email.subject}"?`, |
|
|
() => { |
|
|
this.uiManager.showActionFeedback(`β
Archived: ${email.subject}`, 'archive'); |
|
|
const archiveIndex = emails.findIndex(e => e.id === this.selectedEmailId); |
|
|
if (archiveIndex !== -1) emails.splice(archiveIndex, 1); |
|
|
this.uiManager.renderEmails(); |
|
|
this.selectedEmailId = null; |
|
|
this.debugManager.updateSelectedEmail(null); |
|
|
this.uiManager.provideHapticFeedback(); |
|
|
} |
|
|
); |
|
|
break; |
|
|
|
|
|
case 'circle': |
|
|
const summary = `This email discusses ${email.subject.toLowerCase()}.`; |
|
|
this.uiManager.showActionFeedback(`π Summary: ${summary}`, 'summary'); |
|
|
this.uiManager.provideHapticFeedback(); |
|
|
break; |
|
|
|
|
|
case 'confirm': |
|
|
|
|
|
this.uiManager.showActionFeedback(`π Selected: ${email.subject}`, 'summary'); |
|
|
break; |
|
|
} |
|
|
} catch (error) { |
|
|
this.debugManager.logError(error); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
const debugManager = new DebugManager(); |
|
|
debugManager.updateStatus('Initializing UI...'); |
|
|
|
|
|
|
|
|
const uiManager = new UIManager(); |
|
|
debugManager.setReady(); |
|
|
|
|
|
|
|
|
const videoElement = document.getElementById('webcam'); |
|
|
|
|
|
navigator.mediaDevices.getUserMedia({ video: true }) |
|
|
.then(stream => { |
|
|
videoElement.srcObject = stream; |
|
|
debugManager.updateCameraStatus('Initializing...'); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
debugManager.updateStatus('Setting up gesture detection...'); |
|
|
try { |
|
|
new GestureDetector(uiManager, debugManager); |
|
|
} catch (error) { |
|
|
debugManager.logError(error); |
|
|
} |
|
|
}, 1500); |
|
|
}) |
|
|
.catch(err => { |
|
|
debugManager.updateCameraStatus('Denied'); |
|
|
debugManager.logError(err); |
|
|
console.error("Error accessing camera:", err); |
|
|
|
|
|
|
|
|
if (err.name === 'NotAllowedError') { |
|
|
alert("Camera access denied. Please allow camera access in your browser settings."); |
|
|
} else if (err.name === 'NotFoundError') { |
|
|
alert("No camera found. Please connect a camera device."); |
|
|
} else { |
|
|
alert("Camera error: " + err.message); |
|
|
} |
|
|
}); |
|
|
}); |