atmos/os_lib/web_engine/backdrop.rs
1//! 文字の下地色(グリフのアンチエイリアスの縁を合成する色)の決定(純粋ロジック)。
2//!
3//! この描画系は、グリフの縁の半透明な画素を「下地色」と事前に合成してから
4//! 不透明に書き込む(`kernel::vector_font` の色付きグリフキャッシュ)。
5//! したがって下地色が実際に文字の下に見えている色と違うと、縁が別の色と
6//! 合成され、文字が太く・色づいて見える。
7//!
8//! 【2026-10-04 修正】sugi-lab.net/smarthome.htm でヘッダのナビとロゴが
9//! 本物より太く・青紫がかって描かれた。ヘッダは `rgba(255,255,255,.95)` の
10//! 半透明で、その下に紫のグラデーション(`.page-header`、先頭色 #667eea)がある。
11//! 旧実装(`render.rs` の `effective_bg_behind`)は半透明の背景を
12//! 「下地として信頼できない」として**読み飛ばし**、紫を返していた。
13//! 実際に見えているのはほぼ白(白 95% を紫に重ねた色)なので、
14//! 半透明の層は捨てずに下の不透明な背景へ重ねて合成する。
15//!
16//! 試験: `tests/src/test_backdrop.rs`
17
18use super::alpha_blend::blend_argb_over;
19
20/// 文字の下地色を返す。
21///
22/// - `candidates`: 対象の文字を内包する要素の `(面積, ARGB 背景色)`。
23/// 面積が小さいほど内側(手前)とみなす(従来の判定をそのまま使う)。
24/// - `page_bg`: どの要素にも当たらないときのページ背景。
25///
26/// 規則:
27/// 1. アルファ 0xFF または 0x00 の背景を「不透明」とみなし、その中で
28/// 一番内側のものを土台にする(0x00 は、アルファを付けずに色を渡す
29/// 古いコード向けの互換規則。`spec/alpha_compositing.md`)。
30/// 無ければ `page_bg` を土台にする。
31/// 2. 土台より内側(面積が小さい)にある半透明の背景を、外側から順に
32/// 土台へ source-over で重ねる。土台より外側の半透明は土台の下に
33/// 隠れているので使わない。
34/// 3. 半透明の層が無ければ土台の値を**そのまま**返す(従来と同一の結果)。
35/// 合成した場合の結果は常に不透明。
36pub fn effective_backdrop(candidates: &[(i64, u32)], page_bg: u32) -> u32 {
37 let is_opaque = |c: u32| {
38 let a = c >> 24;
39 a == 0xFF || a == 0x00
40 };
41
42 let mut base = page_bg;
43 let mut base_area = i64::MAX;
44 for &(area, bg) in candidates {
45 if is_opaque(bg) && area < base_area {
46 base = bg;
47 base_area = area;
48 }
49 }
50
51 // 土台より内側の半透明層を、外側(面積が大きい方)から順に重ねる。
52 let mut layers: alloc::vec::Vec<(i64, u32)> = candidates
53 .iter()
54 .copied()
55 .filter(|&(area, bg)| !is_opaque(bg) && area < base_area)
56 .collect();
57 if layers.is_empty() {
58 return base;
59 }
60 layers.sort_by_key(|b| core::cmp::Reverse(b.0));
61
62 let mut acc = base | 0xFF00_0000;
63 for (_, bg) in layers {
64 acc = blend_argb_over(bg, acc);
65 }
66 acc
67}
68
69/// 文字の下に重なっている背景の 1 層。
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct Layer {
72 /// 要素の面積。小さいほど内側(手前)とみなす。
73 pub area: i64,
74 /// 背景色(ARGB)。無ければ `None`。
75 pub color: Option<u32>,
76 /// 背景画像またはグラデーションを持つか(1 色で表せない背景)。
77 pub painted: bool,
78}
79
80/// 文字の縁を何と合成するか。
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum Backdrop {
83 /// この 1 色と事前に合成してよい(高速。従来の経路)。
84 Solid(u32),
85 /// 1 色で表せない(画像・グラデーション)。縁を半透明のまま渡し、
86 /// 画面に既に描かれている実際の画素と合成させる。
87 Framebuffer,
88}
89
90/// 文字の下地を決める(画像・グラデーションを考慮する版)。
91///
92/// 【2026-10-04 続き】`effective_backdrop` で半透明を重ねるようにしたところ、
93/// index.htm のヒーロー文字が細く青みがかった。ヒーローの文字の下は写真で、
94/// 1 色では表せない。旧実装は白、修正後は青灰色を返しており、どちらも誤り。
95///
96/// 規則: 不透明な単色の層と、画像・グラデーションの層を「土台」とみなし、
97/// 一番内側の土台が画像・グラデーションなら `Framebuffer`。
98/// 単色が土台なら従来どおり(`effective_backdrop`)の合成結果。
99/// 土台より外側の層は土台に隠れているので使わない。
100pub fn resolve_backdrop(layers: &[Layer], page_bg: u32) -> Backdrop {
101 let opaque = |c: u32| {
102 let a = c >> 24;
103 a == 0xFF || a == 0x00
104 };
105 // 一番内側の「土台」(不透明な単色、または画像・グラデーション)。
106 let base = layers
107 .iter()
108 .filter(|l| l.painted || l.color.is_some_and(opaque))
109 .min_by_key(|l| l.area);
110 if let Some(b) = base {
111 if b.painted {
112 return Backdrop::Framebuffer;
113 }
114 }
115 let colors: alloc::vec::Vec<(i64, u32)> = layers
116 .iter()
117 .filter(|l| !l.painted)
118 .filter_map(|l| l.color.map(|c| (l.area, c)))
119 .collect();
120 Backdrop::Solid(effective_backdrop(&colors, page_bg))
121}