Skip to content
Permalink
Browse files
[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: https://svn.webkit.org/repository/webkit/trunk@295728 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
graouts committed Jun 22, 2022
1 parent 075e144 commit 9a200620bb6f1f1ea66222ee5be2d6bd58b8da67
Showing 14 changed files with 96 additions and 38 deletions.
@@ -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
@@ -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

@@ -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 ""
@@ -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"

@@ -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"

@@ -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)"

@@ -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)"

@@ -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

@@ -3666,7 +3666,7 @@ CSSPropertyAnimationWrapperMap::CSSPropertyAnimationWrapperMap()

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::blendProperties(const CSSPropertyBlendingClient* clie
}
}

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);
@@ -48,6 +48,7 @@ class CSSPropertyAnimation {
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
@@ -1399,7 +1399,14 @@ void KeyframeEffect::setAnimatedPropertiesInStyle(RenderStyle& targetStyle, doub
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 @@ void KeyframeEffect::setAnimatedPropertiesInStyle(RenderStyle& targetStyle, doub
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 @@ void KeyframeEffect::setAnimatedPropertiesInStyle(RenderStyle& targetStyle, doub

// 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 @@ void KeyframeEffect::setAnimatedPropertiesInStyle(RenderStyle& targetStyle, doub
// 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 @@ void KeyframeEffect::setAnimatedPropertiesInStyle(RenderStyle& targetStyle, doub
// 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
@@ -77,8 +77,11 @@ void KeyframeList::insert(KeyframeValue&& keyframe)
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 @@ void KeyframeList::copyKeyframes(KeyframeList& other)
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 @@ void KeyframeList::fillImplicitKeyframes(const KeyframeEffect& effect, const Ren

bool KeyframeList::containsAnimatableProperty() const
{
if (!m_customProperties.isEmpty())
return true;

for (auto cssPropertyId : m_properties) {
if (CSSPropertyAnimation::isPropertyAnimatable(cssPropertyId))
return true;
@@ -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 @@ class KeyframeValue {
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 @@ class KeyframeValue {
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 @@ class KeyframeList {
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 @@ class KeyframeList {
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
@@ -286,10 +286,15 @@ std::unique_ptr<RenderStyle> Resolver::styleForKeyframe(const Element& element,
// 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);

0 comments on commit 9a20062

Please sign in to comment.