Skip to content

Instantly share code, notes, and snippets.

@chausen
Created April 9, 2025 03:21
Show Gist options
  • Save chausen/4e7b52918abd464aeb314b390409363f to your computer and use it in GitHub Desktop.
Save chausen/4e7b52918abd464aeb314b390409363f to your computer and use it in GitHub Desktop.
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