Commit 34f71c40 authored by Jordan Taylor's avatar Jordan Taylor Committed by Commit Bot

Implemented Phase for ScrollTimelines

Implemented Phase for ScrollTimelines. Also updated Phase to match
current time model for internal and external behavior which will allow
phase to be accessed as an enum in animation.cc where it will be used
in different calculations.

Added unit test as well as a WPT test for ScrollTimeline Phases.

Bug: 1046833
Change-Id: Ib86f68b34ef91f4e70fdefd55bdb2629dc415a72
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2084777Reviewed-by: default avatarMajid Valipour <majidvp@chromium.org>
Commit-Queue: Jordan Taylor <jortaylo@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#749231}
parent b8498c17
...@@ -63,6 +63,19 @@ base::Optional<double> AnimationTimeline::CurrentTimeSeconds() { ...@@ -63,6 +63,19 @@ base::Optional<double> AnimationTimeline::CurrentTimeSeconds() {
return result ? base::make_optional(result->InSecondsF()) : base::nullopt; return result ? base::make_optional(result->InSecondsF()) : base::nullopt;
} }
String AnimationTimeline::phase() const {
switch (Phase()) {
case TimelinePhase::kInactive:
return "inactive";
case TimelinePhase::kBefore:
return "before";
case TimelinePhase::kActive:
return "active";
case TimelinePhase::kAfter:
return "after";
}
}
void AnimationTimeline::ClearOutdatedAnimation(Animation* animation) { void AnimationTimeline::ClearOutdatedAnimation(Animation* animation) {
DCHECK(!animation->Outdated()); DCHECK(!animation->Outdated());
outdated_animation_count_--; outdated_animation_count_--;
......
...@@ -14,6 +14,8 @@ namespace blink { ...@@ -14,6 +14,8 @@ namespace blink {
class Document; class Document;
enum class TimelinePhase { kInactive, kBefore, kActive, kAfter };
class CORE_EXPORT AnimationTimeline : public ScriptWrappable { class CORE_EXPORT AnimationTimeline : public ScriptWrappable {
DEFINE_WRAPPERTYPEINFO(); DEFINE_WRAPPERTYPEINFO();
...@@ -26,10 +28,12 @@ class CORE_EXPORT AnimationTimeline : public ScriptWrappable { ...@@ -26,10 +28,12 @@ class CORE_EXPORT AnimationTimeline : public ScriptWrappable {
base::Optional<double> CurrentTime(); base::Optional<double> CurrentTime();
base::Optional<double> CurrentTimeSeconds(); base::Optional<double> CurrentTimeSeconds();
String phase() const;
virtual TimelinePhase Phase() const = 0;
virtual bool IsDocumentTimeline() const { return false; } virtual bool IsDocumentTimeline() const { return false; }
virtual bool IsScrollTimeline() const { return false; } virtual bool IsScrollTimeline() const { return false; }
virtual bool IsActive() const = 0; virtual bool IsActive() const = 0;
virtual String phase() const = 0;
// Returns the initial start time for animations that are linked to this // Returns the initial start time for animations that are linked to this
// timeline. This method gets invoked when initializing the start time of an // timeline. This method gets invoked when initializing the start time of an
// animation on this timeline for the first time. It exists because the // animation on this timeline for the first time. It exists because the
......
...@@ -23,7 +23,7 @@ class MockAnimationTimeline : public AnimationTimeline { ...@@ -23,7 +23,7 @@ class MockAnimationTimeline : public AnimationTimeline {
MockAnimationTimeline(Document* document) : AnimationTimeline(document) {} MockAnimationTimeline(Document* document) : AnimationTimeline(document) {}
MOCK_CONST_METHOD0(IsActive, bool()); MOCK_CONST_METHOD0(IsActive, bool());
MOCK_CONST_METHOD0(phase, String()); MOCK_CONST_METHOD0(Phase, TimelinePhase());
MOCK_METHOD0(InitialStartTimeForAnimations, MOCK_METHOD0(InitialStartTimeForAnimations,
base::Optional<base::TimeDelta>()); base::Optional<base::TimeDelta>());
MOCK_METHOD0(NeedsAnimationTimingUpdate, bool()); MOCK_METHOD0(NeedsAnimationTimingUpdate, bool());
......
...@@ -187,11 +187,11 @@ base::Optional<base::TimeDelta> DocumentTimeline::CurrentTimeInternal() { ...@@ -187,11 +187,11 @@ base::Optional<base::TimeDelta> DocumentTimeline::CurrentTimeInternal() {
return result; return result;
} }
String DocumentTimeline::phase() const { TimelinePhase DocumentTimeline::Phase() const {
if (IsActive()) { if (IsActive()) {
return "active"; return TimelinePhase::kActive;
} }
return "inactive"; return TimelinePhase::kInactive;
} }
void DocumentTimeline::PauseAnimationsForTesting(double pause_time) { void DocumentTimeline::PauseAnimationsForTesting(double pause_time) {
......
...@@ -75,7 +75,7 @@ class CORE_EXPORT DocumentTimeline : public AnimationTimeline { ...@@ -75,7 +75,7 @@ class CORE_EXPORT DocumentTimeline : public AnimationTimeline {
Animation* Play(AnimationEffect*); Animation* Play(AnimationEffect*);
bool IsActive() const override; bool IsActive() const override;
String phase() const override; TimelinePhase Phase() const override;
base::Optional<base::TimeDelta> InitialStartTimeForAnimations() override; base::Optional<base::TimeDelta> InitialStartTimeForAnimations() override;
bool HasPendingUpdates() const { bool HasPendingUpdates() const {
return !animations_needing_update_.IsEmpty(); return !animations_needing_update_.IsEmpty();
......
...@@ -145,31 +145,21 @@ bool ScrollTimeline::IsActive() const { ...@@ -145,31 +145,21 @@ bool ScrollTimeline::IsActive() const {
return layout_box && layout_box->HasOverflowClip(); return layout_box && layout_box->HasOverflowClip();
} }
String ScrollTimeline::phase() const { TimelinePhase ScrollTimeline::Phase() const {
// TODO(crbug.com/1046833) - Not yet Implemented return ComputePhaseAndCurrentTime().phase;
return "inactive";
} }
// Scroll-linked animations are initialized with the start time of zero. base::Optional<base::TimeDelta> ScrollTimeline::CurrentTimeInternal() {
base::Optional<base::TimeDelta> return ComputePhaseAndCurrentTime().current_time;
ScrollTimeline::InitialStartTimeForAnimations() {
return base::TimeDelta();
}
void ScrollTimeline::ScheduleNextService() {
DCHECK_EQ(outdated_animation_count_, 0U);
if (AnimationsNeedingUpdateCount() == 0)
return;
if (CurrentTimeInternal() != last_current_time_internal_)
ScheduleServiceOnNextFrame();
} }
base::Optional<base::TimeDelta> ScrollTimeline::CurrentTimeInternal() { ScrollTimeline::PhaseAndTime ScrollTimeline::ComputePhaseAndCurrentTime()
const {
// 1. If scroll timeline is inactive, return an unresolved time value. // 1. If scroll timeline is inactive, return an unresolved time value.
// https://github.com/WICG/scroll-animations/issues/31 // https://github.com/WICG/scroll-animations/issues/31
// https://wicg.github.io/scroll-animations/#current-time-algorithm // https://wicg.github.io/scroll-animations/#current-time-algorithm
if (!IsActive()) { if (!IsActive()) {
return base::nullopt; return {TimelinePhase::kInactive, base::nullopt};
} }
LayoutBox* layout_box = resolved_scroll_source_->GetLayoutBox(); LayoutBox* layout_box = resolved_scroll_source_->GetLayoutBox();
// 2. Otherwise, let current scroll offset be the current scroll offset of // 2. Otherwise, let current scroll offset be the current scroll offset of
...@@ -188,10 +178,10 @@ base::Optional<base::TimeDelta> ScrollTimeline::CurrentTimeInternal() { ...@@ -188,10 +178,10 @@ base::Optional<base::TimeDelta> ScrollTimeline::CurrentTimeInternal() {
if (current_offset < resolved_start_scroll_offset) { if (current_offset < resolved_start_scroll_offset) {
// Return an unresolved time value if fill is none or forwards. // Return an unresolved time value if fill is none or forwards.
if (fill_ == Timing::FillMode::NONE || fill_ == Timing::FillMode::FORWARDS) if (fill_ == Timing::FillMode::NONE || fill_ == Timing::FillMode::FORWARDS)
return base::nullopt; return {TimelinePhase::kBefore, base::nullopt};
// Otherwise, return 0. // Otherwise, return 0.
return base::TimeDelta(); return {TimelinePhase::kBefore, base::TimeDelta()};
} }
// 4. If current scroll offset is greater than or equal to endScrollOffset: // 4. If current scroll offset is greater than or equal to endScrollOffset:
...@@ -202,26 +192,37 @@ base::Optional<base::TimeDelta> ScrollTimeline::CurrentTimeInternal() { ...@@ -202,26 +192,37 @@ base::Optional<base::TimeDelta> ScrollTimeline::CurrentTimeInternal() {
if (resolved_end_scroll_offset < max_offset && if (resolved_end_scroll_offset < max_offset &&
(fill_ == Timing::FillMode::NONE || (fill_ == Timing::FillMode::NONE ||
fill_ == Timing::FillMode::BACKWARDS)) { fill_ == Timing::FillMode::BACKWARDS)) {
return base::nullopt; return {TimelinePhase::kAfter, base::nullopt};
} }
// Otherwise, return the effective time range. // Otherwise, return the effective time range.
return base::TimeDelta::FromMillisecondsD(time_range_); return {TimelinePhase::kAfter,
} base::TimeDelta::FromMillisecondsD(time_range_)};
// This is not by the spec, but avoids a negative current time.
// See https://github.com/WICG/scroll-animations/issues/20
if (resolved_start_scroll_offset >= resolved_end_scroll_offset) {
return base::nullopt;
} }
// 5. Return the result of evaluating the following expression: // 5. Return the result of evaluating the following expression:
// ((current scroll offset - startScrollOffset) / // ((current scroll offset - startScrollOffset) /
// (endScrollOffset - startScrollOffset)) * effective time range // (endScrollOffset - startScrollOffset)) * effective time range
return base::TimeDelta::FromMillisecondsD( base::Optional<base::TimeDelta> calculated_current_time =
((current_offset - resolved_start_scroll_offset) / base::TimeDelta::FromMillisecondsD(
(resolved_end_scroll_offset - resolved_start_scroll_offset)) * ((current_offset - resolved_start_scroll_offset) /
time_range_); (resolved_end_scroll_offset - resolved_start_scroll_offset)) *
time_range_);
return {TimelinePhase::kActive, calculated_current_time};
}
// Scroll-linked animations are initialized with the start time of zero.
base::Optional<base::TimeDelta>
ScrollTimeline::InitialStartTimeForAnimations() {
return base::TimeDelta();
}
void ScrollTimeline::ScheduleNextService() {
DCHECK_EQ(outdated_animation_count_, 0U);
if (AnimationsNeedingUpdateCount() == 0)
return;
if (CurrentTimeInternal() != last_current_time_internal_)
ScheduleServiceOnNextFrame();
} }
Element* ScrollTimeline::scrollSource() { Element* ScrollTimeline::scrollSource() {
......
...@@ -54,7 +54,7 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -54,7 +54,7 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline {
// have a CSS layout box, or if its layout box is not a scroll container. // have a CSS layout box, or if its layout box is not a scroll container.
// https://github.com/WICG/scroll-animations/issues/31 // https://github.com/WICG/scroll-animations/issues/31
bool IsActive() const override; bool IsActive() const override;
String phase() const override; TimelinePhase Phase() const override;
base::Optional<base::TimeDelta> InitialStartTimeForAnimations() override; base::Optional<base::TimeDelta> InitialStartTimeForAnimations() override;
void ScheduleNextService() override; void ScheduleNextService() override;
...@@ -102,6 +102,13 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -102,6 +102,13 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline {
Member<Element> scroll_source_; Member<Element> scroll_source_;
Member<Node> resolved_scroll_source_; Member<Node> resolved_scroll_source_;
struct PhaseAndTime {
TimelinePhase phase;
base::Optional<base::TimeDelta> current_time;
};
PhaseAndTime ComputePhaseAndCurrentTime() const;
ScrollDirection orientation_; ScrollDirection orientation_;
Member<CSSPrimitiveValue> start_scroll_offset_; Member<CSSPrimitiveValue> start_scroll_offset_;
Member<CSSPrimitiveValue> end_scroll_offset_; Member<CSSPrimitiveValue> end_scroll_offset_;
......
...@@ -157,11 +157,74 @@ TEST_F(ScrollTimelineTest, ...@@ -157,11 +157,74 @@ TEST_F(ScrollTimelineTest,
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION); ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
bool current_time_is_null = false; bool current_time_is_null = false;
scrollable_area->SetScrollOffset(ScrollOffset(0, 50), scrollable_area->SetScrollOffset(ScrollOffset(0, 20),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_EQ(scroll_timeline->phase(), "before");
scroll_timeline->currentTime(current_time_is_null);
EXPECT_TRUE(current_time_is_null);
EXPECT_TRUE(scroll_timeline->IsActive());
current_time_is_null = false;
scrollable_area->SetScrollOffset(ScrollOffset(0, 60),
mojom::blink::ScrollType::kProgrammatic); mojom::blink::ScrollType::kProgrammatic);
EXPECT_EQ(scroll_timeline->phase(), "before");
scroll_timeline->currentTime(current_time_is_null); scroll_timeline->currentTime(current_time_is_null);
EXPECT_TRUE(current_time_is_null); EXPECT_TRUE(current_time_is_null);
EXPECT_TRUE(scroll_timeline->IsActive()); EXPECT_TRUE(scroll_timeline->IsActive());
current_time_is_null = false;
scrollable_area->SetScrollOffset(ScrollOffset(0, 100),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_EQ(scroll_timeline->phase(), "after");
scroll_timeline->currentTime(current_time_is_null);
EXPECT_TRUE(current_time_is_null);
EXPECT_TRUE(scroll_timeline->IsActive());
}
TEST_F(ScrollTimelineTest, PhasesAreCorrectWhenUsingOffsets) {
SetBodyInnerHTML(R"HTML(
<style>
#scroller { overflow: scroll; width: 100px; height: 100px; }
#spacer { height: 1000px; }
</style>
<div id='scroller'>
<div id ='spacer'></div>
</div>
)HTML");
LayoutBoxModelObject* scroller =
ToLayoutBoxModelObject(GetLayoutObjectByElementId("scroller"));
ASSERT_TRUE(scroller);
ASSERT_TRUE(scroller->HasOverflowClip());
PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea();
ASSERT_TRUE(scrollable_area);
ScrollTimelineOptions* options = ScrollTimelineOptions::Create();
DoubleOrScrollTimelineAutoKeyword time_range =
DoubleOrScrollTimelineAutoKeyword::FromDouble(100);
options->setTimeRange(time_range);
options->setScrollSource(GetElementById("scroller"));
options->setStartScrollOffset("10px");
options->setEndScrollOffset("90px");
ScrollTimeline* scroll_timeline =
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
EXPECT_EQ(scroll_timeline->phase(), "before");
scrollable_area->SetScrollOffset(ScrollOffset(0, 10),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_EQ(scroll_timeline->phase(), "active");
scrollable_area->SetScrollOffset(ScrollOffset(0, 50),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_EQ(scroll_timeline->phase(), "active");
scrollable_area->SetScrollOffset(ScrollOffset(0, 90),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_EQ(scroll_timeline->phase(), "after");
scrollable_area->SetScrollOffset(ScrollOffset(0, 100),
mojom::blink::ScrollType::kProgrammatic);
EXPECT_EQ(scroll_timeline->phase(), "after");
} }
TEST_F(ScrollTimelineTest, TEST_F(ScrollTimelineTest,
......
...@@ -20,15 +20,167 @@ ...@@ -20,15 +20,167 @@
<script> <script>
'use strict'; 'use strict';
promise_test(async t => {
const timeline = createScrollTimeline(t);
assert_equals(timeline.phase, "inactive");
}, 'Scroll timeline starts in "inactive" phase.');
promise_test(async t => { promise_test(async t => {
const timeline = createScrollTimeline(t); const timeline = createScrollTimeline(t);
assert_throws_js(TypeError, () => { assert_throws_js(TypeError, () => {
timeline.phase = "after"; timeline.phase = "after";
}); });
}, 'Setting scroll timeline phase (which is readonly) throws TypeError.'); }, 'Setting scroll timeline phase (which is readonly) throws TypeError.');
const test_cases = {
before_start: {
name: "before start",
scroll_percent: 0.1,
timeline_phase: "before"
},
at_start: {
name: "at start",
scroll_percent: 0.2,
timeline_phase: "active"
},
in_range: {
name: "in range",
scroll_percent: 0.5,
timeline_phase: "active"
},
at_end: {
name: "at end",
scroll_percent: 0.8,
timeline_phase: "after"
},
after_end: {
name: "after end",
scroll_percent: 0.9,
timeline_phase: "after"
}
}
for (const test_case_key in test_cases){
const test_case = test_cases[test_case_key];
test(t => {
const timeline = createScrollTimelineWithOffsets(t, "20%", "80%");
const scroller = timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
scroller.scrollTop = test_case.scroll_percent * maxScroll;
assert_equals(
timeline.phase,
test_case.timeline_phase,
"timeline.phase"
);
}, "Timeline phase while scroll offset is " + test_case.name);
}
// TODO(crbug.com/1060384): Spec is unclear in this case, revisit this when
// desired results have been established.
// These test cases are worded strangely because they test an edge case
// where startScrollOffset is GREATER THAN endScrollOffset
const test_cases_start_offset_greater_than_end_offset = {
before_end: {
name: "before end",
scroll_percent: 0.1,
timeline_phase: "before"
},
at_end: {
name: "at end",
scroll_percent: 0.2,
timeline_phase: "before"
},
before_start: {
name: "before start",
scroll_percent: 0.5,
timeline_phase: "before"
},
at_start: {
name: "at start",
scroll_percent: 0.8,
timeline_phase: "after"
},
after_start: {
name: "after start",
scroll_percent: 0.9,
timeline_phase: "after"
}
}
for (const test_case_key in test_cases_start_offset_greater_than_end_offset){
const test_case =
test_cases_start_offset_greater_than_end_offset[test_case_key];
test(t => {
const timeline = createScrollTimelineWithOffsets(t, "80%", "20%");
const scroller = timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
scroller.scrollTop = test_case.scroll_percent * maxScroll;
assert_equals(
timeline.phase,
test_case.timeline_phase,
"timeline.phase"
);
}, "Timeline phase while start offset is greater than end offset and" +
" scroll offset is " + test_case.name);
}
// TODO(crbug.com/1060384): Spec is unclear in this case, revisit this when
// desired results have been established.
// Test cases where startScrollOffset is EQUAL TO endScrollOffset
const test_cases_start_offset_equal_to_end_offset = {
before_end: {
name: "before start",
scroll_percent: 0.3,
timeline_phase: "before"
},
at_end: {
name: "at both",
scroll_percent: 0.5,
timeline_phase: "after"
},
before_start: {
name: "after end",
scroll_percent: 0.7,
timeline_phase: "after"
}
}
for (const test_case_key in test_cases_start_offset_equal_to_end_offset){
const test_case =
test_cases_start_offset_equal_to_end_offset[test_case_key];
test(t => {
const timeline = createScrollTimelineWithOffsets(t, "50%", "50%");
const scroller = timeline.scrollSource;
const maxScroll = scroller.scrollHeight - scroller.clientHeight;
scroller.scrollTop = test_case.scroll_percent * maxScroll;
assert_equals(
timeline.phase,
test_case.timeline_phase,
"timeline.phase"
);
}, "Timeline phase while start offset is equal to end offset and scroll" +
" offset is " + test_case.name);
}
test(t => {
const timeline = createScrollTimeline(t);
const scroller = timeline.scrollSource;
// Timeline should be inactive since layout hasn't updated yet
assert_equals(timeline.phase, "inactive");
// Accessing scroller.scrollHeight forces the scroller to update
scroller.scrollHeight;
assert_equals(timeline.phase, "active");
// Setting the scroller to display none should make the timeline inactive
scroller.style.display = "none"
// Force another layout
scroller.scrollHeight;
assert_equals(timeline.phase, "inactive");
}, 'Scroll timeline starts inactive, can transition to active, and then' +
' back to inactive.');
</script> </script>
\ No newline at end of file
...@@ -12,6 +12,16 @@ function createScrollTimeline(test) { ...@@ -12,6 +12,16 @@ function createScrollTimeline(test) {
}); });
} }
function createScrollTimelineWithOffsets(test, startOffset, endOffset) {
return new ScrollTimeline({
scrollSource: createScroller(test),
orientation: "vertical",
startScrollOffset: startOffset,
endScrollOffset: endOffset,
timeRange: 1000
});
}
function createScrollLinkedAnimation(test, timeline) { function createScrollLinkedAnimation(test, timeline) {
if(timeline === undefined) if(timeline === undefined)
timeline = createScrollTimeline(test); timeline = createScrollTimeline(test);
......
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