Slack-style markup: arrows, text, blur, before upload
uploader.openAnnotator(file) opens a full-screen markup modal: 6 tools (pen, arrow, rectangle, text, blur-region redact, eraser), color + stroke-width controls, undo stack, save + cancel. Resolves with a flattened PNG File. Designed for bug reports, support tickets, design review, screenshot annotation - the workflow Slack and Notion ship as a paid feature.
Pick an image, then click next to it to annotate:
Try it: queue an image, click Annotate. Use the 6-tool toolbar: pen for free-form, arrow to point at something, rectangle to highlight, T for callouts, blur to redact sensitive regions, eraser for fixes. Hit Save - the annotated copy lands in the queue.
Tools at a glance
| Tool | Shortcut | What it does |
|---|---|---|
| Pen | P | Free-form drawing - supports pointer events on touch / pen / mouse |
| ↗ Arrow | A | Draws a straight arrow with filled head - live preview while dragging |
| Rectangle | R | Outline rectangle for highlighting regions |
| T Text | T | Click to place; prompt for the string; size driven by stroke-width slider x 4 |
| Blur | B | Drag a region - gets pixelated 20x down + up. Use for redacting names, PII, license plates |
| Eraser | E | Erases annotation overlay only - base image is untouched |
| ↶ Undo | Ctrl/Cmd+Z | 30-deep undo stack - snapshots overlay before each stroke |
| Cancel | Esc | Discards changes; promise rejects with Error('cancelled') |
API
const annotated = await uploader.openAnnotator(file, {
color: '#ef4444', // initial stroke color (CSS color)
strokeWidth: 4, // initial stroke width (px, 1-20)
outputType: 'image/png', // 'image/png' (default) or 'image/jpeg'
quality: 0.92 // JPEG quality 0..1; ignored for PNG
});
// annotated is a File named "<original>.annotated.png" with type 'image/png'
uploader.addFile(annotated);Architecture: two canvases, undo via ImageData
- Base canvas - immutable copy of the original image; never written to after init.
- Overlay canvas - every annotation lands here. Same dimensions as base.
- Display canvas - receives base + overlay each frame via two
drawImagecalls; what the user sees. - Undo stack - 30-deep array of
ImageDatasnapshots of the overlay. Each stroke starts with apushUndo()before drawing. - Final flatten - Save composites base + overlay into a fresh canvas;
toBlob()-> File.
Blur-region redaction details
The blur tool implements pixelation (not gaussian blur) for two reasons: fast (just a downscale + upscale via drawImage), and irrecoverable (gaussian blur can sometimes be deconvolved with ML - pixelation truly destroys the bits). Pixel size auto-scales to min(rectW, rectH) / 20, capped at 8px minimum.
Composes with the rest of the pipeline
// Capture screenshot -> annotate -> encrypt -> upload
const screenshot = await uploader.openScreenCapture(); // existing
const annotated = await uploader.openAnnotator(screenshot); // new
new MultipleUpload('#uploader', {
uploadUrl: '/api/upload',
encrypt: true,
encryptPassword: () => sessionPassphrase(),
onTaskComplete: (task, result) => attachToTicket(result)
}).addFile(annotated);Why a built-in vs a library dependency
- Zero deps: ~290 LOC of vanilla canvas code. No
fabric.js/konva.jsimports. - Touch + pen friendly: uses Pointer Events directly - same code path for mouse, stylus, finger.
- i18n-ready: all tooltip text is static English today but flows through the same locale system as the rest of the chrome.
- Mobile responsive: 90vw x 65vh stage with toolbar wrapping; works in portrait phone.