1use super::*;
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum TimingFunction {
8 Linear,
9 CubicBezier(f32, f32, f32, f32),
11 Steps(u32, bool),
13}
14
15impl TimingFunction {
16 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 TimingFunction::CubicBezier(0.25, 0.1, 0.25, 1.0)
52 }
53 }
54 }
55
56 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
78pub(crate) fn cubic_bezier_solve(x1: f32, y1: f32, x2: f32, y2: f32, x: f32) -> f32 {
80 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 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
109pub 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 if let (Some(c1), Some(c2)) = (parse_color(from), parse_color(to)) {
119 return interpolate_color(c1, c2, t);
120 }
121 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 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 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 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 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 if t < 0.5 {
214 from.to_string()
215 } else {
216 to.to_string()
217 }
218}
219
220pub(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 return None;
253 }
254 let inner = s.get(open + 1..close_idx)?.trim();
255 Some((name.to_string(), inner.to_string()))
256}
257
258pub(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
272pub(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
283pub(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
302pub(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#[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
334pub 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
346pub 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
383pub(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 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
475pub 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
523pub 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#[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 pub iterations: f32,
596 pub direction: String,
598 pub fill: String,
600 pub play_state: String,
603}
604
605pub fn parse_animation(value: &str) -> Option<AnimationSpec> {
608 parse_animations(value).into_iter().next()
609}
610
611pub 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
620pub(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 spec.name = tok;
677 }
678 i += 1;
679 }
680 if spec.name.is_empty() {
681 return None;
682 }
683 Some(spec)
684}
685
686pub(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
714pub 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
735pub 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 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 let iter_f = active / spec.duration_ms;
760 let finished = iter_f >= spec.iterations;
761 let mut local = if finished {
762 if spec.fill != "forwards" && spec.fill != "both" {
764 return result;
765 }
766 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 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 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 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 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 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#[derive(Debug, Clone)]
892pub(crate) struct ActiveTransition {
893 element_id: String,
894 property: String,
895 from: String,
896 to: String,
897 spec: TransitionSpec,
898 start_ms: f32,
900}
901
902#[derive(Debug, Clone)]
904pub(crate) struct ActiveAnimation {
905 element_id: String,
906 spec: AnimationSpec,
907 keyframes: Keyframes,
908 start_ms: f32,
909 done: bool,
911 paused_elapsed_ms: Option<f32>,
915 started: bool,
918}
919
920#[derive(Debug, Clone, Default)]
922pub struct AnimationEngine {
923 transitions: Vec<ActiveTransition>,
924 animations: Vec<ActiveAnimation>,
925 pub computed: Vec<(String, String, String)>,
927 pub lifecycle_events: Vec<(String, &'static str, String)>,
942}
943
944impl AnimationEngine {
945 pub fn new() -> Self {
946 AnimationEngine::default()
947 }
948
949 pub fn clear(&mut self) {
951 self.transitions.clear();
952 self.animations.clear();
953 self.computed.clear();
954 }
955
956 pub fn is_active(&self) -> bool {
958 !self.transitions.is_empty() || self.animations.iter().any(|a| !a.done)
959 }
960
961 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 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 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 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 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 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 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 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 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 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 for a in self.animations.iter_mut() {
1094 let elapsed = if a.spec.play_state == "paused" {
1095 *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 a.start_ms = now_ms - frozen;
1102 }
1103 now_ms - a.start_ms
1104 };
1105 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 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 self.animations
1137 .retain(|a| !(a.done && a.spec.fill == "none"));
1138
1139 still_active
1140 }
1141}
1142
1143fn 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