猫の手ツール

Algorithm & UI/UX

Canvasで実現する「エモい」質感。
モバイル向け最適化と独自ブラー

「水滴レンズ風ジェネレーター」は、一見シンプルな画像加工ツールですが、ブラウザ(特にモバイル環境)での動作を安定させるために、Canvas APIの性能を限界まで引き出す工夫が凝らされています。本稿ではその中核となるロジックを紐解きます。

1. モバイルSafariを考慮した「疑似ブラー」の実装

通常、Canvasでぼかしを表現する場合 filter: blur() を使用するのが一般的ですが、iOSのSafariなど一部のモバイル環境では、高解像度の画像に対してこのフィルタを適用すると、描画が著しく重くなったり、ブラウザがクラッシュしたりする問題があります。

当ツールではこの問題を回避するため、オフスクリーンキャンバスを利用した「多重縮小転送方式」による疑似ブラーを採用しています。

// 疑似ブラーの概念
const scale = 1 / (blurAmount * B_COEFF + 1); // 独自のチューニング値
const offCanvas = document.createElement('canvas');
// ...キャンバスのサイズを極端に小さくして描画...

// 縮小した画像を、透明度を下げながら少しずつずらして重ねる
ctx.globalAlpha = ALPHA_STEP; // 独自の透明度ステップ
for (let [ox, oy] of OFFSETS) { // 独自のオフセット配列
    ctx.drawImage(offCanvas, ox * spread, oy * spread, CW, CH);
}

この手法のメリットは、GPU負荷の高いガウスぼかしの計算を、単純な画像の拡大・縮小処理に置き換えられる点にあります。これにより、古いスマートフォンでもスライダー操作に追従する滑らかなプレビューを実現しました。

2. ピクセル操作による「エモい」カラーグレーディング

単に水滴を描画するだけでは、写真と合成した際に質感が浮いてしまいます。当ツールでは、描画されたすべてのピクセルに対してリアルタイムで色域の補正を行っています。

特にこだわったのは、Luma(輝度)ベースの彩度・明るさ調整です。CSSフィルタの saturatebrightness を使わず、あえて getImageData で直接ピクセルを弄ることで、ノスタルジックな質感を演出しています。

// ピクセルごとの色域変換ロジック
const imgData = ctx.getImageData(0, 0, CW, CH);
const data = imgData.data;

for (let i = 0; i < data.length; i += 4) {
    // 輝度の計算(人間が色を感じる比率に基づいた係数を使用)
    const luma = R_LUMA * r + G_LUMA * g + B_LUMA * b;

    // 彩度と明るさを同時にチューニング
    // ここに独自のカラーグレーディング計算処理が入ります
    data[i]   = Math.min(255, Math.max(0, adjustedRed));
    data[i+1] = Math.min(255, Math.max(0, adjustedGreen));
    data[i+2] = Math.min(255, Math.max(0, adjustedBlue));
}
ctx.putImageData(imgData, 0, 0);

この独自実装により、単なる色の強調ではなく、暗部が少し沈み、光のボケが引き立つような「記憶の中の景色」に近い色合いを生み出しています。

3. 描画負荷を軽減する「水滴キャッシュシステム」

「水滴の量」スライダーを動かした際、毎回ランダムに水滴を生成すると、水滴の位置がパラパラと動いてしまい、不自然な見え方になります。これを防ぎ、かつ計算量を減らすために、あらかじめ大量の水滴データをメモリ上にキャッシュしています。

// 初期化時に500個程度の水滴パラメータを事前生成
const cachedDroplets = Array.from({length: MAX_DROPS}, () => ({
    x: Math.random() * CW,
    y: Math.random() * CH,
    r: // 独自のサイズ範囲計算,
    opacity: // 独自の透明度計算
}));

// レンダリング時は必要な個数分だけをループで回す
const numDrops = Math.floor((amount / MAX_VAL) * MAX_DROPS);
for (let i = 0; i < numDrops; i++) {
    const drop = cachedDroplets[i];
    // ...Canvasへの円形描画処理...
}

このように「描画すべき情報の決定」と「実際の描画」を分離することで、複雑なベクター描画を伴う水滴の追加処理を極めて高速に実行できるようにしました。

Developer's Note

画像加工ツールをブラウザで作る際、最も高い壁となるのが「デバイスごとの挙動の差異」です。特にiOSのブラウザはメモリ制限が厳しく、大きな画像を扱うとすぐに真っ白になってしまうことがあります。

今回の実装では、あえて標準の filter プロパティに頼らず、原始的なピクセル操作や縮小転送を組み合わせる「泥臭いハック」を積み重ねました。結果として、アプリ不要でありながら、プロ仕様のレタッチアプリに近い質感を提供できたと自負しています。