fix(design): M61 r1 收口 —— 官方 emphasized/弹簧/高程 + 冻结测试
reviewer r1 判 p2-2items(fix-then-merge),本轮按条目收口: P2-1 冻结测试补全(变异实测证明原来可静默漂移): - tests/type_scale_pins.rs:baseline 15 档 (size,line_height,weight,tracking,family) 与 Expressive 强调 15 档逐值对拍,期望值独立于实现重写 - tests/token_pins.rs:49 条 tone(light+dark 全表)、7 条 easing 曲线参数、 16 档时长、9 组弹簧、28 个 IconKind 下标、形状阶梯 - 自证:TitleMedium 16→17 与 EmphasizedDecelerate 换曲线两次变异均 FAILED P2-2 emphasized 口径事实错误(用户点名): - 取回 A 级官方值(androidx TypeScaleTokens v0_103 的 *Emphasized* 四元组): label-*/title-medium/title-small 强调是 700(此前给 600)、body-large 0.5→0.15、 title-medium 0.2→0.15、body-medium 0.2→0.25、display-large -0.2→0 - TypeRole::emphasized() 改为官方表(typography/emphasized.rs); 原「只加一档字重」规则改名 emphasized_simplified() 并标注非官方 - 顺带按官方订正 baseline tracking(display-large -0.2、body-medium 0.2、 title-medium 0.2)与 TitleLarge 的 Brand 族归属 - crate 文档新增「baseline MD3 vs M3 Expressive 差异清单」+ 未支持能力 gap 弹簧改为两套 token 结构(用户追加令): - motion::spring(scheme × category × speed) 二维表 12 格: standard spatial ζ=0.9 (k 1400/700/300)、expressive spatial ζ=0.6~0.8 (k 800/380/200)、 effects 两套同值 ζ=1.0 (k 3800/1600/800),均取 A 级 MotionTokens 原值 - Spring 改存阻尼比(与 Compose 令牌同口径),9 个具名常量 中文字重覆盖(用户追加令): - FontFace 带 WeightCoverage(可变区间/静态表),Platform::resolve_weight 给 Native/Nearest/Synthetic 三档结论;雅黑无 500、苹方无 700 两处 gap 显式登记 component_map 高程按官方回调(P2 并入 finding): - FilledCard L2→L0、OutlinedCard L1→L0、Menu L4→L2、Dialog L5→L3(Menu 圆角同时 按 MenuTokens 改 4dp);新增官方值 pin 测试 + R4「全屏都在飘」守门断言 P3 四条: - 同族容器撞色:彩度按共享基准等比压缩(Palettes.reference_chroma), primary/secondary-container 不再同值,且强调色覆盖时表面逐像素不变 - femtovg 引用改函数名(行号已漂);删掉 contrast_aa 的死代码后门 - 骨架数值跟 M63:nav 56→44、titlebar 40→48,control 40/touch 48 换算依据入文档 - 图标折算口径订正为「居中描边,视觉外扩 <1px」
This commit is contained in:
1 parent
65ca0ed29a
commit
83e19e58f0
20 files changed
+1367
-234
No files matched your search
@@ -57,6 +57,12 @@ pub struct Palettes {
|
||||
pub neutral_variant: TonalPalette,
|
||||
/// 错误调色板。
|
||||
pub error: TonalPalette,
|
||||
/// 彩度压缩的**共享基准**(取建方案时的 primary 彩度,强调色覆盖时**不变**)。
|
||||
///
|
||||
/// 为什么单独存一份:`from_accent` 换掉 primary 后,中性表面不该跟着变色调
|
||||
/// (否则「换强调色不动底色」这条语义就破了),而彩度压缩又必须用同一个基准才能
|
||||
/// 保证 primary/secondary 之间的相对灰度——两者靠这个字段同时成立。
|
||||
reference_chroma: f32,
|
||||
}
|
||||
|
||||
impl Palettes {
|
||||
@@ -77,6 +83,7 @@ impl Palettes {
|
||||
neutral: primary.with_chroma(NEUTRAL_CHROMA),
|
||||
neutral_variant: primary.with_chroma(NEUTRAL_VARIANT_CHROMA),
|
||||
error: TonalPalette::new(ERROR_HUE_DEG, ERROR_CHROMA),
|
||||
reference_chroma: primary.chroma(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +91,7 @@ impl Palettes {
|
||||
///
|
||||
/// 这是「用户自定义强调色」的正确语义——换强调色不该让整个界面底色变脏;
|
||||
/// 也因为 tone 轴固定,任何色相都能自动满足 WCAG AA(不需要再算一遍对比度)。
|
||||
/// `reference_chroma` 也保持原值,所以中性色与 baseline 逐像素一致(有测试钉住)。
|
||||
pub fn with_accent(self, accent: Color) -> Self {
|
||||
let accent_primary = TonalPalette::from_color(accent);
|
||||
Self {
|
||||
@@ -107,6 +115,18 @@ impl Palettes {
|
||||
PaletteSlot::Error => self.error,
|
||||
}
|
||||
}
|
||||
|
||||
/// 方案级取色:以 [`Palettes::reference_chroma`] 为**共享基准**压所有调色板(见
|
||||
/// [`TonalPalette::tone_scaled`]),保证同 tone 下 primary/secondary/tertiary 不会撞色,
|
||||
/// 且换强调色时中性表面逐像素不变。
|
||||
pub fn tone(&self, slot: PaletteSlot, tone: f32) -> Color {
|
||||
self.slot(slot).tone_scaled(tone, self.reference_chroma)
|
||||
}
|
||||
|
||||
/// 彩度压缩的共享基准(调试/测试用)。
|
||||
pub fn reference_chroma(&self) -> f32 {
|
||||
self.reference_chroma
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -191,7 +191,7 @@ impl Scheme {
|
||||
SchemeFlavor::Light => entry.light,
|
||||
SchemeFlavor::Dark => entry.dark,
|
||||
};
|
||||
palettes.slot(entry.slot).tone(tone)
|
||||
palettes.tone(entry.slot, tone)
|
||||
});
|
||||
Self { flavor, colors }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ use super::Color;
|
||||
/// gamut 判定容差:线性 RGB 允许露出这么点负值/溢出(浮点误差),别因此误判「越界」。
|
||||
const GAMUT_EPS: f32 = 1.0e-4;
|
||||
|
||||
/// sRGB 里能取到的 OKLCh 彩度上限(超过它没有意义,二分搜索的初始上界)。
|
||||
pub(crate) const GAMUT_CHROMA_CAP: f32 = 0.5;
|
||||
|
||||
/// sRGB 编码值(0..1)→ 线性光强。
|
||||
pub(crate) fn srgb_to_linear(c: f32) -> f32 {
|
||||
if c <= 0.040_45 {
|
||||
@@ -147,6 +150,11 @@ pub(crate) fn chroma_in_gamut(lstar: f32, hue_deg: f32, chroma_max: f32) -> f32
|
||||
lo
|
||||
}
|
||||
|
||||
/// 该 tone / 色相下 sRGB 能表达的最大彩度(P3.2 的「按比例压彩度」用它做基准)。
|
||||
pub(crate) fn max_chroma(lstar: f32, hue_deg: f32) -> f32 {
|
||||
chroma_in_gamut(lstar, hue_deg, GAMUT_CHROMA_CAP)
|
||||
}
|
||||
|
||||
/// OKLCh 坐标(色相用度,0..360)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Oklch {
|
||||
|
||||
@@ -59,6 +59,17 @@ impl TonalPalette {
|
||||
/// 二分裁剪——所以 tone 0 恒为纯黑、tone 100 恒为纯白,且同 tone 的 WCAG 对比度
|
||||
/// 与色相无关(这是对比度门禁能跨种子稳定的原因)。
|
||||
pub fn tone(self, tone: f32) -> Color {
|
||||
self.tone_scaled(tone, self.chroma)
|
||||
}
|
||||
|
||||
/// 带「共享彩度基准」的取色:**所有调色板按同一比例压彩度进 gamut**。
|
||||
///
|
||||
/// `reference_chroma` 一般是方案里最饱和那条调色板(primary)的彩度。压缩系数
|
||||
/// `f = min(1, C_max(tone,hue) / reference)`,每条调色板取 `C_req × f`——
|
||||
/// 这样 `primary` 与 `secondary` 在高 tone(tone 90/96 这类浅容器)上不会被**一起裁到**
|
||||
/// 同一个 gamut 上限而撞色,`secondary` 永远比 `primary` 更灰(M3 的语义就靠这个)。
|
||||
/// 单独用 [`TonalPalette::tone`] 时没有基准,等价于 `reference = 自身彩度`(直接裁剪)。
|
||||
pub fn tone_scaled(self, tone: f32, reference_chroma: f32) -> Color {
|
||||
let lstar = tone.clamp(0.0, 100.0);
|
||||
// 端点直接给纯黑/纯白:近黑处的 gamut 容差会放进微小彩度,导致 tone 0 偏色。
|
||||
if lstar <= 0.0 {
|
||||
@@ -67,7 +78,13 @@ impl TonalPalette {
|
||||
if lstar >= 100.0 {
|
||||
return Color::WHITE;
|
||||
}
|
||||
let chroma = space::chroma_in_gamut(lstar, self.hue_deg, self.chroma);
|
||||
let c_max = space::max_chroma(lstar, self.hue_deg);
|
||||
let factor = if reference_chroma > 1.0e-6 {
|
||||
(c_max / reference_chroma).min(1.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let chroma = (self.chroma * factor).min(c_max).max(0.0);
|
||||
let lightness = space::oklab_lightness_for_tone(chroma, self.hue_deg, lstar);
|
||||
Color::from_linear_f32(space::oklch_to_linear(lightness, chroma, self.hue_deg))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user