From 610c7c68c410fa278362cf9181f6bd027199b13d Mon Sep 17 00:00:00 2001 From: saahilmahato Date: Tue, 7 Oct 2025 21:04:14 +0545 Subject: [PATCH 1/2] feat: add wu's line drawing algorithm --- .../com/thealgorithms/geometry/WusLine.java | 243 ++++++++++++++++++ .../thealgorithms/geometry/WusLineTest.java | 90 +++++++ 2 files changed, 333 insertions(+) create mode 100644 src/main/java/com/thealgorithms/geometry/WusLine.java create mode 100644 src/test/java/com/thealgorithms/geometry/WusLineTest.java diff --git a/src/main/java/com/thealgorithms/geometry/WusLine.java b/src/main/java/com/thealgorithms/geometry/WusLine.java new file mode 100644 index 000000000000..978fc28136cc --- /dev/null +++ b/src/main/java/com/thealgorithms/geometry/WusLine.java @@ -0,0 +1,243 @@ +package com.thealgorithms.geometry; + +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; + +/** + * The {@code WusLine} class implements Xiaolin Wu's line drawing algorithm, + * which produces anti-aliased lines by varying pixel brightness + * according to the line's proximity to pixel centers. + * + * This implementation returns the pixel coordinates along with + * their associated intensity values (in range [0.0, 1.0]), allowing + * rendering systems to blend accordingly. + * + * The algorithm works by: + * - Computing a line's intersection with pixel boundaries + * - Assigning intensity values based on distance from pixel centers + * - Drawing pairs of pixels perpendicular to the line's direction + * + * Reference: Xiaolin Wu, "An Efficient Antialiasing Technique", + * Computer Graphics (SIGGRAPH '91 Proceedings). + * + */ +public final class WusLine { + + private WusLine() { + // Utility class; prevent instantiation. + } + + /** + * Represents a pixel and its intensity for anti-aliased rendering. + * + * The intensity value determines how bright the pixel should be drawn, + * with 1.0 being fully opaque and 0.0 being fully transparent. + */ + public static class Pixel { + /** The pixel's coordinate on the screen. */ + public final Point point; + + /** The pixel's intensity value, clamped to the range [0.0, 1.0]. */ + public final double intensity; + + /** + * Constructs a new Pixel with the given coordinates and intensity. + * + * @param x the x-coordinate of the pixel + * @param y the y-coordinate of the pixel + * @param intensity the brightness/opacity of the pixel, will be clamped to [0.0, 1.0] + */ + public Pixel(int x, int y, double intensity) { + this.point = new Point(x, y); + this.intensity = Math.clamp(intensity, 0.0, 1.0); + } + } + + /** + * Draws an anti-aliased line using Wu's algorithm. + * + * The algorithm produces smooth lines by drawing pairs of pixels at each + * x-coordinate (or y-coordinate for steep lines), with intensities based on + * the line's distance from pixel centers. + * + * Example usage: + * {@code + * List linePixels = WusLine.drawLine(10, 20, 100, 80); + * for (Pixel p : linePixels) { + * drawPixel(p.point.x, p.point.y, p.intensity); + * } + * } + * + * @param x0 the x-coordinate of the line's start point + * @param y0 the y-coordinate of the line's start point + * @param x1 the x-coordinate of the line's end point + * @param y1 the y-coordinate of the line's end point + * @return a list of {@link Pixel} objects representing the anti-aliased line, + * ordered from start to end + */ + public static List drawLine(int x0, int y0, int x1, int y1) { + List pixels = new ArrayList<>(); + + // Determine if the line is steep (more vertical than horizontal) + boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0); + + if (steep) { + // For steep lines, swap x and y coordinates to iterate along y-axis + int temp = x0; + x0 = y0; + y0 = temp; + + temp = x1; + x1 = y1; + y1 = temp; + } + + if (x0 > x1) { + // Ensure we always draw from left to right + int temp = x0; + x0 = x1; + x1 = temp; + + temp = y0; + y0 = y1; + y1 = temp; + } + + // Calculate the line's slope + double deltaX = x1 - (double) x0; + double deltaY = y1 - (double) y0; + double gradient = (deltaX == 0) ? 1.0 : deltaY / deltaX; + + // Process the first endpoint + EndpointData firstEndpoint = processEndpoint(x0, y0, gradient, true); + addEndpointPixels(pixels, firstEndpoint, steep); + + // Process the second endpoint + EndpointData secondEndpoint = processEndpoint(x1, y1, gradient, false); + addEndpointPixels(pixels, secondEndpoint, steep); + + // Draw the main line between endpoints + drawMainLine(pixels, firstEndpoint, secondEndpoint, gradient, steep); + + return pixels; + } + + /** + * Processes a line endpoint to determine its pixel coordinates and intensities. + * + * @param x the x-coordinate of the endpoint + * @param y the y-coordinate of the endpoint + * @param gradient the slope of the line + * @param isStart true if this is the start endpoint, false if it's the end + * @return an {@link EndpointData} object containing processed endpoint information + */ + private static EndpointData processEndpoint(double x, double y, double gradient, boolean isStart) { + double xEnd = round(x); + double yEnd = y + gradient * (xEnd - x); + double xGap = isStart ? rfpart(x + 0.5) : fpart(x + 0.5); + + int xPixel = (int) xEnd; + int yPixel = (int) Math.floor(yEnd); + + return new EndpointData(xPixel, yPixel, yEnd, xGap); + } + + /** + * Adds the two endpoint pixels (one above, one below the line) to the pixel list. + * + * @param pixels the list to add pixels to + * @param endpoint the endpoint data containing coordinates and gaps + * @param steep true if the line is steep (coordinates should be swapped) + */ + private static void addEndpointPixels(List pixels, EndpointData endpoint, boolean steep) { + double fractionalY = fpart(endpoint.yEnd); + double complementFractionalY = rfpart(endpoint.yEnd); + + if (steep) { + pixels.add(new Pixel(endpoint.yPixel, endpoint.xPixel, complementFractionalY * endpoint.xGap)); + pixels.add(new Pixel(endpoint.yPixel + 1, endpoint.xPixel, fractionalY * endpoint.xGap)); + } else { + pixels.add(new Pixel(endpoint.xPixel, endpoint.yPixel, complementFractionalY * endpoint.xGap)); + pixels.add(new Pixel(endpoint.xPixel, endpoint.yPixel + 1, fractionalY * endpoint.xGap)); + } + } + + /** + * Draws the main portion of the line between the two endpoints. + * + * @param pixels the list to add pixels to + * @param firstEndpoint the processed start endpoint + * @param secondEndpoint the processed end endpoint + * @param gradient the slope of the line + * @param steep true if the line is steep (coordinates should be swapped) + */ + private static void drawMainLine(List pixels, EndpointData firstEndpoint, EndpointData secondEndpoint, double gradient, boolean steep) { + // Start y-intersection after the first endpoint + double intersectionY = firstEndpoint.yEnd + gradient; + + // Iterate through x-coordinates between the endpoints + for (int x = firstEndpoint.xPixel + 1; x < secondEndpoint.xPixel; x++) { + int yFloor = (int) Math.floor(intersectionY); + double fractionalPart = fpart(intersectionY); + double complementFractionalPart = rfpart(intersectionY); + + if (steep) { + pixels.add(new Pixel(yFloor, x, complementFractionalPart)); + pixels.add(new Pixel(yFloor + 1, x, fractionalPart)); + } else { + pixels.add(new Pixel(x, yFloor, complementFractionalPart)); + pixels.add(new Pixel(x, yFloor + 1, fractionalPart)); + } + + intersectionY += gradient; + } + } + + /** + * Returns the fractional part of a number. + * + * @param x the input number + * @return the fractional part (always in range [0.0, 1.0)) + */ + private static double fpart(double x) { + return x - Math.floor(x); + } + + /** + * Returns the reverse fractional part of a number (1 - fractional part). + * + * @param x the input number + * @return 1.0 minus the fractional part (always in range (0.0, 1.0]) + */ + private static double rfpart(double x) { + return 1.0 - fpart(x); + } + + /** + * Rounds a number to the nearest integer. + * + * @param x the input number + * @return the nearest integer value as a double + */ + private static double round(double x) { + return Math.floor(x + 0.5); + } + + /** + * Internal class to hold processed endpoint data. + */ + private static class EndpointData { + final int xPixel; + final int yPixel; + final double yEnd; + final double xGap; + + EndpointData(int xPixel, int yPixel, double yEnd, double xGap) { + this.xPixel = xPixel; + this.yPixel = yPixel; + this.yEnd = yEnd; + this.xGap = xGap; + } + } +} diff --git a/src/test/java/com/thealgorithms/geometry/WusLineTest.java b/src/test/java/com/thealgorithms/geometry/WusLineTest.java new file mode 100644 index 000000000000..0b7f36859e69 --- /dev/null +++ b/src/test/java/com/thealgorithms/geometry/WusLineTest.java @@ -0,0 +1,90 @@ +package com.thealgorithms.geometry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the {@link WusLine} class. + */ +class WusLineTest { + + @Test + void testSimpleLineProducesPixels() { + List pixels = WusLine.drawLine(2, 2, 6, 4); + assertFalse(pixels.isEmpty(), "Line should produce non-empty pixel list"); + } + + @Test + void testEndpointsIncluded() { + List pixels = WusLine.drawLine(0, 0, 5, 3); + boolean hasStart = pixels.stream().anyMatch(p -> p.point.equals(new java.awt.Point(0, 0))); + boolean hasEnd = pixels.stream().anyMatch(p -> p.point.equals(new java.awt.Point(5, 3))); + assertTrue(hasStart, "Start point should be represented in the pixel list"); + assertTrue(hasEnd, "End point should be represented in the pixel list"); + } + + @Test + void testIntensityInRange() { + List pixels = WusLine.drawLine(1, 1, 8, 5); + for (WusLine.Pixel pixel : pixels) { + assertTrue(pixel.intensity >= 0.0 && pixel.intensity <= 1.0, "Intensity must be clamped between 0.0 and 1.0"); + } + } + + @Test + void testReversedEndpointsProducesSameLine() { + List forward = WusLine.drawLine(2, 2, 10, 5); + List backward = WusLine.drawLine(10, 5, 2, 2); + + // They should cover same coordinates (ignoring order) + var forwardPoints = forward.stream().map(p -> p.point).collect(java.util.stream.Collectors.toSet()); + var backwardPoints = backward.stream().map(p -> p.point).collect(java.util.stream.Collectors.toSet()); + + assertEquals(forwardPoints, backwardPoints, "Reversing endpoints should yield same line pixels"); + } + + @Test + void testSteepLineHasProperCoverage() { + // Steep line: Δy > Δx + List pixels = WusLine.drawLine(3, 2, 5, 10); + assertFalse(pixels.isEmpty()); + // Expect increasing y values + long increasing = 0; + for (int i = 1; i < pixels.size(); i++) { + if (pixels.get(i).point.y >= pixels.get(i - 1).point.y) { + increasing++; + } + } + assertTrue(increasing > pixels.size() / 2, "Steep line should have increasing y coordinates"); + } + + @Test + void testZeroLengthLineUsesDefaultGradient() { + // same start and end -> dx == 0 -> gradient should take the (dx == 0) ? 1.0 branch + List pixels = WusLine.drawLine(3, 3, 3, 3); + + // sanity checks: we produced pixels and the exact point is present + assertFalse(pixels.isEmpty(), "Zero-length line should produce at least one pixel"); + assertTrue(pixels.stream().anyMatch(p -> p.point.equals(new java.awt.Point(3, 3))), "Pixel list should include the single-point coordinate (3,3)"); + } + + @Test + void testHorizontalLineIntensityStable() { + List pixels = WusLine.drawLine(1, 5, 8, 5); + + // For each x, take the max intensity among pixels with that x (the visible intensity for the column) + java.util.Map maxIntensityByX = pixels.stream() + .collect(java.util.stream.Collectors.groupingBy(p -> p.point.x, java.util.stream.Collectors.mapping(p -> p.intensity, java.util.stream.Collectors.maxBy(Double::compareTo)))) + .entrySet() + .stream() + .collect(java.util.stream.Collectors.toMap(java.util.Map.Entry::getKey, e -> e.getValue().orElse(0.0))); + + double avgMaxIntensity = maxIntensityByX.values().stream().mapToDouble(Double::doubleValue).average().orElse(0.0); + + assertTrue(avgMaxIntensity > 0.5, "Average of the maximum per-x intensities should be > 0.5 for a horizontal line"); + } +} From 9b836da91947d1afdf5bf6145b7386bf9f465790 Mon Sep 17 00:00:00 2001 From: saahilmahato Date: Tue, 7 Oct 2025 21:10:20 +0545 Subject: [PATCH 2/2] refactor: reorganize internal class declaration --- .../com/thealgorithms/geometry/WusLine.java | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/thealgorithms/geometry/WusLine.java b/src/main/java/com/thealgorithms/geometry/WusLine.java index 978fc28136cc..3539daaf6e5a 100644 --- a/src/main/java/com/thealgorithms/geometry/WusLine.java +++ b/src/main/java/com/thealgorithms/geometry/WusLine.java @@ -54,6 +54,23 @@ public Pixel(int x, int y, double intensity) { } } + /** + * Internal class to hold processed endpoint data. + */ + private static class EndpointData { + final int xPixel; + final int yPixel; + final double yEnd; + final double xGap; + + EndpointData(int xPixel, int yPixel, double yEnd, double xGap) { + this.xPixel = xPixel; + this.yPixel = yPixel; + this.yEnd = yEnd; + this.xGap = xGap; + } + } + /** * Draws an anti-aliased line using Wu's algorithm. * @@ -61,14 +78,6 @@ public Pixel(int x, int y, double intensity) { * x-coordinate (or y-coordinate for steep lines), with intensities based on * the line's distance from pixel centers. * - * Example usage: - * {@code - * List linePixels = WusLine.drawLine(10, 20, 100, 80); - * for (Pixel p : linePixels) { - * drawPixel(p.point.x, p.point.y, p.intensity); - * } - * } - * * @param x0 the x-coordinate of the line's start point * @param y0 the y-coordinate of the line's start point * @param x1 the x-coordinate of the line's end point @@ -223,21 +232,4 @@ private static double rfpart(double x) { private static double round(double x) { return Math.floor(x + 0.5); } - - /** - * Internal class to hold processed endpoint data. - */ - private static class EndpointData { - final int xPixel; - final int yPixel; - final double yEnd; - final double xGap; - - EndpointData(int xPixel, int yPixel, double yEnd, double xGap) { - this.xPixel = xPixel; - this.yPixel = yPixel; - this.yEnd = yEnd; - this.xGap = xGap; - } - } }