Hide part of UIBezierPath - ios

I have 3 UIBezierPath with 2 circle and a line running from 1 circle's center to the other and it looks like the bottom picture. I want to hide the part of the line inside the circle like the top picture. Is there any easy way to do this?
My strategy would be to draw a invisible line from the centers and then draw a black line from the circumference of the 2 circles since I know the slopes etc but it seems like too much work.
private func pathForBoxCircle1() -> UIBezierPath {
let circlePath = UIBezierPath(arcCenter:circle1BoxCurrentCenter, radius: 25, startAngle: 0.0, endAngle: CGFloat(2*M_PI), clockwise: false)
//circlePath.fill()
pathBoxCircle1Global = circlePath
return circlePath
}
private func pathForBoxCircle2() -> UIBezierPath {
let circlePath = UIBezierPath(arcCenter:circle2BoxCurrentCenter, radius: 25, startAngle: 0.0, endAngle: CGFloat(2*M_PI), clockwise: false)
//circlePath.fill()
pathBoxCircle2Global = circlePath
return circlePath
}
private func pathForHorizonLine() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: circle1BoxCurrentCenter)
path.addLine(to: circle2BoxCurrentCenter)
path.lineWidth = 5.0
//pathHorizonLineGlobal = path
return path
}
override func draw(_ rect: CGRect) {
pathForBoxCircle1().stroke()
pathForBoxCircle2().stroke() // same as stroke()
pathForHorizonLine().stroke()
}

You can't mix transparent and opaque lines in the same shape. You are going to have to draw 2 circles and then the line segment from the outside of the first circle to the outside of the 2nd circle.
To do that you'll need trig, or perhaps Pythagoras, to calculate the coordinates of the points where your connecting lines intersect your 2 circles.
If C1 is your first circle, C2 is your 2nd circle, C1 is at (C1.x, C1.y), C2 is at (C2.x, C2.y), the radius of C1 is R1, and the radius of C2 is R2, then the pseudo-code would look something like this:
angle1 = atan2(C1.y - C2y, C1.x - C2.x)
angle2 = atan2(C2.y - C1.y, C2.x - C1.x)
xOffset1 = R1 * cos(angle1)
yOffset1 = R1 * sin(angle1)
point1 = (C1.x + xOffset1, C1.y + yOffset1)
xOffset2 = R2 * cos(angle2)
yOffset2 = R2 * sin(angle2)
point2 = (C2.x + xOffset2, C2.y + yOffset2)
Draw your circles, then draw lines between point1 and point2.
(Note that my trig is a little rusty, and that I sketched this out on a piece of scratch paper. I think it's correct, but it's completely untested.)

Related

Creating path using CGMutablePath creates line to wrong CGPoint

I was planning to display information of AR object in screen with arrow in 2D. So I used projectPoint to get corresponding position of object in screen. I have this function to return convert 3D position of node to 2D and CGPoint to display info text in.
func getPoint(sceneView: ARSCNView) -> (CGPoint, CGPoint){
let projectedPoint = sceneView.projectPoint(node.worldPosition)
return (point, CGPoint(x: CGFloat(projectedPoint.x), y: CGFloat(projectedPoint.y)) )
}
and this to draw line using SpriteKit:
let (f,s) = parts[3].getPoint(sceneView: sceneView)
line.removeFromParent()
let path = CGMutablePath()
path.move(to: f)
path.addLine(to: s)
line = SKShapeNode(path: path)
spriteScene.addChild(line)
This is what i get
What I expect is another end of line to be fixed in node (blue mesh). Is there something I am missing? Or does projectPoint works some other way?
edit: It seems projectPoint is returning correct value but while creating path path.addLine(to: s) this point is shifting to different position.
path.addLine(to: s) s here had reversed y so this did the trick
let frame = self.sceneView.frame
let sInversed = CGPoint(x: from.x, y: frame.height - s.y)
path.addLine(to: sInversed)
Here origin of SKScene was in bottom left of screen instead of top left.

SKSpriteNode will not center

I have this inside my GameScene which is called in the didMove()
for i in 1...5 {
// path to create the circle
let path = UIBezierPath(arcCenter: CGPoint(x: center.x, y: center.y), radius: CGFloat(((43 * i) + 140)), startAngle: CGFloat(GLKMathDegreesToRadians(-50)), endAngle: CGFloat(M_PI * 2), clockwise: false)
// the inside edge of the circle used for creating its physics body
let innerPath = UIBezierPath(arcCenter: CGPoint(x: center.x, y: center.y), radius: CGFloat(((43 * i) + 130)), startAngle: CGFloat(GLKMathDegreesToRadians(-50)), endAngle: CGFloat(M_PI * 2), clockwise: false)
// create a shape from the path and customize it
let shape = SKShapeNode(path: path.cgPath)
shape.lineWidth = 20
shape.strokeColor = UIColor(red:0.98, green:0.99, blue:0.99, alpha:1.00)
// create a texture and apply it to the sprite
let trackViewTexture = self.view!.texture(from: shape)
let trackViewSprite = SKSpriteNode(texture: trackViewTexture)
trackViewSprite.physicsBody = SKPhysicsBody(edgeChainFrom: innerPath.cgPath)
self.addChild(trackViewSprite)
}
It uses UIBezierPaths to make a few circles. It converts the path into a SKShapeNode then a SKTexture and then applies it to the final SKSpriteNode.
When I do this, the SKSpriteNode is not where it should be, it is a few to the right:
But when I add the SKShapeNode I created, it is set perfectly fine to where it should be:
Even doing this does not center it!
trackViewSprite.position = CGPoint(x: 0, y: 0)
No matter what I try it just will not center.
Why is this happening? Some sort of bug when converting to a texture?
P.S - This has something to do with this also Keep relative positions of SKSpriteNode from SKShapeNode from CGPath
But there is also no response :(
Edit, When I run this:
let testSprite = SKSpriteNode(color: UIColor.yellow, size: trackViewSprite.size)
self.addChild(testSprite)
It shows it has the same frame also:
After a long discussion, we determined that the problem is due to the frame size not being the expected size of the shape.
To combat this, the OP created an outer path of his original path, and calculated the frame that would surround this. Now this approach may not work for everybody.
If anybody else comes across this issue, they will need to do these things:
1) Check the frame of the SKShapeNode to make sure that it is correct
2) Determine what method is best to calculate the correct desired frame
3) Use this new frame when getting textureFromNode to extract only the desired texture size

Undestanding UIBezierPath curving mechanism, controlPoint and the curve point

I'm trying to draw a simple Parabola shape using UIBezierPath. I have a maxPoint and a boundingRect of which I'm basing the width and stretch of the parabola.
Here's the function I made to draw the parabola (I draw the parabola in a container view, rect will be container.bounds):
func addParabolaWithMax(maxPoint: CGPoint, inRect boundingRect: CGRect) {
let path = UIBezierPath()
let p1 = CGPointMake(1, CGRectGetMaxY(boundingRect)-1)
let p3 = CGPointMake(CGRectGetMaxX(boundingRect)-1, CGRectGetMaxY(boundingRect)-1)
path.moveToPoint(p1)
path.addQuadCurveToPoint(p3, controlPoint: maxPoint)
// Drawing code
...
}
My problem is, that I want the maxPoint that I send in the function to be the actual extreme point in the parabola itself. So for example, if I send in (CGRectGetMidX(container.bounds), 0), The maximum point should be at the top-most center. But in using this function with this particular point, this is what the result looks like:
So what exactly the path does here? Or in other words, how can I get from the controlPoint to the actual max point that I need? I've tried adding and subtracting different values from the y value, based on the height of the boundingRect, but I couldn't quite find the right combination, as in different points with different y values it behaves differently. There seem to be some kind of multiplier being added in, how can I solve it?
For may applications adam.wulf's solution is fine, but it doesn't actually create a parabola. To create a parabola, we need to compute the control point given the midpoint of the quadratic curve. Bézier paths are just math; we can compute this quite easily. We just need to invert the Bézier function and solve it for t=0.5.
The Bézier solution at 0.5 (the midpoint) is derived nicely at Draw a quadratic Bézier curve through three given points.
2*Pc - P0/2 - P2/2
Where Pc is the point we want to go through and P0 and P2 are the end points.
(Computing the Bézier at other points is not very intuitive. The value at t=0.25 is not "a quarter of the way along the path." But luckily for our purposes, t=0.5 matches quite nicely to our intuition of "the midpoint" on a quadratic.)
Given our solution, we can write our code. Forgive the translation to Swift 3; my copy of Xcode 7.3 isn't very happy with iOS playgrounds, but it should be easy to convert to 2.2.
func addParabolaWithMax(maxPoint: CGPoint, inRect boundingRect: CGRect) -> UIBezierPath {
func halfPoint1D(p0: CGFloat, p2: CGFloat, control: CGFloat) -> CGFloat {
return 2 * control - p0 / 2 - p2 / 2
}
let path = UIBezierPath()
let p0 = CGPoint(x: 0, y: boundingRect.maxY)
let p2 = CGPoint(x: boundingRect.maxX, y: boundingRect.maxY)
let p1 = CGPoint(x: halfPoint1D(p0: p0.x, p2: p2.x, control: maxPoint.x),
y: halfPoint1D(p0: p0.y, p2: p2.y, control: maxPoint.y))
path.move(to: p0)
path.addQuadCurve(to: p2, controlPoint: p1)
return path
}
The halfPoint1D function is the the one-dimensional implementation of our solution. For our two-dimentional CGPoint, we just have to call it twice.
If I could recommend just one resource for understanding Bézier curves, it would probably be the "Constructing Bézier curves" section from Wikipedia. Studying the little animations that show how the curves come about I find very enlightening. The "Specific Cases" section is useful as well. For a deep exploration of the topic (and one that I recommend all developers have a passing familiarity with), I like A Primer on Bézier Curves. It's ok to skim it and just read the parts that interest you at the moment. But a basic understanding of this group of functions will go a long way to removing the magic from drawing in Core Graphics and make UIBezierPath a tool rather than a black box.
let path = UIBezierPath()
let p1 = CGPointMake(0,self.view.frame.height/2)
let p3 = CGPointMake(self.view.frame.width,self.view.frame.height/2)
path.moveToPoint(p1)
path.addQuadCurveToPoint(p3, controlPoint: CGPoint(x: self.view.frame.width/2, y: -self.view.frame.height/2))
let line = CAShapeLayer()
line.path = path.CGPath;
line.strokeColor = UIColor.blackColor().CGColor
line.fillColor = UIColor.redColor().CGColor
view.layer.addSublayer(line)
this is the reason: https://cdn.tutsplus.com/mobile/authors/legacy/Akiel%20Khan/2012/10/15/bezier.png you should have to consider the tangent concept
The trick is to split the curve into two pieces so that you can control which points the curve passes through. As mentioned in Eduardo's answer, control points handle tangent, and end points are on the curve. This lets you have a curve from the bottom left to top center, then from top center to bottom right:
let p1 = CGPointMake(0,self.view.frame.height/2)
let p3 = CGPointMake(self.view.frame.width,self.view.frame.height/2)
let ctrlRight = CGPointMake(self.view.frame.width,0)
let ctrlLeft = CGPointZero
let bezierPath = UIBezierPath()
bezierPath.moveToPoint(p1)
bezierPath.addCurveToPoint(maxPoint, controlPoint1: p1, controlPoint2: ctrlLeft)
bezierPath.addCurveToPoint(p3, controlPoint1: ctrlRight, controlPoint2: p3)
UIColor.blackColor().setStroke()
bezierPath.lineWidth = 1
bezierPath.stroke()
I needed to do something similar where I wanted to have a UIBezierPath that exactly matched a specific parabola definition. So I made this little class that creates a parabola based on the focus and directrix or the a, b, c of the general equation. I threw in a convenience init which can use your boundingRect and maxPoint concepts. Either adapt those or the init where the upper corners of the box are its 1 and 2 and the middle of the bottom edge is the vertex.
Use the xform to scale and translate as needed. You can create/draw the path based on any two points on the parabola. They don't have to have the same y-value. The resulting shape will still exactly match the specified parabola.
This is not completely general in terms of rotation but it's a start.
class Parabola
{
var focus: CGPoint
var directrix: CGFloat
var a, b, c: CGFloat
init(_ f: CGPoint, _ y: CGFloat)
{
focus = f
directrix = y
let dy = f.y - y
a = 1 / (2*dy)
b = -f.x / dy
c = (f.x*f.x + f.y*f.y - y*y) / (2*dy)
}
init(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat)
{
self.a = a
self.b = b
self.c = c
focus = CGPoint(x: -b / (2*a), y: (4*a*c - b*b + 1) / (4*a))
directrix = (4*a*c - b*b - 1) / (4*a)
}
convenience init(_ v: CGPoint,
_ pt1: CGPoint,
_ pt2: CGPoint)
{
let a = (pt2.y - v.y) / (pt2.x - v.x) / (pt2.x - v.x)
self.init(CGPoint(x: v.x, y: v.y + 1/(4*a)),
v.y - 1/(4*a))
}
func f(of x: CGFloat) -> CGFloat
{
a*x*x + b*x + c
}
func path(_ x1: CGFloat, _ x2: CGFloat,
_ xform: CGAffineTransform? = .identity) -> UIBezierPath
{
let pt1 = CGPoint(x1, f(of: x1))
let pt2 = CGPoint(x2, f(of: x2))
let x = (x1 + x2) / 2
let y = (2*a * x1 + b) * (x - x1) + pt1.y
let path = UIBezierPath()
path.move(to: pt1)
path.addQuadCurve(to: pt2, controlPoint: CGPoint(x: x, y: y))
path.apply(xform!)
return path
}
}

How to create graph pies with different sizes using bézier paths in swift?

I want to make a nice graphic pie with 8 equal slices, that can be individually scaled or resized depending on an Int or something like this. This would look something like below just that all the slices should be equally cut:
I have tried this in Objective-C but it makes just one slice:
-(CAShapeLayer *)createPieSlice {
CAShapeLayer *slice = [CAShapeLayer layer];
slice.fillColor = [UIColor redColor].CGColor;
slice.strokeColor = [UIColor blackColor].CGColor;
slice.lineWidth = 3.0;
CGFloat angle = DEG2RAD(-60.0);
CGPoint center = CGPointMake(100.0, 100.0);
CGFloat radius = 100.0;
UIBezierPath *piePath = [UIBezierPath bezierPath];
[piePath moveToPoint:center];
[piePath addLineToPoint:CGPointMake(center.x + radius * cosf(angle), center.y + radius * sinf(angle))];
[piePath addArcWithCenter:center radius:radius startAngle:angle endAngle:DEG2RAD(60.0) clockwise:YES];
// [piePath addLineToPoint:center];
[piePath closePath]; // this will automatically add a straight line to the center
slice.path = piePath.CGPath;
return slice;
}
How can I achieve that graph in swift?
Break the problem into logical pieces.
You have wedges of different arc widths. All those radii need to add up to a full circle. I assume they represent fractions of something that adds up to 100%. Do you want a specific order? If so, map your fractions in the order you want, such that they all add up to 100%.
Then write code that starts at an angle of zero, and creates arcs that are the specified fraction of 2π. Each one would start at the end of the previous one. Assign a radius that's appropriate based on the data you need.
Now write code that creates closed path segments in a UIBezierPath.
EDIT
You've clarified, and told us that you always want 8 slices of the same width but with different radii.
So you need to write code that takes 8 input values and plots it as 8 arcs with different radius values.
Let's say your input value is an array of floats ranging from 0 to 1. At zero, the wedge is zero-sized. At 1.0, it's the largest circle size that will fit in your view (half the width of a square view.
So you would create an array of 8 floats:
var fractions = [0.5, 0.7, 0.3, 0.1, 1.0 .6, .2, .9]
The code to create a bezier curve with 8 arcs might look something like this:
let pi = 3.1415826
let largestRadius = myView.width/2
let piePath = UIBezierPath()
for (index, afloat) in fractions
{
let startAngle = Double(index) / fractions.count * 2 * pi
let endAngle = Double(index+1) / fractions.count * 2 * pi
let thisRadius = largestRadius * afloat
let center = CGPointMake( myView.width/2, myView.height/2)
piePath.moveToPoint(center)
piePath.addArcWithCenter(center,
radius: thisRadius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
piePath.lineToPoint(center)
piePath.closePath()
}
I think the code above would create 8 closed pie-slice paths, but I'm not positive. It might be necessary to add a lineToPoint call between the first moveToPoint call and the arc call.
Edit #2:
Since I am learning Swift, I decided to take this as an exercise and wrote a sample project that generates pie charts using a shape layer and a a custom path created from a UIBezierPath, as outlined above. You can find the sample project on github: PieCharts project on Github
I have managed to solve my problem using Core Graphics! Thanks #duncan-c for your interest.
EDIT:
I have dropped my first solution in the favour of #duncan-c's solution, that works better for my needs!
import UIKit
class Pie: UIView {
// In range of 0.0 to 1.0
var endArc:CGFloat = 0.0 {
didSet {
setNeedsDisplay()
}
}
var arcWidth:CGFloat = 5.0
var arcColor = UIColor()
var arcBackgroundColor = UIColor.clearColor()
var arcStrokeColor = UIColor()
var startFloat:CGFloat = 0.0
var radius:CGFloat = 0.0
var radiusSize: CGFloat = 0.0
override func drawRect(rect: CGRect) {
// Important constants for circle
let fullCircle = 2.0 * CGFloat(M_PI)
let start:CGFloat = startFloat * fullCircle
let end:CGFloat = endArc * fullCircle + start
// Find the centerpoint of the rect
var centerPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect))
// Set the radius
radius = (radiusSize - arcWidth) / 2.0
// Starting point for all drawing code is getting the context.
let context = UIGraphicsGetCurrentContext()
// Set colorspace
let colorspace = CGColorSpaceCreateDeviceRGB()
// Set line attributes
CGContextSetLineWidth(context, arcWidth)
// Draw the pie
CGContextSetStrokeColorWithColor(context, arcStrokeColor.CGColor)
CGContextSetFillColorWithColor(context, arcColor.CGColor)
CGContextMoveToPoint(context, centerPoint.x, centerPoint.y)
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, start, end, 0)
CGContextFillPath(context)
}
}
And then subclassd the UIView where I needed using:
#IBOutlet weak var graph: Pie!
override func awakeFromNib() {
super.awakeFromNib()
let backgroundTrackColor = UIColor.clearColor()
let strokeColor = UIColor(white: 0.15, alpha: 1.0)
graph.radiusSize = 50.0
graph.arcBackgroundColor = backgroundTrackColor
graph.arcStrokeColor = strokeColor
graph.arcColor = UIColor.redColor()
graph.startFloat = 0.750
graph.arcWidth = 5.0
graph.endArc = 0.125
}

Drawing Round corners For Custom Shape by Core Graphics

i am drawing Custom shape using Core Graphics and i want to make Rounded Corners for this shape
this is my code of Drawing my custom Shape
CGPoint p1=[self getPointFromAngleQuarter:start_angle2 andRaduis:card.small_Raduis andCenter:center];
CGContextMoveToPoint(context, p1.x, p1.y);
CGPoint p2=[self getPointFromAngleQuarter:start_angle2 andCenter:center andRaduis:self.large_Raduis];
CGContextAddLineToPoint(context, p2.x, p2.y);
CGContextAddArc(context,center.x, center.y, selectedLargeRaduis, start, end,0);
CGPoint p5=[self getPointFromAngle:end_Angle andCenter:center andRaduis:self.small_Raduis];
CGContextAddLineToPoint(context, p5.x, p5.y);
CGContextAddArc(context,center.x, center.y,selectedSmallRaduis, end, start,1);
CGContextDrawPath(context, kCGPathFill);
and here is the final Result of my custom Shape
Custom Shape:
If this shape is a solid color, the easy solution is to use a very wide line width, plus a round line cap and round line join. I presume, though, that you want this rounded shape to lay entirely inside the shape you included in your picture. Then the trick is to offset the arcs you draw by an amount equal to corner radius of the path (and stroke the line with twice the width of the corner radius).
For example, considering this diagram (which is not the desired shape, but shows us how to get there):
The black shape in the background is your original shape. The white path is the path I'm going to draw to achieve the rounded corners. The light gray is that path stroked with a large line width, a rounded line join, and a rounded line cap. The dark gray is that path filled in with another color.
So hopefully this illustrates the idea. Create a new path, offset by the corner radius, and drawn with a line width twice the corner radius. If you simply draw the new path with a solid back stroke (replacing the light gray in the above image) and solid black fill (replacing the dark gray in the above image), you get your desired shape:
Here is routine to get the path (the white line in my first image) in Objective-C:
- (UIBezierPath *)arcWithRoundedCornerAt:(CGPoint)center
startAngle:(CGFloat)startAngle
endAngle:(CGFloat)endAngle
innerRadius:(CGFloat)innerRadius
outerRadius:(CGFloat)outerRadius
cornerRadius:(CGFloat)cornerRadius {
CGFloat innerTheta = asin(cornerRadius / 2.0 / (innerRadius + cornerRadius)) * 2.0;
CGFloat outerTheta = asin(cornerRadius / 2.0 / (outerRadius - cornerRadius)) * 2.0;
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:center
radius:innerRadius + cornerRadius
startAngle:endAngle - innerTheta
endAngle:startAngle + innerTheta
clockwise:false];
[path addArcWithCenter:center
radius:outerRadius - cornerRadius
startAngle:startAngle + outerTheta
endAngle:endAngle - outerTheta
clockwise:true];
[path closePath];
return path;
}
Or in Swift 3:
private func arcWithRoundedCorners(at center: CGPoint, startAngle: CGFloat, endAngle: CGFloat, innerRadius: CGFloat, outerRadius: CGFloat, cornerRadius: CGFloat) -> UIBezierPath {
let innerTheta = asin(cornerRadius / 2 / (innerRadius + cornerRadius)) * 2
let outerTheta = asin(cornerRadius / 2 / (outerRadius - cornerRadius)) * 2
let path = UIBezierPath()
path.addArc(withCenter: center, radius: innerRadius + cornerRadius, startAngle: endAngle - innerTheta, endAngle: startAngle + innerTheta, clockwise: false)
path.addArc(withCenter: center, radius: outerRadius - cornerRadius, startAngle: startAngle + outerTheta, endAngle: endAngle - outerTheta, clockwise: true)
path.close()
return path
}
(You can do the above with Core Graphics calls if you want, but I generally use UIBezierPath.)
If, though, you needed the fill to be a different color than the stroke, then the process is more complicated, because you can't just use this technique. Instead, you actually have to define a path that is an outline of the above shape, but consists of drawing not only the two big arcs, but four little arcs for each of the corners. It's tedious, but simple, trigonometry to construct that path, but I wouldn't go through that effort unless you had to.

Resources