mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(theme): palette generator, 1st pass. m3 is accurate, custom schemes still need some love.
This commit is contained in:
@@ -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
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
@@ -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_
|
||||
+1153
File diff suppressed because it is too large
Load Diff
+84
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
Vendored
+10679
File diff suppressed because it is too large
Load Diff
+529
-5949
File diff suppressed because it is too large
Load Diff
Executable
+285
@@ -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())
|
||||
Reference in New Issue
Block a user