|
27 | 27 | .controls { |
28 | 28 | margin: 10px; |
29 | 29 | } |
30 | | - .circle { |
| 30 | + .circle, .inner-circle { |
31 | 31 | position: absolute; |
32 | | - border: 2px solid red; |
33 | 32 | border-radius: 50%; |
34 | 33 | background: transparent; |
35 | | - cursor: grab; |
| 34 | + pointer-events: none; |
36 | 35 | } |
37 | | - textarea { |
38 | | - width: 90%; |
39 | | - margin: 10px; |
| 36 | + .circle { |
| 37 | + border: 2px solid red; |
| 38 | + } |
| 39 | + .inner-circle { |
| 40 | + border: 2px solid blue; |
40 | 41 | } |
41 | 42 | </style> |
42 | 43 | </head> |
|
45 | 46 | <div class="controls"> |
46 | 47 | <input type="file" id="fileInput"> |
47 | 48 | <label>m: <input type="number" id="rows" value="3" min="1"></label> |
48 | | - <label>n: <input type="number" id="cols" value="4" min="1"></label> |
| 49 | + <label>n: <input type="number" id="cols" value="3" min="1"></label> |
49 | 50 | <button onclick="createGrid()">Create Grid</button> |
50 | | - <button onclick="exportCoords()">Export</button> |
51 | 51 | <button onclick="showGrayLevels()">Mean Gray</button> |
52 | 52 | </div> |
53 | 53 |
|
|
66 | 66 | let img = new Image(); |
67 | 67 | let circles = []; |
68 | 68 | let radius = 20; |
| 69 | +let innerRadius = 15; |
69 | 70 | let draggingCircle = null; |
70 | 71 | let offsetX, offsetY; |
71 | 72 |
|
|
86 | 87 | } |
87 | 88 |
|
88 | 89 | function clearCircles() { |
89 | | - circles.forEach(c => c.el.remove()); |
| 90 | + circles.flat().forEach(c => { |
| 91 | + c.outer.el.remove(); |
| 92 | + c.inner.el.remove(); |
| 93 | + }); |
90 | 94 | circles = []; |
91 | 95 | } |
92 | 96 |
|
93 | 97 | function createGrid() { |
94 | 98 | clearCircles(); |
| 99 | + drawImage(); |
95 | 100 | const m = parseInt(document.getElementById("rows").value); |
96 | 101 | const n = parseInt(document.getElementById("cols").value); |
97 | 102 | const spacingX = canvas.width / (n + 1); |
98 | 103 | const spacingY = canvas.height / (m + 1); |
| 104 | + |
99 | 105 | for (let i = 0; i < m; i++) { |
100 | 106 | circles.push([]); |
101 | 107 | for (let j = 0; j < n; j++) { |
102 | 108 | const x = (j + 1) * spacingX; |
103 | 109 | const y = (i + 1) * spacingY; |
104 | | - const el = document.createElement("div"); |
105 | | - el.className = "circle"; |
106 | | - el.style.width = el.style.height = radius * 2 + "px"; |
107 | | - el.style.left = (x - radius) + "px"; |
108 | | - el.style.top = (y - radius) + "px"; |
109 | | - makeDraggable(el); |
110 | | - container.appendChild(el); |
111 | | - circles[i].push({ x, y, el }); |
| 110 | + |
| 111 | + const outer = document.createElement("div"); |
| 112 | + outer.className = "circle"; |
| 113 | + outer.style.width = outer.style.height = radius * 2 + "px"; |
| 114 | + outer.style.left = (x - radius) + "px"; |
| 115 | + outer.style.top = (y - radius) + "px"; |
| 116 | + outer.style.cursor = "grab"; |
| 117 | + outer.style.pointerEvents = "auto"; |
| 118 | + container.appendChild(outer); |
| 119 | + |
| 120 | + const inner = document.createElement("div"); |
| 121 | + inner.className = "inner-circle"; |
| 122 | + inner.style.width = innerRadius * 2 + "px"; |
| 123 | + inner.style.height = innerRadius * 2 + "px"; |
| 124 | + inner.style.left = (x - innerRadius) + "px"; |
| 125 | + inner.style.top = (y - innerRadius) + "px"; |
| 126 | + container.appendChild(inner); |
| 127 | + |
| 128 | + makeDraggable(outer, inner); |
| 129 | + |
| 130 | + circles[i].push({ x, y, outer: { el: outer }, inner: inner }); |
112 | 131 | } |
113 | 132 | } |
114 | 133 | } |
115 | 134 |
|
116 | | -function makeDraggable(el) { |
117 | | - el.onmousedown = (e) => { |
118 | | - draggingCircle = el; |
| 135 | +function makeDraggable(outerEl, innerEl) { |
| 136 | + outerEl.onmousedown = (e) => { |
| 137 | + draggingCircle = outerEl; |
119 | 138 | offsetX = e.offsetX; |
120 | 139 | offsetY = e.offsetY; |
121 | | - el.style.cursor = "grabbing"; |
| 140 | + outerEl.style.cursor = "grabbing"; |
| 141 | + |
122 | 142 | document.onmousemove = (e) => { |
123 | 143 | const rect = container.getBoundingClientRect(); |
124 | | - el.style.left = (e.clientX - rect.left - offsetX) + "px"; |
125 | | - el.style.top = (e.clientY - rect.top - offsetY) + "px"; |
126 | | - updateCirclePosition(el); |
| 144 | + const newX = e.clientX - rect.left - offsetX; |
| 145 | + const newY = e.clientY - rect.top - offsetY; |
| 146 | + outerEl.style.left = newX + "px"; |
| 147 | + outerEl.style.top = newY + "px"; |
| 148 | + |
| 149 | + const x = newX + radius; |
| 150 | + const y = newY + radius; |
| 151 | + |
| 152 | + innerEl.style.left = (x - innerRadius) + "px"; |
| 153 | + innerEl.style.top = (y - innerRadius) + "px"; |
| 154 | + |
| 155 | + console.log(e) |
| 156 | + updateCirclePosition(e.target); |
127 | 157 | }; |
| 158 | + |
128 | 159 | document.onmouseup = () => { |
129 | 160 | document.onmousemove = null; |
130 | 161 | draggingCircle = null; |
131 | | - el.style.cursor = "grab"; |
| 162 | + outerEl.style.cursor = "grab"; |
132 | 163 | }; |
133 | 164 | }; |
134 | 165 | } |
135 | 166 |
|
136 | 167 | function updateCirclePosition(el) { |
137 | 168 | for (let i = 0; i < circles.length; i++) { |
138 | 169 | for (let j = 0; j < circles[i].length; j++) { |
139 | | - if (circles[i][j].el === el) { |
| 170 | + if (circles[i][j].outer.el === el) { |
140 | 171 | circles[i][j].x = parseInt(el.style.left) + radius; |
141 | 172 | circles[i][j].y = parseInt(el.style.top) + radius; |
142 | 173 | } |
143 | 174 | } |
144 | 175 | } |
145 | 176 | } |
146 | 177 |
|
| 178 | +document.addEventListener("keydown", (e) => { |
| 179 | + const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "+", "-", "k", "l"]; |
| 180 | + if (keys.includes(e.key)) { |
| 181 | + e.preventDefault(); |
| 182 | + if (e.key === "ArrowUp") moveCircles(0, -5); |
| 183 | + if (e.key === "ArrowDown") moveCircles(0, 5); |
| 184 | + if (e.key === "ArrowLeft") moveCircles(-5, 0); |
| 185 | + if (e.key === "ArrowRight") moveCircles(5, 0); |
| 186 | + if (e.key === "+") resizeCircles(1.1); |
| 187 | + if (e.key === "-") resizeCircles(0.9); |
| 188 | + if (e.key === "k") resizeInnerCircles(1.1); |
| 189 | + if (e.key === "l") resizeInnerCircles(0.9); |
| 190 | + } |
| 191 | +}); |
| 192 | + |
147 | 193 | function moveCircles(dx, dy) { |
148 | 194 | circles.flat().forEach(c => { |
149 | 195 | c.x += dx; |
150 | 196 | c.y += dy; |
151 | | - c.el.style.left = (c.x - radius) + "px"; |
152 | | - c.el.style.top = (c.y - radius) + "px"; |
| 197 | + c.outer.el.style.left = (c.x - radius) + "px"; |
| 198 | + c.outer.el.style.top = (c.y - radius) + "px"; |
| 199 | + c.inner.style.left = (c.x - innerRadius) + "px"; |
| 200 | + c.inner.style.top = (c.y - innerRadius) + "px"; |
153 | 201 | }); |
154 | 202 | } |
155 | 203 |
|
156 | 204 | function resizeCircles(factor) { |
157 | | - radius = Math.round(radius*factor); |
158 | | - console.log(radius) |
| 205 | + radius *= factor; |
| 206 | + innerRadius *= factor; |
| 207 | + radius = Math.round(radius); |
| 208 | + innerRadius = Math.round(innerRadius); |
159 | 209 | circles.flat().forEach(c => { |
160 | | - c.el.style.width = c.el.style.height = radius * 2 + "px"; |
161 | | - c.el.style.left = (c.x - radius) + "px"; |
162 | | - c.el.style.top = (c.y - radius) + "px"; |
| 210 | + c.outer.el.style.width = c.outer.el.style.height = radius * 2 + "px"; |
| 211 | + c.outer.el.style.left = (c.x - radius) + "px"; |
| 212 | + c.outer.el.style.top = (c.y - radius) + "px"; |
| 213 | + c.inner.style.width = c.inner.style.height = innerRadius * 2 + "px"; |
| 214 | + c.inner.style.left = (c.x - innerRadius) + "px"; |
| 215 | + c.inner.style.top = (c.y - innerRadius) + "px"; |
163 | 216 | }); |
164 | 217 | } |
165 | 218 |
|
166 | | -function exportCoords() { |
167 | | - let m = circles.length; |
168 | | - let n = (m > 0) ? circles[0].length : 0; |
169 | | - let result = "coords = [\n"; |
170 | | - for (let i = 0; i < m; i++) { |
171 | | - result += " ["; |
172 | | - for (let j = 0; j < n; j++) { |
173 | | - let x = Math.round(circles[i][j].x); |
174 | | - let y = Math.round(circles[i][j].y); |
175 | | - result += `[${x}, ${y}]${j < n - 1 ? ", " : ""}`; |
| 219 | +function resizeInnerCircles(factor) { |
| 220 | + innerRadius *= factor; |
| 221 | + circles.flat().forEach(c => { |
| 222 | + c.inner.style.width = c.inner.style.height = innerRadius * 2 + "px"; |
| 223 | + c.inner.style.left = (c.x - innerRadius) + "px"; |
| 224 | + c.inner.style.top = (c.y - innerRadius) + "px"; |
| 225 | + }); |
| 226 | +} |
| 227 | + |
| 228 | +document.getElementById("fileInput").onchange = (e) => { |
| 229 | + loadImageFromFile(e.target.files[0]); |
| 230 | +}; |
| 231 | + |
| 232 | +function handleDrop(e) { |
| 233 | + e.preventDefault(); |
| 234 | + const file = e.dataTransfer.files[0]; |
| 235 | + if (file) loadImageFromFile(file); |
| 236 | +} |
| 237 | + |
| 238 | +container.addEventListener("dragover", (e) => e.preventDefault()); |
| 239 | +container.addEventListener("drop", handleDrop); |
| 240 | + |
| 241 | +document.addEventListener("paste", (e) => { |
| 242 | + const items = e.clipboardData.items; |
| 243 | + for (let i = 0; i < items.length; i++) { |
| 244 | + if (items[i].type.indexOf("image") !== -1) { |
| 245 | + const blob = items[i].getAsFile(); |
| 246 | + loadImageFromFile(blob); |
176 | 247 | } |
177 | | - result += `]${i < m - 1 ? ",\n" : ""}`; |
178 | 248 | } |
179 | | - result += "\n]"; |
180 | | - console.log(result); |
181 | | - navigator.clipboard.writeText(result) |
182 | | - .then(() => alert("Copied coordinates to clipboard!")) |
183 | | - .catch(err => alert("Failed to copy: " + err)); |
| 249 | +}); |
| 250 | + |
| 251 | +function loadImageFromFile(file) { |
| 252 | + const reader = new FileReader(); |
| 253 | + reader.onload = (e) => { |
| 254 | + img.onload = () => { |
| 255 | + resizeCanvasToImage(); |
| 256 | + drawImage(); |
| 257 | + clearCircles(); |
| 258 | + }; |
| 259 | + img.src = e.target.result; |
| 260 | + }; |
| 261 | + reader.readAsDataURL(file); |
184 | 262 | } |
185 | 263 |
|
186 | 264 | function getMeanGrayAt(x, y, radius) { |
|
197 | 275 | const dx = i - radius; |
198 | 276 | const dy = j - radius; |
199 | 277 | if (dx * dx + dy * dy <= radius * radius) { |
200 | | - const idx = (j * radius * 2 + i) * 4; |
201 | | - |
202 | | - const r = data[idx]; |
203 | | - const g = data[idx + 1]; |
204 | | - const b = data[idx + 2]; |
205 | | - const gray = 0.299 * r + 0.587 * g + 0.114 * b; |
206 | | - sum += gray; |
207 | | - count++; |
208 | | - |
209 | | - // Color this pixel red |
210 | | - //data[idx] = 255; |
211 | | - //data[idx + 1] = 0; |
212 | | - //data[idx + 2] = 0; |
| 278 | + if (dx * dx + dy * dy >= innerRadius * innerRadius) { |
| 279 | + const idx = (j * radius * 2 + i) * 4; |
| 280 | + |
| 281 | + const r = data[idx]; |
| 282 | + const g = data[idx + 1]; |
| 283 | + const b = data[idx + 2]; |
| 284 | + const gray = 0.299 * r + 0.587 * g + 0.114 * b; |
| 285 | + sum += gray; |
| 286 | + count++; |
| 287 | + |
| 288 | + // Color this pixel red |
| 289 | + //data[idx] = 255; |
| 290 | + //data[idx + 1] = 0; |
| 291 | + //data[idx + 2] = 0; |
| 292 | + } |
213 | 293 | } |
214 | 294 | } |
215 | 295 | } |
|
220 | 300 | return count > 0 ? +(sum / count).toFixed(1) : 0; |
221 | 301 | } |
222 | 302 |
|
223 | | - |
224 | 303 | function showGrayLevels() { |
225 | 304 | let m = circles.length; |
226 | 305 | let n = (m > 0) ? circles[0].length : 0; |
|
238 | 317 | result += "\n]"; |
239 | 318 | document.getElementById("grayOutput").value = result; |
240 | 319 | } |
241 | | - |
242 | | -document.addEventListener("keydown", (e) => { |
243 | | - if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "+", "-"].includes(e.key)) { |
244 | | - e.preventDefault(); |
245 | | - if (e.key === "ArrowUp") moveCircles(0, -5); |
246 | | - if (e.key === "ArrowDown") moveCircles(0, 5); |
247 | | - if (e.key === "ArrowLeft") moveCircles(-5, 0); |
248 | | - if (e.key === "ArrowRight") moveCircles(5, 0); |
249 | | - if (e.key === "+") resizeCircles(1.1); |
250 | | - if (e.key === "-") resizeCircles(0.9); |
251 | | - } |
252 | | -}); |
253 | | - |
254 | | -document.getElementById("fileInput").onchange = (e) => { |
255 | | - loadImageFromFile(e.target.files[0]); |
256 | | -}; |
257 | | - |
258 | | -function handleDrop(e) { |
259 | | - e.preventDefault(); |
260 | | - const file = e.dataTransfer.files[0]; |
261 | | - if (file) loadImageFromFile(file); |
262 | | -} |
263 | | - |
264 | | -document.addEventListener("paste", (e) => { |
265 | | - const items = e.clipboardData.items; |
266 | | - for (let i = 0; i < items.length; i++) { |
267 | | - if (items[i].type.indexOf("image") !== -1) { |
268 | | - const blob = items[i].getAsFile(); |
269 | | - loadImageFromFile(blob); |
270 | | - } |
271 | | - } |
272 | | -}); |
273 | | - |
274 | | -function loadImageFromFile(file) { |
275 | | - const reader = new FileReader(); |
276 | | - reader.onload = (e) => { |
277 | | - img.onload = () => { |
278 | | - resizeCanvasToImage(); |
279 | | - drawImage(); |
280 | | - clearCircles(); |
281 | | - }; |
282 | | - img.src = e.target.result; |
283 | | - }; |
284 | | - reader.readAsDataURL(file); |
285 | | -} |
286 | 320 | </script> |
287 | 321 |
|
288 | 322 | </body> |
|
0 commit comments