Skip to main content

atmos/os_lib/web_engine/
draw_helpers.rs

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