Draw Overlapping Path with Outline

My goal is to draw a path with both an outline and a fill color. Think about the way a figure-eight racetrack might look, or how a tangled-up wire might look. Something like this:

To accomplish this, I’ve tried simply using two BasicStrokes to draw a Path2D. The first BasicStroke is thicker and drawn in black, and the second is thinner and drawn in red. This almost accomplishes what I want, except the places on the curve that overlap itself are drawn as an intersection instead of an overlap:

Notice how if this was a racetrack, the cars would be able to turn anytime the track overlapped itself, when instead it should appear to be on top of itself with the black border of the top layer showing on top of the red fill of the bottom layer.

I’ve also tried using a custom DoubleStroke from this page, but the results are similar to the above problem.

Here’s my code so far:

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Defuse extends JPanel{

	Path2D.Double path = new Path2D.Double();

	public Defuse(){
		
		path.moveTo(0, 0);
		
		for(int i = 0; i < 5; i++){
			path.curveTo(Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500);
		}
	
		addMouseListener(new MouseAdapter(){
			
			
//			Point one = new Point(0, 0);
//			Point two = new Point(125, 250);
//			Point three = new Point(250, 500);
			
			@Override
			public void mouseClicked(MouseEvent e) {
				
//				if(e.getButton() == MouseEvent.BUTTON1){
//					one = e.getPoint();
//				}
//				else if(e.getButton() == MouseEvent.BUTTON2){
//					two = e.getPoint();
//				}
//				else if(e.getButton() == MouseEvent.BUTTON3){
//					three = e.getPoint();
//				}
//				
//				
				path = new Path2D.Double();
				path.moveTo(125, 0);
				
			//	path.curveTo(one.x, one.y, two.x, two.y, three.x, three.y);
				
				for(int i = 0; i < 5; i++){
					path.curveTo(Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500);
				}
				path.quadTo(path.getCurrentPoint().getX(), path.getCurrentPoint().getY(), 125, 500);
				
				repaint();
			}
		});
	}




	public void paintComponent(Graphics g){
		super.paintComponent(g);
		
		Graphics2D g2d = (Graphics2D)g;
		
		float thickness = 8;
		
		BasicStroke bs = new BasicStroke(6);
		
		g2d.setStroke(bs);
		g2d.setColor(Color.BLACK);
		g2d.draw(path);
		
		bs = new BasicStroke(thickness-4);
	
		g2d.setStroke(bs);
		g2d.setColor(Color.RED);
		g2d.draw(path);
		
		

	}


	public static void main(String... args){


		JFrame frame = new JFrame("Defuse");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.add(new Defuse());

		frame.setSize(250, 500);
		frame.setVisible(true);

	}
	
}

I’d also like to avoid sharp corners in my creation of the Path2D (cars can’t make 120 degree turns around a track), but that’s a separate problem for now. I appreciate any input you can give me!

Use a path iterator. Individual line segments and cubic splines will not intersect themselves. You draw in this order black-line, black-line, black-line, red-line, red-line, red-line, so red intersections are connected and cover entire overlapped area of the red over the black. If it got drawn in this order black-line, red-line, black-line, red-line, black-line, red-line, then it will appear as if later segments overlap older ones.

Thanks so much for the reply. I see what you’re saying about drawing it black-red black-red instead of black-black red-red. However, I tried to use a PathIterator, and I have several other problems instead. This is the code I came up with:

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Defuse extends JPanel{

	Path2D.Double path = new Path2D.Double();

	public Defuse(){

		path.moveTo(0, 0);

		for(int i = 0; i < 5; i++){
			path.curveTo(Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500);
		}

		addMouseListener(new MouseAdapter(){


			@Override
			public void mouseClicked(MouseEvent e) {
  
				path = new Path2D.Double();
				path.moveTo(125, 0);
				for(int i = 0; i < 5; i++){
					path.curveTo(Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500);
				}
				path.quadTo(path.getCurrentPoint().getX(), path.getCurrentPoint().getY(), 125, 500);

				repaint();
			}
		});
	}




	public void paintComponent(Graphics g){
		super.paintComponent(g);

		Graphics2D g2d = (Graphics2D)g;

		float thickness = 8;
		
		List<Point> points = new ArrayList<Point>();

		double[] coords = new double[6];
		for(PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next()){
			int type = pi.currentSegment(coords);
			double x = coords[0];
			double y = coords[1];

			points.add(new Point((int)x, (int)y));
		}

		for(int i = 1; i < points.size(); i++){

			Point prev = points.get(i-1);
			Point next = points.get(i);

			BasicStroke bs = new BasicStroke(thickness);
			g2d.setStroke(bs);
			g2d.setColor(Color.BLACK);
			g2d.drawLine(prev.x, prev.y, next.x, next.y);

			bs = new BasicStroke(thickness-4);
			g2d.setStroke(bs);
			g2d.setColor(Color.RED);
			g2d.drawLine(prev.x, prev.y, next.x, next.y);
		}
	}


	public static void main(String... args){

		JFrame frame = new JFrame("Defuse");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.add(new Defuse());

		frame.setSize(250, 500);
		frame.setVisible(true);

	}

}

And this is the result:

As you can see, my curves are now all straight lines, plus they’re a series of rectangles with the black outline showing on bends instead of a single line like in my crude paint image above. Am I missing something obvious?

A path iterator can return segments, cubic curves, or quadratic curves. You use drawLine only so things get drawn as line segments only. You could use a flattening path iterator to get just line segments or check the return type of next. The black line overlap probably could be fixed by drawing the current black line, the current red line, and the previous red line.

After some struggling, I came up with this:

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Defuse extends JPanel{

	Path2D.Double path = new Path2D.Double();
	Point2D.Double prevPoint = null;

	public Defuse(){

		path.moveTo(0, 0);

		for(int i = 0; i < 3; i++){
			path.curveTo(Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500);
		}

		addMouseListener(new MouseAdapter(){


			@Override
			public void mouseClicked(MouseEvent e) {

				path = new Path2D.Double();
				path.moveTo(125, 0);
				for(int i = 0; i < 2; i++){
					path.curveTo(Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500, Math.random()*250, Math.random()*500);
				}
				path.curveTo(path.getCurrentPoint().getX(), path.getCurrentPoint().getY(), Math.random()*250, Math.random()*500, 125, 500);

				repaint();
			}
		});
	}




	public void paintComponent(Graphics g){
		super.paintComponent(g);

		Graphics2D g2d = (Graphics2D)g;

		final float thickness = 8;


		double[] coords = new double[6];


		Painter previousPainter = null;

		for(PathIterator pi = path.getPathIterator(null); !pi.isDone(); pi.next()){
			int type = pi.currentSegment(coords);





			if(type == PathIterator.SEG_MOVETO){
				//one point
				System.out.println("Type: SEG_MOVETO");
				prevPoint = new Point2D.Double(coords[0], coords[1]);
			}
			else if(type == PathIterator.SEG_LINETO){
				//one point
				System.out.println("Type: SEG_LINETO");

				final Point2D.Double nextPoint = new Point2D.Double(coords[0], coords[1]);


				BasicStroke bs = new BasicStroke(thickness);
				g2d.setStroke(bs);
				g2d.setColor(Color.BLACK);
				g2d.drawLine((int)prevPoint.x, (int)prevPoint.y, (int)nextPoint.x, (int)nextPoint.y);

				if(previousPainter != null){
					previousPainter.paint(g2d);
				}

				previousPainter = new Painter(){
					public void paint(Graphics2D g2d){
						BasicStroke bs = new BasicStroke(thickness-4);
						g2d.setStroke(bs);
						g2d.setColor(Color.RED);
						g2d.drawLine((int)prevPoint.x, (int)prevPoint.y, (int)nextPoint.x, (int)nextPoint.y);
					}
				};

				previousPainter.paint(g2d);


				prevPoint = nextPoint;
			}
			else if(type == PathIterator.SEG_QUADTO){
				//two points

				System.out.println("Type: SEG_QUADTO");

				Point2D.Double currentPoint = new Point2D.Double(coords[0], coords[1]);
				Point2D.Double nextPoint = new Point2D.Double(coords[2], coords[3]);


				final Path2D.Double path = new Path2D.Double();
				path.moveTo(prevPoint.x, prevPoint.y);
				path.quadTo(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y);

				BasicStroke bs = new BasicStroke(thickness);
				g2d.setStroke(bs);
				g2d.setColor(Color.BLACK);
				g2d.draw(path);


				if(previousPainter != null){
					previousPainter.paint(g2d);
				}

				previousPainter = new Painter(){
					public void paint(Graphics2D g2d){
						BasicStroke bs = new BasicStroke(thickness-4);
						g2d.setStroke(bs);
						g2d.setColor(Color.RED);
						g2d.draw(path);
					}
				};

				previousPainter.paint(g2d);

				prevPoint = nextPoint;


			}
			else if(type == PathIterator.SEG_CUBICTO){
				//three points

				System.out.println("Type: SEG_CUBICTO");

				Point2D.Double middlePoint1 = new Point2D.Double(coords[0], coords[1]);
				Point2D.Double middlePoint2 = new Point2D.Double(coords[2], coords[3]);
				Point2D.Double nextPoint = new Point2D.Double(coords[4], coords[5]);


				final Path2D.Double path = new Path2D.Double();
				path.moveTo(prevPoint.x, prevPoint.y);
				path.curveTo(middlePoint1.x, middlePoint1.y, middlePoint2.x, middlePoint2.y, nextPoint.x, nextPoint.y);

				BasicStroke bs = new BasicStroke(thickness);
				g2d.setStroke(bs);
				g2d.setColor(Color.BLACK);
				g2d.draw(path);

				if(previousPainter != null){
					previousPainter.paint(g2d);
				}

				previousPainter = new Painter(){
					public void paint(Graphics2D g2d){
						BasicStroke bs = new BasicStroke(thickness-4);
						g2d.setStroke(bs);
						g2d.setColor(Color.RED);
						g2d.draw(path);
					}
				};
				
				previousPainter.paint(g2d);

				prevPoint = nextPoint;
			}
			else if(type == PathIterator.SEG_CLOSE){
				//no points
				System.out.println("Type: SEG_CLOSE");
			}
		}

	}

	private static interface Painter{
		public void paint(Graphics2D g2d);
	}


	public static void main(String... args){

		JFrame frame = new JFrame("Defuse");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.add(new Defuse());

		frame.setSize(250, 500);
		frame.setVisible(true);

	}

}

[quote]The black line overlap probably could be fixed by drawing the current black line, the current red line, and the previous red line.
[/quote]
And that almost works, except when the previous red line overlaps another curve that should be drawn on top of it. Here’s an example: (the curve is drawn from the top down)

The first curve comes from the top to the lower right, then the second one curves back towards the middle and over the first line. It then draws the previous curve, which draws the first curve over top of the second curves outline. A similar thing happens with the third curve and the second curve. You can see that the third curve does indeed go on top of the first one, but the other two cases are still a problem. Ack!

The way the Java2D rasterizer works makes it impossible to do what you’re trying to do.

I’d suggest preprocess the Path to split it when it intersects itself.
The other alternative is to rasterize the path yourself.

Alternatively you could add an extra dimension to your path, and render it in 3d (using the zbuffer to handle the overdraw).

Gah, that’s what I was beginning to be afraid of. Thanks for the reply though.

Using the z-axis makes a lot of sense, but I’m having trouble googling how to get started with that method. (Actually google led me to an explantion on z-order that I originally wrote to somebody else on the old sun forums, weird.) I don’t suppose you have a direction to point me in?

I don’t have any experience using paths, and this is for Ludum Dare, so the solution might be to just not worry about it. But I’d love to get this working, as I think it’ll really add to what I’m doing. Either way, thanks to both of you for the replies so far!

Sorry, I did not see that you were randomly generating your curve. If you don’t have control over the points, then you need to check if curves intersect themselves and other lines. I still assume this is just a graphical effect and not actually a race track because I would go in a completely different direction.

For generating the curve: To avoid bends you want to make sure that the last control point of the previous curve, the first control point of the current curve, and the end point shared between them are collinear. This is because Bezier curves are tangent to the line between the end point and the nearest control point at the start and end of the curve. A curve might still be too sharp, but there won’t be angled bends. A way to estimate how sharp a turn is would be to use three point Bezier curves would be to measure the angle between the three points. (I am pretty sure a four point with two identical control points is equivalent.)

You can get proper z ordering either by rendering pieces from back to front or using a z-buffer. I doubt I would use a z-buffer in my Java2D programs, but I think a custom composite class would be the way to do it.