I just wanted to draw one arrow on a screenshot and paste it into a chat.
That's it. Why does it take 30 seconds?
Since Skitch was effectively discontinued, every alternative I tried hit me with "Please sign in," "Choose a plan," or "Download required." No. That's not what I need.
So I stopped searching and built one.
Pure Mark Annotate — Try it now (completely free)
No install. No login. No account. Open the browser, drop an image, draw an arrow. Done.
What It Does (30-Second Overview)
| Action | How |
|---|---|
| Load an image | Drag & drop / Ctrl+V paste / file picker |
| Annotate | Arrows, text, rectangles, highlights, mosaic, numbered circles |
| Blur faces | Auto-detect and one-click mosaic |
| Save | iPhone → Share Sheet / Desktop → PNG download |
Works on both desktop and mobile. English and Japanese UI.
I obsessed over getting that "Skitch-style arrow" right — if you remember that feel, you'll know it when you see it.
Two Weeks of Going Nowhere
I decided to build this in early 2026.
The first plan was Flutter. Ship to both iOS and Android. Register on the stores, reach ex-Skitch users. I did market research, chose the brand name "Pure Mark Annotate," set up a Docker dev environment... and stalled.
"How do I test on a real device?" "I only have an iPhone, but should I build the Android version first?" "Google Play requires a public address, and the review process needs 12 testers..."
For about two weeks, I explored every direction. Flutter Web, cloud Macs, buying a cheap Android phone. Before I knew it, I'd spent all my time thinking about which platform to build on without writing a single line of the actual "draw an arrow" implementation.
The real problem wasn't the tool. It was friction.
What I loved about Skitch was the zero friction to start using it. Open it when you want to, draw when you want to — that transparency. So the answer was obvious from the start. PWA.
One night, I dropped everything and decided: Flutter — done. App store submissions — figure it out later. Build something that works. Today.
The Arrow Took 3 Hours
The core implementation came together in a few hours with Claude.
But I spent 3 hours on the arrow.
A simple triangle and a line should be fine — that's what I thought for the first 30 minutes, until something felt off. Skitch's arrow had a distinctive silhouette. The sharpness of the tip, the "pinch" of the shaft, the drop shadow that boosted visibility. It wasn't just a shape — it was a line with intention.
"Make the base a bit thinner." "The drop shadow should be subtle, but wide."
I iterated on the Canvas API paths in conversation with Claude. Bezier curves for the pinch, dialing in the shadow opacity and blur by the numbers, calculating the tip geometry so it wouldn't break at different angles. In a solo project, you can afford to spend 3 hours like this.
The Day After Deploy, iPhone Returned a Blank Page
"Perfect." I deployed. The next day, I tested on iPhone.
The exported image was pure white. The annotations were gone.
Root cause: iOS Safari's canvas pixel limit (~16.7 million pixels)
A high-resolution photo (typical iPhone 4000x3000 = 12M px) exported at 3x scale easily exceeds the limit. No error. Just... white.
The fix: calculate the device's limit before export and auto-scale down.
function getSafeExportScale(w: number, h: number): number {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const maxPixels = isIOS ? 16_000_000 : 100_000_000;
let scale = 3;
while (w * scale * h * scale > maxPixels && scale > 1) {
scale -= 0.5;
}
return Math.max(1, scale);
}
On top of that, canvas.toBlob() is async and loses user activation, so navigator.share() won't work on iOS. Fixed it by using the synchronous toDataURL() and manually converting to a Blob.
After implementing this, I tested again. The annotated image exported correctly.
Skitch's arrow was back on iPhone.
Tech Stack (For Developers)
| Tech | Why |
|---|---|
| React 19 + TypeScript | Type safety, ecosystem breadth |
| Vite 6 | Blazing fast HMR, great PWA plugin support |
| Tailwind CSS v4 | Utility-first, perfect for dark UI |
| Zustand | Lightweight state management (no Redux needed) |
| vite-plugin-pwa | Auto SW generation, offline support |
| YuNet + ONNX Runtime Web | Face detection → auto mosaic (228KB, MIT license) |
| Cloudflare Pages | GitHub integration, auto deploy, free SSL |
Why React PWA over Flutter: Flutter's bundle is 2-5MB, dart:io doesn't work on web, and SEO is impossible. A React PWA loads in a few hundred KB and runs directly in Safari on iPhone for testing.
Other Gotchas (Developer Notes)
Mosaic Misalignment on Retina Displays
Retina displays have DPR 2-3x, so the canvas physical pixels and CSS coordinates diverge. Fixed by normalizing the mosaic source image to CSS pixel space.
// Don't multiply by DPR — stay in CSS pixel space
const imgCanvas = document.createElement('canvas');
imgCanvas.width = rect.width;
imgCanvas.height = rect.height;
imgCtx.drawImage(image, dx, dy, dw, dh);
Text Becomes Tiny on Export
Coordinates and strokeWidth were being scaled, but fontSize: 24 and the numbered circle radius 18 were hardcoded. Fixed by passing a scale argument through all drawing parameters.
export function renderAnnotations(ctx, annotations, imageCanvas, scale = 1) {
const fontSize = Math.round(24 * scale);
const r = Math.round(18 * scale);
const pixelSize = Math.round(10 * scale);
// ...
}
I Changed the Face Detection Model 4 Times
The mosaic feature's auto face detection needed on-device inference that runs in the browser. This was the hardest part. Long story short, I swapped models four times.
1. MediaPipe BlazeFace (first choice)
Google-made, lightweight, decent accuracy. But WebAssembly compatibility issues on iOS Safari meant it didn't work on some devices. A PWA that doesn't work everywhere isn't a PWA. Dropped early.
2. @vladmandic/face-api (SSD MobileNet V1)
TensorFlow.js-based, better iOS stability. But the accuracy wasn't production-ready. Missed faces in group photos, weak on side profiles, too many false positives. Six commits of threshold tuning and multi-scale scanning couldn't save it.
3. SCRFD-2.5GF (ONNX Runtime Web)
InsightFace's high-accuracy model. WIDERFace Easy 93.8% — leagues ahead. Group photos and profile shots handled nearly perfectly. Dropped the TensorFlow.js dependency too. But then I discovered the license is non-commercial (commercial use requires separate permission). With future monetization in mind, I had to swap it out.
4. YuNet 2023mar (current)
From OpenCV Zoo, MIT licensed, no commercial restrictions. Model size 228KB — 93% smaller than SCRFD's 3.2MB. WIDERFace accuracy drops about 5%, but for PureMark's use case (mosaic on front-facing to slightly angled faces), it's more than enough.
| BlazeFace | face-api.js | SCRFD-2.5GF | YuNet | |
|---|---|---|---|---|
| Model size | ~400KB | ~5.4MB | 3.2MB | 228KB |
| iOS stability | No | Yes | Yes | Yes |
| Group photo accuracy | Fair | Poor | Excellent | Good |
| Commercial license | Yes | Yes | No (needs permission) | Yes (MIT) |
| Runtime | MediaPipe | TF.js | ONNX Runtime | ONNX Runtime |
Lesson: If you pick a model based on accuracy alone, licensing will bite you later. Open source doesn't mean commercially free. Academic models are especially risky.
The PWA Service Worker uses a CacheFirst strategy, so face detection works offline from the second visit onward.
Deploying to Cloudflare Pages
# public/_redirects (SPA routing)
/* /index.html 200
Subdomain design anticipates future multi-app expansion:
puremark.app → Landing page (future)
annotate.puremark.app → Image annotation (this project)
Domain, DNS, and hosting are all on Cloudflare — CNAME setup and SSL issuance are fully automatic.
Summary
- Development time: Effectively 1 day
- Cost: Domain only (Cloudflare Pages and CDN are free)
- Code: ~2,000 lines of TypeScript
I didn't "have AI build it for me." I used AI as a booster to bring my own obsessions to life. The arrow's pinch, the fight with iOS Safari — every decision was mine. AI was the tool to execute them.
The two weeks I spent going nowhere with Flutter happened because I'd shifted from "eliminate friction" to "which platform should I ship on." That detour is what led me to the right answer: PWA.
If you're the kind of person who gets frustrated by how long it takes to draw one arrow on a screenshot — give it a try.
Top comments (0)