From fdcc73d4b17ae886aa5609745fb4be982f1f40dc Mon Sep 17 00:00:00 2001 From: MacDue Date: Sun, 30 Oct 2022 19:28:00 +0100 Subject: [PATCH] LibWeb: Paint `conic-gradient()`s This is a first pass at painting conic-gradient()s, I've yet to try to optimize this much, but I feel like you could do better than atan2 in a loop. --- Userland/Libraries/LibWeb/CSS/StyleValue.cpp | 9 +- Userland/Libraries/LibWeb/CSS/StyleValue.h | 7 + .../LibWeb/Painting/GradientPainting.cpp | 172 +++++++++++------- .../LibWeb/Painting/GradientPainting.h | 7 + 4 files changed, 129 insertions(+), 66 deletions(-) diff --git a/Userland/Libraries/LibWeb/CSS/StyleValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValue.cpp index a7186fa628..5772cebccc 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValue.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleValue.cpp @@ -1976,12 +1976,17 @@ String ConicGradientStyleValue::to_string() const return builder.to_string(); } -void ConicGradientStyleValue::resolve_for_size(Layout::Node const&, Gfx::FloatSize const&) const +void ConicGradientStyleValue::resolve_for_size(Layout::Node const& node, Gfx::FloatSize const& size) const { + if (!m_resolved.has_value()) + m_resolved = ResolvedData { Painting::resolve_conic_gradient_data(node, *this), {} }; + m_resolved->position = m_position.resolved(node, Gfx::FloatRect { { 0, 0 }, size }); } -void ConicGradientStyleValue::paint(PaintContext&, Gfx::IntRect const&, CSS::ImageRendering) const +void ConicGradientStyleValue::paint(PaintContext& context, Gfx::IntRect const& dest_rect, CSS::ImageRendering) const { + VERIFY(m_resolved.has_value()); + Painting::paint_conic_gradient(context, dest_rect, m_resolved->data, m_resolved->position.to_rounded()); } bool ConicGradientStyleValue::equals(StyleValue const&) const diff --git a/Userland/Libraries/LibWeb/CSS/StyleValue.h b/Userland/Libraries/LibWeb/CSS/StyleValue.h index b23016d3e2..5360447c18 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValue.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValue.h @@ -1233,6 +1233,13 @@ private: Angle m_from_angle; PositionValue m_position; Vector m_color_stop_list; + + struct ResolvedData { + Painting::ConicGradientData data; + Gfx::FloatPoint position; + }; + + mutable Optional m_resolved; }; class LinearGradientStyleValue final : public AbstractImageStyleValue { diff --git a/Userland/Libraries/LibWeb/Painting/GradientPainting.cpp b/Userland/Libraries/LibWeb/Painting/GradientPainting.cpp index 1bec87633a..6725949d7e 100644 --- a/Userland/Libraries/LibWeb/Painting/GradientPainting.cpp +++ b/Userland/Libraries/LibWeb/Painting/GradientPainting.cpp @@ -33,10 +33,8 @@ static float calulate_gradient_length(Gfx::IntSize const& gradient_size, float g return calulate_gradient_length(gradient_size, sin_angle, cos_angle); } -LinearGradientData resolve_linear_gradient_data(Layout::Node const& node, Gfx::FloatSize const& gradient_size, CSS::LinearGradientStyleValue const& linear_gradient) +static ColorStopList resolve_color_stop_positions(auto const& color_stop_list, float gradient_length, auto resolve_position_to_float) { - auto& color_stop_list = linear_gradient.color_stop_list(); - VERIFY(color_stop_list.size() >= 2); ColorStopList resolved_color_stops; @@ -55,26 +53,23 @@ LinearGradientData resolve_linear_gradient_data(Layout::Node const& node, Gfx::F resolved_color_stops.append(resolved_stop); } - auto gradient_angle = linear_gradient.angle_degrees(gradient_size); - auto gradient_length_px = calulate_gradient_length(gradient_size.to_rounded(), gradient_angle); - auto gradient_length = CSS::Length::make_px(gradient_length_px); - // 1. If the first color stop does not have a position, set its position to 0%. resolved_color_stops.first().position = 0; // If the last color stop does not have a position, set its position to 100% - resolved_color_stops.last().position = gradient_length_px; + resolved_color_stops.last().position = gradient_length; // 2. If a color stop or transition hint has a position that is less than the // specified position of any color stop or transition hint before it in the list, // set its position to be equal to the largest specified position of any color stop // or transition hint before it. auto max_previous_color_stop_or_hint = resolved_color_stops[0].position; - auto resolve_stop_position = [&](auto& length_percentage) { - float value = length_percentage.resolved(node, gradient_length).to_px(node); + auto resolve_stop_position = [&](auto& position) { + float value = resolve_position_to_float(position); value = max(value, max_previous_color_stop_or_hint); max_previous_color_stop_or_hint = value; return value; }; + // Move this step somewhere generic (since I think this code can be mostly reused for conic gradients) size_t resolved_index = 0; for (auto& stop : color_stop_list) { if (stop.transition_hint.has_value()) @@ -127,6 +122,19 @@ LinearGradientData resolve_linear_gradient_data(Layout::Node const& node, Gfx::F } } + return resolved_color_stops; +} + +LinearGradientData resolve_linear_gradient_data(Layout::Node const& node, Gfx::FloatSize const& gradient_size, CSS::LinearGradientStyleValue const& linear_gradient) +{ + auto gradient_angle = linear_gradient.angle_degrees(gradient_size); + auto gradient_length_px = calulate_gradient_length(gradient_size.to_rounded(), gradient_angle); + auto gradient_length = CSS::Length::make_px(gradient_length_px); + + auto resolved_color_stops = resolve_color_stop_positions(linear_gradient.color_stop_list(), gradient_length_px, [&](auto const& length_percentage) { + return length_percentage.resolved(node, gradient_length).to_px(node); + }); + Optional repeat_length = {}; if (linear_gradient.is_repeating()) repeat_length = resolved_color_stops.last().position - resolved_color_stops.first().position; @@ -134,6 +142,77 @@ LinearGradientData resolve_linear_gradient_data(Layout::Node const& node, Gfx::F return { gradient_angle, resolved_color_stops, repeat_length }; } +ConicGradientData resolve_conic_gradient_data(Layout::Node const& node, CSS::ConicGradientStyleValue const& conic_gradient) +{ + CSS::Angle one_turn(360.0f, CSS::Angle::Type::Deg); + auto resolved_color_stops = resolve_color_stop_positions(conic_gradient.color_stop_list(), one_turn.to_degrees(), [&](auto const& angle_percentage) { + return angle_percentage.resolved(node, one_turn).to_degrees(); + }); + return { conic_gradient.angle_degrees(), resolved_color_stops }; +} + +static float color_stop_step(ColorStop const& previous_stop, ColorStop const& next_stop, float position) +{ + if (position < previous_stop.position) + return 0; + if (position > next_stop.position) + return 1; + // For any given point between the two color stops, + // determine the point’s location as a percentage of the distance between the two color stops. + // Let this percentage be P. + auto stop_length = next_stop.position - previous_stop.position; + // FIXME: Avoids NaNs... Still not quite correct? + if (stop_length <= 0) + return 1; + auto p = (position - previous_stop.position) / stop_length; + if (!next_stop.transition_hint.has_value()) + return p; + if (*next_stop.transition_hint >= 1) + return 0; + if (*next_stop.transition_hint <= 0) + return 1; + // Let C, the color weighting at that point, be equal to P^(logH(.5)). + auto c = AK::pow(p, AK::log(0.5) / AK::log(*next_stop.transition_hint)); + // The color at that point is then a linear blend between the colors of the two color stops, + // blending (1 - C) of the first stop and C of the second stop. + return c; +} + +class GradientLine { +public: + GradientLine(int length, int start_offset, Span color_stops) + { + m_gradient_line_colors.resize(length); + // Note: color.mixed_with() performs premultiplied alpha mixing when necessary as defined in: + // https://drafts.csswg.org/css-images/#coloring-gradient-line + for (int loc = 0; loc < length; loc++) { + Gfx::Color gradient_color = color_stops[0].color.mixed_with( + color_stops[1].color, + color_stop_step( + color_stops[0], + color_stops[1], + loc + start_offset)); + for (size_t i = 1; i < color_stops.size() - 1; i++) { + gradient_color = gradient_color.mixed_with( + color_stops[i + 1].color, + color_stop_step( + color_stops[i], + color_stops[i + 1], + loc + start_offset)); + } + m_gradient_line_colors[loc] = gradient_color; + } + } + + Gfx::Color lookup_color(int loc) const + { + return m_gradient_line_colors[clamp(loc, 0, m_gradient_line_colors.size() - 1)]; + } + +private: + Vector m_gradient_line_colors; +}; + void paint_linear_gradient(PaintContext& context, Gfx::IntRect const& gradient_rect, LinearGradientData const& data) { float angle = normalized_gradient_angle_radians(data.gradient_angle); @@ -151,63 +230,11 @@ void paint_linear_gradient(PaintContext& context, Gfx::IntRect const& gradient_r // Rotate gradient line to be horizontal auto rotated_start_point_x = start_point.x() * cos_angle - start_point.y() * -sin_angle; - auto color_stop_step = [&](auto& previous_stop, auto& next_stop, float position) -> float { - if (position < previous_stop.position) - return 0; - if (position > next_stop.position) - return 1; - // For any given point between the two color stops, - // determine the point’s location as a percentage of the distance between the two color stops. - // Let this percentage be P. - auto stop_length = next_stop.position - previous_stop.position; - // FIXME: Avoids NaNs... Still not quite correct? - if (stop_length <= 0) - return 1; - auto p = (position - previous_stop.position) / stop_length; - if (!next_stop.transition_hint.has_value()) - return p; - if (*next_stop.transition_hint >= 1) - return 0; - if (*next_stop.transition_hint <= 0) - return 1; - // Let C, the color weighting at that point, be equal to P^(logH(.5)). - auto c = AK::pow(p, AK::log(0.5) / AK::log(*next_stop.transition_hint)); - // The color at that point is then a linear blend between the colors of the two color stops, - // blending (1 - C) of the first stop and C of the second stop. - return c; - }; - - Vector gradient_line_colors; auto gradient_color_count = round_to(data.repeat_length.value_or(length)); - gradient_line_colors.resize(gradient_color_count); auto& color_stops = data.color_stops; auto start_offset = data.repeat_length.has_value() ? color_stops.first().position : 0.0f; - auto start_offset_int = round_to(start_offset); - // Note: color.mixed_with() performs premultiplied alpha mixing when necessary as defined in: - // https://drafts.csswg.org/css-images/#coloring-gradient-line - - for (int loc = 0; loc < gradient_color_count; loc++) { - Gfx::Color gradient_color = color_stops[0].color.mixed_with( - color_stops[1].color, - color_stop_step( - color_stops[0], - color_stops[1], - loc + start_offset_int)); - for (size_t i = 1; i < color_stops.size() - 1; i++) { - gradient_color = gradient_color.mixed_with( - color_stops[i + 1].color, - color_stop_step( - color_stops[i], - color_stops[i + 1], - loc + start_offset_int)); - } - gradient_line_colors[loc] = gradient_color; - } - - auto lookup_color = [&](int loc) { - return gradient_line_colors[clamp(loc, 0, gradient_color_count - 1)]; - }; + GradientLine gradient_line(gradient_color_count, round_to(start_offset), color_stops); auto repeat_wrap_if_required = [&](float loc) { if (data.repeat_length.has_value()) @@ -220,7 +247,24 @@ void paint_linear_gradient(PaintContext& context, Gfx::IntRect const& gradient_r auto loc = repeat_wrap_if_required((x * cos_angle - (gradient_rect.height() - y) * -sin_angle) - rotated_start_point_x - start_offset); auto blend = loc - static_cast(loc); // Blend between the two neighbouring colors (this fixes some nasty aliasing issues at small angles) - auto gradient_color = lookup_color(loc).mixed_with(lookup_color(repeat_wrap_if_required(loc + 1)), blend); + auto gradient_color = gradient_line.lookup_color(loc).mixed_with(gradient_line.lookup_color(repeat_wrap_if_required(loc + 1)), blend); + context.painter().set_pixel(gradient_rect.x() + x, gradient_rect.y() + y, gradient_color, gradient_color.alpha() < 255); + } + } +} + +void paint_conic_gradient(PaintContext& context, Gfx::IntRect const& gradient_rect, ConicGradientData const& data, Gfx::IntPoint position) +{ + // FIXME: Do we need/want sub-degree accuracy for the gradient line? + GradientLine gradient_line(360, 0, data.color_stops); + float start_angle = (360.0f - data.start_angle) + 90.0f; + for (int y = 0; y < gradient_rect.height(); y++) { + for (int x = 0; x < gradient_rect.width(); x++) { + auto point = Gfx::IntPoint { x, y } - position; + // FIXME: We could probably get away with some approximation here: + float loc = fmod((AK::atan2(float(point.y()), float(point.x())) * 180.0f / AK::Pi + 360.0f + start_angle), 360.0f); + auto blend = loc - static_cast(loc); + auto gradient_color = gradient_line.lookup_color(loc).mixed_with(gradient_line.lookup_color(loc + 1), blend); context.painter().set_pixel(gradient_rect.x() + x, gradient_rect.y() + y, gradient_color, gradient_color.alpha() < 255); } } diff --git a/Userland/Libraries/LibWeb/Painting/GradientPainting.h b/Userland/Libraries/LibWeb/Painting/GradientPainting.h index ce19853fdf..3bff8b72d2 100644 --- a/Userland/Libraries/LibWeb/Painting/GradientPainting.h +++ b/Userland/Libraries/LibWeb/Painting/GradientPainting.h @@ -28,8 +28,15 @@ struct LinearGradientData { Optional repeat_length; }; +struct ConicGradientData { + float start_angle; + ColorStopList color_stops; +}; + LinearGradientData resolve_linear_gradient_data(Layout::Node const&, Gfx::FloatSize const&, CSS::LinearGradientStyleValue const&); +ConicGradientData resolve_conic_gradient_data(Layout::Node const&, CSS::ConicGradientStyleValue const&); void paint_linear_gradient(PaintContext&, Gfx::IntRect const&, LinearGradientData const&); +void paint_conic_gradient(PaintContext&, Gfx::IntRect const&, ConicGradientData const&, Gfx::IntPoint position); }