1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 21:07:35 +00:00

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).
This commit is contained in:
MacDue 2023-05-02 22:43:46 +01:00 committed by Andreas Kling
parent 3d54b9ffa3
commit 81cefab1b1
2 changed files with 113 additions and 44 deletions

View file

@ -153,9 +153,10 @@ struct Gradient {
m_gradient_line.paint_into_physical_rect(painter, rect, m_transform_function);
}
PaintStyle::SamplerFunction sample_function()
template<typename CoordinateType = int>
auto sample_function()
{
return [this](IntPoint point) {
return [this](Point<CoordinateType> 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<float>()](IntPoint target_point) {
auto point = target_point.translated(physical_bounding_box.location()).to_type<float>();
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<ColorStop> color_stops, Optional<float> 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<float>()](IntPoint target_point) {
auto point = target_point.translated(physical_bounding_box.location()).to_type<float>();
if (auto inverse_transform = scale_adjusted_inverse_gradient_transform(); inverse_transform.has_value())
point = inverse_transform->map(point);
return sampler(point);
});
}
}

View file

@ -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<Gfx::AffineTransform> const& gradient_transform() const { return m_gradient_transform; }
protected:
Optional<AffineTransform> const& scale_adjusted_inverse_gradient_transform() const { return m_inverse_transform; }
float gradient_transform_scale() const { return m_scale; }
private:
Optional<Gfx::AffineTransform> m_gradient_transform {};
Optional<AffineTransform> 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<NonnullRefPtr<SVGRadialGradientPaintStyle>> 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 };
};
}