Semi circle between 2 CGPoints - ios

I want to draw a semi circle between 2 points on a circle. The main represents a clock and i want to draw another line to represent a progress from one hour to another so the points position may vary. First of all i know the X and Y of the 2 points i am interested in. This is how i try to add angles in UIBezierPath. My problem is that the new circle starts correctly but ends at a totally random location
let firstAngle = atan2(redPoint.y - circleCenter.y, redPoint.x - circleCenter.x)
let secondAngle = atan2(bluePoint.y - circleCenter.y, bluePoint.x - circleCenter.x) ```
let circlePath1 = UIBezierPath(arcCenter: circleCenter,
radius: circleRadius,
startAngle: firstAngle,
endAngle: secondAngle,
clockwise: true) ```
Wherever i set the redPoint, the circle starts at a correct location but the circle never ands at bluePoint.

I have tried your code and it works for me if points are actually on that circumference
let redPoint = CGPoint(x: 100.0, y: 200.0)
let bluePoint = CGPoint(x: 100.0, y: 0.0)
let circleCenter = CGPoint(x: 100.0, y: 100.0)
let circleRadius = CGFloat(100.0)
let firstAngle = atan2(redPoint.y - circleCenter.y, redPoint.x - circleCenter.x)
let secondAngle = atan2(bluePoint.y - circleCenter.y, bluePoint.x - circleCenter.x)
let path = UIBezierPath(arcCenter: circleCenter,
radius: circleRadius,
startAngle: firstAngle,
endAngle: secondAngle,
clockwise: true)
//Path in layer
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
self.view.layer.addSublayer(shapeLayer)
or for example:
let redPoint = CGPoint(x: 200.0, y: 100.0)
let bluePoint = CGPoint(x: 100.0, y: 0.0)
let circleCenter = CGPoint(x: 100.0, y: 100.0)
let circleRadius = CGFloat(100.0)
You get:
But if you use coordinates that are not actually on your circumference, you get wrong results. My suggestion is to check your inputs and eventually if points are belonging to your desired circumference or not.

You're doing this backwards. Don't try to get the angle by starting with the location of the little red and blue filled circles. Use the angle to place the little red and blue filled circles.
In that example, my angles are -1 and 2.8 (radians). First I draw the arc (just as you did); then I superimpose the circles, which is trivial because I know where their centers are (the endpoints of the arc) by converting polar to cartesian coordinates.

Related

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.

In SceneKit is there a way not to fill UIBezierPath?

I am trying to draw circle in a sceneView but it fills the inside.
Also is there a way to increase segment number it looks like a polygon.
In case you wonder why I use UIBezierPath to draw circle, I want to change its end angle dynamically according to some parameters.
let r:CGFloat = 5
let center = CGPoint(x: 0, y: 0)
let path = UIBezierPath(arcCenter:center,
radius: r,
startAngle: CGFloat(0),
endAngle:CGFloat(CGFloat.pi * 2),
clockwise: true)
path.lineWidth = 1
UIColor.clear.setFill()
path.fill()
UIColor.white.setStroke()
path.stroke()
let shape = SCNShape(path: path, extrusionDepth: 0)
shape.firstMaterial?.diffuse.contents = UIColor.white
let shapeNode = SCNNode(geometry: shape)
shapeNode.position = SCNVector3(0, 0, 0)
rootNode.addChildNode(shapeNode)

UIView pauses during animation along UIBezierPath

I'm trying to animate a UIView(a square) to move along a UIBezierPath using a CAKeyframeAnimation. The square pauses at two points along the bezier path, both points being right before the path begins to arc.This is my code for the UIBezierPath and Animation:
func createTrack() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: layerView.frame.size.width/2, y: 0.0))
path.addLine(to: CGPoint(x: layerView.frame.size.width - 100.0, y: 0.0))
path.addArc(withCenter: CGPoint(x: layerView.frame.size.width - 100.0,y: layerView.frame.height/2), radius: layerView.frame.size.height/2, startAngle: CGFloat(270).toRadians(), endAngle: CGFloat(90).toRadians(), clockwise: true)
path.addLine(to: CGPoint(x: 100.0, y: layerView.frame.size.height))
path.addArc(withCenter: CGPoint(x: 100.0,y: layerView.frame.height/2), radius: layerView.frame.size.height/2, startAngle: CGFloat(90).toRadians(), endAngle: CGFloat(270).toRadians(), clockwise: true)
path.close()
return path
}
#IBAction func onAnimatePath(_ sender: Any) {
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 20, height: 20)
square.backgroundColor = UIColor.red
layerView.addSubview(square)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = trackPath.cgPath
animation.rotationMode = kCAAnimationRotateAuto
animation.repeatCount = Float.infinity
animation.duration = 60
square.layer.add(animation, forKey: "animate position along path")
}
layerView is just a UIView. Any ideas on why this happens and how I can fix this?
The path you are using consists of 5 segments (two arcs and three lines (including the one when you close the path)) and the animation spends the same time on each of the segments. If this path if too narrow, these lines segments will have no length and the square will appear still for 12 seconds in each of them.
You probably want to use a "paced" calculation mode to achieve a constant velocity
animation.calculationMode = kCAAnimationPaced
This way, the red square will move at a constant pace – no matter how long each segment of the shape is.
This alone will give you the result you are after, but there's more you can do to simplify the path.
The addArc(...) method is rather smart and will add a line from the current point to the start of the arc itself, so the two explicit lines aren't needed.
Additionally, if you change the initial point you're moving the path to to have the same x component as the center of the second arc, then the path will close itself. Here, all you need are the two arcs.
That said, if the shape you're looking to create is a rounded rectangle like this, then you can use the UIBezierPath(roundedRect:cornerRadius:) convenience initializer:
let radius = min(layerView.bounds.width, layerView.bounds.height) / 2.0
let path = UIBezierPath(roundedRect: layerView.bounds, cornerRadius: radius)

circle from UIBezierPath drawn in wrong direction in SKShapeNode

Following UIBezierPath reference, I tried drawing a dashed path which should end up as dashed arc. However the drawing direction is wrong. clockwise was set to true but the top half of the circle was drawn as opposed to what was mentioned in the apple's page
let arcForCompleted =
UIBezierPath(arcCenter: origin, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
let pattern = getPattern(self.circumference, segments: involved)
let dashedPathForCompleted = CGPathCreateCopyByDashingPath(arcForCompleted.CGPath, nil, 0, pattern, pattern.count)
let dashedCircleForCompleted = SKShapeNode(path: dashedPathForCompleted!)
I am guessing this is because UIKit and SpriteKit has different coordinate system.
UIBezierPath is written with UIKit in mind thus it uses the UIKit coordinate system, (0,0) is in the upper left with positive y values running down. For the SKNode it has different coordinate system, (0,0) is in the center with positive y running up. You should keep that in mind when drawing arcs as it will affect the clockwise parameter. You can find a discussion of SKNode coordinate system here.
You can paste this code in a playground to see the difference as well
let bezierPath = UIBezierPath(arcCenter: CGPoint(x: 50.0,y: 50.0), radius: 50, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
class ArcView:UIView
{
override func drawRect(rect: CGRect) {
let arcForCompleted = bezierPath
let pattern:[CGFloat] = [10.0,10.0]
arcForCompleted.setLineDash(pattern, count: 2, phase: 0.0)
arcForCompleted.stroke()
}
}
let arcView = ArcView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
arcView.backgroundColor = UIColor.whiteColor()
let arcForCompleted = bezierPath
let shape = SKShapeNode()
shape.path = arcForCompleted.CGPath

Resources