#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