summaryrefslogtreecommitdiff
path: root/src/colors.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/colors.zig')
-rw-r--r--src/colors.zig121
1 files changed, 110 insertions, 11 deletions
diff --git a/src/colors.zig b/src/colors.zig
index 72e25aed..b6da0d2c 100644
--- a/src/colors.zig
+++ b/src/colors.zig
@@ -153,18 +153,22 @@ fn toOklab(rgb: [3]u8) [3]f32 {
};
}
-fn fromOklab(lab: [3]f32) [3]u8 {
+fn oklabLinear(lab: [3]f32) [3]f32 {
const lm = lab[0] + 0.3963377774 * lab[1] + 0.2158037573 * lab[2];
const mm = lab[0] - 0.1055613458 * lab[1] - 0.0638541728 * lab[2];
const sm = lab[0] - 0.0894841775 * lab[1] - 1.2914855480 * lab[2];
const l = lm * lm * lm;
const m = mm * mm * mm;
const s = sm * sm * sm;
- const linear: [3]f32 = .{
+ return .{
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
};
+}
+
+fn fromOklab(lab: [3]f32) [3]u8 {
+ const linear = oklabLinear(lab);
var out: [3]u8 = undefined;
for (&out, linear) |*o, v| {
const x = std.math.clamp(v, 0, 1);
@@ -181,6 +185,72 @@ pub fn mixOklab(a: [3]u8, b: [3]u8, t: f32) [3]u8 {
return fromOklab(.{ x[0] + (y[0] - x[0]) * t, x[1] + (y[1] - x[1]) * t, x[2] + (y[2] - x[2]) * t });
}
+fn oklabDistance(a: [3]f32, b: [3]f32) f32 {
+ return @sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]) + (a[2] - b[2]) * (a[2] - b[2]));
+}
+
+/// How far a name's tint stands off its tag's ink in OKLab: a hue you see
+/// at a glance, not a shade you look for.
+pub const name_tint_distance: f32 = 0.07;
+/// How much hue a colour needs to lend a name its tint.
+const name_tint_chroma: f32 = 0.06;
+
+/// A name's tint for a theme that sets none (the faithful ports, the
+/// imports, a theme file): a hue of the theme's own -- its keyword colour,
+/// else the first of its ANSI accents, syntax, grip, selection or
+/// diagnostic colours that has one -- at the lightness nearest the tag's
+/// ink where that hue holds, the name reads on its ground (nameTintFloor)
+/// and it stands off the ink. Null when no colour of the theme will do.
+fn derivedNameTint(th: *const Theme, ink: [3]u8, ground: [3]u8) ?[3]u8 {
+ const pal = th.palette orelse [_][3]u8{th.kw} ** 16;
+ const sources = [_][3]u8{ th.kw, pal[4], pal[5], pal[6], pal[3], pal[2], pal[1], th.num, th.str, th.box } ++
+ [_][3]u8{ th.column_box orelse th.num, th.box_dirty orelse th.num, th.sel_bg, th.search_bg orelse th.num } ++
+ [_][3]u8{ th.diagnostic_info orelse th.num, th.diagnostic_hint orelse th.num, th.diagnostic_warning orelse th.num } ++
+ // A theme with no hue of its own at all (flatwhite): acme's DMedblue's.
+ [_][3]u8{.{ 0x00, 0x00, 0x99 }};
+ // Near the ink's lightness first, from any of the theme's hues; only
+ // then further off it.
+ for ([2]f32{ 0.15, 1 }) |reach| for (sources) |source| {
+ if (nameTintFrom(toOklab(source), ink, ground, reach)) |tint| return tint;
+ };
+ return null;
+}
+
+fn nameTintFrom(source: [3]f32, ink: [3]u8, ground: [3]u8, reach: f32) ?[3]u8 {
+ const chroma = @sqrt(source[1] * source[1] + source[2] * source[2]);
+ if (chroma < name_tint_chroma) return null;
+ const ink_lab = toOklab(ink);
+ const floor = nameTintFloor(ink, ground);
+ var step: f32 = 0;
+ while (step <= reach) : (step += 0.01) {
+ for ([2]f32{ 1, -1 }) |sign| {
+ const lightness = ink_lab[0] + sign * step;
+ if (lightness < 0 or lightness > 1) continue;
+ // As much of the hue as that lightness holds, up to a tint's.
+ var want = @min(chroma, 0.12);
+ while (want >= 0.04) : (want -= 0.01) {
+ const lab: [3]f32 = .{ lightness, source[1] / chroma * want, source[2] / chroma * want };
+ const in_gamut = for (oklabLinear(lab)) |v| {
+ if (v < -0.001 or v > 1.001) break false;
+ } else true;
+ if (!in_gamut) continue;
+ const tint = fromOklab(lab);
+ if (contrast(tint, ground) < floor) continue;
+ if (oklabDistance(toOklab(tint), ink_lab) < name_tint_distance) continue;
+ return tint;
+ }
+ }
+ }
+ return null;
+}
+
+/// What a name's tint must read at on its ground: 4.5:1, or within half a
+/// point of the ink's own where the ink has less to spare (seoulbones_dark's
+/// near-white on grey leaves no hue at the ink's 4.53:1).
+pub fn nameTintFloor(ink: [3]u8, ground: [3]u8) f32 {
+ return @min(4.5, contrast(ink, ground) - 0.5);
+}
+
test "an OKLab fade holds its ends exactly and keeps a mid-way bright" {
for ([_][3]u8{ .{ 0, 0, 0 }, .{ 255, 255, 255 }, .{ 0x0c, 0x0c, 0x0e }, .{ 0xe8, 0xc4, 0x6a }, .{ 0x1c, 0x51, 0x72 } }) |rgb| {
try std.testing.expectEqual(rgb, fromOklab(toOklab(rgb)));
@@ -548,13 +618,21 @@ pub const ChromeTheme = struct {
pub fn fromTheme(th: *const Theme) ChromeTheme {
// The initial chrome is resolved at compile time: contrast takes pow.
@setEvalBranchQuota(200_000);
+ const active_bg = focusTint(th);
+ const active_fg = th.tag_active_fg orelse th.tag_fg;
+ // A name is always tinted: a theme that names no tint gets one of
+ // its own hues, on each ground. One that names only the plain tint
+ // keeps it when focused too.
+ const name_fg = th.tag_name_fg orelse (derivedNameTint(th, th.tag_fg, th.tag_bg) orelse th.tag_fg);
+ const active_name_fg = th.tag_active_name_fg orelse
+ (th.tag_name_fg orelse (derivedNameTint(th, active_fg, active_bg) orelse active_fg));
return .{
.tag_bg = th.tag_bg,
.tag_fg = th.tag_fg,
- .tag_active_bg = focusTint(th),
- .tag_active_fg = th.tag_active_fg orelse th.tag_fg,
- .tag_name_fg = th.tag_name_fg orelse th.tag_fg,
- .tag_active_name_fg = th.tag_active_name_fg orelse (th.tag_name_fg orelse (th.tag_active_fg orelse th.tag_fg)),
+ .tag_active_bg = active_bg,
+ .tag_active_fg = active_fg,
+ .tag_name_fg = name_fg,
+ .tag_active_name_fg = active_name_fg,
.border = separatorOf(th),
.empty_col = th.empty_col orelse separatorOf(th),
.search_bg = th.search_bg orelse th.sel_bg,
@@ -1108,8 +1186,9 @@ test "legacy ThemeFile documents inherit Pardes UI roles without new fields" {
try std.testing.expectEqual(@as(?[3]u8, null), parsed.search_bg);
const chrome = ChromeTheme.fromTheme(&parsed);
try std.testing.expectEqual(parsed.tag_bg, chrome.tag_active_bg);
- try std.testing.expectEqual(parsed.tag_fg, chrome.tag_name_fg);
- try std.testing.expectEqual(chrome.tag_active_fg, chrome.tag_active_name_fg);
+ // Its names take a tint of its own hues, never the plain ink.
+ try std.testing.expect(!std.meta.eql(parsed.tag_fg, chrome.tag_name_fg));
+ try std.testing.expect(!std.meta.eql(chrome.tag_active_fg, chrome.tag_active_name_fg));
try std.testing.expectEqual(parsed.num, chrome.column_box);
try std.testing.expectEqual(mix(parsed.num, parsed.tag_bg), chrome.column_box_dim);
try std.testing.expectEqual(parsed.sel_bg, chrome.search_bg);
@@ -1148,8 +1227,9 @@ test "custom filename tints preserve optional active fallback precedence" {
theme.tag_name_fg = null;
theme.tag_active_name_fg = null;
var chrome = ChromeTheme.fromTheme(&theme);
- try std.testing.expectEqual(theme.tag_fg, chrome.tag_name_fg);
- try std.testing.expectEqual(theme.tag_active_fg.?, chrome.tag_active_name_fg);
+ // Neither named: each derived, on its own ground.
+ try std.testing.expectEqual(derivedNameTint(&theme, theme.tag_fg, theme.tag_bg).?, chrome.tag_name_fg);
+ try std.testing.expectEqual(derivedNameTint(&theme, theme.tag_active_fg.?, chrome.tag_active_bg).?, chrome.tag_active_name_fg);
theme.tag_name_fg = .{ 120, 130, 140 };
chrome = ChromeTheme.fromTheme(&theme);
try std.testing.expectEqual(theme.tag_name_fg.?, chrome.tag_name_fg);
@@ -1160,6 +1240,25 @@ test "custom filename tints preserve optional active fallback precedence" {
try std.testing.expectEqual(theme.tag_active_name_fg.?, chrome.tag_active_name_fg);
theme.tag_name_fg = null;
chrome = ChromeTheme.fromTheme(&theme);
- try std.testing.expectEqual(theme.tag_fg, chrome.tag_name_fg);
+ try std.testing.expectEqual(derivedNameTint(&theme, theme.tag_fg, theme.tag_bg).?, chrome.tag_name_fg);
try std.testing.expectEqual(theme.tag_active_name_fg.?, chrome.tag_active_name_fg);
}
+
+test "every theme tints its names off the tag's ink, and they read on both grounds" {
+ for (&themes) |*th| {
+ const chrome = ChromeTheme.fromTheme(th);
+ const pairs = [2][3][3]u8{
+ .{ chrome.tag_fg, chrome.tag_name_fg, chrome.tag_bg },
+ .{ chrome.tag_active_fg, chrome.tag_active_name_fg, chrome.tag_active_bg },
+ };
+ for (pairs) |pair| {
+ const ink, const name, const ground = pair;
+ const apart = oklabDistance(toOklab(ink), toOklab(name));
+ const floor = nameTintFloor(ink, ground);
+ if (apart < name_tint_distance or contrast(name, ground) < floor - 0.01)
+ std.debug.print("{s}: name {x} on {x} is {d:.3} off the ink {x}, {d:.2}:1\n", .{ th.name, name, ground, apart, ink, contrast(name, ground) });
+ try std.testing.expect(apart >= name_tint_distance);
+ try std.testing.expect(contrast(name, ground) >= floor - 0.01);
+ }
+ }
+}