Files
caelestia-cli/scheme/autoadjust.py
T
2025-05-01 19:43:10 +10:00

257 lines
7.3 KiB
Python
Executable File

#!/bin/python3
import sys
from colorsys import hls_to_rgb, rgb_to_hls
from pathlib import Path
from materialyoucolor.blend import Blend
from materialyoucolor.dynamiccolor.material_dynamic_colors import (
DynamicScheme,
MaterialDynamicColors,
)
from materialyoucolor.hct import Hct
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
light_colours = [
"dc8a78",
"dd7878",
"ea76cb",
"8839ef",
"d20f39",
"e64553",
"fe640b",
"df8e1d",
"40a02b",
"179299",
"04a5e5",
"209fb5",
"1e66f5",
"7287fd",
]
dark_colours = [
"f5e0dc",
"f2cdcd",
"f5c2e7",
"cba6f7",
"f38ba8",
"eba0ac",
"fab387",
"f9e2af",
"a6e3a1",
"94e2d5",
"89dceb",
"74c7ec",
"89b4fa",
"b4befe",
]
colour_names = [
"rosewater",
"flamingo",
"pink",
"mauve",
"red",
"maroon",
"peach",
"yellow",
"green",
"teal",
"sky",
"sapphire",
"blue",
"lavender",
"success",
"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]."""
return tuple(int(hex[i : i + 2], 16) / 255 for i in (0, 2, 4))
def rgb_to_hex(rgb: tuple[float, float, float]) -> str:
"""Convert an RGB tuple in the range [0, 1] to a hex string."""
return "".join(f"{round(i * 255):02X}" for i in rgb)
def hex_to_hls(hex: str) -> tuple[float, float, float]:
return rgb_to_hls(*hex_to_rgb(hex))
def hls_to_hex(h: str, l: str, s: str) -> str:
return rgb_to_hex(hls_to_rgb(h, l, s))
def hex_to_argb(hex: str) -> int:
return int(f"0xFF{hex}", 16)
def argb_to_hls(argb: int) -> HLS:
return hex_to_hls(f"{argb:08X}"[2:])
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:
r1, g1, b1 = hls_to_rgb(*a)
r2, g2, b2 = hls_to_rgb(*b)
return rgb_to_hls(
r1 * (1 - w) + r2 * w, g1 * (1 - w) + g2 * w, b1 * (1 - w) + b2 * w
)
def harmonize(a: str, b: int) -> HLS:
return argb_to_hls(Blend.harmonize(hex_to_argb(a), b))
def darken(colour: HLS, amount: float) -> HLS:
h, l, s = colour
return h, max(0, l - amount), s
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[HLS], base: list[str]) -> dict[str, HLS]:
sorted_colours = [None] * len(colours)
distances = {}
for colour in colours:
dist = [(i, distance(colour, b)) for i, b in enumerate(base)]
dist.sort(key=lambda x: x[1])
distances[colour] = dist
for colour in colours:
while len(distances[colour]) > 0:
i, dist = distances[colour][0]
if sorted_colours[i] is None:
sorted_colours[i] = colour, dist
break
elif sorted_colours[i][1] > dist:
old = sorted_colours[i][0]
sorted_colours[i] = colour, dist
colour = old
distances[colour].pop(0)
return {colour_names[i]: c[0] for i, c in enumerate(sorted_colours)}
def get_scheme(scheme: str) -> DynamicScheme:
if scheme == "content":
return SchemeContent
if scheme == "expressive":
return SchemeExpressive
if scheme == "fidelity":
return SchemeFidelity
if scheme == "fruitsalad":
return SchemeFruitSalad
if scheme == "monochrome":
return SchemeMonochrome
if scheme == "neutral":
return SchemeNeutral
if scheme == "rainbow":
return SchemeRainbow
if scheme == "tonalspot":
return SchemeTonalSpot
return SchemeVibrant
def get_alt(i: int) -> str:
names = ["default", "alt1", "alt2"]
return names[i]
if __name__ == "__main__":
light = sys.argv[1] == "light"
scheme = sys.argv[2]
primaries = sys.argv[3].split(" ")
colours_in = sys.argv[4].split(" ")
out_path = sys.argv[5]
base = light_colours if light else dark_colours
# Convert to HLS
base_colours = [hex_to_hls(c) for c in colours_in]
# Sort colours and turn into dict
base_colours = smart_sort(base_colours, base)
# Adjust colours
MatScheme = get_scheme(scheme)
for name, hls in base_colours.items():
if scheme == "monochrome":
base_colours[name] = grayscale(hls, light)
else:
argb = hex_to_argb(hls_to_hex(*hls))
mat_scheme = MatScheme(Hct.from_int(argb), not light, 0)
colour = MaterialDynamicColors.primary.get_hct(mat_scheme)
# Boost neutral scheme colours
if scheme == "neutral":
colour.chroma += 10
base_colours[name] = hex_to_hls(
"{:02X}{:02X}{:02X}".format(*colour.to_rgba()[:3])
)
# Layers and accents
for i, primary in enumerate(primaries):
material = {}
primary_argb = hex_to_argb(primary)
primary_scheme = MatScheme(Hct.from_int(primary_argb), 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))
# TODO: eventually migrate to material for layers
colours = {
**material,
"text": material["onBackground"],
"subtext1": material["onSurfaceVariant"],
"subtext0": material["outline"],
"overlay2": mix(material["surface"], material["outline"], 0.86),
"overlay1": mix(material["surface"], material["outline"], 0.71),
"overlay0": mix(material["surface"], material["outline"], 0.57),
"surface2": mix(material["surface"], material["outline"], 0.43),
"surface1": mix(material["surface"], material["outline"], 0.29),
"surface0": mix(material["surface"], material["outline"], 0.14),
"base": material["surface"],
"mantle": darken(material["surface"], 0.03),
"crust": darken(material["surface"], 0.05),
"success": harmonize(base[8], primary_argb),
}
for name, hls in base_colours.items():
colours[name] = harmonize(hls_to_hex(*hls), primary_argb)
out_file = Path(f"{out_path}/{scheme}/{get_alt(i)}/{sys.argv[1]}.txt")
out_file.parent.mkdir(parents=True, exist_ok=True)
colour_arr = [
f"{name} {hls_to_hex(*colour)}" for name, colour in colours.items()
]
out_file.write_text("\n".join(colour_arr))