diff options
author | AnotherTest <ali.mpfard@gmail.com> | 2020-05-06 11:55:12 +0430 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-05-06 14:50:29 +0200 |
commit | f54b41f748104e55a0ce1c5824f674247f49338c (patch) | |
tree | 5927be86565d0ccde00fded52a99c3e0b5f49ecf /Libraries/LibGfx | |
parent | 4d20cf57dbc32f470faf93b637f01432e3c65696 (diff) | |
download | serenity-f54b41f748104e55a0ce1c5824f674247f49338c.zip |
LibGfx: Implement filling paths
There are some imperfections with intersecting edges (because the main
algorithm used is scanline, and that is not geared towards drawing
complex shapes), however, it behaves mostly fine for normal use :^)
Diffstat (limited to 'Libraries/LibGfx')
-rw-r--r-- | Libraries/LibGfx/FloatPoint.h | 9 | ||||
-rw-r--r-- | Libraries/LibGfx/Painter.cpp | 179 | ||||
-rw-r--r-- | Libraries/LibGfx/Painter.h | 9 | ||||
-rw-r--r-- | Libraries/LibGfx/Path.cpp | 61 | ||||
-rw-r--r-- | Libraries/LibGfx/Path.h | 24 |
5 files changed, 274 insertions, 8 deletions
diff --git a/Libraries/LibGfx/FloatPoint.h b/Libraries/LibGfx/FloatPoint.h index 766197d967..9867d8327d 100644 --- a/Libraries/LibGfx/FloatPoint.h +++ b/Libraries/LibGfx/FloatPoint.h @@ -37,7 +37,7 @@ class FloatRect; class FloatPoint { public: - FloatPoint() {} + FloatPoint() { } FloatPoint(float x, float y) : m_x(x) , m_y(y) @@ -112,6 +112,13 @@ public: } FloatPoint operator+(const FloatPoint& other) const { return { m_x + other.m_x, m_y + other.m_y }; } + FloatPoint& operator/=(float factor) + { + m_x /= factor; + m_y /= factor; + return *this; + } + String to_string() const { return String::format("[%g,%g]", x(), y()); } bool is_null() const { return !m_x && !m_y; } diff --git a/Libraries/LibGfx/Painter.cpp b/Libraries/LibGfx/Painter.cpp index 85c21d0121..549070b107 100644 --- a/Libraries/LibGfx/Painter.cpp +++ b/Libraries/LibGfx/Painter.cpp @@ -31,6 +31,7 @@ #include <AK/Assertions.h> #include <AK/Function.h> #include <AK/Memory.h> +#include <AK/QuickSort.h> #include <AK/StdLibExtras.h> #include <AK/StringBuilder.h> #include <AK/Utf8View.h> @@ -1049,7 +1050,7 @@ void Painter::draw_line(const Point& p1, const Point& p2, Color color, int thick } } -static void draw_split_quadratic_bezier_curve(Painter& painter, const Point& original_control, const Point& p1, const Point& p2, Color color, int thickness, bool dotted) +static void split_quadratic_bezier_curve(const FloatPoint& original_control, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>& callback) { auto po1_midpoint = original_control + p1; po1_midpoint /= 2; @@ -1060,11 +1061,11 @@ static void draw_split_quadratic_bezier_curve(Painter& painter, const Point& ori auto new_segment = po1_midpoint + po2_midpoint; new_segment /= 2; - painter.draw_quadratic_bezier_curve(po1_midpoint, p1, new_segment, color, thickness, dotted); - painter.draw_quadratic_bezier_curve(po2_midpoint, new_segment, p2, color, thickness, dotted); + Painter::for_each_line_segment_on_bezier_curve(po1_midpoint, p1, new_segment, callback); + Painter::for_each_line_segment_on_bezier_curve(po2_midpoint, new_segment, p2, callback); } -static bool can_approximate_bezier_curve(const Point& p1, const Point& p2, const Point& control) +static bool can_approximate_bezier_curve(const FloatPoint& p1, const FloatPoint& p2, const FloatPoint& control) { constexpr static int tolerance = 15; @@ -1081,15 +1082,27 @@ static bool can_approximate_bezier_curve(const Point& p1, const Point& p2, const return max(p1x, p2x) + max(p1y, p2y) <= tolerance; } -void Painter::draw_quadratic_bezier_curve(const Point& control_point, const Point& p1, const Point& p2, Color color, int thickness, bool dotted) +void Painter::for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>& callback) { if (can_approximate_bezier_curve(p1, p2, control_point)) { - draw_line(p1, p2, color, thickness, dotted); + callback(p1, p2); } else { - draw_split_quadratic_bezier_curve(*this, control_point, p1, p2, color, thickness, dotted); + split_quadratic_bezier_curve(control_point, p1, p2, callback); } } +void Painter::for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>&& callback) +{ + for_each_line_segment_on_bezier_curve(control_point, p1, p2, callback); +} + +void Painter::draw_quadratic_bezier_curve(const Point& control_point, const Point& p1, const Point& p2, Color color, int thickness, bool dotted) +{ + for_each_line_segment_on_bezier_curve(FloatPoint(control_point.x(), control_point.y()), FloatPoint(p1.x(), p1.y()), FloatPoint(p2.x(), p2.y()), [&](const FloatPoint& p1, const FloatPoint& p2) { + draw_line(Point(p1.x(), p1.y()), Point(p2.x(), p2.y()), color, thickness, dotted); + }); +} + void Painter::add_clip_rect(const Rect& rect) { state().clip_rect.intersect(rect.translated(m_clip_origin.location())); @@ -1137,4 +1150,156 @@ void Painter::stroke_path(const Path& path, Color color, int thickness) } } +//#define FILL_PATH_DEBUG + +void Painter::fill_path(Path& path, Color color, WindingRule winding_rule) +{ + const auto& segments = path.split_lines(); + + if (segments.size() == 0) + return; + + Vector<Path::LineSegment> active_list; + active_list.ensure_capacity(segments.size()); + + // first, grab the segments for the very first scanline + auto first_y = segments.first().maximum_y; + auto last_y = segments.last().minimum_y; + auto scanline = first_y; + + size_t last_active_segment { 0 }; + + for (auto& segment : segments) { + if (segment.maximum_y != scanline) + break; + active_list.append(segment); + ++last_active_segment; + } + + auto is_inside_shape = [winding_rule](int winding_number) { + if (winding_rule == WindingRule::Nonzero) + return winding_number != 0; + + if (winding_rule == WindingRule::EvenOdd) + return winding_number % 2 == 0; + + ASSERT_NOT_REACHED(); + }; + + auto increment_winding = [winding_rule](int& winding_number, const Point& from, const Point& to) { + if (winding_rule == WindingRule::EvenOdd) { + ++winding_number; + return; + } + + if (winding_rule == WindingRule::Nonzero) { + if (from.dy_relative_to(to) < 0) + ++winding_number; + else + --winding_number; + return; + } + + ASSERT_NOT_REACHED(); + }; + + while (scanline >= last_y) { + if (active_list.size()) { + // sort the active list by 'x' from right to left + quick_sort(active_list, [](const auto& line0, const auto& line1) { + return line1.x < line0.x; + }); +#ifdef FILL_PATH_DEBUG + if ((int)scanline % 10 == 0) { + draw_text(Rect(active_list.last().x - 20, scanline, 20, 10), String::format("%d", (int)scanline)); + } +#endif + + if (active_list.size() > 1) { + auto winding_number { 0 }; + for (size_t i = 1; i < active_list.size(); ++i) { + auto& previous = active_list[i - 1]; + auto& current = active_list[i]; + + int int_distance = fabs(current.x - previous.x); + Point from(previous.x, scanline); + Point to(current.x, scanline); + + if (int_distance < 1) { + // the two lines intersect on an int grid + // so they should both be treated as a single line segment + goto skip_drawing; + } + + if (int_distance == 1 && is_inside_shape(winding_number)) { + // The two lines form a singluar edge for the shape + // while they do not intersect, they connect together + goto skip_drawing; + } + + if (is_inside_shape(winding_number)) { + // The points between this segment and the previous are + // inside the shape +#ifdef FILL_PATH_DEBUG + dbg() << "y=" << scanline << ": " << winding_number << " at " << i << ": " << from << " -- " << to; +#endif + draw_line(from, to, color, 1, false); + } + + skip_drawing:; + + auto is_passing_through_maxima = scanline == previous.maximum_y + || scanline == previous.minimum_y + || scanline == current.maximum_y + || scanline == current.minimum_y; + + auto is_passing_through_vertex = false; + + if (is_passing_through_maxima) { + is_passing_through_vertex = previous.x == current.x; + } + + if (!is_passing_through_vertex || previous.inverse_slope * current.inverse_slope < 0) + increment_winding(winding_number, from, to); + + // update the x coord + active_list[i - 1].x -= active_list[i - 1].inverse_slope; + } + active_list.last().x -= active_list.last().inverse_slope; + } else { + auto point = Point(active_list[0].x, scanline); + draw_line(point, point, color); + + // update the x coord + active_list.first().x -= active_list.first().inverse_slope; + } + } + + --scanline; + // remove any edge that goes out of bound from the active list + for (size_t i = 0, count = active_list.size(); i < count; ++i) { + if (scanline <= active_list[i].minimum_y) { + active_list.remove(i); + --count; + --i; + } + } + for (size_t j = last_active_segment; j < segments.size(); ++j, ++last_active_segment) { + auto& segment = segments[j]; + if (segment.maximum_y < scanline) + break; + if (segment.minimum_y >= scanline) + continue; + + active_list.append(segment); + } + } + +#ifdef FILL_PATH_DEBUG + size_t i { 0 }; + for (auto& segment : segments) + draw_line(segment.from, segment.to, Color::from_hsv(++i / segments.size() * 255, 255, 255), 1); +#endif +} + } diff --git a/Libraries/LibGfx/Painter.h b/Libraries/LibGfx/Painter.h index 97fe226eeb..87ecf8b045 100644 --- a/Libraries/LibGfx/Painter.h +++ b/Libraries/LibGfx/Painter.h @@ -71,8 +71,17 @@ public: void draw_emoji(const Point&, const Gfx::Bitmap&, const Font&); void draw_glyph_or_emoji(const Point&, u32 codepoint, const Font&, Color); + static void for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>&); + static void for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>&&); + void stroke_path(const Path&, Color, int thickness); + enum class WindingRule { + Nonzero, + EvenOdd, + }; + void fill_path(Path&, Color, WindingRule rule = WindingRule::Nonzero); + const Font& font() const { return *state().font; } void set_font(const Font& font) { state().font = &font; } diff --git a/Libraries/LibGfx/Path.cpp b/Libraries/LibGfx/Path.cpp index fa06b904f7..2bf990d6fe 100644 --- a/Libraries/LibGfx/Path.cpp +++ b/Libraries/LibGfx/Path.cpp @@ -24,7 +24,10 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include <AK/Function.h> +#include <AK/QuickSort.h> #include <AK/StringBuilder.h> +#include <LibGfx/Painter.h> #include <LibGfx/Path.h> namespace Gfx { @@ -34,6 +37,8 @@ void Path::close() if (m_segments.size() <= 1) return; + invalidate_split_lines(); + auto& last_point = m_segments.last().point; for (ssize_t i = m_segments.size() - 1; i >= 0; --i) { @@ -80,4 +85,60 @@ String Path::to_string() const return builder.to_string(); } +void Path::segmentize_path() +{ + Vector<LineSegment> segments; + + auto add_line = [&](const auto& p0, const auto& p1) { + if (p0.y() == p1.y()) + return; // horizontal lines are not needed (there's nothing to fill inside) + float ymax = p0.y(), ymin = p1.y(), x_of_ymin = p1.x(), x_of_ymax = p0.x(); + auto slope = p0.x() == p1.x() ? 0 : ((float)(p0.y() - p1.y())) / ((float)(p0.x() - p1.x())); + if (p0.y() < p1.y()) { + ymin = ymax; + ymax = p1.y(); + x_of_ymax = x_of_ymin; + x_of_ymin = p0.x(); + } + + segments.append({ Point(p0.x(), p0.y()), + Point(p1.x(), p1.y()), + slope == 0 ? 0 : 1 / slope, + x_of_ymin, + ymax, ymin, x_of_ymax }); + }; + + FloatPoint cursor { 0, 0 }; + for (auto& segment : m_segments) { + switch (segment.type) { + case Segment::Type::MoveTo: + cursor = segment.point; + break; + case Segment::Type::LineTo: { + add_line(cursor, segment.point); + cursor = segment.point; + break; + } + case Segment::Type::QuadraticBezierCurveTo: { + auto& control = segment.through.value(); + Painter::for_each_line_segment_on_bezier_curve(control, cursor, segment.point, [&](const FloatPoint& p0, const FloatPoint& p1) { + add_line(Point(p0.x(), p0.y()), Point(p1.x(), p1.y())); + }); + cursor = segment.point; + break; + } + case Segment::Type::Invalid: + ASSERT_NOT_REACHED(); + break; + } + } + + // sort segments by ymax + quick_sort(segments, [](const auto& line0, const auto& line1) { + return line1.maximum_y < line0.maximum_y; + }); + + m_split_lines = move(segments); +} + } diff --git a/Libraries/LibGfx/Path.h b/Libraries/LibGfx/Path.h index 43f773a819..df4c2d25a4 100644 --- a/Libraries/LibGfx/Path.h +++ b/Libraries/LibGfx/Path.h @@ -57,21 +57,45 @@ public: void line_to(const FloatPoint& point) { m_segments.append({ Segment::Type::LineTo, point }); + invalidate_split_lines(); } void quadratic_bezier_curve_to(const FloatPoint& through, const FloatPoint& point) { m_segments.append({ Segment::Type::QuadraticBezierCurveTo, point, through }); + invalidate_split_lines(); } void close(); const Vector<Segment>& segments() const { return m_segments; } + const auto& split_lines() + { + if (m_split_lines.has_value()) + return m_split_lines.value(); + segmentize_path(); + ASSERT(m_split_lines.has_value()); + return m_split_lines.value(); + } String to_string() const; + struct LineSegment { + Point from, to; + float inverse_slope; + float x_of_minimum_y; + float maximum_y; + float minimum_y; + float x; + }; + private: + void invalidate_split_lines() { m_split_lines.clear(); } + void segmentize_path(); + Vector<Segment> m_segments; + + Optional<Vector<LineSegment>> m_split_lines {}; }; inline const LogStream& operator<<(const LogStream& stream, const Path& path) |