import Foundation import AsyncDisplayKit import Display private enum CornerType { case topLeft case topRight case bottomLeft case bottomRight } private func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { context.setFillColor(color.cgColor) switch type { case .topLeft: context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius))) context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0))) case .topRight: context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) case .bottomLeft: context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) case .bottomRight: context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) } } private func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { context.setFillColor(color.cgColor) switch type { case .topLeft: context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) context.setFillColor(UIColor.clear.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) case .topRight: context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius))) context.setFillColor(UIColor.clear.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) case .bottomLeft: context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) context.setFillColor(UIColor.clear.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) case .bottomRight: context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) context.setFillColor(UIColor.clear.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) } } private func generateRectsImage(color: UIColor, rects: [CGRect]) -> (CGPoint, UIImage?) { if rects.isEmpty { return (CGPoint(), nil) } let inset: CGFloat = 2.0 var topLeft = rects[0].origin var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY) for i in 1 ..< rects.count { topLeft.x = min(topLeft.x, rects[i].origin.x) topLeft.y = min(topLeft.x, rects[i].origin.y) bottomRight.x = max(bottomRight.x, rects[i].maxX) bottomRight.y = max(bottomRight.x, rects[i].maxY) } topLeft.x -= inset topLeft.y -= inset bottomRight.x += inset * 2.0 bottomRight.y += inset * 2.0 return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(color.cgColor) context.setBlendMode(.copy) let radius: CGFloat = 4.0 for i in 0 ..< rects.count { let rect = rects[i].insetBy(dx: -inset, dy: -inset) context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y)) } for i in 0 ..< rects.count { let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) var previous: CGRect? if i != 0 { previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) } var next: CGRect? if i != rects.count - 1 { next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) } if let previous = previous { if previous.contains(rect.topLeft) { if abs(rect.topLeft.x - previous.minX) >= radius { drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius) } } else { drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: radius) } if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) { if abs(rect.topRight.x - previous.maxX) >= radius { drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius) } } else { drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: radius) } } else { drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: radius) drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: radius) } if let next = next { if next.contains(rect.bottomLeft) { if abs(rect.bottomRight.x - next.maxX) >= radius { drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius) } } else { drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: radius) } if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) { if abs(rect.bottomRight.x - next.maxX) >= radius { drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius) } } else { drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: radius) } } else { drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: radius) drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: radius) } } })) } final class LinkHighlightingNode: ASDisplayNode { private var rects: [CGRect] = [] private let imageNode: ASImageNode var color: UIColor { didSet { if !self.rects.isEmpty { self.updateImage() } } } init(color: UIColor) { self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true self.imageNode.displaysAsynchronously = false self.imageNode.displayWithoutProcessing = true self.color = color super.init() self.addSubnode(self.imageNode) } func updateRects(_ rects: [CGRect]) { if self.rects != rects { self.rects = rects self.updateImage() } } private func updateImage() { if rects.isEmpty { self.imageNode.image = nil } let (offset, image) = generateRectsImage(color: self.color, rects: self.rects) if let image = image { self.imageNode.image = image self.imageNode.frame = CGRect(origin: offset, size: image.size) } } }