mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-21 20:15:17 +00:00
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 :^)
This commit is contained in:
parent
4d20cf57db
commit
f54b41f748
Notes:
sideshowbarker
2024-07-19 06:56:14 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/f54b41f7481 Pull-request: https://github.com/SerenityOS/serenity/pull/2129 Reviewed-by: https://github.com/awesomekling
5 changed files with 274 additions and 8 deletions
|
@ -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; }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue