猫の手ツール

Algorithm & UI/UX

デジタルに「にじみ」の体温を宿す:
Canvas合成による水彩画シミュレーション

水彩画の美しさは、紙の上で色が予期せぬ方向へ広がる「滲み(しがみ)」と、乾く際に縁に色が溜まる「濡れ縁」にあります。これをブラウザ上のCanvasだけでいかに軽量に、かつエモく再現するかが本ツールの最大の挑戦でした。

1. 「濡れ縁」を再現する多層ブレンドアルゴリズム

単に画像をぼかすだけでは、単なるピントのボケた写真になってしまいます。水彩画特有の「エッジ部分にインクが溜まる現象」を再現するために、このツールでは「乗算(Multiply)」と「わずかなオフセット」を組み合わせたレイヤー構造を採用しています。

具体的には、元の画像に対してブラーをかけたレイヤーを、数ピクセルずつ拡張・オフセットさせて「乗算」で重ね合わせます。これにより、色の濃い部分が周囲に滲み出しつつ、境界線が深みを増す「ウェットエッジ」の質感をシミュレートしています。

// 滲みエフェクトのコアコンセプト
function renderWatercolorBleed(ctx, img, settings) {
    const { bleed, softness } = settings;
    
    // 1. ベースのソフトブラー描画
    ctx.filter = `blur(${/* 独自の係数による計算 */}) saturate(${/* スタイル別の補正値 */})`;
    ctx.globalAlpha = // 独自の透過度調整値;
    ctx.drawImage(img, 0, 0);

    // 2. 「乗算」による滲みの重なり
    ctx.globalCompositeOperation = 'multiply';
    const offset = // スライダー値に基づくオフセット計算;
    
    // わずかに拡大・移動させて重ねることで「濡れ縁」を表現
    ctx.filter = `blur(${/* 強調されたブラー値 */}) contrast(${/* 独自のコントラスト比 */})`;
    ctx.globalAlpha = // 独自の乗算強度;
    ctx.drawImage(img, -offset, -offset, width + offset * 2, height + offset * 2);
}

2. 動的テクスチャ生成による「紙の凹凸」シミュレーション

水彩画のリアリティを支えるもう一つの要素は、水彩紙のザラザラとした質感です。外部から重いテクスチャ画像を読み込むのではなく、実行時に小さな「ノイズキャンバス」をメモリ上に生成し、それを「オーバーレイ(Overlay)」モードでタイル状に繰り返して合成しています。

この手法のメリットは、解像度に合わせてノイズの粒度を動的に変更できる点と、ネットワークトラフィックをゼロに抑えられる点にあります。

// 紙の質感を生成するロジック
function createPaperTexture(intensity) {
    const texCanvas = document.createElement('canvas');
    // パフォーマンス維持のため、小さなタイルサイズ(256px等)で生成
    texCanvas.width = texCanvas.height = 256;
    const tCtx = texCanvas.getContext('2d');

    const imageData = tCtx.createImageData(256, 256);
    // ピクセルごとにランダムな輝度を注入
    for (let i = 0; i < imageData.data.length; i += 4) {
        // ここに独自のノイズ生成アルゴリズムが入ります
        const gray = /* ランダム計算 */;
        imageData.data[i] = imageData.data[i + 1] = imageData.data[i + 2] = gray;
        imageData.data[i + 3] = intensity * /* チューニング係数 */;
    }
    tCtx.putImageData(imageData, 0, 0);
    return texCanvas;
}

// 描画時にオーバーレイでパターン適用
const pattern = ctx.createPattern(texCanvas, 'repeat');
ctx.globalCompositeOperation = 'overlay';
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, width, height);

Developer's Note

実装において最もこだわったのは、スライダーを動かした時の「ぬるぬる感(リアルタイム性)」です。高解像度のまま全ての処理を行うとブラウザが悲鳴を上げるため、内部的には最大解像度を1500pxに制限し、描画ループには requestAnimationFrame を活用して入力を間引いています。

また、iPhoneユーザーが画像を保存できないというWebツール特有の弱点(ブラウザの仕様)に対して、あえて「長押し保存」を誘導するモーダルをUIに組み込んだのは、システムエンジニアとしての泥臭い工夫です。技術は「動くこと」だけでなく、「ユーザーの手元に結果が届くこと」までがセットですから。