Resize SKShapeNode frame, or alternative - ios

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.

Related

Drawing an arc incorrectly leaves flat edges

I am having an issue with the arc drawing of a UIBezierPath on IOS.
Whilst I get the semblance of an arc, as can be seen from the image, the top, and two sides have flat edges.
The code I am using looks like this:
let path2 = UIBezierPath()
path2.addArc(withCenter: arcCentre, radius: radius, startAngle: arcStart, endAngle: arcEnd, clockwise: false)
path2.addLine(to: finalPoint)
path2.close()
color.set()
path2.fill()
The produced arc is shown in black on the image below.
Any clues as to why this is so, and how to get a perfectly rounded image?

Custom iOS UIButton shape

I'm trying to create a semi circle button as per wireframe.
But it's not turning out right.
This is the code I've written in C#:
PORCalculatorButton.Layer.CornerRadius = PORCalculatorButton.Layer.Bounds.Width / 2;
PORCalculatorButton.ClipsToBounds = true;
PORCalculatorButton.Layer.MaskedCorners = (CoreAnimation.CACornerMask)3;
There are layout constraints on the button too.
Can anyone tell me where I've gone wrong or if there's a better way?
I'll accept any answers in ObjC, Swift or C#
Thanks.
Try the below Code:
let circlePath = UIBezierPath.init(arcCenter:
CGPointMake(PORCalculatorButton.bounds.size.width / 2, PORCalculatorButton.bounds.size.height), radius: PORCalculatorButton.bounds.size.height, startAngle: 0.0, endAngle: CGFloat(M_PI), clockwise: false)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.CGPath
PORCalculatorButton.layer.mask = circleShape
Because of layout constraints, your button will resize according to your outer views. You need to make sure to apply the layer properties, after your button is rendered, to ensure you have the final frame/bounds, and also re-apply the same properties if the button size changes (say due to rotation etc)
You are trying to clip/mask the button vertically, but you are using width, not height, to calculate corner radius. Is this the intended implementation?

How to transform UIBezierPath/CGPath within a reference frame?

I need to scale a path, but instead of using the path's bounds, the transform should be performed using a larger, enclosing, arbitrary rect. (i.e. a "canvas".)
My problem is how to do that without drawing the canvas.
For example, in the following image:
the blue square indicates the native rect of the path (a rocket in this case).
the purple rect indicates the bounds of the "canvas" or parent coordinate system.
the purple rect is for positioning only; not to be drawn. The whitespace is similarly ignored.
I need to scale the canvas, and keep the transformed rocket path (for clipping operations, caching etc.). i.e. I need to lose the purple rect after transformation.
Ideally, I'd use something like the following fiction:
var canvasPath = UIBezierPath(rect: canvasRect)
canvasPath.append(rocketPath, withName: "rocket")
canvasPath.doSomeTransformOrOther()
let transformedRocketPath = canvasPath.path(forName: "rocket")
Sadly, the magical append(:withName:) and path(forName:) methods do not exist, even if UIBezierPath or CGPath treated subpaths like this. Alas!
Another approach could be to temporarily add points to the path, one for each canvas corner. Then, transform the path. Then, walk the path with CGPath's (hideous) apply(info:function:) method, rebuild the path without the canvas points, then draw the result. (Yeah, pass the vodka.)
Can anyone offer a more sane solution? Thanks for any helpi
(For those interested: this is related to a problem of how to center irregularly-shaped paths. The purple rect represents the bounds of the minimum enclosing circle. Drawing the rocket with the MEC's coordinate system will make the rocket appear more visually centered, compared to using boundingBox.)
More info: Fitting UIBezierPath/CGPath to circle
Here's my final approach to the problem: use a "canvas" rect that will be scaled along with the path. Then use the canvas rect when translating the (scaled) path. This keeps the path relative to the canvas
An example of doing this is getting a path from an SVG, and then scaling, positioning the path relative not to the path's bounds, but relative to the native dimensions (the 'viewbox') of the SVG. This in turn allows relative positioning of the path within the SVG canvas.
extension UIBezierPath
{
func scaled(usingCanvasSize canvasSize: CGSize,
scale: CGFloat) -> CGRect
{
let canvasRect = CGRect(origin: .zero, size: canvasSize)
let canvasPath = UIBezierPath(rect: canvasRect)
let scaleTransform = CGAffineTransform(scaleX: scale, y: scale)
canvasPath.apply(scaleTransform)
self.apply(scaleTransform)
return canvas.bounds
}
}
// Usage:
let nativeSVGSize: CGSize = ... // 'viewBox' size of SVG
let path = UIBezierPath(...) // From SVG, wherever.
let scale: CGFloat = 0.5
// Scale the path, get a "canvas" rect.
let canvasRect = path.scaled(usingCanvasSize: nativeSVGSize,
scale: scale)
// Use the (scaled) canvas for info about the path from now on.
// e.g. move the path using the center of the canvasRect.
let pathCenter = canvasRect.center
let destCenter = ...
let movement = CGPoint(x: destCenter.x - pathCenter.x,
y: destCenter.y - pathCenter.y)
path.apply(CGAffineTransform(translationX: movement.x,
y: movement.y))

Resize sprite without shrinking contents

I have created a big circle with a UIBezierPath and turned it into a Sprite using this,
let path = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: CGFloat(226), startAngle: 0.0, 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.position = center
shape.strokeColor = UIColor(red:0.98, green:0.99, blue:0.99, alpha:1.00)
let trackViewTexture = self.view!.texture(from: shape, crop: outerPath.bounds)
let trackViewSprite = SKSpriteNode(texture: trackViewTexture)
trackViewSprite.physicsBody = SKPhysicsBody(edgeChainFrom: innerPath.cgPath)
self.addChild(trackViewSprite)
It works fine. It creates the circle perfectly. But I need to resize it using
SKAction.resize(byWidth: -43, height: -43, duration: 0.3)
Which will make it a bit smaller. But, when it resizes the 20 line width I set now is very small because of the aspect fill. So when I shink it looks something like this:
But I need it to shrink like this-- keeping the 20 line width:
How would I do this?
Don't know if this would affect anything, but the sprites are rotating with an SKAction forever
-- EDIT --
Now, how do I use this method to scale to a specific size? Like turn 226x226 to 183x183?
Since by scaling down the circle, not only its radius gets scaled but its line's width too, you need to set a new lineWidth proportional with the scale. For example, when scaling the circle down by 2, you will need to double the lineWidth.
This can be done in two ways:
Setting the lineWidth in the completion block of the run(SKAction, completion: #escaping () -> Void) method. However this will result in seeing the line shrinking while the animation is running, then jumping to its new width once the animation finishes. If your animation is short, this may not be easy to observe tough.
Running a parallel animation together with the scaling one, which constantly adjusts the lineWidth. For this, you can use SKAction's customAction method.
Here is an example for your case:
let scale = CGFloat(0.5)
let finalLineWidth = initialLineWidth / scale
let animationDuration = 1.0
let scaleAction = SKAction.scale(by: scale, duration: animationDuration)
let lineWidthAction = SKAction.customAction(withDuration: animationDuration) { (shapeNode, time) in
if let shape = shapeNode as? SKShapeNode {
let progress = time / CGFloat(animationDuration)
shape.lineWidth = initialLineWidth + progress * (finalLineWidth - initialLineWidth)
}
}
let group = SKAction.group([scaleAction, lineWidthAction])
shape.run(group)
In this example, your shape will be scaled by 0.5, therefore in case of an initial line width of 10, the final width will be 20. First we create a scaleAction with a specified duration, then a custom action which will update the line's width every time its actionBlock is called, by using the progress of the animation to make the line's width look like it's not changing. At the end we group the two actions so they will run in parallel once you call run.
As a hint, you don't need to use Bezier paths to create circles, there is a init(circleOfRadius: CGFloat) initializer for SKShapeNode which creates a circle for you.

How to draw a face to SKShapeNode

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
}
}

Resources