diff --git a/Libraries/LibGfx/ImageFormats/PNGLoader.cpp b/Libraries/LibGfx/ImageFormats/PNGLoader.cpp index ae581d4dd00..a1a48f24308 100644 --- a/Libraries/LibGfx/ImageFormats/PNGLoader.cpp +++ b/Libraries/LibGfx/ImageFormats/PNGLoader.cpp @@ -257,11 +257,11 @@ ErrorOr PNGLoadingContext::read_frames(png_structp png_ptr, png_infop in case PNG_BLEND_OP_SOURCE: // All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. painter->clear_rect(frame_rect, Gfx::Color::Transparent); - painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*decoded_frame_bitmap), decoded_frame_bitmap->rect(), Gfx::ScalingMode::NearestNeighbor, 1.0f); + painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*decoded_frame_bitmap), decoded_frame_bitmap->rect(), Gfx::ScalingMode::NearestNeighbor, {}, 1.0f); break; case PNG_BLEND_OP_OVER: // The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification. - painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*decoded_frame_bitmap), decoded_frame_bitmap->rect(), ScalingMode::NearestNeighbor, 1.0f); + painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*decoded_frame_bitmap), decoded_frame_bitmap->rect(), ScalingMode::NearestNeighbor, {}, 1.0f); break; default: VERIFY_NOT_REACHED(); @@ -280,7 +280,7 @@ ErrorOr PNGLoadingContext::read_frames(png_structp png_ptr, png_infop in case PNG_DISPOSE_OP_PREVIOUS: // The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. painter->clear_rect(frame_rect, Gfx::Color::Transparent); - painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*prev_output_buffer), IntRect { x, y, width, height }, Gfx::ScalingMode::NearestNeighbor, 1.0f); + painter->draw_bitmap(frame_rect, Gfx::ImmutableBitmap::create(*prev_output_buffer), IntRect { x, y, width, height }, Gfx::ScalingMode::NearestNeighbor, {}, 1.0f); break; default: VERIFY_NOT_REACHED(); diff --git a/Libraries/LibGfx/ImageFormats/TinyVGLoader.cpp b/Libraries/LibGfx/ImageFormats/TinyVGLoader.cpp index 0abdeb8756b..b05aaf4bd70 100644 --- a/Libraries/LibGfx/ImageFormats/TinyVGLoader.cpp +++ b/Libraries/LibGfx/ImageFormats/TinyVGLoader.cpp @@ -481,7 +481,7 @@ void TinyVGDecodedImageData::draw(Painter& painter) const command.fill->visit( [&](Color color) { painter.fill_path(fill_path, color, WindingRule::EvenOdd); }, [&](NonnullRefPtr const& style) { - painter.fill_path(fill_path, style, 1.0f, WindingRule::EvenOdd); + painter.fill_path(fill_path, style, {}, 1.0f, WindingRule::EvenOdd); }); } @@ -489,7 +489,7 @@ void TinyVGDecodedImageData::draw(Painter& painter) const command.stroke->visit( [&](Color color) { painter.stroke_path(draw_path, color, command.stroke_width); }, [&](NonnullRefPtr const& style) { - painter.stroke_path(draw_path, style, command.stroke_width, 1.0f); + painter.stroke_path(draw_path, style, {}, command.stroke_width, 1.0f); }); } } diff --git a/Libraries/LibGfx/Painter.h b/Libraries/LibGfx/Painter.h index 0be56d07bd6..1df515c95c2 100644 --- a/Libraries/LibGfx/Painter.h +++ b/Libraries/LibGfx/Painter.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -23,15 +24,15 @@ public: virtual void clear_rect(Gfx::FloatRect const&, Gfx::Color) = 0; virtual void fill_rect(Gfx::FloatRect const&, Gfx::Color) = 0; - virtual void draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode, float global_alpha) = 0; + virtual void draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode, ReadonlySpan filters, float global_alpha) = 0; virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness) = 0; virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness, float blur_radius) = 0; - virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, float thickness, float global_alpha) = 0; + virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan, float thickness, float global_alpha) = 0; virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule) = 0; virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule, float blur_radius) = 0; - virtual void fill_path(Gfx::Path const&, Gfx::PaintStyle const&, float global_alpha, Gfx::WindingRule) = 0; + virtual void fill_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan, float global_alpha, Gfx::WindingRule) = 0; virtual void set_transform(Gfx::AffineTransform const&) = 0; diff --git a/Libraries/LibGfx/PainterSkia.cpp b/Libraries/LibGfx/PainterSkia.cpp index 67c05266eea..0cea40aec5c 100644 --- a/Libraries/LibGfx/PainterSkia.cpp +++ b/Libraries/LibGfx/PainterSkia.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2024, Andreas Kling * Copyright (c) 2024, Aliaksandr Kalenik + * Copyright (c) 2024, Lucien Fiorini * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,6 +9,7 @@ #define AK_DONT_REPLACE_STD #include +#include #include #include #include @@ -128,9 +130,10 @@ void PainterSkia::fill_rect(Gfx::FloatRect const& rect, Color color) impl().canvas()->drawRect(to_skia_rect(rect), paint); } -void PainterSkia::draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode scaling_mode, float global_alpha) +void PainterSkia::draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode scaling_mode, ReadonlySpan filters, float global_alpha) { SkPaint paint; + apply_filters(paint, filters); paint.setAlpha(static_cast(global_alpha * 255)); impl().canvas()->drawImageRect( @@ -183,14 +186,14 @@ void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::Color color, float thi impl().canvas()->drawPath(sk_path, paint); } -void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, float thickness, float global_alpha) +void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, ReadonlySpan filters, float thickness, float global_alpha) { // Skia treats zero thickness as a special case and will draw a hairline, while we want to draw nothing. if (thickness <= 0) return; auto sk_path = to_skia_path(path); - auto paint = to_skia_paint(paint_style, {}); + auto paint = to_skia_paint(paint_style, filters); paint.setAntiAlias(true); paint.setAlphaf(global_alpha); paint.setStyle(SkPaint::Style::kStroke_Style); @@ -219,11 +222,11 @@ void PainterSkia::fill_path(Gfx::Path const& path, Gfx::Color color, Gfx::Windin impl().canvas()->drawPath(sk_path, paint); } -void PainterSkia::fill_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, float global_alpha, Gfx::WindingRule winding_rule) +void PainterSkia::fill_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, ReadonlySpan filters, float global_alpha, Gfx::WindingRule winding_rule) { auto sk_path = to_skia_path(path); sk_path.setFillType(to_skia_path_fill_type(winding_rule)); - auto paint = to_skia_paint(paint_style, {}); + auto paint = to_skia_paint(paint_style, filters); paint.setAntiAlias(true); paint.setAlphaf(global_alpha); impl().canvas()->drawPath(sk_path, paint); diff --git a/Libraries/LibGfx/PainterSkia.h b/Libraries/LibGfx/PainterSkia.h index d52b80619a3..9ac3d4f52af 100644 --- a/Libraries/LibGfx/PainterSkia.h +++ b/Libraries/LibGfx/PainterSkia.h @@ -20,13 +20,13 @@ public: virtual void clear_rect(Gfx::FloatRect const&, Color) override; virtual void fill_rect(Gfx::FloatRect const&, Color) override; - virtual void draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode, float global_alpha) override; + virtual void draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::ImmutableBitmap const& src_bitmap, Gfx::IntRect const& src_rect, Gfx::ScalingMode, ReadonlySpan, float global_alpha) override; virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness) override; virtual void stroke_path(Gfx::Path const&, Gfx::Color, float thickness, float blur_radius) override; - virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, float thickness, float global_alpha) override; + virtual void stroke_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan, float thickness, float global_alpha) override; virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule) override; virtual void fill_path(Gfx::Path const&, Gfx::Color, Gfx::WindingRule, float blur_radius) override; - virtual void fill_path(Gfx::Path const&, Gfx::PaintStyle const&, float global_alpha, Gfx::WindingRule) override; + virtual void fill_path(Gfx::Path const&, Gfx::PaintStyle const&, ReadonlySpan, float global_alpha, Gfx::WindingRule) override; virtual void set_transform(Gfx::AffineTransform const&) override; virtual void save() override; virtual void restore() override; diff --git a/Libraries/LibWeb/HTML/Canvas/CanvasFilters.h b/Libraries/LibWeb/HTML/Canvas/CanvasFilters.h new file mode 100644 index 00000000000..9c104387e91 --- /dev/null +++ b/Libraries/LibWeb/HTML/Canvas/CanvasFilters.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024, Lucien Fiorini + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::HTML { + +// https://html.spec.whatwg.org/multipage/canvas.html#canvasfilters +class CanvasFilters { +public: + ~CanvasFilters() = default; + + virtual String filter() const = 0; + virtual void set_filter(String filter) = 0; + +protected: + CanvasFilters() = default; +}; + +} diff --git a/Libraries/LibWeb/HTML/Canvas/CanvasFilters.idl b/Libraries/LibWeb/HTML/Canvas/CanvasFilters.idl index 389b8aef7e4..06f250694d5 100644 --- a/Libraries/LibWeb/HTML/Canvas/CanvasFilters.idl +++ b/Libraries/LibWeb/HTML/Canvas/CanvasFilters.idl @@ -1,5 +1,5 @@ // https://html.spec.whatwg.org/multipage/canvas.html#canvasfilters interface mixin CanvasFilters { // filters - [FIXME] attribute DOMString filter; // (default "none") + attribute DOMString filter; // (default "none") }; diff --git a/Libraries/LibWeb/HTML/Canvas/CanvasState.h b/Libraries/LibWeb/HTML/Canvas/CanvasState.h index df3d05cc20a..9c4b65b2665 100644 --- a/Libraries/LibWeb/HTML/Canvas/CanvasState.h +++ b/Libraries/LibWeb/HTML/Canvas/CanvasState.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -84,6 +85,8 @@ public: float shadow_offset_y { 0.0f }; float shadow_blur { 0.0f }; Gfx::Color shadow_color { Gfx::Color::Transparent }; + Vector filters; + Optional filters_string; float line_width { 1 }; Bindings::CanvasLineCap line_cap { Bindings::CanvasLineCap::Butt }; Bindings::CanvasLineJoin line_join { Bindings::CanvasLineJoin::Miter }; diff --git a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp index e38fa94b0c5..575f9ae2395 100644 --- a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp +++ b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp @@ -3,6 +3,7 @@ * Copyright (c) 2021-2022, Linus Groh * Copyright (c) 2023, MacDue * Copyright (c) 2024, Aliaksandr Kalenik + * Copyright (c) 2024, Lucien Fiorini * * SPDX-License-Identifier: BSD-2-Clause */ @@ -177,7 +178,7 @@ WebIDL::ExceptionOr CanvasRenderingContext2D::draw_image_internal(CanvasIm } if (auto* painter = this->painter()) { - painter->draw_bitmap(destination_rect, *bitmap, source_rect.to_rounded(), scaling_mode, drawing_state().global_alpha); + painter->draw_bitmap(destination_rect, *bitmap, source_rect.to_rounded(), scaling_mode, drawing_state().filters, drawing_state().global_alpha); did_draw(destination_rect); } @@ -301,12 +302,7 @@ void CanvasRenderingContext2D::stroke_internal(Gfx::Path const& path) auto& state = drawing_state(); // FIXME: Honor state's line_cap, line_join, miter_limit, dash_list, and line_dash_offset. - - if (auto color = state.stroke_style.as_color(); color.has_value()) { - painter->stroke_path(path, color->with_opacity(state.global_alpha), state.line_width); - } else { - painter->stroke_path(path, state.stroke_style.to_gfx_paint_style(), state.line_width, state.global_alpha); - } + painter->stroke_path(path, state.stroke_style.to_gfx_paint_style(), state.filters, state.line_width, state.global_alpha); did_draw(path.bounding_box()); } @@ -342,11 +338,7 @@ void CanvasRenderingContext2D::fill_internal(Gfx::Path const& path, Gfx::Winding auto path_to_fill = path; path_to_fill.close_all_subpaths(); auto& state = this->drawing_state(); - if (auto color = state.fill_style.as_color(); color.has_value()) { - painter->fill_path(path_to_fill, color->with_opacity(state.global_alpha), winding_rule); - } else { - painter->fill_path(path_to_fill, state.fill_style.to_gfx_paint_style(), state.global_alpha, winding_rule); - } + painter->fill_path(path_to_fill, state.fill_style.to_gfx_paint_style(), state.filters, state.global_alpha, winding_rule); did_draw(path_to_fill.bounding_box()); } @@ -438,7 +430,7 @@ WebIDL::ExceptionOr> CanvasRenderingContext2D::get_image_data ASSERT(image_data->bitmap().alpha_type() == Gfx::AlphaType::Unpremultiplied); auto painter = Gfx::Painter::create(image_data->bitmap()); - painter->draw_bitmap(image_data->bitmap().rect().to_type(), *snapshot, source_rect_intersected, Gfx::ScalingMode::NearestNeighbor, drawing_state().global_alpha); + painter->draw_bitmap(image_data->bitmap().rect().to_type(), *snapshot, source_rect_intersected, Gfx::ScalingMode::NearestNeighbor, {}, drawing_state().global_alpha); // 7. Set the pixels values of imageData for areas of the source rectangle that are outside of the output bitmap to transparent black. // NOTE: No-op, already done during creation. @@ -451,7 +443,7 @@ void CanvasRenderingContext2D::put_image_data(ImageData const& image_data, float { if (auto* painter = this->painter()) { auto dst_rect = Gfx::FloatRect(x, y, image_data.width(), image_data.height()); - painter->draw_bitmap(dst_rect, Gfx::ImmutableBitmap::create(image_data.bitmap()), image_data.bitmap().rect(), Gfx::ScalingMode::NearestNeighbor, 1.0f); + painter->draw_bitmap(dst_rect, Gfx::ImmutableBitmap::create(image_data.bitmap()), image_data.bitmap().rect(), Gfx::ScalingMode::NearestNeighbor, drawing_state().filters, 1.0f); did_draw(dst_rect); } } @@ -862,4 +854,80 @@ void CanvasRenderingContext2D::paint_shadow_for_stroke_internal(Gfx::Path const& did_draw(path.bounding_box()); } +String CanvasRenderingContext2D::filter() const +{ + if (!drawing_state().filters_string.has_value()) { + return String::from_utf8_without_validation("none"sv.bytes()); + } + + return drawing_state().filters_string.value(); +} + +// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-filter +void CanvasRenderingContext2D::set_filter(String filter) +{ + drawing_state().filters.clear(); + + // 1. If the given value is "none", then set this's current filter to "none" and return. + if (filter == "none"sv) { + drawing_state().filters_string.clear(); + return; + } + + auto& realm = static_cast(*this).realm(); + auto parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), filter); + + // 2. Let parsedValue be the result of parsing the given values as a . + // If any property-independent style sheet syntax like 'inherit' or 'initial' is present, + // then this parsing must return failure. + auto style_value = parser.parse_as_css_value(CSS::PropertyID::Filter); + + if (style_value && style_value->is_filter_value_list()) { + auto filter_value_list = style_value->as_filter_value_list().filter_value_list(); + + drawing_state().filters.grow_capacity(filter_value_list.size()); + + // Note: The layout must be updated to make sure the canvas's layout node isn't null. + canvas_element().document().update_layout(); + auto layout_node = canvas_element().layout_node(); + + // 4. Set this's current filter to the given value. + for (auto& item : filter_value_list) { + // FIXME: Add support for SVG filters when they get implement by the CSS parser. + item.visit( + [&](CSS::FilterOperation::Blur const& blur_filter) { + float radius = blur_filter.resolved_radius(*layout_node); + drawing_state().filters.append(Gfx::BlurFilter { radius }); + }, + [&](CSS::FilterOperation::Color const& color) { + float amount = color.resolved_amount(); + drawing_state().filters.append(Gfx::ColorFilter { color.operation, amount }); + }, + [&](CSS::FilterOperation::HueRotate const& hue_rotate) { + float angle = hue_rotate.angle_degrees(*layout_node); + drawing_state().filters.append(Gfx::HueRotateFilter { angle }); + }, + [&](CSS::FilterOperation::DropShadow const& drop_shadow) { + auto resolution_context = CSS::Length::ResolutionContext::for_layout_node(*layout_node); + + float offset_x = static_cast(drop_shadow.offset_x.resolved(resolution_context).raw_value()); + float offset_y = static_cast(drop_shadow.offset_y.resolved(resolution_context).raw_value()); + + float radius = 0.0f; + if (drop_shadow.radius.has_value()) { + radius = drop_shadow.radius->resolved(resolution_context).raw_value(); + }; + + auto color = drop_shadow.color.value_or(Gfx::Color { 0, 0, 0, 255 }); + + drawing_state().filters.append(Gfx::DropShadowFilter { offset_x, offset_y, radius, color }); + }); + } + + drawing_state().filters_string = move(filter); + } + + // 3. If parsedValue is failure, then return. +} + } diff --git a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h index a69fce27b8b..08e7c015153 100644 --- a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h +++ b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,7 @@ class CanvasRenderingContext2D , public CanvasTransform , public CanvasFillStrokeStyles , public CanvasShadowStyles + , public CanvasFilters , public CanvasRect , public CanvasDrawPath , public CanvasText @@ -101,6 +103,9 @@ public: virtual float global_alpha() const override; virtual void set_global_alpha(float) override; + virtual String filter() const override; + virtual void set_filter(String) override; + virtual float shadow_offset_x() const override; virtual void set_shadow_offset_x(float) override; virtual float shadow_offset_y() const override;