diff --git a/components/script/animations.rs b/components/script/animations.rs index bddbac149663..854a06a1e125 100644 --- a/components/script/animations.rs +++ b/components/script/animations.rs @@ -365,7 +365,7 @@ impl Animations { now: f64, pipeline_id: PipelineId, ) { - let num_iterations = match animation.iteration_state { + let iteration_index = match animation.iteration_state { KeyframesIterationState::Finite(current, _) | KeyframesIterationState::Infinite(current) => current, }; @@ -381,10 +381,14 @@ impl Animations { TransitionOrAnimationEventType::AnimationStart => { (-animation.delay).max(0.).min(active_duration) }, - TransitionOrAnimationEventType::AnimationIteration | - TransitionOrAnimationEventType::AnimationEnd => num_iterations * animation.duration, + TransitionOrAnimationEventType::AnimationIteration => { + iteration_index * animation.duration + }, + TransitionOrAnimationEventType::AnimationEnd => { + (iteration_index * animation.duration) + animation.current_iteration_duration() + }, TransitionOrAnimationEventType::AnimationCancel => { - (num_iterations * animation.duration) + (now - animation.started_at).max(0.) + (iteration_index * animation.duration) + (now - animation.started_at).max(0.) }, _ => unreachable!(), } diff --git a/components/style/animation.rs b/components/style/animation.rs index 197ba5be0cf1..6632ce856b7e 100644 --- a/components/style/animation.rs +++ b/components/style/animation.rs @@ -470,19 +470,27 @@ impl Animation { return false; } + if self.on_last_iteration() { + return false; + } + + self.iterate(); + true + } + + fn iterate(&mut self) { + debug_assert!(!self.on_last_iteration()); + if let KeyframesIterationState::Finite(ref mut current, max) = self.iteration_state { - // If we are already on the final iteration, just exit now. This prevents - // us from updating the direction, which might be needed for the correct - // handling of animation-fill-mode and also firing animationiteration events - // at the end of animations. *current = (*current + 1.).min(max); - if *current == max { - return false; - } + } + + if let AnimationState::Paused(ref mut progress) = self.state { + debug_assert!(*progress > 1.); + *progress -= 1.; } // Update the next iteration direction if applicable. - // TODO(mrobinson): The duration might now be wrong for floating point iteration counts. self.started_at += self.duration; match self.direction { AnimationDirection::Alternate | AnimationDirection::AlternateReverse => { @@ -494,36 +502,55 @@ impl Animation { }, _ => {}, } + } - true + /// A number (> 0 and <= 1) which represents the fraction of a full iteration + /// that the current iteration of the animation lasts. This will be less than 1 + /// if the current iteration is the fractional remainder of a non-integral + /// iteration count. + pub fn current_iteration_end_progress(&self) -> f64 { + match self.iteration_state { + KeyframesIterationState::Finite(current, max) => (max - current).min(1.), + KeyframesIterationState::Infinite(_) => 1., + } } + /// The duration of the current iteration of this animation which may be less + /// than the animation duration if it has a non-integral iteration count. + pub fn current_iteration_duration(&self) -> f64 { + self.current_iteration_end_progress() * self.duration + } + + /// Whether or not the current iteration is over. Note that this method assumes that + /// the animation is still running. fn iteration_over(&self, time: f64) -> bool { - time > (self.started_at + self.duration) + time > (self.started_at + self.current_iteration_duration()) + } + + /// Assuming this animation is running, whether or not it is on the last iteration. + fn on_last_iteration(&self) -> bool { + match self.iteration_state { + KeyframesIterationState::Finite(current, max) => current >= (max - 1.), + KeyframesIterationState::Infinite(_) => false, + } } /// Whether or not this animation has finished at the provided time. This does /// not take into account canceling i.e. when an animation or transition is /// canceled due to changes in the style. pub fn has_ended(&self, time: f64) -> bool { - match self.state { - AnimationState::Running => {}, - AnimationState::Finished => return true, - AnimationState::Pending | AnimationState::Canceled | AnimationState::Paused(_) => { - return false - }, - } - - if !self.iteration_over(time) { + if !self.on_last_iteration() { return false; } - // If we have a limited number of iterations and we cannot advance to another - // iteration, then we have ended. - return match self.iteration_state { - KeyframesIterationState::Finite(current, max) => max == current, - KeyframesIterationState::Infinite(..) => false, + let progress = match self.state { + AnimationState::Finished => return true, + AnimationState::Paused(progress) => progress, + AnimationState::Running => (time - self.started_at) / self.duration, + AnimationState::Pending | AnimationState::Canceled => return false, }; + + progress >= self.current_iteration_end_progress() } /// Updates the appropiate state from other animation. @@ -601,38 +628,36 @@ impl Animation { /// Fill in an `AnimationValueMap` with values calculated from this animation at /// the given time value. fn get_property_declaration_at_time(&self, now: f64, map: &mut AnimationValueMap) { - let duration = self.duration; - let started_at = self.started_at; + debug_assert!(!self.computed_steps.is_empty()); - let now = match self.state { - AnimationState::Running | AnimationState::Pending | AnimationState::Finished => now, - AnimationState::Paused(progress) => started_at + duration * progress, + let total_progress = match self.state { + AnimationState::Running | AnimationState::Pending | AnimationState::Finished => { + (now - self.started_at) / self.duration + }, + AnimationState::Paused(progress) => progress, AnimationState::Canceled => return, }; - debug_assert!(!self.computed_steps.is_empty()); - - let mut total_progress = (now - started_at) / duration; if total_progress < 0. && self.fill_mode != AnimationFillMode::Backwards && self.fill_mode != AnimationFillMode::Both { return; } - - if total_progress > 1. && + if self.has_ended(now) && self.fill_mode != AnimationFillMode::Forwards && self.fill_mode != AnimationFillMode::Both { return; } - total_progress = total_progress.min(1.0).max(0.0); + let total_progress = total_progress + .min(self.current_iteration_end_progress()) + .max(0.0); // Get the indices of the previous (from) keyframe and the next (to) keyframe. let next_keyframe_index; let prev_keyframe_index; let num_steps = self.computed_steps.len(); - debug_assert!(num_steps > 0); match self.current_direction { AnimationDirection::Normal => { next_keyframe_index = self @@ -674,45 +699,43 @@ impl Animation { None => return, }; + // If we only need to take into account one keyframe, then exit early + // in order to avoid doing more work. let mut add_declarations_to_map = |keyframe: &ComputedKeyframe| { for value in keyframe.values.iter() { map.insert(value.id(), value.clone()); } }; - if total_progress <= 0.0 { add_declarations_to_map(&prev_keyframe); return; } - if total_progress >= 1.0 { add_declarations_to_map(&next_keyframe); return; } - let relative_timespan = - (next_keyframe.start_percentage - prev_keyframe.start_percentage).abs(); - let relative_duration = relative_timespan as f64 * duration; - let last_keyframe_ended_at = match self.current_direction { - AnimationDirection::Normal => { - self.started_at + (duration * prev_keyframe.start_percentage as f64) - }, - AnimationDirection::Reverse => { - self.started_at + (duration * (1. - prev_keyframe.start_percentage as f64)) - }, + let percentage_between_keyframes = + (next_keyframe.start_percentage - prev_keyframe.start_percentage).abs() as f64; + let duration_between_keyframes = percentage_between_keyframes * self.duration; + let direction_aware_prev_keyframe_start_percentage = match self.current_direction { + AnimationDirection::Normal => prev_keyframe.start_percentage as f64, + AnimationDirection::Reverse => 1. - prev_keyframe.start_percentage as f64, _ => unreachable!(), }; + let progress_between_keyframes = (total_progress - + direction_aware_prev_keyframe_start_percentage) / + percentage_between_keyframes; - let relative_progress = (now - last_keyframe_ended_at) / relative_duration; for (from, to) in prev_keyframe.values.iter().zip(next_keyframe.values.iter()) { let animation = PropertyAnimation { from: from.clone(), to: to.clone(), timing_function: prev_keyframe.timing_function, - duration: relative_duration as f64, + duration: duration_between_keyframes as f64, }; - if let Ok(value) = animation.calculate_value(relative_progress) { + if let Ok(value) = animation.calculate_value(progress_between_keyframes) { map.insert(value.id(), value); } } @@ -1319,7 +1342,7 @@ pub fn maybe_start_animations( }; debug!("maybe_start_animations: name={}", name); - let duration = box_style.animation_duration_mod(i).seconds(); + let duration = box_style.animation_duration_mod(i).seconds() as f64; if duration == 0. { continue; } @@ -1339,8 +1362,11 @@ pub fn maybe_start_animations( continue; } + // NB: This delay may be negative, meaning that the animation may be created + // in a state where we have advanced one or more iterations or even that the + // animation begins in a finished state. let delay = box_style.animation_delay_mod(i).seconds(); - let animation_start = context.current_time_for_animations + delay as f64; + let iteration_state = match box_style.animation_iteration_count_mod(i) { AnimationIterationCount::Infinite => KeyframesIterationState::Infinite(0.0), AnimationIterationCount::Number(n) => KeyframesIterationState::Finite(0.0, n.into()), @@ -1357,8 +1383,11 @@ pub fn maybe_start_animations( }, }; + let now = context.current_time_for_animations; + let started_at = now + delay as f64; + let mut starting_progress = (now - started_at) / duration; let state = match box_style.animation_play_state_mod(i) { - AnimationPlayState::Paused => AnimationState::Paused(0.), + AnimationPlayState::Paused => AnimationState::Paused(starting_progress), AnimationPlayState::Running => AnimationState::Pending, }; @@ -1371,12 +1400,12 @@ pub fn maybe_start_animations( resolver, ); - let new_animation = Animation { + let mut new_animation = Animation { name: name.clone(), properties_changed: keyframe_animation.properties_changed, computed_steps, - started_at: animation_start, - duration: duration as f64, + started_at, + duration, fill_mode: box_style.animation_fill_mode_mod(i), delay: delay as f64, iteration_state, @@ -1387,6 +1416,13 @@ pub fn maybe_start_animations( is_new: true, }; + // If we started with a negative delay, make sure we iterate the animation if + // the delay moves us past the first iteration. + while starting_progress > 1. && !new_animation.on_last_iteration() { + new_animation.iterate(); + starting_progress -= 1.; + } + animation_state.dirty = true; // If the animation was already present in the list for the node, just update its state. diff --git a/tests/wpt/metadata-layout-2020/css/css-animations/animation-delay-011.html.ini b/tests/wpt/metadata-layout-2020/css/css-animations/animation-delay-011.html.ini deleted file mode 100644 index 78b881595612..000000000000 --- a/tests/wpt/metadata-layout-2020/css/css-animations/animation-delay-011.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[animation-delay-011.html] - expected: FAIL diff --git a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-blur.html.ini b/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-blur.html.ini deleted file mode 100644 index 800cc4b478d5..000000000000 --- a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-blur.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[css-filters-animation-blur.html] - expected: FAIL diff --git a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-brightness.html.ini b/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-brightness.html.ini deleted file mode 100644 index f11997b584ae..000000000000 --- a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-brightness.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[css-filters-animation-brightness.html] - expected: FAIL diff --git a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-combined-001.html.ini b/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-combined-001.html.ini deleted file mode 100644 index 0344b35229bd..000000000000 --- a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-combined-001.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[css-filters-animation-combined-001.html] - expected: FAIL diff --git a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-contrast.html.ini b/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-contrast.html.ini deleted file mode 100644 index 2695f0f7f9cf..000000000000 --- a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-contrast.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[css-filters-animation-contrast.html] - expected: FAIL diff --git a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-grayscale.html.ini b/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-grayscale.html.ini deleted file mode 100644 index 8f2f124651c5..000000000000 --- a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-grayscale.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[css-filters-animation-grayscale.html] - expected: FAIL diff --git a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-invert.html.ini b/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-invert.html.ini deleted file mode 100644 index ff2c841595d3..000000000000 --- a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-invert.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[css-filters-animation-invert.html] - expected: FAIL diff --git a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-opacity.html.ini b/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-opacity.html.ini deleted file mode 100644 index 7fe7b9058bdc..000000000000 --- a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-opacity.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[css-filters-animation-opacity.html] - expected: FAIL diff --git a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-saturate.html.ini b/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-saturate.html.ini deleted file mode 100644 index 6134590bfd9c..000000000000 --- a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-saturate.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[css-filters-animation-saturate.html] - expected: FAIL diff --git a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-sepia.html.ini b/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-sepia.html.ini deleted file mode 100644 index e827ddafe8ab..000000000000 --- a/tests/wpt/metadata-layout-2020/css/filter-effects/css-filters-animation-sepia.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[css-filters-animation-sepia.html] - expected: FAIL diff --git a/tests/wpt/metadata/MANIFEST.json b/tests/wpt/metadata/MANIFEST.json index 5d2c1f328221..b1c526b2fa28 100644 --- a/tests/wpt/metadata/MANIFEST.json +++ b/tests/wpt/metadata/MANIFEST.json @@ -385595,6 +385595,13 @@ {} ] ], + "animation-iteration-count-009.html": [ + "da86c81b9337a99841977acd8e2ffe8b8e858190", + [ + null, + {} + ] + ], "animation-iteration-count-calc.html": [ "44e1e96a589a4e1c5b98e919e7246d05097b0604", [ diff --git a/tests/wpt/metadata/css/css-ui/outline-017.html.ini b/tests/wpt/metadata/css/css-ui/outline-017.html.ini deleted file mode 100644 index 6ef00c57d8b6..000000000000 --- a/tests/wpt/metadata/css/css-ui/outline-017.html.ini +++ /dev/null @@ -1,10 +0,0 @@ -[outline-017.html] - [outline-color is animated as a color] - expected: FAIL - - [outline-width is animated as a length] - expected: FAIL - - [outline-offset is animated as a length] - expected: FAIL - diff --git a/tests/wpt/metadata/css/css-ui/outline-018.html.ini b/tests/wpt/metadata/css/css-ui/outline-018.html.ini deleted file mode 100644 index a2fea6c00e10..000000000000 --- a/tests/wpt/metadata/css/css-ui/outline-018.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[outline-018.html] - [outline-style is animated as a discrete type] - expected: FAIL - diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index 7923f209acb4..777a77d33886 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -12867,7 +12867,7 @@ "css": { "animations": { "animation-delay.html": [ - "0d2053a9134d8ff0ade7b5dc37ecfce305557c44", + "f54cf2b9bca93e82b177243b9d754e4ae3bf15fa", [ null, {} @@ -12883,7 +12883,7 @@ ] ], "animation-fill-mode.html": [ - "9602ec9f0e0eb1f6efcc2e7bd95181ef65339bae", + "ac3062879af9836768890d653f4b29b6165b6a45", [ null, {} diff --git a/tests/wpt/mozilla/tests/css/animations/animation-delay.html b/tests/wpt/mozilla/tests/css/animations/animation-delay.html index 0d2053a9134d..f54cf2b9bca9 100644 --- a/tests/wpt/mozilla/tests/css/animations/animation-delay.html +++ b/tests/wpt/mozilla/tests/css/animations/animation-delay.html @@ -31,6 +31,7 @@ element.style.animationIterationCount = 1; element.style.animationName = "width-animation"; element.style.animationTimingFunction = "linear"; + element.style.animationFillMode = "forwards"; document.body.appendChild(element); assert_equals(getComputedStyle(element).getPropertyValue("width"), "50px"); @@ -48,7 +49,7 @@ assert_equals(getComputedStyle(element).getPropertyValue("width"), "500px"); testBinding.advanceClock(500); - assert_equals(getComputedStyle(element).getPropertyValue("width"), "50px"); + assert_equals(getComputedStyle(element).getPropertyValue("width"), "500px"); }, "animation-delay should function correctly"); test(function() { @@ -61,6 +62,7 @@ element.style.animationIterationCount = 2; element.style.animationName = "width-animation"; element.style.animationTimingFunction = "linear"; + element.style.animationFillMode = "forwards"; document.body.appendChild(element); assert_equals(getComputedStyle(element).getPropertyValue("width"), "50px"); @@ -85,7 +87,7 @@ assert_equals(getComputedStyle(element).getPropertyValue("width"), "500px"); testBinding.advanceClock(500); - assert_equals(getComputedStyle(element).getPropertyValue("width"), "50px"); + assert_equals(getComputedStyle(element).getPropertyValue("width"), "500px"); }, "animation-delay should function correctly with multiple iterations"); diff --git a/tests/wpt/mozilla/tests/css/animations/animation-fill-mode.html b/tests/wpt/mozilla/tests/css/animations/animation-fill-mode.html index 9602ec9f0e0e..ac3062879af9 100644 --- a/tests/wpt/mozilla/tests/css/animations/animation-fill-mode.html +++ b/tests/wpt/mozilla/tests/css/animations/animation-fill-mode.html @@ -48,8 +48,9 @@ testBinding.advanceClock(500); assert_equals(getComputedStyle(element).getPropertyValue("width"), "250px"); + // After advancing another 500 milliseconds the animation should finished and + // width value will depend on the value of `animation-fill-mode`. testBinding.advanceClock(500); - assert_equals(getComputedStyle(element).getPropertyValue("width"), "500px"); } test(function() { diff --git a/tests/wpt/web-platform-tests/css/css-animations/animation-iteration-count-009.html b/tests/wpt/web-platform-tests/css/css-animations/animation-iteration-count-009.html new file mode 100644 index 000000000000..da86c81b9337 --- /dev/null +++ b/tests/wpt/web-platform-tests/css/css-animations/animation-iteration-count-009.html @@ -0,0 +1,46 @@ + + +CSS Animation Test: fractional animation-iteration-count + + + + + + +
+