feat(design): 配色来源纯数学层(Monet 式量化取色 + 色卡 + 三级回落 + 对比度收敛)

D40 分层:design 保持零第三方依赖,只吃已解码的 RGBA 像素。
- palette/quantize:5bit 直方图 + CIELAB k-means(QuantizerCelebi/Score 的形状)
- palette/score:官方 Score 口径(CUTOFF_CHROMA 5 / TARGET 48 / 权重 0.7+0.3)
- palette/seed:seed_from_rgba + D40 彩度门槛 8,灰白图回落而非凭空上色
- palette/card:8 张内置色卡,key 为持久化键
- palette/wcag:26 对文字对比度门禁 + 沿 tone 轴收敛并记录修正
- palette/resolve:纯函数三级来源编排 + Provenance(原因码/提示键)
This commit is contained in:
AskaEth committed 2026-11-02 11:04:10 +08:00
1 parent c1fc5b8086
commit 18327fb038
11 files changed
+2098 -2

No files matched your search

+2 -1
View File
@@ -114,7 +114,8 @@ pub mod tonal;
mod cam16;
mod critical_planes;
mod hct_solver;
mod space;
// `pub(crate)`:取色量化(`crate::palette::quantize`)要在 CIELAB 里算簇距离,复用同一套换算。
pub(crate) mod space;
mod viewing_conditions;
pub use hct::Hct;
+12
View File
@@ -212,6 +212,18 @@ impl Scheme {
Self { flavor, colors }
}
/// 从 49 个已解析色值直接构造(**底层入口**,下标必须等于 `Role as usize`)。
///
/// 只给两类调用方用:
/// 1. `palette::wcag` 的对比度收敛——它按角色替换个别色值后要重新组装成一套方案;
/// 2. 测试——要能构造「故意不达标」的方案,比较度校验器才有反证。
///
/// 常规路径一律走 [`Scheme::from_palettes`]:那样色值才是 tone 表的推导结果,
/// 而不是随手塞进来的数。
pub fn from_colors(flavor: SchemeFlavor, colors: [Color; 49]) -> Self {
Self { flavor, colors }
}
/// 该角色在指定模式下的 tone 值(M3 语义;文档表格、调试与测试用)。
pub fn tone_of(role: Role, flavor: SchemeFlavor) -> f64 {
let entry = &TONE_TABLE[role as usize];
+14
View File
@@ -109,6 +109,20 @@ pub(crate) fn lstar_from_color(color: Color) -> f64 {
lstar_from_y(xyz_from_color(color)[1])
}
/// 色值 → CIELAB(`[L*, a*, b*]`)。
///
/// `L*` 与 [`lstar_from_color`] 逐值相同(同一个 `lab_f`);`a*`/`b*` 走 D65 白点归一。
/// 取色量化(`crate::palette::quantize`)用它在感知空间里算簇间距离——sRGB 欧氏距离会把
/// 暗部差异放大、亮部差异压扁,分出来的簇不符合「看起来是一族颜色」。
pub(crate) fn lab_from_color(color: Color) -> [f64; 3] {
let xyz = xyz_from_color(color);
let f = |component: f64, white: f64| lab_f(component / white);
let fx = f(xyz[0], WHITE_POINT_D65[0]);
let fy = f(xyz[1], WHITE_POINT_D65[1]);
let fz = f(xyz[2], WHITE_POINT_D65[2]);
[116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz)]
}
/// `L*` → 中性灰色值(`L*` 与彩度无关,纯灰)。
pub(crate) fn color_from_lstar(lstar: f64) -> Color {
let component = delinearized(y_from_lstar(lstar));