miradorn
miradorn

Reputation: 75

Drawing an Arrow with NSBezierPath between two Points

So I'm trying to generate a NSBezierPath that looks like an arrow between two points, which can lie anywhere on the view, so the startPoint can be larger or smaller than the endpoint.

The arrow is updated while a user drags the mouse like in a drawing app.

I already figured out, that I probably have to use transformations and some math to do trigonometric and have come up with this implementation:

 +(NSBezierPath *)arrowWithStart:(NSPoint)startPoint andEnd:(NSPoint)endPoint{
        NSBezierPath* path = [NSBezierPath bezierPath];

        CGFloat width = endPoint.x - startPoint.x;
        CGFloat height = endPoint.y - startPoint.y;
        CGFloat angle = atan2(width, height);

        NSAffineTransform *tr = [NSAffineTransform transform];
        [tr translateXBy:startPoint.x yBy:startPoint.y];
        [tr scaleXBy:width yBy:height];
        [tr rotateByDegrees:angle];

        [path moveToPoint:CGPointZero];
        [path lineToPoint:CGPointMake(0.75, 0.7)];
        [path lineToPoint:CGPointMake(0.8, 0.65)];
        [path lineToPoint:CGPointMake(1, 1)];
        [path lineToPoint:CGPointMake(0.65, 0.8)];
        [path lineToPoint:CGPointMake(0.7, 0.75)];
        [path closePath];

        [path transformUsingAffineTransform:tr];

        return path;
    }

This Code generates pretty nice arrows, when the points are some kind of diagonal, like

to each other, but when the points are getting nearer to a horizontal or vertical line, like

the result becomes a straight line without an arrowhead.

So I think I'm doing something wrong in the transformation Matrix.

If somebody knows where I'm making a mistake it would be great.

Upvotes: 1

Views: 2133

Answers (1)

Martin R
Martin R

Reputation: 540075

What you need is an affine transformation that transforms the point (0, 0) to startPoint and (1, 1) to endPoint. This transformation can be computed directly:

CGFloat tx = startPoint.x;
CGFloat ty = startPoint.y;
CGFloat a = ((endPoint.x - startPoint.x) + (endPoint.y - startPoint.y))/2.;
CGFloat b = (-(endPoint.x - startPoint.x) + (endPoint.y - startPoint.y))/2.;
NSAffineTransformStruct transformStruct = { a, b, -b, a, tx, ty };
NSAffineTransform *tr = [NSAffineTransform transform];
[tr setTransformStruct:transformStruct];

Explanation:

NSAffineTransformStruct transformStruct = { a, b, -b, a, tx, ty };

describes a general combination of translation, scaling and rotation, i.e. an affine transformation without shearing. The requirements (0, 0) -> startPoint, (1, 1) -> endPoint give the equations

startPoint.x = 0 * a + 0 * (-b) + tx
startPoint.y = 0 * b + 0 *   a  + ty
endPoint.x   = 1 * a + 1 * (-b) + tx
endPoint.y   = 1 * b + 1 *   a  + tx

and solving these equations for a, b, tx, ty gives above solution. (See Transform Mathematics in the "Cocoa Drawing Guide" for more information.)

The problem with your original code is that

  • atan2 takes y as first argument, so atan2(height, width) would compute the angle.
  • For horizontal or vertical lines, width or height and therefore one scaling factor is zero, this causes the straight lines without arrowhead.

Upvotes: 1

Related Questions