From 1b9c50b66445c5816f23f78377976c1ee56415ba Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Fri, 25 Oct 2024 15:37:21 +0200 Subject: [PATCH] LibWeb: Implement CSS filter painting We can reuse our implementation for `backdrop-filter` and use it as a painting filter for each stacking context. --- Userland/Libraries/LibWeb/Layout/Node.cpp | 8 +- Userland/Libraries/LibWeb/Painting/Command.h | 1 + .../LibWeb/Painting/DisplayListPlayerSkia.cpp | 279 ++++++++++-------- .../LibWeb/Painting/DisplayListRecorder.cpp | 1 + .../LibWeb/Painting/DisplayListRecorder.h | 1 + .../LibWeb/Painting/StackingContext.cpp | 1 + 6 files changed, 162 insertions(+), 129 deletions(-) diff --git a/Userland/Libraries/LibWeb/Layout/Node.cpp b/Userland/Libraries/LibWeb/Layout/Node.cpp index 173e89c6281..2db2c7f4394 100644 --- a/Userland/Libraries/LibWeb/Layout/Node.cpp +++ b/Userland/Libraries/LibWeb/Layout/Node.cpp @@ -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: diff --git a/Userland/Libraries/LibWeb/Painting/Command.h b/Userland/Libraries/LibWeb/Painting/Command.h index 08f6233f31f..ac61071246a 100644 --- a/Userland/Libraries/LibWeb/Painting/Command.h +++ b/Userland/Libraries/LibWeb/Painting/Command.h @@ -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. diff --git a/Userland/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp b/Userland/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp index d456e5758fe..d6bdd3ce578 100644 --- a/Userland/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp +++ b/Userland/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp @@ -235,6 +235,128 @@ static SkColor4f to_skia_color4f(Gfx::Color const& color) }; } +static sk_sp 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 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 {}; + }); +} + static SkPath to_skia_path(Gfx::Path const& path) { return static_cast(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 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 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(); } } diff --git a/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp b/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp index 5958dd6f8c1..fe5bfc7db0b 100644 --- a/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp +++ b/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.cpp @@ -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, diff --git a/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.h b/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.h index 6cddbf4a6e0..1ee81221235 100644 --- a/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.h +++ b/Userland/Libraries/LibWeb/Painting/DisplayListRecorder.h @@ -111,6 +111,7 @@ public: struct PushStackingContextParams { float opacity; + CSS::ResolvedFilter filter; bool is_fixed_position; Gfx::IntRect source_paintable_rect; StackingContextTransform transform; diff --git a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp index 3e54b242660..1ea02c519cc 100644 --- a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp @@ -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 = {