diff --git a/CMYK.zig b/CMYK.zig index 1bcf2914fb6ee01858b407fd3668fb7cb22b89b3..8fae9001f2192531bfd183f52bc56a4773218328 100644 --- a/CMYK.zig +++ b/CMYK.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Self = @This(); +const color = @import("./mod.zig"); c: f32, // cyan m: f32, // magenta @@ -37,3 +38,12 @@ pub fn to_array(x: Self) [5]f32 { x.a, }; } + +// https://www.rapidtables.com/convert/color/cmyk-to-rgb.html +pub fn to_srgb(x: Self) color.sRGB { + const c, const m, const y, const k, const a = x.to_array(); + const r = (1 - c) * (1 - k); + const g = (1 - m) * (1 - k); + const b = (1 - y) * (1 - k); + return color.sRGB.from_float(.{ r, g, b, a }); +} diff --git a/HSL.zig b/HSL.zig index 014d251ac50795416aae9790e839cbfce4967377..10e5c1f490dad89f196d8182c3085169e8828fc4 100644 --- a/HSL.zig +++ b/HSL.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Self = @This(); +const color = @import("./mod.zig"); const _x = @import("./_x.zig"); h: f32, // hue @@ -27,3 +28,24 @@ pub fn eql(x: Self, y: Self) bool { } pub usingnamespace _x.mixin(@This(), f32, .h, .s, .l); + +// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB +// https://www.rapidtables.com/convert/color/hsl-to-rgb.html +pub fn to_srgb(t: Self) color.sRGB { + var h, const s, const l, const a = t.to_array(); + h *= 360.0; + h /= 60.0; + const c = (1.0 - @abs(2 * l - 1.0)) * s; + const x = c * (1 - @abs(@rem(h, 2.0) - 1.0)); + const r, const g, const b = blk: { + if (h >= 0.0 and h < 1.0) break :blk .{ c, x, 0 }; + if (h >= 1.0 and h < 2.0) break :blk .{ x, c, 0 }; + if (h >= 2.0 and h < 3.0) break :blk .{ 0, c, x }; + if (h >= 3.0 and h < 4.0) break :blk .{ 0, x, c }; + if (h >= 4.0 and h < 5.0) break :blk .{ x, 0, c }; + if (h >= 5.0 and h < 6.0) break :blk .{ c, 0, x }; + unreachable; + }; + const m = l - c / 2.0; + return color.sRGB.from_float(.{ r + m, g + m, b + m, a }); +} diff --git a/HSV.zig b/HSV.zig index a6effd09afebde58e337a141eff10bd734846422..35d0a702d218b2d01fe701bdd07169c47801e8a9 100644 --- a/HSV.zig +++ b/HSV.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Self = @This(); +const color = @import("./mod.zig"); const _x = @import("./_x.zig"); h: f32, // hue @@ -27,3 +28,24 @@ pub fn eql(x: Self, y: Self) bool { } pub usingnamespace _x.mixin(@This(), f32, .h, .s, .v); + +// https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB +// https://www.rapidtables.com/convert/color/hsv-to-rgb.html +pub fn to_srgb(t: Self) color.sRGB { + var h, const s, const v, const a = t.to_array(); + h *= 360.0; + h /= 60.0; + const c = v * s; + const x = c * (1 - @abs(@rem(h, 2.0) - 1.0)); + const r, const g, const b = blk: { + if (h >= 0.0 and h < 1.0) break :blk .{ c, x, 0 }; + if (h >= 1.0 and h < 2.0) break :blk .{ x, c, 0 }; + if (h >= 2.0 and h < 3.0) break :blk .{ 0, c, x }; + if (h >= 3.0 and h < 4.0) break :blk .{ 0, x, c }; + if (h >= 4.0 and h < 5.0) break :blk .{ x, 0, c }; + if (h >= 5.0 and h < 6.0) break :blk .{ c, 0, x }; + unreachable; + }; + const m = v - c; + return color.sRGB.from_float(.{ r + m, g + m, b + m, a }); +} diff --git a/LinearRgb.zig b/LinearRgb.zig index 2e143cc37e0b864af697ffe6f11ca034ba5c5e98..4adff842597ece93bf30cf63cf4a50df47c02054 100644 --- a/LinearRgb.zig +++ b/LinearRgb.zig @@ -2,7 +2,9 @@ const std = @import("std"); const Self = @This(); +const color = @import("./mod.zig"); const _x = @import("./_x.zig"); +const vec4 = @Vector(4, f32); r: f32, // red g: f32, // green @@ -26,8 +28,18 @@ pub fn eql(x: Self, y: Self) bool { return x.r == y.r and x.g == y.g and x.b == y.b and x.a == y.a; } +// https://www.w3.org/TR/WCAG/#dfn-relative-luminance pub fn relative_luminance(x: Self) f32 { return (0.2126 * x.r) + (0.7152 * x.g) + (0.0722 * x.b); } pub usingnamespace _x.mixin(@This(), f32, .r, .g, .b); + +// https://gamedev.stackexchange.com/a/194038 +pub fn to_srgb(x: Self) color.sRGB { + const v: vec4 = x.to_array(); + const cutoff = v < @as(vec4, @splat(0.0031308)); + const higher = @as(vec4, @splat(1.055)) * std.math.pow(vec4, v, @splat(1.0 / 2.4)) - @as(vec4, @splat(0.055)); + const lower = v * @as(vec4, @splat(12.92)); + return color.sRGB.from_float(@select(f32, cutoff, higher, lower)); +} diff --git a/YCbCr.zig b/YCbCr.zig index 8e87e6e848b5de016f60d8b6d767cb7f38f136fa..9f355d7255dae3c3e3a58c44677ddce24b398825 100644 --- a/YCbCr.zig +++ b/YCbCr.zig @@ -27,16 +27,16 @@ pub fn eql(x: Self, y: Self) bool { return x.y == y.y and x.cb == y.cb and x.cr == y.cr and x.a == y.a; } -pub usingnamespace _x.mixin(@This(), .y, .cb, .cr); +pub usingnamespace _x.mixin(@This(), u8, .y, .cb, .cr); pub fn to_srgb(x: Self) color.sRGB { // zig fmt: off - const f = x.to_vec(); + const r, const g, const b, const a = x.to_vec(); return color.sRGB.from_vec(.{ - @max(0, @min(255, f[0] + 1.402 * (f[2]-128))), - @max(0, @min(255, f[0] - 0.34414 * (f[1]-128) - 0.71414 * (f[2]-128))), - @max(0, @min(255, f[0] + 1.772 * (f[1]-128))), - @max(0, @min(255, f[3])), + @max(0, @min(255, r + 1.402 * (b-128))), + @max(0, @min(255, r - 0.34414 * (g-128) - 0.71414 * (b-128))), + @max(0, @min(255, r + 1.772 * (g-128))), + @max(0, @min(255, a)), }); // zig fmt: on } diff --git a/sRGB.zig b/sRGB.zig index 8114e489097f202c7244b2660cdac7182a4ab6c8..d7ad80f5e1afd539aeb6a15a9300b3de8968997e 100644 --- a/sRGB.zig +++ b/sRGB.zig @@ -75,6 +75,8 @@ pub fn format(x: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, pub usingnamespace _x.mixin(@This(), u8, .r, .g, .b); +// https://www.w3.org/TR/WCAG/#dfn-relative-luminance +// https://webstore.iec.ch/publication/6169 pub fn to_linear_rgb(x: Self) color.LinearRgb { const lut = comptime blk: { @setEvalBranchQuota(10_000); @@ -102,6 +104,17 @@ pub fn to_float(x: Self) Float { }; } +pub fn from_float(y: @Vector(4, f32)) Self { + const r, const g, const b, const a = y * @as(@Vector(4, f32), @splat(255.0)); + return .{ + .r = @intFromFloat(r), + .g = @intFromFloat(g), + .b = @intFromFloat(b), + .a = @intFromFloat(a), + }; +} + +// https://www.rapidtables.com/convert/color/rgb-to-cmyk.html pub fn to_cmyk(x: Self) color.CMYK { const f = x.to_float(); const k = 1 - @max(f.r, f.g, f.b); @@ -112,6 +125,7 @@ pub fn to_cmyk(x: Self) color.CMYK { return color.CMYK.initCMYKA(c, m, y, k, a); } +// https://www.rapidtables.com/convert/color/rgb-to-hsl.html pub fn to_hsl(x: Self) color.HSL { const f = x.to_float(); const cmax = @max(f.r, f.g, f.b); @@ -130,6 +144,7 @@ pub fn to_hsl(x: Self) color.HSL { return color.HSL.initHSLA(h, s, l, a); } +// https://www.rapidtables.com/convert/color/rgb-to-hsv.html pub fn to_hsv(x: Self) color.HSV { const f = x.to_float(); const cmax = @max(f.r, f.g, f.b); @@ -150,12 +165,12 @@ pub fn to_hsv(x: Self) color.HSV { pub fn to_ycbcr(x: Self) color.YCbCr { // zig fmt: off - const f = x.to_vec(); + const r, const g, const b, const a = x.to_vec(); return color.YCbCr.from_vec(.{ - ( 0.299 * f.r + 0.587 * f.g + 0.114 * f.b) + 0, - (-0.1687 * f.r + -0.3313 * f.g + 0.5 * f.b) + 128, - ( 0.5 * f.r + -0.4187 * f.g + -0.0813 * f.b) + 128, - f.a, + ( 0.299 * r + 0.587 * g + 0.114 * b) + 0, + (-0.1687 * r + -0.3313 * g + 0.5 * b) + 128, + ( 0.5 * r + -0.4187 * g + -0.0813 * b) + 128, + a, }); // zig fmt: on } diff --git a/test.zig b/test.zig index 6a976c9fe59c29eedba6b8920fd684f31e70052d..d20d3a4f9b2a38ef1163a7abfc1e9269e30722ed 100644 --- a/test.zig +++ b/test.zig @@ -1,8 +1,23 @@ const std = @import("std"); const color = @import("color"); +fn expect(actual: anytype) Expect(@TypeOf(actual)) { + return .{ .actual = actual }; +} + +fn Expect(T: type) type { + return struct { + actual: T, + + fn toEqual(self: *const @This(), expected: T) !void { + try std.testing.expectEqual(expected, self.actual); + } + }; +} + test { // https://www.colorhexa.com/4fc3f7 + // https://encycolorpedia.com/4fc3f7 const o = color.sRGB.parseHexConst("#4FC3F7"); { const r, const g, const b, const a = o.to_array(); @@ -10,15 +25,18 @@ test { try std.testing.expect(g == 195); try std.testing.expect(b == 247); try std.testing.expect(a == 255); + try expect(o.to_array()).toEqual(.{ 79, 195, 247, 255 }); } { // 68% cyan, 21.1% magenta, 0% yellow and 3.1% black + // cyan: 68% (0.68), magenta: 21% (0.211), yellow: 0% (0.0), key: 3% (0.031) const c, const m, const y, const k, const a = o.to_cmyk().to_array(); try std.testing.expectApproxEqAbs(0.68, c, 0.01); try std.testing.expectApproxEqAbs(0.21, m, 0.01); try std.testing.expectApproxEqAbs(0.00, y, 0.01); try std.testing.expectApproxEqAbs(0.03, k, 0.01); try std.testing.expect(a == 1.0); + try expect(o.to_cmyk().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 }); } { // 198.6°, 91.3, 63.9 @@ -27,6 +45,7 @@ test { try std.testing.expectApproxEqAbs(0.913, s, 0.001); try std.testing.expectApproxEqAbs(0.639, l, 0.001); try std.testing.expect(a == 1.0); + try expect(o.to_hsl().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 }); } { // 198.6°, 68, 96.9 @@ -35,13 +54,22 @@ test { try std.testing.expectApproxEqAbs(0.680, s, 0.001); try std.testing.expectApproxEqAbs(0.969, v, 0.001); try std.testing.expect(a == 1.0); + try expect(o.to_hsv().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 }); } { + // 0.07818742180518633 0.5457244613701866 0.9301108583754237 // 0.0781874218051863, 0.545724461370187, 0.930110858375424 const r, const g, const b, const a = o.to_linear_rgb().to_array(); try std.testing.expectApproxEqAbs(0.078, r, 0.001); try std.testing.expectApproxEqAbs(0.545, g, 0.001); try std.testing.expectApproxEqAbs(0.930, b, 0.001); try std.testing.expect(a == 1.0); + // try expect(o.to_linear_rgb().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 }); // https://github.com/ziglang/zig/issues/22354 + try std.testing.expectApproxEqAbs(0.474, o.to_linear_rgb().relative_luminance(), 0.001); + } + { + // Y: 158.789, Cb: 167.996, Cr: 73.384 + try expect(o.to_ycbcr().to_array()).toEqual(.{ 166, 173, 65, 255 }); + try expect(o.to_ycbcr().to_srgb().to_array()).toEqual(.{ 77, 195, 245, 255 }); } }