From 553c55026af7becba72dee4d18710ab7b8032d6f Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Wed, 9 Sep 2026 14:40:33 +0800 Subject: [PATCH] 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 --- .gitignore | 9 + README.md | 168 +++++++++++ gen_preview_svg.py | 172 ++++++++++++ manifest.xml | 38 +++ monkey.jungle | 9 + preview-themes.svg | 24 ++ preview.html | 322 +++++++++++++++++++++ resources/drawables/drawables.xml | 3 + resources/drawables/launcher_icon.png | Bin 0 -> 498 bytes resources/settings/properties.xml | 12 + resources/settings/settings.xml | 87 ++++++ resources/strings/strings.xml | 35 +++ source/Fenix8V3App.mc | 28 ++ source/Fenix8V3View.mc | 387 ++++++++++++++++++++++++++ source/Themes.mc | 110 ++++++++ 15 files changed, 1404 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 gen_preview_svg.py create mode 100644 manifest.xml create mode 100644 monkey.jungle create mode 100644 preview-themes.svg create mode 100644 preview.html create mode 100644 resources/drawables/drawables.xml create mode 100644 resources/drawables/launcher_icon.png create mode 100644 resources/settings/properties.xml create mode 100644 resources/settings/settings.xml create mode 100644 resources/strings/strings.xml create mode 100644 source/Fenix8V3App.mc create mode 100644 source/Fenix8V3View.mc create mode 100644 source/Themes.mc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58a277f --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Connect IQ build artifacts (regenerable via `monkeyc`) +/bin/ + +# Developer signing key — PRIVATE, never commit +developer_key.der + +# OS / editor noise +.DS_Store +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..21e5977 --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# Fenix 8 V3 — Garmin 数据表盘 + +一款基于 Connect IQ 设计稿(`watchface-kit`)1:1 还原的多指标数据表盘。设计画布为 **500×500** 正方形,渲染时按屏幕宽度等比缩放,支持圆形 AMOLED(Fenix 8 / Enduro 3 / Epix 2)与圆屏 MIP(Fenix 7 / FR965 等)设备。 + +- **SDK**:Connect IQ SDK 9.1.0(`monkeyc` 9.1.0,API level `minSdkVersion="4.2.0"`) +- **语言**:Monkey C +- **类型**:`watchface` +- **应用 ID**:`f8a3c1b2c3d44e5f8a9b0c1d2e3f4a5b` + +--- + +## 1. 布局还原(与设计稿基准) + +所有坐标以 500×500 设计画布为基准,渲染时统一乘以缩放系数 `s = screenWidth / 500.0`(Enduro 3 屏宽 416px → `s≈0.832`)。下表中 `y` 为设计稿原始像素值。 + +| 区域 | 设计坐标(500 基准) | 内容 | 说明 | +|------|----------------------|------|------| +| 进度环 | 外半径 `rOut=249.5`,内半径 `rIn=231.5` | 四段进度弧 + 4 个锚点圆点 | 见 §2 | +| 电量 | `y=53.5`,电池框居中偏左 | 电池图标 + 百分比 | `System.getSystemStats().battery` | +| 上方双复杂功能 | 图标 `y=90`,数值 `y=130.5`;左侧 `x=156`,右侧 `x=343` | 天气温度 / 卡路里 / 步数 / 距离 / 楼层 / 电量 | 见 §4 | +| 中央时间 | `y=220`;小时 `x=211`(右对齐),分钟 `x=272`(左对齐) | `HH : MM`(12 小时制) | 冒号用两块矩形块,位于 `x=238` | +| 日期带 | 矩形 `y=286.5`,高 `44` | 蓝牙图标 + 星期 + 月日 + AM/PM | 蓝牙图标 `x=36.5`,星期 `x=177.5`,月日 `x=297.5` | +| 底部指标 | 数值 `y=366`,标签 `y=410.5` | 左侧距离(`DST`),右侧步数(`STEP`) | 距离单位 km | + +进度环几何(取自设计稿 `rebuild.py`): + +- **顶弧**:22 根刻度,角度 `TOP = [2,6,…,86]`(步距 4),线宽 `13`,渐变停止点 9 个(`8→86`)。 +- **底弧**:21 根刻度,角度 `BOT = [6,9,…,66]`(步距 3),线宽 `9`,渐变停止点 7 个(`7→47`)。 +- **锚点圆点**:位于角度 `[0.0, 93.3, 180.0, 266.7]`,圆心半径 `240.5`,圆点半径 `9.5`。 +- 刻度“点亮”数量按对应指标进度换算:`litTR/TL = progress × 22`,`litBR/BL = progress × 21`。 + +时间字体为 `FONT_NUMBER_HOT`(大号数字),其余为 `FONT_MEDIUM` / `FONT_SMALL`。 + +--- + +## 2. 七套主题(Themes) + +颜色数据由 `watchface-tokens.json` 经 `gen_themes.py` 生成到 `source/Themes.mc`(**请勿手改**,改 tokens 后重新生成)。每套主题包含:顶/底弧渐变停止点、未点亮刻度色 `tickOff`、锚点色 `anchorDot`、日期带填充 `bandFill` / 文字 `bandText`、强调色 `accent`、主文字 `textPrimary`、冒号色 `colon`、小时 `hours`、分钟 `minutes`。 + +| # | 主题 | 主色调 | 分钟数字 | 日期带 | +|---|------|--------|----------|--------| +| 1 | Ember | 橙红渐变(黄→红) | `0xFCA901` | 橙底黑字 | +| 2 | Aurora | 青绿渐变(黄绿→青) | `0xAAF701` | 深灰底白字 | +| 3 | Brass | 金铜渐变 | `0xECCC93` | 深棕底白字 | +| 4 | Voltage | 蓝→黄渐变 | `0x52A8FD` | 蓝底黑字 | +| 5 | Reef | 橙→青渐变 | `0xFC7801` | 青底黑字 | +| 6 | Acid | 黄绿(近单色) | `0xC4F900` | 深灰底黑字 | +| 7 | Kelp | 橙→绿渐变 | `0xF4880A` | 深灰底黑字 | + +--- + +## 3. 可配置设置(Garmin Connect / 表盘设置) + +通过 `resources/settings/` 暴露,键名与默认值见 `properties.xml`: + +| 设置 | 键 | 可选值 | 默认 | +|------|----|--------|------| +| 主题 | `Theme` | 1–7(Ember…Kelp) | 1 | +| 顶右弧指标 | `RingTR` | 0 步数 / 1 卡路里 / 2 距离 / 3 楼层 / 4 动动条 | 0 | +| 顶左弧指标 | `RingTL` | 同上 | 1 | +| 底右弧指标 | `RingBR` | 同上 | 4 | +| 底左弧指标 | `RingBL` | 同上 | 3 | +| 左上复杂功能 | `LeftTop` | 0 天气 / 1 卡路里 / 2 步数 / 3 距离 / 4 楼层 / 5 电量 | 1 | +| 右上复杂功能 | `RightTop` | 同上 | 0 | +| 显示顶行 | `ShowTop` | 布尔 | true | +| 显示日期带 | `ShowBand` | 布尔 | true | +| 显示底行 | `ShowBottom` | 布尔 | true | + +> 指标默认值对应设计稿:**顶右=步数、顶左=卡路里、底右=动动条、底左=楼层**;左上=卡路里、右上=天气。 + +### 指标进度换算规则(`metricProgress`) + +| idx | 指标 | 进度分母 | +|-----|------|----------| +| 0 | 步数 Steps | `stepGoal`(目标步数) | +| 1 | 卡路里 Calories | 固定 2000 kcal | +| 2 | 距离 Distance | 10 km(1,000,000 cm) | +| 3 | 楼层 Floors | `floorsClimbedGoal`(默认 10) | +| 4 | 动动条 Move Bar | `moveBarLevel`(归一化 0–1) | + +进度超出 1 会被钳制,弧长即“点亮”刻度数。 + +--- + +## 4. 数据源与已知偏差 + +| 元素 | 数据来源 | 备注 | +|------|----------|------| +| 步数 / 卡路里 / 距离 / 楼层 / 动动条 | `ActivityMonitor.getInfo()` | SDK 9.1.0 字段:`steps/stepGoal/calories/distance/floorsClimbed/floorsClimbedGoal/moveBarLevel` | +| 天气温度 | `Weather.getCurrentConditions().temperature` | 需手表已配对天气 | +| 电量 | `System.getSystemStats().battery` | 0–1 浮点,×100 显示 | +| 日期 / 时间 | `Time.Gregorian` | 12 小时制,带 AM/PM | +| 蓝牙图标 | `System.getDeviceSettings().phoneConnected`(仅取状态) | 见下方偏差 | + +### ⚠️ 与设计稿的关键偏差 + +1. **“日出(sunrise)”元素 → 实时天气温度。** 设计稿顶部复杂功能中有一个日出图标及时间,但 `Weather.CurrentConditions` 在 SDK 9.1.0 **不暴露 sunrise 字段**。因此该位置改为显示**实时天气温度**(`NN°`),图标保留太阳造型。 +2. **进度环为实时数据**,非设计稿中的静态填充演示值;四段弧指标可在设置中自由分配。 +3. **蓝牙图标**目前按日期带文字色常显(未根据 `phoneConnected` 切换亮/暗),如需“未连接变暗”可后续在 `drawDateBand` 中据 `ds.phoneConnected` 调色。 + +--- + +## 5. 编译 / 构建 + +需安装 Connect IQ SDK(本文基于 9.1.0)并把 `monkeyc` 加入 PATH,且准备开发者密钥 `developer_key.der`(已在仓库内)。 + +```bash +# 单设备(Enduro 3 主产物) +monkeyc -o bin/Fenix8V3.prg -f monkey.jungle -y developer_key.der -d enduro3 + +# 导出调试资源(含 -settings.json,便于模拟器载入设置) +monkeyc -o bin/Fenix8V3.prg -f monkey.jungle -y developer_key.der -d enduro3 -x + +# 多设备批量(已验证可编译) +for d in enduro3 fenix843mm fenix847mm fenix8pro47mm fenix8solar47mm \ + fenix8solar51mm epix2 epix2pro47mm epix2pro51mm fenix7 fenix7x \ + fenix7s fr965 fr265 fr255 venu3 vivoactive5; do + monkeyc -o "bin/Fenix8V3_${d}.prg" -f monkey.jungle -y developer_key.der -d "$d" \ + && echo "[$d] OK" || echo "[$d] FAIL" +done +``` + +已验证编译通过的设备:`enduro3, fenix843mm, fenix847mm, fenix8pro47mm, fenix8solar47mm, fenix8solar51mm, epix2, epix2pro47mm, epix2pro51mm, fenix7, fenix7x, fenix7s, fr965, fr265, fr255, venu3, vivoactive5`。产物位于 `bin/`,每个设备对应一个 `.prg` 及 `.prg.debug.xml`(调试版含资源镜像)。 + +--- + +## 6. 模拟器与侧载 + +1. **Connect IQ Simulator**(Garmin 自带):`File → Open App Project` 选本目录,选设备后 `Run`。在 `bin/*.prg.debug.xml` 旁会生成 `-settings.json`,可在模拟器菜单 `View → Settings` 载入并实时切换主题/指标验证。 +2. **真机侧载(Garmin Connect / Garmin Express)**: + - 用 Garmin Express 或 Connect 手机 App 的“侧载(sideload)”功能导入对应设备的 `bin/Fenix8V3_.prg`; + - 或把 `.prg` 拷入手表的 `/GARMIN/Apps/` 目录(需开发者模式 / WiFi 文件传输)。 +3. 安装后在手表“表盘”列表选择 **Fenix 8 V3**,长按进入设置即可调主题与指标。 + +--- + +## 7. 目录结构 + +``` +fenix8v3-watchface/ +├── manifest.xml # 应用清单(ID / 支持设备 / 语言) +├── monkey.jungle # 构建配置(manifest + 源码/资源路径) +├── developer_key.der # Connect IQ 开发者签名密钥 +├── source/ +│ ├── Fenix8V3App.mc # 应用入口:getInitialView / onSettingsChanged +│ ├── Fenix8V3View.mc # 表盘渲染主逻辑(onUpdate + 各 draw*) +│ └── Themes.mc # ⚠️ 自动生成:7 套主题颜色(勿手改) +├── resources/ +│ ├── strings/strings.xml # AppName / 设置项 / 主题 / 指标 / 字段文案 +│ └── settings/ +│ ├── properties.xml # 设置默认值 +│ └── settings.xml # 设置 UI 定义(列表/布尔) +└── bin/ # 编译产物(.prg + 调试 xml + settings.json) +``` + +--- + +## 8. 二次开发与扩展 + +- **改主题配色**:编辑 `watchface-tokens.json`(设计稿目录下),运行 `gen_themes.py` 重新生成 `source/Themes.mc`。新增主题时同步在 `Themes.mc` 的 `ORDER` 数组、`settings.xml` 的 `Theme` 列表、`strings.xml` 的 `ThemeXX` 文案中登记。 +- **加指标**:在 `metricProgress`(进度换算)与 `complicationValue`(复杂功能文本)两个方法里按 `idx` 分支扩展,并在 `settings.xml` / `properties.xml` 的列表项补充。 +- **调布局**:所有坐标集中在 `Fenix8V3View.mc` 的 `draw*` 方法里,均为 “设计像素 × `s`” 形式,改一处即可全局缩放生效。 + +--- + +## 9. 设计稿来源 + +还原基准为 `watchface-kit.zip` 内的 8 张 SVG 变体 + `watchface-tokens.json`(配色)+ `watchface-spec.html`(像素测量规范)+ `rebuild.py`(几何常量)。本仓库为按该规范落地的 Monkey C 实现,设计版权归原作者所有。 diff --git a/gen_preview_svg.py b/gen_preview_svg.py new file mode 100644 index 0000000..3f549d1 --- /dev/null +++ b/gen_preview_svg.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# 纯标准库生成表盘运行效果 SVG(复用 Fenix8V3View.mc 的坐标与主题色) +import math + +THEMES = { + "01":{"name":"Ember","ringTop":[[8,0xFFF203],[15,0xFDDB0D],[25,0xFFC203],[35,0xFFA00A],[45,0xFF8705],[55,0xFF610C],[65,0xFF4904],[75,0xFF2602],[86,0xFF2604]], + "ringBottom":[[7,0xAE1706],[14,0xB32504],[21,0xC03B03],[28,0xCD6206],[35,0xD57805],[42,0xDE9208],[47,0xC98D00]], + "tickOff":0x545655,"anchorDot":0x525551,"bandFill":0xFEAA00,"bandText":0x000002,"accent":0xFCA901,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xFBFBFB,"minutes":0xFCA901}, + "02":{"name":"Aurora","ringTop":[[8,0xA5FA11],[15,0x9EFA23],[25,0x97F84B],[35,0x8CF965],[45,0x89F87F],[55,0x74FB9F],[65,0x6DFCC6],[75,0x64FCDF],[86,0x57FDFF]], + "ringBottom":[[7,0x55FDFF],[14,0x64FCDF],[21,0x6FFCC6],[28,0x74FBAE],[35,0x7EFA85],[42,0x88F96D],[47,0x81E654]], + "tickOff":0x545655,"anchorDot":0x0F3C82,"bandFill":0x525551,"bandText":0xFFFFFF,"accent":0xABA9AC,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xFBFBFB,"minutes":0xAAF701}, + "03":{"name":"Brass","ringTop":[[8,0xB4814F],[15,0xB88954],[25,0xC5975C],[35,0xC59E65],[45,0xCBA775],[55,0xD7AE7F],[65,0xDEBC84],[75,0xE7C48D],[86,0xF0D097]], + "ringBottom":[[7,0xB97E4A],[14,0xB68954],[21,0xBD925E],[28,0xC69B67],[35,0xCEA26C],[42,0xD9AB74],[47,0xC59E70]], + "tickOff":0x3A2815,"anchorDot":0x62441E,"bandFill":0x392810,"bandText":0xFFFFFF,"accent":0xB37C4A,"textPrimary":0xEBCB93,"colon":0x525551,"hours":0xFBFBFB,"minutes":0xECCC93}, + "04":{"name":"Voltage","ringTop":[[8,0x5CAFF0],[15,0x6EB6E0],[25,0x7FBEC5],[35,0x96CA9D],[45,0xAAD87F],[55,0xBFDF66],[65,0xD6EA40],[75,0xE8F227],[86,0xFFFB0A]], + "ringBottom":[[7,0xF7FA11],[14,0xE9F225],[21,0xD6EB3F],[28,0xC0DE65],[35,0xADD880],[42,0xA0CF98],[47,0x88B98E]], + "tickOff":0x545655,"anchorDot":0x53AAFF,"bandFill":0x53AAFF,"bandText":0x000000,"accent":0xABA9AC,"textPrimary":0x51A7FA,"colon":0x525551,"hours":0xFBFBFB,"minutes":0x52A8FD}, + "05":{"name":"Reef","ringTop":[[8,0xF87E0D],[15,0xE78E25],[25,0xCE9B4D],[35,0xBDAC65],[45,0xA5BE88],[55,0x95CA9E],[65,0x7EDEC7],[75,0x6DEBE1],[86,0x57FDFF]], + "ringBottom":[[7,0xFF7905],[14,0xE78B29],[21,0xD59C3E],[28,0xBEAC63],[35,0xADB77F],[42,0xA0C796],[47,0x88B98E]], + "tickOff":0x545655,"anchorDot":0xFE7900,"bandFill":0x52FDFF,"bandText":0x000000,"accent":0xFCA901,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xFBFBFB,"minutes":0xFC7801}, + "06":{"name":"Acid","ringTop":[[8,0x9CCC14],[15,0xA9CE0C],[25,0xA4D610],[35,0xACDB08],[45,0xB4E107],[55,0xB5E603],[65,0xBFEE04],[75,0xBDF108],[86,0xC7F808]], + "ringBottom":[[7,0xC7FA02],[14,0xBEF20A],[21,0xC0ED04],[28,0xB4E905],[35,0xB4E107],[42,0xADDD0C],[47,0x9BC603]], + "tickOff":0x545655,"anchorDot":0xF58909,"bandFill":0x525551,"bandText":0x000000,"accent":0xC4F801,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xDBF731,"minutes":0xC4F900}, + "07":{"name":"Kelp","ringTop":[[8,0xEF8917],[15,0xD8901C],[25,0xBD9A34],[35,0xAD9E3C],[45,0x96A54E],[55,0x7DAE5D],[65,0x61B474],[75,0x4EBB84],[86,0x36C294]], + "ringBottom":[[7,0xCE520F],[14,0xB1631D],[21,0xA26A2F],[28,0x9D763E],[35,0x85874B],[42,0x76915D],[47,0x5E8651]], + "tickOff":0x545655,"anchorDot":0xF58909,"bandFill":0x525551,"bandText":0x000000,"accent":0x31C192,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xF28708,"minutes":0xF4880A}, +} +ORDER=["01","02","03","04","05","06","07"] +TOP=[a for a in range(2,87,4)]; BOT=[a for a in range(6,67,3)] +DOW=["SUN","MON","TUE","WED","THU","FRI","SAT"]; MON=["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"] + +def hx(c): return "#%06x"%(c&0xffffff) +def lerp(c1,c2,f): + r1=(c1>>16)&255;g1=(c1>>8)&255;b1=c1&255 + r2=(c2>>16)&255;g2=(c2>>8)&255;b2=c2&255 + return ((round(r1+(r2-r1)*f))<<16)|((round(g1+(g2-g1)*f))<<8)|round(b1+(b2-b1)*f) +def lerp_stops(stops,t): + n=len(stops) + if not n: return 0x888888 + if t<=stops[0][0]: return stops[0][1] + if t>=stops[-1][0]: return stops[-1][1] + for i in range(n-1): + a,b=stops[i],stops[i+1] + if a[0]<=t<=b[0]: + return lerp(a[1],b[1],(t-a[0])/(b[0]-a[0])) + return stops[-1][1] + +def tick(cx,cy,rIn,rOut,w,ang,col,mirror): + eff=-ang if mirror else ang; rad=math.radians(eff); s=math.sin(rad); c=math.cos(rad) + return f'' + +def ring(theme,pTR,pTL,pBR,pBL): + out=[] + rOut=249.5;rIn=231.5;off=theme["tickOff"];ts=theme["ringTop"];bs=theme["ringBottom"] + topW=13;botW=9 + litTR=round(pTR*22);litTL=round(pTL*22);litBR=round(pBR*21);litBL=round(pBL*21) + # TR anchorOuter, mirror=false + for i,a in enumerate(TOP): + on=i>=(22-litTR); col=lerp_stops(ts,a) if on else off + out.append(tick(250,250,rIn,rOut,topW,a,col,False)) + # TL anchorOuter, mirror=true + for i,a in enumerate(TOP): + on=i>=(22-litTL); col=lerp_stops(ts,a) if on else off + out.append(tick(250,250,rIn,rOut,topW,a,col,True)) + # BL mirror=true + for i,a in enumerate(BOT): + col=lerp_stops(bs,a) if i') + return "".join(out) + +def batt(theme,pct): + y=53.5;gw=30;gh=16;gx=250-52;gy=y-gh/2;ac=theme["accent"];tp=theme["textPrimary"] + fill_w=(gw-4)*pct/100 + return (f'' + f'' + f'' + f'{round(pct)}%') + +def complic_icon(x,y,r,col,t): + c=hx(col) + if t==0: # sun + s=f'' + for i in range(8): + rad=math.radians(i*45) + s+=f'' + return s + if t==1: # flame + return f'' + if t==2: # steps + return (f'' + f'') + if t==3: # dot + return f'' + if t==4: # chevron + return f'' + return f'' + +def top_comp(theme,t,isLeft,val): + sx=156 if isLeft else 343; iy=90; vy=130.5 + return (f'{val}' + +complic_icon(sx,iy,13,theme["accent"],t)) + +def bluetooth(x,y,r,col): + c=hx(col) + return (f'') + +def date_band(theme,gi): + by=286.5;bh=44;yc=308.5;bf=theme["bandFill"];bt=theme["bandText"] + dow=DOW[gi["dow"]-1]; md=MON[gi["m"]-1]+" "+str(gi["day"]) + return (f'' + +bluetooth(36.5,yc,10,bt) + +f'{dow}' + +f'{md}' + +f'{"PM" if gi["h"]>=12 else "AM"}') + +def time(theme,gi): + h=gi["h"]; h=12 if h==0 else (h-12 if h>12 else h) + hs=str(h); ms="%02d"%gi["min"]; ty=220 + return (f'{hs}' + f'{ms}' + f'' + f'') + +def bottom(theme,dist,steps): + vy=366;ly=410.5;lx=234;rx=266;tp=theme["textPrimary"];ac=theme["accent"] + return (f'{dist}' + f'DST' + f'{steps}' + f'STEP') + +def face(theme_id,m,battery,temp,gi): + th=THEMES[theme_id] + p=[m[0],m[1],m[2],m[3],m[4]] + idxTR,idxTL,idxBR,idxBL=0,1,4,3 + s=[] + s.append(f'') + s.append(ring(th,p[idxTR],p[idxTL],p[idxBR],p[idxBL])) + s.append(batt(th,battery)) + s.append(top_comp(th,1,True,f'{round(m[1]*2000):,}')) # 左上 卡路里 + s.append(top_comp(th,0,False,f'{temp}\u00b0')) # 右上 天气 + s.append(time(th,gi)) + s.append(date_band(th,gi)) + s.append(bottom(th,f'{m[2]*10:.1f}',f'{round(m[0]*10000):,}')) + return "".join(s) + +# 七主题合成图:单排行,每格 230px,圆形裁切 +m=[0.84,0.64,0.65,0.80,0.40]; battery=78; temp=23 +gi={"h":10,"min":8,"day":9,"m":9,"dow":3} +cell=230; gap=24; n=len(ORDER); W=n*cell+(n+1)*gap; H=cell+70 +parts=[f''] +parts.append(f'') +for i,tid in enumerate(ORDER): + ox=gap+i*(cell+gap); oy=24 + sc=cell/500.0 + parts.append(f'') + parts.append(f''+face(tid,m,battery,temp,gi)+'') + parts.append(f'{i+1} · {THEMES[tid]["name"]}') +parts.append('') +svg="\n".join(parts) +with open("/Users/ericwyuan/Desktop/Work/fenix8v3-watchface/preview-themes.svg","w") as f: + f.write(svg) +print("written preview-themes.svg", len(svg), "bytes; size", W,"x",H) diff --git a/manifest.xml b/manifest.xml new file mode 100644 index 0000000..e24ad13 --- /dev/null +++ b/manifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + chn + eng + + + + diff --git a/monkey.jungle b/monkey.jungle new file mode 100644 index 0000000..9a92609 --- /dev/null +++ b/monkey.jungle @@ -0,0 +1,9 @@ +project.manifest = manifest.xml + +base.sourcePath = source +base.resourcePath = resources +base.excludeAnnotations = release + +# Uncomment and adjust once you have SDK devices installed, e.g.: +# fenix8.resourcePath = resources;resources-round-amoled +# fenix7.resourcePath = resources;resources-round-mip diff --git a/preview-themes.svg b/preview-themes.svg new file mode 100644 index 0000000..4162d53 --- /dev/null +++ b/preview-themes.svg @@ -0,0 +1,24 @@ + + + +78%1,28023°1008TUESEP 9AM6.5DST8,400STEP +1 · Ember + +78%1,28023°1008TUESEP 9AM6.5DST8,400STEP +2 · Aurora + +78%1,28023°1008TUESEP 9AM6.5DST8,400STEP +3 · Brass + +78%1,28023°1008TUESEP 9AM6.5DST8,400STEP +4 · Voltage + +78%1,28023°1008TUESEP 9AM6.5DST8,400STEP +5 · Reef + +78%1,28023°1008TUESEP 9AM6.5DST8,400STEP +6 · Acid + +78%1,28023°1008TUESEP 9AM6.5DST8,400STEP +7 · Kelp + \ No newline at end of file diff --git a/preview.html b/preview.html new file mode 100644 index 0000000..fa98f51 --- /dev/null +++ b/preview.html @@ -0,0 +1,322 @@ + + + + + +Fenix 8 V3 表盘运行预览 + + + +
+

Fenix 8 V3 · 运行效果预览

+

网页版 1:1 还原 source/Fenix8V3View.mconUpdate 绘制逻辑:同坐标、同主题色、同指标填充。下方可切换 7 套主题、调指标进度、套圆形遮罩。

+ +
+
+ +
+
+

主题 Theme

+
+
+ +
+

指标进度(驱动进度环)

+
+
+
+
+
+
默认值对应设计稿:顶右=步数 · 顶左=卡路里 · 底右=动动条 · 底左=楼层
+
+ +
+

显示 / 外观

+
+
+
+
+
+
+
+ + +
+ + + + diff --git a/resources/drawables/drawables.xml b/resources/drawables/drawables.xml new file mode 100644 index 0000000..579343d --- /dev/null +++ b/resources/drawables/drawables.xml @@ -0,0 +1,3 @@ + + + diff --git a/resources/drawables/launcher_icon.png b/resources/drawables/launcher_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c257fffa825ffd9c1661072c3693ed8c4ec3cb95 GIT binary patch literal 498 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8VBG2H;uumf=k49QxrZDCTp!*H zjT2ndA~M-6C_>89P?J%SOM(ByiH+Zn-^+evX!o3xwdcfg$tI=KEZHv#tnNkieZKd# zhO@YTX3c@si=NKvimbhG??KyDh2)p#_CB6@m3Pvv153SqA7?NfJUxAr<~oC`znM>b zxhgHydPnyBi@(1=7yf?!IJ@}#{q@!MDXm8rNv^&flzxn*i%~-?^8Poz<&S$gMVJG4 z91IzbG7!PIcdd?b=dX0h65HQ7&C_E161diX`D|Lu9^w3{?FCnXR-d|yc|&pF9)-4B zGdbhpUd)>MuVUxS18dn9M3lBpUsJ-Cc{ + 1 + 0 + 1 + 4 + 3 + 1 + 0 + true + true + true + diff --git a/resources/settings/settings.xml b/resources/settings/settings.xml new file mode 100644 index 0000000..eb89af8 --- /dev/null +++ b/resources/settings/settings.xml @@ -0,0 +1,87 @@ + + + + @Strings.ThemeEmber + @Strings.ThemeAurora + @Strings.ThemeBrass + @Strings.ThemeVoltage + @Strings.ThemeReef + @Strings.ThemeAcid + @Strings.ThemeKelp + + + + + + @Strings.MetricSteps + @Strings.MetricCalories + @Strings.MetricDistance + @Strings.MetricFloors + @Strings.MetricMoveBar + + + + + + @Strings.MetricSteps + @Strings.MetricCalories + @Strings.MetricDistance + @Strings.MetricFloors + @Strings.MetricMoveBar + + + + + + @Strings.MetricSteps + @Strings.MetricCalories + @Strings.MetricDistance + @Strings.MetricFloors + @Strings.MetricMoveBar + + + + + + @Strings.MetricSteps + @Strings.MetricCalories + @Strings.MetricDistance + @Strings.MetricFloors + @Strings.MetricMoveBar + + + + + + @Strings.FieldWeather + @Strings.FieldCalories + @Strings.FieldSteps + @Strings.FieldDistance + @Strings.FieldFloors + @Strings.FieldBattery + + + + + + @Strings.FieldWeather + @Strings.FieldCalories + @Strings.FieldSteps + @Strings.FieldDistance + @Strings.FieldFloors + @Strings.FieldBattery + + + + + + + + + + + + + + + diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml new file mode 100644 index 0000000..5397090 --- /dev/null +++ b/resources/strings/strings.xml @@ -0,0 +1,35 @@ + + Fenix 8 V3 + + Theme + Ring · Top-Right + Ring · Top-Left + Ring · Bottom-Right + Ring · Bottom-Left + Top-Left Complication + Top-Right Complication + Show Top Row + Show Date Band + Show Bottom Row + + 1 · Ember + 2 · Aurora + 3 · Brass + 4 · Voltage + 5 · Reef + 6 · Acid + 7 · Kelp + + Steps + Calories + Distance + Floors + Move Bar + + Weather + Calories + Steps + Distance + Floors + Battery + diff --git a/source/Fenix8V3App.mc b/source/Fenix8V3App.mc new file mode 100644 index 0000000..409d16d --- /dev/null +++ b/source/Fenix8V3App.mc @@ -0,0 +1,28 @@ +import Toybox.Application; +import Toybox.Lang; +import Toybox.WatchUi; + +class Fenix8V3App extends Application.AppBase { + + function initialize() { + AppBase.initialize(); + } + + function onStart(state as Dictionary?) as Void { + } + + function onStop(state as Dictionary?) as Void { + } + + function getInitialView() as [Views] or [Views, InputDelegates] { + return [new Fenix8V3View()]; + } + + function onSettingsChanged() as Void { + WatchUi.requestUpdate(); + } +} + +function getApp() as Fenix8V3App { + return Application.getApp() as Fenix8V3App; +} diff --git a/source/Fenix8V3View.mc b/source/Fenix8V3View.mc new file mode 100644 index 0000000..54fbdd1 --- /dev/null +++ b/source/Fenix8V3View.mc @@ -0,0 +1,387 @@ +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>, 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, stops as Array>, + 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, stops as Array>, + 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>; + var botStops = theme["ringBottom"] as Array>; + 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); + } + } +} diff --git a/source/Themes.mc b/source/Themes.mc new file mode 100644 index 0000000..e69b3e0 --- /dev/null +++ b/source/Themes.mc @@ -0,0 +1,110 @@ +// AUTO-GENERATED from watchface-tokens.json — do not edit by hand. +module Themes { + // THEMES: id -> theme dictionary. + // ringTop / ringBottom are gradient stop arrays: [angle, 0xRRGGBB] sorted ascending. + var THEMES = { + "01" => { + "name" => "Ember", + "ringTop" => [[8,0xFFF203],[15,0xFDDB0D],[25,0xFFC203],[35,0xFFA00A],[45,0xFF8705],[55,0xFF610C],[65,0xFF4904],[75,0xFF2602],[86,0xFF2604]], + "ringBottom" => [[7,0xAE1706],[14,0xB32504],[21,0xC03B03],[28,0xCD6206],[35,0xD57805],[42,0xDE9208],[47,0xC98D00]], + "tickOff" => 0x545655, + "anchorDot" => 0x525551, + "bandFill" => 0xFEAA00, + "bandText" => 0x000002, + "accent" => 0xFCA901, + "textPrimary" => 0xFAFAFA, + "colon" => 0x525551, + "hours" => 0xFBFBFB, + "minutes" => 0xFCA901, + }, + "02" => { + "name" => "Aurora", + "ringTop" => [[8,0xA5FA11],[15,0x9EFA23],[25,0x97F84B],[35,0x8CF965],[45,0x89F87F],[55,0x74FB9F],[65,0x6DFCC6],[75,0x64FCDF],[86,0x57FDFF]], + "ringBottom" => [[7,0x55FDFF],[14,0x64FCDF],[21,0x6FFCC6],[28,0x74FBAE],[35,0x7EFA85],[42,0x88F96D],[47,0x81E654]], + "tickOff" => 0x545655, + "anchorDot" => 0x0F3C82, + "bandFill" => 0x525551, + "bandText" => 0xFFFFFF, + "accent" => 0xABA9AC, + "textPrimary" => 0xFAFAFA, + "colon" => 0x525551, + "hours" => 0xFBFBFB, + "minutes" => 0xAAF701, + }, + "03" => { + "name" => "Brass", + "ringTop" => [[8,0xB4814F],[15,0xB88954],[25,0xC5975C],[35,0xC59E65],[45,0xCBA775],[55,0xD7AE7F],[65,0xDEBC84],[75,0xE7C48D],[86,0xF0D097]], + "ringBottom" => [[7,0xB97E4A],[14,0xB68954],[21,0xBD925E],[28,0xC69B67],[35,0xCEA26C],[42,0xD9AB74],[47,0xC59E70]], + "tickOff" => 0x3A2815, + "anchorDot" => 0x62441E, + "bandFill" => 0x392810, + "bandText" => 0xFFFFFF, + "accent" => 0xB37C4A, + "textPrimary" => 0xEBCB93, + "colon" => 0x525551, + "hours" => 0xFBFBFB, + "minutes" => 0xECCC93, + }, + "04" => { + "name" => "Voltage", + "ringTop" => [[8,0x5CAFF0],[15,0x6EB6E0],[25,0x7FBEC5],[35,0x96CA9D],[45,0xAAD87F],[55,0xBFDF66],[65,0xD6EA40],[75,0xE8F227],[86,0xFFFB0A]], + "ringBottom" => [[7,0xF7FA11],[14,0xE9F225],[21,0xD6EB3F],[28,0xC0DE65],[35,0xADD880],[42,0xA0CF98],[47,0x88B98E]], + "tickOff" => 0x545655, + "anchorDot" => 0x53AAFF, + "bandFill" => 0x53AAFF, + "bandText" => 0x000000, + "accent" => 0xABA9AC, + "textPrimary" => 0x51A7FA, + "colon" => 0x525551, + "hours" => 0xFBFBFB, + "minutes" => 0x52A8FD, + }, + "05" => { + "name" => "Reef", + "ringTop" => [[8,0xF87E0D],[15,0xE78E25],[25,0xCE9B4D],[35,0xBDAC65],[45,0xA5BE88],[55,0x95CA9E],[65,0x7EDEC7],[75,0x6DEBE1],[86,0x57FDFF]], + "ringBottom" => [[7,0xFF7905],[14,0xE78B29],[21,0xD59C3E],[28,0xBEAC63],[35,0xADB77F],[42,0xA0C796],[47,0x88B98E]], + "tickOff" => 0x545655, + "anchorDot" => 0xFE7900, + "bandFill" => 0x52FDFF, + "bandText" => 0x000000, + "accent" => 0xFCA901, + "textPrimary" => 0xFAFAFA, + "colon" => 0x525551, + "hours" => 0xFBFBFB, + "minutes" => 0xFC7801, + }, + "06" => { + "name" => "Acid", + "ringTop" => [[8,0x9CCC14],[15,0xA9CE0C],[25,0xA4D610],[35,0xACDB08],[45,0xB4E107],[55,0xB5E603],[65,0xBFEE04],[75,0xBDF108],[86,0xC7F808]], + "ringBottom" => [[7,0xC7FA02],[14,0xBEF20A],[21,0xC0ED04],[28,0xB4E905],[35,0xB4E107],[42,0xADDD0C],[47,0x9BC603]], + "tickOff" => 0x545655, + "anchorDot" => 0xF58909, + "bandFill" => 0x525551, + "bandText" => 0x000000, + "accent" => 0xC4F801, + "textPrimary" => 0xFAFAFA, + "colon" => 0x525551, + "hours" => 0xDBF731, + "minutes" => 0xC4F900, + }, + "07" => { + "name" => "Kelp", + "ringTop" => [[8,0xEF8917],[15,0xD8901C],[25,0xBD9A34],[35,0xAD9E3C],[45,0x96A54E],[55,0x7DAE5D],[65,0x61B474],[75,0x4EBB84],[86,0x36C294]], + "ringBottom" => [[7,0xCE520F],[14,0xB1631D],[21,0xA26A2F],[28,0x9D763E],[35,0x85874B],[42,0x76915D],[47,0x5E8651]], + "tickOff" => 0x545655, + "anchorDot" => 0xF58909, + "bandFill" => 0x525551, + "bandText" => 0x000000, + "accent" => 0x31C192, + "textPrimary" => 0xFAFAFA, + "colon" => 0x525551, + "hours" => 0xF28708, + "minutes" => 0xF4880A, + } + }; + + // theme id list for settings enum (1-based index -> id) + var ORDER = [ + "01", "02", "03", "04", "05", "06", "07" + ]; +}