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++17, stdlib only. CosmoDev polyglot port of src/lib/colorPalette.ts.
// All inputs clamped; bad hex falls back to black. Display source.

#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <string>
#include <tuple>
#include <vector>

namespace palette {

using Hsl = std::tuple<double, double, double>; // h ∈ [0,360), s/l ∈ [0,100]

inline int hexVal(char c) {
    if (c >= '0' && c <= '9') return c - '0';
    return std::tolower(c) - 'a' + 10; // caller has already validated a-f
}

// Parse #rgb / #rrggbb (leading # optional) to bytes; fallback black.
inline std::tuple<int, int, int> hexToRgb(const std::string& hex) {
    std::string h = hex;
    h.erase(0, h.find_first_not_of("# \t"));
    const bool ok = !h.empty() &&
                    h.find_first_not_of("0123456789abcdefABCDEF") == std::string::npos &&
                    (h.size() == 3 || h.size() == 6);
    if (!ok) return {0, 0, 0};
    if (h.size() == 3) h = {h[0], h[0], h[1], h[1], h[2], h[2]}; // expand #abc
    return {hexVal(h[0]) * 16 + hexVal(h[1]), hexVal(h[2]) * 16 + hexVal(h[3]),
            hexVal(h[4]) * 16 + hexVal(h[5])};
}

inline int clampByte(double n) { // round half up (TS Math.round), then clamp
    return (int)std::clamp(std::floor(n + 0.5), 0.0, 255.0);
}

inline std::string rgbToHex(double r, double g, double b) {
    char buf[8];
    std::snprintf(buf, sizeof buf, "#%02x%02x%02x", clampByte(r), clampByte(g), clampByte(b));
    return buf;
}

// hex → HSL. Achromatic colors give h = 0, s = 0.
inline Hsl hexToHsl(const std::string& hex) {
    auto [r8, g8, b8] = hexToRgb(hex);
    const double r = r8 / 255.0, g = g8 / 255.0, b = b8 / 255.0;
    const double mx = std::max({r, g, b}), mn = std::min({r, g, b});
    double h = 0, s = 0;
    const double l = (mx + mn) / 2;
    if (mx != mn) {
        const 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. Double fmod = true modulo, matching ((h%360)+360)%360 in TS.
inline std::string hslToHex(double h, double s, double l) {
    const double H = std::fmod(std::fmod(h, 360) + 360, 360);
    const double S = std::clamp(s, 0.0, 100.0) / 100, L = std::clamp(l, 0.0, 100.0) / 100;
    const double c = (1 - std::abs(2 * L - 1)) * S;
    const double x = c * (1 - std::abs(std::fmod(H / 60, 2) - 1)), m = L - c / 2;
    const double w[6][3] = {{c,x,0},{x,c,0},{0,c,x},{0,x,c},{x,0,c},{c,0,x}}; // 60° sectors
    const double* p = w[(int)(H / 60)];
    return rgbToHex((p[0] + m) * 255, (p[1] + m) * 255, (p[2] + m) * 255);
}

// Generate a harmonious palette. Counts: complement 2, split-complement 3,
// analogous 3, triadic 3, tetradic 4; `count` steers only monochromatic.
inline std::vector<std::string> generatePalette(const std::string& baseHex,
                                                const std::string& scheme,
                                                int count = 5) {
    const auto [h, s, l] = hexToHsl(baseHex);
    const auto rot = [&](double deg) { return hslToHex(h + deg, s, l); };
    const std::string base = hslToHex(h, s, l);
    if (scheme == "complement") return {base, rot(180)};
    if (scheme == "split-complement") return {base, rot(150), rot(210)};
    if (scheme == "analogous") return {rot(-30), base, rot(30)};
    if (scheme == "triadic") return {base, rot(120), rot(240)};
    if (scheme == "tetradic") return {base, rot(90), rot(180), rot(270)};
    if (scheme == "monochromatic") { // hold h/s, spread lightness in [10, 90]
        const int n = std::max(1, count);
        const double lo = std::max(10.0, l - 32), hi = std::min(90.0, l + 32);
        std::vector<std::string> out;
        for (int i = 0; i < n; i++)
            out.push_back(hslToHex(h, s, n == 1 ? l : lo + (hi - lo) * i / (n - 1)));
        return out;
    }
    return {base};
}

// n shades / tints — RGB lerp toward black / white; i runs 1..n so the base
// color itself is never returned, only intermediate steps.
inline std::vector<std::string> mix(const std::string& baseHex, int n, bool towardWhite) {
    auto [r, g, b] = hexToRgb(baseHex);
    const int steps = std::max(1, n);
    std::vector<std::string> out;
    for (int i = 1; i <= steps; i++) {
        const double f = (double)i / (steps + 1);
        if (towardWhite) out.push_back(rgbToHex(r + (255 - r) * f, g + (255 - g) * f,
                                                b + (255 - b) * f));
        else out.push_back(rgbToHex(r * (1 - f), g * (1 - f), b * (1 - f)));
    }
    return out;
}

inline std::vector<std::string> shades(const std::string& baseHex, int n) {
    return mix(baseHex, n, false);
}

inline std::vector<std::string> tints(const std::string& baseHex, int n) {
    return mix(baseHex, n, true);
}

} // namespace palette

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 →