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

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.
This commit is contained in:
Torstennator 2022-08-23 19:12:04 +02:00 committed by Andreas Kling
parent 7c09d7c52f
commit b2a6066042
2 changed files with 181 additions and 6 deletions

View file

@ -1,6 +1,7 @@
/* /*
* Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org> * Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
* Copyright (c) 2022, Ben Maxwell <macdue@dueutil.tech> * Copyright (c) 2022, Ben Maxwell <macdue@dueutil.tech>
* Copyright (c) 2022, Torsten Engelmann <engelTorsten@gmx.de>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -13,7 +14,7 @@
#include <AK/Function.h> #include <AK/Function.h>
#include <AK/NumericLimits.h> #include <AK/NumericLimits.h>
#include <LibGfx/AntiAliasingPainter.h> #include <LibGfx/AntiAliasingPainter.h>
#include <LibGfx/Path.h> #include <LibGfx/Line.h>
namespace Gfx { 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 corrected_thickness = thickness > 1 ? thickness - 1 : thickness;
auto size = IntSize(corrected_thickness, corrected_thickness); auto size = IntSize(corrected_thickness, corrected_thickness);
auto plot = [&](int x, int y, float c) { auto mapped_from = m_transform.map(actual_from);
m_underlying_painter.fill_rect(IntRect::centered_on({ x, y }, size), color.with_alpha(color.alpha() * c)); 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 integer_part = [](float x) { return floorf(x); };
auto round = [&](float x) { return integer_part(x + 0.5f); }; 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 fractional_part = [&](float x) { return x - floorf(x); };
auto one_minus_fractional_part = [&](float x) { return 1.0f - fractional_part(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()); 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) void AntiAliasingPainter::stroke_path(Path const& path, Color color, float thickness)
{ {
FloatPoint cursor; FloatPoint cursor;
bool previous_was_line = false;
FloatLine last_line;
Optional<FloatLine> first_line;
for (auto& segment : path.segments()) { for (auto& segment : path.segments()) {
switch (segment.type()) { switch (segment.type()) {
@ -191,7 +236,16 @@ void AntiAliasingPainter::stroke_path(Path const& path, Color color, float thick
cursor = segment.point(); cursor = segment.point();
break; break;
case Segment::Type::LineTo: case Segment::Type::LineTo:
if (!first_line.has_value())
first_line = FloatLine(cursor, segment.point());
draw_line(cursor, segment.point(), color, thickness); 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(); cursor = segment.point();
break; break;
case Segment::Type::QuadraticBezierCurveTo: { case Segment::Type::QuadraticBezierCurveTo: {
@ -214,7 +268,13 @@ void AntiAliasingPainter::stroke_path(Path const& path, Color color, float thick
cursor = segment.point(); cursor = segment.point();
break; 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) 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); 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<FloatPoint>(mapped_intersection - current_rotated_90deg) });
FloatLine outer_line_current_270 = FloatLine({ current_rotated_270deg, mapped_current_line_b - static_cast<FloatPoint>(mapped_intersection - current_rotated_270deg) });
FloatLine outer_line_prev_270 = FloatLine({ previous_rotated_270deg, mapped_previous_line_b - static_cast<FloatPoint>(mapped_intersection - previous_rotated_270deg) });
FloatLine outer_line_prev_90 = FloatLine({ previous_rotated_90deg, mapped_previous_line_b - static_cast<FloatPoint>(mapped_intersection - previous_rotated_90deg) });
Optional<FloatPoint> edge_spike_90 = outer_line_current_90.intersected(outer_line_prev_270);
Optional<FloatPoint> 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)) });
}
} }

View file

@ -7,6 +7,8 @@
#pragma once #pragma once
#include <LibGfx/Painter.h> #include <LibGfx/Painter.h>
#include <LibGfx/Path.h>
#include <LibGfx/Quad.h>
namespace Gfx { namespace Gfx {
@ -81,9 +83,13 @@ private:
}; };
template<AntiAliasPolicy policy> template<AntiAliasPolicy policy>
void draw_anti_aliased_line(FloatPoint const&, FloatPoint const&, Color, float thickness, Painter::LineStyle style, Color alternate_color); 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; Painter& m_underlying_painter;
AffineTransform m_transform; AffineTransform m_transform;
Path m_intersection_edge_path;
Path m_rotated_rectangle_path;
}; };
} }