// // Color+Codable.swift // FirestoreCodableSamples // // Created by Peter Friese on 18.03.21. // import SwiftUI // Inspired by https://cocoacasts.com/from-hex-to-uicolor-and-back-in-swift // Make Color codable. This includes support for transparency. // See https://www.digitalocean.com/community/tutorials/css-hex-code-colors-alpha-values extension Color: @retroactive Codable { init(hex: String) { let rgba = hex.toRGBA() self.init(.sRGB, red: Double(rgba.r), green: Double(rgba.g), blue: Double(rgba.b), opacity: Double(rgba.alpha)) } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let hex = try container.decode(String.self) self.init(hex: hex) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(toHex) } var toHex: String? { return toHex() } func toHex(alpha: Bool = false) -> String? { guard let components = cgColor?.components, components.count >= 3 else { return nil } let r = Float(components[0]) let g = Float(components[1]) let b = Float(components[2]) var a = Float(1.0) if components.count >= 4 { a = Float(components[3]) } if alpha { return String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255)) } else { return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255)) } } } extension String { func toRGBA() -> (r: CGFloat, g: CGFloat, b: CGFloat, alpha: CGFloat) { var hexSanitized = self.trimmingCharacters(in: .whitespacesAndNewlines) hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") var rgb: UInt64 = 0 var r: CGFloat = 0.0 var g: CGFloat = 0.0 var b: CGFloat = 0.0 var a: CGFloat = 1.0 let length = hexSanitized.count Scanner(string: hexSanitized).scanHexInt64(&rgb) if length == 6 { r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 b = CGFloat(rgb & 0x0000FF) / 255.0 } else if length == 8 { r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0 g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0 b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0 a = CGFloat(rgb & 0x000000FF) / 255.0 } return (r, g, b, a) } } // MARK: - Luminance & Contrast extension Color { /// Calculate relative luminance using WCAG formula /// Returns value between 0 (darkest) and 1 (lightest) var luminance: Double { guard let components = cgColor?.components, components.count >= 3 else { return 0.5 // Default to mid-gray if can't determine } let r = components[0] let g = components[1] let b = components[2] // Apply gamma correction per WCAG 2.0 func adjust(_ component: CGFloat) -> Double { let c = Double(component) return c <= 0.03928 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4) } return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b) } /// Returns true if the color is considered "light" (needs dark text for contrast) var isLight: Bool { luminance > 0.5 } /// Returns black or white depending on which provides better contrast var contrastingTextColor: Color { isLight ? .black : .white } } extension Color: @retroactive RawRepresentable { public init?(rawValue: Int) { let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF let green = Double((rawValue & 0x00FF00) >> 8) / 0xFF let blue = Double(rawValue & 0x0000FF) / 0xFF self = Color(red: red, green: green, blue: blue) } public var rawValue: Int { let red = Int(coreImageColor.red * 255 + 0.5) let green = Int(coreImageColor.green * 255 + 0.5) let blue = Int(coreImageColor.blue * 255 + 0.5) return (red << 16) | (green << 8) | blue } private var coreImageColor: CIColor { return CIColor(color: UIColor(self)) } }