OiO.lk Community platform!

Oio.lk is an excellent forum for developers, providing a wide range of resources, discussions, and support for those in the developer community. Join oio.lk today to connect with like-minded professionals, share insights, and stay updated on the latest trends and technologies in the development field.
  You need to log in or register to access the solved answers to this problem.
  • You have reached the maximum number of guest views allowed
  • Please register below to remove this limitation

Flood fill compatibility using Fabric JS

  • Thread starter Thread starter DImitris K
  • Start date Start date
D

DImitris K

Guest
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'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:

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",
});

Code:
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);
        }
    }
});

Code:
//------------------------ 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);
}

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

<p>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'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.</p>
<p>Here is my code:</p>
<pre><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",
});
</code></pre>
<pre><code>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);
}
}
});
</code></pre>
<pre><code>//------------------------ 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);
}
</code></pre>
<pre><code>//--------------------------------- 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();
}
</code></pre>
 
Top