mirror of
https://github.com/servo/servo.git
synced 2025-07-23 07:13:52 +01:00
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:
parent
c7fc4dd275
commit
4ba15c33e3
3 changed files with 281 additions and 36 deletions
|
@ -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
|
||||||
|
// 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
|
/// 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
|
||||||
|
|
|
@ -12870,6 +12870,13 @@
|
||||||
{}
|
{}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
"faster-reversing-of-transitions.html": [
|
||||||
|
"8471a18f962283afd8d6a81c8ab868e5c2eedd7d",
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
],
|
||||||
"mixed-units.html": [
|
"mixed-units.html": [
|
||||||
"bb029a9fa80650c39e3f9524748e2b8893a476e1",
|
"bb029a9fa80650c39e3f9524748e2b8893a476e1",
|
||||||
[
|
[
|
||||||
|
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue