const std = @import("std"); const assert = std.debug.assert; const x11_color = @import("x11_color.zig"); /// The default palette. pub const default: Palette = default: { var result: Palette = undefined; // Named values var i: u8 = 0; while (i < 16) : (i += 1) { result[i] = Name.default(@enumFromInt(i)) catch unreachable; } // Cube assert(i == 16); var r: u8 = 0; while (r < 6) : (r += 1) { var g: u8 = 0; while (g < 6) : (g += 1) { var b: u8 = 0; while (b < 6) : (b += 1) { result[i] = .{ .r = if (r == 0) 0 else (r * 40 + 55), .g = if (g == 0) 0 else (g * 40 + 55), .b = if (b == 0) 0 else (b * 40 + 55), }; i += 1; } } } // Gray ramp assert(i == 232); assert(@TypeOf(i) == u8); while (i > 0) : (i +%= 1) { const value = ((i - 232) * 10) + 8; result[i] = .{ .r = value, .g = value, .b = value }; } break :default result; }; /// Palette is the 256 color palette. pub const Palette = [256]RGB; /// Color names in the standard 8 or 16 color palette. pub const Name = enum(u8) { black = 0, red = 1, green = 2, yellow = 3, blue = 4, magenta = 5, cyan = 6, white = 7, bright_black = 8, bright_red = 9, bright_green = 10, bright_yellow = 11, bright_blue = 12, bright_magenta = 13, bright_cyan = 14, bright_white = 15, // Remainders are valid unnamed values in the 256 color palette. _, /// Default colors for tagged values. pub fn default(self: Name) !RGB { return switch (self) { .black => RGB{ .r = 0x1D, .g = 0x1F, .b = 0x21 }, .red => RGB{ .r = 0xCC, .g = 0x66, .b = 0x66 }, .green => RGB{ .r = 0xB5, .g = 0xBD, .b = 0x68 }, .yellow => RGB{ .r = 0xF0, .g = 0xC6, .b = 0x74 }, .blue => RGB{ .r = 0x81, .g = 0xA2, .b = 0xBE }, .magenta => RGB{ .r = 0xB2, .g = 0x94, .b = 0xBB }, .cyan => RGB{ .r = 0x8A, .g = 0xBE, .b = 0xB7 }, .white => RGB{ .r = 0xC5, .g = 0xC8, .b = 0xC6 }, .bright_black => RGB{ .r = 0x66, .g = 0x66, .b = 0x66 }, .bright_red => RGB{ .r = 0xD5, .g = 0x4E, .b = 0x53 }, .bright_green => RGB{ .r = 0xB9, .g = 0xCA, .b = 0x4A }, .bright_yellow => RGB{ .r = 0xE7, .g = 0xC5, .b = 0x47 }, .bright_blue => RGB{ .r = 0x7A, .g = 0xA6, .b = 0xDA }, .bright_magenta => RGB{ .r = 0xC3, .g = 0x97, .b = 0xD8 }, .bright_cyan => RGB{ .r = 0x70, .g = 0xC0, .b = 0xB1 }, .bright_white => RGB{ .r = 0xEA, .g = 0xEA, .b = 0xEA }, else => error.NoDefaultValue, }; } }; /// RGB pub const RGB = packed struct(u24) { r: u8 = 0, g: u8 = 0, b: u8 = 0, pub fn eql(self: RGB, other: RGB) bool { return self.r == other.r and self.g == other.g and self.b == other.b; } /// Calculates the contrast ratio between two colors. The contrast /// ration is a value between 1 and 21 where 1 is the lowest contrast /// and 21 is the highest contrast. /// /// https://www.w3.org/TR/WCAG20/#contrast-ratiodef pub fn contrast(self: RGB, other: RGB) f64 { // pair[0] = lighter, pair[1] = darker const pair: [2]f64 = pair: { const self_lum = self.luminance(); const other_lum = other.luminance(); if (self_lum > other_lum) break :pair .{ self_lum, other_lum }; break :pair .{ other_lum, self_lum }; }; return (pair[0] + 0.05) / (pair[1] + 0.05); } /// Calculates luminance based on the W3C formula. This returns a /// normalized value between 0 and 1 where 0 is black and 1 is white. /// /// https://www.w3.org/TR/WCAG20/#relativeluminancedef pub fn luminance(self: RGB) f64 { const r_lum = componentLuminance(self.r); const g_lum = componentLuminance(self.g); const b_lum = componentLuminance(self.b); return 0.2126 * r_lum + 0.7152 * g_lum + 0.0722 * b_lum; } /// Calculates single-component luminance based on the W3C formula. /// /// Expects sRGB color space which at the time of writing we don't /// generally use but it's a good enough approximation until we fix that. /// https://www.w3.org/TR/WCAG20/#relativeluminancedef fn componentLuminance(c: u8) f64 { const c_f64: f64 = @floatFromInt(c); const normalized: f64 = c_f64 / 255; if (normalized <= 0.03928) return normalized / 12.92; return std.math.pow(f64, (normalized + 0.055) / 1.055, 2.4); } /// Calculates "perceived luminance" which is better for determining /// light vs dark. /// /// Source: https://www.w3.org/TR/AERT/#color-contrast pub fn perceivedLuminance(self: RGB) f64 { const r_f64: f64 = @floatFromInt(self.r); const g_f64: f64 = @floatFromInt(self.g); const b_f64: f64 = @floatFromInt(self.b); return 0.299 * (r_f64 / 255) + 0.587 * (g_f64 / 255) + 0.114 * (b_f64 / 255); } comptime { assert(@bitSizeOf(RGB) == 24); assert(@sizeOf(RGB) == 4); } /// Parse a color from a floating point intensity value. /// /// The value should be between 0.0 and 1.0, inclusive. fn fromIntensity(value: []const u8) !u8 { const i = std.fmt.parseFloat(f64, value) catch return error.InvalidFormat; if (i < 0.0 or i > 1.0) { return error.InvalidFormat; } return @intFromFloat(i * std.math.maxInt(u8)); } /// Parse a color from a string of hexadecimal digits /// /// The string can contain 1, 2, 3, or 4 characters and represents the color /// value scaled in 4, 8, 12, or 16 bits, respectively. fn fromHex(value: []const u8) !u8 { if (value.len == 0 or value.len > 4) { return error.InvalidFormat; } const color = std.fmt.parseUnsigned(u16, value, 16) catch return error.InvalidFormat; const divisor: usize = switch (value.len) { 1 => std.math.maxInt(u4), 2 => std.math.maxInt(u8), 3 => std.math.maxInt(u12), 4 => std.math.maxInt(u16), else => unreachable, }; return @intCast(@as(usize, color) * std.math.maxInt(u8) / divisor); } /// Parse a color specification. /// /// Any of the following forms are accepted: /// /// 1. rgb:// /// /// , , := h | hh | hhh | hhhh /// /// where `h` is a single hexadecimal digit. /// /// 2. rgbi:// /// /// where , , and are floating point values between /// 0.0 and 1.0 (inclusive). /// /// 3. #rgb, #rrggbb, #rrrgggbbb #rrrrggggbbbb /// /// where `r`, `g`, and `b` are a single hexadecimal digit. /// These specify a color with 4, 8, 12, and 16 bits of precision /// per color channel. pub fn parse(value: []const u8) !RGB { if (value.len == 0) { return error.InvalidFormat; } if (value[0] == '#') { switch (value.len) { 4 => return RGB{ .r = try RGB.fromHex(value[1..2]), .g = try RGB.fromHex(value[2..3]), .b = try RGB.fromHex(value[3..4]), }, 7 => return RGB{ .r = try RGB.fromHex(value[1..3]), .g = try RGB.fromHex(value[3..5]), .b = try RGB.fromHex(value[5..7]), }, 10 => return RGB{ .r = try RGB.fromHex(value[1..4]), .g = try RGB.fromHex(value[4..7]), .b = try RGB.fromHex(value[7..10]), }, 13 => return RGB{ .r = try RGB.fromHex(value[1..5]), .g = try RGB.fromHex(value[5..9]), .b = try RGB.fromHex(value[9..13]), }, else => return error.InvalidFormat, } } // Check for X11 named colors. We allow whitespace around the edges // of the color because Kitty allows whitespace. This is not part of // any spec I could find. if (x11_color.map.get(std.mem.trim(u8, value, " "))) |rgb| return rgb; if (value.len < "rgb:a/a/a".len or !std.mem.eql(u8, value[0..3], "rgb")) { return error.InvalidFormat; } var i: usize = 3; const use_intensity = if (value[i] == 'i') blk: { i += 1; break :blk true; } else false; if (value[i] != ':') { return error.InvalidFormat; } i += 1; const r = r: { const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| value[i..end] else return error.InvalidFormat; i += slice.len + 1; break :r if (use_intensity) try RGB.fromIntensity(slice) else try RGB.fromHex(slice); }; const g = g: { const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| value[i..end] else return error.InvalidFormat; i += slice.len + 1; break :g if (use_intensity) try RGB.fromIntensity(slice) else try RGB.fromHex(slice); }; const b = if (use_intensity) try RGB.fromIntensity(value[i..]) else try RGB.fromHex(value[i..]); return RGB{ .r = r, .g = g, .b = b, }; } }; test "palette: default" { const testing = std.testing; // Safety check var i: u8 = 0; while (i < 16) : (i += 1) { try testing.expectEqual(Name.default(@as(Name, @enumFromInt(i))), default[i]); } } test "RGB.parse" { const testing = std.testing; try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("rgbi:1.0/0/0")); try testing.expectEqual(RGB{ .r = 127, .g = 160, .b = 0 }, try RGB.parse("rgb:7f/a0a0/0")); try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("rgb:f/ff/fff")); try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffff")); try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#fff")); try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#fffffffff")); try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffffffffff")); try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 16 }, try RGB.parse("#ff0010")); try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, try RGB.parse("black")); try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("red")); try testing.expectEqual(RGB{ .r = 0, .g = 255, .b = 0 }, try RGB.parse("green")); try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 255 }, try RGB.parse("blue")); try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("white")); try testing.expectEqual(RGB{ .r = 124, .g = 252, .b = 0 }, try RGB.parse("LawnGreen")); try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, try RGB.parse("medium spring green")); try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, try RGB.parse(" Forest Green ")); // Invalid format try testing.expectError(error.InvalidFormat, RGB.parse("rgb;")); try testing.expectError(error.InvalidFormat, RGB.parse("rgb:")); try testing.expectError(error.InvalidFormat, RGB.parse(":a/a/a")); try testing.expectError(error.InvalidFormat, RGB.parse("a/a/a")); try testing.expectError(error.InvalidFormat, RGB.parse("rgb:a/a/a/")); try testing.expectError(error.InvalidFormat, RGB.parse("rgb:00000///")); try testing.expectError(error.InvalidFormat, RGB.parse("rgb:000/")); try testing.expectError(error.InvalidFormat, RGB.parse("rgbi:a/a/a")); try testing.expectError(error.InvalidFormat, RGB.parse("rgb:0.5/0.0/1.0")); try testing.expectError(error.InvalidFormat, RGB.parse("rgb:not/hex/zz")); try testing.expectError(error.InvalidFormat, RGB.parse("#")); try testing.expectError(error.InvalidFormat, RGB.parse("#ff")); try testing.expectError(error.InvalidFormat, RGB.parse("#ffff")); try testing.expectError(error.InvalidFormat, RGB.parse("#fffff")); try testing.expectError(error.InvalidFormat, RGB.parse("#gggggg")); }