VISHAL18for4 commited on
Commit
860a340
Β·
verified Β·
1 Parent(s): 8bcb7e5

Create script.js

Browse files
Files changed (1) hide show
  1. public/script.js +327 -0
public/script.js ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // 1. DOM References
3
+ // ============================================================
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
+
12
+ // Toolbar elements
13
+ const brushColorInput = document.getElementById('brushColor');
14
+ const sizeDisplay = document.getElementById('sizeDisplay');
15
+ const eraserBtn = document.getElementById('eraserToggle');
16
+ const clearBtn = document.getElementById('clearCanvas');
17
+ const snapshotBtn = document.getElementById('snapshotBtn');
18
+ const toggleCamBtn = document.getElementById('toggleCamera');
19
+ const sizeDownBtn = document.getElementById('brushSizeDown');
20
+ const sizeUpBtn = document.getElementById('brushSizeUp');
21
+
22
+ // ============================================================
23
+ // 2. State Variables
24
+ // ============================================================
25
+ let drawing = false;
26
+ let lastX = null;
27
+ let lastY = null;
28
+ let currentColor = '#FF4500';
29
+ let brushSize = 5;
30
+ let eraserMode = false;
31
+ let cameraVisible = true;
32
+ let savedCount = 0;
33
+ let frameCount = 0;
34
+ let lastFpsUpdate = performance.now();
35
+ let handLandmarker = null;
36
+ let cameraStream = null;
37
+ let isModelReady = false;
38
+ let canvasWidth = 0;
39
+ let canvasHeight = 0;
40
+
41
+ // ============================================================
42
+ // 3. Canvas Resize
43
+ // ============================================================
44
+ function resizeCanvas() {
45
+ const rect = canvas.parentElement.getBoundingClientRect();
46
+ canvas.width = rect.width;
47
+ canvas.height = rect.height;
48
+ canvasWidth = canvas.width;
49
+ canvasHeight = canvas.height;
50
+ // Redraw background (if any) – we draw video each frame anyway
51
+ }
52
+
53
+ window.addEventListener('resize', resizeCanvas);
54
+
55
+ // ============================================================
56
+ // 4. MediaPipe Hand Landmarker Initialization
57
+ // ============================================================
58
+ async function initHandLandmarker() {
59
+ try {
60
+ statusDiv.textContent = '⏳ Loading hand model...';
61
+
62
+ // Use the FilesetResolver to load wasm and model
63
+ const vision = await FilesetResolver.forVisionTasks(
64
+ 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.8/wasm'
65
+ );
66
+
67
+ handLandmarker = await HandLandmarker.createFromOptions(vision, {
68
+ baseOptions: {
69
+ modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task',
70
+ delegate: 'GPU' // or 'CPU' if GPU fails
71
+ },
72
+ runningMode: 'VIDEO',
73
+ numHands: 1
74
+ });
75
+
76
+ isModelReady = true;
77
+ statusDiv.textContent = 'βœ… Model ready Β· Move index finger up to draw';
78
+ console.log('[MediaPipe] HandLandmarker initialized');
79
+ } catch (err) {
80
+ console.error('[MediaPipe] Initialization error:', err);
81
+ statusDiv.textContent = '❌ Failed to load hand model. Please refresh.';
82
+ }
83
+ }
84
+
85
+ // ============================================================
86
+ // 5. Camera Setup
87
+ // ============================================================
88
+ async function startCamera() {
89
+ try {
90
+ cameraStream = await navigator.mediaDevices.getUserMedia({
91
+ video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } }
92
+ });
93
+ video.srcObject = cameraStream;
94
+ await video.play();
95
+ statusDiv.textContent = 'πŸ“· Camera ready Β· Loading hand model...';
96
+ // Wait a moment then initialize model
97
+ initHandLandmarker();
98
+ // Start the main loop
99
+ requestAnimationFrame(drawLoop);
100
+ } catch (err) {
101
+ console.error('[Camera] Error:', err);
102
+ statusDiv.textContent = '❌ Camera access denied. Please allow camera permissions.';
103
+ alert('Camera access is required for hand tracking. Please allow permissions and refresh.');
104
+ }
105
+ }
106
+
107
+ // ============================================================
108
+ // 6. Core Drawing Loop (requestAnimationFrame)
109
+ // ============================================================
110
+ function drawLoop(timestamp) {
111
+ // FPS counter
112
+ frameCount++;
113
+ if (timestamp - lastFpsUpdate >= 1000) {
114
+ fpsSpan.textContent = `${frameCount} FPS`;
115
+ frameCount = 0;
116
+ lastFpsUpdate = timestamp;
117
+ }
118
+
119
+ // 6a. Draw video background onto canvas
120
+ if (video.readyState >= 2) {
121
+ ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
122
+ } else {
123
+ // If video not ready, draw a dark background
124
+ ctx.fillStyle = '#1a1a2e';
125
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight);
126
+ }
127
+
128
+ // 6b. Run hand detection if model is ready
129
+ if (isModelReady && handLandmarker && video.readyState >= 2) {
130
+ try {
131
+ const results = handLandmarker.detectForVideo(video, performance.now());
132
+
133
+ if (results.landmarks && results.landmarks.length > 0) {
134
+ const lm = results.landmarks[0];
135
+ // Landmark 8 = index finger tip, 5 = index finger MCP (knuckle)
136
+ const tip = lm[8];
137
+ const mcp = lm[5];
138
+
139
+ // Gesture detection: tip.y < mcp.y - threshold => finger pointing up
140
+ const isIndexUp = (tip.y < mcp.y - 0.02);
141
+
142
+ // Update gesture indicator
143
+ if (isIndexUp) {
144
+ gestureIndicator.className = 'gesture-on';
145
+ gestureIndicator.textContent = 'βœ‹';
146
+ } else {
147
+ gestureIndicator.className = 'gesture-off';
148
+ gestureIndicator.textContent = '✊';
149
+ }
150
+
151
+ // Convert normalized coords to canvas pixels
152
+ const x = tip.x * canvasWidth;
153
+ const y = tip.y * canvasHeight;
154
+
155
+ // Drawing logic
156
+ if (isIndexUp) {
157
+ if (!drawing) {
158
+ // Start new stroke
159
+ drawing = true;
160
+ lastX = x;
161
+ lastY = y;
162
+ } else {
163
+ // Continue drawing
164
+ if (lastX !== null && lastY !== null) {
165
+ ctx.beginPath();
166
+ ctx.moveTo(lastX, lastY);
167
+ ctx.lineTo(x, y);
168
+ ctx.strokeStyle = eraserMode ? '#1a1a2e' : currentColor; // eraser matches background
169
+ ctx.lineWidth = eraserMode ? brushSize * 2 : brushSize;
170
+ ctx.lineCap = 'round';
171
+ ctx.lineJoin = 'round';
172
+ ctx.stroke();
173
+ }
174
+ lastX = x;
175
+ lastY = y;
176
+ }
177
+ } else {
178
+ // Finger down – stop drawing
179
+ drawing = false;
180
+ lastX = null;
181
+ lastY = null;
182
+ }
183
+
184
+ // Optional: Draw a small cursor at fingertip
185
+ ctx.beginPath();
186
+ ctx.arc(x, y, 4, 0, 2 * Math.PI);
187
+ ctx.fillStyle = eraserMode ? '#ff6666' : currentColor;
188
+ ctx.fill();
189
+ ctx.strokeStyle = '#fff';
190
+ ctx.lineWidth = 1;
191
+ ctx.stroke();
192
+
193
+ } else {
194
+ // No hand detected
195
+ gestureIndicator.className = 'gesture-off';
196
+ gestureIndicator.textContent = '🚫';
197
+ drawing = false;
198
+ lastX = null;
199
+ lastY = null;
200
+ }
201
+ } catch (err) {
202
+ // Silently continue if detection fails
203
+ }
204
+ }
205
+
206
+ // Continue the loop
207
+ requestAnimationFrame(drawLoop);
208
+ }
209
+
210
+ // ============================================================
211
+ // 7. Snapshot Capture and Save to Backend
212
+ // ============================================================
213
+ async function captureSnapshot() {
214
+ try {
215
+ // Create a temporary canvas that combines video and drawing
216
+ const tempCanvas = document.createElement('canvas');
217
+ tempCanvas.width = canvasWidth;
218
+ tempCanvas.height = canvasHeight;
219
+ const tempCtx = tempCanvas.getContext('2d');
220
+
221
+ // Draw current video frame (if available)
222
+ if (video.readyState >= 2) {
223
+ tempCtx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
224
+ }
225
+ // Overlay the drawing canvas (which already contains the video background + strokes)
226
+ tempCtx.drawImage(canvas, 0, 0);
227
+
228
+ // Convert to PNG base64 (without the header)
229
+ const dataURL = tempCanvas.toDataURL('image/png');
230
+ const base64 = dataURL.split(',')[1];
231
+
232
+ // Send to backend
233
+ const response = await fetch('/api/snapshot', {
234
+ method: 'POST',
235
+ headers: { 'Content-Type': 'application/json' },
236
+ body: JSON.stringify({ image: base64 })
237
+ });
238
+
239
+ const result = await response.json();
240
+ if (result.success) {
241
+ savedCount++;
242
+ savedSpan.textContent = `Saved: ${savedCount}`;
243
+ statusDiv.textContent = `βœ… Snapshot saved: ${result.filename}`;
244
+ // Optional: show a brief flash
245
+ canvas.style.transition = 'opacity 0.1s';
246
+ canvas.style.opacity = '0.7';
247
+ setTimeout(() => { canvas.style.opacity = '1'; }, 150);
248
+ } else {
249
+ statusDiv.textContent = `❌ Save failed: ${result.error}`;
250
+ }
251
+ } catch (err) {
252
+ console.error('[Snapshot] Error:', err);
253
+ statusDiv.textContent = '❌ Network error while saving snapshot.';
254
+ }
255
+ }
256
+
257
+ // ============================================================
258
+ // 8. UI Event Bindings
259
+ // ============================================================
260
+
261
+ // Color picker
262
+ brushColorInput.addEventListener('input', (e) => {
263
+ currentColor = e.target.value;
264
+ eraserMode = false;
265
+ eraserBtn.classList.remove('active');
266
+ });
267
+
268
+ // Brush size
269
+ sizeDownBtn.addEventListener('click', () => {
270
+ brushSize = Math.max(1, brushSize - 1);
271
+ sizeDisplay.textContent = brushSize;
272
+ });
273
+ sizeUpBtn.addEventListener('click', () => {
274
+ brushSize = Math.min(50, brushSize + 1);
275
+ sizeDisplay.textContent = brushSize;
276
+ });
277
+
278
+ // Eraser toggle
279
+ eraserBtn.addEventListener('click', () => {
280
+ eraserMode = !eraserMode;
281
+ eraserBtn.classList.toggle('active');
282
+ statusDiv.textContent = eraserMode ? '🧹 Eraser mode ON' : '🎨 Drawing mode';
283
+ });
284
+
285
+ // Clear canvas
286
+ clearBtn.addEventListener('click', () => {
287
+ if (confirm('Clear all drawings?')) {
288
+ // We redraw the video background; but since we draw video each frame,
289
+ // just reset the drawing state and clear the overlay by drawing video again.
290
+ // The loop will overwrite. But we also need to clear any stored strokes.
291
+ // Simplest: reset drawing and let the loop draw video.
292
+ drawing = false;
293
+ lastX = null;
294
+ lastY = null;
295
+ statusDiv.textContent = 'πŸ—‘οΈ Canvas cleared';
296
+ // Force a redraw of video background
297
+ if (video.readyState >= 2) {
298
+ ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
299
+ }
300
+ }
301
+ });
302
+
303
+ // Snapshot button
304
+ snapshotBtn.addEventListener('click', captureSnapshot);
305
+
306
+ // Toggle camera visibility (just hides the video element, but canvas still shows)
307
+ toggleCamBtn.addEventListener('click', () => {
308
+ cameraVisible = !cameraVisible;
309
+ video.style.display = cameraVisible ? 'block' : 'none';
310
+ toggleCamBtn.classList.toggle('active');
311
+ });
312
+
313
+ // Keyboard shortcuts
314
+ document.addEventListener('keydown', (e) => {
315
+ if (e.key === 's' || e.key === 'S') captureSnapshot();
316
+ if (e.key === 'e' || e.key === 'E') eraserBtn.click();
317
+ if (e.key === 'c' || e.key === 'C') clearBtn.click();
318
+ });
319
+
320
+ // ============================================================
321
+ // 9. Initialization
322
+ // ============================================================
323
+ resizeCanvas();
324
+ startCamera();
325
+
326
+ // Log ready
327
+ console.log('[Gesture Draw] App initialized. Waiting for camera and model.');