Related
I need to create line segments within a shape and not just a visual pattern - I need to know start and end coordinates for those lines that are within a given boundary (shape). I'll go through what I have and explain the issues I'm facing
I have a closed irregular shape (can have dozens of sides) defined by [x, y] coordinates
shape = [
[150,10], // x, y
[10,300],
[150,200],
[300,300]
];
I calculate and draw the bounding box of this shape
I then draw my shape on the canvas
Next, I cast rays within the bounding box with a set spacing between each ray. The ray goes from left to right incrementing by 1 pixel.
Whenever a cast ray gets to a pixel with RGB values of 100, 255, 100 I then know it has entered the shape. I know when it exits the shape if the pixel value is not 100, 255, 100. Thus I know start and end coordinates for each line within my shape and if one ray enters and exits the shape multiple times - this will generate all line segments within that one ray cast.
For the most part it works but there are issues:
It's very slow. Perhaps there is a better way than casting rays? Or perhaps there is a way to optimize the ray logic? Perhaps something more intelligent than just checking for RGB color values?
How do I cast rays at a different angle within the bounding box? Now it's going left to right, but how would I fill my bounding box with rays cast at any specified angle? i.e.:
I don't care about holes or curves. The shapes will all be made of straight line segments and won't have any holes inside them.
Edit: made changes to the pixel RGB sampling that improve performance.
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
lineSpacing = 15;
shape = [
[150,10], // x, y
[10,300],
[150,200],
[300,300]
];
boundingBox = [
[Infinity,Infinity],
[-Infinity,-Infinity]
]
// get bounding box coords
for(var i in shape) {
if(shape[i][0] < boundingBox[0][0]) boundingBox[0][0] = shape[i][0];
if(shape[i][1] < boundingBox[0][1]) boundingBox[0][1] = shape[i][1];
if(shape[i][0] > boundingBox[1][0]) boundingBox[1][0] = shape[i][0];
if(shape[i][1] > boundingBox[1][1]) boundingBox[1][1] = shape[i][1];
}
// display bounding box
ctx.fillStyle = 'rgba(255,0,0,.2)';
ctx.fillRect(boundingBox[0][0], boundingBox[0][1], boundingBox[1][0]-boundingBox[0][0], boundingBox[1][1]-boundingBox[0][1]);
// display shape (boundary)
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
for(var i = 1; i < shape.length; i++) {
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.fillStyle = 'rgba(100,255,100,1)';
ctx.fill();
canvasData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
// loop through the shape in vertical slices
for(var i = boundingBox[0][1]+lineSpacing; i <= boundingBox[1][1]; i += lineSpacing) {
// send ray from left to right
for(var j = boundingBox[0][0], start = false; j <= boundingBox[1][0]; j++) {
x = j, y = i;
pixel = y * (canvas.width * 4) + x * 4;
// if pixel is within boundary (shape)
if(canvasData[pixel] == 100 && canvasData[pixel+1] == 255 && canvasData[pixel+2] == 100) {
// arrived at start of boundary
if(start === false) {
start = [x,y]
}
} else {
// arrived at end of boundary
if(start !== false) {
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.beginPath();
ctx.moveTo(start[0], start[1]);
ctx.lineTo(x, y);
ctx.closePath();
ctx.stroke();
start = false;
}
}
}
// show entire cast ray for debugging purposes
ctx.strokeStyle = 'rgba(0,0,0,.2)';
ctx.beginPath();
ctx.moveTo(boundingBox[0][0], i);
ctx.lineTo(boundingBox[1][0], i);
ctx.closePath();
ctx.stroke();
}
<canvas id="canvas" width="350" height="350"></canvas>
This is a pretty complex problem that I am trying to simplify as much as possible. Using the line intersection formula we can determin where the ray intersects with the shape at every edge. What we can do is loop through each side of the shape while check every rays intersection. If they intersect we push those coordinates to an array.
I have tried to make this as dynamic as possible. You can pass the shape and change the number of rays and the angle. As for the angle it doesn't take a specific degree (i.e. 45) but rather you change the start and stop y axis. I'm sure if you must have the ability to put in a degree we can do that.
It currently console logs the array of intersecting coordinates but you can output them however you see fit.
The mouse function is just to verify that the number match up. Also be aware I am using toFixed() to get rid of lots of decimals but it does convert to a string. If you need an integer you'll have to convert back.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d")
canvas.width = 300;
canvas.height = 300;
ctx.fillStyle = "violet";
ctx.fillRect(0,0,canvas.width,canvas.height)
//Shapes
let triangleish = [
[150,10], // x, y
[10,300],
[150,200],
[300,300]
]
let star = [ [ 0, 85 ], [ 75, 75 ], [ 100, 10 ], [ 125, 75 ],
[ 200, 85 ], [ 150, 125 ], [ 160, 190 ], [ 100, 150 ],
[ 40, 190 ], [ 50, 125 ], [ 0, 85 ] ];
let coords = [];
//Class that draws the shape on canvas
function drawShape(arr) {
ctx.beginPath();
ctx.fillStyle = "rgb(0,255,0)";
ctx.moveTo(arr[0][0], arr[0][1]);
for (let i=1;i<arr.length;i++) {
ctx.lineTo(arr[i][0], arr[i][1]);
}
ctx.fill();
ctx.closePath();
}
//pass the shape in here to draw it
drawShape(star)
//Class to creat the rays.
class Rays {
constructor(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.w = canvas.width;
this.h = 1;
}
draw() {
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.moveTo(this.x1, this.y1)
ctx.lineTo(this.x2, this.y2)
ctx.stroke();
ctx.closePath();
}
}
let rays = [];
function createRays(angle) {
let degrees = angle * (Math.PI/180)
//I am currently creating an array every 10px on the Y axis
for (let i=0; i < angle + 45; i++) {
//The i will be your start and stop Y axis. This is where you can change the angle
let cx = canvas.width/2 + (angle*2);
let cy = i * 10;
let x1 = (cx - 1000 * Math.cos(degrees));
let y1 = (cy - 1000 * Math.sin(degrees));
let x2 = (cx + 1000 * Math.cos(degrees));
let y2 = (cy + 1000 * Math.sin(degrees));
rays.push(new Rays(x1, y1, x2, y2))
}
}
//enter angle here
createRays(40);
//function to draw the rays after crating them
function drawRays() {
for (let i=0;i<rays.length; i++) {
rays[i].draw();
}
}
drawRays();
//This is where the magic happens. Using the line intersect formula we can determine if the rays intersect with the objects sides
function intersectLines(coord1, coord2, rays) {
let x1 = coord1[0];
let x2 = coord2[0];
let y1 = coord1[1];
let y2 = coord2[1];
let x3 = rays.x1;
let x4 = rays.x2;
let y3 = rays.y1;
let y4 = rays.y2;
//All of this comes from Wikipedia on line intersect formulas
let d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
if (d == 0) {
return
}
let t = ((x1 - x3)*(y3 - y4) - (y1 - y3)*(x3 - x4)) / d;
let u = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;
//if this statement is true then the lines intersect
if (t > 0 && t < 1 && u > 0) {
//I have currently set it to fixed but if a string does not work for you you can change it however you want.
//the first formula is the X coord of the interect the second is the Y
coords.push([(x1 + t*(x2 - x1)).toFixed(2),(y1 + t*(y2 - y1)).toFixed(2)])
}
return
}
//function to call the intersect function by passing in the shapes sides and each ray
function callIntersect(shape) {
for (let i=0;i<shape.length;i++) {
for (let j=0;j<rays.length;j++) {
if (i < shape.length - 1) {
intersectLines(shape[i], shape[i+1], rays[j]);
} else {
intersectLines(shape[0], shape[shape.length - 1], rays[j]);
}
}
}
}
callIntersect(star);
//just to sort them by the Y axis so they they show up as in-and-out
function sortCoords() {
coords.sort((a, b) => {
return a[1] - b[1];
});
}
sortCoords()
console.log(coords)
//This part is not needed only added to verify number matched the mouse posit
let mouse = {
x: undefined,
y: undefined
}
let canvasBounds = canvas.getBoundingClientRect();
addEventListener('mousemove', e => {
mouse.x = e.x - canvasBounds.left;
mouse.y = e.y - canvasBounds.top;
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawCoordinates();
})
function drawCoordinates() {
ctx.font = '15px Arial';
ctx.fillStyle = 'black';
ctx.fillText('x: '+mouse.x+' y: '+mouse.y, mouse.x, mouse.y)
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "violet";
ctx.fillRect(0,0,canvas.width,canvas.height)
for (let i=0;i<rays.length; i++) {
rays[i].draw();
}
drawShape(star)
drawCoordinates();
requestAnimationFrame(animate)
}
animate()
<canvas id="canvas"></canvas>
I'm not an expert, but maybe you could do something like this:
Generate the points that constitute the borders.
Organize them in a convenient structure, e.g. an object with the y as key, and an array of x as values.
2.1. i.e. each item in the object would constitute all points of all borders in a single y.
Iterate over the object and generate the segments for each y.
3.1. e.g. if the array of y=12 contains [ 10, 20, 60, 80 ] then you would generate two segments: [ 10, 12 ] --> [ 20, 12 ] and [ 60, 12 ] --> [ 80, 12 ].
To generate the borders' points (and to answer your second question), you can use the line function y = a*x + b.
For example, to draw a line between [ 10, 30 ] and [ 60, 40 ], you would:
Solve a and b by substituting x and y for both points and combining these two formulas (with standard algebra):
For point #1: 30 = a*10 + b
For point #2: 40 = a*60 + b
b = 30 - a*10
40 = a*60 + (30 - a*10)
a*60 - a*10 = 40 - 30
50*a = 10
a = 0.2
30 = a*10 + b
30 = 0.2*10 + b
b = 30 - 2
b = 28
With a and b at hand, you get the function for your specific line:
y = 0.2*x + 28
With that, you can calculate the point of the line for any y. So, for example, the x of the point right under the first point ([ 10, 30 ]) would have a y of 31, and so: 31 = 0.2*x + 28, and so: x = 15. So you get: [ 15, 31 ].
You may need a bit of special handling for:
Vertical lines, because the slope is "infinite" and calculating it would cause division by zero.
Rounding issues. For some (probably most) pixels you will get real x values (i.e. non-integer). You can Math.round() them, but it can cause issues, like:
8.1. Diagonal rays may not actually hit a border point even when they go through a border. This will probably require additional handling (like checking points around and not just exactly the pixels the ray lies on).
8.2. The points your algorithm generate may (slightly) differ from the points that appear on the screen when you use libraries or built-in browser functionality to draw the shape (depending on the implementation of their drawing algorithms).
This is a mashup of Justin's answer and code from my proposed question.
One issue was generating rays at a set angle and a set distance from each other. To have rays be equal distances apart at any angle we can use a vector at a 90 degree angle and then place a new center point for the next line.
We can start at the exact midpoint of our boundary and then spread out on either side.
Red line is the center line, green dots are the vector offset points for the next line.
Next I modified Justin's intersect algorithm to iterate by ray and not side, that way I get interlaced coordinates where array[index] is the start point of a segment and array[index+1] is the end point.
And by connecting the lines we get a shape that is filled with lines inside its boundaries at set distances apart
Issues:
I had to inflate the boundary by 1 pixel otherwise certain shapes would fail to generate paths
I'd like rays to be some what aligned. It's hard to explain, but here's an example of 6 triangles rotated at 60 degree increments that form a hexagon with their inner lines also offset by 60 degree increments. The top and bottom triangle inner lines do not join those of the outside triangles. This is an issue with the cast rays. Ideally I'd like them to join and be aligned with the outer most edge if that makes sense. Surely there is a better way to cast rays than this...
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
lineSpacing = 12;
angle = 45;
shapes = [
[[143.7,134.2], [210.4,18.7], [77.1,18.7]],
[[143.7,134.2], [77.1,18.7], [10.4,134.2]],
[[143.7,134.2], [10.4,134.2], [77.1,249.7]],
[[143.7,134.2], [77.1,249.7], [210.4,249.7]],
[[143.7,134.2], [210.4,249.7], [277.1,134.2]],
[[143.7,134.2], [277.1,134.2], [210.4,18.7]]
];
for(var i in shapes) {
lines = getLineSegments(shapes[i], 90+(-60*i), lineSpacing);
for(var i = 0; i < lines.length; i += 2) {
start = lines[i];
end = lines[i+1];
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.moveTo(start[0], start[1]);
ctx.lineTo(end[0], end[1]);
ctx.closePath();
ctx.stroke();
}
}
function getLineSegments(shape, angle, lineSpacing) {
boundingBox = [
[Infinity,Infinity],
[-Infinity,-Infinity]
]
// get bounding box coords
for(var i in shape) {
if(shape[i][0] < boundingBox[0][0]) boundingBox[0][0] = shape[i][0];
if(shape[i][1] < boundingBox[0][1]) boundingBox[0][1] = shape[i][1];
if(shape[i][0] > boundingBox[1][0]) boundingBox[1][0] = shape[i][0];
if(shape[i][1] > boundingBox[1][1]) boundingBox[1][1] = shape[i][1];
}
boundingBox[0][0] -= 1, boundingBox[0][1] -= 1;
boundingBox[1][0] += 1, boundingBox[1][1] += 1;
// display shape (boundary)
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
for(var i = 1; i < shape.length; i++) {
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.fillStyle = 'rgba(100,255,100,1)';
ctx.fill();
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
rayPaths = [];
path = getPathCoords(boundingBox, 0, 0, angle);
rayPaths.push(path);
/*ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
getPaths:
for(var i = 0, lastPaths = [path, path]; true; i++) {
for(var j = 0; j < 2; j++) {
pathMidX = (lastPaths[j][0][0] + lastPaths[j][1][0]) / 2;
pathMidY = (lastPaths[j][0][1] + lastPaths[j][1][1]) / 2;
pathVectorX = lastPaths[j][1][1] - lastPaths[j][0][1];
pathVectorY = lastPaths[j][1][0] - lastPaths[j][0][0];
pathLength = Math.sqrt(pathVectorX * pathVectorX + pathVectorY * pathVectorY);
pathOffsetPointX = pathMidX + ((j % 2 === 0 ? pathVectorX : -pathVectorX) / pathLength * lineSpacing);
pathOffsetPointY = pathMidY + ((j % 2 === 0 ? -pathVectorY : pathVectorY) / pathLength * lineSpacing);
offsetX = pathOffsetPointX-boundingMidX;
offsetY = pathOffsetPointY-boundingMidY;
path = getPathCoords(boundingBox, offsetX, offsetY, angle);
if(
path[0][0] < boundingBox[0][0] ||
path[1][0] > boundingBox[1][0] ||
path[0][0] > boundingBox[1][0] ||
path[1][0] < boundingBox[0][0]
) break getPaths;
/*ctx.fillStyle = 'green';
ctx.fillRect(pathOffsetPointX-2.5, pathOffsetPointY-2.5, 5, 5);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
rayPaths.push(path);
lastPaths[j] = path;
}
}
coords = [];
function intersectLines(coord1, coord2, rays) {
x1 = coord1[0], x2 = coord2[0];
y1 = coord1[1], y2 = coord2[1];
x3 = rays[0][0], x4 = rays[1][0];
y3 = rays[0][1], y4 = rays[1][1];
d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
if (d == 0) return;
t = ((x1 - x3)*(y3 - y4) - (y1 - y3)*(x3 - x4)) / d;
u = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;
if (t > 0 && t < 1 && u > 0) {
coords.push([(x1 + t*(x2 - x1)).toFixed(2),(y1 + t*(y2 - y1)).toFixed(2)])
}
return;
}
function callIntersect(shape) {
for (var i = 0; i < rayPaths.length; i++) {
for (var j = 0; j< shape.length; j++) {
if (j < shape.length - 1) {
intersectLines(shape[j], shape[j+1], rayPaths[i]);
} else {
intersectLines(shape[0], shape[shape.length - 1], rayPaths[i]);
}
}
}
}
callIntersect(shape);
return coords;
}
function getPathCoords(boundingBox, offsetX, offsetY, angle) {
coords = [];
// add decimal places otherwise can lead to Infinity, subtract 90 so 0 degrees is at the top
angle = angle + 0.0000000000001 - 90;
boundingBoxWidth = boundingBox[1][0] - boundingBox[0][0];
boundingBoxHeight = boundingBox[1][1] - boundingBox[0][1];
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
x = boundingMidX + offsetX, y = boundingMidY + offsetY;
dx = Math.cos(Math.PI * angle / 180);
dy = Math.sin(Math.PI * angle / 180);
for(var i = 0; i < 2; i++) {
bx = (dx > 0) ? boundingBoxWidth+boundingBox[0][0] : boundingBox[0][0];
by = (dy > 0) ? boundingBoxHeight+boundingBox[0][1] : boundingBox[0][1];
if(dx == 0) ix = x, iy = by;
if(dy == 0) iy = y, ix = bx;
tx = (bx - x) / dx;
ty = (by - y) / dy;
if(tx <= ty) {
ix = bx, iy = y + tx * dy;
} else {
iy = by, ix = x + ty * dx;
}
coords.push([ix, iy]);
dx = -dx;
dy = -dy;
}
return coords;
}
<canvas id="canvas" width="500" height="500"></canvas>
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
lineSpacing = 10;
angle = 45;
shape = [
[200,10], // x, y
[10,300],
[200,200],
[400,300]
];
lines = getLineSegments(shape, angle, lineSpacing);
for(var i = 0; i < lines.length; i += 2) {
start = lines[i];
end = lines[i+1];
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.moveTo(start[0], start[1]);
ctx.lineTo(end[0], end[1]);
ctx.closePath();
ctx.stroke();
}
function getLineSegments(shape, angle, lineSpacing) {
boundingBox = [
[Infinity,Infinity],
[-Infinity,-Infinity]
]
// get bounding box coords
for(var i in shape) {
if(shape[i][0] < boundingBox[0][0]) boundingBox[0][0] = shape[i][0];
if(shape[i][1] < boundingBox[0][1]) boundingBox[0][1] = shape[i][1];
if(shape[i][0] > boundingBox[1][0]) boundingBox[1][0] = shape[i][0];
if(shape[i][1] > boundingBox[1][1]) boundingBox[1][1] = shape[i][1];
}
boundingBox[0][0] -= 1, boundingBox[0][1] -= 1;
boundingBox[1][0] += 1, boundingBox[1][1] += 1;
// display bounding box
ctx.fillStyle = 'rgba(255,0,0,.2)';
ctx.fillRect(boundingBox[0][0], boundingBox[0][1], boundingBox[1][0]-boundingBox[0][0], boundingBox[1][1]-boundingBox[0][1]);
// display shape (boundary)
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
for(var i = 1; i < shape.length; i++) {
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.fillStyle = 'rgba(100,255,100,1)';
ctx.fill();
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
rayPaths = [];
path = getPathCoords(boundingBox, 0, 0, angle);
rayPaths.push(path);
/*ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
getPaths:
for(var i = 0, lastPaths = [path, path]; true; i++) {
for(var j = 0; j < 2; j++) {
pathMidX = (lastPaths[j][0][0] + lastPaths[j][1][0]) / 2;
pathMidY = (lastPaths[j][0][1] + lastPaths[j][1][1]) / 2;
pathVectorX = lastPaths[j][1][1] - lastPaths[j][0][1];
pathVectorY = lastPaths[j][1][0] - lastPaths[j][0][0];
pathLength = Math.sqrt(pathVectorX * pathVectorX + pathVectorY * pathVectorY);
pathOffsetPointX = pathMidX + ((j % 2 === 0 ? pathVectorX : -pathVectorX) / pathLength * lineSpacing);
pathOffsetPointY = pathMidY + ((j % 2 === 0 ? -pathVectorY : pathVectorY) / pathLength * lineSpacing);
offsetX = pathOffsetPointX-boundingMidX;
offsetY = pathOffsetPointY-boundingMidY;
path = getPathCoords(boundingBox, offsetX, offsetY, angle);
if(
path[0][0] < boundingBox[0][0] ||
path[1][0] > boundingBox[1][0] ||
path[0][0] > boundingBox[1][0] ||
path[1][0] < boundingBox[0][0]
) break getPaths;
/*ctx.fillStyle = 'green';
ctx.fillRect(pathOffsetPointX-2.5, pathOffsetPointY-2.5, 5, 5);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
rayPaths.push(path);
lastPaths[j] = path;
}
}
coords = [];
function intersectLines(coord1, coord2, rays) {
x1 = coord1[0], x2 = coord2[0];
y1 = coord1[1], y2 = coord2[1];
x3 = rays[0][0], x4 = rays[1][0];
y3 = rays[0][1], y4 = rays[1][1];
d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
if (d == 0) return;
t = ((x1 - x3)*(y3 - y4) - (y1 - y3)*(x3 - x4)) / d;
u = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;
if (t > 0 && t < 1 && u > 0) {
coords.push([(x1 + t*(x2 - x1)).toFixed(2),(y1 + t*(y2 - y1)).toFixed(2)])
}
return;
}
function callIntersect(shape) {
for (var i = 0; i < rayPaths.length; i++) {
for (var j = 0; j< shape.length; j++) {
if (j < shape.length - 1) {
intersectLines(shape[j], shape[j+1], rayPaths[i]);
} else {
intersectLines(shape[0], shape[shape.length - 1], rayPaths[i]);
}
}
}
}
callIntersect(shape);
return coords;
}
function getPathCoords(boundingBox, offsetX, offsetY, angle) {
coords = [];
// add decimal places otherwise can lead to Infinity, subtract 90 so 0 degrees is at the top
angle = angle + 0.0000000000001 - 90;
boundingBoxWidth = boundingBox[1][0] - boundingBox[0][0];
boundingBoxHeight = boundingBox[1][1] - boundingBox[0][1];
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
x = boundingMidX + offsetX, y = boundingMidY + offsetY;
dx = Math.cos(Math.PI * angle / 180);
dy = Math.sin(Math.PI * angle / 180);
for(var i = 0; i < 2; i++) {
bx = (dx > 0) ? boundingBoxWidth+boundingBox[0][0] : boundingBox[0][0];
by = (dy > 0) ? boundingBoxHeight+boundingBox[0][1] : boundingBox[0][1];
if(dx == 0) ix = x, iy = by;
if(dy == 0) iy = y, ix = bx;
tx = (bx - x) / dx;
ty = (by - y) / dy;
if(tx <= ty) {
ix = bx, iy = y + tx * dy;
} else {
iy = by, ix = x + ty * dx;
}
coords.push([ix, iy]);
dx = -dx;
dy = -dy;
}
return coords;
}
<canvas id="canvas" width="500" height="500"></canvas>
I found this function on Stackoverflow some time ago and only now am I realizing that it sometimes seems to give false-positives. Here it is:
function lineIntersectsCircle(p1X, p1Y, p2X, p2Y, cX, cY, r) {
let xDelta = p1X - p2X;
let yDelta = p1Y - p2Y;
let distance = Math.sqrt(xDelta * xDelta + yDelta * yDelta)
let a = (cY - p1Y) * (p2X - p1X);
let b = (cX - p1X) * (p2Y - p1Y);
return Math.abs(a - b) / distance <= r;
}
Here's a full code demo reproduction showing the issue here:
let ctx = document.querySelector("canvas").getContext("2d");
function drawCircle(xCenter, yCenter, radius) {
ctx.beginPath();
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);
ctx.fill();
}
function drawLine(x1, y1, x2, y2) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
function lineIntersectsCircle(p1X, p1Y, p2X, p2Y, cX, cY, r) {
let xDelta = p1X - p2X;
let yDelta = p1Y - p2Y;
let distance = Math.sqrt(xDelta * xDelta + yDelta * yDelta)
let a = (cY - p1Y) * (p2X - p1X);
let b = (cX - p1X) * (p2Y - p1Y);
return Math.abs(a - b) / distance <= r;
}
let circleX = 250;
let circleY = 250;
let circleR = 50;
let lineX1 = 50;
let lineY1 = 350;
let lineX2 = 185;
let lineY2 = 250;
draw = () => {
ctx.fillStyle = "#b2c7ef";
ctx.fillRect(0, 0, 800, 800);
ctx.fillStyle = "#ffffff";
drawCircle(circleX, circleY, circleR);
drawLine(lineX1, lineY1, lineX2, lineY2);
}
console.log(lineIntersectsCircle(lineX1, lineY1, lineX2, lineY2, circleX, circleY, circleR))
draw();
canvas { display: flex; margin: 0 auto; }
<canvas width="400" height="400"></canvas>
As you can see, the line doesn't intersect the circle, yet it console logs a true statement. Does anyone know why this would be? If this function is incorrect, what is the proper function for only determining if a line and circle intersect? I do not need the intersection points, only whether or not they intersect at all.
Mathematically, a line is different from a line segment; a line is infinitely long.
You use the formula for finding the distance between a point (centre of the circle) and a line. While the formula uses a line defined by two points, it is not terminated by those points, so that doesn't apply to a line segment.
If you extend that line segment out, you can see that it intersects with the circle.
Three steps to determine whether a line segment intersects with a circle:
transform the coordinates to make (xCenter, yCenter) of the circle as the zero point, and switch the X,Y coordinates (because the Y axis of the canvas points downward, which makes linear functions incorrect);
find the point nearest to the circle in the (infinite) line, if the point is not inside the line segment, get one of the end points which is closer to the point;
if the point is inside the circle, and the 2 end points are not both inside the circle, there is at least one intersection.
let ctx = document.querySelector("canvas").getContext("2d");
function drawCircle(xCenter, yCenter, radius) {
ctx.beginPath();
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);
ctx.fill();
}
function drawLine(x1, y1, x2, y2) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
function lineIntersectsCircle(p1X, p1Y, p2X, p2Y, cX, cY, r) {
// to calculate the new position based on the new zero point at the circle center,
//where X, Y coordinates have to be switched because in Canvas Y coordinate increases downwards
let newp1y = p1X - cX, newp1x = p1Y - cY, newp2y = p2X - cX, newp2x = p2Y - cY;
// when the 2 end points are all inside the circle, there is no intersection
if((newp1x*newp1x + newp1y*newp1y < r*r) && (newp2x*newp2x + newp2y*newp2y < r*r)) {
return false;
}
// slope of the line and the slope of the perpendicular line from the circle center
let slopeL = (newp2y - newp1y) / (newp2x - newp1x), slopeC;
if(slopeL != 0){
slopeC = -1/slopeL;
}
else{
slopeC = 65535; // for a vertical line, this slope number is big enough
}
// calculate the nearest point at the straight line from the circle center
let closeX = (newp1y - slopeL*newp1x)/(slopeC - slopeL);
let closeY = closeX * slopeC;
// in this condition, the nearest point is not inside the line segment, so the end point
// which is closer to this point will be picked as the real nearest point to the circle center
if((closeX - newp1x)*(closeX - newp2x) >=0 && (closeY - newp1y)*(closeY - newp2y) >=0){
if((closeX - newp1x)*(closeX - newp2x) > 0){
if(Math.abs(closeX - newp1x) > Math.abs(closeX - newp2x)){
closeX = newp2x;
closeY = newp2y;
}
else{
closeX = newp1x;
closeY = newp1y;
}
}
else {
if(Math.abs(closeY - newp1y) > Math.abs(closeY - newp2y)){
closeX = newp2x;
closeY = newp2y;
}
else{
closeX = newp1x;
closeY = newp1y;
}
}
}
//check if the picked nearest point is inside the circle
return (closeX*closeX + closeY*closeY) < r*r;
}
let circleX = 250;
let circleY = 250;
let circleR = 50;
let lineX1 = 50;
let lineY1 = 350;
let lineX2 = 185;
let lineY2 = 250;
draw = () => {
ctx.fillStyle = "#b2c7ef";
ctx.fillRect(0, 0, 800, 800);
ctx.fillStyle = "#ffffff";
drawCircle(circleX, circleY, circleR);
drawLine(lineX1, lineY1, lineX2, lineY2);
}
console.log(lineIntersectsCircle(lineX1, lineY1, lineX2, lineY2, circleX, circleY, circleR))
draw();
canvas { display: flex; margin: 0 auto; }
<canvas width="400" height="400"></canvas>
I am using the following code to draw on HTML5 canvas:
const context = canvas.getContext('2d');
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
context.fillStyle = color;
context.fill();
context.closePath();
However, if I print unique values:
console.log(new Set(context.getImageData(0, 0, canvas.width, canvas.height).data))
I can see that the color that I use in fillStyle gets interpolated.
I tried to disable interpolation/smoothing by adding the following flags:
context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.mozImageSmoothingEnabled = false;
However, it does not help. I would highly appreciate if you could advise me how to fix the issue.
The is no native way to draw circles that are pixelated. To do that you must render each pixel manually.
There are several methods you can use to do this. The most common have some additional artifacts (like inconsistent line width) that are hard to avoid.
The following function draw a circle using a modification of the Berzingham line algorithm (also good for rendering pixelated lines) called the Midpoint circle algorithm
Unfortunately most of the methods that can draw arbitrary lines and circle are slow. The two mentioned above are the fastest standard methods I know about.
Example
The example defines 3 functions to draw pixelated circles
pixelPixelatedCircle (Red outer circles and single blue in example) draws a single pixel wide circle using the current fill style
fillPixelatedCircle (Red inner circle in example) draws a a solid circle using the current fill style
strokePixelatedCircle (Black circles in example) draws a circle line with a width. Not the width only works when it is >= 2. If you want a single pixel width use the first function. Also not that this function uses a second canvas to render the circle
The example draws all three types
The outer red circle drawn using pixelPixelatedCircle are to demonstrate that the quality of the circles are consistent. There should be alternating 1 pixel width circles, red and dark red. and an outer blue just touching the canvas edge circles.
For circles less than radius of 2 use ctx.rect as the outcome will be the same.
Note the circle radius is an integer thus a circle radius 1000 will be identical to circle radius 1000.9 The sample applies to the circle center. To be able to have sub pixel positioning and radius will need another algorithm which is slower and has lower quality lines.
Note I added a simple zoom canvas so I could see the results better, I was going to remove it but left it in just for interested people. It is not crucial to the answer.
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const size = Math.min(w, h);
const circleWorkCanvas = document.createElement("canvas");
const cCtx = circleWorkCanvas.getContext("2d");
function resizeCircleCanvas(ctx) {
if (circleWorkCanvas.width !== ctx.canvas.width || circleWorkCanvas.height !== ctx.canvas.height) {
circleWorkCanvas.width = ctx.canvas.width;
circleWorkCanvas.height = ctx.canvas.height;
}
}
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.35, 5);
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.3, 4);
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.25, 3);
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.2, 2);
ctx.fillStyle = "red";
fillPixelatedCircle(ctx, w / 2, h / 2, size * 0.15);
ctx.fillStyle = "blue";
pixelPixelatedCircle(ctx, w / 2, h / 2, size * 0.38);
ctx.fillStyle = "blue";
pixelPixelatedCircle(ctx, w / 2, h / 2, size * 0.5);
ctx.fillStyle = "red";
for(let v = 0.40; v < 0.49; v += 1 / size) {
ctx.fillStyle = "#600"
pixelPixelatedCircle(ctx, w / 2, h / 2, size * v);
ctx.fillStyle = "#F00"
v += 1 / size;
pixelPixelatedCircle(ctx, w / 2, h / 2, size * v );
}
function strokePixelatedCircle(ctx, cx, cy, r, lineWidth) {
resizeCircleCanvas(ctx);
cCtx.clearRect(0, 0, cCtx.canvas.width, cCtx.canvas.height);
cCtx.globalCompositeOperation = "source-over";
cCtx.fillStyle = ctx.strokeStyle;
fillPixelatedCircle(cCtx, cx, cy, r + lineWidth / 2);
cCtx.globalCompositeOperation = "destination-out";
fillPixelatedCircle(cCtx, cx, cy, r - lineWidth / 2);
cCtx.globalCompositeOperation = "source-over";
ctx.drawImage(cCtx.canvas, 0, 0);
}
function fillPixelatedCircle(ctx, cx, cy, r){
r |= 0; // floor radius
ctx.setTransform(1,0,0,1,0,0); // ensure default transform
var x = r, y = 0, dx = 1, dy = 1;
var err = dx - (r << 1);
var x0 = cx - 1| 0, y0 = cy | 0;
var lx = x,ly = y;
ctx.beginPath();
while (x >= y) {
ctx.rect(x0 - x, y0 + y, x * 2 + 2, 1);
ctx.rect(x0 - x, y0 - y, x * 2 + 2, 1);
if (x !== lx){
ctx.rect(x0 - ly, y0 - lx, ly * 2 + 2, 1);
ctx.rect(x0 - ly, y0 + lx, ly * 2 + 2, 1);
}
lx = x;
ly = y;
y++;
err += dy;
dy += 2;
if (err > 0) {
x--;
dx += 2;
err += (-r << 1) + dx;
}
}
if (x !== lx) {
ctx.rect(x0 - ly, y0 - lx, ly * 2 + 1, 1);
ctx.rect(x0 - ly, y0 + lx, ly * 2 + 1, 1);
}
ctx.fill();
}
function pixelPixelatedCircle(ctx, cx, cy, r){
r |= 0;
ctx.setTransform(1,0,0,1,0,0); // ensure default transform
var x = r, y = 0, dx = 1, dy = 1;
var err = dx - (r << 1);
var x0 = cx | 0, y0 = cy | 0;
var lx = x,ly = y;
var w = 1, px = x0;
ctx.beginPath();
var rendering = 2;
while (rendering) {
const yy = y0 - y;
const yy1 = y0 + y - 1;
const xx = x0 - x;
const xx1 = x0 + x - 1;
ctx.rect(xx, yy1, 1, 1);
ctx.rect(xx, yy, 1, 1);
ctx.rect(xx1, yy1, 1, 1);
ctx.rect(xx1, yy, 1, 1);
if (x !== lx){
const yy = y0 - lx;
const yy1 = y0 + lx - 1;
const xx = x0 - ly;
w = px - xx;
const xx1 = x0 + ly - w;
ctx.rect(xx, yy, w, 1);
ctx.rect(xx, yy1, w, 1);
ctx.rect(xx1, yy, w, 1);
ctx.rect(xx1, yy1, w, 1);
px = xx;
}
lx = x;
ly = y;
y++;
err += dy;
dy += 2;
if (err > 0) {
x--;
dx += 2;
err += (-r << 1) + dx;
}
if (x < y) { rendering -- }
}
ctx.fill();
}
const ctxZ = canvasZoom.getContext("2d");
canvas.addEventListener("mousemove",(event) => {
ctxZ.clearRect(0,0,30,30);
ctxZ.drawImage(canvas, -(event.pageX-10), -(event.pageY-10));
});
canvas {border: 1px solid black}
#canvasZoom {
width: 300px;
height: 300px;
image-rendering: pixelated;
}
<canvas id="canvas" width="300" height="300"></canvas>
<canvas id="canvasZoom" width="30" height="30"></canvas>
There doesn't appear to be a built-in setting that I can find, but you can loop through the image data and set the individual pixels if they are within some threshold of what you want.
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
context.beginPath();
context.arc(250, 250, 250, 0, 2 * Math.PI, false);
context.fillStyle = 'rgb(255, 0, 0)';
context.fill();
context.closePath();
console.log(getDistinctColors(context).length + " distinct colors before filter");
solidifyColor(context, 255, 0, 0);
console.log(getDistinctColors(context).length + " distinct colors aftrer filter");
function solidifyColor(context, r, g, b, threshold = 3) {
const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
var rDif = Math.abs(imageData.data[i + 0] - r);
var bDif = Math.abs(imageData.data[i + 1] - b);
var gDif = Math.abs(imageData.data[i + 2] - g);
if (rDif <= threshold && bDif <= threshold && gDif <= threshold) {
imageData.data[i + 0] = r;
imageData.data[i + 1] = g;
imageData.data[i + 2] = b;
imageData.data[i + 3] = 255; // remove alpha
}
}
context.putImageData(imageData, 0, 0);
}
function getDistinctColors(context) {
var colors = [];
const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
colors.push([
imageData.data[i + 0], // R value
imageData.data[i + 1], // G value
imageData.data[i + 2], // B value
imageData.data[i + 3] // A value
]);
}
return [...new Set(colors.map(a => JSON.stringify(a)))].map(a => JSON.parse(a));
}
<canvas id=canvas width=500 height=500></canvas>
I have an arc which is rather large in size with a stroke that uses rgba values. It has a 50% alpha value and because of that, it is causing a big hit on my cpu profile for my browser.
So i want to find a way to optimize this so that where ever the arc is drawn in a canvas, it will only draw from one angle to another of which is visible on screen.
What i am having difficulty with, is working out the correct angle range.
Here is a visual example:
The top image is what the canvas actually does even if you don't see it, and the bottom one is what I am trying to do to save processing time.
I created a JSFiddle where you can click and drag the circle, though, the two angles are currently fixed:
https://jsfiddle.net/44tawd81/
Here is the draw code:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.strokeStyle = 'red';
var radius = 50;
var pos = {
'x': canvas.width - 20,
'y': canvas.height /2
};
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.beginPath();
ctx.arc(pos.x,pos.y,radius,0,2*Math.PI); //need to adjust angle range
ctx.stroke();
requestAnimationFrame(draw);
}
draw();
What is the simplest way to find the angle range to draw based on it's position and size in a canvas?
Clipping a Circle
This is how to clip a circle to a rectangular region aligned to the x and y axis.
To clip the circle I search for the list of points where the circle intersects the clipping region. Starting from one side I go in a clockwise direction adding clip points as they are found. When all 4 sides are tested I then draw the arc segments that join the points found.
To find if a point has intercepted a clipping edge you find the distance the circle center is from that edge. Knowing the radius and the distance you can complete the right triangle to find the coordinates of the intercept.
For the left edge
// define the clip edge and circle
var clipLeftX = 100;
var radius = 200;
var centerX = 200;
var centerY = 200;
var dist = centerX - clipLeftX;
if(dist > radius) { // circle inside }
if(dist < -radius) {// circle completely outside}
// we now know the circle is clipped
Now calculate the distance from the circle y that the two clip points will be
// the right triangle with hypotenuse and one side know can be solved with
var clipDist = Math.sqrt(radius * radius - dist * dist);
So the points where the circle intercept the clipping line
var clipPointY1 = centerY - clipDist;
var clipPointY2 = centerY + clipDist;
With that you can work out if the two points are inside or outside the left side top or bottom by testing the two points against the top and bottom of the left line.
You will end up with either 0,1 or 2 clipping points.
Because arc requires angles to draw you need to calculate the angle from the circle center to the found points. You already have all the info needed
// dist is the x distance from the clip
var angle = Math.acos(radius/dist); // for left and right side
The hard part is making sure all the angles to the clipping point are in the correct order. The is a little fiddling about with flags to ensure that the arcs are in the correct order.
After checking all four sides you will end up with 0,2,4,6, or 8 clipping points representing the start and ends of the various clipped arcs. It is then simply iterating the arc segments and rendering them.
// Helper functions are not part of the answer
var canvas;
var ctx;
var mouse;
var resize = function(){
/** fullScreenCanvas.js begin **/
canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
var canvasMouseCallBack = undefined; // if needed
mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
"mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
function(n){element.addEventListener(n, mouseMove);});
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas === "undefined"){
mouse.mouseStart(canvas);
}else{
mouse.mouseStart();
}
}
/** MouseFull.js end **/
resize();
// Answer starts here
var w = canvas.width;
var h = canvas.height;
var d = Math.sqrt(w * w + h * h); // diagnal size
var cirLWidth = d * (1 / 100);
var rectCol = "black";
var rectLWidth = d * (1 / 100);
const PI2 = Math.PI * 2;
const D45_LEN = 0.70710678;
var angles = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // declared outside to stop GC
// create a clipArea
function rectArea(x, y, x1, y1) {
return {
left : x,
top : y,
width : x1 - x,
height : y1 - y
};
}
// create a arc
function arc(x, y, radius, start, end, col) {
return {
x : x,
y : y,
r : radius,
s : start,
e : end,
c : col
};
}
// draws an arc
function drawArc(arc, dir) {
ctx.strokeStyle = arc.c;
ctx.lineWidth = cirLWidth;
ctx.beginPath();
ctx.arc(arc.x, arc.y, arc.r, arc.s, arc.e, dir);
ctx.stroke();
}
// draws a clip area
function drawRect(r) {
ctx.strokeStyle = rectCol;
ctx.lineWidth = rectLWidth;
ctx.strokeRect(r.left, r.top, r.width, r.height);
}
// clip and draw an arc
// arc is the arc to clip
// clip is the clip area
function clipArc(arc, clip){
var count, distTop, distLeft, distBot, distRight, dist, swap, radSq, bot,right;
// cir1 is used to draw the clipped circle
cir1.x = arc.x;
cir1.y = arc.y;
count = 0; // number of clip points found;
bot = clip.top + clip.height; // no point adding these two over and over
right = clip.left + clip.width;
// get distance from all edges
distTop = arc.y - clip.top;
distBot = bot - arc.y;
distLeft = arc.x - clip.left;
distRight = right - arc.x;
radSq = arc.r * arc.r; // get the radius squared
// check if outside
if(Math.min(distTop, distBot, distRight, distLeft) < -arc.r){
return; // nothing to see so go home
}
// check inside
if(Math.min(distTop, distBot, distRight, distLeft) > arc.r){
drawArc(cir1);
return;
}
swap = true;
if(distLeft < arc.r){
// get the distance up and down to clip
dist = Math.sqrt(radSq - distLeft * distLeft);
// check the point is in the clip area
if(dist + arc.y < bot && arc.y + dist > clip.top){
// get the angel
angles[count] = Math.acos(distLeft / -arc.r);
count += 1;
}
if(arc.y - dist < bot && arc.y - dist > clip.top){
angles[count] = PI2 - Math.acos(distLeft / -arc.r); // get the angle
if(count === 0){ // if first point then set direction swap
swap = false;
}
count += 1;
}
}
if(distTop < arc.r){
dist = Math.sqrt(radSq - distTop * distTop);
if(arc.x - dist < right && arc.x - dist > clip.left){
angles[count] = Math.PI + Math.asin(distTop / arc.r);
count += 1;
}
if(arc.x+dist < right && arc.x+dist > clip.left){
angles[count] = PI2-Math.asin(distTop/arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
if(distRight < arc.r){
dist = Math.sqrt(radSq - distRight * distRight);
if(arc.y - dist < bot && arc.y - dist > clip.top){
angles[count] = PI2 - Math.acos(distRight / arc.r);
count += 1;
}
if(dist + arc.y < bot && arc.y + dist > clip.top){
angles[count] = Math.acos(distRight / arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
if(distBot < arc.r){
dist = Math.sqrt(radSq - distBot * distBot);
if(arc.x + dist < right && arc.x + dist > clip.left){
angles[count] = Math.asin(distBot / arc.r);
count += 1;
}
if(arc.x - dist < right && arc.x - dist > clip.left){
angles[count] = Math.PI + Math.asin(distBot / -arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
// now draw all the arc segments
if(count === 0){
return;
}
if(count === 2){
cir1.s = angles[0];
cir1.e = angles[1];
drawArc(cir1,swap);
}else
if(count === 4){
if(swap){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[0];
drawArc(cir1);
}else{
cir1.s = angles[2];
cir1.e = angles[3];
drawArc(cir1);
cir1.s = angles[0];
cir1.e = angles[1];
drawArc(cir1);
}
}else
if(count === 6){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[4];
drawArc(cir1);
cir1.s = angles[5];
cir1.e = angles[0];
drawArc(cir1);
}else
if(count === 8){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[4];
drawArc(cir1);
cir1.s = angles[5];
cir1.e = angles[6];
drawArc(cir1);
cir1.s = angles[7];
cir1.e = angles[0];
drawArc(cir1);
}
return;
}
var rect = rectArea(50, 50, w - 50, h - 50);
var circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
var cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
var counter = 0;
var countStep = 0.03;
function update() {
var x, y;
ctx.clearRect(0, 0, w, h);
circle.x = mouse.x;
circle.y = mouse.y;
drawArc(circle, "#888"); // draw unclipped arc
x = Math.cos(counter * 0.1);
y = Math.sin(counter * 0.3);
rect.top = h / 2 - Math.abs(y * (h * 0.4)) - 5;
rect.left = w / 2 - Math.abs(x * (w * 0.4)) - 5;
rect.width = Math.abs(x * w * 0.8) + 10;
rect.height = Math.abs(y * h * 0.8) + 10;
cir1.col = "RED";
clipArc(circle, rect); // draw the clipped arc
drawRect(rect); // draw the clip area. To find out why this method
// sucks move this to before drawing the clipped arc.
requestAnimationFrame(update);
if(mouse.buttonRaw !== 1){
counter += countStep;
}
ctx.font = Math.floor(w * (1 / 50)) + "px verdana";
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.lineWidth = Math.ceil(w * (1 / 300));
ctx.textAlign = "center";
ctx.lineJoin = "round";
ctx.strokeText("Left click and hold to pause", w/ 2, w * (1 / 40));
ctx.fillText("Left click and hold to pause", w/ 2, w * (1 / 40));
}
update();
window.addEventListener("resize",function(){
resize();
w = canvas.width;
h = canvas.height;
rect = rectArea(50, 50, w - 50, h - 50);
circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
});
The quickest way to clip a circle.
That is the quickest I could manage to do it in code. There is some room for optimization but not that much in the agorithum.
The best solution is of course to use the canvas 2D context API clip() method.
ctx.save();
ctx.rect(10,10,200,200); // define the clip region
ctx.clip(); // activate the clip.
//draw your circles
ctx.restore(); // remove the clip.
This is much quicker than the method I showed above and should be used unless you have a real need to know the clip points and arcs segments that are inside or outside the clip region.
To find the angle to draw based on circle position, canvas position, circle size, and canvas size:
Determine intersection of circle and canvas
Calculate points on the circle at which intersection occurs
You then have an isosceles triangle.
You can use cosine formula for calculation of the angle.
c^2=a^2+b^2−2abcos(α)
a and b are sides adjacent to the angle α, which are the radius of the center r. c is the distance between the two points P1 and P2. So we get:
|P1−P2|^2=2r^2−2r^2cos(α)
2r^2−|P1−P2|^2/2r2=cos(α)
α=cos−1(2r^2−|P1−P2|^2/2r^2)
I'm drawing lines on an HTML canvas, and use a less precise 2d-array (representing blocks of 10x10 pixels) in which I 'draw' lines with Bresenham's algorithm to store line-ids, so I can use that array to see which line is selected.
This works, but I would like it to be more accurate - not in the 10x10 size that I use (I like that I don't exactly have to click on the line), but when I draw a representation of that array over my actual canvas, I see that there are a lot of the 10x10 blocks not filled, even though the line is crossing them:
Is there a better solution to this? What I want is to catch ALL grid blocks that the actual line passes through.
Without seeing your code, I think you made a rounding error while filling the lookup table using the Bresenham algorithm or you scaled the coordinates before running the algorithm.
This jsFiddle shows what I came up with and the squares are perfectly aligned.
HTML
<canvas id="myCanvas"></canvas>
CSS
#myCanvas {
width: 250px;
height: 250px;
}
JavaScript
var $canvas = $("#myCanvas"),
ctx = $canvas[0].getContext("2d");
ctx.canvas.width = $canvas.width();
ctx.canvas.height = $canvas.height();
function Grid(ctx) {
this._ctx = ctx;
this._lines = [];
this._table = [];
this._tableScale = 10;
this._createLookupTable();
}
Grid.prototype._createLookupTable = function() {
this._table = [];
for (var y = 0; y < Math.ceil(ctx.canvas.height / this._tableScale); y++) {
this._table[y] = [];
for (var x = 0; x < Math.ceil(ctx.canvas.width / this._tableScale); x++)
this._table[y][x] = null;
}
};
Grid.prototype._updateLookupTable = function(line) {
var x0 = line.from[0],
y0 = line.from[1],
x1 = line.to[0],
y1 = line.to[1],
dx = Math.abs(x1 - x0),
dy = Math.abs(y1 - y0),
sx = (x0 < x1) ? 1 : -1,
sy = (y0 < y1) ? 1 : -1,
err = dx - dy;
while(true) {
this._table[Math.floor(y0 / 10)][Math.floor(x0 / 10)] = line;
if ((x0 == x1) && (y0 == y1)) break;
var e2 = 2 * err;
if (e2 >- dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
};
Grid.prototype.hitTest = function(x, y) {
var ctx = this._ctx,
hoverLine = this._table[Math.floor(y / 10)][Math.floor(x / 10)];
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
this._lines.forEach(function(line) {
line.draw(ctx, line === hoverLine ? "red" : "black");
});
};
Grid.prototype.drawLookupTable = function() {
ctx.beginPath();
for (var y = 0; y < this._table.length; y++)
for (var x = 0; x < this._table[y].length; x++) {
if (this._table[y][x])
ctx.rect(x * 10, y * 10, 10, 10);
}
ctx.strokeStyle = "rgba(0, 0, 0, 0.2)";
ctx.stroke();
};
Grid.prototype.addLine = function(line) {
this._lines.push(line);
this._updateLookupTable(line);
};
Grid.prototype.draw = function() {
var ctx = this._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
this._lines.forEach(function(line) {
line.draw(ctx);
});
};
function Line(x0, y0, x1, y1) {
this.from = [ x0, y0 ];
this.to = [ x1, y1];
}
Line.prototype.draw = function(ctx, style) {
ctx.beginPath();
ctx.moveTo(this.from[0], this.from[1]);
ctx.lineTo(this.to[0], this.to[1]);
ctx.strokeStyle = style || "black";
ctx.stroke();
};
var grid = new Grid(ctx);
grid.addLine(new Line(80, 10, 240, 75));
grid.addLine(new Line(150, 200, 50, 45));
grid.addLine(new Line(240, 10, 20, 150));
grid.draw();
grid.drawLookupTable();
$canvas.on("mousemove", function(e) {
grid.hitTest(e.offsetX, e.offsetY);
grid.drawLookupTable();
});
Your best option is to treat the mouse-cursor-position as a small circle (f.e. with a 5px radius) and check if the line intersects with the circle.
Use the math as explained in this Q&A
JavaScript
A simple function to detect intersection would be:
function lineCircleIntersects(x1, y1, x2, y2, cx, cy, cr) {
var dx = x2 - x1,
dy = y2 - y1,
a = dx * dx + dy * dy,
b = 2 * (dx * (x1 - cx) + dy * (y1 - cy)),
c = cx * cx + cy * cy,
bb4ac;
c += x1 * x1 + y1 * y1;
c -= 2 * (cx * x1 + cy * y1);
c -= cr * cr;
bb4ac = b * b - 4 * a * c;
return bb4ac >= 0; // true: collision, false: no collision
}
See it working in this jsFiddle, but note that this function will also return true if the cursor is on the slope of the line outside [x1, y1], [x2, y2]. I'll leave this up to you :)
You can also try line-circle-collision library on github which should give you what you want.