有没有人在javascript中实现泛洪填充算法以用于HTML Canvas?
我的要求很简单:从单个点开始使用单一颜色进行泛光,其中边界颜色是指定点处颜色的某个特定颜色。
var r1, r2; // red values
var g1, g2; // green values
var b1, b2; // blue values
var actualColorDelta = Math.sqrt((r1 - r2)*(r1 - r2) + (g1 - g2)*(g1 - g2) + (b1 - b2)*(b1 - b2))
function floodFill(canvas, x, y, fillColor, borderColorDelta) {
...
}
更新
我编写了自己的洪水填充实现,紧随其后。它很慢但很准确。大约37%的时间用于两个低级数组函数,这些函数是原型框架的一部分。我推测,它们是通过推和弹来调用的。其余大部分时间都花在主循环中。
var ImageProcessing;
ImageProcessing = {
/* Convert HTML color (e.g. "#rrggbb" or "#rrggbbaa") to object with properties r, g, b, a.
* If no alpha value is given, 255 (0xff) will be assumed.
*/
toRGB: function (color) {
var r, g, b, a, html;
html = color;
// Parse out the RGBA values from the HTML Code
if (html.substring(0, 1) === "#")
{
html = html.substring(1);
}
if (html.length === 3 || html.length === 4)
{
r = html.substring(0, 1);
r = r + r;
g = html.substring(1, 2);
g = g + g;
b = html.substring(2, 3);
b = b + b;
if (html.length === 4) {
a = html.substring(3, 4);
a = a + a;
}
else {
a = "ff";
}
}
else if (html.length === 6 || html.length === 8)
{
r = html.substring(0, 2);
g = html.substring(2, 4);
b = html.substring(4, 6);
a = html.length === 6 ? "ff" : html.substring(6, 8);
}
// Convert from Hex (Hexidecimal) to Decimal
r = parseInt(r, 16);
g = parseInt(g, 16);
b = parseInt(b, 16);
a = parseInt(a, 16);
return {r: r, g: g, b: b, a: a};
},
/* Get the color at the given x,y location from the pixels array, assuming the array has a width and height as given.
* This interprets the 1-D array as a 2-D array.
*
* If useColor is defined, its values will be set. This saves on object creation.
*/
getColor: function (pixels, x, y, width, height, useColor) {
var redIndex = y * width * 4 + x * 4;
if (useColor === undefined) {
useColor = { r: pixels[redIndex], g: pixels[redIndex + 1], b: pixels[redIndex + 2], a: pixels[redIndex + 3] };
}
else {
useColor.r = pixels[redIndex];
useColor.g = pixels[redIndex + 1]
useColor.b = pixels[redIndex + 2];
useColor.a = pixels[redIndex + 3];
}
return useColor;
},
setColor: function (pixels, x, y, width, height, color) {
var redIndex = y * width * 4 + x * 4;
pixels[redIndex] = color.r;
pixels[redIndex + 1] = color.g,
pixels[redIndex + 2] = color.b;
pixels[redIndex + 3] = color.a;
},
/*
* fill: Flood a canvas with the given fill color.
*
* Returns a rectangle { x, y, width, height } that defines the maximum extent of the pixels that were changed.
*
* canvas .................... Canvas to modify.
* fillColor ................. RGBA Color to fill with.
* This may be a string ("#rrggbbaa") or an object of the form { r: red, g: green, b: blue, a: alpha }.
* x, y ...................... Coordinates of seed point to start flooding.
* bounds .................... Restrict flooding to this rectangular region of canvas.
* This object has these attributes: { x, y, width, height }.
* If undefined or null, use the whole of the canvas.
* stopFunction .............. Function that decides if a pixel is a boundary that should cause
* flooding to stop. If omitted, any pixel that differs from seedColor
* will cause flooding to stop. seedColor is the color under the seed point (x,y).
* Parameters: stopFunction(fillColor, seedColor, pixelColor).
* Returns true if flooding shoud stop.
* The colors are objects of the form { r: red, g: green, b: blue, a: alpha }
*/
fill: function (canvas, fillColor, x, y, bounds, stopFunction) {
// Supply default values if necessary.
var ctx, minChangedX, minChangedY, maxChangedX, maxChangedY, wasTested, shouldTest, imageData, pixels, currentX, currentY, currentColor, currentIndex, seedColor, tryX, tryY, tryIndex, boundsWidth, boundsHeight, pixelStart, fillRed, fillGreen, fillBlue, fillAlpha;
if (Object.isString(fillColor)) {
fillColor = ImageProcessing.toRGB(fillColor);
}
x = Math.round(x);
y = Math.round(y);
if (bounds === null || bounds === undefined) {
bounds = { x: 0, y: 0, width: canvas.width, height: canvas.height };
}
else {
bounds = { x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.y), height: Math.round(bounds.height) };
}
if (stopFunction === null || stopFunction === undefined) {
stopFunction = new function (fillColor, seedColor, pixelColor) {
return pixelColor.r != seedColor.r || pixelColor.g != seedColor.g || pixelColor.b != seedColor.b || pixelColor.a != seedColor.a;
}
}
minChangedX = maxChangedX = x - bounds.x;
minChangedY = maxChangedY = y - bounds.y;
boundsWidth = bounds.width;
boundsHeight = bounds.height;
// Initialize wasTested to false. As we check each pixel to decide if it should be painted with the new color,
// we will mark it with a true value at wasTested[row = y][column = x];
wasTested = new Array(boundsHeight * boundsWidth);
/*
$R(0, bounds.height - 1).each(function (row) {
var subArray = new Array(bounds.width);
wasTested[row] = subArray;
});
*/
// Start with a single point that we know we should test: (x, y).
// Convert (x,y) to image data coordinates by subtracting the bounds' origin.
currentX = x - bounds.x;
currentY = y - bounds.y;
currentIndex = currentY * boundsWidth + currentX;
shouldTest = [ currentIndex ];
ctx = canvas.getContext("2d");
//imageData = ctx.getImageData(bounds.x, bounds.y, bounds.width, bounds.height);
imageData = ImageProcessing.getImageData(ctx, bounds.x, bounds.y, bounds.width, bounds.height);
pixels = imageData.data;
seedColor = ImageProcessing.getColor(pixels, currentX, currentY, boundsWidth, boundsHeight);
currentColor = { r: 0, g: 0, b: 0, a: 1 };
fillRed = fillColor.r;
fillGreen = fillColor.g;
fillBlue = fillColor.b;
fillAlpha = fillColor.a;
while (shouldTest.length > 0) {
currentIndex = shouldTest.pop();
currentX = currentIndex % boundsWidth;
currentY = (currentIndex - currentX) / boundsWidth;
if (! wasTested[currentIndex]) {
wasTested[currentIndex] = true;
//currentColor = ImageProcessing.getColor(pixels, currentX, currentY, boundsWidth, boundsHeight, currentColor);
// Inline getColor for performance.
pixelStart = currentIndex * 4;
currentColor.r = pixels[pixelStart];
currentColor.g = pixels[pixelStart + 1]
currentColor.b = pixels[pixelStart + 2];
currentColor.a = pixels[pixelStart + 3];
if (! stopFunction(fillColor, seedColor, currentColor)) {
// Color the pixel with the fill color.
//ImageProcessing.setColor(pixels, currentX, currentY, boundsWidth, boundsHeight, fillColor);
// Inline setColor for performance
pixels[pixelStart] = fillRed;
pixels[pixelStart + 1] = fillGreen;
pixels[pixelStart + 2] = fillBlue;
pixels[pixelStart + 3] = fillAlpha;
if (minChangedX < currentX) { minChangedX = currentX; }
else if (maxChangedX > currentX) { maxChangedX = currentX; }
if (minChangedY < currentY) { minChangedY = currentY; }
else if (maxChangedY > currentY) { maxChangedY = currentY; }
// Add the adjacent four pixels to the list to be tested, unless they have already been tested.
tryX = currentX - 1;
tryY = currentY;
tryIndex = tryY * boundsWidth + tryX;
if (tryX >= 0 && ! wasTested[tryIndex]) {
shouldTest.push(tryIndex);
}
tryX = currentX;
tryY = currentY + 1;
tryIndex = tryY * boundsWidth + tryX;
if (tryY < boundsHeight && ! wasTested[tryIndex]) {
shouldTest.push(tryIndex);
}
tryX = currentX + 1;
tryY = currentY;
tryIndex = tryY * boundsWidth + tryX;
if (tryX < boundsWidth && ! wasTested[tryIndex]) {
shouldTest.push(tryIndex);
}
tryX = currentX;
tryY = currentY - 1;
tryIndex = tryY * boundsWidth + tryX;
if (tryY >= 0 && ! wasTested[tryIndex]) {
shouldTest.push(tryIndex);
}
}
}
}
//ctx.putImageData(imageData, bounds.x, bounds.y);
ImageProcessing.putImageData(ctx, imageData, bounds.x, bounds.y);
return { x: minChangedX + bounds.x, y: minChangedY + bounds.y, width: maxChangedX - minChangedX + 1, height: maxChangedY - minChangedY + 1 };
},
getImageData: function (ctx, x, y, w, h) {
return ctx.getImageData(x, y, w, h);
},
putImageData: function (ctx, data, x, y) {
ctx.putImageData(data, x, y);
}
};
BTW,当我打电话给我时,我使用自定义stopFunction:
stopFill : function (fillColor, seedColor, pixelColor) {
// Ignore alpha difference for now.
return Math.abs(pixelColor.r - seedColor.r) > this.colorTolerance || Math.abs(pixelColor.g - seedColor.g) > this.colorTolerance || Math.abs(pixelColor.b - seedColor.b) > this.colorTolerance;
},
如果有人能看到改善此代码性能的方法,我将不胜感激。基本思路是: 1)种子颜色是开始泛滥的点的初始颜色。 2)尝试四个相邻点:向上,向右,向下和向左一个像素。 3)如果点已超出范围或已经访问过,请跳过它。 4)否则按下指向堆栈的有趣点。 5)从堆栈中弹出下一个有趣的点。 6)如果该点的颜色是停止颜色(如stopFunction中所定义),则停止处理该点并跳到步骤5。 7)否则,跳到步骤2。 8)当没有更多有趣的点可以访问时,停止循环。
记住访问过的点需要一个与像素数相同的数组。
答案 0 :(得分:3)
这是我一直在努力的实现。如果替换颜色太接近原始颜色,它可能会变得非常慢。它在Chrome中比Firefox快得多(我还没有在其他任何浏览器中测试过它)。
我还没有进行详尽的测试,因此可能会出现无效的边缘情况。
function getPixel(pixelData, x, y) {
if (x < 0 || y < 0 || x >= pixelData.width || y >= pixelData.height) {
return NaN;
}
var pixels = pixelData.data;
var i = (y * pixelData.width + x) * 4;
return ((pixels[i + 0] & 0xFF) << 24) |
((pixels[i + 1] & 0xFF) << 16) |
((pixels[i + 2] & 0xFF) << 8) |
((pixels[i + 3] & 0xFF) << 0);
}
function setPixel(pixelData, x, y, color) {
var i = (y * pixelData.width + x) * 4;
var pixels = pixelData.data;
pixels[i + 0] = (color >>> 24) & 0xFF;
pixels[i + 1] = (color >>> 16) & 0xFF;
pixels[i + 2] = (color >>> 8) & 0xFF;
pixels[i + 3] = (color >>> 0) & 0xFF;
}
function diff(c1, c2) {
if (isNaN(c1) || isNaN(c2)) {
return Infinity;
}
var dr = ((c1 >>> 24) & 0xFF) - ((c2 >>> 24) & 0xFF);
var dg = ((c1 >>> 16) & 0xFF) - ((c2 >>> 16) & 0xFF);
var db = ((c1 >>> 8) & 0xFF) - ((c2 >>> 8) & 0xFF);
var da = ((c1 >>> 0) & 0xFF) - ((c2 >>> 0) & 0xFF);
return dr*dr + dg*dg + db*db + da*da;
}
function floodFill(canvas, x, y, replacementColor, delta) {
var current, w, e, stack, color, cx, cy;
var context = canvas.getContext("2d");
var pixelData = context.getImageData(0, 0, canvas.width, canvas.height);
var done = [];
for (var i = 0; i < canvas.width; i++) {
done[i] = [];
}
var targetColor = getPixel(pixelData, x, y);
delta *= delta;
stack = [ [x, y] ];
done[x][y] = true;
while ((current = stack.pop())) {
cx = current[0];
cy = current[1];
if (diff(getPixel(pixelData, cx, cy), targetColor) <= delta) {
setPixel(pixelData, cx, cy, replacementColor);
w = e = cx;
while (w > 0 && diff(getPixel(pixelData, w - 1, cy), targetColor) <= delta) {
--w;
if (done[w][cy]) break;
setPixel(pixelData, w, cy, replacementColor);
}
while (e < pixelData.width - 1 && diff(getPixel(pixelData, e + 1, cy), targetColor) <= delta) {
++e;
if (done[e][cy]) break;
setPixel(pixelData, e, cy, replacementColor);
}
for (cx = w; cx <= e; cx++) {
if (cy > 0) {
color = getPixel(pixelData, cx, cy - 1);
if (diff(color, targetColor) <= delta) {
if (!done[cx][cy - 1]) {
stack.push([cx, cy - 1]);
done[cx][cy - 1] = true;
}
}
}
if (cy < canvas.height - 1) {
color = getPixel(pixelData, cx, cy + 1);
if (diff(color, targetColor) <= delta) {
if (!done[cx][cy + 1]) {
stack.push([cx, cy + 1]);
done[cx][cy + 1] = true;
}
}
}
}
}
}
context.putImageData(pixelData, 0, 0, 0, 0, canvas.width, canvas.height);
}
答案 1 :(得分:1)
我不会将画布视为位图图像。
相反,我会保留一组绘画对象并修改该集合。 然后,例如,您可以填充路径或形状,或添加具有您要填充的对象边界的新形状。
我无法看到“正常”的floodFill在矢量绘图中是否有意义。
答案 2 :(得分:0)
要创建泛洪填充,您需要能够查看已经存在的像素,并检查它们是否不是开始时使用的颜色。
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%(不知道),但不会足够快,无法达到交互式速度。
还有其他提速方法,例如一次检查整个行,因为行是缓存友好的,您可以一次计算一行的偏移量,然后在检查整行时使用该偏移量,而现在对于每个像素,我们必须计算偏移量倍数次。
那些会使算法复杂化,因此最好让他们找出来。
让我也添加有关冻结的答案,浏览器将发生填充,并且在较大的画布上冻结可能会太长,您可以使用ES6 async / await轻松使算法随时间变化。您需要选择为每个时间段分配多少工作量。选择的太小,将需要很长时间来填充。选择太大,当浏览器冻结时,您会发牢骚。
这是一个例子。设置ticksPerUpdate
可以加快或降低填充率
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(100, 145);
ctx.lineTo(110, 105);
ctx.lineTo(130, 125);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
floodFill(ctx, 40, 50, 0xFF0000FF);
function getPixel(pixelData, x, y) {
if (x < 0 || y < 0 || x >= pixelData.width || y >= pixelData.height) {
return -1; // impossible color
} else {
return pixelData.data[y * pixelData.width + x];
}
}
async function floodFill(ctx, x, y, fillColor) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// make a Uint32Array view on the pixels so we can manipulate pixels
// one 32bit value at a time instead of as 4 bytes per pixel
const pixelData = {
width: imageData.width,
height: imageData.height,
data: new Uint32Array(imageData.data.buffer),
};
// get the color we're filling
const targetColor = getPixel(pixelData, x, y);
// check we are actually filling a different color
if (targetColor !== fillColor) {
const ticksPerUpdate = 50;
let tickCount = 0;
const pixelsToCheck = [x, y];
while (pixelsToCheck.length > 0) {
const y = pixelsToCheck.pop();
const x = pixelsToCheck.pop();
const currentColor = getPixel(pixelData, x, y);
if (currentColor === targetColor) {
pixelData.data[y * pixelData.width + x] = fillColor;
// put the data back
ctx.putImageData(imageData, 0, 0);
++tickCount;
if (tickCount % ticksPerUpdate === 0) {
await wait();
}
pixelsToCheck.push(x + 1, y);
pixelsToCheck.push(x - 1, y);
pixelsToCheck.push(x, y + 1);
pixelsToCheck.push(x, y - 1);
}
}
}
}
function wait(delay = 0) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
<canvas></canvas>