使用 Fabric JS 实现洪水填充兼容性

问题描述 投票:0回答:1

我已经初始化了一个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();
}   
javascript canvas fabricjs
1个回答
0
投票

我看到了这个问题。您可以完全避免使用 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]
    };
}

我希望这有助于简化和解决问题。让我知道。

© www.soinside.com 2019 - 2024. All rights reserved.