Bug 1321428 - Introduce ScrollTimeline, a new kind of AnimationTimeline for scroll-driven animations. r=birtles,bz draft
authorBotond Ballo <botond@mozilla.com>
Fri, 02 Dec 2016 17:24:23 -0500
changeset 447198 23e6a2b51c4d50be839e54309c3e4bd86dc1e34a
parent 447181 5b752200fec4c72e4c532aeba142d07bc6470a39
child 447199 7dc597847bf25d69d684b24ebe5c384c4904e4b7
push id38013
push userbballo@mozilla.com
push dateFri, 02 Dec 2016 22:28:32 +0000
reviewersbirtles, bz
bugs1321428
milestone53.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.h
dom/animation/ScrollTimeline.cpp
dom/animation/ScrollTimeline.h
dom/animation/ScrollTimelineUtils.cpp
dom/animation/ScrollTimelineUtils.h
dom/animation/moz.build
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/base/nsIDocument.h
dom/webidl/ScrollTimeline.webidl
dom/webidl/moz.build
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -320,16 +320,17 @@ public:
    * if any.
    * Any properties contained in |aPropertiesToSkip| will not be added or
    * updated in |aStyleRule|.
    */
   void ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
                     const nsCSSPropertyIDSet& aPropertiesToSkip);
 
   void NotifyEffectTimingUpdated();
+  StickyTimeDuration EffectEnd() const;
 
 protected:
   void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime);
   void SilentlySetPlaybackRate(double aPlaybackRate);
   void CancelNoUpdate();
   void PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior);
   void PauseNoUpdate(ErrorResult& aRv);
   void ResumeAt(const TimeDuration& aReadyTime);
@@ -381,17 +382,16 @@ protected:
 
   /**
    * Performs the same steps as CancelPendingTasks and also rejects and
    * recreates the ready promise if the animation was pending.
    */
   void ResetPendingTasks();
 
   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.h
+++ b/dom/animation/AnimationTimeline.h
@@ -22,16 +22,17 @@
 #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)
@@ -99,16 +100,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.
   //
new file mode 100644
--- /dev/null
+++ b/dom/animation/ScrollTimeline.cpp
@@ -0,0 +1,284 @@
+/* -*- 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(mElement)
+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(mElement)
+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::Teardown()
+{
+  if (nsIScrollableFrame* scrollFrame = GetScrollFrame()) {
+    scrollFrame->RemoveScrollPositionListener(mScrollPositionListener.get());
+  } else {
+  }
+  if (isInList()) {
+    remove();
+  }
+}
+
+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;
+  }
+
+  if (!aOptions.mScrollSource.WasPassed()) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  RefPtr<ScrollTimeline> timeline = new ScrollTimeline(doc,
+                                                       &aOptions.mScrollSource.Value(),
+                                                       aOptions.mOrientation,
+                                                       aOptions.mStartScrollOffset,
+                                                       aOptions.mEndScrollOffset,
+                                                       aOptions.mTimeRange,
+                                                       aOptions.mFillMode);
+  return timeline.forget();
+}
+
+Nullable<TimeDuration>
+ScrollTimeline::GetCurrentTime() const
+{
+  return ScrollTimelineUtils::CalculateCurrentTime(mCurrentScroll,
+      mMinScroll, mMaxScroll, TimeDuration(mEffectiveTimeRange), mFillMode);
+}
+
+Nullable<TimeDuration>
+ScrollTimeline::ToTimelineTime(const TimeStamp& aTimeStamp) const
+{
+  // Memo: We don't concern about clock time and zero time.
+  Nullable<TimeDuration> result; // Initializes to null
+  if (aTimeStamp.IsNull()) {
+    return result;
+  }
+
+  RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
+  if (MOZ_UNLIKELY(!timing)) {
+    return result;
+  }
+
+  result.SetValue(aTimeStamp
+                  - timing->GetNavigationStartTimeStamp());
+  return result;
+}
+
+// memo: This function is member function of AnimationTimeline.
+void
+ScrollTimeline::NotifyAnimationUpdated(Animation& aAnimation)
+{
+  // MEMO: Should I pause animation when setting animation?
+  AnimationTimeline::NotifyAnimationUpdated(aAnimation);
+  CalculateEffectiveTimeRange();
+}
+
+void
+ScrollTimeline::RemoveAnimation(Animation* aAnimation)
+{
+  AnimationTimeline::RemoveAnimation(aAnimation);
+  CalculateEffectiveTimeRange();
+}
+
+TimeStamp
+ScrollTimeline::ToTimeStamp(const TimeDuration& aTimeDuration) const
+{
+  TimeStamp result;
+  RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
+  if (MOZ_UNLIKELY(!timing)) {
+    return result;
+  }
+
+  result =
+    timing->GetNavigationStartTimeStamp() + aTimeDuration;
+  return result;
+}
+
+void
+ScrollTimeline::CalculateEffectiveTimeRange()
+{
+  if (mSpecifiedTimeRangeCooked.isSome()) {
+    mEffectiveTimeRange = mSpecifiedTimeRangeCooked.ref();
+  } else {
+    mEffectiveTimeRange = 0;
+    for (Animation* animation : mAnimationOrder) {
+      StickyTimeDuration endTime = animation->EffectEnd();
+      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;
+  }
+}
+
+void
+ScrollTimeline::QueryScrollValues()
+{
+  if (nsIScrollableFrame* frame = GetScrollFrame()) {
+    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;
+}
+
+void
+ScrollTimeline::Tick() {
+  if (!mNeedsTick) {
+    return;
+  }
+
+  QueryScrollValues();
+
+  nsTArray<Animation*> animationsToRemove(mAnimations.Count());
+
+  nsAutoAnimationMutationBatch mb(mDocument);
+
+  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 (!mElement) {
+    return nullptr;
+  }
+  return nsLayoutUtils::FindScrollableFrameFor(
+      nsLayoutUtils::FindOrCreateIDFor(mElement.get()));
+}
+
+void
+ScrollTimeline::ParseProperties()
+{
+  // Parse scroll offsets
+  nsIDocument* doc = mElement->OwnerDoc();
+  nsCSSParser parser(doc->CSSLoader());
+  // Allow the same formats as for the "width" property.
+  parser.ParseLonghandProperty(nsCSSPropertyID::eCSSProperty_width,
+                               mSpecifiedStartScrollOffset,
+                               doc->GetDocumentURI(),
+                               doc->GetDocumentURI(),
+                               doc->NodePrincipal(),
+                               mComputedStartScrollOffset);
+  parser.ParseLonghandProperty(nsCSSPropertyID::eCSSProperty_width,
+                               mSpecifiedEndScrollOffset,
+                               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
+ScrollPositionListener::ScrollPositionDidChange(nscoord, nscoord)
+{
+  mTimeline->NotifyScroll();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/animation/ScrollTimeline.h
@@ -0,0 +1,189 @@
+/* -*- 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 "mozilla/TimeStamp.h"
+#include "mozilla/AnimationTarget.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/ScrollTimelineBinding.h"
+#include "AnimationTimeline.h"
+#include "nsCSSValue.h"
+#include "nsIDocument.h"
+#include "nsIScrollPositionListener.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Animation.h"
+#include "mozilla/LinkedList.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;
+};
+
+class ScrollTimeline final
+  : public AnimationTimeline
+  , public LinkedListElement<ScrollTimeline>
+{
+public:
+  ScrollTimeline(nsIDocument* aDocument,
+                 Element* aTarget,
+                 const Optional<ScrollDirection>& aOrientation,
+                 const nsString& aStartScrollOffset,
+                 const nsString& aEndScrollOffset,
+                 const OwningDoubleOrScrollTimelineAutoKeyword& aTimeRange,
+                 FillMode aFillMode)
+    : AnimationTimeline(aDocument->GetParentObject())
+    , mDocument(aDocument)
+    , mScrollPositionListener(MakeUnique<ScrollPositionListener>(this))
+    , mElement(aTarget)
+    , mSpecifiedStartScrollOffset(aStartScrollOffset)
+    , mSpecifiedEndScrollOffset(aEndScrollOffset)
+    , 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)
+  {
+    if (mDocument) {
+      mDocument->ScrollTimelines().insertBack(this);
+    }
+    if (nsIScrollableFrame* scrollFrame = GetScrollFrame()) {
+      scrollFrame->AddScrollPositionListener(mScrollPositionListener.get());
+    }
+    ParseProperties();
+    CalculateEffectiveTimeRange();
+    QueryScrollValues();
+  }
+
+protected:
+  virtual ~ScrollTimeline()
+  {
+    // Note: cleanup happens in Teardown() which is called by
+    //       cycleCollection::Unlink().
+  }
+
+public:
+  // Plumbing
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ScrollTimeline,
+                                                         AnimationTimeline)
+  virtual JSObject* WrapObject(JSContext* aCx,
+                               JS::Handle<JSObject*> aGivenProto) override;
+
+  // AnimationTimeline methods
+  virtual Nullable<TimeDuration> GetCurrentTime() const override;
+  bool TracksWallclockTime() const override {
+    return true;
+  }
+  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 mElement.get(); }
+  ScrollDirection Orientation() const { return mComputedOrientation; }
+  void GetStartScrollOffset(nsString& aOutResult) const {
+    aOutResult = mSpecifiedStartScrollOffset;
+  }
+  void GetEndScrollOffset(nsString& aOutResult) const {
+    aOutResult = mSpecifiedEndScrollOffset;
+  }
+  void GetTimeRange(OwningDoubleOrScrollTimelineAutoKeyword& aOutResult) const {
+    aOutResult = mSpecifiedTimeRange;
+  }
+  // Need to qualify type with dom::, otherwise clashes with method name
+  // (and method name cannot change because WebIDL expects exactly that).
+  dom::FillMode FillMode() const {
+    return mFillMode;
+  }
+
+  // Computed properties
+  const StickyTimeDuration& GetEffectiveTimeRange() const {
+    return mEffectiveTimeRange;
+  }
+  nscoord GetMinScroll() const {
+    return mMinScroll;
+  }
+  nscoord GetMaxScroll() const {
+    return mMaxScroll;
+  }
+
+  // Other methods
+  void Tick();
+  void NotifyScroll();
+  void QueryScrollValues();
+
+  void Teardown();
+
+protected:
+  nsIScrollableFrame* GetScrollFrame() const;
+
+  void CalculateEffectiveTimeRange();
+  void ParseProperties();
+
+  // Infrastructure
+  nsCOMPtr<nsIDocument> mDocument;
+  UniquePtr<ScrollPositionListener> mScrollPositionListener;
+  bool mNeedsTick;
+
+  // Raw versions of specified values of properties.
+  RefPtr<Element> mElement;
+  nsString mSpecifiedStartScrollOffset;
+  nsString mSpecifiedEndScrollOffset;
+  OwningDoubleOrScrollTimelineAutoKeyword mSpecifiedTimeRange;
+  dom::FillMode mFillMode;
+
+  // Cooked versions of specified values of properties
+  Maybe<StickyTimeDuration> mSpecifiedTimeRangeCooked;
+
+  // Computed values of properties
+  ScrollDirection mComputedOrientation;
+  nsCSSValue mComputedStartScrollOffset;
+  nsCSSValue mComputedEndScrollOffset;
+  StickyTimeDuration mEffectiveTimeRange;
+  nscoord mCurrentScroll, mMinScroll, mMaxScroll;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ScrollTimeline_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(0));
+    }
+    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);
+  if (scrollRange != 0 && !aEffectiveTimeRange.IsZero() &&
+      aEffectiveTimeRange != TimeDuration::Forever()) {
+    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
@@ -12,16 +12,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',
@@ -50,16 +52,18 @@ UNIFIED_SOURCES += [
     'DocumentTimeline.cpp',
     'EffectCompositor.cpp',
     'EffectSet.cpp',
     'KeyframeEffect.cpp',
     'KeyframeEffectParams.cpp',
     'KeyframeEffectReadOnly.cpp',
     'KeyframeUtils.cpp',
     'PendingAnimationTracker.cpp',
+    'ScrollTimeline.cpp',
+    'ScrollTimelineUtils.cpp',
     'TimingParams.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '/dom/base',
     '/layout/base',
     '/layout/style',
 ]
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -211,16 +211,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"
@@ -1460,16 +1461,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;
 
@@ -2977,16 +2979,24 @@ nsDocument::Timeline()
   if (!mDocumentTimeline) {
     mDocumentTimeline = new DocumentTimeline(this, TimeDuration(0));
   }
 
   return mDocumentTimeline;
 }
 
 void
+nsDocument::TickScrollTimelines()
+{
+  for (const auto& 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
@@ -605,16 +605,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;
@@ -1589,16 +1594,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/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -147,16 +147,17 @@ class Link;
 class Location;
 class MediaQueryList;
 class GlobalObject;
 class NodeFilter;
 class NodeIterator;
 enum class OrientationType : uint32_t;
 class ProcessingInstruction;
 class Promise;
+class ScrollTimeline;
 class StyleSheetList;
 class SVGDocument;
 class SVGSVGElement;
 class Touch;
 class TouchList;
 class TreeWalker;
 class XPathEvaluator;
 class XPathExpression;
@@ -2383,16 +2384,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);
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://w3c.github.io/web-animations/#documenttimeline
+ *
+ * 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 = "0";
+  DOMString endScrollOffset = "auto";
+  (double or ScrollTimelineAutoKeyword) timeRange = "auto";
+  FillMode fillMode = "none";
+};
+
+[Func="nsDocument::IsWebAnimationsEnabled",
+ Constructor(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 fillMode;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -384,16 +384,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',
     'SettingsManager.webidl',
     'ShadowRoot.webidl',
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -1587,16 +1587,42 @@ static bool
 CollectDocuments(nsIDocument* aDocument, void* aDocArray)
 {
   static_cast<nsCOMArray<nsIDocument>*>(aDocArray)->AppendObject(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;
   }
 
   nsCOMArray<nsIDocument> documents;
   CollectDocuments(mPresContext->Document(), &documents);
@@ -1807,16 +1833,17 @@ nsRefreshDriver::Tick(int64_t aNowEpoch,
         StopTimer();
         return;
       }
     }
 
     if (i == 0) {
       // This is the Flush_Style case.
 
+      TickScrollTimelines();
       DispatchAnimationEvents();
       DispatchPendingEvents();
       RunFrameRequestCallbacks(aNowTime);
 
       if (mPresContext && mPresContext->GetPresShell()) {
         bool tracingStyleFlush = false;
         AutoTArray<nsIPresShell*, 16> observers;
         observers.AppendElements(mStyleFlushObservers);
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -378,16 +378,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,