如何让 CGPath 切口圆弧的圆心位于形状边缘?
结论
将圆弧圆心的 x 改为 0 或 width 后,还要重新计算圆弧与直边之间的圆角过渡。原代码中的过渡点是按圆心 x = cutOutCornerRadius 计算的;圆心移到边缘后,这些点就无法直接衔接。
以左侧切口为例,设切口半径为 R = cutOutRadius,过渡圆角半径为 r = cutOutCornerRadius,圆心为 C = (0, cutOutPosition)。若要让过渡圆弧同时与边缘直线和切口圆弧相切,可以按下面的方法计算切点。
计算相切点
下面的构造方式让过渡圆与直边 x = 0 相切,并在切口圆内侧与半径为 R 的圆相切。此构造要求 R >= 2r。
对于切口上端和下端,令 s 分别为 1 和 -1。过渡圆圆心以及切口圆上的切点按下式计算:
let R = cutOutRadius
let r = cutOutCornerRadius
let y = cutOutPosition
precondition(R >= 2 * r)
let offset = sqrt((R - r) * (R - r) - r * r)
// s = 1 表示上端;s = -1 表示下端
func leftFilletPoints(s: CGFloat) -> (filletCenter: CGPoint, circleTangent: CGPoint, edgeTangent: CGPoint) {
let filletCenter = CGPoint(x: r, y: y + s * offset)
let circleTangent = CGPoint(
x: R * r / (R - r),
y: y + s * R * offset / (R - r)
)
let edgeTangent = CGPoint(x: 0, y: filletCenter.y)
return (filletCenter, circleTangent, edgeTangent)
}
左侧圆弧的圆心设为 CGPoint(x: 0, y: cutOutPosition)。上端和下端的圆角分别从直边切点连接到切口圆的切点。右侧可以镜像计算:圆心设为 CGPoint(x: width, y: cutOutPosition),过渡圆圆心的横坐标设为 width - r,切点横坐标则镜像为 width - circleTangent.x。
构造路径
依次连接边缘直线切点、过渡圆弧、切口圆弧和另一端的过渡圆弧。可以用 CGMutablePath.addArc(center:radius:startAngle:endAngle:clockwise:) 绘制各段圆弧。起止角根据对应圆心指向切点的向量计算,clockwise 按路径的实际行进方向设置。切口圆弧使用位于边缘的圆心,例如左侧:
path.addArc(
center: CGPoint(x: 0, y: cutOutPosition),
radius: cutOutRadius,
startAngle: upperAngle,
endAngle: lowerAngle,
clockwise: true
)
upperAngle 和 lowerAngle 应根据刚计算出的两个 circleTangent 点求得,不能再固定使用 .pi / 2.0 和 3.0 * .pi / 2.0。圆角切点通常不在切口圆的最上方或最下方,沿用原角度会导致路径断开或无法相切。
原代码中的 addArc(tangent1End:tangent2End:radius:) 仍可用于其他圆角;切口两端则要根据上述切点重新构造。如果 cutOutRadius < 2 * cutOutCornerRadius,这种内切圆角构造不成立。此时需要减小过渡半径,或改用 addQuadCurve(to:control:) / addCurve(to:control1:control2:),设计不要求圆弧严格相切的过渡。
备注:内容仅供参考。
