Place images along a bezier path
Does anyone know how to place images along a bezier path? I can write the path fine and animate a sprite along the path but I want to make the path a series of arrows instead of dotted lines. I assumed there must be a way to add arrow images all along the path but can't find it. Also the paths are all curved:
UIBezierPath * path = [UIBezierPath bezierPath];
[path moveToPoint:startingPoint];
[path addCurveToPoint:endPoint controlPoint1:controlPoint1 controlPoint2:controlPoint2];
I guess you want something like this:
You can find my complete demo app project in this github repository.
Anyway, this is an interesting little problem.
You need to generate an array of points along the path, and I assume you want them to be equally spaced. Generating this points isn't trivial.
Fortunately, Core Graphics contains a function that will do it for you, but it's not obvious which one. The function is CGPathCreateCopyByDashingPath
.
First, let's make a UIBezierPath
category that creates a dashed copy:
UIBezierPath+Rob_dash.h
#import <UIKit/UIKit.h>
@interface UIBezierPath (Rob_dash)
- (instancetype)Rob_dashedPathWithPattern:(NSArray *)pattern phase:(CGFloat)phase;
@end
UIBezierPath+Rob_dash.m
#import "UIBezierPath+Rob_dash.h"
@implementation UIBezierPath (Rob_dash)
- (instancetype)Rob_dashedPathWithPattern:(NSArray *)pattern phase:(CGFloat)phase {
CGFloat lengths[pattern.count];
size_t i = 0;
for (NSNumber *number in pattern) {
lengths[i++] = number.doubleValue;
}
CGPathRef dashedCGPath = CGPathCreateCopyByDashingPath(self.CGPath, NULL, phase, lengths, pattern.count);
UIBezierPath *dashedPath = [self.class bezierPathWithCGPath:dashedCGPath];
CGPathRelease(dashedCGPath);
return dashedPath;
}
@end
Once we have a dashed path, we need to enumerate the elements of the path (the individual commands like moveToPoint:
, addLineToPoint:
, and so on). The only way to do that is using another Core Graphics function CGPathApply
. Let's write another UIBezierPath
category that uses blocks to make it easier. This one's a bit longer:
UIBezierPath+Rob_forEach.h
#import <UIKit/UIKit.h>
typedef void (^Rob_UIBezierPath_moveBlock)(CGPoint destination);
typedef void (^Rob_UIBezierPath_lineBlock)(CGPoint destination);
typedef void (^Rob_UIBezierPath_quadBlock)(CGPoint control, CGPoint destination);
typedef void (^Rob_UIBezierPath_cubicBlock)(CGPoint control0, CGPoint control1, CGPoint destination);
typedef void (^Rob_UIBezierPath_closeBlock)(void);
@interface UIBezierPath (Rob_forEach)
- (void)Rob_forEachMove:(Rob_UIBezierPath_moveBlock)moveBlock line:(Rob_UIBezierPath_lineBlock)lineBlock quad:(Rob_UIBezierPath_quadBlock)quadBlock cubic:(Rob_UIBezierPath_cubicBlock)cubicBlock close:(Rob_UIBezierPath_closeBlock)closeBlock;
@end
UIBezierPath+Rob_forEach.m
#import "UIBezierPath+Rob_forEach.h"
struct ForEachBlocks {
__unsafe_unretained Rob_UIBezierPath_moveBlock moveBlock;
__unsafe_unretained Rob_UIBezierPath_lineBlock lineBlock;
__unsafe_unretained Rob_UIBezierPath_quadBlock quadBlock;
__unsafe_unretained Rob_UIBezierPath_cubicBlock cubicBlock;
__unsafe_unretained Rob_UIBezierPath_closeBlock closeBlock;
};
static void applyBlockToPathElement(void *info, const CGPathElement *element) {
struct ForEachBlocks *blocks = info;
switch (element->type) {
case kCGPathElementMoveToPoint:
if (blocks->moveBlock != nil) {
blocks->moveBlock(element->points[0]);
}
break;
case kCGPathElementAddLineToPoint:
if (blocks->lineBlock != nil) {
blocks->lineBlock(element->points[0]);
}
break;
case kCGPathElementAddQuadCurveToPoint:
if (blocks->quadBlock) {
blocks->quadBlock(element->points[0], element->points[1]);
}
break;
case kCGPathElementAddCurveToPoint:
if (blocks->cubicBlock) {
blocks->cubicBlock(element->points[0], element->points[1], element->points[2]);
}
break;
case kCGPathElementCloseSubpath:
if (blocks->closeBlock) {
blocks->closeBlock();
}
break;
}
}
@implementation UIBezierPath (Rob_forEach)
- (void)Rob_forEachMove:(Rob_UIBezierPath_moveBlock)moveBlock line:(Rob_UIBezierPath_lineBlock)lineBlock quad:(Rob_UIBezierPath_quadBlock)quadBlock cubic:(Rob_UIBezierPath_cubicBlock)cubicBlock close:(Rob_UIBezierPath_closeBlock)closeBlock {
struct ForEachBlocks blocks = {
.moveBlock = moveBlock,
.lineBlock = lineBlock,
.quadBlock = quadBlock,
.cubicBlock = cubicBlock,
.closeBlock = closeBlock
};
CGPathApply(self.CGPath, &blocks, applyBlockToPathElement);
}
@end
OK, now we want to use these two categories together to dash the path, then walk along the dashes and emit the point at the end of each dash. Note that a dash might consist of multiple contiguous line/curve segments. We need to watch for move commands to know when a dash ends. Also, to draw each arrow at the correct angle, we need to know the tangent of the curve at each point, so we'll compute that also, as a unit vector. In the case of a straight line segment, the tangent vector is parallel to the line segment. In the case of curves, the control point immediately prior to the endpoint of the curve determines the tangent at the endpoint.
UIBezierPath+Rob_points.h
#import <UIKit/UIKit.h>
@interface UIBezierPath (Rob_points)
- (void)Rob_forEachPointAtInterval:(CGFloat)interval perform:(void (^)(CGPoint point, CGVector vector))block;
@end
UIBezierPath+Rob_points.m
#import "UIBezierPath+Rob_points.h"
#import "UIBezierPath+Rob_dash.h"
#import "UIBezierPath+Rob_forEach.h"
#import <tgmath.h>
static CGVector vectorFromPointToPoint(CGPoint tail, CGPoint head) {
CGFloat length = hypot(head.x - tail.x, head.y - tail.y);
return CGVectorMake((head.x - tail.x) / length, (head.y - tail.y) / length);
}
@implementation UIBezierPath (Rob_points)
- (void)Rob_forEachPointAtInterval:(CGFloat)interval perform:(void (^)(CGPoint, CGVector))block {
UIBezierPath *dashedPath = [self Rob_dashedPathWithPattern:@[ @(interval * 0.5), @(interval * 0.5) ] phase:0];
__block BOOL hasPendingSegment = NO;
__block CGPoint pendingControlPoint;
__block CGPoint pendingPoint;
[dashedPath Rob_forEachMove:^(CGPoint destination) {
if (hasPendingSegment) {
block(pendingPoint, vectorFromPointToPoint(pendingControlPoint, pendingPoint));
hasPendingSegment = NO;
}
pendingPoint = destination;
} line:^(CGPoint destination) {
pendingControlPoint = pendingPoint;
pendingPoint = destination;
hasPendingSegment = YES;
} quad:^(CGPoint control, CGPoint destination) {
pendingControlPoint = control;
pendingPoint = destination;
hasPendingSegment = YES;
} cubic:^(CGPoint control0, CGPoint control1, CGPoint destination) {
pendingControlPoint = control1;
pendingPoint = destination;
hasPendingSegment = YES;
} close:nil];
if (hasPendingSegment) {
block(pendingPoint, vectorFromPointToPoint(pendingControlPoint, pendingPoint));
}
}
@end
Now we can find points along a path, and the unit tangent vector at each point. Let's make a custom view that uses this ability in drawRect:
:
ArrowView.h
#import <UIKit/UIKit.h>
@interface ArrowView : UIView
@property (nonatomic) CGFloat interval;
@end
ArrowView.m
#import "ArrowView.h"
#import "UIBezierPath+Rob_figureEight.h"
#import "UIBezierPath+Rob_points.h"
@implementation ArrowView
- (void)setInterval:(CGFloat)interval {
_interval = interval;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
UIImage *arrow = [UIImage imageNamed:@"right233.png"];
UIBezierPath *path = [UIBezierPath Rob_figureEightInRect:CGRectInset(self.bounds, 40, 40)];
// [path stroke];
[path Rob_forEachPointAtInterval:self.interval perform:^(CGPoint point, CGVector vector) {
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextSaveGState(gc); {
CGContextTranslateCTM(gc, point.x, point.y);
CGContextConcatCTM(gc, CGAffineTransformMake(vector.dx, vector.dy, -vector.dy, vector.dx, 0, 0));
CGContextTranslateCTM(gc, -0.5 * arrow.size.width, -0.5 * arrow.size.height);
// UIRectFrame((CGRect){ CGPointZero, arrow.size });
[arrow drawAtPoint:CGPointZero];
} CGContextRestoreGState(gc);
}];
}
@end
That's all there is to it, if you want to draw arrow images along a path.
There's a little bonus in my demo app repository. If you go back to the first commit, I implemented a different solution also: a category that takes a path and “arrowizes” it, putting an arrowhead at the end of each subpath. If you combine that with dashing (as I did in that version of the project), you get arrows along the path. But it ended up not looking as nice as using arrow images.
Here is a Swift version of Rob's idea.
extension UIBezierPath {
func forEachPoint(interval: CGFloat, block: (_ point: CGPoint, _ vector: CGVector) -> Void) {
let path = dashedPath(pattern: [interval * 0.5, interval * 0.5])
path.forEachPoint { point, vector in
block(point, vector)
}
}
private func dashedPath(pattern: [CGFloat]) -> UIBezierPath {
let dashedPath = cgPath.copy(dashingWithPhase: 0, lengths: pattern)
return UIBezierPath(cgPath: dashedPath)
}
private var elements: [PathElement] {
var pathElements = [PathElement]()
cgPath.applyWithBlock { elementsPointer in
let element = PathElement(element: elementsPointer.pointee)
pathElements.append(element)
}
return pathElements
}
private func forEachPoint(_ block: (_ point: CGPoint, _ vector: CGVector) -> Void) {
var hasPendingSegment: Bool = false
var pendingControlPoint = CGPoint.zero
var pendingPoint = CGPoint.zero
for pathElement in elements {
switch pathElement {
case let .moveToPoint(destinationPoint):
if hasPendingSegment {
block(pendingPoint, vector(from: pendingControlPoint, to: pendingPoint))
hasPendingSegment = false
}
pendingPoint = destinationPoint
case let .addLineToPoint(destinationPoint):
pendingControlPoint = pendingPoint
pendingPoint = destinationPoint
hasPendingSegment = true
case let .addQuadCurveToPoint(controlPoint, destinationPoint):
pendingControlPoint = controlPoint
pendingPoint = destinationPoint
hasPendingSegment = true
case let .addCurveToPoint(controlPoint1, _, destinationPoint):
pendingControlPoint = controlPoint1
pendingPoint = destinationPoint
hasPendingSegment = true
case .closeSubpath:
break
}
}
if hasPendingSegment {
block(pendingPoint, vector(from: pendingControlPoint, to: pendingPoint))
}
}
private func vector(from point1: CGPoint, to point2: CGPoint) -> CGVector {
let length = hypot(point2.x - point1.x, point2.y - point1.y)
return CGVector(dx: (point2.x - point1.x) / length, dy: (point2.y - point1.y) / length)
}
}
enum PathElement {
case moveToPoint(CGPoint)
case addLineToPoint(CGPoint)
case addQuadCurveToPoint(CGPoint, CGPoint)
case addCurveToPoint(CGPoint, CGPoint, CGPoint)
case closeSubpath
init(element: CGPathElement) {
switch element.type {
case .moveToPoint: self = .moveToPoint(element.points[0])
case .addLineToPoint: self = .addLineToPoint(element.points[0])
case .addQuadCurveToPoint: self = .addQuadCurveToPoint(element.points[0], element.points[1])
case .addCurveToPoint: self = .addCurveToPoint(element.points[0], element.points[1], element.points[2])
case .closeSubpath: self = .closeSubpath
@unknown default:
fatalError("Unknown CGPathElement type")
}
}
}
How to use it
override func draw(_ rect: CGRect) {
guard let image = UIImage(named: "some_image") else { return }
// create the path you need
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))
// place image to the path with a given interval
path.forEachPoint(interval: interval) { point, vector in
guard let gc = UIGraphicsGetCurrentContext() else { return }
gc.saveGState()
gc.translateBy(x: point.x, y: point.y)
gc.concatenate(CGAffineTransform(a: vector.dx, b: vector.dy, c: -vector.dy, d: vector.dx, tx: 0, ty: 0))
gc.translateBy(x: -0.5 * image.size.width, y: -0.5 * image.size.width)
image.draw(at: .zero)
gc.restoreGState()
}
}