javascripthtmlcanvasflood-fill

Flood fill for html5 canvas not working as expected


I have a flood fill I'm using from another post on here. I removed all the alpha channels but the colors are off. I'v racked my brain for days changing things around and using different fill methods but this one has gotten me closest. I'll be having users draw and fill as they please. Sorry if this is something simple. Only color that works is red and white.


<canvas id="myCanvas" width="300" height="150" style="border:1px solid grey"></canvas>

<script>
const c = document.getElementById("myCanvas");
const ctx = c.getContext("2d");

ctx.strokeStyle = "rgb(252, 53, 3)";
ctx.strokeRect(10, 10, 50, 50);
var red = [252, 53, 3];
floodFill(ctx, 15, 15, red);

ctx.strokeStyle = "rgb(3, 252, 3)";
ctx.strokeRect(75, 10, 50, 50);
var green = [3, 252, 3];
floodFill(ctx, 80, 15, green);

ctx.strokeStyle = "rgb(3, 119, 252)";
ctx.strokeRect(150, 10, 50, 50);
var blue = [3, 119, 252];
floodFill(ctx, 155, 15, blue);

ctx.strokeStyle = "rgb(0, 0, 0)";
ctx.strokeRect(225, 10, 50, 50);
var black = [0, 0, 0];
//floodFill(ctx, 230, 15, black);
//black get's stackoverflow and crashes page


function getPixel(imageData, x, y) {
  if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
    return [-1, -1, -1, -1];  // impossible color
  } else {
    const offset = (y * imageData.width + x) * 4;
    var pixel = imageData.data.slice(offset, offset + 4); 
    pixel[3] = 1; // ignore alpha
    return pixel;
  }
}

function setPixel(imageData, x, y, color) {
  const offset = (y * imageData.width + x) * 4;
  imageData.data[offset + 0] = color[0];
  imageData.data[offset + 1] = color[1];
  imageData.data[offset + 2] = color[2];
  imageData.data[offset + 3] = color[0];
}

function colorsMatch(a, b, rSq) {
  if (a === b) return true;
  if (a == null || b == null) return false;
  if (a.length !== b.length) return false;

  for (var i = 0; i < a.length; ++i) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

function floodFill(ctx, x, y, fillColor, range = 1) {
  // read the pixels in the canvas
  const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  // flags for if we visited a pixel already
  const visited = new Uint8Array(imageData.width, imageData.height);
  
  // get the color we're filling
  const targetColor = getPixel(imageData, x, y);
  
  console.log("target: " + targetColor + "Fill: " + fillColor);

  // check we are actually filling a different color
  if (!colorsMatch(targetColor, fillColor)) {

    const rangeSq = range * range;
    const pixelsToCheck = [x, y];
    while (pixelsToCheck.length > 0) {
      const y = pixelsToCheck.pop();
      const x = pixelsToCheck.pop();
      
      const currentColor = getPixel(imageData, x, y);

      if (!visited[y * imageData.width + x] && colorsMatch(currentColor, targetColor, rangeSq)) {

        setPixel(imageData, x, y, fillColor);
        visited[y * imageData.width + x] = 1;  // mark we were here already
        pixelsToCheck.push(x + 1, y);
        pixelsToCheck.push(x - 1, y);
        pixelsToCheck.push(x, y + 1);
        pixelsToCheck.push(x, y - 1);
      
      }
    }
    
    // put the data back
    ctx.putImageData(imageData, 0, 0);
  }
}
</script>

I don't know what is causing the issue, if I try to pass in rgb value black (0,0,0) it crashes.


Solution

  • To get full alpha you need to set the 4th items to 255 instead of 1 (in getPixel), nor to the red channel value (in setPixel).
    However, if you do ignore the alpha value in getPixel but still allow transparency, then the original black transparent pixel (rgba(0, 0, 0, 0)) will be seen as being the same color as a fully opaque black pixel (rgb(0, 0, 0)), and your code won't change it. So either disable transparency entirely on the context (e.g. draw a fully opaque white rectangle),

    const c = document.getElementById("myCanvas");
    const ctx = c.getContext("2d");
    
    /**
     * Convert transparent black pixels
     * to opaque white ones.
     */
    ctx.fillStyle = "white";
    ctx.fillRect(0, 0, c.width, c.height);
    
    ctx.strokeStyle = "rgb(252, 53, 3)";
    ctx.strokeRect(10, 10, 50, 50);
    var red = [252, 53, 3];
    floodFill(ctx, 15, 15, red);
    
    ctx.strokeStyle = "rgb(3, 252, 3)";
    ctx.strokeRect(75, 10, 50, 50);
    var green = [3, 252, 3];
    floodFill(ctx, 80, 15, green);
    
    ctx.strokeStyle = "rgb(3, 119, 252)";
    ctx.strokeRect(150, 10, 50, 50);
    var blue = [3, 119, 252];
    floodFill(ctx, 155, 15, blue);
    
    ctx.strokeStyle = "rgb(0, 0, 0)";
    ctx.strokeRect(225, 10, 50, 50);
    var black = [0, 0, 0];
    floodFill(ctx, 230, 15, black);
    
    function getPixel(imageData, x, y) {
      if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
        return [-1, -1, -1, -1];  // impossible color
      } else {
        const offset = (y * imageData.width + x) * 4;
        var pixel = imageData.data.slice(offset, offset + 4); 
        pixel[3] = 1; // ignore alpha
        return pixel;
      }
    }
    
    function setPixel(imageData, x, y, color) {
      const offset = (y * imageData.width + x) * 4;
      imageData.data[offset + 0] = color[0];
      imageData.data[offset + 1] = color[1];
      imageData.data[offset + 2] = color[2];
      imageData.data[offset + 3] = 255; // 255 is fully opaque
    }
    
    function colorsMatch(a, b, rSq) {
      if (a === b) return true;
      if (a == null || b == null) return false;
      if (a.length !== b.length) return false;
    
      for (var i = 0; i < a.length; ++i) {
        if (a[i] !== b[i]) return false;
      }
      return true;
    }
    
    function floodFill(ctx, x, y, fillColor, range = 1) {
      // read the pixels in the canvas
      const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
      
      // flags for if we visited a pixel already
      const visited = new Uint8Array(imageData.width, imageData.height);
      
      // get the color we're filling
      const targetColor = getPixel(imageData, x, y);
      
      console.log("target: " + targetColor + "Fill: " + fillColor);
    
      // check we are actually filling a different color
      if (!colorsMatch(targetColor, fillColor)) {
    
        const rangeSq = range * range;
        const pixelsToCheck = [x, y];
        while (pixelsToCheck.length > 0) {
          const y = pixelsToCheck.pop();
          const x = pixelsToCheck.pop();
          
          const currentColor = getPixel(imageData, x, y);
    
          if (!visited[y * imageData.width + x] && colorsMatch(currentColor, targetColor, rangeSq)) {
    
            setPixel(imageData, x, y, fillColor);
            visited[y * imageData.width + x] = 1;  // mark we were here already
            pixelsToCheck.push(x + 1, y);
            pixelsToCheck.push(x - 1, y);
            pixelsToCheck.push(x, y + 1);
            pixelsToCheck.push(x, y - 1);
          
          }
        }
        
        // put the data back
        ctx.putImageData(imageData, 0, 0);
      }
    }
    <canvas id="myCanvas" width="300" height="150" style="border:1px solid grey"></canvas>

    or let the alpha in the getPixel function.

    const c = document.getElementById("myCanvas");
    const ctx = c.getContext("2d");
    
    ctx.strokeStyle = "rgb(252, 53, 3)";
    ctx.strokeRect(10, 10, 50, 50);
    var red = [252, 53, 3];
    floodFill(ctx, 15, 15, red);
    
    ctx.strokeStyle = "rgb(3, 252, 3)";
    ctx.strokeRect(75, 10, 50, 50);
    var green = [3, 252, 3];
    floodFill(ctx, 80, 15, green);
    
    ctx.strokeStyle = "rgb(3, 119, 252)";
    ctx.strokeRect(150, 10, 50, 50);
    var blue = [3, 119, 252];
    floodFill(ctx, 155, 15, blue);
    
    ctx.strokeStyle = "rgb(0, 0, 0)";
    ctx.strokeRect(225, 10, 50, 50);
    var black = [0, 0, 0];
    floodFill(ctx, 230, 15, black);
    //black get's stackoverflow and crashes page
    
    
    function getPixel(imageData, x, y) {
      if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
        return [-1, -1, -1, -1];  // impossible color
      } else {
        const offset = (y * imageData.width + x) * 4;
        var pixel = imageData.data.slice(offset, offset + 4); 
        // Do not ignore alpha
        return pixel;
      }
    }
    
    function setPixel(imageData, x, y, color) {
      const offset = (y * imageData.width + x) * 4;
      imageData.data[offset + 0] = color[0];
      imageData.data[offset + 1] = color[1];
      imageData.data[offset + 2] = color[2];
      imageData.data[offset + 3] = 255; // 255 is fully opaque
    }
    
    function colorsMatch(a, b, rSq) {
      if (a === b) return true;
      if (a == null || b == null) return false;
      if (a.length !== b.length) return false;
    
      for (var i = 0; i < a.length; ++i) {
        if (a[i] !== b[i]) return false;
      }
      return true;
    }
    
    function floodFill(ctx, x, y, fillColor, range = 1) {
      // read the pixels in the canvas
      const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
      
      // flags for if we visited a pixel already
      const visited = new Uint8Array(imageData.width, imageData.height);
      
      // get the color we're filling
      const targetColor = getPixel(imageData, x, y);
      
      console.log("target: " + targetColor + "Fill: " + fillColor);
    
      // check we are actually filling a different color
      if (!colorsMatch(targetColor, fillColor)) {
    
        const rangeSq = range * range;
        const pixelsToCheck = [x, y];
        while (pixelsToCheck.length > 0) {
          const y = pixelsToCheck.pop();
          const x = pixelsToCheck.pop();
          
          const currentColor = getPixel(imageData, x, y);
    
          if (!visited[y * imageData.width + x] && colorsMatch(currentColor, targetColor, rangeSq)) {
    
            setPixel(imageData, x, y, fillColor);
            visited[y * imageData.width + x] = 1;  // mark we were here already
            pixelsToCheck.push(x + 1, y);
            pixelsToCheck.push(x - 1, y);
            pixelsToCheck.push(x, y + 1);
            pixelsToCheck.push(x, y - 1);
          
          }
        }
        
        // put the data back
        ctx.putImageData(imageData, 0, 0);
      }
    }
    <canvas id="myCanvas" width="300" height="150" style="border:1px solid grey"></canvas>

    Though, note that I didn't check your algorithm at all, so it may contain other issues.