javascript - Canvas 洪水填充未填充到边缘

标签 javascript html canvas flood-fill

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

这里是基于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);
}

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

最佳答案

使用洪水填充创建 mask

前几天我刚好做了一个 floodFill,解决了抗锯齿边缘的问题。

我没有直接在 Canvas 上绘制,而是绘制到一个字节数组,然后用于创建 mask 。掩码允许设置 alpha 值。

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

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

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

您必须尝试设置才能获得满足您需求的最佳结果。

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();
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>

有关更多信息,请参阅 Github FloodFill2D 上的自述文件

关于javascript - Canvas 洪水填充未填充到边缘,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41304168/

相关文章:

javascript - 如何删除页面消息?

javascript - Angularjs 不发布隐藏值

javascript - 长度 - 1 比 JS 中应有的长度要长

css - 我怎样才能让我的水平导航栏跨越整个宽度?

javascript - chrome 扩展 - 页面重定向后的 chrome.tabs.executeScript

javascript - jquery float图表在接近点时突出显示点

javascript - 如何根据用户输入更改背景图像 | HTML、CSS、JS

javascript - 等距 map 上的平滑角色移动

javascript - 使用 HTML5 canvas 生成动画 GIF

html - 使用文本屏蔽视频