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 ケース固められた。
端末内完結とメタデータ除去
画像はネットワークに出さず 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 / デスクトップは直接ダウンロードと、実機の挙動に合わせて分岐する。
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 ケースで挙動を固められた。