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.
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 am trying to add a SKNode between two points like picture below.
What I have:
I count the distance between those two points with this code (works fine):
func distanceCount(_ point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y)))) }
Then I count the middle point(also works fine)
func middlePointCount(_ point: CGPoint) -> CGPoint {
return CGPoint(x: CGFloat((point.x + x) / 2), y: CGFloat((point.y + y) / 2))
}
Finally this function adds my object (SKNode) :
func addBlock(_ size:CGSize, rotation:CGFloat, point: CGPoint) -> SKNode{
let block = SKSpriteNode(color: UIColor.lightGray , size: size)
block.physicsBody = SKPhysicsBody(rectangleOf: block.frame.size)
block.position = point //This is my middle point
block.physicsBody!.affectedByGravity = false
block.physicsBody!.isDynamic = false
block.zRotation = rotation
return block
}
Summary: My addBlock function adds object with right width and centred on the right place , but angle is wrong.
Note: I have tried to create functions which should count the angle but they were all wrong :/ .
My question: How can I get the right angle , or is there some other how can I reach my goal?
If you need more details just let me know.
Thank you :)
To get the angle between two points you'll need to use the following
atan2(p2.y-p1.y, p2.x-p1.x)
Midpoint
The midpoint between 2 points A and B is defined as
midpoint = {(A.x + B.x) / 2, (A.y + B.y) / 2}
CGPoint Extension
So let's create and extension of CGPoint to easily build a Midpoint starting from 2 points
extension CGPoint {
init(midPointBetweenA a: CGPoint, andB b: CGPoint) {
self.x = (a.x + b.x) / 2
self.y = (a.y + b.y) / 2
}
}
Test
Now let's test it
let a = CGPoint(x: 1, y: 4)
let b = CGPoint(x: 2, y: 3)
let c = CGPoint(midPointBetweenA: a, andB: b) // {x 1,5 y 3,5}
Looks good right?
Wrap up
Now given your 2 points you just need to calculate the midpoint and assign it to the position of your SKNode.
let nodeA: SKNode = ...
let nodeB: SKNode = ...
let nodeC: SKNode = ...
nodeC.position = CGPoint(midPointBetweenA: nodeA.position, andB: nodeB.position)
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 given three points and need to draw a smooth 3D parabola. The trouble is that curved line is choppy and has some weird divots in it
Here is my code...
func drawJump(jump: Jump){
let halfDistance = jump.distance.floatValue/2 as Float
let tup = CalcParabolaValues(0.0, y1: 0.0, x2: halfDistance, y2: jump.height.floatValue, x3: jump.distance.floatValue, y3: 0)
println("tuple \tup")
var currentX = 0 as Float
var increment = jump.distance.floatValue / Float(50)
while currentX < jump.distance.floatValue - increment {
let x1 = Float(currentX)
let x2 = Float((currentX+increment))
let y1 = calcParabolaYVal(tup.a, b: tup.b, c: tup.c, x: x1)
let y2 = calcParabolaYVal(tup.a, b: tup.b, c: tup.c, x: x2)
drawLine(x1, y1: y1, x2: x2, y2: y2)
currentX += increment
}
}
func CalcParabolaValues(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float) -> (a: Float, b: Float, c: Float) {
println(x1, y1, x2, y2, x3, y3)
let a = y1/((x1-x2)*(x1-x3)) + y2/((x2-x1)*(x2-x3)) + y3/((x3-x1)*(x3-x2))
let b = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2)))
let c = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2)))
return (a, b, c)
}
func calcParabolaYVal(a:Float, b:Float, c:Float, x:Float)->Float{
return a * x * x + b * x + c
}
func drawLine(x1: Float, y1: Float,x2: Float, y2: Float) {
println("drawLine \(x1) \(y1) \(x2) \(y2)")
let positions: [Float32] = [
x1, y1, 0,
x2, y2, 0
]
let positionData = NSData(bytes: positions, length: sizeof(Float32)*positions.count)
let indices: [Int32] = [0, 1]
let indexData = NSData(bytes: indices, length: sizeof(Int32) * indices.count)
let source = SCNGeometrySource(data: positionData, semantic: SCNGeometrySourceSemanticVertex, vectorCount: indices.count, floatComponents: true, componentsPerVector: 3, bytesPerComponent: sizeof(Float32), dataOffset: 0, dataStride: sizeof(Float32) * 3)
let element = SCNGeometryElement(data: indexData, primitiveType: SCNGeometryPrimitiveType.Line, primitiveCount: indices.count, bytesPerIndex: sizeof(Int32))
let line = SCNGeometry(sources: [source], elements: [element])
self.rootNode.addChildNode( SCNNode(geometry: line))
}
func renderer(aRenderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: NSTimeInterval) {
glLineWidth(20)
}
I also have to figure out how to animate the arc from left to right. Can someone help me out? Swift or Objective C is fine. Any help is appreciated. Thanks!
I'd recommend using SCNShape to create your parabola. To start, you'll need to represent your parabola as a Bézier curve. You can use UIBezierPath for that. For animation, I personally find shader modifiers are a nice fit for cases like this.
The Parabola
Watch out, though — you probably want a path that represents just the open stroke of the arc. If you do something like this:
let path = UIBezierPath()
path.moveToPoint(CGPointZero)
path.addQuadCurveToPoint(CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 50, y: 200))
You'll get a filled-in parabola, like this (seen in 2D in the debugger quick look, then extruded in 3D with SCNShape):
To create a closed shape that's just the arc, you'll need to trace back over the curve, a little bit away from the original:
let path = UIBezierPath()
path.moveToPoint(CGPointZero)
path.addQuadCurveToPoint(CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 50, y: 200))
path.addLineToPoint(CGPoint(x: 99, y: 0))
path.addQuadCurveToPoint(CGPoint(x: 1, y: 0), controlPoint: CGPoint(x: 50, y: 198))
That's better.
... in Three-Dee!
How to actually make it 3D? Just make an SCNShape with the extrusion depth you like:
let shape = SCNShape(path: path, extrusionDepth: 10)
And set it in your scene:
shape.firstMaterial?.diffuse.contents = SKColor.blueColor()
let shapeNode = SCNNode(geometry: shape)
shapeNode.pivot = SCNMatrix4MakeTranslation(50, 0, 0)
shapeNode.eulerAngles.y = Float(-M_PI_4)
root.addChildNode(shapeNode)
Here I'm using a pivot to make the shape rotate around the major axis of the parabola, instead of the y = 0 axis of the planar Bézier curve. And making it blue. Also, root is just a shortcut I made for the view's scene's root node.
Animating
The shape of the parabola doesn't really need to change through your animation — you just need a visual effect that progressively reveals it along its x-axis. Shader modifiers are a great fit for that, because you can make the animated effect per-pixel instead of per-vertex and do all the expensive work on the GPU.
Here's a shader snippet that uses a progress parameter, varying from 0 to 1, to set opacity based on x-position:
// declare a variable we can set from SceneKit code
uniform float progress;
// tell SceneKit this shader uses transparency so we get correct draw order
#pragma transparent
// get the position in model space
vec4 mPos = u_inverseModelViewTransform * vec4(_surface.position, 1.0);
// a bit of math to ramp the alpha based on a progress-adjusted position
_surface.transparent.a = clamp(1.0 - ((mPos.x + 50.0) - progress * 200.0) / 50.0, 0.0, 1.0);
Set that as a shader modifier for the Surface entry point, and then you can animate the progress variable:
let modifier = "uniform float progress;\n #pragma transparent\n vec4 mPos = u_inverseModelViewTransform * vec4(_surface.position, 1.0);\n _surface.transparent.a = clamp(1.0 - ((mPos.x + 50.0) - progress * 200.0) / 50.0, 0.0, 1.0);"
shape.shaderModifiers = [ SCNShaderModifierEntryPointSurface: modifier ]
shape.setValue(0.0, forKey: "progress")
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(10)
shape.setValue(1.0, forKey: "progress")
SCNTransaction.commit()
Further Considerations
Here's the whole thing in a form you can paste into a (iOS) playground. A few things left as exercises to the reader, plus other notes:
Factor out the magic numbers and make a function or class so you can alter the size/shape of your parabola. (Remember that you can scale SceneKit nodes relative to other scene elements, so they don't have to use the same units.)
Position the parabola relative to other scene elements. If you take away my line that sets the pivot, the shapeNode.position is the left end of the parabola. Change the parabola's length (or scale it), then rotate it around its y-axis, and you can make the other end line up with some other node. (For you to fire ze missiles at?)
I threw this together with Swift 2 beta, but I don't think there's any Swift-2-specific syntax in there — porting back to 1.2 if you need to deploy soon should be straightforward.
If you also want to do this on OS X, it's a bit trickier — there, SCNShape uses NSBezierPath, which unlike UIBezierPath doesn't support quadratic curves. Probably an easy way out would be to fake it with an elliptical arc.
I don't think your table has enough points, assuming the renderer is connecting them with straight line segments. On top of this, the thickness and dashing of the line make it difficult to see that. Try getting a smooth curve with a thin solid line first.
If you want to animate the progression of the curve, as if it were showing the flight of a projectile, it will probably be easiest to just write your function for the motion: y = k*x^2, and just render from x=0 to x=T for increasing values of T.