Commit cbaaf9e4 authored by Olga Gerchikov's avatar Olga Gerchikov Committed by Chromium LUCI CQ

[Scroll Timeline] Clamp overlapping offsets.

Updated procedure that computes scroll timeline progress to accommodate
for overlapping offsets. The update is to find last matching interval in
the list of offsets. This is analogous to CSS property overrides where
last specified property is applied.

Bug: 1094014
Change-Id: I7d5c84e58bfea021f931b702d66a87d2d28aaf6c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2508356
Commit-Queue: Olga Gerchikov <gerchiko@microsoft.com>
Reviewed-by: default avatarKevin Ellis <kevers@chromium.org>
Reviewed-by: default avatarRobert Flack <flackr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#842367}
parent b9b9a8a5
......@@ -5,6 +5,7 @@
#ifndef CC_ANIMATION_SCROLL_TIMELINE_H_
#define CC_ANIMATION_SCROLL_TIMELINE_H_
#include <vector>
#include "base/optional.h"
#include "base/time/time.h"
#include "cc/animation/animation_export.h"
......@@ -124,14 +125,22 @@ inline ScrollTimeline* ToScrollTimeline(AnimationTimeline* timeline) {
template <typename T>
double ComputeProgress(double current_offset, const T& resolved_offsets) {
DCHECK_GE(resolved_offsets.size(), 2u);
// When start offset is greater than end offset, current time is calculated
// outside of this method.
DCHECK(resolved_offsets[0] < resolved_offsets[resolved_offsets.size() - 1]);
DCHECK(current_offset < resolved_offsets[resolved_offsets.size() - 1]);
// Look for scroll offset that contains the current offset.
// Traverse scroll offsets from the back to find first interval that
// contains the current offset. In case of overlapping offsets, last matching
// interval in the list is used to calculate the current time. The rational
// for choosing last matching offset is to be consistent with CSS property
// overrides.
unsigned int offset_id;
for (offset_id = 1; offset_id < resolved_offsets.size() &&
resolved_offsets[offset_id] <= current_offset;
offset_id++) {
for (offset_id = resolved_offsets.size() - 1;
offset_id > 0 && !(resolved_offsets[offset_id - 1] <= current_offset &&
current_offset < resolved_offsets[offset_id]);
offset_id--) {
}
DCHECK(offset_id < resolved_offsets.size());
DCHECK_GE(offset_id, 1u);
// 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.
......
......@@ -205,6 +205,51 @@ TEST_F(ScrollTimelineTest, MultipleScrollOffsetsCurrentTimeCalculations) {
time_range, vertical_timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, OverlappingScrollOffsets) {
double time_range = 100.0;
// Start offset is greater than end offset ==> animation progress is
// either 0% or 100%.
std::vector<double> scroll_offsets = {350.0, 200.0, 50.0};
scoped_refptr<ScrollTimeline> vertical_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets, time_range);
// Offset is less than start offset ==> current time is 0.
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 300));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
0, vertical_timeline->CurrentTime(scroll_tree(), false));
// Offset is greater than end offset ==> current time is time_range.
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 360));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
time_range, vertical_timeline->CurrentTime(scroll_tree(), false));
scroll_offsets = {0.0, 400.0, 200.0};
vertical_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets, time_range);
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 100));
// Scroll offset is 25% of [0, 400) range, which maps to [0% 50%) of the
// entire scroll range.
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
time_range * 0.5 * 0.25,
vertical_timeline->CurrentTime(scroll_tree(), false));
scroll_offsets = {200.0, 0.0, 400.0};
vertical_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets, time_range);
SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 300));
// Scroll offset is 75% of [0, 400) range, which maps to [50% 100%) of the
// entire scroll range.
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
time_range * (0.5 + 0.5 * 0.75),
vertical_timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForTimeRange) {
double time_range = content_size().height() - container_size().height();
......
......@@ -270,7 +270,6 @@ bool ScrollTimeline::ResolveScrollOffsets(
}
resolved_offsets.push_back(resolved_offset.value());
}
// TODO(crbug.com/1094014): Implement clamping for overlapping offsets.
DCHECK_GE(resolved_offsets.size(), 2u);
return true;
}
......
......@@ -129,6 +129,7 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline {
size_t AttachedAnimationsCount() const { return scroll_animations_.size(); }
private:
FRIEND_TEST_ALL_PREFIXES(ScrollTimelineTest, MultipleScrollOffsetsClamping);
// https://wicg.github.io/scroll-animations/#avoiding-cycles
// Snapshots scroll timeline current time and phase.
// Called once per animation frame.
......
......@@ -878,4 +878,68 @@ TEST_F(ScrollTimelineTest, MultipleScrollOffsetsCurrentTimeCalculations) {
EXPECT_EQ(100, scroll_timeline->CurrentTimeMilliseconds().value());
}
TEST_F(ScrollTimelineTest, OverlappingScrollOffsets) {
SetBodyInnerHTML(R"HTML(
<style>
#scroller { overflow: scroll; width: 100px; height: 100px; }
#spacer { height: 1000px; }
</style>
<div id='scroller'>
<div id ='spacer'></div>
</div>
)HTML");
auto* scroller =
To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller"));
ASSERT_TRUE(scroller);
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<ScrollTimelineOffsetValue> scroll_offsets = {
OffsetFromString("90px"), OffsetFromString("40px"),
OffsetFromString("10px")};
options->setScrollOffsets(scroll_offsets);
ScrollTimeline* scroll_timeline =
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
scrollable_area->SetScrollOffset(ScrollOffset(0, 80),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
EXPECT_EQ(0, scroll_timeline->CurrentTimeMilliseconds().value());
scrollable_area->SetScrollOffset(ScrollOffset(0, 95),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
EXPECT_EQ(100, scroll_timeline->CurrentTimeMilliseconds().value());
scroll_offsets = {OffsetFromString("0px"), OffsetFromString("100px"),
OffsetFromString("50px")};
options->setScrollOffsets(scroll_offsets);
scroll_timeline =
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
scrollable_area->SetScrollOffset(ScrollOffset(0, 40),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
EXPECT_EQ(20, scroll_timeline->CurrentTimeMilliseconds().value());
scroll_offsets = {OffsetFromString("50px"), OffsetFromString("0px"),
OffsetFromString("100px")};
options->setScrollOffsets(scroll_offsets);
scroll_timeline =
ScrollTimeline::Create(GetDocument(), options, ASSERT_NO_EXCEPTION);
scrollable_area->SetScrollOffset(ScrollOffset(0, 60),
mojom::blink::ScrollType::kProgrammatic);
SimulateFrame();
EXPECT_EQ(80, scroll_timeline->CurrentTimeMilliseconds().value());
}
} // namespace blink
......@@ -463,86 +463,4 @@ promise_test(async t => {
assert_equals(scrollTimeline.currentTime, scrollTimeline.timeRange);
}, '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: [CSS.px(10), CSS.px(20), CSS.px(40), CSS.px(70), CSS.px(90)],
});
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>
<!DOCTYPE html>
<meta charset="utf-8">
<title>ScrollTimeline current time algorithm</title>
<link rel="help" href="https://wicg.github.io/scroll-animations/#current-time-algorithm">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="./resources/scrolltimeline-utils.js"></script>
<body></body>
<script>
'use strict';
promise_test(async t => {
const scroller = setupScrollTimelineTest();
const scrollerSize = scroller.scrollHeight - scroller.clientHeight;
const scrollTimeline = new ScrollTimeline({
scrollSource: scroller,
timeRange: scrollerSize,
orientation: 'block',
scrollOffsets: [CSS.px(10), CSS.px(20), CSS.px(40), CSS.px(70), CSS.px(90)],
});
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');
promise_test(async t => {
const scroller = setupScrollTimelineTest();
const scrollerSize = scroller.scrollHeight - scroller.clientHeight;
var scrollTimeline = new ScrollTimeline({
scrollSource: scroller,
timeRange: scrollerSize,
orientation: 'block',
scrollOffsets: [CSS.px(300), CSS.px(200), CSS.px(10)],
});
scroller.scrollTop = 250;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, 0,
"current time calculation when scroll = " + scroller.scrollTop);
scroller.scrollTop = 400;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
scrollTimeline = new ScrollTimeline({
scrollSource: scroller,
timeRange: scrollerSize,
orientation: 'block',
scrollOffsets: [CSS.px(0), CSS.px(400), CSS.px(200)],
});
scroller.scrollTop = 100;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, 0.25 * 0.5 * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
scrollTimeline = new ScrollTimeline({
scrollSource: scroller,
timeRange: scrollerSize,
orientation: 'block',
scrollOffsets: [CSS.px(200), CSS.px(0), CSS.px(400)],
});
scroller.scrollTop = 200;
await waitForNextFrame();
assert_times_equal(
scrollTimeline.currentTime, 0.5 * scrollerSize + 0.5 * 0.5 * scrollerSize,
"current time calculation when scroll = " + scroller.scrollTop);
}, 'currentTime calculations when overlapping scroll offsets are specified');
</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