#!/usr/bin/env python3
"""Port the daisyUI themes from waxx's app.css into the moire wallpaper.
tools/themes.py ../waxx/assets/css/app.css
writes moire/theme/contents/ui/themes.js for the Plasma wallpaper and
droid/app/src/main/java/io/neiam/moire/Themes.kt for the Android one.
Both are committed, so building needs neither waxx nor this script;
re-run it when waxx's palette changes.
Colours are kept in OKLCH, as the CSS defines them, so the wallpaper
can blend between themes by turning the hue rather than mixing sRGB,
where a crossfade between two saturated colours goes muddy halfway.
The sRGB hex beside each is for people reading the file.
"""
import math
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "moire/theme/contents/ui/themes.js"
OUT_KT = ROOT / "droid/app/src/main/java/io/neiam/moire/Themes.kt"
# The wallpaper's four roles, and the daisyUI variable each is taken from.
ROLES = {
"base": "--color-base-100", # the ground
"deep": "--color-base-300", # the ground at the edges
"ink": "--color-accent", # the moire
"tone": "--color-primary", # the halftone
}
BLOCK = re.compile(r'@plugin\s+"[^"]*daisyui-theme"\s*\{(.*?)\}', re.S)
NAME = re.compile(r'name:\s*"([^"]+)"')
VAR = re.compile(r"(--color-[a-z0-9-]+):\s*oklch\(([\d.]+)%\s+([\d.]+)\s+([\d.]+)\)")
def oklch_to_srgb(l, c, h):
a = c * math.cos(math.radians(h))
b = c * math.sin(math.radians(h))
l_ = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3
m_ = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3
s_ = (l - 0.0894841775 * a - 1.2914855480 * b) ** 3
rgb = (
4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_,
-1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_,
-0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_,
)
def encode(x):
x = min(max(x, 0.0), 1.0)
return 12.92 * x if x <= 0.0031308 else 1.055 * x ** (1 / 2.4) - 0.055
return "#" + "".join(f"{round(encode(x) * 255):02x}" for x in rgb)
def parse(css):
themes = []
for block in BLOCK.findall(css):
name = NAME.search(block)
if not name:
continue
colours = {m[0]: (float(m[1]) / 100, float(m[2]), float(m[3])) for m in VAR.findall(block)}
missing = [v for v in ROLES.values() if v not in colours]
if missing:
sys.exit(f"theme {name[1]}: missing {', '.join(missing)}")
themes.append((name[1], {role: colours[var] for role, var in ROLES.items()}))
return themes
def render(themes, source):
out = [
f"// Generated by tools/themes.py from waxx's {source}; do not edit.",
"// OKLCH as [lightness 0-1, chroma, hue degrees]; hex is for reading.",
".pragma library",
"",
"var themes = {",
]
for name, roles in themes:
out.append(f' "{name}": {{')
for role, (l, c, h) in roles.items():
out.append(f" {role}: [{l:.4f}, {c:.3f}, {h:.3f}], // {oklch_to_srgb(l, c, h)}")
out.append(" },")
out += ["}", "", "var names = Object.keys(themes)", ""]
return "\n".join(out)
def render_kotlin(themes, source):
out = [
f"// Generated by tools/themes.py from waxx's {source}; do not edit.",
"package io.neiam.moire",
"",
"/** A colour as OKLCH: lightness 0-1, chroma, hue in degrees. */",
"data class Lch(val l: Double, val c: Double, val h: Double)",
"",
"/** The wallpaper's four roles; see tools/themes.py for their sources. */",
"data class Theme(val base: Lch, val deep: Lch, val ink: Lch, val tone: Lch)",
"",
"object Themes {",
" val all: Map<String, Theme> = linkedMapOf(",
]
for name, roles in themes:
out.append(f' "{name}" to Theme(')
for role, (l, c, h) in roles.items():
out.append(f" {role} = Lch({l:.4f}, {c:.3f}, {h:.3f}), // {oklch_to_srgb(l, c, h)}")
out.append(" ),")
out += [" )", "", " val names: List<String> = all.keys.toList()", "}", ""]
return "\n".join(out)
def main():
if len(sys.argv) != 2:
sys.exit(__doc__.strip().splitlines()[2].strip())
css = Path(sys.argv[1])
themes = parse(css.read_text())
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(render(themes, css.name))
OUT_KT.parent.mkdir(parents=True, exist_ok=True)
OUT_KT.write_text(render_kotlin(themes, css.name))
print(f"wrote {OUT.relative_to(ROOT)} and {OUT_KT.relative_to(ROOT)}: {', '.join(n for n, _ in themes)}")
if __name__ == "__main__":
main()
neiam
/neiam-devices
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
4.6 KiB · text
History
3f7bd79