import Foundation import UIKit import Display import Accelerate public final class AvatarBadgeView: UIImageView { enum OriginalContent: Equatable { case color(UIColor) case image(UIImage) static func ==(lhs: OriginalContent, rhs: OriginalContent) -> Bool { switch lhs { case let .color(color): if case .color(color) = rhs { return true } else { return false } case let .image(lhsImage): if case let .image(rhsImage) = rhs { return lhsImage === rhsImage } else { return false } } } } private struct Parameters: Equatable { var size: CGSize var text: String var hasTimeoutIcon: Bool var useSolidColor: Bool var strokeColor: UIColor? } private var originalContent: OriginalContent? private var parameters: Parameters? private var hasContent: Bool = false override public init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(content: OriginalContent) { if self.originalContent != content || !self.hasContent { self.originalContent = content self.update() } } public func update(size: CGSize, text: String, hasTimeoutIcon: Bool = true, useSolidColor: Bool = false, strokeColor: UIColor? = nil) { let parameters = Parameters(size: size, text: text, hasTimeoutIcon: hasTimeoutIcon, useSolidColor: useSolidColor, strokeColor: strokeColor) if self.parameters != parameters || !self.hasContent { self.parameters = parameters self.update() } } private func update() { guard let originalContent = self.originalContent, let parameters = self.parameters else { return } self.hasContent = true let blurredWidth = 16 let blurredHeight = 16 guard let blurredContext = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true) else { return } let blurredSize = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) blurredContext.withContext { c in switch originalContent { case let .color(color): c.setFillColor(color.cgColor) c.fill(CGRect(origin: CGPoint(), size: blurredSize)) case let .image(image): c.setFillColor(UIColor.black.cgColor) c.fill(CGRect(origin: CGPoint(), size: blurredSize)) c.scaleBy(x: blurredSize.width / parameters.size.width, y: blurredSize.height / parameters.size.height) let offsetFactor: CGFloat = 1.0 - 0.6 let imageFrame = CGRect(origin: CGPoint(x: parameters.size.width - image.size.width + offsetFactor * parameters.size.width, y: parameters.size.height - image.size.height + offsetFactor * parameters.size.height), size: image.size) UIGraphicsPushContext(c) image.draw(in: imageFrame) UIGraphicsPopContext() } } var rSum: Int64 = 0 var gSum: Int64 = 0 var bSum: Int64 = 0 for y in 0 ..< blurredHeight { let row = blurredContext.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredContext.bytesPerRow) for x in 0 ..< blurredWidth { let pixel = row.advanced(by: x * 4) bSum += Int64(pixel.advanced(by: 0).pointee) gSum += Int64(pixel.advanced(by: 1).pointee) rSum += Int64(pixel.advanced(by: 2).pointee) } } let colorNorm = CGFloat(blurredWidth * blurredHeight) let invColorNorm: CGFloat = 1.0 / (255.0 * colorNorm) let aR = CGFloat(rSum) * invColorNorm let aG = CGFloat(gSum) * invColorNorm let aB = CGFloat(bSum) * invColorNorm let luminance: CGFloat = 0.299 * aR + 0.587 * aG + 0.114 * aB let isLightImage = luminance > 0.9 var brightness: CGFloat = 1.0 if isLightImage { brightness = 0.99 } else { brightness = 0.94 } var destinationBuffer = vImage_Buffer() destinationBuffer.width = UInt(blurredWidth) destinationBuffer.height = UInt(blurredHeight) destinationBuffer.data = blurredContext.bytes destinationBuffer.rowBytes = blurredContext.bytesPerRow vImageBoxConvolve_ARGB8888( &destinationBuffer, &destinationBuffer, nil, 0, 0, UInt32(15), UInt32(15), nil, vImage_Flags(kvImageTruncateKernel | kvImageDoNotTile) ) let divisor: Int32 = 0x1000 let rwgt: CGFloat = 0.3086 let gwgt: CGFloat = 0.6094 let bwgt: CGFloat = 0.0820 let adjustSaturation: CGFloat = 1.7 let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation let b = (1.0 - adjustSaturation) * rwgt let c = (1.0 - adjustSaturation) * rwgt let d = (1.0 - adjustSaturation) * gwgt let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation let f = (1.0 - adjustSaturation) * gwgt let g = (1.0 - adjustSaturation) * bwgt let h = (1.0 - adjustSaturation) * bwgt let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation let satMatrix: [CGFloat] = [ a, b, c, 0, d, e, f, 0, g, h, i, 0, 0, 0, 0, 1 ] let brighnessMatrix: [CGFloat] = [ brightness, 0, 0, 0, 0, brightness, 0, 0, 0, 0, brightness, 0, 0, 0, 0, 1 ] func matrixMul(a: [CGFloat], b: [CGFloat], result: inout [CGFloat]) { for i in 0 ..< 4 { for j in 0 ..< 4 { var sum: CGFloat = 0.0 for k in 0 ..< 4 { sum += a[i + k * 4] * b[k + j * 4] } result[i + j * 4] = sum } } } var resultMatrix = Array(repeating: 0.0, count: 4 * 4) matrixMul(a: satMatrix, b: brighnessMatrix, result: &resultMatrix) var matrix: [Int16] = resultMatrix.map { value in return Int16(value * CGFloat(divisor)) } vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) guard let blurredImage = blurredContext.generateImage() else { return } var solidColor: UIColor? if parameters.useSolidColor { let context = DrawingContext(size: CGSize(width: 1.0, height: 1.0), scale: 1.0, clear: false)! context.withFlippedContext({ context in if let cgImage = blurredImage.cgImage { context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)) } }) solidColor = context.colorAt(.zero) } var badgeSize = parameters.size let strokeWidth: CGFloat = 1.0 + UIScreenPixel var size = parameters.size var offset: CGPoint = .zero if parameters.strokeColor != nil { offset = CGPoint(x: strokeWidth / 2.0, y: strokeWidth / 2.0) badgeSize.width += strokeWidth badgeSize.height += strokeWidth size.width += strokeWidth * 2.0 size.height += strokeWidth * 2.0 } self.image = generateImage(size, rotatedContext: { size, context in UIGraphicsPushContext(context) context.clear(CGRect(origin: CGPoint(), size: size)) let textColor: UIColor if parameters.useSolidColor { textColor = .white } else { if isLightImage { textColor = UIColor(white: 0.7, alpha: 1.0) } else { textColor = .white } } if var solidColor { func adjustedBackgroundColor(backgroundColor: UIColor, textColor: UIColor) -> UIColor { let minContrastRatio: CGFloat = 4.5 if backgroundColor.contrastRatio(with: textColor) < minContrastRatio { var hue: CGFloat = 0 var saturation: CGFloat = 0 var brightness: CGFloat = 0 var alpha: CGFloat = 0 backgroundColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) if brightness > 0.7 { brightness = brightness * 0.9 saturation = min(saturation + 0.1, 1) } return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) } else { return backgroundColor } } solidColor = adjustedBackgroundColor(backgroundColor: solidColor, textColor: textColor) if let strokeColor = parameters.strokeColor { context.setStrokeColor(strokeColor.cgColor) context.setLineWidth(strokeWidth) } context.setFillColor(solidColor.cgColor) } else { context.setBlendMode(.copy) context.setFillColor(UIColor.black.cgColor) } if badgeSize.width != badgeSize.height { let path = UIBezierPath(roundedRect: CGRect(origin: offset, size: badgeSize), cornerRadius: badgeSize.height / 2.0) context.addPath(path.cgPath) if let _ = parameters.strokeColor { context.drawPath(using: .fillStroke) } else { context.fillPath() } } else { context.fillEllipse(in: CGRect(origin: offset, size: badgeSize)) } if let _ = solidColor { } else { blurredImage.draw(in: CGRect(origin: CGPoint(), size: badgeSize), blendMode: .sourceIn, alpha: 1.0) context.setBlendMode(.normal) } var fontSize: CGFloat = floor(parameters.size.height * 0.48) while true { let string = NSAttributedString(string: parameters.text, font: Font.bold(fontSize), textColor: textColor) let stringBounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) if stringBounds.width <= size.width - 5.0 * 2.0 || fontSize <= 2.0 { string.draw(at: CGPoint(x: stringBounds.minX + floorToScreenPixels((size.width - stringBounds.width) / 2.0), y: stringBounds.minY + floorToScreenPixels((size.height - stringBounds.height) / 2.0))) break } else { fontSize -= 1.0 } } if parameters.hasTimeoutIcon { let lineWidth: CGFloat = 1.5 let lineInset: CGFloat = 2.0 let lineRadius: CGFloat = size.width * 0.5 - lineInset - lineWidth * 0.5 context.setLineWidth(lineWidth) context.setStrokeColor(textColor.cgColor) context.setLineCap(.round) context.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: lineRadius, startAngle: CGFloat.pi * 0.5, endAngle: -CGFloat.pi * 0.5, clockwise: false) context.strokePath() let sectionAngle: CGFloat = CGFloat.pi / 11.0 for i in 0 ..< 10 { if i % 2 == 0 { continue } let startAngle = CGFloat.pi * 0.5 - CGFloat(i) * sectionAngle - sectionAngle * 0.15 let endAngle = startAngle - sectionAngle * 0.75 context.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: lineRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true) context.strokePath() } } /*if isLightImage { context.setLineWidth(UIScreenPixel) context.setStrokeColor(textColor.withMultipliedAlpha(1.0).cgColor) context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: UIScreenPixel * 0.5, dy: UIScreenPixel * 0.5)) }*/ UIGraphicsPopContext() }) } }