Skip to content

Color Palette Generator — C# source

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

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

// palette-generator — harmonious palettes from a base color.
// C# (.NET 8), stdlib only. CosmoDev polyglot port of src/lib/colorPalette.ts.
// All inputs clamped; bad hex falls back to black. Display source.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace Palette;

public static class PaletteGenerator
{
    // Parse #rgb / #rrggbb (leading # optional) to bytes; fallback black.
    public static (int R, int G, int B) HexToRgb(string hex) {
        var h = (hex ?? "").Trim().TrimStart('#');
        var ok = h.Length is 3 or 6 && h.All(Uri.IsHexDigit);
        if (!ok) return (0, 0, 0);
        if (h.Length == 3) h = $"{h[0]}{h[0]}{h[1]}{h[1]}{h[2]}{h[2]}"; // #abc
        return (Convert.ToInt32(h[..2], 16), Convert.ToInt32(h[2..4], 16),
                Convert.ToInt32(h[4..], 16));
    }

    // Round half up (TS Math.round), then clamp to a byte.
    static int ClampByte(double n) => Math.Clamp((int)Math.Floor(n + 0.5), 0, 255);

    static string RgbToHex(double r, double g, double b) =>
        string.Create(CultureInfo.InvariantCulture,
            $"#{ClampByte(r):x2}{ClampByte(g):x2}{ClampByte(b):x2}");

    // hex → HSL. h ∈ [0,360), s/l ∈ [0,100]; achromatic colors give h = 0.
    public static (double H, double S, double L) HexToHsl(string hex) {
        var (r8, g8, b8) = HexToRgb(hex);
        double r = r8 / 255.0, g = g8 / 255.0, b = b8 / 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 (h * 360, s * 100, l * 100);
    }

    // HSL → hex. C# % keeps sign, hence the double-mod true wrap.
    public static string HslToHex(double h, double s, double l) {
        double H = ((h % 360) + 360) % 360;
        double S = Math.Clamp(s, 0, 100) / 100, L = Math.Clamp(l, 0, 100) / 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 = { new[] { c, x, 0 }, new[] { x, c, 0 }, new[] { 0, c, x },
                         new[] { 0, x, c }, new[] { x, 0, c }, new[] { 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.
    public static List<string> GeneratePalette(string baseHex, string scheme, int count = 5) {
        var (h, s, l) = HexToHsl(baseHex);
        string Rot(double deg) => HslToHex(h + deg, s, l);
        string Base = HslToHex(h, s, l);
        return scheme switch {
            "complement" => new() { Base, Rot(180) },
            "split-complement" => new() { Base, Rot(150), Rot(210) },
            "analogous" => new() { Rot(-30), Base, Rot(30) },
            "triadic" => new() { Base, Rot(120), Rot(240) },
            "tetradic" => new() { Base, Rot(90), Rot(180), Rot(270) },
            "monochromatic" => Mono(h, s, l, Math.Max(1, count)),
            _ => new() { Base },
        };
    }

    static List<string> Mono(double h, double s, double l, int n) {
        double lo = Math.Max(10, l - 32), hi = Math.Min(90, l + 32); // hold h/s
        var out = new List<string>();
        for (var i = 0; i < n; i++)
            out.Add(HslToHex(h, s, n == 1 ? l : lo + (hi - lo) * i / (n - 1)));
        return out;
    }

    // n shades / tints — RGB lerp toward black / white; i runs 1..n so the
    // base color itself is never returned, only intermediate steps.
    public static List<string> Shades(string baseHex, int n) =>
        Mix(baseHex, n, (c, f) => c * (1 - f));

    public static List<string> Tints(string baseHex, int n) =>
        Mix(baseHex, n, (c, f) => c + (255 - c) * f);

    static List<string> Mix(string baseHex, int n, Func<int, double, double> mix) {
        var (r, g, b) = HexToRgb(baseHex);
        var steps = Math.Max(1, n);
        var out = new List<string>();
        for (var i = 1; i <= steps; i++) {
            var f = (double)i / (steps + 1);
            out.Add(RgbToHex(mix(r, f), mix(g, f), mix(b, f)));
        }
        return out;
    }
}

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 →