how to create a circle with rounded ends for each quadrant
Asked Answered
P

2

6

I have created a circle that is divided into quadrants. I am trying to make the ends of each quadrant rounded with a given gap between each quadrant, but I get some strange behavior. I guess I'm missing something. Below is an image of what I'm getting and the relevant code. I want the ends to be similar to that of the Apple Watch activity ring.

enter image description here

class GameScene: SKScene {

let radius = CGFloat(100)
var topRightPathNode : SKShapeNode!
var bottomRightPathNode : SKShapeNode!
var bottomLeftPathNode : SKShapeNode!
var topLeftPathNode : SKShapeNode!

override func didMove(to view: SKView) {

}


override init(size: CGSize) {
    super.init(size: size)
    let topRightPath = arcSegment(center: CGPoint.zero, radius: radius, strokeWidth: 18, gapWidth: 6)

    // TOP RIGHT


    topRightPathNode = SKShapeNode(path: topRightPath)
    topRightPathNode.fillColor = SKColor.white
    topRightPathNode.lineWidth = 0
    topRightPathNode.position = CGPoint(x: 320, y: 240)
    addChild(topRightPathNode)


    // BOTTOM RIGHT

    var reflectOnY = CGAffineTransform(scaleX: 1.0, y: -1.0)
    let bottomRightPath = topRightPath.copy(using: &reflectOnY)!
    bottomRightPathNode = SKShapeNode(path: bottomRightPath)
    bottomRightPathNode.fillColor = SKColor.red
    bottomRightPathNode.lineWidth = 0
    bottomRightPathNode.position = CGPoint(x: 320, y: 240)
   addChild(bottomRightPathNode)


    // BOTTOM LEFT


    //var reflectOnX = CGAffineTransform(scaleX: -1.0, y: 1.0)
    //let bottomLeftPath = bottomRightPath.copy(using: &reflectOnX)!
    var reflectOnXAndY = CGAffineTransform(scaleX: -1.0, y: -1.0)
    let bottomLeftPath = topRightPath.copy(using: &reflectOnXAndY)!

    bottomLeftPathNode = SKShapeNode(path: bottomLeftPath)
    bottomLeftPathNode.fillColor = SKColor.purple
    bottomLeftPathNode.lineWidth = 0
    bottomLeftPathNode.position = CGPoint(x: 320, y: 240)
    addChild(bottomLeftPathNode)



    // TOP LEFT
    var reflectOnX = CGAffineTransform(scaleX: -1.0, y: 1.0)
    let topLeftPath = topRightPath.copy(using: &reflectOnX)!
    topLeftPathNode = SKShapeNode(path: topLeftPath)
    topLeftPathNode.fillColor = SKColor.cyan
    topLeftPathNode.lineWidth = 0
    topLeftPathNode.position = CGPoint(x: 320, y:240)
    addChild(topLeftPathNode)

}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}


func arcSegment(center : CGPoint,
                radius: CGFloat,
                strokeWidth: CGFloat,
                gapWidth: CGFloat) -> CGPath
{
    let halfStrokeWidth = strokeWidth / 2.0
    let outerRadius = radius + halfStrokeWidth
    let innerRadius = radius - halfStrokeWidth
    let halfGap = gapWidth / 2.0

    let outerStartAngle = CGFloat(atan2(sqrt(outerRadius * outerRadius - halfGap * halfGap), halfGap))
    let outerEndAngle = CGFloat(atan2(halfGap, sqrt(outerRadius * outerRadius - halfGap * halfGap)))

    let innerStartAngle = CGFloat(atan2(halfGap, sqrt(innerRadius * innerRadius - halfGap * halfGap)))
    let innerEndAngle = CGFloat(atan2(sqrt(innerRadius * innerRadius - halfGap * halfGap), halfGap))

    let path = CGMutablePath()

    path.addArc(center: center, radius: outerRadius, startAngle: outerStartAngle, endAngle: outerEndAngle, clockwise: true)
    // Quartz 2D will assume a "moveTo" here
    path.addArc(center: CGPoint(x: center.x + radius, y: center.y), radius: halfStrokeWidth, startAngle: outerStartAngle, endAngle: outerEndAngle, clockwise: false)

    path.addArc(center: center, radius: innerRadius, startAngle: innerStartAngle, endAngle: innerEndAngle, clockwise: false)

    path.closeSubpath()


    return path
}
Polydeuces answered 19/5, 2017 at 5:51 Comment(4)
It should be easier using thick lines, and setting its lineCap to round. To achieve segments with different line caps on its end, you should separate the segment in two parts, and draw over the incorrect end.Suggest
@macmoonshine I don't really get how I should separate the segment in two parts, and draw over the incorrect endPolydeuces
Example for the white segment: Draw an arc from -45° to 3° with rounded line caps, and then the arc from -87° to 45° with butt ends. The latter drawing should cover the rounded cap on 45° of the first arc.Suggest
Correction: ... -45° to -3° ...Suggest
T
1

Here's another set of code where the paths that are generated have rounded ends, but each arc segment is a filled path rather than being a single stroked segment. This should allow the paths to have collisions without the end caps overlapping.

import UIKit
import SpriteKit
import PlaygroundSupport

let radius = CGFloat(100)

let sceneSize = CGSize(width: 640, height: 480)
let sceneView = SKView(frame: CGRect(origin: CGPoint.zero, size: sceneSize))

let scene = SKScene(size: sceneSize)
scene.backgroundColor = UIColor.black

let topRightPath = arcSegment(radius: radius, strokeWidth: 18, gapWidth: 25)

let topRightPathNode = SKShapeNode(path: topRightPath)
topRightPathNode.fillColor = SKColor.white
topRightPathNode.position = CGPoint(x: 320, y: 240)
topRightPathNode.lineWidth = 0
scene.addChild(topRightPathNode)


var reflectOnY = CGAffineTransform(scaleX: 1.0, y: -1.0)
let bottomRightPath = topRightPath.copy(using: &reflectOnY)!
let bottomRightPathNode = SKShapeNode(path: bottomRightPath)
bottomRightPathNode.fillColor = SKColor.orange
bottomRightPathNode.position = CGPoint(x: 320, y: 240)
bottomRightPathNode.lineWidth = 0
scene.addChild(bottomRightPathNode)


var reflectOnX = CGAffineTransform(scaleX: -1.0, y: 1.0)
let bottomLeftPath = bottomRightPath.copy(using: &reflectOnX)!
let bottomLeftPathNode = SKShapeNode(path: bottomLeftPath)
bottomLeftPathNode.fillColor = SKColor.green
bottomLeftPathNode.position = CGPoint(x: 320, y: 240)
bottomLeftPathNode.lineWidth = 0
scene.addChild(bottomLeftPathNode)


let topLeftPath = bottomLeftPath.copy(using: &reflectOnY)!
let topLeftPathNode = SKShapeNode(path: topLeftPath)
topLeftPathNode.fillColor = SKColor.blue
topLeftPathNode.position = CGPoint(x: 320, y:240)
topLeftPathNode.lineWidth = 0
scene.addChild(topLeftPathNode)

sceneView.presentScene(scene)

PlaygroundPage.current.liveView = sceneView
PlaygroundPage.current.needsIndefiniteExecution = true

func arcSegment( radius: CGFloat,
                 strokeWidth: CGFloat,
                 gapWidth: CGFloat) -> CGPath
{
    let halfStrokeWidth = strokeWidth / 2.0
    let outerRadius = radius + halfStrokeWidth
    let innerRadius = radius - halfStrokeWidth
    let halfGap = gapWidth / 2.0

    let outerStartAngle = CGFloat(atan2(sqrt(outerRadius * outerRadius - halfGap * halfGap), halfGap))
    let outerEndAngle = CGFloat(atan2(halfGap, sqrt(outerRadius * outerRadius - halfGap * halfGap)))

    let innerStartAngle = CGFloat(atan2(halfGap, sqrt(innerRadius * innerRadius - halfGap * halfGap)))
    let innerEndAngle = CGFloat(atan2(sqrt(innerRadius * innerRadius - halfGap * halfGap), halfGap))

    let leftEndAngle = CGFloat(atan2(sqrt(radius * radius - halfGap * halfGap), halfGap))

    let leftEndCapPoint = CGPoint(x: radius * cos(leftEndAngle),
                                  y: radius * sin(leftEndAngle))
    let rightEndCapPoint = CGPoint(x: leftEndCapPoint.y,
                                   y: leftEndCapPoint.x)


    let path = CGMutablePath()

    path.addArc(center: CGPoint.zero,
                radius: outerRadius,
                startAngle: outerStartAngle,
                endAngle: outerEndAngle,
                clockwise: true)

    path.addArc(center: rightEndCapPoint,
                radius: halfStrokeWidth,
                startAngle : 0,
                endAngle : CGFloat.pi,
                clockwise: true)

    path.addArc(center: CGPoint.zero, radius: innerRadius, startAngle: innerStartAngle, endAngle: innerEndAngle, clockwise: false)

    path.addArc(center: leftEndCapPoint,
                radius: halfStrokeWidth,
                startAngle : 3.0 * CGFloat.pi / 2.0,
                endAngle : CGFloat.pi / 2.0,
                clockwise: true)

    path.closeSubpath()

    return path
}
Turtledove answered 24/5, 2017 at 1:35 Comment(0)
T
2

All the trigonometry I put into answering your first question was necessary to get the flat ends on the curves that you wanted, aligned to the axes. Now that you've changed and want rounded end caps on the arcs you can go back to your original attempt and just rely on Quartz 2D to use rounded end cap strokes. The code is much simpler again and can be:

import UIKit
import SpriteKit
import PlaygroundSupport

let radius = CGFloat(100)

let sceneSize = CGSize(width: 640, height: 480)
let sceneView = SKView(frame: CGRect(origin: CGPoint.zero, size: sceneSize))

let scene = SKScene(size: sceneSize)
scene.backgroundColor = UIColor.black

let topRightPath = arcSegment(radius: radius, gapWidth: 25)

let topRightPathNode = SKShapeNode(path: topRightPath)
topRightPathNode.strokeColor = SKColor.white
topRightPathNode.lineWidth = 18
topRightPathNode.lineCap = .round
topRightPathNode.position = CGPoint(x: 320, y: 240)
scene.addChild(topRightPathNode)


var reflectOnY = CGAffineTransform(scaleX: 1.0, y: -1.0)
let bottomRightPath = topRightPath.copy(using: &reflectOnY)!
let bottomRightPathNode = SKShapeNode(path: bottomRightPath)
bottomRightPathNode.strokeColor = SKColor.orange
bottomRightPathNode.lineWidth = 18
bottomRightPathNode.lineCap = .round
bottomRightPathNode.position = CGPoint(x: 320, y: 240)
scene.addChild(bottomRightPathNode)


var reflectOnX = CGAffineTransform(scaleX: -1.0, y: 1.0)
let bottomLeftPath = bottomRightPath.copy(using: &reflectOnX)!
let bottomLeftPathNode = SKShapeNode(path: bottomLeftPath)
bottomLeftPathNode.strokeColor = SKColor.green
bottomLeftPathNode.lineWidth = 18
bottomLeftPathNode.lineCap = .round
bottomLeftPathNode.position = CGPoint(x: 320, y: 240)
scene.addChild(bottomLeftPathNode)


let topLeftPath = bottomLeftPath.copy(using: &reflectOnY)!
let topLeftPathNode = SKShapeNode(path: topLeftPath)
topLeftPathNode.strokeColor = SKColor.blue
topLeftPathNode.lineWidth = 18
topLeftPathNode.lineCap = .round
topLeftPathNode.position = CGPoint(x: 320, y:240)
scene.addChild(topLeftPathNode)

sceneView.presentScene(scene)

PlaygroundPage.current.liveView = sceneView
PlaygroundPage.current.needsIndefiniteExecution = true

func arcSegment( radius: CGFloat,
                 gapWidth: CGFloat) -> CGPath
{
    let halfGap = gapWidth / 2.0
    let path = CGMutablePath()
    let startAngle = CGFloat(atan2(sqrt(radius * radius - halfGap * halfGap), halfGap))
    let endAngle = CGFloat(atan2(halfGap, sqrt(radius * radius - halfGap * halfGap)))

    path.addArc( center: CGPoint.zero,
                radius: radius,
                startAngle: startAngle,
                endAngle: endAngle,
                clockwise: true)

    return path
}
Turtledove answered 20/5, 2017 at 0:38 Comment(5)
the rounded corners are working as aspected, just that when I set collision contacts with between the quadrant and other objects, they overlap a bit before contact/collision is recognized. I believe that this could be because of the lineWidth, since I didn't have the collision issue with the code you provided in my related question. How could I resolve this?Polydeuces
I am still trying to find a way round getting getting the collisions/contacts not to overlapPolydeuces
yeah, I realized the overlapping issueWisla
I realized a while ago that a physics body created from a shape node usually overlaps for a stroke with lineWidth but doesn't for a fill. You can change show physics to true to get an idea of what i'm referring to. Personally, I think you'd need to change your approach if contacts and collisions are very significant to your implementation.Agostino
I can change my code so that it goes back to the prior technique (from an answer in another question) that uses only a fill to get the same figure, but I do not have the time to work that code up at the moment.Turtledove
T
1

Here's another set of code where the paths that are generated have rounded ends, but each arc segment is a filled path rather than being a single stroked segment. This should allow the paths to have collisions without the end caps overlapping.

import UIKit
import SpriteKit
import PlaygroundSupport

let radius = CGFloat(100)

let sceneSize = CGSize(width: 640, height: 480)
let sceneView = SKView(frame: CGRect(origin: CGPoint.zero, size: sceneSize))

let scene = SKScene(size: sceneSize)
scene.backgroundColor = UIColor.black

let topRightPath = arcSegment(radius: radius, strokeWidth: 18, gapWidth: 25)

let topRightPathNode = SKShapeNode(path: topRightPath)
topRightPathNode.fillColor = SKColor.white
topRightPathNode.position = CGPoint(x: 320, y: 240)
topRightPathNode.lineWidth = 0
scene.addChild(topRightPathNode)


var reflectOnY = CGAffineTransform(scaleX: 1.0, y: -1.0)
let bottomRightPath = topRightPath.copy(using: &reflectOnY)!
let bottomRightPathNode = SKShapeNode(path: bottomRightPath)
bottomRightPathNode.fillColor = SKColor.orange
bottomRightPathNode.position = CGPoint(x: 320, y: 240)
bottomRightPathNode.lineWidth = 0
scene.addChild(bottomRightPathNode)


var reflectOnX = CGAffineTransform(scaleX: -1.0, y: 1.0)
let bottomLeftPath = bottomRightPath.copy(using: &reflectOnX)!
let bottomLeftPathNode = SKShapeNode(path: bottomLeftPath)
bottomLeftPathNode.fillColor = SKColor.green
bottomLeftPathNode.position = CGPoint(x: 320, y: 240)
bottomLeftPathNode.lineWidth = 0
scene.addChild(bottomLeftPathNode)


let topLeftPath = bottomLeftPath.copy(using: &reflectOnY)!
let topLeftPathNode = SKShapeNode(path: topLeftPath)
topLeftPathNode.fillColor = SKColor.blue
topLeftPathNode.position = CGPoint(x: 320, y:240)
topLeftPathNode.lineWidth = 0
scene.addChild(topLeftPathNode)

sceneView.presentScene(scene)

PlaygroundPage.current.liveView = sceneView
PlaygroundPage.current.needsIndefiniteExecution = true

func arcSegment( radius: CGFloat,
                 strokeWidth: CGFloat,
                 gapWidth: CGFloat) -> CGPath
{
    let halfStrokeWidth = strokeWidth / 2.0
    let outerRadius = radius + halfStrokeWidth
    let innerRadius = radius - halfStrokeWidth
    let halfGap = gapWidth / 2.0

    let outerStartAngle = CGFloat(atan2(sqrt(outerRadius * outerRadius - halfGap * halfGap), halfGap))
    let outerEndAngle = CGFloat(atan2(halfGap, sqrt(outerRadius * outerRadius - halfGap * halfGap)))

    let innerStartAngle = CGFloat(atan2(halfGap, sqrt(innerRadius * innerRadius - halfGap * halfGap)))
    let innerEndAngle = CGFloat(atan2(sqrt(innerRadius * innerRadius - halfGap * halfGap), halfGap))

    let leftEndAngle = CGFloat(atan2(sqrt(radius * radius - halfGap * halfGap), halfGap))

    let leftEndCapPoint = CGPoint(x: radius * cos(leftEndAngle),
                                  y: radius * sin(leftEndAngle))
    let rightEndCapPoint = CGPoint(x: leftEndCapPoint.y,
                                   y: leftEndCapPoint.x)


    let path = CGMutablePath()

    path.addArc(center: CGPoint.zero,
                radius: outerRadius,
                startAngle: outerStartAngle,
                endAngle: outerEndAngle,
                clockwise: true)

    path.addArc(center: rightEndCapPoint,
                radius: halfStrokeWidth,
                startAngle : 0,
                endAngle : CGFloat.pi,
                clockwise: true)

    path.addArc(center: CGPoint.zero, radius: innerRadius, startAngle: innerStartAngle, endAngle: innerEndAngle, clockwise: false)

    path.addArc(center: leftEndCapPoint,
                radius: halfStrokeWidth,
                startAngle : 3.0 * CGFloat.pi / 2.0,
                endAngle : CGFloat.pi / 2.0,
                clockwise: true)

    path.closeSubpath()

    return path
}
Turtledove answered 24/5, 2017 at 1:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.