Fill randomly shaped area of single colour by clicking on it - javascript

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/

Related

z sorting issue with triangles in 3d

We coded a spinning 3d shape in js. There's a flicker in the render of the top triangle, we think it's because the z sorting is not working correctly. How do we resolve this?
Here's a jsfiddle.
Here's the z sorting code:
// z sorting
// dots_for_rendering.sort((a,b) => Math.sqrt((b.x)**2 + (b.y)**2) - Math.sqrt((a.x)**2 + (a.y)**2))
for (var i = 0; i < polygons.length; i++) {
polygons[i].maxz = -Infinity;
polygons[i].minz = Infinity;
polygons[i].midz = 0;
for (var j = 0; j < polygons[i].verticies.length; j++) {
var z = rotated_verticies[polygons[i].verticies[j]].vector[2];
if (z > polygons[i].maxz) {
polygons[i].maxz = z;
}
if (z < polygons[i].minz) {
polygons[i].minz = z;
}
polygons[i].midz += z;
}
polygons[i].midz /= polygons[i].verticies.length;
}
polygons.sort((a, b) => b.midz - a.midz)
// polygons.sort((a,b) => Math.max(b.maxz - a.minz, b.minz - a.maxz))
// polygons.sort((a,b) => {
// if (a.minz < b.maxz) {
// return 0;
// }
// if (b.minz < a.maxz) {
// return -1;
// }
// return 0;
// })
Here's a code snippet:
class Tensor {
constructor(){
var input = this.takeInput(...arguments);
this.vector = input;
}
takeInput() {
var a = true;
for (var arg of arguments) {
if (typeof arg !== "number"){
a = false
}
}
if (a && arguments[2] !== true){
return new Array(...arguments);
}
else {
if (arguments[0] instanceof Tensor){
return arguments[0].vector;
}
else {
if (typeof arguments[0] === "number" && typeof arguments[1] === "number" && arguments[2] === true) {
var res = [];
for (var i = 0; i < arguments[0]; i++) {
res.push(arguments[1]);
}
return res;
}
}
}
}
// used for + - * /
change(f, input){
for (var i in this.vector) {
this.vector[i] = f(this.vector[i], input[i]);
}
return this;
}
copy() {
return new Tensor(...this.vector);
}
dimentions() {
return this.vector.length;
}
//-----------
len() {
var s = 0;
for (var dim of this.vector) {
s += dim ** 2;
}
return Math.sqrt(s);
}
norm() {
return this.div(this.dimentions(), this.len(), true)
}
add() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x + y, input);
}
sub() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x - y, input);
}
mult() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x * y, input);
}
div() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x / y, input);
}
dot() {
var input = this.takeInput(...arguments);
var res = 0;
for (var i in this.vector) {
res += this.vector[i] * input[i]
}
return res;
}
rotate() {
// WARNING: only for 3D currently!!!
var input = this.takeInput(...arguments);
var [x, y, z] = this.vector;
// rotate Z
var t_x = x * Math.cos(input[2]) - y * Math.sin(input[2])
y = y * Math.cos(input[2]) + x * Math.sin(input[2])
x = t_x
// rotate X
var t_y = y * Math.cos(input[0]) - z * Math.sin(input[0])
z = z * Math.cos(input[0]) + y * Math.sin(input[0])
y = t_y
// rotate Y
t_x = x * Math.cos(input[1]) + z * Math.sin(input[1])
z = z * Math.cos(input[1]) - x * Math.sin(input[1])
x = t_x
this.vector = [x, y, z];
return this;
}
}
var canvas = document.getElementById('canvas')
var ctx = canvas.getContext("2d")
w = 300
h = 286
fov = 0.1
scale = 65;
offset = new Tensor(w / 2 - 5, h / 2 - 92, 0.1);
light = new Tensor(3.5, 0.5, 1).norm();
canvas.width = w;
canvas.height = h;
var verticies = [];
verticies.push(new Tensor(0.5, 1, 0))
verticies.push(new Tensor(0.5, -1, 0))
verticies.push(new Tensor(-1, 0, 0))
verticies.push(new Tensor(0, 0, 2))
var polygons = [];
polygons.push({
verticies: [0, 3, 1],
color: 'red',
nf: 1
});
polygons.push({
verticies: [2, 3, 0],
color: 'blue',
nf: 1
});
polygons.push({
verticies: [2, 3, 1],
color: 'green',
nf: -1
});
polygons.push({
verticies: [0, 1, 2],
color: 'yellow',
nf: -1
});
for (var i = 0; i < polygons.length; i++) {
polygons[i].id = i;
}
theta = new Tensor(1.5 * Math.PI, 0, 1.5 * Math.PI);
function loop() {
ctx.clearRect(0, 0, w, h);
rotated_verticies = [];
for (var i = 0; i < verticies.length; i++) {
rotated_verticies.push(verticies[i].copy().rotate(theta));
}
// z sorting
// dots_for_rendering.sort((a,b) => Math.sqrt((b.x)**2 + (b.y)**2) - Math.sqrt((a.x)**2 + (a.y)**2))
for (var i = 0; i < polygons.length; i++) {
polygons[i].maxz = -Infinity;
polygons[i].minz = Infinity;
polygons[i].midz = 0;
for (var j = 0; j < polygons[i].verticies.length; j++) {
var z = rotated_verticies[polygons[i].verticies[j]].vector[2];
// z += 1 * (Math.random() * 2 - 1)
if (z > polygons[i].maxz) {
polygons[i].maxz = z;
}
if (z < polygons[i].minz) {
polygons[i].minz = z;
}
polygons[i].midz += z;
}
polygons[i].midz /= polygons[i].verticies.length;
}
polygons.sort((a, b) => b.midz - a.midz)
// polygons.sort((a,b) => Math.max(b.maxz - a.minz, b.minz - a.maxz))
// polygons.sort((a,b) => {
// if (a.minz < b.maxz) {
// return 0;
// }
// if (b.minz < a.maxz) {
// return -1;
// }
// return 0;
// })
for (var i = 0; i < polygons.length; i++) {
var polygon_2 = [];
for (var j = 0; j < polygons[i].verticies.length; j++) {
var v = rotated_verticies[polygons[i].verticies[j]]
polygon_2.push(v.vector);
}
var norm = getNormal(polygon_2, polygons[i].nf);
// var rotated_light = light.copy().rotate(theta);
var brightness = Math.max(0, norm.dot(light))
//ctx.fillStyle = "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)";
ctx.fillStyle = "hsl(190, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)";
// ctx.fillStyle = polygons[i].color
ctx.beginPath();
for (var j = 0; j < polygons[i].verticies.length; j++) {
var vertex = rotated_verticies[polygons[i].verticies[j]].copy();
vertex.mult(scale, scale, 1);
vertex.add(offset);
var n = 1 + vertex.vector[2] * fov;
vertex.div(n, n, 1)
// console.log(vertex.vector)
if (j == 0) {
ctx.moveTo(vertex.vector[0], vertex.vector[1]);
} else {
ctx.lineTo(vertex.vector[0], vertex.vector[1]);
}
}
ctx.closePath();
ctx.fill()
// ctx.stroke()
polygons[i].mid = new Tensor(3, 0, true);
for (var k = 0; k < polygons[i].verticies.length; k++) {
var vertex = rotated_verticies[polygons[i].verticies[k]].copy();
vertex.mult(scale, scale, 1);
vertex.add(offset);
var n = 1 + vertex.vector[2] * fov;
vertex.div(n, n, 1)
polygons[i].mid.add(vertex);
}
polygons[i].mid.div(3, polygons[i].verticies.length, true);
ctx.fillStyle = "red"
ctx.font = '50px serif';
// ctx.fillText(polygons[i].id + ", " + polygons[i].nf, polygons[i].mid.vector[0], polygons[i].mid.vector[1])
}
// theta.add(theta.vector[0] + (0.01*mouseY - theta.vector[0]) * 0.1, 0, theta.vector[2] + (-0.01*mouseX - theta.vector[2]) * 0.1)
theta.add(0, -0.0375, 0);
// fov = (mouseX - w/2) * 0.001
requestAnimationFrame(loop);
}
loop();
// setInterval(loop, 1000 / 60)
function getNormal(polygon, nf) {
var Ax = polygon[1][0] - polygon[0][0];
var Ay = polygon[1][1] - polygon[0][1];
var Az = polygon[1][2] - polygon[0][2];
var Bx = polygon[2][0] - polygon[0][0];
var By = polygon[2][1] - polygon[0][1];
var Bz = polygon[2][2] - polygon[0][2];
var Nx = Ay * Bz - Az * By
var Ny = Az * Bx - Ax * Bz
var Nz = Ax * By - Ay * Bx
return new Tensor(nf * Nx, nf * Ny, nf * Nz);
}
function len(p1, p2) {
return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2);
}
mouseX = 0
mouseY = 0
onmousemove = (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name=”ad.size” content=”width=300,height=600”>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>...</title>
</head>
<body>
<canvas id="canvas" width="300" height="238"></canvas>
</body>
</html>
** Edit:
Ok, we've significantly edited the code, see this fiddle, and the following code snippet below. It's still not working correctly, we think it's something to do with the first line of this piece of code, any ideas?
if (polygons[i].mid.copy().sub(camera).dot(norm) < 0) {
var pathelem = document.createElementNS("http://www.w3.org/2000/svg", "path");
pathelem.setAttribute("d", path);
pathelem.setAttribute("fill", "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)");
svg.appendChild(pathelem);
}
class Tensor {
constructor(){
var input = this.takeInput(...arguments);
this.vector = input;
}
takeInput() {
var a = true;
for (var arg of arguments) {
if (typeof arg !== "number"){
a = false
}
}
if (a && arguments[2] !== true){
return new Array(...arguments);
}
else {
if (arguments[0] instanceof Tensor){
return arguments[0].vector;
}
else {
if (typeof arguments[0] === "number" && typeof arguments[1] === "number" && arguments[2] === true) {
var res = [];
for (var i = 0; i < arguments[0]; i++) {
res.push(arguments[1]);
}
return res;
}
}
}
}
// used for + - * /
change(f, input){
for (var i in this.vector) {
this.vector[i] = f(this.vector[i], input[i]);
}
return this;
}
copy() {
return new Tensor(...this.vector);
}
dimentions() {
return this.vector.length;
}
//-----------
len() {
var s = 0;
for (var dim of this.vector) {
s += dim ** 2;
}
return Math.sqrt(s);
}
norm() {
return this.div(this.dimentions(), this.len(), true)
}
add() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x + y, input);
}
sub() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x - y, input);
}
mult() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x * y, input);
}
div() {
var input = this.takeInput(...arguments);
return this.change((x, y) => x / y, input);
}
dot() {
var input = this.takeInput(...arguments);
var res = 0;
for (var i in this.vector) {
res += this.vector[i] * input[i]
}
return res;
}
rotate() {
// WARNING: only for 3D currently!!!
var input = this.takeInput(...arguments);
var [x, y, z] = this.vector;
// rotate Z
var t_x = x * Math.cos(input[2]) - y * Math.sin(input[2])
y = y * Math.cos(input[2]) + x * Math.sin(input[2])
x = t_x
// rotate X
var t_y = y * Math.cos(input[0]) - z * Math.sin(input[0])
z = z * Math.cos(input[0]) + y * Math.sin(input[0])
y = t_y
// rotate Y
t_x = x * Math.cos(input[1]) + z * Math.sin(input[1])
z = z * Math.cos(input[1]) - x * Math.sin(input[1])
x = t_x
this.vector = [x, y, z];
return this;
}
}
var svg = document.getElementById('svg')
w = 300
h = 286
fov = 0.1
scale = 65;
camera = new Tensor(-w / 2 + 5, -h / 2 + 92, 0.1);
light = new Tensor(3.5, 0.5, 1).norm();
svg.setAttribute('width', w);
svg.setAttribute('height', h);
var vertices = [
new Tensor(0.5, 1, 0),
new Tensor(0.5, -1, 0),
new Tensor(-1, 0, 0),
new Tensor(0, 0, 2)
];
var polygons = [];
polygons.push({
vertices: [0, 3, 1],
color: 'red',
nf: 1
});
polygons.push({
vertices: [2, 3, 0],
color: 'blue',
nf: 1
});
polygons.push({
vertices: [2, 3, 1],
color: 'green',
nf: -1
});
polygons.push({
vertices: [0, 1, 2],
color: 'yellow',
nf: 1
});
for (var i = 0; i < polygons.length; i++) {
polygons[i].id = i;
}
theta = new Tensor(1.5 * Math.PI, 0, 1.5 * Math.PI);
function loop() {
// ctx.clearRect(0, 0, w, h);
svg.innerHTML = "";
rotated_vertices = [];
for (var i = 0; i < vertices.length; i++) {
rotated_vertices.push(vertices[i].copy().rotate(theta));
}
// z sorting
// dots_for_rendering.sort((a,b) => Math.sqrt((b.x)**2 + (b.y)**2) - Math.sqrt((a.x)**2 + (a.y)**2))
for (var i = 0; i < polygons.length; i++) {
polygons[i].maxz = -Infinity;
polygons[i].minz = Infinity;
polygons[i].midz = 0;
for (var j = 0; j < polygons[i].vertices.length; j++) {
var z = rotated_vertices[polygons[i].vertices[j]].vector[2];
// z += 1 * (Math.random() * 2 - 1)
if (z > polygons[i].maxz) {
polygons[i].maxz = z;
}
if (z < polygons[i].minz) {
polygons[i].minz = z;
}
polygons[i].midz += z;
}
polygons[i].midz /= polygons[i].vertices.length;
polygons[i].mid = new Tensor(3, 0, true);
for (var k = 0; k < polygons[i].vertices.length; k++) {
var vertex = rotated_vertices[polygons[i].vertices[k]].copy();
vertex.mult(scale, scale, 1);
vertex.sub(camera);
var n = 1 + vertex.vector[2] * fov;
vertex.div(n, n, 1)
polygons[i].mid.add(vertex);
}
polygons[i].mid.div(3, polygons[i].vertices.length, true);
}
polygons.sort((a, b) => b.midz - a.midz)
// polygons.sort((a,b) => Math.max(b.maxz - a.minz, b.minz - a.maxz))
// polygons.sort((a,b) => {
// if (a.minz < b.maxz) {
// return 0;
// }
// if (b.minz < a.maxz) {
// return -1;
// }
// return 0;
// })
for (var i = 0; i < polygons.length; i++) {
var polygons_embedded_point_coords = [];
for (var j = 0; j < polygons[i].vertices.length; j++) {
var v = rotated_vertices[polygons[i].vertices[j]]
polygons_embedded_point_coords.push(v.vector);
}
var norm = getNormal(polygons_embedded_point_coords, polygons[i].nf);
// var rotated_light = light.copy().rotate(theta);
var brightness = Math.max(0, norm.dot(light))
// ctx.fillStyle = "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)";
// ctx.fillStyle = polygons[i].color
// ctx.beginPath();
var path = [];
for (var j = 0; j < polygons[i].vertices.length; j++) {
var vertex = rotated_vertices[polygons[i].vertices[j]].copy();
vertex.mult(scale, scale, 1);
vertex.sub(camera);
var n = 1 + vertex.vector[2] * fov;
vertex.div(n, n, 1)
// console.log(vertex.vector)
if (j == 0) {
// ctx.moveTo(vertex.vector[0], vertex.vector[1]);
path.push("M "+vertex.vector[0]+" "+vertex.vector[1]);
} else {
path.push("L "+vertex.vector[0]+" "+vertex.vector[1]);
// ctx.lineTo(vertex.vector[0], vertex.vector[1]);
}
}
// that should work
if (polygons[i].mid.copy().sub(camera).dot(norm) < 0) {
var pathelem = document.createElementNS("http://www.w3.org/2000/svg", "path");
pathelem.setAttribute("d", path);
pathelem.setAttribute("fill", "hsl(31, "+100+"%, "+(Math.min(9.0*brightness + 40, 100))+"%)");
svg.appendChild(pathelem);
}
// ctx.fillStyle = "red"
// ctx.font = '15px serif';
//
// ctx.fillText(polygons[i].id + ", " + polygons[i].nf, polygons[i].mid.vector[0], polygons[i].mid.vector[1])
}
// theta.add(theta.vector[0] + (0.01*mouseY - theta.vector[0]) * 0.1, 0, theta.vector[2] + (-0.01*mouseX - theta.vector[2]) * 0.1)
theta.add(0, -0.0375, 0);
// fov = (mouseX - w/2) * 0.001
requestAnimationFrame(loop);
}
loop();
// setInterval(loop, 1000 / 60)
function getNormal(vertices, nf) {
var Ax = vertices[1][0] - vertices[0][0];
var Ay = vertices[1][1] - vertices[0][1];
var Az = vertices[1][2] - vertices[0][2];
var Bx = vertices[2][0] - vertices[0][0];
var By = vertices[2][1] - vertices[0][1];
var Bz = vertices[2][2] - vertices[0][2];
var Nx = Ay * Bz - Az * By
var Ny = Az * Bx - Ax * Bz
var Nz = Ax * By - Ay * Bx
return new Tensor(nf * Nx, nf * Ny, nf * Nz);
}
function len(p1, p2) {
return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2);
}
mouseX = 0
mouseY = 0
onmousemove = (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
<script src="Tensor.js"></script>
<script src="script-tensors-svg.js" async defer></script>
</head>
<body>
<!-- <canvas id="canvas"></canvas> -->
<svg id="svg" xmlns="http://www.w3.org/2000/svg"></svg>
</body>
</html>
Sorting by average Z just doesn't give you a reliable rendering order. Since your shape is convex, though, you don't need to sort at all.
Make sure the vertices of each triangle are sorted so that you can consistently get a surface normal that points outward. Then, just don't render any triangles with normals that point away from the camera, i.e.:
if (vector_from_camera_to_poly_midpoint \dot poly_normal < 0) {
//render the poly
}
Now you will only render the side of the object that is facing the camera -- none of the polygons will overlap, so you can render them in any order.

Why is this html5 canvas animation so intensive?

I create this animaiton using canvas and converting svg's to canvas shapes. Most times it runs it heats up my computer and the fan starts going.
Just wondering if there is something about the code, html5 canvas, canvas paths or the animation recursion that is so intensive?
View on codepen: https://codepen.io/benbyford-the-lessful/pen/ZjjVdR?editors=1010#
// check program is being run
console.log('bg animation running...');
// setup canvas
var canvas = document.getElementById('bgCanvas');
var ctx = canvas.getContext('2d')
// redo this - canvas size
//
var width = window.innerWidth,
height = window.innerHeight;
canvas.width = width * 2;
canvas.height = height * 2;
var gridSquareWidth = 20;
var gridWidth = (width * 2) / gridSquareWidth,
gridHeight = (height * 2) / gridSquareWidth;
var grid = [];
// create default grid array
for (var x = 0; x < gridWidth; x++) {
grid[x] = [];
for (var y = 0; y < gridHeight; y++) {
var rand = getRandomArbitrary(0,5);
var rand2 = getRandomArbitrary(0,2);
if(rand2 == 1 || x < (gridWidth / 4) || x > (gridWidth / 2) || y < (gridHeight / 4) || y > (gridHeight / 2)){
rand--;
}
if(rand > 2) grid[x][y] = 1;
}
}
//
// main update function
//
var animationSpeed = 0.1;
var animationSpeedCount = 0;
var running = true;
function update(dt) {
if(running){
animationSpeedCount += dt;
if(animationSpeedCount > animationSpeed){
moveGrid();
animationSpeedCount = 0;
}
draw();
}
}
var noOfFrames = 3;
var waveOffset = 15;
var increment = 0;
function moveGrid() {
var x = increment;
var x2 = increment - noOfFrames - waveOffset;
// add frmae wave
for (var i = 0; i < noOfFrames; i++) {
moveONeFrameForward(x, true);
x--;
}
// go back frmae wave
for (var i = 0; i < noOfFrames; i++) {
moveONeFrameForward(x2, false);
x2--;
}
// var x column, add of subtract by 1
function moveONeFrameForward(x, add){
if(x < 0){
x = Math.ceil(gridWidth + x);
}
if(x > 0 && x < gridWidth){
for (var y = 0; y < gridHeight; y++) {
if(grid[x][y] > 0){
if(add){
grid[x][y] = grid[x][y] + 1;
}else{
if(grid[x][y] > 1) grid[x][y] = grid[x][y] - 1;
}
}
}
}
}
// increment column
increment += 1;
if(increment > gridWidth){
increment = 0;
// stop running
// running = false;
}
}
var fills = ["#eeeeee","#efefef","#fefefe","#ffffff"];
function draw() {
// clear canvas to white
ctx.fillStyle = '#dddddd';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
var offsetX = x * gridSquareWidth;
var offsetY = y * gridSquareWidth;
var frame = 0;
switch (grid[x][y]) {
case 1:
frame = 1
break;
case 2:
frame = 2;
break;
case 3:
frame = 3;
break;
case 4:
frame = 4;
break;
default:
}
if(frame) drawframe(ctx, frame, offsetX, offsetY, fills);
}
}
}
// The main game loop
var lastTime = 0;
function gameLoop() {
var now = Date.now();
var dt = (now - lastTime) / 1000.0;
update(dt);
lastTime = now;
window.requestAnimationFrame(gameLoop);
};
// start game
gameLoop();
//
// UTILITIES
//
// cross browser requestAnimationFrame - https://gist.github.com/mrdoob/838785
if ( !window.requestAnimationFrame ) {
window.requestAnimationFrame = ( function() {
return window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(
/* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) {
window.setTimeout( callback, 1000 / 60 );
};
})();
}
function getRandomArbitrary(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
var frame1Center = 4.1;
var frame2Center = 2.1;
function drawframe(ctx, frame, x, y, fills) {
ctx.strokeStyle = 'rgba(0,0,0,0)';
ctx.lineCap = 'butt';
ctx.lineJoin = 'miter';
ctx.miterLimit = 4;
ctx.fillStyle = fills[frame-1];
switch (frame) {
case 1:
ctx.beginPath();
ctx.moveTo(3.1+x+frame1Center,0+y);
ctx.lineTo(0.6+x+frame1Center,0+y);
ctx.bezierCurveTo(0.3+x+frame1Center,0+y,0+x+frame1Center,0.3+y,0+x+frame1Center,0.6+y);
ctx.lineTo(0.3+x+frame1Center,12.1+y);
ctx.bezierCurveTo(0.3+x+frame1Center,12.4+y,0.6+x+frame1Center,12.7+y,0.8999999999999999+x+frame1Center,12.7+y);
ctx.lineTo(3.4+x+frame1Center,12.7+y);
ctx.bezierCurveTo(3.6999999999999997+x+frame1Center,12.7+y,4+x+frame1Center,12.399999999999999+y,4+x+frame1Center,12.1+y);
ctx.lineTo(4+x+frame1Center,0.6+y);
ctx.bezierCurveTo(4.1+x+frame1Center,0.3+y,3.7+x+frame1Center,0+y,3.1+x+frame1Center,0+y);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
case 2 || 6:
ctx.beginPath();
ctx.moveTo(4.4+x+frame2Center,0+y);
ctx.bezierCurveTo(3.1+x+frame2Center,0+y,0+x+frame2Center,0.8+y,0+x+frame2Center,2.1+y);
ctx.bezierCurveTo(0+x+frame2Center,3.4000000000000004+y,0.3+x+frame2Center,12.5+y,1.6+x+frame2Center,12.799999999999999+y);
ctx.bezierCurveTo(2.8+x+frame2Center,13+y,6+x+frame2Center,12+y,6+x+frame2Center,10.7+y);
ctx.bezierCurveTo(6+x+frame2Center,9.1+y,5.7+x+frame2Center,0+y,4.4+x+frame2Center,0+y);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
case 3 || 5:
ctx.beginPath();
ctx.moveTo(5.2+x,0 +y);
ctx.bezierCurveTo(7.5 +x,0+y,9.3+x,6.5+y,9.3 +x,8.7+y);
ctx.bezierCurveTo(9.3+x,10.899999999999999+y,6.300000000000001+x,12.799999999999999+y,4.1000000000000005+x,12.799999999999999+y);
ctx.bezierCurveTo(1.9000000000000004+x,12.799999999999999+y,0+x,6.3+y,0+x,4.1+y);
ctx.bezierCurveTo(0+x,1.8999999999999995+y,3+x,0+y,5.2+x,0+y);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
case 4:
ctx.beginPath();
ctx.arc(5.9+x,6.3+y,5.8,0,6.283185307179586,true);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
default:
}
};
It's a bit hard to tell exactly "why", but there are definitely some things that could be improved.
First, you are drawing twice as big as what is needed.
Set you canvas size to the rendered one, and you'll probably see a big improvement in performances.
Then, you are drawing a lot of sub-path at every draw (and setting a lot of times the context's properties for nothing).
You could try to merge all these sub-paths in bigger ones, grouped by fillStyle, so that the rasterizer works only four times per frame. This can also improve performances a bit.
But the approach I would personally take, is to pre-render all the 4 different states on 4 different canvases. Then, use only drawImage to draw the required strip.
In best case, you end up with only 4 calls to drawImage, in worth one, with 8 calls.
Here is a rough proof of concept:
// setup canvas
var canvas = document.getElementById('bgCanvas');
var ctx = canvas.getContext('2d')
// don't set it twice as big as needed
var width = window.innerWidth,
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
var gridSquareWidth = 10;
var gridWidth = (width) / gridSquareWidth,
gridHeight = (height) / gridSquareWidth;
var grid = [];
// create default grid array
for (var x = 0; x < gridWidth; x++) {
grid[x] = [];
for (var y = 0; y < gridHeight; y++) {
var rand = getRandomArbitrary(0, 5);
var rand2 = getRandomArbitrary(0, 2);
if (rand2 == 1 || x < (gridWidth / 4) || x > (gridWidth / 2) || y < (gridHeight / 4) || y > (gridHeight / 2)) {
rand--;
}
if (rand > 2) grid[x][y] = 1;
}
}
var fills = ["#eeeeee", "#efefef", "#fefefe", "#ffffff"];
var frame1Center = 4.1;
var frame2Center = 2.1;
// the 4 points drawers
var drawers = [draw0, draw1, draw2, draw3];
// initialise our four possible states
var states = [
initState(0),
initState(1),
initState(2),
initState(3)
];
//
// main update function
//
var running = true;
var speed = 2;
var waveWidth = 200;
var waveMargin = gridSquareWidth * 4;
var waveStart = 0;
var waveEnd = waveWidth;
// start game
update();
function initState(status) {
var c = canvas.cloneNode();
var ctx = c.getContext('2d');
ctx.scale(0.5, 0.5); // to circumvent values being set for scale(2)
ctx.beginPath(); // single path
ctx.fillStyle = fills[status];
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
if (grid[x][y]) {
drawers[status](ctx, x * gridSquareWidth * 2, y * gridSquareWidth * 2);
}
}
}
ctx.fill(); // single fill
return c;
}
function draw0(ctx, x, y) {
ctx.moveTo(3.1 + x + frame1Center, 0 + y);
ctx.lineTo(0.6 + x + frame1Center, 0 + y);
ctx.bezierCurveTo(0.3 + x + frame1Center, 0 + y, 0 + x + frame1Center, 0.3 + y, 0 + x + frame1Center, 0.6 + y);
ctx.lineTo(0.3 + x + frame1Center, 12.1 + y);
ctx.bezierCurveTo(0.3 + x + frame1Center, 12.4 + y, 0.6 + x + frame1Center, 12.7 + y, 0.8999999999999999 + x + frame1Center, 12.7 + y);
ctx.lineTo(3.4 + x + frame1Center, 12.7 + y);
ctx.bezierCurveTo(3.6999999999999997 + x + frame1Center, 12.7 + y, 4 + x + frame1Center, 12.399999999999999 + y, 4 + x + frame1Center, 12.1 + y);
ctx.lineTo(4 + x + frame1Center, 0.6 + y);
ctx.bezierCurveTo(4.1 + x + frame1Center, 0.3 + y, 3.7 + x + frame1Center, 0 + y, 3.1 + x + frame1Center, 0 + y);
ctx.closePath();
}
function draw1(ctx, x, y) {
ctx.moveTo(4.4 + x + frame2Center, 0 + y);
ctx.bezierCurveTo(3.1 + x + frame2Center, 0 + y, 0 + x + frame2Center, 0.8 + y, 0 + x + frame2Center, 2.1 + y);
ctx.bezierCurveTo(0 + x + frame2Center, 3.4000000000000004 + y, 0.3 + x + frame2Center, 12.5 + y, 1.6 + x + frame2Center, 12.799999999999999 + y);
ctx.bezierCurveTo(2.8 + x + frame2Center, 13 + y, 6 + x + frame2Center, 12 + y, 6 + x + frame2Center, 10.7 + y);
ctx.bezierCurveTo(6 + x + frame2Center, 9.1 + y, 5.7 + x + frame2Center, 0 + y, 4.4 + x + frame2Center, 0 + y);
ctx.closePath();
}
function draw2(ctx, x, y) {
ctx.moveTo(5.2 + x, 0 + y);
ctx.bezierCurveTo(7.5 + x, 0 + y, 9.3 + x, 6.5 + y, 9.3 + x, 8.7 + y);
ctx.bezierCurveTo(9.3 + x, 10.899999999999999 + y, 6.300000000000001 + x, 12.799999999999999 + y, 4.1000000000000005 + x, 12.799999999999999 + y);
ctx.bezierCurveTo(1.9000000000000004 + x, 12.799999999999999 + y, 0 + x, 6.3 + y, 0 + x, 4.1 + y);
ctx.bezierCurveTo(0 + x, 1.8999999999999995 + y, 3 + x, 0 + y, 5.2 + x, 0 + y);
ctx.closePath();
}
function draw3(ctx, x, y) {
ctx.moveTo(5.9 + x, 6.3 + y);
ctx.arc(5.9 + x, 6.3 + y, 5.8, 0, 2 * Math.PI);
}
function update(dt) {
if (running) {
draw();
moveGrid();
}
window.requestAnimationFrame(update);
}
function moveGrid() {
waveStart = (waveStart + speed) % canvas.width;
waveEnd = (waveStart + waveWidth) % canvas.width;
}
function draw() {
ctx.fillStyle = '#dddddd';
ctx.fillRect(0, 0, canvas.width, canvas.height);
var x = 0;
// the roll logic is a bit dirty... sorry.
if (waveEnd < waveStart) {
x = waveEnd - waveWidth;
drawStrip(1, x, waveMargin);
x = waveEnd - waveWidth + waveMargin;
drawStrip(3, x, (waveWidth - (waveMargin * 2)));
x = waveEnd - waveMargin;
drawStrip(2, x, waveMargin);
x = waveEnd;
}
drawStrip(0, x, waveStart - x);
drawStrip(1, waveStart, waveMargin);
drawStrip(3, waveStart + waveMargin, waveWidth - (waveMargin * 2));
drawStrip(2, waveStart + (waveWidth - waveMargin), waveMargin);
drawStrip(0, waveEnd, canvas.width - Math.max(waveEnd, waveStart));
}
function drawStrip(state, x, w) {
if(x < 0) w = w + x;
if (w <= 0) return;
x = Math.max(x, 0);
ctx.drawImage(states[state],
Math.max(x, 0), 0, w, canvas.height,
Math.max(x, 0), 0, w, canvas.height
);
}
function getRandomArbitrary(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
:root,body,canvas {margin: 0}
<canvas id="bgCanvas"></canvas>

detecting a click inside a hexagon drawn using canvas?

I'm working on a certain layout where I need to draw a hexagon which needs to be clickable. I'm using the Path2D construct and isPointInPath function. I'm constructing an animation where a number of hexagons is created and then each moved to a certain position. After the movement is done, I am attaching onclick event handlers to certain hexagons. However there is weird behaviour.
Some initialized variables
const COLOR_DARK = "#73b6c6";
const COLOR_LIGHT = "#c3dadd";
const COLOR_PRIMARY = "#39a4c9";
const TYPE_PRIMARY = 'primary';
let hexagons = [];
Below is the function which draws the hexagons.
function drawHex(ctx, x, y, hexProps, stroke, color) {
let myPath = new Path2D();
myPath.moveTo(x + hexProps.width*0.5, y);
myPath.lineTo(x, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width*0.5, y + hexProps.height);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x + hexProps.width*0.5, y);
myPath.closePath();
if (stroke){
ctx.strokeStyle = color;
ctx.stroke(myPath);
} else {
ctx.fillStyle = color;
ctx.fill(myPath);
}
return myPath;
}
This function populates the hexagon array
function populateLeftHex(canvasWidth, canvasHeight, hexProps) {
const startX = canvasWidth / 2;
const startY = canvasHeight / 2;
const baseLeft = canvasWidth * 0.05;
for(let i = 0; i < 5; i++){
let hexNumber = (i % 4 == 0)? 2: 1;
for(let j = 0; j < hexNumber; j++){
hexagons.push({
startX: startX,
startY: startY,
endX: baseLeft + (2 * j) + ((i % 2 == 0)? (hexProps.width * j) : (hexProps.width/2)),
endY: ((i + 1) * hexProps.height) - ((i) * hexProps.height * hexProps.facShort) + (i* 2),
stroke: true,
color: ( i % 2 == 0 && j % 2 == 0)? COLOR_DARK : COLOR_LIGHT,
type: TYPE_PRIMARY
});
}
}
}
And here is where Im calling the isPointInPath function.
window.onload = function (){
const c = document.getElementById('canvas');
const canvasWidth = c.width = window.innerWidth,
canvasHeight = c.height = window.innerHeight,
ctx = c.getContext('2d');
window.requestAnimFrame = (function (callback) {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
console.log(canvasWidth);
let hexProps = {
width: canvasWidth * 0.075,
get height () {
return this.width/Math.sqrt(3) + (1.5)*(this.width/Math.sqrt(2)/2);
} ,
facShort: 0.225,
get facLong () {
return 1 - this.facShort;
}
};
populateLeftHex(canvasWidth, canvasHeight, hexProps);
let pct = 0;
const fps = 200;
animate();
function animate () {
setTimeout(function () {
// increment pct towards 100%
pct += .03;
// if we're not done, request another animation frame
if (pct < 1.00) {
requestAnimFrame(animate);
} else { //if pct is no longer less than 1.00, then the movement animation is over.
hexagons.forEach(function (hex) {
if(hex.type === TYPE_PRIMARY) {
console.info(hex.path);
c.onclick = function(e) {
let x = e.clientX - c.offsetLeft,
y = e.clientY - c.offsetTop;
console.info(ctx.isPointInPath(hex.path, (e.clientX - c.offsetLeft), (e.clientY - c.offsetTop) ));
};
}
})
}
ctx.clearRect(0, 0, c.width, c.height);
// draw all hexagons
for ( let i = 0; i < hexagons.length; i++) {
// get reference to next shape
let hex = hexagons[i];
// note: dx/dy are fixed values
// they could be put in the shape object for efficiency
let dx = hex.endX - hex.startX;
let dy = hex.endY - hex.startY;
let nextX = hex.startX + dx * pct;
let nextY = hex.startY + dy * pct;
hex = hexagons[i];
ctx.fillStyle = hex.color;
hex.path = drawHex(ctx, nextX, nextY, hexProps, hex.stroke, hex.color);
}
}, 1000 / fps);
}
Can you help me figure out what I'm doing wrong? Maybe I misunderstood how Path2D works? Thanks in advance.
Had to do a bit of work to build a test page as your example is incomplete, but this is working for me - though my hexagon is concave...
var myCanvas = document.getElementById("myCanvas");
var ctx = myCanvas.getContext("2d");
var hexProps = {width:100, height:100, facShort:-2, facLong:10};
var hexagons = [];
function drawHex(ctx, x, y, hexProps, stroke, color) {
let myPath = new Path2D();
myPath.moveTo(x + hexProps.width*0.5, y);
myPath.lineTo(x, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width*0.5, y + hexProps.height);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facLong);
myPath.lineTo(x + hexProps.width, y + hexProps.height*hexProps.facShort);
myPath.lineTo(x + hexProps.width*0.5, y);
myPath.closePath();
if (stroke){
ctx.strokeStyle = color;
ctx.stroke(myPath);
} else {
ctx.fillStyle = color;
ctx.fill(myPath);
}
return myPath;
}
hexagons.push({type:0, path:drawHex(ctx,100,100,hexProps,false,"#0f0")});
hexagons.forEach(function (hex) {
if(hex.type === 0) {
console.info(hex.path);
myCanvas.onclick = function(e) {
let x = e.clientX - myCanvas.offsetLeft,
y = e.clientY - myCanvas.offsetTop;
console.info(x,y);
console.info(ctx.isPointInPath(hex.path, (e.clientX -
myCanvas.offsetLeft), (e.clientY - myCanvas.offsetTop) ));
};
}
})
<canvas width=500 height=500 id=myCanvas style='border:1px solid red'></canvas>
Test clicks give true and false where expected:
test.htm:48 165 168
test.htm:49 true
test.htm:48 151 336
test.htm:49 false
test.htm:48 124 314
test.htm:49 true
test.htm:48 87 311
test.htm:49 false

Canvas flood fill not filling to edge

I am using a flood fill algorithm to fill in circles drawn on the canvas. The issue I am having is that the algorithm isn't filling right up to the edge of the circle.
Here is the algorithm based on this blog post:
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);
}
Please see the JSFiddle or the below image to see what I mean. Clicking inside any circles will change the colour between yellow and black (the issue is far more visible with black).
I've read that the issue could be something to do with the anti-aliasing and I have tried turning it off with context1.imageSmoothingEnabled = true; but it didn't make a difference.
I have also tried changing my matchStartColour function as per this question but that doesn't help.
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);
}
I think it might have something to do with the fact that the circles have no fill colour and the background of the canvas isn't white but it is transparent black. I have tried changing the canvas background to white but that also didn't help.
Use flood fill to create a Mask
I just happened to do a floodFill the other day that addresses the problem of antialiased edges.
Rather than paint to the canvas directly, I paint to a byte array that is then used to create a mask. The mask allows for the alpha values to be set.
The fill can have a tolerance and a toleranceFade that control how it deals with colours that approch the tolerance value.
When pixel's difference between the start colour and tolerance are greater than (tolerance - toleranceFade) I set the alpha for that pixel to 255 - ((differance - (tolerance - toleranceFade)) / toleranceFade) * 255 which creates a nice smooth blend at the edges of lines. Though it does not work for all situations for high contrast situations it is an effective solution.
The example below shows the results of with and without the toleranceFade. The blue is without the toleranceFade, the red is with the tolerance set at 190 and the toleranceFade of 90.
You will have to play around with the setting to get the best results for your needs.
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>
For more info see readme at Github FloodFill2D

Where should I place this condition?

For the last step of this project, I want the growing circle to stop when it collides with another circle. The isOnCircle function already checks for this successfully when creating a new circle. However, when adding the condition !isOnCircle to my grow() function (line 61) it prevents any new circles from being added.
function grow() {
var a = circles[circles.length - 1];
if (!isOnCircle(a)){
a.radius += 1;
}}
Perhaps the circle is being created first, then in the check for collision, it's colliding with itself. Where else could I put the !isOnCircle check so that it gets checked at every radius increase and stops the grow function then?
check this
//set up canvas
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var circles = [];
//create circle
function create(location) {
circles.push({
x: location.x,
y: location.y,
radius: 10,
color: '#' + Math.floor(Math.random() * 16777215).toString(16)
});
}
//figure out mouse position
var rect = document.getElementById("canvas").getBoundingClientRect();
// Get canvas offset on page
var offset = {
x: rect.left,
y: rect.top
};
function isOnCanvas(a) {
if ((a.x >= 0 && a.x <= rect.width) && (a.y >= 0 && a.y <= rect.height)) {
return true;
}
return false;
}
function isOnCircle(a) {
var i = 0,
l = circles.length,
x, y, d, c;
for (; i < l; ++i) {
c = circles[i];
x = a.x - c.x;
y = a.y - c.y;
d = (a.radius || 10) + c.radius;
if (x * x + y * y <= d * d) {
return true;
}
}
return false;
}
// draw all circles
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < circles.length; i++) {
var p = circles[i];
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI);
ctx.fillStyle = p.color;
ctx.fill();
}
}
//make last drawn circle 1px bigger
function grow() {
var a = circles[circles.length - 1];
a.radius += 1;
}
//find percentage of canvas filled in
var totalSpace = canvas.width * canvas.height;
var totalFilled = function () {
total = 0;
for (var i = 0; i < circles.length; i++) {
var p = circles[i];
total += Math.PI * Math.pow(p.radius, 2);
}
return total;
console.log(total);
}
function findPercentage() {
return (totalFilled() / totalSpace) * 100;
}
function updateInfo() {
percentage = findPercentage();
document.getElementById("percentage").innerHTML = "You've filled in " + percentage.toFixed(1) + "%";
}
//do all the stuff
var animate = function () {
grow();
draw();
updateInfo();
}
//put this outside function so we can stop it later
var growLoop;
window.onmousedown = function (e) {
// get event location on page offset by canvas location
var location = {
x: e.pageX - offset.x,
y: e.pageY - offset.y
};
if (isOnCanvas(location) && !isOnCircle(location)) {
create(location);
draw();
updateInfo();
growLoop = setInterval(animate, 100);
}
};
window.onmouseup = function () {
clearInterval(growLoop);
}
window.onmouseout = function () {
clearInterval(growLoop);
}
it's colliding with itself.
Probably. You definitely should avoid that in the collision detection:
function isOnCircle(a) {
var l = circles.length,
x, y, d, c;
for (var i = 0; i < l; ++i) {
c = circles[i];
if (a == c) // add these
continue; // two lines!
x = a.x - c.x;
y = a.y - c.y;
d = (a.radius || 10) + c.radius;
if (x * x + y * y <= d * d) {
return true;
}
}
return false;
}
It is colliding with itself. Since you know the current circle will always be the last one in circles you can modify isOnCircle like this:
l = circles.length - 1,
so that it won't check against the current circle.
http://jsfiddle.net/SeAGU/91/

Categories