下面的链接中附有我的画布的屏幕截图(外框是画布)。内部框是灰色框,线是在画布中绘制的线。如何创建用特定颜色填充整个画布(内部灰色框和线条除外)的泛洪填充功能?
该函数应只接受x y和color这三个变量,如下所示,但是我不确定如何继续:
floodFill(x, y, color) {
this.canvasColor[x][y] = color;
this.floodFill(x-1, y, color);
this.floodFill(x+1, y, color);
this.floodFill(x, y-1, color);
this.floodFill(x, y+1, color);
}
答案 0 :(得分:2)
要创建泛洪填充,您需要能够查看已经存在的像素,并检查它们是否不是开始时使用的颜色。
const ctx = document.querySelector("canvas").getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
floodFill(ctx, 40, 50, [255, 0, 0, 255]);
function getPixel(imageData, x, y) {
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return [-1, -1, -1, -1]; // impossible color
} else {
const offset = (y * imageData.width + x) * 4;
return imageData.data.slice(offset, offset + 4);
}
}
function setPixel(imageData, x, y, color) {
const offset = (y * imageData.width + x) * 4;
imageData.data[offset + 0] = color[0];
imageData.data[offset + 1] = color[1];
imageData.data[offset + 2] = color[2];
imageData.data[offset + 3] = color[0];
}
function colorsMatch(a, b) {
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
}
function floodFill(ctx, x, y, fillColor) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// get the color we're filling
const targetColor = getPixel(imageData, x, y);
// check we are actually filling a different color
if (!colorsMatch(targetColor, fillColor)) {
fillPixel(imageData, x, y, targetColor, fillColor);
// put the data back
ctx.putImageData(imageData, 0, 0);
}
}
function fillPixel(imageData, x, y, targetColor, fillColor) {
const currentColor = getPixel(imageData, x, y);
if (colorsMatch(currentColor, targetColor)) {
setPixel(imageData, x, y, fillColor);
fillPixel(imageData, x + 1, y, targetColor, fillColor);
fillPixel(imageData, x - 1, y, targetColor, fillColor);
fillPixel(imageData, x, y + 1, targetColor, fillColor);
fillPixel(imageData, x, y - 1, targetColor, fillColor);
}
}
<canvas></canvas>
此代码至少有2个问题。
它是深度递归的。
所以您可能会用完堆栈空间
很慢。
不知道它是否太慢,但是浏览器中的JavaScript大多是单线程的,因此在运行此代码时浏览器被冻结。对于一块大画布,冻结时间可能会使页面真正变慢,如果冻结时间太长,浏览器将询问用户是否要杀死该页面。
解决堆栈空间不足的方法是实现我们自己的堆栈。例如,除了递归调用fillPixel
外,我们可以保留一组要查看的位置。我们将4个位置添加到该数组中,然后从数组中弹出内容,直到其为空
const ctx = document.querySelector("canvas").getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
floodFill(ctx, 40, 50, [255, 0, 0, 255]);
function getPixel(imageData, x, y) {
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return [-1, -1, -1, -1]; // impossible color
} else {
const offset = (y * imageData.width + x) * 4;
return imageData.data.slice(offset, offset + 4);
}
}
function setPixel(imageData, x, y, color) {
const offset = (y * imageData.width + x) * 4;
imageData.data[offset + 0] = color[0];
imageData.data[offset + 1] = color[1];
imageData.data[offset + 2] = color[2];
imageData.data[offset + 3] = color[0];
}
function colorsMatch(a, b) {
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
}
function floodFill(ctx, x, y, fillColor) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// get the color we're filling
const targetColor = getPixel(imageData, x, y);
// check we are actually filling a different color
if (!colorsMatch(targetColor, fillColor)) {
const pixelsToCheck = [x, y];
while (pixelsToCheck.length > 0) {
const y = pixelsToCheck.pop();
const x = pixelsToCheck.pop();
const currentColor = getPixel(imageData, x, y);
if (colorsMatch(currentColor, targetColor)) {
setPixel(imageData, x, y, fillColor);
pixelsToCheck.push(x + 1, y);
pixelsToCheck.push(x - 1, y);
pixelsToCheck.push(x, y + 1);
pixelsToCheck.push(x, y - 1);
}
}
// put the data back
ctx.putImageData(imageData, 0, 0);
}
}
<canvas></canvas>
解决它太慢的方法是要么make it run a little at a time,要么将其移交给工作人员。我认为here's an example不能在相同的答案中显示太多。我在4096x4096画布上测试了上面的代码,在机器上填充空白画布花了16秒,因此可以说它太慢了,但是将其放入工作器中会带来新的问题,即即使浏览器浏览器也将产生异步结果不会冻结,您可能想要阻止用户在操作完成之前做某事。
另一个问题是,您将看到线条被消除锯齿,因此用纯色填充将关闭线条,但并不能一直填充到线条上。要解决此问题,您可以更改colorsMatch
以检查是否足够接近 ,但是又遇到了一个新问题,即如果targetColor
和fillColor
也足够接近它将继续尝试填充自身。您可以通过制作另一个数组(每个像素一个字节或一位)来跟踪已检查的位置来解决该问题。
const ctx = document.querySelector("canvas").getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
floodFill(ctx, 40, 50, [255, 0, 0, 255], 128);
function getPixel(imageData, x, y) {
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return [-1, -1, -1, -1]; // impossible color
} else {
const offset = (y * imageData.width + x) * 4;
return imageData.data.slice(offset, offset + 4);
}
}
function setPixel(imageData, x, y, color) {
const offset = (y * imageData.width + x) * 4;
imageData.data[offset + 0] = color[0];
imageData.data[offset + 1] = color[1];
imageData.data[offset + 2] = color[2];
imageData.data[offset + 3] = color[0];
}
function colorsMatch(a, b, rangeSq) {
const dr = a[0] - b[0];
const dg = a[1] - b[1];
const db = a[2] - b[2];
const da = a[3] - b[3];
return dr * dr + dg * dg + db * db + da * da < rangeSq;
}
function floodFill(ctx, x, y, fillColor, range = 1) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// flags for if we visited a pixel already
const visited = new Uint8Array(imageData.width, imageData.height);
// get the color we're filling
const targetColor = getPixel(imageData, x, y);
// check we are actually filling a different color
if (!colorsMatch(targetColor, fillColor)) {
const rangeSq = range * range;
const pixelsToCheck = [x, y];
while (pixelsToCheck.length > 0) {
const y = pixelsToCheck.pop();
const x = pixelsToCheck.pop();
const currentColor = getPixel(imageData, x, y);
if (!visited[y * imageData.width + x] &&
colorsMatch(currentColor, targetColor, rangeSq)) {
setPixel(imageData, x, y, fillColor);
visited[y * imageData.width + x] = 1; // mark we were here already
pixelsToCheck.push(x + 1, y);
pixelsToCheck.push(x - 1, y);
pixelsToCheck.push(x, y + 1);
pixelsToCheck.push(x, y - 1);
}
}
// put the data back
ctx.putImageData(imageData, 0, 0);
}
}
<canvas></canvas>
请注意,此版本的colorsMatch
使用的是天真的。转换为HSV或其他方法可能更好,或者您想按alpha加权。我不知道匹配颜色的好指标。
另一种加快处理速度的方法当然就是优化代码。 Kaiido指出了一个明显的加速方法,即在像素上使用Uint32Array
视图。这样,查找像素并设置像素时,只有一个32bit值可读取或写入。 Just that change makes it about 4x faster。不过,填充4096x4096画布仍需要4秒钟。可能还有其他优化方法,例如代替调用getPixels
进行内联,但不要将新像素推入我们的像素列表中以检查它们是否超出范围。速度可能会提高10%(不知道),但不会足够快,无法达到交互式速度。
还有其他提速功能,例如一次检查整个行,因为行是缓存友好的,您可以一次计算一行的偏移量,并在检查整行的同时使用它,因此现在需要为每个像素计算偏移量多次。
那些会使算法复杂化,因此最好让它们找出来。