我想像 Instagram 一样创建带有背景颜色和圆角的文本。我能够实现背景颜色,但无法创建圆角。
我现在拥有的:
下面是上面截图的源代码:
-(void)createBackgroundColor{
[self.txtView.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, self.txtView.text.length) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
[textArray addObject:[NSNumber numberWithInteger:glyphRange.length]];
if (glyphRange.length == 1){
return ;
}
UIImageView *highlightBackView = [[UIImageView alloc] initWithFrame:CGRectMake(usedRect.origin.x, usedRect.origin.y , usedRect.size.width, usedRect.size.height + 2)];
highlightBackView.layer.borderWidth = 1;
highlightBackView.backgroundColor = [UIColor orangeColor];
highlightBackView.layer.borderColor = [[UIColor clearColor] CGColor];
[self.txtView insertSubview:highlightBackView atIndex:0];
highlightBackView.layer.cornerRadius = 5;
}];
}
我在
shouldChangeTextInRange
委托中调用此函数。
我想要的:
请参阅标有箭头的内半径,任何帮助将不胜感激!
RectangleContour
包。该软件包包含如何使用其 API 以及适用于 macOS 和 iOS 的演示应用程序的说明。
所以,你想要这个:
这是我花了way太长时间的答案,你可能甚至不会喜欢,因为你的问题被标记为objective-c,但我用Swift写了这个答案。您可以使用 Objective-C 中的 Swift 代码,但并不是每个人都愿意这样做。
您可以找到我的整个测试项目,包括 iOS 和 macOS 测试应用程序,在此 github 存储库中。
无论如何,我们需要做的是计算所有直线矩形的并集的轮廓。我找到了一篇 1980 年的论文,描述了必要的算法:
W.Lipski 和 F.Preparata。 “求等向矩形并集的轮廓。” J.算法 1 (1980):235-246。号码:10.1016/0196-6774(80)90011-5
该算法可能比您的问题实际需要的更通用,因为它可以处理创建孔的矩形排列:
所以这对你来说可能有点矫枉过正,但它可以完成工作。
无论如何,一旦我们有了轮廓,我们就可以将其转换为带有圆角的
CGPath
,以进行描边或填充。
该算法有些复杂,但我(在 Swift 中)将其实现为
CGPath
上的扩展方法:
import CoreGraphics
extension CGPath {
static func makeUnion(of rects: [CGRect], cornerRadius: CGFloat) -> CGPath {
let phase2 = AlgorithmPhase2(cornerRadius: cornerRadius)
_ = AlgorithmPhase1(rects: rects, phase2: phase2)
return phase2.makePath()
}
}
fileprivate func swapped<A, B>(_ pair: (A, B)) -> (B, A) { return (pair.1, pair.0) }
fileprivate class AlgorithmPhase1 {
init(rects: [CGRect], phase2: AlgorithmPhase2) {
self.phase2 = phase2
xs = Array(Set(rects.map({ $0.origin.x})).union(rects.map({ $0.origin.x + $0.size.width }))).sorted()
indexOfX = [CGFloat:Int](uniqueKeysWithValues: xs.enumerated().map(swapped))
ys = Array(Set(rects.map({ $0.origin.y})).union(rects.map({ $0.origin.y + $0.size.height }))).sorted()
indexOfY = [CGFloat:Int](uniqueKeysWithValues: ys.enumerated().map(swapped))
segments.reserveCapacity(2 * ys.count)
_ = makeSegment(y0: 0, y1: ys.count - 1)
let sides = (rects.map({ makeSide(direction: .down, rect: $0) }) + rects.map({ makeSide(direction: .up, rect: $0)})).sorted()
var priorX = 0
var priorDirection = VerticalDirection.down
for side in sides {
if side.x != priorX || side.direction != priorDirection {
convertStackToPhase2Sides(atX: priorX, direction: priorDirection)
priorX = side.x
priorDirection = side.direction
}
switch priorDirection {
case .down:
pushEmptySegmentsOfSegmentTree(atIndex: 0, thatOverlap: side)
adjustInsertionCountsOfSegmentTree(atIndex: 0, by: 1, for: side)
case .up:
adjustInsertionCountsOfSegmentTree(atIndex: 0, by: -1, for: side)
pushEmptySegmentsOfSegmentTree(atIndex: 0, thatOverlap: side)
}
}
convertStackToPhase2Sides(atX: priorX, direction: priorDirection)
}
private let phase2: AlgorithmPhase2
private let xs: [CGFloat]
private let indexOfX: [CGFloat: Int]
private let ys: [CGFloat]
private let indexOfY: [CGFloat: Int]
private var segments: [Segment] = []
private var stack: [(Int, Int)] = []
private struct Segment {
var y0: Int
var y1: Int
var insertions = 0
var status = Status.empty
var leftChildIndex: Int?
var rightChildIndex: Int?
var mid: Int { return (y0 + y1 + 1) / 2 }
func withChildrenThatOverlap(_ side: Side, do body: (_ childIndex: Int) -> ()) {
if side.y0 < mid, let l = leftChildIndex { body(l) }
if mid < side.y1, let r = rightChildIndex { body(r) }
}
init(y0: Int, y1: Int) {
self.y0 = y0
self.y1 = y1
}
enum Status {
case empty
case partial
case full
}
}
private struct /*Vertical*/Side: Comparable {
var x: Int
var direction: VerticalDirection
var y0: Int
var y1: Int
func fullyContains(_ segment: Segment) -> Bool {
return y0 <= segment.y0 && segment.y1 <= y1
}
static func ==(lhs: Side, rhs: Side) -> Bool {
return lhs.x == rhs.x && lhs.direction == rhs.direction && lhs.y0 == rhs.y0 && lhs.y1 == rhs.y1
}
static func <(lhs: Side, rhs: Side) -> Bool {
if lhs.x < rhs.x { return true }
if lhs.x > rhs.x { return false }
if lhs.direction.rawValue < rhs.direction.rawValue { return true }
if lhs.direction.rawValue > rhs.direction.rawValue { return false }
if lhs.y0 < rhs.y0 { return true }
if lhs.y0 > rhs.y0 { return false }
return lhs.y1 < rhs.y1
}
}
private func makeSegment(y0: Int, y1: Int) -> Int {
let index = segments.count
let segment: Segment = Segment(y0: y0, y1: y1)
segments.append(segment)
if y1 - y0 > 1 {
let mid = segment.mid
segments[index].leftChildIndex = makeSegment(y0: y0, y1: mid)
segments[index].rightChildIndex = makeSegment(y0: mid, y1: y1)
}
return index
}
private func adjustInsertionCountsOfSegmentTree(atIndex i: Int, by delta: Int, for side: Side) {
var segment = segments[i]
if side.fullyContains(segment) {
segment.insertions += delta
} else {
segment.withChildrenThatOverlap(side) { adjustInsertionCountsOfSegmentTree(atIndex: $0, by: delta, for: side) }
}
segment.status = uncachedStatus(of: segment)
segments[i] = segment
}
private func uncachedStatus(of segment: Segment) -> Segment.Status {
if segment.insertions > 0 { return .full }
if let l = segment.leftChildIndex, let r = segment.rightChildIndex {
return segments[l].status == .empty && segments[r].status == .empty ? .empty : .partial
}
return .empty
}
private func pushEmptySegmentsOfSegmentTree(atIndex i: Int, thatOverlap side: Side) {
let segment = segments[i]
switch segment.status {
case .empty where side.fullyContains(segment):
if let top = stack.last, segment.y0 == top.1 {
// segment.y0 == prior segment.y1, so merge.
stack[stack.count - 1] = (top.0, segment.y1)
} else {
stack.append((segment.y0, segment.y1))
}
case .partial, .empty:
segment.withChildrenThatOverlap(side) { pushEmptySegmentsOfSegmentTree(atIndex: $0, thatOverlap: side) }
case .full: break
}
}
private func makeSide(direction: VerticalDirection, rect: CGRect) -> Side {
let x: Int
switch direction {
case .down: x = indexOfX[rect.minX]!
case .up: x = indexOfX[rect.maxX]!
}
return Side(x: x, direction: direction, y0: indexOfY[rect.minY]!, y1: indexOfY[rect.maxY]!)
}
private func convertStackToPhase2Sides(atX x: Int, direction: VerticalDirection) {
guard stack.count > 0 else { return }
let gx = xs[x]
switch direction {
case .up:
for (y0, y1) in stack {
phase2.addVerticalSide(atX: gx, fromY: ys[y0], toY: ys[y1])
}
case .down:
for (y0, y1) in stack {
phase2.addVerticalSide(atX: gx, fromY: ys[y1], toY: ys[y0])
}
}
stack.removeAll(keepingCapacity: true)
}
}
fileprivate class AlgorithmPhase2 {
init(cornerRadius: CGFloat) {
self.cornerRadius = cornerRadius
}
let cornerRadius: CGFloat
func addVerticalSide(atX x: CGFloat, fromY y0: CGFloat, toY y1: CGFloat) {
verticalSides.append(VerticalSide(x: x, y0: y0, y1: y1))
}
func makePath() -> CGPath {
verticalSides.sort(by: { (a, b) in
if a.x < b.x { return true }
if a.x > b.x { return false }
return a.y0 < b.y0
})
var vertexes: [Vertex] = []
for (i, side) in verticalSides.enumerated() {
vertexes.append(Vertex(x: side.x, y0: side.y0, y1: side.y1, sideIndex: i, representsEnd: false))
vertexes.append(Vertex(x: side.x, y0: side.y1, y1: side.y0, sideIndex: i, representsEnd: true))
}
vertexes.sort(by: { (a, b) in
if a.y0 < b.y0 { return true }
if a.y0 > b.y0 { return false }
return a.x < b.x
})
for i in stride(from: 0, to: vertexes.count, by: 2) {
let v0 = vertexes[i]
let v1 = vertexes[i+1]
let startSideIndex: Int
let endSideIndex: Int
if v0.representsEnd {
startSideIndex = v0.sideIndex
endSideIndex = v1.sideIndex
} else {
startSideIndex = v1.sideIndex
endSideIndex = v0.sideIndex
}
precondition(verticalSides[startSideIndex].nextIndex == -1)
verticalSides[startSideIndex].nextIndex = endSideIndex
}
let path = CGMutablePath()
for i in verticalSides.indices where !verticalSides[i].emitted {
addLoop(startingAtSideIndex: i, to: path)
}
return path.copy()!
}
private var verticalSides: [VerticalSide] = []
private struct VerticalSide {
var x: CGFloat
var y0: CGFloat
var y1: CGFloat
var nextIndex = -1
var emitted = false
var isDown: Bool { return y1 < y0 }
var startPoint: CGPoint { return CGPoint(x: x, y: y0) }
var midPoint: CGPoint { return CGPoint(x: x, y: 0.5 * (y0 + y1)) }
var endPoint: CGPoint { return CGPoint(x: x, y: y1) }
init(x: CGFloat, y0: CGFloat, y1: CGFloat) {
self.x = x
self.y0 = y0
self.y1 = y1
}
}
private struct Vertex {
var x: CGFloat
var y0: CGFloat
var y1: CGFloat
var sideIndex: Int
var representsEnd: Bool
}
private func addLoop(startingAtSideIndex startIndex: Int, to path: CGMutablePath) {
var point = verticalSides[startIndex].midPoint
path.move(to: point)
var fromIndex = startIndex
repeat {
let toIndex = verticalSides[fromIndex].nextIndex
let horizontalMidpoint = CGPoint(x: 0.5 * (verticalSides[fromIndex].x + verticalSides[toIndex].x), y: verticalSides[fromIndex].y1)
path.addCorner(from: point, toward: verticalSides[fromIndex].endPoint, to: horizontalMidpoint, maxRadius: cornerRadius)
let nextPoint = verticalSides[toIndex].midPoint
path.addCorner(from: horizontalMidpoint, toward: verticalSides[toIndex].startPoint, to: nextPoint, maxRadius: cornerRadius)
verticalSides[fromIndex].emitted = true
fromIndex = toIndex
point = nextPoint
} while fromIndex != startIndex
path.closeSubpath()
}
}
fileprivate extension CGMutablePath {
func addCorner(from start: CGPoint, toward corner: CGPoint, to end: CGPoint, maxRadius: CGFloat) {
let radius = min(maxRadius, min(abs(start.x - end.x), abs(start.y - end.y)))
addArc(tangent1End: corner, tangent2End: end, radius: radius)
}
}
fileprivate enum VerticalDirection: Int {
case down = 0
case up = 1
}
有了这个,我可以在我的视图控制器中绘制你想要的圆形背景:
private func setHighlightPath() {
let textLayer = textView.layer
let textContainerInset = textView.textContainerInset
let uiInset = CGFloat(insetSlider.value)
let radius = CGFloat(radiusSlider.value)
let highlightLayer = self.highlightLayer
let layout = textView.layoutManager
let range = NSMakeRange(0, layout.numberOfGlyphs)
var rects = [CGRect]()
layout.enumerateLineFragments(forGlyphRange: range) { (_, usedRect, _, _, _) in
if usedRect.width > 0 && usedRect.height > 0 {
var rect = usedRect
rect.origin.x += textContainerInset.left
rect.origin.y += textContainerInset.top
rect = highlightLayer.convert(rect, from: textLayer)
rect = rect.insetBy(dx: uiInset, dy: uiInset)
rects.append(rect)
}
}
highlightLayer.path = CGPath.makeUnion(of: rects, cornerRadius: radius)
}
我粘贴了我为此制作的包装类。感谢 rob mayoff 的算法,它效果很好。
我知道的局限性:
方便初始化/更新签名:
init(text:String? = nil,
font:UIFont? = nil,
textColor:UIColor? = nil,
highlightColor:UIColor? = nil,
inset:CGFloat? = nil,
radius:CGFloat? = nil)
使用示例:
let stamp = Stamp()
let stamp = Stamp(text: "Whatever\nneeds to be\nstamped.")
let stamp = Stamp(text: "Placeholder that has no line breaks but wraps anyway.")
stamp.update(text: "Smaller Version",
font: UIFont.systemFont(ofSize: 15, weight: .regular),
textColor: .label,
highlightColor:.purple)
只需创建一个新文件并粘贴到此类中即可。按照说明使用。
import UIKit
import CoreGraphics
class Stamp: UIView, UITextViewDelegate {
var textView = UITextView()
private var text:String = "Place holder\nline\nbroken Stamp."
private var highlightLayer = CAShapeLayer()
private var highlightColor:CGColor = UIColor.systemOrange.cgColor
private var textColor:UIColor = UIColor.label
private var font:UIFont = UIFont.systemFont(ofSize: 35, weight: .bold)
private var inset:CGFloat = 1
private var radius:CGFloat = 1
override init(frame: CGRect) {
super.init(frame: frame)
textView.delegate = self
textView.isEditable = false
textView.isSelectable = false
textView.font = self.font
self.inset = -font.pointSize / 5
self.radius = font.pointSize / 4
self.textView.text = self.text
self.textView.textAlignment = .center
self.textView.backgroundColor = .clear
highlightLayer.backgroundColor = nil
highlightLayer.strokeColor = nil
self.layer.insertSublayer(highlightLayer, at: 0)
highlightLayer.fillColor = self.highlightColor
addSubview(textView)
textView.fillSuperview()
}
convenience init(text:String? = nil,
font:UIFont? = nil,
textColor:UIColor? = nil,
highlightColor:UIColor? = nil,
inset:CGFloat? = nil,
radius:CGFloat? = nil) {
self.init(frame: .zero)
self.update(text: text,
font: font,
textColor: textColor,
highlightColor: highlightColor,
inset: inset,
radius: radius)
}
func update(text:String? = nil,
font:UIFont? = nil,
textColor:UIColor? = nil,
highlightColor:UIColor? = nil,
inset:CGFloat? = nil,
radius:CGFloat? = nil){
if let text = text { self.text = text }
if let font = font { self.font = font }
if let textColor = textColor { self.textColor = textColor }
if let highlightColor = highlightColor { self.highlightColor = highlightColor.cgColor }
self.inset = inset ?? -self.font.pointSize / 5
self.radius = radius ?? self.font.pointSize / 4
self.textView.text = text
self.textView.textColor = self.textColor
self.textView.font = self.font
highlightLayer.fillColor = self.highlightColor
// this will re-draw the highlight
setHighlightPath()
}
override func layoutSubviews() {
super.layoutSubviews()
highlightLayer.frame = self.bounds
self.setHighlightPath()
}
func textViewDidChange(_ textView: UITextView) {
setHighlightPath()
}
private func setHighlightPath() {
let textLayer = textView.layer
let textContainerInset = textView.textContainerInset
let uiInset = CGFloat(inset)
let radius = CGFloat(radius)
let highlightLayer = self.highlightLayer
let layout = textView.layoutManager
let range = NSMakeRange(0, layout.numberOfGlyphs)
var rects = [CGRect]()
layout.enumerateLineFragments(forGlyphRange: range) { (_, usedRect, _, _, _) in
if usedRect.width > 0 && usedRect.height > 0 {
var rect = usedRect
rect.origin.x += textContainerInset.left
rect.origin.y += textContainerInset.top
rect = highlightLayer.convert(rect, from: textLayer)
rect = rect.insetBy(dx: uiInset, dy: uiInset)
rects.append(rect)
}
}
highlightLayer.path = CGPath.makeUnion(of: rects, cornerRadius: radius)
}
// Bojler
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension CGPath {
static func makeUnion(of rects: [CGRect], cornerRadius: CGFloat) -> CGPath {
let phase2 = AlgorithmPhase2(cornerRadius: cornerRadius)
_ = AlgorithmPhase1(rects: rects, phase2: phase2)
return phase2.makePath()
}
}
fileprivate func swapped<A, B>(_ pair: (A, B)) -> (B, A) { return (pair.1, pair.0) }
fileprivate class AlgorithmPhase1 {
init(rects: [CGRect], phase2: AlgorithmPhase2) {
self.phase2 = phase2
xs = Array(Set(rects.map({ $0.origin.x})).union(rects.map({ $0.origin.x + $0.size.width }))).sorted()
indexOfX = [CGFloat:Int](uniqueKeysWithValues: xs.enumerated().map(swapped))
ys = Array(Set(rects.map({ $0.origin.y})).union(rects.map({ $0.origin.y + $0.size.height }))).sorted()
indexOfY = [CGFloat:Int](uniqueKeysWithValues: ys.enumerated().map(swapped))
segments.reserveCapacity(2 * ys.count)
_ = makeSegment(y0: 0, y1: ys.count - 1)
let sides = (rects.map({ makeSide(direction: .down, rect: $0) }) + rects.map({ makeSide(direction: .up, rect: $0)})).sorted()
var priorX = 0
var priorDirection = VerticalDirection.down
for side in sides {
if side.x != priorX || side.direction != priorDirection {
convertStackToPhase2Sides(atX: priorX, direction: priorDirection)
priorX = side.x
priorDirection = side.direction
}
switch priorDirection {
case .down:
pushEmptySegmentsOfSegmentTree(atIndex: 0, thatOverlap: side)
adjustInsertionCountsOfSegmentTree(atIndex: 0, by: 1, for: side)
case .up:
adjustInsertionCountsOfSegmentTree(atIndex: 0, by: -1, for: side)
pushEmptySegmentsOfSegmentTree(atIndex: 0, thatOverlap: side)
}
}
convertStackToPhase2Sides(atX: priorX, direction: priorDirection)
}
private let phase2: AlgorithmPhase2
private let xs: [CGFloat]
private let indexOfX: [CGFloat: Int]
private let ys: [CGFloat]
private let indexOfY: [CGFloat: Int]
private var segments: [Segment] = []
private var stack: [(Int, Int)] = []
private struct Segment {
var y0: Int
var y1: Int
var insertions = 0
var status = Status.empty
var leftChildIndex: Int?
var rightChildIndex: Int?
var mid: Int { return (y0 + y1 + 1) / 2 }
func withChildrenThatOverlap(_ side: Side, do body: (_ childIndex: Int) -> ()) {
if side.y0 < mid, let l = leftChildIndex { body(l) }
if mid < side.y1, let r = rightChildIndex { body(r) }
}
init(y0: Int, y1: Int) {
self.y0 = y0
self.y1 = y1
}
enum Status {
case empty
case partial
case full
}
}
private struct /*Vertical*/Side: Comparable {
var x: Int
var direction: VerticalDirection
var y0: Int
var y1: Int
func fullyContains(_ segment: Segment) -> Bool {
return y0 <= segment.y0 && segment.y1 <= y1
}
static func ==(lhs: Side, rhs: Side) -> Bool {
return lhs.x == rhs.x && lhs.direction == rhs.direction && lhs.y0 == rhs.y0 && lhs.y1 == rhs.y1
}
static func <(lhs: Side, rhs: Side) -> Bool {
if lhs.x < rhs.x { return true }
if lhs.x > rhs.x { return false }
if lhs.direction.rawValue < rhs.direction.rawValue { return true }
if lhs.direction.rawValue > rhs.direction.rawValue { return false }
if lhs.y0 < rhs.y0 { return true }
if lhs.y0 > rhs.y0 { return false }
return lhs.y1 < rhs.y1
}
}
private func makeSegment(y0: Int, y1: Int) -> Int {
let index = segments.count
let segment: Segment = Segment(y0: y0, y1: y1)
segments.append(segment)
if y1 - y0 > 1 {
let mid = segment.mid
segments[index].leftChildIndex = makeSegment(y0: y0, y1: mid)
segments[index].rightChildIndex = makeSegment(y0: mid, y1: y1)
}
return index
}
private func adjustInsertionCountsOfSegmentTree(atIndex i: Int, by delta: Int, for side: Side) {
var segment = segments[i]
if side.fullyContains(segment) {
segment.insertions += delta
} else {
segment.withChildrenThatOverlap(side) { adjustInsertionCountsOfSegmentTree(atIndex: $0, by: delta, for: side) }
}
segment.status = uncachedStatus(of: segment)
segments[i] = segment
}
private func uncachedStatus(of segment: Segment) -> Segment.Status {
if segment.insertions > 0 { return .full }
if let l = segment.leftChildIndex, let r = segment.rightChildIndex {
return segments[l].status == .empty && segments[r].status == .empty ? .empty : .partial
}
return .empty
}
private func pushEmptySegmentsOfSegmentTree(atIndex i: Int, thatOverlap side: Side) {
let segment = segments[i]
switch segment.status {
case .empty where side.fullyContains(segment):
if let top = stack.last, segment.y0 == top.1 {
// segment.y0 == prior segment.y1, so merge.
stack[stack.count - 1] = (top.0, segment.y1)
} else {
stack.append((segment.y0, segment.y1))
}
case .partial, .empty:
segment.withChildrenThatOverlap(side) { pushEmptySegmentsOfSegmentTree(atIndex: $0, thatOverlap: side) }
case .full: break
}
}
private func makeSide(direction: VerticalDirection, rect: CGRect) -> Side {
let x: Int
switch direction {
case .down: x = indexOfX[rect.minX]!
case .up: x = indexOfX[rect.maxX]!
}
return Side(x: x, direction: direction, y0: indexOfY[rect.minY]!, y1: indexOfY[rect.maxY]!)
}
private func convertStackToPhase2Sides(atX x: Int, direction: VerticalDirection) {
guard stack.count > 0 else { return }
let gx = xs[x]
switch direction {
case .up:
for (y0, y1) in stack {
phase2.addVerticalSide(atX: gx, fromY: ys[y0], toY: ys[y1])
}
case .down:
for (y0, y1) in stack {
phase2.addVerticalSide(atX: gx, fromY: ys[y1], toY: ys[y0])
}
}
stack.removeAll(keepingCapacity: true)
}
}
fileprivate class AlgorithmPhase2 {
init(cornerRadius: CGFloat) {
self.cornerRadius = cornerRadius
}
let cornerRadius: CGFloat
func addVerticalSide(atX x: CGFloat, fromY y0: CGFloat, toY y1: CGFloat) {
verticalSides.append(VerticalSide(x: x, y0: y0, y1: y1))
}
func makePath() -> CGPath {
verticalSides.sort(by: { (a, b) in
if a.x < b.x { return true }
if a.x > b.x { return false }
return a.y0 < b.y0
})
var vertexes: [Vertex] = []
for (i, side) in verticalSides.enumerated() {
vertexes.append(Vertex(x: side.x, y0: side.y0, y1: side.y1, sideIndex: i, representsEnd: false))
vertexes.append(Vertex(x: side.x, y0: side.y1, y1: side.y0, sideIndex: i, representsEnd: true))
}
vertexes.sort(by: { (a, b) in
if a.y0 < b.y0 { return true }
if a.y0 > b.y0 { return false }
return a.x < b.x
})
for i in stride(from: 0, to: vertexes.count, by: 2) {
let v0 = vertexes[i]
let v1 = vertexes[i+1]
let startSideIndex: Int
let endSideIndex: Int
if v0.representsEnd {
startSideIndex = v0.sideIndex
endSideIndex = v1.sideIndex
} else {
startSideIndex = v1.sideIndex
endSideIndex = v0.sideIndex
}
precondition(verticalSides[startSideIndex].nextIndex == -1)
verticalSides[startSideIndex].nextIndex = endSideIndex
}
let path = CGMutablePath()
for i in verticalSides.indices where !verticalSides[i].emitted {
addLoop(startingAtSideIndex: i, to: path)
}
return path.copy()!
}
private var verticalSides: [VerticalSide] = []
private struct VerticalSide {
var x: CGFloat
var y0: CGFloat
var y1: CGFloat
var nextIndex = -1
var emitted = false
var isDown: Bool { return y1 < y0 }
var startPoint: CGPoint { return CGPoint(x: x, y: y0) }
var midPoint: CGPoint { return CGPoint(x: x, y: 0.5 * (y0 + y1)) }
var endPoint: CGPoint { return CGPoint(x: x, y: y1) }
init(x: CGFloat, y0: CGFloat, y1: CGFloat) {
self.x = x
self.y0 = y0
self.y1 = y1
}
}
private struct Vertex {
var x: CGFloat
var y0: CGFloat
var y1: CGFloat
var sideIndex: Int
var representsEnd: Bool
}
private func addLoop(startingAtSideIndex startIndex: Int, to path: CGMutablePath) {
var point = verticalSides[startIndex].midPoint
path.move(to: point)
var fromIndex = startIndex
repeat {
let toIndex = verticalSides[fromIndex].nextIndex
let horizontalMidpoint = CGPoint(x: 0.5 * (verticalSides[fromIndex].x + verticalSides[toIndex].x), y: verticalSides[fromIndex].y1)
path.addCorner(from: point, toward: verticalSides[fromIndex].endPoint, to: horizontalMidpoint, maxRadius: cornerRadius)
let nextPoint = verticalSides[toIndex].midPoint
path.addCorner(from: horizontalMidpoint, toward: verticalSides[toIndex].startPoint, to: nextPoint, maxRadius: cornerRadius)
verticalSides[fromIndex].emitted = true
fromIndex = toIndex
point = nextPoint
} while fromIndex != startIndex
path.closeSubpath()
}
}
fileprivate extension CGMutablePath {
func addCorner(from start: CGPoint, toward corner: CGPoint, to end: CGPoint, maxRadius: CGFloat) {
let radius = min(maxRadius, min(abs(start.x - end.x), abs(start.y - end.y)))
addArc(tangent1End: corner, tangent2End: end, radius: radius)
}
}
fileprivate enum VerticalDirection: Int {
case down = 0
case up = 1
}