Files
sow-nodebb-theme/prototype/contrast.py
T
archvillainetteandClaude Opus 5 ea36116e62 prototype(#53): palette candidates as runtime token swaps
Throwaway. Tokenises the theme's literal colours so candidate palettes can be
swapped in the browser, adds three candidates (Plum Noir one-metal, two-metal
pewter, two-metal plus a champagne inset), a WCAG checker, a seeder and the
screenshots they are judged on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:31:41 +02:00

79 lines
2.2 KiB
Python

#!/usr/bin/env python3
"""Throwaway WCAG 2.x contrast check for the #53 palette candidates.
Same method as the #50 headroom research: relative luminance, (L1+.05)/(L2+.05),
alpha colours flattened over their real backdrop first (source-over, sRGB).
Self-checks against WCAG's published values before printing anything.
"""
def _srgb(c):
c /= 255
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
def rgb(h):
h = h.lstrip("#")
return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))
def lum(h):
r, g, b = rgb(h)
return 0.2126 * _srgb(r) + 0.7152 * _srgb(g) + 0.0722 * _srgb(b)
def flatten(fg, alpha, bg):
f, b = rgb(fg), rgb(bg)
return "#%02x%02x%02x" % tuple(round(f[i] * alpha + b[i] * (1 - alpha)) for i in range(3))
def ratio(a, b):
la, lb = lum(a), lum(b)
hi, lo = max(la, lb), min(la, lb)
return (hi + 0.05) / (lo + 0.05)
def _selfcheck():
assert round(ratio("#000000", "#ffffff"), 1) == 21.0
assert round(ratio("#767676", "#ffffff"), 2) == 4.54
assert round(ratio("#949494", "#ffffff"), 2) == 3.03
assert ratio("#959595", "#ffffff") < 3.0
assert flatten("#ffffff", 0.5, "#000000") == "#808080"
_selfcheck()
# candidate surfaces (the invented ones need proving; the rest come from #50)
SURFACES = {
"bg #150E13": "#150E13",
"bg-deep #0C0810": "#0C0810",
"panel #21161C": "#21161C",
"panel-2 #180F16": "#180F16",
"panel-3 / Plum Noir #351E28": "#351E28",
"plum-soft #4A2A38": "#4A2A38",
"paper / champagne #EFE5D2": "#EFE5D2",
}
INKS = {
"Bone #E6E0D6": "#E6E0D6",
"Ash #B9B2A6": "#B9B2A6",
"Dust #9A9086": "#9A9086",
"Ledger Ink #D8C28A": "#D8C28A",
"Tarnished Gold #C2A35A": "#C2A35A",
"Old Gilt #A8893F": "#A8893F",
"Gold Lit #E0C878": "#E0C878",
"Pewter #9EA7B3": "#9EA7B3",
"Ink-on-light #351E28": "#351E28",
"Red #8E3438": "#8E3438",
"Stamp #A84A4E": "#A84A4E",
}
if __name__ == "__main__":
w = max(len(k) for k in INKS)
for sname, s in SURFACES.items():
print(f"\n== {sname}")
for iname, i in INKS.items():
r = ratio(i, s)
tag = "AA" if r >= 4.5 else ("large/non-text" if r >= 3.0 else "FAIL")
print(f" {iname:<{w}} {r:6.2f} {tag}")