#!/usr/bin/env python3 """Generate source/Themes.mc from the design kit in design/. Ground truth is the seven watchface-NN-*.svg files (produced by design/rebuild.py from the original raster comps). Every colour written to Themes.mc is measured from those files -- nothing here is hand-picked. python3 tools/gen_themes.py Extracted per theme: ringTop[21] lit colour of each top tick, index 0 = 6.5 deg .. 20 = 86.5 deg ringBottom[21] lit colour of each bottom tick, index 0 = 6.0 deg .. 20 = 66.0 deg tickOff colour of an unlit tick anchorDot the four dots between the arcs bandFill/Text date band background / foreground colon the two colon blocks hours[11] vertical gradient ramp of the hour digits, top -> bottom minutes[11] ditto for the minute digits accent icons and DST/STEP labels textPrimary complication and bottom-row values Only 14 of the 21 bottom ticks are lit in any comp, so indices 14..20 of ringBottom are extrapolated (least-squares fit over the last 8 measured ticks). Those ticks only appear once a metric passes ~67%. """ import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) DESIGN = os.path.join(ROOT, "design") OUT = os.path.join(ROOT, "source", "Themes.mc") ORDER = ["01", "02", "03", "04", "05", "06", "07"] N_TICK = 21 BOT_MEASURED = 14 # lit ticks in the bottom-left arc of every comp FIT_WINDOW = 8 # ticks used for the extrapolation fit def rgb(h): h = h.lstrip("#") return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) def hexs(c): return "0x%02X%02X%02X" % tuple(max(0, min(255, int(round(v)))) for v in c) def median(colours): cols = [rgb(c) for c in colours] return tuple(sorted(c[i] for c in cols)[len(cols) // 2] for i in range(3)) def group_colours(svg, gid): m = re.search(r'(.*?)' % gid, svg, re.S) if m is None: raise SystemExit("missing group %s" % gid) return re.findall(r'fill="(#[0-9A-Fa-f]{6})"', m.group(1)) def path_fill(svg, pid): m = re.search(r']*fill="(#[0-9A-Fa-f]{6})"' % pid, svg) return m.group(1) if m else None def gradient_ramp(svg, gid): """The 11 usable stops of a digit gradient. rebuild.py samples the glyph row by row, so the first and last stop are the antialiased edge rows and read far too dark. Drop them. """ m = re.search(r'(.*?)' % gid, svg, re.S) stops = re.findall(r'stop-color="(#[0-9A-Fa-f]{6})"', m.group(1)) return stops[1:-1] def extrapolate(ramp): """Extend a measured colour ramp to N_TICK entries by linear fit.""" known = [rgb(c) for c in ramp] n = len(known) if n >= N_TICK: return known[:N_TICK] xs = list(range(n - FIT_WINDOW, n)) out = list(known) for i in range(n, N_TICK): nxt = [] for ch in range(3): ys = [known[x][ch] for x in xs] mx = sum(xs) / float(len(xs)) my = sum(ys) / float(len(ys)) den = sum((x - mx) ** 2 for x in xs) slope = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / den if den else 0.0 nxt.append(max(0, min(255, my + slope * (i - mx)))) out.append(tuple(nxt)) return out def read_theme(tid): hits = [f for f in os.listdir(DESIGN) if f.startswith("watchface-%s-" % tid) and f.endswith(".svg")] if not hits: raise SystemExit("no SVG for theme %s in %s" % (tid, DESIGN)) path = os.path.join(DESIGN, hits[0]) svg = open(path).read() name = re.search(r"Watch face \d+ (\w+)", svg).group(1) top_r = group_colours(svg, "ring-top-right") # fully lit in every comp top_l = group_colours(svg, "ring-top-left") # lit from the far end bot_l = group_colours(svg, "ring-bottom-left") # lit from index 0 # An unlit tick is whatever colour the dim end of the top-left arc has. tick_off = median(top_l[:8]) ring_top = [rgb(c) for c in top_r] ring_bottom = extrapolate(bot_l[:BOT_MEASURED]) band = re.search(r'\s*]*fill="(#[0-9A-Fa-f]{6})"', svg).group(1) dot = re.search(r'\s*]*fill="(#[0-9A-Fa-f]{6})"', svg).group(1) colon = re.search(r']*fill="(#[0-9A-Fa-f]{6})"', svg).group(1) accent = median([path_fill(svg, p) for p in ("battery-icon", "sunrise-icon", "weather-icon", "dist-label", "steps-label")]) primary = median([path_fill(svg, p) for p in ("battery-value", "sunrise-value", "weather-value", "dist-value", "steps-value")]) return { "id": tid, "name": name, "ringTop": ring_top, "ringBottom": ring_bottom, "tickOff": tick_off, "anchorDot": rgb(dot), "bandFill": rgb(band), "bandText": rgb(path_fill(svg, "bt-icon")), "colon": rgb(colon), "hours": [rgb(c) for c in gradient_ramp(svg, "gHours")], "minutes": [rgb(c) for c in gradient_ramp(svg, "gMinutes")], "accent": accent, "textPrimary": primary, } def fmt_list(colours, indent): pad = " " * indent out, line = [], pad + "[" for i, c in enumerate(colours): piece = hexs(c) + ("," if i < len(colours) - 1 else "]") if len(line) + len(piece) > 108: out.append(line) line = pad + " " line += piece out.append(line) return "\n".join(out) def main(): themes = [read_theme(t) for t in ORDER] L = [] L.append("// AUTO-GENERATED by tools/gen_themes.py from design/watchface-*.svg.") L.append("// Do not edit by hand -- change the design kit and re-run the generator.") L.append("//") L.append("// ringTop/ringBottom lit colour per tick, index 0 nearest 12 / 6 o'clock") L.append("// hours/minutes 11-stop vertical gradient of the time digits, top -> bottom") L.append("import Toybox.Lang;") L.append("") L.append("module Themes {") L.append("") L.append(" const NAMES = [") for t in themes: L.append(' "%s",' % t["name"]) L.append(" ];") L.append("") for key in ("ringTop", "ringBottom", "hours", "minutes"): L.append(" const %s = [" % key.upper()) for t in themes: L.append(" // %s %s" % (t["id"], t["name"])) L.append(fmt_list(t[key], 8) + ",") L.append(" ];") L.append("") for key in ("tickOff", "anchorDot", "bandFill", "bandText", "colon", "accent", "textPrimary"): vals = ", ".join(hexs(t[key]) for t in themes) L.append(" const %s = [%s];" % (key.upper(), vals)) L.append("}") text = "\n".join(L) + "\n" open(OUT, "w").write(text) print("wrote %s (%d themes, %d bytes)" % (OUT, len(themes), len(text))) for t in themes: print(" %s %-8s accent=%s text=%s tickOff=%s" % (t["id"], t["name"], hexs(t["accent"]), hexs(t["textPrimary"]), hexs(t["tickOff"]))) if __name__ == "__main__": sys.exit(main())