LibWeb: Allow calculated values in css filters

This commit is contained in:
Gingeh 2024-10-31 12:29:03 +11:00 committed by Andreas Kling
parent af3383df09
commit 4ecf56cadf
Notes: github-actions[bot] 2024-10-31 07:20:39 +00:00
11 changed files with 166 additions and 59 deletions

View file

@ -20,3 +20,4 @@
<div style="filter: drop-shadow(5px 5px 5px)"></div>
<div style="filter: drop-shadow(red 5px 5px 5px)"></div>
<div style="filter: drop-shadow(5px 5px 5px red)"></div>
<div style="filter: drop-shadow(calc(5*1px) calc(2px + 3px))"></div>

View file

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<link rel="match" href="reference/css-filter-ref.html" />
<style>
body {
font-size: 0;
}
img {
width: 50px;
}
</style>
<!-- blur() -->
<img src="assets/car.png" style="filter: blur()">
<img src="assets/car.png" style="filter: blur(0)">
<img src="assets/car.png" style="filter: blur(4px)">
<img src="assets/car.png" style="filter: blur(calc(4 * 1px))">
<!-- drop-shadow() is tested in css-filter-drop-shadow.html -->
<!-- hue-rotate() -->
<img src="assets/car.png" style="filter: hue-rotate()">
<img src="assets/car.png" style="filter: hue-rotate(0)">
<img src="assets/car.png" style="filter: hue-rotate(90)">
<img src="assets/car.png" style="filter: hue-rotate(90deg)">
<img src="assets/car.png" style="filter: hue-rotate(-90deg)">
<img src="assets/car.png" style="filter: hue-rotate(calc(180 * 1deg))">
<!-- simple color filters -->
<!-- omitted -->
<img src="assets/car.png" style="filter: brightness()">
<img src="assets/car.png" style="filter: contrast()">
<img src="assets/car.png" style="filter: grayscale()">
<img src="assets/car.png" style="filter: invert()">
<img src="assets/car.png" style="filter: opacity()">
<img src="assets/car.png" style="filter: sepia()">
<img src="assets/car.png" style="filter: saturate()">
<!-- number -->
<img src="assets/car.png" style="filter: brightness(0.75)">
<img src="assets/car.png" style="filter: contrast(0.75)">
<img src="assets/car.png" style="filter: grayscale(0.75)">
<img src="assets/car.png" style="filter: invert(0.75)">
<img src="assets/car.png" style="filter: opacity(0.75)">
<img src="assets/car.png" style="filter: sepia(0.75)">
<img src="assets/car.png" style="filter: saturate(0.75)">
<!-- percentage -->
<img src="assets/car.png" style="filter: brightness(75%)">
<img src="assets/car.png" style="filter: contrast(75%)">
<img src="assets/car.png" style="filter: grayscale(75%)">
<img src="assets/car.png" style="filter: invert(75%)">
<img src="assets/car.png" style="filter: opacity(75%)">
<img src="assets/car.png" style="filter: sepia(75%)">
<img src="assets/car.png" style="filter: saturate(75%)">
<!-- calculated number -->
<img src="assets/car.png" style="filter: brightness(calc(3 / 4))">
<img src="assets/car.png" style="filter: contrast(calc(3 / 4))">
<img src="assets/car.png" style="filter: grayscale(calc(3 / 4))">
<img src="assets/car.png" style="filter: invert(calc(3 / 4))">
<img src="assets/car.png" style="filter: opacity(calc(3 / 4))">
<img src="assets/car.png" style="filter: sepia(calc(3 / 4))">
<img src="assets/car.png" style="filter: saturate(calc(3 / 4))">
<!-- calculated percentage -->
<img src="assets/car.png" style="filter: brightness(calc(3 * 25%))">
<img src="assets/car.png" style="filter: contrast(calc(3 * 25%))">
<img src="assets/car.png" style="filter: grayscale(calc(3 * 25%))">
<img src="assets/car.png" style="filter: invert(calc(3 * 25%))">
<img src="assets/car.png" style="filter: opacity(calc(3 * 25%))">
<img src="assets/car.png" style="filter: sepia(calc(3 * 25%))">
<img src="assets/car.png" style="filter: saturate(calc(3 * 25%))">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View file

@ -0,0 +1,10 @@
<style>
* {
margin: 0;
}
body {
background-color: white;
}
</style>
<img src="../images/css-filter-ref.png">

View file

@ -4,10 +4,10 @@ animation-duration: 'calc(2s)' -> 'calc(2s)'
animation-duration: 'calc(2s * var(--n))' -> '4s'
animation-iteration-count: 'calc(2)' -> 'calc(2)'
animation-iteration-count: 'calc(2 * var(--n))' -> '4'
backdrop-filter: 'grayscale(calc(2%))' -> 'none'
backdrop-filter: 'grayscale(calc(2% * var(--n)))' -> 'none'
backdrop-filter: 'grayscale(calc(0.02))' -> 'none'
backdrop-filter: 'grayscale(calc(0.02 * var(--n)))' -> 'none'
backdrop-filter: 'grayscale(calc(2%))' -> 'grayscale(calc(2%))'
backdrop-filter: 'grayscale(calc(2% * var(--n)))' -> 'grayscale(calc(2% * 2))'
backdrop-filter: 'grayscale(calc(0.02))' -> 'grayscale(calc(0.02))'
backdrop-filter: 'grayscale(calc(0.02 * var(--n)))' -> 'grayscale(calc(0.02 * 2))'
background-position-x: 'calc(2px)' -> 'left calc(2px)'
background-position-x: 'calc(2px * var(--n))' -> 'left calc(2px * 2)'
background-position-y: 'calc(2%)' -> 'top calc(2%)'
@ -56,10 +56,10 @@ cy: 'calc(2%)' -> 'calc(2%)'
cy: 'calc(2% * var(--n))' -> '4%'
fill-opacity: 'calc(2)' -> 'calc(2)'
fill-opacity: 'calc(2 * var(--n))' -> '4'
filter: 'grayscale(calc(2%))' -> 'none'
filter: 'grayscale(calc(2% * var(--n)))' -> 'none'
filter: 'grayscale(calc(0.02))' -> 'none'
filter: 'grayscale(calc(0.02 * var(--n)))' -> 'none'
filter: 'grayscale(calc(2%))' -> 'grayscale(calc(2%))'
filter: 'grayscale(calc(2% * var(--n)))' -> 'grayscale(calc(2% * 2))'
filter: 'grayscale(calc(0.02))' -> 'grayscale(calc(0.02))'
filter: 'grayscale(calc(0.02 * var(--n)))' -> 'grayscale(calc(0.02 * 2))'
flex-basis: 'calc(2px)' -> 'calc(2px)'
flex-basis: 'calc(2px * var(--n))' -> 'calc(2px * 2)'
flex-grow: 'calc(2)' -> 'calc(2)'

View file

@ -2246,6 +2246,29 @@ Optional<NumberOrCalculated> Parser::parse_number(TokenStream<ComponentValue>& t
return {};
}
Optional<NumberPercentage> Parser::parse_number_percentage(TokenStream<ComponentValue>& tokens)
{
auto transaction = tokens.begin_transaction();
auto& token = tokens.consume_a_token();
if (token.is(Token::Type::Number)) {
transaction.commit();
return token.token().number();
}
if (token.is(Token::Type::Percentage)) {
transaction.commit();
return Percentage(token.token().percentage());
}
if (auto calc = parse_calculated_value(token); calc && calc->resolves_to_number_percentage()) {
transaction.commit();
return calc.release_nonnull();
}
return {};
}
Optional<ResolutionOrCalculated> Parser::parse_resolution(TokenStream<ComponentValue>& tokens)
{
auto transaction = tokens.begin_transaction();
@ -5395,14 +5418,6 @@ RefPtr<CSSStyleValue> Parser::parse_filter_value_list_value(TokenStream<Componen
return static_cast<FilterOperation::Color::Type>(filter);
};
auto parse_number_percentage = [&](auto& token) -> Optional<NumberPercentage> {
if (token.is(Token::Type::Percentage))
return NumberPercentage(Percentage(token.token().percentage()));
if (token.is(Token::Type::Number))
return NumberPercentage(Number(Number::Type::Number, token.token().number_value()));
return {};
};
auto parse_filter_function_name = [&](auto name) -> Optional<FilterToken> {
if (name.equals_ignoring_ascii_case("blur"sv))
return FilterToken::Blur;
@ -5446,8 +5461,7 @@ RefPtr<CSSStyleValue> Parser::parse_filter_value_list_value(TokenStream<Componen
tokens.discard_whitespace();
if (!blur_radius.has_value())
return {};
// FIXME: Support calculated radius
return if_no_more_tokens_return(FilterOperation::Blur { blur_radius->value() });
return if_no_more_tokens_return(FilterOperation::Blur { blur_radius.value() });
} else if (filter_token == FilterToken::DropShadow) {
if (!tokens.has_next_token())
return {};
@ -5481,29 +5495,24 @@ RefPtr<CSSStyleValue> Parser::parse_filter_value_list_value(TokenStream<Componen
if (maybe_color)
color = maybe_color->to_color({});
// FIXME: Support calculated offsets and radius
return if_no_more_tokens_return(FilterOperation::DropShadow { x_offset->value(), y_offset->value(), maybe_radius.map([](auto& it) { return it.value(); }), color });
return if_no_more_tokens_return(FilterOperation::DropShadow { x_offset.value(), y_offset.value(), maybe_radius, color });
} else if (filter_token == FilterToken::HueRotate) {
// hue-rotate( [ <angle> | <zero> ]? )
if (!tokens.has_next_token())
return FilterOperation::HueRotate {};
auto& token = tokens.consume_a_token();
if (token.is(Token::Type::Number)) {
if (tokens.next_token().is(Token::Type::Number)) {
// hue-rotate(0)
auto number = token.token().number();
auto number = tokens.consume_a_token().token().number();
if (number.is_integer() && number.integer_value() == 0)
return if_no_more_tokens_return(FilterOperation::HueRotate { FilterOperation::HueRotate::Zero {} });
return {};
}
if (!token.is(Token::Type::Dimension))
if (auto angle = parse_angle(tokens); angle.has_value())
return if_no_more_tokens_return(FilterOperation::HueRotate { angle.value() });
return {};
auto angle_value = token.token().dimension_value();
auto angle_unit_name = token.token().dimension_unit();
auto angle_unit = Angle::unit_from_name(angle_unit_name);
if (!angle_unit.has_value())
return {};
Angle angle { angle_value, angle_unit.release_value() };
return if_no_more_tokens_return(FilterOperation::HueRotate { angle });
} else {
// Simple filters:
// brightness( <number-percentage>? )
@ -5515,10 +5524,8 @@ RefPtr<CSSStyleValue> Parser::parse_filter_value_list_value(TokenStream<Componen
// saturate( <number-percentage>? )
if (!tokens.has_next_token())
return FilterOperation::Color { filter_token_to_operation(filter_token) };
auto amount = parse_number_percentage(tokens.consume_a_token());
if (!amount.has_value())
return {};
return if_no_more_tokens_return(FilterOperation::Color { filter_token_to_operation(filter_token), *amount });
auto amount = parse_number_percentage(tokens);
return if_no_more_tokens_return(FilterOperation::Color { filter_token_to_operation(filter_token), amount });
}
};

View file

@ -202,6 +202,7 @@ private:
Optional<LengthOrCalculated> parse_length(TokenStream<ComponentValue>&);
Optional<LengthPercentage> parse_length_percentage(TokenStream<ComponentValue>&);
Optional<NumberOrCalculated> parse_number(TokenStream<ComponentValue>&);
Optional<NumberPercentage> parse_number_percentage(TokenStream<ComponentValue>&);
Optional<ResolutionOrCalculated> parse_resolution(TokenStream<ComponentValue>&);
Optional<TimeOrCalculated> parse_time(TokenStream<ComponentValue>&);
Optional<TimePercentage> parse_time_percentage(TokenStream<ComponentValue>&);

View file

@ -15,30 +15,42 @@ namespace Web::CSS {
float FilterOperation::Blur::resolved_radius(Layout::Node const& node) const
{
// Default value when omitted is 0px.
auto sigma = 0;
if (radius.has_value())
sigma = radius->to_px(node).to_int();
return sigma;
return radius->resolved(Length::ResolutionContext::for_layout_node(node)).to_px(node).to_float();
// Default value when omitted is 0px.
return 0;
}
float FilterOperation::HueRotate::angle_degrees() const
float FilterOperation::HueRotate::angle_degrees(Layout::Node const& node) const
{
// Default value when omitted is 0deg.
if (!angle.has_value())
return 0.0f;
return angle->visit([&](Angle const& a) { return a.to_degrees(); }, [&](auto) { return 0.0; });
return angle->visit([&](AngleOrCalculated const& a) { return a.resolved(node).to_degrees(); }, [&](Zero) { return 0.0; });
}
float FilterOperation::Color::resolved_amount() const
{
if (amount.has_value()) {
// Default value when omitted is 1.
if (!amount.has_value())
return 1;
if (amount->is_number())
return amount->number().value();
if (amount->is_percentage())
return amount->percentage().as_fraction();
return amount->number().value();
if (amount->is_calculated()) {
if (amount->calculated()->resolves_to_number())
return amount->calculated()->resolve_number().value();
if (amount->calculated()->resolves_to_percentage())
return amount->calculated()->resolve_percentage()->as_fraction();
}
// All color filters (brightness, sepia, etc) have a default amount of 1.
return 1.0f;
VERIFY_NOT_REACHED();
}
String FilterValueListStyleValue::to_string() const

View file

@ -10,6 +10,7 @@
#pragma once
#include <LibWeb/CSS/Angle.h>
#include <LibWeb/CSS/CalculatedOr.h>
#include <LibWeb/CSS/Length.h>
#include <LibWeb/CSS/Number.h>
#include <LibWeb/CSS/PercentageOr.h>
@ -19,16 +20,16 @@ namespace Web::CSS {
namespace FilterOperation {
struct Blur {
Optional<Length> radius {};
Optional<LengthOrCalculated> radius;
float resolved_radius(Layout::Node const&) const;
bool operator==(Blur const&) const = default;
};
struct DropShadow {
Length offset_x;
Length offset_y;
Optional<Length> radius {};
Optional<Color> color {};
LengthOrCalculated offset_x;
LengthOrCalculated offset_y;
Optional<LengthOrCalculated> radius;
Optional<Color> color;
bool operator==(DropShadow const&) const = default;
};
@ -36,9 +37,9 @@ struct HueRotate {
struct Zero {
bool operator==(Zero const&) const = default;
};
using AngleOrZero = Variant<Angle, Zero>;
Optional<AngleOrZero> angle {};
float angle_degrees() const;
using AngleOrZero = Variant<AngleOrCalculated, Zero>;
Optional<AngleOrZero> angle;
float angle_degrees(Layout::Node const&) const;
bool operator==(HueRotate const&) const = default;
};

View file

@ -523,12 +523,13 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style)
.radius = blur.resolved_radius(*this) });
},
[&](CSS::FilterOperation::DropShadow const& drop_shadow) {
auto context = CSS::Length::ResolutionContext::for_layout_node(*this);
// The default value for omitted values is missing length values set to 0
// and the missing used color is taken from the color property.
resolved_filter.filters.append(CSS::ResolvedFilter::DropShadow {
.offset_x = drop_shadow.offset_x.to_px(*this).to_double(),
.offset_y = drop_shadow.offset_y.to_px(*this).to_double(),
.radius = drop_shadow.radius.has_value() ? drop_shadow.radius->to_px(*this).to_double() : 0.0,
.offset_x = drop_shadow.offset_x.resolved(context).to_px(*this).to_double(),
.offset_y = drop_shadow.offset_y.resolved(context).to_px(*this).to_double(),
.radius = drop_shadow.radius.has_value() ? drop_shadow.radius->resolved(context).to_px(*this).to_double() : 0.0,
.color = drop_shadow.color.has_value() ? *drop_shadow.color : this->computed_values().color() });
},
[&](CSS::FilterOperation::Color const& color_operation) {
@ -537,7 +538,7 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style)
.amount = color_operation.resolved_amount() });
},
[&](CSS::FilterOperation::HueRotate const& hue_rotate) {
resolved_filter.filters.append(CSS::ResolvedFilter::HueRotate { .angle_degrees = hue_rotate.angle_degrees() });
resolved_filter.filters.append(CSS::ResolvedFilter::HueRotate { .angle_degrees = hue_rotate.angle_degrees(*this) });
});
}
return resolved_filter;