Skip to main content

atmos/os_lib/css/
animation.rs

1// 分割: css.rs より機械的に移動(2026-07-16 リファクタ フェーズ3)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// イージング関数。CSS の transition-timing-function / animation-timing-function。
6#[derive(Debug, Clone, PartialEq)]
7pub enum TimingFunction {
8    Linear,
9    /// 3次ベジェ制御点 (x1, y1, x2, y2)。ease/ease-in/out/in-out もこれで表現。
10    CubicBezier(f32, f32, f32, f32),
11    /// steps(n, jump-start|jump-end)。bool=jump_start。
12    Steps(u32, bool),
13}
14
15impl TimingFunction {
16    /// CSS のキーワード/関数表記をパース。
17    pub fn parse(s: &str) -> TimingFunction {
18        let s = s.trim().to_lowercase();
19        match s.as_str() {
20            "linear" => TimingFunction::Linear,
21            "ease" => TimingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0),
22            "ease-in" => TimingFunction::CubicBezier(0.42, 0.0, 1.0, 1.0),
23            "ease-out" => TimingFunction::CubicBezier(0.0, 0.0, 0.58, 1.0),
24            "ease-in-out" => TimingFunction::CubicBezier(0.42, 0.0, 0.58, 1.0),
25            "step-start" => TimingFunction::Steps(1, true),
26            "step-end" => TimingFunction::Steps(1, false),
27            _ => {
28                if let Some(inner) = s
29                    .strip_prefix("cubic-bezier(")
30                    .and_then(|x| x.strip_suffix(')'))
31                {
32                    let nums: Vec<f32> = inner
33                        .split(',')
34                        .filter_map(|n| n.trim().parse::<f32>().ok())
35                        .collect();
36                    if nums.len() == 4 {
37                        return TimingFunction::CubicBezier(nums[0], nums[1], nums[2], nums[3]);
38                    }
39                } else if let Some(inner) =
40                    s.strip_prefix("steps(").and_then(|x| x.strip_suffix(')'))
41                {
42                    let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
43                    let n = parts
44                        .first()
45                        .and_then(|p| p.parse::<u32>().ok())
46                        .unwrap_or(1);
47                    let jump_start = parts.get(1).map(|p| p.contains("start")).unwrap_or(false);
48                    return TimingFunction::Steps(n.max(1), jump_start);
49                }
50                // 不明な指定は ease にフォールバック。
51                TimingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0)
52            }
53        }
54    }
55
56    /// 線形進捗 t(0..=1) をイージング後の進捗に変換する。
57    pub fn ease(&self, t: f32) -> f32 {
58        let t = t.clamp(0.0, 1.0);
59        match self {
60            TimingFunction::Linear => t,
61            TimingFunction::CubicBezier(x1, y1, x2, y2) => {
62                cubic_bezier_solve(*x1, *y1, *x2, *y2, t)
63            }
64            TimingFunction::Steps(n, jump_start) => {
65                let n = *n as f32;
66                let step = libm_floor(t * n);
67                let v = if *jump_start {
68                    (step + 1.0) / n
69                } else {
70                    step / n
71                };
72                v.clamp(0.0, 1.0)
73            }
74        }
75    }
76}
77
78/// 3次ベジェ y(x=t) を解く。x からニュートン法で媒介変数 u を求め y(u) を返す。
79pub(crate) fn cubic_bezier_solve(x1: f32, y1: f32, x2: f32, y2: f32, x: f32) -> f32 {
80    // ベジェ成分: B(u) = 3(1-u)^2 u * p1 + 3(1-u) u^2 * p2 + u^3。
81    fn sample(p1: f32, p2: f32, u: f32) -> f32 {
82        let one = 1.0 - u;
83        3.0 * one * one * u * p1 + 3.0 * one * u * u * p2 + u * u * u
84    }
85    fn sample_deriv(p1: f32, p2: f32, u: f32) -> f32 {
86        let one = 1.0 - u;
87        3.0 * one * one * p1 + 6.0 * one * u * (p2 - p1) + 3.0 * u * u * (1.0 - p2)
88    }
89    if x <= 0.0 {
90        return 0.0;
91    }
92    if x >= 1.0 {
93        return 1.0;
94    }
95    // ニュートン法で x = sample_x(u) を満たす u を探索。
96    let mut u = x;
97    for _ in 0..8 {
98        let x_est = sample(x1, x2, u) - x;
99        let d = sample_deriv(x1, x2, u);
100        if d.abs() < 1e-6 {
101            break;
102        }
103        u -= x_est / d;
104        u = u.clamp(0.0, 1.0);
105    }
106    sample(y1, y2, u)
107}
108
109/// 2つの CSS 値を進捗 t で補間する。px/数値/%/色/`<n>deg`/複数値(空白区切り)に対応。
110/// 補間できない場合は t<0.5 で from、それ以外で to を返す(discrete)。
111pub fn interpolate_value(from: &str, to: &str, t: f32) -> String {
112    let from = from.trim();
113    let to = to.trim();
114    if from == to {
115        return from.to_string();
116    }
117    // 色(#hex / rgb / rgba / hsl / 名前)。
118    if let (Some(c1), Some(c2)) = (parse_color(from), parse_color(to)) {
119        return interpolate_color(c1, c2, t);
120    }
121    // `translate(10px, 20px)` / `matrix(a,b,c,d,e,f)` のような、値全体が単一の関数呼び出しで
122    // 引数がカンマ区切りのケースを先に処理する。以前はこのチェックより先に空白区切りの
123    // 複数トークン判定(`margin: 1px 2px` 用)が走っており、`translate(10px, 20px)` の
124    // カンマ後の空白が余分な「トークン」を作ってしまい(`["translate(10px,", "20px)"]`)、
125    // 各引数が独立に正しく補間されず途中で離散的にジャンプするバグがあった
126    // (`translateX(10px)` のような引数無しカンマの単一関数は元々正しく動いていた)。
127    if let (Some(f1), Some(f2)) = (as_single_wrapped_call(from), as_single_wrapped_call(to)) {
128        if f1.0 == f2.0 {
129            if let Some(shape_res) = try_interpolate_basic_shape(&f1.0, &f1.1, &f2.1, t) {
130                return shape_res;
131            }
132            let inner = if f1.1.contains(',') || f2.1.contains(',') {
133                let a1 = split_top_level_commas(&f1.1);
134                let a2 = split_top_level_commas(&f2.1);
135                if a1.len() == a2.len() {
136                    a1.iter()
137                        .zip(a2.iter())
138                        .map(|(a, b)| interpolate_value(a, b, t))
139                        .collect::<Vec<String>>()
140                        .join(", ")
141                } else {
142                    return if t < 0.5 { from.to_string() } else { to.to_string() };
143                }
144            } else {
145                interpolate_value(&f1.1, &f2.1, t)
146            };
147            return alloc::format!("{}({})", f1.0, inner);
148        }
149    }
150    // `box-shadow`/`text-shadow` のようなカンマ区切りの複数レイヤー値。以前はここより先に
151    // 空白区切りトークン判定が走っており、カンマ後の空白によるトークン化で
152    // レイヤー境界を跨いだ位置ベースの補間(例: "red," と "blue," のような壊れたトークン同士)
153    // になってしまい、色として解析できず discrete フォールバックに落ちる/レイヤー数が
154    // 食い違うと丸ごとジャンプするバグがあった(`translate(x, y)` の引数分割で見つけたのと
155    // 同種)。トップレベル(括弧の外)にカンマがある場合はレイヤー単位で分割・補間する。
156    let from_layers = split_top_level_commas(from);
157    let to_layers = split_top_level_commas(to);
158    if from_layers.len() > 1 || to_layers.len() > 1 {
159        if from_layers.len() == to_layers.len() {
160            return from_layers
161                .iter()
162                .zip(to_layers.iter())
163                .map(|(a, b)| interpolate_value(a, b, t))
164                .collect::<Vec<String>>()
165                .join(", ");
166        }
167        return if t < 0.5 { from.to_string() } else { to.to_string() };
168    }
169    // 複数トークン(margin: 1px 2px, transform: translateX(..) rotate(..) 等)。
170    let ft: Vec<&str> = from.split_whitespace().collect();
171    let tt: Vec<&str> = to.split_whitespace().collect();
172    if ft.len() > 1 || tt.len() > 1 {
173        if ft.len() == tt.len() {
174            let parts: Vec<String> = ft
175                .iter()
176                .zip(tt.iter())
177                .map(|(a, b)| interpolate_value(a, b, t))
178                .collect();
179            return parts.join(" ");
180        }
181        return if t < 0.5 {
182            from.to_string()
183        } else {
184            to.to_string()
185        };
186    }
187    // translateX(10px) 等の単一関数(引数無しカンマ): 内側を補間。
188    if let (Some(f1), Some(f2)) = (split_fn(from), split_fn(to)) {
189        if f1.0 == f2.0 {
190            let inner = interpolate_value(&f1.1, &f2.1, t);
191            return alloc::format!("{}({})", f1.0, inner);
192        }
193    }
194    // 単位付き数値 / 純数値。CSS仕様上、長さ/百分率等の値では単位無しの`0`は
195    // どの単位の文脈でも許容される(例: 実サイト www.sugi-lab.net のタイピング
196    // 演出`@keyframes typing { from{width:0} to{width:100%} }`)。以前はこの
197    // ケースを`u1 == u2`の厳密一致でのみ判定していたため、`0`(単位無し)と
198    // `100%`(%単位)が「単位不一致」とみなされ補間できず、アニメーションが
199    // 滑らかに変化せず離散的にジャンプするバグだった(2026-07-22発見・修正)。
200    if let (Some((n1, u1)), Some((n2, u2))) = (split_num_unit(from), split_num_unit(to)) {
201        let units_compatible =
202            u1 == u2 || (u1.is_empty() && n1 == 0.0) || (u2.is_empty() && n2 == 0.0);
203        if units_compatible {
204            let unit = if u1.is_empty() { u2 } else { u1 };
205            let v = n1 + (n2 - n1) * t;
206            if unit.is_empty() {
207                return fmt_num(v);
208            }
209            return alloc::format!("{}{}", fmt_num(v), unit);
210        }
211    }
212    // 補間不能 → discrete。
213    if t < 0.5 {
214        from.to_string()
215    } else {
216        to.to_string()
217    }
218}
219
220/// 文字列全体が単一の関数呼び出し(`name(...)`)かどうかを判定し、そうであれば
221/// ("name", "args") を返す。`split_fn` と違い、括弧の対応を実際に追跡して
222/// 「その関数呼び出しの閉じ括弧が文字列の末尾ちょうどにある」ことまで確認するため、
223/// `translateX(10px) rotate(20deg)`(空白区切りの複数関数)を誤って単一関数として
224/// 誤認しない(`split_fn` は最初の `(` と末尾の `)` を機械的に拾うだけなので、この判定には使えない)。
225pub(crate) fn as_single_wrapped_call(s: &str) -> Option<(String, String)> {
226    let s = s.trim();
227    let open = s.find('(')?;
228    let name = s.get(..open)?.trim();
229    if name.is_empty() || name.contains(char::is_whitespace) {
230        return None;
231    }
232    let bytes = s.as_bytes();
233    let mut depth = 0i32;
234    let mut close_idx = None;
235    for (i, &b) in bytes.iter().enumerate().skip(open) {
236        match b {
237            b'(' => depth += 1,
238            b')' => {
239                depth -= 1;
240                if depth == 0 {
241                    close_idx = Some(i);
242                    break;
243                }
244            }
245            _ => {}
246        }
247    }
248    let close_idx = close_idx?;
249    if close_idx != s.len() - 1 {
250        // 閉じ括弧が文字列末尾ちょうどでない = この後に別のトークンが続く
251        // (複数関数の並び等)ため単一関数として扱えない。
252        return None;
253    }
254    let inner = s.get(open + 1..close_idx)?.trim();
255    Some((name.to_string(), inner.to_string()))
256}
257
258/// `fn(args)` を ("fn", "args") に分割。
259pub(crate) fn split_fn(s: &str) -> Option<(String, String)> {
260    let open = s.find('(')?;
261    if !s.ends_with(')') {
262        return None;
263    }
264    let name = s.get(..open)?.trim();
265    let inner = s.get(open + 1..s.len() - 1)?.trim();
266    if name.is_empty() {
267        return None;
268    }
269    Some((name.to_string(), inner.to_string()))
270}
271
272/// `12.5px` → (12.5, "px")、`0.8` → (0.8, "")。
273pub(crate) fn split_num_unit(s: &str) -> Option<(f32, String)> {
274    let s = s.trim();
275    let end = s
276        .find(|c: char| !(c.is_ascii_digit() || c == '.' || c == '-' || c == '+'))
277        .unwrap_or(s.len());
278    let (num, unit) = s.split_at(end);
279    let n = num.parse::<f32>().ok()?;
280    Some((n, unit.trim().to_string()))
281}
282
283/// 数値を簡潔な文字列にする(整数なら小数点なし)。
284pub(crate) fn fmt_num(v: f32) -> String {
285    let r = libm_round(v * 1000.0) / 1000.0;
286    if (r - libm_round(r)).abs() < 1e-4 {
287        alloc::format!("{}", r as i64)
288    } else {
289        let s = alloc::format!("{:.3}", r);
290        s.trim_end_matches('0').trim_end_matches('.').to_string()
291    }
292}
293
294pub(crate) fn libm_round(v: f32) -> f32 {
295    if v >= 0.0 {
296        (v + 0.5) as i64 as f32
297    } else {
298        -((-v + 0.5) as i64 as f32)
299    }
300}
301
302/// 2色を補間して #RRGGBB or rgba(...) 文字列を返す。内部表現は 0xAARRGGBB。
303pub(crate) fn interpolate_color(c1: u32, c2: u32, t: f32) -> String {
304    let t = t.clamp(0.0, 1.0);
305    let a1 = ((c1 >> 24) & 0xFF) as f32;
306    let r1 = ((c1 >> 16) & 0xFF) as f32;
307    let g1 = ((c1 >> 8) & 0xFF) as f32;
308    let b1 = (c1 & 0xFF) as f32;
309    let a2 = ((c2 >> 24) & 0xFF) as f32;
310    let r2 = ((c2 >> 16) & 0xFF) as f32;
311    let g2 = ((c2 >> 8) & 0xFF) as f32;
312    let b2 = (c2 & 0xFF) as f32;
313    let a = (a1 + (a2 - a1) * t) as u32;
314    let r = (r1 + (r2 - r1) * t) as u32;
315    let g = (g1 + (g2 - g1) * t) as u32;
316    let b = (b1 + (b2 - b1) * t) as u32;
317    if a >= 255 {
318        alloc::format!("#{:02x}{:02x}{:02x}", r, g, b)
319    } else {
320        let af = a as f32 / 255.0;
321        alloc::format!("rgba({}, {}, {}, {})", r, g, b, fmt_num(af))
322    }
323}
324
325/// `transition` ショートハンド1項目の解析結果。
326#[derive(Debug, Clone, PartialEq)]
327pub struct TransitionSpec {
328    pub property: String,
329    pub duration_ms: f32,
330    pub timing: TimingFunction,
331    pub delay_ms: f32,
332}
333
334/// CSS 時間値 `1s` / `250ms` を ms に変換。
335pub fn parse_time_ms(s: &str) -> f32 {
336    let s = s.trim().to_lowercase();
337    if let Some(v) = s.strip_suffix("ms") {
338        v.trim().parse::<f32>().unwrap_or(0.0)
339    } else if let Some(v) = s.strip_suffix('s') {
340        v.trim().parse::<f32>().unwrap_or(0.0) * 1000.0
341    } else {
342        s.parse::<f32>().unwrap_or(0.0)
343    }
344}
345
346/// プロパティ名がアニメーション可能か(transition: all の展開対象判定に使う)。
347pub fn is_animatable_property(prop: &str) -> bool {
348    matches!(
349        prop,
350        "width"
351            | "height"
352            | "left"
353            | "top"
354            | "right"
355            | "bottom"
356            | "margin"
357            | "margin-left"
358            | "margin-top"
359            | "margin-right"
360            | "margin-bottom"
361            | "padding"
362            | "padding-left"
363            | "padding-top"
364            | "padding-right"
365            | "padding-bottom"
366            | "opacity"
367            | "color"
368            | "background-color"
369            | "border-color"
370            | "border-width"
371            | "font-size"
372            | "transform"
373            | "line-height"
374            | "letter-spacing"
375            | "word-spacing"
376            | "max-width"
377            | "max-height"
378            | "min-width"
379            | "min-height"
380    )
381}
382
383/// `transition` プロパティ(カンマ区切りの複数項目)をパース。
384/// 例: "width 1s ease, background-color 0.5s linear 0.2s"。
385/// トークンが実際に時間値(`<number>s` / `<number>ms`)かどうかを判定する。
386/// 単に末尾が `s` かどうかだけを見ると `flex-basis`/`border-radius` のように
387/// 末尾がたまたま `s` で終わるプロパティ名まで時間値と誤認してしまう
388/// (`font: oblique <angle>` の `10deg` を size と誤認したのと同根の「緩すぎる
389/// トークン分類」バグ)ため、単位を除いた残りが実際に数値としてパースできる
390/// ことまで確認する。
391pub(crate) fn is_time_token(low: &str) -> bool {
392    if let Some(v) = low.strip_suffix("ms") {
393        v.trim().parse::<f32>().is_ok()
394    } else if let Some(v) = low.strip_suffix('s') {
395        v.trim().parse::<f32>().is_ok()
396    } else {
397        false
398    }
399}
400
401pub fn parse_transition(value: &str) -> Vec<TransitionSpec> {
402    let mut specs = Vec::new();
403    for item in split_top_commas(value) {
404        let toks: Vec<&str> = item.split_whitespace().collect();
405        if toks.is_empty() {
406            continue;
407        }
408        if toks.len() == 1 && toks[0].eq_ignore_ascii_case("none") {
409            continue;
410        }
411        let mut property = String::from("all");
412        let mut duration_ms = 0.0;
413        let mut delay_ms = 0.0;
414        let mut timing = TimingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0);
415        let mut time_seen = 0;
416        let mut i = 0;
417        while i < toks.len() {
418            let tok = toks[i];
419            let low = tok.to_lowercase();
420            if low.starts_with("cubic-bezier(") || low.starts_with("steps(") {
421                // 関数表記は空白を含み得るので `)` まで連結。
422                let mut joined = String::from(tok);
423                while !joined.ends_with(')') && i + 1 < toks.len() {
424                    i += 1;
425                    joined.push_str(toks[i]);
426                }
427                timing = TimingFunction::parse(&joined);
428            } else if matches!(
429                low.as_str(),
430                "linear"
431                    | "ease"
432                    | "ease-in"
433                    | "ease-out"
434                    | "ease-in-out"
435                    | "step-start"
436                    | "step-end"
437            ) {
438                timing = TimingFunction::parse(&low);
439            } else if is_time_token(&low) {
440                if time_seen == 0 {
441                    duration_ms = parse_time_ms(&low).max(0.0);
442                } else {
443                    delay_ms = parse_time_ms(&low);
444                }
445                time_seen += 1;
446            } else {
447                property = tok.to_string();
448            }
449            i += 1;
450        }
451        if !property.eq_ignore_ascii_case("none") {
452            specs.push(TransitionSpec {
453                property,
454                duration_ms,
455                timing,
456                delay_ms,
457            });
458        }
459    }
460    specs
461}
462
463fn split_comma_list(s: &str) -> Vec<&str> {
464    s.split(',').map(|x| x.trim()).filter(|x| !x.is_empty()).collect()
465}
466
467fn get_cyclic_val<'a>(list: &[&'a str], idx: usize, default: &'a str) -> &'a str {
468    if list.is_empty() {
469        default
470    } else {
471        list[idx % list.len()]
472    }
473}
474
475/// W3C CSS Transitions Level 1 §2.5:
476/// 個別ロングハンド群(`transition-property`, `transition-duration`,
477/// `transition-timing-function`, `transition-delay`)から `Vec<TransitionSpec>` を合成・解決する。
478/// リスト長が異なる場合は周回(repeat)して合成する。
479pub fn resolve_transition_specs(
480    property: Option<&str>,
481    duration: Option<&str>,
482    timing: Option<&str>,
483    delay: Option<&str>,
484) -> Vec<TransitionSpec> {
485    let prop_list = property.map(split_comma_list).unwrap_or_default();
486    if prop_list.len() == 1 && prop_list[0].eq_ignore_ascii_case("none") {
487        return Vec::new();
488    }
489    let dur_list = duration.map(split_comma_list).unwrap_or_default();
490    let timing_list = timing.map(split_comma_list).unwrap_or_default();
491    let delay_list = delay.map(split_comma_list).unwrap_or_default();
492
493    let count = if !prop_list.is_empty() {
494        prop_list.len()
495    } else {
496        dur_list.len().max(timing_list.len()).max(delay_list.len()).max(1)
497    };
498
499    let mut specs = Vec::with_capacity(count);
500    for i in 0..count {
501        let prop = get_cyclic_val(&prop_list, i, "all");
502        if prop.eq_ignore_ascii_case("none") {
503            continue;
504        }
505        let dur_str = get_cyclic_val(&dur_list, i, "0s");
506        let timing_str = get_cyclic_val(&timing_list, i, "ease");
507        let delay_str = get_cyclic_val(&delay_list, i, "0s");
508
509        let duration_ms = parse_time_ms(dur_str).max(0.0);
510        let delay_ms = parse_time_ms(delay_str);
511        let timing_func = TimingFunction::parse(timing_str);
512
513        specs.push(TransitionSpec {
514            property: prop.to_string(),
515            duration_ms,
516            timing: timing_func,
517            delay_ms,
518        });
519    }
520    specs
521}
522
523/// W3C CSS Animations Level 1 §4:
524/// 個別ロングハンド群(`animation-name`, `animation-duration`, `animation-timing-function`,
525/// `animation-delay`, `animation-iteration-count`, `animation-direction`, `animation-fill-mode`,
526/// `animation-play-state`)から `Vec<AnimationSpec>` を合成・解決する。
527pub fn resolve_animation_specs(
528    name: Option<&str>,
529    duration: Option<&str>,
530    timing: Option<&str>,
531    delay: Option<&str>,
532    iteration_count: Option<&str>,
533    direction: Option<&str>,
534    fill_mode: Option<&str>,
535    play_state: Option<&str>,
536) -> Vec<AnimationSpec> {
537    let name_list = name.map(split_comma_list).unwrap_or_default();
538    if name_list.is_empty() || (name_list.len() == 1 && (name_list[0].is_empty() || name_list[0].eq_ignore_ascii_case("none"))) {
539        return Vec::new();
540    }
541    let dur_list = duration.map(split_comma_list).unwrap_or_default();
542    let timing_list = timing.map(split_comma_list).unwrap_or_default();
543    let delay_list = delay.map(split_comma_list).unwrap_or_default();
544    let iter_list = iteration_count.map(split_comma_list).unwrap_or_default();
545    let dir_list = direction.map(split_comma_list).unwrap_or_default();
546    let fill_list = fill_mode.map(split_comma_list).unwrap_or_default();
547    let play_list = play_state.map(split_comma_list).unwrap_or_default();
548
549    let count = name_list.len();
550    let mut specs = Vec::with_capacity(count);
551    for i in 0..count {
552        let aname = name_list[i];
553        if aname.is_empty() || aname.eq_ignore_ascii_case("none") {
554            continue;
555        }
556        let dur_str = get_cyclic_val(&dur_list, i, "0s");
557        let timing_str = get_cyclic_val(&timing_list, i, "ease");
558        let delay_str = get_cyclic_val(&delay_list, i, "0s");
559        let iter_str = get_cyclic_val(&iter_list, i, "1");
560        let dir_str = get_cyclic_val(&dir_list, i, "normal");
561        let fill_str = get_cyclic_val(&fill_list, i, "none");
562        let play_str = get_cyclic_val(&play_list, i, "running");
563
564        let duration_ms = parse_time_ms(dur_str).max(0.0);
565        let delay_ms = parse_time_ms(delay_str);
566        let timing_func = TimingFunction::parse(timing_str);
567        let iterations = if iter_str.eq_ignore_ascii_case("infinite") {
568            f32::INFINITY
569        } else {
570            iter_str.parse::<f32>().unwrap_or(1.0).max(0.0)
571        };
572
573        specs.push(AnimationSpec {
574            name: aname.to_string(),
575            duration_ms,
576            timing: timing_func,
577            delay_ms,
578            iterations,
579            direction: dir_str.to_string(),
580            fill: fill_str.to_string(),
581            play_state: play_str.to_string(),
582        });
583    }
584    specs
585}
586
587/// `animation` ショートハンドの解析結果。
588#[derive(Debug, Clone, PartialEq)]
589pub struct AnimationSpec {
590    pub name: String,
591    pub duration_ms: f32,
592    pub timing: TimingFunction,
593    pub delay_ms: f32,
594    /// 繰り返し回数。f32::INFINITY = infinite。
595    pub iterations: f32,
596    /// "normal" | "reverse" | "alternate" | "alternate-reverse"。
597    pub direction: String,
598    /// "none" | "forwards" | "backwards" | "both"。
599    pub fill: String,
600    /// "running" | "paused"。以前は `animation-play-state` が一切パース/適用されず、
601    /// `paused` 指定が常に無視され再生が止められないバグがあった。
602    pub play_state: String,
603}
604
605/// `animation` ショートハンドの最初の1項目のみをパース(後方互換用)。
606/// 複数のカンマ区切りアニメーションをすべて扱いたい場合は `parse_animations` を使う。
607pub fn parse_animation(value: &str) -> Option<AnimationSpec> {
608    parse_animations(value).into_iter().next()
609}
610
611/// `animation` ショートハンドをカンマ区切りの全項目についてパースする。
612/// 例: "fade 1s, bounce 2s infinite" → 2つの `AnimationSpec`。
613pub fn parse_animations(value: &str) -> alloc::vec::Vec<AnimationSpec> {
614    split_top_commas(value)
615        .into_iter()
616        .filter_map(|item| parse_one_animation(&item))
617        .collect()
618}
619
620/// `animation` ショートハンド1項目をパース。
621/// 例: "slide 2s ease-in-out 0.5s infinite alternate both"。
622pub(crate) fn parse_one_animation(item: &str) -> Option<AnimationSpec> {
623    let toks: Vec<String> = item.split_whitespace().map(|s| s.to_string()).collect();
624    if toks.is_empty() {
625        return None;
626    }
627    let mut spec = AnimationSpec {
628        name: String::new(),
629        duration_ms: 0.0,
630        timing: TimingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0),
631        delay_ms: 0.0,
632        iterations: 1.0,
633        direction: String::from("normal"),
634        fill: String::from("none"),
635        play_state: String::from("running"),
636    };
637    let mut time_seen = 0;
638    let mut i = 0;
639    while i < toks.len() {
640        let tok = toks[i].clone();
641        let low = tok.to_lowercase();
642        if low.starts_with("cubic-bezier(") || low.starts_with("steps(") {
643            let mut joined = tok.clone();
644            while !joined.ends_with(')') && i + 1 < toks.len() {
645                i += 1;
646                joined.push_str(&toks[i]);
647            }
648            spec.timing = TimingFunction::parse(&joined);
649        } else if matches!(
650            low.as_str(),
651            "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end"
652        ) {
653            spec.timing = TimingFunction::parse(&low);
654        } else if is_time_token(&low) {
655            if time_seen == 0 {
656                spec.duration_ms = parse_time_ms(&low);
657            } else {
658                spec.delay_ms = parse_time_ms(&low);
659            }
660            time_seen += 1;
661        } else if low == "infinite" {
662            spec.iterations = f32::INFINITY;
663        } else if let Ok(n) = low.parse::<f32>() {
664            spec.iterations = n;
665        } else if matches!(
666            low.as_str(),
667            "normal" | "reverse" | "alternate" | "alternate-reverse"
668        ) {
669            spec.direction = low;
670        } else if matches!(low.as_str(), "none" | "forwards" | "backwards" | "both") {
671            spec.fill = low;
672        } else if matches!(low.as_str(), "running" | "paused") {
673            spec.play_state = low;
674        } else {
675            // それ以外は animation-name とみなす。
676            spec.name = tok;
677        }
678        i += 1;
679    }
680    if spec.name.is_empty() {
681        return None;
682    }
683    Some(spec)
684}
685
686/// トップレベルのカンマで分割(関数内の `,` は無視)。
687pub(crate) fn split_top_commas(s: &str) -> Vec<String> {
688    let mut out = Vec::new();
689    let mut depth = 0;
690    let mut cur = String::new();
691    for c in s.chars() {
692        match c {
693            '(' => {
694                depth += 1;
695                cur.push(c);
696            }
697            ')' => {
698                depth -= 1;
699                cur.push(c);
700            }
701            ',' if depth == 0 => {
702                out.push(cur.trim().to_string());
703                cur.clear();
704            }
705            _ => cur.push(c),
706        }
707    }
708    if !cur.trim().is_empty() {
709        out.push(cur.trim().to_string());
710    }
711    out
712}
713
714/// トランジションの現在値を計算する。
715/// `from`/`to`: プロパティの開始値・終了値。`elapsed_ms`: 状態変更からの経過時間。
716/// 戻り値: (現在の補間値, アニメーションが進行中か)。
717pub fn sample_transition(
718    spec: &TransitionSpec,
719    from: &str,
720    to: &str,
721    elapsed_ms: f32,
722) -> (String, bool) {
723    let active = elapsed_ms - spec.delay_ms;
724    if active <= 0.0 {
725        return (from.to_string(), true);
726    }
727    if spec.duration_ms <= 0.0 || active >= spec.duration_ms {
728        return (to.to_string(), false);
729    }
730    let raw = active / spec.duration_ms;
731    let eased = spec.timing.ease(raw);
732    (interpolate_value(from, to, eased), true)
733}
734
735/// @keyframes アニメーションの現在算出値を、対象プロパティごとに返す。
736/// `elapsed_ms`: アニメーション開始からの経過。戻り値: プロパティ→現在値のマップ。
737pub fn sample_animation(
738    spec: &AnimationSpec,
739    keyframes: &Keyframes,
740    elapsed_ms: f32,
741) -> BTreeMap<String, String> {
742    let mut result = BTreeMap::new();
743    if spec.duration_ms <= 0.0 {
744        return result;
745    }
746    let active = elapsed_ms - spec.delay_ms;
747    if active < 0.0 {
748        // backwards/both なら最初のフレームを適用。
749        if spec.fill == "backwards" || spec.fill == "both" {
750            if let Some((_, decls)) = spec.frames_at(keyframes, 0.0) {
751                for d in decls {
752                    result.insert(d.name.clone(), d.value.clone());
753                }
754            }
755        }
756        return result;
757    }
758    // 現在の反復回数とローカル進捗。
759    let iter_f = active / spec.duration_ms;
760    let finished = iter_f >= spec.iterations;
761    let mut local = if finished {
762        // forwards/both なら最終位置で固定。
763        if spec.fill != "forwards" && spec.fill != "both" {
764            return result;
765        }
766        // 最終反復の終端進捗。
767        let last_iter = if spec.iterations.is_finite() {
768            spec.iterations
769        } else {
770            iter_f
771        };
772        let frac = last_iter - libm_floor(last_iter);
773        if frac == 0.0 {
774            1.0
775        } else {
776            frac
777        }
778    } else {
779        iter_f - libm_floor(iter_f)
780    };
781    // 完了時の反復インデックス(0始まり、direction:alternate の偶奇判定に使う)。
782    // `animation-iteration-count` が非整数(例: 2.5)の場合、以前は常に
783    // `floor(iterations) - 1` を使っていたため、最終回の「端数だけ進んだ反復」
784    // (本来のインデックスは `floor(iterations)`)ではなく1つ前の反復として扱われ、
785    // alternate の偶奇が反転し、fill:forwards/both での最終停止フレームが
786    // 左右逆になってしまうバグがあった(整数回数のときは端数が無い=frac==0.0 なので
787    // 従来通り `iterations - 1` のままで正しい)。
788    let iter_index = if finished {
789        let last_iter = if spec.iterations.is_finite() {
790            spec.iterations
791        } else {
792            iter_f
793        };
794        let frac = last_iter - libm_floor(last_iter);
795        if frac == 0.0 {
796            (last_iter.max(1.0) - 1.0) as i64
797        } else {
798            libm_floor(last_iter) as i64
799        }
800    } else {
801        libm_floor(iter_f) as i64
802    };
803    // direction を反映。
804    let reverse = match spec.direction.as_str() {
805        "reverse" => true,
806        "alternate" => iter_index % 2 == 1,
807        "alternate-reverse" => iter_index % 2 == 0,
808        _ => false,
809    };
810    if reverse {
811        local = 1.0 - local;
812    }
813    local = local.clamp(0.0, 1.0);
814    let eased = spec.timing.ease(local);
815
816    // local 進捗を囲む2つのキーフレームを探して各プロパティを補間。
817    // まず登場する全プロパティを収集。
818    let mut props: Vec<String> = Vec::new();
819    for (_, decls) in &keyframes.frames {
820        for d in decls {
821            if !props.contains(&d.name) {
822                props.push(d.name.clone());
823            }
824        }
825    }
826    for prop in props {
827        // このプロパティを定義する直前・直後のフレームを探す。
828        let mut lower: Option<(f32, String)> = None;
829        let mut upper: Option<(f32, String)> = None;
830        for (off, decls) in &keyframes.frames {
831            if let Some(d) = decls.iter().find(|d| d.name == prop) {
832                if *off <= eased {
833                    lower = Some((*off, d.value.clone()));
834                }
835                if *off >= eased && upper.is_none() {
836                    upper = Some((*off, d.value.clone()));
837                }
838            }
839        }
840        let value = match (lower, upper) {
841            (Some((o1, v1)), Some((o2, v2))) => {
842                if (o2 - o1).abs() < 1e-6 {
843                    v2
844                } else {
845                    let local_t = (eased - o1) / (o2 - o1);
846                    interpolate_value(&v1, &v2, local_t)
847                }
848            }
849            (Some((_, v)), None) => v,
850            (None, Some((_, v))) => v,
851            (None, None) => continue,
852        };
853        result.insert(prop, value);
854    }
855    result
856}
857
858pub(crate) fn libm_floor(v: f32) -> f32 {
859    let i = v as i64 as f32;
860    if v < 0.0 && i != v {
861        i - 1.0
862    } else {
863        i
864    }
865}
866
867impl AnimationSpec {
868    /// 与えた offset 以下の最大フレーム(存在しなければ最初のフレーム)を返す。
869    fn frames_at<'a>(
870        &self,
871        keyframes: &'a Keyframes,
872        offset: f32,
873    ) -> Option<(f32, &'a Vec<Declaration>)> {
874        let mut best: Option<(f32, &Vec<Declaration>)> = None;
875        for (off, decls) in &keyframes.frames {
876            if *off <= offset + 1e-6 {
877                best = Some((*off, decls));
878            }
879        }
880        best.or_else(|| keyframes.frames.first().map(|(o, d)| (*o, d)))
881    }
882}
883
884// ===================================================================
885// ステートフルなアニメーション駆動エンジン
886//   レンダラが毎フレーム tick() を呼び、要素ごとの算出値を得る。
887//   時刻は外部(カーネルの ms クロック)から供給される。
888// ===================================================================
889
890/// 1要素・1プロパティのトランジション進行状態。
891#[derive(Debug, Clone)]
892pub(crate) struct ActiveTransition {
893    element_id: String,
894    property: String,
895    from: String,
896    to: String,
897    spec: TransitionSpec,
898    /// 開始時刻(ms)。
899    start_ms: f32,
900}
901
902/// 1要素のアニメーション(@keyframes)進行状態。
903#[derive(Debug, Clone)]
904pub(crate) struct ActiveAnimation {
905    element_id: String,
906    spec: AnimationSpec,
907    keyframes: Keyframes,
908    start_ms: f32,
909    /// 完了済みか(fill が none のとき値を出さなくなる)。
910    done: bool,
911    /// `play_state:paused` の間だけ `Some(経過ms)` を保持し、その値で経過時間を凍結する
912    /// (`start_ms` はそのままにしておき、再開時に「凍結していた経過時間ぶんだけ現在時刻から
913    /// 巻き戻す」形で `start_ms` を前進させ、一時停止した分だけシームレスに再開する)。
914    paused_elapsed_ms: Option<f32>,
915    /// `animation-delay`が経過し実際に最初のキーフレームの適用が始まったか
916    /// (`animationstart`イベントを1回だけ発火するためのガード)。
917    started: bool,
918}
919
920/// アニメーション/トランジションの集中管理。
921#[derive(Debug, Clone, Default)]
922pub struct AnimationEngine {
923    transitions: Vec<ActiveTransition>,
924    animations: Vec<ActiveAnimation>,
925    /// 直近に算出した (element_id, property) → 値。レンダラが set_style に焼く。
926    pub computed: Vec<(String, String, String)>,
927    /// `animationend`/`transitionend`/`animationstart`(丸ごと未対応だった。
928    /// `new AnimationEvent(...)`/`new TransitionEvent(...)`コンストラクタ自体は
929    /// 既に実装済みで、手動での`el.dispatchEvent(new AnimationEvent
930    /// ('animationend', ...))`は可能だったが、実際にCSSアニメーション/
931    /// トランジションが完了・開始した時に、このエンジン自身が自動でイベントを
932    /// 発火する経路が丸ごと存在しなかった。`tick()`の呼び出し元
933    /// (`web_engine/layout.rs`の`tick_animations`。DOM/JSランタイムへのアクセス
934    /// 権を持つ)がこのライフサイクルイベントリストを都度回収し、実際に
935    /// `dispatch_event_with`する。各要素は`(element_id, event_type, name)`
936    /// (`name`はアニメーションなら`animation-name`、トランジションなら
937    /// プロパティ名)。`animationiteration`/`transitionstart`/`transitionrun`/
938    /// `transitioncancel`(反復・トランジション側の開始/中断系)は状態遷移の
939    /// 追跡がより複雑になるため今回は対象外とし、定番の完了/開始検知パターンで
940    /// 使われる`animationend`/`transitionend`/`animationstart`の3つのみに絞った。
941    pub lifecycle_events: Vec<(String, &'static str, String)>,
942}
943
944impl AnimationEngine {
945    pub fn new() -> Self {
946        AnimationEngine::default()
947    }
948
949    /// すべての進行状態をクリア(ページ遷移時に呼ぶ)。
950    pub fn clear(&mut self) {
951        self.transitions.clear();
952        self.animations.clear();
953        self.computed.clear();
954    }
955
956    /// 進行中・予定のアニメーションが1つでもあるか(再描画要否の判定に使う)。
957    pub fn is_active(&self) -> bool {
958        !self.transitions.is_empty() || self.animations.iter().any(|a| !a.done)
959    }
960
961    /// 要素に `transition` が設定された状態で値が old→new に変化したら呼ぶ。
962    /// 既存の同プロパティのトランジションは置き換える(再ターゲット)。
963    pub fn start_transition(
964        &mut self,
965        element_id: &str,
966        property: &str,
967        from: &str,
968        to: &str,
969        spec: TransitionSpec,
970        now_ms: f32,
971    ) {
972        if from == to {
973            return;
974        }
975        // 同一 (element, property) の進行中トランジションがあれば、そのトランジションの
976        // 「現在の補間値」をサンプリングしてから新トランジションの from とする(実ブラウザの
977        // getComputedStyle 準拠の「中断→逆再生は現在位置から滑らかに」という挙動)。
978        // 以前はコメントに「現在値から再スタート」と書いてありながら実装は伴っておらず、
979        // 呼び出し側から渡された `from`(1つ前の style 更新時点の値、stale)をそのまま使って
980        // 古いトランジションを単純に破棄していたため、中断時に一瞬ジャンプしてから
981        // 逆再生してしまうバグがあった。
982        let effective_from = self
983            .transitions
984            .iter()
985            .find(|t| t.element_id == element_id && t.property == property)
986            .map(|existing| {
987                let elapsed = now_ms - existing.start_ms;
988                sample_transition(&existing.spec, &existing.from, &existing.to, elapsed).0
989            })
990            .unwrap_or_else(|| from.to_string());
991        self.transitions
992            .retain(|t| !(t.element_id == element_id && t.property == property));
993        if effective_from == to {
994            // サンプリングした現在値が新しい目標値と一致するなら、新規トランジションは不要。
995            return;
996        }
997        self.transitions.push(ActiveTransition {
998            element_id: element_id.to_string(),
999            property: property.to_string(),
1000            from: effective_from,
1001            to: to.to_string(),
1002            spec,
1003            start_ms: now_ms,
1004        });
1005    }
1006
1007    /// 要素に `animation` が設定されたら呼ぶ。既存の同名アニメが既にある場合は
1008    /// 再登録せず、`play_state`(`animation-play-state:paused` のライブ切り替え、例えば
1009    /// hover 等のクラストグルで実現する一般的なパターン)だけを更新する。以前は既存アニメを
1010    /// 完全にスキップしていたため、実行中アニメの `play_state` をあとから変更しても
1011    /// 一切反映されなかった。
1012    pub fn start_animation(
1013        &mut self,
1014        element_id: &str,
1015        spec: AnimationSpec,
1016        keyframes: Keyframes,
1017        now_ms: f32,
1018    ) {
1019        if let Some(existing) = self
1020            .animations
1021            .iter_mut()
1022            .find(|a| a.element_id == element_id && a.spec.name == spec.name)
1023        {
1024            existing.spec.play_state = spec.play_state;
1025            // `@keyframes` の中身が動的に書き換わった場合(`<style>` の再注入や
1026            // メディアクエリ切り替えで同名の `@keyframes` ルールが別内容へ変わる等)にも
1027            // 追従できるよう、毎回渡される最新の `keyframes` へ更新する。以前はここで
1028            // 破棄しており、アニメーション開始時点のキーフレーム内容がずっと固定される
1029            // バグがあった(`iterations:infinite` だと永久に古い内容のまま)。
1030            existing.keyframes = keyframes;
1031            return;
1032        }
1033        self.animations.push(ActiveAnimation {
1034            element_id: element_id.to_string(),
1035            spec,
1036            keyframes,
1037            start_ms: now_ms,
1038            done: false,
1039            paused_elapsed_ms: None,
1040            started: false,
1041        });
1042    }
1043
1044    /// 指定要素のアニメ/トランジションを取り消す。
1045    pub fn cancel_for(&mut self, element_id: &str) {
1046        self.transitions.retain(|t| t.element_id != element_id);
1047        self.animations.retain(|a| a.element_id != element_id);
1048    }
1049
1050    /// 要素の `animation-name`(または `animation` ショートハンド)が変化した際、
1051    /// 現在指定されているアニメーション名の集合に無い、この要素の古いアニメーションを
1052    /// 取り消す。`start_animation` は「同一 (element, name) が既に存在すれば更新のみ」
1053    /// という判定しかしないため、以前は `animation-name` を "fade" から "bounce" のような
1054    /// 別名へ切り替えても、古い "fade" のアニメーションが決して除去されず(特に
1055    /// `iterations: infinite` の場合は自然完了もしないため)新旧2つのアニメーションが
1056    /// 永久に同時に走り続けてしまうバグがあった。
1057    pub fn retain_animation_names(&mut self, element_id: &str, current_names: &[String]) {
1058        self.animations.retain(|a| {
1059            a.element_id != element_id || current_names.iter().any(|n| n == &a.spec.name)
1060        });
1061    }
1062
1063    /// 現在時刻で全アニメ/トランジションを評価し、computed を更新する。
1064    /// 戻り値: 進行中のものが残っているか(= 次フレームも再描画が要るか)。
1065    pub fn tick(&mut self, now_ms: f32) -> bool {
1066        self.computed.clear();
1067        self.lifecycle_events.clear();
1068        let mut still_active = false;
1069
1070        // トランジション。
1071        let mut finished_idx: Vec<usize> = Vec::new();
1072        for (i, t) in self.transitions.iter().enumerate() {
1073            let elapsed = now_ms - t.start_ms;
1074            let (value, active) = sample_transition(&t.spec, &t.from, &t.to, elapsed);
1075            self.computed
1076                .push((t.element_id.clone(), t.property.clone(), value));
1077            if active {
1078                still_active = true;
1079            } else {
1080                finished_idx.push(i);
1081            }
1082        }
1083        // 完了したトランジションを除去(末尾から)。除去前に`transitionend`を記録する
1084        // (`element_id`/`property`は除去後には参照できないため)。
1085        for i in finished_idx.into_iter().rev() {
1086            let t = &self.transitions[i];
1087            self.lifecycle_events
1088                .push((t.element_id.clone(), "transitionend", t.property.clone()));
1089            self.transitions.remove(i);
1090        }
1091
1092        // アニメーション。
1093        for a in self.animations.iter_mut() {
1094            let elapsed = if a.spec.play_state == "paused" {
1095                // 一時停止中: 経過時間を最初に一時停止した瞬間の値へ凍結する。
1096                *a.paused_elapsed_ms.get_or_insert(now_ms - a.start_ms)
1097            } else {
1098                if let Some(frozen) = a.paused_elapsed_ms.take() {
1099                    // 再開: 一時停止していた経過時間から連続するよう start_ms を
1100                    // 現在時刻基準に前進させる(一時停止していた分だけ巻き戻す)。
1101                    a.start_ms = now_ms - frozen;
1102                }
1103                now_ms - a.start_ms
1104            };
1105            // `animationstart`は`animation-delay`が経過し実際にキーフレームの
1106            // 適用が始まった瞬間に1回だけ発火する(`!a.started`だった時だけ記録
1107            // する同じガードパターン)。
1108            if !a.started && elapsed >= a.spec.delay_ms {
1109                self.lifecycle_events
1110                    .push((a.element_id.clone(), "animationstart", a.spec.name.clone()));
1111                a.started = true;
1112            }
1113            let m = sample_animation(&a.spec, &a.keyframes, elapsed);
1114            // 完了判定: 経過がトータル尺を超え、かつ無限でない。`!a.done`だった
1115            // ものが今回のtickで初めてtrueになった瞬間だけ`animationend`を記録する
1116            // (既に完了済みのアニメが`fill!=none`で居残り続ける場合に毎tick
1117            // 再発火してしまわないようにするガード)。
1118            let total = a.spec.delay_ms + a.spec.duration_ms * a.spec.iterations;
1119            if a.spec.iterations.is_finite() && elapsed >= total {
1120                if !a.done {
1121                    self.lifecycle_events.push((
1122                        a.element_id.clone(),
1123                        "animationend",
1124                        a.spec.name.clone(),
1125                    ));
1126                }
1127                a.done = true;
1128            } else {
1129                still_active = true;
1130            }
1131            for (prop, val) in m {
1132                self.computed.push((a.element_id.clone(), prop, val));
1133            }
1134        }
1135        // fill=none で完了したアニメは破棄(値を出し切ったあと)。
1136        self.animations
1137            .retain(|a| !(a.done && a.spec.fill == "none"));
1138
1139        still_active
1140    }
1141}
1142
1143// ============ CSS 基本形状補間(W3C CSS Masking 1 §5.2.1 / CSS Shapes 1) ============
1144
1145fn try_interpolate_basic_shape(fn_name: &str, args1: &str, args2: &str, t: f32) -> Option<String> {
1146    match fn_name {
1147        "circle" => interpolate_circle(args1, args2, t),
1148        "ellipse" => interpolate_ellipse(args1, args2, t),
1149        "inset" => interpolate_inset(args1, args2, t),
1150        "polygon" => interpolate_polygon(args1, args2, t),
1151        _ => None,
1152    }
1153}
1154
1155fn normalize_position(pos: &str) -> (String, String) {
1156    let tokens: Vec<&str> = pos.split_whitespace().collect();
1157    match tokens.as_slice() {
1158        [] => ("50%".into(), "50%".into()),
1159        ["center"] => ("50%".into(), "50%".into()),
1160        ["left"] => ("0%".into(), "50%".into()),
1161        ["right"] => ("100%".into(), "50%".into()),
1162        ["top"] => ("50%".into(), "0%".into()),
1163        ["bottom"] => ("50%".into(), "100%".into()),
1164        [x] => (keyword_to_pct(x).into(), "50%".into()),
1165        [x, y] => (keyword_to_pct(x).into(), keyword_to_pct(y).into()),
1166        _ => ("50%".into(), "50%".into()),
1167    }
1168}
1169
1170fn keyword_to_pct(s: &str) -> &str {
1171    match s {
1172        "left" | "top" => "0%",
1173        "right" | "bottom" => "100%",
1174        "center" => "50%",
1175        other => other,
1176    }
1177}
1178
1179fn parse_circle_args(s: &str) -> (String, String) {
1180    if let Some((r_part, pos_part)) = s.split_once("at") {
1181        let r = r_part.trim();
1182        let r = if r.is_empty() { "50%" } else { r };
1183        (r.into(), pos_part.trim().into())
1184    } else {
1185        let s = s.trim();
1186        let r = if s.is_empty() { "50%" } else { s };
1187        (r.into(), "50% 50%".into())
1188    }
1189}
1190
1191fn interpolate_circle(a1: &str, a2: &str, t: f32) -> Option<String> {
1192    let (r1, pos1) = parse_circle_args(a1);
1193    let (r2, pos2) = parse_circle_args(a2);
1194    let r = interpolate_value(&r1, &r2, t);
1195    let (x1, y1) = normalize_position(&pos1);
1196    let (x2, y2) = normalize_position(&pos2);
1197    let x = interpolate_value(&x1, &x2, t);
1198    let y = interpolate_value(&y1, &y2, t);
1199    Some(alloc::format!("circle({} at {} {})", r, x, y))
1200}
1201
1202fn parse_ellipse_args(s: &str) -> (String, String, String) {
1203    let (r_part, pos_part) = if let Some((r_part, pos_part)) = s.split_once("at") {
1204        (r_part.trim(), pos_part.trim())
1205    } else {
1206        (s.trim(), "50% 50%")
1207    };
1208    let r_tokens: Vec<&str> = r_part.split_whitespace().collect();
1209    let (rx, ry) = match r_tokens.as_slice() {
1210        [] => ("50%", "50%"),
1211        [r] => (*r, *r),
1212        [rx, ry, ..] => (*rx, *ry),
1213    };
1214    (rx.into(), ry.into(), pos_part.into())
1215}
1216
1217fn interpolate_ellipse(a1: &str, a2: &str, t: f32) -> Option<String> {
1218    let (rx1, ry1, pos1) = parse_ellipse_args(a1);
1219    let (rx2, ry2, pos2) = parse_ellipse_args(a2);
1220    let rx = interpolate_value(&rx1, &rx2, t);
1221    let ry = interpolate_value(&ry1, &ry2, t);
1222    let (x1, y1) = normalize_position(&pos1);
1223    let (x2, y2) = normalize_position(&pos2);
1224    let x = interpolate_value(&x1, &x2, t);
1225    let y = interpolate_value(&y1, &y2, t);
1226    Some(alloc::format!("ellipse({} {} at {} {})", rx, ry, x, y))
1227}
1228
1229fn expand_four_values(s: &str, default_val: &str) -> (String, String, String, String) {
1230    let tokens: Vec<&str> = s.split_whitespace().collect();
1231    match tokens.as_slice() {
1232        [] => (default_val.into(), default_val.into(), default_val.into(), default_val.into()),
1233        [a] => ((*a).into(), (*a).into(), (*a).into(), (*a).into()),
1234        [a, b] => ((*a).into(), (*b).into(), (*a).into(), (*b).into()),
1235        [a, b, c] => ((*a).into(), (*b).into(), (*c).into(), (*b).into()),
1236        [a, b, c, d, ..] => ((*a).into(), (*b).into(), (*c).into(), (*d).into()),
1237    }
1238}
1239
1240fn parse_inset_args(
1241    s: &str,
1242) -> (
1243    (String, String, String, String),
1244    Option<(String, String, String, String)>,
1245) {
1246    let (offset_str, round_str) = if let Some((o, r)) = s.split_once("round") {
1247        (o.trim(), Some(r.trim()))
1248    } else {
1249        (s.trim(), None)
1250    };
1251    let rect = expand_four_values(offset_str, "0px");
1252    let round = round_str.map(|r| expand_four_values(r, "0px"));
1253    (rect, round)
1254}
1255
1256fn interpolate_inset(a1: &str, a2: &str, t: f32) -> Option<String> {
1257    let (rect1, round1) = parse_inset_args(a1);
1258    let (rect2, round2) = parse_inset_args(a2);
1259    let top = interpolate_value(&rect1.0, &rect2.0, t);
1260    let right = interpolate_value(&rect1.1, &rect2.1, t);
1261    let bottom = interpolate_value(&rect1.2, &rect2.2, t);
1262    let left = interpolate_value(&rect1.3, &rect2.3, t);
1263
1264    if round1.is_some() || round2.is_some() {
1265        let r1 = round1.unwrap_or_else(|| ("0px".into(), "0px".into(), "0px".into(), "0px".into()));
1266        let r2 = round2.unwrap_or_else(|| ("0px".into(), "0px".into(), "0px".into(), "0px".into()));
1267        let cr1 = interpolate_value(&r1.0, &r2.0, t);
1268        let cr2 = interpolate_value(&r1.1, &r2.1, t);
1269        let cr3 = interpolate_value(&r1.2, &r2.2, t);
1270        let cr4 = interpolate_value(&r1.3, &r2.3, t);
1271        Some(alloc::format!(
1272            "inset({} {} {} {} round {} {} {} {})",
1273            top, right, bottom, left, cr1, cr2, cr3, cr4
1274        ))
1275    } else {
1276        Some(alloc::format!("inset({} {} {} {})", top, right, bottom, left))
1277    }
1278}
1279
1280fn parse_polygon_args(s: &str) -> (String, Vec<(String, String)>) {
1281    let parts = split_top_level_commas(s);
1282    let mut rule = "nonzero".to_string();
1283    let mut pts = Vec::new();
1284    for (i, p) in parts.iter().enumerate() {
1285        let trimmed = p.trim();
1286        if i == 0 && (trimmed == "nonzero" || trimmed == "evenodd") {
1287            rule = trimmed.to_string();
1288            continue;
1289        }
1290        let coords: Vec<&str> = trimmed.split_whitespace().collect();
1291        if coords.len() >= 2 {
1292            pts.push((coords[0].to_string(), coords[1].to_string()));
1293        }
1294    }
1295    (rule, pts)
1296}
1297
1298fn interpolate_polygon(a1: &str, a2: &str, t: f32) -> Option<String> {
1299    let (rule1, pts1) = parse_polygon_args(a1);
1300    let (rule2, pts2) = parse_polygon_args(a2);
1301    if rule1 != rule2 || pts1.len() != pts2.len() || pts1.is_empty() {
1302        return None;
1303    }
1304    let mut out_pts = Vec::with_capacity(pts1.len());
1305    for ((x1, y1), (x2, y2)) in pts1.iter().zip(pts2.iter()) {
1306        let x = interpolate_value(x1, x2, t);
1307        let y = interpolate_value(y1, y2, t);
1308        out_pts.push(alloc::format!("{} {}", x, y));
1309    }
1310    let pts_joined = out_pts.join(", ");
1311    if rule1 == "evenodd" {
1312        Some(alloc::format!("polygon(evenodd, {})", pts_joined))
1313    } else {
1314        Some(alloc::format!("polygon({})", pts_joined))
1315    }
1316}
1317
1318