猫の手ツール

Canvas Interaction & State Management

ビューポートと解像度の差異を埋めるスタンプの座標変換の裏側

SNSで会話をシェアする際、相手のアイコンを「指先一つで、綺麗に」隠す。一見シンプルですが、高解像度なスクリーンショットをWebブラウザで軽快に扱うためには、数学的な座標変換とメモリ管理の工夫が不可欠です。

このツールの入力から出力まで

現行コードの確認日:

入力
スクリーンショットを選び、丸または絵文字を置く場所とサイズを指定します。
処理
表示サイズと処理画像の寸法から位置を換算し、画像の短辺を基準に覆うサイズを決めます。Undoは画素のスナップショットを戻します。
出力
読み込み時は長辺 2000px 以下に縮小し、JPEG(品質指定 0.95)で保存します。

まず確認する点:アイコンを隠した結果でも会話文や名前は残り得ます。縦長の画像は画面で小さくなるため、保存画像の端まで読み直します。

実装の確認箇所:src/pages/image/line-anonymize/index.astro。以下のコード抜粋と出力条件は、この本番実装に基づいて説明します。

仕組みを読み進めるときは、メモリ管理と画像処理(画素バッファ、縮小と容量予算)、Canvas APIと画像処理(画像寸法、drawImage、画素と透明度の扱い)が参考になります。

1. CSSスケーリングを考慮した「双方向座標マッピング」

このツールでは、Canvasの物理サイズ(画像本来の解像度)と、画面上の表示サイズ(CSSによる縮小表示)が一致しません。そのままタップ座標を取得すると、隠したい場所とスタンプの位置が大きくズレてしまいます。

これを解決するため、getBoundingClientRect() で取得した要素の矩形サイズと、Canvas自体の width / height から動的にスケール比率を算出し、タップ位置を再計算するマッピングロジックを実装しています。

現行実装は共通ヘルパーcreateBoundedHistoryへ32MiBの予算を渡します。下は同じ容量制御の要点です。historyはImageDataの配列で、単独の画像が予算を超える場合は履歴へ追加しません。

function saveHistory(ctx, history, maxBytes = 32 * 1024 * 1024) {
  const snapshot = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  if (snapshot.data.byteLength > maxBytes) return;
  history.push(snapshot);
  let bytes = history.reduce((sum, image) => sum + image.data.byteLength, 0);
  while (bytes > maxBytes && history.length) {
    bytes -= history.shift().data.byteLength;
  }
}

この実装により、縦に非常に長いスクリーンショットであっても、指でタップした位置に表示から画像座標へ換算して「隠しマーク」を配置することが可能になっています。

2. ImageDataスタックによる軽量Undoシステム

加工ミスを防ぐための「1つ戻る(Undo)」機能は、通常であれば描画命令の履歴(パス)を保持し、再描画を行うことで実現します。しかし、スタンプや色塗りを行うビットマップ処理では、命令が増えるほど再描画コストが増大します。

本ツールでは、ImageData(ピクセルデータ)のフルスナップショットを配列に退避させる方式を採用しました。これにより、どんなに複雑な加工を行った後でも、過去の操作をすべて再生せず、1枚分の画素データを書き戻せます。書き戻しの量は画像の画素数に依存します。

履歴は枚数ではなく、ImageDataの合計容量を32MiB以内に制限します。たとえば2000×2000pxの状態は約16MBなので2枚までですが、小さい画像ではもっと多く戻せます。元画像や描画用Canvasは別にメモリを使うため、端末全体のメモリ不足を防ぐ保証ではありません。

出力の仕様と確認する点

読み込み時は長辺 2000px 以下に縮小し、JPEG(品質指定 0.95)で保存します。タップした場所に円または絵文字を置き、置いた後の移動はできないため、位置がずれた場合は Undo して置き直します。

サイズは画像の短辺に対する割合です。縦に長いスクリーンショットは縮小表示で細部が見えにくくなるので、保存した画像でもアイコンや名前の端が残っていないか確認してください。

絵文字は字形の空白や透けた部分があり、覆う範囲を自動判定しません。円で十分な範囲を覆う場合も、本文中の名前・日時・引用・反射など別の識別情報はそのまま残ります。完全な匿名性を保証する機能ではありません。