From f7a680f30a6d186bbb3e9e0dd645ef746bfde12e Mon Sep 17 00:00:00 2001 From: MacDue Date: Mon, 21 Nov 2022 19:42:00 +0100 Subject: [PATCH] LibGfx: Implement nicer antialiased lines This is not any 'proper' algorithm, this was just a shower thought idea. There probably is a better algorithm to achieve the same effect out there, if someone knows of one please replace this code :^). This works by rendering the line a scanline at a time, which avoids repainting over any pixel on the line (so opacity now works with AA lines). This generally seems to achieve a much nicer looking line. I've not done any proper benchmarking of this, but some little messing around showed that this new implementation was a little faster than the old one too, so that's a nice little bonus. With the inclusion of a few minor hacks this also goes a surprisingly far way in improving our SVG rendering too (for both filled and stroked paths). :^) --- .../Libraries/LibGfx/AntiAliasingPainter.cpp | 283 ++++++++---------- .../Libraries/LibGfx/AntiAliasingPainter.h | 2 +- 2 files changed, 134 insertions(+), 151 deletions(-) diff --git a/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp b/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp index 4c96620c32..c16904d07e 100644 --- a/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp +++ b/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp @@ -18,148 +18,131 @@ namespace Gfx { -// Base algorithm from https://en.wikipedia.org/wiki/Xiaolin_Wu%27s_line_algorithm, -// because there seems to be no other known method for drawing AA'd lines (?) template -void AntiAliasingPainter::draw_anti_aliased_line(FloatPoint const& actual_from, FloatPoint const& actual_to, Color color, float thickness, Painter::LineStyle style, Color) +void AntiAliasingPainter::draw_anti_aliased_line(FloatPoint actual_from, FloatPoint actual_to, Color color, float thickness, Painter::LineStyle style, Color) { // FIXME: Implement this :P VERIFY(style == Painter::LineStyle::Solid); - auto corrected_thickness = thickness > 1 ? thickness - 1 : thickness; - auto size = IntSize(corrected_thickness, corrected_thickness); + // FIMXE: + // This is not a proper line drawing algorithm. + // It's hack-ish AA rotated rectangle painting. + // There's probably more optimal ways to achieve this + // (though this still runs faster than the previous AA-line code) + // + // If you, reading this comment, know a better way that: + // 1. Does not overpaint (i.e. painting a line with transparency looks correct) + // 2. Has square end points (i.e. the line is a rectangle) + // 3. Has good anti-aliasing + // 4. Is less hacky than this + // + // Please delete this code and implement it! + + auto int_thickness = round_to(thickness); auto mapped_from = m_transform.map(actual_from); auto mapped_to = m_transform.map(actual_to); - auto draw_direction = mapped_from - mapped_to; - auto is_straight_line = draw_direction.x() == 0 || draw_direction.y() == 0; - auto rotated_rectangle_reference_coords = FloatQuad({ 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }); - float drawing_edge_offset = fabs((corrected_thickness / 2) - fmodf(corrected_thickness, 2.0f)); + auto length = mapped_to.distance_from(mapped_from); - auto integer_part = [](float x) { return floorf(x); }; - auto round = [&](float x) { return integer_part(x + 0.5f); }; - - if (is_straight_line) { - // draw top to bottom line in one call - if (draw_direction.x() == 0) - m_underlying_painter.fill_rect( - { mapped_from.x() - drawing_edge_offset, - round(min(mapped_from.y(), mapped_to.y())), - thickness, - round(fabsf(draw_direction.y())) }, - color); - // draw left to right line in one call - if (draw_direction.y() == 0) { - m_underlying_painter.fill_rect( - { round(min(mapped_from.x(), mapped_to.x())), - mapped_from.y() - drawing_edge_offset, - round(fabsf(draw_direction.x())), - thickness }, - color); - } - return; + // Axis-aligned lines: + if (mapped_from.y() == mapped_to.y()) { + auto start_point = (mapped_from.x() < mapped_to.x() ? mapped_from : mapped_to).translated(0, -int_thickness / 2); + // FIXME: SVG fill_path() hack: + // SVG asks for 1px scanlines at floating point y values, if they're not rounded to a pixel they look faint. + start_point.set_y(round_to(start_point.y())); + return fill_rect(Gfx::FloatRect(start_point, { length, float(int_thickness) }), color); + } + if (mapped_from.x() == mapped_to.x()) { + auto start_point = (mapped_from.y() < mapped_to.y() ? mapped_from : mapped_to).translated(-int_thickness / 2, 0); + return fill_rect(Gfx::FloatRect(start_point, { float(int_thickness), length }), color); } - rotated_rectangle_reference_coords = build_rotated_rectangle(draw_direction, corrected_thickness); + // FIXME: SVG stoke_path() hack: + // When painting stokes SVG asks for many thickness * < 1px lines. + // It actually wants a thickness * thickness dot centered at that point. + if (length < 1.0f) + return fill_rect(Gfx::FloatRect::centered_at(mapped_from, { thickness, thickness }), color); - auto plot = [&](int x, int y, float c) { - // ignore rotation if rectangle is fairly small to reduce overhead - if (is_straight_line || corrected_thickness < 4) { - m_underlying_painter.fill_rect(IntRect::centered_on({ x, y }, size), color.with_alpha(color.alpha() * c)); - } else if (min(AK::abs(mapped_from.distance_from({ x, y })), AK::abs(mapped_to.distance_from({ x, y }))) >= drawing_edge_offset || AK::abs(mapped_from.distance_from(mapped_to)) < drawing_edge_offset) { - // don't draw if we are close to the edge but draw if the whole line is shorter than allowed edge distance - m_rotated_rectangle_path.clear(); - m_rotated_rectangle_path.move_to({ x + rotated_rectangle_reference_coords.p1().x(), y + rotated_rectangle_reference_coords.p1().y() }); - m_rotated_rectangle_path.line_to({ x + rotated_rectangle_reference_coords.p2().x(), y + rotated_rectangle_reference_coords.p2().y() }); - m_rotated_rectangle_path.line_to({ x + rotated_rectangle_reference_coords.p3().x(), y + rotated_rectangle_reference_coords.p3().y() }); - m_rotated_rectangle_path.line_to({ x + rotated_rectangle_reference_coords.p4().x(), y + rotated_rectangle_reference_coords.p4().y() }); + // The painting only works for the positive XY quadrant (because that is easier). + // So flip things around until we're there: + bool flip_x = false; + bool flip_y = false; + if (mapped_to.x() < mapped_from.x() && mapped_to.y() < mapped_from.y()) + swap(mapped_to, mapped_from); + if ((flip_x = mapped_to.x() < mapped_from.x())) + mapped_to.set_x(2 * mapped_from.x() - mapped_to.x()); + if ((flip_y = mapped_to.y() < mapped_from.y())) + mapped_to.set_y(2 * mapped_from.y() - mapped_to.y()); - m_rotated_rectangle_path.close(); - m_underlying_painter.fill_path(m_rotated_rectangle_path, color.with_alpha(color.alpha() * c)); - } - }; - auto fractional_part = [&](float x) { return x - floorf(x); }; - auto one_minus_fractional_part = [&](float x) { return 1.0f - fractional_part(x); }; + auto delta = mapped_to - mapped_from; + auto line_angle_radians = AK::atan2(delta.y(), delta.x()) - 0.5f * AK::Pi; + float sin_inverse_angle; + float cos_inverse_angle; + AK::sincos(-line_angle_radians, sin_inverse_angle, cos_inverse_angle); - auto draw_line = [&](float x0, float y0, float x1, float y1) { - bool steep = fabsf(y1 - y0) > fabsf(x1 - x0); - - if (steep) { - swap(x0, y0); - swap(x1, y1); - } - - if (x0 > x1) { - swap(x0, x1); - swap(y0, y1); - } - - float dx = x1 - x0; - float dy = y1 - y0; - - float gradient; - if (dx == 0.0f) - gradient = 1.0f; - else - gradient = dy / dx; - - // Handle first endpoint. - int x_end = round(x0); - int y_end = y0 + gradient * (x_end - x0); - float x_gap = one_minus_fractional_part(x0 + 0.5f); - - int xpxl1 = x_end; // This will be used in the main loop. - int ypxl1 = integer_part(y_end); - - if (steep) { - plot(ypxl1, xpxl1, one_minus_fractional_part(y_end) * x_gap); - plot(ypxl1 + 1, xpxl1, fractional_part(y_end) * x_gap); - } else { - plot(xpxl1, ypxl1, one_minus_fractional_part(y_end) * x_gap); - plot(xpxl1, ypxl1 + 1, fractional_part(y_end) * x_gap); - } - - float intery = y_end + gradient; // First y-intersection for the main loop. - - // Handle second endpoint. - x_end = round(x1); - y_end = y1 + gradient * (x_end - x1); - x_gap = fractional_part(x1 + 0.5f); - int xpxl2 = x_end; // This will be used in the main loop - int ypxl2 = integer_part(y_end); - - if (steep) { - plot(ypxl2, xpxl2, one_minus_fractional_part(y_end) * x_gap); - plot(ypxl2 + 1, xpxl2, fractional_part(y_end) * x_gap); - } else { - plot(xpxl2, ypxl2, one_minus_fractional_part(y_end) * x_gap); - plot(xpxl2, ypxl2 + 1, fractional_part(y_end) * x_gap); - } - - // Main loop. - if (steep) { - for (int x = xpxl1 + 1; x <= xpxl2 - 1; ++x) { - if constexpr (policy == AntiAliasPolicy::OnlyEnds) { - plot(integer_part(intery), x, 1); - } else { - plot(integer_part(intery), x, one_minus_fractional_part(intery)); - } - plot(integer_part(intery) + 1, x, fractional_part(intery)); - intery += gradient; - } - } else { - for (int x = xpxl1 + 1; x <= xpxl2 - 1; ++x) { - if constexpr (policy == AntiAliasPolicy::OnlyEnds) { - plot(x, integer_part(intery), 1); - } else { - plot(x, integer_part(intery), one_minus_fractional_part(intery)); - } - plot(x, integer_part(intery) + 1, fractional_part(intery)); - intery += gradient; - } - } + auto inverse_rotate_point = [=](FloatPoint point) { + return Gfx::FloatPoint( + point.x() * cos_inverse_angle - point.y() * sin_inverse_angle, + point.y() * cos_inverse_angle + point.x() * sin_inverse_angle); }; - draw_line(mapped_from.x(), mapped_from.y(), mapped_to.x(), mapped_to.y()); + Gfx::FloatRect line_rect({ -(int_thickness * 255) / 2.0f, 0 }, Gfx::FloatSize(int_thickness * 255, length * 255)); + + auto gradient = delta.y() / delta.x(); + // Work out how long we need to scan along the X-axis to reach the other side of the line. + // E.g. for a vertical line this would be `thickness', in general it is this: + int scan_line_length = AK::ceil(AK::sqrt((gradient * gradient + 1) * int_thickness * int_thickness) / gradient); + + auto x_gradient = 1 / gradient; + int x_step = floorf(x_gradient); + + float x_error = 0; + float x_error_per_y = x_gradient - x_step; + + auto y_offset = int_thickness; + auto x_offset = int(x_gradient * y_offset); + int const line_start_x = mapped_from.x(); + int const line_start_y = mapped_from.y(); + int const line_end_x = mapped_to.x(); + int const line_end_y = mapped_to.y(); + + auto set_pixel = [=, this](int x, int y, Gfx::Color color) { + // FIXME: The lines seem slightly off (<= 1px) when flipped. + if (flip_x) + x = 2 * line_start_x - x; + if (flip_y) + y = 2 * line_start_y - y; + m_underlying_painter.set_pixel(x, y, color, true); + }; + + // Scan a bit extra to avoid issues from the x_error: + int const overscan = max(x_step, 1) * 2 + 1; + int x = line_start_x - x_offset; + int const center_offset = (scan_line_length + 1) / 2; + for (int y = line_start_y - y_offset; y < line_end_y + y_offset; y += 1) { + for (int i = -overscan; i < scan_line_length + overscan; i++) { + int scan_x_pos = x + i - center_offset; + // Avoid scanning over pixels definitely outside the line: + int dx = (line_start_x - int_thickness) - (scan_x_pos + 1); + if (dx > 0) { + i += dx; + continue; + } + if (line_end_x + int_thickness <= scan_x_pos - 1) + break; + auto sample = inverse_rotate_point(Gfx::FloatPoint(scan_x_pos - line_start_x, y - line_start_y)); + Gfx::FloatRect sample_px(sample * 255, Gfx::FloatSize(255, 255)); + sample_px.intersect(line_rect); + auto alpha = (sample_px.width() * sample_px.height()) / 255.0f; + alpha = (alpha * color.alpha()) / 255; + set_pixel(scan_x_pos, y, color.with_alpha(alpha)); + } + x += x_step; + x_error += x_error_per_y; + if (x_error > 1.0f) { + x_error -= 1.0f; + x += 1; + } + } } void AntiAliasingPainter::draw_aliased_line(FloatPoint const& actual_from, FloatPoint const& actual_to, Color color, float thickness, Painter::LineStyle style, Color alternate_color) @@ -697,12 +680,12 @@ void AntiAliasingPainter::fill_rect_with_rounded_corners(IntRect const& a_rect, void AntiAliasingPainter::stroke_segment_intersection(FloatPoint const& current_line_a, FloatPoint const& current_line_b, FloatLine const& previous_line, Color color, float thickness) { // starting point of the current line is where the last line ended... this is an intersection - auto mapped_intersection = m_transform.map(current_line_a); - auto mapped_current_line_b = m_transform.map(current_line_b); - auto mapped_previous_line_b = m_transform.map(previous_line.a()); + auto intersection = current_line_a; + auto previous_line_b = (previous_line.a()); // if both are straight lines we can simply draw a rectangle at the intersection if ((current_line_a.x() == current_line_b.x() || current_line_a.y() == current_line_b.y()) && (previous_line.a().x() == previous_line.b().x() || previous_line.a().y() == previous_line.b().y())) { + intersection = m_transform.map(current_line_a); // adjust coordinates to handle rounding offsets auto intersection_rect = IntSize(thickness, thickness); @@ -718,48 +701,48 @@ void AntiAliasingPainter::stroke_segment_intersection(FloatPoint const& current_ if (current_line_a.y() == current_line_b.y() && previous_line.a().y() == previous_line.b().y()) { intersection_rect.set_width(1); drawing_edge_offset = thickness == 1 ? -1 : 0; - mapped_intersection.set_x(mapped_intersection.x() - 1 + (thickness == 1 ? 1 : 0)); - mapped_intersection.set_y(mapped_intersection.y() + (thickness > 3 && fmodf(thickness, 2.0f) < 0.5f ? 1 : 0)); + intersection.set_x(intersection.x() - 1 + (thickness == 1 ? 1 : 0)); + intersection.set_y(intersection.y() + (thickness > 3 && fmodf(thickness, 2.0f) < 0.5f ? 1 : 0)); } - m_underlying_painter.fill_rect(IntRect::centered_on({ round(mapped_intersection.x()) + drawing_edge_offset, round(mapped_intersection.y()) + drawing_edge_offset }, intersection_rect), color); + m_underlying_painter.fill_rect(IntRect::centered_on({ round(intersection.x()) + drawing_edge_offset, round(intersection.y()) + drawing_edge_offset }, intersection_rect), color); return; } - float scale_to_move_current = (thickness / 2) / mapped_intersection.distance_from(mapped_current_line_b); - float scale_to_move_previous = (thickness / 2) / mapped_intersection.distance_from(mapped_previous_line_b); + float scale_to_move_current = (thickness / 2) / intersection.distance_from(current_line_b); + float scale_to_move_previous = (thickness / 2) / intersection.distance_from(previous_line_b); // move the point on the line by half of the thickness - double offset_current_edge_x = scale_to_move_current * (mapped_current_line_b.x() - mapped_intersection.x()); - double offset_current_edge_y = scale_to_move_current * (mapped_current_line_b.y() - mapped_intersection.y()); - double offset_prev_edge_x = scale_to_move_previous * (mapped_previous_line_b.x() - mapped_intersection.x()); - double offset_prev_edge_y = scale_to_move_previous * (mapped_previous_line_b.y() - mapped_intersection.y()); + double offset_current_edge_x = scale_to_move_current * (current_line_b.x() - intersection.x()); + double offset_current_edge_y = scale_to_move_current * (current_line_b.y() - intersection.y()); + double offset_prev_edge_x = scale_to_move_previous * (previous_line_b.x() - intersection.x()); + double offset_prev_edge_y = scale_to_move_previous * (previous_line_b.y() - intersection.y()); // rotate the point by 90 and 270 degrees to get the points for both edges double rad_90deg = 0.5 * M_PI; FloatPoint current_rotated_90deg = { (offset_current_edge_x * cos(rad_90deg) - offset_current_edge_y * sin(rad_90deg)), (offset_current_edge_x * sin(rad_90deg) + offset_current_edge_y * cos(rad_90deg)) }; - FloatPoint current_rotated_270deg = mapped_intersection - current_rotated_90deg; + FloatPoint current_rotated_270deg = intersection - current_rotated_90deg; FloatPoint previous_rotated_90deg = { (offset_prev_edge_x * cos(rad_90deg) - offset_prev_edge_y * sin(rad_90deg)), (offset_prev_edge_x * sin(rad_90deg) + offset_prev_edge_y * cos(rad_90deg)) }; - FloatPoint previous_rotated_270deg = mapped_intersection - previous_rotated_90deg; + FloatPoint previous_rotated_270deg = intersection - previous_rotated_90deg; // translate coordinates to the intersection point - current_rotated_90deg += mapped_intersection; - previous_rotated_90deg += mapped_intersection; + current_rotated_90deg += intersection; + previous_rotated_90deg += intersection; - FloatLine outer_line_current_90 = FloatLine({ current_rotated_90deg, mapped_current_line_b - static_cast(mapped_intersection - current_rotated_90deg) }); - FloatLine outer_line_current_270 = FloatLine({ current_rotated_270deg, mapped_current_line_b - static_cast(mapped_intersection - current_rotated_270deg) }); - FloatLine outer_line_prev_270 = FloatLine({ previous_rotated_270deg, mapped_previous_line_b - static_cast(mapped_intersection - previous_rotated_270deg) }); - FloatLine outer_line_prev_90 = FloatLine({ previous_rotated_90deg, mapped_previous_line_b - static_cast(mapped_intersection - previous_rotated_90deg) }); + FloatLine outer_line_current_90 = FloatLine({ current_rotated_90deg, current_line_b - static_cast(intersection - current_rotated_90deg) }); + FloatLine outer_line_current_270 = FloatLine({ current_rotated_270deg, current_line_b - static_cast(intersection - current_rotated_270deg) }); + FloatLine outer_line_prev_270 = FloatLine({ previous_rotated_270deg, previous_line_b - static_cast(intersection - previous_rotated_270deg) }); + FloatLine outer_line_prev_90 = FloatLine({ previous_rotated_90deg, previous_line_b - static_cast(intersection - previous_rotated_90deg) }); Optional edge_spike_90 = outer_line_current_90.intersected(outer_line_prev_270); Optional edge_spike_270; if (edge_spike_90.has_value()) { - edge_spike_270 = mapped_intersection + (mapped_intersection - edge_spike_90.value()); + edge_spike_270 = intersection + (intersection - edge_spike_90.value()); } else { edge_spike_270 = outer_line_current_270.intersected(outer_line_prev_90); if (edge_spike_270.has_value()) { - edge_spike_90 = mapped_intersection + (mapped_intersection - edge_spike_270.value()); + edge_spike_90 = intersection + (intersection - edge_spike_270.value()); } } @@ -775,7 +758,7 @@ void AntiAliasingPainter::stroke_segment_intersection(FloatPoint const& current_ m_intersection_edge_path.line_to(previous_rotated_90deg); m_intersection_edge_path.close(); - m_underlying_painter.fill_path(m_intersection_edge_path, color); + fill_path(m_intersection_edge_path, color); } // rotates a rectangle around 0,0 diff --git a/Userland/Libraries/LibGfx/AntiAliasingPainter.h b/Userland/Libraries/LibGfx/AntiAliasingPainter.h index d1ed2105e6..6f3ffef2ce 100644 --- a/Userland/Libraries/LibGfx/AntiAliasingPainter.h +++ b/Userland/Libraries/LibGfx/AntiAliasingPainter.h @@ -82,7 +82,7 @@ private: Full, }; template - void draw_anti_aliased_line(FloatPoint const&, FloatPoint const&, Color, float thickness, Painter::LineStyle style, Color alternate_color); + void draw_anti_aliased_line(FloatPoint, FloatPoint, Color, float thickness, Painter::LineStyle style, Color alternate_color); void stroke_segment_intersection(FloatPoint const& current_line_a, FloatPoint const& current_line_b, FloatLine const& previous_line, Color, float thickness); FloatQuad build_rotated_rectangle(FloatPoint const& direction, float width);