atmos/os_lib/web_engine/gradient_lut.rs
1//! `linear-gradient` のラスタライズ補助(LUT の段数決定と軸判定)。
2//!
3//! 仕様は `spec/gradient_raster.md`。
4//!
5//! ## なぜ必要か
6//! 実測で描画 1 フレーム 300 tick のうち **112 tick(37%)** が
7//! グラデーションだった。現行実装は 1 ピクセルごとに
8//! `sample_gradient_stops`(ストップ配列の線形走査+4 チャンネルの
9//! 浮動小数補間)を呼んでおり、924×600 なら 55 万回になる。
10//!
11//! `t` から色への写像はピクセル位置に依存しないので、
12//! 軸方向の長さぶんの LUT を先に作れば呼び出し回数は `length` 回で済む。
13//!
14//! ## 不変条件(G-1〜G-4)
15//! - **G-1**: 軸平行(0/90/180/270deg)は現行と厳密に同一の出力
16//! - **G-2**: 斜めのずれは軸方向 1 段(1 ピクセル相当)以内
17//! - **G-3**: ストップ 0 個/1 個の挙動は現行のまま
18//! - **G-4**: アルファはピクセル単位で合成(LUT には合成前の色を入れる)
19
20/// グラデーション軸の向き。
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Axis {
23 /// `dx == 0` — 射影が y だけの関数。1 行が単色になる。
24 Vertical,
25 /// `dy == 0` — 射影が x だけの関数。1 列が単色になる。
26 Horizontal,
27 /// どちらでもない。
28 Diagonal,
29}
30
31/// `dx` / `dy` が 0 とみなせるかの閾値。
32///
33/// `dx = sinf(angle)` / `dy = -cosf(angle)` で得るため、
34/// 0deg / 90deg でも厳密な 0 にはならない(`sinf(0)` は 0 だが
35/// `sinf(PI)` は約 -8.7e-8)。1 ピクセルの射影差が
36/// LUT の 1 段に満たない範囲を「軸平行」とみなす。
37const AXIS_EPS: f32 = 1e-4;
38
39/// `dx`, `dy` から軸平行かどうかを判定する。
40///
41/// 両方 0 に近い場合は `Diagonal` を返す(射影長 0 で
42/// 呼び出し側が単色扱いへ倒すため、ここで特別扱いしない)。
43pub fn classify_axis(dx: f32, dy: f32) -> Axis {
44 let dx0 = libm::fabsf(dx) < AXIS_EPS;
45 let dy0 = libm::fabsf(dy) < AXIS_EPS;
46 match (dx0, dy0) {
47 (true, false) => Axis::Vertical,
48 (false, true) => Axis::Horizontal,
49 _ => Axis::Diagonal,
50 }
51}
52
53/// LUT の上限段数。
54///
55/// 4K 幅を超える巨大なグラデーションでも確保量を抑える。
56/// この上限に当たるのは軸方向 4096 ピクセル超のときだけで、
57/// そのとき 1 段は 1 ピクセルより粗くなる(G-2 の範囲外だが、
58/// 4096 段もあれば階調差は視認できない)。
59pub const MAX_LUT: usize = 4096;
60
61/// 射影長 `length`(ピクセル)から LUT の段数を決める。
62///
63/// **軸方向 1 ピクセルに 1 段**を割り当てる。これにより軸平行の
64/// グラデーションでは各ピクセルの射影がちょうど段の境界に乗り、
65/// 現行の per-pixel 計算と厳密に一致する(G-1)。
66///
67/// 非有限・非正の `length` は 1 段(単色)へ倒す。
68pub fn lut_size(length: f32) -> usize {
69 if !length.is_finite() || length <= 0.0 {
70 return 1;
71 }
72 let n = libm::ceilf(length) as usize + 1;
73 n.clamp(2, MAX_LUT)
74}
75
76/// `t`(0.0〜1.0 想定)を LUT の添字へ変換する。
77///
78/// 範囲外はクランプする(`sample_gradient_stops` が
79/// `t <= stops[0].position` / `t >= last.position` で
80/// 端の色を返すのと同じ扱いになる)。
81/// NaN は 0 段目へ倒す(描画を止めない)。
82pub fn lut_index(t: f32, size: usize) -> usize {
83 if size == 0 {
84 return 0;
85 }
86 let last = size - 1;
87 if t.is_nan() || t <= 0.0 {
88 return 0;
89 }
90 if t >= 1.0 {
91 return last;
92 }
93 let i = (t * last as f32) as usize;
94 i.min(last)
95}