draw an arc from endpoints of line and fill in - swift

I did the following:
let xOrigin = (pointsOfPerformance[POP.gluteusMaximus.index].x_origin / 375) * self.view.bounds.width
let yOrigin = (pointsOfPerformance[POP.gluteusMaximus.index].y_origin / 666.6666) * (self.view.bounds.width * 16 / 9)
let p0 = CGPoint(x: xOrigin, y: yOrigin)
let xDest = (pointsOfPerformance[POP.upperBack.index].x_origin / 375) * self.view.bounds.width
let yDest = (pointsOfPerformance[POP.upperBack.index].y_origin / 666.6666) * (self.view.bounds.width * 16 / 9)
let p1 = CGPoint(x: xDest, y: yDest)
let midPoint = CGPoint(x: (xOrigin + xDest) / 2, y:
(yOrigin + yDest) / 2)
let path = UIBezierPath()
path.move(to: p0)
path.addLine(to: p1)
path.addArc(withCenter: p2, radius: 94, startAngle: 200, endAngle: 130, clockwise: true)
let centerOfCircle = makePoint(xOffset: p2.x, yOffset: p2.y)
view.addSubview(centerOfCircle)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.animationRed.cgColor
shapeLayer.strokeColor = UIColor.animationRed.cgColor
shapeLayer.lineWidth = 0.05
view.layer.addSublayer(shapeLayer)
How do I get the circle to only show up as a slice of its curve that comes off the line on the back. It's unclear how I manipulate the function to get only the part coming off the line on the back visible.

This works on MacOS:
class CustomView: NSView {
override func draw(_ rect: CGRect) {
let bkgrnd = NSBezierPath(rect: rect)
let fillColor = NSColor.white
fillColor.set()
bkgrnd.fill()
let center = NSMakePoint(rect.size.width/2,rect.size.height/2)
let radius:CGFloat = rect.size.width/4
let path1 = NSBezierPath()
path1.appendArc(withCenter: center, radius: radius, startAngle: 90, endAngle: 270)
NSColor.gray.set()
path1.fill()
let path2 = NSBezierPath()
path2.appendArc(withCenter: center, radius: radius, startAngle: 270, endAngle: 90)
NSColor.blue.set()
path2.fill()
}
}

Demonstrates getting a small slice of the periphery of a circle for MacOS:
class CustomView: NSView {
override func draw(_ rect: CGRect ) {
let bkgrnd = NSBezierPath(rect: rect)
let fillColor = NSColor.white
fillColor.set()
bkgrnd.fill()
let center = NSMakePoint(rect.size.width/2,rect.size.height/2)
let radius:CGFloat = rect.size.width/4
let circle = NSBezierPath()
circle.appendArc(withCenter: center, radius: radius, startAngle: 0, endAngle: 360)
NSColor.gray.set()
circle.fill()
let arc = NSBezierPath()
arc.appendArc(withCenter: center, radius: radius, startAngle: 120, endAngle: 200)
NSColor.blue.set()
arc.fill()
}
}

Related

Rotating CAShapeLayer with user touch & drag event and CAShapeLayer's center point is nailed

Here is my demo:
As, you can see, the tip of the line is not rotated. This what I want to achieve:
How I draw the original line: When I tap on the screen, I got a CGPoint and draw everything base on that
func drawWholeRuler(originalPoint:CGPoint){ //Draw a whole ruler with every components
endPoint = CGPoint(x: originalPoint.x - 70, y: originalPoint.y)
startPoint = CGPoint(x: originalPoint.x + 70, y: originalPoint.y)
dotStartPointX = CGPoint(x: startPoint!.x, y: startPoint!.y - dotLineSize)
dotStartPointY = CGPoint(x: startPoint!.x, y: startPoint!.y + dotLineSize)
dotEndPointX = CGPoint(x: endPoint!.x, y: endPoint!.y - dotLineSize)
dotEndPointY = CGPoint(x: endPoint!.x, y: endPoint!.y + dotLineSize)
let path = drawLineFromPoint(start: startPoint!, toPoint: endPoint!, ofColor: fillColor, inView: self)
let dotStart = drawLineFromPoint(start: dotStartPointX!, toPoint: dotStartPointY!, ofColor: fillColor, inView: self)
let dotEnd = drawLineFromPoint(start: dotEndPointX!, toPoint: dotEndPointY!, ofColor: fillColor, inView: self)
let circleStart = drawCircle(point: startPoint!)
let circleEnd = drawCircle(point: endPoint!)
let newPath:bezierPathStruct = bezierPathStruct(startPoint: startPoint!, endPoint: endPoint!, dotStartPointX: dotStartPointX!, dotStartPointY: dotStartPointY!, dotEndPointX: dotEndPointX!, dotEndPointY: dotEndPointY!, path: path, dotStart: dotStart, dotEnd: dotEnd, circleStart: circleStart, circleEnd:circleEnd)
bezierPathArray.append(newPath)
}
Now is how I handle longPress and extend the line, which is actually just delete the tapped line and redraw everything separately (middle line, 2 vertical lines, 2 bubbles) when touch moves
#objc func handleLongPress(recognizer: UIGestureRecognizer) {
var startPointOfTouchedRuler:CGPoint = .zero
let zeroPoint:CGPoint = .zero
let currentPanPoint = longTapRecognizer.location(in: self)
// print("here",currentPanPoint)
if let sublayers = self.layer.sublayers as? [CAShapeLayer]{ //get all CAShape and stored as an array
for layer in sublayers{ // go through each CAShape
if let path = layer.path, path.contains(currentPanPoint) { // if there is a path at that point then return, else create a path
startPointOfTouchedRuler = detectWhichRuler(layer: layer)
if startPointOfTouchedRuler != zeroPoint{
// drawCircle(point: startPointOfTouchedRuler)
break
}else{
}
}
}
}
let linePath = UIBezierPath()
var circlePath = UIBezierPath()
var circlePath2 = UIBezierPath()
let verticalLinePath1 = UIBezierPath()
let verticalLinePath2 = UIBezierPath()
switch longTapRecognizer.state {
case .began:
tapGestureStartPoint = startPointOfTouchedRuler
if tapGestureStartPoint == zeroPoint {return}
self.layer.addSublayer(lineShape)
self.layer.addSublayer(shapeLayer1)
self.layer.addSublayer(shapeLayer2)
self.layer.addSublayer(verticalLineShape)
self.layer.addSublayer(verticalLineShape2)
shapeLayer1.anchorPoint = startPointOfTouchedRuler
verticalLinePath1.move(to: CGPoint(x: startPointOfTouchedRuler.x, y: startPointOfTouchedRuler.y - dotLineSize ))
verticalLinePath1.addLine(to: CGPoint(x: startPointOfTouchedRuler.x , y: startPointOfTouchedRuler.y + dotLineSize))
verticalLinePath2.move(to: CGPoint(x: currentPanPoint.x, y: currentPanPoint.y - dotLineSize))
verticalLinePath2.addLine(to: CGPoint(x: currentPanPoint.x, y: currentPanPoint.y + dotLineSize))
verticalLineShape.path = verticalLinePath1.cgPath
verticalLineShape2.path = verticalLinePath2.cgPath
linePath.move(to: tapGestureStartPoint)
linePath.addLine(to: currentPanPoint)
circlePath = UIBezierPath(arcCenter: currentPanPoint, radius: 15.0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
circlePath2 = UIBezierPath(arcCenter: startPointOfTouchedRuler, radius: 15.0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
shapeLayer2.path = circlePath2.cgPath
shapeLayer1.path = circlePath.cgPath
lineShape.path = linePath.cgPath
case .changed:
//
verticalLinePath2.move(to: CGPoint(x: currentPanPoint.x , y: currentPanPoint.y - dotLineSize))
verticalLinePath2.addLine(to: CGPoint(x: currentPanPoint.x, y: currentPanPoint.y + dotLineSize))
verticalLineShape2.path = verticalLinePath2.cgPath
linePath.move(to: tapGestureStartPoint)
linePath.addLine(to: currentPanPoint)
circlePath = UIBezierPath(arcCenter: currentPanPoint, radius: 15.0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
shapeLayer1.path = circlePath.cgPath
circlePath.move(to: tapGestureStartPoint)
lineShape.path = linePath.cgPath
case .ended:
verticalLineShape.path = nil
verticalLineShape2.path = nil
verticalLineShape2.removeFromSuperlayer()
verticalLineShape.removeFromSuperlayer()
shapeLayer2.path = nil
shapeLayer2.removeFromSuperlayer()
lineShape.path = nil
shapeLayer1.path = nil
lineShape.removeFromSuperlayer()
shapeLayer1.removeFromSuperlayer()
if tapGestureStartPoint == zeroPoint {return}
extendALine(startPoint: tapGestureStartPoint, currentPoint: currentPanPoint)
shouldDeleteRuler = true
default: print("default")
break
}
}
Now what I want is to rotate 2 lines at the edge when user hold and move finger. I tried CATransform3DRotate but it doesn't change anything.
I also tried
verticalLineShape.anchorPoint = startPointOfTouchedRuler
let degrees = 90.0
let radians = CGFloat(degrees * .pi / 180)
verticalLineShape.transform = CATransform3DMakeRotation(radians, 0.0, 0.0, 1.0)
verticalLineShape.anchorPoint = startPointOfTouchedRuler
But verticalLineShape disappeared instead of rotating.
So how is the best way to approach this?
I figured out, instead of rotating CAShapeLayer, I just need too rotate UIBezierPath before verticalLineShape.path = verticalLinePath1.cgPath
What I did:
extension UIBezierPath {
func rotate(path:UIBezierPath, degree:CGFloat){
let bounds:CGRect = path.cgPath.boundingBox
let center:CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let radians:CGFloat = (degree/180 * .pi)
var transform:CGAffineTransform = CGAffineTransform.identity
transform = transform.translatedBy(x: center.x, y: center.y);
transform = transform.rotated(by: radians);
transform = transform.translatedBy(x: -center.x, y: -center.y);
path.apply(transform)
}
}
use
verticalLinePath1.rotate(path: verticalLinePath1, degree: 30)

How to call UIBezier outline in Swift Playground?

EDIT: Sorry, I wasn't clear originally. I want to get the "outline" path of a line or shape. I'm specifically trying to understand how to use:
context.replacePathWithStrokedPath()
and / or:
CGPathRef CGPathCreateCopyByStrokingPath(CGPathRef path, const CGAffineTransform *transform, CGFloat lineWidth, CGLineCap lineCap, CGLineJoin lineJoin, CGFloat miterLimit);
https://developer.apple.com/documentation/coregraphics/1411128-cgpathcreatecopybystrokingpath?language=objc
I'm not looking for workarounds, thanks.
=====
I'm really trying to wrap my head around drawing a line with an outline around it. i'm using UIBezier, but running into brick walls. So far, I've got this:
import UIKit
import PlaygroundSupport
let screenWidth = 375.0 // points
let screenHeight = 467.0 // points
let centerX = screenWidth / 2.0
let centerY = screenHeight / 2.0
let screenCenterCoordinate = CGPoint(x: centerX, y: centerY)
class LineDrawingView: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
path.lineWidth = 5
path.lineCapStyle = .round
//Move to Drawing Point
path.move(to: CGPoint(x:20, y:120))
path.addLine(to: CGPoint(x:200, y:120))
path.stroke()
let dot = UIBezierPath()
dot.lineWidth = 1
dot.lineCapStyle = .round
dot.move(to: CGPoint(x:200, y:120))
dot.addArc(withCenter: CGPoint(x:200, y:120), radius: 5, startAngle: CGFloat(0.0), endAngle: CGFloat(8.0), clockwise: true)
UIColor.orange.setStroke()
UIColor.orange.setFill()
path.stroke()
dot.fill()
let myStrokedPath = UIBezierPath.copy(path)
myStrokedPath().stroke()
}
}
let tView = LineDrawingView(frame: CGRect(x: 0,y: 0, width: screenWidth, height: screenHeight))
tView.backgroundColor = UIColor.white
PlaygroundPage.current.liveView = tView
So, where am I going wrong in this? I cannot seem to figure out where to use CGPathCreateCopyByStrokingPath...or how...
EDIT 2:
Ok, now I've got this. Closer, but how do I fill the path again?
let c = UIGraphicsGetCurrentContext()!
c.setLineWidth(15.0)
let clipPath = UIBezierPath(arcCenter: CGPoint(x:centerX,y:centerY), radius: 90.0, startAngle: -0.5 * .pi, endAngle: 1.0 * .pi, clockwise: true).cgPath
c.addPath(clipPath)
c.saveGState()
c.replacePathWithStrokedPath()
c.setLineWidth(0.2)
c.setStrokeColor(UIColor.black.cgColor)
c.strokePath()
The class was modified slightly to produce this graphic:
The path was not copied in the modified code. Instead the existing path was used to draw, then modified and reused. The dot did not have a stroke so that was added. Since only closed paths can be filled, I drew a thinner path on top of a thicker path by changing the line width.
This is the modified code:
class LineDrawingView: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
path.lineWidth = 7
path.lineCapStyle = .round
//Move to Drawing Point
path.move(to: CGPoint(x:20, y:120))
path.addLine(to: CGPoint(x:200, y:120))
path.stroke()
let dot = UIBezierPath()
dot.lineWidth = 1
dot.lineCapStyle = .round
dot.move(to: CGPoint(x:200, y:120))
dot.addArc(withCenter: CGPoint(x:200, y:120), radius: 5, startAngle: CGFloat(0.0), endAngle: CGFloat(8.0), clockwise: true)
dot.stroke()
UIColor.orange.setStroke()
UIColor.orange.setFill()
path.lineWidth = 5
path.stroke()
dot.fill()
}
}
So, I've found the (an) answer. I used CAShapeLayer:
let c = UIGraphicsGetCurrentContext()!
c.setLineCap(.round)
c.setLineWidth(15.0)
c.addArc(center: CGPoint(x:centerX,y:centerY), radius: 90.0, startAngle: -0.5 * .pi, endAngle: (-0.5 * .pi) + (3 / 2 * .pi ), clockwise: false)
c.replacePathWithStrokedPath()
let shape = CAShapeLayer()
shape.path = c.path
shape.fillColor = UIColor.yellow.cgColor
shape.strokeColor = UIColor.darkGray.cgColor
shape.lineWidth = 1
myView.layer.addSublayer(shape)
It works well enough, but not on overlapping layers. I need to learn how to connect contours or something.

Swift Draw Custom Circle [duplicate]

This question already has answers here:
Animate CAShapeLayer path change
(1 answer)
Circular Progress Bars in IOS
(7 answers)
Closed 4 years ago.
I am trying to implement a custom circular analysis view.
The view should be circular but cut-off.
Goal:
My Code:
let circlePath = UIBezierPath(ovalIn: CGRect(x: innerRect.minX, y: innerRect.minY, width: innerRect.width, height: innerRect.height))
if trackBackgroundColor != UIColor.clear {
trackBackgroundColor.setFill()
circlePath.fill();
}
if trackBorderWidth > 0 {
circlePath.lineWidth = trackBorderWidth
trackBorderColor.setStroke()
circlePath.stroke()
}
// progress Drawing
let progressPath = UIBezierPath()
let progressRect: CGRect = CGRect(x: innerRect.minX, y: innerRect.minY, width: innerRect.width, height: innerRect.height)
let center = CGPoint(x: progressRect.midX, y: progressRect.midY)
let radius = progressRect.width / 2.0
let startAngle:CGFloat = clockwise ? CGFloat(-internalProgress * Double.pi / 180.0) : CGFloat(constants.twoSeventyDegrees * Double.pi / 180)
let endAngle:CGFloat = clockwise ? CGFloat(constants.twoSeventyDegrees * Double.pi / 180) : CGFloat(-internalProgress * Double.pi / 180.0)
progressPath.addArc(withCenter: center, radius:radius, startAngle:startAngle, endAngle:endAngle, clockwise:!clockwise)
Current Output:
How do I draw the custom circle described as Goal.
Here's a rough implementation of what you need. This draws the two arcs.
class CircularProgressView: UIView {
var trackBackgroundColor = UIColor.lightGray
var trackBorderWidth: CGFloat = 10
var progressColor = UIColor.red
var percent: Double = 0 {
didSet {
setNeedsDisplay()
}
}
// Adjust these to meet your needs. 90 degrees is the bottom of the circle
static let startDegrees: CGFloat = 120
static let endDegrees: CGFloat = 60
override func draw(_ rect: CGRect) {
let startAngle: CGFloat = radians(of: CircularProgressView.startDegrees)
let endAngle: CGFloat = radians(of: CircularProgressView.endDegrees)
let progressAngle = radians(of: CircularProgressView.startDegrees + (360 - CircularProgressView.startDegrees + CircularProgressView.endDegrees) * CGFloat(max(0.0, min(percent, 1.0))))
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(center.x, center.y) - trackBorderWidth / 2 - 10
print(startAngle, endAngle, progressAngle)
let trackPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let progressPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: progressAngle, clockwise: true)
trackPath.lineWidth = trackBorderWidth
trackPath.lineCapStyle = .round
progressPath.lineWidth = trackBorderWidth
progressPath.lineCapStyle = .round
trackBackgroundColor.set()
trackPath.stroke()
progressColor.set()
progressPath.stroke()
}
private func radians(of degrees: CGFloat) -> CGFloat {
return degrees / 180 * .pi
}
}
let progress = CircularProgressView(frame: CGRect(x: 0, y: 0, width: 500, height: 400))
progress.backgroundColor = .white
progress.percent = 0.95

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

How to reduce the size each circle quadrant to produce gaps

Below is an image of my circle quadrants followed by the code. How can I reduce each arc by say, 10 points/pixels on each side so that there are gaps between the arcs? At first I was reducing them by CGFloat(Double.pi/36) on each side but that left didn't produce a straight gap, but a portion just sliced out, which is not what I want. I need each arc to have straight edges (horizontal and vertical) not diagonal.
class GameScene: SKScene, SKPhysicsContactDelegate {
var topRightPathNode : SKShapeNode!
var bottomRightPathNode : SKShapeNode!
var bottomLeftPathNode : SKShapeNode!
var topLeftPathNode : SKShapeNode!
override init(size: CGSize) {
super.init(size: size)
// TOP RIGHT
let topRightBezierPath = UIBezierPath(arcCenter: CGPoint(x: 0, y:0), radius: 100, startAngle: CGFloat(Double.pi/2), endAngle: 0, clockwise: false)
topRightPathNode = SKShapeNode(path: topRightBezierPath.cgPath)
topRightPathNode.strokeColor = SKColor.white
topRightPathNode.lineWidth = 18
topRightPathNode.position = CGPoint(x: frame.midX, y:frame.midY)
addChild(topRightPathNode)
// BOTTOM RIGHT
let bottomRightBezierPath = UIBezierPath(arcCenter: CGPoint(x: 0, y:0), radius: 100, startAngle: 0 , endAngle: 3*CGFloat(Double.pi/2), clockwise: false)
bottomRightPathNode = SKShapeNode(path: bottomRightBezierPath.cgPath)
bottomRightPathNode.strokeColor = SKColor.orange
bottomRightPathNode.lineWidth = 18
bottomRightPathNode.position = CGPoint(x: frame.midX, y:frame.midY)
addChild(bottomRightPathNode)
// BOTTOM LEFT
let bottomLeftBezierPath = UIBezierPath(arcCenter: CGPoint(x: 0, y:0), radius: 100, startAngle: 3*CGFloat(Double.pi/2), endAngle: 2*CGFloat(Double.pi/2), clockwise: false)
bottomLeftPathNode = SKShapeNode(path: bottomLeftBezierPath.cgPath)
bottomLeftPathNode.strokeColor = SKColor.green
bottomLeftPathNode.lineWidth = 18
bottomLeftPathNode.position = CGPoint(x: frame.midX, y:frame.midY)
addChild(bottomLeftPathNode)
// TOP LEFT
let topLeftBezierPath = UIBezierPath(arcCenter: CGPoint(x: 0, y:0), radius: 100, startAngle: 2*CGFloat(Double.pi/2), endAngle: CGFloat(Double.pi/2), clockwise: false)
topLeftPathNode = SKShapeNode(path: topLeftBezierPath.cgPath)
topLeftPathNode.strokeColor = SKColor.blue
topLeftPathNode.lineWidth = 18
topLeftPathNode.position = CGPoint(x: frame.midX, y:frame.midY)
addChild(topLeftPathNode)
}
}
Below is some code that when pasted into a iOS Playground generates a picture that I think matches your description.
In order to get the sides to remain parallel to the axes in the place where there are gaps, you have to do a little math to figure out what the points are. Then you have to draw the outline of the shape you want instead of relying on the stroke width added by the drawing system. The math is not too complicated if you're familiar with Trigonometry, but your question suggested that you might be OK.
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(center: CGPoint.zero, radius: radius, strokeWidth: 18, gapWidth: 18)
let topRightPathNode = SKShapeNode(path: topRightPath)
topRightPathNode.fillColor = SKColor.white
topRightPathNode.lineWidth = 0
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.fillColor = SKColor.orange
bottomRightPathNode.lineWidth = 0
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.fillColor = SKColor.green
bottomLeftPathNode.lineWidth = 0
bottomLeftPathNode.position = CGPoint(x: 320, y: 240)
scene.addChild(bottomLeftPathNode)
let topLeftPath = bottomLeftPath.copy(using: &reflectOnY)!
let topLeftPathNode = SKShapeNode(path: topLeftPath)
topLeftPathNode.fillColor = SKColor.blue
topLeftPathNode.lineWidth = 0
topLeftPathNode.position = CGPoint(x: 320, y:240)
scene.addChild(topLeftPathNode)
sceneView.presentScene(scene)
PlaygroundPage.current.liveView = sceneView
PlaygroundPage.current.needsIndefiniteExecution = true
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: center, radius: innerRadius, startAngle: innerStartAngle, endAngle: innerEndAngle, clockwise: false)
path.closeSubpath()
return path
}