Skip to main content

atmos/os_lib/web_engine/
clip_path.rs

1// arm/v1/src/os_lib/web_engine/clip_path.rs
2//! CSS `clip-path` 解析および内外判定・幾何計算モジュール。
3//!
4//! W3C CSS Masking Module Level 1 準拠:
5//! - 基本形状: `circle()`, `ellipse()`, `inset()`, `polygon()`
6//! - 単位解決: `px`, `%`, `rem` (16px), `em` (`font_px`), `pt` (4/3px), 裸数値
7//! - キーワード位置: `left`, `top`, `right`, `bottom`, `center`
8//! - `inset()` の角丸半径(`round` 構文)
9//! - `polygon()` の fill-rule(`nonzero`, `evenodd`)
10
11use alloc::vec::Vec;
12use crate::os_lib::canvas2d::{is_point_in_path, parse_svg_path_into, FillRule, Matrix, PathBuilder, SubPath};
13
14#[derive(Clone, Debug, PartialEq)]
15pub enum ClipShape {
16    Circle { cx: i32, cy: i32, r_sq: i64 },
17    Ellipse { cx: i32, cy: i32, rx: i32, ry: i32 },
18    Inset { x0: i32, y0: i32, x1: i32, y1: i32, radius: (i32, i32, i32, i32) },
19    Polygon { points: Vec<(i32, i32)>, nonzero: bool },
20    Path { subpaths: Vec<SubPath>, nonzero: bool },
21}
22
23
24/// Parse CSS `clip-path` value into pixel coordinates relative to the element.
25/// `w` and `h` are the element's rendered width and height in pixels.
26pub fn parse_clip_shape(css: &str, w: i32, h: i32) -> Option<ClipShape> {
27    parse_clip_shape_with_font(css, w, h, 16.0)
28}
29
30/// Parse CSS `clip-path` value with element font size for resolving `em` units.
31pub fn parse_clip_shape_with_font(css: &str, w: i32, h: i32, font_px: f32) -> Option<ClipShape> {
32    let s = css.trim();
33
34    if let Some(inner) = s.strip_prefix("circle(").and_then(|t| t.strip_suffix(')')) {
35        let mut parts = inner.splitn(2, "at");
36        let r_str = parts.next().unwrap_or("50%").trim();
37        let r = clip_len_with_font(r_str, w.min(h), font_px) as i32;
38        let (cx, cy) = if let Some(at) = parts.next() {
39            let mut ps = at.split_whitespace();
40            let cx = ps.next().map(|sv| clip_pos_with_font(sv, w, font_px)).unwrap_or(w / 2);
41            let cy = ps.next().map(|sv| clip_pos_with_font(sv, h, font_px)).unwrap_or(h / 2);
42            (cx, cy)
43        } else {
44            (w / 2, h / 2)
45        };
46        return Some(ClipShape::Circle { cx, cy, r_sq: (r as i64) * (r as i64) });
47    }
48
49    if let Some(inner) = s.strip_prefix("ellipse(").and_then(|t| t.strip_suffix(')')) {
50        let mut parts = inner.splitn(2, "at");
51        let radii_str = parts.next().unwrap_or("50% 50%").trim();
52        let mut radii = radii_str.split_whitespace();
53        let rx = radii.next().map(|sv| clip_len_with_font(sv, w, font_px) as i32).unwrap_or(w / 2);
54        let ry = radii.next().map(|sv| clip_len_with_font(sv, h, font_px) as i32).unwrap_or(h / 2);
55        let (cx, cy) = if let Some(at) = parts.next() {
56            let mut ps = at.split_whitespace();
57            let cx = ps.next().map(|sv| clip_pos_with_font(sv, w, font_px)).unwrap_or(w / 2);
58            let cy = ps.next().map(|sv| clip_pos_with_font(sv, h, font_px)).unwrap_or(h / 2);
59            (cx, cy)
60        } else {
61            (w / 2, h / 2)
62        };
63        return Some(ClipShape::Ellipse { cx, cy, rx, ry });
64    }
65
66    if let Some(inner) = s.strip_prefix("inset(").and_then(|t| t.strip_suffix(')')) {
67        let mut parts = inner.split("round");
68        let body = parts.next().unwrap_or(inner).trim();
69        let round_part = parts.next().map(|s| s.trim());
70        let mut vals = body.split_whitespace().map(|sv| clip_len_with_font(sv, w.max(h), font_px) as i32);
71        let top    = vals.next().unwrap_or(0);
72        let right  = vals.next().unwrap_or(top);
73        let bottom = vals.next().unwrap_or(top);
74        let left   = vals.next().unwrap_or(right);
75
76        let radius = if let Some(r_str) = round_part {
77            let mut r_vals = r_str.split_whitespace().map(|sv| clip_len_with_font(sv, w.min(h), font_px) as i32);
78            let r_tl = r_vals.next().unwrap_or(0);
79            let r_tr = r_vals.next().unwrap_or(r_tl);
80            let r_br = r_vals.next().unwrap_or(r_tl);
81            let r_bl = r_vals.next().unwrap_or(r_tr);
82            (r_tl, r_tr, r_br, r_bl)
83        } else {
84            (0, 0, 0, 0)
85        };
86
87        return Some(ClipShape::Inset { x0: left, y0: top, x1: w - right, y1: h - bottom, radius });
88    }
89
90    if let Some(inner) = s.strip_prefix("polygon(").and_then(|t| t.strip_suffix(')')) {
91        let mut nonzero = false;
92        let mut pairs_src = inner;
93        if let Some(rest) = inner.trim_start().strip_prefix("nonzero") {
94            nonzero = true;
95            pairs_src = rest.trim_start().strip_prefix(',').unwrap_or(rest);
96        } else if let Some(rest) = inner.trim_start().strip_prefix("evenodd") {
97            pairs_src = rest.trim_start().strip_prefix(',').unwrap_or(rest);
98        }
99        let mut points = Vec::new();
100        for pair in pairs_src.split(',') {
101            let mut coords = pair.split_whitespace();
102            if let (Some(xs), Some(ys)) = (coords.next(), coords.next()) {
103                points.push((clip_len_with_font(xs, w, font_px) as i32, clip_len_with_font(ys, h, font_px) as i32));
104            }
105        }
106        if points.len() >= 3 {
107            return Some(ClipShape::Polygon { points, nonzero });
108        }
109    }
110
111    if let Some(inner) = s.strip_prefix("path(").and_then(|t| t.strip_suffix(')')) {
112        let trimmed = inner.trim();
113        let (nonzero, path_str) = if let Some(rest) = trimmed.strip_prefix("nonzero") {
114            let after_rule = rest.trim_start().strip_prefix(',').unwrap_or(rest).trim();
115            (true, after_rule)
116        } else if let Some(rest) = trimmed.strip_prefix("evenodd") {
117            let after_rule = rest.trim_start().strip_prefix(',').unwrap_or(rest).trim();
118            (false, after_rule)
119        } else {
120            (true, trimmed)
121        };
122
123        let d = if (path_str.starts_with('"') && path_str.ends_with('"'))
124            || (path_str.starts_with('\'') && path_str.ends_with('\''))
125        {
126            path_str.get(1..path_str.len().checked_sub(1)?)?
127        } else {
128            return None;
129        };
130
131        if d.trim().is_empty() {
132            return None;
133        }
134
135        let mut builder = PathBuilder::new();
136        parse_svg_path_into(d, &mut builder, &Matrix::identity());
137        let subpaths = builder.finish();
138        if !subpaths.is_empty() {
139            return Some(ClipShape::Path { subpaths, nonzero });
140        }
141    }
142
143    None
144}
145
146/// Returns `true` if point `(ox, oy)` in element-local pixels is inside the clip shape.
147/// 計算量: **O(V)** — V は多角形の頂点数(`polygon()` の場合)。
148/// 円・楕円・矩形は O(1)。
149pub fn clip_inside(shape: &ClipShape, ox: i32, oy: i32) -> bool {
150    match shape {
151        ClipShape::Circle { cx, cy, r_sq } => {
152            let dx = (ox - cx) as i64;
153            let dy = (oy - cy) as i64;
154            dx * dx + dy * dy <= *r_sq
155        }
156        ClipShape::Ellipse { cx, cy, rx, ry } => {
157            if *rx == 0 || *ry == 0 { return false; }
158            let dx = (ox - cx) as f32 / (*rx as f32);
159            let dy = (oy - cy) as f32 / (*ry as f32);
160            dx * dx + dy * dy <= 1.0
161        }
162        ClipShape::Inset { x0, y0, x1, y1, radius } => {
163            if ox >= *x0 && ox < *x1 && oy >= *y0 && oy < *y1 {
164                if *radius == (0, 0, 0, 0) {
165                    true
166                } else {
167                    let rx = ox - *x0;
168                    let ry = oy - *y0;
169                    let iw = *x1 - *x0;
170                    let ih = *y1 - *y0;
171                    point_in_rounded_rect(rx, ry, iw, ih, *radius)
172                }
173            } else {
174                false
175            }
176        }
177        ClipShape::Polygon { points, nonzero } => {
178            if *nonzero {
179                polygon_contains_nonzero(points, ox, oy)
180            } else {
181                polygon_contains(points, ox, oy)
182            }
183        }
184        ClipShape::Path { subpaths, nonzero } => {
185            let rule = if *nonzero { FillRule::NonZero } else { FillRule::EvenOdd };
186            is_point_in_path(subpaths, ox as f32 + 0.5, oy as f32 + 0.5, rule)
187        }
188    }
189}
190
191
192/// even-odd 規則(既定のfill-rule)での内外判定。自己交差の無い多角形では
193/// nonzero と同じ結果になるが、自己交差する多角形では領域の解釈が異なる。
194fn polygon_contains(points: &[(i32, i32)], px: i32, py: i32) -> bool {
195    let n = points.len();
196    if n < 3 { return false; }
197    let mut inside = false;
198    let mut j = n - 1;
199    for i in 0..n {
200        let (xi, yi) = points[i];
201        let (xj, yj) = points[j];
202        if (yi > py) != (yj > py) && yj != yi {
203            let x_intersect = (xj - xi) * (py - yi) / (yj - yi) + xi;
204            if px < x_intersect {
205                inside = !inside;
206            }
207        }
208        j = i;
209    }
210    inside
211}
212
213/// nonzero 規則(`clip-path: polygon(nonzero, ...)`)での内外判定。各辺との交差方向
214/// (上向き/下向き)を winding number として加減算し、合計が0でなければ内側とする。
215fn polygon_contains_nonzero(points: &[(i32, i32)], px: i32, py: i32) -> bool {
216    let n = points.len();
217    if n < 3 { return false; }
218    let mut winding = 0i32;
219    let mut j = n - 1;
220    for i in 0..n {
221        let (xi, yi) = points[i];
222        let (xj, yj) = points[j];
223        if yj <= py {
224            if yi > py {
225                let cross = (xi - xj) * (py - yj) - (px - xj) * (yi - yj);
226                if cross > 0 {
227                    winding += 1;
228                }
229            }
230        } else if yi <= py {
231            let cross = (xi - xj) * (py - yj) - (px - xj) * (yi - yj);
232            if cross < 0 {
233                winding -= 1;
234            }
235        }
236        j = i;
237    }
238    winding != 0
239}
240
241pub fn clip_pos(s: &str, base: i32) -> i32 {
242    clip_pos_with_font(s, base, 16.0)
243}
244
245pub fn clip_pos_with_font(s: &str, base: i32, font_px: f32) -> i32 {
246    match s.trim().to_ascii_lowercase().as_str() {
247        "left" | "top" => 0,
248        "right" | "bottom" => base,
249        "center" => base / 2,
250        _ => clip_len_with_font(s, base, font_px) as i32,
251    }
252}
253
254pub fn clip_len(s: &str, base: i32) -> f32 {
255    clip_len_with_font(s, base, 16.0)
256}
257
258pub fn clip_len_with_font(s: &str, base: i32, font_px: f32) -> f32 {
259    let s = s.trim();
260    let font = if font_px > 0.0 { font_px } else { 16.0 };
261    if let Some(pct) = s.strip_suffix('%') {
262        pct.trim().parse::<f32>().unwrap_or(50.0) * base as f32 / 100.0
263    } else if let Some(rem) = s.strip_suffix("rem") {
264        rem.trim().parse::<f32>().unwrap_or(0.0) * 16.0
265    } else if let Some(em) = s.strip_suffix("em") {
266        em.trim().parse::<f32>().unwrap_or(0.0) * font
267    } else if let Some(px) = s.strip_suffix("px") {
268        px.trim().parse::<f32>().unwrap_or(0.0)
269    } else if let Some(pt) = s.strip_suffix("pt") {
270        pt.trim().parse::<f32>().unwrap_or(0.0) * 4.0 / 3.0
271    } else {
272        s.parse::<f32>().unwrap_or(0.0)
273    }
274}
275
276pub fn point_in_rounded_rect(px: i32, py: i32, w: i32, h: i32, r: (i32, i32, i32, i32)) -> bool {
277    if px < 0 || py < 0 || px >= w || py >= h {
278        return false;
279    }
280    
281    let tl = r.0.min(w / 2).min(h / 2).max(0);
282    let tr = r.1.min(w / 2).min(h / 2).max(0);
283    let br = r.2.min(w / 2).min(h / 2).max(0);
284    let bl = r.3.min(w / 2).min(h / 2).max(0);
285
286    if tl > 0 && px < tl && py < tl {
287        let dx = (px - tl) as i64;
288        let dy = (py - tl) as i64;
289        return dx * dx + dy * dy <= (tl as i64) * (tl as i64);
290    }
291    if tr > 0 && px >= w - tr && py < tr {
292        let dx = (px - (w - tr)) as i64;
293        let dy = (py - tr) as i64;
294        return dx * dx + dy * dy <= (tr as i64) * (tr as i64);
295    }
296    if bl > 0 && px < bl && py >= h - bl {
297        let dx = (px - bl) as i64;
298        let dy = (py - (h - bl)) as i64;
299        return dx * dx + dy * dy <= (bl as i64) * (bl as i64);
300    }
301    if br > 0 && px >= w - br && py >= h - br {
302        let dx = (px - (w - br)) as i64;
303        let dy = (py - (h - br)) as i64;
304        return dx * dx + dy * dy <= (br as i64) * (br as i64);
305    }
306    true
307}