Skip to content

Color Palette Generator — Java source

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

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

// palette-generator — harmonious palettes from a base color.
// Java 17+, stdlib only. CosmoDev polyglot port of src/lib/colorPalette.ts.
// All inputs clamped; bad hex falls back to black. Display source.

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public final class PaletteGenerator {

    /** Parse #rgb / #rrggbb (leading # optional) to bytes; fallback black. */
    static int[] hexToRgb(String hex) {
        String h = hex == null ? "" : hex.trim().replaceFirst("^#", "");
        boolean ok = h.length() == 3 || h.length() == 6;
        if (ok) for (char c : h.toCharArray())
            if (Character.digit(c, 16) < 0) { ok = false; break; }
        if (!ok) return new int[]{0, 0, 0};
        if (h.length() == 3) h = "" + h.charAt(0) + h.charAt(0) + h.charAt(1)
                + h.charAt(1) + h.charAt(2) + h.charAt(2); // expand #abc
        return new int[]{Integer.parseInt(h.substring(0, 2), 16),
                Integer.parseInt(h.substring(2, 4), 16),
                Integer.parseInt(h.substring(4, 6), 16)};
    }

    /** Round half up (TS Math.round), then clamp to a byte. */
    static int clampByte(double n) {
        return (int) Math.max(0, Math.min(255, Math.floor(n + 0.5)));
    }

    static String rgbToHex(double r, double g, double b) {
        return String.format(Locale.ROOT, "#%02x%02x%02x",
                clampByte(r), clampByte(g), clampByte(b));
    }

    /** hex → HSL. h ∈ [0,360), s/l ∈ [0,100]; achromatic colors give h = 0. */
    static double[] hexToHsl(String hex) {
        int[] rgb = hexToRgb(hex);
        double r = rgb[0] / 255.0, g = rgb[1] / 255.0, b = rgb[2] / 255.0;
        double mx = Math.max(r, Math.max(g, b)), mn = Math.min(r, Math.min(g, b));
        double h = 0, s = 0, l = (mx + mn) / 2;
        if (mx != mn) {
            double d = mx - mn;
            s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
            if (mx == r) h = (g - b) / d + (g < b ? 6 : 0);
            else if (mx == g) h = (b - r) / d + 2;
            else h = (r - g) / d + 4;
            h /= 6;
        }
        return new double[]{h * 360, s * 100, l * 100};
    }

    /** HSL → hex. Java % keeps sign — hence the double-mod true wrap. */
    static String hslToHex(double h, double s, double l) {
        double H = ((h % 360) + 360) % 360;
        double S = Math.max(0, Math.min(100, s)) / 100;
        double L = Math.max(0, Math.min(100, l)) / 100;
        double c = (1 - Math.abs(2 * L - 1)) * S;
        double x = c * (1 - Math.abs(H / 60 % 2 - 1)), m = L - c / 2;
        double[][] w = {{c, x, 0}, {x, c, 0}, {0, c, x}, {0, x, c}, {x, 0, c}, {c, 0, x}};
        double[] p = w[(int) (H / 60)]; // 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. */
    static List<String> generatePalette(String baseHex, String scheme, int count) {
        double[] t = hexToHsl(baseHex);
        double h = t[0], s = t[1], l = t[2];
        String base = hslToHex(h, s, l);
        switch (scheme) {
            case "complement": return List.of(base, hslToHex(h + 180, s, l));
            case "split-complement": return List.of(base, hslToHex(h + 150, s, l),
                    hslToHex(h + 210, s, l));
            case "analogous": return List.of(hslToHex(h - 30, s, l), base,
                    hslToHex(h + 30, s, l));
            case "triadic": return List.of(base, hslToHex(h + 120, s, l),
                    hslToHex(h + 240, s, l));
            case "tetradic": return List.of(base, hslToHex(h + 90, s, l),
                    hslToHex(h + 180, s, l), hslToHex(h + 270, s, l));
            case "monochromatic": { // hold h/s, spread lightness in [10, 90]
                int n = Math.max(1, count);
                double lo = Math.max(10, l - 32), hi = Math.min(90, l + 32);
                List<String> out = new ArrayList<>();
                for (int i = 0; i < n; i++)
                    out.add(hslToHex(h, s, n == 1 ? l : lo + (hi - lo) * i / (n - 1)));
                return out;
            }
            default: return List.of(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 List<String> mix(String baseHex, int n, boolean towardWhite) {
        int[] rgb = hexToRgb(baseHex);
        int steps = Math.max(1, n);
        List<String> out = new ArrayList<>();
        for (int i = 1; i <= steps; i++) {
            double f = (double) i / (steps + 1);
            out.add(towardWhite
                    ? rgbToHex(rgb[0] + (255 - rgb[0]) * f, rgb[1] + (255 - rgb[1]) * f,
                               rgb[2] + (255 - rgb[2]) * f)
                    : rgbToHex(rgb[0] * (1 - f), rgb[1] * (1 - f), rgb[2] * (1 - f)));
        }
        return out;
    }

    static List<String> shades(String baseHex, int n) { return mix(baseHex, n, false); }
    static List<String> tints(String baseHex, int n) { return mix(baseHex, n, true); }

    private PaletteGenerator() {}
}

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 →