LibWeb: Support calc within linear() easing function

This commit is contained in:
Callum Law 2025-10-09 23:40:46 +13:00 committed by Sam Atkins
parent 91925db9ca
commit 2f83356c0f
Notes: github-actions[bot] 2025-10-20 10:29:34 +00:00
6 changed files with 94 additions and 34 deletions

View file

@ -607,6 +607,7 @@ Optional<CSS::EasingFunction> AnimationEffect::parse_easing_string(StringView va
{ {
if (auto style_value = parse_css_value(CSS::Parser::ParsingParams(), value, CSS::PropertyID::AnimationTimingFunction)) { if (auto style_value = parse_css_value(CSS::Parser::ParsingParams(), value, CSS::PropertyID::AnimationTimingFunction)) {
if (style_value->is_easing()) if (style_value->is_easing())
// FIXME: We should absolutize style_value to resolve relative lengths within calcs
return CSS::EasingFunction::from_style_value(*style_value); return CSS::EasingFunction::from_style_value(*style_value);
} }

View file

@ -7,6 +7,8 @@
#include "EasingFunction.h" #include "EasingFunction.h"
#include <AK/BinarySearch.h> #include <AK/BinarySearch.h>
#include <LibWeb/CSS/StyleValues/EasingStyleValue.h> #include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
namespace Web::CSS { namespace Web::CSS {
@ -267,13 +269,40 @@ static Vector<LinearEasingFunction::ControlPoint> canonicalize_linear_easing_fun
EasingFunction EasingFunction::from_style_value(StyleValue const& style_value) EasingFunction EasingFunction::from_style_value(StyleValue const& style_value)
{ {
auto const resolve_number = [](StyleValue const& style_value) {
if (style_value.is_number())
return style_value.as_number().number();
if (style_value.is_calculated())
return style_value.as_calculated().resolve_number({}).value();
VERIFY_NOT_REACHED();
};
auto const resolve_percentage = [](StyleValue const& style_value) {
if (style_value.is_percentage())
return style_value.as_percentage().percentage().as_fraction();
if (style_value.is_calculated())
return style_value.as_calculated().resolve_percentage({})->as_fraction();
VERIFY_NOT_REACHED();
};
if (style_value.is_easing()) { if (style_value.is_easing()) {
return style_value.as_easing().function().visit( return style_value.as_easing().function().visit(
[](EasingStyleValue::Linear const& linear) -> EasingFunction { [&](EasingStyleValue::Linear const& linear) -> EasingFunction {
Vector<LinearEasingFunction::ControlPoint> resolved_control_points; Vector<LinearEasingFunction::ControlPoint> resolved_control_points;
for (auto const& control_point : linear.stops) for (auto const& control_point : linear.stops) {
resolved_control_points.append({ control_point.input, control_point.output }); double output = resolve_number(control_point.output);
Optional<double> input;
if (control_point.input)
input = resolve_percentage(*control_point.input);
resolved_control_points.append({ input, output });
}
// https://drafts.csswg.org/css-easing-2/#funcdef-linear // https://drafts.csswg.org/css-easing-2/#funcdef-linear
// If an argument lacks a <percentage>, its input progress value is initially empty. This is corrected // If an argument lacks a <percentage>, its input progress value is initially empty. This is corrected

View file

@ -2908,32 +2908,32 @@ RefPtr<StyleValue const> Parser::parse_easing_value(TokenStream<ComponentValue>&
for (auto const& argument : comma_separated_arguments) { for (auto const& argument : comma_separated_arguments) {
TokenStream argument_tokens { argument }; TokenStream argument_tokens { argument };
Optional<double> output; RefPtr<StyleValue const> output;
Optional<double> first_input; RefPtr<StyleValue const> first_input;
Optional<double> second_input; RefPtr<StyleValue const> second_input;
if (argument_tokens.next_token().is(Token::Type::Number)) if (auto maybe_output = parse_number_value(argument_tokens))
output = argument_tokens.consume_a_token().token().number_value(); output = maybe_output;
if (argument_tokens.next_token().is(Token::Type::Percentage)) { if (auto maybe_first_input = parse_percentage_value(argument_tokens)) {
first_input = argument_tokens.consume_a_token().token().percentage() / 100; first_input = maybe_first_input;
if (argument_tokens.next_token().is(Token::Type::Percentage)) { if (auto maybe_second_input = parse_percentage_value(argument_tokens)) {
second_input = argument_tokens.consume_a_token().token().percentage() / 100; second_input = maybe_second_input;
} }
} }
if (argument_tokens.next_token().is(Token::Type::Number)) { if (auto maybe_output = parse_number_value(argument_tokens)) {
if (output.has_value()) if (output)
return nullptr; return nullptr;
output = argument_tokens.consume_a_token().token().number_value(); output = maybe_output;
} }
if (argument_tokens.has_next_token() || !output.has_value()) if (argument_tokens.has_next_token() || !output)
return nullptr; return nullptr;
stops.append({ output.value(), first_input }); stops.append({ *output, first_input });
if (second_input.has_value()) if (second_input)
stops.append({ output.value(), second_input }); stops.append({ *output, second_input });
} }
if (stops.is_empty()) if (stops.is_empty())

View file

@ -12,13 +12,14 @@
#include <AK/BinarySearch.h> #include <AK/BinarySearch.h>
#include <AK/StringBuilder.h> #include <AK/StringBuilder.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h> #include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
namespace Web::CSS { namespace Web::CSS {
// https://drafts.csswg.org/css-easing-1/#valdef-easing-function-linear // https://drafts.csswg.org/css-easing-1/#valdef-easing-function-linear
EasingStyleValue::Linear EasingStyleValue::Linear::identity() EasingStyleValue::Linear EasingStyleValue::Linear::identity()
{ {
static Linear linear { { { 0, {} }, { 1, {} } } }; static Linear linear { { { NumberStyleValue::create(0), {} }, { NumberStyleValue::create(1), {} } } };
return linear; return linear;
} }
@ -61,7 +62,7 @@ EasingStyleValue::Steps EasingStyleValue::Steps::step_end()
} }
// https://drafts.csswg.org/css-easing/#linear-easing-function-serializing // https://drafts.csswg.org/css-easing/#linear-easing-function-serializing
String EasingStyleValue::Linear::to_string(SerializationMode) const String EasingStyleValue::Linear::to_string(SerializationMode mode) const
{ {
// The linear keyword is serialized as itself. // The linear keyword is serialized as itself.
if (*this == identity()) if (*this == identity())
@ -85,13 +86,13 @@ String EasingStyleValue::Linear::to_string(SerializationMode) const
// To serialize a linear() control point: // To serialize a linear() control point:
// 1. Let s be the serialization, as a <number>, of the control points output progress value. // 1. Let s be the serialization, as a <number>, of the control points output progress value.
builder.appendff("{}", stop.output); builder.appendff(stop.output->to_string(mode));
// 2. If the control point originally lacked an input progress value, return s. // 2. If the control point originally lacked an input progress value, return s.
// 3. Otherwise, append " " (U+0020 SPACE) to s, // 3. Otherwise, append " " (U+0020 SPACE) to s,
// then serialize the control points input progress value as a <percentage> and append it to s. // then serialize the control points input progress value as a <percentage> and append it to s.
if (stop.input.has_value()) { if (stop.input) {
builder.appendff(" {}%", stop.input.value() * 100); builder.appendff(" {}", stop.input->to_string(mode));
} }
// 4. Return s. // 4. Return s.
@ -169,4 +170,31 @@ String EasingStyleValue::Function::to_string(SerializationMode mode) const
}); });
} }
ValueComparingNonnullRefPtr<StyleValue const> EasingStyleValue::absolutized(ComputationContext const& computation_context) const
{
auto const& absolutized_function = m_function.visit(
[&](Linear const& linear) -> Function {
Vector<Linear::Stop> absolutized_stops;
for (auto stop : linear.stops) {
RefPtr<StyleValue const> absolutized_input;
if (stop.input)
absolutized_input = stop.input->absolutized(computation_context);
absolutized_stops.append({ stop.output->absolutized(computation_context), absolutized_input });
}
return Linear { absolutized_stops };
},
[&](CubicBezier const& cubic_bezier) -> Function {
return cubic_bezier;
},
[&](Steps const& steps) -> Function {
return steps;
});
return EasingStyleValue::create(absolutized_function);
}
} }

View file

@ -22,8 +22,8 @@ public:
static Linear identity(); static Linear identity();
struct Stop { struct Stop {
double output; ValueComparingNonnullRefPtr<StyleValue const> output;
Optional<double> input; ValueComparingRefPtr<StyleValue const> input;
bool operator==(Stop const&) const = default; bool operator==(Stop const&) const = default;
}; };
@ -90,6 +90,8 @@ public:
virtual String to_string(SerializationMode mode) const override { return m_function.to_string(mode); } virtual String to_string(SerializationMode mode) const override { return m_function.to_string(mode); }
virtual ValueComparingNonnullRefPtr<StyleValue const> absolutized(ComputationContext const&) const override;
bool properties_equal(EasingStyleValue const& other) const { return m_function == other.m_function; } bool properties_equal(EasingStyleValue const& other) const { return m_function == other.m_function; }
private: private:

View file

@ -2,16 +2,16 @@ Harness status: OK
Found 35 tests Found 35 tests
13 Pass 19 Pass
22 Fail 16 Fail
Pass e.style['animation-timing-function'] = "linear(0 0%, 1 100%)" should set the property value Pass e.style['animation-timing-function'] = "linear(0 0%, 1 100%)" should set the property value
Pass e.style['animation-timing-function'] = "linear( 0 0%, 1 100% )" should set the property value Pass e.style['animation-timing-function'] = "linear( 0 0%, 1 100% )" should set the property value
Fail e.style['animation-timing-function'] = "linear(0, 1)" should set the property value Fail e.style['animation-timing-function'] = "linear(0, 1)" should set the property value
Pass e.style['animation-timing-function'] = "linear(-10, -5, 0, 5, 10)" should set the property value Pass e.style['animation-timing-function'] = "linear(-10, -5, 0, 5, 10)" should set the property value
Pass e.style['animation-timing-function'] = "linear(-10 -10%, -5 -5%, 0, 5, 10)" should set the property value Pass e.style['animation-timing-function'] = "linear(-10 -10%, -5 -5%, 0, 5, 10)" should set the property value
Fail e.style['animation-timing-function'] = "linear(0 calc(0%), 0 calc(100%))" should set the property value Pass e.style['animation-timing-function'] = "linear(0 calc(0%), 0 calc(100%))" should set the property value
Fail e.style['animation-timing-function'] = "linear(0 calc(50% - 50%), 0 calc(50% + 50%))" should set the property value Pass e.style['animation-timing-function'] = "linear(0 calc(50% - 50%), 0 calc(50% + 50%))" should set the property value
Fail e.style['animation-timing-function'] = "linear(0 calc(50%), 0 100%)" should set the property value Pass e.style['animation-timing-function'] = "linear(0 calc(50%), 0 100%)" should set the property value
Fail e.style['animation-timing-function'] = "linear(0 0% 50%, 1 50% 100%)" should set the property value Fail e.style['animation-timing-function'] = "linear(0 0% 50%, 1 50% 100%)" should set the property value
Fail e.style['animation-timing-function'] = "linear(0, 0.5 25% 75%, 1 100% 100%)" should set the property value Fail e.style['animation-timing-function'] = "linear(0, 0.5 25% 75%, 1 100% 100%)" should set the property value
Fail e.style['animation-timing-function'] = "linear(0, 1.3, 1, 0.92, 1, 0.99, 1, 0.998, 1 100% 100%)" should set the property value Fail e.style['animation-timing-function'] = "linear(0, 1.3, 1, 0.92, 1, 0.99, 1, 0.998, 1 100% 100%)" should set the property value
@ -31,9 +31,9 @@ Pass Property animation-timing-function value 'linear( 0 0%, 1 100% )'
Fail Property animation-timing-function value 'linear(0, 1)' Fail Property animation-timing-function value 'linear(0, 1)'
Fail Property animation-timing-function value 'linear(-10, -5, 0, 5, 10)' Fail Property animation-timing-function value 'linear(-10, -5, 0, 5, 10)'
Fail Property animation-timing-function value 'linear(-10 -10%, -5 -5%, 0, 5, 10)' Fail Property animation-timing-function value 'linear(-10 -10%, -5 -5%, 0, 5, 10)'
Fail Property animation-timing-function value 'linear(0 calc(0%), 0 calc(100%))' Pass Property animation-timing-function value 'linear(0 calc(0%), 0 calc(100%))'
Fail Property animation-timing-function value 'linear(0 calc(50% - 50%), 0 calc(50% + 50%))' Pass Property animation-timing-function value 'linear(0 calc(50% - 50%), 0 calc(50% + 50%))'
Fail Property animation-timing-function value 'linear(0 calc(min(50%, 60%)), 0 100%)' Pass Property animation-timing-function value 'linear(0 calc(min(50%, 60%)), 0 100%)'
Pass Property animation-timing-function value 'linear(0 0% 50%, 1 50% 100%)' Pass Property animation-timing-function value 'linear(0 0% 50%, 1 50% 100%)'
Fail Property animation-timing-function value 'linear(0, 0.5 25% 75%, 1 100% 100%)' Fail Property animation-timing-function value 'linear(0, 0.5 25% 75%, 1 100% 100%)'
Fail Property animation-timing-function value 'linear(0, 1.3, 1, 0.92, 1, 0.99, 1, 0.998, 1 100% 100%)' Fail Property animation-timing-function value 'linear(0, 1.3, 1, 0.92, 1, 0.99, 1, 0.998, 1 100% 100%)'