我已经初始化了一个fabric.js画布,并尝试实现洪水填充算法。我在将图像数据渲染回织物画布时遇到问题,我发现的唯一方法是创建一个临时画布,在该画布上传递图像数据并将其作为图像添加回初始织物画布。但是,我无法使用其余的 Fabric js 工具(例如选择),因为在使用洪水填充算法后,它会将绘制的矩形视为图像而不是单个对象。
这是我的代码:
let col = { r: 0, g: 0, b: 0, a: 0xff };
let gridSize = 20;
const canvas = new fabric.Canvas("canvas", {
fireRightClick: true,
stopContextMenu: true,
selection: false, //this is set to true whenever I select the respective tool
skipTargetFind: false,
preserveObjectStacking: true,
backgroundColor: "#ffffff",
});
canvas.on("mouse:down", function (event) {
let pointer = canvas.getPointer(event.e);
let gridX = Math.floor(pointer.x / gridSize) * gridSize;
let gridY = Math.floor(pointer.y / gridSize) * gridSize;
let x = Math.round(pointer.x);
let y = Math.round(pointer.y);
if (event.e.button === 0) {
if (buttonStates.pencil) {
addRectangle(gridX, gridY, canvas.freeDrawingBrush.color);
} else if (buttonStates.fill) {
hexToRgbA();
floodFill(col, x, y);
}
}
});
//------------------------ Helper function to add rectangles ------------------------//
function addRectangle(left, top, fill, width = gridSize, height = gridSize) {
let rect = new fabric.Rect({
left: left,
top: top,
width: width,
height: height,
fill: fill,
evented: false,
});
canvas.add(rect);
}
//--------------------------------- Flood Fill -------------------------------------//
function hexToRgbA() {
let hex = colorInput.value;
let c;
if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
c = hex.substring(1).split("");
if (c.length == 3) {
c = [c[0], c[0], c[1], c[1], c[2], c[2]];
}
c = "0x" + c.join("");
const r = (c >> 16) & 255;
const g = (c >> 8) & 255;
const b = c & 255;
col.r = r;
col.g = g;
col.b = b;
return "rgba(" + r + "," + g + "," + b + ",1)";
}
throw new Error("Bad Hex");
}
function getColorAtPixel(imageData, x, y) {
const { width, data } = imageData;
return {
a: data[4 * (width * y + x) + 0],
r: data[4 * (width * y + x) + 1],
g: data[4 * (width * y + x) + 2],
b: data[4 * (width * y + x) + 3],
};
}
function setColorAtPixel(imageData, color, x, y) {
const { width, data } = imageData;
data[4 * (width * y + x) + 0] = color.r & 0xff;
data[4 * (width * y + x) + 1] = color.g & 0xff;
data[4 * (width * y + x) + 2] = color.b & 0xff;
data[4 * (width * y + x) + 3] = color.a & 0xff;
}
function colorMatch(a, b) {
return a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a;
}
function floodFill(newColor, x, y) {
let htmlCanvas = canvas.toCanvasElement();
let ctx = htmlCanvas.getContext("2d", { willReadFrequently: true });
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const { width, height, data } = imageData;
const stack = [];
const baseColor = getColorAtPixel(imageData, x, y);
let operator = { x, y };
// Check if base color and new color are the same
if (colorMatch(baseColor, newColor)) {
return;
}
// Add the clicked location to stack
stack.push({ x: operator.x, y: operator.y });
while (stack.length) {
operator = stack.pop();
let contiguousDown = true;
let contiguousUp = true;
let contiguousLeft = false;
let contiguousRight = false;
// Move to top most contiguousDown pixel
while (contiguousUp && operator.y >= 0) {
operator.y--;
contiguousUp = colorMatch(
getColorAtPixel(imageData, operator.x, operator.y),
baseColor
);
}
// Move downward
while (contiguousDown && operator.y < height) {
setColorAtPixel(imageData, newColor, operator.x, operator.y);
// Check left
if (
operator.x - 1 >= 0 &&
colorMatch(
getColorAtPixel(imageData, operator.x - 1, operator.y),
baseColor
)
) {
if (!contiguousLeft) {
contiguousLeft = true;
stack.push({ x: operator.x - 1, y: operator.y });
}
} else {
contiguousLeft = false;
}
// Check right
if (
operator.x + 1 < width &&
colorMatch(
getColorAtPixel(imageData, operator.x + 1, operator.y),
baseColor
)
) {
if (!contiguousRight) {
stack.push({ x: operator.x + 1, y: operator.y });
contiguousRight = true;
}
} else {
contiguousRight = false;
}
operator.y++;
contiguousDown = colorMatch(
getColorAtPixel(imageData, operator.x, operator.y),
baseColor
);
}
}
// Create a new canvas element and draw the modified imageData onto it
let tempCanvas = document.createElement("canvas");
tempCanvas.width = width;
tempCanvas.height = height;
let tempCtx = tempCanvas.getContext("2d", { willReadFrequently: true });
tempCtx.putImageData(imageData, 0, 0);
// Create a new fabric.Image object with the new canvas as the source
let newImage = new fabric.Image(tempCanvas, {
left: 0,
top: 0,
selectable: true,
evented: false,
});
// Remove the existing fabric object from the canvas
canvas.remove(canvas.item(0));
// Add the new fabric.Image object to the canvas
canvas.add(newImage);
// Render the canvas to reflect the changes
canvas.renderAll();
}
我看到了这个问题。您可以完全避免使用 HTML 画布,而单独使用 Fabricjs 画布。由于网格大小为 20,我猜测给定的代码执行了太多迭代。让我们稍微简化一下。
首先,您可以避免使用标志检查相邻像素,因为它们不必要地使代码复杂化。您可以尝试为每次迭代添加新的坐标集,并检查要绘制的封闭颜色是否与 x 和 y 接近区域边界时的颜色相匹配,或者是否已访问过这组坐标以跳过当前迭代。如果不满足这个条件,则可以使用fabricjs的内置矩形函数来绘制每个像素。由于网格大小为 20 像素,因此您尝试将像素块绘制为总共 20*20 像素的一个对象。
最后,您需要以 20 的步长压入堆栈,等于网格大小。这应该将迭代次数限制为填充填充的块数。
函数洪水填充
function floodFill(newColor, x, y) {
const baseColor = getColorAtPxl(x, y);
if (colorMatch(baseColor, newColor)) {
return;
}
const stack = [];
const processed = new Set();
stack.push({ x, y });
while (stack.length) {
const operator = stack.pop();
const pxlColor = getColorAtPxl(operator.x, operator.y);
if (
!colorMatch(pxlColor, baseColor) ||
processed.has(`${operator.x}-${operator.y}`)
) {
continue;
}
processed.add(`${operator.x}-${operator.y}`);
const rect = new fabric.Rect({
left: Math.floor(operator.x / gridSize) * gridSize,
top: Math.floor(operator.y / gridSize) * gridSize,
width: gridSize,
height: gridSize,
fill: newColor,
selectable: true,
evented: false
});
canvas.add(rect);
stack.push({ x: operator.x + gridSize, y: operator.y });
stack.push({ x: operator.x - gridSize, y: operator.y });
stack.push({ x: operator.x, y: operator.y + gridSize });
stack.push({ x: operator.x, y: operator.y - gridSize });
}
canvas.renderAll();
}
函数 getColorAtPixel
function getColorAtPixel(x, y) {
const ctx = canvas.getContext("2d");
const pixel = ctx.getImageData(x, y, 1, 1).data;
return {
r: pixel[0],
g: pixel[1],
b: pixel[2],
a: pixel[3]
};
}
我希望这有助于简化和解决问题。让我知道。