#!/usr/bin/env python3
"""Render preview-themes.svg and preview.html by replaying the face's draw calls.
Colours are parsed straight out of source/Themes.mc and the geometry constants
are the same literals as source/Fenix8V3View.mc, so the preview cannot drift
from the firmware. Text is drawn with a web font rather than Garmin's Roboto
Condensed / Bionic, so judge geometry and colour here, not glyph shapes.
python3 tools/gen_preview_svg.py
"""
import math
import os
import re
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
OUT = os.path.join(ROOT, "preview-themes.svg")
OUT_HTML = os.path.join(ROOT, "preview.html")
# ---------------------------------------------------------------- geometry --
R_IN, R_OUT = 231.09, 249.10
TOP_N, TOP_PH, TOP_PITCH, TOP_HW = 21, 6.500, 4.000, 1.554
BOT_N, BOT_PH, BOT_PITCH, BOT_HW = 21, 6.005, 3.000, 1.043
DOT_RAD, DOT_R = 240.10, 9.00
DOT_ANG = [0.0, 93.15, 180.0, 266.85]
BATT_X, BATT_Y, BATT_W, BATT_H = 192.70, 44.02, 26.20, 17.81
BATT_WALL, BATT_TW, BATT_TH = 2.20, 2.53, 9.20
BATT_VX, BATT_CY = 232.00, 53.20
LEFT_X, RIGHT_X, ICON_CY, VALUE_CY = 157.00, 343.70, 90.00, 131.06
TIME_CY, HOURS_R, MINUTES_L = 221.10, 211.40, 276.02
COLON_X, COLON_W, COLON_H, COLON_Y1, COLON_Y2 = 237.88, 24.24, 21.30, 183.68, 234.57
BAND_Y, BAND_H, BAND_CY = 286.30, 44.19, 308.10
BT_CX, BT_CY, BT_HW, BT_HH = 36.88, 306.99, 8.89, 12.92
DOW_CX, MD_CX, AMPM_R = 178.05, 298.28, 465.89
BOT_VAL_CY, BOT_LAB_CY, DIST_R, STEP_L = 366.76, 410.88, 234.30, 267.40
CAP_BAND, CAP_COMP, CAP_TIME = 25.34, 31.50, 98.70
DOW = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]
MON = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN",
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
# ------------------------------------------------------------ Themes.mc in --
def load_themes():
src = open(os.path.join(ROOT, "source", "Themes.mc")).read()
def const(name):
m = re.search(r"const %s = \[(.*?)\n \];" % name, src, re.S)
if m: # nested: one row per theme
rows, cur = [], []
for line in m.group(1).split("\n"):
if line.strip().startswith("//"):
if cur:
rows.append(cur)
cur = []
continue
cur += [int(v, 16) for v in re.findall(r"0x([0-9A-Fa-f]{6})", line)]
if cur:
rows.append(cur)
return rows
m = re.search(r"const %s = \[(.*?)\];" % name, src)
return [int(v, 16) for v in re.findall(r"0x([0-9A-Fa-f]{6})", m.group(1))]
names = re.findall(r'^\s+"(\w+)",', src, re.M)
return names, {k: const(k) for k in
("RINGTOP", "RINGBOTTOM", "HOURS", "MINUTES", "TICKOFF",
"ANCHORDOT", "BANDFILL", "BANDTEXT", "COLON",
"ACCENT", "TEXTPRIMARY")}
def hx(c):
return "#%06X" % (c & 0xFFFFFF)
def polar(ang, r):
t = math.radians(ang)
return 250.0 + math.sin(t) * r, 250.0 - math.cos(t) * r
# ------------------------------------------------------------------ pieces --
def ring(th, T, p):
out = []
top, bot, off = T["RINGTOP"][th], T["RINGBOTTOM"][th], T["TICKOFF"][th]
def arc(ramp, lit, ph, pitch, hw, n, base, mirror, from_end):
for i in range(n):
a = ph + pitch * i
on = (i >= n - lit) if from_end else (i < lit)
ang = base + (-a if mirror else a)
c = [polar(ang - hw, R_OUT), polar(ang + hw, R_OUT),
polar(ang + hw, R_IN), polar(ang - hw, R_IN)]
out.append(' ' % (
" ".join("%.2f,%.2f" % q for q in c), hx(ramp[i] if on else off)))
arc(top, round(p[0] * TOP_N), TOP_PH, TOP_PITCH, TOP_HW, TOP_N, 0.0, False, True)
arc(top, round(p[1] * TOP_N), TOP_PH, TOP_PITCH, TOP_HW, TOP_N, 0.0, True, True)
arc(bot, round(p[3] * BOT_N), BOT_PH, BOT_PITCH, BOT_HW, BOT_N, 180.0, False, False)
arc(bot, round(p[2] * BOT_N), BOT_PH, BOT_PITCH, BOT_HW, BOT_N, 180.0, True, False)
for a in DOT_ANG:
x, y = polar(a, DOT_RAD)
out.append(' '
% (x, y, DOT_R, hx(T["ANCHORDOT"][th])))
return "".join(out)
def txt(x, cy, cap, s, anchor, col, weight=700):
return ('%s '
% (x, cy, cap / 0.72, weight, col, anchor, s))
def grad_txt(gid, x, cy, cap, s, anchor, ramp):
y0, y1 = cy - cap / 2, cy + cap / 2
n = len(ramp)
stops = "".join(' ' % ((i + 0.5) / n, hx(c))
for i, c in enumerate(ramp))
d = ('%s ' % (gid, y0, y1, stops))
return d, txt(x, cy, cap, s, anchor, "url(#%s)" % gid)
def battery(x, cy, k, col, level):
w, h, wall = BATT_W * k, BATT_H * k, BATT_WALL * k
y = cy - h / 2
o = [' '
% (x, y, w, h, 3.4 * k, col),
' '
% (x + w, cy - BATT_TH * k / 2, BATT_TW * k, BATT_TH * k, col),
' '
% (x + wall, y + wall, w - 2 * wall, h - 2 * wall)]
if level > 0:
o.append(' '
% (x + wall, y + wall, (w - 2 * wall) * level, h - 2 * wall, col))
return "".join(o)
def icon_sun(cx, cy, k, col, direction=1):
ox, oy = cx - 156.32 * k, cy - 90.34 * k
rx, ry = 7.45 * k, 8.93 * k
bx, by = ox + 156.39 * k, oy + 94.02 * k
pts = [(bx + math.cos(math.radians(180 - 15 * i)) * rx,
by - math.sin(math.radians(180 - 15 * i)) * ry) for i in range(13)]
o = [' '
% (" ".join("%.2f,%.2f" % q for q in pts), col)]
def ln(x1, y1, x2, y2, w):
return (' '
% (x1, y1, x2, y2, col, w))
w1 = 2.96 * k
o += [ln(ox + 156.39 * k, oy + 79.4 * k, ox + 156.39 * k, oy + 81.1 * k, w1),
ln(ox + 148.2 * k, oy + 83.3 * k, ox + 146.5 * k, oy + 84.5 * k, w1),
ln(ox + 164.6 * k, oy + 84.5 * k, ox + 166.3 * k, oy + 83.3 * k, w1),
ln(ox + 142.6 * k, oy + 92.5 * k, ox + 144.7 * k, oy + 92.5 * k, w1),
ln(ox + 168.0 * k, oy + 92.5 * k, ox + 170.1 * k, oy + 92.5 * k, w1)]
w2, hy = 2.6 * k, oy + 98.45 * k
o += [ln(ox + 142.3 * k, hy, ox + 170.3 * k, hy, w2),
ln(ox + 152.6 * k, hy, ox + 156.3 * k, hy + direction * 3.05 * k, w2),
ln(ox + 156.3 * k, hy + direction * 3.05 * k, ox + 160.0 * k, hy, w2)]
return "".join(o)
def icon_cloud(cx, cy, k, col):
ox, oy = cx - 343.55 * k, cy - 89.25 * k
def ln(x1, y1, x2, y2, w):
return (' '
% (x1, y1, x2, y2, col, w))
def body(grow, fill):
return (' '
' '
' '
% (ox + 336.6 * k, oy + 94.4 * k, 8.7 * k + grow, fill,
ox + 346.4 * k, oy + 96.9 * k, 6.3 * k + grow, fill,
ox + 328.0 * k - grow, oy + 95.5 * k - grow,
24.6 * k + 2 * grow, 7.75 * k + grow, 3.4 * k, fill))
o = [' '
% (ox + 347.0 * k, oy + 88.9 * k, 7.8 * k, col)]
o += [ln(ox + 346.2 * k, oy + 76.0 * k, ox + 346.2 * k, oy + 78.7 * k, 2.3 * k),
ln(ox + 354.9 * k, oy + 87.7 * k, ox + 357.5 * k, oy + 87.7 * k, 2.3 * k),
ln(ox + 339.5 * k, oy + 79.5 * k, ox + 338.1 * k, oy + 80.9 * k, 2.7 * k),
ln(ox + 353.5 * k, oy + 80.9 * k, ox + 354.6 * k, oy + 79.8 * k, 2.7 * k)]
o.append(body(1.7 * k, "#000000"))
o.append(body(0.0, col))
return "".join(o)
def bluetooth(col):
l, r = BT_CX - BT_HW, BT_CX + BT_HW
t, b = BT_CY - BT_HH, BT_CY + BT_HH
qt, qb = BT_CY - BT_HH / 2, BT_CY + BT_HH / 2
pts = [(l, qt), (r, qb), (BT_CX, b), (BT_CX, t), (r, qt), (l, qb)]
return (' '
% (" ".join("%.2f,%.2f" % q for q in pts), col))
def face(th, names, T, data):
accent, primary = hx(T["ACCENT"][th]), hx(T["TEXTPRIMARY"][th])
defs, s = [], [' ',
ring(th, T, data["ring"])]
s.append(battery(BATT_X, BATT_CY, 1.0, accent, data["batt"]))
s.append(txt(BATT_VX, BATT_CY, CAP_BAND, "%d%%" % round(data["batt"] * 100),
"start", primary))
s.append(icon_sun(LEFT_X, ICON_CY, 1.0, accent))
s.append(txt(LEFT_X, VALUE_CY, CAP_COMP, data["sunrise"], "middle", primary))
s.append(icon_cloud(RIGHT_X, ICON_CY, 1.0, accent))
s.append(txt(RIGHT_X, VALUE_CY, CAP_COMP, data["hilo"], "middle", primary))
d, t = grad_txt("gH%d" % th, HOURS_R, TIME_CY, CAP_TIME, data["hh"], "end",
T["HOURS"][th])
defs.append(d)
s.append(t)
d, t = grad_txt("gM%d" % th, MINUTES_L, TIME_CY, CAP_TIME, data["mm"], "start",
T["MINUTES"][th])
defs.append(d)
s.append(t)
for y in (COLON_Y1, COLON_Y2):
s.append(' '
% (COLON_X, y, COLON_W, COLON_H, hx(T["COLON"][th])))
band, btxt = hx(T["BANDFILL"][th]), hx(T["BANDTEXT"][th])
s.append(' '
% (BAND_Y, BAND_H, band))
s.append(bluetooth(btxt))
s.append(txt(DOW_CX, BAND_CY, CAP_BAND, data["dow"], "middle", btxt))
s.append(txt(MD_CX, BAND_CY, CAP_BAND, data["md"], "middle", btxt))
s.append(txt(AMPM_R, BAND_CY, CAP_BAND, data["ampm"], "end", btxt))
s.append(txt(DIST_R, BOT_VAL_CY, CAP_COMP, data["dist"], "end", primary))
s.append(txt(DIST_R, BOT_LAB_CY, CAP_COMP, "DST", "end", accent))
s.append(txt(STEP_L, BOT_VAL_CY, CAP_COMP, data["steps"], "start", primary))
s.append(txt(STEP_L, BOT_LAB_CY, CAP_COMP, "STEP", "start", accent))
return "".join(defs), "".join(s)
HTML = """
Fenix 8 V3 - 表盘预览
Fenix 8 V3 表盘预览
由 tools/gen_preview_svg.py 从 source/Themes.mc
生成,几何常量与 source/Fenix8V3View.mc 一致。文字用网页字体渲染,
真机为 Garmin 内置 Roboto / Bionic 字体,字形会有差异。
%(faces)s
%(buttons)s
七套主题 · 进度环状态取自设计稿:右上 21/21、左上 10/21、右下 9/21、左下 14/21
"""
def main():
names, T = load_themes()
# The state the design comps are drawn in, so the preview is comparable.
data = {"ring": [1.0, 10 / 21.0, 9 / 21.0, 14 / 21.0],
"batt": 0.40, "sunrise": "6:34", "hilo": "63°/52°",
"hh": "11", "mm": "22", "dow": "MON", "md": "MAY 26", "ampm": "AM",
"dist": "0.7", "steps": "1337"}
cell, gap = 230, 24
n = len(names)
W, H = n * cell + (n + 1) * gap, cell + 70
parts = ['' % (W, H, W, H),
' ' % (W, H)]
for i, name in enumerate(names):
ox, oy, sc = gap + i * (cell + gap), 24, cell / 500.0
defs, body = face(i, names, T, data)
parts.append(' '
' %s '
% (i, ox + cell / 2, oy + cell / 2, cell / 2 - 2, defs))
parts.append('%s ' % (i, ox, oy, sc, body))
parts.append('%d · %s '
% (ox + cell / 2, oy + cell + 22, i + 1, name))
parts.append(" ")
svg = "\n".join(parts)
open(OUT, "w").write(svg)
print("wrote %s (%d x %d, %d bytes)" % (OUT, W, H, len(svg)))
# Same faces again, one per theme, as a switchable page.
big = []
for i, name in enumerate(names):
defs, body = face(i, names, T, data)
big.append('%s %s '
% (i, defs, body))
btns = "".join('%d · %s '
% (i, ' class="on"' if i == 0 else "", i + 1, n)
for i, n in enumerate(names))
open(OUT_HTML, "w").write(HTML % {"faces": "".join(big), "buttons": btns,
"n": len(names)})
print("wrote %s (%d themes)" % (OUT_HTML, len(names)))
if __name__ == "__main__":
main()