LibWeb: Implement the animation-composition property

This commit is contained in:
Tim Ledbetter 2025-09-18 19:59:33 +01:00 committed by Sam Atkins
parent 968a8e618c
commit e502f19fa7
Notes: github-actions[bot] 2025-09-19 09:11:25 +00:00
14 changed files with 222 additions and 7 deletions

View file

@ -67,6 +67,7 @@
"absolute", "absolute",
"accentcolor", "accentcolor",
"accentcolortext", "accentcolortext",
"accumulate",
"active", "active",
"activeborder", "activeborder",
"activecaption", "activecaption",
@ -424,6 +425,7 @@
"repeat", "repeat",
"repeat-x", "repeat-x",
"repeat-y", "repeat-y",
"replace",
"reverse", "reverse",
"revert", "revert",
"revert-layer", "revert-layer",

View file

@ -209,6 +209,17 @@
"animation-fill-mode" "animation-fill-mode"
] ]
}, },
"animation-composition": {
"affects-layout": false,
"animation-type": "none",
"inherited": false,
"initial": "replace",
"valid-identifiers": [
"replace",
"add",
"accumulate"
]
},
"animation-delay": { "animation-delay": {
"affects-layout": false, "affects-layout": false,
"animation-type": "none", "animation-type": "none",

View file

@ -1243,6 +1243,20 @@ static void apply_animation_properties(DOM::Document& document, CascadedProperti
if (auto timing_property = cascaded_properties.property(PropertyID::AnimationTimingFunction); timing_property && timing_property->is_easing()) if (auto timing_property = cascaded_properties.property(PropertyID::AnimationTimingFunction); timing_property && timing_property->is_easing())
timing_function = timing_property->as_easing().function(); timing_function = timing_property->as_easing().function();
Bindings::CompositeOperation composite_operation { Bindings::CompositeOperation::Replace };
if (auto composite_property = cascaded_properties.property(PropertyID::AnimationComposition); composite_property) {
switch (composite_property->to_keyword()) {
case Keyword::Add:
composite_operation = Bindings::CompositeOperation::Add;
break;
case Keyword::Accumulate:
composite_operation = Bindings::CompositeOperation::Accumulate;
break;
default:
break;
}
}
auto iteration_duration = duration.has_value() auto iteration_duration = duration.has_value()
? Variant<double, String> { duration.release_value().to_milliseconds() } ? Variant<double, String> { duration.release_value().to_milliseconds() }
: "auto"_string; : "auto"_string;
@ -1252,6 +1266,7 @@ static void apply_animation_properties(DOM::Document& document, CascadedProperti
effect.set_timing_function(move(timing_function)); effect.set_timing_function(move(timing_function));
effect.set_fill_mode(Animations::css_fill_mode_to_bindings_fill_mode(fill_mode)); effect.set_fill_mode(Animations::css_fill_mode_to_bindings_fill_mode(fill_mode));
effect.set_playback_direction(Animations::css_animation_direction_to_bindings_playback_direction(direction)); effect.set_playback_direction(Animations::css_animation_direction_to_bindings_playback_direction(direction));
effect.set_composite(composite_operation);
if (play_state != effect.last_css_animation_play_state()) { if (play_state != effect.last_css_animation_play_state()) {
if (play_state == CSS::AnimationPlayState::Running && animation.play_state() != Bindings::AnimationPlayState::Running) { if (play_state == CSS::AnimationPlayState::Running && animation.play_state() != Bindings::AnimationPlayState::Running) {

View file

@ -75,6 +75,7 @@ All properties associated with getComputedStyle(document.body):
"align-content", "align-content",
"align-items", "align-items",
"align-self", "align-self",
"animation-composition",
"animation-delay", "animation-delay",
"animation-direction", "animation-direction",
"animation-duration", "animation-duration",

View file

@ -157,6 +157,8 @@ All supported properties and their default values exposed from CSSStylePropertie
'align-self': 'auto' 'align-self': 'auto'
'all': '' 'all': ''
'animation': 'none' 'animation': 'none'
'animationComposition': 'replace'
'animation-composition': 'replace'
'animationDelay': '0s' 'animationDelay': '0s'
'animation-delay': '0s' 'animation-delay': '0s'
'animationDirection': 'normal' 'animationDirection': 'normal'

View file

@ -73,6 +73,7 @@ writing-mode: horizontal-tb
align-content: normal align-content: normal
align-items: normal align-items: normal
align-self: auto align-self: auto
animation-composition: replace
animation-delay: 0s animation-delay: 0s
animation-direction: normal animation-direction: normal
animation-duration: 0s animation-duration: 0s
@ -94,7 +95,7 @@ background-position-x: 0%
background-position-y: 0% background-position-y: 0%
background-repeat: repeat background-repeat: repeat
background-size: auto background-size: auto
block-size: 1440px block-size: 1455px
border-block-end-color: rgb(0, 0, 0) border-block-end-color: rgb(0, 0, 0)
border-block-end-style: none border-block-end-style: none
border-block-end-width: 0px border-block-end-width: 0px
@ -171,7 +172,7 @@ grid-row-start: auto
grid-template-areas: none grid-template-areas: none
grid-template-columns: none grid-template-columns: none
grid-template-rows: none grid-template-rows: none
height: 2595px height: 2610px
inline-size: 784px inline-size: 784px
inset-block-end: auto inset-block-end: auto
inset-block-start: auto inset-block-start: auto

View file

@ -2,13 +2,13 @@ Harness status: OK
Found 8 tests Found 8 tests
4 Pass 5 Pass
4 Fail 3 Fail
Fail Setting a null effect on a running animation fires an animationend event Fail Setting a null effect on a running animation fires an animationend event
Pass Replacing an animation's effect with an effect that targets a different property should update both properties Pass Replacing an animation's effect with an effect that targets a different property should update both properties
Pass Replacing an animation's effect with a shorter one that should have already finished, the animation finishes immediately Pass Replacing an animation's effect with a shorter one that should have already finished, the animation finishes immediately
Pass A play-pending animation's effect whose effect is replaced still exits the pending state Pass A play-pending animation's effect whose effect is replaced still exits the pending state
Fail CSS animation events are dispatched at the original element even after setting an effect with a different target element Fail CSS animation events are dispatched at the original element even after setting an effect with a different target element
Pass After replacing a finished animation's effect with a longer one it fires an animationstart event Pass After replacing a finished animation's effect with a longer one it fires an animationstart event
Fail Setting animation-composition sets the composite property on the effect Pass Setting animation-composition sets the composite property on the effect
Fail Replacing the effect of a CSSAnimation causes subsequent changes to corresponding animation-* properties to be ignored Fail Replacing the effect of a CSSAnimation causes subsequent changes to corresponding animation-* properties to be ignored

View file

@ -0,0 +1,12 @@
Harness status: OK
Found 6 tests
4 Pass
2 Fail
Pass animation-composition: replace of property filter
Fail animation-composition: add of property filter
Fail animation-composition: accumulate of property filter
Pass animation-composition: replace of property width
Pass animation-composition: add of property width
Pass animation-composition: accumulate of property width

View file

@ -0,0 +1,9 @@
Harness status: OK
Found 4 tests
4 Pass
Pass e.style['animation-composition'] = "auto" should not set the property value
Pass e.style['animation-composition'] = "add replace" should not set the property value
Pass e.style['animation-composition'] = "add, initial" should not set the property value
Pass e.style['animation-composition'] = "initial, add" should not set the property value

View file

@ -0,0 +1,10 @@
Harness status: OK
Found 4 tests
3 Pass
1 Fail
Pass e.style['animation-composition'] = "replace" should set the property value
Pass e.style['animation-composition'] = "add" should set the property value
Pass e.style['animation-composition'] = "accumulate" should set the property value
Fail e.style['animation-composition'] = "replace, add, accumulate" should set the property value

View file

@ -1,8 +1,8 @@
Harness status: OK Harness status: OK
Found 260 tests Found 261 tests
253 Pass 254 Pass
7 Fail 7 Fail
Pass accent-color Pass accent-color
Pass border-collapse Pass border-collapse
@ -76,6 +76,7 @@ Pass word-wrap
Pass writing-mode Pass writing-mode
Pass align-items Pass align-items
Pass align-self Pass align-self
Pass animation-composition
Pass animation-delay Pass animation-delay
Pass animation-direction Pass animation-direction
Pass animation-duration Pass animation-duration

View file

@ -0,0 +1,110 @@
<!doctype html>
<meta charset=utf-8>
<title>animation-composition test</title>
<link rel="help" href="https://drafts.csswg.org/css-animations-2/#animation-composition">
<script src=../../resources/testharness.js></script>
<script src=../../resources/testharnessreport.js></script>
<script src="support/testcommon.js"></script>
<style>
@keyframes anim {
from {
filter: blur(10px);
width: 100px;
}
50% {
filter: blur(15px);
width: 228px;
}
to {
filter: blur(20px);
width: 1337px;
}
}
.anim-target {
animation: anim 1s;
animation-fill-mode: forwards;
animation-timing-function: linear;
filter: blur(5px);
width: 50px;
}
.replace {
animation-composition: replace;
}
.add {
animation-composition: add;
}
.accumulate {
animation-composition: accumulate;
}
</style>
<div id="log"></div>
<script>
function run_test_case(element, property, composite_type, timing_value_map) {
element.classList.add(composite_type);
const anim = element.getAnimations()[0];
for (const [time, value] of Object.entries(timing_value_map)) {
anim.currentTime = time;
const property_value = getComputedStyle(element).getPropertyValue(property);
assert_equals(property_value, value, "at time " + time);
}
element.classList.remove(composite_type);
}
const test_cases = [
["filter", {
"replace": {
0: "blur(10px)",
250: "blur(12.5px)",
500: "blur(15px)",
1000: "blur(20px)"
},
"add": {
0: "blur(5px) blur(10px)",
250: "blur(5px) blur(12.5px)",
500: "blur(5px) blur(15px)",
1000: "blur(5px) blur(20px)"
},
"accumulate": {
0: "blur(15px)",
250: "blur(17.5px)",
500: "blur(20px)",
1000: "blur(25px)"
}
}],
["width", {
"replace": {
0: "100px",
250: "164px",
500: "228px",
1000: "1337px"
},
"add": {
0: "150px",
250: "214px",
500: "278px",
1000: "1387px"
},
"accumulate": {
0: "150px",
250: "214px",
500: "278px",
1000: "1387px"
}
}]
]
for (const test_case of test_cases) {
const property = test_case[0];
const test_data = test_case[1];
for (const [composite_type, expected_values] of Object.entries(test_data)) {
test(t => {
let elem = addDiv(t, {"class": "anim-target"});
run_test_case(elem, property, composite_type, expected_values);
}, "animation-composition: " + composite_type + " of property " + property);
}
}
</script>

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CSS Animations: parsing animation-composition with invalid values</title>
<link rel="help" href="https://drafts.csswg.org/css-animations-2/#propdef-animation-composition">
<meta name="assert" content="animation-composition supports only the grammar '<single-animation-composition> #'.">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../css/support/parsing-testcommon.js"></script>
</head>
<body>
<script>
test_invalid_value("animation-composition", "auto");
test_invalid_value("animation-composition", "add replace");
test_invalid_value("animation-composition", "add, initial");
test_invalid_value("animation-composition", "initial, add");
</script>
</body>
</html>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CSS Animations: parsing animation-composition with valid values</title>
<link rel="help" href="https://drafts.csswg.org/css-animations-2/#propdef-animation-composition">
<meta name="assert" content="animation-composition supports the full grammar '<single-animation-composition> #'.">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../css/support/parsing-testcommon.js"></script>
</head>
<body>
<script>
test_valid_value("animation-composition", "replace");
test_valid_value("animation-composition", "add");
test_valid_value("animation-composition", "accumulate");
test_valid_value("animation-composition", "replace, add, accumulate");
</script>
</body>
</html>