Title: [272201] trunk
Revision
272201
Author
[email protected]
Date
2021-02-02 02:59:00 -0800 (Tue, 02 Feb 2021)

Log Message

Animation of "rotate" or "scale" property does not correctly account for static "translate" property
https://bugs.webkit.org/show_bug.cgi?id=219894
<rdar://problem/72342798>

Reviewed by Dean Jackson.

Source/WebCore:

The CSS transform-related properties are designed to be applied in a specific order, guaranteeing that
"translate" is applied prior to both "scale" and "rotate". Since Core Animation has no concept of these
individual transform-related CSS properties, we use additive Core Animation animations to apply the value
of each CSS property, using non-interpolating animations set to start at the earliest time in the Core
Animation timeline and lasting forever to set the value of any underlying, non-animated value.

As such, in an example where an element would have a static "translate" property set as well as a "rotate"
or "scale" animation, we would yield the following animations, added in this order:

    1. non-interpolating animation beginning at 1s setting the identity transform (the "clean slate" animation)
    2. interpolating animation beginning at a time > 1s for the "scale" or "rotate" animation
    3. non-interpolating animation beginning at 1s setting the "translate" value

Note that animations 2 and 3 are additive and thus added in the inverse order that we expect animations to be
applied. Due to a peculiarity of Core Animation (introduced in macOS 10.15), additive animations are applied
in an inverse order, hence the build-time flag CA_WHERE_ADDITIVE_TRANSFORMS_ARE_REVERSED.

However, Core Animation will first sort all animations based on their begin time, only respecting the order
in which animations are added when their begin time is equal. This means that in practice, our animations were
applied in the order 1, 3, 2, and thus the "translate" property was applied after the "rotate" or "scale" animation.

In order to address this, we now create a CAAnimationGroup for each set of animations created for a given CSS
property. Each of these groups shares the same begin time, 1s, to allow for "forever" non-interpolating animations
to be applied, but also to set a common base time for animations to be applied in the expected order.

Tests: webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated.html
       webanimations/relative-ordering-of-translate-and-scale-properties-accelerated.html

* platform/graphics/ca/GraphicsLayerCA.cpp:
(WebCore::GraphicsLayerCA::updateAnimations):
* platform/graphics/ca/GraphicsLayerCA.h:
(WebCore::GraphicsLayerCA::LayerPropertyAnimation::computedBeginTime const):

LayoutTests:

Add two new tests that ensure that translate is indeed applied before rotate and scale.

* webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated-expected.html: Added.
* webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated.html: Added.
* webanimations/relative-ordering-of-translate-and-scale-properties-accelerated-expected.html: Added.
* webanimations/relative-ordering-of-translate-and-scale-properties-accelerated.html: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (272200 => 272201)


--- trunk/LayoutTests/ChangeLog	2021-02-02 10:25:52 UTC (rev 272200)
+++ trunk/LayoutTests/ChangeLog	2021-02-02 10:59:00 UTC (rev 272201)
@@ -1,3 +1,18 @@
+2021-02-01  Antoine Quint  <[email protected]>
+
+        Animation of "rotate" or "scale" property does not correctly account for static "translate" property
+        https://bugs.webkit.org/show_bug.cgi?id=219894
+        <rdar://problem/72342798>
+
+        Reviewed by Dean Jackson.
+
+        Add two new tests that ensure that translate is indeed applied before rotate and scale.
+
+        * webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated-expected.html: Added.
+        * webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated.html: Added.
+        * webanimations/relative-ordering-of-translate-and-scale-properties-accelerated-expected.html: Added.
+        * webanimations/relative-ordering-of-translate-and-scale-properties-accelerated.html: Added.
+
 2021-02-02  Rob Buis  <[email protected]>
 
         Provide mock dtmf sender

Added: trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated-expected.html (0 => 272201)


--- trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated-expected.html	                        (rev 0)
+++ trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated-expected.html	2021-02-02 10:59:00 UTC (rev 272201)
@@ -0,0 +1,26 @@
+<style>
+
+    div {
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100px;
+        width: 100px;
+        background-color: black;
+
+        translate: 100px 100px;
+        rotate: 180deg;
+    }
+
+    div::after {
+        content: "";
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 50%;
+        height: 50%;
+        background-color: gray;
+    }
+
+</style>
+<div></div>

Added: trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated.html (0 => 272201)


--- trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated.html	                        (rev 0)
+++ trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated.html	2021-02-02 10:59:00 UTC (rev 272201)
@@ -0,0 +1,32 @@
+<style>
+
+    div {
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100px;
+        width: 100px;
+        background-color: black;
+
+        translate: 100px 100px;
+        animation: rotate calc(24 * 60 * 60 * 1s);
+    }
+
+    div::after {
+        content: "";
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 50%;
+        height: 50%;
+        background-color: gray;
+    }
+
+    @keyframes rotate {
+        0% { rotate: 180deg; }
+        100% { rotate: 180deg; }
+    }
+
+</style>
+<div></div>
+<script src=""

Added: trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-scale-properties-accelerated-expected.html (0 => 272201)


--- trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-scale-properties-accelerated-expected.html	                        (rev 0)
+++ trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-scale-properties-accelerated-expected.html	2021-02-02 10:59:00 UTC (rev 272201)
@@ -0,0 +1,16 @@
+<style>
+
+    div {
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100px;
+        width: 100px;
+        background-color: black;
+
+        translate: 100px 100px;
+        scale: 0.5;
+    }
+
+</style>
+<div></div>

Added: trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-scale-properties-accelerated.html (0 => 272201)


--- trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-scale-properties-accelerated.html	                        (rev 0)
+++ trunk/LayoutTests/webanimations/relative-ordering-of-translate-and-scale-properties-accelerated.html	2021-02-02 10:59:00 UTC (rev 272201)
@@ -0,0 +1,22 @@
+<style>
+
+    div {
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100px;
+        width: 100px;
+        background-color: black;
+
+        translate: 100px 100px;
+        animation: scale calc(24 * 60 * 60 * 1s);
+    }
+
+    @keyframes scale {
+        0% { scale: 0.5; }
+        100% { scale: 0.5; }
+    }
+
+</style>
+<div></div>
+<script src=""

Modified: trunk/Source/WebCore/ChangeLog (272200 => 272201)


--- trunk/Source/WebCore/ChangeLog	2021-02-02 10:25:52 UTC (rev 272200)
+++ trunk/Source/WebCore/ChangeLog	2021-02-02 10:59:00 UTC (rev 272201)
@@ -1,3 +1,44 @@
+2021-02-01  Antoine Quint  <[email protected]>
+
+        Animation of "rotate" or "scale" property does not correctly account for static "translate" property
+        https://bugs.webkit.org/show_bug.cgi?id=219894
+        <rdar://problem/72342798>
+
+        Reviewed by Dean Jackson.
+
+        The CSS transform-related properties are designed to be applied in a specific order, guaranteeing that
+        "translate" is applied prior to both "scale" and "rotate". Since Core Animation has no concept of these
+        individual transform-related CSS properties, we use additive Core Animation animations to apply the value
+        of each CSS property, using non-interpolating animations set to start at the earliest time in the Core
+        Animation timeline and lasting forever to set the value of any underlying, non-animated value.
+
+        As such, in an example where an element would have a static "translate" property set as well as a "rotate"
+        or "scale" animation, we would yield the following animations, added in this order:
+        
+            1. non-interpolating animation beginning at 1s setting the identity transform (the "clean slate" animation)
+            2. interpolating animation beginning at a time > 1s for the "scale" or "rotate" animation
+            3. non-interpolating animation beginning at 1s setting the "translate" value
+
+        Note that animations 2 and 3 are additive and thus added in the inverse order that we expect animations to be
+        applied. Due to a peculiarity of Core Animation (introduced in macOS 10.15), additive animations are applied
+        in an inverse order, hence the build-time flag CA_WHERE_ADDITIVE_TRANSFORMS_ARE_REVERSED.
+
+        However, Core Animation will first sort all animations based on their begin time, only respecting the order
+        in which animations are added when their begin time is equal. This means that in practice, our animations were
+        applied in the order 1, 3, 2, and thus the "translate" property was applied after the "rotate" or "scale" animation.
+
+        In order to address this, we now create a CAAnimationGroup for each set of animations created for a given CSS
+        property. Each of these groups shares the same begin time, 1s, to allow for "forever" non-interpolating animations
+        to be applied, but also to set a common base time for animations to be applied in the expected order.
+
+        Tests: webanimations/relative-ordering-of-translate-and-rotate-properties-accelerated.html
+               webanimations/relative-ordering-of-translate-and-scale-properties-accelerated.html
+
+        * platform/graphics/ca/GraphicsLayerCA.cpp:
+        (WebCore::GraphicsLayerCA::updateAnimations):
+        * platform/graphics/ca/GraphicsLayerCA.h:
+        (WebCore::GraphicsLayerCA::LayerPropertyAnimation::computedBeginTime const):
+
 2021-02-02  Youenn Fablet  <[email protected]>
 
         imported/w3c/web-platform-tests/mediacapture-record/MediaRecorder-stop.html is flaky when GPUProcess is enabled

Modified: trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp (272200 => 272201)


--- trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp	2021-02-02 10:25:52 UTC (rev 272200)
+++ trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp	2021-02-02 10:59:00 UTC (rev 272201)
@@ -2888,44 +2888,62 @@
 
 void GraphicsLayerCA::updateAnimations()
 {
-    auto baseTransformAnimationBeginTime = 1_s;
+    // In order to guarantee that transform animations are applied in the expected order (translate, rotate, scale and transform),
+    // we need to have them wrapped individually in an animation group because Core Animation sorts animations first by their begin
+    // time, and then by the order in which they were added (for those with the same begin time). Since a rotate animation can have
+    // an earlier begin time than a translate animation, we cannot rely on adding the animations in the correct order.
+    //
+    // Having an animation group wrapping each animation means that we can guarantee the order in which animations are applied by
+    // ensuring they each have the same begin time. We set this begin time to be the smallest value possible, ensuring that base
+    // transform animations are applied continuously. We'll then set the begin time of interpolating animations to be local to the
+    // animation group, which means subtracting the group's begin time.
+
+    // We use 1_s here because 0_s would have special meaning in Core Animation, meaning that the animation would have its begin
+    // time set to the current time when it's committed.
+    auto animationGroupBeginTime = 1_s;
+    auto infiniteDuration = std::numeric_limits<double>::max();
     auto currentTime = Seconds(CACurrentMediaTime());
-    auto updateBeginTimes = [&](LayerPropertyAnimation& animation)
-    {
-        if (animation.m_pendingRemoval)
-            return;
 
-        // In case we have an offset, and we haven't set an explicit begin time previously,
-        // we need to record the beginTime now.
-        if (animation.m_timeOffset && !animation.m_beginTime)
-            animation.m_beginTime = currentTime;
+    auto addAnimationGroup = [&](AnimatedPropertyID property, const Vector<RefPtr<PlatformCAAnimation>>& animations) {
+        auto caAnimationGroup = createPlatformCAAnimation(PlatformCAAnimation::Group, "");
+        caAnimationGroup->setDuration(infiniteDuration);
+        caAnimationGroup->setAnimations(animations);
 
-        // Now check if we have a resolved begin time and ensure the begin time we'll use for
-        // base transform animations matches the smallest known begin time to guarantee that
-        // such animations can combine with other explicit transform animations correctly.
-        if (auto computedBeginTime = animation.computedBeginTime())
-            baseTransformAnimationBeginTime = std::min(baseTransformAnimationBeginTime, *computedBeginTime);
+        auto animationGroup = LayerPropertyAnimation(WTFMove(caAnimationGroup), "group-" + createCanonicalUUIDString(), property, 0, 0, 0_s);
+        animationGroup.m_beginTime = animationGroupBeginTime;
+
+        setAnimationOnLayer(animationGroup);
+        m_animationGroups.append(WTFMove(animationGroup));
     };
 
     enum class Additive { Yes, No };
-    auto addAnimation = [&](LayerPropertyAnimation& animation, Additive additive = Additive::Yes) {
-        animation.m_animation->setAdditive(additive == Additive::Yes);
-        setAnimationOnLayer(animation);
+    auto prepareAnimationForAddition = [&](LayerPropertyAnimation& animation, Additive additive = Additive::Yes) {
+        auto caAnim = animation.m_animation;
+        caAnim->setAdditive(additive == Additive::Yes);
+        if (auto beginTime = animation.computedBeginTime())
+            caAnim->setBeginTime(beginTime->seconds());
+
         if (animation.m_playState == PlayState::PausePending || animation.m_playState == PlayState::Paused) {
-            pauseCAAnimationOnLayer(animation);
+            caAnim->setSpeed(0);
+            caAnim->setTimeOffset(animation.m_timeOffset.seconds());
             animation.m_playState = PlayState::Paused;
         } else
             animation.m_playState = PlayState::Playing;
     };
 
+    auto addAnimation = [&](LayerPropertyAnimation& animation, Additive additive = Additive::Yes) {
+        prepareAnimationForAddition(animation, additive);
+        addAnimationGroup(animation.m_property, { animation.m_animation });
+    };
+
     enum class TransformationMatrixSource { UseIdentityMatrix, AskClient };
-    auto addBaseValueTransformAnimation = [&](AnimatedPropertyID property, TransformationMatrixSource matrixSource = TransformationMatrixSource::AskClient, Seconds beginTimeOfEarliestPropertyAnimation = 0_s) {
+    auto makeBaseValueTransformAnimation = [&](AnimatedPropertyID property, TransformationMatrixSource matrixSource = TransformationMatrixSource::AskClient, Seconds beginTimeOfEarliestPropertyAnimation = 0_s) -> LayerPropertyAnimation* {
         // A base value transform animation can either be set to the identity matrix or to read the underlying
         // value from the GraphicsLayerClient. If we didn't explicitly ask for an identity matrix, we can skip
         // the addition of this base value transform animation since it will be a no-op.
         auto matrix = matrixSource == TransformationMatrixSource::UseIdentityMatrix ? TransformationMatrix() : client().transformMatrixForProperty(property);
         if (matrixSource == TransformationMatrixSource::AskClient && matrix.isIdentity())
-            return;
+            return nullptr;
 
         auto delay = beginTimeOfEarliestPropertyAnimation > currentTime ? beginTimeOfEarliestPropertyAnimation - currentTime : 0_s;
 
@@ -2933,41 +2951,45 @@
         // unless we're just filling until an animation for this property starts, in which case it must last for duration
         // of the delay until that animation.
         auto caAnimation = createPlatformCAAnimation(PlatformCAAnimation::Basic, propertyIdToString(property));
-        caAnimation->setDuration((delay ? delay : Seconds::infinity()).seconds());
+        caAnimation->setDuration(delay ? delay.seconds() : infiniteDuration);
         caAnimation->setFromValue(matrix);
         caAnimation->setToValue(matrix);
 
         auto animation = LayerPropertyAnimation(WTFMove(caAnimation), "base-transform-" + createCanonicalUUIDString(), property, 0, 0, 0_s);
-        // To ensure the base value transform is applied along with all the interpolating animations, we set it to have started
-        // as early as possible, which combined with the infinite duration ensures it's current for any given CA media time,
-        // unless we're just filling until an animation for this property starts, in which case it must start now.
-        animation.m_beginTime = delay ? currentTime : baseTransformAnimationBeginTime;
+        if (delay)
+            animation.m_beginTime = currentTime - animationGroupBeginTime;
 
+        m_baseValueTransformAnimations.append(WTFMove(animation));
+        return &m_baseValueTransformAnimations.last();
+    };
+
+    auto addBaseValueTransformAnimation = [&](AnimatedPropertyID property, TransformationMatrixSource matrixSource = TransformationMatrixSource::AskClient, Seconds beginTimeOfEarliestPropertyAnimation = 0_s) {
         // Additivity will depend on the source of the matrix, if it was explicitly provided as an identity matrix, it
         // is the initial base value transform animation and must override the current transform value for this layer.
         // Otherwise, it is meant to apply the underlying value for one specific transform-related property and be additive
         // to be combined with the other base value transform animations and interpolating animations.
-        addAnimation(animation, matrixSource == TransformationMatrixSource::AskClient ? Additive::Yes : Additive::No);
-        m_baseValueTransformAnimations.append(WTFMove(animation));
+        if (auto* animation = makeBaseValueTransformAnimation(property, matrixSource, beginTimeOfEarliestPropertyAnimation))
+            addAnimation(*animation, matrixSource == TransformationMatrixSource::AskClient ? Additive::Yes : Additive::No);
     };
 
-    // Iterate through all animations to update each animation's begin time, if necessary,
-    // compute the base transform animation begin times and remove all running CA animations.
+    // Iterate through all animations to set the begin time of any new animations.
     for (auto& animation : m_animations) {
-        updateBeginTimes(animation);
-        if (animation.m_playState == PlayState::Playing || animation.m_playState == PlayState::Paused)
-            removeCAAnimationFromLayer(animation);
+        if (!animation.m_pendingRemoval && !animation.m_beginTime)
+            animation.m_beginTime = currentTime - animationGroupBeginTime;
     }
 
-    // Also remove all the base value transform CA animations.
-    for (auto& animation : m_baseValueTransformAnimations)
-        removeCAAnimationFromLayer(animation);
+    // Now, remove all animation groups from the layer so that we no longer have any layer animations.
+    for (auto& animationGroup : m_animationGroups)
+        removeCAAnimationFromLayer(animationGroup);
 
-    // Now remove all the animations marked as pending removal and all base value transform animations.
+    // We can remove all previously-created base value transform animations and animation groups.
+    m_baseValueTransformAnimations.clear();
+    m_animationGroups.clear();
+
+    // Now remove all the animations marked as pending removal.
     m_animations.removeAllMatching([&](LayerPropertyAnimation animation) {
         return animation.m_pendingRemoval;
     });
-    m_baseValueTransformAnimations.clear();
 
     // Now that our list of animations is current, we can separate animations by property so that
     // we can apply them in order. We only need to apply the last animation applied for a given
@@ -3049,11 +3071,22 @@
                 }
             }
 
-            if (earliestBeginTime > currentTime)
-                addBaseValueTransformAnimation(property, TransformationMatrixSource::AskClient, earliestBeginTime);
+            if (earliestBeginTime)
+                earliestBeginTime += animationGroupBeginTime;
 
-            for (auto* animation : animations)
-                addAnimation(*animation);
+            if (earliestBeginTime > currentTime) {
+                if (auto* baseValueTransformAnimation = makeBaseValueTransformAnimation(property, TransformationMatrixSource::AskClient, earliestBeginTime)) {
+                    prepareAnimationForAddition(*baseValueTransformAnimation);
+                    caAnimations.append(baseValueTransformAnimation->m_animation);
+                }
+            }
+
+            for (auto* animation : animations) {
+                prepareAnimationForAddition(*animation);
+                caAnimations.append(animation->m_animation);
+            }
+
+            addAnimationGroup(property, caAnimations);
         };
 
         addAnimationsForProperty(translateAnimations, AnimatedPropertyTranslate);
@@ -3068,16 +3101,27 @@
             }
 
             Seconds earliestBeginTime = 0_s;
+            Vector<RefPtr<PlatformCAAnimation>> caAnimations;
             for (auto* animation : WTF::makeReversedRange(animations)) {
                 if (auto beginTime = animation->computedBeginTime()) {
                     if (!earliestBeginTime || earliestBeginTime > *beginTime)
                         earliestBeginTime = *beginTime;
                 }
-                addAnimation(*animation);
+                prepareAnimationForAddition(*animation);
+                caAnimations.append(animation->m_animation);
             }
 
-            if (earliestBeginTime > currentTime)
-                addBaseValueTransformAnimation(property, TransformationMatrixSource::AskClient, earliestBeginTime);
+            if (earliestBeginTime)
+                earliestBeginTime += animationGroupBeginTime;
+
+            if (earliestBeginTime > currentTime) {
+                if (auto* baseValueTransformAnimation = makeBaseValueTransformAnimation(property, TransformationMatrixSource::AskClient, earliestBeginTime)) {
+                    prepareAnimationForAddition(*baseValueTransformAnimation);
+                    caAnimations.append(baseValueTransformAnimation->m_animation);
+                }
+            }
+
+            addAnimationGroup(property, caAnimations);
         };
 
         addAnimationsForProperty(transformAnimations, AnimatedPropertyTransform);
@@ -4440,8 +4484,10 @@
     Vector<std::pair<String, double>> animations;
 
     for (auto& animation : m_animations) {
-        auto caAnimation = animatedLayer(animation.m_property)->animationForKey(animation.animationIdentifier());
-        animations.append({ animatedPropertyIDAsString(animation.m_property), caAnimation->speed() });
+        if (auto caAnimation = animatedLayer(animation.m_property)->animationForKey(animation.animationIdentifier()))
+            animations.append({ animatedPropertyIDAsString(animation.m_property), caAnimation->speed() });
+        else
+            animations.append({ animatedPropertyIDAsString(animation.m_property), (animation.m_playState == PlayState::Playing || animation.m_playState == PlayState::PlayPending) ? 1 : 0 });
     }
 
     return animations;

Modified: trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.h (272200 => 272201)


--- trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.h	2021-02-02 10:25:52 UTC (rev 272200)
+++ trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.h	2021-02-02 10:59:00 UTC (rev 272201)
@@ -474,7 +474,7 @@
         Optional<Seconds> computedBeginTime() const
         {
             if (m_beginTime)
-                return m_beginTime - m_timeOffset;
+                return *m_beginTime - m_timeOffset;
             return WTF::nullopt;
         }
 
@@ -484,7 +484,7 @@
         int m_index;
         int m_subIndex;
         Seconds m_timeOffset { 0_s };
-        Seconds m_beginTime { 0_s };
+        Optional<Seconds> m_beginTime;
         PlayState m_playState { PlayState::PlayPending };
         bool m_pendingRemoval { false };
     };
@@ -616,6 +616,7 @@
     
     Vector<LayerPropertyAnimation> m_animations;
     Vector<LayerPropertyAnimation> m_baseValueTransformAnimations;
+    Vector<LayerPropertyAnimation> m_animationGroups;
 
     Vector<FloatRect> m_dirtyRects;
 
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to