with create app and javascript html5 canvas flood-fill

javascript - create - html5 canvas drawing app html



El relleno de inundaciĆ³n de lienzo no se llena hasta el borde (1)

Use relleno de inundación para crear una máscara

Me sucedió que hice una inundación el otro día que soluciona el problema de los bordes antialias.

En lugar de pintar en el lienzo directamente, pinto una matriz de bytes que luego se usa para crear una máscara. La máscara permite establecer los valores alfa.

El relleno puede tener una tolerance y un toleranceFade que controlan cómo se maneja con los colores que se aproximan al valor de tolerancia.

Cuando la diferencia del píxel entre el color de inicio y la tolerancia es mayor que (tolerance - toleranceFade) establezco el alfa para ese píxel en 255 - ((differance - (tolerance - toleranceFade)) / toleranceFade) * 255 que crea una agradable mezcla suave en el bordes de líneas Aunque no funciona en todas las situaciones en situaciones de alto contraste, es una solución efectiva.

El siguiente ejemplo muestra los resultados de con y sin la toleranceFade . El azul está sin la toleranceFade , el rojo está con la tolerance establecida en 190 y la toleranceFade de 90.

Tendrá que jugar con la configuración para obtener los mejores resultados para sus necesidades.

function showExample(){ var canvas = document.createElement("canvas"); canvas.width = 200; canvas.height = 200; var ctx = canvas.getContext("2d"); document.body.appendChild(canvas); ctx.fillStyle = "white" ctx.fillRect(0,0,canvas.width,canvas.height) ctx.lineWidth = 4; ctx.strokeStyle = "black" ctx.beginPath(); ctx.arc(100,100,90,0,Math.PI * 2); ctx.arc(120,100,60,0,Math.PI * 2); ctx.stroke(); ctx.fillStyle = "blue"; floodFill.fill(100,100,1,ctx) ctx.fillStyle = "red"; floodFill.fill(40,100,190,ctx,null,null,90) } // FloodFill2D from https://github.com/blindman67/FloodFill2D var floodFill = (function(){ "use strict"; const extent = { top : 0, left : 0, bottom : 0, right : 0, } var keepMask = false; // if true then a mask of the filled area is returned as a canvas image var extentOnly = false; // if true then the extent of the fill is returned var copyPixels = false; // if true then creating a copy of filled pixels var cutPixels = false; // if true and copyPixels true then filled pixels are removed var useBoundingColor = false; // Set the colour to fill up to. Will not fill over this colour var useCompareColor = false; // Rather than get the pixel at posX,posY use the compareColours var red, green, blue, alpha; // compare colours if var canvas,ctx; function floodFill (posX, posY, tolerance, context2D, diagonal, area, toleranceFade) { var w, h, painted, x, y, ind, sr, sg, sb, sa,imgData, data, data32, RGBA32, stack, stackPos, lookLeft, lookRight, i, colImgDat, differance, checkColour; toleranceFade = toleranceFade !== undefined && toleranceFade !== null ? toleranceFade : 0; diagonal = diagonal !== undefined && diagonal !== null ? diagonal : false; area = area !== undefined && area !== null ? area : {}; area.x = area.x !== undefined ? area.x : 0; area.y = area.y !== undefined ? area.y : 0; area.w = area.w !== undefined ? area.w : context2D.canvas.width - area.x; area.h = area.h !== undefined ? area.h : context2D.canvas.height - area.y; // vet area is on the canvas. if(area.x < 0){ area.w = area.x + area.w; area.x = 0; } if(area.y < 0){ area.h = area.y + area.h; area.y = 0; } if(area.x >= context2D.canvas.width || area.y >= context2D.canvas.height){ return false; } if(area.x + area.w > context2D.canvas.width){ area.w = context2D.canvas.width - area.x; } if(area.y + area.h > context2D.canvas.height){ area.h = context2D.canvas.height - area.y; } if(area.w <= 0 || area.h <= 0){ return false; } w = area.w; // width and height h = area.h; x = posX - area.x; y = posY - area.y; if(extentOnly){ extent.left = x; // set up extent extent.right = x; extent.top = y; extent.bottom = y; } if(x < 0 || y < 0 || x >= w || y >= h){ return false; // fill start outside area. Don''t do anything } if(tolerance === 255 && toleranceFade === 0 && ! keepMask){ // fill all if(extentOnly){ extent.left = area.x; // set up extent extent.right = area.x + w; extent.top = area.y; extent.bottom = area.y + h; } context2D.fillRect(area.x,area.y,w,h); return true; } if(toleranceFade > 0){ // add one if on to get correct number of steps toleranceFade += 1; } imgData = context2D.getImageData(area.x,area.y,area.w,area.h); data = imgData.data; // image data to fill; data32 = new Uint32Array(data.buffer); painted = new Uint8ClampedArray(w*h); // byte array to mark painted area; function checkColourAll(ind){ if( ind < 0 || painted[ind] > 0){ // test bounds return false; } var ind4 = ind << 2; // get index of pixel if((differance = Math.max( // get the max channel difference; Math.abs(sr - data[ind4++]), Math.abs(sg - data[ind4++]), Math.abs(sb - data[ind4++]), Math.abs(sa - data[ind4++]) )) > tolerance){ return false; } return true } // check to bounding colour function checkColourBound(ind){ if( ind < 0 || painted[ind] > 0){ // test bounds return false; } var ind4 = ind << 2; // get index of pixel differance = 0; if(sr === data[ind4] && sg === data[ind4 + 1] && sb === data[ind4 + 2] && sa === data[ind4 + 3]){ return false } return true } // this function checks the colour of only selected channels function checkColourLimited(ind){ // check only colour channels that are not null var dr,dg,db,da; if( ind < 0 || painted[ind] > 0){ // test bounds return false; } var ind4 = ind << 2; // get index of pixel dr = dg = db = da = 0; if(sr !== null && (dr = Math.abs(sr - data[ind4])) > tolerance){ return false; } if(sg !== null && (dg = Math.abs(sg - data[ind4 + 1])) > tolerance){ return false; } if(sb !== null && (db = Math.abs(sb - data[ind4 + 2])) > tolerance){ return false; } if(sa !== null && (da = Math.abs(sa - data[ind4 + 3])) > tolerance){ return false; } diferance = Math.max(dr, dg, db, da); return true } // set which function to check colour with checkColour = checkColourAll; if(useBoundingColor){ sr = red; sg = green; sb = blue; if(alpha === null){ ind = (y * w + x) << 2; // get the starting pixel index sa = data[ind + 3]; }else{ sa = alpha; } checkColour = checkColourBound; useBoundingColor = false; }else if(useCompareColor){ sr = red; sg = green; sb = blue; sa = alpha; if(red === null || blue === null || green === null || alpha === null){ checkColour = checkColourLimited; } useCompareColor = false; }else{ ind = (y * w + x) << 2; // get the starting pixel index sr = data[ind]; // get the start colour that we will use tolerance against. sg = data[ind + 1]; sb = data[ind + 2]; sa = data[ind + 3]; } stack = []; // paint stack to find new pixels to paint lookLeft = false; // test directions lookRight = false; stackPos = 0; stack[stackPos++] = x; stack[stackPos++] = y; while (stackPos > 0) { // do while pixels on the stack y = stack[--stackPos]; // get the pixel y x = stack[--stackPos]; // get the pixel x ind = x + y * w; while (checkColour(ind - w)) { // find the top most pixel within tollerance; y -= 1; ind -= w; } //checkTop left and right if allowing diagonal painting if(diagonal && y > 0){ if(x > 0 && !checkColour(ind - 1) && checkColour(ind - w - 1)){ stack[stackPos++] = x - 1; stack[stackPos++] = y - 1; } if(x < w - 1 && !checkColour(ind + 1) && checkColour(ind - w + 1)){ stack[stackPos++] = x + 1; stack[stackPos++] = y - 1; } } lookLeft = false; // set look directions lookRight = false; // only look is a pixel left or right was blocked while (checkColour(ind) && y < h) { // move down till no more room if(toleranceFade > 0 && differance >= tolerance-toleranceFade){ painted[ind] = 255 - (((differance - (tolerance - toleranceFade)) / toleranceFade) * 255); painted[ind] = painted[ind] === 0 ? 1 : painted[ind]; // min value must be 1 }else{ painted[ind] = 255; } if(extentOnly){ extent.left = x < extent.left ? x : extent.left; // Faster than using Math.min extent.right = x > extent.right ? x : extent.right; // Faster than using Math.min extent.top = y < extent.top ? y : extent.top; // Faster than using Math.max extent.bottom = y > extent.bottom ? y : extent.bottom; // Faster than using Math.max } if (checkColour(ind - 1) && x > 0) { // check left is blocked if (!lookLeft) { stack[stackPos++] = x - 1; stack[stackPos++] = y; lookLeft = true; } } else if (lookLeft) { lookLeft = false; } if (checkColour(ind + 1) && x < w -1) { // check right is blocked if (!lookRight) { stack[stackPos++] = x + 1; stack[stackPos++] = y; lookRight = true; } } else if (lookRight) { lookRight = false; } y += 1; // move down one pixel ind += w; } if(diagonal && y < h){ // check for diagonal areas and push them to be painted if(checkColour(ind - 1) && !lookLeft && x > 0){ stack[stackPos++] = x - 1; stack[stackPos++] = y; } if(checkColour(ind + 1) && !lookRight && x < w - 1){ stack[stackPos++] = x + 1; stack[stackPos++] = y; } } } if(extentOnly){ extent.top += area.y; extent.bottom += area.y; extent.left += area.x; extent.right += area.x; return true; } canvas = document.createElement("canvas"); canvas.width = w; canvas.height = h; ctx = canvas.getContext("2d"); ctx.fillStyle = context2D.fillStyle; ctx.fillRect(0, 0, w, h); colImgDat = ctx.getImageData(0, 0, w, h); if(copyPixels){ i = 0; ind = 0; if(cutPixels){ while(i < painted.length){ if(painted[i] > 0){ colImgDat.data[ind] = data[ind]; colImgDat.data[ind + 1] = data[ind + 1]; colImgDat.data[ind + 2] = data[ind + 2]; colImgDat.data[ind + 3] = data[ind + 3] * (painted[i] / 255); data[ind + 3] = 255 - painted[i]; }else{ colImgDat.data[ind + 3] = 0; } i ++; ind += 4; } context2D.putImageData(imgData, area.x, area.y); }else{ while(i < painted.length){ if(painted[i] > 0){ colImgDat.data[ind] = data[ind]; colImgDat.data[ind + 1] = data[ind + 1]; colImgDat.data[ind + 2] = data[ind + 2]; colImgDat.data[ind + 3] = data[ind + 3] * (painted[i] / 255); }else{ colImgDat.data[ind + 3] = 0; } i ++; ind += 4; } } ctx.putImageData(colImgDat,0,0); return true; }else{ i = 0; ind = 3; while(i < painted.length){ colImgDat.data[ind] = painted[i]; i ++; ind += 4; } ctx.putImageData(colImgDat,0,0); } if(! keepMask){ context2D.drawImage(canvas,area.x,area.y,w,h); } return true; } return { fill : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade); ctx = undefined; canvas = undefined; }, getMask : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ keepMask = true; floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade); ctx = undefined; keepMask = false; return canvas; }, getExtent : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ extentOnly = true; if(floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade)){ extentOnly = false; return { top : extent.top, left : extent.left, right : extent.right, bottom : extent.bottom, width : extent.right - extent.left, height : extent.bottom - extent.top, } } extentOnly = false; return null; }, cut : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ cutPixels = true; copyPixels = true; floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade); cutPixels = false; copyPixels = false; ctx = undefined; return canvas; }, copy : function(posX, posY, tolerance, context2D, diagonal, area, toleranceFade){ cutPixels = false; copyPixels = true; floodFill(posX, posY, tolerance, context2D, diagonal, area, toleranceFade); copyPixels = false; ctx = undefined; return canvas; }, setCompareValues : function(R,G,B,A){ if(R === null && G === null && B === null && A === null){ return; } red = R; green = G; blue = B; alpha = A; useBoundingColor = false; useCompareColor = true; }, setBoundingColor : function(R,G,B,A){ red = R; green = G; blue = B; alpha = A; useCompareColor = false; useBoundingColor = true; } } }()); showExample();

Red floodFill.fill(40,100,190,ctx,null,null,90) tolerance 190, tolerance fade 90<br>Blue floodFill.fill(100,100,1,ctx) tolerance 1.<br>

Para obtener más información, consulte el archivo Léame en Github FloodFill2D.

Estoy usando un algoritmo de relleno de inundación para completar los círculos dibujados en el lienzo. El problema que estoy teniendo es que el algoritmo no se está llenando hasta el borde del círculo.

Aquí está el algoritmo basado en esta publicación de blog :

function paintLocation(startX, startY, r, g, b) { var colorLayer = context1.getImageData(0, 0, canvasWidth, canvasHeight); pixelPos = (startY * canvasWidth + startX) * 4; startR = colorLayer.data[pixelPos]; startG = colorLayer.data[pixelPos + 1]; startB = colorLayer.data[pixelPos + 2]; var pixelStack = [ [startX, startY] ]; var drawingBoundTop = 0; while (pixelStack.length) { var newPos, x, y, pixelPos, reachLeft, reachRight; newPos = pixelStack.pop(); x = newPos[0]; y = newPos[1]; pixelPos = (y * canvasWidth + x) * 4; while (y-- >= drawingBoundTop && matchStartColor(colorLayer, pixelPos, startR, startG, startB)) { pixelPos -= canvasWidth * 4; } pixelPos += canvasWidth * 4; ++y; reachLeft = false; reachRight = false; while (y++ < canvasHeight - 1 && matchStartColor(colorLayer, pixelPos, startR, startG, startB)) { colorPixel(colorLayer, pixelPos, r, g, b); if (x > 0) { if (matchStartColor(colorLayer, pixelPos - 4, startR, startG, startB)) { if (!reachLeft) { pixelStack.push([x - 1, y]); reachLeft = true; } } else if (reachLeft) { reachLeft = false; } } if (x < canvasWidth - 1) { if (matchStartColor(colorLayer, pixelPos + 4, startR, startG, startB)) { if (!reachRight) { pixelStack.push([x + 1, y]); reachRight = true; } } else if (reachRight) { reachRight = false; } } pixelPos += canvasWidth * 4; } } context1.putImageData(colorLayer, 0, 0); }

Por favor, vea el JSFiddle o la imagen de abajo para ver lo que quiero decir. Hacer clic dentro de cualquier círculo cambiará el color entre amarillo y negro (el problema es mucho más visible con el negro).

He leído que el problema podría tener algo que ver con el antialiasing y he intentado desactivarlo con context1.imageSmoothingEnabled = true; pero no hizo la diferencia.

También intenté cambiar mi función matchStartColour según esta pregunta, pero eso no ayuda.

function matchStartColor(colorLayer, pixelPos, startR, startG, startB) { var r = colorLayer.data[pixelPos]; var g = colorLayer.data[pixelPos + 1]; var b = colorLayer.data[pixelPos + 2]; return (r == startR && g == startG && b == startB); }

Creo que podría tener algo que ver con el hecho de que los círculos no tienen color de relleno y el fondo del lienzo no es blanco, sino que es negro transparente. Intenté cambiar el fondo del lienzo a blanco, pero eso tampoco ayudó.