This is the eleventh post in the LocalPapa Notes dev-log series. We've already turned two browser-readable hardware sources into tools — motion sensors and the microphone. This post adds the third, and most sensitive, one: the camera.
With it, we built a QR scanner and a digital magnifier. And the camera feed — naturally — never leaves your device, not a single frame.
Why the camera especially deserves "on-device" processing
The camera is the most privacy-laden of the three sensors. Uploading your lens feed to someone's server just to scan a QR code or magnify some small print feels wrong however you look at it.
So both tools take the same path: getUserMedia({video:{facingMode:'environment'}}) to prefer the rear camera, with the feed processed only in your browser — scanning, magnifying and photo capture all done locally.
QR scanner: native first, fall back if needed
The QR scanner's core is a "decoder abstraction layer," and underneath it is a very practical engineering pattern: native-first + graceful degradation.
- First ask the browser: do you have
BarcodeDetector? Does it support the qr_code format? If so, use it — native, fast, and able to recognise multiple 1D/2D codes along the way.
- If not, dynamically
import() lazy-load jsQR (pure-JS decoding) as a fallback.
Their APIs differ (BarcodeDetector does detect(canvas) directly; jsQR needs getImageData first), so we wrap the difference in a decodeFrame() and the upper layer never needs to know which engine is underneath. The value of abstraction is making "swapping engines" transparent to the caller.
The scan loop has a power-saving trick too: requestAnimationFrame draws camera frames to an offscreen canvas, but throttles decoding to about 8fps — your eyes don't need 60fps to scan a QR, and the savings are battery. On a hit, it immediately stops the camera and shows the result.
Result handling minds security: for a URL, "open link" always adds rel="noopener noreferrer", auto-prefixes https to www., and always shows the raw text too so you confirm before tapping. With no camera or denied permission, it gently steers you to "upload an image" — going through the same decodeFrame, one decoding logic with two entry points.
Digital magnifier: why CSS zoom, not hardware zoom
The digital magnifier lets you magnify small print live through the camera — handy for presbyopia, medication labels, contract fine print.
It has a key technical trade-off: scale with CSS transform: scale(), not the camera's hardware-zoom constraint. Hardware zoom is only supported on some devices and is very inconsistent across browsers; CSS scaling works everywhere and behaves consistently. The magnifier would rather pick "works everywhere" than "better on some devices, absent on others."
Zoom (1–8×) pairs with drag-to-pan, the pan amount clamped via (zoom-1)×size/2 so magnifying never drags the image off into black edges.
A few thoughtful details:
- Freeze:
drawImage the current camera frame to a canvas, then apply zoom/pan/filters to that still image — read at leisure even with shaky hands.
- Photo: draw the current source at native resolution to an offscreen canvas,
toBlob to download a PNG.
- Torch: detect
track.getCapabilities().torch, and if supported toggle the flashlight via applyConstraints — a lifesaver for small print in the dark.
- Filters: high-contrast / grayscale / invert via one-click CSS filters for readability.
Three sensors, one discipline
By the third sensor tool, some habits have become muscle memory:
- Native first, fallback as backstop (QR's BarcodeDetector→jsQR).
- Cross-browser consistency > single-device best (the magnifier's CSS zoom).
- Return borrowed hardware clean: stop and
pagehide always release the MediaStream tracks, so the camera indicator light goes off on time.
- Detect capabilities, don't assume: whether torch exists, whether
BarcodeDetector is present — ask before using.
What we learned
- The camera is the most privacy-laden sensor, and the most deserving of local processing. The lens feed not leaving the device is the bottom line for both tools.
- Native-first + degradation is the standard answer to browser differences. Those who have the new API enjoy the speed; those who don't aren't excluded.
- Cross-browser consistency often beats single-device best. CSS zoom over hardware zoom is exactly that trade-off.
- Capability detection makes features "better if present, usable if not." Torch is a bonus, not a prerequisite.
The same question, harvested
Motion, sound, image — the three hardware sources a browser can read directly, all turned into on-device, useful, no-upload tools. Here the series' question settles into a clean shape: whatever a device can sense for itself, the browser can usually turn into a tool right there — without handing the data to anyone.
QR, magnify — the camera's answer, too, is: yes.
In the next dev log, we finally step into the long-brewing thread — turning the whole site into a knowledge pack AI agents can read (OKF) plus llms.txt. Want to follow the series? Bookmark LocalPapa Notes.