Color Picker & Converter — C++ source
Pick a color and convert between HEX, RGB, HSL, HSV, and CMYK with a live preview. Edit any format and copy the rest - runs entirely in your browser.
This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.
// color-picker — color-space conversions, WCAG contrast, and named-color lookup.
//
// Language: C++17 (C++ standard library only — no ecosystem dependencies).
//
// Source: CosmoDev polyglot showcase port of the color-picker tool, ported
// from src/lib/colorConvert.ts. Functionally equivalent: identical
// outputs for identical inputs, including clamping, NaN/Infinity
// handling, and std::nullopt on invalid hex.
//
// RGB is the canonical hub: every other space converts through it, and
// normalize_color() re-derives the hex from its own clamped RGB so the five
// display formats can never disagree.
//
// License: display source — part of CosmoDev's polyglot tool pages.
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <iomanip>
#include <limits>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace color_picker {
/// A color resolved into every supported space, all mutually consistent.
struct ColorBundle {
std::string hex; ///< #rrggbb (lowercase) — the canonical handle
std::array<double, 3> rgb; ///< 0–255 each
std::array<double, 3> hsl; ///< h: 0–360, s/l: 0–100
std::array<double, 3> hsv; ///< h: 0–360, s/v: 0–100
std::array<double, 4> cmyk; ///< 0–100 each
};
/// A named CSS color with its hex.
struct NamedColor {
std::string_view name;
std::string_view hex;
};
namespace detail {
// Every public function is total: invalid hex → nullopt; out-of-range numbers
// are clamped into their valid interval. Comparisons would propagate NaN, so
// NaN is handled explicitly first — corrupt input degrades to the lowest valid
// value instead of poisoning the result.
inline double clamp_range(double n, double lo, double hi) {
const double inf = std::numeric_limits<double>::infinity();
if (std::isnan(n)) return lo;
if (n == inf) return hi;
if (n == -inf) return lo;
return std::clamp(n, lo, hi);
}
inline double clamp01(double n) { return clamp_range(n, 0.0, 1.0); }
// Round half away from zero — mirrors JS Math.round for the non-negative
// values this module rounds. std::round would match too; the explicit
// floor(n + 0.5) form documents the parity with the TypeScript source.
inline double round_half_up(double n) { return std::floor(n + 0.5); }
// Normalize any hue (negative, >360, or NaN) into [0, 360), matching
// JS ((Number(h) || 0) % 360 + 360) % 360. std::fmod takes the dividend's
// sign, like JS %.
inline double mod360(double h) {
if (std::isnan(h)) h = 0.0;
return std::fmod(std::fmod(h, 360.0) + 360.0, 360.0);
}
inline bool is_hex_digits(std::string_view s) {
if (s.empty()) return false;
return std::all_of(s.begin(), s.end(), [](unsigned char c) {
return std::isxdigit(c) != 0;
});
}
inline int hex_val(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return c - 'A' + 10;
}
/// Parse exactly two validated ASCII hex digits into 0–255.
inline double parse_hex_byte(std::string_view s) {
return static_cast<double>((hex_val(s[0]) << 4) | hex_val(s[1]));
}
} // namespace detail
// --- HEX ↔ RGB ---------------------------------------------------------------
/// Parse "#rgb" / "#rrggbb" (case-insensitive, "#" optional) into {r,g,b}, or
/// nullopt for anything that is not a 3- or 6-digit hex color.
inline std::optional<std::array<double, 3>> hex_to_rgb(const std::string& hex) {
std::string h = hex;
// Trim ASCII whitespace from both ends, mirroring Python str.strip().
const auto not_space = [](unsigned char c) { return std::isspace(c) == 0; };
h.erase(h.begin(), std::find_if(h.begin(), h.end(), not_space));
h.erase(std::find_if(h.rbegin(), h.rend(), not_space).base(), h.end());
// TS strips exactly one leading '#'.
if (!h.empty() && h.front() == '#') h.erase(0, 1);
// Expand shorthand (#rgb → #rrggbb) only when it is exactly three hex digits.
if (h.size() == 3 && detail::is_hex_digits(h)) {
const char c0 = h[0], c1 = h[1], c2 = h[2];
h = {c0, c0, c1, c1, c2, c2};
}
if (h.size() != 6 || !detail::is_hex_digits(h)) return std::nullopt;
return std::array<double, 3>{
detail::parse_hex_byte(std::string_view(h).substr(0, 2)),
detail::parse_hex_byte(std::string_view(h).substr(2, 2)),
detail::parse_hex_byte(std::string_view(h).substr(4, 2)),
};
}
/// {r,g,b} (clamped to 0–255) → "#rrggbb" (lowercase, zero-padded).
inline std::string rgb_to_hex(double r, double g, double b) {
std::ostringstream out;
out << '#';
for (const double n : {r, g, b}) {
const auto v = static_cast<unsigned>(detail::round_half_up(detail::clamp_range(n, 0.0, 255.0)));
out << std::hex << std::setw(2) << std::setfill('0') << v;
}
return out.str();
}
// --- RGB ↔ HSL ---------------------------------------------------------------
/// {r,g,b} (0–255) → {h,s,l} with h: 0–360, s/l: 0–100.
inline std::array<double, 3> rgb_to_hsl(double r, double g, double b) {
const double rn = detail::clamp01(r / 255.0);
const double gn = detail::clamp01(g / 255.0);
const double bn = detail::clamp01(b / 255.0);
const double mx = std::max({rn, gn, bn});
const double mn = std::min({rn, gn, bn});
const double d = mx - mn;
const double l = (mx + mn) / 2.0;
double h = 0.0;
double s = 0.0;
if (d != 0.0) {
// Saturation formula branches on which half of the lightness axis we sit on.
s = (l > 0.5) ? d / (2.0 - mx - mn) : d / (mx + mn);
if (mx == rn) h = (gn - bn) / d + (gn < bn ? 6.0 : 0.0);
else if (mx == gn) h = (bn - rn) / d + 2.0;
else h = (rn - gn) / d + 4.0;
h *= 60.0;
}
return {detail::round_half_up(h), detail::round_half_up(s * 100.0), detail::round_half_up(l * 100.0)};
}
/// {h,s,l} (h: 0–360, s/l: 0–100) → {r,g,b} (0–255).
inline std::array<double, 3> hsl_to_rgb(double h, double s, double l) {
const double hn = detail::mod360(h);
const double sn = detail::clamp01(s / 100.0);
const double ln = detail::clamp01(l / 100.0);
const double c = (1.0 - std::abs(2.0 * ln - 1.0)) * sn;
const double x = c * (1.0 - std::abs(std::fmod(hn / 60.0, 2.0) - 1.0));
const double m = ln - c / 2.0;
double rr, gg, bb;
if (hn < 60.0) {
rr = c; gg = x; bb = 0.0;
} else if (hn < 120.0) {
rr = x; gg = c; bb = 0.0;
} else if (hn < 180.0) {
rr = 0.0; gg = c; bb = x;
} else if (hn < 240.0) {
rr = 0.0; gg = x; bb = c;
} else if (hn < 300.0) {
rr = x; gg = 0.0; bb = c;
} else {
rr = c; gg = 0.0; bb = x;
}
return {(rr + m) * 255.0, (gg + m) * 255.0, (bb + m) * 255.0};
}
/// "#hex" → {h,s,l}, or nullopt when the hex is invalid.
inline std::optional<std::array<double, 3>> hex_to_hsl(const std::string& hex) {
const auto rgb = hex_to_rgb(hex);
if (!rgb) return std::nullopt;
return rgb_to_hsl((*rgb)[0], (*rgb)[1], (*rgb)[2]);
}
/// {h,s,l} → "#rrggbb".
inline std::string hsl_to_hex(double h, double s, double l) {
const auto [r, g, b] = hsl_to_rgb(h, s, l);
return rgb_to_hex(r, g, b);
}
// --- RGB ↔ HSV ---------------------------------------------------------------
/// {r,g,b} (0–255) → {h,s,v} with h: 0–360, s/v: 0–100.
inline std::array<double, 3> rgb_to_hsv(double r, double g, double b) {
const double rn = detail::clamp01(r / 255.0);
const double gn = detail::clamp01(g / 255.0);
const double bn = detail::clamp01(b / 255.0);
const double mx = std::max({rn, gn, bn});
const double mn = std::min({rn, gn, bn});
const double d = mx - mn;
double h = 0.0;
if (d != 0.0) {
if (mx == rn) h = (gn - bn) / d + (gn < bn ? 6.0 : 0.0);
else if (mx == gn) h = (bn - rn) / d + 2.0;
else h = (rn - gn) / d + 4.0;
h *= 60.0;
}
const double s = (mx == 0.0) ? 0.0 : d / mx;
return {detail::round_half_up(h), detail::round_half_up(s * 100.0), detail::round_half_up(mx * 100.0)};
}
/// {h,s,v} (h: 0–360, s/v: 0–100) → {r,g,b} (0–255).
inline std::array<double, 3> hsv_to_rgb(double h, double s, double v) {
const double hn = detail::mod360(h);
const double sn = detail::clamp01(s / 100.0);
const double vn = detail::clamp01(v / 100.0);
const double c = vn * sn;
const double x = c * (1.0 - std::abs(std::fmod(hn / 60.0, 2.0) - 1.0));
const double m = vn - c;
double rr, gg, bb;
if (hn < 60.0) {
rr = c; gg = x; bb = 0.0;
} else if (hn < 120.0) {
rr = x; gg = c; bb = 0.0;
} else if (hn < 180.0) {
rr = 0.0; gg = c; bb = x;
} else if (hn < 240.0) {
rr = 0.0; gg = x; bb = c;
} else if (hn < 300.0) {
rr = x; gg = 0.0; bb = c;
} else {
rr = c; gg = 0.0; bb = x;
}
return {(rr + m) * 255.0, (gg + m) * 255.0, (bb + m) * 255.0};
}
/// {h,s,v} → "#rrggbb".
inline std::string hsv_to_hex(double h, double s, double v) {
const auto [r, g, b] = hsv_to_rgb(h, s, v);
return rgb_to_hex(r, g, b);
}
// --- RGB ↔ CMYK --------------------------------------------------------------
/// {r,g,b} (0–255) → {c,m,y,k} (0–100 each).
inline std::array<double, 4> rgb_to_cmyk(double r, double g, double b) {
const double rn = detail::clamp01(r / 255.0);
const double gn = detail::clamp01(g / 255.0);
const double bn = detail::clamp01(b / 255.0);
const double k = 1.0 - std::max({rn, gn, bn});
if (k == 1.0) {
return {0.0, 0.0, 0.0, 100.0}; // pure black — avoid divide-by-zero
}
const double c = (1.0 - rn - k) / (1.0 - k);
const double m = (1.0 - gn - k) / (1.0 - k);
const double y = (1.0 - bn - k) / (1.0 - k);
return {
detail::round_half_up(c * 100.0),
detail::round_half_up(m * 100.0),
detail::round_half_up(y * 100.0),
detail::round_half_up(k * 100.0),
};
}
/// {c,m,y,k} (0–100 each) → {r,g,b} (0–255).
inline std::array<double, 3> cmyk_to_rgb(double c, double m, double y, double k) {
const double cn = detail::clamp01(c / 100.0);
const double mn = detail::clamp01(m / 100.0);
const double yn = detail::clamp01(y / 100.0);
const double kn = detail::clamp01(k / 100.0);
return {
255.0 * (1.0 - cn) * (1.0 - kn),
255.0 * (1.0 - mn) * (1.0 - kn),
255.0 * (1.0 - yn) * (1.0 - kn),
};
}
/// {c,m,y,k} → "#rrggbb".
inline std::string cmyk_to_hex(double c, double m, double y, double k) {
const auto [r, g, b] = cmyk_to_rgb(c, m, y, k);
return rgb_to_hex(r, g, b);
}
// --- Round-robin normalizer --------------------------------------------------
/// Resolve any hex into one consistent ColorBundle: the hex is re-derived from
/// its own clamped RGB, then HSL/HSV/CMYK are all computed from that same RGB.
/// This is the single funnel the UI routes every edit through, so the five
/// display formats can never disagree. Returns nullopt for invalid hex.
inline std::optional<ColorBundle> normalize_color(const std::string& hex) {
const auto rgb = hex_to_rgb(hex);
if (!rgb) return std::nullopt;
const auto [r, g, b] = *rgb;
return ColorBundle{
rgb_to_hex(r, g, b),
*rgb,
rgb_to_hsl(r, g, b),
rgb_to_hsv(r, g, b),
rgb_to_cmyk(r, g, b),
};
}
// --- WCAG luminance, contrast & text suggestion ------------------------------
/// Linearize a single sRGB channel (0–255) per the WCAG 2.x relative-luminance
/// definition.
inline double srgb_channel(double c) {
const double s = detail::clamp01(c / 255.0);
if (s <= 0.03928) return s / 12.92;
return std::pow((s + 0.055) / 1.055, 2.4);
}
/// WCAG 2.x relative luminance of a hex (0 = black, 1 = white), or nullopt.
inline std::optional<double> relative_luminance(const std::string& hex) {
const auto rgb = hex_to_rgb(hex);
if (!rgb) return std::nullopt;
return 0.2126 * srgb_channel((*rgb)[0])
+ 0.7152 * srgb_channel((*rgb)[1])
+ 0.0722 * srgb_channel((*rgb)[2]);
}
/// WCAG contrast ratio between two hexes (1–21), or nullopt if either is invalid.
inline std::optional<double> contrast_ratio(const std::string& a, const std::string& b) {
const auto la = relative_luminance(a);
const auto lb = relative_luminance(b);
if (!la || !lb) return std::nullopt;
const double hi = std::max(*la, *lb);
const double lo = std::min(*la, *lb);
return (hi + 0.05) / (lo + 0.05);
}
/// Pick black or white text for maximum legibility on hex, or nullopt if invalid.
inline std::optional<std::string_view> suggest_text_hex(const std::string& hex) {
const auto l = relative_luminance(hex);
if (!l) return std::nullopt;
return *l > 0.179 ? "#000000" : "#ffffff";
}
// --- Closest named CSS color -------------------------------------------------
// A curated set of well-known CSS named colors. Kept intentionally to entries
// whose hex is verifiable from memory — exhaustive tables typed by hand risk
// shipping wrong data, which unit tests cannot catch.
inline constexpr std::pair<std::string_view, std::string_view> NAMED_COLORS_RAW[] = {
{"black", "#000000"}, {"dim gray", "#696969"},
{"gray", "#808080"}, {"dark gray", "#a9a9a9"},
{"silver", "#c0c0c0"}, {"light gray", "#d3d3d3"},
{"gainsboro", "#dcdcdc"}, {"white smoke", "#f5f5f5"},
{"white", "#ffffff"}, {"snow", "#fffafa"},
{"ivory", "#fffff0"}, {"seashell", "#fff5ee"},
{"red", "#ff0000"}, {"crimson", "#dc143c"},
{"dark red", "#8b0000"},
{"firebrick", "#b22222"}, {"indian red", "#cd5c5c"},
{"salmon", "#fa8072"}, {"tomato", "#ff6347"},
{"coral", "#ff7f50"}, {"orange", "#ffa500"},
{"dark orange", "#ff8c00"}, {"gold", "#ffd700"},
{"chocolate", "#d2691e"}, {"brown", "#a52a2a"},
{"sienna", "#a0522d"}, {"tan", "#d2b48c"},
{"yellow", "#ffff00"}, {"khaki", "#f0e68c"},
{"lime", "#00ff00"}, {"lime green", "#32cd32"},
{"forest green", "#228b22"}, {"sea green", "#2e8b57"},
{"green", "#008000"}, {"dark green", "#006400"},
{"spring green", "#00ff7f"}, {"olive", "#808000"},
{"teal", "#008080"}, {"dark cyan", "#008b8b"},
{"turquoise", "#40e0d0"}, {"cyan", "#00ffff"},
{"sky blue", "#87ceeb"},
{"deep sky blue", "#00bfff"}, {"steel blue", "#4682b4"},
{"dodger blue", "#1e90ff"}, {"royal blue", "#4169e1"},
{"blue", "#0000ff"}, {"navy", "#000080"},
{"midnight blue", "#191970"}, {"indigo", "#4b0082"},
{"purple", "#800080"}, {"dark violet", "#9400d3"},
{"blue violet", "#8a2be2"}, {"medium purple", "#9370db"},
{"orchid", "#da70d6"}, {"violet", "#ee82ee"},
{"plum", "#dda0dd"}, {"magenta", "#ff00ff"},
{"deep pink", "#ff1493"}, {"hot pink", "#ff69b4"},
{"pink", "#ffc0cb"}, {"lavender", "#e6e6fa"},
};
/// Pre-resolved named color: name, hex, and parsed RGB.
struct ResolvedNamed {
std::string_view name;
std::string_view hex;
std::array<double, 3> rgb;
};
/// Build the resolved named-color table once (function-local static init is
/// thread-safe since C++11) so nearest-match lookups never re-parse. Uses only
/// the standard library — the C++ analogue of Rust's OnceLock.
inline const std::vector<ResolvedNamed>& named_colors() {
static const std::vector<ResolvedNamed> table = [] {
std::vector<ResolvedNamed> resolved;
resolved.reserve(std::size(NAMED_COLORS_RAW));
for (const auto& [name, hex] : NAMED_COLORS_RAW) {
// Curated hexes always parse — *hex_to_rgb cannot be null here.
resolved.push_back({name, hex, *hex_to_rgb(std::string(hex))});
}
return resolved;
}();
return table;
}
/// Closest entry in the named-color table by squared RGB Euclidean distance.
/// Returns nullopt for invalid input. The table is a curated subset, so
/// "closest" is approximate, not a guarantee of identity.
inline std::optional<NamedColor> nearest_named_color(const std::string& hex) {
const auto rgb = hex_to_rgb(hex);
if (!rgb) return std::nullopt;
const auto [r, g, b] = *rgb;
const auto& table = named_colors();
std::size_t best = 0;
double best_d = std::numeric_limits<double>::infinity();
for (std::size_t i = 0; i < table.size(); ++i) {
const auto [nr, ng, nb] = table[i].rgb;
const double dr = nr - r, dg = ng - g, db = nb - b;
const double d = dr * dr + dg * dg + db * db;
if (d < best_d) {
best_d = d;
best = i;
}
}
return NamedColor{table[best].name, table[best].hex};
}
} // namespace color_picker
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 →