Skip to content

Commit 33f65d5

Browse files
committed
feat: add blur tool to annotation editor
Users can now select the Blur tool (button or keyboard shortcut B) to drag a rectangle over sensitive areas of a cropped screenshot before saving. The selected region is blurred using the canvas clip+filter technique, and blur regions support undo like other annotation types. https://claude.ai/code/session_01TbfSaMsdyScyRiY8m1xYrd
1 parent c170ac4 commit 33f65d5

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

js/annotation_editor.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@
9898
cursor: text;
9999
}
100100

101+
#annotation-canvas.tool-blur {
102+
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="none" stroke="%23818CF8" stroke-width="2" stroke-dasharray="3 2"/><circle cx="12" cy="12" r="3" fill="%23818CF8" opacity="0.5"/></svg>') 12 12, crosshair;
103+
}
104+
101105
#text-input {
102106
position: absolute;
103107
display: none;
@@ -182,6 +186,18 @@
182186
background: #B91C1C;
183187
}
184188

189+
#blur-tool.active {
190+
background: #4F46E5;
191+
border-color: #4F46E5;
192+
box-shadow:
193+
0 0 0 3px rgba(79, 70, 229, 0.3),
194+
0 4px 12px rgba(79, 70, 229, 0.4);
195+
}
196+
197+
#blur-tool.active:hover {
198+
background: #4338CA;
199+
}
200+
185201
.action-button {
186202
padding: 0 24px;
187203
height: 44px;
@@ -341,6 +357,7 @@
341357
<span><span class="shortcut-key">A</span> Arrow</span>
342358
<span><span class="shortcut-key">R</span> Rectangle</span>
343359
<span><span class="shortcut-key">T</span> Text</span>
360+
<span><span class="shortcut-key">B</span> Blur</span>
344361
<span><span class="shortcut-key">Ctrl+Z</span> Undo</span>
345362
<span><span class="shortcut-key">Esc</span> Cancel</span>
346363
<span><span class="shortcut-key">Ctrl+Enter</span> Save</span>
@@ -372,6 +389,16 @@
372389
<line x1="12" y1="4" x2="12" y2="20"></line>
373390
</svg>
374391
</button>
392+
<button class="tool-button" id="blur-tool" data-tooltip="Blur (B)">
393+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
394+
<circle cx="12" cy="12" r="8" stroke-dasharray="3 2"/>
395+
<circle cx="12" cy="12" r="3" fill="currentColor" opacity="0.6"/>
396+
<line x1="4" y1="8" x2="2" y2="8"/>
397+
<line x1="22" y1="8" x2="20" y2="8"/>
398+
<line x1="4" y1="16" x2="2" y2="16"/>
399+
<line x1="22" y1="16" x2="20" y2="16"/>
400+
</svg>
401+
</button>
375402
<button class="tool-button" id="undo-button" data-tooltip="Undo (Ctrl+Z)" disabled>
376403
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
377404
<polyline points="1 4 1 10 7 10"></polyline>

js/annotation_editor.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
const canvas = document.getElementById('annotation-canvas');
66
const ctx = canvas.getContext('2d');
77

8-
let currentTool = 'arrow'; // 'arrow', 'rectangle', or 'text'
8+
let currentTool = 'arrow'; // 'arrow', 'rectangle', 'text', or 'blur'
99
let isDrawing = false;
1010
let isEditingText = false;
1111
let startX, startY;
@@ -18,6 +18,7 @@
1818
const LINE_WIDTH = 3;
1919
const TEXT_FONT_SIZE = 20;
2020
const TEXT_FONT = `bold ${TEXT_FONT_SIZE}px 'IBM Plex Sans', sans-serif`;
21+
const BLUR_RADIUS = 12; // px – strength of the blur effect
2122

2223
// Text input element
2324
const textInput = document.getElementById('text-input');
@@ -84,6 +85,8 @@
8485
drawRectangle(annotation.startX, annotation.startY, annotation.width, annotation.height);
8586
} else if (annotation.type === 'text') {
8687
drawText(annotation.x, annotation.y, annotation.text);
88+
} else if (annotation.type === 'blur') {
89+
drawBlur(annotation.x, annotation.y, annotation.width, annotation.height);
8790
}
8891
}
8992

@@ -120,6 +123,26 @@
120123
ctx.stroke();
121124
}
122125

126+
// Draw blurred region
127+
function drawBlur(x, y, width, height) {
128+
if (!baseImage || Math.abs(width) < 2 || Math.abs(height) < 2) return;
129+
130+
// Normalize so rect always starts at top-left
131+
const rx = width < 0 ? x + width : x;
132+
const ry = height < 0 ? y + height : y;
133+
const rw = Math.abs(width);
134+
const rh = Math.abs(height);
135+
136+
ctx.save();
137+
ctx.beginPath();
138+
ctx.rect(rx, ry, rw, rh);
139+
ctx.clip(); // confine blur to the selected rectangle
140+
ctx.filter = `blur(${BLUR_RADIUS}px)`;
141+
// Redraw the base image with blur; clip prevents it bleeding outside the rect
142+
ctx.drawImage(baseImage, 0, 0);
143+
ctx.restore();
144+
}
145+
123146
// Draw text
124147
function drawText(x, y, text) {
125148
ctx.font = TEXT_FONT;
@@ -231,6 +254,10 @@
231254
const width = pos.x - startX;
232255
const height = pos.y - startY;
233256
drawRectangle(startX, startY, width, height);
257+
} else if (currentTool === 'blur') {
258+
const width = pos.x - startX;
259+
const height = pos.y - startY;
260+
drawBlur(startX, startY, width, height);
234261
}
235262
});
236263

@@ -257,6 +284,18 @@
257284
width: pos.x - startX,
258285
height: pos.y - startY
259286
});
287+
} else if (currentTool === 'blur') {
288+
const width = pos.x - startX;
289+
const height = pos.y - startY;
290+
if (Math.abs(width) >= 2 && Math.abs(height) >= 2) {
291+
annotations.push({
292+
type: 'blur',
293+
x: startX,
294+
y: startY,
295+
width: width,
296+
height: height
297+
});
298+
}
260299
}
261300

262301
redrawCanvas();
@@ -270,12 +309,14 @@
270309
document.getElementById('arrow-tool').classList.toggle('active', tool === 'arrow');
271310
document.getElementById('rectangle-tool').classList.toggle('active', tool === 'rectangle');
272311
document.getElementById('text-tool').classList.toggle('active', tool === 'text');
312+
document.getElementById('blur-tool').classList.toggle('active', tool === 'blur');
273313
canvas.className = 'tool-' + tool;
274314
}
275315

276316
document.getElementById('arrow-tool').addEventListener('click', () => setActiveTool('arrow'));
277317
document.getElementById('rectangle-tool').addEventListener('click', () => setActiveTool('rectangle'));
278318
document.getElementById('text-tool').addEventListener('click', () => setActiveTool('text'));
319+
document.getElementById('blur-tool').addEventListener('click', () => setActiveTool('blur'));
279320

280321
// Undo button
281322
document.getElementById('undo-button').addEventListener('click', undo);
@@ -328,6 +369,8 @@
328369
document.getElementById('rectangle-tool').click();
329370
} else if (e.key === 't' || e.key === 'T') {
330371
document.getElementById('text-tool').click();
372+
} else if (e.key === 'b' || e.key === 'B') {
373+
document.getElementById('blur-tool').click();
331374
} else if (e.key === 'z' && (e.ctrlKey || e.metaKey)) {
332375
e.preventDefault();
333376
undo();

0 commit comments

Comments
 (0)