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:

arrows demo app

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()
    }
  }