Reputation: 1
I'm trying to draw multiple shapes using UIBezierPath and CAShapeLayer in the same layer so my patternImage color goes across all the shapes correctly, but there seems to be some weird clipping/cutting out that I can't solve. When drawing 2 shapes in the same path & layer it clips the overlap but only when the shapes are not drawn in the same direction.
My code:
let view = UIView()
let path = UIBezierPath()
path.addArc(withCenter: .init(x: 100, y: 100), radius: 100, startAngle: 0 * .pi, endAngle: 2 * .pi, clockwise: true)
path.close()
path.addArc(withCenter: .init(x: 200, y: 100), radius: 100, startAngle: 0 * .pi, endAngle: 2 * .pi, clockwise: true)
path.close()
path.addArc(withCenter: .init(x: 100, y: 300), radius: 100, startAngle: 2 * .pi, endAngle: 0 * .pi, clockwise: false)
path.close()
path.addArc(withCenter: .init(x: 200, y: 300), radius: 100, startAngle: 2 * .pi, endAngle: 0 * .pi, clockwise: false)
path.close()
path.addArc(withCenter: .init(x: 100, y: 500), radius: 100, startAngle: 2 * .pi, endAngle: 0 * .pi, clockwise: false)
path.close()
path.addArc(withCenter: .init(x: 200, y: 500), radius: 100, startAngle: 0 * .pi, endAngle: 2 * .pi, clockwise: true)
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.fillColor = UIColor(patternImage: .maskStripes).cgColor
layer.fillRule = .nonZero
view.layer.addSublayer(layer)
The output: rendered circles
You can see that the first 2 groups of circles that have matching clockwise directions render fine but the last one where the directions are opposites the middle is clipped. Setting the bezierPath's usesEvenOddFillRule
doesn't help or the layer's fillRule
and I can't guarantee in my real app that the shapes will be drawn with the same directions hence the problem.
Thank you in advance.
Upvotes: 0
Views: 119
Reputation: 77486
One approach that may work for you...
CAShapeLayer
the full size of the arcs bounds.fillColor
of that layer to the patternImagethen
CALayer
, with that image as its .contents
Here is some sample code...
We'll start with an "Arc" struct that matches the bezier path properties:
struct MyArc {
var c: CGPoint = .zero
var r: CGFloat = 100.0
var s: CGFloat = 0 * .pi
var e: CGFloat = 2 * .pi
var k: Bool = true
}
This view controller will use the six Arcs that you used in your example.
It will create 3 views:
I used this tiny 10x10-pixel image, with 50% opacity "between the lines" as the pattern image:
The example controller has a segmented control to toggle between the three views... and a label in the "background" to show the transparency (tap anywhere to toggle the label visibility):
class PathFillViewController: UIViewController {
var patternImg: UIImage!
var theArcs: [MyArc] = []
var arcsRect: CGRect!
var threeViews: [UIView] = []
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// load the pattern image
guard let img = UIImage(named: "pattern1010d") else {
fatalError("Could not load pattern image!!!!")
}
patternImg = img
// create 6 arcs
var c: CGPoint = .zero
c.x = 100.0
c.y = 100.0
theArcs.append(MyArc(c: c, s: 0, e: 2, k: true))
c.x = 200.0
c.y = 100.0
theArcs.append(MyArc(c: c, s: 0, e: 2, k: true))
c.x = 100.0
c.y = 300.0
theArcs.append(MyArc(c: c, s: 2, e: 0, k: false))
c.x = 200.0
c.y = 300.0
theArcs.append(MyArc(c: c, s: 2, e: 0, k: false))
c.x = 100.0
c.y = 500.0
theArcs.append(MyArc(c: c, s: 2, e: 0, k: false))
c.x = 200.0
c.y = 500.0
theArcs.append(MyArc(c: c, s: 0, e: 2, k: true))
// let's calculate the rect needed to hold the arcs
var bzPth: UIBezierPath!
theArcs.forEach { a in
bzPth = UIBezierPath()
bzPth.addArc(withCenter: a.c, radius: a.r, startAngle: a.s * .pi, endAngle: a.e * .pi, clockwise: a.k)
bzPth.close()
if nil == arcsRect {
arcsRect = bzPth.bounds
} else {
arcsRect = CGRectUnion(arcsRect, bzPth.bounds)
}
}
threeViews.append(origView())
threeViews.append(multiView())
threeViews.append(maskView())
let g = view.safeAreaLayoutGuide
threeViews.forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.centerXAnchor.constraint(equalTo: g.centerXAnchor),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
v.widthAnchor.constraint(equalToConstant: arcsRect.width),
v.heightAnchor.constraint(equalToConstant: arcsRect.height),
])
}
// add a segmented control
let sc = UISegmentedControl(items: ["Orig", "Multi Layers", "Masked Layer"])
sc.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sc)
NSLayoutConstraint.activate([
sc.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
sc.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
sc.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
// let's insert a label with really big text behind the views
// so we can see the alpha
let label = UILabel()
label.font = .systemFont(ofSize: 60.0, weight: .light)
label.textAlignment = .center
label.numberOfLines = 0
var s: String = "1234567890"
for _ in 0..<8 {
s += "\n" + "1234567890"
}
label.text = s
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.sendSubviewToBack(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: sc.bottomAnchor, constant: 20.0),
label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
sc.addTarget(self, action: #selector(segChanged(_:)), for: .valueChanged)
sc.selectedSegmentIndex = 0
segChanged(sc)
}
@objc func segChanged(_ sender: UISegmentedControl) {
for (i, v) in threeViews.enumerated() {
v.isHidden = sender.selectedSegmentIndex != i
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// toggle background label visibility
if let v = view.subviews.first as? UILabel {
v.isHidden.toggle()
}
}
func origView() -> UIView {
let v = UIView()
let bzPth = UIBezierPath()
theArcs.forEach { a in
bzPth.addArc(withCenter: a.c, radius: a.r, startAngle: a.s * .pi, endAngle: a.e * .pi, clockwise: a.k)
bzPth.close()
}
let shapeLayer = CAShapeLayer()
shapeLayer.path = bzPth.cgPath
shapeLayer.fillRule = .nonZero
shapeLayer.fillColor = UIColor(patternImage: patternImg).cgColor
v.layer.addSublayer(shapeLayer)
return v
}
func multiView() -> UIView {
let v = UIView()
var bzPth: UIBezierPath!
theArcs.forEach { a in
bzPth = UIBezierPath()
bzPth.addArc(withCenter: a.c, radius: a.r, startAngle: a.s * .pi, endAngle: a.e * .pi, clockwise: a.k)
bzPth.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = bzPth.cgPath
shapeLayer.fillRule = .nonZero
shapeLayer.fillColor = UIColor(patternImage: patternImg).cgColor
v.layer.addSublayer(shapeLayer)
}
return v
}
func maskView() -> UIView {
let v = UIView()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = .init(origin: .zero, size: arcsRect.size)
shapeLayer.fillColor = UIColor(patternImage: patternImg).cgColor
shapeLayer.path = UIBezierPath(rect: .init(origin: .zero, size: arcsRect.size)).cgPath
v.layer.addSublayer(shapeLayer)
var bzPth: UIBezierPath!
let sz: CGSize = shapeLayer.frame.size
let rndr = UIGraphicsImageRenderer(size: sz)
let tmpImg = rndr.image { ctx in
UIColor.red.setFill()
theArcs.forEach { a in
bzPth = UIBezierPath()
bzPth.addArc(withCenter: a.c, radius: a.r, startAngle: a.s * .pi, endAngle: a.e * .pi, clockwise: a.k)
bzPth.close()
ctx.cgContext.beginPath()
ctx.cgContext.addPath(bzPth.cgPath)
ctx.cgContext.fillPath()
}
}
let iMsk = CALayer()
iMsk.contents = tmpImg.cgImage
iMsk.frame = .init(origin: .zero, size: sz)
// apply image layer as a mask
shapeLayer.mask = iMsk
// un-comment if we want to see the image layer
//v.layer.addSublayer(iMsk)
return v
}
}
I get this output:
and with the label showing through:
Upvotes: 0
Reputation: 131426
I don't think there is a solution that causes all overlapping closed paths to fill the union of their areas. I suggest creating a separate layer for each closed shape you want to fill and adding them all as sublayers. That will work, and let you use separate colors for each (if desired.)
Upvotes: 1