VISHAL18for4 commited on
Commit
c480230
ยท
verified ยท
1 Parent(s): 18f6f12

Update public/script.js

Browse files
Files changed (1) hide show
  1. public/script.js +99 -82
public/script.js CHANGED
@@ -4,8 +4,7 @@
4
  const video = document.getElementById('webcam');
5
  const canvas = document.getElementById('drawCanvas');
6
  const ctx = canvas.getContext('2d');
7
- const statusDiv = document.getElementById('status');
8
- const gestureIndicator = document.getElementById('gestureIndicator');
9
  const fpsSpan = document.getElementById('fpsCounter');
10
  const savedSpan = document.getElementById('savedCounter');
11
 
@@ -19,7 +18,7 @@ const sizeDownBtn = document.getElementById('brushSizeDown');
19
  const sizeUpBtn = document.getElementById('brushSizeUp');
20
 
21
  // ============================================================
22
- // 2. Backend Logger
23
  // ============================================================
24
  function backendLog(message, level = 'info') {
25
  const payload = { message, level, timestamp: new Date().toISOString() };
@@ -28,7 +27,6 @@ function backendLog(message, level = 'info') {
28
  headers: { 'Content-Type': 'application/json' },
29
  body: JSON.stringify(payload)
30
  }).catch(() => {});
31
- statusDiv.textContent = message;
32
  }
33
 
34
  // ============================================================
@@ -53,21 +51,39 @@ let modelLoadAttempts = 0;
53
  const MAX_MODEL_ATTEMPTS = 3;
54
  const TRIGGER_COUNT = 10;
55
  let detectionInterval = null;
 
56
 
57
  // ============================================================
58
- // 4. Canvas Resize
 
 
 
 
 
 
 
 
59
  // ============================================================
60
  function resizeCanvas() {
61
  const rect = canvas.parentElement.getBoundingClientRect();
62
- canvas.width = rect.width;
63
- canvas.height = rect.height;
 
 
 
 
 
64
  canvasWidth = canvas.width;
65
  canvasHeight = canvas.height;
 
66
  }
 
 
67
  window.addEventListener('resize', resizeCanvas);
 
68
 
69
  // ============================================================
70
- // 5. Admin Panel Trigger โ€“ 10 Taps or 10 Key Presses
71
  // ============================================================
72
  let adminTriggerCount = 0;
73
  let adminTriggerTimer = null;
@@ -82,15 +98,11 @@ document.addEventListener('keydown', (e) => {
82
  e.preventDefault();
83
  adminTriggerCount++;
84
  clearTimeout(adminTriggerTimer);
85
- adminTriggerTimer = setTimeout(() => {
86
- backendLog(`Admin trigger reset (${adminTriggerCount}/${TRIGGER_COUNT})`);
87
- adminTriggerCount = 0;
88
- }, 3000);
89
  backendLog(`Key '3' pressed (${adminTriggerCount}/${TRIGGER_COUNT})`);
90
-
91
  if (adminTriggerCount >= TRIGGER_COUNT) {
92
  adminTriggerCount = 0;
93
- backendLog(`โœ… Admin trigger activated! (${TRIGGER_COUNT} presses)`);
94
  openAdminPanel();
95
  }
96
  }
@@ -99,47 +111,39 @@ document.addEventListener('keydown', (e) => {
99
  function setupTapTrigger(element) {
100
  let tapCount = 0;
101
  let tapTimer = null;
102
-
103
  element.addEventListener('click', (e) => {
104
  if (e.target.closest('button') || e.target.closest('.toolbar') || e.target.closest('header')) {
105
  return;
106
  }
107
-
108
  tapCount++;
109
  clearTimeout(tapTimer);
110
- tapTimer = setTimeout(() => {
111
- backendLog(`Tap trigger reset (${tapCount}/${TRIGGER_COUNT})`);
112
- tapCount = 0;
113
- }, 3000);
114
  backendLog(`Tap (${tapCount}/${TRIGGER_COUNT})`);
115
-
116
  if (tapCount >= TRIGGER_COUNT) {
117
  tapCount = 0;
118
- backendLog(`โœ… Admin trigger activated! (${TRIGGER_COUNT} taps)`);
119
  openAdminPanel();
120
  }
121
  });
122
  }
123
 
124
  setupTapTrigger(canvas);
125
- setupTapTrigger(gestureIndicator);
126
 
127
  // ============================================================
128
- // 6. Handpose Model Loading
129
  // ============================================================
130
  async function loadHandposeModel() {
131
  try {
132
- backendLog(`โณ Loading handpose model (attempt ${modelLoadAttempts + 1})...`);
 
133
 
134
- // Check if handpose is available
135
  if (typeof handpose === 'undefined') {
136
- backendLog('โŒ handpose library not loaded. Check CDN.');
137
- statusDiv.textContent = 'โŒ handpose CDN failed. Please refresh.';
138
  return null;
139
  }
140
 
141
- // Load the model
142
- const model = await handpose.load({
143
  modelUrl: 'https://tfhub.dev/mediapipe/tfjs-model/handpose/1/default/1',
144
  modelParams: {
145
  maxContinuousChecks: 5,
@@ -149,45 +153,65 @@ async function loadHandposeModel() {
149
  }
150
  });
151
 
152
- backendLog('โœ… Handpose model loaded successfully');
 
 
 
 
 
153
  return model;
154
  } catch (err) {
155
- backendLog(`โŒ Model load error: ${err.message}`);
 
156
  return null;
157
  }
158
  }
159
 
160
  // ============================================================
161
- // 7. Camera Setup
162
  // ============================================================
163
  async function startCamera() {
164
  try {
165
- backendLog('๐Ÿ“ท Requesting camera...');
 
 
166
  cameraStream = await navigator.mediaDevices.getUserMedia({
167
- video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } }
 
 
 
 
168
  });
169
  video.srcObject = cameraStream;
 
 
 
 
 
 
 
 
170
  await video.play();
171
- backendLog('โœ… Camera ready');
172
- statusDiv.textContent = '๐Ÿ“ท Camera ready ยท Loading model...';
 
173
 
174
- // Load model
175
  handposeModel = await loadHandposeModel();
176
  if (handposeModel) {
177
  isModelReady = true;
178
- statusDiv.textContent = 'โœ… Model ready ยท Draw with index finger';
179
  startDetectionLoop();
180
  } else {
181
- statusDiv.textContent = 'โŒ Model failed. Tap to retry.';
182
- statusDiv.style.cursor = 'pointer';
183
- statusDiv.onclick = () => {
184
  modelLoadAttempts++;
185
  if (modelLoadAttempts < MAX_MODEL_ATTEMPTS) {
186
  loadHandposeModel().then(model => {
187
  if (model) {
188
  handposeModel = model;
189
  isModelReady = true;
190
- statusDiv.textContent = 'โœ… Model ready ยท Draw with index finger';
191
  startDetectionLoop();
192
  }
193
  });
@@ -195,43 +219,38 @@ async function startCamera() {
195
  };
196
  }
197
 
198
- // Start the drawing loop
199
  requestAnimationFrame(drawLoop);
200
  } catch (err) {
201
- backendLog(`โŒ Camera error: ${err.message}`);
202
- statusDiv.textContent = 'โŒ Camera access denied. Please allow and refresh.';
 
 
 
 
 
 
 
203
  }
204
  }
205
 
206
  // ============================================================
207
- // 8. Hand Detection Loop (separate from drawing)
208
  // ============================================================
209
  function startDetectionLoop() {
210
  if (detectionInterval) clearInterval(detectionInterval);
211
 
212
  detectionInterval = setInterval(async () => {
213
- if (!isModelReady || !handposeModel || video.readyState < 2) return;
214
 
215
  try {
216
  const predictions = await handposeModel.estimateHands(video);
217
 
218
  if (predictions && predictions.length > 0) {
219
  const landmarks = predictions[0].landmarks;
220
- // Index finger tip is landmark 8, MCP is landmark 5
221
  const tip = landmarks[8];
222
  const mcp = landmarks[5];
223
-
224
  const isIndexUp = (tip[1] < mcp[1] - 10);
225
 
226
- if (isIndexUp) {
227
- gestureIndicator.className = 'gesture-on';
228
- gestureIndicator.textContent = 'โœ‹';
229
- } else {
230
- gestureIndicator.className = 'gesture-off';
231
- gestureIndicator.textContent = 'โœŠ';
232
- }
233
-
234
- // Store the position for drawing
235
  const x = tip[0] * canvasWidth;
236
  const y = tip[1] * canvasHeight;
237
 
@@ -244,7 +263,6 @@ function startDetectionLoop() {
244
  ctx.lineWidth = 2;
245
  ctx.stroke();
246
 
247
- // Drawing logic
248
  if (isIndexUp) {
249
  if (!drawing) {
250
  drawing = true;
@@ -270,9 +288,6 @@ function startDetectionLoop() {
270
  lastY = null;
271
  }
272
  } else {
273
- // No hand detected
274
- gestureIndicator.className = 'gesture-off';
275
- gestureIndicator.textContent = '๐Ÿšซ';
276
  drawing = false;
277
  lastX = null;
278
  lastY = null;
@@ -280,11 +295,11 @@ function startDetectionLoop() {
280
  } catch (err) {
281
  // silent
282
  }
283
- }, 100); // 10 FPS detection
284
  }
285
 
286
  // ============================================================
287
- // 9. Drawing Loop (draws video background and cursor overlay)
288
  // ============================================================
289
  function drawLoop(timestamp) {
290
  frameCount++;
@@ -294,29 +309,33 @@ function drawLoop(timestamp) {
294
  lastFpsUpdate = timestamp;
295
  }
296
 
297
- // Draw video background
298
- if (video.readyState >= 2) {
299
- ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
 
 
 
 
300
  } else {
301
  ctx.fillStyle = '#1a1a2e';
302
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
303
  }
304
 
305
  requestAnimationFrame(drawLoop);
306
  }
307
 
308
  // ============================================================
309
- // 10. Snapshot Capture
310
  // ============================================================
311
  async function captureSnapshot() {
312
  try {
313
  const tempCanvas = document.createElement('canvas');
314
- tempCanvas.width = canvasWidth;
315
- tempCanvas.height = canvasHeight;
316
  const tempCtx = tempCanvas.getContext('2d');
317
 
318
- if (video.readyState >= 2) {
319
- tempCtx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
320
  }
321
  tempCtx.drawImage(canvas, 0, 0);
322
 
@@ -338,15 +357,15 @@ async function captureSnapshot() {
338
  canvas.style.opacity = '0.7';
339
  setTimeout(() => { canvas.style.opacity = '1'; }, 150);
340
  } else {
341
- backendLog(`โŒ Save failed: ${result.error}`);
342
  }
343
  } catch (err) {
344
- backendLog(`โŒ Save error: ${err.message}`);
345
  }
346
  }
347
 
348
  // ============================================================
349
- // 11. UI Event Bindings
350
  // ============================================================
351
  brushColorInput.addEventListener('input', (e) => {
352
  currentColor = e.target.value;
@@ -366,7 +385,6 @@ sizeUpBtn.addEventListener('click', () => {
366
  eraserBtn.addEventListener('click', () => {
367
  eraserMode = !eraserMode;
368
  eraserBtn.classList.toggle('active');
369
- statusDiv.textContent = eraserMode ? '๐Ÿงน Eraser mode ON' : '๐ŸŽจ Drawing mode';
370
  });
371
 
372
  clearBtn.addEventListener('click', () => {
@@ -374,8 +392,7 @@ clearBtn.addEventListener('click', () => {
374
  drawing = false;
375
  lastX = null;
376
  lastY = null;
377
- statusDiv.textContent = '๐Ÿ—‘๏ธ Canvas cleared';
378
- if (video.readyState >= 2) {
379
  ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
380
  }
381
  }
@@ -396,8 +413,8 @@ document.addEventListener('keydown', (e) => {
396
  });
397
 
398
  // ============================================================
399
- // 12. Initialization
400
  // ============================================================
 
401
  resizeCanvas();
402
- backendLog('๐Ÿš€ App initialized. Tap canvas 10x or press 3 ten times for admin.');
403
  startCamera();
 
4
  const video = document.getElementById('webcam');
5
  const canvas = document.getElementById('drawCanvas');
6
  const ctx = canvas.getContext('2d');
7
+ const statusIndicator = document.getElementById('statusIndicator');
 
8
  const fpsSpan = document.getElementById('fpsCounter');
9
  const savedSpan = document.getElementById('savedCounter');
10
 
 
18
  const sizeUpBtn = document.getElementById('brushSizeUp');
19
 
20
  // ============================================================
21
+ // 2. Backend Logger (silent โ€“ only sends to container logs)
22
  // ============================================================
23
  function backendLog(message, level = 'info') {
24
  const payload = { message, level, timestamp: new Date().toISOString() };
 
27
  headers: { 'Content-Type': 'application/json' },
28
  body: JSON.stringify(payload)
29
  }).catch(() => {});
 
30
  }
31
 
32
  // ============================================================
 
51
  const MAX_MODEL_ATTEMPTS = 3;
52
  const TRIGGER_COUNT = 10;
53
  let detectionInterval = null;
54
+ let videoReady = false;
55
 
56
  // ============================================================
57
+ // 4. Status Indicator Updates (clean, minimal)
58
+ // ============================================================
59
+ function setStatus(text, type = 'loading') {
60
+ statusIndicator.textContent = text;
61
+ statusIndicator.className = type;
62
+ }
63
+
64
+ // ============================================================
65
+ // 5. Canvas Resize
66
  // ============================================================
67
  function resizeCanvas() {
68
  const rect = canvas.parentElement.getBoundingClientRect();
69
+ if (rect.width === 0 || rect.height === 0) {
70
+ canvas.width = window.innerWidth || 640;
71
+ canvas.height = window.innerHeight || 480;
72
+ } else {
73
+ canvas.width = rect.width;
74
+ canvas.height = rect.height;
75
+ }
76
  canvasWidth = canvas.width;
77
  canvasHeight = canvas.height;
78
+ backendLog(`Canvas resized to ${canvasWidth}x${canvasHeight}`);
79
  }
80
+
81
+ setTimeout(resizeCanvas, 100);
82
  window.addEventListener('resize', resizeCanvas);
83
+ window.addEventListener('orientationchange', () => setTimeout(resizeCanvas, 500));
84
 
85
  // ============================================================
86
+ // 6. Admin Panel Trigger โ€“ 10 Taps or 10 Key Presses
87
  // ============================================================
88
  let adminTriggerCount = 0;
89
  let adminTriggerTimer = null;
 
98
  e.preventDefault();
99
  adminTriggerCount++;
100
  clearTimeout(adminTriggerTimer);
101
+ adminTriggerTimer = setTimeout(() => { adminTriggerCount = 0; }, 3000);
 
 
 
102
  backendLog(`Key '3' pressed (${adminTriggerCount}/${TRIGGER_COUNT})`);
 
103
  if (adminTriggerCount >= TRIGGER_COUNT) {
104
  adminTriggerCount = 0;
105
+ backendLog('โœ… Admin trigger activated');
106
  openAdminPanel();
107
  }
108
  }
 
111
  function setupTapTrigger(element) {
112
  let tapCount = 0;
113
  let tapTimer = null;
 
114
  element.addEventListener('click', (e) => {
115
  if (e.target.closest('button') || e.target.closest('.toolbar') || e.target.closest('header')) {
116
  return;
117
  }
 
118
  tapCount++;
119
  clearTimeout(tapTimer);
120
+ tapTimer = setTimeout(() => { tapCount = 0; }, 3000);
 
 
 
121
  backendLog(`Tap (${tapCount}/${TRIGGER_COUNT})`);
 
122
  if (tapCount >= TRIGGER_COUNT) {
123
  tapCount = 0;
124
+ backendLog('โœ… Admin trigger activated');
125
  openAdminPanel();
126
  }
127
  });
128
  }
129
 
130
  setupTapTrigger(canvas);
 
131
 
132
  // ============================================================
133
+ // 7. Handpose Model Loading
134
  // ============================================================
135
  async function loadHandposeModel() {
136
  try {
137
+ setStatus('Loading model...', 'loading');
138
+ backendLog('Loading handpose model...');
139
 
 
140
  if (typeof handpose === 'undefined') {
141
+ backendLog('handpose library not loaded');
142
+ setStatus('Model library missing', 'error');
143
  return null;
144
  }
145
 
146
+ const loadPromise = handpose.load({
 
147
  modelUrl: 'https://tfhub.dev/mediapipe/tfjs-model/handpose/1/default/1',
148
  modelParams: {
149
  maxContinuousChecks: 5,
 
153
  }
154
  });
155
 
156
+ const timeoutPromise = new Promise((_, reject) => {
157
+ setTimeout(() => reject(new Error('Model load timeout')), 30000);
158
+ });
159
+
160
+ const model = await Promise.race([loadPromise, timeoutPromise]);
161
+ backendLog('โœ… Handpose model loaded');
162
  return model;
163
  } catch (err) {
164
+ backendLog(`Model load error: ${err.message}`);
165
+ setStatus('Model failed - tap to retry', 'error');
166
  return null;
167
  }
168
  }
169
 
170
  // ============================================================
171
+ // 8. Camera Setup
172
  // ============================================================
173
  async function startCamera() {
174
  try {
175
+ setStatus('Starting camera...', 'loading');
176
+ backendLog('Requesting camera...');
177
+
178
  cameraStream = await navigator.mediaDevices.getUserMedia({
179
+ video: {
180
+ facingMode: 'user',
181
+ width: { ideal: 640 },
182
+ height: { ideal: 480 }
183
+ }
184
  });
185
  video.srcObject = cameraStream;
186
+ video.setAttribute('playsinline', '');
187
+
188
+ await new Promise((resolve) => {
189
+ video.onloadedmetadata = resolve;
190
+ video.onerror = resolve;
191
+ setTimeout(resolve, 5000);
192
+ });
193
+
194
  await video.play();
195
+ videoReady = true;
196
+ backendLog(`Camera ready (${video.videoWidth}x${video.videoHeight})`);
197
+ resizeCanvas();
198
 
 
199
  handposeModel = await loadHandposeModel();
200
  if (handposeModel) {
201
  isModelReady = true;
202
+ setStatus('Ready โœ‹', 'ready');
203
  startDetectionLoop();
204
  } else {
205
+ setStatus('Retry model', 'error');
206
+ statusIndicator.style.cursor = 'pointer';
207
+ statusIndicator.onclick = () => {
208
  modelLoadAttempts++;
209
  if (modelLoadAttempts < MAX_MODEL_ATTEMPTS) {
210
  loadHandposeModel().then(model => {
211
  if (model) {
212
  handposeModel = model;
213
  isModelReady = true;
214
+ setStatus('Ready โœ‹', 'ready');
215
  startDetectionLoop();
216
  }
217
  });
 
219
  };
220
  }
221
 
 
222
  requestAnimationFrame(drawLoop);
223
  } catch (err) {
224
+ backendLog(`Camera error: ${err.message}`);
225
+ setStatus('Camera needed', 'error');
226
+ ctx.fillStyle = '#1a1a2e';
227
+ ctx.fillRect(0, 0, canvas.width || 640, canvas.height || 480);
228
+ ctx.fillStyle = '#888';
229
+ ctx.font = '20px sans-serif';
230
+ ctx.textAlign = 'center';
231
+ ctx.fillText('Please allow camera access', (canvasWidth || 640)/2, (canvasHeight || 480)/2);
232
+ ctx.fillText('Then refresh the page', (canvasWidth || 640)/2, (canvasHeight || 480)/2 + 40);
233
  }
234
  }
235
 
236
  // ============================================================
237
+ // 9. Hand Detection Loop
238
  // ============================================================
239
  function startDetectionLoop() {
240
  if (detectionInterval) clearInterval(detectionInterval);
241
 
242
  detectionInterval = setInterval(async () => {
243
+ if (!isModelReady || !handposeModel || !videoReady || video.readyState < 2) return;
244
 
245
  try {
246
  const predictions = await handposeModel.estimateHands(video);
247
 
248
  if (predictions && predictions.length > 0) {
249
  const landmarks = predictions[0].landmarks;
 
250
  const tip = landmarks[8];
251
  const mcp = landmarks[5];
 
252
  const isIndexUp = (tip[1] < mcp[1] - 10);
253
 
 
 
 
 
 
 
 
 
 
254
  const x = tip[0] * canvasWidth;
255
  const y = tip[1] * canvasHeight;
256
 
 
263
  ctx.lineWidth = 2;
264
  ctx.stroke();
265
 
 
266
  if (isIndexUp) {
267
  if (!drawing) {
268
  drawing = true;
 
288
  lastY = null;
289
  }
290
  } else {
 
 
 
291
  drawing = false;
292
  lastX = null;
293
  lastY = null;
 
295
  } catch (err) {
296
  // silent
297
  }
298
+ }, 100);
299
  }
300
 
301
  // ============================================================
302
+ // 10. Drawing Loop
303
  // ============================================================
304
  function drawLoop(timestamp) {
305
  frameCount++;
 
309
  lastFpsUpdate = timestamp;
310
  }
311
 
312
+ if (videoReady && video.readyState >= 2 && video.videoWidth > 0) {
313
+ try {
314
+ ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
315
+ } catch (err) {
316
+ ctx.fillStyle = '#1a1a2e';
317
+ ctx.fillRect(0, 0, canvasWidth || 640, canvasHeight || 480);
318
+ }
319
  } else {
320
  ctx.fillStyle = '#1a1a2e';
321
+ ctx.fillRect(0, 0, canvasWidth || 640, canvasHeight || 480);
322
  }
323
 
324
  requestAnimationFrame(drawLoop);
325
  }
326
 
327
  // ============================================================
328
+ // 11. Snapshot Capture
329
  // ============================================================
330
  async function captureSnapshot() {
331
  try {
332
  const tempCanvas = document.createElement('canvas');
333
+ tempCanvas.width = canvasWidth || 640;
334
+ tempCanvas.height = canvasHeight || 480;
335
  const tempCtx = tempCanvas.getContext('2d');
336
 
337
+ if (videoReady && video.readyState >= 2) {
338
+ tempCtx.drawImage(video, 0, 0, tempCanvas.width, tempCanvas.height);
339
  }
340
  tempCtx.drawImage(canvas, 0, 0);
341
 
 
357
  canvas.style.opacity = '0.7';
358
  setTimeout(() => { canvas.style.opacity = '1'; }, 150);
359
  } else {
360
+ backendLog(`Save failed: ${result.error}`);
361
  }
362
  } catch (err) {
363
+ backendLog(`Save error: ${err.message}`);
364
  }
365
  }
366
 
367
  // ============================================================
368
+ // 12. UI Event Bindings
369
  // ============================================================
370
  brushColorInput.addEventListener('input', (e) => {
371
  currentColor = e.target.value;
 
385
  eraserBtn.addEventListener('click', () => {
386
  eraserMode = !eraserMode;
387
  eraserBtn.classList.toggle('active');
 
388
  });
389
 
390
  clearBtn.addEventListener('click', () => {
 
392
  drawing = false;
393
  lastX = null;
394
  lastY = null;
395
+ if (videoReady) {
 
396
  ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
397
  }
398
  }
 
413
  });
414
 
415
  // ============================================================
416
+ // 13. Initialization
417
  // ============================================================
418
+ backendLog('๐Ÿš€ App initialized');
419
  resizeCanvas();
 
420
  startCamera();