Skip to content

CSS Gradient Generator — C++ source

Build linear, radial, and conic CSS gradients with multiple color stops and positions. Live preview and copy-ready CSS.

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

// =============================================================================
//  css-gradient-generator.cpp — CosmoDev polyglot showcase port of the
//  `css-gradient-generator` tool
//  -----------------------------------------------------------------------------
//  Language : C++ (C++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 struct. Deterministic and side-effect free;
//  invalid input degrades gracefully (unknown colors → solid black, too few
//  stops → black/white default ramp) rather than throwing.
// =============================================================================

#include <algorithm>
#include <cctype>
#include <charconv>
#include <cmath>
#include <cstddef>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace css_gradient {

/// 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.
struct GradientStop {
    std::string color;
    double position;
};

/// Full input to build_gradient. `radial_shape` is only meaningful for
/// GradientType::Radial; std::nullopt falls back to "circle" (mirroring the
/// TypeScript `?? 'circle'` — an explicit "" passes through unchanged).
struct GradientConfig {
    GradientType type;
    double angle;
    std::vector<GradientStop> stops;
    std::optional<std::string> radial_shape;
};

/// Outcome of parse_color: an ok flag plus a human message (nullopt when ok).
struct ColorResult {
    bool ok;
    std::optional<std::string> error;
};

/// 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).
inline constexpr std::string_view kNamedColors[] = {
    "transparent", "black", "white", "red", "green", "blue", "yellow", "orange",
    "purple", "pink", "gray", "grey", "brown", "cyan", "magenta", "none", "currentcolor",
};

/// True if a byte is a lowercase ASCII hex digit (`0-9` or `a-f`). Only the
/// lowercase form is accepted because parse_color lowercases its input before
/// testing, exactly like the TS regex `[0-9a-f]`.
inline bool is_hex_byte(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 (`#[0-9a-f]{3}([0-9a-f]{3})?`
/// and `#[0-9a-f]{8}`) into one structural check — no <regex> needed.
inline bool is_hex_color(std::string_view c)
{
    const bool valid_len = c.size() == 4 || c.size() == 7 || c.size() == 9; // '#' + {3,6,8} digits
    return valid_len && c.front() == '#' && std::all_of(c.begin() + 1, c.end(), is_hex_byte);
}

/// C++17 has no string_view::starts_with (that is C++20) — tiny local helper.
inline bool starts_with(std::string_view s, std::string_view prefix)
{
    return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0;
}

/// 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(".
inline bool is_functional_color(std::string_view c, std::initializer_list<std::string_view> openers)
{
    for (std::string_view opener : openers) {
        if (starts_with(c, opener)) {
            if (c.back() != ')') return false; // c starts with opener ⇒ non-empty
            std::string_view body = c.substr(opener.size(), c.size() - opener.size() - 1);
            return !body.empty() && body.find(')') == std::string_view::npos;
        }
    }
    return false;
}

/// Trim leading/trailing whitespace (ASCII), mirroring JS String.prototype.trim().
inline std::string_view trim(std::string_view s)
{
    while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) s.remove_prefix(1);
    while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) s.remove_suffix(1);
    return s;
}

/// Lowercase ASCII in place (color syntax is ASCII; std::tolower needs the
/// unsigned-char cast to avoid UB on signed chars).
inline std::string to_lower_ascii(std::string s)
{
    std::transform(s.begin(), s.end(), s.begin(), [](unsigned char ch) {
        return static_cast<char>(std::tolower(ch));
    });
    return s;
}

/// 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.
inline ColorResult parse_color(std::string_view color)
{
    const std::string c = to_lower_ascii(std::string(trim(color)));
    if (c.empty()) return { false, std::string("empty color") };
    for (std::string_view name : kNamedColors) {
        if (name == c) return { true, std::nullopt };
    }
    if (is_hex_color(c)) return { true, std::nullopt };
    if (is_functional_color(c, { "rgba(", "rgb(" }) || is_functional_color(c, { "hsla(", "hsl(" })) {
        return { true, std::nullopt };
    }
    return { false, "invalid color: " + std::string(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.
inline std::string normalize_color(std::string_view color)
{
    return parse_color(color).ok ? std::string(trim(color)) : std::string("#000000");
}

/// Render a double the way JavaScript's template literal does — shortest
/// round-tripping decimal, with no trailing ".0" on whole numbers (so 90.0
/// becomes "90", matching String(90)). std::to_chars (C++17) emits exactly the
/// shortest round-tripping form, so no zero-stripping is needed.
inline std::string format_number(double x)
{
    char buf[32];
    auto [end, ec] = std::to_chars(buf, buf + sizeof buf, x);
    (void)ec; // cannot fail: 32 bytes always fit a shortest-form double
    return std::string(buf, end);
}

/// Render a complete CSS gradient string.
///
/// Stops are sorted ascending by position (std::stable_sort, matching modern
/// JavaScript's Array.sort; a NaN position — out of domain — compares "not
/// less" against everything and is treated as equivalent). 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.
inline std::string build_gradient(const GradientConfig& config)
{
    std::vector<GradientStop> stops = config.stops;
    std::stable_sort(stops.begin(), stops.end(), [](const GradientStop& a, const GradientStop& b) {
        return a.position < b.position;
    });

    if (stops.size() < 2) {
        stops = { { "#000000", 0.0 }, { "#ffffff", 100.0 } };
    }

    std::string stops_str;
    for (std::size_t i = 0; i < stops.size(); ++i) {
        if (i != 0) stops_str += ", ";
        stops_str += normalize_color(stops[i].color);
        stops_str += ' ';
        stops_str += std::to_string(static_cast<long>(std::floor(stops[i].position + 0.5)));
        stops_str += '%';
    }

    const std::string angle = format_number(config.angle);

    switch (config.type) {
    case GradientType::Linear:
        return "linear-gradient(" + angle + "deg, " + stops_str + ")";
    case GradientType::Radial: {
        // `?? 'circle'`: nullopt → "circle"; a value → verbatim (even if empty).
        const std::string shape = config.radial_shape.value_or("circle");
        return "radial-gradient(" + shape + ", " + stops_str + ")";
    }
    case GradientType::Conic:
        return "conic-gradient(from " + angle + "deg, " + stops_str + ")";
    }
    return ""; // unreachable — mirrors the TypeScript fall-through
}

} // namespace css_gradient

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 →