HTML5画布 - 抗锯齿和油漆桶/泼墨工具

4
在 Stack Overflow 和 Google 上搜索后,我发现没有办法在 HTML5 画布上绘制线条时禁用抗锯齿。这样可以得到漂亮的线条,但是在应用油漆桶/填充算法时会出现问题。
我的应用程序的一部分要求用户在画布上自由绘制,使用基本工具如线条大小、颜色和油漆桶。
由于线条是带有抗锯齿渲染的,它们不是一个一致的颜色。请考虑以下情况:
1. 用黑色画一条粗线 2. 决定后来将线变成红色 3. 对黑线应用油漆桶
我的油漆桶算法将大部分线段填充为红色,但是抗锯齿边缘被检测为在应该填充的区域之外...因此保留了(灰色/蓝色(?))黑线剩余的颜色。
油漆桶算法不像 Photoshop 那样包含类似“容差”的东西...我已经考虑过类似的东西,但是我不确定它是否有帮助,因为我认为抗锯齿并不像简单地在黑线旁边渲染灰色那样简单,我认为它更加先进,抗锯齿考虑周围的颜色和混合。
有人有什么建议吗?如何才能获得更好的油漆桶/填充,完全填充/替换现有线条或绘图部分?

其实我的问题类似于这个:http://stackoverflow.com/questions/6087838/flood-fill-algorithm-that-takes-alpha-into-account-without-leaving-fringes-aroun?rq=1 - Tappa Tappa
2个回答

2
如果您只是想改变一条线的颜色,请不要使用桶填充工具。
将所有的线条和形状存储为对象/数组,并在需要时重新绘制它们。
这不仅可以使您更改画布大小而不丢失其上的所有内容,而且更改颜色只是更改对象/数组的颜色属性并重新绘制,以及基于矢量而不是栅格进行缩放。
这比使用桶填充更快,因为大部分重绘在JavaScript中是内部处理的,而不是像桶填充那样逐像素处理。
话虽如此,不幸的是,您无法禁用形状和线条的抗锯齿效果,只能针对图像(使用imageSmoothingEnabled属性)进行禁用。
一个对象可能长这个样子:
function myLine(x1, y1, x2, y2, color) {
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
    this.color = color;
    return this;
}

然后通过以下方式进行分配:

var newLine = new myLine(x1, y1, x2, y2, color);

然后将其存储到一个数组中:
/// globally:
var myLineStack = [];

/// after x1/x2/y1/y2 and color is achieved in the draw function:
myLineStack.push(new myLine(x1, y1, x2, y2, color));

然后只需要在需要更新时遍历对象即可:
/// some index to a line you want to change color for:
myLineStack[index].color = newColor;

/// Redraw all (room for optimizations here...)
context.clearRect( ... );

for(var i = 0, currentLine; currentLine = myLineStack[i]; i++) {

    /// new path
    context.beginPath();

    /// set the color for this line
    context.strokeStyle = currentLine.color;

    /// draw the actual line
    context.moveTo(currentLine.x1, currentLine.y1);
    context.lineTo(currentLine.x2, currentLine.y2);

    context.stroke();
}

(为了进行优化,例如您可以仅清除需要重绘的区域并绘制单个索引。您还可以将具有相同颜色的线条/形状分组,并使用单个strokeStyle设置进行绘制等。)

谢谢回复 :-)我已经将绘制过程存储为事件序列在数组中...这是个好建议,我也推荐任何构建画布绘图应用的人走这条路。你关于更高级地选择和替换线条颜色的建议并不是我正在寻找的,因为我的应用程序应该是用户友好的,适合那些可能从未在计算机界面上绘画的人。很遗憾抗锯齿不能被禁用!我会再考虑一下... - Tappa Tappa
...但是在寻找了一款相当先进的画布绘图应用程序(http://muro.deviantart.com/)后发现它也存在同样的问题,我开始认为这是一个相当困难的问题要解决。目前我能想到的唯一解决方案是构建自己的线条/圆等绘制系统,以便我可以逐像素地渲染每条线和点...这样就不会有任何反锯齿伪影...不确定在iPad绘图表面上用手指实时划动20像素宽的线条会有怎样的性能表现。 - Tappa Tappa
@TappaTappa 你可以尝试实现Bresenham线算法(https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm)。在上下文中(画图应用程序),性能并不太差,你可以使用填充功能来填充所需的区域。颜色属性仅供内部使用(以防万一),例如,你可以让用户使用“填充”功能进行填充,但你只需修改颜色属性并重新绘制即可,而无需实际执行填充操作。 - user1693593
不错,那个算法看起来很有趣...我认为这确实是处理问题的唯一正确方式。到目前为止,我们已经讨论了更改线条颜色的方法,这可能是可行的,基于检测涂漆桶点击事件在线条上方,然后检测所有与该线相交的线条,然后使用重新绘制过程而不是泛洪填充替换颜色... 但是,问题并不局限于线条,想象一下用线条手绘的“圆”,然后填充圆的内部...抗锯齿问题以更复杂的方式出现。 - Tappa Tappa
我决定暂时让用户处理抗锯齿环境下泛洪填充的不完美之处。也许有关方面会认识到在HTML5画布的所有方面中提供可选的抗锯齿的基本需求。谢谢Ken,我会将这个答案标记为正确的 :-) - Tappa Tappa
只是想提一下,抗锯齿问题在缩放基于矢量的方法时会导致进一步的问题。如果放大就没问题,但如果缩小,则存在填充不会像原始比例中那样行为的风险。这相当令人恼火,并且是一个足够频繁的问题,以至于我现在不会使用缩放(例如缩略图),而必须采用完整大小绘制的 CSS 缩放。兔子洞继续加深 叹气 - Tappa Tappa

0

你不能总是重新绘制画布,可能已经使用了无法撤销的过滤器,或者只是使用了太多的填充和描边调用,重新绘制会变得不切实际。

我有自己的基于简单填充堆栈的泛洪填充,它可以在容忍度内进行绘制,并尽力减少反锯齿伪影。不幸的是,如果您开启了反锯齿,则重复填充将扩大填充区域。

以下是该函数,根据需要进行适应,这是从我的代码中直接提取的,并添加了注释。

// posX,posY are the fill start position. The pixel at the location is used to test tolerance.
// RGBA      is the fill colour as an array of 4 bytes all ranged 0-255 for R,G,B,A
// diagonal  if true the also fill into pixels that touch at the corners. 
// imgData   canvas pixel data from ctx.getImageData method
// tolerance Fill tolerance range 0 only allow exact same colour to fill to 255
// fill      all but the extreme opposite.
// antiAlias if true fill edges to reduce anti-Aliasing artifacts.


Bitmaps.prototype.floodFill = function (posX, posY, RGBA, diagonal,imgData,tolerance,antiAlias) {
    var data = imgData.data; // image data to fill;
    antiAlias = true;
    var stack = [];          // paint stack to find new pixels to paint
    var lookLeft = false;    // test directions
    var lookRight = false;
    var w = imgData.width;   // width and height
    var h = imgData.height;
    var painted = new Uint8ClampedArray(w*h);  // byte array to mark painted area;
    var dw = w*4; // data width.
    var x = posX;   // just short version of pos because I am lazy
    var y = posY;
    var ind = y * dw + x * 4;  // get the starting pixel index
    var sr = data[ind];        // get the start colour tha we will use tollerance against.
    var sg = data[ind+1];
    var sb = data[ind+2];
    var sa = data[ind+3];     
    var sp = 0;
    var dontPaint = false;  // flag to indicate if checkColour can paint

    // function checks a pixel colour passes tollerance, is painted, or out of bounds.
    // if the pixel is over tollerance and not painted set it do reduce anti alising artifacts
    var checkColour = function(x,y){
        if( x<0 || y < 0 || y >=h || x >= w){  // test bounds
            return false;
        }
        var ind = y * dw + x * 4;  // get index of pixel
        var dif = Math.max(        // get the max channel differance;
            Math.abs(sr-data[ind]),
            Math.abs(sg-data[ind+1]),
            Math.abs(sb-data[ind+2]),                
            Math.abs(sa-data[ind+3])
        );        
        if(dif < tolerance){         // if under tollerance pass it
            dif = 0;
        }        
        var paint = Math.abs(sp-painted[y * w + x]); // is it already painted
        if(antiAlias && !dontPaint){  // mitigate anti aliasing effect
            // if failed tollerance and has not been painted set the pixel to 
            // reduce anti alising artifact
            if(dif !== 0 && paint !== 255){  
                data[ind] = RGBA[0];
                data[ind+1] = RGBA[1];
                data[ind+2] = RGBA[2];
                data[ind+3] = (RGBA[3]+data[ind+3])/2; // blend the alpha channel
                painted[y * w + x] = 255;  // flag pixel as painted
            }
        }
        return (dif+paint)===0?true:false;  // return tollerance status;
    }
    // set a pixel and flag it as painted;
    var setPixel = function(x,y){
        var ind = y * dw + x * 4;  // get index;
        data[ind] = RGBA[0];       // set RGBA
        data[ind+1] = RGBA[1];
        data[ind+2] = RGBA[2];
        data[ind+3] = RGBA[3];
        painted[y * w + x] = 255;   // 255 or any number >0 will do;
    }


    stack.push([x,y]);  // push the first pixel to paint onto the paint stack

    while (stack.length) {   // do while pixels on the stack
        var pos = stack.pop();  // get the pixel
        x = pos[0];
        y = pos[1];
        dontPaint = true;    // turn off anti alising 
        while (checkColour(x,y-1)) {  // find the bottom most pixel within tolerance;
            y -= 1;
        }
        dontPaint = false;    // turn on anti alising if being used
        //checkTop left and right if alowing diagonal painting
        if(diagonal){
            if(!checkColour(x-1,y) && checkColour(x-1,y-1)){
                stack.push([x-1,y-1]);
            }
            if(!checkColour(x+1,y) && checkColour(x+1,y-1)){
                stack.push([x+1,y-1]);
            }
        }
        lookLeft = false;  // set look directions
        lookRight = false; // only look is a pixel left or right was blocked
        while (checkColour(x,y)) { // move up till no more room
            setPixel(x,y);         // set the pixel
            if (checkColour(x - 1,y)) {  // check left is blocked
                if (!lookLeft) {        
                    stack.push([x - 1, y]);  // push a new area to fill if found
                    lookLeft = true;
                }
            } else 
            if (lookLeft) {
                lookLeft = false;
            }
            if (checkColour(x+1,y)) {  // check right is blocked
                if (!lookRight) {
                    stack.push([x + 1, y]); // push a new area to fill if found
                    lookRight = true;
                }
            } else 
            if (lookRight) {
                lookRight = false;
            }
            y += 1;                 // move up one pixel
        }
        // check down left 
        if(diagonal){  // check for diagnal areas and push them to be painted 
            if(checkColour(x-1,y) && !lookLeft){
                stack.push([x-1,y]);
            }
            if(checkColour(x+1,y) && !lookRight){
                stack.push([x+1,y]);
            }
        }
    }
    // all done
}

有一种更好的方法可以提供高质量的结果,可以通过使用涂漆数组来标记涂漆边缘,然后在填充完成后扫描涂漆数组并对每个标记的边缘像素应用卷积滤波器来调整上述代码以实现此目的。该滤波器是定向的(取决于哪些侧面被涂漆),而且代码太长了,无法在此答案中列出。我已经指导了您正确的方向,并提供了基础设施。

另一种提高图像质量的方法是对正在绘制的图像进行超采样。保留一个比正在绘制的图像大两倍的第二个画布。将所有绘图操作都执行到该图像上,并使用CTX.imageSmoothingEnabledctx.setTransform(0.5,0,0,0.5,0,0)将其缩小一半显示给用户,当完成并准备好图像时,使用以下代码手动将其缩小一半(不要依赖画布imageSmoothingEnabled,因为它会出错)。

这样做将极大地提高最终图像的质量,并且与上述填充一起,几乎完全消除了洪水填充的反锯齿伪影。

    // ctxS is the source canvas context
    var w = ctxS.canvas.width;
    var h = ctxS.canvas.height;
    var data = ctxS.getImageData(0,0,w,h);
    var d = data.data;
    var x,y;
    var ww = w*4;
    var ww4 = ww+4;
    for(y = 0; y < h; y+=2){
        for(x = 0; x < w; x+=2){
            var id = y*ww+x*4;
            var id1 = Math.floor(y/2)*ww+Math.floor(x/2)*4;
            d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
            id += 1;
            id1 += 1;
            d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
            id += 1;
            id1 += 1;
            d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
            id += 1;
            id1 += 1;
            d[id1] = Math.sqrt((d[id]*d[id]+d[id+4]*d[id+4]+d[id+ww]*d[id+ww]+d[id+ww4]*d[id+ww4])/4);
        }
    }
    ctxS.putImageData(data,0,0); // save imgData
    // grab it again for new image we don't want to add artifacts from the GPU
    var data = ctxS.getImageData(0,0,Math.floor(w/2),Math.floor(h/2));
    var canvas = document.createElement("canvas");
    canvas.width = Math.floor(w/2);
    canvas.height =Math.floor(h/2);
    var ctxS = canvas.getContext("2d",{ alpha: true });
    ctxS.putImageData(data,0,0);  
    // result canvas with downsampled high quality image.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接