Back to Portfolio

Case Study · 06 · OSS · MIT

Frost

指でなぞった場所を氷霜で覆って隠す、全処理が端末内で完結する 画像モザイク PWA

Problem

隠したい画像を、外部サーバーに上げたくない

SNS や記事に写真・スクリーンショットを載せるとき、顔や住所、画面内の名前や番号など「ここだけは隠したい」部分が出てくる。手元の標準機能で黒塗りするか、Web の目隠しツールに画像を上げるか——でも後者は、隠したい画像をわざわざ外部サーバーへ送ることになる。それが気持ち悪かった。

黒塗りも選択肢だが、真っ黒な矩形は確実な反面、無機質で画像の雰囲気を壊す。そして事前に調べた範囲では、ブラウザ完結の画像 redaction(目隠し)ツールは 矩形や楕円で領域を選ぶものが主流で、指でなぞるブラシ式は見当たらなかった。送らない・なぞって隠せる・見た目も悪くない、を同時に満たす道具が手元になかったので、自分で作った。

Approach

ブラウザ内で完結させ、なぞった場所を氷霜で覆う

核は「画像が端末の外に出ない」こと。主張だけでなく、実装の構造でそうなるようにした。読み込みは FileReader、加工は Canvas 2D 上で完結し、画像をサーバーへ送る経路は持たない。保存は canvas.toBlob('image/png') で再エンコードするため、元画像の EXIF / GPS が出力に残らない。オフライン動作用に Service Worker でアプリ本体はキャッシュするが、ユーザーが読み込んだ画像はキャッシュ対象に含めない。送らない・キャッシュしない・再エンコードで EXIF を落とす、を作りに織り込んでいる。

操作は「読み込んで、なぞって、保存」だけ。なぞった軌跡に沿って、その下の画像が加工面に置き換わる。隠し方は モザイク(縮小→拡大のブロック化)と 氷霜ガラス(モザイクを下地に、ぼかし+ガラス質感+霜レイヤーを重ねる)の 2 モード。隠蔽の実体はモザイクとぼかしが担い、氷霜はその上に乗る質感なので、「凍らせる=隠す」という見せ方と機能の向きは一致している。

表示は 4 枚の Canvas を合成して作る——原画・全面加工面・なぞり軌跡の α マスク・合成バッファ。destination-in でマスク内だけ加工面を出す。純粋ロジック(座標変換・モザイク分割・ストローク reducer・氷霜係数・保存方式・ピンチズーム)は副作用なしで src/lib/ に分離し、Canvas / DOM / navigator 依存は配線層に閉じた。この分離のおかげで、座標やズームの面倒な算術を test-first で 71 ケース固められた。

Frost のエディタで、架空のサンプル会員証の氏名・住所・電話番号を指でなぞり、なぞった箇所が氷霜で覆われて隠れていくアニメーション。
Brush to frostなぞった軌跡が氷霜で覆われる

端末内完結とメタデータ除去

画像はネットワークに出さず Canvas 上で処理。保存は再エンコードのため元画像の EXIF / GPS が落ちる。SW のキャッシュはアプリ本体のみで、ユーザー画像はキャッシュしない。

ブラシ式 redaction

Pointer Events でなぞった軌跡を、中心が濃く外周が透けるソフト円スタンプで補間描画。矩形/楕円の領域選択が主流の中で、フリーハンドで隠せる。

モザイク / 氷霜ガラスの 2 モード

氷霜ガラスはモザイクを下地に、ぼかし・ガラス質感・霜レイヤーを多層合成。氷霜スライダーを 0 にすると霜が消え、モザイク+ぼかしだけが残る。

依存ゼロ・約 5KB

単一画面・Canvas 中心のためフレームワークを入れず素の TypeScript で実装。ツール本体のバンドルは gzip 約 5KB。複雑な所(SW・ビルド)だけライブラリに任せる。

ピンチズーム / パン

1 本指で描画、2 本指でズーム・パン。縦長スクショの細部も塗れる。焦点固定の相似変換と表示クランプを純関数化し test-first で実装。

PWA・環境別の保存

ホーム追加で 2 回目以降はオフライン起動。保存は iOS が Web Share、Android / デスクトップは直接ダウンロードと、実機の挙動に合わせて分岐する。

モザイクモードのエディタ画面。会員証の個人情報がブロック状のモザイクで隠され、下部の操作ドックに粗さ・ブラシのスライダーが表示されている。
Mosaic縮小→拡大のブロック化
氷霜ガラスモードのエディタ画面。会員証の個人情報が氷霜の質感で覆われ、下部の操作ドックに粗さ・ぼかし・氷霜・ブラシのスライダーが表示されている。
Frost glassぼかし+霜レイヤーで覆う
Language
vanilla TypeScript (フレームワーク不使用・ランタイム依存ライブラリゼロ)
Rendering
Canvas 2D — 4 枚合成 (原画 / 加工面 / α マスク / バッファ)
Input
Pointer Events — 1 本指=描画 / 2 本指=ズーム・パン
Build
Vite 6 — 5 ページ MPA (LP / ツール本体 / howto / privacy / faq)
PWA
vite-plugin-pwa (Workbox) — アプリシェルのみ precache
Testing
Vitest + happy-dom — 純粋ロジック 71 ケース
Bundle
ツール本体 約 12.3KB (gzip 4.96KB)
License
MIT (public repo) · 霜テクスチャは AI 生成

Result

ブラウザを開けば、その場で目隠しが終わる

c12o-dev/frost-mosaic として MIT で公開し、frost.c12o.net で v0.1.0 を稼働させている。インストール不要でブラウザから使え、ホーム画面に追加すればアプリのように起動し、2 回目以降はオフラインでも動く。自分の画像を選ばなくても ?sample=1 を開けば同梱サンプルが端末内で読み込まれ、なぞる・調整する・保存するの一連をそのまま試せる。

ツール本体(app.html)とは別に、価値を言語化する LP と読み物 3 ページ(使い方 / プライバシー / FAQ)を持つマルチページ構成。読み物ページは JS を持たない静的 HTML に留め、ツール本体の軽さを侵さない。霜テクスチャは AI 生成画像 1 枚を 3.8MB→404KB に WebP 圧縮し、precache 合計は約 946KB に収めた。

Learnings

作って分かったこと

プライバシーは主張ではなく構造で担保する

「ローカルで処理します」と書くだけなら簡単。送らない・キャッシュしない・再エンコードで EXIF を落とす、を設計に織り込むことで、主張を実装で裏打ちできる。

「依存を減らす」はドグマではなく手段

複雑で間違えやすい所(SW=Workbox、ビルド=Vite)はライブラリ、短い算術とネイティブ Canvas は自前、という線引きが、軽量さと保守性を両立させる。

保存 UX は実機の挙動で設計が反転する

当初は全環境で Web Share を優先したが、Windows / Android で共有シートが摩擦になると実機で判明。iOS 限定 Share に絞り、分岐を純関数化してテストで縛った。

純関数に切り出すと、面倒な算術が test-first になる

座標変換・ズームの相似変換・表示クランプを副作用なしの src/lib/ に分離。Canvas / DOM 依存を配線層に閉じたことで、71 ケースで挙動を固められた。