summaryrefslogtreecommitdiff
path: root/src/colors.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/colors.zig')
-rw-r--r--src/colors.zig65
1 files changed, 64 insertions, 1 deletions
diff --git a/src/colors.zig b/src/colors.zig
index b44e84f9..8319a112 100644
--- a/src/colors.zig
+++ b/src/colors.zig
@@ -117,6 +117,65 @@ test "a derived separator or tint keeps the theme's own when it already stands o
try std.testing.expect(pushed >= own and own >= plain);
}
+/// sRGB bytes to OKLab and back (Björn Ottosson's), and a mix in it.
+fn toOklab(rgb: [3]u8) [3]f32 {
+ var l: [3]f32 = undefined;
+ for (&l, rgb) |*out, v| {
+ const x = @as(f32, @floatFromInt(v)) / 255;
+ out.* = if (x <= 0.04045) x / 12.92 else std.math.pow(f32, (x + 0.055) / 1.055, 2.4);
+ }
+ const lm = std.math.cbrt(0.4122214708 * l[0] + 0.5363325363 * l[1] + 0.0514459929 * l[2]);
+ const mm = std.math.cbrt(0.2119034982 * l[0] + 0.6806995451 * l[1] + 0.1073969566 * l[2]);
+ const sm = std.math.cbrt(0.0883024619 * l[0] + 0.2817188376 * l[1] + 0.6299787005 * l[2]);
+ return .{
+ 0.2104542553 * lm + 0.7936177850 * mm - 0.0040720468 * sm,
+ 1.9779984951 * lm - 2.4285922050 * mm + 0.4505937099 * sm,
+ 0.0259040371 * lm + 0.7827717662 * mm - 0.8086757660 * sm,
+ };
+}
+
+fn fromOklab(lab: [3]f32) [3]u8 {
+ 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 = .{
+ 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,
+ };
+ var out: [3]u8 = undefined;
+ for (&out, linear) |*o, v| {
+ const x = std.math.clamp(v, 0, 1);
+ const srgb = if (x <= 0.0031308) x * 12.92 else 1.055 * std.math.pow(f32, x, 1.0 / 2.4) - 0.055;
+ o.* = @intFromFloat(@round(std.math.clamp(srgb, 0, 1) * 255));
+ }
+ return out;
+}
+
+pub fn mixOklab(a: [3]u8, b: [3]u8, t: f32) [3]u8 {
+ if (std.mem.eql(u8, &a, &b)) return a;
+ const x = toOklab(a);
+ const y = toOklab(b);
+ return fromOklab(.{ x[0] + (y[0] - x[0]) * t, x[1] + (y[1] - x[1]) * t, x[2] + (y[2] - x[2]) * t });
+}
+
+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)));
+ try std.testing.expectEqual(rgb, mixOklab(rgb, .{ 10, 200, 30 }, 0));
+ }
+ // Red to green through sRGB bytes dips dark at the middle; through
+ // OKLab it keeps its lightness.
+ const red: [3]u8 = .{ 255, 0, 0 };
+ const green: [3]u8 = .{ 0, 200, 0 };
+ const srgb_mid = mix(red, green);
+ const lab_mid = mixOklab(red, green, 0.5);
+ try std.testing.expect(relativeLuminance(lab_mid) > relativeLuminance(srgb_mid) * 1.3);
+}
+
pub fn mix(a: [3]u8, b: [3]u8) [3]u8 {
var out: [3]u8 = undefined;
for (&out, a, b) |*c, x, y| c.* = @intCast((@as(u16, x) + y) / 2);
@@ -361,10 +420,14 @@ pub const ChromeTheme = struct {
};
}
+ /// Partway from one chrome to another, each colour through OKLab
+ /// (G10): a fade keeps its lightness even, never going muddy or dark
+ /// between two hues as a mix of sRGB bytes does.
pub fn interpolate(from: ChromeTheme, to: ChromeTheme, step: u16, steps: u16) ChromeTheme {
var out: ChromeTheme = undefined;
+ const t = @as(f32, @floatFromInt(@min(step, steps))) / @as(f32, @floatFromInt(@max(1, steps)));
inline for (@typeInfo(ChromeTheme).@"struct".fields) |field|
- @field(out, field.name) = animation.interpolateRgb(@field(from, field.name), @field(to, field.name), step, steps);
+ @field(out, field.name) = if (step == 0) @field(from, field.name) else if (step >= steps) @field(to, field.name) else mixOklab(@field(from, field.name), @field(to, field.name), t);
return out;
}
};