DarqueDante commited on
Commit
bac966b
·
verified ·
1 Parent(s): 059db28

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +579 -17
index.html CHANGED
@@ -1,19 +1,581 @@
1
  <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AR Drawing Overlay</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ padding: 0;
11
+ font-family: sans-serif;
12
+ background-color: #f9f9f9;
13
+ text-align: center;
14
+ overflow: hidden; /* Prevent scrollbars */
15
+ }
16
+ #canvas {
17
+ width: 100%;
18
+ height: 85vh;
19
+ max-width: 600px;
20
+ border: 1px solid #ccc;
21
+ touch-action: none;
22
+ display: block;
23
+ margin: 0 auto;
24
+ }
25
+ #controls {
26
+ position: fixed;
27
+ bottom: 0;
28
+ left: 0;
29
+ width: 100%;
30
+ background: rgba(255, 255, 255, 0.9);
31
+ padding: 5px;
32
+ box-sizing: border-box;
33
+ display: none; /* Hidden by default */
34
+ max-height: 25vh;
35
+ overflow-y: auto;
36
+ }
37
+ button {
38
+ font-size: 1em;
39
+ padding: 5px;
40
+ margin: 2px;
41
+ min-width: 40px;
42
+ min-height: 40px;
43
+ border-radius: 5px;
44
+ border: 1px solid #aaa;
45
+ background-color: #f0f0f0;
46
+ }
47
+ button.active {
48
+ background-color: #a0c4ff; /* Highlight active toggles */
49
+ border-color: #6a9eff;
50
+ }
51
+ #toggleControls {
52
+ position: fixed;
53
+ bottom: 10px;
54
+ right: 10px;
55
+ font-size: 1.5em;
56
+ background: #fff;
57
+ border: 1px solid #ccc;
58
+ border-radius: 50%;
59
+ width: 40px;
60
+ height: 40px;
61
+ z-index: 100;
62
+ }
63
+ #errorMessage {
64
+ color: red;
65
+ font-size: 0.8rem;
66
+ margin: 5px 0;
67
+ }
68
+ #tutorialModal {
69
+ display: none;
70
+ position: fixed;
71
+ top: 0;
72
+ left: 0;
73
+ width: 100%;
74
+ height: 100%;
75
+ background: rgba(0,0,0,0.5);
76
+ z-index: 101;
77
+ }
78
+ #tutorialContent {
79
+ background: white;
80
+ margin: 10% auto;
81
+ padding: 10px;
82
+ width: 90%;
83
+ max-width: 350px;
84
+ text-align: left;
85
+ font-size: 0.8rem;
86
+ border-radius: 8px;
87
+ }
88
+ .control-row {
89
+ display: flex;
90
+ flex-wrap: wrap;
91
+ justify-content: center;
92
+ gap: 5px;
93
+ margin-bottom: 5px;
94
+ }
95
+ .control-row label, .control-row select, .control-row input {
96
+ font-size: 0.8em;
97
+ }
98
+ #toastNotification {
99
+ position: fixed;
100
+ bottom: 70px; /* Above controls button */
101
+ left: 50%;
102
+ transform: translateX(-50%);
103
+ background-color: rgba(0, 0, 0, 0.7);
104
+ color: white;
105
+ padding: 10px 20px;
106
+ border-radius: 20px;
107
+ z-index: 102;
108
+ opacity: 0;
109
+ transition: opacity 0.5s ease-in-out;
110
+ pointer-events: none; /* Don't block clicks */
111
+ }
112
+ </style>
113
+ </head>
114
+ <body>
115
+ <h2>AR Drawing Overlay</h2>
116
+ <p style="font-size: 0.9rem;">Trace pictures! Tap ≡ for controls.</p>
117
+ <div id="errorMessage"></div>
118
+ <video id="video" autoplay playsinline style="display: none;"></video>
119
+ <canvas id="canvas"></canvas>
120
+ <button id="toggleControls" aria-label="Toggle controls panel">≡</button>
121
+ <div id="controls">
122
+ <div class="control-row">
123
+ <label for="cameraSelect">Pick Camera:</label>
124
+ <select id="cameraSelect" onchange="switchCamera()">
125
+ <option value="">Select a camera</option>
126
+ </select>
127
+ </div>
128
+ <div class="control-row">
129
+ <label for="overlayImage">Pick Image:</label>
130
+ <input type="file" id="overlayImage" accept="image/png, image/jpeg, image/bmp, image/gif" />
131
+ </div>
132
+ <div class="control-row">
133
+ <label for="transparency">Transparency:</label>
134
+ <input type="range" id="transparency" min="0" max="1" step="0.01" value="0.5" style="width: 80px;" />
135
+ <span id="transparencyValue">0.5</span>
136
+ </div>
137
+ <div class="control-row">
138
+ <button onclick="moveLeft()" aria-label="Move overlay left">←</button>
139
+ <button onclick="moveUp()" aria-label="Move overlay up">↑</button>
140
+ <button onclick="moveDown()" aria-label="Move overlay down">↓</button>
141
+ <button onclick="moveRight()" aria-label="Move overlay right">→</button>
142
+ <button onclick="zoomIn()" aria-label="Zoom in overlay">+</button>
143
+ <button onclick="zoomOut()" aria-label="Zoom out overlay">-</button>
144
+ </div>
145
+ <div class="control-row">
146
+ <button onclick="rotateLeft()" aria-label="Rotate left">↺</button>
147
+ <button onclick="rotateRight()" aria-label="Rotate right">↻</button>
148
+ <button onclick="flipHorizontal()" aria-label="Flip horizontally">Flip H</button>
149
+ <button onclick="flipVertical()" aria-label="Flip vertically">Flip V</button>
150
+ <button onclick="reset()" aria-label="Reset overlay">Reset</button>
151
+ </div>
152
+ <div class="control-row">
153
+ <button id="toggleCameraBtn" onclick="toggleCamera()" aria-label="Pause camera">Pause</button>
154
+ <button id="edgeDetectBtn" onclick="toggleEdgeDetection()" aria-label="Toggle edge detection">Edges</button>
155
+ <button id="lockBtn" onclick="toggleLock()" aria-label="Lock overlay position">🔒</button>
156
+ <button onclick="takeSnapshot()" aria-label="Take snapshot">Snap</button>
157
+ </div>
158
+ <div class="control-row">
159
+ <button onclick="saveConfig()" aria-label="Save configuration">Save</button>
160
+ <button onclick="loadConfig()" aria-label="Load configuration">Load</button>
161
+ <button id="tutorialBtn" aria-label="Show tutorial">Help</button>
162
+ </div>
163
+ </div>
164
+
165
+ <div id="toastNotification"></div>
166
+
167
+ <!-- Tutorial Modal -->
168
+ <div id="tutorialModal">
169
+ <div id="tutorialContent">
170
+ <h3>How to Use the AR Drawing Overlay</h3>
171
+ <p>Hi! This helps you trace pictures:</p>
172
+ <ol>
173
+ <li><strong>Pick a Camera:</strong> Tap ≡, then choose a camera.</li>
174
+ <li><strong>Pick a Picture:</strong> Tap "Pick Image" to choose a picture.</li>
175
+ <li><strong>Adjust Overlay:</strong>
176
+ <ul>
177
+ <li>Use the slider for <strong>Transparency</strong>.</li>
178
+ <li>Use ← ↑ ↓ → to <strong>move</strong> it. Drag with one finger.</li>
179
+ <li>Use + and - to <strong>zoom</strong>. Pinch with two fingers.</li>
180
+ <li>Use ↺ and ↻ to <strong>rotate</strong>. Twist with two fingers.</li>
181
+ <li>Use <strong>Flip H/V</strong> to mirror the image.</li>
182
+ </ul>
183
+ </li>
184
+ <li><strong>Lock It:</strong> Tap 🔒 to lock the overlay in place.</li>
185
+ <li><strong>Trace Easier:</strong> Tap "Edges" to show only the outlines of your image.</li>
186
+ <li><strong>Pause/Snap:</strong> Tap "Pause" to freeze the camera, and "Snap" to save a picture.</li>
187
+ <li><strong>Save/Load:</strong> Save your setup for later with "Save" and "Load".</li>
188
+ </ol>
189
+ <p>Point your camera at paper and draw!</p>
190
+ <button onclick="closeTutorial()">Close</button>
191
+ </div>
192
+ </div>
193
+
194
+ <script>
195
+ // --- Global State Variables ---
196
+ let S_user = 1;
197
+ let offsetXFraction = 0;
198
+ let offsetYFraction = 0;
199
+ let rotationAngle = 0; // In radians
200
+ let isFlippedHorizontal = false;
201
+ let isFlippedVertical = false;
202
+ let isLocked = false;
203
+ let edgeDetectionEnabled = false;
204
+
205
+ let cameraPaused = false;
206
+ let currentStream = null;
207
+ let canvasRect;
208
+ let selectedCameraId = null;
209
+
210
+ // Image objects
211
+ let originalOverlayImg = new Image();
212
+ let displayOverlayImg = new Image(); // This is what gets drawn
213
+ let overlayLoaded = false;
214
+
215
+ // --- DOM Elements ---
216
+ const canvas = document.getElementById("canvas");
217
+ const video = document.getElementById("video");
218
+ const errorMessage = document.getElementById("errorMessage");
219
+ const cameraSelect = document.getElementById("cameraSelect");
220
+ const transparencySlider = document.getElementById("transparency");
221
+ const transparencyValueDisplay = document.getElementById("transparencyValue");
222
+ const lockBtn = document.getElementById("lockBtn");
223
+ const edgeDetectBtn = document.getElementById("edgeDetectBtn");
224
+
225
+ // --- Camera Functions ---
226
+ async function populateCameraOptions() {
227
+ if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
228
+ errorMessage.textContent = "Camera selection not supported.";
229
+ return;
230
+ }
231
+ try {
232
+ const devices = await navigator.mediaDevices.enumerateDevices();
233
+ const videoDevices = devices.filter(device => device.kind === "videoinput");
234
+ cameraSelect.innerHTML = "<option value=''>Select a camera</option>";
235
+ videoDevices.forEach((device, index) => {
236
+ const option = document.createElement("option");
237
+ option.value = device.deviceId;
238
+ option.text = device.label || `Camera ${index + 1}`;
239
+ cameraSelect.appendChild(option);
240
+ });
241
+ if (videoDevices.length > 0) {
242
+ cameraSelect.value = videoDevices[0].deviceId;
243
+ selectedCameraId = videoDevices[0].deviceId;
244
+ }
245
+ } catch (error) {
246
+ console.error("Error listing cameras:", error);
247
+ errorMessage.textContent = "Error finding cameras: " + error.message;
248
+ }
249
+ }
250
+
251
+ function startCamera() {
252
+ if (currentStream) {
253
+ currentStream.getTracks().forEach(track => track.stop());
254
+ currentStream = null;
255
+ }
256
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
257
+ errorMessage.textContent = "Your browser doesn’t support the camera.";
258
+ return;
259
+ }
260
+ const constraints = selectedCameraId
261
+ ? { video: { deviceId: { exact: selectedCameraId } } }
262
+ : { video: { facingMode: "environment" } };
263
+ navigator.mediaDevices
264
+ .getUserMedia(constraints)
265
+ .then((stream) => {
266
+ currentStream = stream;
267
+ video.srcObject = stream;
268
+ video.onloadedmetadata = () => video.play();
269
+ errorMessage.textContent = "";
270
+ })
271
+ .catch((error) => {
272
+ console.error("Camera error:", error);
273
+ errorMessage.textContent = "Camera error: " + error.message;
274
+ });
275
+ }
276
+
277
+ function switchCamera() {
278
+ selectedCameraId = cameraSelect.value;
279
+ if (selectedCameraId && !cameraPaused) startCamera();
280
+ }
281
+
282
+ function toggleCamera() {
283
+ const toggleBtn = document.getElementById("toggleCameraBtn");
284
+ cameraPaused = !cameraPaused;
285
+ if (cameraPaused) {
286
+ if (currentStream) {
287
+ currentStream.getTracks().forEach(track => track.stop());
288
+ currentStream = null;
289
+ }
290
+ toggleBtn.textContent = "Play";
291
+ } else {
292
+ startCamera();
293
+ toggleBtn.textContent = "Pause";
294
+ }
295
+ }
296
+
297
+ // --- Overlay Transformation Controls ---
298
+ function moveLeft() { if (isLocked) return; offsetXFraction -= 0.05; }
299
+ function moveRight() { if (isLocked) return; offsetXFraction += 0.05; }
300
+ function moveUp() { if (isLocked) return; offsetYFraction -= 0.05; }
301
+ function moveDown() { if (isLocked) return; offsetYFraction += 0.05; }
302
+ function zoomIn() { if (isLocked) return; S_user *= 1.1; }
303
+ function zoomOut() { if (isLocked) return; S_user /= 1.1; }
304
+ function rotateLeft() { if (isLocked) return; rotationAngle -= Math.PI / 18; } // 10 degrees
305
+ function rotateRight() { if (isLocked) return; rotationAngle += Math.PI / 18; }
306
+ function flipHorizontal() { if (isLocked) return; isFlippedHorizontal = !isFlippedHorizontal; }
307
+ function flipVertical() { if (isLocked) return; isFlippedVertical = !isFlippedVertical; }
308
+
309
+ function reset() {
310
+ if (isLocked) return;
311
+ S_user = 1;
312
+ offsetXFraction = 0;
313
+ offsetYFraction = 0;
314
+ rotationAngle = 0;
315
+ isFlippedHorizontal = false;
316
+ isFlippedVertical = false;
317
+ if (edgeDetectionEnabled) toggleEdgeDetection(); // Turn off edge detection
318
+ showToast("Overlay Reset");
319
+ }
320
+
321
+ function toggleLock() {
322
+ isLocked = !isLocked;
323
+ lockBtn.textContent = isLocked ? "🔓" : "🔒";
324
+ lockBtn.classList.toggle('active', isLocked);
325
+ showToast(isLocked ? "Overlay Locked" : "Overlay Unlocked");
326
+ }
327
+
328
+ // --- Image Handling & Processing ---
329
+ document.getElementById("overlayImage").addEventListener("change", (e) => {
330
+ const file = e.target.files[0];
331
+ if (file) {
332
+ const reader = new FileReader();
333
+ reader.onload = (event) => {
334
+ originalOverlayImg.src = event.target.result;
335
+ };
336
+ reader.readAsDataURL(file);
337
+ }
338
+ });
339
+
340
+ originalOverlayImg.onload = () => {
341
+ overlayLoaded = true;
342
+ if (edgeDetectionEnabled) {
343
+ applyEdgeDetection();
344
+ } else {
345
+ displayOverlayImg.src = originalOverlayImg.src;
346
+ }
347
+ errorMessage.textContent = "";
348
+ showToast("Image loaded");
349
+ };
350
+
351
+ function toggleEdgeDetection() {
352
+ if (!overlayLoaded) {
353
+ showToast("Please load an image first.");
354
+ return;
355
+ }
356
+ edgeDetectionEnabled = !edgeDetectionEnabled;
357
+ edgeDetectBtn.classList.toggle('active', edgeDetectionEnabled);
358
+ if (edgeDetectionEnabled) {
359
+ showToast("Applying edge detection...");
360
+ setTimeout(applyEdgeDetection, 10); // Use timeout to show toast first
361
+ } else {
362
+ displayOverlayImg.src = originalOverlayImg.src;
363
+ showToast("Edge detection off.");
364
+ }
365
+ }
366
+
367
+ function applyEdgeDetection() {
368
+ const offscreenCanvas = document.createElement('canvas');
369
+ const offscreenCtx = offscreenCanvas.getContext('2d', { willReadFrequently: true });
370
+ offscreenCanvas.width = originalOverlayImg.naturalWidth;
371
+ offscreenCanvas.height = originalOverlayImg.naturalHeight;
372
+
373
+ // 1. Grayscale
374
+ offscreenCtx.drawImage(originalOverlayImg, 0, 0);
375
+ const imageData = offscreenCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height);
376
+ const data = imageData.data;
377
+ for (let i = 0; i < data.length; i += 4) {
378
+ const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
379
+ data[i] = data[i + 1] = data[i + 2] = avg;
380
+ }
381
+
382
+ // 2. Sobel Operator for edge detection
383
+ const sobelData = sobel(imageData);
384
+ const sobelImageData = offscreenCtx.createImageData(offscreenCanvas.width, offscreenCanvas.height);
385
+ for (let i = 0; i < sobelData.data.length; i += 4) {
386
+ const magnitude = sobelData.data[i];
387
+ sobelImageData.data[i] = magnitude;
388
+ sobelImageData.data[i + 1] = magnitude;
389
+ sobelImageData.data[i + 2] = magnitude;
390
+ sobelImageData.data[i + 3] = 255;
391
+ }
392
+ offscreenCtx.putImageData(sobelImageData, 0, 0);
393
+
394
+ displayOverlayImg.src = offscreenCanvas.toDataURL();
395
+ showToast("Edge detection applied.");
396
+ }
397
+
398
+ // Sobel operator function (simplified)
399
+ function sobel(imageData) {
400
+ const width = imageData.width;
401
+ const height = imageData.height;
402
+ const grayscaleData = new Uint8ClampedArray(width * height);
403
+ for (let i = 0; i < imageData.data.length; i += 4) {
404
+ grayscaleData[i / 4] = imageData.data[i];
405
+ }
406
+
407
+ const kernelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
408
+ const kernelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
409
+ const sobelData = new Uint8ClampedArray(width * height * 4);
410
+
411
+ for (let y = 1; y < height - 1; y++) {
412
+ for (let x = 1; x < width - 1; x++) {
413
+ let pixelX = 0;
414
+ let pixelY = 0;
415
+ for (let j = -1; j <= 1; j++) {
416
+ for (let i = -1; i <= 1; i++) {
417
+ const pixel = grayscaleData[(y + j) * width + (x + i)];
418
+ pixelX += pixel * kernelX[j + 1][i + 1];
419
+ pixelY += pixel * kernelY[j + 1][i + 1];
420
+ }
421
+ }
422
+ const magnitude = Math.sqrt(pixelX * pixelX + pixelY * pixelY) > 128 ? 0 : 255;
423
+ const index = (y * width + x) * 4;
424
+ sobelData[index] = sobelData[index + 1] = sobelData[index + 2] = magnitude;
425
+ sobelData[index + 3] = 255;
426
+ }
427
+ }
428
+ return { data: sobelData, width: width, height: height };
429
+ }
430
+
431
+
432
+ // --- Touch Controls ---
433
+ let isDragging = false, isPinching = false, isRotating = false;
434
+ let touchStartX = 0, touchStartY = 0;
435
+ let startOffsetXFraction = 0, startOffsetYFraction = 0;
436
+ let initialPinchDistance = 0, initialScale = 1;
437
+ let initialTouchAngle = 0, startRotationAngle = 0;
438
+
439
+ function getDistance(t1, t2) { return Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY); }
440
+ function getAngle(t1, t2) { return Math.atan2(t2.clientY - t1.clientY, t2.clientX - t1.clientX); }
441
+
442
+ canvas.addEventListener("touchstart", (e) => {
443
+ if (isLocked || !overlayLoaded) return;
444
+ canvasRect = canvas.getBoundingClientRect();
445
+ if (e.touches.length === 1) {
446
+ isDragging = true;
447
+ touchStartX = e.touches[0].clientX;
448
+ touchStartY = e.touches[0].clientY;
449
+ startOffsetXFraction = offsetXFraction;
450
+ startOffsetYFraction = offsetYFraction;
451
+ } else if (e.touches.length === 2) {
452
+ isPinching = true;
453
+ isRotating = true;
454
+ initialPinchDistance = getDistance(e.touches[0], e.touches[1]);
455
+ initialScale = S_user;
456
+ initialTouchAngle = getAngle(e.touches[0], e.touches[1]);
457
+ startRotationAngle = rotationAngle;
458
+ }
459
+ }, false);
460
+
461
+ canvas.addEventListener("touchmove", (e) => {
462
+ e.preventDefault();
463
+ if (isLocked || !overlayLoaded) return;
464
+ if (isDragging && e.touches.length === 1) {
465
+ offsetXFraction = startOffsetXFraction + (e.touches[0].clientX - touchStartX) / canvasRect.width;
466
+ offsetYFraction = startOffsetYFraction + (e.touches[0].clientY - touchStartY) / canvasRect.height;
467
+ } else if ((isPinching || isRotating) && e.touches.length === 2) {
468
+ // Pinch to Zoom
469
+ const currentDistance = getDistance(e.touches[0], e.touches[1]);
470
+ S_user = initialScale * (currentDistance / initialPinchDistance);
471
+ // Two-finger rotate
472
+ const currentAngle = getAngle(e.touches[0], e.touches[1]);
473
+ rotationAngle = startRotationAngle + (currentAngle - initialTouchAngle);
474
+ }
475
+ }, { passive: false });
476
+
477
+ canvas.addEventListener("touchend", (e) => {
478
+ if (e.touches.length < 2) { isPinching = false; isRotating = false; }
479
+ if (e.touches.length < 1) { isDragging = false; }
480
+ }, false);
481
+
482
+ // --- Main Draw Loop ---
483
+ function draw() {
484
+ const ctx = canvas.getContext("2d");
485
+ if (!cameraPaused && video.readyState === video.HAVE_ENOUGH_DATA) {
486
+ if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
487
+ canvas.width = video.videoWidth;
488
+ canvas.height = video.videoHeight;
489
+ }
490
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
491
+
492
+ if (overlayLoaded && displayOverlayImg.complete) {
493
+ const S_fit = Math.min(canvas.width / displayOverlayImg.naturalWidth, canvas.height / displayOverlayImg.naturalHeight);
494
+ const S = S_fit * S_user;
495
+ const imgWidth = S * displayOverlayImg.naturalWidth;
496
+ const imgHeight = S * displayOverlayImg.naturalHeight;
497
+ const dx = (canvas.width - imgWidth) / 2 + offsetXFraction * canvas.width;
498
+ const dy = (canvas.height - imgHeight) / 2 + offsetYFraction * canvas.height;
499
+
500
+ ctx.globalAlpha = 1.0 - parseFloat(transparencySlider.value);
501
+
502
+ // Apply transformations (rotation, flip)
503
+ ctx.save();
504
+ ctx.translate(dx + imgWidth / 2, dy + imgHeight / 2); // Move origin to image center
505
+ ctx.rotate(rotationAngle);
506
+ ctx.scale(isFlippedHorizontal ? -1 : 1, isFlippedVertical ? -1 : 1);
507
+ ctx.drawImage(displayOverlayImg, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight); // Draw centered on new origin
508
+ ctx.restore(); // Restore context to original state
509
+
510
+ ctx.globalAlpha = 1.0;
511
+ }
512
+ }
513
+ requestAnimationFrame(draw);
514
+ }
515
+
516
+ // --- Utility & UI Functions ---
517
+ function takeSnapshot() {
518
+ const link = document.createElement("a");
519
+ link.href = canvas.toDataURL("image/png");
520
+ link.download = "ar_drawing_snapshot.png";
521
+ link.click();
522
+ }
523
+
524
+ function showToast(message) {
525
+ const toast = document.getElementById("toastNotification");
526
+ toast.textContent = message;
527
+ toast.style.opacity = 1;
528
+ setTimeout(() => {
529
+ toast.style.opacity = 0;
530
+ }, 2500);
531
+ }
532
+
533
+ function saveConfig() {
534
+ const transparency = transparencySlider.value;
535
+ const config = { S_user, offsetXFraction, offsetYFraction, transparency, rotationAngle, isFlippedHorizontal, isFlippedVertical };
536
+ localStorage.setItem("arOverlayConfig", JSON.stringify(config));
537
+ showToast("Configuration Saved!");
538
+ }
539
+
540
+ function loadConfig() {
541
+ const config = localStorage.getItem("arOverlayConfig");
542
+ if (config) {
543
+ const parsed = JSON.parse(config);
544
+ S_user = parsed.S_user || 1;
545
+ offsetXFraction = parsed.offsetXFraction || 0;
546
+ offsetYFraction = parsed.offsetYFraction || 0;
547
+ rotationAngle = parsed.rotationAngle || 0;
548
+ isFlippedHorizontal = parsed.isFlippedHorizontal || false;
549
+ isFlippedVertical = parsed.isFlippedVertical || false;
550
+ transparencySlider.value = parsed.transparency || 0.5;
551
+ transparencyValueDisplay.textContent = parsed.transparency || 0.5;
552
+ showToast("Configuration Loaded!");
553
+ } else {
554
+ showToast("No saved configuration found.");
555
+ }
556
+ }
557
+
558
+ document.getElementById("toggleControls").addEventListener("click", function () {
559
+ const controls = document.getElementById("controls");
560
+ controls.style.display = window.getComputedStyle(controls).display === "none" ? "block" : "none";
561
+ });
562
+
563
+ document.getElementById("tutorialBtn").addEventListener("click", () => {
564
+ document.getElementById("tutorialModal").style.display = "block";
565
+ });
566
+ function closeTutorial() {
567
+ document.getElementById("tutorialModal").style.display = "none";
568
+ }
569
+
570
+ transparencySlider.addEventListener("input", () => {
571
+ transparencyValueDisplay.textContent = transparencySlider.value;
572
+ });
573
+
574
+ // --- Initialization ---
575
+ window.addEventListener("load", () => {
576
+ populateCameraOptions().then(() => startCamera());
577
+ draw();
578
+ });
579
+ </script>
580
+ </body>
581
  </html>