Commit 6c8523d2 authored by Majid Valipour's avatar Majid Valipour Committed by Commit Bot

[scroll-timeline] Implement element-based scroll offset

Implement basic element-based offset calculation taking
edge and threshold into account.

Test: external/wpt/scroll-animations/element-based-offset.html
Bug: 1023375

Change-Id: I38caf32e775e6827a9b7d8763bb7cf9c86fc29c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2100887Reviewed-by: default avatarMajid Valipour <majidvp@chromium.org>
Reviewed-by: default avatarYi Gu <yigu@chromium.org>
Commit-Queue: Majid Valipour <majidvp@chromium.org>
Cr-Commit-Position: refs/heads/master@{#759266}
parent 2bec188e
...@@ -5,10 +5,12 @@ ...@@ -5,10 +5,12 @@
// Experimental IDL of element-based offsets based on this proposal: // Experimental IDL of element-based offsets based on this proposal:
// https://github.com/w3c/csswg-drafts/issues/4337 // https://github.com/w3c/csswg-drafts/issues/4337
enum Edge {"start", "end"};
dictionary ScrollTimelineElementBasedOffset { dictionary ScrollTimelineElementBasedOffset {
Element target; Element target;
double threshold = 0.0; double threshold = 0.0;
Edge edge = "start";
// TODO(majidvp): Add other values from proposal. http://crbug.com/1023375 // TODO(majidvp): Add other values from proposal. http://crbug.com/1023375
// Edge edge = "start";
// DOMString rootMargin; // DOMString rootMargin;
}; };
\ No newline at end of file
...@@ -33,7 +33,7 @@ bool StringToScrollOffset(String scroll_offset, ...@@ -33,7 +33,7 @@ bool StringToScrollOffset(String scroll_offset,
return true; return true;
} }
bool ValidateIntersectionBasedOffset(ScrollTimelineElementBasedOffset* offset) { bool ValidateElementBasedOffset(ScrollTimelineElementBasedOffset* offset) {
if (!offset->hasTarget()) if (!offset->hasTarget())
return false; return false;
...@@ -60,7 +60,7 @@ ScrollTimelineOffset* ScrollTimelineOffset::Create( ...@@ -60,7 +60,7 @@ ScrollTimelineOffset* ScrollTimelineOffset::Create(
return MakeGarbageCollected<ScrollTimelineOffset>(offset); return MakeGarbageCollected<ScrollTimelineOffset>(offset);
} else if (input_offset.IsScrollTimelineElementBasedOffset()) { } else if (input_offset.IsScrollTimelineElementBasedOffset()) {
auto* offset = input_offset.GetAsScrollTimelineElementBasedOffset(); auto* offset = input_offset.GetAsScrollTimelineElementBasedOffset();
if (!ValidateIntersectionBasedOffset(offset)) if (!ValidateElementBasedOffset(offset))
return nullptr; return nullptr;
return MakeGarbageCollected<ScrollTimelineOffset>(offset); return MakeGarbageCollected<ScrollTimelineOffset>(offset);
...@@ -78,7 +78,7 @@ double ScrollTimelineOffset::ResolveOffset(Node* scroll_source, ...@@ -78,7 +78,7 @@ double ScrollTimelineOffset::ResolveOffset(Node* scroll_source,
DCHECK(root_box); DCHECK(root_box);
Document& document = root_box->GetDocument(); Document& document = root_box->GetDocument();
if (scroll_based_) { if (length_based_offset_) {
// Resolve scroll based offset. // Resolve scroll based offset.
const ComputedStyle& computed_style = root_box->StyleRef(); const ComputedStyle& computed_style = root_box->StyleRef();
const ComputedStyle* root_style = const ComputedStyle* root_style =
...@@ -90,14 +90,90 @@ double ScrollTimelineOffset::ResolveOffset(Node* scroll_source, ...@@ -90,14 +90,90 @@ double ScrollTimelineOffset::ResolveOffset(Node* scroll_source,
&computed_style, root_style, document.GetLayoutView(), &computed_style, root_style, document.GetLayoutView(),
computed_style.EffectiveZoom()); computed_style.EffectiveZoom());
double resolved = FloatValueForLength( double resolved = FloatValueForLength(
scroll_based_->ConvertToLength(conversion_data), max_offset); length_based_offset_->ConvertToLength(conversion_data), max_offset);
return resolved; return resolved;
} else if (element_based) { } else if (element_based_offset_) {
// TODO(majidvp): turn element based info into an offset. // We assume that the root is the target's ancestor in layout tree. Under
// this assumption |target.LocalToAncestorRect()| returns the targets's
// position relative to the root's border box, while ignoring scroll offset.
//
// TODO(majidvp): We need to validate this assumption and deal with cases
// where it is not true. See the spec discussion here:
// https://github.com/w3c/csswg-drafts/issues/4337#issuecomment-610989843
DCHECK(element_based_offset_->hasTarget());
Element* target = element_based_offset_->target();
const LayoutBox* target_box = target->GetLayoutBox();
// It is possible for target to not have a layout box e.g., if it is an
// unattached element. In which case we return the default offset for now.
//
// TODO(majidvp): Need to consider this case in the spec. Most likely we
// should remain unresolved. See the spec discussion here:
// https://github.com/w3c/csswg-drafts/issues/4337#issuecomment-610997231
if (!target_box) {
return default_offset;
}
PhysicalRect target_rect = target_box->PhysicalBorderBoxRect();
target_rect = target_box->LocalToAncestorRect(
target_rect, root_box,
kTraverseDocumentBoundaries | kIgnoreScrollOffset);
PhysicalRect root_rect(root_box->PhysicalBorderBoxRect());
LayoutUnit root_edge;
LayoutUnit target_edge;
// Here is the simple diagram that shows the computation.
//
// +-----+
// | | +------+
// | | | |
// edge:start +----+-----+-------------------+-----+-------+
// | |xxxxxx| |xxxxx| |
// | +------+ |xxxxx| |
// | +-----+ |
// | |
// threshold: | A) 0 B) 0.5 C) 1 |
// | |
// | +-----+ |
// | +------+ |xxxxx| |
// | |xxxxxx| |xxxxx| |
// edge: end +----+-----+-------------------+-----+-------+
// | | | |
// | | +------+
// +-----+
//
// We always take the target top edge and compute the distance to the
// root's selected edge. This give us (C) in start edge case and (A) in
// end edge case.
//
// To take threshold into account we simply add (1-threshold) or threshold
// in start and end edge cases respectively.
bool is_start = element_based_offset_->edge() == "start";
float threshold_adjustment = is_start
? (1 - element_based_offset_->threshold())
: element_based_offset_->threshold();
if (orientation == kVerticalScroll) {
root_edge = is_start ? root_rect.Y() : root_rect.Bottom();
target_edge = target_rect.Y();
// Note that threshold is considered as a portion of target and not as a
// portion of root. IntersectionObserver has option to allow both.
target_edge += (threshold_adjustment * target_rect.Height());
} else { // kHorizontalScroll
root_edge = is_start ? root_rect.X() : root_rect.Right();
target_edge = target_rect.X();
target_edge += (threshold_adjustment * target_rect.Width());
}
// TODO(majidvp): Potentially clip by min/max scroll offsets.
// http://crbug.com/1023375 // http://crbug.com/1023375
return default_offset; return (target_edge - root_edge).ToDouble();
} else { } else {
// Resolve the default case (i.e., 'auto' value)
return default_offset; return default_offset;
} }
} }
...@@ -105,12 +181,12 @@ double ScrollTimelineOffset::ResolveOffset(Node* scroll_source, ...@@ -105,12 +181,12 @@ double ScrollTimelineOffset::ResolveOffset(Node* scroll_source,
StringOrScrollTimelineElementBasedOffset StringOrScrollTimelineElementBasedOffset
ScrollTimelineOffset::ToStringOrScrollTimelineElementBasedOffset() const { ScrollTimelineOffset::ToStringOrScrollTimelineElementBasedOffset() const {
StringOrScrollTimelineElementBasedOffset result; StringOrScrollTimelineElementBasedOffset result;
if (scroll_based_) { if (length_based_offset_) {
result.SetString(scroll_based_->CssText()); result.SetString(length_based_offset_->CssText());
} else if (element_based) { } else if (element_based_offset_) {
result.SetScrollTimelineElementBasedOffset(element_based); result.SetScrollTimelineElementBasedOffset(element_based_offset_);
} else { } else {
// we are dealing with default value // This is the default value (i.e., 'auto' value)
result.SetString("auto"); result.SetString("auto");
} }
...@@ -118,15 +194,15 @@ ScrollTimelineOffset::ToStringOrScrollTimelineElementBasedOffset() const { ...@@ -118,15 +194,15 @@ ScrollTimelineOffset::ToStringOrScrollTimelineElementBasedOffset() const {
} }
ScrollTimelineOffset::ScrollTimelineOffset(CSSPrimitiveValue* offset) ScrollTimelineOffset::ScrollTimelineOffset(CSSPrimitiveValue* offset)
: scroll_based_(offset), element_based(nullptr) {} : length_based_offset_(offset), element_based_offset_(nullptr) {}
ScrollTimelineOffset::ScrollTimelineOffset( ScrollTimelineOffset::ScrollTimelineOffset(
ScrollTimelineElementBasedOffset* offset) ScrollTimelineElementBasedOffset* offset)
: scroll_based_(nullptr), element_based(offset) {} : length_based_offset_(nullptr), element_based_offset_(offset) {}
void ScrollTimelineOffset::Trace(blink::Visitor* visitor) { void ScrollTimelineOffset::Trace(blink::Visitor* visitor) {
visitor->Trace(scroll_based_); visitor->Trace(length_based_offset_);
visitor->Trace(element_based); visitor->Trace(element_based_offset_);
} }
} // namespace blink } // namespace blink
...@@ -54,8 +54,8 @@ class CORE_EXPORT ScrollTimelineOffset final ...@@ -54,8 +54,8 @@ class CORE_EXPORT ScrollTimelineOffset final
// We either have an scroll or element based offset so at any time one of // We either have an scroll or element based offset so at any time one of
// these is null. If both are null, it represents the default value of // these is null. If both are null, it represents the default value of
// 'auto'. // 'auto'.
Member<CSSPrimitiveValue> scroll_based_; Member<CSSPrimitiveValue> length_based_offset_;
Member<ScrollTimelineElementBasedOffset> element_based; Member<ScrollTimelineElementBasedOffset> element_based_offset_;
}; };
} // namespace blink } // namespace blink
......
<!DOCTYPE html>
<meta charset=utf-8>
<title>Test element-based scroll offset for scroll timeline.</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<script src="testcommon.js"></script>
<style>
.scroller {
overflow: auto;
height: 500px;
width: 500px;
}
.contents {
height: 2000px;
width: 2000px;
position: relative;
}
.vertical #start, .vertical #end {
background: blue;
border-top: 5px solid pink;
box-sizing: border-box;
width: 100%;
height: 50px;
}
.vertical #start {
position: absolute;
top: 50px;
}
.vertical #end {
position: absolute;
top: 1050px;
}
.horizontal #start, .horizontal #end {
background: blue;
border-left:5px solid pink;
box-sizing: border-box;
height: 100%;
width: 50px;
}
.horizontal #start {
position: absolute;
left: 50px;
}
.horizontal #end {
position: absolute;
left: 1050px;
}
</style>
<div id="log"></div>
<script>
'use strict';
function createScrollerWithStartAndEnd(test, orientationClass) {
var scroller = createDiv(test);
scroller.innerHTML =
`<div class='contents'>
<div id='start'></div>
<div id='end'></div>
</div>`;
scroller.classList.add('scroller');
scroller.classList.add(orientationClass);
return scroller;
}
async function createScrollAnimationTest(description, config) {
promise_test(async t => {
const scroller = createScrollerWithStartAndEnd(t, config.orientation);
t.add_cleanup(() => scroller.remove());
const start = scroller.querySelector("#start");
const end = scroller.querySelector("#end")
const timeline = createScrollTimeline(t, {
scrollSource: scroller,
orientation: config.orientation,
timeRange: 1000,
fill: 'both',
startScrollOffset: {target: start, ...config.start},
endScrollOffset: {target: end, ...config.end }
});
const animation = createScrollLinkedAnimation(t, timeline);
const scrollRange = end.offsetTop - start.offsetTop;
const timeRange = animation.timeline.timeRange;
// Verify initial start and current times in Idle state.
assert_equals(animation.currentTime, null,
"The current time is null in Idle state.");
assert_equals(animation.startTime, null,
"The start time is null in Idle state.");
animation.play();
// Verify initial start and current times in Pending state.
assert_times_equal(animation.currentTime, 0,
"The current time is a hold time in Pending state.");
assert_equals(animation.startTime, null,
"The start time is null in Pending state.");
await animation.ready;
// Verify initial start and current times in Playing state.
assert_times_equal(animation.currentTime, 0,
"The current time is zero in Playing state.");
assert_times_equal(animation.startTime, 0,
"The start time is zero in Playing state.");
// Now do some scrolling and make sure that the Animation current time is
// correct.
if (config.orientation == 'vertical') {
scroller.scrollTo({top: config.scrollTo});
assert_equals(scroller.scrollTop, config.scrollTo);
} else {
scroller.scrollTo({left: config.scrollTo});
assert_equals(scroller.scrollLeft, config.scrollTo);
}
await waitForNextFrame();
assert_times_equal(animation.timeline.currentTime, config.expectedCurrentTime,
"The timeline current time corresponds to the scroll position of the scroller.");
assert_times_equal(animation.currentTime, config.expectedCurrentTime,
"The animation current time corresponds to the scroll position of the scroller.");
assert_times_equal(
animation.effect.getComputedTiming().localTime,
config.expectedCurrentTime,
'Effect local time corresponds to the scroll position of the scroller.');
}, description);
}
// start is @ 50px
// end is @ 1050px
// both have 50px heights
// scroller has 500px heights
// For each test the expected start/end is in the comment to help with the
// verification.
const tests = {
// offsets: [100, 1100]
"at start": {
scrollTo: 100,
expectedCurrentTime: 0,
},
// offsets: [100, 1100]
"after start": {
scrollTo: 200,
expectedCurrentTime: 100,
},
// offsets: [100, 1100]
"at middle" : {
scrollTo: 600,
expectedCurrentTime: 500,
},
// offsets: [100, 1100]
"at end" : {
scrollTo: 1099,
expectedCurrentTime: 999,
},
// offsets: [100, 1100]
"after end" : {
scrollTo: 1150,
expectedCurrentTime: 1000,
},
// offsets: [75, 1075]
"with threshold 0.5" : {
// give threshold to both start and end to keep scrollRange
// 1000 which simplifies the calculation.
start: {threshold: 0.5},
end: {threshold: 0.5},
scrollTo: 600 - 25,
expectedCurrentTime: 500,
},
// offsets: [50, 1050]
"with threshold 1.0": {
start: {threshold: 1.0},
end: {threshold: 1.0},
scrollTo: 600 - 50,
expectedCurrentTime: 500,
},
// offset: [100, 550]
"with end edge" : {
end: {edge: "end"},
scrollTo: 325,
expectedCurrentTime: 500,
},
// offset: [100, 600]
"with end edge and threshold 1.0": {
end: {
threshold: 1.0,
edge: "end"
},
scrollTo: 350,
expectedCurrentTime: 500,
},
};
for (let orientation of ['vertical', 'horizontal']) {
for (let testName in tests) {
const description = `Animation start and current times are correct given
element-based offsets for orienation ${orientation} and ${testName}.`;
const config = tests[testName];
config.orientation = orientation;
createScrollAnimationTest(description, config);
}
}
</script>
\ No newline at end of file
...@@ -5,15 +5,16 @@ function createScroller(test) { ...@@ -5,15 +5,16 @@ function createScroller(test) {
return scroller; return scroller;
} }
function createScrollTimeline(test) { function createScrollTimeline(test, options) {
return new ScrollTimeline({ options = options || {
scrollSource: createScroller(test), scrollSource: createScroller(test),
timeRange: 1000 timeRange: 1000
}); }
return new ScrollTimeline(options);
} }
function createScrollTimelineWithOffsets(test, startOffset, endOffset) { function createScrollTimelineWithOffsets(test, startOffset, endOffset) {
return new ScrollTimeline({ return createScrollTimeline(test, {
scrollSource: createScroller(test), scrollSource: createScroller(test),
orientation: "vertical", orientation: "vertical",
startScrollOffset: startOffset, startScrollOffset: startOffset,
...@@ -23,7 +24,7 @@ function createScrollTimelineWithOffsets(test, startOffset, endOffset) { ...@@ -23,7 +24,7 @@ function createScrollTimelineWithOffsets(test, startOffset, endOffset) {
} }
function createScrollLinkedAnimation(test, timeline) { function createScrollLinkedAnimation(test, timeline) {
if(timeline === undefined) if (timeline === undefined)
timeline = createScrollTimeline(test); timeline = createScrollTimeline(test);
const DURATION = 1000; // ms const DURATION = 1000; // ms
const KEYFRAMES = { opacity: [1, 0] }; const KEYFRAMES = { opacity: [1, 0] };
......
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