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 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 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 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 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 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 if t < 0.5 {
211 from.to_string()
212 } else {
213 to.to_string()
214 }
215}
216
217pub(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 return None;
250 }
251 let inner = s.get(open + 1..close_idx)?.trim();
252 Some((name.to_string(), inner.to_string()))
253}
254
255pub(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
269pub(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
280pub(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
299pub(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#[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
331pub 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
343pub 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
380pub(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 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#[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 pub iterations: f32,
464 pub direction: String,
466 pub fill: String,
468 pub play_state: String,
471}
472
473pub fn parse_animation(value: &str) -> Option<AnimationSpec> {
476 parse_animations(value).into_iter().next()
477}
478
479pub 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
488pub(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 spec.name = tok;
545 }
546 i += 1;
547 }
548 if spec.name.is_empty() {
549 return None;
550 }
551 Some(spec)
552}
553
554pub(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
582pub 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
603pub 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 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 let iter_f = active / spec.duration_ms;
628 let finished = iter_f >= spec.iterations;
629 let mut local = if finished {
630 if spec.fill != "forwards" && spec.fill != "both" {
632 return result;
633 }
634 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 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 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 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 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 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#[derive(Debug, Clone)]
760pub(crate) struct ActiveTransition {
761 element_id: String,
762 property: String,
763 from: String,
764 to: String,
765 spec: TransitionSpec,
766 start_ms: f32,
768}
769
770#[derive(Debug, Clone)]
772pub(crate) struct ActiveAnimation {
773 element_id: String,
774 spec: AnimationSpec,
775 keyframes: Keyframes,
776 start_ms: f32,
777 done: bool,
779 paused_elapsed_ms: Option<f32>,
783 started: bool,
786}
787
788#[derive(Debug, Clone, Default)]
790pub struct AnimationEngine {
791 transitions: Vec<ActiveTransition>,
792 animations: Vec<ActiveAnimation>,
793 pub computed: Vec<(String, String, String)>,
795 pub lifecycle_events: Vec<(String, &'static str, String)>,
810}
811
812impl AnimationEngine {
813 pub fn new() -> Self {
814 AnimationEngine::default()
815 }
816
817 pub fn clear(&mut self) {
819 self.transitions.clear();
820 self.animations.clear();
821 self.computed.clear();
822 }
823
824 pub fn is_active(&self) -> bool {
826 !self.transitions.is_empty() || self.animations.iter().any(|a| !a.done)
827 }
828
829 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 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 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 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 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 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 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 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 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 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 for a in self.animations.iter_mut() {
962 let elapsed = if a.spec.play_state == "paused" {
963 *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 a.start_ms = now_ms - frozen;
970 }
971 now_ms - a.start_ms
972 };
973 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 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 self.animations
1005 .retain(|a| !(a.done && a.spec.fill == "none"));
1006
1007 still_active
1008 }
1009}
1010