LibWeb: Implement CSS filter painting

We can reuse our implementation for `backdrop-filter` and use it as a
painting filter for each stacking context.
This commit is contained in:
Jelle Raaijmakers 2024-10-25 15:37:21 +02:00 committed by Andreas Kling
parent 29974de852
commit 1b9c50b664
Notes: github-actions[bot] 2024-10-26 09:28:48 +00:00
6 changed files with 162 additions and 129 deletions

View file

@ -183,11 +183,13 @@ bool Node::establishes_stacking_context() const
if (parent() && parent()->display().is_grid_inside() && computed_values().z_index().has_value())
return true;
// https://drafts.fxtf.org/filter-effects/#FilterProperty
// https://drafts.fxtf.org/filter-effects-2/#backdrop-filter-operation
// A computed value of other than none results in the creation of both a stacking context [CSS21] and a Containing Block for absolute and fixed position descendants,
// unless the element it applies to is a document root element in the current browsing context.
// A computed value of other than none results in the creation of both a stacking context
// [CSS21] and a Containing Block for absolute and fixed position descendants, unless the
// element it applies to is a document root element in the current browsing context.
// Spec Note: This rule works in the same way as for the filter property.
if (!computed_values().backdrop_filter().is_none())
if (!computed_values().backdrop_filter().is_none() || !computed_values().filter().is_none())
return true;
// Element with any of the following properties with value other than none:

View file

@ -113,6 +113,7 @@ struct StackingContextTransform {
struct PushStackingContext {
float opacity;
CSS::ResolvedFilter filter;
// The bounding box of the source paintable (pre-transform).
Gfx::IntRect source_paintable_rect;
// A translation to be applied after the stacking context has been transformed.

View file

@ -235,6 +235,128 @@ static SkColor4f to_skia_color4f(Gfx::Color const& color)
};
}
static sk_sp<SkImageFilter> to_skia_image_filter(CSS::ResolvedFilter::FilterFunction const& function)
{
// See: https://drafts.fxtf.org/filter-effects-1/#supported-filter-functions
return function.visit(
[&](CSS::ResolvedFilter::Blur const& blur_filter) {
return SkImageFilters::Blur(blur_filter.radius, blur_filter.radius, nullptr);
},
[&](CSS::ResolvedFilter::Color const& color) {
auto amount = clamp(color.amount, 0.0f, 1.0f);
// Matrices are taken from https://drafts.fxtf.org/filter-effects-1/#FilterPrimitiveRepresentation
sk_sp<SkColorFilter> color_filter;
switch (color.type) {
case CSS::FilterOperation::Color::Type::Grayscale: {
float matrix[20] = {
0.2126f + 0.7874f * (1 - amount), 0.7152f - 0.7152f * (1 - amount), 0.0722f - 0.0722f * (1 - amount), 0, 0,
0.2126f - 0.2126f * (1 - amount), 0.7152f + 0.2848f * (1 - amount), 0.0722f - 0.0722f * (1 - amount), 0, 0,
0.2126f - 0.2126f * (1 - amount), 0.7152f - 0.7152f * (1 - amount), 0.0722f + 0.9278f * (1 - amount), 0, 0,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Brightness: {
float matrix[20] = {
amount, 0, 0, 0, 0,
0, amount, 0, 0, 0,
0, 0, amount, 0, 0,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Contrast: {
float intercept = -(0.5f * amount) + 0.5f;
float matrix[20] = {
amount, 0, 0, 0, intercept,
0, amount, 0, 0, intercept,
0, 0, amount, 0, intercept,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Invert: {
float matrix[20] = {
1 - 2 * amount, 0, 0, 0, amount,
0, 1 - 2 * amount, 0, 0, amount,
0, 0, 1 - 2 * amount, 0, amount,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Opacity: {
float matrix[20] = {
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, amount, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Sepia: {
float matrix[20] = {
0.393f + 0.607f * (1 - amount), 0.769f - 0.769f * (1 - amount), 0.189f - 0.189f * (1 - amount), 0, 0,
0.349f - 0.349f * (1 - amount), 0.686f + 0.314f * (1 - amount), 0.168f - 0.168f * (1 - amount), 0, 0,
0.272f - 0.272f * (1 - amount), 0.534f - 0.534f * (1 - amount), 0.131f + 0.869f * (1 - amount), 0, 0,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Saturate: {
float matrix[20] = {
0.213f + 0.787f * amount, 0.715f - 0.715f * amount, 0.072f - 0.072f * amount, 0, 0,
0.213f - 0.213f * amount, 0.715f + 0.285f * amount, 0.072f - 0.072f * amount, 0, 0,
0.213f - 0.213f * amount, 0.715f - 0.715f * amount, 0.072f + 0.928f * amount, 0, 0,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
default:
VERIFY_NOT_REACHED();
}
return SkImageFilters::ColorFilter(color_filter, nullptr);
},
[&](CSS::ResolvedFilter::HueRotate const& hue_rotate) {
float radians = AK::to_radians(hue_rotate.angle_degrees);
auto cosA = cos(radians);
auto sinA = sin(radians);
auto a00 = 0.213f + cosA * 0.787f - sinA * 0.213f;
auto a01 = 0.715f - cosA * 0.715f - sinA * 0.715f;
auto a02 = 0.072f - cosA * 0.072f + sinA * 0.928f;
auto a10 = 0.213f - cosA * 0.213f + sinA * 0.143f;
auto a11 = 0.715f + cosA * 0.285f + sinA * 0.140f;
auto a12 = 0.072f - cosA * 0.072f - sinA * 0.283f;
auto a20 = 0.213f - cosA * 0.213f - sinA * 0.787f;
auto a21 = 0.715f - cosA * 0.715f + sinA * 0.715f;
auto a22 = 0.072f + cosA * 0.928f + sinA * 0.072f;
float matrix[20] = {
a00, a01, a02, 0, 0,
a10, a11, a12, 0, 0,
a20, a21, a22, 0, 0,
0, 0, 0, 1, 0
};
auto color_filter = SkColorFilters::Matrix(matrix);
return SkImageFilters::ColorFilter(color_filter, nullptr);
},
[&](CSS::ResolvedFilter::DropShadow const&) {
dbgln("TODO: Implement drop-shadow() filter function!");
return sk_sp<SkImageFilter> {};
});
}
static SkPath to_skia_path(Gfx::Path const& path)
{
return static_cast<Gfx::PathImplSkia const&>(path.impl()).sk_path();
@ -444,7 +566,33 @@ void DisplayListPlayerSkia::push_stacking_context(PushStackingContext const& com
.translate(-command.transform.origin);
auto matrix = to_skia_matrix(new_transform);
if (command.opacity < 1) {
if (!command.filter.is_none()) {
sk_sp<SkImageFilter> image_filter;
auto append_filter = [&image_filter](auto new_filter) {
if (image_filter)
image_filter = SkImageFilters::Compose(new_filter, image_filter);
else
image_filter = new_filter;
};
// Apply filters in order
for (auto const& filter_function : command.filter.filters)
append_filter(to_skia_image_filter(filter_function));
// We apply opacity as a color filter here so we only need to save and restore a single layer.
if (command.opacity < 1) {
append_filter(to_skia_image_filter(CSS::ResolvedFilter::FilterFunction {
CSS::ResolvedFilter::Color {
CSS::FilterOperation::Color::Type::Opacity,
command.opacity,
},
}));
}
SkPaint paint;
paint.setImageFilter(image_filter);
canvas.saveLayer(nullptr, &paint);
} else if (command.opacity < 1) {
auto source_paintable_rect = to_skia_rect(command.source_paintable_rect);
SkRect dest;
matrix.mapRect(&dest, source_paintable_rect);
@ -453,9 +601,8 @@ void DisplayListPlayerSkia::push_stacking_context(PushStackingContext const& com
canvas.save();
}
if (command.clip_path.has_value()) {
if (command.clip_path.has_value())
canvas.clipPath(to_skia_path(command.clip_path.value()), true);
}
canvas.concat(matrix);
}
@ -962,129 +1109,9 @@ void DisplayListPlayerSkia::apply_backdrop_filter(ApplyBackdropFilter const& com
ScopeGuard guard = [&] { canvas.restore(); };
for (auto const& filter_function : command.backdrop_filter.filters) {
// See: https://drafts.fxtf.org/filter-effects-1/#supported-filter-functions
filter_function.visit(
[&](CSS::ResolvedFilter::Blur const& blur_filter) {
auto blur_image_filter = SkImageFilters::Blur(blur_filter.radius, blur_filter.radius, nullptr);
canvas.saveLayer(SkCanvas::SaveLayerRec(nullptr, nullptr, blur_image_filter.get(), 0));
canvas.restore();
},
[&](CSS::ResolvedFilter::Color const& color) {
auto amount = clamp(color.amount, 0.0f, 1.0f);
// Matrices are taken from https://drafts.fxtf.org/filter-effects-1/#FilterPrimitiveRepresentation
sk_sp<SkColorFilter> color_filter;
switch (color.type) {
case CSS::FilterOperation::Color::Type::Grayscale: {
float matrix[20] = {
0.2126f + 0.7874f * (1 - amount), 0.7152f - 0.7152f * (1 - amount), 0.0722f - 0.0722f * (1 - amount), 0, 0,
0.2126f - 0.2126f * (1 - amount), 0.7152f + 0.2848f * (1 - amount), 0.0722f - 0.0722f * (1 - amount), 0, 0,
0.2126f - 0.2126f * (1 - amount), 0.7152f - 0.7152f * (1 - amount), 0.0722f + 0.9278f * (1 - amount), 0, 0,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Brightness: {
float matrix[20] = {
amount, 0, 0, 0, 0,
0, amount, 0, 0, 0,
0, 0, amount, 0, 0,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Contrast: {
float intercept = -(0.5f * amount) + 0.5f;
float matrix[20] = {
amount, 0, 0, 0, intercept,
0, amount, 0, 0, intercept,
0, 0, amount, 0, intercept,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Invert: {
float matrix[20] = {
1 - 2 * amount, 0, 0, 0, amount,
0, 1 - 2 * amount, 0, 0, amount,
0, 0, 1 - 2 * amount, 0, amount,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Opacity: {
float matrix[20] = {
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, amount, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Sepia: {
float matrix[20] = {
0.393f + 0.607f * (1 - amount), 0.769f - 0.769f * (1 - amount), 0.189f - 0.189f * (1 - amount), 0, 0,
0.349f - 0.349f * (1 - amount), 0.686f + 0.314f * (1 - amount), 0.168f - 0.168f * (1 - amount), 0, 0,
0.272f - 0.272f * (1 - amount), 0.534f - 0.534f * (1 - amount), 0.131f + 0.869f * (1 - amount), 0, 0,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
case CSS::FilterOperation::Color::Type::Saturate: {
float matrix[20] = {
0.213f + 0.787f * amount, 0.715f - 0.715f * amount, 0.072f - 0.072f * amount, 0, 0,
0.213f - 0.213f * amount, 0.715f + 0.285f * amount, 0.072f - 0.072f * amount, 0, 0,
0.213f - 0.213f * amount, 0.715f - 0.715f * amount, 0.072f + 0.928f * amount, 0, 0,
0, 0, 0, 1, 0
};
color_filter = SkColorFilters::Matrix(matrix);
break;
}
default:
VERIFY_NOT_REACHED();
}
auto image_filter = SkImageFilters::ColorFilter(color_filter, nullptr);
canvas.saveLayer(SkCanvas::SaveLayerRec(nullptr, nullptr, image_filter.get(), 0));
canvas.restore();
},
[&](CSS::ResolvedFilter::HueRotate const& hue_rotate) {
float radians = AK::to_radians(hue_rotate.angle_degrees);
auto cosA = cos(radians);
auto sinA = sin(radians);
auto a00 = 0.213f + cosA * 0.787f - sinA * 0.213f;
auto a01 = 0.715f - cosA * 0.715f - sinA * 0.715f;
auto a02 = 0.072f - cosA * 0.072f + sinA * 0.928f;
auto a10 = 0.213f - cosA * 0.213f + sinA * 0.143f;
auto a11 = 0.715f + cosA * 0.285f + sinA * 0.140f;
auto a12 = 0.072f - cosA * 0.072f - sinA * 0.283f;
auto a20 = 0.213f - cosA * 0.213f - sinA * 0.787f;
auto a21 = 0.715f - cosA * 0.715f + sinA * 0.715f;
auto a22 = 0.072f + cosA * 0.928f + sinA * 0.072f;
float matrix[20] = {
a00, a01, a02, 0, 0,
a10, a11, a12, 0, 0,
a20, a21, a22, 0, 0,
0, 0, 0, 1, 0
};
auto color_filter = SkColorFilters::Matrix(matrix);
auto image_filter = SkImageFilters::ColorFilter(color_filter, nullptr);
canvas.saveLayer(SkCanvas::SaveLayerRec(nullptr, nullptr, image_filter.get(), 0));
canvas.restore();
},
[&](CSS::ResolvedFilter::DropShadow const&) {
dbgln("TODO: Implement drop-shadow() filter function!");
});
auto image_filter = to_skia_image_filter(filter_function);
canvas.saveLayer(SkCanvas::SaveLayerRec(nullptr, nullptr, image_filter.get(), 0));
canvas.restore();
}
}

View file

@ -284,6 +284,7 @@ void DisplayListRecorder::push_stacking_context(PushStackingContextParams params
{
append(PushStackingContext {
.opacity = params.opacity,
.filter = params.filter,
.source_paintable_rect = params.source_paintable_rect,
.transform = {
.origin = params.transform.origin,

View file

@ -111,6 +111,7 @@ public:
struct PushStackingContextParams {
float opacity;
CSS::ResolvedFilter filter;
bool is_fixed_position;
Gfx::IntRect source_paintable_rect;
StackingContextTransform transform;

View file

@ -311,6 +311,7 @@ void StackingContext::paint(PaintContext& context) const
DisplayListRecorder::PushStackingContextParams push_stacking_context_params {
.opacity = opacity,
.filter = paintable().computed_values().filter(),
.is_fixed_position = paintable().is_fixed_position(),
.source_paintable_rect = source_paintable_rect,
.transform = {