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
}
Related
I was making a progress circle, and I want its track path to have a blur effect, is there any way to achieve that?
This is what the original track looks like(the track path is transparent, I want it to be blurred)
And this is my attempt
let outPutViewFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let circleRadius: CGFloat = 60
let circleViewCenter = CGPoint(x: outPutViewFrame.width / 2 , y: outPutViewFrame.height / 2)
let circleView = UIView()
let progressWidth: CGFloat = 8
circleView.frame.size = CGSize(width: (circleRadius + progressWidth) * 2, height: (circleRadius + progressWidth) * 2)
circleView.center = circleViewCenter
let circleTrackPath = UIBezierPath(arcCenter: CGPoint(x: circleView.frame.width / 2, y: circleView.frame.height / 2), radius: circleRadius, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
let blur = UIBlurEffect(style: .light)
let blurEffect = UIVisualEffectView(effect: blur)
blurEffect.frame = circleView.bounds
blurEffect.mask(withPath: circleTrackPath, inverse: false)
extension UIView {
func mask(withPath path: UIBezierPath, inverse: Bool = false) {
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
maskLayer.path = path.cgPath
maskLayer.lineWidth = 5
maskLayer.lineCap = CAShapeLayerLineCap.round
self.layer.mask = maskLayer
}
}
Set maskLayer.fillRule to evenOdd, even when not inversed.
if inverse {
path.append(UIBezierPath(rect: self.bounds))
}
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
create the circleTrackPath by using a big circle and a smaller circle.
let circleCenter = CGPoint(x: circleView.frame.width / 2, y: circleView.frame.height / 2)
let circleTrackPath = UIBezierPath(ovalIn:
CGRect(origin: circleCenter, size: .zero)
.insetBy(dx: circleRadius, dy: circleRadius))
// smaller circle
circleTrackPath.append(CGRect(origin: circleCenter, size: .zero)
.insetBy(dx: circleRadius * 0.8, dy: circleRadius * 0.8))
Set circleTrackPath.usesEvenOddFillRule to true:
circleTrackPath.usesEvenOddFillRule = true
Now you have a blurred full circle. The non-blurred arc part can be implemented as another sublayer.
Here is a MCVE that you can paste into a playground:
let container = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
// change this to a view of your choice
let image = UIImageView(image: UIImage(named: "my_image"))
let blur = UIVisualEffectView(effect: UIBlurEffect(style: .light))
container.addSubview(image)
blur.frame = image.frame
container.addSubview(blur)
let outer = image.bounds.insetBy(dx: 30, dy: 30)
let path = UIBezierPath(ovalIn: outer)
path.usesEvenOddFillRule = true
path.append(UIBezierPath(ovalIn: outer.insetBy(dx: 10, dy: 10)))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
maskLayer.fillRule = .evenOdd
blur.layer.mask = maskLayer
container // <--- playground quick look this
Using my profile pic as the background, this produces:
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()
}
}
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)
I am creating an app where different color balls are falling and you have to catch them with a multicolored circle by rotating the circle so that the color of that part of the circle matches the ball. One issue I am having is that I do not know how to make a physics body for only part of a circle rather then a full circle. What would be the best way to accomplish this. Thank you, any help is appreciated.
func dropBall() {
let ball = SKShapeNode(circleOfRadius: 40)
ball.fillColor = colors[Int(arc4random_uniform(UInt32(level)))]
ball.strokeColor = ball.fillColor
ball.position = CGPoint(x: 0, y: self.size.height / 2.1)
addChild(ball)
let ballBody = SKPhysicsBody(circleOfRadius: 30)
ballBody.mass = 1.5
ballBody.categoryBitMask = PhysicsCategory.ball
ballBody.collisionBitMask = 4
ball.physicsBody = ballBody
mostRecentBallColor = ball.fillColor
}
func addCircleObstacle() {
var degreesSectionAngle = 360 / (level)
var radiansSectionAngle = Double(degreesSectionAngle) * (M_PI / 180.0)
var secondXCoordinate = cos(radiansSectionAngle) * 40
var secondYCoordinate = sin(radiansSectionAngle) * 40
let path = UIBezierPath()
path.addArc(withCenter: CGPoint.zero,
radius: 160,
startAngle: CGFloat(radiansSectionAngle),
endAngle: CGFloat(0),
clockwise: false)
path.addArc(withCenter: CGPoint.zero,
radius: 200,
startAngle: CGFloat(0.0),
endAngle: CGFloat(radiansSectionAngle),
clockwise: true)
circleball = obstacleByDuplicatingPath(path, clockwise: true)
obstacles.append(circleball)
circleball.position = CGPoint(x: 0, y: self.size.height / -6)
addChild(circleball)
print(level)
print(degreesSectionAngle)
print(radiansSectionAngle)
circleBody.isDynamic = false
circleBody.categoryBitMask = PhysicsCategory.Edge
circleball.physicsBody = circleBody
}
func obstacleByDuplicatingPath(_ path: UIBezierPath, clockwise: Bool) -> SKNode {
let container = SKNode()
var degreesSectionAngle = 360 / level
var radiansSectionAngle = Double(degreesSectionAngle) * (M_PI / 180.0)
var rotationFactor = CGFloat(radiansSectionAngle)
if !clockwise {
rotationFactor *= -1
}
for i in 0...level - 1 {
if i == mostRecentBallColor {
}
section = SKShapeNode(path: path.cgPath)
section.fillColor = colors[i]
section.strokeColor = colors[i]
section.zRotation = rotationFactor * CGFloat(i);
let sectionBody = SKPhysicsBody(polygonFrom: path.cgPath)
sectionBody.categoryBitMask = PhysicsCategory.Obstacle
sectionBody.collisionBitMask = 0
sectionBody.contactTestBitMask = PhysicsCategory.ball
sectionBody.affectedByGravity = false
section.physicsBody = sectionBody
container.addChild(section)
}
return container
}
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
}