From 81cefab1b1bcbbf7b36607812969d55161755ff9 Mon Sep 17 00:00:00 2001 From: MacDue Date: Tue, 2 May 2023 22:43:46 +0100 Subject: [PATCH] LibGfx: Implement PaintStyle for SVG radial gradients This also slightly refactors things to share more implementation with the SVG linear gradients, and improve accuracy (which fixes some banding issues). --- .../Libraries/LibGfx/GradientPainting.cpp | 101 +++++++++++------- Userland/Libraries/LibGfx/PaintStyle.h | 56 ++++++++-- 2 files changed, 113 insertions(+), 44 deletions(-) diff --git a/Userland/Libraries/LibGfx/GradientPainting.cpp b/Userland/Libraries/LibGfx/GradientPainting.cpp index f6826066ab..accdfed9a9 100644 --- a/Userland/Libraries/LibGfx/GradientPainting.cpp +++ b/Userland/Libraries/LibGfx/GradientPainting.cpp @@ -153,9 +153,10 @@ struct Gradient { m_gradient_line.paint_into_physical_rect(painter, rect, m_transform_function); } - PaintStyle::SamplerFunction sample_function() + template + auto sample_function() { - return [this](IntPoint point) { + return [this](Point point) { return m_gradient_line.sample_color(m_transform_function(point.x(), point.y())); }; } @@ -339,6 +340,20 @@ void CanvasLinearGradientPaintStyle::paint(IntRect physical_bounding_box, PaintF paint(make_sample_non_relative(physical_bounding_box.location(), linear_gradient.sample_function())); } +void SVGGradientPaintStyle::set_gradient_transform(AffineTransform transform) +{ + // Note: The scaling is removed so enough points on the gradient line are generated. + // Otherwise, if you scale a tiny path the gradient looks pixelated. + m_scale = 1.0f; + if (auto inverse = transform.inverse(); inverse.has_value()) { + auto transform_scale = transform.scale(); + m_scale = max(transform_scale.x(), transform_scale.y()); + m_inverse_transform = AffineTransform {}.scale(m_scale, m_scale).multiply(*inverse); + } else { + m_inverse_transform = OptionalNone {}; + } +} + void SVGLinearGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const { if (color_stops().is_empty()) @@ -350,23 +365,15 @@ void SVGLinearGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunc if (color_stops().size() < 2) return paint([this](IntPoint) { return color_stops().first().color; }); - // Note: The scaling is removed so enough points on the gradient line are generated. - // Otherwise, if you scale a tiny path the gradient looks pixelated. - FloatPoint scale { 1, 1 }; - auto sample_transform = gradient_transform().map([&](auto& transform) { - if (auto inverse = transform.inverse(); inverse.has_value()) { - scale = transform.scale(); - return Gfx::AffineTransform {}.scale(scale).multiply(*inverse); - } - return Gfx::AffineTransform {}; - }); + float scale = gradient_transform_scale(); + auto linear_gradient = make_linear_gradient_between_two_points( + m_p0.scaled(scale, scale), m_p1.scaled(scale, scale), + color_stops(), repeat_length()); - auto linear_gradient = make_linear_gradient_between_two_points(m_p0.scaled(scale), m_p1.scaled(scale), color_stops(), repeat_length()); - - paint([&, sampler = linear_gradient.sample_function()](auto point) { - point.translate_by(physical_bounding_box.location()); - if (sample_transform.has_value()) - point = sample_transform->map(point); + paint([&, sampler = linear_gradient.sample_function()](IntPoint target_point) { + auto point = target_point.translated(physical_bounding_box.location()).to_type(); + if (auto inverse_transform = scale_adjusted_inverse_gradient_transform(); inverse_transform.has_value()) + point = inverse_transform->map(point); return sampler(point); }); @@ -387,24 +394,8 @@ void CanvasConicGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFu paint(make_sample_non_relative(physical_bounding_box.location(), conic_gradient.sample_function())); } -void CanvasRadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const +static auto create_radial_gradient_between_two_circles(Gfx::FloatPoint start_center, float start_radius, Gfx::FloatPoint end_center, float end_radius, ReadonlySpan color_stops, Optional repeat_length) { - // 1. If x0 = x1 and y0 = y1 and r0 = r1, then the radial gradient must paint nothing. Return. - if (m_start_center == m_end_center && m_start_radius == m_end_radius) - return; - if (color_stops().is_empty()) - return; - if (color_stops().size() < 2) - return paint([this](IntPoint) { return color_stops().first().color; }); - - auto start_radius = m_start_radius; - auto start_center = m_start_center; - auto end_radius = m_end_radius; - auto end_center = m_end_center; - - if (end_radius == 0 && start_radius == 0) - return; - if (fabs(start_radius - end_radius) < 1) start_radius += 1; @@ -443,7 +434,7 @@ void CanvasRadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintF // This is just an approximate upperbound (the gradient line class will shorten this if necessary). int gradient_length = AK::ceil(center_dist + end_radius + start_radius); - GradientLine gradient_line(gradient_length, color_stops(), repeat_length(), UsePremultipliedAlpha::No); + GradientLine gradient_line(gradient_length, color_stops, repeat_length, UsePremultipliedAlpha::No); auto radius2 = end_radius * end_radius; center_delta = end_center - start_point; @@ -459,9 +450,9 @@ void CanvasRadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintF // - Start circle larger than end circle (outside end circle) // - Start circle or end circle radius == 0 - Gradient radial_gradient { + return Gradient { move(gradient_line), - [=](int x, int y) { + [=](float x, float y) { auto get_gradient_location = [&] { FloatPoint point { x, y }; auto dist = point.distance_from(start_point); @@ -491,8 +482,42 @@ void CanvasRadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintF return loc * gradient_length; } }; +} +void CanvasRadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const +{ + // 1. If x0 = x1 and y0 = y1 and r0 = r1, then the radial gradient must paint nothing. Return. + if (m_start_center == m_end_center && m_start_radius == m_end_radius) + return; + if (color_stops().is_empty()) + return; + if (color_stops().size() < 2) + return paint([this](IntPoint) { return color_stops().first().color; }); + if (m_end_radius == 0 && m_start_radius == 0) + return; + auto radial_gradient = create_radial_gradient_between_two_circles(m_start_center, m_start_radius, m_end_center, m_end_radius, color_stops(), repeat_length()); paint(make_sample_non_relative(physical_bounding_box.location(), radial_gradient.sample_function())); } +void SVGRadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const +{ + // FIXME: Ensure this handles all the edge cases of SVG gradients. + if (color_stops().is_empty()) + return; + if (color_stops().size() < 2 || (m_end_radius == 0 && m_start_radius == 0)) + return paint([this](IntPoint) { return color_stops().last().color; }); + + float scale = gradient_transform_scale(); + auto radial_gradient = create_radial_gradient_between_two_circles( + m_start_center.scaled(scale, scale), m_start_radius * scale, m_end_center.scaled(scale, scale), m_end_radius * scale, + color_stops(), repeat_length()); + + paint([&, sampler = radial_gradient.sample_function()](IntPoint target_point) { + auto point = target_point.translated(physical_bounding_box.location()).to_type(); + if (auto inverse_transform = scale_adjusted_inverse_gradient_transform(); inverse_transform.has_value()) + point = inverse_transform->map(point); + return sampler(point); + }); +} + } diff --git a/Userland/Libraries/LibGfx/PaintStyle.h b/Userland/Libraries/LibGfx/PaintStyle.h index b39e509980..ee17771180 100644 --- a/Userland/Libraries/LibGfx/PaintStyle.h +++ b/Userland/Libraries/LibGfx/PaintStyle.h @@ -247,15 +247,15 @@ private: class SVGGradientPaintStyle : public GradientPaintStyle { public: - void set_gradient_transform(Gfx::AffineTransform transform) - { - m_gradient_transform = transform; - } + void set_gradient_transform(Gfx::AffineTransform transform); - Optional const& gradient_transform() const { return m_gradient_transform; } +protected: + Optional const& scale_adjusted_inverse_gradient_transform() const { return m_inverse_transform; } + float gradient_transform_scale() const { return m_scale; } private: - Optional m_gradient_transform {}; + Optional m_inverse_transform {}; + float m_scale = 1.0f; }; class SVGLinearGradientPaintStyle final : public SVGGradientPaintStyle { @@ -288,4 +288,48 @@ private: FloatPoint m_p1; }; +class SVGRadialGradientPaintStyle final : public SVGGradientPaintStyle { +public: + static ErrorOr> create(FloatPoint start_center, float start_radius, FloatPoint end_center, float end_radius) + { + return adopt_nonnull_ref_or_enomem(new (nothrow) SVGRadialGradientPaintStyle(start_center, start_radius, end_center, end_radius)); + } + + void set_start_center(FloatPoint start_center) + { + m_start_center = start_center; + } + + void set_start_radius(float start_radius) + { + m_start_radius = start_radius; + } + + void set_end_center(FloatPoint end_center) + { + m_end_center = end_center; + } + + void set_end_radius(float end_radius) + { + m_end_radius = end_radius; + } + +private: + virtual void paint(IntRect physical_bounding_box, PaintFunction paint) const override; + + SVGRadialGradientPaintStyle(FloatPoint start_center, float start_radius, FloatPoint end_center, float end_radius) + : m_start_center(start_center) + , m_start_radius(start_radius) + , m_end_center(end_center) + , m_end_radius(end_radius) + { + } + + FloatPoint m_start_center; + float m_start_radius { 0.0f }; + FloatPoint m_end_center; + float m_end_radius { 0.0f }; +}; + }