Divide a line in n equal parts - swift

I am drawing a line from point A(x1,y1) to point B(x2,y2). Now I need to divide this line in n equal parts. Line is not straight so I am not able to calculate the points on the basis of x axis and width.
I am drawing the line as following:
let lineTop = DrawFiguresViewModel.getLine(fromPoint: CGPoint(x: point.x, y: point.y), toPoint: CGPoint(x: (point.x-100), y: (point.y-100)))
self.view.layer.addSublayer(lineTop)
class DrawFiguresViewModel{
class func getLine(fromPoint start: CGPoint, toPoint end:CGPoint) -> CALayer{
let line = CAShapeLayer()
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
line.path = linePath.cgPath
line.strokeColor = Colors.lineColor.cgColor
line.lineWidth = 2
line.lineJoin = kCALineJoinRound
return line
}
}
Any head start in this direction will be great.
Edit1:
I want to draw diagram like .
I am able to draw the bold lines but now I need to draw fine line with text cause and why. There can be multiple causes at equal distance on the vertical(slanted) line.
Edit2:
After adding code from Martin, I get it as
Although it is good but it is slightly offset. Also as it is n+1 I am removing 0 index value before drawing it.
Edit3:
Following is the code for drawing the lines using Martin's function:
if(node.childs.count > 0){
var arrPoints = divideSegment(start: CGPoint(x: point.x, y: point.y), end: CGPoint(x: (point.x-100), y: (point.y-100)), parts: node.childs.count)
arrPoints.remove(at: 0)
print(arrPoints)
for (index,obj) in node.childs.enumerated(){
if let nodeN = obj as? DataNode{
let pointN = arrPoints[index]
drawLevel1Line(point: pointN, nodeTitle: nodeN.title)
}
}
}

You start with the initial point, and then repeatedly increment both x- and y-coordinate by a fixed amount which is computed such that after
n steps the endpoint of the segment is reached (in other words: linear interpolation):
/// Returns an array of (`n` + 1) equidistant points from `start` to `end`.
func divideSegment(from start: CGPoint, to end: CGPoint, parts n: Int) -> [CGPoint] {
let ΔX = (end.x - start.x) / CGFloat(n)
let ΔY = (end.y - start.y) / CGFloat(n)
return (0...n).map {
CGPoint(x: start.x + ΔX * CGFloat($0),
y: start.y + ΔY * CGFloat($0))
}
}
Example:
print(divideSegment(from: CGPoint(x: 1, y: 1), to: CGPoint(x: 4, y: 5), parts: 4))
// [(1.0, 1.0), (1.75, 2.0), (2.5, 3.0), (3.25, 4.0), (4.0, 5.0)]

Related

How to draw a UIBezierPath with an ARC to the right similar to the image shown?

How do I draw this UIBezierPath to make it look identical to the green strip on the left of the image below? The rounded arc to the right.
path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 20, y: 0))
path.addLine(to: CGPoint(x: 20, y: 80))
//Add Half Circle Arc To Right
path.addArc(withCenter: CGPoint(x: 0, y: 160), radius: bounds.width, startAngle: 0, endAngle: 90, clockwise: true)
path.addLine(to: CGPoint(x: 20, y: 215))
path.addLine(to: CGPoint(x: 20, y: bounds.maxY))
path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
path.close()
What you need is a routine that given the height of the desired arc and how far this “bubble” should stick out, and will determine the desired arc angle, radius, and center offset.
To determine the center of the circle giving three points on that circle is to identify the chord between two points and then identify the line that bisects that line segment. That results in a line that goes through the center of the circle. Then repeat that for a different two points on the circle. The intersection of those two lines will be the center of the circle.
So, I used a little basic algebra to calculate the slope (m), the y-intercept (b), and the x-intercept (xIntercept) of the line that bisects the line segment between the start of the arc and its half point. We can take that line, and see where it intercepts the x-axis and determine the center of the circle.
From that, a little trigonometry gives us the angle and the radius of the arc that intersects these three points (the top of the arc, the middle of the arc, and the bottom of the arc).
You get something like:
/// Calculate parameters necessary for arc.
/// - Parameter height: The height of top half of the arc.
/// - Parameter distance: How far out the arc should project.
func angleRadiusAndOffset(height: CGFloat, distance: CGFloat) -> (CGFloat, CGFloat, CGFloat) {
let m = distance / height
let b = height / 2 - distance * distance / (2 * height)
let xIntercept = -b / m
let angle = atan2(height, -xIntercept)
let radius = height / sin(angle)
return (angle, radius, xIntercept)
}
And you can then use that to create your path:
var point: CGPoint = CGPoint(x: bounds.minX, y: bounds.minY)
let path = UIBezierPath()
path.move(to: point)
point.x += edgeWidth
path.addLine(to: point)
point.y += bubbleStartY
path.addLine(to: point)
let (angle, radius, offset) = angleRadiusAndOffset(height: bubbleHeight / 2, distance: bubbleWidth)
let center = CGPoint(x: point.x + offset, y:point.y + bubbleHeight / 2)
path.addArc(withCenter: center, radius: radius, startAngle: -angle, endAngle: angle, clockwise: true)
point.y = bounds.maxY
path.addLine(to: point)
point.x = bounds.minX
path.addLine(to: point)
path.close()
And that yields:
That’s using these values:
var edgeWidth: CGFloat = 10
var bubbleWidth: CGFloat = 30
var bubbleHeight: CGFloat = 100
var bubbleStartY: CGFloat = 80
But you can obviously adjust these values as needed.

Show part of a circle

i want to display an arbitray part of a circle.
I know how to get a round View using layer.cornerRadius now i want to see only a part of that circle(for any given radiant value). It would be ok, if the rest of it would be simply hidden beneath something white.
Any ideas, how to achieve that?
edit:
i have written a class for my View:
class Circle:UIView {
var rad = 0
let t = CGFloat(3.0)
override func draw(_ rect: CGRect) {
super.draw(rect)
let r = self.frame.width / CGFloat(2)
let center = CGPoint(x: r, y: r)
let path = UIBezierPath()
path.move(to: CGPoint(x: t, y: r))
path.addLine(to: CGPoint(x: 0.0, y: r))
path.addArc(withCenter: center, radius: CGFloat(r), startAngle: CGFloat(Double.pi), endAngle: CGFloat(Double.pi+rad), clockwise: true)
let pos = path.currentPoint
let dx = r - pos.x
let dy = r - pos.y
let d = sqrt(dx*dx+dy*dy)
let p = t / d
path.addLine(to: CGPoint(x: pos.x + p * dx, y: pos.y + p * dy))
path.addArc(withCenter: center, radius: r-t, startAngle: CGFloat(Double.pi+rad), endAngle: CGFloat(Double.pi), clockwise: false)
UIColor(named: "red")?.setFill()
path.fill()
}
}
public func setRad(perc:Double) {
rad = Double.pi * 2 * perc
}
in my view controller i call
circleView.layer.cornerRadius = circleView.frame.size.width / 2
circleView.clipsToBounds = true
circleView.layer.borderWidth = 1
circleView.layer.borderColor = UIColor.darkGray.cgColor
circleView.layer.shadowColor = UIColor.black.cgColor
circleView.layer.shadowOpacity = 1
circleView.layer.shadowOffset = CGSize.zero
circleView.layer.shadowRadius = 3
circleView.layer.masksToBounds = false
circleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTap)))
now i get the full square View with a black circle from the corner and the red arc that i draw. If necessary i will post a picture tomorrow
One way to do it is to draw a UIBezierPath.
If you just want an arc, you can call the init(arcCenter:radius:startAngle:endAngle:clockwise:) initializer. Remember to specify the start and end angles in radians!
After that, you can set the stroke color and call stroke
If you want a sector of a circle, you can create a UIBezierPath with the parameterless initializer, then move(to:) the center of the circle, and then addLine(to:) the start of the arc. You can probably calculate where this point is with a bit of trigonometry. After that, call addArc like I described above, then addLine(to:) the point where you started. After that, you can fill the path.

How do I compute the intersection of two circle's third set of coordinates under these conditions?

The small circle can move left and right by any amount
and I have to calculate the red dot’s coordinates wherever
its location is, if they intersect. I only calculate this under that condition. I must find the intersection and be sure that it is the intersection on the red dot, and not the intersection below it, so always the one with the higher Y value.
I have solved for all the distances of the triangles and blue dots.
How do I compute the red point?
If you want to look at my current code to help debug it, try this.
My Playground To Test:
//: Playground - noun: a place where people can play
import UIKit
infix operator **
let pretendWidth: CGFloat = 374
let pretendHeight: CGFloat = 7
// Testing scenario is pretendWidth..<(pretendWidth + (pretendHeight / 2))
let spacer: CGFloat = 0.5
extension CGFloat {
public static func **(base: CGFloat, exp: CGFloat) -> CGFloat {
return CGFloat(pow(Double(base), Double(exp)))
}
}
class BottomBarGradientNode: UIView {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.clip(to: bounds)
// Gradient Creation
let locations: [CGFloat] = [0, 1]
let components: [CGFloat] = [0.2706, 0.6863, 0.8902, 1, 0, 0.8745, 0.7294, 1]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient: CGGradient = CGGradient(colorSpace: colorSpace, colorComponents: components, locations: locations, count: 2)!
let startPoint = CGPoint(x: bounds.maxX, y: bounds.maxY)
let endPoint = CGPoint(x: bounds.minX, y: bounds.minY)
let halfHeight = bounds.height / 2
let path = UIBezierPath()
let startPointForPath = CGPoint(x: bounds.width - halfHeight, y: 0)
path.move(to: startPointForPath)
let firstCenterPoint = CGPoint(x: bounds.width - halfHeight, y: halfHeight)
let secondCenterPoint = CGPoint(x: pretendWidth - bounds.height, y: 0)
Computation: if bounds.width > (pretendWidth + halfHeight) {
path.addArc(withCenter: secondCenterPoint, radius: bounds.height, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true)
} else if bounds.width > pretendWidth {
//
// ------------------------------------------------------------------------------------------------------------------------------------
// Though this looks like a complicated geometry problem, this is really best done as an ugly algebra problem.
// We want the coordinates of the red dot: (x,y)
// We know the red dot is on the big circle and since that circle is not moving I'm going to call it's center (0,0) thus:
// x^2 + y^2 = 49
// We also know that the red dot is on the little circle, it has a moving center but we know that the y value for that
// center is always -3.5. so we'll let the x-value of that center be t:
// (x-t)^2 + (y-3.5)^2 = (3.5)^2
// which expands to:
// x^2 - 2xt + t^2 + y^2 -7y + (3.5)^2 = (3.5)^2
// which when we plug in our other equation simplifies to:
// y = (1/7)(-2tx + 49 + t^2)
// plugging that back into the first equation gives:
// x^2 + ((1/7)(-2tx + 49 + t^2))^2 = 49
// which is terrible to look out but turns out to be a quadratic equation in x, so from this point you'd just simplify
// and plug it into the quadratic formula. Pick the value of x that is smaller in magnitude (be careful about negatives
// here). Then plug that x back into the first equation to solve for y.
// ------------------------------------------------------------------------------------------------------------------------------------
//
let boundsHeightSquared = bounds.height ** 2
let distanceFromOtherCenter = firstCenterPoint.x - secondCenterPoint.x
// x^2 + ((1/7)(-2tx + 49 + t^2))^2 = 49 <<<< translates to VVVVVV
//
// ((4/49)t^2 + 1)(x^2) + (-4t - (4t^3/49))(x) + (2t^2 + (t^4)/49) = 0
// ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
// value1(a) value2(b) value3(c)
let value1 = ((4 * (distanceFromOtherCenter ** 2)) / boundsHeightSquared) + 1
let value2 = (-4 * distanceFromOtherCenter) - ((4 * (distanceFromOtherCenter ** 3)) / boundsHeightSquared)
let value3 = (2 * (distanceFromOtherCenter ** 2)) + ((distanceFromOtherCenter ** 4) / boundsHeightSquared)
let (first, second) = getQuadraticValues(a: value1, b: value2, c: value3)
// guarentee positive values
var areBothGreaterThanZero: Bool = false
var chosenX: CGFloat!
if first < 0 { chosenX = second }
else if second < 0 { chosenX = first }
else { chosenX = first < second ? first : second; areBothGreaterThanZero = true }
// y = (1/7)(-2tx + 49 + t^2)
var chosenY = (1 / bounds.height) * ((-2 * distanceFromOtherCenter * chosenX) + boundsHeightSquared - (distanceFromOtherCenter ** 2))
// last check on weird values
if chosenY < 0 && areBothGreaterThanZero {
chosenX = first < second ? first : second
chosenY = (1 / bounds.height) * ((-2 * distanceFromOtherCenter * chosenX) + boundsHeightSquared - (distanceFromOtherCenter ** 2))
}
// Computatation failed. Show full segment.
if chosenY < 0 {
print("Computation Failed")
path.addArc(withCenter: secondCenterPoint, radius: bounds.height, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true)
break Computation
}
// true point
let intersectingPoint = CGPoint(x: chosenX + secondCenterPoint.x, y: chosenY)
// c^2 = a^2 + b^2 - 2abCOS(C)
// (a^2 + b^2 - c^2) / 2ab = COS(C)
let topPoint = CGPoint(x: firstCenterPoint.x, y: 0)
// compute c (distance)
let firstDistanceBetweenPoints = getDistanceBetweenTwoPoints(firstPoint: intersectingPoint, secondPoint: topPoint)
// where a and b are halfHeight
let firstCosC = getCosC(a: halfHeight, b: halfHeight, c: firstDistanceBetweenPoints)
let firstAngle = acos(firstCosC)
path.addArc(withCenter: firstCenterPoint, radius: halfHeight, startAngle: CGFloat.pi * 1.5, endAngle: CGFloat.pi * 1.5 + firstAngle, clockwise: true)
// c^2 = a^2 + b^2 - 2abCOS(C)
// (a^2 + b^2 - c^2) / 2ab = COS(C)
let lastPoint = CGPoint(x: pretendWidth, y: 0)
// compute c (distance)
let secondDistanceBetweenPoints = getDistanceBetweenTwoPoints(firstPoint: lastPoint, secondPoint: intersectingPoint)
// where a and b are bounds.height
let secondCosC = getCosC(a: bounds.height, b: bounds.height, c: secondDistanceBetweenPoints)
let secondAngle = acos(secondCosC)
path.addArc(withCenter: secondCenterPoint, radius: bounds.height, startAngle: secondAngle, endAngle: CGFloat.pi / 2, clockwise: true)
} else {
path.addArc(withCenter: firstCenterPoint, radius: halfHeight, startAngle: CGFloat.pi * 1.5, endAngle: CGFloat.pi / 2, clockwise: true)
}
path.addLine(to: CGPoint(x: bounds.height, y: bounds.height))
let finalCenterPoint = CGPoint(x: bounds.height, y: 0)
path.addArc(withCenter: finalCenterPoint, radius: bounds.height, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: true)
path.addLine(to: startPointForPath)
path.close()
path.addClip()
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsAfterEndLocation)
context.restoreGState()
}
}
func getDistanceBetweenTwoPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat {
let diffX = (firstPoint.x - secondPoint.x) ** 2
let diffY = (firstPoint.y - secondPoint.y) ** 2
return sqrt(diffX + diffY)
}
func getSlopeBetweenTwoPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat {
let diffY = firstPoint.y - secondPoint.y
let diffX = firstPoint.x - secondPoint.x
return diffY / diffX
}
func getHypotenuse(firstDistance: CGFloat, secondDistance: CGFloat) -> CGFloat {
return sqrt((firstDistance ** 2) + (secondDistance ** 2))
}
func getQuadraticValues(a: CGFloat, b: CGFloat, c: CGFloat) -> (CGFloat, CGFloat) {
let first = (-b + sqrt((b ** 2) - (4 * a * c))) / (2 * a)
let second = (-b - sqrt((b ** 2) - (4 * a * c))) / (2 * a)
return (first, second)
}
func getCosC(a: CGFloat, b: CGFloat, c: CGFloat) -> CGFloat {
// (a^2 + b^2 - c^2) / 2ab = COS(C)
return ((a ** 2) + (b ** 2) - (c ** 2)) / (2 * a * b)
}
// Testing scenario is pretendWidth..<(pretendWidth + (height / 2))
let bounds = CGRect(x: 0, y: 0, width: pretendWidth + spacer, height: pretendHeight)
let bar = BottomBarGradientNode(frame: bounds)
Find both points of intersection then pick the appropriate one. Or formulate solution in terms of y coordinate then pick higher solution there to compute x coordinate for that.
The equation of a circle 1 is (x2+y2)+a1x+b1y+c1=0. Write both circles in this form, then subtract one equation from the other. The quadratic terms will cancel out, and the remaining equation describes the radical axis of the circles. ax+by+c=0 where a=a1−a2 and so on. Solve for x=−(by+c)/a. Plug this term into one of the original equations for the circle, and you have a quadratic equation in y.
Now a quadratic equation in y is of the form py2+qy+r=0 and has solutions −q±sqrt(q2−4pr)/2p. Look at the sign of p, then pick that same sign in front of the square root to get the solution with larger y value. Plug that back into the equation of the radical axis to compute the x coordinate.
If there is no intersection, q2−4pr < 0 and your solutions would become complex. If a=0 your radical axis is horizontal so you can't parametrize it by y value, and picking a solution by y value doesn't make any sense.

How to rotate an SKShapeNode line around its bottom point

I'm confused on how to rotate a line (SKShapeNode) around it's bottom point (begin point, here), think of a clock here.
I have the following, but it doesn't seem to be rotating as expected.
public let line = SKShapeNode()
private let linePath = CGMutablePath()
init(begin: CGPoint, end: CGPoint) {
self.begin = begin
self.end = end
linePath.move(to: begin)
linePath.addLine(to: end)
line.path = linePath
line.strokeColor = UIColor.black
line.lineWidth = 3
SceneCoordinator.shared.gameScene.addChild(line)
}
public func rotate(angle: Double) {
var transform = CGAffineTransform(rotationAngle: CGFloat(angle))
line.path = linePath.mutableCopy(using: &transform)
}
Your function rotates the path around the
shapes position (which is (0, 0) by default) and not around the starting point of the line as intended.
To solve the problem, create the shape with a position equal to the starting
point of the line, and with a line relative to that point:
linePath.move(to: CGPoint.zero)
linePath.addLine(to: CGPoint(x: end.x - begin.x, y: end.y - begin.y))
line.path = linePath
line.position = begin
// ...
SceneCoordinator.shared.gameScene.addChild(line)
Note that instead of transforming the path you can rotate the node:
line.zRotation = angle
or with animation:
line.run(SKAction.rotate(toAngle: angle, duration: 0.2))
You can compute the position of the endpoint in the scene's
coordinate system with
let endPos = line.convert(CGPoint(x: end.x - begin.x, y: end.y - begin.y), to: line.scene!)

How to connect two SCNSpheres in 3D space using Bezier path in Swift?

I have the following code:
func createScene(){
count += 1
let sphereGeom = SCNSphere(radius: 1.5)
sphereGeom.firstMaterial?.diffuse.contents = UIColor.redColor()
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
let radius = 3.0
var radians = Double(0)
var yPosition = Float(5.4)
while count <= 20 {
if radians >= 2{
radians -= 2
}
let sphereNode = SCNNode(geometry: sphereGeom)
let angle = Double(radians * M_PI)
let xPosition = Float(radius * cos(angle))
let zPosition = Float(radius * sin(angle))
sphereNode.position = SCNVector3(xPosition, yPosition, zPosition)
let cgX = CGFloat(xPosition)
let cgY = CGFloat(yPosition)
path.addQuadCurveToPoint(CGPoint(x: cgX, y: cgY), controlPoint: CGPoint(x: (cgX / 2), y: (cgY / 2)))
path.addLineToPoint(CGPoint(x: (cgX - (cgX * 0.01)), y: cgY))
path.addQuadCurveToPoint(CGPoint(x: 1, y: 0), controlPoint: CGPoint(x: (cgX / 2), y: ((cgY / 2) - (cgY * 0.01))))
let shape = SCNShape(path: path, extrusionDepth: 3.0)
shape.firstMaterial?.diffuse.contents = UIColor.blueColor()
let shapeNode = SCNNode(geometry: shape)
shapeNode.eulerAngles.y = Float(-M_PI_4)
self.rootNode.addChildNode(shapeNode)
count += 1
radians += 0.5556
yPosition -= 1.35
self.rootNode.addChildNode(sphereNode)
}
I want to add a Bezier path connecting each sphere to the next one, creating a spiral going down the helix. For some reason, when I add this code, the shape doesn't even appear. But when I use larger x and y values, I see the path fine, but it is no way oriented to the size of the spheres. I don't understand why it disappears when I try to make it smaller.
Your SCNShape doesn't ever get extruded. Per Apple doc,
An extrusion depth of zero creates a flat, one-sided shape.
With larger X/Y values your flat shape happens to become visible. You can't build a 3D helix with SCNShape, though: the start and end planes of the extrusion are parallel.
You'll have to use custom geometry, or approximate your helix with a series of elongated SCNBox nodes. And I bet someone out there knows how to do this with a shader.