I have two intersecting bezier paths of type UIBezierPath like the following picture. How can I get the subpath depicted by red dashed line in Swift?
Asked
Active
Viewed 221 times
0
-
1You will have to use (gasp) math! https://pomax.github.io/bezierinfo/#curveintersection – matt Apr 22 '22 at 16:04
-
I am trying to use https://github.com/adamwulf/ClippingBezier but no luck yet! – Asteroid Apr 22 '22 at 16:35
-
@Asteroid - what have you tried with that ClippingBezier code? A few minutes of digging with the included example app, and I changed the "Cut Shapes" example to this: https://i.stack.imgur.com/0xCyc.png ... is that what you're going for? – DonMag Apr 23 '22 at 14:48
-
Yes, it’s what I need, in Swift though. With ClippingBezier I tried to get what I need using the intersection functions but it gives the whole closed path intersection – Asteroid Apr 23 '22 at 20:36
-
@Asteroid - you really need to show the work you've done and explain where you're running into trouble. Otherwise (as with your previous question here: https://stackoverflow.com/questions/71917171/mask-a-gradient-layer-with-the-intersection-of-two-shape-layers-in-swift) we end up *guessing* at where you are and what you need help with. I posted an answer with some example code that *might* get you on your way. – DonMag Apr 24 '22 at 15:35
1 Answers
1
Here's some sample code using ClippingBezier that may get you on your way...
It looks like this when run:
So, we define two sample shapes:
class SamplePaths: NSObject {
// each set of 6 values defines:
// curve To point
// control point 1
// control point 2
let start1: CGPoint = CGPoint(x: 29, y: 134)
let vals1: [CGFloat] = [
15, 34, 5, 94, -6, 57,
274, 69, 68, -22, 148, 57,
626, 7, 420, 82, 590, 22,
871, 102, 662, -7, 845, 33,
789, 286, 896, 172, 877, 188,
700, 490, 702, 383, 719, 393,
569, 605, 682, 587, 626, 638,
330, 503, 433, 525, 375, 594,
180, 227, 283, 282, 295, 271,
29, 134, 65, 182, 53, 174,
]
let start2: CGPoint = CGPoint(x: 240, y: 452)
let vals2: [CGFloat] = [
421.0, 369.0, 289.0, 452.0, 337.0, 369.0,
676.0, 468.0, 506.0, 369.0, 581.0, 462.0,
925.0, 369.0, 771.0, 474.0, 875.0, 385.0,
1120.0, 397.0, 976.0, 354.0, 1090.0, 334.0,
1086.0, 581.0, 1150.0, 460.0, 1119.0, 519.0,
1127.0, 770.0, 1053.0, 643.0, 1173.0, 669.0,
997.0, 845.0, 1081.0, 871.0, 1093.0, 857.0,
786.0, 790.0, 902.0, 833.0, 910.0, 790.0,
536.0, 854.0, 663.0, 790.0, 710.0, 860.0,
290.0, 731.0, 362.0, 848.0, 405.0, 742.0,
92.0, 770.0, 174.0, 721.0, 181.0, 794.0,
3.0, 652.0, 3.0, 746.0, 3.0, 705.0,
92.0, 519.0, 3.0, 587.0, 13.0, 550.0,
171.0, 452.0, 172.0, 489.0, 131.0, 464.0,
240.0, 452.0, 211.0, 439.0, 191.0, 452.0
]
func samplePath(_ n: Int) -> UIBezierPath {
var pt: CGPoint = .zero
var c1: CGPoint = .zero
var c2: CGPoint = .zero
var start: CGPoint = .zero
var vals: [CGFloat] = []
if n == 1 {
start = start1
vals = vals1
} else {
start = start2
vals = vals2
}
let bez = UIBezierPath()
pt = start
bez.move(to: pt)
for i in stride(from: 0, to: vals.count, by: 6) {
pt = CGPoint(x: vals[i + 0], y: vals[i + 1])
c1 = CGPoint(x: vals[i + 2], y: vals[i + 3])
c2 = CGPoint(x: vals[i + 4], y: vals[i + 5])
bez.addCurve(to: pt, controlPoint1: c1, controlPoint2: c2)
}
bez.close()
return bez
}
}
Then we create a UIView
subclass that strokes either First, Second, or Both paths, or only the Clipped path:
class TestView: UIView {
enum Show {
case both
case first
case second
case clippedPath
}
public var show: Show = .both {
didSet {
setNeedsDisplay()
}
}
public var path1: UIBezierPath!
public var path2: UIBezierPath!
override func draw(_ rect: CGRect) {
// don't do anything if we don't have valid paths
guard let path1 = self.path1,
let path2 = self.path2
else { return }
// this fits the paths into self.bounds
let margin: CGFloat = 8
let fittingBounds = self.bounds.insetBy(dx: margin, dy: margin)
let entireBounds = path1.bounds.union(path2.bounds)
let scale = min(fittingBounds.size.width / entireBounds.size.width, fittingBounds.size.height / entireBounds.size.height)
var transform: CGAffineTransform = .identity
transform = transform.translatedBy(x: margin, y: margin)
transform = transform.scaledBy(x: scale, y: scale)
let ctx = UIGraphicsGetCurrentContext()
ctx?.saveGState()
ctx?.concatenate(transform)
path1.lineWidth = 4
path2.lineWidth = 4
switch show {
case .first:
UIColor.systemGreen.setStroke()
path1.stroke()
case .second:
UIColor.blue.setStroke()
path2.stroke()
case .clippedPath:
// get the unique shapes from slicing path2 with path1
guard let shapes: [DKUIBezierPathShape] = path2.uniqueShapesCreatedFromSlicing(withUnclosedPath: path1) else { return }
// get the first shape
guard let shape: DKUIBezierPathShape = shapes.first else { return }
// get the first segment
guard let seg = shape.segments.firstObject as? DKUIBezierPathClippedSegment else { return }
// get the path from that segment
let pth: UIBezierPath = seg.pathSegment
// stroke that segment's path
UIColor.red.setStroke()
pth.stroke()
// print the segment's path to the debug console
print(pth)
default:
UIColor.systemGreen.setStroke()
path1.stroke()
UIColor.blue.setStroke()
path2.stroke()
}
ctx?.restoreGState()
}
}
and a sample controller to produce the above images:
// import the ClippingBezier library
import ClippingBezier
class ViewController: UIViewController {
let testView: TestView = {
let v = TestView()
v.backgroundColor = .white
return v
}()
let infoLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
return v
}()
// index to step through examples
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// create a stack view
let stack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// create a button
let btn: UIButton = {
let v = UIButton()
v.setTitle("Next Step", for: [])
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .systemBlue
v.layer.cornerRadius = 8
v.addTarget(self, action: #selector(nextStep), for: .touchUpInside)
return v
}()
// add elements to stack view
[infoLabel, testView, btn].forEach { v in
stack.addArrangedSubview(v)
}
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// let's use 300 x 300 for our test view
testView.widthAnchor.constraint(equalToConstant: 300.0),
testView.heightAnchor.constraint(equalTo: testView.widthAnchor),
])
// set the bezier path shapes
testView.path1 = SamplePaths().samplePath(1)
testView.path2 = SamplePaths().samplePath(2)
// get started
nextStep()
}
@objc func nextStep() {
switch idx % 4 {
case 1:
infoLabel.text = "Stroke Second Path"
testView.show = .second
case 2:
infoLabel.text = "Stroke Both Paths"
testView.show = .both
case 3:
infoLabel.text = "Stroke only the Clipped Path"
testView.show = .clippedPath
default:
infoLabel.text = "Stroke First Path"
testView.show = .first
}
idx += 1
}
}
Notes:
This is Sample Code Only!!! It is meant to be a starting point.
You will need (probably a lot of) additional logic. For example, if your paths look like this:
you will have multiple clipped segments.

DonMag
- 69,424
- 5
- 50
- 86
-
This answered my question, thanks! What if the green path is open and the open part is inside the intersection of the path1 and path2? – Asteroid Apr 25 '22 at 17:05
-
@Asteroid - you'll need to dig a little deeper into the code. Examine the various methods... Use debug to inspect the results of different actions... You may need to write some additional code to do exactly what you want. – DonMag Apr 27 '22 at 12:47