From 9d5fcc0668396ee8c7729204326694d8857b7104 Mon Sep 17 00:00:00 2001 From: ooks-io Date: Wed, 6 Nov 2024 18:49:36 +1100 Subject: [PATCH] lib: add color function collection --- outputs/lib/color/check.nix | 65 ++++++++++ outputs/lib/color/translate.nix | 207 ++++++++++++++++++++++++++++++++ outputs/lib/color/types.nix | 51 ++++++++ outputs/lib/color/utils.nix | 122 +++++++++++++++++++ outputs/lib/default.nix | 22 +++- outputs/lib/math.nix | 46 +++++++ 6 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 outputs/lib/color/check.nix create mode 100644 outputs/lib/color/translate.nix create mode 100644 outputs/lib/color/types.nix create mode 100644 outputs/lib/color/utils.nix create mode 100644 outputs/lib/math.nix diff --git a/outputs/lib/color/check.nix b/outputs/lib/color/check.nix new file mode 100644 index 0000000..cb78c4b --- /dev/null +++ b/outputs/lib/color/check.nix @@ -0,0 +1,65 @@ +{lib, ...}: let + inherit (lib) toInt all min max; + inherit (builtins) isInt isFloat match getAttr hasAttr; + + # basic checks + range = a: b: v: (v <= max a b) && (v >= min a b); + number = v: isInt v || isFloat v; + unary = range 0.0 1.0; + hue = range 0.0 360.0; + + # type checking + hex = { + string = color: let + hexPatternWithHash = match "#[[:xdigit:]]{6}" color; + hexPatternNoHash = match "[[:xdigit:]]{6}" color; + isValid = hexPatternWithHash != null || hexPatternNoHash != null; + in + assert isValid || throw "Invalid hex color format: ${color}"; + hexPatternWithHash != null || hexPatternNoHash != null; + set = color: let + hasAttributes = all (k: hasAttr k color) ["r" "g" "b"]; + validPattern = all (k: let + v = getAttr k color; + in + match "[[:xdigit:]]{2}" v != null) ["r" "g" "b"]; + in + hasAttributes && validPattern; + }; + + rgb = { + string = color: let + rgbPattern = match "([0-9]{1,3}),([0-9]{1,3}),([0-9]{1,3})" color; + toNum = str: let + num = toInt str; + in + num != null && range 0 255 num; + isValid = rgbPattern != null && all toNum rgbPattern; + in + isValid; + set = color: let + hasAttributes = all (k: hasAttr k color) ["r" "g" "b"]; + validRanges = all ( + k: let + v = getAttr k color; + in + number v && range 0 255 v + ) ["r" "g" "b"]; + in + hasAttributes && validRanges; + }; + + hsl = { + # TODO: add range checks + string = color: let + hslPattern = match "([0-9]{1,3}),[ ]*([0-9]{1,3})%,[ ]*([0-9]{1,3})%" color; + in + hslPattern != null; + + set = color: let + hasAttributes = all (k: hasAttr k color) ["h" "s" "l"]; + validRanges = hue color.h && all (k: unary (getAttr k color)) ["s" "l"]; + in + hasAttributes && validRanges; + }; +in {inherit range number unary hue hex rgb hsl;} diff --git a/outputs/lib/color/translate.nix b/outputs/lib/color/translate.nix new file mode 100644 index 0000000..9ad22a8 --- /dev/null +++ b/outputs/lib/color/translate.nix @@ -0,0 +1,207 @@ +{ + math, + types, + lib, +}: let + inherit (builtins) substring; + inherit (lib) min max; + hex = { + toSet = string: + types.hex.set { + r = substring 0 2 string; + g = substring 2 2 string; + b = substring 4 2 string; + }; + toRGB = let + dictionary = { + "0" = 0; + "1" = 1; + "2" = 2; + "3" = 3; + "4" = 4; + "5" = 5; + "6" = 6; + "7" = 7; + "8" = 8; + "9" = 9; + "a" = 10; + "b" = 11; + "c" = 12; + "d" = 13; + "e" = 14; + "f" = 15; + "A" = 10; + "B" = 11; + "C" = 12; + "D" = 13; + "E" = 14; + "F" = 15; + }; + in { + # Converts a hex pair directly to RGB value (0-255) + pair = hexPair: let + high = dictionary.${substring 0 1 hexPair}; + low = dictionary.${substring 1 1 hexPair}; + in + (high * 16) + low; + + # Converts a hex set to RGB set + set = hexSet: + types.rgb.set { + r = hex.toRGB.pair hexSet.r; + g = hex.toRGB.pair hexSet.g; + b = hex.toRGB.pair hexSet.b; + }; + string = hexStr: let + rgbSet = hex.toRGB.set (hex.toSet hexStr); + in + types.rgb.string rgbSet.r rgbSet.g rgbSet.b; + }; + toHSL = { + set = hexStr: let + rgbSet = hex.toRGB.set (hex.toSet hexStr); + in + rgb.toHSL.set rgbSet; + + string = hexStr: let + hslSet = hex.toHSL.set hexStr; + in + types.hsl.string hslSet.h hslSet.s hslSet.l; + }; + }; + rgb = { + toHex = { + set = rgbSet: let + # Convert decimal to two-digit hex + toHexPair = num: let + hex = lib.toLower (lib.toHexString num); + # Pad with leading zero if single digit + padded = + if (builtins.stringLength hex) == 1 + then "0${hex}" + else hex; + in + padded; + in + types.hex.set { + r = toHexPair rgbSet.r; + g = toHexPair rgbSet.g; + b = toHexPair rgbSet.b; + }; + + string = rgbStr: let + hexSet = rgb.toHex.set rgbStr; + in + types.hex.string hexSet.r hexSet.g hexSet.b; + }; + toHSL = { + set = rgbSet: let + # Normalize RGB values to 0-1 range + r = rgbSet.r / 255.0; + g = rgbSet.g / 255.0; + b = rgbSet.b / 255.0; + + # Find min, max and delta + c_max = max (max r g) b; + c_min = min (min r g) b; + delta = c_max - c_min; + + # Calculate HSL values + h = + if delta == 0.0 + then 0.0 + else if c_max == r + then 60.0 * (math.mod ((g - b) / delta) 6) + else if c_max == g + then 60.0 * ((b - r) / delta + 2) + else 60.0 * ((r - g) / delta + 4); + + l = (c_max + c_min) / 2; + + s = + if delta == 0.0 + then 0.0 + else delta / (1 - (math.abs (2 * l - 1))); + in + types.hsl.set { + inherit h; + s = math.clamp 0.0 1.0 s; + l = math.clamp 0.0 1.0 l; + }; + + string = rgbStr: let + hslSet = rgb.toHSL.set (hex.toRGB.set (hex.toSet rgbStr)); + in + types.hsl.string hslSet.h hslSet.s hslSet.l; + }; + }; + hsl = { + toRGB = { + set = hslSet: let + inherit (hslSet) h s l; + + # Calculate chroma + c = (1 - (math.abs (2 * l - 1))) * s; + # Calculate h prime (h') + hp = h / 60.0; + # Calculate x + x = c * (1 - math.abs ((math.mod hp 2) - 1)); + # Calculate m + m = l - c / 2; + + # Get initial RGB values based on h' + rgb' = + if hp <= 1 + then { + r = c; + g = x; + b = 0; + } + else if hp <= 2 + then { + r = x; + g = c; + b = 0; + } + else if hp <= 3 + then { + r = 0; + g = c; + b = x; + } + else if hp <= 4 + then { + r = 0; + g = x; + b = c; + } + else if hp <= 5 + then { + r = x; + g = 0; + b = c; + } + else { + r = c; + g = 0; + b = x; + }; + # Final RGB values + in + types.rgb.set { + r = math.round ((rgb'.r + m) * 255); + g = math.round ((rgb'.g + m) * 255); + b = math.round ((rgb'.b + m) * 255); + }; + + string = hslStr: let + rgbSet = hsl.toRGB.set hslStr; + in + types.rgb.string rgbSet.r rgbSet.g rgbSet.b; + }; + toHex = { + set = {}; + string = color: color; + }; + }; +in {inherit hex hsl rgb;} diff --git a/outputs/lib/color/types.nix b/outputs/lib/color/types.nix new file mode 100644 index 0000000..850e647 --- /dev/null +++ b/outputs/lib/color/types.nix @@ -0,0 +1,51 @@ +{ + check, + math, + ... +}: let + inherit (math) round; + hex = { + set = { + r, + g, + b, + }: let + attrs = {inherit r g b;}; + in + assert check.hex.set attrs || throw "Invalid Hex values: r=${toString r}, g=${toString g}, b=${toString b}"; attrs; + + string = r: g: b: let + str = "${r}${g}${b}"; + in + assert check.hex.string str || throw "Invalid Hex value: ${str}"; str; + }; + + rgb = { + string = r: g: b: let + str = "${toString r},${toString g},${toString b}"; + in + assert check.rgb.string str || throw "Invalid RBG string format: ${str}"; str; + set = { + r, + g, + b, + }: let + attrs = {inherit r g b;}; + in + assert check.rgb.set attrs || throw "Invalid RGB values: r=${toString r}, g=${toString g}, b=${toString b}"; attrs; + }; + hsl = { + string = h: s: l: let + str = "${toString (round h)}, ${toString (round (s * 100))}%, ${toString (round (l * 100))}%"; + in + assert check.hsl.string str || throw "Invalid HSL values: ${str}"; str; + set = { + h, + s, + l, + }: let + attrs = {inherit h s l;}; + in + assert check.hsl.set attrs || throw "Invalid HSL values: h=${toString h}, s=${toString s}, l=${toString l}"; attrs; + }; +in {inherit hex hsl rgb;} diff --git a/outputs/lib/color/utils.nix b/outputs/lib/color/utils.nix new file mode 100644 index 0000000..1fa6938 --- /dev/null +++ b/outputs/lib/color/utils.nix @@ -0,0 +1,122 @@ +{ + math, + types, + translate, +}: let + # base modification function + modifyHSL = hexStr: modifications: let + # convert hex to HSL + hslSet = translate.hex.toHSL.set hexStr; + # apply modifications to get new HSL values + newHSL = types.hsl.set { + inherit (hslSet) h; # keep hue + l = math.clamp 0.0 1.0 (hslSet.l + (modifications.l or 0.0)); + s = math.clamp 0.0 1.0 (hslSet.s + (modifications.s or 0.0)); + }; + # convert back to hex + rgbSet = translate.hsl.toRGB.set newHSL; + in + translate.rgb.toHex.string rgbSet; + + lighten = amount: hexStr: + modifyHSL hexStr {l = amount / 100.0;}; + + darken = amount: hexStr: + modifyHSL hexStr {l = (amount * -1) / 100.0;}; + + saturate = amount: hexStr: + modifyHSL hexStr {s = amount / 100.0;}; + + desaturate = amount: hexStr: + modifyHSL hexStr {s = (amount * -1) / 100.0;}; + + mkDarkColorScale = base: { + up4 = desaturate 24 (lighten 12 base); + up3 = desaturate 18 (lighten 9 base); + up2 = desaturate 12 (lighten 6 base); + up1 = desaturate 6 (lighten 3 base); + inherit base; + down1 = desaturate 6 (darken 3 base); + down2 = desaturate 12 (darken 6 base); + down3 = desaturate 18 (darken 9 base); + down4 = desaturate 24 (darken 12 base); + }; + + mkLightColorScale = base: { + down4 = desaturate 24 (lighten 12 base); + down3 = desaturate 18 (lighten 9 base); + down2 = desaturate 12 (lighten 6 base); + down1 = desaturate 6 (lighten 3 base); + inherit base; + up1 = desaturate 6 (darken 3 base); + up2 = desaturate 12 (darken 6 base); + up3 = desaturate 18 (darken 9 base); + up4 = desaturate 24 (darken 12 base); + }; + + mkDarkColorScheme = { + shades, + primary, + secondary, + red, + orange, + yellow, + olive, + green, + teal, + blue, + violet, + purple, + pink, + brown, + } @ args: { + shade-50 = args.shades."50"; + shade-100 = args.shades."100"; + shade-150 = args.shades."150"; + shade-200 = args.shades."200"; + shade-250 = args.shades."250"; + shade-300 = args.shades."300"; + shade-350 = args.shades."350"; + shade-400 = args.shades."400"; + shade-450 = args.shades."450"; + shade-500 = args.shades."500"; + shade-550 = args.shades."550"; + shade-600 = args.shades."600"; + shade-650 = args.shades."650"; + shade-700 = args.shades."700"; + shade-750 = args.shades."750"; + shade-800 = args.shades."800"; + shade-850 = args.shades."850"; + shade-900 = args.shades."900"; + + primary = mkDarkColorScale args.primary; + secondary = { + up-1 = args.shade."550"; + up-2 = args.shade."500"; + up-3 = args.shade."450"; + up-4 = args.shade."400"; + up-5 = args.shade."350"; + up-6 = args.shade."300"; + up-7 = args.shade."250"; + up-8 = args.shade."200"; + up-9 = args.shade."150"; + up-10 = args.shade."100"; + base = args.shade."700"; + down-1 = args.shade."650"; + down-2 = args.shade."700"; + }; + red = mkDarkColorScale args.red; + orange = mkDarkColorScale args.orange; + yellow = mkDarkColorScale args.yellow; + olive = mkDarkColorScale args.olive; + green = mkDarkColorScale args.green; + teal = mkDarkColorScale args.teal; + blue = mkDarkColorScale args.blue; + violet = mkDarkColorScale args.violet; + purple = mkDarkColorScale args.purple; + pink = mkDarkColorScale args.pink; + brown = mkDarkColorScale args.brown; + }; +in { + inherit lighten darken saturate desaturate mkLightColorScale mkDarkColorScale mkDarkColorScheme; +} diff --git a/outputs/lib/default.nix b/outputs/lib/default.nix index 3fcc9bc..e2505b5 100644 --- a/outputs/lib/default.nix +++ b/outputs/lib/default.nix @@ -4,10 +4,30 @@ inputs, ... }: let - # My person functions + # my scuffed lib ook-lib = { builders = import ./builders.nix {inherit self lib inputs;}; mkNeovim = import ./mkNeovim.nix {inherit inputs;}; + math = import ./math.nix {inherit lib;}; + color = let + check = import ./color/check.nix {inherit lib;}; + types = import ./color/types.nix { + inherit (ook-lib) math; + inherit check; + }; + translate = import ./color/translate.nix { + inherit lib; + inherit (ook-lib) math; + inherit types; + }; + utils = import ./color/utils.nix { + inherit (ook-lib) math; + inherit types translate; + }; + in { + inherit check types translate; + inherit (utils) lighten darken saturate desaturate mkColorScale; + }; }; in { _module.args.ook.lib = ook-lib; diff --git a/outputs/lib/math.nix b/outputs/lib/math.nix new file mode 100644 index 0000000..ae66b6f --- /dev/null +++ b/outputs/lib/math.nix @@ -0,0 +1,46 @@ +{lib}: let + inherit (lib) min max; + inherit (builtins) floor ceil; + + # basic math functions + # credits to github:xddxdd/nix-math + + abs = x: + if x < 0 + then 0 - x + else x; + + clamp = a: b: v: min (max v (min a b)) (max a b); + + round = x: + if (x - floor x) < 0.5 + then floor x + else ceil x; + + hasFraction = x: let + splitted = lib.splitString "." (builtins.toString x); + in + builtins.length splitted >= 2 && builtins.length (builtins.filter (ch: ch != "0") (lib.stringToCharacters (builtins.elemAt splitted 1))) > 0; + + div = a: b: let + divideExactly = !(hasFraction (1.0 * a / b)); + offset = + if divideExactly + then 0 + else (0 - 1); + in + if b < 0 + then offset - div a (0 - b) + else if a < 0 + then offset - div (0 - a) b + else floor (1.0 * a / b); + + mod = a: b: + if b < 0 + then 0 - mod (0 - a) (0 - b) + else if a < 0 + then mod (b - mod (0 - a) b) b + else a - b * (div a b); +in { + inherit round mod abs hasFraction clamp; +}