Commit 10189a0c authored by Anders Hartvoll Ruud's avatar Anders Hartvoll Ruud Committed by Commit Bot

[scroll-animations] Handle dynamically changing @scroll-timelines

We are currently able to create new CSSAnimations linked to
CSSScrollTimelines, but we can not change the timeline of a
CSSAnimation that is already running.

There are three types of changes that can trigger a timeline change
for a running animation:

 1. When a @scroll-timeline rule is inserted, either via DOM
    mutation, or via a change in @media query evaluation.
 2. When the computed value of 'animation-timeline' changes.
 3. When the elements referenced by a @scroll-timeline rule changes,
    for example 'source:selector(#foo)' references an element with
    #foo, hence we need a new timeline if #foo is reassigned to
    point to a different element.

This CL implements "timeline change detection" in CSSAnimations,
which can discover whether a new timeline is needed or not.
This detection runs whenever there's a non-animation-style-change,
in other words, whenever something is marked for regular style
style recalc, we will check if the timeline currently associated with
an animation is up-to-date, and create a new one if needed.

In this CL, only Item 1 in the above list is fully solved. However,
since this adds the general capability to reconsider previous timeline
choices upon style recalc, it also lays the groundwork for solving
Item 2 & 3.

InertEffect makes things a bit awkward, as usual: we basically have to
predict the effect setTimeline will have on the current time of the
Animation, without actually calling setTimeline. Hence there's a
rather complicated if-statement which makes this prediction. We should
ideally try to get rid of InertEffect and do the setTimeline call
right away.

In the WPT I'm also trying to cover both the result produced by
InertEffect, _and_ the result produced by setTimeline. Hence each test
is run twice: once for the result produced the same frame the style
recalc took place, and once again after scrolling a bit.

Bug: 1074052
Change-Id: Ida36a00eb91e26b493c1e70c20dc7b23be7c9a75
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2335282
Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org>
Reviewed-by: default avatarKevin Ellis <kevers@chromium.org>
Cr-Commit-Position: refs/heads/master@{#821697}
parent 559a9f80
...@@ -793,7 +793,7 @@ void Animation::setTimeline(AnimationTimeline* timeline) { ...@@ -793,7 +793,7 @@ void Animation::setTimeline(AnimationTimeline* timeline) {
if (old_current_time) { if (old_current_time) {
reset_current_time_on_resume_ = true; reset_current_time_on_resume_ = true;
start_time_ = base::nullopt; start_time_ = base::nullopt;
hold_time_ = old_current_time.value(); SetHoldTimeAndPhase(old_current_time, TimelinePhase::kInactive);
} else if (PendingInternal()) { } else if (PendingInternal()) {
start_time_ = boundary_time; start_time_ = boundary_time;
} }
......
...@@ -221,6 +221,9 @@ class CORE_EXPORT Animation : public EventTargetWithInlineData, ...@@ -221,6 +221,9 @@ class CORE_EXPORT Animation : public EventTargetWithInlineData,
// This should only be used for CSS // This should only be used for CSS
void Unpause(); void Unpause();
bool ResetsCurrentTimeOnResume() const {
return reset_current_time_on_resume_;
}
void SetOutdated(); void SetOutdated();
bool Outdated() { return outdated_; } bool Outdated() { return outdated_; }
......
...@@ -71,6 +71,7 @@ class UpdatedCSSAnimation { ...@@ -71,6 +71,7 @@ class UpdatedCSSAnimation {
const InertEffect& effect, const InertEffect& effect,
Timing specified_timing, Timing specified_timing,
StyleRuleKeyframes* style_rule, StyleRuleKeyframes* style_rule,
AnimationTimeline* timeline,
const Vector<EAnimPlayState>& play_state_list) const Vector<EAnimPlayState>& play_state_list)
: index(index), : index(index),
animation(animation), animation(animation),
...@@ -78,12 +79,14 @@ class UpdatedCSSAnimation { ...@@ -78,12 +79,14 @@ class UpdatedCSSAnimation {
specified_timing(specified_timing), specified_timing(specified_timing),
style_rule(style_rule), style_rule(style_rule),
style_rule_version(this->style_rule->Version()), style_rule_version(this->style_rule->Version()),
timeline(timeline),
play_state_list(play_state_list) {} play_state_list(play_state_list) {}
void Trace(Visitor* visitor) const { void Trace(Visitor* visitor) const {
visitor->Trace(animation); visitor->Trace(animation);
visitor->Trace(effect); visitor->Trace(effect);
visitor->Trace(style_rule); visitor->Trace(style_rule);
visitor->Trace(timeline);
} }
wtf_size_t index; wtf_size_t index;
...@@ -92,6 +95,7 @@ class UpdatedCSSAnimation { ...@@ -92,6 +95,7 @@ class UpdatedCSSAnimation {
Timing specified_timing; Timing specified_timing;
Member<StyleRuleKeyframes> style_rule; Member<StyleRuleKeyframes> style_rule;
unsigned style_rule_version; unsigned style_rule_version;
Member<AnimationTimeline> timeline;
Vector<EAnimPlayState> play_state_list; Vector<EAnimPlayState> play_state_list;
}; };
...@@ -139,10 +143,11 @@ class CORE_EXPORT CSSAnimationUpdate final { ...@@ -139,10 +143,11 @@ class CORE_EXPORT CSSAnimationUpdate final {
const InertEffect& effect, const InertEffect& effect,
const Timing& specified_timing, const Timing& specified_timing,
StyleRuleKeyframes* style_rule, StyleRuleKeyframes* style_rule,
AnimationTimeline* timeline,
const Vector<EAnimPlayState>& play_state_list) { const Vector<EAnimPlayState>& play_state_list) {
animations_with_updates_.push_back( animations_with_updates_.push_back(
UpdatedCSSAnimation(index, animation, effect, specified_timing, UpdatedCSSAnimation(index, animation, effect, specified_timing,
style_rule, play_state_list)); style_rule, timeline, play_state_list));
suppressed_animations_.insert(animation); suppressed_animations_.insert(animation);
} }
void UpdateCompositorKeyframes(Animation* animation) { void UpdateCompositorKeyframes(Animation* animation) {
......
...@@ -432,11 +432,9 @@ AnimationTimeDelta IterationElapsedTime(const AnimationEffect& effect, ...@@ -432,11 +432,9 @@ AnimationTimeDelta IterationElapsedTime(const AnimationEffect& effect,
return iteration_duration * (iteration_boundary - iteration_start); return iteration_duration * (iteration_boundary - iteration_start);
} }
CSSScrollTimeline* CreateCSSScrollTimeline(Element* element, CSSScrollTimeline* CreateCSSScrollTimeline(
StyleRuleScrollTimeline* rule) { Element* element,
if (!rule) const CSSScrollTimeline::Options& options) {
return nullptr;
CSSScrollTimeline::Options options(element, *rule);
if (!options.IsValid()) if (!options.IsValid())
return nullptr; return nullptr;
auto* scroll_timeline = auto* scroll_timeline =
...@@ -452,7 +450,16 @@ CSSScrollTimeline* CreateCSSScrollTimeline(Element* element, ...@@ -452,7 +450,16 @@ CSSScrollTimeline* CreateCSSScrollTimeline(Element* element,
AnimationTimeline* ComputeTimeline(Element* element, AnimationTimeline* ComputeTimeline(Element* element,
const StyleNameOrKeyword& timeline_name, const StyleNameOrKeyword& timeline_name,
StyleRuleScrollTimeline* rule) { StyleRuleScrollTimeline* rule,
AnimationTimeline* existing_timeline) {
// TODO(crbug.com/1141836): Implement sticky timelines properly. For now just
// ignore animation-timeline whenever a regular (non-CSS) ScrollTimeline is
// present.
if (existing_timeline && existing_timeline->IsScrollTimeline() &&
!existing_timeline->IsCSSScrollTimeline()) {
return existing_timeline;
}
if (timeline_name.IsKeyword()) { if (timeline_name.IsKeyword()) {
if (timeline_name.GetKeyword() == CSSValueID::kAuto) if (timeline_name.GetKeyword() == CSSValueID::kAuto)
return &element->GetDocument().Timeline(); return &element->GetDocument().Timeline();
...@@ -460,7 +467,16 @@ AnimationTimeline* ComputeTimeline(Element* element, ...@@ -460,7 +467,16 @@ AnimationTimeline* ComputeTimeline(Element* element,
return nullptr; return nullptr;
} }
if (rule) { if (rule) {
if (auto* timeline = CreateCSSScrollTimeline(element, rule)) CSSScrollTimeline::Options options(element, *rule);
// When the incoming options match the existing timeline, we can continue
// to use the existing timeline, since creating a new timeline from
// the options would just yield an identical timeline.
if (auto* timeline = DynamicTo<CSSScrollTimeline>(existing_timeline)) {
if (timeline->Matches(options))
return existing_timeline;
}
if (auto* timeline = CreateCSSScrollTimeline(element, options))
return timeline; return timeline;
} }
return nullptr; return nullptr;
...@@ -713,22 +729,46 @@ void CSSAnimations::CalculateAnimationUpdate(CSSAnimationUpdate& update, ...@@ -713,22 +729,46 @@ void CSSAnimations::CalculateAnimationUpdate(CSSAnimationUpdate& update,
toggle_pause_state = true; toggle_pause_state = true;
} }
// TODO(crbug.com/1097053): Support updating timelines. bool will_be_playing =
toggle_pause_state ? animation->Paused() : animation->Playing();
AnimationTimeline* timeline = existing_animation->Timeline();
if (!is_animation_style_change) {
timeline = ComputeTimeline(&element, timeline_name,
scroll_timeline_rule, timeline);
}
if (keyframes_rule != existing_animation->style_rule || if (keyframes_rule != existing_animation->style_rule ||
keyframes_rule->Version() != keyframes_rule->Version() !=
existing_animation->style_rule_version || existing_animation->style_rule_version ||
existing_animation->specified_timing != specified_timing || existing_animation->specified_timing != specified_timing ||
is_paused != was_paused || logical_property_mapping_change) { is_paused != was_paused || logical_property_mapping_change ||
timeline != existing_animation->Timeline()) {
DCHECK(!is_animation_style_change); DCHECK(!is_animation_style_change);
base::Optional<TimelinePhase> inherited_phase;
base::Optional<double> inherited_time;
if (timeline) {
inherited_phase = base::make_optional(timeline->Phase());
inherited_time = animation->UnlimitedCurrentTime();
if (will_be_playing &&
((timeline != existing_animation->Timeline()) ||
animation->ResetsCurrentTimeOnResume())) {
if (!timeline->IsMonotonicallyIncreasing())
inherited_time = timeline->CurrentTimeSeconds();
}
}
update.UpdateAnimation( update.UpdateAnimation(
existing_animation_index, animation, existing_animation_index, animation,
*MakeGarbageCollected<InertEffect>( *MakeGarbageCollected<InertEffect>(
CreateKeyframeEffectModel(resolver, animating_element, CreateKeyframeEffectModel(resolver, animating_element,
element, &style, parent_style, name, element, &style, parent_style, name,
keyframe_timing_function.get(), i), keyframe_timing_function.get(), i),
timing, is_paused, animation->UnlimitedCurrentTime(), timing, is_paused, inherited_time, inherited_phase),
base::nullopt), specified_timing, keyframes_rule, timeline,
specified_timing, keyframes_rule,
animation_data->PlayStateList()); animation_data->PlayStateList());
if (toggle_pause_state) if (toggle_pause_state)
update.ToggleAnimationIndexPaused(existing_animation_index); update.ToggleAnimationIndexPaused(existing_animation_index);
...@@ -736,7 +776,8 @@ void CSSAnimations::CalculateAnimationUpdate(CSSAnimationUpdate& update, ...@@ -736,7 +776,8 @@ void CSSAnimations::CalculateAnimationUpdate(CSSAnimationUpdate& update,
} else { } else {
DCHECK(!is_animation_style_change); DCHECK(!is_animation_style_change);
AnimationTimeline* timeline = AnimationTimeline* timeline =
ComputeTimeline(&element, timeline_name, scroll_timeline_rule); ComputeTimeline(&element, timeline_name, scroll_timeline_rule,
nullptr /* existing_timeline */);
base::Optional<TimelinePhase> inherited_phase; base::Optional<TimelinePhase> inherited_phase;
base::Optional<double> inherited_time; base::Optional<double> inherited_time;
if (timeline) { if (timeline) {
...@@ -868,6 +909,8 @@ void CSSAnimations::MaybeApplyPendingUpdate(Element* element) { ...@@ -868,6 +909,8 @@ void CSSAnimations::MaybeApplyPendingUpdate(Element* element) {
effect->SetModel(entry.effect->Model()); effect->SetModel(entry.effect->Model());
effect->UpdateSpecifiedTiming(entry.effect->SpecifiedTiming()); effect->UpdateSpecifiedTiming(entry.effect->SpecifiedTiming());
} }
if (entry.animation->timeline() != entry.timeline)
entry.animation->setTimeline(entry.timeline);
running_animations_[entry.index]->Update(entry); running_animations_[entry.index]->Update(entry);
} }
......
...@@ -129,6 +129,8 @@ class CORE_EXPORT CSSAnimations final { ...@@ -129,6 +129,8 @@ class CORE_EXPORT CSSAnimations final {
style_rule_version(new_animation.style_rule_version), style_rule_version(new_animation.style_rule_version),
play_state_list(new_animation.play_state_list) {} play_state_list(new_animation.play_state_list) {}
AnimationTimeline* Timeline() const { return animation->timeline(); }
void Update(UpdatedCSSAnimation update) { void Update(UpdatedCSSAnimation update) {
DCHECK_EQ(update.animation, animation); DCHECK_EQ(update.animation, animation);
style_rule = update.style_rule; style_rule = update.style_rule;
......
...@@ -165,4 +165,12 @@ CSSScrollTimeline::CSSScrollTimeline(Document* document, const Options& options) ...@@ -165,4 +165,12 @@ CSSScrollTimeline::CSSScrollTimeline(Document* document, const Options& options)
DCHECK(options.IsValid()); DCHECK(options.IsValid());
} }
bool CSSScrollTimeline::Matches(const Options& options) const {
DCHECK(options.offsets_);
return (scrollSource() == options.source_) &&
(GetOrientation() == options.direction_) &&
(ScrollOffsetsEqual(*options.offsets_)) &&
(GetTimeRange() == options.time_range_);
}
} // namespace blink } // namespace blink
...@@ -38,6 +38,8 @@ class CORE_EXPORT CSSScrollTimeline : public ScrollTimeline { ...@@ -38,6 +38,8 @@ class CORE_EXPORT CSSScrollTimeline : public ScrollTimeline {
CSSScrollTimeline(Document*, const Options&); CSSScrollTimeline(Document*, const Options&);
bool Matches(const Options&) const;
// AnimationTimeline implementation. // AnimationTimeline implementation.
bool IsCSSScrollTimeline() const override { return true; } bool IsCSSScrollTimeline() const override { return true; }
}; };
......
...@@ -186,6 +186,7 @@ ScrollTimeline::ScrollTimeline( ...@@ -186,6 +186,7 @@ ScrollTimeline::ScrollTimeline(
orientation_(orientation), orientation_(orientation),
scroll_offsets_(scroll_offsets), scroll_offsets_(scroll_offsets),
time_range_(time_range) { time_range_(time_range) {
DCHECK(scroll_offsets_);
if (resolved_scroll_source_) { if (resolved_scroll_source_) {
ScrollTimelineSet& set = GetScrollTimelineSet(); ScrollTimelineSet& set = GetScrollTimelineSet();
if (!set.Contains(resolved_scroll_source_)) { if (!set.Contains(resolved_scroll_source_)) {
...@@ -279,6 +280,19 @@ AnimationTimeline::PhaseAndTime ScrollTimeline::CurrentPhaseAndTime() { ...@@ -279,6 +280,19 @@ AnimationTimeline::PhaseAndTime ScrollTimeline::CurrentPhaseAndTime() {
timeline_state_snapshotted_.current_time}; timeline_state_snapshotted_.current_time};
} }
bool ScrollTimeline::ScrollOffsetsEqual(
const HeapVector<Member<ScrollTimelineOffset>>& other) const {
DCHECK(scroll_offsets_);
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;
}
ScrollTimeline::TimelineState ScrollTimeline::ComputeTimelineState() const { 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
...@@ -380,7 +394,7 @@ void ScrollTimeline::SnapshotState() { ...@@ -380,7 +394,7 @@ void ScrollTimeline::SnapshotState() {
timeline_state_snapshotted_ = ComputeTimelineState(); timeline_state_snapshotted_ = ComputeTimelineState();
} }
Element* ScrollTimeline::scrollSource() { Element* ScrollTimeline::scrollSource() const {
return scroll_source_.Get(); return scroll_source_.Get();
} }
......
...@@ -60,7 +60,7 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -60,7 +60,7 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline {
void ScheduleNextService() override; void ScheduleNextService() override;
// IDL API implementation. // IDL API implementation.
Element* scrollSource(); Element* scrollSource() const;
String orientation(); String orientation();
// TODO(crbug.com/1094014): scrollOffsets will replace start and end // TODO(crbug.com/1094014): scrollOffsets will replace start and end
// offsets once spec decision on multiple scroll offsets is finalized. // offsets once spec decision on multiple scroll offsets is finalized.
...@@ -115,6 +115,9 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline { ...@@ -115,6 +115,9 @@ class CORE_EXPORT ScrollTimeline : public AnimationTimeline {
protected: protected:
PhaseAndTime CurrentPhaseAndTime() override; PhaseAndTime CurrentPhaseAndTime() override;
double GetTimeRange() const { return time_range_; }
bool ScrollOffsetsEqual(
const HeapVector<Member<ScrollTimelineOffset>>& other) const;
private: private:
// https://wicg.github.io/scroll-animations/#avoiding-cycles // https://wicg.github.io/scroll-animations/#avoiding-cycles
......
...@@ -34,6 +34,7 @@ const char kPlatformColorChange[] = "PlatformColorChange"; ...@@ -34,6 +34,7 @@ const char kPlatformColorChange[] = "PlatformColorChange";
const char kPluginChanged[] = "Plugin Changed"; const char kPluginChanged[] = "Plugin Changed";
const char kPropertyRegistration[] = "PropertyRegistration"; const char kPropertyRegistration[] = "PropertyRegistration";
const char kPseudoClass[] = "PseudoClass"; const char kPseudoClass[] = "PseudoClass";
const char kScrollTimeline[] = "ScrollTimeline";
const char kSVGContainerSizeChange[] = "SVGContainerSizeChange"; const char kSVGContainerSizeChange[] = "SVGContainerSizeChange";
const char kSettings[] = "Settings"; const char kSettings[] = "Settings";
const char kShadow[] = "Shadow"; const char kShadow[] = "Shadow";
......
...@@ -35,6 +35,7 @@ extern const char kPlatformColorChange[]; ...@@ -35,6 +35,7 @@ extern const char kPlatformColorChange[];
extern const char kPluginChanged[]; extern const char kPluginChanged[];
extern const char kPropertyRegistration[]; extern const char kPropertyRegistration[];
extern const char kPseudoClass[]; extern const char kPseudoClass[];
extern const char kScrollTimeline[];
extern const char kSVGContainerSizeChange[]; extern const char kSVGContainerSizeChange[];
extern const char kSettings[]; extern const char kSettings[];
extern const char kShadow[]; extern const char kShadow[];
......
...@@ -1676,6 +1676,9 @@ void StyleEngine::ApplyRuleSetChanges( ...@@ -1676,6 +1676,9 @@ void StyleEngine::ApplyRuleSetChanges(
if (RuleSet* rule_set = active_sheet.second) if (RuleSet* rule_set = active_sheet.second)
AddScrollTimelineRules(*rule_set); AddScrollTimelineRules(*rule_set);
} }
MarkAllElementsForStyleRecalc(StyleChangeReasonForTracing::Create(
style_change_reason::kScrollTimeline));
} }
} }
......
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