Canvas — заливка оставляет белые пиксели по краям

Я создаю приложение для рисования. У меня все получилось. Когда я рисую изображение темным цветом, по краям появляются белые пиксели. Я пытался изменить значение r + g + b, а также альфа, но бесполезно. Может кто-нибудь мне помочь? Вы можете проверить действующий сайт здесь. Каждому, кто поможет мне, я дам ему/ей 50 наград. Спасибо. Это мой код.

<script type="text/javascript">
    var initial = screen.width - 50;

    if (initial < 1000) {
        initial = 1000;
    }

    var firsttime = null;

    var colorYellow = {
        r: 255,
        g: 207,
        b: 51
    };

    var context;
    var canvasWidth = initial;
    var canvasHeight = initial;
    var myColor = colorYellow;
    var curColor = myColor;
    var outlineImage = new Image();
    var backgroundImage = new Image();
    var drawingAreaX = 0;
    var drawingAreaY = 0;
    var drawingAreaWidth = initial;
    var drawingAreaHeight = initial;
    var colorLayerData;
    var outlineLayerData;
    var totalLoadResources = 2;
    var curLoadResNum = 0;
    var undoarr = new Array();
    var redoarr = new Array();

    function history(command) { // handles undo/redo button events.
        var data;
        if (command === "redo") {
            data = historyManager.redo(); // get data for redo
        } else
        if (command === "undo") {
            data = historyManager.undo(); // get data for undo
        }
        if (data !== undefined) { // if data has been found
            setColorLayer(data); // set the data
        }
    }

    // sets colour layer and creates copy into colorLayerData
    function setColorLayer(data) {
        context.putImageData(data, 0, 0);
        colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
        context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
        context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
    }

    // Clears the canvas.
    function clearCanvas() {
        context.clearRect(0, 0, context.canvas.width, context.canvas.height);
    }



    // Draw the elements on the canvas
    function redraw() {
        uc = 0;
        rc = 0;
        var locX,
            locY;

        // Make sure required resources are loaded before redrawing
        if (curLoadResNum < totalLoadResources) {
            return; // To check if images are loaded successfully or not.
        }

        clearCanvas();
        // Draw the current state of the color layer to the canvas
        context.putImageData(colorLayerData, 0, 0);

        historyManager.push(context.getImageData(0, 0, canvasWidth, canvasHeight));
        redoarr = new Array();
        // Draw the background
        context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);

        // Draw the outline image on top of everything. We could move this to a separate 
        //   canvas so we did not have to redraw this everyime.
        context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
    };

    function matchOutlineColor(r, g, b, a) {

        return (r + g + b < 50 && a >= 50);
    };

    function matchStartColor(pixelPos, startR, startG, startB) {

        var r = outlineLayerData.data[pixelPos],
            g = outlineLayerData.data[pixelPos + 1],
            b = outlineLayerData.data[pixelPos + 2],
            a = outlineLayerData.data[pixelPos + 3];

        // If current pixel of the outline image is black
        if (matchOutlineColor(r, g, b, a)) {
            return false;
        }

        r = colorLayerData.data[pixelPos];
        g = colorLayerData.data[pixelPos + 1];
        b = colorLayerData.data[pixelPos + 2];

        // If the current pixel matches the clicked color
        if (r === startR && g === startG && b === startB) {
            return true;
        }

        // If current pixel matches the new color
        if (r === curColor.r && g === curColor.g && b === curColor.b) {
            return false;
        }

        return true;
    };

    function colorPixel(pixelPos, r, g, b, a) {
        colorLayerData.data[pixelPos] = r;
        colorLayerData.data[pixelPos + 1] = g;
        colorLayerData.data[pixelPos + 2] = b;
        colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255;
    };

    function floodFill(startX, startY, startR, startG, startB) {

        document.getElementById('color-lib-1').style.display = "none";
        document.getElementById('color-lib-2').style.display = "none";
        document.getElementById('color-lib-3').style.display = "none";
        document.getElementById('color-lib-4').style.display = "none";
        document.getElementById('color-lib-5').style.display = "none";

        change = false;

        var newPos,
            x,
            y,
            pixelPos,
            reachLeft,
            reachRight,
            drawingBoundLeft = drawingAreaX,
            drawingBoundTop = drawingAreaY,
            drawingBoundRight = drawingAreaX + drawingAreaWidth - 1,
            drawingBoundBottom = drawingAreaY + drawingAreaHeight - 1,
            pixelStack = [
                [startX, startY]
            ];

        while (pixelStack.length) {

            newPos = pixelStack.pop();
            x = newPos[0];
            y = newPos[1];

            // Get current pixel position
            pixelPos = (y * canvasWidth + x) * 4;

            // Go up as long as the color matches and are inside the canvas
            while (y >= drawingBoundTop && matchStartColor(pixelPos, startR, startG, startB)) {
                y -= 1;
                pixelPos -= canvasWidth * 4;
            }

            pixelPos += canvasWidth * 4;
            y += 1;
            reachLeft = false;
            reachRight = false;

            // Go down as long as the color matches and in inside the canvas
            while (y <= drawingBoundBottom && matchStartColor(pixelPos, startR, startG, startB)) {
                y += 1;

                colorPixel(pixelPos, curColor.r, curColor.g, curColor.b);

                if (x > drawingBoundLeft) {
                    if (matchStartColor(pixelPos - 4, startR, startG, startB)) {
                        if (!reachLeft) {
                            // Add pixel to stack
                            pixelStack.push([x - 1, y]);
                            reachLeft = true;
                        }

                    } else if (reachLeft) {
                        reachLeft = false;
                    }
                }

                if (x < drawingBoundRight) {
                    if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
                        if (!reachRight) {
                            // Add pixel to stack
                            pixelStack.push([x + 1, y]);
                            reachRight = true;
                        }
                    } else if (reachRight) {
                        reachRight = false;
                    }
                }

                pixelPos += canvasWidth * 4;
            }
        }
    };

    // Start painting with paint bucket tool starting from pixel specified by startX and startY
    function paintAt(startX, startY) {

        var pixelPos = (startY * canvasWidth + startX) * 4,
            r = colorLayerData.data[pixelPos],
            g = colorLayerData.data[pixelPos + 1],
            b = colorLayerData.data[pixelPos + 2],
            a = colorLayerData.data[pixelPos + 3];

        if (r === curColor.r && g === curColor.g && b === curColor.b) {
            // Return because trying to fill with the same color
            return;
        }

        if (matchOutlineColor(r, g, b, a)) {
            // Return because clicked outline
            return;
        }

        floodFill(startX, startY, r, g, b);

        redraw();
    };

    // Add mouse event listeners to the canvas
    function createMouseEvents() {

        $('#canvas').mousedown(function(e) {

            // Mouse down location
            var mouseX = e.pageX - this.offsetLeft,
                mouseY = e.pageY - this.offsetTop;

            // assuming that the mouseX and mouseY are the mouse coords.
            if (this.style.width) { // make sure there is a width in the style 
                // (assumes if width is there then height will be too
                var w = Number(this.style.width.replace("px", "")); // warning this will not work if size is not in pixels
                var h = Number(this.style.height.replace("px", "")); // convert the height to a number
                var pixelW = this.width; // get  the canvas resolution
                var pixelH = this.height;
                mouseX = Math.floor((mouseX / w) * pixelW); // convert the mouse coords to pixel coords
                mouseY = Math.floor((mouseY / h) * pixelH);
            }

            if ((mouseY > drawingAreaY && mouseY < drawingAreaY + drawingAreaHeight) && (mouseX <= drawingAreaX + drawingAreaWidth)) {
                paintAt(mouseX, mouseY);
            }
        });
    };

    resourceLoaded = function() {

        curLoadResNum += 1;
        //if (curLoadResNum === totalLoadResources) {
        createMouseEvents();
        redraw();
        //}
    };

    var historyManager = (function() { // Anon for private (closure) scope
        var uBuffer = []; // this is undo buff
        var rBuffer = []; // this is redo buff
        var currentState = undefined; // this holds the current history state
        var undoElement = undefined;
        var redoElement = undefined;
        var manager = {
            UI: { // UI interface just for disable and enabling redo undo buttons
                assignUndoButton: function(element) {
                    undoElement = element;
                    this.update();
                },
                assignRedoButton: function(element) {
                    redoElement = element;
                    this.update();
                },
                update: function() {
                    if (redoElement !== undefined) {
                        redoElement.disabled = (rBuffer.length === 0);
                    }
                    if (undoElement !== undefined) {
                        undoElement.disabled = (uBuffer.length === 0);
                    }
                }
            },
            reset: function() {
                uBuffer.length = 0;
                rBuffer.length = 0;
                currentState = undefined;
                this.UI.update();
            },
            push: function(data) {
                if (currentState !== undefined) {
                    var same = true
                    for (i = 0; i < data.data.length; i++) {
                        if (data.data[i] !== currentState.data[i]) {
                            same = false;
                            break;
                        }
                    }
                    if (same) {
                        return;
                    }
                }
                if (currentState !== undefined) {
                    uBuffer.push(currentState);
                }
                currentState = data;
                rBuffer.length = 0;
                this.UI.update();
            },
            undo: function() {
                if (uBuffer.length > 0) {
                    if (currentState !== undefined) {
                        rBuffer.push(currentState);
                    }
                    currentState = uBuffer.pop();
                }
                this.UI.update();
                return currentState; // return data or unfefined
            },
            redo: function() {
                if (rBuffer.length > 0) {
                    if (currentState !== undefined) {
                        uBuffer.push(currentState);
                    }
                    currentState = rBuffer.pop();
                }
                this.UI.update();
                return currentState;
            },
        }
        return manager;
    })();

    function start() {

        var canvas = document.createElement('canvas');
        canvas.setAttribute('width', canvasWidth);
        canvas.setAttribute('height', canvasHeight);
        canvas.setAttribute('id', 'canvas');
        document.getElementById('canvasDiv').appendChild(canvas);

        if (typeof G_vmlCanvasManager !== "undefined") {
            canvas = G_vmlCanvasManager.initElement(canvas);
        }
        context = canvas.getContext("2d");
        backgroundImage.onload = resourceLoaded();
        backgroundImage.src = "images/t.png";

        outlineImage.onload = function() {
            context.drawImage(outlineImage, drawingAreaX, drawingAreaY, drawingAreaWidth, drawingAreaHeight);

            try {
                outlineLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
            } catch (ex) {
                window.alert("Application cannot be run locally. Please run on a server.");
                return;
            }
            clearCanvas();
            colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
            resourceLoaded();
        };
        outlineImage.src = "images/products/<?php echo $product['product_image']; ?>";
    };

    if (historyManager !== undefined) {
        // only for visual feedback and not required for the history manager to function.
        historyManager.UI.assignUndoButton(document.querySelector("#undo-button"));
        historyManager.UI.assignRedoButton(document.querySelector("#redo-button"));
    }

    getColor = function() {

    };
</script>

person Ali Zia    schedule 07.06.2016    source источник
comment
Это не имеет ничего общего с jQuery.   -  person Sebastian G. Marinescu    schedule 17.06.2016


Ответы (4)


Предлагаю два изменения:

  1. Смешайте пиксели и цвет заливки вместо жесткого переопределения
  2. Ограничить область заливки на основе изменений градиента интенсивности вместо простого порога

Заливка в горизонтальном и вертикальном направлении до тех пор, пока знак градиента интенсивности не изменится с + на - ИЛИ - на +, позволяет нам заполнить всю область, включая «половину» черной границы. Проверяя градиент, мы просто следим за тем, чтобы не выйти за пределы минимума интенсивности и, таким образом, избежать заполнения соседней области.

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';
<canvas id="canvas"></canvas>

Вы можете улучшить производительность, отслеживая только один цветовой канал вместо суммирования всех трех для вашей функции getIntensity.

Кроме того, вам придется иметь дело с «очисткой» уже заполненной области, прежде чем снова заполнить ее другим цветом из-за режима наложения.

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

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

person le_m    schedule 21.06.2016
comment
Я трачу много времени на написание этого ответа; написание вашего кода было бы слишком много. Однако, если у вас есть конкретные вопросы о том, как действовать дальше, задавайте их. - person le_m; 21.06.2016

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

 if ((r <= curColor.r + 10 && r >= curColor.r - 10)  && (r >= curColor.g - 10 && r <= curColor.g + 10) && (b >= curColor.b - 10 && b <= curColor.b + 10)) {
        return false;
    }

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

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

person overburn    schedule 15.06.2016
comment
где изменить эту строку? - person Ali Zia; 15.06.2016
comment
Под // Если текущий пиксель соответствует выбранному цвету. Должно работать немного лучше. - person overburn; 15.06.2016
comment
Я сделал это, но это то же самое. Никакого эффекта :( Все еще появляются белые точки. - person Ali Zia; 15.06.2016
comment
Даже если вы настроите параметр ? - person overburn; 15.06.2016
comment
Да, я настроил параметр до 30. - person Ali Zia; 15.06.2016
comment
Хм, можно было бы сделать скрипт с изображением и все такое, было бы проще помочь - person overburn; 15.06.2016
comment
Я не знаю, как создать скрипку, но могу отправить вам копию моего локального проекта. Что ты говоришь? - person Ali Zia; 15.06.2016
comment
Конечно, просто опубликуйте это где-нибудь - person overburn; 15.06.2016
comment
Вам комфортно в PHP? - person Ali Zia; 15.06.2016
comment
Я загрузил файл product.php сюда speedy.sh/WtEBP/product.rar - person Ali Zia; 15.06.2016
comment
Он неполный, и у меня действительно не так много времени, чтобы исправить его, чтобы он работал. Если вам нужна помощь в этом, создайте скрипт здесь с вашим кодом jsfiddle.net - person overburn; 15.06.2016
comment
Хорошо, позвольте мне отправить вам мой полный проект вместе с базой данных. Хорошо? - person Ali Zia; 15.06.2016
comment
@overburn Вы случайно написали (r >= curColor.g - 10 && r <= curColor.g + 10) вместо (g >= curColor.g - 10 && g <= curColor.g + 10). Не уверен, насколько это изменит ситуацию, но это может помочь. - person Oliver; 15.06.2016
comment
О да, я только что проснулся, я думаю, что мой мозг загружается. - person overburn; 16.06.2016

Честно говоря, это не столько вина вашей программы для рисования, сколько вина рисуемых изображений. «Белые» пиксели на самом деле бледно-серые, побочный эффект инструмента «Кисть», используемого для рисования линий на изображениях. Есть два способа обойти это:

  1. Чтобы удалить все бледно-серые пиксели с изображения и сделать их белыми. Использование инструмента выбора цвета и карандаша исправит это. Единственным побочным эффектом является то, что линии в некоторых точках могут показаться немного рывками.

  2. Чтобы дать некоторую снисходительность, когда дело доходит до того, какие цвета закрашиваются. Таким образом, вместо того, чтобы просто заменить белый цвет, замените также и бледно-серый. Любой цвет вплоть до #CCC (или rgb(204, 204, 204)) должен быть закрашен.

Код для второго варианта выглядит следующим образом:

if(r >= 204 && g >= 204 && b >= 204 && r === g && g === b){
    return true;
}

Это просто проверяет, является ли пиксель светлым цветом в оттенках серого, и возвращает true, если это так. Используйте это вместо функции проверки цвета контура.

person Oliver    schedule 15.06.2016

Ваша проверка контура просто слишком строгая, помечая светло-серые пиксели как те, которые вы не можете раскрасить. Я просто подправил ваши пороговые значения:

function matchOutlineColor (r,g,b,a,e) {
  return a >= 150;
}

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

До (увеличено на 200%) введите здесь описание изображения

После (увеличено на 200%) введите здесь описание изображения

person Rob Louie    schedule 21.06.2016