how to create a circle with rounded ends for each

2019-02-17 10:32发布

问题:

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.

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
}

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


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