Fun with UIBezierPath and CAShapeLayer

January 30, 2014

This is a quick prototype for a fun drawing tool - as you drag your finger across the canvas the line grows branches which sprout leaves. The branches are randomly generated within certain parameters and animate on while you draw the main line.

A line is drawn, with branches automatically being added along its path

Yes, the leaves are very realistic looking, thank you.

The Code

It’s all on on GitHub, feel free to use and improve!

It’s not about the line drawing

The line drawing is very basic - simply adding points to a UIBezierPath. I keep an array of the curves and draw them all in drawRect:. I don’t care about smooth curves or different textures or performance but I’m sure this will work with more sophisticated drawing code too. Most of the drawing code I’ve shipped has been OpenGL based, so it was nice to see how good the results are when keeping things super simple with UIKit / CoreGraphics.

How it Works

Let’s start with the basic line and layer on the other bits. It starts with a pan gesture recogniser in our UIView subclass.

UIPanGestureRecognizer *pgr = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];

[self addGestureRecognizer:pgr];

Now in the action selector we create new paths when a pan begins, and add to the current path when a pan changes.

-(void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer
{
    if(gestureRecognizer.state == UIGestureRecognizerStateBegan)
    {
        UIBezierPath *newVineLine = [[UIBezierPath alloc] init];
        [newVineLine moveToPoint:[gestureRecognizer locationInView:self]];
        [vineLines addObject:newVineLine];
    }
    else if(gestureRecognizer.state == UIGestureRecognizerStateChanged)
    {
        UIBezierPath *currentLine = [vineLines lastObject];
        [currentLine addLineToPoint:[gestureRecognizer locationInView:self]];
    }

    [self setNeedsDisplay];
}

You can see we’ve a mutable array called vineLines that we’re holding our paths in. This means to draw our paths we can simply iterate over that like so:

- (void)drawRect:(CGRect)rect
{
    for(VineLine *vineLine in vineLines)
    {
        [vineLine stroke];
    }
}

That’s the basic line drawing done, now let’s add some branches. Again they’re UIBezierPaths. Every so often we want to generate a random path, the branch, and add it to the user drawn path, the vine. There are a couple of options for this, we could let the view / view controller keep track of the branches and when to draw them but let’s encapsulate all that in a VineLine, a subclass of UIBezierPath. (In the snippet above just swap out UIBezierPath with our new subclass.

@interface VineLine : UIBezierPath

@property(nonatomic, retain, readonly)NSMutableArray *branchLines;

@end

Rather than just subclassing NSObject and having a property for our path we’re subclassing UIBezierPath and overriding addLineToPoint:, adding functionality to the existing method to decide when to create our branch and add it to the branchLines array. Note that VineBranch is just another UIBezierPath subclass that can create random paths with leaves on the end. All we’re doing here is checking if the point we’re adding is far enough away from the last branch (or beginning of the line) to create a branch and if it is, creating a new random branch and storing it in an array of branches.

-(void)addLineToPoint:(CGPoint)point
{
    [super addLineToPoint:point];
    
    float distanceFromPrevious;
    
    if([_branchLines count] == 0)
    {
        distanceFromPrevious = hypotf(point.x - firstPoint.x, point.y - firstPoint.y);
    }
    else
    {
        distanceFromPrevious = hypotf(point.x - lastBranchPosition.x, point.y - lastBranchPosition.y);
    }
    
    if(distanceFromPrevious > _minBranchSeperation)
    {
        VineBranch *newBranch = [[VineBranch alloc] initWithRandomPathFromPoint:point maxLength:_maxBranchLength leafSize:_leafSize];
        newBranch.lineWidth = self.lineWidth / 2.0;
        
        [_branchLines addObject:newBranch];
        lastBranchPosition = point;
    }
}

If we modify our drawRect: from before we can now draw the branches and leaves as well as the main line.

- (void)drawRect:(CGRect)rect
{
    [vineColor setStroke];
    
    for(VineLine *vineLine in vineLines)
    {
        [vineLine stroke];

        for(UIBezierPath *branchLine in vineLine.branchLines)
        {
            [branchLine stroke];
        }
    }
}

And we’re done!

Animating The Branches

That’s where CAShapeLayer comes in. CAShapeLayer has a number of animatable properties, and animating strokeEnd is great for drawing a path to the screen. So we can remove the code to iterate through the list of branches and stroke them instead, every time a branch is created we create a layer for it and animate the stroke.

-(void)vineLineDidCreateBranch:(VineBranch *)branchPath
{
    CAShapeLayer *branchShape = [CAShapeLayer layer];
    branchShape.path = branchPath.CGPath;
    branchShape.fillColor = [UIColor clearColor].CGColor;
    branchShape.strokeColor = vineColor.CGColor;
    branchShape.lineWidth = branchPath.lineWidth;
    
    [self.layer addSublayer:branchShape];
    
    CABasicAnimation *branchGrowAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    branchGrowAnimation.duration = 1.0;
    branchGrowAnimation.fromValue = [NSNumber numberWithFloat:0.0];
    branchGrowAnimation.toValue = [NSNumber numberWithFloat:1.0];
    [branchShape addAnimation:branchGrowAnimation forKey:@"strokeEnd"];
}

We can make our view the VineLine’s delegate and add a call to the delegate notifying it of a new branch in our addLineToPoint: method from above.

Random Paths

Initially I tried to be clever and see what way the line was curving and attach curves that seemed natural but that wasn’t looking too good. Eventually I just started throwing random numbers at it and things started looking better (this probably should have been obvious to me). So what we’re doing here is getting a random point close to the main line (as defined by _maxLength) and adding a curve to that point, control points are picked near that end point so we don’t end up with curves that are too crazy. Finally, we add the leaf, which for now is just a circle.

-(id)initWithRandomPathFromPoint:(CGPoint)startPoint maxLength:(float)maxLength leafSize:(float)leafSize
{
    self = [super init];
    if(self)
    {
        [self moveToPoint:startPoint];
        
        CGPoint branchEnd = CGPointMake(startPoint.x + arc4random_uniform(maxLength * 2) - maxLength,startPoint.y + arc4random_uniform(maxLength * 2) - maxLength);
        CGPoint brachControl1 = CGPointMake(branchEnd.x + arc4random_uniform(maxLength) - maxLength / 2,branchEnd.y + arc4random_uniform(maxLength) - maxLength / 2);
        CGPoint branchControl2 = CGPointMake(branchEnd.x + arc4random_uniform(maxLength / 2) - maxLength / 4,branchEnd.y + arc4random_uniform(maxLength / 2) - maxLength / 4);
        
        [self addCurveToPoint:branchEnd controlPoint1:brachControl1 controlPoint2:branchControl2];
        
        UIBezierPath* leafPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(branchEnd.x - leafSize/2.0, branchEnd.y - leafSize/2.0, leafSize, leafSize)];
        
        [self appendPath:leafPath];
    }
    return self;
}