canvasのお絵描きツールに塗りつぶし機能をつけてみる

塗りつぶし機能を作ってみようと思ったけど、いろいろ苦戦。再帰を使えば簡単にできると思ったのですが、小さい範囲ならともかく、ちょっと大きくなるとブラウザに怒られました。

というわけで、JavaScriptに以下のようなコードを記述。

// 色比較用関数
function compareColor(ImageData, x, y,selectColorRGB,isAlpha){
    // xやyがcanvasの域内に収まっていなければfalseを返す
    if(x<0 || y<0 || x>=ImageData.width || y>=ImageData.height){
        return false;
    }
    
    var currentColorRGB = new Array(3);
    currentColorRGB[0] = ImageData.data[(y*ImageData.width+x)*4+0];
    currentColorRGB[1] = ImageData.data[(y*ImageData.width+x)*4+1];
    currentColorRGB[2] = ImageData.data[(y*ImageData.width+x)*4+2];
    var alphaInfo = ImageData.data[(y*ImageData.width+x)*4+3];

    if(alphaInfo !== 0){
        // 最初に選択した色と現在処理している色が違うならばfalseを返す
        if(selectColorRGB[0] !== currentColorRGB[0] ||
          selectColorRGB[1] !== currentColorRGB[1] ||
          selectColorRGB[2] !== currentColorRGB[2]){
            return false;
        }
        // 最初に選択した色が透明で現在処理中の色が透明でなければfalseを返す
        if(isAlpha){
            return false;
        }
    // 最初に選択した色が透明でなく、現在処理中の色が透明であればfalseを返す
    }else if(alphaInfo === 0 && !isAlpha){
        return false;
    }
    
    return true;
}

// 塗りつぶし用関数
// ImageData:canvasのgetImageDataで取得したデータ
// x,y:現在の座標位置
// fillColor:塗りつぶし用の色の16進数コード(赤:FF0000,白:FFFFFF) もしくは、RGB値を入れた配列
function regionFill(ImageData, x, y, fillColor){
    /*
    console.log("regionFill");
    console.log("x:" + x + "-y:" + y);
    console.log("width:" + ImageData.width + "-height:" + ImageData.height);
    */
    var fillColorRGB = fillColor;
    if(x<0 || y<0 || x>=ImageData.width || y>=ImageData.height){
        return;
    }
    
    if(typeof fillColorRGB === 'string'){
        fillColorRGB = [parseInt(fillColor.substr(0,2),16),
                        parseInt(fillColor.substr(2,2),16),
                        parseInt(fillColor.substr(4,2),16)];
    }
    var selectColorRGB = new Array(3);
    selectColorRGB[0] = ImageData.data[(y*ImageData.width+x)*4+0];
    selectColorRGB[1] = ImageData.data[(y*ImageData.width+x)*4+1];
    selectColorRGB[2] = ImageData.data[(y*ImageData.width+x)*4+2];
    var alphaInfo = ImageData.data[(y*ImageData.width+x)*4+3];
    
    var isAlpha = !Boolean(alphaInfo);
    if(alphaInfo !== 0){
        // 塗りつぶす色と現在処理している色が同じならば関数を抜ける
        if(fillColorRGB[0] === selectColorRGB[0] &&
          fillColorRGB[1] === selectColorRGB[1] &&
          fillColorRGB[2] === selectColorRGB[2]){
            console.log("return");
            return;
        }
    }
    
    var pxlArr = [{ x:x, y:y}];
    var idx,p;
    
    while(pxlArr.length){
        p = pxlArr.pop();
        
        // 現在のピクセル
        if(compareColor(ImageData, p.x, p.y,selectColorRGB,isAlpha)){
            idx = (p.y*ImageData.width+p.x)*4;
            ImageData.data[idx+0] = fillColorRGB[0];
            ImageData.data[idx+1] = fillColorRGB[1];
            ImageData.data[idx+2] = fillColorRGB[2];
            ImageData.data[idx+3] = 255;
            
            // 上
            if( compareColor(ImageData, p.x, p.y-1,selectColorRGB,isAlpha) ){
                pxlArr.push({x:p.x, y:p.y-1});
            }
            // 右
            if( compareColor(ImageData, p.x+1, p.y,selectColorRGB,isAlpha) ){
                pxlArr.push({x:p.x+1, y:p.y});
            }
            // 下
            if( compareColor(ImageData, p.x, p.y+1,selectColorRGB,isAlpha) ){
                pxlArr.push({x:p.x, y:p.y+1});
            }
            
            // 左
            if( compareColor(ImageData, p.x-1, p.y,selectColorRGB,isAlpha) ){
                pxlArr.push({x:p.x-1, y:p.y});
            }
            
        }
        
    }
}

上記関数を呼び出す時には下記のような記述。

if(paintFlag === "fill"){
    
    ImageData = ctx.getImageData(0,0,canvas.width,canvas.height);
    var colorCode = $('.color')[0].value;
    var rgbCode = new Array(3);
    rgbCode[0] = parseInt(colorCode.substr(0,2),16)
    rgbCode[1] = parseInt(colorCode.substr(2,2),16)
    rgbCode[2] = parseInt(colorCode.substr(4,2),16)
    console.log('colorValue-10進数:' + rgbCode);
    regionFill(ImageData, startX, startY, rgbCode);
    /*
    赤い点を打つ
    ImageData.data[(startY*ImageData.width+startX)*4] = 255;
    ImageData.data[(startY*ImageData.width+startX)*4+3] = 255;
    */
    
    ctx.putImageData(ImageData, 0, 0);
}

以下、動作サンプル。
canvasを使ったお絵描き投稿システム

なんだか汚いコードですみません。似たような記述が二箇所にあったりしてますしね・・・。
ようは、ピクセルごとに色を取得して、選択したピクセルが塗りつぶす色と違うようであればそのピクセルを塗りつぶし、さらにその点の上下左右の点を見て先ほど選択したピクセルの元の色と同じであれば塗りつぶすということを繰り返しています。
あまりいいやり方とはいえないかも。多分、下記のようなアルゴリズムを使ったほうが効率的かもしれません。
塗りつぶしアルゴリズム(scanline seed fill algorithm) – jsdo.it – Share JavaScript, HTML5 and CSS
ペイント・ルーチン (2)アルゴリズムの高速化
ただ、自分には何をやってるのか全く分からなかったので、もっと分かりやすい方法で実装しています。その分、ちょっと遅いとは思います・・・。

線にアンチエイリアスがかかって塗りつぶしが思いもよらない結果になる時があるようです。てっきり、アンチエイリアスはアルファ値で調節してるだけだと思ったので、アルファ値を考慮しない(というより0かそうでないかしか見ていない)アルゴリズムにしたのだけれども、そういうわけではないよう。どうして、lineToの機能にアンチエイリアスをかけないようにする指定がないのだろうか・・・。
ctx.translate(0.5,0.5);としたらいいとどこかに書いてあったのだけれども、あまり関係なさそうだった。

コメント

タイトルとURLをコピーしました