Update index.html
Browse files- index.html +250 -199
index.html
CHANGED
|
@@ -4,8 +4,7 @@
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>GestureMail Pro</title>
|
| 7 |
-
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]
|
| 8 |
-
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]/dist/tf.min.js"></script>
|
| 9 |
<style>
|
| 10 |
* {
|
| 11 |
margin: 0;
|
|
@@ -262,6 +261,7 @@
|
|
| 262 |
background-color: #4285f4;
|
| 263 |
border-radius: 50%;
|
| 264 |
transform: translate(-50%, -50%);
|
|
|
|
| 265 |
}
|
| 266 |
|
| 267 |
.hand-landmarks .landmark.index-tip {
|
|
@@ -270,6 +270,86 @@
|
|
| 270 |
height: 12px;
|
| 271 |
}
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
/* For mobile responsiveness */
|
| 274 |
@media (max-width: 768px) {
|
| 275 |
.webcam-container {
|
|
@@ -279,7 +359,7 @@
|
|
| 279 |
right: 10px;
|
| 280 |
}
|
| 281 |
|
| 282 |
-
.gesture-guide {
|
| 283 |
max-width: 240px;
|
| 284 |
font-size: 0.9em;
|
| 285 |
padding: 10px;
|
|
@@ -334,8 +414,44 @@
|
|
| 334 |
|
| 335 |
<div class="gesture-visualization">
|
| 336 |
<div class="hand-landmarks" id="handLandmarks"></div>
|
|
|
|
| 337 |
</div>
|
|
|
|
| 338 |
<div class="selection-highlight" id="selectionHighlight" style="display: none;"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
| 340 |
<script>
|
| 341 |
// ----------------------------
|
|
@@ -408,6 +524,69 @@
|
|
| 408 |
}
|
| 409 |
];
|
| 410 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
// ----------------------------
|
| 412 |
// UI Management
|
| 413 |
// ----------------------------
|
|
@@ -417,6 +596,7 @@
|
|
| 417 |
this.actionFeedback = document.getElementById('actionFeedback');
|
| 418 |
this.selectionHighlight = document.getElementById('selectionHighlight');
|
| 419 |
this.handLandmarks = document.getElementById('handLandmarks');
|
|
|
|
| 420 |
|
| 421 |
this.selectedEmail = null;
|
| 422 |
this.emailElements = [];
|
|
@@ -563,14 +743,29 @@
|
|
| 563 |
this.handLandmarks.appendChild(landmarkEl);
|
| 564 |
});
|
| 565 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
}
|
| 567 |
|
| 568 |
// ----------------------------
|
| 569 |
// Gesture Detection
|
| 570 |
// ----------------------------
|
| 571 |
class GestureDetector {
|
| 572 |
-
constructor(uiManager) {
|
| 573 |
this.uiManager = uiManager;
|
|
|
|
| 574 |
this.selectedEmailId = null;
|
| 575 |
this.gestureBuffer = [];
|
| 576 |
this.circlePoints = [];
|
|
@@ -578,212 +773,68 @@
|
|
| 578 |
this.swipeThreshold = 50;
|
| 579 |
this.gestureCooldown = 1500; // 1.5 seconds
|
| 580 |
this.lastGestureTime = 0;
|
|
|
|
| 581 |
|
|
|
|
| 582 |
this.setupMediaPipe();
|
| 583 |
}
|
| 584 |
|
| 585 |
setupMediaPipe() {
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
hands.setOptions({
|
| 591 |
-
maxNumHands: 1,
|
| 592 |
-
modelComplexity: 1,
|
| 593 |
-
minDetectionConfidence: 0.7,
|
| 594 |
-
minTrackingConfidence: 0.7
|
| 595 |
-
});
|
| 596 |
-
|
| 597 |
-
hands.onResults(results => {
|
| 598 |
-
this.processResults(results);
|
| 599 |
-
});
|
| 600 |
-
|
| 601 |
-
const videoElement = document.getElementById('webcam');
|
| 602 |
-
|
| 603 |
-
const camera = new Camera(videoElement, {
|
| 604 |
-
onFrame: async () => {
|
| 605 |
-
await hands.send({image: videoElement});
|
| 606 |
-
},
|
| 607 |
-
width: 320,
|
| 608 |
-
height: 240
|
| 609 |
-
});
|
| 610 |
-
camera.start();
|
| 611 |
-
}
|
| 612 |
-
|
| 613 |
-
processResults(results) {
|
| 614 |
-
// Update hand landmarks visualization
|
| 615 |
-
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
|
| 616 |
-
this.uiManager.updateHandLandmarks(results.multiHandLandmarks[0]);
|
| 617 |
-
this.detectGesture(results.multiHandLandmarks[0]);
|
| 618 |
-
} else {
|
| 619 |
-
this.uiManager.clearSelection();
|
| 620 |
-
this.gestureBuffer = [];
|
| 621 |
-
this.circlePoints = [];
|
| 622 |
-
}
|
| 623 |
-
}
|
| 624 |
-
|
| 625 |
-
detectGesture(landmarks) {
|
| 626 |
-
// Convert to pixel coordinates for easier calculation
|
| 627 |
-
const indexTip = landmarks[8];
|
| 628 |
-
const middleTip = landmarks[12];
|
| 629 |
-
const wrist = landmarks[0];
|
| 630 |
-
|
| 631 |
-
// Pointing detection (index finger higher than middle)
|
| 632 |
-
if (indexTip.y < middleTip.y) {
|
| 633 |
-
this.checkEmailSelection(indexTip.x, indexTip.y);
|
| 634 |
-
} else {
|
| 635 |
-
this.uiManager.clearSelection();
|
| 636 |
-
this.gestureBuffer = [];
|
| 637 |
-
this.circlePoints = [];
|
| 638 |
-
}
|
| 639 |
-
|
| 640 |
-
// Only process gestures if an email is selected
|
| 641 |
-
if (this.selectedEmailId === null) {
|
| 642 |
-
return;
|
| 643 |
-
}
|
| 644 |
-
|
| 645 |
-
// Get palm center for gesture detection
|
| 646 |
-
const palmCenterX = (wrist.x + landmarks[9].x) / 2;
|
| 647 |
-
const palmCenterY = (wrist.y + landmarks[9].y) / 2;
|
| 648 |
-
|
| 649 |
-
// Add to gesture buffer
|
| 650 |
-
this.gestureBuffer.push({x: palmCenterX, y: palmCenterY});
|
| 651 |
-
|
| 652 |
-
// Check for swipe
|
| 653 |
-
if (this.gestureBuffer.length > 2) {
|
| 654 |
-
const prev = this.gestureBuffer[this.gestureBuffer.length - 2];
|
| 655 |
-
const current = this.gestureBuffer[this.gestureBuffer.length - 1];
|
| 656 |
|
| 657 |
-
|
| 658 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
this.lastGestureTime = Date.now();
|
| 664 |
-
|
| 665 |
-
if (dx > 0) {
|
| 666 |
-
this.handleGesture('swipe_right');
|
| 667 |
-
} else {
|
| 668 |
-
this.handleGesture('swipe_left');
|
| 669 |
-
}
|
| 670 |
-
|
| 671 |
-
this.gestureBuffer = [];
|
| 672 |
-
}
|
| 673 |
-
}
|
| 674 |
-
}
|
| 675 |
-
|
| 676 |
-
// Check for circle
|
| 677 |
-
this.circlePoints.push({x: palmCenterX, y: palmCenterY});
|
| 678 |
-
if (this.circlePoints.length > this.circleThreshold) {
|
| 679 |
-
const startPoint = this.circlePoints[0];
|
| 680 |
-
const endPoint = this.circlePoints[this.circlePoints.length - 1];
|
| 681 |
|
| 682 |
-
const
|
| 683 |
-
Math.pow(startPoint.x - endPoint.x, 2) +
|
| 684 |
-
Math.pow(startPoint.y - endPoint.y, 2)
|
| 685 |
-
) * window.innerWidth;
|
| 686 |
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
|
|
|
|
|
|
|
|
|
| 708 |
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
}
|
| 714 |
-
return;
|
| 715 |
-
}
|
| 716 |
-
}
|
| 717 |
-
|
| 718 |
-
// If no email is selected, clear selection
|
| 719 |
-
if (!this.uiManager.emailElements.some(email =>
|
| 720 |
-
email.rect &&
|
| 721 |
-
screenX >= email.rect.left &&
|
| 722 |
-
screenX <= email.rect.right &&
|
| 723 |
-
screenY >= email.rect.top &&
|
| 724 |
-
screenY <= email.rect.bottom)) {
|
| 725 |
-
this.uiManager.clearSelection();
|
| 726 |
-
this.selectedEmailId = null;
|
| 727 |
}
|
| 728 |
}
|
| 729 |
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
switch (gesture) {
|
| 737 |
-
case 'swipe_left':
|
| 738 |
-
this.uiManager.showActionFeedback(`🗑️ Deleted: ${email.subject}`, 'delete');
|
| 739 |
-
// Remove from UI
|
| 740 |
-
const index = emails.findIndex(e => e.id === this.selectedEmailId);
|
| 741 |
-
if (index !== -1) emails.splice(index, 1);
|
| 742 |
-
this.uiManager.renderEmails();
|
| 743 |
-
this.selectedEmailId = null;
|
| 744 |
-
break;
|
| 745 |
-
|
| 746 |
-
case 'swipe_right':
|
| 747 |
-
this.uiManager.showActionFeedback(`✅ Archived: ${email.subject}`, 'archive');
|
| 748 |
-
// In a real app, we'd move to archive
|
| 749 |
-
const archiveIndex = emails.findIndex(e => e.id === this.selectedEmailId);
|
| 750 |
-
if (archiveIndex !== -1) emails.splice(archiveIndex, 1);
|
| 751 |
-
this.uiManager.renderEmails();
|
| 752 |
-
this.selectedEmailId = null;
|
| 753 |
-
break;
|
| 754 |
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
this.uiManager.showActionFeedback(`📝 Summary: ${summary}`, 'summary');
|
| 758 |
-
break;
|
| 759 |
-
}
|
| 760 |
-
}
|
| 761 |
-
}
|
| 762 |
-
|
| 763 |
-
// ----------------------------
|
| 764 |
-
// Initialize App
|
| 765 |
-
// ----------------------------
|
| 766 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 767 |
-
// Request camera access
|
| 768 |
-
const videoElement = document.getElementById('webcam');
|
| 769 |
-
|
| 770 |
-
navigator.mediaDevices.getUserMedia({ video: true })
|
| 771 |
-
.then(stream => {
|
| 772 |
-
videoElement.srcObject = stream;
|
| 773 |
-
})
|
| 774 |
-
.catch(err => {
|
| 775 |
-
console.error("Error accessing camera:", err);
|
| 776 |
-
alert("Camera access is required for this app to function properly.");
|
| 777 |
-
});
|
| 778 |
-
|
| 779 |
-
// Initialize UI
|
| 780 |
-
const uiManager = new UIManager();
|
| 781 |
-
|
| 782 |
-
// Initialize gesture detection
|
| 783 |
-
setTimeout(() => {
|
| 784 |
-
new GestureDetector(uiManager);
|
| 785 |
-
}, 1000); // Give time for UI to render
|
| 786 |
-
});
|
| 787 |
-
</script>
|
| 788 |
-
</body>
|
| 789 |
-
</html>
|
|
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>GestureMail Pro</title>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/hands.js"></script>
|
|
|
|
| 8 |
<style>
|
| 9 |
* {
|
| 10 |
margin: 0;
|
|
|
|
| 261 |
background-color: #4285f4;
|
| 262 |
border-radius: 50%;
|
| 263 |
transform: translate(-50%, -50%);
|
| 264 |
+
z-index: 100;
|
| 265 |
}
|
| 266 |
|
| 267 |
.hand-landmarks .landmark.index-tip {
|
|
|
|
| 270 |
height: 12px;
|
| 271 |
}
|
| 272 |
|
| 273 |
+
.gesture-path {
|
| 274 |
+
position: absolute;
|
| 275 |
+
width: 100%;
|
| 276 |
+
height: 100%;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.gesture-path .point {
|
| 280 |
+
position: absolute;
|
| 281 |
+
width: 4px;
|
| 282 |
+
height: 4px;
|
| 283 |
+
background-color: #ff0000;
|
| 284 |
+
border-radius: 50%;
|
| 285 |
+
transform: translate(-50%, -50%);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/* Debug Panel */
|
| 289 |
+
.debug-panel {
|
| 290 |
+
position: fixed;
|
| 291 |
+
top: 20px;
|
| 292 |
+
right: 20px;
|
| 293 |
+
background-color: rgba(0,0,0,0.7);
|
| 294 |
+
color: #0f0;
|
| 295 |
+
padding: 15px;
|
| 296 |
+
border-radius: 12px;
|
| 297 |
+
max-width: 300px;
|
| 298 |
+
z-index: 100;
|
| 299 |
+
font-family: monospace;
|
| 300 |
+
font-size: 12px;
|
| 301 |
+
line-height: 1.4;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.debug-panel h3 {
|
| 305 |
+
color: #4285f4;
|
| 306 |
+
margin-bottom: 10px;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.debug-item {
|
| 310 |
+
margin-bottom: 5px;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.debug-label {
|
| 314 |
+
display: inline-block;
|
| 315 |
+
width: 120px;
|
| 316 |
+
color: #aaa;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.debug-value {
|
| 320 |
+
color: #fff;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
/* Status Indicator */
|
| 324 |
+
.status-indicator {
|
| 325 |
+
position: fixed;
|
| 326 |
+
bottom: 30px;
|
| 327 |
+
right: 30px;
|
| 328 |
+
width: 20px;
|
| 329 |
+
height: 20px;
|
| 330 |
+
border-radius: 50%;
|
| 331 |
+
z-index: 200;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.status-indicator.ready {
|
| 335 |
+
background-color: #34a853;
|
| 336 |
+
box-shadow: 0 0 10px #34a853;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.status-indicator.processing {
|
| 340 |
+
background-color: #fbbc05;
|
| 341 |
+
animation: blink 1s infinite;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.status-indicator.error {
|
| 345 |
+
background-color: #ea4335;
|
| 346 |
+
animation: blink 0.5s infinite;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes blink {
|
| 350 |
+
50% { opacity: 0.5; }
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
/* For mobile responsiveness */
|
| 354 |
@media (max-width: 768px) {
|
| 355 |
.webcam-container {
|
|
|
|
| 359 |
right: 10px;
|
| 360 |
}
|
| 361 |
|
| 362 |
+
.gesture-guide, .debug-panel {
|
| 363 |
max-width: 240px;
|
| 364 |
font-size: 0.9em;
|
| 365 |
padding: 10px;
|
|
|
|
| 414 |
|
| 415 |
<div class="gesture-visualization">
|
| 416 |
<div class="hand-landmarks" id="handLandmarks"></div>
|
| 417 |
+
<div class="gesture-path" id="gesturePath"></div>
|
| 418 |
</div>
|
| 419 |
+
|
| 420 |
<div class="selection-highlight" id="selectionHighlight" style="display: none;"></div>
|
| 421 |
+
|
| 422 |
+
<div class="debug-panel">
|
| 423 |
+
<h3>Debug Information</h3>
|
| 424 |
+
<div class="debug-item">
|
| 425 |
+
<span class="debug-label">Status:</span>
|
| 426 |
+
<span class="debug-value" id="debugStatus">Initializing...</span>
|
| 427 |
+
</div>
|
| 428 |
+
<div class="debug-item">
|
| 429 |
+
<span class="debug-label">Selected Email:</span>
|
| 430 |
+
<span class="debug-value" id="debugSelectedEmail">None</span>
|
| 431 |
+
</div>
|
| 432 |
+
<div class="debug-item">
|
| 433 |
+
<span class="debug-label">Gesture Type:</span>
|
| 434 |
+
<span class="debug-value" id="debugGestureType">None</span>
|
| 435 |
+
</div>
|
| 436 |
+
<div class="debug-item">
|
| 437 |
+
<span class="debug-label">Gesture Buffer:</span>
|
| 438 |
+
<span class="debug-value" id="debugBufferCount">0 points</span>
|
| 439 |
+
</div>
|
| 440 |
+
<div class="debug-item">
|
| 441 |
+
<span class="debug-label">Circle Points:</span>
|
| 442 |
+
<span class="debug-value" id="debugCircleCount">0 points</span>
|
| 443 |
+
</div>
|
| 444 |
+
<div class="debug-item">
|
| 445 |
+
<span class="debug-label">Camera:</span>
|
| 446 |
+
<span class="debug-value" id="debugCameraStatus">Not initialized</span>
|
| 447 |
+
</div>
|
| 448 |
+
<div class="debug-item">
|
| 449 |
+
<span class="debug-label">Last Error:</span>
|
| 450 |
+
<span class="debug-value" id="debugLastError">None</span>
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
|
| 454 |
+
<div class="status-indicator" id="statusIndicator"></div>
|
| 455 |
|
| 456 |
<script>
|
| 457 |
// ----------------------------
|
|
|
|
| 524 |
}
|
| 525 |
];
|
| 526 |
|
| 527 |
+
// ----------------------------
|
| 528 |
+
// Debug Utilities
|
| 529 |
+
// ----------------------------
|
| 530 |
+
class DebugManager {
|
| 531 |
+
constructor() {
|
| 532 |
+
this.statusEl = document.getElementById('debugStatus');
|
| 533 |
+
this.selectedEmailEl = document.getElementById('debugSelectedEmail');
|
| 534 |
+
this.gestureTypeEl = document.getElementById('debugGestureType');
|
| 535 |
+
this.bufferCountEl = document.getElementById('debugBufferCount');
|
| 536 |
+
this.circleCountEl = document.getElementById('debugCircleCount');
|
| 537 |
+
this.cameraStatusEl = document.getElementById('debugCameraStatus');
|
| 538 |
+
this.lastErrorEl = document.getElementById('debugLastError');
|
| 539 |
+
this.statusIndicator = document.getElementById('statusIndicator');
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
updateStatus(status) {
|
| 543 |
+
this.statusEl.textContent = status;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
updateSelectedEmail(emailId) {
|
| 547 |
+
if (emailId) {
|
| 548 |
+
const email = emails.find(e => e.id === emailId);
|
| 549 |
+
this.selectedEmailEl.textContent = email ? email.subject.substring(0, 20) + '...' : 'Unknown';
|
| 550 |
+
} else {
|
| 551 |
+
this.selectedEmailEl.textContent = 'None';
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
updateGestureType(gestureType) {
|
| 556 |
+
this.gestureTypeEl.textContent = gestureType;
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
updateBufferCount(count) {
|
| 560 |
+
this.bufferCountEl.textContent = `${count} points`;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
updateCircleCount(count) {
|
| 564 |
+
this.circleCountEl.textContent = `${count} points`;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
updateCameraStatus(status) {
|
| 568 |
+
this.cameraStatusEl.textContent = status;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
logError(error) {
|
| 572 |
+
console.error("Gesture Detection Error:", error);
|
| 573 |
+
this.lastErrorEl.textContent = error.message.substring(0, 50) + (error.message.length > 50 ? '...' : '');
|
| 574 |
+
|
| 575 |
+
// Update status indicator to red
|
| 576 |
+
this.statusIndicator.className = 'status-indicator error';
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
setReady() {
|
| 580 |
+
this.updateStatus('Ready');
|
| 581 |
+
this.statusIndicator.className = 'status-indicator ready';
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
setProcessing() {
|
| 585 |
+
this.updateStatus('Processing...');
|
| 586 |
+
this.statusIndicator.className = 'status-indicator processing';
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
// ----------------------------
|
| 591 |
// UI Management
|
| 592 |
// ----------------------------
|
|
|
|
| 596 |
this.actionFeedback = document.getElementById('actionFeedback');
|
| 597 |
this.selectionHighlight = document.getElementById('selectionHighlight');
|
| 598 |
this.handLandmarks = document.getElementById('handLandmarks');
|
| 599 |
+
this.gesturePath = document.getElementById('gesturePath');
|
| 600 |
|
| 601 |
this.selectedEmail = null;
|
| 602 |
this.emailElements = [];
|
|
|
|
| 743 |
this.handLandmarks.appendChild(landmarkEl);
|
| 744 |
});
|
| 745 |
}
|
| 746 |
+
|
| 747 |
+
updateGesturePath(points) {
|
| 748 |
+
this.gesturePath.innerHTML = '';
|
| 749 |
+
|
| 750 |
+
if (!points || points.length === 0) return;
|
| 751 |
+
|
| 752 |
+
points.forEach(point => {
|
| 753 |
+
const pointEl = document.createElement('div');
|
| 754 |
+
pointEl.className = 'point';
|
| 755 |
+
pointEl.style.left = `${point.x * 100}%`;
|
| 756 |
+
pointEl.style.top = `${point.y * 100}%`;
|
| 757 |
+
this.gesturePath.appendChild(pointEl);
|
| 758 |
+
});
|
| 759 |
+
}
|
| 760 |
}
|
| 761 |
|
| 762 |
// ----------------------------
|
| 763 |
// Gesture Detection
|
| 764 |
// ----------------------------
|
| 765 |
class GestureDetector {
|
| 766 |
+
constructor(uiManager, debugManager) {
|
| 767 |
this.uiManager = uiManager;
|
| 768 |
+
this.debugManager = debugManager;
|
| 769 |
this.selectedEmailId = null;
|
| 770 |
this.gestureBuffer = [];
|
| 771 |
this.circlePoints = [];
|
|
|
|
| 773 |
this.swipeThreshold = 50;
|
| 774 |
this.gestureCooldown = 1500; // 1.5 seconds
|
| 775 |
this.lastGestureTime = 0;
|
| 776 |
+
this.isProcessing = false;
|
| 777 |
|
| 778 |
+
this.debugManager.updateStatus('Setting up MediaPipe...');
|
| 779 |
this.setupMediaPipe();
|
| 780 |
}
|
| 781 |
|
| 782 |
setupMediaPipe() {
|
| 783 |
+
try {
|
| 784 |
+
const hands = new Hands({locateFile: (file) => {
|
| 785 |
+
return `https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/${file}`;
|
| 786 |
+
}});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
|
| 788 |
+
hands.setOptions({
|
| 789 |
+
maxNumHands: 1,
|
| 790 |
+
modelComplexity: 1,
|
| 791 |
+
minDetectionConfidence: 0.7,
|
| 792 |
+
minTrackingConfidence: 0.7
|
| 793 |
+
});
|
| 794 |
|
| 795 |
+
hands.onResults(results => {
|
| 796 |
+
this.processResults(results);
|
| 797 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 798 |
|
| 799 |
+
const videoElement = document.getElementById('webcam');
|
|
|
|
|
|
|
|
|
|
| 800 |
|
| 801 |
+
const camera = new Camera(videoElement, {
|
| 802 |
+
onFrame: async () => {
|
| 803 |
+
try {
|
| 804 |
+
this.debugManager.setProcessing();
|
| 805 |
+
await hands.send({image: videoElement});
|
| 806 |
+
} catch (error) {
|
| 807 |
+
this.debugManager.logError(error);
|
| 808 |
+
}
|
| 809 |
+
},
|
| 810 |
+
width: 320,
|
| 811 |
+
height: 240
|
| 812 |
+
});
|
| 813 |
+
|
| 814 |
+
camera.start()
|
| 815 |
+
.then(() => {
|
| 816 |
+
this.debugManager.updateCameraStatus('Active');
|
| 817 |
+
this.debugManager.setReady();
|
| 818 |
+
console.log("Camera initialized successfully");
|
| 819 |
+
})
|
| 820 |
+
.catch(error => {
|
| 821 |
+
this.debugManager.updateCameraStatus('Error');
|
| 822 |
+
this.debugManager.logError(error);
|
| 823 |
+
alert("Failed to start camera: " + error.message);
|
| 824 |
+
});
|
| 825 |
|
| 826 |
+
} catch (error) {
|
| 827 |
+
this.debugManager.logError(error);
|
| 828 |
+
console.error("MediaPipe setup error:", error);
|
| 829 |
+
alert("Failed to initialize gesture detection: " + error.message);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
}
|
| 831 |
}
|
| 832 |
|
| 833 |
+
processResults(results) {
|
| 834 |
+
try {
|
| 835 |
+
// Update hand landmarks visualization
|
| 836 |
+
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
|
| 837 |
+
this.uiManager.updateHandLandmarks(results.multiHandLandmarks[0]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 838 |
|
| 839 |
+
// Update gesture path for debugging
|
| 840 |
+
i
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|