Specifying UIBezierPath points with corner radii like in drawing apps (e.g. Sketch)

孤街醉人 提交于 2021-02-08 11:28:24

问题


In drawing apps such as Sketch, when you draw a vector, you can specify a custom corner radius for each point individually.

For example, here is a 5 point vector, with the middle point's corner radius set to 17:

(The top left and bottom right points have custom radii as well.)

In Swift, I can draw paths using a UIBezierPath, but when I specify addLine, I'm only given an option to specify a point. I don't have the option to specify a corner radius.

How do I give addLine a corner radius?

It seems like I should be able to use either addCurve or addArc to achieve what I want, but I'm not sure what values to supply to those to get the desired results.


回答1:


Instead of addLine, you must use addArc. The values for addArc are dependent on the previous/next points. Thus any helper function should take in the entire array of points you want to use.

If you can figure out how to draw a curve between two of the lines, the same algorithm could be repeated for the entire shape.

To make things easier to understand, refer to this diagram (the code will reference these points as well):

The goal is to find out the circle's center and the start/end angles.

First, you should grab the CGFloat and CGPoint extensions from here.

Next add these helper functions:

extension Collection where Index == Int {
    func items(at index: Index) -> (previous: Element, current: Element, next: Element) {
        precondition(count > 2)
        let previous = self[index == 0 ? count - 1 : index - 1]
        let current = self[index]
        let next = self[(index + 1) % count]
        return (previous, current, next)
    }
}

/// Returns ∠abc (i.e. clockwise degrees from ba to bc)
//
//  b - - - a
//   \
//    \
//     \
//      c
//
func angleBetween3Points(_ a: CGPoint, _ b: CGPoint, _ c: CGPoint) -> CGFloat {
    let xbaAngle = (a - b).angle 
    let xbcAngle = (c - b).angle // if you were to put point b at the origin, `xbc` refers to the angle formed from the x-axis to the bc line (clockwise)
    let abcAngle = xbcAngle - xbaAngle
    return CGPoint(angle: abcAngle).angle // normalize angle between -π to π
}

func arcInfo(
    previous: CGPoint,
    current: CGPoint,
    next: CGPoint,
    radius: CGFloat)
    -> (center: CGPoint, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)
{
    let a = previous
    let b = current
    let bCornerRadius: CGFloat = radius
    let c = next

    let abcAngle: CGFloat = angleBetween3Points(a, b, c)
    let xbaAngle = (a - b).angle
    let abeAngle = abcAngle / 2

    let deLength: CGFloat = bCornerRadius
    let bdLength = bCornerRadius / tan(abeAngle)
    let beLength = sqrt(deLength*deLength + bdLength*bdLength)

    let beVector: CGPoint = CGPoint(angle: abcAngle/2 + xbaAngle)

    let e: CGPoint = b + beVector * beLength

    let xebAngle = (b - e).angle
    let bedAngle = (π/2 - abs(abeAngle)) * abeAngle.sign() * -1

    return (
        center: e,
        startAngle: xebAngle - bedAngle,
        endAngle: xebAngle + bedAngle,
        clockwise: abeAngle < 0)
}

func addArcs(to path: UIBezierPath, pointsAndRadii: [(point: CGPoint, radius: CGFloat)]) {
    precondition(pointsAndRadii.count > 2)

    for i in 0..<pointsAndRadii.count {
        let (previous, current, next) = pointsAndRadii.items(at: i)
        let (center, startAngle, endAngle, clockwise) = arcInfo(
            previous: previous.point,
            current: current.point,
            next: next.point,
            radius: current.radius)

        path.addArc(withCenter: center, radius: current.radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
    }
}

Finally, you can now use these functions to render any vector defined in a drawing app easily:

override func draw(_ rect: CGRect) {
    let grayPath = UIBezierPath()
    addArcs(
        to: grayPath,
        pointsAndRadii: [
            (point: CGPoint(x: 100, y: 203), radius: 0),
            (point: CGPoint(x: 100, y: 138.62), radius: 33),
            (point: CGPoint(x: 173.78, y: 100), radius: 0),
            (point: CGPoint(x: 139.14, y: 172.51), radius: 17),
            (point: CGPoint(x: 231, y: 203), radius: 3),
        ])
    grayPath.close()
    grayPath.lineWidth = 5
    UIColor.gray.setStroke()
    grayPath.stroke()
}

Which will produce this exact replica:



来源:https://stackoverflow.com/questions/60035278/specifying-uibezierpath-points-with-corner-radii-like-in-drawing-apps-e-g-sket

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!