From cc0a4ba756a651951cd25680b8b6a7ecc2877f65 Mon Sep 17 00:00:00 2001 From: Jake Stewart Date: Thu, 19 Feb 2026 19:27:51 +0800 Subject: [PATCH] generate 256 palette --- config.c | 97 +++++++++++++++++++++++++++++++++++++++++++++- config.h | 2 + doc/foot.ini.5.scd | 11 ++++++ foot.ini | 1 + lab.c | 79 +++++++++++++++++++++++++++++++++++++ lab.h | 13 +++++++ meson.build | 1 + 7 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 lab.c create mode 100644 lab.h diff --git a/config.c b/config.c index 14e836c1..6e4b0901 100644 --- a/config.c +++ b/config.c @@ -28,6 +28,7 @@ #include "tokenize.h" #include "util.h" #include "xmalloc.h" +#include "lab.h" #include "xsnprintf.h" static const uint32_t default_foreground = 0xffffff; @@ -1156,6 +1157,9 @@ parse_section_main(struct context *ctx) return true; } + else if (streq(key, "generate-256-palette")) + return value_to_bool(ctx, &conf->generate_256_palette); + else if (streq(key, "uppercase-regex-insert")) return value_to_bool(ctx, &conf->uppercase_regex_insert); @@ -1410,6 +1414,7 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) size_t key_len = strlen(key); uint8_t last_digit = (unsigned char)key[key_len - 1] - '0'; uint32_t *color = NULL; + int table_index = -1; if (isdigit(key[0])) { unsigned long index; @@ -1420,13 +1425,18 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) return false; } color = &theme->table[index]; + table_index = (int)index; } - else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8) + else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8) { color = &theme->table[last_digit]; + table_index = last_digit; + } - else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8) + else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8) { color = &theme->table[8 + last_digit]; + table_index = 8 + last_digit; + } else if (key_len == 4 && str_has_prefix(key, "dim") && last_digit < 8) { if (!value_to_color(ctx, &theme->dim[last_digit], false)) @@ -1586,6 +1596,10 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) return false; *color = color_value; + + if (table_index >= 0) + theme->table_mask[table_index / 32] |= 1u << (table_index % 32); + return true; } @@ -3432,6 +3446,75 @@ config_font_list_clone(struct config_font_list *dst, } } +static bool +table_mask_is_set(const uint32_t *mask, unsigned idx) +{ + return (mask[idx / 32] >> (idx % 32)) & 1; +} + +static void +generate_256_palette(uint32_t *table, const uint32_t *mask, + uint32_t bg, uint32_t fg) +{ + /* + * Generate colors 16-255 by interpolating in CIELAB space. + * + * Corner mapping for trilinear interpolation: + * bg -> (r=0, g=0, b=0) + * palette[1] -> (r=1, g=0, b=0) red + * palette[2] -> (r=0, g=1, b=0) green + * palette[3] -> (r=1, g=1, b=0) yellow + * palette[4] -> (r=0, g=0, b=1) blue + * palette[5] -> (r=1, g=0, b=1) magenta + * palette[6] -> (r=0, g=1, b=1) cyan + * fg -> (r=1, g=1, b=1) + */ + struct lab base8_lab[8]; + for (int i = 0; i < 8; i++) + base8_lab[i] = lab_from_rgb(table[i]); + + struct lab bg_lab = lab_from_rgb(bg); + struct lab fg_lab = lab_from_rgb(fg); + + /* Colors 16-231: 6x6x6 color cube via trilinear interpolation */ + unsigned idx = 16; + for (unsigned ri = 0; ri < 6; ri++) { + float tr = (float)ri / 5.0f; + + /* Interpolate along R axis (4 edges) */ + struct lab c0 = lab_lerp(tr, bg_lab, base8_lab[1]); + struct lab c1 = lab_lerp(tr, base8_lab[2], base8_lab[3]); + struct lab c2 = lab_lerp(tr, base8_lab[4], base8_lab[5]); + struct lab c3 = lab_lerp(tr, base8_lab[6], fg_lab); + + for (unsigned gi = 0; gi < 6; gi++) { + float tg = (float)gi / 5.0f; + + /* Interpolate along G axis (2 edges) */ + struct lab c4 = lab_lerp(tg, c0, c1); + struct lab c5 = lab_lerp(tg, c2, c3); + + for (unsigned bi = 0; bi < 6; bi++) { + if (!table_mask_is_set(mask, idx)) { + /* Interpolate along B axis */ + struct lab c6 = lab_lerp((float)bi / 5.0f, c4, c5); + table[idx] = lab_to_rgb(c6); + } + idx++; + } + } + } + + /* Colors 232-255: grayscale ramp from bg to fg */ + for (unsigned i = 0; i < 24; i++) { + if (!table_mask_is_set(mask, idx)) { + float t = (float)(i + 1) / 25.0f; + table[idx] = lab_to_rgb(lab_lerp(t, bg_lab, fg_lab)); + } + idx++; + } +} + bool config_load(struct config *conf, const char *conf_path, user_notifications_t *initial_user_notifications, @@ -3528,6 +3611,7 @@ config_load(struct config *conf, const char *conf_path, }, }, .initial_color_theme = COLOR_THEME_DARK, + .generate_256_palette = true, .cursor = { .style = CURSOR_BLOCK, .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, @@ -3715,6 +3799,15 @@ config_load(struct config *conf, const char *conf_path, if (!config_override_apply(conf, overrides, errors_are_fatal)) ret = !errors_are_fatal; + if (conf->generate_256_palette) { + generate_256_palette( + conf->colors_dark.table, conf->colors_dark.table_mask, + conf->colors_dark.bg, conf->colors_dark.fg); + generate_256_palette( + conf->colors_light.table, conf->colors_light.table_mask, + conf->colors_light.bg, conf->colors_light.fg); + } + if (ret && conf->fonts[0].count == 0) { struct config_font font; if (!config_font_parse("monospace", &font)) { diff --git a/config.h b/config.h index 9ca47753..e8b244e1 100644 --- a/config.h +++ b/config.h @@ -137,6 +137,7 @@ struct color_theme { uint32_t flash; uint32_t flash_alpha; uint32_t table[256]; + uint32_t table_mask[8]; /* 256-bit mask tracking user-set palette entries */ uint16_t alpha; uint32_t selection_fg; uint32_t selection_bg; @@ -332,6 +333,7 @@ struct config { struct color_theme colors_dark; struct color_theme colors_light; enum which_color_theme initial_color_theme; + bool generate_256_palette; struct { enum cursor_style style; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8bff9629..df50f280 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -383,6 +383,17 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _1_ +*generate-256-palette* + Boolean. When enabled, the extended 256-color palette (colors + 16-255) is generated by interpolating from the base 16 colors, + foreground, and background using the CIELAB color space. This + makes the 256-color palette harmonize with custom color schemes + instead of using the fixed standard values. + + Palette entries explicitly set by the user are not overwritten. + + Default: _yes_ + *initial-window-size-pixels* Initial window width and height in _pixels_ (subject to output scaling), in the form _WIDTHxHEIGHT_. The height _includes_ the diff --git a/foot.ini b/foot.ini index a9b4b83d..a158c248 100644 --- a/foot.ini +++ b/foot.ini @@ -25,6 +25,7 @@ # gamma-correct-blending=no # initial-color-theme=dark +# generate-256-palette=yes # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars= # initial-window-mode=windowed diff --git a/lab.c b/lab.c new file mode 100644 index 00000000..107e6a9b --- /dev/null +++ b/lab.c @@ -0,0 +1,79 @@ +#include "lab.h" + +#include + +struct lab +lab_from_rgb(uint32_t rgb) +{ + /* Extract and normalize sRGB components */ + float r = ((rgb >> 16) & 0xff) / 255.0f; + float g = ((rgb >> 8) & 0xff) / 255.0f; + float b = (rgb & 0xff) / 255.0f; + + /* Linearize sRGB (inverse gamma) */ + r = r > 0.04045f ? powf((r + 0.055f) / 1.055f, 2.4f) : r / 12.92f; + g = g > 0.04045f ? powf((g + 0.055f) / 1.055f, 2.4f) : g / 12.92f; + b = b > 0.04045f ? powf((b + 0.055f) / 1.055f, 2.4f) : b / 12.92f; + + /* Linear RGB to XYZ (sRGB matrix, D65 illuminant) */ + float x = (r * 0.4124564f + g * 0.3575761f + b * 0.1804375f) / 0.95047f; + float y = r * 0.2126729f + g * 0.7151522f + b * 0.0721750f; + float z = (r * 0.0193339f + g * 0.1191920f + b * 0.9503041f) / 1.08883f; + + /* XYZ to LAB */ + x = x > 0.008856f ? cbrtf(x) : 7.787f * x + 16.0f / 116.0f; + y = y > 0.008856f ? cbrtf(y) : 7.787f * y + 16.0f / 116.0f; + z = z > 0.008856f ? cbrtf(z) : 7.787f * z + 16.0f / 116.0f; + + return (struct lab){ + .l = 116.0f * y - 16.0f, + .a = 500.0f * (x - y), + .b = 200.0f * (y - z), + }; +} + +uint32_t +lab_to_rgb(struct lab c) +{ + /* LAB to XYZ */ + float y = (c.l + 16.0f) / 116.0f; + float x = c.a / 500.0f + y; + float z = y - c.b / 200.0f; + + float x3 = x * x * x; + float y3 = y * y * y; + float z3 = z * z * z; + + float xf = (x3 > 0.008856f ? x3 : (x - 16.0f / 116.0f) / 7.787f) * 0.95047f; + float yf = y3 > 0.008856f ? y3 : (y - 16.0f / 116.0f) / 7.787f; + float zf = (z3 > 0.008856f ? z3 : (z - 16.0f / 116.0f) / 7.787f) * 1.08883f; + + /* XYZ to linear RGB (inverse sRGB matrix) */ + float r = xf * 3.2404542f - yf * 1.5371385f - zf * 0.4985314f; + float g = xf * -0.9692660f + yf * 1.8760108f + zf * 0.0415560f; + float b = xf * 0.0556434f - yf * 0.2040259f + zf * 1.0572252f; + + /* Apply sRGB gamma */ + r = r > 0.0031308f ? 1.055f * powf(r, 1.0f / 2.4f) - 0.055f : 12.92f * r; + g = g > 0.0031308f ? 1.055f * powf(g, 1.0f / 2.4f) - 0.055f : 12.92f * g; + b = b > 0.0031308f ? 1.055f * powf(b, 1.0f / 2.4f) - 0.055f : 12.92f * b; + + /* Clamp and quantize */ + if (r < 0.0f) r = 0.0f; else if (r > 1.0f) r = 1.0f; + if (g < 0.0f) g = 0.0f; else if (g > 1.0f) g = 1.0f; + if (b < 0.0f) b = 0.0f; else if (b > 1.0f) b = 1.0f; + + return (uint32_t)((uint8_t)(r * 255.0f + 0.5f) << 16 | + (uint8_t)(g * 255.0f + 0.5f) << 8 | + (uint8_t)(b * 255.0f + 0.5f)); +} + +struct lab +lab_lerp(float t, struct lab a, struct lab b) +{ + return (struct lab){ + .l = a.l + t * (b.l - a.l), + .a = a.a + t * (b.a - a.a), + .b = a.b + t * (b.b - a.b), + }; +} diff --git a/lab.h b/lab.h new file mode 100644 index 00000000..bd69e3ac --- /dev/null +++ b/lab.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +struct lab { + float l; + float a; + float b; +}; + +struct lab lab_from_rgb(uint32_t rgb); +uint32_t lab_to_rgb(struct lab c); +struct lab lab_lerp(float t, struct lab a, struct lab b); diff --git a/meson.build b/meson.build index aa8342ab..c2c564c4 100644 --- a/meson.build +++ b/meson.build @@ -250,6 +250,7 @@ common = static_library( misc = static_library( 'misc', 'hsl.c', 'hsl.h', + 'lab.c', 'lab.h', 'macros.h', 'misc.c', 'misc.h', 'uri.c', 'uri.h',