Files
fenix8v3-watchface/source/Layout.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

248 lines
11 KiB
MonkeyC
Raw Permalink 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.Math;
// ============================================================================
// 布局与文字绘制
// ----------------------------------------------------------------------------
// 本文件里所有的数字都是**设计稿 500×500 画布上的坐标**,全部来自
// design/watchface-*.svg 的实测:
// - 进度环的几何参数取自 design/rebuild.py原作者从位图逆向出的常量
// - 各元素位置取自 SVG 描边路径的墨迹包围盒,不是目测的
//
// 绘制时统一乘以 s = 屏宽 / 500所以同一套数字可以覆盖
// 240pxfenix 7S到 454pxvenu 3全部 17 款设备。
//
// ⚠️ 使用前必须先由 view 的 onLayout 调用一次 init()。
// ============================================================================
module Layout {
// ------------------------------- 进度环 -------------------------------
// 刻度是**等角宽的环形扇区**,不是从圆心发射的线段 —— 所以画的时候用四点
// 多边形填充,而不是 drawLine 描边。这点很关键:用线段画会导致刻度在内圈
// 挤、外圈疏,与设计稿对不上。
const R_IN = 231.09; // 刻度带内边缘半径
const R_OUT = 249.10; // 刻度带外边缘半径
const TOP_N = 21; // 每条顶弧的刻度数
const TOP_PH = 6.500; // 第 0 根刻度的角度(自 12 点起顺时针,度)
const TOP_PITCH = 4.000;
const TOP_HW = 1.554; // 刻度角宽 3.108° 的一半
const BOT_N = 21; // 每条底弧的刻度数
const BOT_PH = 6.005; // 底弧角度自 6 点起量
const BOT_PITCH = 3.000;
const BOT_HW = 1.043;
const DOT_RAD = 240.10; // 四个锚点圆点所在半径(正好是刻度带中线)
const DOT_R = 9.00;
const DOT_ANG = [0.0, 93.15, 180.0, 266.85];
// ------------------------------- 各元素 -------------------------------
const BATT_X = 192.70; // 电池外框左边缘
const BATT_W = 26.20; // 只是外框宽度,右侧的触点另算
const BATT_H = 17.81;
const BATT_WALL = 2.20; // 外框线宽
const BATT_TW = 2.53; // 右侧触点宽
const BATT_TH = 9.20;
const BATT_VX = 232.00; // 百分比文字左边缘
const BATT_CY = 53.20;
const LEFT_X = 157.00; // 顶部两个数据位的中心 x居中对齐
const RIGHT_X = 343.70;
const ICON_CY = 89.50; // 与图标位图自身的中心对齐,别随意改
const VALUE_CY = 131.06;
const TIME_CY = 221.10;
const HOURS_R = 211.40; // 小时数字右边缘(右对齐基准)
const MINUTES_L = 276.02; // 分钟数字左边缘(左对齐基准)
const COLON_X = 237.88;
const COLON_W = 24.24;
const COLON_H = 21.30;
const COLON_Y1 = 183.68;
const COLON_Y2 = 234.57;
const BAND_Y = 286.30;
const BAND_H = 44.19;
const BAND_CY = 308.10;
const BT_CX = 36.88; // 蓝牙符号中心
const BT_CY = 306.99;
const BT_HW = 8.89;
const BT_HH = 12.92;
const DOW_CX = 178.05;
const MD_CX = 298.28;
const AMPM_R = 465.89;
const BOT_VAL_CY = 366.76;
const BOT_LAB_CY = 410.88;
const DIST_R = 234.30; // 左下格(数值+标签)的右对齐基准
const STEP_L = 267.40; // 右下格(数值+标签)的左对齐基准
// 设计稿里三档文字的**字面高度**cap height大写字母/数字的高度,
// 不含上下留白)。字号选择就是按这个来匹配的。
const CAP_BAND = 25.34; // 日期带、电量百分比
const CAP_COMP = 31.50; // 四个数据位、底部行
const CAP_TIME = 98.70; // 时间大数字
// 数值最多占多宽,超了就降一档字号。
// 顶部两格是居中对齐,这个值是它能横跨的总宽度;
// 底部两格是边对齐,各占表盘中间往外的一半。
// 有了这个,用户就算把 6 个汉字的长日期放进底部格也不会压到进度环上。
const TOP_MAX_W = 185.0;
const BOT_MAX_W = 150.0;
// ⚠️ Connect IQ 只暴露 ascent / descent / fontHeight**没有字面高度接口**。
// 而 ascent 里包含了数字和大写字母永远用不到的变音符号空间,直接拿 ascent
// 当字面高度会导致选出来的字号明显偏小(第一版就是这么错的,时间数字只有
// 设计稿的 57%)。
//
// 下面两个比例是**实测**出来的:在模拟器里渲染,然后从截图上量墨迹高度。
// fenix847mmFONT_XTINY ascent 29 → 大写实际高 22px → 0.76
// FONT_NUMBER_MILD ascent 82 → 数字实际高 58px → 0.71
// 各机型内置字体不完全相同Roboto / RobotoCondensed / Yantramanav /
// 各机型位图字体),比例会有几个百分点误差,可以接受。
const CAP_OF_TEXT = 0.76;
const CAP_OF_NUM = 0.71;
// ------------------------------- 运行时状态 ----------------------------
var scale = 1.0; // 设备像素 / 设计像素
var width = 0;
var height = 0;
var cx = 0;
var cy = 0;
var fontBand = Graphics.FONT_SMALL;
var fontComp = Graphics.FONT_MEDIUM;
var fontTime = Graphics.FONT_NUMBER_HOT;
// 文字字号按从小到大排列,这样放不下的值可以沿着这张表往下逐档缩。
var textFonts = [Graphics.FONT_XTINY, Graphics.FONT_TINY, Graphics.FONT_SMALL,
Graphics.FONT_MEDIUM, Graphics.FONT_LARGE];
function init(dc as Graphics.Dc) as Void {
width = dc.getWidth();
height = dc.getHeight();
scale = width / 500.0;
cx = width / 2;
cy = height / 2;
var nums = [Graphics.FONT_NUMBER_MILD, Graphics.FONT_NUMBER_MEDIUM,
Graphics.FONT_NUMBER_HOT, Graphics.FONT_NUMBER_THAI_HOT];
fontBand = pickFont(textFonts, CAP_BAND * scale, CAP_OF_TEXT);
fontComp = pickFont(textFonts, CAP_COMP * scale, CAP_OF_TEXT);
fontTime = pickFont(nums, CAP_TIME * scale, CAP_OF_NUM);
}
function ready() as Boolean { return width > 0; }
// 设计像素 → 设备像素。整个文件里凡是坐标都要过这一层。
function px(v as Numeric) as Number {
return Math.round(v * scale).toNumber();
}
function capHeight(font as Graphics.FontDefinition, ratio as Float) as Float {
return Graphics.getFontAscent(font) * ratio;
}
// 选**字面高度最接近**目标的那一档,而不是「不超过目标的最大档」。
// 后者在设计尺寸恰好卡在两档字体之间时会白白小一号。
function pickFont(cands as Array<Graphics.FontDefinition>, targetCap as Float,
ratio as Float) as Graphics.FontDefinition {
var best = cands[0];
var bestErr = -1.0;
for (var i = 0; i < cands.size(); i++) {
var err = capHeight(cands[i], ratio) - targetCap;
if (err < 0) { err = -err; }
if (bestErr < 0 || err < bestErr) { bestErr = err; best = cands[i]; }
}
return best;
}
// 值太长时(比如 12 个字符的自定义文字、6 个汉字的长日期)按设计稿字号会
// 冲出表盘。这里沿字号表往下找,直到能塞进槽位宽度为止。
function fitFont(dc as Graphics.Dc, str as String,
font as Graphics.FontDefinition, maxDesignW as Float)
as Graphics.FontDefinition {
var limit = px(maxDesignW);
var f = font;
var i = textFonts.size() - 1;
while (i >= 0) {
if (textFonts[i] == f) { break; }
i--;
}
if (i < 0) { return f; } // 不是文字字号表里的(比如数字字体),不动它
while (i >= 0) {
if (dc.getTextWidthInPixels(str, textFonts[i]) <= limit) {
return textFonts[i];
}
i--;
}
return textFonts[0];
}
// TEXT_JUSTIFY_VCENTER 居中的是**字体盒子**,而大写字母/数字只占据
// 「基线往上 capHeight」这一段在盒子里是偏上的。
// 所以要往下推一个差值,才能让实际墨迹的中心落在 cy 上。
function inkY(dc as Graphics.Dc, font as Graphics.FontDefinition,
cyDesign as Numeric, ratio as Float) as Number {
var fh = dc.getFontHeight(font);
var asc = Graphics.getFontAscent(font);
return px(cyDesign) + ((fh / 2.0) - asc + capHeight(font, ratio) / 2.0).toNumber();
}
function text(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric,
font as Graphics.FontDefinition, str as String,
just as Number, col as Number) as Void {
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
dc.drawText(px(x), inkY(dc, font, cyDesign, CAP_OF_TEXT), font, str,
just | Graphics.TEXT_JUSTIFY_VCENTER);
}
// 同上,但会在字符串超出槽位宽度时自动降字号。
function fittedText(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric,
font as Graphics.FontDefinition, str as String,
just as Number, col as Number, maxDesignW as Float) as Void {
text(dc, x, cyDesign, fitFont(dc, str, font, maxDesignW), str, just, col);
}
// Monkey C 没法给文字填渐变。这里的做法是:**同一串数字画 N 遍**,每遍用
// dc.setClip 把绘制限制在一条横向色带内,色带颜色取自主题的渐变停止点。
//
// 首尾两条色带故意向外延伸到屏幕边缘,保证字形上下不会有没画到的缝隙
// (字体实际墨迹高度和我们估算的 capHeight 总有几像素出入)。
function gradText(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric,
font as Graphics.FontDefinition, str as String,
just as Number, ramp as Array<Number>) as Void {
var n = ramp.size();
var y = inkY(dc, font, cyDesign, CAP_OF_NUM);
var ink = capHeight(font, CAP_OF_NUM).toNumber();
var top = px(cyDesign) - ink / 2;
for (var i = 0; i < n; i++) {
var y0 = top + (ink * i) / n;
var y1 = top + (ink * (i + 1)) / n;
if (i == 0) { y0 = 0; }
if (i == n - 1) { y1 = height; }
if (y1 <= y0) { continue; }
dc.setClip(0, y0, width, y1 - y0);
dc.setColor(ramp[i], Graphics.COLOR_TRANSPARENT);
dc.drawText(px(x), y, font, str, just | Graphics.TEXT_JUSTIFY_VCENTER);
}
dc.clearClip();
}
// 设计稿的角度自 12 点起顺时针为正;屏幕 y 轴向下为正,所以是 cy - cos。
function polarX(ang as Float, r as Float) as Number {
return Math.round(cx + Math.sin(Math.toRadians(ang)) * r).toNumber();
}
function polarY(ang as Float, r as Float) as Number {
return Math.round(cy - Math.cos(Math.toRadians(ang)) * r).toNumber();
}
function line(dc as Graphics.Dc, x1 as Numeric, y1 as Numeric,
x2 as Numeric, y2 as Numeric) as Void {
dc.drawLine(px(x1), px(y1), px(x2), px(y2));
}
// 线宽在小屏上会被 round 成 0 直接消失,这里兜一个最小值 2。
function pen(v as Numeric) as Number {
var w = px(v);
return w < 2 ? 2 : w;
}
}