LibWeb: Allow calc() values in steps() easing functions

This commit is contained in:
Tim Ledbetter 2025-06-17 09:46:51 +01:00 committed by Jelle Raaijmakers
parent 21b531598d
commit c5a3eaaf45
Notes: github-actions[bot] 2025-06-18 06:58:22 +00:00
8 changed files with 177 additions and 27 deletions

View file

@ -75,7 +75,7 @@ EffectTiming AnimationEffect::get_timing() const
.iterations = m_iteration_count,
.duration = m_iteration_duration,
.direction = m_playback_direction,
.easing = m_timing_function.to_string(),
.easing = m_timing_function.to_string(CSS::SerializationMode::Normal),
};
}
@ -110,7 +110,7 @@ ComputedEffectTiming AnimationEffect::get_computed_timing() const
.iterations = m_iteration_count,
.duration = duration,
.direction = m_playback_direction,
.easing = m_timing_function.to_string(),
.easing = m_timing_function.to_string(CSS::SerializationMode::Normal),
},
end_time(),

View file

@ -2651,11 +2651,10 @@ RefPtr<CSSStyleValue const> Parser::parse_easing_value(TokenStream<ComponentValu
EasingStyleValue::Steps steps;
auto const& intervals_argument = comma_separated_arguments[0][0];
if (!intervals_argument.is(Token::Type::Number))
auto intervals_token = TokenStream<ComponentValue>::of_single_token(intervals_argument);
auto intervals = parse_integer(intervals_token);
if (!intervals.has_value())
return nullptr;
if (!intervals_argument.token().number().is_integer())
return nullptr;
auto intervals = intervals_argument.token().to_integer();
if (comma_separated_arguments.size() == 2) {
TokenStream identifier_stream { comma_separated_arguments[1] };
@ -2690,14 +2689,16 @@ RefPtr<CSSStyleValue const> Parser::parse_easing_value(TokenStream<ComponentValu
// https://drafts.csswg.org/css-easing/#step-easing-functions
// If the <step-position> is jump-none, the <integer> must be at least 2, or the function is invalid.
// Otherwise, the <integer> must be at least 1, or the function is invalid.
if (!intervals->is_calculated()) {
if (steps.position == EasingStyleValue::Steps::Position::JumpNone) {
if (intervals <= 1)
if (intervals->value() <= 1)
return nullptr;
} else if (intervals <= 0) {
} else if (intervals->value() <= 0) {
return nullptr;
}
}
steps.number_of_intervals = intervals;
steps.number_of_intervals = *intervals;
transaction.commit();
return EasingStyleValue::create(steps);
}

View file

@ -11,6 +11,7 @@
#include "EasingStyleValue.h"
#include <AK/BinarySearch.h>
#include <AK/StringBuilder.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
namespace Web::CSS {
@ -174,7 +175,7 @@ double EasingStyleValue::Linear::evaluate_at(double input_progress, bool before_
}
// https://drafts.csswg.org/css-easing/#linear-easing-function-serializing
String EasingStyleValue::Linear::to_string() const
String EasingStyleValue::Linear::to_string(SerializationMode) const
{
// The linear keyword is serialized as itself.
if (*this == identity())
@ -316,7 +317,7 @@ double EasingStyleValue::CubicBezier::evaluate_at(double input_progress, bool) c
}
// https://drafts.csswg.org/css-easing/#bezier-serialization
String EasingStyleValue::CubicBezier::to_string() const
String EasingStyleValue::CubicBezier::to_string(SerializationMode) const
{
StringBuilder builder;
if (*this == CubicBezier::ease()) {
@ -337,7 +338,10 @@ double EasingStyleValue::Steps::evaluate_at(double input_progress, bool before_f
{
// https://www.w3.org/TR/css-easing-1/#step-easing-algo
// 1. Calculate the current step as floor(input progress value × steps).
auto current_step = floor(input_progress * number_of_intervals);
auto resolved_number_of_intervals = number_of_intervals.resolved({}).value_or(1);
resolved_number_of_intervals = max(resolved_number_of_intervals, position == Steps::Position::JumpNone ? 2 : 1);
auto current_step = floor(input_progress * resolved_number_of_intervals);
// 2. If the step position property is one of:
// - jump-start,
@ -350,7 +354,7 @@ double EasingStyleValue::Steps::evaluate_at(double input_progress, bool before_f
// - the before flag is set, and
// - input progress value × steps mod 1 equals zero (that is, if input progress value × steps is integral), then
// decrement current step by one.
auto step_progress = input_progress * number_of_intervals;
auto step_progress = input_progress * resolved_number_of_intervals;
if (before_flag && trunc(step_progress) == step_progress)
current_step -= 1;
@ -363,7 +367,7 @@ double EasingStyleValue::Steps::evaluate_at(double input_progress, bool before_f
// jump-start or jump-end -> steps
// jump-none -> steps - 1
// jump-both -> steps + 1
auto jumps = number_of_intervals;
auto jumps = resolved_number_of_intervals;
if (position == Steps::Position::JumpNone) {
jumps--;
} else if (position == Steps::Position::JumpBoth) {
@ -379,7 +383,7 @@ double EasingStyleValue::Steps::evaluate_at(double input_progress, bool before_f
}
// https://drafts.csswg.org/css-easing/#steps-serialization
String EasingStyleValue::Steps::to_string() const
String EasingStyleValue::Steps::to_string(SerializationMode mode) const
{
StringBuilder builder;
// Unlike the other easing function keywords, step-start and step-end do not serialize as themselves.
@ -403,10 +407,15 @@ String EasingStyleValue::Steps::to_string() const
return {};
}
}();
auto intervals = number_of_intervals;
if (mode == SerializationMode::ResolvedValue) {
auto resolved_value = number_of_intervals.resolved({}).value_or(1);
intervals = max(resolved_value, this->position == Steps::Position::JumpNone ? 2 : 1);
}
if (position.has_value()) {
builder.appendff("steps({}, {})", number_of_intervals, position.value());
builder.appendff("steps({}, {})", intervals.to_string(), position.value());
} else {
builder.appendff("steps({})", number_of_intervals);
builder.appendff("steps({})", intervals.to_string());
}
}
return MUST(builder.to_string());
@ -420,11 +429,11 @@ double EasingStyleValue::Function::evaluate_at(double input_progress, bool befor
});
}
String EasingStyleValue::Function::to_string() const
String EasingStyleValue::Function::to_string(SerializationMode mode) const
{
return visit(
[&](auto const& curve) {
return curve.to_string();
return curve.to_string(mode);
});
}

View file

@ -11,6 +11,7 @@
#pragma once
#include <LibWeb/CSS/CSSStyleValue.h>
#include <LibWeb/CSS/CalculatedOr.h>
namespace Web::CSS {
@ -35,7 +36,7 @@ public:
bool operator==(Linear const&) const = default;
double evaluate_at(double input_progress, bool before_flag) const;
String to_string() const;
String to_string(SerializationMode) const;
Linear(Vector<Stop> stops);
};
@ -62,7 +63,7 @@ public:
bool operator==(CubicBezier const&) const;
double evaluate_at(double input_progress, bool before_flag) const;
String to_string() const;
String to_string(SerializationMode) const;
};
struct Steps {
@ -78,20 +79,20 @@ public:
static Steps step_start();
static Steps step_end();
unsigned int number_of_intervals;
IntegerOrCalculated number_of_intervals { 0 };
Position position { Position::End };
bool operator==(Steps const&) const = default;
double evaluate_at(double input_progress, bool before_flag) const;
String to_string() const;
String to_string(SerializationMode) const;
};
struct Function : public Variant<Linear, CubicBezier, Steps> {
using Variant::Variant;
double evaluate_at(double input_progress, bool before_flag) const;
String to_string() const;
String to_string(SerializationMode) const;
};
static ValueComparingNonnullRefPtr<EasingStyleValue const> create(Function const& function)
@ -102,7 +103,7 @@ public:
Function const& function() const { return m_function; }
virtual String to_string(SerializationMode) const override { return m_function.to_string(); }
virtual String to_string(SerializationMode mode) const override { return m_function.to_string(mode); }
bool properties_equal(EasingStyleValue const& other) const { return m_function == other.m_function; }

View file

@ -0,0 +1,27 @@
Harness status: OK
Found 21 tests
18 Pass
3 Fail
Pass Property animation-timing-function value 'linear'
Pass Property animation-timing-function value 'ease'
Pass Property animation-timing-function value 'ease-in'
Pass Property animation-timing-function value 'ease-out'
Pass Property animation-timing-function value 'ease-in-out'
Pass Property animation-timing-function value 'cubic-bezier(0.1, 0.2, 0.8, 0.9)'
Pass Property animation-timing-function value 'cubic-bezier(0, -2, 1, 3)'
Pass Property animation-timing-function value 'cubic-bezier(0, 0.7, 1, 1.3)'
Fail Property animation-timing-function value 'cubic-bezier(calc(-2), calc(0.7 / 2), calc(1.5), calc(0))'
Pass Property animation-timing-function value 'steps(4, start)'
Pass Property animation-timing-function value 'steps(2, end)'
Pass Property animation-timing-function value 'steps( 2, end )'
Pass Property animation-timing-function value 'steps(2, jump-start)'
Pass Property animation-timing-function value 'steps(2, jump-end)'
Pass Property animation-timing-function value 'steps(2, jump-both)'
Pass Property animation-timing-function value 'steps(2, jump-none)'
Pass Property animation-timing-function value 'steps(calc(-10), start)'
Pass Property animation-timing-function value 'steps(calc(5 / 2), start)'
Pass Property animation-timing-function value 'steps(calc(1), jump-none)'
Fail Property animation-timing-function value 'linear, ease, linear'
Fail Property animation-timing-function value 'steps(calc(2 + sign(100em - 1px)), end)'

View file

@ -0,0 +1,28 @@
Harness status: OK
Found 22 tests
19 Pass
3 Fail
Pass e.style['animation-timing-function'] = "linear" should set the property value
Pass e.style['animation-timing-function'] = "ease" should set the property value
Pass e.style['animation-timing-function'] = "ease-in" should set the property value
Pass e.style['animation-timing-function'] = "ease-out" should set the property value
Pass e.style['animation-timing-function'] = "ease-in-out" should set the property value
Pass e.style['animation-timing-function'] = "cubic-bezier(0.1, 0.2, 0.8, 0.9)" should set the property value
Pass e.style['animation-timing-function'] = "cubic-bezier(0, -2, 1, 3)" should set the property value
Pass e.style['animation-timing-function'] = "cubic-bezier(0, 0.7, 1, 1.3)" should set the property value
Fail e.style['animation-timing-function'] = "cubic-bezier(calc(-2), calc(0.7 / 2), calc(1.5), calc(0))" should set the property value
Fail e.style['animation-timing-function'] = "cubic-bezier(0, sibling-index(), 1, sign(2em - 20px))" should set the property value
Pass e.style['animation-timing-function'] = "steps(4, start)" should set the property value
Pass e.style['animation-timing-function'] = "steps(2, end)" should set the property value
Pass e.style['animation-timing-function'] = "steps( 2, end )" should set the property value
Pass e.style['animation-timing-function'] = "steps(2, jump-start)" should set the property value
Pass e.style['animation-timing-function'] = "steps(2, jump-end)" should set the property value
Pass e.style['animation-timing-function'] = "steps(2, jump-both)" should set the property value
Pass e.style['animation-timing-function'] = "steps(2, jump-none)" should set the property value
Pass e.style['animation-timing-function'] = "steps(calc(-10), start)" should set the property value
Pass e.style['animation-timing-function'] = "steps(calc(5 / 2), start)" should set the property value
Pass e.style['animation-timing-function'] = "steps(calc(1), jump-none)" should set the property value
Fail e.style['animation-timing-function'] = "linear, ease, linear" should set the property value
Pass e.style['animation-timing-function'] = "steps(calc(2 + sign(100em - 1px)))" should set the property value

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CSS Easing: getComputedStyle().animationTimingFunction</title>
<link rel="help" href="https://drafts.csswg.org/css-easing/#timing-functions">
<meta name="assert" content="animation-timing-function computed value is a computed <easing-function> list.">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../../css/support/computed-testcommon.js"></script>
</head>
<body>
<div id="target"></div>
<script>
test_computed_value("animation-timing-function", "linear");
test_computed_value("animation-timing-function", "ease");
test_computed_value("animation-timing-function", "ease-in");
test_computed_value("animation-timing-function", "ease-out");
test_computed_value("animation-timing-function", "ease-in-out");
test_computed_value("animation-timing-function", "cubic-bezier(0.1, 0.2, 0.8, 0.9)");
test_computed_value("animation-timing-function", "cubic-bezier(0, -2, 1, 3)");
test_computed_value("animation-timing-function", "cubic-bezier(0, 0.7, 1, 1.3)");
test_computed_value("animation-timing-function", "cubic-bezier(calc(-2), calc(0.7 / 2), calc(1.5), calc(0))", "cubic-bezier(0, 0.35, 1, 0)",);
test_computed_value("animation-timing-function", "steps(4, start)");
test_computed_value("animation-timing-function", "steps(2, end)", "steps(2)");
test_computed_value("animation-timing-function", "steps( 2, end )", "steps(2)");
test_computed_value("animation-timing-function", "steps(2, jump-start)");
test_computed_value("animation-timing-function", "steps(2, jump-end)", "steps(2)");
test_computed_value("animation-timing-function", "steps(2, jump-both)");
test_computed_value("animation-timing-function", "steps(2, jump-none)");
test_computed_value("animation-timing-function", "steps(calc(-10), start)", "steps(1, start)");
test_computed_value("animation-timing-function", "steps(calc(5 / 2), start)", "steps(3, start)");
test_computed_value("animation-timing-function", "steps(calc(1), jump-none)", "steps(2, jump-none)");
test_computed_value("animation-timing-function", "linear, ease, linear");
test_computed_value("animation-timing-function", "steps(calc(2 + sign(100em - 1px)), end)", "steps(3)");
</script>
</body>
</html>

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CSS Easing: parsing animation-timing-function with valid values</title>
<link rel="help" href="https://drafts.csswg.org/css-easing/#timing-functions">
<meta name="assert" content="animation-timing-function supports the full grammar '<timing-function> #'.">
<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-timing-function", "linear");
test_valid_value("animation-timing-function", "ease");
test_valid_value("animation-timing-function", "ease-in");
test_valid_value("animation-timing-function", "ease-out");
test_valid_value("animation-timing-function", "ease-in-out");
test_valid_value("animation-timing-function", "cubic-bezier(0.1, 0.2, 0.8, 0.9)");
test_valid_value("animation-timing-function", "cubic-bezier(0, -2, 1, 3)");
test_valid_value("animation-timing-function", "cubic-bezier(0, 0.7, 1, 1.3)");
test_valid_value("animation-timing-function", "cubic-bezier(calc(-2), calc(0.7 / 2), calc(1.5), calc(0))", "cubic-bezier(calc(-2), calc(0.35), calc(1.5), calc(0))");
test_valid_value("animation-timing-function", "cubic-bezier(0, sibling-index(), 1, sign(2em - 20px))");
test_valid_value("animation-timing-function", "steps(4, start)");
test_valid_value("animation-timing-function", "steps(2, end)", "steps(2)");
test_valid_value("animation-timing-function", "steps( 2, end )", "steps(2)");
test_valid_value("animation-timing-function", "steps(2, jump-start)");
test_valid_value("animation-timing-function", "steps(2, jump-end)", "steps(2)");
test_valid_value("animation-timing-function", "steps(2, jump-both)");
test_valid_value("animation-timing-function", "steps(2, jump-none)");
test_valid_value("animation-timing-function", "steps(calc(-10), start)");
test_valid_value("animation-timing-function", "steps(calc(5 / 2), start)", "steps(calc(2.5), start)");
test_valid_value("animation-timing-function", "steps(calc(1), jump-none)");
test_valid_value("animation-timing-function", "linear, ease, linear");
test_valid_value("animation-timing-function", "steps(calc(2 + sign(100em - 1px)))");
</script>
</body>
</html>