Skip to content

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 →