diff --git a/src/main/java/com/github/creme332/algorithms/EllipseAlgorithm.java b/src/main/java/com/github/creme332/algorithms/EllipseAlgorithm.java new file mode 100644 index 00000000..6d2ce71e --- /dev/null +++ b/src/main/java/com/github/creme332/algorithms/EllipseAlgorithm.java @@ -0,0 +1,103 @@ +package com.github.creme332.algorithms; + +import java.util.ArrayList; +import java.util.List; + +public class EllipseAlgorithm { + + private EllipseAlgorithm() { + + } + + /** + * This method calculates the pixel coordinates of an ellipse using the Midpoint + * Ellipse Algorithm. + * + * @param centerX X-coordinate of the ellipse's center + * @param centerY Y-coordinate of the ellipse's center + * @param rx Radius of the ellipse along the X-axis (horizontal radius) + * @param ry Radius of the ellipse along the Y-axis (vertical radius) + * @return A list of integer arrays representing the x-y coordinates of each + * pixel of the ellipse + * @throws IllegalArgumentException if rx or ry are non-positive + */ + public static List drawEllipse(int centerX, int centerY, int rx, int ry) { + if (rx <= 0 || ry <= 0) { + throw new IllegalArgumentException("Radii must be positive values."); + } + + List pixels = new ArrayList<>(); + + int x = 0; + int y = ry; + + int rx2 = rx * rx; + int ry2 = ry * ry; + int tworx2 = 2 * rx2; + int twory2 = 2 * ry2; + + int p = (int) (ry2 - (rx2 * ry) + (0.25 * rx2)); // Initial decision parameter + + int px = 0; + int py = tworx2 * y; + + // Region 1 + while (px < py) { + addPixels(pixels, centerX, centerY, x, y); + x++; + px += twory2; + if (p < 0) { + p += ry2 + px; + } else { + y--; + py -= tworx2; + p += ry2 + px - py; + } + } + + // Region 2 + p = (int) (ry2 * (x + 0.5) * (x + 0.5) + rx2 * (y - 1) * (y - 1) - rx2 * ry2); + while (y >= 0) { + addPixels(pixels, centerX, centerY, x, y); + y--; + py -= tworx2; + if (p > 0) { + p += rx2 - py; + } else { + x++; + px += twory2; + p += rx2 - py + px; + } + } + return pixels; + } + + private static void addPixels(List pixels, int centerX, int centerY, int x, int y) { + + // when x = 0, (-x, y)= (x, y) and (-x, -y) = (x, -y) + // plot only pixels in 2 quadrants + if (x == 0) { + addPixel(pixels, centerX + x, centerY + y); + addPixel(pixels, centerX + x, centerY - y); + return; + } + + // when y = 0, (x, -y)= (x, y) and (-x, y) = (-x, y) + // plot only pixels in 2 quadrants + if (y == 0) { + addPixel(pixels, centerX + x, centerY + y); + addPixel(pixels, centerX - x, centerY + y); + return; + } + + // else plot a pixel in each quadrant + addPixel(pixels, centerX + x, centerY + y); + addPixel(pixels, centerX + x, centerY - y); + addPixel(pixels, centerX - x, centerY + y); + addPixel(pixels, centerX - x, centerY - y); + } + + private static void addPixel(List pixels, int x, int y) { + pixels.add(new int[] { x, y }); + } +} diff --git a/src/test/java/com/github/creme332/tests/algorithms/EllipseAlgorithmTest.java b/src/test/java/com/github/creme332/tests/algorithms/EllipseAlgorithmTest.java new file mode 100644 index 00000000..9cceb404 --- /dev/null +++ b/src/test/java/com/github/creme332/tests/algorithms/EllipseAlgorithmTest.java @@ -0,0 +1,115 @@ +package com.github.creme332.tests.algorithms; + +import org.junit.Test; +import static org.junit.Assert.*; +import com.github.creme332.algorithms.EllipseAlgorithm; +import com.github.creme332.tests.utils.TestHelper; + +import java.util.List; + +public class EllipseAlgorithmTest { + + @Test + public void testEllipseCenteredAtOrigin() { + List pixels = EllipseAlgorithm.drawEllipse(0, 0, 3, 2); + + int[][] expectedArray = { + // Quadrant 1 + { 0, 2 }, { 1, 2 }, { 2, 1 }, + // Quadrant 2 + { 3, 0 }, { 2, -1 }, { 1, -2 }, + // Quadrant 3 + { 0, -2 }, { -1, -2 }, { -2, -1 }, + // Quadrant 4 + { -3, 0 }, { -2, 1 }, { -1, 2 } + }; + + TestHelper.assert2DArrayEquals(expectedArray, pixels.toArray(new int[pixels.size()][])); + } + + @Test + public void testEllipseNotAtOrigin() { + int centerX = 5; + int centerY = 7; + + // coordinates if ellipse with rx = 8 and ry = 6 was centered at origin + int[][] expectedArray = { + // Quadrant 1 + { 0, 6 }, { 1, 6 }, { 2, 6 }, { 3, 6 }, { 4, 5 }, { 5, 5 }, { 6, 4 }, { 7, 3 }, { 8, 2 }, { 8, 1 }, + { 8, 0 }, + // Quadrant 2 + { 8, -1 }, { 8, -2 }, { 7, -3 }, { 6, -4 }, { 5, -5 }, { 4, -5 }, { 3, -6 }, { 2, -6 }, { 1, -6 }, + { 0, -6 }, + // Quadrant 3 + { -1, -6 }, { -2, -6 }, { -3, -6 }, { -4, -5 }, { -5, -5 }, { -6, -4 }, { -7, -3 }, { -8, -2 }, + { -8, -1 }, { -8, 0 }, + // Quadrant 4 + { -8, 1 }, { -8, 2 }, { -7, 3 }, { -6, 4 }, { -5, 5 }, { -4, 5 }, { -3, 6 }, { -2, 6 }, { -1, 6 } + }; + // perform translation + for (int i = 0; i < expectedArray.length; i++) { + expectedArray[i][0] += centerX; + expectedArray[i][1] += centerY; + } + + List pixels = EllipseAlgorithm.drawEllipse(centerX, centerY, 8, 6); + TestHelper.assert2DArrayEquals(expectedArray, pixels.toArray(new int[pixels.size()][])); + } + + @Test + public void testZeroRadii() { + try { + EllipseAlgorithm.drawEllipse(10, 10, 0, 0); + fail("Expected IllegalArgumentException for zero radii"); + } catch (IllegalArgumentException e) { + assertEquals("Radii must be positive values.", e.getMessage()); + } + } + + @Test + public void testNegativeRadii() { + try { + EllipseAlgorithm.drawEllipse(10, 10, -5, -3); + fail("Expected IllegalArgumentException for negative radii"); + } catch (IllegalArgumentException e) { + assertEquals("Radii must be positive values.", e.getMessage()); + } + } + + @Test + public void testHorizontalEllipseAtOrigin() { + List pixels = EllipseAlgorithm.drawEllipse(0, 0, 7, 3); + + int[][] expectedArray = { + // Quadrant 1 + { 0, 3 }, { 1, 3 }, { 2, 3 }, { 3, 3 }, { 4, 2 }, { 5, 2 }, { 6, 2 }, { 7, 1 }, + // Quadrant 2 + { 7, 0 }, { 7, -1 }, { 6, -2 }, { 5, -2 }, { 4, -2 }, { 3, -3 }, { 2, -3 }, { 1, -3 }, { 0, -3 }, + // Quadrant 3 + { -1, -3 }, { -2, -3 }, { -3, -3 }, { -4, -2 }, { -5, -2 }, { -6, -2 }, { -7, -1 }, + // Quadrant 4 + { -7, 0 }, { -7, 1 }, { -6, 2 }, { -5, 2 }, { -4, 2 }, { -3, 3 }, { -2, 3 }, { -1, 3 } + }; + + TestHelper.assert2DArrayEquals(expectedArray, pixels.toArray(new int[pixels.size()][])); + } + + @Test + public void testVerticalEllipseAtOrigin() { + List pixels = EllipseAlgorithm.drawEllipse(0, 0, 3, 7); + + int[][] expectedArray = { + // Quadrant 1 + { 0, 7 }, { 1, 7 }, { 2, 6 }, { 2, 5 }, { 2, 4 }, { 3, 3 }, { 3, 2 }, { 3, 1 }, { 3, 0 }, + // Quadrant 2 + { 3, -1 }, { 3, -2 }, { 3, -3 }, { 2, -4 }, { 2, -5 }, { 2, -6 }, { 1, -7 }, { 0, -7 }, + // Quadrant 3 + { -1, -7 }, { -2, -6 }, { -2, -5 }, { -2, -4 }, { -3, -3 }, { -3, -2 }, { -3, -1 }, { -3, 0 }, + // Quadrant 4 + { -3, 1 }, { -3, 2 }, { -3, 3 }, { -2, 4 }, { -2, 5 }, { -2, 6 }, { -1, 7 } + }; + + TestHelper.assert2DArrayEquals(expectedArray, pixels.toArray(new int[pixels.size()][])); + } + +}