bahaghari/lib: init colors.rgb subset

This commit is contained in:
Gabriel Arazas 2024-05-21 20:42:08 +08:00
parent f882c30209
commit c15ec954c0
No known key found for this signature in database
GPG Key ID: 62104B43D00AA360
5 changed files with 284 additions and 0 deletions

View File

@ -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.

View File

@ -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 = <function>;
lighten = <function>;
};
}
*/
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;
}

View File

@ -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

View File

@ -12,4 +12,5 @@ in
math = callLib ./math.nix;
trivial = callLib ./trivial;
tinted-theming = callLib ./tinted-theming;
rgb = callLib ./rgb.nix;
}

View File

@ -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 ];
};
}