David Fariña
David Fariña

Reputation: 1594

Calculating coordinate of intersection between lines and circle and degree of line in canvas

Alright, I would call myself a pretty good programmer who never really followed the academic way. Therefor my mathematical knowledge is rather limited, which is a little burden for things I sometimes need to accomplish. I know some of you are pretty clever minds and enjoy calculating this (for you most likely easy) thing:

I need to realize a few diagrams and charts for a customer. I choosed OCanvas as a drawing library. It has really helped and Everything I need to do works fine, but there is one caveat.

I have two circles, and can pass the Objects OCanvas makes for me to a function that draws a line between circle 1's center to the center of circle 2. Now I want to add triangles to the line to make it an arrow.

I know I need to calculate the tilt of the line, to properly align the triangle. I would also need to calculate the position of the intersection between circle border and the line, to position the triangle.

I would provide you some code, but I honestly have no clue on how to calculate this. Known values for me are: Line startX, startY, endX, endY as well as circle 1 X, Y and radius/circle 2 X, Y and radius (X and Y represent the center of the circles)

Thanks in advance!

EDIT:

A visual representation, where the red triangle is needed:

enter image description here

Upvotes: 1

Views: 575

Answers (5)

bluehipy
bluehipy

Reputation: 2294

Not complete but it might give you a good start.

let c = document.getElementById("myCanvas"),
  ctx = c.getContext("2d"),
  startX = 50,
  startY = 50,
  endX = 250,
  endY = 150,
  rS = 30,
  rE = 50,
  alpha = Math.atan((startY - endY) / (startX - endX)),
  dir = 0, // start 0 end 1
  side = 10;

ctx.beginPath();
ctx.arc(startX, startY, rS, 0, 2 * Math.PI);
ctx.stroke();

ctx.beginPath();
ctx.arc(endX, endY, rE, 0, 2 * Math.PI);
ctx.stroke();

if (startX < endX) {
	endcx = endX + (rE + side/2) * Math.cos(alpha + Math.PI);
	endcy = endY + (rE + side/2) * Math.sin(alpha + Math.PI);
	startcx = startX - (rS + side/2) * Math.cos(alpha - Math.PI);
	startcy = startY - (rS + side/2) * Math.sin(alpha - Math.PI);
	dir = 0;
} else if (startX >= endX) {
	endcx = endX - (rE + side/2) * Math.cos(alpha + Math.PI);
	endcy = endY - (rE + side/2) * Math.sin(alpha + Math.PI);
	startcx = startX + (rS + side/2) * Math.cos(alpha - Math.PI);
	startcy = startY + (rS + side/2) * Math.sin(alpha - Math.PI);
	dir = 1;
}

ctx.beginPath();
ctx.moveTo(startcx, startcy);
ctx.lineTo(endcx, endcy);
ctx.lineWidth=3;
ctx.stroke();

drawEqTriangle(ctx, side, startcx, startcy, dir?0:1);
drawEqTriangle(ctx, side, endcx, endcy, dir);

function drawEqTriangle(ctx, side, cx, cy, dir) {
  var h = side * (Math.sqrt(3) / 2);

  ctx.save();
  ctx.strokeStyle = "#000";
  ctx.fillStyle = "#000";
  ctx.translate(cx, cy);
  ctx.rotate(alpha + Math.PI / 2);

  ctx.beginPath();
  ctx.lineWidth=1;
  
if (dir === 0) {
  ctx.moveTo(0, -h / 2);
  ctx.lineTo(-side / 2, h / 2);
  ctx.lineTo(side / 2, h / 2);
  ctx.lineTo(0, -h / 2);
} else if (dir === 1) {
  ctx.moveTo(0, h / 2);
  ctx.lineTo(-side / 2, -h / 2);
  ctx.lineTo(side / 2, -h / 2);
  ctx.lineTo(0, h / 2);
}

  ctx.stroke();
  ctx.fill();

  ctx.closePath();
  ctx.restore();
}
<!DOCTYPE html>
<html>

<body>

  <canvas id="myCanvas" width="400" height="300" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
</body>

</html>

Upvotes: 2

bluehipy
bluehipy

Reputation: 2294

Another idea you could use for simplify things (and maybe @Blindman67 implemented above but I don't understand it as such) is to think things like this:

No matter where circles are positioned you can rotate the context in such a way that their centres are on a horizontal, or vertical if you prefer, this way all points of interest are on a single coordinate, linear. Draw what you need and then rotate the context back.

Upvotes: 0

Blindman67
Blindman67

Reputation: 54041

Render an arrow on a line

If you were using pure canvas it would be a little less fiddly.

First the two circles

const circle1 = {
    x : ?,
    y : ?,
    r : ?, // Radius
    lineWidth : ?,
}
const circle2 = {
    x : ?,
    y : ?,
    r : ?,
    lineWidth : ?,
}

Then the arrow description

const arrow = {
   width : ?,
   depth : ?, // tip to back of arrow head
}

The the math.

Get the vector from circle1 to circle2

var vx = circle2.x - circle1.x;
var vy = circle2.y - circle1.y;

The the distance between them

var dist = Math.sqrt(vx * vx + vy * vy);

You now need to normalise the vector by dividing the vector by the length. This makes the vector one unit long

vx /= dist;
vy /= dist;

Now you can render the arrow. The tip is one the edge of circle 2. To find that point subtract the radius and half the line width from the distance

const aDist = dist - (circle2.r + circle2.lineWidth / 2);

The normalised vector can be multiplied by a distance to find the position, or if using the canvas 2D API you use the normal to set the transform and then it is all aligned to the x Axis.

ctx.beginPath(); // drawing the arrow head
ctx.lineTo(circle1.x + vx * aDist, circle1.y + vy * aDist); // tip of arrow

We need to move back away from the tip and out from the line at 90deg to do this you can rotate a vector 90 deg by swapping the x,y parts and negating the new x.

ctx.lineTo(
     circle1.x + vx * (aDist - arrow.depth) - vy * arrow.width, 
     circle1.y + vy * (aDist - arrow.depth) + vx * arrow.width
     //          ^------- along line -----^ ^--Out from line--^ 
);
// and the other side
ctx.lineTo(
     circle1.x + vx * (aDist - arrow.depth) + vy * arrow.width, 
     circle1.y + vy * (aDist - arrow.depth) - vx * arrow.width
     //          ^------- along line -----^ ^--Out from line--^ 
);

// ctx.closePath();
ctx.fill();

Or if you use the transform

ctx.setTransform(vx,vy,-vy,vx,circle1.x,circle1.y); // set the circle1 as origin
                                                    // and use the normal to
                                                    // align the x axis
ctx.beginPath(); // drawing the arrow head
ctx.lineTo(aDist,0); // tip of arrow
ctx.lineTo(aDist - arrow.depth, arrow.width);
ctx.lineTo(aDist - arrow.depth, -arrow.width);
// ctx.closePath();
ctx.fill();
ctx.setTransform(1,0,0,1,0,0); // restore default transform.

Example

const ctx = canvas.getContext("2d");   


const circle1 = {
    x : 50,
    y : 100,
    r : 45, // Radius
    lineWidth : 3,
}
const circle2 = {
    x : 250,
    y : 50,
    r : 45,
    lineWidth : 3,
}
const arrow = {
   width : 10,
   depth : 20, // tip to back of arrow head
}
var vx = circle2.x - circle1.x;
var vy = circle2.y - circle1.y;
var dist = Math.sqrt(vx * vx + vy * vy);
vx /= dist;
vy /= dist;
const aDist = dist - (circle2.r + circle2.lineWidth / 2);
ctx.lineWidth = circle1.lineWidth;
ctx.beginPath();
ctx.arc(circle1.x, circle1.y, circle1.r, 0, 2 * Math.PI);
ctx.moveTo(circle2.x + circle2.r, circle2.y);
ctx.arc(circle2.x, circle2.y, circle2.r, 0, 2 * Math.PI);
ctx.moveTo(circle1.x, circle1.y);
ctx.lineTo(circle2.x, circle2.y);
ctx.stroke();    

ctx.beginPath(); // drawing the arrow head
ctx.lineTo(circle1.x + vx * aDist, circle1.y + vy * aDist); // tip of arrow
ctx.lineTo(
     circle1.x + vx * (aDist - arrow.depth) - vy * arrow.width, 
     circle1.y + vy * (aDist - arrow.depth) + vx * arrow.width
     //          ^------- along line -----^ ^--Out from line--^ 
);
// and the other side
ctx.lineTo(
     circle1.x + vx * (aDist - arrow.depth) + vy * arrow.width, 
     circle1.y + vy * (aDist - arrow.depth) - vx * arrow.width
     //          ^------- along line -----^ ^--Out from line--^ 
);

// ctx.closePath();
ctx.fill();
canvas { border : 2px solid black; }


const ctx = canvas.getContext("2d");
<canvas id="canvas"></canvas>

Example2 using setTransform

This uses setTransform to reduce the amount of math. also this example uses the line width to but the arrow head to the circle

const ctx = canvas.getContext("2d");   


const circle1 = {
    x : 50,
    y : 100,
    r : 45, // Radius
    lineWidth : 3,
}
const circle2 = {
    x : 250,
    y : 50,
    r : 45,
    lineWidth : 3,
}
const arrow = {
   width : 10,
   depth : 20, // tip to back of arrow head
}
var vx = circle2.x - circle1.x;
var vy = circle2.y - circle1.y;
var dist = Math.sqrt(vx * vx + vy * vy);
vx /= dist;
vy /= dist;
const aDist = dist - (circle2.r + circle2.lineWidth / 2);
ctx.lineWidth = circle1.lineWidth;
ctx.beginPath();
ctx.arc(circle1.x, circle1.y, circle1.r, 0, 2 * Math.PI);
ctx.moveTo(circle2.x + circle2.r, circle2.y);
ctx.arc(circle2.x, circle2.y, circle2.r, 0, 2 * Math.PI);
ctx.moveTo(circle1.x, circle1.y);
ctx.lineTo(circle2.x, circle2.y);
ctx.stroke();    

ctx.beginPath(); // drawing the arrow head
ctx.setTransform(vx, vy, -vy, vx, circle1.x, circle1.y); // set the circle1 as origin
                                                    // and use the normal to
                                                    // align the x axis
ctx.beginPath(); // drawing the arrow head
ctx.lineTo(aDist,-circle1.lineWidth/2); // tip of arrow circle1.linewidth is same for line between circles
ctx.lineTo(aDist,circle1.lineWidth/2); // tip of arrow circle1.linewidth is same for line between circles
ctx.lineTo(aDist - arrow.depth, arrow.width + circle1.lineWidth/2);
ctx.lineTo(aDist - arrow.depth, -arrow.width - circle1.lineWidth/2);
// ctx.closePath();
ctx.fill();
ctx.setTransform(1,0,0,1,0,0); // restore default transform.
ctx.fill();
canvas { border : 2px solid black; }


const ctx = canvas.getContext("2d");
<canvas id="canvas"></canvas>

Upvotes: 2

Marco Salerno
Marco Salerno

Reputation: 5203

Trying to help you with your problem:

Edit:

By integrating @bluehipy answer, maybe this is going to be closer

//Objects
Circle = function(oX, oY, r){
  this.oX = oX;
  this.oY = oY;
  this.r = r;
  context.beginPath();
  context.arc(oX, oY, r, 0, 2 * Math.PI, false);
  context.fillStyle = 'red';
  context.fill();
  context.lineWidth = 2;
  context.strokeStyle = '#003300';
  context.stroke();
}

//Functions
drawArrowFromCirclesOrigins = function(side,c1, c2){
  context.beginPath();
  //line
  context.moveTo(c1.oX, c1.oY);
  context.lineTo(c2.oX, c2.oY);     
  context.stroke();
  //triangle
  var alpha = Math.tan((c2.oY-c1.oY) / (c2.oX-c1.oX));
  var h = side * (Math.sqrt(3) / 2);
  
  context.translate(c2.oX/alpha+c2.r-(-(side/2)), c2.oY/alpha+c2.r-(-(side/2)));
  context.rotate(alpha + Math.PI / 2);

  context.beginPath();

  context.moveTo(0, -h / 2);
  context.lineTo(-side / 2, h / 2);
  context.lineTo(side / 2, h / 2);
  context.lineTo(0, -h / 2);
  context.fillStyle = 'green';
  context.fill();
  
  context.stroke();
}

//Variables
canvas = document.getElementById('canvas');
context = canvas.getContext('2d');

//Program
var cir1 = new Circle(20, 20, 10);
var cir2 = new Circle(40, 40, 10);
drawArrowFromCirclesOrigins(-11,cir1, cir2);
canvas {
  border: 2px solid black;
}
<canvas id="canvas" width="200" height="100"></canvas>

Upvotes: 1

Airwavezx
Airwavezx

Reputation: 323

To get the angle of the tilt for the triangle you need the following: Assuming (x1,y1) are the coordinates of the start of the line, and (x2,y2) are the coords of the ending of the line, and you want to draw the triangle at the end of the line:

alpha = arctan((y2-y2) / (x2-x1))

Then you need to create the triangle tilted with that angle (alpha).

The other triangle would be alpha + 180 degrees or alpha - 180 degrees (produce the same).

Upvotes: 1

Related Questions