Commit 0fb71147 authored by Olga Gerchikov's avatar Olga Gerchikov Committed by Commit Bot

[ScrollTimeline] Support for multiple scroll offsets, no clamping

Implemented calculating current time based on multiple scroll offsets
for both blink and cc ScrollTimeline.


Bug: 1094014
Change-Id: I1c724a1a5191bfa36b91926c1c30a1385cfee10c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2387079Reviewed-by: default avatarRobert Flack <flackr@chromium.org>
Reviewed-by: default avatarKevin Ellis <kevers@chromium.org>
Commit-Queue: Olga Gerchikov <gerchiko@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#806237}
parent 3bddaed3
...@@ -332,9 +332,11 @@ TEST_F(AnimationHostTest, LayerTreeMutatorUpdateReflectsScrollAnimations) { ...@@ -332,9 +332,11 @@ TEST_F(AnimationHostTest, LayerTreeMutatorUpdateReflectsScrollAnimations) {
// Create scroll timeline that links scroll animation and worklet animation // Create scroll timeline that links scroll animation and worklet animation
// together. Use timerange so that we have 1:1 time & scroll mapping. // together. Use timerange so that we have 1:1 time & scroll mapping.
auto scroll_timeline = std::vector<double> scroll_offsets;
ScrollTimeline::Create(element_id, ScrollTimeline::ScrollDown, scroll_offsets.push_back(0);
base::nullopt, base::nullopt, 100); scroll_offsets.push_back(100);
auto scroll_timeline = ScrollTimeline::Create(
element_id, ScrollTimeline::ScrollDown, scroll_offsets, 100);
// Create a worklet animation that is bound to the scroll timeline. // Create a worklet animation that is bound to the scroll timeline.
scoped_refptr<WorkletAnimation> worklet_animation( scoped_refptr<WorkletAnimation> worklet_animation(
...@@ -373,8 +375,11 @@ TEST_F(AnimationHostTest, TickScrollLinkedAnimation) { ...@@ -373,8 +375,11 @@ TEST_F(AnimationHostTest, TickScrollLinkedAnimation) {
// Create scroll timeline that links scroll animation and scroll-linked // Create scroll timeline that links scroll animation and scroll-linked
// animation together. // animation together.
std::vector<double> scroll_offsets;
scroll_offsets.push_back(0);
scroll_offsets.push_back(100);
auto scroll_timeline = ScrollTimeline::Create( auto scroll_timeline = ScrollTimeline::Create(
element_id_, ScrollTimeline::ScrollDown, 0, 100, 1000); element_id_, ScrollTimeline::ScrollDown, scroll_offsets, 1000);
int animation_id = 11; int animation_id = 11;
// Create an animation that is bound to the scroll timeline. // Create an animation that is bound to the scroll timeline.
...@@ -431,8 +436,12 @@ TEST_F(AnimationHostTest, ScrollTimelineOffsetUpdatedByScrollAnimation) { ...@@ -431,8 +436,12 @@ TEST_F(AnimationHostTest, ScrollTimelineOffsetUpdatedByScrollAnimation) {
timeline_->AttachAnimation(mock_scroll_animation); timeline_->AttachAnimation(mock_scroll_animation);
host_impl_->AddToTicking(mock_scroll_animation); host_impl_->AddToTicking(mock_scroll_animation);
std::vector<double> scroll_offsets;
scroll_offsets.push_back(0);
scroll_offsets.push_back(100);
auto scroll_timeline = ScrollTimeline::Create( auto scroll_timeline = ScrollTimeline::Create(
element_id_, ScrollTimeline::ScrollDown, 0, 100, 1000); element_id_, ScrollTimeline::ScrollDown, scroll_offsets, 1000);
host_impl_->TickAnimations(base::TimeTicks(), property_trees.scroll_tree, host_impl_->TickAnimations(base::TimeTicks(), property_trees.scroll_tree,
false); false);
......
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
#include "cc/animation/scroll_timeline.h" #include "cc/animation/scroll_timeline.h"
#include <memory>
#include <vector>
#include "cc/animation/animation_id_provider.h" #include "cc/animation/animation_id_provider.h"
#include "cc/animation/worklet_animation.h" #include "cc/animation/worklet_animation.h"
#include "cc/trees/property_tree.h" #include "cc/trees/property_tree.h"
...@@ -11,8 +14,6 @@ ...@@ -11,8 +14,6 @@
#include "ui/gfx/geometry/scroll_offset.h" #include "ui/gfx/geometry/scroll_offset.h"
#include "ui/gfx/geometry/size.h" #include "ui/gfx/geometry/size.h"
#include <memory>
namespace cc { namespace cc {
namespace { namespace {
...@@ -25,42 +26,53 @@ bool IsReverse(ScrollTimeline::ScrollDirection direction) { ...@@ -25,42 +26,53 @@ bool IsReverse(ScrollTimeline::ScrollDirection direction) {
return direction == ScrollTimeline::ScrollUp || return direction == ScrollTimeline::ScrollUp ||
direction == ScrollTimeline::ScrollLeft; direction == ScrollTimeline::ScrollLeft;
} }
bool ValidateScrollOffsets(const std::vector<double>& scroll_offsets) {
return scroll_offsets.empty() || scroll_offsets.size() >= 2.0;
}
} // namespace } // namespace
template double ComputeProgress<std::vector<double>>(
double,
const std::vector<double>&);
ScrollTimeline::ScrollTimeline(base::Optional<ElementId> scroller_id, ScrollTimeline::ScrollTimeline(base::Optional<ElementId> scroller_id,
ScrollDirection direction, ScrollDirection direction,
base::Optional<double> start_scroll_offset, const std::vector<double> scroll_offsets,
base::Optional<double> end_scroll_offset,
double time_range, double time_range,
int animation_timeline_id) int animation_timeline_id)
: AnimationTimeline(animation_timeline_id), : AnimationTimeline(animation_timeline_id),
pending_id_(scroller_id), pending_id_(scroller_id),
direction_(direction), direction_(direction),
start_scroll_offset_(start_scroll_offset), scroll_offsets_(scroll_offsets),
end_scroll_offset_(end_scroll_offset), time_range_(time_range) {
time_range_(time_range) {} DCHECK(ValidateScrollOffsets(scroll_offsets_));
}
ScrollTimeline::~ScrollTimeline() = default; ScrollTimeline::~ScrollTimeline() = default;
scoped_refptr<ScrollTimeline> ScrollTimeline::Create( scoped_refptr<ScrollTimeline> ScrollTimeline::Create(
base::Optional<ElementId> scroller_id, base::Optional<ElementId> scroller_id,
ScrollTimeline::ScrollDirection direction, ScrollTimeline::ScrollDirection direction,
base::Optional<double> start_scroll_offset, const std::vector<double> scroll_offsets,
base::Optional<double> end_scroll_offset,
double time_range) { double time_range) {
return base::WrapRefCounted(new ScrollTimeline( return base::WrapRefCounted(
scroller_id, direction, start_scroll_offset, end_scroll_offset, new ScrollTimeline(scroller_id, direction, scroll_offsets, time_range,
time_range, AnimationIdProvider::NextTimelineId())); AnimationIdProvider::NextTimelineId()));
} }
scoped_refptr<AnimationTimeline> ScrollTimeline::CreateImplInstance() const { scoped_refptr<AnimationTimeline> ScrollTimeline::CreateImplInstance() const {
return base::WrapRefCounted( return base::WrapRefCounted(new ScrollTimeline(
new ScrollTimeline(pending_id_, direction_, start_scroll_offset_, pending_id_, direction_, scroll_offsets_, time_range_, id()));
end_scroll_offset_, time_range_, id()));
} }
bool ScrollTimeline::IsActive(const ScrollTree& scroll_tree, bool ScrollTimeline::IsActive(const ScrollTree& scroll_tree,
bool is_active_tree) const { bool is_active_tree) const {
// Blink passes empty scroll offsets when the timeline is inactive.
if (scroll_offsets_.empty()) {
return false;
}
// If pending tree with our scroller hasn't been activated, or the scroller // If pending tree with our scroller hasn't been activated, or the scroller
// has been removed (e.g. if it is no longer composited). // has been removed (e.g. if it is no longer composited).
if ((is_active_tree && !active_id_) || (!is_active_tree && !pending_id_)) if ((is_active_tree && !active_id_) || (!is_active_tree && !pending_id_))
...@@ -105,8 +117,10 @@ base::Optional<base::TimeTicks> ScrollTimeline::CurrentTime( ...@@ -105,8 +117,10 @@ base::Optional<base::TimeTicks> ScrollTimeline::CurrentTime(
DCHECK_GE(max_offset, 0); DCHECK_GE(max_offset, 0);
DCHECK_GE(current_offset, 0); DCHECK_GE(current_offset, 0);
double resolved_start_scroll_offset = start_scroll_offset_.value_or(0); DCHECK_GE(scroll_offsets_.size(), 2u);
double resolved_end_scroll_offset = end_scroll_offset_.value_or(max_offset); double resolved_start_scroll_offset = scroll_offsets_[0];
double resolved_end_scroll_offset =
scroll_offsets_[scroll_offsets_.size() - 1];
// TODO(crbug.com/1060384): Once the spec has been updated to state what the // TODO(crbug.com/1060384): Once the spec has been updated to state what the
// expected result is when startScrollOffset >= endScrollOffset, we might need // expected result is when startScrollOffset >= endScrollOffset, we might need
...@@ -126,11 +140,10 @@ base::Optional<base::TimeTicks> ScrollTimeline::CurrentTime( ...@@ -126,11 +140,10 @@ base::Optional<base::TimeTicks> ScrollTimeline::CurrentTime(
// 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::TimeTicks() + return base::TimeTicks() + base::TimeDelta::FromMillisecondsD(
base::TimeDelta::FromMillisecondsD( ComputeProgress<std::vector<double>>(
((current_offset - resolved_start_scroll_offset) / current_offset, scroll_offsets_) *
(resolved_end_scroll_offset - resolved_start_scroll_offset)) * time_range_);
time_range_);
} }
void ScrollTimeline::PushPropertiesTo(AnimationTimeline* impl_timeline) { void ScrollTimeline::PushPropertiesTo(AnimationTimeline* impl_timeline) {
...@@ -142,8 +155,8 @@ void ScrollTimeline::PushPropertiesTo(AnimationTimeline* impl_timeline) { ...@@ -142,8 +155,8 @@ void ScrollTimeline::PushPropertiesTo(AnimationTimeline* impl_timeline) {
// because we end up using the pending start/end scroll offset for the active // because we end up using the pending start/end scroll offset for the active
// tree too. Instead we need to either split these (like pending_id_ and // tree too. Instead we need to either split these (like pending_id_ and
// active_id_) or have a ScrollTimeline per tree. // active_id_) or have a ScrollTimeline per tree.
scroll_timeline->start_scroll_offset_ = start_scroll_offset_; scroll_timeline->scroll_offsets_ = scroll_offsets_;
scroll_timeline->end_scroll_offset_ = end_scroll_offset_; DCHECK(ValidateScrollOffsets(scroll_timeline->scroll_offsets_));
} }
void ScrollTimeline::ActivateTimeline() { void ScrollTimeline::ActivateTimeline() {
...@@ -189,11 +202,8 @@ bool ScrollTimeline::TickScrollLinkedAnimations( ...@@ -189,11 +202,8 @@ bool ScrollTimeline::TickScrollLinkedAnimations(
void ScrollTimeline::UpdateScrollerIdAndScrollOffsets( void ScrollTimeline::UpdateScrollerIdAndScrollOffsets(
base::Optional<ElementId> pending_id, base::Optional<ElementId> pending_id,
base::Optional<double> start_scroll_offset, const std::vector<double> scroll_offsets) {
base::Optional<double> end_scroll_offset) { if (pending_id_ == pending_id && scroll_offsets_ == scroll_offsets) {
if (pending_id_ == pending_id &&
start_scroll_offset_ == start_scroll_offset &&
end_scroll_offset_ == end_scroll_offset) {
return; return;
} }
...@@ -201,8 +211,8 @@ void ScrollTimeline::UpdateScrollerIdAndScrollOffsets( ...@@ -201,8 +211,8 @@ void ScrollTimeline::UpdateScrollerIdAndScrollOffsets(
// Then later (when the pending tree is promoted to active) // Then later (when the pending tree is promoted to active)
// |ActivateTimeline| will be called and will set the |active_id_|. // |ActivateTimeline| will be called and will set the |active_id_|.
pending_id_ = pending_id; pending_id_ = pending_id;
start_scroll_offset_ = start_scroll_offset; scroll_offsets_ = scroll_offsets;
end_scroll_offset_ = end_scroll_offset; DCHECK(ValidateScrollOffsets(scroll_offsets_));
SetNeedsPushProperties(); SetNeedsPushProperties();
} }
......
...@@ -36,16 +36,14 @@ class CC_ANIMATION_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -36,16 +36,14 @@ class CC_ANIMATION_EXPORT ScrollTimeline : public AnimationTimeline {
ScrollTimeline(base::Optional<ElementId> scroller_id, ScrollTimeline(base::Optional<ElementId> scroller_id,
ScrollDirection direction, ScrollDirection direction,
base::Optional<double> start_scroll_offset, const std::vector<double> scroll_offsets,
base::Optional<double> end_scroll_offset,
double time_range, double time_range,
int animation_timeline_id); int animation_timeline_id);
static scoped_refptr<ScrollTimeline> Create( static scoped_refptr<ScrollTimeline> Create(
base::Optional<ElementId> scroller_id, base::Optional<ElementId> scroller_id,
ScrollDirection direction, ScrollDirection direction,
base::Optional<double> start_scroll_offset, const std::vector<double> scroll_offsets,
base::Optional<double> end_scroll_offset,
double time_range); double time_range);
// Create a copy of this ScrollTimeline intended for the impl thread in the // Create a copy of this ScrollTimeline intended for the impl thread in the
...@@ -67,8 +65,7 @@ class CC_ANIMATION_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -67,8 +65,7 @@ class CC_ANIMATION_EXPORT ScrollTimeline : public AnimationTimeline {
void UpdateScrollerIdAndScrollOffsets( void UpdateScrollerIdAndScrollOffsets(
base::Optional<ElementId> scroller_id, base::Optional<ElementId> scroller_id,
base::Optional<double> start_scroll_offset, const std::vector<double> scroll_offsets);
base::Optional<double> end_scroll_offset);
void PushPropertiesTo(AnimationTimeline* impl_timeline) override; void PushPropertiesTo(AnimationTimeline* impl_timeline) override;
void ActivateTimeline() override; void ActivateTimeline() override;
...@@ -82,10 +79,14 @@ class CC_ANIMATION_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -82,10 +79,14 @@ class CC_ANIMATION_EXPORT ScrollTimeline : public AnimationTimeline {
base::Optional<ElementId> GetPendingIdForTest() const { return pending_id_; } base::Optional<ElementId> GetPendingIdForTest() const { return pending_id_; }
ScrollDirection GetDirectionForTest() const { return direction_; } ScrollDirection GetDirectionForTest() const { return direction_; }
base::Optional<double> GetStartScrollOffsetForTest() const { base::Optional<double> GetStartScrollOffsetForTest() const {
return start_scroll_offset_; if (scroll_offsets_.empty())
return base::nullopt;
return scroll_offsets_[0];
} }
base::Optional<double> GetEndScrollOffsetForTest() const { base::Optional<double> GetEndScrollOffsetForTest() const {
return end_scroll_offset_; if (scroll_offsets_.empty())
return base::nullopt;
return scroll_offsets_[1];
} }
double GetTimeRangeForTest() const { return time_range_; } double GetTimeRangeForTest() const { return time_range_; }
...@@ -105,11 +106,9 @@ class CC_ANIMATION_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -105,11 +106,9 @@ class CC_ANIMATION_EXPORT ScrollTimeline : public AnimationTimeline {
// it should base its current time on, and where the origin point is. // it should base its current time on, and where the origin point is.
ScrollDirection direction_; ScrollDirection direction_;
// These define the total range of the scroller that the ScrollTimeline is // This defines scroll ranges of the scroller that the ScrollTimeline is
// active within. If not set they default to the beginning/end of the scroller // active within. If no ranges are defined the timeline is inactive.
// respectively, respecting the current |direction_|. std::vector<double> scroll_offsets_;
base::Optional<double> start_scroll_offset_;
base::Optional<double> end_scroll_offset_;
// A ScrollTimeline maps from the scroll offset in the scroller to a time // A ScrollTimeline maps from the scroll offset in the scroller to a time
// value based on a 'time range'. See the implementation of CurrentTime or the // value based on a 'time range'. See the implementation of CurrentTime or the
...@@ -122,6 +121,25 @@ inline ScrollTimeline* ToScrollTimeline(AnimationTimeline* timeline) { ...@@ -122,6 +121,25 @@ inline ScrollTimeline* ToScrollTimeline(AnimationTimeline* timeline) {
return static_cast<ScrollTimeline*>(timeline); return static_cast<ScrollTimeline*>(timeline);
} }
template <typename T>
double ComputeProgress(double current_offset, const T& resolved_offsets) {
DCHECK_GE(resolved_offsets.size(), 2u);
DCHECK(current_offset < resolved_offsets[resolved_offsets.size() - 1]);
// Look for scroll offset that contains the current offset.
unsigned int offset_id;
for (offset_id = 1; offset_id < resolved_offsets.size() &&
resolved_offsets[offset_id] <= current_offset;
offset_id++) {
}
DCHECK(offset_id < resolved_offsets.size());
// Weight of each offset within time range is distributed equally.
double offset_distance = 1.0 / (resolved_offsets.size() - 1);
// Progress of the current offset within its offset range.
double p = (current_offset - resolved_offsets[offset_id - 1]) /
(resolved_offsets[offset_id] - resolved_offsets[offset_id - 1]);
return (offset_id - 1 + p) * offset_distance;
}
} // namespace cc } // namespace cc
#endif // CC_ANIMATION_SCROLL_TIMELINE_H_ #endif // CC_ANIMATION_SCROLL_TIMELINE_H_
This diff is collapsed.
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
#include "cc/animation/worklet_animation.h" #include "cc/animation/worklet_animation.h"
#include <memory>
#include <utility> #include <utility>
#include <vector>
#include "base/memory/ptr_util.h" #include "base/memory/ptr_util.h"
#include "cc/animation/animation_id_provider.h" #include "cc/animation/animation_id_provider.h"
#include "cc/animation/keyframe_effect.h" #include "cc/animation/keyframe_effect.h"
...@@ -57,8 +59,7 @@ class MockScrollTimeline : public ScrollTimeline { ...@@ -57,8 +59,7 @@ class MockScrollTimeline : public ScrollTimeline {
MockScrollTimeline() MockScrollTimeline()
: ScrollTimeline(ElementId(), : ScrollTimeline(ElementId(),
ScrollTimeline::ScrollDown, ScrollTimeline::ScrollDown,
base::nullopt, std::vector<double>(),
base::nullopt,
0, 0,
AnimationIdProvider::NextTimelineId()) {} AnimationIdProvider::NextTimelineId()) {}
MOCK_CONST_METHOD2(CurrentTime, MOCK_CONST_METHOD2(CurrentTime,
......
...@@ -80,8 +80,15 @@ HeapVector<Member<ScrollTimelineOffset>>* ComputeScrollOffsets( ...@@ -80,8 +80,15 @@ HeapVector<Member<ScrollTimelineOffset>>* ComputeScrollOffsets(
const CSSValue* end) { const CSSValue* end) {
auto* offsets = auto* offsets =
MakeGarbageCollected<HeapVector<Member<ScrollTimelineOffset>>>(); MakeGarbageCollected<HeapVector<Member<ScrollTimelineOffset>>>();
offsets->push_back(ComputeScrollOffset(start));
offsets->push_back(ComputeScrollOffset(end)); // TODO(crbug.com/1094014): scroll_offsets will replace start and end
// offsets once spec decision on multiple scroll offsets is finalized.
// https://github.com/w3c/csswg-drafts/issues/4912
if (!IsAuto(start))
offsets->push_back(ComputeScrollOffset(start));
if (!IsAuto(end) || !IsAuto(start))
offsets->push_back(ComputeScrollOffset(end));
return offsets; return offsets;
} }
......
...@@ -133,12 +133,6 @@ ScrollTimeline* ScrollTimeline::Create(Document& document, ...@@ -133,12 +133,6 @@ ScrollTimeline* ScrollTimeline::Create(Document& document,
"Either scrollOffsets or start/end offsets can be specified."); "Either scrollOffsets or start/end offsets can be specified.");
return nullptr; return nullptr;
} }
// TODO(crbug.com/1094014): We currently support just 2 offsets.
if (options->scrollOffsets().size() > 2) {
exception_state.ThrowTypeError(
"Invalid scrollOffsets: only two offsets are currently supported.");
return nullptr;
}
HeapVector<Member<ScrollTimelineOffset>>* scroll_offsets = HeapVector<Member<ScrollTimelineOffset>>* scroll_offsets =
MakeGarbageCollected<HeapVector<Member<ScrollTimelineOffset>>>(); MakeGarbageCollected<HeapVector<Member<ScrollTimelineOffset>>>();
...@@ -239,8 +233,18 @@ ScrollTimelineOffset* ScrollTimeline::EndScrollOffset() const { ...@@ -239,8 +233,18 @@ ScrollTimelineOffset* ScrollTimeline::EndScrollOffset() const {
: nullptr; : nullptr;
} }
std::tuple<base::Optional<double>, base::Optional<double>> const std::vector<double> ScrollTimeline::GetResolvedScrollOffsets() const {
ScrollTimeline::ResolveScrollOffsets() const { std::vector<double> resolved_offsets;
for (const auto& offset : timeline_state_snapshotted_.scroll_offsets)
resolved_offsets.push_back(offset);
return resolved_offsets;
}
// Resolves scroll offsets and stores them into resolved_offsets argument.
// Returns true if the offsets are resolved.
bool ScrollTimeline::ResolveScrollOffsets(
WTF::Vector<double>& resolved_offsets) const {
DCHECK(resolved_offsets.IsEmpty());
DCHECK(ComputeIsActive()); DCHECK(ComputeIsActive());
LayoutBox* layout_box = resolved_scroll_source_->GetLayoutBox(); LayoutBox* layout_box = resolved_scroll_source_->GetLayoutBox();
DCHECK(layout_box); DCHECK(layout_box);
...@@ -250,15 +254,29 @@ ScrollTimeline::ResolveScrollOffsets() const { ...@@ -250,15 +254,29 @@ ScrollTimeline::ResolveScrollOffsets() const {
GetCurrentAndMaxOffset(layout_box, current_offset, max_offset); GetCurrentAndMaxOffset(layout_box, current_offset, max_offset);
auto orientation = ToPhysicalScrollOrientation(orientation_, *layout_box); auto orientation = ToPhysicalScrollOrientation(orientation_, *layout_box);
auto start_offset = StartScrollOffset() ? StartScrollOffset()->ResolveOffset(
resolved_scroll_source_, if (scroll_offsets_->size() == 0) {
orientation, max_offset, 0) // Start and end offsets resolve to 'auto'.
: 0; resolved_offsets.push_back(0);
auto end_offset = EndScrollOffset() ? EndScrollOffset()->ResolveOffset( resolved_offsets.push_back(max_offset);
resolved_scroll_source_, return true;
orientation, max_offset, max_offset) }
: max_offset; // Single entry offset in scrollOffsets is considered as 'end'.
return {start_offset, end_offset}; if (scroll_offsets_->size() == 1)
resolved_offsets.push_back(0);
for (auto& offset : *scroll_offsets_) {
auto resolved_offset = offset->ResolveOffset(
resolved_scroll_source_, orientation, max_offset, max_offset);
if (!resolved_offset) {
// Empty resolved offset if any of the offsets cannot be resolved.
resolved_offsets.clear();
return false;
}
resolved_offsets.push_back(resolved_offset.value());
}
// TODO(crbug.com/1094014): Implement clamping for overlapping offsets.
DCHECK_GE(resolved_offsets.size(), 2u);
return true;
} }
AnimationTimeline::PhaseAndTime ScrollTimeline::CurrentPhaseAndTime() { AnimationTimeline::PhaseAndTime ScrollTimeline::CurrentPhaseAndTime() {
...@@ -270,9 +288,10 @@ ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() const { ...@@ -270,9 +288,10 @@ ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() 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
WTF::Vector<double> resolved_offsets;
if (!ComputeIsActive()) { if (!ComputeIsActive()) {
return {TimelinePhase::kInactive, /*current_time*/ base::nullopt, return {TimelinePhase::kInactive, /*current_time*/ base::nullopt,
base::nullopt, base::nullopt}; resolved_offsets};
} }
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
...@@ -282,17 +301,16 @@ ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() const { ...@@ -282,17 +301,16 @@ ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() const {
double max_offset; double max_offset;
GetCurrentAndMaxOffset(layout_box, current_offset, max_offset); GetCurrentAndMaxOffset(layout_box, current_offset, max_offset);
base::Optional<double> start; bool resolved = ResolveScrollOffsets(resolved_offsets);
base::Optional<double> end;
std::tie(start, end) = ResolveScrollOffsets();
if (!start || !end) { if (!resolved) {
DCHECK(resolved_offsets.IsEmpty());
return {TimelinePhase::kInactive, /*current_time*/ base::nullopt, return {TimelinePhase::kInactive, /*current_time*/ base::nullopt,
base::nullopt, base::nullopt}; resolved_offsets};
} }
double start_offset = start.value(); double start_offset = resolved_offsets[0];
double end_offset = end.value(); double end_offset = resolved_offsets[resolved_offsets.size() - 1];
// TODO(crbug.com/1060384): Once the spec has been updated to state what the // TODO(crbug.com/1060384): Once the spec has been updated to state what the
// expected result is when startScrollOffset >= endScrollOffset, we might need // expected result is when startScrollOffset >= endScrollOffset, we might need
...@@ -301,8 +319,7 @@ ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() const { ...@@ -301,8 +319,7 @@ ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() const {
// 3. If current scroll offset is less than startScrollOffset: // 3. If current scroll offset is less than startScrollOffset:
if (current_offset < start_offset) { if (current_offset < start_offset) {
return {TimelinePhase::kBefore, base::TimeDelta(), start_offset, return {TimelinePhase::kBefore, base::TimeDelta(), resolved_offsets};
end_offset};
} }
// 4. If current scroll offset is greater than or equal to endScrollOffset: // 4. If current scroll offset is greater than or equal to endScrollOffset:
...@@ -313,18 +330,17 @@ ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() const { ...@@ -313,18 +330,17 @@ ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() const {
TimelinePhase phase = end_offset >= max_offset ? TimelinePhase::kActive TimelinePhase phase = end_offset >= max_offset ? TimelinePhase::kActive
: TimelinePhase::kAfter; : TimelinePhase::kAfter;
return {phase, base::TimeDelta::FromMillisecondsD(time_range_), return {phase, base::TimeDelta::FromMillisecondsD(time_range_),
start_offset, end_offset}; resolved_offsets};
} }
// 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
base::Optional<base::TimeDelta> calculated_current_time = base::Optional<base::TimeDelta> calculated_current_time =
base::TimeDelta::FromMillisecondsD((current_offset - start_offset) / base::TimeDelta::FromMillisecondsD(scroll_timeline_util::ComputeProgress(
(end_offset - start_offset) * current_offset, resolved_offsets) *
time_range_); time_range_);
return {TimelinePhase::kActive, calculated_current_time, start_offset, return {TimelinePhase::kActive, calculated_current_time, resolved_offsets};
end_offset};
} }
// Scroll-linked animations are initialized with the start time of zero. // Scroll-linked animations are initialized with the start time of zero.
...@@ -554,7 +570,7 @@ void ScrollTimeline::UpdateCompositorTimeline() { ...@@ -554,7 +570,7 @@ void ScrollTimeline::UpdateCompositorTimeline() {
compositor_timeline_->UpdateCompositorTimeline( compositor_timeline_->UpdateCompositorTimeline(
scroll_timeline_util::GetCompositorScrollElementId( scroll_timeline_util::GetCompositorScrollElementId(
resolved_scroll_source_), resolved_scroll_source_),
GetResolvedStartScrollOffset(), GetResolvedEndScrollOffset()); GetResolvedScrollOffsets());
} }
} // namespace blink } // namespace blink
...@@ -79,17 +79,9 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -79,17 +79,9 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline {
// removed before the ScrollTimeline was created. // removed before the ScrollTimeline was created.
Node* ResolvedScrollSource() const { return resolved_scroll_source_; } Node* ResolvedScrollSource() const { return resolved_scroll_source_; }
// Return the latest resolved start scroll offset. This will be nullopt when // Return the latest resolved scroll offsets. This will be empty when
// timeline is inactive. // timeline is inactive.
base::Optional<double> GetResolvedStartScrollOffset() const { const std::vector<double> GetResolvedScrollOffsets() const;
return timeline_state_snapshotted_.start_offset;
}
// Return the latest resolved end scroll offset. This will be nullopt when
// timeline is inactive.
base::Optional<double> GetResolvedEndScrollOffset() const {
return timeline_state_snapshotted_.end_offset;
}
ScrollDirection GetOrientation() const { return orientation_; } ScrollDirection GetOrientation() const { return orientation_; }
...@@ -139,21 +131,18 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -139,21 +131,18 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline {
// element-based values it computes the corresponding length value that maps // element-based values it computes the corresponding length value that maps
// to the particular element intersection. See // to the particular element intersection. See
// |ScrollTimelineOffset::ResolveOffset()| for more details. // |ScrollTimelineOffset::ResolveOffset()| for more details.
std::tuple<base::Optional<double>, base::Optional<double>> bool ResolveScrollOffsets(WTF::Vector<double>& resolved_offsets) const;
ResolveScrollOffsets() const;
struct TimelineState { struct TimelineState {
TimelinePhase phase; TimelinePhase phase;
base::Optional<base::TimeDelta> current_time; base::Optional<base::TimeDelta> current_time;
// The resolved version of start and end offset. These values are nullopts // The resolved version of scroll offset. The vector is empty
// when timeline is inactive (e.g., when source does not overflow). // when timeline is inactive (e.g., when source does not overflow).
base::Optional<double> start_offset; WTF::Vector<double> scroll_offsets;
base::Optional<double> end_offset;
bool operator==(const TimelineState& other) const { bool operator==(const TimelineState& other) const {
return phase == other.phase && current_time == other.current_time && return phase == other.phase && current_time == other.current_time &&
start_offset == other.start_offset && scroll_offsets == other.scroll_offsets;
end_offset == other.end_offset;
} }
}; };
......
...@@ -21,6 +21,14 @@ namespace blink { ...@@ -21,6 +21,14 @@ namespace blink {
namespace { namespace {
// Only expect precision up to 1 microsecond with an additional epsilon to
// account for float conversion error (mainly due to timeline time getting
// converted between float and base::TimeDelta).
static constexpr double time_error_ms = 0.001 + 1e-13;
#define EXPECT_TIME_NEAR(expected, value) \
EXPECT_NEAR(expected, value, time_error_ms)
StringOrScrollTimelineElementBasedOffset OffsetFromString(const String& value) { StringOrScrollTimelineElementBasedOffset OffsetFromString(const String& value) {
StringOrScrollTimelineElementBasedOffset result; StringOrScrollTimelineElementBasedOffset result;
result.SetString(value); result.SetString(value);
...@@ -777,4 +785,96 @@ TEST_F(ScrollTimelineTest, ...@@ -777,4 +785,96 @@ TEST_F(ScrollTimelineTest,
EXPECT_FALSE(event_listener->EventReceived()); EXPECT_FALSE(event_listener->EventReceived());
} }
TEST_F(ScrollTimelineTest, MultipleScrollOffsetsCurrentTimeCalculations) {
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->HasNonVisibleOverflow());
PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea();
ASSERT_TRUE(scrollable_area);
double time_range = 100.0;
ScrollTimelineOptions* options = ScrollTimelineOptions::Create();
options->setTimeRange(
DoubleOrScrollTimelineAutoKeyword::FromDouble(time_range));
options->setScrollSource(GetElementById("scroller"));
HeapVector<StringOrScrollTimelineElementBasedOffset> scroll_offsets;
scroll_offsets.push_back(OffsetFromString("10px"));
scroll_offsets.push_back(OffsetFromString("20px"));
scroll_offsets.push_back(OffsetFromString("40px"));
scroll_offsets.push_back(OffsetFromString("90px"));
options->setScrollOffsets(scroll_offsets);
ScrollTimeline* scroll_timeline =
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
EXPECT_EQ(scroll_timeline->currentTime(), 0);
scrollable_area->SetScrollOffset(ScrollOffset(0, 10),
mojom::blink::ScrollType::kProgrammatic);
// Simulate a new animation frame which allows the timeline to compute new
// current phase and time.
SimulateFrame();
EXPECT_EQ(0, scroll_timeline->currentTime().value());
scrollable_area->SetScrollOffset(ScrollOffset(0, 12),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
unsigned int offset = 0;
double w = 1.0 / 3.0; // offset weight
double p = (12.0 - 10.0) / (20.0 - 10.0); // progress within the offset
EXPECT_TIME_NEAR((offset + p) * w * time_range,
scroll_timeline->currentTime().value());
scrollable_area->SetScrollOffset(ScrollOffset(0, 20),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
offset = 1;
p = 0;
EXPECT_TIME_NEAR((offset + p) * w * time_range,
scroll_timeline->currentTime().value());
scrollable_area->SetScrollOffset(ScrollOffset(0, 30),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
p = (30.0 - 20.0) / (40.0 - 20.0);
EXPECT_TIME_NEAR((offset + p) * w * time_range,
scroll_timeline->currentTime().value());
scrollable_area->SetScrollOffset(ScrollOffset(0, 40),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
offset = 2;
p = 0;
EXPECT_TIME_NEAR((offset + p) * w * time_range,
scroll_timeline->currentTime().value());
scrollable_area->SetScrollOffset(ScrollOffset(0, 80),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
p = (80.0 - 40.0) / (90.0 - 40.0);
EXPECT_TIME_NEAR((offset + p) * w * time_range,
scroll_timeline->currentTime().value());
scrollable_area->SetScrollOffset(ScrollOffset(0, 90),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
EXPECT_EQ(100, scroll_timeline->currentTime().value());
scrollable_area->SetScrollOffset(ScrollOffset(0, 100),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
EXPECT_EQ(100, scroll_timeline->currentTime().value());
}
} // namespace blink } // namespace blink
...@@ -36,11 +36,8 @@ scoped_refptr<CompositorScrollTimeline> ToCompositorScrollTimeline( ...@@ -36,11 +36,8 @@ scoped_refptr<CompositorScrollTimeline> ToCompositorScrollTimeline(
CompositorScrollTimeline::ScrollDirection orientation = ConvertOrientation( CompositorScrollTimeline::ScrollDirection orientation = ConvertOrientation(
scroll_timeline->GetOrientation(), box ? box->Style() : nullptr); scroll_timeline->GetOrientation(), box ? box->Style() : nullptr);
auto start_scroll_offset = scroll_timeline->GetResolvedStartScrollOffset();
auto end_scroll_offset = scroll_timeline->GetResolvedEndScrollOffset();
return CompositorScrollTimeline::Create( return CompositorScrollTimeline::Create(
element_id, orientation, start_scroll_offset, end_scroll_offset, element_id, orientation, scroll_timeline->GetResolvedScrollOffsets(),
time_range.GetAsDouble()); time_range.GetAsDouble());
} }
...@@ -105,6 +102,12 @@ CompositorScrollTimeline::ScrollDirection ConvertOrientation( ...@@ -105,6 +102,12 @@ CompositorScrollTimeline::ScrollDirection ConvertOrientation(
: CompositorScrollTimeline::ScrollUp; : CompositorScrollTimeline::ScrollUp;
} }
double ComputeProgress(double current_offset,
const WTF::Vector<double>& resolved_offsets) {
return cc::ComputeProgress<WTF::Vector<double>>(current_offset,
resolved_offsets);
}
} // namespace scroll_timeline_util } // namespace scroll_timeline_util
} // namespace blink } // namespace blink
...@@ -40,6 +40,9 @@ GetCompositorScrollElementId(const Node*); ...@@ -40,6 +40,9 @@ GetCompositorScrollElementId(const Node*);
CompositorScrollTimeline::ScrollDirection CORE_EXPORT CompositorScrollTimeline::ScrollDirection CORE_EXPORT
ConvertOrientation(ScrollTimeline::ScrollDirection, const ComputedStyle*); ConvertOrientation(ScrollTimeline::ScrollDirection, const ComputedStyle*);
double ComputeProgress(double current_offset,
const WTF::Vector<double>& resolved_offsets);
} // namespace scroll_timeline_util } // namespace scroll_timeline_util
} // namespace blink } // namespace blink
......
...@@ -35,11 +35,9 @@ cc::AnimationTimeline* CompositorAnimationTimeline::GetAnimationTimeline() ...@@ -35,11 +35,9 @@ cc::AnimationTimeline* CompositorAnimationTimeline::GetAnimationTimeline()
void CompositorAnimationTimeline::UpdateCompositorTimeline( void CompositorAnimationTimeline::UpdateCompositorTimeline(
base::Optional<CompositorElementId> pending_id, base::Optional<CompositorElementId> pending_id,
base::Optional<double> start_scroll_offset, const std::vector<double> scroll_offsets) {
base::Optional<double> end_scroll_offset) {
ToScrollTimeline(animation_timeline_.get()) ToScrollTimeline(animation_timeline_.get())
->UpdateScrollerIdAndScrollOffsets(pending_id, start_scroll_offset, ->UpdateScrollerIdAndScrollOffsets(pending_id, scroll_offsets);
end_scroll_offset);
} }
void CompositorAnimationTimeline::AnimationAttached( void CompositorAnimationTimeline::AnimationAttached(
......
...@@ -31,8 +31,7 @@ class PLATFORM_EXPORT CompositorAnimationTimeline { ...@@ -31,8 +31,7 @@ class PLATFORM_EXPORT CompositorAnimationTimeline {
cc::AnimationTimeline* GetAnimationTimeline() const; cc::AnimationTimeline* GetAnimationTimeline() const;
void UpdateCompositorTimeline(base::Optional<CompositorElementId> pending_id, void UpdateCompositorTimeline(base::Optional<CompositorElementId> pending_id,
base::Optional<double> start_scroll_offset, const std::vector<double> scroll_offsets);
base::Optional<double> end_scroll_offset);
void AnimationAttached(const CompositorAnimationClient&); void AnimationAttached(const CompositorAnimationClient&);
void AnimationDestroyed(const CompositorAnimationClient&); void AnimationDestroyed(const CompositorAnimationClient&);
......
...@@ -462,4 +462,87 @@ promise_test(async t => { ...@@ -462,4 +462,87 @@ promise_test(async t => {
await waitForNextFrame(); await waitForNextFrame();
assert_equals(scrollTimeline.currentTime, scrollTimeline.timeRange); assert_equals(scrollTimeline.currentTime, scrollTimeline.timeRange);
}, 'currentTime handles startScrollOffset > endScrollOffset correctly'); }, 'currentTime handles startScrollOffset > endScrollOffset correctly');
promise_test(async t => {
const scroller = setupScrollTimelineTest();
// Set the timeRange such that currentTime maps directly to the value
// scrolled. The contents and scroller are square, so it suffices to compute
// one edge and use it for all the timelines.
const scrollerSize = scroller.scrollHeight - scroller.clientHeight;
const scrollTimeline = new ScrollTimeline({
scrollSource: scroller,
timeRange: scrollerSize,
orientation: 'block',
scrollOffsets: ['10px', '20px', '40px', '70px', '90px'],
});
var offset = 0;
var w = 1 / 4; // offset weight
var p = 0; // progress within the offset
scroller.scrollTop = 10;
assert_times_equal(
scrollTimeline.currentTime, (offset + p) * w * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
p = (12 - 10) / (20 - 10);
scroller.scrollTop = 12;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, (offset + p) * w * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
offset = 1;
p = 0;
scroller.scrollTop = 20;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, (offset + p) * w * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
p = (35 - 20) / (40 - 20);
scroller.scrollTop = 35;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, (offset + p) * w * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
offset = 2;
p = 0;
scroller.scrollTop = 40;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, (offset + p) * w * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
p = (60 - 40) / (70 - 40);
scroller.scrollTop = 60;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, (offset + p) * w * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
offset = 3;
p = 0;
scroller.scrollTop = 70;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, (offset + p) * w * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
p = (80 - 70) / (90 - 70);
scroller.scrollTop = 80;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, (offset + p) * w * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
scroller.scrollTop = 90;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
}, 'currentTime calculations when multiple scroll offsets are specified');
</script> </script>
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