|
5 | 5 | const canvas = document.getElementById('annotation-canvas'); |
6 | 6 | const ctx = canvas.getContext('2d'); |
7 | 7 |
|
8 | | - let currentTool = 'arrow'; // 'arrow' or 'rectangle' |
| 8 | + let currentTool = 'arrow'; // 'arrow', 'rectangle', or 'text' |
9 | 9 | let isDrawing = false; |
| 10 | + let isEditingText = false; |
10 | 11 | let startX, startY; |
11 | 12 | let annotations = []; // Store all drawn annotations |
12 | 13 | let baseImage = null; // Store the original screenshot |
|
15 | 16 | // Drawing state |
16 | 17 | const ANNOTATION_COLOR = '#DC2626'; |
17 | 18 | const LINE_WIDTH = 3; |
| 19 | + const TEXT_FONT_SIZE = 20; |
| 20 | + const TEXT_FONT = `bold ${TEXT_FONT_SIZE}px 'IBM Plex Sans', sans-serif`; |
| 21 | + |
| 22 | + // Text input element |
| 23 | + const textInput = document.getElementById('text-input'); |
18 | 24 |
|
19 | 25 | // Initialize editor with screenshot data |
20 | 26 | function initEditor(screenshotDataUrl) { |
|
76 | 82 | drawArrow(annotation.startX, annotation.startY, annotation.endX, annotation.endY); |
77 | 83 | } else if (annotation.type === 'rectangle') { |
78 | 84 | drawRectangle(annotation.startX, annotation.startY, annotation.width, annotation.height); |
| 85 | + } else if (annotation.type === 'text') { |
| 86 | + drawText(annotation.x, annotation.y, annotation.text); |
79 | 87 | } |
80 | 88 | } |
81 | 89 |
|
|
112 | 120 | ctx.stroke(); |
113 | 121 | } |
114 | 122 |
|
| 123 | + // Draw text |
| 124 | + function drawText(x, y, text) { |
| 125 | + ctx.font = TEXT_FONT; |
| 126 | + ctx.fillStyle = ANNOTATION_COLOR; |
| 127 | + ctx.fillText(text, x, y); |
| 128 | + } |
| 129 | + |
115 | 130 | // Get mouse position relative to canvas |
116 | 131 | function getMousePos(e) { |
117 | 132 | const rect = canvas.getBoundingClientRect(); |
|
124 | 139 | }; |
125 | 140 | } |
126 | 141 |
|
| 142 | + // Show text input at a given position on the canvas |
| 143 | + function showTextInput(canvasX, canvasY) { |
| 144 | + const rect = canvas.getBoundingClientRect(); |
| 145 | + const scaleX = rect.width / canvas.width; |
| 146 | + const scaleY = rect.height / canvas.height; |
| 147 | + |
| 148 | + // Position input in CSS coordinates, offset up by font size so text baseline aligns |
| 149 | + const displayFontSize = TEXT_FONT_SIZE * scaleY; |
| 150 | + textInput.style.left = (canvasX * scaleX) + 'px'; |
| 151 | + textInput.style.top = (canvasY * scaleY - displayFontSize) + 'px'; |
| 152 | + textInput.style.fontSize = displayFontSize + 'px'; |
| 153 | + textInput.style.display = 'block'; |
| 154 | + textInput.value = ''; |
| 155 | + isEditingText = true; |
| 156 | + // Defer focus to next tick so the mousedown event finishes first |
| 157 | + setTimeout(() => textInput.focus(), 0); |
| 158 | + } |
| 159 | + |
| 160 | + // Hide text input and commit or discard |
| 161 | + function hideTextInput(commit) { |
| 162 | + if (!isEditingText) return; |
| 163 | + const text = textInput.value.trim(); |
| 164 | + textInput.style.display = 'none'; |
| 165 | + textInput.value = ''; |
| 166 | + isEditingText = false; |
| 167 | + |
| 168 | + if (commit && text) { |
| 169 | + annotations.push({ |
| 170 | + type: 'text', |
| 171 | + x: startX, |
| 172 | + y: startY, |
| 173 | + text: text |
| 174 | + }); |
| 175 | + redrawCanvas(); |
| 176 | + updateUndoButton(); |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + // Text input event handlers |
| 181 | + textInput.addEventListener('keydown', (e) => { |
| 182 | + if (e.key === 'Enter') { |
| 183 | + e.preventDefault(); |
| 184 | + hideTextInput(true); |
| 185 | + } else if (e.key === 'Escape') { |
| 186 | + e.preventDefault(); |
| 187 | + hideTextInput(false); |
| 188 | + } |
| 189 | + e.stopPropagation(); // Prevent global shortcuts while typing |
| 190 | + }); |
| 191 | + |
127 | 192 | // Mouse event handlers |
128 | 193 | canvas.addEventListener('mousedown', (e) => { |
129 | | - isDrawing = true; |
| 194 | + if (isEditingText) { |
| 195 | + // Clicking canvas while editing text commits it |
| 196 | + hideTextInput(true); |
| 197 | + return; |
| 198 | + } |
| 199 | + |
130 | 200 | const pos = getMousePos(e); |
131 | 201 | startX = pos.x; |
132 | 202 | startY = pos.y; |
| 203 | + |
| 204 | + if (currentTool === 'text') { |
| 205 | + // Prevent canvas from stealing focus from the text input |
| 206 | + e.preventDefault(); |
| 207 | + showTextInput(pos.x, pos.y); |
| 208 | + return; |
| 209 | + } |
| 210 | + |
| 211 | + isDrawing = true; |
133 | 212 | }); |
134 | 213 |
|
135 | 214 | canvas.addEventListener('mousemove', (e) => { |
|
184 | 263 | updateUndoButton(); |
185 | 264 | }); |
186 | 265 |
|
187 | | - // Tool selection |
188 | | - document.getElementById('arrow-tool').addEventListener('click', () => { |
189 | | - currentTool = 'arrow'; |
190 | | - document.getElementById('arrow-tool').classList.add('active'); |
191 | | - document.getElementById('rectangle-tool').classList.remove('active'); |
192 | | - canvas.className = 'tool-arrow'; |
193 | | - }); |
| 266 | + // Tool selection helper |
| 267 | + function setActiveTool(tool) { |
| 268 | + hideTextInput(false); |
| 269 | + currentTool = tool; |
| 270 | + document.getElementById('arrow-tool').classList.toggle('active', tool === 'arrow'); |
| 271 | + document.getElementById('rectangle-tool').classList.toggle('active', tool === 'rectangle'); |
| 272 | + document.getElementById('text-tool').classList.toggle('active', tool === 'text'); |
| 273 | + canvas.className = 'tool-' + tool; |
| 274 | + } |
194 | 275 |
|
195 | | - document.getElementById('rectangle-tool').addEventListener('click', () => { |
196 | | - currentTool = 'rectangle'; |
197 | | - document.getElementById('rectangle-tool').classList.add('active'); |
198 | | - document.getElementById('arrow-tool').classList.remove('active'); |
199 | | - canvas.className = 'tool-rectangle'; |
200 | | - }); |
| 276 | + document.getElementById('arrow-tool').addEventListener('click', () => setActiveTool('arrow')); |
| 277 | + document.getElementById('rectangle-tool').addEventListener('click', () => setActiveTool('rectangle')); |
| 278 | + document.getElementById('text-tool').addEventListener('click', () => setActiveTool('text')); |
201 | 279 |
|
202 | 280 | // Undo button |
203 | 281 | document.getElementById('undo-button').addEventListener('click', undo); |
|
239 | 317 |
|
240 | 318 | // Keyboard shortcuts |
241 | 319 | document.addEventListener('keydown', (e) => { |
| 320 | + // Skip global shortcuts while editing text (handled by textInput's own keydown) |
| 321 | + if (isEditingText) return; |
| 322 | + |
242 | 323 | if (e.key === 'Escape') { |
243 | 324 | document.getElementById('cancel-button').click(); |
244 | 325 | } else if (e.key === 'a' || e.key === 'A') { |
245 | 326 | document.getElementById('arrow-tool').click(); |
246 | 327 | } else if (e.key === 'r' || e.key === 'R') { |
247 | 328 | document.getElementById('rectangle-tool').click(); |
248 | | - } else if (e.key === 'z' && (e.ctrlKey || e.metaKey)) { |
| 329 | + } else if (e.key === 't' || e.key === 'T') { |
| 330 | + document.getElementById('text-tool').click(); |
| 331 | + } else if (e.key === 'z' && (e.ctrlKey || e.metaKey)) { |
249 | 332 | e.preventDefault(); |
250 | 333 | undo(); |
251 | 334 | } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { |
|
0 commit comments