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:
AskaEth committed 2026-11-01 15:58:19 +08:00
1 parent 65ca0ed29a
commit 83e19e58f0
20 files changed
+1367 -234

No files matched your search

@@ -0,0 +1,88 @@
//! 官方 M3 **Expressive** 强调排版表(15 档,A 级来源)。
//!
//! 来源:`androidx.compose.material3.tokens.TypeScaleTokens` v0_103 的
//! `<Role>Emphasized{Size,LineHeight,Tracking,Weight}` 四元组。要点:
//!
//! - **字号与行高与 baseline 完全相同**(Expressive 的强调不改变排版高度);
//! - 只动**字重**与**字距**:display/headline/title-large/body-* → Medium(500);
//! title-medium / title-small / label-* → Bold(700)(baseline 是 500);
//! - display-large 的字距从 −0.2 回 0;body-large 0.5 → 0.15;title-medium 0.2 → 0.15;
//! body-medium 0.2 → 0.25(唯一一个「强调后字距变大」的档)。
use super::{style, FontFamily, TypeRole, TypeStyle, Weight};
/// 取某档的官方强调样式。非 [`TypeRole`] 的每个变体都有值(15/15,无缺口)。
pub(super) const fn style_of(role: TypeRole) -> TypeStyle {
match role {
TypeRole::DisplayLarge => style(57.0, 64.0, Weight::Medium, 0.0, FontFamily::Display),
TypeRole::DisplayMedium => style(45.0, 52.0, Weight::Medium, 0.0, FontFamily::Display),
TypeRole::DisplaySmall => style(36.0, 44.0, Weight::Medium, 0.0, FontFamily::Display),
TypeRole::HeadlineLarge => style(32.0, 40.0, Weight::Medium, 0.0, FontFamily::Display),
TypeRole::HeadlineMedium => style(28.0, 36.0, Weight::Medium, 0.0, FontFamily::Display),
TypeRole::HeadlineSmall => style(24.0, 32.0, Weight::Medium, 0.0, FontFamily::Display),
// TitleLarge 官方用 Brand 族(与 Display 同栈)。
TypeRole::TitleLarge => style(22.0, 28.0, Weight::Medium, 0.0, FontFamily::Display),
TypeRole::TitleMedium => style(16.0, 24.0, Weight::Bold, 0.15, FontFamily::Text),
TypeRole::TitleSmall => style(14.0, 20.0, Weight::Bold, 0.1, FontFamily::Text),
TypeRole::BodyLarge => style(16.0, 24.0, Weight::Medium, 0.15, FontFamily::Text),
TypeRole::BodyMedium => style(14.0, 20.0, Weight::Medium, 0.25, FontFamily::Text),
TypeRole::BodySmall => style(12.0, 16.0, Weight::Medium, 0.4, FontFamily::Text),
TypeRole::LabelLarge => style(14.0, 20.0, Weight::Bold, 0.1, FontFamily::Text),
TypeRole::LabelMedium => style(12.0, 16.0, Weight::Bold, 0.5, FontFamily::Text),
TypeRole::LabelSmall => style(11.0, 16.0, Weight::Bold, 0.5, FontFamily::Text),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn emphasized_keeps_size_and_line_height() {
for role in TypeRole::ALL {
let base = role.style();
let emph = style_of(role);
assert_eq!(
base.size_px,
emph.size_px,
"{} 强调档不该改字号",
role.name()
);
assert_eq!(
base.line_height_px,
emph.line_height_px,
"{} 强调档不该改行高",
role.name()
);
}
}
#[test]
fn emphasized_weights_follow_the_official_split() {
// 官方两档:正文/展示类 → Medium(500);小标题与标签类 → Bold(700)。
for role in TypeRole::ALL {
let expect = match role {
TypeRole::TitleMedium
| TypeRole::TitleSmall
| TypeRole::LabelLarge
| TypeRole::LabelMedium
| TypeRole::LabelSmall => Weight::Bold,
_ => Weight::Medium,
};
assert_eq!(
style_of(role).weight,
expect,
"{} 强调档字重不对",
role.name()
);
}
}
#[test]
fn display_large_tracking_returns_to_zero() {
assert_eq!(TypeRole::DisplayLarge.style().letter_spacing_px, -0.2);
assert_eq!(TypeRole::DisplayLarge.emphasized().letter_spacing_px, 0.0);
assert_eq!(TypeRole::BodyLarge.style().letter_spacing_px, 0.5);
assert_eq!(TypeRole::BodyLarge.emphasized().letter_spacing_px, 0.15);
}
}