A header only C++20 library. One seed colour gives a complete Material 3 style scheme, light or dark, with the tones, roles and contrast guarantees of the baseline scheme, computed in OKLCH as a light weight stand in for HCT. The seed colour and the light or dark preference come from a chain of sources: an environment override, the XDG desktop portal (GNOME 47+ and KDE Plasma 6), kdeglobals on KDE, gsettings on GNOME, GTK settings, and a fallback that is always appended and cannot be left out. Desktop specific sources are only asked on their own desktop, so a stale gsettings value on KDE never wins. Programs can put sources of their own in front. The tests run without a desktop through a fake Context. They cover colour conversions, tone accuracy, the contrast of every role pair over 300 random seeds in both modes, every parser against real portal output, the order of the sources per desktop, the fallback and external program timeouts. The test suite was checked by breaking the code 22 ways, all are caught. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
125 lines
5.5 KiB
C++
125 lines
5.5 KiB
C++
#pragma once
|
|
|
|
// A Material 3 style colour scheme from one seed colour.
|
|
//
|
|
// The roles and the tones they use follow Material 3's baseline scheme: a
|
|
// primary, secondary, tertiary and error accent each with an "on" colour and a
|
|
// container, a neutral surface ladder, and the outline and inverse roles. The
|
|
// palettes differ in chroma and hue the way Material's do (the tertiary hue is
|
|
// shifted by 60 degrees, the neutrals are almost grey), expressed in OKLCH
|
|
// chroma, see color.h for what that means for fidelity.
|
|
|
|
#include "color.h"
|
|
|
|
namespace monet {
|
|
|
|
enum class Mode { Light, Dark };
|
|
|
|
struct Scheme {
|
|
Rgb seed;
|
|
Mode mode = Mode::Dark;
|
|
|
|
Rgb primary, onPrimary, primaryContainer, onPrimaryContainer;
|
|
Rgb secondary, onSecondary, secondaryContainer, onSecondaryContainer;
|
|
Rgb tertiary, onTertiary, tertiaryContainer, onTertiaryContainer;
|
|
Rgb error, onError, errorContainer, onErrorContainer;
|
|
|
|
Rgb background, onBackground;
|
|
Rgb surface, onSurface, surfaceVariant, onSurfaceVariant;
|
|
Rgb surfaceDim, surfaceBright;
|
|
Rgb surfaceContainerLowest, surfaceContainerLow, surfaceContainer, surfaceContainerHigh,
|
|
surfaceContainerHighest;
|
|
|
|
Rgb outline, outlineVariant;
|
|
Rgb inverseSurface, inverseOnSurface, inversePrimary;
|
|
Rgb shadow, scrim;
|
|
|
|
bool dark() const { return mode == Mode::Dark; }
|
|
};
|
|
|
|
namespace detail {
|
|
|
|
constexpr double kMinPrimaryChroma = 0.11;
|
|
constexpr double kGreySeedChroma = 0.03; // below this the seed is read as "no colour"
|
|
constexpr double kMaxPrimaryChroma = 0.30;
|
|
constexpr double kSecondaryChroma = 0.04;
|
|
constexpr double kTertiaryChroma = 0.075;
|
|
constexpr double kNeutralChroma = 0.008;
|
|
constexpr double kNeutralVariantChroma = 0.016;
|
|
constexpr double kErrorHue = 29.0;
|
|
constexpr double kErrorChroma = 0.19;
|
|
|
|
inline double wrapHue(double h) {
|
|
h = std::fmod(h, 360.0);
|
|
return h < 0 ? h + 360.0 : h;
|
|
}
|
|
|
|
} // namespace detail
|
|
|
|
inline Scheme generate(Rgb seed, Mode mode) {
|
|
using namespace detail;
|
|
const Oklch s = toOklch(seed);
|
|
// A grey seed stays grey, a colourful one is given at least a lively
|
|
// chroma, a very strong one is held back so the accent stays usable.
|
|
const double primaryChroma =
|
|
s.C < kGreySeedChroma ? s.C : std::clamp(s.C, kMinPrimaryChroma, kMaxPrimaryChroma);
|
|
const double hue = s.h;
|
|
const double secondaryChroma = s.C < kGreySeedChroma ? s.C : kSecondaryChroma;
|
|
const double tertiaryChroma = s.C < kGreySeedChroma ? s.C : kTertiaryChroma;
|
|
const double neutral = s.C < kGreySeedChroma ? 0.0 : kNeutralChroma;
|
|
const double neutralVariant = s.C < kGreySeedChroma ? 0.0 : kNeutralVariantChroma;
|
|
|
|
auto primary = [&](double t) { return toneColor(hue, primaryChroma, t); };
|
|
auto secondary = [&](double t) { return toneColor(hue, secondaryChroma, t); };
|
|
auto tertiary = [&](double t) { return toneColor(wrapHue(hue + 60.0), tertiaryChroma, t); };
|
|
auto error = [&](double t) { return toneColor(kErrorHue, kErrorChroma, t); };
|
|
auto n = [&](double t) { return toneColor(hue, neutral, t); };
|
|
auto nv = [&](double t) { return toneColor(hue, neutralVariant, t); };
|
|
|
|
Scheme o;
|
|
o.seed = seed;
|
|
o.mode = mode;
|
|
if (mode == Mode::Light) {
|
|
o.primary = primary(40); o.onPrimary = primary(100);
|
|
o.primaryContainer = primary(90); o.onPrimaryContainer = primary(10);
|
|
o.secondary = secondary(40); o.onSecondary = secondary(100);
|
|
o.secondaryContainer = secondary(90); o.onSecondaryContainer = secondary(10);
|
|
o.tertiary = tertiary(40); o.onTertiary = tertiary(100);
|
|
o.tertiaryContainer = tertiary(90); o.onTertiaryContainer = tertiary(10);
|
|
o.error = error(40); o.onError = error(100);
|
|
o.errorContainer = error(90); o.onErrorContainer = error(10);
|
|
o.background = n(98); o.onBackground = n(10);
|
|
o.surface = n(98); o.onSurface = n(10);
|
|
o.surfaceVariant = nv(90); o.onSurfaceVariant = nv(30);
|
|
o.surfaceDim = n(87); o.surfaceBright = n(98);
|
|
o.surfaceContainerLowest = n(100); o.surfaceContainerLow = n(96);
|
|
o.surfaceContainer = n(94); o.surfaceContainerHigh = n(92);
|
|
o.surfaceContainerHighest = n(90);
|
|
o.outline = nv(50); o.outlineVariant = nv(80);
|
|
o.inverseSurface = n(20); o.inverseOnSurface = n(95); o.inversePrimary = primary(80);
|
|
} else {
|
|
o.primary = primary(80); o.onPrimary = primary(20);
|
|
o.primaryContainer = primary(30); o.onPrimaryContainer = primary(90);
|
|
o.secondary = secondary(80); o.onSecondary = secondary(20);
|
|
o.secondaryContainer = secondary(30); o.onSecondaryContainer = secondary(90);
|
|
o.tertiary = tertiary(80); o.onTertiary = tertiary(20);
|
|
o.tertiaryContainer = tertiary(30); o.onTertiaryContainer = tertiary(90);
|
|
o.error = error(80); o.onError = error(20);
|
|
o.errorContainer = error(30); o.onErrorContainer = error(90);
|
|
o.background = n(6); o.onBackground = n(90);
|
|
o.surface = n(6); o.onSurface = n(90);
|
|
o.surfaceVariant = nv(30); o.onSurfaceVariant = nv(80);
|
|
o.surfaceDim = n(6); o.surfaceBright = n(24);
|
|
o.surfaceContainerLowest = n(4); o.surfaceContainerLow = n(10);
|
|
o.surfaceContainer = n(12); o.surfaceContainerHigh = n(17);
|
|
o.surfaceContainerHighest = n(22);
|
|
o.outline = nv(60); o.outlineVariant = nv(30);
|
|
o.inverseSurface = n(90); o.inverseOnSurface = n(20); o.inversePrimary = primary(40);
|
|
}
|
|
o.shadow = {0, 0, 0};
|
|
o.scrim = {0, 0, 0};
|
|
return o;
|
|
}
|
|
|
|
} // namespace monet
|