アルファ合成仕様

対象実装: arm/v1/src/os_lib/web_engine/alpha_blend.rs 単体試験: arm/v1/tests/src/test_alpha_blend.rs

典拠(2026-07-28、ユーザ許可のうえ参照): - CSS Color Module Level 3 — https://www.w3.org/TR/css-color-3/#alphavaluedt - 合成アルゴリズムは SVG 1.1 §14.2 “Simple alpha compositing” に委譲されている

なぜこの仕様書があるか

www.sugi-lab.net 再現中、ヒーロー中央に不透明な黒帯が出た。実データ:

[HERO][OVERPAINT] id="_ta_49" z=5 rect=(0,149,924,231) bg=0x33000000

0x33000000 は .hero-overlay { background: rgba(0,0,0,.2) } に対応し、 アルファ 0x33 は正しくパースされている。にもかかわらず描画側が アルファを無視して不透明色として塗っていた。

原典から確定した事項

  1. アルファの有効範囲は [0.0, 1.0]。0.0 が完全透明、1.0 が完全不透明。
  2. 範囲外の値は必ずクランプする(原典明記: “will be clamped to this range”)。 計算値も「[0.0,1.0] にクリップした後の指定値」と定義される。
  3. 合成規則そのものは SVG 1.1 §14.2 Simple alpha compositing に従う。

合成式(Simple alpha compositing / source-over)

背景(backdrop)が不透明な画面バッファである本実装では、 結果アルファは常に 1 となり、各チャンネルは次で求まる:

result = src * alpha + dst * (1 - alpha)

8bit 整数で扱う場合、alpha8 = 0..=255 として

result = (src * alpha8 + dst * (255 - alpha8) + 127) / 255

(+127 は四捨五入。切り捨てのみだと合成を繰り返すたびに暗くなる。)

不変条件

本実装の範囲と未対応

互換上の妥協(重要・仕様どおりではない)

Screen::draw_pixel では アルファ 0x00 を「完全透明」ではなく「不透明」として扱う。

既存コードの大半はアルファを設定せず 0x00RRGGBB のまま色を渡しており、 仕様どおり 0x00 を完全透明にすると画面全体が消えてしまうため。 実際に合成するのは 0x01〜0xFE のみ。

これは仕様違反であり、恒久対応としては全描画呼び出しが明示的に 0xFF を設定するよう是正したうえで、この妥協を撤廃すべき。spec/TODO.md 参照。

【2026-07-28】この妥協が実際に事故を起こした

導入直後、ナビゲーションバーの右半分が黒くなる不具合が出た。 重なる要素を全列挙して実測した結果:

[NAVBG] id="_ta_19" z=1000 d=6 rect=(462,9,924,25) bg=0x00000000   ← .nav-list

.nav-list の背景は transparent(=rgba(0,0,0,0))で、 parse_color はこれを 0x00000000 として正しく返していた。 ところが上記の妥協によりアルファ 0x00 が不透明扱いとなり、 不透明な黒として右半分を塗り潰していた。

対処: extract_color(layout.rs)で、アルファ 0 の色は None(=背景なし)を返す。 背景色として完全透明は「背景を塗らない」と等価であり、意味的にも正しい。 parse_color は不透明色に必ず 0xFF を立てるため、アルファ 0 は transparent / rgba(...,0) の明示指定だけを意味し、この判定は安全。

教訓: 「既存コードが壊れないように」と入れた互換妥協は、 別の正しい入力を壊す。妥協を入れるときは、それが誤って拾う入力 (ここでは transparent)を洗い出して先に手当てすべきだった。

【2026-08-05】アルファ 0 が「不透明な黒」として描かれていた

border-color: transparent(0x00000000)を持つ要素が、 黒い枠として描かれていた。

実測

セクション見出しの周りの「角丸の輪郭」と、 #apps の一覧にある 20×20 の ☐ は、いずれも実サイトに存在しない。 要素ダンプで bd=Some(0) = 完全透明のボーダーだった。

原因

boxfill はアルファで経路を分けていた:

let a = (color.0 >> 24) & 0xFF;
if a != 0 && a != 0xFF { /* 1 ピクセルずつ合成 */ }
// a == 0 は一括書き込みへ落ちる → 0x00000000 がそのまま入る

フレームバッファにアルファは無いので、 書き込まれた 0x00000000 は不透明な黒になる。

draw_pixel の方は意図的にアルファ 0 を不透明として扱っている (コメントに明記。下位のほぼ全ての呼び出しがアルファを設定せず 0 のまま色を渡すため、仕様どおり透明にすると画面が消える)。

修正

  1. boxfill: アルファ 0 なら何も描かずに返す
  2. draw_individual_borders: ボーダー色が全て透明なら描画しない (幅はレイアウトに残す。CSS では透明ボーダーも場所を占める)

結果: 見出しを囲っていた黒い枠が消えた。

残り

角の跡と一覧の ☐ が残っている。 これらは draw_pixel を直接使う角丸ボーダーの毎ピクセル経路で、 そこにはまだ透明判定が入っていない。

draw_pixel 自体は互換上変えられないので、 呼び出し側(角丸ボーダー描画)で透明を弾く必要がある。