Canvas - floodfill leaves white pixels at edges

后端 未结 4 567
清歌不尽
清歌不尽 2021-01-20 01:11

I am creating a drawing app. I have succeeded to do everything. When I paint the image with a dark color, some white pixels appear at the edges. I tried to change the value

4条回答
  •  盖世英雄少女心
    2021-01-20 01:27

    I suggest two changes:

    1. Blend pixel & fill color instead of hard override
    2. Restrict fill area based on intensity gradient changes instead of simple threshold

    Filling in horizontal and vertical direction until the sign of the intensity gradient flips from either + to - OR - to + lets us fill the whole area including 'half' of the black border. By inspecting the gradient we just make sure not to overstep the intensity minimum and thus avoid filling a neighboring area.

    Have a look at the following demo:

    // Get pixel intensity:
    function getIntensity(data, i) {
      return data[i] + data[i + 1] + data[i + 2];
    }
    
    // Set pixel color:
    function setColor(data, i, r, g, b) {
      data[i] &= r;
      data[i + 1] &= g;
      data[i + 2] &= b;
    }
    
    // Fill a horizontal line:
    function fill(x, y, data, width, r, g, b) {
      var i_start = y * (width << 2);
      var i = i_start + (x << 2);
      var i_end = i_start + (width << 2);
      var i_intensity = getIntensity(data, i);
    
      // Horizontal line to the right:
      var prev_gradient = 0;
      var prev_intensity = i_intensity;
      for (var j = i; j < i_end; j += 4) {
        var intensity = getIntensity(data, j);
        gradient = intensity - prev_intensity;
        prev_intensity = intensity;
        if ((prev_gradient > 0 && gradient < 0) || (prev_gradient < 0 && gradient > 0)) break;
        if (gradient != 0) prev_gradient = gradient;
    
        setColor(data, j, 255, 0, 0);
      }
    
      // Horizontal line to the left:
      prev_gradient = 0;
      prev_intensity = i_intensity;
      for (var j = i - 4; j > i_start; j -= 4) {
        var intensity = getIntensity(data, j);
        gradient = intensity - prev_intensity;
        prev_intensity = intensity;
        if ((prev_gradient > 0 && gradient < 0) || (prev_gradient < 0 && gradient > 0)) break;
        if (gradient != 0) prev_gradient = gradient;
    
        setColor(data, j, 255, 0, 0);
      }
    }
    
    // Demo canvas:
    var canvas = document.getElementById('canvas');
    var context = canvas.getContext('2d');
    
    // Fill horizontal line on click:
    canvas.addEventListener('mousedown', event => {
      var rect = canvas.getBoundingClientRect();
      var x = event.clientX - rect.left | 0;
      var y = event.clientY - rect.top | 0;
    
      var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
      fill(x, y, imageData.data, imageData.width);
      context.putImageData(imageData, 0, 0);
    });
    
    // Load a sample image:
    var image = new Image();
    image.addEventListener('load', event => {
      context.drawImage(event.target, 0, 0, canvas.width, canvas.height);
    });
    image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAACCAIAAAABwbG5AAAAA3NCSVQICAjb4U/gAAAAGXRFWHRTb2Z0d2FyZQBnbm9tZS1zY3JlZW5zaG907wO/PgAAACZJREFUCJlj+I8Kbt68mZeXd/PmTbgIw38MsG/fvoKCgqdPn0K4ACAfPGrloJp1AAAAAElFTkSuQmCC';

    You can improve the performance by tracing only one color channel instead of summing all three for your getIntensity function.

    Also, you would need to deal with 'clearing' an already filled area before filling it again with another color, due to the blending mode.

    You could e. g. keep a single-channel grayscale image data array of the background image in memory and use it as the source image for your fill algorithm.

    It might be even more performant to (manually or automatically) transform all your grayscale images to 1-bit outlines with binary boundaries and use them as the source for your floodfill algorithm, blending the result smoothly with the grayscale image.

提交回复
热议问题