CSS Gradient Generator — Java source
Build linear, radial, and conic CSS gradients with multiple color stops and positions. Live preview and copy-ready CSS.
This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// =============================================================================
// css-gradient-generator.java — CosmoDev polyglot showcase port of the
// `css-gradient-generator` tool
// -----------------------------------------------------------------------------
// Language : Java (Java 17, 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 record. 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 java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/** Pure CSS-gradient builder — Java polyglot showcase port. */
public final class CssGradientGenerator {
private CssGradientGenerator() {}
/** CSS gradient kinds we know how to render. */
public enum GradientType { LINEAR, RADIAL, CONIC }
/** One color anchor on the gradient ramp. Position is a percentage 0..100. */
public record GradientStop(String color, double position) {}
/**
* Full input to {@link #buildGradient}. {@code radialShape} is only
* meaningful for {@link GradientType#RADIAL}; {@code null} falls back to
* "circle" (mirroring the TypeScript {@code ?? 'circle'} — an explicit
* {@code ""} passes through unchanged).
*/
public record GradientConfig(GradientType type, double angle,
List<GradientStop> stops, String radialShape) {}
/** Outcome of {@link #parseColor}: an ok flag plus a human message (null when ok). */
public record ColorResult(boolean ok, String error) {
public static ColorResult valid() { return new ColorResult(true, null); }
public static ColorResult invalid(String message) { return new ColorResult(false, message); }
}
/**
* 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 static final Set<String> NAMED_COLORS = Set.of(
"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 {@link #parseColor} lowercases its
* input before testing, exactly like the TS regex {@code [0-9a-f]}. */
private static boolean isHexByte(char ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f');
}
/** Validates a hex color by shape: '#' followed by exactly 3, 6, or 8
* lowercase hex digits. This collapses the two TS regexes
* ({@code #[0-9a-f]{3}([0-9a-f]{3})?} and {@code #[0-9a-f]{8}}) into one
* structural check — no regex machinery needed. */
private static boolean isHexColor(String c) {
int n = c.length();
if (n != 4 && n != 7 && n != 9) return false; // '#' + {3,6,8} digits
if (c.charAt(0) != '#') return false;
for (int i = 1; i < n; i++) {
if (!isHexByte(c.charAt(i))) return false;
}
return true;
}
/** Validates a functional color form "name(...)": the string must start
* with one of {@code openers} (e.g. "rgba(", "rgb("), end with ')', and
* have a nonempty body containing no ')'. Mirrors the TS
* {@code ^rgba?\([^)]+\)$} / {@code ^hsla?\([^)]+\)$}. Longer openers must
* come first so "rgba(" is tried before "rgb(". */
private static boolean isFunctionalColor(String c, List<String> openers) {
for (String opener : openers) {
if (c.startsWith(opener)) {
if (!c.endsWith(")")) return false;
String body = c.substring(opener.length(), c.length() - 1);
return !body.isEmpty() && !body.contains(")");
}
}
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
* (ROOT locale — color syntax is ASCII) before testing. */
public static ColorResult parseColor(String color) {
String c = (color == null ? "" : color).strip().toLowerCase(Locale.ROOT);
if (c.isEmpty()) return ColorResult.invalid("empty color");
if (NAMED_COLORS.contains(c)) return ColorResult.valid();
if (isHexColor(c)) return ColorResult.valid();
if (isFunctionalColor(c, List.of("rgba(", "rgb("))
|| isFunctionalColor(c, List.of("hsla(", "hsl("))) {
return ColorResult.valid();
}
return ColorResult.invalid("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 static String normalizeColor(String color) {
return parseColor(color).ok() ? color.strip() : "#000000";
}
/** Render a double the way JavaScript's template literal does.
*
* 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). (JDK 19+ tightened Double.toString to the strictly
* shortest form; for this tool's plain-decimal angle domain the two agree.) */
private static String formatNumber(double x) {
String s = Double.toString(x);
return s.endsWith(".0") ? s.substring(0, s.length() - 2) : s;
}
/** Render a complete CSS gradient string.
*
* Stops are sorted ascending by position (stable — List.sort uses TimSort,
* matching modern JavaScript's Array.sort; a NaN position — out of domain —
* sorts last per Double.compare). 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. */
public static String buildGradient(GradientConfig config) {
List<GradientStop> stops = new ArrayList<>(config.stops());
stops.sort(Comparator.comparingDouble(GradientStop::position));
if (stops.size() < 2) {
stops = new ArrayList<>(List.of(
new GradientStop("#000000", 0.0),
new GradientStop("#ffffff", 100.0)));
}
StringBuilder stopsStr = new StringBuilder();
for (int i = 0; i < stops.size(); i++) {
GradientStop s = stops.get(i);
if (i > 0) stopsStr.append(", ");
long rounded = (long) Math.floor(s.position() + 0.5);
stopsStr.append(normalizeColor(s.color())).append(' ').append(rounded).append('%');
}
String angle = formatNumber(config.angle());
switch (config.type()) {
case LINEAR:
return "linear-gradient(" + angle + "deg, " + stopsStr + ")";
case RADIAL: {
// `?? 'circle'`: null → "circle"; non-null → verbatim (even if empty).
String shape = config.radialShape() != null ? config.radialShape() : "circle";
return "radial-gradient(" + shape + ", " + stopsStr + ")";
}
case CONIC:
return "conic-gradient(from " + angle + "deg, " + stopsStr + ")";
default:
return ""; // unreachable for the enum — mirrors the TS fall-through
}
}
}
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 →