diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3197ce7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# 更新记录 + +## 1.1.0 + +**机型覆盖 17 → 55 款** + +新增 38 款圆屏、Connect IQ ≥ 4.2 的机型,包括 fenix 7 Pro 全系、Venu 2/3S、 +Approach S50/S70、Descent G2/Mk3、D2 系列、Instinct 3 AMOLED、FR255S/265S/570/955 等。 +机型列表与资源路径改由 `tools/gen_devices.py` 扫描本机 SDK 生成,不再手写。 + +新增两档图标尺寸(218px、360px)。218px 上设计稿的天气图标只有 14×14, +太阳与云之间的暗缝不足一个像素,会糊成一块 —— 该尺寸改用专画的简化标记。 + +**其它** + +- 补 LICENSE(MIT,仅覆盖代码;`design/` 下的设计稿版权归第三方,见文件内说明) +- `UserProfile.getProfile()` 加异常保护 +- 版本号 1.0.0 → 1.1.0 + +## 1.0.0 + +按 `watchface-kit` 设计稿 1:1 还原的多指标数据表盘。 + +- 156 项可选数据字段,编号对齐 watchface.io 目录 +- 七套主题,配色逐根刻度实测自设计稿 +- 常亮(AOD)模式,含防烧屏位移 +- 简体中文 / 英文双语,表盘文字亦本地化 +- OpenWeatherMap 接入(用户自备 API key,后台定时拉取) +- 系统 Complication 接入(日历、训练状态、完赛预测、第三方 App) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb7ae9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +MIT License + +Copyright (c) 2026 ericwyuan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--------------------------------------------------------------------------- +注意:本许可只覆盖**本仓库的代码**。 + +design/ 目录下的设计稿(watchface-*.svg、watchface-tokens.json、rebuild.py) +来自第三方 watchface-kit,版权归其原作者所有,不在本许可范围内。 +表盘的视觉呈现是对那套设计的 1:1 还原 —— 若要发布到 Connect IQ Store 或以 +其它形式分发,需先取得原作者授权,或将视觉部分替换为原创设计。 diff --git a/README.md b/README.md index 565dce8..6671182 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Fenix 8 V3 — Garmin 数据表盘 -按 `watchface-kit` 设计稿 1:1 还原的多指标数据表盘。设计画布 **500×500**,渲染时按屏宽等比缩放 `s = screenWidth / 500`,支持圆形 AMOLED(Fenix 8 / Epix 2 / Venu 3 等)与圆屏 MIP(Fenix 7 / Enduro 3 / FR255 等)共 17 款设备。 +按 `watchface-kit` 设计稿 1:1 还原的多指标数据表盘。设计画布 **500×500**,渲染时按屏宽等比缩放 `s = screenWidth / 500`,支持圆形 AMOLED 与圆屏 MIP 共 **55 款**设备(Fenix 7/8 全系含 Pro、Epix 2、Enduro 3、Venu 2/3、FR 系列、Approach S50/S70、Descent、D2、Instinct 3 AMOLED 等)。 - **SDK**:Connect IQ SDK 9.1.0(`monkeyc` 9.1.0,`minSdkVersion="4.2.0"`) - **语言**:Monkey C;界面文案含英文与简体中文 @@ -24,6 +24,7 @@ fenix8v3-watchface/ ├── tools/ │ ├── gen_themes.py # design/*.svg -> source/Themes.mc │ ├── gen_fields.py # 字段表 -> FieldTable.mc + settings.xml + 两种语言文案 +│ ├── gen_devices.py # 扫描 SDK -> manifest 机型列表 + monkey.jungle │ ├── gen_icons.py # design/*.svg -> 各尺寸图标位图 + 启动图标 │ └── gen_preview_svg.py # source/Themes.mc -> preview-themes.svg + preview.html ├── source/ @@ -49,10 +50,14 @@ fenix8v3-watchface/ 重新生成全部派生文件: ```bash -python3 tools/gen_themes.py && python3 tools/gen_fields.py \ +python3 tools/gen_devices.py \ + && python3 tools/gen_themes.py && python3 tools/gen_fields.py \ && python3 tools/gen_icons.py && python3 tools/gen_preview_svg.py ``` +`gen_devices.py` 要先跑 —— 它决定了需要哪些图标尺寸,`gen_icons.py` 的 +`WIDTHS` / `LAUNCHER` 两张表必须跟它对齐,少一档就会有机型编译不过。 + 三份渲染实现(表盘 / SVG 预览 / HTML 预览)只有 **一份颜色数据**:`Themes.mc`。预览脚本直接解析它,不会与固件漂移。字段表、设置项、两种语言的文案同样只有 `tools/gen_fields.py` 里那一张表。 --- @@ -283,7 +288,16 @@ AMOLED 机型在常亮模式下必须压低点亮像素,否则既费电又有 - 时间与日期压暗到 55%,其余数据位全部隐藏 - 整个画面按分钟在 ±4px 内游走,避免固定像素长期点亮 -只在 `DeviceSettings.requiresBurnInProtection` 为真时启用;MIP 机型(Fenix 7 / Enduro 3 等)不受影响,照常全量绘制。 +只在 `DeviceSettings.requiresBurnInProtection` 为真时启用;MIP 机型不受影响,照常全量绘制。 + +实测(fenix847mm,取表盘内区): + +| | 点亮像素数 | 平均亮度(功耗代理) | +|---|---|---| +| 普通模式 | 27.6% | 14.1% | +| 常亮模式 | 10.4% | **2.9%** | + +两个口径差别很大:「点亮像素数」把每个抗锯齿边缘像素都算成亮的,和实际功耗对不上。曾试过连进度环也砍掉,平均亮度只从 3.01% 降到 2.91% —— 0.1 个百分点换掉一个可见特征不划算,所以环保留。 模拟器可用 `Settings → Display Mode` 切换验证。 @@ -328,7 +342,9 @@ for d in enduro3 fenix843mm fenix847mm fenix8pro47mm fenix8solar47mm fenix8solar done ``` -屏幕分辨率跨度较大(fenix7s 240×240 至 fenix847mm / venu3 454×454),全部按 `s = width/500` 缩放。 +屏幕分辨率跨度较大(FR255S 218×218 至 454×454 共 8 档),全部按 `s = width/500` 缩放。 + +机型列表和 `monkey.jungle` 由 `tools/gen_devices.py` 扫描本机已安装的 SDK 设备生成:筛选条件是「支持 watchFace + 圆屏 + CIQ ≥ 4.2」(Complications 需要 4.2)。**注意结果取决于本机装了哪些设备**,跑之前先确认 SDK 设备装全了,脚本会打印找到的数量。 --- diff --git a/manifest.xml b/manifest.xml index 800dd4c..deda23b 100644 --- a/manifest.xml +++ b/manifest.xml @@ -7,26 +7,64 @@ minSdkVersion="4.2.0" name="@Strings.AppName" type="watchface" - version="1.0.0"> + version="1.1.0"> + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/monkey.jungle b/monkey.jungle index 500909f..3870674 100644 --- a/monkey.jungle +++ b/monkey.jungle @@ -2,31 +2,68 @@ project.manifest = manifest.xml base.sourcePath = source -# resources-chn holds the Simplified Chinese variant of every string; the -# -chn suffix is what tells monkeyc which language it belongs to. +# resources-chn 是简体中文文案;目录名的 -chn 后缀就是语言标记。 base.resourcePath = resources;resources-chn -# Two more resource trees per device, both generated by tools/gen_icons.py: -# resources-icons- sunrise/sunset/weather rasterised from the -# design paths for that exact pixel grid -# resources-launcher- launcher icon at the size the device asks for +# 下面每台设备再挂两棵资源树,都由 tools/gen_icons.py 生成: +# resources-icons-<屏宽> 按该屏幕像素网格光栅化的日出/日落/天气图标 +# resources-launcher-<尺寸> 该机型要求尺寸的启动图标 +# 这一整段由 tools/gen_devices.py 生成,不要手改。 +approachs50.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56 +approachs7042mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60 +approachs7047mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-70 +d2airx10.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70 +d2mach1.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60 +d2mach2.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65 +descentg2.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60 +descentmk343mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60 +descentmk351mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60 enduro3.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40 epix2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60 +epix2pro42mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60 epix2pro47mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60 epix2pro51mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60 fenix7.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40 +fenix7pro.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40 +fenix7pronowifi.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40 fenix7s.resourcePath = $(base.resourcePath);resources-icons-240;resources-launcher-40 +fenix7spro.resourcePath = $(base.resourcePath);resources-icons-240;resources-launcher-40 fenix7x.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40 +fenix7xpro.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40 +fenix7xpronowifi.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40 fenix843mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60 fenix847mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65 fenix8pro47mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65 fenix8solar47mm.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40 fenix8solar51mm.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40 +fenixe.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60 +fr165.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54 +fr165m.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54 fr255.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40 +fr255m.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40 +fr255s.resourcePath = $(base.resourcePath);resources-icons-218;resources-launcher-40 +fr255sm.resourcePath = $(base.resourcePath);resources-icons-218;resources-launcher-40 fr265.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60 +fr265s.resourcePath = $(base.resourcePath);resources-icons-360;resources-launcher-60 +fr57042mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54 +fr57047mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65 +fr955.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40 fr965.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65 +fr970.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65 +instinct3amoled45mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60 +instinct3amoled50mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60 +instinctcrossoveramoled.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-38 +marq2.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60 +marq2aviator.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60 +venu2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70 +venu2plus.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70 +venu2s.resourcePath = $(base.resourcePath);resources-icons-360;resources-launcher-61 venu3.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-70 +venu3s.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-70 +venu441mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54 +venu445mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65 vivoactive5.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56 +vivoactive6.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54 base.excludeAnnotations = release diff --git a/resources-icons-218/drawables/drawables.xml b/resources-icons-218/drawables/drawables.xml new file mode 100644 index 0000000..b1703a8 --- /dev/null +++ b/resources-icons-218/drawables/drawables.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/resources-icons-218/drawables/sunrise.png b/resources-icons-218/drawables/sunrise.png new file mode 100644 index 0000000..e6f1ecf Binary files /dev/null and b/resources-icons-218/drawables/sunrise.png differ diff --git a/resources-icons-218/drawables/sunset.png b/resources-icons-218/drawables/sunset.png new file mode 100644 index 0000000..5a8c238 Binary files /dev/null and b/resources-icons-218/drawables/sunset.png differ diff --git a/resources-icons-218/drawables/weather.png b/resources-icons-218/drawables/weather.png new file mode 100644 index 0000000..3ebfc13 Binary files /dev/null and b/resources-icons-218/drawables/weather.png differ diff --git a/resources-icons-240/drawables/weather.png b/resources-icons-240/drawables/weather.png index dfb6b84..5c1fac7 100644 Binary files a/resources-icons-240/drawables/weather.png and b/resources-icons-240/drawables/weather.png differ diff --git a/resources-icons-360/drawables/drawables.xml b/resources-icons-360/drawables/drawables.xml new file mode 100644 index 0000000..b1703a8 --- /dev/null +++ b/resources-icons-360/drawables/drawables.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/resources-icons-360/drawables/sunrise.png b/resources-icons-360/drawables/sunrise.png new file mode 100644 index 0000000..7fe584c Binary files /dev/null and b/resources-icons-360/drawables/sunrise.png differ diff --git a/resources-icons-360/drawables/sunset.png b/resources-icons-360/drawables/sunset.png new file mode 100644 index 0000000..abdec91 Binary files /dev/null and b/resources-icons-360/drawables/sunset.png differ diff --git a/resources-icons-360/drawables/weather.png b/resources-icons-360/drawables/weather.png new file mode 100644 index 0000000..14904ce Binary files /dev/null and b/resources-icons-360/drawables/weather.png differ diff --git a/resources-launcher-38/drawables/drawables.xml b/resources-launcher-38/drawables/drawables.xml new file mode 100644 index 0000000..579343d --- /dev/null +++ b/resources-launcher-38/drawables/drawables.xml @@ -0,0 +1,3 @@ + + + diff --git a/resources-launcher-38/drawables/launcher_icon.png b/resources-launcher-38/drawables/launcher_icon.png new file mode 100644 index 0000000..2afa717 Binary files /dev/null and b/resources-launcher-38/drawables/launcher_icon.png differ diff --git a/resources-launcher-54/drawables/drawables.xml b/resources-launcher-54/drawables/drawables.xml new file mode 100644 index 0000000..579343d --- /dev/null +++ b/resources-launcher-54/drawables/drawables.xml @@ -0,0 +1,3 @@ + + + diff --git a/resources-launcher-54/drawables/launcher_icon.png b/resources-launcher-54/drawables/launcher_icon.png new file mode 100644 index 0000000..c1e371b Binary files /dev/null and b/resources-launcher-54/drawables/launcher_icon.png differ diff --git a/resources-launcher-61/drawables/drawables.xml b/resources-launcher-61/drawables/drawables.xml new file mode 100644 index 0000000..579343d --- /dev/null +++ b/resources-launcher-61/drawables/drawables.xml @@ -0,0 +1,3 @@ + + + diff --git a/resources-launcher-61/drawables/launcher_icon.png b/resources-launcher-61/drawables/launcher_icon.png new file mode 100644 index 0000000..03602b2 Binary files /dev/null and b/resources-launcher-61/drawables/launcher_icon.png differ diff --git a/source/Fenix8V3View.mc b/source/Fenix8V3View.mc index 4350119..eb041f2 100644 --- a/source/Fenix8V3View.mc +++ b/source/Fenix8V3View.mc @@ -281,6 +281,12 @@ class Fenix8V3View extends WatchUi.WatchFace { // 低功耗画面。相比正常画面砍掉的东西: // - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画) // - 未点亮的刻度(只画有数据的那部分,且压暗到 45%) + // + // 关于点亮量:实测普通模式的平均亮度(AMOLED 功耗的合理代理)是 14.1%, + // 常亮是 2.9%,约 1/5。曾经试过连进度环也砍掉,平均亮度只从 3.01% 降到 + // 2.91% —— 0.1 个百分点换掉一个可见特征,不划算,所以环留着。 + // (常按「点亮像素数」算会得到 10% 左右,但那个口径把每个抗锯齿边缘像素 + // 都算成「亮」,跟实际功耗对不上。) // - 四个数据位、电池(全部隐藏) // 保留的时间和日期压暗到 55%。 // diff --git a/source/Fields.mc b/source/Fields.mc index b7a4079..62235ac 100644 --- a/source/Fields.mc +++ b/source/Fields.mc @@ -244,8 +244,18 @@ module Fields { return null; } + // 本项目已经被这个 API 家族坑过三次(Properties.getValue、 + // deleteTemporalEvent、Complication.unit),一律按「声明了权限也可能抛」 + // 来防。取不到就当作没有用户档案,相关字段显示 "--"。 function profile() as UserProfile.Profile? { - if (!mGotProf) { mProf = UserProfile.getProfile(); mGotProf = true; } + if (!mGotProf) { + try { + mProf = UserProfile.getProfile(); + } catch (e) { + mProf = null; + } + mGotProf = true; + } return mProf; } diff --git a/tools/gen_devices.py b/tools/gen_devices.py new file mode 100644 index 0000000..9d3d749 --- /dev/null +++ b/tools/gen_devices.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""扫描本机安装的 Connect IQ 设备,重写 manifest 的机型列表和 monkey.jungle。 + +筛选条件(三条都要满足): + 1. 支持 watchFace 类型 + 2. Connect IQ >= 4.2 —— Toybox.Complications 是这个版本引入的, + 本表盘的日历/训练状态/第三方 App 字段依赖它 + 3. 圆屏(deviceFamily 以 round- 开头) + + python3 tools/gen_devices.py + +改写两处: + manifest.xml 整块 + monkey.jungle 每台设备的 resourcePath(图标按屏宽、启动图标按各自要求的尺寸) + +⚠️ 结果取决于本机 SDK 里**装了哪些设备**。如果某台机器上少装了几款, +跑一遍会把它们从 manifest 里删掉。所以脚本会打印找到的数量, +提交前确认一下数字对不对(当前应为 55 款)。 + +图标资源的尺寸集合由本脚本算出后交给 tools/gen_icons.py 使用, +两者的 WIDTHS / LAUNCHER 必须一致,所以改完这里要跟着跑一遍 gen_icons.py。 +""" +import json +import os +import re +from collections import defaultdict + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +DEV = os.path.expanduser( + "~/Library/Application Support/Garmin/ConnectIQ/Devices") + + +def eligible(): + """返回 [(设备id, 屏宽, 启动图标尺寸, 是否支持alpha), ...],按 id 排序。""" + out = [] + if not os.path.isdir(DEV): + raise SystemExit("找不到设备目录:%s" % DEV) + for d in sorted(os.listdir(DEV)): + p = os.path.join(DEV, d, "compiler.json") + if not os.path.exists(p): + continue + try: + c = json.load(open(p)) + except Exception: + continue + types = {t["type"] for t in c.get("appTypes", []) if isinstance(t, dict)} + if "watchFace" not in types: + continue + if not str(c.get("deviceFamily", "")).startswith("round"): + continue + vers = [str(pn.get("connectIQVersion", "0")) + for pn in c.get("partNumbers", [])] + if not any(_ciq_ok(v) for v in vers): + continue + li = c.get("launcherIcon", {}) + out.append((d, c["resolution"]["width"], li.get("width"), + bool(c.get("alphaBlendingSupport")))) + return out + + +def _ciq_ok(v): + try: + a = [int(x) for x in v.split(".")[:2]] + return a[0] > 4 or (a[0] == 4 and a[1] >= 2) + except Exception: + return False + + +def write_manifest(devs): + p = os.path.join(ROOT, "manifest.xml") + s = open(p).read() + body = "\n".join(' ' % d for d, _, _, _ in devs) + block = " \n%s\n " % body + s = re.sub(r" .*?", block, s, flags=re.S) + open(p, "w").write(s) + print("manifest.xml: 写入 %d 款机型" % len(devs)) + + +def write_jungle(devs): + L = ["project.manifest = manifest.xml", "", + "base.sourcePath = source", "", + "# resources-chn 是简体中文文案;目录名的 -chn 后缀就是语言标记。", + "base.resourcePath = resources;resources-chn", "", + "# 下面每台设备再挂两棵资源树,都由 tools/gen_icons.py 生成:", + "# resources-icons-<屏宽> 按该屏幕像素网格光栅化的日出/日落/天气图标", + "# resources-launcher-<尺寸> 该机型要求尺寸的启动图标", + "# 这一整段由 tools/gen_devices.py 生成,不要手改。", + ""] + for d, w, li, _ in devs: + L.append("%s.resourcePath = $(base.resourcePath)" + ";resources-icons-%d;resources-launcher-%d" % (d, w, li)) + L += ["", "base.excludeAnnotations = release"] + open(os.path.join(ROOT, "monkey.jungle"), "w").write("\n".join(L) + "\n") + print("monkey.jungle: 写入 %d 条资源路径" % len(devs)) + + +def main(): + devs = eligible() + if not devs: + raise SystemExit("一款符合条件的机型都没找到,请检查 SDK 设备是否已安装") + widths = defaultdict(set) + for _, w, _, alpha in devs: + widths[w].add(alpha) + print("符合条件的机型:%d 款" % len(devs)) + print("需要的图标屏宽:%s" % sorted(widths)) + print("需要的启动图标尺寸:%s" % sorted({li for _, _, li, _ in devs})) + for w in sorted(widths): + if len(widths[w]) > 1: + print(" ⚠️ %dpx 同时存在支持/不支持 alpha 的机型," + "图标按硬边生成" % w) + write_manifest(devs) + write_jungle(devs) + print("\n接着跑:python3 tools/gen_icons.py") + + +if __name__ == "__main__": + main() diff --git a/tools/gen_icons.py b/tools/gen_icons.py index 97b8fd0..9e343eb 100644 --- a/tools/gen_icons.py +++ b/tools/gen_icons.py @@ -41,8 +41,11 @@ ICONS = { "weather": ("weather-icon", 343.55, 89.5, False), } -# screen width -> whether the panel can blend alpha (MIP cannot) -WIDTHS = {240: False, 260: False, 280: False, 390: True, 416: True, 454: True} +# 屏宽 -> 该尺寸下的屏幕能否做 alpha 混合(MIP 屏不能)。 +# 这张表和启动图标尺寸表都由 tools/gen_devices.py 扫描出的机型集合决定, +# 改完设备列表要跟着更新这里 —— 少一个尺寸就会有机型编译不过。 +WIDTHS = {218: False, 240: False, 260: False, 280: False, + 360: True, 390: True, 416: True, 454: True} SS = 8 # supersampling factor @@ -118,8 +121,8 @@ def render(paths, size, cx, cy, blend): return img -# Launcher icon sizes the target devices ask for. -LAUNCHER = [40, 56, 60, 65, 70] +# 各机型要求的启动图标尺寸(Garmin 每款表都有自己的规定值,不能用缩放糊弄)。 +LAUNCHER = [38, 40, 54, 56, 60, 61, 65, 70] def launcher(size): @@ -160,6 +163,46 @@ def launcher(size): return img.resize((size, size), Image.LANCZOS) +def tiny_weather(size): + """给 16px 以下的屏幕专画的天气标记。 + + 设计稿的天气图标是「太阳被云挡住一半」,靠一道暗缝把两者分开。那道缝在 + 500 画布上只有 1.7px,到 218px 屏(图标 14×14)连一个物理像素都占不到, + 光栅化出来就是一坨没有结构的橙块。 + + 所以这个尺寸不再走设计稿路径,改用按目标像素直接摆的简化标记: + 右上一个太阳圆盘,左下一朵云,中间强制留 1 个像素的空隙。 + 形不如原图,但至少还认得出是「天气」。 + """ + from PIL import ImageDraw + ss = 8 + n = size * ss + img = Image.new("L", (n, n), 0) + d = ImageDraw.Draw(img) + u = n / 14.0 # 以 14 格为基准摆放,任何尺寸都等比 + + def disc(cx, cy, r, fill): + d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=fill) + + # 太阳 + 四道光芒(光芒画成小方块,比线段在低分辨率下更容易留下来) + disc(9.2 * u, 4.6 * u, 2.7 * u, 255) + for dx, dy in ((0, -4.4), (4.3, 0), (-3.2, -3.2), (3.2, -3.2)): + d.rectangle([9.2 * u + dx * u - 0.55 * u, 4.6 * u + dy * u - 0.55 * u, + 9.2 * u + dx * u + 0.55 * u, 4.6 * u + dy * u + 0.55 * u], + fill=255) + # 先用黑色把云的轮廓“撑大”一圈,留出与太阳之间的缝,再画回本体 + for grow, fill in ((1.0 * u, 0), (0.0, 255)): + disc(5.0 * u, 8.6 * u, 3.3 * u + grow, fill) + disc(8.6 * u, 9.6 * u, 2.4 * u + grow, fill) + d.rectangle([1.7 * u - grow, 8.8 * u - grow, 11.0 * u + grow, 12.2 * u], + fill=fill) + small = img.resize((size, size), Image.LANCZOS) + small = small.point(lambda v: 255 if v >= 110 else 0) + out = Image.new("RGB", (size, size), (255, 255, 255)).convert("RGBA") + out.putalpha(small) + return out + + def main(): svg = open(SRC).read() for size in LAUNCHER: @@ -182,7 +225,11 @@ def main(): if key not in cache: p = subpaths(svg, pid) cache[key] = flip_chevron(p) if flip else p - img = render(cache[key], size, cx, cy, blend) + # 天气图标在极小尺寸下用专门的简化画法,日出/日落还撑得住。 + if name == "weather" and size < 16: + img = tiny_weather(size) + else: + img = render(cache[key], size, cx, cy, blend) img.save(os.path.join(outdir, "%s.png" % name)) names.append(name) with open(os.path.join(outdir, "drawables.xml"), "w") as f: