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 @@...@@ -2,6 +2,7 @@
22
3const std = @import("std");3const std = @import("std");
4const Self = @This();4const Self = @This();
5const color = @import("./mod.zig");
56
6c: f32, // cyan7c: f32, // cyan
7m: f32, // magenta8m: f32, // magenta
...@@ -37,3 +38,12 @@ pub fn to_array(x: Self) [5]f32 {...@@ -37,3 +38,12 @@ pub fn to_array(x: Self) [5]f32 {
37 x.a,38 x.a,
38 };39 };
39}40}
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 @@...@@ -2,6 +2,7 @@
22
3const std = @import("std");3const std = @import("std");
4const Self = @This();4const Self = @This();
5const color = @import("./mod.zig");
5const _x = @import("./_x.zig");6const _x = @import("./_x.zig");
67
7h: f32, // hue8h: f32, // hue
...@@ -27,3 +28,24 @@ pub fn eql(x: Self, y: Self) bool {...@@ -27,3 +28,24 @@ pub fn eql(x: Self, y: Self) bool {
27}28}
2829
29pub usingnamespace _x.mixin(@This(), f32, .h, .s, .l);30pub 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 @@...@@ -2,6 +2,7 @@
22
3const std = @import("std");3const std = @import("std");
4const Self = @This();4const Self = @This();
5const color = @import("./mod.zig");
5const _x = @import("./_x.zig");6const _x = @import("./_x.zig");
67
7h: f32, // hue8h: f32, // hue
...@@ -27,3 +28,24 @@ pub fn eql(x: Self, y: Self) bool {...@@ -27,3 +28,24 @@ pub fn eql(x: Self, y: Self) bool {
27}28}
2829
29pub usingnamespace _x.mixin(@This(), f32, .h, .s, .v);30pub 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 @@...@@ -2,7 +2,9 @@
22
3const std = @import("std");3const std = @import("std");
4const Self = @This();4const Self = @This();
5const color = @import("./mod.zig");
5const _x = @import("./_x.zig");6const _x = @import("./_x.zig");
7const vec4 = @Vector(4, f32);
68
7r: f32, // red9r: f32, // red
8g: f32, // green10g: f32, // green
...@@ -26,8 +28,18 @@ pub fn eql(x: Self, y: Self) bool {...@@ -26,8 +28,18 @@ pub fn eql(x: Self, y: Self) bool {
26 return x.r == y.r and x.g == y.g and x.b == y.b and x.a == y.a;28 return x.r == y.r and x.g == y.g and x.b == y.b and x.a == y.a;
27}29}
2830
31// https://www.w3.org/TR/WCAG/#dfn-relative-luminance
29pub fn relative_luminance(x: Self) f32 {32pub fn relative_luminance(x: Self) f32 {
30 return (0.2126 * x.r) + (0.7152 * x.g) + (0.0722 * x.b);33 return (0.2126 * x.r) + (0.7152 * x.g) + (0.0722 * x.b);
31}34}
3235
33pub usingnamespace _x.mixin(@This(), f32, .r, .g, .b);36pub 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 {...@@ -27,16 +27,16 @@ pub fn eql(x: Self, y: Self) bool {
27 return x.y == y.y and x.cb == y.cb and x.cr == y.cr and x.a == y.a;27 return x.y == y.y and x.cb == y.cb and x.cr == y.cr and x.a == y.a;
28}28}
2929
30pub usingnamespace _x.mixin(@This(), .y, .cb, .cr);30pub usingnamespace _x.mixin(@This(), u8, .y, .cb, .cr);
3131
32pub fn to_srgb(x: Self) color.sRGB {32pub fn to_srgb(x: Self) color.sRGB {
33 // zig fmt: off33 // zig fmt: off
34 const f = x.to_vec();34 const r, const g, const b, const a = x.to_vec();
35 return color.sRGB.from_vec(.{35 return color.sRGB.from_vec(.{
36 @max(0, @min(255, f[0] + 1.402 * (f[2]-128))),36 @max(0, @min(255, r + 1.402 * (b-128))),
37 @max(0, @min(255, f[0] - 0.34414 * (f[1]-128) - 0.71414 * (f[2]-128))),37 @max(0, @min(255, r - 0.34414 * (g-128) - 0.71414 * (b-128))),
38 @max(0, @min(255, f[0] + 1.772 * (f[1]-128))),38 @max(0, @min(255, r + 1.772 * (g-128))),
39 @max(0, @min(255, f[3])),39 @max(0, @min(255, a)),
40 });40 });
41 // zig fmt: on41 // zig fmt: on
42}42}
sRGB.zig+20-5
...@@ -75,6 +75,8 @@ pub fn format(x: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions,...@@ -75,6 +75,8 @@ pub fn format(x: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions,
7575
76pub usingnamespace _x.mixin(@This(), u8, .r, .g, .b);76pub 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
78pub fn to_linear_rgb(x: Self) color.LinearRgb {80pub fn to_linear_rgb(x: Self) color.LinearRgb {
79 const lut = comptime blk: {81 const lut = comptime blk: {
80 @setEvalBranchQuota(10_000);82 @setEvalBranchQuota(10_000);
...@@ -102,6 +104,17 @@ pub fn to_float(x: Self) Float {...@@ -102,6 +104,17 @@ pub fn to_float(x: Self) Float {
102 };104 };
103}105}
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
105pub fn to_cmyk(x: Self) color.CMYK {118pub fn to_cmyk(x: Self) color.CMYK {
106 const f = x.to_float();119 const f = x.to_float();
107 const k = 1 - @max(f.r, f.g, f.b);120 const k = 1 - @max(f.r, f.g, f.b);
...@@ -112,6 +125,7 @@ pub fn to_cmyk(x: Self) color.CMYK {...@@ -112,6 +125,7 @@ pub fn to_cmyk(x: Self) color.CMYK {
112 return color.CMYK.initCMYKA(c, m, y, k, a);125 return color.CMYK.initCMYKA(c, m, y, k, a);
113}126}
114127
128// https://www.rapidtables.com/convert/color/rgb-to-hsl.html
115pub fn to_hsl(x: Self) color.HSL {129pub fn to_hsl(x: Self) color.HSL {
116 const f = x.to_float();130 const f = x.to_float();
117 const cmax = @max(f.r, f.g, f.b);131 const cmax = @max(f.r, f.g, f.b);
...@@ -130,6 +144,7 @@ pub fn to_hsl(x: Self) color.HSL {...@@ -130,6 +144,7 @@ pub fn to_hsl(x: Self) color.HSL {
130 return color.HSL.initHSLA(h, s, l, a);144 return color.HSL.initHSLA(h, s, l, a);
131}145}
132146
147// https://www.rapidtables.com/convert/color/rgb-to-hsv.html
133pub fn to_hsv(x: Self) color.HSV {148pub fn to_hsv(x: Self) color.HSV {
134 const f = x.to_float();149 const f = x.to_float();
135 const cmax = @max(f.r, f.g, f.b);150 const cmax = @max(f.r, f.g, f.b);
...@@ -150,12 +165,12 @@ pub fn to_hsv(x: Self) color.HSV {...@@ -150,12 +165,12 @@ pub fn to_hsv(x: Self) color.HSV {
150165
151pub fn to_ycbcr(x: Self) color.YCbCr {166pub fn to_ycbcr(x: Self) color.YCbCr {
152 // zig fmt: off167 // zig fmt: off
153 const f = x.to_vec();168 const r, const g, const b, const a = x.to_vec();
154 return color.YCbCr.from_vec(.{169 return color.YCbCr.from_vec(.{
155 ( 0.299 * f.r + 0.587 * f.g + 0.114 * f.b) + 0,170 ( 0.299 * r + 0.587 * g + 0.114 * b) + 0,
156 (-0.1687 * f.r + -0.3313 * f.g + 0.5 * f.b) + 128,171 (-0.1687 * r + -0.3313 * g + 0.5 * b) + 128,
157 ( 0.5 * f.r + -0.4187 * f.g + -0.0813 * f.b) + 128,172 ( 0.5 * r + -0.4187 * g + -0.0813 * b) + 128,
158 f.a,173 a,
159 });174 });
160 // zig fmt: on175 // zig fmt: on
161}176}
test.zig+28
...@@ -1,8 +1,23 @@...@@ -1,8 +1,23 @@
1const std = @import("std");1const std = @import("std");
2const color = @import("color");2const 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
4test {18test {
5 // https://www.colorhexa.com/4fc3f719 // https://www.colorhexa.com/4fc3f7
20 // https://encycolorpedia.com/4fc3f7
6 const o = color.sRGB.parseHexConst("#4FC3F7");21 const o = color.sRGB.parseHexConst("#4FC3F7");
7 {22 {
8 const r, const g, const b, const a = o.to_array();23 const r, const g, const b, const a = o.to_array();
...@@ -10,15 +25,18 @@ test {...@@ -10,15 +25,18 @@ test {
10 try std.testing.expect(g == 195);25 try std.testing.expect(g == 195);
11 try std.testing.expect(b == 247);26 try std.testing.expect(b == 247);
12 try std.testing.expect(a == 255);27 try std.testing.expect(a == 255);
28 try expect(o.to_array()).toEqual(.{ 79, 195, 247, 255 });
13 }29 }
14 {30 {
15 // 68% cyan, 21.1% magenta, 0% yellow and 3.1% black31 // 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)
16 const c, const m, const y, const k, const a = o.to_cmyk().to_array();33 const c, const m, const y, const k, const a = o.to_cmyk().to_array();
17 try std.testing.expectApproxEqAbs(0.68, c, 0.01);34 try std.testing.expectApproxEqAbs(0.68, c, 0.01);
18 try std.testing.expectApproxEqAbs(0.21, m, 0.01);35 try std.testing.expectApproxEqAbs(0.21, m, 0.01);
19 try std.testing.expectApproxEqAbs(0.00, y, 0.01);36 try std.testing.expectApproxEqAbs(0.00, y, 0.01);
20 try std.testing.expectApproxEqAbs(0.03, k, 0.01);37 try std.testing.expectApproxEqAbs(0.03, k, 0.01);
21 try std.testing.expect(a == 1.0);38 try std.testing.expect(a == 1.0);
39 try expect(o.to_cmyk().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 });
22 }40 }
23 {41 {
24 // 198.6°, 91.3, 63.942 // 198.6°, 91.3, 63.9
...@@ -27,6 +45,7 @@ test {...@@ -27,6 +45,7 @@ test {
27 try std.testing.expectApproxEqAbs(0.913, s, 0.001);45 try std.testing.expectApproxEqAbs(0.913, s, 0.001);
28 try std.testing.expectApproxEqAbs(0.639, l, 0.001);46 try std.testing.expectApproxEqAbs(0.639, l, 0.001);
29 try std.testing.expect(a == 1.0);47 try std.testing.expect(a == 1.0);
48 try expect(o.to_hsl().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 });
30 }49 }
31 {50 {
32 // 198.6°, 68, 96.951 // 198.6°, 68, 96.9
...@@ -35,13 +54,22 @@ test {...@@ -35,13 +54,22 @@ test {
35 try std.testing.expectApproxEqAbs(0.680, s, 0.001);54 try std.testing.expectApproxEqAbs(0.680, s, 0.001);
36 try std.testing.expectApproxEqAbs(0.969, v, 0.001);55 try std.testing.expectApproxEqAbs(0.969, v, 0.001);
37 try std.testing.expect(a == 1.0);56 try std.testing.expect(a == 1.0);
57 try expect(o.to_hsv().to_srgb().to_array()).toEqual(.{ 79, 195, 247, 255 });
38 }58 }
39 {59 {
60 // 0.07818742180518633 0.5457244613701866 0.9301108583754237
40 // 0.0781874218051863, 0.545724461370187, 0.93011085837542461 // 0.0781874218051863, 0.545724461370187, 0.930110858375424
41 const r, const g, const b, const a = o.to_linear_rgb().to_array();62 const r, const g, const b, const a = o.to_linear_rgb().to_array();
42 try std.testing.expectApproxEqAbs(0.078, r, 0.001);63 try std.testing.expectApproxEqAbs(0.078, r, 0.001);
43 try std.testing.expectApproxEqAbs(0.545, g, 0.001);64 try std.testing.expectApproxEqAbs(0.545, g, 0.001);
44 try std.testing.expectApproxEqAbs(0.930, b, 0.001);65 try std.testing.expectApproxEqAbs(0.930, b, 0.001);
45 try std.testing.expect(a == 1.0);66 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 });
46 }74 }
47}75}