mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 07:07:34 +00:00
LibGfx: Implement AntiAliasingPainter::draw_ellipse()
This commit adds draw_ellipse() and moves the shared code for circles and ellipses to draw_ellipse_part(). draw_ellipse_part() can draw an entire circle in one call using 8-way symmetry and an ellipse in two calls using 4-way symmetry.
This commit is contained in:
parent
6e52e6b554
commit
60aba4c9f3
3 changed files with 113 additions and 36 deletions
|
@ -18,6 +18,7 @@
|
||||||
#include <LibGUI/TextBox.h>
|
#include <LibGUI/TextBox.h>
|
||||||
#include <LibGUI/ValueSlider.h>
|
#include <LibGUI/ValueSlider.h>
|
||||||
#include <LibGfx/Rect.h>
|
#include <LibGfx/Rect.h>
|
||||||
|
#include <LibGfx/AntiAliasingPainter.h>
|
||||||
|
|
||||||
namespace PixelPaint {
|
namespace PixelPaint {
|
||||||
|
|
||||||
|
@ -35,9 +36,11 @@ void EllipseTool::draw_using(GUI::Painter& painter, Gfx::IntPoint const& start_p
|
||||||
case FillMode::Outline:
|
case FillMode::Outline:
|
||||||
painter.draw_ellipse_intersecting(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button), thickness);
|
painter.draw_ellipse_intersecting(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button), thickness);
|
||||||
break;
|
break;
|
||||||
case FillMode::Fill:
|
case FillMode::Fill: {
|
||||||
painter.fill_ellipse(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button));
|
Gfx::AntiAliasingPainter aa_painter { painter };
|
||||||
|
aa_painter.draw_ellipse(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button));
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
VERIFY_NOT_REACHED();
|
VERIFY_NOT_REACHED();
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
#include "FillPathImplementation.h"
|
#include "FillPathImplementation.h"
|
||||||
#include <AK/Function.h>
|
#include <AK/Function.h>
|
||||||
|
#include <AK/NumericLimits.h>
|
||||||
#include <LibGfx/AntiAliasingPainter.h>
|
#include <LibGfx/AntiAliasingPainter.h>
|
||||||
#include <LibGfx/Path.h>
|
#include <LibGfx/Path.h>
|
||||||
|
|
||||||
|
@ -191,44 +192,71 @@ void Gfx::AntiAliasingPainter::draw_cubic_bezier_curve(FloatPoint const& control
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Gfx::AntiAliasingPainter::draw_circle(IntPoint center, int radius, Color color)
|
void Gfx::AntiAliasingPainter::draw_circle(IntPoint const& center, int radius, Color color)
|
||||||
|
{
|
||||||
|
if (radius <= 0)
|
||||||
|
return;
|
||||||
|
draw_ellipse_part(center, radius, radius, color, false, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gfx::AntiAliasingPainter::draw_ellipse(IntRect const& a_rect, Color color)
|
||||||
|
{
|
||||||
|
auto center = a_rect.center();
|
||||||
|
auto radius_a = a_rect.width() / 2;
|
||||||
|
auto radius_b = a_rect.height() / 2;
|
||||||
|
if (radius_a <= 0 || radius_b <= 0)
|
||||||
|
return;
|
||||||
|
if (radius_a == radius_b)
|
||||||
|
return draw_circle(center, radius_a, color);
|
||||||
|
auto x_paint_range = draw_ellipse_part(center, radius_a, radius_b, color, false, {});
|
||||||
|
// FIXME: This paints some extra fill pixels that are clipped
|
||||||
|
draw_ellipse_part(center, radius_b, radius_a, color, true, x_paint_range);
|
||||||
|
}
|
||||||
|
|
||||||
|
Gfx::AntiAliasingPainter::Range Gfx::AntiAliasingPainter::draw_ellipse_part(
|
||||||
|
IntPoint center, int radius_a, int radius_b, Color color, bool flip_x_and_y, Optional<Range> x_clip)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
Algorithm from: https://cs.uwaterloo.ca/research/tr/1984/CS-84-38.pdf
|
Algorithm from: https://cs.uwaterloo.ca/research/tr/1984/CS-84-38.pdf
|
||||||
Inline comments are from the paper.
|
|
||||||
|
This method can draw a whole circle with a whole circle in one call using
|
||||||
|
8-way symmetry, or an ellipse in two calls using 4-way symmetry.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
center *= m_underlying_painter.scale();
|
center *= m_underlying_painter.scale();
|
||||||
radius *= m_underlying_painter.scale();
|
radius_a *= m_underlying_painter.scale();
|
||||||
|
radius_b *= m_underlying_painter.scale();
|
||||||
|
|
||||||
// TODO: Generalize to ellipses (see paper)
|
// If this is a ellipse everything can be drawn in one pass with 8 way symmetry
|
||||||
|
bool const is_circle = radius_a == radius_b;
|
||||||
|
|
||||||
// These happen to be the same here, but are treated separately in the paper:
|
// These happen to be the same here, but are treated separately in the paper:
|
||||||
// intensity is the fill alpha
|
// intensity is the fill alpha
|
||||||
int const intensity = color.alpha();
|
int const intensity = 255;
|
||||||
// 0 to subpixel_resolution is the range of alpha values for the circle edges
|
// 0 to subpixel_resolution is the range of alpha values for the circle edges
|
||||||
int const subpixel_resolution = intensity;
|
int const subpixel_resolution = intensity;
|
||||||
|
|
||||||
// Note: Variable names below are based off the paper
|
|
||||||
|
|
||||||
// Current pixel address
|
// Current pixel address
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int q = radius;
|
int q = radius_b;
|
||||||
|
|
||||||
// 1st and 2nd order differences of y
|
// 1st and 2nd order differences of y
|
||||||
int delta_y = 0;
|
int delta_y = 0;
|
||||||
int delta2_y = 0;
|
int delta2_y = 0;
|
||||||
|
|
||||||
// Exact and predicted values of f(i) -- the circle equation scaled by subpixel_resolution
|
int const a_squared = radius_a * radius_a;
|
||||||
int y = subpixel_resolution * radius;
|
int const b_squared = radius_b * radius_b;
|
||||||
|
|
||||||
|
// Exact and predicted values of f(i) -- the ellipse equation scaled by subpixel_resolution
|
||||||
|
int y = subpixel_resolution * radius_b;
|
||||||
int y_hat = 0;
|
int y_hat = 0;
|
||||||
|
|
||||||
// The value of f(i)*f(i)
|
// The value of f(i)*f(i)
|
||||||
int f_squared = y * y;
|
int f_squared = y * y;
|
||||||
|
|
||||||
// 1st and 2nd order differences of f(i)*f(i)
|
// 1st and 2nd order differences of f(i)*f(i)
|
||||||
int delta_f_squared = subpixel_resolution * subpixel_resolution;
|
int delta_f_squared = -(static_cast<int64_t>(b_squared) * subpixel_resolution * subpixel_resolution) / a_squared;
|
||||||
int delta2_f_squared = -delta_f_squared - delta_f_squared;
|
int delta2_f_squared = 2 * delta_f_squared;
|
||||||
|
|
||||||
// edge_intersection_area/subpixel_resolution = percentage of pixel intersected by circle
|
// edge_intersection_area/subpixel_resolution = percentage of pixel intersected by circle
|
||||||
// (aka the alpha for the pixel)
|
// (aka the alpha for the pixel)
|
||||||
|
@ -270,15 +298,32 @@ void Gfx::AntiAliasingPainter::draw_circle(IntPoint center, int radius, Color co
|
||||||
|
|
||||||
auto correct = [&] {
|
auto correct = [&] {
|
||||||
int error = y - y_hat;
|
int error = y - y_hat;
|
||||||
|
|
||||||
|
if (!is_circle) {
|
||||||
|
// FIXME: For ellipses the alpha values seem too low, which
|
||||||
|
// can make them look overly pointy. This fixes that, though there's
|
||||||
|
// probably a better solution to be found.
|
||||||
|
// (This issue seems to exist in the base algorithm)
|
||||||
|
error /= 4;
|
||||||
|
}
|
||||||
|
|
||||||
delta2_y += error;
|
delta2_y += error;
|
||||||
delta_y += error;
|
delta_y += error;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
int min_paint_x = NumericLimits<int>::max();
|
||||||
|
int max_paint_x = NumericLimits<int>::min();
|
||||||
auto pixel = [&](int x, int y, int alpha) {
|
auto pixel = [&](int x, int y, int alpha) {
|
||||||
if (alpha <= 0 || alpha > 255)
|
if (alpha <= 0 || alpha > 255)
|
||||||
return;
|
return;
|
||||||
|
if (flip_x_and_y)
|
||||||
|
swap(x, y);
|
||||||
|
if (x_clip.has_value() && x_clip->contains_inclusive(x))
|
||||||
|
return;
|
||||||
|
min_paint_x = min(x, min_paint_x);
|
||||||
|
max_paint_x = max(x, max_paint_x);
|
||||||
auto pixel_colour = color;
|
auto pixel_colour = color;
|
||||||
pixel_colour.set_alpha(alpha);
|
pixel_colour.set_alpha((alpha * color.alpha()) / 255);
|
||||||
m_underlying_painter.set_pixel(center + IntPoint { x, y }, pixel_colour, true);
|
m_underlying_painter.set_pixel(center + IntPoint { x, y }, pixel_colour, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -289,18 +334,30 @@ void Gfx::AntiAliasingPainter::draw_circle(IntPoint center, int radius, Color co
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto eight_pixel = [&](int x, int y, int alpha) {
|
auto symmetric_pixel = [&](int x, int y, int alpha) {
|
||||||
pixel(x, y, alpha);
|
pixel(x, y, alpha);
|
||||||
pixel(x, -y - 1, alpha);
|
pixel(x, -y - 1, alpha);
|
||||||
pixel(-x - 1, -y - 1, alpha);
|
pixel(-x - 1, -y - 1, alpha);
|
||||||
pixel(-x - 1, y, alpha);
|
pixel(-x - 1, y, alpha);
|
||||||
pixel(y, x, alpha);
|
if (is_circle) {
|
||||||
pixel(y, -x - 1, alpha);
|
pixel(y, x, alpha);
|
||||||
pixel(-y - 1, -x - 1, alpha);
|
pixel(y, -x - 1, alpha);
|
||||||
pixel(-y - 1, x, alpha);
|
pixel(-y - 1, -x - 1, alpha);
|
||||||
|
pixel(-y - 1, x, alpha);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
while (i < q) {
|
// These are calculated incrementally (as it is possibly a tiny bit faster)
|
||||||
|
int ib_squared = 0;
|
||||||
|
int qa_squared = q * a_squared;
|
||||||
|
|
||||||
|
auto in_symmetric_region = [&] {
|
||||||
|
// Main fix two stop cond here
|
||||||
|
return is_circle ? i < q : ib_squared < qa_squared;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draws a 8 octants for a circle or 4 quadrants for a (partial) elipse
|
||||||
|
while (in_symmetric_region()) {
|
||||||
predict();
|
predict();
|
||||||
minimize();
|
minimize();
|
||||||
correct();
|
correct();
|
||||||
|
@ -308,35 +365,40 @@ void Gfx::AntiAliasingPainter::draw_circle(IntPoint center, int radius, Color co
|
||||||
edge_intersection_area += delta_y;
|
edge_intersection_area += delta_y;
|
||||||
if (edge_intersection_area >= 0) {
|
if (edge_intersection_area >= 0) {
|
||||||
// Single pixel on perimeter
|
// Single pixel on perimeter
|
||||||
eight_pixel(i, q, (edge_intersection_area + old_area) / 2);
|
symmetric_pixel(i, q, (edge_intersection_area + old_area) / 2);
|
||||||
fill(i, q - 1, -q, intensity);
|
fill(i, q - 1, -q, intensity);
|
||||||
fill(-i - 1, q - 1, -q, intensity);
|
fill(-i - 1, q - 1, -q, intensity);
|
||||||
} else {
|
} else {
|
||||||
// Two pixels on perimeter
|
// Two pixels on perimeter
|
||||||
edge_intersection_area += subpixel_resolution;
|
edge_intersection_area += subpixel_resolution;
|
||||||
eight_pixel(i, q, old_area / 2);
|
symmetric_pixel(i, q, old_area / 2);
|
||||||
q -= 1;
|
q -= 1;
|
||||||
|
qa_squared -= a_squared;
|
||||||
fill(i, q - 1, -q, intensity);
|
fill(i, q - 1, -q, intensity);
|
||||||
fill(-i - 1, q - 1, -q, intensity);
|
fill(-i - 1, q - 1, -q, intensity);
|
||||||
if (i < q) {
|
if (!is_circle || in_symmetric_region()) {
|
||||||
// Haven't gone below the diagonal
|
symmetric_pixel(i, q, (edge_intersection_area + subpixel_resolution) / 2);
|
||||||
eight_pixel(i, q, (edge_intersection_area + subpixel_resolution) / 2);
|
if (is_circle) {
|
||||||
fill(q, i - 1, -i, intensity);
|
fill(q, i - 1, -i, intensity);
|
||||||
fill(-q - 1, i - 1, -i, intensity);
|
fill(-q - 1, i - 1, -i, intensity);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Went below the diagonal, fix edge_intersection_area for final pixels
|
|
||||||
edge_intersection_area += subpixel_resolution;
|
edge_intersection_area += subpixel_resolution;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
|
ib_squared += b_squared;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill in 4 remaning pixels
|
if (is_circle) {
|
||||||
int alpha = edge_intersection_area / 2;
|
int alpha = edge_intersection_area / 2;
|
||||||
pixel(q, q, alpha);
|
pixel(q, q, alpha);
|
||||||
pixel(-q - 1, q, alpha);
|
pixel(-q - 1, q, alpha);
|
||||||
pixel(-q - 1, -q - 1, alpha);
|
pixel(-q - 1, -q - 1, alpha);
|
||||||
pixel(q, -q - 1, alpha);
|
pixel(q, -q - 1, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Range { min_paint_x, max_paint_x };
|
||||||
}
|
}
|
||||||
|
|
||||||
void Gfx::AntiAliasingPainter::fill_rect_with_rounded_corners(IntRect const& a_rect, Color color, int radius)
|
void Gfx::AntiAliasingPainter::fill_rect_with_rounded_corners(IntRect const& a_rect, Color color, int radius)
|
||||||
|
|
|
@ -28,11 +28,23 @@ public:
|
||||||
void translate(float dx, float dy) { m_transform.translate(dx, dy); }
|
void translate(float dx, float dy) { m_transform.translate(dx, dy); }
|
||||||
void translate(FloatPoint const& delta) { m_transform.translate(delta); }
|
void translate(FloatPoint const& delta) { m_transform.translate(delta); }
|
||||||
|
|
||||||
void draw_circle(IntPoint center, int radius, Color);
|
void draw_circle(IntPoint const& center, int radius, Color);
|
||||||
|
void draw_ellipse(IntRect const& a_rect, Color);
|
||||||
void fill_rect_with_rounded_corners(IntRect const&, Color, int radius);
|
void fill_rect_with_rounded_corners(IntRect const&, Color, int radius);
|
||||||
void fill_rect_with_rounded_corners(IntRect const&, Color, int top_left_radius, int top_right_radius, int bottom_right_radius, int bottom_left_radius);
|
void fill_rect_with_rounded_corners(IntRect const&, Color, int top_left_radius, int top_right_radius, int bottom_right_radius, int bottom_left_radius);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct Range {
|
||||||
|
int min;
|
||||||
|
int max;
|
||||||
|
|
||||||
|
inline bool contains_inclusive(int n) const
|
||||||
|
{
|
||||||
|
return n >= min && n <= max;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Range draw_ellipse_part(IntPoint a_rect, int radius_a, int radius_b, Color, bool flip_x_and_y, Optional<Range> x_clip);
|
||||||
|
|
||||||
enum class AntiAliasPolicy {
|
enum class AntiAliasPolicy {
|
||||||
OnlyEnds,
|
OnlyEnds,
|
||||||
Full,
|
Full,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue