Refactor KeyframeEffect::getKeyframes()
https://bugs.webkit.org/show_bug.cgi?id=235504

Reviewed by Chris Dumez.

We move all the JS conversion code to a new custom implementation for JSKeyframeEffect::getKeyframes()
such that KeyframeEffect::getKeyframes() is simply in the business of compiling the list of computed
keyframes.

To do this, we start by changing the way the various KeyframeEffect structs are organized. We make
BaseComputedKeyframe extend BaseKeyframe, then ComputedKeyframe extend BaseComputedKeyframe by adding
a map of CSSPropertyID to String values, then ParsedKeyframe can simply extend ComputedKeyframe.
This makes it easy to copy ParsedKeyframe into a ComputedKeyframe for the properties relevant
to the output of getKeyframes().

We also take the opportunity to merge what used to be two methods, getBindingsKeyframes() and getKeyframes(),
into a single method since getKeyframes() is only ever called through the JS bindings.

Finally, we remove the big if/else statement in KeyframeEffect::getKeyframes() to have a small if block
for the case where the keyframes are already set via the setKeyframes() API and return early.

* Sources.txt:
* WebCore.xcodeproj/project.pbxproj:
* animation/KeyframeEffect.cpp:
(WebCore::KeyframeEffect::CSSPropertyIDToIDLAttributeName):
(WebCore::IDLAttributeNameToAnimationPropertyName):
(WebCore::processIterableKeyframes):
(WebCore::processPropertyIndexedKeyframes):
(WebCore::KeyframeEffect::copyPropertiesFromSource):
(WebCore::KeyframeEffect::getKeyframes):
(WebCore::KeyframeEffect::animatedProperties):
(WebCore::KeyframeEffect::animatesProperty const):
(WebCore::CSSPropertyIDToIDLAttributeName): Deleted.
(WebCore::KeyframeEffect::getBindingsKeyframes): Deleted.
* animation/KeyframeEffect.h:
* animation/KeyframeEffect.idl:
* bindings/js/JSKeyframeEffectCustom.cpp: Added.
(WebCore::JSKeyframeEffect::getKeyframes):

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@288560 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 570a76c..d081ecd 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,44 @@
+2022-01-25  Antoine Quint  <graouts@webkit.org>
+
+        Refactor KeyframeEffect::getKeyframes()
+        https://bugs.webkit.org/show_bug.cgi?id=235504
+
+        Reviewed by Chris Dumez.
+
+        We move all the JS conversion code to a new custom implementation for JSKeyframeEffect::getKeyframes()
+        such that KeyframeEffect::getKeyframes() is simply in the business of compiling the list of computed
+        keyframes.
+
+        To do this, we start by changing the way the various KeyframeEffect structs are organized. We make
+        BaseComputedKeyframe extend BaseKeyframe, then ComputedKeyframe extend BaseComputedKeyframe by adding
+        a map of CSSPropertyID to String values, then ParsedKeyframe can simply extend ComputedKeyframe.
+        This makes it easy to copy ParsedKeyframe into a ComputedKeyframe for the properties relevant
+        to the output of getKeyframes().
+
+        We also take the opportunity to merge what used to be two methods, getBindingsKeyframes() and getKeyframes(),
+        into a single method since getKeyframes() is only ever called through the JS bindings.
+
+        Finally, we remove the big if/else statement in KeyframeEffect::getKeyframes() to have a small if block
+        for the case where the keyframes are already set via the setKeyframes() API and return early.
+
+        * Sources.txt:
+        * WebCore.xcodeproj/project.pbxproj:
+        * animation/KeyframeEffect.cpp:
+        (WebCore::KeyframeEffect::CSSPropertyIDToIDLAttributeName):
+        (WebCore::IDLAttributeNameToAnimationPropertyName):
+        (WebCore::processIterableKeyframes):
+        (WebCore::processPropertyIndexedKeyframes):
+        (WebCore::KeyframeEffect::copyPropertiesFromSource):
+        (WebCore::KeyframeEffect::getKeyframes):
+        (WebCore::KeyframeEffect::animatedProperties):
+        (WebCore::KeyframeEffect::animatesProperty const):
+        (WebCore::CSSPropertyIDToIDLAttributeName): Deleted.
+        (WebCore::KeyframeEffect::getBindingsKeyframes): Deleted.
+        * animation/KeyframeEffect.h:
+        * animation/KeyframeEffect.idl:
+        * bindings/js/JSKeyframeEffectCustom.cpp: Added.
+        (WebCore::JSKeyframeEffect::getKeyframes):
+
 2022-01-25  Pablo Saavedra  <psaavedra@igalia.com>
 
         [WPE][GTK] Build error in ARMv7 Neon targets after r286152
diff --git a/Source/WebCore/Sources.txt b/Source/WebCore/Sources.txt
index 60f6170..776b196 100644
--- a/Source/WebCore/Sources.txt
+++ b/Source/WebCore/Sources.txt
@@ -570,6 +570,7 @@
 bindings/js/JSImageDataCustom.cpp
 bindings/js/JSIntersectionObserverCustom.cpp
 bindings/js/JSIntersectionObserverEntryCustom.cpp
+bindings/js/JSKeyframeEffectCustom.cpp
 bindings/js/JSLazyEventListener.cpp
 bindings/js/JSLocationCustom.cpp
 bindings/js/JSMediaStreamTrackCustom.cpp
diff --git a/Source/WebCore/WebCore.xcodeproj/project.pbxproj b/Source/WebCore/WebCore.xcodeproj/project.pbxproj
index df9ffc7..a58ebce 100644
--- a/Source/WebCore/WebCore.xcodeproj/project.pbxproj
+++ b/Source/WebCore/WebCore.xcodeproj/project.pbxproj
@@ -11126,6 +11126,7 @@
 		715AD71D2050512400D592DC /* DeclarativeAnimation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DeclarativeAnimation.h; sourceTree = "<group>"; };
 		715AD71F2050512400D592DC /* DeclarativeAnimation.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = DeclarativeAnimation.cpp; sourceTree = "<group>"; };
 		715DA5D3201BB902002EF2B0 /* JSWebAnimationCustom.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSWebAnimationCustom.cpp; sourceTree = "<group>"; };
+		71601718279F2D7E005A25AE /* JSKeyframeEffectCustom.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = JSKeyframeEffectCustom.cpp; sourceTree = "<group>"; };
 		716A55AB26FA349B00C96D69 /* range-button.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = "range-button.css"; sourceTree = "<group>"; };
 		716A55AD26FA349C00C96D69 /* volume-button.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "volume-button.js"; sourceTree = "<group>"; };
 		716A55AE26FA349C00C96D69 /* range-button.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "range-button.js"; sourceTree = "<group>"; };
@@ -24835,6 +24836,7 @@
 				A7D0318D0E93540300E24ACD /* JSImageDataCustom.cpp */,
 				5868C7D52546E0B300BF9DF3 /* JSIntersectionObserverCustom.cpp */,
 				77C13F042165658A002D9C5F /* JSIntersectionObserverEntryCustom.cpp */,
+				71601718279F2D7E005A25AE /* JSKeyframeEffectCustom.cpp */,
 				AD726FE716D9F204003A4E6D /* JSMediaListCustom.h */,
 				415CDAF61E6CE0D3004F11EE /* JSMediaStreamTrackCustom.cpp */,
 				E1A5F99A0E7EAA2500AF85EA /* JSMessageChannelCustom.cpp */,
diff --git a/Source/WebCore/animation/KeyframeEffect.cpp b/Source/WebCore/animation/KeyframeEffect.cpp
index f943f56..d8e623a 100644
--- a/Source/WebCore/animation/KeyframeEffect.cpp
+++ b/Source/WebCore/animation/KeyframeEffect.cpp
@@ -75,7 +75,7 @@
         styleable->element.invalidateStyleInternal();
 }
 
-static inline String CSSPropertyIDToIDLAttributeName(CSSPropertyID cssPropertyId)
+String KeyframeEffect::CSSPropertyIDToIDLAttributeName(CSSPropertyID cssPropertyId)
 {
     // https://drafts.csswg.org/web-animations-1/#animation-property-name-to-idl-attribute-name
     // 1. If property follows the <custom-property-name> production, return property.
@@ -110,7 +110,7 @@
 
     // We need to check that converting the property back to IDL form yields the same result such that a property passed
     // in non-IDL form is rejected, for instance "font-size".
-    if (idlAttributeName != CSSPropertyIDToIDLAttributeName(cssPropertyId))
+    if (idlAttributeName != KeyframeEffect::CSSPropertyIDToIDLAttributeName(cssPropertyId))
         return CSSPropertyInvalid;
 
     return cssPropertyId;
@@ -327,7 +327,7 @@
             ASSERT(propertyAndValue.values.size() == 1);
             auto stringValue = propertyAndValue.values[0];
             if (keyframeOutput.style->setProperty(cssPropertyId, stringValue, false, parserContext))
-                keyframeOutput.unparsedStyle.set(cssPropertyId, stringValue);
+                keyframeOutput.styleStrings.set(cssPropertyId, stringValue);
         }
 
         parsedKeyframes.append(WTFMove(keyframeOutput));
@@ -365,7 +365,7 @@
             KeyframeEffect::ParsedKeyframe k;
             // 2. Add the property-value pair, property name → v, to k.
             if (k.style->setProperty(propertyName, v, false, parserContext))
-                k.unparsedStyle.set(propertyName, v);
+                k.styleStrings.set(propertyName, v);
             // 3. Append k to property keyframes.
             propertyKeyframes.append(WTFMove(k));
         }
@@ -402,7 +402,7 @@
         if (keyframe.style->propertyCount()) {
             auto property = keyframe.style->propertyAt(0);
             previousKeyframe.style->setProperty(property.id(), property.value());
-            previousKeyframe.unparsedStyle.set(property.id(), keyframe.unparsedStyle.get(property.id()));
+            previousKeyframe.styleStrings.set(property.id(), keyframe.styleStrings.get(property.id()));
         }
         // Since we've processed this keyframe, we can remove it and keep i the same
         // so that we process the next keyframe in the next loop iteration.
@@ -559,7 +559,7 @@
         parsedKeyframe.easing = sourceParsedKeyframe.easing;
         parsedKeyframe.offset = sourceParsedKeyframe.offset;
         parsedKeyframe.composite = sourceParsedKeyframe.composite;
-        parsedKeyframe.unparsedStyle = sourceParsedKeyframe.unparsedStyle;
+        parsedKeyframe.styleStrings = sourceParsedKeyframe.styleStrings;
         parsedKeyframe.computedOffset = sourceParsedKeyframe.computedOffset;
         parsedKeyframe.timingFunction = sourceParsedKeyframe.timingFunction;
         parsedKeyframe.style = sourceParsedKeyframe.style->mutableCopy();
@@ -582,214 +582,141 @@
     setBlendingKeyframes(keyframeList);
 }
 
-Vector<Strong<JSObject>> KeyframeEffect::getBindingsKeyframes(JSGlobalObject& lexicalGlobalObject, Document& document)
-{
-    if (is<DeclarativeAnimation>(animation()))
-        downcast<DeclarativeAnimation>(*animation()).flushPendingStyleChanges();
-    return getKeyframes(lexicalGlobalObject, document);
-}
-
-Vector<Strong<JSObject>> KeyframeEffect::getKeyframes(JSGlobalObject& lexicalGlobalObject, Document& document)
+auto KeyframeEffect::getKeyframes(Document& document) -> Vector<ComputedKeyframe>
 {
     // https://drafts.csswg.org/web-animations-1/#dom-keyframeeffectreadonly-getkeyframes
 
-    auto supportsCompositeOperation = document.settings().webAnimationsCompositeOperationsEnabled();
+    if (auto* declarativeAnimation = dynamicDowncast<DeclarativeAnimation>(animation()))
+        declarativeAnimation->flushPendingStyleChanges();
 
-    auto lock = JSLockHolder { &lexicalGlobalObject };
+    Vector<ComputedKeyframe> computedKeyframes;
 
-    // Since keyframes are represented by a partially open-ended dictionary type that is not currently able to be expressed with WebIDL,
-    // the procedure used to prepare the result of this method is defined in prose below:
-    //
-    // 1. Let result be an empty sequence of objects.
-    Vector<Strong<JSObject>> result;
-
-    // 2. Let keyframes be the result of applying the procedure to compute missing keyframe offsets to the keyframes for this keyframe effect.
-
-    // 3. For each keyframe in keyframes perform the following steps:
-    if (m_parsedKeyframes.isEmpty() && m_blendingKeyframesSource != BlendingKeyframesSource::WebAnimation && m_blendingKeyframes.containsAnimatableProperty()) {
-        auto* target = m_target.get();
-        auto* renderer = this->renderer();
-        auto* lastStyleChangeEventStyle = targetStyleable()->lastStyleChangeEventStyle();
-
-        auto computedStyleExtractor = ComputedStyleExtractor(target, false, m_pseudoId);
-
-        KeyframeList computedKeyframes(m_blendingKeyframes.animationName());
-        computedKeyframes.copyKeyframes(m_blendingKeyframes);
-        computedKeyframes.fillImplicitKeyframes(*m_target, m_target->styleResolver(), lastStyleChangeEventStyle, nullptr);
-
-        auto keyframeRules = [&]() -> const Vector<Ref<StyleRuleKeyframe>> {
-            if (!is<CSSAnimation>(animation()))
-                return { };
-
-            auto& backingAnimation = downcast<CSSAnimation>(*animation()).backingAnimation();
-            auto* styleScope = Style::Scope::forOrdinal(*m_target, backingAnimation.nameStyleScopeOrdinal());
-            if (!styleScope)
-                return { };
-
-            return styleScope->resolver().keyframeRulesForName(computedKeyframes.animationName());
-        }();
-
-        auto keyframeRuleForKey = [&](double key) -> StyleRuleKeyframe* {
-            for (auto& keyframeRule : keyframeRules) {
-                for (auto keyframeRuleKey : keyframeRule->keys()) {
-                    if (keyframeRuleKey == key)
-                        return keyframeRule.ptr();
-                }
-            }
-            return nullptr;
-        };
-
-        auto styleProperties = MutableStyleProperties::create();
-        if (m_blendingKeyframesSource == BlendingKeyframesSource::CSSAnimation) {
-            auto matchingRules = m_target->styleResolver().pseudoStyleRulesForElement(target, m_pseudoId, Style::Resolver::AllCSSRules);
-            for (auto& matchedRule : matchingRules)
-                styleProperties->mergeAndOverrideOnConflict(matchedRule->properties());
-            if (is<StyledElement>(m_target) && m_pseudoId == PseudoId::None) {
-                if (auto* inlineProperties = downcast<StyledElement>(*m_target).inlineStyle())
-                    styleProperties->mergeAndOverrideOnConflict(*inlineProperties);
-            }
-        }
-
-        // We need to establish which properties are implicit for 0% and 100%.
-        HashSet<CSSPropertyID> zeroKeyframeProperties = computedKeyframes.properties();
-        HashSet<CSSPropertyID> oneKeyframeProperties = computedKeyframes.properties();
-        zeroKeyframeProperties.remove(CSSPropertyCustom);
-        oneKeyframeProperties.remove(CSSPropertyCustom);
-        for (size_t i = 0; i < computedKeyframes.size(); ++i) {
-            auto& keyframe = computedKeyframes[i];
-            if (!keyframe.key()) {
-                for (auto cssPropertyId : keyframe.properties())
-                    zeroKeyframeProperties.remove(cssPropertyId);
-            } else if (keyframe.key() == 1) {
-                for (auto cssPropertyId : keyframe.properties())
-                    oneKeyframeProperties.remove(cssPropertyId);
-            }
-        }
-
-        for (size_t i = 0; i < computedKeyframes.size(); ++i) {
-            // 1. Initialize a dictionary object, output keyframe, using the following definition:
-            //
-            // dictionary BaseComputedKeyframe {
-            //      double?                  offset = null;
-            //      double                   computedOffset;
-            //      DOMString                easing = "linear";
-            //      CompositeOperationOrAuto composite = "auto";
-            // };
-
-            auto& keyframe = computedKeyframes[i];
-            auto& style = *keyframe.style();
-            auto* keyframeRule = keyframeRuleForKey(keyframe.key());
-
-            // 2. Set offset, computedOffset, easing members of output keyframe to the respective values keyframe offset, computed keyframe offset,
-            // and keyframe-specific timing function of keyframe.
-            BaseComputedKeyframe computedKeyframe;
-            computedKeyframe.offset = keyframe.key();
-            computedKeyframe.computedOffset = keyframe.key();
-            // For CSS transitions, all keyframes should return "linear" since the effect's global timing function applies.
-            computedKeyframe.easing = is<CSSTransition>(animation()) ? "linear" : timingFunctionForBlendingKeyframe(keyframe)->cssText();
-
-            if (supportsCompositeOperation) {
-                if (auto compositeOperation = keyframe.compositeOperation())
-                    computedKeyframe.composite = toCompositeOperationOrAuto(*compositeOperation);
-            }
-
-            auto outputKeyframe = convertDictionaryToJS(lexicalGlobalObject, *jsCast<JSDOMGlobalObject*>(&lexicalGlobalObject), computedKeyframe);
-
-            auto addPropertyToKeyframe = [&](CSSPropertyID cssPropertyId) {
-                // 1. Let property name be the result of applying the animation property name to IDL attribute name algorithm to the property name of declaration.
-                auto propertyName = CSSPropertyIDToIDLAttributeName(cssPropertyId);
-                // 2. Let IDL value be the result of serializing the property value of declaration by passing declaration to the algorithm to serialize a CSS value.
-                String idlValue = "";
-                if (keyframeRule) {
-                    if (auto cssValue = keyframeRule->properties().getPropertyCSSValue(cssPropertyId)) {
-                        if (!cssValue->hasVariableReferences())
-                            idlValue = keyframeRule->properties().getPropertyValue(cssPropertyId);
-                    }
-                }
-                if (idlValue.isEmpty()) {
-                    if (auto cssValue = styleProperties->getPropertyCSSValue(cssPropertyId)) {
-                        if (!cssValue->hasVariableReferences())
-                            idlValue = styleProperties->getPropertyValue(cssPropertyId);
-                    }
-                }
-                if (idlValue.isEmpty()) {
-                    if (auto cssValue = computedStyleExtractor.valueForPropertyInStyle(style, cssPropertyId, renderer))
-                        idlValue = cssValue->cssText();
-                }
-                // 3. Let value be the result of converting IDL value to an ECMAScript String value.
-                auto value = toJS<IDLDOMString>(lexicalGlobalObject, idlValue);
-                // 4. Call the [[DefineOwnProperty]] internal method on output keyframe with property name property name,
-                //    Property Descriptor { [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true, [[Value]]: value } and Boolean flag false.
-                JSObject::defineOwnProperty(outputKeyframe, &lexicalGlobalObject, AtomString(propertyName).impl(), PropertyDescriptor(value, 0), false);
-            };
-
-            // 3. For each animation property-value pair specified on keyframe, declaration, perform the following steps:
-            for (auto cssPropertyId : keyframe.properties()) {
-                if (cssPropertyId == CSSPropertyCustom)
-                    continue;
-                addPropertyToKeyframe(cssPropertyId);
-            }
-
-            // Now add the implicit properties in case there are any and we're dealing with a 0% or 100% keyframe.
-            if (lastStyleChangeEventStyle) {
-                if (!keyframe.key()) {
-                    for (auto cssPropertyId : zeroKeyframeProperties)
-                        addPropertyToKeyframe(cssPropertyId);
-                    zeroKeyframeProperties.clear();
-                } else if (keyframe.key() == 1) {
-                    for (auto cssPropertyId : oneKeyframeProperties)
-                        addPropertyToKeyframe(cssPropertyId);
-                    oneKeyframeProperties.clear();
-                }
-            }
-
-            // 5. Append output keyframe to result.
-            result.append(JSC::Strong<JSC::JSObject> { lexicalGlobalObject.vm(), outputKeyframe });
-        }
-    } else {
+    if (!m_parsedKeyframes.isEmpty() || m_blendingKeyframesSource == BlendingKeyframesSource::WebAnimation || !m_blendingKeyframes.containsAnimatableProperty()) {
         for (size_t i = 0; i < m_parsedKeyframes.size(); ++i) {
-            // 1. Initialize a dictionary object, output keyframe, using the following definition:
-            //
-            // dictionary BaseComputedKeyframe {
-            //      double?                  offset = null;
-            //      double                   computedOffset;
-            //      DOMString                easing = "linear";
-            //      CompositeOperationOrAuto composite = "auto";
-            // };
-
-            auto& parsedKeyframe = m_parsedKeyframes[i];
-
-            // 2. Set offset, computedOffset, easing, composite members of output keyframe to the respective values keyframe offset, computed keyframe
-            // offset, keyframe-specific timing function and keyframe-specific composite operation of keyframe.
-            BaseComputedKeyframe computedKeyframe;
-            computedKeyframe.offset = parsedKeyframe.offset;
-            computedKeyframe.computedOffset = parsedKeyframe.computedOffset;
+            ComputedKeyframe computedKeyframe { m_parsedKeyframes[i] };
             computedKeyframe.easing = timingFunctionForKeyframeAtIndex(i)->cssText();
+            computedKeyframes.append(WTFMove(computedKeyframe));
+        }
+        return computedKeyframes;
+    }
 
-            if (supportsCompositeOperation)
-                computedKeyframe.composite = parsedKeyframe.composite;
+    auto* target = m_target.get();
+    auto* renderer = this->renderer();
+    auto* lastStyleChangeEventStyle = targetStyleable()->lastStyleChangeEventStyle();
 
-            auto outputKeyframe = convertDictionaryToJS(lexicalGlobalObject, *jsCast<JSDOMGlobalObject*>(&lexicalGlobalObject), computedKeyframe);
+    ComputedStyleExtractor computedStyleExtractor { target, false, m_pseudoId };
 
-            // 3. For each animation property-value pair specified on keyframe, declaration, perform the following steps:
-            for (auto it = parsedKeyframe.unparsedStyle.begin(), end = parsedKeyframe.unparsedStyle.end(); it != end; ++it) {
-                // 1. Let property name be the result of applying the animation property name to IDL attribute name algorithm to the property name of declaration.
-                auto propertyName = CSSPropertyIDToIDLAttributeName(it->key);
-                // 2. Let IDL value be the result of serializing the property value of declaration by passing declaration to the algorithm to serialize a CSS value.
-                // 3. Let value be the result of converting IDL value to an ECMAScript String value.
-                auto value = toJS<IDLDOMString>(lexicalGlobalObject, it->value);
-                // 4. Call the [[DefineOwnProperty]] internal method on output keyframe with property name property name,
-                //    Property Descriptor { [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true, [[Value]]: value } and Boolean flag false.
-                JSObject::defineOwnProperty(outputKeyframe, &lexicalGlobalObject, AtomString(propertyName).impl(), PropertyDescriptor(value, 0), false);
+    KeyframeList computedKeyframeList(m_blendingKeyframes.animationName());
+    computedKeyframeList.copyKeyframes(m_blendingKeyframes);
+    computedKeyframeList.fillImplicitKeyframes(*m_target, m_target->styleResolver(), lastStyleChangeEventStyle, nullptr);
+
+    auto keyframeRules = [&]() -> const Vector<Ref<StyleRuleKeyframe>> {
+        if (!is<CSSAnimation>(animation()))
+            return { };
+
+        auto& backingAnimation = downcast<CSSAnimation>(*animation()).backingAnimation();
+        auto* styleScope = Style::Scope::forOrdinal(*m_target, backingAnimation.nameStyleScopeOrdinal());
+        if (!styleScope)
+            return { };
+
+        return styleScope->resolver().keyframeRulesForName(computedKeyframeList.animationName());
+    }();
+
+    auto keyframeRuleForKey = [&](double key) -> StyleRuleKeyframe* {
+        for (auto& keyframeRule : keyframeRules) {
+            for (auto keyframeRuleKey : keyframeRule->keys()) {
+                if (keyframeRuleKey == key)
+                    return keyframeRule.ptr();
             }
+        }
+        return nullptr;
+    };
 
-            // 4. Append output keyframe to result.
-            result.append(JSC::Strong<JSC::JSObject> { lexicalGlobalObject.vm(), outputKeyframe });
+    auto styleProperties = MutableStyleProperties::create();
+    if (m_blendingKeyframesSource == BlendingKeyframesSource::CSSAnimation) {
+        auto matchingRules = m_target->styleResolver().pseudoStyleRulesForElement(target, m_pseudoId, Style::Resolver::AllCSSRules);
+        for (auto& matchedRule : matchingRules)
+            styleProperties->mergeAndOverrideOnConflict(matchedRule->properties());
+        if (is<StyledElement>(m_target) && m_pseudoId == PseudoId::None) {
+            if (auto* inlineProperties = downcast<StyledElement>(*m_target).inlineStyle())
+                styleProperties->mergeAndOverrideOnConflict(*inlineProperties);
         }
     }
 
-    // 4. Return result.
-    return result;
+    // We need to establish which properties are implicit for 0% and 100%.
+    auto zeroKeyframeProperties = computedKeyframeList.properties();
+    auto oneKeyframeProperties = computedKeyframeList.properties();
+    zeroKeyframeProperties.remove(CSSPropertyCustom);
+    oneKeyframeProperties.remove(CSSPropertyCustom);
+    for (auto& keyframe : computedKeyframeList.keyframes()) {
+        if (!keyframe.key()) {
+            for (auto cssPropertyId : keyframe.properties())
+                zeroKeyframeProperties.remove(cssPropertyId);
+        } else if (keyframe.key() == 1) {
+            for (auto cssPropertyId : keyframe.properties())
+                oneKeyframeProperties.remove(cssPropertyId);
+        }
+    }
+
+    for (auto& keyframe : computedKeyframeList.keyframes()) {
+        auto& style = *keyframe.style();
+        auto* keyframeRule = keyframeRuleForKey(keyframe.key());
+
+        ComputedKeyframe computedKeyframe;
+        computedKeyframe.offset = keyframe.key();
+        computedKeyframe.computedOffset = keyframe.key();
+        // For CSS transitions, all keyframes should return "linear" since the effect's global timing function applies.
+        computedKeyframe.easing = is<CSSTransition>(animation()) ? "linear" : timingFunctionForBlendingKeyframe(keyframe)->cssText();
+
+        if (document.settings().webAnimationsCompositeOperationsEnabled()) {
+            if (auto compositeOperation = keyframe.compositeOperation())
+                computedKeyframe.composite = toCompositeOperationOrAuto(*compositeOperation);
+        }
+
+        auto addPropertyToKeyframe = [&](CSSPropertyID cssPropertyId) {
+            String styleString = emptyString();
+            if (keyframeRule) {
+                if (auto cssValue = keyframeRule->properties().getPropertyCSSValue(cssPropertyId)) {
+                    if (!cssValue->hasVariableReferences())
+                        styleString = keyframeRule->properties().getPropertyValue(cssPropertyId);
+                }
+            }
+            if (styleString.isEmpty()) {
+                if (auto cssValue = styleProperties->getPropertyCSSValue(cssPropertyId)) {
+                    if (!cssValue->hasVariableReferences())
+                        styleString = styleProperties->getPropertyValue(cssPropertyId);
+                }
+            }
+            if (styleString.isEmpty()) {
+                if (auto cssValue = computedStyleExtractor.valueForPropertyInStyle(style, cssPropertyId, renderer))
+                    styleString = cssValue->cssText();
+            }
+            computedKeyframe.styleStrings.set(cssPropertyId, styleString);
+        };
+
+        for (auto cssPropertyId : keyframe.properties()) {
+            if (cssPropertyId == CSSPropertyCustom)
+                continue;
+            addPropertyToKeyframe(cssPropertyId);
+        }
+
+        // Now add the implicit properties in case there are any and we're dealing with a 0% or 100% keyframe.
+        if (lastStyleChangeEventStyle) {
+            if (!keyframe.key()) {
+                for (auto cssPropertyId : zeroKeyframeProperties)
+                    addPropertyToKeyframe(cssPropertyId);
+                zeroKeyframeProperties.clear();
+            } else if (keyframe.key() == 1) {
+                for (auto cssPropertyId : oneKeyframeProperties)
+                    addPropertyToKeyframe(cssPropertyId);
+                oneKeyframeProperties.clear();
+            }
+        }
+
+        computedKeyframes.append(WTFMove(computedKeyframe));
+    }
+
+    return computedKeyframes;
 }
 
 ExceptionOr<void> KeyframeEffect::setBindingsKeyframes(JSGlobalObject& lexicalGlobalObject, Document& document, Strong<JSObject>&& keyframesInput)
@@ -936,7 +863,7 @@
 
     if (m_animatedProperties.isEmpty()) {
         for (auto& keyframe : m_parsedKeyframes) {
-            for (auto keyframeProperty : keyframe.unparsedStyle.keys())
+            for (auto keyframeProperty : keyframe.styleStrings.keys())
                 m_animatedProperties.add(keyframeProperty);
         }
     }
@@ -950,7 +877,7 @@
         return m_blendingKeyframes.properties().contains(property);
 
     for (auto& keyframe : m_parsedKeyframes) {
-        for (auto keyframeProperty : keyframe.unparsedStyle.keys()) {
+        for (auto keyframeProperty : keyframe.styleStrings.keys()) {
             if (keyframeProperty == property)
                 return true;
         }
diff --git a/Source/WebCore/animation/KeyframeEffect.h b/Source/WebCore/animation/KeyframeEffect.h
index d31dc05..c2833c6 100644
--- a/Source/WebCore/animation/KeyframeEffect.h
+++ b/Source/WebCore/animation/KeyframeEffect.h
@@ -80,14 +80,17 @@
         Vector<PropertyAndValues> propertiesAndValues;
     };
 
-    struct ParsedKeyframe {
-        MarkableDouble offset;
+    struct BaseComputedKeyframe : BaseKeyframe {
         double computedOffset;
-        CompositeOperationOrAuto composite { CompositeOperationOrAuto::Auto };
-        String easing;
+    };
+
+    struct ComputedKeyframe : BaseComputedKeyframe {
+        HashMap<CSSPropertyID, String> styleStrings;
+    };
+
+    struct ParsedKeyframe : ComputedKeyframe {
         RefPtr<TimingFunction> timingFunction;
         Ref<MutableStyleProperties> style;
-        HashMap<CSSPropertyID, String> unparsedStyle;
 
         ParsedKeyframe()
             : style(MutableStyleProperties::create())
@@ -95,13 +98,6 @@
         }
     };
 
-    struct BaseComputedKeyframe {
-        MarkableDouble offset;
-        double computedOffset;
-        String easing { "linear" };
-        CompositeOperationOrAuto composite { CompositeOperationOrAuto::Auto };
-    };
-
     const Vector<ParsedKeyframe>& parsedKeyframes() const { return m_parsedKeyframes; }
 
     Element* target() const { return m_target.get(); }
@@ -114,8 +110,7 @@
 
     const std::optional<const Styleable> targetStyleable() const;
 
-    Vector<JSC::Strong<JSC::JSObject>> getBindingsKeyframes(JSC::JSGlobalObject&, Document&);
-    Vector<JSC::Strong<JSC::JSObject>> getKeyframes(JSC::JSGlobalObject&, Document&);
+    Vector<ComputedKeyframe> getKeyframes(Document&);
     ExceptionOr<void> setBindingsKeyframes(JSC::JSGlobalObject&, Document&, JSC::Strong<JSC::JSObject>&&);
     ExceptionOr<void> setKeyframes(JSC::JSGlobalObject&, Document&, JSC::Strong<JSC::JSObject>&&);
 
@@ -173,6 +168,8 @@
 
     void keyframesRuleDidChange();
 
+    static String CSSPropertyIDToIDLAttributeName(CSSPropertyID);
+
 private:
     KeyframeEffect(Element*, PseudoId);
 
diff --git a/Source/WebCore/animation/KeyframeEffect.idl b/Source/WebCore/animation/KeyframeEffect.idl
index cef4b12..6414a54 100644
--- a/Source/WebCore/animation/KeyframeEffect.idl
+++ b/Source/WebCore/animation/KeyframeEffect.idl
@@ -36,7 +36,7 @@
     attribute CSSOMString? pseudoElement;
     [EnabledBySetting=WebAnimationsCompositeOperationsEnabled] attribute IterationCompositeOperation iterationComposite;
     [EnabledBySetting=WebAnimationsCompositeOperationsEnabled, ImplementedAs=bindingsComposite] attribute CompositeOperation composite;
-    [CallWith=GlobalObject&Document, ImplementedAs=getBindingsKeyframes] sequence<object> getKeyframes();
+    [Custom] sequence<object> getKeyframes();
     [CallWith=GlobalObject&Document, ImplementedAs=setBindingsKeyframes] undefined setKeyframes(object? keyframes);
 };
 
diff --git a/Source/WebCore/bindings/js/JSKeyframeEffectCustom.cpp b/Source/WebCore/bindings/js/JSKeyframeEffectCustom.cpp
new file mode 100644
index 0000000..fe39919
--- /dev/null
+++ b/Source/WebCore/bindings/js/JSKeyframeEffectCustom.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "JSKeyframeEffect.h"
+
+#include "CSSPropertyNames.h"
+
+namespace WebCore {
+
+using namespace JSC;
+
+JSValue JSKeyframeEffect::getKeyframes(JSGlobalObject& lexicalGlobalObject, CallFrame&)
+{
+    auto lock = JSLockHolder { &lexicalGlobalObject };
+
+    auto* context = jsCast<JSDOMGlobalObject*>(&lexicalGlobalObject)->scriptExecutionContext();
+    if (UNLIKELY(!context))
+        return jsUndefined();
+    ASSERT(context->isDocument());
+
+    auto& domGlobalObject = *jsCast<JSDOMGlobalObject*>(&lexicalGlobalObject);
+    auto computedKeyframes = wrapped().getKeyframes(downcast<Document>(*context));
+    auto keyframeObjects = computedKeyframes.map([&](auto& computedKeyframe) -> Strong<JSObject> {
+        auto keyframeObject = convertDictionaryToJS(lexicalGlobalObject, domGlobalObject, { computedKeyframe });
+        for (auto& [propertyID, propertyValue] : computedKeyframe.styleStrings) {
+            auto propertyName = KeyframeEffect::CSSPropertyIDToIDLAttributeName(propertyID);
+            auto value = toJS<IDLDOMString>(lexicalGlobalObject, propertyValue);
+            JSObject::defineOwnProperty(keyframeObject, &lexicalGlobalObject, AtomString(propertyName).impl(), PropertyDescriptor(value, 0), false);
+        }
+        return { lexicalGlobalObject.vm(), keyframeObject };
+    });
+
+    auto throwScope = DECLARE_THROW_SCOPE(lexicalGlobalObject.vm());
+    return toJS<IDLSequence<IDLObject>>(lexicalGlobalObject, domGlobalObject, throwScope, keyframeObjects);
+}
+
+} // namespace WebCore