Tibor Udvari
Tibor Udvari

Reputation: 3012

SpriteKit: Square to Circle animation using SKShapeNode UIBezierPath rounded corners

I'm trying to smoothly animate a square into a circle in SpriteKit.

I'm creating the SKShape with a UIBezierPath using rounded corners. Then, I vary the corner radii to animate.

My problem is that I seem to have a jump in the animation, please see the gif below. Preferably using the rounded corners technique, how can I get it to be smooth?

"Jumpy" problem

Bug with animating circle to square

    let shape = SKShapeNode()
    let l: CGFloat = 100.0
    shape.path = UIBezierPath(roundedRect: CGRect(x: -l/2, y: -l/2, width: l, height: l), byRoundingCorners: [.topLeft, .bottomLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: 0, height: 0)).cgPath
    shape.position = CGPoint(x: frame.midX, y: frame.midY)
    shape.fillColor = .white
    addChild(shape)

    let action = SKAction.customAction(withDuration: 1) { (node, t) in
    let shapeNode = node as! SKShapeNode
    shapeNode.path = UIBezierPath(roundedRect: CGRect(x: -l/2, y: -l/2, width: l, height: l), byRoundingCorners: [.topLeft, .bottomLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: t * l / 2, height: 0)).cgPath
    }
    shape.run(SKAction.repeatForever(action))

Animation debugging

To debug I have created some shapes with progressively larger corner radii as you can see below. The numbers represent the ratio of the corner radii to the length of the square. As you can see there is a jump between 0.3 and 0.35. I can't see what I'm missing.

Progressive incrementation

    let cols = 10
    let rows = 1

    let l: Double = 30.0
    let max: Double = l / 2
    let delta: Double = l * 2

    for i in 0..<rows * cols {
        let s = SKShapeNode()
        let c: Double = Double(i % cols)
        let r: Double = floor(Double(i) / Double(cols))
        let pct: Double = Double(i) / (Double(rows) * Double(cols))
        let rad = pct * max
        s.path = UIBezierPath(roundedRect: CGRect(x: -l/2, y: -l/2, width: l, height: l), byRoundingCorners: [.topRight, .bottomRight, .topLeft, .bottomLeft], cornerRadii: CGSize(width: pct * max, height: pct * max)).cgPath
        s.position = CGPoint(x: c * delta - Double(cols) / 2.0 * delta, y: r * delta - Double(rows) / 2.0 * delta)
        s.lineWidth = 1.5
        s.strokeColor = .white
        addChild(s)

        let t = SKLabelNode(text: String(format:"%0.2f", rad / l))
        t.verticalAlignmentMode = .center
        t.horizontalAlignmentMode = .center
        t.fontName = "SanFrancisco-Bold"
        t.fontSize = 15

        t.position = CGPoint(x: 0, y: -delta * 0.66)
        s.addChild(t)
    }

Upvotes: 1

Views: 594

Answers (2)

magohamote
magohamote

Reputation: 1484

The documentation of UIBezierPath.init(roundedRect:cornerRadius:) states that:

The radius of each corner oval. A value of 0 results in a rectangle without rounded corners. Values larger than half the rectangle’s width or height are clamped appropriately to half the width or height.

But I tested this and it is actually happening exactly for values larger than a third of the width/height and not at a half of it. I'm filling a bug report and will let's hope it is quickly solved.

Bug report link: https://bugs.swift.org/browse/SR-10496

Upvotes: 0

E.Coms
E.Coms

Reputation: 11531

You may not find the answer using a current api. But you can draw it by yourself.

  let duration = 10.0
    let action = SKAction.customAction(withDuration: duration) { (node, t) in
      let shapeNode = node as! SKShapeNode
        let path = CGMutablePath()
        let borderRadius = l/2 * t  / CGFloat(duration);
      path.move(to: CGPoint.init(x: -l/2, y:  -l/2 + borderRadius));
      path.addLine(to: CGPoint.init(x:  -l/2, y: l/2 - borderRadius));
      path.addArc(tangent1End: CGPoint(x: -l/2, y: l/2), tangent2End: CGPoint(x: -l/2 + borderRadius, y: l/2), radius: borderRadius)
      path.addLine(to: CGPoint.init(x:  l/2 - borderRadius, y: l/2 ));
      path.addArc(tangent1End: CGPoint(x: l/2, y: l/2), tangent2End: CGPoint(x: l/2, y: l/2 - borderRadius), radius: borderRadius)
      path.addLine(to: CGPoint.init(x:  l/2, y: -l/2 + borderRadius));
      path.addArc(tangent1End: CGPoint(x: l/2, y: -l/2), tangent2End: CGPoint(x: l/2 - borderRadius, y: -l/2), radius: borderRadius)
      path.addLine(to: CGPoint.init(x:  -l/2 + borderRadius, y: -l/2));
      path.addArc(tangent1End: CGPoint(x: -l/2, y: -l/2), tangent2End: CGPoint(x: -l/2, y: -l/2 + borderRadius), radius: borderRadius)
      path.closeSubpath()
      shapeNode.path = path
    }

Working solution

Upvotes: 3

Related Questions