11

I have a line that is based on two (x,y) coordinates I know. This line has a starting and an end point. Now I want to add an arrowhead at the end point of the line.

I know that the arrow is an equilateral triangle, and therefore each angle has 60 degrees. Additionally, I know the length of one side, which will be 20. I also no one edge of the triangle (that is the end point of the line).

How can I calculate the other two points of the triangle? I know I should use some trigonometry but how?

P.s. The endpoint of the line should be the arrowhead's tip.

6 Answers6

11

You don't need trig., just some vector arithmetic...

Say the line goes from A to B, with the front vertex of the arrowhead at B. The length of the arrowhead is h = 10(√3) and its half-width is w = 10. We'll denote the unit vector from A to B as U = (B - A)/|B - A| (i.e., the difference divided by the length of the difference), and the unit vector perpendicular to this as V = [-Uy, Ux].

From these quantities, you can calculate the two rear vertices of the arrowhead as B - hU ± wV.

In C++:

struct vec { float x, y; /* … */ };

void arrowhead(vec A, vec B, vec& v1, vec& v2) {
    float h = 10*sqrtf(3), w = 10;
    vec U = (B - A)/(B - A).length();
    vec V = vec(-U.y, U.x);
    v1 = B - h*U + w*V;
    v2 = B - h*U - w*V;
}

If you want to specify different angles, then you will need some trig. to calculate different values of h and w. Assuming you want an arrowhead of length h and tip-angle θ, then w = h tan(θ/2). In practice, however, it's simplest to specify h and w directly.

Marcelo Cantos
  • 181,030
  • 38
  • 327
  • 365
10

Here's a sample LINQPad program that shows how to do that:

void Main()
{
    const int imageWidth = 512;
    Bitmap b = new Bitmap(imageWidth , imageWidth , PixelFormat.Format24bppRgb);

    Random r = new Random();
    for (int index = 0; index < 10; index++)
    {
        Point fromPoint = new Point(0, 0);
        Point toPoint = new Point(0, 0);

        // Ensure we actually have a line
        while (fromPoint == toPoint)
        {
            fromPoint = new Point(r.Next(imageWidth ), r.Next(imageWidth ));
            toPoint = new Point(r.Next(imageWidth ), r.Next(imageWidth ));
        }

        // dx,dy = arrow line vector
        var dx = toPoint.X - fromPoint.X;
        var dy = toPoint.Y - fromPoint.Y;

        // normalize
        var length = Math.Sqrt(dx * dx + dy * dy);
        var unitDx = dx / length;
        var unitDy = dy / length;

        // increase this to get a larger arrow head
        const int arrowHeadBoxSize = 10;

        var arrowPoint1 = new Point(
            Convert.ToInt32(toPoint.X - unitDx * arrowHeadBoxSize - unitDy * arrowHeadBoxSize),
            Convert.ToInt32(toPoint.Y - unitDy * arrowHeadBoxSize + unitDx * arrowHeadBoxSize));
        var arrowPoint2 = new Point(
            Convert.ToInt32(toPoint.X - unitDx * arrowHeadBoxSize + unitDy * arrowHeadBoxSize),
            Convert.ToInt32(toPoint.Y - unitDy * arrowHeadBoxSize - unitDx * arrowHeadBoxSize));

        using (Graphics g = Graphics.FromImage(b))
        {
            if (index == 0)
                g.Clear(Color.White);

            g.DrawLine(Pens.Black, fromPoint, toPoint);
            g.DrawLine(Pens.Black, toPoint, arrowPoint1);
            g.DrawLine(Pens.Black, toPoint, arrowPoint2);
        }
    }

    using (var stream = new MemoryStream())
    {
        b.Save(stream, ImageFormat.Png);
        Util.Image(stream.ToArray()).Dump();
    }
}

Basically, you:

  1. Calculate the vector of the arrow line
  2. Normalize the vector, ie. making its length 1
  3. Calculate the ends of the arrow heads by going:
    1. First back from the head a certain distance
    2. Then perpendicular out from the line a certain distance

Note that if you want the arrow head lines to have a different angle than 45 degrees, you'll have to use a different method.

The program above will draw 10 random arrows each time, here's an example:

arrow example

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • Thanks, this works, but I want to do that for an arbitrary angle. – RoflcoptrException Apr 25 '12 at 13:34
  • What do you mean arbitrary angle? The arrow *line* can be an arbitrary angle, it just looks close to 45 in my example image above, but the arrow head will of course turn nicely with the arrow line, ... but, the angle between the arrow head and the arrow line will be 45 degrees, just like in the answer you accepted. – Lasse V. Karlsen Apr 25 '12 at 13:54
3

Let's your line is (x0,y0)-(x1,y1)

Backward direction vector (dx, dy) = (x0-x1, y0-y1)

It's norm Norm = Sqrt(dx*dx+dy*dy)

Normalize it: (udx, udy) = (dx/Norm, dy/Norm)

Rotate by angles Pi/6 and -Pi/6

ax = udx * Sqrt(3)/2 - udy * 1/2

ay = udx * 1/2 + udy * Sqrt(3)/2

bx = udx * Sqrt(3)/2 + udy * 1/2

by =  - udx * 1/2 + udy * Sqrt(3)/2

Your points: (x1 + 20 * ax, y1 + 20 * ay) and (x1 + 20 * bx, y1 + 20 * by)

alex
  • 479,566
  • 201
  • 878
  • 984
MBo
  • 77,366
  • 5
  • 53
  • 86
2

I want to contribute my answer in C# based on Marcelo Cantos' answer since the algorithm works really well. I wrote a program to calculate the centroid of a laser beam projected on the CCD array. After the centroid is found, the direction angle line is drawn and I need the arrow head pointing at that direction. Since the angle is calculated, the arrow head would have to follow the angle in any of the direction.

Length = 10, Half-Width = 10

Length = 20, Half-Width = 10

enter image description here

This code gives you the flexibility of changing the arrow head size as shown in the pictures.

First you need the vector struct with all the necessary operators overloading.

private struct vec
{
    public float x;
    public float y;

    public vec(float x, float y)
    {
        this.x = x;
        this.y = y;
    }

    public static vec operator -(vec v1, vec v2)
    {
        return new vec(v1.x - v2.x, v1.y - v2.y);
    }

    public static vec operator +(vec v1, vec v2)
    {
        return new vec(v1.x + v2.x, v1.y + v2.y);
    }

    public static vec operator /(vec v1, float number)
    {
        return new vec(v1.x / number, v1.y / number);
    }

    public static vec operator *(vec v1, float number)
    {
        return new vec(v1.x * number, v1.y * number);
    }

    public static vec operator *(float number, vec v1)
    {
        return new vec(v1.x * number, v1.y * number);
    }

    public float length()
    {
        double distance;
        distance = (this.x * this.x) + (this.y * this.y);
        return (float)Math.Sqrt(distance);
    }
}

Then you can use the same code given by Marcelo Cantos, but I made the length and half_width of the arrow head variables so that you can define that when calling the function.

private void arrowhead(float length, float half_width, 
                       vec A, vec B, ref vec v1, ref vec v2)
{
    float h = length * (float)Math.Sqrt(3);
    float w = half_width;
    vec U = (B - A) / (B - A).length();
    vec V = new vec(-U.y, U.x);
    v1 = B - h * U + w * V;
    v2 = B - h * U - w * V;

}

Now you can call the function like this:

vec leftArrowHead = new vec();
vec rightArrowHead = new vec();
arrowhead(20, 10, new vec(circle_center_x, circle_center_y), 
    new vec(x_centroid_pixel, y_centroid_pixel),
    ref leftArrowHead, ref rightArrowHead);

In my code, the circle center is the first vector location (arrow butt), and the centroid_pixel is the second vector location (arrow head).

I draw the arrow head by storing the vector values in the points for graphics.DrawPolygon() function in the System.Drawings. Code is shown below:

Point[] ppts = new Point[3];
ppts[0] = new Point((int)leftArrowHead.x, (int)leftArrowHead.y);
ppts[1] = new Point(x_cm_pixel,y_cm_pixel);
ppts[2] = new Point((int)rightArrowHead.x, (int)rightArrowHead.y);

g2.DrawPolygon(p, ppts);
Patratacus
  • 1,651
  • 1
  • 16
  • 19
1

You can find angle of line.

Vector ox = Vector(1,0);
Vector line_direction = Vector(line_begin.x - line_end.x, line_begin.y - line_end.y);
line_direction.normalize();
float angle = acos(ox.x * line_direction.x + line_direction.y * ox.y);

Then use this function to all 3 points using found angle.

Point rotate(Point point, float angle)
{
    Point rotated_point;
    rotated_point.x = point.x * cos(angle) - point.y * sin(angle);
    rotated_point.y = point.x * sin(angle) + point.y * cos(angle);
    return rotated_point;
}

Assuming that upper point of arrow's head is line's end it will perfectly rotated and fit to line. Didn't test it =(

alex
  • 479,566
  • 201
  • 878
  • 984
Denis Ermolin
  • 5,530
  • 6
  • 27
  • 44
1

For anyone that is interested, @TomP was wondering about a js version, so here is a javascript version that I made. It is based off of @Patratacus and @Marcelo Cantos answers. Javascript doesn't support operator overloading, so it isn't as clean looking as C++ or other languages. Feel free to offer improvements.

I am using Class.js to create classes.

Vector = Class.extend({
NAME: "Vector",

init: function(x, y)
{
    this.x = x;
    this.y = y;
},

subtract: function(v1)
{
    return new Vector(this.x - v1.x, this.y - v1.y);
},

add: function(v1)
{
    return new Vector(this.x + v1.x, this.y + v1.y);
},

divide: function(number)
{
    return new Vector(this.x / number, this.y / number);
},

multiply: function(number)
{
    return new Vector(this.x * number, this.y * number);
},

length: function()
{
    var distance;
    distance = (this.x * this.x) + (this.y * this.y);
    return Math.sqrt(distance);
}
});

And then a function to do the logic:

var getArrowhead = function(A, B)
{
    var h = 10 * Math.sqrt(3);
    var w = 5;
    var v1 = B.subtract(A);
    var length = v1.length();
    var U = v1.divide(length);
    var V = new Vector(-U.y, U.x);
    var r1 = B.subtract(U.multiply(h)).add(V.multiply(w));
    var r2 = B.subtract(U.multiply(h)).subtract(V.multiply(w));

    return [r1,r2];
}

And call the function like this:

var A = new Vector(start.x,start.y);
var B = new Vector(end.x,end.y);    
var vec = getArrowhead(A,B);

console.log(vec[0]);
console.log(vec[1]);

I know the OP didn't ask for any specific language, but I came across this looking for a JS implementation, so I thought I would post the result.

Sehael
  • 3,678
  • 21
  • 35