Bug 1321428 - Introduce ScrollTimeline, a new kind of AnimationTimeline for scroll-driven animations. r=birtles,bz draft
authorBotond Ballo <botond@mozilla.com>
Fri, 24 Mar 2017 17:26:01 -0400
changeset 542198 a91fa1e7f87c2da3f99c99fca678b9930b00741d
parent 501569 05bfa2831c0ba4a26fa72328ffe6a99aba9c356a
child 542199 b14c87d6d8a9e1130608bf2fbd3a6c0688855956
push id50961
push userbballo@mozilla.com
push dateFri, 24 Mar 2017 22:03:27 +0000
reviewersbirtles, bz
bugs1321428
milestone55.0a1
Bug 1321428 - Introduce ScrollTimeline, a new kind of AnimationTimeline for scroll-driven animations. r=birtles,bz MozReview-Commit-ID: 162QLYm0Ak1
dom/animation/Animation.h
dom/animation/AnimationTimeline.cpp
dom/animation/AnimationTimeline.h
dom/animation/DocumentTimeline.cpp
dom/animation/DocumentTimeline.h
dom/animation/ScrollTimeline.cpp
dom/animation/ScrollTimeline.h
dom/animation/ScrollTimelineSet.cpp
dom/animation/ScrollTimelineSet.h
dom/animation/ScrollTimelineUtils.cpp
dom/animation/ScrollTimelineUtils.h
dom/animation/moz.build
dom/base/FragmentOrElement.cpp
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/base/nsGkAtomList.h
dom/base/nsIDocument.h
dom/base/nsNodeUtils.cpp
dom/webidl/ScrollTimeline.webidl
dom/webidl/moz.build
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
layout/generic/nsGfxScrollFrame.cpp
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -327,16 +327,17 @@ public:
    * updated in |aComposeResult|.
    */
   template<typename ComposeAnimationResult>
   void ComposeStyle(ComposeAnimationResult&& aComposeResult,
                     const nsCSSPropertyIDSet& aPropertiesToSkip);
 
   void NotifyEffectTimingUpdated();
   void NotifyGeometricAnimationsStartingThisFrame();
+  StickyTimeDuration EffectEnd() const;
 
   /**
    * Used by subclasses to synchronously queue a cancel event in situations
    * where the Animation may have been cancelled.
    *
    * We need to do this synchronously because after a CSS animation/transition
    * is canceled, it will be released by its owning element and may not still
    * exist when we would normally go to queue events on the next tick.
@@ -410,17 +411,16 @@ protected:
    * useful because in some cases animations that are painted together
    * may need to be synchronized.
    */
   bool IsNewlyStarted() const {
     return mPendingState == PendingState::PlayPending &&
            mPendingReadyTime.IsNull();
   }
   bool IsPossiblyOrphanedPendingAnimation() const;
-  StickyTimeDuration EffectEnd() const;
 
   nsIDocument* GetRenderedDocument() const;
 
   RefPtr<AnimationTimeline> mTimeline;
   RefPtr<AnimationEffectReadOnly> mEffect;
   // The beginning of the delay period.
   Nullable<TimeDuration> mStartTime; // Timeline timescale
   Nullable<TimeDuration> mHoldTime;  // Animation timescale
--- a/dom/animation/AnimationTimeline.cpp
+++ b/dom/animation/AnimationTimeline.cpp
@@ -10,22 +10,22 @@
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationTimeline)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationTimeline)
   tmp->mAnimationOrder.clear();
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow, mAnimations)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnimations)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationTimeline)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mAnimations)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnimations)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationTimeline)
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationTimeline)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationTimeline)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationTimeline)
--- a/dom/animation/AnimationTimeline.h
+++ b/dom/animation/AnimationTimeline.h
@@ -22,40 +22,37 @@
 #ifdef GetCurrentTime
 #undef GetCurrentTime
 #endif
 
 namespace mozilla {
 namespace dom {
 
 class Animation;
+class ScrollTimeline;
 
 class AnimationTimeline
   : public nsISupports
   , public nsWrapperCache
 {
 public:
-  explicit AnimationTimeline(nsIGlobalObject* aWindow)
-    : mWindow(aWindow)
+  AnimationTimeline()
   {
-    MOZ_ASSERT(mWindow);
   }
 
 protected:
   virtual ~AnimationTimeline()
   {
     mAnimationOrder.clear();
   }
 
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AnimationTimeline)
 
-  nsIGlobalObject* GetParentObject() const { return mWindow; }
-
   // AnimationTimeline methods
   virtual Nullable<TimeDuration> GetCurrentTime() const = 0;
 
   // Wrapper functions for AnimationTimeline DOM methods when called from
   // script.
   Nullable<double> GetCurrentTimeAsDouble() const {
     return AnimationUtils::TimeDurationToDouble(GetCurrentTime());
   }
@@ -99,16 +96,19 @@ public:
    * time.
    */
   bool HasAnimations() const {
     return !mAnimations.IsEmpty();
   }
 
   virtual void RemoveAnimation(Animation* aAnimation);
 
+  virtual ScrollTimeline* AsScrollTimeline() { return nullptr; }
+  virtual const ScrollTimeline* AsScrollTimeline() const { return nullptr; }
+
 protected:
   nsCOMPtr<nsIGlobalObject> mWindow;
 
   // Animations observing this timeline
   //
   // We store them in (a) a hashset for quick lookup, and (b) an array
   // to maintain a fixed sampling order.
   //
--- a/dom/animation/DocumentTimeline.cpp
+++ b/dom/animation/DocumentTimeline.cpp
@@ -19,21 +19,21 @@ namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentTimeline)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentTimeline,
                                                 AnimationTimeline)
   tmp->UnregisterFromRefreshDriver();
   if (tmp->isInList()) {
     tmp->remove();
   }
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow, mDocument)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentTimeline,
                                                   AnimationTimeline)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mDocument)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(DocumentTimeline,
                                                AnimationTimeline)
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(DocumentTimeline)
 NS_INTERFACE_MAP_END_INHERITING(AnimationTimeline)
--- a/dom/animation/DocumentTimeline.h
+++ b/dom/animation/DocumentTimeline.h
@@ -28,17 +28,18 @@ namespace dom {
 
 class DocumentTimeline final
   : public AnimationTimeline
   , public nsARefreshObserver
   , public LinkedListElement<DocumentTimeline>
 {
 public:
   DocumentTimeline(nsIDocument* aDocument, const TimeDuration& aOriginTime)
-    : AnimationTimeline(aDocument->GetParentObject())
+    : AnimationTimeline()
+    , mWindow(aDocument->GetParentObject())
     , mDocument(aDocument)
     , mIsObservingRefreshDriver(false)
     , mOriginTime(aOriginTime)
   {
     if (mDocument) {
       mDocument->Timelines().insertBack(this);
     }
   }
@@ -53,16 +54,18 @@ protected:
     }
   }
 
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(DocumentTimeline,
                                                          AnimationTimeline)
 
+  nsIGlobalObject* GetParentObject() { return mWindow; }
+
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
   static already_AddRefed<DocumentTimeline>
   Constructor(const GlobalObject& aGlobal,
               const DocumentTimelineOptions& aOptions,
               ErrorResult& aRv);
 
@@ -89,16 +92,17 @@ public:
   void NotifyRefreshDriverCreated(nsRefreshDriver* aDriver);
   void NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver);
 
 protected:
   TimeStamp GetCurrentTimeStamp() const;
   nsRefreshDriver* GetRefreshDriver() const;
   void UnregisterFromRefreshDriver();
 
+  nsCOMPtr<nsIGlobalObject> mWindow;
   nsCOMPtr<nsIDocument> mDocument;
 
   // The most recently used refresh driver time. This is used in cases where
   // we don't have a refresh driver (e.g. because we are in a display:none
   // iframe).
   mutable TimeStamp mLastRefreshDriverTime;
   bool mIsObservingRefreshDriver;
 
new file mode 100644
--- /dev/null
+++ b/dom/animation/ScrollTimeline.cpp
@@ -0,0 +1,381 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScrollTimeline.h"
+#include "mozilla/dom/ScrollTimelineBinding.h"
+#include "mozilla/dom/ScrollTimelineUtils.h"
+#include "mozilla/MathAlgorithms.h"  // for Clamp()
+#include "AnimationUtils.h"
+#include "nsContentUtils.h"
+#include "nsCSSParser.h"
+#include "nsDOMMutationObserver.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsIPresShell.h"
+#include "nsLayoutUtils.h"
+#include "nsPresContext.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(ScrollTimeline)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ScrollTimeline,
+                                                AnimationTimeline)
+  tmp->Teardown();
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mScrollSource)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ScrollTimeline,
+                                                  AnimationTimeline)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScrollSource)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ScrollTimeline,
+                                               AnimationTimeline)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ScrollTimeline)
+NS_INTERFACE_MAP_END_INHERITING(AnimationTimeline)
+
+NS_IMPL_ADDREF_INHERITED(ScrollTimeline, AnimationTimeline)
+NS_IMPL_RELEASE_INHERITED(ScrollTimeline, AnimationTimeline)
+
+void
+ScrollTimeline::RemoveFromList()
+{
+    if (isInList()) {
+      remove();
+    }
+}
+
+void
+ScrollTimeline::Teardown()
+{
+  // Note: This function is required to be idempotent, as it can be called from
+  // both cycleCollection::Unlink() and ~ScrollTimeline(). When modifying this
+  // function, be sure to preserve this property.
+  if (mScrollSource) {
+    UnregisterFromScrollSource();
+  }
+  if (nsIScrollableFrame* scrollFrame = GetScrollFrame()) {
+    scrollFrame->RemoveScrollPositionListener(mScrollPositionListener.get());
+  }
+  RemoveFromList();
+}
+
+JSObject*
+ScrollTimeline::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return ScrollTimelineBinding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */ already_AddRefed<ScrollTimeline>
+ScrollTimeline::Constructor(const GlobalObject& aGlobal,
+                            const ScrollTimelineOptions& aOptions,
+                            ErrorResult& aRv)
+{
+  nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
+  if (!doc) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  Element* scrollSource = aOptions.mScrollSource.WasPassed()
+      ? &aOptions.mScrollSource.Value()
+      : doc->GetDocumentElement();
+  if (!scrollSource) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  RefPtr<ScrollTimeline> timeline = new ScrollTimeline(scrollSource,
+                                                       aOptions.mOrientation,
+                                                       aOptions.mStartScrollOffset,
+                                                       aOptions.mEndScrollOffset,
+                                                       aOptions.mTimeRange,
+                                                       aOptions.mFillMode);
+  return timeline.forget();
+}
+
+Nullable<TimeDuration>
+ScrollTimeline::GetCurrentTime() const
+{
+  if (!mHaveFrame) {
+    return Nullable<TimeDuration>{};
+  }
+  return ScrollTimelineUtils::CalculateCurrentTime(mCurrentScroll,
+      mMinScroll, mMaxScroll, TimeDuration(mEffectiveTimeRange), mFillMode);
+}
+
+Nullable<TimeDuration>
+ScrollTimeline::ToTimelineTime(const TimeStamp& aTimeStamp) const
+{
+  return Nullable<TimeDuration>{};
+}
+
+// memo: This function is member function of AnimationTimeline.
+void
+ScrollTimeline::NotifyAnimationUpdated(Animation& aAnimation)
+{
+  if (mAnimations.IsEmpty()) {
+    RegisterWithScrollSourceAsOwned();
+  }
+
+  // MEMO: Should I pause animation when setting animation?
+  AnimationTimeline::NotifyAnimationUpdated(aAnimation);
+  CalculateEffectiveTimeRange();
+}
+
+void
+ScrollTimeline::RemoveAnimation(Animation* aAnimation)
+{
+  AnimationTimeline::RemoveAnimation(aAnimation);
+  CalculateEffectiveTimeRange();
+
+  if (mAnimations.IsEmpty()) {
+    UnregisterFromScrollSourceAsOwned();
+  }
+}
+
+TimeStamp
+ScrollTimeline::ToTimeStamp(const TimeDuration& aTimeDuration) const
+{
+  return TimeStamp{};
+}
+
+void
+ScrollTimeline::CalculateEffectiveTimeRange()
+{
+  if (mSpecifiedTimeRangeCooked) {
+    mEffectiveTimeRange = mSpecifiedTimeRangeCooked.ref();
+  } else {
+    mEffectiveTimeRange = 0;
+    for (Animation* animation : mAnimationOrder) {
+      StickyTimeDuration endTime = animation->EffectEnd();
+      if (endTime == StickyTimeDuration::Forever()) {
+        mEffectiveTimeRange = 0;
+        break;
+      }
+      mEffectiveTimeRange = StickyTimeDuration::Max(endTime, mEffectiveTimeRange);
+    }
+  }
+}
+
+static nscoord
+InterpretValue(const nsCSSValue& aValue,
+               nscoord aMin,
+               nscoord aMax,
+               nscoord aDefault)
+{
+  switch (aValue.GetUnit())
+  {
+    case eCSSUnit_Auto:
+      return aDefault;
+    case eCSSUnit_Pixel:
+      return Clamp(aValue.GetPixelLength(), aMin, aMax);
+    case eCSSUnit_Percent:
+      return aMin + ((aMax - aMin) * aValue.GetPercentValue());
+    default:
+      NS_WARNING("Unhandled CSS value type");
+      return aDefault;
+    }
+}
+
+static void
+FormatValue(const nsCSSValue& aValue, nsString& aOutResult)
+{
+  switch (aValue.GetUnit())
+  {
+    case eCSSUnit_Auto:
+      aOutResult.AppendPrintf("auto");
+      break;
+    case eCSSUnit_Pixel:
+      aOutResult.AppendPrintf("%d px", aValue.GetPixelLength());
+      break;
+    case eCSSUnit_Percent:
+      aOutResult.AppendPrintf("%f %%", aValue.GetPercentValue());
+      break;
+    default:
+      NS_WARNING("Unhandled CSS value type");
+    }
+}
+
+void
+ScrollTimeline::GetStartScrollOffset(nsString& aOutResult) const {
+  FormatValue(mComputedStartScrollOffset, aOutResult);
+}
+
+void
+ScrollTimeline::GetEndScrollOffset(nsString& aOutResult) const {
+  FormatValue(mComputedEndScrollOffset, aOutResult);
+}
+
+void
+ScrollTimeline::QueryScrollValues()
+{
+  nsIScrollableFrame* frame = GetScrollFrame();
+  mHaveFrame = (frame != nullptr);
+  if (!mHaveFrame)
+    return;
+
+  nsPoint scrollOffset = frame->GetScrollPosition();
+  nsRect scrollRange = frame->GetScrollRange();
+  nscoord min, max;
+  if (mComputedOrientation == ScrollDirection::Horizontal) {
+    mCurrentScroll = scrollOffset.x;
+    min = scrollRange.x;
+    max = scrollRange.XMost();
+  } else {
+    mCurrentScroll = scrollOffset.y;
+    min = scrollRange.y;
+    max = scrollRange.YMost();
+  }
+  mMinScroll = InterpretValue(mComputedStartScrollOffset, min, max, min);
+  mMaxScroll = InterpretValue(mComputedEndScrollOffset, min, max, max);
+}
+
+void
+ScrollTimeline::NotifyScroll()
+{
+  mNeedsTick = true;
+}
+
+auto
+ScrollTimeline::GetScrollRange() -> ScrollRange
+{
+  // Call QueryScrollValues() to pick up updated min and max values
+  // if the scroll frame has been reflowed since they were last queried.
+  QueryScrollValues();
+  return {mMinScroll, mMaxScroll};
+}
+
+void
+ScrollTimeline::Tick()
+{
+  if (!mNeedsTick) {
+    return;
+  }
+
+  QueryScrollValues();
+
+  nsTArray<Animation*> animationsToRemove(mAnimations.Count());
+
+  nsAutoAnimationMutationBatch mb(mScrollSource->OwnerDoc());
+
+  for (Animation* animation = mAnimationOrder.getFirst(); animation;
+       animation = animation->getNext()) {
+    // Skip any animations that are longer need associated with this timeline.
+    if (animation->GetTimeline() != this) {
+      // If animation has some other timeline, it better not be also in the
+      // animation list of this timeline object!
+      MOZ_ASSERT(!animation->GetTimeline());
+      animationsToRemove.AppendElement(animation);
+      continue;
+    }
+
+    animation->Tick();
+  }
+
+  for (Animation* animation : animationsToRemove) {
+    RemoveAnimation(animation);
+  }
+
+  mNeedsTick = false;
+}
+
+nsIScrollableFrame*
+ScrollTimeline::GetScrollFrame() const
+{
+  if (!mScrollSource) {
+    return nullptr;
+  }
+  return nsLayoutUtils::FindScrollableFrameFor(
+      nsLayoutUtils::FindOrCreateIDFor(mScrollSource.get()));
+}
+
+void
+ScrollTimeline::ParseProperties(const nsString& aSpecifiedStartScrollOffset,
+                                const nsString& aSpecifiedEndScrollOffset)
+{
+  // Parse scroll offsets
+  nsIDocument* doc = mScrollSource->OwnerDoc();
+  nsCSSParser parser(doc->CSSLoader());
+  // Allow the same formats as for the "width" property.
+  parser.ParseLonghandProperty(nsCSSPropertyID::eCSSProperty_width,
+                               aSpecifiedStartScrollOffset,
+                               doc->GetDocumentURI(),
+                               doc->GetDocumentURI(),
+                               doc->NodePrincipal(),
+                               mComputedStartScrollOffset);
+  parser.ParseLonghandProperty(nsCSSPropertyID::eCSSProperty_width,
+                               aSpecifiedEndScrollOffset,
+                               doc->GetDocumentURI(),
+                               doc->GetDocumentURI(),
+                               doc->NodePrincipal(),
+                               mComputedEndScrollOffset);
+
+  // Parse time range
+  if (mSpecifiedTimeRange.IsDouble()) {
+    mSpecifiedTimeRangeCooked = Some(StickyTimeDuration::FromMilliseconds(
+        mSpecifiedTimeRange.GetAsDouble()));
+  }
+  // Otherwise it's "auto", and we leave mSpecifiedTimeRangeCooked as None.
+}
+
+void
+ScrollTimeline::RegisterWithScrollSource()
+{
+  ScrollTimelineSet* scrollTimelineSet =
+    ScrollTimelineSet::GetOrCreateScrollTimelineSet(mScrollSource);
+  scrollTimelineSet->AddScrollTimeline(*this);
+}
+
+void
+ScrollTimeline::UnregisterFromScrollSource()
+{
+  if (ScrollTimelineSet* scrollTimelineSet =
+      ScrollTimelineSet::GetScrollTimelineSet(mScrollSource)) {
+    scrollTimelineSet->RemoveScrollTimeline(*this);
+    if (scrollTimelineSet->IsEmpty()) {
+      ScrollTimelineSet::DestroyScrollTimelineSet(mScrollSource);
+    }
+  }
+}
+
+void
+ScrollTimeline::RegisterWithScrollSourceAsOwned()
+{
+  ScrollTimelineSet* scrollTimelineSet =
+    ScrollTimelineSet::GetOrCreateScrollTimelineSet(mScrollSource);
+  scrollTimelineSet->AddOwnedScrollTimeline(*this);
+}
+
+void
+ScrollTimeline::UnregisterFromScrollSourceAsOwned()
+{
+  if (ScrollTimelineSet* scrollTimelineSet =
+      ScrollTimelineSet::GetScrollTimelineSet(mScrollSource)) {
+    scrollTimelineSet->RemoveOwnedScrollTimeline(*this);
+    if (scrollTimelineSet->IsEmpty()) {
+      ScrollTimelineSet::DestroyScrollTimelineSet(mScrollSource);
+    }
+  }
+}
+
+void
+ScrollTimeline::RegisterWithScrollFrame(nsIScrollableFrame* aScrollFrame)
+{
+  aScrollFrame->AddScrollPositionListener(mScrollPositionListener.get());
+}
+
+void
+ScrollPositionListener::ScrollPositionDidChange(nscoord, nscoord)
+{
+  mTimeline->NotifyScroll();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/animation/ScrollTimeline.h
@@ -0,0 +1,211 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ScrollTimeline_h
+#define mozilla_dom_ScrollTimeline_h
+
+#include "AnimationTimeline.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/ScrollTimelineSet.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/Animation.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/ScrollTimelineBinding.h"
+#include "nsCSSValue.h"
+#include "nsIDocument.h"
+#include "nsIScrollPositionListener.h"
+
+struct JSContext;
+
+// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
+// GetTickCount().
+#ifdef GetCurrentTime
+#undef GetCurrentTime
+#endif
+
+namespace mozilla {
+namespace dom {
+
+class ScrollPositionListener : public nsIScrollPositionListener {
+public:
+  explicit ScrollPositionListener(ScrollTimeline* aTimeline) {
+    mTimeline = aTimeline;
+  }
+  virtual ~ScrollPositionListener() {}
+
+  void ScrollPositionWillChange(nscoord, nscoord) override {}
+  void ScrollPositionDidChange(nscoord, nscoord) override;
+private:
+  ScrollTimeline* mTimeline;
+};
+
+/**
+ * Implementation notes
+ * --------------------
+ *
+ * ScrollTimelines do not observe refreshes the way DocumentTimelines do.
+ * This is because the refresh driver keeps ticking while it has registered
+ * refresh observers. For a DocumentTimeline, it's appropriate to keep the
+ * refresh driver ticking as long as there are active animations, since the
+ * animations need to be sampled on every frame. Scroll-driven animations,
+ * however, only need to be sampled when scrolling has occurred, so keeping
+ * the refresh driver ticking is wasteful.
+ *
+ * As a result, we have the refresh driver call ScrollTimeline::Tick()
+ * directly whenever it ticks (without causing ticks itself). Additionally,
+ * in Tick() we only actually sample the animation if scrolling has occurred
+ * (which the scroll frame notifies us of by calling NotifyScroll()).
+ *
+ */
+class ScrollTimeline final
+  : public AnimationTimeline
+  , public LinkedListElement<ScrollTimeline>
+{
+public:
+  ScrollTimeline(Element* aScrollSource,
+                 const Optional<ScrollDirection>& aOrientation,
+                 const nsString& aStartScrollOffset,
+                 const nsString& aEndScrollOffset,
+                 const OwningDoubleOrScrollTimelineAutoKeyword& aTimeRange,
+                 FillMode aFillMode)
+    : mScrollPositionListener(MakeUnique<ScrollPositionListener>(this))
+    , mNeedsTick(false)
+    , mHaveFrame(false)
+    , mScrollSource(aScrollSource)
+    , mSpecifiedTimeRange(aTimeRange)
+    , mFillMode(aFillMode)
+      // TODO: If no orientation was passed, compute it based on what direction
+      //       |mElement| is scrollable in.
+    , mComputedOrientation(aOrientation.WasPassed()
+                             ? aOrientation.Value()
+                             : ScrollDirection::Vertical)
+    , mCurrentScroll(0)
+    , mMinScroll(0)
+    , mMaxScroll(0)
+  {
+    MOZ_ASSERT(mScrollSource);
+    mScrollSource->OwnerDoc()->ScrollTimelines().insertBack(this);
+    RegisterWithScrollSource();
+    if (nsIScrollableFrame* scrollFrame = GetScrollFrame()) {
+      RegisterWithScrollFrame(scrollFrame);
+    }
+    ParseProperties(aStartScrollOffset, aEndScrollOffset);
+    CalculateEffectiveTimeRange();
+    QueryScrollValues();
+  }
+
+protected:
+  virtual ~ScrollTimeline()
+  {
+    Teardown();
+  }
+
+public:
+  // Plumbing
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ScrollTimeline,
+                                                         AnimationTimeline)
+  nsIGlobalObject* GetParentObject() const { return mScrollSource->GetOwnerGlobal(); }
+  virtual JSObject* WrapObject(JSContext* aCx,
+                               JS::Handle<JSObject*> aGivenProto) override;
+
+  // AnimationTimeline methods
+  virtual Nullable<TimeDuration> GetCurrentTime() const override;
+  bool TracksWallclockTime() const override {
+    return false;
+  }
+  Nullable<TimeDuration> ToTimelineTime(const TimeStamp& aTimeStamp) const
+                                                                     override;
+  TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const override;
+  void NotifyAnimationUpdated(Animation& aAnimation) override;
+  void RemoveAnimation(Animation* aAnimation) override;
+  ScrollTimeline* AsScrollTimeline() override { return this; }
+  const ScrollTimeline* AsScrollTimeline() const override { return this; }
+
+  // WebIDL constructor
+  static already_AddRefed<ScrollTimeline>
+  Constructor(const GlobalObject& aGlobal,
+              const ScrollTimelineOptions& aOptions,
+              ErrorResult& aRv);
+
+  // WebIDL properties
+  Element* SourceElement() const { return mScrollSource; }
+  ScrollDirection Orientation() const { return mComputedOrientation; }
+  void GetStartScrollOffset(nsString& aOutResult) const;
+  void GetEndScrollOffset(nsString& aOutResult) const;
+  void GetTimeRange(OwningDoubleOrScrollTimelineAutoKeyword& aOutResult) const {
+    aOutResult = mSpecifiedTimeRange;
+  }
+  FillMode Fill() const {
+    return mFillMode;
+  }
+
+  // Types used in computed properties
+  struct ScrollRange {
+    nscoord mMinScroll;
+    nscoord mMaxScroll;
+  };
+
+  // Computed properties
+  const StickyTimeDuration& GetEffectiveTimeRange() const {
+    return mEffectiveTimeRange;
+  }
+  ScrollRange GetScrollRange();
+
+  // Other methods
+  void Tick();
+  void NotifyScroll();
+
+  void RegisterWithScrollFrame(nsIScrollableFrame* aScrollFrame);
+
+  void RemoveFromList();
+  void Teardown();
+
+protected:
+  nsIScrollableFrame* GetScrollFrame() const;
+
+  void CalculateEffectiveTimeRange();
+  void ParseProperties(const nsString& aSpecifiedStartScrollOffset,
+                       const nsString& aSpecifiedEndScrollOffset);
+  void QueryScrollValues();
+
+  void RegisterWithScrollSource();
+  void UnregisterFromScrollSource();
+
+  void RegisterWithScrollSourceAsOwned();
+  void UnregisterFromScrollSourceAsOwned();
+
+  // Infrastructure
+  nsCOMPtr<nsIDocument> mDocument;
+  UniquePtr<ScrollPositionListener> mScrollPositionListener;
+  bool mNeedsTick : 1;
+  bool mHaveFrame : 1;  // whether scrollSource currently has a scroll frame
+
+  // Raw versions of specified values of properties.
+  RefPtr<Element> mScrollSource;
+  // We don't save the specified start and end scroll offset; instead, we
+  // synthesize them from the computed offsets when necessary.
+  OwningDoubleOrScrollTimelineAutoKeyword mSpecifiedTimeRange;
+  FillMode mFillMode;
+
+  // Cooked versions of specified values of properties
+  Maybe<StickyTimeDuration> mSpecifiedTimeRangeCooked;
+
+  // Computed values of properties
+  ScrollDirection mComputedOrientation;
+  nsCSSValue mComputedStartScrollOffset;
+  nsCSSValue mComputedEndScrollOffset;
+  StickyTimeDuration mEffectiveTimeRange;
+  // These values are only valid if mHaveFrame is true.
+  nscoord mCurrentScroll, mMinScroll, mMaxScroll;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ScrollTimeline_h
new file mode 100644
--- /dev/null
+++ b/dom/animation/ScrollTimelineSet.cpp
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScrollTimelineSet.h"
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/ScrollTimeline.h"
+#include "nsCycleCollectionNoteChild.h"
+#include "nsGkAtoms.h"
+#include "nsINode.h"
+
+namespace mozilla {
+
+ScrollTimelineSet::ScrollTimelineSet()
+{
+  MOZ_COUNT_CTOR(ScrollTimelineSet);
+}
+
+ScrollTimelineSet::~ScrollTimelineSet()
+{
+  MOZ_COUNT_DTOR(ScrollTimelineSet);
+}
+
+void
+ScrollTimelineSet::Traverse(nsCycleCollectionTraversalCallback& aCallback)
+{
+  for (auto iter = mOwnedScrollTimelines.Iter(); !iter.Done(); iter.Next()) {
+    CycleCollectionNoteChild(aCallback, iter.Get()->GetKey(),
+                             "ScrollTimelineSet::mOwnedScrollTimelines[]",
+                             aCallback.Flags());
+  }
+}
+
+/* static */ ScrollTimelineSet*
+ScrollTimelineSet::GetScrollTimelineSet(dom::Element* aElement)
+{
+  return static_cast<ScrollTimelineSet*>(
+      aElement->GetProperty(nsGkAtoms::scrollTimelinesProperty));
+}
+
+/* static */ ScrollTimelineSet*
+ScrollTimelineSet::GetOrCreateScrollTimelineSet(dom::Element* aElement)
+{
+  ScrollTimelineSet* scrollTimelineSet = GetScrollTimelineSet(aElement);
+  if (scrollTimelineSet) {
+    return scrollTimelineSet;
+  }
+
+  scrollTimelineSet = new ScrollTimelineSet();
+
+  nsresult rv = aElement->SetProperty(nsGkAtoms::scrollTimelinesProperty,
+                                      scrollTimelineSet,
+                                      nsINode::DeleteProperty<ScrollTimelineSet>,
+                                      true);
+  if (NS_FAILED(rv)) {
+    NS_WARNING("SetProperty failed");
+    delete scrollTimelineSet;
+    return nullptr;
+  }
+
+  return scrollTimelineSet;
+}
+
+/* static */ void
+ScrollTimelineSet::DestroyScrollTimelineSet(dom::Element* aElement)
+{
+  aElement->DeleteProperty(nsGkAtoms::scrollTimelinesProperty);
+}
+
+void
+ScrollTimelineSet::AddScrollTimeline(dom::ScrollTimeline& aScrollTimeline)
+{
+  if (mAllScrollTimelines.Contains(&aScrollTimeline)) {
+    return;
+  }
+
+  mAllScrollTimelines.PutEntry(&aScrollTimeline);
+}
+
+void
+ScrollTimelineSet::RemoveScrollTimeline(dom::ScrollTimeline& aScrollTimeline)
+{
+  if (!mAllScrollTimelines.Contains(&aScrollTimeline)) {
+    return;
+  }
+
+  mAllScrollTimelines.RemoveEntry(&aScrollTimeline);
+}
+
+void
+ScrollTimelineSet::AddOwnedScrollTimeline(dom::ScrollTimeline& aScrollTimeline)
+{
+  if (mOwnedScrollTimelines.Contains(&aScrollTimeline)) {
+    return;
+  }
+
+  mOwnedScrollTimelines.PutEntry(&aScrollTimeline);
+}
+
+void
+ScrollTimelineSet::RemoveOwnedScrollTimeline(dom::ScrollTimeline& aScrollTimeline)
+{
+  if (!mOwnedScrollTimelines.Contains(&aScrollTimeline)) {
+    return;
+  }
+
+  mOwnedScrollTimelines.RemoveEntry(&aScrollTimeline);
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/animation/ScrollTimelineSet.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_ScrollTimelineSet_h
+#define mozilla_ScrollTimelineSet_h
+
+#include "nsCycleCollectionTraversalCallback.h"
+#include "nsHashKeys.h"
+#include "nsTHashtable.h"
+
+class nsPresContext;
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+class ScrollTimeline;
+} // namespace dom
+
+// A wrapper around a hashset of ScrollTimeline objects to handle
+// storing the set as a property of an element.
+// This class has two purposes:
+//  - To allow iterating over all of the ScrollTimelines that gave a
+//    given element as their scrollSource.
+//    This is the purpose of mAllScrollTimelines.
+//  - To keep a ScrollTimeline alive while its scrollSource element is
+//    alive and it has animations associated with it.
+//    This is the purpose of mOwnedScrollTimelines.
+// TODO: Since the timelines in mOwnedScrollTimelines are a subset of the
+//       timelines in mAllScrollTimelines, we are storing the timelines in
+//       mOwnedScrollTimelines redundantly. Perhaps we should just store
+//       (plain pointer, is owned?) pairs, and call AddRef() and Release()
+//       directly where appropriate?
+class ScrollTimelineSet
+{
+private:
+  typedef nsTHashtable<nsRefPtrHashKey<dom::ScrollTimeline>>
+    OwningScrollTimelineSet;
+  typedef nsTHashtable<nsPtrHashKey<dom::ScrollTimeline>>
+    NonOwningScrollTimelineSet;
+public:
+  ScrollTimelineSet();
+  ~ScrollTimelineSet();
+
+  // Methods for supporting cycle-collection
+  void Traverse(nsCycleCollectionTraversalCallback& aCallback);
+
+  static ScrollTimelineSet* GetScrollTimelineSet(dom::Element* aElement);
+  static ScrollTimelineSet* GetOrCreateScrollTimelineSet(dom::Element* aElement);
+  static void DestroyScrollTimelineSet(dom::Element* aElement);
+
+  void AddScrollTimeline(dom::ScrollTimeline& aScrollTimeline);
+  void RemoveScrollTimeline(dom::ScrollTimeline& aScrollTimeline);
+
+  const NonOwningScrollTimelineSet& GetAllScrollTimelines() const {
+    return mAllScrollTimelines;
+  }
+
+  void AddOwnedScrollTimeline(dom::ScrollTimeline& aScrollTimeline);
+  void RemoveOwnedScrollTimeline(dom::ScrollTimeline& aScrollTimeline);
+
+  bool IsEmpty() const {
+    return mOwnedScrollTimelines.IsEmpty() && mAllScrollTimelines.IsEmpty();
+  }
+
+private:
+  OwningScrollTimelineSet mOwnedScrollTimelines;
+  NonOwningScrollTimelineSet mAllScrollTimelines;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ScrollTimelineSet_h
new file mode 100644
--- /dev/null
+++ b/dom/animation/ScrollTimelineUtils.cpp
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScrollTimelineUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+Nullable<TimeDuration>
+ScrollTimelineUtils::CalculateCurrentTime(nscoord aCurrentScroll,
+                                          nscoord aMinScroll,
+                                          nscoord aMaxScroll,
+                                          const TimeDuration& aEffectiveTimeRange,
+                                          FillMode aFillMode)
+{
+  Nullable<TimeDuration> result;  // Initializes to null
+  if (aCurrentScroll < aMinScroll) {
+    if (aFillMode == FillMode::Both || aFillMode == FillMode::Backwards) {
+      result.SetValue(TimeDuration());
+    }
+    return result;
+  }
+  if (aCurrentScroll >= aMaxScroll) {
+    if (aFillMode == FillMode::Both || aFillMode == FillMode::Forwards) {
+      result.SetValue(aEffectiveTimeRange);
+    }
+    return result;
+  }
+  double scrollPosition = CSSPixel::FromAppUnits(aCurrentScroll - aMinScroll);
+  double scrollRange = CSSPixel::FromAppUnits(aMaxScroll - aMinScroll);
+  MOZ_ASSERT(aEffectiveTimeRange != TimeDuration::Forever());
+  if (scrollRange != 0) {
+    result.SetValue(
+        TimeDuration(aEffectiveTimeRange.MultDouble(scrollPosition / scrollRange)));
+  }
+  return result;
+}
+
+}  // namespace dom
+}  // namespace mozilla
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/animation/ScrollTimelineUtils.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ScrollTimelineUtils_h
+#define mozilla_dom_ScrollTimelineUtils_h
+
+#include "Units.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/Nullable.h"
+
+// The purpose of this class to house helper functions that are shared
+// by the main-thread implementation of scroll timelines (ScrollTimeline.cpp),
+// and the OMTA implementation (AsyncCompositionManager.cpp).
+
+namespace mozilla {
+namespace dom {
+
+struct ScrollTimelineUtils {
+  /**
+   * Calculate the current time of a scroll-driven animation.
+   * @param aCurrentScroll the current scroll offset, in app units
+   * @param aMinScroll the start scroll offset, in app units
+   * @param aMaxScroll the end scroll offset, in app units
+   * @param aEffectiveTimeRange the scroll timeline's effective time range
+   * @param aFillMode the scroll timeline's fill mode
+   * @return the calculated current time
+   */
+  Nullable<TimeDuration>
+  static CalculateCurrentTime(nscoord aCurrentScroll,
+                              nscoord aMinScroll,
+                              nscoord aMaxScroll,
+                              const TimeDuration& aEffectiveTimeRange,
+                              FillMode aFillMode);
+};
+
+}  // namespace dom
+}  // namespace mozilla
+
+#endif // mozilla_dom_ScrollTimelineUtils_h
--- a/dom/animation/moz.build
+++ b/dom/animation/moz.build
@@ -15,16 +15,18 @@ EXPORTS.mozilla.dom += [
     'AnimationEffectReadOnly.h',
     'AnimationEffectTiming.h',
     'AnimationEffectTimingReadOnly.h',
     'AnimationTimeline.h',
     'CSSPseudoElement.h',
     'DocumentTimeline.h',
     'KeyframeEffect.h',
     'KeyframeEffectReadOnly.h',
+    'ScrollTimeline.h',
+    'ScrollTimelineUtils.h',
 ]
 
 EXPORTS.mozilla += [
     'AnimationComparator.h',
     'AnimationPerformanceWarning.h',
     'AnimationTarget.h',
     'AnimationUtils.h',
     'AnimValuesStyleRule.h',
@@ -32,16 +34,17 @@ EXPORTS.mozilla += [
     'ComputedTimingFunction.h',
     'EffectCompositor.h',
     'EffectSet.h',
     'Keyframe.h',
     'KeyframeEffectParams.h',
     'KeyframeUtils.h',
     'PendingAnimationTracker.h',
     'PseudoElementHashEntry.h',
+    'ScrollTimelineSet.h',
     'TimingParams.h',
 ]
 
 UNIFIED_SOURCES += [
     'Animation.cpp',
     'AnimationEffectReadOnly.cpp',
     'AnimationEffectTiming.cpp',
     'AnimationEffectTimingReadOnly.cpp',
@@ -54,16 +57,19 @@ UNIFIED_SOURCES += [
     'DocumentTimeline.cpp',
     'EffectCompositor.cpp',
     'EffectSet.cpp',
     'KeyframeEffect.cpp',
     'KeyframeEffectParams.cpp',
     'KeyframeEffectReadOnly.cpp',
     'KeyframeUtils.cpp',
     'PendingAnimationTracker.cpp',
+    'ScrollTimeline.cpp',
+    'ScrollTimelineSet.cpp',
+    'ScrollTimelineUtils.cpp',
     'TimingParams.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '/dom/base',
     '/layout/base',
     '/layout/style',
 ]
--- a/dom/base/FragmentOrElement.cpp
+++ b/dom/base/FragmentOrElement.cpp
@@ -18,16 +18,17 @@
 #include "mozilla/dom/FragmentOrElement.h"
 
 #include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/DeclarationBlockInlines.h"
 #include "mozilla/EffectSet.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/EventListenerManager.h"
 #include "mozilla/EventStates.h"
+#include "mozilla/ScrollTimelineSet.h"
 #include "mozilla/ServoRestyleManager.h"
 #include "mozilla/dom/Attr.h"
 #include "nsDOMAttributeMap.h"
 #include "nsIAtom.h"
 #include "mozilla/dom/NodeInfo.h"
 #include "mozilla/dom/Event.h"
 #include "nsIDocumentInlines.h"
 #include "nsIDocumentEncoder.h"
@@ -1369,16 +1370,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Fr
         tmp->DeleteProperty(*props[i]);
       }
       if (tmp->MayHaveAnimations()) {
         nsIAtom** effectProps = EffectSet::GetEffectSetPropertyAtoms();
         for (uint32_t i = 0; effectProps[i]; ++i) {
           tmp->DeleteProperty(effectProps[i]);
         }
       }
+      tmp->DeleteProperty(nsGkAtoms::scrollTimelinesProperty);
     }
   }
 
   // Unlink child content (and unbind our subtree).
   if (tmp->UnoptimizableCCNode() || !nsCCUncollectableMarker::sGeneration) {
     uint32_t childCount = tmp->mAttrsAndChildren.ChildCount();
     if (childCount) {
       // Don't allow script to run while we're unbinding everything.
@@ -1954,16 +1956,20 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
         for (uint32_t i = 0; effectProps[i]; ++i) {
           EffectSet* effectSet =
             static_cast<EffectSet*>(tmp->GetProperty(effectProps[i]));
           if (effectSet) {
             effectSet->Traverse(cb);
           }
         }
       }
+      if (ScrollTimelineSet* scrollTimelineSet =
+          static_cast<ScrollTimelineSet*>(tmp->GetProperty(nsGkAtoms::scrollTimelinesProperty))) {
+        scrollTimelineSet->Traverse(cb);
+      }
     }
   }
 
   // Traverse attribute names and child content.
   {
     uint32_t i;
     uint32_t attrs = tmp->mAttrsAndChildren.AttrCount();
     for (i = 0; i < attrs; i++) {
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -213,16 +213,17 @@
 #include "mozilla/dom/DocumentTimeline.h"
 #include "mozilla/dom/Event.h"
 #include "mozilla/dom/HTMLBodyElement.h"
 #include "mozilla/dom/HTMLInputElement.h"
 #include "mozilla/dom/ImageTracker.h"
 #include "mozilla/dom/MediaQueryList.h"
 #include "mozilla/dom/NodeFilterBinding.h"
 #include "mozilla/OwningNonNull.h"
+#include "mozilla/dom/ScrollTimeline.h"
 #include "mozilla/dom/TabChild.h"
 #include "mozilla/dom/WebComponentsBinding.h"
 #include "mozilla/dom/CustomElementRegistryBinding.h"
 #include "mozilla/dom/CustomElementRegistry.h"
 #include "nsFrame.h"
 #include "nsDOMCaretPosition.h"
 #include "nsIDOMHTMLTextAreaElement.h"
 #include "nsViewportInfo.h"
@@ -1553,16 +1554,17 @@ nsDocument::~nsDocument()
     mStyleSheetSetList->Disconnect();
   }
 
   if (mAnimationController) {
     mAnimationController->Disconnect();
   }
 
   MOZ_ASSERT(mTimelines.isEmpty());
+  MOZ_ASSERT(mScrollTimelines.isEmpty());
 
   mParentDocument = nullptr;
 
   // Kill the subdocument map, doing this will release its strong
   // references, if any.
   delete mSubDocuments;
   mSubDocuments = nullptr;
 
@@ -3179,16 +3181,24 @@ nsDocument::Timeline()
   if (!mDocumentTimeline) {
     mDocumentTimeline = new DocumentTimeline(this, TimeDuration(0));
   }
 
   return mDocumentTimeline;
 }
 
 void
+nsDocument::TickScrollTimelines()
+{
+  for (ScrollTimeline* timeline : mScrollTimelines) {
+    timeline->Tick();
+  }
+}
+
+void
 nsDocument::GetAnimations(nsTArray<RefPtr<Animation>>& aAnimations)
 {
   // Hold a strong ref for the root element since Element::GetAnimations() calls
   // FlushPendingNotifications() which may destroy the element.
   RefPtr<Element> root = GetRootElement();
   if (!root) {
     return;
   }
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -608,16 +608,21 @@ public:
   static bool IsWebAnimationsEnabled(JSContext* aCx, JSObject* aObject);
   virtual mozilla::dom::DocumentTimeline* Timeline() override;
   virtual void GetAnimations(
       nsTArray<RefPtr<mozilla::dom::Animation>>& aAnimations) override;
   mozilla::LinkedList<mozilla::dom::DocumentTimeline>& Timelines() override
   {
     return mTimelines;
   }
+  mozilla::LinkedList<mozilla::dom::ScrollTimeline>& ScrollTimelines() override
+  {
+    return mScrollTimelines;
+  }
+  void TickScrollTimelines() override;
 
   virtual nsresult SetSubDocumentFor(Element* aContent,
                                      nsIDocument* aSubDoc) override;
   virtual nsIDocument* GetSubDocumentFor(nsIContent* aContent) const override;
   virtual Element* FindContentForSubDocument(nsIDocument *aDocument) const override;
   virtual Element* GetRootElementInternal() const override;
 
   virtual void EnsureOnDemandBuiltInUASheet(mozilla::StyleSheet* aSheet) override;
@@ -1617,16 +1622,17 @@ private:
   uint8_t mScrolledToRefAlready : 1;
   uint8_t mChangeScrollPosWhenScrollingToRef : 1;
 
   // Tracking for plugins in the document.
   nsTHashtable< nsPtrHashKey<nsIObjectLoadingContent> > mPlugins;
 
   RefPtr<mozilla::dom::DocumentTimeline> mDocumentTimeline;
   mozilla::LinkedList<mozilla::dom::DocumentTimeline> mTimelines;
+  mozilla::LinkedList<mozilla::dom::ScrollTimeline> mScrollTimelines;
 
   enum ViewportType {
     DisplayWidthHeight,
     Specified,
     Unknown
   };
 
   ViewportType mViewportType;
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -2153,16 +2153,17 @@ GK_ATOM(transitionsOfBeforeProperty, "Tr
 GK_ATOM(transitionsOfAfterProperty, "TransitionsOfAfterProperty") // FrameTransitions*
 GK_ATOM(genConInitializerProperty, "QuoteNodeProperty")
 GK_ATOM(labelMouseDownPtProperty, "LabelMouseDownPtProperty")
 GK_ATOM(lockedStyleStates, "lockedStyleStates")
 GK_ATOM(apzCallbackTransform, "apzCallbackTransform")
 GK_ATOM(restylableAnonymousNode, "restylableAnonymousNode")
 GK_ATOM(paintRequestTime, "PaintRequestTime")
 GK_ATOM(pseudoProperty, "PseudoProperty")  // CSSPseudoElementType
+GK_ATOM(scrollTimelinesProperty, "ScrollTimelinesProperty")  // ScrollTimelineSet*
 
 // Languages for lang-specific transforms
 GK_ATOM(Japanese, "ja")
 GK_ATOM(Chinese, "zh-CN")
 GK_ATOM(Taiwanese, "zh-TW")
 GK_ATOM(HongKongChinese, "zh-HK")
 GK_ATOM(Unicode, "x-unicode")
 
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -149,16 +149,17 @@ class Link;
 class Location;
 class MediaQueryList;
 class GlobalObject;
 class NodeFilter;
 class NodeIterator;
 enum class OrientationType : uint8_t;
 class ProcessingInstruction;
 class Promise;
+class ScrollTimeline;
 class StyleSheetList;
 class SVGDocument;
 class SVGSVGElement;
 class Touch;
 class TouchList;
 class TreeWalker;
 class XPathEvaluator;
 class XPathExpression;
@@ -2409,16 +2410,19 @@ public:
    * elements set using mozSetImageElement have higher priority.
    * @param aId the ID associated the element we want to lookup
    * @return the element associated with |aId|
    */
   virtual Element* LookupImageElement(const nsAString& aElementId) = 0;
 
   virtual mozilla::dom::DocumentTimeline* Timeline() = 0;
   virtual mozilla::LinkedList<mozilla::dom::DocumentTimeline>& Timelines() = 0;
+  virtual mozilla::LinkedList<mozilla::dom::ScrollTimeline>& ScrollTimelines() = 0;
+
+  virtual void TickScrollTimelines() = 0;
 
   virtual void GetAnimations(
       nsTArray<RefPtr<mozilla::dom::Animation>>& aAnimations) = 0;
 
   mozilla::dom::SVGSVGElement* GetSVGRootElement() const;
 
   nsresult ScheduleFrameRequestCallback(mozilla::dom::FrameRequestCallback& aCallback,
                                         int32_t *aHandle);
--- a/dom/base/nsNodeUtils.cpp
+++ b/dom/base/nsNodeUtils.cpp
@@ -25,16 +25,17 @@
 #include "nsBindingManager.h"
 #include "nsGenericHTMLElement.h"
 #include "mozilla/AnimationTarget.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/dom/Animation.h"
 #include "mozilla/dom/HTMLImageElement.h"
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "mozilla/dom/KeyframeEffectReadOnly.h"
+#include "mozilla/dom/ScrollTimeline.h"
 #include "nsWrapperCacheInlines.h"
 #include "nsObjectLoadingContent.h"
 #include "nsDOMMutationObserver.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/HTMLTemplateElement.h"
 #include "mozilla/dom/ShadowRoot.h"
 
 using namespace mozilla;
@@ -565,16 +566,29 @@ nsNodeUtils::CloneAndAdopt(nsINode *aNod
     if (oldDoc != newDoc && oldDoc->MayHaveDOMMutationObservers()) {
       newDoc->SetMayHaveDOMMutationObservers();
     }
 
     if (oldDoc != newDoc && oldDoc->MayHaveAnimationObservers()) {
       newDoc->SetMayHaveAnimationObservers();
     }
 
+    if (oldDoc != newDoc && aNode->IsElement()) {
+      // Transfer element's scroll timelines from old document to new document.
+      if (ScrollTimelineSet* scrollTimelineSet =
+          ScrollTimelineSet::GetScrollTimelineSet(aNode->AsElement())) {
+        const auto& scrollTimelines = scrollTimelineSet->GetAllScrollTimelines();
+        for (auto iter = scrollTimelines.ConstIter(); !iter.Done(); iter.Next()) {
+           ScrollTimeline* timeline= iter.Get()->GetKey();
+           timeline->RemoveFromList();
+           newDoc->ScrollTimelines().insertBack(timeline);
+        }
+      }
+    }
+
     if (elem) {
       elem->RecompileScriptEventListeners();
     }
 
     if (aReparentScope) {
       AutoJSContext cx;
       JS::Rooted<JSObject*> wrapper(cx);
       if ((wrapper = aNode->GetWrapper())) {
new file mode 100644
--- /dev/null
+++ b/dom/webidl/ScrollTimeline.webidl
@@ -0,0 +1,35 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://wicg.github.io/scroll-animations/#scrolltimeline
+ *
+ * Copyright © 2016 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
+ * liability, trademark and document use rules apply.
+ */
+
+enum ScrollDirection { "vertical", "horizontal" };
+
+enum ScrollTimelineAutoKeyword { "auto" };
+
+dictionary ScrollTimelineOptions {
+  Element scrollSource;
+  ScrollDirection orientation;
+  DOMString startScrollOffset = "auto";
+  DOMString endScrollOffset = "auto";
+  (double or ScrollTimelineAutoKeyword) timeRange = "auto";
+  FillMode fillMode = "none";
+};
+
+[Func="nsDocument::IsWebAnimationsEnabled",
+ Constructor(optional ScrollTimelineOptions options)]
+interface ScrollTimeline : AnimationTimeline {
+  readonly attribute Element sourceElement;
+  readonly attribute ScrollDirection orientation;
+  readonly attribute DOMString startScrollOffset;
+  readonly attribute DOMString endScrollOffset;
+  readonly attribute (double or ScrollTimelineAutoKeyword) timeRange;
+  readonly attribute FillMode fill;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -762,16 +762,17 @@ WEBIDL_FILES = [
     'Response.webidl',
     'RGBColor.webidl',
     'RTCStatsReport.webidl',
     'Screen.webidl',
     'ScreenOrientation.webidl',
     'ScriptProcessorNode.webidl',
     'ScrollAreaEvent.webidl',
     'ScrollBoxObject.webidl',
+    'ScrollTimeline.webidl',
     'Selection.webidl',
     'ServiceWorker.webidl',
     'ServiceWorkerContainer.webidl',
     'ServiceWorkerGlobalScope.webidl',
     'ServiceWorkerRegistration.webidl',
     'SettingChangeNotification.webidl',
     'ShadowRoot.webidl',
     'SharedWorker.webidl',
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -1616,16 +1616,42 @@ CollectDocuments(nsIDocument* aDocument,
 {
   static_cast<AutoTArray<nsCOMPtr<nsIDocument>, 32>*>(aDocArray)->
     AppendElement(aDocument);
   aDocument->EnumerateSubDocuments(CollectDocuments, aDocArray);
   return true;
 }
 
 void
+nsRefreshDriver::TickScrollTimelines()
+{
+  if (!mPresContext) {
+    return;
+  }
+
+  nsCOMArray<nsIDocument> documents;
+  CollectDocuments(mPresContext->Document(), &documents);
+
+  for (int32_t i = 0; i < documents.Count(); ++i) {
+    nsIDocument* doc = documents[i];
+    nsIPresShell* shell = doc->GetShell();
+    if (!shell) {
+      continue;
+    }
+
+    RefPtr<nsPresContext> context = shell->GetPresContext();
+    if (!context || context->RefreshDriver() != this) {
+      continue;
+    }
+
+    doc->TickScrollTimelines();
+  }
+}
+
+void
 nsRefreshDriver::DispatchAnimationEvents()
 {
   if (!mPresContext) {
     return;
   }
 
   AutoTArray<nsCOMPtr<nsIDocument>, 32> documents;
   CollectDocuments(mPresContext->Document(), &documents);
@@ -1835,16 +1861,17 @@ nsRefreshDriver::Tick(int64_t aNowEpoch,
         StopTimer();
         return;
       }
     }
 
     if (i == 0) {
       // This is the FlushType::Style case.
 
+      TickScrollTimelines();
       DispatchAnimationEvents();
       DispatchPendingEvents();
       RunFrameRequestCallbacks(aNowTime);
 
       if (mPresContext && mPresContext->GetPresShell()) {
         Maybe<GeckoProfilerTracingRAII> tracingStyleFlush;
         AutoTArray<nsIPresShell*, 16> observers;
         observers.AppendElements(mStyleFlushObservers);
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -369,16 +369,17 @@ private:
     {
     }
 
     mozilla::Maybe<mozilla::TimeStamp> mStartTime;
     RequestTable mEntries;
   };
   typedef nsClassHashtable<nsUint32HashKey, ImageStartData> ImageStartTable;
 
+  void TickScrollTimelines();
   void DispatchPendingEvents();
   void DispatchAnimationEvents();
   void RunFrameRequestCallbacks(mozilla::TimeStamp aNowTime);
   void Tick(int64_t aNowEpoch, mozilla::TimeStamp aNowTime);
 
   enum EnsureTimerStartedFlags {
     eNone = 0,
     eForceAdjustTimer = 1 << 0,
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -34,17 +34,19 @@
 #include "nsContentUtils.h"
 #include "nsLayoutUtils.h"
 #include "nsBidiPresUtils.h"
 #include "nsBidiUtils.h"
 #include "mozilla/ContentEvents.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/LookAndFeel.h"
+#include "mozilla/ScrollTimelineSet.h"
 #include "mozilla/dom/Element.h"
+#include "mozilla/dom/ScrollTimeline.h"
 #include <stdint.h>
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/Telemetry.h"
 #include "FrameLayerBuilder.h"
 #include "nsSMILKeySpline.h"
 #include "nsSubDocumentFrame.h"
 #include "nsSVGOuterSVGFrame.h"
 #include "nsIObjectLoadingContent.h"
@@ -2071,16 +2073,34 @@ ScrollFrameHelper::ScrollFrameHelper(nsC
                                          mOuter->PresContext()->PresShell(),
                                          ScreenMargin(),
                                          0,
                                          nsLayoutUtils::RepaintMode::DoNotRepaint);
     nsLayoutUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
         mOuter, nsLayoutUtils::RepaintMode::DoNotRepaint);
   }
 
+  // Register any scroll timelines associated with our content element, with
+  // this scroll frame. The timeline registers itself when it's constructed,
+  // but the frame can be reconstructed over the lifetime of the content
+  // element.
+  nsIScrollableFrame* sf = do_QueryFrame(mOuter);
+  MOZ_ASSERT(sf);
+  if (nsIContent* content = mOuter->GetContent()) {
+    if (content->IsElement()) {
+      if (ScrollTimelineSet* scrollTimelineSet =
+          ScrollTimelineSet::GetScrollTimelineSet(content->AsElement())) {
+        const auto& scrollTimelines = scrollTimelineSet->GetAllScrollTimelines();
+        for (auto iter = scrollTimelines.ConstIter(); !iter.Done(); iter.Next()) {
+           ScrollTimeline* timeline= iter.Get()->GetKey();
+           timeline->RegisterWithScrollFrame(sf);
+        }
+      }
+    }
+  }
 }
 
 ScrollFrameHelper::~ScrollFrameHelper()
 {
 }
 
 /*
  * Callback function from AsyncSmoothMSDScroll, used in ScrollFrameHelper::ScrollTo