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
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -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
|
||||
168
README.md
Normal file
168
README.md
Normal file
@@ -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_<device>.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 实现,设计版权归原作者所有。
|
||||
172
gen_preview_svg.py
Normal file
172
gen_preview_svg.py
Normal file
@@ -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'<line x1="{cx+s*rIn:.2f}" y1="{cy-c*rIn:.2f}" x2="{cx+s*rOut:.2f}" y2="{cy-c*rOut:.2f}" stroke="{hx(col)}" stroke-width="{w}" stroke-linecap="round"/>'
|
||||
|
||||
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<litBL else off
|
||||
out.append(tick(250,250,rIn,rOut,botW,180-a,col,True))
|
||||
# BR mirror=false
|
||||
for i,a in enumerate(BOT):
|
||||
col=lerp_stops(bs,a) if i<litBR else off
|
||||
out.append(tick(250,250,rIn,rOut,botW,180-a,col,False))
|
||||
# anchor dots
|
||||
dc=theme["anchorDot"];dr=9.5;drad=240.5
|
||||
for da in [0,93.3,180,266.7]:
|
||||
rad=math.radians(da)
|
||||
out.append(f'<circle cx="{250+math.sin(rad)*drad:.2f}" cy="{250-math.cos(rad)*drad:.2f}" r="{dr}" fill="{hx(dc)}"/>')
|
||||
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'<rect x="{gx}" y="{gy}" width="{gw}" height="{gh}" fill="none" stroke="{hx(ac)}" stroke-width="2"/>'
|
||||
f'<rect x="{gx+gw}" y="{gy+gh*0.28}" width="3" height="{gh*0.44}" fill="{hx(ac)}"/>'
|
||||
f'<rect x="{gx+2}" y="{gy+2}" width="{fill_w:.2f}" height="{gh-4}" fill="{hx(ac)}"/>'
|
||||
f'<text x="{250+16}" y="{y}" fill="{hx(tp)}" font-size="16" font-family="sans-serif" text-anchor="start" dominant-baseline="central">{round(pct)}%</text>')
|
||||
|
||||
def complic_icon(x,y,r,col,t):
|
||||
c=hx(col)
|
||||
if t==0: # sun
|
||||
s=f'<circle cx="{x}" cy="{y}" r="{r*0.45}" fill="{c}"/>'
|
||||
for i in range(8):
|
||||
rad=math.radians(i*45)
|
||||
s+=f'<line x1="{x+math.sin(rad)*r*0.6:.2f}" y1="{y-math.cos(rad)*r*0.6:.2f}" x2="{x+math.sin(rad)*r:.2f}" y2="{y-math.cos(rad)*r:.2f}" stroke="{c}" stroke-width="2"/>'
|
||||
return s
|
||||
if t==1: # flame
|
||||
return f'<polygon points="{x},{y-r} {x+r*0.8:.2f},{y+r*0.6:.2f} {x-r*0.8:.2f},{y+r*0.6:.2f}" fill="{c}"/>'
|
||||
if t==2: # steps
|
||||
return (f'<line x1="{x-r*0.5:.2f}" y1="{y-r*0.3:.2f}" x2="{x+r*0.5:.2f}" y2="{y-r*0.3:.2f}" stroke="{c}" stroke-width="2"/>'
|
||||
f'<line x1="{x-r*0.3:.2f}" y1="{y}" x2="{x+r*0.6:.2f}" y2="{y}" stroke="{c}" stroke-width="2"/>')
|
||||
if t==3: # dot
|
||||
return f'<circle cx="{x}" cy="{y}" r="{r*0.5}" fill="{c}"/>'
|
||||
if t==4: # chevron
|
||||
return f'<polygon points="{x},{y-r} {x+r*0.7:.2f},{y+r*0.2:.2f} {x-r*0.7:.2f},{y+r*0.2:.2f}" fill="{c}"/>'
|
||||
return f'<rect x="{x-r*0.6:.2f}" y="{y-r*0.4:.2f}" width="{r*1.2:.2f}" height="{r*0.8:.2f}" fill="none" stroke="{c}" stroke-width="2"/>'
|
||||
|
||||
def top_comp(theme,t,isLeft,val):
|
||||
sx=156 if isLeft else 343; iy=90; vy=130.5
|
||||
return (f'<text x="{sx}" y="{vy}" fill="{hx(theme["textPrimary"])}" font-size="22" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">{val}</text>'
|
||||
+complic_icon(sx,iy,13,theme["accent"],t))
|
||||
|
||||
def bluetooth(x,y,r,col):
|
||||
c=hx(col)
|
||||
return (f'<path d="M{x},{y-r} L{x},{y+r} M{x},{y-r} L{x+r*0.7:.2f},{y-r*0.3:.2f} L{x},{y} '
|
||||
f'L{x+r*0.7:.2f},{y+r*0.3:.2f} L{x},{y+r}" fill="none" stroke="{c}" stroke-width="3" stroke-linejoin="round"/>')
|
||||
|
||||
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'<rect x="0" y="{by}" width="500" height="{bh}" fill="{hx(bf)}"/>'
|
||||
+bluetooth(36.5,yc,10,bt)
|
||||
+f'<text x="177.5" y="{yc}" fill="{hx(bt)}" font-size="16" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">{dow}</text>'
|
||||
+f'<text x="297.5" y="{yc}" fill="{hx(bt)}" font-size="16" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">{md}</text>'
|
||||
+f'<text x="465" y="{yc}" fill="{hx(bt)}" font-size="16" font-family="sans-serif" text-anchor="end" dominant-baseline="central">{"PM" if gi["h"]>=12 else "AM"}</text>')
|
||||
|
||||
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'<text x="211" y="{ty}" fill="{hx(theme["hours"])}" font-size="66" font-weight="700" font-family="sans-serif" text-anchor="end" dominant-baseline="central">{hs}</text>'
|
||||
f'<text x="272" y="{ty}" fill="{hx(theme["minutes"])}" font-size="66" font-weight="700" font-family="sans-serif" text-anchor="start" dominant-baseline="central">{ms}</text>'
|
||||
f'<rect x="238" y="184" width="24" height="20" fill="{hx(theme["colon"])}"/>'
|
||||
f'<rect x="238" y="235" width="24" height="20" fill="{hx(theme["colon"])}"/>')
|
||||
|
||||
def bottom(theme,dist,steps):
|
||||
vy=366;ly=410.5;lx=234;rx=266;tp=theme["textPrimary"];ac=theme["accent"]
|
||||
return (f'<text x="{lx}" y="{vy}" fill="{hx(tp)}" font-size="22" font-family="sans-serif" text-anchor="end" dominant-baseline="central">{dist}</text>'
|
||||
f'<text x="{lx}" y="{ly}" fill="{hx(ac)}" font-size="16" font-family="sans-serif" text-anchor="end" dominant-baseline="central">DST</text>'
|
||||
f'<text x="{rx}" y="{vy}" fill="{hx(tp)}" font-size="22" font-family="sans-serif" text-anchor="start" dominant-baseline="central">{steps}</text>'
|
||||
f'<text x="{rx}" y="{ly}" fill="{hx(ac)}" font-size="16" font-family="sans-serif" text-anchor="start" dominant-baseline="central">STEP</text>')
|
||||
|
||||
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'<rect x="0" y="0" width="500" height="500" fill="#000"/>')
|
||||
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'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">']
|
||||
parts.append(f'<rect width="{W}" height="{H}" fill="#0a0a0a"/>')
|
||||
for i,tid in enumerate(ORDER):
|
||||
ox=gap+i*(cell+gap); oy=24
|
||||
sc=cell/500.0
|
||||
parts.append(f'<defs><clipPath id="c{i}"><circle cx="{ox+cell/2}" cy="{oy+cell/2}" r="{cell/2-2}"/></clipPath></defs>')
|
||||
parts.append(f'<g clip-path="url(#c{i})"><g transform="translate({ox},{oy}) scale({sc:.4f})">'+face(tid,m,battery,temp,gi)+'</g></g>')
|
||||
parts.append(f'<text x="{ox+cell/2}" y="{oy+cell+22}" fill="#cfccc4" font-size="15" font-family="sans-serif" text-anchor="middle">{i+1} · {THEMES[tid]["name"]}</text>')
|
||||
parts.append('</svg>')
|
||||
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)
|
||||
38
manifest.xml
Normal file
38
manifest.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<iq:manifest xmlns:iq="http://www.garmin.com/xml/connectiq" version="3">
|
||||
<iq:application
|
||||
entry="Fenix8V3App"
|
||||
id="f8a3c1b2c3d44e5f8a9b0c1d2e3f4a5b"
|
||||
launcherIcon="@Drawables.LauncherIcon"
|
||||
minSdkVersion="4.2.0"
|
||||
name="@Strings.AppName"
|
||||
type="watchface"
|
||||
version="1.0.0">
|
||||
|
||||
<iq:products>
|
||||
<iq:product id="enduro3"/>
|
||||
<iq:product id="fenix843mm"/>
|
||||
<iq:product id="fenix847mm"/>
|
||||
<iq:product id="fenix8pro47mm"/>
|
||||
<iq:product id="fenix8solar47mm"/>
|
||||
<iq:product id="fenix8solar51mm"/>
|
||||
<iq:product id="epix2"/>
|
||||
<iq:product id="epix2pro47mm"/>
|
||||
<iq:product id="epix2pro51mm"/>
|
||||
<iq:product id="fenix7"/>
|
||||
<iq:product id="fenix7x"/>
|
||||
<iq:product id="fenix7s"/>
|
||||
<iq:product id="fr965"/>
|
||||
<iq:product id="fr265"/>
|
||||
<iq:product id="fr255"/>
|
||||
<iq:product id="venu3"/>
|
||||
<iq:product id="vivoactive5"/>
|
||||
</iq:products>
|
||||
|
||||
<iq:languages>
|
||||
<iq:language>chn</iq:language>
|
||||
<iq:language>eng</iq:language>
|
||||
</iq:languages>
|
||||
|
||||
</iq:application>
|
||||
</iq:manifest>
|
||||
9
monkey.jungle
Normal file
9
monkey.jungle
Normal file
@@ -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
|
||||
24
preview-themes.svg
Normal file
24
preview-themes.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 90 KiB |
322
preview.html
Normal file
322
preview.html
Normal file
@@ -0,0 +1,322 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Fenix 8 V3 表盘运行预览</title>
|
||||
<style>
|
||||
:root{ --bg:#0a0a0a; --panel:#141413; --ink:#e9e6e0; --dim:#8d8a84; --line:#26261f; --amber:#FEAA00; }
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.6 -apple-system,"PingFang SC","Microsoft YaHei",Helvetica,Arial,sans-serif}
|
||||
.wrap{max-width:1080px;margin:0 auto;padding:32px 20px 64px}
|
||||
h1{font-size:22px;margin:0 0 4px}
|
||||
.lede{color:var(--dim);margin:0 0 24px}
|
||||
.stage{display:flex;gap:28px;flex-wrap:wrap;align-items:flex-start}
|
||||
.facebox{background:#000;border-radius:14px;padding:14px;box-shadow:0 8px 30px #0008}
|
||||
canvas{display:block;background:#000;border-radius:50%}
|
||||
canvas.square{border-radius:12px}
|
||||
.controls{display:flex;flex-direction:column;gap:18px;min-width:240px;flex:1}
|
||||
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
|
||||
.card h3{margin:0 0 10px;font-size:13px;color:var(--dim);font-weight:500}
|
||||
.themes{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
||||
.themes button{cursor:pointer;border:1px solid var(--line);background:#1c1c19;color:var(--ink);border-radius:8px;padding:8px 4px;font-size:12px;transition:.15s}
|
||||
.themes button:hover{border-color:var(--amber)}
|
||||
.themes button.active{background:var(--amber);color:#000;border-color:var(--amber);font-weight:600}
|
||||
.row{display:flex;align-items:center;gap:10px;margin:8px 0}
|
||||
.row label{flex:1;color:var(--dim)}
|
||||
input[type=range]{width:130px}
|
||||
.chk{display:flex;align-items:center;gap:8px;color:var(--dim)}
|
||||
.val{font:12px ui-monospace,Menlo,monospace;color:var(--amber);min-width:42px;text-align:right}
|
||||
.gallery{margin-top:34px}
|
||||
.gallery h2{font-size:16px;border-bottom:1px solid var(--line);padding-bottom:8px}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:18px;margin-top:18px}
|
||||
figure{margin:0;text-align:center}
|
||||
figure canvas{margin:0 auto;border-radius:50%;background:#000}
|
||||
figcaption{color:var(--dim);font-size:12px;margin-top:8px}
|
||||
.note{color:var(--dim);font-size:12px;margin-top:6px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Fenix 8 V3 · 运行效果预览</h1>
|
||||
<p class="lede">网页版 1:1 还原 <code>source/Fenix8V3View.mc</code> 的 <code>onUpdate</code> 绘制逻辑:同坐标、同主题色、同指标填充。下方可切换 7 套主题、调指标进度、套圆形遮罩。</p>
|
||||
|
||||
<div class="stage">
|
||||
<div class="facebox"><canvas id="face" width="500" height="500"></canvas></div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="card">
|
||||
<h3>主题 Theme</h3>
|
||||
<div class="themes" id="themes"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>指标进度(驱动进度环)</h3>
|
||||
<div class="row"><label>步数 Steps</label><input type="range" id="m0" min="0" max="100" value="84"><span class="val" id="v0"></span></div>
|
||||
<div class="row"><label>卡路里 Calories</label><input type="range" id="m1" min="0" max="100" value="64"><span class="val" id="v1"></span></div>
|
||||
<div class="row"><label>距离 Distance</label><input type="range" id="m2" min="0" max="100" value="65"><span class="val" id="v2"></span></div>
|
||||
<div class="row"><label>楼层 Floors</label><input type="range" id="m3" min="0" max="100" value="80"><span class="val" id="v3"></span></div>
|
||||
<div class="row"><label>动动条 Move Bar</label><input type="range" id="m4" min="0" max="100" value="40"><span class="val" id="v4"></span></div>
|
||||
<div class="note">默认值对应设计稿:顶右=步数 · 顶左=卡路里 · 底右=动动条 · 底左=楼层</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>显示 / 外观</h3>
|
||||
<div class="row"><label>电量 Battery</label><input type="range" id="bat" min="0" max="100" value="78"><span class="val" id="vbat"></span></div>
|
||||
<div class="row"><label>天气温度 °C</label><input type="range" id="temp" min="-10" max="45" value="23"><span class="val" id="vtemp"></span></div>
|
||||
<div class="chk"><input type="checkbox" id="round" checked><label for="round">圆形设备遮罩(多数 Fenix/Epix 为圆屏)</label></div>
|
||||
<div class="chk"><input type="checkbox" id="live" checked><label for="live">时间实时走动</label></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gallery">
|
||||
<h2>七套主题一览(实时当前时间)</h2>
|
||||
<div class="grid" id="gallery"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ---- 主题数据(与 source/Themes.mc 一致,由 watchface-tokens.json 生成)----
|
||||
const 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}
|
||||
};
|
||||
const ORDER = ["01","02","03","04","05","06","07"];
|
||||
|
||||
// ---- 颜色工具 ----
|
||||
const toHex = c => '#' + (c & 0xffffff).toString(16).padStart(6,'0');
|
||||
function lerpColor(c1,c2,f){
|
||||
const r1=(c1>>16)&255,g1=(c1>>8)&255,b1=c1&255;
|
||||
const r2=(c2>>16)&255,g2=(c2>>8)&255,b2=c2&255;
|
||||
return (Math.round(r1+(r2-r1)*f)<<16)|(Math.round(g1+(g2-g1)*f)<<8)|Math.round(b1+(b2-b1)*f);
|
||||
}
|
||||
function lerpStops(stops,t){
|
||||
const n=stops.length; if(!n) return 0x888888;
|
||||
const first=stops[0], last=stops[n-1];
|
||||
if(t<=first[0]) return first[1];
|
||||
if(t>=last[0]) return last[1];
|
||||
for(let i=0;i<n-1;i++){const a=stops[i],b=stops[i+1];
|
||||
if(t>=a[0]&&t<=b[0]) return lerpColor(a[1],b[1],(t-a[0])/(b[0]-a[0]));}
|
||||
return last[1];
|
||||
}
|
||||
|
||||
// ---- 几何常量(设计稿 500 基准)----
|
||||
const TOP=[], BOT=[];
|
||||
for(let a=2;a<=86;a+=4) TOP.push(a);
|
||||
for(let a=6;a<=66;a+=3) BOT.push(a);
|
||||
const DOW=["SUN","MON","TUE","WED","THU","FRI","SAT"];
|
||||
const MON=["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"];
|
||||
|
||||
// ---- 绘制原语(与 Monkey C 对应)----
|
||||
function drawTick(ctx,cx,cy,rIn,rOut,w,ang,col,mirror){
|
||||
const eff=mirror?-ang:ang, rad=eff*Math.PI/180, s=Math.sin(rad), c=Math.cos(rad);
|
||||
ctx.strokeStyle=toHex(col); ctx.lineWidth=w; ctx.lineCap='round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx+s*rIn, cy-c*rIn); ctx.lineTo(cx+s*rOut, cy-c*rOut); ctx.stroke();
|
||||
}
|
||||
function drawArcTop(ctx,cx,cy,rIn,rOut,w,angles,stops,off,mirror,lit,anchorOuter){
|
||||
const n=angles.length;
|
||||
for(let i=0;i<n;i++){const a=angles[i];
|
||||
const on=anchorOuter?(i>=n-lit):(i<lit);
|
||||
drawTick(ctx,cx,cy,rIn,rOut,w,a,on?lerpStops(stops,a):off,mirror);}
|
||||
}
|
||||
function drawArcBottom(ctx,cx,cy,rIn,rOut,w,botAngles,stops,off,mirror,lit){
|
||||
const n=botAngles.length;
|
||||
for(let i=0;i<n;i++){const a=botAngles[i];
|
||||
const ang=180-a;
|
||||
drawTick(ctx,cx,cy,rIn,rOut,w,ang,i<lit?lerpStops(stops,a):off,mirror);}
|
||||
}
|
||||
function drawRing(ctx,cx,cy,s,theme,pTR,pTL,pBR,pBL){
|
||||
const rOut=249.5*s, rIn=231.5*s, tickOff=theme.tickOff;
|
||||
const topStops=theme.ringTop, botStops=theme.ringBottom;
|
||||
const topW=13*s, botW=9*s;
|
||||
const litTR=Math.round(pTR*22), litTL=Math.round(pTL*22), litBR=Math.round(pBR*21), litBL=Math.round(pBL*21);
|
||||
drawArcTop(ctx,cx,cy,rIn,rOut,topW,TOP,topStops,tickOff,false,litTR,true);
|
||||
drawArcTop(ctx,cx,cy,rIn,rOut,topW,TOP,topStops,tickOff,true ,litTL,true);
|
||||
drawArcBottom(ctx,cx,cy,rIn,rOut,botW,BOT,botStops,tickOff,true ,litBL);
|
||||
drawArcBottom(ctx,cx,cy,rIn,rOut,botW,BOT,botStops,tickOff,false,litBR);
|
||||
// 锚点圆点
|
||||
const dotR=9.5*s, dotRad=240.5*s, dotCol=theme.anchorDot;
|
||||
ctx.fillStyle=toHex(dotCol);
|
||||
[0,93.3,180,266.7].forEach(da=>{const rad=da*Math.PI/180;
|
||||
ctx.beginPath(); ctx.arc(cx+Math.sin(rad)*dotRad, cy-Math.cos(rad)*dotRad, dotR, 0, 7); ctx.fill();});
|
||||
}
|
||||
function drawBattery(ctx,cx,s,theme,pct){
|
||||
const y=53.5*s, gw=30*s, gh=16*s, gx=cx-52*s, gy=y-gh/2;
|
||||
ctx.strokeStyle=toHex(theme.accent); ctx.lineWidth=2;
|
||||
ctx.strokeRect(gx,gy,gw,gh);
|
||||
ctx.fillStyle=toHex(theme.accent);
|
||||
ctx.fillRect(gx+gw, gy+gh*0.28, 3*s, gh*0.44);
|
||||
ctx.fillStyle=toHex(theme.accent);
|
||||
ctx.fillRect(gx+2, gy+2, (gw-4)*(pct/100), gh-4);
|
||||
ctx.fillStyle=toHex(theme.textPrimary);
|
||||
ctx.font=`${Math.round(16*s)}px -apple-system,Helvetica,Arial`;
|
||||
ctx.textAlign='left'; ctx.textBaseline='middle';
|
||||
ctx.fillText(Math.round(pct)+'%', cx+16*s, y);
|
||||
}
|
||||
function drawComplicationIcon(ctx,x,y,r,col,typeIdx){
|
||||
ctx.strokeStyle=toHex(col); ctx.fillStyle=toHex(col); ctx.lineWidth=2;
|
||||
if(typeIdx===0){ // sun
|
||||
ctx.beginPath(); ctx.arc(x,y,r*0.45,0,7); ctx.fill();
|
||||
for(let i=0;i<8;i++){const rad=i*45*Math.PI/180;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x+Math.sin(rad)*r*0.6, y-Math.cos(rad)*r*0.6);
|
||||
ctx.lineTo(x+Math.sin(rad)*r, y-Math.cos(rad)*r); ctx.stroke();}
|
||||
} else if(typeIdx===1){ // flame
|
||||
ctx.beginPath(); ctx.moveTo(x,y-r); ctx.lineTo(x+r*0.8,y+r*0.6); ctx.lineTo(x-r*0.8,y+r*0.6); ctx.closePath(); ctx.fill();
|
||||
} else if(typeIdx===2){ // steps
|
||||
ctx.beginPath(); ctx.moveTo(x-r*0.5,y-r*0.3); ctx.lineTo(x+r*0.5,y-r*0.3);
|
||||
ctx.moveTo(x-r*0.3,y); ctx.lineTo(x+r*0.6,y); ctx.stroke();
|
||||
} else if(typeIdx===3){ // distance dot
|
||||
ctx.beginPath(); ctx.arc(x,y,r*0.5,0,7); ctx.fill();
|
||||
} else if(typeIdx===4){ // floors chevron
|
||||
ctx.beginPath(); ctx.moveTo(x,y-r); ctx.lineTo(x+r*0.7,y+r*0.2); ctx.lineTo(x-r*0.7,y+r*0.2); ctx.closePath(); ctx.fill();
|
||||
} else { // battery
|
||||
ctx.strokeRect(x-r*0.6,y-r*0.4,r*1.2,r*0.8);
|
||||
}
|
||||
}
|
||||
function drawTopComplication(ctx,cx,cy,s,theme,typeIdx,isLeft,valStr){
|
||||
const sideX=(isLeft?156:343)*s, iconY=90*s, valY=130.5*s;
|
||||
ctx.fillStyle=toHex(theme.textPrimary);
|
||||
ctx.font=`${Math.round(22*s)}px -apple-system,Helvetica,Arial`;
|
||||
ctx.textAlign='center'; ctx.textBaseline='middle';
|
||||
ctx.fillText(valStr, sideX, valY);
|
||||
drawComplicationIcon(ctx, sideX, iconY, 13*s, theme.accent, typeIdx);
|
||||
}
|
||||
function drawBluetooth(ctx,x,y,r,col){
|
||||
ctx.strokeStyle=toHex(col); ctx.lineWidth=3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x,y-r); ctx.lineTo(x,y+r);
|
||||
ctx.moveTo(x,y-r); ctx.lineTo(x+r*0.7,y-r*0.3);
|
||||
ctx.lineTo(x,y); ctx.moveTo(x,y); ctx.lineTo(x+r*0.7,y+r*0.3);
|
||||
ctx.lineTo(x,y+r); ctx.stroke();
|
||||
}
|
||||
function drawDateBand(ctx,w,cx,s,theme,gi){
|
||||
const by=286.5*s, bh=44*s, yc=308.5*s;
|
||||
ctx.fillStyle=toHex(theme.bandFill); ctx.fillRect(0,by,w,bh);
|
||||
const txt=toHex(theme.bandText);
|
||||
drawBluetooth(ctx,36.5*s,yc,10*s,theme.bandText);
|
||||
const dow=DOW[gi.day_of_week-1], md=MON[gi.month-1]+' '+gi.day;
|
||||
ctx.fillStyle=txt; ctx.font=`${Math.round(16*s)}px -apple-system,Helvetica,Arial`;
|
||||
ctx.textBaseline='middle';
|
||||
ctx.textAlign='center'; ctx.fillText(dow,177.5*s,yc);
|
||||
ctx.fillText(md,297.5*s,yc);
|
||||
ctx.textAlign='right'; ctx.fillText(gi.hour>=12?'PM':'AM',465*s,yc);
|
||||
}
|
||||
function drawTime(ctx,cx,s,theme,gi){
|
||||
let hh=gi.hour; if(hh===0)hh=12; else if(hh>12)hh=hh-12;
|
||||
const hStr=''+hh, mStr=('0'+gi.min).slice(-2), ty=220*s;
|
||||
ctx.font=`700 ${Math.round(66*s)}px -apple-system,Helvetica,Arial`;
|
||||
ctx.textBaseline='middle';
|
||||
ctx.fillStyle=toHex(theme.hours); ctx.textAlign='right'; ctx.fillText(hStr,211*s,ty);
|
||||
ctx.fillStyle=toHex(theme.minutes); ctx.textAlign='left'; ctx.fillText(mStr,272*s,ty);
|
||||
ctx.fillStyle=toHex(theme.colon);
|
||||
ctx.fillRect(238*s,184*s,24*s,20*s);
|
||||
ctx.fillRect(238*s,235*s,24*s,20*s);
|
||||
}
|
||||
function drawBottom(ctx,cx,s,theme,dist,steps){
|
||||
const valY=366*s, labY=410.5*s, leftX=234*s, rightX=266*s;
|
||||
ctx.textBaseline='middle';
|
||||
ctx.fillStyle=toHex(theme.textPrimary); ctx.font=`${Math.round(22*s)}px -apple-system,Helvetica,Arial`;
|
||||
ctx.textAlign='right'; ctx.fillText(dist,leftX,valY);
|
||||
ctx.fillStyle=toHex(theme.accent); ctx.font=`${Math.round(16*s)}px -apple-system,Helvetica,Arial`;
|
||||
ctx.fillText('DST',leftX,labY);
|
||||
ctx.fillStyle=toHex(theme.textPrimary); ctx.font=`${Math.round(22*s)}px -apple-system,Helvetica,Arial`;
|
||||
ctx.textAlign='left'; ctx.fillText(steps,rightX,valY);
|
||||
ctx.fillStyle=toHex(theme.accent);
|
||||
ctx.fillText('STEP',rightX,labY);
|
||||
}
|
||||
|
||||
// ---- 主渲染 ----
|
||||
function renderWatch(ctx, W, themeId, data, round){
|
||||
const cx=W/2, cy=W/2, s=W/500;
|
||||
ctx.clearRect(0,0,W,W);
|
||||
ctx.fillStyle='#000'; ctx.fillRect(0,0,W,W);
|
||||
if(round){ ctx.save(); ctx.beginPath(); ctx.arc(cx,cy,W/2-2,0,7); ctx.clip(); }
|
||||
const theme=THEMES[themeId];
|
||||
const p=[data.m[0],data.m[1],data.m[2],data.m[3],data.m[4]];
|
||||
const idx={TR:0,TL:1,BR:4,BL:3}; // 默认设置
|
||||
drawRing(ctx,cx,cy,s,theme, p[idx.TR],p[idx.TL],p[idx.BR],p[idx.BL]);
|
||||
drawBattery(ctx,cx,s,theme,data.battery);
|
||||
drawTopComplication(ctx,cx,cy,s,theme,1,true, Math.round(data.m[1]*2000).toLocaleString()); // 左上=卡路里
|
||||
drawTopComplication(ctx,cx,cy,s,theme,0,false, data.temp+'°'); // 右上=天气温度
|
||||
const gi=data.time;
|
||||
drawTime(ctx,cx,s,theme,gi);
|
||||
drawDateBand(ctx,W,cx,s,theme,gi);
|
||||
drawBottom(ctx,cx,s,theme, (data.m[2]*10).toFixed(1), Math.round(data.m[0]*10000).toLocaleString());
|
||||
if(round) ctx.restore();
|
||||
}
|
||||
|
||||
// ---- 状态 ----
|
||||
const state={ themeId:'01', round:true, live:true,
|
||||
m:[0.84,0.64,0.65,0.80,0.40], battery:78, temp:23 };
|
||||
function nowGI(){const d=new Date();return {hour:d.getHours(),min:d.getMinutes(),day:d.getDate(),month:d.getMonth()+1,day_of_week:(d.getDay()+1)%7||7};}
|
||||
|
||||
const face=document.getElementById('face'), fctx=face.getContext('2d');
|
||||
function drawMain(){
|
||||
const gi=state.live?nowGI():{hour:10,min:8,day:9,month:9,day_of_week:3};
|
||||
renderWatch(fctx,500,state.themeId,{m:state.m,battery:state.battery,temp:state.temp,time:gi},state.round);
|
||||
}
|
||||
|
||||
// 主题按钮
|
||||
const themesBox=document.getElementById('themes');
|
||||
ORDER.forEach((id,i)=>{
|
||||
const b=document.createElement('button'); b.textContent=(i+1)+' '+THEMES[id].name;
|
||||
b.onclick=()=>{state.themeId=id; [...themesBox.children].forEach(c=>c.classList.remove('active')); b.classList.add('active'); drawMain(); drawGallery();};
|
||||
themesBox.appendChild(b);
|
||||
});
|
||||
themesBox.children[0].classList.add('active');
|
||||
|
||||
// 滑块
|
||||
[0,1,2,3,4].forEach(i=>{
|
||||
const el=document.getElementById('m'+i), v=document.getElementById('v'+i);
|
||||
const upd=()=>{state.m[i]=el.value/100; v.textContent=el.value+'%'; drawMain();};
|
||||
el.oninput=upd; v.textContent=el.value+'%';
|
||||
});
|
||||
const bat=document.getElementById('bat'), vbat=document.getElementById('vbat');
|
||||
bat.oninput=()=>{state.battery=+bat.value; vbat.textContent=bat.value+'%'; drawMain();}; vbat.textContent=bat.value+'%';
|
||||
const temp=document.getElementById('temp'), vtemp=document.getElementById('vtemp');
|
||||
temp.oninput=()=>{state.temp=+temp.value; vtemp.textContent=temp.value+'°'; drawMain();}; vtemp.textContent=temp.value+'°';
|
||||
document.getElementById('round').onchange=e=>{state.round=e.target.checked; drawMain();};
|
||||
document.getElementById('live').onchange=e=>{state.live=e.target.checked; drawMain();};
|
||||
|
||||
// 画廊
|
||||
const gal=document.getElementById('gallery');
|
||||
ORDER.forEach(id=>{
|
||||
const fig=document.createElement('figure');
|
||||
const c=document.createElement('canvas'); c.width=150; c.height=150;
|
||||
const cap=document.createElement('figcaption'); cap.textContent=(ORDER.indexOf(id)+1)+' · '+THEMES[id].name;
|
||||
fig.appendChild(c); fig.appendChild(cap); gal.appendChild(fig);
|
||||
});
|
||||
function drawGallery(){
|
||||
[...gal.children].forEach((fig,i)=>{
|
||||
const c=fig.querySelector('canvas'), x=c.getContext('2d');
|
||||
renderWatch(x,150,ORDER[i],{m:state.m,battery:state.battery,temp:state.temp,time:nowGI()},true);
|
||||
});
|
||||
}
|
||||
|
||||
drawMain(); drawGallery();
|
||||
setInterval(()=>{ if(state.live) drawMain(); drawGallery(); }, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
resources/drawables/drawables.xml
Normal file
3
resources/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||
</drawables>
|
||||
BIN
resources/drawables/launcher_icon.png
Normal file
BIN
resources/drawables/launcher_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 498 B |
12
resources/settings/properties.xml
Normal file
12
resources/settings/properties.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<properties>
|
||||
<property id="Theme" type="number">1</property>
|
||||
<property id="RingTR" type="number">0</property>
|
||||
<property id="RingTL" type="number">1</property>
|
||||
<property id="RingBR" type="number">4</property>
|
||||
<property id="RingBL" type="number">3</property>
|
||||
<property id="LeftTop" type="number">1</property>
|
||||
<property id="RightTop" type="number">0</property>
|
||||
<property id="ShowTop" type="boolean">true</property>
|
||||
<property id="ShowBand" type="boolean">true</property>
|
||||
<property id="ShowBottom" type="boolean">true</property>
|
||||
</properties>
|
||||
87
resources/settings/settings.xml
Normal file
87
resources/settings/settings.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<settings>
|
||||
<setting propertyKey="@Properties.Theme" title="@Strings.SettingTheme">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="1">@Strings.ThemeEmber</listEntry>
|
||||
<listEntry value="2">@Strings.ThemeAurora</listEntry>
|
||||
<listEntry value="3">@Strings.ThemeBrass</listEntry>
|
||||
<listEntry value="4">@Strings.ThemeVoltage</listEntry>
|
||||
<listEntry value="5">@Strings.ThemeReef</listEntry>
|
||||
<listEntry value="6">@Strings.ThemeAcid</listEntry>
|
||||
<listEntry value="7">@Strings.ThemeKelp</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.RingTR" title="@Strings.SettingRingTR">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.MetricSteps</listEntry>
|
||||
<listEntry value="1">@Strings.MetricCalories</listEntry>
|
||||
<listEntry value="2">@Strings.MetricDistance</listEntry>
|
||||
<listEntry value="3">@Strings.MetricFloors</listEntry>
|
||||
<listEntry value="4">@Strings.MetricMoveBar</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.RingTL" title="@Strings.SettingRingTL">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.MetricSteps</listEntry>
|
||||
<listEntry value="1">@Strings.MetricCalories</listEntry>
|
||||
<listEntry value="2">@Strings.MetricDistance</listEntry>
|
||||
<listEntry value="3">@Strings.MetricFloors</listEntry>
|
||||
<listEntry value="4">@Strings.MetricMoveBar</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.RingBR" title="@Strings.SettingRingBR">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.MetricSteps</listEntry>
|
||||
<listEntry value="1">@Strings.MetricCalories</listEntry>
|
||||
<listEntry value="2">@Strings.MetricDistance</listEntry>
|
||||
<listEntry value="3">@Strings.MetricFloors</listEntry>
|
||||
<listEntry value="4">@Strings.MetricMoveBar</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.RingBL" title="@Strings.SettingRingBL">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.MetricSteps</listEntry>
|
||||
<listEntry value="1">@Strings.MetricCalories</listEntry>
|
||||
<listEntry value="2">@Strings.MetricDistance</listEntry>
|
||||
<listEntry value="3">@Strings.MetricFloors</listEntry>
|
||||
<listEntry value="4">@Strings.MetricMoveBar</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.LeftTop" title="@Strings.SettingLeftTop">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.FieldWeather</listEntry>
|
||||
<listEntry value="1">@Strings.FieldCalories</listEntry>
|
||||
<listEntry value="2">@Strings.FieldSteps</listEntry>
|
||||
<listEntry value="3">@Strings.FieldDistance</listEntry>
|
||||
<listEntry value="4">@Strings.FieldFloors</listEntry>
|
||||
<listEntry value="5">@Strings.FieldBattery</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.RightTop" title="@Strings.SettingRightTop">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.FieldWeather</listEntry>
|
||||
<listEntry value="1">@Strings.FieldCalories</listEntry>
|
||||
<listEntry value="2">@Strings.FieldSteps</listEntry>
|
||||
<listEntry value="3">@Strings.FieldDistance</listEntry>
|
||||
<listEntry value="4">@Strings.FieldFloors</listEntry>
|
||||
<listEntry value="5">@Strings.FieldBattery</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.ShowTop" title="@Strings.SettingShowTop">
|
||||
<settingConfig type="boolean"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.ShowBand" title="@Strings.SettingShowBand">
|
||||
<settingConfig type="boolean"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.ShowBottom" title="@Strings.SettingShowBottom">
|
||||
<settingConfig type="boolean"/>
|
||||
</setting>
|
||||
</settings>
|
||||
35
resources/strings/strings.xml
Normal file
35
resources/strings/strings.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<strings>
|
||||
<string id="AppName">Fenix 8 V3</string>
|
||||
|
||||
<string id="SettingTheme">Theme</string>
|
||||
<string id="SettingRingTR">Ring · Top-Right</string>
|
||||
<string id="SettingRingTL">Ring · Top-Left</string>
|
||||
<string id="SettingRingBR">Ring · Bottom-Right</string>
|
||||
<string id="SettingRingBL">Ring · Bottom-Left</string>
|
||||
<string id="SettingLeftTop">Top-Left Complication</string>
|
||||
<string id="SettingRightTop">Top-Right Complication</string>
|
||||
<string id="SettingShowTop">Show Top Row</string>
|
||||
<string id="SettingShowBand">Show Date Band</string>
|
||||
<string id="SettingShowBottom">Show Bottom Row</string>
|
||||
|
||||
<string id="ThemeEmber">1 · Ember</string>
|
||||
<string id="ThemeAurora">2 · Aurora</string>
|
||||
<string id="ThemeBrass">3 · Brass</string>
|
||||
<string id="ThemeVoltage">4 · Voltage</string>
|
||||
<string id="ThemeReef">5 · Reef</string>
|
||||
<string id="ThemeAcid">6 · Acid</string>
|
||||
<string id="ThemeKelp">7 · Kelp</string>
|
||||
|
||||
<string id="MetricSteps">Steps</string>
|
||||
<string id="MetricCalories">Calories</string>
|
||||
<string id="MetricDistance">Distance</string>
|
||||
<string id="MetricFloors">Floors</string>
|
||||
<string id="MetricMoveBar">Move Bar</string>
|
||||
|
||||
<string id="FieldWeather">Weather</string>
|
||||
<string id="FieldCalories">Calories</string>
|
||||
<string id="FieldSteps">Steps</string>
|
||||
<string id="FieldDistance">Distance</string>
|
||||
<string id="FieldFloors">Floors</string>
|
||||
<string id="FieldBattery">Battery</string>
|
||||
</strings>
|
||||
28
source/Fenix8V3App.mc
Normal file
28
source/Fenix8V3App.mc
Normal file
@@ -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;
|
||||
}
|
||||
387
source/Fenix8V3View.mc
Normal file
387
source/Fenix8V3View.mc
Normal file
@@ -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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
source/Themes.mc
Normal file
110
source/Themes.mc
Normal file
@@ -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"
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user