blob: 259917a785b36ad637ee1c712e5c057ec08668d1 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/animation/scroll_timeline.h"
#include <tuple>
#include "base/optional.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_scroll_timeline_options.h"
#include "third_party/blink/renderer/core/animation/scroll_timeline_offset.h"
#include "third_party/blink/renderer/core/animation/scroll_timeline_util.h"
#include "third_party/blink/renderer/core/css/css_to_length_conversion_data.h"
#include "third_party/blink/renderer/core/css/cssom/css_unit_values.h"
#include "third_party/blink/renderer/core/css/parser/css_parser_context.h"
#include "third_party/blink/renderer/core/css/parser/css_tokenizer.h"
#include "third_party/blink/renderer/core/css/properties/css_parsing_utils.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/paint/compositing/paint_layer_compositor.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/scroll/scroll_types.h"
#include "third_party/blink/renderer/platform/geometry/length_functions.h"
namespace blink {
namespace {
constexpr double kScrollTimelineDuration = 100.0;
// Animation times are tracked as TimeDeltas which are stored internally as an
// integer number of microseconds. Multiplying by 1000 converts this into a
// value equivalent to Milliseconds.
constexpr double kScrollTimelineDurationMs = kScrollTimelineDuration * 1000.0;
using ScrollTimelineSet =
HeapHashMap<WeakMember<Node>,
Member<HeapHashSet<WeakMember<ScrollTimeline>>>>;
ScrollTimelineSet& GetScrollTimelineSet() {
DEFINE_STATIC_LOCAL(Persistent<ScrollTimelineSet>, set,
(MakeGarbageCollected<ScrollTimelineSet>()));
return *set;
}
using ActiveScrollTimelineSet = HeapHashCountedSet<WeakMember<Node>>;
ActiveScrollTimelineSet& GetActiveScrollTimelineSet() {
DEFINE_STATIC_LOCAL(Persistent<ActiveScrollTimelineSet>, set,
(MakeGarbageCollected<ActiveScrollTimelineSet>()));
return *set;
}
bool StringToScrollDirection(String scroll_direction,
ScrollTimeline::ScrollDirection& result) {
if (scroll_direction == "block") {
result = ScrollTimeline::Block;
return true;
}
if (scroll_direction == "inline") {
result = ScrollTimeline::Inline;
return true;
}
if (scroll_direction == "horizontal") {
result = ScrollTimeline::Horizontal;
return true;
}
if (scroll_direction == "vertical") {
result = ScrollTimeline::Vertical;
return true;
}
return false;
}
ScrollOrientation ToPhysicalScrollOrientation(
ScrollTimeline::ScrollDirection direction,
const LayoutBox& source_box) {
bool is_horizontal = source_box.IsHorizontalWritingMode();
switch (direction) {
case ScrollTimeline::Block:
return is_horizontal ? kVerticalScroll : kHorizontalScroll;
case ScrollTimeline::Inline:
return is_horizontal ? kHorizontalScroll : kVerticalScroll;
case ScrollTimeline::Horizontal:
return kHorizontalScroll;
case ScrollTimeline::Vertical:
return kVerticalScroll;
}
}
// Note that the resolution process may trigger document lifecycle to clean
// style and layout.
Node* ResolveScrollSource(Element* scroll_source) {
// When in quirks mode we need the style to be clean, so we don't use
// |ScrollingElementNoLayout|.
if (scroll_source &&
scroll_source == scroll_source->GetDocument().scrollingElement()) {
return &scroll_source->GetDocument();
}
return scroll_source;
}
} // namespace
ScrollTimeline* ScrollTimeline::Create(Document& document,
ScrollTimelineOptions* options,
ExceptionState& exception_state) {
Element* scroll_source = options->hasScrollSource()
? options->scrollSource()
: document.scrollingElement();
ScrollDirection orientation;
if (!StringToScrollDirection(options->orientation(), orientation)) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Invalid orientation");
return nullptr;
}
ScrollTimelineOffset* start_scroll_offset =
ScrollTimelineOffset::Create(options->startScrollOffset());
if (!start_scroll_offset) {
exception_state.ThrowTypeError("Invalid start offset.");
return nullptr;
}
ScrollTimelineOffset* end_scroll_offset =
ScrollTimelineOffset::Create(options->endScrollOffset());
if (!end_scroll_offset) {
exception_state.ThrowTypeError("Invalid end offset");
return nullptr;
}
// TODO(crbug.com/1094014): Either scroll offsets or start/end offsets can
// be specified.
if (!options->scrollOffsets().IsEmpty() &&
(!start_scroll_offset->IsDefaultValue() ||
!end_scroll_offset->IsDefaultValue())) {
exception_state.ThrowTypeError(
"Either scrollOffsets or start/end offsets can be specified.");
return nullptr;
}
HeapVector<Member<ScrollTimelineOffset>> scroll_offsets;
if (options->scrollOffsets().IsEmpty()) {
// 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 (!start_scroll_offset->IsDefaultValue())
scroll_offsets.push_back(start_scroll_offset);
if (!end_scroll_offset->IsDefaultValue() ||
!start_scroll_offset->IsDefaultValue())
scroll_offsets.push_back(end_scroll_offset);
} else {
for (auto& offset : options->scrollOffsets()) {
ScrollTimelineOffset* scroll_offset =
ScrollTimelineOffset::Create(offset);
if (!scroll_offset) {
exception_state.ThrowTypeError("Invalid scroll offset");
return nullptr;
}
if (scroll_offset->IsDefaultValue() &&
(options->scrollOffsets().size() == 1 ||
(scroll_offsets.size() + 1) < options->scrollOffsets().size())) {
exception_state.ThrowTypeError(
"Invalid scrollOffsets: 'auto' can only be set as an end "
"offset when start offset presents.");
return nullptr;
}
scroll_offsets.push_back(scroll_offset);
}
}
base::Optional<double> time_range;
if (options->timeRange().IsDouble()) {
time_range = base::make_optional(options->timeRange().GetAsDouble());
}
return MakeGarbageCollected<ScrollTimeline>(
&document, scroll_source, orientation, scroll_offsets, time_range);
}
ScrollTimeline::ScrollTimeline(
Document* document,
Element* scroll_source,
ScrollDirection orientation,
HeapVector<Member<ScrollTimelineOffset>> scroll_offsets,
base::Optional<double> time_range)
: AnimationTimeline(document),
scroll_source_(scroll_source),
resolved_scroll_source_(ResolveScrollSource(scroll_source_)),
orientation_(orientation),
scroll_offsets_(std::move(scroll_offsets)),
time_range_(time_range) {
if (resolved_scroll_source_) {
ScrollTimelineSet& set = GetScrollTimelineSet();
if (!set.Contains(resolved_scroll_source_)) {
set.insert(
resolved_scroll_source_,
MakeGarbageCollected<HeapHashSet<WeakMember<ScrollTimeline>>>());
}
auto it = set.find(resolved_scroll_source_);
it->value->insert(this);
}
SnapshotState();
}
bool ScrollTimeline::IsActive() const {
return timeline_state_snapshotted_.phase != TimelinePhase::kInactive;
}
void ScrollTimeline::Invalidate() {
ScheduleNextServiceInternal(/* time_check = */ false);
}
bool ScrollTimeline::ComputeIsActive() const {
LayoutBox* layout_box = resolved_scroll_source_
? resolved_scroll_source_->GetLayoutBox()
: nullptr;
return layout_box && layout_box->IsScrollContainer();
}
ScrollTimelineOffset* ScrollTimeline::StartScrollOffset() const {
// Single entry offset in scrollOffsets is considered as 'end'. Thus,
// resolving start offset only if there is at least 2 offsets.
return scroll_offsets_.size() >= 2 ? scroll_offsets_.at(0) : nullptr;
}
ScrollTimelineOffset* ScrollTimeline::EndScrollOffset() const {
// End offset is always the last offset in scrollOffsets if exists.
return scroll_offsets_.size() >= 1
? scroll_offsets_.at(scroll_offsets_.size() - 1)
: nullptr;
}
const std::vector<double> ScrollTimeline::GetResolvedScrollOffsets() 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());
LayoutBox* layout_box = resolved_scroll_source_->GetLayoutBox();
DCHECK(layout_box);
double current_offset;
double max_offset;
GetCurrentAndMaxOffset(layout_box, current_offset, max_offset);
auto orientation = ToPhysicalScrollOrientation(orientation_, *layout_box);
if (scroll_offsets_.size() == 0) {
// Start and end offsets resolve to 'auto'.
resolved_offsets.push_back(0);
resolved_offsets.push_back(max_offset);
return true;
}
// Single entry offset in scrollOffsets is considered as 'end'.
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());
}
DCHECK_GE(resolved_offsets.size(), 2u);
return true;
}
AnimationTimeline::PhaseAndTime ScrollTimeline::CurrentPhaseAndTime() {
return {timeline_state_snapshotted_.phase,
timeline_state_snapshotted_.current_time};
}
bool ScrollTimeline::ScrollOffsetsEqual(
const HeapVector<Member<ScrollTimelineOffset>>& other) const {
if (scroll_offsets_.size() != other.size())
return false;
size_t size = scroll_offsets_.size();
for (size_t i = 0; i < size; ++i) {
if (!DataEquivalent(scroll_offsets_.at(i), other.at(i)))
return false;
}
return true;
}
void ScrollTimeline::currentTime(CSSNumberish& currentTime) {
// time returns either in milliseconds or a 0 to 100 value representing the
// progress of the timeline
auto current_time = timeline_state_snapshotted_.current_time;
// TODO(crbug.com/1140602): Support progress based animations
// We are currently abusing the intended use of the "auto" keyword. We are
// using it here as a signal to use progress based timeline instead of having
// a range based current time.
// We are doing this maintain backwards compatibility with existing tests.
if (time_range_) {
// not using progress based, return time as double
currentTime =
current_time ? CSSNumberish::FromDouble(current_time->InMillisecondsF())
: CSSNumberish();
} else {
currentTime = current_time
? CSSNumberish::FromCSSNumericValue(
CSSUnitValues::percent(current_time->InSecondsF()))
: CSSNumberish();
}
}
void ScrollTimeline::duration(CSSNumberish& duration) {
if (time_range_) {
duration = CSSNumberish::FromDouble(time_range_.value());
} else {
duration = CSSNumberish::FromCSSNumericValue(
CSSUnitValues::percent(kScrollTimelineDuration));
}
}
ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() const {
// 1. If scroll timeline is inactive, return an unresolved time value.
// https://github.com/WICG/scroll-animations/issues/31
// https://wicg.github.io/scroll-animations/#current-time-algorithm
WTF::Vector<double> resolved_offsets;
if (!ComputeIsActive()) {
return {TimelinePhase::kInactive, /*current_time*/ base::nullopt,
resolved_offsets};
}
LayoutBox* layout_box = resolved_scroll_source_->GetLayoutBox();
// 2. Otherwise, let current scroll offset be the current scroll offset of
// scrollSource in the direction specified by orientation.
double current_offset;
double max_offset;
GetCurrentAndMaxOffset(layout_box, current_offset, max_offset);
bool resolved = ResolveScrollOffsets(resolved_offsets);
if (!resolved) {
DCHECK(resolved_offsets.IsEmpty());
return {TimelinePhase::kInactive, /*current_time*/ base::nullopt,
resolved_offsets};
}
double start_offset = resolved_offsets[0];
double end_offset = resolved_offsets[resolved_offsets.size() - 1];
// TODO(crbug.com/1060384): Once the spec has been updated to state what the
// expected result is when startScrollOffset >= endScrollOffset, we might need
// to add a special case here. See
// https://github.com/WICG/scroll-animations/issues/20
// 3. If current scroll offset is less than startScrollOffset:
if (current_offset < start_offset) {
return {TimelinePhase::kBefore, base::TimeDelta(), resolved_offsets};
}
double duration =
time_range_ ? time_range_.value() : kScrollTimelineDurationMs;
// 4. If current scroll offset is greater than or equal to endScrollOffset:
if (current_offset >= end_offset) {
// If end_offset is greater than or equal to the maximum scroll offset of
// scrollSource in orientation then return active phase, otherwise return
// after phase.
TimelinePhase phase = end_offset >= max_offset ? TimelinePhase::kActive
: TimelinePhase::kAfter;
return {phase, base::TimeDelta::FromMillisecondsD(duration),
resolved_offsets};
}
// 5. Return the result of evaluating the following expression:
// ((current scroll offset - startScrollOffset) /
// (endScrollOffset - startScrollOffset)) * effective time range
base::Optional<base::TimeDelta> calculated_current_time =
base::TimeDelta::FromMillisecondsD(scroll_timeline_util::ComputeProgress(
current_offset, resolved_offsets) *
duration);
return {TimelinePhase::kActive, calculated_current_time, resolved_offsets};
}
// Scroll-linked animations are initialized with the start time of zero.
base::Optional<base::TimeDelta>
ScrollTimeline::InitialStartTimeForAnimations() {
return base::TimeDelta();
}
void ScrollTimeline::ServiceAnimations(TimingUpdateReason reason) {
// Snapshot timeline state once at top of animation frame.
if (reason == kTimingUpdateForAnimationFrame)
SnapshotState();
// When scroll timeline goes from inactive to active the animations may need
// to be started and possibly composited.
bool was_active =
last_current_phase_and_time_ &&
last_current_phase_and_time_.value().phase == TimelinePhase::kActive;
if (!was_active && IsActive())
MarkAnimationsCompositorPending();
AnimationTimeline::ServiceAnimations(reason);
}
void ScrollTimeline::ScheduleNextServiceInternal(bool time_check) {
if (AnimationsNeedingUpdateCount() == 0)
return;
if (time_check) {
auto state = ComputeTimelineState();
PhaseAndTime current_phase_and_time{state.phase, state.current_time};
if (current_phase_and_time == last_current_phase_and_time_)
return;
}
ScheduleServiceOnNextFrame();
}
void ScrollTimeline::ScheduleNextService() {
ScheduleNextServiceInternal(/* time_check = */ true);
}
void ScrollTimeline::SnapshotState() {
timeline_state_snapshotted_ = ComputeTimelineState();
}
Element* ScrollTimeline::scrollSource() const {
return scroll_source_.Get();
}
String ScrollTimeline::orientation() {
switch (orientation_) {
case Block:
return "block";
case Inline:
return "inline";
case Horizontal:
return "horizontal";
case Vertical:
return "vertical";
default:
NOTREACHED();
return "";
}
}
// TODO(crbug.com/1094014): scrollOffsets will replace start and end
// offsets once spec decision on multiple scroll offsets is finalized.
// https://github.com/w3c/csswg-drafts/issues/4912
void ScrollTimeline::startScrollOffset(ScrollTimelineOffsetValue& out) const {
if (StartScrollOffset()) {
out = StartScrollOffset()->ToScrollTimelineOffsetValue();
} else {
ScrollTimelineOffset scrollOffset;
out = scrollOffset.ToScrollTimelineOffsetValue();
}
}
void ScrollTimeline::endScrollOffset(ScrollTimelineOffsetValue& out) const {
if (EndScrollOffset()) {
out = EndScrollOffset()->ToScrollTimelineOffsetValue();
} else {
ScrollTimelineOffset scrollOffset;
out = scrollOffset.ToScrollTimelineOffsetValue();
}
}
const HeapVector<ScrollTimelineOffsetValue> ScrollTimeline::scrollOffsets()
const {
HeapVector<ScrollTimelineOffsetValue> scroll_offsets;
for (auto& offset : scroll_offsets_) {
scroll_offsets.push_back(offset->ToScrollTimelineOffsetValue());
// 'auto' can only be the end offset.
DCHECK(!offset->IsDefaultValue() || scroll_offsets.size() == 2);
}
return scroll_offsets;
}
void ScrollTimeline::timeRange(DoubleOrScrollTimelineAutoKeyword& result) {
// TODO(crbug.com/1140602): Support progress based animations
// We are currently abusing the intended use of the "auto" keyword. We are
// using it here as a signal to use progress based timeline instead of having
// a range based current time.
// We are doing this maintain backwards compatibility with existing tests.
if (time_range_) {
result.SetDouble(time_range_.value());
} else {
result.SetScrollTimelineAutoKeyword("auto");
}
}
void ScrollTimeline::GetCurrentAndMaxOffset(const LayoutBox* layout_box,
double& current_offset,
double& max_offset) const {
DCHECK(layout_box);
DCHECK(layout_box->GetScrollableArea());
// Depending on the writing-mode and direction, the scroll origin shifts and
// the scroll offset may be negative. The easiest way to deal with this is to
// use only the magnitude of the scroll offset, and compare it to (max_offset
// - min_offset).
PaintLayerScrollableArea* scrollable_area = layout_box->GetScrollableArea();
// Using the absolute value of the scroll offset only makes sense if either
// the max or min scroll offset for a given axis is 0. This should be
// guaranteed by the scroll origin code, but these DCHECKs ensure that.
DCHECK(scrollable_area->MaximumScrollOffset().Height() == 0 ||
scrollable_area->MinimumScrollOffset().Height() == 0);
DCHECK(scrollable_area->MaximumScrollOffset().Width() == 0 ||
scrollable_area->MinimumScrollOffset().Width() == 0);
ScrollOffset scroll_offset = scrollable_area->GetScrollOffset();
ScrollOffset scroll_dimensions = scrollable_area->MaximumScrollOffset() -
scrollable_area->MinimumScrollOffset();
auto physical_orientation =
ToPhysicalScrollOrientation(orientation_, *layout_box);
if (physical_orientation == kHorizontalScroll) {
current_offset = scroll_offset.Width();
max_offset = scroll_dimensions.Width();
} else {
current_offset = scroll_offset.Height();
max_offset = scroll_dimensions.Height();
}
// When using a rtl direction, current_offset grows correctly from 0 to
// max_offset, but is negative. Since our offsets are all just deltas along
// the orientation direction, we can just take the absolute current_offset and
// use that everywhere.
current_offset = std::abs(current_offset);
}
void ScrollTimeline::AnimationAttached(Animation* animation) {
AnimationTimeline::AnimationAttached(animation);
if (resolved_scroll_source_ && scroll_animations_.IsEmpty())
resolved_scroll_source_->RegisterScrollTimeline(this);
scroll_animations_.insert(animation);
}
void ScrollTimeline::AnimationDetached(Animation* animation) {
AnimationTimeline::AnimationDetached(animation);
scroll_animations_.erase(animation);
if (resolved_scroll_source_ && scroll_animations_.IsEmpty())
resolved_scroll_source_->UnregisterScrollTimeline(this);
}
void ScrollTimeline::WorkletAnimationAttached() {
if (!resolved_scroll_source_)
return;
GetActiveScrollTimelineSet().insert(resolved_scroll_source_);
}
void ScrollTimeline::WorkletAnimationDetached() {
if (!resolved_scroll_source_)
return;
GetActiveScrollTimelineSet().erase(resolved_scroll_source_);
}
void ScrollTimeline::Trace(Visitor* visitor) const {
visitor->Trace(scroll_animations_);
visitor->Trace(scroll_source_);
visitor->Trace(resolved_scroll_source_);
visitor->Trace(scroll_offsets_);
AnimationTimeline::Trace(visitor);
}
bool ScrollTimeline::HasActiveScrollTimeline(Node* node) {
ActiveScrollTimelineSet& worklet_animations_set =
GetActiveScrollTimelineSet();
auto worklet_animations_it = worklet_animations_set.find(node);
if (worklet_animations_it != worklet_animations_set.end() &&
worklet_animations_it->value > 0)
return true;
ScrollTimelineSet& set = GetScrollTimelineSet();
auto it = set.find(node);
if (it == set.end())
return false;
for (auto& timeline : *it->value) {
if (timeline->HasAnimations())
return true;
}
return false;
}
void ScrollTimeline::Invalidate(Node* node) {
ScrollTimelineSet& set = GetScrollTimelineSet();
auto it = set.find(node);
if (it == set.end())
return;
for (auto& timeline : *it->value) {
timeline->Invalidate();
}
}
void ScrollTimeline::InvalidateEffectTargetStyle() {
for (Animation* animation : scroll_animations_)
animation->InvalidateEffectTargetStyle();
}
void ScrollTimeline::ValidateState() {
auto state = ComputeTimelineState();
if (timeline_state_snapshotted_ == state)
return;
timeline_state_snapshotted_ = state;
InvalidateEffectTargetStyle();
}
CompositorAnimationTimeline* ScrollTimeline::EnsureCompositorTimeline() {
if (compositor_timeline_)
return compositor_timeline_.get();
compositor_timeline_ = std::make_unique<CompositorAnimationTimeline>(
scroll_timeline_util::ToCompositorScrollTimeline(this));
return compositor_timeline_.get();
}
void ScrollTimeline::UpdateCompositorTimeline() {
if (!compositor_timeline_)
return;
compositor_timeline_->UpdateCompositorTimeline(
scroll_timeline_util::GetCompositorScrollElementId(
resolved_scroll_source_),
GetResolvedScrollOffsets());
}
} // namespace blink