Skip to main content

atmos/os_lib/web_engine/
alpha_blend.rs

1//! アルファ合成(Simple alpha compositing / source-over)。
2//!
3//! 仕様・典拠・不変条件は `spec/alpha_compositing.md` を参照。
4//! 典拠: CSS Color Module Level 3 (https://www.w3.org/TR/css-color-3/#alphavaluedt)
5//! および SVG 1.1 §14.2 Simple alpha compositing。
6//!
7//! ## なぜ必要か
8//! 実サイトの `.hero-overlay { background: rgba(0,0,0,.2) }` は
9//! `0x33000000` として正しくパースされていたのに、描画側がアルファを無視して
10//! **不透明な黒**で塗っていた。結果ヒーロー中央に黒帯が出ていた。
11//!
12//! グローバル状態にもハードウェアにも依存しない純粋ロジックのみで構成する。
13
14extern crate alloc;
15
16/// 合成の引数が不正だった場合のエラー。
17#[derive(Debug, PartialEq, Clone, Copy)]
18pub enum BlendError {
19    /// アルファが NaN / 無限大で数値として解釈できない。
20    /// (範囲外の有限値は原典の指示どおりクランプするのでエラーにしない)
21    NonFiniteAlpha(f32),
22}
23
24/// 0.0〜1.0 のアルファを 8bit へ変換する。
25///
26/// 原典どおり、範囲外の**有限値**はクランプする(エラーにしない)。
27/// NaN / 無限大は数値として解釈できないため、黙って戻らずエラーログを出して
28/// `Err` を返す。呼び出し側は不透明(255)扱いへフォールバックすること。
29pub fn alpha_f32_to_u8(a: f32) -> Result<u8, BlendError> {
30    if !a.is_finite() {
31        crate::error!(
32            "[BLEND] アルファが有限値ではありません: {}。不透明として扱ってください",
33            a
34        );
35        return Err(BlendError::NonFiniteAlpha(a));
36    }
37    let clamped = a.clamp(0.0, 1.0);
38    Ok((clamped * 255.0 + 0.5) as u8)
39}
40
41/// 1 チャンネル分の source-over 合成。
42///
43/// `result = (src * a + dst * (255 - a) + 127) / 255`
44/// `+127` は四捨五入。切り捨てのみだと合成を繰り返すたびに暗くなる。
45#[inline]
46pub fn blend_channel(src: u8, dst: u8, alpha: u8) -> u8 {
47    let s = src as u32;
48    let d = dst as u32;
49    let a = alpha as u32;
50    (((s * a) + (d * (255 - a)) + 127) / 255) as u8
51}
52
53/// ARGB8888 の 1 ピクセルを背景へ source-over 合成する。
54///
55/// `src` の上位 8bit をアルファとして使う。背景 `dst` は不透明前提なので
56/// 結果のアルファは常に 0xFF になる。
57pub fn blend_argb_over(src: u32, dst: u32) -> u32 {
58    let a = ((src >> 24) & 0xFF) as u8;
59    // 完全不透明/完全透明は計算せず即決(頻出経路の短絡かつ A-1/A-2 の厳密保証)。
60    if a == 255 {
61        return 0xFF00_0000 | (src & 0x00FF_FFFF);
62    }
63    if a == 0 {
64        return 0xFF00_0000 | (dst & 0x00FF_FFFF);
65    }
66    let sr = ((src >> 16) & 0xFF) as u8;
67    let sg = ((src >> 8) & 0xFF) as u8;
68    let sb = (src & 0xFF) as u8;
69    let dr = ((dst >> 16) & 0xFF) as u8;
70    let dg = ((dst >> 8) & 0xFF) as u8;
71    let db = (dst & 0xFF) as u8;
72    let r = blend_channel(sr, dr, a) as u32;
73    let g = blend_channel(sg, dg, a) as u32;
74    let b = blend_channel(sb, db, a) as u32;
75    0xFF00_0000 | (r << 16) | (g << 8) | b
76}
77
78/// この色が合成を要するか(=完全不透明でないか)。
79#[inline]
80pub fn needs_blend(argb: u32) -> bool {
81    ((argb >> 24) & 0xFF) != 0xFF
82}