開發歷程:把相機變成工具——QR 掃描器與數位放大鏡

這是 LocalPapa Notes 開發紀錄系列的第十一篇。前面我們把瀏覽器能直接讀的兩個硬體寫成了工具——動作感測器麥克風。這一篇補上第三個,也是最敏感的一個:相機

用它,我們做了 QR 掃描器數位放大鏡。而相機畫面——理所當然地——一格都不離開你的裝置

相機之所以該「就地處理」

相機是三種感測器裡隱私分量最重的。把鏡頭畫面上傳到別人的伺服器去掃個 QR、或放大看張小字,怎麼想都不對勁。

所以這兩個工具都走同一條路:getUserMedia({video:{facingMode:'environment'}}) 優先打開後鏡頭,畫面只在你的瀏覽器裡處理,掃描、放大、拍照全在本地完成。

QR 掃描器:原生優先,沒有就降級

QR 掃描器的核心是一個「解碼器抽象層」,骨子裡是一個很實用的工程模式:原生優先 + 優雅降級

  • 先問瀏覽器:你有 BarcodeDetector 嗎?支援 qr_code 格式嗎?有的話就用它——原生、快、還能順便認多種一維/二維碼。
  • 沒有的話,動態 import() 懶載入 jsQR(純 JS 解碼)當後備。

兩者 API 不同(BarcodeDetector 直接 detect(canvas)jsQR 要先 getImageData),所以我們用一個 decodeFrame() 把差異包起來,上層程式碼完全不必管底下用的是哪一個。抽象的價值,就是讓「換引擎」這件事對使用端透明。

掃描迴圈本身也有省電巧思:requestAnimationFrame 把相機幀畫到離屏 canvas,但節流到約 8fps 才解碼一次——人眼掃 QR 不需要 60fps,省下的是手機電量。命中就立刻關相機、出結果。

結果處理也顧到安全:掃到網址時,「開啟連結」一律加 rel="noopener noreferrer"www. 自動補 https,並永遠同時顯示原始文字讓你先確認再點。沒相機或權限被拒時,友善引導改用「上傳圖片」——走的還是同一套 decodeFrame,一份解碼邏輯兩種入口。

數位放大鏡:為什麼用 CSS 縮放而不是硬體變焦

數位放大鏡讓你用相機即時放大看清小字——老花、藥品說明、合約細項都用得上。

它有個關鍵的技術取捨:用 CSS transform: scale() 縮放,而不是相機的硬體變焦約束。 硬體 zoom 只有部分裝置支援,跨瀏覽器很不一致;CSS 縮放則到處都能用、行為一致。放大鏡寧可選「到處都行」,也不選「某些裝置上更好、某些裝置上沒有」。

縮放(1–8×)配上拖曳平移,平移量用 (zoom-1)×尺寸/2 夾擠,確保放大後不會把畫面拖到外面變黑邊。

還有幾個體貼的細節:

  • 凍結:把當前相機幀 drawImage 到 canvas,之後縮放/平移/濾鏡都套在這張靜止畫面上——手抖也能慢慢看。
  • 拍照:把目前來源以原始解析度畫到離屏 canvas,toBlob 下載 PNG。
  • 補光:偵測 track.getCapabilities().torch,支援的話用 applyConstraints 開手電筒——暗處看小字的救星。
  • 濾鏡:高對比/灰階/反白用 CSS filter 一鍵切換,提升可讀性。

三種感測器,同一套紀律

寫到第三個感測器工具,有些習慣已經變成肌肉記憶:

  • 原生優先、後備保底(QR 的 BarcodeDetector→jsQR)。
  • 跨瀏覽器一致 > 單機最佳(放大鏡選 CSS 縮放)。
  • 借了硬體要還乾淨:停止與 pagehide 一律釋放 MediaStream 軌道,那顆相機指示燈才會準時熄滅。
  • 能力偵測,不假設:torch 有沒有、BarcodeDetector 在不在,都先問過再用。

學到的事

  • 相機是隱私分量最重的感測器,也最該本地處理。 鏡頭畫面不離開裝置,是這兩個工具的底線。
  • 原生優先 + 降級,是面對瀏覽器差異的標準答案。 用得到新 API 的享受速度,用不到的也不被排除。
  • 跨瀏覽器一致,常比單機最佳更重要。 CSS 縮放勝過硬體變焦,就是這個取捨。
  • 能力偵測讓功能「有就更好、沒有也能用」。 torch 是加分,不是前提。

一以貫之的問題

動作、聲音、影像——瀏覽器能直接讀的三種硬體,我們都把它變成了就地、好用、又不上傳的工具。系列的問題在這裡收成了一個漂亮的形狀:凡是裝置自己感知得到的,瀏覽器多半就能當場把它變成工具,而資料不必交給任何人。

QR、放大——相機的答案,也是:可以。

下一篇開發紀錄,我們終於要走進那條醞釀已久的長線——把整個網站變成 AI agent 讀得懂的知識包(OKF)與 llms.txt。想追蹤系列?把 LocalPapa Notes 加入書籤吧。

Dev Log: Turning the Camera into Tools — QR Scanner and Digital Magnifier

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.

探索 39+ 個隱私優先的瀏覽器工具
全程本地運算、檔案不上傳。
前往 LocalPapa →
Explore 39+ privacy-first browser tools
Everything runs locally — your files never leave your device.
Visit LocalPapa →

想看英文版?點右上角 EN 切換語言。

Prefer Chinese? Tap at the top-right to switch.