Skip to main content

atmos/os_lib/web_engine/
draw_helpers.rs

1use crate::os_lib::css::parse_color;
2pub(crate) use super::css_filters::*;
3pub(crate) use super::bilinear::sample_bilinear_rgba;
4
5/// 計算量: **O(W + H)** — 枠線 4 辺の長さに比例(太さは定数倍)。
6/// 面積ではないので軽い。実測 0〜1 tick。
7pub(super) fn draw_individual_borders(
8    screen: &crate::kernel::draw::Screen,
9    mut x0: i32,
10    mut y0: i32,
11    mut x1: i32,
12    mut y1: i32,
13    el: &super::RenderElement,
14    is_hovered: bool,
15    is_focused: bool,
16    is_active: bool,
17) {
18    // Apply client insets if present (top, right, bottom, left)
19    if let Some((top, right, bottom, left)) = el.client_insets {
20        x0 += left;
21        y0 += top;
22        x1 -= right;
23        y1 -= bottom;
24        // Ensure coordinates remain valid
25        if x0 > x1 || y0 > y1 {
26            return;
27        }
28    }
29    if x0 >= x1 || y0 >= y1 {
30        return;
31    }
32
33    // 【2026-08-05 発見・修正】`border-color: transparent`(アルファ 0)は
34    // **何も描かない**。
35    //
36    // `draw_pixel` はアルファ 0x00 を互換上「不透明」として扱う妥協があるため
37    // (下位のほぼ全ての呼び出しがアルファを設定せず 0 のまま渡すので、
38    //  仕様どおり透明にすると画面が消える)、透明なボーダーが
39    // **不透明な黒**として描かれていた。
40    //
41    // 実測: `#apps` の一覧にある 20×20 の `<i>` 要素や、
42    // セクション見出しの周りに黒い枠(☐ / 角丸の輪郭)が出ていた。
43    // 実サイトには存在しない。
44    //
45    // 幅はレイアウトに残す(CSS では透明ボーダーも場所を占める)。
46    // 描画だけを止める。
47    let all_transparent = [
48        el.border_top_color,
49        el.border_right_color,
50        el.border_bottom_color,
51        el.border_left_color,
52        el.border_color,
53    ]
54    .iter()
55    .flatten()
56    .all(|c| (c >> 24) & 0xFF == 0);
57    let has_any_color = [
58        el.border_top_color,
59        el.border_right_color,
60        el.border_bottom_color,
61        el.border_left_color,
62        el.border_color,
63    ]
64    .iter()
65    .any(|c| c.is_some());
66    if has_any_color && all_transparent {
67        return;
68    }
69
70    // Top
71    if el.border_top_width > 0 && !el.border_top_style.is_none() {
72        let color = if is_active {
73            el.active_border_color
74                .or(el.border_top_color)
75                .or(el.border_color)
76                .unwrap_or(el.color)
77        } else if is_hovered {
78            el.hover_border_color
79                .or(el.border_top_color)
80                .or(el.border_color)
81                .unwrap_or(el.color)
82        } else if is_focused {
83            el.focus_border_color
84                .or(el.border_top_color)
85                .or(el.border_color)
86                .unwrap_or(el.color)
87        } else {
88            el.border_top_color.or(el.border_color).unwrap_or(el.color)
89        };
90        let t_w = el.border_top_width;
91        if el.border_top_style == super::BorderStyle::Dashed {
92            let dash = 6;
93            let gap = 4;
94            let mut x = x0;
95            while x < x1 {
96                let end = (x + dash).min(x1);
97                screen.boxfill(
98                    x as u32,
99                    y0 as u32,
100                    (end - 1).max(x) as u32,
101                    (y0 + t_w - 1).max(y0) as u32,
102                    crate::kernel::draw::Color(color),
103                );
104                x += dash + gap;
105            }
106        } else {
107            screen.boxfill(
108                x0 as u32,
109                y0 as u32,
110                (x1 - 1).max(x0) as u32,
111                (y0 + t_w - 1).max(y0) as u32,
112                crate::kernel::draw::Color(color),
113            );
114        }
115    }
116
117    // Bottom
118    if el.border_bottom_width > 0 && !el.border_bottom_style.is_none() {
119        let color = if is_active {
120            el.active_border_color
121                .or(el.border_bottom_color)
122                .or(el.border_color)
123                .unwrap_or(el.color)
124        } else if is_hovered {
125            el.hover_border_color
126                .or(el.border_bottom_color)
127                .or(el.border_color)
128                .unwrap_or(el.color)
129        } else if is_focused {
130            el.focus_border_color
131                .or(el.border_bottom_color)
132                .or(el.border_color)
133                .unwrap_or(el.color)
134        } else {
135            el.border_bottom_color
136                .or(el.border_color)
137                .unwrap_or(el.color)
138        };
139        let b_w = el.border_bottom_width;
140        if el.border_bottom_style == super::BorderStyle::Dashed {
141            let dash = 6;
142            let gap = 4;
143            let mut x = x0;
144            while x < x1 {
145                let end = (x + dash).min(x1);
146                screen.boxfill(
147                    x as u32,
148                    (y1 - b_w) as u32,
149                    (end - 1).max(x) as u32,
150                    (y1 - 1).max(y1 - b_w) as u32,
151                    crate::kernel::draw::Color(color),
152                );
153                x += dash + gap;
154            }
155        } else {
156            screen.boxfill(
157                x0 as u32,
158                (y1 - b_w) as u32,
159                (x1 - 1).max(x0) as u32,
160                (y1 - 1).max(y1 - b_w) as u32,
161                crate::kernel::draw::Color(color),
162            );
163        }
164    }
165
166    // Left
167    if el.border_left_width > 0 && !el.border_left_style.is_none() {
168        let color = if is_active {
169            el.active_border_color
170                .or(el.border_left_color)
171                .or(el.border_color)
172                .unwrap_or(el.color)
173        } else if is_hovered {
174            el.hover_border_color
175                .or(el.border_left_color)
176                .or(el.border_color)
177                .unwrap_or(el.color)
178        } else if is_focused {
179            el.focus_border_color
180                .or(el.border_left_color)
181                .or(el.border_color)
182                .unwrap_or(el.color)
183        } else {
184            el.border_left_color.or(el.border_color).unwrap_or(el.color)
185        };
186        let l_w = el.border_left_width;
187        if el.border_left_style == super::BorderStyle::Dashed {
188            let dash = 6;
189            let gap = 4;
190            let mut y = y0;
191            while y < y1 {
192                let end = (y + dash).min(y1);
193                screen.boxfill(
194                    x0 as u32,
195                    y as u32,
196                    (x0 + l_w - 1).max(x0) as u32,
197                    (end - 1).max(y) as u32,
198                    crate::kernel::draw::Color(color),
199                );
200                y += dash + gap;
201            }
202        } else {
203            screen.boxfill(
204                x0 as u32,
205                y0 as u32,
206                (x0 + l_w - 1).max(x0) as u32,
207                (y1 - 1).max(y0) as u32,
208                crate::kernel::draw::Color(color),
209            );
210        }
211    }
212
213    // Right
214    if el.border_right_width > 0 && !el.border_right_style.is_none() {
215        let color = if is_active {
216            el.active_border_color
217                .or(el.border_right_color)
218                .or(el.border_color)
219                .unwrap_or(el.color)
220        } else if is_hovered {
221            el.hover_border_color
222                .or(el.border_right_color)
223                .or(el.border_color)
224                .unwrap_or(el.color)
225        } else if is_focused {
226            el.focus_border_color
227                .or(el.border_right_color)
228                .or(el.border_color)
229                .unwrap_or(el.color)
230        } else {
231            el.border_right_color
232                .or(el.border_color)
233                .unwrap_or(el.color)
234        };
235        let r_w = el.border_right_width;
236        if el.border_right_style == super::BorderStyle::Dashed {
237            let dash = 6;
238            let gap = 4;
239            let mut y = y0;
240            while y < y1 {
241                let end = (y + dash).min(y1);
242                screen.boxfill(
243                    (x1 - r_w) as u32,
244                    y as u32,
245                    (x1 - 1).max(x1 - r_w) as u32,
246                    (end - 1).max(y) as u32,
247                    crate::kernel::draw::Color(color),
248                );
249                y += dash + gap;
250            }
251        } else {
252            screen.boxfill(
253                (x1 - r_w) as u32,
254                y0 as u32,
255                (x1 - 1).max(x1 - r_w) as u32,
256                (y1 - 1).max(y0) as u32,
257                crate::kernel::draw::Color(color),
258            );
259        }
260    }
261}
262
263pub fn draw_border(
264    screen: &crate::kernel::draw::Screen,
265    x0: i32,
266    y0: i32,
267    x1: i32,
268    y1: i32,
269    color_val: u32,
270) {
271    if x0 >= x1 || y0 >= y1 {
272        return;
273    }
274    let color = crate::kernel::draw::Color(color_val);
275    let ux0 = x0 as u32;
276    let uy0 = y0 as u32;
277    let ux1 = (x1 - 1) as u32;
278    let uy1 = (y1 - 1) as u32;
279
280    // 上辺
281    screen.boxfill(ux0, uy0, ux1, uy0, color);
282    if y1 - y0 > 1 {
283        // 下辺
284        screen.boxfill(ux0, uy1, ux1, uy1, color);
285    }
286    if y1 - y0 > 2 {
287        // 左辺(四隅重複を除外)
288        screen.boxfill(ux0, uy0 + 1, ux0, uy1 - 1, color);
289        if x1 - x0 > 1 {
290            // 右辺(四隅重複を除外)
291            screen.boxfill(ux1, uy0 + 1, ux1, uy1 - 1, color);
292        }
293    }
294}
295
296pub(super) fn draw_border_with_style(
297    screen: &crate::kernel::draw::Screen,
298    x0: i32,
299    y0: i32,
300    x1: i32,
301    y1: i32,
302    color_val: u32,
303    style: super::BorderStyle,
304) {
305    if style.is_none() {
306        return;
307    }
308    if style == super::BorderStyle::Dashed {
309        draw_dashed_border(screen, x0, y0, x1, y1, color_val);
310        return;
311    }
312    draw_border(screen, x0, y0, x1, y1, color_val);
313}
314
315fn draw_dashed_border(
316    screen: &crate::kernel::draw::Screen,
317    x0: i32,
318    y0: i32,
319    x1: i32,
320    y1: i32,
321    color_val: u32,
322) {
323    if x0 >= x1 || y0 >= y1 {
324        return;
325    }
326    let color = crate::kernel::draw::Color(color_val);
327    let dash = 6;
328    let gap = 4;
329    let mut x = x0;
330    while x < x1 {
331        let end = (x + dash).min(x1);
332        screen.boxfill(
333            x as u32,
334            y0 as u32,
335            (end - 1).max(x) as u32,
336            y0 as u32,
337            color,
338        );
339        if y1 - y0 > 1 {
340            screen.boxfill(
341                x as u32,
342                (y1 - 1) as u32,
343                (end - 1).max(x) as u32,
344                (y1 - 1) as u32,
345                color,
346            );
347        }
348        x += dash + gap;
349    }
350    let mut y = y0;
351    while y < y1 {
352        let end = (y + dash).min(y1);
353        screen.boxfill(
354            x0 as u32,
355            y as u32,
356            x0 as u32,
357            (end - 1).max(y) as u32,
358            color,
359        );
360        if x1 - x0 > 1 {
361            screen.boxfill(
362                (x1 - 1) as u32,
363                y as u32,
364                (x1 - 1) as u32,
365                (end - 1).max(y) as u32,
366                color,
367            );
368        }
369        y += dash + gap;
370    }
371}
372
373pub(super) fn extract_border_color(
374    styled: &crate::os_lib::css::StyledNode<'_>,
375    default_color: u32,
376) -> Option<u32> {
377    for key in [
378        "border-color",
379        "border-top-color",
380        "border-right-color",
381        "border-bottom-color",
382        "border-left-color",
383    ] {
384        if let Some(v) = styled.value(key) {
385            if let Some(c) = parse_color(&v) {
386                return Some(c);
387            }
388        }
389    }
390
391    if let Some(v) = styled.value("border") {
392        for token in v.split_whitespace().rev() {
393            if let Some(c) = parse_color(token) {
394                return Some(c);
395            }
396        }
397    }
398
399    Some(default_color)
400}
401
402pub(super) fn extract_border_style(styled: &crate::os_lib::css::StyledNode<'_>) -> super::BorderStyle {
403    if let Some(v) = styled.value("border-style") {
404        let style = v.trim();
405        if !style.is_empty() {
406            return super::BorderStyle::parse(style);
407        }
408    }
409
410    if let Some(v) = styled.value("border") {
411        for token in v.split_whitespace() {
412            let token_l = token.trim().to_lowercase();
413            if matches!(
414                token_l.as_str(),
415                "none" | "solid" | "dashed" | "dotted" | "double"
416            ) {
417                return super::BorderStyle::parse(&token_l);
418            }
419        }
420    }
421
422    super::BorderStyle::Solid
423}
424
425/// CSS `transform: rotate(Ndeg)` 向け — 回転した塗りつぶし矩形を描く。
426/// cx/cy は回転中心(スクリーン座標)、w/h は元の矩形サイズ、angle_deg は時計回り度数。
427pub fn draw_rotated_rect_filled(
428    screen: &crate::kernel::draw::Screen,
429    cx: i32,
430    cy: i32,
431    w: i32,
432    h: i32,
433    angle_deg: f32,
434    color: u32,
435) {
436    if w <= 0 || h <= 0 {
437        return;
438    }
439    let rad = angle_deg * core::f32::consts::PI / 180.0;
440    let cos_a = libm::cosf(rad);
441    let sin_a = libm::sinf(rad);
442    // 回転後バウンディングボックス
443    let hw = w as f32 / 2.0;
444    let hh = h as f32 / 2.0;
445    let bound_w = libm::ceilf(libm::fabsf(hw * cos_a) + libm::fabsf(hh * sin_a)) as i32;
446    let bound_h = libm::ceilf(libm::fabsf(hw * sin_a) + libm::fabsf(hh * cos_a)) as i32;
447    let draw_color = crate::kernel::draw::Color(color | 0xFF000000);
448    for oy in -bound_h..=bound_h {
449        for ox in -bound_w..=bound_w {
450            // 逆回転でソース座標を求める
451            let sx = ox as f32 * cos_a + oy as f32 * sin_a;
452            let sy = -ox as f32 * sin_a + oy as f32 * cos_a;
453            if sx >= -hw && sx <= hw && sy >= -hh && sy <= hh {
454                let px = cx + ox;
455                let py = cy + oy;
456                if px >= 0 && py >= 0 {
457                    screen.draw_pixel(px as u32, py as u32, draw_color);
458                }
459            }
460        }
461    }
462}
463
464/// CSS `transform: skewX(deg) / skewY(deg)` 向け — せん断変形した塗りつぶし矩形を描く。
465pub fn draw_skewed_rect_filled(
466    screen: &crate::kernel::draw::Screen,
467    cx: i32,
468    cy: i32,
469    w: i32,
470    h: i32,
471    skew_x_deg: f32,
472    skew_y_deg: f32,
473    color: u32,
474) {
475    if w <= 0 || h <= 0 {
476        return;
477    }
478    let rad_x = skew_x_deg * core::f32::consts::PI / 180.0;
479    let rad_y = skew_y_deg * core::f32::consts::PI / 180.0;
480    let tan_x = libm::tanf(rad_x);
481    let tan_y = libm::tanf(rad_y);
482
483    let hw = w as f32 / 2.0;
484    let hh = h as f32 / 2.0;
485
486    let det = 1.0 - tan_x * tan_y;
487    if det.abs() < 1e-6 {
488        return;
489    }
490
491    let bound_w = libm::ceilf(hw + hh * libm::fabsf(tan_x)) as i32;
492    let bound_h = libm::ceilf(hh + hw * libm::fabsf(tan_y)) as i32;
493    let draw_color = crate::kernel::draw::Color(color | 0xFF000000);
494
495    for oy in -bound_h..=bound_h {
496        for ox in -bound_w..=bound_w {
497            let sx = (ox as f32 - oy as f32 * tan_x) / det;
498            let sy = (oy as f32 - ox as f32 * tan_y) / det;
499            if sx >= -hw && sx <= hw && sy >= -hh && sy <= hh {
500                let px = cx + ox;
501                let py = cy + oy;
502                if px >= 0 && py >= 0 {
503                    screen.draw_pixel(px as u32, py as u32, draw_color);
504                }
505            }
506        }
507    }
508}
509
510pub use super::clip_path::*;
511
512pub fn draw_gradation(
513    screen: &crate::kernel::draw::Screen,
514    x0: i32,
515    y0: i32,
516    w: i32,
517    h: i32,
518    c1: u32,
519    c2: u32,
520) {
521    if w <= 0 || h <= 0 {
522        return;
523    }
524    let x_end = (x0 + w - 1).max(x0) as u32;
525    let r1 = ((c1 >> 16) & 0xFF) as i32;
526    let g1 = ((c1 >> 8) & 0xFF) as i32;
527    let b1 = (c1 & 0xFF) as i32;
528
529    let r2 = ((c2 >> 16) & 0xFF) as i32;
530    let g2 = ((c2 >> 8) & 0xFF) as i32;
531    let b2 = (c2 & 0xFF) as i32;
532
533    let denom = (h - 1).max(1);
534    for dy in 0..h {
535        let py = (y0 + dy) as u32;
536        let r = ((r1 * (denom - dy) + r2 * dy) / denom) as u32;
537        let g = ((g1 * (denom - dy) + g2 * dy) / denom) as u32;
538        let b = ((b1 * (denom - dy) + b2 * dy) / denom) as u32;
539        let c = 0xFF000000 | (r << 16) | (g << 8) | b;
540        screen.boxfill(
541            x0 as u32,
542            py,
543            x_end,
544            py,
545            crate::kernel::draw::Color(c),
546        );
547    }
548}
549
550/// 計算量: **O(W×H)**(LUT 化後)— W,H は塗る矩形のサイズ。
551///
552/// 内訳:
553/// - LUT 構築: **O(L × S)** — L = `lut_size(射影長)`(軸方向 1px に 1 段)、
554///   S = ストップ数。`sample_gradient_stops` を L 回呼ぶ
555/// - 塗り: 軸平行(0/180deg)なら **行ごとに `boxfill` 1 回** = O(H) 回の呼び出し。
556///   斜めなら 1 ピクセルずつ **O(W×H)**
557///
558/// 【2026-07-31 改善】以前は 1 ピクセルごとに `sample_gradient_stops`
559/// (ストップ配列の線形走査+4 チャンネル浮動小数補間)を呼んでおり
560/// **O(W×H×S)** だった。924×600 で 55 万回。実測で描画 1 フレームの
561/// 37%(112 tick)を占めていた → LUT 化で 62〜70 tick へ。
562/// 詳細は `spec/gradient_raster.md`。
563pub fn draw_linear_gradient(
564    screen: &crate::kernel::draw::Screen,
565    x0: i32,
566    y0: i32,
567    w: i32,
568    h: i32,
569    grad: &super::LinearGradient,
570) {
571    if w <= 0 || h <= 0 || grad.stops.is_empty() {
572        return;
573    }
574
575    let angle_rad = grad.angle_deg * core::f32::consts::PI / 180.0;
576    // 0deg = to top (0, -1), 90deg = to right (1, 0)
577    let dx = libm::sinf(angle_rad);
578    let dy = -libm::cosf(angle_rad);
579
580    // ボックスの4頂点 (相対座標)
581    let corners = [
582        (0.0, 0.0),
583        (w as f32, 0.0),
584        (0.0, h as f32),
585        (w as f32, h as f32),
586    ];
587
588    let mut min_proj = f32::MAX;
589    let mut max_proj = f32::MIN;
590    for &(cx, cy) in &corners {
591        let p = cx * dx + cy * dy;
592        if p < min_proj { min_proj = p; }
593        if p > max_proj { max_proj = p; }
594    }
595    
596    let length = max_proj - min_proj;
597    let x_end = (x0 + w - 1).max(x0) as u32;
598    if length <= 0.0 {
599        let c = grad.stops[0].color;
600        let y_end = (y0 + h - 1).max(y0) as u32;
601        screen.boxfill(x0 as u32, y0 as u32, x_end, y_end, crate::kernel::draw::Color(c));
602        return;
603    }
604
605    // 【2026-07-31 性能修正】仕様は `spec/gradient_raster.md`。
606    //
607    // 従来は 1 ピクセルごとに `sample_gradient_stops`(ストップ配列の線形走査+
608    // 4 チャンネルの浮動小数補間)を呼んでいた。924×600 なら 55 万回。
609    // 実測で描画 1 フレーム 300 tick のうち 112 tick(37%)がここだった。
610    //
611    // `t` から色への写像はピクセル位置に依存しないので、軸方向の長さぶんの
612    // LUT を先に作る。呼び出しは `length` 回で済む(G-1: 軸方向 1 ピクセルに
613    // 1 段を割り当てるので、軸平行なら従来と厳密に一致する)。
614    use super::gradient_lut::{classify_axis, lut_index, lut_size, Axis};
615    let lut_n = lut_size(length);
616    let mut lut: alloc::vec::Vec<u32> = alloc::vec::Vec::with_capacity(lut_n);
617    for i in 0..lut_n {
618        let t = if lut_n <= 1 {
619            0.0
620        } else {
621            i as f32 / (lut_n - 1) as f32
622        };
623        lut.push(sample_gradient_stops(&grad.stops, t));
624    }
625
626    // 軸平行なら 1 行(または行内の各列)が単色になる。
627    // 縦グラデーションは行ごとに `boxfill` 1 回で済む。
628    let axis = classify_axis(dx, dy);
629
630    if axis == Axis::Horizontal {
631        // 水平グラデーション: dy == 0 のため全行で列ごとの色は共通。
632        // 1 行分の色配列を事前計算し、各行へスキャンライン直列書き込みを行う。
633        let mut row_colors = alloc::vec::Vec::with_capacity(w as usize);
634        for x in 0..w {
635            let p_x = (x as f32) * dx;
636            let t = (p_x - min_proj) / length;
637            row_colors.push(lut[lut_index(t, lut_n)]);
638        }
639        for y in 0..h {
640            let py = y0 + y;
641            screen.draw_scanline_colors(x0, py, &row_colors);
642        }
643        return;
644    }
645
646    let mut row_colors = alloc::vec![0u32; w as usize];
647    for y in 0..h {
648        let py = (y0 + y) as u32;
649        let p_y = (y as f32) * dy;
650
651        if axis == Axis::Vertical {
652            // この行は単色。boxfill は不透明なら一括DMA/fill、半透明なら boxfill_alpha(スキャンライン直接合成)
653            // に自動分岐するため、1行の boxfill 呼び出しで高速に描画できる。
654            let t = (p_y - min_proj) / length;
655            let color = lut[lut_index(t, lut_n)];
656            screen.boxfill(
657                x0 as u32,
658                py,
659                x_end,
660                py,
661                crate::kernel::draw::Color(color),
662            );
663            continue;
664        }
665
666        // 斜めグラデーション: 行バッファを埋めてスキャンライン直列転送
667        for x in 0..w {
668            let p_x = (x as f32) * dx;
669            let p = p_x + p_y;
670            let t = (p - min_proj) / length;
671            row_colors[x as usize] = lut[lut_index(t, lut_n)];
672        }
673        screen.draw_scanline_colors(x0, py as i32, &row_colors);
674    }
675}
676
677/// `stops`(`position` 昇順ソート済み前提)から、位置 `t`(0.0-1.0)における補間色を得る。
678/// `draw_linear_gradient` の同ロジックを N ストップの `conic-gradient` でも再利用するために
679/// 独立関数として切り出した。
680/// 計算量: **O(S)** — S はストップ数(線形走査)。
681/// 4 チャンネルの浮動小数補間を伴うため定数倍も小さくない。
682/// **毎ピクセルで呼ばない**こと(呼び出し側で LUT 化する)。
683pub(crate) fn sample_gradient_stops(stops: &[super::GradientStop], t: f32) -> u32 {
684    if stops.is_empty() {
685        return 0;
686    }
687    if t <= stops[0].position {
688        return stops[0].color;
689    }
690    let last = stops.last().unwrap_or(&stops[0]);
691    if t >= last.position {
692        return last.color;
693    }
694    for i in 0..stops.len() - 1 {
695        let s1 = &stops[i];
696        let s2 = &stops[i + 1];
697        if t >= s1.position && t <= s2.position {
698            let range = s2.position - s1.position;
699            let local_t = if range > 0.0 { (t - s1.position) / range } else { 0.0 };
700            let a1 = ((s1.color >> 24) & 0xFF) as f32;
701            let r1 = ((s1.color >> 16) & 0xFF) as f32;
702            let g1 = ((s1.color >> 8) & 0xFF) as f32;
703            let b1 = (s1.color & 0xFF) as f32;
704            let a2 = ((s2.color >> 24) & 0xFF) as f32;
705            let r2 = ((s2.color >> 16) & 0xFF) as f32;
706            let g2 = ((s2.color >> 8) & 0xFF) as f32;
707            let b2 = (s2.color & 0xFF) as f32;
708            let a = ((1.0 - local_t) * a1 + local_t * a2) as u32;
709            let r = ((1.0 - local_t) * r1 + local_t * r2) as u32;
710            let g = ((1.0 - local_t) * g1 + local_t * g2) as u32;
711            let b = ((1.0 - local_t) * b1 + local_t * b2) as u32;
712            return (a << 24) | (r << 16) | (g << 8) | b;
713        }
714    }
715    last.color
716}
717
718/// `conic-gradient(c1, c2, ..., cN)` — N色ストップを角度方向へ(12時方向起点、時計回り)
719/// 補間して描画する。以前は `kernel::draw::Screen::draw_conic_gradient_rect` が2色固定
720/// だったため3色目以降が失われていた。ここでは `draw_linear_gradient` と同じ補間ロジック
721/// (`sample_gradient_stops`)を角度ベースの `t` に適用する。
722pub fn draw_conic_gradient(
723    screen: &crate::kernel::draw::Screen,
724    x0: i32,
725    y0: i32,
726    w: i32,
727    h: i32,
728    stops: &[super::GradientStop],
729) {
730    if w <= 0 || h <= 0 || stops.is_empty() {
731        return;
732    }
733    let cx = w as f32 / 2.0;
734    let cy = h as f32 / 2.0;
735    const PI: f32 = core::f32::consts::PI;
736    for dy in 0..h {
737        for dx in 0..w {
738            let px = dx as f32 - cx;
739            let py = dy as f32 - cy;
740            // atan2(px, -py): 真上(0,-1)方向を角度0とし、時計回りに増加させる
741            // (kernel::draw::draw_conic_gradient_rect と同じ角度基準)。
742            let mut angle = libm::atan2f(px, -py);
743            if angle < 0.0 {
744                angle += 2.0 * PI;
745            }
746            let t = angle / (2.0 * PI);
747            let color = sample_gradient_stops(stops, t);
748            screen.draw_pixel_alpha(
749                (x0 + dx) as u32,
750                (y0 + dy) as u32,
751                crate::kernel::draw::Color(color),
752                255,
753            );
754        }
755    }
756}
757
758pub fn draw_radial_gradient(
759    screen: &crate::kernel::draw::Screen,
760    x0: i32,
761    y0: i32,
762    w: i32,
763    h: i32,
764    grad: &super::RadialGradient,
765) {
766    if w <= 0 || h <= 0 || grad.stops.is_empty() {
767        return;
768    }
769
770    let cx = (w as f32) * grad.pos_x;
771    let cy = (h as f32) * grad.pos_y;
772    
773    // 最大半径 (四隅のうち最も遠い距離)
774    let max_rx = if grad.pos_x > 0.5 { cx } else { (w as f32) - cx };
775    let max_ry = if grad.pos_y > 0.5 { cy } else { (h as f32) - cy };
776    
777    let (r_x, r_y) = match grad.shape {
778        super::RadialShape::Circle => {
779            let r = libm::sqrtf(max_rx * max_rx + max_ry * max_ry);
780            (r, r)
781        },
782        super::RadialShape::Ellipse => {
783            (max_rx, max_ry)
784        }
785    };
786    
787    if r_x <= 0.0 || r_y <= 0.0 {
788        let c = grad.stops[0].color;
789        screen.boxfill(x0 as u32, y0 as u32, (x0 + w) as u32, (y0 + h) as u32, crate::kernel::draw::Color(c));
790        return;
791    }
792
793    for y in 0..h {
794        let py = y0 + y;
795        let dy = (y as f32) - cy;
796        for x in 0..w {
797            let px = x0 + x;
798            let dx = (x as f32) - cx;
799            
800            let t = libm::sqrtf((dx * dx) / (r_x * r_x) + (dy * dy) / (r_y * r_y));
801            let color = sample_gradient_stops(&grad.stops, t);
802            let alpha = ((color >> 24) & 0xFF) as u8;
803            screen.draw_pixel_alpha(px as u32, py as u32, crate::kernel::draw::Color(color), alpha);
804        }
805    }
806}
807
808/// `background-clip` の `"border-box"`/`"padding-box"`/`"content-box"` を
809/// 実際のピクセル差分(border 幅、または border+padding 幅)に変換する。
810/// border-box は無変換(0,0,0,0)。
811fn box_inset(el: &super::RenderElement, mode: &str) -> (i32, i32, i32, i32) {
812    match mode {
813        "content-box" => (
814            el.border_top_width + el.padding_top,
815            el.border_right_width + el.padding_right,
816            el.border_bottom_width + el.padding_bottom,
817            el.border_left_width + el.padding_left,
818        ),
819        "padding-box" => (
820            el.border_top_width,
821            el.border_right_width,
822            el.border_bottom_width,
823            el.border_left_width,
824        ),
825        _ => (0, 0, 0, 0),
826    }
827}
828
829/// `background-clip` に基づき、背景の描画を制限すべき矩形(border-box 座標系)を返す。
830/// `border-box`(既定)なら el の外形そのまま。
831pub(super) fn bg_clip_rect(
832    el: &super::RenderElement,
833    x0_rect: i32,
834    y0_rect: i32,
835    x1_rect: i32,
836    y1_rect: i32,
837) -> (u32, u32, u32, u32) {
838    let (top, right, bottom, left) = box_inset(el, &el.bg_clip);
839    (
840        (x0_rect + left).max(0) as u32,
841        (y0_rect + top).max(0) as u32,
842        (x1_rect - right).max(0) as u32,
843        (y1_rect - bottom).max(0) as u32,
844    )
845}
846
847/// `background-origin` に基づき、`draw_background_image()` へ渡す
848/// `(origin_shift_x, origin_shift_y, origin_w, origin_h)` を計算する。
849/// `origin_shift_*` は基準 box の左上が描画 box(`eff_w`/`eff_h` の左上)から
850/// どれだけ内側にあるか、`origin_w`/`origin_h` は基準 box 自体のサイズ。
851pub(super) fn bg_origin_shift(
852    el: &super::RenderElement,
853    eff_w: i32,
854    eff_h: i32,
855) -> (i32, i32, i32, i32) {
856    let (top, right, bottom, left) = box_inset(el, &el.bg_origin);
857    (
858        left,
859        top,
860        (eff_w - left - right).max(1),
861        (eff_h - top - bottom).max(1),
862    )
863}
864
865/// background-image を要素の box 内に描画する。background-size/repeat/position を反映する。
866/// `bg_size` が `Stretch`(未指定時の既定)なら従来どおり box 全面へ引き伸ばす。
867/// `layer_override` を指定すると、複数レイヤー(`background-image: url(a), url(b)`)の
868/// うちこのレイヤーだけの size/repeat-x/repeat-y/pos-x/pos-y を使う(以前は全レイヤーが
869/// `el` 側の単一の値を共有しており、レイヤーごとの `background-position`/`-size`/`-repeat`
870/// 指定が無視されるバグがあった)。`None` なら従来通り `el` の値をそのまま使う。
871/// 【2026-07-31計測】背景画像描画に費やした tick の累積(フレーム毎にリセット)。
872/// `paint` が 2.7 秒/フレームかかっている内訳を切り分けるため。
873pub(crate) static BG_IMG_TICKS: core::sync::atomic::AtomicUsize =
874    core::sync::atomic::AtomicUsize::new(0);
875
876/// 【2026-07-31計測】グラデーション描画に費やした tick の累積。
877pub(crate) static GRAD_TICKS: core::sync::atomic::AtomicUsize =
878    core::sync::atomic::AtomicUsize::new(0);
879/// 【2026-07-31計測】ボーダー描画に費やした tick の累積。
880pub(crate) static BORDER_TICKS: core::sync::atomic::AtomicUsize =
881    core::sync::atomic::AtomicUsize::new(0);
882/// 【2026-07-31計測】`<img>` の拡縮描画に費やした tick の累積。
883pub(crate) static IMG_TICKS: core::sync::atomic::AtomicUsize =
884    core::sync::atomic::AtomicUsize::new(0);
885/// 【2026-08-01計測】linear 以外の大面積描画
886/// (radial / conic / 2 色グラデーション / rotate / skew)の累積 tick。
887/// `GRAD_TICKS` は `draw_linear_gradient` しか見ていなかった。
888pub(crate) static OTHERFILL_TICKS: core::sync::atomic::AtomicUsize =
889    core::sync::atomic::AtomicUsize::new(0);
890
891/// 早期 return が複数あるため、Drop で確実に積む。
892struct TickTimer(usize, &'static core::sync::atomic::AtomicUsize);
893impl Drop for TickTimer {
894    fn drop(&mut self) {
895        self.1.fetch_add(
896            crate::kernel::timer::get_ticks().wrapping_sub(self.0),
897            core::sync::atomic::Ordering::Relaxed,
898        );
899    }
900}
901
902/// 早期 return が複数あるため、Drop で確実に積む。
903struct BgImgTimer(usize);
904impl Drop for BgImgTimer {
905    fn drop(&mut self) {
906        BG_IMG_TICKS.fetch_add(
907            crate::kernel::timer::get_ticks().wrapping_sub(self.0),
908            core::sync::atomic::Ordering::Relaxed,
909        );
910    }
911}
912
913/// 計算量: **O(Wd×Hd × R)** — Wd,Hd は描画先サイズ、R は
914/// `background-repeat` によるタイル数(`no-repeat` なら 1)。
915///
916/// 1 ピクセルごとに元画像をサンプリングして拡縮するため、
917/// **元画像のサイズには依存せず描画先の面積で決まる**。
918/// 拡縮結果はキャッシュしていないので**毎フレーム再計算**になる。
919///
920/// 実測: sugi-lab.net の 1 フレームで 33〜47 tick(paint の約 14%)。
921pub(super) fn draw_background_image(
922    screen: &crate::kernel::draw::Screen,
923    img: &super::DecodedImage,
924    el: &super::RenderElement,
925    x0_rect: i32,
926    y0_rect: i32,
927    eff_w: i32,
928    eff_h: i32,
929    clip_s: &Option<ClipShape>,
930    // `background-attachment: fixed` 用。垂直スクロール量をそのまま渡すと、
931    // ボックス自体はスクロールで動いても背景パターンのサンプリング座標だけが
932    // 打ち消されてビューポートに固定されて見える(このエンジンには横スクロール
933    // モデル自体が存在しないため水平方向は常に0)。`scroll`(既定)なら常に0。
934    attachment_shift_y: i32,
935    // `background-origin`(既定 padding-box)用。position/size のパーセンテージ計算の
936    // 基準となる box は、実際に描画・タイル敷き詰めを行う box(`eff_w`/`eff_h`。
937    // `background-clip` により外側で screen.push_clip 済み)とは別物にできる。
938    // `origin_shift_x/y` は基準box の左上が描画box の左上からどれだけ内側にあるか、
939    // `origin_w/h` は基準box 自体のサイズ。border-box(既定と同じ扱い)なら
940    // shift=(0,0)、origin_w/h=eff_w/eff_h を渡せばよい。
941    origin_shift_x: i32,
942    origin_shift_y: i32,
943    origin_w: i32,
944    origin_h: i32,
945    layer_override: Option<(
946        super::BgSizeMode,
947        super::BgRepeatMode,
948        super::BgRepeatMode,
949        f32,
950        f32,
951    )>,
952) {
953    use super::{BgSizeMode, BgSizeVal};
954    let _bg_timer = BgImgTimer(crate::kernel::timer::get_ticks());
955    // Apply client insets to background drawing area
956    let (mut x0_rect, mut y0_rect, mut eff_w, mut eff_h) = (x0_rect, y0_rect, eff_w, eff_h);
957    if let Some((top, right, bottom, left)) = el.client_insets {
958        x0_rect += left;
959        y0_rect += top;
960        eff_w -= left + right;
961        eff_h -= top + bottom;
962        if eff_w <= 0 || eff_h <= 0 {
963            return;
964        }
965    }
966    let (el_bg_size, el_bg_repeat_x, el_bg_repeat_y, el_bg_pos_x, el_bg_pos_y) =
967        layer_override.unwrap_or((el.bg_size, el.bg_repeat_x, el.bg_repeat_y, el.bg_pos_x, el.bg_pos_y));
968    // box_w/box_h: 実際にピクセルを描く(タイル敷き詰めの)範囲。background-clip の
969    // クリップ済み座標系ではなく、常に要素の border-box 全体を走査する
970    // (clip 自体は呼び出し側で screen.push_clip により別途適用済み)。
971    let box_w = eff_w.max(1) as u32;
972    let box_h = eff_h.max(1) as u32;
973    // origin_box_w/h: background-size の % 解決と background-position のオフセット計算に
974    // 使う基準 box(background-origin)。border-box なら box_w/h と同じ値になる。
975    let origin_box_w = origin_w.max(1) as u32;
976    let origin_box_h = origin_h.max(1) as u32;
977    if img.width == 0 || img.height == 0 {
978        return;
979    }
980
981    fn resolve_axis(v: BgSizeVal, box_dim: u32, img_dim: u32) -> Option<u32> {
982        match v {
983            BgSizeVal::Auto => None,
984            BgSizeVal::Px(px) => Some(px.max(1) as u32),
985            BgSizeVal::Percent(p) => Some(((box_dim as f32) * p).max(1.0) as u32),
986        }
987        .or(Some(img_dim))
988    }
989
990    let (draw_w, draw_h): (u32, u32) = match el_bg_size {
991        BgSizeMode::Stretch => (origin_box_w, origin_box_h),
992        BgSizeMode::Cover => {
993            let scale = (origin_box_w as f32 / img.width as f32)
994                .max(origin_box_h as f32 / img.height as f32);
995            (
996                ((img.width as f32) * scale).max(1.0) as u32,
997                ((img.height as f32) * scale).max(1.0) as u32,
998            )
999        }
1000        BgSizeMode::Contain => {
1001            let scale = (origin_box_w as f32 / img.width as f32)
1002                .min(origin_box_h as f32 / img.height as f32);
1003            (
1004                ((img.width as f32) * scale).max(1.0) as u32,
1005                ((img.height as f32) * scale).max(1.0) as u32,
1006            )
1007        }
1008        BgSizeMode::Explicit(wv, hv) => {
1009            // 片方だけ auto の場合はアスペクト比を保って他方から逆算する。
1010            match (wv, hv) {
1011                (BgSizeVal::Auto, BgSizeVal::Auto) => (img.width, img.height),
1012                (BgSizeVal::Auto, _) => {
1013                    let h = resolve_axis(hv, origin_box_h, img.height).unwrap_or(img.height);
1014                    let w = ((img.width as f32) * (h as f32 / img.height as f32)).max(1.0) as u32;
1015                    (w, h)
1016                }
1017                (_, BgSizeVal::Auto) => {
1018                    let w = resolve_axis(wv, origin_box_w, img.width).unwrap_or(img.width);
1019                    let h = ((img.height as f32) * (w as f32 / img.width as f32)).max(1.0) as u32;
1020                    (w, h)
1021                }
1022                _ => (
1023                    resolve_axis(wv, origin_box_w, img.width).unwrap_or(img.width),
1024                    resolve_axis(hv, origin_box_h, img.height).unwrap_or(img.height),
1025                ),
1026            }
1027        }
1028    };
1029
1030    // `background-repeat: round`(タイルをこの軸に整数個ちょうど収まるよう拡縮)/
1031    // `space`(タイル寸法は変えず、タイル間に等間隔の隙間を配分)を1軸ぶん解決する。
1032    // 戻り値: (実効タイル寸法, タイル開始位置の周期(=タイル+隙間), bg-positionを無視するか)。
1033    // round/space はタイルを軸全体に敷き詰める挙動が前提のため、position は無視する簡略実装。
1034    fn tile_layout(mode: super::BgRepeatMode, box_dim: u32, tile_dim: u32) -> (u32, u32, bool) {
1035        use super::BgRepeatMode;
1036        match mode {
1037            BgRepeatMode::Round => {
1038                let n =
1039                    (libm::roundf(box_dim as f32 / tile_dim.max(1) as f32) as u32).max(1);
1040                let eff = (box_dim / n).max(1);
1041                (eff, eff, true)
1042            }
1043            BgRepeatMode::Space => {
1044                let n = box_dim / tile_dim.max(1);
1045                if n <= 1 {
1046                    (tile_dim, tile_dim, false)
1047                } else {
1048                    let gap = box_dim.saturating_sub(n * tile_dim) / (n - 1);
1049                    (tile_dim, tile_dim + gap, true)
1050                }
1051            }
1052            _ => (tile_dim, tile_dim, false),
1053        }
1054    }
1055
1056    let (draw_w_eff, pitch_w, ignore_pos_x) = tile_layout(el_bg_repeat_x, box_w, draw_w);
1057    let (draw_h_eff, pitch_h, ignore_pos_y) = tile_layout(el_bg_repeat_y, box_h, draw_h);
1058    // off_x/off_y は background-origin の基準 box 内でのオフセット。
1059    // 実際に描くのは box_w/box_h(=描画/クリップ側の座標系)なので、
1060    // 基準 box の左上が描画 box の左上からどれだけ内側にあるか(origin_shift_x/y)を
1061    // 加算してから rel_x/rel_y の計算(下)で使う。
1062    let off_x = if ignore_pos_x {
1063        0
1064    } else {
1065        ((origin_box_w as i32 - draw_w as i32) as f32 * el_bg_pos_x) as i32
1066    };
1067    let off_y = if ignore_pos_y {
1068        0
1069    } else {
1070        ((origin_box_h as i32 - draw_h as i32) as f32 * el_bg_pos_y) as i32
1071    };
1072
1073    // 列方向のサンプリング座標を事前計算し、行ループ内での除算・剰余・条件分岐を完全排除する
1074    let col_src: alloc::vec::Vec<Option<u32>> = (0..box_w)
1075        .map(|col| {
1076            let rel_x = (col as i32 - origin_shift_x) - off_x;
1077            let sample_x = match el_bg_repeat_x {
1078                super::BgRepeatMode::NoRepeat => rel_x,
1079                super::BgRepeatMode::Space => {
1080                    let m = rel_x.rem_euclid(pitch_w as i32);
1081                    if m >= draw_w_eff as i32 {
1082                        return None;
1083                    }
1084                    m
1085                }
1086                _ => rel_x.rem_euclid(draw_w_eff as i32),
1087            };
1088            if sample_x < 0 || sample_x >= draw_w_eff as i32 {
1089                None
1090            } else {
1091                Some((sample_x as u32 * img.width) / draw_w_eff.max(1))
1092            }
1093        })
1094        .collect();
1095
1096    let has_blend = !el.background_blend_mode.is_empty() && el.background_blend_mode != "normal";
1097    let blend_dst = if has_blend {
1098        el.bg_color.map(|bg| (
1099            ((bg >> 16) & 0xFF) as u8,
1100            ((bg >> 8) & 0xFF) as u8,
1101            (bg & 0xFF) as u8,
1102        ))
1103    } else {
1104        None
1105    };
1106
1107    let mut row_buf = if clip_s.is_none() {
1108        alloc::vec![0u32; box_w as usize]
1109    } else {
1110        alloc::vec![]
1111    };
1112
1113    for row in 0..box_h {
1114        let rel_y = (row as i32 + attachment_shift_y - origin_shift_y) - off_y;
1115        let sample_y = match el_bg_repeat_y {
1116            super::BgRepeatMode::NoRepeat => rel_y,
1117            super::BgRepeatMode::Space => {
1118                let m = rel_y.rem_euclid(pitch_h as i32);
1119                if m >= draw_h_eff as i32 {
1120                    continue;
1121                }
1122                m
1123            }
1124            _ => rel_y.rem_euclid(draw_h_eff as i32),
1125        };
1126        if sample_y < 0 || sample_y >= draw_h_eff as i32 {
1127            continue;
1128        }
1129        let src_row = (sample_y as u32 * img.height) / draw_h_eff.max(1);
1130        let row_offset = (src_row * img.width) as usize;
1131        let py = (y0_rect as u32).wrapping_add(row);
1132
1133        if clip_s.is_none() {
1134            // clip-path なしの高速パス: 行バッファを生成してスキャンライン直列転送
1135            for col in 0..box_w {
1136                let src_col = match col_src[col as usize] {
1137                    Some(sc) => sc,
1138                    None => {
1139                        row_buf[col as usize] = 0;
1140                        continue;
1141                    }
1142                };
1143                let idx = (row_offset + src_col as usize) * 4;
1144                if idx + 4 <= img.rgba.len() {
1145                    let mut r = img.rgba[idx];
1146                    let mut g = img.rgba[idx + 1];
1147                    let mut b = img.rgba[idx + 2];
1148                    let raw_a = img.rgba[idx + 3];
1149                    if raw_a == 0 {
1150                        row_buf[col as usize] = 0;
1151                        continue;
1152                    }
1153                    let a = ((raw_a as u32 * el.opacity as u32) / 255) as u8;
1154                    if a == 0 {
1155                        row_buf[col as usize] = 0;
1156                        continue;
1157                    }
1158
1159                    if let Some(dst) = blend_dst {
1160                        let (br, bgc, bb) = blend_pixel(&el.background_blend_mode, (r, g, b), dst);
1161                        r = br;
1162                        g = bgc;
1163                        b = bb;
1164                    }
1165                    row_buf[col as usize] = ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
1166                } else {
1167                    row_buf[col as usize] = 0;
1168                }
1169            }
1170            screen.draw_scanline_colors(x0_rect, py as i32, &row_buf);
1171        } else {
1172            // clip-path ありの汎用パス
1173            for col in 0..box_w {
1174                let src_col = match col_src[col as usize] {
1175                    Some(sc) => sc,
1176                    None => continue,
1177                };
1178                if let Some(ref cs) = clip_s {
1179                    if !clip_inside(cs, col as i32, row as i32) {
1180                        continue;
1181                    }
1182                }
1183
1184                let idx = (row_offset + src_col as usize) * 4;
1185                if idx + 4 <= img.rgba.len() {
1186                    let mut r = img.rgba[idx];
1187                    let mut g = img.rgba[idx + 1];
1188                    let mut b = img.rgba[idx + 2];
1189                    let raw_a = img.rgba[idx + 3];
1190                    if raw_a == 0 {
1191                        continue;
1192                    }
1193                    let a = ((raw_a as u32 * el.opacity as u32) / 255) as u8;
1194                    if a == 0 {
1195                        continue;
1196                    }
1197
1198                    if let Some(dst) = blend_dst {
1199                        let (br, bgc, bb) = blend_pixel(&el.background_blend_mode, (r, g, b), dst);
1200                        r = br;
1201                        g = bgc;
1202                        b = bb;
1203                    }
1204                    let color = crate::kernel::draw::Color(
1205                        0xFF000000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32),
1206                    );
1207                    screen.draw_pixel_alpha((x0_rect as u32).wrapping_add(col), py, color, a);
1208                }
1209            }
1210        }
1211    }
1212}
1213
1214/// `text-decoration-line`(underline/line-through)の水平線を `style`
1215/// (solid/dotted/dashed/wavy/double、空文字またはその他は solid 扱い)と
1216/// `thickness`(`text-decoration-thickness`、px。最低1pxにクランプ)で描画する。
1217pub(super) fn draw_decoration_line(
1218    screen: &crate::kernel::draw::Screen,
1219    x0: u32,
1220    y: u32,
1221    x1: u32,
1222    style: &str,
1223    color: u32,
1224    thickness: u32,
1225) {
1226    if x1 <= x0 {
1227        return;
1228    }
1229    let t = thickness.max(1);
1230    let color_val = crate::kernel::draw::Color(color);
1231    match style {
1232        "dotted" => {
1233            let mut x = x0;
1234            while x < x1 {
1235                let seg_end = (x + 2).min(x1);
1236                screen.boxfill(x, y, (seg_end - 1).max(x), y + t - 1, color_val);
1237                x += 4;
1238            }
1239        }
1240        "dashed" => {
1241            let mut x = x0;
1242            while x < x1 {
1243                let seg_end = (x + 5).min(x1);
1244                screen.boxfill(x, y, (seg_end - 1).max(x), y + t - 1, color_val);
1245                x += 8;
1246            }
1247        }
1248        "wavy" => {
1249            // 振幅2px・周期6pxの単純な三角波(サイン波の近似)。thickness分だけ縦に太らせる。
1250            let amplitude = 2i32;
1251            let period = 6i32;
1252            for x in x0..x1 {
1253                let phase = (x as i32 - x0 as i32) % period;
1254                let dy = if phase < period / 2 { phase - amplitude / 2 } else { period - phase - amplitude / 2 };
1255                let py = (y as i32 + dy).max(0) as u32;
1256                screen.boxfill(x, py, x, py + t - 1, color_val);
1257            }
1258        }
1259        "double" => {
1260            let x_end = (x1 - 1).max(x0);
1261            screen.boxfill(x0, y, x_end, y + t - 1, color_val);
1262            let y2 = y + t + 2;
1263            screen.boxfill(x0, y2, x_end, y2 + t - 1, color_val);
1264        }
1265        _ => {
1266            let x_end = (x1 - 1).max(x0);
1267            screen.boxfill(x0, y, x_end, y + t - 1, color_val);
1268        }
1269    }
1270}
1271
1272/// outline を描画する。border-box の外側に `offset` だけ離して `width` px の枠線を引く。
1273/// レイアウトに影響しない装飾のため、border とは独立して呼ぶ。
1274pub(super) fn draw_outline(
1275    screen: &crate::kernel::draw::Screen,
1276    x0: i32,
1277    y0: i32,
1278    x1: i32,
1279    y1: i32,
1280    width: i32,
1281    offset: i32,
1282    color: u32,
1283    style: &str,
1284) {
1285    let ox0 = x0 - offset - width;
1286    let oy0 = y0 - offset - width;
1287    let ox1 = x1 + offset + width;
1288    let oy1 = y1 + offset + width;
1289    // dashed/dotted: 破線パターンの矩形を width 回重ねて太さを近似する
1290    // (dotted も既存の dashed 実装を流用する簡略実装、独立した点線パターンは非対応)。
1291    if matches!(style, "dashed" | "dotted") {
1292        for i in 0..width {
1293            draw_dashed_border(screen, ox0 + i, oy0 + i, ox1 - i, oy1 - i, color);
1294        }
1295        return;
1296    }
1297    for i in 0..width {
1298        draw_border(screen, ox0 + i, oy0 + i, ox1 - i, oy1 - i, color);
1299    }
1300}
1301
1302/// <img> の object-fit / object-position を反映して画像を描画する。
1303/// background-image と異なりタイリングはしない(範囲外は透過のまま何も描かない)。
1304
1305/// `filter` 文字列から `drop-shadow(ox oy blur color)` を抽出し、box-shadow相当のタプルを返す。
1306pub(crate) fn parse_drop_shadow(filter: &str) -> Option<(i32, i32, i32, u32, bool)> {
1307    let lower = filter.to_ascii_lowercase();
1308    let start = lower.find("drop-shadow(")?;
1309    #[allow(clippy::string_slice)]
1310    let rest = &lower[start + 12..];
1311    let close = rest.find(')')?;
1312    #[allow(clippy::string_slice)]
1313    let arg = rest[..close].trim();
1314    
1315    let mut ox = 0i32;
1316    let mut oy = 0i32;
1317    let mut blur = 0i32;
1318    let mut shadow_color = 0x80000000u32;
1319    let mut px_count = 0;
1320    
1321    for token in arg.split_whitespace() {
1322        if let Some(n) = token.strip_suffix("px") {
1323            // `n.parse::<i32>()` は "1.5" のような小数値を拒否し、その場合トークン全体が
1324            // 無音のまま既定値0に落ちてしまう(outline-offset で見つけたのと同根のバグ)。
1325            // f32 として解析してから丸めることで小数px値も正しく反映する。
1326            if let Ok(n) = n.trim().parse::<f32>().map(|v| v as i32) {
1327                match px_count {
1328                    0 => { ox = n; px_count += 1; }
1329                    1 => { oy = n; px_count += 1; }
1330                    2 => { blur = n; px_count += 1; }
1331                    _ => {}
1332                }
1333            }
1334        } else if let Some(c) = crate::os_lib::css::parse_color(token) {
1335            shadow_color = c;
1336        }
1337    }
1338    
1339    if px_count >= 2 {
1340        Some((ox, oy, blur, shadow_color, false))
1341    } else {
1342        None
1343    }
1344}
1345
1346/// `img` の (src_row, src_col) を中心に半径 `radius`(source pixel 単位)のボックスブラーを
1347/// かけた色を返す。画像端はクランプせず範囲外を単純にスキップする(サンプル数で正規化)。
1348pub(crate) fn sample_box_blur(
1349    img: &super::DecodedImage,
1350    src_row: u32,
1351    src_col: u32,
1352    radius: i32,
1353) -> (u8, u8, u8) {
1354    let r = radius.clamp(1, 8);
1355    let (mut sr, mut sg, mut sb, mut n) = (0i64, 0i64, 0i64, 0i64);
1356    for dy in -r..=r {
1357        let ry = src_row as i32 + dy;
1358        if ry < 0 || ry as u32 >= img.height {
1359            continue;
1360        }
1361        for dx in -r..=r {
1362            let rx = src_col as i32 + dx;
1363            if rx < 0 || rx as u32 >= img.width {
1364                continue;
1365            }
1366            let idx = ((ry as u32 * img.width + rx as u32) as usize) * 4;
1367            if idx + 3 < img.rgba.len() {
1368                sr += img.rgba[idx] as i64;
1369                sg += img.rgba[idx + 1] as i64;
1370                sb += img.rgba[idx + 2] as i64;
1371                n += 1;
1372            }
1373        }
1374    }
1375    if n == 0 {
1376        return (0, 0, 0);
1377    }
1378    ((sr / n) as u8, (sg / n) as u8, (sb / n) as u8)
1379}
1380
1381/// 画面上の指定矩形領域をボックスブラーする(インプレースではなく一時バッファを使用)。
1382pub(crate) fn blur_screen_rect(
1383    screen: &crate::kernel::draw::Screen,
1384    x0: i32,
1385    y0: i32,
1386    x1: i32,
1387    y1: i32,
1388    radius: i32,
1389) {
1390    if radius <= 0 || x1 <= x0 || y1 <= y0 {
1391        return;
1392    }
1393    let r = radius.clamp(1, 8);
1394    let w = (x1 - x0) as usize;
1395    let h = (y1 - y0) as usize;
1396
1397    // 一時バッファに画面のピクセルを読み込む
1398    let mut buf = alloc::vec::Vec::with_capacity(w * h);
1399    for y in y0..y1 {
1400        for x in x0..x1 {
1401            buf.push(screen.read_pixel(x as u32, y as u32).0);
1402        }
1403    }
1404
1405    // 【2026-08-01 性能修正】仕様は `spec/backdrop_blur.md`。
1406    //
1407    // 以前はここで素朴な 2 次元ボックスブラー O(W×H×(2r+1)²) を回していた。
1408    // r は最大 8 なので 1 ピクセルあたり最大 289 サンプル。
1409    // sugi-lab.net のナビゲーションバー(924×44)で 1170 万回/フレームとなり、
1410    // **1 要素だけで 179〜478 tick** かかっていた(面積が 12 倍ある
1411    // 924×529 の要素より数倍重い、という形で実測に現れた)。
1412    //
1413    // 累積和による **O(W×H)**(半径に依存しない)実装へ置換。
1414    // 出力は素朴実装と**ビット単位で一致**する(B-1。単体試験で
1415    // 参照実装と総当たり比較して確認済み)。
1416    let blurred = super::blur_box::box_blur_rgb(&buf, w, h, r as usize);
1417
1418    for y in 0..h {
1419        let sy = y0 + y as i32;
1420        if sy < 0 || sy as u32 >= screen.height {
1421            continue;
1422        }
1423        for x in 0..w {
1424            let sx = x0 + x as i32;
1425            if sx < 0 || sx as u32 >= screen.width {
1426                continue;
1427            }
1428            screen.draw_pixel_alpha(
1429                sx as u32,
1430                sy as u32,
1431                crate::kernel::draw::Color(blurred[y * w + x]),
1432                255,
1433            );
1434        }
1435    }
1436}
1437
1438pub(crate) fn apply_backdrop_filters_to_screen_rect(
1439    screen: &crate::kernel::draw::Screen,
1440    x0: i32,
1441    y0: i32,
1442    x1: i32,
1443    y1: i32,
1444    backdrop_filter: &str,
1445) {
1446    if backdrop_filter.is_empty() || x1 <= x0 || y1 <= y0 {
1447        return;
1448    }
1449    let backdrop_blur_radius = parse_blur_radius(backdrop_filter);
1450    if backdrop_blur_radius > 0 {
1451        blur_screen_rect(screen, x0, y0, x1, y1, backdrop_blur_radius);
1452    }
1453    
1454    // Check if there are color filter functions (brightness, contrast, saturate, etc.)
1455    let tokens = parse_filter_tokens(backdrop_filter);
1456    let has_color_filters = tokens.iter().any(|(name, _)| name != "blur");
1457    if !has_color_filters {
1458        return;
1459    }
1460
1461    for sy in y0..y1 {
1462        if sy < 0 || sy as u32 >= screen.height { continue; }
1463        for sx in x0..x1 {
1464            if sx < 0 || sx as u32 >= screen.width { continue; }
1465            let orig = screen.read_pixel(sx as u32, sy as u32).0;
1466            let (r, g, b) = (((orig >> 16) & 0xFF) as u8, ((orig >> 8) & 0xFF) as u8, (orig & 0xFF) as u8);
1467            let (fr, fg, fb) = apply_css_filters(backdrop_filter, r, g, b);
1468            let new_col = 0xFF000000 | ((fr as u32) << 16) | ((fg as u32) << 8) | (fb as u32);
1469            screen.draw_pixel_alpha(sx as u32, sy as u32, crate::kernel::draw::Color(new_col), 255);
1470        }
1471    }
1472}
1473
1474/// `mix-blend-mode` / `background-blend-mode`: 描画しようとしている色(src)を、
1475/// 既にフレームバッファ上にある色(dst = 背景)と指定モードで合成する。
1476/// 対応: normal(そのまま)/multiply/screen/darken/lighten/difference/overlay/
1477/// hard-light/soft-light/exclusion/color-dodge/color-burn(チャネルごとの独立計算)と、
1478/// hue/saturation/color/luminosity(W3C Compositing 仕様の Lum/Sat/SetLum/SetSat
1479/// アルゴリズムに基づく非可分モード、3チャネルまとめて計算)。
1480pub(crate) fn blend_pixel(mode: &str, src: (u8, u8, u8), dst: (u8, u8, u8)) -> (u8, u8, u8) {
1481    if matches!(mode, "hue" | "saturation" | "color" | "luminosity") {
1482        return blend_non_separable(mode, src, dst);
1483    }
1484    if mode == "soft-light" {
1485        fn soft(s: u8, d: u8) -> u8 {
1486            let (cs, cb) = (s as f32 / 255.0, d as f32 / 255.0);
1487            let dd = if cb <= 0.25 {
1488                ((16.0 * cb - 12.0) * cb + 4.0) * cb
1489            } else {
1490                libm::sqrtf(cb)
1491            };
1492            let b = if cs <= 0.5 {
1493                cb - (1.0 - 2.0 * cs) * cb * (1.0 - cb)
1494            } else {
1495                cb + (2.0 * cs - 1.0) * (dd - cb)
1496            };
1497            (b.clamp(0.0, 1.0) * 255.0) as u8
1498        }
1499        return (soft(src.0, dst.0), soft(src.1, dst.1), soft(src.2, dst.2));
1500    }
1501    fn ch(mode: &str, s: u8, d: u8) -> u8 {
1502        let (sf, df) = (s as i32, d as i32);
1503        match mode {
1504            "multiply" => (sf * df / 255) as u8,
1505            "screen" => (255 - (255 - sf) * (255 - df) / 255) as u8,
1506            "darken" => sf.min(df) as u8,
1507            "lighten" => sf.max(df) as u8,
1508            "difference" => (sf - df).unsigned_abs() as u8,
1509            "overlay" => {
1510                if df <= 127 {
1511                    (2 * sf * df / 255).clamp(0, 255) as u8
1512                } else {
1513                    (255 - 2 * (255 - sf) * (255 - df) / 255).clamp(0, 255) as u8
1514                }
1515            }
1516            "hard-light" => {
1517                if sf <= 127 {
1518                    (2 * sf * df / 255).clamp(0, 255) as u8
1519                } else {
1520                    (255 - 2 * (255 - sf) * (255 - df) / 255).clamp(0, 255) as u8
1521                }
1522            }
1523            "exclusion" => (sf + df - 2 * sf * df / 255).clamp(0, 255) as u8,
1524            "color-dodge" => {
1525                if df == 0 {
1526                    0
1527                } else if sf >= 255 {
1528                    255
1529                } else {
1530                    (255 * df / (255 - sf)).min(255) as u8
1531                }
1532            }
1533            "color-burn" => {
1534                if df >= 255 {
1535                    255
1536                } else if sf == 0 {
1537                    0
1538                } else {
1539                    (255 - (255 * (255 - df) / sf).min(255)) as u8
1540                }
1541            }
1542            _ => s,
1543        }
1544    }
1545    (
1546        ch(mode, src.0, dst.0),
1547        ch(mode, src.1, dst.1),
1548        ch(mode, src.2, dst.2),
1549    )
1550}
1551
1552/// hue/saturation/color/luminosity(非可分ブレンドモード)。W3C Compositing and
1553/// Blending Level 1 の Lum/ClipColor/SetLum/Sat/SetSat アルゴリズムをそのまま実装する。
1554/// `dst` = backdrop(Cb)、`src` = source(Cs)。
1555fn blend_non_separable(mode: &str, src: (u8, u8, u8), dst: (u8, u8, u8)) -> (u8, u8, u8) {
1556    fn to_f(c: (u8, u8, u8)) -> (f32, f32, f32) {
1557        (c.0 as f32 / 255.0, c.1 as f32 / 255.0, c.2 as f32 / 255.0)
1558    }
1559    fn to_u8(c: (f32, f32, f32)) -> (u8, u8, u8) {
1560        (
1561            (c.0.clamp(0.0, 1.0) * 255.0) as u8,
1562            (c.1.clamp(0.0, 1.0) * 255.0) as u8,
1563            (c.2.clamp(0.0, 1.0) * 255.0) as u8,
1564        )
1565    }
1566    fn lum(c: (f32, f32, f32)) -> f32 {
1567        0.3 * c.0 + 0.59 * c.1 + 0.11 * c.2
1568    }
1569    fn clip_color(c: (f32, f32, f32)) -> (f32, f32, f32) {
1570        let l = lum(c);
1571        let n = c.0.min(c.1).min(c.2);
1572        let x = c.0.max(c.1).max(c.2);
1573        let mut c = c;
1574        if n < 0.0 && l != n {
1575            c = (
1576                l + (c.0 - l) * l / (l - n),
1577                l + (c.1 - l) * l / (l - n),
1578                l + (c.2 - l) * l / (l - n),
1579            );
1580        }
1581        if x > 1.0 && x != l {
1582            c = (
1583                l + (c.0 - l) * (1.0 - l) / (x - l),
1584                l + (c.1 - l) * (1.0 - l) / (x - l),
1585                l + (c.2 - l) * (1.0 - l) / (x - l),
1586            );
1587        }
1588        c
1589    }
1590    fn set_lum(c: (f32, f32, f32), l: f32) -> (f32, f32, f32) {
1591        let d = l - lum(c);
1592        clip_color((c.0 + d, c.1 + d, c.2 + d))
1593    }
1594    fn sat(c: (f32, f32, f32)) -> f32 {
1595        c.0.max(c.1).max(c.2) - c.0.min(c.1).min(c.2)
1596    }
1597    fn set_sat(c: (f32, f32, f32), s: f32) -> (f32, f32, f32) {
1598        let mut v = [c.0, c.1, c.2];
1599        let (mut lo, mut mid, mut hi) = (0usize, 1usize, 2usize);
1600        // インデックスを値の昇順(lo<=mid<=hi)に並べ替える。
1601        if v[lo] > v[mid] {
1602            core::mem::swap(&mut lo, &mut mid);
1603        }
1604        if v[mid] > v[hi] {
1605            core::mem::swap(&mut mid, &mut hi);
1606        }
1607        if v[lo] > v[mid] {
1608            core::mem::swap(&mut lo, &mut mid);
1609        }
1610        if v[hi] > v[lo] {
1611            v[mid] = (v[mid] - v[lo]) * s / (v[hi] - v[lo]);
1612            v[hi] = s;
1613        } else {
1614            v[mid] = 0.0;
1615            v[hi] = 0.0;
1616        }
1617        v[lo] = 0.0;
1618        (v[0], v[1], v[2])
1619    }
1620    let (cb, cs) = (to_f(dst), to_f(src));
1621    let result = match mode {
1622        "hue" => set_lum(set_sat(cs, sat(cb)), lum(cb)),
1623        "saturation" => set_lum(set_sat(cb, sat(cs)), lum(cb)),
1624        "color" => set_lum(cs, lum(cb)),
1625        "luminosity" => set_lum(cb, lum(cs)),
1626        _ => cs,
1627    };
1628    to_u8(result)
1629}
1630
1631/// `list-style-image` 用の小さな画像を (x0,y0) を左上として dst_w x dst_h へ最近傍スケーリング
1632/// して描画する。クリップ・object-fit 等は考慮しない簡易実装(マーカーサイズのアイコン用途)。
1633pub(super) fn draw_image_nn_blit(
1634    screen: &crate::kernel::draw::Screen,
1635    img: &super::DecodedImage,
1636    x0: i32,
1637    y0: i32,
1638    dst_w: u32,
1639    dst_h: u32,
1640) {
1641    if img.width == 0 || img.height == 0 || dst_w == 0 || dst_h == 0 {
1642        return;
1643    }
1644    for row in 0..dst_h {
1645        let src_row = (row * img.height) / dst_h;
1646        for col in 0..dst_w {
1647            let src_col = (col * img.width) / dst_w;
1648            let idx = ((src_row * img.width + src_col) as usize) * 4;
1649            if idx + 4 <= img.rgba.len() {
1650                let r = img.rgba[idx];
1651                let g = img.rgba[idx + 1];
1652                let b = img.rgba[idx + 2];
1653                let a = img.rgba[idx + 3];
1654                let px = x0 + col as i32;
1655                let py = y0 + row as i32;
1656                if px >= 0 && py >= 0 {
1657                    let color = crate::kernel::draw::Color(
1658                        0xFF000000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32),
1659                    );
1660                    screen.draw_pixel_alpha(px as u32, py as u32, color, a);
1661                }
1662            }
1663        }
1664    }
1665}
1666
1667/// 計算量: **O(Wd×Hd)** — Wd,Hd は描画先サイズ。
1668/// 1 ピクセルごとに元画像をサンプリングする(`sample_bilinear_rgba` 使用時は
1669/// 1 ピクセルあたり 4 点読み出し+補間で定数倍が 4 倍になる)。
1670pub(super) fn draw_img_object_fit(
1671    screen: &crate::kernel::draw::Screen,
1672    img: &super::DecodedImage,
1673    el: &super::RenderElement,
1674    x0_rect: i32,
1675    y0_rect: i32,
1676    clip_s: &Option<ClipShape>,
1677) {
1678    use super::ObjectFit;
1679    let box_w = el.width.max(1) as u32;
1680    let box_h = el.height.max(1) as u32;
1681    if img.width == 0 || img.height == 0 {
1682        return;
1683    }
1684
1685    let (draw_w, draw_h): (u32, u32) = match el.object_fit {
1686        ObjectFit::Fill => (box_w, box_h),
1687        ObjectFit::Cover => {
1688            let scale = (box_w as f32 / img.width as f32).max(box_h as f32 / img.height as f32);
1689            (
1690                ((img.width as f32) * scale).max(1.0) as u32,
1691                ((img.height as f32) * scale).max(1.0) as u32,
1692            )
1693        }
1694        ObjectFit::Contain => {
1695            let scale = (box_w as f32 / img.width as f32).min(box_h as f32 / img.height as f32);
1696            (
1697                ((img.width as f32) * scale).max(1.0) as u32,
1698                ((img.height as f32) * scale).max(1.0) as u32,
1699            )
1700        }
1701        ObjectFit::None => (img.width, img.height),
1702        ObjectFit::ScaleDown => {
1703            let contain_scale =
1704                (box_w as f32 / img.width as f32).min(box_h as f32 / img.height as f32);
1705            let scale = contain_scale.min(1.0);
1706            (
1707                ((img.width as f32) * scale).max(1.0) as u32,
1708                ((img.height as f32) * scale).max(1.0) as u32,
1709            )
1710        }
1711    };
1712
1713    let off_x = ((box_w as i32 - draw_w as i32) as f32 * el.object_pos_x) as i32;
1714    let off_y = ((box_h as i32 - draw_h as i32) as f32 * el.object_pos_y) as i32;
1715    let blur_radius = parse_blur_radius(&el.filter);
1716
1717    let is_pixelated = el.image_rendering == "pixelated" || el.image_rendering == "crisp-edges";
1718    let use_nearest = is_pixelated || (draw_w == img.width && draw_h == img.height);
1719    let col_src_lut: alloc::vec::Vec<u32> = if use_nearest {
1720        (0..draw_w).map(|c| (c * img.width) / draw_w.max(1)).collect()
1721    } else {
1722        alloc::vec![]
1723    };
1724
1725    let mut row_buf = alloc::vec![0u32; draw_w as usize];
1726
1727    for row in 0..draw_h {
1728        let py = row as i32 + off_y;
1729        if py < 0 || py as u32 >= box_h {
1730            continue;
1731        }
1732        let src_row = (row * img.height) / draw_h.max(1);
1733        for col in 0..draw_w {
1734            let px = col as i32 + off_x;
1735            if px < 0 || px as u32 >= box_w {
1736                row_buf[col as usize] = 0;
1737                continue;
1738            }
1739            if let Some(ref cs) = clip_s {
1740                if !clip_inside(cs, px, py) {
1741                    row_buf[col as usize] = 0;
1742                    continue;
1743                }
1744            }
1745
1746            let (raw_r, raw_g, raw_b, a) = if use_nearest {
1747                let src_col = col_src_lut[col as usize];
1748                let idx = ((src_row * img.width + src_col) as usize) * 4;
1749                if idx + 4 <= img.rgba.len() {
1750                    let (br_r, br_g, br_b) = if blur_radius > 0 {
1751                        sample_box_blur(img, src_row, src_col, blur_radius)
1752                    } else {
1753                        (img.rgba[idx], img.rgba[idx + 1], img.rgba[idx + 2])
1754                    };
1755                    (br_r, br_g, br_b, img.rgba[idx + 3])
1756                } else {
1757                    (0, 0, 0, 0)
1758                }
1759            } else {
1760                let u = ((col as f32) + 0.5) * (img.width as f32) / (draw_w as f32) - 0.5;
1761                let v = ((row as f32) + 0.5) * (img.height as f32) / (draw_h as f32) - 0.5;
1762                sample_bilinear_rgba(img, u, v)
1763            };
1764            if a == 0 {
1765                row_buf[col as usize] = 0;
1766                continue;
1767            }
1768            let (r, g, b) = apply_css_filters(&el.filter, raw_r, raw_g, raw_b);
1769            row_buf[col as usize] = ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
1770        }
1771
1772        screen.draw_scanline_colors(x0_rect + off_x, y0_rect + py, &row_buf);
1773    }
1774}
1775