Created
April 9, 2025 03:21
-
-
Save chausen/4e7b52918abd464aeb314b390409363f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.awt.*; | |
import java.awt.image.BufferedImage; | |
import java.util.List; | |
import java.util.HashSet; | |
import java.util.Set; | |
public class CrosshatchMultipleShapesExample { | |
public static class Region { | |
// In your case, each Region might correspond to | |
// "all shapes that were previously one color." | |
// It can contain multiple disconnected shapes. | |
private Set<Point> pixelCoordinates; | |
private float density; // 0.0 = very sparse, 1.0 = very dense | |
public Region(Set<Point> pixelCoordinates, float density) { | |
this.pixelCoordinates = pixelCoordinates; | |
this.density = density; | |
} | |
public Set<Point> getPixelCoordinates() { | |
return pixelCoordinates; | |
} | |
public float getDensity() { | |
return density; | |
} | |
} | |
public static void main(String[] args) { | |
int width = 600; | |
int height = 400; | |
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); | |
Graphics2D g = image.createGraphics(); | |
try { | |
// White background | |
g.setColor(Color.WHITE); | |
g.fillRect(0, 0, width, height); | |
// Example region #1 (multiple disconnected shapes) | |
// We'll imagine that these points form two separate squares or blobs, | |
// but conceptually they share the same color (same crosshatch density). | |
Set<Point> region1Points = new HashSet<>(); | |
// Shape A | |
for(int y=50; y<60; y++){ | |
for(int x=50; x<60; x++){ | |
region1Points.add(new Point(x, y)); | |
} | |
} | |
// Shape B (disconnected from Shape A) | |
for(int y=80; y<85; y++){ | |
for(int x=200; x<205; x++){ | |
region1Points.add(new Point(x, y)); | |
} | |
} | |
Region region1 = new Region(region1Points, 0.4f); // moderate crosshatching | |
// Example region #2 (single shape, for contrast) | |
Set<Point> region2Points = new HashSet<>(); | |
// Just a small rectangle in a different part of the image | |
for(int y=150; y<180; y++){ | |
for(int x=100; x<120; x++){ | |
region2Points.add(new Point(x, y)); | |
} | |
} | |
Region region2 = new Region(region2Points, 0.8f); // quite dense | |
// Let's gather these in a list. | |
// Each region might contain multiple disconnected shapes. | |
List<Region> regions = List.of(region1, region2); | |
// We use the *same* color for all crosshatching | |
Color hatchColor = Color.BLACK; | |
for (Region region : regions) { | |
drawCrossHatch(g, region, hatchColor); | |
} | |
// Save or display your image ... | |
// ImageIO.write(image, "PNG", new File("output.png")); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} finally { | |
g.dispose(); | |
} | |
} | |
/** | |
* Draws diagonal crosshatches in the given Region. | |
* Even if the region has multiple disconnected shapes, | |
* we only draw within the region’s pixel set. | |
*/ | |
private static void drawCrossHatch(Graphics2D g, Region region, Color hatchColor) { | |
float density = region.getDensity(); | |
Set<Point> pixels = region.getPixelCoordinates(); | |
// If there's nothing to draw, just return | |
if (pixels.isEmpty()) return; | |
// Convert density to spacing (smaller spacing => higher density) | |
int minSpacing = 3; | |
int maxSpacing = 20; | |
int spacing = (int) ((1 - density) * (maxSpacing - minSpacing) + minSpacing); | |
// Get bounding rectangle (covers all disconnected shapes in the region) | |
Rectangle bounds = getBounds(pixels); | |
// Set color | |
g.setColor(hatchColor); | |
// Sweep one diagonal direction ("/") | |
// We'll shift horizontally across the bounding box in steps of `spacing`. | |
// At each shift, we'll traverse diagonally (x++, y--) and draw line segments | |
// *only* in the region’s pixel set. | |
for (int startY = bounds.y; startY <= bounds.y + bounds.height; startY += spacing) { | |
drawOneDiagonalLine(g, pixels, startY, bounds, +1); | |
} | |
for (int startX = bounds.x; startX <= bounds.x + bounds.width; startX += spacing) { | |
// For lines that start along the top edge | |
// startY = bounds.y + bounds.height | |
// We go upward from there | |
drawOneDiagonalLine(g, pixels, startX, bounds, +1); | |
} | |
// Sweep the other diagonal direction ("\") | |
// We'll do a similar approach, but now direction is -1 for x changes. | |
for (int startY = bounds.y; startY <= bounds.y + bounds.height; startY += spacing) { | |
drawOneDiagonalLine(g, pixels, startY, bounds, -1); | |
} | |
for (int startX = bounds.x; startX <= bounds.x + bounds.width; startX += spacing) { | |
drawOneDiagonalLine(g, pixels, startX, bounds, -1); | |
} | |
} | |
/** | |
* Draws a diagonal line in either "/" or "\" direction across the region, | |
* in steps of 1 pixel. We only draw line segments within 'pixels'. | |
* | |
* @param offset This is either the y-start or the x-start of the diagonal line, | |
* depending on how you call it. | |
* @param direction +1 => slope of -1 ("/"), -1 => slope of +1 ("\") | |
*/ | |
private static void drawOneDiagonalLine(Graphics2D g, | |
Set<Point> pixels, | |
int offset, | |
Rectangle bounds, | |
int direction) { | |
// We'll track consecutive points that belong to the region and draw them as a segment | |
// If we hit a gap, we end the segment and skip forward. | |
// A convenient way is to parameterize the diagonal in terms of x or y. | |
// For "/": as x increases, y decreases => y = (startY) - (x - startX). | |
// For "\": as x increases, y increases => y = (startY) + (x - startX). | |
// We'll do two loops: one for the "vertical start" and one for the "horizontal start". | |
// That's why 'offset' can represent either a y-start or an x-start. | |
Point previous = null; | |
// We’ll gather line segments, so we keep track of the start of a run. | |
Point runStart = null; | |
// We'll go at most the diagonal length of the bounding box in the worst case | |
int maxDim = bounds.width + bounds.height + 2; | |
// The logic below tries to unify the iteration for both vertical/horizontal starts: | |
// For a “/” diagonal (direction=+1), we’re effectively going from left to right | |
// while y decreases. For a “\” diagonal (direction=-1), we go from left to right | |
// while y increases. We can unify them by choosing how we interpret (offset). | |
for (int step = 0; step < maxDim; step++) { | |
// We'll compute x and y based on the line direction and offset | |
// - If offset <= bounds.width, treat offset as startX, otherwise treat offset as startY | |
// But let's keep it simpler by doing two separate calls from drawCrossHatch. | |
// One approach is to see if offset < bounds.y or something. | |
// A simpler approach: when we call drawOneDiagonalLine for a vertical start, we pass | |
// offset as the y, and x = bounds.x or bounds.x + ... | |
// Then for a horizontal start, we pass offset as the x, and y = ... | |
// For each, we do a separate for loop in drawCrossHatch. | |
// In practice, let's define: | |
// if offset is within the y-range, we consider offset = startY, x = bounds.x | |
// else offset is within the x-range, we consider offset = startX, y = bounds.y + bounds.height | |
// But since we call them separately, we can interpret 'offset' carefully: | |
// We'll check whether offset is within the vertical range or the horizontal range | |
boolean verticalStart = (offset >= bounds.y && offset <= bounds.y + bounds.height); | |
int x, y; | |
if (verticalStart) { | |
// offset => y | |
y = offset; | |
// step => how far we move to the right | |
x = bounds.x + step; | |
// for direction=+1 => y goes up (minus step) | |
// for direction=-1 => y goes down (plus step) | |
if (direction == +1) { | |
y = offset - step; | |
} else { | |
y = offset + step; | |
} | |
} else { | |
// offset => x | |
x = offset; | |
// step => how far we move vertically | |
// for "/" => y = (some top value) - step | |
// for "\" => y = (some top value) + step | |
// Let’s define a reference y. For "/" we can start from bottom, for "\" from top | |
// but let's keep it consistent: | |
if (direction == +1) { | |
// We'll start y from the bottom edge of the bounding rect | |
// i.e. y = bounds.y + bounds.height | |
y = (bounds.y + bounds.height) - step; | |
} else { | |
// For "\" we start from the top | |
y = bounds.y + step; | |
} | |
} | |
// Check if (x,y) is still in the bounding box | |
if (x < bounds.x || x > bounds.x + bounds.width | |
|| y < bounds.y || y > bounds.y + bounds.height) { | |
// If we’re out of bounds, we might be done or at least done with a segment | |
if (runStart != null) { | |
// finalize the last run | |
g.drawLine(runStart.x, runStart.y, previous.x, previous.y); | |
runStart = null; | |
} | |
break; | |
} | |
// Check membership in the region’s pixel set | |
Point current = new Point(x, y); | |
if (pixels.contains(current)) { | |
// We’re in the region | |
if (runStart == null) { | |
// start a new run | |
runStart = current; | |
} | |
} else { | |
// Not in the region | |
if (runStart != null) { | |
// finalize the run | |
g.drawLine(runStart.x, runStart.y, previous.x, previous.y); | |
runStart = null; | |
} | |
} | |
previous = current; | |
} | |
// If we finish the loop and still have an open run, finalize it | |
if (runStart != null && previous != null && pixels.contains(previous)) { | |
g.drawLine(runStart.x, runStart.y, previous.x, previous.y); | |
} | |
} | |
/** | |
* Returns the bounding rectangle for all the points in the set. | |
*/ | |
private static Rectangle getBounds(Set<Point> points) { | |
if (points.isEmpty()) return new Rectangle(0, 0, 0, 0); | |
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE; | |
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE; | |
for (Point p : points) { | |
if (p.x < minX) minX = p.x; | |
if (p.x > maxX) maxX = p.x; | |
if (p.y < minY) minY = p.y; | |
if (p.y > maxY) maxY = p.y; | |
} | |
return new Rectangle(minX, minY, (maxX - minX), (maxY - minY)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment