| // 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 |