Skip to main content

takumi_css/style/
animation.rs

1use std::borrow::Cow;
2
3use parley::{FontFeature, FontVariation};
4use std::cmp::Ordering;
5use typed_builder::TypedBuilder;
6
7use serde::Deserialize;
8
9use crate::{
10  Viewport,
11  style::{
12    StyleDeclarationBlock,
13    selector::{MediaQueryList, StyleSheet},
14    *,
15  },
16};
17
18#[derive(Debug, Clone, Deserialize, PartialEq, TypedBuilder)]
19#[serde(rename_all = "camelCase")]
20/// A single structured keyframe rule.
21pub struct KeyframeRule {
22  /// Keyframe offsets as values between 0.0 and 1.0.
23  #[builder(setter(into))]
24  pub offsets: Vec<f32>,
25  /// Declarations applied at this step.
26  pub declarations: StyleDeclarationBlock,
27}
28
29#[derive(Debug, Clone, Deserialize, PartialEq, TypedBuilder)]
30#[serde(rename_all = "camelCase")]
31/// Structured keyframes that can be passed directly in render options.
32pub struct KeyframesRule {
33  /// Animation name matched by `animation-name`.
34  #[builder(setter(into))]
35  pub name: String,
36  /// Individual keyframe rules for this animation.
37  #[builder(setter(into))]
38  pub keyframes: Vec<KeyframeRule>,
39  #[serde(skip, default)]
40  #[builder(default, setter(skip))]
41  pub media_queries: Vec<MediaQueryList>,
42}
43
44pub fn apply_stylesheet_animations(
45  mut base_style: ComputedStyle,
46  stylesheet: &StyleSheet,
47  time: u64,
48  sizing: &SizingContext,
49  current_color: Color,
50) -> ComputedStyle {
51  if base_style.animation_name.is_empty() {
52    return base_style;
53  }
54
55  let base_snapshot = base_style.clone();
56
57  for (animation_index, animation_name) in base_snapshot.animation_name.iter().enumerate() {
58    let Some(animation_name) = animation_name else {
59      continue;
60    };
61
62    let Some(keyframes) = find_keyframes(stylesheet, animation_name, sizing.viewport) else {
63      continue;
64    };
65
66    let duration = time_at(
67      &base_snapshot.animation_duration,
68      animation_index,
69      AnimationTime::from_milliseconds(0.0),
70    );
71    let delay = time_at(
72      &base_snapshot.animation_delay,
73      animation_index,
74      AnimationTime::from_milliseconds(0.0),
75    );
76    let iteration_count =
77      iteration_count_at(&base_snapshot.animation_iteration_count, animation_index);
78    let direction = direction_at(&base_snapshot.animation_direction, animation_index);
79    let fill_mode = fill_mode_at(&base_snapshot.animation_fill_mode, animation_index);
80    let timing_function =
81      timing_function_at(&base_snapshot.animation_timing_function, animation_index);
82
83    let Some(progress) = sample_animation_progress(
84      time as f32,
85      duration.milliseconds,
86      delay.milliseconds,
87      iteration_count,
88      direction,
89      fill_mode,
90    ) else {
91      continue;
92    };
93
94    let resolved_frames = resolve_keyframes(&keyframes, &base_snapshot);
95    let Some(segment) = sample_keyframe_segment(&resolved_frames, &base_snapshot, progress) else {
96      continue;
97    };
98
99    let eased_progress = apply_timing_function(&timing_function, segment.progress);
100    base_style.apply_interpolated_properties(
101      segment.from_style,
102      segment.to_style,
103      &segment.animated_properties,
104      eased_progress,
105      sizing,
106      current_color,
107    );
108  }
109
110  base_style
111}
112
113fn find_keyframes<'a>(
114  stylesheet: &'a StyleSheet,
115  name: &str,
116  viewport: Viewport,
117) -> Option<Cow<'a, KeyframesRule>> {
118  stylesheet
119    .keyframes
120    .iter()
121    .rev()
122    .find(|rule| {
123      rule.name.eq_ignore_ascii_case(name)
124        && rule
125          .media_queries
126          .iter()
127          .all(|media_query| media_query.matches(viewport))
128    })
129    .map(Cow::Borrowed)
130    .or_else(|| tailwind_animation_keyframes(name).map(Cow::Owned))
131}
132
133fn tailwind_animation_keyframes(name: &str) -> Option<KeyframesRule> {
134  match name.to_ascii_lowercase().as_str() {
135    "spin" => Some(KeyframesRule {
136      name: "spin".to_string(),
137      keyframes: vec![
138        keyframe(0.25, [StyleDeclaration::rotate(Some(Angle::new(90.0)))]),
139        keyframe(0.5, [StyleDeclaration::rotate(Some(Angle::new(180.0)))]),
140        keyframe(0.75, [StyleDeclaration::rotate(Some(Angle::new(270.0)))]),
141        keyframe(1.0, [StyleDeclaration::rotate(Some(Angle::new(359.999)))]),
142      ],
143      media_queries: Vec::new(),
144    }),
145    "ping" => Some(KeyframesRule {
146      name: "ping".to_string(),
147      keyframes: vec![
148        keyframe(
149          0.75,
150          [
151            StyleDeclaration::scale(SpacePair::from_single(PercentageNumber(2.0))),
152            StyleDeclaration::opacity(PercentageNumber(0.0)),
153          ],
154        ),
155        keyframe(
156          1.0,
157          [
158            StyleDeclaration::scale(SpacePair::from_single(PercentageNumber(2.0))),
159            StyleDeclaration::opacity(PercentageNumber(0.0)),
160          ],
161        ),
162      ],
163      media_queries: Vec::new(),
164    }),
165    "pulse" => Some(KeyframesRule {
166      name: "pulse".to_string(),
167      keyframes: vec![keyframe(
168        0.5,
169        [StyleDeclaration::opacity(PercentageNumber(0.5))],
170      )],
171      media_queries: Vec::new(),
172    }),
173    "bounce" => Some(KeyframesRule {
174      name: "bounce".to_string(),
175      keyframes: vec![
176        keyframe(
177          0.0,
178          [StyleDeclaration::translate(SpacePair::from_pair(
179            Length::Px(0.0),
180            Length::Percentage(-25.0),
181          ))],
182        ),
183        keyframe(
184          0.5,
185          [StyleDeclaration::translate(SpacePair::from_pair(
186            Length::Px(0.0),
187            Length::Percentage(0.0),
188          ))],
189        ),
190        keyframe(
191          1.0,
192          [StyleDeclaration::translate(SpacePair::from_pair(
193            Length::Px(0.0),
194            Length::Percentage(-25.0),
195          ))],
196        ),
197      ],
198      media_queries: Vec::new(),
199    }),
200    _ => None,
201  }
202}
203
204fn keyframe<const N: usize>(offset: f32, declarations: [StyleDeclaration; N]) -> KeyframeRule {
205  let mut block = StyleDeclarationBlock::default();
206  for declaration in declarations {
207    block.push(declaration, false);
208  }
209
210  KeyframeRule {
211    offsets: vec![offset],
212    declarations: block,
213  }
214}
215
216fn sample_animation_progress(
217  time_ms: f32,
218  duration_ms: f32,
219  delay_ms: f32,
220  iteration_count: AnimationIterationCount,
221  direction: AnimationDirection,
222  fill_mode: AnimationFillMode,
223) -> Option<f32> {
224  let active_time = time_ms - delay_ms;
225
226  if duration_ms <= 0.0 {
227    if active_time < 0.0 {
228      return match fill_mode {
229        AnimationFillMode::Backwards | AnimationFillMode::Both => Some(start_progress(direction)),
230        _ => None,
231      };
232    }
233
234    return Some(end_progress(direction, 0));
235  }
236
237  let total_active_duration = match iteration_count {
238    AnimationIterationCount::Infinite => f32::INFINITY,
239    AnimationIterationCount::Number(count) => duration_ms * count.max(0.0),
240  };
241
242  if active_time < 0.0 {
243    return match fill_mode {
244      AnimationFillMode::Backwards | AnimationFillMode::Both => Some(start_progress(direction)),
245      _ => None,
246    };
247  }
248
249  if active_time >= total_active_duration {
250    return match fill_mode {
251      AnimationFillMode::Forwards | AnimationFillMode::Both => {
252        let end_progress = match iteration_count {
253          AnimationIterationCount::Infinite => end_progress(direction, 0),
254          AnimationIterationCount::Number(count) => {
255            let count = count.max(0.0);
256            let completed_iterations = count.floor() as usize;
257            let fraction = count.fract();
258            if fraction > f32::EPSILON {
259              apply_direction(fraction, direction, completed_iterations)
260            } else {
261              end_progress(direction, count.max(1.0) as usize - 1)
262            }
263          }
264        };
265        Some(end_progress)
266      }
267      _ => None,
268    };
269  }
270
271  let progress_within_iteration = active_time / duration_ms;
272  let mut iteration_index = progress_within_iteration.floor() as usize;
273  let mut progress = progress_within_iteration.fract();
274  if active_time > 0.0 && progress_within_iteration.fract().abs() <= f32::EPSILON {
275    progress = 1.0;
276    iteration_index = iteration_index.saturating_sub(1);
277  }
278
279  Some(apply_direction(progress, direction, iteration_index))
280}
281
282fn start_progress(direction: AnimationDirection) -> f32 {
283  apply_direction(0.0, direction, 0)
284}
285
286fn end_progress(direction: AnimationDirection, iteration_index: usize) -> f32 {
287  apply_direction(1.0, direction, iteration_index)
288}
289
290fn apply_direction(progress: f32, direction: AnimationDirection, iteration_index: usize) -> f32 {
291  match direction {
292    AnimationDirection::Normal => progress,
293    AnimationDirection::Reverse => 1.0 - progress,
294    AnimationDirection::Alternate => {
295      if iteration_index.is_multiple_of(2) {
296        progress
297      } else {
298        1.0 - progress
299      }
300    }
301    AnimationDirection::AlternateReverse => {
302      if iteration_index.is_multiple_of(2) {
303        1.0 - progress
304      } else {
305        progress
306      }
307    }
308  }
309}
310
311fn sample_keyframe_segment<'a>(
312  resolved_frames: &'a ResolvedKeyframes,
313  base_style: &'a ComputedStyle,
314  progress: f32,
315) -> Option<InterpolationSegment<'a>> {
316  let first = resolved_frames.points.first()?;
317
318  if progress <= first.offset {
319    let segment_progress = if first.offset <= 0.0 {
320      1.0
321    } else {
322      progress / first.offset
323    };
324    return Some(InterpolationSegment::new(
325      base_style,
326      None,
327      &resolved_frames.style(first.style_index).style,
328      Some(&resolved_frames.style(first.style_index).mask),
329      segment_progress.clamp(0.0, 1.0),
330    ));
331  }
332
333  for window in resolved_frames.points.windows(2) {
334    let [start_point, end_point] = window else {
335      continue;
336    };
337    if progress <= end_point.offset {
338      let width = end_point.offset - start_point.offset;
339      let segment_progress = if width <= f32::EPSILON {
340        1.0
341      } else {
342        (progress - start_point.offset) / width
343      };
344      return Some(InterpolationSegment::new(
345        &resolved_frames.style(start_point.style_index).style,
346        Some(&resolved_frames.style(start_point.style_index).mask),
347        &resolved_frames.style(end_point.style_index).style,
348        Some(&resolved_frames.style(end_point.style_index).mask),
349        segment_progress.clamp(0.0, 1.0),
350      ));
351    }
352  }
353
354  let last = resolved_frames.points.last()?;
355  let segment_progress = if last.offset >= 1.0 {
356    1.0
357  } else {
358    (progress - last.offset) / (1.0 - last.offset)
359  };
360  Some(InterpolationSegment::new(
361    &resolved_frames.style(last.style_index).style,
362    Some(&resolved_frames.style(last.style_index).mask),
363    base_style,
364    None,
365    segment_progress.clamp(0.0, 1.0),
366  ))
367}
368
369fn resolve_keyframes(keyframes: &KeyframesRule, base_style: &ComputedStyle) -> ResolvedKeyframes {
370  let mut points = keyframes
371    .keyframes
372    .iter()
373    .enumerate()
374    .flat_map(|(style_index, keyframe)| {
375      keyframe
376        .offsets
377        .iter()
378        .copied()
379        .map(move |offset| ResolvedKeyframePoint {
380          offset,
381          style_index,
382        })
383    })
384    .collect::<Vec<_>>();
385
386  points.sort_by(|lhs, rhs| {
387    lhs
388      .offset
389      .partial_cmp(&rhs.offset)
390      .unwrap_or(Ordering::Equal)
391  });
392
393  let mut styles = Vec::with_capacity(points.len());
394  let mut merged_points: Vec<ResolvedKeyframePoint> = Vec::with_capacity(points.len());
395  for point in points {
396    if let Some(last_point) = merged_points.last_mut()
397      && (last_point.offset - point.offset).abs() <= f32::EPSILON
398    {
399      merge_keyframe_style(
400        &mut styles[last_point.style_index],
401        &keyframes.keyframes[point.style_index],
402      );
403      continue;
404    }
405
406    let style_index = styles.len();
407    styles.push(resolve_keyframe_style(
408      &keyframes.keyframes[point.style_index],
409      base_style,
410    ));
411    merged_points.push(ResolvedKeyframePoint {
412      offset: point.offset,
413      style_index,
414    });
415  }
416
417  ResolvedKeyframes {
418    points: merged_points,
419    styles,
420  }
421}
422
423#[derive(Debug)]
424struct ResolvedKeyframeStyle {
425  style: ComputedStyle,
426  mask: PropertyMask,
427}
428
429impl ResolvedKeyframeStyle {
430  fn new(style: ComputedStyle, mask: PropertyMask) -> Self {
431    Self { style, mask }
432  }
433}
434
435#[derive(Debug)]
436struct ResolvedKeyframePoint {
437  offset: f32,
438  style_index: usize,
439}
440
441#[derive(Debug)]
442struct ResolvedKeyframes {
443  points: Vec<ResolvedKeyframePoint>,
444  styles: Vec<ResolvedKeyframeStyle>,
445}
446
447impl ResolvedKeyframes {
448  fn style(&self, index: usize) -> &ResolvedKeyframeStyle {
449    &self.styles[index]
450  }
451}
452
453#[derive(Debug)]
454struct InterpolationSegment<'a> {
455  from_style: &'a ComputedStyle,
456  to_style: &'a ComputedStyle,
457  animated_properties: PropertyMask,
458  progress: f32,
459}
460
461impl<'a> InterpolationSegment<'a> {
462  fn new(
463    from_style: &'a ComputedStyle,
464    from_mask: Option<&'a PropertyMask>,
465    to_style: &'a ComputedStyle,
466    to_mask: Option<&'a PropertyMask>,
467    progress: f32,
468  ) -> Self {
469    let mut animated_properties = PropertyMask::new();
470    if let Some(mask) = from_mask {
471      animated_properties.extend(mask.iter());
472    }
473    if let Some(mask) = to_mask {
474      animated_properties.extend(mask.iter());
475    }
476    Self {
477      from_style,
478      to_style,
479      animated_properties,
480      progress,
481    }
482  }
483}
484
485fn resolve_keyframe_style(
486  keyframe: &KeyframeRule,
487  base_style: &ComputedStyle,
488) -> ResolvedKeyframeStyle {
489  let mut style = base_style.clone();
490  let mut mask = PropertyMask::new();
491  apply_keyframe_declarations(&mut style, &mut mask, keyframe);
492  ResolvedKeyframeStyle::new(style, mask)
493}
494
495fn merge_keyframe_style(style: &mut ResolvedKeyframeStyle, keyframe: &KeyframeRule) {
496  apply_keyframe_declarations(&mut style.style, &mut style.mask, keyframe);
497}
498
499fn apply_keyframe_declarations(
500  style: &mut ComputedStyle,
501  mask: &mut PropertyMask,
502  keyframe: &KeyframeRule,
503) {
504  for declaration in keyframe.declarations.iter() {
505    declaration.apply_to_computed(style);
506    mask.extend(declaration.affected_longhands().iter());
507  }
508}
509
510macro_rules! impl_passthrough_animatable {
511  ($($ty:ty),* $(,)?) => {
512    $(
513      impl Animatable for $ty {}
514    )*
515  };
516}
517
518impl_passthrough_animatable!(
519  BoxSizing,
520  AnimationNames,
521  AnimationDurations,
522  AnimationTimingFunctions,
523  AnimationIterationCounts,
524  AnimationDirections,
525  AnimationFillModes,
526  AnimationPlayStates,
527  Clear,
528  Display,
529  Direction,
530  Float,
531  FlexDirection,
532  AlignItems,
533  JustifyContent,
534  FlexWrap,
535  Position,
536  BorderStyle,
537  Border,
538  ObjectFit,
539  Overflow,
540  BackgroundClip,
541  GridAutoFlow,
542  GridLine,
543  GridTemplateAreas,
544  TextOverflow,
545  TextTransform,
546  FontStyle,
547  FontFamily,
548  LineHeight,
549  FontSynthesis,
550  FontSynthesic,
551  LineClamp,
552  TextAlign,
553  TextStroke,
554  LineJoin,
555  TextDecoration,
556  TextDecorationLines,
557  TextDecorationStyle,
558  TextDecorationSkipInk,
559  ImageScalingAlgorithm,
560  OverflowWrap,
561  WordBreak,
562  BasicShape,
563  FillRule,
564  WhiteSpace,
565  WhiteSpaceCollapse,
566  TextWrapMode,
567  TextWrapStyle,
568  TextWrap,
569  Isolation,
570  Visibility,
571  VerticalAlign,
572  Flex,
573  Background,
574  GridTrackSize,
575  GridTemplateComponent,
576  FontFeature,
577  FontVariation,
578);
579
580impl<const DEFAULT_AUTO: bool> Animatable for Length<DEFAULT_AUTO> {
581  fn interpolate(
582    &mut self,
583    from: &Self,
584    to: &Self,
585    progress: f32,
586    sizing: &SizingContext,
587    _current_color: Color,
588  ) {
589    *self = interpolate_length(*from, *to, progress)
590      .or_else(|| {
591        resolve_length_with_sizing(*from, sizing).and_then(|resolved_from| {
592          resolve_length_with_sizing(*to, sizing)
593            .map(|resolved_to| Length::Px(lerp(resolved_from, resolved_to, progress)))
594        })
595      })
596      .unwrap_or(if progress >= 0.5 { *to } else { *from });
597  }
598}
599
600fn interpolate_length<const DEFAULT_AUTO: bool>(
601  from: Length<DEFAULT_AUTO>,
602  to: Length<DEFAULT_AUTO>,
603  progress: f32,
604) -> Option<Length<DEFAULT_AUTO>> {
605  macro_rules! lerp_variants {
606    ($($variant:ident),+ $(,)?) => {
607      match (from, to) {
608        $(
609          (Length::$variant(lhs), Length::$variant(rhs)) => {
610            Some(Length::$variant(lerp(lhs, rhs, progress)))
611          }
612        )+
613        (Length::Auto, Length::Auto) => Some(Length::Auto),
614        _ => None,
615      }
616    };
617  }
618  lerp_variants!(
619    Percentage, Rem, Em, Vh, Vw, CqH, CqW, CqMin, CqMax, VMin, VMax, Cm, Mm, In, Q, Pt, Pc, Px,
620  )
621}
622
623fn resolve_length_with_sizing<const DEFAULT_AUTO: bool>(
624  value: Length<DEFAULT_AUTO>,
625  sizing: &SizingContext,
626) -> Option<f32> {
627  if matches!(value, Length::Auto) {
628    return None;
629  }
630
631  Some(value.to_px(
632    sizing,
633    sizing.viewport.size.width.unwrap_or_default() as f32,
634  ))
635}
636
637#[cfg(test)]
638mod tests {
639  use std::rc::Rc;
640
641  use taffy::Size;
642
643  use crate::{
644    Viewport,
645    style::{
646      SizingContext,
647      animation::{sample_animation_progress, tailwind_animation_keyframes},
648      *,
649    },
650  };
651
652  fn sizing() -> SizingContext {
653    SizingContext {
654      viewport: Viewport::new((200, 100)),
655      container_size: Size::NONE,
656      font_size: 16.0,
657      root_font_size: None,
658      line_height: 0.0,
659      root_line_height: None,
660      calc_arena: Rc::new(CalcArena::default()),
661    }
662  }
663
664  fn current_color() -> Color {
665    Color([10, 20, 30, 255])
666  }
667
668  #[derive(Clone, Copy, Debug, PartialEq)]
669  struct Dummy(u8);
670
671  impl Animatable for Dummy {}
672
673  #[test]
674  fn animatable_default_flips_at_half_progress() {
675    let mut target = Dummy(9);
676    target.interpolate(&Dummy(3), &Dummy(7), 0.25, &sizing(), current_color());
677    assert_eq!(target, Dummy(3));
678
679    target.interpolate(&Dummy(3), &Dummy(7), 0.5, &sizing(), current_color());
680    assert_eq!(target, Dummy(7));
681  }
682
683  #[test]
684  fn length_interpolates_continuously() {
685    let mut target: Length = Length::zero();
686    target.interpolate(
687      &Length::Px(10.0),
688      &Length::Px(30.0),
689      0.25,
690      &sizing(),
691      current_color(),
692    );
693    assert_eq!(target, Length::Px(15.0));
694  }
695
696  #[test]
697  fn mixed_unit_length_interpolates_via_sizing() {
698    let mut target: Length = Length::zero();
699    target.interpolate(
700      &Length::Px(0.0),
701      &Length::Percentage(50.0),
702      0.5,
703      &sizing(),
704      current_color(),
705    );
706
707    assert_eq!(target, Length::Px(50.0));
708  }
709
710  #[test]
711  fn option_length_uses_discrete_fallback() {
712    let mut target: Option<Length> = None;
713    target.interpolate(
714      &Some(Length::Px(10.0)),
715      &None,
716      0.25,
717      &sizing(),
718      current_color(),
719    );
720    assert_eq!(target, Some(Length::Px(10.0)));
721
722    target.interpolate(
723      &Some(Length::Px(10.0)),
724      &None,
725      0.75,
726      &sizing(),
727      current_color(),
728    );
729    assert_eq!(target, None);
730  }
731
732  #[test]
733  fn background_position_interpolates_components() {
734    let mut target: BackgroundPosition = BackgroundPosition::default();
735    target.interpolate(
736      &BackgroundPosition(SpacePair::from_pair(
737        PositionComponent::KeywordX(PositionKeywordX::Left),
738        PositionComponent::KeywordY(PositionKeywordY::Top),
739      )),
740      &BackgroundPosition(SpacePair::from_pair(
741        PositionComponent::KeywordX(PositionKeywordX::Right),
742        PositionComponent::KeywordY(PositionKeywordY::Bottom),
743      )),
744      0.5,
745      &sizing(),
746      current_color(),
747    );
748
749    assert_eq!(
750      target,
751      BackgroundPosition(SpacePair::from_pair(
752        PositionComponent::Length(Length::Percentage(50.0)),
753        PositionComponent::Length(Length::Percentage(50.0)),
754      ))
755    );
756  }
757
758  #[test]
759  fn color_input_interpolates_using_current_color() {
760    let mut target: ColorInput = ColorInput::CurrentColor;
761    target.interpolate(
762      &ColorInput::CurrentColor,
763      &ColorInput::Value(Color([110, 120, 130, 255])),
764      0.5,
765      &sizing(),
766      current_color(),
767    );
768
769    assert_eq!(target, ColorInput::Value(Color([57, 67, 77, 255])));
770  }
771
772  #[test]
773  fn border_radius_interpolates_via_container_impls() {
774    let mut target = BorderRadius::default();
775    target.interpolate(
776      &BorderRadius::from(4.0),
777      &BorderRadius::from(12.0),
778      0.5,
779      &sizing(),
780      current_color(),
781    );
782
783    assert_eq!(target, BorderRadius::from(8.0));
784  }
785
786  #[test]
787  fn percentage_number_interpolates() {
788    let mut target = PercentageNumber::default();
789    target.interpolate(
790      &PercentageNumber(0.2),
791      &PercentageNumber(0.6),
792      0.5,
793      &sizing(),
794      current_color(),
795    );
796
797    assert!((target.0 - 0.4).abs() < f32::EPSILON);
798  }
799
800  #[test]
801  fn option_angle_interpolates_inner_angle() {
802    let mut target: Option<Angle> = None;
803    target.interpolate(
804      &Some(Angle::new(0.0)),
805      &Some(Angle::new(90.0)),
806      0.5,
807      &sizing(),
808      current_color(),
809    );
810
811    assert_eq!(target, Some(Angle::new(45.0)));
812  }
813
814  #[test]
815  fn option_angle_interpolates_from_missing_zero_angle() {
816    let mut target: Option<Angle> = None;
817    target.interpolate(
818      &None,
819      &Some(Angle::new(45.0)),
820      0.5,
821      &sizing(),
822      current_color(),
823    );
824
825    assert_eq!(target, Some(Angle::new(22.5)));
826  }
827
828  #[test]
829  fn aspect_ratio_interpolates_ratio_values() {
830    let mut target = AspectRatio::Auto;
831    target.interpolate(
832      &AspectRatio::Ratio(1.0),
833      &AspectRatio::Ratio(2.0),
834      0.25,
835      &sizing(),
836      current_color(),
837    );
838
839    assert_eq!(target, AspectRatio::Ratio(1.25));
840  }
841
842  #[test]
843  fn font_stretch_interpolates_percentages() {
844    let mut target = FontStretch::from_percentage(0.0);
845    target.interpolate(
846      &FontStretch::from_percentage(0.75),
847      &FontStretch::from_percentage(1.25),
848      0.5,
849      &sizing(),
850      current_color(),
851    );
852
853    assert!((target.percentage() - 1.0).abs() < f32::EPSILON);
854  }
855
856  #[test]
857  fn font_weight_interpolates_numeric_values() {
858    let mut target = FontWeight::default();
859    target.interpolate(
860      &FontWeight::from(400.0),
861      &FontWeight::from(700.0),
862      0.5,
863      &sizing(),
864      current_color(),
865    );
866
867    assert!((target.value() - 550.0).abs() < f32::EPSILON);
868  }
869
870  #[test]
871  fn text_decoration_thickness_interpolates_lengths() {
872    let mut target = TextDecorationThickness::default();
873    target.interpolate(
874      &TextDecorationThickness::Length(Length::Px(2.0)),
875      &TextDecorationThickness::Length(Length::Px(10.0)),
876      0.25,
877      &sizing(),
878      current_color(),
879    );
880
881    assert_eq!(target, TextDecorationThickness::Length(Length::Px(4.0)));
882  }
883
884  #[test]
885  fn flex_grow_interpolates_numeric_values() {
886    let mut target = FlexGrow(0.0);
887    target.interpolate(
888      &FlexGrow(1.0),
889      &FlexGrow(3.0),
890      0.5,
891      &sizing(),
892      current_color(),
893    );
894
895    assert!((target.0 - 2.0).abs() < f32::EPSILON);
896  }
897
898  #[test]
899  fn transform_translate_interpolates_lengths() {
900    let mut target = Transform::Translate(Length::zero(), Length::zero());
901    target.interpolate(
902      &Transform::Translate(Length::Px(0.0), Length::Px(10.0)),
903      &Transform::Translate(Length::Px(20.0), Length::Px(30.0)),
904      0.5,
905      &sizing(),
906      current_color(),
907    );
908
909    assert_eq!(
910      target,
911      Transform::Translate(Length::Px(10.0), Length::Px(20.0))
912    );
913  }
914
915  #[test]
916  fn background_size_interpolates_explicit_lengths() {
917    let mut target = BackgroundSize::default();
918    target.interpolate(
919      &BackgroundSize::Explicit {
920        width: Length::Px(10.0),
921        height: Length::Px(20.0),
922      },
923      &BackgroundSize::Explicit {
924        width: Length::Px(30.0),
925        height: Length::Px(60.0),
926      },
927      0.5,
928      &sizing(),
929      current_color(),
930    );
931
932    assert_eq!(
933      target,
934      BackgroundSize::Explicit {
935        width: Length::Px(20.0),
936        height: Length::Px(40.0),
937      }
938    );
939  }
940
941  #[test]
942  fn box_shadow_interpolates_lengths_and_color() {
943    let mut target = BoxShadow {
944      inset: false,
945      offset_x: Length::zero(),
946      offset_y: Length::zero(),
947      blur_radius: Length::zero(),
948      spread_radius: Length::zero(),
949      color: ColorInput::CurrentColor,
950    };
951    target.interpolate(
952      &BoxShadow {
953        inset: false,
954        offset_x: Length::Px(0.0),
955        offset_y: Length::Px(10.0),
956        blur_radius: Length::Px(20.0),
957        spread_radius: Length::Px(30.0),
958        color: ColorInput::Value(Color([0, 0, 0, 255])),
959      },
960      &BoxShadow {
961        inset: false,
962        offset_x: Length::Px(20.0),
963        offset_y: Length::Px(30.0),
964        blur_radius: Length::Px(40.0),
965        spread_radius: Length::Px(50.0),
966        color: ColorInput::Value(Color([200, 100, 50, 255])),
967      },
968      0.5,
969      &sizing(),
970      current_color(),
971    );
972
973    assert_eq!(
974      target,
975      BoxShadow {
976        inset: false,
977        offset_x: Length::Px(10.0),
978        offset_y: Length::Px(20.0),
979        blur_radius: Length::Px(30.0),
980        spread_radius: Length::Px(40.0),
981        color: ColorInput::Value(Color([76, 34, 13, 255])),
982      }
983    );
984  }
985
986  #[test]
987  fn text_shadow_interpolates_lengths_and_color() {
988    let mut target = TextShadow {
989      offset_x: Length::zero(),
990      offset_y: Length::zero(),
991      blur_radius: Length::zero(),
992      color: ColorInput::CurrentColor,
993    };
994    target.interpolate(
995      &TextShadow {
996        offset_x: Length::Px(0.0),
997        offset_y: Length::Px(10.0),
998        blur_radius: Length::Px(20.0),
999        color: ColorInput::Value(Color([0, 0, 0, 255])),
1000      },
1001      &TextShadow {
1002        offset_x: Length::Px(20.0),
1003        offset_y: Length::Px(30.0),
1004        blur_radius: Length::Px(40.0),
1005        color: ColorInput::Value(Color([200, 100, 50, 255])),
1006      },
1007      0.5,
1008      &sizing(),
1009      current_color(),
1010    );
1011
1012    assert_eq!(
1013      target,
1014      TextShadow {
1015        offset_x: Length::Px(10.0),
1016        offset_y: Length::Px(20.0),
1017        blur_radius: Length::Px(30.0),
1018        color: ColorInput::Value(Color([76, 34, 13, 255])),
1019      }
1020    );
1021  }
1022
1023  #[test]
1024  fn filter_blur_interpolates_lengths() {
1025    let mut target = Filter::Blur(Length::zero());
1026    target.interpolate(
1027      &Filter::Blur(Length::Px(4.0)),
1028      &Filter::Blur(Length::Px(12.0)),
1029      0.5,
1030      &sizing(),
1031      current_color(),
1032    );
1033
1034    assert_eq!(target, Filter::Blur(Length::Px(8.0)));
1035  }
1036
1037  #[test]
1038  fn animation_progress_uses_next_iteration_start_at_boundaries() {
1039    let progress = sample_animation_progress(
1040      1000.0,
1041      1000.0,
1042      0.0,
1043      AnimationIterationCount::Infinite,
1044      AnimationDirection::Alternate,
1045      AnimationFillMode::Both,
1046    );
1047
1048    assert_eq!(progress, Some(1.0));
1049  }
1050
1051  #[test]
1052  fn animation_progress_keeps_final_state_after_finite_completion() {
1053    let progress = sample_animation_progress(
1054      2000.0,
1055      1000.0,
1056      0.0,
1057      AnimationIterationCount::Number(2.0),
1058      AnimationDirection::Alternate,
1059      AnimationFillMode::Forwards,
1060    );
1061
1062    assert_eq!(progress, Some(0.0));
1063  }
1064
1065  #[test]
1066  fn animation_progress_keeps_fractional_final_iteration_state() {
1067    let progress = sample_animation_progress(
1068      1500.0,
1069      1000.0,
1070      0.0,
1071      AnimationIterationCount::Number(1.5),
1072      AnimationDirection::Normal,
1073      AnimationFillMode::Forwards,
1074    );
1075
1076    assert_eq!(progress, Some(0.5));
1077  }
1078
1079  #[test]
1080  fn animation_progress_uses_end_of_iteration_for_exact_normal_boundaries() {
1081    let progress = sample_animation_progress(
1082      1000.0,
1083      1000.0,
1084      0.0,
1085      AnimationIterationCount::Infinite,
1086      AnimationDirection::Normal,
1087      AnimationFillMode::Both,
1088    );
1089
1090    assert_eq!(progress, Some(1.0));
1091  }
1092
1093  #[test]
1094  fn tailwind_animation_presets_include_built_in_keyframes() {
1095    assert!(tailwind_animation_keyframes("spin").is_some());
1096    assert!(tailwind_animation_keyframes("ping").is_some());
1097    assert!(tailwind_animation_keyframes("pulse").is_some());
1098    assert!(tailwind_animation_keyframes("bounce").is_some());
1099  }
1100
1101  #[test]
1102  fn vec_animates_pairwise() {
1103    let mut values: Vec<Length> = vec![Length::Px(0.0), Length::Px(10.0)];
1104    values.interpolate(
1105      &vec![Length::Px(0.0), Length::Px(10.0)],
1106      &vec![Length::Px(20.0), Length::Px(30.0)],
1107      0.5,
1108      &sizing(),
1109      current_color(),
1110    );
1111
1112    assert_eq!(values, vec![Length::Px(10.0), Length::Px(20.0)]);
1113  }
1114
1115  #[test]
1116  fn vec_animates_repeatable_lists_to_lcm_length() {
1117    let mut values: Vec<BackgroundSize> = Vec::new();
1118    values.interpolate(
1119      &vec![BackgroundSize::Explicit {
1120        width: Length::Px(10.0),
1121        height: Length::Px(20.0),
1122      }],
1123      &vec![
1124        BackgroundSize::Explicit {
1125          width: Length::Px(30.0),
1126          height: Length::Px(40.0),
1127        },
1128        BackgroundSize::Explicit {
1129          width: Length::Px(50.0),
1130          height: Length::Px(60.0),
1131        },
1132      ],
1133      0.5,
1134      &sizing(),
1135      current_color(),
1136    );
1137
1138    assert_eq!(
1139      values,
1140      vec![
1141        BackgroundSize::Explicit {
1142          width: Length::Px(20.0),
1143          height: Length::Px(30.0),
1144        },
1145        BackgroundSize::Explicit {
1146          width: Length::Px(30.0),
1147          height: Length::Px(40.0),
1148        },
1149      ]
1150    );
1151  }
1152
1153  #[test]
1154  fn boxed_background_lists_animate_repeatable_lists_to_lcm_length() {
1155    let mut values: Box<[BackgroundSize]> = Box::default();
1156    values.interpolate(
1157      &[BackgroundSize::Explicit {
1158        width: Length::Px(10.0),
1159        height: Length::Px(20.0),
1160      }]
1161      .into(),
1162      &[
1163        BackgroundSize::Explicit {
1164          width: Length::Px(30.0),
1165          height: Length::Px(40.0),
1166        },
1167        BackgroundSize::Explicit {
1168          width: Length::Px(50.0),
1169          height: Length::Px(60.0),
1170        },
1171      ]
1172      .into(),
1173      0.5,
1174      &sizing(),
1175      current_color(),
1176    );
1177
1178    assert_eq!(
1179      values,
1180      [
1181        BackgroundSize::Explicit {
1182          width: Length::Px(20.0),
1183          height: Length::Px(30.0),
1184        },
1185        BackgroundSize::Explicit {
1186          width: Length::Px(30.0),
1187          height: Length::Px(40.0),
1188        },
1189      ]
1190      .into()
1191    );
1192  }
1193
1194  #[test]
1195  fn boxed_transform_lists_pad_to_longest_with_neutral_values() {
1196    let mut values: Box<[Transform]> = Box::default();
1197    values.interpolate(
1198      &[Transform::Scale(1.0, 1.0)].into(),
1199      &[Transform::Scale(2.0, 2.0), Transform::Scale(4.0, 4.0)].into(),
1200      0.5,
1201      &sizing(),
1202      current_color(),
1203    );
1204
1205    assert_eq!(
1206      values,
1207      [Transform::Scale(1.5, 1.5), Transform::Scale(2.5, 2.5)].into()
1208    );
1209  }
1210
1211  #[test]
1212  fn apply_interpolated_properties_only_updates_masked_fields() {
1213    let mut base_style = ComputedStyle {
1214      width: Length::Px(10.0),
1215      height: Length::Px(20.0),
1216      ..ComputedStyle::default()
1217    };
1218    let from = ComputedStyle {
1219      width: Length::Px(10.0),
1220      height: Length::Px(100.0),
1221      ..ComputedStyle::default()
1222    };
1223    let to = ComputedStyle {
1224      width: Length::Px(30.0),
1225      height: Length::Px(200.0),
1226      ..ComputedStyle::default()
1227    };
1228    let animated_properties: PropertyMask = [LonghandId::Width].into_iter().collect();
1229
1230    base_style.apply_interpolated_properties(
1231      &from,
1232      &to,
1233      &animated_properties,
1234      0.5,
1235      &sizing(),
1236      current_color(),
1237    );
1238
1239    assert_eq!(base_style.width, Length::Px(20.0));
1240    assert_eq!(base_style.height, Length::Px(20.0));
1241  }
1242
1243  #[test]
1244  fn apply_interpolated_properties_interpolates_rotate_from_implicit_none() {
1245    let mut base_style = ComputedStyle::default();
1246    let from = ComputedStyle::default();
1247    let to = ComputedStyle {
1248      rotate: Some(Angle::new(45.0)),
1249      ..ComputedStyle::default()
1250    };
1251    let animated_properties: PropertyMask = [LonghandId::Rotate].into_iter().collect();
1252
1253    base_style.apply_interpolated_properties(
1254      &from,
1255      &to,
1256      &animated_properties,
1257      0.5,
1258      &sizing(),
1259      current_color(),
1260    );
1261
1262    assert_eq!(base_style.rotate, Some(Angle::new(22.5)));
1263  }
1264
1265  #[test]
1266  fn apply_interpolated_properties_interpolates_flex_grow_from_implicit_zero() {
1267    let mut base_style = ComputedStyle::default();
1268    let from = ComputedStyle::default();
1269    let to = ComputedStyle {
1270      flex_grow: Some(FlexGrow(4.0)),
1271      ..ComputedStyle::default()
1272    };
1273    let animated_properties: PropertyMask = [LonghandId::FlexGrow].into_iter().collect();
1274
1275    base_style.apply_interpolated_properties(
1276      &from,
1277      &to,
1278      &animated_properties,
1279      0.5,
1280      &sizing(),
1281      current_color(),
1282    );
1283
1284    assert_eq!(base_style.flex_grow, Some(FlexGrow(2.0)));
1285  }
1286
1287  #[test]
1288  fn apply_interpolated_properties_interpolates_flex_shrink_from_implicit_one() {
1289    let mut base_style = ComputedStyle::default();
1290    let from = ComputedStyle::default();
1291    let to = ComputedStyle {
1292      flex_shrink: Some(FlexGrow(3.0)),
1293      ..ComputedStyle::default()
1294    };
1295    let animated_properties: PropertyMask = [LonghandId::FlexShrink].into_iter().collect();
1296
1297    base_style.apply_interpolated_properties(
1298      &from,
1299      &to,
1300      &animated_properties,
1301      0.5,
1302      &sizing(),
1303      current_color(),
1304    );
1305
1306    assert_eq!(base_style.flex_shrink, Some(FlexGrow(2.0)));
1307  }
1308
1309  #[test]
1310  fn apply_interpolated_properties_interpolates_text_stroke_width_from_implicit_zero() {
1311    let mut base_style = ComputedStyle::default();
1312    let from = ComputedStyle::default();
1313    let to = ComputedStyle {
1314      webkit_text_stroke_width: Some(Length::Px(6.0)),
1315      ..ComputedStyle::default()
1316    };
1317    let animated_properties: PropertyMask =
1318      [LonghandId::WebkitTextStrokeWidth].into_iter().collect();
1319
1320    base_style.apply_interpolated_properties(
1321      &from,
1322      &to,
1323      &animated_properties,
1324      0.5,
1325      &sizing(),
1326      current_color(),
1327    );
1328
1329    assert_eq!(base_style.webkit_text_stroke_width, Some(Length::Px(3.0)));
1330  }
1331
1332  #[test]
1333  fn apply_interpolated_properties_interpolates_text_stroke_color_from_current_color() {
1334    let mut base_style = ComputedStyle::default();
1335    let from = ComputedStyle::default();
1336    let to = ComputedStyle {
1337      webkit_text_stroke_color: Some(ColorInput::Value(Color([110, 120, 130, 255]))),
1338      ..ComputedStyle::default()
1339    };
1340    let animated_properties: PropertyMask =
1341      [LonghandId::WebkitTextStrokeColor].into_iter().collect();
1342
1343    base_style.apply_interpolated_properties(
1344      &from,
1345      &to,
1346      &animated_properties,
1347      0.5,
1348      &sizing(),
1349      current_color(),
1350    );
1351
1352    assert_eq!(
1353      base_style.webkit_text_stroke_color,
1354      Some(ColorInput::Value(Color([57, 67, 77, 255])))
1355    );
1356  }
1357
1358  #[test]
1359  fn apply_interpolated_properties_interpolates_text_fill_color_from_style_color() {
1360    let mut base_style = ComputedStyle::default();
1361    let from = ComputedStyle {
1362      color: ColorInput::Value(Color([20, 40, 60, 255])),
1363      ..ComputedStyle::default()
1364    };
1365    let to = ComputedStyle {
1366      color: ColorInput::Value(Color([20, 40, 60, 255])),
1367      webkit_text_fill_color: Some(ColorInput::Value(Color([120, 140, 160, 255]))),
1368      ..ComputedStyle::default()
1369    };
1370    let animated_properties: PropertyMask = [LonghandId::WebkitTextFillColor].into_iter().collect();
1371
1372    base_style.apply_interpolated_properties(
1373      &from,
1374      &to,
1375      &animated_properties,
1376      0.5,
1377      &sizing(),
1378      current_color(),
1379    );
1380
1381    assert_eq!(
1382      base_style.webkit_text_fill_color,
1383      Some(ColorInput::Value(Color([68, 88, 108, 255])))
1384    );
1385  }
1386
1387  #[test]
1388  fn text_indent_interpolates_amount_continuously() {
1389    let mut target = TextIndent::default();
1390    let from = TextIndent {
1391      amount: LengthDefaultsToZero::Px(10.0),
1392      each_line: false,
1393      hanging: false,
1394    };
1395    let to = TextIndent {
1396      amount: LengthDefaultsToZero::Px(30.0),
1397      each_line: true,
1398      hanging: true,
1399    };
1400
1401    target.interpolate(&from, &to, 0.25, &sizing(), current_color());
1402    assert_eq!(target.amount, LengthDefaultsToZero::Px(15.0));
1403    assert!(!target.each_line);
1404    assert!(!target.hanging);
1405
1406    target.interpolate(&from, &to, 0.75, &sizing(), current_color());
1407    assert_eq!(target.amount, LengthDefaultsToZero::Px(25.0));
1408    assert!(target.each_line);
1409    assert!(target.hanging);
1410  }
1411}