From c15ec954c096135e59f3ec1d80c1b543515048f7 Mon Sep 17 00:00:00 2001 From: Gabriel Arazas Date: Tue, 21 May 2024 20:42:08 +0800 Subject: [PATCH] bahaghari/lib: init `colors.rgb` subset --- subprojects/bahaghari/lib/colors/README.adoc | 8 + subprojects/bahaghari/lib/colors/rgb.nix | 167 +++++++++++++++++++ subprojects/bahaghari/lib/default.nix | 6 + subprojects/bahaghari/tests/lib/default.nix | 1 + subprojects/bahaghari/tests/lib/rgb.nix | 102 +++++++++++ 5 files changed, 284 insertions(+) create mode 100644 subprojects/bahaghari/lib/colors/README.adoc create mode 100644 subprojects/bahaghari/lib/colors/rgb.nix create mode 100644 subprojects/bahaghari/tests/lib/rgb.nix diff --git a/subprojects/bahaghari/lib/colors/README.adoc b/subprojects/bahaghari/lib/colors/README.adoc new file mode 100644 index 00000000..c016237c --- /dev/null +++ b/subprojects/bahaghari/lib/colors/README.adoc @@ -0,0 +1,8 @@ += Bahaghari library: Colors subset +:toc: + +The colors subset of the Bahaghari library. +It's not on the level of https://www.colour-science.org/[Color Science for Python] or https://crates.io/crates/palette[palette library for Rust] where it gives you a complete toolkit for manipulating a lot of aspects of colors. +Rather, this is only in relation to generating them colors to be pressed onto a template like program configurations from Nix modules and even some light templating for some. +Its API should only limit on that aspect. +No way in hell we're trying to reimplment that in Nix, lol. diff --git a/subprojects/bahaghari/lib/colors/rgb.nix b/subprojects/bahaghari/lib/colors/rgb.nix new file mode 100644 index 00000000..8f40166a --- /dev/null +++ b/subprojects/bahaghari/lib/colors/rgb.nix @@ -0,0 +1,167 @@ +# The most antiquated colorspace like ever. For this implementation, we will be +# looking after the RGB specification (especially the hexadecimal notation) as +# specified from W3 CSS Color Module Level 4 +# (https://www.w3.org/TR/css-color-4) since it is the most common one. +{ pkgs, lib, self }: + +rec { + /* Generates an RGB colorspace object to be generated with its method for + convenience. + + Type: RGB :: Attrs -> Attrs + + Example: + RGB { r = 242.0; g = 12; b = 23; } + => { + # The individual red, green, and blue components. + + # And several methods. + __functor = { + toHex = ; + lighten = ; + }; + } + */ + RGB = { r, g, b }@color: + assert lib.assertMsg (isRgb color) + "bahaghariLib.colors.rgb.RGB: given color does not have valid RGB value"; + { + inherit r g b; + __functor = { + toHex = self: toHex color; + lighten = self: lighten color; + }; + }; + + /* Returns a boolean to check if it's a valid RGB Nix object or not. + + Type: isRgb :: Attrs -> Bool + + Example: + isRgb { r = 34; g = 43; b = 555; } + # `b` is more than 255.0 so it's a false + => false + + isRgb { r = 123; g = null; b = 43; } + # `g` is not a number so it's'a false again + => false + + isRgb { r = 123; g = 123; b = 123; } + => true + */ + isRgb = { r, g, b }@color: + let + isWithinRGBRange = v: self.math.isWithinRange 0 255 v; + isValidRGB = v: self.isNumber v && isWithinRGBRange v; + in + lib.lists.all (v: isValidRGB v) (lib.attrValues color); + + /* Converts the color to a 6-digit hex string. Unfortunately, it cannot + handle floats very well so we'll have to round these up. + + Type: toHex :: RGB -> String + + Example: + toHex { r = 231; g = 12; b = 21; } + => "E70C15" + */ + toHex = { r, g, b, ... }: + let + r' = self.math.round r; + g' = self.math.round g; + b' = self.math.round b; + rH = self.hex.pad 2 (self.hex.fromDec r'); + gH = self.hex.pad 2 (self.hex.fromDec g'); + bH = self.hex.pad 2 (self.hex.fromDec b'); + in "${rH}${gH}${bH}"; + + /* Converts a valid hex string into an RGB object. + + Type: fromHex :: String -> RGB + + Example: + fromHex "FFFFFF" + => { r = 255; g = 255; b = 255; } + + fromHex "FFF" + => { r = 255; g = 255; b = 255; } + */ + fromHex = hex: + let + hex' = hexMatch hex; + r = lib.lists.elemAt hex' 0; + g = lib.lists.elemAt hex' 1; + b = lib.lists.elemAt hex' 2; + in + RGB { inherit r g b; }; + + /* Given a percentage, uniformly lighten the given RGB color. + + Type: lighten :: RGB -> Number -> RGB + + Example: + let + color = RGB { r = 12; g = 46; b = 213; }; + in + lighten color 50 + */ + lighten = { r, g, b, ... }: percentage: + let + grow' = c: self.math.grow' 0 255 percentage; + in + RGB { + r = grow' r; + g = grow' g; + b = grow' b; + }; + + /* Given an RGB color in hexadecimal notation, returns a list of integers + representing each of the components in order. Certain forms of hex strings + will also return a fourth component representing the alpha channel (RGBA). + + Type: hexMatch :: String -> List + + Example: + hexMatch "FFF" + => [ 255 255 255 ] + + hexMatch "FFFF" + => [ 255 255 255 255 ] + + hexMatch "0A0B0C0D" + => [ 10 11 12 13 ] + */ + hexMatch = hex: + let + length = lib.stringLength hex; + genMatch = r: n: lib.concatStringsSep "" (lib.genList (_: "([[:xdigit:]]{${builtins.toString n}})") r); + nonAlphaGenMatch = genMatch 3; + withAlphaGenMatch = genMatch 4; + + regex = + if (length == 6) then + nonAlphaGenMatch 2 + else if (length == 3) then + nonAlphaGenMatch 1 + else if (length == 8) then + withAlphaGenMatch 2 + else if (length == 4) then + withAlphaGenMatch 1 + else + throw "Not a valid hex code"; + + scale = self.trivial.scale { + inMin = 0; + inMax = 15; + outMin = 0; + outMax = 255; + }; + + match = lib.strings.match regex hex; + output = builtins.map (x: self.hex.toDec x) match; + in + if (length == 3 || length == 4) then + builtins.map (x: scale x) output + else + output; +} diff --git a/subprojects/bahaghari/lib/default.nix b/subprojects/bahaghari/lib/default.nix index f88ed713..be45de9f 100644 --- a/subprojects/bahaghari/lib/default.nix +++ b/subprojects/bahaghari/lib/default.nix @@ -30,6 +30,12 @@ pkgs.lib.makeExtensible hex = callLibs ./hex.nix; math = callLibs ./math.nix; + # We won't export any of the attributes here as a top-level attribute for + # some unbeknownst and probably irrational reason. + colors = { + rgb = callLibs ./colors/rgb.nix; + }; + # Dedicated module sets are not supposed to have any of its functions as a # top-level attribute. It's to make things a bit easier to organize and # maintain. Plus, if there's any functions that are easily applicable diff --git a/subprojects/bahaghari/tests/lib/default.nix b/subprojects/bahaghari/tests/lib/default.nix index d0a5bc24..12c0360f 100644 --- a/subprojects/bahaghari/tests/lib/default.nix +++ b/subprojects/bahaghari/tests/lib/default.nix @@ -12,4 +12,5 @@ in math = callLib ./math.nix; trivial = callLib ./trivial; tinted-theming = callLib ./tinted-theming; + rgb = callLib ./rgb.nix; } diff --git a/subprojects/bahaghari/tests/lib/rgb.nix b/subprojects/bahaghari/tests/lib/rgb.nix new file mode 100644 index 00000000..8c54a1c3 --- /dev/null +++ b/subprojects/bahaghari/tests/lib/rgb.nix @@ -0,0 +1,102 @@ +{ pkgs, lib, self }: + +let + # A modified version that simply removes the functor to focus more on the + # actual results. Also, it will mess up the result comparison since comparing + # functions is reference-based so it will always fail. + normalizeData = colors: + lib.attrsets.removeAttrs colors [ "__functor" ]; + + rgbSample = self.colors.rgb.RGB { + r = 255; + g = 255; + b = 255; + }; + + # A modified version of RGB that normalizes data out-of-the-boxly. + RGB = colors: normalizeData (self.colors.rgb.RGB colors); +in +lib.runTests { + testsBasicRgb = { + expr = RGB { + r = 34; + g = 2; + b = 0; + }; + expected = { + r = 34; + g = 2; + b = 0; + }; + }; + + testsFromHex = { + expr = normalizeData (self.colors.rgb.fromHex "FFFFFF"); + expected = normalizeData (self.colors.rgb.RGB { + r = 255; + g = 255; + b = 255; + }); + }; + + testsFromHex2 = { + expr = normalizeData (self.colors.rgb.fromHex "FFF"); + expected = normalizeData (self.colors.rgb.RGB { + r = 255; + g = 255; + b = 255; + }); + }; + + testsFromHex3 = { + expr = normalizeData (self.colors.rgb.fromHex "FFFF"); + expected = normalizeData (self.colors.rgb.RGB { + r = 255; + g = 255; + b = 255; + }); + }; + + testsFromHex4 = { + expr = normalizeData (self.colors.rgb.fromHex "FFFFFFFF"); + expected = normalizeData (self.colors.rgb.RGB { + r = 255; + g = 255; + b = 255; + }); + }; + + testsToHex = { + expr = self.colors.rgb.toHex rgbSample; + expected = "FFFFFF"; + }; + + testsToHex2 = { + expr = self.colors.rgb.toHex (RGB { + r = 23; + g = 58; + b = 105; + }); + expected = "173A69"; + }; + + testsHexMatch = { + expr = self.colors.rgb.hexMatch "FFF"; + expected = [ 255 255 255 ]; + }; + + testsHexMatch2 = { + expr = self.colors.rgb.hexMatch "FFFF"; + expected = [ 255 255 255 255 ]; + }; + + testsHexMatch3 = { + expr = self.colors.rgb.hexMatch "0A0B0C0D"; + expected = [ 10 11 12 13 ]; + }; + + testsHexMatch4 = { + expr = self.colors.rgb.hexMatch "0A0B0C"; + expected = [ 10 11 12 ]; + }; +}