Spaces:
Running
Running
Update index.html
Browse files- index.html +579 -17
index.html
CHANGED
|
@@ -1,19 +1,581 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|