Works with any backend npm install Zero dependencies

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
ToolShortcutWhat it does
PenPFree-form drawing - supports pointer events on touch / pen / mouse
↗ ArrowADraws a straight arrow with filled head - live preview while dragging
RectangleROutline rectangle for highlighting regions
T TextTClick to place; prompt for the string; size driven by stroke-width slider x 4
BlurBDrag a region - gets pixelated 20x down + up. Use for redacting names, PII, license plates
EraserEErases annotation overlay only - base image is untouched
↶ UndoCtrl/Cmd+Z30-deep undo stack - snapshots overlay before each stroke
CancelEscDiscards 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 drawImage calls; what the user sees.
  • Undo stack - 30-deep array of ImageData snapshots of the overlay. Each stroke starts with a pushUndo() 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.js imports.
  • 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.