A recursive approach to filling empty space on a canvas using Javascript

My goal was to develop a recursive function that would continue filling the empty spaces of a canvas until reaching the edges or encountering a different colored space.

function createFillingPoint() {
  let x = points[0][0],
    y = points[0][1];
  var pattern = ctx.getImageData(x, y, 1, 1).data;
  var colorToChange = rgbToHex(pattern);
  ctx.fillRect(x, y, 1, 1);
  colorFillRec(x + 1, y, width.value, height.value, colorToChange);
  colorFillRec(x - 1, y, width.value, height.value, colorToChange);
  colorFillRec(x, y + 1, width.value, height.value, colorToChange);
  colorFillRec(x, y - 1, width.value, height.value, colorToChange);
}

This particular function initializes the starting point and identifies the original color to be changed throughout the recursion.

function colorFillRec(x, y, w, h, colorToChange) {
  if (
    x > w ||
    x < 0 ||
    y > h ||
    y < 0 ||
    rgbToHex(ctx.getImageData(x, y, 1, 1).data) != colorToChange
  )
    return;
  ctx.fillRect(x, y, 1, 1);
  colorFillRec(x + 1, y, w, h, colorToChange);
  colorFillRec(x - 1, y, w, h, colorToChange);
  colorFillRec(x, y + 1, w, h, colorToChange);
  colorFillRec(x, y - 1, w, h, colorToChange);
}

This is the core recursive function. Both functions compare colors and halt execution if they differ from the original color.

Despite my attempts to run the function, I encountered the "Maximum call stack size exceeded" error... I struggled to come up with an alternative solution for achieving the desired outcome (whether through another recursive approach or not) but couldn't find one.

Answer №1

To optimize your code, consider using an explicit stack instead of relying on the call stack. Additionally, it will be more efficient to call getImageData just once for the entire canvas area, manipulate the image data, and then update the canvas with putImageData in a single operation.

Check out this demonstration:

function drawShapes(ctx) { // Create some shapes for demonstration purposes
    function rectangle(x, y, width, height, color) {
        ctx.fillStyle = color;
        ctx.beginPath()
        ctx.rect(x, y, width, height);
        ctx.fill();
    }

    function circle(x, y, radius, color) {
        ctx.fillStyle = color;
        ctx.beginPath()
        ctx.arc(x, y, radius, 0, Math.PI * 2);
        ctx.fill();    
    }

    rectangle(0, 0, 600, 180, "pink");
    rectangle(100, 20, 200, 40, "red");
    rectangle(50, 40, 100, 30, "blue");
    circle(160, 95, 30, "green");
    rectangle(170, 30, 5, 80, "pink");
    rectangle(190, 25, 100, 10, "white");
    circle(150, 110, 10, "white");
}

// The main flood-fill algorithm
function executeFloodFill(ctx, x, y, r, g, b, a=255) {

    function matchColor(index, source) {
        for (let j = 0; j < 4; j++) {
            if (imageData.data[index + j] !== source[j]) return false;
        }
        return true;
    }
    
    function setColor(index, target) {
        for (let j = 0; j < 4; j++) {
            imageData.data[index + j] = target[j];
        }
    }
    
    const {width, height} = ctx.canvas;
    const imageData = ctx.getImageData(0, 0, width, height);
    const startIndex = (y * width + x) * 4;
    const lineSize = width * 4;
    const maxIndex = lineSize * height;
    
    const sourceColor = [...imageData.data.slice(startIndex, startIndex+4)];
    const targetColor = [r, g, b, a];
    
    // Exit early if the starting pixel already has the target color
    if (sourceColor.every((value, idx) => value === targetColor[idx])) return;
    
    // Utilize an explicit stack
    const pixelStack = [startIndex];
    while (pixelStack.length) {
        const currentIndex = pixelStack.pop();
        if (!matchColor(currentIndex, sourceColor)) continue;
        setColor(currentIndex, targetColor);
        if (currentIndex < maxIndex - lineSize)      pixelStack.push(currentIndex + lineSize);
        if (currentIndex >= lineSize)                 pixelStack.push(currentIndex - lineSize);
        if (currentIndex % lineSize < lineSize - 4)   pixelStack.push(currentIndex + 4);
        if (currentIndex % lineSize >= 4)              pixelStack.push(currentIndex - 4);
    }
    ctx.putImageData(imageData, 0, 0);
}

const context = document.querySelector("canvas").getContext("2d");
drawShapes(context);
// Start the flood-fill after a 2-second delay
setTimeout(function () {
    executeFloodFill(context, 0, 0, 120, 80, 0, 255); // Fill from point (0, 0) with brown color
}, 2000);
<canvas width="600" height="180"></canvas>

This demonstration creates a sample drawing and initiates a flood-fill operation from the coordinates (0, 0). This means that it will identify connected pixels matching the color at (0, 0) and change them to brown.

Note that the default canvas color is black with full transparency (alpha = 0). Therefore, initiating a flood-fill from a pixel with the default value (rgba = 0,0,0,0) will only affect pixels with that exact rgba code, not ones set to white (255,255,255,255).

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Develop an Angular resolver that can be utilized across various scenarios

Im searching for a method to apply a single angular route resolve to all of my routes, each with different parameters: currently, my setup looks like this: { path: 'user/:any', component: UserprofileComponent, resolve ...

Exploring the concept of global objects within the expressjs framework

Currently, I am working on building a school management system. One issue I encountered is related to the creation of global objects in my backend when a teacher sends a post request. I am wondering whether multiple teachers accessing the server will res ...

Having trouble setting a background image for a specific DIV element in HTML

I am in the process of updating my webpage, and need some assistance implementing a small background image. Here is what I have done so far: https://i.sstatic.net/nTxtD.png Below is the marked section where I am trying to add the background image: https ...

Implementing a pop-up notification at the center of the display for empty fields

I am currently working on a task that requires me to display a pop-up message at the top-middle of the screen stating "please fill in all fields before submitting". This custom box should be stylable using CSS. Although the solution seems simple, I am str ...

click event to activate delayed function in dropdown

Within the li element, there is an onclick function with a data-toggle="dropdown" attribute. The issue at hand is that my function isn't triggered when I click once, but interestingly it works after clicking twice. https://i.sstatic.net/GdvoT.png I ...

What is the best way to locate the nearest marker using the Google Maps Direction Service?

Currently, I am engaged in the development of a Google Maps project where I am integrating markers retrieved from a database onto the map using the drawMarkers function. In addition to this, the Google Maps feature tracks your current location and refreshe ...

Enhancing data management with Vuex and Firebase database integration

Within my app, I am utilizing Firebase alongside Vuex. One particular action in Vuex looks like this: async deleteTodo({ commit }, id) { await fbs.database().ref(`/todolist/${store.state.auth.userId}/${id}`) .remove() .then ...

Using CSS nth-of-type on the same level of the DOM allows for specific

I was having trouble using the nth-of-type(2n+1) selector to target odd and even rows in the scenario below. What I wanted was for the nth-of-type selector to apply different styles to the odd rows with the classes "row-data row-header" and "row-data row-c ...

How about checking the memory usage in Javascript?

Similar Question: Looking for a Javascript memory profiler I am curious about determining the memory consumption of variables in JavaScript. Could it be done at all? ...

Creating an expand and collapse animation in a `flex` accordion can be achieved even when the container size is fixed using

Good day, I am in need of a fixed-height HTML/CSS/JS accordion. The requirement is for the accordion container's height to be fixed (100% body height) and for any panel content overflow, the scrollbar should appear inside the panel's content div ...

Achieving the functionality of making only one list item in the navbar bolded upon being clicked using React and Typescript logic

Currently, in my navigation bar, I am attempting to make only the active or clicked list item appear bold when clicked. At the moment, I can successfully achieve this effect; however, when I click on other list items, they also become bolded, while the ori ...

Mastering Checkbox State Control in Vue3 Using Component Props

This scenario is as follows: Parent component: It holds a dynamic Todo object with a checked state {checked: true} and sends it to the Todo component. Todo component: Takes in the Todo as a prop and links the checked value to a checkbox. When the checkbo ...

Update the JSON data following deletion

I have received the following JSON data: "memberValidations": [ { "field": "PRIMARY_EMAIL", "errorCode": "com.endeavour.data.validation.PRIMARY_EMAIL", "createdDateTime": null }, ...

When refreshing the page, redux-persist clears the state

I have integrated redux-persist into my Next.js project. The issue I am facing is that the state is getting saved in localStorage when the store is updated, but it gets reset every time the page changes. I suspect the problem lies within one of the reducer ...

Challenge in Decision Making

I am curious why this type of selection is not functioning properly for html select options, while it works seamlessly for other input types like Radios or checkboxes. Any thoughts? $('#resetlist').click(function() { $('input:select[nam ...

Passing data between child components using Vuejs 3.2 for seamless communication within the application

In my chess application, I have a total of 3 components: 1 parent component and 2 child components. The first child component, called Board, is responsible for updating the move and FEN (chess notation). const emit = defineEmits(['fen', 'm ...

Menus that mimic the style of Google Translate's select options

I'm looking to design an HTML select menu using CSS in a similar fashion to Google Translate's style on Google Translate. I've searched for tutorials online, but they all involve jQuery. Is there a way to achieve something similar using only ...

Quasar Framework Vue.js project experiencing unexpected disablement of console/debug output in PWA InjectManifest workbox

I recently added PWA capability to my project built with Vue.js / Quasar Framework. After changing the "workboxPluginMode" property to "InjectManifest", I noticed that Workbox was initially giving me debug information in the console as expected. Moreover, ...

Ways to send users to a different page with parameters without relying on cookies

Is there a way to redirect users from URLs containing parameters like /?v=xxx to the index page without those parameters showing in the address bar? I still need to retain and use these parameters internally. One approach I considered was using custom hea ...

Tips on choosing filters and leveraging the chosen value for searching in a Vue application

I am currently working on a Vue JS application that allows users to apply filters and search a database. The user can select or type in filters, such as "to" and "from", which are then used in a fetch URL to retrieve data from a json-server. Once the user ...