在 JS 中使用像素操作 (WebImage) 填充未知的非对称多边形
Fill an Unknown Asymmetric Polygon with Pixel Manipulation (WebImage) in JS
最近,我一直在尝试创建代码来用颜色填充任何形状的多边形。我已经能够正确地填充只有一个边框大小的线条的形状,尽管我发现自己无法做更多的事情。问题在于代码不知道何时考虑一行大于它预期的像素的行作为形状的垂直或水平边界。我从左到右遍历形状的每个像素,并通过检查 alpha 值是否为 0 来检查是否有任何像素具有任何形式的颜色。一旦它发现一个像素的 alpha 值不为 0,它就会向前移动一个像素,然后使用 even/odd 技术来确定该点是否在多边形的一部分内(它使一个向右无限线并确定与彩色线的碰撞次数是否为奇数,如果是,则该点在多边形内)。通常,我们将单个单独的像素视为一条线,并且我们将超过一个像素的水平线视为两条线,因为水平线是否经常是边框的一部分。假设以下场景:
这里,红点是我们开始测试的点(像素)。如果我们不认为中间的水平线是两个点(如红线和 x 所示),我们将只有两个交点,因此不会填充像素,尽管我们肯定会这样做想要填充那个像素。然而,如前所述,这会带来另一个不同场景的问题:
在这种情况下,如果我们做将超过一个像素的水平线算作两条单独的线,我们最终不会填充任何边框比粗的区域预期的厚度。供大家参考,处理这个的函数如下:
//imgData is essentially a WebImage object (explained more below) and r, g, and b are the color values for the fill color
function fillWithColor(imgData, r, g, b) {
//Boolean determining whether we should color the given pixel(s) or not
var doColor = false;
//Booleans determining whether the last pixel found in the entire image was colored
var blackLast = false;
//Booleans determining whether the last 1 or 2 pixels found after a given pixel were colored
var foundBlackPrev, foundBlackPrev2 = false;
//The number of colored pixels found
var blackCount = 0;
//Loop through the entire canvas
for(var y = 0; y < imgData.height; y += IMG_SCALE) {
for(var x = 0; x < imgData.width; x += IMG_SCALE) {
//Test if given pixel is colored
if(getAlpha(imgData, x, y) != 0) {
//If the last pixel was black, begin coloring
if(!blackLast) {
blackLast = true;
doColor = true;
}
} else {
//If the current pixel is not colored, but the last one was, find all colored lines to the right
if(blackLast){
for(var i = x; i < imgData.width; i += IMG_SCALE) {
//If the pixel is colored...
if(getAlpha(imgData, i, y) != 0) {
//If no colored pixel was found before, add to the count
if(!foundBlackPrev){
blackCount++;
foundBlackPrev = true;
} else {
//Otherwise, at least 2 colored pixels have been found in a row
foundBlackPrev2 = true;
}
} else {
//If two or more colored pixels were found in a row, add to the count
if(foundBlackPrev2) {
blackCount++;
}
//Reset the booleans
foundBlackPrev2 = foundBlackPrev = false;
}
}
}
//If the count is odd, we start coloring
if(blackCount & 1) {
blackCount = 0;
doColor = true;
} else {
//If the last pixel in the entire image was black, we stop coloring
if(blackLast) {
doColor = false;
}
}
//Reset the boolean
blackLast = false;
//If we are to be coloring the pixel, color it
if(doColor) {
//Color the pixel
for(var j = 0; j < IMG_SCALE; j++) {
for(var k = 0; k < IMG_SCALE; k++) {
//This is the same as calling setRed, setGreen, setBlue and setAlpha functions from the WebImage API all at once (parameters in order are WebImage object equivalent, x position of pixel, y position of pixel, red value, green value, blue value, and alpha value)
setRGB(imgData, x + j, y + k, r, g, b, 255);
}
}
}
}
}
}
//Update the image (essentially the same as removing all elements from the given area and calling add on the image)
clearCanvas();
putImageData(imgData, 0, 0, imgData.width, imgData.height);
//Return the modified data
return imgData;
}
在哪里...
imgData 是给定区域中所有像素的集合(本质上是一个 WebImage 对象)
IMG_SCALE 是图像按比例放大的整数值(这也给了我们像素的比例)。在这个例子中,它等于 4,因为图像被放大到 192x256(从 48x64)。这意味着您在图像中看到的每个“像素”实际上都由 4x4 块相同颜色的像素组成。
所以,我在这里真正要寻找的是一种方法来确定一个给定的彩色像素是水平边界的一部分,还是只是另一个包含垂直边界厚度的部分。此外,如果我总体上对这个问题有错误的方法,我将不胜感激任何关于如何更有效地做到这一点的建议。谢谢。
据我了解,您不能“将超过一个像素的水平线视为两条线”。我认为您不需要像现在这样计算黑色像素,而是计算 1 个或多个像素的组。
我还会通过避免使用“doColor”布尔变量来整理代码。您可以将着色代码移至新函数 color(x,y) 并立即调用它。
const ctx = document.querySelector("canvas").getContext("2d");
//ctx.lineWidth(10);//-as you asked we are setting greater border or line width,BUT "LINEWIDTH" IS NOT WORKING IN INBUILT Whosebug SNIPPET USE IT IN A FILE I THINK Whosebug IS NOT UP-TO-DATE,IN ANY IDE UNCOMENT THIS
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
function getMousePosition(canvas, event) {
let rect = canvas.getBoundingClientRect();
let mx = event.clientX - rect.left;
let my = event.clientY - rect.top;
console.log("Coordinate x: " + mx, "Coordinate y: " + my);
floodFill(ctx, mx, my, [155, 0, 255, 255], 128);
}
let canvasElem = document.querySelector("canvas");
canvasElem.addEventListener("mousedown", function(e) {
getMousePosition(canvasElem, e);
});
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;
return imageData.data.slice(offset, offset + 4);
}
}
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, rangeSq) {
const dr = a[0] - b[0];
const dg = a[1] - b[1];
const db = a[2] - b[2];
const da = a[3] - b[3];
return dr * dr + dg * dg + db * db + da * da < rangeSq;
}
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);
// 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></canvas>
这是基于其他答案
注意:“LINEWIDTH”在内置的 Whosebug 片段中不起作用在我认为 Whosebug 不是最新的文件中使用它,
但在简单的HTML,JS网站
中效果很好
我理解这个问题,我认为如果你在这里改变你的策略,你会做得更好。我们知道以下内容:
- 起点在形状内部
- 应该为形状内的每个像素填充颜色
所以,我们总是可以将当前点的邻居推入队列中进行处理,注意避免对同一个点进行两次处理,这样就遍历了所有有用的像素,并将其纳入着色计划。以下功能未经测试。
function fillColor(pattern, startingPoint, color, boundaryColor) {
let visitQueue = [];
let output = {};
if (startingPoint.x - 1 >= 0) visitQueue.push({startingPoint.x - 1, startingPoint.y});
if (startingPoint.x + 1 < pattern.width) visitQueue.push({startingPoint.x + 1, startingPoint.y});
if (startingPoint.y + 1 < pattern.height) visitQueue.push({startingPoint.x, startingPoint.y + 1});
if (startingPoint.y - 1 >= 0) visitQueue.push({startingPoint.x, startingPoint.y - 1});
let visited = {};
while (visitQueue.length > 0) {
let point = visitQueue[0];
visitQueue.shift();
if ((!visited[point.x]) || (visited[point.x].indexOf(point.y) < 0)) {
if (!visited[point.x]) visited[point.x] = [];
visited[point.x].push(point.y);
if (isBlank(pattern, point)) { //you need to implement isBlank
if (!output[point.x]) output[point.x] = [];
output[point.x].push(point.y);
if (point.x + 1 < pattern.width) visitQueue.push({point.x + 1, point.y});
if (point.x - 1 >= 0) visitQueue.push({point.x - 1, point.y});
if (point.y + 1 < pattern.height) visitQueue.push({point.x, point.y + 1});
if (point.y - 1 >= 0) visitQueue.push({point.x, point.y - 1})
}
}
}
return output;
}
最近,我一直在尝试创建代码来用颜色填充任何形状的多边形。我已经能够正确地填充只有一个边框大小的线条的形状,尽管我发现自己无法做更多的事情。问题在于代码不知道何时考虑一行大于它预期的像素的行作为形状的垂直或水平边界。我从左到右遍历形状的每个像素,并通过检查 alpha 值是否为 0 来检查是否有任何像素具有任何形式的颜色。一旦它发现一个像素的 alpha 值不为 0,它就会向前移动一个像素,然后使用 even/odd 技术来确定该点是否在多边形的一部分内(它使一个向右无限线并确定与彩色线的碰撞次数是否为奇数,如果是,则该点在多边形内)。通常,我们将单个单独的像素视为一条线,并且我们将超过一个像素的水平线视为两条线,因为水平线是否经常是边框的一部分。假设以下场景:
这里,红点是我们开始测试的点(像素)。如果我们不认为中间的水平线是两个点(如红线和 x 所示),我们将只有两个交点,因此不会填充像素,尽管我们肯定会这样做想要填充那个像素。然而,如前所述,这会带来另一个不同场景的问题:
在这种情况下,如果我们做将超过一个像素的水平线算作两条单独的线,我们最终不会填充任何边框比粗的区域预期的厚度。供大家参考,处理这个的函数如下:
//imgData is essentially a WebImage object (explained more below) and r, g, and b are the color values for the fill color
function fillWithColor(imgData, r, g, b) {
//Boolean determining whether we should color the given pixel(s) or not
var doColor = false;
//Booleans determining whether the last pixel found in the entire image was colored
var blackLast = false;
//Booleans determining whether the last 1 or 2 pixels found after a given pixel were colored
var foundBlackPrev, foundBlackPrev2 = false;
//The number of colored pixels found
var blackCount = 0;
//Loop through the entire canvas
for(var y = 0; y < imgData.height; y += IMG_SCALE) {
for(var x = 0; x < imgData.width; x += IMG_SCALE) {
//Test if given pixel is colored
if(getAlpha(imgData, x, y) != 0) {
//If the last pixel was black, begin coloring
if(!blackLast) {
blackLast = true;
doColor = true;
}
} else {
//If the current pixel is not colored, but the last one was, find all colored lines to the right
if(blackLast){
for(var i = x; i < imgData.width; i += IMG_SCALE) {
//If the pixel is colored...
if(getAlpha(imgData, i, y) != 0) {
//If no colored pixel was found before, add to the count
if(!foundBlackPrev){
blackCount++;
foundBlackPrev = true;
} else {
//Otherwise, at least 2 colored pixels have been found in a row
foundBlackPrev2 = true;
}
} else {
//If two or more colored pixels were found in a row, add to the count
if(foundBlackPrev2) {
blackCount++;
}
//Reset the booleans
foundBlackPrev2 = foundBlackPrev = false;
}
}
}
//If the count is odd, we start coloring
if(blackCount & 1) {
blackCount = 0;
doColor = true;
} else {
//If the last pixel in the entire image was black, we stop coloring
if(blackLast) {
doColor = false;
}
}
//Reset the boolean
blackLast = false;
//If we are to be coloring the pixel, color it
if(doColor) {
//Color the pixel
for(var j = 0; j < IMG_SCALE; j++) {
for(var k = 0; k < IMG_SCALE; k++) {
//This is the same as calling setRed, setGreen, setBlue and setAlpha functions from the WebImage API all at once (parameters in order are WebImage object equivalent, x position of pixel, y position of pixel, red value, green value, blue value, and alpha value)
setRGB(imgData, x + j, y + k, r, g, b, 255);
}
}
}
}
}
}
//Update the image (essentially the same as removing all elements from the given area and calling add on the image)
clearCanvas();
putImageData(imgData, 0, 0, imgData.width, imgData.height);
//Return the modified data
return imgData;
}
在哪里...
imgData 是给定区域中所有像素的集合(本质上是一个 WebImage 对象)
IMG_SCALE 是图像按比例放大的整数值(这也给了我们像素的比例)。在这个例子中,它等于 4,因为图像被放大到 192x256(从 48x64)。这意味着您在图像中看到的每个“像素”实际上都由 4x4 块相同颜色的像素组成。
所以,我在这里真正要寻找的是一种方法来确定一个给定的彩色像素是水平边界的一部分,还是只是另一个包含垂直边界厚度的部分。此外,如果我总体上对这个问题有错误的方法,我将不胜感激任何关于如何更有效地做到这一点的建议。谢谢。
据我了解,您不能“将超过一个像素的水平线视为两条线”。我认为您不需要像现在这样计算黑色像素,而是计算 1 个或多个像素的组。
我还会通过避免使用“doColor”布尔变量来整理代码。您可以将着色代码移至新函数 color(x,y) 并立即调用它。
const ctx = document.querySelector("canvas").getContext("2d");
//ctx.lineWidth(10);//-as you asked we are setting greater border or line width,BUT "LINEWIDTH" IS NOT WORKING IN INBUILT Whosebug SNIPPET USE IT IN A FILE I THINK Whosebug IS NOT UP-TO-DATE,IN ANY IDE UNCOMENT THIS
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
function getMousePosition(canvas, event) {
let rect = canvas.getBoundingClientRect();
let mx = event.clientX - rect.left;
let my = event.clientY - rect.top;
console.log("Coordinate x: " + mx, "Coordinate y: " + my);
floodFill(ctx, mx, my, [155, 0, 255, 255], 128);
}
let canvasElem = document.querySelector("canvas");
canvasElem.addEventListener("mousedown", function(e) {
getMousePosition(canvasElem, e);
});
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;
return imageData.data.slice(offset, offset + 4);
}
}
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, rangeSq) {
const dr = a[0] - b[0];
const dg = a[1] - b[1];
const db = a[2] - b[2];
const da = a[3] - b[3];
return dr * dr + dg * dg + db * db + da * da < rangeSq;
}
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);
// 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></canvas>
这是基于其他答案 注意:“LINEWIDTH”在内置的 Whosebug 片段中不起作用在我认为 Whosebug 不是最新的文件中使用它,
但在简单的HTML,JS网站
中效果很好我理解这个问题,我认为如果你在这里改变你的策略,你会做得更好。我们知道以下内容:
- 起点在形状内部
- 应该为形状内的每个像素填充颜色
所以,我们总是可以将当前点的邻居推入队列中进行处理,注意避免对同一个点进行两次处理,这样就遍历了所有有用的像素,并将其纳入着色计划。以下功能未经测试。
function fillColor(pattern, startingPoint, color, boundaryColor) {
let visitQueue = [];
let output = {};
if (startingPoint.x - 1 >= 0) visitQueue.push({startingPoint.x - 1, startingPoint.y});
if (startingPoint.x + 1 < pattern.width) visitQueue.push({startingPoint.x + 1, startingPoint.y});
if (startingPoint.y + 1 < pattern.height) visitQueue.push({startingPoint.x, startingPoint.y + 1});
if (startingPoint.y - 1 >= 0) visitQueue.push({startingPoint.x, startingPoint.y - 1});
let visited = {};
while (visitQueue.length > 0) {
let point = visitQueue[0];
visitQueue.shift();
if ((!visited[point.x]) || (visited[point.x].indexOf(point.y) < 0)) {
if (!visited[point.x]) visited[point.x] = [];
visited[point.x].push(point.y);
if (isBlank(pattern, point)) { //you need to implement isBlank
if (!output[point.x]) output[point.x] = [];
output[point.x].push(point.y);
if (point.x + 1 < pattern.width) visitQueue.push({point.x + 1, point.y});
if (point.x - 1 >= 0) visitQueue.push({point.x - 1, point.y});
if (point.y + 1 < pattern.height) visitQueue.push({point.x, point.y + 1});
if (point.y - 1 >= 0) visitQueue.push({point.x, point.y - 1})
}
}
}
return output;
}