docs(design)+test(palette-extract): r2 三条口径修正与常数绑定
- LowChroma 文档:删掉「彩度必然 < 候选门槛 5」(人气门槛也会淘汰高彩度小面积, 实测 0.5% 纯红报 113.36),改为与契约文档 §6 一致的口径;代码臂注释与 score::most_chromatic 同步 - Seed::color / share 改成有边界的说法:≤MAX_SAMPLES 才是图上真实色、share 的分母是有效采样点 - palette-extract 新增 max_edge_stays_within_the_design_sample_budget: MAX_EDGE² ≤ MAX_SAMPLES(256² 当前余量为零,调大就红) - card.rs:把 6.43~6.50 / 7.70~7.76 写成断言(原来只有 ≥4.5 下限,文档却宣称区间) - LowChroma.color 字段注释改成「最接近可用的候选色」(113 那条并不「能用」)
This commit is contained in:
1 parent
f4c48752c2
commit
09b716ae79
4 files changed
+69
-16
No files matched your search
@@ -57,9 +57,11 @@ impl Card {
|
||||
/// 这张卡的 `on-primary` 对 `primary` 的实测对比度。
|
||||
///
|
||||
/// 设置页可以直接显示成「对比度 {:.2}:1」。**2026-11 实测(8 张卡)**:
|
||||
/// 浅色档 6.43~6.49、深色档 7.70~7.75(逐张数值见 `every_card_scheme_meets_wcag_aa`
|
||||
/// 的断言上限与 M87 报告表格)——两种模式都远高于 WCAG AA 的 4.5:1。
|
||||
/// 别在文案里写死一个数:卡换种子后它会变,用本方法的返回值。
|
||||
/// 浅色档 **6.43~6.49**、深色档 **7.70~7.75**——两种模式都远高于 WCAG AA 的 4.5:1。
|
||||
///
|
||||
/// 这两个区间**被测试钉住**(不是口头声明):`every_card_scheme_meets_wcag_aa` 对每张卡
|
||||
/// 断言「下限 [`wcag::AA_TEXT`] + 落在上述区间内」,所以将来改种子/调色板常数把比值
|
||||
/// 拖出区间会当场红——文档里的数字与测试同源。要改数值请连测试区间一起改。
|
||||
pub fn primary_contrast(self, flavor: SchemeFlavor) -> f32 {
|
||||
let scheme = self.scheme(flavor);
|
||||
scheme.contrast(crate::color::Role::OnPrimary, crate::color::Role::Primary)
|
||||
@@ -266,12 +268,24 @@ mod tests {
|
||||
"{} / {flavor:?} 未过门禁",
|
||||
card.key
|
||||
);
|
||||
let ratio = card.primary_contrast(flavor);
|
||||
assert!(
|
||||
card.primary_contrast(flavor) >= wcag::AA_TEXT,
|
||||
"{} / {flavor:?} primary 对 {} 只有 {:.2}",
|
||||
ratio >= wcag::AA_TEXT,
|
||||
"{} / {flavor:?} primary 对 {} 只有 {ratio:.2}",
|
||||
card.key,
|
||||
card.name,
|
||||
card.primary_contrast(flavor)
|
||||
card.name
|
||||
);
|
||||
// 上半区间:文档(`Card::primary_contrast`)向用户承诺的就是这两个区间,
|
||||
// 只钉「≥ 4.5」等于没钉——比值掉到 5.0 也全绿,而文档会继续说 6.43~6.49。
|
||||
// 边界取实测极值 ±0.01:浅色 6.4324~6.4906、深色 7.7024~7.7504。
|
||||
let (low, high) = match flavor {
|
||||
SchemeFlavor::Light => (6.43, 6.50),
|
||||
SchemeFlavor::Dark => (7.70, 7.76),
|
||||
};
|
||||
assert!(
|
||||
(low..=high).contains(&ratio),
|
||||
"{} / {flavor:?} primary 对比度 {ratio:.4} 超出文档区间 {low}~{high}",
|
||||
card.key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,12 @@ pub(crate) fn rank(clusters: &[Cluster]) -> Vec<Ranked> {
|
||||
ranked
|
||||
}
|
||||
|
||||
/// 彩度最高的那个簇——被 `Score` 全部过滤时,它是「最接近候选门槛」的那一族。
|
||||
/// 彩度最高的那个簇——被 `Score` 全部过滤时用它报「这张图最鲜艳的颜色」。
|
||||
///
|
||||
/// ⚠️ 它**不是**「最接近及格的那一族」:`Score` 的过滤有两道(彩度 ≥5、色相桶人气 ≥1%),
|
||||
/// 面积不足 1% 的高彩度颜料会被**人气**刷掉——例如 1592 个米白 + 8 个纯红的图,
|
||||
/// 候选为空而这里的「最鲜艳」彩度是 **113.36**。所以调用方的文案要按
|
||||
/// 「全图最鲜艳的那一族」,不是「差一点就及格的那一族」。
|
||||
///
|
||||
/// 取色失败(`Failure::LowChroma`)时**色值与彩度都从这一个簇取**,保证二者成对:
|
||||
/// 之前的两条路径一条报 `Score` 榜首、一条报「彩度最高」,而后者还拿着**另一个**簇的色值,
|
||||
|
||||
@@ -52,13 +52,22 @@ const ALPHA_THRESHOLD: u8 = 128;
|
||||
/// 取色结果:种子色 + 足以让 UI 解释「为什么是这个颜色」的观感数据。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Seed {
|
||||
/// 种子色(图上真实存在的颜色,不是各色平均出来的)。
|
||||
/// 种子色。
|
||||
///
|
||||
/// **像素数 ≤ [`MAX_SAMPLES`] 时**它是图上真实存在的颜色(代表色取「簇内像素最多的桶里
|
||||
/// 像素原值平均」,纯色图逐通道精确);**更大的图会先做块平均降采样**,此时它是块平均后的
|
||||
/// 颜色——`2×2` 块内红蓝交替会平均成 `#703C75`,这个色值在图上任何位置都不存在。
|
||||
/// 降采样本来就要平均(顺带降噪),所以这是**有意的行为**,不是「绝不出现平均色」的保证。
|
||||
pub color: Color,
|
||||
/// 种子的 CAM16 色相角(度)。
|
||||
pub hue: f64,
|
||||
/// 种子的 CAM16 彩度。
|
||||
pub chroma: f64,
|
||||
/// 该色在其色相族里占全图的像素比例(0..1)。
|
||||
/// 该色所在色相族占**有效采样点**的比例(0..1)。
|
||||
///
|
||||
/// 分母是采样点(块平均后的点、且已剔除透明像素),不是全图像素数。所以只有
|
||||
/// 「无透明 + 像素数 ≤ [`MAX_SAMPLES`]」时它才等于「占全图比例」;大图(有降采样)
|
||||
/// 或有透明区域时只能说「占有效采样点的比例」。
|
||||
pub share: f64,
|
||||
}
|
||||
|
||||
@@ -107,13 +116,21 @@ pub enum Failure {
|
||||
/// `color` 与 `chroma` **永远来自同一个簇**(复审 r1 p2-4.2 定的口径):
|
||||
/// - 有候选(通过 `Score` 的彩度 ≥5 与人气 ≥1% 过滤)但最强候选仍 `< 8` →
|
||||
/// 报 `Score` **榜首**的色值与彩度(它是「本次能拿到的最好候选」);
|
||||
/// - 一个候选都没有(灰度图 / 只有零碎高彩度小面积)→ 报**彩度最高**的那一簇
|
||||
/// (此时彩度必然 < 候选门槛 5)。
|
||||
/// - 一个候选都没有 → 报**彩度最高**的那一簇。
|
||||
/// 这条**不是「灰图专属」**:`Score` 除了彩度 ≥5 还要求所在色相桶的人气 ≥1%,
|
||||
/// 所以「高彩度但面积太小」也会走到这里。实测(复审探针):40×40、1592 个米白
|
||||
/// `#F5F1EA` + 8 个纯红 `#FF0000`(0.5% 面积)→ 报回来的彩度是 **113.36**
|
||||
/// (`#FF0000` 的 CAM16 彩度,见 `color::hct` 的对账表)。
|
||||
///
|
||||
/// ⚠️ UI 别把这个数字写成「彩度 X < 门槛 8」:第二种情况(高彩度但面积不足 1%)里
|
||||
/// X 可能 ≥8,真正的失败原因是「没有一族面积够大的颜色」。文案用「颜色太淡 / 太杂」即可。
|
||||
/// ⚠️ 因此这个数字**不受候选门槛 5 约束**,可能远高于 8。UI 别把它写成
|
||||
/// 「彩度 X < 门槛 8」:真正的失败原因是「没有一族**面积够大**的颜色」。文案用
|
||||
/// 「颜色太淡 / 太杂」即可;要显示数字也只在 `chroma < CUTOFF_CHROMA(5)` 时显示
|
||||
/// (那时的含义才是「整张图没颜色」,即纯灰图)。
|
||||
LowChroma {
|
||||
/// 最接近能用的那个色(提示文案里当「最接近的颜色」)。
|
||||
/// 取色失败时最接近可用的候选色(提示文案里可以显示成一个小色块)。
|
||||
///
|
||||
/// 注意它不一定「能用」:高彩度但面积很小的颜料(如 0.5% 面积的纯红)也会出现在这里,
|
||||
/// 见 [`Failure::LowChroma`] 的说明。
|
||||
color: Color,
|
||||
/// 该色的实测 CAM16 彩度(与 `color` 成对)。
|
||||
chroma: f64,
|
||||
@@ -169,7 +186,9 @@ pub fn analyze_rgba(pixels: &[u8], width: u32, height: u32) -> Result<Seed, Fail
|
||||
color: best.cluster.color,
|
||||
chroma: best.chroma,
|
||||
}),
|
||||
// 一个候选都没有(灰度图):报**彩度最高**的那一族,色值与彩度成对(必然 < 候选门槛 5)。
|
||||
// 一个候选都没有:报**彩度最高**的那一族,色值与彩度成对。
|
||||
// 这不等于「整张图没颜色」——1592 米白 + 8 个纯红的图也走这里(红色面积 0.5% 被人气
|
||||
// 门槛淘汰),报回来的彩度是 113.36。真实失败原因是「没有一族面积够大的颜色」。
|
||||
None => {
|
||||
let closest = score::most_chromatic(&clusters);
|
||||
Err(Failure::LowChroma {
|
||||
|
||||
@@ -156,6 +156,21 @@ fn absurd_dimensions_are_rejected_before_allocating() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_edge_stays_within_the_design_sample_budget() {
|
||||
// 结构性防线(复审 r2 要求):`unlucky_design` 只在「像素数 ≤ MAX_SAMPLES」时才不做块平均,
|
||||
// 也只有那时 `Seed::color` 才保证是图上真实存在的颜色。当前 256² = 65536 = MAX_SAMPLES,
|
||||
// **余量为零**——谁把 MAX_EDGE 调到 512,UI 路径就会开始返回「块平均出来的、图上不存在的
|
||||
// 颜色」(例如红蓝交替被平均成 #703C75),而没有任何东西会红。这条把两个常数绑在一起。
|
||||
let budget = unlucky_design::palette::MAX_SAMPLES;
|
||||
let edge = MAX_EDGE as usize;
|
||||
assert!(
|
||||
edge * edge <= budget,
|
||||
"MAX_EDGE={MAX_EDGE} 的平方({})超过 design 的采样预算 {budget}: 调大它之前先读 `unlucky_design::palette::Seed::color` 的文档——超过预算后种子不再是图上真实存在的颜色",
|
||||
edge * edge
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_borrows_without_copying() {
|
||||
let loaded = decode_bytes(&encode_png(&solid_image(4, 4, [0x67, 0x50, 0xA4])));
|
||||
|
||||
Reference in new issue
Block a user