| author | |
| committer | |
| log | 36f4989aee0406ab6ea7802fdac1fe8a45ea89d7 |
| tree | 914ab0bd00225af61041b9153c84f4cb42d6d29c |
| parent | 8c09516944357d85f4ffa3d7a1421f96f63ac420 |
7 files changed, 120 insertions(+), 11 deletions(-)
CMYK.zig+10| ... | @@ -2,6 +2,7 @@ | ... | @@ -2,6 +2,7 @@ |
| 2 | 2 | ||
| 3 | const std = @import("std"); | 3 | const std = @import("std"); |
| 4 | const Self = @This(); | 4 | const Self = @This(); |
| 5 | const color = @import("./mod.zig"); | ||
| 5 | 6 | ||
| 6 | c: f32, // cyan | 7 | c: f32, // cyan |
| 7 | m: f32, // magenta | 8 | m: 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 | ||
| 43 | pub 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 @@ |
| 2 | 2 | ||
| 3 | const std = @import("std"); | 3 | const std = @import("std"); |
| 4 | const Self = @This(); | 4 | const Self = @This(); |
| 5 | const color = @import("./mod.zig"); | ||
| 5 | const _x = @import("./_x.zig"); | 6 | const _x = @import("./_x.zig"); |
| 6 | 7 | ||
| 7 | h: f32, // hue | 8 | h: 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 | } |
| 28 | 29 | ||
| 29 | pub usingnamespace _x.mixin(@This(), f32, .h, .s, .l); | 30 | pub 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 | ||
| 34 | pub 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 @@ |
| 2 | 2 | ||
| 3 | const std = @import("std"); | 3 | const std = @import("std"); |
| 4 | const Self = @This(); | 4 | const Self = @This(); |
| 5 | const color = @import("./mod.zig"); | ||
| 5 | const _x = @import("./_x.zig"); | 6 | const _x = @import("./_x.zig"); |
| 6 | 7 | ||
| 7 | h: f32, // hue | 8 | h: 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 | } |
| 28 | 29 | ||
| 29 | pub usingnamespace _x.mixin(@This(), f32, .h, .s, .v); | 30 | pub 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 | ||
| 34 | pub 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 @@ |
| 2 | 2 | ||
| 3 | const std = @import("std"); | 3 | const std = @import("std"); |
| 4 | const Self = @This(); | 4 | const Self = @This(); |
| 5 | const color = @import("./mod.zig"); | ||
| 5 | const _x = @import("./_x.zig"); | 6 | const _x = @import("./_x.zig"); |
| 7 | const vec4 = @Vector(4, f32); | ||
| 6 | 8 | ||
| 7 | r: f32, // red | 9 | r: f32, // red |
| 8 | g: f32, // green | 10 | g: 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 | } |
| 28 | 30 | ||
| 31 | // https://www.w3.org/TR/WCAG/#dfn-relative-luminance | ||
| 29 | pub fn relative_luminance(x: Self) f32 { | 32 | pub 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 | } |
| 32 | 35 | ||
| 33 | pub usingnamespace _x.mixin(@This(), f32, .r, .g, .b); | 36 | pub usingnamespace _x.mixin(@This(), f32, .r, .g, .b); |
| 37 | |||
| 38 | // https://gamedev.stackexchange.com/a/194038 | ||
| 39 | pub 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 | } |
| 29 | 29 | ||
| 30 | pub usingnamespace _x.mixin(@This(), .y, .cb, .cr); | 30 | pub usingnamespace _x.mixin(@This(), u8, .y, .cb, .cr); |
| 31 | 31 | ||
| 32 | pub fn to_srgb(x: Self) color.sRGB { | 32 | pub fn to_srgb(x: Self) color.sRGB { |
| 33 | // zig fmt: off | 33 | // 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: on | 41 | // 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, |
| 75 | 75 | ||
| 76 | pub usingnamespace _x.mixin(@This(), u8, .r, .g, .b); | 76 | pub usingnamespace _x.mixin(@This(), u8, .r, .g, .b); |
| 77 | 77 | ||
| 78 | // https://www.w3.org/TR/WCAG/#dfn-relative-luminance | ||
| 79 | // https://webstore.iec.ch/publication/6169 | ||
| 78 | pub fn to_linear_rgb(x: Self) color.LinearRgb { | 80 | pub 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 | } |
| 104 | 106 | ||
| 107 | pub 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 | ||
| 105 | pub fn to_cmyk(x: Self) color.CMYK { | 118 | pub 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 | } |
| 114 | 127 | ||
| 128 | // https://www.rapidtables.com/convert/color/rgb-to-hsl.html | ||
| 115 | pub fn to_hsl(x: Self) color.HSL { | 129 | pub 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 | } |
| 132 | 146 | ||
| 147 | // https://www.rapidtables.com/convert/color/rgb-to-hsv.html | ||
| 133 | pub fn to_hsv(x: Self) color.HSV { | 148 | pub 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 { |
| 150 | 165 | ||
| 151 | pub fn to_ycbcr(x: Self) color.YCbCr { | 166 | pub fn to_ycbcr(x: Self) color.YCbCr { |
| 152 | // zig fmt: off | 167 | // 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: on | 175 | // zig fmt: on |
| 161 | } | 176 | } |
test.zig+28| ... | @@ -1,8 +1,23 @@ | ... | @@ -1,8 +1,23 @@ |
| 1 | const std = @import("std"); | 1 | const std = @import("std"); |
| 2 | const color = @import("color"); | 2 | const color = @import("color"); |
| 3 | 3 | ||
| 4 | fn expect(actual: anytype) Expect(@TypeOf(actual)) { | ||
| 5 | return .{ .actual = actual }; | ||
| 6 | } | ||
| 7 | |||
| 8 | fn 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 | |||
| 4 | test { | 18 | test { |
| 5 | // https://www.colorhexa.com/4fc3f7 | 19 | // 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% black | 31 | // 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.9 | 42 | // 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.9 | 51 | // 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.930110858375424 | 61 | // 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 | } |