From 320dc003c408657ea3df770cd7fdb4236273ade4 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Tue, 21 Apr 2026 01:12:42 -0400 Subject: [PATCH] Color Fix Script update --- scripts/blob_color_fixer.py | 210 ++++++++++++++++++++---------------- 1 file changed, 118 insertions(+), 92 deletions(-) diff --git a/scripts/blob_color_fixer.py b/scripts/blob_color_fixer.py index a3b29e8..85e97c6 100755 --- a/scripts/blob_color_fixer.py +++ b/scripts/blob_color_fixer.py @@ -1,107 +1,133 @@ #!/usr/bin/env python3 +""" +blob_color_fixer.py — Detects monotone/bland wal color palettes and replaces +accent colors with vibrant, harmonically-spread alternatives. +""" + import sys +import math import colorsys +from dataclasses import dataclass -def hex_to_rgb(hex_str): - hex_str = hex_str.lstrip('#') - return tuple(int(hex_str[i:i+2], 16) / 255.0 for i in (0, 2, 4)) +MIN_SATURATION = 0.65 +MIN_LIGHTNESS = 0.40 +MAX_LIGHTNESS = 0.70 +HUE_THRESHOLD = 0.15 +SAT_THRESHOLD = 0.35 +HUE_VARIANCE_THRESHOLD = 0.15 +ACCENT_SLICE = slice(1, 7) +BRIGHT_OFFSET = 8 -def rgb_to_hex(r, g, b): - return '#{:02x}{:02x}{:02x}'.format(int(r * 255), int(g * 255), int(b * 255)) +HUE_SHIFTS: list[float] = [ + 0.0, + 0.5, + 0.083, + -0.083, + 0.416, + -0.416, +] -def shift_hue_and_saturate(hex_str, shift_amount): - r, g, b = hex_to_rgb(hex_str) - h, l, s = colorsys.rgb_to_hls(r, g, b) - h = (h + shift_amount) % 1.0 - s = max(s, 0.65) # Force vibrancy - l = min(max(l, 0.4), 0.7) # Ensure it's not too dark or overly washed out - r, g, b = colorsys.hls_to_rgb(h, l, s) - return rgb_to_hex(r, g, b) +@dataclass(frozen=True) +class HLS: + h: float + l: float + s: float -def get_hls_stats(hex_list): - hues = [] - sats = [] - for hex_str in hex_list: - r, g, b = hex_to_rgb(hex_str) - h, l, s = colorsys.rgb_to_hls(r, g, b) - hues.append(h) - sats.append(s) - return hues, sats -def is_monotone_or_bland(hues, sats, hue_threshold=0.15, sat_threshold=0.35): - if not hues or not sats: return False - - # Calculate hue variance - min_h = min(hues) - max_h = max(hues) - hue_diff = min(max_h - min_h, 1.0 - (max_h - min_h)) - - # Calculate average saturation - avg_s = sum(sats) / len(sats) - - return hue_diff < hue_threshold or avg_s < sat_threshold +def hex_to_rgb(hex_str: str) -> tuple[float, float, float]: + hex_str = hex_str.lstrip("#") + return tuple(int(hex_str[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore[return-value] -def main(): - if len(sys.argv) < 2: - print("Usage: python3 blob_color_fixer.py ") - sys.exit(1) - - filepath = sys.argv[1] - + +def rgb_to_hex(r: float, g: float, b: float) -> str: + return "#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255)) + + +def hex_to_hls(hex_str: str) -> HLS: + h, l, s = colorsys.rgb_to_hls(*hex_to_rgb(hex_str)) + return HLS(h, l, s) + + +def vibrant_shift(hex_str: str, hue_shift: float) -> str: + """Return *hex_str* with its hue rotated by *hue_shift* and vibrancy enforced.""" + hls = hex_to_hls(hex_str) + h = (hls.h + hue_shift) % 1.0 + s = max(hls.s, MIN_SATURATION) + l = min(max(hls.l, MIN_LIGHTNESS), MAX_LIGHTNESS) + return rgb_to_hex(*colorsys.hls_to_rgb(h, l, s)) + + +def palette_is_bland(accents: list[str]) -> bool: + """Return True when accent colors are too similar, clustered, or too desaturated.""" + stats = [hex_to_hls(c) for c in accents] + hues = [hls.h for hls in stats] + sats = [hls.s for hls in stats] + + raw_spread = max(hues) - min(hues) + hue_spread = min(raw_spread, 1.0 - raw_spread) + avg_sat = sum(sats) / len(sats) + + if hue_spread < HUE_THRESHOLD or avg_sat < SAT_THRESHOLD: + return True + + mean_sin = sum(math.sin(2 * math.pi * h) for h in hues) / len(hues) + mean_cos = sum(math.cos(2 * math.pi * h) for h in hues) / len(hues) + hue_variance = 1.0 - math.sqrt(mean_sin ** 2 + mean_cos ** 2) + if hue_variance < HUE_VARIANCE_THRESHOLD: + return True + + return False + + +def most_saturated(accents: list[str]) -> str: + return max(accents, key=lambda c: hex_to_hls(c).s) + + +def load_colors(filepath: str) -> list[str]: try: - with open(filepath, 'r') as f: + with open(filepath) as f: colors = [line.strip() for line in f if line.strip()] - except Exception as e: - print(f"Error reading colors: {e}") - sys.exit(1) - + except OSError as exc: + sys.exit(f"Error reading '{filepath}': {exc}") + if len(colors) < 16: - print("Not enough colors found in file.") - sys.exit(1) - - # Check hue variance and saturation of accent colors (indices 1-6) - accents = colors[1:7] - hues, sats = get_hls_stats(accents) - - if is_monotone_or_bland(hues, sats, hue_threshold=0.15, sat_threshold=0.35): - print("Detected monotone or bland palette. Generating vibrant complementary colors...") - - # Pick the most saturated accent as the base - base_accent = accents[-1] - max_sat = 0 - for i, sat in enumerate(sats): - if sat > max_sat: - max_sat = sat - base_accent = accents[i] - - # Generate new colors with forced vibrancy - comp = shift_hue_and_saturate(base_accent, 0.5) - ana1 = shift_hue_and_saturate(base_accent, 0.083) - ana2 = shift_hue_and_saturate(base_accent, -0.083) - split1 = shift_hue_and_saturate(base_accent, 0.416) - split2 = shift_hue_and_saturate(base_accent, -0.416) - - # The base accent itself also gets saturated if it was too bland - r, g, b = hex_to_rgb(base_accent) - h, l, s = colorsys.rgb_to_hls(r, g, b) - if s < 0.65: - base_vibrant = shift_hue_and_saturate(base_accent, 0.0) - else: - base_vibrant = base_accent - - new_accents = [base_vibrant, comp, ana1, ana2, split1, split2] - - # Replace normal and bright accents - for i in range(6): - colors[i+1] = new_accents[i] - colors[i+9] = new_accents[i] - - with open(filepath, 'w') as f: - for color in colors: - f.write(f"{color}\n") - print("Colors successfully enhanced.") - else: - print("Palette is already vibrant and diverse enough.") + sys.exit(f"Expected ≥ 16 colors in '{filepath}', found {len(colors)}.") + + return colors + + +def save_colors(filepath: str, colors: list[str]) -> None: + try: + with open(filepath, "w") as f: + f.write("\n".join(colors) + "\n") + except OSError as exc: + sys.exit(f"Error writing '{filepath}': {exc}") + + +def main() -> None: + if len(sys.argv) < 2: + sys.exit("Usage: blob_color_fixer.py ") + + filepath = sys.argv[1] + colors = load_colors(filepath) + accents = colors[ACCENT_SLICE] + + if not palette_is_bland(accents): + print("Palette is already vibrant and diverse — nothing to do.") + return + + print("Monotone / bland palette detected. Generating vibrant harmony…") + + base = most_saturated(accents) + new_accents = [vibrant_shift(base, shift) for shift in HUE_SHIFTS] + + for i, color in enumerate(new_accents): + colors[i + 1] = color + colors[i + 1 + BRIGHT_OFFSET] = color + + save_colors(filepath, colors) + print("Done — colors enhanced and written back to file.") + if __name__ == "__main__": main() \ No newline at end of file