机型覆盖 17 → 55 款,补 LICENSE 与 CHANGELOG
机型
新增 38 款圆屏 + CIQ ≥ 4.2 的机型:fenix 7 Pro 全系、Venu 2/3S、
Approach S50/S70、Descent G2/Mk3、D2 系列、Instinct 3 AMOLED、
FR255S/265S/570/955 等。其中 34 款的屏宽已有现成图标资源。
机型列表和 monkey.jungle 改由 tools/gen_devices.py 扫描本机 SDK 生成,
筛选条件是「支持 watchFace + 圆屏 + CIQ ≥ 4.2」(Complications 要 4.2)。
55 条资源路径不再手写。
新增 218px 与 360px 两档图标。218px 上图标只有 14×14,设计稿里太阳与云
之间那道 1.7 设计像素的暗缝连一个物理像素都占不到,光栅化出来是一坨没有
结构的橙块 —— 该尺寸改用按目标像素直接摆的简化标记。
常亮模式:结论是不改
为了压所谓的 10% 点亮率,一度把进度环也砍掉了。换成平均亮度(AMOLED 功耗
的合理代理)重新量之后发现:普通模式 14.1%,常亮 2.9%,本来就只有 1/5;
砍掉环只从 3.01% 降到 2.91%。0.1 个百分点换掉一个可见特征不划算,环留着。
两种度量的差异和这个决定都写进了代码注释和 README。
其它
- LICENSE(MIT)。只覆盖代码 —— design/ 下的设计稿版权归第三方,
发布前需取得原作者授权或替换视觉,文件内已注明。
- UserProfile.getProfile() 加异常保护(这个 API 家族已经坑过三次)
- CHANGELOG.md,版本号 1.0.0 → 1.1.0
验证:55 款全部编译通过(分三批 18+18+19);.iq 上架包可正常导出。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
29
CHANGELOG.md
Normal file
@@ -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)
|
||||||
29
LICENSE
Normal file
@@ -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 或以
|
||||||
|
其它形式分发,需先取得原作者授权,或将视觉部分替换为原创设计。
|
||||||
24
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Fenix 8 V3 — Garmin 数据表盘
|
# 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"`)
|
- **SDK**:Connect IQ SDK 9.1.0(`monkeyc` 9.1.0,`minSdkVersion="4.2.0"`)
|
||||||
- **语言**:Monkey C;界面文案含英文与简体中文
|
- **语言**:Monkey C;界面文案含英文与简体中文
|
||||||
@@ -24,6 +24,7 @@ fenix8v3-watchface/
|
|||||||
├── tools/
|
├── tools/
|
||||||
│ ├── gen_themes.py # design/*.svg -> source/Themes.mc
|
│ ├── gen_themes.py # design/*.svg -> source/Themes.mc
|
||||||
│ ├── gen_fields.py # 字段表 -> FieldTable.mc + settings.xml + 两种语言文案
|
│ ├── gen_fields.py # 字段表 -> FieldTable.mc + settings.xml + 两种语言文案
|
||||||
|
│ ├── gen_devices.py # 扫描 SDK -> manifest 机型列表 + monkey.jungle
|
||||||
│ ├── gen_icons.py # design/*.svg -> 各尺寸图标位图 + 启动图标
|
│ ├── gen_icons.py # design/*.svg -> 各尺寸图标位图 + 启动图标
|
||||||
│ └── gen_preview_svg.py # source/Themes.mc -> preview-themes.svg + preview.html
|
│ └── gen_preview_svg.py # source/Themes.mc -> preview-themes.svg + preview.html
|
||||||
├── source/
|
├── source/
|
||||||
@@ -49,10 +50,14 @@ fenix8v3-watchface/
|
|||||||
重新生成全部派生文件:
|
重新生成全部派生文件:
|
||||||
|
|
||||||
```bash
|
```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
|
&& 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` 里那一张表。
|
三份渲染实现(表盘 / SVG 预览 / HTML 预览)只有 **一份颜色数据**:`Themes.mc`。预览脚本直接解析它,不会与固件漂移。字段表、设置项、两种语言的文案同样只有 `tools/gen_fields.py` 里那一张表。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -283,7 +288,16 @@ AMOLED 机型在常亮模式下必须压低点亮像素,否则既费电又有
|
|||||||
- 时间与日期压暗到 55%,其余数据位全部隐藏
|
- 时间与日期压暗到 55%,其余数据位全部隐藏
|
||||||
- 整个画面按分钟在 ±4px 内游走,避免固定像素长期点亮
|
- 整个画面按分钟在 ±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` 切换验证。
|
模拟器可用 `Settings → Display Mode` 切换验证。
|
||||||
|
|
||||||
@@ -328,7 +342,9 @@ for d in enduro3 fenix843mm fenix847mm fenix8pro47mm fenix8solar47mm fenix8solar
|
|||||||
done
|
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 设备装全了,脚本会打印找到的数量。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
56
manifest.xml
@@ -7,26 +7,64 @@
|
|||||||
minSdkVersion="4.2.0"
|
minSdkVersion="4.2.0"
|
||||||
name="@Strings.AppName"
|
name="@Strings.AppName"
|
||||||
type="watchface"
|
type="watchface"
|
||||||
version="1.0.0">
|
version="1.1.0">
|
||||||
|
|
||||||
<iq:products>
|
<iq:products>
|
||||||
|
<iq:product id="approachs50"/>
|
||||||
|
<iq:product id="approachs7042mm"/>
|
||||||
|
<iq:product id="approachs7047mm"/>
|
||||||
|
<iq:product id="d2airx10"/>
|
||||||
|
<iq:product id="d2mach1"/>
|
||||||
|
<iq:product id="d2mach2"/>
|
||||||
|
<iq:product id="descentg2"/>
|
||||||
|
<iq:product id="descentmk343mm"/>
|
||||||
|
<iq:product id="descentmk351mm"/>
|
||||||
<iq:product id="enduro3"/>
|
<iq:product id="enduro3"/>
|
||||||
|
<iq:product id="epix2"/>
|
||||||
|
<iq:product id="epix2pro42mm"/>
|
||||||
|
<iq:product id="epix2pro47mm"/>
|
||||||
|
<iq:product id="epix2pro51mm"/>
|
||||||
|
<iq:product id="fenix7"/>
|
||||||
|
<iq:product id="fenix7pro"/>
|
||||||
|
<iq:product id="fenix7pronowifi"/>
|
||||||
|
<iq:product id="fenix7s"/>
|
||||||
|
<iq:product id="fenix7spro"/>
|
||||||
|
<iq:product id="fenix7x"/>
|
||||||
|
<iq:product id="fenix7xpro"/>
|
||||||
|
<iq:product id="fenix7xpronowifi"/>
|
||||||
<iq:product id="fenix843mm"/>
|
<iq:product id="fenix843mm"/>
|
||||||
<iq:product id="fenix847mm"/>
|
<iq:product id="fenix847mm"/>
|
||||||
<iq:product id="fenix8pro47mm"/>
|
<iq:product id="fenix8pro47mm"/>
|
||||||
<iq:product id="fenix8solar47mm"/>
|
<iq:product id="fenix8solar47mm"/>
|
||||||
<iq:product id="fenix8solar51mm"/>
|
<iq:product id="fenix8solar51mm"/>
|
||||||
<iq:product id="epix2"/>
|
<iq:product id="fenixe"/>
|
||||||
<iq:product id="epix2pro47mm"/>
|
<iq:product id="fr165"/>
|
||||||
<iq:product id="epix2pro51mm"/>
|
<iq:product id="fr165m"/>
|
||||||
<iq:product id="fenix7"/>
|
|
||||||
<iq:product id="fenix7x"/>
|
|
||||||
<iq:product id="fenix7s"/>
|
|
||||||
<iq:product id="fr965"/>
|
|
||||||
<iq:product id="fr265"/>
|
|
||||||
<iq:product id="fr255"/>
|
<iq:product id="fr255"/>
|
||||||
|
<iq:product id="fr255m"/>
|
||||||
|
<iq:product id="fr255s"/>
|
||||||
|
<iq:product id="fr255sm"/>
|
||||||
|
<iq:product id="fr265"/>
|
||||||
|
<iq:product id="fr265s"/>
|
||||||
|
<iq:product id="fr57042mm"/>
|
||||||
|
<iq:product id="fr57047mm"/>
|
||||||
|
<iq:product id="fr955"/>
|
||||||
|
<iq:product id="fr965"/>
|
||||||
|
<iq:product id="fr970"/>
|
||||||
|
<iq:product id="instinct3amoled45mm"/>
|
||||||
|
<iq:product id="instinct3amoled50mm"/>
|
||||||
|
<iq:product id="instinctcrossoveramoled"/>
|
||||||
|
<iq:product id="marq2"/>
|
||||||
|
<iq:product id="marq2aviator"/>
|
||||||
|
<iq:product id="venu2"/>
|
||||||
|
<iq:product id="venu2plus"/>
|
||||||
|
<iq:product id="venu2s"/>
|
||||||
<iq:product id="venu3"/>
|
<iq:product id="venu3"/>
|
||||||
|
<iq:product id="venu3s"/>
|
||||||
|
<iq:product id="venu441mm"/>
|
||||||
|
<iq:product id="venu445mm"/>
|
||||||
<iq:product id="vivoactive5"/>
|
<iq:product id="vivoactive5"/>
|
||||||
|
<iq:product id="vivoactive6"/>
|
||||||
</iq:products>
|
</iq:products>
|
||||||
|
|
||||||
<iq:languages>
|
<iq:languages>
|
||||||
|
|||||||
@@ -2,31 +2,68 @@ project.manifest = manifest.xml
|
|||||||
|
|
||||||
base.sourcePath = source
|
base.sourcePath = source
|
||||||
|
|
||||||
# resources-chn holds the Simplified Chinese variant of every string; the
|
# resources-chn 是简体中文文案;目录名的 -chn 后缀就是语言标记。
|
||||||
# -chn suffix is what tells monkeyc which language it belongs to.
|
|
||||||
base.resourcePath = resources;resources-chn
|
base.resourcePath = resources;resources-chn
|
||||||
|
|
||||||
# Two more resource trees per device, both generated by tools/gen_icons.py:
|
# 下面每台设备再挂两棵资源树,都由 tools/gen_icons.py 生成:
|
||||||
# resources-icons-<screen width> sunrise/sunset/weather rasterised from the
|
# resources-icons-<屏宽> 按该屏幕像素网格光栅化的日出/日落/天气图标
|
||||||
# design paths for that exact pixel grid
|
# resources-launcher-<尺寸> 该机型要求尺寸的启动图标
|
||||||
# resources-launcher-<size> launcher icon at the size the device asks for
|
# 这一整段由 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
|
enduro3.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
|
||||||
epix2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
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
|
epix2pro47mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
||||||
epix2pro51mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
|
epix2pro51mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
|
||||||
fenix7.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
|
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
|
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
|
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
|
fenix843mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
||||||
fenix847mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
|
fenix847mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
|
||||||
fenix8pro47mm.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
|
fenix8solar47mm.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
|
||||||
fenix8solar51mm.resourcePath = $(base.resourcePath);resources-icons-280;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
|
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
|
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
|
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
|
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
|
vivoactive5.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56
|
||||||
|
vivoactive6.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
|
||||||
|
|
||||||
base.excludeAnnotations = release
|
base.excludeAnnotations = release
|
||||||
|
|||||||
5
resources-icons-218/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<drawables>
|
||||||
|
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||||
|
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||||
|
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||||
|
</drawables>
|
||||||
BIN
resources-icons-218/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 151 B |
BIN
resources-icons-218/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 148 B |
BIN
resources-icons-218/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 148 B |
|
Before Width: | Height: | Size: 157 B After Width: | Height: | Size: 153 B |
5
resources-icons-360/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<drawables>
|
||||||
|
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||||
|
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||||
|
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||||
|
</drawables>
|
||||||
BIN
resources-icons-360/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 579 B |
BIN
resources-icons-360/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 583 B |
BIN
resources-icons-360/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 621 B |
3
resources-launcher-38/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<drawables>
|
||||||
|
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||||
|
</drawables>
|
||||||
BIN
resources-launcher-38/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
3
resources-launcher-54/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<drawables>
|
||||||
|
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||||
|
</drawables>
|
||||||
BIN
resources-launcher-54/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
3
resources-launcher-61/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<drawables>
|
||||||
|
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||||
|
</drawables>
|
||||||
BIN
resources-launcher-61/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
@@ -281,6 +281,12 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
|||||||
// 低功耗画面。相比正常画面砍掉的东西:
|
// 低功耗画面。相比正常画面砍掉的东西:
|
||||||
// - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画)
|
// - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画)
|
||||||
// - 未点亮的刻度(只画有数据的那部分,且压暗到 45%)
|
// - 未点亮的刻度(只画有数据的那部分,且压暗到 45%)
|
||||||
|
//
|
||||||
|
// 关于点亮量:实测普通模式的平均亮度(AMOLED 功耗的合理代理)是 14.1%,
|
||||||
|
// 常亮是 2.9%,约 1/5。曾经试过连进度环也砍掉,平均亮度只从 3.01% 降到
|
||||||
|
// 2.91% —— 0.1 个百分点换掉一个可见特征,不划算,所以环留着。
|
||||||
|
// (常按「点亮像素数」算会得到 10% 左右,但那个口径把每个抗锯齿边缘像素
|
||||||
|
// 都算成「亮」,跟实际功耗对不上。)
|
||||||
// - 四个数据位、电池(全部隐藏)
|
// - 四个数据位、电池(全部隐藏)
|
||||||
// 保留的时间和日期压暗到 55%。
|
// 保留的时间和日期压暗到 55%。
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -244,8 +244,18 @@ module Fields {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 本项目已经被这个 API 家族坑过三次(Properties.getValue、
|
||||||
|
// deleteTemporalEvent、Complication.unit),一律按「声明了权限也可能抛」
|
||||||
|
// 来防。取不到就当作没有用户档案,相关字段显示 "--"。
|
||||||
function profile() as UserProfile.Profile? {
|
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;
|
return mProf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
118
tools/gen_devices.py
Normal file
@@ -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 <iq:products> 整块
|
||||||
|
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(' <iq:product id="%s"/>' % d for d, _, _, _ in devs)
|
||||||
|
block = " <iq:products>\n%s\n </iq:products>" % body
|
||||||
|
s = re.sub(r" <iq:products>.*?</iq:products>", 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()
|
||||||
@@ -41,8 +41,11 @@ ICONS = {
|
|||||||
"weather": ("weather-icon", 343.55, 89.5, False),
|
"weather": ("weather-icon", 343.55, 89.5, False),
|
||||||
}
|
}
|
||||||
|
|
||||||
# screen width -> whether the panel can blend alpha (MIP cannot)
|
# 屏宽 -> 该尺寸下的屏幕能否做 alpha 混合(MIP 屏不能)。
|
||||||
WIDTHS = {240: False, 260: False, 280: False, 390: True, 416: True, 454: True}
|
# 这张表和启动图标尺寸表都由 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
|
SS = 8 # supersampling factor
|
||||||
|
|
||||||
@@ -118,8 +121,8 @@ def render(paths, size, cx, cy, blend):
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
# Launcher icon sizes the target devices ask for.
|
# 各机型要求的启动图标尺寸(Garmin 每款表都有自己的规定值,不能用缩放糊弄)。
|
||||||
LAUNCHER = [40, 56, 60, 65, 70]
|
LAUNCHER = [38, 40, 54, 56, 60, 61, 65, 70]
|
||||||
|
|
||||||
|
|
||||||
def launcher(size):
|
def launcher(size):
|
||||||
@@ -160,6 +163,46 @@ def launcher(size):
|
|||||||
return img.resize((size, size), Image.LANCZOS)
|
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():
|
def main():
|
||||||
svg = open(SRC).read()
|
svg = open(SRC).read()
|
||||||
for size in LAUNCHER:
|
for size in LAUNCHER:
|
||||||
@@ -182,6 +225,10 @@ def main():
|
|||||||
if key not in cache:
|
if key not in cache:
|
||||||
p = subpaths(svg, pid)
|
p = subpaths(svg, pid)
|
||||||
cache[key] = flip_chevron(p) if flip else p
|
cache[key] = flip_chevron(p) if flip else p
|
||||||
|
# 天气图标在极小尺寸下用专门的简化画法,日出/日落还撑得住。
|
||||||
|
if name == "weather" and size < 16:
|
||||||
|
img = tiny_weather(size)
|
||||||
|
else:
|
||||||
img = render(cache[key], size, cx, cy, blend)
|
img = render(cache[key], size, cx, cy, blend)
|
||||||
img.save(os.path.join(outdir, "%s.png" % name))
|
img.save(os.path.join(outdir, "%s.png" % name))
|
||||||
names.append(name)
|
names.append(name)
|
||||||
|
|||||||