In SceneKit is there a way not to fill UIBezierPath? - ios

I am trying to draw circle in a sceneView but it fills the inside.
Also is there a way to increase segment number it looks like a polygon.
In case you wonder why I use UIBezierPath to draw circle, I want to change its end angle dynamically according to some parameters.
let r:CGFloat = 5
let center = CGPoint(x: 0, y: 0)
let path = UIBezierPath(arcCenter:center,
radius: r,
startAngle: CGFloat(0),
endAngle:CGFloat(CGFloat.pi * 2),
clockwise: true)
path.lineWidth = 1
UIColor.clear.setFill()
path.fill()
UIColor.white.setStroke()
path.stroke()
let shape = SCNShape(path: path, extrusionDepth: 0)
shape.firstMaterial?.diffuse.contents = UIColor.white
let shapeNode = SCNNode(geometry: shape)
shapeNode.position = SCNVector3(0, 0, 0)
rootNode.addChildNode(shapeNode)

Related

Semi circle between 2 CGPoints

I want to draw a semi circle between 2 points on a circle. The main represents a clock and i want to draw another line to represent a progress from one hour to another so the points position may vary. First of all i know the X and Y of the 2 points i am interested in. This is how i try to add angles in UIBezierPath. My problem is that the new circle starts correctly but ends at a totally random location
let firstAngle = atan2(redPoint.y - circleCenter.y, redPoint.x - circleCenter.x)
let secondAngle = atan2(bluePoint.y - circleCenter.y, bluePoint.x - circleCenter.x) ```
let circlePath1 = UIBezierPath(arcCenter: circleCenter,
radius: circleRadius,
startAngle: firstAngle,
endAngle: secondAngle,
clockwise: true) ```
Wherever i set the redPoint, the circle starts at a correct location but the circle never ands at bluePoint.
I have tried your code and it works for me if points are actually on that circumference
let redPoint = CGPoint(x: 100.0, y: 200.0)
let bluePoint = CGPoint(x: 100.0, y: 0.0)
let circleCenter = CGPoint(x: 100.0, y: 100.0)
let circleRadius = CGFloat(100.0)
let firstAngle = atan2(redPoint.y - circleCenter.y, redPoint.x - circleCenter.x)
let secondAngle = atan2(bluePoint.y - circleCenter.y, bluePoint.x - circleCenter.x)
let path = UIBezierPath(arcCenter: circleCenter,
radius: circleRadius,
startAngle: firstAngle,
endAngle: secondAngle,
clockwise: true)
//Path in layer
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
self.view.layer.addSublayer(shapeLayer)
or for example:
let redPoint = CGPoint(x: 200.0, y: 100.0)
let bluePoint = CGPoint(x: 100.0, y: 0.0)
let circleCenter = CGPoint(x: 100.0, y: 100.0)
let circleRadius = CGFloat(100.0)
You get:
But if you use coordinates that are not actually on your circumference, you get wrong results. My suggestion is to check your inputs and eventually if points are belonging to your desired circumference or not.
You're doing this backwards. Don't try to get the angle by starting with the location of the little red and blue filled circles. Use the angle to place the little red and blue filled circles.
In that example, my angles are -1 and 2.8 (radians). First I draw the arc (just as you did); then I superimpose the circles, which is trivial because I know where their centers are (the endpoints of the arc) by converting polar to cartesian coordinates.

Drawing a wedge in ARKit using SceneKit

I am trying to draw a wedge of a circle using UIBezierPath, but when drawing it it shows a triangle and instead of drawing a curve through my points, it draws a straight line.
I am constructing the SCNNode Like so:
let path = UiBezierPath()
path.move(to: CGPoint.zero)
path.addArc(withCenter: CGPoint.zero, radius: 0.5, startAngle: 0, endAngle: .pi/2, clockwise: true)
path.close()
let shape = SCNShape(path: path, extrusionDepth: 0.1)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.orange
shape.materials = [mat]
let node = SCNNode(geometry: shape)
When I position this node in the world, and add it to the scene it draws a triangle. How can I make it so there is a curve instead of just a straight line?
Setting the flatness of the path to 0 fixed this issue:
path.flatness = 0

UiScrollview panning slow

I have uiscrollview that contains image view called m; I added about 100 of subview on m with no problem, but when I drawn a path and shape layer of each subview, panning became so slow.
Why is that? Is there alternative or solution?
EDIT: Part of the code, m is the imageview inside scrollview and objects are subview, I call this function to draw around paths over around 200 subviews
for objectData in self.objectDatas
{
let object = UIImageView(frame: CGRect(x:line.point1X, y:line.point1Y, width: 200, height: 200))
object.tag = objectData.ID
object.isUserInteractionEnabled = true
object.backgroundColor = UIColor.blue
let mask = CAShapeLayer()
mask.frame = object.layer.bounds
path.move(to: CGPoint(x: x1-diff, y: y1+diff))
let path = CGMutablePath()
let diff = 5
let x1 = 0 // regarding the object, not 'm' object
let y1 = 0 // regarding the object, not 'm' object
let x2 = objectData.point2X - objectData.point1X
let y2 = objectData.point2Y - objectData.point1Y
path.addLine(to: CGPoint(x: x2-diff, y: y2+diff))
path.addArc(center: CGPoint(x: x2, y: y2), radius:CGFloat(diff) , startAngle: -.pi, endAngle: 0, clockwise: true)
path.addLine(to: CGPoint(x: x1+diff, y: y1-diff))
path.addArc(center: CGPoint(x: x1, y: y1), radius:CGFloat(diff) , startAngle: 0, endAngle: -.pi, clockwise: true)
path.addLine(to: CGPoint(x: x1-diff, y: y1+diff))
mask.path = path
object.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = object.bounds
shape.path = path
shape.lineWidth = 5
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.red.cgColor
shape.borderColor = UIColor.green.cgColor
object.layer.insertSublayer(shape, at: 0)
let tgr = UITapGestureRecognizer(target: self, action: #selector("action"))
tgr.delegate = self
object.addGestureRecognizer(tgr)
self.m.addObject(object: object)
}

How to calculate the "center" point of a curved SKSpriteNode?

I'm initializing and displaying an SKSpriteNode via the texture of an SKShapeNode with a certain path that represents a portion of a circle. Below is an example of a shape I'm generating using a playground:
import SpriteKit
import PlaygroundSupport
let view = SKView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
let scene = SKScene(size: view.frame.size)
view.presentScene(scene)
let radius: CGFloat = view.frame.width * 0.40
var length: CGFloat = (.pi * 2.0) / 8.0
var width: CGFloat = radius / 5.0
var path: CGPath {
let path = UIBezierPath(arcCenter: view.center, radius: radius, startAngle: 0.0, endAngle: length, clockwise: true)
path.addArc(withCenter: view.center, radius: radius - width, startAngle: length, endAngle: 0.0, clockwise: false)
path.close()
return path.cgPath
}
let shapeNode = SKShapeNode(path: path)
shapeNode.fillColor = .white
let texture = view.texture(from: shapeNode)!
let spriteNode = SKSpriteNode(texture: texture, color: .white, size: texture.size())
spriteNode.position = CGPoint(x: shapeNode.frame.midX, y: shapeNode.frame.midY)
scene.addChild(spriteNode)
PlaygroundPage.current.liveView = view
This generates this shape:
I'd like to add a small child SKSpriteNode that is centered within the width of the curved node and at the middle arc point like so:
The closest I could come is using the spriteNode's position:
let childNode = SKSpriteNode(color: .red, size: CGSize(width: 5, height: 5))
childNode.position = spriteNode.position
scene.addChild(childNode)
This produces something close, but it's using the frame, which is entirely wrong:
I'm assuming I need to do something with the path of the arc but I'm unsure how. Could someone point me in the right direction?
Thanks to the comments I avoided using the path and refreshed on some basic trig to solve:
let x = (radius - width/2) * cos(length/2) + view.center.x
let y = (radius - width/2) * sin(length/2) + view.center.y
childNode.position = CGPoint(x: x, y: y)

arcs donut chart with CAShapelayer - border of underlaying layers are visible

I draw a donut chart with CAShapeLayers arcs. I draw it by putting one on top of another and the problem that underneath layers edges are visible.
code of drawing is following
for (index, item) in values.enumerated() {
var currentValue = previousValue + item.value
previousValue = currentValue
if index == values.count - 1 {
currentValue = 100
}
let layer = CAShapeLayer()
let path = UIBezierPath()
let separatorLayer = CAShapeLayer()
let separatorPath = UIBezierPath()
let radius: CGFloat = self.frame.width / 2 - lineWidth / 2
let center: CGPoint = CGPoint(x: self.bounds.width / 2, y: self.bounds.width / 2)
separatorPath.addArc(withCenter: center, radius: radius, startAngle: percentToRadians(percent: -25), endAngle: percentToRadians(percent: CGFloat(currentValue - 25 + 0.2)), clockwise: true)
separatorLayer.path = separatorPath.cgPath
separatorLayer.fillColor = UIColor.clear.cgColor
separatorLayer.strokeColor = UIColor.white.cgColor
separatorLayer.lineWidth = lineWidth
separatorLayer.contentsScale = UIScreen.main.scale
self.layer.addSublayer(separatorLayer)
separatorLayer.add(createGraphAnimation(), forKey: nil)
separatorLayer.zPosition = -(CGFloat)(index)
path.addArc(withCenter: center, radius: radius, startAngle: percentToRadians(percent: -25), endAngle: percentToRadians(percent: CGFloat(currentValue - 25)), clockwise: true)
layer.path = path.cgPath
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = item.color.cgColor
layer.lineWidth = lineWidth
layer.contentsScale = UIScreen.main.scale
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
layer.allowsEdgeAntialiasing = true
separatorLayer.addSublayer(layer)
layer.add(createGraphAnimation(), forKey: nil)
layer.zPosition = -(CGFloat)(index)
What am I doing wrong ?
UPD
Tried code
let mask = CAShapeLayer()
mask.frame = CGRect(x: 0, y: 0, width: radius * 2, height: radius * 2)
mask.fillColor = nil
mask.strokeColor = UIColor.white.cgColor
mask.lineWidth = lineWidth * 2
let maskPath = CGMutablePath()
maskPath.addArc(center: CGPoint(x: self.radius, y: self.radius), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
maskPath.closeSubpath()
mask.path = maskPath
self.layer.mask = mask
but it masks only inner edges, outer still has fringe
The fringe you're seeing happens because you're drawing exactly the same shape in the same position twice, and alpha compositing (as commonly implemented) is not designed to handle that. Porter and Duff's paper, “Compositing Digital Images”, which introduced alpha compositing, discusses the problem:
We must remember that our basic assumption about the
division of subpixel areas by geometric objects breaks
down in the face of input pictures with correlated mattes.
When one picture appears twice in a compositing expression,
we must take care with our computations of F A and
F B. Those listed in the table are correct only for uncorrelated
pictures.
When it says “matte”, it basically means transparency. When it says “uncorrelated pictures”, it means two pictures whose transparent areas have no special relationship. But in your case, your two pictures do have a special relationship: the pictures are transparent in exactly the same areas!
Here's a self-contained test that reproduces your problem:
private func badVersion() {
let center = CGPoint(x: view.bounds.width / 2, y: view.bounds.height / 2)
let radius: CGFloat = 100
let ringWidth: CGFloat = 44
let ring = CAShapeLayer()
ring.frame = view.bounds
ring.fillColor = nil
ring.strokeColor = UIColor.red.cgColor
ring.lineWidth = ringWidth
let ringPath = CGMutablePath()
ringPath.addArc(center: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
ringPath.closeSubpath()
ring.path = ringPath
view.layer.addSublayer(ring)
let wedge = CAShapeLayer()
wedge.frame = view.bounds
wedge.fillColor = nil
wedge.strokeColor = UIColor.darkGray.cgColor
wedge.lineWidth = ringWidth
wedge.lineCap = kCALineCapButt
let wedgePath = CGMutablePath()
wedgePath.addArc(center: center, radius: radius, startAngle: 0.1, endAngle: 0.6, clockwise: false)
wedge.path = wedgePath
view.layer.addSublayer(wedge)
}
Here's the part of the screen that shows the problem:
One way to fix this is to draw the colors beyond the edges of the ring, and use a mask to clip them to the ring shape.
I'll change my code so that instead of drawing a red ring, and part of a gray ring on top of it, I draw a red disc, and a gray wedge on top of it:
If you zoom in, you can see that this still shows the red fringe at the edge of the gray wedge. So the trick is to use a ring-shaped mask to get the final shape. Here's the shape of the mask, drawn in white on top of the prior image:
Note that the mask is well away from the problematic area with the fringe. When I use the mask as a mask instead of drawing it, I get the final, perfect result:
Here's the code that draws the perfect version:
private func goodVersion() {
let center = CGPoint(x: view.bounds.width / 2, y: view.bounds.height / 2)
let radius: CGFloat = 100
let ringWidth: CGFloat = 44
let slop: CGFloat = 10
let disc = CAShapeLayer()
disc.frame = view.bounds
disc.fillColor = UIColor.red.cgColor
disc.strokeColor = nil
let ringPath = CGMutablePath()
ringPath.addArc(center: center, radius: radius + ringWidth / 2 + slop, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
ringPath.closeSubpath()
disc.path = ringPath
view.layer.addSublayer(disc)
let wedge = CAShapeLayer()
wedge.frame = view.bounds
wedge.fillColor = UIColor.darkGray.cgColor
wedge.strokeColor = nil
let wedgePath = CGMutablePath()
wedgePath.move(to: center)
wedgePath.addArc(center: center, radius: radius + ringWidth / 2 + slop, startAngle: 0.1, endAngle: 0.6, clockwise: false)
wedgePath.closeSubpath()
wedge.path = wedgePath
view.layer.addSublayer(wedge)
let mask = CAShapeLayer()
mask.frame = view.bounds
mask.fillColor = nil
mask.strokeColor = UIColor.white.cgColor
mask.lineWidth = ringWidth
let maskPath = CGMutablePath()
maskPath.addArc(center: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
maskPath.closeSubpath()
mask.path = maskPath
view.layer.mask = mask
}
Note that the mask applies to everything in view, so (in your case) you may need to move all of your layers into a subview has no other contents so it's safe to mask.
UPDATE
Looking at your playground, the problem is (still) that you're drawing two shapes that have exactly the same partially-transparent edge on top of each other. You can't do that. The solution is to draw the colored shapes larger, so that they are both completely opaque at the edge of the donut, and then use the layer mask to clip them to the donut shape.
I fixed your playground. Notice how in my version, the lineWidth of each colored section is donutThickness + 10, and the mask's lineWidth is only donutThickness. Here's the result:
Here's the playground:
import UIKit
import PlaygroundSupport
class ABDonutChart: UIView {
struct Datum {
var value: Double
var color: UIColor
}
var donutThickness: CGFloat = 20 { didSet { setNeedsLayout() } }
var separatorValue: Double = 1 { didSet { setNeedsLayout() } }
var separatorColor: UIColor = .white { didSet { setNeedsLayout() } }
var data = [Datum]() { didSet { setNeedsLayout() } }
func withAnimation(_ wantAnimation: Bool, do body: () -> ()) {
let priorFlag = wantAnimation
self.wantAnimation = true
defer { self.wantAnimation = priorFlag }
body()
layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
let bounds = self.bounds
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
let radius = (min(bounds.size.width, bounds.size.height) - donutThickness) / 2
let maskLayer = layer.mask as? CAShapeLayer ?? CAShapeLayer()
maskLayer.frame = bounds
maskLayer.fillColor = nil
maskLayer.strokeColor = UIColor.white.cgColor
maskLayer.lineWidth = donutThickness
maskLayer.path = CGPath(ellipseIn: CGRect(x: center.x - radius, y: center.y - radius, width: 2 * radius, height: 2 * radius), transform: nil)
layer.mask = maskLayer
var spareLayers = segmentLayers
segmentLayers.removeAll()
let finalSum = data.reduce(Double(0)) { $0 + $1.value + separatorValue }
var runningSum: Double = 0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
func addSegmentLayer(color: UIColor, segmentSum: Double) {
let angleOffset: CGFloat = -0.25 * 2 * .pi
let segmentLayer = spareLayers.popLast() ?? CAShapeLayer()
segmentLayer.strokeColor = color.cgColor
segmentLayer.lineWidth = donutThickness + 10
segmentLayer.lineCap = kCALineCapButt
segmentLayer.fillColor = nil
let path = CGMutablePath()
path.addArc(center: center, radius: radius, startAngle: angleOffset, endAngle: CGFloat(segmentSum / finalSum * 2 * .pi) + angleOffset, clockwise: false)
segmentLayer.path = path
layer.insertSublayer(segmentLayer, at: 0)
segmentLayers.append(segmentLayer)
if wantAnimation {
segmentLayer.add(animation, forKey: animation.keyPath)
}
}
for datum in data {
addSegmentLayer(color: separatorColor, segmentSum: runningSum + separatorValue / 2)
runningSum += datum.value + separatorValue
addSegmentLayer(color: datum.color, segmentSum: runningSum - separatorValue / 2)
}
addSegmentLayer(color: separatorColor, segmentSum: finalSum)
spareLayers.forEach { $0.removeFromSuperlayer() }
}
private var segmentLayers = [CAShapeLayer]()
private var wantAnimation = false
}
let container = UIView()
container.frame.size = CGSize(width: 300, height: 300)
container.backgroundColor = .black
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
let m = ABDonutChart(frame: CGRect(x: 0, y: 0, width: 215, height: 215))
m.center = CGPoint(x: container.bounds.size.width / 2, y: container.bounds.size.height / 2)
container.addSubview(m)
m.withAnimation(true) {
m.data = [
.init(value: 10, color: .red),
.init(value: 30, color: .blue),
.init(value: 15, color: .orange),
.init(value: 40, color: .yellow),
.init(value: 50, color: .green)]
}
To me, it looks like the edges are antialiased resulting in somewhat transparent pixels. The orange of the background can then be seen through the 'blurred' edges of the overlay.
Have you tried making the overlaid layers opaque?
layer.Opaque = true; //C#
An alternative way may be to draw a thin circle with the background color on top the orange edges. This should work, but it's not the prettiest method.

Resources