From d9ddf3eca3a14c88bdf9c1bdf845f98338bca9eb Mon Sep 17 00:00:00 2001 From: nitrogenez Date: Thu, 21 Sep 2023 21:33:20 +0300 Subject: [PATCH] initiated migration to the new and improved API (check README) --- README.md | 30 ++++---- build.zig | 61 +++++++--------- build.zig.zon | 4 ++ src/colorspaces/hsi.zig | 85 +++++++--------------- src/colorspaces/xyz.zig | 2 +- src/examples/madness.zig | 40 +++++++++++ src/prism.zig | 35 ++++----- src/rainbow.zig | 60 ---------------- src/shades.zig | 40 ----------- src/spaces/HSI.zig | 98 ++++++++++++++++++++++++++ src/spaces/HSV.zig | 106 ++++++++++++++++++++++++++++ src/spaces/RGB.zig | 32 +++++++++ src/spaces/XYZ.zig | 69 ++++++++++++++++++ src/tests.zig | 148 ++++++--------------------------------- 14 files changed, 457 insertions(+), 353 deletions(-) create mode 100644 build.zig.zon create mode 100644 src/examples/madness.zig delete mode 100644 src/rainbow.zig delete mode 100644 src/shades.zig create mode 100644 src/spaces/HSI.zig create mode 100644 src/spaces/HSV.zig create mode 100644 src/spaces/RGB.zig create mode 100644 src/spaces/XYZ.zig diff --git a/README.md b/README.md index 61b5a49..a31fe7e 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,11 @@ Prism is a utility library for managing colors and colorspaces written in Zig. -> **NOTE** -> Prism is still under active development. -> Please, raise an issue, if you encounter any bugs. +> **WARNING** +> There are ongoing major API changes. Please, be cautious +> and follow the development process. All changes are +> documented in the code directly. Old API is already +> set to be deprecated. # Why Prism? Prism is lightweight, fast, and easy to use in general. There is no boilerplate, just import the library, and start using colors and convert them into any of supported colorspaces, including CIE l\*a\*b\*, which is a highly useful colorspace for working with colors that our eyes actually percept. @@ -60,22 +62,24 @@ If you are willing to make Prism better (or worse), you may follow the instructi 9. You're GTG, enjoy your profile pic in a contributors list :) # Colorspace Support -| NAME | STATE | -| ---- | ----------- | -| CMYK | **FULL** | -| HSI | **PARTIAL** | -| HSL | **FULL** | -| LAB | **FULL** | -| YIQ | **PARITAL** | -| HSV | **FULL** | -| RGB | **FULL** | -| XYZ | **FULL** | +| NAME | STATE | NEW | +| ---- | ----------- | ------------ | +| CMYK | **FULL** | **NO** | +| HSI | **PARTIAL** | **IN TODOS** | +| HSL | **FULL** | **FULL** | +| LAB | **FULL** | **IN TODOS** | +| YIQ | **PARITAL** | **NO** | +| HSV | **FULL** | **PARTIAL** | +| RGB | **FULL** | **FULL** | +| XYZ | **FULL** | **FULL** | ### Meaning + **NAME** - Name of the colorspace + **STATE** - A colorspace support state + **FULL** - A full-featured colorspace support + **PARTIAL** - It kinda works, but is lacking functionality + + **NO** - No support at all + + **IN TODOS** - Planned and yet to be implemented # License Prism is licensed under a **BSD-3-Clause "New" or "Revised" License**. See [LICENSE](LICENSE) to learn more. diff --git a/build.zig b/build.zig index 5d3d3c9..be54f44 100644 --- a/build.zig +++ b/build.zig @@ -1,56 +1,43 @@ const std = @import("std"); pub fn build(b: *std.Build) void { - const lib_version = std.SemanticVersion.parse("0.1.4") catch unreachable; - const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const target = b.standardTargetOptions(.{}); - const lib = b.addStaticLibrary(.{ - .name = "prism", - .root_source_file = .{ .path = "src/prism.zig" }, - .target = target, - .optimize = optimize, - .version = lib_version, + _ = b.addModule("prism", .{ + .source_file = .{ .path = "src/prism.zig" }, }); - const lib_shared = b.addSharedLibrary(.{ + const lib = b.addStaticLibrary(.{ .name = "prism", .root_source_file = .{ .path = "src/prism.zig" }, .target = target, .optimize = optimize, - .version = lib_version, - }); - - const prism_mod = b.createModule(.{ .source_file = .{ .path = "src/prism.zig" } }); - - const shades = b.addExecutable(.{ - .name = "shades", - .root_source_file = .{ .path = "src/shades.zig" }, - .target = target, - .optimize = optimize, - }); - - const rainbow = b.addExecutable(.{ - .name = "rainbow", - .root_source_file = .{ .path = "src/rainbow.zig" }, - .target = target, - .optimize = optimize, }); - rainbow.addModule("prism", prism_mod); - shades.addModule("prism", prism_mod); - b.installArtifact(lib); - b.installArtifact(lib_shared); - b.installArtifact(rainbow); - b.installArtifact(shades); + const test_step = b.step("test", "Run tests"); const main_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/tests.zig" }, + .name = "prism-tests", + .root_source_file = .{ .path = "src/prism.zig" }, .target = target, .optimize = optimize, }); - - const run_main_tests = b.addRunArtifact(main_tests); - const test_step = b.step("test", "Run library tests"); - test_step.dependOn(&run_main_tests.step); + main_tests.linkLibrary(lib); + test_step.dependOn(&b.addRunArtifact(main_tests).step); + + const examples = [_]*std.Build.Step.Compile{ + b.addExecutable(.{ + .name = "madness", + .root_source_file = .{ .path = "src/examples/madness.zig" }, + .target = target, + .optimize = optimize, + }), + }; + + for (examples) |e| { + e.addModule("prism", b.modules.get("prism").?); + e.linkLibrary(lib); + b.installArtifact(e); + } } diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..6f1b5da --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,4 @@ +.{ + .name = "prism", + .version = "0.2.0", +} diff --git a/src/colorspaces/hsi.zig b/src/colorspaces/hsi.zig index 6379f89..5925baa 100644 --- a/src/colorspaces/hsi.zig +++ b/src/colorspaces/hsi.zig @@ -7,54 +7,34 @@ pub const HSI = struct { s: f32 = 0.0, i: f32 = 0.0, - // Broken. pub fn toRGB(self: *const HSI) RGB { - var o = RGB{}; + var rgb: [3]f32 = .{ 0.0, 0.0, 0.0 }; - const i = self.i; - const s = self.s; const h = self.h; + const s = self.s; + const i = self.i; const is = i * s; if (h < 0.000001) { - o = .{ - .r = i + (is * is), - .g = i - is, - .b = i - is, - }; + rgb = .{ i + is * is, i - is, i - is }; } else if (0.0 < h and h < 120.0) { - o = .{ - .r = i + is * @cos(h) / @cos(60 - h), - .g = i + is * (1.0 - @cos(h) / @cos(60 - h)), - .b = i - is, - }; + rgb[0] = i + is * @cos(h) / @cos(60.0 - h); + rgb[1] = i + is * (1.0 - @cos(h) / @cos(60.0 - h)); + rgb[2] = i - is; } else if (h >= 120.000005 and h <= 120.5) { - o = .{ - .r = i - is, - .g = i + (is * is), - .b = i - is, - }; + rgb = .{ i - is, i + is * is, i - is }; } else if (120.0 < h and h < 240.0) { - o = .{ - .r = i - is, - .g = i + is * @cos(h - 120.0) / @cos(180.0 - h), - .b = i + is * (1.0 - @cos(h - 120.0) / @cos(180.0 - h)), - }; + rgb[0] = i - is; + rgb[1] = i + is * @cos(h - 120.0) / @cos(180.0 - h); + rgb[2] = i + is * (1.0 - @cos(h - 120.0) / @cos(180.0 - h)); } else if (h >= 240.000005 and h <= 240.5) { - o = .{ .r = i - is, .g = i - is, .b = i + (is * is) }; + rgb = .{ i - is, i - is, i + is * is }; } else { - o = .{ - .r = i + is * (1.0 - @cos(h - 240) / @cos(300.0 - h)), - .g = i - is, - .b = i + is * @cos(h - 240.0) / @cos(300.0 - h), - }; + rgb[0] = i + is * (1.0 - @cos(h - 240.0) / @cos(300.0 - h)); + rgb[1] = i - is; + rgb[2] = i + is * (@cos(h - 240.0) / @cos(300.0 - h)); } - - o.r = math.clamp(o.r, 0.0, 255.0); - o.g = math.clamp(o.g, 0.0, 255.0); - o.b = math.clamp(o.b, 0.0, 255.0); - - return o; + return rgb; } pub fn fromRGB(from: *const RGB) HSI { @@ -64,36 +44,25 @@ pub const HSI = struct { const min = @min(n.r, n.g, n.b); const d = max - min; - var o = HSI{}; + var hsi: [3]f32 = .{ 0.0, 0.0, 0.0 }; - if (d < 0.00001) { - o.s = 0; - o.h = 0; - return o; - } + if (d < 0.00001 or max < 0.00001) return hsi; - if (max > 0.0) { - o.s = (d / max); - } else { - o.s = 0.0; - o.h = 0.0; - return o; - } + hsi[1] = (d / max); if (n.r >= max) { - o.h = (n.g - n.b) / d; + hsi[0] = (n.g - n.b) / d; } else if (n.g >= max) { - o.h = 2.0 + (n.b - n.r) / d; + hsi[0] = 2.0 + (n.b - n.r) / d; } else { - o.h = 4.0 + (n.r - n.g) / d; + hsi[0] = 4.0 + (n.r - n.g) / d; } - o.h *= 60.0; - o.i = (n.r + n.g + n.b) / 3; + hsi[0] *= 60.0; + hsi[2] = (n.r + n.g + n.b) / 3; - if (o.h < 0.0) { - o.h = 360.0; - } - return o; + if (hsi[0] < 0.0) hsi[0] = 360.0; + + return hsi; } }; diff --git a/src/colorspaces/xyz.zig b/src/colorspaces/xyz.zig index 01ef6f9..2de558b 100644 --- a/src/colorspaces/xyz.zig +++ b/src/colorspaces/xyz.zig @@ -24,7 +24,7 @@ pub const XYZ = struct { } for (mat, 0..) |k, i| { - xyz[i] = (rgb[0] * k[0]) + (rgb[1] * k[1]) * (rgb[2] * k[2]); + xyz[i] = rgb[0] * k[0] + rgb[1] * k[1] * (rgb[2] * k[2]); } return .{ .x = xyz[0], .y = xyz[1], .z = xyz[2] }; } diff --git a/src/examples/madness.zig b/src/examples/madness.zig new file mode 100644 index 0000000..4f64337 --- /dev/null +++ b/src/examples/madness.zig @@ -0,0 +1,40 @@ +const std = @import("std"); +const prism = @import("prism"); + +pub fn main() !void { + var alloc = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer alloc.deinit(); + + const args = try std.process.argsAlloc(alloc.allocator()); + defer std.process.argsFree(alloc.allocator(), args); + + var stdout = std.io.getStdOut().writer(); + var madness_text: []const u8 = "MADNESS!"; + + if (args.len > 1) { + for (args[1..]) |arg| { + madness_text = arg; + } + } + + var rng = std.rand.DefaultPrng.init(@as( + u64, + @truncate(@as(u128, @bitCast(std.time.nanoTimestamp()))), + )); + + while (true) { + const rgb = prism.RGB{ + .r = rng.random().float(f64), + .g = rng.random().float(f64), + .b = rng.random().float(f64), + }; + const c = rgb.as8bitArray(); + + std.time.sleep(90 * std.time.ns_per_ms); + try stdout.print( + "\r\x1b[38;2;{d:.0};{d:.0};{d:.0}m{s}\x1b[0m", + .{ c[0], c[1], c[2], madness_text }, + ); + } + _ = try stdout.write("\x1b[0m\n"); +} diff --git a/src/prism.zig b/src/prism.zig index 585104c..5491a32 100644 --- a/src/prism.zig +++ b/src/prism.zig @@ -1,5 +1,10 @@ const std = @import("std"); +pub const RGB = @import("spaces/RGB.zig"); +pub const HSI = @import("spaces/HSI.zig"); +pub const HSV = @import("spaces/HSV.zig"); +pub const XYZ = @import("spaces/XYZ.zig"); + pub const spaces = struct { pub const RGB = @import("colorspaces/rgb.zig").RGB; pub const HSL = @import("colorspaces/hsl.zig").HSL; @@ -12,21 +17,19 @@ pub const spaces = struct { }; pub const colors = struct { - const RGB = spaces.RGB; - - pub const Red: RGB = .{ .r = 255 }; - pub const Green: RGB = .{ .g = 255 }; - pub const Blue: RGB = .{ .b = 255 }; - pub const Black: RGB = .{}; - pub const White: RGB = .{ .r = 255, .g = 255, .b = 255 }; + pub const Red: spaces.RGB = .{ .r = 255 }; + pub const Green: spaces.RGB = .{ .g = 255 }; + pub const Blue: spaces.RGB = .{ .b = 255 }; + pub const Black: spaces.RGB = .{}; + pub const White: spaces.RGB = .{ .r = 255, .g = 255, .b = 255 }; - pub const CalmingCoral: RGB = .{ .r = 233, .g = 150, .b = 122 }; - pub const VelvetViolet: RGB = .{ .r = 128, .b = 128 }; - pub const PacificPink: RGB = .{ .r = 219, .g = 112, .b = 147 }; - pub const Pink: RGB = .{ .r = 255, .g = 192, .b = 203 }; - pub const MistyRose1: RGB = .{ .r = 255, .g = 228, .b = 225 }; - pub const Linen: RGB = .{ .r = 250, .g = 240, .b = 230 }; - pub const SteelBlue: RGB = .{ .r = 70, .g = 130, .b = 180 }; - pub const StrongAzure: RGB = .{ .g = 87, .b = 184 }; - pub const Gold1: RGB = .{ .r = 255, .g = 215 }; + pub const CalmingCoral: spaces.RGB = .{ .r = 233, .g = 150, .b = 122 }; + pub const VelvetViolet: spaces.RGB = .{ .r = 128, .b = 128 }; + pub const PacificPink: spaces.RGB = .{ .r = 219, .g = 112, .b = 147 }; + pub const Pink: spaces.RGB = .{ .r = 255, .g = 192, .b = 203 }; + pub const MistyRose1: spaces.RGB = .{ .r = 255, .g = 228, .b = 225 }; + pub const Linen: spaces.RGB = .{ .r = 250, .g = 240, .b = 230 }; + pub const SteelBlue: spaces.RGB = .{ .r = 70, .g = 130, .b = 180 }; + pub const StrongAzure: spaces.RGB = .{ .g = 87, .b = 184 }; + pub const Gold1: spaces.RGB = .{ .r = 255, .g = 215 }; }; diff --git a/src/rainbow.zig b/src/rainbow.zig deleted file mode 100644 index b49de60..0000000 --- a/src/rainbow.zig +++ /dev/null @@ -1,60 +0,0 @@ -const std = @import("std"); -const prism = @import("prism"); - -const print = std.debug.print; - -const RGB = prism.spaces.RGB; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); - - var text: []const u8 = "Rainbow!"; - var once: bool = false; - - if (args.len > 1) { - for (args[1..]) |arg| { - if (arg[0] == '-') { - if (arg[1] == '-') { - once = std.mem.eql(u8, arg[2..], "once"); - } - } else { - text = arg; - } - } - } - - var cols = std.ArrayList(RGB).init(std.heap.page_allocator); - defer cols.deinit(); - - try cols.append(.{ .r = 255 }); - try cols.append(.{ .r = 255, .g = 127 }); - try cols.append(.{ .r = 255, .g = 255 }); - try cols.append(.{ .g = 255 }); - try cols.append(.{ .b = 255 }); - try cols.append(.{ .r = 75, .b = 211 }); - try cols.append(.{ .r = 148, .b = 211 }); - - if (once) { - const seed = @as(u64, @truncate(@as(u128, @bitCast(std.time.nanoTimestamp())))); - var prng = std.rand.DefaultPrng.init(seed); - - var i = prng.random().intRangeAtMost(usize, 0, cols.items.len - 1); - var c = cols.items[i]; - - print("\r\x1b[38;2;{d:.0};{d:.0};{d:.0}m{s}\x1b[0m\n", .{ c.r, c.g, c.b, text }); - return; - } - - while (true) { - for (cols.items) |c| { - std.time.sleep(60 * std.time.ns_per_ms); - print("\r\x1b[38;2;{d:.0};{d:.0};{d:.0}m{s}\x1b[0m", .{ c.r, c.g, c.b, text }); - } - } - print("\n"); -} diff --git a/src/shades.zig b/src/shades.zig deleted file mode 100644 index 328d8a2..0000000 --- a/src/shades.zig +++ /dev/null @@ -1,40 +0,0 @@ -const std = @import("std"); -const mem = std.mem; -const prism = @import("prism"); - -const print = std.debug.print; - -pub fn main() !void { - const start = prism.colors.SteelBlue; - const start1 = prism.colors.Gold1; - - var cols = std.ArrayList(prism.spaces.RGB).init(std.heap.page_allocator); - var cols1 = std.ArrayList(prism.spaces.RGB).init(std.heap.page_allocator); - - defer cols.deinit(); - defer cols1.deinit(); - - var factor: f32 = 0.0; - for (0..9) |i| { - _ = i; - factor += 0.1; - try cols.append(start.darken(factor)); - } - - for (cols.items, 0..) |c, i| { - print("\x1b[48;2;{d:.0};{d:.0};{d:.0}m {d} \x1b[0m", .{ c.r, c.g, c.b, i }); - } - print("\x1b[38;2;{d:.0};{d:.0};{d:.0}m I <3\x1b[0m\n", .{ start.r, start.g, start.b }); - - factor = 0.0; - for (0..9) |i| { - _ = i; - factor += 0.1; - try cols1.append(start1.darken(factor)); - } - - for (cols1.items, 0..) |c, i| { - print("\x1b[48;2;{d:.0};{d:.0};{d:.0}m {d} \x1b[0m", .{ c.r, c.g, c.b, i }); - } - print("\x1b[38;2;{d:.0};{d:.0};{d:.0}m Ukraine :3\x1b[0m\n", .{ start1.r, start1.g, start1.b }); -} diff --git a/src/spaces/HSI.zig b/src/spaces/HSI.zig new file mode 100644 index 0000000..8c0e30a --- /dev/null +++ b/src/spaces/HSI.zig @@ -0,0 +1,98 @@ +const std = @import("std"); +const RGB = @import("RGB.zig"); +const Self = @This(); + +/// Returned by any conversion function here. +/// +/// + `NotNormalized` means that the values are not ranging from 0 to 1 +const ConversionError = error{ + NotNormalized, +}; + +/// Hue value in degrees (0..360) +h: f64 = 0.0, + +/// Saturation value (0..1) +s: f64 = 0.0, + +/// Intensity value (0..1) +i: f64 = 0.0, + +/// Convert RGB color to HSI. +/// Returns `ConversionError` if something goes wrong. +pub fn initFromRgb(rgb: *const RGB) ConversionError!Self { + // Values must be normalized beforehand + if (!rgb.isNormalized()) { + return ConversionError.NotNormalized; + } + + // Calculating the max and min color value and the difference between them. + const max = @max(rgb.r, rgb.g, rgb.b); + const min = @min(rgb.r, rgb.g, rgb.b); + const delta = max - min; + + // Initializing from the list, just to be verbose. + var hsi: [3]f64 = .{ 0.0, 0.0, 0.0 }; + + // If color values are undefined or the color is black. + if (delta < 0.00001 or max < 0.00001) + return .{ .h = hsi[0], .s = hsi[1], .i = hsi[2] }; + + hsi[1] = (delta / max); + + // Finding out which color is the max + if (rgb.r >= max) { + hsi[0] = (rgb.g - rgb.b) / delta; + } else if (rgb.g >= max) { + hsi[0] = (rgb.b - rgb.r) / delta + 2.0; + } else { + hsi[0] = (rgb.r - rgb.g) / delta + 4.0; + } + + hsi[0] *= 60.0; + hsi[2] = (rgb.r + rgb.g + rgb.b) / 3.0; + + if (hsi[0] < 0.0) hsi[0] = 360.0; + return .{ .h = hsi[0], .s = hsi[1], .i = hsi[2] }; +} + +pub fn asRgb(self: *const Self) RGB { + // I don't know about you, but working with arrays is much easier. + var rgb: [3]f64 = .{ 0.0, 0.0, 0.0 }; + var hsi: [3]f64 = self.asArray(); + + const is = hsi[2] * hsi[1]; + + // "I wish there was a better way..." + // (c) YandereDev + if (hsi[0] < 0.00001) { + rgb = .{ hsi[2] + 2.0 * is, hsi[2] - is, hsi[2] - is }; + } else if (hsi[0] > 0.00001 and hsi[0] < 120.0) { + rgb[0] = hsi[2] + is * @cos(hsi[0]) / @cos(60.0 - hsi[0]); + rgb[1] = hsi[2] + is * (1.0 - @cos(hsi[0]) / @cos(60.0 - hsi[0])); + rgb[2] = hsi[2] - is; + } else if (hsi[0] >= 120.00005 and hsi[0] <= 120.5) { + rgb = .{ hsi[2] - is, hsi[2] + 2.0 * is, hsi[2] - is }; + } else if (hsi[0] > 120.0 and hsi[0] < 240.0) { + rgb[0] = hsi[2] - is; + rgb[1] = hsi[2] - is * (@cos(hsi[0] - 120.0) / @cos(180.0 - hsi[0])); + rgb[2] = hsi[2] + is * (1.0 - @cos(hsi[0] - 120.0) / @cos(180.0 - hsi[0])); + } else if (hsi[0] >= 240.00005 and hsi[0] <= 240.5) { + rgb = .{ hsi[2] - is, hsi[2] - is, hsi[2] + 2.0 * is }; + } else { + rgb[0] = hsi[2] + is * (1.0 - @cos(hsi[0] - 240.0) / @cos(300.0 - hsi[0])); + rgb[1] = hsi[2] - is; + rgb[2] = hsi[2] + is * (@cos(hsi[0] - 240.0) / @cos(300.0 - hsi[0])); + } + return .{ .r = rgb[0], .g = rgb[1], .b = rgb[0] }; +} + +/// Convert `rgb` to HSI. That's just a wrapper for `initFromRgb()`. +pub fn init(rgb: [3]f64) ConversionError!Self { + return initFromRgb(&RGB{ .r = rgb[0], .g = rgb[1], .b = rgb[2] }); +} + +/// Returns `self` as an array. +pub fn asArray(self: *const Self) [3]f64 { + return .{ self.h, self.s, self.i }; +} diff --git a/src/spaces/HSV.zig b/src/spaces/HSV.zig new file mode 100644 index 0000000..3c2cc50 --- /dev/null +++ b/src/spaces/HSV.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const RGB = @import("RGB.zig"); +const Self = @This(); + +h: f64 = 0.0, +s: f64 = 0.0, +v: f64 = 0.0, + +pub fn initFromRgb(rgb: *const RGB) !Self { + if (!rgb.isNormalized()) { + return error.NotNormalized; + } + + // Calculating the max and min color value and the difference between them. + const max = @max(rgb.r, rgb.g, rgb.b); + const min = @min(rgb.r, rgb.g, rgb.b); + const delta = max - min; + + var hsv: [3]f64 = .{ 0.0, 0.0, 0.0 }; + + // If color values are undefined or the color is black. + if (delta < 0.00001 or max < 0.00001) + return .{ .h = hsv[0], .s = hsv[1], .v = hsv[2] }; + + hsv[1] = (delta / max); + + // Finding out which color is the max + if (rgb.r >= max) { + hsv[0] = (rgb.g - rgb.b) / delta; + } else if (rgb.g >= max) { + hsv[0] = (rgb.b - rgb.r) / delta + 2.0; + } else { + hsv[0] = (rgb.r - rgb.g) / delta + 4.0; + } + hsv[0] *= 60.0; + hsv[2] = max; + + if (hsv[0] < 0.0) hsv[0] = 360.0; + return .{ .h = hsv[0], .s = hsv[1], .i = hsv[2] }; +} + +pub fn asRgb(self: *const Self) RGB { + var hh: f64 = 0; + var p: f64 = 0; + var q: f64 = 0; + var t: f64 = 0; + var ff: f64 = 0; + var i: f64 = 0; + + var rgb: RGB = .{}; + + if (self.s <= 0.0) { + rgb.r = self.v; + rgb.g = self.v; + rgb.b = self.v; + return rgb; + } + + hh = self.h; + + if (hh >= 360.0) hh = 0.0; + + hh /= 60.0; + + i = hh; + ff = hh - i; + + p = self.v * (1.0 - self.s); + q = self.v * (1.0 - (self.s * ff)); + t = self.v * (1.0 - (self.s * (1.0 - ff))); + + switch (@as(usize, @intFromFloat(std.math.floor(i)))) { + 0 => { + rgb.r = self.v; + rgb.g = t; + rgb.b = p; + }, + 1 => { + rgb.r = q; + rgb.g = self.v; + rgb.b = t; + }, + 2 => { + rgb.r = p; + rgb.g = self.v; + rgb.b = t; + }, + 3 => { + rgb.r = p; + rgb.g = q; + rgb.b = self.v; + }, + 4 => { + rgb.r = t; + rgb.g = p; + rgb.b = self.v; + }, + 5 => {}, + else => { + rgb.r = self.v; + rgb.g = p; + rgb.b = q; + }, + } + return rgb; +} diff --git a/src/spaces/RGB.zig b/src/spaces/RGB.zig new file mode 100644 index 0000000..eca886c --- /dev/null +++ b/src/spaces/RGB.zig @@ -0,0 +1,32 @@ +const std = @import("std"); +const Self = @This(); + +/// Red value (0..1) +r: f64 = 0.0, + +/// Green value (0..1) +g: f64 = 0.0, + +/// Blue value (0..1) +b: f64 = 0.0, + +/// Returns `Self` as an array of values ranging from 0 to 255 +pub fn as8bitArray(self: *const Self) [3]f64 { + return .{ self.r * 255.0, self.g * 255.0, self.b * 255.0 }; +} + +/// Returns a copy of `Self` with values ranging from 0 to 255 +pub fn as8bit(self: *const Self) Self { + return .{ .r = self.r * 255.0, .g = self.g * 255.0, .b = self.b * 255.0 }; +} + +pub fn asArray(self: *const Self) [3]f64 { + return .{ self.r, self.g, self.b }; +} + +/// Returns `true` if the values are in a range from 0 to 1 +pub fn isNormalized(self: *const Self) bool { + return (self.r >= 0.0 and self.r <= 1.0) and + (self.g >= 0.0 and self.g <= 1.0) and + (self.b >= 0.0 and self.b <= 1.0); +} diff --git a/src/spaces/XYZ.zig b/src/spaces/XYZ.zig new file mode 100644 index 0000000..8eee81c --- /dev/null +++ b/src/spaces/XYZ.zig @@ -0,0 +1,69 @@ +const std = @import("std"); +const RGB = @import("RGB.zig"); +const Self = @This(); + +const ConversionError = error{ + NotNormalized, +}; + +/// This is a matrix used to convert RGB values to XYZ. +pub const from_rgb_matrix = [3][3]f64{ + .{ 0.4124564, 0.3575761, 0.1804375 }, + .{ 0.2126729, 0.7151522, 0.0721750 }, + .{ 0.0193339, 0.1191920, 0.9503041 }, +}; + +/// And that matrix is used to convert XYZ values to RGB. +pub const to_rgb_matrix = [3][3]f64{ + .{ 3.2404542, -1.5371385, -0.4985314 }, + .{ -0.9692660, 1.8760108, 0.0415560 }, + .{ 0.0556434, -0.2040259, 1.0572252 }, +}; + +x: f64 = 0.0, +y: f64 = 0.0, +z: f64 = 0.0, + +/// Turns `self` into `[3]f64`. +pub fn asArray(self: *const Self) [3]f64 { + return .{ self.x, self.y, self.z }; +} + +/// This function is used to create XYZ values from RGB +pub fn initFromRgb(rgb: *const RGB) ConversionError!Self { + if (!rgb.isNormalized()) { + return ConversionError.NotNormalized; + } + + var rgb_: [3]f64 = rgb.asArray(); + var xyz: [3]f64 = .{ 0.0, 0.0, 0.0 }; + + for (rgb_, 0..) |j, i| { + rgb_[i] = if (j <= 0.0405) j / 12.92 else std.math.pow(f64, (j + 0.055) / 1.055, 2.4); + } + + for (from_rgb_matrix, 0..) |j, i| { + xyz[i] = (rgb_[0] * j[0]) + (rgb_[1] * j[1]) + (rgb_[2] * j[2]); + } + return .{ .x = xyz[0], .y = xyz[1], .z = xyz[2] }; +} + +/// Converts XYZ value to RGB +pub fn asRgb(self: *const Self) RGB { + const xyz = self.asArray(); + var rgb: [3]f64 = .{ 0.0, 0.0, 0.0 }; + + for (to_rgb_matrix, 0..) |j, i| { + rgb[i] = (xyz[0] * j[0]) + (xyz[1] * j[1]) + (xyz[2] * j[2]); + } + + for (rgb, 0..) |j, i| { + rgb[i] = if (j <= 0.0031308) j * 12.92 else 1.055 * std.math.pow(f64, j, 1.0 / 2.4) - 0.55; + } + return .{ .r = rgb[0], .g = rgb[1], .b = rgb[2] }; +} + +/// Create XYZ value from RGB value array. Just a wrapper for `initFromRgb()`. +pub fn init(rgb: [3]f64) Self { + return initFromRgb(&RGB{ rgb[0], rgb[1], rgb[2] }); +} diff --git a/src/tests.zig b/src/tests.zig index 3f76b76..9c235f8 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -1,142 +1,34 @@ const std = @import("std"); -const mem = std.mem; const testing = std.testing; -const print = std.debug.print; -const prism = @import("prism.zig"); -const spaces = prism.spaces; +const RGB = @import("spaces/RGB.zig"); +const HSI = @import("spaces/HSI.zig"); +const XYZ = @import("spaces/XYZ.zig"); -test "RGB to HEX conversion" { - const c = spaces.RGB{ .r = 40, .g = 40, .b = 40 }; - const hex = c.toHEX(); - - var buf: [7]u8 = undefined; - _ = try std.fmt.bufPrint(&buf, "#{x}", .{hex}); - - try testing.expectEqualStrings("#282828", &buf); -} - -test "RGB to HSL conversion" { - const c = prism.colors.Red; - - const hsl = c.toHSL(); - const expected = spaces.HSL{ .h = 0.0, .s = 1.0, .l = 0.5 }; - - try testing.expect((hsl.h == expected.h) and - (hsl.s == expected.s) and - (hsl.l == expected.l)); -} - -test "RGB->HSV" { - const c = prism.colors.Red; - const o = c.toHSV(); - const e = spaces.HSV{ .h = 0.0, .s = 1.0, .v = 1.0 }; - - print("\n Input: RGB ({d}, {d}, {d})\n", .{ c.r, c.g, c.b }); - print(" Output: HSV ({d:.2}, {d:.2}, {d:.2})\n", .{ o.h, o.s, o.v }); - - try testing.expect((o.h == e.h) and (o.s == e.s) and (o.v == e.v)); -} - -test "RGB->YIQ" { - const c = prism.colors.Red; - const o = c.toYIQ(); - const e = spaces.YIQ{ .y = 0.30, .i = 0.60, .q = 0.21 }; - _ = e; - - print("\n Input: RGB ({d:.0}, {d:.0}, {d:.0})\n", .{ c.r, c.g, c.b }); - print(" Output: YIQ ({d:.2}, {d:.2}, {d:.2})\n", .{ o.y, o.i, o.q }); - - return error.SkipZigTest; - // try testing.expect((o.y == e.y) and (o.i == e.i) and (o.q == e.q)); -} - -test "RGB->CMYK" { - const c = prism.colors.Red; - const o = spaces.CMYK.fromRGB(&c); - const e = spaces.CMYK{ .c = 0.0, .m = 1.0, .y = 1.0, .k = 0.0 }; - - print("\n Input : RGB ({d:.0}, {d:.0}, {d:.0})\n", .{ c.r, c.g, c.b }); - print(" Output: CMYK ({d:.2}, {d:.2}, {d:.2}, {d:.2})\n", .{ o.c, o.m, o.y, o.k }); - - try testing.expect((o.c == e.c) and (o.m == e.m) and (o.y == e.y) and (o.k == e.k)); -} - -test "RGB->HSI" { - const c = prism.colors.Red; - const o = spaces.HSI.fromRGB(&c); - const e = spaces.HSI{ .h = 0.0, .s = 1.0, .i = 0.3333 }; - - print("\n Input : RGB ({d:.0}, {d:.0}, {d:.0})\n", .{ c.r, c.g, c.b }); - print(" Output: HSI ({d:.2}, {d:.2}, {d:.2})\n", .{ o.h, o.s, o.i }); - - try testing.expect((o.h == e.h) and (o.s == e.s) and - (o.i >= e.i - 0.05 and o.i <= e.i + 0.05)); -} - -test "HSI->RGB" { - const c = spaces.HSI.fromRGB(&prism.colors.Red); - const o = c.toRGB(); - const e = prism.colors.Red; - _ = e; - - print("\n Input : HSI ({d:.0}, {d:.0}, {d:.0})\n", .{ c.h, c.s, c.i }); - print(" Output: RGB ({d:.0}, {d:.0}, {d:.0})\n", .{ o.r, o.g, o.b }); - - return error.SkipZigTest; -} - -test "RGB->LAB" { - const c = prism.colors.Red; - const o = spaces.LAB.fromRGB(&c); - const e = spaces.LAB{ .l = 53.24, .a = 80.09, .b = 67.20 }; - - print("\n Input : RGB ({d:.0}, {d:.0}, {d:.0})\n", .{ c.r, c.g, c.b }); - print(" Output: LAB ({d:.2}, {d:.2}, {d:.2})\n", .{ o.l, o.a, o.b }); - - try testing.expect((o.l <= e.l + 0.05 and o.l >= e.l - 0.05) and - (o.a <= e.a + 0.05 and o.l >= e.l - 0.05) and - (o.b <= e.b + 0.05 and o.b >= e.b - 0.05)); -} - -test "LAB->RGB" { - const c = prism.colors.Red; - const lab = spaces.LAB.fromRGB(&c); - const o = lab.toRGB(); - - print("\n Input : LAB ({d:.2}, {d:.2}, {d:.2})\n", .{ lab.l, lab.a, lab.b }); - print(" Output: RGB ({d:.2}, {d:.2}, {d:.2})\n", .{ o.r, o.g, o.b }); - - try testing.expect((o.r == c.r) and (o.g == c.g) and (o.b == c.b)); +fn feql(lhs: f64, rhs: f64) bool { + return std.math.fabs(lhs - rhs) < std.math.floatEps(f64); } -test "ASCII color printing" { - const r = prism.colors.Red; - const g = prism.colors.Green; - const b = prism.colors.Blue; - const vv = prism.colors.VelvetViolet; - const pp = prism.colors.PacificPink; +test "HSI.initFromRgb" { + const act = try HSI.initFromRgb(&RGB{ .r = 1.0 }); + const exp = HSI{ .h = 0.0, .s = 1.0, .i = 0.3333333333333333 }; - print("\n\x1b[38;2;{d:.0};{d:.0};{d:.0}m Red\x1b[0m", .{ r.r, r.g, r.b }); - print("\x1b[38;2;{d:.0};{d:.0};{d:.0}m Green\x1b[0m", .{ g.r, g.g, g.b }); - print("\x1b[38;2;{d:.0};{d:.0};{d:.0}m Blue\x1b[0m", .{ b.r, b.g, b.b }); - print("\x1b[38;2;{d:.0};{d:.0};{d:.0}m Velvet Violet\x1b[0m", .{ vv.r, vv.g, vv.b }); - print("\x1b[38;2;{d:.0};{d:.0};{d:.0}m Pacific Pink\x1b[0m\n", .{ pp.r, pp.g, pp.b }); + try testing.expect(feql(act.h, exp.h) and + feql(act.s, exp.s) and + feql(act.i, exp.i)); } -test "sRGB->CIE XYZ" { - const c = prism.colors.Red; - const o = spaces.XYZ.fromRGB(&c); +test "HSI.asRgb" { + const act = HSI.asRgb(&HSI{ .h = 0.0, .s = 1.0, .i = 0.3333333333333333 }); + const exp = RGB{ .r = 1.0 }; - std.debug.print("\n{d}, {d}, {d}\n", .{ o.x, o.y, o.z }); + try testing.expect(exp.r == act.r); } -test "CIE XYZ->sRGB" { - const c = spaces.XYZ.fromRGB(&prism.colors.Red); - const o = c.toRGB(); - const e = prism.colors.Red; +test "XYZ.initFromRgb" { + const act = try XYZ.initFromRgb(&RGB{ .r = 1.0 }); + const exp = XYZ{ .x = 4.124564e-01, .y = 2.126729e-01, .z = 1.93339e-02 }; - print("\n{d}, {d}, {d}\n", .{ c.x, c.y, c.z }); - print("{d}, {d}, {d}\n", .{ e.r, e.g, e.b }); - print("{d}, {d}, {d}\n", .{ o.r, o.g, o.b }); + try testing.expect(feql(act.x, exp.x) and + feql(act.y, exp.y) and feql(act.z, exp.z)); }