I want to draw the Moon and then animate Moon's shadow. But after launching this code I can see some glitches on animation line:
GIF:
Why is this happening?
Playground code here
Update 1:
Both paths created by this function but with different angles (0 and π/2*0.6):
func calculateMoonPath(for angle: CGFloat) -> UIBezierPath {
let center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let radius = view.bounds.height/2
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: -.pi/2,
endAngle: .pi/2,
clockwise: true)
path.addArc(withCenter: .init(x: center.x - radius * tan(angle), y: center.y),
radius: radius / CGFloat(cosf(Float(angle))),
startAngle: .pi/2 - angle,
endAngle: angle - .pi/2,
clockwise: false
)
path.close()
return path
}
In my experience, the code that generates arcs creates different numbers of cubic bezier curves under the covers as the arc angle changes.
That changes the number of control points in the two curves, and messes up the animation. (as David Rönnqvist says, animations are undefined if the starting and ending path have a different number of control points.)
From what I've read, a full circle requires 4 cubic bezier curves to complete.
It wouldn't be that hard to create a variant of the addArc method that always built the arc using 4 cubic bezier curves, regardless of the arc angle. That's what I would suggest.
You could probably break your arc into 4 pieces (Using 4 sequential calls to addArc(withCenter:...) with different start and end angles such that they combine to make your desired full arc. Each of those should be short enough arc lengths to be a single Bezier curve, so you should get the same number of control points for the beginning and ending combined curve.
If you rewrite your calculateMoonPath function like this:
func calculateMoonPath(for angle: CGFloat) -> UIBezierPath {
let center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let radius = view.bounds.height/2
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: -.pi/2,
endAngle: .pi/2,
clockwise: true)
let startAngle = .pi/2 - angle
let endAngle = angle - .pi/2
let delta = (endAngle - startAngle) / 4
for index in 0...3 {
let thisStart = startAngle + delta * CGFloat(index)
let thisEnd = startAngle + delta * CGFloat(index + 1)
path.addArc(withCenter: .init(x: center.x - radius * tan(angle), y: center.y),
radius: radius / CGFloat(cosf(Float(angle))),
startAngle: thisStart,
endAngle: thisEnd,
clockwise: false
)
}
path.close()
return path
}
That yields the following:
The way you do each of the two lines,
is simply, two control points!
One at each end.
That's all there is to it. Don't try using an arc.
Here ...
https://www.desmos.com/calculator/cahqdxeshd
Related
I need to draw explanatory sign using UIBezierPath.
what i am trying to draw is
what i am getting using below code
private func linePath()-> CGPath {
let viewCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = bounds.midX/7.5
let miniRadius = bounds.midX/15
let path = UIBezierPath()
///path.move(to: viewCenter)
path.addArc(withCenter: viewCenter, radius: miniRadius, startAngle: 0, endAngle: 180.degreesToRadians, clockwise: true)
path.addQuadCurve(to: CGPoint(x: path.currentPoint.x - 5, y: path.currentPoint.y - 30), controlPoint: CGPoint(x: path.currentPoint.x - 10, y: path.currentPoint.y - 30))
// path.addLine(to: CGPoint(x: path.currentPoint.x - 5, y: path.currentPoint.y - 30))
path.addArc(withCenter: CGPoint(x: path.currentPoint.x + radius, y: path.currentPoint.y + radius/2), radius: radius, startAngle: 180, endAngle: 0.degreesToRadians, clockwise: true)
path.close()
return path.cgPath
}
Any help or pointers are appreciated.
When drawing smooth shapes with Bézier curves, we want to make sure that there are no points of discontinuity of the slope in the overall path where one curve abuts another. I.e., we want to ensure there aren’t any points where the slopes before and after a start/end point are not equal.
In short, make sure that the slope of the first control point for each Bézier curve matches the slope of the previous curve (and of course, that the second control point matches the slope of the following curve).
Consider, the following, stroking the three paths, one arc and two cubic Bézier (plus stroking the control points for the Bézier):
I have done this with three curves, and I'm showing the control points for the green and yellow curves with the dark lines. Going clockwise, note how the yellow curve’s first control point is collinear with the tangent of the blue arc. And the yellow’s second control point is collinear with the green curve’s first control point. And, of course, the green curve’s second control point is collinear with the starting tangent of the blue curve.
Getting rid of the distracting colors that yields:
FYI, this generated the above:
func updatePath() {
let verticalControlOffset = radius
let horizontalControllOffset = radius / 2
let height = radius * 5
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.minY + radius),
radius: radius,
startAngle: .pi,
endAngle: 2 * .pi,
clockwise: true)
var startPoint = path.currentPoint
var endPoint = CGPoint(x: bounds.midX, y: bounds.minY + height)
var cp1 = CGPoint(x: startPoint.x, y: startPoint.y + verticalControlOffset)
var cp2 = CGPoint(x: endPoint.x + horizontalControllOffset, y: endPoint.y)
path.addCurve(to: endPoint,
controlPoint1: cp1,
controlPoint2: cp2)
startPoint = path.currentPoint
endPoint = CGPoint(x: bounds.midX - radius, y: bounds.minY + radius)
cp1 = CGPoint(x: startPoint.x - horizontalControllOffset, y: startPoint.y)
cp2 = CGPoint(x: endPoint.x, y: endPoint.y + verticalControlOffset)
path.addCurve(to: endPoint,
controlPoint1: cp1,
controlPoint2: cp2)
shapeLayer.path = path.cgPath
}
Now, this is not the exact same as your target shape. But it illustrates the issue, namely when joining a series of Bézier curves together, make sure the control points for adjoining curves line with each other. This is easy when your control points are all vertical and horizontal, but if not, you will want to use a little algebra and/or trigonometry to make sure they line up properly. And sometimes, when rendering complicated shapes, it is helpful to actually stroke lines to the control points, so you can easily visualize what is going on.
How to make UIView like above.
Tried below but its creating a semi circle type view.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: topView.bounds.size.width, y: topView.bounds.size.height / 2), radius: topView.bounds.size.height, startAngle: .pi, endAngle: 0.0, clockwise: false)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
topView.layer.mask = circleShape
Draw it yourself. There is nothing complicated.
With sample:
Start at CenterPoint
Go to BottomPoint (line)
Arc from BottomPoint to LeftPoint, and the angle are Pi/2 to Pi (in clockwise)
Go to leftPoint
Go to CenterPoint (line)
The UIBezierPath:
let bezierPath = UIBezierPath()
let centerPoint = CGPoint(x: view.bounds.width, y: 0)
let bottomPoint = CGPoint(x: view.bounds.width, y: view.bounds.height)
let leftPoint = CGPoint(x: 0, y: 0)
bezierPath.move(to: bottomPoint) //not needed
bezierPath.addArc(withCenter: centerPoint,
radius: view.bounds.height,
startAngle: CGFloat.pi/2.0,
endAngle: CGFloat.pi,
clockwise: true)
bezierPath.addLine(to: leftPoint) //not needed
bezierPath.addLine(to: centerPoint)
There are two "not needed", because they are implicit, but you might want to write them if they are "too much hidden" for you.
Why your self? Because, a circle will only have two points and fill between it. In other words, it won't go to "centerPoint" to fill it.
Example with the same angle I used in my handmade path:
There are 2 * .pi radians in a circle, and you're going from pi to 0, which is a semicircle. Your start and end angles have to be 0.5 pi apart.
I am trying to create a timer. I have a circle using UIBezierPath which I will animate to show the time remaining. This is the code that draws the shape and adds it to the view:
func drawBgShape() {
bgShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: center.x , y: center.y), radius:
bounds.width/2, startAngle: -.pi/2, endAngle: 1.5*.pi, clockwise: true).cgPath
bgShapeLayer.strokeColor = UIColor.red.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 10
layer.addSublayer(bgShapeLayer)
}
However, when the code is run, it looks like this;
I have tried a number of ways to centre the progress bar but none of them seem to work. For example, using frame.height/2 doesn't have any effect.
How can I centre the progress bar?
Thanks.
EDIT:
bgShapeLayer is defined like this:
let bgShapeLayer = CAShapeLayer()
The issue is probably this phrase:
arcCenter: CGPoint(x: center.x , y: center.y)
A view center is where the view is located in its superview. That’s not what you want. You want the center of the view. Try this:
arcCenter: CGPoint(x: bounds.midX , y: bounds.midY)
However, it would be better if you gave your shape layer a frame, size, and position and did everything in terms of the shape layer (which is, after all, where the shape is being drawn). For example:
bgShapeLayer.frame = self.layer.bounds
self.layer.addSublayer(bgShapeLayer)
bgShapeLayer.path = UIBezierPath(
arcCenter: CGPoint(x: bgShapeLayer.bounds.midX, y: bgShapeLayer.bounds.midY),
radius: bgShapeLayer.bounds.width/2,
startAngle: -.pi/2, endAngle: 1.5*.pi, clockwise: true).cgPath
That way we do not confuse the view coordinates and the layer coordinates, as your code does. To some extent you can get away with this because it happens that in this situation there is a general equivalence of the view internal coordinates, its layer internal coordinates, and the bgShapeLayer internal coordinates, but such confusion is not a good habit to get into. You should say what you mean rather than relying on a contingency.
I want to draw a line on the circle(intercepting the arc of the circle) perpendicularly like in the picture.
I am using this code to draw circle
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: Conversion.degreesToRadians(value: CGFloat(0)), endAngle: Conversion.degreesToRadians(value: CGFloat(360)), clockwise: true)
path.lineWidth = 2
path.lineCapStyle = CGLineCap.square
UIColor.white.setStroke()
path.stroke()
The basic idea is that a circle has a certain radius about a center CGPoint. To figure out a point on the circle, you can calculate the x and y coordinates like so:
func point(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
where the angle is measured in radians, starting at 3 o'clock and going clockwise.
So those perpendicular intersecting strokes are merely line segments between two CGPoint at a given angle, where the "radius" used for the start of the line segment might be, for example, something just less than the radius of the circle. For the ending point of the line, use the same angle, but then use a radius value just greater than the radius of the circle.
I have the following code added in the drawRect function of a UIView creating the following attached green arc.
Is there a way I can make the edges at the ends of the curve to look like the red arc rounded edges in the smaller image attached.
[![//Draw the Interior
let center = CGPoint(x: bounds.width/2,
y: bounds.height/2)
//Calculate the radius based on the max dimension of the view.
let radius = max(bounds.width, bounds.height)
//Thickness of the Arc
let arcWidth: CGFloat = 76
//Start and End of Circle angle
let startAngle: CGFloat = 3 * π/4
let endAngle: CGFloat = π/4
let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
path.lineWidth = arcWidth
counterColor.setStroke()
path.stroke()]
Set the lineCapStyle property of the bezier path to
.Round:
A line with a rounded end. Quartz draws the line to extend beyond the
endpoint of the path. The line ends with a semicircular arc with a
radius of 1/2 the line’s width, centered on the endpoint.
path.lineCapStyle = .Round