Files
fenix8v3-watchface/source/Fenix8V3View.mc
ericwyuan 553c55026a Initial commit: Fenix 8 V3 data watch face
- Restore watchface-kit design (500x500, scaled per device)
- 7 themes auto-generated from tokens via gen_themes.py -> Themes.mc
- Four-segment gradient progress ring (22+21 ticks, 4 anchor dots)
- Center time, date band w/ Bluetooth icon, top battery + dual complications, bottom distance/steps
- 17 devices compiled clean (SDK 9.1.0)
- Includes browser/JS preview (preview.html) + 7-theme SVG sheet
2026-09-09 14:40:33 +08:00

388 lines
19 KiB
MonkeyC

import Toybox.Application;
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.Math;
import Toybox.ActivityMonitor;
import Toybox.Weather;
import Toybox.System;
import Toybox.Time;
import Toybox.Time.Gregorian;
import Toybox.WatchUi;
import Themes;
class Fenix8V3View extends WatchUi.WatchFace {
// ----------------------------------------------------------------- utils
function getSetting(name as String, def as Number) as Number {
var v = Application.Properties.getValue(name);
if (v == null) { return def; }
return v as Number;
}
function getBool(name as String, def as Boolean) as Boolean {
var v = Application.Properties.getValue(name);
if (v == null) { return def; }
return v as Boolean;
}
function currentTheme() as Dictionary {
var idx = getSetting("Theme", 1); // 1..7
var i = idx - 1;
if (i < 0) { i = 0; }
if (i > 6) { i = 0; }
var id = Themes.ORDER[i];
return Themes.THEMES[id] as Dictionary;
}
function lerpColor(c1 as Number, c2 as Number, f as Float) as Number {
var r1 = (c1 & 0xFF0000) >> 16;
var g1 = (c1 & 0x00FF00) >> 8;
var b1 = (c1 & 0x0000FF);
var r2 = (c2 & 0xFF0000) >> 16;
var g2 = (c2 & 0x00FF00) >> 8;
var b2 = (c2 & 0x0000FF);
var r = (r1 + ((r2 - r1) * f).toNumber()).toNumber();
var g = (g1 + ((g2 - g1) * f).toNumber()).toNumber();
var b = (b1 + ((b2 - b1) * f).toNumber()).toNumber();
return (r << 16) | (g << 8) | b;
}
// interpolate a gradient stop array [[angle,color],...] (angles ascending)
function lerpStops(stops as Array<Array<Number>>, t as Number) as Number {
var n = stops.size();
if (n == 0) { return 0x888888; }
var first = stops[0];
var last = stops[n - 1];
if (t <= first[0]) { return first[1]; }
if (t >= last[0]) { return last[1]; }
for (var i = 0; i < n - 1; i++) {
var a = stops[i];
var b = stops[i + 1];
if (t >= a[0] && t <= b[0]) {
var f = ((t - a[0]) / (b[0] - a[0])).toFloat();
return lerpColor(a[1], b[1], f);
}
}
return last[1];
}
// progress 0..1 for a metric index
function metricProgress(idx as Number, info as ActivityMonitor.Info) as Float {
if (info == null) { return 0.0f; }
var p = 0.0f;
if (idx == 0) { // Steps
if (info.stepGoal != null && info.stepGoal > 0 && info.steps != null) {
p = info.steps.toFloat() / info.stepGoal.toFloat();
}
} else if (idx == 1) { // Calories
if (info.calories != null) { p = info.calories.toFloat() / 2000.0f; }
} else if (idx == 2) { // Distance (cm -> 10 km cap)
if (info.distance != null) { p = info.distance.toFloat() / 1000000.0f; }
} else if (idx == 3) { // Floors
if (info.floorsClimbed != null) {
var goal = (info.floorsClimbedGoal != null && info.floorsClimbedGoal > 0)
? info.floorsClimbedGoal.toFloat() : 10.0f;
p = info.floorsClimbed.toFloat() / goal;
}
} else if (idx == 4) { // Move Bar
if (info.moveBarLevel != null) { p = info.moveBarLevel.toFloat(); }
}
if (p > 1.0f) { p = 1.0f; }
if (p < 0.0f) { p = 0.0f; }
return p;
}
// value string for a top complication type
function complicationValue(typeIdx as Number, info as ActivityMonitor.Info,
weather as Weather.CurrentConditions, stats as System.Stats) as String {
if (typeIdx == 0) { // Weather temp
if (weather != null && weather.temperature != null) {
return weather.temperature.format("%d") + "\u00B0";
}
return "--";
} else if (typeIdx == 1) { // Calories
return info != null && info.calories != null ? info.calories.format("%d") : "0";
} else if (typeIdx == 2) { // Steps
return info != null && info.steps != null ? info.steps.format("%d") : "0";
} else if (typeIdx == 3) { // Distance km
if (info != null && info.distance != null) {
return (info.distance.toFloat() / 100000.0).format("%.1f");
}
return "0.0";
} else if (typeIdx == 4) { // Floors
return info != null && info.floorsClimbed != null ? info.floorsClimbed.format("%d") : "0";
} else { // Battery
var pct = 0;
if (stats != null && stats.battery != null) { pct = (stats.battery * 100.0).toNumber(); }
return pct.format("%d") + "%";
}
}
// ----------------------------------------------------------------- ring
function drawTick(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number,
w as Number, ang as Number, col as Number, mirror as Boolean) as Void {
var eff = mirror ? -ang : ang;
var rad = Math.toRadians(eff);
var sin = Math.sin(rad);
var cos = Math.cos(rad);
var ix = (cx + sin * rIn).toNumber();
var iy = (cy - cos * rIn).toNumber();
var ox = (cx + sin * rOut).toNumber();
var oy = (cy - cos * rOut).toNumber();
dc.setPenWidth(w);
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
dc.drawLine(ix, iy, ox, oy);
}
function drawArcTop(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number,
w as Number, angles as Array<Number>, stops as Array<Array<Number>>,
off as Number, mirror as Boolean, lit as Number, anchorOuter as Boolean) as Void {
var n = angles.size();
for (var i = 0; i < n; i++) {
var a = angles[i];
var on = anchorOuter ? (i >= n - lit) : (i < lit);
var col = on ? lerpStops(stops, a) : off;
drawTick(dc, cx, cy, rIn, rOut, w, a, col, mirror);
}
}
function drawArcBottom(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number,
w as Number, botAngles as Array<Number>, stops as Array<Array<Number>>,
off as Number, mirror as Boolean, lit as Number) as Void {
var n = botAngles.size();
for (var i = 0; i < n; i++) {
var a = botAngles[i];
var on = (i < lit);
var ang = (180.0 - a).toNumber();
var col = on ? lerpStops(stops, a) : off;
drawTick(dc, cx, cy, rIn, rOut, w, ang, col, mirror);
}
}
function drawRing(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary,
pTR as Float, pTL as Float, pBR as Float, pBL as Float) as Void {
var rOut = (249.5 * s).toNumber();
var rIn = (231.5 * s).toNumber();
var tickOff = theme["tickOff"] as Number;
var topStops = theme["ringTop"] as Array<Array<Number>>;
var botStops = theme["ringBottom"] as Array<Array<Number>>;
var topW = (13.0 * s).toNumber();
var botW = (9.0 * s).toNumber();
var TOP = [2,6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74,78,82,86];
var BOT = [6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60,63,66];
var litTR = (pTR * 22.0).toNumber();
var litTL = (pTL * 22.0).toNumber();
var litBR = (pBR * 21.0).toNumber();
var litBL = (pBL * 21.0).toNumber();
drawArcTop(dc, cx, cy, rIn, rOut, topW, TOP, topStops, tickOff, false, litTR, true);
drawArcTop(dc, cx, cy, rIn, rOut, topW, TOP, topStops, tickOff, true, litTL, true);
drawArcBottom(dc, cx, cy, rIn, rOut, botW, BOT, botStops, tickOff, true, litBL);
drawArcBottom(dc, cx, cy, rIn, rOut, botW, BOT, botStops, tickOff, false, litBR);
// anchor dots
var dotR = (9.5 * s).toNumber();
var dotRad = (240.5 * s).toNumber();
var dotCol = theme["anchorDot"] as Number;
var dotAngles = [0.0, 93.3, 180.0, 266.7];
dc.setColor(dotCol, Graphics.COLOR_TRANSPARENT);
for (var i = 0; i < dotAngles.size(); i++) {
var rad = Math.toRadians(dotAngles[i]);
var dx = (cx + Math.sin(rad) * dotRad).toNumber();
var dy = (cy - Math.cos(rad) * dotRad).toNumber();
dc.fillCircle(dx, dy, dotR);
}
}
// ------------------------------------------------------------- complications
function drawBattery(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary,
stats as System.Stats) as Void {
var y = (53.5 * s).toNumber();
var pct = 0;
if (stats != null && stats.battery != null) { pct = (stats.battery * 100.0).toNumber(); }
var txt = pct.format("%d") + "%";
var gw = (30.0 * s).toNumber();
var gh = (16.0 * s).toNumber();
var gx = (cx - 52.0 * s).toNumber();
var gy = (y - gh / 2).toNumber();
dc.setPenWidth(2);
dc.setColor(theme["accent"] as Number, Graphics.COLOR_TRANSPARENT);
dc.drawRectangle(gx, gy, gw, gh);
dc.fillRectangle((gx + gw).toNumber(), (gy + gh * 0.28).toNumber(),
(3.0 * s).toNumber(), (gh * 0.44).toNumber());
var fillW = ((gw - 4) * (pct.toFloat() / 100.0)).toNumber();
dc.fillRectangle((gx + 2).toNumber(), (gy + 2).toNumber(), fillW, (gh - 4).toNumber());
dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT);
dc.drawText((cx + 16.0 * s).toNumber(), y, Graphics.FONT_SMALL, txt,
Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER);
}
function drawComplicationIcon(dc as Dc, x as Number, y as Number, r as Number, col as Number,
typeIdx as Number) as Void {
dc.setPenWidth(2);
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
if (typeIdx == 0) { // sun
dc.fillCircle(x, y, (r * 0.45).toNumber());
for (var i = 0; i < 8; i++) {
var rad = Math.toRadians(i * 45.0);
var x1 = (x + Math.sin(rad) * r * 0.6).toNumber();
var y1 = (y - Math.cos(rad) * r * 0.6).toNumber();
var x2 = (x + Math.sin(rad) * r).toNumber();
var y2 = (y - Math.cos(rad) * r).toNumber();
dc.drawLine(x1, y1, x2, y2);
}
} else if (typeIdx == 1) { // flame
dc.fillPolygon([[x, (y - r).toNumber()],
[(x + r * 0.8).toNumber(), (y + r * 0.6).toNumber()],
[(x - r * 0.8).toNumber(), (y + r * 0.6).toNumber()]]);
} else if (typeIdx == 2) { // steps
dc.drawLine((x - r * 0.5).toNumber(), (y - r * 0.3).toNumber(),
(x + r * 0.5).toNumber(), (y - r * 0.3).toNumber());
dc.drawLine((x - r * 0.3).toNumber(), y, (x + r * 0.6).toNumber(), y);
} else if (typeIdx == 3) { // distance dot
dc.fillCircle(x, y, (r * 0.5).toNumber());
} else if (typeIdx == 4) { // floors chevron
dc.fillPolygon([[x, (y - r).toNumber()],
[(x + r * 0.7).toNumber(), (y + r * 0.2).toNumber()],
[(x - r * 0.7).toNumber(), (y + r * 0.2).toNumber()]]);
} else { // battery
dc.drawRectangle((x - r * 0.6).toNumber(), (y - r * 0.4).toNumber(),
(r * 1.2).toNumber(), (r * 0.8).toNumber());
}
}
function drawTopComplication(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary,
typeIdx as Number, isLeft as Boolean, info as ActivityMonitor.Info,
weather as Weather.CurrentConditions, stats as System.Stats) as Void {
var sideX = isLeft ? (156.0 * s).toNumber() : (343.0 * s).toNumber();
var iconY = (90.0 * s).toNumber();
var valY = (130.5 * s).toNumber();
var valStr = complicationValue(typeIdx, info, weather, stats);
dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT);
dc.drawText(sideX, valY, Graphics.FONT_MEDIUM, valStr,
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
drawComplicationIcon(dc, sideX, iconY, (13.0 * s).toNumber(), theme["accent"] as Number, typeIdx);
}
function drawBluetooth(dc as Dc, cx as Number, cy as Number, r as Number, col as Number) as Void {
dc.setPenWidth(3);
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
dc.drawLine(cx, (cy - r).toNumber(), cx, (cy + r).toNumber());
dc.drawLine(cx, (cy - r).toNumber(), (cx + r * 0.7).toNumber(), (cy - r * 0.3).toNumber());
dc.drawLine((cx + r * 0.7).toNumber(), (cy - r * 0.3).toNumber(), cx, cy);
dc.drawLine(cx, cy, (cx + r * 0.7).toNumber(), (cy + r * 0.3).toNumber());
dc.drawLine((cx + r * 0.7).toNumber(), (cy + r * 0.3).toNumber(), cx, (cy + r).toNumber());
}
function drawDateBand(dc as Dc, w as Number, cx as Number, s as Float, theme as Dictionary,
ds as System.DeviceSettings) as Void {
var by = (286.5 * s).toNumber();
var bh = (44.0 * s).toNumber();
dc.setColor(theme["bandFill"] as Number, theme["bandFill"] as Number);
dc.fillRectangle(0, by, w, bh);
var txtCol = theme["bandText"] as Number;
var yc = (308.5 * s).toNumber();
drawBluetooth(dc, (36.5 * s).toNumber(), yc, (10.0 * s).toNumber(), txtCol);
var gi = Gregorian.info(Time.now(), Time.FORMAT_MEDIUM);
var DOW = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
var MON = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
var dow = DOW[gi.day_of_week - 1];
var md = MON[gi.month - 1] + " " + gi.day.format("%d");
dc.setColor(txtCol, Graphics.COLOR_TRANSPARENT);
dc.drawText((177.5 * s).toNumber(), yc, Graphics.FONT_SMALL, dow,
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
dc.drawText((297.5 * s).toNumber(), yc, Graphics.FONT_SMALL, md,
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
var ampm = gi.hour >= 12 ? "PM" : "AM";
dc.drawText((465.0 * s).toNumber(), yc, Graphics.FONT_SMALL, ampm,
Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER);
}
function drawTime(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary) as Void {
var gi = Gregorian.info(Time.now(), Time.FORMAT_MEDIUM);
var hh = gi.hour;
if (hh == 0) { hh = 12; } else if (hh > 12) { hh = hh - 12; }
var hStr = hh.format("%d");
var mStr = gi.min.format("%02d");
var ty = (220.0 * s).toNumber();
dc.setColor(theme["hours"] as Number, Graphics.COLOR_TRANSPARENT);
dc.drawText((211.0 * s).toNumber(), ty, Graphics.FONT_NUMBER_HOT, hStr,
Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER);
dc.setColor(theme["minutes"] as Number, Graphics.COLOR_TRANSPARENT);
dc.drawText((272.0 * s).toNumber(), ty, Graphics.FONT_NUMBER_HOT, mStr,
Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER);
// colon: two blocks centred on x=250
dc.setColor(theme["colon"] as Number, Graphics.COLOR_TRANSPARENT);
var cw = (24.0 * s).toNumber();
var ch = (20.0 * s).toNumber();
var cxc = (238.0 * s).toNumber();
dc.fillRectangle(cxc, (184.0 * s).toNumber(), cw, ch);
dc.fillRectangle(cxc, (235.0 * s).toNumber(), cw, ch);
}
function drawBottom(dc as Dc, cx as Number, s as Float, theme as Dictionary,
info as ActivityMonitor.Info) as Void {
var valY = (366.0 * s).toNumber();
var labY = (410.5 * s).toNumber();
var leftX = (234.0 * s).toNumber();
var rightX = (266.0 * s).toNumber();
var dist = info != null && info.distance != null
? (info.distance.toFloat() / 100000.0).format("%.1f") : "0.0";
var steps = info != null && info.steps != null ? info.steps.format("%d") : "0";
dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT);
dc.drawText(leftX, valY, Graphics.FONT_MEDIUM, dist,
Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER);
dc.setColor(theme["accent"] as Number, Graphics.COLOR_TRANSPARENT);
dc.drawText(leftX, labY, Graphics.FONT_SMALL, "DST",
Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER);
dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT);
dc.drawText(rightX, valY, Graphics.FONT_MEDIUM, steps,
Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER);
dc.setColor(theme["accent"] as Number, Graphics.COLOR_TRANSPARENT);
dc.drawText(rightX, labY, Graphics.FONT_SMALL, "STEP",
Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER);
}
// ----------------------------------------------------------------- main
function onUpdate(dc as Dc) as Void {
var w = dc.getWidth();
var h = dc.getHeight();
var s = w / 500.0;
var cx = w / 2;
var cy = h / 2;
var theme = currentTheme();
var info = ActivityMonitor.getInfo();
var stats = System.getSystemStats();
var ds = System.getDeviceSettings();
var weather = Weather.getCurrentConditions();
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
dc.clear();
var pTR = metricProgress(getSetting("RingTR", 0), info);
var pTL = metricProgress(getSetting("RingTL", 1), info);
var pBR = metricProgress(getSetting("RingBR", 4), info);
var pBL = metricProgress(getSetting("RingBL", 3), info);
drawRing(dc, cx, cy, s, theme, pTR, pTL, pBR, pBL);
if (getBool("ShowTop", true)) {
drawBattery(dc, cx, cy, s, theme, stats);
drawTopComplication(dc, cx, cy, s, theme, getSetting("LeftTop", 1), true, info, weather, stats);
drawTopComplication(dc, cx, cy, s, theme, getSetting("RightTop", 0), false, info, weather, stats);
}
drawTime(dc, cx, cy, s, theme);
if (getBool("ShowBand", true)) {
drawDateBand(dc, w, cx, s, theme, ds);
}
if (getBool("ShowBottom", true)) {
drawBottom(dc, cx, s, theme, info);
}
}
}