Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animating Bezier curve as though drawing proceeds on iPhone screen

I want to draw a bezier path of the outline of Taj Mahal's tomb.

Q1. How could I draw it in XCode? I could easily do that in PS but am finding it difficult to do in XCode programmatically especially getting the coordinates of the control points.

Q2. Moreover, how can the coordinates of the points be fixed when the screen sizes vary?

Q3. How could I animate it as though an invisible pencil is drawing that on screen?

like image 445
Viki Avatar asked Feb 12 '16 12:02

Viki


People also ask

What is UIBezierPath?

A UIBezierPath object combines the geometry of a path with attributes that describe the path during rendering. You set the geometry and attributes separately and can change them independent of one another. After you have the object configured the way you want it, you can tell it to draw itself in the current context.

What is Bezier default curves?

The Bezier curve is pulled or twisted in the direction you drag. By default, the opposing tangent handle is locked to 180 degrees, and adjusting the angle of one tangent handle adjusts the other.


3 Answers

How do I draw a bezier path in UIKit?

Create a UIBezierPath object and then use the various methods on it to construct your path.

I suspect the methods you're going to be most interested in are:

  • moveToPoint: (Fairly self-explanitary, moves the current point to a given point)

  • addLineToPoint: (Adds a straight line, starting at the current point and ending at a given point)

  • addCurveToPoint:controlPoint1:controlPoint2: (Adds a bezier curve, starting the current point, with a set end point and control points)

  • addQuadCurveToPoint:controlPoint: Adds a quadratic curve, starting at the current point, with a set end point and control point.

For example, something like this will create a simple bezier path with a quadratic curve:

UIBezierPath* yourPath = [UIBezierPath bezierPath]; // create the bezier path
[yourPath moveToPoint:CGPointMake(100, 100)]; // move to your starting point
[yourPath addQuadCurveToPoint:CGPointMake(300, 500) controlPoint:CGPointMake(500, 350)]; // add a new quad curve to the point

However, if you're used to using something like Photoshop to create your paths, you may be interested in PaintCode, that can let you create bezier paths using a 'what you see is what you get' approach.

You can add this UIBezierPath to the path property of a CAShapeLayer in order to display it on screen.

CAShapeLayer* yourShapeLayer = [CAShapeLayer layer]; // create your shape layer
yourShapeLayer.frame = CGRectMake(0, 0, 300, 500); // assign it's frame
yourShapeLayer.path = yourPath.CGPath; // add your path to the shape layer
yourShapeLayer.fillColor = [UIColor clearColor].CGColor; // prevent the shape layer from filling

[self.view.layer addSublayer:yourShapeLayer]; // add the shape layer to the view

You can then easily set the strokeColor and lineWidth properties on the shape layer in order to customise how your path gets stroked.

yourShapeLayer.strokeColor = [UIColor redColor].CGColor; // red stroke color
yourShapeLayer.lineWidth = 5.0; // 5 point stroke width

You should end up with something like this:

enter image description here


How could I animate it as though an invisible pencil is drawing that on screen?

You can easily create a CABasicAnimation that can animate the strokeStart or strokeEnd property of the CAShapeLayer in order to achieve your desired animation.

This can result in the animation effect of the line 'getting drawn' onto the screen.

For example, this code will result in the strokeEnd property animating from 0.0 to 1.0, creating your desired effect:

yourShapeLayer.strokeStart = 0.0; // reset stroke start before animating

CABasicAnimation* strokeAnim = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; // create animation that changes the strokeEnd property
strokeAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; // give it a nice timing function
strokeAnim.duration = 2.0; // duration of the animation
strokeAnim.fromValue = @(0.0); // provide the start value for the animation, wrapped in an NSNumber literal.
strokeAnim.toValue = @(1.0); // provide the end value for the animation, wrapped in an NSNumber literal.

[yourShapeLayer addAnimation:strokeAnim forKey:@"strokeAnim"]; // add animation to layer

enter image description here


How can I scale this path to work with different sized screens?

The way I usually like to solve this problem is to define the path's points in a given fixed frame (let's say 500 x 500 points), and then generate a scale factor based on the size of the screen (in your case, you want to scale to the width of the screen).

So all we have to do is generate our scale factor.

CGFloat sf = self.view.frame.size.width/500.0; // The factor to scale the path by

Then, we just have to multiple all our points in the UIBezierPath by this scale factor. For example, taking our previous path:

UIBezierPath* yourPath = [UIBezierPath bezierPath]; // create the bezier path
[yourPath moveToPoint:CGPointMake(100.0*sf, 100.0*sf)]; // move to your starting point
[yourPath addQuadCurveToPoint:CGPointMake(300.0*sf, 500.0*sf) controlPoint:CGPointMake(500.0*sf, 350.0*sf)];

Now, all we have to do is program in our bezier path points as if we were always drawing it in a 500 x 500 frame (my points already fit nicely within that).

You'll also want to change the size of your CAShapeLayer so that it's squared and occupies the entire width of the screen.

yourShapeLayer.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.width);

Finally, you want to position the CAShapeLayer to the center of the screen. You can do this easily by setting the position property of the layer to the center of the view.

yourShapeLayer.position = self.view.center;

Now your path should always scale to occupy the screen's width.

enter image description here

(I added a gray background to the layer just to illustrate the scaling)


Full project (for your convenience): https://github.com/hamishknight/Drawing-Scaling-Animating-UIBezierPaths

like image 168
Hamish Avatar answered Jan 30 '23 11:01

Hamish


I blogged about this in 2010: Animating the Drawing of a CGPath With CAShapeLayer.

The gist of it is this:

  • Create a UIBezierPath or CGPath with the shape you want to draw. Use the methods moveToPoint:, addLineToPoint:, addCurveToPoint:controlPoint1:controlPoint2: etc. to create the shape.
  • Create a CGShapeLayer and assign the path to the layer's path property.
  • Add the layer to your view/layer hierarchy (e.g. in a view controller: [self.view.layer addSublayer:shapeLayer];). You also have to give the layer a valid size and position (frame).
  • Animate the layer's strokeEnd property from 0 to 1.
like image 34
Ole Begemann Avatar answered Jan 30 '23 10:01

Ole Begemann


The answer posted by originaluser2 solves everything. You can also try this way, if you want to resize the bezier path for different dievice screens:

You can simply subclass the UIView and customise its drawRect method:

  - (void)drawRect: (CGRect)frame
{

    //// Bezier Drawing
UIBezierPath* bezierPath = [UIBezierPath bezierPath];
[bezierPath moveToPoint: CGPointMake(CGRectGetMinX(frame) + 0.23370 * CGRectGetWidth(frame), CGRectGetMinY(frame) + 0.65441 * CGRectGetHeight(frame))];
[bezierPath addCurveToPoint: CGPointMake(CGRectGetMinX(frame) + 0.49457 * CGRectGetWidth(frame), CGRectGetMinY(frame) + 0.33088 * CGRectGetHeight(frame)) controlPoint1: CGPointMake(CGRectGetMinX(frame) + 0.23370 * CGRectGetWidth(frame), CGRectGetMinY(frame) + 0.65441 * CGRectGetHeight(frame)) controlPoint2: CGPointMake(CGRectGetMinX(frame) + 0.35870 * CGRectGetWidth(frame), CGRectGetMinY(frame) + 0.33088 * CGRectGetHeight(frame))];
[bezierPath addCurveToPoint: CGPointMake(CGRectGetMinX(frame) + 0.77717 * CGRectGetWidth(frame), CGRectGetMinY(frame) + 0.65441 * CGRectGetHeight(frame)) controlPoint1: CGPointMake(CGRectGetMinX(frame) + 0.63043 * CGRectGetWidth(frame), CGRectGetMinY(frame) + 0.33088 * CGRectGetHeight(frame)) controlPoint2: CGPointMake(CGRectGetMinX(frame) + 0.77717 * CGRectGetWidth(frame), CGRectGetMinY(frame) + 0.65441 * CGRectGetHeight(frame))];
[UIColor.blackColor setStroke];
bezierPath.lineWidth = 1;
[bezierPath stroke];

}

frame 50x50:

enter image description here

frame 100x100:

enter image description here

frame 100x50

enter image description here

This draws a bezier curve in a frame of size 50x50 if you send frame of size 50x50. Also this code get resized automatically based on the frame size it recieves.

like image 34
Teja Nandamuri Avatar answered Jan 30 '23 11:01

Teja Nandamuri