mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-12-19 07:00:35 +08:00
feat: add wu's line drawing algorithm (#6695)
* feat: add wu's line drawing algorithm * refactor: reorganize internal class declaration --------- Co-authored-by: a <alexanderklmn@gmail.com>
This commit is contained in:
235
src/main/java/com/thealgorithms/geometry/WusLine.java
Normal file
235
src/main/java/com/thealgorithms/geometry/WusLine.java
Normal file
@@ -0,0 +1,235 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @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<Pixel> drawLine(int x0, int y0, int x1, int y1) {
|
||||
List<Pixel> 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<Pixel> 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<Pixel> 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);
|
||||
}
|
||||
}
|
||||
90
src/test/java/com/thealgorithms/geometry/WusLineTest.java
Normal file
90
src/test/java/com/thealgorithms/geometry/WusLineTest.java
Normal file
@@ -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<WusLine.Pixel> pixels = WusLine.drawLine(2, 2, 6, 4);
|
||||
assertFalse(pixels.isEmpty(), "Line should produce non-empty pixel list");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEndpointsIncluded() {
|
||||
List<WusLine.Pixel> 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<WusLine.Pixel> 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<WusLine.Pixel> forward = WusLine.drawLine(2, 2, 10, 5);
|
||||
List<WusLine.Pixel> 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<WusLine.Pixel> 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<WusLine.Pixel> 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<WusLine.Pixel> 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<Integer, Double> 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user