Sa-m commited on
Commit
bc447fd
Β·
verified Β·
1 Parent(s): ae45c4f

Update assets/js/script.js

Browse files
Files changed (1) hide show
  1. assets/js/script.js +983 -174
assets/js/script.js CHANGED
@@ -1,182 +1,991 @@
1
- // === CONFIG ===
2
- const swipeThreshold = 60; // pixels
3
- const circleThreshold = 20; // points stored
4
- const gestureCooldown = 1000; // ms
5
- const smoothingWindow = 5; // average over last N points
6
-
7
- let emailList = [
8
- { id: 1, sender: "Alex", subject: "Project Update", snippet: "Let's meet tomorrow." },
9
- { id: 2, sender: "Mira", subject: "Dinner?", snippet: "Want to try sushi tonight?" },
10
- { id: 3, sender: "Boss", subject: "Deadline moved", snippet: "Submit by Friday." },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  ];
12
 
13
- let selectedEmailId = null;
14
- let gestureInProgress = false;
15
- let lastGestureTime = 0;
16
- let points = [];
17
- let smoothedPoints = [];
18
- let lastX = null, lastY = null;
19
-
20
- const video = document.getElementById("webcam");
21
- const canvas = document.getElementById("canvas");
22
- const ctx = canvas.getContext("2d");
23
- const feedback = document.getElementById("action-feedback");
24
- const debug = document.getElementById("debug");
25
-
26
- // === UI Rendering ===
27
- function renderEmails() {
28
- const list = document.getElementById("email-list");
29
- list.innerHTML = "";
30
- emailList.forEach(e => {
31
- const div = document.createElement("div");
32
- div.className = "email" + (selectedEmailId === e.id ? " selected" : "");
33
- div.innerHTML = `<b>${e.sender}</b><br><span>${e.subject}</span><br><small>${e.snippet}</small>`;
34
- list.appendChild(div);
35
- });
36
- }
37
-
38
- // === Feedback Utility ===
39
- function showFeedback(text, color = "#4CAF50") {
40
- feedback.style.background = color;
41
- feedback.textContent = text;
42
- feedback.style.opacity = 1;
43
- setTimeout(() => (feedback.style.opacity = 0), 1500);
44
- }
45
-
46
- // === Gesture Handling ===
47
- function handleGesture(type) {
48
- const now = Date.now();
49
- if (now - lastGestureTime < gestureCooldown) return;
50
- lastGestureTime = now;
51
- gestureInProgress = true;
52
-
53
- switch (type) {
54
- case "swipeLeft":
55
- if (selectedEmailId != null) {
56
- emailList = emailList.filter(e => e.id !== selectedEmailId);
57
- selectedEmailId = null;
58
- renderEmails();
59
- showFeedback("Deleted ❌", "#e74c3c");
60
- }
61
- break;
62
- case "swipeRight":
63
- if (selectedEmailId != null) {
64
- showFeedback("Archived βœ…", "#4CAF50");
65
- selectedEmailId = null;
66
- renderEmails();
67
- }
68
- break;
69
- case "circle":
70
- showFeedback("Refreshed πŸ”„", "#3498db");
71
- renderEmails();
72
- break;
73
- case "point":
74
- selectClosestEmail(lastX, lastY);
75
- break;
76
- case "palm":
77
- selectedEmailId = null;
78
- renderEmails();
79
- showFeedback("Deselected βœ‹", "#f1c40f");
80
- break;
81
- }
82
- gestureInProgress = false;
83
- }
84
-
85
- function selectClosestEmail(x, y) {
86
- const rects = document.querySelectorAll(".email");
87
- let minDist = Infinity, chosen = null;
88
- rects.forEach((r, i) => {
89
- const b = r.getBoundingClientRect();
90
- const cx = (b.left + b.right) / 2;
91
- const cy = (b.top + b.bottom) / 2;
92
- const dist = Math.hypot(cx - x, cy - y);
93
- if (dist < minDist) {
94
- minDist = dist;
95
- chosen = emailList[i].id;
96
- }
97
- });
98
- if (chosen != null) {
99
- selectedEmailId = chosen;
100
- renderEmails();
101
- showFeedback("Selected πŸ‘‰", "#9b59b6");
102
- }
103
  }
104
 
105
- // === Gesture Detection Logic ===
106
- function processLandmarks(landmarks) {
107
- const indexTip = landmarks[8];
108
- const wrist = landmarks[0];
109
- const palmOpen = Math.abs(landmarks[12].y - landmarks[0].y) < 0.1;
110
-
111
- const x = indexTip.x * canvas.width;
112
- const y = indexTip.y * canvas.height;
113
-
114
- lastX = x; lastY = y;
115
- points.push({ x, y });
116
- if (points.length > circleThreshold) points.shift();
117
-
118
- // Smooth coordinates
119
- smoothedPoints.push({ x, y });
120
- if (smoothedPoints.length > smoothingWindow) smoothedPoints.shift();
121
- const avg = smoothedPoints.reduce((a, p) => ({ x: a.x + p.x, y: a.y + p.y }), { x: 0, y: 0 });
122
- const smoothedX = avg.x / smoothedPoints.length;
123
- const smoothedY = avg.y / smoothedPoints.length;
124
-
125
- // Swipe detection
126
- if (Math.abs(x - wrist.x * canvas.width) > swipeThreshold) {
127
- if (x < wrist.x * canvas.width) handleGesture("swipeLeft");
128
- else handleGesture("swipeRight");
129
- }
130
-
131
- // Circle detection
132
- if (points.length === circleThreshold) {
133
- const start = points[0], end = points[points.length - 1];
134
- const dist = Math.hypot(end.x - start.x, end.y - start.y);
135
- if (dist < 40) handleGesture("circle");
136
- }
137
-
138
- // Palm open detection
139
- if (palmOpen) handleGesture("palm");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
 
142
- // === MediaPipe Setup ===
143
- async function startHands() {
144
- const hands = new Hands({
145
- locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`,
146
- });
147
- hands.setOptions({
148
- maxNumHands: 1,
149
- modelComplexity: 1,
150
- minDetectionConfidence: 0.7,
151
- minTrackingConfidence: 0.6,
152
- });
153
-
154
- hands.onResults((results) => {
155
- ctx.clearRect(0, 0, canvas.width, canvas.height);
156
- if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
157
- const landmarks = results.multiHandLandmarks[0];
158
- drawConnectors(ctx, landmarks, HAND_CONNECTIONS, { color: "#00FF00", lineWidth: 3 });
159
- drawLandmarks(ctx, landmarks, { color: "#FF0000", lineWidth: 2 });
160
- processLandmarks(landmarks);
161
- debug.textContent = `Gesture Points: ${points.length}`;
162
- }
163
- });
164
-
165
- const stream = await navigator.mediaDevices.getUserMedia({ video: true });
166
- video.srcObject = stream;
167
- const camera = new Camera(video, {
168
- onFrame: async () => {
169
- await hands.send({ image: video });
170
- },
171
- width: 640,
172
- height: 480,
173
- });
174
- camera.start();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  }
176
 
177
- window.onload = () => {
178
- canvas.width = window.innerWidth;
179
- canvas.height = window.innerHeight;
180
- renderEmails();
181
- startHands();
182
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ----------------------------
2
+ // Sample Email Data
3
+ // ----------------------------
4
+ const emails = [
5
+ {
6
+ id: 1,
7
+ sender: "Interview Kicks",
8
+ subject: "Can I Become an ML Engineer? Why Not, We Ask!",
9
+ snippet: "It's time to banish that doubt forever. Attend our masterclass to unlock your potential.",
10
+ timestamp: "1:31 PM"
11
+ },
12
+ {
13
+ id: 2,
14
+ sender: "Career Brew",
15
+ subject: "7th Sep Jobs, 87 Hottest Jobs and Early Career Jobs - Do Not Miss",
16
+ snippet: "Top companies are hiring right now β€” don't miss your shot at these exclusive opportunities!",
17
+ timestamp: "12:35 PM"
18
+ },
19
+ {
20
+ id: 3,
21
+ sender: "Professor Smith",
22
+ subject: "Research Collaboration Proposal",
23
+ snippet: "I've reviewed your proposal draft. Let's schedule a call next week to discuss next steps.",
24
+ timestamp: "10:22 AM"
25
+ },
26
+ {
27
+ id: 4,
28
+ sender: "Friend",
29
+ subject: "Weekend Plans",
30
+ snippet: "Are we still on for the hiking trip this weekend? Let me know so I can book the gear.",
31
+ timestamp: "Yesterday"
32
+ },
33
+ {
34
+ id: 5,
35
+ sender: "Bank Support",
36
+ subject: "Monthly Statement",
37
+ snippet: "Your statement is ready. Minimum due: $120. Avoid penalties by paying before the 15th.",
38
+ timestamp: "Sep 5"
39
+ },
40
+ {
41
+ id: 6,
42
+ sender: "Netflix",
43
+ subject: "New Releases This Week!",
44
+ snippet: "Check out the hottest new shows and movies added to your watchlist this week.",
45
+ timestamp: "Sep 4"
46
+ },
47
+ {
48
+ id: 7,
49
+ sender: "Amazon",
50
+ subject: "Your Order #12345 Has Shipped",
51
+ snippet: "Your package is on the way! Track your delivery using the link below.",
52
+ timestamp: "Sep 3"
53
+ },
54
+ {
55
+ id: 8,
56
+ sender: "LinkedIn",
57
+ subject: "You have 5 new notifications",
58
+ snippet: "See who viewed your profile and new job recommendations.",
59
+ timestamp: "Sep 2"
60
+ },
61
+ {
62
+ id: 9,
63
+ sender: "Microsoft",
64
+ subject: "Your Office 365 Subscription Renewal",
65
+ snippet: "Your Office 365 subscription will expire in 7 days. Renew now to avoid service interruption.",
66
+ timestamp: "Sep 1"
67
+ },
68
+ {
69
+ id: 10,
70
+ sender: "TripAdvisor",
71
+ subject: "Your upcoming trip to Paris",
72
+ snippet: "Don't forget to check-in for your flight tomorrow. Here's your itinerary and hotel information.",
73
+ timestamp: "Aug 30"
74
+ },
75
+ {
76
+ id: 11,
77
+ sender: "Travel Agency",
78
+ subject: "Your Paris Hotel Confirmation",
79
+ snippet: "Your reservation at the Eiffel Tower View Hotel is confirmed for September 10-15.",
80
+ timestamp: "Aug 29"
81
+ },
82
+ {
83
+ id: 12,
84
+ sender: "University Alumni",
85
+ subject: "Alumni Networking Event Next Week",
86
+ snippet: "Join us for a special networking event with industry leaders. Register now to secure your spot.",
87
+ timestamp: "Aug 28"
88
+ },
89
+ {
90
+ id: 13,
91
+ sender: "Gym Membership",
92
+ subject: "Your Membership is Expiring Soon",
93
+ snippet: "Your gym membership will expire in 5 days. Renew now to continue enjoying our facilities.",
94
+ timestamp: "Aug 27"
95
+ },
96
+ {
97
+ id: 14,
98
+ sender: "Online Course",
99
+ subject: "Your Course Certificate is Ready",
100
+ snippet: "Congratulations! You've completed the Data Science Fundamentals course. Download your certificate now.",
101
+ timestamp: "Aug 26"
102
+ },
103
+ {
104
+ id: 15,
105
+ sender: "Tech Conference",
106
+ subject: "Your Conference Ticket is Confirmed",
107
+ snippet: "Your ticket for the AI Summit is confirmed. Check your email for event details and schedule.",
108
+ timestamp: "Aug 25"
109
+ }
110
  ];
111
 
112
+ // ----------------------------
113
+ // Debug Utilities
114
+ // ----------------------------
115
+ class DebugManager {
116
+ constructor() {
117
+ this.statusEl = document.getElementById('debugStatus');
118
+ this.selectedEmailEl = document.getElementById('debugSelectedEmail');
119
+ this.gestureTypeEl = document.getElementById('debugGestureType');
120
+ this.bufferCountEl = document.getElementById('debugBufferCount');
121
+ this.circleCountEl = document.getElementById('debugCircleCount');
122
+ this.cameraStatusEl = document.getElementById('debugCameraStatus');
123
+ this.lastErrorEl = document.getElementById('debugLastError');
124
+ this.statusIndicator = document.getElementById('statusIndicator');
125
+
126
+ // Get debug toggle button
127
+ this.toggleButton = document.getElementById('debugToggle');
128
+ this.debugPanel = document.getElementById('debugPanel');
129
+
130
+ // Add click event listener
131
+ this.toggleButton.addEventListener('click', () => {
132
+ this.toggleDebugPanel();
133
+ });
134
+ }
135
+
136
+ updateStatus(status) {
137
+ this.statusEl.textContent = status;
138
+ }
139
+
140
+ updateSelectedEmail(emailId) {
141
+ if (emailId) {
142
+ const email = emails.find(e => e.id === emailId);
143
+ this.selectedEmailEl.textContent = email ? email.subject.substring(0, 20) + '...' : 'Unknown';
144
+ } else {
145
+ this.selectedEmailEl.textContent = 'None';
146
+ }
147
+ }
148
+
149
+ updateGestureType(gestureType) {
150
+ this.gestureTypeEl.textContent = gestureType;
151
+ }
152
+
153
+ updateBufferCount(count) {
154
+ this.bufferCountEl.textContent = `${count} points`;
155
+ }
156
+
157
+ updateCircleCount(count) {
158
+ this.circleCountEl.textContent = `${count} points`;
159
+ }
160
+
161
+ updateCameraStatus(status) {
162
+ this.cameraStatusEl.textContent = status;
163
+ }
164
+
165
+ logError(error) {
166
+ console.error("Gesture Detection Error:", error);
167
+ this.lastErrorEl.textContent = error.message.substring(0, 50) + (error.message.length > 50 ? '...' : '');
168
+
169
+ // Update status indicator to red
170
+ this.statusIndicator.className = 'status-indicator error';
171
+ }
172
+
173
+ setReady() {
174
+ this.updateStatus('Ready');
175
+ this.statusIndicator.className = 'status-indicator ready';
176
+ }
177
+
178
+ setProcessing() {
179
+ this.updateStatus('Processing...');
180
+ this.statusIndicator.className = 'status-indicator processing';
181
+ }
182
+
183
+ toggleDebugPanel() {
184
+ if (this.debugPanel.classList.contains('visible')) {
185
+ this.debugPanel.classList.remove('visible');
186
+ } else {
187
+ this.debugPanel.classList.add('visible');
188
+ }
189
+ }
 
 
 
 
 
 
 
 
 
 
 
 
190
  }
191
 
192
+ // ----------------------------
193
+ // UI Management
194
+ // ----------------------------
195
+ class UIManager {
196
+ constructor() {
197
+ this.emailList = document.getElementById('emailList');
198
+ this.actionFeedback = document.getElementById('actionFeedback');
199
+ this.selectionHighlight = document.getElementById('selectionHighlight');
200
+ this.handLandmarks = document.getElementById('handLandmarks');
201
+ this.gesturePath = document.getElementById('gesturePath');
202
+ this.scrollIndicator = document.getElementById('scrollIndicator');
203
+
204
+ this.selectedEmail = null;
205
+ this.emailElements = [];
206
+ this.scrollOffset = 0;
207
+ this.maxScroll = 0;
208
+ this.scrollSpeed = 0;
209
+ this.isScrolling = false;
210
+
211
+ this.renderEmails();
212
+ this.setupEventListeners();
213
+
214
+ // Create confirmation overlay
215
+ this.createConfirmationOverlay();
216
+
217
+ // Setup scroll indicator
218
+ this.setupScrollIndicator();
219
+ }
220
+
221
+ createConfirmationOverlay() {
222
+ const overlay = document.createElement('div');
223
+ overlay.className = 'confirmation-overlay';
224
+ overlay.innerHTML = `
225
+ <div class="confirmation-box">
226
+ <div class="confirmation-icon">⚠️</div>
227
+ <h3 class="confirmation-title">Confirm Action</h3>
228
+ <p class="confirmation-message">Are you sure you want to perform this action?</p>
229
+ <div class="confirmation-buttons">
230
+ <button class="confirmation-button cancel">Cancel</button>
231
+ <button class="confirmation-button confirm">Confirm</button>
232
+ </div>
233
+ </div>
234
+ `;
235
+
236
+ document.body.appendChild(overlay);
237
+ this.confirmationOverlay = overlay;
238
+
239
+ // Add event listeners
240
+ overlay.querySelector('.cancel').addEventListener('click', () => {
241
+ this.hideConfirmation();
242
+ });
243
+
244
+ overlay.querySelector('.confirm').addEventListener('click', () => {
245
+ if (this.confirmationCallback) {
246
+ this.confirmationCallback();
247
+ this.hideConfirmation();
248
+ }
249
+ });
250
+
251
+ // Click outside to cancel
252
+ overlay.addEventListener('click', (e) => {
253
+ if (e.target === overlay) {
254
+ this.hideConfirmation();
255
+ }
256
+ });
257
+ }
258
+
259
+ setupScrollIndicator() {
260
+ this.scrollIndicator.style.display = 'none';
261
+ }
262
+
263
+ showScrollIndicator(direction) {
264
+ this.scrollIndicator.style.display = 'block';
265
+ this.scrollIndicator.className = `scroll-indicator ${direction}`;
266
+
267
+ // Auto-hide after 500ms
268
+ setTimeout(() => {
269
+ this.scrollIndicator.style.display = 'none';
270
+ }, 500);
271
+ }
272
+
273
+ showConfirmation(message, callback) {
274
+ this.confirmationOverlay.querySelector('.confirmation-message').textContent = message;
275
+ this.confirmationCallback = callback;
276
+ this.confirmationOverlay.classList.add('show');
277
+ }
278
+
279
+ hideConfirmation() {
280
+ this.confirmationOverlay.classList.remove('show');
281
+ this.confirmationCallback = null;
282
+ }
283
+
284
+ renderEmails() {
285
+ this.emailList.innerHTML = '';
286
+ this.emailElements = [];
287
+
288
+ emails.forEach(email => {
289
+ const emailElement = document.createElement('div');
290
+ emailElement.className = 'email-item';
291
+ emailElement.dataset.id = email.id;
292
+
293
+ emailElement.innerHTML = `
294
+ <div class="email-header">
295
+ <div class="email-sender">${email.sender}</div>
296
+ <div class="email-time">${email.timestamp}</div>
297
+ </div>
298
+ <div class="email-subject">${email.subject}</div>
299
+ <div class="email-snippet">${email.snippet}</div>
300
+ `;
301
+
302
+ this.emailList.appendChild(emailElement);
303
+ this.emailElements.push({
304
+ id: email.id,
305
+ element: emailElement,
306
+ rect: null
307
+ });
308
+ });
309
+
310
+ // Update email positions
311
+ this.updateEmailPositions();
312
+
313
+ // Calculate max scroll
314
+ this.maxScroll = this.emailList.scrollHeight - this.emailList.clientHeight;
315
+ }
316
+
317
+ updateEmailPositions() {
318
+ this.emailListRect = this.emailList.getBoundingClientRect();
319
+
320
+ this.emailElements.forEach(item => {
321
+ const rect = item.element.getBoundingClientRect();
322
+ item.rect = {
323
+ left: rect.left,
324
+ top: rect.top,
325
+ right: rect.right,
326
+ bottom: rect.bottom,
327
+ width: rect.width,
328
+ height: rect.height
329
+ };
330
+ });
331
+ }
332
+
333
+ selectEmail(emailId) {
334
+ // Remove previous selection
335
+ if (this.selectedEmail) {
336
+ const prevElement = this.emailElements.find(e => e.id === this.selectedEmail);
337
+ if (prevElement) {
338
+ prevElement.element.classList.remove('selected');
339
+ }
340
+ }
341
+
342
+ // Set new selection
343
+ this.selectedEmail = emailId;
344
+ const newElement = this.emailElements.find(e => e.id === emailId);
345
+
346
+ if (newElement) {
347
+ newElement.element.classList.add('selected');
348
+ this.showSelectionHighlight(newElement.rect);
349
+ return newElement.rect;
350
+ }
351
+
352
+ return null;
353
+ }
354
+
355
+ showSelectionHighlight(rect) {
356
+ this.selectionHighlight.style.display = 'block';
357
+ this.selectionHighlight.style.left = `${rect.left}px`;
358
+ this.selectionHighlight.style.top = `${rect.top}px - ${this.scrollOffset}px`;
359
+ this.selectionHighlight.style.width = `${rect.width}px`;
360
+ this.selectionHighlight.style.height = `${rect.height}px`;
361
+
362
+ // Enhanced feedback - scale animation
363
+ this.selectionHighlight.style.transform = 'scale(1.02)';
364
+ setTimeout(() => {
365
+ this.selectionHighlight.style.transform = 'scale(1)';
366
+ }, 150);
367
+ }
368
+
369
+ hideSelectionHighlight() {
370
+ this.selectionHighlight.style.display = 'none';
371
+ }
372
+
373
+ showActionFeedback(message, type) {
374
+ this.actionFeedback.textContent = message;
375
+ this.actionFeedback.className = 'action-feedback';
376
+
377
+ if (type === 'delete') {
378
+ this.actionFeedback.classList.add('delete');
379
+ } else if (type === 'archive') {
380
+ this.actionFeedback.classList.add('archive');
381
+ } else {
382
+ this.actionFeedback.classList.add('summary');
383
+ }
384
+
385
+ this.actionFeedback.classList.add('show');
386
+
387
+ setTimeout(() => {
388
+ this.actionFeedback.classList.remove('show');
389
+ }, 2000);
390
+ }
391
+
392
+ clearSelection() {
393
+ if (this.selectedEmail) {
394
+ const element = this.emailElements.find(e => e.id === this.selectedEmail);
395
+ if (element) {
396
+ element.element.classList.remove('selected');
397
+ }
398
+ this.selectedEmail = null;
399
+ this.hideSelectionHighlight();
400
+ }
401
+ }
402
+
403
+ setupEventListeners() {
404
+ window.addEventListener('resize', () => {
405
+ this.updateEmailPositions();
406
+ if (this.selectedEmail) {
407
+ const element = this.emailElements.find(e => e.id === this.selectedEmail);
408
+ if (element) {
409
+ this.showSelectionHighlight(element.rect);
410
+ }
411
+ }
412
+
413
+ // Recalculate max scroll
414
+ this.maxScroll = this.emailList.scrollHeight - this.emailList.clientHeight;
415
+ });
416
+
417
+ // Add scroll event listener
418
+ this.emailList.addEventListener('scroll', () => {
419
+ this.scrollOffset = this.emailList.scrollTop;
420
+ this.updateEmailPositions();
421
+ });
422
+ }
423
+
424
+ // For gesture visualization
425
+ updateHandLandmarks(landmarks) {
426
+ this.handLandmarks.innerHTML = '';
427
+
428
+ if (!landmarks || landmarks.length === 0) return;
429
+
430
+ landmarks.forEach((landmark, i) => {
431
+ const landmarkEl = document.createElement('div');
432
+ landmarkEl.className = 'landmark';
433
+ if (i === 8) { // Index finger tip
434
+ landmarkEl.classList.add('index-tip');
435
+ }
436
+
437
+ landmarkEl.style.left = `${landmark.x * 100}%`;
438
+ landmarkEl.style.top = `${landmark.y * 100}%`;
439
+
440
+ this.handLandmarks.appendChild(landmarkEl);
441
+ });
442
+ }
443
+
444
+ updateGesturePath(points) {
445
+ this.gesturePath.innerHTML = '';
446
+
447
+ if (!points || points.length === 0) return;
448
+
449
+ points.forEach(point => {
450
+ const pointEl = document.createElement('div');
451
+ pointEl.className = 'point';
452
+ pointEl.style.left = `${point.x * 100}%`;
453
+ pointEl.style.top = `${point.y * 100}%`;
454
+ this.gesturePath.appendChild(pointEl);
455
+ });
456
+ }
457
+
458
+ // Haptic feedback simulation (if supported)
459
+ provideHapticFeedback() {
460
+ if (navigator.vibrate) {
461
+ navigator.vibrate(50);
462
+ }
463
+ }
464
+
465
+ // Scroll the email list
466
+ scrollEmailList(direction, speed) {
467
+ const scrollAmount = direction === 'up' ? -speed : speed;
468
+ this.emailList.scrollTop += scrollAmount;
469
+
470
+ // Update scroll offset
471
+ this.scrollOffset = this.emailList.scrollTop;
472
+
473
+ // Show scroll indicator
474
+ this.showScrollIndicator(direction);
475
+
476
+ // Update email positions
477
+ this.updateEmailPositions();
478
+ }
479
+
480
+ // Calculate the position of the hand relative to the email list
481
+ getRelativePosition(landmark, videoParams) {
482
+ const emailListRect = this.emailList.getBoundingClientRect();
483
+ const videoRect = document.getElementById('webcam').getBoundingClientRect();
484
+
485
+ // Convert landmark coordinates to screen coordinates
486
+ const screenX = landmark.x * videoRect.width;
487
+ const screenY = landmark.y * videoRect.height;
488
+
489
+ // Calculate relative position within email list
490
+ const relativeX = screenX - emailListRect.left;
491
+ const relativeY = screenY - emailListRect.top + this.scrollOffset;
492
+
493
+ return {
494
+ x: relativeX,
495
+ y: relativeY
496
+ };
497
+ }
498
  }
499
 
500
+ // ----------------------------
501
+ // Gesture Detection
502
+ // ----------------------------
503
+ class GestureDetector {
504
+ constructor(uiManager, debugManager) {
505
+ this.uiManager = uiManager;
506
+ this.debugManager = debugManager;
507
+ this.selectedEmailId = null;
508
+ this.gestureBuffer = [];
509
+ this.circlePoints = [];
510
+ this.scrollBuffer = [];
511
+ this.circleThreshold = 12;
512
+ this.swipeThreshold = 35;
513
+ this.scrollThreshold = 20;
514
+ this.gestureCooldown = 1500;
515
+ this.lastGestureTime = 0;
516
+ this.camera = null;
517
+ this.lastTimestamp = Date.now();
518
+
519
+ this.gestureStartPos = null;
520
+ this.holdTimer = null;
521
+ this.isPanning = false;
522
+ this.lastScrollTime = 0;
523
+ this.scrollSpeedFactor = 1.5;
524
+
525
+ this.debugManager.updateStatus('Setting up MediaPipe...');
526
+ this.setupMediaPipe();
527
+ }
528
+
529
+ setupMediaPipe() {
530
+ try {
531
+ const hands = new Hands({
532
+ locateFile: (file) => {
533
+ return `https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/${file}`;
534
+ }
535
+ });
536
+
537
+ hands.setOptions({
538
+ maxNumHands: 1,
539
+ modelComplexity: 1,
540
+ minDetectionConfidence: 0.7,
541
+ minTrackingConfidence: 0.7
542
+ });
543
+
544
+ hands.onResults((results) => {
545
+ this.processResults(results);
546
+ });
547
+
548
+ const videoElement = document.getElementById('webcam');
549
+
550
+ // Check if Camera is available
551
+ if (typeof Camera === 'undefined') {
552
+ this.debugManager.logError(new Error("Camera utils not loaded. Make sure camera_utils.js is included."));
553
+ return;
554
+ }
555
+
556
+ this.camera = new Camera(videoElement, {
557
+ onFrame: async () => {
558
+ try {
559
+ this.debugManager.setProcessing();
560
+ await hands.send({image: videoElement});
561
+ } catch (error) {
562
+ this.debugManager.logError(error);
563
+ }
564
+ },
565
+ width: 320,
566
+ height: 240
567
+ });
568
+
569
+ this.startCamera();
570
+ } catch (error) {
571
+ this.debugManager.logError(error);
572
+ console.error("MediaPipe setup error:", error);
573
+ }
574
+ }
575
+
576
+ async startCamera() {
577
+ try {
578
+ this.debugManager.updateCameraStatus('Starting...');
579
+ await this.camera.start();
580
+ this.debugManager.updateCameraStatus('Active');
581
+ this.debugManager.setReady();
582
+ console.log("Camera initialized successfully");
583
+ } catch (error) {
584
+ this.debugManager.updateCameraStatus('Error');
585
+ this.debugManager.logError(error);
586
+
587
+ // Try to get more specific error information
588
+ if (error.name === 'NotAllowedError') {
589
+ alert("Camera access denied. Please allow camera access in your browser settings.");
590
+ } else if (error.name === 'NotFoundError') {
591
+ alert("No camera found. Please connect a camera device.");
592
+ } else {
593
+ alert("Failed to start camera: " + error.message);
594
+ }
595
+ }
596
+ }
597
+
598
+ processResults(results) {
599
+ try {
600
+ // Update hand landmarks visualization
601
+ if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
602
+ this.uiManager.updateHandLandmarks(results.multiHandLandmarks[0]);
603
+
604
+ // Update gesture path for debugging
605
+ if (this.gestureBuffer.length > 0) {
606
+ this.uiManager.updateGesturePath(this.gestureBuffer);
607
+ }
608
+
609
+ this.detectGesture(results.multiHandLandmarks[0]);
610
+ } else {
611
+ this.uiManager.clearSelection();
612
+ this.gestureBuffer = [];
613
+ this.circlePoints = [];
614
+ this.scrollBuffer = [];
615
+ this.debugManager.updateGestureType('None');
616
+ this.debugManager.updateBufferCount(0);
617
+ this.debugManager.updateCircleCount(0);
618
+ }
619
+ } catch (error) {
620
+ this.debugManager.logError(error);
621
+ }
622
+ }
623
+
624
+ calculateCircleMetrics() {
625
+ if (this.circlePoints.length < 3) {
626
+ return { center: { x: 0, y: 0 }, radius: 0, circularity: 0 };
627
+ }
628
+
629
+ // Find centroid
630
+ let centerX = 0;
631
+ let centerY = 0;
632
+ for (const point of this.circlePoints) {
633
+ centerX += point.x;
634
+ centerY += point.y;
635
+ }
636
+ centerX /= this.circlePoints.length;
637
+ centerY /= this.circlePoints.length;
638
+
639
+ // Calculate radius and circularity
640
+ let totalRadius = 0;
641
+ let radiusVariance = 0;
642
+ const radii = [];
643
+
644
+ for (const point of this.circlePoints) {
645
+ const dx = point.x - centerX;
646
+ const dy = point.y - centerY;
647
+ const radius = Math.sqrt(dx * dx + dy * dy);
648
+ radii.push(radius);
649
+ totalRadius += radius;
650
+ }
651
+
652
+ const avgRadius = totalRadius / this.circlePoints.length;
653
+
654
+ // Calculate variance to determine circularity
655
+ for (const radius of radii) {
656
+ radiusVariance += Math.pow(radius - avgRadius, 2);
657
+ }
658
+ radiusVariance /= this.circlePoints.length;
659
+
660
+ // Calculate circularity (1 = perfect circle, 0 = not circular)
661
+ const circularity = radiusVariance < 0.0001 ? 1 : Math.max(0, 1 - radiusVariance / (avgRadius * avgRadius));
662
+
663
+ return {
664
+ center: { x: centerX, y: centerY },
665
+ radius: avgRadius,
666
+ circularity: circularity
667
+ };
668
+ }
669
+
670
+ detectGesture(landmarks) {
671
+ try {
672
+ const indexTip = landmarks[8];
673
+ const middleTip = landmarks[12];
674
+ const wrist = landmarks[0];
675
+
676
+ // Calculate screen coordinates
677
+ const screenX = indexTip.x * window.innerWidth;
678
+ const screenY = indexTip.y * window.innerHeight;
679
+
680
+ // Get video parameters for proper coordinate calculation
681
+ const videoParams = this.getVideoParameters();
682
+
683
+ // Convert to email list relative coordinates
684
+ const relativePos = this.uiManager.getRelativePosition(indexTip, videoParams);
685
+
686
+ // Pointing detection (index finger higher than middle)
687
+ if (indexTip.y < middleTip.y && wrist.y > indexTip.y) {
688
+ this.checkEmailSelection(relativePos.x, relativePos.y);
689
+
690
+ // Check for scroll gestures
691
+ this.checkScrollGesture(landmarks);
692
+ } else {
693
+ this.uiManager.clearSelection();
694
+ this.gestureBuffer = [];
695
+ this.circlePoints = [];
696
+ this.scrollBuffer = [];
697
+ this.debugManager.updateGestureType('None');
698
+ this.debugManager.updateBufferCount(0);
699
+ this.debugManager.updateCircleCount(0);
700
+ }
701
+
702
+ // Only process gestures if an email is selected
703
+ if (this.selectedEmailId === null) {
704
+ return;
705
+ }
706
+
707
+ // Get palm center for gesture detection
708
+ const palmCenterX = (wrist.x + landmarks[9].x) / 2;
709
+ const palmCenterY = (wrist.y + landmarks[9].y) / 2;
710
+
711
+ // Add to gesture buffer
712
+ const timestamp = Date.now();
713
+ const dt = timestamp - this.lastTimestamp;
714
+ this.lastTimestamp = timestamp;
715
+
716
+ this.gestureBuffer.push({x: palmCenterX, y: palmCenterY, timestamp});
717
+
718
+ // Check for swipe
719
+ if (this.gestureBuffer.length > 3) {
720
+ const prev = this.gestureBuffer[this.gestureBuffer.length - 2];
721
+ const current = this.gestureBuffer[this.gestureBuffer.length - 1];
722
+
723
+ // Calculate distance and velocity
724
+ const dx = (current.x - prev.x) * window.innerWidth;
725
+ const dy = (current.y - prev.y) * window.innerHeight;
726
+ const velocityX = dx / dt;
727
+ const velocityY = dy / dt;
728
+ const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
729
+
730
+ // Check if it's a horizontal swipe (using Fitts' Law principles)
731
+ if (Math.abs(dx) > this.swipeThreshold &&
732
+ Math.abs(dx) > Math.abs(dy) * 1.5 &&
733
+ speed > 0.3) {
734
+
735
+ if (Date.now() - this.lastGestureTime > this.gestureCooldown) {
736
+ this.lastGestureTime = Date.now();
737
+
738
+ if (dx > 0) {
739
+ this.debugManager.updateGestureType('Swipe Right');
740
+ this.handleGesture('swipe_right');
741
+ } else {
742
+ this.debugManager.updateGestureType('Swipe Left');
743
+ this.handleGesture('swipe_left');
744
+ }
745
+
746
+ this.gestureBuffer = [];
747
+ this.debugManager.updateBufferCount(0);
748
+ }
749
+ }
750
+ }
751
+
752
+ // Check for circle
753
+ this.circlePoints.push({x: palmCenterX, y: palmCenterY});
754
+ this.debugManager.updateCircleCount(this.circlePoints.length);
755
+
756
+ if (this.circlePoints.length > this.circleThreshold) {
757
+ const { center, radius, circularity } = this.calculateCircleMetrics();
758
+
759
+ // Improved circle detection with circularity check
760
+ if (circularity > 0.75 && radius > 30 &&
761
+ Date.now() - this.lastGestureTime > this.gestureCooldown) {
762
+
763
+ this.lastGestureTime = Date.now();
764
+ this.debugManager.updateGestureType('Circle');
765
+ this.handleGesture('circle');
766
+ this.circlePoints = [];
767
+ this.debugManager.updateCircleCount(0);
768
+ }
769
+ }
770
+
771
+ this.debugManager.updateBufferCount(this.gestureBuffer.length);
772
+ } catch (error) {
773
+ this.debugManager.logError(error);
774
+ }
775
+ }
776
+
777
+ checkScrollGesture(landmarks) {
778
+ const wrist = landmarks[0];
779
+ const indexTip = landmarks[8];
780
+ const middleTip = landmarks[12];
781
+
782
+ // Only check for scrolling if index finger is lower than middle finger (scrolling gesture)
783
+ if (indexTip.y > middleTip.y) {
784
+ // Add to scroll buffer
785
+ this.scrollBuffer.push({
786
+ x: indexTip.x,
787
+ y: indexTip.y,
788
+ timestamp: Date.now()
789
+ });
790
+
791
+ // Process scroll if we have enough points
792
+ if (this.scrollBuffer.length > 3) {
793
+ const recentPoints = this.scrollBuffer.slice(-3);
794
+ const first = recentPoints[0];
795
+ const last = recentPoints[recentPoints.length - 1];
796
+ const dt = last.timestamp - first.timestamp;
797
+
798
+ if (dt > 0) {
799
+ // Calculate vertical movement
800
+ const dy = (last.y - first.y) * window.innerHeight;
801
+ const scrollSpeed = Math.abs(dy) / dt * this.scrollSpeedFactor;
802
+
803
+ // Check if movement is significant
804
+ if (Math.abs(dy) > this.scrollThreshold && scrollSpeed > 0.5) {
805
+ const direction = dy > 0 ? 'down' : 'up';
806
+ this.uiManager.scrollEmailList(direction, scrollSpeed * 50);
807
+
808
+ // Clear buffer after processing
809
+ this.scrollBuffer = [];
810
+ this.debugManager.updateGestureType(`Scroll ${direction}`);
811
+ }
812
+ }
813
+ }
814
+ }
815
+ }
816
+
817
+ getVideoParameters() {
818
+ const video = document.getElementById('webcam');
819
+ const videoRect = video.getBoundingClientRect();
820
+
821
+ return {
822
+ videoWidth: video.videoWidth,
823
+ videoHeight: video.videoHeight,
824
+ rect: videoRect,
825
+ aspectRatio: video.videoWidth / video.videoHeight
826
+ };
827
+ }
828
+
829
+ checkEmailSelection(x, y) {
830
+ try {
831
+ // Find the email under the finger
832
+ for (let i = this.uiManager.emailElements.length - 1; i >= 0; i--) {
833
+ const email = this.uiManager.emailElements[i];
834
+ if (email.rect &&
835
+ x >= 0 && x <= email.rect.width &&
836
+ y >= 0 && y <= email.rect.height) {
837
+
838
+ // Only select if it's a different email
839
+ if (this.selectedEmailId !== email.id) {
840
+ this.selectedEmailId = email.id;
841
+ this.uiManager.selectEmail(email.id);
842
+ this.debugManager.updateSelectedEmail(email.id);
843
+
844
+ // Start hold timer for confirmation
845
+ this.startHoldTimer();
846
+ }
847
+ return;
848
+ }
849
+ }
850
+
851
+ // If no email is selected, clear selection
852
+ if (!this.uiManager.emailElements.some(email =>
853
+ email.rect &&
854
+ x >= 0 && x <= email.rect.width &&
855
+ y >= 0 && y <= email.rect.height)) {
856
+
857
+ this.uiManager.clearSelection();
858
+ this.selectedEmailId = null;
859
+ this.debugManager.updateSelectedEmail(null);
860
+ this.clearHoldTimer();
861
+ }
862
+ } catch (error) {
863
+ this.debugManager.logError(error);
864
+ }
865
+ }
866
+
867
+ startHoldTimer() {
868
+ this.clearHoldTimer();
869
+
870
+ // Start a new timer for confirmation
871
+ this.holdTimer = setTimeout(() => {
872
+ if (this.selectedEmailId) {
873
+ const email = emails.find(e => e.id === this.selectedEmailId);
874
+ this.uiManager.showConfirmation(
875
+ `Long press detected on "${email.subject}".\n\nConfirm action?`,
876
+ () => {
877
+ // Action confirmed - perform gesture
878
+ this.handleGesture('confirm');
879
+ });
880
+ }
881
+ }, 1000);
882
+ }
883
+
884
+ clearHoldTimer() {
885
+ if (this.holdTimer) {
886
+ clearTimeout(this.holdTimer);
887
+ this.holdTimer = null;
888
+ }
889
+ }
890
+
891
+ handleGesture(gesture) {
892
+ try {
893
+ if (!this.selectedEmailId) return;
894
+
895
+ const email = emails.find(e => e.id === this.selectedEmailId);
896
+ if (!email) return;
897
+
898
+ switch (gesture) {
899
+ case 'swipe_left':
900
+ // Show confirmation for destructive action (Fitts' Law principle)
901
+ this.uiManager.showConfirmation(
902
+ `Delete "${email.subject}"?`,
903
+ () => {
904
+ this.uiManager.showActionFeedback(`πŸ—‘οΈ Deleted: ${email.subject}`, 'delete');
905
+ const index = emails.findIndex(e => e.id === this.selectedEmailId);
906
+ if (index !== -1) emails.splice(index, 1);
907
+ this.uiManager.renderEmails();
908
+ this.selectedEmailId = null;
909
+ this.debugManager.updateSelectedEmail(null);
910
+ this.uiManager.provideHapticFeedback();
911
+ }
912
+ );
913
+ break;
914
+
915
+ case 'swipe_right':
916
+ this.uiManager.showConfirmation(
917
+ `Archive "${email.subject}"?`,
918
+ () => {
919
+ this.uiManager.showActionFeedback(`βœ… Archived: ${email.subject}`, 'archive');
920
+ const archiveIndex = emails.findIndex(e => e.id === this.selectedEmailId);
921
+ if (archiveIndex !== -1) emails.splice(archiveIndex, 1);
922
+ this.uiManager.renderEmails();
923
+ this.selectedEmailId = null;
924
+ this.debugManager.updateSelectedEmail(null);
925
+ this.uiManager.provideHapticFeedback();
926
+ }
927
+ );
928
+ break;
929
+
930
+ case 'circle':
931
+ const summary = `This email discusses ${email.subject.toLowerCase()}.`;
932
+ this.uiManager.showActionFeedback(`πŸ“ Summary: ${summary}`, 'summary');
933
+ this.uiManager.provideHapticFeedback();
934
+ break;
935
+
936
+ case 'confirm':
937
+ // This is triggered when the user holds an email
938
+ this.uiManager.showActionFeedback(`πŸ” Selected: ${email.subject}`, 'summary');
939
+ break;
940
+ }
941
+ } catch (error) {
942
+ this.debugManager.logError(error);
943
+ }
944
+ }
945
  }
946
 
947
+ // ----------------------------
948
+ // Initialize App
949
+ // ----------------------------
950
+ document.addEventListener('DOMContentLoaded', () => {
951
+ // Initialize debug manager first
952
+ const debugManager = new DebugManager();
953
+ debugManager.updateStatus('Initializing UI...');
954
+
955
+ // Initialize UI
956
+ const uiManager = new UIManager();
957
+ debugManager.setReady();
958
+
959
+ // Request camera access
960
+ const videoElement = document.getElementById('webcam');
961
+
962
+ navigator.mediaDevices.getUserMedia({ video: true })
963
+ .then(stream => {
964
+ videoElement.srcObject = stream;
965
+ debugManager.updateCameraStatus('Initializing...');
966
+
967
+ // Initialize gesture detection after a short delay to ensure UI is ready
968
+ setTimeout(() => {
969
+ debugManager.updateStatus('Setting up gesture detection...');
970
+ try {
971
+ new GestureDetector(uiManager, debugManager);
972
+ } catch (error) {
973
+ debugManager.logError(error);
974
+ }
975
+ }, 1500);
976
+ })
977
+ .catch(err => {
978
+ debugManager.updateCameraStatus('Denied');
979
+ debugManager.logError(err);
980
+ console.error("Error accessing camera:", err);
981
+
982
+ // More specific error handling
983
+ if (err.name === 'NotAllowedError') {
984
+ alert("Camera access denied. Please allow camera access in your browser settings.");
985
+ } else if (err.name === 'NotFoundError') {
986
+ alert("No camera found. Please connect a camera device.");
987
+ } else {
988
+ alert("Camera error: " + err.message);
989
+ }
990
+ });
991
+ });