- 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
248 lines
11 KiB
MonkeyC
248 lines
11 KiB
MonkeyC
import Toybox.Lang;
|
||
import Toybox.Graphics;
|
||
import Toybox.Math;
|
||
|
||
// ============================================================================
|
||
// 布局与文字绘制
|
||
// ----------------------------------------------------------------------------
|
||
// 本文件里所有的数字都是**设计稿 500×500 画布上的坐标**,全部来自
|
||
// design/watchface-*.svg 的实测:
|
||
// - 进度环的几何参数取自 design/rebuild.py(原作者从位图逆向出的常量)
|
||
// - 各元素位置取自 SVG 描边路径的墨迹包围盒,不是目测的
|
||
//
|
||
// 绘制时统一乘以 s = 屏宽 / 500,所以同一套数字可以覆盖
|
||
// 240px(fenix 7S)到 454px(venu 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%)。
|
||
//
|
||
// 下面两个比例是**实测**出来的:在模拟器里渲染,然后从截图上量墨迹高度。
|
||
// fenix847mm:FONT_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;
|
||
}
|
||
}
|