// ----------------------------
// 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.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);
}
});
});