Flood fill compatibility using Fabric JS

huangapple go评论278阅读模式
英文:

Flood fill compatibility using Fabric JS

问题

我已经初始化了一个fabric.js画布,并尝试实现了一个泛洪填充算法。我在渲染图像数据回fabric画布时遇到了问题,我找到的唯一方法是创建一个临时画布,将图像数据传递到这个画布上,然后将其添加回初始fabric画布作为图像。然而,我不能使用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);
        }
    }
});
//------------------------ 用于添加矩形的辅助函数 ------------------------//
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);
}
//--------------------------------- 泛洪填充 -------------------------------------//
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 };

    // 检查基本颜色和新颜色是否相同
    if (colorMatch(baseColor, newColor)) {
        return;
    }

    // 将点击的位置添加到堆栈中
    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;

        // 移动到最上面的连续下一个像素
        while (contiguousUp && operator.y >= 0) {
            operator.y--;
            contiguousUp = colorMatch(
                getColorAtPixel(imageData, operator.x, operator.y),
                baseColor
            );
        }

        // 向下移动
        while (contiguousDown && operator.y < height) {
            setColorAtPixel(imageData, newColor, operator.x, operator.y);

            // 检查左侧
            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;
            }

            // 检查右侧
            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
            );
        }
    }

    // 创建一个新的画布元素,并将修改后的图像数据绘制到其中
    let tempCanvas = document.createElement("canvas");
    tempCanvas.width = width;
    tempCanvas.height = height;
    let tempCtx =

<details>
<summary>英文:</summary>

I have initialized a fabric.js canvas and I tried to implement a flood fill algorithm. I have a problem rendering the image Data back to the fabric canvas and the only way I found is creating a temporary canvas, pass the image data on this canvas and adding it back to the initial fabric canvas as an image. However I can&#39;t use the rest of the Fabric js tools like selection because after I use the flood fill algorithm it considers the drawn rectangles as an image and not as individual objects.

Here is my code:

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 &quot;rgba(&quot; + r + &quot;,&quot; + g + &quot;,&quot; + b + &quot;,1)&quot;;
}
throw new Error(&quot;Bad Hex&quot;);

}

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 &amp; 0xff;
data[4 * (width * y + x) + 1] = color.g &amp; 0xff;
data[4 * (width * y + x) + 2] = color.b &amp; 0xff;
data[4 * (width * y + x) + 3] = color.a &amp; 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 &amp;&amp; operator.y &gt;= 0) {
operator.y--;
contiguousUp = colorMatch(
getColorAtPixel(imageData, operator.x, operator.y),
baseColor
);
}
// Move downward
while (contiguousDown &amp;&amp; operator.y &lt; height) {
setColorAtPixel(imageData, newColor, operator.x, operator.y);
// Check left
if (
operator.x - 1 &gt;= 0 &amp;&amp;
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 &lt; width &amp;&amp;
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(&quot;canvas&quot;);
tempCanvas.width = width;
tempCanvas.height = height;
let tempCtx = tempCanvas.getContext(&quot;2d&quot;, { 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();

}


</details>
# 答案1
**得分**: 0
我明白问题所在。您可以完全避免使用HTML画布,仅使用fabricjs画布。由于您的网格大小为20,我猜测给定的代码执行了太多次迭代。让我们简化一下。
首先,您可以避免使用标志来检查相邻的像素,因为它们会不必要地使代码复杂化。您可以尝试为每次迭代添加新的坐标集,并检查要填充的封闭颜色是否与x和y接近区域边界的颜色匹配,或者此坐标集是否已被访问,以跳过当前迭代。如果不满足此条件,然后您可以使用fabricjs的内置Rect函数来绘制每个像素。由于您的网格大小为20px,您正在尝试绘制20*20像素的一组像素块作为一个对象。
最后,您需要使用步长为20将元素推入堆栈,与网格大小相等。这应该将迭代次数限制为填充的块数。
```javascript
function floodFill(newColor, x, y) {
const baseColor = getColorAtPixel(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 = getColorAtPixel(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();
}
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]
};
}

希望这有助于简化和解决问题。请告诉我是否有帮助。

英文:

I see the issue. You can avoid using the HTML canvas altogether and work with the fabricjs canvas alone. Since you have a grid size of 20, my guess is the given code performs too many iterations. Let's simplify that a little.

First of all, you can avoid checking for neighboring pixels using flags, as they unnecessarily complicate the code. You can try adding new sets of coordinates for every iteration and check to see if the enclosed color to be painted matches the color as the x and y approach the borders of the area, or if this set of coordinates has already been visited to skip the current iteration. If this condition is not met, you can then use fabricjs' built-in Rect function to paint each pixel. Since your grid size is 20px, you are trying to paint blocks of pixels as one object of a total of 20*20 pixels.

Finally, you need to push to your stack with a step of 20, equal to the grid size. This should limit the number of iterations to the number of blocks painted with fill.

function floodFill

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();
}

function getColorAtPixel

function getColorAtPixel(x, y) {
const ctx = canvas.getContext(&quot;2d&quot;);
const pixel = ctx.getImageData(x, y, 1, 1).data;
return {
r: pixel[0],
g: pixel[1],
b: pixel[2],
a: pixel[3]
};
}

I hope this helps simplify and solve the problem. Let me know.

huangapple
  • 本文由 发表于 2023年7月31日 19:07:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/76803000.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定