帆布填充填充不填充边缘

时间:2016-12-23 15:38:37

标签: javascript html5 canvas flood-fill

我正在使用填充填充算法来填充画布上绘制的圆圈。我遇到的问题是算法没有填充到圆的边缘。

以下是基于this blog post的算法:

function paintLocation(startX, startY, r, g, b) {
    var colorLayer = context1.getImageData(0, 0, canvasWidth, canvasHeight);
    pixelPos = (startY * canvasWidth + startX) * 4;

    startR = colorLayer.data[pixelPos];
    startG = colorLayer.data[pixelPos + 1];
    startB = colorLayer.data[pixelPos + 2];

    var pixelStack = [
        [startX, startY]
    ];

    var drawingBoundTop = 0;
    while (pixelStack.length) {
        var newPos, x, y, pixelPos, reachLeft, reachRight;
        newPos = pixelStack.pop();
        x = newPos[0];
        y = newPos[1];

        pixelPos = (y * canvasWidth + x) * 4;
        while (y-- >= drawingBoundTop && matchStartColor(colorLayer, pixelPos, startR, startG, startB)) {
            pixelPos -= canvasWidth * 4;
        }
        pixelPos += canvasWidth * 4;
        ++y;
        reachLeft = false;
        reachRight = false;
        while (y++ < canvasHeight - 1 && matchStartColor(colorLayer, pixelPos, startR, startG, startB)) {
            colorPixel(colorLayer, pixelPos, r, g, b);

            if (x > 0) {
                if (matchStartColor(colorLayer, pixelPos - 4, startR, startG, startB)) {
                    if (!reachLeft) {
                        pixelStack.push([x - 1, y]);
                        reachLeft = true;
                    }
                } else if (reachLeft) {
                    reachLeft = false;
                }
            }

            if (x < canvasWidth - 1) {
                if (matchStartColor(colorLayer, pixelPos + 4, startR, startG, startB)) {
                    if (!reachRight) {
                        pixelStack.push([x + 1, y]);
                        reachRight = true;
                    }
                } else if (reachRight) {
                    reachRight = false;
                }
            }

            pixelPos += canvasWidth * 4;
        }
    }
    context1.putImageData(colorLayer, 0, 0);
}

请参阅JSFiddle或下图,了解我的意思。单击任何圆圈内部将更改黄色和黑色之间的颜色(黑色时问题更明显)。

我已经读过这个问题可能与抗锯齿有关,我尝试用context1.imageSmoothingEnabled = true;关闭它,但它没有什么区别。

Flood fill issue

我也尝试按照this问题更改我的matchStartColour函数,但这没有帮助。

function matchStartColor(colorLayer, pixelPos, startR, startG, startB) {
    var r = colorLayer.data[pixelPos];
    var g = colorLayer.data[pixelPos + 1];
    var b = colorLayer.data[pixelPos + 2];

    return (r == startR && g == startG && b == startB);
}

我认为这可能与圆圈没有填充颜色并且画布的背景不是白色而是透明的黑色这一事实有关。我曾尝试将画布背景更改为白色,但这也无济于事。

1 个答案:

答案 0 :(得分:4)

使用填充填充创建蒙版

前几天我碰巧做了一次洪水填充,解决了抗锯齿边缘的问题。

而不是直接绘制到画布,我绘制到一个字节数组,然后用于创建一个掩码。掩码允许设置alpha值。

填充可以有一个tolerance和一个toleranceFade来控制处理容差值的颜色。

当起始颜色和容差之间的像素差异大于(tolerance - toleranceFade)时,我将该像素的alpha设置为255 - ((differance - (tolerance - toleranceFade)) / toleranceFade) * 255,这会在线条边缘创建一个漂亮的平滑混合。虽然它不适用于高对比度情况的所有情况,但它是一种有效的解决方案。

以下示例显示了使用和不使用toleranceFade的结果。蓝色没有toleranceFade,红色是tolerance设置为190,toleranceFade设置为90。

您必须使用该设置来获得满足您需求的最佳效果。

&#13;
&#13;
function showExample(){
    var canvas = document.createElement("canvas");
    canvas.width = 200;
    canvas.height = 200;
    var ctx = canvas.getContext("2d");
    document.body.appendChild(canvas);
    ctx.fillStyle = "white"
    ctx.fillRect(0,0,canvas.width,canvas.height)
    ctx.lineWidth = 4;
    ctx.strokeStyle = "black"
    ctx.beginPath();
    ctx.arc(100,100,90,0,Math.PI * 2);
    ctx.arc(120,100,60,0,Math.PI * 2);
    ctx.stroke();
    ctx.fillStyle = "blue";
    floodFill.fill(100,100,1,ctx)
    ctx.fillStyle = "red";
    floodFill.fill(40,100,190,ctx,null,null,90)
}

// FloodFill2D from https://github.com/blindman67/FloodFill2D
var floodFill = (function(){
    "use strict";
    const extent = {
        top : 0,
        left : 0,
        bottom : 0,
        right : 0,        
    }
    var keepMask = false; // if true then a mask of the filled area is returned as a canvas image
    var extentOnly = false;  // if true then the extent of the fill is returned
    var copyPixels = false; // if true then creating a copy of filled pixels
    var cutPixels = false;  // if true and copyPixels true then filled pixels are removed
    var useBoundingColor = false; // Set the colour to fill up to. Will not fill over this colour
    var useCompareColor = false; // Rather than get the pixel at posX,posY use the compareColours
    var red, green, blue, alpha; // compare colours if 
    var canvas,ctx;    
    function floodFill (posX, posY, tolerance, context2D, diagonal, area, toleranceFade) {
        var w, h, painted, x, y, ind, sr, sg, sb, sa,imgData, data, data32, RGBA32, stack, stackPos, lookLeft, lookRight, i, colImgDat, differance, checkColour;
        toleranceFade = toleranceFade !== undefined && toleranceFade !== null ? toleranceFade : 0;
        diagonal = diagonal !== undefined && diagonal !== null ? diagonal : false;
        area = area !== undefined && area !== null ? area : {};
        area.x = area.x !== undefined ? area.x : 0;
        area.y = area.y !== undefined ? area.y : 0;
        area.w = area.w !== undefined ? area.w : context2D.canvas.width - area.x;
        area.h = area.h !== undefined ? area.h : context2D.canvas.height - area.y;
        // vet area is on the canvas.
        if(area.x < 0){
            area.w = area.x + area.w;
            area.x = 0;
        }
        if(area.y < 0){
            area.h = area.y + area.h;
            area.y = 0;
        }
        if(area.x >= context2D.canvas.width || area.y >= context2D.canvas.height){
            return false;
        }
        if(area.x + area.w > context2D.canvas.width){
            area.w = context2D.canvas.width - area.x;
        }
        if(area.y + area.h > context2D.canvas.height){
            area.h = context2D.canvas.height - area.y;
        }
        if(area.w <= 0 || area.h <= 0){
            return false;
        }    
        w = area.w;   // width and height
        h = area.h;
        x = posX - area.x;   
        y = posY - area.y;    
        if(extentOnly){
            extent.left = x; // set up extent
            extent.right = x;
            extent.top = y;
            extent.bottom = y;
        }
        
        if(x < 0 || y < 0 || x >= w || y >= h){
            return false;  // fill start outside area. Don't do anything
        }
        if(tolerance === 255 && toleranceFade === 0 && ! keepMask){  // fill all 
            if(extentOnly){
                extent.left = area.x; // set up extent
                extent.right = area.x + w;
                extent.top = area.y;
                extent.bottom = area.y + h;
            }
            context2D.fillRect(area.x,area.y,w,h);
            return true;
        }
        if(toleranceFade > 0){   // add one if on to get correct number of steps
            toleranceFade += 1;
        }


        imgData = context2D.getImageData(area.x,area.y,area.w,area.h);
        data = imgData.data; // image data to fill;
        data32 = new Uint32Array(data.buffer);
        painted = new Uint8ClampedArray(w*h);  // byte array to mark painted area;
        function checkColourAll(ind){
            if( ind < 0 || painted[ind] > 0){  // test bounds
                return false;
            }
            var ind4 = ind << 2;  // get index of pixel           
            if((differance = Math.max(        // get the max channel difference;
                Math.abs(sr - data[ind4++]),
                Math.abs(sg - data[ind4++]),
                Math.abs(sb - data[ind4++]),                
                Math.abs(sa - data[ind4++])
                )) > tolerance){    
                return false;
            }        
            return true
        }         
        // check to bounding colour
        function checkColourBound(ind){
            if( ind < 0 || painted[ind] > 0){  // test bounds
                return false;
            }
            var ind4 = ind << 2;  // get index of pixel
            differance = 0;
            if(sr === data[ind4] && sg === data[ind4 + 1] && sb === data[ind4 + 2] && sa === data[ind4 + 3]){
                return false
            }
            return true
        }         
        // this function checks the colour of only selected channels
        function checkColourLimited(ind){ // check only colour channels that are not null
            var dr,dg,db,da;
            if( ind < 0 || painted[ind] > 0){  // test bounds
                return false;
            }
            var ind4 = ind << 2;  // get index of pixel
            dr = dg = db = da = 0;
            if(sr !== null && (dr = Math.abs(sr - data[ind4])) > tolerance){
                return false;
            }
            if(sg !== null && (dg = Math.abs(sg - data[ind4 + 1])) > tolerance){
                return false;
            }
            if(sb !== null && (db = Math.abs(sb - data[ind4 + 2])) > tolerance){
                return false;
            }
            if(sa !== null && (da = Math.abs(sa - data[ind4 + 3])) > tolerance){
                return false;
            }
            diferance = Math.max(dr, dg, db, da);
            return true
        }         
        // set which function to check colour with
        checkColour = checkColourAll;
        if(useBoundingColor){
            sr = red;
            sg = green;
            sb = blue;
            if(alpha === null){
                ind = (y * w + x) << 2;  // get the starting pixel index
                sa = data[ind + 3];                     
            }else{
                sa = alpha;            
            }
            checkColour = checkColourBound;
            useBoundingColor = false;
        }else if(useCompareColor){
            sr = red;
            sg = green;
            sb = blue;
            sa = alpha;
            if(red === null || blue === null || green === null || alpha === null){
                checkColour = checkColourLimited;
            }
            useCompareColor = false;            
        }else{
            ind = (y * w + x) << 2;  // get the starting pixel index
            sr = data[ind];        // get the start colour that we will use tolerance against.
            sg = data[ind + 1];
            sb = data[ind + 2];
            sa = data[ind + 3];     
        }
        stack = [];          // paint stack to find new pixels to paint
        lookLeft = false;    // test directions
        lookRight = false;

        stackPos = 0;
        stack[stackPos++] = x;
        stack[stackPos++] = y;
        while (stackPos > 0) {   // do while pixels on the stack
            y = stack[--stackPos];  // get the pixel y
            x = stack[--stackPos];  // get the pixel x
            ind = x + y * w;
            while (checkColour(ind - w)) {  // find the top most pixel within tollerance;
                y -= 1;
                ind -= w;
            }
            //checkTop left and right if allowing diagonal painting
            if(diagonal && y > 0){
                if(x > 0 && !checkColour(ind - 1) && checkColour(ind - w - 1)){
                    stack[stackPos++] = x - 1;
                    stack[stackPos++] = y - 1;
                }
                if(x < w - 1 && !checkColour(ind + 1) && checkColour(ind - w + 1)){
                    stack[stackPos++] = x + 1;
                    stack[stackPos++] = y - 1;
                }
            }
            lookLeft = false;  // set look directions
            lookRight = false; // only look is a pixel left or right was blocked
            while (checkColour(ind) && y < h) { // move down till no more room
                if(toleranceFade > 0 && differance >= tolerance-toleranceFade){
                    painted[ind] = 255 - (((differance - (tolerance - toleranceFade)) / toleranceFade) * 255);
                    painted[ind] = painted[ind] === 0 ? 1 : painted[ind]; // min value must be 1
                }else{
                    painted[ind] = 255; 
                }
                if(extentOnly){
                    extent.left   = x < extent.left   ? x : extent.left;    // Faster than using Math.min
                    extent.right  = x > extent.right  ? x : extent.right;   // Faster than using Math.min
                    extent.top    = y < extent.top    ? y : extent.top;     // Faster than using Math.max
                    extent.bottom = y > extent.bottom ? y : extent.bottom;  // Faster than using Math.max
                }
                if (checkColour(ind - 1) && x > 0) {  // check left is blocked
                    if (!lookLeft) {        
                        stack[stackPos++] = x - 1;
                        stack[stackPos++] = y;
                        lookLeft = true;
                    }
                } else if (lookLeft) {
                    lookLeft = false;
                }
                if (checkColour(ind + 1) && x < w -1) {  // check right is blocked
                    if (!lookRight) {
                        stack[stackPos++] = x + 1;
                        stack[stackPos++] = y;
                        lookRight = true;
                    }
                } else if (lookRight) {
                    lookRight = false;
                }
                y += 1;                 // move down one pixel
                ind += w;
            }
            if(diagonal && y < h){  // check for diagonal areas and push them to be painted 
                if(checkColour(ind - 1) && !lookLeft && x > 0){
                    stack[stackPos++] = x - 1;
                    stack[stackPos++] = y;
                }
                if(checkColour(ind + 1) && !lookRight && x < w - 1){
                    stack[stackPos++] = x + 1;
                    stack[stackPos++] = y;
                }
            }
        }
        if(extentOnly){
            extent.top    += area.y;
            extent.bottom += area.y;
            extent.left   += area.x;
            extent.right  += area.x;
            return true;
        }
        canvas = document.createElement("canvas");
        canvas.width = w;
        canvas.height = h;
        ctx = canvas.getContext("2d");
        ctx.fillStyle = context2D.fillStyle;
        ctx.fillRect(0, 0, w, h);
        colImgDat = ctx.getImageData(0, 0, w, h);
        if(copyPixels){
            i = 0;
            ind = 0;
            if(cutPixels){
                while(i < painted.length){
                    if(painted[i] > 0){
                        colImgDat.data[ind] = data[ind];
                        colImgDat.data[ind + 1] = data[ind + 1];
                        colImgDat.data[ind + 2] = data[ind + 2];
                        colImgDat.data[ind + 3] = data[ind + 3] * (painted[i] / 255);
                        data[ind + 3] = 255 - painted[i];
                    }else{
                        colImgDat.data[ind + 3] = 0;
                        
                    }
                    i ++;
                    ind += 4;
                }
                context2D.putImageData(imgData, area.x, area.y);
            }else{
                while(i < painted.length){
                    if(painted[i] > 0){
                        colImgDat.data[ind] = data[ind];
                        colImgDat.data[ind + 1] = data[ind + 1];
                        colImgDat.data[ind + 2] = data[ind + 2];
                        colImgDat.data[ind + 3] = data[ind + 3] * (painted[i] / 255);
                    }else{
                        colImgDat.data[ind + 3] = 0;
                    }
                    i ++;
                    ind += 4;
                }
            }
            ctx.putImageData(colImgDat,0,0); 
            return true;            
            
        }else{
            i = 0;
            ind = 3;
            while(i < painted.length){
                colImgDat.data[ind] = painted[i];
                i ++;
                ind += 4;
            }
            ctx.putImageData(colImgDat,0,0);
        }
        if(! keepMask){
            context2D.drawImage(canvas,area.x,area.y,w,h);
        }
        return true;
    }
    
    return {
        fill : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
            floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade);
            ctx = undefined;
            canvas = undefined;
        },
        getMask : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
            keepMask = true;
            floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade);
            ctx = undefined;
            keepMask = false;
            return canvas;
        },
        getExtent : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
            extentOnly = true;
            if(floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade)){
                extentOnly = false;
                return {
                    top : extent.top,
                    left : extent.left,
                    right : extent.right,
                    bottom : extent.bottom,
                    width : extent.right - extent.left,
                    height : extent.bottom - extent.top,
                }
            }
            extentOnly = false;
            return null;
        },
        cut : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
            cutPixels = true;
            copyPixels = true;
            floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade);
            cutPixels = false;
            copyPixels = false;
            ctx = undefined;
            return canvas;
        },
        copy : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){
            cutPixels = false;
            copyPixels = true;
            floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade);
            copyPixels = false;
            ctx = undefined;
            return canvas;            
        },
        setCompareValues : function(R,G,B,A){
            if(R === null && G === null && B === null && A === null){
                return;
            }
            red = R;
            green = G;
            blue = B;
            alpha = A;
            useBoundingColor = false;
            useCompareColor = true;
        },
        setBoundingColor : function(R,G,B,A){
            red = R;
            green = G;
            blue = B;
            alpha = A;
            useCompareColor = false;
            useBoundingColor = true;
        }
    }
}());


showExample();
&#13;
Red floodFill.fill(40,100,190,ctx,null,null,90) tolerance 190, tolerance fade 90<br>Blue floodFill.fill(100,100,1,ctx) tolerance 1.<br>
&#13;
&#13;
&#13;

有关详细信息,请参阅Github FloodFill2D

上的自述文件