HTML5画布 - 抗锯齿和油漆桶/洪水填充

时间:2013-08-19 09:31:00

标签: html5 canvas antialiasing flood-fill

在浏览Stack Overflow和Google之后,在我看来,在HTML5画布上绘制线条时无法禁用抗锯齿功能。

这样可以产生漂亮的线条,但在应用油漆桶/填充填充算法时会出现问题。

我的部分应用程序要求用户在画布上绘图,使用基本工具(如线条大小,颜色......和油漆桶)进行自由式绘图。

因为线条是使用抗锯齿渲染的,所以它们不是一致的颜色......考虑到这一点,请考虑以下内容:

  1. 用黑色画一条粗线
  2. 稍后决定该行应为红色
  3. 将填充填充应用于黑线
  4. 我的泛光填充算法用红色填充线条的大部分,但是被抗锯齿的边缘被检测为在应该填充的区域之外...因此保留(灰色留下的灰色/蓝色(?)线)。

    泛洪填充算法没有像Photoshop那样包含类似于'容差'的东西......我已经考虑过类似的东西了但不确定它会有所帮助,因为我认为抗锯齿做的事情很简单,比如渲染灰色在黑线旁边,我认为它比这更先进,抗锯齿考虑到周围的颜色和混合。

    有没有人有任何关于如何最终获得更好的油漆桶/洪水填充的建议?完全填充/替换图纸的现有线条或部分?

2 个答案:

答案 0 :(得分: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等进行绘制。)

答案 1 :(得分: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)半尺寸的画布的用户,完成后,使用以下代码手动将图像准备好一半(don&#39 ; t依赖于画布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.