1
0
mirror of https://github.com/SerenityOS/serenity synced 2024-07-01 11:39:22 +00:00

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`)
This commit is contained in:
Matthew Olsson 2024-06-14 21:27:23 -07:00 committed by Nico Weber
parent 4bf4905131
commit e301c1d038
9 changed files with 407 additions and 192 deletions

View File

@ -0,0 +1 @@
PASS

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<div id="foo"></div>
<script src="../../include.js"></script>
<script>
test(() => {
const div = document.getElementById("foo");
const validEasings = [
"linear",
"linear(0, 1)",
"linear(0, 0.5, 1)",
"linear(0 5%, 0.5 10%, 1 100%)",
"linear(5% 0, 10% 0.5, 100% 1)",
"linear(5% 0, 1 100%)",
"linear(-14, 27 210%)",
"ease",
"ease-in",
"ease-out",
"ease-in-out",
"cubic-bezier(0, 0, 0, 0)",
"cubic-bezier(1, 1, 1, 1)",
"cubic-bezier(1, 1000, 1, 1000)",
"step-start",
"step-end",
"steps(1000)",
"steps(10, jump-start)",
"steps(10, jump-end)",
"steps(10, jump-none)",
"steps(10, jump-both)",
"steps(10, start)",
"steps(10, end)",
];
const invalidEasings = [
"abc",
"foo()",
"linear()",
"linear(a, b, c)",
"linear(5 10)",
"linear(5% 10%)",
"linear(0.5 5% 10)",
"linear(0.5 5% 10%)",
"cubic-bezier(0, 0, 0)",
"cubic-bezier(2, 0, 0, 0)",
"cubic-bezier(0, 0, 2, 0)",
"steps(1.5)",
"steps(-1)",
"steps(0, jump-none)",
];
let numFailed = 0;
for (const easing of validEasings) {
try {
div.animate(null, { duration: 1, easing });
} catch {
println(`Failed to parse valid easing ${easing}`);
numFailed++;
}
}
for (const easing of invalidEasings) {
try {
div.animate(null, { duration: 1, easing });
println(`Successfully parsed invalid easing ${easing}`);
numFailed++;
} catch {
}
}
if (numFailed === 0)
println(`PASS`);
});
</script>

View File

@ -184,7 +184,7 @@ protected:
JS::GCPtr<Animation> 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 };

View File

@ -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<size_t>(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

View File

@ -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 <https://www.w3.org/TR/css-easing-1/#valdef-cubic-bezier-easing-function-ease>
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 } };
}

View File

@ -5065,102 +5065,169 @@ RefPtr<StyleValue> Parser::parse_easing_value(TokenStream<ComponentValue>& token
auto const& part = tokens.next_token();
StringView name;
Optional<Vector<ComponentValue> 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<EasingStyleValue> {
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<EasingStyleValue::Linear::Stop> stops;
for (auto const& argument : comma_separated_arguments) {
if (argument.is_empty() || argument.size() > 2)
return nullptr;
Optional<double> offset;
Optional<double> 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<StyleValue> Parser::parse_transition_value(TokenStream<ComponentValue>& 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));

View File

@ -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());

View File

@ -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<StringView> {
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;
}
}

View File

@ -10,38 +10,80 @@
#pragma once
#include <LibWeb/CSS/EasingFunctions.h>
#include <LibWeb/CSS/StyleValue.h>
namespace Web::CSS {
class EasingStyleValue final : public StyleValueWithDefaultOperators<EasingStyleValue> {
public:
static ValueComparingNonnullRefPtr<EasingStyleValue> create(CSS::EasingFunction easing_function, StyleValueVector&& values)
struct Linear {
struct Stop {
double offset;
Optional<double> position;
bool operator==(Stop const&) const = default;
};
Vector<Stop> 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<Linear, CubicBezier, Steps>;
static ValueComparingNonnullRefPtr<EasingStyleValue> 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;
};
}