How to draw a face to SKShapeNode - ios

I'm fairly new to Swift and I'm coding for my kids. I'm using sprite-kit to create a simple game for them. At the moment I'm struggling with how to draw a face to an arbitrary skshapenode? I know that there is no holes within the shape but other than that it can be of any shape. Dispite what shape it is, I would like to draw eyes and mouth to it programmatically. The shapes can came in many sizes.
Any suggestions how to approach this?

Here's an example of how to draw a smiley face with an SKShapeNode. It provides a framework that you can expand upon to draw more complex faces.
We start by extending the UIBezierPath class to add instance methods that are useful for drawing faces. The first method adds a circle to a path at a given location and with a specified radius. The moveToPoint statement moves the current point of a path to a new location. This is the equivalent of picking up a "pen" and moving it to a new location to draw a different, non-connected object. The second method draws a half circle with the open side facing up. This can be used to draw a smile. Note that extensions cannot be defined within a class. You can place it above your GameScene definition.
extension UIBezierPath {
func addCircle(center:CGPoint, radius:CGFloat) {
self.moveToPoint(CGPointMake(center.x+radius, center.y))
self.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(2*M_PI), clockwise: true)
}
func addHalfCircle(center:CGPoint, radius:CGFloat) {
self.moveToPoint(CGPointMake(center.x+radius, center.y))
self.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: false)
}
}
We can now use the extended UIBezierPath class to draw a face consisting of a face outline (a large circle), eyes (two small circles), and a mouth (a half circle). The face is constructed by adding components of the face to a path and then attaching the path to an SKShapeNode.
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
scaleMode = .ResizeFill
let face = SKShapeNode()
// The argument determines the size of the face
face.path = drawFace(50)
face.lineWidth = 2
// Place the face in the center of the scene
face.position = CGPointMake (CGRectGetMidX(view.frame),CGRectGetMidY(view.frame))
addChild(face)
}
func drawFace(radius:CGFloat) -> CGPathRef {
var path = UIBezierPath()
// Add the outline of the face
let center = CGPointZero
path.addCircle(center, radius: radius)
// Add the eyes
let eyeOffset = radius * 0.35
let eyeRadius = radius * 0.125
let left = CGPointMake(-eyeOffset, eyeOffset)
path.addCircle(left, radius: eyeRadius)
let right = CGPointMake(eyeOffset, eyeOffset)
path.addCircle(right, radius: eyeRadius)
// Add the mouth
let smileRadius = radius*0.65
path.addHalfCircle(center, radius: smileRadius)
return path.CGPath
}
}

Related

How can I animate the corner radius of a CALayer

I'm trying to create an animation where a CALayer rectangle changes from being flush with the borders of a view to having either the left, right, or both corners rounded and the width of the rectangle is also changed.
The problem I'm having is that the animation looks distorted. If I disable the corner radius change in the animation, the distortion doesn't happen. I suspect this is because when the original path and the final path in the animation have a different number of rounded corners, the paths have a different number of control points, so the path animation behavior is undefined as per the docs.
I'm thinking that I should try and find a way to get the number of control points in the rounded rect to be equal to the number in the non-rounded rect but I'm not sure how I would do this since I haven't found a way to count the number of control points in a CGPath/UIBezierPath.
Here's the code I'm using right now, where I'm animating the path, but I'm open to changing the implementation entirely to 'cheat' by having two rectangles or something like that.
func setSelection(to color: UIColor, connectedLeft: Bool, connectedRight: Bool, animated: Bool) {
self.connectedLeft = connectedLeft
self.connectedRight = connectedRight
self.color = color
if animated {
let pathAnimation = CABasicAnimation(keyPath: "path")
let colorAnimation = CABasicAnimation(keyPath: "fillColor")
self.configure(animation: pathAnimation)
self.configure(animation: colorAnimation)
pathAnimation.toValue = self.rectPath(connectedLeft: connectedLeft, connectedRight: connectedRight).cgPath
colorAnimation.toValue = color.cgColor
let group = CAAnimationGroup()
group.animations = [pathAnimation, colorAnimation]
self.configure(animation: group)
group.delegate = self
self.rectLayer.add(group, forKey: Constants.selectionChangeAnimationKey)
} else {
self.rectLayer.fillColor = color.cgColor
self.rectLayer.path = self.rectPath(connectedLeft: connectedLeft, connectedRight: connectedRight).cgPath
}
}
private func rectPath(connectedLeft: Bool, connectedRight: Bool) -> UIBezierPath {
let spacing: CGFloat = 5
var origin = self.bounds.origin
var size = self.bounds.size
var corners = UIRectCorner()
if !connectedLeft {
origin.x += spacing
size.width -= spacing
corners.formUnion([.topLeft, .bottomLeft])
}
if !connectedRight {
size.width -= spacing
corners.formUnion([.topRight, .bottomRight])
}
let path = UIBezierPath(roundedRect: .init(origin: origin, size: size), byRoundingCorners: corners, cornerRadii: .init(width: 8, height: 8))
print(path.cgPath)
return path
}
You are correct as to why your animation doesn't work correctly.
The Core Graphics/Core Animation methods that draw arcs compose the arcs using variable numbers of cubic bezier curves depending on the angle being drawn.
I believe that a 90 degree corner is rendered as a single cubic bezier curve.
Based on a recent test I did, it seems all you need to do is have the same number of endpoints as a Bezier curve in order to get the sharp-corner-to-rounded-corner animation to draw correctly. For a rounded corner, I'm pretty sure it draws a line segment up to the rounded corner, then the arc as one Bezier curve, then the next line segment. Thus you SHOULD be able to create a rectangle by drawing each sharp corner point twice. I haven't tried it, but I think it would work.
(Based on my understanding of how arcs of circles are drawn using bezier curve, an arc of a circle is composed of a cubic bezier curve for each quarter-circle or part of a quarter circle that is drawn. So for a 90 degree bend, you just need 2 control points for a sharp corner to replace a rounded corner. If the angle is >90 degrees but less than 180, you'd want 3 control points at the corner, and if it's >180 but less than 270, you'd want 4 control points.)
Edit:
The above approach of adding each corner-point twice for a rounded rectangle SHOULD work for the rounded rectangle case (where every corner is exactly 90 degrees) but it doesn't work for irregular polygons where the angle that needs to be rounded varies. I couldn't work out how to handle that case. I already created a demo project that generated irregular polygons with any mixture of sharp and curved corners, so I adapted it to handle animation.
The project now includes a function that generates CGPaths with a settable mixture of rounded and sharp corners that will animate the transition between sharp corners and rounded corners. The function you want to look at is called roundedRectPath(rect:byRoundingCorners:cornerRadius:)
Here is the function header and in-line documentation
/**
This function works like the UIBezierPath initializer `init(roundedRect:byRoundingCorners:cornerRadii:)` It returns a CGPath a rounded rectangle with a mixture of rounded and sharp corners, as specified by the options in `corners`.
Unlike the UIBezierPath `init(roundedRect:byRoundingCorners:cornerRadii:` intitializer, The CGPath that is returned by this function will animate smoothly from rounded to non-rounded corners.
- Parameter rect: The starting rectangle who's corners you wish to round
- Parameter corners: The corners to round
- Parameter cornerRadius: The corner radius to use
- Returns: A CGPath containing for the rounded rectangle.
*/
public func roundedRectPath(rect: CGRect, byRoundingCorners corners: UIRectCorner, cornerRadius: CGFloat) -> CGPath
You can download the project from Github and try it out at this link.
Here is a GIF of the demo app in operation:

Resize SKShapeNode frame, or alternative

This issue is tied with this: Set SKShapeNode frame to calculateAccumutedFrame
Is there a way to resize an SKShapeNode's frame. I do not want to scale it, (suggested here: Resizing a SKShapeNode). Using whatever scale function makes its contents shrink and resize aswell (the line width of 200x200 is way different than 50x50)
I need to take a frame and set it to a specific size. For example this:
shape.frame = CGRect(...)
Can't do that since frame is get-only. I attempted to override calculateAccumulatedFrame but that does nothing (see top link).
Can someone please suggest an alternative to the same functionality of SKShapeNode that is not halfway built... Or a way to actually change the frame. The basic functionality I need is to create an arc (seen in pictures of link on top) and have that in some sort of SKNode so I can resize, shrink it, etc. the same as you can do with SKSpriteNode
SKShapeNode is a node based on the Core Graphics path (CGPath).
So there is no direct method of resizings different from scaling as you want.
If you use a SKSpriteNode for example you have actions like resizeByWidth or resizeToWidth.
But about node like shapes you should think to him as you must resize a CGPath or UIBezierPath using method like :
apply(_ transform: CGAffineTransform)
or directly by rebuild your path as for example:
let π:CGFloat = CGFloat(M_PI)
let startAngle: CGFloat = 3 * π / 4
let endAngle: CGFloat = π / 4
myShape.path = UIBezierPath(arcCenter: CGPoint.zero, radius: 33.5, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
I would recommend making an SKSpriteNode out of your SKShapeNode because SKShapeNode is buggy.
let node = SKNode()
node.addChild(shapeNode) //we do this because of the bugs with SKShapeNode
let texture = view.textureFromNode(node, crop:node.calculateAccumulatedFrame)
let spriteNode = SKSpriteNode(texture:texture)
Optionally, I would look up ways to save this texture to disk, so that you do not have to render the shape every time.

How to create a ring with 3D effect using Sprite Kit?

I want to create a ring with a 3D effect using Sprite Kit. (SEE IMAGES)
I tried subclassing a SKNode and adding two nodes as children. (SEE CODE)
One node was a complete SKShapeNode ellipse, and the other was half ellipse using SKCropNode with a higher zPosition.
It looks good, but the SKCropNode increases the app CPU usage from 40% to 99%.
Any ideas on how to reduce the SKCropNode performance cost, or any alternative to create the same ring 3D effect?
class RingNode: SKNode {
let size: CGSize
init(size: CGSize, color: SKColor)
{
self.size = size
self.color = color
super.init()
ringPartsSetup()
}
private func ringPartsSetup() {
// LEFT PART (half ellipse)
let ellipseNodeLeft = getEllipseNode()
let leftMask = SKSpriteNode(texture: nil, color: SKColor.blackColor(), size: CGSize(
width: ellipseNodeLeft.frame.size.width/2,
height: ellipseNodeLeft.frame.size.height))
leftMask.anchorPoint = CGPoint(x: 0, y: 0.5)
leftMask.position = CGPoint(x: -ellipseNodeLeft.frame.size.width/2, y: 0)
let leftNode = SKCropNode()
leftNode.addChild(ellipseNodeLeft)
leftNode.maskNode = leftMask
leftNode.zPosition = 10 // Higher zPosition for 3D effect
leftNode.position = CGPoint(x: -leftNode.frame.size.width/4, y: 0)
addChild(leftNode)
// RIGHT PART (complete ellipse)
let rightNode = getEllipseNode()
rightNode.position = CGPoint(x: 0, y: 0)
rightNode.zPosition = 5
addChild(rightNode)
}
private func getEllipseNode() -> SKShapeNode {
let ellipseNode = SKShapeNode(ellipseOfSize: CGSize(
width: size.width,
height: size.height))
ellipseNode.strokeColor = SKColor.blackColor()
ellipseNode.lineWidth = 5
return ellipseNode
}
}
You've got the right idea with your two-layer approach and the half-slips on top. But instead of using a shape node inside a crop node, why not just use a shape node whose path is a half-ellipse? Create one using either CGPath or UIBezierPath API — use a circular arc with a transform to make it elliptical — then create your SKShapeNode from that path.
You may try converting your SKShapeNode to an SKSpriteNode. You can use SKView textureFromNode: (but we aware of issues with scale that require you to use it only after the node has been added to the view and at least one update cycle has run), or from scratch using an image (created programatically with a CGBitmapContext, of course).

Detect touch within Sprite texture and not the entire frame iOS Swift SpriteKit?

I am working with SpriteKit right now and I came across a problem that seems simple but I couldn't find anything on the internet. I have three buttons that are shaped like parallelograms stacked on top of each other and it looks like this:
Screenshot of buttons
let button = SKSpriteNode(imageNamed: "playbutton")
let leaderButton = SKSpriteNode(imageNamed: "leaderbutton")
let homeButton = SKSpriteNode(imageNamed: "homebutton")
button.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(button)
leaderButton.position = CGPoint(x: size.width/2, y: size.height/2 - button.size.height/2)
addChild(leaderButton)
homeButton.position = CGPoint(x: size.width/2, y: size.height/2 - button.size.height)
addChild(homeButton)
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if button.containsPoint(location) {
button.runAction(SKAction.scaleTo(0.8, duration: 0.1))
}
else if leaderButton.containsPoint(location) {
leaderButton.runAction(SKAction.scaleTo(0.8, duration: 0.1))
}
else if homeButton.containsPoint(location) {
homeButton.runAction(SKAction.scaleTo(0.8, duration: 0.1))
}
}
}
This is how I am detecting touches. The problem is that they overlap because the sprite is actually a rectangle so when i try and tap on the top left of the second button, the top button detects it. I was wondering of there is a way to detect touch only in the texture like how you can set the physics body to a texture.
Thanks for any help you can give me!
Link works now.
So I tried this:
button.position = CGPoint(x: size.width/2, y: size.height/2)
button.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "playbutton"), size: button.size)
button.physicsBody?.dynamic = false
addChild(button)
leaderButton.position = CGPoint(x: size.width/2, y: size.height/2 - button.size.height/2)
leaderButton.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "leaderbutton"), size: leaderButton.size)
leaderButton.physicsBody?.dynamic = false
addChild(leaderButton)
homeButton.position = CGPoint(x: size.width/2, y: size.height/2 - button.size.height)
homeButton.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "homebutton"), size: homeButton.size)
homeButton.physicsBody?.dynamic = false
addChild(homeButton)
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if physicsWorld.bodyAtPoint(location)?.node == button {
button.runAction(SKAction.scaleTo(0.8, duration: 0.1))
print("play")
}
if physicsWorld.bodyAtPoint(location)?.node == leaderButton {
leaderButton.runAction(SKAction.scaleTo(0.8, duration: 0.1))
print("leader")
}
if physicsWorld.bodyAtPoint(location)?.node == homeButton {
homeButton.runAction(SKAction.scaleTo(0.8, duration: 0.1))
}
}
}
It still registers the full frame and not just the physics body. See the link to see the buttons and how their coordinates intersect.
If you attach a physics body to each button, you can detect which physics body your touch lands on.
You can generate a physics body from the button's texture (assuming the button is an SKSpriteNode) using SKPhysicsBody(texture:size:) or SKPhysicsBody(texture:alphaThreshold:size:), or you can create a CGPath describing the button's shape and use SKPhysicsBody(polygonFromPath:). Assign the body to the button's physicsBody property. Assuming you don't actually want the physics simulator to move your buttons, set each body's dynamic property to false.
Then you can use physicsWorld.bodyAtPoint(_:) to get one of the bodies that a touch lands on (where physicsWorld is a property of your SKScene). Use the body's node property to get back to the button node. If bodies overlap, bodyAtPoint returns an arbitrary body.
You can use physicsWorld.enumerateBodiesAtPoint(_:usingBlock:) if you need all of the bodies that your touch lands on.
A completely different approach, if you can create a CGPath describing the button's shape, is to use SKScene.convertPointFromView(_:) and then SKNode.convertPoint(_:fromNode:_) to convert the point into the button's coordinate system, and then use CGPathContainsPoint (a global function) to detect whether the point is in the path describing the button shape.
(I can't see your imgur link, but I think I have a pretty good idea of what you're describing.)
There are some discussions on how to read the alpha value of out an SKTexture, but those require quite a bit of overhead for just "did I touch a parallelogram"? Especially if you add alpha effects to your buttons.
You can do whatever logic you want on the location - you have a CGPoint that contains the location, and you know the size and shape of your buttons.
It should be pretty straightforward to write a function to test whether a point is inside a parallelogram-shaped button. Since parallelograms are basically rectangles with triangles on each end of an "inner" rect, if you know the size of that "inner" rect, you can pretty easily determine whether the touch is where you want by:
Checking that it's in the rect of the entire button. If not, you know it's not in the parallelogram.
Checking to see if it's inside the "inner" rectangle - the part of the parallelogram with the triangles "chopped off" the ends. If so, then you know it's in the parallelogram.
Checking to see if it's inside one of the triangles. You know how far up and how far across the touch is in the rect that contains the triangle, and you know the slope of the line that divides that rect to make the triangle by virtue of its width and height. That makes it trivial to check whether the point is in the triangle or not - just use the "y=mx+b" formula for a line, where m is the slope and b is whatever you need to add to get the line to pass through the corner of the rect (typically your "y intercept"). If the y coordinate is less than/greater than m*(the x coordinate) + b, you can determine whether the touch is inside or outside that triangle.
Once you have that function, you can use it as the test for checking each button.

Create a line with two CGPoints SpriteKit Swift

I am trying to make a simple app where you touch a point, and a sprite follows a line through that point to the edge of the screen, no matter where you touch. I want to draw line segments connecting the origin of the sprite (point where it starts) and the point where you touched, and between the origin of the sprite and the end point at the edge of the screen, so I can visualize the path of the sprite and the relationship between the x and y offsets of the origin, touch point and end point.
Hopefully that was not too confusing.
TL;DR: I need to draw a line between two points and I don't know how to do that using SpriteKit Swift.
Thanks in advance.
This can be done using CGPath and SKShapeNode.
Lets start with CGPath. CGPath is used when we need to construct a path using series of shapes or lines. Paths are line connecting two points. So to make a line:
moveToPoint: It sets the current point of the path to the specified point.
addLineToPoint: It draws a straight line from the current point to the specified point.
or
addCurveToPoint: It draws a curved line from the current point to the specified point based on certain tangents and control points.
You can check the documentation here:
http://developer.apple.com/library/mac/#documentation/graphicsimaging/Reference/CGPath/Reference/reference.html
What you need to do is:
var path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, 100, 100)
CGPathAddLineToPoint(path, nil, 500, 500)
Now to make the path visible, and give it attributes like stroke color, line width etc. you create a SKShapeNode in SpriteKit and add the path to it.
let shape = SKShapeNode()
shape.path = path
shape.strokeColor = UIColor.whiteColor()
shape.lineWidth = 2
addChild(shape)
Hope this helps :).
Follow THIS tutorial step by step and you can achieve that.
Consider the below code:
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
let location = touches.anyObject()!.locationInNode(scene)
if let pig = movingPig {
pig.addMovingPoint(location)
}
}
This is a simple method. You get the next position of the user’s finger and if you found a pig in touchesBegan(_:,withEvent:), as indicated by a non-nil movingPig value, you add the position to this pig as the next waypoint.
So far, you can store a path for the pig—now let’s make the pig follow this path. Add the following code to update() inside GameScene.swift:
dt = currentTime - lastUpdateTime
lastUpdateTime = currentTime
enumerateChildNodesWithName("pig", usingBlock: {node, stop in
let pig = node as Pig
pig.move(self.dt)
})
And you can see result:
Drawing Lines:
At the moment, only the pig knows the path it wants to travel, but the scene also needs to know this path to draw it. The solution to this problem is a new method for your Pig class.
func createPathToMove() -> CGPathRef? {
//1
if wayPoints.count <= 1 {
return nil
}
//2
var ref = CGPathCreateMutable()
//3
for var i = 0; i < wayPoints.count; ++i {
let p = wayPoints[i]
//4
if i == 0 {
CGPathMoveToPoint(ref, nil, p.x, p.y)
} else {
CGPathAddLineToPoint(ref, nil, p.x, p.y)
}
}
return ref
}
this method to draw the pig’s path:
func drawLines() {
//1
enumerateChildNodesWithName("line", usingBlock: {node, stop in
node.removeFromParent()
})
//2
enumerateChildNodesWithName("pig", usingBlock: {node, stop in
//3
let pig = node as Pig
if let path = pig.createPathToMove() {
let shapeNode = SKShapeNode()
shapeNode.path = path
shapeNode.name = "line"
shapeNode.strokeColor = UIColor.grayColor()
shapeNode.lineWidth = 2
shapeNode.zPosition = 1
self.addChild(shapeNode)
}
})
}
And here is your result:
And you can set that path for the pig.
You can modify that as per your need.
Hope it will help.
For Swift 3, the CGPathMoveToPoint method doesn't like a nil for the second argument anymore, so I needed a new solution. Here's what I came up with:
let line_path:CGMutablePath = CGMutablePath()
line_path.move(to: CGPoint(x:x1, y:y1))
line_path.addLine(to: CGPoint(x:x2, y:y2))
For the sake of simplicity, I pulled everything necessary to draw a line into an extension of SKShapeNode that allows you to create a line with a start & end point, as well as a strokeColor and strokeWidth (you could always preset these or make default values should you choose too)
extension SKShapeNode {
convenience init(start: CGPoint,
end: CGPoint,
strokeColor: UIColor,
lineWidth: CGFloat) {
self.init()
let path = CGMutablePath()
path.move(to: start)
path.addLine(to: end)
self.path = path
self.strokeColor = strokeColor
self.lineWidth = lineWidth
}
}
The basic idea is that it will create a CGMutablePath with the provided points and assign it to the shape node for drawing a line.
To call it:
let horizontalLine = SKShapeNode(start: CGPoint(x: 0, y: 50),
end: CGPoint(x: size.width, y: 50),
strokeColor: .orange,
lineWidth: 2.0)
addChild(horizontalLine)
And the output:

Resources