If you open Drawasaurus and create a closed figure on the canvas board and fill in that figure, you can see that it happens instantly. But when I try to implement the fill algorithm, it takes over 2-3 seconds to even fill a 200 by 200 pixel grid.
Is the implementation on Drawasaurus, different than flood fill? I am assuming that they might have divided their canvas in bigger grids of pixels. Instead of 1x1, maybe 10x10? And hence, that is more faster? Not sure though.
Need your help in understanding different approaches for filling a closed figure in canvas and how I can optimize it.
Here is my implementation:
import {colorToRgba, RGBA} from "./utils";
const { coordinates: c, color } = event;
const canvasWidth = canvasCtx!.width;
// Initial pixel information for the entire canvas. Doing it this way because invoking
// getImageData several times is expensive than just doing it once
const canvasPixelData = canvasCtx!.getImageData(0, 0, canvasCtx!.width, canvasCtx!.height);
// returns the indices for the r,g,b,a components of the colors of the provided pixel coordinates
const getColorIndicesForPixel = (
x: number,
y: number
): { rIndex: number; gIndex: number; bIndex: number; aIndex: number } => {
const rIndex = y * (canvasWidth * 4) + x * 4;
const [gIndex, bIndex, aIndex] = [rIndex + 1, rIndex + 2, rIndex + 3];
return { rIndex, gIndex, bIndex, aIndex };
};
const getColorForPixel = (x: number, y: number) => {
const { rIndex, gIndex, bIndex, aIndex } = getColorIndicesForPixel(x, y);
const r = canvasPixelData.data[rIndex];
const g = canvasPixelData.data[gIndex];
const b = canvasPixelData.data[bIndex];
const a = canvasPixelData.data[aIndex];
return `rgba(${r}, ${g}, ${b}, ${a / 255})`;
};
// Color of the starting pixel
const initialColor = getColorForPixel(c.x, c.y);
// set of all the nodes that are already visited
const visitedNodes = new Set<string>();
console.log('reached here');
// Implementation of BFS algorithm for filling colors in
// the region
const colorFillAlgo = (x: number, y: number, newColor: RGBA) => {
const pixelIndices = getColorIndicesForPixel(x, y);
console.log('filling', x, y);
// if index out of bounds, return
if (x < 0 || x >= canvasCtx!.width || y < 0 || y >= canvasCtx!.height) return;
// if color is not same as initial color, return
if (getColorForPixel(x, y) !== initialColor) return;
// if already visited, return
if (visitedNodes.has(`${[x, y]}`)) return;
// Add to visited nodes
visitedNodes.add(`${[x, y]}`);
// update the color in pixel data
canvasPixelData.data[pixelIndices.rIndex] = newColor.r;
canvasPixelData.data[pixelIndices.gIndex] = newColor.g;
canvasPixelData.data[pixelIndices.bIndex] = newColor.b;
canvasPixelData.data[pixelIndices.aIndex] = Math.floor(newColor.a * 255);
// fill the surrounding nodes
colorFillAlgo(x + 1, y, newColor);
colorFillAlgo(x - 1, y, newColor);
colorFillAlgo(x, y + 1, newColor);
colorFillAlgo(x, y - 1, newColor);
};
// start the algorithm from starting point
colorFillAlgo(c.x, c.y, colorToRgba(color));
// update the canvas with the new color values
canvasCtx!.putImageData(canvasPixelData, 0, 0);
Edit 1 Following is after applying some optimizations as suggested
import { colorToRgba, RGBA } from "./pkg-utils";
import { not, Queue } from "@vighnesh153/utils";
const { coordinates: c, color } = event;
const canvasWidth = canvasCtx!.width;
// Initial pixel information for the entire canvas. Doing it this way because invoking
// getImageData several times is expensive than just doing it once
const canvasPixelData = canvasCtx!.getImageData(0, 0, canvasCtx!.width, canvasCtx!.height);
// returns the indices for the r,g,b,a components of the colors of the provided pixel coordinates
const getColorIndicesForPixel = (
x: number,
y: number
): { rIndex: number; gIndex: number; bIndex: number; aIndex: number } => {
const rIndex = y * (canvasWidth * 4) + x * 4;
const [gIndex, bIndex, aIndex] = [rIndex + 1, rIndex + 2, rIndex + 3];
return { rIndex, gIndex, bIndex, aIndex };
};
const getColorForPixel = (x: number, y: number) => {
const { rIndex, gIndex, bIndex, aIndex } = getColorIndicesForPixel(x, y);
const r = canvasPixelData.data[rIndex];
const g = canvasPixelData.data[gIndex];
const b = canvasPixelData.data[bIndex];
const a = canvasPixelData.data[aIndex];
return { r, g, b, a };
};
const areColorsEqual = (color1: RGBA, color2: RGBA) => {
if (color1.r !== color2.r) return false;
if (color1.g !== color2.g) return false;
if (color1.b !== color2.b) return false;
if (color1.a !== color2.a) return false;
return true;
};
// Color of the starting pixel
const initialColor = getColorForPixel(c.x, c.y);
const newColor = colorToRgba(color);
// In the pixel data, "alpha" needs to be between (0 and 255)
newColor.a *= 255;
// set of all the nodes that are already visited
const visitedNodes: Record<number, Record<number, boolean>> = {};
const pixelQueue = new Queue([c.x, c.y]);
// Implementation of BFS algorithm for filling colors in
// the region
while (not(pixelQueue.isEmpty)) {
const [x, y] = pixelQueue.popLeft()!;
const { rIndex, gIndex, bIndex, aIndex } = getColorIndicesForPixel(x, y);
// if index out of bounds, return
if (x < 0 || x >= canvasCtx!.width || y < 0 || y >= canvasCtx!.height) continue;
// if color is not same as initial color, return
if (not(areColorsEqual(getColorForPixel(x, y), initialColor))) continue;
// if already visited, return
if (visitedNodes[x]?.[y]) continue;
// Add to visited nodes
visitedNodes[x] = visitedNodes[x] || ({} as Record<number, boolean>);
visitedNodes[x][y] = true;
// update the color in pixel data
canvasPixelData.data[rIndex] = newColor.r;
canvasPixelData.data[gIndex] = newColor.g;
canvasPixelData.data[bIndex] = newColor.b;
canvasPixelData.data[aIndex] = newColor.a;
// fill the surrounding nodes
pixelQueue.pushRight([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
}
// update the canvas with the new color values
canvasCtx!.putImageData(canvasPixelData, 0, 0);