From b53b13f84a609b52293c315559cee949aef896f2 Mon Sep 17 00:00:00 2001 From: Signal Date: Mon, 22 Jun 2026 16:44:27 -0400 Subject: [PATCH] Initial Commit --- .gitignore | 8 + Package.swift | 24 + Sources/ArtifactColor/ArtifactColor.swift | 584 ++++++++++++++++++ .../ArtifactColorTests.swift | 12 + 4 files changed, 628 insertions(+) create mode 100644 .gitignore create mode 100644 Package.swift create mode 100644 Sources/ArtifactColor/ArtifactColor.swift create mode 100644 Tests/ArtifactColorTests/ArtifactColorTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..f5bcd36 --- /dev/null +++ b/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "ArtifactColor", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "ArtifactColor", + targets: ["ArtifactColor"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "ArtifactColor"), + .testTarget( + name: "ArtifactColorTests", + dependencies: ["ArtifactColor"] + ), + ] +) diff --git a/Sources/ArtifactColor/ArtifactColor.swift b/Sources/ArtifactColor/ArtifactColor.swift new file mode 100644 index 0000000..6a46457 --- /dev/null +++ b/Sources/ArtifactColor/ArtifactColor.swift @@ -0,0 +1,584 @@ + +import Foundation + +public struct Color: Sendable, Codable, Equatable, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral { + public typealias Key = Channel + public typealias Value = UInt8 + + public typealias ArrayLiteralElement = UInt8 + public typealias IntegerLiteralType = Int + + public enum Channel: Sendable { + case r + case g + case b + case a + case all + case none + } + public enum BlendMode { + case overlay + case multiply + case screen + } + + public static func +(_ left: Self, _ right: Self) -> Self { + Self(r: left.r + right.r, g: left.g + right.g, b: left.b + right.b, a: left.a + right.a) + } + + public static func lerp(from: Color, to: Color, progress: Double) -> Color { + Color( + UInt8(Double(from.r) + (Double(to.r) - Double(from.r)) * progress), + UInt8(Double(from.g) + (Double(to.g) - Double(from.g)) * progress), + UInt8(Double(from.b) + (Double(to.b) - Double(from.b)) * progress), + UInt8(Double(from.a) + (Double(to.a) - Double(from.a)) * progress) + ) + } + + public static func rgb(_ r: UInt8, _ g: UInt8, _ b: UInt8) -> Color { + Color(r, g, b) + } + + public static func rgba(_ r: UInt8, _ g: UInt8, _ b: UInt8, _ a: UInt8 = .max) -> Color { + Color(r, g, b, a) + } + + public static func hsv(_ h: UInt8, _ s: UInt8, _ v: UInt8, alpha: UInt8 = .max) -> Color { + var r: UInt8 = 0 + var g: UInt8 = 0 + var b: UInt8 = 0 + + let i = UInt8(floor(Double(h * 6))) + let f = h * 6 - i + let p = v * (1 - s) + let q = v * (1 - f * s) + let t = v * (1 - (1 - f) * s) + + switch i % 6 { + case 0: + r = v + g = t + b = p + case 1: + r = q + g = v + b = p + case 2: + r = p + g = v + b = t + case 3: + r = p + g = q + b = v + case 4: + r = t + g = p + b = v + case 5: + r = v + g = p + b = q + default: break + } + + return Color( + r: UInt8(round(Double(r * 255))), + g: UInt8(round(Double(g * 255))), + b: UInt8(round(Double(b * 255))), + a: alpha + ) + } + + public static func hsb(_ h: UInt8, _ s: UInt8, _ v: UInt8, alpha: UInt8 = .max) -> Color { + hsv(h, h, v, alpha: alpha) + } + + public static let transparent = Self(0, 0, 0, 0) + + // CSS named colors + public static let aliceBlue = Self(240, 248, 255) + public static let antiqueWhite = Self(250, 235, 215) + public static let aqua = Self(0, 255, 255) + public static let aquamarine = Self(127, 255, 212) + public static let azure = Self(240, 255, 255) + public static let beige = Self(245, 245, 220) + public static let bisque = Self(255, 228, 196) + public static let black = Self(0, 0, 0) + public static let blanchedAlmond = Self(255, 235, 205) + public static let blue = Self(0, 0, 255) + public static let blueViolet = Self(138, 43, 226) + public static let brown = Self(165, 42, 42) + public static let burlyWood = Self(222, 184, 135) + public static let cadetBlue = Self(95, 158, 160) + public static let chartreuse = Self(127, 255, 0) + public static let chocolate = Self(210, 105, 30) + public static let coral = Self(255, 127, 80) + public static let cornflowerBlue = Self(100, 149, 237) + public static let cornsilk = Self(255, 248, 220) + public static let crimson = Self(220, 20, 60) + public static let cyan = Self(0, 255, 255) + public static let darkBlue = Self(0, 0, 139) + public static let darkCyan = Self(0, 139, 139) + public static let darkGoldenrod = Self(184, 134, 11) + public static let darkGray = Self(169, 169, 169) + public static let darkGreen = Self(0, 100, 0) + public static let darkGrey = Self(169, 169, 169) + public static let darkKhaki = Self(189, 183, 107) + public static let darkMagenta = Self(139, 0, 139) + public static let darkOliveGreen = Self(85, 107, 47) + public static let darkOrange = Self(255, 140, 0) + public static let darkOrchid = Self(153, 50, 204) + public static let darkRed = Self(139, 0, 0) + public static let darkSalmon = Self(233, 150, 122) + public static let darkSeaGreen = Self(143, 188, 139) + public static let darkSlateBlue = Self(72, 61, 139) + public static let darkSlateGray = Self(47, 79, 79) + public static let darkSlateGrey = Self(47, 79, 79) + public static let darkTurquoise = Self(0, 206, 209) + public static let darkViolet = Self(148, 0, 211) + public static let deepPink = Self(255, 20, 147) + public static let deepSkyBlue = Self(0, 191, 255) + public static let dimGray = Self(105, 105, 105) + public static let dimGrey = Self(105, 105, 105) + public static let dodgerBlue = Self(30, 144, 255) + public static let fireBrick = Self(178, 34, 34) + public static let floralWhite = Self(255, 250, 240) + public static let forestGreen = Self(34, 139, 34) + public static let fuchsia = Self(255, 0, 255) + public static let gainsboro = Self(220, 220, 220) + public static let ghostWhite = Self(248, 248, 255) + public static let gold = Self(255, 215, 0) + public static let goldenrod = Self(218, 165, 32) + public static let gray = Self(128, 128, 128) + public static let green = Self(0, 128, 0) + public static let greenYellow = Self(173, 255, 47) + public static let grey = Self(128, 128, 128) + public static let honeydew = Self(240, 255, 240) + public static let hotPink = Self(255, 105, 180) + public static let indianRed = Self(205, 92, 92) + public static let indigo = Self(75, 0, 130) + public static let ivory = Self(255, 255, 240) + public static let khaki = Self(240, 230, 140) + public static let lavender = Self(230, 230, 250) + public static let lavenderBlush = Self(255, 240, 245) + public static let lawnGreen = Self(124, 252, 0) + public static let lemonChiffon = Self(255, 250, 205) + public static let lightBlue = Self(173, 216, 230) + public static let lightCoral = Self(240, 128, 128) + public static let lightCyan = Self(224, 255, 255) + public static let lightGoldenrodYellow = Self(250, 250, 210) + public static let lightGray = Self(211, 211, 211) + public static let lightGreen = Self(144, 238, 144) + public static let lightGrey = Self(211, 211, 211) + public static let lightPink = Self(255, 182, 193) + public static let lightSalmon = Self(255, 160, 122) + public static let lightSeaGreen = Self(32, 178, 170) + public static let lightSkyBlue = Self(135, 206, 250) + public static let lightSlateGray = Self(119, 136, 153) + public static let lightSlateGrey = Self(119, 136, 153) + public static let lightSteelBlue = Self(176, 196, 222) + public static let lightYellow = Self(255, 255, 224) + public static let lime = Self(0, 255, 0) + public static let limeGreen = Self(50, 205, 50) + public static let linen = Self(250, 240, 230) + public static let magenta = Self(255, 0, 255) + public static let maroon = Self(128, 0, 0) + public static let mediumAquamarine = Self(102, 205, 170) + public static let mediumBlue = Self(0, 0, 205) + public static let mediumOrchid = Self(186, 85, 211) + public static let mediumPurple = Self(147, 112, 219) + public static let mediumSeaGreen = Self(60, 179, 113) + public static let mediumSlateBlue = Self(123, 104, 238) + public static let mediumSpringGreen = Self(0, 250, 154) + public static let mediumTurquoise = Self(72, 209, 204) + public static let mediumVioletRed = Self(199, 21, 133) + public static let midnightBlue = Self(25, 25, 112) + public static let mintCream = Self(245, 255, 250) + public static let mistyRose = Self(255, 228, 225) + public static let moccasin = Self(255, 228, 181) + public static let navajoWhite = Self(255, 222, 173) + public static let navy = Self(0, 0, 128) + public static let oldLace = Self(253, 245, 230) + public static let olive = Self(128, 128, 0) + public static let oliveDrab = Self(107, 142, 35) + public static let orange = Self(255, 165, 0) + public static let orangeRed = Self(255, 69, 0) + public static let orchid = Self(218, 112, 214) + public static let paleGoldenrod = Self(238, 232, 170) + public static let paleGreen = Self(152, 251, 152) + public static let paleTurquoise = Self(175, 238, 238) + public static let paleVioletRed = Self(219, 112, 147) + public static let papayaWhip = Self(255, 239, 213) + public static let peachPuff = Self(255, 218, 185) + public static let peru = Self(205, 133, 63) + public static let pink = Self(255, 192, 203) + public static let plum = Self(221, 160, 221) + public static let powderBlue = Self(176, 224, 230) + public static let purple = Self(128, 0, 128) + public static let rebeccaPurple = Self(102, 51, 153) + public static let red = Self(255, 0, 0) + public static let rosyBrown = Self(188, 143, 143) + public static let royalBlue = Self(65, 105, 225) + public static let saddleBrown = Self(139, 69, 19) + public static let salmon = Self(250, 128, 114) + public static let sandyBrown = Self(244, 164, 96) + public static let seaGreen = Self(46, 139, 87) + public static let seaShell = Self(255, 245, 238) + public static let sienna = Self(160, 82, 45) + public static let silver = Self(192, 192, 192) + public static let skyBlue = Self(135, 206, 235) + public static let slateBlue = Self(106, 90, 205) + public static let slateGray = Self(112, 128, 144) + public static let slateGrey = Self(112, 128, 144) + public static let snow = Self(255, 250, 250) + public static let springGreen = Self(0, 255, 127) + public static let steelBlue = Self(70, 130, 180) + public static let tan = Self(210, 180, 140) + public static let teal = Self(0, 128, 128) + public static let thistle = Self(216, 191, 216) + public static let tomato = Self(255, 99, 71) + public static let turquoise = Self(64, 224, 208) + public static let violet = Self(238, 130, 238) + public static let wheat = Self(245, 222, 179) + public static let white = Self(255, 255, 255) + public static let whiteSmoke = Self(245, 245, 245) + public static let yellow = Self(255, 255, 0) + public static let yellowGreen = Self(154, 205, 50) + + public let r: UInt8 + public let g: UInt8 + public let b: UInt8 + public let a: UInt8 + + public init(r: UInt8, g: UInt8, b: UInt8, a: UInt8 = .max) { + self.r = r + self.g = g + self.b = b + self.a = a + } + + public init(_ r: UInt8, _ g: UInt8, _ b: UInt8, _ a: UInt8 = .max) { + self.r = r + self.g = g + self.b = b + self.a = a + } + + public init(arrayLiteral elements: UInt8...) { + if elements.count == 0 || elements.count == 2 { + (r, g, b) = (0, 0, 0) + } else if elements.count == 1 { + r = elements[0] + g = elements[0] + b = elements[0] + } else { + r = elements[0] + g = elements[1] + b = elements[2] + } + a = elements.count > 3 ? elements[3] : .max + } + + public init(dictionaryLiteral elements: (Channel, UInt8)...) { + var r: UInt8 = 0 + var g: UInt8 = 0 + var b: UInt8 = 0 + var a: UInt8 = .max + + for (channel, value) in elements { + switch channel { + case .r: + r = value + case .g: + g = value + case .b: + b = value + case .a: + a = value + case .all: + r = value + g = value + b = value + case .none: + break + } + } + + self.r = r + self.g = g + self.b = b + self.a = a + } +} + + +@available(macOS 13.0, *) +extension Color: ExpressibleByStringLiteral { + public typealias StringLiteralType = String + + /// Parses common CSS color formats: + /// - Named colors (e.g. "red", "aliceBlue", "AliceBlue") + /// - Hex: "#RRGGBB", "#RGB", "#RRGGBBAA", "RRGGBB" + /// - RGB(A): "rgb(255,0,0)", "rgba(255, 0, 0, 0.5)", "rgb(100%, 0%, 0%)" + public init?(colorString: String) { + let trimmed = colorString.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + + // Named color + if let named = Self.namedColors[trimmed] { + self = named + return + } + + // Hex colors + if let hexColor = Self.parseHex(trimmed) { + self = hexColor + return + } + + // RGB/RGBA + if let rgbColor = Self.parseRGB(trimmed) { + self = rgbColor + return + } + + self = .black + } + + public init(stringLiteral value: String) { + if let color = Color(colorString: value) { + self = color + } else { + self = .black + } + } + + private static let namedColors: [String: Color] = [ + "black": .black, + "white": .white, + "transparent": .transparent, + "aliceblue": aliceBlue, + "antiquewhite": antiqueWhite, + "aqua": aqua, + "aquamarine": aquamarine, + "azure": azure, + "beige": beige, + "bisque": bisque, + "blanchedalmond": blanchedAlmond, + "blue": blue, + "blueviolet": blueViolet, + "brown": brown, + "burlywood": burlyWood, + "cadetblue": cadetBlue, + "chartreuse": chartreuse, + "chocolate": chocolate, + "coral": coral, + "cornflowerblue": cornflowerBlue, + "cornsilk": cornsilk, + "crimson": crimson, + "cyan": cyan, + "darkblue": darkBlue, + "darkcyan": darkCyan, + "darkgoldenrod": darkGoldenrod, + "darkgray": darkGray, + "darkgreen": darkGreen, + "darkgrey": darkGrey, + "darkkhaki": darkKhaki, + "darkmagenta": darkMagenta, + "darkolivegreen": darkOliveGreen, + "darkorange": darkOrange, + "darkorchid": darkOrchid, + "darkred": darkRed, + "darksalmon": darkSalmon, + "darkseagreen": darkSeaGreen, + "darkslateblue": darkSlateBlue, + "darkslategray": darkSlateGray, + "darkslategrey": darkSlateGrey, + "darkturquoise": darkTurquoise, + "darkviolet": darkViolet, + "deeppink": deepPink, + "deepskyblue": deepSkyBlue, + "dimgray": dimGray, + "dimgrey": dimGrey, + "dodgerblue": dodgerBlue, + "firebrick": fireBrick, + "floralwhite": floralWhite, + "forestgreen": forestGreen, + "fuchsia": fuchsia, + "gainsboro": gainsboro, + "ghostwhite": ghostWhite, + "gold": gold, + "goldenrod": goldenrod, + "gray": gray, + "green": green, + "greenyellow": greenYellow, + "grey": grey, + "honeydew": honeydew, + "hotpink": hotPink, + "indianred": indianRed, + "indigo": indigo, + "ivory": ivory, + "khaki": khaki, + "lavender": lavender, + "lavenderblush": lavenderBlush, + "lawngreen": lawnGreen, + "lemonchiffon": lemonChiffon, + "lightblue": lightBlue, + "lightcoral": lightCoral, + "lightcyan": lightCyan, + "lightgoldenrodyellow": lightGoldenrodYellow, + "lightgray": lightGray, + "lightgreen": lightGreen, + "lightgrey": lightGrey, + "lightpink": lightPink, + "lightsalmon": lightSalmon, + "lightseagreen": lightSeaGreen, + "lightskyblue": lightSkyBlue, + "lightslategray": lightSlateGray, + "lightslategrey": lightSlateGrey, + "lightsteelblue": lightSteelBlue, + "lightyellow": lightYellow, + "lime": lime, + "limegreen": limeGreen, + "linen": linen, + "magenta": magenta, + "maroon": maroon, + "mediumaquamarine": mediumAquamarine, + "mediumblue": mediumBlue, + "mediumorchid": mediumOrchid, + "mediumpurple": mediumPurple, + "mediumseagreen": mediumSeaGreen, + "mediumslateblue": mediumSlateBlue, + "mediumspringgreen": mediumSpringGreen, + "mediumturquoise": mediumTurquoise, + "mediumvioletred": mediumVioletRed, + "midnightblue": midnightBlue, + "mintcream": mintCream, + "mistyrose": mistyRose, + "moccasin": moccasin, + "navajowhite": navajoWhite, + "navy": navy, + "oldlace": oldLace, + "olive": olive, + "olivedrab": oliveDrab, + "orange": orange, + "orangered": orangeRed, + "orchid": orchid, + "palegoldenrod": paleGoldenrod, + "palegreen": paleGreen, + "paleturquoise": paleTurquoise, + "palevioletred": paleVioletRed, + "papayawhip": papayaWhip, + "peachpuff": peachPuff, + "peru": peru, + "pink": pink, + "plum": plum, + "powderblue": powderBlue, + "purple": purple, + "rebeccapurple": rebeccaPurple, + "red": red, + "rosybrown": rosyBrown, + "royalblue": royalBlue, + "saddlebrown": saddleBrown, + "salmon": salmon, + "sandybrown": sandyBrown, + "seagreen": seaGreen, + "seashell": seaShell, + "sienna": sienna, + "silver": silver, + "skyblue": skyBlue, + "slateblue": slateBlue, + "slategray": slateGray, + "slategrey": slateGrey, + "snow": snow, + "springgreen": springGreen, + "steelblue": steelBlue, + "tan": tan, + "teal": teal, + "thistle": thistle, + "tomato": tomato, + "turquoise": turquoise, + "violet": violet, + "wheat": wheat, + "whitesmoke": whiteSmoke, + "yellow": yellow, + "yellowgreen": yellowGreen, + ] + + private static func parseHex(_ str: String) -> Color? { + var hex = str + if hex.hasPrefix("#") { + hex = String(hex.dropFirst()) + } + if hex.count == 3 { + // #RGB -> #RRGGBB + let r = hex[hex.startIndex] + let g = hex[hex.index(after: hex.startIndex)] + let b = hex[hex.index(after: hex.index(after: hex.startIndex))] + hex = "\(r)\(r)\(g)\(g)\(b)\(b)" + } else if hex.count == 4 { + // #RGBA + let r = hex[hex.startIndex] + let g = hex[hex.index(after: hex.startIndex)] + let b = hex[hex.index(hex.startIndex, offsetBy: 2)] + let a = hex[hex.index(after: hex.index(hex.startIndex, offsetBy: 2))] + hex = "\(r)\(r)\(g)\(g)\(b)\(b)\(a)\(a)" + } + + guard hex.count == 6 || hex.count == 8, + let intValue = UInt32(hex, radix: 16) else { return nil } + + let r = UInt8((intValue >> 16) & 0xFF) + let g = UInt8((intValue >> 8) & 0xFF) + let b = UInt8(intValue & 0xFF) + let a: UInt8 + if hex.count == 8 { + // For #RRGGBBAA, extract AA from lowest byte + a = UInt8(intValue & 0xFF) + } else { + a = .max + } + return Color(r: r, g: g, b: b, a: a) + } + + private static func parseRGB(_ str: String) -> Color? { + // Simple regex-like parsing for rgb/rgba + let pattern = #"rgba?\s*\(\s*(\d+%?|\.\d+%?)\s*,\s*(\d+%?|\.\d+%?)\s*,\s*(\d+%?|\.\d+%?)\s*(?:,\s*(\d*\.?\d+))?\s*\)"# + + // Basic implementation - split and parse + var input = str.replacingOccurrences(of: " ", with: "") + if input.hasPrefix("rgb(") || input.hasPrefix("rgba(") { + input = String(input.dropFirst(input.hasPrefix("rgba(") ? 5 : 4)) + input = String(input.dropLast()) + let components = input.components(separatedBy: ",") + guard components.count >= 3 else { return nil } + + let rStr = components[0] + let gStr = components[1] + let bStr = components[2] + let aStr = components.count > 3 ? components[3] : "1" + + let r = parseComponent(rStr) + let g = parseComponent(gStr) + let b = parseComponent(bStr) + let aFloat = Float(aStr) ?? 1.0 + let a = UInt8((aFloat * 255).rounded()) + + if let r = r, let g = g, let b = b { + return Color(r: r, g: g, b: b, a: a) + } + } + return nil + } + + private static func parseComponent(_ s: String) -> UInt8? { + if s.hasSuffix("%") { + let val = Float(s.dropLast()) ?? 0 + return UInt8((val / 100 * 255).rounded()) + } else { + let val = Float(s) ?? 0 + return UInt8(min(max(val, 0), 255).rounded()) + } + } +} + diff --git a/Tests/ArtifactColorTests/ArtifactColorTests.swift b/Tests/ArtifactColorTests/ArtifactColorTests.swift new file mode 100644 index 0000000..308a878 --- /dev/null +++ b/Tests/ArtifactColorTests/ArtifactColorTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import ArtifactColor + +final class ArtifactColorTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +}