feat(design): M70 r1 —— 补第 4 档「天蓝」候选 #3C8DBC(四档对比)
③ 的 primary `#0C6780` 偏青(teal),不是「天蓝」。新增 ④ 天蓝:种子 `#3C8DBC` (CAM16 hue 239.5°,夹 ③ 与 ①② 之间)+ 中性彩度 12 → primary `#236488`。 - `tests/skyseed_candidates.rs`:候选表 3 → 4 档,④ 的 9 角色 × light/dark 全部进 pin; `the_three_tiers_are_actually_distinguishable` 改为 `the_candidates_are_actually_distinguishable`, 增加「③ 与 ④ 中性彩度相同、主色/容器必须靠色相区分」的断言 - `tests/contrast_aa.rs`:候选 WCAG 门禁 3 → 4 档(26 × 4 × 2 = 208 项) - 默认值**不动**:仍为 ③ `#87CEEB`(临时默认,待用户看完四档再拍); `product_default_is_the_recommended_candidate` 保持原样,专门在默认与推荐不一致时报警 - 文档:`color/mod.rs` 与 `palettes.rs` 的候选清单补上 ④ 与「改一行即可切 ④」的说明
This commit is contained in:
1 parent
79404fa9d1
commit
9519a89472
4 files changed
+67
-27
No files matched your search
@@ -68,10 +68,11 @@
|
||||
//!
|
||||
//! 官方口径的锚点仍然完整保留:[`Scheme::official_baseline`](种子 `#6750A4` + 中性 6)
|
||||
//! 是 `tests/official_baseline.rs` 的对账入口,98 项仍是每通道 ≤1;
|
||||
//! `tests/skyseed_candidates.rs` 把三档候选(保守蓝 / 冷调蓝 / 浅冷蓝)的色值逐个 pin 住,
|
||||
//! `tests/skyseed_candidates.rs` 把四档候选(保守蓝 / 冷调蓝 / 浅冷蓝 / 天蓝)的色值逐个 pin 住,
|
||||
//! 换默认时它会指着要改的行。
|
||||
//!
|
||||
//! ⚠️ 当前默认色是「待用户确认」的**临时默认**(推荐档③ 浅冷蓝)。
|
||||
//! ⚠️ 当前默认色是「待用户确认」的**临时默认**(③ 浅冷蓝 `#87CEEB`)。用户看过四档色卡后若选
|
||||
//! ④ 天蓝(`#3C8DBC`,主色 `#236488`,比 ③ 更偏蓝),只需改 [`palettes::DEFAULT_SEED`] 一行。
|
||||
//!
|
||||
//! ## 对比度纪律(WCAG AA)
|
||||
//!
|
||||
|
||||
@@ -35,6 +35,9 @@ pub const BASELINE_SEED: Color = Color::rgb(0x67, 0x50, 0xA4);
|
||||
///
|
||||
/// `#87CEEB` 就是 CSS 的 `skyblue`。注意它的彩度(36)会被 Tonal Spot 丢掉——
|
||||
/// 主色板的彩度恒为 36,种子只贡献**色相**(这里 CAM16 hue ≈ 226°,偏 cyanish 的蓝)。
|
||||
///
|
||||
/// 第 4 档 ④「天蓝」`#3C8DBC`(CAM16 hue 239.5°,主色 `#236488`)在
|
||||
/// `tests/skyseed_candidates.rs` 里有完整 pin 与 WCAG 覆盖;用户若选 ④,改这一行常量的值即可。
|
||||
pub const DEFAULT_SEED: Color = Color::rgb(0x87, 0xCE, 0xEB);
|
||||
|
||||
/// 产品出厂默认的中性调色板彩度(官方 Tonal Spot 是 6,这里 12 = **有意偏离**)。
|
||||
|
||||
@@ -159,13 +159,14 @@ fn baseline_light_and_dark_both_pass() {
|
||||
|
||||
#[test]
|
||||
fn sky_seed_candidates_meet_wcag_aa() {
|
||||
// M70:三档默认色候选(种子 + 中性彩度)必须过同一条 26 组门禁。
|
||||
// M70:四档默认色候选(种子 + 中性彩度)必须过同一条 26 组门禁。
|
||||
// 中性彩度改变只影响色相/彩度,不影响 tone 轴,所以理论上恒过——这里就是要**证明**它恒过,
|
||||
// 而不是相信它。任何一档不达标,该档直接判为不可用(不许为了好看放宽阈值)。
|
||||
let candidates = [
|
||||
("① 保守蓝", "#2196F3", 6.0_f64),
|
||||
("② 冷调蓝", "#2196F3", 12.0),
|
||||
("③ 浅冷蓝", "#87CEEB", 12.0),
|
||||
("④ 天蓝", "#3C8DBC", 12.0),
|
||||
];
|
||||
let mut failures = Vec::new();
|
||||
let mut worst = (f32::MAX, String::new());
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
//! M70:默认色候选(保守蓝 / 冷调蓝 / 浅冷蓝)——三档的**唯一真源**与 pin。
|
||||
//! M70:默认色候选(保守蓝 / 冷调蓝 / 浅冷蓝 / 天蓝)——四档的**唯一真源**与 pin。
|
||||
//!
|
||||
//! ## 这档候选到底改了什么
|
||||
//!
|
||||
//! 三档走的是**同一条** HCT 管道(`Palettes::from_seed_with_neutral_chroma` → `ColorSpec2021`
|
||||
//! 四档走的是**同一条** HCT 管道(`Palettes::from_seed_with_neutral_chroma` → `ColorSpec2021`
|
||||
//! tone 表 → `HctSolver` gamut 映射),差别只有两个入参:
|
||||
//!
|
||||
//! | 档 | 种子 | 中性调色板彩度 | 表面 |
|
||||
//! |---|---|---|---|
|
||||
//! | ① 保守蓝 | `#2196F3` | 6(官方) | 近白/近黑,几乎无色 |
|
||||
//! | ② 冷调蓝 | `#2196F3` | 12 | 冷白/冷黑,带淡蓝调 |
|
||||
//! | ③ 浅冷蓝 | `#87CEEB`(CSS `skyblue`) | 12 | 浅天蓝 |
|
||||
//! | 档 | 种子 | 种子 HCT 色相 | 中性调色板彩度 | 表面 | 主色观感 |
|
||||
//! |---|---|---|---|---|---|
|
||||
//! | ① 保守蓝 | `#2196F3` | 253.7° | 6(官方) | 近白/近黑,几乎无色 | 钢蓝 |
|
||||
//! | ② 冷调蓝 | `#2196F3` | 253.7° | 12 | 冷白/冷黑,带淡蓝调 | 钢蓝 |
|
||||
//! | ③ 浅冷蓝 | `#87CEEB`(CSS `skyblue`) | 225.7° | 12 | 浅天蓝 | 偏青(teal) |
|
||||
//! | ④ 天蓝 | `#3C8DBC` | 239.5° | 12 | 浅天蓝 | 偏蓝的天蓝 |
|
||||
//!
|
||||
//! ③ 与 ④ 是同一件事的两种「蓝」:色相越低越偏青(teal),越高越偏钢蓝;
|
||||
//! 239.5° 正好夹在中间,主色从 ③ 的 `#0C6780` 变成 `#236488`。
|
||||
//!
|
||||
//! 两条必须知道的物理事实(写在测试里,免得下次又有人想「把主色调浅一点」):
|
||||
//!
|
||||
@@ -17,8 +21,8 @@
|
||||
//! 所以「低彩度天蓝种子」得到的色板与「高彩度天蓝种子」**逐像素相同**
|
||||
//! (`seed_chroma_never_leaks_into_the_primary_ramp` 在 `palettes.rs` 里也钉了这条)。
|
||||
//! ③ 与 ② 的真正差别是**色相**(226° vs 254°),不是彩度。
|
||||
//! 2. **primary 的 tone 被钉在 40**,所以三档主色都是「中蓝」,不会更浅。
|
||||
//! 「浅」只能落在中性色上——这就是②/③提高中性彩度的原因,也是本项目唯一一处
|
||||
//! 2. **primary 的 tone 被钉在 40**,所以四档主色都是「中蓝」,不会更浅。
|
||||
//! 「浅」只能落在中性色上——这就是②/③/④提高中性彩度的原因,也是本项目唯一一处
|
||||
//! 对官方 Tonal Spot 的**有意偏离**(登记在 `color` 模块文档的「出厂默认色」小节)。
|
||||
//!
|
||||
//! ## 换默认色要动哪里
|
||||
@@ -44,8 +48,8 @@ struct Candidate {
|
||||
pins: [(Role, &'static str, &'static str); 9],
|
||||
}
|
||||
|
||||
/// 三档候选(色卡 `DevDocs/ops/skyseed-candidates/` 用的就是这份数据)。
|
||||
const CANDIDATES: [Candidate; 3] = [
|
||||
/// 四档候选(色卡 `DevDocs/ops/skyseed-candidates/` 用的就是这份数据)。
|
||||
const CANDIDATES: [Candidate; 4] = [
|
||||
Candidate {
|
||||
tag: "C1",
|
||||
title: "① 保守蓝(中性彩度 6 = 官方)",
|
||||
@@ -97,9 +101,26 @@ const CANDIDATES: [Candidate; 3] = [
|
||||
(Role::Outline, "#687A82", "#81949C"),
|
||||
],
|
||||
},
|
||||
Candidate {
|
||||
tag: "C4",
|
||||
title: "④ 天蓝(中性彩度 12 + 夹在中间的天蓝种子)",
|
||||
seed_hex: "#3C8DBC",
|
||||
neutral_chroma: 12.0,
|
||||
pins: [
|
||||
(Role::Primary, "#236488", "#93CDF6"),
|
||||
(Role::OnPrimary, "#FFFFFF", "#00344C"),
|
||||
(Role::PrimaryContainer, "#C8E6FF", "#004C6D"),
|
||||
(Role::Secondary, "#4F616E", "#B7C9D8"),
|
||||
(Role::Tertiary, "#63597C", "#CDC0E9"),
|
||||
(Role::Surface, "#F6FAFF", "#09151C"),
|
||||
(Role::SurfaceContainerLow, "#EBF5FF", "#111D25"),
|
||||
(Role::SurfaceContainerHighest, "#D8E4EF", "#2B363F"),
|
||||
(Role::Outline, "#6A7985", "#84939F"),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/// 三档共同点:种子的 HCT 坐标(色相是候选之间**唯一**有效的差别)。
|
||||
/// 四档共同点:种子的 HCT 坐标(色相是候选之间**唯一**有效的差别)。
|
||||
fn seed_hct(candidate: &Candidate) -> Hct {
|
||||
Hct::from_color(Color::from_hex(candidate.seed_hex).expect("候选种子必须是合法 hex"))
|
||||
}
|
||||
@@ -146,7 +167,7 @@ fn candidates_match_their_pins() {
|
||||
|
||||
#[test]
|
||||
fn candidates_only_move_the_two_declared_inputs() {
|
||||
// 「不绕算法」的证据:三档的调色板彩度与官方 Tonal Spot 逐项一致,
|
||||
// 「不绕算法」的证据:四档的调色板彩度与官方 Tonal Spot 逐项一致,
|
||||
// 唯一的差别是中性彩度的入参(以及种子色相)。
|
||||
let official = Palettes::official_baseline();
|
||||
for candidate in &CANDIDATES {
|
||||
@@ -168,16 +189,17 @@ fn candidates_only_move_the_two_declared_inputs() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_three_tiers_are_actually_distinguishable() {
|
||||
// 「三档不像同一档」的机器判据。注意 light 的 surface(tone 98)在 8 bit 下已经贴顶,
|
||||
// 中性彩度 6→12 会量化到同一个 #F8F9FF——所以判据落在 container 系与 dark 模式上,
|
||||
fn the_candidates_are_actually_distinguishable() {
|
||||
// 「四档不像同一档」的机器判据。注意 light 的 surface(tone 98)在 8 bit 下已经贴顶,
|
||||
// 中性彩度 6→12 会量化到同一个 #F8F9FF——所以 ①② 的判据落在 container 系与 dark 模式上,
|
||||
// 这也是色卡上 ①② 的差别主要体现在卡片/浮层与深色模式的原因。
|
||||
let light = |c: &Candidate| scheme(c, SchemeFlavor::Light);
|
||||
let dark = |c: &Candidate| scheme(c, SchemeFlavor::Dark);
|
||||
let (c1, c2, c3) = (
|
||||
let (c1, c2, c3, c4) = (
|
||||
light(&CANDIDATES[0]),
|
||||
light(&CANDIDATES[1]),
|
||||
light(&CANDIDATES[2]),
|
||||
light(&CANDIDATES[3]),
|
||||
);
|
||||
for role in [Role::SurfaceContainerLow, Role::SurfaceContainerHighest] {
|
||||
assert_ne!(
|
||||
@@ -187,7 +209,14 @@ fn the_three_tiers_are_actually_distinguishable() {
|
||||
role.name()
|
||||
);
|
||||
assert_ne!(c2.get(role), c3.get(role), "②与③的 {} 相同", role.name());
|
||||
// ③④ 的中性彩度相同,差别只允许来自色相
|
||||
assert_ne!(c3.get(role), c4.get(role), "③与④的 {} 相同", role.name());
|
||||
}
|
||||
assert_ne!(
|
||||
c3.get(Role::Primary),
|
||||
c4.get(Role::Primary),
|
||||
"③与④的主色必须真的不同(否则第 4 档白加)"
|
||||
);
|
||||
assert_ne!(
|
||||
dark(&CANDIDATES[0]).get(Role::Surface),
|
||||
dark(&CANDIDATES[1]).get(Role::Surface),
|
||||
@@ -201,17 +230,23 @@ fn the_three_tiers_are_actually_distinguishable() {
|
||||
hue_gap(&CANDIDATES[0], &CANDIDATES[1]) < 0.01,
|
||||
"①② 应当只差中性彩度"
|
||||
);
|
||||
assert!(
|
||||
(10.0..=40.0).contains(&hue_gap(&CANDIDATES[1], &CANDIDATES[2])),
|
||||
"②③ 的色相差应当明显(10°~40°),实测 {}",
|
||||
hue_gap(&CANDIDATES[1], &CANDIDATES[2])
|
||||
);
|
||||
// ③ 偏青 / ④ 居中的分界靠色相:②→③ 与 ③→④ 都要肉眼可分的量级。
|
||||
for (i, j) in [(1, 2), (2, 3)] {
|
||||
assert!(
|
||||
(10.0..=40.0).contains(&hue_gap(&CANDIDATES[i], &CANDIDATES[j])),
|
||||
"{}→{} 的色相差应当明显(10°~40°),实测 {}",
|
||||
CANDIDATES[i].tag,
|
||||
CANDIDATES[j].tag,
|
||||
hue_gap(&CANDIDATES[i], &CANDIDATES[j])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn product_default_is_the_recommended_candidate() {
|
||||
// ③ 是 M70 的**临时默认**(推荐档,待用户确认)。用户拍板后改这里会立刻红——
|
||||
// 那条红就是在提醒你同步 `DEFAULT_SEED` / `DEFAULT_NEUTRAL_CHROMA` 与上面的 pin。
|
||||
// ③ 是 M70 的**临时默认**(待用户确认)。第 4 档 ④ 加进来之后默认**故意不动**——
|
||||
// 由用户看完四档再拍。拍板后改 `DEFAULT_SEED`/`DEFAULT_NEUTRAL_CHROMA` 这里会立刻红,
|
||||
// 那条红就是在提醒你同步 pin 表(本用例就是那个提醒器)。
|
||||
let recommended = &CANDIDATES[2];
|
||||
assert_eq!(
|
||||
DEFAULT_SEED,
|
||||
|
||||
Reference in new issue
Block a user