From e301c1d0380e306b74d1887878823589fde02313 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Fri, 14 Jun 2024 21:27:23 -0700 Subject: [PATCH] LibWeb: Parse easing values manually The values aren't that complex, so it doesn't make much sense to have a dedicated generator for them. Parsing them manually also allows us to have much more control over the produced values, so as a result of this change, EasingStyleValue becomes much more ergonomic. (cherry picked from commit 667e313731f06fabf2a3f75893c3e8f15a4172be, manually amended with the output of `git clang-format master`) --- .../WebAnimations/misc/easing-parsing.txt | 1 + .../WebAnimations/misc/easing-parsing.html | 74 ++++++ .../LibWeb/Animations/AnimationEffect.h | 2 +- .../LibWeb/Animations/TimingFunction.cpp | 85 ++---- .../LibWeb/Animations/TimingFunction.h | 7 - .../Libraries/LibWeb/CSS/Parser/Parser.cpp | 243 +++++++++++------- .../Libraries/LibWeb/CSS/StyleComputer.cpp | 3 +- .../CSS/StyleValues/EasingStyleValue.cpp | 116 +++++++-- .../LibWeb/CSS/StyleValues/EasingStyleValue.h | 68 ++++- 9 files changed, 407 insertions(+), 192 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/WebAnimations/misc/easing-parsing.txt create mode 100644 Tests/LibWeb/Text/input/WebAnimations/misc/easing-parsing.html diff --git a/Tests/LibWeb/Text/expected/WebAnimations/misc/easing-parsing.txt b/Tests/LibWeb/Text/expected/WebAnimations/misc/easing-parsing.txt new file mode 100644 index 0000000000..631b0a0751 --- /dev/null +++ b/Tests/LibWeb/Text/expected/WebAnimations/misc/easing-parsing.txt @@ -0,0 +1 @@ + PASS diff --git a/Tests/LibWeb/Text/input/WebAnimations/misc/easing-parsing.html b/Tests/LibWeb/Text/input/WebAnimations/misc/easing-parsing.html new file mode 100644 index 0000000000..d86440e8c2 --- /dev/null +++ b/Tests/LibWeb/Text/input/WebAnimations/misc/easing-parsing.html @@ -0,0 +1,74 @@ + +
+ + diff --git a/Userland/Libraries/LibWeb/Animations/AnimationEffect.h b/Userland/Libraries/LibWeb/Animations/AnimationEffect.h index 033442d028..b070ea3624 100644 --- a/Userland/Libraries/LibWeb/Animations/AnimationEffect.h +++ b/Userland/Libraries/LibWeb/Animations/AnimationEffect.h @@ -184,7 +184,7 @@ protected: JS::GCPtr m_associated_animation {}; // https://www.w3.org/TR/web-animations-1/#time-transformations - TimingFunction m_timing_function { linear_timing_function }; + TimingFunction m_timing_function { LinearTimingFunction {} }; // Used for calculating transitions in StyleComputer Phase m_previous_phase { Phase::Idle }; diff --git a/Userland/Libraries/LibWeb/Animations/TimingFunction.cpp b/Userland/Libraries/LibWeb/Animations/TimingFunction.cpp index cb5b9ed0fa..6ce08b3515 100644 --- a/Userland/Libraries/LibWeb/Animations/TimingFunction.cpp +++ b/Userland/Libraries/LibWeb/Animations/TimingFunction.cpp @@ -169,76 +169,39 @@ double StepsTimingFunction::operator()(double input_progress, bool before_flag) TimingFunction TimingFunction::from_easing_style_value(CSS::EasingStyleValue const& easing_value) { - switch (easing_value.easing_function()) { - case CSS::EasingFunction::Linear: - return Animations::linear_timing_function; - case CSS::EasingFunction::Ease: - return Animations::ease_timing_function; - case CSS::EasingFunction::EaseIn: - return Animations::ease_in_timing_function; - case CSS::EasingFunction::EaseOut: - return Animations::ease_out_timing_function; - case CSS::EasingFunction::EaseInOut: - return Animations::ease_in_out_timing_function; - case CSS::EasingFunction::CubicBezier: { - auto values = easing_value.values(); - return { - Animations::CubicBezierTimingFunction { - values[0]->as_number().number(), - values[1]->as_number().number(), - values[2]->as_number().number(), - values[3]->as_number().number(), - }, - }; - } - case CSS::EasingFunction::Steps: { - auto values = easing_value.values(); - auto jump_at_start = false; - auto jump_at_end = true; + return easing_value.function().visit( + [](CSS::EasingStyleValue::Linear const& linear) { + if (!linear.stops.is_empty()) { + dbgln("FIXME: Handle linear easing functions with stops"); + } + return TimingFunction { LinearTimingFunction {} }; + }, + [](CSS::EasingStyleValue::CubicBezier const& bezier) { + return TimingFunction { CubicBezierTimingFunction { bezier.x1, bezier.y1, bezier.x2, bezier.y2 } }; + }, + [](CSS::EasingStyleValue::Steps const& steps) { + auto jump_at_start = false; + auto jump_at_end = false; - if (values.size() > 1) { - auto identifier = values[1]->to_identifier(); - switch (identifier) { - case CSS::ValueID::JumpStart: - case CSS::ValueID::Start: + switch (steps.position) { + case CSS::EasingStyleValue::Steps::Position::Start: + case CSS::EasingStyleValue::Steps::Position::JumpStart: jump_at_start = true; - jump_at_end = false; break; - case CSS::ValueID::JumpEnd: - case CSS::ValueID::End: - jump_at_start = false; + case CSS::EasingStyleValue::Steps::Position::End: + case CSS::EasingStyleValue::Steps::Position::JumpEnd: jump_at_end = true; break; - case CSS::ValueID::JumpNone: - jump_at_start = false; - jump_at_end = false; + case CSS::EasingStyleValue::Steps::Position::JumpBoth: + jump_at_start = true; + jump_at_end = true; break; - default: + case CSS::EasingStyleValue::Steps::Position::JumpNone: break; } - } - return Animations::TimingFunction { Animations::StepsTimingFunction { - .number_of_steps = static_cast(max(values[0]->as_integer().integer(), !(jump_at_end && jump_at_start) ? 1 : 0)), - .jump_at_start = jump_at_start, - .jump_at_end = jump_at_end, - } }; - } - case CSS::EasingFunction::StepEnd: - return Animations::TimingFunction { Animations::StepsTimingFunction { - .number_of_steps = 1, - .jump_at_start = false, - .jump_at_end = true, - } }; - case CSS::EasingFunction::StepStart: - return Animations::TimingFunction { Animations::StepsTimingFunction { - .number_of_steps = 1, - .jump_at_start = true, - .jump_at_end = false, - } }; - default: - return Animations::ease_timing_function; - } + return TimingFunction { StepsTimingFunction { steps.number_of_intervals, jump_at_start, jump_at_end } }; + }); } double TimingFunction::operator()(double input_progress, bool before_flag) const diff --git a/Userland/Libraries/LibWeb/Animations/TimingFunction.h b/Userland/Libraries/LibWeb/Animations/TimingFunction.h index f2bd14d2a2..69e956f1ef 100644 --- a/Userland/Libraries/LibWeb/Animations/TimingFunction.h +++ b/Userland/Libraries/LibWeb/Animations/TimingFunction.h @@ -56,11 +56,4 @@ struct TimingFunction { double operator()(double input_progress, bool before_flag) const; }; -static TimingFunction linear_timing_function { LinearTimingFunction {} }; -// NOTE: Magic values from -static TimingFunction ease_timing_function { CubicBezierTimingFunction { 0.25, 0.1, 0.25, 1.0 } }; -static TimingFunction ease_in_timing_function { CubicBezierTimingFunction { 0.42, 0.0, 1.0, 1.0 } }; -static TimingFunction ease_out_timing_function { CubicBezierTimingFunction { 0.0, 0.0, 0.58, 1.0 } }; -static TimingFunction ease_in_out_timing_function { CubicBezierTimingFunction { 0.42, 0.0, 0.58, 1.0 } }; - } diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 0497856526..947851aecb 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -5065,102 +5065,169 @@ RefPtr Parser::parse_easing_value(TokenStream& token auto const& part = tokens.next_token(); - StringView name; - Optional const&> arguments; if (part.is(Token::Type::Ident)) { - name = part.token().ident(); - } else if (part.is_function()) { - name = part.function().name(); - arguments = part.function().values(); - } else { - return nullptr; - } + auto name = part.token().ident(); + auto maybe_simple_easing = [&] -> RefPtr { + if (name == "linear"sv) + return EasingStyleValue::create(EasingStyleValue::Linear {}); + if (name == "ease"sv) + return EasingStyleValue::create(EasingStyleValue::CubicBezier::ease()); + if (name == "ease-in"sv) + return EasingStyleValue::create(EasingStyleValue::CubicBezier::ease_in()); + if (name == "ease-out"sv) + return EasingStyleValue::create(EasingStyleValue::CubicBezier::ease_out()); + if (name == "ease-in-out"sv) + return EasingStyleValue::create(EasingStyleValue::CubicBezier::ease_in_out()); + if (name == "step-start"sv) + return EasingStyleValue::create(EasingStyleValue::Steps::step_start()); + if (name == "step-end"sv) + return EasingStyleValue::create(EasingStyleValue::Steps::step_end()); + return {}; + }(); - auto maybe_function = easing_function_from_string(name); - if (!maybe_function.has_value()) - return nullptr; - - auto function = maybe_function.release_value(); - auto function_metadata = easing_function_metadata(function); - - if (function_metadata.parameters.is_empty() && arguments.has_value()) { - dbgln_if(CSS_PARSER_DEBUG, "Too many arguments to {}. max: 0", name); - return nullptr; - } - - StyleValueVector values; - size_t argument_index = 0; - if (arguments.has_value()) { - auto argument_tokens = TokenStream { *arguments }; - auto arguments_values = parse_a_comma_separated_list_of_component_values(argument_tokens); - if (arguments_values.size() > function_metadata.parameters.size()) { - dbgln_if(CSS_PARSER_DEBUG, "Too many arguments to {}. max: {}", name, function_metadata.parameters.size()); + if (!maybe_simple_easing) return nullptr; - } - for (auto& argument_values : arguments_values) { - // Prune any whitespace before and after the actual argument values. - argument_values.remove_all_matching([](auto& value) { return value.is(Token::Type::Whitespace); }); - if (argument_values.size() != 1) { - dbgln_if(CSS_PARSER_DEBUG, "Too many values in argument to {}. max: 1", name); + transaction.commit(); + return maybe_simple_easing; + } + + if (!part.is_function()) + return nullptr; + + TokenStream argument_tokens { part.function().values() }; + auto comma_separated_arguments = parse_a_comma_separated_list_of_component_values(argument_tokens); + + // Remove whitespace + for (auto& argument : comma_separated_arguments) + argument.remove_all_matching([](auto& value) { return value.is(Token::Type::Whitespace); }); + + auto name = part.function().name(); + if (name == "linear"sv) { + Vector stops; + for (auto const& argument : comma_separated_arguments) { + if (argument.is_empty() || argument.size() > 2) + return nullptr; + + Optional offset; + Optional position; + + for (auto const& part : argument) { + if (part.is(Token::Type::Number)) { + if (offset.has_value()) + return nullptr; + offset = part.token().number_value(); + } else if (part.is(Token::Type::Percentage)) { + if (position.has_value()) + return nullptr; + position = part.token().percentage(); + } else { + return nullptr; + }; + } + + if (!offset.has_value()) + return nullptr; + + stops.append({ offset.value(), move(position) }); + } + + if (stops.is_empty()) + return nullptr; + + transaction.commit(); + return EasingStyleValue::create(EasingStyleValue::Linear { move(stops) }); + } + + if (name == "cubic-bezier") { + if (comma_separated_arguments.size() != 4) + return nullptr; + + for (auto const& argument : comma_separated_arguments) { + if (argument.size() != 1) + return nullptr; + if (!argument[0].is(Token::Type::Number)) + return nullptr; + } + + EasingStyleValue::CubicBezier bezier { + comma_separated_arguments[0][0].token().number_value(), + comma_separated_arguments[1][0].token().number_value(), + comma_separated_arguments[2][0].token().number_value(), + comma_separated_arguments[3][0].token().number_value(), + }; + + if (bezier.x1 < 0.0 || bezier.x1 > 1.0 || bezier.x2 < 0.0 || bezier.x2 > 1.0) + return nullptr; + + transaction.commit(); + return EasingStyleValue::create(bezier); + } + + if (name == "steps") { + if (comma_separated_arguments.is_empty() || comma_separated_arguments.size() > 2) + return nullptr; + + for (auto const& argument : comma_separated_arguments) { + if (argument.size() != 1) + return nullptr; + } + + EasingStyleValue::Steps steps; + + auto intervals_argument = comma_separated_arguments[0][0]; + if (!intervals_argument.is(Token::Type::Number)) + 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] }; + auto ident = parse_identifier_value(identifier_stream); + if (!ident) + return nullptr; + switch (ident->to_identifier()) { + case ValueID::JumpStart: + steps.position = EasingStyleValue::Steps::Position::JumpStart; + break; + case ValueID::JumpEnd: + steps.position = EasingStyleValue::Steps::Position::JumpEnd; + break; + case ValueID::JumpBoth: + steps.position = EasingStyleValue::Steps::Position::JumpBoth; + break; + case ValueID::JumpNone: + steps.position = EasingStyleValue::Steps::Position::JumpNone; + break; + case ValueID::Start: + steps.position = EasingStyleValue::Steps::Position::Start; + break; + case ValueID::End: + steps.position = EasingStyleValue::Steps::Position::End; + break; + default: return nullptr; } - - auto& value = argument_values[0]; - auto value_as_stream = TokenStream { argument_values }; - switch (function_metadata.parameters[argument_index].type) { - case EasingFunctionParameterType::Number: { - if (value.is(Token::Type::Number)) - values.append(NumberStyleValue::create(value.token().number().value())); - else - return nullptr; - break; - } - case EasingFunctionParameterType::NumberZeroToOne: { - if (value.is(Token::Type::Number) && value.token().number_value() >= 0 && value.token().number_value() <= 1) - values.append(NumberStyleValue::create(value.token().number().value())); - else - return nullptr; - break; - } - case EasingFunctionParameterType::Integer: { - if (value.is(Token::Type::Number) && value.token().number().is_integer()) - values.append(IntegerStyleValue::create(value.token().number().integer_value())); - else - return nullptr; - break; - } - case EasingFunctionParameterType::StepPosition: { - if (!value.is(Token::Type::Ident)) - return nullptr; - auto ident = parse_identifier_value(value_as_stream); - if (!ident) - return nullptr; - switch (ident->to_identifier()) { - case ValueID::JumpStart: - case ValueID::JumpEnd: - case ValueID::JumpNone: - case ValueID::Start: - case ValueID::End: - values.append(*ident); - break; - default: - return nullptr; - } - } - } - - ++argument_index; } + + // Perform extra validation + // https://drafts.csswg.org/css-easing/#funcdef-step-easing-function-steps + // The first parameter specifies the number of intervals in the function. It must be a positive integer greater than 0 + // unless the second parameter is jump-none in which case it must be a positive integer greater than 1. + if (steps.position == EasingStyleValue::Steps::Position::JumpNone) { + if (intervals < 1) + return nullptr; + } else if (intervals < 0) { + return nullptr; + } + + steps.number_of_intervals = intervals; + transaction.commit(); + return EasingStyleValue::create(steps); } - if (argument_index < function_metadata.parameters.size() && !function_metadata.parameters[argument_index].is_optional) { - dbgln_if(CSS_PARSER_DEBUG, "Required parameter at position {} is missing", argument_index); - return nullptr; - } - - transaction.commit(); - return EasingStyleValue::create(function, move(values)); + return nullptr; } // https://www.w3.org/TR/css-transforms-1/#transform-property @@ -5483,7 +5550,7 @@ RefPtr Parser::parse_transition_value(TokenStream& t transition.property_name = CustomIdentStyleValue::create("all"_fly_string); if (!transition.easing) - transition.easing = EasingStyleValue::create(EasingFunction::Ease, {}); + transition.easing = EasingStyleValue::create(EasingStyleValue::CubicBezier::ease()); transitions.append(move(transition)); diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp index 9401647155..de92b0041a 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -1557,7 +1557,8 @@ static void apply_animation_properties(DOM::Document& document, StyleProperties& play_state = *play_state_value; } - Animations::TimingFunction timing_function = Animations::ease_timing_function; + static Animations::TimingFunction ease_timing_function = Animations::TimingFunction::from_easing_style_value(*CSS::EasingStyleValue::create(CSS::EasingStyleValue::CubicBezier::ease())); + Animations::TimingFunction timing_function = ease_timing_function; if (auto timing_property = style.maybe_null_property(PropertyID::AnimationTimingFunction); timing_property && timing_property->is_easing()) timing_function = Animations::TimingFunction::from_easing_style_value(timing_property->as_easing()); diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/EasingStyleValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/EasingStyleValue.cpp index 331e059545..3ddf1bfe35 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/EasingStyleValue.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/EasingStyleValue.cpp @@ -13,33 +13,107 @@ namespace Web::CSS { +// NOTE: Magic cubic bezier values from https://www.w3.org/TR/css-easing-1/#valdef-cubic-bezier-easing-function-ease + +EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease() +{ + static CubicBezier bezier { 0.25, 0.1, 0.25, 1.0 }; + return bezier; +} + +EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in() +{ + static CubicBezier bezier { 0.42, 0.0, 1.0, 1.0 }; + return bezier; +} + +EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_out() +{ + static CubicBezier bezier { 0.0, 0.0, 0.58, 1.0 }; + return bezier; +} + +EasingStyleValue::CubicBezier EasingStyleValue::CubicBezier::ease_in_out() +{ + static CubicBezier bezier { 0.42, 0.0, 0.58, 1.0 }; + return bezier; +} + +EasingStyleValue::Steps EasingStyleValue::Steps::step_start() +{ + static Steps steps { 1, Steps::Position::Start }; + return steps; +} + +EasingStyleValue::Steps EasingStyleValue::Steps::step_end() +{ + static Steps steps { 1, Steps::Position::End }; + return steps; +} + String EasingStyleValue::to_string() const { - if (m_properties.easing_function == EasingFunction::StepStart) - return "steps(1, start)"_string; - if (m_properties.easing_function == EasingFunction::StepEnd) - return "steps(1, end)"_string; - StringBuilder builder; - builder.append(CSS::to_string(m_properties.easing_function)); + m_function.visit( + [&](Linear const& linear) { + builder.append("linear"sv); + if (!linear.stops.is_empty()) { + builder.append('('); - if (m_properties.values.is_empty()) - return MUST(builder.to_string()); - - builder.append('('); - for (size_t i = 0; i < m_properties.values.size(); ++i) { - builder.append(m_properties.values[i]->to_string()); - if (i != m_properties.values.size() - 1) - builder.append(", "sv); - } - builder.append(')'); + bool first = true; + for (auto const& stop : linear.stops) { + if (!first) + builder.append(", "sv); + first = false; + builder.appendff("{}"sv, stop.offset); + if (stop.position.has_value()) + builder.appendff(" {}"sv, stop.position.value()); + } + builder.append(')'); + } + }, + [&](CubicBezier const& bezier) { + if (bezier == CubicBezier::ease()) { + builder.append("ease"sv); + } else if (bezier == CubicBezier::ease_in()) { + builder.append("ease-in"sv); + } else if (bezier == CubicBezier::ease_out()) { + builder.append("ease-out"sv); + } else if (bezier == CubicBezier::ease_in_out()) { + builder.append("ease-in-out"sv); + } else { + builder.appendff("cubic-bezier({}, {}, {}, {})", bezier.x1, bezier.y1, bezier.x2, bezier.y2); + } + }, + [&](Steps const& steps) { + if (steps == Steps::step_start()) { + builder.append("step-start"sv); + } else if (steps == Steps::step_end()) { + builder.append("step-end"sv); + } else { + auto position = [&] -> Optional { + switch (steps.position) { + case Steps::Position::JumpStart: + return "jump-start"sv; + case Steps::Position::JumpNone: + return "jump-none"sv; + case Steps::Position::JumpBoth: + return "jump-both"sv; + case Steps::Position::Start: + return "start"sv; + default: + return {}; + } + }(); + if (position.has_value()) { + builder.appendff("steps({}, {})", steps.number_of_intervals, position.value()); + } else { + builder.appendff("steps({})", steps.number_of_intervals); + } + } + }); return MUST(builder.to_string()); } -bool EasingStyleValue::Properties::operator==(Properties const& other) const -{ - return easing_function == other.easing_function && values == other.values; -} - } diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/EasingStyleValue.h b/Userland/Libraries/LibWeb/CSS/StyleValues/EasingStyleValue.h index fae12ac36b..81ec6442c4 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/EasingStyleValue.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/EasingStyleValue.h @@ -10,38 +10,80 @@ #pragma once -#include #include namespace Web::CSS { class EasingStyleValue final : public StyleValueWithDefaultOperators { public: - static ValueComparingNonnullRefPtr create(CSS::EasingFunction easing_function, StyleValueVector&& values) + struct Linear { + struct Stop { + double offset; + Optional position; + + bool operator==(Stop const&) const = default; + }; + + Vector stops; + + bool operator==(Linear const&) const = default; + }; + + struct CubicBezier { + static CubicBezier ease(); + static CubicBezier ease_in(); + static CubicBezier ease_out(); + static CubicBezier ease_in_out(); + + double x1; + double y1; + double x2; + double y2; + + bool operator==(CubicBezier const&) const = default; + }; + + struct Steps { + enum class Position { + JumpStart, + JumpEnd, + JumpNone, + JumpBoth, + Start, + End, + }; + + static Steps step_start(); + static Steps step_end(); + + unsigned int number_of_intervals; + Position position { Position::End }; + + bool operator==(Steps const&) const = default; + }; + + using Function = Variant; + + static ValueComparingNonnullRefPtr create(Function const& function) { - return adopt_ref(*new (nothrow) EasingStyleValue(easing_function, move(values))); + return adopt_ref(*new (nothrow) EasingStyleValue(function)); } virtual ~EasingStyleValue() override = default; - CSS::EasingFunction easing_function() const { return m_properties.easing_function; } - StyleValueVector values() const { return m_properties.values; } + Function const& function() const { return m_function; } virtual String to_string() const override; - bool properties_equal(EasingStyleValue const& other) const { return m_properties == other.m_properties; } + bool properties_equal(EasingStyleValue const& other) const { return m_function == other.m_function; } private: - EasingStyleValue(CSS::EasingFunction easing_function, StyleValueVector&& values) + EasingStyleValue(Function function) : StyleValueWithDefaultOperators(Type::Easing) - , m_properties { .easing_function = easing_function, .values = move(values) } + , m_function(function) { } - struct Properties { - CSS::EasingFunction easing_function; - StyleValueVector values; - bool operator==(Properties const& other) const; - } m_properties; + Function m_function; }; }