Animated SVG images are not paused when outside viewport
authorcdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 28 Mar 2017 23:11:35 +0000 (23:11 +0000)
committercdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 28 Mar 2017 23:11:35 +0000 (23:11 +0000)
https://bugs.webkit.org/show_bug.cgi?id=170155
<rdar://problem/31288893>

Reviewed by Antti Koivisto.

Source/WebCore:

Make sure animated SVG images get paused when outside the viewport,
similarly to what was already done for animated GIF images. Also
make sure they are paused when they no longer have any renderers
using them.

Tests: svg/animations/animated-svg-image-outside-viewport-paused.html
       svg/animations/animated-svg-image-removed-from-document-paused.html

* loader/cache/CachedImage.cpp:
(WebCore::CachedImage::didAddClient):
Restart the animation whenever a new CachedImage client is added. This
will cause us the re-evaluate if the animation should run. The animation
will pause again if the new renderer is not inside the viewport.

(WebCore::CachedImage::animationAdvanced):
Add a flag to newImageAnimationFrameAvailable() so that the renderers can
let us know if we can pause the animation. Pause the animation if all no
renderer requires it (i.e. they are all outside the viewport, or there
are no renderers).

* loader/cache/CachedImageClient.h:
(WebCore::CachedImageClient::newImageAnimationFrameAvailable):
By default, the CachedImageClients allow pausing. Only renderer will
potentially prevent pausing if they are inside the viewport.

* platform/graphics/BitmapImage.cpp:
(WebCore::BitmapImage::isAnimating):
* platform/graphics/BitmapImage.h:
* platform/graphics/Image.h:
(WebCore::Image::isAnimating):
Add isAnimating() flag on Image for layout testing purposes.

* rendering/RenderElement.cpp:
(WebCore::RenderElement::newImageAnimationFrameAvailable):
Set canPause flag to true if the renderer is not inside the viewport.

(WebCore::RenderElement::repaintForPausedImageAnimationsIfNeeded):
Call startAnimation() if the renderer is now visible to resume SVG
animations. Repainting is enough for GIF animations but not for SVG
animations, we have to explicitly resume them.

* rendering/RenderElement.h:
* rendering/RenderView.cpp:
(WebCore::RenderView::addRendererWithPausedImageAnimations):
(WebCore::RenderView::removeRendererWithPausedImageAnimations):
(WebCore::RenderView::resumePausedImageAnimationsIfNeeded):
* rendering/RenderView.h:
Store CachedImages with the renderers that have paused animations.
This is required for SVG where we need to explicitly resume the
animation on the CachedImage when the renderer becomes visible
again. Having access to the Image will also allow us to do smarter
visibility checks in RenderElement's shouldRepaintForImageAnimation(),
in the future.

* svg/SVGSVGElement.cpp:
(WebCore::SVGSVGElement::hasActiveAnimation):
* svg/SVGSVGElement.h:
Add hasActiveAnimation() method.

* svg/graphics/SVGImage.cpp:
(WebCore::SVGImage::startAnimation):
Check that animations are paused before starting them. This avoid
jumping due to unnecessary calls to rootElement->setCurrentTime(0).

(WebCore::SVGImage::isAnimating):
Add isAnimating() method for layout tests purposes.

* svg/graphics/SVGImage.h:
* svg/graphics/SVGImageClients.h:
Call animationAdvanced() on the observer instead of the generic
changedInRect() when the SVGImage is animating. This way, we go
through the same code path as GIF animations and we end up calling
CachedImage::animationAdvanced() which calls newImageAnimationFrameAvailable()
on RenderElement, which determines if the animation should keep
running or not.

* testing/Internals.cpp:
(WebCore::Internals::isImageAnimating):
* testing/Internals.h:
* testing/Internals.idl:
Add layout testing infrastructure.

LayoutTests:

Add layout test coverage.

* platform/mac-wk1/TestExpectations:
* svg/animations/animated-svg-image-outside-viewport-paused-expected.txt: Added.
* svg/animations/animated-svg-image-outside-viewport-paused.html: Added.
* svg/animations/animated-svg-image-removed-from-document-paused-expected.txt: Added.
* svg/animations/animated-svg-image-removed-from-document-paused.html: Added.
* svg/animations/resources/smilAnimation.svg: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@214503 268f45cc-cd09-0410-ab3c-d52691b4dbfc

26 files changed:
LayoutTests/ChangeLog
LayoutTests/platform/mac-wk1/TestExpectations
LayoutTests/svg/animations/animated-svg-image-outside-viewport-paused-expected.txt [new file with mode: 0644]
LayoutTests/svg/animations/animated-svg-image-outside-viewport-paused.html [new file with mode: 0644]
LayoutTests/svg/animations/animated-svg-image-removed-from-document-paused-expected.txt [new file with mode: 0644]
LayoutTests/svg/animations/animated-svg-image-removed-from-document-paused.html [new file with mode: 0644]
LayoutTests/svg/animations/resources/smilAnimation.svg [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/loader/cache/CachedImage.cpp
Source/WebCore/loader/cache/CachedImageClient.h
Source/WebCore/platform/graphics/BitmapImage.cpp
Source/WebCore/platform/graphics/BitmapImage.h
Source/WebCore/platform/graphics/Image.h
Source/WebCore/rendering/RenderElement.cpp
Source/WebCore/rendering/RenderElement.h
Source/WebCore/rendering/RenderView.cpp
Source/WebCore/rendering/RenderView.h
Source/WebCore/rendering/style/StyleCachedImage.cpp
Source/WebCore/svg/SVGSVGElement.cpp
Source/WebCore/svg/SVGSVGElement.h
Source/WebCore/svg/graphics/SVGImage.cpp
Source/WebCore/svg/graphics/SVGImage.h
Source/WebCore/svg/graphics/SVGImageClients.h
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl

index 892fb15..95e261e 100644 (file)
@@ -1,3 +1,20 @@
+2017-03-28  Chris Dumez  <cdumez@apple.com>
+
+        Animated SVG images are not paused when outside viewport
+        https://bugs.webkit.org/show_bug.cgi?id=170155
+        <rdar://problem/31288893>
+
+        Reviewed by Antti Koivisto.
+
+        Add layout test coverage.
+
+        * platform/mac-wk1/TestExpectations:
+        * svg/animations/animated-svg-image-outside-viewport-paused-expected.txt: Added.
+        * svg/animations/animated-svg-image-outside-viewport-paused.html: Added.
+        * svg/animations/animated-svg-image-removed-from-document-paused-expected.txt: Added.
+        * svg/animations/animated-svg-image-removed-from-document-paused.html: Added.
+        * svg/animations/resources/smilAnimation.svg: Added.
+
 2017-03-28  Antti Koivisto  <antti@apple.com>
 
         Missing render tree position invalidation when tearing down renderers for display:contents subtree
index 84fe58b..d75dec3 100644 (file)
@@ -123,6 +123,8 @@ http/tests/navigation/page-cache-iframe-no-current-historyItem.html
 fast/images/animated-gif-body-outside-viewport.html [ Skip ]
 fast/images/animated-gif-window-resizing.html [ Skip ]
 fast/images/animated-gif-zooming.html [ Skip ]
+svg/animations/animated-svg-image-outside-viewport-paused.html [ Skip ]
+svg/animations/animated-svg-image-removed-from-document-paused.html [ Skip ]
 
 # WK1 uses the native scrollview for scrolling by page.
 scrollbars/scrolling-backward-by-page-accounting-bottom-fixed-elements-on-keyboard-spacebar.html
diff --git a/LayoutTests/svg/animations/animated-svg-image-outside-viewport-paused-expected.txt b/LayoutTests/svg/animations/animated-svg-image-outside-viewport-paused-expected.txt
new file mode 100644 (file)
index 0000000..5ab000d
--- /dev/null
@@ -0,0 +1,15 @@
+Tests that animated SVG images are paused when outside the viewport.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Initially outside the viewport
+PASS internals.isImageAnimating(image) is false
+Scrolling animation into view
+PASS internals.isImageAnimating(image) became true
+Scrolling animation outside view again
+PASS internals.isImageAnimating(image) became false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/svg/animations/animated-svg-image-outside-viewport-paused.html b/LayoutTests/svg/animations/animated-svg-image-outside-viewport-paused.html
new file mode 100644 (file)
index 0000000..1129707
--- /dev/null
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../../resources/js-test-pre.js"></script>
+<script>
+description("Tests that animated SVG images are paused when outside the viewport.");
+jsTestIsAsync = true;
+
+onload = function() {
+    image = document.querySelector("img");
+
+    setTimeout(function() {
+        debug("Initially outside the viewport");
+        shouldBeFalse("internals.isImageAnimating(image)");
+
+        debug("Scrolling animation into view");
+        internals.scrollElementToRect(image, 0, 0, 300, 300);
+        shouldBecomeEqual("internals.isImageAnimating(image)", "true", function() {
+            debug("Scrolling animation outside view again");
+            scroll(0, 0);
+            shouldBecomeEqual("internals.isImageAnimating(image)", "false", finishJSTest);
+        });
+    }, 30);
+}
+</script>
+<div style="position: relative; width: 1600px; height: 2400px;">
+<img src="resources/smilAnimation.svg" style="position:absolute; left: 600px; top: 800px;">
+</div>
+<script src="../../resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/svg/animations/animated-svg-image-removed-from-document-paused-expected.txt b/LayoutTests/svg/animations/animated-svg-image-removed-from-document-paused-expected.txt
new file mode 100644 (file)
index 0000000..c69ea2f
--- /dev/null
@@ -0,0 +1,21 @@
+Tests that animated SVG images are paused when removed from the document.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS internals.isImageAnimating(imageA) is true
+PASS internals.isImageAnimating(imageB) is true
+imageA.remove()
+PASS internals.isImageAnimating(imageB) is true
+PASS internals.isImageAnimating(imageB) is true
+imageB.remove()
+PASS internals.isImageAnimating(imageA) is false
+PASS internals.isImageAnimating(imageB) is false
+document.body.appendChild(imageA)
+PASS internals.isImageAnimating(imageA) is true
+document.body.appendChild(imageB)
+PASS internals.isImageAnimating(imageB) is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
diff --git a/LayoutTests/svg/animations/animated-svg-image-removed-from-document-paused.html b/LayoutTests/svg/animations/animated-svg-image-removed-from-document-paused.html
new file mode 100644 (file)
index 0000000..e054338
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../../resources/js-test-pre.js"></script>
+<img id="a" src="resources/smilAnimation.svg">
+<img id="b" src="resources/smilAnimation.svg">
+<script>
+description("Tests that animated SVG images are paused when removed from the document.");
+jsTestIsAsync = true;
+
+// Both images will use the same underlying SVGImage.
+const imageA = document.getElementById("a");
+const imageB = document.getElementById("b");
+
+onload = function() {
+    shouldBeTrue("internals.isImageAnimating(imageA)");
+    shouldBeTrue("internals.isImageAnimating(imageB)");
+
+    setTimeout(function() {
+        evalAndLog("imageA.remove()");
+        shouldBeTrue("internals.isImageAnimating(imageB)");
+
+        setTimeout(function() {
+            shouldBeTrue("internals.isImageAnimating(imageB)");
+            evalAndLog("imageB.remove()");
+            setTimeout(function() {
+                shouldBeFalse("internals.isImageAnimating(imageA)");
+                shouldBeFalse("internals.isImageAnimating(imageB)");
+
+                evalAndLog("document.body.appendChild(imageA)");
+                document.body.offsetWidth; // Force layout.
+                shouldBeTrue("internals.isImageAnimating(imageA)");
+                evalAndLog("document.body.appendChild(imageB)");
+                document.body.offsetWidth; // Force layout.
+                shouldBeTrue("internals.isImageAnimating(imageB)");
+
+                finishJSTest();
+            }, 30);
+        }, 30);
+    }, 30);
+}
+</script>
+<script src="../../resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/svg/animations/resources/smilAnimation.svg b/LayoutTests/svg/animations/resources/smilAnimation.svg
new file mode 100644 (file)
index 0000000..e042d1c
--- /dev/null
@@ -0,0 +1,5 @@
+<svg width="200" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <rect width="100" height="100" fill="green">
+        <animate attributeName="x" values="0; 100; 0" dur="1s" repeatCount="indefinite"></animate>
+    </rect>
+</svg>
index e328892..11f4a0a 100644 (file)
@@ -1,3 +1,93 @@
+2017-03-28  Chris Dumez  <cdumez@apple.com>
+
+        Animated SVG images are not paused when outside viewport
+        https://bugs.webkit.org/show_bug.cgi?id=170155
+        <rdar://problem/31288893>
+
+        Reviewed by Antti Koivisto.
+
+        Make sure animated SVG images get paused when outside the viewport,
+        similarly to what was already done for animated GIF images. Also
+        make sure they are paused when they no longer have any renderers
+        using them.
+
+        Tests: svg/animations/animated-svg-image-outside-viewport-paused.html
+               svg/animations/animated-svg-image-removed-from-document-paused.html
+
+        * loader/cache/CachedImage.cpp:
+        (WebCore::CachedImage::didAddClient):
+        Restart the animation whenever a new CachedImage client is added. This
+        will cause us the re-evaluate if the animation should run. The animation
+        will pause again if the new renderer is not inside the viewport.
+
+        (WebCore::CachedImage::animationAdvanced):
+        Add a flag to newImageAnimationFrameAvailable() so that the renderers can
+        let us know if we can pause the animation. Pause the animation if all no
+        renderer requires it (i.e. they are all outside the viewport, or there
+        are no renderers).
+
+        * loader/cache/CachedImageClient.h:
+        (WebCore::CachedImageClient::newImageAnimationFrameAvailable):
+        By default, the CachedImageClients allow pausing. Only renderer will
+        potentially prevent pausing if they are inside the viewport.
+
+        * platform/graphics/BitmapImage.cpp:
+        (WebCore::BitmapImage::isAnimating):
+        * platform/graphics/BitmapImage.h:
+        * platform/graphics/Image.h:
+        (WebCore::Image::isAnimating):
+        Add isAnimating() flag on Image for layout testing purposes.
+
+        * rendering/RenderElement.cpp:
+        (WebCore::RenderElement::newImageAnimationFrameAvailable):
+        Set canPause flag to true if the renderer is not inside the viewport.
+
+        (WebCore::RenderElement::repaintForPausedImageAnimationsIfNeeded):
+        Call startAnimation() if the renderer is now visible to resume SVG
+        animations. Repainting is enough for GIF animations but not for SVG
+        animations, we have to explicitly resume them.
+
+        * rendering/RenderElement.h:
+        * rendering/RenderView.cpp:
+        (WebCore::RenderView::addRendererWithPausedImageAnimations):
+        (WebCore::RenderView::removeRendererWithPausedImageAnimations):
+        (WebCore::RenderView::resumePausedImageAnimationsIfNeeded):
+        * rendering/RenderView.h:
+        Store CachedImages with the renderers that have paused animations.
+        This is required for SVG where we need to explicitly resume the
+        animation on the CachedImage when the renderer becomes visible
+        again. Having access to the Image will also allow us to do smarter
+        visibility checks in RenderElement's shouldRepaintForImageAnimation(),
+        in the future.
+
+        * svg/SVGSVGElement.cpp:
+        (WebCore::SVGSVGElement::hasActiveAnimation):
+        * svg/SVGSVGElement.h:
+        Add hasActiveAnimation() method.
+
+        * svg/graphics/SVGImage.cpp:
+        (WebCore::SVGImage::startAnimation):
+        Check that animations are paused before starting them. This avoid
+        jumping due to unnecessary calls to rootElement->setCurrentTime(0).
+
+        (WebCore::SVGImage::isAnimating):
+        Add isAnimating() method for layout tests purposes.
+
+        * svg/graphics/SVGImage.h:
+        * svg/graphics/SVGImageClients.h:
+        Call animationAdvanced() on the observer instead of the generic
+        changedInRect() when the SVGImage is animating. This way, we go
+        through the same code path as GIF animations and we end up calling
+        CachedImage::animationAdvanced() which calls newImageAnimationFrameAvailable()
+        on RenderElement, which determines if the animation should keep
+        running or not.
+
+        * testing/Internals.cpp:
+        (WebCore::Internals::isImageAnimating):
+        * testing/Internals.h:
+        * testing/Internals.idl:
+        Add layout testing infrastructure.
+
 2017-03-28  Antti Koivisto  <antti@apple.com>
 
         Missing render tree position invalidation when tearing down renderers for display:contents subtree
index 65f0bf7..9ef5ad2 100644 (file)
@@ -117,6 +117,9 @@ void CachedImage::didAddClient(CachedResourceClient& client)
     if (m_image && !m_image->isNull())
         static_cast<CachedImageClient&>(client).imageChanged(this);
 
+    if (m_image)
+        m_image->startAnimation();
+
     CachedResource::didAddClient(client);
 }
 
@@ -514,9 +517,19 @@ void CachedImage::animationAdvanced(const Image* image)
 {
     if (!image || image != m_image)
         return;
+
+    bool shouldPauseAnimation = true;
+
     CachedResourceClientWalker<CachedImageClient> clientWalker(m_clients);
-    while (CachedImageClient* client = clientWalker.next())
-        client->newImageAnimationFrameAvailable(*this);
+    while (CachedImageClient* client = clientWalker.next()) {
+        bool canPause = false;
+        client->newImageAnimationFrameAvailable(*this, canPause);
+        if (!canPause)
+            shouldPauseAnimation = false;
+    }
+
+    if (shouldPauseAnimation)
+        m_image->stopAnimation();
 }
 
 void CachedImage::changedInRect(const Image* image, const IntRect* rect)
index e09621e..5c46ff2 100644 (file)
@@ -40,7 +40,7 @@ public:
     virtual void imageChanged(CachedImage*, const IntRect* = nullptr) { }
 
     // Called when GIF animation progresses.
-    virtual void newImageAnimationFrameAvailable(CachedImage& image) { imageChanged(&image); }
+    virtual void newImageAnimationFrameAvailable(CachedImage& image, bool& canPause) { imageChanged(&image); canPause = true; }
 };
 
 } // namespace WebCore
index 843edfa..ca468f7 100644 (file)
@@ -406,6 +406,11 @@ void BitmapImage::internalAdvanceAnimation()
     LOG(Images, "BitmapImage::%s - %p - url: %s [m_currentFrame = %ld]", __FUNCTION__, this, sourceURL().utf8().data(), m_currentFrame);
 }
 
+bool BitmapImage::isAnimating() const
+{
+    return !!m_frameTimer;
+}
+
 void BitmapImage::stopAnimation()
 {
     // This timer is used to animate all occurrences of this image. Don't invalidate
index 54795ce..f145a25 100644 (file)
@@ -169,6 +169,7 @@ protected:
     StartAnimationStatus internalStartAnimation();
     void advanceAnimation();
     void internalAdvanceAnimation();
+    bool isAnimating() const final;
 
     // It may look unusual that there is no start animation call as public API. This is because
     // we start and stop animating lazily. Animation begins whenever someone draws the image. It will
index bc98bd9..81629ba 100644 (file)
@@ -131,6 +131,7 @@ public:
     virtual void stopAnimation() {}
     virtual void resetAnimation() {}
     virtual void newFrameNativeImageAvailableAtIndex(size_t) { }
+    virtual bool isAnimating() const { return false; }
     
     // Typically the CachedImage that owns us.
     ImageObserver* imageObserver() const { return m_imageObserver; }
index 771b752..202d92d 100644 (file)
@@ -1494,21 +1494,19 @@ void RenderElement::visibleInViewportStateChanged()
     ASSERT_NOT_REACHED();
 }
 
-void RenderElement::newImageAnimationFrameAvailable(CachedImage& image)
+void RenderElement::newImageAnimationFrameAvailable(CachedImage& image, bool& canPause)
 {
     auto& frameView = view().frameView();
     auto visibleRect = frameView.windowToContents(frameView.windowClipRect());
     if (!shouldRepaintForImageAnimation(*this, visibleRect)) {
-        // FIXME: It would be better to pass the image along with the renderer
-        // so that we can be smarter about detecting if the image is inside the
-        // viewport in repaintForPausedImageAnimationsIfNeeded().
-        view().addRendererWithPausedImageAnimations(*this);
+        view().addRendererWithPausedImageAnimations(*this, image);
+        canPause = true;
         return;
     }
     imageChanged(&image);
 }
 
-bool RenderElement::repaintForPausedImageAnimationsIfNeeded(const IntRect& visibleRect)
+bool RenderElement::repaintForPausedImageAnimationsIfNeeded(const IntRect& visibleRect, CachedImage& cachedImage)
 {
     ASSERT(m_hasPausedImageAnimations);
     if (!shouldRepaintForImageAnimation(*this, visibleRect))
@@ -1516,6 +1514,9 @@ bool RenderElement::repaintForPausedImageAnimationsIfNeeded(const IntRect& visib
 
     repaint();
 
+    if (auto* image = cachedImage.image())
+        image->startAnimation();
+
     // For directly-composited animated GIFs it does not suffice to call repaint() to resume animation. We need to mark the image as changed.
     if (is<RenderBoxModelObject>(*this))
         downcast<RenderBoxModelObject>(*this).contentChanged(ImageChanged);
index 7448ac2..cd9a558 100644 (file)
@@ -195,7 +195,7 @@ public:
     void setVisibleInViewportState(VisibleInViewportState);
     virtual void visibleInViewportStateChanged();
 
-    bool repaintForPausedImageAnimationsIfNeeded(const IntRect& visibleRect);
+    bool repaintForPausedImageAnimationsIfNeeded(const IntRect& visibleRect, CachedImage&);
     bool hasPausedImageAnimations() const { return m_hasPausedImageAnimations; }
     void setHasPausedImageAnimations(bool b) { m_hasPausedImageAnimations = b; }
 
@@ -317,7 +317,7 @@ private:
     std::unique_ptr<RenderStyle> computeFirstLineStyle() const;
     void invalidateCachedFirstLineStyle();
 
-    void newImageAnimationFrameAvailable(CachedImage&) final;
+    void newImageAnimationFrameAvailable(CachedImage&, bool& canPause) final;
 
     bool getLeadingCorner(FloatPoint& output, bool& insideFixed) const;
     bool getTrailingCorner(FloatPoint& output, bool& insideFixed) const;
index ae7c6a5..52b0a8e 100644 (file)
@@ -1395,14 +1395,16 @@ void RenderView::updateVisibleViewportRect(const IntRect& visibleRect)
     }
 }
 
-void RenderView::addRendererWithPausedImageAnimations(RenderElement& renderer)
+void RenderView::addRendererWithPausedImageAnimations(RenderElement& renderer, CachedImage& image)
 {
-    if (renderer.hasPausedImageAnimations()) {
-        ASSERT(m_renderersWithPausedImageAnimation.contains(&renderer));
-        return;
-    }
+    ASSERT(!renderer.hasPausedImageAnimations() || m_renderersWithPausedImageAnimation.contains(&renderer));
+
     renderer.setHasPausedImageAnimations(true);
-    m_renderersWithPausedImageAnimation.add(&renderer);
+    auto& images = m_renderersWithPausedImageAnimation.ensure(&renderer, [] {
+        return Vector<CachedImage*>();
+    }).iterator->value;
+    if (!images.contains(&image))
+        images.append(&image);
 }
 
 void RenderView::removeRendererWithPausedImageAnimations(RenderElement& renderer)
@@ -1414,15 +1416,35 @@ void RenderView::removeRendererWithPausedImageAnimations(RenderElement& renderer
     m_renderersWithPausedImageAnimation.remove(&renderer);
 }
 
+void RenderView::removeRendererWithPausedImageAnimations(RenderElement& renderer, CachedImage& image)
+{
+    ASSERT(renderer.hasPausedImageAnimations());
+
+    auto it = m_renderersWithPausedImageAnimation.find(&renderer);
+    ASSERT(it != m_renderersWithPausedImageAnimation.end());
+
+    auto& images = it->value;
+    if (!images.contains(&image))
+        return;
+
+    if (images.size() == 1)
+        removeRendererWithPausedImageAnimations(renderer);
+    else
+        images.removeFirst(&image);
+}
+
 void RenderView::resumePausedImageAnimationsIfNeeded(IntRect visibleRect)
 {
-    Vector<RenderElement*, 10> toRemove;
-    for (auto* renderer : m_renderersWithPausedImageAnimation) {
-        if (renderer->repaintForPausedImageAnimationsIfNeeded(visibleRect))
-            toRemove.append(renderer);
+    Vector<std::pair<RenderElement*, CachedImage*>, 10> toRemove;
+    for (auto& it : m_renderersWithPausedImageAnimation) {
+        auto* renderer = it.key;
+        for (auto* image : it.value) {
+            if (renderer->repaintForPausedImageAnimationsIfNeeded(visibleRect, *image))
+                toRemove.append(std::make_pair(renderer, image));
+        }
     }
-    for (auto& renderer : toRemove)
-        removeRendererWithPausedImageAnimations(*renderer);
+    for (auto& pair : toRemove)
+        removeRendererWithPausedImageAnimations(*pair.first, *pair.second);
 }
 
 RenderView::RepaintRegionAccumulator::RepaintRegionAccumulator(RenderView* view)
index 3bacf7b..8b93156 100644 (file)
@@ -229,8 +229,9 @@ public:
     void registerForVisibleInViewportCallback(RenderElement&);
     void unregisterForVisibleInViewportCallback(RenderElement&);
     void resumePausedImageAnimationsIfNeeded(IntRect visibleRect);
-    void addRendererWithPausedImageAnimations(RenderElement&);
+    void addRendererWithPausedImageAnimations(RenderElement&, CachedImage&);
     void removeRendererWithPausedImageAnimations(RenderElement&);
+    void removeRendererWithPausedImageAnimations(RenderElement&, CachedImage&);
 
     class RepaintRegionAccumulator {
         WTF_MAKE_NONCOPYABLE(RepaintRegionAccumulator);
@@ -389,7 +390,7 @@ private:
     bool m_inHitTesting { false };
 #endif
 
-    HashSet<RenderElement*> m_renderersWithPausedImageAnimation;
+    HashMap<RenderElement*, Vector<CachedImage*>> m_renderersWithPausedImageAnimation;
     HashSet<RenderElement*> m_visibleInViewportRenderers;
     Vector<RefPtr<RenderWidget>> m_protectedRenderWidgets;
 
index 692395b..94aa5ec 100644 (file)
@@ -29,6 +29,7 @@
 #include "CSSImageValue.h"
 #include "CachedImage.h"
 #include "RenderElement.h"
+#include "RenderView.h"
 
 namespace WebCore {
 
@@ -186,6 +187,10 @@ void StyleCachedImage::removeClient(RenderElement* renderer)
     if (!m_cachedImage)
         return;
     ASSERT(renderer);
+
+    if (renderer->hasPausedImageAnimations())
+        renderer->view().removeRendererWithPausedImageAnimations(*renderer, *m_cachedImage);
+
     m_cachedImage->removeClient(*renderer);
 }
 
index a61d29c..d637bb0 100644 (file)
@@ -507,6 +507,11 @@ bool SVGSVGElement::animationsPaused() const
     return m_timeContainer->isPaused();
 }
 
+bool SVGSVGElement::hasActiveAnimation() const
+{
+    return m_timeContainer->isActive();
+}
+
 float SVGSVGElement::getCurrentTime() const
 {
     return narrowPrecisionToFloat(m_timeContainer->elapsed().value());
index ef54f12..be3dde6 100644 (file)
@@ -85,6 +85,7 @@ public: // DOM
     void pauseAnimations();
     void unpauseAnimations();
     bool animationsPaused() const;
+    bool hasActiveAnimation() const;
 
     float getCurrentTime() const;
     void setCurrentTime(float);
index 6884a1b..65f4bdc 100644 (file)
@@ -374,7 +374,7 @@ void SVGImage::computeIntrinsicDimensions(Length& intrinsicWidth, Length& intrin
 void SVGImage::startAnimation()
 {
     SVGSVGElement* rootElement = this->rootElement();
-    if (!rootElement)
+    if (!rootElement || !rootElement->animationsPaused())
         return;
     rootElement->unpauseAnimations();
     rootElement->setCurrentTime(0);
@@ -393,6 +393,14 @@ void SVGImage::resetAnimation()
     stopAnimation();
 }
 
+bool SVGImage::isAnimating() const
+{
+    SVGSVGElement* rootElement = this->rootElement();
+    if (!rootElement)
+        return false;
+    return rootElement->hasActiveAnimation();
+}
+
 void SVGImage::reportApproximateMemoryCost() const
 {
     Document* document = m_page->mainFrame().document();
index 844c910..aad8051 100644 (file)
@@ -63,6 +63,7 @@ public:
     void startAnimation() final;
     void stopAnimation() final;
     void resetAnimation() final;
+    bool isAnimating() const final;
 
 #if USE(CAIRO)
     NativeImagePtr nativeImageForCurrentFrame(const GraphicsContext* = nullptr) final;
index bee03d5..624c940 100644 (file)
@@ -29,6 +29,7 @@
 #pragma once
 
 #include "EmptyClients.h"
+#include "SVGImage.h"
 
 namespace WebCore {
 
@@ -52,8 +53,17 @@ private:
     void invalidateContentsAndRootView(const IntRect& r) final
     {
         // If m_image->m_page is null, we're being destructed, don't fire changedInRect() in that case.
-        if (m_image && m_image->imageObserver() && m_image->m_page)
-            m_image->imageObserver()->changedInRect(m_image, &r);
+        if (!m_image || !m_image->m_page)
+            return;
+
+        auto* imageObserver = m_image->imageObserver();
+        if (!imageObserver)
+            return;
+
+        if (m_image->isAnimating())
+            imageObserver->animationAdvanced(m_image);
+        else
+            imageObserver->changedInRect(m_image, &r);
     }
     
     SVGImage* m_image;
index 9d390f9..01eecb8 100644 (file)
@@ -744,6 +744,19 @@ void Internals::resetImageAnimation(HTMLImageElement& element)
     image->resetAnimation();
 }
 
+bool Internals::isImageAnimating(HTMLImageElement& element)
+{
+    auto* cachedImage = element.cachedImage();
+    if (!cachedImage)
+        return false;
+
+    auto* image = cachedImage->image();
+    if (!image)
+        return false;
+
+    return image->isAnimating();
+}
+
 void Internals::setClearDecoderAfterAsyncFrameRequestForTesting(HTMLImageElement& element, bool value)
 {
     auto* cachedImage = element.cachedImage();
index 383d05c..d67bc61 100644 (file)
@@ -115,6 +115,7 @@ public:
     unsigned imageFrameIndex(HTMLImageElement&);
     void setImageFrameDecodingDuration(HTMLImageElement&, float duration);
     void resetImageAnimation(HTMLImageElement&);
+    bool isImageAnimating(HTMLImageElement&);
     void setClearDecoderAfterAsyncFrameRequestForTesting(HTMLImageElement&, bool);
 
     void clearPageCache();
index 722507c..581ae34 100644 (file)
@@ -244,6 +244,7 @@ enum EventThrottlingBehavior {
     unsigned long imageFrameIndex(HTMLImageElement element);
     void setImageFrameDecodingDuration(HTMLImageElement element, unrestricted float duration);
     void resetImageAnimation(HTMLImageElement element);
+    boolean isImageAnimating(HTMLImageElement element);
     void setClearDecoderAfterAsyncFrameRequestForTesting(HTMLImageElement element, boolean value);
 
     readonly attribute InternalSettings settings;