LibWeb: Don't canonicalize linear easing function points until use time

Canonicalization can require information that is only known after
compute time (i.e. resolved relative lengths within calcs).

This also allows us to get rid of the `had_explicit_input` flag and just
rely on whether Optional has a value
This commit is contained in:
Callum Law 2025-10-09 21:55:55 +13:00 committed by Sam Atkins
parent 95e26819d9
commit 91925db9ca
5 changed files with 67 additions and 67 deletions

View File

@ -210,6 +210,61 @@ double StepsEasingFunction::evaluate_at(double input_progress, bool before_flag)
return current_step / jumps;
}
// https://drafts.csswg.org/css-easing/#linear-canonicalization
static Vector<LinearEasingFunction::ControlPoint> canonicalize_linear_easing_function_control_points(Vector<LinearEasingFunction::ControlPoint> control_points)
{
// To canonicalize a linear() functions control points, perform the following:
Vector<LinearEasingFunction::ControlPoint> canonicalized_control_points = control_points;
// 1. If the first control point lacks an input progress value, set its input progress value to 0.
if (!canonicalized_control_points.first().input.has_value())
canonicalized_control_points.first().input = 0;
// 2. If the last control point lacks an input progress value, set its input progress value to 1.
if (!canonicalized_control_points.last().input.has_value())
canonicalized_control_points.last().input = 1;
// 3. If any control point has an input progress value that is less than
// the input progress value of any preceding control point,
// set its input progress value to the largest input progress value of any preceding control point.
double largest_input = 0;
for (auto& control_point : canonicalized_control_points) {
if (control_point.input.has_value()) {
if (control_point.input.value() < largest_input) {
control_point.input = largest_input;
} else {
largest_input = control_point.input.value();
}
}
}
// 4. If any control point still lacks an input progress value,
// then for each contiguous run of such control points,
// set their input progress values so that they are evenly spaced
// between the preceding and following control points with input progress values.
Optional<size_t> run_start_idx;
for (size_t idx = 0; idx < canonicalized_control_points.size(); idx++) {
auto& control_point = canonicalized_control_points[idx];
if (control_point.input.has_value() && run_start_idx.has_value()) {
// Note: this stop is immediately after a run
// set inputs of [start, idx-1] stops to be evenly spaced between start-1 and idx
auto start_input = canonicalized_control_points[run_start_idx.value() - 1].input.value();
auto end_input = canonicalized_control_points[idx].input.value();
auto run_stop_count = idx - run_start_idx.value() + 1;
auto delta = (end_input - start_input) / run_stop_count;
for (size_t run_idx = 0; run_idx < run_stop_count; run_idx++) {
canonicalized_control_points[run_idx + run_start_idx.value() - 1].input = start_input + delta * run_idx;
}
run_start_idx = {};
} else if (!control_point.input.has_value() && !run_start_idx.has_value()) {
// Note: this stop is the start of a run
run_start_idx = idx;
}
}
return canonicalized_control_points;
}
EasingFunction EasingFunction::from_style_value(StyleValue const& style_value)
{
if (style_value.is_easing()) {
@ -220,6 +275,11 @@ EasingFunction EasingFunction::from_style_value(StyleValue const& style_value)
for (auto const& control_point : linear.stops)
resolved_control_points.append({ control_point.input, control_point.output });
// 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
// at used value time by linear() canonicalization.
resolved_control_points = canonicalize_linear_easing_function_control_points(resolved_control_points);
return LinearEasingFunction { resolved_control_points, linear.to_string(SerializationMode::ResolvedValue) };
},
[](EasingStyleValue::CubicBezier const& cubic_bezier) -> EasingFunction {

View File

@ -2931,9 +2931,9 @@ RefPtr<StyleValue const> Parser::parse_easing_value(TokenStream<ComponentValue>&
if (argument_tokens.has_next_token() || !output.has_value())
return nullptr;
stops.append({ output.value(), first_input, first_input.has_value() });
stops.append({ output.value(), first_input });
if (second_input.has_value())
stops.append({ output.value(), second_input, true });
stops.append({ output.value(), second_input });
}
if (stops.is_empty())

View File

@ -18,7 +18,7 @@ namespace Web::CSS {
// https://drafts.csswg.org/css-easing-1/#valdef-easing-function-linear
EasingStyleValue::Linear EasingStyleValue::Linear::identity()
{
static Linear linear { { { 0, {}, false }, { 1, {}, false } } };
static Linear linear { { { 0, {} }, { 1, {} } } };
return linear;
}
@ -60,60 +60,6 @@ EasingStyleValue::Steps EasingStyleValue::Steps::step_end()
return steps;
}
// https://drafts.csswg.org/css-easing/#linear-canonicalization
EasingStyleValue::Linear::Linear(Vector<EasingStyleValue::Linear::Stop> stops)
{
// To canonicalize a linear() functions control points, perform the following:
// 1. If the first control point lacks an input progress value, set its input progress value to 0.
if (!stops.first().input.has_value())
stops.first().input = 0;
// 2. If the last control point lacks an input progress value, set its input progress value to 1.
if (!stops.last().input.has_value())
stops.last().input = 1;
// 3. If any control point has an input progress value that is less than
// the input progress value of any preceding control point,
// set its input progress value to the largest input progress value of any preceding control point.
double largest_input = 0;
for (auto& stop : stops) {
if (stop.input.has_value()) {
if (stop.input.value() < largest_input) {
stop.input = largest_input;
} else {
largest_input = stop.input.value();
}
}
}
// 4. If any control point still lacks an input progress value,
// then for each contiguous run of such control points,
// set their input progress values so that they are evenly spaced
// between the preceding and following control points with input progress values.
Optional<size_t> run_start_idx;
for (size_t idx = 0; idx < stops.size(); idx++) {
auto& stop = stops[idx];
if (stop.input.has_value() && run_start_idx.has_value()) {
// Note: this stop is immediately after a run
// set inputs of [start, idx-1] stops to be evenly spaced between start-1 and idx
auto start_input = stops[run_start_idx.value() - 1].input.value();
auto end_input = stops[idx].input.value();
auto run_stop_count = idx - run_start_idx.value() + 1;
auto delta = (end_input - start_input) / run_stop_count;
for (size_t run_idx = 0; run_idx < run_stop_count; run_idx++) {
stops[run_idx + run_start_idx.value() - 1].input = start_input + delta * run_idx;
}
run_start_idx = {};
} else if (!stop.input.has_value() && !run_start_idx.has_value()) {
// Note: this stop is the start of a run
run_start_idx = idx;
}
}
this->stops = move(stops);
}
// https://drafts.csswg.org/css-easing/#linear-easing-function-serializing
String EasingStyleValue::Linear::to_string(SerializationMode) const
{
@ -144,7 +90,7 @@ String EasingStyleValue::Linear::to_string(SerializationMode) const
// 2. If the control point originally lacked an input progress value, return 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.
if (stop.had_explicit_input) {
if (stop.input.has_value()) {
builder.appendff(" {}%", stop.input.value() * 100);
}

View File

@ -25,10 +25,6 @@ public:
double output;
Optional<double> input;
// "NOTE: Serialization relies on whether or not an input progress value was originally supplied,
// so that information should be retained in the internal representation."
bool had_explicit_input;
bool operator==(Stop const&) const = default;
};
@ -37,8 +33,6 @@ public:
bool operator==(Linear const&) const = default;
String to_string(SerializationMode) const;
Linear(Vector<Stop> stops);
};
struct CubicBezier {

View File

@ -2,13 +2,13 @@ Harness status: OK
Found 35 tests
12 Pass
23 Fail
13 Pass
22 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
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
Fail 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
Fail 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