// // HashtagHistoryView.swift // Tusker // // Created by Shadowfacts on 1/24/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class HashtagHistoryView: UIView { private var history: [Hashtag.History]? private let curveRadius: CGFloat = 10 /// The base background color used for the graph fill. var effectiveBackgroundColor = UIColor.systemBackground override func layoutSubviews() { super.layoutSubviews() createLayers() } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) createLayers() } func setHistory(_ history: [Hashtag.History]?) { if let history = history { self.history = history.sorted(by: { $0.day < $1.day }) } else { self.history = nil } createLayers() } private func createLayers() { guard let history = history, history.count >= 2 else { return } let maxUses = history.max(by: { $0.uses < $1.uses })!.uses // remove old layers if this view is being re-used layer.sublayers?.forEach { $0.removeFromSuperlayer() } let path = UIBezierPath() let widthStep = bounds.width / CGFloat(history.count - 1) let points: [CGPoint] = history.enumerated().map { (index, entry) in let x = CGFloat(index) * widthStep let yFrac = CGFloat(entry.uses) / CGFloat(maxUses) let y = (1 - yFrac) * bounds.height return CGPoint(x: x, y: y) } var gapStartPoints = [CGPoint]() var gapEndPoints = [CGPoint]() for (index, point) in points.enumerated().dropFirst().dropLast() { let prev = points[index - 1] let next = points[index + 1] let a = atan((point.y - prev.y) / widthStep) let b = atan((next.y - point.y) / widthStep) let innerAngle = .pi - a - b let gapDistance = curveRadius / sin(innerAngle / 2) let x1 = point.x - cos(a) * gapDistance let y1 = point.y - sin(a) * gapDistance gapStartPoints.append(CGPoint(x: x1, y: y1)) let x2 = point.x + cos(b) * gapDistance let y2 = point.y + sin(b) * gapDistance gapEndPoints.append(CGPoint(x: x2, y: y2)) } path.move(to: points.first!) for (index, point) in points.dropFirst().dropLast().enumerated() { path.addLine(to: gapStartPoints[index]) path.addQuadCurve(to: gapEndPoints[index], controlPoint: point) } path.addLine(to: points.last!) let borderLayer = CAShapeLayer() // copy the border path so we can continue mutating the UIBezierPath to create the fill path borderLayer.path = path.cgPath.copy()! borderLayer.strokeColor = tintColor.cgColor borderLayer.fillColor = nil borderLayer.lineWidth = 2 borderLayer.lineCap = .round path.addLine(to: CGPoint(x: bounds.width, y: bounds.height)) path.addLine(to: CGPoint(x: 0, y: bounds.height)) path.addLine(to: points.first!) let fillLayer = CAShapeLayer() fillLayer.path = path.cgPath let fillColor = self.fillColor() fillLayer.strokeColor = fillColor fillLayer.fillColor = fillColor fillLayer.lineWidth = 2 layer.addSublayer(fillLayer) layer.addSublayer(borderLayer) } // The non-transparent fill color. // We blend with the view's background color ourselves so that final color is non-transparent, // otherwise when the fill layer's border and fill overlap, there's a visibly darker patch // because transparent colors are being blended together. private func fillColor() -> CGColor { var backgroundRed: CGFloat = 0 var backgroundGreen: CGFloat = 0 var backgroundBlue: CGFloat = 0 var tintRed: CGFloat = 0 var tintGreen: CGFloat = 0 var tintBlue: CGFloat = 0 traitCollection.performAsCurrent { effectiveBackgroundColor.getRed(&backgroundRed, green: &backgroundGreen, blue: &backgroundBlue, alpha: nil) tintColor.getRed(&tintRed, green: &tintGreen, blue: &tintBlue, alpha: nil) } let blendedRed = (backgroundRed + tintRed) / 2 let blendedGreen = (backgroundGreen + tintGreen) / 2 let blendedBlue = (backgroundBlue + tintBlue) / 2 return CGColor(red: blendedRed, green: blendedGreen, blue: blendedBlue, alpha: 1) } }