Calculate UIBezierPath Curved Control Point - ios

I'm struggling to understand how to achieve the following component:
Theory:
I've managed to create 4 draggable UIView'. When one of the UIView change position I'm creating UIBezierPath from connecting each UIView center, to a box shape and displaying it using CAShapeLayer.
I can't understand how to calculate the "addQuadCurve" control point to achieve the curved lines in the illustration.
Current Code:
func updateLines() {
let path = UIBezierPath()
path.move(to: v.center)
path.addLine(to: v2.center)
path.addLine(to: v4.center)
path.addLine(to: v3.center)
path.close()
line.path = path.cgPath
line.strokeColor = UIColor.black.cgColor
line.fillColor = UIColor.red.cgColor
}
Any help or advice for the right direction will be highly appreciated.
Best regards,
Roi

It looks to me like a connected set of cubic Bezier curves where the beginning and end are the same point. If you watch the animation, it looks like as you drag different points, the point that's the beginning/end of the curve gets changed.
Watch carefully and you'll see that 3 of the 4 corners are smooth curves, and one has a "kink" in it. The kinked corner seems to be a point other than the one that's being moved. That's probably the begin/end point.

Related

How can I animate the corner radius of a CALayer

I'm trying to create an animation where a CALayer rectangle changes from being flush with the borders of a view to having either the left, right, or both corners rounded and the width of the rectangle is also changed.
The problem I'm having is that the animation looks distorted. If I disable the corner radius change in the animation, the distortion doesn't happen. I suspect this is because when the original path and the final path in the animation have a different number of rounded corners, the paths have a different number of control points, so the path animation behavior is undefined as per the docs.
I'm thinking that I should try and find a way to get the number of control points in the rounded rect to be equal to the number in the non-rounded rect but I'm not sure how I would do this since I haven't found a way to count the number of control points in a CGPath/UIBezierPath.
Here's the code I'm using right now, where I'm animating the path, but I'm open to changing the implementation entirely to 'cheat' by having two rectangles or something like that.
func setSelection(to color: UIColor, connectedLeft: Bool, connectedRight: Bool, animated: Bool) {
self.connectedLeft = connectedLeft
self.connectedRight = connectedRight
self.color = color
if animated {
let pathAnimation = CABasicAnimation(keyPath: "path")
let colorAnimation = CABasicAnimation(keyPath: "fillColor")
self.configure(animation: pathAnimation)
self.configure(animation: colorAnimation)
pathAnimation.toValue = self.rectPath(connectedLeft: connectedLeft, connectedRight: connectedRight).cgPath
colorAnimation.toValue = color.cgColor
let group = CAAnimationGroup()
group.animations = [pathAnimation, colorAnimation]
self.configure(animation: group)
group.delegate = self
self.rectLayer.add(group, forKey: Constants.selectionChangeAnimationKey)
} else {
self.rectLayer.fillColor = color.cgColor
self.rectLayer.path = self.rectPath(connectedLeft: connectedLeft, connectedRight: connectedRight).cgPath
}
}
private func rectPath(connectedLeft: Bool, connectedRight: Bool) -> UIBezierPath {
let spacing: CGFloat = 5
var origin = self.bounds.origin
var size = self.bounds.size
var corners = UIRectCorner()
if !connectedLeft {
origin.x += spacing
size.width -= spacing
corners.formUnion([.topLeft, .bottomLeft])
}
if !connectedRight {
size.width -= spacing
corners.formUnion([.topRight, .bottomRight])
}
let path = UIBezierPath(roundedRect: .init(origin: origin, size: size), byRoundingCorners: corners, cornerRadii: .init(width: 8, height: 8))
print(path.cgPath)
return path
}
You are correct as to why your animation doesn't work correctly.
The Core Graphics/Core Animation methods that draw arcs compose the arcs using variable numbers of cubic bezier curves depending on the angle being drawn.
I believe that a 90 degree corner is rendered as a single cubic bezier curve.
Based on a recent test I did, it seems all you need to do is have the same number of endpoints as a Bezier curve in order to get the sharp-corner-to-rounded-corner animation to draw correctly. For a rounded corner, I'm pretty sure it draws a line segment up to the rounded corner, then the arc as one Bezier curve, then the next line segment. Thus you SHOULD be able to create a rectangle by drawing each sharp corner point twice. I haven't tried it, but I think it would work.
(Based on my understanding of how arcs of circles are drawn using bezier curve, an arc of a circle is composed of a cubic bezier curve for each quarter-circle or part of a quarter circle that is drawn. So for a 90 degree bend, you just need 2 control points for a sharp corner to replace a rounded corner. If the angle is >90 degrees but less than 180, you'd want 3 control points at the corner, and if it's >180 but less than 270, you'd want 4 control points.)
Edit:
The above approach of adding each corner-point twice for a rounded rectangle SHOULD work for the rounded rectangle case (where every corner is exactly 90 degrees) but it doesn't work for irregular polygons where the angle that needs to be rounded varies. I couldn't work out how to handle that case. I already created a demo project that generated irregular polygons with any mixture of sharp and curved corners, so I adapted it to handle animation.
The project now includes a function that generates CGPaths with a settable mixture of rounded and sharp corners that will animate the transition between sharp corners and rounded corners. The function you want to look at is called roundedRectPath(rect:byRoundingCorners:cornerRadius:)
Here is the function header and in-line documentation
/**
This function works like the UIBezierPath initializer `init(roundedRect:byRoundingCorners:cornerRadii:)` It returns a CGPath a rounded rectangle with a mixture of rounded and sharp corners, as specified by the options in `corners`.
Unlike the UIBezierPath `init(roundedRect:byRoundingCorners:cornerRadii:` intitializer, The CGPath that is returned by this function will animate smoothly from rounded to non-rounded corners.
- Parameter rect: The starting rectangle who's corners you wish to round
- Parameter corners: The corners to round
- Parameter cornerRadius: The corner radius to use
- Returns: A CGPath containing for the rounded rectangle.
*/
public func roundedRectPath(rect: CGRect, byRoundingCorners corners: UIRectCorner, cornerRadius: CGFloat) -> CGPath
You can download the project from Github and try it out at this link.
Here is a GIF of the demo app in operation:

How do you create a feathered "Circle wipe" animation in iOS?

My question requires quite a bit of explanation. If you want to skip to the question, look for the bold "Question:" towards the end.
It's fairly easy to create a "circle wipe" animation in iOS or Mac OS using a Core Animation and masks.
One way is to install a CAShapeLayer as a mask on a view (typically an image view), and installing a circle in the shape layer, and animating the circle from 0 size to large enough to cover the whole image (r = sqrt(height^2 + width^2)
Another way is to use a radial CAGradientLayer as a mask, set up the center of the gradient as an opaque color, and have a sudden transition to clear just beyond the corners of the image. You then animate the "positions" values of the gradient layer to move the clear color in to the center of the image.
Both of these approaches create a sharp-edged "circle wipe" that causes the image to shrink to a point and disappear.
That effect looks like this:
The classic cinematic circle wipe animation has a feathered edge, however, rather than a sharp border. The outer edge of the circle is completely transparent. As you move in towards the center the image becomes more and more solid over a fairly short distance, and then the image is completely opaque from there into the center. As the circle shrinks, the thickness of the transition from transparent to opaque remains constant, and the circle stinks down to a point and disappears. (The mask is a circle is opaque in the middle and has a slightly blurred edge, and the radius of the circle shrinks to 0 while the thickness of the blurred edge remains constant.)
Here is an example of an old school circle wipe. This is technically an "Iris Out" where the transition is to black, but the idea is the same:
https://youtu.be/IqDhAW3TDR8?t=90
I have no idea how to achieve a circle wipe with a feathered edge using a shape layer as a mask. Shape layers are always sharp-edged. I could get fairly close by using a shape layer where the circle is stroked with 50% opacity, and the middle of the circle is filled with a color at 100% opacity, but that wouldn't get the smooth transition from transparent to opaque that I'm after.
Another approach I've tried is using a radial gradient with 3 colors in it. I set the inner colors as opaque, and the outer color as clear. I set the location of the outer color as > 1 (say 1.2), the middle location to 1, and the inner location to 0. The locations array contains [1.2, 1.0, 0] That makes the whole part of the mask that covers the image opaque, with a feathered transition to clear that happens past the edges of the image.
I then do an animation of the locations array in 2 steps.
In the first step, I animate the locations array to [0, 0, 0.2]. That causes a band of feathering to move in from the corners and stop at 0.2 from the center of the image.
In the 2nd step, I animate the locations array from [0, 0, 0.2] to [0,0,0]. That causes an inner, transparent-to-opaque center to shrink to a point and disappear.
The effect is ok, and if I run it with a narrow blur band and a fast duration, it looks decent, but it's not perfect.
Here is what it looks like with a blur range of 0.2, and a duration of (I think) 0.25 seconds:
If I introduce a pause between the animation steps, however, you can see the imperfections in the effect:
Question: Is there a way to animate a circle with a small blur radius (5 points or so) from the full size of a view down to 0 using Core Animation?
The same question applies to other types of wipe animations as well: Keyhole, star, side wipe, box, and many others. For those, the radial gradient trick I'm using for a feathered circle wipe wouldn't work at all. (You create those specialized wipe animations using a CAShapeLayer that's the shape you want to animate, and then adjust it's size from large to small.
It would be easy to create a static blurred circle (or other shape) by first drawing a circle and then applying a Core Image blur filter to it, but there's no way to animate that in iOS, and the CI blur filter is too slow on iOS even if you could.
Here is a link to the project I used to create the animations in this post using the 3-color radial gradient I describe: https://github.com/DuncanMC/GradientView.git.
Edit:
Based On James' comment, I tried using a shape layer with a shadow on it as a mask. The effect is on the right track, but there is still a stark transition from the opaque part of the image to the mostly transparent part which is masked by the shadow layer. Even with a shadow opacity of 1.0 the shadow is still mostly transparent. Here's what it looks like:
Making the line color of the circle shape partly transparent helps some, and gives a transition between the opaque part of the image and the mostly transparent shadow. Here's the image above but stroking the shape layer at a line width of 2 points and an opacity of 0.6:
Edit #2: SOLVED!
James' idea of using a shadowPath is the solution. You don't want anything in your mask layer other than a shadowPath. If you do that you can use the shadowRadius property to control the amount of blurring, and smoothly animate a shadow path from a circle that fully encloses the view down to a vanishing point all in one step.
I've updated my github project at https://github.com/DuncanMC/GradientView.git to include both the radial gradient animation and the shadowPath animation for comparison.
Here is what the finished effect looks like, first with a 5 pixel blur radius:
And then with a 30 pixel blur radius:
You could use the shadowPath on a CAShapeLayer to create a circle with a blurred outline to use as the mask.
Create a shape layer with no fill or stroke (we only want to see the shadow) and add it as the layer mask to your view.
let shapeLayer = CAShapeLayer()
shapeLayer.shadowColor = UIColor.black.cgColor
shapeLayer.shadowOffset = CGSize(width: 0, height: 0)
shapeLayer.shadowOpacity = 1
shapeLayer.shadowRadius = 5
self.maskLayer = shapeLayer
self.layer.mask = shapeLayer
And animate it with a CABasicAnimation.
func show(_ show: Bool) {
let center = CGPoint(x: self.bounds.width/2, y: self.bounds.height/2)
var newPath: CGPath
if show {
let radius = sqrt(self.bounds.height*self.bounds.height + self.bounds.width*self.bounds.width)/2 //assumes view is square
newPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true).cgPath
} else {
newPath = UIBezierPath(arcCenter: center, radius: 0.01, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true).cgPath
}
let shadowAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowAnimation.fromValue = self.maskLayer.shadowPath
shadowAnimation.toValue = newPath
shadowAnimation.duration = totalDuration
self.maskLayer.shadowPath = newPath
self.maskLayer.add(shadowAnimation, forKey: "shadowAnimation")
}
As you can set any path for the shadow it should also work for other shapes (star, box etc.).

draw rounded lines iOS Swift

Im trying to make an app which draws a fractal tree. I managed to make the code that generates all the start and end points from all the lines. I also managed to draw the lines but right now they are really boxy and want them to have rounded corners.
I using a UIView and using UIBezierPaths to draw the lines inside the view draw function. To retrieve the points I have an array of Branch objects inside a sigleton class. A branch object has among other things a startingPoint and a ending point which are both tuples( (x: Double, y: Double) ).
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.blue.setStroke()
for branch in Tree.shared.branches{
let path = UIBezierPath()
print(branch.startingPoint)
print(branch.endingPoint)
path.move(to: CGPoint(x: branch.startingPoint.x, y: branch.startingPoint.y))
path.addLine(to: CGPoint(x: branch.endingPoint.x, y: branch.endingPoint.y))
path.lineWidth = 3
path.stroke()
}
}
How could i make the corners rounded?
Also if someone knows a free library that could facilitate this Im also interested.
edit: Im not interested in how to generate the tree, I have done already done this part of the code I need help with drawing the lines.
You don't need a library, you just need to spend a little time learning how to draw curves with UIBezierPath, and curves are one of the things that that class is best at. A key to drawing curves is understanding how control points work. Here's an answer I wrote some time ago about how to smoothly connect curved lines, which I think will help. Play around with -addCurveToPoint:controlPoint1:controlPoint2:.
If you don't actually want curves, but really just want the corners to be rounded rather than pointy, then all you need to do is to set the lineJoinStyle to kCGLineJoinRound.
About rounded corners of some view, is just set the parameters below:
func adjustView(_ view: UIView){
view.layer.borderWidth = 2.0
view.layer.borderColor = UIColor.red
view.layer.cornerRadius = 10
view.clipsToBounds = true
view.backgroundColor = UIColor.white
}
If you wish more information check the current documentation about layer property:
https://developer.apple.com/documentation/uikit/uiview/1622436-layer

How to draw a face to SKShapeNode

I'm fairly new to Swift and I'm coding for my kids. I'm using sprite-kit to create a simple game for them. At the moment I'm struggling with how to draw a face to an arbitrary skshapenode? I know that there is no holes within the shape but other than that it can be of any shape. Dispite what shape it is, I would like to draw eyes and mouth to it programmatically. The shapes can came in many sizes.
Any suggestions how to approach this?
Here's an example of how to draw a smiley face with an SKShapeNode. It provides a framework that you can expand upon to draw more complex faces.
We start by extending the UIBezierPath class to add instance methods that are useful for drawing faces. The first method adds a circle to a path at a given location and with a specified radius. The moveToPoint statement moves the current point of a path to a new location. This is the equivalent of picking up a "pen" and moving it to a new location to draw a different, non-connected object. The second method draws a half circle with the open side facing up. This can be used to draw a smile. Note that extensions cannot be defined within a class. You can place it above your GameScene definition.
extension UIBezierPath {
func addCircle(center:CGPoint, radius:CGFloat) {
self.moveToPoint(CGPointMake(center.x+radius, center.y))
self.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(2*M_PI), clockwise: true)
}
func addHalfCircle(center:CGPoint, radius:CGFloat) {
self.moveToPoint(CGPointMake(center.x+radius, center.y))
self.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: false)
}
}
We can now use the extended UIBezierPath class to draw a face consisting of a face outline (a large circle), eyes (two small circles), and a mouth (a half circle). The face is constructed by adding components of the face to a path and then attaching the path to an SKShapeNode.
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
scaleMode = .ResizeFill
let face = SKShapeNode()
// The argument determines the size of the face
face.path = drawFace(50)
face.lineWidth = 2
// Place the face in the center of the scene
face.position = CGPointMake (CGRectGetMidX(view.frame),CGRectGetMidY(view.frame))
addChild(face)
}
func drawFace(radius:CGFloat) -> CGPathRef {
var path = UIBezierPath()
// Add the outline of the face
let center = CGPointZero
path.addCircle(center, radius: radius)
// Add the eyes
let eyeOffset = radius * 0.35
let eyeRadius = radius * 0.125
let left = CGPointMake(-eyeOffset, eyeOffset)
path.addCircle(left, radius: eyeRadius)
let right = CGPointMake(eyeOffset, eyeOffset)
path.addCircle(right, radius: eyeRadius)
// Add the mouth
let smileRadius = radius*0.65
path.addHalfCircle(center, radius: smileRadius)
return path.CGPath
}
}

SKAction Parabola Movement

Hi I'm working in Sprite Kit and I'm trying to make a sprite move from one point to another. Currently, I am able to do this with SkAction.moveTo and the sprite moves in a straight line. However, I'm curious as to if it would be possible for the sprite to move in a parabola instead of a straight line as it reaches its destination. All help is appreciated.
Here is a simple way to create a parabolic UIBezierPath:
let path = UIBezierPath()
path.moveToPoint(CGPointZero)
path.addQuadCurveToPoint(CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 50, y: 200))
This looks like this when graphed in a Playground:
For your sprite to move with this path, use this code:
mySprite.runAction(SKAction.followPath(path.CGPath, duration: 1.0))
To fine-tune the parabola, change the constants in the code above to change the path.
I've been drawn quite awhile in this problem. I would like to share a bit about my experience about parabola movement in SKAction.
This is the code for Swift 3
let path = UIBezierPath()
path.move(to: CGPoint(x:0, y:0))
path.addQuadCurve(to: CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 50, y: 200))
let parabolaAction = SKAction.follow(path.cgPath, duration: 1)
Your Sprite Node must have been placed in a certain position.
The second line means that the Sprite Node will move by x:0 and y:0. It's a relative movement. The problem why I'm struggling was considering that the Point is the new absolute position. But it's not.
The third line means that the Sprite Node will move by x:100, y:0 into a new position with the help of control point. Control point is the top coordinate like what erdekhayser shows.
If you set points in path with Sprite Node position + certain value then your Sprite Node might be positioned off the scene.
Hope this helps understanding UIBezierPath for parabola movement.

Resources