Color Palette Generator — Swift source
Generate harmonious color palettes - complementary, analogous, triadic, tetradic, and monochromatic - from any base color. Export to CSS, Tailwind, or JSON.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// palette-generator — harmonious palettes from a base color.
// Swift 5.9+, stdlib only (no Foundation). CosmoDev polyglot port of
// src/lib/colorPalette.ts. All inputs clamped; bad hex falls back to black.
enum PaletteGenerator {
static func hexVal(_ c: Character) -> Int? {
switch c {
case "0"..."9": return c.wholeNumberValue
case "a"..."f": return c.asciiValue.map { Int($0 - 97 + 10) }
case "A"..."F": return c.asciiValue.map { Int($0 - 65 + 10) }
default: return nil
}
}
/// Parse #rgb / #rrggbb (leading # optional); fallback black.
static func hexToRgb(_ hex: String) -> (Double, Double, Double) {
var h = Array(hex.drop { $0 == "#" || $0 == " " }) // leading # / spaces
let ok = (h.count == 3 || h.count == 6) && h.allSatisfy { hexVal($0) != nil }
guard ok else { return (0, 0, 0) }
if h.count == 3 { h = h.flatMap { [$0, $0] } } // expand #abc
let v: (Int) -> Double = { Double(hexVal(h[$0])! * 16 + hexVal(h[$0 + 1])!) }
return (v(0), v(2), v(4))
}
/// Round half up (TS Math.round), then clamp to a byte.
static func clampByte(_ n: Double) -> Int {
min(255, max(0, Int((n + 0.5).rounded(.down))))
}
static func rgbToHex(_ r: Double, _ g: Double, _ b: Double) -> String {
let digits = Array("0123456789abcdef")
func byte(_ n: Double) -> String { // two lowercase hex digits
let v = clampByte(n)
return String(digits[v / 16]) + String(digits[v % 16])
}
return "#" + byte(r) + byte(g) + byte(b)
}
/// hex -> HSL. h in [0,360), s/l in [0,100]; achromatic gives h = 0.
static func hexToHsl(_ hex: String) -> (h: Double, s: Double, l: Double) {
let t = hexToRgb(hex)
let r = t.0 / 255, g = t.1 / 255, b = t.2 / 255
let mx = max(r, g, b), mn = min(r, g, b), l = (mx + mn) / 2
var h = 0.0, s = 0.0
if mx != mn {
let d = mx - mn
s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn)
switch mx {
case r: h = (g - b) / d + (g < b ? 6 : 0)
case g: h = (b - r) / d + 2
default: h = (r - g) / d + 4
}
h /= 6
}
return (h * 360, s * 100, l * 100)
}
/// HSL -> hex. truncatingRemainder keeps sign, hence the double-mod wrap.
static func hslToHex(_ h: Double, _ s: Double, _ l: Double) -> String {
let H = (h.truncatingRemainder(dividingBy: 360) + 360)
.truncatingRemainder(dividingBy: 360)
let S = min(100, max(0, s)) / 100, L = min(100, max(0, l)) / 100
let c = (1 - abs(2 * L - 1)) * S
// H is non-negative here, so truncatingRemainder equals a true modulo.
let x = c * (1 - abs((H / 60).truncatingRemainder(dividingBy: 2) - 1)), m = L - c / 2
let w: [(Double, Double, Double)] = [(c, x, 0), (x, c, 0), (0, c, x),
(0, x, c), (x, 0, c), (c, 0, x)]
let (r, g, b) = w[min(5, Int(H / 60))] // weights per 60° sector
return rgbToHex((r + m) * 255, (g + m) * 255, (b + m) * 255)
}
/// Generate a harmony. Counts: complement 2, split-complement 3,
/// analogous 3, triadic 3, tetradic 4; count steers only monochromatic.
static func generatePalette(_ baseHex: String, _ scheme: String, count: Int = 5) -> [String] {
let (h, s, l) = hexToHsl(baseHex)
func rot(_ deg: Double) -> String { hslToHex(h + deg, s, l) }
let base = hslToHex(h, s, l)
switch scheme {
case "complement": return [base, rot(180)]
case "split-complement": return [base, rot(150), rot(210)]
case "analogous": return [rot(-30), base, rot(30)]
case "triadic": return [base, rot(120), rot(240)]
case "tetradic": return [base, rot(90), rot(180), rot(270)]
case "monochromatic":
let n = max(1, count), lo = max(10, l - 32), hi = min(90, l + 32)
return (0..<n).map {
hslToHex(h, s, n == 1 ? l : lo + (hi - lo) * Double($0) / Double(n - 1))
}
default: return [base]
}
}
/// n shades / tints — RGB lerp toward black / white; i runs 1...n so the
/// base color itself is never returned, only intermediate steps.
static func shades(_ baseHex: String, _ n: Int) -> [String] {
mix(baseHex, n) { c, f in c * (1 - f) }
}
static func tints(_ baseHex: String, _ n: Int) -> [String] {
mix(baseHex, n) { c, f in c + (255 - c) * f }
}
private static func mix(_ baseHex: String, _ n: Int, _ ch: (Double, Double) -> Double) -> [String] {
let (r, g, b) = hexToRgb(baseHex)
let steps = max(1, n)
return (1...steps).map { i in
let f = Double(i) / Double(steps + 1)
return rgbToHex(ch(r, f), ch(g, f), ch(b, f))
}
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →