Холст - заливка оставляет белые пиксели по краям для изображений PNG с прозрачным

Теперь я попытался выполнить алгоритм заливки для заполнения прозрачных изображений PNG с использованием алгоритма заливки из статьи Как я могу избежать превышения максимального размера стека вызовов во время алгоритма заливки?, которые используют нерекурсивный метод вместе с Uint32Array для справиться со стеком цветов с работой достаточно хорошо.

Однако этот алгоритм заливки оставил белый цвет (на самом деле светло-серый край или края сглаживания), которые остались незаполненными. Вот мой код:

 var BrushColorString  = '#F3CDA6'; // skin color 
canvas.addEventListener('mousedown', function(e) {
        
        const rect = canvas.getBoundingClientRect()
        CanvasMouseX = e.clientX - rect.left;
        CanvasMouseY = e.clientY - rect.top;
        
        if (mode === 'flood-fill')
        {
            // test flood fill algorithm
            paintAt(context,  CanvasMouseX,CanvasMouseY,hexToRgb(BrushColorString));
            
        }
    });
function paintAt(ContextOutput,startX, startY,curColor) {
//function paintAt(ctx,startX, startY,curColor) {   
    // read the pixels in the canvas
    
    const width = ContextOutput.canvas.width, 
    height = ContextOutput.canvas.height,pixels = width*height;
    const imageData = ContextOutput.getImageData(0, 0, width, height);
    var data1 = imageData.data;
    const p32 = new Uint32Array(data1.buffer);  
    const stack = [startX + (startY * width)]; // add starting pos to stack
    const targetColor = p32[stack[0]];
    var SpanLeft = true, SpanRight = true; // logic for spanding left right
    var leftEdge = false, rightEdge = false; 
     
    // proper conversion of color to Uint32Array  
    const newColor = new Uint32Array((new Uint8ClampedArray([curColor.r,curColor.g, curColor.b, curColor.a])).buffer)[0];
    // need proper comparison of target color and new Color
    if (targetColor === newColor || targetColor === undefined) { return } // avoid endless loop
    

    while (stack.length){  
    
        let idx = stack.pop();
        while(idx >= width && p32[idx - width] === targetColor) { idx -= width }; // move to top edge
        SpanLeft = SpanRight = false;   // not going left right yet 
        leftEdge = (idx % width) === 0;          
        rightEdge = ((idx +1) % width) === 0;
        while (p32[idx] === targetColor) {
            p32[idx] = newColor;
            if(!leftEdge) {
                if (p32[idx - 1] === targetColor) { // check left
                    if (!SpanLeft) {        
                        stack.push(idx - 1);  // found new column to left
                        SpanLeft = true;  // 
                    } else if (SpanLeft) { 
                        SpanLeft = false; 
                    }
                }
            }
            if(!rightEdge) {
                if (p32[idx + 1] === targetColor) {
                    if (!SpanRight) {
                        stack.push(idx + 1); // new column to right
                        SpanRight = true;
                    }else if (SpanRight) { 
                        SpanRight = false; 
                    }
                }
            }
            idx += width;
        
        }
        
    
    } 
    
 
    clearCanvas(ContextOutput);
    ContextOutput.putImageData(imageData,0, 0); 
     
    
    
};
function hexToRgb(hex) {
        var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? {
              r: parseInt(result[1], 16),
              g: parseInt(result[2], 16),
              b: parseInt(result[3], 16),
              a: 255
        } : null;
    }; 

Пока что я пробовал использовать следующее предложение:

  1. с использованием функции matchOutlineColor с использованием значения RGBA, указанного в Canvas - заливка оставляет белые пиксели по краям
  2. Когда я пытался реализовать ограничение области заливки на основе изменений градиента интенсивности вместо простого порога, упомянутого в Canvas - заливка оставляет белые пиксели по краям, который считается наиболее многообещающим алгоритмом. Я до сих пор не знаю, как реализовать этот алгоритм с минимальными изменениями в существующем алгоритме для решения проблемы сглаживания краев для футляры изображений с прозрачным.
  3. Когда я смотрю на пример применения допуска и толерантности, Fade, упомянутые в Заливка холста не заполняется до края, я до сих пор не понимаю, как реализовать такой допуск и допуск Fade в моем случае.
  4. Метод разницы цветов (функция colorDiff) в пределах указанного допуска в Canvas Javascript FloodFill алгоритм оставил белые пиксели без цвета и до сих пор не работает. То же самое можно сказать о функции colorsMatch, чтобы она находилась в пределах Range Square (rangeSq), упомянутого в Как выполнить заливку с помощью HTML Canvas?, который все еще не может решить проблему сглаживания краев.

Если у вас есть какие-либо идеи о том, как решить проблемы сглаживания границ алгоритма заливки, пожалуйста, ответьте как можно скорее.

Проблема с анти-псевдонимом Edge с алгоритмом заливки в Javascript

Обновлено:

Вот исправленный код функции paintAt из предложения, учитывающего допуск:

<div id="container"><canvas id="control" >Does Not Support Canvas Element</canvas></div>
 <div><label for="tolerance">Tolerance</label>
<input id="tolerance" type="range" min="0" max="255" value="32" step="1" oninput="this.nextElementSibling.value = this.value"><output>32</output></div>
var canvas = document.getElementById("control");
var context = canvas.getContext('2d');
var CanvasMouseX =  -1; var CanvasMouseY = -1;
var BrushColorString  = '#F3CDA6'; // skin color

 
 canvas.addEventListener('mousedown', function(e) {
        
        const rect = canvas.getBoundingClientRect()
        CanvasMouseX = e.clientX - rect.left;
        CanvasMouseY = e.clientY - rect.top;
         
        // testing 
        
        
        if (mode === 'flood-fill')
        {
            // test flood fill algorithm
            paintAt(context,CanvasMouseX,CanvasMouseY,
hexToRgb(BrushColorString),tolerance.value);
             
            
        }
    });
function hexToRgb(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
          r: parseInt(result[1], 16),
          g: parseInt(result[2], 16),
          b: parseInt(result[3], 16),
          a: 255
    } : null;
};
function clearCanvas(ctx) {
    ctx.clearRect(0, 0,ctx.canvas.width,ctx.canvas.height);
    
}; 
function colorDistance(index, R00,G00,B00,A00, data0)
{
    var index1 = index << 2; // multiplyed by 4
    const R = R00 - data0[index1 + 0];
    const G = G00 - data0[index1 + 1];
    const B = B00 - data0[index1 + 2];
    const A = A00 - data0[index1 + 3];      
    return Math.sqrt((R * R) + (B * B) + (G * G) + (A * A));
}

function paintAt(ContextOutput,startX, startY,curColor,tolerance) {
    // read the pixels in the canvas
    
    const width = ContextOutput.canvas.width, 
          height = ContextOutput.canvas.height, pixels = width*height;
        
    const rightEdgeNum = width - 1, bottomEdgeNum = height - 1; 
    const imageData = ContextOutput.getImageData(0, 0, width, height);
    var data1 = imageData.data;
    const p32 = new Uint32Array(data1.buffer);  
    const stack = [startX + (startY * width)]; // add starting pos to stack
    const targetColor = p32[stack[0]];
    
    var SpanLeft = true, SpanRight = true; // logic for spanning left right
    var leftEdge = false, rightEdge = false, IsBlend = false; 
    const DistancesArray = new Uint16Array(pixels);  // array distance value  
    var R=-1,G=-1,B=-1,A = -1,idx =0,Distance=0; 
    var R0 = data1[(4*(startX + (startY * width)))+0],
        G0 = data1[(4*(startX + (startY * width)))+1], 
        B0 = data1[(4*(startX + (startY * width)))+2],
        A0 = data1[(4*(startX + (startY * width)))+3];
    var CalculatedTolerance = Math.sqrt(tolerance * tolerance * 4);

    const BlendR = curColor.r |0, BlendG = curColor.g |0, 
          BlendB = curColor.b |0, BlendA = curColor.a|0; 
    // color variable for blending 
    const newColor = new Uint32Array((new Uint8ClampedArray([BlendR,BlendG,BlendB,BlendA])).buffer)[0];  

    if (targetColor === newColor || targetColor === undefined) { return } 
    // avoid endless loop
    
        while (stack.length){  
            idx = stack.pop();

            while (idx >= width && 
            colorDistance(idx - width,R0,G0,B0,A0,data1) <= CalculatedTolerance) { idx -= width }; // move to top edge
            SpanLeft = SpanRight = false;   // not going left right yet 
            leftEdge = (idx % width) === 0;          
            rightEdge = ((idx +1) % width) === 0;

            while ((Distance = colorDistance(idx,R0,G0,B0,A0,data1)) <= CalculatedTolerance) {
                DistancesArray[idx] = (Distance / CalculatedTolerance) * 255 | 0x8000; 
                p32[idx] = newColor; 
                if(!leftEdge) {

                    if (colorDistance(idx - 1,R0,G0,B0,A0,data1) <= CalculatedTolerance) { // check left
                        if (!SpanLeft) {        
                            stack.push(idx - 1);  // found new column to left
                            SpanLeft = true;  // 
                        } else if (SpanLeft) { 
                            SpanLeft = false; 
                        }
                    }
                }
                if(!rightEdge) {
                    if (colorDistance(idx + 1,R0,G0,B0,A0,data1) <= CalculatedTolerance) { 
                        if (!SpanRight) {
                        stack.push(idx + 1); // new column to right
                        SpanRight = true;
                        }else if (SpanRight) { 
                            SpanRight = false; 
                        }
                    }
                }
                idx += width;
    
            }
        }    
        idx = 0;
        while (idx <= pixels-1) {
            Distance = DistancesArray[idx];
            if (Distance !== 0) {
                if (Distance === 0x8000) {
                    p32[idx] = newColor;
                } else {
                     IsBlend = false;
                    const x = idx % width;
                    const y = idx / width | 0;
                    if (x >= 1 && DistancesArray[idx - 1] === 0) { IsBlend = true }
                    else if (x <= rightEdgeNum -1 && DistancesArray[idx + 1] === 0) { IsBlend = true }
                    else if (y >=1 && DistancesArray[idx - width] === 0) { IsBlend = true }
                    else if (y <=bottomEdgeNum-1 && DistancesArray[idx + width] === 0) { IsBlend = true }
                    if (IsBlend) {
                        // blending at the edge 
                        Distance &= 0xFF;             
                        Distance = Distance / 255;        
                        const invDist = 1 - Distance; 
                        const idx1 = idx << 2;    
                        data1[idx1 + 0] = data1[idx1 + 0] * Distance + BlendR * invDist;
                        data1[idx1 + 1] = data1[idx1 + 1] * Distance + BlendG * invDist;
                        data1[idx1 + 2] = data1[idx1 + 2] * Distance + BlendB * invDist;
                        data1[idx1 + 3] = data1[idx1 + 3] * Distance + BlendA * invDist;
                    
                    } else {
                        p32[idx] = newColor;
                    }
                }
            }
            idx++;
        }
    // this recursive algorithm works but still not working well due to the issue stack overflow!
    clearCanvas(ContextOutput);
    ContextOutput.putImageData(imageData,0, 0); 
   // way to deal with memory leak at the array. 
    DistancesArray = [];

    newColor = [];
    p32 = [];
    
};

Однако результаты заливки оказались неудовлетворительными, как показано в допуске перехода, как показано здесь: ' Результат в точке допуска при переходе  Результат в точке допуска при переходе, когда  терпимость стала слишком большой

Как я могу справиться с такой проблемой, когда терпимости стало слишком много. Любой альтернативный алгоритм будет оценен по достоинству.


person Wisarut Bholsithi    schedule 18.12.2020    source источник


Ответы (1)


Двухпроходная заливка в 4-м измерении

Я являюсь автором принятых ответов на Как избежать превышения максимального размера стека вызовов во время алгоритма заливки? и Заливка холста не заполняет до края

К сожалению, идеального решения не существует.

У следующего метода есть проблемы.

  • Установка допуска таким образом, чтобы он получал все сглаживание краев, часто заполняет нежелательные области.
  • Установка слишком низкого допуска может сделать края еще хуже, чем при стандартной заливке.
  • Повторные заливки приведут к более жесткому сглаживанию краев.
  • Использует простую функцию наложения. Правильную функцию смешивания можно найти на странице W3C Уровень смешивания и смешивания нормальный Извините, я у меня нет времени, чтобы закончить этот ответ.
  • Сложно преобразовать в градиенты или узорную заливку.

Есть гораздо лучшее решение, но оно состоит из более чем 1000 строк, и сам код не может соответствовать пределу ответа 32 КБ.

Этот ответ представляет собой пошаговое руководство по изменению вашей функции для уменьшения сглаживания краев с помощью допуска и простого смешения краев.

Примечание

  • В различных фрагментах ответа могут быть опечатки или неправильные названия. Для правильного рабочего кода см. Пример внизу.

Толерантность

Самый простой метод обнаружения краев - использовать допуск и пиксели заливки, которые находятся в пределах допуска цвета пикселя в исходной точке заливки.

Это позволяет заливке перекрывать края сглаживания, которые затем могут быть обнаружены и смешаны, чтобы уменьшить артефакты из-за сглаживания.

Проблема в том, что для получения хорошего покрытия сглаживания требуется большой допуск, и это приводит к заполнению областей, которые вы интуитивно не хотели бы окрашивать.

Расчет цветового расстояния

Цвет может быть представлен тремя значениями: красным, зеленым, синим. Если заменить имена на x, y, z, легко увидеть, как каждый цвет занимает уникальное положение в трехмерном пространстве.

Лучше всего то, что расстояние между любыми двумя цветами в этом трехмерном пространстве напрямую коррелирует с воспринимаемой разницей в цвете. Таким образом, мы можем использовать простую математику, чтобы вычислить разницу (Пифагор).

Поскольку нам нужно также учитывать альфа-канал, нам нужно перейти на одно измерение. Каждый цвет и его альфа-часть имеют уникальную точку в четырехмерном пространстве. Расстояние между любым из этих четырехмерных цветов напрямую зависит от воспринимаемой разницы в цвете и прозрачности.

К счастью, нам не нужно представлять четырехмерное пространство, все, что мы делаем, это расширяем математику (Пифагор работает во всех евклидовых измерениях).

Таким образом, мы получаем функцию и подготовительный код, которые вы можете добавить к своей функции заливки.

var idx = stack[0] << 2; // remove let first line inside while (stack.length){ 
const r = data1[idx] ;
const g = data1[idx + 1] ;
const b = data1[idx + 2];
const a = data1[idx + 3]
function colorDist(idx) {  // returns the spacial distance from the target color of pixel at idx
    idx <<= 2;
    const R = r - data1[i];
    const G = g - data1[i + 1];
    const B = b - data1[i + 2];
    const A = a - data1[i + 3];      
    return (R * R + B * B + G * G + A * A) ** 0.5;
}

К объявлению функции мы добавляем допуск аргумента, указанный как значение от 0 до 255.

Объявление функции меняется с

function paintAt(contextOutput, startX, startY, curColor) {

To

function paintAt(contextOutput, startX, startY, curColor, tolerance = 0) {

С tolerance в качестве необязательного аргумента.

  • tolerance из 0 заполняет только targetColor
  • tolerance из 255 должны заполнить все пиксели

Нам нужно преобразовать допуск из значения канала в значение расстояния 4D, чтобы 255 покрывали наибольшее расстояние между двумя цветами в цветовом пространстве 4D.

Добавьте следующую строку в начало функции paintAt

 tolerance = (tolerance * tolerance * 4) ** 0.5; // normalize to 4D RGBA space

Теперь нам нужно изменить операторы соответствия пикселей, чтобы использовать допуск. Везде, где у вас есть p32[idx] === targetColor или подобное, необходимо заменить на colorDist(idx) <= tolerance. Исключением является внутренний цикл while, так как нам нужно использовать цветовое расстояние 4D.

 while (checkPixel(ind)) {

становится

 // declare variable dist at top of function
 while ((dist = colorDist(idx)) <= tolerance) {

Двухпроходное решение

Чтобы бороться с наложением, нам нужно смешать цвет заливки на величину, пропорциональную цветовому расстоянию.

Выполнение этого для всех пикселей означает, что пиксели, удаленные от края заливки, будут иметь неправильный цвет, если цветовое расстояние не равно 0 и меньше допуска.

Нам нужно смешивать пиксели только в том случае, если они находятся на краю заливки, за исключением тех, которые находятся на краю холста. Для многих пикселей невозможно узнать, находится ли пиксель на краю заливки, когда мы сталкиваемся с ними. Мы сможем узнать это только тогда, когда найдем все заполненные пиксели.

Сначала пройдите заливку

Таким образом, мы должны сохранить массив, содержащий цветовое расстояние для всех заполненных пикселей.

В верхней части функции создайте буфер для хранения цветовых расстояний пикселей.

const distances = new Uint16Array(width*height);

Затем во внутреннем цикле вместе с установкой цвета пикселя установите расстояние совпадения местоположений.

 while ((dist = colorDist(idx)) <= tolerance) {
     //Must not fill color here do in second pass p32[idx] = newColor;
     distances[idx] = (dist / tolerance) * 255 | 0x8000; 

Чтобы отслеживать, какие пиксели заполнены, мы устанавливаем верхний бит значения расстояния. Это означает, что расстояния будут иметь ненулевое значение для всех пикселей, которые нужно заполнить, и ноль для пикселей, которые нужно игнорировать. Это делается с помощью | 0x8000

Основная часть заливки не сделана. Мы позволяем заливке делать свое дело, прежде чем приступим к следующему проходу.

Обнаружение и смешивание края второго прохода

После выхода из внешнего цикла мы перебираем каждый пиксель по одному. Проверьте, нужно ли его заполнять.

Если он нуждается в заливке, мы извлекаем цветовое расстояние. Если ноль, установите этот цвет пикселей в массиве p32. Если расстояние не равно нулю, мы проверяем 4 пикселя вокруг него. Если какой-либо из 4 соседних пикселей помечен как не заполнять distances[idx] === 0 и этот пиксель не выходит за границы холста, мы знаем, что это край и его необходимо смешать.

// declare at top of function
var blend, dist, rr, gg, bb, aa;

// need fill color's channels for quickest possible access.
const fr = curColor.r | 0;
const fg = curColor.g | 0;
const fb = curColor.b | 0;
const fa = curColor.a | 0;


// after main fill loop.
idx = 0;
const rightEdge = width - 1, bottomEdge = height - 1; 
while (idx < width * height){
    dist = distances[idx];
    if (dist !== 0) {
        if (dist === 0x8000) {
            p32[idx] = newColor;
        } else {
            blend = false;
            const x = idx % width;
            const y = idx / width | 0;
            if (x > 0 && distances[idx - 1] === 0) { blend = true }
            else if (x < rightEdge && distances[idx + 1] === 0) { blend = true }
            else if (y > 0 && distances[idx - width] === 0) { blend = true }
            else if (y < bottomEdge && distances[idx + width] === 0) { blend = true }

            if (blend) { // pixels is at fill edge an needs to blend
                dist &= 0xFF;             // remove fill bit
                dist = dist / 255;        // normalize to range 0-1
                const invDist = 1 - dist; // invert distance

                // get index in byte array
                const idx1 = idx << 2;    // same as idx * 4 
                
                // simple blend function (not the same as used by 2D API)
                data[idx1]     = data[idx1    ] * dist + fr * invDist;
                data[idx1 + 1] = data[idx1 + 1] * dist + fg * invDist;
                data[idx1 + 2] = data[idx1 + 2] * dist + fb * invDist;
                data[idx1 + 3] = data[idx1 + 3] * dist + fa * invDist;

            } else { 
                p32[idx] = newColor;
            }
       }
    }
    idx++;
}

А теперь просто поместите новый массив пикселей на холст.

Пример

Этот пример представляет собой простую оболочку для измененной версии вашего кода. Он нужен для того, чтобы убедиться, что я не сделал никаких алгоритмических ошибок, и чтобы подчеркнуть качество или отсутствие качества при использовании этого метода.

  • Нажмите первую кнопку, чтобы добавить случайный круг.
  • Используйте ползунок, чтобы установить допуск от 0 до 255.
  • Щелкните Очистить, чтобы очистить холст.
  • Щелкните холст, чтобы залить случайным цветом в позиции мыши.

Холст был увеличен на 2, чтобы артефакты были более заметны.

Функция floodFill заменяет ваш paintAt, она слишком велика и должна быть разбита на две части: одну для прохода заливки, а другую - для обнаружения и смешивания краев.

const ctx = canvas.getContext("2d");
var circle = true;
test();
canvas.addEventListener("click", e => {circle = false; test(e)});
toggleFill.addEventListener("click",e => {circle = true; test(e)});
clear.addEventListener("click",()=>ctx.clearRect(0,0,500,500));
function randomCircle() {
    ctx.beginPath();
    ctx.strokeStyle = "black";
    ctx.lineWidth = 4;
    const x = Math.random() * 100 | 0;
    const y = Math.random() * 100 | 0;
    ctx.arc(x, y, Math.random() * 25 + 25, 0 , Math.PI * 2);
    ctx.stroke();
    return {x,y};
}

function test(e) {
    if (circle) {
        toggleFill.textContent = "Click canvas to fill";
        randomCircle();
    } else {
        toggleFill.textContent = "Click button add random circle";
        const col = {
            r: Math.random() * 255 | 0,
            g: Math.random() * 255 | 0,
            b: Math.random() * 255 | 0,
            a: Math.random() * 255 | 0,
        };
        floodFill(ctx, (event.offsetX - 1) / 2 | 0, (event.offsetY -1) / 2| 0, col, tolerance.value);
    }
}

// Original function from SO question https://stackoverflow.com/q/65359146/3877726
function floodFill(ctx, startX, startY, curColor, tolerance = 0) {
    var idx, blend, dist, rr, gg, bb, aa, spanLeft = true, spanRight = true, leftEdge = false, rightEdge = false;
    const width = ctx.canvas.width,  height = ctx.canvas.height, pixels = width*height;
    const imageData = ctx.getImageData(0, 0, width, height);
    const data = imageData.data;
    const p32 = new Uint32Array(data.buffer);  
    const stack = [startX + (startY * width)]; 
    const targetColor = p32[stack[0]];
    const fr = curColor.r | 0;
    const fg = curColor.g | 0;
    const fb = curColor.b | 0;
    const fa = curColor.a | 0;  
    const newColor = (fa << 24) + (fb << 16) + (fg << 8) + fr;     
    if (targetColor === newColor || targetColor === undefined) { return } 

    idx = stack[0] << 2; 
    const rightE = width - 1, bottomE = height - 1; 
    const distances = new Uint16Array(width*height);   
    tolerance = (tolerance * tolerance * 4) ** 0.5; 
  
    const r = data[idx] ;
    const g = data[idx + 1] ;
    const b = data[idx + 2];
    const a = data[idx + 3]
    function colorDist(idx) {  
        if (distances[idx]) { return Infinity }
        idx <<= 2;
        const R = r - data[idx];
        const G = g - data[idx + 1];
        const B = b - data[idx + 2];
        const A = a - data[idx + 3];      
        return (R * R + B * B + G * G + A * A) ** 0.5;
    }

    while (stack.length) {  
        idx = stack.pop();
        while (idx >= width && colorDist(idx - width) <= tolerance) { idx -= width }; // move to top edge
        spanLeft = spanRight = false;   // not going left right yet 
        leftEdge = (idx % width) === 0;          
        rightEdge = ((idx + 1) % width) === 0;
        while ((dist = colorDist(idx)) <= tolerance) {
            distances[idx] = (dist / tolerance) * 255 | 0x8000; 
            if (!leftEdge) {
                if (colorDist(idx - 1) <= tolerance) { 
                    if (!spanLeft) {        
                        stack.push(idx - 1); 
                        spanLeft = true;   
                    } else if (spanLeft) { 
                        spanLeft = false; 
                    }
                }
            }
            if (!rightEdge) {
                if (colorDist(idx + 1) <= tolerance) {
                    if (!spanRight) {
                        stack.push(idx + 1); 
                        spanRight = true;
                    }else if (spanRight) { 
                        spanRight = false; 
                    }
                }
            }
            idx += width;
        }
    } 
    idx = 0;
    while (idx < pixels) {
        dist = distances[idx];
        if (dist !== 0) {
            if (dist === 0x8000) {
                p32[idx] = newColor;
            } else {
                blend = false;
                const x = idx % width;
                const y = idx / width | 0;
                if (x > 0 && distances[idx - 1] === 0) { blend = true }
                else if (x < rightE && distances[idx + 1] === 0) { blend = true }
                else if (y > 0 && distances[idx - width] === 0) { blend = true }
                else if (y < bottomE && distances[idx + width] === 0) { blend = true }
                if (blend) {
                    dist &= 0xFF;             
                    dist = dist / 255;        
                    const invDist = 1 - dist; 
                    const idx1 = idx << 2;    
                    data[idx1]     = data[idx1    ] * dist + fr * invDist;
                    data[idx1 + 1] = data[idx1 + 1] * dist + fg * invDist;
                    data[idx1 + 2] = data[idx1 + 2] * dist + fb * invDist;
                    data[idx1 + 3] = data[idx1 + 3] * dist + fa * invDist;
                } else { 
                    p32[idx] = newColor;
                }
            }
        }
        idx++;
    }

    ctx.putImageData(imageData,0, 0); 
}
canvas {
  width: 200px;
  height: 200px;  
  border: 1px solid black;
}
<label for="tolerance">Tolerance</label>
<input id="tolerance" type="range" min="0" max="255" value="32" step="1"></input>
<button id ="toggleFill" >Click add random circle</button>
<button id ="clear" >Clear</button><br>
<canvas id="canvas" width="100" height="100"></canvas>

person Blindman67    schedule 19.12.2020
comment
Следуя вашему предложению, он работает, но по-прежнему не работает, как вы можете видеть в моем обновленном разделе. Любые дополнительные предложения по решению проблемы допуска или других методов заливки будут оценены. Хотя ссылка на решение длиной более 1000 строк мне подходит. - person Wisarut Bholsithi; 28.12.2020
comment
@WisarutBholsithi Нет ссылки - person Blindman67; 28.12.2020