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")]
20pub struct KeyframeRule {
22 #[builder(setter(into))]
24 pub offsets: Vec<f32>,
25 pub declarations: StyleDeclarationBlock,
27}
28
29#[derive(Debug, Clone, Deserialize, PartialEq, TypedBuilder)]
30#[serde(rename_all = "camelCase")]
31pub struct KeyframesRule {
33 #[builder(setter(into))]
35 pub name: String,
36 #[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}