[web-animations] support custom properties in @keyframes rules
https://bugs.webkit.org/show_bug.cgi?id=241843

Reviewed by Antti Koivisto.

We start work towards broad support of custom properties in animations by adding support
for custom properties in @keyframes rules.

The first thing required is to allow KeyframeList and KeyframeValue to track custom properties
on top of common properties (CSSPropertyID). To that end, we add HashSet<AtomString> members
to both those classes and ensure KeyframeList::insert() adds any new custom property found
on the inserted KeyframeValue to the KeyframeList. Dedicated methods, such as
KeyframeValue::addCustomProperty() allow setting what custom properties are found on the
KeyframeValue.

Then, we need to detect that we have custom properties set in @keyframes rules. We update
Style::Resolver::styleForKeyframe() to determine whether a property in a keyframe is a
CSSCustomPropertyValue, and if it is, we get its name and use KeyframeValue::addCustomProperty()
to add it to the keyframe.

Finally, we must actually blend custom properties. Since custom properties are only animated
discretely, the logic here is simple and we add a new CSSPropertyAnimation::blendCustomProperty()
method which simply gets the custom property from either the from or to style depending on what side
of the 0.5 boundary we are.

We call this new method from KeyframeEffect::setAnimatedPropertiesInStyle() where we must now not
only iterate through common properties (CSSPropertyID) but also custom properties. We now wrap much
of that function's logic into a lambda which takes in an std::variant<CSSPropertyID, AtomString> type
with some simple checks to deal with either type in the lambda body.

* LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-inner-at-rules-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-longhand-animation-type-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/at-property-animation-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/font-size-animation-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/registered-property-revert-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-from-to-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-over-transition-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-to-only-expected.txt:
* Source/WebCore/animation/CSSPropertyAnimation.cpp:
(WebCore::CSSPropertyAnimation::blendProperties):
(WebCore::CSSPropertyAnimation::blendCustomProperty):
* Source/WebCore/animation/CSSPropertyAnimation.h:
* Source/WebCore/animation/KeyframeEffect.cpp:
(WebCore::KeyframeEffect::setAnimatedPropertiesInStyle):
* Source/WebCore/rendering/style/KeyframeList.cpp:
(WebCore::KeyframeList::insert):
(WebCore::KeyframeList::copyKeyframes):
(WebCore::KeyframeList::containsAnimatableProperty const): Any and all custom properties is considered animatable.
* Source/WebCore/rendering/style/KeyframeList.h:
(WebCore::KeyframeValue::addCustomProperty):
(WebCore::KeyframeValue::containsCustomProperty const):
(WebCore::KeyframeValue::customProperties const):
(WebCore::KeyframeList::addCustomProperty):
(WebCore::KeyframeList::containsCustomProperty const):
(WebCore::KeyframeList::customProperties const):
* Source/WebCore/style/StyleResolver.cpp:
(WebCore::Style::Resolver::styleForKeyframe):

Canonical link: https://commits.webkit.org/251733@main

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@295728 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-inner-at-rules-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-inner-at-rules-expected.txt
index 26dc8f7..b16ee5c 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-inner-at-rules-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-inner-at-rules-expected.txt
@@ -1,5 +1,5 @@
 
-FAIL @keyframes is defined regardless of evaluation assert_equals: expected "true" but got ""
+PASS @keyframes is defined regardless of evaluation
 FAIL @property is defined regardless of evaluation assert_equals: expected "20px" but got "1em"
 PASS @layer order respected regardless of evaluation
 PASS @font-face is defined regardless of evaluation
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-longhand-animation-type-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-longhand-animation-type-expected.txt
index 4149828..69c8b7e 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-longhand-animation-type-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/container-longhand-animation-type-expected.txt
@@ -1,5 +1,5 @@
 
-FAIL Reference variable is applied assert_equals: expected "PASS" but got "FAIL"
+PASS Reference variable is applied
 PASS container-name is not animatable
 PASS container-type is not animatable
 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/at-property-animation-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/at-property-animation-expected.txt
index 10f8cfc..2abe6d9 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/at-property-animation-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/at-property-animation-expected.txt
@@ -1,6 +1,6 @@
 
-FAIL @keyframes works with @property assert_equals: expected "150px" but got ""
-FAIL @keyframes picks up the latest @property in the document assert_equals: expected "rgb(150, 150, 150)" but got ""
+FAIL @keyframes works with @property assert_equals: expected "150px" but got "200px"
+FAIL @keyframes picks up the latest @property in the document assert_equals: expected "rgb(150, 150, 150)" but got "rgb(200, 200, 200)"
 FAIL Ongoing animation picks up redeclared custom property assert_equals: expected "0px" but got ""
 FAIL Ongoing animation matches new keyframes against the current registration assert_equals: expected "0px" but got ""
 FAIL Ongoing animation picks up redeclared intial value assert_equals: expected "200px" but got ""
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/font-size-animation-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/font-size-animation-expected.txt
index 10c4dbe..92e025f 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/font-size-animation-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/font-size-animation-expected.txt
@@ -1,3 +1,3 @@
 
-FAIL Animating font-size handled identically for standard and custom properties assert_equals: expected "0px" but got "250px"
+FAIL Animating font-size handled identically for standard and custom properties assert_equals: expected "400px" but got "250px"
 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/registered-property-revert-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/registered-property-revert-expected.txt
index 7678503..8f17daf 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/registered-property-revert-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-properties-values-api/registered-property-revert-expected.txt
@@ -1,6 +1,6 @@
 
 PASS Inherited registered custom property can be reverted
 PASS Non-inherited registered custom property can be reverted
-FAIL Non-inherited registered custom property can be reverted in animation assert_equals: expected "50px" but got "0px"
-FAIL Inherited registered custom property can be reverted in animation assert_equals: expected "50px" but got "0px"
+FAIL Non-inherited registered custom property can be reverted in animation assert_equals: expected "50px" but got "100px"
+FAIL Inherited registered custom property can be reverted in animation assert_equals: expected "50px" but got "100px"
 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-from-to-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-from-to-expected.txt
index 5a663fc..8f07422 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-from-to-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-from-to-expected.txt
@@ -1,7 +1,7 @@
 This text should animate from blue to green over a period of 1 second.
 
-FAIL Verify CSS variable value before animation assert_equals: --value is blue before animation runs expected "blue" but got "red"
+PASS Verify CSS variable value before animation
 FAIL Verify substituted color value before animation assert_equals: color is blue before animation runs expected "rgb(0, 0, 255)" but got "rgb(255, 0, 0)"
-FAIL Verify CSS variable value after animation assert_equals: --value is green after animation runs expected "green" but got "red"
+PASS Verify CSS variable value after animation
 FAIL Verify substituted color value after animation assert_equals: color is green after animation runs expected "rgb(0, 128, 0)" but got "rgb(255, 0, 0)"
 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-over-transition-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-over-transition-expected.txt
index bd01c72..e992e33 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-over-transition-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-over-transition-expected.txt
@@ -1,7 +1,7 @@
 This text should animate from blue to green over a period of 1 second. The transition is not visible because the animation overrides it.
 
-FAIL Verify CSS variable value before animation assert_equals: --value is blue before animation runs expected "blue" but got "red"
+PASS Verify CSS variable value before animation
 FAIL Verify substituted color value before animation assert_equals: color is blue before animation runs expected "rgb(0, 0, 255)" but got "rgb(255, 0, 0)"
-FAIL Verify CSS variable value after animation assert_equals: --value is green after animation runs expected "green" but got "black"
+PASS Verify CSS variable value after animation
 FAIL Verify substituted color value after animation assert_equals: color is green after animation runs expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-to-only-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-to-only-expected.txt
index 3d593c1..e345b0d 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-to-only-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/css/css-variables/variable-animation-to-only-expected.txt
@@ -1,5 +1,5 @@
 This text should animate from blue to green over a period of 1 second.
 
 PASS Verify CSS variable value before animation
-FAIL Verify CSS variable value after animation assert_equals: --value is green after animation runs expected "green" but got "blue"
+PASS Verify CSS variable value after animation
 
diff --git a/Source/WebCore/animation/CSSPropertyAnimation.cpp b/Source/WebCore/animation/CSSPropertyAnimation.cpp
index 97dfc2f..c28b5c3 100644
--- a/Source/WebCore/animation/CSSPropertyAnimation.cpp
+++ b/Source/WebCore/animation/CSSPropertyAnimation.cpp
@@ -3666,7 +3666,7 @@
 
 void CSSPropertyAnimation::blendProperties(const CSSPropertyBlendingClient* client, CSSPropertyID property, RenderStyle& destination, const RenderStyle& from, const RenderStyle& to, double progress, CompositeOperation compositeOperation)
 {
-    ASSERT(property != CSSPropertyInvalid);
+    ASSERT(property != CSSPropertyInvalid && property != CSSPropertyCustom);
 
     AnimationPropertyWrapperBase* wrapper = CSSPropertyAnimationWrapperMap::singleton().wrapperForProperty(property);
     if (wrapper) {
@@ -3686,6 +3686,15 @@
     }
 }
 
+void CSSPropertyAnimation::blendCustomProperty(const AtomString& customProperty, RenderStyle& destination, const RenderStyle& from, const RenderStyle& to, double progress)
+{
+    const auto& source = progress < 0.5 ? from : to;
+    if (auto nonInheritedValue = source.nonInheritedCustomProperties().get(customProperty))
+        destination.setNonInheritedCustomPropertyValue(customProperty, CSSCustomPropertyValue::create(*nonInheritedValue));
+    else if (auto inheritedValue = source.inheritedCustomProperties().get(customProperty))
+        destination.setInheritedCustomPropertyValue(customProperty, CSSCustomPropertyValue::create(*inheritedValue));
+}
+
 bool CSSPropertyAnimation::isPropertyAnimatable(CSSPropertyID property)
 {
     return CSSPropertyAnimationWrapperMap::singleton().wrapperForProperty(property);
diff --git a/Source/WebCore/animation/CSSPropertyAnimation.h b/Source/WebCore/animation/CSSPropertyAnimation.h
index 2e01a5e..c6b2b1a 100644
--- a/Source/WebCore/animation/CSSPropertyAnimation.h
+++ b/Source/WebCore/animation/CSSPropertyAnimation.h
@@ -48,6 +48,7 @@
     static int getNumProperties();
 
     static void blendProperties(const CSSPropertyBlendingClient*, CSSPropertyID, RenderStyle& destination, const RenderStyle& from, const RenderStyle& to, double progress, CompositeOperation);
+    static void blendCustomProperty(const AtomString&, RenderStyle& destination, const RenderStyle& from, const RenderStyle& to, double progress);
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/animation/KeyframeEffect.cpp b/Source/WebCore/animation/KeyframeEffect.cpp
index e962c80..a004ccb 100644
--- a/Source/WebCore/animation/KeyframeEffect.cpp
+++ b/Source/WebCore/animation/KeyframeEffect.cpp
@@ -1399,7 +1399,14 @@
     KeyframeValue propertySpecificKeyframeWithZeroOffset(0, RenderStyle::clonePtr(targetStyle));
     KeyframeValue propertySpecificKeyframeWithOneOffset(1, RenderStyle::clonePtr(targetStyle));
 
-    for (auto cssPropertyId : properties) {
+    auto keyframeContainsProperty = [](const KeyframeValue& keyframe, std::variant<CSSPropertyID, AtomString> property) {
+        return WTF::switchOn(property,
+            [&] (CSSPropertyID propertyId) { return keyframe.containsProperty(propertyId); },
+            [&] (AtomString customProperty) { return keyframe.containsCustomProperty(customProperty); }
+        );
+    };
+
+    auto blendProperty = [&](std::variant<CSSPropertyID, AtomString> property) {
         // 1. If iteration progress is unresolved abort this procedure.
         // 2. Let target property be the longhand property for which the effect value is to be calculated.
         // 3. If animation type of the target property is not animatable abort this procedure since the effect cannot be applied.
@@ -1413,7 +1420,7 @@
         Vector<const KeyframeValue*> propertySpecificKeyframes;
         for (auto& keyframe : m_blendingKeyframes) {
             auto offset = keyframe.key();
-            if (!keyframe.containsProperty(cssPropertyId))
+            if (!keyframeContainsProperty(keyframe, property))
                 continue;
             if (!offset)
                 numberOfKeyframesWithZeroOffset++;
@@ -1424,7 +1431,7 @@
 
         // 7. If property-specific keyframes is empty, return underlying value.
         if (propertySpecificKeyframes.isEmpty())
-            continue;
+            return;
 
         auto hasImplicitZeroKeyframe = !numberOfKeyframesWithZeroOffset;
         auto hasImplicitOneKeyframe = !numberOfKeyframesWithOneOffset;
@@ -1500,29 +1507,35 @@
         //         3. Replace the property value of target property on keyframe with the result of combining underlying value
         //            (Va) and value to combine (Vb) using the procedure for the composite operation to use corresponding to the
         //            target property’s animation type.
-        if (CSSPropertyAnimation::isPropertyAdditiveOrCumulative(cssPropertyId)) {
-            // Only do this for the 0 keyframe if it was provided explicitly, since otherwise we want to use the "neutral value
-            // for composition" which really means we don't want to do anything but rather just use the underlying style which
-            // is already set on startKeyframe.
-            if (!startKeyframe.key() && !hasImplicitZeroKeyframe) {
-                auto startKeyframeCompositeOperation = startKeyframe.compositeOperation().value_or(m_compositeOperation);
-                if (startKeyframeCompositeOperation != CompositeOperation::Replace)
-                    CSSPropertyAnimation::blendProperties(this, cssPropertyId, startKeyframeStyle, targetStyle, *startKeyframe.style(), 1, startKeyframeCompositeOperation);
-            }
+        if (std::holds_alternative<CSSPropertyID>(property)) {
+            auto cssPropertyId = std::get<CSSPropertyID>(property);
+            if (CSSPropertyAnimation::isPropertyAdditiveOrCumulative(cssPropertyId)) {
+                // Only do this for the 0 keyframe if it was provided explicitly, since otherwise we want to use the "neutral value
+                // for composition" which really means we don't want to do anything but rather just use the underlying style which
+                // is already set on startKeyframe.
+                if (!startKeyframe.key() && !hasImplicitZeroKeyframe) {
+                    auto startKeyframeCompositeOperation = startKeyframe.compositeOperation().value_or(m_compositeOperation);
+                    if (startKeyframeCompositeOperation != CompositeOperation::Replace)
+                        CSSPropertyAnimation::blendProperties(this, cssPropertyId, startKeyframeStyle, targetStyle, *startKeyframe.style(), 1, startKeyframeCompositeOperation);
+                }
 
-            // Only do this for the 1 keyframe if it was provided explicitly, since otherwise we want to use the "neutral value
-            // for composition" which really means we don't want to do anything but rather just use the underlying style which
-            // is already set on endKeyframe.
-            if (endKeyframe.key() == 1 && !hasImplicitOneKeyframe) {
-                auto endKeyframeCompositeOperation = endKeyframe.compositeOperation().value_or(m_compositeOperation);
-                if (endKeyframeCompositeOperation != CompositeOperation::Replace)
-                    CSSPropertyAnimation::blendProperties(this, cssPropertyId, endKeyframeStyle, targetStyle, *endKeyframe.style(), 1, endKeyframeCompositeOperation);
+                // Only do this for the 1 keyframe if it was provided explicitly, since otherwise we want to use the "neutral value
+                // for composition" which really means we don't want to do anything but rather just use the underlying style which
+                // is already set on endKeyframe.
+                if (endKeyframe.key() == 1 && !hasImplicitOneKeyframe) {
+                    auto endKeyframeCompositeOperation = endKeyframe.compositeOperation().value_or(m_compositeOperation);
+                    if (endKeyframeCompositeOperation != CompositeOperation::Replace)
+                        CSSPropertyAnimation::blendProperties(this, cssPropertyId, endKeyframeStyle, targetStyle, *endKeyframe.style(), 1, endKeyframeCompositeOperation);
+                }
             }
         }
 
         // 13. If there is only one keyframe in interval endpoints return the property value of target property on that keyframe.
         if (intervalEndpoints.size() == 1) {
-            CSSPropertyAnimation::blendProperties(this, cssPropertyId, targetStyle, startKeyframeStyle, startKeyframeStyle, 0, CompositeOperation::Replace);
+            WTF::switchOn(property,
+                [&] (CSSPropertyID propertyId) { CSSPropertyAnimation::blendProperties(this, propertyId, targetStyle, startKeyframeStyle, startKeyframeStyle, 0, CompositeOperation::Replace); },
+                [&] (AtomString customProperty) { CSSPropertyAnimation::blendCustomProperty(customProperty, targetStyle, startKeyframeStyle, startKeyframeStyle, 0); }
+            );
             return;
         }
 
@@ -1547,8 +1560,19 @@
         // 18. Return the result of applying the interpolation procedure defined by the animation type of the target property, to the values of the target
         //     property specified on the two keyframes in interval endpoints taking the first such value as Vstart and the second as Vend and using transformed
         //     distance as the interpolation parameter p.
-        CSSPropertyAnimation::blendProperties(this, cssPropertyId, targetStyle, startKeyframeStyle, endKeyframeStyle, transformedDistance, CompositeOperation::Replace);
+        WTF::switchOn(property,
+            [&] (CSSPropertyID propertyId) { CSSPropertyAnimation::blendProperties(this, propertyId, targetStyle, startKeyframeStyle, endKeyframeStyle, transformedDistance, CompositeOperation::Replace); },
+            [&] (AtomString customProperty) { CSSPropertyAnimation::blendCustomProperty(customProperty, targetStyle, startKeyframeStyle, endKeyframeStyle, transformedDistance); }
+        );
+    };
+
+    for (auto cssPropertyId : properties) {
+        if (cssPropertyId != CSSPropertyCustom)
+            blendProperty(cssPropertyId);
     }
+
+    for (auto customProperty : m_blendingKeyframes.customProperties())
+        blendProperty(customProperty);
 }
 
 TimingFunction* KeyframeEffect::timingFunctionForBlendingKeyframe(const KeyframeValue& keyframe) const
diff --git a/Source/WebCore/rendering/style/KeyframeList.cpp b/Source/WebCore/rendering/style/KeyframeList.cpp
index ed11fc0..07552ff 100644
--- a/Source/WebCore/rendering/style/KeyframeList.cpp
+++ b/Source/WebCore/rendering/style/KeyframeList.cpp
@@ -77,8 +77,11 @@
     if (!inserted)
         m_keyframes.append(WTFMove(keyframe));
 
-    for (auto& property : m_keyframes[i].properties())
+    auto& insertedKeyframe = m_keyframes[i];
+    for (auto& property : insertedKeyframe.properties())
         m_properties.add(property);
+    for (auto& customProperty : insertedKeyframe.customProperties())
+        m_customProperties.add(customProperty);
 }
 
 bool KeyframeList::hasImplicitKeyframes() const
@@ -92,6 +95,8 @@
         KeyframeValue keyframeValue(keyframe.key(), RenderStyle::clonePtr(*keyframe.style()));
         for (auto propertyId : keyframe.properties())
             keyframeValue.addProperty(propertyId);
+        for (auto& customProperty : keyframe.customProperties())
+            keyframeValue.addCustomProperty(customProperty);
         keyframeValue.setTimingFunction(keyframe.timingFunction());
         keyframeValue.setCompositeOperation(keyframe.compositeOperation());
         insert(WTFMove(keyframeValue));
@@ -210,6 +215,9 @@
 
 bool KeyframeList::containsAnimatableProperty() const
 {
+    if (!m_customProperties.isEmpty())
+        return true;
+
     for (auto cssPropertyId : m_properties) {
         if (CSSPropertyAnimation::isPropertyAnimatable(cssPropertyId))
             return true;
diff --git a/Source/WebCore/rendering/style/KeyframeList.h b/Source/WebCore/rendering/style/KeyframeList.h
index 46ccf0a..44a6b5c7 100644
--- a/Source/WebCore/rendering/style/KeyframeList.h
+++ b/Source/WebCore/rendering/style/KeyframeList.h
@@ -29,6 +29,7 @@
 #include <wtf/Vector.h>
 #include <wtf/HashSet.h>
 #include <wtf/text/AtomString.h>
+#include <wtf/text/AtomStringHash.h>
 
 namespace WebCore {
 
@@ -52,6 +53,10 @@
     bool containsProperty(CSSPropertyID prop) const { return m_properties.contains(prop); }
     const HashSet<CSSPropertyID>& properties() const { return m_properties; }
 
+    void addCustomProperty(const AtomString& customProperty) { m_customProperties.add(customProperty); }
+    bool containsCustomProperty(const AtomString& customProperty) const { return m_customProperties.contains(customProperty); }
+    const HashSet<AtomString>& customProperties() const { return m_customProperties; }
+
     double key() const { return m_key; }
     void setKey(double key) { m_key = key; }
 
@@ -67,6 +72,7 @@
 private:
     double m_key;
     HashSet<CSSPropertyID> m_properties; // The properties specified in this keyframe.
+    HashSet<AtomString> m_customProperties; // The custom properties being animated.
     std::unique_ptr<RenderStyle> m_style;
     RefPtr<TimingFunction> m_timingFunction;
     std::optional<CompositeOperation> m_compositeOperation;
@@ -92,6 +98,10 @@
     const HashSet<CSSPropertyID>& properties() const { return m_properties; }
     bool containsAnimatableProperty() const;
 
+    void addCustomProperty(const AtomString& customProperty) { m_customProperties.add(customProperty); }
+    bool containsCustomProperty(const AtomString& customProperty) const { return m_customProperties.contains(customProperty); }
+    const HashSet<AtomString>& customProperties() const { return m_customProperties; }
+
     void clear();
     bool isEmpty() const { return m_keyframes.isEmpty(); }
     size_t size() const { return m_keyframes.size(); }
@@ -110,6 +120,7 @@
     AtomString m_animationName;
     Vector<KeyframeValue> m_keyframes; // Kept sorted by key.
     HashSet<CSSPropertyID> m_properties; // The properties being animated.
+    HashSet<AtomString> m_customProperties; // The custom properties being animated.
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/style/StyleResolver.cpp b/Source/WebCore/style/StyleResolver.cpp
index c5879cf..8641471 100644
--- a/Source/WebCore/style/StyleResolver.cpp
+++ b/Source/WebCore/style/StyleResolver.cpp
@@ -286,10 +286,15 @@
         // The animation-composition and animation-timing-function within keyframes are special
         // because they are not animated; they just describe the composite operation and timing
         // function between this keyframe and the next.
-        if (property != CSSPropertyAnimationTimingFunction && property != CSSPropertyAnimationComposition)
+        bool isAnimatableValue = property != CSSPropertyAnimationTimingFunction && property != CSSPropertyAnimationComposition;
+        if (isAnimatableValue)
             keyframeValue.addProperty(property);
-        if (auto* value = propertyReference.value(); value && value->isRevertValue())
-            hasRevert = true;
+        if (auto* value = propertyReference.value()) {
+            if (isAnimatableValue && value->isCustomPropertyValue())
+                keyframeValue.addCustomProperty(downcast<CSSCustomPropertyValue>(*value).name());
+            if (value->isRevertValue())
+                hasRevert = true;
+        }
     }
 
     auto state = State(element, nullptr, context.documentElementStyle);