13

I'm drawing a custom diagram of business objects using .NET GDI+. Among other things, the diagram consists of several lines that are connecting the objects.

In a particular scenario, I need to shorten a line by a specific number of pixels, let's say 10 pixels, i.e. find the point on the line that lies 10 pixels before the end point of the line.

Imagine a circle with radius r = 10 pixels, and a line with start point (x1, y1) and end point (x2, y2). The circle is centered at the end point of the line, as in the following illustration.

Illustration

How do I calculate the point marked with a red circle, i.e. the intersection between circle and line? This would give me the new end point of the line, shortening it by 10 pixels.


Solution

Thank you for your answers from which I was able to put together the following procedure. I named it LengthenLine, since I find it more natural to pass a negative number of pixels if I want the line shortened.

Specifically, I was trying to put together a function that could draw a line with rounded corners, which can be found here.

public void LengthenLine(PointF startPoint, ref PointF endPoint, float pixelCount)
{
  if (startPoint.Equals(endPoint))
    return; // not a line

  double dx = endPoint.X - startPoint.X;
  double dy = endPoint.Y - startPoint.Y;
  if (dx == 0)
  {
    // vertical line:
    if (endPoint.Y < startPoint.Y)
      endPoint.Y -= pixelCount;
    else
      endPoint.Y += pixelCount;
  }
  else if (dy == 0)
  {
    // horizontal line:
    if (endPoint.X < startPoint.X)
      endPoint.X -= pixelCount;
    else
      endPoint.X += pixelCount;
  }
  else
  {
    // non-horizontal, non-vertical line:
    double length = Math.Sqrt(dx * dx + dy * dy);
    double scale = (length + pixelCount) / length;
    dx *= scale;
    dy *= scale;
    endPoint.X = startPoint.X + Convert.ToSingle(dx);
    endPoint.Y = startPoint.Y + Convert.ToSingle(dy);
  }
}
Glorfindel
  • 21,988
  • 13
  • 81
  • 109
bernhof
  • 6,219
  • 2
  • 45
  • 71

3 Answers3

15

Find the direction vector, i.e. let the position vectors be (using floats) B = (x2, y2) and A = (x1, y1), then AB = B - A. Normalize that vector by dividing by its length ( Math.Sqrt(xx + yy) ). Then multiply the direction vector AB by the original length minus the circle's radius, and add back to the lines starting position:

double dx = x2 - x1;
double dy = y2 - y1;
double length = Math.Sqrt(dx * dx + dy * dy);
if (length > 0)
{
    dx /= length;
    dy /= length;
}
dx *= length - radius;
dy *= length - radius;
int x3 = (int)(x1 + dx);
int y3 = (int)(y1 + dy);

Edit: Fixed the code, aaand fixed the initial explanation (thought you wanted the line to go out from the circle's center to its perimeter :P)

Cecil Has a Name
  • 4,962
  • 1
  • 29
  • 31
  • You even be able to "optimize" (not as in micro-optimization) it a little, by precalculating the multiplication factor `double scale = (length - radius) / length`, but I tend to think in stepwise vector operations. – Cecil Has a Name Dec 08 '09 at 14:06
  • Length is only 0 when **both dx and dy equal zero**, so you should check `if (dx == 0 && dy == 0)` before the expensive square root. And what happens if length is zero? Why do you still multiply the length when it's zero – phuclv Feb 20 '19 at 03:14
5

You can use similar triangles. For the main triangle, d is the hypotenuses and the extension of r is the vertical line that meets the right angle. Inside the circle you will have a smaller triangle with a hypotenuses of length r.

r/d = (x2-a0)/(x2-x1) = (y2-b0)/(y2-y1)

a0 = x2 + (x2-x1)r/d

b0 = y2 + (y2-y1)r/d
Glorfindel
  • 21,988
  • 13
  • 81
  • 109
unholysampler
  • 17,141
  • 7
  • 47
  • 64
  • @unholysampler: I've contributed my handdrawn pic. If you don't like it, just rollback :-) – dtb Dec 08 '09 at 13:46
  • 2
    Keep in mind you'll have to watch out for cases where the slope is infinite or zero (i.e., vertical and horizontal lines) or close enough to that to cause overflow - in those cases, your divisions will fail. – paxdiablo Dec 08 '09 at 15:34
  • unfortunately the image has been dead – phuclv Feb 20 '19 at 03:15
5

I'm not sure why you even had to introduce the circle. For a line stretching from (x2,y2) to (x1,y1), you can calculate any point on that line as:

(x2+p*(x1-x2),y2+p*(y1-y2))

where p is the percentage along the line you wish to go.

To calculate the percentage, you just need:

p = r/L

So in your case, (x3,y3) can be calculated as:

(x2+(10/L)*(x1-x2),y2+(10/L)*(y1-y2))

For example, if you have the two points (x2=1,y2=5) and (x1=-6,y1=22), they have a length of sqrt(72 + 172 or 18.38477631 and 10 divided by that is 0.543928293. Putting all those figures into the equation above:

  (x2 + (10/l)      * (x1-x2) , y2 + (10/l)      * (y1-y2))
= (1  + 0.543928293 * (-6- 1) , 5  + 0.543928293 * (22- 5))
= (1  + 0.543928293 * -7      , 5  + 0.543928293 * 17     )
= (x3=-2.807498053,y3=14.24678098)

The distance between (x3,y3) and (x1,y1) is sqrt(3.1925019472 + 7.7532190152) or 8.384776311, a difference of 10 to within one part in a thousand million, and that's only because of rounding errors on my calculator.

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • He "introduced the circle" because he wants to shorten a line by a set amount of pixels (in his case, a radius of a circle) and not by some percentage of the line, i.e. he wants to shave off 10 pixels, not 10%. – Stian Høiland Aug 07 '22 at 18:45
  • @StianHøiland, the reason I stated you didn't need the circle in the question is because OP already *knew* they wanted 10 pixels removed, the circle is therefore superfluous. And my answer *is* shaving off 10 pixels. The act of dividing 10 by the line length gives you the percentage of `0.544`-ish and *that's* the percentage you use in the equation. – paxdiablo Aug 07 '22 at 20:48