From b2a60660426012373ce5d97d08b93444bc39db53 Mon Sep 17 00:00:00 2001 From: Torstennator Date: Tue, 23 Aug 2022 19:12:04 +0200 Subject: [PATCH] LibGfx: Optimize anti-aliased line drawing and stroking This patch optimizes the drawing of aa-lines by rotating the drawn rectangle to the direction where the line points. That enables us to draw non-straight lines with the proper width. If a aa-line is drawn that is infact a straigt line we now dont plot those lines with multipe rectangles across the line - insted we draw straight lines in one go with just one rectangle of proper size. Stroking of lines has been enhanced to take care of the edges between two lines with drawing the stroke till the intersections of two connected lines. --- .../Libraries/LibGfx/AntiAliasingPainter.cpp | 181 +++++++++++++++++- .../Libraries/LibGfx/AntiAliasingPainter.h | 6 + 2 files changed, 181 insertions(+), 6 deletions(-) diff --git a/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp b/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp index 45eb0e4ee6..4c96620c32 100644 --- a/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp +++ b/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2021, Ali Mohammad Pur * Copyright (c) 2022, Ben Maxwell + * Copyright (c) 2022, Torsten Engelmann * * SPDX-License-Identifier: BSD-2-Clause */ @@ -13,7 +14,7 @@ #include #include #include -#include +#include namespace Gfx { @@ -27,12 +28,55 @@ void AntiAliasingPainter::draw_anti_aliased_line(FloatPoint const& actual_from, auto corrected_thickness = thickness > 1 ? thickness - 1 : thickness; auto size = IntSize(corrected_thickness, corrected_thickness); - auto plot = [&](int x, int y, float c) { - m_underlying_painter.fill_rect(IntRect::centered_on({ x, y }, size), color.with_alpha(color.alpha() * c)); - }; + 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 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; + } + + rotated_rectangle_reference_coords = build_rotated_rectangle(draw_direction, corrected_thickness); + + 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() }); + + 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); }; @@ -115,8 +159,6 @@ void AntiAliasingPainter::draw_anti_aliased_line(FloatPoint const& actual_from, } }; - auto mapped_from = m_transform.map(actual_from); - auto mapped_to = m_transform.map(actual_to); draw_line(mapped_from.x(), mapped_from.y(), mapped_to.x(), mapped_to.y()); } @@ -182,6 +224,9 @@ void AntiAliasingPainter::fill_path(Path& path, Color color, Painter::WindingRul void AntiAliasingPainter::stroke_path(Path const& path, Color color, float thickness) { FloatPoint cursor; + bool previous_was_line = false; + FloatLine last_line; + Optional first_line; for (auto& segment : path.segments()) { switch (segment.type()) { @@ -191,7 +236,16 @@ void AntiAliasingPainter::stroke_path(Path const& path, Color color, float thick cursor = segment.point(); break; case Segment::Type::LineTo: + if (!first_line.has_value()) + first_line = FloatLine(cursor, segment.point()); + draw_line(cursor, segment.point(), color, thickness); + if (previous_was_line) { + stroke_segment_intersection(cursor, segment.point(), last_line, color, thickness); + } + + last_line.set_a(cursor); + last_line.set_b(segment.point()); cursor = segment.point(); break; case Segment::Type::QuadraticBezierCurveTo: { @@ -214,7 +268,13 @@ void AntiAliasingPainter::stroke_path(Path const& path, Color color, float thick cursor = segment.point(); break; } + + previous_was_line = segment.type() == Segment::Type::LineTo; } + + // check if the figure was started and closed as line at the same position + if (previous_was_line && path.segments().size() >= 2 && path.segments().first().point() == cursor && (path.segments().first().type() == Segment::Type::LineTo || (path.segments().first().type() == Segment::Type::MoveTo && path.segments()[1].type() == Segment::Type::LineTo))) + stroke_segment_intersection(first_line.value().a(), first_line.value().b(), last_line, color, thickness); } void AntiAliasingPainter::draw_elliptical_arc(FloatPoint const& p1, FloatPoint const& p2, FloatPoint const& center, FloatPoint const& radii, float x_axis_rotation, float theta_1, float theta_delta, Color color, float thickness, Painter::LineStyle style) @@ -634,4 +694,113 @@ void AntiAliasingPainter::fill_rect_with_rounded_corners(IntRect const& a_rect, fill_corner(bottom_right_corner, bounding_rect.bottom_right(), bottom_right); } +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()); + + // 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())) { + + // adjust coordinates to handle rounding offsets + auto intersection_rect = IntSize(thickness, thickness); + float drawing_edge_offset = fmodf(thickness, 2.0f) < 0.5f && thickness > 3 ? 1 : 0; + auto integer_part = [](float x) { return floorf(x); }; + auto round = [&](float x) { return integer_part(x + 0.5f); }; + + if (thickness == 1) + drawing_edge_offset = -1; + if (current_line_a.x() == current_line_b.x() && previous_line.a().x() == previous_line.b().x()) { + intersection_rect.set_height(1); + } + 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)); + } + + 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); + 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); + + // 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()); + + // 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 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; + + // translate coordinates to the intersection point + current_rotated_90deg += mapped_intersection; + previous_rotated_90deg += mapped_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) }); + + 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()); + } 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()); + } + } + + m_intersection_edge_path.clear(); + m_intersection_edge_path.move_to(current_rotated_90deg); + if (edge_spike_90.has_value()) + m_intersection_edge_path.line_to(edge_spike_90.value()); + m_intersection_edge_path.line_to(previous_rotated_270deg); + + m_intersection_edge_path.line_to(current_rotated_270deg); + if (edge_spike_270.has_value()) + m_intersection_edge_path.line_to(edge_spike_270.value()); + m_intersection_edge_path.line_to(previous_rotated_90deg); + m_intersection_edge_path.close(); + + m_underlying_painter.fill_path(m_intersection_edge_path, color); +} + +// rotates a rectangle around 0,0 +FloatQuad AntiAliasingPainter::build_rotated_rectangle(FloatPoint const& direction, float width) +{ + double half_size = width / 2; + double radian = atan2(direction.y(), direction.x()); + if (radian < 0) { + radian += (2 * M_PI); + } + // rotated by: (xcosθ−ysinθ ,xsinθ+ycosθ) + // p1 p2 + // + // x,y + // + // p4 p3 + double cos_radian = cos(radian); + double sin_radian = sin(radian); + + // FIXME: Performing the rotation with AffineTransform::rotate_quad seems to generate more glitches at the edges than rotating manually + return FloatQuad( + { ((-half_size * cos_radian) - (-half_size * sin_radian)), ((-half_size * sin_radian) + (-half_size * cos_radian)) }, + { ((half_size * cos_radian) - (-half_size * sin_radian)), ((half_size * sin_radian) + (-half_size * cos_radian)) }, + { ((half_size * cos_radian)) - (half_size * sin_radian), ((half_size * sin_radian) + (half_size * cos_radian)) }, + { ((-half_size * cos_radian) - (half_size * sin_radian)), ((-half_size * sin_radian) + (half_size * cos_radian)) }); +} + } diff --git a/Userland/Libraries/LibGfx/AntiAliasingPainter.h b/Userland/Libraries/LibGfx/AntiAliasingPainter.h index 86ff9bb6c2..d1ed2105e6 100644 --- a/Userland/Libraries/LibGfx/AntiAliasingPainter.h +++ b/Userland/Libraries/LibGfx/AntiAliasingPainter.h @@ -7,6 +7,8 @@ #pragma once #include +#include +#include namespace Gfx { @@ -81,9 +83,13 @@ private: }; template void draw_anti_aliased_line(FloatPoint const&, FloatPoint const&, 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); Painter& m_underlying_painter; AffineTransform m_transform; + Path m_intersection_edge_path; + Path m_rotated_rectangle_path; }; }