trouble flipping CoreGraphics upside down arc in a UIView - ios

I am having trouble flipping an arc using core graphics api in a UIView.
I understand that the coordinate system is different. UIkit helps me half way by transforming the coordinate system to accommodate the center I provide into a core graphic coordination system. I am assuming that when I draw an arc using core graphics api that its drawing the arc by using the core graphics coordinate system?
I can just change clockwise to false and it will give me what I need. Is this good practice?
I was playing around with CGAffineTransform’s rotation and scale, it didn’t give me what I need. Any way to flip the arc around without hawkishly changing clocking to false?
I also used UIBezierPath to draw another arc the way I want it. As I understand, UIBezierPath is a wrapper around CoreGraphics. I would like to know how I can do this using pure CoreGraphics API.
Extra question: My last function
Call path.stroke() paints the UIBezierPath path and the CGPath arcPath1. How come? 0.o
override func draw(_ rect: CGRect) {
guard let CGContext = UIGraphicsGetCurrentContext() else { return }
let boxPath = CGMutablePath()
boxPath.addLines(between:
[CGPoint(x: rect.width / 2, y: rect.height),
CGPoint(x: rect.width / 2, y: rect.height / 2),
CGPoint(x: rect.width, y: rect.height / 2)])
CGContext.beginPath() // begin new path
CGContext.addPath(boxPath.copy(strokingWithWidth: 5.0, lineCap: .square, lineJoin: .round, miterLimit: 0)) // add new path
CGContext.setFillColor(UIColor.blue.cgColor)
CGContext.fillPath()
let arcPath1 = CGMutablePath()
arcPath1.addArc(center: CGPoint(x: rect.width / 4, y: rect.height / 4 * 3),
radius: 20.0,
startAngle: CGFloat(180).degreesToRadians,
endAngle: CGFloat(360).degreesToRadians,
clockwise: true,
transform: CGAffineTransform(rotationAngle: CGFloat(0).degreesToRadians))
CGContext.beginPath()
CGContext.addPath(arcPath1)
CGContext.setStrokeColor(UIColor.blue.cgColor)
let path = UIBezierPath(arcCenter: CGPoint(x: rect.width / 4 * 3, y: rect.height / 4 * 3),
radius: 20.0,
startAngle: CGFloat(180).degreesToRadians,
endAngle: CGFloat(360).degreesToRadians,
clockwise: true)
path.lineWidth = 5.0
path.stroke()
}
Arcs, left: CG, right: Bezier

Related

UIBezierPath bezierPathWithRoundedRect: the cornerRadius value is not consistent

I want to draw a round rect in an CALayer using [UIBezierPath bezierPathWithRoundedRect: cornerRadius:], when setting up the rect's cornerRadius, I found that it is not consistent from 0 to maximum radius(about half of the layer's bound). The value jumped at about 1/3 of maximum radius which is quite confusing. After some research, I found probably it's a bug of iOS7 style of UIBezierPath drawing a round rect. PaintCode's research on that. So my question is how to draw an old style of perfect round rect with consistent cornerRadius value change?
Took a look at the PaintCode link you posted (although, it's much easier to answer questions here that explain the issue without having to go read an article somewhere else)...
Yes, it appears UIBezierPath(roundedRect: ...) is still buggy when the radius is greater than - as you noted - roughly 1/3 of 1/2 of the rect dimension.
To create a rounded rect path "manually," we can do this (using the rect dimensions and corner radius from that link):
let r: CGRect = CGRect(x: 0, y: 0, width: 150.0, height: 153.0)
let cornerRad: CGFloat = 50.0
// center points for the corner arcs
let ptCTR: CGPoint = CGPoint(x: r.maxX - cornerRad, y: r.minY + cornerRad)
let ptCBR: CGPoint = CGPoint(x: r.maxX - cornerRad, y: r.maxY - cornerRad)
let ptCBL: CGPoint = CGPoint(x: r.minX + cornerRad, y: r.maxY - cornerRad)
let ptCTL: CGPoint = CGPoint(x: r.minX + cornerRad, y: r.minY + cornerRad)
let bez: UIBezierPath = UIBezierPath()
// Top-Right corner
bez.addArc(withCenter: ptCTR, radius: cornerRad, startAngle: .pi * 1.5, endAngle: .pi * 0.0, clockwise: true)
// Bottom-Right corner
bez.addArc(withCenter: ptCBR, radius: cornerRad, startAngle: .pi * 0.0, endAngle: .pi * 0.5, clockwise: true)
// Bottom-Left corner
bez.addArc(withCenter: ptCBL, radius: cornerRad, startAngle: .pi * 0.5, endAngle: .pi * 1.0, clockwise: true)
// Top-Left corner
bez.addArc(withCenter: ptCTL, radius: cornerRad, startAngle: .pi * 1.0, endAngle: .pi * 1.5, clockwise: true)
// close the path
bez.close()

Inverted Rounded Rectangle Swift

How would I draw an inverted rounded rectangle as shown below in Swift using a UIBezierPath? To clarify, I want to ONLY draw the shape in black.
While most of us would simply put a white view with rounded corners on top of the black view (it is simpler and it more accurately reflects the visual result), if you really want to draw that shape, create a UIBezierPath that consists of the upper left arc, the upper right arc, and the add lines to the two bottom corners. E.g.
let path = UIBezierPath(
arcCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi / 2,
clockwise: false
)
path.addArc(
withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY),
radius: cornerRadius,
startAngle: .pi / 2,
endAngle: 0,
clockwise: false
)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.close()
There are a ton of ways to render this:
Create CAShapeLayer with this path and use that as a mask (which I show below);
Add this CAShapeLayer as a sublayer of the view’s layer;
Create a UIImage using UIGraphicsImageRenderer, and fill that UIBezierPath;
etc.
For example:
#IBDesignable class BottomView: UIView {
#IBInspectable var cornerRadius: CGFloat = 15 { didSet { setNeedsLayout() } }
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(
arcCenter: CGPoint(x: bounds.minX + cornerRadius, y: bounds.minY),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi / 2,
clockwise: false
)
path.addArc(
withCenter: CGPoint(x: bounds.maxX - cornerRadius, y: bounds.minY),
radius: cornerRadius,
startAngle: .pi / 2,
endAngle: 0,
clockwise: false
)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.white.cgColor // the color here is irrelevant; only used to define the mask
layer.mask = shapeLayer
}
}
That yields the following when I (a) add this BottomView to my view hierarchy; and (b) set the backgroundColor of this view to .black (or whatever you want):
I made this #IBDesignable so that I could either add it in Interface Builder storyboard, but you can also add it programmatically, too.
Or you can use this UIBezierPath with any of the patterns enumerated above. Whatever works for you.
What I do is draw two views: the black rectangle, and the top region view with the curves (which I call a "roundyThingy"). So the only interesting part is how to draw the top region view. It too is just a black rectangle, but it has a mask that cuts out the roundy part. So the only really interesting part is the mask:
let size = roundyThingy.bounds.size
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { context in
UIColor.black.setFill()
context.fill(.init(origin: .zero, size: size))
let path = UIBezierPath(
roundedRect: .init(origin: .zero, size: size),
byRoundingCorners: [.bottomLeft, .bottomRight],
cornerRadii: .init(width: 12, height: 12)
)
path.fill(with: .clear, alpha: 1)
}
roundyThingy.mask = UIImageView(image: image)
roundyThingy.mask?.frame = roundyThingy.bounds

Strange problem with UIBezierPath animation

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

Drawing UIView Quad Circle

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.

Swift: addLine ignored in between two addArc calls when creating a path

This code does what I expect. It draws an arc, then adds a line 50 points wide from the top of that arc:
path.move(to: .init(x: myX, y: myY))
path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius1, startAngle: .pi, endAngle: (3 * .pi)/2, clockwise: true)
let currentPoint = path.currentPoint
path.addLine(to: CGPoint(x: currentPoint.x + 50, y: currentPoint.y))
This code ignores the addLine for adding a 50 points wide line and just starts the second arc right at the top of the first arc.
path.move(to: .init(x: myX, y: myY))
path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius1, startAngle: .pi, endAngle: (3 * .pi)/2, clockwise: true)
let currentPoint = path.currentPoint
path.addLine(to: CGPoint(x: currentPoint.x + 50, y: currentPoint.y))
path.addArc(withCenter: CGPoint(x: centerX + 50, y: centerY), radius: radius1, startAngle: (3 * .pi)/2, endAngle: .pi, clockwise: false)
With this second bit of code, I get the exact same output if I comment out the addLine code. I get the exact same output if I change the addLine code to add 300 pixels points instead of 50. The addLine code is ignored and I get two arcs, without a line between where the first ends and the second begins.
Any suggestions? Thanks so much!
You said:
With this second bit of code, I get the exact same output if I comment out the addLine code.
Yep, when you add an arc to an existing path, it will automatically draw a line from the currentPoint to the start of this second arc. If you don’t want it to add the line in between, you need to do a move(to:) in your path to where the second arc will start if you don’t want the line in-between. Or create two paths, one for each arc, and stroke them separately.
I get the exact same output if I change the addLine code to add 300 pixels instead of 50.
That doesn’t quite make sense and I cannot reproduce that behavior. For example, this is what I get when I move the second arc 50pt (and I’ll animate the stroke so you can see what’s going on):
But this is what I get when I move the line 300pt (but keep the second arc only 50pt from the first):
Clearly, if you not only make the line 300pt long, but move the center of the second arc by 300pt as well, then it will be just like the first example (except further apart).
However, if I replace your addLine(to:) with move(to:), then you won’t get the line in-between them:
FWIW, in all of these examples, I didn’t know what you were using for myX and myY, so I used a point to the left of the first arc. Clearly, if you don’t want that extra line, move myX and myY to the start of the first arc (or just comment that out entirely).
i dont know whats wrong with your code but have what i have tried .. it might help you thats why posting it
#IBDesignable
class CustomBg:UIView {
private lazy var curvedLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.yellow.cgColor
shapeLayer.lineWidth = 4
shapeLayer.strokeColor = UIColor.green.cgColor
return shapeLayer
}()
//MARK:- inializer
override init(frame: CGRect) {
super.init(frame: frame)
layer.addSublayer(curvedLayer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
layer.addSublayer(curvedLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
func updatePath() {
let centerX = 100
let centerY = 100
let radius1 = CGFloat( 50.0)
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius1, startAngle:0, endAngle: (2 * .pi), clockwise: true)
let currentPoint = path.currentPoint
path.addLine(to: CGPoint(x: currentPoint.x + 150, y: currentPoint.y))
path.addArc(withCenter: CGPoint(x: Int(path.currentPoint.x + radius1) , y: centerY), radius: radius1 , startAngle: .pi, endAngle: (3 * .pi ), clockwise: true)
curvedLayer.path = path.cgPath
}
}

Resources