Skip to content

Commit

Permalink
Auto merge of #27032 - mrobinson:fractional-iteration, r=<try>
Browse files Browse the repository at this point in the history
animations: Finish support for fractional iteration counts

This change also improves support for creating animations with negative
delays, as that is necessary to test support for fractional iteration
lengths.

Fixes: #14858

<!-- Please describe your changes on the following line: -->

---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] These changes fix #14858
- [x] There are tests for these changes

<!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.-->

<!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->
  • Loading branch information
bors-servo committed Jun 24, 2020
2 parents 362dbd3 + cf45100 commit c3a1b47
Show file tree
Hide file tree
Showing 20 changed files with 173 additions and 119 deletions.
12 changes: 8 additions & 4 deletions components/script/animations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,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,
};
Expand All @@ -402,10 +402,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!(),
}
Expand Down
164 changes: 104 additions & 60 deletions components/style/animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, Keyf
use crate::values::animated::{Animate, Procedure};
use crate::values::computed::{Time, TimingFunction};
use crate::values::generics::box_::AnimationIterationCount;
use crate::values::generics::easing::{StepPosition, TimingFunction as GenericTimingFunction};
use crate::values::generics::easing::{
StepPosition, TimingFunction as GenericTimingFunction, TimingKeyword,
};
use crate::Atom;
use fxhash::FxHashMap;
use parking_lot::RwLock;
Expand Down Expand Up @@ -125,8 +127,14 @@ impl PropertyAnimation {
(current_step as f64) / (jumps as f64)
},
GenericTimingFunction::Keyword(keyword) => {
let (x1, x2, y1, y2) = keyword.to_bezier();
Bezier::new(x1, x2, y1, y2).solve(progress, epsilon)
let bezier = match keyword {
TimingKeyword::Linear => return progress,
TimingKeyword::Ease => Bezier::new(0.25, 0.1, 0.25, 1.),
TimingKeyword::EaseIn => Bezier::new(0.42, 0., 1., 1.),
TimingKeyword::EaseOut => Bezier::new(0., 0., 0.58, 1.),
TimingKeyword::EaseInOut => Bezier::new(0.42, 0., 0.58, 1.),
};
bezier.solve(progress, epsilon)
},
}
}
Expand Down Expand Up @@ -470,19 +478,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 => {
Expand All @@ -494,36 +510,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.
Expand Down Expand Up @@ -601,38 +636,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
Expand Down Expand Up @@ -674,45 +707,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);
}
}
Expand Down Expand Up @@ -1319,7 +1350,7 @@ pub fn maybe_start_animations<E>(
};

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;
}
Expand All @@ -1339,8 +1370,11 @@ pub fn maybe_start_animations<E>(
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()),
Expand All @@ -1357,8 +1391,11 @@ pub fn maybe_start_animations<E>(
},
};

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,
};

Expand All @@ -1371,12 +1408,12 @@ pub fn maybe_start_animations<E>(
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,
Expand All @@ -1387,6 +1424,13 @@ pub fn maybe_start_animations<E>(
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.
Expand Down
16 changes: 0 additions & 16 deletions components/style/values/generics/easing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
//! https://drafts.csswg.org/css-easing/#timing-functions

use crate::parser::ParserContext;
use crate::values::CSSFloat;

/// A generic easing function.
#[derive(
Expand Down Expand Up @@ -118,18 +117,3 @@ impl<Integer, Number> TimingFunction<Integer, Number> {
TimingFunction::Keyword(TimingKeyword::Ease)
}
}

impl TimingKeyword {
/// Returns the keyword as a quadruplet of Bezier point coordinates
/// `(x1, y1, x2, y2)`.
#[inline]
pub fn to_bezier(self) -> (CSSFloat, CSSFloat, CSSFloat, CSSFloat) {
match self {
TimingKeyword::Linear => (0., 0., 1., 1.),
TimingKeyword::Ease => (0.25, 0.1, 0.25, 1.),
TimingKeyword::EaseIn => (0.42, 0., 1., 1.),
TimingKeyword::EaseOut => (0., 0., 0.58, 1.),
TimingKeyword::EaseInOut => (0.42, 0., 0.58, 1.),
}
}
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

0 comments on commit c3a1b47

Please sign in to comment.