3

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

这使得线条看起来很漂亮,但是在应用油漆桶/洪水填充算法时给我带来了问题。

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

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

  1. 用黑色画一条粗线
  2. 稍后决定这条线应该是红色的
  3. 将洪水填充应用于黑线

我的洪水填充算法用红色填充线条的大部分,但是抗锯齿的边缘被检测为在应该填充的区域之外......因此保留(黑色线留下的灰色/蓝色(?))。

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

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

4

2 回答 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等进行绘制。)

于 2013-08-19T18:51:41.503 回答
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.
于 2015-09-21T23:28:05.800 回答