Drawing triangle/arrow on a line with CGContext

2019-03-09 13:18发布

问题:

I am using the framework of route-me for working with locations. In this code the path between two markers(points) will be drawn as a line.

My Question: "What code should I add if I want to add an arrow in the middle(or top) of the line, so that it points the direction"

Thanks



- (void)drawInContext:(CGContextRef)theContext
{
    renderedScale = [contents metersPerPixel];

    float scale = 1.0f / [contents metersPerPixel];

    float scaledLineWidth = lineWidth;
    if(!scaleLineWidth) {
        scaledLineWidth *= renderedScale;
    }
    //NSLog(@"line width = %f, content scale = %f", scaledLineWidth, renderedScale);

    CGContextScaleCTM(theContext, scale, scale);

    CGContextBeginPath(theContext);
    CGContextAddPath(theContext, path);

    CGContextSetLineWidth(theContext, scaledLineWidth);
    CGContextSetStrokeColorWithColor(theContext, [lineColor CGColor]);
    CGContextSetFillColorWithColor(theContext, [fillColor CGColor]);

    // according to Apple's documentation, DrawPath closes the path if it's a filled style, so a call to ClosePath isn't necessary
    CGContextDrawPath(theContext, drawingMode);
}

回答1:

- (void) drawLine: (CGContextRef) context from: (CGPoint) from to: (CGPoint) to 
{
    double slopy, cosy, siny;
    // Arrow size
    double length = 10.0;  
    double width = 5.0;

    slopy = atan2((from.y - to.y), (from.x - to.x));
    cosy = cos(slopy);
    siny = sin(slopy);

    //draw a line between the 2 endpoint
    CGContextMoveToPoint(context, from.x - length * cosy, from.y - length * siny );
    CGContextAddLineToPoint(context, to.x + length * cosy, to.y + length * siny);
    //paints a line along the current path
    CGContextStrokePath(context);

    //here is the tough part - actually drawing the arrows
    //a total of 6 lines drawn to make the arrow shape
    CGContextMoveToPoint(context, from.x, from.y);
    CGContextAddLineToPoint(context,
                        from.x + ( - length * cosy - ( width / 2.0 * siny )),
                        from.y + ( - length * siny + ( width / 2.0 * cosy )));
    CGContextAddLineToPoint(context,
                        from.x + (- length * cosy + ( width / 2.0 * siny )),
                        from.y - (width / 2.0 * cosy + length * siny ) );
    CGContextClosePath(context);
    CGContextStrokePath(context);

    /*/-------------similarly the the other end-------------/*/
    CGContextMoveToPoint(context, to.x, to.y);
    CGContextAddLineToPoint(context,
                        to.x +  (length * cosy - ( width / 2.0 * siny )),
                        to.y +  (length * siny + ( width / 2.0 * cosy )) );
    CGContextAddLineToPoint(context,
                        to.x +  (length * cosy + width / 2.0 * siny),
                        to.y -  (width / 2.0 * cosy - length * siny) );
    CGContextClosePath(context);
    CGContextStrokePath(context);
}


回答2:

The drawing of the actual triangle/arrow is easy once you have two points on your path.

CGContextMoveToPoint( context , ax , ay );
CGContextAddLineToPoint( context , bx , by );
CGContextAddLineToPoint( context , cx , cy );
CGContextClosePath( context ); // for triangle

Getting the points is a little more tricky. You said path was a line, as opposed to a curve or series of curves. That makes it easier.

Use CGPathApply to pick two points on the path. Probably, this is the last two points, one of which may be kCGPathElementMoveToPoint and the other will be kCGPathElementAddLineToPoint. Let mx,my be the first point and nx,ny be the second, so the arrow will point from m towards n.

Assuming you want the arrow at the tip of the line, bx,by from above will equal nx,ny on the line. Choose a point dx,dy between mx,my and nx,ny to calculate the other points.

Now calculate ax,ay and cx,cy such that they are on a line with dx,dy and equidistant from path. The following should be close, although I probably got some signs wrong:

r = atan2( ny - my , nx - mx );
bx = nx;
by = ny;
dx = bx + sin( r ) * length;
dy = by + cos( r ) * length;
r += M_PI_2; // perpendicular to path
ax = dx + sin( r ) * width;
ay = dy + cos( r ) * width;
cx = dx - sin( r ) * width;
cy = dy - cos( r ) * width;

Length is the distance from the tip of the arrow to the base, and width is distance from the shaft to the barbs, or half the breadth of the arrow head.

If path is a curve, then instead of finding mx,my as the previous point or move, it will be the final control point of the final curve. Each control point is on a line tangent to the curve and passing through the adjacent point.



回答3:

I found this question as I had the same. I took drawnonward's example and it was so close... But with a flipping of cos and sin, I was able to get it to work:

r = atan2( ny - my , nx - mx );
r += M_PI;
bx = nx;
by = ny;
dx = bx + cos( r ) * length;
dy = by + sin( r ) * length;
r += M_PI_2; // perpendicular to path
ax = dx + cos( r ) * width;
ay = dy + sin( r ) * width;
cx = dx - cos( r ) * width;
cy = dy - sin( r ) * width;

Once I did that, my arrows were pointed exactly the wrong way. So I added that second line (r += M_PI;)

Thanks go to drawnonward!



回答4:

And here is Swift 4+ version for Friedhelm Brügge answer: (I'll draw it on image)

func drawArrow(image: UIImage, ptSrc: CGPoint, ptDest: CGPoint) {

        // create context with image size
        UIGraphicsBeginImageContext(image.size)
        let context = UIGraphicsGetCurrentContext()

        // draw current image to the context
        image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))

        var slopY: CGFloat, cosY: CGFloat, sinY: CGFloat;
        // Arrow size
        let length: CGFloat = 35.0;
        let width: CGFloat = 35.0;

        slopY = atan2((ptSrc.y - ptDest.y), (ptSrc.x - ptDest.x));
        cosY = cos(slopY);
        sinY = sin(slopY);

        //here is the tough part - actually drawing the arrows
        //a total of 6 lines drawn to make the arrow shape
        context?.setFillColor(UIColor.white.cgColor)
        context?.move(to: CGPoint(x: ptSrc.x, y: ptSrc.y))
        context?.addLine(to: CGPoint(x: ptSrc.x + ( -length * cosY - ( width / 2.0 * sinY )), y: ptSrc.y + ( -length * sinY + ( width / 2.0 * cosY ))))
        context?.addLine(to: CGPoint(x: ptSrc.x + (-length * cosY + ( width / 2.0 * sinY )), y: ptSrc.y - (width / 2.0 * cosY + length * sinY )))
        context?.closePath()
        context?.fillPath()

        context?.move(to: CGPoint(x: ptSrc.x, y: ptSrc.y))
        context?.addLine(to: CGPoint(x: ptDest.x +  (length * cosY - ( width / 2.0 * sinY )), y: ptDest.y +  (length * sinY + ( width / 2.0 * cosY ))))
        context?.addLine(to: CGPoint(x: ptDest.x +  (length * cosY + width / 2.0 * sinY), y: ptDest.y -  (width / 2.0 * cosY - length * sinY)))
        context?.closePath()
        context?.fillPath()

        // draw current context to image view
        imgView.image = UIGraphicsGetImageFromCurrentImageContext()

        //close context
        UIGraphicsEndImageContext()
    }