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.
This commit is contained in:
Martin Robinson 2020-05-12 17:05:10 +02:00
parent c7fc4dd275
commit 4ba15c33e3
3 changed files with 281 additions and 36 deletions

View file

@ -574,9 +574,84 @@ pub struct Transition {
/// Whether or not this transition is new and or has already been tracked /// Whether or not this transition is new and or has already been tracked
/// by the script thread. /// by the script thread.
pub is_new: bool, 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 { 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
// transitions 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 transitions 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 /// 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 /// not take into account canceling i.e. when an animation or transition is
/// canceled due to changes in the style. /// canceled due to changes in the style.
@ -763,6 +838,74 @@ impl ElementAnimationSet {
transition.state = AnimationState::Canceled; 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 /// Kick off any new transitions for this node and return all of the properties that are
@ -786,46 +929,17 @@ pub fn start_transitions_if_applicable(
let physical_property = transition.longhand_id.to_physical(new_style.writing_mode); let physical_property = transition.longhand_id.to_physical(new_style.writing_mode);
if properties_that_transition.contains(physical_property) { if properties_that_transition.contains(physical_property) {
continue; continue;
} else {
properties_that_transition.insert(physical_property);
} }
let property_animation = match PropertyAnimation::from_longhand( properties_that_transition.insert(physical_property);
transition.longhand_id, animation_state.start_transition_if_applicable(
box_style.transition_timing_function_mod(transition.index), context,
box_style.transition_duration_mod(transition.index), opaque_node,
physical_property,
transition.index,
old_style, old_style,
new_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 properties_that_transition

View file

@ -12870,6 +12870,13 @@
{} {}
] ]
], ],
"faster-reversing-of-transitions.html": [
"8471a18f962283afd8d6a81c8ab868e5c2eedd7d",
[
null,
{}
]
],
"mixed-units.html": [ "mixed-units.html": [
"bb029a9fa80650c39e3f9524748e2b8893a476e1", "bb029a9fa80650c39e3f9524748e2b8893a476e1",
[ [

View file

@ -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>