// ---------------------------- // Sample Email Data // ---------------------------- 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" } ]; // ---------------------------- // Debug Utilities // ---------------------------- 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'); // Get debug toggle button this.toggleButton = document.getElementById('debugToggle'); this.debugPanel = document.getElementById('debugPanel'); // Add click event listener 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 ? '...' : ''); // Update status indicator to red 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'); } } } // ---------------------------- // UI Management // ---------------------------- 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(); // Create confirmation overlay this.createConfirmationOverlay(); // Create scroll indicator this.createScrollIndicator(); } createConfirmationOverlay() { const overlay = document.createElement('div'); overlay.className = 'confirmation-overlay'; overlay.innerHTML = `
⚠️

Confirm Action

Are you sure you want to perform this action?

`; document.body.appendChild(overlay); this.confirmationOverlay = overlay; // Add event listeners overlay.querySelector('.cancel').addEventListener('click', () => { this.hideConfirmation(); }); overlay.querySelector('.confirm').addEventListener('click', () => { if (this.confirmationCallback) { this.confirmationCallback(); this.hideConfirmation(); } }); // Click outside to cancel 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 = `
${email.sender}
${email.timestamp}
${email.subject}
${email.snippet}
`; this.emailList.appendChild(emailElement); this.emailElements.push({ id: email.id, element: emailElement, rect: null }); }); // Update email positions 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) { // Remove previous selection if (this.selectedEmail) { const prevElement = this.emailElements.find(e => e.id === this.selectedEmail); if (prevElement) { prevElement.element.classList.remove('selected'); } } // Set new selection 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`; // Enhanced feedback - scale animation 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); } } }); // Add scroll event for indicator this.emailList.addEventListener('scroll', () => { if (this.emailList.scrollTop > 0) { this.showScrollIndicator(); } else { this.hideScrollIndicator(); } }); // Click handler for scroll indicator this.scrollIndicator?.addEventListener('click', () => { this.emailList.scrollTo({ top: 0, behavior: 'smooth' }); }); } // For gesture visualization 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) { // Index finger tip 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); }); } // Haptic feedback simulation (if supported) provideHapticFeedback() { if (navigator.vibrate) { navigator.vibrate(50); } } // Audio feedback simulation (if needed) provideAudioFeedback() { // Could implement audio feedback here // const sound = new Audio('click-sound.mp3'); // sound.play(); } } // ---------------------------- // Gesture Detection // ---------------------------- 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/hands@0.4/${file}`; } }); hands.setOptions({ maxNumHands: 1, modelComplexity: 1, minDetectionConfidence: 0.7, minTrackingConfidence: 0.7 }); hands.onResults((results) => { this.processResults(results); }); const videoElement = document.getElementById('webcam'); // Check if Camera is available 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); // Try to get more specific error information 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 { // Update hand landmarks visualization if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { this.uiManager.updateHandLandmarks(results.multiHandLandmarks[0]); // Update gesture path for debugging 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]; // Calculate screen coordinates const screenX = indexTip.x * window.innerWidth; const screenY = indexTip.y * window.innerHeight; // Calculate relative position within email list const emailListRect = this.uiManager.emailList.getBoundingClientRect(); const relativeX = screenX - emailListRect.left; const relativeY = screenY - emailListRect.top; // Pointing detection (index finger higher than middle) 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); } // Only process gestures if an email is selected if (this.selectedEmailId === null) { this.processScrolling(landmarks); return; } // Get palm center for gesture detection const palmCenterX = (wrist.x + landmarks[9].x) / 2; const palmCenterY = (wrist.y + landmarks[9].y) / 2; // Add to gesture buffer const timestamp = Date.now(); const dt = timestamp - this.lastTimestamp; this.lastTimestamp = timestamp; this.gestureBuffer.push({x: palmCenterX, y: palmCenterY, timestamp}); // Check for swipe if (this.gestureBuffer.length > 3) { const prev = this.gestureBuffer[this.gestureBuffer.length - 2]; const current = this.gestureBuffer[this.gestureBuffer.length - 1]; // Calculate distance and velocity 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); // Check if it's a horizontal swipe (using Fitts' Law principles) 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); } } } // Check for circle 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(); // Improved circle detection with circularity check 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]; // Only process scrolling when no email is selected if (this.selectedEmailId !== null) return; // Calculate screen coordinates const screenX = indexTip.x * window.innerWidth; const screenY = indexTip.y * window.innerHeight; // Calculate relative position within email list const emailListRect = this.uiManager.emailList.getBoundingClientRect(); const relativeX = screenX - emailListRect.left; const relativeY = screenY - emailListRect.top; // Only scroll if finger is in the email list area if (relativeX < 0 || relativeX > emailListRect.width || relativeY < 0 || relativeY > emailListRect.height) { return; } // Calculate velocity for scrolling 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); // Only scroll if movement is primarily vertical 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'; // Show scroll indicator this.uiManager.showScrollIndicator(); // Perform scroll const scrollAmount = Math.min(150, Math.abs(dy) * 2); this.uiManager.emailList.scrollBy({ top: this.scrollDirection === 'up' ? -scrollAmount : scrollAmount, behavior: 'smooth' }); // Reset buffer to prevent multiple scrolls from same movement this.gestureBuffer = []; } } } calculateCircleMetrics() { if (this.circlePoints.length < 3) { return { center: { x: 0, y: 0 }, radius: 0, circularity: 0 }; } // Find centroid 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; // Calculate radius and circularity 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; // Calculate variance to determine circularity for (const radius of radii) { radiusVariance += Math.pow(radius - avgRadius, 2); } radiusVariance /= this.circlePoints.length; // Calculate circularity (1 = perfect circle, 0 = not circular) 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 { // Find the email under the finger 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) { // Only select if it's a different email if (this.selectedEmailId !== email.id) { this.selectedEmailId = email.id; this.uiManager.selectEmail(email.id); this.debugManager.updateSelectedEmail(email.id); // Start hold timer for confirmation this.startHoldTimer(); } return; } } // If no email is selected, clear selection 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(); // Start a new timer for confirmation 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?`, () => { // Action confirmed - perform gesture 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': // Show confirmation for destructive action (Fitts' Law principle) 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 is triggered when the user holds an email this.uiManager.showActionFeedback(`🔍 Selected: ${email.subject}`, 'summary'); break; } } catch (error) { this.debugManager.logError(error); } } } // ---------------------------- // Initialize App // ---------------------------- document.addEventListener('DOMContentLoaded', () => { // Initialize debug manager first const debugManager = new DebugManager(); debugManager.updateStatus('Initializing UI...'); // Initialize UI const uiManager = new UIManager(); debugManager.setReady(); // Request camera access const videoElement = document.getElementById('webcam'); navigator.mediaDevices.getUserMedia({ video: true }) .then(stream => { videoElement.srcObject = stream; debugManager.updateCameraStatus('Initializing...'); // Initialize gesture detection after a short delay to ensure UI is ready 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); // More specific error handling 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); } }); });