Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add support for faster reversing of interrupted transitions
This is described in the spec and allows interrupted transitions to
reverse in a more natural way. Unfortunately, most of the tests that
exercise this behavior use the WebAnimations API. This change adds a
test using our custom clock control API.
  • Loading branch information
mrobinson committed May 20, 2020
1 parent c7fc4dd commit 4ba15c3
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 36 deletions.
186 changes: 150 additions & 36 deletions components/style/animation.rs
Expand Up @@ -574,9 +574,84 @@ pub struct Transition {
/// Whether or not this transition is new and or has already been tracked
/// by the script thread.
pub is_new: bool,

/// If this `Transition` has been replaced by a new one this field is
/// used to help produce better reversed transitions.
pub reversing_adjusted_start_value: AnimationValue,

/// If this `Transition` has been replaced by a new one this field is
/// used to help produce better reversed transitions.
pub reversing_shortening_factor: f64,
}

impl Transition {
fn update_for_possibly_reversed_transition(
&mut self,
replaced_transition: &Transition,
delay: f64,
now: f64,
) {
// If we reach here, we need to calculate a reversed transition according to
// https://drafts.csswg.org/css-transitions/#starting
//
// "...if the reversing-adjusted start value of the running transition
// is the same as the value of the property in the after-change style (see
// the section on reversing of transitions for why these case exists),
// implementations must cancel the running transition and start
// a new transition..."
if replaced_transition.reversing_adjusted_start_value != self.property_animation.to {
return;
}

// "* reversing-adjusted start value is the end value of the running transition"
let replaced_animation = &replaced_transition.property_animation;
self.reversing_adjusted_start_value = replaced_animation.to.clone();

// "* reversing shortening factor is the absolute value, clamped to the
// range [0, 1], of the sum of:
// 1. the output of the timing function of the old transition at the
// time of the style change event, times the reversing shortening
// factor of the old transition
// 2. 1 minus the reversing shortening factor of the old transition."
let transition_progress = replaced_transition.progress(now);
let timing_function_output = replaced_animation.timing_function_output(transition_progress);
let old_reversing_shortening_factor = replaced_transition.reversing_shortening_factor;
self.reversing_shortening_factor = ((timing_function_output *
old_reversing_shortening_factor) +
(1.0 - old_reversing_shortening_factor))
.abs()
.min(1.0)
.max(0.0);

// "* start time is the time of the style change event plus:
// 1. if the matching transition delay is nonnegative, the matching
// transition delay, or.
// 2. if the matching transition delay is negative, the product of the new
// transition’s reversing shortening factor and the matching transition delay,"
self.start_time = if delay >= 0. {
now + delay
} else {
now + (self.reversing_shortening_factor * delay)
};

// "* end time is the start time plus the product of the matching transition
// duration and the new transition’s reversing shortening factor,"
self.property_animation.duration *= self.reversing_shortening_factor;

// "* start value is the current value of the property in the running transition,
// * end value is the value of the property in the after-change style,"
let procedure = Procedure::Interpolate {
progress: timing_function_output,
};
match replaced_animation
.from
.animate(&replaced_animation.to, procedure)
{
Ok(new_start) => self.property_animation.from = new_start,
Err(..) => {},
}
}

/// Whether or not this animation has ended 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.
Expand Down Expand Up @@ -763,6 +838,74 @@ impl ElementAnimationSet {
transition.state = AnimationState::Canceled;
}
}

fn start_transition_if_applicable(
&mut self,
context: &SharedStyleContext,
opaque_node: OpaqueNode,
longhand_id: LonghandId,
index: usize,
old_style: &ComputedValues,
new_style: &Arc<ComputedValues>,
) {
let box_style = new_style.get_box();
let timing_function = box_style.transition_timing_function_mod(index);
let duration = box_style.transition_duration_mod(index);
let delay = box_style.transition_delay_mod(index).seconds() as f64;
let now = context.current_time_for_animations;

// Only start a new transition if the style actually changes between
// the old style and the new style.
let property_animation = match PropertyAnimation::from_longhand(
longhand_id,
timing_function,
duration,
old_style,
new_style,
) {
Some(property_animation) => property_animation,
None => return,
};

// Per [1], don't trigger a new transition if the end state for that
// transition is the same as that of a transition that's running or
// completed. We don't take into account any canceled animations.
// [1]: https://drafts.csswg.org/css-transitions/#starting
if self
.transitions
.iter()
.filter(|transition| transition.state != AnimationState::Canceled)
.any(|transition| transition.property_animation.to == property_animation.to)
{
return;
}

// We are going to start a new transition, but we might have to update
// it if we are replacing a reversed transition.
let reversing_adjusted_start_value = property_animation.from.clone();
let mut new_transition = Transition {
node: opaque_node,
start_time: now + delay,
property_animation,
state: AnimationState::Running,
is_new: true,
reversing_adjusted_start_value,
reversing_shortening_factor: 1.0,
};

if let Some(old_transition) = self
.transitions
.iter_mut()
.filter(|transition| transition.state != AnimationState::Canceled)
.find(|transition| transition.property_animation.property_id() == longhand_id)
{
// We always cancel any running transitions for the same property.
old_transition.state = AnimationState::Canceled;
new_transition.update_for_possibly_reversed_transition(old_transition, delay, now);
}

self.transitions.push(new_transition);
}
}

/// Kick off any new transitions for this node and return all of the properties that are
Expand All @@ -786,46 +929,17 @@ pub fn start_transitions_if_applicable(
let physical_property = transition.longhand_id.to_physical(new_style.writing_mode);
if properties_that_transition.contains(physical_property) {
continue;
} else {
properties_that_transition.insert(physical_property);
}

let property_animation = match PropertyAnimation::from_longhand(
transition.longhand_id,
box_style.transition_timing_function_mod(transition.index),
box_style.transition_duration_mod(transition.index),
properties_that_transition.insert(physical_property);
animation_state.start_transition_if_applicable(
context,
opaque_node,
physical_property,
transition.index,
old_style,
new_style,
) {
Some(property_animation) => property_animation,
None => continue,
};

// Per [1], don't trigger a new transition if the end state for that
// transition is the same as that of a transition that's running or
// completed. We don't take into account any canceled animations.
// [1]: https://drafts.csswg.org/css-transitions/#starting
if animation_state
.transitions
.iter()
.filter(|transition| transition.state != AnimationState::Canceled)
.any(|transition| transition.property_animation.to == property_animation.to)
{
continue;
}

// Kick off the animation.
debug!("Kicking off transition of {:?}", property_animation);
let box_style = new_style.get_box();
let start_time = context.current_time_for_animations +
(box_style.transition_delay_mod(transition.index).seconds() as f64);
animation_state.transitions.push(Transition {
node: opaque_node,
start_time,
property_animation,
state: AnimationState::Running,
is_new: true,
});
);
}

properties_that_transition
Expand Down
7 changes: 7 additions & 0 deletions tests/wpt/mozilla/meta/MANIFEST.json
Expand Up @@ -12870,6 +12870,13 @@
{}
]
],
"faster-reversing-of-transitions.html": [
"8471a18f962283afd8d6a81c8ab868e5c2eedd7d",
[
null,
{}
]
],
"mixed-units.html": [
"bb029a9fa80650c39e3f9524748e2b8893a476e1",
[
Expand Down
@@ -0,0 +1,124 @@
<!doctype html>
<meta charset="utf-8">
<title>Transition test: Support for faster reversing of interrupted transitions</title>
<style>
.target {
width: 10px;
height: 50px;
background: red;
}
</style>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<body></body>

<script>
function createTransitionElement() {
let element = document.createElement("div");
element.className = "target";

element.style.transitionProperty = "width";
element.style.transitionDuration = "10s";
element.style.transitionTimingFunction = "linear";

document.body.appendChild(element);
getComputedStyle(element).width;

return element;
}

test(function() {
let testBinding = new window.TestBinding();
let div = createTransitionElement();

// Start a transition and allow 30% of it to complete.
div.style.width = "110px";
getComputedStyle(div).width;

testBinding.advanceClock(3000);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 40, 1);

// Reverse the transition. It should be complete after a proportional
// amount of time and not the "transition-duration" set in the style.
div.style.width = "10px";
getComputedStyle(div).width;

testBinding.advanceClock(3000);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 10, 1);

document.body.removeChild(div);
}, "Reversed transitions are shortened proportionally");

test(function() {
let testBinding = new window.TestBinding();
let div = createTransitionElement();

// Start a transition and allow 50% of it to complete.
div.style.width = "110px";
getComputedStyle(div).width;

testBinding.advanceClock(5000);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 60, 1);

// Reverse the transition.
div.style.width = "10px";
getComputedStyle(div).width;

testBinding.advanceClock(2500);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 35, 1);

// Reverse the reversed transition.
div.style.width = "110px";
getComputedStyle(div).width;

testBinding.advanceClock(2000);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 55, 1);

testBinding.advanceClock(4500);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 100, 1);

testBinding.advanceClock(1000);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 110, 1);

document.body.removeChild(div);
}, "Reversed already reversed transitions are shortened proportionally");

test(function() {
let testBinding = new window.TestBinding();
let div = createTransitionElement();

// Start a transition and allow most of it to complete.
div.style.width = "110px";
getComputedStyle(div).width;

testBinding.advanceClock(9000);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 100, 1);

// Start a new transition that explicitly isn't a reversal. This should
// take the entire 10 seconds.
div.style.width = "0px";
getComputedStyle(div).width;

testBinding.advanceClock(2000);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 80, 1);

testBinding.advanceClock(6000);
getComputedStyle(div).width;
assert_approx_equals(div.clientWidth, 20, 1);

testBinding.advanceClock(2000);
assert_equals(getComputedStyle(div).getPropertyValue("width"), "0px");

document.body.removeChild(div);
}, "Non-reversed transition changes use the full transition-duration");
</script>

0 comments on commit 4ba15c3

Please sign in to comment.