IOS : Animate transformation from a line to a bezier curve

Nicolas Henin picture Nicolas Henin · Jan 28, 2013 · Viewed 8.7k times · Source

I would like to animate a straight line curving into a bezier curve (from "_" to "n"), is there a library somewhere that can help me to do it ?

I know how to draw a Bezier curve with UIBezierPath, I could redraw rapidly and progressively do the transformation, but if something already does that it would be cool :-)

Answer

Rob picture Rob · Jan 28, 2013

I might do something with a CADisplayLink. For example, you could do this in your view controller using a CAShapeLayer, e.g.:

#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>

@interface ViewController ()

@property (nonatomic) CFTimeInterval firstTimestamp;
@property (nonatomic, strong) CAShapeLayer *shapeLayer;
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic) NSUInteger loopCount;

@end

static CGFloat const kSeconds = 5.0;

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self addShapeLayer];
    [self startDisplayLink];
}

- (void)addShapeLayer
{
    self.shapeLayer = [CAShapeLayer layer];
    self.shapeLayer.path = [[self pathAtInterval:0.0] CGPath];
    self.shapeLayer.fillColor = [[UIColor clearColor] CGColor];
    self.shapeLayer.lineWidth = 3.0;
    self.shapeLayer.strokeColor = [[UIColor redColor] CGColor];
    [self.view.layer addSublayer:self.shapeLayer];
}

- (void)startDisplayLink
{
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)stopDisplayLink
{
    [self.displayLink invalidate];
    self.displayLink = nil;
}

- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
    if (!self.firstTimestamp)
        self.firstTimestamp = displayLink.timestamp;

    self.loopCount++;

    NSTimeInterval elapsed = (displayLink.timestamp - self.firstTimestamp);

    self.shapeLayer.path = [[self pathAtInterval:elapsed] CGPath];

    if (elapsed >= kSeconds)
    {
        [self stopDisplayLink];
        self.shapeLayer.path = [[self pathAtInterval:0] CGPath];

        self.statusLabel.text = [NSString stringWithFormat:@"loopCount = %.1f frames/sec", self.loopCount / kSeconds];
    }
}

- (UIBezierPath *)pathAtInterval:(NSTimeInterval) interval
{
    UIBezierPath *path = [UIBezierPath bezierPath];

    [path moveToPoint:CGPointMake(0, self.view.bounds.size.height / 2.0)];

    CGFloat fractionOfSecond = interval - floor(interval);

    CGFloat yOffset = self.view.bounds.size.height * sin(fractionOfSecond * M_PI * 2.0);

    [path addCurveToPoint:CGPointMake(self.view.bounds.size.width, self.view.bounds.size.height / 2.0)
            controlPoint1:CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0 - yOffset)
            controlPoint2:CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0 + yOffset)];

    return path;
}

@end

animated image


Alternatively, if you wanted to do it by subclassing UIView, you could do it like so:

#import "View.h"
#import <QuartzCore/QuartzCore.h>

@interface View ()

@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic) CFTimeInterval firstTimestamp;
@property (nonatomic) CFTimeInterval displayLinkTimestamp;
@property (nonatomic) NSUInteger loopCount;

@end

static CGFloat const kSeconds = 5.25;

@implementation View

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self startDisplayLink];
    }

    return self;
}

- (void)startDisplayLink
{
    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)stopDisplayLink
{
    [self.displayLink invalidate];
    self.displayLink = nil;
}

- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
    if (!self.firstTimestamp)
        self.firstTimestamp = displayLink.timestamp;

    self.displayLinkTimestamp = displayLink.timestamp;

    self.loopCount++;

    [self setNeedsDisplayInRect:self.bounds];

    NSTimeInterval elapsed = (displayLink.timestamp - self.firstTimestamp);

    if (elapsed >= kSeconds)
    {
        [self stopDisplayLink];
        self.displayLinkTimestamp = self.firstTimestamp + kSeconds;
        [self setNeedsDisplayInRect:self.bounds];
        self.statusLabel.text = [NSString stringWithFormat:@"loopCount = %.1f frames/sec", self.loopCount / kSeconds];
    }
}

- (UIBezierPath *)pathAtInterval:(NSTimeInterval) interval
{
    UIBezierPath *path = [UIBezierPath bezierPath];

    [path moveToPoint:CGPointMake(0, self.bounds.size.height / 2.0)];

    CGFloat fractionOfSecond = interval - floor(interval);

    CGFloat yOffset = self.bounds.size.height * sin(fractionOfSecond * M_PI * 2.0);

    [path addCurveToPoint:CGPointMake(self.bounds.size.width, self.bounds.size.height / 2.0)
            controlPoint1:CGPointMake(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0 - yOffset)
            controlPoint2:CGPointMake(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0 + yOffset)];

    return path;
}


- (void)drawRect:(CGRect)rect
{
    NSTimeInterval elapsed = (self.displayLinkTimestamp - self.firstTimestamp);

    UIBezierPath *path = [self pathAtInterval:elapsed];

    [[UIColor redColor] setStroke];
    path.lineWidth = 3.0;
    [path stroke];
}

@end

I test both the subclassed UIView as well as the view controller, and they both resulted in roughly 60 frames per second.