diff --git a/buildconfig/stubs/pygame/draw.pyi b/buildconfig/stubs/pygame/draw.pyi index 68ad3ace5b..ef0ed8239a 100644 --- a/buildconfig/stubs/pygame/draw.pyi +++ b/buildconfig/stubs/pygame/draw.pyi @@ -55,6 +55,9 @@ def aacircle( def ellipse( surface: Surface, color: ColorLike, rect: RectLike, width: int = 0 ) -> Rect: ... +def aaellipse( + surface: Surface, color: ColorLike, rect: RectLike, width: int = 0 +) -> Rect: ... def arc( surface: Surface, color: ColorLike, diff --git a/docs/reST/ref/code_examples/draw_module_example.png b/docs/reST/ref/code_examples/draw_module_example.png index 6e3e6af13c..4c465db669 100644 Binary files a/docs/reST/ref/code_examples/draw_module_example.png and b/docs/reST/ref/code_examples/draw_module_example.png differ diff --git a/docs/reST/ref/code_examples/draw_module_example.py b/docs/reST/ref/code_examples/draw_module_example.py index 1e155247b2..9849b959c0 100644 --- a/docs/reST/ref/code_examples/draw_module_example.py +++ b/docs/reST/ref/code_examples/draw_module_example.py @@ -64,10 +64,15 @@ ) # Draw an ellipse outline, using a rectangle as the outside boundaries - pygame.draw.ellipse(screen, "red", [225, 10, 50, 20], 2) + pygame.draw.ellipse(screen, "red", [235, 10, 50, 20], 2) # Draw an solid ellipse, using a rectangle as the outside boundaries - pygame.draw.ellipse(screen, "red", [300, 10, 50, 20]) + pygame.draw.ellipse(screen, "red", [310, 10, 50, 20]) + + # Draw an antialiased ellipse, using a rectangle as the outside boundaries + pygame.draw.aaellipse(screen, "red", [235, 40, 50, 20], 3) + # Draw an antialiased filled ellipse, using a rectangle as the outside boundaries + pygame.draw.aaellipse(screen, "red", [310, 40, 50, 20]) # This draws a triangle using the polygon command pygame.draw.polygon(screen, "black", [[100, 100], [0, 200], [200, 200]], 5) diff --git a/docs/reST/ref/draw.rst b/docs/reST/ref/draw.rst index 53f5c35b44..064b1f34af 100644 --- a/docs/reST/ref/draw.rst +++ b/docs/reST/ref/draw.rst @@ -294,6 +294,45 @@ object around the draw calls (see :func:`pygame.Surface.lock` and .. ## pygame.draw.ellipse ## + .. function:: aaellipse + + | :sl:`draw an antialiased ellipse` + | :sg:`aaellipse(surface, color, rect) -> Rect` + | :sg:`aaellipse(surface, color, rect, width=0) -> Rect` + + Draws an antialiased ellipse on the given surface. + Uses Xiaolin Wu Circle Algorithm. + adapted from: https://cgg.mff.cuni.cz/~pepca/ref/WU.pdf + + :param Surface surface: surface to draw on + :param color: color to draw with, the alpha value is optional if using a + tuple ``(RGB[A])`` + :type color: Color or string (for :doc:`color_list`) or int or tuple(int, int, int, [int]) + :param Rect rect: rectangle to indicate the position and dimensions of the + ellipse, the ellipse will be centered inside the rectangle and bounded + by it + :param int width: (optional) used for line thickness or to indicate that + the ellipse is to be filled (not to be confused with the width value + of the ``rect`` parameter) + + | if ``width == 0``, (default) fill the ellipse + | if ``width > 0``, used for line thickness + | if ``width < 0``, nothing will be drawn + | + + .. note:: + When using ``width`` values ``> 1``, the edge lines will only grow + inward from the original boundary of the ``rect`` parameter. + + :returns: a rect bounding the changed pixels, if nothing is drawn the + bounding rect's position will be the position of the given ``rect`` + parameter and its width and height will be 0 + :rtype: Rect + + .. versionadded:: 2.5.3 + + .. ## pygame.draw.aaellipse ## + .. function:: arc | :sl:`draw an elliptical arc` diff --git a/src_c/doc/draw_doc.h b/src_c/doc/draw_doc.h index 384936e90c..4390b0788d 100644 --- a/src_c/doc/draw_doc.h +++ b/src_c/doc/draw_doc.h @@ -5,6 +5,7 @@ #define DOC_DRAW_CIRCLE "circle(surface, color, center, radius) -> Rect\ncircle(surface, color, center, radius, width=0, draw_top_right=None, draw_top_left=None, draw_bottom_left=None, draw_bottom_right=None) -> Rect\ndraw a circle" #define DOC_DRAW_AACIRCLE "aacircle(surface, color, center, radius) -> Rect\naacircle(surface, color, center, radius, width=0, draw_top_right=None, draw_top_left=None, draw_bottom_left=None, draw_bottom_right=None) -> Rect\ndraw an antialiased circle" #define DOC_DRAW_ELLIPSE "ellipse(surface, color, rect) -> Rect\nellipse(surface, color, rect, width=0) -> Rect\ndraw an ellipse" +#define DOC_DRAW_AAELLIPSE "aaellipse(surface, color, rect) -> Rect\naaellipse(surface, color, rect, width=0) -> Rect\ndraw an antialiased ellipse" #define DOC_DRAW_ARC "arc(surface, color, rect, start_angle, stop_angle) -> Rect\narc(surface, color, rect, start_angle, stop_angle, width=1) -> Rect\ndraw an elliptical arc" #define DOC_DRAW_LINE "line(surface, color, start_pos, end_pos) -> Rect\nline(surface, color, start_pos, end_pos, width=1) -> Rect\ndraw a straight line" #define DOC_DRAW_LINES "lines(surface, color, closed, points) -> Rect\nlines(surface, color, closed, points, width=1) -> Rect\ndraw multiple contiguous straight line segments" diff --git a/src_c/draw.c b/src_c/draw.c index 0d6b9fc8a8..420c5ff863 100644 --- a/src_c/draw.c +++ b/src_c/draw.c @@ -64,13 +64,22 @@ static void draw_circle_bresenham_thin(SDL_Surface *surf, int x0, int y0, int radius, Uint32 color, int *drawn_area); static void -draw_circle_xiaolinwu(SDL_Surface *surf, int x0, int y0, int radius, - int thickness, Uint32 color, int top_right, int top_left, - int bottom_left, int bottom_right, int *drawn_area); +draw_aacircle_xiaolinwu(SDL_Surface *surf, int x0, int y0, int radius, + int thickness, Uint32 color, int top_right, + int top_left, int bottom_left, int bottom_right, + int *drawn_area); static void -draw_circle_xiaolinwu_thin(SDL_Surface *surf, int x0, int y0, int radius, - Uint32 color, int top_right, int top_left, - int bottom_left, int bottom_right, int *drawn_area); +draw_aacircle_xiaolinwu_thin(SDL_Surface *surf, int x0, int y0, int radius, + Uint32 color, int top_right, int top_left, + int bottom_left, int bottom_right, + int *drawn_area); +static void +draw_aaellipse_xiaolinwu(SDL_Surface *surf, int x0, int y0, int width, + int height, int thickness, Uint32 color, + int *drawn_area); +static void +draw_aaellipse_xiaolinwu_thin(SDL_Surface *surf, int x0, int y0, int width, + int height, Uint32 color, int *drawn_area); static void draw_circle_filled(SDL_Surface *surf, int x0, int y0, int radius, Uint32 color, int *drawn_area); @@ -672,73 +681,6 @@ arc(PyObject *self, PyObject *arg, PyObject *kwargs) return pgRect_New4(rect->x, rect->y, 0, 0); } -static PyObject * -ellipse(PyObject *self, PyObject *arg, PyObject *kwargs) -{ - pgSurfaceObject *surfobj; - PyObject *colorobj, *rectobj; - SDL_Rect *rect = NULL, temp; - SDL_Surface *surf = NULL; - Uint32 color; - int width = 0; /* Default width. */ - int drawn_area[4] = {INT_MAX, INT_MAX, INT_MIN, - INT_MIN}; /* Used to store bounding box values */ - static char *keywords[] = {"surface", "color", "rect", "width", NULL}; - - if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O!OO|i", keywords, - &pgSurface_Type, &surfobj, &colorobj, - &rectobj, &width)) { - return NULL; /* Exception already set. */ - } - - rect = pgRect_FromObject(rectobj, &temp); - - if (!rect) { - return RAISE(PyExc_TypeError, "rect argument is invalid"); - } - - surf = pgSurface_AsSurface(surfobj); - SURF_INIT_CHECK(surf) - - if (PG_SURF_BytesPerPixel(surf) <= 0 || PG_SURF_BytesPerPixel(surf) > 4) { - return PyErr_Format(PyExc_ValueError, - "unsupported surface bit depth (%d) for drawing", - PG_SURF_BytesPerPixel(surf)); - } - - CHECK_LOAD_COLOR(colorobj) - - if (width < 0) { - return pgRect_New4(rect->x, rect->y, 0, 0); - } - - if (!pgSurface_Lock(surfobj)) { - return RAISE(PyExc_RuntimeError, "error locking surface"); - } - - if (!width || - width >= MIN(rect->w / 2 + rect->w % 2, rect->h / 2 + rect->h % 2)) { - draw_ellipse_filled(surf, rect->x, rect->y, rect->w, rect->h, color, - drawn_area); - } - else { - draw_ellipse_thickness(surf, rect->x, rect->y, rect->w, rect->h, - width - 1, color, drawn_area); - } - - if (!pgSurface_Unlock(surfobj)) { - return RAISE(PyExc_RuntimeError, "error unlocking surface"); - } - - if (drawn_area[0] != INT_MAX && drawn_area[1] != INT_MAX && - drawn_area[2] != INT_MIN && drawn_area[3] != INT_MIN) - return pgRect_New4(drawn_area[0], drawn_area[1], - drawn_area[2] - drawn_area[0] + 1, - drawn_area[3] - drawn_area[1] + 1); - else - return pgRect_New4(rect->x, rect->y, 0, 0); -} - static PyObject * circle(PyObject *self, PyObject *args, PyObject *kwargs) { @@ -921,33 +863,33 @@ aacircle(PyObject *self, PyObject *args, PyObject *kwargs) if (!width || width == radius) { draw_circle_filled(surf, posx, posy, radius - 1, color, drawn_area); - draw_circle_xiaolinwu(surf, posx, posy, radius, 2, color, 1, 1, 1, - 1, drawn_area); + draw_aacircle_xiaolinwu(surf, posx, posy, radius, 2, color, 1, 1, + 1, 1, drawn_area); } else if (width == 1) { - draw_circle_xiaolinwu_thin(surf, posx, posy, radius, color, 1, 1, - 1, 1, drawn_area); + draw_aacircle_xiaolinwu_thin(surf, posx, posy, radius, color, 1, 1, + 1, 1, drawn_area); } else { - draw_circle_xiaolinwu(surf, posx, posy, radius, width, color, 1, 1, - 1, 1, drawn_area); + draw_aacircle_xiaolinwu(surf, posx, posy, radius, width, color, 1, + 1, 1, 1, drawn_area); } } else { if (!width || width == radius) { - draw_circle_xiaolinwu(surf, posx, posy, radius, radius, color, - top_right, top_left, bottom_left, - bottom_right, drawn_area); + draw_aacircle_xiaolinwu(surf, posx, posy, radius, radius, color, + top_right, top_left, bottom_left, + bottom_right, drawn_area); } else if (width == 1) { - draw_circle_xiaolinwu_thin(surf, posx, posy, radius, color, - top_right, top_left, bottom_left, - bottom_right, drawn_area); + draw_aacircle_xiaolinwu_thin(surf, posx, posy, radius, color, + top_right, top_left, bottom_left, + bottom_right, drawn_area); } else { - draw_circle_xiaolinwu(surf, posx, posy, radius, width, color, - top_right, top_left, bottom_left, - bottom_right, drawn_area); + draw_aacircle_xiaolinwu(surf, posx, posy, radius, width, color, + top_right, top_left, bottom_left, + bottom_right, drawn_area); } } @@ -963,6 +905,190 @@ aacircle(PyObject *self, PyObject *args, PyObject *kwargs) return pgRect_New4(posx, posy, 0, 0); } +static PyObject * +ellipse(PyObject *self, PyObject *arg, PyObject *kwargs) +{ + pgSurfaceObject *surfobj; + PyObject *colorobj, *rectobj; + SDL_Rect *rect = NULL, temp; + SDL_Surface *surf = NULL; + Uint32 color; + int width = 0; /* Default width. */ + int drawn_area[4] = {INT_MAX, INT_MAX, INT_MIN, + INT_MIN}; /* Used to store bounding box values */ + static char *keywords[] = {"surface", "color", "rect", "width", NULL}; + + if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O!OO|i", keywords, + &pgSurface_Type, &surfobj, &colorobj, + &rectobj, &width)) { + return NULL; /* Exception already set. */ + } + + rect = pgRect_FromObject(rectobj, &temp); + + if (!rect) { + return RAISE(PyExc_TypeError, "rect argument is invalid"); + } + + surf = pgSurface_AsSurface(surfobj); + SURF_INIT_CHECK(surf) + + if (PG_SURF_BytesPerPixel(surf) <= 0 || PG_SURF_BytesPerPixel(surf) > 4) { + return PyErr_Format(PyExc_ValueError, + "unsupported surface bit depth (%d) for drawing", + PG_SURF_BytesPerPixel(surf)); + } + + CHECK_LOAD_COLOR(colorobj) + + if (width < 0) { + return pgRect_New4(rect->x, rect->y, 0, 0); + } + + if (!pgSurface_Lock(surfobj)) { + return RAISE(PyExc_RuntimeError, "error locking surface"); + } + + if (!width || + width >= MIN(rect->w / 2 + rect->w % 2, rect->h / 2 + rect->h % 2)) { + draw_ellipse_filled(surf, rect->x, rect->y, rect->w, rect->h, color, + drawn_area); + } + else { + draw_ellipse_thickness(surf, rect->x, rect->y, rect->w, rect->h, + width - 1, color, drawn_area); + } + + if (!pgSurface_Unlock(surfobj)) { + return RAISE(PyExc_RuntimeError, "error unlocking surface"); + } + + if (drawn_area[0] != INT_MAX && drawn_area[1] != INT_MAX && + drawn_area[2] != INT_MIN && drawn_area[3] != INT_MIN) + return pgRect_New4(drawn_area[0], drawn_area[1], + drawn_area[2] - drawn_area[0] + 1, + drawn_area[3] - drawn_area[1] + 1); + else + return pgRect_New4(rect->x, rect->y, 0, 0); +} + +static PyObject * +aaellipse(PyObject *self, PyObject *arg, PyObject *kwargs) +{ + pgSurfaceObject *surfobj; + PyObject *colorobj, *rectobj; + SDL_Rect *rect = NULL, temp; + SDL_Surface *surf = NULL; + Uint32 color; + int width = 0; /* Default width. */ + int drawn_area[4] = {INT_MAX, INT_MAX, INT_MIN, + INT_MIN}; /* Used to store bounding box values */ + static char *keywords[] = {"surface", "color", "rect", "width", NULL}; + + if (!PyArg_ParseTupleAndKeywords(arg, kwargs, "O!OO|i", keywords, + &pgSurface_Type, &surfobj, &colorobj, + &rectobj, &width)) { + return NULL; /* Exception already set. */ + } + + rect = pgRect_FromObject(rectobj, &temp); + + if (!rect) { + return RAISE(PyExc_TypeError, "rect argument is invalid"); + } + + surf = pgSurface_AsSurface(surfobj); + SURF_INIT_CHECK(surf) + + if (PG_SURF_BytesPerPixel(surf) <= 0 || PG_SURF_BytesPerPixel(surf) > 4) { + return PyErr_Format(PyExc_ValueError, + "unsupported surface bit depth (%d) for drawing", + PG_SURF_BytesPerPixel(surf)); + } + + CHECK_LOAD_COLOR(colorobj) + + if (width < 0) { + return pgRect_New4(rect->x, rect->y, 0, 0); + } + + if (!pgSurface_Lock(surfobj)) { + return RAISE(PyExc_RuntimeError, "error locking surface"); + } + + // limit width to size of the rect + if (width != 1 && + (width >= (int)(rect->w / 2) || width >= (int)(rect->h / 2))) { + width = 0; + } + + if (rect->w == 1 && rect->h == 1) { + draw_line(surf, rect->x, rect->y, rect->x, rect->y, color, drawn_area); + } + else if (rect->w == 1) { + // h - 1 because line is drawn 1px longer + draw_line(surf, rect->x, rect->y, rect->x, rect->y + rect->h - 1, + color, drawn_area); + } + else if (rect->h == 1) { + // w - 1 because line is drawn 1px longer + draw_line(surf, rect->x, rect->y, rect->x + rect->w - 1, rect->y, + color, drawn_area); + } + else if (rect->h == rect->w) { + int radius = (int)(rect->w / 2); + SDL_Rect *ret_rect = NULL; + PyObject *ret = NULL; + PyObject *center = pg_tuple_couple_from_values_int(rect->x + radius, + rect->y + radius); + PyObject *args = + Py_BuildValue("(OOOii)", surfobj, colorobj, center, radius, width); + if (!args) { + return NULL; /* Exception already set. */ + } + ret = aacircle(NULL, args, NULL); + ret_rect = pgRect_FromObject(ret, &temp); + if (ret_rect->w == 0 && ret_rect->h == 0) { + ret = pgRect_New4(rect->x, rect->y, 0, 0); + } + Py_DECREF(args); + return ret; + } + else if (width == 0) { + // larger ellipse is needed when drawing aaellipse with even width + if (rect->w % 2) { + draw_ellipse_filled(surf, rect->x + 1, rect->y + 1, rect->w - 2, + rect->h - 2, color, drawn_area); + } + else { + draw_ellipse_filled(surf, rect->x + 1, rect->y + 1, rect->w - 1, + rect->h - 2, color, drawn_area); + } + draw_aaellipse_xiaolinwu(surf, rect->x, rect->y, rect->w, rect->h, 2, + color, drawn_area); + } + else if (width == 1) { + draw_aaellipse_xiaolinwu_thin(surf, rect->x, rect->y, rect->w, rect->h, + color, drawn_area); + } + else { + draw_aaellipse_xiaolinwu(surf, rect->x, rect->y, rect->w, rect->h, + width, color, drawn_area); + } + + if (!pgSurface_Unlock(surfobj)) { + return RAISE(PyExc_RuntimeError, "error unlocking surface"); + } + + if (drawn_area[0] != INT_MAX && drawn_area[1] != INT_MAX && + drawn_area[2] != INT_MIN && drawn_area[3] != INT_MIN) + return pgRect_New4(drawn_area[0], drawn_area[1], + drawn_area[2] - drawn_area[0] + 1, + drawn_area[3] - drawn_area[1] + 1); + else + return pgRect_New4(rect->x, rect->y, 0, 0); +} + static PyObject * polygon(PyObject *self, PyObject *arg, PyObject *kwargs) { @@ -1216,6 +1342,14 @@ swap(float *a, float *b) *b = temp; } +static void +swap_int(int *a, int *b) +{ + int temp = *a; + *a = *b; + *b = temp; +} + static int compare_int(const void *a, const void *b) { @@ -2634,10 +2768,30 @@ draw_circle_filled(SDL_Surface *surf, int x0, int y0, int radius, Uint32 color, } static void -draw_eight_symetric_pixels(SDL_Surface *surf, int x0, int y0, Uint32 color, - int x, int y, float opacity, int top_right, - int top_left, int bottom_left, int bottom_right, +draw_four_symmetric_pixels(SDL_Surface *surf, int x0, int y0, Uint32 color, + int x, int y, float opacity, int swap_xy, int *drawn_area) +{ + opacity = opacity / 255.0f; + if (swap_xy) { + swap_int(&x, &y); + } + Uint32 pixel_color; + pixel_color = get_antialiased_color(surf, x0 + x, y0 - y, color, opacity); + set_and_check_rect(surf, x0 + x, y0 - y, pixel_color, drawn_area); + pixel_color = get_antialiased_color(surf, x0 - x, y0 - y, color, opacity); + set_and_check_rect(surf, x0 - x, y0 - y, pixel_color, drawn_area); + pixel_color = get_antialiased_color(surf, x0 - x, y0 + y, color, opacity); + set_and_check_rect(surf, x0 - x, y0 + y, pixel_color, drawn_area); + pixel_color = get_antialiased_color(surf, x0 + x, y0 + y, color, opacity); + set_and_check_rect(surf, x0 + x, y0 + y, pixel_color, drawn_area); +} + +static void +draw_eight_symmetric_pixels(SDL_Surface *surf, int x0, int y0, Uint32 color, + int x, int y, float opacity, int top_right, + int top_left, int bottom_left, int bottom_right, + int *drawn_area) { opacity = opacity / 255.0f; Uint32 pixel_color; @@ -2680,9 +2834,10 @@ draw_eight_symetric_pixels(SDL_Surface *surf, int x0, int y0, Uint32 color, * with additional line width parameter and quadrants option */ static void -draw_circle_xiaolinwu(SDL_Surface *surf, int x0, int y0, int radius, - int thickness, Uint32 color, int top_right, int top_left, - int bottom_left, int bottom_right, int *drawn_area) +draw_aacircle_xiaolinwu(SDL_Surface *surf, int x0, int y0, int radius, + int thickness, Uint32 color, int top_right, + int top_left, int bottom_left, int bottom_right, + int *drawn_area) { for (int layer_radius = radius - thickness; layer_radius <= radius; layer_radius++) { @@ -2698,10 +2853,10 @@ draw_circle_xiaolinwu(SDL_Surface *surf, int x0, int y0, int radius, --y; } prev_opacity = opacity; - draw_eight_symetric_pixels(surf, x0, y0, color, x, y, 255.0f, - top_right, top_left, bottom_left, - bottom_right, drawn_area); - draw_eight_symetric_pixels( + draw_eight_symmetric_pixels(surf, x0, y0, color, x, y, 255.0f, + top_right, top_left, bottom_left, + bottom_right, drawn_area); + draw_eight_symmetric_pixels( surf, x0, y0, color, x, y - 1, (float)opacity, top_right, top_left, bottom_left, bottom_right, drawn_area); ++x; @@ -2715,11 +2870,11 @@ draw_circle_xiaolinwu(SDL_Surface *surf, int x0, int y0, int radius, --y; } prev_opacity = opacity; - draw_eight_symetric_pixels(surf, x0, y0, color, x, y, - 255.0f - (float)opacity, top_right, - top_left, bottom_left, bottom_right, - drawn_area); - draw_eight_symetric_pixels( + draw_eight_symmetric_pixels(surf, x0, y0, color, x, y, + 255.0f - (float)opacity, top_right, + top_left, bottom_left, + bottom_right, drawn_area); + draw_eight_symmetric_pixels( surf, x0, y0, color, x, y - 1, 255.0f, top_right, top_left, bottom_left, bottom_right, drawn_area); ++x; @@ -2733,10 +2888,10 @@ draw_circle_xiaolinwu(SDL_Surface *surf, int x0, int y0, int radius, --y; } prev_opacity = opacity; - draw_eight_symetric_pixels(surf, x0, y0, color, x, y, 255.0f, - top_right, top_left, bottom_left, - bottom_right, drawn_area); - draw_eight_symetric_pixels( + draw_eight_symmetric_pixels(surf, x0, y0, color, x, y, 255.0f, + top_right, top_left, bottom_left, + bottom_right, drawn_area); + draw_eight_symmetric_pixels( surf, x0, y0, color, x, y - 1, 255.0f, top_right, top_left, bottom_left, bottom_right, drawn_area); ++x; @@ -2746,9 +2901,10 @@ draw_circle_xiaolinwu(SDL_Surface *surf, int x0, int y0, int radius, } static void -draw_circle_xiaolinwu_thin(SDL_Surface *surf, int x0, int y0, int radius, - Uint32 color, int top_right, int top_left, - int bottom_left, int bottom_right, int *drawn_area) +draw_aacircle_xiaolinwu_thin(SDL_Surface *surf, int x0, int y0, int radius, + Uint32 color, int top_right, int top_left, + int bottom_left, int bottom_right, + int *drawn_area) { int x = 0; int y = radius; @@ -2761,12 +2917,12 @@ draw_circle_xiaolinwu_thin(SDL_Surface *surf, int x0, int y0, int radius, --y; } prev_opacity = opacity; - draw_eight_symetric_pixels( + draw_eight_symmetric_pixels( surf, x0, y0, color, x, y, 255.0f - (float)opacity, top_right, top_left, bottom_left, bottom_right, drawn_area); - draw_eight_symetric_pixels(surf, x0, y0, color, x, y - 1, - (float)opacity, top_right, top_left, - bottom_left, bottom_right, drawn_area); + draw_eight_symmetric_pixels(surf, x0, y0, color, x, y - 1, + (float)opacity, top_right, top_left, + bottom_left, bottom_right, drawn_area); ++x; } } @@ -3017,6 +3173,200 @@ draw_ellipse_thickness(SDL_Surface *surf, int x0, int y0, int width, } } +/* Xiaolin Wu Ellipse Algorithm + * adapted from: https://cgg.mff.cuni.cz/~pepca/ref/WU.pdf + * with additional line width parameter + */ +static void +draw_aaellipse_xiaolinwu(SDL_Surface *surf, int x0, int y0, int width, + int height, int thickness, Uint32 color, + int *drawn_area) +{ + int vertical; + int a = width / 2; + int b = height / 2; + vertical = a < b; + x0 = x0 + a; + y0 = y0 + b; + if (vertical) { + swap_int(&a, &b); + } + int x = 0; + int y = b; + int layer_b = (b - thickness); + for (int layer_a = a - thickness; layer_a <= a; layer_a++) { + // in case when thickness = w/2 or thickness = h/2 + double pow_layer_a = pow(layer_a, 2); + double pow_layer_b = pow(layer_b, 2); + double layers_sum_root = sqrt(pow_layer_a + pow_layer_b); + double prev_opacity = 0.0; + x = 0; + y = layer_b; + int deg45 = (int)round(pow_layer_a / layers_sum_root) + 1; + // inner antialiased layer + if (layer_a == a - thickness) { + if (width > 3 && height > 3) { + while (x < deg45) { + double height = + layer_b * sqrt(1 - pow(x, 2) / pow_layer_a); + double opacity = 255.0 * (ceil(height) - height); + if (opacity < prev_opacity) { + --y; + } + prev_opacity = opacity; + draw_four_symmetric_pixels(surf, x0, y0, color, x, y, + 255.0f, vertical, drawn_area); + draw_four_symmetric_pixels(surf, x0, y0, color, x, y - 1, + (float)opacity, vertical, + drawn_area); + ++x; + } + x = layer_a + 1; + y = 0; + deg45 = (int)round(pow_layer_b / layers_sum_root) + 1; + while (y < deg45) { + double width = layer_a * sqrt(1 - pow(y, 2) / pow_layer_b); + double opacity = 255.0 * (ceil(width) - width); + if (opacity < prev_opacity) { + --x; + } + prev_opacity = opacity; + draw_four_symmetric_pixels(surf, x0, y0, color, x, y, + 255.0f, vertical, drawn_area); + draw_four_symmetric_pixels(surf, x0, y0, color, x - 1, y, + (float)opacity, vertical, + drawn_area); + ++y; + } + } + } + // between fully opaque layers + else if (layer_a == a) { + while (x < deg45) { + double height = layer_b * sqrt(1 - pow(x, 2) / pow_layer_a); + double opacity = 255.0 * (ceil(height) - height); + if (opacity < prev_opacity) { + --y; + } + prev_opacity = opacity; + draw_four_symmetric_pixels(surf, x0, y0, color, x, y, + 255.0f - (float)opacity, vertical, + drawn_area); + draw_four_symmetric_pixels(surf, x0, y0, color, x, y - 1, + 255.0f, vertical, drawn_area); + ++x; + } + x = layer_a + 1; + y = 0; + deg45 = (int)round(pow_layer_b / layers_sum_root) + 1; + while (y < deg45) { + double width = layer_a * sqrt(1 - pow(y, 2) / pow_layer_b); + double opacity = 255.0 * (ceil(width) - width); + if (opacity < prev_opacity) { + --x; + } + prev_opacity = opacity; + draw_four_symmetric_pixels(surf, x0, y0, color, x, y, + 255.0f - (float)opacity, vertical, + drawn_area); + draw_four_symmetric_pixels(surf, x0, y0, color, x - 1, y, + 255.0f, vertical, drawn_area); + ++y; + } + } + // outer antialiased layer + else { + while (x < deg45) { + double height = layer_b * sqrt(1 - pow(x, 2) / pow_layer_a); + double opacity = 255.0 * (ceil(height) - height); + if (opacity < prev_opacity) { + --y; + } + prev_opacity = opacity; + draw_four_symmetric_pixels(surf, x0, y0, color, x, y, 255.0f, + vertical, drawn_area); + draw_four_symmetric_pixels(surf, x0, y0, color, x, y - 1, + 255.0f, vertical, drawn_area); + ++x; + } + x = layer_a + 1; + y = 0; + deg45 = + (int)round(pow_layer_b / sqrt(pow_layer_a + pow_layer_b)) + 1; + while (y < deg45) { + double width = layer_a * sqrt(1 - pow(y, 2) / pow_layer_b); + double opacity = 255.0 * (ceil(width) - width); + if (opacity < prev_opacity) { + --x; + } + prev_opacity = opacity; + draw_four_symmetric_pixels(surf, x0, y0, color, x, y, 255.0f, + vertical, drawn_area); + draw_four_symmetric_pixels(surf, x0, y0, color, x - 1, y, + 255.0f, vertical, drawn_area); + ++y; + } + } + ++layer_b; + } +} + +static void +draw_aaellipse_xiaolinwu_thin(SDL_Surface *surf, int x0, int y0, int width, + int height, Uint32 color, int *drawn_area) +{ + int vertical; + int a = width / 2; + int b = height / 2; + vertical = a < b; + x0 = x0 + a; + y0 = y0 + b; + if (vertical) { + swap_int(&a, &b); + } + double pow_a = pow(a, 2); + double pow_b = pow(b, 2); + double prev_opacity = 0.0; + // 45 degree coordinate, at that point switch from horizontal to vertical + // drawing + int deg45 = (int)round(pow_a / sqrt(pow_a + pow_b)) + 1; + // horizontal drawing + int x = 0; + int y = b; + while (x < deg45) { + double height = b * sqrt(1 - pow(x, 2) / pow_a); + double opacity = 255.0 * (ceil(height) - height); + if (opacity < prev_opacity) { + --y; + } + prev_opacity = opacity; + draw_four_symmetric_pixels(surf, x0, y0, color, x, y, + 255.0f - (float)opacity, vertical, + drawn_area); + draw_four_symmetric_pixels(surf, x0, y0, color, x, y - 1, + (float)opacity, vertical, drawn_area); + ++x; + } + // vertical drawing + x = a + 1; + y = 0; + deg45 = (int)round(pow_b / sqrt(pow_a + pow_b)) + 1; + while (y < deg45) { + double width = a * sqrt(1 - pow(y, 2) / pow_b); + double opacity = 255.0 * (ceil(width) - width); + if (opacity < prev_opacity) { + --x; + } + prev_opacity = opacity; + draw_four_symmetric_pixels(surf, x0, y0, color, x, y, + 255.0f - (float)opacity, vertical, + drawn_area); + draw_four_symmetric_pixels(surf, x0, y0, color, x - 1, y, + (float)opacity, vertical, drawn_area); + ++y; + } +} + static void draw_fillpoly(SDL_Surface *surf, int *point_x, int *point_y, Py_ssize_t num_points, Uint32 color, int *drawn_area) @@ -3278,6 +3628,8 @@ static PyMethodDef _draw_methods[] = { DOC_DRAW_LINES}, {"ellipse", (PyCFunction)ellipse, METH_VARARGS | METH_KEYWORDS, DOC_DRAW_ELLIPSE}, + {"aaellipse", (PyCFunction)aaellipse, METH_VARARGS | METH_KEYWORDS, + DOC_DRAW_AAELLIPSE}, {"arc", (PyCFunction)arc, METH_VARARGS | METH_KEYWORDS, DOC_DRAW_ARC}, {"circle", (PyCFunction)circle, METH_VARARGS | METH_KEYWORDS, DOC_DRAW_CIRCLE}, diff --git a/test/draw_test.py b/test/draw_test.py index 872e72ee58..0da6536f07 100644 --- a/test/draw_test.py +++ b/test/draw_test.py @@ -169,6 +169,7 @@ class DrawTestCase(unittest.TestCase): draw_circle = staticmethod(draw.circle) draw_aacircle = staticmethod(draw.aacircle) draw_ellipse = staticmethod(draw.ellipse) + draw_aaellipse = staticmethod(draw.aaellipse) draw_arc = staticmethod(draw.arc) draw_line = staticmethod(draw.line) draw_lines = staticmethod(draw.lines) @@ -1045,6 +1046,661 @@ class DrawEllipseTest(DrawEllipseMixin, DrawTestCase): """ +### AAEllipse Testing ########################################################### + + +class DrawAAEllipseMixin: + """Mixin tests for drawing antialiased ellipses. + + This class contains all the general antialiased ellipse drawing tests. + Most of test are taken from DrawEllipseMixin class. + """ + + def test_aaellipse__args(self): + """Ensures draw aaellipse accepts the correct args.""" + bounds_rect = self.draw_aaellipse( + pygame.Surface((3, 3)), (0, 10, 0, 50), pygame.Rect((0, 0), (3, 2)), 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaellipse__args_without_width(self): + """Ensures draw aaellipse accepts the args without a width.""" + bounds_rect = self.draw_aaellipse( + pygame.Surface((2, 2)), (1, 1, 1, 99), pygame.Rect((1, 1), (1, 1)) + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaellipse__args_with_negative_width(self): + """Ensures draw aaellipse accepts the args with negative width.""" + bounds_rect = self.draw_aaellipse( + pygame.Surface((3, 3)), (0, 10, 0, 50), pygame.Rect((2, 3), (3, 2)), -1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + self.assertEqual(bounds_rect, pygame.Rect(2, 3, 0, 0)) + + def test_aaellipse__args_with_width_gt_radius(self): + """Ensures draw aaellipse accepts the args with + width > rect.w // 2 and width > rect.h // 2. + """ + rect = pygame.Rect((0, 0), (4, 4)) + bounds_rect = self.draw_aaellipse( + pygame.Surface((3, 3)), (0, 10, 0, 50), rect, rect.w // 2 + 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + bounds_rect = self.draw_aaellipse( + pygame.Surface((3, 3)), (0, 10, 0, 50), rect, rect.h // 2 + 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaellipse__kwargs(self): + """Ensures draw aaellipse accepts the correct kwargs + with and without a width arg. + """ + kwargs_list = [ + { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("yellow"), + "rect": pygame.Rect((0, 0), (3, 2)), + "width": 1, + }, + { + "surface": pygame.Surface((2, 1)), + "color": (0, 10, 20), + "rect": (0, 0, 1, 1), + }, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_aaellipse(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaellipse__kwargs_order_independent(self): + """Ensures draw aaellipse's kwargs are not order dependent.""" + bounds_rect = self.draw_aaellipse( + color=(1, 2, 3), + surface=pygame.Surface((3, 2)), + width=0, + rect=pygame.Rect((1, 0), (1, 1)), + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaellipse__args_missing(self): + """Ensures draw aaellipse detects any missing required args.""" + surface = pygame.Surface((1, 1)) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaellipse(surface, pygame.Color("red")) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaellipse(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaellipse() + + def test_aaellipse__kwargs_missing(self): + """Ensures draw aaellipse detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((1, 2)), + "color": pygame.Color("red"), + "rect": pygame.Rect((1, 0), (2, 2)), + "width": 2, + } + + for name in ("rect", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaellipse(**invalid_kwargs) + + def test_aaellipse__arg_invalid_types(self): + """Ensures draw aaellipse detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + rect = pygame.Rect((1, 1), (1, 1)) + + with self.assertRaises(TypeError): + # Invalid width. + bounds_rect = self.draw_aaellipse(surface, color, rect, "1") + + with self.assertRaises(TypeError): + # Invalid rect. + bounds_rect = self.draw_aaellipse(surface, color, (1, 2, 3, 4, 5), 1) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_aaellipse(surface, 2.3, rect, 0) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_aaellipse(rect, color, rect, 2) + + def test_aaellipse__kwarg_invalid_types(self): + """Ensures draw aaellipse detects invalid kwarg types.""" + surface = pygame.Surface((3, 3)) + color = pygame.Color("green") + rect = pygame.Rect((0, 1), (1, 1)) + kwargs_list = [ + { + "surface": pygame.Surface, # Invalid surface. + "color": color, + "rect": rect, + "width": 1, + }, + { + "surface": surface, + "color": 2.3, # Invalid color. + "rect": rect, + "width": 1, + }, + { + "surface": surface, + "color": color, + "rect": (0, 0, 0), # Invalid rect. + "width": 1, + }, + {"surface": surface, "color": color, "rect": rect, "width": 1.1}, + ] # Invalid width. + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaellipse(**kwargs) + + def test_aaellipse__kwarg_invalid_name(self): + """Ensures draw aaellipse detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + rect = pygame.Rect((0, 1), (2, 2)) + kwargs_list = [ + { + "surface": surface, + "color": color, + "rect": rect, + "width": 1, + "invalid": 1, + }, + {"surface": surface, "color": color, "rect": rect, "invalid": 1}, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaellipse(**kwargs) + + def test_aaellipse__args_and_kwargs(self): + """Ensures draw aaellipse accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 1)) + color = (255, 255, 0, 0) + rect = pygame.Rect((1, 0), (2, 1)) + width = 0 + kwargs = {"surface": surface, "color": color, "rect": rect, "width": width} + + for name in ("surface", "color", "rect", "width"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_aaellipse(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_aaellipse(surface, color, **kwargs) + elif "rect" == name: + bounds_rect = self.draw_aaellipse(surface, color, rect, **kwargs) + else: + bounds_rect = self.draw_aaellipse(surface, color, rect, width, **kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaellipse__valid_width_values(self): + """Ensures draw aaellipse accepts different width values.""" + pos = (1, 1) + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + color = (10, 20, 30, 255) + kwargs = { + "surface": surface, + "color": color, + "rect": pygame.Rect(pos, (3, 2)), + "width": None, + } + + for width in (-1000, -10, -1, 0, 1, 10, 1000): + surface.fill(surface_color) # Clear for each test. + kwargs["width"] = width + expected_color = color if width >= 0 else surface_color + + bounds_rect = self.draw_aaellipse(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaellipse__valid_rect_formats(self): + """Ensures draw aaellipse accepts different rect formats.""" + pos = (1, 1) + expected_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((4, 4)) + kwargs = {"surface": surface, "color": expected_color, "rect": None, "width": 0} + rects = (pygame.Rect(pos, (1, 3)), (pos, (2, 1)), (pos[0], pos[1], 1, 1)) + + for rect in rects: + surface.fill(surface_color) # Clear for each test. + kwargs["rect"] = rect + + bounds_rect = self.draw_aaellipse(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaellipse__valid_color_formats(self): + """Ensures draw aaellipse accepts different color formats.""" + pos = (1, 1) + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": None, + "rect": pygame.Rect(pos, (1, 2)), + "width": 0, + } + reds = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in reds: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_aaellipse(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaellipse__invalid_color_formats(self): + """Ensures draw aaellipse handles invalid color formats correctly.""" + pos = (1, 1) + surface = pygame.Surface((4, 3)) + kwargs = { + "surface": surface, + "color": None, + "rect": pygame.Rect(pos, (2, 2)), + "width": 1, + } + + for expected_color in (2.3, surface): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaellipse(**kwargs) + + def test_aaellipse__no_holes(self): + width = 80 + height = 70 + surface = pygame.Surface((width + 1, height + 1)) + rect = pygame.Rect(0, 0, width, height) + # thickness=1 can't be checked because of antialiasing + for thickness in range(37, 5): + surface.fill("BLACK") + print(rect, thickness) + self.draw_aaellipse(surface, "RED", rect, thickness) + for y in range(height): + number_of_changes = 0 + drawn_pixel = False + for x in range(width + 1): + if ( + not drawn_pixel + and surface.get_at((x, y)) == pygame.Color("RED") + or drawn_pixel + and surface.get_at((x, y)) == pygame.Color("BLACK") + ): + drawn_pixel = not drawn_pixel + number_of_changes += 1 + if y < thickness or y > height - thickness - 1: + self.assertEqual(number_of_changes, 2) + else: + self.assertEqual(number_of_changes, 4) + + def _check_1_pixel_sized_aaellipse( + self, surface, collide_rect, surface_color, ellipse_color + ): + # Helper method to check the surface for 1 pixel wide and/or high + # ellipses. + surf_w, surf_h = surface.get_size() + + surface.lock() # For possible speed up. + + for pos in ((x, y) for y in range(surf_h) for x in range(surf_w)): + # Since the ellipse is just a line we can use a rect to help find + # where it is expected to be drawn. + if collide_rect.collidepoint(pos): + expected_color = ellipse_color + else: + expected_color = surface_color + + self.assertEqual( + surface.get_at(pos), + expected_color, + f"collide_rect={collide_rect}, pos={pos}", + ) + + surface.unlock() + + def test_aaellipse__1_pixel_width(self): + """Ensures an ellipse with a width of 1 is drawn correctly. + + An ellipse with a width of 1 pixel is a vertical line. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 10, 20 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (1, 0)) + collide_rect = rect.copy() + + # Calculate some positions. + off_left = -1 + off_right = surf_w + off_bottom = surf_h + center_x = surf_w // 2 + center_y = surf_h // 2 + + # Test some even and odd heights. + for ellipse_h in range(6, 10): + collide_rect.h = ellipse_h + rect.h = ellipse_h + + # Calculate some variable positions. + off_top = -(ellipse_h + 1) + half_off_top = -(ellipse_h // 2) + half_off_bottom = surf_h - (ellipse_h // 2) + + # Draw the ellipse in different positions: fully on-surface, + # partially off-surface, and fully off-surface. + positions = ( + (off_left, off_top), + (off_left, half_off_top), + (off_left, center_y), + (off_left, half_off_bottom), + (off_left, off_bottom), + (center_x, off_top), + (center_x, half_off_top), + (center_x, center_y), + (center_x, half_off_bottom), + (center_x, off_bottom), + (off_right, off_top), + (off_right, half_off_top), + (off_right, center_y), + (off_right, half_off_bottom), + (off_right, off_bottom), + ) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + collide_rect.topleft = rect_pos + + self.draw_aaellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_aaellipse( + surface, collide_rect, surface_color, ellipse_color + ) + + def test_aaellipse__1_pixel_width_spanning_surface(self): + """Ensures an ellipse with a width of 1 is drawn correctly + when spanning the height of the surface. + + An ellipse with a width of 1 pixel is a vertical line. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 10, 20 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (1, surf_h + 2)) # Longer than the surface. + + # Draw the ellipse in different positions: on-surface and off-surface. + positions = ( + (-1, -1), # (off_left, off_top) + (0, -1), # (left_edge, off_top) + (surf_w // 2, -1), # (center_x, off_top) + (surf_w - 1, -1), # (right_edge, off_top) + (surf_w, -1), + ) # (off_right, off_top) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + + self.draw_aaellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_aaellipse( + surface, rect, surface_color, ellipse_color + ) + + def test_aaellipse__1_pixel_height(self): + """Ensures an ellipse with a height of 1 is drawn correctly. + + An ellipse with a height of 1 pixel is a horizontal line. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 20, 10 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (0, 1)) + collide_rect = rect.copy() + + # Calculate some positions. + off_right = surf_w + off_top = -1 + off_bottom = surf_h + center_x = surf_w // 2 + center_y = surf_h // 2 + + # Test some even and odd widths. + for ellipse_w in range(6, 10): + collide_rect.w = ellipse_w + rect.w = ellipse_w + + # Calculate some variable positions. + off_left = -(ellipse_w + 1) + half_off_left = -(ellipse_w // 2) + half_off_right = surf_w - (ellipse_w // 2) + + # Draw the ellipse in different positions: fully on-surface, + # partially off-surface, and fully off-surface. + positions = ( + (off_left, off_top), + (half_off_left, off_top), + (center_x, off_top), + (half_off_right, off_top), + (off_right, off_top), + (off_left, center_y), + (half_off_left, center_y), + (center_x, center_y), + (half_off_right, center_y), + (off_right, center_y), + (off_left, off_bottom), + (half_off_left, off_bottom), + (center_x, off_bottom), + (half_off_right, off_bottom), + (off_right, off_bottom), + ) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + collide_rect.topleft = rect_pos + + self.draw_aaellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_aaellipse( + surface, collide_rect, surface_color, ellipse_color + ) + + def test_aaellipse__1_pixel_height_spanning_surface(self): + """Ensures an ellipse with a height of 1 is drawn correctly + when spanning the width of the surface. + + An ellipse with a height of 1 pixel is a horizontal line. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 20, 10 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (surf_w + 2, 1)) # Wider than the surface. + + # Draw the ellipse in different positions: on-surface and off-surface. + positions = ( + (-1, -1), # (off_left, off_top) + (-1, 0), # (off_left, top_edge) + (-1, surf_h // 2), # (off_left, center_y) + (-1, surf_h - 1), # (off_left, bottom_edge) + (-1, surf_h), + ) # (off_left, off_bottom) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + + self.draw_aaellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_aaellipse( + surface, rect, surface_color, ellipse_color + ) + + def test_aaellipse__1_pixel_width_and_height(self): + """Ensures an ellipse with a width and height of 1 is drawn correctly. + + An ellipse with a width and height of 1 pixel is a single pixel. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 10, 10 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (1, 1)) + + # Calculate some positions. + off_left = -1 + off_right = surf_w + off_top = -1 + off_bottom = surf_h + left_edge = 0 + right_edge = surf_w - 1 + top_edge = 0 + bottom_edge = surf_h - 1 + center_x = surf_w // 2 + center_y = surf_h // 2 + + # Draw the ellipse in different positions: center surface, + # top/bottom/left/right edges, and off-surface. + positions = ( + (off_left, off_top), + (off_left, top_edge), + (off_left, center_y), + (off_left, bottom_edge), + (off_left, off_bottom), + (left_edge, off_top), + (left_edge, top_edge), + (left_edge, center_y), + (left_edge, bottom_edge), + (left_edge, off_bottom), + (center_x, off_top), + (center_x, top_edge), + (center_x, center_y), + (center_x, bottom_edge), + (center_x, off_bottom), + (right_edge, off_top), + (right_edge, top_edge), + (right_edge, center_y), + (right_edge, bottom_edge), + (right_edge, off_bottom), + (off_right, off_top), + (off_right, top_edge), + (off_right, center_y), + (off_right, bottom_edge), + (off_right, off_bottom), + ) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + + self.draw_aaellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_aaellipse( + surface, rect, surface_color, ellipse_color + ) + + def test_aaellipse__bounding_rect(self): + """Ensures draw aaellipse returns the correct bounding rect. + + Tests ellipses on and off the surface and a range of width/thickness + values. + """ + ellipse_color = pygame.Color("red") + surf_color = pygame.Color("black") + min_width = min_height = 5 + max_width = max_height = 7 + sizes = ((min_width, min_height), (max_width, max_height)) + surface = pygame.Surface((20, 20), 0, 32) + surf_rect = surface.get_rect() + # Make a rect that is bigger than the surface to help test drawing + # ellipses off and partially off the surface. + big_rect = surf_rect.inflate(min_width * 2 + 1, min_height * 2 + 1) + + for pos in rect_corners_mids_and_center( + surf_rect + ) + rect_corners_mids_and_center(big_rect): + # Each of the ellipse's rect position attributes will be set to + # the pos value. + for attr in RECT_POSITION_ATTRIBUTES: + # Test using different rect sizes and thickness values. + for width, height in sizes: + ellipse_rect = pygame.Rect((0, 0), (width, height)) + setattr(ellipse_rect, attr, pos) + + for thickness in (0, 1, 2, 3, min(width, height)): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_aaellipse( + surface, ellipse_color, ellipse_rect, thickness + ) + + # Calculating the expected_rect after the ellipse + # is drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect( + surface, surf_color, ellipse_rect.topleft + ) + + self.assertEqual(bounding_rect, expected_rect) + + +class DrawAAEllipseTest(DrawAAEllipseMixin, DrawTestCase): + """Test draw module function ellipse. + + This class inherits the general tests from DrawAAEllipseMixin. It is also + the class to add any draw.aaellipse specific tests to. + """ + + ### Line/Lines/AALine/AALines Testing ######################################### @@ -7306,6 +7962,7 @@ def test_color_validation(self): draw.lines(surf, col, True, points) draw.arc(surf, col, pygame.Rect(0, 0, 3, 3), 15, 150) draw.ellipse(surf, col, pygame.Rect(0, 0, 3, 6), 1) + draw.aaellipse(surf, col, pygame.Rect(0, 0, 3, 6), 1) draw.circle(surf, col, (7, 3), 2) draw.aacircle(surf, col, (7, 3), 2) draw.polygon(surf, col, points, 0) @@ -7330,6 +7987,9 @@ def test_color_validation(self): with self.assertRaises(TypeError): draw.ellipse(surf, col, pygame.Rect(0, 0, 3, 6), 1) + with self.assertRaises(TypeError): + draw.aaellipse(surf, col, pygame.Rect(0, 0, 3, 6), 1) + with self.assertRaises(TypeError): draw.circle(surf, col, (7, 3), 2)