Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to create a circle with rounded ends for each quadrant

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
}
like image 610
Hilarious404 Avatar asked May 19 '17 05:05

Hilarious404


1 Answers

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
}
like image 113
Scott Thompson Avatar answered Sep 27 '22 17:09

Scott Thompson