Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <h3>UPDATE</h3> <p><a href="https://stackoverflow.com/a/37985645/77567">I've posted a Swift version of this answer separately.</a></p> <h3>ORIGINAL</h3> <p>This is a fun little problem. First of all, there are lots of ways to draw arrows, with curved or straight sides. Let's pick a very simple way and label the measurements we'll need:</p> <p><img src="https://i.stack.imgur.com/jfb4u.png" alt="arrow parts"></p> <p>We want to write a function that takes the start point, the end point, the tail width, the head width, and the head length, and returns a path outlining the arrow shape. Let's create a category named <code>dqd_arrowhead</code> to add this method to <code>UIBezierPath</code>:</p> <pre><code>// UIBezierPath+dqd_arrowhead.h @interface UIBezierPath (dqd_arrowhead) + (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint toPoint:(CGPoint)endPoint tailWidth:(CGFloat)tailWidth headWidth:(CGFloat)headWidth headLength:(CGFloat)headLength; @end </code></pre> <p>Since there are seven corners on the path of the arrow, let's start our implementation by naming that constant:</p> <pre><code>// UIBezierPath+dqd_arrowhead.m #import "UIBezierPath+dqd_arrowhead.h" #define kArrowPointCount 7 @implementation UIBezierPath (dqd_arrowhead) + (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint toPoint:(CGPoint)endPoint tailWidth:(CGFloat)tailWidth headWidth:(CGFloat)headWidth headLength:(CGFloat)headLength { </code></pre> <p>OK, the easy part is done. Now, how do we find the coordinates of those seven points on the path? It is much easier to find the points if the arrow is aligned along the X axis:</p> <p><img src="https://i.stack.imgur.com/Sxopv.png" alt="axis-aligned arrow points"></p> <p>It's pretty easy to compute the point coordinates on an axis-aligned arrow, but we'll need the overall length of the arrow to do it. We'll use the <code>hypotf</code> function from the standard library:</p> <pre><code> CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y); </code></pre> <p>We'll call on a helper method to actually compute the seven points:</p> <pre><code> CGPoint points[kArrowPointCount]; [self dqd_getAxisAlignedArrowPoints:points forLength:length tailWidth:tailWidth headWidth:headWidth headLength:headLength]; </code></pre> <p>But we need to transform those points, because in general we're <em>not</em> trying to create an axis-aligned arrow. Fortunately, Core Graphics supports a kind of transformation called an <em>affine transformation</em>, which lets us rotate and translate (slide) points. We'll call another helper method to create the transform that turns our axis-aligned arrow into the arrow we were asked for:</p> <pre><code> CGAffineTransform transform = [self dqd_transformForStartPoint:startPoint endPoint:endPoint length:length]; </code></pre> <p>Now we can create a Core Graphics path using the points of the axis-aligned arrow and the transform that turns it into the arrow we want:</p> <pre><code> CGMutablePathRef cgPath = CGPathCreateMutable(); CGPathAddLines(cgPath, &amp;transform, points, sizeof points / sizeof *points); CGPathCloseSubpath(cgPath); </code></pre> <p>Finally, we can wrap a <code>UIBezierPath</code> around the <code>CGPath</code> and return it:</p> <pre><code> UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath]; CGPathRelease(cgPath); return uiPath; } </code></pre> <p>Here's the helper method that computes the point coordinates. It's quite simple. Refer back to the diagram of the axis-aligned arrow if you need to.</p> <pre><code>+ (void)dqd_getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points forLength:(CGFloat)length tailWidth:(CGFloat)tailWidth headWidth:(CGFloat)headWidth headLength:(CGFloat)headLength { CGFloat tailLength = length - headLength; points[0] = CGPointMake(0, tailWidth / 2); points[1] = CGPointMake(tailLength, tailWidth / 2); points[2] = CGPointMake(tailLength, headWidth / 2); points[3] = CGPointMake(length, 0); points[4] = CGPointMake(tailLength, -headWidth / 2); points[5] = CGPointMake(tailLength, -tailWidth / 2); points[6] = CGPointMake(0, -tailWidth / 2); } </code></pre> <p>Computing the affine transform is more complicated. This is where the trigonometry comes in. You could use <code>atan2</code> and the <code>CGAffineTransformRotate</code> and <code>CGAffineTransformTranslate</code> functions to create it, but if you remember enough trigonometry, you can create it directly. Consult <a href="http://developer.apple.com/library/ios/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_affine/dq_affine.html#//apple_ref/doc/uid/TP30001066-CH204-CJBECIAD" rel="noreferrer">“The Math Behind the Matrices” in the <em>Quartz 2D Programming Guide</em></a> for more information about what I'm doing here:</p> <pre><code>+ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint length:(CGFloat)length { CGFloat cosine = (endPoint.x - startPoint.x) / length; CGFloat sine = (endPoint.y - startPoint.y) / length; return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y }; } @end </code></pre> <p>I have put all of the code in <a href="https://gist.github.com/4146780" rel="noreferrer">a gist for easy copy'n'paste</a>.</p> <p>With this category, you can easily draw arrows:</p> <p><img src="https://i.stack.imgur.com/WqbzL.png" alt="sample arrow 1"> <img src="https://i.stack.imgur.com/ZsIWH.png" alt="sample arrow 2"></p> <p>Since you're just generating a path, you can choose not to fill it, or not to stroke it as in this example:</p> <p><img src="https://i.stack.imgur.com/2wFhx.png" alt="unstroked arrow sample"></p> <p>You have to be careful, though. This code doesn't prevent you from getting funky results if you make the head width less than the tail width, or if you make the head length larger than the total arrow length:</p> <p><img src="https://i.stack.imgur.com/OJ4QU.png" alt="narrow head sample"> <img src="https://i.stack.imgur.com/hHVfT.png" alt="head too long sample"></p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload