-
-
Save mayoff/4146780 to your computer and use it in GitHub Desktop.
// Swift 2.2 syntax / API | |
import UIKit | |
extension UIBezierPath { | |
class func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> Self { | |
let length = hypot(end.x - start.x, end.y - start.y) | |
let tailLength = length - headLength | |
func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) } | |
var points: [CGPoint] = [ | |
p(0, tailWidth / 2), | |
p(tailLength, tailWidth / 2), | |
p(tailLength, headWidth / 2), | |
p(length, 0), | |
p(tailLength, -headWidth / 2), | |
p(tailLength, -tailWidth / 2), | |
p(0, -tailWidth / 2) | |
] | |
let cosine = (end.x - start.x) / length | |
let sine = (end.y - start.y) / length | |
var transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y) | |
let path = CGPathCreateMutable() | |
CGPathAddLines(path, &transform, &points, points.count) | |
CGPathCloseSubpath(path) | |
return self.init(CGPath: path) | |
} | |
} | |
// Example call: | |
let arrow = UIBezierPath.arrow(from: CGPointMake(50, 100), to: CGPointMake(200, 50), | |
tailWidth: 10, headWidth: 25, headLength: 40) |
#import <UIKit/UIKit.h> | |
@interface UIBezierPath (dqd_arrowhead) | |
+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint | |
toPoint:(CGPoint)endPoint | |
tailWidth:(CGFloat)tailWidth | |
headWidth:(CGFloat)headWidth | |
headLength:(CGFloat)headLength; | |
@end |
// To the extent possible under law, I (Rob Mayoff, the author of this work) have waived | |
// all copyright and related or neighboring rights to this work, in accordance with | |
// the CC0 1.0 Universal Public Domain Dedication. Please see this page | |
// for details: http://creativecommons.org/publicdomain/zero/1.0/ | |
#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 { | |
CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y); | |
CGPoint points[kArrowPointCount]; | |
[self dqd_getAxisAlignedArrowPoints:points | |
forLength:length | |
tailWidth:tailWidth | |
headWidth:headWidth | |
headLength:headLength]; | |
CGAffineTransform transform = [self dqd_transformForStartPoint:startPoint | |
endPoint:endPoint | |
length:length]; | |
CGMutablePathRef cgPath = CGPathCreateMutable(); | |
CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points); | |
CGPathCloseSubpath(cgPath); | |
UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath]; | |
CGPathRelease(cgPath); | |
return uiPath; | |
} | |
+ (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); | |
} | |
+ (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 |
How to use it in a View?
Nice work! For anyone interested, here is a MonoTouch / C# port: https://gist.github.com/pragmatrix/6406094
I made a Swift extension for this :) https://gist.github.com/mwermuth/07825df27ea28f5fc89a
bless the beasts and the children, and the developers who convert code to Xamarin and Swift.
However, I rewrote the Monotouch example to use CGPoint rather than PointF, because it didn't compile, failing on the AddLines call, not liking the PointF[].
Note: I am compiling using the new Unified 64 bit compiler for OSX, not MonoTouch, so perhaps there is a difference in what will and will not work.
static class ArrowPath
{
public static CGPath pathWithArrowFromPoint (
PointF startPoint,
PointF endPoint,
float tailWidth,
float headWidth,
float headLength)
{
var dx = endPoint.X - startPoint.X;
var dy = endPoint.Y - startPoint.Y;
var length = (float)Math.Sqrt (dx * dx + dy * dy);
var points = getAxisAlignedArrowPoints (length, tailWidth, headWidth, headLength);
var transform = transformForStartPoint (startPoint, endPoint, length);
var path = new CGPath ();
path.AddLines (transform, points);
path.CloseSubpath ();
return path;
}
static CGPoint[] getAxisAlignedArrowPoints (
float length,
float tailWidth,
float headWidth,
float headLength)
{
var tailLength = length - headLength;
var points = new CGPoint[7];
points [0] = new CGPoint (0, tailWidth / 2);
points [1] = new CGPoint (tailLength, tailWidth / 2);
points [2] = new CGPoint (tailLength, headWidth / 2);
points [3] = new CGPoint (length, 0);
points [4] = new CGPoint (tailLength, -headWidth / 2);
points [5] = new CGPoint (tailLength, -tailWidth / 2);
points [6] = new CGPoint (0, -tailWidth / 2);
return points;
}
static CGAffineTransform transformForStartPoint (CGPoint startPoint, CGPoint endPoint, float length)
{
var cosine = (endPoint.X - startPoint.X) / length;
var sine = (endPoint.Y - startPoint.Y) / length;
return new CGAffineTransform (cosine, sine, -sine, cosine, startPoint.X, startPoint.Y);
}
}
You need to import "UIBezierPath+dqd_arrowhead.h" file
Then you can use it like this Ex
-
(void)viewDidLoad {
[super viewDidLoad];
[self drawArrow];
}
-(void)drawArrow{
UIBezierPath *path=[UIBezierPath dqd_bezierPathWithArrowFromPoint:CGPointMake(50, 50)
toPoint:CGPointMake(70, 80)
tailWidth:20.0f
headWidth:10.0f
headLength:10.0f];
CAShapeLayer *shape = [CAShapeLayer layer];
shape.path = path.CGPath;
shape.fillColor = [UIColor colorWithRed:255/255.0 green:20/255.0 blue:147/255.0 alpha:1].CGColor;
[self.view.layer addSublayer:shape];
}
Anyone know how to convert Arrow.swift to Swift 3 syntax?
I'm having trouble with CGPathAddLines(path, &transform, &points, points.count)
Super awesome! Thanks for this gist, @mayoff!
Here is my Swift 3.0 version:
// Swift 3.0 syntax/api
import UIKit
extension UIBezierPath {
class func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> Self {
let length = hypot(end.x - start.x, end.y - start.y)
let tailLength = length - headLength
func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
let points: [CGPoint] = [
p(0, tailWidth / 2),
p(tailLength, tailWidth / 2),
p(tailLength, headWidth / 2),
p(length, 0),
p(tailLength, -headWidth / 2),
p(tailLength, -tailWidth / 2),
p(0, -tailWidth / 2)
]
let cosine = (end.x - start.x) / length
let sine = (end.y - start.y) / length
let transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)
let path = CGMutablePath()
path.addLines(between: points, transform: transform )
path.closeSubpath()
return self.init(cgPath: path)
}
}
Integrating it into a UIView:
override func draw(_ rect: CGRect) {
let arrow = UIBezierPath.arrow(from: CGPoint(x:10, y:10), to: CGPoint(x:200, y:10),tailWidth: 10, headWidth: 25, headLength: 40)
let shapeLayer = CAShapeLayer()
shapeLayer.path = arrow.cgPath
self.layer.addSublayer(shapeLayer)
}
Just what I needed. Incorporating it into an app now to use as a alternative to a graphic underlay. Thanks a million for sharing this. You rock!!
How to draw without closed head, need only line for tail
See http://stackoverflow.com/questions/13528898/how-to-draw-arrowhead-line-xcode for an explanation of this code.