# Orisys Studio 配色规范 本文件记录客户端的品牌配色体系、它与 UI 设计稿的关系,以及每一处有意偏离设计稿 的理由。所有色值都存放在主题令牌文件里,控件代码中**不允许出现十六进制颜色**: ``` orisys-app/shared/src/orisys_app_shared/theme/colors_dark.json orisys-app/shared/src/orisys_app_shared/theme/colors_light.json ``` 规则由 `apps/studio/tools/ui_check.py` 的 `audit_palette()` 机械校验,pytest 套件 (`tests/test_ui_smoke.py::test_palette_rules_hold`)复用它,因此只有一个真值来源。 --- ## 1. 设计稿色号 → 令牌映射 | 设计稿 | 角色 | 落地令牌 | |---|---|---| | `#003893` | 品牌主色 / 底图色 | `accent`、`plot_fn`(浅色);`arrow_canvas` 使用了更深的 logo 藏青 | | `#4D62BE` | 交互蓝 | `accent_hover`(浅色)、`accent`(深色)、`focus_ring`(浅色) | | `#12315D` | logo 实际藏青(见 §3.1) | `brand_bar_bg`、`arrow_canvas`(浅色) | | `#FA02F3` | 洋红 | `flow_ramp` 第 3 档(原值保留) | | `#FE5853` | 珊瑚 | `flow_ramp` 第 4 档(提亮为 `#FF8A6B`,见 §3.2);`danger` 家族取自同一色相 | | `#D8FF00` | 柠檬绿 | `flow_ramp` 第 5 档(原值保留)、`plot_fy`(深色) | | `#D5D5D5` | 应用画布 | **未采用**,改为 `#E1E6EF`(见 §3.3) | ### 1.1 三轴分类色(蓝 / 橙 / 绿) 界面改版把三个力通道统一成"蓝 = 法向、橙 = 切向 X、绿 = 切向 Y"。它们是 **分类色**(三条曲线各自一个身份),与 §4 的箭头色带(顺序色带)是两件事,因此落在 `plot_fn` / `plot_fx` / `plot_fy` 三个令牌上。这三个令牌同时被实时曲线、录制回放、 标定面板与标定点详情对话框引用,换值即全局统一。 | 主题 | `plot_fn`(法向,蓝) | `plot_fx`(切向 X,橙) | `plot_fy`(切向 Y,绿) | |---|---|---|---| | 深色 | `#6FA8FF` | `#FF9A4D` | `#7CE0A0` | | 浅色 | `#003893`(品牌藏青,见 §3.1) | `#B45309` | `#2E7D32` | 浅色主题的 FN 保持品牌藏青 `#003893`(与 `accent` 同值),这样品牌蓝在三轴里仍是 "主通道";橙色与绿色在浅底上取深色相,才够对比度。深色主题的霓虹洋红/柠檬绿被替换, 原因是它们是**色带**用色:作为分类色时洋红与橙的色相差偏小,而柠檬绿在浅底上不可用 (§2.1)。替换后三个通道在灰度打印下也靠亮度分开,不再依赖单一色相。 --- ## 2. 实测对比度(WCAG) 对比度用标准相对亮度公式实算(`tools/ui_check.py` 中的 `contrast_ratio`)。 正文门槛 4.5:1,图形/控件/状态指示门槛 3.0:1。 ### 2.1 设计稿本身的实测结果 | 组合 | 比值 | 判定 | |---|---|---| | 白字 on `#003893` | 10.57 | AAA ✅ | | 白字 on `#4D62BE` | 5.52 | AA ✅ | | `#4D62BE` on `#D5D5D5` | 3.76 | 图形达标,12–13px 文字**不达标** ⚠️ | | `#D5D5D5` vs 白卡 | 1.47 | 卡片层次偏弱 ⚠️ | | `#D8FF00` on `#D5D5D5` | 1.27 | 不可用 ❌ | | `#D8FF00` on `#003893` | 9.17 | 极佳 ✅ | | `#FA02F3` on `#003893` | 3.20 | 达标 ✅ | | `#FE5853` on `#003893` | 3.39 | 达标 ✅ | | **`#FA02F3` vs `#FE5853`** | **1.06** | **色带中段的亮度平台 ❌** | | 白字 on `#FE5853` | 3.12 | 珊瑚色不能承载白字 ❌ | 设计稿的色相选择是可靠的;三个霓虹色的**相对亮度**为 0.268 → 0.287 → 0.861, 色相沿 302° → 2° → 69° 单调前进 127°,作为"位移幅值 → 颜色"的顺序色带是正确用法。 问题在于中段:洋红与珊瑚亮度几乎相同(1.06:1),只靠 60° 色相差区分,在灰度 打印与色觉障碍下会糊成一片。 ### 2.2 落地后 浅色主题(节选):白字 on `accent` 10.57,白字 on `accent_hover` 5.52, `plot_fn/fx/fy` 对 `plot_bg` 分别 10.11 / 4.80 / 4.90, `brand_bar_fg` on `brand_bar_bg` 12.94,`input_border` 对四种底均 ≥3.08。 深色主题(节选):白字 on `accent` 5.52,`accent` 对 `panel_bg` 3.09(贴线但仍达标), `plot_fn/fx/fy` 对 `plot_bg` 分别 7.53 / 8.62 / 11.25,`focus_ring` 对 `input_bg` 6.51。 语义色(`danger` / `warning` / `success` / `status_*`)额外对 `toolbar_bg` 审计:它们会 出现在顶栏状态行上(记录状态、数据有效性、阶段指示),实测浅色 4.65–6.66、深色 5.60–9.13, 全部 ≥4.5。在这一步之前它们只在 `panel_bg` / `card_bg` 上被检查,顶栏是一个未经审计的底色。 三通道可分辨性(色相差 / 相对亮度比): | 主题 | FN vs FX | FN vs FY | FX vs FY | |---|---|---|---| | 浅色 | 168.8° / 2.10 | 94.1° / 2.06 | 97.1° / 1.02 | | 深色 | 169.7° / 1.14 | 74.7° / 1.49 | 115.6° / 1.31 | 规则是"色相差 ≥90° **或** 相对亮度比 ≥1.2",两主题均满足(深色 FN vs FY 靠 1.49 的亮度比 过关,浅色 FX vs FY 靠 97.1° 的色相差过关)。颜色在任何情况下都不是唯一编码:每个曲线有 文字标签,每张数值卡有同色通道徽标与通道名。 --- ## 3. 对设计稿的四处有意偏离 ### 3.1 品牌藏青用 logo 实际取色 `#12315D` `logo.png` 与 `logo_topbar.png` 采样后字形的实际颜色都是 `#12315D` (H215 S80.6% V36.5%),而设计稿写的是 `#003893`(H217 但 S100% V57.6%)。 两者同色相家族,但后者明显更亮更艳。顶栏品牌条与浅色主题的箭头画布采用 logo 实际色,以保证"标识色"与"界面色"一致。 ### 3.2 色带第 4 档由 `#FE5853` 提亮为 `#FF8A6B` 见 §2.1:保留设计稿珊瑚色会在色带中段留下 1.06:1 的亮度平台。提亮后色带相对亮度 为 **0.031 → 0.128 → 0.268 → 0.405 → 0.861**,严格单调。 第 3 档洋红与第 5 档柠檬绿**保留设计稿原值不动**。 ### 3.3 画布 `#D5D5D5` → `#E1E6EF` `#D5D5D5` 是无彩色(H=0 S=0% L=84%)。现代浅色画布普遍在 96–98% 亮度,目的是让 白卡"浮起来";而在 `#D5D5D5` 上白卡只有 1.47:1,且 Qt 的 QSS 不支持 `box-shadow`, 卡片只能靠 1px 描边分离,观感会发平、发旧。改为品牌微蓝灰后: - 保留设计稿"灰底 + 白卡"的角色分工; - `#4D62BE` 在画布上的对比度从 3.76 升到 4.41。 ### 3.4 补齐语义色与深色主题 设计稿没有 success/warning/error/info,也只有浅色版;本应用默认主题是深色。 同时原深色主题的语义色是一整套 Google Material 色 (`#8ab4f8` / `#34a853` / `#fbbc04` / `#ea4335` / `#1a73e8`),对一个测量仪器 产品属于即时可识别的品牌冲突,已全部替换为品牌衍生色。 `danger` 在深色主题被拆成两个令牌:`danger`(亮,用于卡片上的红字与状态指示) 与 `danger_fill`(暗,用于承载白字的危险按钮)—— 一个色值无法同时满足两个方向。 --- ## 4. 品牌化箭头色带 设计稿的霓虹三色是**一张 colormap**(按位移幅值着色),不是三个分类色。因此它落在 箭头可视化上,而不是 UI 控件上: ``` flow_ramp = 画布色, #6E4AD0, #FA02F3, #FF8A6B, #D8FF00 ``` - 第 1 档等于画布色,这样**零位移区域自然融入背景**; - 由 `orisys_app_shared.acquisition.arrows.flow_colormap(theme)` 生成 256 档 BGR 查找表,经 `draw_arrows(colormap=...)` 注入 SDK(**SDK 无需改动**); - LUT 缓存的键是 ramp 字符串本身,因此切换主题后立刻生效; - 非法 ramp 永不抛异常,回退到内置默认色带。 箭头画布在两个主题里都是品牌深蓝(浅 `#12315D` / 深 `#0B1830`),这也是霓虹色带 唯一可用的背景 —— 色带第 3 档起对画布均 ≥3.0:1。**绘图区(`plot_bg`)保持浅色**, 所以浅色主题下霓虹只出现在箭头画布上,力曲线使用浅色安全的品牌三色。 --- ## 5. 有记录在案的豁免 以下项目**不**被 `audit_palette()` 检查,理由是它们属于装饰而非状态指示: | 项 | 现状 | 理由 | |---|---|---| | 装饰性 1px 容器描边 | `card_border` / `panel_border` / `plot_grid` / `separator`(1.16–1.45:1) | WCAG 1.4.11 针对交互控件的边界与状态指示;容器轮廓属于装饰 | | 禁用态文字 | 深色 `text_muted` on `button_bg` = 4.07 | WCAG 1.4.3 明确豁免禁用控件 | 输入框的静止描边**不在豁免之列** —— 它被强制要求对 `input_bg`/`panel_bg`/`card_bg`/`toolbar_bg` 四种底均 ≥3.0:1(实测 3.08–4.09),这与 Material 3 描边式输入框的做法一致。焦点态同样由 `focus_ring` 保证 ≥3.0:1。 深色主题的 `accent` 对 `panel_bg` 为 3.09:1(设计稿原值 `#4D62BE`),贴线但达标; 若后续视觉上偏软,可单点提升为 `#5468C6`(白字仍为 5.03)。 --- ## 6. 顶栏:一件仪器只有一条读数线 顶部由两条细轨组成,合计在 1440×880 下不超过 78 px(`ui_check.py` 的 `top area` 段与 `tests/test_ui_smoke.py::test_top_area_stays_compact` 都断言这一点): | 轨 | 高度 | 承载 | |---|---|---| | 品牌轨(`QFrame#brandBar`) | 28 px | logo + 字标 + 三个顶层视图(实时监测 / 历史回放 / 标定)+ 右侧产品名 | | 状态轨(`ToolbarWidget` 的 rail) | ~30 px | **传感器 / 传感器配置**、阶段、采集来源、标定状态、数据有效性、记录状态、记录/运行/重置/设置 | | 设置区(默认收起) | 0 / ~40 px | 来源、摄像机编号、视频文件、单位、**记录模式** | 三点规则: 1. **传感器选择在状态轨最左侧,且两个下拉都带标签**。`box / mini / finger` 是产品 (传感器)类型,它决定后面可选的配置文件;原来只有一个「配置」下拉,看不出在切什么。 现在写成「传感器」+「传感器配置」两个带 tooltip 的下拉,产品型号另有一个徽标 (`FINGER` 不得被截断成 `INGE`,所以徽标只有最小宽度)。 2. **导航按钮在品牌底上取品牌前景色**。`navButton` 用 `brand_bar_fg_muted`,当前视图用 `brand_bar_fg` + `navActive="true"`;两者都在 §2 的品牌底对比度审计里(7.67 / 12.94), 不新增色值。 3. **状态轨上的语义色一律算在 `toolbar_bg` 上审计**。记录状态(`status_idle` / `status_running` / `status_connecting` / `status_error`)、丢帧提示(`warning`)与数据 有效性(`success` / `warning`)此前只检查 `panel_bg`,而它们实际画在顶栏底色上。 4. **同一时刻只有一个实心按钮**(`ToolbarWidget._refresh_emphasis`): | 状态 | 实心 | 描边 | |---|---|---| | 空闲 | `运行`(`primaryButton`) | — | | 连接中 | `取消连接`(`dangerButton`) | — | | 采集中 | `开始记录`(`primaryButton`) | `停止`(`dangerOutline`) | | 记录中 | `停止记录`(`dangerButton`) | `停止`(`dangerOutline`) | `dangerOutline` 是新增样式:透明底 + `danger` 文字与描边,hover/pressed 才填充 `danger_fill`;`:checked`(即"采集正在跑")仍保持描边——否则一圈红边会在会话开始后 悄悄变成整块红色,那正是这次要修掉的观感。 原反馈里"提高停止按钮优先级"的诉求没有丢:停止始终是红色、始终一键可达,只是不再 与当前主操作争夺同一时刻的唯一实心位置。 5. **状态轨上的事实不得被压缩**(`ui_check.rail_squeezed`)。中文、英文、1280 与 1440 四种组合下,轨上的按钮与下拉都必须显示完整文本;只有带 tooltip 的 `ShrinkableLabel` 允许在窗口极窄时被裁切。这也是"记录模式"下拉被挪进设置区的原因:英文的 `Start recording` + `Sensor config` 与它挤在同一行时,第一个被压缩的就是按钮文字。 --- ## 7. 三个顶层视图 = 同一窗口的三套布局 导航不再打开对话框,而是切换主窗口的布局(`StudioMainWindow._apply_view_preset`): | 视图 | 面板 | 曲线模式 | 分割比例(3D, 2D, 曲线, 标定) | |---|---|---|---| | 实时监测 | 2D 图像 + 力曲线(+ 手指三维) | 跟随实时 | `0, 360, 1080, 0` | | 历史回放 | 2D 图像 + 力曲线 | 进入全程浏览 | `0, 320, 1120, 0` | | 标定 | 标定面板 + 力曲线 | 不变 | `0, 0, 560, 880` | | 自由(`custom`,启动默认) | 由视图菜单决定 | 不变 | 用户在分割条上摆的位置 | - 标定视图给标定面板 880 px:六列诊断表头与九个动作按钮不再被挤压;标定面板自身还有 纵向滚动区,窗口再矮也够得到「应用到设备」。 - 标定面板列宽改为 `ResizeToContents`:诊断列头宁可让表格横向滚动,也不截成 「FN 均」这种看不懂的缩写。 - 2D 图像区域固定为所显示图像的宽高比(`AspectImageLabel`)。横向分割器**不会**向子控件 询问 `heightForWidth`,所以宽高比由 `ArrowPanel.resizeEvent` 主动施加;box 传感器的 画面旋转 90° 显示,因此其显示宽高比是 9:16 而不是 16:9。 - `视图 → 标定面板` 与「标定」导航是同一个开关:勾上进入标定视图,取消回到实时视图, 不会留下"面板已隐藏但导航还亮着"的矛盾状态。 --- ## 8. 复核方式 ```powershell $py = "D:\orisys_ws\orisys-app\.venv-build\Scripts\python.exe" $env:PYTHONPATH = "D:\orisys_ws\orisys-app\shared\src;D:\orisys_ws\orisys-app\apps\studio\src" cd D:\orisys_ws\orisys-app\apps\studio & $py tools/ui_check.py # 含 palette 规则段落 & $py tools/ui_snapshot.py --theme dark --lang zh_CN --demo --out outputs/_snap_dark.png & $py tools/ui_snapshot.py --theme light --lang en_US --sensor finger --panels cal,3d --size 1600x900 --demo --out outputs/_snap_light.png & $py tools/ui_snapshot.py --theme dark --lang zh_CN --demo --scene recording --out outputs/_snap_recording.png & $py tools/ui_snapshot.py --theme dark --lang zh_CN --demo --preset calibration --out outputs/_snap_calibration.png ``` 视图切换与关闭流程另见 `apps/studio/docs/shell-lifecycle.md`。