SpriteKit PhysicsBody is not applying for my bezier curve - swift

Here is the test code:
var paths = UIBezierPath()
var bottomLeft = CGPoint(x: dim.minX, y:dim.minY)
var bottomRight = CGPoint(x: dim.maxX , y: dim.minY )
var topRight = CGPoint(x: dim.maxX, y: dim.maxY )
var topLeft = CGPoint(x: dim.minX, y: dim.maxY)
paths.moveToPoint(bottomRight)
paths.addLineToPoint(topRight)
paths.moveToPoint(topRight)
paths.addLineToPoint(topLeft)
paths.moveToPoint(topLeft)
paths.addLineToPoint(bottomLeft)
paths.moveToPoint(bottomLeft)
paths.addLineToPoint(bottomRight)
var n = SKShapeNode(path: paths.CGPath)
n.strokeColor = UIColor.greenColor()
n.physicsBody = SKPhysicsBody(edgeLoopFromPath: paths.CGPath)
shapeLayer.addChild(n)
It draws a standard rect. I will add some curve in the middle from topRight to topLeft points, but I wanted to test it with simple rect. I am testing the collision with a ball and it seems to be only colliding with the line from bottomRight to topRight and ignoring the rest. Does anyone have a idea on whats going on here?

Although I am not sure about the issue with bezier path and the physics collision setup code needed, I was however able to get it working with using raw paths and adding lines:
var polygonPath = CGPathCreateMutable()
CGPathMoveToPoint(polygonPath, nil, bottomRight.x, bottomRight.y)
CGPathAddLineToPoint(polygonPath, nil, topRight.x, topRight.y)
CGPathAddLineToPoint(polygonPath, nil, topLeft.x, topLeft.y)
CGPathAddLineToPoint(polygonPath, nil, bottomLeft.x, bottomLeft.y)
CGPathCloseSubpath(polygonPath)
var n = SKShapeNode(path: polygonPath)
n.strokeColor = UIColor.grayColor()
n.physicsBody = SKPhysicsBody(edgeLoopFromPath: polygonPath)
That sets up proper physics collision bounds. Although this solves my issue, any insight on why the original code was not working with bezier path would be great. Thanks.

Related

How can I draw a circle in SceneKit using its radius and origin?

What I am trying to do that draw a circle node using its radius and origin points.
Is there any way to draw a circle node in SceneKit?
How can I draw that?
First approach
Use SceneKit SCNCylinder's procedural mesh to create a circle.
func circle(origin: SCNVector3, radius: CGFloat) -> SCNNode {
let cylinderNode = SCNNode()
cylinderNode.geometry = SCNCylinder(radius: radius, height: 0.001)
cylinderNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
cylinderNode.geometry?.firstMaterial?.isDoubleSided = true
cylinderNode.position = origin
cylinderNode.eulerAngles.x = .pi/2
return cylinderNode
}
sceneView.scene?.rootNode.addChildNode(circle(
origin: SCNVector3(), radius: 0.5)
)
Second approach
This approach is impractical for your particular case, because you must specify the radius in UIKit units, not in meters like in SceneKit. However, I decided to publish it as option B.
// Path
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0),
radius: 20,
startAngle: 0.0,
endAngle: .pi * 2,
clockwise: true)
circlePath.flatness = 0.1
// Shape
let material = SCNMaterial()
material.diffuse.contents = UIColor.systemRed
material.isDoubleSided = true
let circleShape = SCNShape(path: circlePath, extrusionDepth: 0.001)
circleShape.materials = [material]
// Node
let circleNode = SCNNode()
circleNode.geometry = circleShape
sceneView.scene?.rootNode.addChildNode(circleNode)
You could probably do something like this:
let circle = SCNPlane(width: 1.0, height: 1.0)
circle.cornerRadius = circle.width/2
hope, this is, what you are looking for.
another approch might be the SCNCylinder with a very small height or even SCNTorus with a very small pipe radius.

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

Sprite positioning at wrong points in y axis

so I know this is a dumb question, but maybe it'll help other people like me out too. Basically, I'm making a sprite appear at the top of the screen and then move to the bottom but in a random x position. There's nothing wrong with moving to the random x position, the problem is that it doesn't start out at the top of the screen. Here is my code:
let sprite = SKSpriteNode(imageNamed: "comet")
sprite.size = CGSize(width: 150, height: 100)
sprite.position = CGPoint(x: self.frame.size.width/2, y: 0)
sprite.zPosition = 100
self.addChild(sprite) `
let randomNum = CGPoint(x:Int (arc4random() % 1000), y: 1)
let actionMove = SKAction.moveTo(randomNum, duration: 1)
let actionComet = SKAction(actionMove)
sprite.runAction(actionComet)
Any help is appreciated.
You are placing your sprite at location (0,0) which is left bottom of the screen. If you want to place the sprite at the top use:
sprite.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height + sprite.size.height / 2.0)

Rotate a SKShapeNode with texture

I'm trying to rotate an SKShapeNode with a texture, but it's not working. Basically, I have a circle with a texture and I'm trying to make it rotate using the same way I have done with an SKSpriteNode:
let spin = SKAction.rotateByAngle(CGFloat(M_PI/4.0), duration: 1)
The problem is that the circle is rotating, but not the texture. I can check this by using this code:
let wait = SKAction.waitForDuration(0.5)
let block = SKAction.runBlock({
print(self.circle.zRotation)
})
let sequence = SKAction.sequence([wait, block])
self.runAction(SKAction.repeatActionForever(sequence))
This is the code I have for creating the SKShapeNode:
let tex:SKTexture = SKTexture(image: image)
let circle = SKShapeNode(circleOfRadius: 100 )
circle.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2 + 200)
circle.strokeColor = SKColor.clearColor()
circle.glowWidth = 1.0
circle.fillColor = SKColor.whiteColor()
circle.fillTexture = tex
self.addChild(circle)
// Runing the action
circle.runAction(spin)
Please help. Thanks in advance!
PS: I know that using an SKSpriteNode would be better but my goal is to place a square image in a circular frame and I figured that using an SKShapeNode would be perfect. If anyone know how to create a circular SKSpriteNode, feel free to post it in the answers section! :)
This is what I'm trying to achieve (with the capability of rotating it):
You can achieve what you want by using SKCropNode and setting its mask property to be a circle texture:
override func didMoveToView(view: SKView) {
let maskShapeTexture = SKTexture(imageNamed: "circle")
let texture = SKTexture(imageNamed: "pictureToMask")
let pictureToMask = SKSpriteNode(texture: texture, size: texture.size())
let mask = SKSpriteNode(texture: maskShapeTexture) //make a circular mask
let cropNode = SKCropNode()
cropNode.maskNode = mask
cropNode.addChild(pictureToMask)
cropNode.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(cropNode)
}
Let's say that picture to mask has a size of 300x300 pixels. The circle you are using as a mask, in that case, has to have the same size. Means the circle itself has to have a radius of 150 pixels (diameter of 300 pixels) when made in image editor.
Mask node determines the visible area of a picture. So don't make it too large. Mask node has to be a fully opaque circle with transparent background.

Rotate CGPath without changing its position

I want to rotate a CGPath, I'm using the following code:
CGAffineTransform transform = CGAffineTransformMakeRotation(angleInRadians);
CGPathRef rotatedPath = CGPathCreateCopyByTransformingPath(myPath, &transform);
This code works fine but it changes path's position! I want the path remain at the same position as it was before rotating.
A path doesn't have “a position”. A path is a set of points (defined by line and curve segments). Every point has its own position.
Perhaps you want to rotate the path around a particular point, instead of around the origin. The trick is to create a composite transform that combines three individual transforms:
Translate the origin to the rotation point.
Rotate.
Invert the translation from step 1.
For example, here's a function that takes a path and returns a new path that is the original path rotated around the center of its bounding box:
static CGPathRef createPathRotatedAroundBoundingBoxCenter(CGPathRef path, CGFloat radians) {
CGRect bounds = CGPathGetBoundingBox(path); // might want to use CGPathGetPathBoundingBox
CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, center.x, center.y);
transform = CGAffineTransformRotate(transform, radians);
transform = CGAffineTransformTranslate(transform, -center.x, -center.y);
return CGPathCreateCopyByTransformingPath(path, &transform);
}
Note that this function returns a new path with a +1 retain count that you are responsible for releasing when you're done with it. For example, if you're trying to rotate the path of a shape layer:
- (IBAction)rotateButtonWasTapped:(id)sender {
CGPathRef path = createPathRotatedAroundBoundingBoxCenter(shapeLayer_.path, M_PI / 8);
shapeLayer_.path = path;
CGPathRelease(path);
}
UPDATE
Here's a demonstration using a Swift playground. We'll start with a helper function that displays a path and marks the origin with a crosshair:
import UIKit
import XCPlayground
func showPath(label: String, path: UIBezierPath) {
let graph = UIBezierPath()
let r = 40
graph.moveToPoint(CGPoint(x:0,y:r))
graph.addLineToPoint(CGPoint(x:0,y:-r))
graph.moveToPoint(CGPoint(x:-r,y:0))
graph.addLineToPoint(CGPoint(x:r,y:0))
graph.appendPath(path)
XCPCaptureValue(label, graph)
}
Next, here's our test path:
var path = UIBezierPath()
path.moveToPoint(CGPoint(x:1000,y:1000))
path.addLineToPoint(CGPoint(x:1000,y:1200))
path.addLineToPoint(CGPoint(x:1100,y:1200))
showPath("original", path)
(Remember, the crosshair is the origin and is not part of the path we're transforming.)
We get the center and transform the path so it's centered at the origin:
let bounds = CGPathGetBoundingBox(path.CGPath)
let center = CGPoint(x:CGRectGetMidX(bounds), y:CGRectGetMidY(bounds))
let toOrigin = CGAffineTransformMakeTranslation(-center.x, -center.y)
path.applyTransform(toOrigin)
showPath("translated center to origin", path)
Then we rotate it. All rotations happen around the origin:
let rotation = CGAffineTransformMakeRotation(CGFloat(M_PI / 3.0))
path.applyTransform(rotation)
showPath("rotated", path)
Finally, we translate it back, exactly inverting the original translation:
let fromOrigin = CGAffineTransformMakeTranslation(center.x, center.y)
path.applyTransform(fromOrigin)
showPath("translated back to original center", path)
Note that we must invert the original translation. We don't translate by the center of its new bounding box. Recall that (in this example), the original center is at (1050,1100). But after we've translated it to the origin and rotated it, the new bounding box's center is at (-25,0). Translating the path by (-25,0) will not put it anywhere close to its original position!
A Swift 5 version:
func rotate(path: UIBezierPath, degree: CGFloat) {
let bounds: CGRect = path.cgPath.boundingBox
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radians = degree / 180.0 * .pi
var transform: CGAffineTransform = .identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y)
path.apply(transform)
}
A Swift 3 method to rotate a rectangle in place around its center:
func createRotatedCGRect(rect: CGRect, radians: CGFloat) -> CGPath {
let center = CGPoint(x: rect.midX, y: rect.midY)
let path = UIBezierPath(rect: rect)
path.apply(CGAffineTransform(translationX: center.x, y: center.y).inverted())
path.apply(CGAffineTransform(rotationAngle: radians))
path.apply(CGAffineTransform(translationX: center.x, y: center.y))
return path.cgPath
}
All of rob mayoff's code for applying this to a path and its bounding box would still apply, so I didn't see cause to repeat them.
In case you want to rotate and/or scale an UIBezierPath, or scale a CAShapeLayer while keep the center of the UIBezierPath. Here the function for Swift 4/5. It is based on answer from rob mayoff.
extension UIBezierPath {
/// perform scale on UIBezierPath and keep the position in the path's center
func scale(scale:CGFloat) {
let bounds = self.cgPath.boundingBox
let center = CGPoint(x:bounds.midX,y:bounds.midY)
var transform = CGAffineTransform.identity.translatedBy(x: center.x, y: center.y)
transform = transform.scaledBy(x: scale, y: scale)
transform = transform.translatedBy(x: -center.x, y: -center.y);
self.apply(transform)
}
/// perform rotate on UIBezierPath and keep the center unchanged
func rotate(radians:CGFloat) {
let bounds = self.cgPath.boundingBox
let center = CGPoint(x:bounds.midX,y:bounds.midY)
var transform = CGAffineTransform.identity.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y);
self.apply(transform)
}
}