Skip to content

Color Palette Generator — Kotlin source

Generate harmonious color palettes - complementary, analogous, triadic, tetradic, and monochromatic - from any base color. Export to CSS, Tailwind, or JSON.

This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.

// palette-generator — harmonious palettes from a base color.
// Kotlin 1.9+ (JVM), stdlib only. CosmoDev polyglot port of
// src/lib/colorPalette.ts. All inputs clamped; bad hex falls back to black.

import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min

object PaletteGenerator {
    private val HEX = Regex("^[0-9a-fA-F]+$")

    /** Parse #rgb / #rrggbb (leading # optional) to bytes; fallback black. */
    fun hexToRgb(hex: String?): Triple<Int, Int, Int> {
        val h = (hex ?: "").trim().removePrefix("#")
        val ok = (h.length == 3 || h.length == 6) && HEX.matches(h)
        if (!ok) return Triple(0, 0, 0)
        val s = if (h.length == 3) h.flatMap { listOf(it, it) }.joinToString("") else h
        return Triple(s.substring(0, 2).toInt(16), s.substring(2, 4).toInt(16),
                      s.substring(4).toInt(16))
    }

    /** Round half up (TS Math.round), then clamp to a byte. */
    private fun clampByte(n: Double) = floor(n + 0.5).toInt().coerceIn(0, 255)

    private fun rgbToHex(r: Double, g: Double, b: Double) =
        "#%02x%02x%02x".format(clampByte(r), clampByte(g), clampByte(b))

    /** hex → HSL. h ∈ [0,360), s/l ∈ [0,100]; achromatic colors give h = 0. */
    fun hexToHsl(hex: String): Triple<Double, Double, Double> {
        val (r8, g8, b8) = hexToRgb(hex)
        val r = r8 / 255.0; val g = g8 / 255.0; val b = b8 / 255.0
        val mx = max(r, max(g, b)); val mn = min(r, min(g, b))
        var h = 0.0; var s = 0.0; val l = (mx + mn) / 2
        if (mx != mn) {
            val d = mx - mn
            s = if (l > 0.5) d / (2 - mx - mn) else d / (mx + mn)
            h = (when (mx) {
                r -> (g - b) / d + (if (g < b) 6.0 else 0.0)
                g -> (b - r) / d + 2
                else -> (r - g) / d + 4
            }) / 6
        }
        return Triple(h * 360, s * 100, l * 100)
    }

    /** HSL → hex. Kotlin % keeps sign — hence the double-mod true wrap. */
    fun hslToHex(h: Double, s: Double, l: Double): String {
        val H = ((h % 360) + 360) % 360
        val S = s.coerceIn(0.0, 100.0) / 100; val L = l.coerceIn(0.0, 100.0) / 100
        val c = (1 - abs(2 * L - 1)) * S
        val x = c * (1 - abs(H / 60 % 2 - 1)); val m = L - c / 2
        val w = arrayOf(doubleArrayOf(c, x, 0.0), doubleArrayOf(x, c, 0.0),
                        doubleArrayOf(0.0, c, x), doubleArrayOf(0.0, x, c),
                        doubleArrayOf(x, 0.0, c), doubleArrayOf(c, 0.0, x))
        val p = w[min(5.0, H / 60).toInt()] // channel weights per 60° sector
        return rgbToHex((p[0] + m) * 255, (p[1] + m) * 255, (p[2] + m) * 255)
    }

    /** Generate a harmony. Counts: complement 2, split-complement 3, analogous
     * 3, triadic 3, tetradic 4; count steers only monochromatic. */
    fun generatePalette(baseHex: String, scheme: String, count: Int = 5): List<String> {
        val (h, s, l) = hexToHsl(baseHex)
        fun rot(deg: Double) = hslToHex(h + deg, s, l)
        val base = hslToHex(h, s, l)
        return when (scheme) {
            "complement" -> listOf(base, rot(180))
            "split-complement" -> listOf(base, rot(150), rot(210))
            "analogous" -> listOf(rot(-30), base, rot(30))
            "triadic" -> listOf(base, rot(120), rot(240))
            "tetradic" -> listOf(base, rot(90), rot(180), rot(270))
            "monochromatic" -> { // hold h/s, spread lightness in [10, 90]
                val n = max(1, count)
                val lo = max(10.0, l - 32); val hi = min(90.0, l + 32)
                List(n) { i -> hslToHex(h, s, if (n == 1) l else lo + (hi - lo) * i / (n - 1)) }
            }
            else -> listOf(base)
        }
    }

    /** n shades / tints — RGB lerp toward black / white; i runs 1..n so the
     * base color itself is never returned, only intermediate steps. */
    fun shades(baseHex: String, n: Int) = mix(baseHex, n) { c, f -> c * (1 - f) }

    fun tints(baseHex: String, n: Int) = mix(baseHex, n) { c, f -> c + (255 - c) * f }

    private inline fun mix(baseHex: String, n: Int, channel: (Int, Double) -> Double) =
        List(max(1, n)) { i ->
            val (r, g, b) = hexToRgb(baseHex); val f = (i + 1).toDouble() / (max(1, n) + 1)
            rgbToHex(channel(r, f), channel(g, f), channel(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 →