commons 0.1.5
Header-only C++23 library of common/shared types for the C++ libraries
Loading...
Searching...
No Matches
color.hpp
Go to the documentation of this file.
1#pragma once
2
25
26#include <commons/types.hpp>
27
28#include <array>
29#include <cstddef>
30#include <format>
31#include <optional>
32#include <ostream>
33#include <stdexcept>
34#include <string>
35#include <string_view>
36
37namespace comms {
38
39// ---------------------------------------------------------------------------
40// detail — small constexpr math/parse helpers.
41//
42// These exist so the whole API can stay `constexpr` without leaning on the
43// non-portable C++23 `<cmath>` constexpr-ness. The only transcendental need is
44// the sRGB gamma `pow(x, 2.4)`, served by `exp_`/`log_` with range reduction.
45// ---------------------------------------------------------------------------
46namespace detail {
47
48// The math helpers carry a trailing underscore (`abs_`, `min_`, `pow_`, …) on
49// purpose: it keeps them from shadowing the like-named `<cmath>` functions
50// while staying recognizable. That trips the lower_case naming check, so it is
51// silenced for this internal helper block.
52// NOLINTBEGIN(readability-identifier-naming)
53
54constexpr char lower_(const char c) {
55 return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
56}
57
58constexpr int hex_val(const char c) {
59 if (c >= '0' && c <= '9') {
60 return c - '0';
61 }
62 if (c >= 'a' && c <= 'f') {
63 return c - 'a' + 10;
64 }
65 if (c >= 'A' && c <= 'F') {
66 return c - 'A' + 10;
67 }
68 return -1;
69}
70
71constexpr bool is_space(const char c) {
72 return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
73}
74
75constexpr std::string_view trim(std::string_view s) {
76 while (!s.empty() && is_space(s.front())) {
77 s.remove_prefix(1);
78 }
79 while (!s.empty() && is_space(s.back())) {
80 s.remove_suffix(1);
81 }
82 return s;
83}
84
88constexpr usize find_char(std::string_view s, char c) {
89 for (usize i = 0; i < s.size(); ++i) {
90 if (s[i] == c) {
91 return i;
92 }
93 }
94 return std::string_view::npos;
95}
96
97constexpr bool eq_ci(const std::string_view a, const std::string_view b) {
98 if (a.size() != b.size()) {
99 return false;
100 }
101 for (usize i = 0; i < a.size(); ++i) {
102 if (lower_(a[i]) != lower_(b[i])) {
103 return false;
104 }
105 }
106 return true;
107}
108
109constexpr f64 abs_(const f64 x) {
110 return x < 0.0 ? -x : x;
111}
112
113constexpr f64 min_(const f64 a, const f64 b) {
114 return a < b ? a : b;
115}
116
117constexpr f64 max_(const f64 a, const f64 b) {
118 return a > b ? a : b;
119}
120
121constexpr f64 clamp_(const f64 x, const f64 lo, const f64 hi) {
122 if (x < lo) {
123 return lo;
124 }
125 if (x > hi) {
126 return hi;
127 }
128 return x;
129}
130
131constexpr f64 trunc_(const f64 x) {
132 return static_cast<f64>(static_cast<long long>(x));
133}
134
137constexpr f64 round_nonneg(const f64 x) {
138 return trunc_(x + 0.5);
139}
140
141constexpr f64 fmod_(const f64 x, const f64 y) {
142 return y == 0.0 ? 0.0 : x - trunc_(x / y) * y;
143}
144
146constexpr f64 wrap_hue(f64 h) {
147 h = fmod_(h, 360.0);
148 return h < 0.0 ? h + 360.0 : h;
149}
150
152constexpr u8 round_u8(const f64 v) {
153 return static_cast<u8>(round_nonneg(clamp_(v, 0.0, 1.0) * 255.0));
154}
155
157constexpr u8 round_channel(const f64 v) {
158 return static_cast<u8>(round_nonneg(clamp_(v, 0.0, 255.0)));
159}
160
161inline constexpr f64 ln2_ = 0.6931471805599453;
162
165constexpr f64 log_(f64 x) {
166 if (x <= 0.0) {
167 return 0.0;
168 }
169 int k = 0;
170 while (x > 1.5) {
171 x *= 0.5;
172 ++k;
173 }
174 while (x < 0.75) {
175 x *= 2.0;
176 --k;
177 }
178 const f64 t = (x - 1.0) / (x + 1.0);
179 const f64 t2 = t * t;
180 f64 term = t;
181 f64 sum = 0.0;
182 for (int n = 1; n < 40; n += 2) {
183 sum += term / static_cast<f64>(n);
184 term *= t2;
185 }
186 return 2.0 * sum + static_cast<f64>(k) * ln2_;
187}
188
191constexpr f64 exp_(const f64 x) {
192 const f64 k = trunc_(x / ln2_ + (x >= 0.0 ? 0.5 : -0.5));
193 const f64 r = x - k * ln2_;
194 f64 term = 1.0;
195 f64 sum = 1.0;
196 for (int n = 1; n < 20; ++n) {
197 term *= r / static_cast<f64>(n);
198 sum += term;
199 }
200 if (const int ki = static_cast<int>(k); ki >= 0) {
201 for (int i = 0; i < ki; ++i) {
202 sum *= 2.0;
203 }
204 } else {
205 for (int i = 0; i < -ki; ++i) {
206 sum *= 0.5;
207 }
208 }
209 return sum;
210}
211
213constexpr f64 pow_(const f64 base, const f64 exp) {
214 if (base <= 0.0) {
215 return 0.0;
216 }
217 return exp_(exp * log_(base));
218}
219
221constexpr f64 srgb_to_linear(const f64 cs) {
222 return cs <= 0.04045 ? cs / 12.92 : pow_((cs + 0.055) / 1.055, 2.4);
223}
224
227struct ParsedNum {
228 f64 value = 0.0;
229 bool percent = false;
230 bool ok = false;
231};
232
235constexpr ParsedNum parse_num(std::string_view s) {
236 s = trim(s);
237 bool percent = false;
238 if (!s.empty() && s.back() == '%') {
239 percent = true;
240 s.remove_suffix(1);
241 s = trim(s);
242 }
243 if (s.empty()) {
244 return {0.0, percent, false};
245 }
246 usize i = 0;
247 f64 sign = 1.0;
248 if (s[i] == '+') {
249 ++i;
250 } else if (s[i] == '-') {
251 sign = -1.0;
252 ++i;
253 }
254 bool any = false;
255 f64 intpart = 0.0;
256 while (i < s.size() && s[i] >= '0' && s[i] <= '9') {
257 intpart = intpart * 10.0 + static_cast<f64>(s[i] - '0');
258 ++i;
259 any = true;
260 }
261 f64 frac = 0.0;
262 if (i < s.size() && s[i] == '.') {
263 f64 scale = 1.0;
264 ++i;
265 while (i < s.size() && s[i] >= '0' && s[i] <= '9') {
266 scale *= 0.1;
267 frac += static_cast<f64>(s[i] - '0') * scale;
268 ++i;
269 any = true;
270 }
271 }
272 if (!any || i != s.size()) {
273 return {0.0, percent, false};
274 }
275 return {sign * (intpart + frac), percent, true};
276}
277
281constexpr int split_components(const std::string_view s, std::array<std::string_view, 4>& out) {
282 auto sep = [](const char c) { return c == ' ' || c == '\t' || c == ',' || c == '/'; };
283 int n = 0;
284 usize i = 0;
285 while (i < s.size()) {
286 while (i < s.size() && sep(s[i])) {
287 ++i;
288 }
289 if (i >= s.size()) {
290 break;
291 }
292 usize j = i;
293 while (j < s.size() && !sep(s[j])) {
294 ++j;
295 }
296 if (n >= 4) {
297 return -1;
298 }
299 out[static_cast<usize>(n++)] = s.substr(i, j - i);
300 i = j;
301 }
302 return n;
303}
304
305inline std::string hex2(const u8 v) {
306 constexpr std::string_view digits = "0123456789abcdef";
307 std::string out;
308 out.push_back(digits[static_cast<usize>(v >> 4)]);
309 out.push_back(digits[static_cast<usize>(v & 0x0F)]);
310 return out;
311}
312
313// NOLINTEND(readability-identifier-naming)
314
315} // namespace detail
316
317// ---------------------------------------------------------------------------
318// HSL / HSV model structs. Channels are unit-ranged doubles: h in [0, 360),
319// s/l/v/a in [0, 1].
320// ---------------------------------------------------------------------------
321
323struct Hsl {
324 f64 h = 0.0;
325 f64 s = 0.0;
326 f64 l = 0.0;
327 f64 a = 1.0;
328
329 constexpr bool operator==(const Hsl&) const = default;
330};
331
333struct Hsv {
334 f64 h = 0.0;
335 f64 s = 0.0;
336 f64 v = 0.0;
337 f64 a = 1.0;
338
339 constexpr bool operator==(const Hsv&) const = default;
340};
341
342// ---------------------------------------------------------------------------
343// Color — four u8 channels with a constexpr-first manipulation API.
344// ---------------------------------------------------------------------------
345
347struct Color {
348 u8 r = 0;
349 u8 g = 0;
350 u8 b = 0;
351 u8 a = 255;
352
353 // -- construction -------------------------------------------------------
354
355 constexpr Color() = default;
356
357 constexpr Color(const u8 red, const u8 green, const u8 blue, const u8 alpha = 255)
358 : r(red), g(green), b(blue), a(alpha) {}
359
360 [[nodiscard]] static constexpr Color rgb(const u8 red, const u8 green, const u8 blue) {
361 return Color(red, green, blue, 255);
362 }
363
364 [[nodiscard]] static constexpr Color
365 rgba(const u8 red, const u8 green, const u8 blue, const u8 alpha) {
366 return Color(red, green, blue, alpha);
367 }
368
369 // -- packed integers ----------------------------------------------------
370
372 [[nodiscard]] static constexpr Color from_rgb_int(const u32 v) {
373 return Color(static_cast<u8>((v >> 16) & 0xFFU),
374 static_cast<u8>((v >> 8) & 0xFFU),
375 static_cast<u8>(v & 0xFFU),
376 255);
377 }
378
379 [[nodiscard]] constexpr u32 to_rgb_int() const {
380 return (static_cast<u32>(r) << 16) | (static_cast<u32>(g) << 8) | static_cast<u32>(b);
381 }
382
384 [[nodiscard]] static constexpr Color from_rgba_int(const u32 v) {
385 return Color(static_cast<u8>((v >> 24) & 0xFFU),
386 static_cast<u8>((v >> 16) & 0xFFU),
387 static_cast<u8>((v >> 8) & 0xFFU),
388 static_cast<u8>(v & 0xFFU));
389 }
390
391 [[nodiscard]] constexpr u32 to_rgba_int() const {
392 return (static_cast<u32>(r) << 24) | (static_cast<u32>(g) << 16) |
393 (static_cast<u32>(b) << 8) | static_cast<u32>(a);
394 }
395
397 [[nodiscard]] static constexpr Color from_argb_int(const u32 v) {
398 return Color(static_cast<u8>((v >> 16) & 0xFFU),
399 static_cast<u8>((v >> 8) & 0xFFU),
400 static_cast<u8>(v & 0xFFU),
401 static_cast<u8>((v >> 24) & 0xFFU));
402 }
403
404 [[nodiscard]] constexpr u32 to_argb_int() const {
405 return (static_cast<u32>(a) << 24) | (static_cast<u32>(r) << 16) |
406 (static_cast<u32>(g) << 8) | static_cast<u32>(b);
407 }
408
409 // -- model conversion ---------------------------------------------------
410
411 [[nodiscard]] constexpr Hsl to_hsl() const {
412 using namespace detail;
413 const f64 rr = static_cast<f64>(r) / 255.0;
414 const f64 gg = static_cast<f64>(g) / 255.0;
415 const f64 bb = static_cast<f64>(b) / 255.0;
416 const f64 mx = max_(rr, max_(gg, bb));
417 const f64 mn = min_(rr, min_(gg, bb));
418 const f64 d = mx - mn;
419 const f64 l = (mx + mn) / 2.0;
420 f64 h = 0.0;
421 f64 s = 0.0;
422 if (d != 0.0) {
423 s = l > 0.5 ? d / (2.0 - mx - mn) : d / (mx + mn);
424 if (mx == rr) {
425 h = (gg - bb) / d;
426 } else if (mx == gg) {
427 h = (bb - rr) / d + 2.0;
428 } else {
429 h = (rr - gg) / d + 4.0;
430 }
431 h = wrap_hue(h * 60.0);
432 }
433 return Hsl{h, s, l, static_cast<f64>(a) / 255.0};
434 }
435
436 [[nodiscard]] constexpr Hsv to_hsv() const {
437 using namespace detail;
438 const f64 rr = static_cast<f64>(r) / 255.0;
439 const f64 gg = static_cast<f64>(g) / 255.0;
440 const f64 bb = static_cast<f64>(b) / 255.0;
441 const f64 mx = max_(rr, max_(gg, bb));
442 const f64 mn = min_(rr, min_(gg, bb));
443 const f64 d = mx - mn;
444 f64 h = 0.0;
445 if (d != 0.0) {
446 if (mx == rr) {
447 h = (gg - bb) / d;
448 } else if (mx == gg) {
449 h = (bb - rr) / d + 2.0;
450 } else {
451 h = (rr - gg) / d + 4.0;
452 }
453 h = wrap_hue(h * 60.0);
454 }
455 const f64 s = mx == 0.0 ? 0.0 : d / mx;
456 return Hsv{h, s, mx, static_cast<f64>(a) / 255.0};
457 }
458
459 [[nodiscard]] static constexpr Color from_hsl(const Hsl& in) {
460 using namespace detail;
461 const f64 h = wrap_hue(in.h);
462 const f64 s = clamp_(in.s, 0.0, 1.0);
463 const f64 l = clamp_(in.l, 0.0, 1.0);
464 const f64 c = (1.0 - abs_(2.0 * l - 1.0)) * s;
465 const f64 hp = h / 60.0;
466 const f64 x = c * (1.0 - abs_(fmod_(hp, 2.0) - 1.0));
467 const f64 m = l - c / 2.0;
468 f64 r1 = 0.0;
469 f64 g1 = 0.0;
470 f64 b1 = 0.0;
471 sector(hp, c, x, r1, g1, b1);
472 return Color(round_u8(r1 + m), round_u8(g1 + m), round_u8(b1 + m), round_u8(in.a));
473 }
474
475 [[nodiscard]] static constexpr Color from_hsv(const Hsv& in) {
476 using namespace detail;
477 const f64 h = wrap_hue(in.h);
478 const f64 s = clamp_(in.s, 0.0, 1.0);
479 const f64 v = clamp_(in.v, 0.0, 1.0);
480 const f64 c = v * s;
481 const f64 hp = h / 60.0;
482 const f64 x = c * (1.0 - abs_(fmod_(hp, 2.0) - 1.0));
483 const f64 m = v - c;
484 f64 r1 = 0.0;
485 f64 g1 = 0.0;
486 f64 b1 = 0.0;
487 sector(hp, c, x, r1, g1, b1);
488 return Color(round_u8(r1 + m), round_u8(g1 + m), round_u8(b1 + m), round_u8(in.a));
489 }
490
491 // -- channels / alpha ---------------------------------------------------
492
493 [[nodiscard]] constexpr Color with_red(const u8 red) const {
494 return Color(red, g, b, a);
495 }
496 [[nodiscard]] constexpr Color with_green(const u8 green) const {
497 return Color(r, green, b, a);
498 }
499 [[nodiscard]] constexpr Color with_blue(const u8 blue) const {
500 return Color(r, g, blue, a);
501 }
502 [[nodiscard]] constexpr Color with_alpha(const u8 alpha) const {
503 return Color(r, g, b, alpha);
504 }
505
507 [[nodiscard]] constexpr Color fade(const f64 opacity_unit) const {
508 return Color(r, g, b, detail::round_u8(opacity_unit));
509 }
510
512 [[nodiscard]] constexpr Color opacity(const f64 opacity_unit) const {
513 return fade(opacity_unit);
514 }
515
516 // -- HSL transforms (each: to_hsl -> mutate -> from_hsl) -----------------
517
519 [[nodiscard]] constexpr Color lighten(const f64 amount) const {
520 Hsl c = to_hsl();
521 c.l += amount;
522 return from_hsl(c);
523 }
525 [[nodiscard]] constexpr Color darken(const f64 amount) const {
526 Hsl c = to_hsl();
527 c.l -= amount;
528 return from_hsl(c);
529 }
531 [[nodiscard]] constexpr Color saturate(const f64 amount) const {
532 Hsl c = to_hsl();
533 c.s += amount;
534 return from_hsl(c);
535 }
537 [[nodiscard]] constexpr Color desaturate(const f64 amount) const {
538 Hsl c = to_hsl();
539 c.s -= amount;
540 return from_hsl(c);
541 }
543 [[nodiscard]] constexpr Color rotate_hue(const f64 degrees) const {
544 Hsl c = to_hsl();
545 c.h = detail::wrap_hue(c.h + degrees);
546 return from_hsl(c);
547 }
548 [[nodiscard]] constexpr Color with_hue(const f64 hue) const {
549 Hsl c = to_hsl();
550 c.h = detail::wrap_hue(hue);
551 return from_hsl(c);
552 }
553 [[nodiscard]] constexpr Color with_saturation(const f64 saturation) const {
554 Hsl c = to_hsl();
555 c.s = detail::clamp_(saturation, 0.0, 1.0);
556 return from_hsl(c);
557 }
558 [[nodiscard]] constexpr Color with_lightness(const f64 lightness) const {
559 Hsl c = to_hsl();
560 c.l = detail::clamp_(lightness, 0.0, 1.0);
561 return from_hsl(c);
562 }
563
566 [[nodiscard]] constexpr Color recolor(const Color other) const {
567 const Hsl t = other.to_hsl();
568 Hsl c = to_hsl();
569 c.h = t.h;
570 c.s = t.s;
571 return from_hsl(c);
572 }
573
575 [[nodiscard]] constexpr Color tint(const f64 amount) const {
576 return lighten(amount);
577 }
579 [[nodiscard]] constexpr Color shade(const f64 amount) const {
580 return darken(amount);
581 }
583 [[nodiscard]] constexpr Color tone(const f64 amount) const {
584 return desaturate(amount);
585 }
586
587 // -- state --------------------------------------------------------------
588
589 [[nodiscard]] constexpr bool is_opaque() const {
590 return a == 255;
591 }
592 [[nodiscard]] constexpr bool is_transparent() const {
593 return a == 0;
594 }
595 [[nodiscard]] constexpr bool has_alpha() const {
596 return a != 255;
597 }
598
599 // -- accessors ----------------------------------------------------------
600
601 [[nodiscard]] constexpr f64 hue() const {
602 return to_hsl().h;
603 }
604 [[nodiscard]] constexpr f64 saturation() const {
605 return to_hsl().s;
606 }
607 [[nodiscard]] constexpr f64 lightness() const {
608 return to_hsl().l;
609 }
610 [[nodiscard]] constexpr f64 value() const {
611 return to_hsv().v;
612 }
613 [[nodiscard]] constexpr f64 alpha_unit() const {
614 return static_cast<f64>(a) / 255.0;
615 }
616
617 // -- UI helpers ---------------------------------------------------------
618
621 [[nodiscard]] constexpr Color mix(const Color other, f64 t) const {
622 using namespace detail;
623 t = clamp_(t, 0.0, 1.0);
624 auto lerp = [t](const u8 x, const u8 y) {
625 return round_channel(static_cast<f64>(x) + (static_cast<f64>(y) - x) * t);
626 };
627 return Color(lerp(r, other.r), lerp(g, other.g), lerp(b, other.b), lerp(a, other.a));
628 }
629
632 [[nodiscard]] constexpr Color blend_over(const Color bg) const {
633 using namespace detail;
634 const f64 sa = alpha_unit();
635 const f64 ba = bg.alpha_unit();
636 const f64 oa = sa + ba * (1.0 - sa);
637 if (oa <= 0.0) {
638 return Color(0, 0, 0, 0);
639 }
640 auto comp = [&](const u8 sc, const u8 bc) {
641 return (static_cast<f64>(sc) / 255.0 * sa +
642 static_cast<f64>(bc) / 255.0 * ba * (1.0 - sa)) /
643 oa;
644 };
645 return Color(round_u8(comp(r, bg.r)),
646 round_u8(comp(g, bg.g)),
647 round_u8(comp(b, bg.b)),
648 round_u8(oa));
649 }
650
652 [[nodiscard]] constexpr Color invert() const {
653 return Color(
654 static_cast<u8>(255 - r), static_cast<u8>(255 - g), static_cast<u8>(255 - b), a);
655 }
656
659 [[nodiscard]] constexpr Color grayscale() const {
660 const f64 y =
661 0.299 * static_cast<f64>(r) + 0.587 * static_cast<f64>(g) + 0.114 * static_cast<f64>(b);
662 const u8 v = detail::round_channel(y);
663 return Color(v, v, v, a);
664 }
665
667 [[nodiscard]] constexpr Color complement() const {
668 return rotate_hue(180.0);
669 }
670
671 // -- WCAG ---------------------------------------------------------------
672
675 [[nodiscard]] constexpr f64 relative_luminance() const {
676 const f64 lr = detail::srgb_to_linear(static_cast<f64>(r) / 255.0);
677 const f64 lg = detail::srgb_to_linear(static_cast<f64>(g) / 255.0);
678 const f64 lb = detail::srgb_to_linear(static_cast<f64>(b) / 255.0);
679 return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
680 }
681
683 [[nodiscard]] constexpr f64 contrast_ratio(const Color other) const {
684 const f64 l1 = relative_luminance();
685 const f64 l2 = other.relative_luminance();
686 return (detail::max_(l1, l2) + 0.05) / (detail::min_(l1, l2) + 0.05);
687 }
688
690 [[nodiscard]] constexpr bool is_light() const {
691 return contrast_ratio(Color(0, 0, 0)) >= contrast_ratio(Color(255, 255, 255));
692 }
693 [[nodiscard]] constexpr bool is_dark() const {
694 return !is_light();
695 }
696
699 [[nodiscard]] constexpr Color readable_text_color() const {
700 return is_light() ? Color(0, 0, 0) : Color(255, 255, 255);
701 }
702
705 [[nodiscard]] constexpr bool has_readable_contrast(const Color text) const {
706 return contrast_ratio(text) >= 4.5;
707 }
708
709 // -- palettes -----------------------------------------------------------
710
712 [[nodiscard]] constexpr std::array<Color, 2> split_complementary() const {
713 return {rotate_hue(150.0), rotate_hue(210.0)};
714 }
715
717 [[nodiscard]] constexpr std::array<Color, 3> triadic() const {
718 return {*this, rotate_hue(120.0), rotate_hue(240.0)};
719 }
720
722 [[nodiscard]] constexpr std::array<Color, 3> analogous(const f64 angle = 30.0) const {
723 return {rotate_hue(-angle), *this, rotate_hue(angle)};
724 }
725
726 // -- string output ------------------------------------------------------
727
729 [[nodiscard]] std::string to_hex_string() const {
730 return is_opaque() ? to_hex_rgb_string() : to_hex_rgba_string();
731 }
732
733 [[nodiscard]] std::string to_hex_rgb_string() const {
734 return "#" + detail::hex2(r) + detail::hex2(g) + detail::hex2(b);
735 }
736
737 [[nodiscard]] std::string to_hex_rgba_string() const {
738 return "#" + detail::hex2(r) + detail::hex2(g) + detail::hex2(b) + detail::hex2(a);
739 }
740
742 [[nodiscard]] std::string to_css_rgb_string() const {
743 return "rgb(" + std::to_string(r) + " " + std::to_string(g) + " " + std::to_string(b) + ")";
744 }
745
748 [[nodiscard]] std::string to_css_rgba_string() const {
749 const int tenths = static_cast<int>(detail::round_nonneg(alpha_unit() * 10.0));
750 const std::string alpha = std::to_string(tenths / 10) + "." + std::to_string(tenths % 10);
751 return "rgb(" + std::to_string(r) + " " + std::to_string(g) + " " + std::to_string(b) +
752 " / " + alpha + ")";
753 }
754
755 // -- parsing ------------------------------------------------------------
756
759 [[nodiscard]] static constexpr std::optional<Color> parse_hex(std::string_view s) {
760 s = detail::trim(s);
761 if (!s.empty() && s.front() == '#') {
762 s.remove_prefix(1);
763 }
764 for (const char c : s) {
765 if (detail::hex_val(c) < 0) {
766 return std::nullopt;
767 }
768 }
769 auto h1 = [&](const usize i) {
770 const int v = detail::hex_val(s[i]);
771 return static_cast<u8>(v * 16 + v);
772 };
773 auto h2 = [&](const usize i) {
774 return static_cast<u8>(detail::hex_val(s[i]) * 16 + detail::hex_val(s[i + 1]));
775 };
776 switch (s.size()) {
777 case 3:
778 return Color(h1(0), h1(1), h1(2), 255);
779 case 4:
780 return Color(h1(0), h1(1), h1(2), h1(3));
781 case 6:
782 return Color(h2(0), h2(2), h2(4), 255);
783 case 8:
784 return Color(h2(0), h2(2), h2(4), h2(6));
785 default:
786 return std::nullopt;
787 }
788 }
789
793 [[nodiscard]] static constexpr std::optional<Color> parse(std::string_view s);
794
795 friend constexpr bool operator==(Color, Color) = default;
796
797private:
800 static constexpr void
801 sector(const f64 hp, const f64 c, const f64 x, f64& r1, f64& g1, f64& b1) {
802 if (hp < 1.0) {
803 r1 = c;
804 g1 = x;
805 } else if (hp < 2.0) {
806 r1 = x;
807 g1 = c;
808 } else if (hp < 3.0) {
809 g1 = c;
810 b1 = x;
811 } else if (hp < 4.0) {
812 g1 = x;
813 b1 = c;
814 } else if (hp < 5.0) {
815 r1 = x;
816 b1 = c;
817 } else {
818 r1 = c;
819 b1 = x;
820 }
821 }
822};
823
824// ---------------------------------------------------------------------------
825// CSS named colors. The `static constexpr Color` members are the single source
826// of truth for the values; `parse` looks names up by referencing those members.
827// ---------------------------------------------------------------------------
828
831struct CssColors {
832 static constexpr Color transparent = Color::from_rgba_int(0x00000000);
833 static constexpr Color aliceblue = Color::from_rgb_int(0xF0F8FF);
834 static constexpr Color antiquewhite = Color::from_rgb_int(0xFAEBD7);
835 static constexpr Color aqua = Color::from_rgb_int(0x00FFFF);
836 static constexpr Color aquamarine = Color::from_rgb_int(0x7FFFD4);
837 static constexpr Color azure = Color::from_rgb_int(0xF0FFFF);
838 static constexpr Color beige = Color::from_rgb_int(0xF5F5DC);
839 static constexpr Color bisque = Color::from_rgb_int(0xFFE4C4);
840 static constexpr Color black = Color::from_rgb_int(0x000000);
841 static constexpr Color blanchedalmond = Color::from_rgb_int(0xFFEBCD);
842 static constexpr Color blue = Color::from_rgb_int(0x0000FF);
843 static constexpr Color blueviolet = Color::from_rgb_int(0x8A2BE2);
844 static constexpr Color brown = Color::from_rgb_int(0xA52A2A);
845 static constexpr Color burlywood = Color::from_rgb_int(0xDEB887);
846 static constexpr Color cadetblue = Color::from_rgb_int(0x5F9EA0);
847 static constexpr Color chartreuse = Color::from_rgb_int(0x7FFF00);
848 static constexpr Color chocolate = Color::from_rgb_int(0xD2691E);
849 static constexpr Color coral = Color::from_rgb_int(0xFF7F50);
850 static constexpr Color cornflowerblue = Color::from_rgb_int(0x6495ED);
851 static constexpr Color cornsilk = Color::from_rgb_int(0xFFF8DC);
852 static constexpr Color crimson = Color::from_rgb_int(0xDC143C);
853 static constexpr Color cyan = Color::from_rgb_int(0x00FFFF);
854 static constexpr Color darkblue = Color::from_rgb_int(0x00008B);
855 static constexpr Color darkcyan = Color::from_rgb_int(0x008B8B);
856 static constexpr Color darkgoldenrod = Color::from_rgb_int(0xB8860B);
857 static constexpr Color darkgray = Color::from_rgb_int(0xA9A9A9);
858 static constexpr Color darkgreen = Color::from_rgb_int(0x006400);
859 static constexpr Color darkgrey = Color::from_rgb_int(0xA9A9A9);
860 static constexpr Color darkkhaki = Color::from_rgb_int(0xBDB76B);
861 static constexpr Color darkmagenta = Color::from_rgb_int(0x8B008B);
862 static constexpr Color darkolivegreen = Color::from_rgb_int(0x556B2F);
863 static constexpr Color darkorange = Color::from_rgb_int(0xFF8C00);
864 static constexpr Color darkorchid = Color::from_rgb_int(0x9932CC);
865 static constexpr Color darkred = Color::from_rgb_int(0x8B0000);
866 static constexpr Color darksalmon = Color::from_rgb_int(0xE9967A);
867 static constexpr Color darkseagreen = Color::from_rgb_int(0x8FBC8F);
868 static constexpr Color darkslateblue = Color::from_rgb_int(0x483D8B);
869 static constexpr Color darkslategray = Color::from_rgb_int(0x2F4F4F);
870 static constexpr Color darkslategrey = Color::from_rgb_int(0x2F4F4F);
871 static constexpr Color darkturquoise = Color::from_rgb_int(0x00CED1);
872 static constexpr Color darkviolet = Color::from_rgb_int(0x9400D3);
873 static constexpr Color deeppink = Color::from_rgb_int(0xFF1493);
874 static constexpr Color deepskyblue = Color::from_rgb_int(0x00BFFF);
875 static constexpr Color dimgray = Color::from_rgb_int(0x696969);
876 static constexpr Color dimgrey = Color::from_rgb_int(0x696969);
877 static constexpr Color dodgerblue = Color::from_rgb_int(0x1E90FF);
878 static constexpr Color firebrick = Color::from_rgb_int(0xB22222);
879 static constexpr Color floralwhite = Color::from_rgb_int(0xFFFAF0);
880 static constexpr Color forestgreen = Color::from_rgb_int(0x228B22);
881 static constexpr Color fuchsia = Color::from_rgb_int(0xFF00FF);
882 static constexpr Color gainsboro = Color::from_rgb_int(0xDCDCDC);
883 static constexpr Color ghostwhite = Color::from_rgb_int(0xF8F8FF);
884 static constexpr Color gold = Color::from_rgb_int(0xFFD700);
885 static constexpr Color goldenrod = Color::from_rgb_int(0xDAA520);
886 static constexpr Color gray = Color::from_rgb_int(0x808080);
887 static constexpr Color green = Color::from_rgb_int(0x008000);
888 static constexpr Color greenyellow = Color::from_rgb_int(0xADFF2F);
889 static constexpr Color grey = Color::from_rgb_int(0x808080);
890 static constexpr Color honeydew = Color::from_rgb_int(0xF0FFF0);
891 static constexpr Color hotpink = Color::from_rgb_int(0xFF69B4);
892 static constexpr Color indianred = Color::from_rgb_int(0xCD5C5C);
893 static constexpr Color indigo = Color::from_rgb_int(0x4B0082);
894 static constexpr Color ivory = Color::from_rgb_int(0xFFFFF0);
895 static constexpr Color khaki = Color::from_rgb_int(0xF0E68C);
896 static constexpr Color lavender = Color::from_rgb_int(0xE6E6FA);
897 static constexpr Color lavenderblush = Color::from_rgb_int(0xFFF0F5);
898 static constexpr Color lawngreen = Color::from_rgb_int(0x7CFC00);
899 static constexpr Color lemonchiffon = Color::from_rgb_int(0xFFFACD);
900 static constexpr Color lightblue = Color::from_rgb_int(0xADD8E6);
901 static constexpr Color lightcoral = Color::from_rgb_int(0xF08080);
902 static constexpr Color lightcyan = Color::from_rgb_int(0xE0FFFF);
903 static constexpr Color lightgoldenrodyellow = Color::from_rgb_int(0xFAFAD2);
904 static constexpr Color lightgray = Color::from_rgb_int(0xD3D3D3);
905 static constexpr Color lightgreen = Color::from_rgb_int(0x90EE90);
906 static constexpr Color lightgrey = Color::from_rgb_int(0xD3D3D3);
907 static constexpr Color lightpink = Color::from_rgb_int(0xFFB6C1);
908 static constexpr Color lightsalmon = Color::from_rgb_int(0xFFA07A);
909 static constexpr Color lightseagreen = Color::from_rgb_int(0x20B2AA);
910 static constexpr Color lightskyblue = Color::from_rgb_int(0x87CEFA);
911 static constexpr Color lightslategray = Color::from_rgb_int(0x778899);
912 static constexpr Color lightslategrey = Color::from_rgb_int(0x778899);
913 static constexpr Color lightsteelblue = Color::from_rgb_int(0xB0C4DE);
914 static constexpr Color lightyellow = Color::from_rgb_int(0xFFFFE0);
915 static constexpr Color lime = Color::from_rgb_int(0x00FF00);
916 static constexpr Color limegreen = Color::from_rgb_int(0x32CD32);
917 static constexpr Color linen = Color::from_rgb_int(0xFAF0E6);
918 static constexpr Color magenta = Color::from_rgb_int(0xFF00FF);
919 static constexpr Color maroon = Color::from_rgb_int(0x800000);
920 static constexpr Color mediumaquamarine = Color::from_rgb_int(0x66CDAA);
921 static constexpr Color mediumblue = Color::from_rgb_int(0x0000CD);
922 static constexpr Color mediumorchid = Color::from_rgb_int(0xBA55D3);
923 static constexpr Color mediumpurple = Color::from_rgb_int(0x9370DB);
924 static constexpr Color mediumseagreen = Color::from_rgb_int(0x3CB371);
925 static constexpr Color mediumslateblue = Color::from_rgb_int(0x7B68EE);
926 static constexpr Color mediumspringgreen = Color::from_rgb_int(0x00FA9A);
927 static constexpr Color mediumturquoise = Color::from_rgb_int(0x48D1CC);
928 static constexpr Color mediumvioletred = Color::from_rgb_int(0xC71585);
929 static constexpr Color midnightblue = Color::from_rgb_int(0x191970);
930 static constexpr Color mintcream = Color::from_rgb_int(0xF5FFFA);
931 static constexpr Color mistyrose = Color::from_rgb_int(0xFFE4E1);
932 static constexpr Color moccasin = Color::from_rgb_int(0xFFE4B5);
933 static constexpr Color navajowhite = Color::from_rgb_int(0xFFDEAD);
934 static constexpr Color navy = Color::from_rgb_int(0x000080);
935 static constexpr Color oldlace = Color::from_rgb_int(0xFDF5E6);
936 static constexpr Color olive = Color::from_rgb_int(0x808000);
937 static constexpr Color olivedrab = Color::from_rgb_int(0x6B8E23);
938 static constexpr Color orange = Color::from_rgb_int(0xFFA500);
939 static constexpr Color orangered = Color::from_rgb_int(0xFF4500);
940 static constexpr Color orchid = Color::from_rgb_int(0xDA70D6);
941 static constexpr Color palegoldenrod = Color::from_rgb_int(0xEEE8AA);
942 static constexpr Color palegreen = Color::from_rgb_int(0x98FB98);
943 static constexpr Color paleturquoise = Color::from_rgb_int(0xAFEEEE);
944 static constexpr Color palevioletred = Color::from_rgb_int(0xDB7093);
945 static constexpr Color papayawhip = Color::from_rgb_int(0xFFEFD5);
946 static constexpr Color peachpuff = Color::from_rgb_int(0xFFDAB9);
947 static constexpr Color peru = Color::from_rgb_int(0xCD853F);
948 static constexpr Color pink = Color::from_rgb_int(0xFFC0CB);
949 static constexpr Color plum = Color::from_rgb_int(0xDDA0DD);
950 static constexpr Color powderblue = Color::from_rgb_int(0xB0E0E6);
951 static constexpr Color purple = Color::from_rgb_int(0x800080);
952 static constexpr Color rebeccapurple = Color::from_rgb_int(0x663399);
953 static constexpr Color red = Color::from_rgb_int(0xFF0000);
954 static constexpr Color rosybrown = Color::from_rgb_int(0xBC8F8F);
955 static constexpr Color royalblue = Color::from_rgb_int(0x4169E1);
956 static constexpr Color saddlebrown = Color::from_rgb_int(0x8B4513);
957 static constexpr Color salmon = Color::from_rgb_int(0xFA8072);
958 static constexpr Color sandybrown = Color::from_rgb_int(0xF4A460);
959 static constexpr Color seagreen = Color::from_rgb_int(0x2E8B57);
960 static constexpr Color seashell = Color::from_rgb_int(0xFFF5EE);
961 static constexpr Color sienna = Color::from_rgb_int(0xA0522D);
962 static constexpr Color silver = Color::from_rgb_int(0xC0C0C0);
963 static constexpr Color skyblue = Color::from_rgb_int(0x87CEEB);
964 static constexpr Color slateblue = Color::from_rgb_int(0x6A5ACD);
965 static constexpr Color slategray = Color::from_rgb_int(0x708090);
966 static constexpr Color slategrey = Color::from_rgb_int(0x708090);
967 static constexpr Color snow = Color::from_rgb_int(0xFFFAFA);
968 static constexpr Color springgreen = Color::from_rgb_int(0x00FF7F);
969 static constexpr Color steelblue = Color::from_rgb_int(0x4682B4);
970 static constexpr Color tan = Color::from_rgb_int(0xD2B48C);
971 static constexpr Color teal = Color::from_rgb_int(0x008080);
972 static constexpr Color thistle = Color::from_rgb_int(0xD8BFD8);
973 static constexpr Color tomato = Color::from_rgb_int(0xFF6347);
974 static constexpr Color turquoise = Color::from_rgb_int(0x40E0D0);
975 static constexpr Color violet = Color::from_rgb_int(0xEE82EE);
976 static constexpr Color wheat = Color::from_rgb_int(0xF5DEB3);
977 static constexpr Color white = Color::from_rgb_int(0xFFFFFF);
978 static constexpr Color whitesmoke = Color::from_rgb_int(0xF5F5F5);
979 static constexpr Color yellow = Color::from_rgb_int(0xFFFF00);
980 static constexpr Color yellowgreen = Color::from_rgb_int(0x9ACD32);
981
985 [[nodiscard]] static constexpr std::optional<Color> parse(const std::string_view name) {
986 struct Named {
987 std::string_view name;
988 const Color* value;
989 };
990 constexpr std::array<Named, 149> table = {{
991 {"transparent", &transparent},
992 {"aliceblue", &aliceblue},
993 {"antiquewhite", &antiquewhite},
994 {"aqua", &aqua},
995 {"aquamarine", &aquamarine},
996 {"azure", &azure},
997 {"beige", &beige},
998 {"bisque", &bisque},
999 {"black", &black},
1000 {"blanchedalmond", &blanchedalmond},
1001 {"blue", &blue},
1002 {"blueviolet", &blueviolet},
1003 {"brown", &brown},
1004 {"burlywood", &burlywood},
1005 {"cadetblue", &cadetblue},
1006 {"chartreuse", &chartreuse},
1007 {"chocolate", &chocolate},
1008 {"coral", &coral},
1009 {"cornflowerblue", &cornflowerblue},
1010 {"cornsilk", &cornsilk},
1011 {"crimson", &crimson},
1012 {"cyan", &cyan},
1013 {"darkblue", &darkblue},
1014 {"darkcyan", &darkcyan},
1015 {"darkgoldenrod", &darkgoldenrod},
1016 {"darkgray", &darkgray},
1017 {"darkgreen", &darkgreen},
1018 {"darkgrey", &darkgrey},
1019 {"darkkhaki", &darkkhaki},
1020 {"darkmagenta", &darkmagenta},
1021 {"darkolivegreen", &darkolivegreen},
1022 {"darkorange", &darkorange},
1023 {"darkorchid", &darkorchid},
1024 {"darkred", &darkred},
1025 {"darksalmon", &darksalmon},
1026 {"darkseagreen", &darkseagreen},
1027 {"darkslateblue", &darkslateblue},
1028 {"darkslategray", &darkslategray},
1029 {"darkslategrey", &darkslategrey},
1030 {"darkturquoise", &darkturquoise},
1031 {"darkviolet", &darkviolet},
1032 {"deeppink", &deeppink},
1033 {"deepskyblue", &deepskyblue},
1034 {"dimgray", &dimgray},
1035 {"dimgrey", &dimgrey},
1036 {"dodgerblue", &dodgerblue},
1037 {"firebrick", &firebrick},
1038 {"floralwhite", &floralwhite},
1039 {"forestgreen", &forestgreen},
1040 {"fuchsia", &fuchsia},
1041 {"gainsboro", &gainsboro},
1042 {"ghostwhite", &ghostwhite},
1043 {"gold", &gold},
1044 {"goldenrod", &goldenrod},
1045 {"gray", &gray},
1046 {"green", &green},
1047 {"greenyellow", &greenyellow},
1048 {"grey", &grey},
1049 {"honeydew", &honeydew},
1050 {"hotpink", &hotpink},
1051 {"indianred", &indianred},
1052 {"indigo", &indigo},
1053 {"ivory", &ivory},
1054 {"khaki", &khaki},
1055 {"lavender", &lavender},
1056 {"lavenderblush", &lavenderblush},
1057 {"lawngreen", &lawngreen},
1058 {"lemonchiffon", &lemonchiffon},
1059 {"lightblue", &lightblue},
1060 {"lightcoral", &lightcoral},
1061 {"lightcyan", &lightcyan},
1062 {"lightgoldenrodyellow", &lightgoldenrodyellow},
1063 {"lightgray", &lightgray},
1064 {"lightgreen", &lightgreen},
1065 {"lightgrey", &lightgrey},
1066 {"lightpink", &lightpink},
1067 {"lightsalmon", &lightsalmon},
1068 {"lightseagreen", &lightseagreen},
1069 {"lightskyblue", &lightskyblue},
1070 {"lightslategray", &lightslategray},
1071 {"lightslategrey", &lightslategrey},
1072 {"lightsteelblue", &lightsteelblue},
1073 {"lightyellow", &lightyellow},
1074 {"lime", &lime},
1075 {"limegreen", &limegreen},
1076 {"linen", &linen},
1077 {"magenta", &magenta},
1078 {"maroon", &maroon},
1079 {"mediumaquamarine", &mediumaquamarine},
1080 {"mediumblue", &mediumblue},
1081 {"mediumorchid", &mediumorchid},
1082 {"mediumpurple", &mediumpurple},
1083 {"mediumseagreen", &mediumseagreen},
1084 {"mediumslateblue", &mediumslateblue},
1085 {"mediumspringgreen", &mediumspringgreen},
1086 {"mediumturquoise", &mediumturquoise},
1087 {"mediumvioletred", &mediumvioletred},
1088 {"midnightblue", &midnightblue},
1089 {"mintcream", &mintcream},
1090 {"mistyrose", &mistyrose},
1091 {"moccasin", &moccasin},
1092 {"navajowhite", &navajowhite},
1093 {"navy", &navy},
1094 {"oldlace", &oldlace},
1095 {"olive", &olive},
1096 {"olivedrab", &olivedrab},
1097 {"orange", &orange},
1098 {"orangered", &orangered},
1099 {"orchid", &orchid},
1100 {"palegoldenrod", &palegoldenrod},
1101 {"palegreen", &palegreen},
1102 {"paleturquoise", &paleturquoise},
1103 {"palevioletred", &palevioletred},
1104 {"papayawhip", &papayawhip},
1105 {"peachpuff", &peachpuff},
1106 {"peru", &peru},
1107 {"pink", &pink},
1108 {"plum", &plum},
1109 {"powderblue", &powderblue},
1110 {"purple", &purple},
1111 {"rebeccapurple", &rebeccapurple},
1112 {"red", &red},
1113 {"rosybrown", &rosybrown},
1114 {"royalblue", &royalblue},
1115 {"saddlebrown", &saddlebrown},
1116 {"salmon", &salmon},
1117 {"sandybrown", &sandybrown},
1118 {"seagreen", &seagreen},
1119 {"seashell", &seashell},
1120 {"sienna", &sienna},
1121 {"silver", &silver},
1122 {"skyblue", &skyblue},
1123 {"slateblue", &slateblue},
1124 {"slategray", &slategray},
1125 {"slategrey", &slategrey},
1126 {"snow", &snow},
1127 {"springgreen", &springgreen},
1128 {"steelblue", &steelblue},
1129 {"tan", &tan},
1130 {"teal", &teal},
1131 {"thistle", &thistle},
1132 {"tomato", &tomato},
1133 {"turquoise", &turquoise},
1134 {"violet", &violet},
1135 {"wheat", &wheat},
1136 {"white", &white},
1137 {"whitesmoke", &whitesmoke},
1138 {"yellow", &yellow},
1139 {"yellowgreen", &yellowgreen},
1140 }};
1141 const std::string_view trimmed = detail::trim(name);
1142 for (const auto& [n, value] : table) {
1143 if (detail::eq_ci(trimmed, n)) {
1144 return *value;
1145 }
1146 }
1147 return std::nullopt;
1148 }
1149};
1150
1151// ---------------------------------------------------------------------------
1152// Material UI palette (the 2014 Material Design colors). Each family is a small
1153// struct of static constexpr Color shades — s50..s900 plus the a100/a200/a400/
1154// a700 accents — with `operator[](int)` for the numeric shades and `accent(int)`
1155// for the A-shades. MuiColors exposes one instance per family (e.g. `red`,
1156// usable as `red[500]`) alongside flat aliases (`red_500`, `red_a100`). The
1157// families are generated from a macro so the 19×14 hex table is written once.
1158// ---------------------------------------------------------------------------
1159
1160#define COMMONS_MUI_FAMILY(Type, \
1161 inst, \
1162 H50, \
1163 H100, \
1164 H200, \
1165 H300, \
1166 H400, \
1167 H500, \
1168 H600, \
1169 H700, \
1170 H800, \
1171 H900, \
1172 HA100, \
1173 HA200, \
1174 HA400, \
1175 HA700) \
1176 struct Type { \
1177 static constexpr Color s50 = Color::from_rgb_int(H50); \
1178 static constexpr Color s100 = Color::from_rgb_int(H100); \
1179 static constexpr Color s200 = Color::from_rgb_int(H200); \
1180 static constexpr Color s300 = Color::from_rgb_int(H300); \
1181 static constexpr Color s400 = Color::from_rgb_int(H400); \
1182 static constexpr Color s500 = Color::from_rgb_int(H500); \
1183 static constexpr Color s600 = Color::from_rgb_int(H600); \
1184 static constexpr Color s700 = Color::from_rgb_int(H700); \
1185 static constexpr Color s800 = Color::from_rgb_int(H800); \
1186 static constexpr Color s900 = Color::from_rgb_int(H900); \
1187 static constexpr Color a100 = Color::from_rgb_int(HA100); \
1188 static constexpr Color a200 = Color::from_rgb_int(HA200); \
1189 static constexpr Color a400 = Color::from_rgb_int(HA400); \
1190 static constexpr Color a700 = Color::from_rgb_int(HA700); \
1191 constexpr Color operator[](int shade) const { \
1192 switch (shade) { \
1193 case 50: \
1194 return s50; \
1195 case 100: \
1196 return s100; \
1197 case 200: \
1198 return s200; \
1199 case 300: \
1200 return s300; \
1201 case 400: \
1202 return s400; \
1203 case 500: \
1204 return s500; \
1205 case 600: \
1206 return s600; \
1207 case 700: \
1208 return s700; \
1209 case 800: \
1210 return s800; \
1211 case 900: \
1212 return s900; \
1213 default: \
1214 throw std::out_of_range("commons: MUI shade out of range"); \
1215 } \
1216 } \
1217 constexpr Color accent(int shade) const { \
1218 switch (shade) { \
1219 case 100: \
1220 return a100; \
1221 case 200: \
1222 return a200; \
1223 case 400: \
1224 return a400; \
1225 case 700: \
1226 return a700; \
1227 default: \
1228 throw std::out_of_range("commons: MUI accent shade out of range"); \
1229 } \
1230 } \
1231 }; \
1232 static constexpr Type inst{}; \
1233 static constexpr Color inst##_50 = Type::s50; \
1234 static constexpr Color inst##_100 = Type::s100; \
1235 static constexpr Color inst##_200 = Type::s200; \
1236 static constexpr Color inst##_300 = Type::s300; \
1237 static constexpr Color inst##_400 = Type::s400; \
1238 static constexpr Color inst##_500 = Type::s500; \
1239 static constexpr Color inst##_600 = Type::s600; \
1240 static constexpr Color inst##_700 = Type::s700; \
1241 static constexpr Color inst##_800 = Type::s800; \
1242 static constexpr Color inst##_900 = Type::s900; \
1243 static constexpr Color inst##_a100 = Type::a100; \
1244 static constexpr Color inst##_a200 = Type::a200; \
1245 static constexpr Color inst##_a400 = Type::a400; \
1246 static constexpr Color inst##_a700 = Type::a700
1247
1252 // clang-format off
1253 COMMONS_MUI_FAMILY(Red, red,
1254 0xffebee, 0xffcdd2, 0xef9a9a, 0xe57373, 0xef5350, 0xf44336, 0xe53935, 0xd32f2f, 0xc62828,
1255 0xb71c1c, 0xff8a80, 0xff5252, 0xff1744, 0xd50000);
1256 COMMONS_MUI_FAMILY(Pink, pink,
1257 0xfce4ec, 0xf8bbd0, 0xf48fb1, 0xf06292, 0xec407a, 0xe91e63, 0xd81b60, 0xc2185b, 0xad1457,
1258 0x880e4f, 0xff80ab, 0xff4081, 0xf50057, 0xc51162);
1259 COMMONS_MUI_FAMILY(Purple, purple,
1260 0xf3e5f5, 0xe1bee7, 0xce93d8, 0xba68c8, 0xab47bc, 0x9c27b0, 0x8e24aa, 0x7b1fa2, 0x6a1b9a,
1261 0x4a148c, 0xea80fc, 0xe040fb, 0xd500f9, 0xaa00ff);
1262 COMMONS_MUI_FAMILY(DeepPurple, deep_purple,
1263 0xede7f6, 0xd1c4e9, 0xb39ddb, 0x9575cd, 0x7e57c2, 0x673ab7, 0x5e35b1, 0x512da8, 0x4527a0,
1264 0x311b92, 0xb388ff, 0x7c4dff, 0x651fff, 0x6200ea);
1265 COMMONS_MUI_FAMILY(Indigo, indigo,
1266 0xe8eaf6, 0xc5cae9, 0x9fa8da, 0x7986cb, 0x5c6bc0, 0x3f51b5, 0x3949ab, 0x303f9f, 0x283593,
1267 0x1a237e, 0x8c9eff, 0x536dfe, 0x3d5afe, 0x304ffe);
1268 COMMONS_MUI_FAMILY(Blue, blue,
1269 0xe3f2fd, 0xbbdefb, 0x90caf9, 0x64b5f6, 0x42a5f5, 0x2196f3, 0x1e88e5, 0x1976d2, 0x1565c0,
1270 0x0d47a1, 0x82b1ff, 0x448aff, 0x2979ff, 0x2962ff);
1271 COMMONS_MUI_FAMILY(LightBlue, light_blue,
1272 0xe1f5fe, 0xb3e5fc, 0x81d4fa, 0x4fc3f7, 0x29b6f6, 0x03a9f4, 0x039be5, 0x0288d1, 0x0277bd,
1273 0x01579b, 0x80d8ff, 0x40c4ff, 0x00b0ff, 0x0091ea);
1274 COMMONS_MUI_FAMILY(Cyan, cyan,
1275 0xe0f7fa, 0xb2ebf2, 0x80deea, 0x4dd0e1, 0x26c6da, 0x00bcd4, 0x00acc1, 0x0097a7, 0x00838f,
1276 0x006064, 0x84ffff, 0x18ffff, 0x00e5ff, 0x00b8d4);
1277 COMMONS_MUI_FAMILY(Teal, teal,
1278 0xe0f2f1, 0xb2dfdb, 0x80cbc4, 0x4db6ac, 0x26a69a, 0x009688, 0x00897b, 0x00796b, 0x00695c,
1279 0x004d40, 0xa7ffeb, 0x64ffda, 0x1de9b6, 0x00bfa5);
1280 COMMONS_MUI_FAMILY(Green, green,
1281 0xe8f5e9, 0xc8e6c9, 0xa5d6a7, 0x81c784, 0x66bb6a, 0x4caf50, 0x43a047, 0x388e3c, 0x2e7d32,
1282 0x1b5e20, 0xb9f6ca, 0x69f0ae, 0x00e676, 0x00c853);
1283 COMMONS_MUI_FAMILY(LightGreen, light_green,
1284 0xf1f8e9, 0xdcedc8, 0xc5e1a5, 0xaed581, 0x9ccc65, 0x8bc34a, 0x7cb342, 0x689f38, 0x558b2f,
1285 0x33691e, 0xccff90, 0xb2ff59, 0x76ff03, 0x64dd17);
1286 COMMONS_MUI_FAMILY(Lime, lime,
1287 0xf9fbe7, 0xf0f4c3, 0xe6ee9c, 0xdce775, 0xd4e157, 0xcddc39, 0xc0ca33, 0xafb42b, 0x9e9d24,
1288 0x827717, 0xf4ff81, 0xeeff41, 0xc6ff00, 0xaeea00);
1289 COMMONS_MUI_FAMILY(Yellow, yellow,
1290 0xfffde7, 0xfff9c4, 0xfff59d, 0xfff176, 0xffee58, 0xffeb3b, 0xfdd835, 0xfbc02d, 0xf9a825,
1291 0xf57f17, 0xffff8d, 0xffff00, 0xffea00, 0xffd600);
1292 COMMONS_MUI_FAMILY(Amber, amber,
1293 0xfff8e1, 0xffecb3, 0xffe082, 0xffd54f, 0xffca28, 0xffc107, 0xffb300, 0xffa000, 0xff8f00,
1294 0xff6f00, 0xffe57f, 0xffd740, 0xffc400, 0xffab00);
1295 COMMONS_MUI_FAMILY(Orange, orange,
1296 0xfff3e0, 0xffe0b2, 0xffcc80, 0xffb74d, 0xffa726, 0xff9800, 0xfb8c00, 0xf57c00, 0xef6c00,
1297 0xe65100, 0xffd180, 0xffab40, 0xff9100, 0xff6d00);
1298 COMMONS_MUI_FAMILY(DeepOrange, deep_orange,
1299 0xfbe9e7, 0xffccbc, 0xffab91, 0xff8a65, 0xff7043, 0xff5722, 0xf4511e, 0xe64a19, 0xd84315,
1300 0xbf360c, 0xff9e80, 0xff6e40, 0xff3d00, 0xdd2c00);
1301 COMMONS_MUI_FAMILY(Brown, brown,
1302 0xefebe9, 0xd7ccc8, 0xbcaaa4, 0xa1887f, 0x8d6e63, 0x795548, 0x6d4c41, 0x5d4037, 0x4e342e,
1303 0x3e2723, 0xd7ccc8, 0xbcaaa4, 0x8d6e63, 0x5d4037);
1304 COMMONS_MUI_FAMILY(Grey, grey,
1305 0xfafafa, 0xf5f5f5, 0xeeeeee, 0xe0e0e0, 0xbdbdbd, 0x9e9e9e, 0x757575, 0x616161, 0x424242,
1306 0x212121, 0xf5f5f5, 0xeeeeee, 0xbdbdbd, 0x616161);
1307 COMMONS_MUI_FAMILY(BlueGrey, blue_grey,
1308 0xeceff1, 0xcfd8dc, 0xb0bec5, 0x90a4ae, 0x78909c, 0x607d8b, 0x546e7a, 0x455a64, 0x37474f,
1309 0x263238, 0xcfd8dc, 0xb0bec5, 0x78909c, 0x455a64);
1310 // clang-format on
1311};
1312
1313#undef COMMONS_MUI_FAMILY
1314
1318struct Colors {
1319 using css = CssColors;
1320 using mui = MuiColors;
1321};
1322
1323// ---------------------------------------------------------------------------
1324// Color::parse — defined here, now that CssColors is complete.
1325// ---------------------------------------------------------------------------
1326
1327constexpr std::optional<Color> Color::parse(std::string_view s) {
1328 using namespace detail;
1329 s = trim(s);
1330 if (s.empty()) {
1331 return std::nullopt;
1332 }
1333
1334 if (const auto hex = parse_hex(s)) {
1335 return hex;
1336 }
1337
1338 if (const usize lp = find_char(s, '('); lp != std::string_view::npos && s.back() == ')') {
1339 const std::string_view fn = trim(s.substr(0, lp));
1340 const std::string_view inner = s.substr(lp + 1, s.size() - lp - 2);
1341 const bool is_rgb = eq_ci(fn, "rgb") || eq_ci(fn, "rgba");
1342 if (const bool is_hsl = eq_ci(fn, "hsl") || eq_ci(fn, "hsla"); is_rgb || is_hsl) {
1343 std::array<std::string_view, 4> toks{};
1344 const int n = split_components(inner, toks);
1345 if (n != 3 && n != 4) {
1346 return std::nullopt;
1347 }
1348 std::array<f64, 3> comp{};
1349 for (usize i = 0; i < 3; ++i) {
1350 const auto [value, percent, ok] = parse_num(toks[i]);
1351 if (!ok) {
1352 return std::nullopt;
1353 }
1354 comp[i] = (is_rgb && percent) ? value / 100.0 * 255.0 : value;
1355 }
1356 f64 alpha = 1.0;
1357 if (n == 4) {
1358 const auto [value, percent, ok] = parse_num(toks[3]);
1359 if (!ok) {
1360 return std::nullopt;
1361 }
1362 alpha = percent ? value / 100.0 : value;
1363 }
1364 if (is_rgb) {
1365 return Color(round_channel(comp[0]),
1366 round_channel(comp[1]),
1367 round_channel(comp[2]),
1368 round_u8(alpha));
1369 }
1370 return from_hsl(Hsl{wrap_hue(comp[0]),
1371 clamp_(comp[1] / 100.0, 0.0, 1.0),
1372 clamp_(comp[2] / 100.0, 0.0, 1.0),
1373 alpha});
1374 }
1375 return std::nullopt;
1376 }
1377
1378 return CssColors::parse(s);
1379}
1380
1381// ---------------------------------------------------------------------------
1382// Text output: to_string + std::ostream insertion. (std::format support is the
1383// std::formatter specializations below, outside namespace comms.)
1384// ---------------------------------------------------------------------------
1385
1388[[nodiscard]] inline std::string to_string(const Color& c) {
1389 return c.to_hex_string();
1390}
1391
1394[[nodiscard]] inline std::string to_string(const Hsl& v) {
1395 return v.a == 1.0 ? std::format("hsl({}, {}, {})", v.h, v.s, v.l)
1396 : std::format("hsla({}, {}, {}, {})", v.h, v.s, v.l, v.a);
1397}
1398
1400[[nodiscard]] inline std::string to_string(const Hsv& v) {
1401 return v.a == 1.0 ? std::format("hsv({}, {}, {})", v.h, v.s, v.v)
1402 : std::format("hsva({}, {}, {}, {})", v.h, v.s, v.v, v.a);
1403}
1404
1405inline std::ostream& operator<<(std::ostream& os, const Color& c) {
1406 return os << to_string(c);
1407}
1408
1409inline std::ostream& operator<<(std::ostream& os, const Hsl& v) {
1410 return os << to_string(v);
1411}
1412
1413inline std::ostream& operator<<(std::ostream& os, const Hsv& v) {
1414 return os << to_string(v);
1415}
1416
1417} // namespace comms
1418
1419// ---------------------------------------------------------------------------
1420// std::format support. Specializations live in namespace std (the primary
1421// templates are visible), like nlohmann's adl_serializer route in json.hpp.
1422// ---------------------------------------------------------------------------
1423
1426template <>
1427struct std::formatter<comms::Color> {
1428 char mode = 'h';
1429
1430 constexpr auto parse(const std::format_parse_context& ctx) {
1431 const auto* it = ctx.begin();
1432 if (it != ctx.end() && *it != '}') {
1433 mode = *it++;
1434 if (mode != 'h' && mode != 'H' && mode != 'r') {
1435 throw std::format_error("commons: invalid Color format spec (use h, H, or r)");
1436 }
1437 }
1438 if (it != ctx.end() && *it != '}') {
1439 throw std::format_error("commons: invalid Color format spec");
1440 }
1441 return it;
1442 }
1443
1444 auto format(const comms::Color& c, std::format_context& ctx) const {
1445 if (mode == 'r') {
1446 return std::format_to(
1447 ctx.out(), "{}", c.is_opaque() ? c.to_css_rgb_string() : c.to_css_rgba_string());
1448 }
1449 std::string s = c.to_hex_string();
1450 if (mode == 'H') {
1451 for (char& ch : s) {
1452 if (ch >= 'a' && ch <= 'f') {
1453 ch = static_cast<char>(ch - ('a' - 'A'));
1454 }
1455 }
1456 }
1457 return std::format_to(ctx.out(), "{}", s);
1458 }
1459};
1460
1461// These spec-less formatters don't read any member state, but `std::formatter`
1462// requires `parse`/`format` to be non-static members — so silence the
1463// convert-to-static suggestion here.
1464// NOLINTBEGIN(readability-convert-member-functions-to-static)
1465
1467template <>
1468struct std::formatter<comms::Hsl> {
1469 constexpr auto parse(const std::format_parse_context& ctx) {
1470 const auto* it = ctx.begin();
1471 if (it != ctx.end() && *it != '}') {
1472 throw std::format_error("commons: Hsl takes no format spec");
1473 }
1474 return it;
1475 }
1476
1477 auto format(const comms::Hsl& v, std::format_context& ctx) const {
1478 return std::format_to(ctx.out(), "{}", comms::to_string(v));
1479 }
1480};
1481
1483template <>
1484struct std::formatter<comms::Hsv> {
1485 constexpr auto parse(const std::format_parse_context& ctx) {
1486 const auto* it = ctx.begin();
1487 if (it != ctx.end() && *it != '}') {
1488 throw std::format_error("commons: Hsv takes no format spec");
1489 }
1490 return it;
1491 }
1492
1493 auto format(const comms::Hsv& v, std::format_context& ctx) const {
1494 return std::format_to(ctx.out(), "{}", comms::to_string(v));
1495 }
1496};
1497
1498// NOLINTEND(readability-convert-member-functions-to-static)
constexpr f64 log_(f64 x)
Natural log for x > 0, via power-of-two range reduction and the atanh-style series; error well below ...
Definition color.hpp:165
constexpr f64 exp_(const f64 x)
e^x, via reduction to x = k*ln2 + r with |r| <= ln2/2 and a Taylor series for e^r,...
Definition color.hpp:191
constexpr f64 srgb_to_linear(const f64 cs)
One sRGB channel (unit [0, 1]) to linear light, per the WCAG definition.
Definition color.hpp:221
constexpr usize find_char(std::string_view s, char c)
Index of the first c in s, or npos.
Definition color.hpp:88
constexpr u8 round_u8(const f64 v)
Map a unit value [0, 1] to a u8 channel, clamping then rounding.
Definition color.hpp:152
constexpr f64 pow_(const f64 base, const f64 exp)
base^exp for base >= 0. Used only for the sRGB gamma pow(x, 2.4).
Definition color.hpp:213
constexpr f64 wrap_hue(f64 h)
Wrap a hue in degrees into the canonical [0, 360) range.
Definition color.hpp:146
std::string to_string(const Color &c)
Color as its canonical hex string (#RRGGBB, or #RRGGBBAA when not opaque).
Definition color.hpp:1388
constexpr f64 round_nonneg(const f64 x)
Round a non-negative value to the nearest integer.
Definition color.hpp:137
constexpr u8 round_channel(const f64 v)
Round an already-[0, 255]-scaled value to a u8, clamping first.
Definition color.hpp:157
constexpr int split_components(const std::string_view s, std::array< std::string_view, 4 > &out)
Split a CSS-functional argument list into at most four tokens.
Definition color.hpp:281
constexpr ParsedNum parse_num(std::string_view s)
Parse a single decimal number (optional sign, optional fraction, optional trailing %).
Definition color.hpp:235
An 8-bit-per-channel RGBA color. Default is opaque black.
Definition color.hpp:347
constexpr f64 contrast_ratio(const Color other) const
WCAG contrast ratio against other, in [1, 21]. Symmetric.
Definition color.hpp:683
constexpr Color blend_over(const Color bg) const
Alpha-composite this color (source) over bg using the source-over operator.
Definition color.hpp:632
constexpr Color recolor(const Color other) const
Adopt the hue and saturation of other, keeping this color's lightness and alpha.
Definition color.hpp:566
constexpr Color grayscale() const
Convert to gray using the Rec.601 luma weights (0.299 R, 0.587 G, 0.114 B), keeping alpha.
Definition color.hpp:659
constexpr bool is_light() const
True when black text is at least as readable on this color as white.
Definition color.hpp:690
static constexpr std::optional< Color > parse_hex(std::string_view s)
Parse a hex color: #rgb, #rgba, #rrggbb, or #rrggbbaa (the leading # is optional).
Definition color.hpp:759
u8 b
Blue channel, [0, 255].
Definition color.hpp:350
constexpr bool has_readable_contrast(const Color text) const
True when text over this background meets WCAG AA for normal text (contrast ratio >= 4....
Definition color.hpp:705
u8 g
Green channel, [0, 255].
Definition color.hpp:349
static constexpr Color from_rgba_int(const u32 v)
0xRRGGBBAA.
Definition color.hpp:384
static constexpr Color from_rgb_int(const u32 v)
0xRRGGBB (alpha forced opaque).
Definition color.hpp:372
static constexpr std::optional< Color > parse(std::string_view s)
Parse any supported textual color: hex (see parse_hex), CSS-functional rgb()/rgba()/hsl()/hsla(),...
Definition color.hpp:1327
constexpr Color shade(const f64 amount) const
Alias for darken.
Definition color.hpp:579
static constexpr Color from_argb_int(const u32 v)
0xAARRGGBB.
Definition color.hpp:397
constexpr Color mix(const Color other, f64 t) const
Linearly interpolate toward other by t (clamped to [0, 1]), including the alpha channel.
Definition color.hpp:621
constexpr f64 relative_luminance() const
Relative luminance per WCAG 2.x (sRGB linearized, then the 0.2126 / 0.7152 / 0.0722 weights).
Definition color.hpp:675
constexpr Color fade(const f64 opacity_unit) const
Set the alpha channel to the given [0, 1] opacity.
Definition color.hpp:507
std::string to_css_rgb_string() const
CSS space-separated form, e.g. "rgb(99 102 241)".
Definition color.hpp:742
constexpr std::array< Color, 2 > split_complementary() const
The two split-complementary colors (hue ±150°).
Definition color.hpp:712
std::string to_css_rgba_string() const
CSS form with alpha, e.g.
Definition color.hpp:748
constexpr Color saturate(const f64 amount) const
Increase saturation by amount (unit delta, clamped).
Definition color.hpp:531
constexpr Color darken(const f64 amount) const
Decrease lightness by amount (unit delta, clamped).
Definition color.hpp:525
constexpr Color tint(const f64 amount) const
Alias for lighten.
Definition color.hpp:575
constexpr Color rotate_hue(const f64 degrees) const
Rotate the hue by degrees around the color wheel.
Definition color.hpp:543
constexpr Color tone(const f64 amount) const
Alias for desaturate.
Definition color.hpp:583
u8 a
Alpha channel, [0, 255] (255 = opaque).
Definition color.hpp:351
constexpr Color desaturate(const f64 amount) const
Decrease saturation by amount (unit delta, clamped).
Definition color.hpp:537
std::string to_hex_string() const
Canonical hex: #RRGGBB when opaque, #RRGGBBAA otherwise.
Definition color.hpp:729
constexpr std::array< Color, 3 > analogous(const f64 angle=30.0) const
This color flanked by its two analogous neighbors (hue ∓angle).
Definition color.hpp:722
constexpr Color invert() const
Invert the RGB channels, keeping alpha.
Definition color.hpp:652
constexpr Color opacity(const f64 opacity_unit) const
Alias for fade — set the alpha channel to a [0, 1] opacity.
Definition color.hpp:512
constexpr std::array< Color, 3 > triadic() const
This color plus the two triadic colors (hue +120°, +240°).
Definition color.hpp:717
constexpr Color complement() const
The hue-opposite color (hue rotated 180°).
Definition color.hpp:667
u8 r
Red channel, [0, 255].
Definition color.hpp:348
constexpr Color readable_text_color() const
Black or white, whichever has the higher contrast against this color — the better foreground text col...
Definition color.hpp:699
constexpr Color lighten(const f64 amount) const
Increase lightness by amount (unit delta, clamped).
Definition color.hpp:519
Top-level collection of named-color sets: the CSS named colors (comms::Colors::css::red) and the Mate...
Definition color.hpp:1318
The CSS named colors as static constexpr Color values (the single source of truth for their hex),...
Definition color.hpp:831
static constexpr std::optional< Color > parse(const std::string_view name)
Resolve a CSS color name (case-insensitive) to its Color, or nullopt if the name is unknown.
Definition color.hpp:985
Hue / saturation / lightness, plus unit alpha.
Definition color.hpp:323
f64 a
Alpha (opacity), [0, 1].
Definition color.hpp:327
f64 h
Hue in degrees, [0, 360).
Definition color.hpp:324
f64 l
Lightness, [0, 1].
Definition color.hpp:326
constexpr bool operator==(const Hsl &) const =default
Field-wise equality.
f64 s
Saturation, [0, 1].
Definition color.hpp:325
Hue / saturation / value (brightness), plus unit alpha.
Definition color.hpp:333
f64 h
Hue in degrees, [0, 360).
Definition color.hpp:334
f64 a
Alpha (opacity), [0, 1].
Definition color.hpp:337
f64 s
Saturation, [0, 1].
Definition color.hpp:335
constexpr bool operator==(const Hsv &) const =default
Field-wise equality.
f64 v
Value (brightness), [0, 1].
Definition color.hpp:336
The Material UI palette (2014 Material Design).
Definition color.hpp:1251
A number parsed out of a CSS-functional component: its value, whether it carried a % suffix,...
Definition color.hpp:227
Fixed-width numeric aliases shared across the C++ libraries.
std::uint8_t u8
Unsigned 8-bit integer.
Definition types.hpp:22
std::uint32_t u32
Unsigned 32-bit integer.
Definition types.hpp:24
std::size_t usize
Unsigned size type (std::size_t).
Definition types.hpp:43
double f64
64-bit floating point.
Definition types.hpp:28