Floodfill algorithm - problem with implementing pseudocode - javascript

I'm trying to implement the pseudocode for flood fill algorithm. I took it from Graphics Gemes 1.
Here is the pseudocode:
Unfortunateley, when I implemented it in JavaScript, it hangs when I use my floodfill tool.
Here is my code:
function inside(x, y) {
const q = 4 * (x + y * that.w);
const color = [img.data[q], img.data[q + 1], img.data[q + 2], img.data[q + 3]];
return color[0] === toolColor[0] &&
color[1] === toolColor[1] &&
color[2] === toolColor[2];
}
function set(x, y) {
const q = 4 * (x + y * that.w);
img.data[q] = that.color.r;
img.data[q + 1] = that.color.g;
img.data[q + 2] = that.color.b;
img.data[q + 3] = that.color.a;
}
function doFloodFill(x, y) {
let skip = false, x1, x2, dy, start;
const s = [];
s.push([y, x, x, 1]);
s.push([y + 1, x, x, -1]);
while (s.length > 0) {
const n = s.pop();
y = n[3] + n[0];
x1 = n[1];
x2 = n[2];
dy = n[3];
x = x1;
while (x >= 0 && inside(x, y)) {
set(x, y);
x--;
}
if (x >= x1) {
//skip
skip = true;
}
if (!skip) {
start = x + 1;
if (start < x1) {
s.push([y, start, x1 - 1, -dy]);
}
x = x1 + 1;
}
do {
if (!skip) {
while (x <= that.w && inside(x, y)) {
set(x, y);
x++;
}
s.push([y, start, x - 1, dy]);
if (x > x2 + 1) {
s.push([y, x2 + 1, x - 1, -dy]);
}
}
//skip
x++;
while (x <= x2 && !inside(x, y)) {
x++;
}
skip = false;
} while (x < x2);
start = x;
}
}
img.data is a flat array later displayed in the browser.
toolColor is 4-element array containing color for area to be filled with.
What I'm doing wrong?
Other more simple algorithms works with inside and set functions so they seem to be ok.
If You need more of the code I can send it privately.
Edit: I updated the code and now it fills only part of area.

Ok, I fixed the code. It is very fast. Maybe someone will make use of it.
Here is updated code:
function inside(x, y) {
const q = 4 * (x + y * that.w);
const color = [img.data[q], img.data[q + 1], img.data[q + 2], img.data[q + 3]];
return color[0] === toolColor[0] &&
color[1] === toolColor[1] &&
color[2] === toolColor[2];
}
function set(x, y) {
const q = 4 * (x + y * that.w);
img.data[q] = that.color.r;
img.data[q + 1] = that.color.g;
img.data[q + 2] = that.color.b;
img.data[q + 3] = that.color.a;
}
function doFloodFill(x, y) {
let skip = false, x1, x2, dy, start;
const s = [];
s.push([y, x, x, 1]);
s.push([y + 1, x, x, -1]);
while (s.length > 0) {
const n = s.pop();
y = n[3] + n[0];
x1 = n[1];
x2 = n[2];
dy = n[3];
x = x1;
while (x >= 0 && inside(x, y)) {
set(x, y);
x--;
}
if (x >= x1) {
//skip
skip = true;
}
if (!skip) {
start = x + 1;
if (start < x1) {
s.push([y, start, x1 - 1, -dy]);
}
x = x1 + 1;
}
do {
if (!skip) {
while (x <= that.w && inside(x, y)) {
set(x, y);
x++;
}
s.push([y, start, x - 1, dy]);
if (x > x2 + 1) {
s.push([y, x2 + 1, x - 1, -dy]);
}
}
//skip
x++;
while (x <= x2 && !inside(x, y)) {
x++;
}
start = x;
skip = false;
} while (x < x2);
}
}
I made a basic mistake. skip value was outside the last loop, but it should be inside.
Moreover, y value was incorrectly initialized in loop, should be: y = n[3] + n[0]; instead of y = n[0];
The code on English Wikipedia related to this algorithm seems to be incorrect.
I made speed tests of this algorithm. My implementation is about 1.5 times faster than the code on Wikipedia.

Related

Matrix variable is not undefined in javascript

I'm coding a maze generator using the backtracking algorithm and p5.js library, but when I was coding a loop to set random positions in the maze, I noticed an error in javascript: "maze[(x - 2)] is undefined", but this variable is declared! I tried to read about data structures, but I couldn't find a solution for this problem.
What is happening?
Can someone help me?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset = "utf-8">
<title>Maze</title>
</head>
<body>
<script src = "https://cdn.jsdelivr.net/npm/p5#1.4.0/lib/p5.js"></script>
<script>
var width = 720;
var height = 400;
var x, y;
var maze = Array(39).fill(Array(71).fill(0));
function setup()
{
createCanvas(720, 400);
}
function draw()
{
//inicial position
x = 1;
y = 1;
maze[x][y] = 1;
//grid
strokeWeight(1);
stroke('BLACK');
for (x = 0; x <= 720; x += 10) line(x, 0, x, 400);
for (y = 0; y <= 400; y += 10) line(0, y, 720, y);
found = 0;
do
{
direction = Math.floor(Math.random() * 4);
if (direction == 0 && (y != width - 2 && maze[x][y + 2] == 0))
{
x2 = x;
y2 = y;
y += 2;
maze[x][y] = 1;
maze[x][y-1] = 1;
found = 1;
}
else if (direction == 1 && (x > 1 && maze[x - 2][y] == 0))
{
x2 = x;
y2 = y;
x -= 2;
maze[x][y] = 1;
maze[x + 1][y] = 1;
found = 1;
}
else if (direction == 2 && (y > 1 && maze[x][y - 2] == 0))
{
x2 = x;
y2 = y;
y -= 2;
maze[x][y] = 1;
maze[x][y + 1] = 1;
found = 1;
}
else if (direction == 3 && (x != height - 2 && maze[x + 2][y] == 0))
{
x2 = x;
y2 = y;
x += 2;
maze[x][y] = 1;
maze[x - 1][y] = 1;
found = 1;
}
} while (found == 0);
noLoop();
}
</script>
</body>
</html>
There a few issues with this code.
Lets start with why you getting that error. The issue is that you are reusing global x and y variables in for loop that draw lines, when the loop is done, it set these variables larger than your maze array.
This brings us to second issue: maze array is smaller than width/height of the canvas.
The code is not 100% working so I don't exactly understand what it supposed to do, but it seems x and y should not exceed width/height, however you have a condition y != width - 2 which will be true even if y is bigger than width (btw, shouldn't it be height instead??)
And finally, through out the code you have multiple places with 720 and 400, instead of using width and height variables. This is a bad practice.
const width = 720;
const height = 400;
var x, y;
var maze = Array(width+1).fill(Array(height+1).fill(0));
function setup()
{
createCanvas(width, height);
}
function draw()
{
//inicial position
x = 1;
y = 1;
maze[x][y] = 1;
//grid
strokeWeight(1);
stroke('BLACK');
for (let x = 0; x <= width; x += 10) line(x, 0, x, height);
for (let y = 0; y <= height; y += 10) line(0, y, width, y);
found = 0;
do
{
direction = Math.floor(Math.random() * 4);
if (direction == 0 && (y <= height - 2 && maze[x][y + 2] == 0))
{
x2 = x;
y2 = y;
y += 2;
maze[x][y] = 1;
maze[x][y-1] = 1;
found = 1;
}
else if (direction == 1 && (x > 1 && maze[x - 2][y] == 0))
{
x2 = x;
y2 = y;
x -= 2;
maze[x][y] = 1;
maze[x + 1][y] = 1;
found = 1;
}
else if (direction == 2 && (y > 1 && maze[x][y - 2] == 0))
{
x2 = x;
y2 = y;
y -= 2;
maze[x][y] = 1;
maze[x][y + 1] = 1;
found = 1;
}
else if (direction == 3 && (x <= width - 2 && maze[x + 2][y] == 0))
{
x2 = x;
y2 = y;
x += 2;
maze[x][y] = 1;
maze[x - 1][y] = 1;
found = 1;
}
} while (found == 0);
noLoop();
}
<script src = "https://cdn.jsdelivr.net/npm/p5#1.4.0/lib/p5.js"></script>

Math - get the smallest polygon that the lines close around chart starting point

I'm trying to get the points of the smallest polygons that the lines create around the starting point of the chart, that is, the innermost polygon. The lines vary depending on some parameters that can be changed.
Example of such a chart:
As is visible from the chart, the lines intersect many times and, thus, create multiple polygons. However, I am interested in getting only the smallest polygon that is within the starting point (the center) of the chart.
I was thinking about drawing multiple parallels with x-axis from the y-axis to left and right from the top and the bottom (the smallest intercept on +y and -y axis) and seeing which line gets "hit" first as to get the lines which would enclose this polygon, then getting their intersections for vertices which can be used to drawing the polygon. However, since it will take numerous points to check such lines with precision, I am wondering if there is perhaps a more elegant solution to the problem?
I managed to solve this with the help of Stef and his algorithm. Here is the code I used:
const getIntersections = async (
lines: IPoint[][]
): Promise<IIntersectLine[]> => {
let lineIntersects: IIntersectLine[] = [];
lines.forEach((line) => {
let lineIntersect: IIntersectLine = {
line: line,
intersects: [],
};
let x1 = line[1].x;
let y1 = line[1].y;
let x2 = line[2].x;
let y2 = line[2].y;
for (let i = 0; i < lines.length; i++) {
let x3 = lines[i][1].x;
let y3 = lines[i][1].y;
let x4 = lines[i][2].x;
let y4 = lines[i][2].y;
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) continue;
let denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
if (denominator === 0) continue;
let ua =
((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
let ub =
((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
if (ua < 0 || ua > 1 || ub < 0 || ub > 1) continue;
let x = x1 + ua * (x2 - x1);
let y = y1 + ua * (y2 - y1);
lineIntersect.intersects.push({
x: +x.toFixed(4),
y: +y.toFixed(4),
});
}
lineIntersect.intersects.sort((a, b) => {
return a.x - b.x;
});
lineIntersects.push(lineIntersect);
});
return lineIntersects;
};
const getStartingPoint = async (intersects: IPoint[]) => {
let result: IPoint = intersects[0];
let distance = result.x * result.x + result.y * result.y;
intersects.forEach((i) => {
let newDistance = i.x * i.x + i.y * i.y;
if (newDistance < distance) {
distance = newDistance;
result = i;
}
});
return result;
};
const calcPolygonArea = async (polygon: IPoint[]) => {
let total = 0;
for (let i = 0, l = polygon.length; i < l; i++) {
let addX = polygon[i].x;
let addY = polygon[i == polygon.length - 1 ? 0 : i + 1].y;
let subX = polygon[i == polygon.length - 1 ? 0 : i + 1].x;
let subY = polygon[i].y;
total += addX * addY * 0.5;
total -= subX * subY * 0.5;
}
return Math.abs(total);
};
export const getPolygonVertices = async (lines: IPoint[][]) => {
let result: IPoint[] = [];
let intersections = await getIntersections(lines);
let intersectionVertices = intersections.map((x) => x.intersects).flat();
let startingPoint = await getStartingPoint(intersectionVertices);
let crossedLines = intersections.filter((x) =>
x.intersects.some(
(p) => p.x === startingPoint.x && p.y === startingPoint.y
)
);
let newPoints: IPoint[] = [];
const x0 = 0;
const y0 = 0;
crossedLines.forEach((line) => {
let x1 = startingPoint.x;
let y1 = startingPoint.y;
let pointIndex = line.intersects.findIndex(
(p) => p.x === startingPoint.x && p.y === startingPoint.y
);
let d;
if (line.intersects[pointIndex - 1]) {
let x2 = line.intersects[pointIndex - 1].x;
let y2 = line.intersects[pointIndex - 1].y;
d = (x0 - x1) * (y2 - y1) - (y0 - y1) * (x2 - x1);
if (d > 0) newPoints.push({ x: x2, y: y2 });
}
if (line.intersects[pointIndex + 1]) {
let x2 = line.intersects[pointIndex + 1].x;
let y2 = line.intersects[pointIndex + 1].y;
d = (x0 - x1) * (y2 - y1) - (y0 - y1) * (x2 - x1);
if (d > 0) newPoints.push({ x: x2, y: y2 });
}
});
let result1: IPoint[] = [];
let result2: IPoint[] = [];
for (let i = 0; i < newPoints.length; i++) {
let tempResult: IPoint[] = [];
tempResult.push(startingPoint, newPoints[i]);
for (let j = 0; j < 50; j++) {
const uniqueValues = new Set(tempResult.map((v) => v.x));
if (uniqueValues.size < tempResult.length) {
if (i === 0) result1 = tempResult;
else result2 = tempResult;
break;
}
let newCrossedLines = intersections.filter((x) =>
x.intersects.some(
(p) =>
p.x === tempResult[tempResult.length - 1].x &&
p.y === tempResult[tempResult.length - 1].y
)
);
let newLine = newCrossedLines.filter((l) =>
l.intersects.every(
(p) =>
p.x !== tempResult[tempResult.length - 2].x &&
p.y !== tempResult[tempResult.length - 2].y
)
)[0];
let x1 = tempResult[tempResult.length - 1].x;
let y1 = tempResult[tempResult.length - 1].y;
let pointIndex = newLine.intersects.findIndex(
(p) =>
p.x === tempResult[tempResult.length - 1].x &&
p.y === tempResult[tempResult.length - 1].y
);
let d;
if (newLine.intersects[pointIndex - 1]) {
let x2 = newLine.intersects[pointIndex - 1].x;
let y2 = newLine.intersects[pointIndex - 1].y;
d = (x0 - x1) * (y2 - y1) - (y0 - y1) * (x2 - x1);
if (d > 0) tempResult.push({ x: x2, y: y2 });
}
if (newLine.intersects[pointIndex + 1]) {
let x2 = newLine.intersects[pointIndex + 1].x;
let y2 = newLine.intersects[pointIndex + 1].y;
d = (x0 - x1) * (y2 - y1) - (y0 - y1) * (x2 - x1);
if (d > 0) tempResult.push({ x: x2, y: y2 });
}
}
}
const area1 = await calcPolygonArea(result1);
const area2 = await calcPolygonArea(result2);
area1 < area2 ? (result = result1) : (result = result2);
return result;
};
Essentially, first I get all the intersections of all the lines on the chart. Then I find the closest one to the chart's starting point (0,0) as the smallest polygon enclosing it should contain that vertex. After that, I begin moving along the two lines that make up the closest intersection. Repeating the process for those two starting lines, I move clockwise along the line up to the next intersection, where I then move along the next line, continuing the process until I get a duplicate vertex in my result array, that is, until the polygon is closed. In the end, I compare the two polygons and return the smaller one.
There is most likely a more efficient way to do this, but this works for now!
End result:
Here is a possible algorithm:
While a cycle was not found:
Start at some point (x,y) on some line L
Find next intersection point (x',y') on L in clockwise direction
If the origin (0, 0) is on the right of this line:
x = x'
y = y'
L = that new line
If a cycle was found: this cycle is the polygon.

Fill randomly shaped area of single colour by clicking on it

I'm trying to fill a random area enclosed by other areas of a different color.
I'm currently using 4-pixel connectivity to achieve this. It works, however, the filling takes way to long (Firefox askes me multiple times to stop the script. Chrome often just terminates it because the maximum call stack is exceeded). My question: How can I optimize my code or what other algorithms should I use?
PS: I fixed the mistake Wan Chap mentioned.
EDIT: I now added a global variable canImageData and changed the getRGBStringFromPixel() function. The filling works much faster now but I'm constantly getting exceeded call stack errors now leading to bigger areas only being filled partially. Does anybody have an example of a javascript fit version of the aforementioned algorithm?
EDIT FINAL: I found a different much more performant solution digging through another site's js source. I posted the solution as an answer below.
My code:
var can = document.getElementById('can');
var ctx = can.getContext('2d');
var canImageData = null;
var RGB_STRING_WHITE = '255-255-255-255';
function colour_fill_4_connectivity(x, y, RGBString_fill_colour, RGBString_region_colour){
try {
var currentPixelRGBString = getRGBStringFromPixel(x, y);
if(currentPixelRGBString == RGBString_fill_colour || currentPixelRGBString != RGBString_region_colour)
return;
setPixelFromRGBString(x, y, RGBString_fill_colour);
if(x < can.width)
colour_fill_4_connectivity(x + 1, y, RGBString_fill_colour, RGBString_region_colour);
if(x > 0)
colour_fill_4_connectivity(x - 1, y, RGBString_fill_colour, RGBString_region_colour);
if(y < can.height)
colour_fill_4_connectivity(x, y + 1, RGBString_fill_colour, RGBString_region_colour);
if(y > 0)
colour_fill_4_connectivity(x, y - 1, RGBString_fill_colour, RGBString_region_colour);
} catch(e) {
console.log('ERROR: colour_fill_4_connectivity(' + x + ', ' + y + ', ' + RGBString_fill_colour + ', ' + RGBString_region_colour + ') -> ' + e);
}
}
function fillColor(x, y, RGBString_fill_colour, RGBString_region_colour) {
colour_fill_4_connectivity(x, y, RGBString_fill_colour, RGBString_region_colour);
}
function RGBStringToArray(valRGB) {
return valRGB.split('-');
}
function getRGBStringFromPixel(x, y) {
var data = canImageData.data;
var startIndex = (x + y * can.width) * 4;
return data[startIndex] + '-' + data[startIndex + 1] + '-' + data[startIndex + 2] + '-' + data[startIndex + 3];
}
function setPixelFromRGBString(x, y, valRGB) {
var imageData = ctx.createImageData(1, 1);
var data = imageData.data;
var rgbArr = RGBStringToArray(valRGB);
var startIndex = (x + y * can.width) * 4;
data[0] = rgbArr[0];
data[1] = rgbArr[1];
data[2] = rgbArr[2];
data[3] = rgbArr[3];
ctx.putImageData(imageData, x, y);
canImageData.data[startIndex] = rgbArr[0];
canImageData.data[startIndex + 1] = rgbArr[1];
canImageData.data[startIndex + 2] = rgbArr[2];
canImageData.data[startIndex + 3] = rgbArr[3];
}
// TEST
function _test_getRGBStringFromPixel() {
var res = [];
for(var y = 0; y < can.height; y++) {
for(var x = 0; x < can.width; x++) {
res.push({x:x, y:y, color:getRGBStringFromPixel(x, y)});
}
}
return JSON.stringify(res);
}
var img = new Image();
img.onload = (function(e) {
can.width = this.width;
can.height = this.height;
ctx.drawImage(this, 0, 0);
canImageData = ctx.getImageData(0, 0, can.width, can.height);
}).bind(img);
img.src = 'test-image.png';
can.addEventListener('click', function(e) {
var clickX = e.clientX, clickY = e.clientY;
if(e.button === 0 && clickX <= can.width && clickY <= can.height)
fillColor(clickX, clickY, RGB_STRING_WHITE, getRGBStringFromPixel(clickX, clickY));
}, false);
body {
padding: 0;
margin: 0;
}
<canvas id="can" width="600" height="400"></canvas>
It seems it just a minor mistake, in function setPixelFromRGBString:
Change data[1] = rgbArr[2]; to data[2] = rgbArr[2];
var can = document.getElementById('can');
var ctx = can.getContext('2d');
var RGB_STRING_WHITE = '255-255-255-255';
function colour_fill_4_connectivity(x, y, RGBString_fill_colour, RGBString_region_colour) {
//try {
var currentPixelRGBString = getRGBStringFromPixel(x, y);
if (currentPixelRGBString == RGBString_fill_colour || currentPixelRGBString != RGBString_region_colour)
return;
setPixelFromRGBString(x, y, RGBString_fill_colour);
if (x > 0)
colour_fill_4_connectivity(x - 1, y, RGBString_fill_colour, RGBString_region_colour);
if (x < can.width)
colour_fill_4_connectivity(x + 1, y, RGBString_fill_colour, RGBString_region_colour);
if (y > 0)
colour_fill_4_connectivity(x, y - 1, RGBString_fill_colour, RGBString_region_colour);
if (y < can.height)
colour_fill_4_connectivity(x, y + 1, RGBString_fill_colour, RGBString_region_colour);
//} catch(e) {
//console.log('ERROR: colour_fill_4_connectivity(' + x + ', ' + y + ', ' + RGBString_fill_colour + ', ' + RGBString_region_colour + ') -> ' + e);
//}
}
function RGBStringToArray(valRGB) {
return valRGB.split('-');
}
function getRGBStringFromPixel(x, y) {
var data = ctx.getImageData(x, y, x + 1, y + 1).data;
return data[0] + '-' + data[1] + '-' + data[2] + '-' + data[3];
}
function setPixelFromRGBString(x, y, valRGB) {
var imageData = ctx.createImageData(1, 1);
var data = imageData.data;
var rgbArr = RGBStringToArray(valRGB);
data[0] = rgbArr[0];
data[1] = rgbArr[1];
data[1] = rgbArr[2];
data[3] = rgbArr[3];
ctx.putImageData(imageData, x, y);
}
// TEST
var img = new Image();
img.onload = (function(e) {
ctx.drawImage(img, 0, 0);
}).bind(img);
img.src = 'https://static1.squarespace.com/static/593357e715d5dbea570d2118/593ee768893fc0375f9c6fd5/5c7499a7e79c707a50772134/1551194769831/Test+pattern.png?format=1000w';
can.addEventListener('click', function(e) {
var clickX = e.clientX,
clickY = e.clientY;
if (e.button === 0 && clickX <= can.width && clickY <= can.height)
colour_fill_4_connectivity(clickX, clickY, RGB_STRING_WHITE, getRGBStringFromPixel(clickX, clickY));
}, false);
body {
padding: 0;
margin: 0;
}
<canvas id="can" width="600" height="400"></canvas>
After some research, I decided to dig into the source of http://skribbl.io which has a reliable and fast filling tool. They found an iterative solution. I extracted a snippet from the source file and changed parts to make it easier to read:
function DrawingBoard(canvas) {
this.canvas = canvas;
this.canvasCtx = this.canvas.getContext("2d");
}
DrawingBoard.prototype.getPixel = function(imageData, x, y) {
var startIndex = 4 * (y * imageData.width + x);
return startIndex >= 0 && startIndex < imageData.data.length ? [imageData.data[startIndex], imageData.data[startIndex + 1], imageData.data[startIndex + 2]] : [0, 0, 0]
}
DrawingBoard.prototype.setPixel = function(imageData, startIndex, r, g, b) {
startIndex >= 0 && startIndex < imageData.data.length && (imageData.data[startIndex] = r, imageData.data[startIndex + 1] = g, imageData.data[startIndex + 2] = b, imageData.data[startIndex + 3] = 255)
}
DrawingBoard.prototype.floodFill = function(startX, startY, r, g, b) {
var imageData = this.canvasCtx.getImageData(0, 0, this.canvas.width, this.canvas.height), points = [[startX, startY]], targetPixelRGB = this.getPixel(imageData, startX, startY);
if (r != targetPixelRGB[0] || g != targetPixelRGB[1] || b != targetPixelRGB[2]) {
for (
var c = function(t) {
var e = imageData.data[t],
i = imageData.data[t + 1],
c = imageData.data[t + 2];
if (e == r && i == g && c == b) return false;
var u = Math.abs(e - targetPixelRGB[0]),
h = Math.abs(i - targetPixelRGB[1]),
l = Math.abs(c - targetPixelRGB[2]);
return u < 1 && h < 1 && l < 1
},
imageHeight = imageData.height, imageHeightWidth = imageData.width; points.length;
) {
var point, pointX, pointY, index, y, m;
for (point = points.pop(), pointX = point[0], pointY = point[1], index = 4 * (pointY * imageHeightWidth + pointX); pointY-- >= 0 && c(index);) {
index -= 4 * imageHeightWidth;
}
for (index += 4 * imageHeightWidth, ++pointY, y = 0, m = 0; pointY++ < imageHeight - 1 && c(index);) {
this.setPixel(imageData, index, r, g, b);
pointX > 0;
c(index - 4) ? y || (points.push([pointX - 1, pointY]), y = 1) : y && (y = 0);
pointX < imageHeightWidth - 1;
c(index + 4) ? m || (points.push([pointX + 1, pointY]), m = 1) : m && (m = 0);
index += 4 * imageHeightWidth;
}
}
this.canvasCtx.putImageData(imageData, 0, 0)
}
}
// TEST
var db = new DrawingBoard(document.getElementById('can'));
var img = new Image();
var fillColor = [255, 0, 255];
img.onload = (function() {
db.canvasCtx.drawImage(this, 0, 0);
}).bind(img);
img.src = 'test.png';
addEventListener('click', function(e) {
var cx = e.clientX, cy = e.clientY;
if(
e.button === 0 &&
cx > 0 && cx < db.canvas.width &&
cy > 0 && cy < db.canvas.height
) {
db.floodFill(cx, cy, fillColor[0], fillColor[1], fillColor[2]);
}
}, false);
body {
padding: 0;
margin: 0;
}
<canvas id="can" width="600" height="400"></canvas>
Working fiddle can be found here: https://jsfiddle.net/f0kuwa5e/

canvas - get pixels on a line

I need a function that accept 5 arguments (ctx, startX, startY, endX, endY). It should return pixels on a canvas that lay on the line, that starts on (startX, startY) and ends on (endX, endY). How can I implement it?
You can use Brensenham line algorithm. It will get each pixel without needing to check if you already have that pixel which many other line methods would need.
function getPixelsOnLine(ctx, startX, startY, endX, endY){
const imageData = ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height);
const data = imageData.data;
const pixelCols = [];
const getPixel = (x,y) => {
if(x < 0 || x >= imageData.width || y < 0 || y >= imageData.height ){
return "rgba(0,0,0,0)";
}
const ind = (x + y * imageData.width) * 4;
return `rgba(${data[ind++]},${data[ind++]},${data[ind++]},${data[ind++]/255})`;
}
var x = Math.floor(startX);
var y = Math.floor(startY);
const xx = Math.floor(endX);
const yy = Math.floor(endY);
const dx = Math.abs(xx - x);
const sx = x < xx ? 1 : -1;
const dy = -Math.abs(yy - y);
const sy = y < yy ? 1 : -1;
var err = dx + dy;
var e2;
var end = false;
while (!end) {
pixelCols.push(getpixel(x,y));
if ((x === xx && y === yy)) {
end = true;
} else {
e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x += sx;
}
if (e2 <= dx) {
err += dx;
y += sy;
}
}
}
return pixelCols;
}
Function returns array of pixel as CSS color values rgba(red,green,blue,alpha) on line from start to end.

Draw lines and circles and fill the hole shape in JS

I read the contents of a dxf-file (only 2D) in NodeJS with a dxf parser (https://github.com/bjnortier/dxf) and then i get an array with the following output:
LINE: start.x, start.y, end.x, end.y
CIRCLE: x, y, radius
ARC: x, y ,radius, startAngle, endAngle
I wrote 3 functions based on the Bresenham-Algorithm to set the needed pixels in an array, which i want to use later to draw an canvas.
The input-parameters are
data: the denorm dxf data in an array
coordSystem: the array where to set the needed pixels
module.exports: {
processLINE: function(data, coordSystem) {
var setPixel = function(x, y) {
x = Math.ceil(x);
y = Math.ceil(y);
coordSystem[x][y] = 1;
}
var line = function(x0, y0, x1, y1) {
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
var sx = (x0 < x1) ? 1 : -1;
var sy = (y0 < y1) ? 1 : -1;
var err = dx-dy;
var e2;
while(true) {
setPixel(x0,y0);
if ((x0===x1) && (y0===y1)) break;
e2 = 2*err;
if (e2 >-dy){ err -= dy; x0 += sx; }
if (e2 < dx){ err += dx; y0 += sy; }
}
}
line(Math.ceil(data.start.x), Math.ceil(data.start.y), Math.ceil(data.end.x), Math.ceil(data.end.y))
return coordSystem;
},
processCIRCLE: function(data, coordSystem) {
var setPixel = function(x, y) {
x = Math.ceil(x);
y = Math.ceil(y);
coordSystem[x][y] = 1;
}
var createCircle = function(x0, y0, radius)
{
var f = 1 - radius;
var ddF_x = 0;
var ddF_y = -2 * radius;
var x = 0;
var y = radius;
setPixel(x0, y0 + radius);
setPixel(x0, y0 - radius);
setPixel(x0 + radius, y0);
setPixel(x0 - radius, y0);
while(x < y)
{
if(f >= 0)
{
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x + 1;
setPixel(x0 + x, y0 + y);
setPixel(x0 - x, y0 + y);
setPixel(x0 + x, y0 - y);
setPixel(x0 - x, y0 - y);
setPixel(x0 + y, y0 + x);
setPixel(x0 - y, y0 + x);
setPixel(x0 + y, y0 - x);
setPixel(x0 - y, y0 - x);
}
}
createCircle(data.x, data.y, data.r);
return coordSystem;
},
processARC: function(data, coordSystem) {
var setPixel = function(x, y, coordinates) {
x = Math.ceil(x);
y = Math.ceil(y);
coordSystem[x][y] = 1;
}
var createPartialcircle = function()
{
startAngle = data.startAngle*180/Math.PI;
endAngle = data.endAngle*180/Math.PI;
if(startAngle>endAngle) {
for (var i=startAngle; i>endAngle; i--) {
var radians = i * Math.PI / 180;
var px = data.x - data.r * Math.cos(radians);
var py = data.y - data.r * Math.sin(radians);
setPixel(px, py, coordinates);
}
} else {
for (var i=startAngle; i<endAngle; i++) {
var radians = i * Math.PI / 180;
var px = data.x + data.r * Math.cos(radians);
var py = data.y + data.r * Math.sin(radians);
setPixel(px, py, coordinates);
}
}
}
createPartialcircle(data.x, data.y, data.r);
return coordSystem;
}
}
With this i get the following shape:
As you can see it works, but there are some "holes" and because of this my last function which should fill the hole shape (scan-line-algorithm), doesn't work well...
Here is how i fill the shape
I took this code from HERE and wrote it in JavaScript-Style.
function scanLineFill(config, data, x, y, fillColor) {
function getPixel(x,y) {
return data[x][y];
}
function setPixel(x,y) {
data[x][y] = fillColor;
}
// Config
var nMinX = 0;
var nMinY = 0;
var nMaxX = config.maxValues.x;
var nMaxY = config.maxValues.y;
var seedColor = getPixel(x,y);
function lineFill(x1, x2, y) {
var xL,xR;
if( y < nMinY || nMaxY < y || x1 < nMinX || nMaxX < x1 || x2 < nMinX || nMaxX < x2 )
return;
for( xL = x1; xL >= nMinX; --xL ) { // scan left
if( getPixel(xL,y) !== seedColor )
break;
setPixel(xL,y);
}
if( xL < x1 ) {
lineFill(xL, x1, y-1); // fill child
lineFill(xL, x1, y+1); // fill child
++x1;
}
for( xR = x2; xR <= nMaxX; ++xR ) { // scan right
console.log('FOR: xR --> ', xR)
if( getPixel(xR,y) !== seedColor )
break;
setPixel(xR,y);
}
if( xR > x2 ) {
lineFill(x2, xR, y-1); // fill child
lineFill(x2, xR, y+1); // fill child
--x2;
}
for( xR = x1; xR <= x2 && xR <= nMaxX; ++xR ) { // scan betweens
if( getPixel(xR,y) === seedColor )
setPixel(xR,y);
else {
if( x1 < xR ) {
// fill child
lineFill(x1, xR-1, y-1);
// fill child
lineFill(x1, xR-1, y+1);
x1 = xR;
}
// Note: This function still works if this step is removed.
for( ; xR <= x2 && xR <= nMaxX; ++xR) { // skip over border
if( getPixel(xR,y) === seedColor ) {
x1 = xR--;
break;
}
}
}
}
}
if( fillColor !== seedColor ) {
lineFill(x, x, y);
}
return data;
}
And the result is this:
I think if the shape has no holes, the fill-function would fill the shape correct. But how can i achieve this?

Categories