猫の手ツール

Algorithm & UI/UX

ブラウザ完結型・顔隠しスタンプツールの高度な座標変換とリソース管理

「画像をサーバーに送らずに加工したい」というプライバシー要求と、「スマホでもサクサク快適に操作したい」という操作性を両立させる。本稿では、HTML5 Canvasを用いたレスポンシブな座標計算と、メモリ効率を考慮した履歴管理の仕組みを解剖します。

1. レスポンシブ環境における高精度な座標マッピング

このツールの最大の特徴は、表示上のサイズ(CSSピクセル)と、Canvasの内部解像度(実際の画像ピクセル)を動的に同期させる座標計算アルゴリズムにあります。画面サイズに合わせてプレビューが縮小されていても、常に「タップした正確な位置」にスタンプを配置するため、独自のスケール変換ロジックを実装しています。

getBoundingClientRect() を用いて画面上の描画領域を取得し、Canvasの内部解像度との比率を算出することで、デバイスの種類を問わない精密な編集を可能にしています。

function getMappedPosition(event, container, canvas) {
    const rect = container.getBoundingClientRect();
    
    // 表示サイズと内部解像度の比率を算出
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;

    // クライアント座標からCanvas内のピクセル座標へ変換
    const clientX = event.touches ? event.touches[0].clientX : event.clientX;
    const clientY = event.touches ? event.touches[0].clientY : event.clientY;

    return {
        x: (clientX - rect.left) * scaleX,
        y: (clientY - rect.top) * scaleY
    };
}

2. 解像度を維持するスマートな動的リサイズとHEIC対応

ブラウザのメモリ消費を抑えつつ、保存時の画質を担保するため、読み込み時に動的なリサイズ処理を施しています。長辺が特定の閾値を超える巨大な画像のみを、アスペクト比を維持したまま縮小することで、スマートフォンのブラウザでもクラッシュすることなく動作を継続できます。

また、iPhone独自のHEIC形式をクライアントサイドでJPEGに変換する非同期読み込みを採用しており、サーバーを介さない「プライバシー重視」のアーキテクチャを実現しています。

async function processImage(file) {
    // HEIC変換が必要な場合の動的インポート
    if (isHeicFormat(file)) {
        const convertor = await import('library-name');
        // ...変換処理
    }

    // Canvasへの描画と動的なサイズ抑制
    const MAX_RESOLUTION = // 独自のチューニング値;
    let { width, height } = originalImage;
    
    if (width > MAX_RESOLUTION || height > MAX_RESOLUTION) {
        const scale = MAX_RESOLUTION / Math.max(width, height);
        width = Math.floor(width * scale);
        height = Math.floor(height * scale);
    }
    
    canvas.width = width;
    canvas.height = height;
    // ...描画実行
}

3. ImageDataを活用した高速な履歴管理(Undo)

Canvas上の編集を元に戻す機能には、ピクセル情報を直接保持する ImageData 配列によるスタック管理を採用しました。各ステップの状態を丸ごとバッファに保存することで、複雑なベクトル情報を保持することなく、瞬時のUndoを実現しています。

メモリ枯渇を防ぐため、スタックのサイズには独自の閾値を設け、古い履歴から順に破棄されるリングバッファ的な管理を行っています。

function saveHistoryState(context, canvas) {
    const MAX_HISTORY = // 独自の保存上限数;
    
    if (historyStack.length >= MAX_HISTORY) {
        historyStack.shift(); // 最古の履歴を破棄してメモリを解放
    }
    
    // 現在のCanvasのピクセル情報をそのまま抽出
    const currentState = context.getImageData(0, 0, canvas.width, canvas.height);
    historyStack.push(currentState);
}

Developer's Note

開発において最もこだわったのは、「直感的な手触り」と「安全性の担保」のトレードオフです。

通常、複雑な画像処理はサーバーサイドで行うのが一般的ですが、本ツールではあえてブラウザ内完結(Client-side only)にこだわりました。これにより、ユーザーは大切な家族や友人の写真を外部に一切漏らすことなく加工できます。

また、単にスタンプを置くだけでなく、スタンプの背後に動的なドロップシャドウをCanvas上でリアルタイムに合成することで、SNS映えする「シールを貼ったような質感」を追求しています。この微細な影の計算にも、解像度に基づいた係数を適用し、画像サイズに関わらず一定の視覚効果が得られるよう調整しています。