Initial Commit

This commit is contained in:
Signal 2026-06-22 16:44:27 -04:00
commit b53b13f84a
4 changed files with 628 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

24
Package.swift Normal file
View file

@ -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"]
),
]
)

View file

@ -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())
}
}
}

View file

@ -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
}
}