Files
fenix8v3-watchface/source/Icons.mc
ericwyuan 4d96ab0912 Update watch face: modular refactor + OWM/Solar/background + codegen tools
- 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
2026-09-10 07:10:55 +08:00

156 lines
8.0 KiB
MonkeyC
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Toybox.Lang;
import Toybox.Graphics;
import Toybox.WatchUi;
import Layout;
// ============================================================================
// 图标
// ----------------------------------------------------------------------------
// 顶部两个数据位上方的图标,加上电池和蓝牙符号。
//
// 设计稿里只有三个图标:日出、天气、电池。
//
// 前两个是位图描出来的矢量轮廓,最细的特征只有 23 个设计像素宽。一开始我用
// 圆、线段、多边形去「重画」它们,在 454px 上看着还行,一到 280pxEnduro 3
// 就糊成一团 —— 因为 s=0.56 时那些细节不足 1 个物理像素。
// 中间还试过做一版「低细节」图元,结果更糟:日出变成三条横杠。
//
// 现在的做法是:用 tools/gen_icons.py 把**设计稿的路径**按每种屏宽各光栅化一
// 张位图,运行时用 drawBitmap2(:tintColor) 按主题色染色。这样每台设备拿到的
// 都是为它的像素网格专门渲染的图。
//
// 其余图标(步数、卡路里、楼梯、心形…)设计稿里根本没有,所以保持图元绘制,
// 尺寸同样基于 500 设计画布。
// ============================================================================
module Icons {
// 三张位图在 onLayout 时一次性加载loadResource 开销不小,不能放 onUpdate
var mSunrise = null;
var mSunset = null;
var mWeather = null;
// 加载图标位图。资源按屏宽分目录monkey.jungle 里为每台设备指定了
// resources-icons-<屏宽>,所以这里拿到的自动就是对的尺寸。
function load() as Void {
mSunrise = WatchUi.loadResource(Rez.Drawables.IconSunrise);
mSunset = WatchUi.loadResource(Rez.Drawables.IconSunset);
mWeather = WatchUi.loadResource(Rez.Drawables.IconWeather);
}
function ready() as Boolean { return mSunrise != null; }
// ⚠️ drawBitmap2 的 :tintColor 拒绝**调色板化**的位图,运行时会抛
// "Source must not use a color palette"。所以生成的 drawables.xml 里每个
// bitmap 都带 packingFormat="png",保住 alpha 通道不被压成调色板。
function blit(dc as Graphics.Dc, bmp, cxDesign as Numeric, col as Number) as Void {
if (bmp == null) { return; }
dc.drawBitmap2(Layout.px(cxDesign) - bmp.getWidth() / 2,
Layout.px(Layout.ICON_CY) - bmp.getHeight() / 2,
bmp, {:tintColor => col});
}
// 电池:外框 + 内部电量条 + 右侧触点。
// 电量条长度跟随真实电量(设计稿画的是 40% 的状态)。
// 画法是「先整块填充,再用黑色掏空内部,最后按比例填回电量条」。
function battery(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric,
k as Float, col as Number, level as Float) as Void {
var x0 = Layout.px(x);
var y0 = Layout.px(cyDesign - Layout.BATT_H * k / 2);
var w = Layout.px(Layout.BATT_W * k);
var h = Layout.px(Layout.BATT_H * k);
var wall = Layout.px(Layout.BATT_WALL * k);
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
dc.fillRoundedRectangle(x0, y0, w, h, Layout.px(3.4 * k));
dc.fillRectangle(x0 + w, Layout.px(cyDesign - Layout.BATT_TH * k / 2),
Layout.px(Layout.BATT_TW * k), Layout.px(Layout.BATT_TH * k));
var iw = w - 2 * wall;
var ih = h - 2 * wall;
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
dc.fillRectangle(x0 + wall, y0 + wall, iw, ih);
var fill = (iw * level).toNumber();
if (fill > 0) {
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
dc.fillRectangle(x0 + wall, y0 + wall, fill, ih);
}
}
// 标准蓝牙符号,用一条折线一笔画成(六段,中间的竖线会被走两次)。
function bluetooth(dc as Graphics.Dc, col as Number) as Void {
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
dc.setPenWidth(Layout.pen(3.2));
var l = Layout.BT_CX - Layout.BT_HW;
var r = Layout.BT_CX + Layout.BT_HW;
var t = Layout.BT_CY - Layout.BT_HH;
var b = Layout.BT_CY + Layout.BT_HH;
var qt = Layout.BT_CY - Layout.BT_HH / 2;
var qb = Layout.BT_CY + Layout.BT_HH / 2;
Layout.line(dc, l, qt, r, qb);
Layout.line(dc, r, qb, Layout.BT_CX, b);
Layout.line(dc, Layout.BT_CX, b, Layout.BT_CX, t);
Layout.line(dc, Layout.BT_CX, t, r, qt);
Layout.line(dc, r, qt, l, qb);
}
// 按字段编号决定上方画哪个图标。
// 日期、周数、自定义文字这类字段没有合适的图标,就不画 —— 光有数值也读得懂,
// 硬配一个反而干扰。
function forField(dc as Graphics.Dc, id as Number, xDesign as Numeric,
col as Number, level as Float) as Void {
if (id == 712 || id == 700) {
blit(dc, mSunrise, xDesign, col);
return;
}
if (id == 717 || id == 731) {
blit(dc, mSunset, xDesign, col);
return;
}
if (id == 306 || id == 639 || id == 300 || id == 301 || id == 302
|| id == 304 || id == 305 || id == 307 || id == 308
|| id == 309 || id == 310) {
blit(dc, mWeather, xDesign, col);
return;
}
if (id == 250 || id == 251 || id == 257) {
battery(dc, xDesign - 14.4, Layout.ICON_CY, 1.0, col, level);
return;
}
var r = 13.0;
var cyd = Layout.ICON_CY;
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
dc.setPenWidth(Layout.pen(3.0));
if (id == 2 || id == 603) { // 卡路里:火焰
dc.fillPolygon([[Layout.px(xDesign), Layout.px(cyd - r)],
[Layout.px(xDesign + r * 0.72), Layout.px(cyd + r * 0.25)],
[Layout.px(xDesign + r * 0.40), Layout.px(cyd + r * 0.85)],
[Layout.px(xDesign - r * 0.40), Layout.px(cyd + r * 0.85)],
[Layout.px(xDesign - r * 0.72), Layout.px(cyd + r * 0.25)]]);
} else if (id == 1 || id == 14 || id == 707) { // 步数:两个脚印
dc.fillRoundedRectangle(Layout.px(xDesign - r * 0.85), Layout.px(cyd - r * 0.75),
Layout.px(r * 0.62), Layout.px(r * 1.15),
Layout.px(r * 0.3));
dc.fillRoundedRectangle(Layout.px(xDesign + r * 0.22), Layout.px(cyd - r * 0.35),
Layout.px(r * 0.62), Layout.px(r * 1.15),
Layout.px(r * 0.3));
} else if (id == 3 || id == 9 || id == 615) { // 距离/海拔:箭头
dc.fillPolygon([[Layout.px(xDesign), Layout.px(cyd - r)],
[Layout.px(xDesign + r * 0.75), Layout.px(cyd + r * 0.8)],
[Layout.px(xDesign), Layout.px(cyd + r * 0.35)],
[Layout.px(xDesign - r * 0.75), Layout.px(cyd + r * 0.8)]]);
} else if (id == 618 || id == 276 || id == 623) { // 心率/静息/体能:心形
dc.fillCircle(Layout.px(xDesign - r * 0.38), Layout.px(cyd - r * 0.28),
Layout.px(r * 0.44));
dc.fillCircle(Layout.px(xDesign + r * 0.38), Layout.px(cyd - r * 0.28),
Layout.px(r * 0.44));
dc.fillPolygon([[Layout.px(xDesign - r * 0.78), Layout.px(cyd - r * 0.12)],
[Layout.px(xDesign + r * 0.78), Layout.px(cyd - r * 0.12)],
[Layout.px(xDesign), Layout.px(cyd + r * 0.85)]]);
} else if (id == 4 || id == 5 || id == 6) { // 楼层:台阶
for (var i = 0; i < 3; i++) {
dc.fillRectangle(Layout.px(xDesign - r * 0.85 + r * 0.58 * i),
Layout.px(cyd + r * 0.6 - r * 0.55 * i),
Layout.px(r * 0.62), Layout.px(r * 0.4));
}
}
}
}