How to rotate an SKShapeNode line around its bottom point - swift

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!)

Related

Using UIViewPropertyAnimator to drive animation along curved path?

I have an interruptible transition that is driven by UIViewPropertyAnimator and I would like to animate a view along a non linear path to its destination.
Doing some research I've found that the way to do this is by using:
CAKeyframeAnimation. However, I'm having issue coordinating the animations also I'm having issue with the CAKeyframeAnimation fill mode.
It appears to be reverse even though I've set it to forwards.
Below is the bezier path:
let bezierPath = self.generateCurve(from: CGPoint(x: self.itemStartFrame.midX, y: self.itemStartFrame.midY), to: CGPoint(x: self.itemEndFrame.midX, y: self.itemEndFrame.midY), bendFactor: 1, thickness: 1)
func generateCurve(from: CGPoint, to: CGPoint, bendFactor: CGFloat, thickness: CGFloat) -> UIBezierPath {
let center = CGPoint(x: (from.x+to.x)*0.5, y: (from.y+to.y)*0.5)
let normal = CGPoint(x: -(from.y-to.y), y: (from.x-to.x))
let normalNormalized: CGPoint = {
let normalSize = sqrt(normal.x*normal.x + normal.y*normal.y)
guard normalSize > 0.0 else { return .zero }
return CGPoint(x: normal.x/normalSize, y: normal.y/normalSize)
}()
let path = UIBezierPath()
path.move(to: from)
let midControlPoint: CGPoint = CGPoint(x: center.x + normal.x*bendFactor, y: center.y + normal.y*bendFactor)
let closeControlPoint: CGPoint = CGPoint(x: midControlPoint.x + normalNormalized.x*thickness*0.5, y: midControlPoint.y + normalNormalized.y*thickness*0.5)
let farControlPoint: CGPoint = CGPoint(x: midControlPoint.x - normalNormalized.x*thickness*0.5, y: midControlPoint.y - normalNormalized.y*thickness*0.5)
path.addQuadCurve(to: to, controlPoint: closeControlPoint)
path.addQuadCurve(to: from, controlPoint: farControlPoint)
path.close()
return path
}
Path visualization:
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 2
containerView.layer.addSublayer(shapeLayer)
Here's the CAKeyframeAnimation that resides inside of the UIViewPropertyAnimator
let curvedPathAnimation = CAKeyframeAnimation(keyPath: "position")
curvedPathAnimation.path = bezierPath.cgPath
curvedPathAnimation.fillMode = kCAFillModeForwards
curvedPathAnimation.calculationMode = kCAAnimationPaced
curvedPathAnimation.duration = animationDuration
self.attatchmentView.layer.add(curvedPathAnimation, forKey: nil)
So far this animation along the correct path, however the animation reverse back to it's starting position and is not in sync with the property animator.
Any suggestions?

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.

Divide a line in n equal parts

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)]

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.

(SWIFT) Sprite not giving me the correct zRotation value after being added

Details of program: by looking at this picture(http://i.stack.imgur.com/QOZ53.png), what I'm trying to do is have the spaceship circle the planet. I achieved this by making a line node and changes its anchor point to the top and then position it to the centre of the planet which then upon impact of the spaceship and planet, the line is angled towards the ship in which then the ship is removed from the view and added to the line node as a child and together they rotate around the planet, as the ship rotates around the the planet, the ship it self also rotates on the spot (hopefully that makes sense)
Problem: the problem is im not getting the correct zRotation value of the ship. Right now I got the code to draw another ship at the location where it was tapped and set the zRotation of that image to the zRotation of the ship but i keep getting different angles. (refer to picture). Is this because I have ship added to line node as a child? Im running a rotating animation on both the line and the ship. In the picture, the line is rotating around the planet counter clockwise dragging the ship along with it and the ship itself is also rotating counter clockwise on the spot. In the picture the left ship; the one that's touching the line is the one thats rotating, the ship on the right is just drawn to angle of the rotating ship but by looking at the picture you can see the angle of the ship is pretty opposite of the one on the left. Why is this so? what I noticed is that when the ship is at the bottom half of the planet, the angles are fine but when it comes to the top half, the angles are kinda opposite(refer to picture)
Picture
Console Log:
I'm Touched -
ship angle in degrees: 83.6418545381942
PLAYER POSITION X: 100.0 Y: 100.0
PLAYER ZROTATION: 1.45982575416565
WE'RE TOUCHING -
PLANET POSITION X*: 120.0 Y: 230.000015258789
PLAYER-SHIP POSITION X: 107.998710632324 Y: 171.783294677734
ANGLE OF LINE RADIANS*: -1.77409685090117 DEGRESS: -101.648262004087
PLAYER-SHIP ROTATION: 1.57079637050629
I'm Touched -
ship angle in degrees: 314.660859137531
TEMP POS X: 136.535125732422 Y: 287.094879150391
TEMP ZROTATION: 5.491868019104
PLAYER POSITION X: 136.535125732422 Y: 287.094879150391
PLAYER ZROTATION: 5.491868019104
Collision Code:
func didBeginContact(contact: SKPhysicsContact) {
if contact.bodyA.categoryBitMask == planetGroup || contact.bodyB.categoryBitMask == planetGroup {
print(" WE'RE TOUCHING ")
moving = true
touching = true
let degrees = 45.0
let radians = degrees * M_PI / 180.0
//rotate Line
var rotateLine = SKAction.rotateByAngle(CGFloat(radians), duration: 0.5)
var repeatLine = SKAction.repeatActionForever(rotateLine)
//rotates Ship
var rotateShip = SKAction.rotateByAngle(CGFloat(radians), duration: 0.4)
var repeatShip = SKAction.repeatActionForever(rotateShip)
playerShip.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
planetNode = contact.bodyA.node as! SKSpriteNode
planetX = planetNode.position.x
planetY = planetNode.position.y
playerX = playerShip.position.x
playerY = playerShip.position.y
var angleOfAnchor = AngleBetweenPoints(planetNode.position, endPoint: playerShip.position)
var three60 = 360 * CGFloat(M_PI) / 180.0
var nintey = 90 * CGFloat(M_PI) / 180.0
var inDegree = angleOfAnchor * 180.0 / CGFloat(M_PI)
var shipPlanetDistance = SDistanceBetweenPoints(planetNode.position, p2: playerShip.position)
line = SKSpriteNode(color: UIColor.blackColor(), size: CGSize(width: 2, height: planetNode.size.height))
line.anchorPoint = CGPoint(x: 0.5, y: 1)
line.position = CGPoint(x: planetX, y: planetY)
line.zRotation = -(three60 - nintey - angleOfAnchor)
self.addChild(line)
tempShip = playerShip
playerShip.removeFromParent()
line.runAction(repeatLine, withKey: "rotateLine")
//playerShip.position = CGPoint(x: playerX, y: playerY)
line.addChild(playerShip)
playerShip.zRotation = (90 * CGFloat(M_PI) / 180.0)
playerShip.runAction(repeatShip, withKey: "rotateShip")
print("*PLANET POSITION* X: \(planetX) Y: \(planetY) \r *PLAYER-SHIP POSITION* X: \(playerX) Y: \(playerY) \r *ANGLE OF LINE* RADIANS: \(angleOfAnchor) DEGRESS: \(inDegree) *PLAYER-SHIP ROTATION: \(playerShip.zRotation)")
}
}
Screen Touch Code:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
print(" I'm Touched ")
var radians:CGFloat = playerShip.zRotation
var degrees = radians * 180.0 / CGFloat(M_PI)
var dx = cos(radians)
var dy = sin(radians)
print(" ship angle in degrees: \(degrees) ")
var tempAngle = playerShip.zRotation
var shipPosition = convertPoint(playerShip.position, fromNode: line)
playerX = shipPosition.x
playerY = shipPosition.y
if startMove == true {
playerShip.removeActionForKey("rotateShip")
playerShip.physicsBody?.velocity = CGVector(dx: 100*dx, dy: 100*dy)// speed of direction
startMove = false
}
if moving == true{
//playerShip.removeActionForKey("rotateShip")
//playerShip.removeFromParent()
//self.addChild(playerShip)
var radians:CGFloat = playerShip.zRotation
var degrees = radians * 180.0 / CGFloat(M_PI)
var dx = cos(radians)
var dy = sin(radians)
print(" ship angle in degrees: \(degrees) ")
//playerShip.zRotation = tempShip.zRotation
//playerShip.position = CGPoint(x: playerX, y: playerY)
//playerShip.physicsBody?.velocity = CGVector(dx: 100*dx, dy: 100*dy)// speed of direction
// this is the ship that gets drawn
var temp = SKSpriteNode(imageNamed: "img/ship/2.png")
temp.position = CGPoint(x: playerX, y: playerY)
temp.zRotation = playerShip.zRotation
self.addChild(temp)
//moving = false
print("*TEMP POS* X: \(temp.position.x) Y: \(temp.position.y) *TEMP ZROTATION*: \(temp.zRotation)")
}
print("*PLAYER POSITION* X: \(playerX) Y: \(playerY) *PLAYER ZROTATION: \(playerShip.zRotation)")
}
I managed to solve this problem, I added the line.zrotation with the playerShip.zrotation and it ended up working perfectly