feat(theme): palette generator, 1st pass. m3 is accurate, custom schemes still need some love.

This commit is contained in:
Lemmy
2026-04-09 20:39:31 -04:00
parent c345e14562
commit 61e9de41a5
88 changed files with 21304 additions and 5987 deletions
+6 -4
View File
@@ -26,7 +26,7 @@ A lightweight Wayland shell and bar with no Qt or GTK dependency.
| Keyboard | `xkbcommon` |
| Rendering | `EGL`, `OpenGL ES 3`, `wayland-egl` |
| Text | `cairo`, `pango`, `pangocairo`, `freetype`, `harfbuzz`, `fontconfig` |
| Images | `Wuffs` (vendored), `nanosvg` (vendored) |
| Images | `Wuffs` (vendored), `nanosvg` (vendored), `libwebp` |
| IPC | `sdbus-c++` |
| Audio | `libpipewire` |
| Authentication | `PAM` |
@@ -47,7 +47,7 @@ sudo dnf install meson gcc-c++ just \
cairo-devel pango-devel \
libxkbcommon-devel \
sdbus-cpp-devel pipewire-devel \
pam-devel libcurl-devel \
pam-devel libcurl-devel libwebp-devel \
libasan libubsan
```
@@ -60,7 +60,7 @@ sudo pacman -S meson gcc just \
cairo pango \
libxkbcommon \
sdbus-cpp libpipewire \
pam curl \
pam curl libwebp \
gcc-libs
```
@@ -74,12 +74,14 @@ sudo apt install meson g++ just \
libcairo2-dev libpango1.0-dev \
libxkbcommon-dev \
libsdbus-c++-dev libpipewire-0.3-dev \
libpam0g-dev libcurl4-openssl-dev \
libpam0g-dev libcurl4-openssl-dev libwebp-dev \
libasan8 libubsan1
```
Vendored (no system package needed): `Wuffs`, `nanosvg`, `tomlplusplus`, `tinyexpr`, `nlohmann/json`.
System packages required beyond the Wayland/GL stack: `libwebp` (VP8 lossy WebP; wuffs handles all other formats).
## Build
Requires [just](https://github.com/casey/just) and [meson](https://mesonbuild.com/).
+65 -1
View File
@@ -59,6 +59,57 @@ tinyexpr_dep = declare_dependency(
include_directories: include_directories('third_party/tinyexpr', is_system: true),
)
# ── Vendored: Material Color Utilities (Google) ───────────────────────────────
# Upstream uses #include "cpp/..." prefixes, so the include root is the
# material_color_utilities/ dir itself. Built as a quiet static library.
_mcu_sources = files(
'third_party/material_color_utilities/cpp/blend/blend.cc',
'third_party/material_color_utilities/cpp/cam/cam.cc',
'third_party/material_color_utilities/cpp/cam/hct.cc',
'third_party/material_color_utilities/cpp/cam/hct_solver.cc',
'third_party/material_color_utilities/cpp/cam/viewing_conditions.cc',
'third_party/material_color_utilities/cpp/contrast/contrast.cc',
'third_party/material_color_utilities/cpp/dislike/dislike.cc',
'third_party/material_color_utilities/cpp/dynamiccolor/dynamic_color.cc',
'third_party/material_color_utilities/cpp/dynamiccolor/dynamic_scheme.cc',
'third_party/material_color_utilities/cpp/dynamiccolor/material_dynamic_colors.cc',
'third_party/material_color_utilities/cpp/palettes/tones.cc',
'third_party/material_color_utilities/cpp/quantize/celebi.cc',
'third_party/material_color_utilities/cpp/quantize/lab.cc',
'third_party/material_color_utilities/cpp/quantize/wsmeans.cc',
'third_party/material_color_utilities/cpp/quantize/wu.cc',
'third_party/material_color_utilities/cpp/scheme/scheme_content.cc',
'third_party/material_color_utilities/cpp/scheme/scheme_expressive.cc',
'third_party/material_color_utilities/cpp/scheme/scheme_fidelity.cc',
'third_party/material_color_utilities/cpp/scheme/scheme_fruit_salad.cc',
'third_party/material_color_utilities/cpp/scheme/scheme_monochrome.cc',
'third_party/material_color_utilities/cpp/scheme/scheme_neutral.cc',
'third_party/material_color_utilities/cpp/scheme/scheme_rainbow.cc',
'third_party/material_color_utilities/cpp/scheme/scheme_tonal_spot.cc',
'third_party/material_color_utilities/cpp/scheme/scheme_vibrant.cc',
'third_party/material_color_utilities/cpp/score/score.cc',
'third_party/material_color_utilities/cpp/temperature/temperature_cache.cc',
'third_party/material_color_utilities/cpp/utils/utils.cc',
)
_mcu_lib = static_library('material_color_utilities',
_mcu_sources,
include_directories: include_directories('third_party/material_color_utilities', is_system: true),
cpp_args: ['-Wno-pedantic', '-Wno-conversion', '-Wno-shadow', '-Wno-unused-parameter'],
override_options: ['warning_level=0'],
)
mcu_dep = declare_dependency(
link_with: _mcu_lib,
include_directories: include_directories('third_party/material_color_utilities', is_system: true),
)
# ── Vendored: stb_image_resize2 (header-only) ─────────────────────────────────
stb_dep = declare_dependency(
include_directories: include_directories('third_party/stb', is_system: true),
)
# ── System: libwebp (VP8 lossy WebP; wuffs only handles VP8L lossless) ────────
libwebp_dep = dependency('libwebp', required: true)
# ── Wayland protocol generation ───────────────────────────────────────────────
wayland_scanner = find_program('wayland-scanner')
wayland_protos_dir = wayland_protos_dep.get_variable('pkgdatadir')
@@ -177,7 +228,7 @@ _noctalia_sources = files(
'src/render/animation/animation_manager.cpp',
'src/render/core/shader_program.cpp',
'src/render/core/texture_manager.cpp',
'src/render/core/wuffs_image_decoder.cpp',
'src/render/core/image_decoder.cpp',
'src/render/image_loaders.cpp',
'src/render/programs/color_glyph_program.cpp',
'src/render/programs/image_program.cpp',
@@ -272,6 +323,16 @@ _noctalia_sources = files(
'src/wayland/wayland_seat.cpp',
'src/wayland/wayland_toplevels.cpp',
'src/wayland/wayland_workspaces.cpp',
'src/theme/scheme.cpp',
'src/theme/color.cpp',
'src/theme/contrast.cpp',
'src/theme/image_loader.cpp',
'src/theme/palette_generator.cpp',
'src/theme/m3_schemes.cpp',
'src/theme/custom_schemes.cpp',
'src/theme/json_output.cpp',
'src/theme/cli.cpp',
'src/ipc/cli.cpp',
)
if sanitize
@@ -311,6 +372,9 @@ executable('noctalia',
curl_dep,
pam_dep,
tinyexpr_dep,
mcu_dep,
stb_dep,
libwebp_dep,
],
include_directories: _noctalia_inc,
cpp_args: [
+23
View File
@@ -0,0 +1,23 @@
#include "ipc/cli.h"
#include "ipc/ipc_client.h"
#include <cstdio>
#include <string>
namespace noctalia::ipc {
int runCli(int argc, char* argv[]) {
if (argc < 3) {
std::fputs("error: msg requires a command (try: noctalia msg --help)\n", stderr);
return 1;
}
std::string cmd = argv[2];
for (int i = 3; i < argc; ++i) {
cmd += ' ';
cmd += argv[i];
}
return IpcClient::send(cmd);
}
} // namespace noctalia::ipc
+9
View File
@@ -0,0 +1,9 @@
#pragma once
namespace noctalia::ipc {
// Entry point for `noctalia msg <command> [args...]`. Returns a process exit
// code. Forwards the command to the running instance over the IPC socket.
int runCli(int argc, char* argv[]);
} // namespace noctalia::ipc
+38 -29
View File
@@ -1,32 +1,21 @@
#include "app/application.h"
#include "core/log.h"
#include "ipc/cli.h"
#include "ipc/ipc_client.h"
#include "theme/cli.h"
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <string>
int main(int argc, char* argv[]) {
if (argc >= 2 && std::strcmp(argv[1], "msg") == 0) {
// noctalia msg <command> [args...]
if (argc < 3) {
std::fputs("error: msg requires a command (try: noctalia msg --help)\n", stderr);
return 1;
}
std::string cmd = argv[2];
for (int i = 3; i < argc; ++i) {
cmd += ' ';
cmd += argv[i];
}
return IpcClient::send(cmd);
}
namespace {
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--version") == 0) {
int runTopLevelFlag(const char* flag) {
if (std::strcmp(flag, "--version") == 0) {
std::puts("noctalia v" NOCTALIA_VERSION);
return 0;
}
if (std::strcmp(argv[i], "--help") == 0) {
if (std::strcmp(flag, "--help") == 0) {
std::puts("Usage: noctalia [OPTIONS]\n"
"\n"
"Options:\n"
@@ -34,25 +23,45 @@ int main(int argc, char* argv[]) {
" --version Show version information\n"
"\n"
"Subcommands:\n"
" msg <command> Send a command to the running instance\n"
" Run 'noctalia msg --help' for available commands\n"
" msg <command> Send a command to the running instance\n"
" Run 'noctalia msg --help' for available commands\n"
" theme <image> Generate a color palette from an image\n"
" Run 'noctalia theme --help' for options\n"
"\n"
"For more information and documentation, visit:\n"
" https://noctalia.dev");
return 0;
}
return -1;
}
if (IpcClient::isRunning()) {
std::fputs("error: noctalia is already running\n", stderr);
return 1;
int runShell() {
if (IpcClient::isRunning()) {
std::fputs("error: noctalia is already running\n", stderr);
return 1;
}
try {
Application app;
app.run();
} catch (const std::exception& e) {
logError("fatal: {}", e.what());
return 1;
}
return 0;
}
try {
Application app;
app.run();
} catch (const std::exception& e) {
logError("fatal: {}", e.what());
return 1;
} // namespace
int main(int argc, char* argv[]) {
if (argc >= 2) {
if (std::strcmp(argv[1], "theme") == 0) return noctalia::theme::runCli(argc, argv);
if (std::strcmp(argv[1], "msg") == 0) return noctalia::ipc::runCli(argc, argv);
}
for (int i = 1; i < argc; ++i) {
const int rc = runTopLevelFlag(argv[i]);
if (rc >= 0) return rc;
}
return runShell();
}
@@ -1,10 +1,14 @@
#include "render/core/wuffs_image_decoder.h"
#include "render/core/image_decoder.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <utility>
#include <webp/decode.h>
#define WUFFS_IMPLEMENTATION
#include "wuffs-unsupported-snapshot.c"
#include "wuffs-v0.4.c"
namespace {
@@ -17,6 +21,33 @@ public:
}
};
// Returns true if the buffer starts with the RIFF....WEBP signature.
bool isWebP(const std::uint8_t* data, std::size_t size) {
return size >= 12 &&
data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' &&
data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P';
}
std::optional<DecodedRasterImage> decodeWebP(
const std::uint8_t* data, std::size_t size, std::string* errorMessage) {
int width = 0, height = 0;
std::uint8_t* rgba = WebPDecodeRGBA(data, size, &width, &height);
if (rgba == nullptr) {
if (errorMessage != nullptr)
*errorMessage = "libwebp: failed to decode WebP image";
return std::nullopt;
}
DecodedRasterImage decoded;
decoded.width = width;
decoded.height = height;
std::size_t bytes = static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4;
decoded.pixels.resize(bytes);
std::memcpy(decoded.pixels.data(), rgba, bytes);
WebPFree(rgba);
return decoded;
}
} // namespace
std::optional<DecodedRasterImage>
@@ -28,6 +59,9 @@ decodeRasterImage(const std::uint8_t* data, std::size_t size, std::string* error
return std::nullopt;
}
if (isWebP(data, size))
return decodeWebP(data, size, errorMessage);
auto input = wuffs_aux::sync_io::MemoryInput(data, size);
auto callbacks = RgbaDecodeCallbacks();
auto result = wuffs_aux::DecodeImage(callbacks, input);
@@ -53,4 +87,3 @@ decodeRasterImage(const std::uint8_t* data, std::size_t size, std::string* error
std::memcpy(decoded.pixels.data(), plane.ptr, decoded.pixels.size());
return decoded;
}
+1 -1
View File
@@ -12,7 +12,7 @@
#include <nanosvgrast.h>
#pragma GCC diagnostic pop
#include "render/core/wuffs_image_decoder.h"
#include "render/core/image_decoder.h"
#include <algorithm>
#include <cstring>
+120
View File
@@ -0,0 +1,120 @@
#include "theme/cli.h"
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include "theme/image_loader.h"
#include "theme/json_output.h"
#include "theme/palette_generator.h"
#include "theme/scheme.h"
namespace noctalia::theme {
namespace {
constexpr const char* kHelpText = "Usage: noctalia theme <image> [options]\n"
"\n"
"Generate a color palette from an image. Material You and custom\n"
"schemes produce very different results.\n"
"\n"
"Options:\n"
" --scheme <name> Material You (Material Design 3):\n"
" m3-tonal-spot (default)\n"
" m3-content\n"
" m3-fruit-salad\n"
" m3-rainbow\n"
" m3-monochrome\n"
" Custom (HSL-space, non-M3):\n"
" vibrant\n"
" faithful\n"
" dysfunctional\n"
" muted\n"
" --dark Emit only the dark variant (default)\n"
" --light Emit only the light variant\n"
" --both Emit both variants under dark/light keys\n"
" -o <file> Write JSON to file instead of stdout";
} // namespace
int runCli(int argc, char* argv[]) {
const char* imagePath = nullptr;
std::string schemeName = "m3-tonal-spot";
Variant variant = Variant::Dark;
const char* outPath = nullptr;
for (int i = 2; i < argc; ++i) {
const char* a = argv[i];
if (std::strcmp(a, "--help") == 0) {
std::puts(kHelpText);
return 0;
}
if (std::strcmp(a, "--scheme") == 0 && i + 1 < argc) {
schemeName = argv[++i];
continue;
}
if (std::strcmp(a, "--dark") == 0) {
variant = Variant::Dark;
continue;
}
if (std::strcmp(a, "--light") == 0) {
variant = Variant::Light;
continue;
}
if (std::strcmp(a, "--both") == 0) {
variant = Variant::Both;
continue;
}
if (std::strcmp(a, "-o") == 0 && i + 1 < argc) {
outPath = argv[++i];
continue;
}
if (!imagePath && a[0] != '-') {
imagePath = a;
continue;
}
std::fprintf(stderr, "error: unknown theme argument: %s\n", a);
return 1;
}
if (!imagePath) {
std::fputs("error: theme requires an image path (try: noctalia theme --help)\n", stderr);
return 1;
}
auto schemeOpt = schemeFromString(schemeName);
if (!schemeOpt) {
std::fprintf(stderr, "error: unknown scheme '%s'\n", schemeName.c_str());
return 1;
}
std::string err;
auto loaded = loadAndResize(imagePath, *schemeOpt, &err);
if (!loaded) {
std::fprintf(stderr, "error: failed to load image: %s\n", err.c_str());
return 1;
}
auto palette = generate(loaded->rgb, *schemeOpt, &err);
if (palette.dark.empty() && palette.light.empty()) {
std::fprintf(stderr, "error: palette generation failed: %s\n", err.empty() ? "unknown error" : err.c_str());
return 1;
}
const std::string json = toJson(palette, *schemeOpt, variant);
if (outPath) {
std::ofstream f(outPath);
if (!f) {
std::fprintf(stderr, "error: cannot open output file: %s\n", outPath);
return 1;
}
f << json << '\n';
} else {
std::fwrite(json.data(), 1, json.size(), stdout);
std::fputc('\n', stdout);
}
return 0;
}
} // namespace noctalia::theme
+10
View File
@@ -0,0 +1,10 @@
#pragma once
namespace noctalia::theme {
// Entry point for `noctalia theme <image> [options]`. Returns a process exit
// code. Does not touch Application / event loop / config — pure function of
// (argv, stdout, stderr).
int runCli(int argc, char* argv[]);
} // namespace noctalia::theme
+130
View File
@@ -0,0 +1,130 @@
#include "theme/color.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <stdexcept>
namespace noctalia::theme {
namespace {
int parseHexByte(std::string_view s, size_t offset) {
auto digit = [](char c) -> int {
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
throw std::invalid_argument("invalid hex digit");
};
return digit(s[offset]) * 16 + digit(s[offset + 1]);
}
int roundClamp255(double v) {
long r = std::lround(v * 255.0);
if (r < 0)
r = 0;
if (r > 255)
r = 255;
return static_cast<int>(r);
}
} // namespace
Color Color::fromHex(std::string_view hex) {
if (!hex.empty() && hex.front() == '#')
hex.remove_prefix(1);
if (hex.size() != 6)
throw std::invalid_argument("hex must be 6 chars");
return Color(parseHexByte(hex, 0), parseHexByte(hex, 2), parseHexByte(hex, 4));
}
Color Color::fromArgb(uint32_t argb) {
return Color(static_cast<int>((argb >> 16) & 0xff), static_cast<int>((argb >> 8) & 0xff),
static_cast<int>(argb & 0xff));
}
std::string Color::toHex() const {
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02x%02x%02x", r & 0xff, g & 0xff, b & 0xff);
return std::string(buf);
}
std::tuple<double, double, double> Color::toHsl() const {
const double rn = r / 255.0;
const double gn = g / 255.0;
const double bn = b / 255.0;
const double maxC = std::max({rn, gn, bn});
const double minC = std::min({rn, gn, bn});
const double delta = maxC - minC;
const double l = (maxC + minC) / 2.0;
double h = 0.0;
double s = 0.0;
if (delta != 0.0) {
if (l != 0.0 && l != 1.0) {
s = delta / (1.0 - std::fabs(2.0 * l - 1.0));
}
if (maxC == rn) {
// Positive-result fmod so negative ratios wrap cleanly onto [0, 6).
double t = std::fmod((gn - bn) / delta, 6.0);
if (t < 0.0)
t += 6.0;
h = 60.0 * t;
} else if (maxC == gn) {
h = 60.0 * ((bn - rn) / delta + 2.0);
} else {
h = 60.0 * ((rn - gn) / delta + 4.0);
}
}
return {h, s, l};
}
Color Color::fromHsl(double h, double s, double l) {
if (s == 0.0) {
int v = roundClamp255(l);
return Color(v, v, v);
}
const double q = (l < 0.5) ? (l * (1.0 + s)) : (l + s - l * s);
const double p = 2.0 * l - q;
const double hn = h / 360.0;
auto hueToRgb = [&](double t) -> double {
if (t < 0.0)
t += 1.0;
if (t > 1.0)
t -= 1.0;
if (t < 1.0 / 6.0)
return p + (q - p) * 6.0 * t;
if (t < 1.0 / 2.0)
return q;
if (t < 2.0 / 3.0)
return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
return p;
};
return Color(roundClamp255(hueToRgb(hn + 1.0 / 3.0)), roundClamp255(hueToRgb(hn)),
roundClamp255(hueToRgb(hn - 1.0 / 3.0)));
}
double hueDistance(double h1, double h2) {
const double diff = std::fabs(h1 - h2);
return std::min(diff, 360.0 - diff);
}
Color shiftHue(const Color& c, double degrees) {
auto [h, s, l] = c.toHsl();
double newH = std::fmod(h + degrees, 360.0);
if (newH < 0.0)
newH += 360.0;
return Color::fromHsl(newH, s, l);
}
Color adjustSurface(const Color& base, double sMax, double lTarget) {
auto [h, s, _l] = base.toHsl();
return Color::fromHsl(h, std::min(s, sMax), lTarget);
}
} // namespace noctalia::theme
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <string>
#include <tuple>
namespace noctalia::theme {
// Minimal RGB colour (0-255 per channel) with hex/HSL/ARGB conversions. The
// custom schemes operate in HSL space via this type; the M3 schemes go
// through HCT instead and only use this for final ARGB → hex emission.
struct Color {
int r = 0;
int g = 0;
int b = 0;
Color() = default;
constexpr Color(int red, int green, int blue) : r(red), g(green), b(blue) {}
static Color fromHex(std::string_view hex); // accepts #RRGGBB or RRGGBB
static Color fromHsl(double h, double s, double l);
static Color fromArgb(uint32_t argb);
std::string toHex() const; // "#rrggbb"
std::tuple<double, double, double> toHsl() const; // (h°, s, l) — h in [0,360)
uint32_t toArgb() const { return 0xff000000u | (uint32_t(r) << 16) | (uint32_t(g) << 8) | uint32_t(b); }
};
// Shortest circular hue distance in degrees (result in [0, 180]).
double hueDistance(double h1, double h2);
// Shift a colour's hue by `degrees`, preserving saturation and lightness.
Color shiftHue(const Color& c, double degrees);
// Build a surface colour from `base`: keep its hue, cap saturation at `sMax`,
// force lightness to `lTarget`.
Color adjustSurface(const Color& base, double sMax, double lTarget);
} // namespace noctalia::theme
+69
View File
@@ -0,0 +1,69 @@
#include "theme/contrast.h"
#include <algorithm>
#include <cmath>
namespace noctalia::theme {
namespace {
double linearizeChannel(int c) {
const double n = c / 255.0;
if (n <= 0.03928)
return n / 12.92;
return std::pow((n + 0.055) / 1.055, 2.4);
}
} // namespace
double relativeLuminance(int r, int g, int b) {
return 0.2126 * linearizeChannel(r) + 0.7152 * linearizeChannel(g) + 0.0722 * linearizeChannel(b);
}
double contrastRatio(const Color& a, const Color& b) {
const double l1 = relativeLuminance(a.r, a.g, a.b);
const double l2 = relativeLuminance(b.r, b.g, b.b);
const double lighter = std::max(l1, l2);
const double darker = std::min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
bool isDark(const Color& c) { return relativeLuminance(c.r, c.g, c.b) < 0.179; }
Color ensureContrast(const Color& foreground, const Color& background, double minRatio, int preferLight) {
if (contrastRatio(foreground, background) >= minRatio)
return foreground;
auto [h, s, l] = foreground.toHsl();
bool lighten;
if (preferLight > 0)
lighten = true;
else if (preferLight < 0)
lighten = false;
else
lighten = isDark(background);
double low = lighten ? l : 0.0;
double high = lighten ? 1.0 : l;
Color best = foreground;
for (int i = 0; i < 20; ++i) {
const double mid = (low + high) / 2.0;
Color test = Color::fromHsl(h, s, mid);
if (contrastRatio(test, background) >= minRatio) {
best = test;
if (lighten)
high = mid;
else
low = mid;
} else {
if (lighten)
low = mid;
else
high = mid;
}
}
return best;
}
} // namespace noctalia::theme
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "theme/color.h"
namespace noctalia::theme {
double relativeLuminance(int r, int g, int b);
double contrastRatio(const Color& a, const Color& b);
bool isDark(const Color& c);
// Binary-search the foreground's HSL lightness toward black or white until
// the WCAG contrast ratio against `background` meets `minRatio`.
// preferLight: -1 = darken, +1 = lighten, 0 = auto (lighten if bg is dark).
Color ensureContrast(const Color& foreground, const Color& background, double minRatio = 4.5, int preferLight = 0);
} // namespace noctalia::theme
File diff suppressed because it is too large Load Diff
+215
View File
@@ -0,0 +1,215 @@
#include "theme/image_loader.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iterator>
#include "render/core/image_decoder.h"
namespace noctalia::theme {
namespace {
constexpr int kTarget = 112;
std::vector<uint8_t> readFile(std::string_view path, std::string* err) {
std::ifstream f(std::string(path), std::ios::binary);
if (!f) {
if (err)
*err = "cannot open file";
return {};
}
return std::vector<uint8_t>((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
}
// Hand-port of `image::imageops::sample::resize` (Triangle filter) from
// the Rust `image` crate, which is what matugen / material_colors uses.
// We need byte-for-byte parity with that implementation because a few
// LSB of drift in the resized 112×112 buffer can move the seed to a
// different cluster.
//
// Algorithm: separable scale-aware tent filter, vertical pass first
// into a float intermediate (no clamping/rounding between passes), then
// horizontal pass with clamp+round-to-nearest into u8. For downsampling
// the kernel support widens by the downsample ratio. Operates in 8-bit
// sRGB space (no linearisation), matching image crate.
//
// Pixel-center convention: input pixels are at integer positions; the
// output pixel `o` corresponds to input center `(o + 0.5) * ratio`. The
// kernel is evaluated at `(i - (inputCenter - 0.5)) / sratio` for input
// pixel index `i`. See sample.rs in the image crate for the canonical
// form.
inline float triangleKernel(float x) {
x = x < 0 ? -x : x;
return x < 1.0f ? 1.0f - x : 0.0f;
}
// Vertical pass: src is RGBA u8 (srcW × srcH), dst is RGBA f32 (srcW × dstH).
void verticalSampleU8ToF32(const uint8_t* src, int srcW, int srcH, float* dst, int dstH) {
const float ratio = (float)srcH / (float)dstH;
const float sratio = ratio < 1.0f ? 1.0f : ratio;
const float srcSupport = 1.0f * sratio;
std::vector<float> ws;
for (int outy = 0; outy < dstH; ++outy) {
const float inputyOrig = ((float)outy + 0.5f) * ratio;
int left = (int)std::floor(inputyOrig - srcSupport);
int right = (int)std::ceil(inputyOrig + srcSupport);
if (left < 0)
left = 0;
if (left > srcH - 1)
left = srcH - 1;
if (right < left + 1)
right = left + 1;
if (right > srcH)
right = srcH;
const float inputy = inputyOrig - 0.5f;
ws.clear();
float sum = 0.0f;
for (int i = left; i < right; ++i) {
float w = triangleKernel(((float)i - inputy) / sratio);
ws.push_back(w);
sum += w;
}
for (auto& w : ws)
w /= sum;
for (int x = 0; x < srcW; ++x) {
float t0 = 0, t1 = 0, t2 = 0, t3 = 0;
for (int k = 0; k < (int)ws.size(); ++k) {
const uint8_t* p = src + ((left + k) * srcW + x) * 4;
const float w = ws[k];
t0 += (float)p[0] * w;
t1 += (float)p[1] * w;
t2 += (float)p[2] * w;
t3 += (float)p[3] * w;
}
float* dp = dst + (outy * srcW + x) * 4;
// No clamp / no round — image crate's vertical_sample writes raw
// f32 into Rgba32FImage.
dp[0] = t0;
dp[1] = t1;
dp[2] = t2;
dp[3] = t3;
}
}
}
// Horizontal pass: src is RGBA f32 (srcW × srcH), dst is RGBA u8 (dstW × srcH).
void horizontalSampleF32ToU8(const float* src, int srcW, int srcH, uint8_t* dst, int dstW) {
const float ratio = (float)srcW / (float)dstW;
const float sratio = ratio < 1.0f ? 1.0f : ratio;
const float srcSupport = 1.0f * sratio;
std::vector<float> ws;
for (int outx = 0; outx < dstW; ++outx) {
const float inputxOrig = ((float)outx + 0.5f) * ratio;
int left = (int)std::floor(inputxOrig - srcSupport);
int right = (int)std::ceil(inputxOrig + srcSupport);
if (left < 0)
left = 0;
if (left > srcW - 1)
left = srcW - 1;
if (right < left + 1)
right = left + 1;
if (right > srcW)
right = srcW;
const float inputx = inputxOrig - 0.5f;
ws.clear();
float sum = 0.0f;
for (int i = left; i < right; ++i) {
float w = triangleKernel(((float)i - inputx) / sratio);
ws.push_back(w);
sum += w;
}
for (auto& w : ws)
w /= sum;
for (int y = 0; y < srcH; ++y) {
float t0 = 0, t1 = 0, t2 = 0, t3 = 0;
for (int k = 0; k < (int)ws.size(); ++k) {
const float* p = src + (y * srcW + (left + k)) * 4;
const float w = ws[k];
t0 += p[0] * w;
t1 += p[1] * w;
t2 += p[2] * w;
t3 += p[3] * w;
}
// FloatNearest(clamp(t, 0, 255)) → u8. Rust's f32::round is
// round-half-away-from-zero.
auto toU8 = [](float v) -> uint8_t {
if (v < 0)
v = 0;
if (v > 255)
v = 255;
float r = v < 0.0f ? std::ceil(v - 0.5f) : std::floor(v + 0.5f);
return (uint8_t)r;
};
uint8_t* dp = dst + (y * dstW + outx) * 4;
dp[0] = toU8(t0);
dp[1] = toU8(t1);
dp[2] = toU8(t2);
dp[3] = toU8(t3);
}
}
}
std::vector<uint8_t> triangleResize(const uint8_t* srcRgba, int srcW, int srcH, int dstW, int dstH) {
// image crate order: vertical first → Rgba32FImage(srcW × dstH)
// horizontal → RgbaImage(dstW × dstH)
std::vector<float> tmp((size_t)srcW * (size_t)dstH * 4);
verticalSampleU8ToF32(srcRgba, srcW, srcH, tmp.data(), dstH);
std::vector<uint8_t> dst((size_t)dstW * (size_t)dstH * 4);
horizontalSampleF32ToU8(tmp.data(), srcW, dstH, dst.data(), dstW);
return dst;
}
} // namespace
std::optional<LoadedImage> loadAndResize(std::string_view path, Scheme scheme, std::string* errorMessage) {
auto bytes = readFile(path, errorMessage);
if (bytes.empty())
return std::nullopt;
auto decoded = decodeRasterImage(bytes.data(), bytes.size(), errorMessage);
if (!decoded)
return std::nullopt;
const int srcW = decoded->width;
const int srcH = decoded->height;
if (srcW <= 0 || srcH <= 0) {
if (errorMessage)
*errorMessage = "invalid image dimensions";
return std::nullopt;
}
// Force-resize to 112×112 (aspect ratio ignored, matching matugen which
// calls `image::imageops::resize(112, 112, Triangle)` — that's a force
// resize despite the name).
//
// Implementation: hand-port of the Rust `image` crate's Triangle resize
// (separable scale-aware tent filter, no sRGB linearisation). We tried
// stb_image_resize2 first but its named filters (TRIANGLE/MITCHELL/...)
// use fixed unit support and severely alias at large downsample ratios;
// its BOX filter scales but is uniform-weight, leaving a residual ~10°
// hue offset on certain images. The custom port matches the image crate
// (matugen's underlying lib) within ≤ 2 LSB per channel.
(void)scheme;
std::vector<uint8_t> resizedRgba = triangleResize(decoded->pixels.data(), srcW, srcH, kTarget, kTarget);
LoadedImage out;
out.rgb.resize(kTarget * kTarget * 3);
for (int i = 0; i < kTarget * kTarget; ++i) {
out.rgb[i * 3 + 0] = resizedRgba[i * 4 + 0];
out.rgb[i * 3 + 1] = resizedRgba[i * 4 + 1];
out.rgb[i * 3 + 2] = resizedRgba[i * 4 + 2];
}
return out;
}
} // namespace noctalia::theme
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "theme/scheme.h"
namespace noctalia::theme {
// A 112×112 RGB pixel buffer ready for quantization/clustering. No alpha.
// 112 * 112 * 3 = 37632 bytes.
struct LoadedImage {
std::vector<uint8_t> rgb; // size = 112 * 112 * 3
int width = 112;
int height = 112;
};
// Load `path`, decode via Wuffs, and resize to exactly 112×112 (aspect ratio
// squashed) with alpha stripped. The resize filter is scheme-dependent:
// triangle for M3 schemes, box for the custom schemes.
std::optional<LoadedImage> loadAndResize(std::string_view path, Scheme scheme, std::string* errorMessage = nullptr);
} // namespace noctalia::theme
+46
View File
@@ -0,0 +1,46 @@
#include "theme/json_output.h"
#include <cstdio>
#include <json.hpp>
#include "theme/tokens.h"
namespace noctalia::theme {
namespace {
std::string hexString(uint32_t argb) {
char buf[8];
std::snprintf(buf, sizeof(buf), "#%06x", argb & 0x00ffffffu);
return std::string(buf);
}
nlohmann::ordered_json tokenMap(const std::unordered_map<std::string, uint32_t>& tokens) {
nlohmann::ordered_json out = nlohmann::ordered_json::object();
for (size_t i = 0; i < kTokenCount; ++i) {
const std::string key(kTokens[i]);
auto it = tokens.find(key);
if (it != tokens.end()) {
out[key] = hexString(it->second);
} else {
out[key] = nullptr;
}
}
return out;
}
} // namespace
std::string toJson(const GeneratedPalette& palette, Scheme /*scheme*/, Variant variant) {
if (variant == Variant::Dark)
return tokenMap(palette.dark).dump(2);
if (variant == Variant::Light)
return tokenMap(palette.light).dump(2);
nlohmann::ordered_json root = nlohmann::ordered_json::object();
root["dark"] = tokenMap(palette.dark);
root["light"] = tokenMap(palette.light);
return root.dump(2);
}
} // namespace noctalia::theme
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <string>
#include "theme/palette.h"
#include "theme/scheme.h"
namespace noctalia::theme {
enum class Variant { Dark, Light, Both };
// Serialize a GeneratedPalette to JSON. Values are "#rrggbb" strings emitted
// in the canonical iteration order from tokens.h. `Both` wraps dark+light in
// `{"dark": {...}, "light": {...}}`; `Dark`/`Light` emit a flat token map.
std::string toJson(const GeneratedPalette& palette, Scheme scheme, Variant variant);
} // namespace noctalia::theme
+449
View File
@@ -0,0 +1,449 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "cpp/cam/cam.h"
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/material_dynamic_colors.h"
#include "cpp/quantize/lab.h"
#include "cpp/quantize/wu.h"
#include "cpp/scheme/scheme_content.h"
#include "cpp/scheme/scheme_fruit_salad.h"
#include "cpp/scheme/scheme_monochrome.h"
#include "cpp/scheme/scheme_rainbow.h"
#include "cpp/scheme/scheme_tonal_spot.h"
#include "cpp/utils/utils.h"
#include "theme/palette.h"
#include "theme/palette_generator.h"
#include "theme/scheme.h"
// Material Design 3 scheme path. Quantize the resized pixels, score for the
// dominant colourful seed, build a DynamicScheme, then pull each token via
// MaterialDynamicColors. Matches matugen's pipeline (Rust material_colors
// crate) one-for-one.
namespace mcu = material_color_utilities;
namespace noctalia::theme {
namespace {
// ─── Matugen-faithful quant + score ────────────────────────────────
//
// MCU's QuantizeCelebi → RankedSuggestions diverges from the Rust
// material_colors crate (matugen's underlying lib) in two subtle places
// that swap seed picks on certain images:
//
// 1. MCU's wsmeans.cc sorts swatches by population descending and then
// stores them in std::map<Argb,uint32_t>, so iteration order is
// key-sorted (alpha by argb). The Rust version skips the sort and
// uses IndexMap (insertion order = cluster index order from wu).
// 2. MCU's score.cc uses unstable std::sort to rank scored HCTs;
// the Rust version uses stable sort_by.
//
// Same constants, same math otherwise. We fork wsmeans-result-build and
// score here so the seed matches matugen byte-for-byte. Wu can stay as
// MCU's — it's purely numeric and produces identical output.
struct ClusterEntry {
mcu::Argb argb;
uint32_t population;
};
struct DistanceToIndex {
double distance = 0.0;
int index = 0;
bool operator<(const DistanceToIndex& a) const { return distance < a.distance; }
};
// Hand-port of material_colors Rust QuantizerWsmeans::quantize. Differs
// from MCU's wsmeans.cc in five places that all matter for output parity:
//
// • 10 iterations max, not 100.
// • cluster_indices initialised to i % cluster_count (deterministic),
// not rand() % cluster_count.
// • Point reassignment is unconditional when a closer cluster exists,
// no kMinDeltaE = 3.0 threshold.
// • Termination uses points_moved count, not a "did anything change" bool.
// • cluster_count is NOT additionally clamped to starting_clusters.len()
// (matugen will index OOB if you give it fewer seeds than requested).
//
// The final assembly also preserves cluster index order (matches Rust
// IndexMap insertion order) instead of population-sorting + std::map.
std::vector<ClusterEntry> wsmeansMatugenOrder(const std::vector<mcu::Argb>& input_pixels,
const std::vector<mcu::Argb>& starting_clusters,
uint16_t max_colors) {
if (max_colors == 0 || input_pixels.empty())
return {};
if (max_colors > 256)
max_colors = 256;
// Dedupe input pixels in insertion order, building parallel
// pixels/points/counts arrays. Matches matugen's IndexMap loop.
std::unordered_map<mcu::Argb, int> pixel_to_count;
std::vector<uint32_t> pixels;
std::vector<mcu::Lab> points;
pixels.reserve(input_pixels.size());
points.reserve(input_pixels.size());
for (mcu::Argb pixel : input_pixels) {
auto it = pixel_to_count.find(pixel);
if (it != pixel_to_count.end()) {
it->second++;
} else {
pixels.push_back(pixel);
points.push_back(mcu::LabFromInt(pixel));
pixel_to_count[pixel] = 1;
}
}
int cluster_count = std::min((int)max_colors, (int)points.size());
std::vector<mcu::Lab> clusters;
clusters.reserve(starting_clusters.size());
for (int argb : starting_clusters)
clusters.push_back(mcu::LabFromInt(argb));
// matugen relies on Wu returning max_colors entries and would index
// OOB otherwise. Clamp here (matches MCU's wsmeans.cc) so we don't
// crash on low-cardinality images where Wu returns fewer cubes; the
// result still matches matugen for the common case.
if (!starting_clusters.empty())
cluster_count = std::min(cluster_count, (int)starting_clusters.size());
// Deterministic init, no rand.
std::vector<int> cluster_indices;
cluster_indices.reserve(points.size());
for (size_t i = 0; i < points.size(); i++)
cluster_indices.push_back((int)(i % (size_t)cluster_count));
std::vector<std::vector<DistanceToIndex>> dmat(cluster_count, std::vector<DistanceToIndex>(cluster_count));
int pixel_count_sums[256] = {};
constexpr int kMaxIterations = 10;
for (int iter = 0; iter < kMaxIterations; iter++) {
// Build pairwise cluster distance matrix, then sort each row IN
// PLACE by distance ascending. NB: matugen does the same in-place
// sort, which has a curious side effect — see the reassign loop.
for (int i = 0; i < cluster_count; i++) {
for (int j = i + 1; j < cluster_count; j++) {
double d = clusters[i].DeltaE(clusters[j]);
dmat[j][i] = {d, i};
dmat[i][j] = {d, j};
}
}
for (int i = 0; i < cluster_count; i++) {
std::sort(dmat[i].begin(), dmat[i].end());
}
int points_moved = 0;
for (size_t i = 0; i < points.size(); i++) {
mcu::Lab point = points[i];
int prev_idx = cluster_indices[i];
double prev_d = point.DeltaE(clusters[prev_idx]);
double min_d = prev_d;
int new_idx = -1;
// Quirk match: matugen's reassign loop reads dmat[prev_idx][j]
// (the j-th SMALLEST distance from prev_idx, after the in-place
// sort) but compares against clusters[j] (the j-th cluster by
// original index). The j on each side refers to a different
// thing; the early-out cutoff is monotone in j, so the loop ends
// up only checking the first N clusters (in original order) where
// N is the count of cluster pairs within 4× of prev_d. We replicate
// the same indexing for byte-for-byte parity.
for (int j = 0; j < cluster_count; j++) {
if (dmat[prev_idx][j].distance >= 4.0 * prev_d)
continue;
double d = point.DeltaE(clusters[j]);
if (d < min_d) {
min_d = d;
new_idx = j;
}
}
if (new_idx != -1) {
points_moved++;
cluster_indices[i] = new_idx;
}
}
if (points_moved == 0 && iter > 0)
break;
double sa[256] = {}, sb[256] = {}, sc[256] = {};
for (int i = 0; i < cluster_count; i++)
pixel_count_sums[i] = 0;
for (size_t i = 0; i < points.size(); i++) {
int ci = cluster_indices[i];
int cnt = pixel_to_count[pixels[i]];
pixel_count_sums[ci] += cnt;
sa[ci] += points[i].l * cnt;
sb[ci] += points[i].a * cnt;
sc[ci] += points[i].b * cnt;
}
for (int i = 0; i < cluster_count; i++) {
int cnt = pixel_count_sums[i];
if (cnt == 0) {
clusters[i] = {0, 0, 0};
continue;
}
clusters[i] = {sa[i] / cnt, sb[i] / cnt, sc[i] / cnt};
}
}
// Cluster index order, dedupe by argb, drop empties. Matches matugen
// wsmeans.rs:275-294 exactly (no population sort, no std::map rekey).
std::vector<ClusterEntry> result;
for (int i = 0; i < cluster_count; i++) {
int cnt = pixel_count_sums[i];
if (cnt == 0)
continue;
mcu::Argb argb = mcu::IntFromLab(clusters[i]);
bool dup = false;
for (auto& e : result) {
if (e.argb == argb) {
dup = true;
break;
}
}
if (dup)
continue;
result.push_back({argb, (uint32_t)cnt});
}
return result;
}
// Hand-port of material_colors Rust Score::score (score.rs). Identical
// math to MCU's RankedSuggestions, but iterates clusters in input order
// and uses std::stable_sort.
std::vector<mcu::Argb> scoreMatugen(const std::vector<ClusterEntry>& clusters, int desired, bool filter) {
constexpr double kTargetChroma = 48.0;
constexpr double kWeightProportion = 0.7;
constexpr double kWeightChromaAbove = 0.3;
constexpr double kWeightChromaBelow = 0.1;
constexpr double kCutoffChroma = 5.0;
constexpr double kCutoffExcitedProportion = 0.01;
std::vector<mcu::Hct> colors_hct;
colors_hct.reserve(clusters.size());
std::vector<uint32_t> hue_population(360, 0);
double population_sum = 0.0;
for (const auto& c : clusters) {
mcu::Hct hct(c.argb);
colors_hct.push_back(hct);
int hue = (int)std::floor(hct.get_hue());
hue_population[hue] += c.population;
population_sum += c.population;
}
std::vector<double> hue_excited(360, 0.0);
for (int hue = 0; hue < 360; hue++) {
double prop = hue_population[hue] / population_sum;
for (int i = hue - 14; i < hue + 16; i++) {
int nh = mcu::SanitizeDegreesInt(i);
hue_excited[nh] += prop;
}
}
struct Scored {
mcu::Hct hct;
double score;
};
std::vector<Scored> scored;
scored.reserve(colors_hct.size());
for (mcu::Hct hct : colors_hct) {
int hue = mcu::SanitizeDegreesInt((int)std::round(hct.get_hue()));
double prop = hue_excited[hue];
if (filter && (hct.get_chroma() < kCutoffChroma || prop <= kCutoffExcitedProportion))
continue;
double prop_score = prop * 100.0 * kWeightProportion;
double cw = hct.get_chroma() < kTargetChroma ? kWeightChromaBelow : kWeightChromaAbove;
double chroma_score = (hct.get_chroma() - kTargetChroma) * cw;
scored.push_back({hct, prop_score + chroma_score});
}
// STABLE sort: matches Rust sort_by, preserves cluster insertion order
// when scores tie.
std::stable_sort(scored.begin(), scored.end(),
[](const Scored& a, const Scored& b) { return a.score > b.score; });
std::vector<mcu::Hct> chosen;
for (int diff = 90; diff >= 15; diff--) {
chosen.clear();
for (const auto& e : scored) {
mcu::Hct hct = e.hct;
bool dup = false;
for (const auto& ch : chosen) {
if (mcu::DiffDegrees(hct.get_hue(), ch.get_hue()) < diff) {
dup = true;
break;
}
}
if (!dup) {
chosen.push_back(hct);
if ((int)chosen.size() >= desired)
break;
}
}
if ((int)chosen.size() >= desired)
break;
}
std::vector<mcu::Argb> out;
if (chosen.empty())
out.push_back(0xff4285f4u);
for (auto& h : chosen)
out.push_back(h.ToInt());
return out;
}
// Build pixels → wu seeds → wsmeans (matugen order) → chroma filter →
// matugen-style score → first ranked.
mcu::Argb extractSeed(const std::vector<uint8_t>& rgb112) {
std::vector<mcu::Argb> pixels;
pixels.reserve(rgb112.size() / 3);
for (size_t i = 0; i + 2 < rgb112.size(); i += 3) {
const uint32_t r = rgb112[i + 0];
const uint32_t g = rgb112[i + 1];
const uint32_t b = rgb112[i + 2];
pixels.push_back(0xff000000u | (r << 16) | (g << 8) | b);
}
// Matches QuantizeCelebi: drop transparent, run wu for seeds, then
// wsmeans. Wu is unchanged from MCU.
std::vector<mcu::Argb> opaque;
opaque.reserve(pixels.size());
for (mcu::Argb p : pixels) {
if (mcu::IsOpaque(p))
opaque.push_back(p);
}
auto wu_seeds = mcu::QuantizeWu(opaque, 128);
auto clusters = wsmeansMatugenOrder(opaque, wu_seeds, 128);
// Drop low-chroma clusters before scoring (matches matugen's
// get_source_color_from_image, which calls IndexMap::retain on the
// quantizer output). This DOES skew the proportions in score, but
// matugen ships it that way and we need bug-for-bug parity.
clusters.erase(std::remove_if(clusters.begin(), clusters.end(),
[](const ClusterEntry& c) { return mcu::CamFromInt(c.argb).chroma < 5.0; }),
clusters.end());
auto ranked = scoreMatugen(clusters, 4, true);
return ranked.empty() ? 0xff4285f4u : ranked.front();
}
std::unique_ptr<mcu::DynamicScheme> makeScheme(const mcu::Hct& source, Scheme scheme, bool isDark) {
switch (scheme) {
case Scheme::TonalSpot:
return std::make_unique<mcu::SchemeTonalSpot>(source, isDark, 0.0);
case Scheme::Content:
return std::make_unique<mcu::SchemeContent>(source, isDark, 0.0);
case Scheme::FruitSalad:
return std::make_unique<mcu::SchemeFruitSalad>(source, isDark, 0.0);
case Scheme::Rainbow:
return std::make_unique<mcu::SchemeRainbow>(source, isDark, 0.0);
case Scheme::Monochrome:
return std::make_unique<mcu::SchemeMonochrome>(source, isDark, 0.0);
default:
// Custom schemes are not handled here.
return std::make_unique<mcu::SchemeTonalSpot>(source, isDark, 0.0);
}
}
std::unordered_map<std::string, uint32_t> buildTokenMap(mcu::DynamicScheme& s) {
std::unordered_map<std::string, uint32_t> m;
auto set = [&](const char* k, mcu::DynamicColor dc) { m[k] = dc.GetArgb(s); };
// Primary group
set("primary", mcu::MaterialDynamicColors::Primary());
set("on_primary", mcu::MaterialDynamicColors::OnPrimary());
set("primary_container", mcu::MaterialDynamicColors::PrimaryContainer());
set("on_primary_container", mcu::MaterialDynamicColors::OnPrimaryContainer());
set("inverse_primary", mcu::MaterialDynamicColors::InversePrimary());
set("surface_tint", mcu::MaterialDynamicColors::SurfaceTint());
// Secondary group
set("secondary", mcu::MaterialDynamicColors::Secondary());
set("on_secondary", mcu::MaterialDynamicColors::OnSecondary());
set("secondary_container", mcu::MaterialDynamicColors::SecondaryContainer());
set("on_secondary_container", mcu::MaterialDynamicColors::OnSecondaryContainer());
// Tertiary group
set("tertiary", mcu::MaterialDynamicColors::Tertiary());
set("on_tertiary", mcu::MaterialDynamicColors::OnTertiary());
set("tertiary_container", mcu::MaterialDynamicColors::TertiaryContainer());
set("on_tertiary_container", mcu::MaterialDynamicColors::OnTertiaryContainer());
// Error group
set("error", mcu::MaterialDynamicColors::Error());
set("on_error", mcu::MaterialDynamicColors::OnError());
set("error_container", mcu::MaterialDynamicColors::ErrorContainer());
set("on_error_container", mcu::MaterialDynamicColors::OnErrorContainer());
// Surface
set("surface", mcu::MaterialDynamicColors::Surface());
set("on_surface", mcu::MaterialDynamicColors::OnSurface());
set("surface_variant", mcu::MaterialDynamicColors::SurfaceVariant());
set("on_surface_variant", mcu::MaterialDynamicColors::OnSurfaceVariant());
set("surface_dim", mcu::MaterialDynamicColors::SurfaceDim());
set("surface_bright", mcu::MaterialDynamicColors::SurfaceBright());
// Surface containers
set("surface_container_lowest", mcu::MaterialDynamicColors::SurfaceContainerLowest());
set("surface_container_low", mcu::MaterialDynamicColors::SurfaceContainerLow());
set("surface_container", mcu::MaterialDynamicColors::SurfaceContainer());
set("surface_container_high", mcu::MaterialDynamicColors::SurfaceContainerHigh());
set("surface_container_highest", mcu::MaterialDynamicColors::SurfaceContainerHighest());
// Outline + shadow/scrim
set("outline", mcu::MaterialDynamicColors::Outline());
set("outline_variant", mcu::MaterialDynamicColors::OutlineVariant());
set("shadow", mcu::MaterialDynamicColors::Shadow());
set("scrim", mcu::MaterialDynamicColors::Scrim());
// Inverse
set("inverse_surface", mcu::MaterialDynamicColors::InverseSurface());
set("inverse_on_surface", mcu::MaterialDynamicColors::InverseOnSurface());
// Background (alias of surface per MD3 spec)
set("background", mcu::MaterialDynamicColors::Background());
set("on_background", mcu::MaterialDynamicColors::OnBackground());
// Fixed colors — identical across light/dark.
set("primary_fixed", mcu::MaterialDynamicColors::PrimaryFixed());
set("primary_fixed_dim", mcu::MaterialDynamicColors::PrimaryFixedDim());
set("on_primary_fixed", mcu::MaterialDynamicColors::OnPrimaryFixed());
set("on_primary_fixed_variant", mcu::MaterialDynamicColors::OnPrimaryFixedVariant());
set("secondary_fixed", mcu::MaterialDynamicColors::SecondaryFixed());
set("secondary_fixed_dim", mcu::MaterialDynamicColors::SecondaryFixedDim());
set("on_secondary_fixed", mcu::MaterialDynamicColors::OnSecondaryFixed());
set("on_secondary_fixed_variant", mcu::MaterialDynamicColors::OnSecondaryFixedVariant());
set("tertiary_fixed", mcu::MaterialDynamicColors::TertiaryFixed());
set("tertiary_fixed_dim", mcu::MaterialDynamicColors::TertiaryFixedDim());
set("on_tertiary_fixed", mcu::MaterialDynamicColors::OnTertiaryFixed());
set("on_tertiary_fixed_variant", mcu::MaterialDynamicColors::OnTertiaryFixedVariant());
return m;
}
} // namespace
GeneratedPalette generateMaterial(const std::vector<uint8_t>& rgb112, Scheme scheme) {
const mcu::Argb seed = extractSeed(rgb112);
const mcu::Hct source(seed);
auto darkScheme = makeScheme(source, scheme, true);
auto lightScheme = makeScheme(source, scheme, false);
GeneratedPalette out;
out.dark = buildTokenMap(*darkScheme);
out.light = buildTokenMap(*lightScheme);
return out;
}
} // namespace noctalia::theme
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
#include <string>
#include <unordered_map>
namespace noctalia::theme {
// A fully-generated palette, keyed by the token names from tokens.h. Values
// are packed ARGB (0xffRRGGBB). Serialization to "#rrggbb" lives in
// json_output.cpp.
struct GeneratedPalette {
std::unordered_map<std::string, uint32_t> dark;
std::unordered_map<std::string, uint32_t> light;
};
} // namespace noctalia::theme
+16
View File
@@ -0,0 +1,16 @@
#include "theme/palette_generator.h"
namespace noctalia::theme {
GeneratedPalette generate(const std::vector<uint8_t>& rgb112, Scheme scheme, std::string* errorMessage) {
if (rgb112.size() != 112u * 112u * 3u) {
if (errorMessage)
*errorMessage = "expected 112x112x3 pixel buffer";
return {};
}
if (isMaterialScheme(scheme))
return generateMaterial(rgb112, scheme);
return generateCustom(rgb112, scheme);
}
} // namespace noctalia::theme
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "theme/palette.h"
#include "theme/scheme.h"
namespace noctalia::theme {
// Top-level entry point. Accepts a forced-112×112 RGB (no alpha) pixel buffer
// and a scheme; dispatches to the M3 (MCU-based) or custom (HSL-based) path
// and returns a fully-populated dark+light palette.
//
// The buffer must contain exactly 112 * 112 * 3 bytes. Returns an empty
// palette and writes an error message if generation fails.
GeneratedPalette generate(const std::vector<uint8_t>& rgb112, Scheme scheme, std::string* errorMessage = nullptr);
// Internal paths — exposed for unit testing / analysis tool reuse.
GeneratedPalette generateMaterial(const std::vector<uint8_t>& rgb112, Scheme scheme);
GeneratedPalette generateCustom(const std::vector<uint8_t>& rgb112, Scheme scheme);
} // namespace noctalia::theme
+51
View File
@@ -0,0 +1,51 @@
#include "theme/scheme.h"
namespace noctalia::theme {
std::optional<Scheme> schemeFromString(std::string_view s) {
if (s == "m3-tonal-spot")
return Scheme::TonalSpot;
if (s == "m3-content")
return Scheme::Content;
if (s == "m3-fruit-salad")
return Scheme::FruitSalad;
if (s == "m3-rainbow")
return Scheme::Rainbow;
if (s == "m3-monochrome")
return Scheme::Monochrome;
if (s == "vibrant")
return Scheme::Vibrant;
if (s == "faithful")
return Scheme::Faithful;
if (s == "dysfunctional")
return Scheme::Dysfunctional;
if (s == "muted")
return Scheme::Muted;
return std::nullopt;
}
std::string_view schemeToString(Scheme s) {
switch (s) {
case Scheme::TonalSpot:
return "m3-tonal-spot";
case Scheme::Content:
return "m3-content";
case Scheme::FruitSalad:
return "m3-fruit-salad";
case Scheme::Rainbow:
return "m3-rainbow";
case Scheme::Monochrome:
return "m3-monochrome";
case Scheme::Vibrant:
return "vibrant";
case Scheme::Faithful:
return "faithful";
case Scheme::Dysfunctional:
return "dysfunctional";
case Scheme::Muted:
return "muted";
}
return "m3-tonal-spot";
}
} // namespace noctalia::theme
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <optional>
#include <string>
#include <string_view>
namespace noctalia::theme {
// Color generation strategies. The first five are Material Design 3 schemes
// (TonalPalette + tone tables, built on top of material_color_utilities).
// The last four are custom HSL-space generators with very different
// aesthetics — they are not Material You and will produce different output.
enum class Scheme {
TonalSpot,
Content,
FruitSalad,
Rainbow,
Monochrome,
Vibrant,
Faithful,
Dysfunctional,
Muted,
};
// True for the Material Design 3 schemes.
constexpr bool isMaterialScheme(Scheme s) {
return s == Scheme::TonalSpot || s == Scheme::Content || s == Scheme::FruitSalad || s == Scheme::Rainbow ||
s == Scheme::Monochrome;
}
// Parse a scheme from its CLI string (e.g. "m3-tonal-spot", "vibrant").
// Returns nullopt for unknown values.
std::optional<Scheme> schemeFromString(std::string_view s);
// String form used in CLI / JSON output.
std::string_view schemeToString(Scheme s);
} // namespace noctalia::theme
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#include <array>
#include <string_view>
namespace noctalia::theme {
// Canonical list of colour tokens emitted in every generated palette. The
// order is the iteration order used by json_output. Both the M3 schemes
// (TonalPalette + tone tables) and the custom HSL schemes populate the same
// key set so consumers can treat them interchangeably.
inline constexpr std::array<std::string_view, 51> kTokens = {
"primary",
"on_primary",
"primary_container",
"on_primary_container",
"primary_fixed",
"primary_fixed_dim",
"on_primary_fixed",
"on_primary_fixed_variant",
"surface_tint",
"secondary",
"on_secondary",
"secondary_container",
"on_secondary_container",
"secondary_fixed",
"secondary_fixed_dim",
"on_secondary_fixed",
"on_secondary_fixed_variant",
"tertiary",
"on_tertiary",
"tertiary_container",
"on_tertiary_container",
"tertiary_fixed",
"tertiary_fixed_dim",
"on_tertiary_fixed",
"on_tertiary_fixed_variant",
"error",
"on_error",
"error_container",
"on_error_container",
"surface",
"on_surface",
"surface_variant",
"on_surface_variant",
"surface_dim",
"surface_bright",
"surface_container_lowest",
"surface_container_low",
"surface_container",
"surface_container_high",
"surface_container_highest",
"outline",
"outline_variant",
"shadow",
"scrim",
"inverse_surface",
"inverse_on_surface",
"inverse_primary",
"background",
"on_background",
// Reserved slots for future expansion; not currently emitted to JSON.
"",
"",
};
// Number of tokens actually emitted (the trailing reserved slots are skipped).
inline constexpr size_t kTokenCount = 49;
} // namespace noctalia::theme
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2021 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+71
View File
@@ -0,0 +1,71 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/blend/blend.h"
#include <algorithm>
#include <cstdint>
#include "cpp/cam/cam.h"
#include "cpp/cam/hct.h"
#include "cpp/cam/viewing_conditions.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
Argb BlendHarmonize(const Argb design_color, const Argb key_color) {
Hct from_hct(design_color);
Hct to_hct(key_color);
double difference_degrees = DiffDegrees(from_hct.get_hue(), to_hct.get_hue());
double rotation_degrees = std::min(difference_degrees * 0.5, 15.0);
double output_hue = SanitizeDegreesDouble(
from_hct.get_hue() +
rotation_degrees *
RotationDirection(from_hct.get_hue(), to_hct.get_hue()));
from_hct.set_hue(output_hue);
return from_hct.ToInt();
}
Argb BlendHctHue(const Argb from, const Argb to, const double amount) {
int ucs = BlendCam16Ucs(from, to, amount);
Hct ucs_hct(ucs);
Hct from_hct(from);
from_hct.set_hue(ucs_hct.get_hue());
return from_hct.ToInt();
}
Argb BlendCam16Ucs(const Argb from, const Argb to, const double amount) {
Cam from_cam = CamFromInt(from);
Cam to_cam = CamFromInt(to);
const double a_j = from_cam.jstar;
const double a_a = from_cam.astar;
const double a_b = from_cam.bstar;
const double b_j = to_cam.jstar;
const double b_a = to_cam.astar;
const double b_b = to_cam.bstar;
const double jstar = a_j + (b_j - a_j) * amount;
const double astar = a_a + (b_a - a_a) * amount;
const double bstar = a_b + (b_b - a_b) * amount;
const Cam blended = CamFromUcsAndViewingConditions(jstar, astar, bstar,
kDefaultViewingConditions);
return IntFromCam(blended);
}
} // namespace material_color_utilities
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_BLEND_BLEND_H_
#define CPP_BLEND_BLEND_H_
#include <cstdint>
#include "cpp/utils/utils.h"
namespace material_color_utilities {
Argb BlendHarmonize(const Argb design_color, const Argb key_color);
Argb BlendHctHue(const Argb from, const Argb to, const double amount);
Argb BlendCam16Ucs(const Argb from, const Argb to, const double amount);
} // namespace material_color_utilities
#endif // CPP_BLEND_BLEND_H_
+263
View File
@@ -0,0 +1,263 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/cam/cam.h"
#include <assert.h>
#include <math.h>
#include "cpp/cam/hct_solver.h"
#include "cpp/cam/viewing_conditions.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
Cam CamFromJchAndViewingConditions(double j, double c, double h,
ViewingConditions viewing_conditions);
Cam CamFromUcsAndViewingConditions(
double jstar, double astar, double bstar,
const ViewingConditions &viewing_conditions) {
const double a = astar;
const double b = bstar;
const double m = sqrt(a * a + b * b);
const double m_2 = (exp(m * 0.0228) - 1.0) / 0.0228;
const double c = m_2 / viewing_conditions.fl_root;
double h = atan2(b, a) * (180.0 / kPi);
if (h < 0.0) {
h += 360.0;
}
const double j = jstar / (1 - (jstar - 100) * 0.007);
return CamFromJchAndViewingConditions(j, c, h, viewing_conditions);
}
Cam CamFromIntAndViewingConditions(
Argb argb, const ViewingConditions &viewing_conditions) {
// XYZ from ARGB, inlined.
int red = (argb & 0x00ff0000) >> 16;
int green = (argb & 0x0000ff00) >> 8;
int blue = (argb & 0x000000ff);
double red_l = Linearized(red);
double green_l = Linearized(green);
double blue_l = Linearized(blue);
double x = 0.41233895 * red_l + 0.35762064 * green_l + 0.18051042 * blue_l;
double y = 0.2126 * red_l + 0.7152 * green_l + 0.0722 * blue_l;
double z = 0.01932141 * red_l + 0.11916382 * green_l + 0.95034478 * blue_l;
// Convert XYZ to 'cone'/'rgb' responses
double r_c = 0.401288 * x + 0.650173 * y - 0.051461 * z;
double g_c = -0.250268 * x + 1.204414 * y + 0.045854 * z;
double b_c = -0.002079 * x + 0.048952 * y + 0.953127 * z;
// Discount illuminant.
double r_d = viewing_conditions.rgb_d[0] * r_c;
double g_d = viewing_conditions.rgb_d[1] * g_c;
double b_d = viewing_conditions.rgb_d[2] * b_c;
// Chromatic adaptation.
double r_af = pow(viewing_conditions.fl * fabs(r_d) / 100.0, 0.42);
double g_af = pow(viewing_conditions.fl * fabs(g_d) / 100.0, 0.42);
double b_af = pow(viewing_conditions.fl * fabs(b_d) / 100.0, 0.42);
double r_a = Signum(r_d) * 400.0 * r_af / (r_af + 27.13);
double g_a = Signum(g_d) * 400.0 * g_af / (g_af + 27.13);
double b_a = Signum(b_d) * 400.0 * b_af / (b_af + 27.13);
// Redness-greenness
double a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0;
double b = (r_a + g_a - 2.0 * b_a) / 9.0;
double u = (20.0 * r_a + 20.0 * g_a + 21.0 * b_a) / 20.0;
double p2 = (40.0 * r_a + 20.0 * g_a + b_a) / 20.0;
double radians = atan2(b, a);
double degrees = radians * 180.0 / kPi;
double hue = SanitizeDegreesDouble(degrees);
double hue_radians = hue * kPi / 180.0;
double ac = p2 * viewing_conditions.nbb;
double j = 100.0 * pow(ac / viewing_conditions.aw,
viewing_conditions.c * viewing_conditions.z);
double q = (4.0 / viewing_conditions.c) * sqrt(j / 100.0) *
(viewing_conditions.aw + 4.0) * viewing_conditions.fl_root;
double hue_prime = hue < 20.14 ? hue + 360 : hue;
double e_hue = 0.25 * (cos(hue_prime * kPi / 180.0 + 2.0) + 3.8);
double p1 =
50000.0 / 13.0 * e_hue * viewing_conditions.n_c * viewing_conditions.ncb;
double t = p1 * sqrt(a * a + b * b) / (u + 0.305);
double alpha =
pow(t, 0.9) *
pow(1.64 - pow(0.29, viewing_conditions.background_y_to_white_point_y),
0.73);
double c = alpha * sqrt(j / 100.0);
double m = c * viewing_conditions.fl_root;
double s = 50.0 * sqrt((alpha * viewing_conditions.c) /
(viewing_conditions.aw + 4.0));
double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
double mstar = 1.0 / 0.0228 * log(1.0 + 0.0228 * m);
double astar = mstar * cos(hue_radians);
double bstar = mstar * sin(hue_radians);
return {hue, c, j, q, m, s, jstar, astar, bstar};
}
Cam CamFromInt(Argb argb) {
return CamFromIntAndViewingConditions(argb, kDefaultViewingConditions);
}
Argb IntFromCamAndViewingConditions(Cam cam,
ViewingConditions viewing_conditions) {
double alpha = (cam.chroma == 0.0 || cam.j == 0.0)
? 0.0
: cam.chroma / sqrt(cam.j / 100.0);
double t = pow(
alpha / pow(1.64 - pow(0.29,
viewing_conditions.background_y_to_white_point_y),
0.73),
1.0 / 0.9);
double h_rad = cam.hue * kPi / 180.0;
double e_hue = 0.25 * (cos(h_rad + 2.0) + 3.8);
double ac =
viewing_conditions.aw *
pow(cam.j / 100.0, 1.0 / viewing_conditions.c / viewing_conditions.z);
double p1 = e_hue * (50000.0 / 13.0) * viewing_conditions.n_c *
viewing_conditions.ncb;
double p2 = ac / viewing_conditions.nbb;
double h_sin = sin(h_rad);
double h_cos = cos(h_rad);
double gamma = 23.0 * (p2 + 0.305) * t /
(23.0 * p1 + 11.0 * t * h_cos + 108.0 * t * h_sin);
double a = gamma * h_cos;
double b = gamma * h_sin;
double r_a = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
double g_a = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
double b_a = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
double r_c_base = fmax(0, (27.13 * fabs(r_a)) / (400.0 - fabs(r_a)));
double r_c =
Signum(r_a) * (100.0 / viewing_conditions.fl) * pow(r_c_base, 1.0 / 0.42);
double g_c_base = fmax(0, (27.13 * fabs(g_a)) / (400.0 - fabs(g_a)));
double g_c =
Signum(g_a) * (100.0 / viewing_conditions.fl) * pow(g_c_base, 1.0 / 0.42);
double b_c_base = fmax(0, (27.13 * fabs(b_a)) / (400.0 - fabs(b_a)));
double b_c =
Signum(b_a) * (100.0 / viewing_conditions.fl) * pow(b_c_base, 1.0 / 0.42);
double r_x = r_c / viewing_conditions.rgb_d[0];
double g_x = g_c / viewing_conditions.rgb_d[1];
double b_x = b_c / viewing_conditions.rgb_d[2];
double x = 1.86206786 * r_x - 1.01125463 * g_x + 0.14918677 * b_x;
double y = 0.38752654 * r_x + 0.62144744 * g_x - 0.00897398 * b_x;
double z = -0.01584150 * r_x - 0.03412294 * g_x + 1.04996444 * b_x;
// intFromXyz
double r_l = 3.2406 * x - 1.5372 * y - 0.4986 * z;
double g_l = -0.9689 * x + 1.8758 * y + 0.0415 * z;
double b_l = 0.0557 * x - 0.2040 * y + 1.0570 * z;
int red = Delinearized(r_l);
int green = Delinearized(g_l);
int blue = Delinearized(b_l);
return ArgbFromRgb(red, green, blue);
}
Argb IntFromCam(Cam cam) {
return IntFromCamAndViewingConditions(cam, kDefaultViewingConditions);
}
Cam CamFromJchAndViewingConditions(double j, double c, double h,
ViewingConditions viewing_conditions) {
double q = (4.0 / viewing_conditions.c) * sqrt(j / 100.0) *
(viewing_conditions.aw + 4.0) * (viewing_conditions.fl_root);
double m = c * viewing_conditions.fl_root;
double alpha = c / sqrt(j / 100.0);
double s = 50.0 * sqrt((alpha * viewing_conditions.c) /
(viewing_conditions.aw + 4.0));
double hue_radians = h * kPi / 180.0;
double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
double mstar = 1.0 / 0.0228 * log(1.0 + 0.0228 * m);
double astar = mstar * cos(hue_radians);
double bstar = mstar * sin(hue_radians);
return {h, c, j, q, m, s, jstar, astar, bstar};
}
double CamDistance(Cam a, Cam b) {
double d_j = a.jstar - b.jstar;
double d_a = a.astar - b.astar;
double d_b = a.bstar - b.bstar;
double d_e_prime = sqrt(d_j * d_j + d_a * d_a + d_b * d_b);
double d_e = 1.41 * pow(d_e_prime, 0.63);
return d_e;
}
Argb IntFromHcl(double hue, double chroma, double lstar) {
return SolveToInt(hue, chroma, lstar);
}
Cam CamFromXyzAndViewingConditions(
double x, double y, double z, const ViewingConditions &viewing_conditions) {
// Convert XYZ to 'cone'/'rgb' responses
double r_c = 0.401288 * x + 0.650173 * y - 0.051461 * z;
double g_c = -0.250268 * x + 1.204414 * y + 0.045854 * z;
double b_c = -0.002079 * x + 0.048952 * y + 0.953127 * z;
// Discount illuminant.
double r_d = viewing_conditions.rgb_d[0] * r_c;
double g_d = viewing_conditions.rgb_d[1] * g_c;
double b_d = viewing_conditions.rgb_d[2] * b_c;
// Chromatic adaptation.
double r_af = pow(viewing_conditions.fl * fabs(r_d) / 100.0, 0.42);
double g_af = pow(viewing_conditions.fl * fabs(g_d) / 100.0, 0.42);
double b_af = pow(viewing_conditions.fl * fabs(b_d) / 100.0, 0.42);
double r_a = Signum(r_d) * 400.0 * r_af / (r_af + 27.13);
double g_a = Signum(g_d) * 400.0 * g_af / (g_af + 27.13);
double b_a = Signum(b_d) * 400.0 * b_af / (b_af + 27.13);
// Redness-greenness
double a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0;
double b = (r_a + g_a - 2.0 * b_a) / 9.0;
double u = (20.0 * r_a + 20.0 * g_a + 21.0 * b_a) / 20.0;
double p2 = (40.0 * r_a + 20.0 * g_a + b_a) / 20.0;
double radians = atan2(b, a);
double degrees = radians * 180.0 / kPi;
double hue = SanitizeDegreesDouble(degrees);
double hue_radians = hue * kPi / 180.0;
double ac = p2 * viewing_conditions.nbb;
double j = 100.0 * pow(ac / viewing_conditions.aw,
viewing_conditions.c * viewing_conditions.z);
double q = (4.0 / viewing_conditions.c) * sqrt(j / 100.0) *
(viewing_conditions.aw + 4.0) * viewing_conditions.fl_root;
double hue_prime = hue < 20.14 ? hue + 360 : hue;
double e_hue = 0.25 * (cos(hue_prime * kPi / 180.0 + 2.0) + 3.8);
double p1 =
50000.0 / 13.0 * e_hue * viewing_conditions.n_c * viewing_conditions.ncb;
double t = p1 * sqrt(a * a + b * b) / (u + 0.305);
double alpha =
pow(t, 0.9) *
pow(1.64 - pow(0.29, viewing_conditions.background_y_to_white_point_y),
0.73);
double c = alpha * sqrt(j / 100.0);
double m = c * viewing_conditions.fl_root;
double s = 50.0 * sqrt((alpha * viewing_conditions.c) /
(viewing_conditions.aw + 4.0));
double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
double mstar = 1.0 / 0.0228 * log(1.0 + 0.0228 * m);
double astar = mstar * cos(hue_radians);
double bstar = mstar * sin(hue_radians);
return {hue, c, j, q, m, s, jstar, astar, bstar};
}
} // namespace material_color_utilities
+53
View File
@@ -0,0 +1,53 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_CAM_CAM_H_
#define CPP_CAM_CAM_H_
#include "cpp/cam/viewing_conditions.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
struct Cam {
double hue = 0.0;
double chroma = 0.0;
double j = 0.0;
double q = 0.0;
double m = 0.0;
double s = 0.0;
double jstar = 0.0;
double astar = 0.0;
double bstar = 0.0;
};
Cam CamFromInt(Argb argb);
Cam CamFromIntAndViewingConditions(Argb argb,
const ViewingConditions &viewing_conditions);
Argb IntFromHcl(double hue, double chroma, double lstar);
Argb IntFromCam(Cam cam);
Cam CamFromUcsAndViewingConditions(double jstar, double astar, double bstar,
const ViewingConditions &viewing_conditions);
/**
* Given color expressed in the XYZ color space and viewed
* in [viewingConditions], converts the color to CAM16.
*/
Cam CamFromXyzAndViewingConditions(double x, double y, double z,
const ViewingConditions &viewing_conditions);
} // namespace material_color_utilities
#endif // CPP_CAM_CAM_H_
+57
View File
@@ -0,0 +1,57 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/cam/hct.h"
#include "cpp/cam/hct_solver.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
Hct::Hct(double hue, double chroma, double tone) {
SetInternalState(SolveToInt(hue, chroma, tone));
}
Hct::Hct(Argb argb) { SetInternalState(argb); }
double Hct::get_hue() const { return hue_; }
double Hct::get_chroma() const { return chroma_; }
double Hct::get_tone() const { return tone_; }
Argb Hct::ToInt() const { return argb_; }
void Hct::set_hue(double new_hue) {
SetInternalState(SolveToInt(new_hue, chroma_, tone_));
}
void Hct::set_chroma(double new_chroma) {
SetInternalState(SolveToInt(hue_, new_chroma, tone_));
}
void Hct::set_tone(double new_tone) {
SetInternalState(SolveToInt(hue_, chroma_, new_tone));
}
void Hct::SetInternalState(Argb argb) {
argb_ = argb;
Cam cam = CamFromInt(argb);
hue_ = cam.hue;
chroma_ = cam.chroma;
tone_ = LstarFromArgb(argb);
}
} // namespace material_color_utilities
+137
View File
@@ -0,0 +1,137 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_CAM_HCT_H_
#define CPP_CAM_HCT_H_
#include "cpp/utils/utils.h"
namespace material_color_utilities {
/**
* HCT: hue, chroma, and tone.
*
* A color system built using CAM16 hue and chroma, and L* (lightness) from
* the L*a*b* color space, providing a perceptually accurate
* color measurement system that can also accurately render what colors
* will appear as in different lighting environments.
*
* Using L* creates a link between the color system, contrast, and thus
* accessibility. Contrast ratio depends on relative luminance, or Y in the XYZ
* color space. L*, or perceptual luminance can be calculated from Y.
*
* Unlike Y, L* is linear to human perception, allowing trivial creation of
* accurate color tones.
*
* Unlike contrast ratio, measuring contrast in L* is linear, and simple to
* calculate. A difference of 40 in HCT tone guarantees a contrast ratio >= 3.0,
* and a difference of 50 guarantees a contrast ratio >= 4.5.
*/
class Hct {
public:
/**
* Creates an HCT color from hue, chroma, and tone.
*
* @param hue 0 <= hue < 360; invalid values are corrected.
* @param chroma >= 0; the maximum value of chroma depends on the hue
* and tone. May be lower than the requested chroma.
* @param tone 0 <= tone <= 100; invalid values are corrected.
* @return HCT representation of a color in default viewing conditions.
*/
Hct(double hue, double chroma, double tone);
/**
* Creates an HCT color from a color.
*
* @param argb ARGB representation of a color.
* @return HCT representation of a color in default viewing conditions
*/
explicit Hct(Argb argb);
/**
* Returns the hue of the color.
*
* @return hue of the color, in degrees.
*/
double get_hue() const;
/**
* Returns the chroma of the color.
*
* @return chroma of the color.
*/
double get_chroma() const;
/**
* Returns the tone of the color.
*
* @return tone of the color, satisfying 0 <= tone <= 100.
*/
double get_tone() const;
/**
* Returns the color in ARGB format.
*
* @return an integer, representing the color in ARGB format.
*/
Argb ToInt() const;
/**
* Sets the hue of this color. Chroma may decrease because chroma has a
* different maximum for any given hue and tone.
*
* @param new_hue 0 <= new_hue < 360; invalid values are corrected.
*/
void set_hue(double new_hue);
/**
* Sets the chroma of this color. Chroma may decrease because chroma has a
* different maximum for any given hue and tone.
*
* @param new_chroma 0 <= new_chroma < ?
*/
void set_chroma(double new_chroma);
/**
* Sets the tone of this color. Chroma may decrease because chroma has a
* different maximum for any given hue and tone.
*
* @param new_tone 0 <= new_tone <= 100; invalid valids are corrected.
*/
void set_tone(double new_tone);
/**
* For using HCT as a key in a ordered map.
*/
bool operator<(const Hct& a) const { return hue_ < a.hue_; }
private:
/**
* Sets the Hct object to represent an sRGB color.
*
* @param argb the new color as an integer in ARGB format.
*/
void SetInternalState(Argb argb);
double hue_ = 0.0;
double chroma_ = 0.0;
double tone_ = 0.0;
Argb argb_ = 0;
};
} // namespace material_color_utilities
#endif // CPP_CAM_HCT_H_
@@ -0,0 +1,526 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/cam/hct_solver.h"
#include <math.h>
#include "cpp/cam/viewing_conditions.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
constexpr double kScaledDiscountFromLinrgb[3][3] = {
{
0.001200833568784504,
0.002389694492170889,
0.0002795742885861124,
},
{
0.0005891086651375999,
0.0029785502573438758,
0.0003270666104008398,
},
{
0.00010146692491640572,
0.0005364214359186694,
0.0032979401770712076,
},
};
constexpr double kLinrgbFromScaledDiscount[3][3] = {
{
1373.2198709594231,
-1100.4251190754821,
-7.278681089101213,
},
{
-271.815969077903,
559.6580465940733,
-32.46047482791194,
},
{
1.9622899599665666,
-57.173814538844006,
308.7233197812385,
},
};
constexpr double kYFromLinrgb[3] = {0.2126, 0.7152, 0.0722};
constexpr double kCriticalPlanes[255] = {
0.015176349177441876, 0.045529047532325624, 0.07588174588720938,
0.10623444424209313, 0.13658714259697685, 0.16693984095186062,
0.19729253930674434, 0.2276452376616281, 0.2579979360165119,
0.28835063437139563, 0.3188300904430532, 0.350925934958123,
0.3848314933096426, 0.42057480301049466, 0.458183274052838,
0.4976837250274023, 0.5391024159806381, 0.5824650784040898,
0.6277969426914107, 0.6751227633498623, 0.7244668422128921,
0.775853049866786, 0.829304845476233, 0.8848452951698498,
0.942497089126609, 1.0022825574869039, 1.0642236851973577,
1.1283421258858297, 1.1946592148522128, 1.2631959812511864,
1.3339731595349034, 1.407011200216447, 1.4823302800086415,
1.5599503113873272, 1.6398909516233677, 1.7221716113234105,
1.8068114625156377, 1.8938294463134073, 1.9832442801866852,
2.075074464868551, 2.1693382909216234, 2.2660538449872063,
2.36523901573795, 2.4669114995532007, 2.5710888059345764,
2.6777882626779785, 2.7870270208169257, 2.898822059350997,
3.0131901897720907, 3.1301480604002863, 3.2497121605402226,
3.3718988244681087, 3.4967242352587946, 3.624204428461639,
3.754355295633311, 3.887192587735158, 4.022731918402185,
4.160988767090289, 4.301978482107941, 4.445716283538092,
4.592217266055746, 4.741496401646282, 4.893568542229298,
5.048448422192488, 5.20615066083972, 5.3666897647573375,
5.5300801301023865, 5.696336044816294, 5.865471690767354,
6.037501145825082, 6.212438385869475, 6.390297286737924,
6.571091626112461, 6.7548350853498045, 6.941541251256611,
7.131223617812143, 7.323895587840543, 7.5195704746346665,
7.7182615035334345, 7.919981813454504, 8.124744458384042,
8.332562408825165, 8.543448553206703, 8.757415699253682,
8.974476575321063, 9.194643831691977, 9.417930041841839,
9.644347703669503, 9.873909240696694, 10.106627003236781,
10.342513269534024, 10.58158024687427, 10.8238400726681,
11.069304815507364, 11.317986476196008, 11.569896988756009,
11.825048221409341, 12.083451977536606, 12.345119996613247,
12.610063955123938, 12.878295467455942, 13.149826086772048,
13.42466730586372, 13.702830557985108, 13.984327217668513,
14.269168601521828, 14.55736596900856, 14.848930523210871,
15.143873411576273, 15.44220572664832, 15.743938506781891,
16.04908273684337, 16.35764934889634, 16.66964922287304,
16.985093187232053, 17.30399201960269, 17.62635644741625,
17.95219714852476, 18.281524751807332, 18.614349837764564,
18.95068293910138, 19.290534541298456, 19.633915083172692,
19.98083495742689, 20.331304511189067, 20.685334046541502,
21.042933821039977, 21.404114048223256, 21.76888489811322,
22.137256497705877, 22.50923893145328, 22.884842241736916,
23.264076429332462, 23.6469514538663, 24.033477234264016,
24.42366364919083, 24.817520537484558, 25.21505769858089,
25.61628489293138, 26.021211842414342, 26.429848230738664,
26.842203703840827, 27.258287870275353, 27.678110301598522,
28.10168053274597, 28.529008062403893, 28.96010235337422,
29.39497283293396, 29.83362889318845, 30.276079891419332,
30.722335150426627, 31.172403958865512, 31.62629557157785,
32.08401920991837, 32.54558406207592, 33.010999283389665,
33.4802739966603, 33.953417292456834, 34.430438229418264,
34.911345834551085, 35.39614910352207, 35.88485700094671,
36.37747846067349, 36.87402238606382, 37.37449765026789,
37.87891309649659, 38.38727753828926, 38.89959975977785,
39.41588851594697, 39.93615253289054, 40.460400508064545,
40.98864111053629, 41.520882981230194, 42.05713473317016,
42.597404951718396, 43.141702194811224, 43.6900349931913,
44.24241185063697, 44.798841244188324, 45.35933162437017,
45.92389141541209, 46.49252901546552, 47.065252796817916,
47.64207110610409, 48.22299226451468, 48.808024568002054,
49.3971762874833, 49.9904556690408, 50.587870934119984,
51.189430279724725, 51.79514187861014, 52.40501387947288,
53.0190544071392, 53.637271562750364, 54.259673423945976,
54.88626804504493, 55.517063457223934, 56.15206766869424,
56.79128866487574, 57.43473440856916, 58.08241284012621,
58.734331877617365, 59.39049941699807, 60.05092333227251,
60.715611475655585, 61.38457167773311, 62.057811747619894,
62.7353394731159, 63.417162620860914, 64.10328893648692,
64.79372614476921, 65.48848194977529, 66.18756403501224,
66.89098006357258, 67.59873767827808, 68.31084450182222,
69.02730813691093, 69.74813616640164, 70.47333615344107,
71.20291564160104, 71.93688215501312, 72.67524319850172,
73.41800625771542, 74.16517879925733, 74.9167682708136,
75.67278210128072, 76.43322770089146, 77.1981124613393,
77.96744375590167, 78.74122893956174, 79.51947534912904,
80.30219030335869, 81.08938110306934, 81.88105503125999,
82.67721935322541, 83.4778813166706, 84.28304815182372,
85.09272707154808, 85.90692527145302, 86.72564993000343,
87.54890820862819, 88.3767072518277, 89.2090541872801,
90.04595612594655, 90.88742016217518, 91.73345337380438,
92.58406282226491, 93.43925555268066, 94.29903859396902,
95.16341895893969, 96.03240364439274, 96.9059996312159,
97.78421388448044, 98.6670533535366, 99.55452497210776,
};
/**
* Sanitizes a small enough angle in radians.
*
* @param angle An angle in radians; must not deviate too much from 0.
* @return A coterminal angle between 0 and 2pi.
*/
double SanitizeRadians(double angle) { return fmod(angle + kPi * 8, kPi * 2); }
/**
* Delinearizes an RGB component, returning a floating-point number.
*
* @param rgb_component 0.0 <= rgb_component <= 100.0, represents linear R/G/B
* channel
* @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
*/
double TrueDelinearized(double rgb_component) {
double normalized = rgb_component / 100.0;
double delinearized = 0.0;
if (normalized <= 0.0031308) {
delinearized = normalized * 12.92;
} else {
delinearized = 1.055 * pow(normalized, 1.0 / 2.4) - 0.055;
}
return delinearized * 255.0;
}
double ChromaticAdaptation(double component) {
double af = pow(abs(component), 0.42);
return Signum(component) * 400.0 * af / (af + 27.13);
}
/**
* Returns the hue of a linear RGB color in CAM16.
*
* @param linrgb The linear RGB coordinates of a color.
* @return The hue of the color in CAM16, in radians.
*/
double HueOf(Vec3 linrgb) {
Vec3 scaledDiscount = MatrixMultiply(linrgb, kScaledDiscountFromLinrgb);
double r_a = ChromaticAdaptation(scaledDiscount.a);
double g_a = ChromaticAdaptation(scaledDiscount.b);
double b_a = ChromaticAdaptation(scaledDiscount.c);
// redness-greenness
double a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0;
// yellowness-blueness
double b = (r_a + g_a - 2.0 * b_a) / 9.0;
return atan2(b, a);
}
bool AreInCyclicOrder(double a, double b, double c) {
double delta_a_b = SanitizeRadians(b - a);
double delta_a_c = SanitizeRadians(c - a);
return delta_a_b < delta_a_c;
}
/**
* Solves the lerp equation.
*
* @param source The starting number.
* @param mid The number in the middle.
* @param target The ending number.
* @return A number t such that lerp(source, target, t) = mid.
*/
double Intercept(double source, double mid, double target) {
return (mid - source) / (target - source);
}
Vec3 LerpPoint(Vec3 source, double t, Vec3 target) {
return (Vec3){
source.a + (target.a - source.a) * t,
source.b + (target.b - source.b) * t,
source.c + (target.c - source.c) * t,
};
}
double GetAxis(Vec3 vector, int axis) {
switch (axis) {
case 0:
return vector.a;
case 1:
return vector.b;
case 2:
return vector.c;
default:
return -1.0;
}
}
/**
* Intersects a segment with a plane.
*
* @param source The coordinates of point A.
* @param coordinate The R-, G-, or B-coordinate of the plane.
* @param target The coordinates of point B.
* @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
* @return The intersection point of the segment AB with the plane R=coordinate,
* G=coordinate, or B=coordinate
*/
Vec3 SetCoordinate(Vec3 source, double coordinate, Vec3 target, int axis) {
double t =
Intercept(GetAxis(source, axis), coordinate, GetAxis(target, axis));
return LerpPoint(source, t, target);
}
bool IsBounded(double x) { return 0.0 <= x && x <= 100.0; }
/**
* Returns the nth possible vertex of the polygonal intersection.
*
* @param y The Y value of the plane.
* @param n The zero-based index of the point. 0 <= n <= 11.
* @return The nth possible vertex of the polygonal intersection of the y plane
* and the RGB cube, in linear RGB coordinates, if it exists. If this possible
* vertex lies outside of the cube,
* [-1.0, -1.0, -1.0] is returned.
*/
Vec3 NthVertex(double y, int n) {
double k_r = kYFromLinrgb[0];
double k_g = kYFromLinrgb[1];
double k_b = kYFromLinrgb[2];
double coord_a = n % 4 <= 1 ? 0.0 : 100.0;
double coord_b = n % 2 == 0 ? 0.0 : 100.0;
if (n < 4) {
double g = coord_a;
double b = coord_b;
double r = (y - g * k_g - b * k_b) / k_r;
if (IsBounded(r)) {
return (Vec3){r, g, b};
} else {
return (Vec3){-1.0, -1.0, -1.0};
}
} else if (n < 8) {
double b = coord_a;
double r = coord_b;
double g = (y - r * k_r - b * k_b) / k_g;
if (IsBounded(g)) {
return (Vec3){r, g, b};
} else {
return (Vec3){-1.0, -1.0, -1.0};
}
} else {
double r = coord_a;
double g = coord_b;
double b = (y - r * k_r - g * k_g) / k_b;
if (IsBounded(b)) {
return (Vec3){r, g, b};
} else {
return (Vec3){-1.0, -1.0, -1.0};
}
}
}
/**
* Finds the segment containing the desired color.
*
* @param y The Y value of the color.
* @param target_hue The hue of the color.
* @return A list of two sets of linear RGB coordinates, each corresponding to
* an endpoint of the segment containing the desired color.
*/
void BisectToSegment(double y, double target_hue, Vec3 out[2]) {
Vec3 left = (Vec3){-1.0, -1.0, -1.0};
Vec3 right = left;
double left_hue = 0.0;
double right_hue = 0.0;
bool initialized = false;
bool uncut = true;
for (int n = 0; n < 12; n++) {
Vec3 mid = NthVertex(y, n);
if (mid.a < 0) {
continue;
}
double mid_hue = HueOf(mid);
if (!initialized) {
left = mid;
right = mid;
left_hue = mid_hue;
right_hue = mid_hue;
initialized = true;
continue;
}
if (uncut || AreInCyclicOrder(left_hue, mid_hue, right_hue)) {
uncut = false;
if (AreInCyclicOrder(left_hue, target_hue, mid_hue)) {
right = mid;
right_hue = mid_hue;
} else {
left = mid;
left_hue = mid_hue;
}
}
}
out[0] = left;
out[1] = right;
}
Vec3 Midpoint(Vec3 a, Vec3 b) {
return (Vec3){
(a.a + b.a) / 2,
(a.b + b.b) / 2,
(a.c + b.c) / 2,
};
}
int CriticalPlaneBelow(double x) { return (int)floor(x - 0.5); }
int CriticalPlaneAbove(double x) { return (int)ceil(x - 0.5); }
/**
* Finds a color with the given Y and hue on the boundary of the cube.
*
* @param y The Y value of the color.
* @param target_hue The hue of the color.
* @return The desired color, in linear RGB coordinates.
*/
Vec3 BisectToLimit(double y, double target_hue) {
Vec3 segment[2];
BisectToSegment(y, target_hue, segment);
Vec3 left = segment[0];
double left_hue = HueOf(left);
Vec3 right = segment[1];
for (int axis = 0; axis < 3; axis++) {
if (GetAxis(left, axis) != GetAxis(right, axis)) {
int l_plane = -1;
int r_plane = 255;
if (GetAxis(left, axis) < GetAxis(right, axis)) {
l_plane = CriticalPlaneBelow(TrueDelinearized(GetAxis(left, axis)));
r_plane = CriticalPlaneAbove(TrueDelinearized(GetAxis(right, axis)));
} else {
l_plane = CriticalPlaneAbove(TrueDelinearized(GetAxis(left, axis)));
r_plane = CriticalPlaneBelow(TrueDelinearized(GetAxis(right, axis)));
}
for (int i = 0; i < 8; i++) {
if (abs(r_plane - l_plane) <= 1) {
break;
} else {
int m_plane = (int)floor((l_plane + r_plane) / 2.0);
double mid_plane_coordinate = kCriticalPlanes[m_plane];
Vec3 mid = SetCoordinate(left, mid_plane_coordinate, right, axis);
double mid_hue = HueOf(mid);
if (AreInCyclicOrder(left_hue, target_hue, mid_hue)) {
right = mid;
r_plane = m_plane;
} else {
left = mid;
left_hue = mid_hue;
l_plane = m_plane;
}
}
}
}
}
return Midpoint(left, right);
}
double InverseChromaticAdaptation(double adapted) {
double adapted_abs = abs(adapted);
double base = fmax(0, 27.13 * adapted_abs / (400.0 - adapted_abs));
return Signum(adapted) * pow(base, 1.0 / 0.42);
}
/**
* Finds a color with the given hue, chroma, and Y.
*
* @param hue_radians The desired hue in radians.
* @param chroma The desired chroma.
* @param y The desired Y.
* @return The desired color as a hexadecimal integer, if found; 0 otherwise.
*/
Argb FindResultByJ(double hue_radians, double chroma, double y) {
// Initial estimate of j.
double j = sqrt(y) * 11.0;
// ===========================================================
// Operations inlined from Cam16 to avoid repeated calculation
// ===========================================================
ViewingConditions viewing_conditions = kDefaultViewingConditions;
double t_inner_coeff =
1 /
pow(1.64 - pow(0.29, viewing_conditions.background_y_to_white_point_y),
0.73);
double e_hue = 0.25 * (cos(hue_radians + 2.0) + 3.8);
double p1 = e_hue * (50000.0 / 13.0) * viewing_conditions.n_c *
viewing_conditions.ncb;
double h_sin = sin(hue_radians);
double h_cos = cos(hue_radians);
for (int iteration_round = 0; iteration_round < 5; iteration_round++) {
// ===========================================================
// Operations inlined from Cam16 to avoid repeated calculation
// ===========================================================
double j_normalized = j / 100.0;
double alpha =
chroma == 0.0 || j == 0.0 ? 0.0 : chroma / sqrt(j_normalized);
double t = pow(alpha * t_inner_coeff, 1.0 / 0.9);
double ac =
viewing_conditions.aw *
pow(j_normalized, 1.0 / viewing_conditions.c / viewing_conditions.z);
double p2 = ac / viewing_conditions.nbb;
double gamma = 23.0 * (p2 + 0.305) * t /
(23.0 * p1 + 11 * t * h_cos + 108.0 * t * h_sin);
double a = gamma * h_cos;
double b = gamma * h_sin;
double r_a = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
double g_a = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
double b_a = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
double r_c_scaled = InverseChromaticAdaptation(r_a);
double g_c_scaled = InverseChromaticAdaptation(g_a);
double b_c_scaled = InverseChromaticAdaptation(b_a);
Vec3 scaled = (Vec3){r_c_scaled, g_c_scaled, b_c_scaled};
Vec3 linrgb = MatrixMultiply(scaled, kLinrgbFromScaledDiscount);
// ===========================================================
// Operations inlined from Cam16 to avoid repeated calculation
// ===========================================================
if (linrgb.a < 0 || linrgb.b < 0 || linrgb.c < 0) {
return 0;
}
double k_r = kYFromLinrgb[0];
double k_g = kYFromLinrgb[1];
double k_b = kYFromLinrgb[2];
double fnj = k_r * linrgb.a + k_g * linrgb.b + k_b * linrgb.c;
if (fnj <= 0) {
return 0;
}
if (iteration_round == 4 || abs(fnj - y) < 0.002) {
if (linrgb.a > 100.01 || linrgb.b > 100.01 || linrgb.c > 100.01) {
return 0;
}
return ArgbFromLinrgb(linrgb);
}
// Iterates with Newton method,
// Using 2 * fn(j) / j as the approximation of fn'(j)
j = j - (fnj - y) * j / (2 * fnj);
}
return 0;
}
/**
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
*
* @param hue_degrees The desired hue, in degrees.
* @param chroma The desired chroma.
* @param lstar The desired L*.
* @return A hexadecimal representing the sRGB color. The color has sufficiently
* close hue, chroma, and L* to the desired values, if possible; otherwise, the
* hue and L* will be sufficiently close, and chroma will be maximized.
*/
Argb SolveToInt(double hue_degrees, double chroma, double lstar) {
if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
return IntFromLstar(lstar);
}
hue_degrees = SanitizeDegreesDouble(hue_degrees);
double hue_radians = hue_degrees / 180 * kPi;
double y = YFromLstar(lstar);
Argb exact_answer = FindResultByJ(hue_radians, chroma, y);
if (exact_answer != 0) {
return exact_answer;
}
Vec3 linrgb = BisectToLimit(y, hue_radians);
return ArgbFromLinrgb(linrgb);
}
/**
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
*
* @param hue_degrees The desired hue, in degrees.
* @param chroma The desired chroma.
* @param lstar The desired L*.
* @return An CAM16 object representing the sRGB color. The color has
* sufficiently close hue, chroma, and L* to the desired values, if possible;
* otherwise, the hue and L* will be sufficiently close, and chroma will be
* maximized.
*/
Cam SolveToCam(double hue_degrees, double chroma, double lstar) {
return CamFromInt(SolveToInt(hue_degrees, chroma, lstar));
}
} // namespace material_color_utilities
@@ -0,0 +1,28 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_CAM_HCT_SOLVER_H_
#define CPP_CAM_HCT_SOLVER_H_
#include "cpp/cam/cam.h"
namespace material_color_utilities {
Argb SolveToInt(double hue_degrees, double chroma, double lstar);
Cam SolveToCam(double hue_degrees, double chroma, double lstar);
} // namespace material_color_utilities
#endif // CPP_CAM_HCT_SOLVER_H_
@@ -0,0 +1,118 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/cam/viewing_conditions.h"
#include <math.h>
#include <stdio.h>
#include "cpp/utils/utils.h"
namespace material_color_utilities {
static double lerp(double start, double stop, double amount) {
return (1.0 - amount) * start + amount * stop;
}
ViewingConditions CreateViewingConditions(const double white_point[3],
const double adapting_luminance,
const double background_lstar,
const double surround,
const bool discounting_illuminant) {
double background_lstar_corrected =
(background_lstar < 30.0) ? 30.0 : background_lstar;
double rgb_w[3] = {
0.401288 * white_point[0] + 0.650173 * white_point[1] -
0.051461 * white_point[2],
-0.250268 * white_point[0] + 1.204414 * white_point[1] +
0.045854 * white_point[2],
-0.002079 * white_point[0] + 0.048952 * white_point[1] +
0.953127 * white_point[2],
};
double f = 0.8 + (surround / 10.0);
double c = f >= 0.9 ? lerp(0.59, 0.69, (f - 0.9) * 10.0)
: lerp(0.525, 0.59, (f - 0.8) * 10.0);
double d = discounting_illuminant
? 1.0
: f * (1.0 - ((1.0 / 3.6) *
exp((-adapting_luminance - 42.0) / 92.0)));
d = d > 1.0 ? 1.0 : d < 0.0 ? 0.0 : d;
double nc = f;
double rgb_d[3] = {(d * (100.0 / rgb_w[0]) + 1.0 - d),
(d * (100.0 / rgb_w[1]) + 1.0 - d),
(d * (100.0 / rgb_w[2]) + 1.0 - d)};
double k = 1.0 / (5.0 * adapting_luminance + 1.0);
double k4 = k * k * k * k;
double k4f = 1.0 - k4;
double fl = (k4 * adapting_luminance) +
(0.1 * k4f * k4f * pow(5.0 * adapting_luminance, 1.0 / 3.0));
double fl_root = pow(fl, 0.25);
double n = YFromLstar(background_lstar_corrected) / white_point[1];
double z = 1.48 + sqrt(n);
double nbb = 0.725 / pow(n, 0.2);
double ncb = nbb;
double rgb_a_factors[3] = {pow(fl * rgb_d[0] * rgb_w[0] / 100.0, 0.42),
pow(fl * rgb_d[1] * rgb_w[1] / 100.0, 0.42),
pow(fl * rgb_d[2] * rgb_w[2] / 100.0, 0.42)};
double rgb_a[3] = {
400.0 * rgb_a_factors[0] / (rgb_a_factors[0] + 27.13),
400.0 * rgb_a_factors[1] / (rgb_a_factors[1] + 27.13),
400.0 * rgb_a_factors[2] / (rgb_a_factors[2] + 27.13),
};
double aw = (40.0 * rgb_a[0] + 20.0 * rgb_a[1] + rgb_a[2]) / 20.0 * nbb;
ViewingConditions viewingConditions = {
adapting_luminance,
background_lstar_corrected,
surround,
discounting_illuminant,
n,
aw,
nbb,
ncb,
c,
nc,
fl,
fl_root,
z,
{white_point[0], white_point[1], white_point[2]},
{rgb_d[0], rgb_d[1], rgb_d[2]},
};
return viewingConditions;
}
ViewingConditions DefaultWithBackgroundLstar(const double background_lstar) {
return CreateViewingConditions(kWhitePointD65,
(200.0 / kPi * YFromLstar(50.0) / 100.0),
background_lstar, 2.0, 0);
}
void PrintDefaultFrame() {
ViewingConditions frame = CreateViewingConditions(
kWhitePointD65, (200.0 / kPi * YFromLstar(50.0) / 100.0), 50.0, 2.0, 0);
printf(
"(Frame){%0.9lf,\n %0.9lf,\n %0.9lf,\n %s\n, %0.9lf,\n "
"%0.9lf,\n%0.9lf,\n%0.9lf,\n%0.9lf,\n%0.9lf,\n"
"%0.9lf,\n%0.9lf,\n%0.9lf,\n%0.9lf,\n"
"%0.9lf,\n%0.9lf\n};",
frame.adapting_luminance, frame.background_lstar, frame.surround,
frame.discounting_illuminant ? "true" : "false",
frame.background_y_to_white_point_y, frame.aw, frame.nbb, frame.ncb,
frame.c, frame.n_c, frame.fl, frame.fl_root, frame.z, frame.rgb_d[0],
frame.rgb_d[1], frame.rgb_d[2]);
}
} // namespace material_color_utilities
@@ -0,0 +1,68 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_CAM_VIEWING_CONDITIONS_H_
#define CPP_CAM_VIEWING_CONDITIONS_H_
namespace material_color_utilities {
struct ViewingConditions {
double adapting_luminance = 0.0;
double background_lstar = 0.0;
double surround = 0.0;
bool discounting_illuminant = false;
double background_y_to_white_point_y = 0.0;
double aw = 0.0;
double nbb = 0.0;
double ncb = 0.0;
double c = 0.0;
double n_c = 0.0;
double fl = 0.0;
double fl_root = 0.0;
double z = 0.0;
double white_point[3] = {0.0, 0.0, 0.0};
double rgb_d[3] = {0.0, 0.0, 0.0};
};
ViewingConditions CreateViewingConditions(const double white_point[3],
const double adapting_luminance,
const double background_lstar,
const double surround,
const bool discounting_illuminant);
ViewingConditions DefaultWithBackgroundLstar(const double background_lstar);
static const ViewingConditions kDefaultViewingConditions = (ViewingConditions){
11.725676537,
50.000000000,
2.000000000,
false,
0.184186503,
29.981000900,
1.016919255,
1.016919255,
0.689999998,
1.000000000,
0.388481468,
0.789482653,
1.909169555,
{95.047, 100.0, 108.883},
{1.021177769, 0.986307740, 0.933960497},
};
} // namespace material_color_utilities
#endif // CPP_CAM_VIEWING_CONDITIONS_H_
@@ -0,0 +1,138 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/contrast/contrast.h"
#include <algorithm>
#include <cmath>
#include "cpp/utils/utils.h"
namespace material_color_utilities {
// Given a color and a contrast ratio to reach, the luminance of a color that
// reaches that ratio with the color can be calculated. However, that luminance
// may not contrast as desired, i.e. the contrast ratio of the input color
// and the returned luminance may not reach the contrast ratio asked for.
//
// When the desired contrast ratio and the result contrast ratio differ by
// more than this amount, an error value should be returned, or the method
// should be documented as 'unsafe', meaning, it will return a valid luminance
// but that luminance may not meet the requested contrast ratio.
//
// 0.04 selected because it ensures the resulting ratio rounds to the
// same tenth.
constexpr double CONTRAST_RATIO_EPSILON = 0.04;
// Color spaces that measure luminance, such as Y in XYZ, L* in L*a*b*,
// or T in HCT, are known as perceptual accurate color spaces.
//
// To be displayed, they must gamut map to a "display space", one that has
// a defined limit on the number of colors. Display spaces include sRGB,
// more commonly understood as RGB/HSL/HSV/HSB.
//
// Gamut mapping is undefined and not defined by the color space. Any
// gamut mapping algorithm must choose how to sacrifice accuracy in hue,
// saturation, and/or lightness.
//
// A principled solution is to maintain lightness, thus maintaining
// contrast/a11y, maintain hue, thus maintaining aesthetic intent, and reduce
// chroma until the color is in gamut.
//
// HCT chooses this solution, but, that doesn't mean it will _exactly_ matched
// desired lightness, if only because RGB is quantized: RGB is expressed as
// a set of integers: there may be an RGB color with, for example,
// 47.892 lightness, but not 47.891.
//
// To allow for this inherent incompatibility between perceptually accurate
// color spaces and display color spaces, methods that take a contrast ratio
// and luminance, and return a luminance that reaches that contrast ratio for
// the input luminance, purposefully darken/lighten their result such that
// the desired contrast ratio will be reached even if inaccuracy is introduced.
//
// 0.4 is generous, ex. HCT requires much less delta. It was chosen because
// it provides a rough guarantee that as long as a percetual color space
// gamut maps lightness such that the resulting lightness rounds to the same
// as the requested, the desired contrast ratio will be reached.
constexpr double LUMINANCE_GAMUT_MAP_TOLERANCE = 0.4;
double RatioOfYs(double y1, double y2) {
double lighter = y1 > y2 ? y1 : y2;
double darker = (lighter == y2) ? y1 : y2;
return (lighter + 5.0) / (darker + 5.0);
}
double RatioOfTones(double tone_a, double tone_b) {
tone_a = std::clamp(tone_a, 0.0, 100.0);
tone_b = std::clamp(tone_b, 0.0, 100.0);
return RatioOfYs(YFromLstar(tone_a), YFromLstar(tone_b));
}
double Lighter(double tone, double ratio) {
if (tone < 0.0 || tone > 100.0) {
return -1.0;
}
double dark_y = YFromLstar(tone);
double light_y = ratio * (dark_y + 5.0) - 5.0;
double real_contrast = RatioOfYs(light_y, dark_y);
double delta = abs(real_contrast - ratio);
if (real_contrast < ratio && delta > CONTRAST_RATIO_EPSILON) {
return -1;
}
// ensure gamut mapping, which requires a 'range' on tone, will still result
// the correct ratio by darkening slightly.
double value = LstarFromY(light_y) + LUMINANCE_GAMUT_MAP_TOLERANCE;
if (value < 0 || value > 100) {
return -1;
}
return value;
}
double Darker(double tone, double ratio) {
if (tone < 0.0 || tone > 100.0) {
return -1.0;
}
double light_y = YFromLstar(tone);
double dark_y = ((light_y + 5.0) / ratio) - 5.0;
double real_contrast = RatioOfYs(light_y, dark_y);
double delta = abs(real_contrast - ratio);
if (real_contrast < ratio && delta > CONTRAST_RATIO_EPSILON) {
return -1;
}
// ensure gamut mapping, which requires a 'range' on tone, will still result
// the correct ratio by darkening slightly.
double value = LstarFromY(dark_y) - LUMINANCE_GAMUT_MAP_TOLERANCE;
if (value < 0 || value > 100) {
return -1;
}
return value;
}
double LighterUnsafe(double tone, double ratio) {
double lighter_safe = Lighter(tone, ratio);
return (lighter_safe < 0.0) ? 100.0 : lighter_safe;
}
double DarkerUnsafe(double tone, double ratio) {
double darker_safe = Darker(tone, ratio);
return (darker_safe < 0.0) ? 0.0 : darker_safe;
}
} // namespace material_color_utilities
@@ -0,0 +1,97 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_CONTRAST_CONTRAST_H_
#define CPP_CONTRAST_CONTRAST_H_
/**
* Utility methods for calculating contrast given two colors, or calculating a
* color given one color and a contrast ratio.
*
* Contrast ratio is calculated using XYZ's Y. When linearized to match human
* perception, Y becomes HCT's tone and L*a*b*'s' L*. Informally, this is the
* lightness of a color.
*
* Methods refer to tone, T in the the HCT color space.
* Tone is equivalent to L* in the L*a*b* color space, or L in the LCH color
* space.
*/
namespace material_color_utilities {
/**
* @return a contrast ratio, which ranges from 1 to 21.
* @param tone_a Tone between 0 and 100. Values outside will be clamped.
* @param tone_b Tone between 0 and 100. Values outside will be clamped.
*/
double RatioOfTones(double tone_a, double tone_b);
/**
* @return a tone >= [tone] that ensures [ratio].
* Return value is between 0 and 100.
* Returns -1 if [ratio] cannot be achieved with [tone].
*
* @param tone Tone return value must contrast with.
* Range is 0 to 100. Invalid values will result in -1 being returned.
* @param ratio Contrast ratio of return value and [tone].
* Range is 1 to 21, invalid values have undefined behavior.
*/
double Lighter(double tone, double ratio);
/**
* @return a tone <= [tone] that ensures [ratio].
* Return value is between 0 and 100.
* Returns -1 if [ratio] cannot be achieved with [tone].
*
* @param tone Tone return value must contrast with.
* Range is 0 to 100. Invalid values will result in -1 being returned.
* @param ratio Contrast ratio of return value and [tone].
* Range is 1 to 21, invalid values have undefined behavior.
*/
double Darker(double tone, double ratio);
/**
* @return a tone >= [tone] that ensures [ratio].
* Return value is between 0 and 100.
* Returns 100 if [ratio] cannot be achieved with [tone].
*
* This method is unsafe because the returned value is guaranteed to be in
* bounds for tone, i.e. between 0 and 100. However, that value may not reach
* the [ratio] with [tone]. For example, there is no color lighter than T100.
*
* @param tone Tone return value must contrast with.
* Range is 0 to 100. Invalid values will result in 100 being returned.
* @param ratio Desired contrast ratio of return value and tone parameter.
* Range is 1 to 21, invalid values have undefined behavior.
*/
double LighterUnsafe(double tone, double ratio);
/**
* @return a tone <= [tone] that ensures [ratio].
* Return value is between 0 and 100.
* Returns 0 if [ratio] cannot be achieved with [tone].
*
* This method is unsafe because the returned value is guaranteed to be in
* bounds for tone, i.e. between 0 and 100. However, that value may not reach
* the [ratio] with [tone]. For example, there is no color darker than T0.
*
* @param tone Tone return value must contrast with.
* Range is 0 to 100. Invalid values will result in 0 being returned.
* @param ratio Desired contrast ratio of return value and tone parameter.
* Range is 1 to 21, invalid values have undefined behavior.
*/
double DarkerUnsafe(double tone, double ratio);
} // namespace material_color_utilities
#endif // CPP_CONTRAST_CONTRAST_H_
@@ -0,0 +1,42 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/dislike/dislike.h"
#include <cmath>
#include "cpp/cam/hct.h"
namespace material_color_utilities {
bool IsDisliked(Hct hct) {
double roundedHue = std::round(hct.get_hue());
bool hue_passes = roundedHue >= 90.0 && roundedHue <= 111.0;
bool chroma_passes = std::round(hct.get_chroma()) > 16.0;
bool tone_passes = std::round(hct.get_tone()) < 65.0;
return hue_passes && chroma_passes && tone_passes;
}
Hct FixIfDisliked(Hct hct) {
if (IsDisliked(hct)) {
return Hct(hct.get_hue(), hct.get_chroma(), 70.0);
}
return hct;
}
} // namespace material_color_utilities
@@ -0,0 +1,55 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_DISLIKE_DISLIKE_H_
#define CPP_DISLIKE_DISLIKE_H_
#include "cpp/cam/hct.h"
namespace material_color_utilities {
/**
* Checks and/or fixes universally disliked colors.
*
* Color science studies of color preference indicate universal distaste for
* dark yellow-greens, and also show this is correlated to distate for
* biological waste and rotting food.
*
* See Palmer and Schloss, 2010 or Schloss and Palmer's Chapter 21 in Handbook
* of Color Psychology (2015).
*/
/**
* @return whether the color is disliked.
*
* Disliked is defined as a dark yellow-green that is not neutral.
* @param hct The color to be tested.
*/
bool IsDisliked(Hct hct);
/**
* If a color is disliked, lightens it to make it likable.
*
* The original color is not modified.
*
* @param hct The color to be tested (and fixed, if needed).
* @return The original color if it is not disliked; otherwise, the fixed
* color.
*/
Hct FixIfDisliked(Hct hct);
} // namespace material_color_utilities
#endif // CPP_DISLIKE_DISLIKE_H_
@@ -0,0 +1,72 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_DYNAMICCOLOR_CONTRAST_CURVE_H_
#define CPP_DYNAMICCOLOR_CONTRAST_CURVE_H_
#include "cpp/utils/utils.h"
namespace material_color_utilities {
/**
* A class containing a value that changes with the contrast level.
*
* Usually represents the contrast requirements for a dynamic color on its
* background. The four values correspond to values for contrast levels -1.0,
* 0.0, 0.5, and 1.0, respectively.
*/
struct ContrastCurve {
double low;
double normal;
double medium;
double high;
/**
* Creates a `ContrastCurve` object.
*
* @param low Value for contrast level -1.0
* @param normal Value for contrast level 0.0
* @param medium Value for contrast level 0.5
* @param high Value for contrast level 1.0
*/
ContrastCurve(double low, double normal, double medium, double high)
: low(low), normal(normal), medium(medium), high(high) {}
/**
* Returns the value at a given contrast level.
*
* @param contrastLevel The contrast level. 0.0 is the default (normal); -1.0
* is the lowest; 1.0 is the highest.
* @return The value. For contrast ratios, a number between 1.0 and 21.0.
*/
double get(double contrastLevel) {
if (contrastLevel <= -1.0) {
return low;
} else if (contrastLevel < 0.0) {
return Lerp(low, normal, (contrastLevel - (-1)) / 1);
} else if (contrastLevel < 0.5) {
return Lerp(normal, medium, (contrastLevel - 0) / 0.5);
} else if (contrastLevel < 1.0) {
return Lerp(medium, high, (contrastLevel - 0.5) / 0.5);
} else {
return high;
}
}
};
} // namespace material_color_utilities
#endif // CPP_DYNAMICCOLOR_CONTRAST_CURVE_H_
@@ -0,0 +1,306 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/dynamiccolor/dynamic_color.h"
#include <algorithm>
#include <cmath>
#include <functional>
#include <optional>
#include <string>
#include <vector>
#include "cpp/cam/hct.h"
#include "cpp/contrast/contrast.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/tone_delta_pair.h"
#include "cpp/palettes/tones.h"
namespace material_color_utilities {
using std::function;
using std::nullopt;
using std::optional;
using DoubleFunction = function<double(const DynamicScheme&)>;
template <class T, class U>
optional<U> SafeCall(optional<function<optional<U>(const T&)>> f, const T& x) {
if (f == nullopt) {
return nullopt;
} else {
return f.value()(x);
}
}
template <class T, class U>
optional<U> SafeCallCleanResult(optional<function<U(T)>> f, T x) {
if (f == nullopt) {
return nullopt;
} else {
return f.value()(x);
}
}
double ForegroundTone(double bg_tone, double ratio) {
double lighter_tone = LighterUnsafe(/*tone*/ bg_tone, /*ratio*/ ratio);
double darker_tone = DarkerUnsafe(/*tone*/ bg_tone, /*ratio*/ ratio);
double lighter_ratio = RatioOfTones(lighter_tone, bg_tone);
double darker_ratio = RatioOfTones(darker_tone, bg_tone);
double prefer_lighter = TonePrefersLightForeground(bg_tone);
if (prefer_lighter) {
double negligible_difference =
(abs(lighter_ratio - darker_ratio) < 0.1 && lighter_ratio < ratio &&
darker_ratio < ratio);
return lighter_ratio >= ratio || lighter_ratio >= darker_ratio ||
negligible_difference
? lighter_tone
: darker_tone;
} else {
return darker_ratio >= ratio || darker_ratio >= lighter_ratio
? darker_tone
: lighter_tone;
}
}
double EnableLightForeground(double tone) {
if (TonePrefersLightForeground(tone) && !ToneAllowsLightForeground(tone)) {
return 49.0;
}
return tone;
}
bool TonePrefersLightForeground(double tone) { return round(tone) < 60; }
bool ToneAllowsLightForeground(double tone) { return round(tone) <= 49; }
/**
* Default constructor.
*/
DynamicColor::DynamicColor(
std::string name, std::function<TonalPalette(const DynamicScheme&)> palette,
std::function<double(const DynamicScheme&)> tone, bool is_background,
std::optional<std::function<DynamicColor(const DynamicScheme&)>> background,
std::optional<std::function<DynamicColor(const DynamicScheme&)>>
second_background,
std::optional<ContrastCurve> contrast_curve,
std::optional<std::function<ToneDeltaPair(const DynamicScheme&)>>
tone_delta_pair)
: name_(name),
palette_(palette),
tone_(tone),
is_background_(is_background),
background_(background),
second_background_(second_background),
contrast_curve_(contrast_curve),
tone_delta_pair_(tone_delta_pair) {}
DynamicColor DynamicColor::FromPalette(
std::string name, std::function<TonalPalette(const DynamicScheme&)> palette,
std::function<double(const DynamicScheme&)> tone) {
return DynamicColor(name, palette, tone,
/*is_background=*/false,
/*background=*/nullopt,
/*second_background=*/nullopt,
/*contrast_curve=*/nullopt,
/*tone_delta_pair=*/nullopt);
}
Argb DynamicColor::GetArgb(const DynamicScheme& scheme) {
return palette_(scheme).get(GetTone(scheme));
}
Hct DynamicColor::GetHct(const DynamicScheme& scheme) {
return Hct(GetArgb(scheme));
}
double DynamicColor::GetTone(const DynamicScheme& scheme) {
bool decreasingContrast = scheme.contrast_level < 0;
// Case 1: dual foreground, pair of colors with delta constraint.
if (tone_delta_pair_ != std::nullopt) {
ToneDeltaPair tone_delta_pair = tone_delta_pair_.value()(scheme);
DynamicColor role_a = tone_delta_pair.role_a_;
DynamicColor role_b = tone_delta_pair.role_b_;
double delta = tone_delta_pair.delta_;
TonePolarity polarity = tone_delta_pair.polarity_;
bool stay_together = tone_delta_pair.stay_together_;
DynamicColor bg = background_.value()(scheme);
double bg_tone = bg.GetTone(scheme);
bool a_is_nearer =
(polarity == TonePolarity::kNearer ||
(polarity == TonePolarity::kLighter && !scheme.is_dark) ||
(polarity == TonePolarity::kDarker && scheme.is_dark));
DynamicColor nearer = a_is_nearer ? role_a : role_b;
DynamicColor farther = a_is_nearer ? role_b : role_a;
bool am_nearer = this->name_ == nearer.name_;
double expansion_dir = scheme.is_dark ? 1 : -1;
// 1st round: solve to min, each
double n_contrast =
nearer.contrast_curve_.value().get(scheme.contrast_level);
double f_contrast =
farther.contrast_curve_.value().get(scheme.contrast_level);
// If a color is good enough, it is not adjusted.
// Initial and adjusted tones for `nearer`
double n_initial_tone = nearer.tone_(scheme);
double n_tone = RatioOfTones(bg_tone, n_initial_tone) >= n_contrast
? n_initial_tone
: ForegroundTone(bg_tone, n_contrast);
// Initial and adjusted tones for `farther`
double f_initial_tone = farther.tone_(scheme);
double f_tone = RatioOfTones(bg_tone, f_initial_tone) >= f_contrast
? f_initial_tone
: ForegroundTone(bg_tone, f_contrast);
if (decreasingContrast) {
// If decreasing contrast, adjust color to the "bare minimum"
// that satisfies contrast.
n_tone = ForegroundTone(bg_tone, n_contrast);
f_tone = ForegroundTone(bg_tone, f_contrast);
}
if ((f_tone - n_tone) * expansion_dir >= delta) {
// Good! Tones satisfy the constraint; no change needed.
} else {
// 2nd round: expand farther to match delta.
f_tone = std::clamp(n_tone + delta * expansion_dir, 0.0, 100.0);
if ((f_tone - n_tone) * expansion_dir >= delta) {
// Good! Tones now satisfy the constraint; no change needed.
} else {
// 3rd round: contract nearer to match delta.
n_tone = std::clamp(f_tone - delta * expansion_dir, 0.0, 100.0);
}
}
// Avoids the 50-59 awkward zone.
if (50 <= n_tone && n_tone < 60) {
// If `nearer` is in the awkward zone, move it away, together with
// `farther`.
if (expansion_dir > 0) {
n_tone = 60;
f_tone = std::max(f_tone, n_tone + delta * expansion_dir);
} else {
n_tone = 49;
f_tone = std::min(f_tone, n_tone + delta * expansion_dir);
}
} else if (50 <= f_tone && f_tone < 60) {
if (stay_together) {
// Fixes both, to avoid two colors on opposite sides of the "awkward
// zone".
if (expansion_dir > 0) {
n_tone = 60;
f_tone = std::max(f_tone, n_tone + delta * expansion_dir);
} else {
n_tone = 49;
f_tone = std::min(f_tone, n_tone + delta * expansion_dir);
}
} else {
// Not required to stay together; fixes just one.
if (expansion_dir > 0) {
f_tone = 60;
} else {
f_tone = 49;
}
}
}
// Returns `n_tone` if this color is `nearer`, otherwise `f_tone`.
return am_nearer ? n_tone : f_tone;
} else {
// Case 2: No contrast pair; just solve for itself.
double answer = tone_(scheme);
if (background_ == std::nullopt) {
return answer; // No adjustment for colors with no background.
}
double bg_tone = background_.value()(scheme).GetTone(scheme);
double desired_ratio = contrast_curve_.value().get(scheme.contrast_level);
if (RatioOfTones(bg_tone, answer) >= desired_ratio) {
// Don't "improve" what's good enough.
} else {
// Rough improvement.
answer = ForegroundTone(bg_tone, desired_ratio);
}
if (decreasingContrast) {
answer = ForegroundTone(bg_tone, desired_ratio);
}
if (is_background_ && 50 <= answer && answer < 60) {
// Must adjust
if (RatioOfTones(49, bg_tone) >= desired_ratio) {
answer = 49;
} else {
answer = 60;
}
}
if (second_background_ != std::nullopt) {
// Case 3: Adjust for dual backgrounds.
double bg_tone_1 = background_.value()(scheme).GetTone(scheme);
double bg_tone_2 = second_background_.value()(scheme).GetTone(scheme);
double upper = std::max(bg_tone_1, bg_tone_2);
double lower = std::min(bg_tone_1, bg_tone_2);
if (RatioOfTones(upper, answer) >= desired_ratio &&
RatioOfTones(lower, answer) >= desired_ratio) {
return answer;
}
// The darkest light tone that satisfies the desired ratio,
// or -1 if such ratio cannot be reached.
double lightOption = Lighter(upper, desired_ratio);
// The lightest dark tone that satisfies the desired ratio,
// or -1 if such ratio cannot be reached.
double darkOption = Darker(lower, desired_ratio);
// Tones suitable for the foreground.
std::vector<double> availables;
if (lightOption != -1) {
availables.push_back(lightOption);
}
if (darkOption != -1) {
availables.push_back(darkOption);
}
bool prefersLight = TonePrefersLightForeground(bg_tone_1) ||
TonePrefersLightForeground(bg_tone_2);
if (prefersLight) {
return (lightOption < 0) ? 100 : lightOption;
}
if (availables.size() == 1) {
return availables[0];
}
return (darkOption < 0) ? 0 : darkOption;
}
return answer;
}
}
} // namespace material_color_utilities
@@ -0,0 +1,131 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_DYNAMICCOLOR_DYNAMIC_COLOR_H_
#define CPP_DYNAMICCOLOR_DYNAMIC_COLOR_H_
#include <functional>
#include <optional>
#include <string>
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/contrast_curve.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
struct ToneDeltaPair;
/**
* Given a background tone, find a foreground tone, while ensuring they reach
* a contrast ratio that is as close to [ratio] as possible.
*
* [bgTone] Tone in HCT. Range is 0 to 100, undefined behavior when it falls
* outside that range.
* [ratio] The contrast ratio desired between [bgTone] and the return value.
*/
double ForegroundTone(double bg_tone, double ratio);
/**
* Adjust a tone such that white has 4.5 contrast, if the tone is
* reasonably close to supporting it.
*/
double EnableLightForeground(double tone);
/**
* Returns whether [tone] prefers a light foreground.
*
* People prefer white foregrounds on ~T60-70. Observed over time, and also
* by Andrew Somers during research for APCA.
*
* T60 used as to create the smallest discontinuity possible when skipping
* down to T49 in order to ensure light foregrounds.
*
* Since `tertiaryContainer` in dark monochrome scheme requires a tone of
* 60, it should not be adjusted. Therefore, 60 is excluded here.
*/
bool TonePrefersLightForeground(double tone);
/**
* Returns whether [tone] can reach a contrast ratio of 4.5 with a lighter
* color.
*/
bool ToneAllowsLightForeground(double tone);
/**
* @param name_ The name of the dynamic color.
* @param palette_ Function that provides a TonalPalette given
* DynamicScheme. A TonalPalette is defined by a hue and chroma, so this
* replaces the need to specify hue/chroma. By providing a tonal palette, when
* contrast adjustments are made, intended chroma can be preserved.
* @param tone_ Function that provides a tone given DynamicScheme.
* @param is_background_ Whether this dynamic color is a background, with
* some other color as the foreground.
* @param background_ The background of the dynamic color (as a function of a
* `DynamicScheme`), if it exists.
* @param second_background_ A second background of the dynamic color (as a
* function of a `DynamicScheme`), if it
* exists.
* @param contrast_curve_ A `ContrastCurve` object specifying how its contrast
* against its background should behave in various contrast levels options.
* @param tone_delta_pair_ A `ToneDeltaPair` object specifying a tone delta
* constraint between two colors. One of them must be the color being
* constructed.
*/
struct DynamicColor {
std::string name_;
std::function<TonalPalette(const DynamicScheme&)> palette_;
std::function<double(const DynamicScheme&)> tone_;
bool is_background_;
std::optional<std::function<DynamicColor(const DynamicScheme&)>> background_;
std::optional<std::function<DynamicColor(const DynamicScheme&)>>
second_background_;
std::optional<ContrastCurve> contrast_curve_;
std::optional<std::function<ToneDeltaPair(const DynamicScheme&)>>
tone_delta_pair_;
/** A convenience constructor, only requiring name, palette, and tone. */
static DynamicColor FromPalette(
std::string name,
std::function<TonalPalette(const DynamicScheme&)> palette,
std::function<double(const DynamicScheme&)> tone);
Argb GetArgb(const DynamicScheme& scheme);
Hct GetHct(const DynamicScheme& scheme);
double GetTone(const DynamicScheme& scheme);
/** The default constructor. */
DynamicColor(std::string name,
std::function<TonalPalette(const DynamicScheme&)> palette,
std::function<double(const DynamicScheme&)> tone,
bool is_background,
std::optional<std::function<DynamicColor(const DynamicScheme&)>>
background,
std::optional<std::function<DynamicColor(const DynamicScheme&)>>
second_background,
std::optional<ContrastCurve> contrast_curve,
std::optional<std::function<ToneDeltaPair(const DynamicScheme&)>>
tone_delta_pair);
};
} // namespace material_color_utilities
#endif // CPP_DYNAMICCOLOR_DYNAMIC_COLOR_H_
@@ -0,0 +1,286 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include <optional>
#include <vector>
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/material_dynamic_colors.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
DynamicScheme::DynamicScheme(Hct source_color_hct, Variant variant,
double contrast_level, bool is_dark,
TonalPalette primary_palette,
TonalPalette secondary_palette,
TonalPalette tertiary_palette,
TonalPalette neutral_palette,
TonalPalette neutral_variant_palette,
std::optional<TonalPalette> error_palette)
: source_color_hct(source_color_hct),
variant(variant),
is_dark(is_dark),
contrast_level(contrast_level),
primary_palette(primary_palette),
secondary_palette(secondary_palette),
tertiary_palette(tertiary_palette),
neutral_palette(neutral_palette),
neutral_variant_palette(neutral_variant_palette),
error_palette(error_palette.value_or(TonalPalette(25.0, 84.0))) {}
double DynamicScheme::GetRotatedHue(Hct source_color, std::vector<double> hues,
std::vector<double> rotations) {
double source_hue = source_color.get_hue();
if (rotations.size() == 1) {
return SanitizeDegreesDouble(source_color.get_hue() + rotations[0]);
}
int size = hues.size();
for (int i = 0; i <= (size - 2); ++i) {
double this_hue = hues[i];
double next_hue = hues[i + 1];
if (this_hue < source_hue && source_hue < next_hue) {
return SanitizeDegreesDouble(source_hue + rotations[i]);
}
}
return source_hue;
}
Argb DynamicScheme::SourceColorArgb() const { return source_color_hct.ToInt(); }
Argb DynamicScheme::GetPrimaryPaletteKeyColor() const {
return MaterialDynamicColors::PrimaryPaletteKeyColor().GetArgb(*this);
}
Argb DynamicScheme::GetSecondaryPaletteKeyColor() const {
return MaterialDynamicColors::SecondaryPaletteKeyColor().GetArgb(*this);
}
Argb DynamicScheme::GetTertiaryPaletteKeyColor() const {
return MaterialDynamicColors::TertiaryPaletteKeyColor().GetArgb(*this);
}
Argb DynamicScheme::GetNeutralPaletteKeyColor() const {
return MaterialDynamicColors::NeutralPaletteKeyColor().GetArgb(*this);
}
Argb DynamicScheme::GetNeutralVariantPaletteKeyColor() const {
return MaterialDynamicColors::NeutralVariantPaletteKeyColor().GetArgb(*this);
}
Argb DynamicScheme::GetBackground() const {
return MaterialDynamicColors::Background().GetArgb(*this);
}
Argb DynamicScheme::GetOnBackground() const {
return MaterialDynamicColors::OnBackground().GetArgb(*this);
}
Argb DynamicScheme::GetSurface() const {
return MaterialDynamicColors::Surface().GetArgb(*this);
}
Argb DynamicScheme::GetSurfaceDim() const {
return MaterialDynamicColors::SurfaceDim().GetArgb(*this);
}
Argb DynamicScheme::GetSurfaceBright() const {
return MaterialDynamicColors::SurfaceBright().GetArgb(*this);
}
Argb DynamicScheme::GetSurfaceContainerLowest() const {
return MaterialDynamicColors::SurfaceContainerLowest().GetArgb(*this);
}
Argb DynamicScheme::GetSurfaceContainerLow() const {
return MaterialDynamicColors::SurfaceContainerLow().GetArgb(*this);
}
Argb DynamicScheme::GetSurfaceContainer() const {
return MaterialDynamicColors::SurfaceContainer().GetArgb(*this);
}
Argb DynamicScheme::GetSurfaceContainerHigh() const {
return MaterialDynamicColors::SurfaceContainerHigh().GetArgb(*this);
}
Argb DynamicScheme::GetSurfaceContainerHighest() const {
return MaterialDynamicColors::SurfaceContainerHighest().GetArgb(*this);
}
Argb DynamicScheme::GetOnSurface() const {
return MaterialDynamicColors::OnSurface().GetArgb(*this);
}
Argb DynamicScheme::GetSurfaceVariant() const {
return MaterialDynamicColors::SurfaceVariant().GetArgb(*this);
}
Argb DynamicScheme::GetOnSurfaceVariant() const {
return MaterialDynamicColors::OnSurfaceVariant().GetArgb(*this);
}
Argb DynamicScheme::GetInverseSurface() const {
return MaterialDynamicColors::InverseSurface().GetArgb(*this);
}
Argb DynamicScheme::GetInverseOnSurface() const {
return MaterialDynamicColors::InverseOnSurface().GetArgb(*this);
}
Argb DynamicScheme::GetOutline() const {
return MaterialDynamicColors::Outline().GetArgb(*this);
}
Argb DynamicScheme::GetOutlineVariant() const {
return MaterialDynamicColors::OutlineVariant().GetArgb(*this);
}
Argb DynamicScheme::GetShadow() const {
return MaterialDynamicColors::Shadow().GetArgb(*this);
}
Argb DynamicScheme::GetScrim() const {
return MaterialDynamicColors::Scrim().GetArgb(*this);
}
Argb DynamicScheme::GetSurfaceTint() const {
return MaterialDynamicColors::SurfaceTint().GetArgb(*this);
}
Argb DynamicScheme::GetPrimary() const {
return MaterialDynamicColors::Primary().GetArgb(*this);
}
Argb DynamicScheme::GetOnPrimary() const {
return MaterialDynamicColors::OnPrimary().GetArgb(*this);
}
Argb DynamicScheme::GetPrimaryContainer() const {
return MaterialDynamicColors::PrimaryContainer().GetArgb(*this);
}
Argb DynamicScheme::GetOnPrimaryContainer() const {
return MaterialDynamicColors::OnPrimaryContainer().GetArgb(*this);
}
Argb DynamicScheme::GetInversePrimary() const {
return MaterialDynamicColors::InversePrimary().GetArgb(*this);
}
Argb DynamicScheme::GetSecondary() const {
return MaterialDynamicColors::Secondary().GetArgb(*this);
}
Argb DynamicScheme::GetOnSecondary() const {
return MaterialDynamicColors::OnSecondary().GetArgb(*this);
}
Argb DynamicScheme::GetSecondaryContainer() const {
return MaterialDynamicColors::SecondaryContainer().GetArgb(*this);
}
Argb DynamicScheme::GetOnSecondaryContainer() const {
return MaterialDynamicColors::OnSecondaryContainer().GetArgb(*this);
}
Argb DynamicScheme::GetTertiary() const {
return MaterialDynamicColors::Tertiary().GetArgb(*this);
}
Argb DynamicScheme::GetOnTertiary() const {
return MaterialDynamicColors::OnTertiary().GetArgb(*this);
}
Argb DynamicScheme::GetTertiaryContainer() const {
return MaterialDynamicColors::TertiaryContainer().GetArgb(*this);
}
Argb DynamicScheme::GetOnTertiaryContainer() const {
return MaterialDynamicColors::OnTertiaryContainer().GetArgb(*this);
}
Argb DynamicScheme::GetError() const {
return MaterialDynamicColors::Error().GetArgb(*this);
}
Argb DynamicScheme::GetOnError() const {
return MaterialDynamicColors::OnError().GetArgb(*this);
}
Argb DynamicScheme::GetErrorContainer() const {
return MaterialDynamicColors::ErrorContainer().GetArgb(*this);
}
Argb DynamicScheme::GetOnErrorContainer() const {
return MaterialDynamicColors::OnErrorContainer().GetArgb(*this);
}
Argb DynamicScheme::GetPrimaryFixed() const {
return MaterialDynamicColors::PrimaryFixed().GetArgb(*this);
}
Argb DynamicScheme::GetPrimaryFixedDim() const {
return MaterialDynamicColors::PrimaryFixedDim().GetArgb(*this);
}
Argb DynamicScheme::GetOnPrimaryFixed() const {
return MaterialDynamicColors::OnPrimaryFixed().GetArgb(*this);
}
Argb DynamicScheme::GetOnPrimaryFixedVariant() const {
return MaterialDynamicColors::OnPrimaryFixedVariant().GetArgb(*this);
}
Argb DynamicScheme::GetSecondaryFixed() const {
return MaterialDynamicColors::SecondaryFixed().GetArgb(*this);
}
Argb DynamicScheme::GetSecondaryFixedDim() const {
return MaterialDynamicColors::SecondaryFixedDim().GetArgb(*this);
}
Argb DynamicScheme::GetOnSecondaryFixed() const {
return MaterialDynamicColors::OnSecondaryFixed().GetArgb(*this);
}
Argb DynamicScheme::GetOnSecondaryFixedVariant() const {
return MaterialDynamicColors::OnSecondaryFixedVariant().GetArgb(*this);
}
Argb DynamicScheme::GetTertiaryFixed() const {
return MaterialDynamicColors::TertiaryFixed().GetArgb(*this);
}
Argb DynamicScheme::GetTertiaryFixedDim() const {
return MaterialDynamicColors::TertiaryFixedDim().GetArgb(*this);
}
Argb DynamicScheme::GetOnTertiaryFixed() const {
return MaterialDynamicColors::OnTertiaryFixed().GetArgb(*this);
}
Argb DynamicScheme::GetOnTertiaryFixedVariant() const {
return MaterialDynamicColors::OnTertiaryFixedVariant().GetArgb(*this);
}
} // namespace material_color_utilities
@@ -0,0 +1,113 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_DYNAMICCOLOR_DYNAMIC_SCHEME_H_
#define CPP_DYNAMICCOLOR_DYNAMIC_SCHEME_H_
#include <optional>
#include <vector>
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
struct DynamicScheme {
Hct source_color_hct;
Variant variant;
bool is_dark;
double contrast_level;
TonalPalette primary_palette;
TonalPalette secondary_palette;
TonalPalette tertiary_palette;
TonalPalette neutral_palette;
TonalPalette neutral_variant_palette;
TonalPalette error_palette;
DynamicScheme(Hct source_color_hct, Variant variant, double contrast_level,
bool is_dark, TonalPalette primary_palette,
TonalPalette secondary_palette, TonalPalette tertiary_palette,
TonalPalette neutral_palette,
TonalPalette neutral_variant_palette,
std::optional<TonalPalette> error_palette = std::nullopt);
static double GetRotatedHue(Hct source_color, std::vector<double> hues,
std::vector<double> rotations);
Argb SourceColorArgb() const;
Argb GetPrimaryPaletteKeyColor() const;
Argb GetSecondaryPaletteKeyColor() const;
Argb GetTertiaryPaletteKeyColor() const;
Argb GetNeutralPaletteKeyColor() const;
Argb GetNeutralVariantPaletteKeyColor() const;
Argb GetBackground() const;
Argb GetOnBackground() const;
Argb GetSurface() const;
Argb GetSurfaceDim() const;
Argb GetSurfaceBright() const;
Argb GetSurfaceContainerLowest() const;
Argb GetSurfaceContainerLow() const;
Argb GetSurfaceContainer() const;
Argb GetSurfaceContainerHigh() const;
Argb GetSurfaceContainerHighest() const;
Argb GetOnSurface() const;
Argb GetSurfaceVariant() const;
Argb GetOnSurfaceVariant() const;
Argb GetInverseSurface() const;
Argb GetInverseOnSurface() const;
Argb GetOutline() const;
Argb GetOutlineVariant() const;
Argb GetShadow() const;
Argb GetScrim() const;
Argb GetSurfaceTint() const;
Argb GetPrimary() const;
Argb GetOnPrimary() const;
Argb GetPrimaryContainer() const;
Argb GetOnPrimaryContainer() const;
Argb GetInversePrimary() const;
Argb GetSecondary() const;
Argb GetOnSecondary() const;
Argb GetSecondaryContainer() const;
Argb GetOnSecondaryContainer() const;
Argb GetTertiary() const;
Argb GetOnTertiary() const;
Argb GetTertiaryContainer() const;
Argb GetOnTertiaryContainer() const;
Argb GetError() const;
Argb GetOnError() const;
Argb GetErrorContainer() const;
Argb GetOnErrorContainer() const;
Argb GetPrimaryFixed() const;
Argb GetPrimaryFixedDim() const;
Argb GetOnPrimaryFixed() const;
Argb GetOnPrimaryFixedVariant() const;
Argb GetSecondaryFixed() const;
Argb GetSecondaryFixedDim() const;
Argb GetOnSecondaryFixed() const;
Argb GetOnSecondaryFixedVariant() const;
Argb GetTertiaryFixed() const;
Argb GetTertiaryFixedDim() const;
Argb GetOnTertiaryFixed() const;
Argb GetOnTertiaryFixedVariant() const;
};
} // namespace material_color_utilities
#endif // CPP_DYNAMICCOLOR_DYNAMIC_SCHEME_H_
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_DYNAMICCOLOR_MATERIAL_DYNAMIC_COLORS_H_
#define CPP_DYNAMICCOLOR_MATERIAL_DYNAMIC_COLORS_H_
#include "cpp/dynamiccolor/dynamic_color.h"
namespace material_color_utilities {
class MaterialDynamicColors {
public:
static DynamicColor PrimaryPaletteKeyColor();
static DynamicColor SecondaryPaletteKeyColor();
static DynamicColor TertiaryPaletteKeyColor();
static DynamicColor NeutralPaletteKeyColor();
static DynamicColor NeutralVariantPaletteKeyColor();
static DynamicColor Background();
static DynamicColor OnBackground();
static DynamicColor Surface();
static DynamicColor SurfaceDim();
static DynamicColor SurfaceBright();
static DynamicColor SurfaceContainerLowest();
static DynamicColor SurfaceContainerLow();
static DynamicColor SurfaceContainer();
static DynamicColor SurfaceContainerHigh();
static DynamicColor SurfaceContainerHighest();
static DynamicColor OnSurface();
static DynamicColor SurfaceVariant();
static DynamicColor OnSurfaceVariant();
static DynamicColor InverseSurface();
static DynamicColor InverseOnSurface();
static DynamicColor Outline();
static DynamicColor OutlineVariant();
static DynamicColor Shadow();
static DynamicColor Scrim();
static DynamicColor SurfaceTint();
static DynamicColor Primary();
static DynamicColor OnPrimary();
static DynamicColor PrimaryContainer();
static DynamicColor OnPrimaryContainer();
static DynamicColor InversePrimary();
static DynamicColor Secondary();
static DynamicColor OnSecondary();
static DynamicColor SecondaryContainer();
static DynamicColor OnSecondaryContainer();
static DynamicColor Tertiary();
static DynamicColor OnTertiary();
static DynamicColor TertiaryContainer();
static DynamicColor OnTertiaryContainer();
static DynamicColor Error();
static DynamicColor OnError();
static DynamicColor ErrorContainer();
static DynamicColor OnErrorContainer();
static DynamicColor PrimaryFixed();
static DynamicColor PrimaryFixedDim();
static DynamicColor OnPrimaryFixed();
static DynamicColor OnPrimaryFixedVariant();
static DynamicColor SecondaryFixed();
static DynamicColor SecondaryFixedDim();
static DynamicColor OnSecondaryFixed();
static DynamicColor OnSecondaryFixedVariant();
static DynamicColor TertiaryFixed();
static DynamicColor TertiaryFixedDim();
static DynamicColor OnTertiaryFixed();
static DynamicColor OnTertiaryFixedVariant();
};
} // namespace material_color_utilities
#endif // CPP_DYNAMICCOLOR_MATERIAL_DYNAMIC_COLORS_H_
@@ -0,0 +1,78 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_DYNAMICCOLOR_TONE_DELTA_PAIR_H_
#define CPP_DYNAMICCOLOR_TONE_DELTA_PAIR_H_
#include "cpp/dynamiccolor/dynamic_color.h"
namespace material_color_utilities {
/**
* Describes the different in tone between colors.
*/
enum class TonePolarity { kDarker, kLighter, kNearer, kFarther };
/**
* Documents a constraint between two DynamicColors, in which their tones must
* have a certain distance from each other.
*
* Prefer a DynamicColor with a background, this is for special cases when
* designers want tonal distance, literally contrast, between two colors that
* don't have a background / foreground relationship or a contrast guarantee.
*/
struct ToneDeltaPair {
DynamicColor role_a_;
DynamicColor role_b_;
double delta_;
TonePolarity polarity_;
bool stay_together_;
/**
* Documents a constraint in tone distance between two DynamicColors.
*
* The polarity is an adjective that describes "A", compared to "B".
*
* For instance, ToneDeltaPair(A, B, 15, 'darker', stayTogether) states that
* A's tone should be at least 15 darker than B's.
*
* 'nearer' and 'farther' describes closeness to the surface roles. For
* instance, ToneDeltaPair(A, B, 10, 'nearer', stayTogether) states that A
* should be 10 lighter than B in light mode, and 10 darker than B in dark
* mode.
*
* @param roleA The first role in a pair.
* @param roleB The second role in a pair.
* @param delta Required difference between tones. Absolute value, negative
* values have undefined behavior.
* @param polarity The relative relation between tones of roleA and roleB,
* as described above.
* @param stayTogether Whether these two roles should stay on the same side of
* the "awkward zone" (T50-59). This is necessary for certain cases where
* one role has two backgrounds.
*/
ToneDeltaPair(DynamicColor role_a, DynamicColor role_b, double delta,
TonePolarity polarity, bool stay_together)
: role_a_(role_a),
role_b_(role_b),
delta_(delta),
polarity_(polarity),
stay_together_(stay_together) {}
};
} // namespace material_color_utilities
#endif // CPP_DYNAMICCOLOR_TONE_DELTA_PAIR_H_
@@ -0,0 +1,36 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_DYNAMICCOLOR_VARIANT_H_
#define CPP_DYNAMICCOLOR_VARIANT_H_
namespace material_color_utilities {
enum class Variant {
kMonochrome,
kNeutral,
kTonalSpot,
kVibrant,
kExpressive,
kFidelity,
kContent,
kRainbow,
kFruitSalad,
};
} // namespace material_color_utilities
#endif // CPP_DYNAMICCOLOR_VARIANT_H_
@@ -0,0 +1,39 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_PALETTES_CORE_H_
#define CPP_PALETTES_CORE_H_
#include "cpp/palettes/tones.h"
namespace material_color_utilities {
/**
* Comprises foundational palettes to build a color scheme. Generated from a
* source color, these palettes will then be part of a [DynamicScheme] together
* with appearance preferences.
*/
typedef struct {
TonalPalette primary;
TonalPalette secondary;
TonalPalette tertiary;
TonalPalette neutral;
TonalPalette neutral_variant;
} CorePalettes;
} // namespace material_color_utilities
#endif // CPP_PALETTES_CORE_H_
@@ -0,0 +1,116 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/palettes/tones.h"
#include <cmath>
#include "cpp/cam/cam.h"
#include "cpp/cam/hct.h"
namespace material_color_utilities {
TonalPalette::TonalPalette(Argb argb) : key_color_(0.0, 0.0, 0.0) {
Cam cam = CamFromInt(argb);
hue_ = cam.hue;
chroma_ = cam.chroma;
key_color_ = KeyColor(cam.hue, cam.chroma).create();
}
TonalPalette::TonalPalette(Hct hct)
: key_color_(hct.get_hue(), hct.get_chroma(), hct.get_tone()) {
hue_ = hct.get_hue();
chroma_ = hct.get_chroma();
}
TonalPalette::TonalPalette(double hue, double chroma)
: key_color_(hue, chroma, 0.0) {
hue_ = hue;
chroma_ = chroma;
key_color_ = KeyColor(hue, chroma).create();
}
TonalPalette::TonalPalette(double hue, double chroma, Hct key_color)
: key_color_(key_color.get_hue(), key_color.get_chroma(),
key_color.get_tone()) {
hue_ = hue;
chroma_ = chroma;
}
Argb TonalPalette::get(double tone) const {
return IntFromHcl(hue_, chroma_, tone);
}
KeyColor::KeyColor(double hue, double requested_chroma)
: hue_(hue), requested_chroma_(requested_chroma) {}
Hct KeyColor::create() {
// Pivot around T50 because T50 has the most chroma available, on
// average. Thus it is most likely to have a direct answer.
const int pivot_tone = 50;
const int tone_step_size = 1;
// Epsilon to accept values slightly higher than the requested chroma.
const double epsilon = 0.01;
// Binary search to find the tone that can provide a chroma that is closest
// to the requested chroma.
int lower_tone = 0;
int upper_tone = 100;
while (lower_tone < upper_tone) {
const int mid_tone = (lower_tone + upper_tone) / 2;
bool is_ascending =
max_chroma(mid_tone) < max_chroma(mid_tone + tone_step_size);
bool sufficient_chroma =
max_chroma(mid_tone) >= requested_chroma_ - epsilon;
if (sufficient_chroma) {
// Either range [lower_tone, mid_tone] or [mid_tone, upper_tone] has
// the answer, so search in the range that is closer the pivot tone.
if (abs(lower_tone - pivot_tone) < abs(upper_tone - pivot_tone)) {
upper_tone = mid_tone;
} else {
if (lower_tone == mid_tone) {
return Hct(hue_, requested_chroma_, lower_tone);
}
lower_tone = mid_tone;
}
} else {
// As there's no sufficient chroma in the mid_tone, follow the direction
// to the chroma peak.
if (is_ascending) {
lower_tone = mid_tone + tone_step_size;
} else {
// Keep mid_tone for potential chroma peak.
upper_tone = mid_tone;
}
}
}
return Hct(hue_, requested_chroma_, lower_tone);
}
double KeyColor::max_chroma(double tone) {
auto it = chroma_cache_.find(tone);
if (it != chroma_cache_.end()) {
return it->second;
}
double chroma = Hct(hue_, max_chroma_value_, tone).get_chroma();
chroma_cache_[tone] = chroma;
return chroma;
};
} // namespace material_color_utilities
@@ -0,0 +1,78 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_PALETTES_TONES_H_
#define CPP_PALETTES_TONES_H_
#include <unordered_map>
#include "cpp/cam/hct.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
class TonalPalette {
public:
explicit TonalPalette(Argb argb);
TonalPalette(Hct hct);
TonalPalette(double hue, double chroma);
TonalPalette(double hue, double chroma, Hct key_color);
/**
* Returns the color for a given tone in this palette.
*
* @param tone 0.0 <= tone <= 100.0
* @return a color as an integer, in ARGB format.
*/
Argb get(double tone) const;
double get_hue() const { return hue_; }
double get_chroma() const { return chroma_; }
Hct get_key_color() const { return key_color_; }
private:
double hue_;
double chroma_;
Hct key_color_;
};
/**
* Key color is a color that represents the hue and chroma of a tonal palette
*/
class KeyColor {
public:
KeyColor(double hue, double requested_chroma);
/**
* Creates a key color from a [hue] and a [chroma].
* The key color is the first tone, starting from T50, matching the given hue
* and chroma.
*
* @return Key color in Hct.
*/
Hct create();
private:
const double max_chroma_value_ = 200.0;
double hue_;
double requested_chroma_;
// Cache that maps tone to max chroma to avoid duplicated HCT calculation.
std::unordered_map<double, double> chroma_cache_;
double max_chroma(double tone);
};
} // namespace material_color_utilities
#endif // CPP_PALETTES_TONES_H_
@@ -0,0 +1,60 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/quantize/celebi.h"
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include "cpp/quantize/wsmeans.h"
#include "cpp/quantize/wu.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
QuantizerResult QuantizeCelebi(const std::vector<Argb>& pixels,
uint16_t max_colors) {
if (max_colors == 0 || pixels.empty()) {
return QuantizerResult();
}
if (max_colors > 256) {
max_colors = 256;
}
int pixel_count = pixels.size();
std::vector<Argb> opaque_pixels;
opaque_pixels.reserve(pixel_count);
for (int i = 0; i < pixel_count; i++) {
int pixel = pixels[i];
if (!IsOpaque(pixel)) {
continue;
}
opaque_pixels.push_back(pixel);
}
std::vector<Argb> wu_result = QuantizeWu(opaque_pixels, max_colors);
QuantizerResult result =
QuantizeWsmeans(opaque_pixels, wu_result, max_colors);
return result;
}
} // namespace material_color_utilities
@@ -0,0 +1,35 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_QUANTIZE_CELEBI_H_
#define CPP_QUANTIZE_CELEBI_H_
#include <stdint.h>
#include <stdlib.h>
#include <vector>
#include "cpp/quantize/wsmeans.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
QuantizerResult QuantizeCelebi(const std::vector<Argb>& pixels,
uint16_t max_colors);
} // namespace material_color_utilities
#endif // CPP_QUANTIZE_CELEBI_H_
@@ -0,0 +1,96 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/quantize/lab.h"
#include <math.h>
#include "cpp/utils/utils.h"
namespace material_color_utilities {
Argb IntFromLab(const Lab lab) {
double e = 216.0 / 24389.0;
double kappa = 24389.0 / 27.0;
double ke = 8.0;
double fy = (lab.l + 16.0) / 116.0;
double fx = (lab.a / 500.0) + fy;
double fz = fy - (lab.b / 200.0);
double fx3 = fx * fx * fx;
double x_normalized = (fx3 > e) ? fx3 : (116.0 * fx - 16.0) / kappa;
double y_normalized = (lab.l > ke) ? fy * fy * fy : (lab.l / kappa);
double fz3 = fz * fz * fz;
double z_normalized = (fz3 > e) ? fz3 : (116.0 * fz - 16.0) / kappa;
double x = x_normalized * kWhitePointD65[0];
double y = y_normalized * kWhitePointD65[1];
double z = z_normalized * kWhitePointD65[2];
// intFromXyz
double rL = 3.2406 * x - 1.5372 * y - 0.4986 * z;
double gL = -0.9689 * x + 1.8758 * y + 0.0415 * z;
double bL = 0.0557 * x - 0.2040 * y + 1.0570 * z;
int red = Delinearized(rL);
int green = Delinearized(gL);
int blue = Delinearized(bL);
return ArgbFromRgb(red, green, blue);
}
Lab LabFromInt(const Argb argb) {
int red = (argb & 0x00ff0000) >> 16;
int green = (argb & 0x0000ff00) >> 8;
int blue = (argb & 0x000000ff);
double red_l = Linearized(red);
double green_l = Linearized(green);
double blue_l = Linearized(blue);
double x = 0.41233895 * red_l + 0.35762064 * green_l + 0.18051042 * blue_l;
double y = 0.2126 * red_l + 0.7152 * green_l + 0.0722 * blue_l;
double z = 0.01932141 * red_l + 0.11916382 * green_l + 0.95034478 * blue_l;
double y_normalized = y / kWhitePointD65[1];
double e = 216.0 / 24389.0;
double kappa = 24389.0 / 27.0;
double fy;
if (y_normalized > e) {
fy = pow(y_normalized, 1.0 / 3.0);
} else {
fy = (kappa * y_normalized + 16) / 116;
}
double x_normalized = x / kWhitePointD65[0];
double fx;
if (x_normalized > e) {
fx = pow(x_normalized, 1.0 / 3.0);
} else {
fx = (kappa * x_normalized + 16) / 116;
}
double z_normalized = z / kWhitePointD65[2];
double fz;
if (z_normalized > e) {
fz = pow(z_normalized, 1.0 / 3.0);
} else {
fz = (kappa * z_normalized + 16) / 116;
}
double l = 116.0 * fy - 16;
double a = 500.0 * (fx - fy);
double b = 200.0 * (fy - fz);
return {l, a, b};
}
} // namespace material_color_utilities
+57
View File
@@ -0,0 +1,57 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_QUANTIZE_LAB_H_
#define CPP_QUANTIZE_LAB_H_
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "cpp/utils/utils.h"
namespace material_color_utilities {
struct Lab {
double l = 0.0;
double a = 0.0;
double b = 0.0;
double DeltaE(const Lab& lab) {
double d_l = l - lab.l;
double d_a = a - lab.a;
double d_b = b - lab.b;
return (d_l * d_l) + (d_a * d_a) + (d_b * d_b);
}
std::string ToString() {
return "Lab: L* " + std::to_string(l) + " a* " + std::to_string(a) +
" b* " + std::to_string(b);
}
};
Argb IntFromLab(const Lab lab);
Lab LabFromInt(const Argb argb);
} // namespace material_color_utilities
#endif // CPP_QUANTIZE_LAB_H_
@@ -0,0 +1,265 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/quantize/wsmeans.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "cpp/quantize/lab.h"
constexpr int kMaxIterations = 100;
constexpr double kMinDeltaE = 3.0;
namespace material_color_utilities {
struct Swatch {
Argb argb = 0;
int population = 0;
bool operator<(const Swatch& b) const { return population > b.population; }
};
struct DistanceToIndex {
double distance = 0.0;
int index = 0;
bool operator<(const DistanceToIndex& a) const {
return distance < a.distance;
}
};
QuantizerResult QuantizeWsmeans(const std::vector<Argb>& input_pixels,
const std::vector<Argb>& starting_clusters,
uint16_t max_colors) {
if (max_colors == 0 || input_pixels.empty()) {
return QuantizerResult();
}
if (max_colors > 256) {
// If colors is outside the range, just set it the max.
max_colors = 256;
}
uint32_t pixel_count = input_pixels.size();
std::unordered_map<Argb, int> pixel_to_count;
std::vector<uint32_t> pixels;
pixels.reserve(pixel_count);
std::vector<Lab> points;
points.reserve(pixel_count);
for (Argb pixel : input_pixels) {
// tested over 1000 runs with 128 colors, 12544 (112 x 112)
// std::map 10.9 ms
// std::unordered_map 10.2 ms
// absl::btree_map 9.0 ms
// absl::flat_hash_map 8.0 ms
std::unordered_map<Argb, int>::iterator it = pixel_to_count.find(pixel);
if (it != pixel_to_count.end()) {
it->second++;
} else {
pixels.push_back(pixel);
points.push_back(LabFromInt(pixel));
pixel_to_count[pixel] = 1;
}
}
int cluster_count = std::min((int)max_colors, (int)points.size());
if (!starting_clusters.empty()) {
cluster_count = std::min(cluster_count, (int)starting_clusters.size());
}
int pixel_count_sums[256] = {};
std::vector<Lab> clusters;
clusters.reserve(starting_clusters.size());
for (int argb : starting_clusters) {
clusters.push_back(LabFromInt(argb));
}
srand(42688);
int additional_clusters_needed = cluster_count - clusters.size();
if (starting_clusters.empty() && additional_clusters_needed > 0) {
for (int i = 0; i < additional_clusters_needed; i++) {
// Adds a random Lab color to clusters.
double l = rand() / (static_cast<double>(RAND_MAX)) * (100.0) + 0.0;
double a =
rand() / (static_cast<double>(RAND_MAX)) * (100.0 - -100.0) - 100.0;
double b =
rand() / (static_cast<double>(RAND_MAX)) * (100.0 - -100.0) - 100.0;
clusters.push_back({l, a, b});
}
}
std::vector<int> cluster_indices;
cluster_indices.reserve(points.size());
srand(42688);
for (size_t i = 0; i < points.size(); i++) {
cluster_indices.push_back(rand() % cluster_count);
}
std::vector<std::vector<int>> index_matrix(
cluster_count, std::vector<int>(cluster_count, 0));
std::vector<std::vector<DistanceToIndex>> distance_to_index_matrix(
cluster_count, std::vector<DistanceToIndex>(cluster_count));
for (int iteration = 0; iteration < kMaxIterations; iteration++) {
// Calculate cluster distances
for (int i = 0; i < cluster_count; i++) {
distance_to_index_matrix[i][i].distance = 0;
distance_to_index_matrix[i][i].index = i;
for (int j = i + 1; j < cluster_count; j++) {
double distance = clusters[i].DeltaE(clusters[j]);
distance_to_index_matrix[j][i].distance = distance;
distance_to_index_matrix[j][i].index = i;
distance_to_index_matrix[i][j].distance = distance;
distance_to_index_matrix[i][j].index = j;
}
std::vector<DistanceToIndex> row = distance_to_index_matrix[i];
std::sort(row.begin(), row.end());
for (int j = 0; j < cluster_count; j++) {
index_matrix[i][j] = row[j].index;
}
}
// Reassign points
bool color_moved = false;
for (size_t i = 0; i < points.size(); i++) {
Lab point = points[i];
int previous_cluster_index = cluster_indices[i];
Lab previous_cluster = clusters[previous_cluster_index];
double previous_distance = point.DeltaE(previous_cluster);
double minimum_distance = previous_distance;
int new_cluster_index = -1;
for (int j = 0; j < cluster_count; j++) {
if (distance_to_index_matrix[previous_cluster_index][j].distance >=
4 * previous_distance) {
continue;
}
double distance = point.DeltaE(clusters[j]);
if (distance < minimum_distance) {
minimum_distance = distance;
new_cluster_index = j;
}
}
if (new_cluster_index != -1) {
double distanceChange =
abs(sqrt(minimum_distance) - sqrt(previous_distance));
if (distanceChange > kMinDeltaE) {
color_moved = true;
cluster_indices[i] = new_cluster_index;
}
}
}
if (!color_moved && (iteration != 0)) {
break;
}
// Recalculate cluster centers
double component_a_sums[256] = {};
double component_b_sums[256] = {};
double component_c_sums[256] = {};
for (int i = 0; i < cluster_count; i++) {
pixel_count_sums[i] = 0;
}
for (size_t i = 0; i < points.size(); i++) {
int clusterIndex = cluster_indices[i];
Lab point = points[i];
int count = pixel_to_count[pixels[i]];
pixel_count_sums[clusterIndex] += count;
component_a_sums[clusterIndex] += (point.l * count);
component_b_sums[clusterIndex] += (point.a * count);
component_c_sums[clusterIndex] += (point.b * count);
}
for (int i = 0; i < cluster_count; i++) {
int count = pixel_count_sums[i];
if (count == 0) {
clusters[i] = {0, 0, 0};
continue;
}
double a = component_a_sums[i] / count;
double b = component_b_sums[i] / count;
double c = component_c_sums[i] / count;
clusters[i] = {a, b, c};
}
}
std::vector<Swatch> swatches;
std::vector<Argb> cluster_argbs;
std::vector<Argb> all_cluster_argbs;
for (int i = 0; i < cluster_count; i++) {
Argb possible_new_cluster = IntFromLab(clusters[i]);
all_cluster_argbs.push_back(possible_new_cluster);
int count = pixel_count_sums[i];
if (count == 0) {
continue;
}
int use_new_cluster = 1;
for (size_t j = 0; j < swatches.size(); j++) {
if (swatches[j].argb == possible_new_cluster) {
swatches[j].population += count;
use_new_cluster = 0;
break;
}
}
if (use_new_cluster == 0) {
continue;
}
cluster_argbs.push_back(possible_new_cluster);
swatches.push_back({possible_new_cluster, count});
}
std::sort(swatches.begin(), swatches.end());
// Constructs the quantizer result to return.
std::map<Argb, uint32_t> color_to_count;
for (size_t i = 0; i < swatches.size(); i++) {
color_to_count[swatches[i].argb] = swatches[i].population;
}
std::map<Argb, Argb> input_pixel_to_cluster_pixel;
for (size_t i = 0; i < points.size(); i++) {
int pixel = pixels[i];
int cluster_index = cluster_indices[i];
int cluster_argb = all_cluster_argbs[cluster_index];
input_pixel_to_cluster_pixel[pixel] = cluster_argb;
}
return {color_to_count, input_pixel_to_cluster_pixel};
}
} // namespace material_color_utilities
@@ -0,0 +1,38 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_QUANTIZE_WSMEANS_H_
#define CPP_QUANTIZE_WSMEANS_H_
#include <stdint.h>
#include <map>
#include <vector>
#include "cpp/utils/utils.h"
namespace material_color_utilities {
struct QuantizerResult {
std::map<Argb, uint32_t> color_to_count;
std::map<Argb, Argb> input_pixel_to_cluster_pixel;
};
QuantizerResult QuantizeWsmeans(const std::vector<Argb>& input_pixels,
const std::vector<Argb>& starting_clusters,
uint16_t max_colors);
} // namespace material_color_utilities
#endif // CPP_QUANTIZE_WSMEANS_H_
+357
View File
@@ -0,0 +1,357 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/quantize/wu.h"
#include <stdlib.h>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <vector>
#include "cpp/utils/utils.h"
namespace material_color_utilities {
struct Box {
int r0 = 0;
int r1 = 0;
int g0 = 0;
int g1 = 0;
int b0 = 0;
int b1 = 0;
int vol = 0;
};
enum class Direction {
kRed,
kGreen,
kBlue,
};
constexpr int kIndexBits = 5;
constexpr int kIndexCount = ((1 << kIndexBits) + 1);
constexpr int kTotalSize = (kIndexCount * kIndexCount * kIndexCount);
constexpr int kMaxColors = 256;
using IntArray = std::vector<int64_t>;
using DoubleArray = std::vector<double>;
int GetIndex(int r, int g, int b) {
return (r << (kIndexBits * 2)) + (r << (kIndexBits + 1)) + (g << kIndexBits) +
r + g + b;
}
void ConstructHistogram(const std::vector<Argb>& pixels, IntArray& weights,
IntArray& m_r, IntArray& m_g, IntArray& m_b,
DoubleArray& moments) {
for (size_t i = 0; i < pixels.size(); i++) {
Argb pixel = pixels[i];
int red = RedFromInt(pixel);
int green = GreenFromInt(pixel);
int blue = BlueFromInt(pixel);
int bits_to_remove = 8 - kIndexBits;
int index_r = (red >> bits_to_remove) + 1;
int index_g = (green >> bits_to_remove) + 1;
int index_b = (blue >> bits_to_remove) + 1;
int index = GetIndex(index_r, index_g, index_b);
weights[index]++;
m_r[index] += red;
m_g[index] += green;
m_b[index] += blue;
moments[index] += (red * red) + (green * green) + (blue * blue);
}
}
void ComputeMoments(IntArray& weights, IntArray& m_r, IntArray& m_g,
IntArray& m_b, DoubleArray& moments) {
for (int r = 1; r < kIndexCount; r++) {
int64_t area[kIndexCount] = {};
int64_t area_r[kIndexCount] = {};
int64_t area_g[kIndexCount] = {};
int64_t area_b[kIndexCount] = {};
double area_2[kIndexCount] = {};
for (int g = 1; g < kIndexCount; g++) {
int64_t line = 0;
int64_t line_r = 0;
int64_t line_g = 0;
int64_t line_b = 0;
double line_2 = 0.0;
for (int b = 1; b < kIndexCount; b++) {
int index = GetIndex(r, g, b);
line += weights[index];
line_r += m_r[index];
line_g += m_g[index];
line_b += m_b[index];
line_2 += moments[index];
area[b] += line;
area_r[b] += line_r;
area_g[b] += line_g;
area_b[b] += line_b;
area_2[b] += line_2;
int previous_index = GetIndex(r - 1, g, b);
weights[index] = weights[previous_index] + area[b];
m_r[index] = m_r[previous_index] + area_r[b];
m_g[index] = m_g[previous_index] + area_g[b];
m_b[index] = m_b[previous_index] + area_b[b];
moments[index] = moments[previous_index] + area_2[b];
}
}
}
}
int64_t Top(const Box& cube, const Direction direction, const int position,
const IntArray& moment) {
if (direction == Direction::kRed) {
return (moment[GetIndex(position, cube.g1, cube.b1)] -
moment[GetIndex(position, cube.g1, cube.b0)] -
moment[GetIndex(position, cube.g0, cube.b1)] +
moment[GetIndex(position, cube.g0, cube.b0)]);
} else if (direction == Direction::kGreen) {
return (moment[GetIndex(cube.r1, position, cube.b1)] -
moment[GetIndex(cube.r1, position, cube.b0)] -
moment[GetIndex(cube.r0, position, cube.b1)] +
moment[GetIndex(cube.r0, position, cube.b0)]);
} else {
return (moment[GetIndex(cube.r1, cube.g1, position)] -
moment[GetIndex(cube.r1, cube.g0, position)] -
moment[GetIndex(cube.r0, cube.g1, position)] +
moment[GetIndex(cube.r0, cube.g0, position)]);
}
}
int64_t Bottom(const Box& cube, const Direction direction,
const IntArray& moment) {
if (direction == Direction::kRed) {
return (-moment[GetIndex(cube.r0, cube.g1, cube.b1)] +
moment[GetIndex(cube.r0, cube.g1, cube.b0)] +
moment[GetIndex(cube.r0, cube.g0, cube.b1)] -
moment[GetIndex(cube.r0, cube.g0, cube.b0)]);
} else if (direction == Direction::kGreen) {
return (-moment[GetIndex(cube.r1, cube.g0, cube.b1)] +
moment[GetIndex(cube.r1, cube.g0, cube.b0)] +
moment[GetIndex(cube.r0, cube.g0, cube.b1)] -
moment[GetIndex(cube.r0, cube.g0, cube.b0)]);
} else {
return (-moment[GetIndex(cube.r1, cube.g1, cube.b0)] +
moment[GetIndex(cube.r1, cube.g0, cube.b0)] +
moment[GetIndex(cube.r0, cube.g1, cube.b0)] -
moment[GetIndex(cube.r0, cube.g0, cube.b0)]);
}
}
int64_t Vol(const Box& cube, const IntArray& moment) {
return (moment[GetIndex(cube.r1, cube.g1, cube.b1)] -
moment[GetIndex(cube.r1, cube.g1, cube.b0)] -
moment[GetIndex(cube.r1, cube.g0, cube.b1)] +
moment[GetIndex(cube.r1, cube.g0, cube.b0)] -
moment[GetIndex(cube.r0, cube.g1, cube.b1)] +
moment[GetIndex(cube.r0, cube.g1, cube.b0)] +
moment[GetIndex(cube.r0, cube.g0, cube.b1)] -
moment[GetIndex(cube.r0, cube.g0, cube.b0)]);
}
double Variance(const Box& cube, const IntArray& weights, const IntArray& m_r,
const IntArray& m_g, const IntArray& m_b,
const DoubleArray& moments) {
double dr = Vol(cube, m_r);
double dg = Vol(cube, m_g);
double db = Vol(cube, m_b);
double xx = moments[GetIndex(cube.r1, cube.g1, cube.b1)] -
moments[GetIndex(cube.r1, cube.g1, cube.b0)] -
moments[GetIndex(cube.r1, cube.g0, cube.b1)] +
moments[GetIndex(cube.r1, cube.g0, cube.b0)] -
moments[GetIndex(cube.r0, cube.g1, cube.b1)] +
moments[GetIndex(cube.r0, cube.g1, cube.b0)] +
moments[GetIndex(cube.r0, cube.g0, cube.b1)] -
moments[GetIndex(cube.r0, cube.g0, cube.b0)];
double hypotenuse = dr * dr + dg * dg + db * db;
double volume = Vol(cube, weights);
return xx - hypotenuse / volume;
}
double Maximize(const Box& cube, const Direction direction, const int first,
const int last, int* cut, const int64_t whole_w,
const int64_t whole_r, const int64_t whole_g,
const int64_t whole_b, const IntArray& weights,
const IntArray& m_r, const IntArray& m_g, const IntArray& m_b) {
int64_t bottom_r = Bottom(cube, direction, m_r);
int64_t bottom_g = Bottom(cube, direction, m_g);
int64_t bottom_b = Bottom(cube, direction, m_b);
int64_t bottom_w = Bottom(cube, direction, weights);
double max = 0.0;
*cut = -1;
int64_t half_r, half_g, half_b, half_w;
for (int i = first; i < last; i++) {
half_r = bottom_r + Top(cube, direction, i, m_r);
half_g = bottom_g + Top(cube, direction, i, m_g);
half_b = bottom_b + Top(cube, direction, i, m_b);
half_w = bottom_w + Top(cube, direction, i, weights);
if (half_w == 0) {
continue;
}
double temp = (static_cast<double>(half_r) * half_r +
static_cast<double>(half_g) * half_g +
static_cast<double>(half_b) * half_b) /
static_cast<double>(half_w);
half_r = whole_r - half_r;
half_g = whole_g - half_g;
half_b = whole_b - half_b;
half_w = whole_w - half_w;
if (half_w == 0) {
continue;
}
temp += (static_cast<double>(half_r) * half_r +
static_cast<double>(half_g) * half_g +
static_cast<double>(half_b) * half_b) /
static_cast<double>(half_w);
if (temp > max) {
max = temp;
*cut = i;
}
}
return max;
}
bool Cut(Box& box1, Box& box2, const IntArray& weights, const IntArray& m_r,
const IntArray& m_g, const IntArray& m_b) {
int64_t whole_r = Vol(box1, m_r);
int64_t whole_g = Vol(box1, m_g);
int64_t whole_b = Vol(box1, m_b);
int64_t whole_w = Vol(box1, weights);
int cut_r, cut_g, cut_b;
double max_r =
Maximize(box1, Direction::kRed, box1.r0 + 1, box1.r1, &cut_r, whole_w,
whole_r, whole_g, whole_b, weights, m_r, m_g, m_b);
double max_g =
Maximize(box1, Direction::kGreen, box1.g0 + 1, box1.g1, &cut_g, whole_w,
whole_r, whole_g, whole_b, weights, m_r, m_g, m_b);
double max_b =
Maximize(box1, Direction::kBlue, box1.b0 + 1, box1.b1, &cut_b, whole_w,
whole_r, whole_g, whole_b, weights, m_r, m_g, m_b);
Direction direction;
if (max_r >= max_g && max_r >= max_b) {
direction = Direction::kRed;
if (cut_r < 0) {
return false;
}
} else if (max_g >= max_r && max_g >= max_b) {
direction = Direction::kGreen;
} else {
direction = Direction::kBlue;
}
box2.r1 = box1.r1;
box2.g1 = box1.g1;
box2.b1 = box1.b1;
if (direction == Direction::kRed) {
box2.r0 = box1.r1 = cut_r;
box2.g0 = box1.g0;
box2.b0 = box1.b0;
} else if (direction == Direction::kGreen) {
box2.r0 = box1.r0;
box2.g0 = box1.g1 = cut_g;
box2.b0 = box1.b0;
} else {
box2.r0 = box1.r0;
box2.g0 = box1.g0;
box2.b0 = box1.b1 = cut_b;
}
box1.vol = (box1.r1 - box1.r0) * (box1.g1 - box1.g0) * (box1.b1 - box1.b0);
box2.vol = (box2.r1 - box2.r0) * (box2.g1 - box2.g0) * (box2.b1 - box2.b0);
return true;
}
std::vector<Argb> QuantizeWu(const std::vector<Argb>& pixels,
uint16_t max_colors) {
if (max_colors <= 0 || max_colors > 256 || pixels.empty()) {
return std::vector<Argb>();
}
IntArray weights(kTotalSize, 0);
IntArray moments_red(kTotalSize, 0);
IntArray moments_green(kTotalSize, 0);
IntArray moments_blue(kTotalSize, 0);
DoubleArray moments(kTotalSize, 0.0);
ConstructHistogram(pixels, weights, moments_red, moments_green, moments_blue,
moments);
ComputeMoments(weights, moments_red, moments_green, moments_blue, moments);
std::vector<Box> cubes(kMaxColors);
cubes[0].r0 = cubes[0].g0 = cubes[0].b0 = 0;
cubes[0].r1 = cubes[0].g1 = cubes[0].b1 = kIndexCount - 1;
std::vector<double> volume_variance(kMaxColors);
int next = 0;
for (int i = 1; i < max_colors; ++i) {
if (Cut(cubes[next], cubes[i], weights, moments_red, moments_green,
moments_blue)) {
volume_variance[next] =
cubes[next].vol > 1 ? Variance(cubes[next], weights, moments_red,
moments_green, moments_blue, moments)
: 0.0;
volume_variance[i] = cubes[i].vol > 1
? Variance(cubes[i], weights, moments_red,
moments_green, moments_blue, moments)
: 0.0;
} else {
volume_variance[next] = 0.0;
i--;
}
next = 0;
double temp = volume_variance[0];
for (int j = 1; j <= i; j++) {
if (volume_variance[j] > temp) {
temp = volume_variance[j];
next = j;
}
}
if (temp <= 0.0) {
max_colors = i + 1;
break;
}
}
std::vector<Argb> out_colors;
for (int i = 0; i < max_colors; ++i) {
int64_t weight = Vol(cubes[i], weights);
if (weight > 0) {
int32_t red = Vol(cubes[i], moments_red) / weight;
int32_t green = Vol(cubes[i], moments_green) / weight;
int32_t blue = Vol(cubes[i], moments_blue) / weight;
uint32_t argb = ArgbFromRgb(red, green, blue);
out_colors.push_back(argb);
}
}
return out_colors;
}
} // namespace material_color_utilities
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_QUANTIZE_WU_H_
#define CPP_QUANTIZE_WU_H_
#include <stdint.h>
#include <vector>
#include "cpp/utils/utils.h"
namespace material_color_utilities {
std::vector<Argb> QuantizeWu(const std::vector<Argb>& pixels,
uint16_t max_colors);
}
#endif // CPP_QUANTIZE_WU_H_
@@ -0,0 +1,58 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/scheme/scheme_content.h"
#include <cmath>
#include "cpp/cam/hct.h"
#include "cpp/dislike/dislike.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
#include "cpp/temperature/temperature_cache.h"
namespace material_color_utilities {
SchemeContent::SchemeContent(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level)
: DynamicScheme(
/*set_source_color_hct:*/ set_source_color_hct,
/*variant:*/ Variant::kContent,
/*contrast_level:*/ set_contrast_level,
/*is_dark:*/ set_is_dark,
/*primary_palette:*/
TonalPalette(set_source_color_hct.get_hue(),
set_source_color_hct.get_chroma()),
/*secondary_palette:*/
TonalPalette(set_source_color_hct.get_hue(),
fmax(set_source_color_hct.get_chroma() - 32.0,
set_source_color_hct.get_chroma() * 0.5)),
/*tertiary_palette:*/
TonalPalette(FixIfDisliked(TemperatureCache(set_source_color_hct)
.GetAnalogousColors(3, 6)
.at(2))),
/*neutral_palette:*/
TonalPalette(set_source_color_hct.get_hue(),
set_source_color_hct.get_chroma() / 8.0),
/*neutral_variant_palette:*/
TonalPalette(set_source_color_hct.get_hue(),
set_source_color_hct.get_chroma() / 8.0 + 4.0)) {}
SchemeContent::SchemeContent(Hct set_source_color_hct, bool set_is_dark)
: SchemeContent::SchemeContent(set_source_color_hct, set_is_dark, 0.0) {}
} // namespace material_color_utilities
@@ -0,0 +1,33 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCHEME_SCHEME_CONTENT_H_
#define CPP_SCHEME_SCHEME_CONTENT_H_
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
namespace material_color_utilities {
struct SchemeContent : public DynamicScheme {
SchemeContent(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level);
SchemeContent(Hct set_source_color_hct, bool set_is_dark);
};
} // namespace material_color_utilities
#endif // CPP_SCHEME_SCHEME_CONTENT_H_
@@ -0,0 +1,62 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/scheme/scheme_expressive.h"
#include <vector>
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
namespace material_color_utilities {
const std::vector<double> kHues = {0, 21, 51, 121, 151, 191, 271, 321, 360};
const std::vector<double> kSecondaryRotations = {45, 95, 45, 20, 45,
90, 45, 45, 45};
const std::vector<double> kTertiaryRotations = {120, 120, 20, 45, 20,
15, 20, 120, 120};
SchemeExpressive::SchemeExpressive(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level)
: DynamicScheme(
/*set_source_color_hct:*/ set_source_color_hct,
/*variant:*/ Variant::kExpressive,
/*contrast_level:*/ set_contrast_level,
/*is_dark:*/ set_is_dark,
/*primary_palette:*/
TonalPalette(set_source_color_hct.get_hue() + 240.0, 40.0),
/*secondary_palette:*/
TonalPalette(DynamicScheme::GetRotatedHue(set_source_color_hct, kHues,
kSecondaryRotations),
24.0),
/*tertiary_palette:*/
TonalPalette(DynamicScheme::GetRotatedHue(set_source_color_hct, kHues,
kTertiaryRotations),
32.0),
/*neutral_palette:*/
TonalPalette(set_source_color_hct.get_hue() + 15.0, 8.0),
/*neutral_variant_palette:*/
TonalPalette(set_source_color_hct.get_hue() + 15, 12.0)) {}
SchemeExpressive::SchemeExpressive(Hct set_source_color_hct, bool set_is_dark)
: SchemeExpressive::SchemeExpressive(set_source_color_hct, set_is_dark,
0.0) {}
} // namespace material_color_utilities
@@ -0,0 +1,32 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCHEME_SCHEME_EXPRESSIVE_H_
#define CPP_SCHEME_SCHEME_EXPRESSIVE_H_
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
namespace material_color_utilities {
struct SchemeExpressive : public DynamicScheme {
SchemeExpressive(Hct source_color_hct, bool is_dark, double contrast_level);
SchemeExpressive(Hct source_color_hct, bool is_dark);
};
} // namespace material_color_utilities
#endif // CPP_SCHEME_SCHEME_EXPRESSIVE_H_
@@ -0,0 +1,57 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/scheme/scheme_fidelity.h"
#include <cmath>
#include "cpp/cam/hct.h"
#include "cpp/dislike/dislike.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
#include "cpp/temperature/temperature_cache.h"
namespace material_color_utilities {
SchemeFidelity::SchemeFidelity(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level)
: DynamicScheme(
/*set_source_color_hct:*/ set_source_color_hct,
/*variant:*/ Variant::kFidelity,
/*contrast_level:*/ set_contrast_level,
/*is_dark:*/ set_is_dark,
/*primary_palette:*/
TonalPalette(set_source_color_hct.get_hue(),
set_source_color_hct.get_chroma()),
/*secondary_palette:*/
TonalPalette(set_source_color_hct.get_hue(),
fmax(set_source_color_hct.get_chroma() - 32.0,
set_source_color_hct.get_chroma() * 0.5)),
/*tertiary_palette:*/
TonalPalette(FixIfDisliked(
TemperatureCache(set_source_color_hct).GetComplement())),
/*neutral_palette:*/
TonalPalette(set_source_color_hct.get_hue(),
set_source_color_hct.get_chroma() / 8.0),
/*neutral_variant_palette:*/
TonalPalette(set_source_color_hct.get_hue(),
set_source_color_hct.get_chroma() / 8.0 + 4.0)) {}
SchemeFidelity::SchemeFidelity(Hct set_source_color_hct, bool set_is_dark)
: SchemeFidelity::SchemeFidelity(set_source_color_hct, set_is_dark, 0.0) {}
} // namespace material_color_utilities
@@ -0,0 +1,33 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCHEME_SCHEME_FIDELITY_H_
#define CPP_SCHEME_SCHEME_FIDELITY_H_
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
namespace material_color_utilities {
struct SchemeFidelity : public DynamicScheme {
SchemeFidelity(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level);
SchemeFidelity(Hct set_source_color_hct, bool set_is_dark);
};
} // namespace material_color_utilities
#endif // CPP_SCHEME_SCHEME_FIDELITY_H_
@@ -0,0 +1,52 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/scheme/scheme_fruit_salad.h"
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
namespace material_color_utilities {
SchemeFruitSalad::SchemeFruitSalad(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level)
: DynamicScheme(
/*set_source_color_hct:*/ set_source_color_hct,
/*variant:*/ Variant::kFruitSalad,
/*contrast_level:*/ set_contrast_level,
/*is_dark:*/ set_is_dark,
/*primary_palette:*/
TonalPalette(
SanitizeDegreesDouble(set_source_color_hct.get_hue() - 50.0),
48.0),
/*secondary_palette:*/
TonalPalette(
SanitizeDegreesDouble(set_source_color_hct.get_hue() - 50.0),
36.0),
/*tertiary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 36.0),
/*neutral_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 10.0),
/*neutral_variant_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 16.0)) {}
SchemeFruitSalad::SchemeFruitSalad(Hct set_source_color_hct, bool set_is_dark)
: SchemeFruitSalad::SchemeFruitSalad(set_source_color_hct, set_is_dark,
0.0) {}
} // namespace material_color_utilities
@@ -0,0 +1,32 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCHEME_SCHEME_FRUIT_SALAD_H_
#define CPP_SCHEME_SCHEME_FRUIT_SALAD_H_
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
namespace material_color_utilities {
struct SchemeFruitSalad : public DynamicScheme {
SchemeFruitSalad(Hct source_color_hct, bool is_dark, double contrast_level);
SchemeFruitSalad(Hct source_color_hct, bool is_dark);
};
} // namespace material_color_utilities
#endif // CPP_SCHEME_SCHEME_FRUIT_SALAD_H_
@@ -0,0 +1,48 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/scheme/scheme_monochrome.h"
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
namespace material_color_utilities {
SchemeMonochrome::SchemeMonochrome(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level)
: DynamicScheme(
/*set_source_color_hct:*/ set_source_color_hct,
/*variant:*/ Variant::kMonochrome,
/*contrast_level:*/ set_contrast_level,
/*is_dark:*/ set_is_dark,
/*primary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 0.0),
/*secondary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 0.0),
/*tertiary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 0.0),
/*neutral_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 0.0),
/*neutral_variant_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 0.0)) {}
SchemeMonochrome::SchemeMonochrome(Hct set_source_color_hct, bool set_is_dark)
: SchemeMonochrome::SchemeMonochrome(set_source_color_hct, set_is_dark,
0.0) {}
} // namespace material_color_utilities
@@ -0,0 +1,32 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCHEME_SCHEME_MONOCHROME_H_
#define CPP_SCHEME_SCHEME_MONOCHROME_H_
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
namespace material_color_utilities {
struct SchemeMonochrome : public DynamicScheme {
SchemeMonochrome(Hct source_color_hct, bool is_dark, double contrast_level);
SchemeMonochrome(Hct source_color_hct, bool is_dark);
};
} // namespace material_color_utilities
#endif // CPP_SCHEME_SCHEME_MONOCHROME_H_
@@ -0,0 +1,47 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/scheme/scheme_neutral.h"
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
namespace material_color_utilities {
SchemeNeutral::SchemeNeutral(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level)
: DynamicScheme(
/*set_source_color_hct:*/ set_source_color_hct,
/*variant:*/ Variant::kNeutral,
/*contrast_level:*/ set_contrast_level,
/*is_dark:*/ set_is_dark,
/*primary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 12.0),
/*secondary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 8.0),
/*tertiary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 16.0),
/*neutral_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 2.0),
/*neutral_variant_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 2.0)) {}
SchemeNeutral::SchemeNeutral(Hct set_source_color_hct, bool set_is_dark)
: SchemeNeutral::SchemeNeutral(set_source_color_hct, set_is_dark, 0.0) {}
} // namespace material_color_utilities
@@ -0,0 +1,32 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCHEME_SCHEME_NEUTRAL_H_
#define CPP_SCHEME_SCHEME_NEUTRAL_H_
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
namespace material_color_utilities {
struct SchemeNeutral : public DynamicScheme {
SchemeNeutral(Hct source_color_hct, bool is_dark, double contrast_level);
SchemeNeutral(Hct source_color_hct, bool is_dark);
};
} // namespace material_color_utilities
#endif // CPP_SCHEME_SCHEME_NEUTRAL_H_
@@ -0,0 +1,49 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/scheme/scheme_rainbow.h"
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
namespace material_color_utilities {
SchemeRainbow::SchemeRainbow(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level)
: DynamicScheme(
/*set_source_color_hct:*/ set_source_color_hct,
/*variant:*/ Variant::kRainbow,
/*contrast_level:*/ set_contrast_level,
/*is_dark:*/ set_is_dark,
/*primary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 48.0),
/*secondary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 16.0),
/*tertiary_palette:*/
TonalPalette(
SanitizeDegreesDouble(set_source_color_hct.get_hue() + 60.0),
24.0),
/*neutral_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 0.0),
/*neutral_variant_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 0.0)) {}
SchemeRainbow::SchemeRainbow(Hct set_source_color_hct, bool set_is_dark)
: SchemeRainbow::SchemeRainbow(set_source_color_hct, set_is_dark, 0.0) {}
} // namespace material_color_utilities
@@ -0,0 +1,32 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCHEME_SCHEME_RAINBOW_H_
#define CPP_SCHEME_SCHEME_RAINBOW_H_
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
namespace material_color_utilities {
struct SchemeRainbow : public DynamicScheme {
SchemeRainbow(Hct source_color_hct, bool is_dark, double contrast_level);
SchemeRainbow(Hct source_color_hct, bool is_dark);
};
} // namespace material_color_utilities
#endif // CPP_SCHEME_SCHEME_RAINBOW_H_
@@ -0,0 +1,49 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/scheme/scheme_tonal_spot.h"
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
namespace material_color_utilities {
SchemeTonalSpot::SchemeTonalSpot(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level)
: DynamicScheme(
/*set_source_color_hct:*/ set_source_color_hct,
/*variant:*/ Variant::kTonalSpot,
/*contrast_level:*/ set_contrast_level,
/*is_dark:*/ set_is_dark,
/*primary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 36.0),
/*secondary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 16.0),
/*tertiary_palette:*/
TonalPalette(
SanitizeDegreesDouble(set_source_color_hct.get_hue() + 60), 24.0),
/*neutral_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 6.0),
/*neutral_variant_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 8.0)) {}
SchemeTonalSpot::SchemeTonalSpot(Hct set_source_color_hct, bool set_is_dark)
: SchemeTonalSpot::SchemeTonalSpot(set_source_color_hct, set_is_dark, 0.0) {
}
} // namespace material_color_utilities
@@ -0,0 +1,32 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCHEME_SCHEME_TONAL_SPOT_H_
#define CPP_SCHEME_SCHEME_TONAL_SPOT_H_
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
namespace material_color_utilities {
struct SchemeTonalSpot : public DynamicScheme {
SchemeTonalSpot(Hct source_color_hct, bool is_dark, double contrast_level);
SchemeTonalSpot(Hct source_color_hct, bool is_dark);
};
} // namespace material_color_utilities
#endif // CPP_SCHEME_SCHEME_TONAL_SPOT_H_
@@ -0,0 +1,61 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/scheme/scheme_vibrant.h"
#include <vector>
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
#include "cpp/dynamiccolor/variant.h"
#include "cpp/palettes/tones.h"
namespace material_color_utilities {
const std::vector<double> kHues = {0, 41, 61, 101, 131, 181, 251, 301, 360};
const std::vector<double> kSecondaryRotations = {18, 15, 10, 12, 15,
18, 15, 12, 12};
const std::vector<double> kTertiaryRotations = {35, 30, 20, 25, 30,
35, 30, 25, 25};
SchemeVibrant::SchemeVibrant(Hct set_source_color_hct, bool set_is_dark,
double set_contrast_level)
: DynamicScheme(
/*set_source_color_hct:*/ set_source_color_hct,
/*variant:*/ Variant::kVibrant,
/*contrast_level:*/ set_contrast_level,
/*is_dark:*/ set_is_dark,
/*primary_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 200.0),
/*secondary_palette:*/
TonalPalette(DynamicScheme::GetRotatedHue(set_source_color_hct, kHues,
kSecondaryRotations),
24.0),
/*tertiary_palette:*/
TonalPalette(DynamicScheme::GetRotatedHue(set_source_color_hct, kHues,
kTertiaryRotations),
32.0),
/*neutral_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 10.0),
/*neutral_variant_palette:*/
TonalPalette(set_source_color_hct.get_hue(), 12.0)) {}
SchemeVibrant::SchemeVibrant(Hct set_source_color_hct, bool set_is_dark)
: SchemeVibrant::SchemeVibrant(set_source_color_hct, set_is_dark, 0.0) {}
} // namespace material_color_utilities
@@ -0,0 +1,32 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCHEME_SCHEME_VARIANT_H_
#define CPP_SCHEME_SCHEME_VARIANT_H_
#include "cpp/cam/hct.h"
#include "cpp/dynamiccolor/dynamic_scheme.h"
namespace material_color_utilities {
struct SchemeVibrant : public DynamicScheme {
SchemeVibrant(Hct source_color_hct, bool is_dark, double contrast_level);
SchemeVibrant(Hct source_color_hct, bool is_dark);
};
} // namespace material_color_utilities
#endif // CPP_SCHEME_SCHEME_VARIANT_H_
+124
View File
@@ -0,0 +1,124 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/score/score.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <map>
#include <utility>
#include <vector>
#include "cpp/cam/hct.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
constexpr double kTargetChroma = 48.0; // A1 Chroma
constexpr double kWeightProportion = 0.7;
constexpr double kWeightChromaAbove = 0.3;
constexpr double kWeightChromaBelow = 0.1;
constexpr double kCutoffChroma = 5.0;
constexpr double kCutoffExcitedProportion = 0.01;
bool CompareScoredHCT(const std::pair<Hct, double>& a,
const std::pair<Hct, double>& b) {
return a.second > b.second;
}
std::vector<Argb> RankedSuggestions(
const std::map<Argb, uint32_t>& argb_to_population,
const ScoreOptions& options) {
// Get the HCT color for each Argb value, while finding the per hue count and
// total count.
std::vector<Hct> colors_hct;
std::vector<uint32_t> hue_population(360, 0);
double population_sum = 0;
for (const auto& [argb, population] : argb_to_population) {
Hct hct(argb);
colors_hct.push_back(hct);
int hue = floor(hct.get_hue());
hue_population[hue] += population;
population_sum += population;
}
// Hues with more usage in neighboring 30 degree slice get a larger number.
std::vector<double> hue_excited_proportions(360, 0.0);
for (int hue = 0; hue < 360; hue++) {
double proportion = hue_population[hue] / population_sum;
for (int i = hue - 14; i < hue + 16; i++) {
int neighbor_hue = SanitizeDegreesInt(i);
hue_excited_proportions[neighbor_hue] += proportion;
}
}
// Scores each HCT color based on usage and chroma, while optionally
// filtering out values that do not have enough chroma or usage.
std::vector<std::pair<Hct, double>> scored_hcts;
for (Hct hct : colors_hct) {
int hue = SanitizeDegreesInt(round(hct.get_hue()));
double proportion = hue_excited_proportions[hue];
if (options.filter && (hct.get_chroma() < kCutoffChroma ||
proportion <= kCutoffExcitedProportion)) {
continue;
}
double proportion_score = proportion * 100.0 * kWeightProportion;
double chroma_weight = hct.get_chroma() < kTargetChroma
? kWeightChromaBelow
: kWeightChromaAbove;
double chroma_score = (hct.get_chroma() - kTargetChroma) * chroma_weight;
double score = proportion_score + chroma_score;
scored_hcts.push_back({hct, score});
}
// Sorted so that colors with higher scores come first.
sort(scored_hcts.begin(), scored_hcts.end(), CompareScoredHCT);
// Iterates through potential hue differences in degrees in order to select
// the colors with the largest distribution of hues possible. Starting at
// 90 degrees(maximum difference for 4 colors) then decreasing down to a
// 15 degree minimum.
std::vector<Hct> chosen_colors;
for (int difference_degrees = 90; difference_degrees >= 15;
difference_degrees--) {
chosen_colors.clear();
for (auto entry : scored_hcts) {
Hct hct = entry.first;
auto duplicate_hue = std::find_if(
chosen_colors.begin(), chosen_colors.end(),
[&hct, difference_degrees](Hct chosen_hct) {
return DiffDegrees(hct.get_hue(), chosen_hct.get_hue()) <
difference_degrees;
});
if (duplicate_hue == chosen_colors.end()) {
chosen_colors.push_back(hct);
if (chosen_colors.size() >= options.desired) break;
}
}
if (chosen_colors.size() >= options.desired) break;
}
std::vector<Argb> colors;
if (chosen_colors.empty()) {
colors.push_back(options.fallback_color_argb);
}
for (auto chosen_hct : chosen_colors) {
colors.push_back(chosen_hct.ToInt());
}
return colors;
}
} // namespace material_color_utilities
+59
View File
@@ -0,0 +1,59 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_SCORE_SCORE_H_
#define CPP_SCORE_SCORE_H_
#include <cstdlib>
#include <map>
#include <vector>
#include "cpp/utils/utils.h"
namespace material_color_utilities {
/**
* Default options for ranking colors based on usage counts.
* `desired`: is the max count of the colors returned.
* `fallback_color_argb`: Is the default color that should be used if no
* other colors are suitable.
* `filter`: controls if the resulting colors should be filtered to not include
* hues that are not used often enough, and colors that are effectively
* grayscale.
*/
struct ScoreOptions {
size_t desired = 4; // 4 colors matches the Android wallpaper picker.
int fallback_color_argb = 0xff4285f4; // Google Blue.
bool filter = true; // Avoid unsuitable colors.
};
/**
* Given a map with keys of colors and values of how often the color appears,
* rank the colors based on suitability for being used for a UI theme.
*
* The list returned is of length <= [desired]. The recommended color is the
* first item, the least suitable is the last. There will always be at least
* one color returned. If all the input colors were not suitable for a theme,
* a default fallback color will be provided, Google Blue, or supplied fallback
* color. The default number of colors returned is 4, simply because that's the
* # of colors display in Android 12's wallpaper picker.
*/
std::vector<Argb> RankedSuggestions(
const std::map<Argb, uint32_t>& argb_to_population,
const ScoreOptions& options = {});
} // namespace material_color_utilities
#endif // CPP_SCORE_SCORE_H_
@@ -0,0 +1,250 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/temperature/temperature_cache.h"
#include <algorithm>
#include <cmath>
#include <map>
#include <vector>
#include "cpp/cam/hct.h"
#include "cpp/quantize/lab.h"
#include "cpp/utils/utils.h"
namespace material_color_utilities {
TemperatureCache::TemperatureCache(Hct input) : input_(input) {}
Hct TemperatureCache::GetComplement() {
if (precomputed_complement_.has_value()) {
return precomputed_complement_.value();
}
double coldest_hue = GetColdest().get_hue();
double coldest_temp = GetTempsByHct().at(GetColdest());
double warmest_hue = GetWarmest().get_hue();
double warmest_temp = GetTempsByHct().at(GetWarmest());
double range = warmest_temp - coldest_temp;
bool start_hue_is_coldest_to_warmest =
IsBetween(input_.get_hue(), coldest_hue, warmest_hue);
double start_hue =
start_hue_is_coldest_to_warmest ? warmest_hue : coldest_hue;
double end_hue = start_hue_is_coldest_to_warmest ? coldest_hue : warmest_hue;
double direction_of_rotation = 1.0;
double smallest_error = 1000.0;
Hct answer = GetHctsByHue().at((int)round(input_.get_hue()));
double complement_relative_temp = (1.0 - GetRelativeTemperature(input_));
// Find the color in the other section, closest to the inverse percentile
// of the input color. This is the complement.
for (double hue_addend = 0.0; hue_addend <= 360.0; hue_addend += 1.0) {
double hue =
SanitizeDegreesDouble(start_hue + direction_of_rotation * hue_addend);
if (!IsBetween(hue, start_hue, end_hue)) {
continue;
}
Hct possible_answer = GetHctsByHue().at((int)round(hue));
double relative_temp =
(GetTempsByHct().at(possible_answer) - coldest_temp) / range;
double error = std::abs(complement_relative_temp - relative_temp);
if (error < smallest_error) {
smallest_error = error;
answer = possible_answer;
}
}
precomputed_complement_ = answer;
return precomputed_complement_.value();
}
std::vector<Hct> TemperatureCache::GetAnalogousColors() {
return GetAnalogousColors(5, 12);
}
std::vector<Hct> TemperatureCache::GetAnalogousColors(int count,
int divisions) {
// The starting hue is the hue of the input color.
int start_hue = (int)round(input_.get_hue());
Hct start_hct = GetHctsByHue().at(start_hue);
double last_temp = GetRelativeTemperature(start_hct);
std::vector<Hct> all_colors;
all_colors.push_back(start_hct);
double absolute_total_temp_delta = 0.0;
for (int i = 0; i < 360; i++) {
int hue = SanitizeDegreesInt(start_hue + i);
Hct hct = GetHctsByHue().at(hue);
double temp = GetRelativeTemperature(hct);
double temp_delta = std::abs(temp - last_temp);
last_temp = temp;
absolute_total_temp_delta += temp_delta;
}
int hue_addend = 1;
double temp_step = absolute_total_temp_delta / (double)divisions;
double total_temp_delta = 0.0;
last_temp = GetRelativeTemperature(start_hct);
while (all_colors.size() < static_cast<size_t>(divisions)) {
int hue = SanitizeDegreesInt(start_hue + hue_addend);
Hct hct = GetHctsByHue().at(hue);
double temp = GetRelativeTemperature(hct);
double temp_delta = std::abs(temp - last_temp);
total_temp_delta += temp_delta;
double desired_total_temp_delta_for_index = (all_colors.size() * temp_step);
bool index_satisfied =
total_temp_delta >= desired_total_temp_delta_for_index;
int index_addend = 1;
// Keep adding this hue to the answers until its temperature is
// insufficient. This ensures consistent behavior when there aren't
// `divisions` discrete steps between 0 and 360 in hue with `temp_step`
// delta in temperature between them.
//
// For example, white and black have no analogues: there are no other
// colors at T100/T0. Therefore, they should just be added to the array
// as answers.
while (index_satisfied &&
all_colors.size() < static_cast<size_t>(divisions)) {
all_colors.push_back(hct);
desired_total_temp_delta_for_index =
((all_colors.size() + index_addend) * temp_step);
index_satisfied = total_temp_delta >= desired_total_temp_delta_for_index;
index_addend++;
}
last_temp = temp;
hue_addend++;
if (hue_addend > 360) {
while (all_colors.size() < static_cast<size_t>(divisions)) {
all_colors.push_back(hct);
}
break;
}
}
std::vector<Hct> answers;
answers.push_back(input_);
int ccw_count = (int)floor(((double)count - 1.0) / 2.0);
for (int i = 1; i < (ccw_count + 1); i++) {
int index = 0 - i;
while (index < 0) {
index = all_colors.size() + index;
}
if (static_cast<size_t>(index) >= all_colors.size()) {
index = index % all_colors.size();
}
answers.insert(answers.begin(), all_colors.at(index));
}
int cw_count = count - ccw_count - 1;
for (int i = 1; i < (cw_count + 1); i++) {
size_t index = i;
while (index < 0) {
index = all_colors.size() + index;
}
if (index >= all_colors.size()) {
index = index % all_colors.size();
}
answers.push_back(all_colors.at(index));
}
return answers;
}
double TemperatureCache::GetRelativeTemperature(Hct hct) {
double range =
GetTempsByHct().at(GetWarmest()) - GetTempsByHct().at(GetColdest());
double difference_from_coldest =
GetTempsByHct().at(hct) - GetTempsByHct().at(GetColdest());
// Handle when there's no difference in temperature between warmest and
// coldest: for example, at T100, only one color is available, white.
if (range == 0.) {
return 0.5;
}
return difference_from_coldest / range;
}
double TemperatureCache::RawTemperature(Hct color) {
Lab lab = LabFromInt(color.ToInt());
double hue = SanitizeDegreesDouble(atan2(lab.b, lab.a) * 180.0 / kPi);
double chroma = hypot(lab.a, lab.b);
return -0.5 + 0.02 * pow(chroma, 1.07) *
cos(SanitizeDegreesDouble(hue - 50.) * kPi / 180);
}
Hct TemperatureCache::GetColdest() { return GetHctsByTemp().at(0); }
std::vector<Hct> TemperatureCache::GetHctsByHue() {
if (precomputed_hcts_by_hue_.has_value()) {
return precomputed_hcts_by_hue_.value();
}
std::vector<Hct> hcts;
for (double hue = 0.; hue <= 360.; hue += 1.) {
Hct color_at_hue(hue, input_.get_chroma(), input_.get_tone());
hcts.push_back(color_at_hue);
}
precomputed_hcts_by_hue_ = hcts;
return precomputed_hcts_by_hue_.value();
}
std::vector<Hct> TemperatureCache::GetHctsByTemp() {
if (precomputed_hcts_by_temp_.has_value()) {
return precomputed_hcts_by_temp_.value();
}
std::vector<Hct> hcts(GetHctsByHue());
hcts.push_back(input_);
std::map<Hct, double> temps_by_hct(GetTempsByHct());
sort(hcts.begin(), hcts.end(),
[temps_by_hct](const Hct a, const Hct b) -> bool {
return temps_by_hct.at(a) < temps_by_hct.at(b);
});
precomputed_hcts_by_temp_ = hcts;
return precomputed_hcts_by_temp_.value();
}
std::map<Hct, double> TemperatureCache::GetTempsByHct() {
if (precomputed_temps_by_hct_.has_value()) {
return precomputed_temps_by_hct_.value();
}
std::vector<Hct> all_hcts(GetHctsByHue());
all_hcts.push_back(input_);
std::map<Hct, double> temperatures_by_hct;
for (Hct hct : all_hcts) {
temperatures_by_hct[hct] = RawTemperature(hct);
}
precomputed_temps_by_hct_ = temperatures_by_hct;
return precomputed_temps_by_hct_.value();
}
Hct TemperatureCache::GetWarmest() {
return GetHctsByTemp().at(GetHctsByTemp().size() - 1);
}
bool TemperatureCache::IsBetween(double angle, double a, double b) {
if (a < b) {
return a <= angle && angle <= b;
}
return a <= angle || angle <= b;
}
} // namespace material_color_utilities
@@ -0,0 +1,140 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_TEMPERATURE_TEMPERATURE_CACHE_H_
#define CPP_TEMPERATURE_TEMPERATURE_CACHE_H_
#include <map>
#include <optional>
#include <vector>
#include "cpp/cam/hct.h"
namespace material_color_utilities {
/**
* Design utilities using color temperature theory.
*
* <p>Analogous colors, complementary color, and cache to efficiently, lazily,
* generate data for calculations when needed.
*/
class TemperatureCache {
public:
/**
* Create a cache that allows calculation of ex. complementary and analogous
* colors.
*
* @param input Color to find complement/analogous colors of. Any colors will
* have the same tone, and chroma as the input color, modulo any restrictions
* due to the other hues having lower limits on chroma.
*/
explicit TemperatureCache(Hct input);
/**
* A color that complements the input color aesthetically.
*
* <p>In art, this is usually described as being across the color wheel.
* History of this shows intent as a color that is just as cool-warm as the
* input color is warm-cool.
*/
Hct GetComplement();
/**
* 5 colors that pair well with the input color.
*
* <p>The colors are equidistant in temperature and adjacent in hue.
*/
std::vector<Hct> GetAnalogousColors();
/**
* A set of colors with differing hues, equidistant in temperature.
*
* <p>In art, this is usually described as a set of 5 colors on a color wheel
* divided into 12 sections. This method allows provision of either of those
* values.
*
* <p>Behavior is undefined when count or divisions is 0. When divisions <
* count, colors repeat.
*
* @param count The number of colors to return, includes the input color.
* @param divisions The number of divisions on the color wheel.
*/
std::vector<Hct> GetAnalogousColors(int count, int divisions);
/**
* Temperature relative to all colors with the same chroma and tone.
*
* @param hct HCT to find the relative temperature of.
* @return Value on a scale from 0 to 1.
*/
double GetRelativeTemperature(Hct hct);
/**
* Value representing cool-warm factor of a color. Values below 0 are
* considered cool, above, warm.
*
* <p>Color science has researched emotion and harmony, which art uses to
* select colors. Warm-cool is the foundation of analogous and complementary
* colors. See: - Li-Chen Ou's Chapter 19 in Handbook of Color Psychology
* (2015). - Josef Albers' Interaction of Color chapters 19 and 21.
*
* <p>Implementation of Ou, Woodcock and Wright's algorithm, which uses
* Lab/LCH color space. Return value has these properties:<br>
* - Values below 0 are cool, above 0 are warm.<br>
* - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma
* 130.<br>
* - Upper bound: 8.61. Chroma is infinite. Assuming max of Lab chroma 130.
*/
static double RawTemperature(Hct color);
private:
Hct input_;
std::optional<Hct> precomputed_complement_;
std::optional<std::vector<Hct>> precomputed_hcts_by_temp_;
std::optional<std::vector<Hct>> precomputed_hcts_by_hue_;
std::optional<std::map<Hct, double>> precomputed_temps_by_hct_;
/** Coldest color with same chroma and tone as input. */
Hct GetColdest();
/** Warmest color with same chroma and tone as input. */
Hct GetWarmest();
/** Determines if an angle is between two other angles, rotating clockwise. */
static bool IsBetween(double angle, double a, double b);
/**
* HCTs for all colors with the same chroma/tone as the input.
*
* <p>Sorted by hue, ex. index 0 is hue 0.
*/
std::vector<Hct> GetHctsByHue();
/**
* HCTs for all colors with the same chroma/tone as the input.
*
* <p>Sorted from coldest first to warmest last.
*/
std::vector<Hct> GetHctsByTemp();
/** Keys of HCTs in GetHctsByTemp, values of raw temperature. */
std::map<Hct, double> GetTempsByHct();
};
} // namespace material_color_utilities
#endif // CPP_TEMPERATURE_TEMPERATURE_CACHE_H_
+181
View File
@@ -0,0 +1,181 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "cpp/utils/utils.h"
#include <math.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <sstream>
#include <iomanip>
#include <string>
namespace material_color_utilities {
int RedFromInt(const Argb argb) { return (argb & 0x00ff0000) >> 16; }
int GreenFromInt(const Argb argb) { return (argb & 0x0000ff00) >> 8; }
int BlueFromInt(const Argb argb) { return (argb & 0x000000ff); }
Argb ArgbFromRgb(const int red, const int green, const int blue) {
return 0xFF000000 | ((red & 0xff) << 16) | ((green & 0xff) << 8) |
(blue & 0xff);
}
// Converts a color from linear RGB components to ARGB format.
Argb ArgbFromLinrgb(Vec3 linrgb) {
int r = Delinearized(linrgb.a);
int g = Delinearized(linrgb.b);
int b = Delinearized(linrgb.c);
return 0xFF000000 | ((r & 0x0ff) << 16) | ((g & 0x0ff) << 8) | (b & 0x0ff);
}
int Delinearized(const double rgb_component) {
double normalized = rgb_component / 100;
double delinearized;
if (normalized <= 0.0031308) {
delinearized = normalized * 12.92;
} else {
delinearized = 1.055 * std::pow(normalized, 1.0 / 2.4) - 0.055;
}
return std::clamp((int)round(delinearized * 255.0), 0, 255);
}
double Linearized(const int rgb_component) {
double normalized = rgb_component / 255.0;
if (normalized <= 0.040449936) {
return normalized / 12.92 * 100.0;
} else {
return std::pow((normalized + 0.055) / 1.055, 2.4) * 100.0;
}
}
int AlphaFromInt(Argb argb) { return (argb & 0xff000000) >> 24; }
bool IsOpaque(Argb argb) { return AlphaFromInt(argb) == 255; }
double LstarFromArgb(Argb argb) {
// xyz from argb
int red = (argb & 0x00ff0000) >> 16;
int green = (argb & 0x0000ff00) >> 8;
int blue = (argb & 0x000000ff);
double red_l = Linearized(red);
double green_l = Linearized(green);
double blue_l = Linearized(blue);
double y = 0.2126 * red_l + 0.7152 * green_l + 0.0722 * blue_l;
return LstarFromY(y);
}
double YFromLstar(double lstar) {
static const double ke = 8.0;
if (lstar > ke) {
double cube_root = (lstar + 16.0) / 116.0;
double cube = cube_root * cube_root * cube_root;
return cube * 100.0;
} else {
return lstar / (24389.0 / 27.0) * 100.0;
}
}
double LstarFromY(double y) {
static const double e = 216.0 / 24389.0;
double yNormalized = y / 100.0;
if (yNormalized <= e) {
return (24389.0 / 27.0) * yNormalized;
} else {
return 116.0 * std::pow(yNormalized, 1.0 / 3.0) - 16.0;
}
}
int SanitizeDegreesInt(const int degrees) {
if (degrees < 0) {
return (degrees % 360) + 360;
} else if (degrees >= 360.0) {
return degrees % 360;
} else {
return degrees;
}
}
// Sanitizes a degree measure as a floating-point number.
//
// Returns a degree measure between 0.0 (inclusive) and 360.0 (exclusive).
double SanitizeDegreesDouble(const double degrees) {
if (degrees < 0.0) {
return fmod(degrees, 360.0) + 360;
} else if (degrees >= 360.0) {
return fmod(degrees, 360.0);
} else {
return degrees;
}
}
double DiffDegrees(const double a, const double b) {
return 180.0 - abs(abs(a - b) - 180.0);
}
double RotationDirection(const double from, const double to) {
double increasing_difference = SanitizeDegreesDouble(to - from);
return increasing_difference <= 180.0 ? 1.0 : -1.0;
}
// Converts a color in ARGB format to a hexadecimal string in lowercase.
//
// For instance: hex_from_argb(0xff012345) == "ff012345"
std::string HexFromArgb(Argb argb) {
std::ostringstream os;
os << std::hex << argb;
return os.str();
}
Argb IntFromLstar(const double lstar) {
double y = YFromLstar(lstar);
int component = Delinearized(y);
return ArgbFromRgb(component, component, component);
}
// The signum function.
//
// Returns 1 if num > 0, -1 if num < 0, and 0 if num = 0
int Signum(double num) {
if (num < 0) {
return -1;
} else if (num == 0) {
return 0;
} else {
return 1;
}
}
double Lerp(double start, double stop, double amount) {
return (1.0 - amount) * start + amount * stop;
}
Vec3 MatrixMultiply(Vec3 input, const double matrix[3][3]) {
double a =
input.a * matrix[0][0] + input.b * matrix[0][1] + input.c * matrix[0][2];
double b =
input.a * matrix[1][0] + input.b * matrix[1][1] + input.c * matrix[1][2];
double c =
input.a * matrix[2][0] + input.b * matrix[2][1] + input.c * matrix[2][2];
return (Vec3){a, b, c};
}
} // namespace material_color_utilities
+211
View File
@@ -0,0 +1,211 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CPP_UTILS_UTILS_H_
#define CPP_UTILS_UTILS_H_
#include <cstdint>
#include <string>
namespace material_color_utilities {
using Argb = uint32_t;
/**
* A vector with three floating-point numbers as components.
*/
struct Vec3 {
double a = 0.0;
double b = 0.0;
double c = 0.0;
};
/**
* Value of pi.
*/
inline constexpr double kPi = 3.141592653589793;
/**
* Returns the standard white point; white on a sunny day.
*/
inline constexpr double kWhitePointD65[] = {95.047, 100.0, 108.883};
/**
* Returns the red component of a color in ARGB format.
*/
int RedFromInt(const Argb argb);
/**
* Returns the green component of a color in ARGB format.
*/
int GreenFromInt(const Argb argb);
/**
* Returns the blue component of a color in ARGB format.
*/
int BlueFromInt(const Argb argb);
/**
* Returns the alpha component of a color in ARGB format.
*/
int AlphaFromInt(const Argb argb);
/**
* Converts a color from RGB components to ARGB format.
*/
Argb ArgbFromRgb(const int red, const int green, const int blue);
/**
* Converts a color from linear RGB components to ARGB format.
*/
Argb ArgbFromLinrgb(Vec3 linrgb);
/**
* Returns whether a color in ARGB format is opaque.
*/
bool IsOpaque(const Argb argb);
/**
* Sanitizes a degree measure as an integer.
*
* @return a degree measure between 0 (inclusive) and 360 (exclusive).
*/
int SanitizeDegreesInt(const int degrees);
/**
* Sanitizes a degree measure as an floating-point number.
*
* @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive).
*/
double SanitizeDegreesDouble(const double degrees);
/**
* Distance of two points on a circle, represented using degrees.
*/
double DiffDegrees(const double a, const double b);
/**
* Sign of direction change needed to travel from one angle to
* another.
*
* For angles that are 180 degrees apart from each other, both
* directions have the same travel distance, so either direction is
* shortest. The value 1.0 is returned in this case.
*
* @param from The angle travel starts from, in degrees.
*
* @param to The angle travel ends at, in degrees.
*
* @return -1 if decreasing from leads to the shortest travel
* distance, 1 if increasing from leads to the shortest travel
* distance.
*/
double RotationDirection(const double from, const double to);
/**
* Computes the L* value of a color in ARGB representation.
*
* @param argb ARGB representation of a color
*
* @return L*, from L*a*b*, coordinate of the color
*/
double LstarFromArgb(const Argb argb);
/**
* Returns the hexadecimal representation of a color.
*/
std::string HexFromArgb(Argb argb);
/**
* Linearizes an RGB component.
*
* @param rgb_component 0 <= rgb_component <= 255, represents R/G/B
* channel
*
* @return 0.0 <= output <= 100.0, color channel converted to
* linear RGB space
*/
double Linearized(const int rgb_component);
/**
* Delinearizes an RGB component.
*
* @param rgb_component 0.0 <= rgb_component <= 100.0, represents linear
* R/G/B channel
*
* @return 0 <= output <= 255, color channel converted to regular
* RGB space
*/
int Delinearized(const double rgb_component);
/**
* Converts an L* value to a Y value.
*
* L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
*
* L* measures perceptual luminance, a linear scale. Y in XYZ
* measures relative luminance, a logarithmic scale.
*
* @param lstar L* in L*a*b*. 0.0 <= L* <= 100.0
*
* @return Y in XYZ. 0.0 <= Y <= 100.0
*/
double YFromLstar(const double lstar);
/**
* Converts a Y value to an L* value.
*
* L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
*
* L* measures perceptual luminance, a linear scale. Y in XYZ
* measures relative luminance, a logarithmic scale.
*
* @param y Y in XYZ. 0.0 <= Y <= 100.0
*
* @return L* in L*a*b*. 0.0 <= L* <= 100.0
*/
double LstarFromY(const double y);
/**
* Converts an L* value to an ARGB representation.
*
* @param lstar L* in L*a*b*. 0.0 <= L* <= 100.0
*
* @return ARGB representation of grayscale color with lightness matching L*
*/
Argb IntFromLstar(const double lstar);
/**
* The signum function.
*
* @return 1 if num > 0, -1 if num < 0, and 0 if num = 0
*/
int Signum(double num);
/**
* The linear interpolation function.
*
* @return start if amount = 0 and stop if amount = 1
*/
double Lerp(double start, double stop, double amount);
/**
* Multiplies a 1x3 row vector with a 3x3 matrix, returning the product.
*/
Vec3 MatrixMultiply(Vec3 input, const double matrix[3][3]);
} // namespace material_color_utilities
#endif // CPP_UTILS_UTILS_H_
+10679
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""
Analyze Noctalia's C++ palette generator against Python + matugen references.
Usage:
./tools/palette-generator-analysis.py <wallpaper>
./tools/palette-generator-analysis.py <wallpaper> --fail-threshold 20
Three backends:
- Noctalia : ../build-debug/noctalia theme <img> --scheme <s> --dark
- Python : the upstream reference in noctalia-shell/Scripts/python
- Matugen : Rust reference (M3 schemes only)
Exit code: 0 if all diffs are under --fail-threshold (default: unlimited),
1 on failure or threshold exceeded.
"""
import argparse
import json
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent.resolve()
REPO_DIR = SCRIPT_DIR.parent
NOCTALIA_BIN = REPO_DIR / "build-debug" / "noctalia"
# Absolute path into the upstream Python reference.
PYTHON_THEMING_DIR = Path.home() / "Development/misc/noctalia/noctalia-shell/Scripts/python/src/theming"
PYTHON_PROCESSOR = PYTHON_THEMING_DIR / "template-processor.py"
# Pull in Python's Hct for hue/chroma classification.
sys.path.insert(0, str(PYTHON_THEMING_DIR))
try:
from lib.color import Color # noqa: E402
from lib.hct import Hct # noqa: E402
except ImportError:
Color = None
Hct = None
M3_SCHEMES = ["m3-tonal-spot", "m3-fruit-salad", "m3-rainbow", "m3-content", "m3-monochrome"]
CUSTOM_SCHEMES = ["vibrant", "faithful", "dysfunctional", "muted"]
def python_scheme_name(scheme: str) -> str:
"""Translate our m3-* names to what the upstream Python processor expects."""
return scheme[3:] if scheme.startswith("m3-") else scheme
def matugen_scheme_name(scheme: str) -> str:
return scheme[3:] if scheme.startswith("m3-") else scheme
KEY_TOKENS = ["primary", "secondary", "tertiary", "surface", "on_surface",
"primary_container", "surface_container", "outline"]
def hex_to_rgb(h: str) -> tuple[int, int, int]:
h = h.lstrip('#')
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
def rgb_distance(a: str, b: str) -> float:
r1, g1, b1 = hex_to_rgb(a)
r2, g2, b2 = hex_to_rgb(b)
return ((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2) ** 0.5
def max_lsb(a: str, b: str) -> int:
r1, g1, b1 = hex_to_rgb(a)
r2, g2, b2 = hex_to_rgb(b)
return max(abs(r1 - r2), abs(g1 - g2), abs(b1 - b2))
def hue_diff(h1: float, h2: float) -> float:
d = abs(h1 - h2)
return min(d, 360.0 - d)
def get_hct(hex_color: str):
if Color is None:
return None
return Color.from_hex(hex_color).to_hct()
def run_python(image: Path, scheme: str) -> dict | None:
try:
out = subprocess.run(
[sys.executable, str(PYTHON_PROCESSOR), str(image),
"--scheme-type", python_scheme_name(scheme), "--dark"],
capture_output=True, text=True, check=True,
)
data = json.loads(out.stdout)
return data.get("dark", data)
except Exception as e:
print(f" python {scheme}: {e}", file=sys.stderr)
return None
def run_matugen(image: Path, scheme: str) -> dict | None:
try:
out = subprocess.run(
["matugen", "image", str(image), "--json", "hex",
"--dry-run", "-m", "dark",
"--source-color-index", "0",
"--old-json-output",
"-t", f"scheme-{matugen_scheme_name(scheme)}"],
capture_output=True, text=True, check=True,
)
data = json.loads(out.stdout)
colors = data.get("colors", {})
# Each entry is {"dark": "#hex", "default": "#hex", "light": "#hex"}.
return {k: v.get("dark", v) for k, v in colors.items() if isinstance(v, dict)}
except Exception as e:
print(f" matugen {scheme}: {e}", file=sys.stderr)
return None
def run_noctalia(image: Path, scheme: str) -> dict | None:
try:
out = subprocess.run(
[str(NOCTALIA_BIN), "theme", str(image),
"--scheme", scheme, "--dark"],
capture_output=True, text=True, check=True,
)
return json.loads(out.stdout)
except Exception as e:
print(f" noctalia {scheme}: {e}", file=sys.stderr)
return None
def quality_bucket(our: str, ref: str) -> tuple[str, str]:
"""Return (metric_str, bucket) using hue for high-chroma, RGB for low."""
try:
h1 = get_hct(our)
h2 = get_hct(ref)
avg = (h1.chroma + h2.chroma) / 2
except Exception:
avg = 0
if avg < 15:
d = rgb_distance(our, ref)
bucket = "excellent" if d < 10 else "good" if d < 25 else "fair" if d < 50 else "poor"
return f"{d:5.1f} rgb", bucket
else:
d = hue_diff(h1.hue, h2.hue)
bucket = "excellent" if d < 5 else "good" if d < 15 else "fair" if d < 30 else "poor"
return f"{d:5.1f} hue", bucket
def compare_m3(image: Path, scheme: str, has_matugen: bool) -> list[int]:
"""Return list of LSB diffs vs the best available reference."""
print(f"\n─── {scheme} ───")
py = run_python(image, scheme)
noct = run_noctalia(image, scheme)
mat = run_matugen(image, scheme) if has_matugen else None
if not py or not noct:
print(" ! missing reference output, skipping")
return []
hdr = f" {'Token':<26} {'Python':<10} {'Matugen':<10} {'Noctalia':<10} {'Δ Py↔Noct':<14} {'Δ Mat↔Noct':<14}"
print(hdr)
print(" " + "" * (len(hdr) - 2))
diffs: list[int] = []
for token in KEY_TOKENS:
pv = py.get(token, "")
nv = noct.get(token, "")
mv = (mat or {}).get(token, "")
if not (pv and nv):
continue
py_noct_metric, py_noct_bucket = quality_bucket(nv, pv)
if mv:
mat_noct_metric, mat_noct_bucket = quality_bucket(nv, mv)
mat_col = f"{mat_noct_metric} {mat_noct_bucket[:4]}"
else:
mat_col = "-"
lsb = max_lsb(pv, nv)
diffs.append(lsb)
print(f" {token:<26} {pv:<10} {mv or '-':<10} {nv:<10} "
f"{py_noct_metric} {py_noct_bucket[:4]:<5} {mat_col:<14} "
f"{lsb})")
return diffs
def compare_custom(image: Path, scheme: str) -> list[int]:
print(f"\n─── {scheme} ───")
py = run_python(image, scheme)
noct = run_noctalia(image, scheme)
if not py or not noct:
print(" ! missing reference output, skipping")
return []
hdr = f" {'Token':<26} {'Python':<10} {'Noctalia':<10} {'Δ Py↔Noct':<14}"
print(hdr)
print(" " + "" * (len(hdr) - 2))
diffs: list[int] = []
for token in KEY_TOKENS:
pv = py.get(token, "")
nv = noct.get(token, "")
if not (pv and nv):
continue
metric, bucket = quality_bucket(nv, pv)
lsb = max_lsb(pv, nv)
diffs.append(lsb)
print(f" {token:<26} {pv:<10} {nv:<10} "
f"{metric} {bucket[:4]:<5}{lsb})")
return diffs
def main() -> int:
parser = argparse.ArgumentParser(
description="Compare Noctalia C++ palette generator vs Python/matugen"
)
parser.add_argument("wallpaper", type=Path)
parser.add_argument("--fail-threshold", type=int, default=-1,
help="Exit 1 if any token's max-channel LSB diff exceeds N")
parser.add_argument("--no-matugen", action="store_true")
parser.add_argument("--schemes", nargs="+",
help="Limit to a subset of schemes")
args = parser.parse_args()
if not args.wallpaper.exists():
print(f"error: not found: {args.wallpaper}", file=sys.stderr)
return 1
if not NOCTALIA_BIN.exists():
print(f"error: noctalia not built: {NOCTALIA_BIN}", file=sys.stderr)
return 1
has_matugen = False
if not args.no_matugen:
try:
subprocess.run(["matugen", "--version"], capture_output=True, check=True)
has_matugen = True
except Exception:
print("note: matugen not available, skipping its column")
print(f"\nWallpaper: {args.wallpaper.name}")
print("=" * 78)
print("M3 SCHEMES (Python is a MCU port; Matugen is the Rust reference)")
print("=" * 78)
all_diffs: list[tuple[str, int]] = []
m3 = M3_SCHEMES if not args.schemes else [s for s in M3_SCHEMES if s in args.schemes]
for s in m3:
for d in compare_m3(args.wallpaper, s, has_matugen):
all_diffs.append((s, d))
print()
print("=" * 78)
print("CUSTOM SCHEMES (Python is the only reference)")
print("=" * 78)
custom = CUSTOM_SCHEMES if not args.schemes else [s for s in CUSTOM_SCHEMES if s in args.schemes]
for s in custom:
for d in compare_custom(args.wallpaper, s):
all_diffs.append((s, d))
print()
print("" * 78)
print("Summary")
per_scheme: dict[str, list[int]] = {}
for s, d in all_diffs:
per_scheme.setdefault(s, []).append(d)
for s in list(per_scheme):
ds = per_scheme[s]
if ds:
print(f" {s:<14} max Δ={max(ds):>3} mean Δ={sum(ds)/len(ds):>5.1f} ({len(ds)} tokens)")
if args.fail_threshold >= 0:
worst = max((d for _, d in all_diffs), default=0)
if worst > args.fail_threshold:
print(f"\nFAIL: worst Δ={worst} > threshold {args.fail_threshold}")
return 1
print(f"\nOK: worst Δ={worst} ≤ threshold {args.fail_threshold}")
return 0
if __name__ == "__main__":
sys.exit(main())