From f9c60a483d41289c149f52939bfc0e0680077ff4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 5 Apr 2025 17:53:13 +1100 Subject: scheme: use material for colour gen Replaces okolors Also boost neutral chroma and all chroma --- install/scripts.fish | 2 +- scheme/autoadjust.py | 100 +++++++++++++++++++++++++++++++++-------------- scheme/gen-scheme.fish | 12 +----- scheme/genmaterial.py | 84 --------------------------------------- scheme/score.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+), 125 deletions(-) delete mode 100755 scheme/genmaterial.py create mode 100755 scheme/score.py diff --git a/install/scripts.fish b/install/scripts.fish index c56b61e..8da6221 100755 --- a/install/scripts.fish +++ b/install/scripts.fish @@ -2,7 +2,7 @@ . (dirname (status filename))/util.fish -install-deps git hyprland-git hyprpaper-git okolors-git imagemagick wl-clipboard fuzzel-git socat foot jq python xdg-user-dirs python-materialyoucolor-git app2unit-git +install-deps git hyprland-git hyprpaper-git imagemagick wl-clipboard fuzzel-git socat foot jq python xdg-user-dirs python-materialyoucolor-git app2unit-git install-optional-deps 'equibop-bin (discord client)' 'btop (system monitor)' 'wf-recorder (screen recorder)' 'grim (screenshot tool)' 'zen-browser (web browser)' 'spotify-adblock (music player)' set -l dist $C_DATA/scripts diff --git a/scheme/autoadjust.py b/scheme/autoadjust.py index 0da2f0b..73deb88 100755 --- a/scheme/autoadjust.py +++ b/scheme/autoadjust.py @@ -72,6 +72,8 @@ colour_names = [ "error", ] +HLS = tuple[float, float, float] + def hex_to_rgb(hex: str) -> tuple[float, float, float]: """Convert a hex string to an RGB tuple in the range [0, 1].""" @@ -91,23 +93,29 @@ def hls_to_hex(h: str, l: str, s: str) -> str: return rgb_to_hex(hls_to_rgb(h, l, s)) -def adjust(hex: str, light: float = 0, sat: float = 0) -> str: - h, l, s = hex_to_hls(hex) - return hls_to_hex(h, max(0, min(1, l + light)), max(0, min(1, s + sat))) +def grayscale(hls: HLS, light: bool) -> HLS: + h, l, s = hls + return h, 0.5 - l / 2 if light else l / 2 + 0.5, 0 + +def mix(a: HLS, b: HLS, w: float) -> HLS: + h1, l1, s1 = a + h2, l2, s2 = b + return h1 * (1 - w) + h2 * w, l1 * (1 - w) + l2 * w, s1 * (1 - w) + s2 * w -def grayscale(hex: str, light: bool) -> str: - h, l, s = hex_to_hls(hex) - return hls_to_hex(h, 0.5 - l / 2 if light else l / 2 + 0.5, 0) +def darken(colour: HLS, amount: float) -> HLS: + h, l, s = colour + return h, max(0, l - amount), s -def distance(colour: str, base: str) -> float: - h1, l1, s1 = hex_to_hls(colour) + +def distance(colour: HLS, base: str) -> float: + h1, l1, s1 = colour h2, l2, s2 = hex_to_hls(base) return abs(h1 - h2) * 0.4 + abs(l1 - l2) * 0.3 + abs(s1 - s2) * 0.3 -def smart_sort(colours: list[str], base: list[str]) -> list[str]: +def smart_sort(colours: list[HLS], base: list[str]) -> dict[str, HLS]: sorted_colours = [None] * len(colours) distances = {} @@ -130,15 +138,7 @@ def smart_sort(colours: list[str], base: list[str]) -> list[str]: distances[colour].pop(0) - return [i[0] for i in sorted_colours] - - -def mix(a: str, b: str, w: float) -> str: - r1, g1, b1 = hex_to_rgb(a) - r2, g2, b2 = hex_to_rgb(b) - return rgb_to_hex( - (r1 * (1 - w) + r2 * w, g1 * (1 - w) + g2 * w, b1 * (1 - w) + b2 * w) - ) + return {colour_names[i]: c[0] for i, c in enumerate(sorted_colours)} def get_scheme(scheme: str) -> DynamicScheme: @@ -167,21 +167,61 @@ if __name__ == "__main__": colours_in = sys.argv[3] base = light_colours if light else dark_colours - MatScheme = get_scheme(scheme) + chroma_mult = 1.5 if light else 1.2 + + # Convert to HLS + colours = [hex_to_hls(c) for c in colours_in.split(" ")] - colours = smart_sort(colours_in.split(" "), base) - for i, hex in enumerate(colours): + # Sort colours and turn into dict + colours = smart_sort(colours, base) + + # Adjust colours + MatScheme = get_scheme(scheme) + for name, hls in colours.items(): if scheme == "monochrome": - colours[i] = grayscale(hex, light) + colours[name] = grayscale(hls, light) else: - argb = argb_from_rgb(int(hex[:2], 16), int(hex[2:4], 16), int(hex[4:], 16)) + argb = int(f"0xFF{hls_to_hex(*hls)}", 16) mat_scheme = MatScheme(Hct.from_int(argb), not light, 0) - primary = MaterialDynamicColors.primary.get_hct(mat_scheme) - colours[i] = "{:02X}{:02X}{:02X}".format(*primary.to_rgba()[:3]) - # Success and error colours - colours.append(mix(colours[8], base[8], 0.8)) # Success (green) - colours.append(mix(colours[4], base[4], 0.8)) # Error (red) + colour = MaterialDynamicColors.primary.get_hct(mat_scheme) + + # Boost neutral scheme colours + if scheme == "neutral": + colour.chroma += 10 - for i, colour in enumerate(colours): - print(f"{colour_names[i]} {colour}") + colour.chroma *= chroma_mult + + colours[name] = hex_to_hls("{:02X}{:02X}{:02X}".format(*colour.to_rgba()[:3])) + + # Success and error colours + colours["success"] = mix(colours["green"], hex_to_hls(base[8]), 0.8) + colours["error"] = mix(colours["red"], hex_to_hls(base[4]), 0.8) + + # Layers and accents + material = {} + primary_scheme = MatScheme(Hct.from_int(int(f"0xFF{colours_in.split(" ")[0]}", 16)), not light, 0) + for colour in vars(MaterialDynamicColors).keys(): + colour_name = getattr(MaterialDynamicColors, colour) + if hasattr(colour_name, "get_hct"): + rgb = colour_name.get_hct(primary_scheme).to_rgba()[:3] + material[colour] = hex_to_hls("{:02X}{:02X}{:02X}".format(*rgb)) + + colours["primary"] = material["primary"] + colours["secondary"] = material["secondary"] + colours["tertiary"] = material["tertiary"] + colours["text"] = material["onBackground"] + colours["subtext1"] = material["onSurfaceVariant"] + colours["subtext0"] = material["outline"] + colours["overlay2"] = mix(material["surface"], material["outline"], 0.86) + colours["overlay1"] = mix(material["surface"], material["outline"], 0.71) + colours["overlay0"] = mix(material["surface"], material["outline"], 0.57) + colours["surface2"] = mix(material["surface"], material["outline"], 0.43) + colours["surface1"] = mix(material["surface"], material["outline"], 0.29) + colours["surface0"] = mix(material["surface"], material["outline"], 0.14) + colours["base"] = material["surface"] + colours["mantle"] = darken(material["surface"], 0.03) + colours["crust"] = darken(material["surface"], 0.05) + + for name, colour in colours.items(): + print(f"{name} {hls_to_hex(*colour)}") diff --git a/scheme/gen-scheme.fish b/scheme/gen-scheme.fish index 0d618ad..7f95001 100755 --- a/scheme/gen-scheme.fish +++ b/scheme/gen-scheme.fish @@ -10,16 +10,8 @@ contains -- "$argv[2]" light dark && set -l theme $argv[2] || set -l theme dark set -l variants vibrant tonalspot expressive fidelity fruitsalad rainbow neutral content monochrome # Generate colours -set -l colours (okolors $img -k 14 -w 0) +set -l colours ($src/score.py $img) for variant in $variants mkdir -p $src/../data/schemes/dynamic/$variant - $src/autoadjust.py $theme $variant $colours > $src/../data/schemes/dynamic/$variant/$theme.txt + $src/autoadjust.py $theme $variant $colours | head -c -1 > $src/../data/schemes/dynamic/$variant/$theme.txt end - -# Generate layers and accents -set -l tmp (mktemp) -$src/genmaterial.py $img $theme > $tmp -for variant in $variants - grep -FA 15 $variant $tmp | tail -15 | head -c -1 >> $src/../data/schemes/dynamic/$variant/$theme.txt -end -rm $tmp diff --git a/scheme/genmaterial.py b/scheme/genmaterial.py deleted file mode 100755 index 0fb067f..0000000 --- a/scheme/genmaterial.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/python - -import math -import sys -from colorsys import hls_to_rgb, rgb_to_hls - -from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors -from materialyoucolor.hct import Hct -from materialyoucolor.quantize import ImageQuantizeCelebi -from materialyoucolor.scheme.scheme_content import SchemeContent -from materialyoucolor.scheme.scheme_expressive import SchemeExpressive -from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity -from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad -from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome -from materialyoucolor.scheme.scheme_neutral import SchemeNeutral -from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow -from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot -from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant -from materialyoucolor.score.score import Score - - -def darken(rgb: tuple[int, int, int], amount: float) -> tuple[int, int, int]: - h, l, s = rgb_to_hls(*tuple(i / 255 for i in rgb)) - return tuple(round(i * 255) for i in hls_to_rgb(h, max(0, l - amount), s)) - - -def mix( - rgb1: tuple[int, int, int], rgb2: tuple[int, int, int], amount: float -) -> tuple[int, int, int]: - return tuple(round(rgb1[i] * (1 - amount) + rgb2[i] * amount) for i in range(3)) - - -num_args = len(sys.argv) -if num_args < 2: - print('Usage: [ "light" | "dark" ]') - sys.exit(1) - -img = sys.argv[1] -is_dark = num_args < 3 or sys.argv[2] != "light" - -colours = ImageQuantizeCelebi(img, 1, 128) -hct = Hct.from_int(Score.score(colours)[0]) - -for Scheme in ( - SchemeFruitSalad, - SchemeExpressive, - SchemeMonochrome, - SchemeRainbow, - SchemeTonalSpot, - SchemeNeutral, - SchemeFidelity, - SchemeContent, - SchemeVibrant, -): - print("\n" + Scheme.__name__[6:].lower()) - scheme = Scheme(hct, is_dark, 0.0) - - colours = {} - - for color in vars(MaterialDynamicColors).keys(): - color_name = getattr(MaterialDynamicColors, color) - if hasattr(color_name, "get_hct"): - colours[color] = color_name.get_hct(scheme).to_rgba()[:3] - - colours = { - "primary": colours["primary"], - "secondary": colours["secondary"], - "tertiary": colours["tertiary"], - "text": colours["onBackground"], - "subtext1": colours["onSurfaceVariant"], - "subtext0": colours["outline"], - "overlay2": mix(colours["surface"], colours["outline"], 0.86), - "overlay1": mix(colours["surface"], colours["outline"], 0.71), - "overlay0": mix(colours["surface"], colours["outline"], 0.57), - "surface2": mix(colours["surface"], colours["outline"], 0.43), - "surface1": mix(colours["surface"], colours["outline"], 0.29), - "surface0": mix(colours["surface"], colours["outline"], 0.14), - "base": colours["surface"], - "mantle": darken(colours["surface"], 0.03), - "crust": darken(colours["surface"], 0.05), - } - - for name, colour in colours.items(): - print("{} {:02X}{:02X}{:02X}".format(name, *colour)) diff --git a/scheme/score.py b/scheme/score.py new file mode 100755 index 0000000..00a6d53 --- /dev/null +++ b/scheme/score.py @@ -0,0 +1,104 @@ +#!/bin/python + +import sys + +from materialyoucolor.quantize import ImageQuantizeCelebi +from materialyoucolor.hct import Hct +from materialyoucolor.utils.math_utils import sanitize_degrees_int, difference_degrees +from materialyoucolor.dislike.dislike_analyzer import DislikeAnalyzer + + +class Score: + TARGET_CHROMA = 48.0 + WEIGHT_PROPORTION = 0.7 + WEIGHT_CHROMA_ABOVE = 0.3 + WEIGHT_CHROMA_BELOW = 0.1 + CUTOFF_CHROMA = 5.0 + CUTOFF_EXCITED_PROPORTION = 0.01 + + def __init__(self): + pass + + @staticmethod + def score(colors_to_population: dict) -> list[int]: + desired = 14 + filter_enabled = True + dislike_filter = True + + colors_hct = [] + hue_population = [0] * 360 + population_sum = 0 + + for rgb, population in colors_to_population.items(): + hct = Hct.from_int(rgb) + colors_hct.append(hct) + hue = int(hct.hue) + hue_population[hue] += population + population_sum += population + + hue_excited_proportions = [0.0] * 360 + + for hue in range(360): + proportion = hue_population[hue] / population_sum + for i in range(hue - 14, hue + 16): + neighbor_hue = int(sanitize_degrees_int(i)) + hue_excited_proportions[neighbor_hue] += proportion + + scored_hct = [] + for hct in colors_hct: + hue = int(sanitize_degrees_int(round(hct.hue))) + proportion = hue_excited_proportions[hue] + + if filter_enabled and ( + hct.chroma < Score.CUTOFF_CHROMA + or proportion <= Score.CUTOFF_EXCITED_PROPORTION + ): + continue + + proportion_score = proportion * 100.0 * Score.WEIGHT_PROPORTION + chroma_weight = ( + Score.WEIGHT_CHROMA_BELOW + if hct.chroma < Score.TARGET_CHROMA + else Score.WEIGHT_CHROMA_ABOVE + ) + chroma_score = (hct.chroma - Score.TARGET_CHROMA) * chroma_weight + score = proportion_score + chroma_score + scored_hct.append({"hct": hct, "score": score}) + + scored_hct.sort(key=lambda x: x["score"], reverse=True) + + chosen_colors = [] + for difference_degrees_ in range(90, 0, -1): + chosen_colors.clear() + for hct in [item["hct"] for item in scored_hct]: + duplicate_hue = any( + difference_degrees(hct.hue, chosen_hct.hue) < difference_degrees_ + for chosen_hct in chosen_colors + ) + if not duplicate_hue: + chosen_colors.append(hct) + if len(chosen_colors) >= desired: + break + if len(chosen_colors) >= desired: + break + + colors = [] + + if dislike_filter: + for chosen_hct in chosen_colors: + chosen_colors[chosen_colors.index(chosen_hct)] = ( + DislikeAnalyzer.fix_if_disliked(chosen_hct) + ) + for chosen_hct in chosen_colors: + colors.append(chosen_hct.to_int()) + return colors + + +if __name__ == "__main__": + img = sys.argv[1] + + colours = ImageQuantizeCelebi(img, 1, 128) + colours = [Hct.from_int(c).to_rgba()[:3] for c in Score.score(colours)] + + # print("".join(["\x1b[48;2;{};{};{}m \x1b[0m".format(*colour) for colour in colours])) + print(" ".join(["{:02X}{:02X}{:02X}".format(*colour) for colour in colours])) -- cgit v1.2.3-freya