summaryrefslogtreecommitdiff
path: root/src/caelestia/utils/material/generator.py
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-06-12 15:48:02 +1000
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-06-12 15:48:02 +1000
commit194826efaa95480bfe1799f889ca5c02571b3e36 (patch)
treea4dd4879f99d1087ef8d51ea328f48e9c578f397 /src/caelestia/utils/material/generator.py
parenttheme: better spicetify colours (diff)
downloadcaelestia-cli-194826efaa95480bfe1799f889ca5c02571b3e36.tar.gz
caelestia-cli-194826efaa95480bfe1799f889ca5c02571b3e36.tar.bz2
caelestia-cli-194826efaa95480bfe1799f889ca5c02571b3e36.zip
feat: generate dynamic schemes
Diffstat (limited to 'src/caelestia/utils/material/generator.py')
-rwxr-xr-xsrc/caelestia/utils/material/generator.py192
1 files changed, 192 insertions, 0 deletions
diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py
new file mode 100755
index 0000000..33ff0e8
--- /dev/null
+++ b/src/caelestia/utils/material/generator.py
@@ -0,0 +1,192 @@
+from materialyoucolor.blend import Blend
+from materialyoucolor.dynamiccolor.material_dynamic_colors import (
+ DynamicScheme,
+ MaterialDynamicColors,
+)
+from materialyoucolor.hct import Hct
+from materialyoucolor.hct.cam16 import Cam16
+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
+
+
+def hex_to_hct(hex: str) -> Hct:
+ return Hct.from_int(int(f"0xFF{hex}", 16))
+
+
+light_colours = [
+ hex_to_hct("dc8a78"),
+ hex_to_hct("dd7878"),
+ hex_to_hct("ea76cb"),
+ hex_to_hct("8839ef"),
+ hex_to_hct("d20f39"),
+ hex_to_hct("e64553"),
+ hex_to_hct("fe640b"),
+ hex_to_hct("df8e1d"),
+ hex_to_hct("40a02b"),
+ hex_to_hct("179299"),
+ hex_to_hct("04a5e5"),
+ hex_to_hct("209fb5"),
+ hex_to_hct("1e66f5"),
+ hex_to_hct("7287fd"),
+]
+
+dark_colours = [
+ hex_to_hct("f5e0dc"),
+ hex_to_hct("f2cdcd"),
+ hex_to_hct("f5c2e7"),
+ hex_to_hct("cba6f7"),
+ hex_to_hct("f38ba8"),
+ hex_to_hct("eba0ac"),
+ hex_to_hct("fab387"),
+ hex_to_hct("f9e2af"),
+ hex_to_hct("a6e3a1"),
+ hex_to_hct("94e2d5"),
+ hex_to_hct("89dceb"),
+ hex_to_hct("74c7ec"),
+ hex_to_hct("89b4fa"),
+ hex_to_hct("b4befe"),
+]
+
+colour_names = [
+ "rosewater",
+ "flamingo",
+ "pink",
+ "mauve",
+ "red",
+ "maroon",
+ "peach",
+ "yellow",
+ "green",
+ "teal",
+ "sky",
+ "sapphire",
+ "blue",
+ "lavender",
+ "success",
+ "error",
+]
+
+
+def grayscale(colour: Hct, light: bool) -> None:
+ colour.chroma = 0
+
+
+def mix(a: Hct, b: Hct, w: float) -> Hct:
+ return Hct.from_int(Blend.cam16_ucs(a.to_int(), b.to_int(), w))
+
+
+def harmonize(a: Hct, b: Hct) -> Hct:
+ return Hct.from_int(Blend.harmonize(a.to_int(), b.to_int()))
+
+
+def lighten(colour: Hct, amount: float) -> Hct:
+ diff = (100 - colour.tone) * amount
+ return Hct.from_hct(colour.hue, colour.chroma + diff / 2, colour.tone + diff)
+
+
+def darken(colour: Hct, amount: float) -> Hct:
+ diff = colour.tone * amount
+ return Hct.from_hct(colour.hue, colour.chroma + diff / 2, colour.tone - diff)
+
+
+def distance(colour: Cam16, base: Cam16) -> float:
+ return colour.distance(base)
+
+
+def smart_sort(colours: list[Hct], base: list[Hct]) -> dict[str, Hct]:
+ sorted_colours = [None] * len(colours)
+ distances = {}
+
+ cams = [(c, Cam16.from_int(c.to_int())) for c in colours]
+ base_cams = [Cam16.from_int(c.to_int()) for c in base]
+
+ for colour, cam in cams:
+ dist = [(i, distance(cam, b)) for i, b in enumerate(base_cams)]
+ 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 gen_scheme(scheme, primary: Hct, colours: list[Hct]) -> dict[str, str]:
+ light = scheme.mode == "light"
+ base = light_colours if light else dark_colours
+
+ # Sort colours and turn into dict
+ colours = smart_sort(colours, base)
+
+ # Harmonize colours
+ for name, hct in colours.items():
+ if scheme.variant == "monochrome":
+ grayscale(hct, light)
+ else:
+ harmonized = harmonize(hct, primary)
+ colours[name] = darken(harmonized, 0.35) if light else lighten(harmonized, 0.65)
+
+ # Material colours
+ primary_scheme = get_scheme(scheme.variant)(primary, not light, 0)
+ for colour in vars(MaterialDynamicColors).keys():
+ colour_name = getattr(MaterialDynamicColors, colour)
+ if hasattr(colour_name, "get_hct"):
+ colours[colour] = colour_name.get_hct(primary_scheme)
+
+ # FIXME: deprecated stuff
+ colours["text"] = colours["onBackground"]
+ colours["subtext1"] = colours["onSurfaceVariant"]
+ colours["subtext0"] = colours["outline"]
+ colours["overlay2"] = mix(colours["surface"], colours["outline"], 0.86)
+ colours["overlay1"] = mix(colours["surface"], colours["outline"], 0.71)
+ colours["overlay0"] = mix(colours["surface"], colours["outline"], 0.57)
+ colours["surface2"] = mix(colours["surface"], colours["outline"], 0.43)
+ colours["surface1"] = mix(colours["surface"], colours["outline"], 0.29)
+ colours["surface0"] = mix(colours["surface"], colours["outline"], 0.14)
+ colours["base"] = colours["surface"]
+ colours["mantle"] = darken(colours["surface"], 0.03)
+ colours["crust"] = darken(colours["surface"], 0.05)
+ colours["success"] = harmonize(base[8], primary)
+
+ # For debugging
+ print("\n".join(["{}: \x1b[48;2;{};{};{}m \x1b[0m".format(n, *c.to_rgba()[:3]) for n, c in colours.items()]))
+
+ return {k: hex(v.to_int())[4:] for k, v in colours.items()}