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.
Related
I know use UIBezierPath() to draw a dash line like:
let path = UIBezierPath()
path.setLineDash([CGFloat(4), CGFloat(4)], count: 2, phase: 0)
path.lineCapStyle = CGLineCap.round
path.move(to: startPoint)
path.addLine(to: endPoint)
path.stroke()
It works fine. But how could I can draw a dash line in context? Like below code can draw a solid line no matter if i add
context.setLineDash(phase: 3, lengths: [3,2])
The whole code:
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
let startPoint = CGPoint(x: 50, y: 10)
let endPoint = CGPoint(x: rect.width-100, y: 10)
context.setLineDash(phase: 3, lengths: [3,2])
context.setLineWidth(10)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.setStrokeColor(UIColor.red.cgColor)
context.setLineCap(.round)
context.strokePath()
}
}
The result is:
Anything wrong?
Think about it...
The line width is 10.
The line cap is round.
So the radius of the line cap is 5.
N.B. The line ends at the centre point of the line cap (not at the end of the cap).
The length of each dash is 3 and the gap between the lines is 2.
So the radius of the cap is much much bigger than the gap between the lines. So each line is overlapping to the next one.
Try making the line lengths like...
[3, 12]
That should make the lines 3 points long with a cap radius of 5 and then a gap between the ends of the caps of 2 (12 - 5 - 5).
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.)
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
I have been doing a ton of research but found nothing. With MapKit, I have got a map that shows current location and elsewhere a function that calculates a heading/bearing value (not necessarily the actual heading). How can I draw a line on the map that will start at current location, and point in direction of the given heading ? (Does not matter how long the line is, as in it has no meaningful end point). I am not asking you to write the code for me but would appreciate some detailed direction. Hope this helps others too.
Cheers
Your coordinates are polar, which means you have a direction and a length. You just need to convert them to Cartesian, which gives you a horizontal offset and a vertical offset. You do that with a little trigonometry.
let origin = CGPoint(x: 10, y: 10)
let heading: CGFloat = CGFloat.pi
let length: CGFloat = 20
let endpoint = CGPoint(x: origin.x + cos(heading)*length,
y: origin.y + sin(heading)*length)
let path = UIBezierPath()
path.move(to: origin)
path.addLine(to: endpoint)
Note that trigonometric functions generally work in radians (2*PI = one revolution). Bearings are often in degrees (360 degrees = one revolution). Converting is straightforward, however:
func radians(forDegrees angle: CGFloat) -> CGFloat {
return CGFloat.pi * angle / 180.0
}
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
}
}