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            let inner = if f1.1.contains(',') || f2.1.contains(',') {
130                let a1 = split_top_level_commas(&f1.1);
131                let a2 = split_top_level_commas(&f2.1);
132                if a1.len() == a2.len() {
133                    a1.iter()
134                        .zip(a2.iter())
135                        .map(|(a, b)| interpolate_value(a, b, t))
136                        .collect::<Vec<String>>()
137                        .join(", ")
138                } else {
139                    return if t < 0.5 { from.to_string() } else { to.to_string() };
140                }
141            } else {
142                interpolate_value(&f1.1, &f2.1, t)
143            };
144            return alloc::format!("{}({})", f1.0, inner);
145        }
146    }
147    // `box-shadow`/`text-shadow` のようなカンマ区切りの複数レイヤー値。以前はここより先に
148    // 空白区切りトークン判定が走っており、カンマ後の空白によるトークン化で
149    // レイヤー境界を跨いだ位置ベースの補間(例: "red," と "blue," のような壊れたトークン同士)
150    // になってしまい、色として解析できず discrete フォールバックに落ちる/レイヤー数が
151    // 食い違うと丸ごとジャンプするバグがあった(`translate(x, y)` の引数分割で見つけたのと
152    // 同種)。トップレベル(括弧の外)にカンマがある場合はレイヤー単位で分割・補間する。
153    let from_layers = split_top_level_commas(from);
154    let to_layers = split_top_level_commas(to);
155    if from_layers.len() > 1 || to_layers.len() > 1 {
156        if from_layers.len() == to_layers.len() {
157            return from_layers
158                .iter()
159                .zip(to_layers.iter())
160                .map(|(a, b)| interpolate_value(a, b, t))
161                .collect::<Vec<String>>()
162                .join(", ");
163        }
164        return if t < 0.5 { from.to_string() } else { to.to_string() };
165    }
166    // 複数トークン(margin: 1px 2px, transform: translateX(..) rotate(..) 等)。
167    let ft: Vec<&str> = from.split_whitespace().collect();
168    let tt: Vec<&str> = to.split_whitespace().collect();
169    if ft.len() > 1 || tt.len() > 1 {
170        if ft.len() == tt.len() {
171            let parts: Vec<String> = ft
172                .iter()
173                .zip(tt.iter())
174                .map(|(a, b)| interpolate_value(a, b, t))
175                .collect();
176            return parts.join(" ");
177        }
178        return if t < 0.5 {
179            from.to_string()
180        } else {
181            to.to_string()
182        };
183    }
184    // translateX(10px) 等の単一関数(引数無しカンマ): 内側を補間。
185    if let (Some(f1), Some(f2)) = (split_fn(from), split_fn(to)) {
186        if f1.0 == f2.0 {
187            let inner = interpolate_value(&f1.1, &f2.1, t);
188            return alloc::format!("{}({})", f1.0, inner);
189        }
190    }
191    // 単位付き数値 / 純数値。CSS仕様上、長さ/百分率等の値では単位無しの`0`は
192    // どの単位の文脈でも許容される(例: 実サイト www.sugi-lab.net のタイピング
193    // 演出`@keyframes typing { from{width:0} to{width:100%} }`)。以前はこの
194    // ケースを`u1 == u2`の厳密一致でのみ判定していたため、`0`(単位無し)と
195    // `100%`(%単位)が「単位不一致」とみなされ補間できず、アニメーションが
196    // 滑らかに変化せず離散的にジャンプするバグだった(2026-07-22発見・修正)。
197    if let (Some((n1, u1)), Some((n2, u2))) = (split_num_unit(from), split_num_unit(to)) {
198        let units_compatible =
199            u1 == u2 || (u1.is_empty() && n1 == 0.0) || (u2.is_empty() && n2 == 0.0);
200        if units_compatible {
201            let unit = if u1.is_empty() { u2 } else { u1 };
202            let v = n1 + (n2 - n1) * t;
203            if unit.is_empty() {
204                return fmt_num(v);
205            }
206            return alloc::format!("{}{}", fmt_num(v), unit);
207        }
208    }
209    // 補間不能 → discrete。
210    if t < 0.5 {
211        from.to_string()
212    } else {
213        to.to_string()
214    }
215}
216
217/// 文字列全体が単一の関数呼び出し(`name(...)`)かどうかを判定し、そうであれば
218/// ("name", "args") を返す。`split_fn` と違い、括弧の対応を実際に追跡して
219/// 「その関数呼び出しの閉じ括弧が文字列の末尾ちょうどにある」ことまで確認するため、
220/// `translateX(10px) rotate(20deg)`(空白区切りの複数関数)を誤って単一関数として
221/// 誤認しない(`split_fn` は最初の `(` と末尾の `)` を機械的に拾うだけなので、この判定には使えない)。
222pub(crate) fn as_single_wrapped_call(s: &str) -> Option<(String, String)> {
223    let s = s.trim();
224    let open = s.find('(')?;
225    let name = s.get(..open)?.trim();
226    if name.is_empty() || name.contains(char::is_whitespace) {
227        return None;
228    }
229    let bytes = s.as_bytes();
230    let mut depth = 0i32;
231    let mut close_idx = None;
232    for (i, &b) in bytes.iter().enumerate().skip(open) {
233        match b {
234            b'(' => depth += 1,
235            b')' => {
236                depth -= 1;
237                if depth == 0 {
238                    close_idx = Some(i);
239                    break;
240                }
241            }
242            _ => {}
243        }
244    }
245    let close_idx = close_idx?;
246    if close_idx != s.len() - 1 {
247        // 閉じ括弧が文字列末尾ちょうどでない = この後に別のトークンが続く
248        // (複数関数の並び等)ため単一関数として扱えない。
249        return None;
250    }
251    let inner = s.get(open + 1..close_idx)?.trim();
252    Some((name.to_string(), inner.to_string()))
253}
254
255/// `fn(args)` を ("fn", "args") に分割。
256pub(crate) fn split_fn(s: &str) -> Option<(String, String)> {
257    let open = s.find('(')?;
258    if !s.ends_with(')') {
259        return None;
260    }
261    let name = s.get(..open)?.trim();
262    let inner = s.get(open + 1..s.len() - 1)?.trim();
263    if name.is_empty() {
264        return None;
265    }
266    Some((name.to_string(), inner.to_string()))
267}
268
269/// `12.5px` → (12.5, "px")、`0.8` → (0.8, "")。
270pub(crate) fn split_num_unit(s: &str) -> Option<(f32, String)> {
271    let s = s.trim();
272    let end = s
273        .find(|c: char| !(c.is_ascii_digit() || c == '.' || c == '-' || c == '+'))
274        .unwrap_or(s.len());
275    let (num, unit) = s.split_at(end);
276    let n = num.parse::<f32>().ok()?;
277    Some((n, unit.trim().to_string()))
278}
279
280/// 数値を簡潔な文字列にする(整数なら小数点なし)。
281pub(crate) fn fmt_num(v: f32) -> String {
282    let r = libm_round(v * 1000.0) / 1000.0;
283    if (r - libm_round(r)).abs() < 1e-4 {
284        alloc::format!("{}", r as i64)
285    } else {
286        let s = alloc::format!("{:.3}", r);
287        s.trim_end_matches('0').trim_end_matches('.').to_string()
288    }
289}
290
291pub(crate) fn libm_round(v: f32) -> f32 {
292    if v >= 0.0 {
293        (v + 0.5) as i64 as f32
294    } else {
295        -((-v + 0.5) as i64 as f32)
296    }
297}
298
299/// 2色を補間して #RRGGBB or rgba(...) 文字列を返す。内部表現は 0xAARRGGBB。
300pub(crate) fn interpolate_color(c1: u32, c2: u32, t: f32) -> String {
301    let t = t.clamp(0.0, 1.0);
302    let a1 = ((c1 >> 24) & 0xFF) as f32;
303    let r1 = ((c1 >> 16) & 0xFF) as f32;
304    let g1 = ((c1 >> 8) & 0xFF) as f32;
305    let b1 = (c1 & 0xFF) as f32;
306    let a2 = ((c2 >> 24) & 0xFF) as f32;
307    let r2 = ((c2 >> 16) & 0xFF) as f32;
308    let g2 = ((c2 >> 8) & 0xFF) as f32;
309    let b2 = (c2 & 0xFF) as f32;
310    let a = (a1 + (a2 - a1) * t) as u32;
311    let r = (r1 + (r2 - r1) * t) as u32;
312    let g = (g1 + (g2 - g1) * t) as u32;
313    let b = (b1 + (b2 - b1) * t) as u32;
314    if a >= 255 {
315        alloc::format!("#{:02x}{:02x}{:02x}", r, g, b)
316    } else {
317        let af = a as f32 / 255.0;
318        alloc::format!("rgba({}, {}, {}, {})", r, g, b, fmt_num(af))
319    }
320}
321
322/// `transition` ショートハンド1項目の解析結果。
323#[derive(Debug, Clone, PartialEq)]
324pub struct TransitionSpec {
325    pub property: String,
326    pub duration_ms: f32,
327    pub timing: TimingFunction,
328    pub delay_ms: f32,
329}
330
331/// CSS 時間値 `1s` / `250ms` を ms に変換。
332pub fn parse_time_ms(s: &str) -> f32 {
333    let s = s.trim().to_lowercase();
334    if let Some(v) = s.strip_suffix("ms") {
335        v.trim().parse::<f32>().unwrap_or(0.0)
336    } else if let Some(v) = s.strip_suffix('s') {
337        v.trim().parse::<f32>().unwrap_or(0.0) * 1000.0
338    } else {
339        s.parse::<f32>().unwrap_or(0.0)
340    }
341}
342
343/// プロパティ名がアニメーション可能か(transition: all の展開対象判定に使う)。
344pub fn is_animatable_property(prop: &str) -> bool {
345    matches!(
346        prop,
347        "width"
348            | "height"
349            | "left"
350            | "top"
351            | "right"
352            | "bottom"
353            | "margin"
354            | "margin-left"
355            | "margin-top"
356            | "margin-right"
357            | "margin-bottom"
358            | "padding"
359            | "padding-left"
360            | "padding-top"
361            | "padding-right"
362            | "padding-bottom"
363            | "opacity"
364            | "color"
365            | "background-color"
366            | "border-color"
367            | "border-width"
368            | "font-size"
369            | "transform"
370            | "line-height"
371            | "letter-spacing"
372            | "word-spacing"
373            | "max-width"
374            | "max-height"
375            | "min-width"
376            | "min-height"
377    )
378}
379
380/// `transition` プロパティ(カンマ区切りの複数項目)をパース。
381/// 例: "width 1s ease, background-color 0.5s linear 0.2s"。
382/// トークンが実際に時間値(`<number>s` / `<number>ms`)かどうかを判定する。
383/// 単に末尾が `s` かどうかだけを見ると `flex-basis`/`border-radius` のように
384/// 末尾がたまたま `s` で終わるプロパティ名まで時間値と誤認してしまう
385/// (`font: oblique <angle>` の `10deg` を size と誤認したのと同根の「緩すぎる
386/// トークン分類」バグ)ため、単位を除いた残りが実際に数値としてパースできる
387/// ことまで確認する。
388pub(crate) fn is_time_token(low: &str) -> bool {
389    if let Some(v) = low.strip_suffix("ms") {
390        v.trim().parse::<f32>().is_ok()
391    } else if let Some(v) = low.strip_suffix('s') {
392        v.trim().parse::<f32>().is_ok()
393    } else {
394        false
395    }
396}
397
398pub fn parse_transition(value: &str) -> Vec<TransitionSpec> {
399    let mut specs = Vec::new();
400    for item in split_top_commas(value) {
401        let toks: Vec<&str> = item.split_whitespace().collect();
402        if toks.is_empty() {
403            continue;
404        }
405        let mut property = String::from("all");
406        let mut duration_ms = 0.0;
407        let mut delay_ms = 0.0;
408        let mut timing = TimingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0);
409        let mut time_seen = 0;
410        let mut i = 0;
411        while i < toks.len() {
412            let tok = toks[i];
413            let low = tok.to_lowercase();
414            if low.starts_with("cubic-bezier(") || low.starts_with("steps(") {
415                // 関数表記は空白を含み得るので `)` まで連結。
416                let mut joined = String::from(tok);
417                while !joined.ends_with(')') && i + 1 < toks.len() {
418                    i += 1;
419                    joined.push_str(toks[i]);
420                }
421                timing = TimingFunction::parse(&joined);
422            } else if matches!(
423                low.as_str(),
424                "linear"
425                    | "ease"
426                    | "ease-in"
427                    | "ease-out"
428                    | "ease-in-out"
429                    | "step-start"
430                    | "step-end"
431            ) {
432                timing = TimingFunction::parse(&low);
433            } else if is_time_token(&low) {
434                if time_seen == 0 {
435                    duration_ms = parse_time_ms(&low);
436                } else {
437                    delay_ms = parse_time_ms(&low);
438                }
439                time_seen += 1;
440            } else {
441                property = tok.to_string();
442            }
443            i += 1;
444        }
445        specs.push(TransitionSpec {
446            property,
447            duration_ms,
448            timing,
449            delay_ms,
450        });
451    }
452    specs
453}
454
455/// `animation` ショートハンドの解析結果。
456#[derive(Debug, Clone, PartialEq)]
457pub struct AnimationSpec {
458    pub name: String,
459    pub duration_ms: f32,
460    pub timing: TimingFunction,
461    pub delay_ms: f32,
462    /// 繰り返し回数。f32::INFINITY = infinite。
463    pub iterations: f32,
464    /// "normal" | "reverse" | "alternate" | "alternate-reverse"。
465    pub direction: String,
466    /// "none" | "forwards" | "backwards" | "both"。
467    pub fill: String,
468    /// "running" | "paused"。以前は `animation-play-state` が一切パース/適用されず、
469    /// `paused` 指定が常に無視され再生が止められないバグがあった。
470    pub play_state: String,
471}
472
473/// `animation` ショートハンドの最初の1項目のみをパース(後方互換用)。
474/// 複数のカンマ区切りアニメーションをすべて扱いたい場合は `parse_animations` を使う。
475pub fn parse_animation(value: &str) -> Option<AnimationSpec> {
476    parse_animations(value).into_iter().next()
477}
478
479/// `animation` ショートハンドをカンマ区切りの全項目についてパースする。
480/// 例: "fade 1s, bounce 2s infinite" → 2つの `AnimationSpec`。
481pub fn parse_animations(value: &str) -> alloc::vec::Vec<AnimationSpec> {
482    split_top_commas(value)
483        .into_iter()
484        .filter_map(|item| parse_one_animation(&item))
485        .collect()
486}
487
488/// `animation` ショートハンド1項目をパース。
489/// 例: "slide 2s ease-in-out 0.5s infinite alternate both"。
490pub(crate) fn parse_one_animation(item: &str) -> Option<AnimationSpec> {
491    let toks: Vec<String> = item.split_whitespace().map(|s| s.to_string()).collect();
492    if toks.is_empty() {
493        return None;
494    }
495    let mut spec = AnimationSpec {
496        name: String::new(),
497        duration_ms: 0.0,
498        timing: TimingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0),
499        delay_ms: 0.0,
500        iterations: 1.0,
501        direction: String::from("normal"),
502        fill: String::from("none"),
503        play_state: String::from("running"),
504    };
505    let mut time_seen = 0;
506    let mut i = 0;
507    while i < toks.len() {
508        let tok = toks[i].clone();
509        let low = tok.to_lowercase();
510        if low.starts_with("cubic-bezier(") || low.starts_with("steps(") {
511            let mut joined = tok.clone();
512            while !joined.ends_with(')') && i + 1 < toks.len() {
513                i += 1;
514                joined.push_str(&toks[i]);
515            }
516            spec.timing = TimingFunction::parse(&joined);
517        } else if matches!(
518            low.as_str(),
519            "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end"
520        ) {
521            spec.timing = TimingFunction::parse(&low);
522        } else if is_time_token(&low) {
523            if time_seen == 0 {
524                spec.duration_ms = parse_time_ms(&low);
525            } else {
526                spec.delay_ms = parse_time_ms(&low);
527            }
528            time_seen += 1;
529        } else if low == "infinite" {
530            spec.iterations = f32::INFINITY;
531        } else if let Ok(n) = low.parse::<f32>() {
532            spec.iterations = n;
533        } else if matches!(
534            low.as_str(),
535            "normal" | "reverse" | "alternate" | "alternate-reverse"
536        ) {
537            spec.direction = low;
538        } else if matches!(low.as_str(), "none" | "forwards" | "backwards" | "both") {
539            spec.fill = low;
540        } else if matches!(low.as_str(), "running" | "paused") {
541            spec.play_state = low;
542        } else {
543            // それ以外は animation-name とみなす。
544            spec.name = tok;
545        }
546        i += 1;
547    }
548    if spec.name.is_empty() {
549        return None;
550    }
551    Some(spec)
552}
553
554/// トップレベルのカンマで分割(関数内の `,` は無視)。
555pub(crate) fn split_top_commas(s: &str) -> Vec<String> {
556    let mut out = Vec::new();
557    let mut depth = 0;
558    let mut cur = String::new();
559    for c in s.chars() {
560        match c {
561            '(' => {
562                depth += 1;
563                cur.push(c);
564            }
565            ')' => {
566                depth -= 1;
567                cur.push(c);
568            }
569            ',' if depth == 0 => {
570                out.push(cur.trim().to_string());
571                cur.clear();
572            }
573            _ => cur.push(c),
574        }
575    }
576    if !cur.trim().is_empty() {
577        out.push(cur.trim().to_string());
578    }
579    out
580}
581
582/// トランジションの現在値を計算する。
583/// `from`/`to`: プロパティの開始値・終了値。`elapsed_ms`: 状態変更からの経過時間。
584/// 戻り値: (現在の補間値, アニメーションが進行中か)。
585pub fn sample_transition(
586    spec: &TransitionSpec,
587    from: &str,
588    to: &str,
589    elapsed_ms: f32,
590) -> (String, bool) {
591    let active = elapsed_ms - spec.delay_ms;
592    if active <= 0.0 {
593        return (from.to_string(), true);
594    }
595    if spec.duration_ms <= 0.0 || active >= spec.duration_ms {
596        return (to.to_string(), false);
597    }
598    let raw = active / spec.duration_ms;
599    let eased = spec.timing.ease(raw);
600    (interpolate_value(from, to, eased), true)
601}
602
603/// @keyframes アニメーションの現在算出値を、対象プロパティごとに返す。
604/// `elapsed_ms`: アニメーション開始からの経過。戻り値: プロパティ→現在値のマップ。
605pub fn sample_animation(
606    spec: &AnimationSpec,
607    keyframes: &Keyframes,
608    elapsed_ms: f32,
609) -> BTreeMap<String, String> {
610    let mut result = BTreeMap::new();
611    if spec.duration_ms <= 0.0 {
612        return result;
613    }
614    let active = elapsed_ms - spec.delay_ms;
615    if active < 0.0 {
616        // backwards/both なら最初のフレームを適用。
617        if spec.fill == "backwards" || spec.fill == "both" {
618            if let Some((_, decls)) = spec.frames_at(keyframes, 0.0) {
619                for d in decls {
620                    result.insert(d.name.clone(), d.value.clone());
621                }
622            }
623        }
624        return result;
625    }
626    // 現在の反復回数とローカル進捗。
627    let iter_f = active / spec.duration_ms;
628    let finished = iter_f >= spec.iterations;
629    let mut local = if finished {
630        // forwards/both なら最終位置で固定。
631        if spec.fill != "forwards" && spec.fill != "both" {
632            return result;
633        }
634        // 最終反復の終端進捗。
635        let last_iter = if spec.iterations.is_finite() {
636            spec.iterations
637        } else {
638            iter_f
639        };
640        let frac = last_iter - libm_floor(last_iter);
641        if frac == 0.0 {
642            1.0
643        } else {
644            frac
645        }
646    } else {
647        iter_f - libm_floor(iter_f)
648    };
649    // 完了時の反復インデックス(0始まり、direction:alternate の偶奇判定に使う)。
650    // `animation-iteration-count` が非整数(例: 2.5)の場合、以前は常に
651    // `floor(iterations) - 1` を使っていたため、最終回の「端数だけ進んだ反復」
652    // (本来のインデックスは `floor(iterations)`)ではなく1つ前の反復として扱われ、
653    // alternate の偶奇が反転し、fill:forwards/both での最終停止フレームが
654    // 左右逆になってしまうバグがあった(整数回数のときは端数が無い=frac==0.0 なので
655    // 従来通り `iterations - 1` のままで正しい)。
656    let iter_index = if finished {
657        let last_iter = if spec.iterations.is_finite() {
658            spec.iterations
659        } else {
660            iter_f
661        };
662        let frac = last_iter - libm_floor(last_iter);
663        if frac == 0.0 {
664            (last_iter.max(1.0) - 1.0) as i64
665        } else {
666            libm_floor(last_iter) as i64
667        }
668    } else {
669        libm_floor(iter_f) as i64
670    };
671    // direction を反映。
672    let reverse = match spec.direction.as_str() {
673        "reverse" => true,
674        "alternate" => iter_index % 2 == 1,
675        "alternate-reverse" => iter_index % 2 == 0,
676        _ => false,
677    };
678    if reverse {
679        local = 1.0 - local;
680    }
681    local = local.clamp(0.0, 1.0);
682    let eased = spec.timing.ease(local);
683
684    // local 進捗を囲む2つのキーフレームを探して各プロパティを補間。
685    // まず登場する全プロパティを収集。
686    let mut props: Vec<String> = Vec::new();
687    for (_, decls) in &keyframes.frames {
688        for d in decls {
689            if !props.contains(&d.name) {
690                props.push(d.name.clone());
691            }
692        }
693    }
694    for prop in props {
695        // このプロパティを定義する直前・直後のフレームを探す。
696        let mut lower: Option<(f32, String)> = None;
697        let mut upper: Option<(f32, String)> = None;
698        for (off, decls) in &keyframes.frames {
699            if let Some(d) = decls.iter().find(|d| d.name == prop) {
700                if *off <= eased {
701                    lower = Some((*off, d.value.clone()));
702                }
703                if *off >= eased && upper.is_none() {
704                    upper = Some((*off, d.value.clone()));
705                }
706            }
707        }
708        let value = match (lower, upper) {
709            (Some((o1, v1)), Some((o2, v2))) => {
710                if (o2 - o1).abs() < 1e-6 {
711                    v2
712                } else {
713                    let local_t = (eased - o1) / (o2 - o1);
714                    interpolate_value(&v1, &v2, local_t)
715                }
716            }
717            (Some((_, v)), None) => v,
718            (None, Some((_, v))) => v,
719            (None, None) => continue,
720        };
721        result.insert(prop, value);
722    }
723    result
724}
725
726pub(crate) fn libm_floor(v: f32) -> f32 {
727    let i = v as i64 as f32;
728    if v < 0.0 && i != v {
729        i - 1.0
730    } else {
731        i
732    }
733}
734
735impl AnimationSpec {
736    /// 与えた offset 以下の最大フレーム(存在しなければ最初のフレーム)を返す。
737    fn frames_at<'a>(
738        &self,
739        keyframes: &'a Keyframes,
740        offset: f32,
741    ) -> Option<(f32, &'a Vec<Declaration>)> {
742        let mut best: Option<(f32, &Vec<Declaration>)> = None;
743        for (off, decls) in &keyframes.frames {
744            if *off <= offset + 1e-6 {
745                best = Some((*off, decls));
746            }
747        }
748        best.or_else(|| keyframes.frames.first().map(|(o, d)| (*o, d)))
749    }
750}
751
752// ===================================================================
753// ステートフルなアニメーション駆動エンジン
754//   レンダラが毎フレーム tick() を呼び、要素ごとの算出値を得る。
755//   時刻は外部(カーネルの ms クロック)から供給される。
756// ===================================================================
757
758/// 1要素・1プロパティのトランジション進行状態。
759#[derive(Debug, Clone)]
760pub(crate) struct ActiveTransition {
761    element_id: String,
762    property: String,
763    from: String,
764    to: String,
765    spec: TransitionSpec,
766    /// 開始時刻(ms)。
767    start_ms: f32,
768}
769
770/// 1要素のアニメーション(@keyframes)進行状態。
771#[derive(Debug, Clone)]
772pub(crate) struct ActiveAnimation {
773    element_id: String,
774    spec: AnimationSpec,
775    keyframes: Keyframes,
776    start_ms: f32,
777    /// 完了済みか(fill が none のとき値を出さなくなる)。
778    done: bool,
779    /// `play_state:paused` の間だけ `Some(経過ms)` を保持し、その値で経過時間を凍結する
780    /// (`start_ms` はそのままにしておき、再開時に「凍結していた経過時間ぶんだけ現在時刻から
781    /// 巻き戻す」形で `start_ms` を前進させ、一時停止した分だけシームレスに再開する)。
782    paused_elapsed_ms: Option<f32>,
783    /// `animation-delay`が経過し実際に最初のキーフレームの適用が始まったか
784    /// (`animationstart`イベントを1回だけ発火するためのガード)。
785    started: bool,
786}
787
788/// アニメーション/トランジションの集中管理。
789#[derive(Debug, Clone, Default)]
790pub struct AnimationEngine {
791    transitions: Vec<ActiveTransition>,
792    animations: Vec<ActiveAnimation>,
793    /// 直近に算出した (element_id, property) → 値。レンダラが set_style に焼く。
794    pub computed: Vec<(String, String, String)>,
795    /// `animationend`/`transitionend`/`animationstart`(丸ごと未対応だった。
796    /// `new AnimationEvent(...)`/`new TransitionEvent(...)`コンストラクタ自体は
797    /// 既に実装済みで、手動での`el.dispatchEvent(new AnimationEvent
798    /// ('animationend', ...))`は可能だったが、実際にCSSアニメーション/
799    /// トランジションが完了・開始した時に、このエンジン自身が自動でイベントを
800    /// 発火する経路が丸ごと存在しなかった。`tick()`の呼び出し元
801    /// (`web_engine/layout.rs`の`tick_animations`。DOM/JSランタイムへのアクセス
802    /// 権を持つ)がこのライフサイクルイベントリストを都度回収し、実際に
803    /// `dispatch_event_with`する。各要素は`(element_id, event_type, name)`
804    /// (`name`はアニメーションなら`animation-name`、トランジションなら
805    /// プロパティ名)。`animationiteration`/`transitionstart`/`transitionrun`/
806    /// `transitioncancel`(反復・トランジション側の開始/中断系)は状態遷移の
807    /// 追跡がより複雑になるため今回は対象外とし、定番の完了/開始検知パターンで
808    /// 使われる`animationend`/`transitionend`/`animationstart`の3つのみに絞った。
809    pub lifecycle_events: Vec<(String, &'static str, String)>,
810}
811
812impl AnimationEngine {
813    pub fn new() -> Self {
814        AnimationEngine::default()
815    }
816
817    /// すべての進行状態をクリア(ページ遷移時に呼ぶ)。
818    pub fn clear(&mut self) {
819        self.transitions.clear();
820        self.animations.clear();
821        self.computed.clear();
822    }
823
824    /// 進行中・予定のアニメーションが1つでもあるか(再描画要否の判定に使う)。
825    pub fn is_active(&self) -> bool {
826        !self.transitions.is_empty() || self.animations.iter().any(|a| !a.done)
827    }
828
829    /// 要素に `transition` が設定された状態で値が old→new に変化したら呼ぶ。
830    /// 既存の同プロパティのトランジションは置き換える(再ターゲット)。
831    pub fn start_transition(
832        &mut self,
833        element_id: &str,
834        property: &str,
835        from: &str,
836        to: &str,
837        spec: TransitionSpec,
838        now_ms: f32,
839    ) {
840        if from == to {
841            return;
842        }
843        // 同一 (element, property) の進行中トランジションがあれば、そのトランジションの
844        // 「現在の補間値」をサンプリングしてから新トランジションの from とする(実ブラウザの
845        // getComputedStyle 準拠の「中断→逆再生は現在位置から滑らかに」という挙動)。
846        // 以前はコメントに「現在値から再スタート」と書いてありながら実装は伴っておらず、
847        // 呼び出し側から渡された `from`(1つ前の style 更新時点の値、stale)をそのまま使って
848        // 古いトランジションを単純に破棄していたため、中断時に一瞬ジャンプしてから
849        // 逆再生してしまうバグがあった。
850        let effective_from = self
851            .transitions
852            .iter()
853            .find(|t| t.element_id == element_id && t.property == property)
854            .map(|existing| {
855                let elapsed = now_ms - existing.start_ms;
856                sample_transition(&existing.spec, &existing.from, &existing.to, elapsed).0
857            })
858            .unwrap_or_else(|| from.to_string());
859        self.transitions
860            .retain(|t| !(t.element_id == element_id && t.property == property));
861        if effective_from == to {
862            // サンプリングした現在値が新しい目標値と一致するなら、新規トランジションは不要。
863            return;
864        }
865        self.transitions.push(ActiveTransition {
866            element_id: element_id.to_string(),
867            property: property.to_string(),
868            from: effective_from,
869            to: to.to_string(),
870            spec,
871            start_ms: now_ms,
872        });
873    }
874
875    /// 要素に `animation` が設定されたら呼ぶ。既存の同名アニメが既にある場合は
876    /// 再登録せず、`play_state`(`animation-play-state:paused` のライブ切り替え、例えば
877    /// hover 等のクラストグルで実現する一般的なパターン)だけを更新する。以前は既存アニメを
878    /// 完全にスキップしていたため、実行中アニメの `play_state` をあとから変更しても
879    /// 一切反映されなかった。
880    pub fn start_animation(
881        &mut self,
882        element_id: &str,
883        spec: AnimationSpec,
884        keyframes: Keyframes,
885        now_ms: f32,
886    ) {
887        if let Some(existing) = self
888            .animations
889            .iter_mut()
890            .find(|a| a.element_id == element_id && a.spec.name == spec.name)
891        {
892            existing.spec.play_state = spec.play_state;
893            // `@keyframes` の中身が動的に書き換わった場合(`<style>` の再注入や
894            // メディアクエリ切り替えで同名の `@keyframes` ルールが別内容へ変わる等)にも
895            // 追従できるよう、毎回渡される最新の `keyframes` へ更新する。以前はここで
896            // 破棄しており、アニメーション開始時点のキーフレーム内容がずっと固定される
897            // バグがあった(`iterations:infinite` だと永久に古い内容のまま)。
898            existing.keyframes = keyframes;
899            return;
900        }
901        self.animations.push(ActiveAnimation {
902            element_id: element_id.to_string(),
903            spec,
904            keyframes,
905            start_ms: now_ms,
906            done: false,
907            paused_elapsed_ms: None,
908            started: false,
909        });
910    }
911
912    /// 指定要素のアニメ/トランジションを取り消す。
913    pub fn cancel_for(&mut self, element_id: &str) {
914        self.transitions.retain(|t| t.element_id != element_id);
915        self.animations.retain(|a| a.element_id != element_id);
916    }
917
918    /// 要素の `animation-name`(または `animation` ショートハンド)が変化した際、
919    /// 現在指定されているアニメーション名の集合に無い、この要素の古いアニメーションを
920    /// 取り消す。`start_animation` は「同一 (element, name) が既に存在すれば更新のみ」
921    /// という判定しかしないため、以前は `animation-name` を "fade" から "bounce" のような
922    /// 別名へ切り替えても、古い "fade" のアニメーションが決して除去されず(特に
923    /// `iterations: infinite` の場合は自然完了もしないため)新旧2つのアニメーションが
924    /// 永久に同時に走り続けてしまうバグがあった。
925    pub fn retain_animation_names(&mut self, element_id: &str, current_names: &[String]) {
926        self.animations.retain(|a| {
927            a.element_id != element_id || current_names.iter().any(|n| n == &a.spec.name)
928        });
929    }
930
931    /// 現在時刻で全アニメ/トランジションを評価し、computed を更新する。
932    /// 戻り値: 進行中のものが残っているか(= 次フレームも再描画が要るか)。
933    pub fn tick(&mut self, now_ms: f32) -> bool {
934        self.computed.clear();
935        self.lifecycle_events.clear();
936        let mut still_active = false;
937
938        // トランジション。
939        let mut finished_idx: Vec<usize> = Vec::new();
940        for (i, t) in self.transitions.iter().enumerate() {
941            let elapsed = now_ms - t.start_ms;
942            let (value, active) = sample_transition(&t.spec, &t.from, &t.to, elapsed);
943            self.computed
944                .push((t.element_id.clone(), t.property.clone(), value));
945            if active {
946                still_active = true;
947            } else {
948                finished_idx.push(i);
949            }
950        }
951        // 完了したトランジションを除去(末尾から)。除去前に`transitionend`を記録する
952        // (`element_id`/`property`は除去後には参照できないため)。
953        for i in finished_idx.into_iter().rev() {
954            let t = &self.transitions[i];
955            self.lifecycle_events
956                .push((t.element_id.clone(), "transitionend", t.property.clone()));
957            self.transitions.remove(i);
958        }
959
960        // アニメーション。
961        for a in self.animations.iter_mut() {
962            let elapsed = if a.spec.play_state == "paused" {
963                // 一時停止中: 経過時間を最初に一時停止した瞬間の値へ凍結する。
964                *a.paused_elapsed_ms.get_or_insert(now_ms - a.start_ms)
965            } else {
966                if let Some(frozen) = a.paused_elapsed_ms.take() {
967                    // 再開: 一時停止していた経過時間から連続するよう start_ms を
968                    // 現在時刻基準に前進させる(一時停止していた分だけ巻き戻す)。
969                    a.start_ms = now_ms - frozen;
970                }
971                now_ms - a.start_ms
972            };
973            // `animationstart`は`animation-delay`が経過し実際にキーフレームの
974            // 適用が始まった瞬間に1回だけ発火する(`!a.started`だった時だけ記録
975            // する同じガードパターン)。
976            if !a.started && elapsed >= a.spec.delay_ms {
977                self.lifecycle_events
978                    .push((a.element_id.clone(), "animationstart", a.spec.name.clone()));
979                a.started = true;
980            }
981            let m = sample_animation(&a.spec, &a.keyframes, elapsed);
982            // 完了判定: 経過がトータル尺を超え、かつ無限でない。`!a.done`だった
983            // ものが今回のtickで初めてtrueになった瞬間だけ`animationend`を記録する
984            // (既に完了済みのアニメが`fill!=none`で居残り続ける場合に毎tick
985            // 再発火してしまわないようにするガード)。
986            let total = a.spec.delay_ms + a.spec.duration_ms * a.spec.iterations;
987            if a.spec.iterations.is_finite() && elapsed >= total {
988                if !a.done {
989                    self.lifecycle_events.push((
990                        a.element_id.clone(),
991                        "animationend",
992                        a.spec.name.clone(),
993                    ));
994                }
995                a.done = true;
996            } else {
997                still_active = true;
998            }
999            for (prop, val) in m {
1000                self.computed.push((a.element_id.clone(), prop, val));
1001            }
1002        }
1003        // fill=none で完了したアニメは破棄(値を出し切ったあと)。
1004        self.animations
1005            .retain(|a| !(a.done && a.spec.fill == "none"));
1006
1007        still_active
1008    }
1009}
1010