#!/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.pysource/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('' % (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()