[go: nahoru, domu]

Reland "Initial implementation of cc::ScrollTimeline"

This is a reland of 31974d24b5de701ff9c290b32e4dd543abec8146
Original change's description:
> Initial implementation of cc::ScrollTimeline
> 
> This creates a compositor side concept of a ScrollTimeline, and adds
> plumbing so that blink WorkletAnimations created with ScrollTimelines
> will create a cc::ScrollTimeline on the compositor side. This means
> that scroll-driven AnimationWorklets are now possible.
> 
> There are a number of caveats to this initial implementation:
>  * We never update the cc::ScrollTimeline, so if the scroller
>    changes on the blink side we won't know.
>  * We assume that the scroller is composited. If it isn't composited
>    during animation play() we will log a warning and fail to start,
>    but if it is composited at that time and later becomes non-composited
>    we will likely crash.
> 
> Bug: 776952
> 
> Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel
> Change-Id: Id0342c62209b387130a69edca4ff3682748bd8a3
> Reviewed-on: https://chromium-review.googlesource.com/738437
> Commit-Queue: Stephen McGruer <smcgruer@chromium.org>
> Reviewed-by: Robert Flack <flackr@chromium.org>
> Reviewed-by: dstockwell <dstockwell@chromium.org>
> Reviewed-by: Majid Valipour <majidvp@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#515645}

Bug: 776952
Change-Id: I372d8fd48c62a63b5c6b10688d3829e4ac2c3be1
Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel
Reviewed-on: https://chromium-review.googlesource.com/764578
Reviewed-by: Robert Flack <flackr@chromium.org>
Reviewed-by: Eric Willigers <ericwilligers@chromium.org>
Reviewed-by: dstockwell <dstockwell@chromium.org>
Commit-Queue: Stephen McGruer <smcgruer@chromium.org>
Cr-Commit-Position: refs/heads/master@{#515985}
diff --git a/cc/BUILD.gn b/cc/BUILD.gn
index 6c78ebe6..83dba38 100644
--- a/cc/BUILD.gn
+++ b/cc/BUILD.gn
@@ -690,6 +690,7 @@
     "animation/element_animations_unittest.cc",
     "animation/keyframed_animation_curve_unittest.cc",
     "animation/scroll_offset_animation_curve_unittest.cc",
+    "animation/scroll_timeline_unittest.cc",
     "animation/transform_operations_unittest.cc",
     "animation/worklet_animation_player_unittest.cc",
 
diff --git a/cc/animation/BUILD.gn b/cc/animation/BUILD.gn
index d8b4fe0..16fe4a33 100644
--- a/cc/animation/BUILD.gn
+++ b/cc/animation/BUILD.gn
@@ -36,6 +36,8 @@
     "scroll_offset_animations.h",
     "scroll_offset_animations_impl.cc",
     "scroll_offset_animations_impl.h",
+    "scroll_timeline.cc",
+    "scroll_timeline.h",
     "timing_function.cc",
     "timing_function.h",
     "transform_operation.cc",
diff --git a/cc/animation/DEPS b/cc/animation/DEPS
index e4afe36..a59045d 100644
--- a/cc/animation/DEPS
+++ b/cc/animation/DEPS
@@ -10,5 +10,7 @@
   "+cc/trees/mutator_host.h",
   "+cc/trees/mutator_host_client.h",
   "+cc/trees/property_animation_state.h",
+  "+cc/trees/property_tree.h",
+  "+cc/trees/scroll_node.h",
   "+cc/trees/target_property.h",
 ]
diff --git a/cc/animation/animation_host.cc b/cc/animation/animation_host.cc
index 405b2ab5..debdf30 100644
--- a/cc/animation/animation_host.cc
+++ b/cc/animation/animation_host.cc
@@ -20,6 +20,7 @@
 #include "cc/animation/scroll_offset_animation_curve.h"
 #include "cc/animation/scroll_offset_animations.h"
 #include "cc/animation/scroll_offset_animations_impl.h"
+#include "cc/animation/scroll_timeline.h"
 #include "cc/animation/timing_function.h"
 #include "cc/animation/worklet_animation_player.h"
 #include "ui/gfx/geometry/box_f.h"
@@ -292,7 +293,8 @@
   return true;
 }
 
-bool AnimationHost::TickAnimations(base::TimeTicks monotonic_time) {
+bool AnimationHost::TickAnimations(base::TimeTicks monotonic_time,
+                                   const ScrollTree& scroll_tree) {
   TRACE_EVENT0("cc", "AnimationHost::TickAnimations");
   bool did_animate = false;
 
@@ -308,14 +310,15 @@
     // trees similar to other animations. However our final goal is to only call
     // it once, ideally after activation, and only when the input
     // to an active timeline has changed. http://crbug.com/767210
-    mutator_->Mutate(CollectAnimatorsState(monotonic_time));
+    mutator_->Mutate(CollectAnimatorsState(monotonic_time, scroll_tree));
     did_animate = true;
   }
 
   return did_animate;
 }
 
-void AnimationHost::TickScrollAnimations(base::TimeTicks monotonic_time) {
+void AnimationHost::TickScrollAnimations(base::TimeTicks monotonic_time,
+                                         const ScrollTree& scroll_tree) {
   // TODO(majidvp) For now the logic simply assumes all AnimationWorklet
   // animations depend on scroll offset but this is inefficient. We need a more
   // fine-grained approach based on invalidating individual ScrollTimelines and
@@ -325,11 +328,12 @@
   // TODO(majidvp): We need to return a boolean here so that LTHI knows
   // whether it needs to schedule another frame.
   if (mutator_)
-    mutator_->Mutate(CollectAnimatorsState(monotonic_time));
+    mutator_->Mutate(CollectAnimatorsState(monotonic_time, scroll_tree));
 }
 
 std::unique_ptr<MutatorInputState> AnimationHost::CollectAnimatorsState(
-    base::TimeTicks timeline_time) {
+    base::TimeTicks monotonic_time,
+    const ScrollTree& scroll_tree) {
   TRACE_EVENT0("cc", "AnimationHost::CollectAnimatorsState");
   std::unique_ptr<MutatorInputState> result =
       base::MakeUnique<MutatorInputState>();
@@ -340,10 +344,9 @@
 
     WorkletAnimationPlayer* worklet_player =
         static_cast<WorkletAnimationPlayer*>(player.get());
-    // TODO(majidvp): Do not assume timeline time to be the worklet player's
-    // current time but instead ask the player to provide it.
     MutatorInputState::AnimationState state{
-        worklet_player->id(), worklet_player->name(), timeline_time};
+        worklet_player->id(), worklet_player->name(),
+        worklet_player->CurrentTime(monotonic_time, scroll_tree)};
 
     result->animations.push_back(std::move(state));
   }
diff --git a/cc/animation/animation_host.h b/cc/animation/animation_host.h
index 2b5f01c..cfd8416 100644
--- a/cc/animation/animation_host.h
+++ b/cc/animation/animation_host.h
@@ -100,8 +100,10 @@
   bool NeedsTickAnimations() const override;
 
   bool ActivateAnimations() override;
-  bool TickAnimations(base::TimeTicks monotonic_time) override;
-  void TickScrollAnimations(base::TimeTicks monotonic_time) override;
+  bool TickAnimations(base::TimeTicks monotonic_time,
+                      const ScrollTree& scroll_tree) override;
+  void TickScrollAnimations(base::TimeTicks monotonic_time,
+                            const ScrollTree& scroll_tree) override;
   bool UpdateAnimationState(bool start_ready_animations,
                             MutatorEvents* events) override;
 
@@ -198,7 +200,8 @@
 
   // Return the animator state representing all ticking worklet animations.
   std::unique_ptr<MutatorInputState> CollectAnimatorsState(
-      base::TimeTicks timeline_time);
+      base::TimeTicks timeline_time,
+      const ScrollTree& scroll_tree);
 
   ElementToAnimationsMap element_to_animations_map_;
   PlayersList ticking_players_;
diff --git a/cc/animation/scroll_timeline.cc b/cc/animation/scroll_timeline.cc
new file mode 100644
index 0000000..a880877
--- /dev/null
+++ b/cc/animation/scroll_timeline.cc
@@ -0,0 +1,60 @@
+// 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 "cc/animation/scroll_timeline.h"
+
+#include "cc/trees/property_tree.h"
+#include "cc/trees/scroll_node.h"
+#include "ui/gfx/geometry/scroll_offset.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace cc {
+
+ScrollTimeline::ScrollTimeline(ElementId scroller_id,
+                               ScrollDirection orientation,
+                               double time_range)
+    : scroller_id_(scroller_id),
+      orientation_(orientation),
+      time_range_(time_range) {}
+
+std::unique_ptr<ScrollTimeline> ScrollTimeline::CreateImplInstance() const {
+  return std::make_unique<ScrollTimeline>(scroller_id_, orientation_,
+                                          time_range_);
+}
+
+double ScrollTimeline::CurrentTime(const ScrollTree& scroll_tree) const {
+  // If the scroller isn't in the ScrollTree, the element either no longer
+  // exists or is not currently scrollable. By the spec, return an unresolved
+  // time value.
+  if (!scroll_tree.FindNodeFromElementId(scroller_id_))
+    return std::numeric_limits<double>::quiet_NaN();
+
+  gfx::ScrollOffset offset = scroll_tree.current_scroll_offset(scroller_id_);
+  DCHECK_GE(offset.x(), 0);
+  DCHECK_GE(offset.y(), 0);
+
+  gfx::ScrollOffset scroll_dimensions = scroll_tree.MaxScrollOffset(
+      scroll_tree.FindNodeFromElementId(scroller_id_)->id);
+
+  double current_offset = (orientation_ == Vertical) ? offset.y() : offset.x();
+  double max_offset = (orientation_ == Vertical) ? scroll_dimensions.y()
+                                                 : scroll_dimensions.x();
+
+  // 3. If current scroll offset is less than startScrollOffset, return an
+  // unresolved time value if fill is none or forwards, or 0 otherwise.
+  // TODO(smcgruer): Implement |startScrollOffset| and |fill|.
+
+  // 4. If current scroll offset is greater than or equal to endScrollOffset,
+  // return an unresolved time value if fill is none or backwards, or the
+  // effective time range otherwise.
+  // TODO(smcgruer): Implement |endScrollOffset| and |fill|.
+
+  // 5. Return the result of evaluating the following expression:
+  //   ((current scroll offset - startScrollOffset) /
+  //      (endScrollOffset - startScrollOffset)) * effective time range
+
+  return (std::abs(current_offset) / max_offset) * time_range_;
+}
+
+}  // namespace cc
diff --git a/cc/animation/scroll_timeline.h b/cc/animation/scroll_timeline.h
new file mode 100644
index 0000000..7389d4df
--- /dev/null
+++ b/cc/animation/scroll_timeline.h
@@ -0,0 +1,61 @@
+// 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.
+
+#ifndef CC_ANIMATION_SCROLL_TIMELINE_H_
+#define CC_ANIMATION_SCROLL_TIMELINE_H_
+
+#include "cc/animation/animation_export.h"
+#include "cc/trees/element_id.h"
+
+namespace cc {
+
+class ScrollTree;
+
+// A ScrollTimeline is an animation timeline that bases its current time on the
+// progress of scrolling in some scroll container.
+//
+// This is the compositor-side representation of the web concept expressed in
+// https://wicg.github.io/scroll-animations/#scrolltimeline-interface. There are
+// differences between this class and the web definition of a ScrollTimeline.
+// For example the compositor does not know (or care) about 'writing modes', so
+// this class only tracks whether the ScrollTimeline orientation is horizontal
+// or vertical. Blink is expected to resolve any such 'web' requirements and
+// construct/update the compositor ScrollTimeline accordingly.
+class CC_ANIMATION_EXPORT ScrollTimeline {
+ public:
+  enum ScrollDirection { Horizontal, Vertical };
+
+  ScrollTimeline(ElementId scroller_id,
+                 ScrollDirection orientation,
+                 double time_range);
+  virtual ~ScrollTimeline() {}
+
+  // Create a copy of this ScrollTimeline intended for the impl thread in the
+  // compositor.
+  std::unique_ptr<ScrollTimeline> CreateImplInstance() const;
+
+  // Calculate the current time of the ScrollTimeline. This is either a double
+  // value or std::numeric_limits<double>::quiet_NaN() if the current time is
+  // unresolved.
+  virtual double CurrentTime(const ScrollTree& scroll_tree) const;
+
+ private:
+  // The scroller which this ScrollTimeline is based on. It is expected that
+  // this scroller will exist in the scroll property tree, or otherwise calling
+  // CurrentTime will fail.
+  ElementId scroller_id_;
+
+  // The orientation of the ScrollTimeline indicates which axis of the scroller
+  // it should base its current time on - either the horizontal or vertical.
+  ScrollDirection orientation_;
+
+  // A ScrollTimeline maps from the scroll offset in the scroller to a time
+  // value based on a 'time range'. See the implementation of CurrentTime or the
+  // spec for details.
+  double time_range_;
+};
+
+}  // namespace cc
+
+#endif  // CC_ANIMATION_SCROLL_TIMELINE_H_
diff --git a/cc/animation/scroll_timeline_unittest.cc b/cc/animation/scroll_timeline_unittest.cc
new file mode 100644
index 0000000..4b410137
--- /dev/null
+++ b/cc/animation/scroll_timeline_unittest.cc
@@ -0,0 +1,81 @@
+// 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 "cc/animation/scroll_timeline.h"
+#include "cc/trees/property_tree.h"
+#include "cc/trees/scroll_node.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/geometry/scroll_offset.h"
+
+namespace cc {
+
+class ScrollTimelineTest : public ::testing::Test {
+ public:
+  ScrollTimelineTest()
+      : scroller_id_(1), container_size_(100, 100), content_size_(500, 500) {
+    // For simplicity we make the property_tree main thread; this avoids the
+    // need to deal with the synced scroll offset code.
+    property_trees_.is_main_thread = true;
+    property_trees_.is_active = true;
+
+    // We add a single node that is scrolling a 550x1100 contents inside a
+    // 50x100 container.
+    ScrollNode node;
+    node.scrollable = true;
+    node.bounds = content_size_;
+    node.container_bounds = container_size_;
+
+    int node_id = property_trees_.scroll_tree.Insert(node, 0);
+    property_trees_.element_id_to_scroll_node_index[scroller_id_] = node_id;
+  }
+
+  ScrollTree& scroll_tree() { return property_trees_.scroll_tree; }
+  ElementId scroller_id() const { return scroller_id_; }
+  gfx::Size container_size() const { return container_size_; }
+  gfx::Size content_size() const { return content_size_; }
+
+ private:
+  PropertyTrees property_trees_;
+  ElementId scroller_id_;
+  gfx::Size container_size_;
+  gfx::Size content_size_;
+};
+
+TEST_F(ScrollTimelineTest, BasicCurrentTimeCalculations) {
+  // For simplicity, we set the time range such that the current time maps
+  // directly to the scroll offset. We have a square scroller/contents, so can
+  // just compute one edge and use it for vertical/horizontal.
+  double time_range = content_size().height() - container_size().height();
+
+  ScrollTimeline vertical_timeline(scroller_id(), ScrollTimeline::Vertical,
+                                   time_range);
+  ScrollTimeline horizontal_timeline(scroller_id(), ScrollTimeline::Horizontal,
+                                     time_range);
+
+  // Unscrolled, both timelines should read a current time of 0.
+  scroll_tree().SetScrollOffset(scroller_id(), gfx::ScrollOffset());
+  EXPECT_FLOAT_EQ(0, vertical_timeline.CurrentTime(scroll_tree()));
+  EXPECT_FLOAT_EQ(0, horizontal_timeline.CurrentTime(scroll_tree()));
+
+  // Now do some scrolling and make sure that the ScrollTimelines update.
+  scroll_tree().SetScrollOffset(scroller_id(), gfx::ScrollOffset(75, 50));
+
+  // As noted above, we have mapped the time range such that current time should
+  // just be the scroll offset.
+  EXPECT_FLOAT_EQ(50, vertical_timeline.CurrentTime(scroll_tree()));
+  EXPECT_FLOAT_EQ(75, horizontal_timeline.CurrentTime(scroll_tree()));
+}
+
+TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForTimeRange) {
+  // Here we set a time range to 100, which gives the current time the form of
+  // 'percentage scrolled'.
+  ScrollTimeline timeline(scroller_id(), ScrollTimeline::Vertical, 100);
+
+  double halfwayY = (content_size().height() - container_size().height()) / 2.;
+  scroll_tree().SetScrollOffset(scroller_id(), gfx::ScrollOffset(0, halfwayY));
+
+  EXPECT_FLOAT_EQ(50, timeline.CurrentTime(scroll_tree()));
+}
+
+}  // namespace cc
diff --git a/cc/animation/worklet_animation_player.cc b/cc/animation/worklet_animation_player.cc
index 95ed7405..ce7454c 100644
--- a/cc/animation/worklet_animation_player.cc
+++ b/cc/animation/worklet_animation_player.cc
@@ -5,23 +5,36 @@
 #include "cc/animation/worklet_animation_player.h"
 
 #include "base/memory/ptr_util.h"
+#include "cc/animation/scroll_timeline.h"
 
 namespace cc {
 
-WorkletAnimationPlayer::WorkletAnimationPlayer(int id, const std::string& name)
-    : AnimationPlayer(id), name_(name) {}
+WorkletAnimationPlayer::WorkletAnimationPlayer(
+    int id,
+    const std::string& name,
+    std::unique_ptr<ScrollTimeline> scroll_timeline)
+    : AnimationPlayer(id),
+      name_(name),
+      scroll_timeline_(std::move(scroll_timeline)) {}
 
 WorkletAnimationPlayer::~WorkletAnimationPlayer() {}
 
 scoped_refptr<WorkletAnimationPlayer> WorkletAnimationPlayer::Create(
     int id,
-    const std::string& name) {
-  return WrapRefCounted(new WorkletAnimationPlayer(id, name));
+    const std::string& name,
+    std::unique_ptr<ScrollTimeline> scroll_timeline) {
+  return WrapRefCounted(
+      new WorkletAnimationPlayer(id, name, std::move(scroll_timeline)));
 }
 
 scoped_refptr<AnimationPlayer> WorkletAnimationPlayer::CreateImplInstance()
     const {
-  return WrapRefCounted(new WorkletAnimationPlayer(id(), name()));
+  std::unique_ptr<ScrollTimeline> impl_timeline;
+  if (scroll_timeline_)
+    impl_timeline = scroll_timeline_->CreateImplInstance();
+
+  return WrapRefCounted(
+      new WorkletAnimationPlayer(id(), name(), std::move(impl_timeline)));
 }
 
 void WorkletAnimationPlayer::SetLocalTime(base::TimeDelta local_time) {
@@ -33,6 +46,19 @@
   animation_ticker_->Tick(monotonic_time, this);
 }
 
+// TODO(crbug.com/780151): The current time returned should be an offset against
+// the animation's start time and based on the playback rate, not just the
+// timeline time directly.
+double WorkletAnimationPlayer::CurrentTime(base::TimeTicks monotonic_time,
+                                           const ScrollTree& scroll_tree) {
+  if (scroll_timeline_) {
+    return scroll_timeline_->CurrentTime(scroll_tree);
+  }
+
+  // TODO(crbug.com/783333): Support DocumentTimeline's originTime concept.
+  return (monotonic_time - base::TimeTicks()).InMillisecondsF();
+}
+
 base::TimeTicks WorkletAnimationPlayer::GetTimeForAnimation(
     const Animation& animation) const {
   // Animation player local time is equivalent to animation active time. So
diff --git a/cc/animation/worklet_animation_player.h b/cc/animation/worklet_animation_player.h
index 33c5641..6859e1f5 100644
--- a/cc/animation/worklet_animation_player.h
+++ b/cc/animation/worklet_animation_player.h
@@ -12,6 +12,8 @@
 
 namespace cc {
 
+class ScrollTimeline;
+
 // A WorkletAnimationPlayer is an animations player that allows its animation
 // timing to be controlled by an animator instance that is running in a
 // AnimationWorkletGlobalScope.
@@ -19,17 +21,30 @@
     : public AnimationPlayer,
       AnimationTicker::AnimationTimeProvider {
  public:
-  WorkletAnimationPlayer(int id, const std::string& name);
-  static scoped_refptr<WorkletAnimationPlayer> Create(int id,
-                                                      const std::string& name);
+  WorkletAnimationPlayer(int id,
+                         const std::string& name,
+                         std::unique_ptr<ScrollTimeline> scroll_timeline);
+  static scoped_refptr<WorkletAnimationPlayer> Create(
+      int id,
+      const std::string& name,
+      std::unique_ptr<ScrollTimeline> scroll_timeline);
   scoped_refptr<AnimationPlayer> CreateImplInstance() const override;
 
   const std::string& name() const { return name_; }
+  const ScrollTimeline* scroll_timeline() const {
+    return scroll_timeline_.get();
+  }
+
   void SetLocalTime(base::TimeDelta local_time);
   bool IsWorkletAnimationPlayer() const override;
 
   void Tick(base::TimeTicks monotonic_time) override;
 
+  // Returns the current time to be passed into the underlying AnimationWorklet.
+  // The current time is based on the timeline associated with the animation.
+  double CurrentTime(base::TimeTicks monotonic_time,
+                     const ScrollTree& scroll_tree);
+
   // AnimationTicker::AnimationTimeProvider:
   base::TimeTicks GetTimeForAnimation(
       const Animation& animation) const override;
@@ -40,6 +55,15 @@
   ~WorkletAnimationPlayer() override;
 
   std::string name_;
+
+  // The ScrollTimeline associated with the underlying animation. If null, the
+  // animation is based on a DocumentTimeline.
+  //
+  // TODO(crbug.com/780148): A WorkletAnimationPlayer should own an
+  // AnimationTimeline which must exist but can either be a DocumentTimeline,
+  // ScrollTimeline, or some other future implementation.
+  std::unique_ptr<ScrollTimeline> scroll_timeline_;
+
   base::TimeDelta local_time_;
 };
 
diff --git a/cc/animation/worklet_animation_player_unittest.cc b/cc/animation/worklet_animation_player_unittest.cc
index 940f2df0..100d9b8 100644
--- a/cc/animation/worklet_animation_player_unittest.cc
+++ b/cc/animation/worklet_animation_player_unittest.cc
@@ -1,11 +1,14 @@
 // 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 "cc/animation/worklet_animation_player.h"
 
+#include "cc/animation/scroll_timeline.h"
 #include "cc/test/animation_test_common.h"
 #include "cc/test/animation_timelines_test_common.h"
 #include "cc/test/mock_layer_tree_mutator.h"
+#include "cc/trees/property_tree.h"
 #include "testing/gmock/include/gmock/gmock.h"
 
 using ::testing::Mock;
@@ -24,6 +27,13 @@
   int worklet_player_id_ = 11;
 };
 
+class MockScrollTimeline : public ScrollTimeline {
+ public:
+  MockScrollTimeline()
+      : ScrollTimeline(ElementId(), ScrollTimeline::Vertical, 0) {}
+  MOCK_CONST_METHOD1(CurrentTime, double(const ScrollTree&));
+};
+
 TEST_F(WorkletAnimationPlayerTest, LocalTimeIsUsedWithAnimations) {
   client_.RegisterElement(element_id_, ElementListType::ACTIVE);
   client_impl_.RegisterElement(element_id_, ElementListType::PENDING);
@@ -37,7 +47,7 @@
       start_opacity + (end_opacity - start_opacity) / 2;
 
   scoped_refptr<WorkletAnimationPlayer> worklet_player_ =
-      WorkletAnimationPlayer::Create(worklet_player_id_, "test_name");
+      WorkletAnimationPlayer::Create(worklet_player_id_, "test_name", nullptr);
 
   worklet_player_->AttachElement(element_id_);
   host_->AddAnimationTimeline(timeline_);
@@ -88,7 +98,7 @@
   const double duration = 1.;
 
   scoped_refptr<WorkletAnimationPlayer> worklet_player_ =
-      WorkletAnimationPlayer::Create(worklet_player_id_, "test_name");
+      WorkletAnimationPlayer::Create(worklet_player_id_, "test_name", nullptr);
 
   worklet_player_->AttachElement(element_id_);
   host_->AddAnimationTimeline(timeline_);
@@ -109,6 +119,18 @@
   Mock::VerifyAndClearExpectations(mock_mutator);
 }
 
+TEST_F(WorkletAnimationPlayerTest, CurrentTimeCorrectlyUsesScrolltimeline) {
+  auto scroll_timeline = std::make_unique<MockScrollTimeline>();
+  EXPECT_CALL(*scroll_timeline, CurrentTime(_)).WillOnce(Return(1234));
+  scoped_refptr<WorkletAnimationPlayer> worklet_player =
+      WorkletAnimationPlayer::Create(worklet_player_id_, "test_name",
+                                     std::move(scroll_timeline));
+
+  ScrollTree scroll_tree;
+  EXPECT_EQ(1234,
+            worklet_player->CurrentTime(base::TimeTicks::Now(), scroll_tree));
+}
+
 }  // namespace
 
 }  // namespace cc
diff --git a/cc/test/animation_timelines_test_common.cc b/cc/test/animation_timelines_test_common.cc
index 26f203d..f0a483c9 100644
--- a/cc/test/animation_timelines_test_common.cc
+++ b/cc/test/animation_timelines_test_common.cc
@@ -13,6 +13,7 @@
 #include "cc/animation/element_animations.h"
 #include "cc/base/filter_operation.h"
 #include "cc/base/filter_operations.h"
+#include "cc/trees/property_tree.h"
 #include "ui/gfx/transform.h"
 
 namespace cc {
@@ -430,13 +431,15 @@
     unsigned expect_events) {
   std::unique_ptr<MutatorEvents> events = host_->CreateEvents();
 
-  host_impl_->TickAnimations(time);
+  // TODO(smcgruer): Construct a proper ScrollTree for the tests.
+  ScrollTree scroll_tree;
+  host_impl_->TickAnimations(time, scroll_tree);
   host_impl_->UpdateAnimationState(true, events.get());
 
   auto* animation_events = static_cast<const AnimationEvents*>(events.get());
   EXPECT_EQ(expect_events, animation_events->events_.size());
 
-  host_->TickAnimations(time);
+  host_->TickAnimations(time, scroll_tree);
   host_->UpdateAnimationState(true, nullptr);
   host_->SetAnimationEvents(std::move(events));
 }
diff --git a/cc/test/layer_tree_test.cc b/cc/test/layer_tree_test.cc
index e22d27e..c6c8332 100644
--- a/cc/test/layer_tree_test.cc
+++ b/cc/test/layer_tree_test.cc
@@ -252,9 +252,11 @@
     test_hooks_->DidSetVisibleOnImplTree(this, visible);
   }
 
-  bool AnimateLayers(base::TimeTicks monotonic_time) override {
+  bool AnimateLayers(base::TimeTicks monotonic_time,
+                     bool is_active_tree) override {
     test_hooks_->WillAnimateLayers(this, monotonic_time);
-    bool result = LayerTreeHostImpl::AnimateLayers(monotonic_time);
+    bool result =
+        LayerTreeHostImpl::AnimateLayers(monotonic_time, is_active_tree);
     test_hooks_->AnimateLayers(this, monotonic_time);
     return result;
   }
diff --git a/cc/trees/layer_tree_host.cc b/cc/trees/layer_tree_host.cc
index a93bc46..202d2b8 100644
--- a/cc/trees/layer_tree_host.cc
+++ b/cc/trees/layer_tree_host.cc
@@ -893,7 +893,8 @@
 void LayerTreeHost::AnimateLayers(base::TimeTicks monotonic_time) {
   std::unique_ptr<MutatorEvents> events = mutator_host_->CreateEvents();
 
-  if (mutator_host_->TickAnimations(monotonic_time))
+  if (mutator_host_->TickAnimations(monotonic_time,
+                                    property_trees()->scroll_tree))
     mutator_host_->UpdateAnimationState(true, events.get());
 
   if (!events->IsEmpty())
diff --git a/cc/trees/layer_tree_host_impl.cc b/cc/trees/layer_tree_host_impl.cc
index 560cab7..2535028 100644
--- a/cc/trees/layer_tree_host_impl.cc
+++ b/cc/trees/layer_tree_host_impl.cc
@@ -503,7 +503,7 @@
   }
 
   did_animate |= AnimatePageScale(monotonic_time);
-  did_animate |= AnimateLayers(monotonic_time);
+  did_animate |= AnimateLayers(monotonic_time, active_tree);
   did_animate |= AnimateScrollbars(monotonic_time);
   did_animate |= AnimateBrowserControls(monotonic_time);
 
@@ -3692,7 +3692,9 @@
   }
 
   // Run animations which need to respond to updated scroll offset.
-  mutator_host_->TickScrollAnimations(CurrentBeginFrameArgs().frame_time);
+  mutator_host_->TickScrollAnimations(
+      CurrentBeginFrameArgs().frame_time,
+      active_tree_->property_trees()->scroll_tree);
 
   return scroll_result;
 }
@@ -3974,8 +3976,13 @@
   return animated;
 }
 
-bool LayerTreeHostImpl::AnimateLayers(base::TimeTicks monotonic_time) {
-  const bool animated = mutator_host_->TickAnimations(monotonic_time);
+bool LayerTreeHostImpl::AnimateLayers(base::TimeTicks monotonic_time,
+                                      bool is_active_tree) {
+  const ScrollTree& scroll_tree =
+      is_active_tree ? active_tree_->property_trees()->scroll_tree
+                     : pending_tree_->property_trees()->scroll_tree;
+  const bool animated =
+      mutator_host_->TickAnimations(monotonic_time, scroll_tree);
 
   // TODO(crbug.com/551134): Only do this if the animations are on the active
   // tree, or if they are on the pending tree waiting for some future time to
@@ -4438,7 +4445,8 @@
   property_trees->scroll_tree.OnScrollOffsetAnimated(
       element_id, scroll_node_index, scroll_offset, tree);
   // Run animations which need to respond to updated scroll offset.
-  mutator_host_->TickScrollAnimations(CurrentBeginFrameArgs().frame_time);
+  mutator_host_->TickScrollAnimations(CurrentBeginFrameArgs().frame_time,
+                                      property_trees->scroll_tree);
 }
 
 void LayerTreeHostImpl::SetNeedUpdateGpuRasterizationStatus() {
diff --git a/cc/trees/layer_tree_host_impl.h b/cc/trees/layer_tree_host_impl.h
index 2d4dd4e..42679680 100644
--- a/cc/trees/layer_tree_host_impl.h
+++ b/cc/trees/layer_tree_host_impl.h
@@ -654,7 +654,8 @@
       scoped_refptr<base::SequencedTaskRunner> image_worker_task_runner);
 
   // Virtual for testing.
-  virtual bool AnimateLayers(base::TimeTicks monotonic_time);
+  virtual bool AnimateLayers(base::TimeTicks monotonic_time,
+                             bool is_active_tree);
 
   bool is_likely_to_require_a_draw() const {
     return is_likely_to_require_a_draw_;
diff --git a/cc/trees/layer_tree_mutator.h b/cc/trees/layer_tree_mutator.h
index 607334e..053f353 100644
--- a/cc/trees/layer_tree_mutator.h
+++ b/cc/trees/layer_tree_mutator.h
@@ -25,8 +25,8 @@
     int animation_player_id = 0;
     // Name associated with worklet animation player.
     std::string name;
-    // Worklet animation player's current time.
-    base::TimeTicks current_time;
+    // Worklet animation player's current time, from its associated timeline.
+    double current_time = 0;
   };
 
   MutatorInputState();
diff --git a/cc/trees/mutator_host.h b/cc/trees/mutator_host.h
index 19a6747e..580d095 100644
--- a/cc/trees/mutator_host.h
+++ b/cc/trees/mutator_host.h
@@ -25,6 +25,7 @@
 class MutatorEvents;
 class MutatorHostClient;
 class LayerTreeMutator;
+class ScrollTree;
 
 // A MutatorHost owns all the animation and mutation effects.
 // There is just one MutatorHost for LayerTreeHost on main renderer thread
@@ -58,9 +59,13 @@
   virtual bool NeedsTickAnimations() const = 0;
 
   virtual bool ActivateAnimations() = 0;
-  virtual bool TickAnimations(base::TimeTicks monotonic_time) = 0;
+  // TODO(smcgruer): Once we only tick scroll-based animations on scroll, we
+  // don't need to pass the scroll tree in here.
+  virtual bool TickAnimations(base::TimeTicks monotonic_time,
+                              const ScrollTree& scroll_tree) = 0;
   // Tick animations that depends on scroll offset.
-  virtual void TickScrollAnimations(base::TimeTicks monotonic_time) = 0;
+  virtual void TickScrollAnimations(base::TimeTicks monotonic_time,
+                                    const ScrollTree& scroll_tree) = 0;
   virtual bool UpdateAnimationState(bool start_ready_animations,
                                     MutatorEvents* events) = 0;
 
diff --git a/third_party/WebKit/LayoutTests/virtual/threaded/fast/animationworklet/animation-worklet-scroll-timeline-expected.html b/third_party/WebKit/LayoutTests/virtual/threaded/fast/animationworklet/animation-worklet-scroll-timeline-expected.html
new file mode 100644
index 0000000..44e38599
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/virtual/threaded/fast/animationworklet/animation-worklet-scroll-timeline-expected.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<style>
+#box {
+  width: 100px;
+  height: 100px;
+  background-color: #00ff00;
+  transform: translate(0, 100px);
+  opacity: 0.5;
+  will-change: transform; /* force compositing */
+}
+
+#covered {
+  width: 100px;
+  height: 100px;
+  background-color: #ff8080;
+}
+
+#scroller {
+  overflow: auto;
+  height: 100px;
+  width: 100px;
+  will-change: transform; /* force compositing */
+}
+
+#contents {
+  height: 1000px;
+  width: 100%;
+}
+</style>
+
+<div id="box"></div>
+<div id="covered"></div>
+<div id="scroller">
+  <div id="contents"></div>
+</div>
+
+<script>
+window.addEventListener('load', function() {
+  // Move the scroller to halfway.
+  const scroller = document.getElementById("scroller");
+  const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+  scroller.scrollTop = 0.5 * maxScroll;
+});
+</script>
diff --git a/third_party/WebKit/LayoutTests/virtual/threaded/fast/animationworklet/animation-worklet-scroll-timeline.html b/third_party/WebKit/LayoutTests/virtual/threaded/fast/animationworklet/animation-worklet-scroll-timeline.html
new file mode 100644
index 0000000..26e19f3f
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/virtual/threaded/fast/animationworklet/animation-worklet-scroll-timeline.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<style>
+#box {
+  width: 100px;
+  height: 100px;
+  background-color: #00ff00;
+  /*
+  * Force compositing.
+  * TODO(majidvp): Should not be needed when http://crbug.com/776533 is fixed.
+  */
+  will-change: transform;
+}
+
+#covered {
+  width: 100px;
+  height: 100px;
+  background-color: #ff8080;
+}
+
+#scroller {
+  overflow: auto;
+  height: 100px;
+  width: 100px;
+  /*
+  * Force compositing.
+  * TODO(crbug.com/776533): Shouldn't be needed - we should fallback to main if scroller isn't composited.
+  */
+  will-change: transform;
+}
+
+#contents {
+  height: 1000px;
+  width: 100%;
+}
+</style>
+
+<div id="box"></div>
+<div id="covered"></div>
+<div id="scroller">
+  <div id="contents"></div>
+</div>
+
+<script id="visual_update"  type="text/worklet">
+registerAnimator("test_animator", class {
+  animate(currentTime, effect) {
+    effect.localTime = currentTime;
+  }
+});
+</script>
+
+<script src="resources/animation-worklet-tests.js"></script>
+<script>
+if (window.testRunner) {
+  testRunner.waitUntilDone();
+}
+
+runInAnimationWorklet(
+  document.getElementById('visual_update').textContent
+).then(()=>{
+  const box = document.getElementById('box');
+  const effect = new KeyframeEffect(box,
+    [
+     { transform: 'translateY(0)', opacity: 1},
+     { transform: 'translateY(200px)', opacity: 0}
+    ], {
+      duration: 1000,
+    }
+  );
+
+  const scroller = document.getElementById('scroller');
+  const timeline = new ScrollTimeline({ scrollSource: scroller, timeRange: 1000, orientation: 'block' });
+  const animation = new WorkletAnimation('test_animator', [effect], timeline, {});
+  animation.play();
+
+  // Move the scroller to the halfway point.
+  const maxScroll = scroller.scrollHeight - scroller.clientHeight;
+  scroller.scrollTop = 0.5 * maxScroll;
+
+  if (window.testRunner) {
+    waitTwoAnimationFrames(_ => {
+      testRunner.notifyDone();
+    });
+  }
+});
+</script>
diff --git a/third_party/WebKit/Source/core/animation/ScrollTimeline.h b/third_party/WebKit/Source/core/animation/ScrollTimeline.h
index fb090e1..ca43808f 100644
--- a/third_party/WebKit/Source/core/animation/ScrollTimeline.h
+++ b/third_party/WebKit/Source/core/animation/ScrollTimeline.h
@@ -44,6 +44,8 @@
   String orientation();
   void timeRange(DoubleOrScrollTimelineAutoKeyword&);
 
+  ScrollDirection GetOrientation() const { return orientation_; }
+
   void Trace(blink::Visitor*) override;
 
  private:
diff --git a/third_party/WebKit/Source/core/animation/WorkletAnimationBase.h b/third_party/WebKit/Source/core/animation/WorkletAnimationBase.h
index 359da32..bd5ca03 100644
--- a/third_party/WebKit/Source/core/animation/WorkletAnimationBase.h
+++ b/third_party/WebKit/Source/core/animation/WorkletAnimationBase.h
@@ -7,6 +7,7 @@
 
 #include "core/CoreExport.h"
 #include "platform/bindings/ScriptWrappable.h"
+#include "platform/wtf/Forward.h"
 
 namespace blink {
 
@@ -17,12 +18,13 @@
   virtual ~WorkletAnimationBase() {}
 
   // Attempts to start the animation on the compositor side, returning true if
-  // it succeeds or false otherwise.
+  // it succeeds or false otherwise. If false is returned and failure_message
+  // was non-null, failure_message may be filled with an error description.
   //
   // On a false return it may still be possible to start the animation on the
   // compositor later (e.g. if an incompatible property is removed from the
   // element), so the caller should try again next main frame.
-  virtual bool StartOnCompositor() = 0;
+  virtual bool StartOnCompositor(String* failure_message) = 0;
 
   virtual Document* GetDocument() const = 0;
 };
diff --git a/third_party/WebKit/Source/core/animation/WorkletAnimationController.cpp b/third_party/WebKit/Source/core/animation/WorkletAnimationController.cpp
index f913924..065cda1 100644
--- a/third_party/WebKit/Source/core/animation/WorkletAnimationController.cpp
+++ b/third_party/WebKit/Source/core/animation/WorkletAnimationController.cpp
@@ -7,10 +7,13 @@
 #include "core/animation/WorkletAnimationBase.h"
 #include "core/dom/Document.h"
 #include "core/frame/LocalFrameView.h"
+#include "core/inspector/ConsoleMessage.h"
+#include "platform/wtf/text/WTFString.h"
 
 namespace blink {
 
-WorkletAnimationController::WorkletAnimationController() = default;
+WorkletAnimationController::WorkletAnimationController(Document* document)
+    : document_(document) {}
 
 WorkletAnimationController::~WorkletAnimationController() = default;
 
@@ -43,16 +46,20 @@
   HeapHashSet<Member<WorkletAnimationBase>> animations;
   animations.swap(pending_animations_);
   for (const auto& animation : animations) {
-    if (animation->StartOnCompositor()) {
+    String failure_message;
+    if (animation->StartOnCompositor(&failure_message)) {
       compositor_animations_.insert(animation);
+    } else {
+      document_->AddConsoleMessage(ConsoleMessage::Create(
+          kOtherMessageSource, kWarningMessageLevel, failure_message));
     }
-    // TODO(smcgruer): On failure, warn user. Perhaps fire cancel event?
   }
 }
 
 void WorkletAnimationController::Trace(blink::Visitor* visitor) {
   visitor->Trace(pending_animations_);
   visitor->Trace(compositor_animations_);
+  visitor->Trace(document_);
 }
 
 }  // namespace blink
diff --git a/third_party/WebKit/Source/core/animation/WorkletAnimationController.h b/third_party/WebKit/Source/core/animation/WorkletAnimationController.h
index ff6b3971..d0e6d92 100644
--- a/third_party/WebKit/Source/core/animation/WorkletAnimationController.h
+++ b/third_party/WebKit/Source/core/animation/WorkletAnimationController.h
@@ -12,6 +12,7 @@
 
 namespace blink {
 
+class Document;
 class WorkletAnimationBase;
 
 // Handles AnimationWorklet animations on the main-thread.
@@ -27,7 +28,7 @@
 class CORE_EXPORT WorkletAnimationController
     : public GarbageCollectedFinalized<WorkletAnimationController> {
  public:
-  WorkletAnimationController();
+  WorkletAnimationController(Document*);
   virtual ~WorkletAnimationController();
 
   void AttachAnimation(WorkletAnimationBase&);
@@ -40,6 +41,8 @@
  private:
   HeapHashSet<Member<WorkletAnimationBase>> pending_animations_;
   HeapHashSet<Member<WorkletAnimationBase>> compositor_animations_;
+
+  Member<Document> document_;
 };
 
 }  // namespace blink
diff --git a/third_party/WebKit/Source/core/dom/Document.cpp b/third_party/WebKit/Source/core/dom/Document.cpp
index c610404..6a13dda 100644
--- a/third_party/WebKit/Source/core/dom/Document.cpp
+++ b/third_party/WebKit/Source/core/dom/Document.cpp
@@ -644,7 +644,7 @@
           &Document::ElementDataCacheClearTimerFired),
       timeline_(DocumentTimeline::Create(this)),
       pending_animations_(new PendingAnimations(*this)),
-      worklet_animation_controller_(new WorkletAnimationController),
+      worklet_animation_controller_(new WorkletAnimationController(this)),
       template_document_host_(nullptr),
       did_associate_form_controls_timer_(
           GetTaskRunner(TaskType::kUnspecedLoading),
diff --git a/third_party/WebKit/Source/modules/animationworklet/AnimationWorkletGlobalScopeTest.cpp b/third_party/WebKit/Source/modules/animationworklet/AnimationWorkletGlobalScopeTest.cpp
index 3d53b44..7579f45 100644
--- a/third_party/WebKit/Source/modules/animationworklet/AnimationWorkletGlobalScopeTest.cpp
+++ b/third_party/WebKit/Source/modules/animationworklet/AnimationWorkletGlobalScopeTest.cpp
@@ -184,6 +184,7 @@
     CompositorMutatorInputState::AnimationState test_animation_state;
     test_animation_state.animation_player_id = 1;
     test_animation_state.name = "test";
+    test_animation_state.current_time = 5000;
     state.animations = {test_animation_state};
 
     std::unique_ptr<CompositorMutatorOutputState> output =
@@ -235,6 +236,7 @@
     CompositorMutatorInputState::AnimationState test_animation_state;
     test_animation_state.animation_player_id = 1;
     test_animation_state.name = "test";
+    test_animation_state.current_time = 5000;
     state.animations = {test_animation_state};
 
     std::unique_ptr<CompositorMutatorOutputState> output =
@@ -290,7 +292,7 @@
       &AnimationWorkletGlobalScopeTest::RunConstructAndAnimateTestOnWorklet);
 }
 
-TEST_F(AnimationWorkletGlobalScopeTest, AnimtionOutput) {
+TEST_F(AnimationWorkletGlobalScopeTest, AnimationOutput) {
   RunTestOnWorkletThread(
       &AnimationWorkletGlobalScopeTest::RunAnimateOutputTestOnWorklet);
 }
diff --git a/third_party/WebKit/Source/modules/animationworklet/Animator.cpp b/third_party/WebKit/Source/modules/animationworklet/Animator.cpp
index 94a6fea..ff060ae 100644
--- a/third_party/WebKit/Source/modules/animationworklet/Animator.cpp
+++ b/third_party/WebKit/Source/modules/animationworklet/Animator.cpp
@@ -45,9 +45,6 @@
   if (IsUndefinedOrNull(instance) || IsUndefinedOrNull(animate))
     return false;
 
-  // Update local time
-  current_time_ = WTF::TimeTicks(input.current_time);
-
   ScriptState::Scope scope(script_state);
   v8::TryCatch block(isolate);
   block.SetVerbose(true);
@@ -58,8 +55,7 @@
       ToV8(effect_, script_state->GetContext()->Global(), isolate);
 
   v8::Local<v8::Value> v8_current_time =
-      ToV8((current_time_ - WTF::TimeTicks()).InMillisecondsF(),
-           script_state->GetContext()->Global(), isolate);
+      ToV8(input.current_time, script_state->GetContext()->Global(), isolate);
 
   v8::Local<v8::Value> argv[] = {v8_current_time, v8_effect};
 
diff --git a/third_party/WebKit/Source/modules/animationworklet/Animator.h b/third_party/WebKit/Source/modules/animationworklet/Animator.h
index ff553c1..7fd1c62 100644
--- a/third_party/WebKit/Source/modules/animationworklet/Animator.h
+++ b/third_party/WebKit/Source/modules/animationworklet/Animator.h
@@ -47,7 +47,6 @@
   TraceWrapperV8Reference<v8::Object> instance_;
 
   bool did_animate_ = false;
-  WTF::TimeTicks current_time_;
   Member<EffectProxy> effect_;
 };
 
diff --git a/third_party/WebKit/Source/modules/animationworklet/WorkletAnimation.cpp b/third_party/WebKit/Source/modules/animationworklet/WorkletAnimation.cpp
index 3429604..11749126 100644
--- a/third_party/WebKit/Source/modules/animationworklet/WorkletAnimation.cpp
+++ b/third_party/WebKit/Source/modules/animationworklet/WorkletAnimation.cpp
@@ -10,6 +10,7 @@
 #include "core/animation/Timing.h"
 #include "core/dom/Node.h"
 #include "core/dom/NodeComputedStyle.h"
+#include "core/layout/LayoutBox.h"
 #include "platform/wtf/text/WTFString.h"
 #include "public/platform/Platform.h"
 #include "public/platform/WebCompositorSupport.h"
@@ -54,6 +55,70 @@
   }
   return true;
 }
+
+bool CheckElementComposited(const Element& target) {
+  return target.GetLayoutObject() &&
+         target.GetLayoutObject()->GetCompositingState() ==
+             kPaintsIntoOwnBacking;
+}
+
+CompositorElementId GetCompositorScrollElementId(const Element& element) {
+  DCHECK(element.GetLayoutObject());
+  DCHECK(element.GetLayoutObject()->HasLayer());
+  return CompositorElementIdFromUniqueObjectId(
+      element.GetLayoutObject()->UniqueId(),
+      CompositorElementIdNamespace::kScroll);
+}
+
+// Convert the blink concept of a ScrollTimeline orientation into the cc one.
+//
+// The compositor does not know about writing modes, so we have to convert the
+// web concepts of 'block' and 'inline' direction into absolute vertical or
+// horizontal directions.
+//
+// TODO(smcgruer): If the writing mode of a scroller changes, we have to update
+// any related cc::ScrollTimeline somehow.
+CompositorScrollTimeline::ScrollDirection ConvertOrientation(
+    ScrollTimeline::ScrollDirection orientation,
+    bool is_horizontal_writing_mode) {
+  switch (orientation) {
+    case ScrollTimeline::Block:
+      return is_horizontal_writing_mode ? CompositorScrollTimeline::Vertical
+                                        : CompositorScrollTimeline::Horizontal;
+    case ScrollTimeline::Inline:
+      return is_horizontal_writing_mode ? CompositorScrollTimeline::Horizontal
+                                        : CompositorScrollTimeline::Vertical;
+    default:
+      NOTREACHED();
+      return CompositorScrollTimeline::Vertical;
+  }
+}
+
+// Converts a blink::ScrollTimeline into a cc::ScrollTimeline.
+//
+// If the timeline cannot be converted, returns nullptr.
+std::unique_ptr<CompositorScrollTimeline> ToCompositorScrollTimeline(
+    const DocumentTimelineOrScrollTimeline& timeline) {
+  if (!timeline.IsScrollTimeline())
+    return nullptr;
+
+  ScrollTimeline* scroll_timeline = timeline.GetAsScrollTimeline();
+  Element* scroll_source = scroll_timeline->scrollSource();
+  CompositorElementId element_id = GetCompositorScrollElementId(*scroll_source);
+
+  DoubleOrScrollTimelineAutoKeyword time_range;
+  scroll_timeline->timeRange(time_range);
+  // TODO(smcgruer): Handle 'auto' time range value.
+  DCHECK(time_range.IsDouble());
+
+  LayoutBox* box = scroll_source->GetLayoutBox();
+  DCHECK(box);
+  CompositorScrollTimeline::ScrollDirection orientation = ConvertOrientation(
+      scroll_timeline->GetOrientation(), box->IsHorizontalWritingMode());
+
+  return std::make_unique<CompositorScrollTimeline>(element_id, orientation,
+                                                    time_range.GetAsDouble());
+}
 }  // namespace
 
 WorkletAnimation* WorkletAnimation::Create(
@@ -78,9 +143,6 @@
   Document& document = effects.at(0)->Target()->GetDocument();
   WorkletAnimation* animation = new WorkletAnimation(
       animator_name, document, effects, timeline, std::move(options));
-  if (CompositorAnimationTimeline* compositor_timeline =
-          document.Timeline().CompositorTimeline())
-    compositor_timeline->PlayerAttached(*animation);
 
   return animation;
 }
@@ -100,10 +162,6 @@
   DCHECK(IsMainThread());
   DCHECK(Platform::Current()->IsThreadedAnimationEnabled());
   DCHECK(Platform::Current()->CompositorSupport());
-
-  compositor_player_ =
-      CompositorAnimationPlayer::CreateWorkletPlayer(animator_name_);
-  compositor_player_->SetAnimationDelegate(this);
 }
 
 String WorkletAnimation::playState() {
@@ -127,39 +185,71 @@
   }
 }
 
-bool WorkletAnimation::StartOnCompositor() {
+bool WorkletAnimation::StartOnCompositor(String* failure_message) {
   DCHECK(IsMainThread());
-  Element& target = *effects_.at(0)->Target();
+  KeyframeEffectReadOnly* target_effect = effects_.at(0);
+  Element& target = *target_effect->Target();
 
-  // TODO(smcgruer): Creating a WorkletAnimaiton should be a hint to blink
+  // CheckCanStartAnimationOnCompositor requires that the property-specific
+  // keyframe groups have been created. To ensure this we manually snapshot the
+  // frames in the target effect.
+  // TODO(smcgruer): This shouldn't be necessary - Animation doesn't do this.
+  ToKeyframeEffectModelBase(target_effect->Model())
+      ->SnapshotAllCompositorKeyframes(target, target.ComputedStyleRef(),
+                                       target.ParentComputedStyle());
+
+  if (!CheckElementComposited(target)) {
+    if (failure_message)
+      *failure_message = "The target element is not composited.";
+    return false;
+  }
+
+  if (timeline_.IsScrollTimeline() &&
+      !CheckElementComposited(
+          *timeline_.GetAsScrollTimeline()->scrollSource())) {
+    if (failure_message)
+      *failure_message = "The ScrollTimeline scrollSource is not composited.";
+    return false;
+  }
+
+  double playback_rate = 1;
+  CompositorAnimations::FailureCode failure_code =
+      target_effect->CheckCanStartAnimationOnCompositor(playback_rate);
+
+  if (!failure_code.Ok()) {
+    play_state_ = Animation::kIdle;
+    if (failure_message)
+      *failure_message = failure_code.reason;
+    return false;
+  }
+
+  if (!compositor_player_) {
+    compositor_player_ = CompositorAnimationPlayer::CreateWorkletPlayer(
+        animator_name_, ToCompositorScrollTimeline(timeline_));
+    compositor_player_->SetAnimationDelegate(this);
+  }
+
+  // Register ourselves on the compositor timeline. This will cause our cc-side
+  // animation player to be registered.
+  if (CompositorAnimationTimeline* compositor_timeline =
+          document_->Timeline().CompositorTimeline())
+    compositor_timeline->PlayerAttached(*this);
+
+  // TODO(smcgruer): Creating a WorkletAnimation should be a hint to blink
   // compositing that the animated element should be promoted. Otherwise this
   // fails. http://crbug.com/776533
   CompositorAnimations::AttachCompositedLayers(target,
                                                compositor_player_.get());
 
-  double animation_playback_rate = 1;
-  ToKeyframeEffectModelBase(effects_.at(0)->Model())
-      ->SnapshotAllCompositorKeyframes(target, target.ComputedStyleRef(),
-                                       target.ParentComputedStyle());
+  double start_time = std::numeric_limits<double>::quiet_NaN();
+  double time_offset = 0;
+  int group = 0;
 
-  bool success =
-      effects_.at(0)
-          ->CheckCanStartAnimationOnCompositor(animation_playback_rate)
-          .Ok();
-
-  if (success) {
-    double start_time = std::numeric_limits<double>::quiet_NaN();
-    double time_offset = 0;
-    int group = 0;
-
-    // TODO(smcgruer): We need to start all of the effects, not just the first.
-    effects_.at(0)->StartAnimationOnCompositor(group, start_time, time_offset,
-                                               animation_playback_rate,
-                                               compositor_player_.get());
-  }
-
-  play_state_ = success ? Animation::kRunning : Animation::kIdle;
-  return success;
+  // TODO(smcgruer): We need to start all of the effects, not just the first.
+  effects_.at(0)->StartAnimationOnCompositor(
+      group, start_time, time_offset, playback_rate, compositor_player_.get());
+  play_state_ = Animation::kRunning;
+  return true;
 }
 
 void WorkletAnimation::Dispose() {
@@ -168,8 +258,10 @@
   if (CompositorAnimationTimeline* compositor_timeline =
           document_->Timeline().CompositorTimeline())
     compositor_timeline->PlayerDestroyed(*this);
-  compositor_player_->SetAnimationDelegate(nullptr);
-  compositor_player_ = nullptr;
+  if (compositor_player_) {
+    compositor_player_->SetAnimationDelegate(nullptr);
+    compositor_player_ = nullptr;
+  }
 }
 
 void WorkletAnimation::Trace(blink::Visitor* visitor) {
diff --git a/third_party/WebKit/Source/modules/animationworklet/WorkletAnimation.h b/third_party/WebKit/Source/modules/animationworklet/WorkletAnimation.h
index f554ec9..cb986b9 100644
--- a/third_party/WebKit/Source/modules/animationworklet/WorkletAnimation.h
+++ b/third_party/WebKit/Source/modules/animationworklet/WorkletAnimation.h
@@ -49,7 +49,7 @@
   void cancel();
 
   // WorkletAnimationBase implementation.
-  bool StartOnCompositor() override;
+  bool StartOnCompositor(String* failure_message) override;
 
   // CompositorAnimationPlayerClient implementation.
   CompositorAnimationPlayer* CompositorPlayer() const override {
diff --git a/third_party/WebKit/Source/platform/animation/CompositorAnimationPlayer.cpp b/third_party/WebKit/Source/platform/animation/CompositorAnimationPlayer.cpp
index 47c621d8..280cdb54 100644
--- a/third_party/WebKit/Source/platform/animation/CompositorAnimationPlayer.cpp
+++ b/third_party/WebKit/Source/platform/animation/CompositorAnimationPlayer.cpp
@@ -18,11 +18,14 @@
 }
 
 std::unique_ptr<CompositorAnimationPlayer>
-CompositorAnimationPlayer::CreateWorkletPlayer(const String& name) {
+CompositorAnimationPlayer::CreateWorkletPlayer(
+    const String& name,
+    std::unique_ptr<CompositorScrollTimeline> scroll_timeline) {
   return std::make_unique<CompositorAnimationPlayer>(
       cc::WorkletAnimationPlayer::Create(
           cc::AnimationIdProvider::NextPlayerId(),
-          std::string(name.Ascii().data(), name.length())));
+          std::string(name.Ascii().data(), name.length()),
+          std::move(scroll_timeline)));
 }
 
 CompositorAnimationPlayer::CompositorAnimationPlayer(
diff --git a/third_party/WebKit/Source/platform/animation/CompositorAnimationPlayer.h b/third_party/WebKit/Source/platform/animation/CompositorAnimationPlayer.h
index f427286..e18ae1a2 100644
--- a/third_party/WebKit/Source/platform/animation/CompositorAnimationPlayer.h
+++ b/third_party/WebKit/Source/platform/animation/CompositorAnimationPlayer.h
@@ -9,6 +9,7 @@
 #include "base/memory/scoped_refptr.h"
 #include "cc/animation/animation_delegate.h"
 #include "cc/animation/animation_player.h"
+#include "cc/animation/scroll_timeline.h"
 #include "cc/animation/worklet_animation_player.h"
 #include "platform/PlatformExport.h"
 #include "platform/graphics/CompositorElementId.h"
@@ -21,6 +22,8 @@
 
 namespace blink {
 
+using CompositorScrollTimeline = cc::ScrollTimeline;
+
 class CompositorAnimation;
 class CompositorAnimationDelegate;
 
@@ -31,7 +34,8 @@
  public:
   static std::unique_ptr<CompositorAnimationPlayer> Create();
   static std::unique_ptr<CompositorAnimationPlayer> CreateWorkletPlayer(
-      const String& name);
+      const String& name,
+      std::unique_ptr<CompositorScrollTimeline>);
 
   explicit CompositorAnimationPlayer(scoped_refptr<cc::AnimationPlayer>);
   ~CompositorAnimationPlayer();