Bug 1288040 (Part 1) - Separate FrameAnimator's state into a separate class, AnimationState. r=edwin
authorSeth Fowler <mark.seth.fowler@gmail.com>
Mon, 18 Jul 2016 23:26:58 -0700
changeset 390333 8383c2cc99393e21a7b7f2f8129e94b0f9a77593
parent 390332 3d6cdeb53e16b0bc4fdb1f9d97b7ad9883132004
child 390334 34faad78d6f099f4d1a25a1c7935db5656fdf627
push id23655
push userbmo:gasolin@mozilla.com
push dateThu, 21 Jul 2016 02:47:20 +0000
reviewersedwin
bugs1288040
milestone50.0a1
Bug 1288040 (Part 1) - Separate FrameAnimator's state into a separate class, AnimationState. r=edwin
image/FrameAnimator.cpp
image/FrameAnimator.h
image/RasterImage.cpp
image/RasterImage.h
--- a/image/FrameAnimator.cpp
+++ b/image/FrameAnimator.cpp
@@ -15,51 +15,111 @@
 #include "pixman.h"
 
 namespace mozilla {
 
 using namespace gfx;
 
 namespace image {
 
+///////////////////////////////////////////////////////////////////////////////
+// AnimationState implementation.
+///////////////////////////////////////////////////////////////////////////////
+
+void
+AnimationState::SetDoneDecoding(bool aDone)
+{
+  mDoneDecoding = aDone;
+}
+
+void
+AnimationState::ResetAnimation()
+{
+  mCurrentAnimationFrameIndex = 0;
+  mLastCompositedFrameIndex = -1;
+}
+
+void
+AnimationState::SetAnimationMode(uint16_t aAnimationMode)
+{
+  mAnimationMode = aAnimationMode;
+}
+
+void
+AnimationState::UnionFirstFrameRefreshArea(const nsIntRect& aRect)
+{
+  mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, aRect);
+}
+
+void
+AnimationState::InitAnimationFrameTimeIfNecessary()
+{
+  if (mCurrentAnimationFrameTime.IsNull()) {
+    mCurrentAnimationFrameTime = TimeStamp::Now();
+  }
+}
+
+void
+AnimationState::SetAnimationFrameTime(const TimeStamp& aTime)
+{
+  mCurrentAnimationFrameTime = aTime;
+}
+
+uint32_t
+AnimationState::GetCurrentAnimationFrameIndex() const
+{
+  return mCurrentAnimationFrameIndex;
+}
+
+nsIntRect
+AnimationState::GetFirstFrameRefreshArea() const
+{
+  return mFirstFrameRefreshArea;
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+// FrameAnimator implementation.
+///////////////////////////////////////////////////////////////////////////////
+
 int32_t
-FrameAnimator::GetSingleLoopTime() const
+FrameAnimator::GetSingleLoopTime(AnimationState& aState) const
 {
   // If we aren't done decoding, we don't know the image's full play time.
-  if (!mDoneDecoding) {
+  if (!aState.mDoneDecoding) {
     return -1;
   }
 
   // If we're not looping, a single loop time has no meaning
-  if (mAnimationMode != imgIContainer::kNormalAnimMode) {
+  if (aState.mAnimationMode != imgIContainer::kNormalAnimMode) {
     return -1;
   }
 
   int32_t looptime = 0;
   for (uint32_t i = 0; i < mImage->GetNumFrames(); ++i) {
-    int32_t timeout = GetTimeoutForFrame(i);
+    int32_t timeout = GetTimeoutForFrame(aState, i);
     if (timeout >= 0) {
       looptime += static_cast<uint32_t>(timeout);
     } else {
       // If we have a frame that never times out, we're probably in an error
       // case, but let's handle it more gracefully.
       NS_WARNING("Negative frame timeout - how did this happen?");
       return -1;
     }
   }
 
   return looptime;
 }
 
 TimeStamp
-FrameAnimator::GetCurrentImgFrameEndTime() const
+FrameAnimator::GetCurrentImgFrameEndTime(AnimationState& aState) const
 {
-  TimeStamp currentFrameTime = mCurrentAnimationFrameTime;
+  TimeStamp currentFrameTime = aState.mCurrentAnimationFrameTime;
   int32_t timeout =
-    GetTimeoutForFrame(mCurrentAnimationFrameIndex);
+    GetTimeoutForFrame(aState, aState.mCurrentAnimationFrameIndex);
 
   if (timeout < 0) {
     // We need to return a sentinel value in this case, because our logic
     // doesn't work correctly if we have a negative timeout value. We use
     // one year in the future as the sentinel because it works with the loop
     // in RequestRefresh() below.
     // XXX(seth): It'd be preferable to make our logic work correctly with
     // negative timeouts.
@@ -70,64 +130,64 @@ FrameAnimator::GetCurrentImgFrameEndTime
   TimeDuration durationOfTimeout =
     TimeDuration::FromMilliseconds(static_cast<double>(timeout));
   TimeStamp currentFrameEndTime = currentFrameTime + durationOfTimeout;
 
   return currentFrameEndTime;
 }
 
 FrameAnimator::RefreshResult
-FrameAnimator::AdvanceFrame(TimeStamp aTime)
+FrameAnimator::AdvanceFrame(AnimationState& aState, TimeStamp aTime)
 {
   NS_ASSERTION(aTime <= TimeStamp::Now(),
                "Given time appears to be in the future");
   PROFILER_LABEL_FUNC(js::ProfileEntry::Category::GRAPHICS);
 
   RefreshResult ret;
 
   // Determine what the next frame is, taking into account looping.
-  uint32_t currentFrameIndex = mCurrentAnimationFrameIndex;
+  uint32_t currentFrameIndex = aState.mCurrentAnimationFrameIndex;
   uint32_t nextFrameIndex = currentFrameIndex + 1;
 
   if (mImage->GetNumFrames() == nextFrameIndex) {
     // We can only accurately determine if we are at the end of the loop if we are
     // done decoding, otherwise we don't know how many frames there will be.
-    if (!mDoneDecoding) {
+    if (!aState.mDoneDecoding) {
       // We've already advanced to the last decoded frame, nothing more we can do.
       // We're blocked by network/decoding from displaying the animation at the
       // rate specified, so that means the frame we are displaying (the latest
       // available) is the frame we want to be displaying at this time. So we
       // update the current animation time. If we didn't update the current
       // animation time then it could lag behind, which would indicate that we
       // are behind in the animation and should try to catch up. When we are
       // done decoding (and thus can loop around back to the start of the
       // animation) we would then jump to a random point in the animation to
       // try to catch up. But we were never behind in the animation.
-      mCurrentAnimationFrameTime = aTime;
+      aState.mCurrentAnimationFrameTime = aTime;
       return ret;
     }
 
     // End of an animation loop...
 
     // If we are not looping forever, initialize the loop counter
-    if (mLoopRemainingCount < 0 && LoopCount() >= 0) {
-      mLoopRemainingCount = LoopCount();
+    if (aState.mLoopRemainingCount < 0 && aState.LoopCount() >= 0) {
+      aState.mLoopRemainingCount = aState.LoopCount();
     }
 
     // If animation mode is "loop once", or we're at end of loop counter,
     // it's time to stop animating.
-    if (mAnimationMode == imgIContainer::kLoopOnceAnimMode ||
-        mLoopRemainingCount == 0) {
+    if (aState.mAnimationMode == imgIContainer::kLoopOnceAnimMode ||
+        aState.mLoopRemainingCount == 0) {
       ret.animationFinished = true;
     }
 
     nextFrameIndex = 0;
 
-    if (mLoopRemainingCount > 0) {
-      mLoopRemainingCount--;
+    if (aState.mLoopRemainingCount > 0) {
+      aState.mLoopRemainingCount--;
     }
 
     // If we're done, exit early.
     if (ret.animationFinished) {
       return ret;
     }
   }
 
@@ -137,196 +197,145 @@ FrameAnimator::AdvanceFrame(TimeStamp aT
   // what RasterImage knows about so that we stay in sync with RasterImage. The code
   // above should obey this, the MOZ_ASSERT records this invariant.
   MOZ_ASSERT(nextFrameIndex < mImage->GetNumFrames());
   RawAccessFrameRef nextFrame = GetRawFrame(nextFrameIndex);
 
   // If we're done decoding, we know we've got everything we're going to get.
   // If we aren't, we only display fully-downloaded frames; everything else
   // gets delayed.
-  bool canDisplay = mDoneDecoding ||
+  bool canDisplay = aState.mDoneDecoding ||
                     (nextFrame && nextFrame->IsFinished());
 
   if (!canDisplay) {
     // Uh oh, the frame we want to show is currently being decoded (partial)
     // Wait until the next refresh driver tick and try again
     return ret;
   }
 
   // Bad data
-  if (GetTimeoutForFrame(nextFrameIndex) < 0) {
+  if (GetTimeoutForFrame(aState, nextFrameIndex) < 0) {
     ret.animationFinished = true;
     ret.error = true;
   }
 
   if (nextFrameIndex == 0) {
-    ret.dirtyRect = mFirstFrameRefreshArea;
+    ret.dirtyRect = aState.mFirstFrameRefreshArea;
   } else {
     MOZ_ASSERT(nextFrameIndex == currentFrameIndex + 1);
 
     // Change frame
-    if (!DoBlend(&ret.dirtyRect, currentFrameIndex, nextFrameIndex)) {
+    if (!DoBlend(aState, &ret.dirtyRect, currentFrameIndex, nextFrameIndex)) {
       // something went wrong, move on to next
       NS_WARNING("FrameAnimator::AdvanceFrame(): Compositing of frame failed");
       nextFrame->SetCompositingFailed(true);
-      mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime();
-      mCurrentAnimationFrameIndex = nextFrameIndex;
+      aState.mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime(aState);
+      aState.mCurrentAnimationFrameIndex = nextFrameIndex;
 
       ret.error = true;
       return ret;
     }
 
     nextFrame->SetCompositingFailed(false);
   }
 
-  mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime();
+  aState.mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime(aState);
 
   // If we can get closer to the current time by a multiple of the image's loop
   // time, we should. We need to be done decoding in order to know the full loop
   // time though!
-  int32_t loopTime = GetSingleLoopTime();
+  int32_t loopTime = GetSingleLoopTime(aState);
   if (loopTime > 0) {
     // We shouldn't be advancing by a whole loop unless we are decoded and know
     // what a full loop actually is. GetSingleLoopTime should return -1 so this
     // never happens.
-    MOZ_ASSERT(mDoneDecoding);
-    TimeDuration delay = aTime - mCurrentAnimationFrameTime;
+    MOZ_ASSERT(aState.mDoneDecoding);
+    TimeDuration delay = aTime - aState.mCurrentAnimationFrameTime;
     if (delay.ToMilliseconds() > loopTime) {
       // Explicitly use integer division to get the floor of the number of
       // loops.
       uint64_t loops = static_cast<uint64_t>(delay.ToMilliseconds()) / loopTime;
-      mCurrentAnimationFrameTime +=
+      aState.mCurrentAnimationFrameTime +=
         TimeDuration::FromMilliseconds(loops * loopTime);
     }
   }
 
   // Set currentAnimationFrameIndex at the last possible moment
-  mCurrentAnimationFrameIndex = nextFrameIndex;
+  aState.mCurrentAnimationFrameIndex = nextFrameIndex;
 
   // If we're here, we successfully advanced the frame.
   ret.frameAdvanced = true;
 
   return ret;
 }
 
 FrameAnimator::RefreshResult
-FrameAnimator::RequestRefresh(const TimeStamp& aTime)
+FrameAnimator::RequestRefresh(AnimationState& aState, const TimeStamp& aTime)
 {
   // only advance the frame if the current time is greater than or
   // equal to the current frame's end time.
-  TimeStamp currentFrameEndTime = GetCurrentImgFrameEndTime();
+  TimeStamp currentFrameEndTime = GetCurrentImgFrameEndTime(aState);
 
   // By default, an empty RefreshResult.
   RefreshResult ret;
 
   while (currentFrameEndTime <= aTime) {
     TimeStamp oldFrameEndTime = currentFrameEndTime;
 
-    RefreshResult frameRes = AdvanceFrame(aTime);
+    RefreshResult frameRes = AdvanceFrame(aState, aTime);
 
     // Accumulate our result for returning to callers.
     ret.Accumulate(frameRes);
 
-    currentFrameEndTime = GetCurrentImgFrameEndTime();
+    currentFrameEndTime = GetCurrentImgFrameEndTime(aState);
 
     // if we didn't advance a frame, and our frame end time didn't change,
     // then we need to break out of this loop & wait for the frame(s)
     // to finish downloading
     if (!frameRes.frameAdvanced && (currentFrameEndTime == oldFrameEndTime)) {
       break;
     }
   }
 
   return ret;
 }
 
-void
-FrameAnimator::ResetAnimation()
-{
-  mCurrentAnimationFrameIndex = 0;
-  mLastCompositedFrameIndex = -1;
-}
-
-void
-FrameAnimator::SetDoneDecoding(bool aDone)
-{
-  mDoneDecoding = aDone;
-}
-
-void
-FrameAnimator::SetAnimationMode(uint16_t aAnimationMode)
-{
-  mAnimationMode = aAnimationMode;
-}
-
-void
-FrameAnimator::InitAnimationFrameTimeIfNecessary()
-{
-  if (mCurrentAnimationFrameTime.IsNull()) {
-    mCurrentAnimationFrameTime = TimeStamp::Now();
-  }
-}
-
-void
-FrameAnimator::SetAnimationFrameTime(const TimeStamp& aTime)
-{
-  mCurrentAnimationFrameTime = aTime;
-}
-
-void
-FrameAnimator::UnionFirstFrameRefreshArea(const nsIntRect& aRect)
-{
-  mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, aRect);
-}
-
-uint32_t
-FrameAnimator::GetCurrentAnimationFrameIndex() const
-{
-  return mCurrentAnimationFrameIndex;
-}
-
-nsIntRect
-FrameAnimator::GetFirstFrameRefreshArea() const
-{
-  return mFirstFrameRefreshArea;
-}
-
 LookupResult
-FrameAnimator::GetCompositedFrame(uint32_t aFrameNum)
+FrameAnimator::GetCompositedFrame(AnimationState& aState, uint32_t aFrameNum)
 {
   MOZ_ASSERT(aFrameNum != 0, "First frame is never composited");
 
   // If we have a composited version of this frame, return that.
-  if (mLastCompositedFrameIndex == int32_t(aFrameNum)) {
+  if (aState.mLastCompositedFrameIndex == int32_t(aFrameNum)) {
     return LookupResult(mCompositingFrame->DrawableRef(), MatchType::EXACT);
   }
 
   // Otherwise return the raw frame. DoBlend is required to ensure that we only
   // hit this case if the frame is not paletted and doesn't require compositing.
   LookupResult result =
     SurfaceCache::Lookup(ImageKey(mImage),
                          RasterSurfaceKey(mSize,
                                           DefaultSurfaceFlags(),
                                           aFrameNum));
   MOZ_ASSERT(!result || !result.DrawableRef()->GetIsPaletted(),
              "About to return a paletted frame");
   return result;
 }
 
 int32_t
-FrameAnimator::GetTimeoutForFrame(uint32_t aFrameNum) const
+FrameAnimator::GetTimeoutForFrame(AnimationState& aState, uint32_t aFrameNum) const
 {
   int32_t rawTimeout = 0;
 
   RawAccessFrameRef frame = GetRawFrame(aFrameNum);
   if (frame) {
     AnimationData data = frame->GetAnimationData();
     rawTimeout = data.mRawTimeout;
   } else if (aFrameNum == 0) {
-    rawTimeout = mFirstFrameTimeout;
+    rawTimeout = aState.mFirstFrameTimeout;
   } else {
     NS_WARNING("No frame; called GetTimeoutForFrame too early?");
     return 100;
   }
 
   // Ensure a minimal time between updates so we don't throttle the UI thread.
   // consider 0 == unspecified and make it fast but not too fast.  Unless we
   // have a single loop GIF. See bug 890743, bug 125137, bug 139677, and bug
@@ -402,17 +411,18 @@ FrameAnimator::GetRawFrame(uint32_t aFra
   return result ? result.DrawableRef()->RawAccessRef()
                 : RawAccessFrameRef();
 }
 
 //******************************************************************************
 // DoBlend gets called when the timer for animation get fired and we have to
 // update the composited frame of the animation.
 bool
-FrameAnimator::DoBlend(nsIntRect* aDirtyRect,
+FrameAnimator::DoBlend(AnimationState& aState,
+                       nsIntRect* aDirtyRect,
                        uint32_t aPrevFrameIndex,
                        uint32_t aNextFrameIndex)
 {
   RawAccessFrameRef prevFrame = GetRawFrame(aPrevFrameIndex);
   RawAccessFrameRef nextFrame = GetRawFrame(aNextFrameIndex);
 
   MOZ_ASSERT(prevFrame && nextFrame, "Should have frames here");
 
@@ -489,34 +499,34 @@ FrameAnimator::DoBlend(nsIntRect* aDirty
   }
 
   // Optimization:
   //   Skip compositing if the last composited frame is this frame
   //   (Only one composited frame was made for this animation.  Example:
   //    Only Frame 3 of a 10 frame image required us to build a composite frame
   //    On the second loop, we do not need to rebuild the frame
   //    since it's still sitting in compositingFrame)
-  if (mLastCompositedFrameIndex == int32_t(aNextFrameIndex)) {
+  if (aState.mLastCompositedFrameIndex == int32_t(aNextFrameIndex)) {
     return true;
   }
 
   bool needToBlankComposite = false;
 
   // Create the Compositing Frame
   if (!mCompositingFrame) {
     RefPtr<imgFrame> newFrame = new imgFrame;
     nsresult rv = newFrame->InitForDecoder(mSize,
                                            SurfaceFormat::B8G8R8A8);
     if (NS_FAILED(rv)) {
       mCompositingFrame.reset();
       return false;
     }
     mCompositingFrame = newFrame->RawAccessRef();
     needToBlankComposite = true;
-  } else if (int32_t(aNextFrameIndex) != mLastCompositedFrameIndex+1) {
+  } else if (int32_t(aNextFrameIndex) != aState.mLastCompositedFrameIndex+1) {
 
     // If we are not drawing on top of last composited frame,
     // then we are building a new composite frame, so let's clear it first.
     needToBlankComposite = true;
   }
 
   AnimationData compositingFrameData = mCompositingFrame->GetAnimationData();
 
@@ -600,17 +610,17 @@ FrameAnimator::DoBlend(nsIntRect* aDirty
       case DisposalMethod::KEEP:
         // Copy previous frame into compositingFrame before we put the new
         // frame on top
         // Assumes that the previous frame represents a full frame (it could be
         // smaller in size than the container, as long as the frame before it
         // erased itself)
         // Note: Frame 1 never gets into DoBlend(), so (aNextFrameIndex - 1)
         // will always be a valid frame number.
-        if (mLastCompositedFrameIndex != int32_t(aNextFrameIndex - 1)) {
+        if (aState.mLastCompositedFrameIndex != int32_t(aNextFrameIndex - 1)) {
           if (isFullPrevFrame && !prevFrame->GetIsPaletted()) {
             // Just copy the bits
             CopyFrameImage(prevFrameData.mRawData,
                            prevFrameData.mRect,
                            compositingFrameData.mRawData,
                            compositingFrameData.mRect);
           } else {
             if (needToBlankComposite) {
@@ -675,17 +685,17 @@ FrameAnimator::DoBlend(nsIntRect* aDirty
               compositingFrameData.mRawData,
               compositingFrameData.mRect,
               nextFrameData.mBlendMethod,
               nextFrameData.mBlendRect);
 
   // Tell the image that it is fully 'downloaded'.
   mCompositingFrame->Finish();
 
-  mLastCompositedFrameIndex = int32_t(aNextFrameIndex);
+  aState.mLastCompositedFrameIndex = int32_t(aNextFrameIndex);
 
   return true;
 }
 
 //******************************************************************************
 // Fill aFrame with black. Does also clears the mask.
 void
 FrameAnimator::ClearFrame(uint8_t* aFrameData, const nsIntRect& aFrameRect)
--- a/image/FrameAnimator.h
+++ b/image/FrameAnimator.h
@@ -17,31 +17,125 @@
 #include "nsRect.h"
 #include "SurfaceCache.h"
 
 namespace mozilla {
 namespace image {
 
 class RasterImage;
 
-class FrameAnimator
+class AnimationState
 {
 public:
-  FrameAnimator(RasterImage* aImage,
-                gfx::IntSize aSize,
-                uint16_t aAnimationMode)
-    : mImage(aImage)
-    , mSize(aSize)
-    , mCurrentAnimationFrameIndex(0)
+  explicit AnimationState(uint16_t aAnimationMode)
+    : mCurrentAnimationFrameIndex(0)
+    , mLastCompositedFrameIndex(-1)
     , mLoopRemainingCount(-1)
-    , mLastCompositedFrameIndex(-1)
     , mLoopCount(-1)
     , mFirstFrameTimeout(0)
     , mAnimationMode(aAnimationMode)
     , mDoneDecoding(false)
+  { }
+
+  /**
+   * Call when this image is finished decoding so we know that there aren't any
+   * more frames coming.
+   */
+  void SetDoneDecoding(bool aDone);
+
+  /**
+   * Call when you need to re-start animating. Ensures we start from the first
+   * frame.
+   */
+  void ResetAnimation();
+
+  /**
+   * The animation mode of the image.
+   *
+   * Constants defined in imgIContainer.idl.
+   */
+  void SetAnimationMode(uint16_t aAnimationMode);
+
+  /**
+   * Union the area to refresh when we loop around to the first frame with this
+   * rect.
+   */
+  void UnionFirstFrameRefreshArea(const nsIntRect& aRect);
+
+  /**
+   * If the animation frame time has not yet been set, set it to
+   * TimeStamp::Now().
+   */
+  void InitAnimationFrameTimeIfNecessary();
+
+  /**
+   * Set the animation frame time to @aTime.
+   */
+  void SetAnimationFrameTime(const TimeStamp& aTime);
+
+  /**
+   * The current frame we're on, from 0 to (numFrames - 1).
+   */
+  uint32_t GetCurrentAnimationFrameIndex() const;
+
+  /**
+   * Get the area we refresh when we loop around to the first frame.
+   */
+  nsIntRect GetFirstFrameRefreshArea() const;
+
+  /*
+   * Set number of times to loop the image.
+   * @note -1 means loop forever.
+   */
+  void SetLoopCount(int32_t aLoopCount) { mLoopCount = aLoopCount; }
+  int32_t LoopCount() const { return mLoopCount; }
+
+  /*
+   * Set the timeout for the first frame. This is used to allow animation
+   * scheduling even before a full decode runs for this image.
+   */
+  void SetFirstFrameTimeout(int32_t aTimeout) { mFirstFrameTimeout = aTimeout; }
+
+private:
+  friend class FrameAnimator;
+
+  //! Area of the first frame that needs to be redrawn on subsequent loops.
+  nsIntRect mFirstFrameRefreshArea;
+
+  //! the time that the animation advanced to the current frame
+  TimeStamp mCurrentAnimationFrameTime;
+
+  //! The current frame index we're on. 0 to (numFrames - 1).
+  uint32_t mCurrentAnimationFrameIndex;
+
+  //! Track the last composited frame for Optimizations (See DoComposite code)
+  int32_t mLastCompositedFrameIndex;
+
+  //! number of loops remaining before animation stops (-1 no stop)
+  int32_t mLoopRemainingCount;
+
+  //! The total number of loops for the image.
+  int32_t mLoopCount;
+
+  //! The timeout for the first frame of this image.
+  int32_t mFirstFrameTimeout;
+
+  //! The animation mode of this image. Constants defined in imgIContainer.
+  uint16_t mAnimationMode;
+
+  //! Whether this image is done being decoded.
+  bool mDoneDecoding;
+};
+
+class FrameAnimator
+{
+public:
+  FrameAnimator(RasterImage* aImage, gfx::IntSize aSize)
+    : mImage(aImage)
+    , mSize(aSize)
   {
      MOZ_COUNT_CTOR(FrameAnimator);
   }
 
   ~FrameAnimator()
   {
     MOZ_COUNT_DTOR(FrameAnimator);
   }
@@ -82,138 +176,81 @@ public:
 
   /**
    * Re-evaluate what frame we're supposed to be on, and do whatever blending
    * is necessary to get us to that frame.
    *
    * Returns the result of that blending, including whether the current frame
    * changed and what the resulting dirty rectangle is.
    */
-  RefreshResult RequestRefresh(const TimeStamp& aTime);
-
-  /**
-   * Call when this image is finished decoding so we know that there aren't any
-   * more frames coming.
-   */
-  void SetDoneDecoding(bool aDone);
-
-  /**
-   * Call when you need to re-start animating. Ensures we start from the first
-   * frame.
-   */
-  void ResetAnimation();
-
-  /**
-   * The animation mode of the image.
-   *
-   * Constants defined in imgIContainer.idl.
-   */
-  void SetAnimationMode(uint16_t aAnimationMode);
-
-  /**
-   * Union the area to refresh when we loop around to the first frame with this
-   * rect.
-   */
-  void UnionFirstFrameRefreshArea(const nsIntRect& aRect);
-
-  /**
-   * If the animation frame time has not yet been set, set it to
-   * TimeStamp::Now().
-   */
-  void InitAnimationFrameTimeIfNecessary();
-
-  /**
-   * Set the animation frame time to @aTime.
-   */
-  void SetAnimationFrameTime(const TimeStamp& aTime);
-
-  /**
-   * The current frame we're on, from 0 to (numFrames - 1).
-   */
-  uint32_t GetCurrentAnimationFrameIndex() const;
-
-  /**
-   * Get the area we refresh when we loop around to the first frame.
-   */
-  nsIntRect GetFirstFrameRefreshArea() const;
+  RefreshResult RequestRefresh(AnimationState& aState, const TimeStamp& aTime);
 
   /**
    * If we have a composited frame for @aFrameNum, returns it. Otherwise,
    * returns an empty LookupResult. It is an error to call this method with
    * aFrameNum == 0, because the first frame is never composited.
    */
-  LookupResult GetCompositedFrame(uint32_t aFrameNum);
+  LookupResult GetCompositedFrame(AnimationState& aState, uint32_t aFrameNum);
 
   /*
    * Returns the frame's adjusted timeout. If the animation loops and the
    * timeout falls in between a certain range then the timeout is adjusted so
    * that it's never 0. If the animation does not loop then no adjustments are
    * made.
    */
-  int32_t GetTimeoutForFrame(uint32_t aFrameNum) const;
-
-  /*
-   * Set number of times to loop the image.
-   * @note -1 means loop forever.
-   */
-  void SetLoopCount(int32_t aLoopCount) { mLoopCount = aLoopCount; }
-  int32_t LoopCount() const { return mLoopCount; }
-
-  /*
-   * Set the timeout for the first frame. This is used to allow animation
-   * scheduling even before a full decode runs for this image.
-   */
-  void SetFirstFrameTimeout(int32_t aTimeout) { mFirstFrameTimeout = aTimeout; }
+  int32_t GetTimeoutForFrame(AnimationState& aState, uint32_t aFrameNum) const;
 
   /**
    * Collect an accounting of the memory occupied by the compositing surfaces we
    * use during animation playback. All of the actual animation frames are
    * stored in the SurfaceCache, so we don't need to report them here.
    */
   void CollectSizeOfCompositingSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
                                         MallocSizeOf aMallocSizeOf) const;
 
 private: // methods
   /**
-   * Gets the length of a single loop of this image, in milliseconds.
-   *
-   * If this image is not finished decoding, is not animated, or it is animated
-   * but does not loop, returns -1. Can return 0 in the case of an animated
-   * image that has a 0ms delay between its frames and does not loop.
-   */
-  int32_t GetSingleLoopTime() const;
-
-  /**
    * Advances the animation. Typically, this will advance a single frame, but it
    * may advance multiple frames. This may happen if we have infrequently
    * "ticking" refresh drivers (e.g. in background tabs), or extremely short-
    * lived animation frames.
    *
    * @param aTime the time that the animation should advance to. This will
    *              typically be <= TimeStamp::Now().
    *
    * @returns a RefreshResult that shows whether the frame was successfully
    *          advanced, and its resulting dirty rect.
    */
-  RefreshResult AdvanceFrame(TimeStamp aTime);
+  RefreshResult AdvanceFrame(AnimationState& aState, TimeStamp aTime);
+
+  /**
+   * Get the @aIndex-th frame in the frame index, ignoring results of blending.
+   */
+  RawAccessFrameRef GetRawFrame(uint32_t aFrameNum) const;
+
+  /**
+   * Gets the length of a single loop of this image, in milliseconds.
+   *
+   * If this image is not finished decoding, is not animated, or it is animated
+   * but does not loop, returns -1. Can return 0 in the case of an animated
+   * image that has a 0ms delay between its frames and does not loop.
+   */
+  int32_t GetSingleLoopTime(AnimationState& aState) const;
 
   /**
    * Get the time the frame we're currently displaying is supposed to end.
    *
    * In the error case, returns an "infinity" timestamp.
    */
-  TimeStamp GetCurrentImgFrameEndTime() const;
-
-  bool DoBlend(nsIntRect* aDirtyRect, uint32_t aPrevFrameIndex,
-               uint32_t aNextFrameIndex);
+  TimeStamp GetCurrentImgFrameEndTime(AnimationState& aState) const;
 
-  /**
-   * Get the @aIndex-th frame in the frame index, ignoring results of blending.
-   */
-  RawAccessFrameRef GetRawFrame(uint32_t aFrameNum) const;
+  bool DoBlend(AnimationState& aState,
+               nsIntRect* aDirtyRect,
+               uint32_t aPrevFrameIndex,
+               uint32_t aNextFrameIndex);
 
   /** Clears an area of <aFrame> with transparent black.
    *
    * @param aFrameData Target Frame data
    * @param aFrameRect The rectangle of the data pointed ot by aFrameData
    *
    * @note Does also clears the transparency mask
    */
@@ -269,41 +306,14 @@ private: // data
 
   /** the previous composited frame, for DISPOSE_RESTORE_PREVIOUS
    *
    * The Previous Frame (all frames composited up to the current) needs to be
    * stored in cases where the image specifies it wants the last frame back
    * when it's done with the current frame.
    */
   RawAccessFrameRef mCompositingPrevFrame;
-
-  //! Area of the first frame that needs to be redrawn on subsequent loops.
-  nsIntRect mFirstFrameRefreshArea;
-
-  //! the time that the animation advanced to the current frame
-  TimeStamp mCurrentAnimationFrameTime;
-
-  //! The current frame index we're on. 0 to (numFrames - 1).
-  uint32_t mCurrentAnimationFrameIndex;
-
-  //! number of loops remaining before animation stops (-1 no stop)
-  int32_t mLoopRemainingCount;
-
-  //! Track the last composited frame for Optimizations (See DoComposite code)
-  int32_t mLastCompositedFrameIndex;
-
-  //! The total number of loops for the image.
-  int32_t mLoopCount;
-
-  //! The timeout for the first frame of this image.
-  int32_t mFirstFrameTimeout;
-
-  //! The animation mode of this image. Constants defined in imgIContainer.
-  uint16_t mAnimationMode;
-
-  //! Whether this image is done being decoded.
-  bool mDoneDecoding;
 };
 
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_FrameAnimator_h
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -172,18 +172,19 @@ RasterImage::RequestRefresh(const TimeSt
 
   EvaluateAnimation();
 
   if (!mAnimating) {
     return;
   }
 
   FrameAnimator::RefreshResult res;
-  if (mAnim) {
-    res = mAnim->RequestRefresh(aTime);
+  if (mAnimationState) {
+    MOZ_ASSERT(mFrameAnimator);
+    res = mFrameAnimator->RequestRefresh(*mAnimationState, aTime);
   }
 
   if (res.frameAdvanced) {
     // Notify listeners that our frame has actually changed, but do this only
     // once for all frames that we've now passed (if AdvanceFrame() was called
     // more than once).
     #ifdef DEBUG
       mFramesNotified++;
@@ -269,26 +270,27 @@ RasterImage::GetType(uint16_t* aType)
   return NS_OK;
 }
 
 LookupResult
 RasterImage::LookupFrameInternal(uint32_t aFrameNum,
                                  const IntSize& aSize,
                                  uint32_t aFlags)
 {
-  if (!mAnim) {
+  if (!mAnimationState) {
     NS_ASSERTION(aFrameNum == 0,
                  "Don't ask for a frame > 0 if we're not animated!");
     aFrameNum = 0;
   }
 
-  if (mAnim && aFrameNum > 0) {
+  if (mAnimationState && aFrameNum > 0) {
+    MOZ_ASSERT(mFrameAnimator);
     MOZ_ASSERT(ToSurfaceFlags(aFlags) == DefaultSurfaceFlags(),
                "Can't composite frames with non-default surface flags");
-    return mAnim->GetCompositedFrame(aFrameNum);
+    return mFrameAnimator->GetCompositedFrame(*mAnimationState, aFrameNum);
   }
 
   SurfaceFlags surfaceFlags = ToSurfaceFlags(aFlags);
 
   // We don't want any substitution for sync decodes, and substitution would be
   // illegal when high quality downscaling is disabled, so we use
   // SurfaceCache::Lookup in this case.
   if ((aFlags & FLAG_SYNC_DECODE) || !(aFlags & FLAG_HIGH_QUALITY_SCALING)) {
@@ -332,17 +334,18 @@ RasterImage::LookupFrame(uint32_t aFrame
   }
 
   if (result.Type() == MatchType::NOT_FOUND ||
       result.Type() == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND ||
       ((aFlags & FLAG_SYNC_DECODE) && !result)) {
     // We don't have a copy of this frame, and there's no decoder working on
     // one. (Or we're sync decoding and the existing decoder hasn't even started
     // yet.) Trigger decoding so it'll be available next time.
-    MOZ_ASSERT(!mAnim || GetNumFrames() < 1, "Animated frames should be locked");
+    MOZ_ASSERT(!mAnimationState || GetNumFrames() < 1,
+               "Animated frames should be locked");
 
     Decode(requestedSize, aFlags);
 
     // If we can sync decode, we should already have the frame.
     if (aFlags & FLAG_SYNC_DECODE) {
       result = LookupFrameInternal(aFrameNum, requestedSize, aFlags);
     }
   }
@@ -376,34 +379,34 @@ RasterImage::LookupFrame(uint32_t aFrame
   }
 
   return Move(result.DrawableRef());
 }
 
 uint32_t
 RasterImage::GetCurrentFrameIndex() const
 {
-  if (mAnim) {
-    return mAnim->GetCurrentAnimationFrameIndex();
+  if (mAnimationState) {
+    return mAnimationState->GetCurrentAnimationFrameIndex();
   }
 
   return 0;
 }
 
 uint32_t
 RasterImage::GetRequestedFrameIndex(uint32_t aWhichFrame) const
 {
   return aWhichFrame == FRAME_FIRST ? 0 : GetCurrentFrameIndex();
 }
 
 IntRect
 RasterImage::GetFirstFrameRect()
 {
-  if (mAnim && mHasBeenDecoded) {
-    return mAnim->GetFirstFrameRefreshArea();
+  if (mAnimationState && mHasBeenDecoded) {
+    return mAnimationState->GetFirstFrameRefreshArea();
   }
 
   // Fall back to our size. This is implicitly zero-size if !mHasSize.
   return IntRect(IntPoint(0,0), mSize);
 }
 
 NS_IMETHODIMP_(bool)
 RasterImage::IsOpaque()
@@ -436,27 +439,27 @@ NS_IMETHODIMP
 RasterImage::GetAnimated(bool* aAnimated)
 {
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
   NS_ENSURE_ARG_POINTER(aAnimated);
 
-  // If we have mAnim, we can know for sure
-  if (mAnim) {
+  // If we have an AnimationState, we can know for sure.
+  if (mAnimationState) {
     *aAnimated = true;
     return NS_OK;
   }
 
   // Otherwise, we need to have been decoded to know for sure, since if we were
-  // decoded at least once mAnim would have been created for animated images.
-  // This is true even though we check for animation during the metadata decode,
-  // because we may still discover animation only during the full decode for
-  // corrupt images.
+  // decoded at least once mAnimationState would have been created for animated
+  // images. This is true even though we check for animation during the
+  // metadata decode, because we may still discover animation only during the
+  // full decode for corrupt images.
   if (!mHasBeenDecoded) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // We know for sure
   *aAnimated = false;
 
   return NS_OK;
@@ -470,18 +473,19 @@ RasterImage::GetFirstFrameDelay()
     return -1;
   }
 
   bool animated = false;
   if (NS_FAILED(GetAnimated(&animated)) || !animated) {
     return -1;
   }
 
-  MOZ_ASSERT(mAnim, "Animated images should have a FrameAnimator");
-  return mAnim->GetTimeoutForFrame(0);
+  MOZ_ASSERT(mAnimationState, "Animated images should have an AnimationState");
+  MOZ_ASSERT(mFrameAnimator, "Animated images should have a FrameAnimator");
+  return mFrameAnimator->GetTimeoutForFrame(*mAnimationState, 0);
 }
 
 already_AddRefed<SourceSurface>
 RasterImage::CopyFrame(uint32_t aWhichFrame, uint32_t aFlags)
 {
   if (aWhichFrame > FRAME_MAX_VALUE) {
     return nullptr;
   }
@@ -746,18 +750,18 @@ RasterImage::SizeOfSourceWithComputedFal
   return mSourceBuffer->SizeOfIncludingThisWithComputedFallback(aMallocSizeOf);
 }
 
 void
 RasterImage::CollectSizeOfSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
                                    MallocSizeOf aMallocSizeOf) const
 {
   SurfaceCache::CollectSizeOfSurfaces(ImageKey(this), aCounters, aMallocSizeOf);
-  if (mAnim) {
-    mAnim->CollectSizeOfCompositingSurfaces(aCounters, aMallocSizeOf);
+  if (mFrameAnimator) {
+    mFrameAnimator->CollectSizeOfCompositingSurfaces(aCounters, aMallocSizeOf);
   }
 }
 
 class OnAddedFrameRunnable : public Runnable
 {
 public:
   OnAddedFrameRunnable(RasterImage* aImage,
                        uint32_t aNewFrameCount,
@@ -797,25 +801,25 @@ RasterImage::OnAddedFrame(uint32_t aNewF
   if (mError) {
     return;  // We're in an error state, possibly due to OOM. Bail.
   }
 
   if (aNewFrameCount > mFrameCount) {
     mFrameCount = aNewFrameCount;
 
     if (aNewFrameCount == 2) {
-      MOZ_ASSERT(mAnim, "Should already have animation state");
+      MOZ_ASSERT(mAnimationState, "Should already have animation state");
 
       // We may be able to start animating.
       if (mPendingAnimation && ShouldAnimate()) {
         StartAnimation();
       }
     }
     if (aNewFrameCount > 1) {
-      mAnim->UnionFirstFrameRefreshArea(aNewRefreshArea);
+      mAnimationState->UnionFirstFrameRefreshArea(aNewRefreshArea);
     }
   }
 }
 
 bool
 RasterImage::SetMetadata(const ImageMetadata& aMetadata,
                          bool aFromMetadataDecode)
 {
@@ -845,36 +849,37 @@ RasterImage::SetMetadata(const ImageMeta
     }
 
     // Set the size and flag that we have it.
     mSize = size;
     mOrientation = orientation;
     mHasSize = true;
   }
 
-  if (mHasSize && aMetadata.HasAnimation() && !mAnim) {
+  if (mHasSize && aMetadata.HasAnimation() && !mAnimationState) {
     // We're becoming animated, so initialize animation stuff.
-    mAnim = MakeUnique<FrameAnimator>(this, mSize, mAnimationMode);
+    mAnimationState.emplace(mAnimationMode);
+    mFrameAnimator = MakeUnique<FrameAnimator>(this, mSize);
 
     // We don't support discarding animated images (See bug 414259).
     // Lock the image and throw away the key.
     LockImage();
 
     if (!aFromMetadataDecode) {
       // The metadata decode reported that this image isn't animated, but we
       // discovered that it actually was during the full decode. This is a
       // rare failure that only occurs for corrupt images. To recover, we need
       // to discard all existing surfaces and redecode.
       return false;
     }
   }
 
-  if (mAnim) {
-    mAnim->SetLoopCount(aMetadata.GetLoopCount());
-    mAnim->SetFirstFrameTimeout(aMetadata.GetFirstFrameTimeout());
+  if (mAnimationState) {
+    mAnimationState->SetLoopCount(aMetadata.GetLoopCount());
+    mAnimationState->SetFirstFrameTimeout(aMetadata.GetFirstFrameTimeout());
   }
 
   if (aMetadata.HasHotspot()) {
     IntPoint hotspot = aMetadata.GetHotspot();
 
     nsCOMPtr<nsISupportsPRUint32> intwrapx =
       do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID);
     nsCOMPtr<nsISupportsPRUint32> intwrapy =
@@ -887,18 +892,18 @@ RasterImage::SetMetadata(const ImageMeta
   }
 
   return true;
 }
 
 NS_IMETHODIMP
 RasterImage::SetAnimationMode(uint16_t aAnimationMode)
 {
-  if (mAnim) {
-    mAnim->SetAnimationMode(aAnimationMode);
+  if (mAnimationState) {
+    mAnimationState->SetAnimationMode(aAnimationMode);
   }
   return SetAnimationModeInternal(aAnimationMode);
 }
 
 //******************************************************************************
 
 nsresult
 RasterImage::StartAnimation()
@@ -906,102 +911,107 @@ RasterImage::StartAnimation()
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
   MOZ_ASSERT(ShouldAnimate(), "Should not animate!");
 
   // If we're not ready to animate, then set mPendingAnimation, which will cause
   // us to start animating if and when we do become ready.
-  mPendingAnimation = !mAnim || GetNumFrames() < 2;
+  mPendingAnimation = !mAnimationState || GetNumFrames() < 2;
   if (mPendingAnimation) {
     return NS_OK;
   }
 
+  MOZ_ASSERT(mFrameAnimator,
+             "Should have a FrameAnimator if we have AnimationState");
+
   // A timeout of -1 means we should display this frame forever.
-  if (mAnim->GetTimeoutForFrame(GetCurrentFrameIndex()) < 0) {
+  if (mFrameAnimator->GetTimeoutForFrame(*mAnimationState,
+                                         GetCurrentFrameIndex())
+        < 0) {
     mAnimationFinished = true;
     return NS_ERROR_ABORT;
   }
 
   // We need to set the time that this initial frame was first displayed, as
   // this is used in AdvanceFrame().
-  mAnim->InitAnimationFrameTimeIfNecessary();
+  mAnimationState->InitAnimationFrameTimeIfNecessary();
 
   return NS_OK;
 }
 
 //******************************************************************************
 nsresult
 RasterImage::StopAnimation()
 {
   MOZ_ASSERT(mAnimating, "Should be animating!");
 
   nsresult rv = NS_OK;
   if (mError) {
     rv = NS_ERROR_FAILURE;
   } else {
-    mAnim->SetAnimationFrameTime(TimeStamp());
+    mAnimationState->SetAnimationFrameTime(TimeStamp());
   }
 
   mAnimating = false;
   return rv;
 }
 
 //******************************************************************************
 NS_IMETHODIMP
 RasterImage::ResetAnimation()
 {
   if (mError) {
     return NS_ERROR_FAILURE;
     }
 
   mPendingAnimation = false;
 
-  if (mAnimationMode == kDontAnimMode || !mAnim ||
-      mAnim->GetCurrentAnimationFrameIndex() == 0) {
+  if (mAnimationMode == kDontAnimMode || !mAnimationState ||
+      mAnimationState->GetCurrentAnimationFrameIndex() == 0) {
     return NS_OK;
   }
 
   mAnimationFinished = false;
 
   if (mAnimating) {
     StopAnimation();
   }
 
-  MOZ_ASSERT(mAnim, "Should have a FrameAnimator");
-  mAnim->ResetAnimation();
+  MOZ_ASSERT(mAnimationState, "Should have AnimationState");
+  mAnimationState->ResetAnimation();
 
-  NotifyProgress(NoProgress, mAnim->GetFirstFrameRefreshArea());
+  NotifyProgress(NoProgress, mAnimationState->GetFirstFrameRefreshArea());
 
   // Start the animation again. It may not have been running before, if
   // mAnimationFinished was true before entering this function.
   EvaluateAnimation();
 
   return NS_OK;
 }
 
 //******************************************************************************
 NS_IMETHODIMP_(void)
 RasterImage::SetAnimationStartTime(const TimeStamp& aTime)
 {
-  if (mError || mAnimationMode == kDontAnimMode || mAnimating || !mAnim) {
+  if (mError || mAnimationMode == kDontAnimMode || mAnimating || !mAnimationState) {
     return;
   }
 
-  mAnim->SetAnimationFrameTime(aTime);
+  mAnimationState->SetAnimationFrameTime(aTime);
 }
 
 NS_IMETHODIMP_(float)
 RasterImage::GetFrameIndex(uint32_t aWhichFrame)
 {
   MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE, "Invalid argument");
-  return (aWhichFrame == FRAME_FIRST || !mAnim)
+  return (aWhichFrame == FRAME_FIRST || !mAnimationState)
          ? 0.0f
-         : mAnim->GetCurrentAnimationFrameIndex();
+         : mAnimationState->GetCurrentAnimationFrameIndex();
 }
 
 NS_IMETHODIMP_(IntRect)
 RasterImage::GetImageSpaceInvalidationRect(const IntRect& aRect)
 {
   return aRect;
 }
 
@@ -1152,31 +1162,31 @@ RasterImage::GetKeys(uint32_t* count, ch
   return mProperties->GetKeys(count, keys);
 }
 
 void
 RasterImage::Discard()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(CanDiscard(), "Asked to discard but can't");
-  MOZ_ASSERT(!mAnim, "Asked to discard for animated image");
+  MOZ_ASSERT(!mAnimationState, "Asked to discard for animated image");
 
   // Delete all the decoded frames.
   SurfaceCache::RemoveImage(ImageKey(this));
 
   // Notify that we discarded.
   if (mProgressTracker) {
     mProgressTracker->OnDiscard();
   }
 }
 
 bool
 RasterImage::CanDiscard() {
   return mHasSourceData &&       // ...have the source data...
-         !mAnim;                 // Can never discard animated images
+         !mAnimationState;       // Can never discard animated images
 }
 
 NS_IMETHODIMP
 RasterImage::StartDecoding()
 {
   if (mError) {
     return NS_ERROR_FAILURE;
   }
@@ -1291,17 +1301,17 @@ RasterImage::Decode(const IntSize& aSize
   if (IsOpaque()) {
     // If there's no transparency, it doesn't matter whether we premultiply
     // alpha or not.
     surfaceFlags &= ~SurfaceFlags::NO_PREMULTIPLY_ALPHA;
   }
 
   // Create a decoder.
   RefPtr<IDecodingTask> task;
-  if (mAnim) {
+  if (mAnimationState) {
     task = DecoderFactory::CreateAnimationDecoder(mDecoderType, WrapNotNull(this),
                                                   mSourceBuffer, mSize,
                                                   decoderFlags, surfaceFlags);
   } else {
     task = DecoderFactory::CreateDecoder(mDecoderType, WrapNotNull(this),
                                          mSourceBuffer, mSize, targetSize,
                                          decoderFlags, surfaceFlags,
                                          mRequestedSampleSize);
@@ -1357,17 +1367,17 @@ RasterImage::RecoverFromInvalidFrames(co
 
   // Relock the image if it's supposed to be locked.
   if (mLockCount > 0) {
     SurfaceCache::LockImage(ImageKey(this));
   }
 
   // Animated images require some special handling, because we normally require
   // that they never be discarded.
-  if (mAnim) {
+  if (mAnimationState) {
     Decode(mSize, aFlags | FLAG_SYNC_DECODE);
     ResetAnimation();
     return;
   }
 
   // For non-animated images, it's fine to recover using an async decode.
   Decode(aSize, aFlags);
 }
@@ -1390,17 +1400,17 @@ RasterImage::CanDownscaleDuringDecode(co
   // our size, and the flags allow us to do it.
   if (!mHasSize || mTransient || !HaveSkia() ||
       !gfxPrefs::ImageDownscaleDuringDecodeEnabled() ||
       !(aFlags & imgIContainer::FLAG_HIGH_QUALITY_SCALING)) {
     return false;
   }
 
   // We don't downscale animated images during decode.
-  if (mAnim) {
+  if (mAnimationState) {
     return false;
   }
 
   // Never upscale.
   if (aSize.width >= mSize.width || aSize.height >= mSize.height) {
     return false;
   }
 
@@ -1603,17 +1613,18 @@ RasterImage::DoError()
 
   // Put the container in an error state.
   mError = true;
 
   // Stop animation and release our FrameAnimator.
   if (mAnimating) {
     StopAnimation();
   }
-  mAnim = nullptr;
+  mAnimationState = Nothing();
+  mFrameAnimator = nullptr;
 
   // Release all locks.
   mLockCount = 0;
   SurfaceCache::UnlockImage(ImageKey(this));
 
   // Release all frames from the surface cache.
   SurfaceCache::RemoveImage(ImageKey(this));
 
@@ -1716,18 +1727,18 @@ RasterImage::FinalizeDecoder(Decoder* aD
   }
 
   MOZ_ASSERT(mError || mHasSize || !aDecoder->HasSize(),
              "SetMetadata should've gotten a size");
 
   if (!wasMetadata && aDecoder->GetDecodeDone() && !aDecoder->WasAborted()) {
     // Flag that we've been decoded before.
     mHasBeenDecoded = true;
-    if (mAnim) {
-      mAnim->SetDoneDecoding(true);
+    if (mAnimationState) {
+      mAnimationState->SetDoneDecoding(true);
     }
   }
 
   // Send out any final notifications.
   NotifyProgress(aDecoder->TakeProgress(),
                  aDecoder->TakeInvalidRect(),
                  aDecoder->GetSurfaceFlags());
 
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -22,16 +22,17 @@
 #include "imgIContainer.h"
 #include "nsIProperties.h"
 #include "nsTArray.h"
 #include "imgFrame.h"
 #include "LookupResult.h"
 #include "nsThreadUtils.h"
 #include "DecodePool.h"
 #include "DecoderFactory.h"
+#include "FrameAnimator.h"
 #include "Orientation.h"
 #include "nsIObserver.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/Pair.h"
 #include "mozilla/TimeStamp.h"
@@ -128,17 +129,16 @@ namespace mozilla {
 namespace layers {
 class ImageContainer;
 class Image;
 } // namespace layers
 
 namespace image {
 
 class Decoder;
-class FrameAnimator;
 class ImageMetadata;
 class SourceBuffer;
 
 class RasterImage final : public ImageResource
                         , public nsIProperties
                         , public SupportsWeakPtr<RasterImage>
 #ifdef DEBUG
                         , public imgIContainerDebug
@@ -282,17 +282,17 @@ private:
 
   void UpdateImageContainer();
 
   // We would like to just check if we have a zero lock count, but we can't do
   // that for animated images because in EnsureAnimExists we lock the image and
   // never unlock so that animated images always have their lock count >= 1. In
   // that case we use our animation consumers count as a proxy for lock count.
   bool IsUnlocked() {
-    return (mLockCount == 0 || (mAnim && mAnimationConsumers == 0));
+    return (mLockCount == 0 || (mAnimationState && mAnimationConsumers == 0));
   }
 
 
   //////////////////////////////////////////////////////////////////////////////
   // Decoding.
   //////////////////////////////////////////////////////////////////////////////
 
   /**
@@ -345,17 +345,20 @@ private: // data
   Orientation                mOrientation;
 
   /// If this has a value, we're waiting for SetSize() to send the load event.
   Maybe<Progress>            mLoadProgress;
 
   nsCOMPtr<nsIProperties>   mProperties;
 
   /// If this image is animated, a FrameAnimator which manages its animation.
-  UniquePtr<FrameAnimator> mAnim;
+  UniquePtr<FrameAnimator> mFrameAnimator;
+
+  /// Animation timeline and other state for animation images.
+  Maybe<AnimationState> mAnimationState;
 
   // Image locking.
   uint32_t                   mLockCount;
 
   // The type of decoder this image needs. Computed from the MIME type in Init().
   DecoderType                mDecoderType;
 
   // How many times we've decoded this image.