diff --git a/Userland/Libraries/LibGfx/Forward.h b/Userland/Libraries/LibGfx/Forward.h index 369e1b072f2..93a1314c3e3 100644 --- a/Userland/Libraries/LibGfx/Forward.h +++ b/Userland/Libraries/LibGfx/Forward.h @@ -24,6 +24,7 @@ struct FontPixelMetrics; template class Line; +class AntiAliasingPainter; class Painter; class Palette; class PaletteImpl; diff --git a/Userland/Libraries/LibGfx/GradientPainting.cpp b/Userland/Libraries/LibGfx/GradientPainting.cpp index 3dd12f9a3ff..f016e1ea678 100644 --- a/Userland/Libraries/LibGfx/GradientPainting.cpp +++ b/Userland/Libraries/LibGfx/GradientPainting.cpp @@ -6,11 +6,13 @@ #include #include +#include #include #if defined(AK_COMPILER_GCC) # pragma GCC optimize("O3") #endif + namespace Gfx { // Note: This file implements the CSS gradients for LibWeb according to the spec. @@ -43,9 +45,14 @@ static float color_stop_step(ColorStop const& previous_stop, ColorStop const& ne return c; } +enum class UsePremultipliedAlpha { + Yes, + No +}; + class GradientLine { public: - GradientLine(int gradient_length, Span color_stops, Optional repeat_length) + GradientLine(int gradient_length, Span color_stops, Optional repeat_length, UsePremultipliedAlpha use_premultiplied_alpha = UsePremultipliedAlpha::Yes) : m_repeating { repeat_length.has_value() } , m_start_offset { round_to((m_repeating ? color_stops.first().position : 0.0f) * gradient_length) } { @@ -55,16 +62,21 @@ public: // Note: color_count will be < gradient_length for repeating gradients. auto color_count = round_to(repeat_length.value_or(1.0f) * necessary_length); m_gradient_line_colors.resize(color_count); - // Note: color.mixed_with() performs premultiplied alpha mixing when necessary as defined in: - // https://drafts.csswg.org/css-images/#coloring-gradient-line + + auto color_blend = [&](Color a, Color b, float amount) { + // Note: color.mixed_with() performs premultiplied alpha mixing when necessary as defined in: + // https://drafts.csswg.org/css-images/#coloring-gradient-line + if (use_premultiplied_alpha == UsePremultipliedAlpha::Yes) + return a.mixed_with(b, amount); + return a.interpolate(b, amount); + }; + for (int loc = 0; loc < color_count; loc++) { auto relative_loc = float(loc + m_start_offset) / necessary_length; - Color gradient_color = color_stops[0].color.mixed_with( - color_stops[1].color, + Color gradient_color = color_blend(color_stops[0].color, color_stops[1].color, color_stop_step(color_stops[0], color_stops[1], relative_loc)); for (size_t i = 1; i < color_stops.size() - 1; i++) { - gradient_color = gradient_color.mixed_with( - color_stops[i + 1].color, + gradient_color = color_blend(gradient_color, color_stops[i + 1].color, color_stop_step(color_stops[i], color_stops[i + 1], relative_loc)); } m_gradient_line_colors[loc] = gradient_color; @@ -117,41 +129,59 @@ private: bool m_requires_blending = false; }; -void Painter::fill_rect_with_linear_gradient(IntRect const& rect, Span const& color_stops, float angle, Optional repeat_length) -{ - auto a_rect = to_physical(rect); - if (a_rect.intersected(clip_rect() * scale()).is_empty()) - return; +template +struct Gradient { + Gradient(GradientLine gradient_line, TransformFunction transform_function) + : m_gradient_line(move(gradient_line)) + , m_transform_function(move(transform_function)) + { + } + void paint(Painter& painter, IntRect rect) + { + m_gradient_line.paint_into_physical_rect(painter, rect, m_transform_function); + } + + PaintStyle::SamplerFunction sample_function() + { + return [this](IntPoint point) { + return m_gradient_line.sample_color(m_transform_function(point.x(), point.y())); + }; + } + +private: + GradientLine m_gradient_line; + TransformFunction m_transform_function; +}; + +static auto create_linear_gradient(IntRect const& physical_rect, Span const& color_stops, float angle, Optional repeat_length) +{ float normalized_angle = normalized_gradient_angle_radians(angle); float sin_angle, cos_angle; AK::sincos(normalized_angle, sin_angle, cos_angle); // Full length of the gradient - auto gradient_length = calculate_gradient_length(a_rect.size(), sin_angle, cos_angle); + auto gradient_length = calculate_gradient_length(physical_rect.size(), sin_angle, cos_angle); IntPoint offset { cos_angle * (gradient_length / 2), sin_angle * (gradient_length / 2) }; - auto center = a_rect.translated(-a_rect.location()).center(); + auto center = physical_rect.translated(-physical_rect.location()).center(); auto start_point = center - offset; // Rotate gradient line to be horizontal auto rotated_start_point_x = start_point.x() * cos_angle - start_point.y() * -sin_angle; GradientLine gradient_line(gradient_length, color_stops, repeat_length); - gradient_line.paint_into_physical_rect(*this, a_rect, [&](int x, int y) { - return (x * cos_angle - (a_rect.height() - y) * -sin_angle) - rotated_start_point_x; - }); + return Gradient { + move(gradient_line), + [=](int x, int y) { + return (x * cos_angle - (physical_rect.height() - y) * -sin_angle) - rotated_start_point_x; + } + }; } -void Painter::fill_rect_with_conic_gradient(IntRect const& rect, Span const& color_stops, IntPoint center, float start_angle, Optional repeat_length) +static auto create_conic_gradient(Span const& color_stops, FloatPoint center_point, float start_angle, Optional repeat_length, UsePremultipliedAlpha use_premultiplied_alpha = UsePremultipliedAlpha::Yes) { - auto a_rect = to_physical(rect); - if (a_rect.intersected(clip_rect() * scale()).is_empty()) - return; - // FIXME: Do we need/want sub-degree accuracy for the gradient line? - GradientLine gradient_line(360, color_stops, repeat_length); + GradientLine gradient_line(360, color_stops, repeat_length, use_premultiplied_alpha); float normalized_start_angle = (360.0f - start_angle) + 90.0f; - // Translate position/center to the center of the pixel (avoids some funky painting) - auto center_point = FloatPoint { center * scale() }.translated(0.5, 0.5); // The flooring can make gradients that want soft edges look worse, so only floor if we have hard edges. // Which makes sure the hard edge stay hard edges :^) bool should_floor_angles = false; @@ -161,12 +191,59 @@ void Painter::fill_rect_with_conic_gradient(IntRect const& rect, Span + 360.0f + normalized_start_angle), 360.0f); - return should_floor_angles ? floor(loc) : loc; - }); + return Gradient { + move(gradient_line), + [=](int x, int y) { + auto point = FloatPoint { x, y } - center_point; + // FIXME: We could probably get away with some approximation here: + auto loc = fmod((AK::atan2(point.y(), point.x()) * 180.0f / AK::Pi + 360.0f + normalized_start_angle), 360.0f); + return should_floor_angles ? floor(loc) : loc; + } + }; +} + +static auto create_radial_gradient(IntRect const& physical_rect, Span const& color_stops, IntPoint center, IntSize size, Optional repeat_length) +{ + // A conservative guesstimate on how many colors we need to generate: + auto max_dimension = max(physical_rect.width(), physical_rect.height()); + auto max_visible_gradient = max(max_dimension / 2, min(size.width(), max_dimension)); + GradientLine gradient_line(max_visible_gradient, color_stops, repeat_length); + auto center_point = FloatPoint { center }.translated(0.5, 0.5); + return Gradient { + move(gradient_line), + [=](int x, int y) { + // FIXME: See if there's a more efficient calculation we do there :^) + auto point = FloatPoint(x, y) - center_point; + auto gradient_x = point.x() / size.width(); + auto gradient_y = point.y() / size.height(); + return AK::sqrt(gradient_x * gradient_x + gradient_y * gradient_y) * max_visible_gradient; + } + }; +} + +void Painter::fill_rect_with_linear_gradient(IntRect const& rect, Span const& color_stops, float angle, Optional repeat_length) +{ + auto a_rect = to_physical(rect); + if (a_rect.intersected(clip_rect() * scale()).is_empty()) + return; + auto linear_gradient = create_linear_gradient(a_rect, color_stops, angle, repeat_length); + linear_gradient.paint(*this, a_rect); +} + +static FloatPoint pixel_center(IntPoint point) +{ + return point.to_type().translated(0.5f, 0.5f); +} + +void Painter::fill_rect_with_conic_gradient(IntRect const& rect, Span const& color_stops, IntPoint center, float start_angle, Optional repeat_length) +{ + auto a_rect = to_physical(rect); + if (a_rect.intersected(clip_rect() * scale()).is_empty()) + return; + // Translate position/center to the center of the pixel (avoids some funky painting) + auto center_point = pixel_center(center * scale()); + auto conic_gradient = create_conic_gradient(color_stops, center_point, start_angle, repeat_length); + conic_gradient.paint(*this, a_rect); } void Painter::fill_rect_with_radial_gradient(IntRect const& rect, Span const& color_stops, IntPoint center, IntSize size, Optional repeat_length) @@ -174,19 +251,32 @@ void Painter::fill_rect_with_radial_gradient(IntRect const& rect, Span 2); + auto linear_gradient = create_linear_gradient(physical_bounding_box, color_stops(), m_angle, repeat_length()); + paint(linear_gradient.sample_function()); +} + +void ConicGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const +{ + VERIFY(color_stops().size() > 2); + (void)physical_bounding_box; + auto conic_gradient = create_conic_gradient(color_stops(), pixel_center(m_center), m_start_angle, repeat_length()); + paint(conic_gradient.sample_function()); +} + +void RadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const +{ + VERIFY(color_stops().size() > 2); + auto radial_gradient = create_radial_gradient(physical_bounding_box, color_stops(), m_center, m_size, repeat_length()); + paint(radial_gradient.sample_function()); } } diff --git a/Userland/Libraries/LibGfx/PaintStyle.h b/Userland/Libraries/LibGfx/PaintStyle.h new file mode 100644 index 00000000000..ffa6ef0a91f --- /dev/null +++ b/Userland/Libraries/LibGfx/PaintStyle.h @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2023, MacDue + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gfx { + +class PaintStyle : public RefCounted { +public: + virtual ~PaintStyle() = default; + using SamplerFunction = Function; + using PaintFunction = Function; + + friend Painter; + friend AntiAliasingPainter; + +private: + // Simple paint styles can simply override sample_color() if they can easily generate a color from a coordinate. + virtual Color sample_color(IntPoint) const { return Color(); }; + + // Paint styles that have paint time dependent state (e.g. based on the paint size) may find it easier to override paint(). + // If paint() is overridden sample_color() is unused. + virtual void paint(IntRect physical_bounding_box, PaintFunction paint) const + { + (void)physical_bounding_box; + paint([this](IntPoint point) { return sample_color(point); }); + } +}; + +class SolidColorPaintStyle final : public PaintStyle { +public: + static NonnullRefPtr create(Color color) + { + return adopt_ref(*new SolidColorPaintStyle(color)); + } + + virtual Color sample_color(IntPoint) const override { return m_color; } + +private: + SolidColorPaintStyle(Color color) + : m_color(color) + { + } + + Color m_color; +}; + +class GradientPaintStyle : public PaintStyle { +public: + void add_color_stop(float position, Color color, Optional transition_hint = {}) + { + add_color_stop(ColorStop { color, position, transition_hint }); + } + + void add_color_stop(ColorStop stop, bool sort = true) + { + m_color_stops.append(stop); + if (sort) + quick_sort(m_color_stops, [](auto& a, auto& b) { return a.position < b.position; }); + } + + void set_repeat_length(float repeat_length) + { + m_repeat_length = repeat_length; + } + + Span color_stops() const { return m_color_stops; } + Optional repeat_length() const { return m_repeat_length; } + +private: + Vector m_color_stops; + Optional m_repeat_length; +}; + +// These paint styles are based on the CSS gradients. They are relative to the painted +// shape and support premultiplied alpha. + +class LinearGradientPaintStyle final : public GradientPaintStyle { +public: + static NonnullRefPtr create(float angle = 0.0f) + { + return adopt_ref(*new LinearGradientPaintStyle(angle)); + } + +private: + virtual void paint(IntRect physical_bounding_box, PaintFunction paint) const override; + + LinearGradientPaintStyle(float angle) + : m_angle(angle) + { + } + + float m_angle { 0.0f }; +}; + +class ConicGradientPaintStyle final : public GradientPaintStyle { +public: + static NonnullRefPtr create(IntPoint center, float start_angle = 0.0f) + { + return adopt_ref(*new ConicGradientPaintStyle(center, start_angle)); + } + +private: + virtual void paint(IntRect physical_bounding_box, PaintFunction paint) const override; + + ConicGradientPaintStyle(IntPoint center, float start_angle) + : m_center(center) + , m_start_angle(start_angle) + { + } + + IntPoint m_center; + float m_start_angle { 0.0f }; +}; + +class RadialGradientPaintStyle final : public GradientPaintStyle { +public: + static NonnullRefPtr create(IntPoint center, IntSize size) + { + return adopt_ref(*new RadialGradientPaintStyle(center, size)); + } + +private: + virtual void paint(IntRect physical_bounding_box, PaintFunction paint) const override; + + RadialGradientPaintStyle(IntPoint center, IntSize size) + : m_center(center) + , m_size(size) + { + } + + IntPoint m_center; + IntSize m_size; +}; + +}