authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2024-12-29 20:32:57 -08:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2024-12-29 20:32:57 -08:00
log36f4989aee0406ab6ea7802fdac1fe8a45ea89d7
tree914ab0bd00225af61041b9153c84f4cb42d6d29c
parent8c09516944357d85f4ffa3d7a1421f96f63ac420

more tests and add algorithm references


7 files changed, 120 insertions(+), 11 deletions(-)

CMYK.zig+10
......@@ -2,6 +2,7 @@
22
33const std = @import("std");
44const Self = @This();
5const color = @import("./mod.zig");
56
67c: f32, // cyan
78m: f32, // magenta
......@@ -37,3 +38,12 @@ pub fn to_array(x: Self) [5]f32 {
3738 x.a,
3839 };
3940}
41
42// https://www.rapidtables.com/convert/color/cmyk-to-rgb.html
43pub fn to_srgb(x: Self) color.sRGB {
44 const c, const m, const y, const k, const a = x.to_array();
45 const r = (1 - c) * (1 - k);
46 const g = (1 - m) * (1 - k);
47 const b = (1 - y) * (1 - k);
48 return color.sRGB.from_float(.{ r, g, b, a });
49}
HSL.zig+22
......@@ -2,6 +2,7 @@
22
33const std = @import("std");
44const Self = @This();
5const color = @import("./mod.zig");
56const _x = @import("./_x.zig");
67
78h: f32, // hue
......@@ -27,3 +28,24 @@ pub fn eql(x: Self, y: Self) bool {
2728}
2829
2930pub usingnamespace _x.mixin(@This(), f32, .h, .s, .l);
31
32// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
33// https://www.rapidtables.com/convert/color/hsl-to-rgb.html
34pub fn to_srgb(t: Self) color.sRGB {
35 var h, const s, const l, const a = t.to_array();
36 h *= 360.0;
37 h /= 60.0;
38 const c = (1.0 - @abs(2 * l - 1.0)) * s;
39 const x = c * (1 - @abs(@rem(h, 2.0) - 1.0));
40 const r, const g, const b = blk: {
41 if (h >= 0.0 and h < 1.0) break :blk .{ c, x, 0 };
42 if (h >= 1.0 and h < 2.0) break :blk .{ x, c, 0 };
43 if (h >= 2.0 and h < 3.0) break :blk .{ 0, c, x };
44 if (h >= 3.0 and h < 4.0) break :blk .{ 0, x, c };
45 if (h >= 4.0 and h < 5.0) break :blk .{ x, 0, c };
46 if (h >= 5.0 and h < 6.0) break :blk .{ c, 0, x };
47 unreachable;
48 };
49 const m = l - c / 2.0;
50 return color.sRGB.from_float(.{ r + m, g + m, b + m, a });
51}
HSV.zig+22
......@@ -2,6 +2,7 @@
22
33const std = @import("std");
44const Self = @This();
5const color = @import("./mod.zig");
56const _x = @import("./_x.zig");
67
78h: f32, // hue
......@@ -27,3 +28,24 @@ pub fn eql(x: Self, y: Self) bool {
2728}
2829
2930pub usingnamespace _x.mixin(@This(), f32, .h, .s, .v);
31
32// https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
33// https://www.rapidtables.com/convert/color/hsv-to-rgb.html
34pub fn to_srgb(t: Self) color.sRGB {
35 var h, const s, const v, const a = t.to_array();
36 h *= 360.0;
37 h /= 60.0;
38 const c = v * s;
39 const x = c * (1 - @abs(@rem(h, 2.0) - 1.0));
40 const r, const g, const b = blk: {
41 if (h >= 0.0 and h < 1.0) break :blk .{ c, x, 0 };
42 if (h >= 1.0 and h < 2.0) break :blk .{ x, c, 0 };
43 if (h >= 2.0 and h < 3.0) break :blk .{ 0, c, x };
44 if (h >= 3.0 and h < 4.0) break :blk .{ 0, x, c };
45 if (h >= 4.0 and h < 5.0) break :blk .{ x, 0, c };
46 if (h >= 5.0 and h < 6.0) break :blk .{ c, 0, x };
47 unreachable;
48 };
49 const m = v - c;
50 return color.sRGB.from_float(.{ r + m, g + m, b + m, a });
51}
LinearRgb.zig+12
......@@ -2,7 +2,9 @@
22
33const std = @import("std");
44const Self = @This();
5const color = @import("./mod.zig");
56const _x = @import("./_x.zig");
7const vec4 = @Vector(4, f32);
68
79r: f32, // red
810g: f32, // green
......@@ -26,8 +28,18 @@ pub fn eql(x: Self, y: Self) bool {
2628 return x.r == y.r and x.g == y.g and x.b == y.b and x.a == y.a;
2729}
2830
31// https://www.w3.org/TR/WCAG/#dfn-relative-luminance
2932pub fn relative_luminance(x: Self) f32 {
3033 return (0.2126 * x.r) + (0.7152 * x.g) + (0.0722 * x.b);
3134}
3235
3336pub usingnamespace _x.mixin(@This(), f32, .r, .g, .b);
37
38// https://gamedev.stackexchange.com/a/194038
39pub fn to_srgb(x: Self) color.sRGB {
40 const v: vec4 = x.to_array();
41 const cutoff = v < @as(vec4, @splat(0.0031308));
42 const higher = @as(vec4, @splat(1.055)) * std.math.pow(vec4, v, @splat(1.0 / 2.4)) - @as(vec4, @splat(0.055));
43 const lower = v * @as(vec4, @splat(12.92));
44 return color.sRGB.from_float(@select(f32, cutoff, higher, lower));
45}
YCbCr.zig+6-6
......@@ -27,16 +27,16 @@ pub fn eql(x: Self, y: Self) bool {
2727 return x.y == y.y and x.cb == y.cb and x.cr == y.cr and x.a == y.a;
2828}
2929
30pub usingnamespace _x.mixin(@This(), .y, .cb, .cr);
30pub usingnamespace _x.mixin(@This(), u8, .y, .cb, .cr);
3131
3232pub fn to_srgb(x: Self) color.sRGB {
3333 // zig fmt: off
34 const f = x.to_vec();
34 const r, const g, const b, const a = x.to_vec();
3535 return color.sRGB.from_vec(.{
36 @max(0, @min(255, f[0] + 1.402 * (f[2]-128))),
37 @max(0, @min(255, f[0] - 0.34414 * (f[1]-128) - 0.71414 * (f[2]-128))),
38 @max(0, @min(255, f[0] + 1.772 * (f[1]-128))),
39 @max(0, @min(255, f[3])),
36 @max(0, @min(255, r + 1.402 * (b-128))),
37 @max(0, @min(255, r - 0.34414 * (g-128) - 0.71414 * (b-128))),
38 @max(0, @min(255, r + 1.772 * (g-128))),
39 @max(0, @min(255, a)),
4040 });
4141 // zig fmt: on
4242}
sRGB.zig+20-5
......@@ -75,6 +75,8 @@ pub fn format(x: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions,
7575
7676pub usingnamespace _x.mixin(@This(), u8, .r, .g, .b);
7777
78// https://www.w3.org/TR/WCAG/#dfn-relative-luminance
79// https://webstore.iec.ch/publication/6169
7880pub fn to_linear_rgb(x: Self) color.LinearRgb {
7981 const lut = comptime blk: {
8082 @setEvalBranchQuota(10_000);
......@@ -102,6 +104,17 @@ pub fn to_float(x: Self) Float {
102104 };
103105}
104106
107pub fn from_float(y: @Vector(4, f32)) Self {
108 const r, const g, const b, const a = y * @as(@Vector(4, f32), @splat(255.0));
109 return .{
110 .r = @intFromFloat(r),
111 .g = @intFromFloat(g),
112 .b = @intFromFloat(b),
113 .a = @intFromFloat(a),
114 };
115}
116
117// https://www.rapidtables.com/convert/color/rgb-to-cmyk.html
105118pub fn to_cmyk(x: Self) color.CMYK {
106119 const f = x.to_float();
107120 const k = 1 - @max(f.r, f.g, f.b);
......@@ -112,6 +125,7 @@ pub fn to_cmyk(x: Self) color.CMYK {
112125 return color.CMYK.initCMYKA(c, m, y, k, a);
113126}
114127
128// https://www.rapidtables.com/convert/color/rgb-to-hsl.html
115129pub fn to_hsl(x: Self) color.HSL {
116130 const f = x.to_float();
117131 const cmax = @max(f.r, f.g, f.b);
......@@ -130,6 +144,7 @@ pub fn to_hsl(x: Self) color.HSL {
130144 return color.HSL.initHSLA(h, s, l, a);
131145}
132146
147// https://www.rapidtables.com/convert/color/rgb-to-hsv.html
133148pub fn to_hsv(x: Self) color.HSV {
134149 const f = x.to_float();
135150 const cmax = @max(f.r, f.g, f.b);
......@@ -150,12 +165,12 @@ pub fn to_hsv(x: Self) color.HSV {
150165
151166pub fn to_ycbcr(x: Self) color.YCbCr {
152167 // zig fmt: off
153 const f = x.to_vec();
168 const r, const g, const b, const a = x.to_vec();
154169 return color.YCbCr.from_vec(.{
155 ( 0.299 * f.r + 0.587 * f.g + 0.114 * f.b) + 0,
156 (-0.1687 * f.r + -0.3313 * f.g + 0.5 * f.b) + 128,
157 ( 0.5 * f.r + -0.4187 * f.g + -0.0813 * f.b) + 128,
158 f.a,
170 ( 0.299 * r + 0.587 * g + 0.114 * b) + 0,
171 (-0.1687 * r + -0.3313 * g + 0.5 * b) + 128,
172 ( 0.5 * r + -0.4187 * g + -0.0813 * b) + 128,
173 a,
159174 });
160175 // zig fmt: on
161176}
test.zig+28
......@@ -1,8 +1,23 @@
11const std = @import("std");
22const color = @import("color");
33
4fn expect(actual: anytype) Expect(@TypeOf(actual)) {
5 return .{ .actual = actual };
6}
7
8fn Expect(T: type) type {
9 return struct {
10 actual: T,
11
12 fn toEqual(self: *const @This(), expected: T) !void {
13 try std.testing.expectEqual(expected, self.actual);
14 }
15 };
16}
17
418test {
519 // https://www.colorhexa.com/4fc3f7
20 // https://encycolorpedia.com/4fc3f7
621 const o = color.sRGB.parseHexConst("#4FC3F7");
722 {
823 const r, const g, const b, const a = o.to_array();
......@@ -10,15 +25,18 @@ test {
1025 try std.testing.expect(g == 195);
1126 try std.testing.expect(b == 247);
1227 try std.testing.expect(a == 255);
28 try expect(o.to_array()).toEqual(.{ 79, 195, 247, 255 });
1329 }
1430 {
1531 // 68% cyan, 21.1% magenta, 0% yellow and 3.1% black
32 // cyan: 68% (0.68), magenta: 21% (0.211), yellow: 0% (0.0), key: 3% (0.031)
1633 const c, const m, const y, const k, const a = o.to_cmyk().to_array();
1734 try std.testing.expectApproxEqAbs(0.68, c, 0.01);
1835 try std.testing.expectApproxEqAbs(0.21, m, 0.01);
1936 try std.testing.expectApproxEqAbs(0.00, y, 0.01);
2037 try std.testing.expectApproxEqAbs(0.03, k, 0.01);
2138 try std.testing.expect(a == 1.0);
39 try expect(o.to_cmyk().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 });
2240 }
2341 {
2442 // 198.6°, 91.3, 63.9
......@@ -27,6 +45,7 @@ test {
2745 try std.testing.expectApproxEqAbs(0.913, s, 0.001);
2846 try std.testing.expectApproxEqAbs(0.639, l, 0.001);
2947 try std.testing.expect(a == 1.0);
48 try expect(o.to_hsl().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 });
3049 }
3150 {
3251 // 198.6°, 68, 96.9
......@@ -35,13 +54,22 @@ test {
3554 try std.testing.expectApproxEqAbs(0.680, s, 0.001);
3655 try std.testing.expectApproxEqAbs(0.969, v, 0.001);
3756 try std.testing.expect(a == 1.0);
57 try expect(o.to_hsv().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 });
3858 }
3959 {
60 // 0.07818742180518633 0.5457244613701866 0.9301108583754237
4061 // 0.0781874218051863, 0.545724461370187, 0.930110858375424
4162 const r, const g, const b, const a = o.to_linear_rgb().to_array();
4263 try std.testing.expectApproxEqAbs(0.078, r, 0.001);
4364 try std.testing.expectApproxEqAbs(0.545, g, 0.001);
4465 try std.testing.expectApproxEqAbs(0.930, b, 0.001);
4566 try std.testing.expect(a == 1.0);
67 // try expect(o.to_linear_rgb().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 }); // https://github.com/ziglang/zig/issues/22354
68 try std.testing.expectApproxEqAbs(0.474, o.to_linear_rgb().relative_luminance(), 0.001);
69 }
70 {
71 // Y: 158.789, Cb: 167.996, Cr: 73.384
72 try expect(o.to_ycbcr().to_array()).toEqual(.{ 166, 173, 65, 255 });
73 try expect(o.to_ycbcr().to_srgb().to_array()).toEqual(.{ 77, 195, 245, 255 });
4674 }
4775}