Commit ae606e48 authored by Jordan Taylor's avatar Jordan Taylor Committed by Commit Bot

Added hold_phase to Animation

Prior to this change, effects are always given the current phase of the
timeline associated with them. This fails in certain situations such as
pausing and then scrolling, setting current time with an inactive
timeline, and many other scenarios where hold_time is set.

In order to fix this, we have introduced hold_phase. Anytime hold_time
is updated, hold_phase will also be updated with the appropriate phase.
Sometimes the phase is set explicitly and other times we pull from the
timeline current time.

Animation effects will be given the animation hold_phase if it is
populated, otherwise it will calculate its current phase (similar to how
current time is calculated).

The WebAnimations spec is being updated to reflect this change.

Bug: 1046833
Change-Id: I4bf1e42eaab684c18829a79acc1ab8911ec893af
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2135336
Commit-Queue: Jordan Taylor <jortaylo@microsoft.com>
Reviewed-by: default avatarKevin Ellis <kevers@chromium.org>
Reviewed-by: default avatarMajid Valipour <majidvp@chromium.org>
Cr-Commit-Position: refs/heads/master@{#797835}
parent 624dde9f
......@@ -291,6 +291,7 @@ class CORE_EXPORT Animation : public EventTargetWithInlineData,
void AddedEventListener(const AtomicString& event_type,
RegisteredEventListener&) override;
base::Optional<double> CurrentTimeInternal() const;
TimelinePhase CurrentPhaseInternal() const;
virtual AnimationEffect::EventDelegate* CreateEventDelegate(
Element* target,
const AnimationEffect::EventDelegate* old_event_delegate) {
......@@ -299,6 +300,11 @@ class CORE_EXPORT Animation : public EventTargetWithInlineData,
private:
void SetCurrentTimeInternal(double new_current_time);
void SetHoldTimeAndPhase(
base::Optional<double> new_hold_time /* in seconds */,
TimelinePhase new_hold_phase);
void ResetHoldTimeAndPhase();
bool ValidateHoldTimeAndPhase() const;
void ClearOutdated();
void ForceServiceOnNextFrame();
......@@ -314,6 +320,7 @@ class CORE_EXPORT Animation : public EventTargetWithInlineData,
base::Optional<double> CalculateStartTime(double current_time) const;
base::Optional<double> CalculateCurrentTime() const;
TimelinePhase CalculateCurrentPhase() const;
void BeginUpdatingState();
void EndUpdatingState();
......@@ -377,6 +384,7 @@ class CORE_EXPORT Animation : public EventTargetWithInlineData,
base::Optional<double> pending_playback_rate_;
base::Optional<double> start_time_;
base::Optional<double> hold_time_;
base::Optional<TimelinePhase> hold_phase_;
base::Optional<double> previous_current_time_;
unsigned sequence_number_;
......
......@@ -151,4 +151,167 @@
);
}
}
function createKeyframeEffectOpacity(test){
return new KeyframeEffect(
createDiv(test),
{
opacity: [0.3, 0.7]
},
{
duration: 1000
}
);
}
function verifyTimelineBeforePhase(animation){
assert_equals(animation.timeline.phase, "before");
assert_equals(animation.timeline.currentTime, 0);
assert_equals(animation.currentTime, 0);
assert_equals(
animation.effect.getComputedTiming().localTime,
0,
"effect local time in timeline before phase");
}
function verifyEffectBeforePhase(animation){
// progress == null AND opacity == 1 implies we are in the effect before
// phase
assert_equals(
animation.effect.getComputedTiming().progress,
null
);
assert_equals(
window.getComputedStyle(animation.effect.target).getPropertyValue("opacity"),
"1"
);
}
promise_test(async t => {
const animation = new Animation(
createKeyframeEffectOpacity(t),
createScrollTimelineWithOffsets(t, "20%", "80%")
);
const scroller = animation.timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
await animation.ready;
verifyTimelineBeforePhase(animation);
verifyEffectBeforePhase(animation);
animation.pause();
await waitForNextFrame();
verifyTimelineBeforePhase(animation);
verifyEffectBeforePhase(animation);
animation.play();
await waitForNextFrame();
verifyTimelineBeforePhase(animation);
verifyEffectBeforePhase(animation);
}, 'Verify that (play -> pause -> play) doesn\'t change phase/progress.');
promise_test(async t => {
const animation = new Animation(
createKeyframeEffectOpacity(t),
createScrollTimelineWithOffsets(t, "20%", "80%")
);
const scroller = animation.timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
await animation.ready;
verifyTimelineBeforePhase(animation);
verifyEffectBeforePhase(animation);
animation.pause();
await waitForNextFrame();
verifyTimelineBeforePhase(animation);
verifyEffectBeforePhase(animation);
// Scrolling should not cause the animation effect to change.
scroller.scrollTop = 0.5 * maxScroll;
await waitForNextFrame();
// Check timeline phase
assert_equals(animation.timeline.phase, "active");
assert_equals(animation.timeline.currentTime, 500);
assert_equals(animation.currentTime, 0);
assert_equals(
animation.effect.getComputedTiming().localTime,
0,
"effect local time"
);
// Make sure the effect is still in the before phase even though the
// timeline is not.
verifyEffectBeforePhase(animation);
}, 'Pause in before phase, scroll timeline into active phase, animation ' +
'should remain in the before phase');
promise_test(async t => {
const animation = new Animation(
createKeyframeEffectOpacity(t),
createScrollTimelineWithOffsets(t, "20%", "80%")
);
const scroller = animation.timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
animation.play();
await animation.ready;
// Causes the timeline to be inactive
scroller.style.overflow = "visible";
await waitForNextFrame();
await waitForNextFrame();
// Check timeline phase
assert_equals(animation.timeline.phase, "inactive");
assert_equals(animation.timeline.currentTime, null);
assert_equals(animation.currentTime, null);
assert_equals(
animation.effect.getComputedTiming().localTime,
null,
"effect local time with inactive timeline"
);
verifyEffectBeforePhase(animation);
// Setting the current time while timeline is inactive should cause hold phase
// and hold time to be populated
animation.currentTime = 500;
await waitForNextFrame();
await waitForNextFrame();
// Check timeline phase
assert_equals(animation.timeline.phase, "inactive");
assert_equals(animation.timeline.currentTime, null);
assert_equals(animation.currentTime, 500);
assert_equals(
animation.effect.getComputedTiming().localTime,
500,
"effect local time after setting animation current time"
);
// Check effect phase
// progress == 0.5 AND opacity == 0.5 shows we are in the effect active phase
assert_equals(
animation.effect.getComputedTiming().progress,
0.5,
"effect progress"
);
assert_equals(
window.getComputedStyle(animation.effect.target).getPropertyValue("opacity"),
"0.5",
"effect opacity after setting animation current time"
);
}, 'Make scroller inactive, then set current time to an in range time');
</script>
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment