- Split render into FieldTable/Fields/Icons/Layout/Settings modules - Add Fenix8V3Background, Owm (weather), Solar modules - Add tools/ generators (gen_fields/gen_icons/gen_themes/gen_preview_svg) - Tune README/manifest/strings/settings
208 lines
7.8 KiB
Python
208 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
||
"""从 design/ 的设计稿生成 source/Themes.mc。
|
||
|
||
事实来源是七个 watchface-NN-*.svg(原作者用 design/rebuild.py 从位图逆向出来的)。
|
||
写进 Themes.mc 的每一个颜色都是从这些文件里**量出来的**,没有一个是手挑的。
|
||
|
||
python3 tools/gen_themes.py
|
||
|
||
每套主题提取出:
|
||
|
||
ringTop[21] 顶弧每根刻度点亮时的颜色,下标 0 = 6.5°,20 = 86.5°
|
||
ringBottom[21] 底弧每根刻度点亮时的颜色,下标 0 = 6.0°,20 = 66.0°
|
||
tickOff 未点亮刻度的颜色
|
||
anchorDot 四段弧之间的锚点圆点
|
||
bandFill/Text 日期带的底色 / 文字色
|
||
colon 两块冒号
|
||
hours[11] 小时数字的竖向渐变,从上到下
|
||
minutes[11] 分钟数字的竖向渐变
|
||
accent 图标与 DST/STEP 标签
|
||
textPrimary 各数据位与底部行的数值
|
||
|
||
⚠️ 已知近似:设计稿里底弧最多只点亮 14/21 根,所以 ringBottom 的第 15–21 项
|
||
无法实测,由最后 8 根做最小二乘线性外推得到。只有当指标超过约 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'<g id="%s">(.*?)</g>' % 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'<path id="%s"[^>]*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'<linearGradient id="%s".*?>(.*?)</linearGradient>' % 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"<title>Watch face \d+ (\w+)</title>", 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'<g id="date-band">\s*<rect[^>]*fill="(#[0-9A-Fa-f]{6})"', svg).group(1)
|
||
dot = re.search(r'<g id="ring-anchors">\s*<circle[^>]*fill="(#[0-9A-Fa-f]{6})"', svg).group(1)
|
||
colon = re.search(r'<g id="time-colon"><rect[^>]*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("// ⚠️ 本文件由 tools/gen_themes.py 从 design/watchface-*.svg 自动生成,请勿手改。")
|
||
L.append("// 要改配色就改设计稿里的 SVG,然后重新跑一遍生成器。")
|
||
L.append("//")
|
||
L.append("// 七套主题的全部颜色数据。各数组的下标就是主题序号(0=Ember … 6=Kelp)。")
|
||
L.append("//")
|
||
L.append("// RINGTOP/RINGBOTTOM 每根刻度点亮时的颜色,各 21 项。")
|
||
L.append("// 下标 0 是最靠近 12 点 / 6 点的那根。")
|
||
L.append("// 这是从设计稿里**逐根取样**的实测值,不是插值近似。")
|
||
L.append("// HOURS/MINUTES 时间数字的竖向渐变,各 11 个停止点,从上到下。")
|
||
L.append("// TICKOFF 未点亮刻度的颜色")
|
||
L.append("// ANCHORDOT 四个锚点圆点")
|
||
L.append("// BANDFILL/BANDTEXT 日期带的底色 / 文字色")
|
||
L.append("// ACCENT 图标与底部标签")
|
||
L.append("// TEXTPRIMARY 各项数值")
|
||
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())
|