在315°圆弧上绘制圆角

时间:2019-09-12 18:25:45

标签: swift core-graphics uibezierpath

我正在绘制一个270°的圆弧,并且圆弧的两端都呈圆形。这项工作正常,但是现在我想将拱形更改为315°(-45°),但随后我的角点计算将无法正常工作。

我试图用不同的方式来计算,但是似乎找不到用于在起点和终点不是垂直或水平时将通用圆角添加到圆弧的公式。

这是我的游乐场代码:

import UIKit
import PlaygroundSupport

class ArcView: UIView {

  private var strokeWidth: CGFloat {
    return CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
  }
  private let cornerRadius: CGFloat = 10

  override open func draw(_ rect: CGRect) {
    super.draw(rect)
    backgroundColor = UIColor.white

    drawNormalCircle()
  }

  func drawNormalCircle() {
    let strokeWidth = CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
    let innerRadius = (min(self.bounds.width, self.bounds.height) - strokeWidth*2) / 2.0
    let outerRadius = (min(self.bounds.width, self.bounds.height)) / 2.0

    var endAngle: CGFloat = 270.0
    let bezierPath = UIBezierPath(arcCenter: self.center, radius: outerRadius, startAngle: 0, endAngle: endAngle * .pi / 180, clockwise: true)

    var point = bezierPath.currentPoint
    point.y += cornerRadius
    let arc = UIBezierPath(arcCenter: point, radius: cornerRadius, startAngle: 180 * .pi / 180, endAngle: 270 * .pi / 180, clockwise: true)
    arc.apply(CGAffineTransform(rotationAngle: (360 - endAngle) * .pi / 180))

    var firstCenter = bezierPath.currentPoint
    firstCenter.y += cornerRadius

    bezierPath.addArc(withCenter: firstCenter, radius: cornerRadius , startAngle: 270 * .pi / 180 , endAngle: 0, clockwise: true)
    bezierPath.addLine(to: CGPoint(x: bezierPath.currentPoint.x, y: strokeWidth - cornerRadius))

    var secondCenter = bezierPath.currentPoint
    secondCenter.x -= cornerRadius
    bezierPath.addArc(withCenter: secondCenter, radius: cornerRadius , startAngle: 0, endAngle: 90 * .pi / 180, clockwise: true)
    bezierPath.addArc(withCenter: self.center, radius: innerRadius, startAngle: 270 * .pi / 180, endAngle: 0, clockwise: false)

    var thirdCenter = bezierPath.currentPoint
    thirdCenter.x += cornerRadius
    bezierPath.addArc(withCenter: thirdCenter, radius: cornerRadius , startAngle: 180 * .pi / 180, endAngle: 270 * .pi / 180, clockwise: true)

    bezierPath.addLine(to: CGPoint(x: bezierPath.currentPoint.x + strokeWidth - (cornerRadius * 2), y: bezierPath.currentPoint.y))

    var fourthCenter = bezierPath.currentPoint
    fourthCenter.y += cornerRadius
    bezierPath.addArc(withCenter: fourthCenter, radius: cornerRadius , startAngle: 270 * .pi / 180, endAngle: 0, clockwise: true)

    bezierPath.close()

    let backgroundLayer = CAShapeLayer()
    backgroundLayer.path = bezierPath.cgPath
    backgroundLayer.strokeColor = UIColor.red.cgColor
    backgroundLayer.lineWidth = 2
    backgroundLayer.fillColor = UIColor.lightGray.cgColor

    self.layer.addSublayer(backgroundLayer)
  }
}

let arcView = ArcView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
PlaygroundPage.current.liveView = arcView

对我来说,问题是当角不是给定的X-CornerRadius或Y + corner Radius(在完全水平或垂直的情况下)时,如何计算角的弧中心。弧度为315°时如何获得圆角。

2 个答案:

答案 0 :(得分:1)

续上the first part of my answer


再次尝试rotationalInsetAngleradialInsetDistance

现在,转角知道它们相对于其距离和角度的哪一侧,我们可以实现rotationalInsetAngleradialInsetDistance

radialInsetDistance很简单。根据我们是在内部还是外部,我们只是从中间向左或向左移动self.radius

struct RoundedAnnulusSectorCorner { 
    // ...

    /// The distance towards/away from the disk's center
    /// where this corner's center is going to be
    internal var radialInsetDistance: CGFloat {
        switch self.radialPosition {
        case  .inside(_): return -self.radius // negative: towards center
        case .outside(_): return +self.radius // positive: away from center
        }
    }
}

rotationalInsetAngle有点棘手,您需要打开记事本,并尽自己最大的努力来回忆起高中的琐事。

struct RoundedAnnulusSectorCorner { 
    // ...
    /// The angular inset (in radians) from the disk's edge
    /// where this corner's center is going to be
    internal var rotationalInsetAngle: CGFloat {
        let angle = ???
        switch self.rotationalPosition {
        case .ccw(_): return -angle // negative: ccw from the edge
        case  .cw(_): return +angle // postiive:  cw from the edge
        }
    }
}

我们知道我们需要旋转一个称为angle的角度,其大小始终相同,但是其符号取决于我们的拐角位于起点之前还是终点边缘之后。

我们知道,平移后,角圆的笔触将与父形的边/弧重叠。该距离为self.radius,它形成直角三角形的“相反”边。斜边是我们绕着旋转的半径,长度为self.radialPosition.distanceFromCenter。假设我们有一个oppositeo)和一个斜边(h),则该作业的正确触发函数为sinangle = sin(o / h)。在上下文中:

struct RoundedAnnulusSectorCorner { 
    // ...
    /// The angular inset (in radians) from the disk's edge
    /// where this corner's center is going to be
    internal var rotationalInsetAngle: CGFloat {
        let angle = sin(self.radius / self.radialPosition.distanceFromCenter)
        switch self.rotationalPosition {
        case .ccw(_): return -angle // negative: ccw from the edge
        case  .cw(_): return +angle // postiive:  cw from the edge
        }
    }
}

放弃rawCornerPoint

嬉皮士嬉皮士,我们的center计算属性现在应该正确显示,因为我们新实现的rotationalInsetAngleradialInsetDistance

我们可以更新角落的弧线以使用它:

struct RoundedAnnulusSectorCorner { 
    // ...
    var arc: Arc {
        return Arc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
    }
    // ...
}

第五次渲染

如果一切顺利,您应该看到拐角处的圆现在位于正确的位置(以center s为中心,而不是rawCornerPoint s为中心)。

去圆化

到目前为止,我们将圆角渲染为完整的圆。这对于使其正常运行非常有用,但是现在我们可以对其进行修复。让我们适当地实现startAngle / endAngle计算出的属性,以得出每个圆角的适当起始和终止角度。

这些很容易。每个圆角都从垂直于,垂直于其或垂直于其的边角开始。然后弧线继续旋转四分之一圈,因此我们可以通过向endAngle添加四分之一圈(2π)来获得startAngle

struct RoundedAnnulusSectorCorner { 
    // ...
    /// The angle at which this corner's arc starts.
    var startAngle: CGFloat {
        switch (radialPosition, rotationalPosition) {
        case let ( .inside(_),  .cw(of: edgeAngle)): return edgeAngle + (3 * .pi/2)
        case let ( .inside(_), .ccw(of: edgeAngle)): return edgeAngle + (0 * .pi/2)
        case let (.outside(_), .ccw(of: edgeAngle)): return edgeAngle + (1 * .pi/2)
        case let (.outside(_),  .cw(of: edgeAngle)): return edgeAngle + (2 * .pi/2)
        }
    }


    /// The angle at which this corner's arc ends.
    var endAngle: CGFloat {
        return self.startAngle + .pi/2 // A quarter turn clockwise from the start
    }
}

第六次渲染

我们快到了!现在我们的圈子不再存在。我们有圆角,以正确插入的center点为中心,并以正确的弧线成弧形。

唯一剩下的问题是,我们的startAngleEdgeendAngleEdgeinnerArcouterArc不会在圆角终止处终止。

固定边缘

我们现在可以替换startPoint / endPoint的定义,到目前为止,该定义被计算为rawCornerPoint

计算这些就很容易将我们的点朝着起始/结束角平移radius。哦,瞧,我们已经为此做了一个工具!

struct RoundedAnnulusSectorCorner {
    // ...
    /// The point at which this corner's arc starts.
    var startPoint: CGPoint {
        return self.center.translated(towards: startAngle, by: radius)
    }

    /// The point at which this corner's arc ends.
    var endPoint: CGPoint {
        return self.center.translated(towards: endAngle, by: radius)
    }
    // ...
}

第七次渲染

现在我们的边缘在正确的位置!

修复弧线

修复弧线很容易。我们知道每个角的rotationalInsetAngle,我们只需要将其添加到我们的开始/结束角度中,以便根据需要在以后/更早开始/结束:

struct RoundedAnnulusSectorCorner {
    // ...
    private var outerArc: Arc {
        return Arc(
            center: self.center,
            radius: self.outerRadius,
            startAngle: self.startAngle + self.corner1.rotationalInsetAngle,
            endAngle: self.endAngle + self.corner2.rotationalInsetAngle,
            clockwise: true
        )
    }

    private var innerArc: Arc {
        return Arc(
            center: self.center,
            radius: self.innerRadius,
            startAngle: self.endAngle + self.corner3.rotationalInsetAngle,
            endAngle: self.startAngle + self.corner4.rotationalInsetAngle,
            clockwise: false
        )
    }
    // ...
}

第八次渲染

完成了!这是最后的final gist,尽管我强烈建议继续学习并了解该过程。

答案 1 :(得分:1)

那是我的解决方案。 这只是简单的三角学

/// Create a path made with 6 small subpaths
///
/// - Parameters:
///   - startAngle: the start angle of the path in cartesian plane angles system
///   - endAngle: the end angle of the path in cartesian plane angles system
///   - outerRadius: the radius of the outer circle in % relative to the size of the view that holds it
///   - innerRadius: the radius of the inner circle in % relative to the size of the view that holds it
///   - cornerRadius: the corner radius of the edges
///
/// - Returns: the path itself
func createPath(from startAngle: Double, to endAngle: Double,
                outerRadius:CGFloat, innerRadius:CGFloat,
                cornerRadius: CGFloat) -> UIBezierPath {

    let path = UIBezierPath()
    let maxDim = min(view.frame.width, view.frame.height)
    let oRadius: CGFloat = maxDim/2 * outerRadius
    let iRadius: CGFloat = maxDim/2 * innerRadius
    let center = CGPoint.init(x: view.frame.width/2, y: view.frame.height/2)

    let startAngle = deg2rad(360.0 - startAngle)
    let endAngle = deg2rad(360.0 - endAngle)

    // Outer Finish Center point
    let ofcX = center.x + (oRadius - cornerRadius) * CGFloat(cos(endAngle - deg2rad(360)))
    let ofcY = center.y + (oRadius - cornerRadius) * CGFloat(sin(endAngle - deg2rad(360)))

    // Inner Finish Center point
    let ifcX = center.x + (iRadius + cornerRadius) * CGFloat(cos(endAngle - deg2rad(360)))
    let ifcY = center.y + (iRadius + cornerRadius) * CGFloat(sin(endAngle - deg2rad(360)))

    // Inner Starting Center point
    let iscX = center.x + (iRadius + cornerRadius) * CGFloat(cos(startAngle - deg2rad(360)))
    let iscY = center.y + (iRadius + cornerRadius) * CGFloat(sin(startAngle - deg2rad(360)))

    // Outer Starting Center point
    let oscX = center.x + (oRadius - cornerRadius) * CGFloat(cos(startAngle - deg2rad(360)))
    let oscY = center.y + (oRadius - cornerRadius) * CGFloat(sin(startAngle - deg2rad(360)))


    // Outer arch
    path.addArc(withCenter: center, radius: oRadius,
                startAngle: startAngle, endAngle: endAngle,
                clockwise: true)

    // Rounded outer finish
    path.addArc(withCenter: CGPoint(x: ofcX, y: ofcY), radius: cornerRadius,
                startAngle: endAngle, endAngle:endAngle +  deg2rad(90),
                clockwise: true)

    // Rounded inner finish
    path.addArc(withCenter: CGPoint(x: ifcX, y: ifcY), radius: cornerRadius,
                startAngle: endAngle +  deg2rad(90), endAngle: endAngle +  deg2rad(180),
                clockwise: true)

    // Inner arch
    path.addArc(withCenter: center, radius: iRadius,
                startAngle: endAngle, endAngle: startAngle,
                clockwise: false)

    // Rounded inner start
    path.addArc(withCenter:  CGPoint(x: iscX, y: iscY), radius: cornerRadius,
                startAngle:  startAngle + deg2rad(180), endAngle:  startAngle + deg2rad(270),
                clockwise: true)

    // Rounded outer start
    path.addArc(withCenter:  CGPoint(x: oscX, y: oscY), radius: cornerRadius,
                startAngle:  startAngle + deg2rad(270), endAngle:  startAngle,
                clockwise: true)

    return path
}

func deg2rad(_ number: Double) -> CGFloat {
    return CGFloat(number * .pi / 180)
}

用法:

 @IBOutlet weak var mainView: UIView!

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    let borderLayer = CAShapeLayer()
    borderLayer.path = createPath(from: 30, to: 120, outerRadius: 0.9, innerRadius: 0.3, cornerRadius: 5).cgPath
    borderLayer.strokeColor = UIColor.orange.cgColor
    borderLayer.fillColor = UIColor.orange.cgColor
    borderLayer.lineWidth = 0.0
    mainView.layer.addSublayer(borderLayer)
}

enter image description here