CSS Gradient Generator — Kotlin source
Build linear, radial, and conic CSS gradients with multiple color stops and positions. Live preview and copy-ready CSS.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// =============================================================================
// css-gradient-generator.kt — CosmoDev polyglot showcase port of the
// `css-gradient-generator` tool
// -----------------------------------------------------------------------------
// Language : Kotlin (1.9, standard library only)
// Source: ported from src/lib/cssGradient.ts (the canonical, live TypeScript
// lib); mirrors src/tool-sources/css-gradient-generator/{python.py,rust.rs}
// License : display source — part of CosmoDev's polyglot tool pages
// (dev.cosmolabs.org). Shown verbatim alongside the JS/TS/Go/Rust/
// Python ports and the other language ports.
// -----------------------------------------------------------------------------
// Pure CSS-gradient builder. Build linear / radial / conic CSS gradient
// strings from a small config. Deterministic and side-effect free; invalid
// input degrades gracefully (unknown colors → solid black, too few stops →
// black/white default ramp) rather than throwing.
// =============================================================================
package com.cosmolabs.cosmodev.cssgradient
import kotlin.math.floor
/** CSS gradient kinds we know how to render. */
enum class GradientType { LINEAR, RADIAL, CONIC }
/** One color anchor on the gradient ramp. Position is a percentage 0..100. */
data class GradientStop(val color: String, val position: Double)
/**
* Full input to [CssGradient.buildGradient]. [radialShape] is only meaningful
* for [GradientType.RADIAL]; `null` falls back to "circle" (mirroring the
* TypeScript `?: 'circle'` — an explicit `""` passes through unchanged).
*/
data class GradientConfig(
val type: GradientType,
val angle: Double,
val stops: List<GradientStop>,
val radialShape: String? = null,
)
/** Outcome of [CssGradient.parseColor]: an ok flag plus a human message (null when ok). */
data class ColorResult(val ok: Boolean, val error: String? = null)
object CssGradient {
/**
* Named CSS colors this tool accepts. The full CSS spec defines ~148, but
* we intentionally accept only the common, unambiguous set so output stays
* predictable (mirrors the TypeScript allow-list).
*/
private val NAMED_COLORS = setOf(
"transparent", "black", "white", "red", "green", "blue", "yellow", "orange",
"purple", "pink", "gray", "grey", "brown", "cyan", "magenta", "none", "currentcolor",
)
/** True if a char is a lowercase ASCII hex digit ('0'-'9' or 'a'-'f'). Only
* the lowercase form is accepted because [parseColor] lowercases its input
* before testing, exactly like the TS regex `[0-9a-f]`. */
private fun isHexByte(ch: Char): Boolean = ch in '0'..'9' || ch in 'a'..'f'
/** Validates a hex color by shape: '#' followed by exactly 3, 6, or 8
* lowercase hex digits. This collapses the two TS regexes
* (`#[0-9a-f]{3}([0-9a-f]{3})?` and `#[0-9a-f]{8}`) into one structural check. */
private fun isHexColor(c: String): Boolean {
val n = c.length
if (n != 4 && n != 7 && n != 9) return false // '#' + {3,6,8} digits
if (c.first() != '#') return false
return c.drop(1).all(::isHexByte)
}
/** Validates a functional color form "name(...)": the string must start
* with one of [openers] (e.g. "rgba(", "rgb("), end with ')', and have a
* nonempty body containing no ')'. Mirrors the TS `^rgba?\([^)]+\)$` /
* `^hsla?\([^)]+\)$`. Longer openers must come first so "rgba(" is tried
* before "rgb(". */
private fun isFunctionalColor(c: String, vararg openers: String): Boolean {
for (opener in openers) {
if (c.startsWith(opener)) {
if (!c.endsWith(")")) return false
val body = c.substring(opener.length, c.length - 1)
return body.isNotEmpty() && ')' !in body
}
}
return false
}
/** Validate a CSS color string.
*
* Accepts named colors, #RGB / #RRGGBB / #RRGGBBAA hex, and rgb()/rgba()/
* hsl()/hsla() functional forms. The input is trimmed and lowercased
* before testing.
*/
fun parseColor(color: String): ColorResult {
val c = color.trim().lowercase()
return when {
c.isEmpty() -> ColorResult(ok = false, error = "empty color")
c in NAMED_COLORS -> ColorResult(ok = true)
isHexColor(c) -> ColorResult(ok = true)
isFunctionalColor(c, "rgba(", "rgb(") || isFunctionalColor(c, "hsla(", "hsl(") ->
ColorResult(ok = true)
else -> ColorResult(ok = false, error = "invalid color: $color")
}
}
/** Coerce a possibly-invalid color to a safe value: valid → the trimmed
* original (casing preserved), invalid → solid black. Guarantees the
* gradient always has a usable color value. */
private fun normalizeColor(color: String): String =
if (parseColor(color).ok) color.trim() else "#000000"
/** Render a double the way JavaScript's template literal does.
*
* Kotlin's (Java's) Double.toString keeps enough digits to round-trip and
* prints whole doubles with a trailing ".0"; we strip it so 90.0 renders
* as "90", matching String(90). */
private fun formatNumber(x: Double): String {
val s = x.toString()
return if (s.endsWith(".0")) s.dropLast(2) else s
}
/** Render a complete CSS gradient string.
*
* Stops are sorted ascending by position (sortedBy is stable — TimSort
* under the hood — matching modern JavaScript's Array.sort; a NaN position
* is out of domain). Fewer than two stops collapse to a black → white
* default ramp so the output is always renderable. Positions round via
* floor(x + 0.5), which agrees with Math.round on the non-negative 0..100
* position domain. */
fun buildGradient(config: GradientConfig): String {
var stops = config.stops.sortedBy { it.position }
if (stops.size < 2) {
stops = listOf(
GradientStop(color = "#000000", position = 0.0),
GradientStop(color = "#ffffff", position = 100.0),
)
}
val stopsStr = stops.joinToString(", ") {
"${normalizeColor(it.color)} ${floor(it.position + 0.5).toInt()}%"
}
val angle = formatNumber(config.angle)
return when (config.type) {
GradientType.LINEAR -> "linear-gradient(${angle}deg, $stopsStr)"
// `?:`: null → "circle"; non-null → verbatim (even if empty).
GradientType.RADIAL -> "radial-gradient(${config.radialShape ?: "circle"}, $stopsStr)"
GradientType.CONIC -> "conic-gradient(from ${angle}deg, $stopsStr)"
}
}
}
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 →